Skip to content

Commit 2323c7e

Browse files
committed
Feat: 프로젝트 호버 에니메이션 추가
1 parent b6a9943 commit 2323c7e

File tree

1 file changed

+145
-126
lines changed

1 file changed

+145
-126
lines changed

app/projects/projects-component.tsx

Lines changed: 145 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -31,151 +31,170 @@ const ProjectCard = ({
3131
onClick: () => void;
3232
}) => {
3333
return (
34-
<div className="group overflow-hidden rounded-lg border border-border bg-card shadow-sm transition-all duration-300 hover:shadow-md h-full flex flex-col">
35-
<div className="aspect-video w-full overflow-hidden">
36-
<motion.div
37-
whileHover={{ scale: 1.05 }}
38-
transition={{ duration: 0.3 }}
39-
className="relative h-full w-full"
40-
>
41-
{project.thumbnailType === "video" ||
42-
isVideoFile(project.thumbnail) ||
43-
isGifFile(project.thumbnail) ? (
44-
<div className="h-full w-full">
45-
<video
46-
src={project.thumbnail}
34+
<motion.div
35+
className="h-full w-full"
36+
whileHover={{
37+
y: -5,
38+
boxShadow: "0 12px 24px rgba(0,0,0,0.1)",
39+
}}
40+
transition={{
41+
type: "spring",
42+
stiffness: 400,
43+
damping: 20,
44+
}}
45+
>
46+
<div className="group overflow-hidden rounded-lg border border-border bg-card shadow-sm transition-all duration-300 hover:shadow-md h-full flex flex-col">
47+
<div className="aspect-video w-full overflow-hidden">
48+
<motion.div
49+
whileHover={{ scale: 1.05 }}
50+
transition={{ duration: 0.3 }}
51+
className="relative h-full w-full"
52+
>
53+
{project.thumbnailType === "video" ||
54+
isVideoFile(project.thumbnail) ||
55+
isGifFile(project.thumbnail) ? (
56+
<div className="h-full w-full">
57+
<video
58+
src={project.thumbnail}
59+
className="h-full w-full object-cover transition-transform duration-500 group-hover:opacity-90"
60+
width={600}
61+
height={340}
62+
autoPlay={true}
63+
loop={true}
64+
muted={true}
65+
ref={(videoEl) => {
66+
if (videoEl) {
67+
// 재생 속도 설정
68+
// 명시적으로 숫자 타입으로 변환하여 타입 오류 방지
69+
const rate =
70+
project.thumbnailOptions &&
71+
"playbackRate" in project.thumbnailOptions &&
72+
typeof project.thumbnailOptions.playbackRate ===
73+
"number"
74+
? project.thumbnailOptions.playbackRate
75+
: undefined;
76+
if (rate) videoEl.playbackRate = rate;
77+
}
78+
}}
79+
playsInline
80+
preload="auto"
81+
controls={Boolean(
82+
project.thumbnailOptions &&
83+
"showControls" in project.thumbnailOptions &&
84+
project.thumbnailOptions.showControls === true,
85+
)}
86+
disablePictureInPicture={true}
87+
onError={(e) => console.error("썸네일 비디오 로딩 오류:", e)}
88+
key={`thumbnail-video-${project.id}`}
89+
/>
90+
</div>
91+
) : (
92+
<Image
93+
src={project.thumbnail || DEFAULT_IMAGE}
94+
alt={project.title}
4795
className="h-full w-full object-cover transition-transform duration-500 group-hover:opacity-90"
4896
width={600}
4997
height={340}
50-
autoPlay={true}
51-
loop={true}
52-
muted={true}
53-
ref={(videoEl) => {
54-
if (videoEl) {
55-
// 재생 속도 설정
56-
// 명시적으로 숫자 타입으로 변환하여 타입 오류 방지
57-
const rate = project.thumbnailOptions &&
58-
'playbackRate' in project.thumbnailOptions &&
59-
typeof project.thumbnailOptions.playbackRate === 'number'
60-
? project.thumbnailOptions.playbackRate
61-
: undefined;
62-
if (rate) videoEl.playbackRate = rate;
63-
}
64-
}}
65-
playsInline
66-
preload="auto"
67-
controls={Boolean(
68-
project.thumbnailOptions &&
69-
'showControls' in project.thumbnailOptions &&
70-
project.thumbnailOptions.showControls === true
71-
)}
72-
disablePictureInPicture={true}
73-
onError={(e) => console.error("썸네일 비디오 로딩 오류:", e)}
74-
key={`thumbnail-video-${project.id}`}
7598
/>
76-
</div>
77-
) : (
78-
<Image
79-
src={project.thumbnail || DEFAULT_IMAGE}
80-
alt={project.title}
81-
className="h-full w-full object-cover transition-transform duration-500 group-hover:opacity-90"
82-
width={600}
83-
height={340}
84-
/>
85-
)}
86-
<div className="absolute right-2 top-2 flex flex-col gap-1">
87-
{project.featured && (
88-
<Badge
89-
className="bg-primary text-primary-foreground"
90-
variant="default"
91-
>
92-
주요 프로젝트
93-
</Badge>
94-
)}
95-
{project.inDevelopment && (
96-
<Badge className="bg-amber-500 text-white" variant="default">
97-
개발 중
98-
</Badge>
9999
)}
100-
</div>
101-
102-
{project.tags && project.tags.length > 0 && (
103-
<div className="absolute bottom-0 left-0 right-0 flex flex-wrap gap-1 p-2 bg-gradient-to-t from-slate-900/30 to-transparent dark:from-black/60">
104-
{project.tags.map((tag, index) => (
100+
<div className="absolute right-2 top-2 flex flex-col gap-1">
101+
{project.featured && (
105102
<Badge
106-
key={`img-tag-${index}`}
107-
className="text-xs bg-primary/90 text-primary-foreground border-none shadow-sm hover:bg-primary/100 transition-colors"
103+
className="bg-primary text-primary-foreground"
104+
variant="default"
108105
>
109-
{tag}
106+
주요 프로젝트
110107
</Badge>
111-
))}
108+
)}
109+
{project.inDevelopment && (
110+
<Badge className="bg-amber-500 text-white" variant="default">
111+
개발 중
112+
</Badge>
113+
)}
112114
</div>
113-
)}
114-
</motion.div>
115-
</div>
116115

117-
<div className="p-4 sm:p-6 flex flex-col flex-grow">
118-
<h2 className="text-xl font-bold line-clamp-1 group-hover:text-primary transition-colors">
119-
{project.title}
120-
</h2>
116+
{project.tags && project.tags.length > 0 && (
117+
<div className="absolute bottom-0 left-0 right-0 flex flex-wrap gap-1 p-2 bg-gradient-to-t from-slate-900/30 to-transparent dark:from-black/60">
118+
{project.tags.map((tag, index) => (
119+
<Badge
120+
key={`img-tag-${index}`}
121+
className="text-xs bg-primary/90 text-primary-foreground border-none shadow-sm hover:bg-primary/100 transition-colors"
122+
>
123+
{tag}
124+
</Badge>
125+
))}
126+
</div>
127+
)}
128+
</motion.div>
129+
</div>
130+
<div className="p-4 sm:p-6 flex flex-col flex-grow">
131+
<h2 className="text-xl font-bold line-clamp-1 group-hover:text-primary transition-colors">
132+
{project.title}
133+
</h2>
121134

122-
<p className="mt-2 text-muted-foreground text-sm line-clamp-2">
123-
{project.description}
124-
</p>
135+
<p className="mt-2 text-muted-foreground text-sm line-clamp-2">
136+
{project.description}
137+
</p>
125138

126-
<div className="mt-4 flex flex-wrap gap-1">
127-
{project.technologies &&
128-
project.technologies.slice(0, 4).map((tech) => (
129-
<Badge variant="secondary" key={tech} className="text-xs">
130-
{tech}
139+
<div className="mt-4 flex flex-wrap gap-1">
140+
{project.technologies &&
141+
project.technologies.slice(0, 4).map((tech) => (
142+
<Badge variant="secondary" key={tech} className="text-xs">
143+
{tech}
144+
</Badge>
145+
))}
146+
{project.technologies && project.technologies.length > 4 && (
147+
<Badge variant="outline" className="text-xs">
148+
+{project.technologies.length - 4}
131149
</Badge>
132-
))}
133-
{project.technologies && project.technologies.length > 4 && (
134-
<Badge variant="outline" className="text-xs">
135-
+{project.technologies.length - 4}
136-
</Badge>
137-
)}
138-
</div>
139-
140-
<div className="mt-auto pt-4 flex justify-between items-center">
141-
<div className="flex space-x-2">
142-
{project.githubUrl && (
143-
<Link
144-
href={project.githubUrl}
145-
target="_blank"
146-
rel="noopener noreferrer"
147-
>
148-
<Button size="icon" variant="outline" title="GitHub 저장소">
149-
<Github className="h-4 w-4" />
150-
</Button>
151-
</Link>
152-
)}
153-
{(project.liveSiteUrl || project.serviceUrl) && (
154-
<Link
155-
href={project.liveSiteUrl || project.serviceUrl || ""}
156-
target="_blank"
157-
rel="noopener noreferrer"
158-
>
159-
<Button size="icon" variant="outline" title="서비스 링크">
160-
<ExternalLink className="h-4 w-4" />
161-
</Button>
162-
</Link>
163150
)}
164151
</div>
165152

166-
<Button variant="ghost" className="group" size="sm" onClick={onClick}>
167-
자세히 보기
168-
<motion.span
169-
className="inline-block ml-1"
170-
whileHover={{ x: 3 }}
171-
transition={{ duration: 0.2 }}
153+
<div className="mt-auto pt-4 flex justify-between items-center">
154+
<div className="flex space-x-2">
155+
{project.githubUrl && (
156+
<Link
157+
href={project.githubUrl}
158+
target="_blank"
159+
rel="noopener noreferrer"
160+
>
161+
<Button size="icon" variant="outline" title="GitHub 저장소">
162+
<Github className="h-4 w-4" />
163+
</Button>
164+
</Link>
165+
)}
166+
{(project.liveSiteUrl || project.serviceUrl) && (
167+
<Link
168+
href={project.liveSiteUrl || project.serviceUrl || ""}
169+
target="_blank"
170+
rel="noopener noreferrer"
171+
>
172+
<Button size="icon" variant="outline" title="서비스 링크">
173+
<ExternalLink className="h-4 w-4" />
174+
</Button>
175+
</Link>
176+
)}
177+
</div>
178+
179+
<Button
180+
variant="ghost"
181+
className="group"
182+
size="sm"
183+
onClick={onClick}
172184
>
173-
<ArrowRight className="h-4 w-4" />
174-
</motion.span>
175-
</Button>
185+
자세히 보기
186+
<motion.span
187+
className="inline-block ml-1"
188+
whileHover={{ x: 3 }}
189+
transition={{ duration: 0.2 }}
190+
>
191+
<ArrowRight className="h-4 w-4" />
192+
</motion.span>
193+
</Button>
194+
</div>
176195
</div>
177196
</div>
178-
</div>
197+
</motion.div>
179198
);
180199
};
181200

0 commit comments

Comments
 (0)