Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/api/client/axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
// 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 설정됨
},
Expand All @@ -43,7 +43,7 @@
axiosInstanceForMultipart.interceptors.request.use(onRequestClient);

// refresh token을 사용해서 access token을 재갱신하는 함수
const refreshAccessToken = async (): Promise<string | null> => {

Check warning on line 46 in src/api/client/axios.ts

View workflow job for this annotation

GitHub Actions / lint

Usage of "null" is deprecated except when describing legacy APIs; use "undefined" instead
try {
const response = await axios.get<{ content: { accessToken: string } }>(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/v1/auth/access-token/refresh`,
Expand Down Expand Up @@ -205,7 +205,7 @@
if (originalRequest) {
originalRequest.headers.Authorization = `Bearer ${token}`;

return axiosInstance(originalRequest);
return axiosInstanceForMultipart(originalRequest);
}
})
.catch((err) => {
Expand All @@ -224,7 +224,7 @@
if (originalRequest) {
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;

return axiosInstance(originalRequest);
return axiosInstanceForMultipart(originalRequest);
}
} else {
processFailedQueue(new Error('토큰 갱신 실패'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -156,6 +165,10 @@ export default function CompletedStudyReviewPage({
return;
}

if (!isWithinReviewPeriod(study.endTime)) {
return;
}
Comment thread
HA-SEUNG-JEONG marked this conversation as resolved.

setReviewStudy(study);
};

Expand Down
13 changes: 13 additions & 0 deletions src/app/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand Down
15 changes: 11 additions & 4 deletions src/components/common/modals/question-modal.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 ?? '';
Expand Down Expand Up @@ -203,8 +205,6 @@ export default function QuestionModal({
variant: 'success' | 'info' = 'success',
) => {
showToast(message, variant);
reset(DEFAULT_FORM_VALUES);
resetImageState();
handleOpenChange(false);

if (onAfterSubmit) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -305,7 +312,7 @@ export default function QuestionModal({
<Modal.Root open={open} onOpenChange={handleOpenChange}>
<Modal.Portal>
<Modal.Overlay />
<Modal.Content size="medium" className="w-full sm:w-[500px]">
<Modal.Content size="medium" className="w-full sm:max-w-500">
<Modal.Header className="border-border-default flex items-center justify-between border-b">
<Modal.Title className="font-designer-20b text-text-strong">
{isEditMode ? '스터디 문의 수정하기' : '스터디 문의하기'}
Expand Down Expand Up @@ -357,7 +364,7 @@ export default function QuestionModal({
<TextAreaInput
placeholder="내용을 입력하세요"
maxLength={QUESTION_CONTENT_MAX_LENGTH}
className="font-designer-16m text-text-default h-auto min-h-[150px]"
className="font-designer-16m text-text-default h-auto min-h-150"
/>
</FormField>
<div className="flex flex-col gap-100">
Expand Down
19 changes: 12 additions & 7 deletions src/components/common/ui/image-upload-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -57,13 +57,13 @@ export default function ImageUploadInput({
fileInputRef.current?.click();
};

const handleDragEnter = (e: React.DragEvent<HTMLDivElement>) => {
const handleDragEnter = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(true);
};

const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
const handleDragLeave = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
Expand Down Expand Up @@ -98,7 +98,7 @@ export default function ImageUploadInput({
};

return (
<div className="flex flex-col gap-1">
<div className="flex flex-col gap-50">
<div
onDrop={handleDrop}
onDragEnter={handleDragEnter}
Expand All @@ -118,6 +118,9 @@ export default function ImageUploadInput({
<span className="font-designer-18m text-text-default">
드래그하여 파일 업로드
</span>
<span className="font-designer-14r text-text-assistive">
이미지 파일 · 최대 {(maxSizeBytes / 1024 / 1024).toFixed(0)}MB
</span>
</div>
<input
ref={fileInputRef}
Expand All @@ -142,20 +145,22 @@ export default function ImageUploadInput({
alt="preview"
width={240}
height={180}
className="rounded-lg object-cover"
className="rounded-100 object-cover"
/>
<button
type="button"
onClick={handleRemove}
aria-label="이미지 삭제"
className="bg-background-dimmer border-border-inverse text-text-inverse absolute top-0 right-0 flex h-[36px] w-[36px] translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border"
className="bg-background-dimmer border-border-inverse text-text-inverse absolute top-0 right-0 flex size-icon translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border"
>
</button>
</div>
)}
</div>
{sizeError && <p className="text-text-danger text-sm">{sizeError}</p>}
{sizeError && (
<p className="font-designer-14r text-text-error">{sizeError}</p>
)}
Comment thread
HA-SEUNG-JEONG marked this conversation as resolved.
</div>
);
}
Loading