Thanks to visit codestin.com
Credit goes to github.com

Skip to content

#69 feat: 문제 등록 및 수정 api 연동#70

Merged
JiiminHa merged 18 commits into
developfrom
feat/69-assignments-add-edit
Mar 18, 2026
Merged

#69 feat: 문제 등록 및 수정 api 연동#70
JiiminHa merged 18 commits into
developfrom
feat/69-assignments-add-edit

Conversation

@JiiminHa

@JiiminHa JiiminHa commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

⚙️ Related ISSUE Number

close #69



📄 Work Description

1. 과제 관리 기능 개선

  • 통합 과제 폼 페이지 (AssignmentFormPage) 추가: 기존의 생성 페이지를 대체하고, 생성(/assignments/create)과 수정(/assignments/edit/:id) 경로 모두에서 사용할 수 있는 통합 컴포넌트를 구현했습니다.
  • TestcaseRow 컴포넌트 구현: 과제 테스트 케이스를 관리하기 위한 재사용 가능한 UI 컴포넌트를 추가했습니다.

2. 과제 API 및 스키마 업데이트

  • API 함수 및 Hook 추가: 과제 조회, 생성, 수정을 위한 API 함수를 정의하고, 이를 편리하게 사용할 수 있도록 Mutation/Query Hook을 노출했습니다.
  • 검증 스키마 도입: 폼 유효성 검사 및 API 연동을 위해 assignmentFormSchemaassignmentDetailSchema를 추가했습니다.

3. 코스 필터링 및 스키마 개선

  • 코스 필터링 훅 강화: useCourseFilter에서 ID와 라벨(이름) 간의 상호 매핑 기능을 제공하여, 코스 선택 및 데이터 조회를 간소화했습니다.
  • 코스 스키마 수정: 코스 설명(description) 필드가 null이 될 수 있도록 허용하고 기본값을 빈 문자열로 설정하여 데이터 일관성을 높였습니다.



📷 Screenshot



💬 To Reviewers



🔗 Reference

@vercel

vercel Bot commented Mar 12, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
snow-code-client Ready Ready Preview, Comment Mar 18, 2026 3:57pm

@coderabbitai

coderabbitai Bot commented Mar 12, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

관리자용 과제 생성/수정 UI를 AssignmentFormPage로 통합하고, 과제 관련 API(getAssignment, createAssignment, updateAssignment), 쿼리/뮤테이션, 스키마 및 라우트를 추가·수정했습니다.

Changes

Cohort / File(s) Summary
라우팅 및 앱 진입
src/App.tsx
AssignmentFormPage 추가 임포트; assignments/create, assignments/edit/:id 경로에 AssignmentFormPage 연결; courses/:id/editCourseEditPage 라우트 추가; CourseCreatePage 임포트 방식 변경.
Assignment API 레이어
src/entities/assignment/api/assignmentApi.ts
getAssignment, createAssignment, updateAssignment 엔드포인트 추가 및 응답을 assignmentDetailSchema로 파싱.
쿼리·뮤테이션
src/entities/assignment/api/assignmentQueries.ts, src/entities/assignment/api/assignmentMutations.ts
getAssignment 쿼리 추가; createAssignment/updateAssignment 뮤테이션 추가(타입 TAssignmentForm 사용).
스키마·타입
src/entities/assignment/model/schemas.ts
assignmentFormSchema, assignmentDetailSchema 및 타입 TAssignmentForm, TAssignmentDetail 추가; 기존 Assignment, AssignmentSchedule 타입 제거.
관리자 UI 컴포넌트
src/pages/admin/assignments/AssignmentFormPage.tsx, src/pages/admin/assignments/ui/TestcaseRow.tsx, src/widgets/assignment-form-layout/ui/AssignmentFormLayout.tsx
생성/수정 공용 AssignmentFormPage 구현(편집 모드: id 파라미터로 데이터 로드), TestcaseRowisHidden/onHiddenChange 추가, 레이아웃 prop 타입을 interface로 변경.
과정 관련 변경
src/pages/course-overview/ui/AssignmentList.tsx, src/pages/admin/courses/CourseEditPage.tsx, src/features/course/*
Assignment 링크를 id 기반 동적 라우팅으로 변경; CourseEditPage에 id 유효성 검사 및 Navigate 리다이렉트 적용; course schema/submit payload에서 description nullish 처리 보완.
공용 유틸·UI 개선
src/shared/lib/handleApiError.ts, src/shared/ui/ConfirmModal.tsx, src/shared/ui/LabeledInput.tsx, src/shared/config/endpoints.ts
API 에러 핸들러를 메시지 반환 → alert 호출로 변경(로깅 포함); ConfirmModal에 backdrop/ESC 닫기 추가; LabeledInput의 required 기본값 false로 변경; ENDPOINTS.ASSIGNMENTS에 ROOT 추가.
패키지 메타 데이터
package.json
eslint-plugin-jsx-a11y를 dependencies → devDependencies로 이동.
기타 스타일/유틸 리팩토링
src/features/course/filter-course/*, src/features/auth/kakao/*, src/pages/dashboard/ui/CourseManagementDropdown.tsx
course filter의 맵 구조 변경 및 타입 정리, 카카오 로그인에서 하드코딩 경로를 ROUTES 상수로 교체, 삭제 모달 중복 호출 방지 및 모달 닫기 동작 추가.

Sequence Diagram(s)

sequenceDiagram
  participant AdminUI as Admin UI (AssignmentFormPage)
  participant Queries as assignmentQueries / Mutations
  participant Api as assignmentApi
  participant Server as Backend API

  AdminUI->>Queries: (edit) getAssignment(assignmentId)
  Queries->>Api: getAssignment(assignmentId)
  Api->>Server: GET /assignments/{id}
  Server-->>Api: assignmentDetail
  Api-->>Queries: parsed assignmentDetail
  Queries-->>AdminUI: query result (populate form)

  AdminUI->>Queries: submit (create/update) with TAssignmentForm
  Queries->>Api: createAssignment(form) / updateAssignment(id, form)
  Api->>Server: POST /assignments or PUT /assignments/{id}
  Server-->>Api: assignmentDetail
  Api-->>Queries: parsed assignmentDetail
  Queries-->>AdminUI: mutation success (invalidate queries, navigate)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

🧩 feature, 🎨 ui

Suggested reviewers

  • suminb99
🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (1 warning, 2 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive 연결된 이슈 #69의 체크리스트가 placeholder(TODO)로만 구성되어 있어 구체적인 요구사항이 명시되지 않았습니다. 그러나 PR의 변경 사항들이 API 연동 및 폼 기능 구현이라는 일반적인 목표와는 일치합니다. 이슈 #69의 상세한 요구사항을 구체화하여 체크리스트를 업데이트하고, PR 검증 기준을 명확히 해주시기 바랍니다.
Out of Scope Changes check ❓ Inconclusive 대부분의 변경사항이 과제 API 연동 및 폼 기능과 직접적으로 관련되어 있으나, package.json의 eslint-plugin-jsx-a11y 이동, ConfirmModal의 UX 개선, LabeledInput의 required 기본값 변경 등 부가적인 개선사항들이 포함되어 있습니다. 과제 API 연동의 핵심 범위와 부가적인 개선사항(접근성, UX, 의존성 정리)을 분리하여 별도의 PR로 나누거나, 이슈 설명에서 이들 변경의 필요성을 명시해 주시기 바랍니다.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경사항을 명확히 반영하고 있습니다. 과제 등록 및 수정 API 연동이라는 핵심 목표를 효과적으로 요약하고 있습니다.
Description check ✅ Passed PR 설명이 변경사항과 관련이 있습니다. 과제 관리 기능 개선, API 및 스키마 업데이트, 코스 필터링 개선 등 실제 변경 사항을 구체적으로 설명하고 있습니다.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/69-assignments-add-edit
📝 Coding Plan
  • Generate coding plan for human review comments

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.

@JiiminHa JiiminHa changed the title Feat/69 assignments add edit #69 feat: 문제 등록 및 수정 api 연동 Mar 12, 2026
@JiiminHa JiiminHa self-assigned this Mar 12, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
src/pages/admin/assignments/AssignmentFormPage.tsx (2)

117-134: 배열 인덱스를 key로 사용하면 리렌더링 이슈가 발생할 수 있습니다.

테스트케이스가 삭제되거나 순서가 변경될 경우, 인덱스 기반 key는 React의 reconciliation을 혼란시킬 수 있습니다. 고유한 ID를 사용하거나, 항목 추가 시 고유 ID를 생성하는 것을 권장합니다.

♻️ 개선 예시 (nanoid 또는 uuid 사용)
+import {nanoid} from 'nanoid';

-const [testcases, setTestcases] = useState([{testcase: '', answer: ''}]);
+const [testcases, setTestcases] = useState([{id: nanoid(), testcase: '', answer: ''}]);

 const handleAddTestcase = () => {
-  setTestcases([...testcases, {testcase: '', answer: ''}]);
+  setTestcases([...testcases, {id: nanoid(), testcase: '', answer: ''}]);
 };

 // In JSX:
-<TestcaseRow key={idx} ...
+<TestcaseRow key={tc.id} ...

React 공식 문서 참고: Lists and Keys

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

In `@src/pages/admin/assignments/AssignmentFormPage.tsx` around lines 117 - 134,
The list is using the array index as the React key which can break
reconciliation when items are reordered or removed; update the TestcaseRow
mapping to use a stable unique identifier (e.g., testcase.id) instead of idx as
the key, and ensure new testcases created via whatever function adds to the
testcases state (the code paths that call setTestcases) generate and attach a
unique id (nanoid/uuid or incrementing id) to each testcase object so
TestcaseRow, onTestcaseChange and onAnswerChange continue to locate and update
items by id rather than by index.

144-148: FileUploadonFileChange 콜백이 비어 있습니다.

현재 빈 함수가 전달되어 파일 업로드 기능이 동작하지 않습니다. 추후 구현 예정이라면 TODO 주석을 추가하는 것이 좋습니다.

 <FileUpload
   label='테스트 케이스'
-  onFileChange={() => {}}
+  onFileChange={() => {}} // TODO: 파일 업로드 처리 구현 필요
   className='mb-9'
 />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/admin/assignments/AssignmentFormPage.tsx` around lines 144 - 148,
The FileUpload component is passed an empty onFileChange handler so uploaded
files aren't processed; implement a handler in AssignmentFormPage that accepts
the file or FileList from FileUpload's onFileChange and either updates local
state (e.g., setTestCaseFile) or updates the form state (e.g.,
formik.setFieldValue('testCases', files)), and pass that function to FileUpload,
or if you intentionally leave it unimplemented add a TODO comment explaining the
planned behavior; reference the FileUpload component and its onFileChange prop
in AssignmentFormPage to locate where to add the handler.
🤖 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/App.tsx`:
- Around line 45-49: Routes define path 'assignments/edit/:id' but navigation
uses '/admin/assignments/${id}' (missing 'edit'), so update the navigate calls
in AssignmentManageActionsBar (the function that builds the edit link) and
AssignmentList (where navigate is called with `${index}`) to use
'/admin/assignments/edit/${id}' and '/admin/assignments/edit/${index}'
respectively so the dynamic param matches the Route ('assignments/edit/:id');
ensure the template string variable names match the route param name 'id'.

In `@src/entities/assignment/model/schemas.ts`:
- Around line 34-46: The assignmentDetailSchema is missing the score field which
prevents getAssignment() responses from parsing score and the useEffect in
AssignmentFormPage (the effect that initializes form state from assignmentData)
from populating the form; add a numeric (or nullable numeric if appropriate)
score property to assignmentDetailSchema and update the useEffect in
AssignmentFormPage.tsx to set the form's score state from assignmentData.score
(ensure you reference assignmentDetailSchema and the same field name used
elsewhere so score flows from getAssignment() → assignmentData → form state).

In `@src/pages/admin/assignments/AssignmentFormPage.tsx`:
- Around line 32-43: In AssignmentFormPage's useEffect that syncs assignmentData
into the form, add a call to setScore(assignmentData.score) alongside setTitle,
setDescription and setTestcases so the score is populated in edit mode; also
ensure the backend response includes score and that assignmentDetailSchema has a
score field (update the schema in assignmentDetailSchema if necessary) so
assignmentData.score is available for use in the useEffect.

In `@src/pages/admin/assignments/ui/TestcaseRow.tsx`:
- Around line 39-43: The LabeledDropdown in TestcaseRow is not wired to state:
add an onSelect prop to LabeledDropdown (using the existing PUBLIC_OPTIONS) that
calls a handler passed from the parent to update the testcase visibility, and
update the parent (AssignmentFormPage) to include a visibility field on each
testcase in its testcases state/schema and provide a setter/handler (e.g.,
onUpdateTestcaseVisibility or updateTestcase) that TestcaseRow calls;
alternatively, if visibility isn't needed, remove the LabeledDropdown from
TestcaseRow and any PUBLIC_OPTIONS usage and related UI. Ensure you reference
LabeledDropdown, PUBLIC_OPTIONS, TestcaseRow, AssignmentFormPage, testcases,
visibility, and onSelect when applying these changes.

---

Nitpick comments:
In `@src/pages/admin/assignments/AssignmentFormPage.tsx`:
- Around line 117-134: The list is using the array index as the React key which
can break reconciliation when items are reordered or removed; update the
TestcaseRow mapping to use a stable unique identifier (e.g., testcase.id)
instead of idx as the key, and ensure new testcases created via whatever
function adds to the testcases state (the code paths that call setTestcases)
generate and attach a unique id (nanoid/uuid or incrementing id) to each
testcase object so TestcaseRow, onTestcaseChange and onAnswerChange continue to
locate and update items by id rather than by index.
- Around line 144-148: The FileUpload component is passed an empty onFileChange
handler so uploaded files aren't processed; implement a handler in
AssignmentFormPage that accepts the file or FileList from FileUpload's
onFileChange and either updates local state (e.g., setTestCaseFile) or updates
the form state (e.g., formik.setFieldValue('testCases', files)), and pass that
function to FileUpload, or if you intentionally leave it unimplemented add a
TODO comment explaining the planned behavior; reference the FileUpload component
and its onFileChange prop in AssignmentFormPage to locate where to add the
handler.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 697161e3-660d-4955-960d-004cdb15af11

📥 Commits

Reviewing files that changed from the base of the PR and between f864b1e and 08bb28b.

📒 Files selected for processing (9)
  • src/App.tsx
  • src/entities/assignment/api/assignmentApi.ts
  • src/entities/assignment/api/assignmentMutations.ts
  • src/entities/assignment/api/assignmentQueries.ts
  • src/entities/assignment/model/schemas.ts
  • src/pages/admin/assignments/AssignmentCreatePage.tsx
  • src/pages/admin/assignments/AssignmentFormPage.tsx
  • src/pages/admin/assignments/ui/TestcaseRow.tsx
  • src/shared/ui/LabeledInput.tsx
💤 Files with no reviewable changes (1)
  • src/pages/admin/assignments/AssignmentCreatePage.tsx

Comment thread src/App.tsx
Comment thread src/entities/assignment/model/schemas.ts
Comment thread src/pages/admin/assignments/AssignmentFormPage.tsx
Comment on lines +39 to +43
<LabeledDropdown
label={isFirst ? '공개 여부' : ''}
options={PUBLIC_OPTIONS}
className='w-full'
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

LabeledDropdown의 선택 값이 부모 컴포넌트와 연결되지 않았습니다.

"공개 여부" 드롭다운에 onSelect 콜백이 전달되지 않아 선택 값이 폼 상태에 반영되지 않습니다. 또한 부모 컴포넌트(AssignmentFormPage)의 testcases 상태에도 visibility 필드가 없습니다.

기능이 필요하다면 연결을, 아니라면 UI에서 제거를 권장합니다.

🔧 연결하려면

1. Props 확장:

 interface TestcaseRowProps {
   index: number;
   testcase: string;
   answer: string;
+  visibility: string;
   onTestcaseChange: (value: string) => void;
   onAnswerChange: (value: string) => void;
+  onVisibilityChange: (value: string) => void;
 }

2. 드롭다운에 콜백 연결:

 <LabeledDropdown
   label={isFirst ? '공개 여부' : ''}
   options={PUBLIC_OPTIONS}
   className='w-full'
+  onSelect={onVisibilityChange}
 />

3. 부모 컴포넌트와 스키마도 함께 수정 필요

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

In `@src/pages/admin/assignments/ui/TestcaseRow.tsx` around lines 39 - 43, The
LabeledDropdown in TestcaseRow is not wired to state: add an onSelect prop to
LabeledDropdown (using the existing PUBLIC_OPTIONS) that calls a handler passed
from the parent to update the testcase visibility, and update the parent
(AssignmentFormPage) to include a visibility field on each testcase in its
testcases state/schema and provide a setter/handler (e.g.,
onUpdateTestcaseVisibility or updateTestcase) that TestcaseRow calls;
alternatively, if visibility isn't needed, remove the LabeledDropdown from
TestcaseRow and any PUBLIC_OPTIONS usage and related UI. Ensure you reference
LabeledDropdown, PUBLIC_OPTIONS, TestcaseRow, AssignmentFormPage, testcases,
visibility, and onSelect when applying these changes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (4)
src/shared/ui/LabeledDropdown.tsx (1)

30-57: ⚠️ Potential issue | 🟠 Major

에러 메시지가 스크린리더에 연결되지 않아 접근성 누락이 있습니다.

현재는 에러 텍스트가 시각적으로만 보이고 버튼과 연결되지 않아 보조기기에서 오류 인지가 어렵습니다. aria-invalidaria-describedby를 추가해 주세요.
참고: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-describedby
참고: https://www.w3.org/WAI/WCAG22/Understanding/error-identification.html

♿ 제안 수정안
 const LabeledDropdown = ({
@@
   errorMessage,
   ...rest
 }: LabeledDropdownProps) => {
+  const errorId = rest.id ? `${rest.id}-error` : undefined;
+
   const handleSelect = (option: string) => {
     onSelect?.(option);
   };

   const TriggerButton = (
     <button
       type='button'
+      aria-invalid={Boolean(errorMessage)}
+      aria-describedby={errorId}
       className={`relative h-11 w-full rounded-[9px] border px-[14px] pr-10 text-left outline-none focus:border-primary ${
         errorMessage ? 'border-badge-red' : 'border-purple-stroke'
       } ${className ?? ''}`}
       {...rest}>
@@
-      {errorMessage && (
-        <span className='text-sm text-badge-red'>{errorMessage}</span>
-      )}
+      {errorMessage && (
+        <span id={errorId} role='alert' className='text-sm text-badge-red'>
+          {errorMessage}
+        </span>
+      )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/LabeledDropdown.tsx` around lines 30 - 57, The error text is
only visual and not exposed to screen readers; update the dropdown trigger
button (TriggerButton) to include aria-invalid={!!errorMessage} and
aria-describedby pointing to the error element's id, and give the error <span> a
stable id (e.g., `${id || label}-error` or a generated uniqueId prop) so the
Dropdown/TriggerButton and the errorMessage span are linked; ensure the same id
is passed into the button via its props (dropDownButton) so assistive tech will
announce the error.
src/features/auth/kakao/api/kakaoMutations.ts (1)

4-8: ⚠️ Potential issue | 🟡 Minor

studentId를 선택적 필드로 변경해주세요.

kakaoLogin API는 studentId?: string으로 설계되어 있으나, 인터페이스에서는 필수로 요구합니다. 실제 호출 코드(useKakaoLogin.ts)에서 state 값이 'ADMIN:' 같이 학번 없이 들어올 수 있으므로, 타입에서도 이를 자연스럽게 수용해야 합니다.

 interface KakaoLoginParams {
   code: string;
   role: 'ADMIN' | 'USER';
-  studentId: string;
+  studentId?: string;
 }

참고: TypeScript Optional Properties

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

In `@src/features/auth/kakao/api/kakaoMutations.ts` around lines 4 - 8, The
KakaoLoginParams interface currently requires studentId but the kakaoLogin API
and useKakaoLogin.ts can call it without a student ID (e.g., state like
'ADMIN:'), so update the KakaoLoginParams declaration (interface
KakaoLoginParams) to make studentId optional (studentId?: string) so callers
without a studentId type-check; ensure any validation that needs a studentId is
handled at runtime in kakaoLogin or its callers rather than in the type.
src/pages/manage-assignment/ui/AssignmentManageActionsBar.tsx (1)

29-33: ⚠️ Potential issue | 🟠 Major

아이콘 버튼에 접근성 라벨과 type="button"을 추가해 주세요.

현재 아이콘-only 버튼이라 스크린리더 문맥이 부족하고, type 미지정으로 폼 내부 재사용 시 의도치 않은 submit이 발생할 수 있습니다.

참고 문서:

수정 예시
-      <button onClick={handleOnEdit}>
+      <button type='button' aria-label='과제 수정' onClick={handleOnEdit}>
         <EditIcon className='w-3.5 h-4' />
       </button>
-      <button onClick={handleOnDelete}>
+      <button type='button' aria-label='과제 삭제' onClick={handleOnDelete}>
         <DeleteIcon className='w-3.5 h-4' />
       </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/manage-assignment/ui/AssignmentManageActionsBar.tsx` around lines
29 - 33, In AssignmentManageActionsBar, the icon-only buttons that call
handleOnEdit and handleOnDelete need accessibility labels and an explicit button
type to avoid accidental form submits and to give screen readers context; update
the two <button> elements wrapping EditIcon and DeleteIcon to include
type="button" and an appropriate aria-label (e.g., "Edit assignment" and "Delete
assignment") so the handlers handleOnEdit and handleOnDelete remain unchanged
while accessibility and form behavior are fixed.
src/pages/dashboard/ui/CourseCard.tsx (1)

51-59: ⚠️ Potential issue | 🟡 Minor

드롭다운의 위치와 구조를 정리해야 합니다.

현재 CourseManagementDropdown이 문제 수(<p>문제 수</p>)와 함께 동일한 relative 컨테이너 내부에 위치해 있어 마크업 구조가 혼란스럽습니다. 드롭다운이 절대 위치(absolute -right-2 -top-6)로 배치되어도, 같은 컨테이너 내 텍스트 콘텐츠와 함께 있으면 레이아웃 의도가 명확하지 않습니다.

개선 방안:

  1. 상단 우측에 독립적으로 배치: 드롭다운을 우측 섹션의 최상단(line 46의 바로 자식)으로 이동하여 텍스트 콘텐츠와 분리하세요.
  2. 레이아웃 재구성: 우측 섹션을 flex로 구성하여 드롭다운은 상단 우측, 통계 정보는 하단에 배치하면 더 명확합니다.

또한 lines 52-55의 들여쓰기가 불규칙해 보이니 코드 포맷팅도 함께 정리해주세요.

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

In `@src/pages/dashboard/ui/CourseCard.tsx` around lines 51 - 59, The
CourseManagementDropdown is nested inside the same relative container as the
stats (assignmentCount) which muddles layout; move the CourseManagementDropdown
(the element rendered when pathname.startsWith('/admin')) out to be the
first/top child of the right-side section (the element using class 'relative
pl-4 border-l-[0.5px]') so it is independent of the text, then convert that
right section to a column flex container (dropdown pinned to the top-right,
stats stacked below) so the dropdown sits visually at the top-right and the <p>
문제 수 / assignmentCount block is clearly below; also fix the inconsistent
indentation around those lines for consistent formatting.
🧹 Nitpick comments (12)
src/shared/ui/ConfirmModal.tsx (2)

24-28: Dialog 내부 클릭 시 이벤트 버블링 방지 필요

backdrop 클릭으로 닫기를 구현할 경우, dialog 내부 클릭이 backdrop으로 전파되지 않도록 해야 합니다.

🔧 수정 제안
      <section
        role='dialog'
        aria-modal='true'
        aria-labelledby='confirm-modal-title'
-       className='bg-white rounded-2xl p-8 w-80 flex flex-col gap-6 shadow-lg'>
+       className='bg-white rounded-2xl p-8 w-80 flex flex-col gap-6 shadow-lg'
+       onClick={(e) => e.stopPropagation()}>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/ConfirmModal.tsx` around lines 24 - 28, The dialog's inner
section (the element with role='dialog' and
aria-labelledby='confirm-modal-title' in ConfirmModal) needs to stop click event
propagation so clicks inside don't trigger the backdrop close; add an onClick
handler on that section (or its top-level dialog container) that calls
event.stopPropagation() to prevent bubbling to the backdrop click handler,
keeping the existing backdrop onClick logic intact.

12-47: 접근성 개선이 필요합니다: 포커스 트랩 및 ESC 키 핸들링

모달이 열릴 때 포커스가 모달 내부에 갇히고, ESC 키로 닫을 수 있어야 합니다. 현재 구현에서는 사용자가 Tab으로 모달 외부 요소에 접근할 수 있습니다.

🔧 ESC 키 핸들링 추가 예시
+import { useEffect, useRef } from 'react';

 export default function ConfirmModal({
   // ...props
 }: ConfirmModalProps) {
+  const dialogRef = useRef<HTMLElement>(null);
+
+  useEffect(() => {
+    const handleKeyDown = (e: KeyboardEvent) => {
+      if (e.key === 'Escape') onCancel();
+    };
+    document.addEventListener('keydown', handleKeyDown);
+    dialogRef.current?.focus();
+    return () => document.removeEventListener('keydown', handleKeyDown);
+  }, [onCancel]);
+
   return (
     // ...
+      <section
+        ref={dialogRef}
+        tabIndex={-1}
+        role='dialog'
         // ...

참고: WAI-ARIA Modal Dialog Pattern

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

In `@src/shared/ui/ConfirmModal.tsx` around lines 12 - 47, ConfirmModal lacks a
focus trap and ESC handling; update the ConfirmModal component to (1) store the
previously focused element when the modal mounts and restore focus when it
unmounts, (2) set initial focus to the modal container or the first focusable
button (use a ref like modalRef), (3) add a keydown listener on mount that
closes the modal when Escape is pressed by calling onCancel, and (4) implement
Tab/Shift+Tab trapping within the modal by preventing focus from escaping the
first/last focusable elements inside modalRef; also clean up listeners on
unmount. Use ConfirmModal, modalRef, onCancel and onConfirm identifiers to
locate where to add useEffect and ref logic.
eslint.config.js (1)

59-81: boundaries/entry-point 규칙이 'off'인데 상세 설정이 있습니다.

규칙이 비활성화되어 있어 하위 설정(rules 배열)이 적용되지 않습니다. 의도적으로 비활성화한 것이라면 주석으로 이유를 명시하거나, 규칙을 활성화하려면 'error' 또는 'warn'으로 변경하세요.

// 활성화하려면:
'boundaries/entry-point': [
  'error',  // 'off' → 'error' 또는 'warn'
  { rules: [...] }
]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eslint.config.js` around lines 59 - 81, The 'boundaries/entry-point' rule is
currently set to 'off' while still providing a rules configuration block, so
either enable the rule or remove/annotate the dead config; update the
'boundaries/entry-point' entry from 'off' to 'error' or 'warn' if you intend the
rules to apply (keeping the existing rules array), or leave it 'off' and add a
brief comment explaining why it's disabled or remove the nested rules entirely;
target the 'boundaries/entry-point' config block in eslint.config.js to make
this change.
src/widgets/assignment-form-layout/ui/AssignmentFormLayout.tsx (1)

4-13: 코딩 가이드라인: 객체 타입에는 interface 사용 권장

가이드라인에 따르면 "interface는 객체 타입, type은 union/alias에 사용"합니다. AssignmentFormLayoutProps는 객체 타입이므로 interface로 변경을 고려해보세요.

♻️ 선택적 리팩터링
-type AssignmentFormLayoutProps = {
+interface AssignmentFormLayoutProps {
   title: string;
   titleExtra?: React.ReactNode;
   content: React.ReactNode;
   onCancel: () => void;
   onConfirm: () => void;
   cancelLabel?: string;
   confirmLabel?: string;
   confirmDisabled?: boolean;
-};
+}

As per coding guidelines: "interface는 객체 타입, type은 union/alias에 사용"

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

In `@src/widgets/assignment-form-layout/ui/AssignmentFormLayout.tsx` around lines
4 - 13, AssignmentFormLayoutProps is declared with a type alias but your coding
guideline requires using interface for object shapes; change the declaration of
AssignmentFormLayoutProps from a type to an interface (keeping the same property
names: title, titleExtra, content, onCancel, onConfirm, cancelLabel,
confirmLabel, confirmDisabled), ensure any existing imports/exports and
references (e.g., in AssignmentFormLayout component props) are updated to use
the new interface name, and run a quick type-check to fix any places where
union/alias-specific syntax was mistakenly used so the interface compiles
cleanly.
src/features/course/filter-course/lib/useCourseFilter.ts (1)

41-47: 역방향 조회 로직은 동작하지만, 개선 여지가 있습니다.

현재 courseOptionMap.entries()를 순회하여 label을 찾습니다. 강의 수가 적어 성능 영향은 미미하지만, 양방향 Map을 유지하면 O(1) 조회가 가능합니다.

♻️ 선택적 개선안
 const courseOptionMap = useMemo(() => {
   const map = new Map<string, number>();
+  const reverseMap = new Map<number, string>();

   courses.forEach((course) => {
     const label = formatCourseOptionLabel(
       course.title,
       course.year,
       course.semester,
       course.section
     );
     map.set(label, course.id);
+    reverseMap.set(course.id, label);
   });

-  return map;
+  return { labelToId: map, idToLabel: reverseMap };
 }, [courses]);

 const selectedCourseLabel = useMemo(() => {
   if (selectedCourseId === null) return ALL_COURSES_OPTION;
-  for (const [key, value] of courseOptionMap.entries()) {
-    if (value === selectedCourseId) return key;
-  }
-  return ALL_COURSES_OPTION;
+  return courseOptionMap.idToLabel.get(selectedCourseId) ?? ALL_COURSES_OPTION;
 }, [selectedCourseId, courseOptionMap]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/course/filter-course/lib/useCourseFilter.ts` around lines 41 -
47, The current reverse-lookup in selectedCourseLabel iterates
courseOptionMap.entries() to find a label by selectedCourseId; instead maintain
an O(1) reverse map (e.g., courseLabelById) or keep a bidirectional structure so
selectedCourseLabel can directly lookup by id; update wherever courseOptionMap
is populated to also populate the reverse map and change the useMemo that
computes selectedCourseLabel (which references selectedCourseId and
courseOptionMap) to use the new reverse map for constant-time lookup and fall
back to ALL_COURSES_OPTION when null/missing.
src/shared/config/endpoints.ts (1)

10-15: ASSIGNMENTS.ROOT를 추가해 엔드포인트 단일 소스를 완성해 주세요.

현재 구조는 좋지만, src/entities/assignment/api/assignmentApi.ts Line [60]에서 여전히 '/assignments' 하드코딩이 필요해집니다. ROOT를 추가하면 일관성이 유지됩니다.

🗺️ 제안 수정안
   ASSIGNMENTS: {
+    ROOT: '/assignments',
     SCHEDULE: '/assignments/schedule',
     MY: '/assignments/my',
     DETAIL: (id: number | string) => `/assignments/${id}`,
     BY_COURSE: (courseId: number | string) => `/courses/${courseId}/assignments`,
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/config/endpoints.ts` around lines 10 - 15, Add a ROOT property to
the ASSIGNMENTS object (ASSIGNMENTS.ROOT = '/assignments') in the endpoints
definition so the base assignments path is a single source of truth, and then
replace any hardcoded '/assignments' usages (for example in assignmentApi where
the literal is used) with the shared constant (endpoints.ASSIGNMENTS.ROOT) to
keep endpoint references consistent.
src/features/course/filter-course/ui/CourseSelector.tsx (1)

3-11: Props 타입을 CourseSelectorProps로 분리해 주세요.

동작은 문제없지만, 현재 익명 객체 타입은 프로젝트 규칙과 불일치합니다. 명시적 Props 타입으로 분리하면 재사용/추론도 좋아집니다.

🧩 제안 수정안
+interface CourseSelectorProps {
+  options: string[];
+  value: string;
+  onSelect: (value: string) => void;
+}
+
 export const CourseSelector = ({
   options,
   value,
   onSelect,
-}: {
-  options: string[];
-  value: string;
-  onSelect: (value: string) => void;
-}) => {
+}: CourseSelectorProps) => {

As per coding guidelines "Props 타입: 컴포넌트명Props".

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

In `@src/features/course/filter-course/ui/CourseSelector.tsx` around lines 3 - 11,
Create an explicit Props type named CourseSelectorProps and replace the
anonymous inline type in the CourseSelector declaration: define
CourseSelectorProps with options: string[]; value: string; onSelect: (value:
string) => void; then update the CourseSelector signature to accept props:
CourseSelectorProps (e.g., export const CourseSelector = ({ options, value,
onSelect }: CourseSelectorProps) => { ... }). Ensure the new type name is
exported if the component is used elsewhere.
src/features/auth/kakao/model/useKakaoLogin.ts (1)

14-23: 라우팅 상수 사용을 성공/에러 분기까지 완전히 맞춰주세요.

같은 훅 내에서 문자열 경로와 ROUTES가 섞여 있어 유지보수 시 추적 비용이 늘어납니다. 한 번에 통일하는 편이 안전합니다.
참고 문서: https://reactrouter.com/api/hooks/useNavigate

리팩터 예시
     onSuccess: ({response}) => {
       const userType = response.role === 'ADMIN' ? 'admin' : 'student';
       login(response.name, userType, response.accessToken);
-      navigate(userType === 'admin' ? '/admin' : '/student');
+      navigate(
+        userType === 'admin' ? ROUTES.ADMIN.ROOT : ROUTES.STUDENT.ROOT
+      );
     },
     onError: (error) => {
       console.error('Login error:', error);
       alert('로그인에 실패했습니다. 다시 시도해주세요.');
-      navigate('/');
+      navigate(ROUTES.ROOT);
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/auth/kakao/model/useKakaoLogin.ts` around lines 14 - 23, The
onSuccess/onError handlers in useKakaoLogin mix hardcoded string routes with the
ROUTES constant; change navigate('/admin'), navigate('/student') and
navigate('/') to use the ROUTES constants (e.g. ROUTES.ADMIN, ROUTES.STUDENT,
ROUTES.HOME) so routing is consistent across the hook, keep the existing
userType/login logic (response.role check and login(response.name, userType,
response.accessToken)), and add/import the ROUTES symbol at the top of the file
if it isn’t already imported.
src/entities/assignment/api/assignmentApi.ts (1)

54-70: 신규 API도 ENDPOINTS 상수로 통일해 주세요.

getAssignment, createAssignment, updateAssignment에 문자열 경로가 남아 있어 같은 파일 내 패턴이 깨집니다. 경로 변경 시 누락 리스크가 생깁니다.
참고 문서: https://axios-http.com/docs/instance

리팩터 예시
 export const getAssignment = async (assignmentId: number) => {
-  const response = await privateAxios.get(`/assignments/${assignmentId}`);
+  const response = await privateAxios.get(
+    ENDPOINTS.ASSIGNMENTS.DETAIL(assignmentId)
+  );
   return apiResponseSchema(assignmentDetailSchema).parse(response.data);
 };
 ...
 export const updateAssignment = async (
   assignmentId: number,
   form: TAssignmentForm
 ) => {
-  const response = await privateAxios.put(`/assignments/${assignmentId}`, form);
+  const response = await privateAxios.put(
+    ENDPOINTS.ASSIGNMENTS.DETAIL(assignmentId),
+    form
+  );
   return apiResponseSchema(assignmentDetailSchema).parse(response.data);
 };

createAssignmentENDPOINTS.ASSIGNMENTS에 생성용 루트 상수를 추가해 같은 방식으로 맞추는 것을 권장합니다.

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

In `@src/entities/assignment/api/assignmentApi.ts` around lines 54 - 70, Replace
hard-coded route strings in getAssignment, createAssignment, and
updateAssignment with the shared ENDPOINTS constants to keep endpoints
consistent; use ENDPOINTS.ASSIGNMENTS for the collection POST (createAssignment)
and the collection GET/POST root, and construct the detail path for
getAssignment and updateAssignment by appending the assignmentId to the
appropriate ENDPOINTS constant (e.g.,
`${ENDPOINTS.ASSIGNMENTS}/${assignmentId}`) or introduce a new
ENDPOINTS.ASSIGNMENT_BY_ID pattern if your ENDPOINTS object prefers templated
keys; update the calls in getAssignment, createAssignment, updateAssignment to
use those ENDPOINTS symbols instead of string literals and add the new constant
to the ENDPOINTS definition if needed.
src/entities/course/model/schemas.ts (1)

13-13: description 기본값이 계약 오류까지 삼킵니다.

z.string().catch('')null/undefined 보정뿐 아니라 숫자·객체 같은 모든 검증 실패도 전부 빈 문자열로 바꿉니다. 같은 descriptiondashboardCourseSchema(25행)에서는 여전히 strict이어서, 엔드포인트마다 파싱 기준이 갈려버립니다.

null | undefined만 빈 문자열로 기본화하고 다른 타입 오류는 실패시키려면 Zod 공식 문서nullish() + transform() 패턴을 사용하세요:

-  description: z.string().catch(''),
+  description: z.string().nullish().transform((v) => v ?? ''),

이렇게 하면 null | undefined''로 변환되고, 실제 타입 불일치는 파싱 에러로 올바르게 캐치됩니다.

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

In `@src/entities/course/model/schemas.ts` at line 13, The description field
currently uses z.string().catch('') which swallows all validation errors; change
the schema for description (the description property in the course schema(s),
e.g., where description is defined alongside dashboardCourseSchema) to only
coerce null/undefined to '' and still fail on wrong types by using the
nullish()+transform() pattern from Zod (e.g., apply
z.string().nullish().transform(v => v ?? '') so only null/undefined become an
empty string while other type mismatches continue to produce validation errors).
src/entities/course/model/courseSchema.ts (1)

16-26: 응답 래퍼 스키마는 공통 헬퍼로 통일하는 편이 안전합니다.

현재 success/response 래퍼를 여기서 직접 정의하고 있는데, src/entities/course/api/courseApi.ts Line 15, Line 23, Line 29는 apiResponseSchema(...)를 사용합니다. 응답 래퍼가 이원화되면 추후 필드 변경 시 한쪽만 수정되는 드리프트가 생기기 쉽습니다. 공통 래퍼 헬퍼를 재사용해 한 곳에서 관리하는 쪽을 권장합니다.
참고: Zod object composition docs — https://zod.dev

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

In `@src/entities/course/model/courseSchema.ts` around lines 16 - 26, The response
wrapper is defined inline in createCourseResponseSchema causing duplication with
apiResponseSchema used in courseApi.ts; replace the inline wrapper by composing
the course payload with the shared helper apiResponseSchema — define a
coursePayload schema (fields id, title, section, year, semester:
semesterCodeSchema, description) and createCourseResponseSchema by calling
apiResponseSchema(coursePayload) so the wrapper is centralized and future
changes only need to be made in the shared helper.
src/shared/ui/LabeledInput.tsx (1)

19-19: required 기본값이 true입니다 — 의도된 설계인지 확인해주세요.

일반적으로 HTML input의 required 기본값은 false입니다. 프로젝트의 모든 입력 필드가 기본적으로 필수라면 괜찮지만, 그렇지 않다면 예상치 못한 유효성 검사 동작이 발생할 수 있습니다.

🔧 기본값 변경 제안 (필요한 경우)
-      required = true,
+      required = false,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/LabeledInput.tsx` at line 19, The prop default for required in
the LabeledInput component is set to true which deviates from HTML's default and
may cause unexpected validation; change the default to false (or explicitly
document/confirm the intentional behavior) by updating the LabeledInput
props/default parameter (the required prop) so it defaults to false instead of
true, and adjust any callers/tests that rely on the current default to
explicitly pass required when needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@package.json`:
- Line 21: The package "@types/eslint-plugin-jsx-a11y" is a type-only package
and must be moved out of "dependencies" into "devDependencies": remove the
"@types/eslint-plugin-jsx-a11y": "^6.10.1" entry from dependencies and add the
exact same entry under devDependencies (keeping the version string), then
reinstall/update the lockfile so package-lock.json/yarn.lock reflects the
change.

In `@src/features/course/edit-course/model/useEditCourse.ts`:
- Around line 29-37: The submit handler in useEditCourse currently always sends
students: [] which will overwrite existing students; update the submit function
(the mutate call in submit) to omit the students field when the form does not
provide student data—either remove students: [] entirely so the field is not
sent, or conditionally include students from the existing course state (merge
existingCourse.students) only when present; ensure you reference
CourseFormValues/SEMESTER_CODE_MAP and the mutate call in submit to locate and
change the payload accordingly.
- Around line 18-22: The onSuccess handler in useEditCourse currently only
invalidates courseQueries.getAllCourses; also invalidate the specific course
detail query so the CourseEditPage won't reuse stale detail cache—call
queryClient.invalidateQueries with the
courseQueries.getCourseDetails(courseId).queryKey (or the equivalent detail
query key) before navigate(ROUTES.ADMIN.ROOT) in the onSuccess block of the
mutation so both list and detail queries are invalidated.

In `@src/pages/admin/courses/CourseEditPage.tsx`:
- Around line 28-45: The loading branch (if (isLoading || !data)) masks errors
because !data is true on failure; modify the conditional ordering in
CourseEditPage so you check isError (and optionally error) first, returning the
error UI when isError is true, before evaluating isLoading or !data; update the
blocks that render the loading UI (using isLoading or !data) and the error UI
(using isError) accordingly so the error branch can run when the query fails.
- Around line 18-26: Move the id validation before calling hooks and prevent the
query/mutation from running with an enabled flag: compute and validate id (the
`id`/`courseId` conversion) before invoking useQuery/useEditCourse, pass
enabled: !!courseId (or similar) into the useQuery created by
courseQueries.getCourseDetails, and keep the mutation from being triggered until
id is valid; replace the runtime navigate(ROUTES.ADMIN.ROOT) call with rendering
React Router's <Navigate to={ROUTES.ADMIN.ROOT} /> when id is invalid so
redirection happens during render rather than as a side effect.

In `@src/pages/dashboard/ui/CourseManagementDropdown.tsx`:
- Around line 20-22: The confirm button currently allows re-clicks while a
delete is in-flight; update the click handler that calls handleDelete (from
useDeleteCourse) to immediately return/ignore if isPending is true and to
disable the confirm button via its disabled prop when isPending is true, and
modify the success callback passed into useDeleteCourse so it closes the modal
(instead of only showing alert) — reference useDeleteCourse, handleDelete,
isPending and the modal close function (e.g. closeModal / setShowModal) to
implement double-submit prevention and automatic modal dismissal on success.

In `@src/shared/lib/handleApiError.ts`:
- Around line 26-29: The current handleApiError function logs the entire error
object (console.error(error)), risking leakage of sensitive request/response
data; change it to build and log a minimal safe summary instead: in
handleApiError, extract only non-sensitive fields (e.g., status / statusCode,
name, and a short message from getApiErrorMessage(error) or fallbackMessage) and
log that summary object rather than the raw error; keep alert(message) unchanged
and ensure you reference handleApiError and getApiErrorMessage when implementing
the safeLog construction.

In `@src/shared/ui/ConfirmModal.tsx`:
- Around line 21-23: In ConfirmModal.tsx the backdrop div (the element with
className 'fixed inset-0 z-50 flex items-center justify-center bg-black/40')
incorrectly uses role='presentation'; remove that role attribute and either
leave the backdrop without a role (if purely decorative) or, if it should close
the modal on click, add an onClick handler that calls the modal close function
(and ensure keyboard accessibility by providing an explicit close button inside
the modal rather than giving the backdrop a semantic role). Ensure no other
elements rely on role='presentation' on the backdrop.

In `@src/widgets/course-form/ui/CourseForm.tsx`:
- Around line 90-93: The FileUpload's onFileChange is a no-op and selected files
are discarded; either (A) add file support by extending CourseFormProps with an
onFileChange callback, add a file field to courseFormSchema, wire FileUpload's
onFileChange to pass the file into the form (or call props.onFileChange) and
ensure the submit handler (e.g., handleSubmit) includes and processes that file,
or (B) hide/remove the <FileUpload> from CourseForm UI until file support is
implemented; locate the FileUpload usage, CourseFormProps definition,
courseFormSchema, and the form submit handler to apply the chosen fix.

---

Outside diff comments:
In `@src/features/auth/kakao/api/kakaoMutations.ts`:
- Around line 4-8: The KakaoLoginParams interface currently requires studentId
but the kakaoLogin API and useKakaoLogin.ts can call it without a student ID
(e.g., state like 'ADMIN:'), so update the KakaoLoginParams declaration
(interface KakaoLoginParams) to make studentId optional (studentId?: string) so
callers without a studentId type-check; ensure any validation that needs a
studentId is handled at runtime in kakaoLogin or its callers rather than in the
type.

In `@src/pages/dashboard/ui/CourseCard.tsx`:
- Around line 51-59: The CourseManagementDropdown is nested inside the same
relative container as the stats (assignmentCount) which muddles layout; move the
CourseManagementDropdown (the element rendered when
pathname.startsWith('/admin')) out to be the first/top child of the right-side
section (the element using class 'relative pl-4 border-l-[0.5px]') so it is
independent of the text, then convert that right section to a column flex
container (dropdown pinned to the top-right, stats stacked below) so the
dropdown sits visually at the top-right and the <p> 문제 수 / assignmentCount block
is clearly below; also fix the inconsistent indentation around those lines for
consistent formatting.

In `@src/pages/manage-assignment/ui/AssignmentManageActionsBar.tsx`:
- Around line 29-33: In AssignmentManageActionsBar, the icon-only buttons that
call handleOnEdit and handleOnDelete need accessibility labels and an explicit
button type to avoid accidental form submits and to give screen readers context;
update the two <button> elements wrapping EditIcon and DeleteIcon to include
type="button" and an appropriate aria-label (e.g., "Edit assignment" and "Delete
assignment") so the handlers handleOnEdit and handleOnDelete remain unchanged
while accessibility and form behavior are fixed.

In `@src/shared/ui/LabeledDropdown.tsx`:
- Around line 30-57: The error text is only visual and not exposed to screen
readers; update the dropdown trigger button (TriggerButton) to include
aria-invalid={!!errorMessage} and aria-describedby pointing to the error
element's id, and give the error <span> a stable id (e.g., `${id ||
label}-error` or a generated uniqueId prop) so the Dropdown/TriggerButton and
the errorMessage span are linked; ensure the same id is passed into the button
via its props (dropDownButton) so assistive tech will announce the error.

---

Nitpick comments:
In `@eslint.config.js`:
- Around line 59-81: The 'boundaries/entry-point' rule is currently set to 'off'
while still providing a rules configuration block, so either enable the rule or
remove/annotate the dead config; update the 'boundaries/entry-point' entry from
'off' to 'error' or 'warn' if you intend the rules to apply (keeping the
existing rules array), or leave it 'off' and add a brief comment explaining why
it's disabled or remove the nested rules entirely; target the
'boundaries/entry-point' config block in eslint.config.js to make this change.

In `@src/entities/assignment/api/assignmentApi.ts`:
- Around line 54-70: Replace hard-coded route strings in getAssignment,
createAssignment, and updateAssignment with the shared ENDPOINTS constants to
keep endpoints consistent; use ENDPOINTS.ASSIGNMENTS for the collection POST
(createAssignment) and the collection GET/POST root, and construct the detail
path for getAssignment and updateAssignment by appending the assignmentId to the
appropriate ENDPOINTS constant (e.g.,
`${ENDPOINTS.ASSIGNMENTS}/${assignmentId}`) or introduce a new
ENDPOINTS.ASSIGNMENT_BY_ID pattern if your ENDPOINTS object prefers templated
keys; update the calls in getAssignment, createAssignment, updateAssignment to
use those ENDPOINTS symbols instead of string literals and add the new constant
to the ENDPOINTS definition if needed.

In `@src/entities/course/model/courseSchema.ts`:
- Around line 16-26: The response wrapper is defined inline in
createCourseResponseSchema causing duplication with apiResponseSchema used in
courseApi.ts; replace the inline wrapper by composing the course payload with
the shared helper apiResponseSchema — define a coursePayload schema (fields id,
title, section, year, semester: semesterCodeSchema, description) and
createCourseResponseSchema by calling apiResponseSchema(coursePayload) so the
wrapper is centralized and future changes only need to be made in the shared
helper.

In `@src/entities/course/model/schemas.ts`:
- Line 13: The description field currently uses z.string().catch('') which
swallows all validation errors; change the schema for description (the
description property in the course schema(s), e.g., where description is defined
alongside dashboardCourseSchema) to only coerce null/undefined to '' and still
fail on wrong types by using the nullish()+transform() pattern from Zod (e.g.,
apply z.string().nullish().transform(v => v ?? '') so only null/undefined become
an empty string while other type mismatches continue to produce validation
errors).

In `@src/features/auth/kakao/model/useKakaoLogin.ts`:
- Around line 14-23: The onSuccess/onError handlers in useKakaoLogin mix
hardcoded string routes with the ROUTES constant; change navigate('/admin'),
navigate('/student') and navigate('/') to use the ROUTES constants (e.g.
ROUTES.ADMIN, ROUTES.STUDENT, ROUTES.HOME) so routing is consistent across the
hook, keep the existing userType/login logic (response.role check and
login(response.name, userType, response.accessToken)), and add/import the ROUTES
symbol at the top of the file if it isn’t already imported.

In `@src/features/course/filter-course/lib/useCourseFilter.ts`:
- Around line 41-47: The current reverse-lookup in selectedCourseLabel iterates
courseOptionMap.entries() to find a label by selectedCourseId; instead maintain
an O(1) reverse map (e.g., courseLabelById) or keep a bidirectional structure so
selectedCourseLabel can directly lookup by id; update wherever courseOptionMap
is populated to also populate the reverse map and change the useMemo that
computes selectedCourseLabel (which references selectedCourseId and
courseOptionMap) to use the new reverse map for constant-time lookup and fall
back to ALL_COURSES_OPTION when null/missing.

In `@src/features/course/filter-course/ui/CourseSelector.tsx`:
- Around line 3-11: Create an explicit Props type named CourseSelectorProps and
replace the anonymous inline type in the CourseSelector declaration: define
CourseSelectorProps with options: string[]; value: string; onSelect: (value:
string) => void; then update the CourseSelector signature to accept props:
CourseSelectorProps (e.g., export const CourseSelector = ({ options, value,
onSelect }: CourseSelectorProps) => { ... }). Ensure the new type name is
exported if the component is used elsewhere.

In `@src/shared/config/endpoints.ts`:
- Around line 10-15: Add a ROOT property to the ASSIGNMENTS object
(ASSIGNMENTS.ROOT = '/assignments') in the endpoints definition so the base
assignments path is a single source of truth, and then replace any hardcoded
'/assignments' usages (for example in assignmentApi where the literal is used)
with the shared constant (endpoints.ASSIGNMENTS.ROOT) to keep endpoint
references consistent.

In `@src/shared/ui/ConfirmModal.tsx`:
- Around line 24-28: The dialog's inner section (the element with role='dialog'
and aria-labelledby='confirm-modal-title' in ConfirmModal) needs to stop click
event propagation so clicks inside don't trigger the backdrop close; add an
onClick handler on that section (or its top-level dialog container) that calls
event.stopPropagation() to prevent bubbling to the backdrop click handler,
keeping the existing backdrop onClick logic intact.
- Around line 12-47: ConfirmModal lacks a focus trap and ESC handling; update
the ConfirmModal component to (1) store the previously focused element when the
modal mounts and restore focus when it unmounts, (2) set initial focus to the
modal container or the first focusable button (use a ref like modalRef), (3) add
a keydown listener on mount that closes the modal when Escape is pressed by
calling onCancel, and (4) implement Tab/Shift+Tab trapping within the modal by
preventing focus from escaping the first/last focusable elements inside
modalRef; also clean up listeners on unmount. Use ConfirmModal, modalRef,
onCancel and onConfirm identifiers to locate where to add useEffect and ref
logic.

In `@src/shared/ui/LabeledInput.tsx`:
- Line 19: The prop default for required in the LabeledInput component is set to
true which deviates from HTML's default and may cause unexpected validation;
change the default to false (or explicitly document/confirm the intentional
behavior) by updating the LabeledInput props/default parameter (the required
prop) so it defaults to false instead of true, and adjust any callers/tests that
rely on the current default to explicitly pass required when needed.

In `@src/widgets/assignment-form-layout/ui/AssignmentFormLayout.tsx`:
- Around line 4-13: AssignmentFormLayoutProps is declared with a type alias but
your coding guideline requires using interface for object shapes; change the
declaration of AssignmentFormLayoutProps from a type to an interface (keeping
the same property names: title, titleExtra, content, onCancel, onConfirm,
cancelLabel, confirmLabel, confirmDisabled), ensure any existing imports/exports
and references (e.g., in AssignmentFormLayout component props) are updated to
use the new interface name, and run a quick type-check to fix any places where
union/alias-specific syntax was mistakenly used so the interface compiles
cleanly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 74437900-abdc-4b83-a2e9-96fadf99d4b9

📥 Commits

Reviewing files that changed from the base of the PR and between 08bb28b and a0ddd08.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (45)
  • eslint.config.js
  • package.json
  • src/App.tsx
  • src/entities/assignment/api/assignmentApi.ts
  • src/entities/assignment/index.ts
  • src/entities/auth/api/authApi.ts
  • src/entities/auth/model/schemas.ts
  • src/entities/course/api/courseApi.ts
  • src/entities/course/index.ts
  • src/entities/course/model/courseSchema.ts
  • src/entities/course/model/schemas.ts
  • src/entities/course/model/types.ts
  • src/entities/unit/api/unitApi.ts
  • src/features/auth/kakao/api/kakaoMutations.ts
  • src/features/auth/kakao/model/useKakaoLogin.ts
  • src/features/course/create-course/model/courseFormSchema.ts
  • src/features/course/create-course/model/useCreateCourse.ts
  • src/features/course/delete-course/model/useDeleteCourse.ts
  • src/features/course/edit-course/model/useEditCourse.ts
  • src/features/course/filter-course/lib/useCourseFilter.ts
  • src/features/course/filter-course/ui/CourseSelector.tsx
  • src/pages/admin/assignments/AssignmentCreatePage.tsx
  • src/pages/admin/courses/CourseCreatePage.tsx
  • src/pages/admin/courses/CourseEditPage.tsx
  • src/pages/common/LandingPage.tsx
  • src/pages/common/UserIdInputPage.tsx
  • src/pages/dashboard/Dashboard.tsx
  • src/pages/dashboard/ui/CourseCard.tsx
  • src/pages/dashboard/ui/CourseList.tsx
  • src/pages/dashboard/ui/CourseManagementDropdown.tsx
  • src/pages/manage-assignment/AssignmentManagePage.tsx
  • src/pages/manage-assignment/ui/AssignmentManageActionsBar.tsx
  • src/pages/select-assignment/AssignmentSelectPage.tsx
  • src/pages/unit-editor/ui/UnitForm.tsx
  • src/shared/config/endpoints.ts
  • src/shared/config/routes.ts
  • src/shared/lib/handleApiError.ts
  • src/shared/ui/BaseHeader.tsx
  • src/shared/ui/ConfirmModal.tsx
  • src/shared/ui/Header.tsx
  • src/shared/ui/LabeledDropdown.tsx
  • src/shared/ui/LabeledInput.tsx
  • src/widgets/assignment-form-layout/ui/AssignmentFormLayout.tsx
  • src/widgets/assignment-page-layout/ui/AssignmentPageLayout.tsx
  • src/widgets/course-form/ui/CourseForm.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/pages/admin/assignments/AssignmentCreatePage.tsx

Comment thread package.json Outdated
Comment thread src/features/course/edit-course/model/useEditCourse.ts
Comment on lines +29 to +37
const submit = (data: CourseFormValues) => {
mutate({
title: data.title,
section: data.section,
year: Number(data.year),
semester: SEMESTER_CODE_MAP[data.semester],
description: data.description,
students: [],
});

Copy link
Copy Markdown

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:

#!/bin/bash
rg -n -C2 'students:\s*\[\]' src
rg -n -C2 'useEditCourse|getCourseDetails|getCourseById' src

Repository: 2025-snowCode/snowCode-Client

Length of output: 4261


🏁 Script executed:

#!/bin/bash
# useEditCourse.ts 전체 내용 확인
cat -n src/features/course/edit-course/model/useEditCourse.ts

# courseApi.ts에서 edit/update 관련 메서드 확인
cat -n src/entities/course/api/courseApi.ts

# CourseEditPage에서 데이터 흐름 확인
cat -n src/pages/admin/courses/CourseEditPage.tsx

Repository: 2025-snowCode/snowCode-Client

Length of output: 6631


🏁 Script executed:

#!/bin/bash
# CreateCourseRequest 타입 정의 확인
rg -n -A10 'type CreateCourseRequest|interface CreateCourseRequest' src

# 강의 스키마 정의 확인
find src -name '*courseSchema*' -o -name '*course*schema*' | head -10

Repository: 2025-snowCode/snowCode-Client

Length of output: 1324


🏁 Script executed:

#!/bin/bash
cat -n src/entities/course/model/courseSchema.ts

Repository: 2025-snowCode/snowCode-Client

Length of output: 1183


기존 수강생 정보가 빈 배열로 덮어써질 위험이 있습니다.

students 필드가 스키마에서 .optional()로 정의되어 있음에도 코드에서는 students: []를 명시적으로 전송합니다. 이것은 중요한 차이입니다:

  • .optional()은 필드를 생략 가능하다는 의미
  • students: []는 필드를 빈 배열로 명시적으로 전송한다는 의미

현재 상황:

  • CourseEditPage에서 기존 강의 정보를 조회하지만 students 데이터는 useEditCourse에 전달되지 않음
  • 폼에 수강생 관련 필드가 없어서 사용자가 수정할 수 없음
  • PUT 요청이 항상 students: []를 포함

권장사항:

  1. 필드 생략 (추천): students 필드를 요청에서 제거하면 선택사항이므로 서버가 기존 데이터를 보존할 가능성 높음
  2. 데이터 보존: 기존 students 목록을 유지해야 한다면, courseId로 조회한 데이터의 students를 merge해서 전송
  3. API 확인: 서버의 PUT 엔드포인트 동작 명확히 - students 필드를 무시하는지 전체 교체하는지

form에 students 편집 UI가 없다면, 필드를 생략하는 것이 가장 간단합니다.

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

In `@src/features/course/edit-course/model/useEditCourse.ts` around lines 29 - 37,
The submit handler in useEditCourse currently always sends students: [] which
will overwrite existing students; update the submit function (the mutate call in
submit) to omit the students field when the form does not provide student
data—either remove students: [] entirely so the field is not sent, or
conditionally include students from the existing course state (merge
existingCourse.students) only when present; ensure you reference
CourseFormValues/SEMESTER_CODE_MAP and the mutate call in submit to locate and
change the payload accordingly.

Comment thread src/pages/admin/courses/CourseEditPage.tsx
Comment thread src/pages/admin/courses/CourseEditPage.tsx Outdated
Comment thread src/pages/dashboard/ui/CourseManagementDropdown.tsx
Comment thread src/shared/lib/handleApiError.ts Outdated
Comment thread src/shared/ui/ConfirmModal.tsx Outdated
Comment on lines +90 to +93
<FileUpload
label='강의 공유'
onFileChange={() => {}}
className='mb-9'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd src && find . -name "CourseForm.tsx" -type f

Repository: 2025-snowCode/snowCode-Client

Length of output: 112


🏁 Script executed:

cat -n src/widgets/course-form/ui/CourseForm.tsx

Repository: 2025-snowCode/snowCode-Client

Length of output: 3851


🏁 Script executed:

cat -n src/features/course/create-course/model/courseFormSchema.ts

Repository: 2025-snowCode/snowCode-Client

Length of output: 1408


🏁 Script executed:

cat -n src/shared/ui/FileUpload.tsx

Repository: 2025-snowCode/snowCode-Client

Length of output: 4449


파일 업로드가 폼 제출과 연결되지 않습니다.

현재 상황:

  • Line 92의 onFileChange={() => {}} 때문에 사용자가 선택한 파일이 버려집니다
  • courseFormSchema에 파일 필드가 없어 제출 페이로드에 포함될 수 없습니다
  • FileUpload 컴포넌트는 제대로 작동하지만, 상위 폼에서 콜백을 받지 않습니다

다음 중 하나를 선택하세요:

  1. 파일 필드를 완전히 지원하려면: CourseFormPropsonFileChange 콜백을 추가하고, 스키마에 파일 필드를 추가한 후 제출 시 처리하세요
  2. 현재 미구현 상태라면: UI를 숨겨 사용자 혼동을 방지하세요

참고: React Hook Form과 파일 입력 처리

🔧 제안: 파일 콜백 노출 (1단계)
 interface CourseFormProps {
   defaultValues?: Partial<CourseFormValues>;
   onSubmit: (data: CourseFormValues) => void;
+  onFileChange?: (file: File | null) => void;
 }
 
-export const CourseForm = forwardRef<HTMLFormElement, CourseFormProps>(
-  ({defaultValues, onSubmit}, ref) => {
+export const CourseForm = forwardRef<HTMLFormElement, CourseFormProps>(
+  ({defaultValues, onSubmit, onFileChange}, ref) => {
 
         <FileUpload
           label='강의 공유'
-          onFileChange={() => {}}
+          onFileChange={(file) => onFileChange?.(file)}
           className='mb-9'
         />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/widgets/course-form/ui/CourseForm.tsx` around lines 90 - 93, The
FileUpload's onFileChange is a no-op and selected files are discarded; either
(A) add file support by extending CourseFormProps with an onFileChange callback,
add a file field to courseFormSchema, wire FileUpload's onFileChange to pass the
file into the form (or call props.onFileChange) and ensure the submit handler
(e.g., handleSubmit) includes and processes that file, or (B) hide/remove the
<FileUpload> from CourseForm UI until file support is implemented; locate the
FileUpload usage, CourseFormProps definition, courseFormSchema, and the form
submit handler to apply the chosen fix.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/entities/assignment/api/assignmentQueries.ts (1)

4-4: getAssignment 이름 충돌은 별칭으로 분리하면 더 읽기 좋아집니다.

Line 4 import와 Line 39 프로퍼티 이름이 같아서, Line 42에서 참조 대상을 한 번 더 추적하게 됩니다. 작은 변경으로 가독성을 높일 수 있어요.

🔧 제안 코드
 import {
   getAllAssignments,
-  getAssignment,
+  getAssignment as fetchAssignment,
   getAssignmentsByCourse,
   getAssignmentSchedules,
 } from '@/entities/assignment/api/assignmentApi';
@@
   getAssignment: (assignmentId: number) =>
     queryOptions({
       queryKey: ['assignments', assignmentId],
-      queryFn: () => getAssignment(assignmentId),
+      queryFn: () => fetchAssignment(assignmentId),
       select: (data) => data.response,
     }),

Also applies to: 39-43

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

In `@src/entities/assignment/api/assignmentQueries.ts` at line 4, The imported
symbol getAssignment conflicts with the local/exported property named
getAssignment, making references ambiguous; fix by aliasing the import (e.g.,
import { getAssignment as fetchAssignment } or as getAssignmentApi) and update
the export/object property to use that alias (or alternatively rename the
exported property and all local references) so references to getAssignment in
this module are unambiguous; ensure you update every use of the original
identifier in this file (places that call getAssignment, the export object
around the current property at/near the existing getAssignment entry, and any
internal callers) to the chosen alias.
src/entities/assignment/api/assignmentMutations.ts (1)

17-23: 업데이트 변수 객체 타입을 interface로 분리해 주세요.

Line 20-23의 인라인 객체 타입은 재사용성과 가독성 면에서 개선의 여지가 있습니다. 프로젝트 컨벤션에서 객체 타입은 interface로 정의하도록 안내하고 있으므로, UpdateAssignmentVariables 인터페이스로 추출하면 의도가 더 명확해질 겁니다.

참고: TypeScript Handbook - Interfaces
https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#interfaces

🔧 제안 코드
 import type {TAssignmentForm} from '@/entities/assignment/model/schemas';
 import {createAssignment,updateAssignment,deleteAssignment} from '@/entities/assignment/api/assignmentApi';

+interface UpdateAssignmentVariables {
+  assignmentId: number;
+  form: TAssignmentForm;
+}
+
 export const assignmentMutations = {
   updateAssignment: {
     mutationKey: ['updateAssignment'],
-    mutationFn: ({
-      assignmentId,
-      form,
-    }: {
-      assignmentId: number;
-      form: TAssignmentForm;
-    }) => updateAssignment(assignmentId, form),
+    mutationFn: ({assignmentId, form}: UpdateAssignmentVariables) =>
+      updateAssignment(assignmentId, form),
   },
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/entities/assignment/api/assignmentMutations.ts` around lines 17 - 23,
Extract the inline parameter type used in mutationFn into a named interface
UpdateAssignmentVariables and use it in the mutationFn signature; specifically,
create interface UpdateAssignmentVariables { assignmentId: number; form:
TAssignmentForm } (adjacent to or above mutationFn) and replace the inline type
for the parameters of mutationFn with UpdateAssignmentVariables so the call to
updateAssignment(assignmentId, form) keeps the same semantics but the variables
type is reusable and clearer.
🤖 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/entities/assignment/api/assignmentMutations.ts`:
- Around line 17-23: Extract the inline parameter type used in mutationFn into a
named interface UpdateAssignmentVariables and use it in the mutationFn
signature; specifically, create interface UpdateAssignmentVariables {
assignmentId: number; form: TAssignmentForm } (adjacent to or above mutationFn)
and replace the inline type for the parameters of mutationFn with
UpdateAssignmentVariables so the call to updateAssignment(assignmentId, form)
keeps the same semantics but the variables type is reusable and clearer.

In `@src/entities/assignment/api/assignmentQueries.ts`:
- Line 4: The imported symbol getAssignment conflicts with the local/exported
property named getAssignment, making references ambiguous; fix by aliasing the
import (e.g., import { getAssignment as fetchAssignment } or as
getAssignmentApi) and update the export/object property to use that alias (or
alternatively rename the exported property and all local references) so
references to getAssignment in this module are unambiguous; ensure you update
every use of the original identifier in this file (places that call
getAssignment, the export object around the current property at/near the
existing getAssignment entry, and any internal callers) to the chosen alias.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5f671227-068f-4079-932b-3b78f6402dc3

📥 Commits

Reviewing files that changed from the base of the PR and between a0ddd08 and 392a480.

📒 Files selected for processing (4)
  • src/entities/assignment/api/assignmentApi.ts
  • src/entities/assignment/api/assignmentMutations.ts
  • src/entities/assignment/api/assignmentQueries.ts
  • src/pages/course-overview/ui/AssignmentList.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/entities/assignment/api/assignmentApi.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/entities/assignment/api/assignmentApi.ts (1)

56-78: 엔드포인트 상수화 제안

코드에서 getAssignmentupdateAssignmentENDPOINTS.ASSIGNMENTS.DETAIL()를 활용하는 것이 좋습니다. 이렇게 하면 파일 내 일관성이 유지되고, 엔드포인트 변경 시 한곳에서만 수정할 수 있어 유지보수성이 향상됩니다.

다만 createAssignment/assignments 엔드포인트는 현재 ENDPOINTS 상수에 정의되어 있지 않습니다. 다음 두 가지 방안을 제안합니다:

  1. 권장: @/shared/config/endpointsENDPOINTS.ASSIGNMENTS.ROOT 또는 BASE 상수를 추가 후 사용
  2. 현재 상태 유지: 기본 경로는 하드코딩하되, 상세 조회/수정은 상수화
♻️ 일관성을 위한 리팩토링 예시
 // 과제 조회 API
 export const getAssignment = async (assignmentId: number) => {
-  const response = await privateAxios.get(`/assignments/${assignmentId}`);
+  const response = await privateAxios.get(ENDPOINTS.ASSIGNMENTS.DETAIL(assignmentId));
   const parsed = apiResponseSchema(assignmentDetailSchema).parse(response.data);
   return parsed.response;
 };

 // 과제 추가 API
 export const createAssignment = async (form: TAssignmentForm) => {
-  const response = await privateAxios.post('/assignments', form);
+  const response = await privateAxios.post(ENDPOINTS.ASSIGNMENTS.ROOT, form);
   const parsed = apiResponseSchema(assignmentDetailSchema).parse(response.data);
   return parsed.response;
 };

 // 과제 수정 API
 export const updateAssignment = async (
   assignmentId: number,
   form: TAssignmentForm
 ) => {
-  const response = await privateAxios.put(`/assignments/${assignmentId}`, form);
+  const response = await privateAxios.put(ENDPOINTS.ASSIGNMENTS.DETAIL(assignmentId), form);
   const parsed = apiResponseSchema(assignmentDetailSchema).parse(response.data);
   return parsed.response;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/entities/assignment/api/assignmentApi.ts` around lines 56 - 78, Replace
hardcoded assignment URLs with the centralized endpoint constants: change
getAssignment and updateAssignment to use
ENDPOINTS.ASSIGNMENTS.DETAIL(assignmentId) and replace the '/assignments'
literal in createAssignment with a new ENDPOINTS.ASSIGNMENTS.ROOT (or BASE)
constant; add ENDPOINTS.ASSIGNMENTS.ROOT to the endpoints module (e.g.,
'@/shared/config/endpoints') and update createAssignment to post to that
constant to keep all assignment routes consistent and maintainable.
🤖 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/entities/assignment/api/assignmentApi.ts`:
- Around line 56-78: Replace hardcoded assignment URLs with the centralized
endpoint constants: change getAssignment and updateAssignment to use
ENDPOINTS.ASSIGNMENTS.DETAIL(assignmentId) and replace the '/assignments'
literal in createAssignment with a new ENDPOINTS.ASSIGNMENTS.ROOT (or BASE)
constant; add ENDPOINTS.ASSIGNMENTS.ROOT to the endpoints module (e.g.,
'@/shared/config/endpoints') and update createAssignment to post to that
constant to keep all assignment routes consistent and maintainable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e55a47cc-5106-47b8-8466-df513318ebf6

📥 Commits

Reviewing files that changed from the base of the PR and between 392a480 and fe7f48b.

📒 Files selected for processing (2)
  • src/entities/assignment/api/assignmentApi.ts
  • src/entities/assignment/api/assignmentQueries.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/entities/assignment/api/assignmentQueries.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (3)
src/features/course/filter-course/lib/useCourseFilter.ts (1)

33-41: handleCourseSelectuseCallback 적용을 권장합니다.

현재 handleCourseSelect는 일반 함수로 선언되어 있어 매 렌더링마다 새로운 함수 참조가 생성됩니다. AssignmentManagePage 등에서 이 함수를 자식 컴포넌트에 prop으로 전달할 경우, 불필요한 리렌더링이 발생할 수 있습니다.

♻️ useCallback 적용 제안
+import {useCallback, useMemo, useState} from 'react';
-import {useMemo, useState} from 'react';
-  const handleCourseSelect = (value: string) => {
+  const handleCourseSelect = useCallback((value: string) => {
     if (value === ALL_COURSES_OPTION) {
       setSelectedCourseId(null);
       return;
     }

     const courseId = courseMaps.labelToId.get(value) ?? null;
     setSelectedCourseId(courseId);
-  };
+  }, [courseMaps.labelToId]);

📚 참고: React useCallback 공식 문서

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

In `@src/features/course/filter-course/lib/useCourseFilter.ts` around lines 33 -
41, The handler handleCourseSelect is recreated on every render which can cause
unnecessary re-renders when passed to children; wrap it with React.useCallback
and memoize it by including its dependencies [setSelectedCourseId,
courseMaps.labelToId] (and ALL_COURSES_OPTION if not a constant) so the function
identity is stable; update the declaration of handleCourseSelect to use
useCallback(...) and keep the same internal logic (check ALL_COURSES_OPTION,
lookup courseMaps.labelToId, call setSelectedCourseId).
src/shared/ui/ConfirmModal.tsx (1)

21-23: Dismiss 제어 옵션을 추가해 비동기 진행 중 닫힘을 막는 편이 안전합니다.
Escape/backdrop이 항상 onCancel을 호출해서, 삭제처럼 진행 중인 작업에서 모달이 먼저 닫힐 수 있습니다. isDismissible(기본 true) 같은 prop으로 Escape/backdrop/취소 버튼 동작을 일괄 제어해 주세요.
참고: WAI-ARIA Modal Dialog 패턴 — https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/

🔧 제안 수정안
 interface ConfirmModalProps {
   title: string;
   description?: string;
   confirmLabel?: string;
   cancelLabel?: string;
   onConfirm: () => void;
   onCancel: () => void;
+  isDismissible?: boolean;
 }

 export default function ConfirmModal({
   title,
   description,
   confirmLabel = '확인',
   cancelLabel = '취소',
   onConfirm,
   onCancel,
+  isDismissible = true,
 }: ConfirmModalProps) {
   useEffect(() => {
     const handleEsc = (e: KeyboardEvent) => {
-      if (e.key === 'Escape') onCancel();
+      if (e.key === 'Escape' && isDismissible) onCancel();
     };
     window.addEventListener('keydown', handleEsc);
     return () => window.removeEventListener('keydown', handleEsc);
-  }, [onCancel]);
+  }, [onCancel, isDismissible]);

   return (
     <div
       className='fixed inset-0 z-50 flex items-center justify-center bg-black/40'
-      onClick={onCancel}>
+      onClick={isDismissible ? onCancel : undefined}>
       <section
         role='dialog'
         aria-modal='true'
         aria-labelledby='confirm-modal-title'
         className='bg-white rounded-2xl p-8 w-80 flex flex-col gap-6 shadow-lg'
         onClick={(e) => e.stopPropagation()}>
@@
-          <Button color='secondary' onClick={onCancel}>
+          <Button color='secondary' onClick={() => isDismissible && onCancel()}>
             {cancelLabel}
           </Button>

Also applies to: 32-32

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

In `@src/shared/ui/ConfirmModal.tsx` around lines 21 - 23, The ConfirmModal should
add an isDismissible prop (default true) and make all dismissal paths respect
it: update the Escape handler in the useEffect (handleEsc), the backdrop/click
handler, and the cancel button click handler inside ConfirmModal to no-op when
isDismissible is false so ongoing async actions cannot close the modal; ensure
prop is optional with default true and document that it controls
Escape/backdrop/cancel dismissal behavior.
src/entities/course/model/schemas.ts (1)

16-19: description 필드 중복 정의 검토 필요

courseBaseSchemacourseCoreSchema.extend()를 사용하므로, 이미 courseCoreSchemadescription 변환 로직을 상속받습니다. Line 18의 재정의는 동일한 변환 로직이라 불필요할 수 있습니다.

의도적으로 명시성을 위해 남겨둔 것이라면 주석으로 명확히 해주시고, 그렇지 않다면 제거를 고려해주세요.

♻️ 중복 제거 제안
 /** 강의 기본 정보 (ID 포함) */
 export const courseBaseSchema = courseCoreSchema.extend({
   id: z.number(),
-  description: z.string().nullish().transform((v) => v ?? ''), // 응답에서는 기본값 처리
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/entities/course/model/schemas.ts` around lines 16 - 19, courseBaseSchema
currently redefines the description transform that's already provided by
courseCoreSchema.extend(), creating a redundant duplicate; either remove the
description entry from the object passed to courseCoreSchema.extend() so
courseBaseSchema simply inherits the transform, or if the redefinition is
intentional for explicitness, leave it but add a clear comment above the
description line explaining why it must be redefined; target the
courseBaseSchema declaration and the description property inside it when making
the change.
🤖 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/shared/ui/LabeledInput.tsx`:
- Line 19: The change of LabeledInput's default prop required from true to false
removed the asterisk on many form fields; to fix, decide whether to keep the new
default or revert it—if keeping it, update all current usages (e.g., CourseForm,
UnitForm, AssignmentFormPage/CreatePage, TestcaseRow) to explicitly pass
required={true} for fields that are mandatory (fields listed: "강의 명", "분반", "강의
소개", "제목", "공개일", "마감일", "문제 제목", "점수", "문제 설명", "입력 예제", "출력 예제");
alternatively restore the original default in LabeledInput so omitted props
remain required. Ensure you edit the LabeledInput component's required default
or add required={true} to each usage accordingly.

---

Nitpick comments:
In `@src/entities/course/model/schemas.ts`:
- Around line 16-19: courseBaseSchema currently redefines the description
transform that's already provided by courseCoreSchema.extend(), creating a
redundant duplicate; either remove the description entry from the object passed
to courseCoreSchema.extend() so courseBaseSchema simply inherits the transform,
or if the redefinition is intentional for explicitness, leave it but add a clear
comment above the description line explaining why it must be redefined; target
the courseBaseSchema declaration and the description property inside it when
making the change.

In `@src/features/course/filter-course/lib/useCourseFilter.ts`:
- Around line 33-41: The handler handleCourseSelect is recreated on every render
which can cause unnecessary re-renders when passed to children; wrap it with
React.useCallback and memoize it by including its dependencies
[setSelectedCourseId, courseMaps.labelToId] (and ALL_COURSES_OPTION if not a
constant) so the function identity is stable; update the declaration of
handleCourseSelect to use useCallback(...) and keep the same internal logic
(check ALL_COURSES_OPTION, lookup courseMaps.labelToId, call
setSelectedCourseId).

In `@src/shared/ui/ConfirmModal.tsx`:
- Around line 21-23: The ConfirmModal should add an isDismissible prop (default
true) and make all dismissal paths respect it: update the Escape handler in the
useEffect (handleEsc), the backdrop/click handler, and the cancel button click
handler inside ConfirmModal to no-op when isDismissible is false so ongoing
async actions cannot close the modal; ensure prop is optional with default true
and document that it controls Escape/backdrop/cancel dismissal behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 18ddd2a5-3c13-4b4a-85ce-67440101de9c

📥 Commits

Reviewing files that changed from the base of the PR and between fe7f48b and 5df8dd1.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (19)
  • package.json
  • src/entities/assignment/api/assignmentApi.ts
  • src/entities/assignment/api/assignmentMutations.ts
  • src/entities/assignment/model/schemas.ts
  • src/entities/course/model/schemas.ts
  • src/features/auth/kakao/model/useKakaoLogin.ts
  • src/features/course/create-course/model/useCreateCourse.ts
  • src/features/course/edit-course/model/useEditCourse.ts
  • src/features/course/filter-course/lib/useCourseFilter.ts
  • src/features/course/filter-course/ui/CourseSelector.tsx
  • src/pages/admin/assignments/AssignmentFormPage.tsx
  • src/pages/admin/assignments/ui/TestcaseRow.tsx
  • src/pages/admin/courses/CourseEditPage.tsx
  • src/pages/dashboard/ui/CourseManagementDropdown.tsx
  • src/shared/config/endpoints.ts
  • src/shared/lib/handleApiError.ts
  • src/shared/ui/ConfirmModal.tsx
  • src/shared/ui/LabeledInput.tsx
  • src/widgets/assignment-form-layout/ui/AssignmentFormLayout.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/pages/admin/assignments/AssignmentFormPage.tsx
  • src/entities/assignment/model/schemas.ts
  • src/entities/assignment/api/assignmentMutations.ts
  • src/pages/admin/assignments/ui/TestcaseRow.tsx

showLabel = true,
errorMessage,
required = true,
required = false,

Copy link
Copy Markdown

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:

#!/bin/bash
# Description: LabeledInput 사용처 중 required prop이 명시되지 않은 곳 확인

# LabeledInput을 사용하는 파일들에서 required prop 사용 여부 확인
echo "=== LabeledInput 사용처 검색 ==="
rg -n "LabeledInput" --type=tsx --type=ts -g '!*.test.*' -g '!*.spec.*' -A 3 -B 1

echo ""
echo "=== required prop이 명시된 사용처 ==="
rg -n "LabeledInput.*required" --type=tsx --type=ts

Repository: 2025-snowCode/snowCode-Client

Length of output: 205


🏁 Script executed:

#!/bin/bash
# LabeledInput 컴포넌트 파일 위치 확인
echo "=== LabeledInput 파일 위치 ==="
find . -name "LabeledInput*" -type f 2>/dev/null

# LabeledInput 사용처 검색 (파일 패턴 사용)
echo ""
echo "=== LabeledInput 사용처 ==="
rg "LabeledInput" -g "*.tsx" -g "*.ts" -A 3 -B 1 --max-count=30

# LabeledInput.tsx 파일 내용 확인
echo ""
echo "=== LabeledInput.tsx 파일 내용 ==="
if [ -f "src/shared/ui/LabeledInput.tsx" ]; then
  cat -n "src/shared/ui/LabeledInput.tsx"
fi

Repository: 2025-snowCode/snowCode-Client

Length of output: 11777


기존 사용처에서 필수 표시(*) 아스터리스크가 모두 사라집니다.

required 기본값 변경(truefalse)이 실제 영향을 미칩니다. 현재 코드베이스의 15개 이상 LabeledInput 사용처에서 required prop을 명시하지 않고 있기 때문입니다:

  • CourseForm: "강의 명", "분반", "강의 소개"
  • UnitForm: "제목", "공개일", "마감일"
  • AssignmentFormPage/CreatePage: "문제 제목", "점수", "문제 설명"
  • TestcaseRow: "입력 예제", "출력 예제"

이들 필드가 실제로 필수인지 확인하고, 필수 필드에는 명시적으로 required={true}를 추가해야 합니다. 아니면 이 변경을 기본값 설정 재검토로 보면 좋겠습니다 — opt-in 방식이 더 명확하긴 하지만, 한 번에 모든 사용처를 업데이트해야 하는 부담이 있습니다.

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

In `@src/shared/ui/LabeledInput.tsx` at line 19, The change of LabeledInput's
default prop required from true to false removed the asterisk on many form
fields; to fix, decide whether to keep the new default or revert it—if keeping
it, update all current usages (e.g., CourseForm, UnitForm,
AssignmentFormPage/CreatePage, TestcaseRow) to explicitly pass required={true}
for fields that are mandatory (fields listed: "강의 명", "분반", "강의 소개", "제목",
"공개일", "마감일", "문제 제목", "점수", "문제 설명", "입력 예제", "출력 예제"); alternatively restore
the original default in LabeledInput so omitted props remain required. Ensure
you edit the LabeledInput component's required default or add required={true} to
each usage accordingly.

@suminb99 suminb99 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

확인했습니다!

queryOptions({
queryKey: ['assignments', assignmentId],
queryFn: () => getAssignment(assignmentId),
select: (data) => data,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

데이터 변환이 없으니 select는 생략해도 될 거 같아요

});

return map;
return {labelToId, idToLabel};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

오 양방향 Map으로 label <-> id 변환 가능하게 한 거 너무 좋은 거 같아요!!

@JiiminHa JiiminHa merged commit 862f0bd into develop Mar 18, 2026
5 checks passed
@JiiminHa JiiminHa deleted the feat/69-assignments-add-edit branch March 18, 2026 15:58
This was referenced Mar 21, 2026
@JiiminHa JiiminHa mentioned this pull request Mar 24, 2026
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.

feat: 문제 등록 및 수정 api 연동

2 participants