@@ -31,151 +31,170 @@ const ProjectCard = ({
31
31
onClick : ( ) => void ;
32
32
} ) => {
33
33
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 }
47
95
className = "h-full w-full object-cover transition-transform duration-500 group-hover:opacity-90"
48
96
width = { 600 }
49
97
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 } ` }
75
98
/>
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 >
99
99
) }
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 && (
105
102
< 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 "
108
105
>
109
- { tag }
106
+ 주요 프로젝트
110
107
</ Badge >
111
- ) ) }
108
+ ) }
109
+ { project . inDevelopment && (
110
+ < Badge className = "bg-amber-500 text-white" variant = "default" >
111
+ 개발 중
112
+ </ Badge >
113
+ ) }
112
114
</ div >
113
- ) }
114
- </ motion . div >
115
- </ div >
116
115
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 >
121
134
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 >
125
138
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 }
131
149
</ 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 >
163
150
) }
164
151
</ div >
165
152
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 }
172
184
>
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 >
176
195
</ div >
177
196
</ div >
178
- </ div >
197
+ </ motion . div >
179
198
) ;
180
199
} ;
181
200
0 commit comments