Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions app/entities/common/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,14 @@ const Footer = () => {
});

if (response.data.success) {
toast.success(
response.data.message || '인증 이메일이 발송되었습니다.'
);
toast.success(response.data.message || '인증 이메일이 발송되었습니다.');
setIsSubmitted(true);
setNickname('');
setEmail('');
}
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
toast.error(
error.response.data.error || '구독 신청에 실패했습니다.'
);
toast.error(error.response.data.error || '구독 신청에 실패했습니다.');
} else {
toast.error('구독 신청 중 오류가 발생했습니다.');
}
Expand All @@ -71,8 +67,9 @@ const Footer = () => {
<div className={'footer-col'}>
<b>BLOG</b>
<div className={'text-weak'}>
<p className={'text-left font-serif whitespace-pre-wrap'}>
a developer who never stops growing.
<p className={'text-left text-sm font-serif whitespace-pre-wrap'}>
개발과 기술에 대한 이야기를 공유하는 공간입니다. <br />
문제 해결과 성장의 기록을 만듭니다.
</p>
<p className={'text-left font-serif whitespace-pre-wrap'}></p>
</div>
Expand Down
16 changes: 16 additions & 0 deletions app/entities/common/SectionHeading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
interface SectionHeadingProps {
title: string;
}

const SectionHeading = ({ title }: SectionHeadingProps) => {
return (
<div className="space-y-2">
<h2 className="text-xl md:text-2xl font-bold text-gray-900 dark:text-gray-100">
{title}
</h2>
<div className="h-1 w-24 bg-gray-900 dark:bg-gray-100 rounded-full"></div>
</div>
);
};

export default SectionHeading;
37 changes: 23 additions & 14 deletions app/entities/common/Toast/Toast.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,42 @@
import { CiCircleCheck, CiCircleRemove } from 'react-icons/ci';
import { CiCircleCheck, CiCircleRemove, CiMail } from 'react-icons/ci';

type ToastType = 'success' | 'error' | 'info';

interface ToastProps {
message: string;
type: 'success' | 'error';
title?: string;
type: ToastType;
removeToast: () => void;
}

const Toast = ({ message, type, removeToast }: ToastProps) => {
const iconRender = (type: 'success' | 'error') => {
if (type === 'success') {
return <CiCircleCheck color={'green'} size={40} />;
} else {
return <CiCircleRemove color={'red'} size={40} />;
}
};
const iconMap: Record<ToastType, JSX.Element> = {
success: <CiCircleCheck color={'green'} size={40} />,
error: <CiCircleRemove color={'red'} size={40} />,
info: <CiMail color={'#3b82f6'} size={40} />,
};

const Toast = ({ message, title, type, removeToast }: ToastProps) => {
return (
<div
onClick={() => removeToast()}
className={`
transform transition-all duration-300 ease-out animate-slideUp
bg-gray-200/90 text-black px-3 py-2 rounded-lg flex items-center gap-3
backdrop-blur-sm w-full max-w-md origin-center cursor-pointer
bg-gray-200/90 text-black px-3 py-2 rounded-lg flex items-center gap-3
backdrop-blur-sm w-full max-w-md origin-center cursor-pointer
hover:bg-gray-300/90 hover:shadow-lg
`}
>
<div className={`flex items-center justify-center rounded-full p-0.5`}>
{iconRender(type)}
{iconMap[type]}
</div>
<div className="flex-1 min-w-0">
{title && (
<p className="text font-semibold text-sm">{title}</p>
)}
<p className={`text whitespace-pre-line ${title ? 'text-xs text-gray-600 mt-0.5' : 'line-clamp-1'}`}>
{message}
</p>
</div>
<p className="text line-clamp-1 whitespace-pre-line flex-1">{message}</p>
</div>
);
};
Expand Down
4 changes: 3 additions & 1 deletion app/entities/common/Toast/ToastProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import useToastStore from '@/app/stores/useToastStore';
interface Toast {
id: number;
message: string;
type: 'success' | 'error';
title?: string;
type: 'success' | 'error' | 'info';
}

const ToastProvider = () => {
Expand All @@ -24,6 +25,7 @@ const ToastProvider = () => {
key={toast.id}
removeToast={() => removeToast(toast.id)}
message={toast.message}
title={toast.title}
type={toast.type}
/>
);
Expand Down
4 changes: 2 additions & 2 deletions app/entities/portfolio/Carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ const Carousel = ({ slides }: CarouselProps) => {
] as ReactElement,
{
hideTags: true,
hoverEffect: false,

}
)}
</div>
Expand All @@ -197,7 +197,7 @@ const Carousel = ({ slides }: CarouselProps) => {
] as ReactElement,
{
hideTags: true,
hoverEffect: false,

}
)}
</div>
Expand Down
206 changes: 49 additions & 157 deletions app/entities/portfolio/PortfolioPreview.tsx
Original file line number Diff line number Diff line change
@@ -1,178 +1,67 @@
'use client';
import Image from 'next/image';
import Link from 'next/link';
import { useState } from 'react';
import { FaGithub, FaGlobe } from 'react-icons/fa';
import { MdArrowForward } from 'react-icons/md';
import { Project } from '@/app/types/Portfolio';

interface PortfolioPreviewProps {
project: Project;
hideTags?: boolean;
hoverEffect?: boolean;
}

const PortfolioPreview = ({
project,
hideTags,
hoverEffect = true,
}: PortfolioPreviewProps) => {
const [isHovered, setIsHovered] = useState(false);
const [isTouched, setIsTouched] = useState(false);

const handleTouchStart = () => {
if (hoverEffect) {
setIsTouched(true);
}
};

const handleTouchEnd = () => {
if (hoverEffect) {
setTimeout(() => setIsTouched(false), 3000);
}
};

const showOverlay = hoverEffect && (isHovered || isTouched);

const PortfolioPreview = ({ project, hideTags }: PortfolioPreviewProps) => {
return (
<div
className="group bg-gradient-to-br from-gray-50 to-gray-100 dark:from-primary-rich rounded-2xl overflow-hidden shadow transition-all duration-300 border border-gray-200 dark:border-gray-700"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div
className="relative overflow-hidden"
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
>
<Image
width={500}
height={400}
src={project.image}
alt={`${project.title} 프로젝트 이미지`}
className={`w-full aspect-video object-cover transition-transform duration-500 ${
showOverlay ? 'scale-110' : 'scale-100'
}`}
/>

{/* 데스크톱 호버 오버레이 */}
<div
className={`absolute inset-0 bg-black bg-opacity-50 hidden md:flex items-center justify-center transition-opacity duration-300 ${
isHovered ? 'opacity-100' : 'opacity-0'
}`}
>
<div className="flex gap-3 ">
{project.demoUrl && (
<Link
href={project.demoUrl}
target="_blank"
rel="noopener noreferrer"
>
<button className="inline-flex items-center gap-2 bg-neutral-500 hover:bg-neutral-600 text-white px-4 py-2 rounded-md transition-colors text-sm ">
배포 <FaGlobe size={14} />
</button>
</Link>
)}
{project.githubUrl && (
<Link
href={project.githubUrl}
target="_blank"
rel="noopener noreferrer"
className={'h-full'}
>
<button className="inline-flex items-center gap-2 bg-gray-700 hover:bg-gray-800 text-white px-4 py-2 rounded-md transition-colors text-sm ">
<FaGithub size={14} /> Github
</button>
</Link>
)}
{project.slug && (
<Link
href={`/portfolio/${project.slug}`}
rel="noopener noreferrer"
>
<button className="bg-neutral-700 hover:bg-neutral-800 text-white px-4 py-2 rounded-md transition-colors text-sm">
세부 정보
</button>
</Link>
)}
</div>
</div>

{/* 모바일 터치 오버레이 */}
<div
className={`absolute inset-0 bg-black bg-opacity-50 flex md:hidden items-center justify-center transition-opacity duration-300 ${
isTouched ? 'opacity-100' : 'opacity-0'
}`}
>
<div className="flex flex-col gap-2 px-4 ">
{project.demoUrl && (
<Link
href={project.demoUrl}
target="_blank"
rel="noopener noreferrer"
>
<button className="inline-flex items-center justify-center gap-2 w-full bg-neutral-500 hover:bg-neutral-600 text-white px-4 py-3 rounded-md transition-colors text-sm">
배포 보기 <FaGlobe size={14} />
</button>
</Link>
)}
{project.githubUrl && (
<Link
href={project.githubUrl}
target="_blank"
rel="noopener noreferrer"
>
<button className="inline-flex items-center justify-center gap-2 w-full bg-gray-700 hover:bg-gray-800 text-white px-4 py-3 rounded-md transition-colors text-sm ">
GitHub <FaGithub size={14} />
</button>
</Link>
)}
{project.slug && (
<Link
href={`/portfolio/${project.slug}`}
rel="noopener noreferrer"
>
<button className="w-full bg-neutral-700 hover:bg-neutral-800 text-white px-4 py-3 rounded-md transition-colors text-sm">
세부 정보
</button>
</Link>
)}
</div>
</div>
</div>

<div className="p-6 md:p-8">
<div className="flex flex-col md:flex-row group bg-gradient-to-br from-gray-50 to-gray-100 dark:from-primary-rich rounded-2xl overflow-hidden shadow transition-all duration-300 border border-gray-200 dark:border-gray-700">
{/* 이미지 영역 */}
<div className="relative overflow-hidden w-full md:w-1/3 shrink-0">
<Link href={project.slug ? `/portfolio/${project.slug}` : '#'}>
<h3 className="text-xl md:text-2xl font-bold mb-3 text-gray-900 dark:text-gray-100 hover:text-gray-600 dark:hover:text-gray-400 transition-colors line-clamp-2">
{project.title}
</h3>
<Image
width={500}
height={400}
src={project.image}
alt={`${project.title} 프로젝트 이미지`}
className="w-full h-48 md:h-full object-cover transition-transform duration-500 group-hover:scale-105"
/>
</Link>
<p className="text-sm md:text-base text-gray-600 dark:text-gray-300 mb-4 md:mb-5 line-clamp-3 leading-relaxed">
{project.description}
</p>
</div>

{!hideTags && project.tags && project.tags.length > 0 && (
<div className="flex flex-wrap gap-2 mt-4">
{project.tags.map((tag, index) => (
<span
key={index}
className="text-xs px-3 py-1.5 bg-gray-200 dark:bg-gray-700 rounded-full text-gray-700 dark:text-gray-300 whitespace-nowrap font-medium"
>
{tag}
</span>
))}
</div>
)}
{/* 정보 + 버튼 영역 */}
<div className="flex flex-1 flex-col md:flex-row">
{/* 텍스트 정보 */}
<div className="flex-1 p-5 md:p-6">
<Link href={project.slug ? `/portfolio/${project.slug}` : '#'}>
<h3 className="text-lg md:text-xl font-bold mb-2 text-gray-900 dark:text-gray-100 hover:text-gray-600 dark:hover:text-gray-400 transition-colors line-clamp-2">
{project.title}
</h3>
</Link>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3 line-clamp-3 leading-relaxed">
{project.description}
</p>

{/* 모바일 하단 액션 버튼들 */}
<div className="flex md:hidden gap-2 mt-5 pt-4 border-t border-gray-200 dark:border-gray-700">
{!hideTags && project.tags && project.tags.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{project.tags.map((tag, index) => (
<span
key={index}
className="text-xs px-2.5 py-1 bg-gray-200 dark:bg-gray-700 rounded-full text-gray-700 dark:text-gray-300 whitespace-nowrap font-medium"
>
{tag}
</span>
))}
</div>
)}
</div>
<div className="flex md:flex-col gap-2 p-4 md:p-5 border-t md:border-t-0 md:border-l border-gray-200 dark:border-gray-700 justify-center items-stretch">
{project.demoUrl && (
<Link
href={project.demoUrl}
target="_blank"
rel="noopener noreferrer"
className="flex-1"
className="flex-1 md:flex-none"
title="배포 링크"
>
<button className="inline-flex items-center justify-center gap-1.5 w-full bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200 px-3 py-2.5 rounded-lg transition-colors text-xs font-medium">
<button className="inline-flex items-center justify-center gap-1.5 w-full bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200 px-3 py-2 rounded-lg transition-colors text-xs font-medium">
<FaGlobe size={12} />
배포
</button>
Expand All @@ -183,9 +72,10 @@ const PortfolioPreview = ({
href={project.githubUrl}
target="_blank"
rel="noopener noreferrer"
className="flex-1"
className="flex-1 md:flex-none"
title="GitHub 코드 보기"
>
<button className="inline-flex items-center justify-center gap-1.5 w-full bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200 px-3 py-2.5 rounded-lg transition-colors text-xs font-medium">
<button className="inline-flex items-center justify-center gap-1.5 w-full bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200 px-3 py-2 rounded-lg transition-colors text-xs font-medium">
<FaGithub size={12} />
코드
</button>
Expand All @@ -195,9 +85,11 @@ const PortfolioPreview = ({
<Link
href={`/portfolio/${project.slug}`}
rel="noopener noreferrer"
className="flex-1"
className="flex-1 md:flex-none"
title="자세히 보기"
>
<button className="w-full bg-gray-800 dark:bg-gray-200 hover:bg-gray-700 dark:hover:bg-gray-300 text-gray-100 dark:text-gray-900 px-3 py-2.5 rounded-lg transition-colors text-xs font-medium">
<button className="inline-flex items-center justify-center gap-1.5 w-full bg-gray-800 dark:bg-gray-200 hover:bg-gray-700 dark:hover:bg-gray-300 text-gray-100 dark:text-gray-900 px-3 py-2 rounded-lg transition-colors text-xs font-medium">
<MdArrowForward size={12} />
자세히
</button>
</Link>
Expand Down
Loading