Skip to content

Release v0.1.0#58

Merged
JiiminHa merged 102 commits intomainfrom
release
Feb 26, 2026
Merged

Release v0.1.0#58
JiiminHa merged 102 commits intomainfrom
release

Conversation

@JiiminHa
Copy link
Contributor

@JiiminHa JiiminHa commented Feb 26, 2026

📦 전체 변경 이력

~ v0.1.0

@JiiminHa JiiminHa requested a review from suminb99 February 26, 2026 07:46
@JiiminHa JiiminHa self-assigned this Feb 26, 2026
@vercel
Copy link

vercel bot commented Feb 26, 2026

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

Project Deployment Actions Updated (UTC)
snow-code-client Ready Ready Preview, Comment Feb 26, 2026 10:36am

@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

📝 Walkthrough

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 단원 편집 페이지 추가 (단원 목록 조회, 생성, 수정, 삭제)
    • 단원 내 과제 목록의 드래그 앤 드롭 정렬 기능
    • 단원 생성/수정 시 기간 유효성 검사 강화 (마감일 >= 공개일)
  • UI/UX 개선

    • UI 컴포넌트 구조 개선 및 스타일링 최적화
    • 레이아웃 반응성 개선
  • Chores

    • 자동 빌드/배포 파이프라인 추가
    • 릴리스 자동화 워크플로우 구성

📋 Walkthrough

FSD 아키텍처 기반 대규모 리팩토링으로, 컴포넌트를 공유/위젯 계층으로 재구성하고, Unit 관리 기능을 신규 추가하며, 타입 시스템을 정리하고 CI/CD 파이프라인을 구축했습니다.

📊 Changes

Cohort / File(s) 요약
GitHub Actions & 배포 설정
.github/workflows/ci-cd.yml, .github/workflows/release-drafter.yml, .github/template/release-drafter.yml
ubuntu-latest에서 Node.js 20과 pnpm v9로 동작하는 CI/CD 워크플로우 추가. 자동 린팅, 빌드, Vercel 통합 포함. Release Drafter는 release/*main 브랜치에서 변경 사항을 자동 분류하여 릴리스 드래프트 생성/발행.
구조 마이그레이션: 타입 정의
src/models/assignment.ts, src/models/course.ts, src/entities/assignment/model/types.ts, src/entities/course/model/types.ts, src/shared/model/common.ts
레거시 src/models/*에서 src/entities/*/model/types.tssrc/shared/model/common.ts로 타입 이동. SemesterCodeSubmissionStatus를 shared로 통합하고, Unit 및 API 응답 타입 추가.
구조 마이그레이션: 컴포넌트 & 유틸리티
src/hooks/useClickOutside.ts, src/utils/course.ts, src/components/common/{ActionButton,IconButton,SelectableItem}.tsx, src/components/admin/assignments/AssignmentCard.tsx, src/shared/ui/PrivateRoute.tsx
레거시 훅, 유틸리티, 컴포넌트 제거. useClickOutside, 코스 포매팅 함수들, ActionButton, IconButton, SelectableItem, AssignmentCard 삭제.
공유 UI 컴포넌트 통합
src/shared/ui/{Button,Header,Layout,LabeledInput,LabeledDropdown}.tsx, src/shared/ui/badge/*, src/shared/ui/button/*, src/shared/ui/list-row/*
컴포넌트들을 @/components/common/*에서 @/shared/ui/*로 마이그레이션. Badge를 스타일 모듈로 분리, Button을 dedicated 디렉토리로 재구성, 신규 ListRow 컴포넌트 추가. 레이아웃 스타일 조정 (overflow, 너비 제약 수정).
페이지 & 라우팅 업데이트
src/App.tsx, src/pages/admin/*/ (4개), src/pages/common/*, src/pages/dashboard/*, src/pages/course-overview/*
useSyncUserRole 훅으로 경로 기반 사용자 타입 동기화. UnitEditorPage 라우트 추가 (/admin/units/:id). 모든 페이지에서 import 경로를 shared UI로 업데이트. 로컬 타입 정의 추가/이동.
Unit 관리 기능: API & Query
src/entities/unit/api/unitApi.ts, src/entities/unit/api/unitQueries.ts, src/entities/unit/api/unitMutations.ts, src/entities/unit/model/types.ts
Unit CRUD API 함수 5개 (getAllUnitsByCourseId, getUnitById, createUnit, updateUnit, deleteUnit) 및 React Query 통합. TUnitFormSchema Zod 스키마로 폼 유효성 검증 (releaseDate ≤ dueDate 제약 포함).
Unit 관리 기능: UI & 페이지
src/pages/unit-editor/UnitEditorPage.tsx, src/pages/unit-editor/ui/{UnitList,UnitForm,UnitAssignmentList}.tsx, src/pages/unit-editor/model/types.ts, src/pages/unit-editor/mock/unitMock.ts
신규 Unit 에디터 페이지 및 컴포넌트. UnitList (선택/인덱스 변경), UnitForm (create/update/delete), UnitAssignmentList (dnd-kit 기반 드래그&드롭 정렬). Mode 상태('idle'/'creating'/'editing')로 흐름 관리.
기능 이전 & 타입 정리
src/features/auth/sync-user-role/model/useSyncUserRole.ts, src/pages/select-assignment/AssignmentSelectPage.tsx, src/pages/course-overview/models/types.ts, src/pages/course-overview/ui/*, src/pages/dashboard/models/types.ts, src/pages/dashboard/ui/*
경로 기반 사용자 타입 동기화 훅 추가. SelectableItem → ListRow 교체. 페이지별 타입 정의를 로컬로 옮기고 (models/types.ts 제거), 컴포넌트 간 import 재정렬.
의존성 & 설정
package.json, index.html, src/index.css
dnd-kit 관련 패키지 (@dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities), 폼 검증 (@hookform/resolvers, zod) 추가. /src/style.css 링크 제거. 글로벌 CSS에 box-sizing, shadow 변수, input-field 유틸, label 기본 스타일 추가.
위젯 컴포넌트 마이그레이션
src/widgets/assignment-form-layout/ui/AssignmentFormLayout.tsx, src/widgets/assignment-page-layout/ui/AssignmentPageLayout.tsx
컴포넌트 import 경로를 shared UI로 업데이트 (Button, SurfaceCard).
공유 라이브러리 & Mock
src/shared/lib/course.ts, src/shared/mocks/assignmentDummyResponse.ts
SemesterCode 임포트 경로 변경 (shared/model/common으로 이동). Mock 타입 경로 업데이트.

🔀 Sequence Diagram

sequenceDiagram
    participant User as 사용자
    participant Editor as UnitEditorPage
    participant API as unitApi
    participant RQ as React Query
    participant UI as UnitForm<br/>UnitList
    participant DB as 서버

    User->>Editor: 코스의 Unit 에디터 접근
    Editor->>RQ: getAllUnitsByCourseId(courseId) 요청
    RQ->>API: 데이터 조회
    API->>DB: GET /units?courseId=...
    DB-->>API: 유닛 목록 응답
    API-->>RQ: 응답 반환
    RQ-->>Editor: 데이터 업데이트

    Editor->>UI: unitList, selectedUnitId 전달
    UI->>UI: UnitList 렌더링 (선택 기능)

    User->>Editor: "유닛 추가" 버튼 클릭
    Editor->>Editor: mode = 'creating'
    Editor->>UI: UnitForm에 빈 폼 제공
    
    User->>UI: 폼 작성 (title, releaseDate, dueDate)
    User->>UI: "저장" 버튼 클릭
    UI->>RQ: createUnit({courseId, unit}) 뮤테이션 실행
    RQ->>API: 새 유닛 생성
    API->>DB: POST /units
    DB-->>API: 생성된 유닛 응답
    API-->>RQ: 응답 반환
    RQ->>RQ: unitList 쿼리 무효화
    RQ->>Editor: 뮤테이션 성공 콜백 (mode='idle', 알림)
    
    Editor->>RQ: getAllUnitsByCourseId 재요청
    RQ-->>Editor: 업데이트된 유닛 목록
    Editor->>UI: 새 유닛 포함 목록 렌더링
Loading

🎯 Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~75분

분석 근거:

  • 📁 파일 범위: 60개 이상의 파일 수정/생성/삭제
  • 🔀 변경 패턴 다양성: 마이그레이션(타입, 컴포넌트), 신규 기능(Unit CRUD), 삭제(레거시 컴포넌트), 리팩토링(import 경로)
  • 🧩 논리 밀도:
    • UnitEditorPage (145줄): 복잡한 상태 관리 및 API 호출 흐름
    • UnitAssignmentList (103줄): dnd-kit 기반 드래그&드롭 로직
    • 전역 타입 시스템 재설계 (여러 파일 연쇄 영향)
  • 🏗️ 구조적 영향: FSD 아키텍처 기반 재구성으로 파일 간 의존성이 광범위
  • ⚠️ 주의 필요 영역:
    • Unit 에디터 상태 흐름 (mode 전환 로직)
    • dnd-kit 드래그 드롭 구현의 정확성
    • 타입 마이그레이션으로 인한 import 누락 가능성
    • API 응답 형식 정합성

🔗 Possibly Related PRs


📌 Suggested Labels

🧩 feature, 🛠️ refactor, ⚙️ setting


👥 Suggested Reviewers

  • suminb99: 아키텍처 설계 및 타입 시스템 리뷰 담당

💡 리뷰 포인트

✨ 잘된 점

  • FSD 원칙 준수: entities, features, shared 계층 분리로 확장성 개선
  • 타입 안정성: Zod 스키마 도입으로 폼 유효성 검증 자동화
  • dnd-kit 활용: Unit 과제 목록의 드래그&드롭은 UX 향상에 효과적

🔍 확인 필요 사항

  1. 타입 마이그레이션 검증

    • src/models/course.ts 완전 제거 시, 모든 import가 @/entities/course/model/types 또는 @/shared/model/common으로 리매핑되었는지 확인
    • 누락된 import로 인한 런타임 에러 가능성 점검
    • 📚 참고: TypeScript 모듈 마이그레이션 체크리스트
  2. UnitEditorPage 상태 흐름

    • mode 전환 로직 (idle → creating → editing) 재검토
    • selectedUnitId null 처리 시나리오 (초기 로드, 삭제 후 등)
    • useQuery의 enabled 플래그로 인한 조건부 페칭이 의도대로 동작하는지 확인
  3. dnd-kit 통합

    // UnitAssignmentList에서:
    // - useSortable의 transform 적용이 드래그 시각화에 제대로 반영되는지
    // - onDragEnd에서 배열 재정렬 후 onChangeIndex 콜백 호출 검증
    // - 드롭 실패 시나리오 처리 (over가 null인 경우)
  4. CI/CD 설정 실행 확인

    • ci-cd.yml의 Vercel 환경 변수 (VITE_* 시크릿) 설정 완료 여부
    • Release Drafter 라벨 (feat, fix, chore 등) 실제 PR에 적용 여부
    • 📚 참고: GitHub Actions 환경 변수
  5. 컴포넌트 삭제 영향도

    • SelectableItem 삭제 후 ListRow로 완전 대체되었는지 확인 (AssignmentSelectPage 이외)
    • ActionButton, IconButton 제거 후 사용 흔적 없는지 grep 확인

🎓 개선 제안

  1. 테스트 커버리지 추가 권장

    • UnitEditorPage의 상태 전환 로직에 대한 단위/통합 테스트
    • dnd-kit 드래그 시뮬레이션 테스트
  2. 마이그레이션 가이드 작성

    • 다른 팀원들이 새로운 파일 구조(entities/*, shared/ui/*)를 이해하도록 문서화
    • 신규 프로젝트 온보딩 시 FSD 패턴 설명
  3. 임포트 경로 검증 스크립트

    • deprecated 경로 (@/components/common/*, @/models/*) 사용 감지 linter 추가 고려
🚥 Pre-merge checks | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description contains only the template with placeholders and no actual content filled in. It lacks information about what changes were made, why they were made, and any special considerations for reviewers. PR 설명을 작성해주세요. 최소한 다음을 포함하세요: (1) 주요 변경 사항 요약 (단원 CRUD 구현, 프로젝트 구조 개선 등), (2) 각 변경의 목적, (3) 리뷰어가 특별히 봐야 할 부분 (예: unitId 타입 처리, DnD 로직). 커밋 메시지의 내용을 활용하여 명확하고 구조화된 설명을 작성하면 좋습니다.
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.
Title check ❓ Inconclusive The title 'Release' is overly vague and generic. While it relates to the PR's purpose (a release), it doesn't convey what specific features, fixes, or changes are included in this release. 제목을 더 구체적으로 작성해주세요. 예: 'Release: Unit CRUD 기능 추가 및 프로젝트 구조 개선' 또는 'v1.0.0 Release: 단원 관리 기능 구현'. 스캔하는 개발자가 주요 변경 사항을 한눈에 이해할 수 있도록 해야 합니다.

✏️ 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 release

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

Caution

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

⚠️ Outside diff range comments (1)
src/pages/course-overview/ui/AssignmentList.tsx (1)

49-54: ⚠️ Potential issue | 🟠 Major

과제 라우팅을 index 대신 id를 사용하도록 수정하세요.

현재 /assignments/${index}로 네비게이션하는데, index는 목록 내 순서(1부터 시작)이므로 불안정합니다. 과제가 재정렬되거나 필터링되면 잘못된 과제로 이동하거나 존재하지 않는 경로로 이동할 수 있습니다.

Assignment 타입에 id: number가 정의되어 있고, 스프레드 연산자를 통해 전달되므로 id를 사용할 수 있습니다. AssignmentItemProps에서 id를 구조분해할당에 추가하고 라우팅 경로를 수정하세요:

수정 방법
const AssignmentItem = ({
+  id,
  title,
  index,
  submittedStatus,
  isOpen,
}: AssignmentItemProps) => {
  // ...
-            <Link to={`/assignments/${index}`} className='min-w-0'>
+            <Link to={`/assignments/${id}`} className='min-w-0'>

또한 /assignments/:id 라우트가 App.tsx에 정의되어 있는지 확인하세요.

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

In `@src/pages/course-overview/ui/AssignmentList.tsx` around lines 49 - 54, The
Link in AssignmentList.tsx currently navigates to `/assignments/${index}` which
is unstable; update the component to use the assignment's id instead by adding
id to the props destructuring (in AssignmentItemProps / the component that
receives {...}) and replace `/assignments/${index}` with `/assignments/${id}`;
also confirm the route `/assignments/:id` is defined in App.tsx so the new URL
resolves correctly.
🧹 Nitpick comments (18)
.github/workflows/release-drafter.yml (1)

26-33: 조건문에 중복이 있습니다.

Line 27의 github.event_name == 'push' 조건은 워크플로우가 이미 push 이벤트에서만 트리거되므로 항상 true입니다. 간결함을 위해 제거할 수 있습니다.

🧹 간결한 조건문으로 수정
       - name: Publish Release
-        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
+        if: github.ref == 'refs/heads/main'
         uses: release-drafter/release-drafter@v6
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release-drafter.yml around lines 26 - 33, The if condition
on the "Publish Release" step currently includes a redundant check of
github.event_name == 'push'; remove that redundant part so the step uses only
the branch check (i.e., keep if: github.ref == 'refs/heads/main') in the Publish
Release step to simplify the condition; locate the step by its name "Publish
Release" and the existing if: line and update it accordingly.
.github/workflows/ci-cd.yml (2)

53-68: 빌드가 두 번 수행됩니다 - 의도된 것인지 확인해 주세요.

현재 워크플로우에서 pnpm buildvercel build가 모두 실행되어 빌드가 두 번 수행됩니다. CI 시간을 절약하고 싶다면, 둘 중 하나만 유지하는 것을 고려해 보세요:

  • 옵션 1: pnpm build만 유지 (빠른 검증용)
  • 옵션 2: vercel build만 유지 (Vercel 환경과 동일한 검증)

Vercel은 자체적으로 배포 시 빌드를 수행하므로, CI에서의 사전 검증 목적이 아니라면 중복일 수 있습니다.

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

In @.github/workflows/ci-cd.yml around lines 53 - 68, The workflow currently
runs two build steps ("Build" which runs `pnpm build` and "Simulate Vercel
Build" which runs `npx vercel build`), causing duplicate builds; decide which
you want and remove the other step to avoid redundant CI time. If you only need
quick verification keep the `pnpm build` step and delete the `npx vercel build`
step (and its env block), or if you need parity with Vercel keep `npx vercel
build` and remove the `pnpm build` step (and its env block); ensure remaining
step preserves any required environment variables (`VITE_KAKAO_REST_API_KEY`,
`VITE_KAKAO_REDIRECT_URI`, `VITE_API_BASE_URL`) and that the "Pull Vercel
Environment" step remains if you rely on those pulled envs.

1-12: 깔끔한 워크플로우 설정입니다! 👍

동시성(concurrency) 설정으로 리소스를 효율적으로 관리하고 있네요. 다만, 보안 강화를 위해 명시적인 permissions 선언을 추가하는 것을 권장드립니다.

🛡️ 보안 강화를 위한 권한 명시 제안
 concurrency:
   group: ${{ github.workflow }}-${{ github.ref }}
   cancel-in-progress: true

+permissions:
+  contents: read
+
 jobs:
   build-and-deploy:

GitHub Actions 보안 모범 사례에 따르면 최소 권한 원칙(principle of least privilege)을 적용하여 필요한 권한만 명시적으로 선언하는 것이 좋습니다. 참고: GitHub Actions Security Hardening

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

In @.github/workflows/ci-cd.yml around lines 1 - 12, Add an explicit top-level
permissions declaration in the workflow (introduce the permissions key near the
top alongside on/concurrency) and set only the minimum GitHub Actions
permissions required for this pipeline (e.g., contents: read, actions: read,
pull-requests: write, id-token: write only if OIDC is used) instead of relying
on defaults; update any steps that currently assume broader access to use the
narrowed permissions and remove any unnecessary write rights. Ensure the new
permissions block is placed above or next to the existing concurrency/on
sections so reviewers can find it easily.
.github/template/release-drafter.yml (1)

5-23: 일부 커밋 태그가 카테고리에 매핑되지 않았습니다.

코딩 가이드라인에 정의된 커밋 태그 중 일부가 Release Drafter 카테고리에 포함되지 않았습니다:

  • modify, add, remove, rename, perf, correct

이 태그를 사용한 PR은 릴리즈 노트에서 분류되지 않을 수 있습니다. 해당 태그들을 적절한 카테고리에 추가하는 것을 권장합니다.

♻️ 누락된 태그 추가 제안
   - title: '🚀 기능 추가'
     labels:
       - 'feat'
+      - 'add'
   - title: '♻️ 리팩토링'
     labels:
       - 'refactor'
+      - 'rename'
+      - 'modify'
   - title: '🐛 버그 수정'
     labels:
       - 'fix'
+      - 'correct'
+  - title: '⚡ 성능 개선'
+    labels:
+      - 'perf'
   - title: '🧰 그 외'
     labels:
       - 'chore'
       - 'ci/cd'
       - 'docs'
       - 'style'
       - 'build'
       - 'common'
       - 'test'
+      - 'remove'

Based on learnings: 프로젝트에서 사용하는 표준 커밋 태그(modify, add, remove, rename, perf, correct 등)를 모두 매핑해 주세요.

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

In @.github/template/release-drafter.yml around lines 5 - 23, The Release
Drafter categories block is missing mappings for standard commit tags (modify,
add, remove, rename, perf, correct); update the categories list so each missing
tag is included under an appropriate category—e.g., add 'add' and 'perf' to the
'🚀 기능 추가' (or create a new '성능' category for 'perf'), include 'modify',
'rename' and 'correct' under '♻️ 리팩토링' or '🧰 그 외' as appropriate, and map
'remove' to '🐛 버그 수정' or '🧰 그 외' depending on semantics—ensure the labels
arrays in the categories block include these tag strings so PRs using those
commit tags are classified by Release Drafter (locate and edit the categories
section and the category titles '🚀 기능 추가', '♻️ 리팩토링', '🐛 버그 수정', '🧰 그 외' to
add the missing labels).
src/index.css (2)

96-98: 공용 .input-field에 고정 폭(w-[295px])은 재사용 시 반응형 제약이 큽니다.

공용 유틸이라면 w-full + max-w 조합이 폼 재사용성과 화면 대응에 더 유리합니다.

수정 제안
 .input-field {
-  `@apply` bg-white w-[295px] px-[14.6px] py-[10.5px] rounded-[9px] border-[0.9px] border-purple-stroke focus:outline-1 focus:outline-primary;
+  `@apply` bg-white w-full max-w-[295px] px-[14.6px] py-[10.5px] rounded-[9px] border-[0.9px] border-purple-stroke focus:outline-1 focus:outline-primary;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.css` around lines 96 - 98, The .input-field utility currently uses
a fixed width (w-[295px]) which limits responsiveness; change it to use w-full
combined with a sensible max width (e.g., max-w-[295px] or an existing max-w
token) so the rule reads: keep bg-white, px-[14.6px], py-[10.5px],
rounded-[9px], border-[0.9px] border-purple-stroke and focus styles but replace
w-[295px] with w-full and add max-w-[295px] to allow full-width behavior on
small screens while capping width on larger ones.

31-31: box-sizing:root만으로는 전체 요소에 전파되지 않습니다.

현재 설정만으로는 하위 요소/의사요소가 content-box로 남을 수 있습니다. 전역 일관성을 위해 universal selector를 같이 두는 편이 안전합니다.

수정 제안
 :root {
   font-family: Pretendard;
   background-color: `#f6f5f8`;
   box-sizing: border-box;
@@
 }
+
+@layer base {
+  *,
+  *::before,
+  *::after {
+    box-sizing: border-box;
+  }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.css` at line 31, The :root rule setting box-sizing: border-box
doesn't propagate to all elements/pseudo-elements; add a rule using the
universal selectors to ensure consistency by adding a selector block for *,
*::before, *::after that sets box-sizing: inherit (so all elements inherit the
:root value). Update the CSS near the existing :root/box-sizing declaration and
target the universal selectors to enforce border-box across elements and
pseudo-elements.
src/pages/admin/assignments/AssignmentCreatePage.tsx (1)

42-44: 리스트 key에 대한 참고 사항입니다.

현재 idx(인덱스)를 key로 사용하고 있는데, 추가만 가능한 현재 구현에서는 문제없습니다. 다만 추후 예제 삭제/재정렬 기능이 추가된다면 고유 ID 기반 key로 변경이 필요합니다.

참고: React 리스트와 Key 공식 문서

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

In `@src/pages/admin/assignments/AssignmentCreatePage.tsx` around lines 42 - 44,
The map over examples in AssignmentCreatePage uses the loop index (key={idx})
which will break identity if items are removed or reordered; change the key to a
stable unique identifier from the example object (e.g., use example.id in the
examples.map callback) or ensure a unique id is assigned when examples are
created (generate and persist a UUID) so replace key={idx} with key={example.id}
and update creation logic that produces examples to include that id.
src/pages/admin/courses/CourseCreatePage.tsx (1)

53-54: 빈 핸들러 함수에 TODO 주석을 추가하면 좋겠습니다.

onCancelonConfirm이 빈 함수로 되어 있는데, API 연동 단계에서 구현 예정이라면 주석으로 명시해두면 추후 작업 시 도움이 됩니다.

💡 제안
-      onCancel={() => {}}
-      onConfirm={() => {}}
+      onCancel={() => {}} // TODO: 취소 로직 구현
+      onConfirm={() => {}} // TODO: 강의 생성 API 연동
🤖 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 53 - 54, The empty
inline handlers onCancel and onConfirm in the CourseCreatePage component should
be annotated with a clear TODO comment (or replaced with small named placeholder
handlers like handleCancel and handleConfirm) indicating they are intentionally
unimplemented until API integration; update the JSX where onCancel={() => {}}
and onConfirm={() => {}} are declared to include a TODO comment referencing the
expected future behavior (e.g., "TODO: implement cancel behavior / call
createCourse API") or call a placeholder function that logs a warning so future
work and reviewers can see these are deliberate stubs.
src/pages/unit-editor/ui/UnitAssignmentList.tsx (1)

83-103: DraggableAssignmentItem의 props 타입을 명확히 정의하면 좋겠습니다.

현재 Assignment 전체 타입을 받지만 실제로는 idtitle만 사용합니다. Pick 유틸리티 타입으로 필요한 속성만 명시하면 의도가 더 명확해집니다.

♻️ 타입 명확화 제안
-const DraggableAssignmentItem = ({id, title}: Assignment) => {
+interface DraggableAssignmentItemProps {
+  id: number;
+  title: string;
+}
+
+const DraggableAssignmentItem = ({id, title}: DraggableAssignmentItemProps) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/unit-editor/ui/UnitAssignmentList.tsx` around lines 83 - 103,
DraggableAssignmentItem currently types its props as the full Assignment type
but only uses id and title; change the component signature to accept a narrowed
props type using Pick<Assignment, 'id' | 'title'> (or define an explicit
interface like { id: string; title: string }) and update the parameter typing in
the DraggableAssignmentItem declaration so the intent is clear and excess fields
are not required.
src/shared/ui/button/Button.tsx (1)

7-7: type prop에 'reset' 옵션 추가를 고려해보세요.

HTML <button>type 속성은 'button' | 'submit' | 'reset'을 지원합니다. 폼 리셋 기능이 필요한 경우를 대비해 추가하면 유연성이 높아집니다.

♻️ reset 타입 추가
-  type?: 'button' | 'submit';
+  type?: 'button' | 'submit' | 'reset';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/button/Button.tsx` at line 7, The Button component's type prop
currently only allows 'button' | 'submit', missing the HTML 'reset' option;
update the ButtonProps/type annotation to include 'reset' (e.g., change type?:
'button' | 'submit' to type?: 'button' | 'submit' | 'reset') and ensure any
usages or default for the Button component (e.g., defaultProps or destructured
default in Button) continue to work with the added 'reset' value.
src/pages/unit-editor/ui/UnitList.tsx (2)

59-69: onAddNewUnit 옵셔널 prop 호출 시 안전 처리 필요

onAddNewUnitundefined일 수 있는데, ButtononClick에 직접 전달하고 있습니다. 현재 Button 컴포넌트가 undefined handler를 안전하게 처리한다면 괜찮지만, 명시적으로 처리하는 것이 안전합니다.

🛡️ 제안된 수정
         <Button
-          onClick={onAddNewUnit}
+          onClick={onAddNewUnit ?? undefined}
           color='primary'
           size='compact'
           content='mixed'>

또는 조건부 렌더링:

+      {onAddNewUnit && (
         <div className='px-12 mt-4.5'>
           <Button
             onClick={onAddNewUnit}
             ...
           </Button>
         </div>
+      )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/unit-editor/ui/UnitList.tsx` around lines 59 - 69, The onAddNewUnit
prop may be undefined but is passed directly to Button.onClick; update the
UnitList component to guard the call by passing a safe handler (e.g., an inline
function that checks if onAddNewUnit exists before invoking) or conditionally
render the Button only when onAddNewUnit is provided; locate the usage in
UnitList.tsx where onAddNewUnit is passed to the Button component and wrap the
call with a null-check around the onAddNewUnit prop (reference symbols:
onAddNewUnit, Button, UnitList).

38-56: 리스트 아이템 접근성 개선 권장

<li> 요소에 onClick만 있고 키보드 접근성(onKeyDown, tabIndex, role)이 없습니다. 스크린 리더 사용자나 키보드 사용자를 위해 접근성 속성 추가를 고려해주세요.

♿ 접근성 개선 제안
           <li
             onClick={() => handleSelectUnit(id)}
+            onKeyDown={(e) => e.key === 'Enter' && handleSelectUnit(id)}
+            tabIndex={0}
+            role="button"
             key={id}
             className={`flex flex-col py-5 px-12 gap-2.5 cursor-pointer ${selectedUnitId === id ? 'bg-background' : ''}`}>

참고: MDN - Keyboard-navigable JavaScript widgets

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

In `@src/pages/unit-editor/ui/UnitList.tsx` around lines 38 - 56, The list items
in UnitList.tsx currently only handle mouse clicks, so update the <li> rendering
(the element using handleSelectUnit, selectedUnitId, and ArrowrightIcon) to be
keyboard-accessible by adding tabIndex={0}, role="button" and an onKeyDown
handler that listens for Enter and Space and calls handleSelectUnit(id); also
ensure the onClick remains, and include an appropriate aria-current or
aria-pressed/aria-selected attribute (e.g., aria-current={selectedUnitId ===
id}) to expose selection state to assistive tech.
src/pages/unit-editor/UnitEditorPage.tsx (2)

46-50: alert() 대신 토스트 알림 사용 고려

현재 alert()를 사용하고 있는데, 사용자 경험 향상을 위해 토스트 알림 라이브러리(예: react-hot-toast, sonner) 도입을 고려해보세요. 이는 나중에 개선해도 됩니다.

Also applies to: 59-63, 74-78

🤖 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 46 - 50, Replace
synchronous alert() calls in UnitEditorPage component with toast notifications
from a chosen library (e.g., react-hot-toast or sonner): import the toast API at
the top and replace alert('새 단원이 성공적으로 생성되었습니다.') with toast.success(...) and
replace alert('단원 생성에 실패했습니다. 다시 시도해주세요.') and other alert calls with
toast.error(...) (or toast()) in the relevant mutation/handler callbacks. Update
the onSuccess and onError handlers referenced in UnitEditorPage (the
create/update/delete mutation callbacks around the current onSuccess/onError
blocks and the other occurrences noted at the 59-63 and 74-78 regions) to use
toast, ensure imports are added, and keep the existing console.error logging for
errors.

22-29: useEffect 의존성 배열에서 잠재적 상태 갱신 루프 주의

mode를 의존성 배열에 포함하면서 내부에서 setMode를 호출하고 있습니다. 현재 mode === 'idle' 조건으로 보호되어 있지만, 향후 로직 변경 시 무한 루프가 발생할 수 있습니다. 의도된 동작이라면 주석으로 명시해두면 좋겠습니다.

🤖 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 22 - 29, The effect
currently depends on both unitList and mode while calling setMode (inside
useEffect), which risks a state-update loop; change the dependency array to only
[unitList] so the effect runs when unitList changes and keep the internal guards
(mode === 'idle') intact, or if you intentionally want mode in deps, add a clear
comment above the useEffect explaining the intentional self-update and why it
won't loop; update references to useEffect, setMode, mode, and setSelectedUnitId
accordingly.
src/entities/unit/api/unitQueries.ts (2)

13-17: getUnitDetails 타입 안전성 개선 고려

enabled: !!unitId로 쿼리 실행을 방지하지만, queryFn 내부에서 unitIdnull일 때의 타입 처리가 명시적이지 않습니다. 현재 구현은 동작하지만, 타입 단언을 추가하면 더 명확해집니다.

♻️ 타입 안전성 개선 제안
   getUnitDetails: (unitId: number | null) =>
     queryOptions({
       queryKey: ['units', 'detail', unitId],
-      queryFn: () => getUnitById(unitId),
+      queryFn: () => getUnitById(unitId!),
       enabled: !!unitId,
     }),
🤖 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 13 - 17, The
getUnitDetails query uses enabled: !!unitId but queryFn: () =>
getUnitById(unitId) still accepts unitId | null; update queryFn to be type-safe
by handling null explicitly — e.g., change queryFn to () => { if (unitId ==
null) return Promise.reject(new Error('unitId is required')); return
getUnitById(unitId); } or use a non-null assertion getUnitById(unitId!) if you
guarantee enabled prevents null — modify getUnitDetails accordingly to reference
getUnitById and unitId.

1-2: 상대 경로 대신 절대 경로 임포트 사용 권장

프로젝트 코딩 가이드라인에 따라 @/... 형식의 절대 경로 임포트를 사용하는 것이 좋습니다.

♻️ 제안된 수정
 import {queryOptions} from '@tanstack/react-query';
-import {getAllUnitsByCourseId, getUnitById} from './unitApi';
+import {getAllUnitsByCourseId, getUnitById} from '@/entities/unit/api/unitApi';

As per coding guidelines: "절대 경로 임포트(@/...) 사용"

🤖 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 1 - 2, The current
relative import of getAllUnitsByCourseId and getUnitById from './unitApi' should
be converted to the project's absolute import style; update the import statement
that brings in getAllUnitsByCourseId and getUnitById to use the '@/…' absolute
path (e.g., import { getAllUnitsByCourseId, getUnitById } from
'@/entities/unit/api/unitApi') so it follows the codebase convention while
leaving the external library import (queryOptions from '@tanstack/react-query')
unchanged.
src/entities/unit/api/unitMutations.ts (1)

1-2: 상대 경로 대신 절대 경로 임포트 사용 권장

일관성을 위해 @/ 절대 경로 임포트를 사용하세요.

♻️ 제안된 수정
-import type {TUnitFormSchema} from '../model/types';
-import {createUnit, deleteUnit, updateUnit} from './unitApi';
+import type {TUnitFormSchema} from '@/entities/unit/model/types';
+import {createUnit, deleteUnit, updateUnit} from '@/entities/unit/api/unitApi';

As per coding guidelines: "절대 경로 임포트(@/...) 사용"

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

In `@src/entities/unit/api/unitMutations.ts` around lines 1 - 2, Replace the
relative imports with project absolute imports: change the import that currently
brings in TUnitFormSchema and the import that brings in createUnit, updateUnit,
deleteUnit from relative paths (e.g. './unitApi' and '../model/types') to use
the '@/...' absolute import form; keep the imported symbols (TUnitFormSchema,
createUnit, updateUnit, deleteUnit) unchanged, only update their module
specifiers to the corresponding '@/...' paths to follow the codebase guideline.
src/pages/unit-editor/ui/UnitForm.tsx (1)

10-10: 주석 처리된 코드 두 줄은 지금 정리해두면 더 깔끔합니다.

사용하지 않는 주석(useState import, assignmentIds state)은 추후 오해 포인트가 되기 쉬워요. 작은 정리지만 유지보수성에 꽤 도움 됩니다.

Also applies to: 24-24

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

In `@src/pages/unit-editor/ui/UnitForm.tsx` at line 10, Remove the dead/commented
imports and state to keep UnitForm clean: delete the commented-out "import
{useState} from 'react';" and the unused "assignmentIds" state declaration (the
const [assignmentIds, ...] line) inside the UnitForm component so no
unused/commented code remains; ensure there are no lingering references to
assignmentIds in functions or JSX before committing.
🤖 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/entities/unit/api/unitApi.ts`:
- Around line 15-19: getUnitById currently allows unitId to be null which
results in requests to "/units/null"; update getUnitById to guard against null
by validating unitId at the start of the function (in function getUnitById) and
immediately reject/throw a descriptive error (or return a failed
Promise<ApiResponse<Unit>>) when unitId is null/undefined, so the HTTP call to
privateAxios.get(`/units/${unitId}`) is never made; alternatively, if nullable
must be preserved by contract, narrow the type inside getUnitById (e.g., early
return when unitId == null) and document the behavior.
- Around line 31-36: The createUnit function is calling the wrong endpoint path;
update the POST in createUnit so it uses the same resource path as the list call
(i.e. POST to `/courses/${courseId}/units`) instead of `/units/${courseId}`;
modify the privateAxios.post call inside createUnit to target
`/courses/${courseId}/units` so create and list use a consistent RESTful
collection path.

In `@src/entities/unit/model/types.ts`:
- Around line 11-14: The current refine compares releaseDate and dueDate as
strings and allows invalid date strings; update the schema for releaseDate and
dueDate (the fields referenced in the .refine call) to validate/parse actual
Dates before comparison — e.g., replace z.string().min(1) with a Zod date
coercion or preprocessing (z.coerce.date() or z.preprocess to parse ISO strings)
or add a strict ISO regex check, then in the refine compare Date objects (or
check parsed validity with isNaN(date.getTime())) and return errors on the
appropriate path (dueDate/releaseDate) when parsing fails or releaseDate >
dueDate so the refine operates on validated dates rather than raw strings.

In `@src/features/auth/sync-user-role/model/useSyncUserRole.ts`:
- Around line 10-15: The current logic in useSyncUserRole that derives userType
from pathname and calls setUserType(userType) must be changed to avoid privilege
elevation: remove pathname-based assignment to userType and instead set userType
only from trusted authentication sources (e.g., the authenticated user object,
token validation, or an auth hook called getCurrentUser/getUserFromToken or
similar) inside the same hook; create a separate UI-only value routeRole (e.g.,
setRouteRole(routeRole)) derived from pathname for rendering decisions; ensure
functions/variables referenced are useSyncUserRole, setUserType, userType,
pathname and add setRouteRole/routeRole so route changes no longer overwrite the
trusted userType.

In `@src/index.css`:
- Around line 100-103: The global rule inside `@layer` base targeting label
(selector "label") forces every label to be flex-col and risks breaking
checkbox/radio layouts; narrow the scope by removing or replacing the global
"label" rule and applying these styles to a dedicated form-specific selector
(e.g., ".form-label" or ".field-label" or a container selector like ".form-row
label") instead, update the CSS to move the flex/gap declarations from the
global label to that class, and ensure corresponding HTML/JSX uses the new class
so only intended form labels receive flex-col and gap-[12.5px].
- Line 97: Enable Tailwind directive parsing in Biome so the `@apply` rule in
src/index.css is recognized: update the Biome config to set
css.parser.tailwindDirectives to true (alongside css.parser.cssModules if used)
so the `@apply` declarations (e.g., the rule containing "@apply bg-white w-[295px]
...") will be parsed and avoid noUnknownAtRule CI errors.

In `@src/pages/course-overview/ui/CourseActionsBar.tsx`:
- Line 13: The Link in CourseActionsBar currently hardcodes `/admin/units/3`;
update CourseActionsBar to use a dynamic id (e.g., unitId or courseId) instead
of the literal "3": retrieve the id from the component props or from React
Router (useParams) and replace the Link target with the template using that
variable (e.g., `/admin/units/${unitId}`), ensuring the prop or hook name you
use matches the component signature (CourseActionsBar) and any parent that
supplies the id.

In `@src/pages/dashboard/ui/CourseManagementDropdown.tsx`:
- Line 21: The "수정하기" option in CourseManagementDropdown currently navigates to
'courses/create' but course edit requires a proper route and an edit-capable
form; add a new route in App.tsx (e.g., 'courses/:id/edit') and change the
CourseManagementDropdown onClick to navigate to that route with the courseId,
then refactor CourseCreatePage into a combined create/edit component (follow the
AssignmentCreatePage pattern) by renaming title to '강의 개설 및 수정', using useParams
to read id to determine edit mode, loading existing course data when id is
present, and handling save/update accordingly so the edit flow works end-to-end.

In `@src/pages/unit-editor/ui/UnitAssignmentList.tsx`:
- Line 30: The component's local state assignments
(useState<Assignment[]>(assignmentList)) doesn't update when the assignmentList
prop changes; add a useEffect in UnitAssignmentList that listens to
assignmentList and calls setAssignments(assignmentList) to sync props to state
(keep existing assignments and setAssignments identifiers), ensuring initial
useState remains but subsequent prop updates overwrite the local assignments
state.
- Around line 42-47: getAssignmentIndex can close over stale assignments when
called inside the setAssignments functional updater; fix by using the updater's
current value instead of the outer helper: inside setAssignments((prev) => { ...
}) compute originalIndex and newIndex from prev (e.g. findIndex by active.id and
over.id) and then return arrayMove(prev, originalIndex, newIndex), referencing
setAssignments, getAssignmentIndex, arrayMove, active.id and over.id to locate
where to change.

In `@src/pages/unit-editor/ui/UnitForm.tsx`:
- Around line 49-56: The onSubmit handler currently falls back to creation when
mode === 'editing' but unit is missing; change the logic in onSubmit so that
when mode === 'editing' and unit is falsy it does NOT call onCreateUnit —
instead return early or invoke an error handler (e.g., set form error or call
onError) to prevent unintended creation; keep the existing behavior of calling
onUpdateUnit(unit.id, data) when unit exists and call onCreateUnit(data) only
when mode !== 'editing'.
- Line 132: The UnitAssignmentList component currently captures only the initial
assignmentList and doesn't resync internal state when switching units; fix this
by adding a React key to the UnitAssignmentList usage so it remounts on unit
change—specifically update the UnitForm render where UnitAssignmentList is used
to include key={unit.id} (using the unit.id unique identifier) so React will
unmount/mount a fresh instance and reset its internal state when the unit prop
changes.

In `@src/pages/unit-editor/ui/UnitList.tsx`:
- Around line 22-28: handleSelectUnit uses unitList?.findIndex but doesn't
handle the -1 result; change it to check if index === -1 and bail out (or
provide a clear fallback) before calling onChangeIndex so you don't pass an
incorrect value. Specifically, in handleSelectUnit, compute const index =
unitList?.findIndex(u => u.id === id); then if (index === -1 || index == null)
return (or set a deliberate fallback), otherwise call onChangeIndex(index + 1);
keep the onUnitClick(id) call as-is.

In `@src/pages/unit-editor/UnitEditorPage.tsx`:
- Around line 14-19: The code converts id from useParams() into courseId with
Number(id) which can yield NaN and trigger unitQueries.getUnitList before id is
validated; update the logic in UnitEditorPage to guard the conversion or defer
the query by either (a) validate id first and only set courseId when id is
defined and a valid number, or (b) keep courseId as Number(id) but pass an
enabled option to useQuery for unitQueries.getUnitList(courseId) that requires
id to be present and a non-NaN number (e.g., enabled: !!id &&
!Number.isNaN(Number(id))); adjust any downstream uses of
courseId/selectedUnitId accordingly to account for the guarded/nullable state.

In `@src/shared/ui/badge/Badge.tsx`:
- Around line 18-20: Submission 배지에서 SubmissionBadgeVariants로부터 status가
optional로 오면서 props.status!로 non-null assertion을 쓰게 되어 런타임 크래시 위험이 있으니,
SubmissionBadgeProps의 타입을 변경하여 status를 필수로 강제하고 코드에서 non-null assertion을 제거하세요;
specifically, update the SubmissionBadgeProps definition (currently "type
SubmissionBadgeProps = { variant: 'submission'; } & SubmissionBadgeVariants;")
to require status (e.g. intersect or override with an explicit required status
type) so status is non-optional, then remove any uses of props.status! when
building SubmissionMeta and use props.status directly (refer to
SubmissionBadgeProps, SubmissionBadgeVariants, and SubmissionMeta to locate the
changes).

In `@src/shared/ui/button/Button.tsx`:
- Around line 15-33: The Button component is not forwarding onMouseEnter and
onMouseLeave from ButtonProps to the rendered <button>, so consumers' hover
handlers never fire; update the Button component (function Button) to include
onMouseEnter and onMouseLeave in the props passed to the <button> (e.g., extract
them from props or include ...props that contain them) so the handlers defined
in ButtonProps are forwarded to the DOM element while preserving existing
attributes like type, onClick, disabled, form, className, and other props.

In `@src/shared/ui/Layout.tsx`:
- Around line 12-15: In Layout.tsx replace the non‑standard Tailwind classes by
using arbitrary-value syntax: change any occurrences of "w-289" to "w-[289px]"
and "min-w-289" to "min-w-[289px]" (these appear on the outer wrapper div with
class 'min-h-screen overflow-x-hidden flex-center' and the inner
container/header divs where 'min-w-289' and 'w-289' are used), ensuring all
instances in the JSX className strings are updated to the bracketed pixel form.

In `@src/shared/ui/list-row/ListRow.tsx`:
- Line 1: Replace the relative import on the first line with the project's
absolute alias import: import { ListRowStyles, type ListRowVariants } from
'@/shared/ui/list-row/list-row-styles'; — update the import in ListRow.tsx to
use the '@/...' alias so the symbols ListRowStyles and ListRowVariants follow
the repository's absolute import convention.

---

Outside diff comments:
In `@src/pages/course-overview/ui/AssignmentList.tsx`:
- Around line 49-54: The Link in AssignmentList.tsx currently navigates to
`/assignments/${index}` which is unstable; update the component to use the
assignment's id instead by adding id to the props destructuring (in
AssignmentItemProps / the component that receives {...}) and replace
`/assignments/${index}` with `/assignments/${id}`; also confirm the route
`/assignments/:id` is defined in App.tsx so the new URL resolves correctly.

---

Nitpick comments:
In @.github/template/release-drafter.yml:
- Around line 5-23: The Release Drafter categories block is missing mappings for
standard commit tags (modify, add, remove, rename, perf, correct); update the
categories list so each missing tag is included under an appropriate
category—e.g., add 'add' and 'perf' to the '🚀 기능 추가' (or create a new '성능'
category for 'perf'), include 'modify', 'rename' and 'correct' under '♻️ 리팩토링'
or '🧰 그 외' as appropriate, and map 'remove' to '🐛 버그 수정' or '🧰 그 외' depending
on semantics—ensure the labels arrays in the categories block include these tag
strings so PRs using those commit tags are classified by Release Drafter (locate
and edit the categories section and the category titles '🚀 기능 추가', '♻️ 리팩토링',
'🐛 버그 수정', '🧰 그 외' to add the missing labels).

In @.github/workflows/ci-cd.yml:
- Around line 53-68: The workflow currently runs two build steps ("Build" which
runs `pnpm build` and "Simulate Vercel Build" which runs `npx vercel build`),
causing duplicate builds; decide which you want and remove the other step to
avoid redundant CI time. If you only need quick verification keep the `pnpm
build` step and delete the `npx vercel build` step (and its env block), or if
you need parity with Vercel keep `npx vercel build` and remove the `pnpm build`
step (and its env block); ensure remaining step preserves any required
environment variables (`VITE_KAKAO_REST_API_KEY`, `VITE_KAKAO_REDIRECT_URI`,
`VITE_API_BASE_URL`) and that the "Pull Vercel Environment" step remains if you
rely on those pulled envs.
- Around line 1-12: Add an explicit top-level permissions declaration in the
workflow (introduce the permissions key near the top alongside on/concurrency)
and set only the minimum GitHub Actions permissions required for this pipeline
(e.g., contents: read, actions: read, pull-requests: write, id-token: write only
if OIDC is used) instead of relying on defaults; update any steps that currently
assume broader access to use the narrowed permissions and remove any unnecessary
write rights. Ensure the new permissions block is placed above or next to the
existing concurrency/on sections so reviewers can find it easily.

In @.github/workflows/release-drafter.yml:
- Around line 26-33: The if condition on the "Publish Release" step currently
includes a redundant check of github.event_name == 'push'; remove that redundant
part so the step uses only the branch check (i.e., keep if: github.ref ==
'refs/heads/main') in the Publish Release step to simplify the condition; locate
the step by its name "Publish Release" and the existing if: line and update it
accordingly.

In `@src/entities/unit/api/unitMutations.ts`:
- Around line 1-2: Replace the relative imports with project absolute imports:
change the import that currently brings in TUnitFormSchema and the import that
brings in createUnit, updateUnit, deleteUnit from relative paths (e.g.
'./unitApi' and '../model/types') to use the '@/...' absolute import form; keep
the imported symbols (TUnitFormSchema, createUnit, updateUnit, deleteUnit)
unchanged, only update their module specifiers to the corresponding '@/...'
paths to follow the codebase guideline.

In `@src/entities/unit/api/unitQueries.ts`:
- Around line 13-17: The getUnitDetails query uses enabled: !!unitId but
queryFn: () => getUnitById(unitId) still accepts unitId | null; update queryFn
to be type-safe by handling null explicitly — e.g., change queryFn to () => { if
(unitId == null) return Promise.reject(new Error('unitId is required')); return
getUnitById(unitId); } or use a non-null assertion getUnitById(unitId!) if you
guarantee enabled prevents null — modify getUnitDetails accordingly to reference
getUnitById and unitId.
- Around line 1-2: The current relative import of getAllUnitsByCourseId and
getUnitById from './unitApi' should be converted to the project's absolute
import style; update the import statement that brings in getAllUnitsByCourseId
and getUnitById to use the '@/…' absolute path (e.g., import {
getAllUnitsByCourseId, getUnitById } from '@/entities/unit/api/unitApi') so it
follows the codebase convention while leaving the external library import
(queryOptions from '@tanstack/react-query') unchanged.

In `@src/index.css`:
- Around line 96-98: The .input-field utility currently uses a fixed width
(w-[295px]) which limits responsiveness; change it to use w-full combined with a
sensible max width (e.g., max-w-[295px] or an existing max-w token) so the rule
reads: keep bg-white, px-[14.6px], py-[10.5px], rounded-[9px], border-[0.9px]
border-purple-stroke and focus styles but replace w-[295px] with w-full and add
max-w-[295px] to allow full-width behavior on small screens while capping width
on larger ones.
- Line 31: The :root rule setting box-sizing: border-box doesn't propagate to
all elements/pseudo-elements; add a rule using the universal selectors to ensure
consistency by adding a selector block for *, *::before, *::after that sets
box-sizing: inherit (so all elements inherit the :root value). Update the CSS
near the existing :root/box-sizing declaration and target the universal
selectors to enforce border-box across elements and pseudo-elements.

In `@src/pages/admin/assignments/AssignmentCreatePage.tsx`:
- Around line 42-44: The map over examples in AssignmentCreatePage uses the loop
index (key={idx}) which will break identity if items are removed or reordered;
change the key to a stable unique identifier from the example object (e.g., use
example.id in the examples.map callback) or ensure a unique id is assigned when
examples are created (generate and persist a UUID) so replace key={idx} with
key={example.id} and update creation logic that produces examples to include
that id.

In `@src/pages/admin/courses/CourseCreatePage.tsx`:
- Around line 53-54: The empty inline handlers onCancel and onConfirm in the
CourseCreatePage component should be annotated with a clear TODO comment (or
replaced with small named placeholder handlers like handleCancel and
handleConfirm) indicating they are intentionally unimplemented until API
integration; update the JSX where onCancel={() => {}} and onConfirm={() => {}}
are declared to include a TODO comment referencing the expected future behavior
(e.g., "TODO: implement cancel behavior / call createCourse API") or call a
placeholder function that logs a warning so future work and reviewers can see
these are deliberate stubs.

In `@src/pages/unit-editor/ui/UnitAssignmentList.tsx`:
- Around line 83-103: DraggableAssignmentItem currently types its props as the
full Assignment type but only uses id and title; change the component signature
to accept a narrowed props type using Pick<Assignment, 'id' | 'title'> (or
define an explicit interface like { id: string; title: string }) and update the
parameter typing in the DraggableAssignmentItem declaration so the intent is
clear and excess fields are not required.

In `@src/pages/unit-editor/ui/UnitForm.tsx`:
- Line 10: Remove the dead/commented imports and state to keep UnitForm clean:
delete the commented-out "import {useState} from 'react';" and the unused
"assignmentIds" state declaration (the const [assignmentIds, ...] line) inside
the UnitForm component so no unused/commented code remains; ensure there are no
lingering references to assignmentIds in functions or JSX before committing.

In `@src/pages/unit-editor/ui/UnitList.tsx`:
- Around line 59-69: The onAddNewUnit prop may be undefined but is passed
directly to Button.onClick; update the UnitList component to guard the call by
passing a safe handler (e.g., an inline function that checks if onAddNewUnit
exists before invoking) or conditionally render the Button only when
onAddNewUnit is provided; locate the usage in UnitList.tsx where onAddNewUnit is
passed to the Button component and wrap the call with a null-check around the
onAddNewUnit prop (reference symbols: onAddNewUnit, Button, UnitList).
- Around line 38-56: The list items in UnitList.tsx currently only handle mouse
clicks, so update the <li> rendering (the element using handleSelectUnit,
selectedUnitId, and ArrowrightIcon) to be keyboard-accessible by adding
tabIndex={0}, role="button" and an onKeyDown handler that listens for Enter and
Space and calls handleSelectUnit(id); also ensure the onClick remains, and
include an appropriate aria-current or aria-pressed/aria-selected attribute
(e.g., aria-current={selectedUnitId === id}) to expose selection state to
assistive tech.

In `@src/pages/unit-editor/UnitEditorPage.tsx`:
- Around line 46-50: Replace synchronous alert() calls in UnitEditorPage
component with toast notifications from a chosen library (e.g., react-hot-toast
or sonner): import the toast API at the top and replace alert('새 단원이 성공적으로
생성되었습니다.') with toast.success(...) and replace alert('단원 생성에 실패했습니다. 다시
시도해주세요.') and other alert calls with toast.error(...) (or toast()) in the
relevant mutation/handler callbacks. Update the onSuccess and onError handlers
referenced in UnitEditorPage (the create/update/delete mutation callbacks around
the current onSuccess/onError blocks and the other occurrences noted at the
59-63 and 74-78 regions) to use toast, ensure imports are added, and keep the
existing console.error logging for errors.
- Around line 22-29: The effect currently depends on both unitList and mode
while calling setMode (inside useEffect), which risks a state-update loop;
change the dependency array to only [unitList] so the effect runs when unitList
changes and keep the internal guards (mode === 'idle') intact, or if you
intentionally want mode in deps, add a clear comment above the useEffect
explaining the intentional self-update and why it won't loop; update references
to useEffect, setMode, mode, and setSelectedUnitId accordingly.

In `@src/shared/ui/button/Button.tsx`:
- Line 7: The Button component's type prop currently only allows 'button' |
'submit', missing the HTML 'reset' option; update the ButtonProps/type
annotation to include 'reset' (e.g., change type?: 'button' | 'submit' to type?:
'button' | 'submit' | 'reset') and ensure any usages or default for the Button
component (e.g., defaultProps or destructured default in Button) continue to
work with the added 'reset' value.

ℹ️ 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 914e280 and da21284.

⛔ Files ignored due to path filters (3)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • src/assets/svg/addIcon.svg is excluded by !**/*.svg
  • src/assets/svg/binIcon.svg is excluded by !**/*.svg
📒 Files selected for processing (83)
  • .github/template/release-drafter.yml
  • .github/workflows/ci-cd.yml
  • .github/workflows/release-drafter.yml
  • index.html
  • package.json
  • src/App.tsx
  • src/components/admin/assignments/AssignmentCard.tsx
  • src/components/common/ActionButton.tsx
  • src/components/common/IconButton.tsx
  • src/components/common/SelectableItem.tsx
  • src/entities/assignment/model/types.ts
  • src/entities/course/model/types.ts
  • src/entities/student/ui/AssignmentProgressCard.tsx
  • src/entities/student/ui/StudentProfile.tsx
  • src/entities/student/ui/StudentTable.tsx
  • src/entities/unit/api/unitApi.ts
  • src/entities/unit/api/unitMutations.ts
  • src/entities/unit/api/unitQueries.ts
  • src/entities/unit/model/types.ts
  • src/features/auth/sync-user-role/model/useSyncUserRole.ts
  • src/features/course/filter-course/ui/CourseSelector.tsx
  • src/hooks/useClickOutside.ts
  • src/index.css
  • src/models/assignment.ts
  • src/models/course.ts
  • src/pages/admin/assignments/AssignmentCreatePage.tsx
  • src/pages/admin/courses/CourseCreatePage.tsx
  • src/pages/admin/student/StudentManagementPage.tsx
  • src/pages/admin/student/StudentProfilePage.tsx
  • src/pages/common/LandingPage.tsx
  • src/pages/common/UserIdInputPage.tsx
  • src/pages/course-overview/CourseOverviewPage.tsx
  • src/pages/course-overview/models/types.ts
  • src/pages/course-overview/ui/AssignmentList.tsx
  • src/pages/course-overview/ui/CourseActionsBar.tsx
  • src/pages/course-overview/ui/CourseContent.tsx
  • src/pages/course-overview/ui/CourseHero.tsx
  • src/pages/course-overview/ui/UnitItem.tsx
  • src/pages/dashboard/Dashboard.tsx
  • src/pages/dashboard/models/types.ts
  • src/pages/dashboard/ui/CourseCard.tsx
  • src/pages/dashboard/ui/CourseList.tsx
  • src/pages/dashboard/ui/CourseManagementDropdown.tsx
  • src/pages/dashboard/ui/ScheduleCard.tsx
  • src/pages/dashboard/ui/ScheduleList.tsx
  • src/pages/select-assignment/AssignmentSelectPage.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/common.ts
  • src/shared/ui/BaseHeader.tsx
  • src/shared/ui/Dropdown.tsx
  • src/shared/ui/EmptyState.tsx
  • src/shared/ui/FileUpload.tsx
  • src/shared/ui/Header.tsx
  • src/shared/ui/Input.tsx
  • src/shared/ui/LabeledDropdown.tsx
  • src/shared/ui/LabeledInput.tsx
  • src/shared/ui/Layout.tsx
  • src/shared/ui/PrivateRoute.tsx
  • src/shared/ui/ProgressIndicators.tsx
  • src/shared/ui/SurfaceCard.tsx
  • src/shared/ui/badge/Badge.tsx
  • src/shared/ui/badge/badge-styles.ts
  • src/shared/ui/button/Button.tsx
  • src/shared/ui/button/button-styles.ts
  • src/shared/ui/checkbox/Checkbox.module.css
  • src/shared/ui/checkbox/Checkbox.tsx
  • src/shared/ui/list-row/ListRow.tsx
  • src/shared/ui/list-row/list-row-styles.ts
  • src/utils/course.ts
  • src/widgets/assignment-form-layout/ui/AssignmentFormLayout.tsx
  • src/widgets/assignment-page-layout/ui/AssignmentPageLayout.tsx
💤 Files with no reviewable changes (12)
  • index.html
  • src/shared/ui/PrivateRoute.tsx
  • src/components/common/IconButton.tsx
  • src/components/common/ActionButton.tsx
  • src/hooks/useClickOutside.ts
  • src/components/common/SelectableItem.tsx
  • src/pages/dashboard/models/types.ts
  • src/utils/course.ts
  • src/models/assignment.ts
  • src/components/admin/assignments/AssignmentCard.tsx
  • src/pages/course-overview/models/types.ts
  • src/models/course.ts

Comment on lines +15 to +19
export const getUnitById = async (
unitId: number | null
): Promise<ApiResponse<Unit>> => {
const response = await privateAxios.get(`/units/${unitId}`);
return response.data;
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

null unitId가 그대로 API 경로로 들어가는 케이스를 차단해주세요.

Line [16]-Line [19]에서 unitIdnull이면 /units/null 요청이 발생합니다. 조회 실패를 런타임까지 미루지 말고 함수 레벨에서 막는 게 안전합니다.
참고: TypeScript null 안전성 가이드 — https://www.typescriptlang.org/docs/handbook/2/narrowing.html

수정 제안
-export const getUnitById = async (
-  unitId: number | null
-): Promise<ApiResponse<Unit>> => {
+export const getUnitById = async (
+  unitId: number
+): Promise<ApiResponse<Unit>> => {
   const response = await privateAxios.get(`/units/${unitId}`);
   return response.data;
 };

또는 호출부 계약상 nullable 유지가 필요하다면:

 export const getUnitById = async (
   unitId: number | null
 ): Promise<ApiResponse<Unit>> => {
+  if (unitId == null) {
+    throw new Error('unitId is required');
+  }
   const response = await privateAxios.get(`/units/${unitId}`);
   return response.data;
 };
📝 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
export const getUnitById = async (
unitId: number | null
): Promise<ApiResponse<Unit>> => {
const response = await privateAxios.get(`/units/${unitId}`);
return response.data;
export const getUnitById = async (
unitId: number
): Promise<ApiResponse<Unit>> => {
const response = await privateAxios.get(`/units/${unitId}`);
return 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 15 - 19, getUnitById currently
allows unitId to be null which results in requests to "/units/null"; update
getUnitById to guard against null by validating unitId at the start of the
function (in function getUnitById) and immediately reject/throw a descriptive
error (or return a failed Promise<ApiResponse<Unit>>) when unitId is
null/undefined, so the HTTP call to privateAxios.get(`/units/${unitId}`) is
never made; alternatively, if nullable must be preserved by contract, narrow the
type inside getUnitById (e.g., early return when unitId == null) and document
the behavior.

Comment on lines +31 to +36
export const createUnit = async (
courseId: number,
unit: TUnitFormSchema
): Promise<ApiResponse<Unit>> => {
const response = await privateAxios.post(`/units/${courseId}`, unit);
return response.data;
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:

#!/bin/bash
# Find all unit API endpoints
echo "=== Unit API endpoints ==="
rg -nP --type=ts "privateAxios\.(get|post|put|delete)\(" src/entities/unit -C2

echo -e "\n=== Course-Unit endpoint patterns ==="
rg -nP --type=ts "/courses/\$\{[^}]+\}/units|/units/\$\{[^}]+\}" src -C2

echo -e "\n=== All unit endpoint patterns ==="
rg -nP --type=ts "/units" src -C1

Repository: 2025-snowCode/snowCode-Client

Length of output: 3317


생성 엔드포인트의 경로를 목록 조회와 일치시켜 주세요.

같은 파일에서 목록 조회는 GET /courses/${courseId}/units (라인 10)인데, 생성은 POST /units/${courseId} (라인 35)로 경로 패턴이 다릅니다. RESTful 컨벤션에 따르면 같은 리소스 컬렉션의 조회와 생성 작업은 동일한 경로를 사용해야 합니다.

제안: createUnit 함수의 엔드포인트를 /courses/${courseId}/units로 변경하여 일관성을 맞춰주세요. 현재 상태로는 백엔드 라우팅이 POST /units/${courseId} 를 지원하지 않으면 404/405 에러가 발생합니다.

🤖 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 31 - 36, The createUnit
function is calling the wrong endpoint path; update the POST in createUnit so it
uses the same resource path as the list call (i.e. POST to
`/courses/${courseId}/units`) instead of `/units/${courseId}`; modify the
privateAxios.post call inside createUnit to target `/courses/${courseId}/units`
so create and list use a consistent RESTful collection path.

Comment on lines +11 to +14
.refine((data) => data.releaseDate <= data.dueDate, {
message: '날짜 범위가 올바르지 않습니다.',
path: ['dueDate'],
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "types.ts" | grep -E "unit|entities" | head -20

Repository: 2025-snowCode/snowCode-Client

Length of output: 262


🏁 Script executed:

cat -n src/entities/unit/model/types.ts

Repository: 2025-snowCode/snowCode-Client

Length of output: 680


🏁 Script executed:

# Search for mock data or test files related to unit
find . -type f \( -name "*.ts" -o -name "*.tsx" \) | xargs grep -l "unitFormSchema\|releaseDate" | head -10

Repository: 2025-snowCode/snowCode-Client

Length of output: 341


🌐 Web query:

Zod date validation best practices YYYY-MM-DD string comparison

💡 Result:

Zod best practices for validating YYYY-MM-DD date strings

  1. Don’t rely on z.string().date() unless you’ve confirmed your Zod version supports it
    In Zod v3, people commonly hit “z.string().date is not a function” and use refine()/regex instead. [1]

  2. Validate format and that it’s a real calendar date (not just “2024-02-31”)
    A solid pattern is:

  • enforce YYYY-MM-DD with a regex
  • then verify it round-trips via DatetoISOString() (UTC) so invalid dates don’t slip through [1][2]
import { z } from "zod";

const YMD = z.string()
  .regex(/^\d{4}-\d{2}-\d{2}$/, "Expected YYYY-MM-DD")
  .refine((s) => {
    const d = new Date(s);                 // parsed as an ISO-like input
    return !Number.isNaN(d.valueOf()) &&
      d.toISOString().startsWith(s);       // "YYYY-MM-DDTHH:mm:ss.sssZ" [3]
  }, "Invalid date");
  1. If you actually want a Date output, prefer coercion/pipelines
  • z.coerce.date() is the standard Zod approach to accept a string and produce a Date. [4]
  • You can combine “string shape” + “coerce to Date” with .pipe() (nice for form/API inputs). [5]
const YMDAsDate = z.string()
  .regex(/^\d{4}-\d{2}-\d{2}$/)
  .pipe(z.coerce.date()); // output: Date [4][5]
  1. About “string comparison” ("YYYY-MM-DD" sorting / min / max checks)
    Lexicographic string comparison only works safely if you first guarantee the string is exactly YYYY-MM-DD with zero-padding (as above). After that, "2024-01-09" < "2024-10-01" behaves as expected.
    Best practice is: validate first, then compare strings (or compare parsed Dates if you’re doing anything timezone-sensitive).

  2. If you really mean timestamps, use z.string().datetime() instead
    For ISO 8601 date-times, Zod has z.string().datetime() (with options like allowing offsets). [2]

Sources: [1] [2] [3] [4] [5]


🏁 Script executed:

cat -n src/pages/unit-editor/mock/unitMock.ts | head -50

Repository: 2025-snowCode/snowCode-Client

Length of output: 1333


🏁 Script executed:

cat -n src/pages/unit-editor/ui/UnitForm.tsx | grep -A 10 -B 10 "releaseDate\|dueDate" | head -40

Repository: 2025-snowCode/snowCode-Client

Length of output: 1333


날짜 입력 형식 검증이 필수입니다.

현재 코드는 releaseDatedueDate를 단순히 z.string().min(1)로만 검증하고 있어, "2025-13-45" 같은 유효하지 않은 날짜 문자열도 통과합니다. 이 상태에서 문자열 비교(data.releaseDate <= data.dueDate)는 예상치 못한 결과를 초래할 수 있습니다.

해결 방법: Zod의 정규식 검증 또는 coerce.date()를 사용해 입력 형식을 먼저 검증하세요.

  .object({
    title: z.string().min(1, '단원 제목을 입력해주세요.'),
-   releaseDate: z.string().min(1, '공개일을 입력해주세요.'),
-   dueDate: z.string().min(1, '마감일을 입력해주세요.'),
+   releaseDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, '날짜는 YYYY-MM-DD 형식이어야 합니다.'),
+   dueDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, '날짜는 YYYY-MM-DD 형식이어야 합니다.'),
    assignmentIds: z.array(z.number()).optional(),
  })

참고: Zod 공식 문서 - refine & superRefine

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

In `@src/entities/unit/model/types.ts` around lines 11 - 14, The current refine
compares releaseDate and dueDate as strings and allows invalid date strings;
update the schema for releaseDate and dueDate (the fields referenced in the
.refine call) to validate/parse actual Dates before comparison — e.g., replace
z.string().min(1) with a Zod date coercion or preprocessing (z.coerce.date() or
z.preprocess to parse ISO strings) or add a strict ISO regex check, then in the
refine compare Date objects (or check parsed validity with
isNaN(date.getTime())) and return errors on the appropriate path
(dueDate/releaseDate) when parsing fails or releaseDate > dueDate so the refine
operates on validated dates rather than raw strings.

Comment on lines +10 to +15
const userType = pathname.startsWith('/admin')
? 'admin'
: pathname.startsWith('/student')
? 'student'
: 'guest';
setUserType(userType);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

경로 기반 userType 덮어쓰기는 권한 상승 취약점입니다.

Line [10]~Line [15]에서 URL prefix만으로 admin/student를 설정하면, 사용자가 경로만 바꿔도 전역 권한 상태가 바뀝니다.
userType은 로그인/토큰 검증 결과(서버 신뢰원)로만 설정하고, 경로 파생 값은 별도 routeRole(UI 상태)로 분리하세요.

참고 문서:

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

In `@src/features/auth/sync-user-role/model/useSyncUserRole.ts` around lines 10 -
15, The current logic in useSyncUserRole that derives userType from pathname and
calls setUserType(userType) must be changed to avoid privilege elevation: remove
pathname-based assignment to userType and instead set userType only from trusted
authentication sources (e.g., the authenticated user object, token validation,
or an auth hook called getCurrentUser/getUserFromToken or similar) inside the
same hook; create a separate UI-only value routeRole (e.g.,
setRouteRole(routeRole)) derived from pathname for rendering decisions; ensure
functions/variables referenced are useSyncUserRole, setUserType, userType,
pathname and add setRouteRole/routeRole so route changes no longer overwrite the
trusted userType.

}

.input-field {
@apply bg-white w-[295px] px-[14.6px] py-[10.5px] rounded-[9px] border-[0.9px] border-purple-stroke focus:outline-1 focus:outline-primary;
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:

#!/bin/bash
set -euo pipefail

echo "=== 1) Finding Biome configuration files ==="
find . -maxdepth 3 -type f \( -name 'biome.json' -o -name 'biome.jsonc' \) 2>/dev/null || echo "No biome config found in root/subdirs"

echo -e "\n=== 2) Checking for Biome/Tailwind in package.json ==="
if [ -f "package.json" ]; then
  echo "package.json found. Checking biome and tailwind versions..."
  cat package.json | jq '.devDependencies, .dependencies' 2>/dev/null | grep -E "biome|tailwind" || echo "No biome/tailwind found"
fi

echo -e "\n=== 3) Checking src/index.css around lines 97 and 102 ==="
if [ -f "src/index.css" ]; then
  echo "File src/index.css exists. Reading context around lines 97 and 102..."
  sed -n '92,107p' src/index.css
fi

echo -e "\n=== 4) Looking for CSS-related linting config ==="
ls -la .biome* 2>/dev/null || echo "No .biome files"
find . -maxdepth 2 -type f \( -name '.prettierrc*' -o -name '.eslintrc*' \) 2>/dev/null || echo "No eslint/prettier config found"

Repository: 2025-snowCode/snowCode-Client

Length of output: 912


🏁 Script executed:

#!/bin/bash
echo "=== Checking if Biome is installed and its configuration ==="
npm list `@biomejs/biome` 2>/dev/null | head -20 || echo "Biome not listed"

echo -e "\n=== Full content of biome.json if exists ==="
if [ -f "biome.json" ]; then
  cat biome.json
fi

Repository: 2025-snowCode/snowCode-Client

Length of output: 2001


🌐 Web query:

Biome CSS linter Tailwind CSS @apply support configuration

💡 Result:

To make Biome’s CSS linter/parser understand Tailwind directives like @apply (and avoid “unknown at-rule” errors), enable Tailwind directive parsing:

// biome.json
{
  "css": {
    "parser": {
      "tailwindDirectives": true
    }
  }
}

Biome documents css.parser.tailwindDirectives as enabling parsing for Tailwind-specific syntax including @apply. [1] Biome introduced this as an opt-in Tailwind (v4) parsing feature in v2.3. [2]

If you still want to lint regular CSS strictly but ignore unknown at-rules only in your Tailwind entry files, disable Biome’s noUnknownAtRule for those files via an override:

{
  "overrides": [
    {
      "includes": ["**/global.css", "**/tailwind.css"],
      "linter": {
        "rules": {
          "nursery": {
            "noUnknownAtRule": "off"
          }
        }
      }
    }
  ]
}

(The rule is lint/nursery/noUnknownAtRule.) [3]

Sources:
[1] Biome configuration reference (css.parser.tailwindDirectives) https://biomejs.dev/reference/configuration/
[2] Biome v2.3 blog (Tailwind support + config snippet) https://biomejs.dev/pt-br/blog/biome-v2-3/
[3] Biome linter rule noUnknownAtRule https://biomejs.dev/linter/rules/no-unknown-at-rule/


🌐 Web query:

Biome 2.4.4 CSS parser Tailwind directives

💡 Result:

Biome’s CSS parser will only understand Tailwind-specific at-rules (e.g. @tailwind, @apply, @utility, @variant, @theme, etc.) if you explicitly enable Tailwind-directive parsing.

Enable Tailwind directives (recommended)

biome.json

{
  "css": {
    "parser": {
      "tailwindDirectives": true
    }
  }
}

This option is documented as enabling parsing of Tailwind-specific syntax like @theme, @utility, and @apply. [1]

Enable from the CLI (useful for quick checks / CI)

biome check --css-parse-tailwind-directives=true file.css

(Also works with biome lint, biome format, biome ci.) [2]

If you still see warnings on @tailwind

There’s a known/open bug where @tailwind can still trigger warnings even when css.parser.tailwindDirectives is enabled. [3]

Sources: [1] [2] [3]


biome.json에 Tailwind 지시어 파싱 활성화

@apply 문법을 인식하도록 CSS 파서를 설정하세요. css.parser.tailwindDirectivestrue로 설정하면 Biome이 Tailwind 전용 문법을 올바르게 파싱합니다.

{
  "css": {
    "parser": {
      "cssModules": true,
      "tailwindDirectives": true
    }
  }
}

설정 없이는 CI 파이프라인에서 noUnknownAtRule 오류로 실패할 수 있습니다. Biome 공식 문서 참고.

🧰 Tools
🪛 Biome (2.4.4)

[error] 97-97: Tailwind-specific syntax is disabled.

(parse)

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

In `@src/index.css` at line 97, Enable Tailwind directive parsing in Biome so the
`@apply` rule in src/index.css is recognized: update the Biome config to set
css.parser.tailwindDirectives to true (alongside css.parser.cssModules if used)
so the `@apply` declarations (e.g., the rule containing "@apply bg-white w-[295px]
...") will be parsed and avoid noUnknownAtRule CI errors.

Comment on lines +14 to +19
const {id} = useParams(); // 강의 ID
const courseId = Number(id);
const [mode, setMode] = useState<Mode>('idle');
const [selectedUnitId, setSelectedUnitId] = useState<number | null>(null);
const [currentIndex, setCurrentIndex] = useState<number>(1);
const {data: unitList} = useQuery(unitQueries.getUnitList(courseId));
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

courseId 변환 시 NaN 방어 로직 필요

idundefined일 때 Number(id)NaN을 반환합니다. 현재 !id 체크는 Line 113에서 이루어지지만, 그 전에 이미 NaN을 사용한 쿼리가 실행됩니다. 쿼리 실행 전에 유효성 검사를 추가하거나, enabled 옵션을 활용하는 것이 좋습니다.

🛡️ 제안된 수정
 const UnitEditorPage = () => {
   const {id} = useParams(); // 강의 ID
   const courseId = Number(id);
+  const isValidCourseId = !isNaN(courseId) && courseId > 0;
   const [mode, setMode] = useState<Mode>('idle');
   const [selectedUnitId, setSelectedUnitId] = useState<number | null>(null);
   const [currentIndex, setCurrentIndex] = useState<number>(1);
-  const {data: unitList} = useQuery(unitQueries.getUnitList(courseId));
+  const {data: unitList} = useQuery({
+    ...unitQueries.getUnitList(courseId),
+    enabled: isValidCourseId,
+  });
   const {data: unit} = useQuery(unitQueries.getUnitDetails(selectedUnitId));
🤖 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 14 - 19, The code
converts id from useParams() into courseId with Number(id) which can yield NaN
and trigger unitQueries.getUnitList before id is validated; update the logic in
UnitEditorPage to guard the conversion or defer the query by either (a) validate
id first and only set courseId when id is defined and a valid number, or (b)
keep courseId as Number(id) but pass an enabled option to useQuery for
unitQueries.getUnitList(courseId) that requires id to be present and a non-NaN
number (e.g., enabled: !!id && !Number.isNaN(Number(id))); adjust any downstream
uses of courseId/selectedUnitId accordingly to account for the guarded/nullable
state.

Comment on lines 18 to +20
type SubmissionBadgeProps = {
variant: 'submission';
} & VariantProps<typeof submissionBadgeStyles>;
} & SubmissionBadgeVariants;
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 src/shared/ui/badge/Badge.tsx

Repository: 2025-snowCode/snowCode-Client

Length of output: 1937


🏁 Script executed:

# SubmissionBadgeVariants 타입 정의 찾기
rg "type SubmissionBadgeVariants" -A 10

Repository: 2025-snowCode/snowCode-Client

Length of output: 957


🏁 Script executed:

# 'submission' 배지 사용처 확인
rg "variant=['\"]submission['\"]" -B 2 -A 2

Repository: 2025-snowCode/snowCode-Client

Length of output: 481


🏁 Script executed:

cat src/shared/ui/badge/badge-styles.ts

Repository: 2025-snowCode/snowCode-Client

Length of output: 1354


🏁 Script executed:

# 더 포괄적으로 submission 배지 모든 사용 찾기
rg "Badge" src -A 5 -B 5 | grep -A 5 -B 5 "submission"

Repository: 2025-snowCode/snowCode-Client

Length of output: 4197


🏁 Script executed:

# 모든 Badge 임포트 및 사용처를 더 넓게 검색
rg "from.*Badge" -A 20 | grep -E "(from.*Badge|variant=|submission)" -A 3 -B 3

Repository: 2025-snowCode/snowCode-Client

Length of output: 2080


🏁 Script executed:

# JSX에서 <Badge 패턴 직접 찾기
rg "<Badge" -A 5

Repository: 2025-snowCode/snowCode-Client

Length of output: 1793


submission 배지에서 타입 안전성 강화가 필요합니다.

현재 SubmissionBadgeVariants에서 status 속성이 optional로 추출되어, SubmissionBadgePropsstatus도 optional입니다. 그럼에도 props.status!로 단언하고 SubmissionMeta 객체에 접근하면 누락된 경우 런타임 크래시가 발생합니다.

타입 정의 단계에서 status를 필수로 강제하면 이런 실수를 컴파일 타임에 방지할 수 있습니다.

수정 예시(diff)
 type SubmissionBadgeProps = {
   variant: 'submission';
-} & SubmissionBadgeVariants;
+  status: NonNullable<SubmissionBadgeVariants['status']>;
+};

그러면 사용 시 non-null assertion 제거:

-const {label, icon} = SubmissionMeta[props.status!];
+const {label, icon} = SubmissionMeta[props.status];

참고: tailwind-variants TypeScript 가이드

📝 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
type SubmissionBadgeProps = {
variant: 'submission';
} & VariantProps<typeof submissionBadgeStyles>;
} & SubmissionBadgeVariants;
type SubmissionBadgeProps = {
variant: 'submission';
status: NonNullable<SubmissionBadgeVariants['status']>;
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/badge/Badge.tsx` around lines 18 - 20, Submission 배지에서
SubmissionBadgeVariants로부터 status가 optional로 오면서 props.status!로 non-null
assertion을 쓰게 되어 런타임 크래시 위험이 있으니, SubmissionBadgeProps의 타입을 변경하여 status를 필수로
강제하고 코드에서 non-null assertion을 제거하세요; specifically, update the
SubmissionBadgeProps definition (currently "type SubmissionBadgeProps = {
variant: 'submission'; } & SubmissionBadgeVariants;") to require status (e.g.
intersect or override with an explicit required status type) so status is
non-optional, then remove any uses of props.status! when building SubmissionMeta
and use props.status directly (refer to SubmissionBadgeProps,
SubmissionBadgeVariants, and SubmissionMeta to locate the changes).

Comment on lines +15 to +33
const Button = ({
children,
onClick,
type = 'button',
disabled = false,
formID,
className,
...props
}: ButtonProps) => {
return (
<button
type={type}
onClick={onClick}
disabled={disabled}
form={formID}
className={twMerge(buttonStyles(props), className)}>
{children}
</button>
);
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

onMouseEnter, onMouseLeave 핸들러가 버튼 요소에 전달되지 않습니다.

ButtonProps 인터페이스에 onMouseEnteronMouseLeave가 정의되어 있지만, 실제 <button> 요소에는 전달되지 않아 소비자가 이 핸들러를 전달해도 동작하지 않습니다.

🔧 누락된 핸들러 전달
 const Button = ({
   children,
   onClick,
   type = 'button',
   disabled = false,
   formID,
   className,
+  onMouseEnter,
+  onMouseLeave,
   ...props
 }: ButtonProps) => {
   return (
     <button
       type={type}
       onClick={onClick}
       disabled={disabled}
       form={formID}
+      onMouseEnter={onMouseEnter}
+      onMouseLeave={onMouseLeave}
       className={twMerge(buttonStyles(props), className)}>
       {children}
     </button>
   );
 };
📝 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 Button = ({
children,
onClick,
type = 'button',
disabled = false,
formID,
className,
...props
}: ButtonProps) => {
return (
<button
type={type}
onClick={onClick}
disabled={disabled}
form={formID}
className={twMerge(buttonStyles(props), className)}>
{children}
</button>
);
const Button = ({
children,
onClick,
type = 'button',
disabled = false,
formID,
className,
onMouseEnter,
onMouseLeave,
...props
}: ButtonProps) => {
return (
<button
type={type}
onClick={onClick}
disabled={disabled}
form={formID}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
className={twMerge(buttonStyles(props), className)}>
{children}
</button>
);
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/button/Button.tsx` around lines 15 - 33, The Button component
is not forwarding onMouseEnter and onMouseLeave from ButtonProps to the rendered
<button>, so consumers' hover handlers never fire; update the Button component
(function Button) to include onMouseEnter and onMouseLeave in the props passed
to the <button> (e.g., extract them from props or include ...props that contain
them) so the handlers defined in ButtonProps are forwarded to the DOM element
while preserving existing attributes like type, onClick, disabled, form,
className, and other props.

Comment on lines +12 to +15
<div className='min-h-screen overflow-x-hidden flex-center'>
<div className='min-w-289 px-4 py-12'>
{showHeader && (
<div className='pb-8'>
<div className='w-289 pb-8 mx-auto'>
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:

# tailwind.config 파일 찾기
fd -e "js" -e "ts" -e "json" "tailwind"

Repository: 2025-snowCode/snowCode-Client

Length of output: 55


🏁 Script executed:

# Layout.tsx 파일 확인
fd -e "tsx" "Layout"

Repository: 2025-snowCode/snowCode-Client

Length of output: 223


🏁 Script executed:

# w-289와 min-w-289 클래스 정의 검색
rg "w-289|min-w-289" -t css -t typescript -t javascript

Repository: 2025-snowCode/snowCode-Client

Length of output: 107


🏁 Script executed:

# Tailwind 테마에서 spacing 값 정의 확인
rg "289|flex-center" --type-add "config:*.{config.js,config.ts,tailwind.config.js,tailwind.config.ts}"

Repository: 2025-snowCode/snowCode-Client

Length of output: 7362


🏁 Script executed:

# Layout.tsx 파일 내용 확인
cat -n src/shared/ui/Layout.tsx

Repository: 2025-snowCode/snowCode-Client

Length of output: 903


🏁 Script executed:

# tailwind.config 파일 검색
fd "tailwind.config" -e ts -e js

Repository: 2025-snowCode/snowCode-Client

Length of output: 55


🏁 Script executed:

# src/index.css에서 커스텀 유틸리티 및 테마 확인
cat -n src/index.css | head -100

Repository: 2025-snowCode/snowCode-Client

Length of output: 3137


🏁 Script executed:

# Search for all files that might contain tailwind configuration
find . -name "*.config.*" -o -name "tailwind*" 2>/dev/null | head -20

Repository: 2025-snowCode/snowCode-Client

Length of output: 132


🏁 Script executed:

# Check if spacing values are defined in src/index.css beyond line 100
cat -n src/index.css | grep -E "(spacing|w-|min-w-|289|extend)" | head -30

Repository: 2025-snowCode/snowCode-Client

Length of output: 471


🏁 Script executed:

# Verify that w-289 and min-w-289 are not defined anywhere
rg "w-289|min-w-289" --type css -B 2 -A 2

Repository: 2025-snowCode/snowCode-Client

Length of output: 55


w-289min-w-289 클래스를 Tailwind 임의 값 문법으로 수정하세요.

w-289min-w-289는 프로젝트의 Tailwind 테마에 정의되어 있지 않습니다. 현재 코드베이스의 다른 폭 값들(예: w-[295px])처럼 명시적인 픽셀 단위 문법을 사용해주세요:

  • min-w-289min-w-[289px]
  • w-289w-[289px]

Tailwind CSS v4에서 명시적 단위를 포함한 임의 값은 의도를 명확하게 하고 유지보수를 용이하게 합니다. Tailwind 임의 값 문서를 참고하세요.

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

In `@src/shared/ui/Layout.tsx` around lines 12 - 15, In Layout.tsx replace the
non‑standard Tailwind classes by using arbitrary-value syntax: change any
occurrences of "w-289" to "w-[289px]" and "min-w-289" to "min-w-[289px]" (these
appear on the outer wrapper div with class 'min-h-screen overflow-x-hidden
flex-center' and the inner container/header divs where 'min-w-289' and 'w-289'
are used), ensuring all instances in the JSX className strings are updated to
the bracketed pixel form.

@@ -0,0 +1,27 @@
import {ListRowStyles, type ListRowVariants} from './list-row-styles';
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

절대 경로 import 규칙으로 맞춰주세요.

Line 1의 상대 경로 import는 현재 저장소 규칙과 불일치합니다. 동일 레이어 리팩터링 시 경로 일관성을 위해 alias import로 통일하는 편이 안전합니다.

🔧 제안 수정안
-import {ListRowStyles, type ListRowVariants} from './list-row-styles';
+import {ListRowStyles, type ListRowVariants} from '@/shared/ui/list-row/list-row-styles';

참고 문서:

As per coding guidelines 절대 경로 임포트(@/...) 사용.

📝 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
import {ListRowStyles, type ListRowVariants} from './list-row-styles';
import {ListRowStyles, type ListRowVariants} from '@/shared/ui/list-row/list-row-styles';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/list-row/ListRow.tsx` at line 1, Replace the relative import on
the first line with the project's absolute alias import: import { ListRowStyles,
type ListRowVariants } from '@/shared/ui/list-row/list-row-styles'; — update the
import in ListRow.tsx to use the '@/...' alias so the symbols ListRowStyles and
ListRowVariants follow the repository's absolute import convention.

@JiiminHa JiiminHa merged commit 882cab1 into main Feb 26, 2026
4 checks passed
@JiiminHa JiiminHa changed the title Release Release v0.1.0 Feb 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants