Skip to content

#59 feat: 강의 상세 페이지 API 연동#64

Open
suminb99 wants to merge 50 commits intodevelopfrom
feat/59-course-overview-api
Open

#59 feat: 강의 상세 페이지 API 연동#64
suminb99 wants to merge 50 commits intodevelopfrom
feat/59-course-overview-api

Conversation

@suminb99
Copy link
Contributor

⚙️ Related ISSUE Number

Related #59



📄 Work Description

  • 강의상세페이지에서 헤더가 위로 오도록 스타일 수정
  • 단일 강의 조회 API 및 쿼리 옵션 추가 (getCourseById, courseQueries.getCourseDetails)
  • 강의 상세 페이지 API 연동
  • 학생 목록, 단원 추가 버튼 클릭 시 실제 페이지로 이동되게끔 Link 경로 업데이트
  • 과제 링크 경로 수정



📷 Screenshot

Screen.Recording.2026-02-28.at.22.26.53.mov



💬 To Reviewers



🔗 Reference

@suminb99 suminb99 requested a review from JiiminHa February 28, 2026 13:29
@suminb99 suminb99 self-assigned this Feb 28, 2026
@suminb99 suminb99 added the 🧩 feature 기능 개발 label Feb 28, 2026
@suminb99 suminb99 linked an issue Feb 28, 2026 that may be closed by this pull request
3 tasks
@vercel
Copy link

vercel bot commented Feb 28, 2026

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

Project Deployment Actions Updated (UTC)
snow-code-client Error Error Feb 28, 2026 1:29pm

@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

📝 Walkthrough

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 관리자용 과제 관리 페이지 추가
    • 코스 상세 정보 조회 및 동적 데이터 로드 기능
  • Improvements

    • 입력 폼에 오류 메시지 및 필수 필드 표시 추가
    • 과제 및 코스 데이터 조회 성능 개선
    • 데이터 캐싱 및 쿼리 최적화
  • Bug Fixes

    • 데이터 유효성 검사 및 처리 강화

요약

Walkthrough

API 응답에 Zod 스키마 기반 런타임 검증을 도입하고, React Query 패턴을 재구성하며, 다중 도메인(할당, 코스, 단원, 학생)에 걸친 데이터 페칭 로직을 업데이트했습니다. 모의 데이터를 제거하고 타입을 Zod 추론 타입으로 마이그레이션했습니다.

Changes

Cohort / File(s) Summary
App 라우팅
src/App.tsx
AssignmentManagePage 컴포넌트를 임포트하고 /admin/assignments/manage 경로에 새 관리자 라우트를 등록하여 이전의 주석 처리된 할당 경로를 대체합니다.
할당 도메인 스키마 및 타입
src/entities/assignment/model/schemas.ts, src/entities/assignment/model/types.ts
새로운 Zod 스키마(assignmentSchema, assignmentScheduleSchema)를 정의하고, 타입을 명시적 인터페이스에서 z.infer 기반 타입으로 마이그레이션합니다.
할당 API 및 쿼리
src/entities/assignment/api/assignmentApi.ts, src/entities/assignment/api/assignmentQueries.ts, src/entities/assignment/api/assignmentMutations.ts, src/entities/assignment/api/assignmentQueryOptions.ts
getAssignmentSchedules를 스키마 검증으로 리팩토링하고, getAllAssignments, getAssignmentsByCourse, deleteAssignment 함수를 추가합니다. assignmentQueryOptions를 삭제하고 새로운 assignmentQueries/Mutations 모듈을 도입합니다.
인증 도메인 스키마 및 API
src/entities/auth/model/schemas.ts, src/entities/auth/api/authApi.ts, src/entities/auth/model/useUserStore.ts
kakaoLoginResponseSchema를 정의하고, authApi를 스키마 검증을 사용하도록 리팩토링하며, UserType 임포트 경로를 업데이트합니다.
코스 도메인 스키마 및 타입
src/entities/course/model/schemas.ts, src/entities/course/model/types.ts
courseOverviewSchema, dashboardCourseSchema, assignmentCourseSchema를 정의하고, 타입을 Zod 추론 기반으로 마이그레이션합니다(BaseCourse, Unit 등 대체).
코스 API 및 쿼리
src/entities/course/api/courseApi.ts, src/entities/course/api/courseQueries.ts, src/entities/course/api/courseMutations.ts, src/entities/course/api/courseQueryOptions.ts
getAllCourses, deleteCourse를 스키마 검증으로 리팩토링하고, getCourseById를 추가합니다. courseQueryOptions를 삭제하고 새로운 courseQueries/Mutations 모듈을 도입합니다.
코스 인덱스 및 공개 내보내기
src/entities/course/index.ts
getAllCourses와 deleteCourse의 재내보내기를 제거합니다.
학생 도메인 스키마 및 타입
src/entities/student/model/schemas.ts, src/entities/student/model/types.ts
progressStatusSchema, studentProgressSchema, studentUnitSchema, studentSchema를 정의하고, 타입을 Zod 추론 기반으로 마이그레이션합니다.
단원 도메인 스키마, 타입, 저장소
src/entities/unit/model/schemas.ts, src/entities/unit/model/types.ts, src/entities/unit/model/useUnitStore.ts
unitSchema, unitFormSchema를 정의하고, 타입을 Zod 추론 기반으로 마이그레이션합니다. 세션 스토리지 기반의 Zustand 저장소를 추가합니다.
단원 API 및 쿼리
src/entities/unit/api/unitApi.ts, src/entities/unit/api/unitQueries.ts, src/entities/unit/api/unitMutations.ts
모든 단원 API 호출을 스키마 검증으로 리팩토링하고, deleteAssignmentFromUnit을 추가합니다. 쿼리에 select 변환과 skipToken 조건부 로직을 추가합니다. mutationFn 매개변수를 unit에서 unitForm으로 이름 변경합니다.
공유 모델 스키마 및 타입
src/shared/model/schemas.ts, src/shared/model/type.ts, src/shared/model/index.ts
apiResponseSchema, errorResponseSchema, semesterCodeSchema, submissionStatusSchema를 정의합니다. type.ts에서 ApiResponse와 UserType을 이동하고, 새로운 schemas.ts 내보내기를 추가합니다.
공유 UI 컴포넌트
src/shared/ui/LabeledInput.tsx, src/shared/ui/button/Button.tsx, src/shared/ui/list-row/ListRow.tsx, src/shared/ui/list-row/list-row-styles.ts, src/shared/ui/Layout.tsx
LabeledInput에 errorMessage 지원을 추가하고, Button의 formID를 form으로 이름 변경합니다. ListRow 아이콘 타입을 정리합니다. Layout의 정렬 클래스와 헤더 z-인덱스를 업데이트합니다. ListRowStyles에서 cursor-pointer를 제거합니다.
할당 페이지 레이아웃 및 기능
src/widgets/assignment-page-layout/ui/AssignmentPageLayout.tsx, src/features/assignment/filter-assignment/lib/useAssignmentList.ts
AssignmentPageLayout props를 리팩토링하여 buttons를 추가하고 onCancel/onConfirm을 제거합니다. 중복 제거 및 필터링을 위한 useAssignmentList 훅을 추가합니다.
할당 선택 페이지
src/pages/select-assignment/AssignmentSelectPage.tsx, src/pages/select-assignment/ui/AssignmentListContainer.tsx
모의 데이터를 실제 데이터 페칭으로 대체하고, useUnitStore를 통합합니다. Assignment 객체 기반 선택으로 업데이트하고, AssignmentListContainer props를 리팩토링합니다.
할당 관리 페이지
src/pages/manage-assignment/AssignmentManagePage.tsx, src/pages/manage-assignment/ui/AssignmentManageActionsBar.tsx
새로운 할당 관리 페이지와 액션 바 컴포넌트를 추가합니다. 할당 삭제 뮤테이션과 쿼리 무효화를 포함합니다.
코스 개요 페이지
src/pages/course-overview/CourseOverviewPage.tsx, src/pages/course-overview/ui/CourseHero.tsx, src/pages/course-overview/ui/CourseActionsBar.tsx, src/pages/course-overview/ui/AssignmentList.tsx, src/pages/course-overview/ui/UnitItem.tsx
모의 데이터를 courseQueries를 통한 실제 페칭으로 대체합니다. CourseHero 프롭 이름을 courseData에서 course로 변경합니다. CourseActionsBar에 courseId를 추가하고, 링크 기반 네비게이션으로 업데이트합니다. AssignmentList 링크를 /admin/assignments/{index}로 업데이트합니다. UnitItem의 Unit 타입 임포트 경로를 변경합니다.
대시보드
src/pages/dashboard/Dashboard.tsx
courseQueryOptions/assignmentQueryOptions를 새로운 courseQueries/assignmentQueries로 대체합니다. 쿼리 데이터 접근 경로를 업데이트하고, 뮤테이션을 courseMutations를 사용하도록 리팩토링합니다. 쿼리 키를 새로운 구조에 맞게 업데이트합니다.
단원 편집 페이지
src/pages/unit-editor/UnitEditorPage.tsx, src/pages/unit-editor/model/types.ts, src/pages/unit-editor/ui/UnitForm.tsx, src/pages/unit-editor/ui/UnitList.tsx, src/pages/unit-editor/ui/UnitAssignmentList.tsx, src/pages/unit-editor/mock/unitMock.ts
모드 관리를 activeMode 상태와 useUnitStore 기반으로 리팩토링합니다. UnitForm에 isPending 지원과 할당 선택 네비게이션을 추가합니다. UnitList props를 단순화하고, UnitAssignmentList에서 DeleteIcon을 제거합니다. unitMock.ts를 삭제합니다.
공유 라이브러리 및 모의 데이터
src/shared/lib/course.ts, src/shared/mocks/assignmentDummyResponse.ts, src/shared/mocks/assignmentDummyTypes.ts, src/shared/mocks/assignmentSelectResponse.ts, src/shared/mocks/courseOverviewResponse.ts, src/shared/mocks/dashboardCourseList.ts, src/shared/mocks/dashboardScheduleList.ts
SemesterCode 임포트 경로를 업데이트합니다. 모든 모의 데이터 파일을 삭제합니다(coursesResponse, singleUnitResponse, allUnitsResponse, response, courseOptionsResponse, courseResponse, responseCourseList, responseScheduleList).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 분

Possibly related PRs

Suggested labels

🛠️ refactor, 🎨 ui

Suggested reviewers

  • JiiminHa

상세 피드백

💡 긍정적 측면

스키마 기반 검증 도입 - Zod를 활용한 런타임 검증 계층이 타입 안전성을 크게 향상시킵니다. 이는 API 응답 형식의 변화를 조기에 감지할 수 있는 좋은 방식입니다.

📋 검토 포인트

1. 스키마 정의의 일관성

  • apiResponseSchema 제네릭이 잘 설계되었으나, 모든 API 함수가 정확히 동일한 패턴을 따르는지 확인이 필요합니다.
  • 제안: 스키마 정의 가이드라인 문서를 추가하면 향후 유지보수가 용이해집니다.

2. 쿼리 select 변환의 정확성

// assignmentQueries.ts 예시
getAssignmentSchedules: {
  select: (data) => ({ scheduleCount: data.response.count, schedules: data.response.schedule })
}
  • data.response 객체가 항상 예상된 구조를 가지는지, 스키마 검증 이후에도 안전한지 확인이 필요합니다.
  • 제안: 선택적 필드에 대해 null 병합 연산자(??)를 활용한 방어적 프로그래밍이 권장됩니다.

3. UnitEditorPage의 모드 관리 단순화

  • activeMode 상태와 useUnitStore의 역할이 명확하게 분리되어 있으나, 동기화 로직이 복잡할 수 있습니다.
  • 제안: Zustand 공식 문서의 "리셋 상태" 패턴을 참고하여 resetStore 호출 시점을 명확히 정의하세요.

4. 모의 데이터 제거 시 마이그레이션 경로

  • 많은 모의 데이터 파일이 제거되었으나, 이들이 테스트나 다른 부분에서 여전히 참조되고 있지 않은지 확인이 필요합니다.
  • 제안: 빌드 시간에 미사용 임포트를 감지하는 ESLint 규칙 활성화를 고려해보세요.

5. 타입 마이그레이션 완전성

  • Assignment, Unit, Student 등의 타입이 인터페이스에서 Zod 추론 타입으로 변경되었습니다.
  • 확인 필요: 이 변경이 제네릭 제약, 유니언 타입, 또는 조건부 타입 정의에 미치는 영향을 검토해보세요.
  • 참고: TypeScript 제네릭 가이드

6. AssignmentListContainer의 리팩토링

// 변경 전
onSelect: (id: number) => void

// 변경 후
onSelect?: (item: Assignment) => void
  • 옵셔널로 변경되었으므로, 호출 시에 null 체크가 적절히 이루어지는지 확인이 필요합니다.

7. CourseActionsBar의 동적 스타일링

  • isActiveCourse 기반의 버튼 색상 변경이 시각적 일관성을 유지하는지 디자인팀과 함께 확인해보세요.

✅ 추천 사항

  1. 통합 테스트 추가: 새로운 쿼리/뮤테이션이 실제 API 응답과 정확히 동기화되는지 검증하는 테스트 작성
  2. 타입 안전성 검증: skipToken 사용 시 언제 활성화/비활성화되는지에 대한 주석 추가
  3. 에러 핸들링: 모든 스키마 파싱 에러에 대한 명확한 에러 메시지 정의
  4. 문서화: 새로운 useUnitStore 및 쿼리 패턴에 대한 사용 예제 추가
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 코스 상세 페이지 API 연동이라는 주요 변경사항을 명확하게 설명하고 있으며, 전체 변경사항의 핵심을 잘 나타내고 있습니다.
Description check ✅ Passed PR 설명이 강의 상세 페이지 API 연동, 스타일 수정, 라우팅 경로 업데이트 등 실제 변경사항들을 상세하게 설명하고 있습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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 feat/59-course-overview-api

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: 4

🧹 Nitpick comments (22)
src/pages/course-overview/ui/CourseActionsBar.tsx (1)

17-18: 불필요한 템플릿 리터럴 사용 제거를 권장합니다.

삼항 연산자가 이미 문자열을 반환하므로 템플릿 리터럴로 감쌀 필요가 없습니다. 더 간결한 코드를 위해 직접 값을 전달하세요.

♻️ 제안된 수정
      <Link
        to='/admin/student'
        className={buttonStyles({
-          color: `${isActiveCourse ? 'outlineWhite' : 'outlinePurple'}`,
+          color: isActiveCourse ? 'outlineWhite' : 'outlinePurple',
        })}>
        학생 목록
      </Link>
      <Link
        to={`/admin/units/${courseId}`}
        className={buttonStyles({
-          color: `${isActiveCourse ? 'outlinePurple' : 'primary'}`,
+          color: isActiveCourse ? 'outlinePurple' : 'primary',
        })}>
        단원 추가
      </Link>

Also applies to: 24-25

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

In `@src/pages/course-overview/ui/CourseActionsBar.tsx` around lines 17 - 18,
Remove the unnecessary template literals when passing the color prop to
buttonStyles in CourseActionsBar.tsx: where color is currently set as color:
`${isActiveCourse ? 'outlineWhite' : 'outlinePurple'}` (and the similar usage at
the other occurrence around lines 24-25), change it to pass the string directly
using the ternary result (color: isActiveCourse ? 'outlineWhite' :
'outlinePurple') so buttonStyles receives a plain string instead of a template
literal.
src/entities/auth/api/authApi.ts (1)

16-16: 스키마 기반 검증 적용 - 에러 핸들링 고려

.parse() 사용 시 스키마 불일치가 발생하면 ZodError가 throw됩니다. 이는 API 계약 위반을 빠르게 감지하는 데 유용하지만, 호출하는 쪽에서 이 에러를 적절히 처리할 수 있는지 확인이 필요합니다.

만약 더 유연한 에러 핸들링이 필요하다면 .safeParse()를 고려해볼 수 있습니다.

💡 safeParse 사용 예시 (선택사항)
const result = kakaoLoginResponseSchema.safeParse(response.data.response);
if (!result.success) {
  console.error('API 응답 검증 실패:', result.error);
  throw new Error('Invalid API response');
}
const data = result.data;

참고: Zod 공식 문서 - safeParse

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

In `@src/entities/auth/api/authApi.ts` at line 16, Replace the direct zod parse
call to avoid uncaught ZodError from crashing callers: in authApi.ts where
kakaoLoginResponseSchema.parse(response.data.response) is used, switch to
kakaoLoginResponseSchema.safeParse(...) (or wrap parse in a try/catch for
ZodError) and handle validation failures by logging the validation error and
throwing a controlled Error (or returning a structured failure) so callers of
the function (the auth API method that consumes response.data.response) can
handle the invalid-response case gracefully.
src/entities/auth/model/schemas.ts (1)

3-10: Zod 스키마 기반 런타임 검증 도입 - 좋은 패턴입니다! 🛡️

API 응답에 대한 런타임 검증을 추가하여 타입 안전성을 강화했습니다. 추가로, 이 스키마에서 타입을 추론하여 export하면 다른 곳에서도 재사용할 수 있습니다.

💡 타입 추론 export 제안
 import {z} from 'zod';

 export const kakaoLoginResponseSchema = z.object({
   memberId: z.number(),
   name: z.string(),
   role: z.enum(['ADMIN', 'USER']),
   studentId: z.string(),
   email: z.string().nullable(),
   accessToken: z.string(),
 });
+
+export type KakaoLoginResponse = z.infer<typeof kakaoLoginResponseSchema>;

참고: Zod 공식 문서 - Type inference

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

In `@src/entities/auth/model/schemas.ts` around lines 3 - 10, Export a TypeScript
type inferred from the existing Zod schema so other modules can reuse the
runtime-validated shape; add an exported type alias like "KakaoLoginResponse"
using z.infer<typeof kakaoLoginResponseSchema> (referencing the
kakaoLoginResponseSchema symbol) so callers can import the inferred type for
static typing alongside the runtime schema.
src/shared/ui/LabeledInput.tsx (1)

13-13: required 기본값이 true로 설정되어 있음 - 명시적 설정 권장

컴포넌트가 React.InputHTMLAttributes를 확장하지만 required 속성의 기본값을 true로 설정하는 것은 표준 HTML input 동작(기본값: 필수 아님)과 다릅니다. 현재 모든 사용처가 필수 필드이므로 문제없지만, 향후 선택적 필드가 필요한 경우 명시적으로 required={false}를 추가해야 합니다.

권장사항:

  • 의도를 명확히 하기 위해 컴포넌트 JSDoc에 기본값 설명 추가
  • 또는 기본값을 undefined로 변경하고 사용처에서 필요시 required={true} 명시 (더 직관적)
// 현재: 모든 필드가 자동으로 필수
<LabeledInput label="제목" />

// 선택적 필드 필요 시
<LabeledInput label="소개" 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 13, The LabeledInput component
currently sets the prop default required = true which deviates from standard
HTML input default and forces callers to override; update the component by
either (A) removing the default and setting required to undefined in the
props/defaults so consumers must opt-in with required={true}, or (B) if keeping
true is intentional, add a JSDoc comment on the LabeledInput component
describing that required defaults to true; locate the default assignment
(required = true) in LabeledInput.tsx and implement one of these two changes and
adjust any callers if you choose option A.
src/entities/student/model/schemas.ts (1)

18-25: 날짜 필드 검증 강화 고려

startDate, endDatez.string()으로 정의되어 있는데, 잘못된 형식의 문자열도 통과됩니다. API 계약이 ISO 날짜 형식을 보장한다면, 추후 z.string().datetime() 또는 커스텀 정규식 검증을 추가하면 런타임 안정성이 높아집니다.

// 예시: ISO 날짜 형식 검증
startDate: z.string().datetime(), // ISO 8601 형식
// 또는 커스텀 검증
startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),

참고: Zod datetime 문서

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

In `@src/entities/student/model/schemas.ts` around lines 18 - 25,
studentUnitSchema currently accepts any string for startDate and endDate;
tighten validation by replacing the z.string() types for startDate and endDate
with a datetime or regex validator (e.g., use z.string().datetime() for ISO‑8601
or z.string().regex(...) for a custom YYYY-MM-DD pattern) so the schema
(studentUnitSchema) enforces proper date formatting for startDate and endDate
and fails fast on invalid values.
src/shared/model/schemas.ts (1)

10-14: errorResponseSchema는 현재 사용되지 않는 것으로 확인됨

이 스키마가 향후 에러 응답 검증에 사용될 예정이라면, additionalProp1, additionalProp2, additionalProp3는 Swagger/OpenAPI 자동 생성 예시명처럼 보이므로 z.record(z.string(), z.string())로 개선하는 것이 좋습니다. 현재는 각 엔티티에서 특정 응답 스키마를 정의해서 사용 중입니다.

♻️ 동적 에러 필드 스키마
 export const errorResponseSchema = apiResponseSchema(
   z.object({
     errorCode: z.string(),
     errorMessage: z.string(),
-    errors: z.object({
-      additionalProp1: z.string(),
-      additionalProp2: z.string(),
-      additionalProp3: z.string(),
-    }),
+    errors: z.record(z.string(), z.string()),
   })
 );

참고: Zod record 문서

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

In `@src/shared/model/schemas.ts` around lines 10 - 14, errorResponseSchema에 정의된
errors 객체가 Swagger 예시 필드명(additionalProp1/2/3)을 사용하고 있어 일반적인 에러 키를 표현하지 못하므로,
errors: z.object({...}) 대신 errors: z.record(z.string(), z.string())로 변경해 동적 키/값
형태의 에러 응답을 허용하도록 바꿔주세요; 관련 심볼은 errorResponseSchema과 errors 필드이며, 변경 시 기존 호출부에서
특정 엔티티별 스키마를 별도로 정의하는 방식에는 영향이 없도록 테스트 케이스나 타입 호환성을 확인해 주세요.
src/pages/manage-assignment/ui/AssignmentManageActionsBar.tsx (2)

28-33: 접근성을 위해 aria-label 추가를 권장합니다.

아이콘만 있는 버튼은 스크린 리더 사용자에게 기능이 전달되지 않습니다. 접근성 향상을 위해 aria-label을 추가하세요.

♿ 접근성 개선 제안
-      <button onClick={handleOnEdit}>
+      <button onClick={handleOnEdit} aria-label="과제 수정">
         <EditIcon className='w-3.5 h-4' />
       </button>
-      <button onClick={handleOnDelete}>
+      <button onClick={handleOnDelete} aria-label="과제 삭제">
         <DeleteIcon className='w-3.5 h-4' />
       </button>

📚 참고: WAI-ARIA Authoring Practices - 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
28 - 33, AssignmentManageActionsBar has icon-only buttons for edit/delete
(buttons calling handleOnEdit and handleOnDelete with EditIcon/DeleteIcon) that
lack accessible labels; update the JSX to add descriptive aria-label attributes
(e.g., aria-label="Edit assignment" and aria-label="Delete assignment") on the
respective buttons and ensure the icon SVGs remain aria-hidden (or role="img"
with appropriate title) so screen readers read the button labels, not the icons.

16-22: 핸들러 네이밍을 간소화할 수 있습니다.

handleOnEdithandleEdit, handleOnDeletehandleDeleteOn을 생략하면 더 간결합니다. 선택적 개선 사항입니다.

🤖 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
16 - 22, Rename the handler functions for brevity: change handleOnEdit to
handleEdit and handleOnDelete to handleDelete, update their implementations to
still call navigate(`/admin/assignments/${id}`) and onDelete(id) respectively,
and replace all usages/references (e.g., JSX props or event bindings) that
currently reference handleOnEdit or handleOnDelete to the new names so nothing
breaks; ensure any tests or prop types referencing those function names are
updated as well.
src/pages/unit-editor/model/types.ts (1)

5-10: 'idle' 제거 후 null 사용에 대한 고려사항

Mode에서 'idle'을 제거하고 mode: Mode | null로 변경했는데, null보다 'idle'이 의미를 더 명확히 전달합니다. 현재 설계가 의도적이라면 괜찮지만, 가독성 측면에서 'idle' | 'creating' | 'editing'을 유지하는 것도 고려해 보세요.

💡 대안 제안
-export type Mode = 'creating' | 'editing';
+export type Mode = 'idle' | 'creating' | 'editing';

 export interface UnitFormProps {
   unit?: Unit;
   unitIndex: number;
-  mode: Mode | null;
+  mode: Mode;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/unit-editor/model/types.ts` around lines 5 - 10, The Mode type was
changed to remove 'idle' and UnitFormProps.mode was made nullable, which reduces
clarity; restore 'idle' by updating the Mode type to "type Mode = 'idle' |
'creating' | 'editing'" and change UnitFormProps.mode to use Mode (not Mode |
null); then update any callers of UnitFormProps (e.g., components constructing
UnitFormProps) to pass 'idle' as the default state instead of null (or handle
the new non-nullable Mode accordingly) so the intent remains explicit; refer to
the Mode type and UnitFormProps interface to locate and change the definitions
and call sites.
src/entities/assignment/api/assignmentQueries.ts (1)

11-18: queryKey를 더 구체적으로 지정하는 것을 권장합니다.

['schedules']는 다소 범용적입니다. 다른 도메인에서 스케줄 관련 쿼리가 추가될 경우 충돌 가능성이 있습니다. ['assignments', 'schedules']와 같이 도메인을 포함하면 명확해집니다.

♻️ queryKey 개선 제안
   getAssignmentSchedules: () =>
     queryOptions({
-      queryKey: ['schedules'],
+      queryKey: ['assignments', 'schedules'],
       queryFn: getAssignmentSchedules,

📚 참고: TanStack Query - Query Keys

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

In `@src/entities/assignment/api/assignmentQueries.ts` around lines 11 - 18, The
queryKey used in the queryOptions call is too generic (['schedules']) and may
collide with other schedule queries; update the queryOptions invocation that
references queryKey and getAssignmentSchedules so the key is namespaced (for
example ['assignments','schedules'] or similar) to include the domain, ensuring
any select logic mapping scheduleCount/schedules remains unchanged.
src/entities/unit/model/types.ts (1)

1-1: z는 타입 추론에만 사용되므로 import type을 권장합니다.

zz.infer로만 사용되고 있어 런타임에 필요하지 않습니다. 코딩 가이드라인에 따라 type-only import를 사용하세요.

♻️ 수정 제안
-import {z} from 'zod';
+import type {z} from 'zod';
 import type {unitFormSchema, unitSchema} from './schemas';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/entities/unit/model/types.ts` at line 1, The file currently imports the
zod runtime object with "z" but only uses it for type inference (z.infer);
change the import to a type-only import and update usages accordingly: replace
the value import of z with a type import (so TypeScript erases it at runtime),
update any z.infer usages to reference the imported type alias (e.g., use the
proper imported type helper or a type alias like Infer<typeof YourSchema>), and
if any code actually needs the runtime z object leave a regular import
there—refer to the symbol "z" and occurrences of "z.infer" to locate and modify
the imports and type usages.
src/entities/unit/api/unitQueries.ts (1)

21-24: skipToken 패턴 적절히 사용되었습니다! ✨

React Query v5의 skipToken을 활용하여 조건부 쿼리를 깔끔하게 처리했습니다. select 함수는 한 줄로 단순화할 수 있습니다.

♻️ select 함수 단순화
-select: (data) => {
-  return data.response;
-},
+select: (data) => data.response,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/entities/unit/api/unitQueries.ts` around lines 21 - 24, The select
callback in the query configuration can be simplified to a single expression;
update the select function used alongside queryFn (where queryFn: unitId ? () =>
getUnitById(unitId) : skipToken) to return data.response with a concise one-line
arrow function instead of the current block body — locate the select option in
the query definition and replace the multi-line function with a single-line
expression.
src/entities/assignment/model/schemas.ts (1)

2-2: Zod import 스타일 일관성 확인 필요

import z from 'zod' 대신 import {z} from 'zod'를 사용하면 다른 스키마 파일(예: src/entities/unit/model/schemas.ts)과 일관성을 유지할 수 있습니다.

♻️ import 스타일 통일 제안
-import z from 'zod';
+import {z} from 'zod';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/entities/assignment/model/schemas.ts` at line 2, The module currently
imports Zod with a default import (`import z from 'zod'`); change this to the
named import style used elsewhere (`import { z } from 'zod'`) so the schemas in
this file use the same import pattern as other schema files (e.g.,
`src/entities/unit/model/schemas.ts`)—update the import statement at the top and
ensure all uses of `z` remain unchanged.
src/entities/unit/model/useUnitStore.ts (1)

24-29: 객체 속성 단축 구문 사용 가능

동일한 이름의 변수를 객체에 할당할 때 단축 구문을 사용하면 더 간결해집니다.

♻️ 단축 구문 적용 제안
 storeFormData: (title, releaseDate, dueDate) =>
   set({
-    title: title,
-    releaseDate: releaseDate,
-    dueDate: dueDate,
+    title,
+    releaseDate,
+    dueDate,
   }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/entities/unit/model/useUnitStore.ts` around lines 24 - 29, In
storeFormData, instead of creating an object with explicit key: value pairs, use
the ES object property shorthand for title, releaseDate, and dueDate so the set
call passes { title, releaseDate, dueDate } directly; update the storeFormData
implementation that calls set(...) (function name: storeFormData, caller: set)
to use the shortened property names.
src/entities/unit/model/schemas.ts (1)

23-25: 날짜 문자열 비교 시 형식 의존성 주의

문자열 비교(data.releaseDate <= data.dueDate)는 ISO 8601 형식(YYYY-MM-DD)에서만 올바르게 동작합니다. 다른 형식의 날짜가 입력되면 예기치 않은 결과가 발생할 수 있습니다.

♻️ Date 객체를 사용한 안전한 비교 제안
-.refine((data) => data.releaseDate <= data.dueDate, {
+.refine((data) => new Date(data.releaseDate) <= new Date(data.dueDate), {
   message: '날짜 범위가 올바르지 않습니다.',
   path: ['dueDate'],
 });

📚 참고: Zod refinements 공식 문서

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

In `@src/entities/unit/model/schemas.ts` around lines 23 - 25, The current Zod
.refine on the unit schema compares releaseDate and dueDate as strings which is
format-dependent; modify the refinement in schemas.ts to parse both fields into
Date (or epoch numbers) inside the refine callback (e.g., new
Date(data.releaseDate).getTime() and new Date(data.dueDate).getTime()), check
for invalid dates (isNaN) and return false (or set a specific error) when
parsing fails, then compare the numeric timestamps to ensure a correct
chronological check for releaseDate <= dueDate; update the refine attached to
the schema that references releaseDate and dueDate accordingly.
src/features/assignment/filter-assignment/lib/useAssignmentList.ts (1)

12-17: 조건부 쿼리 실행 최적화 고려

현재 구조에서 두 쿼리가 항상 등록됩니다. enabled 옵션이 !!courseId로 설정되어 있어 courseId = 0일 때 실행되지 않지만, 훅 자체는 항상 호출됩니다.

명시적인 조건부 쿼리를 위해 enabled 옵션을 직접 전달하는 방식도 고려해볼 수 있습니다.

♻️ 대안 구조 제안
+import {skipToken} from '@tanstack/react-query';
+
 export const useAssignmentList = (
   selectedCourseId: number | null
 ): Assignment[] => {
-  const {data: allAssignments} = useQuery(
-    assignmentQueries.getAllAssignments()
-  );
-  const {data: assignments} = useQuery(
-    assignmentQueries.getAssignmentsByCourse(selectedCourseId ?? 0)
-  );
+  const {data: allAssignments} = useQuery({
+    ...assignmentQueries.getAllAssignments(),
+    enabled: !selectedCourseId,
+  });
+  const {data: assignments} = useQuery({
+    ...assignmentQueries.getAssignmentsByCourse(selectedCourseId ?? 0),
+    enabled: !!selectedCourseId,
+  });

📚 참고: TanStack Query Dependent Queries

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

In `@src/features/assignment/filter-assignment/lib/useAssignmentList.ts` around
lines 12 - 17, Both queries in the useAssignmentList hook are always registered;
make them conditional by passing the `enabled` option to useQuery so they only
run when appropriate: add `enabled: true/false` to the useQuery call that uses
`assignmentQueries.getAllAssignments()` and to the call using
`assignmentQueries.getAssignmentsByCourse(selectedCourseId ?? 0)` (use `enabled:
!!selectedCourseId` or `enabled: selectedCourseId !== 0` for the course-specific
query, or `enabled: selectedCourseId == null` for the all-assignment query
depending on desired behavior) so the queries are not executed unless the
required `selectedCourseId` state is present; look for the `useQuery` calls and
the `selectedCourseId` variable inside `useAssignmentList` to apply this change.
src/pages/course-overview/CourseOverviewPage.tsx (1)

15-15: 불필요한 삼항 연산자 단순화 가능 👌

course.unitCount !== 0 ? true : false는 이미 boolean을 반환하는 표현식입니다.

♻️ 단순화 제안
-const hasUnits = course.unitCount !== 0 ? true : false;
+const hasUnits = course.unitCount !== 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/course-overview/CourseOverviewPage.tsx` at line 15, The hasUnits
assignment uses an unnecessary ternary; replace the expression that sets
hasUnits (currently using course.unitCount !== 0 ? true : false) with a direct
boolean expression such as course.unitCount !== 0 or Boolean(course.unitCount)
in CourseOverviewPage.tsx so hasUnits is computed without the redundant ternary.
src/pages/select-assignment/ui/AssignmentListContainer.tsx (1)

5-5: 모듈 레벨에서 T를 고정하면 제네릭의 의미가 사라집니다.

현재 구조에서는 AssignmentListContainerProps<T>가 제네릭처럼 보이지만, T가 모듈 레벨에서 이미 고정되어 있어 실제로는 제네릭이 아닙니다. 진정한 제네릭이 필요 없다면 타입 별칭을 직접 사용하거나, 제네릭이 필요하다면 컴포넌트 정의 시점에 타입 파라미터를 받도록 수정하세요.

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

In `@src/pages/select-assignment/ui/AssignmentListContainer.tsx` at line 5, The
module-level type alias T fixes the generic, so either remove the module-level
alias and inline the concrete type where used (e.g., replace uses of T with
AssignmentCourse['assignments'][number] and keep AssignmentListContainerProps
non-generic), or make the component truly generic by removing the top-level T
and adding a type parameter on the component declaration (e.g., declare
AssignmentListContainer as a generic component using
AssignmentListContainerProps<T>) and update all references to use that
parameter; adjust imports/exports accordingly to ensure
AssignmentListContainerProps and the component share the same generic parameter
rather than relying on a module-scoped T.
src/pages/manage-assignment/AssignmentManagePage.tsx (2)

31-48: 삭제 뮤테이션 구현이 잘 되어 있습니다!

쿼리 무효화 패턴이 적절하게 구현되어 있네요. 다만 한 가지 개선 제안이 있습니다:

selectedCourseIdnull일 때 courseId: 0으로 쿼리를 무효화하게 되는데, 이는 실제로 존재하지 않는 쿼리를 무효화하려는 시도가 될 수 있습니다. 대신 조건부로 무효화하는 것을 고려해 보세요.

♻️ 선택적 개선 제안
  onSuccess: () => {
    queryClient.invalidateQueries({
      queryKey: assignmentQueries.getAllAssignments().queryKey,
    });
-   queryClient.invalidateQueries({
-     queryKey: assignmentQueries.getAssignmentsByCourse(
-       selectedCourseId ?? 0
-     ).queryKey,
-   });
+   if (selectedCourseId) {
+     queryClient.invalidateQueries({
+       queryKey: assignmentQueries.getAssignmentsByCourse(selectedCourseId).queryKey,
+     });
+   }
    alert('문제가 성공적으로 삭제되었습니다.');
  },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/manage-assignment/AssignmentManagePage.tsx` around lines 31 - 48,
The current deleteAssignment mutation always invalidates
assignmentQueries.getAssignmentsByCourse(selectedCourseId ?? 0) which uses 0
when selectedCourseId is null; change this to only call
queryClient.invalidateQueries for
assignmentQueries.getAssignmentsByCourse(selectedCourseId).queryKey when
selectedCourseId is non-null/defined (e.g., guard on selectedCourseId !==
null/undefined) so you don't attempt to invalidate a non-existent course query;
keep the existing invalidateQueries for getAllAssignments and the
onSuccess/onError handlers intact.

42-42: alert()를 커스텀 토스트/모달로 대체를 고려해 보세요.

alert()는 동작하지만 브라우저 기본 UI를 사용하므로 UX 일관성이 떨어질 수 있습니다. 향후 프로젝트의 디자인 시스템에 맞는 토스트 컴포넌트 도입을 권장합니다.

Also applies to: 46-46

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

In `@src/pages/manage-assignment/AssignmentManagePage.tsx` at line 42, Replace the
browser alert() calls in the AssignmentManagePage component with the app's
toast/modal utilities to maintain consistent UX: locate the two alert('문제가 성공적으로
삭제되었습니다.') occurrences (the success callbacks after question/assignment
deletion) and call the project's toast or modal API (e.g., showToast or
openModal) instead, passing a localized success message and optional success
variant/options; ensure you import and use the existing notification component
used elsewhere in the app and remove the alert() usages.
src/entities/unit/api/unitApi.ts (1)

10-17: 인라인 스키마를 별도 파일로 분리하는 것을 고려해 보세요.

getAllUnitsByCourseId의 응답 스키마가 인라인으로 정의되어 있습니다. 일관성을 위해 src/entities/unit/model/schemas.ts에 별도로 정의하면 재사용성과 유지보수성이 향상됩니다.

♻️ 스키마 분리 제안
// src/entities/unit/model/schemas.ts에 추가
export const unitListResponseSchema = z.object({
  count: z.number(),
  units: z.array(
    unitSchema.pick({id: true, title: true, assignmentCount: true})
  ),
});
// unitApi.ts
+import {unitListResponseSchema} from '../model/schemas';

 export const getAllUnitsByCourseId = async (courseId: number) => {
   const response = await privateAxios.get(`/courses/${courseId}/units`);
-  return apiResponseSchema(
-    z.object({
-      count: z.number(),
-      units: z.array(
-        unitSchema.pick({id: true, title: true, assignmentCount: true})
-      ),
-    })
-  ).parse(response.data);
+  return apiResponseSchema(unitListResponseSchema).parse(response.data);
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/entities/unit/api/unitApi.ts` around lines 10 - 17, The inline response
schema in getAllUnitsByCourseId should be extracted to a reusable exported
symbol: create unitListResponseSchema in src/entities/unit/model/schemas.ts
using unitSchema.pick({id: true, title: true, assignmentCount: true}) wrapped
with z.object({count: z.number(), units: z.array(...)}); then in
src/entities/unit/api/unitApi.ts import unitListResponseSchema and pass it to
apiResponseSchema(...).parse(response.data) instead of the inline z.object;
update any imports/usages and tests that referenced the previous inline shape to
use unitListResponseSchema.
src/pages/select-assignment/AssignmentSelectPage.tsx (1)

70-79: 버튼 영역 구현이 깔끔합니다.

취소/등록 버튼이 적절한 색상과 핸들러로 연결되어 있습니다. 다만 returnToPreviousPage()를 직접 전달하는 것도 고려해 보세요:

♻️ 간소화 제안 (선택)
-<Button color='outlinePurple' onClick={() => returnToPreviousPage()}>
+<Button color='outlinePurple' onClick={returnToPreviousPage}>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/select-assignment/AssignmentSelectPage.tsx` around lines 70 - 79,
The onClick prop for the cancel Button is using an unnecessary arrow wrapper;
change the handler to pass the function reference directly by replacing the
onClick={() => returnToPreviousPage()} usage with onClick={returnToPreviousPage}
to simplify the code and avoid creating an extra closure (refer to
returnToPreviousPage, handleConfirm, and the Button component in
AssignmentSelectPage).
🤖 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/pages/course-overview/CourseOverviewPage.tsx`:
- Around line 9-10: The code currently converts useParams()'s id to a number
without validation (const {id} = useParams(); const courseId = Number(id);)
which can produce NaN; update CourseOverviewPage to validate id before
conversion: ensure id is a defined string and parseInt/Number yields a finite
integer (e.g., typeof id === 'string' && !Number.isNaN(Number(id))); if invalid,
handle it (redirect to a 404/error page, show an error state, or bail out before
any API calls that use courseId). Place this check where courseId is
created/used (in the CourseOverviewPage component) so all subsequent API calls
and logic use a guaranteed valid courseId.

In `@src/pages/select-assignment/ui/AssignmentListContainer.tsx`:
- Around line 5-12: The generic type T is constrained to
AssignmentCourse['assignments'][number] while onSelect and handleSelect expect
an Assignment (which may include submittedStatus), causing a type mismatch;
update the component props and handlers to be consistent by either broadening
the generic constraint to Assignment (e.g., make AssignmentListContainerProps<T
extends Assignment>) or by changing onSelect and handleSelect signatures to
accept T instead of Assignment; locate and update AssignmentListContainerProps,
the renderItem prop, and the handleSelect/onSelect usages so types align (choose
one approach and apply it across the component).

In `@src/pages/unit-editor/UnitEditorPage.tsx`:
- Around line 26-43: currentMode can be null and is being passed into UnitForm
causing unintended creation flows; change the render logic so UnitForm only
receives a concrete Mode ('creating' | 'editing') — use activeMode,
hasOngoingCreation and initialMode to derive a non-null mode before rendering or
simply conditionally render/return null while currentMode === null (or while
data/loading). Specifically, update the logic around currentMode/currentUnitId
(referencing currentMode, activeMode, hasOngoingCreation, initialMode,
selectedUnitId) so that UnitForm is not mounted with currentMode === null;
ensure any props passed to UnitForm are strictly 'creating' | 'editing' and
delay form rendering until a definite mode is determined.
- Around line 15-27: The code can call queries and create requests with an
invalid courseId (NaN); compute courseId as Number(id) and guard it with
Number.isFinite(courseId) before using it: create a boolean like isValidCourseId
= Number.isFinite(courseId) and pass enabled: isValidCourseId to
useQuery(unitQueries.getUnitList(courseId)); compute initialMode only when
isValidCourseId is true (otherwise keep null) and add the same isValidCourseId
guard inside onCreateUnit to early-return or block creation when invalid; update
references to useParams, courseId, useQuery, unitQueries.getUnitList,
initialMode, currentMode, onCreateUnit, and UnitForm accordingly so no requests
or create calls are made with NaN.

---

Nitpick comments:
In `@src/entities/assignment/api/assignmentQueries.ts`:
- Around line 11-18: The queryKey used in the queryOptions call is too generic
(['schedules']) and may collide with other schedule queries; update the
queryOptions invocation that references queryKey and getAssignmentSchedules so
the key is namespaced (for example ['assignments','schedules'] or similar) to
include the domain, ensuring any select logic mapping scheduleCount/schedules
remains unchanged.

In `@src/entities/assignment/model/schemas.ts`:
- Line 2: The module currently imports Zod with a default import (`import z from
'zod'`); change this to the named import style used elsewhere (`import { z }
from 'zod'`) so the schemas in this file use the same import pattern as other
schema files (e.g., `src/entities/unit/model/schemas.ts`)—update the import
statement at the top and ensure all uses of `z` remain unchanged.

In `@src/entities/auth/api/authApi.ts`:
- Line 16: Replace the direct zod parse call to avoid uncaught ZodError from
crashing callers: in authApi.ts where
kakaoLoginResponseSchema.parse(response.data.response) is used, switch to
kakaoLoginResponseSchema.safeParse(...) (or wrap parse in a try/catch for
ZodError) and handle validation failures by logging the validation error and
throwing a controlled Error (or returning a structured failure) so callers of
the function (the auth API method that consumes response.data.response) can
handle the invalid-response case gracefully.

In `@src/entities/auth/model/schemas.ts`:
- Around line 3-10: Export a TypeScript type inferred from the existing Zod
schema so other modules can reuse the runtime-validated shape; add an exported
type alias like "KakaoLoginResponse" using z.infer<typeof
kakaoLoginResponseSchema> (referencing the kakaoLoginResponseSchema symbol) so
callers can import the inferred type for static typing alongside the runtime
schema.

In `@src/entities/student/model/schemas.ts`:
- Around line 18-25: studentUnitSchema currently accepts any string for
startDate and endDate; tighten validation by replacing the z.string() types for
startDate and endDate with a datetime or regex validator (e.g., use
z.string().datetime() for ISO‑8601 or z.string().regex(...) for a custom
YYYY-MM-DD pattern) so the schema (studentUnitSchema) enforces proper date
formatting for startDate and endDate and fails fast on invalid values.

In `@src/entities/unit/api/unitApi.ts`:
- Around line 10-17: The inline response schema in getAllUnitsByCourseId should
be extracted to a reusable exported symbol: create unitListResponseSchema in
src/entities/unit/model/schemas.ts using unitSchema.pick({id: true, title: true,
assignmentCount: true}) wrapped with z.object({count: z.number(), units:
z.array(...)}); then in src/entities/unit/api/unitApi.ts import
unitListResponseSchema and pass it to
apiResponseSchema(...).parse(response.data) instead of the inline z.object;
update any imports/usages and tests that referenced the previous inline shape to
use unitListResponseSchema.

In `@src/entities/unit/api/unitQueries.ts`:
- Around line 21-24: The select callback in the query configuration can be
simplified to a single expression; update the select function used alongside
queryFn (where queryFn: unitId ? () => getUnitById(unitId) : skipToken) to
return data.response with a concise one-line arrow function instead of the
current block body — locate the select option in the query definition and
replace the multi-line function with a single-line expression.

In `@src/entities/unit/model/schemas.ts`:
- Around line 23-25: The current Zod .refine on the unit schema compares
releaseDate and dueDate as strings which is format-dependent; modify the
refinement in schemas.ts to parse both fields into Date (or epoch numbers)
inside the refine callback (e.g., new Date(data.releaseDate).getTime() and new
Date(data.dueDate).getTime()), check for invalid dates (isNaN) and return false
(or set a specific error) when parsing fails, then compare the numeric
timestamps to ensure a correct chronological check for releaseDate <= dueDate;
update the refine attached to the schema that references releaseDate and dueDate
accordingly.

In `@src/entities/unit/model/types.ts`:
- Line 1: The file currently imports the zod runtime object with "z" but only
uses it for type inference (z.infer); change the import to a type-only import
and update usages accordingly: replace the value import of z with a type import
(so TypeScript erases it at runtime), update any z.infer usages to reference the
imported type alias (e.g., use the proper imported type helper or a type alias
like Infer<typeof YourSchema>), and if any code actually needs the runtime z
object leave a regular import there—refer to the symbol "z" and occurrences of
"z.infer" to locate and modify the imports and type usages.

In `@src/entities/unit/model/useUnitStore.ts`:
- Around line 24-29: In storeFormData, instead of creating an object with
explicit key: value pairs, use the ES object property shorthand for title,
releaseDate, and dueDate so the set call passes { title, releaseDate, dueDate }
directly; update the storeFormData implementation that calls set(...) (function
name: storeFormData, caller: set) to use the shortened property names.

In `@src/features/assignment/filter-assignment/lib/useAssignmentList.ts`:
- Around line 12-17: Both queries in the useAssignmentList hook are always
registered; make them conditional by passing the `enabled` option to useQuery so
they only run when appropriate: add `enabled: true/false` to the useQuery call
that uses `assignmentQueries.getAllAssignments()` and to the call using
`assignmentQueries.getAssignmentsByCourse(selectedCourseId ?? 0)` (use `enabled:
!!selectedCourseId` or `enabled: selectedCourseId !== 0` for the course-specific
query, or `enabled: selectedCourseId == null` for the all-assignment query
depending on desired behavior) so the queries are not executed unless the
required `selectedCourseId` state is present; look for the `useQuery` calls and
the `selectedCourseId` variable inside `useAssignmentList` to apply this change.

In `@src/pages/course-overview/CourseOverviewPage.tsx`:
- Line 15: The hasUnits assignment uses an unnecessary ternary; replace the
expression that sets hasUnits (currently using course.unitCount !== 0 ? true :
false) with a direct boolean expression such as course.unitCount !== 0 or
Boolean(course.unitCount) in CourseOverviewPage.tsx so hasUnits is computed
without the redundant ternary.

In `@src/pages/course-overview/ui/CourseActionsBar.tsx`:
- Around line 17-18: Remove the unnecessary template literals when passing the
color prop to buttonStyles in CourseActionsBar.tsx: where color is currently set
as color: `${isActiveCourse ? 'outlineWhite' : 'outlinePurple'}` (and the
similar usage at the other occurrence around lines 24-25), change it to pass the
string directly using the ternary result (color: isActiveCourse ? 'outlineWhite'
: 'outlinePurple') so buttonStyles receives a plain string instead of a template
literal.

In `@src/pages/manage-assignment/AssignmentManagePage.tsx`:
- Around line 31-48: The current deleteAssignment mutation always invalidates
assignmentQueries.getAssignmentsByCourse(selectedCourseId ?? 0) which uses 0
when selectedCourseId is null; change this to only call
queryClient.invalidateQueries for
assignmentQueries.getAssignmentsByCourse(selectedCourseId).queryKey when
selectedCourseId is non-null/defined (e.g., guard on selectedCourseId !==
null/undefined) so you don't attempt to invalidate a non-existent course query;
keep the existing invalidateQueries for getAllAssignments and the
onSuccess/onError handlers intact.
- Line 42: Replace the browser alert() calls in the AssignmentManagePage
component with the app's toast/modal utilities to maintain consistent UX: locate
the two alert('문제가 성공적으로 삭제되었습니다.') occurrences (the success callbacks after
question/assignment deletion) and call the project's toast or modal API (e.g.,
showToast or openModal) instead, passing a localized success message and
optional success variant/options; ensure you import and use the existing
notification component used elsewhere in the app and remove the alert() usages.

In `@src/pages/manage-assignment/ui/AssignmentManageActionsBar.tsx`:
- Around line 28-33: AssignmentManageActionsBar has icon-only buttons for
edit/delete (buttons calling handleOnEdit and handleOnDelete with
EditIcon/DeleteIcon) that lack accessible labels; update the JSX to add
descriptive aria-label attributes (e.g., aria-label="Edit assignment" and
aria-label="Delete assignment") on the respective buttons and ensure the icon
SVGs remain aria-hidden (or role="img" with appropriate title) so screen readers
read the button labels, not the icons.
- Around line 16-22: Rename the handler functions for brevity: change
handleOnEdit to handleEdit and handleOnDelete to handleDelete, update their
implementations to still call navigate(`/admin/assignments/${id}`) and
onDelete(id) respectively, and replace all usages/references (e.g., JSX props or
event bindings) that currently reference handleOnEdit or handleOnDelete to the
new names so nothing breaks; ensure any tests or prop types referencing those
function names are updated as well.

In `@src/pages/select-assignment/AssignmentSelectPage.tsx`:
- Around line 70-79: The onClick prop for the cancel Button is using an
unnecessary arrow wrapper; change the handler to pass the function reference
directly by replacing the onClick={() => returnToPreviousPage()} usage with
onClick={returnToPreviousPage} to simplify the code and avoid creating an extra
closure (refer to returnToPreviousPage, handleConfirm, and the Button component
in AssignmentSelectPage).

In `@src/pages/select-assignment/ui/AssignmentListContainer.tsx`:
- Line 5: The module-level type alias T fixes the generic, so either remove the
module-level alias and inline the concrete type where used (e.g., replace uses
of T with AssignmentCourse['assignments'][number] and keep
AssignmentListContainerProps non-generic), or make the component truly generic
by removing the top-level T and adding a type parameter on the component
declaration (e.g., declare AssignmentListContainer as a generic component using
AssignmentListContainerProps<T>) and update all references to use that
parameter; adjust imports/exports accordingly to ensure
AssignmentListContainerProps and the component share the same generic parameter
rather than relying on a module-scoped T.

In `@src/pages/unit-editor/model/types.ts`:
- Around line 5-10: The Mode type was changed to remove 'idle' and
UnitFormProps.mode was made nullable, which reduces clarity; restore 'idle' by
updating the Mode type to "type Mode = 'idle' | 'creating' | 'editing'" and
change UnitFormProps.mode to use Mode (not Mode | null); then update any callers
of UnitFormProps (e.g., components constructing UnitFormProps) to pass 'idle' as
the default state instead of null (or handle the new non-nullable Mode
accordingly) so the intent remains explicit; refer to the Mode type and
UnitFormProps interface to locate and change the definitions and call sites.

In `@src/shared/model/schemas.ts`:
- Around line 10-14: errorResponseSchema에 정의된 errors 객체가 Swagger 예시
필드명(additionalProp1/2/3)을 사용하고 있어 일반적인 에러 키를 표현하지 못하므로, errors: z.object({...})
대신 errors: z.record(z.string(), z.string())로 변경해 동적 키/값 형태의 에러 응답을 허용하도록 바꿔주세요;
관련 심볼은 errorResponseSchema과 errors 필드이며, 변경 시 기존 호출부에서 특정 엔티티별 스키마를 별도로 정의하는
방식에는 영향이 없도록 테스트 케이스나 타입 호환성을 확인해 주세요.

In `@src/shared/ui/LabeledInput.tsx`:
- Line 13: The LabeledInput component currently sets the prop default required =
true which deviates from standard HTML input default and forces callers to
override; update the component by either (A) removing the default and setting
required to undefined in the props/defaults so consumers must opt-in with
required={true}, or (B) if keeping true is intentional, add a JSDoc comment on
the LabeledInput component describing that required defaults to true; locate the
default assignment (required = true) in LabeledInput.tsx and implement one of
these two changes and adjust any callers if you choose option A.

ℹ️ 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 2c2b834 and df8758b.

📒 Files selected for processing (58)
  • src/App.tsx
  • src/entities/assignment/api/assignmentApi.ts
  • src/entities/assignment/api/assignmentMutations.ts
  • src/entities/assignment/api/assignmentQueries.ts
  • src/entities/assignment/api/assignmentQueryOptions.ts
  • src/entities/assignment/model/schemas.ts
  • src/entities/assignment/model/types.ts
  • src/entities/auth/api/authApi.ts
  • src/entities/auth/model/schemas.ts
  • src/entities/auth/model/useUserStore.ts
  • src/entities/course/api/courseApi.ts
  • src/entities/course/api/courseMutations.ts
  • src/entities/course/api/courseQueries.ts
  • src/entities/course/api/courseQueryOptions.ts
  • src/entities/course/index.ts
  • src/entities/course/model/schemas.ts
  • src/entities/course/model/types.ts
  • src/entities/student/model/schemas.ts
  • src/entities/student/model/types.ts
  • src/entities/unit/api/unitApi.ts
  • src/entities/unit/api/unitMutations.ts
  • src/entities/unit/api/unitQueries.ts
  • src/entities/unit/model/schemas.ts
  • src/entities/unit/model/types.ts
  • src/entities/unit/model/useUnitStore.ts
  • src/features/assignment/filter-assignment/lib/useAssignmentList.ts
  • src/pages/course-overview/CourseOverviewPage.tsx
  • src/pages/course-overview/ui/AssignmentList.tsx
  • src/pages/course-overview/ui/CourseActionsBar.tsx
  • src/pages/course-overview/ui/CourseHero.tsx
  • src/pages/course-overview/ui/UnitItem.tsx
  • src/pages/dashboard/Dashboard.tsx
  • src/pages/manage-assignment/AssignmentManagePage.tsx
  • src/pages/manage-assignment/ui/AssignmentManageActionsBar.tsx
  • src/pages/select-assignment/AssignmentSelectPage.tsx
  • src/pages/select-assignment/ui/AssignmentListContainer.tsx
  • src/pages/unit-editor/UnitEditorPage.tsx
  • src/pages/unit-editor/mock/unitMock.ts
  • src/pages/unit-editor/model/types.ts
  • src/pages/unit-editor/ui/UnitAssignmentList.tsx
  • src/pages/unit-editor/ui/UnitForm.tsx
  • src/pages/unit-editor/ui/UnitList.tsx
  • src/shared/lib/course.ts
  • src/shared/mocks/assignmentDummyResponse.ts
  • src/shared/mocks/assignmentDummyTypes.ts
  • src/shared/mocks/assignmentSelectResponse.ts
  • src/shared/mocks/courseOverviewResponse.ts
  • src/shared/mocks/dashboardCourseList.ts
  • src/shared/mocks/dashboardScheduleList.ts
  • src/shared/model/index.ts
  • src/shared/model/schemas.ts
  • src/shared/model/type.ts
  • src/shared/ui/LabeledInput.tsx
  • src/shared/ui/Layout.tsx
  • src/shared/ui/button/Button.tsx
  • src/shared/ui/list-row/ListRow.tsx
  • src/shared/ui/list-row/list-row-styles.ts
  • src/widgets/assignment-page-layout/ui/AssignmentPageLayout.tsx
💤 Files with no reviewable changes (12)
  • src/entities/course/index.ts
  • src/shared/mocks/assignmentSelectResponse.ts
  • src/entities/course/api/courseQueryOptions.ts
  • src/entities/assignment/api/assignmentQueryOptions.ts
  • src/pages/unit-editor/mock/unitMock.ts
  • src/shared/mocks/assignmentDummyResponse.ts
  • src/shared/mocks/assignmentDummyTypes.ts
  • src/pages/unit-editor/ui/UnitAssignmentList.tsx
  • src/shared/mocks/courseOverviewResponse.ts
  • src/shared/mocks/dashboardCourseList.ts
  • src/shared/mocks/dashboardScheduleList.ts
  • src/shared/model/type.ts

Comment on lines +9 to +10
const {id} = useParams();
const courseId = Number(id);
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

id 파라미터 유효성 검증이 필요합니다.

useParams()에서 반환되는 idundefined일 경우, Number(undefined)NaN을 반환하여 API 호출 시 예기치 않은 동작이 발생할 수 있습니다.

🛡️ 유효성 검증 추가 제안
 const {id} = useParams();
 const courseId = Number(id);
+
+if (isNaN(courseId)) {
+  throw new Error('Invalid course ID');
+}

또는 React Router의 타입 안전성을 위해 loader에서 검증하는 방법도 고려해보세요.

📝 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
const {id} = useParams();
const courseId = Number(id);
const {id} = useParams();
const courseId = Number(id);
if (isNaN(courseId)) {
throw new Error('Invalid course ID');
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/course-overview/CourseOverviewPage.tsx` around lines 9 - 10, The
code currently converts useParams()'s id to a number without validation (const
{id} = useParams(); const courseId = Number(id);) which can produce NaN; update
CourseOverviewPage to validate id before conversion: ensure id is a defined
string and parseInt/Number yields a finite integer (e.g., typeof id === 'string'
&& !Number.isNaN(Number(id))); if invalid, handle it (redirect to a 404/error
page, show an error state, or bail out before any API calls that use courseId).
Place this check where courseId is created/used (in the CourseOverviewPage
component) so all subsequent API calls and logic use a guaranteed valid
courseId.

Comment on lines +5 to 12
type T = AssignmentCourse['assignments'][number];

interface AssignmentListContainerProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
renderItem: (item: T) => ReactNode;
title: string;
onSelect: (id: number) => void;
onSelect?: (item: Assignment) => void;
}
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

제네릭 타입 TAssignment 타입 간의 불일치가 있습니다.

TAssignmentCourse['assignments'][number] (즉, {id: number, title: string})로 고정되어 있지만, onSelecthandleSelectAssignment 타입을 기대합니다. AssignmentsubmittedStatus 필드를 포함할 수 있어 타입이 정확히 일치하지 않습니다.

🔧 타입 일관성 개선 제안
-import type {Assignment} from '@/entities/assignment/model/types';
 import type {AssignmentCourse} from '@/entities/course/model/types';
 import type {MouseEvent, ReactNode} from 'react';

 type T = AssignmentCourse['assignments'][number];

 interface AssignmentListContainerProps<T> {
   items: T[];
   renderItem: (item: T) => ReactNode;
   title: string;
-  onSelect?: (item: Assignment) => void;
+  onSelect?: (item: T) => void;
 }

 const AssignmentListContainer = ({
   onSelect,
   items,
   renderItem,
   title,
 }: AssignmentListContainerProps<T>) => {
-  const handleSelect = (item: Assignment, event: MouseEvent) => {
+  const handleSelect = (item: T, event: MouseEvent) => {
     event.stopPropagation();
     onSelect?.(item);
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/select-assignment/ui/AssignmentListContainer.tsx` around lines 5 -
12, The generic type T is constrained to AssignmentCourse['assignments'][number]
while onSelect and handleSelect expect an Assignment (which may include
submittedStatus), causing a type mismatch; update the component props and
handlers to be consistent by either broadening the generic constraint to
Assignment (e.g., make AssignmentListContainerProps<T extends Assignment>) or by
changing onSelect and handleSelect signatures to accept T instead of Assignment;
locate and update AssignmentListContainerProps, the renderItem prop, and the
handleSelect/onSelect usages so types align (choose one approach and apply it
across the component).

Comment on lines 15 to +27
const {id} = useParams(); // 강의 ID
const courseId = Number(id);
const [mode, setMode] = useState<Mode>('idle');

const {
resetStore,
title: storedTitle,
assignments: storedAssignments,
releaseDate: storedReleaseDate,
dueDate: storedDueDate,
} = useUnitStore();
const {data} = useQuery(unitQueries.getUnitList(courseId));
const initialMode =
data === undefined ? null : data.unitCount === 0 ? 'creating' : 'editing';
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

🧩 Analysis chain

🏁 Script executed:

cat -n src/pages/unit-editor/UnitEditorPage.tsx | head -80

Repository: 2025-snowCode/snowCode-Client

Length of output: 3267


🏁 Script executed:

cat -n src/pages/unit-editor/UnitEditorPage.tsx | sed -n '80,170p'

Repository: 2025-snowCode/snowCode-Client

Length of output: 2918


🏁 Script executed:

cat -n src/pages/unit-editor/UnitEditorPage.tsx | sed -n '1,50p'

Repository: 2025-snowCode/snowCode-Client

Length of output: 2236


🏁 Script executed:

rg -A5 "export.*getUnitList" src/entities/unit/api/ -B2

Repository: 2025-snowCode/snowCode-Client

Length of output: 55


🏁 Script executed:

find src -name "*unitQueries*" -o -name "*unitMutations*"

Repository: 2025-snowCode/snowCode-Client

Length of output: 148


🏁 Script executed:

cat src/entities/unit/api/unitQueries.ts

Repository: 2025-snowCode/snowCode-Client

Length of output: 848


🏁 Script executed:

cat src/entities/unit/api/unitApi.ts

Repository: 2025-snowCode/snowCode-Client

Length of output: 1786


🏁 Script executed:

cat src/entities/unit/api/unitMutations.ts

Repository: 2025-snowCode/snowCode-Client

Length of output: 767


🏁 Script executed:

python3 << 'EOF'
# Verify the order of operations
operations = {
    15: "const {id} = useParams()",
    16: "const courseId = Number(id)",
    25: "useQuery(unitQueries.getUnitList(courseId)) - CALLED HERE",
    140: "if (!id) return <EmptyState>",
}

print("Order of operations:")
for line, desc in sorted(operations.items()):
    print(f"  Line {line}: {desc}")
print("\n❌ ISSUE: useQuery is called at line 25 with potentially NaN courseId")
print("           Guard check doesn't happen until line 140")
print(f"\n✓ When id is undefined:")
print(f"  - Number(undefined) = NaN")
print(f"  - API call: /courses/NaN/units")
EOF

Repository: 2025-snowCode/snowCode-Client

Length of output: 487


🏁 Script executed:

rg "useQuery.*courseId|useQuery.*Number\(id\)" src/ -B3 -A2

Repository: 2025-snowCode/snowCode-Client

Length of output: 567


🏁 Script executed:

cat src/pages/unit-editor/ui/UnitForm.tsx | head -50

Repository: 2025-snowCode/snowCode-Client

Length of output: 1596


유효하지 않은 courseId로 쿼리와 생성 요청이 나갈 수 있습니다.

Line 16에서 Number(id)로 변환한 후 Line 25에서 바로 useQuery를 호출하는데, id가 없으면 courseIdNaN이 됩니다. Line 140의 가드 체크는 렌더링만 방지할 뿐 이미 발동한 쿼리는 /courses/NaN/units 요청을 보내게 됩니다. Line 124의 onCreateUnit에서도 동일하게 NaN 값이 전달됩니다.

Line 26-27의 initialModenull이 될 수 있고 Line 39에서 currentModenull로 유지되어 Line 158에서 UnitForm에 전달되는데, 여기서 의도하지 않은 생성 모드 로직이 발동할 수 있습니다.

Number.isFinite(courseId) 검증 가드를 먼저 두고, 쿼리의 enabled 옵션으로 막아주세요.

🔧 제안 수정안
 const {id} = useParams(); // 강의 ID
 const courseId = Number(id);
+const hasValidCourseId = Number.isFinite(courseId);

-const {data} = useQuery(unitQueries.getUnitList(courseId));
+const {data} = useQuery({
+  ...unitQueries.getUnitList(courseId),
+  enabled: hasValidCourseId,
+});

 const onCreateUnit = (unitForm: TUnitFormSchema) => {
+  if (!hasValidCourseId) return;
   addUnit({courseId, unitForm});
 };

-if (!id) {
+if (!hasValidCourseId) {
   return <EmptyState>선택한 강의가 없습니다.</EmptyState>;
 }

참고: https://tanstack.com/query/latest/docs/framework/react/guides/disabling-queries

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

In `@src/pages/unit-editor/UnitEditorPage.tsx` around lines 15 - 27, The code can
call queries and create requests with an invalid courseId (NaN); compute
courseId as Number(id) and guard it with Number.isFinite(courseId) before using
it: create a boolean like isValidCourseId = Number.isFinite(courseId) and pass
enabled: isValidCourseId to useQuery(unitQueries.getUnitList(courseId)); compute
initialMode only when isValidCourseId is true (otherwise keep null) and add the
same isValidCourseId guard inside onCreateUnit to early-return or block creation
when invalid; update references to useParams, courseId, useQuery,
unitQueries.getUnitList, initialMode, currentMode, onCreateUnit, and UnitForm
accordingly so no requests or create calls are made with NaN.

Comment on lines +26 to +43
const initialMode =
data === undefined ? null : data.unitCount === 0 ? 'creating' : 'editing';

const [activeMode, setActiveMode] = useState<Mode | null>(null);
const [selectedUnitId, setSelectedUnitId] = useState<number | null>(null);
const [currentIndex, setCurrentIndex] = useState<number>(1);
const {data: unitList} = useQuery(unitQueries.getUnitList(courseId));
const {data: unit} = useQuery(unitQueries.getUnitDetails(selectedUnitId));

useEffect(() => {
if (unitList && unitList?.response.count !== 0 && mode === 'idle') {
setSelectedUnitId(unitList.response.units[0].id);
setMode('editing'); // 편집 모드
} else if (unitList?.response.count === 0 && mode === 'idle') {
setMode('creating'); // 생성 모드

const hasOngoingCreation =
storedTitle !== '' ||
storedReleaseDate !== '' ||
storedDueDate !== '' ||
storedAssignments.length > 0;

const currentMode: Mode | null =
activeMode ?? (hasOngoingCreation ? 'creating' : initialMode);
const currentUnitId =
currentMode === 'creating'
? null
: (selectedUnitId ?? data?.firstUnitId ?? null);
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

currentModenull 상태가 폼 동작으로 전파됩니다.

Line 38-39에서 currentModenull일 수 있고, Line 161에서 그대로 UnitForm에 전달됩니다. 이 상태에서 저장 시 생성 분기로 흐를 수 있어 의도치 않은 단원 생성이 가능합니다. 로딩 중에는 폼 렌더를 보류하거나, UnitForm에는 확정된 모드(creating | editing)만 전달되도록 정리해 주세요.

🔧 제안 수정안
-const currentMode: Mode | null =
+const currentMode: Mode | null =
   activeMode ?? (hasOngoingCreation ? 'creating' : initialMode);

+if (!hasOngoingCreation && currentMode === null) {
+  return <EmptyState>단원 정보를 불러오는 중입니다.</EmptyState>;
+}

 <UnitForm
   unit={unitDetail}
   unitIndex={currentIndex}
-  mode={currentMode}
+  mode={currentMode}
   onCreateUnit={onCreateUnit}

참고: https://react.dev/learn/conditional-rendering

Also applies to: 158-162

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

In `@src/pages/unit-editor/UnitEditorPage.tsx` around lines 26 - 43, currentMode
can be null and is being passed into UnitForm causing unintended creation flows;
change the render logic so UnitForm only receives a concrete Mode ('creating' |
'editing') — use activeMode, hasOngoingCreation and initialMode to derive a
non-null mode before rendering or simply conditionally render/return null while
currentMode === null (or while data/loading). Specifically, update the logic
around currentMode/currentUnitId (referencing currentMode, activeMode,
hasOngoingCreation, initialMode, selectedUnitId) so that UnitForm is not mounted
with currentMode === null; ensure any props passed to UnitForm are strictly
'creating' | 'editing' and delay form rendering until a definite mode is
determined.

Copy link
Contributor

@JiiminHa JiiminHa left a comment

Choose a reason for hiding this comment

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

#59 커밋들만 확인 완료했습니다!


{isOpen ? (
<Link to={`/assignments/${index}`} className='min-w-0'>
<Link to={`/admin/assignments/${index}`} className='min-w-0'>
Copy link
Contributor

Choose a reason for hiding this comment

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

이것도 라우트 상수로 쓰면 어떨까해요!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🧩 feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 강의 상세 페이지 API 연동

2 participants