Skip to content

이미지 크기 검증 로직 및 민감 데이터 정리#402

Merged
HA-SEUNG-JEONG merged 10 commits intodevelopfrom
hotfix/login
Feb 26, 2026
Merged

이미지 크기 검증 로직 및 민감 데이터 정리#402
HA-SEUNG-JEONG merged 10 commits intodevelopfrom
hotfix/login

Conversation

@HA-SEUNG-JEONG
Copy link
Contributor

@HA-SEUNG-JEONG HA-SEUNG-JEONG commented Feb 26, 2026

🌱 연관된 이슈

☘️ 작업 내용

  • 이미지 크기 검증
  • 민감 데이터 정리

🍀 참고사항

스크린샷 (선택)

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 이미지 업로드에 기본 5MB 제한과 클라이언트 검사 및 구성 가능 옵션 추가. 미리보기와 삭제 오버레이 제공.
    • 그룹 스터디 폼에 제목·요약·설명·위치·면접 질문별 최대 길이 검증 도입.
  • 버그 수정/안전성

    • 로그아웃 시 리다이렉트 대상에 대한 안전성 검증 추가로 위험한 URL 차단.
  • 기타

    • 보안 헤더를 Report-Only로 적용해 검증 시작, 세션 저장소로 전환 및 로컬 정리 실행.

@HA-SEUNG-JEONG HA-SEUNG-JEONG self-assigned this Feb 26, 2026
@vercel
Copy link

vercel bot commented Feb 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
study-platform-client-dev Ready Ready Preview, Comment Feb 26, 2026 7:06am

@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

📝 Walkthrough

Walkthrough

CSP Report-Only 헤더 추가, 이미지 업로드 클라이언트 사이즈 검증 도입, 리다이렉트 안전성 검사 추가, 그룹 스터디 폼 필드 최대 길이 제약 도입, 사용자 상태를 sessionStorage로 전환하고 localStorage 정리하는 변경입니다.

Changes

Cohort / File(s) Summary
보안 및 헤더 설정
next.config.ts
개발 플래그(isDev) 기반으로 Content-Security-Policy-Report-Only 지시문을 구성하고 모든 경로에 적용하는 async headers() 추가. 추가 보안 헤더(X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Strict-Transport-Security, Permissions-Policy)를 반환.
리다이렉트 보안 검증
src/app/api/auth/clear-session/route.ts
isSafeRedirectPath(path) 도입: 빈값/절대 URL/프로토콜-상대(//) 등을 차단하고, 안전하지 않으면 /login으로 폴백하도록 리다이렉트 대상 검증 로직 변경.
이미지 업로드 검증 (컴포넌트 변경)
src/components/ui/image-upload-input.tsx
기본 5MB(DEFAULT_MAX_SIZE_BYTES) 제한과 maxSizeBytes? prop 추가. 파일 타입·크기 검증(validateAndChange) 통합, 에러 상태(sizeError) 및 UI(프리뷰·삭제 버튼) 업데이트.
이미지 처리 호출부
src/features/study/group/ui/step/step2-group.tsx
handleImageChange 시그니처를 `File
폼 스키마 및 상수 추가
src/features/study/group/model/group-study-form.schema.ts
타이틀/요약/설명/위치/면접문항에 대한 최대 길이 상수(GROUP_STUDY_*_MAX_LENGTH) 추가 및 스키마에 max 길이 검증 적용(면접문항은 항목별 max 적용).에러 메시지와 일부 Zod 이슈 코드 참조 조정.
세션 저장소 전환 및 로컬 정리
src/providers/index.tsx, src/stores/useUserStore.ts
Zustand persisted store에 createJSONStorage(() => sessionStorage) 사용으로 세션 저장소 전환. UserInitializer에 마운트 시 localStorageuser-info-storage 삭제 사이드 이펙트 추가(마이그레이션 정리).
폼 UI 개선(오류 표기)
src/features/study/group/ui/step/step3-group.tsx
useFormContext에서 formState를 사용해 각 면접 질문 항목별 에러를 인라인으로 렌더링하도록 UI 레이아웃 및 오류 표시 위치 조정.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 새벽빛에 깡충, 코드 밭을 뛰어넘네,
헤더로 문단단히, 경로는 살피고,
이미지 가볍게, 길이는 꼭 재고,
세션은 옮겨두고 오래된 상자 비우네,
토끼가 깡충—축하의 당근을 드려요! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Pull request 제목이 주요 변경 사항을 명확하게 요약하고 있습니다. 이미지 크기 검증 로직 추가와 민감 데이터 정리(localStorage 제거)라는 두 가지 핵심 변경 사항을 정확히 반영하고 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch hotfix/login

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/api/auth/clear-session/route.ts (1)

27-40: ⚠️ Potential issue | 🟠 Major

Origin 헤더 우선 사용은 오픈 리다이렉트 우회 지점이 됩니다.

경로 검증을 추가했더라도, Line 27처럼 Origin 헤더를 base URL로 신뢰하면 응답 Location의 도메인을 조작할 수 있습니다. 기준 origin은 서버가 파싱한 요청 URL(request.nextUrl.origin)을 사용하세요.

제안 패치
-  const origin = request.headers.get('origin');
-
   const baseUrl =
-    origin ||
-    (() => {
-      const host = request.headers.get('host');
-      const protocol = request.headers.get('x-forwarded-proto') || 'https';
-
-      return host ? `${protocol}://${host}` : null;
-    })() ||
+    request.nextUrl.origin ||
     process.env.NEXT_PUBLIC_SITE_URL ||
     process.env.NEXT_PUBLIC_API_PROD_BASE_URL?.replace('/api/v1', '') ||
     process.env.NEXT_PUBLIC_API_BASE_URL?.replace('/api/v1', '') ||
     request.url.split('/api')[0];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/auth/clear-session/route.ts` around lines 27 - 40, The code
trusts the incoming Origin header when building baseUrl (variable origin and
baseUrl), which enables open-redirect manipulation; replace use of
request.headers.get('origin') with the server-parsed origin
request.nextUrl.origin (or otherwise derive baseUrl from request.nextUrl.origin)
and remove or ignore the untrusted Origin header, ensuring all fallback chain
(process.env... and request.url split) remains but using request.nextUrl.origin
as the primary trusted origin; update any tests or callers that assumed
header-origin behavior and keep baseUrl construction logic (baseUrl variable)
intact otherwise.
🧹 Nitpick comments (1)
src/components/ui/image-upload-input.tsx (1)

90-91: 클래스 조합에 clsx 유틸 사용 권장

템플릿 문자열 대신 clsx 유틸을 사용하면 Tailwind 클래스 충돌 관리와 가독성이 더 좋습니다.

제안 패치
+import clsx from 'clsx';
 import Image from 'next/image';
 import { useState, DragEvent, ChangeEvent, useRef } from 'react';
 import Button from '@/components/ui/button';
@@
-        className={`${inputStyles.base} ${isDragging ? inputStyles.dragging : inputStyles.notDragging}`}
+        className={clsx(
+          inputStyles.base,
+          isDragging ? inputStyles.dragging : inputStyles.notDragging,
+        )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/image-upload-input.tsx` around lines 90 - 91, Replace the
template-string className composition in the ImageUploadInput component with the
clsx utility: import clsx (or classnames) at the top and use
clsx(inputStyles.base, isDragging ? inputStyles.dragging :
inputStyles.notDragging) instead of the current `${inputStyles.base}
${isDragging ? inputStyles.dragging : inputStyles.notDragging}` expression; this
touches the JSX prop where className is set and uses the existing inputStyles
and isDragging identifiers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/ui/image-upload-input.tsx`:
- Around line 130-136: The remove button (inside the ImageUploadInput component)
only contains the "✕" glyph so screen readers can't determine its purpose;
update the button element that calls handleRemove to include an accessible label
by adding either an aria-label (e.g., aria-label="Remove image") or a
visually-hidden text node (e.g., <span className="sr-only">Remove image</span>)
inside the button while keeping the existing styling and onClick handler; ensure
the label text clearly describes the action (Remove image) and that no visual
layout changes occur.
- Around line 73-80: The file-input does not clear its value after selecting or
removing a file, so re-selecting the same file may not fire change events;
update handleFileChange and handleRemove to reset the input value after
processing by either using the existing input event (e.g., set
e.currentTarget.value = '' after calling validateAndChange) or add a ref (e.g.,
fileInputRef) to the input element and set fileInputRef.current.value = '' after
calling validateAndChange in handleFileChange and after
setSizeError/onChangeImage in handleRemove so the same file can be reselected
reliably.

In `@src/providers/index.tsx`:
- Around line 19-23: Wrap the localStorage cleanup inside the useEffect in a
safe guard: detect environment (window and window.localStorage) and execute
localStorage.removeItem('user-info-storage') inside a try/catch (inside the same
useEffect) so any SecurityError/DOMException is caught and ignored or logged via
your logger; ensure the catch does not rethrow so the component init cannot
fail. Reference: the useEffect block containing
localStorage.removeItem('user-info-storage').

---

Outside diff comments:
In `@src/app/api/auth/clear-session/route.ts`:
- Around line 27-40: The code trusts the incoming Origin header when building
baseUrl (variable origin and baseUrl), which enables open-redirect manipulation;
replace use of request.headers.get('origin') with the server-parsed origin
request.nextUrl.origin (or otherwise derive baseUrl from request.nextUrl.origin)
and remove or ignore the untrusted Origin header, ensuring all fallback chain
(process.env... and request.url split) remains but using request.nextUrl.origin
as the primary trusted origin; update any tests or callers that assumed
header-origin behavior and keep baseUrl construction logic (baseUrl variable)
intact otherwise.

---

Nitpick comments:
In `@src/components/ui/image-upload-input.tsx`:
- Around line 90-91: Replace the template-string className composition in the
ImageUploadInput component with the clsx utility: import clsx (or classnames) at
the top and use clsx(inputStyles.base, isDragging ? inputStyles.dragging :
inputStyles.notDragging) instead of the current `${inputStyles.base}
${isDragging ? inputStyles.dragging : inputStyles.notDragging}` expression; this
touches the JSX prop where className is set and uses the existing inputStyles
and isDragging identifiers.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 915ed57 and 3b472e6.

📒 Files selected for processing (7)
  • next.config.ts
  • src/app/api/auth/clear-session/route.ts
  • src/components/ui/image-upload-input.tsx
  • src/features/study/group/model/group-study-form.schema.ts
  • src/features/study/group/ui/step/step2-group.tsx
  • src/providers/index.tsx
  • src/stores/useUserStore.ts

Comment on lines +19 to +23
useEffect(() => {
// [보안 마이그레이션] sessionStorage 전환 이후 localStorage의 잔여 민감 데이터 정리.
// 기존 사용자 브라우저에 남아있는 user-info-storage를 삭제하여 XSS 노출 경로 제거.
localStorage.removeItem('user-info-storage');
}, []);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

스토리지 접근 예외를 방어 처리해 주세요.

브라우저 보안 설정에 따라 localStorage 접근이 예외를 던질 수 있어, 초기화 이펙트가 실패할 수 있습니다. 안전하게 감싸는 편이 좋습니다.

제안 패치
   useEffect(() => {
-    localStorage.removeItem('user-info-storage');
+    try {
+      localStorage.removeItem('user-info-storage');
+    } catch {
+      // storage 접근이 제한된 환경에서는 무시
+    }
   }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/providers/index.tsx` around lines 19 - 23, Wrap the localStorage cleanup
inside the useEffect in a safe guard: detect environment (window and
window.localStorage) and execute localStorage.removeItem('user-info-storage')
inside a try/catch (inside the same useEffect) so any SecurityError/DOMException
is caught and ignored or logged via your logger; ensure the catch does not
rethrow so the component init cannot fail. Reference: the useEffect block
containing localStorage.removeItem('user-info-storage').

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/app/api/auth/clear-session/route.ts (1)

4-11: 오픈 리다이렉트 방어 로직은 양호하지만, 백슬래시 처리를 추가하면 더 안전합니다.

일부 브라우저에서 백슬래시(\)를 슬래시로 해석할 수 있어, /\evil.com 같은 경로가 예상치 못한 동작을 유발할 가능성이 있습니다. 방어적 코딩 관점에서 백슬래시 검증 추가를 권장합니다.

🛡️ 백슬래시 검증 추가 제안
 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) 차단
+  if (path.includes('\\')) return false; // 백슬래시 포함 경로 차단

   return true;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/auth/clear-session/route.ts` around lines 4 - 11, The
isSafeRedirectPath function should also reject paths containing backslashes to
prevent browser interpretation of "\" as "/", so update isSafeRedirectPath to
return false if path includes a backslash (e.g., check path.includes('\\') or a
regex) in addition to the existing checks; ensure the new check references the
isSafeRedirectPath function so the validation runs before allowing any redirect.
src/features/study/group/model/group-study-form.schema.ts (1)

111-118: interviewPost 항목도 trim()을 선적용해 다른 필드와 일관성을 맞추는 것을 권장합니다.

현재는 공백 포함 길이로 max가 평가되어, 앞뒤 공백이 많은 입력에서 UX가 다소 불일치할 수 있습니다.

제안 diff
     interviewPost: z
       .array(
         z
           .string()
+          .trim()
           .max(
             GROUP_STUDY_INTERVIEW_Q_MAX_LENGTH,
             `질문은 ${GROUP_STUDY_INTERVIEW_Q_MAX_LENGTH}자 이하로 입력해주세요.`,
           ),
       )
-      .refine((arr) => arr.length > 0 && arr.every((v) => v.trim() !== ''), {
+      .refine((arr) => arr.length > 0 && arr.every((v) => v !== ''), {
         message: '모든 질문을 입력해야 합니다.',
       })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/study/group/model/group-study-form.schema.ts` around lines 111 -
118, The interviewPost array items are validated for max length without
trimming, causing leading/trailing spaces to affect length checks; update the
Zod schema for interviewPost to apply .trim() before .max (i.e., change the item
validator from z.string().max(...) to
z.string().trim().max(GROUP_STUDY_INTERVIEW_Q_MAX_LENGTH, ...)) so it matches
other fields and enforces length on trimmed content; ensure the error message
string remains the same and that this change is applied to the schema that
constructs interviewPost.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/app/api/auth/clear-session/route.ts`:
- Around line 4-11: The isSafeRedirectPath function should also reject paths
containing backslashes to prevent browser interpretation of "\" as "/", so
update isSafeRedirectPath to return false if path includes a backslash (e.g.,
check path.includes('\\') or a regex) in addition to the existing checks; ensure
the new check references the isSafeRedirectPath function so the validation runs
before allowing any redirect.

In `@src/features/study/group/model/group-study-form.schema.ts`:
- Around line 111-118: The interviewPost array items are validated for max
length without trimming, causing leading/trailing spaces to affect length
checks; update the Zod schema for interviewPost to apply .trim() before .max
(i.e., change the item validator from z.string().max(...) to
z.string().trim().max(GROUP_STUDY_INTERVIEW_Q_MAX_LENGTH, ...)) so it matches
other fields and enforces length on trimmed content; ensure the error message
string remains the same and that this change is applied to the schema that
constructs interviewPost.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3b472e6 and 56ff11e.

📒 Files selected for processing (3)
  • next.config.ts
  • src/app/api/auth/clear-session/route.ts
  • src/features/study/group/model/group-study-form.schema.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • next.config.ts

@HA-SEUNG-JEONG HA-SEUNG-JEONG changed the title Hotfix/login 이미지 크기 검증 로직 및 민감 데이터 정리 Feb 26, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/features/study/group/model/group-study-form.schema.ts (1)

111-125: superRefine를 사용하여 항목별 에러 경로를 지정하면 폼 피드백이 더 정확해집니다.

현재 refine는 배열 단위 메시지만 제공하여 어떤 질문이 비었는지 바로 알기 어렵습니다. superRefine에서 ctx.addIssue()path: [...ctx.path, index]를 지정하면 각 항목별로 독립적인 에러를 표시할 수 있어 사용자 경험이 개선됩니다.

Zod v4의 권장 패턴:

.superRefine((arr, ctx) => {
  arr.forEach((item, idx) => {
    if (!item || item.trim() === '') {
      ctx.addIssue({
        code: 'custom',
        message: '질문을 입력해주세요.',
        path: [...ctx.path, idx],
      });
    }
  });
  if (arr.length > 10) {
    ctx.addIssue({
      code: 'custom',
      message: '질문은 최대 10개까지만 입력할 수 있습니다.',
      path: ctx.path,
    });
  }
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/study/group/model/group-study-form.schema.ts` around lines 111 -
125, Replace the two array-level .refine(...) checks with a single
.superRefine((arr, ctx) => { ... }) on the interview questions array schema:
iterate arr with arr.forEach((item, idx) => { if (!item || item.trim() === '')
ctx.addIssue({ code: 'custom', message: '질문을 입력해주세요.', path: [...ctx.path, idx]
}) }) to add per-item errors, and check length with if (arr.length > 10)
ctx.addIssue({ code: 'custom', message: '질문은 최대 10개까지만 입력할 수 있습니다.', path:
ctx.path }); keep the existing GROUP_STUDY_INTERVIEW_Q_MAX_LENGTH validation on
the string schema (the .max call) so item length still enforces that constant.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/ui/image-upload-input.tsx`:
- Around line 29-39: The validateAndChange function leaves a previous sizeError
when a non-image file is dropped because it returns early on the type check;
update validateAndChange to clear any existing sizeError before returning for
non-image files (i.e., call setSizeError(null) when file.type does not start
with 'image/'), then keep the existing size check logic and onChangeImage(file)
call; reference validateAndChange, setSizeError, and onChangeImage to locate the
change.

---

Nitpick comments:
In `@src/features/study/group/model/group-study-form.schema.ts`:
- Around line 111-125: Replace the two array-level .refine(...) checks with a
single .superRefine((arr, ctx) => { ... }) on the interview questions array
schema: iterate arr with arr.forEach((item, idx) => { if (!item || item.trim()
=== '') ctx.addIssue({ code: 'custom', message: '질문을 입력해주세요.', path:
[...ctx.path, idx] }) }) to add per-item errors, and check length with if
(arr.length > 10) ctx.addIssue({ code: 'custom', message: '질문은 최대 10개까지만 입력할 수
있습니다.', path: ctx.path }); keep the existing GROUP_STUDY_INTERVIEW_Q_MAX_LENGTH
validation on the string schema (the .max call) so item length still enforces
that constant.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 56ff11e and 887f589.

📒 Files selected for processing (3)
  • src/app/api/auth/clear-session/route.ts
  • src/components/ui/image-upload-input.tsx
  • src/features/study/group/model/group-study-form.schema.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/api/auth/clear-session/route.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/ui/image-upload-input.tsx`:
- Line 147: The validation message element that renders {sizeError} should
announce dynamically to assistive tech; update the JSX that renders sizeError
(the <p className="text-text-danger text-sm">{sizeError}</p>) to include an
accessibility live region such as role="alert" (or aria-live="assertive") so
screen readers are notified immediately when sizeError appears; keep the
conditional render as-is and only add the attribute to the <p> element.
- Line 96: Replace the template-string class composition with the project's cn()
util and CVA-based style tokens: stop using `${inputStyles.base} ${isDragging ?
inputStyles.dragging : inputStyles.notDragging}` and instead compose classes
with cn(inputStyles.base, isDragging ? inputStyles.dragging :
inputStyles.notDragging). If inputStyles is not already a cva(...) descriptor,
convert its definition to use cva(...) (exported as inputStyles) with variants
for dragging vs notDragging, then call cn(inputStyles(), { /* variant keys if
used */ }) or cn(inputStyles.base, ...) as appropriate so the component (using
isDragging) follows the project's CVA + cn pattern.
- Around line 101-106: Replace the raw Image tag using src="/icons/camera.svg"
with an SVG React component imported via the project's svgr setup: import the
SVG (e.g. camera.svg) as a React component and render that component in place of
the <Image ... src="/icons/camera.svg" .../> element (adjust props like
width/height/alt or className as needed); update the import at the top of the
file accordingly and remove the static Image usage so it matches other files
(e.g. home-dashboard.tsx) that use SVGs as React components.

In `@src/features/study/group/model/group-study-form.schema.ts`:
- Around line 110-136: The interviewPost array schema currently only validates
items so an empty array passes; update the interviewPost z.array(...) definition
to enforce at least one entry (e.g., call .min(1, { message: '질문을 최소 1개 입력해주세요.'
}) or use .nonempty({ message: '질문을 최소 1개 입력해주세요.' })) on the array schema while
keeping the inner string validation and the existing superRefine logic intact so
item-level checks and custom errors still run.

In `@src/features/study/group/ui/step/step3-group.tsx`:
- Around line 65-68: formState.errors.interviewPost array-level errors are not
rendered, only per-index errors are shown; update the component (step3-group.tsx
/ wherever interviewPost is rendered) to also render the array-level message
(formState.errors.interviewPost?.message or
formState.errors.interviewPost?.type) in addition to the existing per-item block
so users see messages like "최대 10개" — place the array-level error display above
or below the list of items where interviewPost is mapped.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 887f589 and 0cde969.

📒 Files selected for processing (3)
  • src/components/ui/image-upload-input.tsx
  • src/features/study/group/model/group-study-form.schema.ts
  • src/features/study/group/ui/step/step3-group.tsx

onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
className={`${inputStyles.base} ${isDragging ? inputStyles.dragging : inputStyles.notDragging}`}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/components/ui/image-upload-input.tsx | head -150

Repository: code-zero-to-one/study-platform-client

Length of output: 5532


🏁 Script executed:

grep -r "clsx\|tailwind-merge\|class-variance-authority" --include="package.json" --include="*.ts" --include="*.tsx" | head -20

Repository: code-zero-to-one/study-platform-client

Length of output: 1650


🏁 Script executed:

cat -n src/components/ui/button/index.tsx | head -50

Repository: code-zero-to-one/study-platform-client

Length of output: 2111


🏁 Script executed:

cat -n src/components/ui/button/index.tsx | sed -n '50,70p'

Repository: code-zero-to-one/study-platform-client

Length of output: 739


🏁 Script executed:

cat -n src/components/ui/\(shadcn\)/lib/utils.ts

Repository: code-zero-to-one/study-platform-client

Length of output: 292


동적 클래스 조합을 CVA와 cn() 유틸로 통일해 주세요.

현재 템플릿 문자열 조합은 프로젝트 스타일 가이드와 불일치합니다. 다른 UI 컴포넌트(Button, Badge, Chip 등)와 같이 CVA를 사용하고, 프로젝트의 cn() 유틸리티를 활용해주세요.

제안 패치
+import { cva } from 'class-variance-authority';
+import { cn } from '@/components/ui/(shadcn)/lib/utils';
 import Image from 'next/image';
 import { useState, DragEvent, ChangeEvent, useRef } from 'react';
 import Button from '@/components/ui/button';

-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',
-  notDragging: 'border-gray-300 border-dashed',
-};
+const inputStyles = cva(
+  'rounded-100 flex w-full flex-col items-center justify-center border-2 p-500',
+  {
+    variants: {
+      dragging: {
+        true: 'border-border-brand bg-fill-brand-subtle-hover',
+        false: 'border-gray-300 border-dashed',
+      },
+    },
+    defaultVariants: {
+      dragging: false,
+    },
+  },
+);
...
- className={`${inputStyles.base} ${isDragging ? inputStyles.dragging : inputStyles.notDragging}`}
+ className={cn(inputStyles({ dragging: isDragging }))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/image-upload-input.tsx` at line 96, Replace the
template-string class composition with the project's cn() util and CVA-based
style tokens: stop using `${inputStyles.base} ${isDragging ?
inputStyles.dragging : inputStyles.notDragging}` and instead compose classes
with cn(inputStyles.base, isDragging ? inputStyles.dragging :
inputStyles.notDragging). If inputStyles is not already a cva(...) descriptor,
convert its definition to use cva(...) (exported as inputStyles) with variants
for dragging vs notDragging, then call cn(inputStyles(), { /* variant keys if
used */ }) or cn(inputStyles.base, ...) as appropriate so the component (using
isDragging) follows the project's CVA + cn pattern.

Comment on lines +101 to +106
<Image
src="/icons/camera.svg"
width={32}
height={32}
alt="파일 업로드"
/>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists
fd "image-upload-input.tsx" src/

Repository: code-zero-to-one/study-platform-client

Length of output: 122


🏁 Script executed:

# Check next.config configuration
cat -n next.config.ts

Repository: code-zero-to-one/study-platform-client

Length of output: 5783


🏁 Script executed:

# Search for SVG imports to see current patterns
rg "\.svg['\"]" src/ -A 2 -B 2 | head -40

Repository: code-zero-to-one/study-platform-client

Length of output: 2824


🏁 Script executed:

# Check if icons/camera.svg exists
fd "camera.svg"

Repository: code-zero-to-one/study-platform-client

Length of output: 105


🏁 Script executed:

# Read the image-upload-input.tsx file to verify the current code
cat -n src/components/ui/image-upload-input.tsx | head -120 | tail -30

Repository: code-zero-to-one/study-platform-client

Length of output: 1328


🏁 Script executed:

# Check the complete context around lines 101-106
sed -n '95,110p' src/components/ui/image-upload-input.tsx

Repository: code-zero-to-one/study-platform-client

Length of output: 709


🏁 Script executed:

# Check imports at the top of the file to understand current pattern
head -20 src/components/ui/image-upload-input.tsx

Repository: code-zero-to-one/study-platform-client

Length of output: 683


SVG를 React 컴포넌트로 import해 주세요.

@svgr/webpack으로 설정된 SVG React 컴포넌트 import 방식을 사용하세요. 다른 파일들(예: home-dashboard.tsx)처럼 SVG를 컴포넌트로 가져와 사용하는 것이 프로젝트 가이드에 맞습니다.

제안 패치
+import CameraIcon from 'public/icons/camera.svg';
...
-<Image
-  src="/icons/camera.svg"
-  width={32}
-  height={32}
-  alt="파일 업로드"
-/>

+<CameraIcon
+  width={32}
+  height={32}
+  aria-label="파일 업로드"
+/>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Image
src="/icons/camera.svg"
width={32}
height={32}
alt="파일 업로드"
/>
<CameraIcon
width={32}
height={32}
aria-label="파일 업로드"
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/image-upload-input.tsx` around lines 101 - 106, Replace the
raw Image tag using src="/icons/camera.svg" with an SVG React component imported
via the project's svgr setup: import the SVG (e.g. camera.svg) as a React
component and render that component in place of the <Image ...
src="/icons/camera.svg" .../> element (adjust props like width/height/alt or
className as needed); update the import at the top of the file accordingly and
remove the static Image usage so it matches other files (e.g.
home-dashboard.tsx) that use SVGs as React components.

)}
)}
</div>
{sizeError && <p className="text-text-danger text-sm">{sizeError}</p>}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

오류 메시지에 라이브 리전을 추가해 주세요.

동적으로 바뀌는 검증 오류는 스크린리더에 즉시 전달되도록 role="alert"(또는 aria-live)를 붙이는 게 안전합니다.

제안 패치
-      {sizeError && <p className="text-text-danger text-sm">{sizeError}</p>}
+      {sizeError && (
+        <p role="alert" aria-live="polite" className="text-text-danger text-sm">
+          {sizeError}
+        </p>
+      )}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{sizeError && <p className="text-text-danger text-sm">{sizeError}</p>}
{sizeError && (
<p role="alert" aria-live="polite" className="text-text-danger text-sm">
{sizeError}
</p>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/image-upload-input.tsx` at line 147, The validation message
element that renders {sizeError} should announce dynamically to assistive tech;
update the JSX that renders sizeError (the <p className="text-text-danger
text-sm">{sizeError}</p>) to include an accessibility live region such as
role="alert" (or aria-live="assertive") so screen readers are notified
immediately when sizeError appears; keep the conditional render as-is and only
add the attribute to the <p> element.

Comment on lines +65 to +68
{formState.errors.interviewPost?.[index]?.message && (
<p role="alert" className="font-designer-14r text-text-error">
{formState.errors.interviewPost[index]?.message as string}
</p>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

배열 단위 검증 메시지가 화면에 표시되지 않습니다.

Line [65]~Line [68]은 항목 인덱스 에러만 렌더링해서, interviewPost 자체 에러(예: “최대 10개”)가 사용자에게 보이지 않을 수 있습니다. 배열 레벨 메시지도 별도로 렌더링해 주세요.

수정 예시
           {questions.map((q, index) => (
             <div key={index} className="flex flex-col gap-75">
@@
               {formState.errors.interviewPost?.[index]?.message && (
                 <p role="alert" className="font-designer-14r text-text-error">
                   {formState.errors.interviewPost[index]?.message as string}
                 </p>
               )}
             </div>
           ))}
+          {typeof (formState.errors.interviewPost as { message?: string } | undefined)?.message === 'string' && (
+            <p role="alert" className="font-designer-14r text-text-error">
+              {(formState.errors.interviewPost as { message?: string }).message}
+            </p>
+          )}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{formState.errors.interviewPost?.[index]?.message && (
<p role="alert" className="font-designer-14r text-text-error">
{formState.errors.interviewPost[index]?.message as string}
</p>
{questions.map((q, index) => (
<div key={index} className="flex flex-col gap-75">
{formState.errors.interviewPost?.[index]?.message && (
<p role="alert" className="font-designer-14r text-text-error">
{formState.errors.interviewPost[index]?.message as string}
</p>
)}
</div>
))}
{typeof (formState.errors.interviewPost as { message?: string } | undefined)?.message === 'string' && (
<p role="alert" className="font-designer-14r text-text-error">
{(formState.errors.interviewPost as { message?: string }).message}
</p>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/study/group/ui/step/step3-group.tsx` around lines 65 - 68,
formState.errors.interviewPost array-level errors are not rendered, only
per-index errors are shown; update the component (step3-group.tsx / wherever
interviewPost is rendered) to also render the array-level message
(formState.errors.interviewPost?.message or
formState.errors.interviewPost?.type) in addition to the existing per-item block
so users see messages like "최대 10개" — place the array-level error display above
or below the list of items where interviewPost is mapped.

@HA-SEUNG-JEONG HA-SEUNG-JEONG merged commit 60bcced into develop Feb 26, 2026
14 of 16 checks passed
@HA-SEUNG-JEONG HA-SEUNG-JEONG deleted the hotfix/login branch February 26, 2026 14:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant