Skip to content

#53 feat: 강의 개설 페이지 API 연동#67

Merged
JiiminHa merged 54 commits intodevelopfrom
feat/53-course-add
Mar 15, 2026
Merged

#53 feat: 강의 개설 페이지 API 연동#67
JiiminHa merged 54 commits intodevelopfrom
feat/53-course-add

Conversation

@JiiminHa
Copy link
Copy Markdown
Contributor

@JiiminHa JiiminHa commented Mar 11, 2026

⚙️ Related ISSUE Number

closed #53



📄 Work Description

1. API 엔드포인트 중앙화 및 리팩토링

  • 중앙 집중식 관리: assignmentApi.ts, authApi.ts, courseApi.ts, unitApi.ts 내의 모든 API 호출이 @/shared/config/endpoints에 정의된 ENDPOINTS 객체를 사용하도록 리팩토링되었습니다. 하드코딩된 URL 문자열을 대체함으로써 유지보수성을 높였습니다.

2. 코스 관리 기능 추가

  • 코스 생성 및 편집 API: createCourse, updateCourse API와 관련 요청/응답 스키마, TypeScript 타입을 추가했습니다. 또한, 앱 전체에서 사용할 수 있도록 모듈 인덱스에서 이를 내보내기(export) 하도록 설정했습니다.
  • 타입 안전성 강화: Schedule, Unit, BaseCourse, CourseOverview 등 코스 관련 엔티티에 대한 포괄적인 TypeScript 인터페이스를 정의하여 코드의 명확성과 타입 안정성을 확보했습니다.

3. ESLint 설정 및 프로젝트 구조 강제화

  • FSD(Feature-Sliced Design) 도입: 아키텍처 경계와 FSD 원칙을 강제하기 위해 eslint-plugin-boundaries@feature-sliced/steiger-plugin을 설정했습니다. 이를 통해 app, pages, widgets, features, entities, shared 계층 간의 임포트를 제한하는 상세 규칙을 적용했습니다.
  • 의존성 업데이트: 새 ESLint 플러그인을 추가하고, zod 등 기존 라이브러리를 업데이트했습니다.

4. 인증 및 카카오 로그인 리팩토링

  • 관심사 분리: 카카오 로그인 관련 Mutation을 entities/auth에서 features/auth/kakao로 이동하고, authMutationskakaoMutations로 이름을 변경하여 역할을 명확히 분리했습니다.
  • 데이터 견고함: 카카오 로그인 스키마에서 studentIdnull이 될 수 있도록 허용하여 예외 상황에 대한 대응력을 높였습니다. 또한, 로그인 훅 내의 내비게이션 로직이 중앙화된 ROUTES 객체를 사용하도록 업데이트했습니다.

5. 기타 개선 사항

  • 일관성 유지: 과제(assignment) 관련 쿼리와 타입에 대한 별칭(alias) 및 내보내기를 추가하여 임포트 과정을 간소화했습니다.
  • 스키마 보완: 코스 개요 스키마에서 설명(description) 항목이 누락되었을 경우 기본값을 사용하도록 처리했습니다.
  • 라우팅 업데이트: App.tsx에서 코스 편집 경로를 지원하고 관련 페이지 임포트 경로를 수정했습니다.


📷 Screenshot

스크린샷 2026-03-15 오후 12 34 21 스크린샷 2026-03-15 오후 12 34 31 스크린샷 2026-03-15 오후 12 33 53



💬 To Reviewers



🔗 Reference

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 11, 2026

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 15, 2026 9:09am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 11, 2026

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

강의 생성·수정 기능(스키마, API, 훅, 폼, 페이지)과 경로 상수화(ROUTES)가 추가되었습니다. API 오류 처리 유틸(handleApiError), ConfirmModal, 입력 컴포넌트 개선(forwardRef, 에러/기본값), 삭제 흐름의 ConfirmModal 전환, ESLint 경계 설정 및 패키지 변경이 포함됩니다.

Changes

Cohort / File(s) Summary
ESLint & 패키지
eslint.config.js, package.json
eslint-plugin-boundaries, @feature-sliced/steiger-plugin 설정 추가 및 zod 버전 업(및 devDependency 추가).
라우팅 상수화
src/shared/config/routes.ts, src/App.tsx
전역 ROUTES 도입; App에 강의 편집 경로(/courses/:id/edit) 추가 및 관련 import/exports를 명명형으로 변경.
강의 API/엔티티 인덱스
src/entities/course/api/courseApi.ts, src/entities/course/index.ts
createCourse, updateCourse API 추가 및 엔티티 인덱스에 재내보내기 추가.
도메인 스키마·타입
src/entities/course/model/courseSchema.ts, src/entities/course/model/schemas.ts, src/entities/course/model/types.ts
강의 생성/응답 Zod 스키마와 타입 추가, courseOverview에 description 필드 추가, 도메인 타입(인터페이스) 추가.
폼·훅·위젯
src/features/course/create-course/model/courseFormSchema.ts, src/features/course/create-course/model/useCreateCourse.ts, src/features/course/edit-course/model/useEditCourse.ts, src/widgets/course-form/ui/CourseForm.tsx
폼 옵션/맵, Zod 폼 스키마, CourseForm 컴포넌트와 useCreateCourse/useEditCourse 훅(React Query 연동) 추가.
삭제 흐름 리팩터링
src/features/course/delete-course/model/useDeleteCourse.ts, src/pages/dashboard/ui/CourseManagementDropdown.tsx
useDeleteCourse 훅 추가. Dropdown에서 onDelete prop 제거하고 ConfirmModal 기반 내부 삭제 흐름으로 전환.
관리 페이지(생성/수정)
src/pages/admin/courses/CourseCreatePage.tsx, src/pages/admin/courses/CourseEditPage.tsx
CourseCreatePage를 CourseForm으로 리팩터링(명명된 export), CourseEditPage 신규 추가(기본값/로딩 처리 포함).
대시보드·리스트·카드 변경
src/pages/dashboard/Dashboard.tsx, src/pages/dashboard/ui/CourseList.tsx, src/pages/dashboard/ui/CourseCard.tsx
클라이언트 삭제 로직 제거, CourseList/CourseCard에서 onDelete 제거, ROUTES 사용으로 네비게이션 통일.
공용 UI 추가·수정
src/shared/ui/ConfirmModal.tsx, src/shared/ui/BaseHeader.tsx, src/shared/ui/LabeledDropdown.tsx, src/shared/ui/LabeledInput.tsx, src/widgets/assignment-form-layout/ui/AssignmentFormLayout.tsx
ConfirmModal 추가, BaseHeader에 logoHref 추가, LabeledInput/LabeledDropdown에 기본값·오류표시 및 forwardRef 적용, AssignmentFormLayout에 confirmDisabled 추가.
API 오류 처리 유틸 및 경로 교체
src/shared/lib/handleApiError.ts, src/shared/ui/Header.tsx, 여러 페이지들
Axios 에러를 한국어 메시지로 매핑하는 handleApiError 도입; 여러 컴포넌트에서 하드코딩 경로를 ROUTES로 교체.
기타 타입·재내보내기
src/entities/assignment/index.ts, src/entities/assignment/model/schemas.ts, src/entities/auth/model/schemas.ts
assignmentQueries 재내보내기 추가, Assignment 타입 alias 추가, kakaoLoginResponseSchema.studentId가 nullable로 변경.

Sequence Diagram(s)

sequenceDiagram
    participant User as 사용자
    participant Form as CourseForm<br/>(UI)
    participant Hook as useCreateCourse<br/>(훅)
    participant API as courseApi<br/>(HTTP)
    participant Cache as React Query<br/>캐시
    participant Router as 라우터

    User->>Form: 강의 정보 입력 및 확인 클릭
    Form->>Form: courseFormSchema로 검증
    Form->>Hook: submit(formValues)
    Hook->>Hook: SEMESTER_CODE_MAP으로 매핑
    Hook->>API: POST /courses (createCourse)
    API-->>Hook: CreateCourseResponse
    Hook->>Cache: courseQueries.getAllCourses 무효화
    Hook->>Router: navigate(ROUTES.ADMIN.ROOT)
    Router-->>User: 강의 관리 페이지로 이동
Loading
sequenceDiagram
    participant User as 사용자
    participant Page as CourseEditPage<br/>(페이지)
    participant Cache as React Query<br/>캐시
    participant Form as CourseForm<br/>(UI)
    participant Hook as useEditCourse<br/>(훅)
    participant API as courseApi<br/>(HTTP)
    participant Router as 라우터

    Page->>Cache: useQuery로 강의 조회 (getCourseDetails)
    Cache-->>Page: CourseOverview 데이터
    Page->>Form: defaultValues 전달
    User->>Form: 수정 및 확인 클릭
    Form->>Hook: submit(formValues)
    Hook->>API: PUT /courses/{id} (updateCourse)
    API-->>Hook: CreateCourseResponse
    Hook->>Cache: courseQueries.getAllCourses 무효화
    Hook->>Router: navigate(ROUTES.ADMIN.ROOT)
    Router-->>User: 강의 관리 페이지로 이동
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

🧩 feature, 🎨 ui, ⚙️ setting

Suggested reviewers

  • suminb99

짧은 칭찬: 훅·폼·라우트 분리는 깔끔해요 — 유지보수하기 좋습니다.
권장 문서: React Query 캐시 무효화 가이드(https://tanstack.com/query/latest/docs) 및 Zod 사용 가이드(https://github.com/colinhacks/zod), React Hook Form + Zod 통합 예제(https://react-hook-form.com/advanced-usage/#SchemaValidation).

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning 대부분의 변경사항이 강의 개설/수정 기능 구현과 직접 관련 있으나, eslint-plugin-boundaries 추가와 의존성 업데이트는 PR 범위 외입니다. eslint 설정 변경과 zod 의존성 업데이트를 별도 PR로 분리하여 리뷰 범위를 명확히 하세요.
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.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경사항(강의 개설 페이지 API 연동)을 명확하게 설명하고 있으며, 변경 내용과 완벽하게 일치합니다.
Linked Issues check ✅ Passed PR은 강의 개설 페이지 API 연동 구현을 완료했으며, 스키마 정의, API 함수 추가, 폼 컴포넌트 개발, 라우팅 통합 등 모든 필요한 구현을 포함합니다.
Description check ✅ Passed PR 설명이 변경 사항과 직접적으로 관련되어 있습니다. API 엔드포인트 중앙화, 코스 관리 기능, ESLint 설정, 인증 리팩토링 등의 작업 내용이 실제 파일 변경과 일치합니다.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/53-course-add
📝 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.

Copy link
Copy Markdown

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

Caution

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

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

8-35: ⚠️ Potential issue | 🔴 Critical

required/오류 상태를 시각적으로만 표시하지 말고 input에도 전달해 주세요.

지금은 별표와 빨간 테두리만 바뀌고, 실제 input에는 required, aria-invalid, aria-describedby가 없어 브라우저 기본 검증과 스크린리더 안내가 빠집니다.

이 컴포넌트는 CourseForm, UnitForm, AssignmentCreatePage 등 4곳 이상에서 사용 중이므로, 접근성 회귀가 여러 폼에 즉시 영향을 줍니다.

♿ 제안 패치
 const LabeledInput = ({
   label,
   className,
   showLabel = true,
   errorMessage,
   required = true,
   ...rest
 }: LabeledInputProps) => {
+  const errorId = rest.id ? `${rest.id}-error` : undefined;
+
   return (
     <label className='flex flex-col gap-3'>
@@
       <input
+        required={required}
+        aria-invalid={Boolean(errorMessage)}
+        aria-describedby={errorMessage ? errorId : undefined}
         className={`h-11 rounded-[9px] border px-[14px] outline-none focus:border-primary ${
           errorMessage ? 'border-badge-red' : 'border-purple-stroke'
         } ${className ?? ''}`}
         {...rest}
       />
 
       {errorMessage && (
-        <span className='text-sm text-badge-red'>{errorMessage}</span>
+        <span id={errorId} className='text-sm text-badge-red'>
+          {errorMessage}
+        </span>
       )}

참고: MDN required / aria-invalid / 폼 접근성
https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/required
https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-invalid
https://developer.mozilla.org/en-US/docs/Learn/Accessibility/WAI-ARIA_basics/

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

In `@src/shared/ui/LabeledInput.tsx` around lines 8 - 35, The LabeledInput
component currently only shows visual required/error cues; update it to pass
accessibility attributes into the <input> by forwarding the required prop to the
input, setting aria-invalid to true when errorMessage is present, and adding
aria-describedby that points to the error message element. To implement this,
ensure the input has an id (use the incoming rest.id if provided or React's
useId() inside LabeledInput to generate one), render the error <span> with a
matching unique id (e.g., `${id}-error`), and set aria-describedby to that id
only when errorMessage exists; keep existing visual changes intact and reference
the LabeledInput props required and errorMessage and the input/error span
identifiers when making the change.
🧹 Nitpick comments (6)
src/shared/lib/handleApiError.ts (2)

26-29: fallbackMessage 사용 시 원본 에러 메시지 손실 가능성

fallbackMessage가 제공되면 getApiErrorMessage 결과가 무시됩니다. 의도된 동작이라면 괜찮지만, 에러 컨텍스트를 유지하면서 커스텀 메시지를 보여주고 싶다면 로깅에 원본 메시지를 포함하는 것을 고려해보세요.

현재 코드베이스 전반에서 alert()을 사용하고 있어 일관성은 유지되지만, 추후 Toast/Modal 시스템으로 전환할 때 이 함수가 중앙 포인트가 될 수 있습니다.

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

In `@src/shared/lib/handleApiError.ts` around lines 26 - 29, The current
handleApiError function ignores the original error message when fallbackMessage
is provided; update handleApiError to always capture and log the original API
error message from getApiErrorMessage(error) while still showing the
fallbackMessage to the user (use console.error to emit both the fallbackMessage
and the original message/context from getApiErrorMessage(error) along with the
raw error), keep alert(message) behavior but ensure logging retains full error
context so callers using handleApiError (and functions like getApiErrorMessage)
can debug later.

6-21: HTTP 상태 코드 매핑 - 429 (Too Many Requests) 추가 고려

현재 400, 401, 403, 404, 409, 5xx를 처리하고 있습니다. Rate limiting 대응을 위해 429 상태도 추가하면 좋겠습니다.

♻️ 429 상태 추가 제안
     if (status === 409) return '이미 존재하는 정보입니다.';
+    if (status === 429) return '요청이 너무 많습니다. 잠시 후 다시 시도해주세요.';
     if (status && status >= 500) return '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/lib/handleApiError.ts` around lines 6 - 21, Add handling for HTTP
429 inside getApiErrorMessage: when axios.isAxiosError(error) detects a response
with status === 429 return a clear rate-limit message (e.g., '요청이 너무 많습니다. 잠시 후
다시 시도해주세요.') and place this check alongside the other status checks (400, 401,
403, 404, 409, 5xx) so 429 is matched before the generic fallbacks; keep the
rest of the function behavior unchanged.
src/features/course/delete-course/model/useDeleteCourse.ts (1)

2-2: 변수명 섀도잉 주의: deleteCourse 중복 사용

Line 2에서 deleteCourse API 함수를 import하고, Line 24에서 mutatedeleteCourse로 재명명하여 반환합니다. 이름이 동일해 혼란을 줄 수 있습니다.

♻️ 명확한 네이밍 제안
- import {deleteCourse, courseQueries} from '@/entities/course';
+ import {deleteCourse as deleteCourseApi, courseQueries} from '@/entities/course';

  const {mutate, isPending} = useMutation({
-   mutationFn: () => deleteCourse(courseId),
+   mutationFn: () => deleteCourseApi(courseId),
    ...
  });

  return {deleteCourse: mutate, isPending};

또는 반환값 이름을 deleteCourseMutation으로 변경하는 방법도 있습니다.

Also applies to: 24-24

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

In `@src/features/course/delete-course/model/useDeleteCourse.ts` at line 2, The
imported API function name deleteCourse is shadowed by the returned mutate value
which is renamed to deleteCourse; to fix, avoid name collision by renaming one
of them (e.g., import { deleteCourse as deleteCourseApi } from
'@/entities/course' or return the mutation as deleteCourseMutation) and update
all references accordingly (look for the import line referencing deleteCourse
and the hook return where mutate is aliased to deleteCourse).
src/features/course/create-course/model/courseFormSchema.ts (1)

3-3: YEAR_OPTIONS 하드코딩에 대해 고려해 보세요.

현재 2026년까지만 포함되어 있어 매년 업데이트가 필요합니다. 현재 연도 기준으로 동적 생성하는 방안을 고려해 볼 수 있습니다.

♻️ 동적 연도 생성 예시
const currentYear = new Date().getFullYear();
export const YEAR_OPTIONS = Array.from(
  {length: 6},
  (_, i) => String(currentYear - 5 + i)
) as unknown as readonly string[];

다만, zod enum에서 동적 값을 사용하려면 추가 작업이 필요하므로 현재 방식도 유효합니다.

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

In `@src/features/course/create-course/model/courseFormSchema.ts` at line 3,
YEAR_OPTIONS is hardcoded through 2026 which requires yearly updates; replace it
with a dynamic generator that builds the array from the current year (e.g.,
compute currentYear = new Date().getFullYear() and create a sliding window of
years of desired length, like last 5 years + current) and assign that result to
YEAR_OPTIONS instead of the literal list; keep it typed as a readonly string
array (or cast appropriately) so existing zod enum usage (if any) can be
adjusted—if zod needs static literal types, keep the current const and add a
TODO comment referencing the dynamic approach and handle zod enum changes in a
follow-up.
src/pages/admin/courses/CourseCreatePage.tsx (1)

16-20: getElementById 대신 ref 사용을 고려해 보세요.

DOM을 직접 조회하는 방식은 폼의 id 값에 의존하게 되어 결합도가 높아집니다. React의 ref를 사용하거나, CourseForm에서 submit 함수를 노출하는 패턴이 더 안전합니다.

♻️ ref를 사용한 개선 예시
// CourseForm에서 ref를 통해 submit 노출
const formRef = useRef<HTMLFormElement>(null);

// onConfirm에서 사용
onConfirm={() => formRef.current?.requestSubmit()}

또는 useImperativeHandle을 활용하여 명시적인 submit 메서드를 노출할 수 있습니다.

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

In `@src/pages/admin/courses/CourseCreatePage.tsx` around lines 16 - 20, The code
in CourseCreatePage uses document.getElementById('course-form') inside the
onConfirm handler which couples to a DOM id; replace this with a React ref or an
exposed submit method from CourseForm: add a formRef (useRef<HTMLFormElement |
null>) in the parent, pass it to CourseForm (or use useImperativeHandle in
CourseForm to expose a submit/requestSubmit method), and change onConfirm to
call formRef.current?.requestSubmit() or the exposed submit method; update
CourseForm props/signature accordingly to accept the ref or imperative handle
(refer to CourseCreatePage, CourseForm, formRef, and onConfirm).
src/widgets/course-form/ui/CourseForm.tsx (1)

87-91: FileUploadonFileChange가 빈 함수입니다.

현재 파일 선택이 무시되고 있습니다. 의도적인 placeholder라면 TODO 주석을 추가하거나, 기능 구현이 필요하다면 파일 처리 로직을 추가해 주세요.

파일 업로드 기능 구현이 필요하시면 도움을 드릴 수 있습니다. 이 작업을 추적할 이슈를 생성할까요?

🤖 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 87 - 91, The
FileUpload's onFileChange is a no-op, so selected files are ignored; implement a
handler (e.g., add a handleFileChange function inside CourseForm and pass it to
the FileUpload onFileChange prop) that receives the File or FileList, validates
type/size, stores the file reference in the form state (or invokes the existing
form setValue/dispatch/upload helper used elsewhere in CourseForm), and ensures
the file is included when submitting; if this is intentionally not implemented,
replace the empty function with a clear TODO comment referencing
handleFileChange and a short explanation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@eslint.config.js`:
- Around line 60-82: The boundaries/entry-point rule is set to 'error' which
will break CI due to many existing deep-import violations; change the rule value
for 'boundaries/entry-point' from 'error' to 'warn' to allow a staged migration,
then update imports to re-export via index.ts (fixing deep imports like
entities/auth/model/useUserStore, entities/assignment/api/assignmentQueries,
features/course/create-course/model/courseFormSchema) and once all deep imports
are replaced and verified, flip the rule back to 'error'.

In `@src/entities/course/api/courseApi.ts`:
- Around line 1-4: createCourse와 updateCourse가 현재 서버 응답을 검증하지 않고 raw data를 반환하고
있으니 getAllCourses/getCourseById/deleteCourse에서 사용하는 것과 동일하게
apiResponseSchema(...) 래퍼를 사용해 응답을 검증하도록 변경하세요: HTTP 응답을 받은 후 response.data를
apiResponseSchema(CreateCourseResponse) 등 적절한 제네릭으로 감싸고 .parse(response.data)를
호출해 런타임에서 스키마를 검증한 결과를 반환하도록 createCourse와 updateCourse 로직을 수정하세요.

In `@src/pages/dashboard/ui/CourseCard.tsx`:
- Around line 30-32: Replace the clickable <div> with a semantic <button> to
make the CourseCard keyboard-accessible: change the element that currently has
the onClick handler (the one calling navigate(userType === 'admin' ?
ROUTES.ADMIN.COURSES.DETAIL(id) : ROUTES.STUDENT.COURSES.DETAIL(id))) to a
<button>, preserve the existing className layout classes and add focus classes
(e.g. focus:outline-none focus:ring-2 focus:ring-primary) so focus styles are
visible, and ensure any default button behavior (type="button") is set so
navigation only happens via the onClick handler.

In `@src/shared/config/routes.ts`:
- Around line 8-13: ROUTES.ADMIN.CHAT and ROUTES.ADMIN.ASSIGNMENTS.DETAIL are
defined but not routed; inside the existing <Route path='admin'> block in your
router (where assignments/manage|create|select are registered) add the missing
routes using the same pattern: add a Route with path='chat' rendering your chat
component (e.g., YourChatComponent) and a Route with path='assignments/:id'
rendering AssignmentDetailPage so that navigate(ROUTES.ADMIN.CHAT) and
navigate(ROUTES.ADMIN.ASSIGNMENTS.DETAIL(id)) resolve correctly.

In `@src/shared/ui/ConfirmModal.tsx`:
- Around line 21-38: The ConfirmModal component is missing ARIA dialog
attributes; update the outer container div (the modal root) to include
role="dialog" and aria-modal="true", add an id (e.g. modal-title-<unique>) on
the title element and set aria-labelledby on the dialog to that id, and if
description exists set aria-describedby on the dialog pointing to the
description element id; ensure you reference the ConfirmModal component's root
div, the title element that renders {title}, and the description element that
renders {description} when adding these attributes.

In `@src/shared/ui/LabeledDropdown.tsx`:
- Line 26: The LabeledDropdown component initializes selectedValue with
useState(defaultValue ?? '') which only runs once and causes the dropdown to
stay stale when defaultValue arrives asynchronously; update the component by
adding a useEffect that watches defaultValue and calls
setSelectedValue(defaultValue ?? '') when it changes (optionally guard to only
update when defaultValue is different to avoid overwriting user edits), or
convert LabeledDropdown to a controlled component that takes value and onChange
props and uses defaultValue only as an initial fallback; refer to the
selectedValue/setSelectedValue state and the defaultValue prop in
LabeledDropdown when implementing this change.

In `@src/widgets/course-form/ui/CourseForm.tsx`:
- Around line 77-83: The LabeledInput usage uses identical text for label and
placeholder; update the props passed to the LabeledInput component (the instance
that spreads {...register('description')} and uses
errorMessage={errors.description?.message}) so the label succinctly names the
field (e.g., "강의 소개") and the placeholder provides input guidance (e.g., "강의에 대한
간단한 설명을 입력하세요"); keep the same props order and bindings (label, placeholder,
className, errorMessage, {...register('description')}) and only change the
string values for label and placeholder.

---

Outside diff comments:
In `@src/shared/ui/LabeledInput.tsx`:
- Around line 8-35: The LabeledInput component currently only shows visual
required/error cues; update it to pass accessibility attributes into the <input>
by forwarding the required prop to the input, setting aria-invalid to true when
errorMessage is present, and adding aria-describedby that points to the error
message element. To implement this, ensure the input has an id (use the incoming
rest.id if provided or React's useId() inside LabeledInput to generate one),
render the error <span> with a matching unique id (e.g., `${id}-error`), and set
aria-describedby to that id only when errorMessage exists; keep existing visual
changes intact and reference the LabeledInput props required and errorMessage
and the input/error span identifiers when making the change.

---

Nitpick comments:
In `@src/features/course/create-course/model/courseFormSchema.ts`:
- Line 3: YEAR_OPTIONS is hardcoded through 2026 which requires yearly updates;
replace it with a dynamic generator that builds the array from the current year
(e.g., compute currentYear = new Date().getFullYear() and create a sliding
window of years of desired length, like last 5 years + current) and assign that
result to YEAR_OPTIONS instead of the literal list; keep it typed as a readonly
string array (or cast appropriately) so existing zod enum usage (if any) can be
adjusted—if zod needs static literal types, keep the current const and add a
TODO comment referencing the dynamic approach and handle zod enum changes in a
follow-up.

In `@src/features/course/delete-course/model/useDeleteCourse.ts`:
- Line 2: The imported API function name deleteCourse is shadowed by the
returned mutate value which is renamed to deleteCourse; to fix, avoid name
collision by renaming one of them (e.g., import { deleteCourse as
deleteCourseApi } from '@/entities/course' or return the mutation as
deleteCourseMutation) and update all references accordingly (look for the import
line referencing deleteCourse and the hook return where mutate is aliased to
deleteCourse).

In `@src/pages/admin/courses/CourseCreatePage.tsx`:
- Around line 16-20: The code in CourseCreatePage uses
document.getElementById('course-form') inside the onConfirm handler which
couples to a DOM id; replace this with a React ref or an exposed submit method
from CourseForm: add a formRef (useRef<HTMLFormElement | null>) in the parent,
pass it to CourseForm (or use useImperativeHandle in CourseForm to expose a
submit/requestSubmit method), and change onConfirm to call
formRef.current?.requestSubmit() or the exposed submit method; update CourseForm
props/signature accordingly to accept the ref or imperative handle (refer to
CourseCreatePage, CourseForm, formRef, and onConfirm).

In `@src/shared/lib/handleApiError.ts`:
- Around line 26-29: The current handleApiError function ignores the original
error message when fallbackMessage is provided; update handleApiError to always
capture and log the original API error message from getApiErrorMessage(error)
while still showing the fallbackMessage to the user (use console.error to emit
both the fallbackMessage and the original message/context from
getApiErrorMessage(error) along with the raw error), keep alert(message)
behavior but ensure logging retains full error context so callers using
handleApiError (and functions like getApiErrorMessage) can debug later.
- Around line 6-21: Add handling for HTTP 429 inside getApiErrorMessage: when
axios.isAxiosError(error) detects a response with status === 429 return a clear
rate-limit message (e.g., '요청이 너무 많습니다. 잠시 후 다시 시도해주세요.') and place this check
alongside the other status checks (400, 401, 403, 404, 409, 5xx) so 429 is
matched before the generic fallbacks; keep the rest of the function behavior
unchanged.

In `@src/widgets/course-form/ui/CourseForm.tsx`:
- Around line 87-91: The FileUpload's onFileChange is a no-op, so selected files
are ignored; implement a handler (e.g., add a handleFileChange function inside
CourseForm and pass it to the FileUpload onFileChange prop) that receives the
File or FileList, validates type/size, stores the file reference in the form
state (or invokes the existing form setValue/dispatch/upload helper used
elsewhere in CourseForm), and ensures the file is included when submitting; if
this is intentionally not implemented, replace the empty function with a clear
TODO comment referencing handleFileChange and a short explanation.
🪄 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: 1f095a1e-705d-4899-854c-d07150295b2c

📥 Commits

Reviewing files that changed from the base of the PR and between f864b1e and 5d87a57.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (32)
  • eslint.config.js
  • package.json
  • src/App.tsx
  • src/entities/assignment/model/types.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/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/pages/admin/courses/CourseCreatePage.tsx
  • src/pages/admin/courses/CourseEditPage.tsx
  • src/pages/common/LandingPage.tsx
  • src/pages/common/UserIdInputPage.tsx
  • src/pages/dashboard/ui/CourseCard.tsx
  • src/pages/dashboard/ui/CourseManagementDropdown.tsx
  • src/pages/manage-assignment/ui/AssignmentManageActionsBar.tsx
  • src/pages/unit-editor/ui/UnitForm.tsx
  • 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/course-form/ui/CourseForm.tsx

Comment thread eslint.config.js
Comment thread src/entities/course/api/courseApi.ts Outdated
Comment thread src/pages/dashboard/ui/CourseCard.tsx Outdated
Comment thread src/shared/config/routes.ts
Comment thread src/shared/ui/ConfirmModal.tsx Outdated
Comment thread src/shared/ui/LabeledDropdown.tsx Outdated
Comment on lines +77 to +83
<LabeledInput
label='강의 소개를 입력하세요'
placeholder='강의 소개를 입력하세요'
className='w-full'
errorMessage={errors.description?.message}
{...register('description')}
/>
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

라벨과 플레이스홀더가 동일합니다.

label은 필드의 역할을 설명하고, placeholder는 입력 예시를 보여주는 것이 UX 관점에서 좋습니다. 예: label="강의 소개", placeholder="강의에 대한 간단한 설명을 입력하세요".

🤖 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 77 - 83, The
LabeledInput usage uses identical text for label and placeholder; update the
props passed to the LabeledInput component (the instance that spreads
{...register('description')} and uses
errorMessage={errors.description?.message}) so the label succinctly names the
field (e.g., "강의 소개") and the placeholder provides input guidance (e.g., "강의에 대한
간단한 설명을 입력하세요"); keep the same props order and bindings (label, placeholder,
className, errorMessage, {...register('description')}) and only change the
string values for label and placeholder.

Copy link
Copy Markdown
Contributor

@suminb99 suminb99 left a comment

Choose a reason for hiding this comment

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

확인했습니다!

이번 PR 리뷰 저에게도 정말 많은 도움이 됐습니다. 정말 수고많으셨어요!! 😆

Comment thread src/entities/course/api/courseApi.ts
Comment thread src/entities/course/model/courseSchema.ts Outdated
Comment thread src/entities/course/model/courseSchema.ts Outdated
title: data.title,
section: data.section,
year: String(data.year) as CourseFormValues['year'],
semester: SEMESTER_DISPLAY_MAP[data.semester],
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.

SEMESTER_DISPLAY_MAP을 정의해서 사용하는 대신 기존의 formatSemester 함수를 활용하는 방식도 고려해볼 수 있을 것 같아요. 어차피 서버에서 내려오는 값은 고정되어 있고, 프론트에서도 드롭다운으로 선택되게 처리해서 고정된 4가지 값 외에는 들어올 수 없으니까요. 사용자를 위한 표시용 변환이라면 formatSemester로 충분할 것 같습니다.

semester: formatSemester(data.semester)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

오 좋습니다!

Comment on lines +13 to +18
export const SEMESTER_DISPLAY_MAP = Object.fromEntries(
Object.entries(SEMESTER_CODE_MAP).map(([display, code]) => [code, display])
) as Record<
(typeof SEMESTER_CODE_MAP)[keyof typeof SEMESTER_CODE_MAP],
keyof typeof SEMESTER_CODE_MAP
>;
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.

아래 CourseEditPage.tsx 리뷰 참고해 주세요!

Comment thread src/entities/assignment/model/schemas.ts
@JiiminHa JiiminHa merged commit 4945da1 into develop Mar 15, 2026
5 checks passed
@JiiminHa JiiminHa deleted the feat/53-course-add branch March 15, 2026 09:14
@JiiminHa JiiminHa mentioned this pull request Mar 24, 2026
This was referenced Mar 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🛠️ refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 강의 개설 페이지 API 연동

2 participants