1- import { useState } from "react" ;
1+ import { useState , useRef } from "react" ;
22import { useLocation , useNavigate } from "react-router-dom" ;
33import { api } from "../../api/axios" ;
44import { API_ENDPOINTS } from "../../api/endpoints" ;
5- import { useToastStore } from "../../store/toastStore" ;
5+ import { toast } from "../../store/toastStore" ;
66import { Spinner } from "../../components/common/Spinner" ;
77import specialcandy from "../../assets/icons/specialcandy.svg" ;
8+ import imageCompression from "browser-image-compression" ;
89import type { Recommendation } from "../../types/recommendation" ;
910
1011// 이미지 추가 아이콘 컴포넌트
@@ -26,35 +27,65 @@ const ImageAddIcon = () => (
2627export default function AchievementPage ( ) {
2728 const location = useLocation ( ) ;
2829 const navigate = useNavigate ( ) ;
29- const { addToast } = useToastStore ( ) ;
3030
3131 const item = location . state ?. item as Recommendation | undefined ;
3232 const [ memo , setMemo ] = useState ( "" ) ;
33+ const [ images , setImages ] = useState < File [ ] > ( [ ] ) ;
3334 const [ loading , setLoading ] = useState ( false ) ;
35+ const fileInputRef = useRef < HTMLInputElement | null > ( null ) ;
3436
3537 // 현재 날짜 포맷
3638 const today = new Date ( ) ;
3739 const dateStr = `${ today . getMonth ( ) + 1 } . ${ today . getDate ( ) } .` ;
3840 const dayNames = [ "일요일" , "월요일" , "화요일" , "수요일" , "목요일" , "금요일" , "토요일" ] ;
3941 const dayStr = dayNames [ today . getDay ( ) ] ;
4042
43+ const handleImageChange = async ( e : React . ChangeEvent < HTMLInputElement > ) => {
44+ const files = e . target . files ;
45+ if ( ! files ) return ;
46+
47+ const compressedFiles : File [ ] = [ ] ;
48+ for ( let i = 0 ; i < files . length ; i ++ ) {
49+ try {
50+ const compressed = await imageCompression ( files [ i ] , {
51+ maxSizeMB : 0.5 ,
52+ maxWidthOrHeight : 1024 ,
53+ useWebWorker : true ,
54+ } ) ;
55+ compressedFiles . push ( compressed ) ;
56+ } catch ( err ) {
57+ console . error ( "이미지 압축 실패:" , err ) ;
58+ }
59+ }
60+
61+ if ( compressedFiles . length + images . length > 4 ) {
62+ toast . warning ( "이미지는 최대 4장까지 업로드 가능합니다!" ) ;
63+ }
64+
65+ setImages ( ( prev ) => [ ...prev , ...compressedFiles ] . slice ( 0 , 4 ) ) ;
66+ e . target . value = "" ;
67+ } ;
68+
4169 const handleSubmit = async ( ) => {
4270 if ( ! item ) {
43- addToast ( "추천 정보가 없습니다." , "error ") ;
71+ toast . error ( "추천 정보가 없습니다." ) ;
4472 return ;
4573 }
4674
4775 setLoading ( true ) ;
4876 try {
49- // 완료 API 호출
50- await api . post ( API_ENDPOINTS . RECOMMENDATIONS . COMPLETE ( item . id ) , {
51- memo,
77+ const formData = new FormData ( ) ;
78+ formData . append ( "request" , JSON . stringify ( { memo } ) ) ;
79+ images . forEach ( ( img ) => formData . append ( "images" , img ) ) ;
80+
81+ await api . post ( API_ENDPOINTS . RECOMMENDATIONS . COMPLETE ( item . id ) , formData , {
82+ headers : { "Content-Type" : "multipart/form-data" } ,
5283 } ) ;
53- addToast ( "사탕이 만들어졌어요!" , "success ") ;
84+ toast . success ( "사탕이 만들어졌어요!" ) ;
5485 navigate ( "/archive" ) ;
5586 } catch ( err ) {
5687 console . error ( err ) ;
57- addToast ( "저장에 실패했습니다." , "error ") ;
88+ toast . error ( "저장에 실패했습니다." ) ;
5889 } finally {
5990 setLoading ( false ) ;
6091 }
@@ -109,15 +140,39 @@ export default function AchievementPage() {
109140 </ div >
110141
111142 { /* 이미지 업로드 영역 */ }
112- < div className = "flex gap-3 mt-4" >
113- { [ 0 , 1 , 2 , 3 ] . map ( ( index ) => (
114- < button
115- key = { index }
116- className = "w-[80px] h-[80px] rounded-[8px] border border-[#D5D5D5] bg-[#F8F8F8] flex items-center justify-center"
117- >
118- { index === 0 && < ImageAddIcon /> }
119- </ button >
120- ) ) }
143+ < div className = "mt-4" >
144+ < input
145+ ref = { fileInputRef }
146+ type = "file"
147+ accept = "image/*"
148+ multiple
149+ onChange = { handleImageChange }
150+ className = "hidden"
151+ />
152+
153+ < div className = "grid grid-cols-4 gap-3" >
154+ { images . map ( ( file , idx ) => (
155+ < div
156+ key = { idx }
157+ className = "relative h-20 overflow-hidden rounded-xl border"
158+ >
159+ < img
160+ src = { URL . createObjectURL ( file ) }
161+ className = "h-full w-full object-cover"
162+ />
163+ </ div >
164+ ) ) }
165+
166+ { images . length < 4 && (
167+ < button
168+ type = "button"
169+ onClick = { ( ) => fileInputRef . current ?. click ( ) }
170+ className = "flex h-20 items-center justify-center rounded-xl border border-[#D5D5D5] bg-[#F8F8F8]"
171+ >
172+ < ImageAddIcon />
173+ </ button >
174+ ) }
175+ </ div >
121176 </ div >
122177 </ div >
123178 </ div >
0 commit comments