diff --git a/src/api/client/axios.ts b/src/api/client/axios.ts index 407ccc528..fd720774b 100644 --- a/src/api/client/axios.ts +++ b/src/api/client/axios.ts @@ -20,7 +20,7 @@ export const axiosInstance = axios.create({ // multipart 요청용 export const axiosInstanceForMultipart = axios.create({ baseURL: `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/v1/`, - timeout: 10000, + timeout: 60000, // 파일 업로드 전용 — 클라이언트→백엔드 전송 + 백엔드→S3 처리 여유 headers: { // JS에서 formData 를 넘길땐 Content-Type 생략해야 자동으로 multipart/form-data + boundary 설정됨 }, @@ -205,7 +205,7 @@ axiosInstanceForMultipart.interceptors.response.use( if (originalRequest) { originalRequest.headers.Authorization = `Bearer ${token}`; - return axiosInstance(originalRequest); + return axiosInstanceForMultipart(originalRequest); } }) .catch((err) => { @@ -224,7 +224,7 @@ axiosInstanceForMultipart.interceptors.response.use( if (originalRequest) { originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; - return axiosInstance(originalRequest); + return axiosInstanceForMultipart(originalRequest); } } else { processFailedQueue(new Error('토큰 갱신 실패')); diff --git a/src/app/(service)/(my)/my-study-review/_components/completed-study-review-page.tsx b/src/app/(service)/(my)/my-study-review/_components/completed-study-review-page.tsx index bd4bd9306..fff32422b 100644 --- a/src/app/(service)/(my)/my-study-review/_components/completed-study-review-page.tsx +++ b/src/app/(service)/(my)/my-study-review/_components/completed-study-review-page.tsx @@ -40,6 +40,15 @@ interface StudyRoleSectionProps { onMemberClick?: (study: MemberStudyItem) => void; } +const REVIEW_PERIOD_DAYS = 7; + +const isWithinReviewPeriod = (endTime: string) => { + const deadline = dayjs(endTime).add(REVIEW_PERIOD_DAYS, 'day'); + const now = dayjs(); + + return now.isBefore(deadline) || now.isSame(deadline); +}; + function StudyRoleSection({ title, studies, @@ -156,6 +165,10 @@ export default function CompletedStudyReviewPage({ return; } + if (!isWithinReviewPeriod(study.endTime)) { + return; + } + setReviewStudy(study); }; diff --git a/src/app/global.css b/src/app/global.css index 4161a6f26..628d454bf 100644 --- a/src/app/global.css +++ b/src/app/global.css @@ -462,6 +462,10 @@ https://velog.io/@oneook/tailwindcss-4.0-%EB%AC%B4%EC%97%87%EC%9D%B4-%EB%8B%AC%E min-height: 200px; } +@utility min-h-150 { + min-height: 150px; +} + @utility h-study-card { height: 244px; } @@ -478,10 +482,19 @@ https://velog.io/@oneook/tailwindcss-4.0-%EB%AC%B4%EC%97%87%EC%9D%B4-%EB%8B%AC%E max-width: 1164px; } +@utility max-w-500 { + max-width: 500px; +} + @utility max-h-modal { max-height: 90vh; } +@utility size-icon { + width: 36px; + height: 36px; +} + @utility grid-cols-content-sidebar-360 { grid-template-columns: minmax(0, 1fr) 360px; } diff --git a/src/components/common/modals/question-modal.tsx b/src/components/common/modals/question-modal.tsx index 93635b2d3..3c5daca01 100644 --- a/src/components/common/modals/question-modal.tsx +++ b/src/components/common/modals/question-modal.tsx @@ -1,6 +1,7 @@ 'use client'; import { zodResolver } from '@hookform/resolvers/zod'; +import { useQueryClient } from '@tanstack/react-query'; import { XIcon } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; @@ -94,6 +95,7 @@ export default function QuestionModal({ const [isImageRemoved, setIsImageRemoved] = useState(false); const [isFinalizingSubmission, setIsFinalizingSubmission] = useState(false); + const queryClient = useQueryClient(); const isEditMode = mode === 'edit' && !!questionId; const initialTitle = initialValues?.title ?? ''; const initialContent = initialValues?.content ?? ''; @@ -203,8 +205,6 @@ export default function QuestionModal({ variant: 'success' | 'info' = 'success', ) => { showToast(message, variant); - reset(DEFAULT_FORM_VALUES); - resetImageState(); handleOpenChange(false); if (onAfterSubmit) { @@ -242,6 +242,13 @@ export default function QuestionModal({ try { await uploadImage(imageUploadUrl, imageFile); + // S3 업로드 완료 후 재무효화: 훅의 onSuccess가 업로드 전에 실행되므로 + // 이 시점에 다시 무효화해야 이미지가 포함된 최신 데이터를 가져올 수 있다. + if (isEditMode && questionId) { + await queryClient.invalidateQueries({ + queryKey: ['question', studyId, questionId], + }); + } finalizeSubmission(successMessage); } catch (error) { console.error('문의 이미지 업로드 오류:', error); @@ -305,7 +312,7 @@ export default function QuestionModal({ - + {isEditMode ? '스터디 문의 수정하기' : '스터디 문의하기'} @@ -357,7 +364,7 @@ export default function QuestionModal({
diff --git a/src/components/common/ui/image-upload-input.tsx b/src/components/common/ui/image-upload-input.tsx index bd99d60be..a1494b4be 100644 --- a/src/components/common/ui/image-upload-input.tsx +++ b/src/components/common/ui/image-upload-input.tsx @@ -15,7 +15,7 @@ const dropzoneVariants = cva( variants: { dragging: { true: 'border-border-brand bg-fill-brand-subtle-hover', - false: 'border-gray-300 border-dashed', + false: 'border-border-default border-dashed', }, }, defaultVariants: { @@ -57,13 +57,13 @@ export default function ImageUploadInput({ fileInputRef.current?.click(); }; - const handleDragEnter = (e: React.DragEvent) => { + const handleDragEnter = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(true); }; - const handleDragLeave = (e: React.DragEvent) => { + const handleDragLeave = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); @@ -98,7 +98,7 @@ export default function ImageUploadInput({ }; return ( -
+
드래그하여 파일 업로드 + + 이미지 파일 · 최대 {(maxSizeBytes / 1024 / 1024).toFixed(0)}MB +
)}
- {sizeError &&

{sizeError}

} + {sizeError && ( +

{sizeError}

+ )}
); }