@@ -39,6 +39,8 @@ export default function PostDetail() {
3939 const navigate = useNavigate ( )
4040
4141 const [ postData , setPostData ] = useState < PostData | null > ( null )
42+ const [ isLoading , setIsLoading ] = useState ( true )
43+ const [ isNotFound , setIsNotFound ] = useState ( false )
4244 const [ currentIndex , setCurrentIndex ] = useState ( 0 )
4345 const [ showHeart , setShowHeart ] = useState ( false )
4446 const [ randomRotate , setRandomRotate ] = useState ( 0 )
@@ -48,16 +50,29 @@ export default function PostDetail() {
4850
4951 useEffect ( ( ) => {
5052 const fetchPost = async ( ) => {
53+ setIsLoading ( true )
54+ setIsNotFound ( false )
55+
56+ if ( ! / ^ \d + $ / . test ( postId ) ) {
57+ setIsNotFound ( true )
58+ setIsLoading ( false )
59+ return
60+ }
61+
5162 try {
5263 const res = await instance
5364 . get ( `api/v1/posts/${ postId } ` )
5465 . json < { data : PostData ; isSuccess : boolean } > ( )
5566
5667 if ( res . isSuccess ) {
5768 setPostData ( { ...res . data } )
69+ } else {
70+ setIsNotFound ( true )
5871 }
5972 } catch {
60- console . error ( 'Failed to fetch post' )
73+ setIsNotFound ( true )
74+ } finally {
75+ setIsLoading ( false )
6176 }
6277 }
6378 fetchPost ( )
@@ -76,7 +91,7 @@ export default function PostDetail() {
7691 search : returnToSearch ,
7792 } )
7893 } else {
79- navigate ( { to : '/' } )
94+ navigate ( { to : '/' , search : { page : 1 } } )
8095 }
8196 }
8297
@@ -132,108 +147,154 @@ export default function PostDetail() {
132147 className = "relative flex h-fit max-h-[90%] w-[95%] max-w-[1200px] overflow-hidden rounded-sm bg-white shadow-2xl"
133148 onClick = { ( e ) => e . stopPropagation ( ) }
134149 >
135- < div className = "relative hidden w-[60%] flex-col items-center justify-center bg-black md:flex" >
136- < div
137- className = "relative w-full overflow-hidden"
138- style = { { aspectRatio : '1 / 1' } }
139- >
140- < motion . div
141- className = "flex h-full w-full"
142- animate = { { x : `-${ currentIndex * 100 } %` } }
143- transition = { { type : 'spring' , stiffness : 260 , damping : 26 } }
144- onDoubleClick = { handleDoubleLike }
145- >
146- { images . map ( ( img ) => (
147- < div
148- key = { img . id }
149- className = "flex h-full min-w-full shrink-0 items-center justify-center"
150- >
151- < img
152- src = { img . url }
153- alt = ""
154- className = "h-full w-full object-cover select-none"
155- />
150+ { ( isLoading || isNotFound ) && (
151+ < >
152+ < div
153+ className = "relative hidden w-[60%] items-center justify-center bg-black md:flex"
154+ style = { { aspectRatio : '1 / 1' } }
155+ />
156+ < div className = "flex w-full flex-col items-center justify-center border-l border-gray-200 bg-white md:w-[40%]" >
157+ { isLoading && (
158+ < p className = "text-sm text-gray-500" > 게시물을 불러오는 중...</ p >
159+ ) }
160+ { isNotFound && (
161+ < div className = "flex flex-col items-center justify-center gap-4 p-8" >
162+ < p className = "text-lg font-semibold" >
163+ 게시물을 찾을 수 없습니다
164+ </ p >
165+ < p className = "text-center text-sm text-gray-500" >
166+ 삭제되었거나 잘못된 링크일 수 있습니다.
167+ </ p >
168+ < button
169+ type = "button"
170+ onClick = { handleClose }
171+ className = "mt-2 rounded-lg bg-blue-500 px-4 py-2 text-sm font-medium text-white hover:bg-blue-600"
172+ >
173+ 돌아가기
174+ </ button >
156175 </ div >
157- ) ) }
158- </ motion . div >
176+ ) }
177+ </ div >
178+ </ >
179+ ) }
159180
160- < AnimatePresence >
161- { showHeart && (
181+ { ! isLoading && ! isNotFound && (
182+ < >
183+ < div className = "relative hidden w-[60%] flex-col items-center justify-center bg-black md:flex" >
184+ < div
185+ className = "relative w-full overflow-hidden"
186+ style = { { aspectRatio : '1 / 1' } }
187+ >
162188 < motion . div
163- key = "rising-heart"
164- initial = { { scale : 0 , opacity : 1 , y : 0 , rotate : randomRotate } }
165- animate = { {
166- scale : [ 0 , 1.2 , 1 ] ,
167- y : - 20 ,
168- rotate : [ randomRotate , randomRotate , 0 ] ,
169- } }
170- exit = { {
171- y : - 700 ,
172- transition : { duration : 0.2 , ease : 'circIn' } ,
173- } }
174- transition = { {
175- duration : 0.7 ,
176- times : [ 0 , 0.57 , 1 ] ,
177- ease : 'easeInOut' ,
178- } }
179- onAnimationComplete = { ( ) => setIsAnimating ( false ) }
180- className = "pointer-events-none absolute inset-0 z-20 flex items-center justify-center"
189+ className = "flex h-full w-full"
190+ animate = { { x : `-${ currentIndex * 100 } %` } }
191+ transition = { { type : 'spring' , stiffness : 260 , damping : 26 } }
192+ onDoubleClick = { handleDoubleLike }
181193 >
182- < Heart
183- className = "h-32 w-32 drop-shadow-2xl"
184- style = { { fill : 'url(#heart-gradient)' , stroke : 'none' } }
185- />
194+ { images . map ( ( img ) => (
195+ < div
196+ key = { img . id }
197+ className = "flex h-full min-w-full shrink-0 items-center justify-center"
198+ >
199+ < img
200+ src = { img . url }
201+ alt = ""
202+ className = "h-full w-full object-cover select-none"
203+ />
204+ </ div >
205+ ) ) }
186206 </ motion . div >
187- ) }
188- </ AnimatePresence >
189-
190- { images . length > 1 && currentIndex > 0 && (
191- < button
192- type = "button"
193- onClick = { ( e ) => {
194- e . stopPropagation ( )
195- moveSlide ( - 1 )
196- } }
197- className = "absolute top-1/2 left-4 z-30 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full bg-white/80 shadow-md hover:bg-white"
198- >
199- < ChevronLeft className = "h-5 w-5 text-black" strokeWidth = { 3 } />
200- </ button >
201- ) }
202- { images . length > 1 && currentIndex < images . length - 1 && (
203- < button
204- type = "button"
205- onClick = { ( e ) => {
206- e . stopPropagation ( )
207- moveSlide ( 1 )
208- } }
209- className = "absolute top-1/2 right-4 z-30 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full bg-white/80 shadow-md hover:bg-white"
210- >
211- < ChevronRight className = "h-5 w-5 text-black" strokeWidth = { 3 } />
212- </ button >
213- ) }
214-
215- { images . length > 1 && (
216- < div className = "absolute bottom-4 left-1/2 z-30 flex -translate-x-1/2 gap-1.5" >
217- { images . map ( ( _ , i ) => (
218- < div
219- key = { i }
220- className = { `h-1.5 w-1.5 rounded-full transition-colors ${
221- i === currentIndex ? 'bg-white' : 'bg-white/50'
222- } `}
223- />
224- ) ) }
207+
208+ < AnimatePresence >
209+ { showHeart && (
210+ < motion . div
211+ key = "rising-heart"
212+ initial = { {
213+ scale : 0 ,
214+ opacity : 1 ,
215+ y : 0 ,
216+ rotate : randomRotate ,
217+ } }
218+ animate = { {
219+ scale : [ 0 , 1.2 , 1 ] ,
220+ y : - 20 ,
221+ rotate : [ randomRotate , randomRotate , 0 ] ,
222+ } }
223+ exit = { {
224+ y : - 700 ,
225+ transition : { duration : 0.2 , ease : 'circIn' } ,
226+ } }
227+ transition = { {
228+ duration : 0.7 ,
229+ times : [ 0 , 0.57 , 1 ] ,
230+ ease : 'easeInOut' ,
231+ } }
232+ onAnimationComplete = { ( ) => setIsAnimating ( false ) }
233+ className = "pointer-events-none absolute inset-0 z-20 flex items-center justify-center"
234+ >
235+ < Heart
236+ className = "h-32 w-32 drop-shadow-2xl"
237+ style = { { fill : 'url(#heart-gradient)' , stroke : 'none' } }
238+ />
239+ </ motion . div >
240+ ) }
241+ </ AnimatePresence >
242+
243+ { images . length > 1 && currentIndex > 0 && (
244+ < button
245+ type = "button"
246+ onClick = { ( e ) => {
247+ e . stopPropagation ( )
248+ moveSlide ( - 1 )
249+ } }
250+ className = "absolute top-1/2 left-4 z-30 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full bg-white/80 shadow-md hover:bg-white"
251+ >
252+ < ChevronLeft
253+ className = "h-5 w-5 text-black"
254+ strokeWidth = { 3 }
255+ />
256+ </ button >
257+ ) }
258+ { images . length > 1 && currentIndex < images . length - 1 && (
259+ < button
260+ type = "button"
261+ onClick = { ( e ) => {
262+ e . stopPropagation ( )
263+ moveSlide ( 1 )
264+ } }
265+ className = "absolute top-1/2 right-4 z-30 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full bg-white/80 shadow-md hover:bg-white"
266+ >
267+ < ChevronRight
268+ className = "h-5 w-5 text-black"
269+ strokeWidth = { 3 }
270+ />
271+ </ button >
272+ ) }
273+
274+ { images . length > 1 && (
275+ < div className = "absolute bottom-4 left-1/2 z-30 flex -translate-x-1/2 gap-1.5" >
276+ { images . map ( ( _ , i ) => (
277+ < div
278+ key = { i }
279+ className = { `h-1.5 w-1.5 rounded-full transition-colors ${
280+ i === currentIndex ? 'bg-white' : 'bg-white/50'
281+ } `}
282+ />
283+ ) ) }
284+ </ div >
285+ ) }
225286 </ div >
226- ) }
227- </ div >
228- </ div >
229-
230- < div className = "flex w-full flex-col border-l border-gray-200 bg-white md:w-[40%]" >
231- < PostInfoSection
232- data = { postData }
233- ref = { infoSectionRef }
234- onDataChange = { handlePostDataChange }
235- />
236- </ div >
287+ </ div >
288+
289+ < div className = "flex w-full flex-col border-l border-gray-200 bg-white md:w-[40%]" >
290+ < PostInfoSection
291+ data = { postData }
292+ ref = { infoSectionRef }
293+ onDataChange = { handlePostDataChange }
294+ />
295+ </ div >
296+ < />
297+ ) }
237298 </ div >
238299 </ div >
239300 )
0 commit comments