From 1c27bf9cbad6c0ad7ec8e6326767e6fcfe4043a0 Mon Sep 17 00:00:00 2001 From: Jeong Ha Seung <88266129+HA-SEUNG-JEONG@users.noreply.github.com> Date: Thu, 26 Feb 2026 13:44:18 +0900 Subject: [PATCH 01/10] =?UTF-8?q?fix:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=EC=97=90=20=ED=8C=8C=EC=9D=BC=20=ED=81=AC=EA=B8=B0=20?= =?UTF-8?q?=EC=A0=9C=ED=95=9C=20=EB=B0=8F=20=EC=98=A4=EB=A5=98=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/image-upload-input.tsx | 132 +++++++++++++---------- 1 file changed, 75 insertions(+), 57 deletions(-) diff --git a/src/components/ui/image-upload-input.tsx b/src/components/ui/image-upload-input.tsx index 234e9cd7..d183522f 100644 --- a/src/components/ui/image-upload-input.tsx +++ b/src/components/ui/image-upload-input.tsx @@ -4,6 +4,9 @@ import Image from 'next/image'; import { useState, DragEvent, ChangeEvent, useRef } from 'react'; import Button from '@/components/ui/button'; +// 클라이언트에서 선제 차단하여 불필요한 413 에러 및 대용량 업로드 요청을 방지. +const DEFAULT_MAX_SIZE_BYTES = 5 * 1024 * 1024; // 5MB + const inputStyles = { base: 'rounded-100 flex w-full flex-col items-center justify-center border-2 p-500', dragging: 'border-border-brand bg-fill-brand-subtle-hover', @@ -13,12 +16,27 @@ const inputStyles = { export default function ImageUploadInput({ image, onChangeImage, + maxSizeBytes = DEFAULT_MAX_SIZE_BYTES, }: { image?: string; onChangeImage: (file: File | undefined) => void; + maxSizeBytes?: number; }) { const fileInputRef = useRef(null); const [isDragging, setIsDragging] = useState(false); + const [sizeError, setSizeError] = useState(null); + + const validateAndChange = (file: File) => { + if (!file.type.startsWith('image/')) return; + if (file.size > maxSizeBytes) { + const maxMb = (maxSizeBytes / 1024 / 1024).toFixed(0); + setSizeError(`이미지 파일 크기는 ${maxMb}MB 이하만 업로드할 수 있어요.`); + + return; + } + setSizeError(null); + onChangeImage(file); + }; const handleOpenFileDialog = () => { fileInputRef.current?.click(); @@ -49,77 +67,77 @@ export default function ImageUploadInput({ e.stopPropagation(); setIsDragging(false); const file = e.dataTransfer.files[0]; - if (file && file.type.startsWith('image/')) { - onChangeImage(file); - } + if (file) validateAndChange(file); }; const handleFileChange = (e: ChangeEvent) => { const file = e.target.files?.[0]; - if (file && file.type.startsWith('image/')) { - onChangeImage(file); - } + if (file) validateAndChange(file); }; const handleRemove = () => { + setSizeError(null); onChangeImage(undefined); }; return ( -
- {!image ? ( -
-
+
+
+ {!image ? ( +
+
+ 파일 업로드 + + 드래그하여 파일 업로드 + +
+ + +
+ ) : ( +
파일 업로드 - - 드래그하여 파일 업로드 - +
- - -
- ) : ( -
- preview - -
- )} + )} +
+ {sizeError &&

{sizeError}

}
); } From d98822f967eeb5172d51a9645b4ce3a4f9e5fa7b Mon Sep 17 00:00:00 2001 From: Jeong Ha Seung <88266129+HA-SEUNG-JEONG@users.noreply.github.com> Date: Thu, 26 Feb 2026 13:45:35 +0900 Subject: [PATCH 02/10] =?UTF-8?q?fix:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=8B=9C=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=ED=81=AC=EA=B8=B0=20=EA=B2=80=EC=A6=9D=EC=9D=84=20ImageUploadI?= =?UTF-8?q?nput=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A1=9C=20?= =?UTF-8?q?=EC=9C=84=EC=9E=84=ED=95=98=EA=B3=A0=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/study/group/ui/step/step2-group.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/features/study/group/ui/step/step2-group.tsx b/src/features/study/group/ui/step/step2-group.tsx index 5746c2e8..c15abfdb 100644 --- a/src/features/study/group/ui/step/step2-group.tsx +++ b/src/features/study/group/ui/step/step2-group.tsx @@ -33,7 +33,7 @@ export default function Step2OpenGroupStudy() { } }, [thumbnailFile, thumbnailExtension]); - const handleImageChange = (file: File | null) => { + const handleImageChange = (file: File | undefined) => { if (!file) { setValue('thumbnailExtension', 'DEFAULT', { shouldValidate: true }); setValue('thumbnailFile', null); @@ -42,13 +42,6 @@ export default function Step2OpenGroupStudy() { return; } - const MAX_SIZE = 5 * 1024 * 1024; // 5MB - if (file.size > MAX_SIZE) { - alert('이미지 용량은 5MB 이하만 업로드할 수 있어요.'); - - return; - } - const ext = file.name.split('.').pop()?.toUpperCase(); const validExt = ext && THUMBNAIL_EXTENSION.includes(ext as any) From 6b56234d96607cf5603f87f4e917cf19887e363f Mon Sep 17 00:00:00 2001 From: Jeong Ha Seung <88266129+HA-SEUNG-JEONG@users.noreply.github.com> Date: Thu, 26 Feb 2026 13:45:44 +0900 Subject: [PATCH 03/10] =?UTF-8?q?fix:=20=EA=B7=B8=EB=A3=B9=20=EC=8A=A4?= =?UTF-8?q?=ED=84=B0=EB=94=94=20=ED=8F=BC=EC=9D=98=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=B5=9C=EB=8C=80=20=EA=B8=B8=EC=9D=B4=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../group/model/group-study-form.schema.ts | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/features/study/group/model/group-study-form.schema.ts b/src/features/study/group/model/group-study-form.schema.ts index 0d80eded..e834480d 100644 --- a/src/features/study/group/model/group-study-form.schema.ts +++ b/src/features/study/group/model/group-study-form.schema.ts @@ -18,6 +18,14 @@ import { const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/; +// [보안/데이터 무결성] 백엔드 DTO의 @Size 어노테이션과 동기화. +// 출처: GroupStudyDetailInfoRequestDto.java, GroupStudyInterviewPostRequestDto.java +export const GROUP_STUDY_TITLE_MAX_LENGTH = 100; +export const GROUP_STUDY_SUMMARY_MAX_LENGTH = 200; +export const GROUP_STUDY_DESCRIPTION_MAX_LENGTH = 1000; +export const GROUP_STUDY_LOCATION_MAX_LENGTH = 255; +export const GROUP_STUDY_INTERVIEW_Q_MAX_LENGTH = 500; + export const STUDY_CLASSIFICATION = ['GROUP_STUDY', 'PREMIUM_STUDY'] as const; export type StudyClassification = (typeof STUDY_CLASSIFICATION)[number]; @@ -44,7 +52,7 @@ export const GroupStudyFormSchema = z // 진행 방식(1) method: z.enum(STUDY_METHODS), // 진행 방식 (위치) optional (1) - location: z.string().trim(), + location: z.string().trim().max(GROUP_STUDY_LOCATION_MAX_LENGTH, `위치는 ${GROUP_STUDY_LOCATION_MAX_LENGTH}자 이하로 입력해주세요.`), // 정기 모임(1) regularMeeting: z.enum(REGULAR_MEETINGS), // 진행 기간 (시작일) (1) @@ -59,11 +67,23 @@ export const GroupStudyFormSchema = z .regex(ISO_DATE_REGEX, 'YYYY-MM-DD 형식의 종료일을 입력해 주세요.'), price: z.string().trim().optional(), // 스터디 제목(2) - title: z.string().trim().min(1, '스터디 제목을 입력해주세요.'), + title: z + .string() + .trim() + .min(1, '스터디 제목을 입력해주세요.') + .max(GROUP_STUDY_TITLE_MAX_LENGTH, `제목은 ${GROUP_STUDY_TITLE_MAX_LENGTH}자 이하로 입력해주세요.`), // 스터디 한 줄 소개(2) - summary: z.string().trim().min(1, '한 줄 소개를 입력해주세요.'), + summary: z + .string() + .trim() + .min(1, '한 줄 소개를 입력해주세요.') + .max(GROUP_STUDY_SUMMARY_MAX_LENGTH, `한 줄 소개는 ${GROUP_STUDY_SUMMARY_MAX_LENGTH}자 이하로 입력해주세요.`), // 스터디 소개(2) - description: z.string().trim().min(1, '스터디 소개를 입력해주세요.'), + description: z + .string() + .trim() + .min(1, '스터디 소개를 입력해주세요.') + .max(GROUP_STUDY_DESCRIPTION_MAX_LENGTH, `스터디 소개는 ${GROUP_STUDY_DESCRIPTION_MAX_LENGTH}자 이하로 입력해주세요.`), // 썸네일 START(2) thumbnailExtension: z .enum(THUMBNAIL_EXTENSION) @@ -73,7 +93,11 @@ export const GroupStudyFormSchema = z // 썸네일 END(2) // 스터디원에게 보여줄 질문을 입력하세요(3) interviewPost: z - .array(z.string()) + .array( + z + .string() + .max(GROUP_STUDY_INTERVIEW_Q_MAX_LENGTH, `질문은 ${GROUP_STUDY_INTERVIEW_Q_MAX_LENGTH}자 이하로 입력해주세요.`), + ) .refine((arr) => arr.length > 0 && arr.every((v) => v.trim() !== ''), { message: '모든 질문을 입력해야 합니다.', }) From caac426ca8ae1fbf54464742cf2c3937f00bad21 Mon Sep 17 00:00:00 2001 From: Jeong Ha Seung <88266129+HA-SEUNG-JEONG@users.noreply.github.com> Date: Thu, 26 Feb 2026 13:55:42 +0900 Subject: [PATCH 04/10] =?UTF-8?q?fix:=20Open=20Redirect=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20=EB=B0=8F?= =?UTF-8?q?=20=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=A0=89=ED=8A=B8=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/auth/clear-session/route.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/app/api/auth/clear-session/route.ts b/src/app/api/auth/clear-session/route.ts index 7a612835..55a31258 100644 --- a/src/app/api/auth/clear-session/route.ts +++ b/src/app/api/auth/clear-session/route.ts @@ -1,9 +1,20 @@ import { cookies } from 'next/headers'; import { NextRequest, NextResponse } from 'next/server'; + +// new URL(path, base)는 path가 절대 URL이면 base를 무시하므로 반드시 사전 검증 필요. +function isSafeRedirectPath(path: string) { + if (!path) return false; + if (!path.startsWith('/')) return false; // 절대 URL(https://evil.com) 차단 + if (path.startsWith('//')) return false; // 프로토콜-상대 URL(//evil.com) 차단 + + return true; +} + export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams; - const redirectTo = searchParams.get('redirect') || '/login'; + const rawRedirect = searchParams.get('redirect') || '/login'; + const redirectTo = isSafeRedirectPath(rawRedirect) ? rawRedirect : '/login'; // 쿠키 삭제 const cookieStore = await cookies(); From 3b472e611fca8b0aa4fafcac988b22191c529f35 Mon Sep 17 00:00:00 2001 From: Jeong Ha Seung <88266129+HA-SEUNG-JEONG@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:04:35 +0900 Subject: [PATCH 05/10] =?UTF-8?q?fix:=20=EB=B3=B4=EC=95=88=20HTTP=20?= =?UTF-8?q?=ED=97=A4=EB=8D=94=20=EC=84=A4=EC=A0=95=20=EB=B0=8F=20sessionSt?= =?UTF-8?q?orage=EB=A1=9C=EC=9D=98=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EB=AF=BC?= =?UTF-8?q?=EA=B0=90=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- next.config.ts | 39 ++++++++++++++++++++++++++++++++++++++ src/providers/index.tsx | 6 ++++++ src/stores/useUserStore.ts | 5 ++++- 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/next.config.ts b/next.config.ts index 51ef58b3..10adbf8c 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,8 +2,47 @@ import type { NextConfig } from 'next'; import type { RemotePattern } from 'next/dist/shared/lib/image-config'; const isProd = process.env.NODE_ENV === 'production'; +const isDev = process.env.NODE_ENV === 'development'; const nextConfig: NextConfig = { + // [보안] 보안 HTTP 헤더 설정. + // CSP는 스테이징 검증 중이므로 Content-Security-Policy-Report-Only 모드로 먼저 배포. + // 결제·로그인 E2E 검증 후 위반 없으면 Content-Security-Policy로 전환할 것. + async headers() { + const cspDirectives = [ + "default-src 'self'", + // GTM, Clarity, 토스페이먼츠 스크립트 허용. 개발 환경에서는 'unsafe-eval' 추가(Next.js HMR 필요). + `script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.clarity.ms https://js.tosspayments.com${isDev ? " 'unsafe-eval'" : ''}`, + "style-src 'self' 'unsafe-inline'", + // 카카오·구글 프로필 이미지, 자사 API/CMS 이미지 도메인 허용 + "img-src 'self' data: blob: https://img1.kakaocdn.net https://lh3.googleusercontent.com https://api.zeroone.it.kr https://test-api.zeroone.it.kr https://www.zeroone.it.kr https://test-blog.zeroone.it.kr", + // API, GA, Clarity, 토스, 카카오/구글 OAuth 연결 허용. 개발에서는 HMR WebSocket 추가. + `connect-src 'self' https://api.zeroone.it.kr https://test-api.zeroone.it.kr https://www.google-analytics.com https://www.clarity.ms https://api.tosspayments.com https://kauth.kakao.com https://accounts.google.com${isDev ? ' ws://localhost:*' : ''}`, + // 토스 결제창 iframe 허용 + "frame-src https://pay.toss.im https://cert.tosspayments.com", + "font-src 'self' data:", + ].join('; '); + + return [ + { + source: '/(.*)', + headers: [ + // Report-Only 모드: 위반을 차단하지 않고 콘솔에 경고만 출력. 검증 완료 후 CSP로 전환 예정. + { key: 'Content-Security-Policy-Report-Only', value: cspDirectives }, + // [클릭재킹 방지] 동일 출처의 iframe만 허용 + { key: 'X-Frame-Options', value: 'SAMEORIGIN' }, + // [MIME 스니핑 방지] 브라우저가 Content-Type을 임의로 변경하는 행위 차단 + { key: 'X-Content-Type-Options', value: 'nosniff' }, + // [정보 유출 방지] 크로스 오리진 요청 시 origin만 전송 + { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, + // [HTTPS 강제] 1년간 HTTPS만 허용 (프로덕션에서 유효) + { key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' }, + // [브라우저 API 제한] 불필요한 카메라·마이크·위치정보 API 비활성화 + { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' }, + ], + }, + ]; + }, // output: 'standalone', /* config options here */ // 외부 이미지 도메인 허용 설정 추가 diff --git a/src/providers/index.tsx b/src/providers/index.tsx index 3968e5a6..bf38191b 100644 --- a/src/providers/index.tsx +++ b/src/providers/index.tsx @@ -16,6 +16,12 @@ function UserInitializer({ children }: ProviderProps) { const { memberId: authMemberId, isAuthReady } = useAuthReady(); const { memberId, fetchAndSetUser } = useUserStore(); + useEffect(() => { + // [보안 마이그레이션] sessionStorage 전환 이후 localStorage의 잔여 민감 데이터 정리. + // 기존 사용자 브라우저에 남아있는 user-info-storage를 삭제하여 XSS 노출 경로 제거. + localStorage.removeItem('user-info-storage'); + }, []); + useEffect(() => { if (isAuthReady && authMemberId && !memberId) { fetchAndSetUser(authMemberId).catch(console.error); diff --git a/src/stores/useUserStore.ts b/src/stores/useUserStore.ts index 6720ce35..b028bf77 100644 --- a/src/stores/useUserStore.ts +++ b/src/stores/useUserStore.ts @@ -1,5 +1,5 @@ import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; +import { createJSONStorage, persist } from 'zustand/middleware'; import { getUserProfile } from '@/entities/user/api/get-user-profile'; interface UserInfo { @@ -44,6 +44,9 @@ export const useUserStore = create()( }), { name: 'user-info-storage', + + // 새 탭에서 열면 providers/index.tsx의 UserInitializer가 fetchAndSetUser()로 재취득하므로 UX 영향 없음. + storage: createJSONStorage(() => sessionStorage), }, ), ); From b768c7d6a483708af5743c32bf5b63fc7cfafdb9 Mon Sep 17 00:00:00 2001 From: Jeong Ha Seung <88266129+HA-SEUNG-JEONG@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:13:25 +0900 Subject: [PATCH 06/10] =?UTF-8?q?fix:=20next.config.ts=EC=9D=98=20CSP=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=EC=97=90=EC=84=9C=20=EB=AC=B8=EC=9E=90?= =?UTF-8?q?=EC=97=B4=20=ED=8F=AC=EB=A7=B7=20=ED=86=B5=EC=9D=BC=20=EB=B0=8F?= =?UTF-8?q?=20route.ts=EC=97=90=EC=84=9C=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EA=B3=B5=EB=B0=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- next.config.ts | 12 +++++++++--- src/app/api/auth/clear-session/route.ts | 1 - 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/next.config.ts b/next.config.ts index 10adbf8c..62a9106d 100644 --- a/next.config.ts +++ b/next.config.ts @@ -19,7 +19,7 @@ const nextConfig: NextConfig = { // API, GA, Clarity, 토스, 카카오/구글 OAuth 연결 허용. 개발에서는 HMR WebSocket 추가. `connect-src 'self' https://api.zeroone.it.kr https://test-api.zeroone.it.kr https://www.google-analytics.com https://www.clarity.ms https://api.tosspayments.com https://kauth.kakao.com https://accounts.google.com${isDev ? ' ws://localhost:*' : ''}`, // 토스 결제창 iframe 허용 - "frame-src https://pay.toss.im https://cert.tosspayments.com", + 'frame-src https://pay.toss.im https://cert.tosspayments.com', "font-src 'self' data:", ].join('; '); @@ -36,9 +36,15 @@ const nextConfig: NextConfig = { // [정보 유출 방지] 크로스 오리진 요청 시 origin만 전송 { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, // [HTTPS 강제] 1년간 HTTPS만 허용 (프로덕션에서 유효) - { key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' }, + { + key: 'Strict-Transport-Security', + value: 'max-age=31536000; includeSubDomains', + }, // [브라우저 API 제한] 불필요한 카메라·마이크·위치정보 API 비활성화 - { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' }, + { + key: 'Permissions-Policy', + value: 'camera=(), microphone=(), geolocation=()', + }, ], }, ]; diff --git a/src/app/api/auth/clear-session/route.ts b/src/app/api/auth/clear-session/route.ts index 55a31258..27922173 100644 --- a/src/app/api/auth/clear-session/route.ts +++ b/src/app/api/auth/clear-session/route.ts @@ -1,7 +1,6 @@ import { cookies } from 'next/headers'; import { NextRequest, NextResponse } from 'next/server'; - // new URL(path, base)는 path가 절대 URL이면 base를 무시하므로 반드시 사전 검증 필요. function isSafeRedirectPath(path: string) { if (!path) return false; From 56ff11ef4d7b1a4531c87f4ffb7b16a74c2809b0 Mon Sep 17 00:00:00 2001 From: Jeong Ha Seung <88266129+HA-SEUNG-JEONG@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:15:59 +0900 Subject: [PATCH 07/10] =?UTF-8?q?fix:=20=EA=B7=B8=EB=A3=B9=20=EC=8A=A4?= =?UTF-8?q?=ED=84=B0=EB=94=94=20=ED=8F=BC=EC=9D=98=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=B5=9C=EB=8C=80=20=EA=B8=B8=EC=9D=B4=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=ED=8F=AC=EB=A7=B7=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../group/model/group-study-form.schema.ts | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/features/study/group/model/group-study-form.schema.ts b/src/features/study/group/model/group-study-form.schema.ts index e834480d..c365d32f 100644 --- a/src/features/study/group/model/group-study-form.schema.ts +++ b/src/features/study/group/model/group-study-form.schema.ts @@ -52,7 +52,13 @@ export const GroupStudyFormSchema = z // 진행 방식(1) method: z.enum(STUDY_METHODS), // 진행 방식 (위치) optional (1) - location: z.string().trim().max(GROUP_STUDY_LOCATION_MAX_LENGTH, `위치는 ${GROUP_STUDY_LOCATION_MAX_LENGTH}자 이하로 입력해주세요.`), + location: z + .string() + .trim() + .max( + GROUP_STUDY_LOCATION_MAX_LENGTH, + `위치는 ${GROUP_STUDY_LOCATION_MAX_LENGTH}자 이하로 입력해주세요.`, + ), // 정기 모임(1) regularMeeting: z.enum(REGULAR_MEETINGS), // 진행 기간 (시작일) (1) @@ -71,19 +77,28 @@ export const GroupStudyFormSchema = z .string() .trim() .min(1, '스터디 제목을 입력해주세요.') - .max(GROUP_STUDY_TITLE_MAX_LENGTH, `제목은 ${GROUP_STUDY_TITLE_MAX_LENGTH}자 이하로 입력해주세요.`), + .max( + GROUP_STUDY_TITLE_MAX_LENGTH, + `제목은 ${GROUP_STUDY_TITLE_MAX_LENGTH}자 이하로 입력해주세요.`, + ), // 스터디 한 줄 소개(2) summary: z .string() .trim() .min(1, '한 줄 소개를 입력해주세요.') - .max(GROUP_STUDY_SUMMARY_MAX_LENGTH, `한 줄 소개는 ${GROUP_STUDY_SUMMARY_MAX_LENGTH}자 이하로 입력해주세요.`), + .max( + GROUP_STUDY_SUMMARY_MAX_LENGTH, + `한 줄 소개는 ${GROUP_STUDY_SUMMARY_MAX_LENGTH}자 이하로 입력해주세요.`, + ), // 스터디 소개(2) description: z .string() .trim() .min(1, '스터디 소개를 입력해주세요.') - .max(GROUP_STUDY_DESCRIPTION_MAX_LENGTH, `스터디 소개는 ${GROUP_STUDY_DESCRIPTION_MAX_LENGTH}자 이하로 입력해주세요.`), + .max( + GROUP_STUDY_DESCRIPTION_MAX_LENGTH, + `스터디 소개는 ${GROUP_STUDY_DESCRIPTION_MAX_LENGTH}자 이하로 입력해주세요.`, + ), // 썸네일 START(2) thumbnailExtension: z .enum(THUMBNAIL_EXTENSION) @@ -96,7 +111,10 @@ export const GroupStudyFormSchema = z .array( z .string() - .max(GROUP_STUDY_INTERVIEW_Q_MAX_LENGTH, `질문은 ${GROUP_STUDY_INTERVIEW_Q_MAX_LENGTH}자 이하로 입력해주세요.`), + .max( + GROUP_STUDY_INTERVIEW_Q_MAX_LENGTH, + `질문은 ${GROUP_STUDY_INTERVIEW_Q_MAX_LENGTH}자 이하로 입력해주세요.`, + ), ) .refine((arr) => arr.length > 0 && arr.every((v) => v.trim() !== ''), { message: '모든 질문을 입력해야 합니다.', From 54fd6c52372cd96a5f312edcc30688ee397d517a Mon Sep 17 00:00:00 2001 From: Jeong Ha Seung <88266129+HA-SEUNG-JEONG@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:25:14 +0900 Subject: [PATCH 08/10] =?UTF-8?q?fix:=20=ED=8C=8C=EC=9D=BC=20=EC=9E=AC?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=ED=97=88=EC=9A=A9=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EC=9E=85=EB=A0=A5=EA=B0=92=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94=20=EB=B0=8F=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EB=B2=84=ED=8A=BC=EC=97=90=20=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=EC=84=B1=20=EB=A0=88=EC=9D=B4=EB=B8=94=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/image-upload-input.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/ui/image-upload-input.tsx b/src/components/ui/image-upload-input.tsx index d183522f..07ce0ffb 100644 --- a/src/components/ui/image-upload-input.tsx +++ b/src/components/ui/image-upload-input.tsx @@ -73,10 +73,12 @@ export default function ImageUploadInput({ const handleFileChange = (e: ChangeEvent) => { const file = e.target.files?.[0]; if (file) validateAndChange(file); + e.target.value = ''; // 같은 파일 재업로드 허용을 위해 입력값 초기화 }; const handleRemove = () => { setSizeError(null); + if (fileInputRef.current) fileInputRef.current.value = ''; onChangeImage(undefined); }; @@ -130,6 +132,7 @@ export default function ImageUploadInput({ +
+
+ handleChange(index, e.target.value)} + /> + {index > 0 && ( + + )} +
+ {formState.errors.interviewPost?.[index]?.message && ( +

+ {formState.errors.interviewPost[index]?.message as string} +

)}
))}