1- 'use client' ;
2-
31import Image from 'next/image' ;
42import { useQuery , useMutation , useQueryClient } from '@tanstack/react-query' ;
53import { ProductResponse } from '@/types/product' ;
64import { getProductById } from '@/api/products' ;
75import ProductDetailButtonGroup from '@/components/products/ProductDetailButtonGroup' ;
6+ import CategoryTag from './CategoryTag' ;
7+ import Share from '../../../public/icon/common/share.png' ;
8+ import unFilledHeart from '../../../public/icon/common/unsave.png' ;
9+ import FilledHeart from '../../../public/icon/common/save.png' ;
10+ import useAuthStore from '@/stores/authStores' ;
11+ import { useRouter } from 'next/router' ;
12+ import axiosInstance from '@/api/axiosInstance' ;
813
914interface ProductDetailProps {
1015 id : number ;
1116}
1217
1318const toggleFavorite = async ( productId : number , isFavorite : boolean ) : Promise < void > => {
14- const method = isFavorite ? 'DELETE' : 'POST' ;
15- const res = await fetch ( `https://mogazoa-api.vercel.app/13-3/products/${ productId } /favorite` , {
16- method,
17- } ) ;
18- if ( ! res . ok ) {
19- throw new Error ( `Failed to ${ isFavorite ? 'unlike' : 'like' } product` ) ;
19+ try {
20+ if ( isFavorite ) {
21+ await axiosInstance . delete ( `/products/${ productId } /favorite` ) ;
22+ } else {
23+ await axiosInstance . post ( `/products/${ productId } /favorite` ) ;
24+ }
25+ } catch ( err : any ) {
26+ throw new Error ( err . message || 'Failed to toggle favorite' ) ;
2027 }
2128} ;
2229
2330export default function ProductDetail ( { id } : ProductDetailProps ) {
2431 const queryClient = useQueryClient ( ) ;
32+ const router = useRouter ( ) ;
33+ const isLoggedIn = useAuthStore ( ( state ) => state . isLoggedIn ) ;
2534
2635 const { data : product , isLoading } = useQuery < ProductResponse > ( {
2736 queryKey : [ 'product' , id ] ,
@@ -34,14 +43,37 @@ export default function ProductDetail({ id }: ProductDetailProps) {
3443 onSuccess : ( ) => {
3544 queryClient . invalidateQueries ( { queryKey : [ 'product' , id ] } ) ;
3645 } ,
46+ onError : ( err ) => {
47+ if ( err . message === 'Unauthorized' ) {
48+ alert ( '로그인이 필요한 기능입니다.' ) ;
49+ router . push ( '/login' ) ;
50+ } else {
51+ alert ( err . message ) ;
52+ }
53+ } ,
3754 } ) ;
3855
3956 const handleFavoriteClick = ( ) => {
57+ if ( ! isLoggedIn ) {
58+ alert ( '로그인이 필요한 기능입니다.' ) ;
59+ return router . push ( '/login' ) ;
60+ }
4061 if ( product ) {
4162 favoriteMutation . mutate ( { productId : product . id , isFavorite : product . isFavorite } ) ;
4263 }
4364 } ;
4465
66+ const handleShareClick = async ( ) => {
67+ try {
68+ const url = window . location . href ;
69+ await navigator . clipboard . writeText ( url ) ;
70+ alert ( '클립보드에 URL이 복사되었습니다.' ) ;
71+ } catch ( err ) {
72+ console . error ( '클립보드 복사 실패:' , err ) ;
73+ alert ( 'URL 복사에 실패했습니다.' ) ;
74+ }
75+ } ;
76+
4577 if ( isLoading ) {
4678 return (
4779 < section className = "flex items-center justify-center min-h-[200px]" >
@@ -59,40 +91,62 @@ export default function ProductDetail({ id }: ProductDetailProps) {
5991 }
6092
6193 return (
62- < section className = "w-full mx-auto mb-4 sm:mb-6 lg:mb-8 " >
63- < div className = "w-full bg-black-400 rounded-xl flex flex-col lg :flex-row items-center gap-4 lg:gap-8 p-4 sm:p-6 lg:p-8 " >
64- < div className = "w-full sm :w-[180px] h-[180px] flex-shrink-0 flex items-center justify-center bg-black-500 rounded-lg border border-gray-200 " >
94+ < section className = "w-full mx-auto mb-[60px] lg:mb-20 " >
95+ < div className = "w-full flex flex-col md :flex-row items-center gap-4" >
96+ < div className = "lg :w-[355px] min-w-[240px] w-full flex items-center justify-center" >
6597 < Image
66- src = {
67- product . image ||
68- 'https://store.storeimages.cdn-apple.com/8756/as-images.apple.com/is/MWP22?wid=1144&hei=1144&fmt=jpeg&qlt=80&.v=1591634795000'
69- }
70- alt = { product . name || '상품 이미지' }
98+ src = { product . image }
99+ alt = { product . name }
71100 width = { 160 }
72101 height = { 160 }
73- className = "object-contain"
102+ priority
103+ className = "object-contain h-auto"
74104 />
75105 </ div >
76- < div className = "flex-1 flex flex-col justify-between w-full h-full gap-4" >
77- < div >
78- < span className = "inline-block bg-gray-200 text-white text-xs px-2 py-1 rounded mb-2" >
79- { product . category ?. name || '브랜드명' }
80- </ span >
81- < div className = "text-lg sm:text-xl lg:text-2xl font-bold mb-2 flex items-center justify-between gap-4" >
82- < span > { product . name || '상품명' } </ span >
83- < button
84- onClick = { handleFavoriteClick }
85- className = "text-gray-400 hover:text-red-500"
86- disabled = { favoriteMutation . isPending }
106+ < div className = "flex flex-col justify-between min-w-0 w-full" >
107+ < >
108+ < div className = "flex justify-between mb-[10px]" >
109+ < CategoryTag name = { product . category ?. name } />
110+ < div
111+ className = "p-[5px] bg-black-400 rounded-[6px] block md:hidden"
112+ onClick = { handleShareClick }
113+ >
114+ < Image src = { Share } alt = "공유하기 아이콘" width = { 14 } height = { 14 } />
115+ </ div >
116+ </ div >
117+ < div className = "text-lg sm:text-xl lg:text-2xl font-bold mb-5 md:mb-[50px] flex items-center justify-between" >
118+ < div className = "flex items-center gap-[15px]" >
119+ < span > { product . name || '상품명' } </ span >
120+ { product . isFavorite ? (
121+ < Image
122+ src = { FilledHeart }
123+ alt = "좋아요 아이콘"
124+ width = { 24 }
125+ height = { 24 }
126+ onClick = { handleFavoriteClick }
127+ />
128+ ) : (
129+ < Image
130+ src = { unFilledHeart }
131+ alt = "비어있는 좋아요 아이콘"
132+ width = { 24 }
133+ height = { 24 }
134+ onClick = { handleFavoriteClick }
135+ />
136+ ) }
137+ </ div >
138+ < div
139+ className = "p-[5px] bg-black-400 rounded-[6px] hidden md:block"
140+ onClick = { handleShareClick }
87141 >
88- { product . isFavorite ? '❤️' : '🤍' }
89- </ button >
142+ < Image src = { Share } alt = "공유하기 아이콘" width = { 14 } height = { 14 } />
143+ </ div >
90144 </ div >
91- < div className = "text-sm lg:text-base text-gray-300 " >
92- { product . description || '상품 설명이 들어갑니다.' }
145+ < div className = "text-sm lg:text-base mb-[67px] md:mb-[60px] text-gray-50 " >
146+ { product . description }
93147 </ div >
94- </ div >
95- < ProductDetailButtonGroup />
148+ </ >
149+ < ProductDetailButtonGroup product = { product } />
96150 </ div >
97151 </ div >
98152 </ section >
0 commit comments