Skip to content

[FEAT] 강사 대시보드 퍼블리싱 및 새 외주 작성 수정사항 반영#24

Merged
waldls merged 15 commits into
mainfrom
feature/#23-instructor-dashboard
Jun 3, 2026
Merged

[FEAT] 강사 대시보드 퍼블리싱 및 새 외주 작성 수정사항 반영#24
waldls merged 15 commits into
mainfrom
feature/#23-instructor-dashboard

Conversation

@waldls

@waldls waldls commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

📢 PR 유형

어떤 변경 사항이 있었나요?

  • 새로운 기능 추가
  • 버그 수정
  • 코드에 영향을 주지 않는 변경사항(오타 수정, 탭 사이즈 변경, 변수명 변경)
  • 코드 리팩토링
  • 주석 추가 및 수정
  • 문서 수정
  • 빌드 부분 혹은 패키지 매니저 수정
  • 파일 혹은 폴더명 수정
  • 파일 혹은 폴더 삭제

📌 관련 이슈번호


✅ Key Changes

강사 홈 페이지 대시보드 (/instructor)

  • 시안 제출 현황, 매칭 중인 외주, 수정 중인 외주 3개 섹션 UI 구현
  • 각 섹션은 페이지네이션 지원 (이전/다음 버튼 + 페이지 인디케이터)

신규 컴포넌트

  • PageIndicator — 현재 페이지 위치를 점(dot)으로 표시하는 공통 컴포넌트
  • Tag — 디데이 등 레이블 표시용 공통 컴포넌트 (default / black / gray variant)
  • CommissionsHeader — 대시보드 각 섹션 공통 헤더 컴포넌트
  • DraftSubmissionStatusRow / MatchingCommissionsRow / ModifyingCommissionsRow — 각 섹션 행 컴포넌트

기존 컴포넌트 수정

  • Badge — size prop을 variant로 변경, pass / fail / waiting variant 추가

유틸 및 훅

  • usePagination — 페이지네이션 상태 관리 커스텀 훅
  • getDDay — 마감일 기반 D-day 문자열 반환 유틸

새 외주 작성 수정사항

  • [사이즈] 섹션 — 크기 표시 UI 수정
  • [필수 페이지 선택하기] 섹션 — 텍스트 수정
  • 사이즈 추천 카드 — '추천 규격' 폰트 변경 및 단일 선택 가능하도록 수정
  • [색상 선택] 섹션 — 첫 번째 행 디폴트 포커스 (섹션 보더 제외)
  • 결제 모달 — 계좌번호 수정
  • 백엔드 명세 기반으로 일부 필드 정렬

📸 스크린샷 or 실행영상

대시보드

home.mp4

새 외주 작성

write.mp4

🎸 기타 사항 or 추가 코멘트

대시보드 섹션에 데이터 없을 때의 화면은 추후 디자인팀에서 작업 예정입니다!

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 강사 홈 페이지 대시보드 개편: 시안 제출 현황, 매칭 진행 상황, 수정 필요 외주를 한눈에 확인할 수 있는 섹션 추가
    • 배지 및 태그 컴포넌트 확장으로 더 다양한 상태 표시 지원
  • 개선 사항

    • 레이아웃 및 UI 컴포넌트 스타일 조정
    • 결제 정보 업데이트

@waldls waldls requested a review from KOJ50 June 2, 2026 09:31
@waldls waldls self-assigned this Jun 2, 2026
@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

전체 변경 사항

이 PR은 강사용 대시보드 페이지를 새로 구현하고, 외주 작성 폼의 상태 스키마를 업데이트하며, 공통 UI 컴포넌트를 확장합니다. 라우팅을 정리하고 폼 검증 조건을 완화합니다.

변경 사항

강사 대시보드 페이지

계층 / 파일 요약
대시보드 데이터 계약 및 상수
src/data/instructor/home.ts, src/constants/home.ts
DraftSubmissionItem, MatchingItem, ModifyingItem 타입과 각각에 대한 샘플 데이터 배열(draftSubmissionStatusData, matchingStatusData, modifyingStatusData)을 정의합니다. 페이징 상수(ITEMS_PER_PAGE = 3)를 추가합니다.
대시보드 행 컴포넌트
src/components/instructor/home/CommissionsHeader.tsx, src/components/instructor/home/DraftSubmissionStatusRow.tsx, src/components/instructor/home/MatchingCommissionsRow.tsx, src/components/instructor/home/ModifyingCommissionsRow.tsx
섹션 헤더와 각 섹션의 목록 행을 렌더링합니다. D-Day 계산, 아이콘 표시, 조건부 버튼 렌더링을 구현합니다.
페이징 인프라
src/lib/hooks/usePagination.ts, src/components/common/PageIndicator.tsx, src/lib/utils/date.ts
제네릭 usePagination 훅으로 배열을 페이지 단위로 나누고, PageIndicator 컴포넌트와 getDDay 유틸로 네비게이션과 날짜 표시를 지원합니다.
대시보드 섹션 컨테이너
src/containers/instructor/home/DraftSubmissionStatusSection.tsx, src/containers/instructor/home/MatchingCommissionsSection.tsx, src/containers/instructor/home/ModifyingCommissionsSection.tsx
각 섹션이 usePagination으로 데이터를 페이징하고, 행 컴포넌트로 렌더링하며, 이전/다음 버튼과 페이지 표시기로 네비게이션합니다.
대시보드 라우팅 및 통합
src/app/instructor/page.tsx, src/app/instructor/layout.tsx, src/app/instructor/ongoing/page.tsx
강사 홈 페이지에서 3개 섹션을 배치합니다. 레이아웃에 배경 색상을 추가하고 메인 영역의 flex 방향을 지정합니다. /instructor/ongoing 페이지를 제거하고 라우팅을 /instructor로 통합합니다.

공통 UI 컴포넌트 확장

계층 / 파일 요약
Badge 컴포넌트 리팩터링
src/components/common/Badge.tsx, src/constants/write.ts, src/assets/icons/index.ts
BadgeVariant 타입을 도입하여 "3인|4인|5인" 크기에 "pass|fail|waiting" 상태를 추가합니다. 스타일 매핑을 wrapper/text/label 구조로 개선하고, props 이름을 size에서 variant로 변경합니다. MatchingOffIcon과 MatchingOnIcon을 익스포트합니다.
새 Tag 컴포넌트
src/components/common/Tag.tsx
variant("default"|"black"|"gray") 기반으로 스타일을 선택하고 라벨을 렌더링하는 새 Tag 컴포넌트를 추가합니다.

외주 작성 폼 리팩터링

계층 / 파일 요약
상태 구조 마이그레이션
src/store/writeFormStore.ts
WriteFormState에서 materialNote/referenceNote를 materialDescription/referenceDescription으로 변경하고, 초기값과 setter를 업데이트합니다.
API 요청 타입 변경
src/types/write.ts
WriteOrderRequest에서 payment/terms 구조를 plan/dates/term 구조로 재구성합니다. WriteOrderDateRequest를 추가하고, WriteOrderTermRequest의 type을 "SETTLEMENT"으로 단순화합니다.
스토어 빌드 로직 업데이트
src/store/writeFormStore.ts
buildOrderRequest가 새로운 상태 필드를 읽고, designInfo에 materialDescription/referenceDescription을 조건부로 포함하며, plan/dates/term 구조로 요청을 구성합니다.
폼 컨테이너 상태 바인딩
src/containers/instructor/write/AttachFileSection.tsx, src/containers/instructor/write/ReferenceSection.tsx
AttachFileSection과 ReferenceSection이 materialDescription/referenceDescription을 읽고 업데이트하도록 변경합니다.
폼 UI 조정
src/components/instructor/write/PaperSizeCard.tsx, src/containers/instructor/write/ColorChooseSection.tsx, src/components/instructor/write/SizeRecommendedCard.tsx, src/components/instructor/write/PlanChooseCard.tsx, src/containers/instructor/write/NecessaryPageChooseSection.tsx, src/containers/instructor/write/Steps/Step2Content.tsx, src/components/instructor/write/PaymentModal/Step2.tsx
PaperSizeCard가 라벨을 포함하도록 렌더링을 수정합니다. ColorChooseSection의 포커스 상태 로직을 개선합니다. NecessaryPageChooseSection 헤더에서 보조 문구를 제거합니다. SizeRecommendedCard의 조건부 스타일을 단순화합니다. PlanChooseCard와 Badge prop을 동기화합니다. Step2Content의 검증 조건을 selectedPages >= 2에서 >= 1로 변경합니다. PaymentModal의 계좌번호를 업데이트합니다.

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 강사 대시보드 퍼블리싱과 새 외주 작성 수정사항을 명확히 요약하여 주요 변경사항을 잘 전달하고 있습니다.
Linked Issues check ✅ Passed PR이 linked issue #23의 대부분 목표를 충족했습니다: 시안 제출 현황, 매칭 중인 외주, 수정 중인 외주 섹션 구현, 새 외주 작성 FLOW 수정사항 반영 완료.
Out of Scope Changes check ✅ Passed 모든 변경사항이 linked issue #23 범위 내에 있습니다: 대시보드 페이지 구현, 새 외주 작성 수정사항 반영 관련 변경입니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/#23-instructor-dashboard

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.

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/components/instructor/home/MatchingCommissionsRow.tsx (1)

11-11: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Tailwind 클래스 문법을 확인해주세요.

DraftSubmissionStatusRow와 동일하게 border-b-gray-10 문법이 사용되었습니다. 표준 Tailwind에서는 border-b border-gray-10과 같이 방향과 색상을 분리합니다. 커스텀 유틸리티인지 확인이 필요합니다.

🔧 표준 Tailwind 문법으로 수정 제안
-    <div className="border-b-gray-10 hover:bg-gray-5 flex h-15 cursor-pointer items-center border-b py-3 transition-colors duration-150">
+    <div className="border-b border-gray-10 hover:bg-gray-5 flex h-15 cursor-pointer items-center py-3 transition-colors duration-150">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/instructor/home/MatchingCommissionsRow.tsx` at line 11, The
Tailwind class "border-b-gray-10" in the MatchingCommissionsRow component is
likely invalid; update the class string on the root div in
MatchingCommissionsRow.tsx to use standard Tailwind utility separation (add
"border-b" and keep the color as "border-gray-10") unless "border-b-gray-10" is
a defined custom utility; also check and align the same pattern with
DraftSubmissionStatusRow to keep consistency across components.
🧹 Nitpick comments (8)
src/components/common/Tag.tsx (1)

14-22: ⚡ Quick win

조건부 클래스 조합 시 cn() 유틸리티 사용 권장.

템플릿 리터럴로 클래스를 직접 이어붙이고 있습니다. 가독성과 안전성을 위해 cn() 유틸리티 함수 사용을 고려해보세요.

또한 h-6.5 같은 소수점 값이 Tailwind 기본 스케일에 포함되어 있는지 확인이 필요합니다. 작동하지 않는 경우 h-[1.625rem] 또는 h-[26px] 형태로 명시해야 할 수 있습니다.

♻️ cn() 유틸 사용 예시
+import { cn } from "`@/lib/utils`";
+
 const Tag = ({ variant, label }: TagProps) => {
   return (
     <div
-      className={`rounded-4 text-body2-sb flex h-6.5 w-11 items-center justify-center ${tagStyleMap[variant]}`}
+      className={cn(
+        "rounded-4 text-body2-sb flex h-6.5 w-11 items-center justify-center",
+        tagStyleMap[variant]
+      )}
     >
       {label}
     </div>

코딩 가이드라인에서 조건부 클래스 문자열을 직접 이어붙이는 경우 cn() 유틸리티 사용을 제안하도록 명시되어 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/common/Tag.tsx` around lines 14 - 22, The Tag component is
concatenating classes with a template literal; switch it to use the cn() utility
(importing it where Tag is defined) to combine the static classes and the
dynamic class from tagStyleMap[variant] (e.g., cn('rounded-4 text-body2-sb flex
items-center justify-center', tagStyleMap[variant], ...)). Also verify and
replace the non-standard Tailwind class h-6.5 with a bracketed value like
h-[1.625rem] or h-[26px] if your Tailwind scale doesn't include 6.5. Update the
Tag component and ensure tagStyleMap usage and cn import are correct.
src/components/instructor/write/PlanChooseCard.tsx (1)

17-21: 💤 Low value

조건부 클래스에 cn() 유틸리티 사용 권장

코딩 가이드라인에 따라 조건부 클래스 문자열을 직접 이어붙이는 경우 cn() 유틸리티 사용을 권장합니다.

♻️ cn() 사용 제안
+import { cn } from "`@/lib/utils/cn`";
+
 const PlanChooseCard = ({ plan, isSelected = false, onClick }: PlanChooseCardProps) => {
   const { label, size, price, description } = PLAN_MAP[plan];

   return (
     <div
       onClick={onClick}
       tabIndex={0}
-      className={`rounded-14 w-full cursor-pointer border p-6 transition-colors duration-150 outline-none ${
-        isSelected
-          ? "border-main-main bg-purple-5"
-          : "border-gray-40 hover:bg-gray-10 bg-white hover:border-transparent"
-      }`}
+      className={cn(
+        "rounded-14 w-full cursor-pointer border p-6 transition-colors duration-150 outline-none",
+        isSelected
+          ? "border-main-main bg-purple-5"
+          : "border-gray-40 hover:bg-gray-10 bg-white hover:border-transparent"
+      )}
     >

As per coding guidelines: 조건부 클래스 문자열을 직접 이어붙이는 경우 cn() 유틸리티 사용을 제안해주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/instructor/write/PlanChooseCard.tsx` around lines 17 - 21,
Replace the inline conditional template literal used for className in
PlanChooseCard (the expression that switches on isSelected to apply
"border-main-main bg-purple-5" vs "border-gray-40 hover:bg-gray-10 bg-white
hover:border-transparent") with the cn() utility; import cn if it's not already
imported and build the classes by passing the static classes ("rounded-14 w-full
cursor-pointer border p-6 transition-colors duration-150 outline-none") plus an
object or conditional arguments that apply the selected classes when isSelected
is true and the alternate classes when false so behavior remains identical.
src/components/instructor/write/SizeRecommendedCard.tsx (1)

24-24: 💤 Low value

조건부 클래스에 cn() 유틸리티 사용 권장

코딩 가이드라인에 따라 조건부 클래스 문자열을 직접 이어붙이는 경우 cn() 유틸리티 사용을 권장합니다.

♻️ cn() 사용 제안
+import { cn } from "`@/lib/utils/cn`";
+
 const SizeRecommendedCard = ({
   ...
 }: SizeRecommendedCardProps) => {
   return (
     <div
       onClick={onClick}
       tabIndex={0}
-      className={`rounded-8 w-49.25 cursor-pointer border px-4 py-3 transition-colors duration-150 ${isSelected ? "border-main-main bg-purple-10" : "border-gray-20 hover:bg-gray-10 bg-white"}`}
+      className={cn(
+        "rounded-8 w-49.25 cursor-pointer border px-4 py-3 transition-colors duration-150",
+        isSelected ? "border-main-main bg-purple-10" : "border-gray-20 hover:bg-gray-10 bg-white"
+      )}
     >

As per coding guidelines: 조건부 클래스 문자열을 직접 이어붙이는 경우 cn() 유틸리티 사용을 제안해주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/instructor/write/SizeRecommendedCard.tsx` at line 24, The JSX
className string in SizeRecommendedCard (the template using isSelected:
className={`rounded-8 w-49.25 cursor-pointer border px-4 py-3 transition-colors
duration-150 ${isSelected ? "border-main-main bg-purple-10" : "border-gray-20
hover:bg-gray-10 bg-white"}`}) should be refactored to use the cn() utility;
replace the inline conditional string concatenation with a cn(...) call that
always includes the static classes and conditionally includes "border-main-main
bg-purple-10" when isSelected is true and "border-gray-20 hover:bg-gray-10
bg-white" when false, referencing the SizeRecommendedCard component and the
isSelected prop to locate the code.
src/store/writeFormStore.ts (1)

133-140: 💤 Low value

필수 값(null) → 빈 문자열 폴백은 UI 경로에서 전송 위험이 낮음

src/store/writeFormStore.tsbuildOrderRequestselectedPlan/firstDate/finalDatenull이면 각각 ""로 매핑합니다. 다만 src/containers/instructor/write/Steps/Step3Content.tsx에서 결제하기 버튼은 selectedPlan !== null && firstDate !== null && finalDate !== null일 때만 활성화되므로, 해당 경로에서는 빈 문자열이 실제 요청 payload로 이어질 가능성이 낮습니다. 그래도 buildOrderRequest가 상태 변경마다 sessionStorage에 동기화되기 때문에, 이후 이 값을 소비/전송에 사용한다면 "" 대신 필드 미포함/런타임 가드로 스키마 안정성을 확보하는 개선을 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/store/writeFormStore.ts` around lines 133 - 140, buildOrderRequest
currently maps nullable fields selectedPlan/firstDate/finalDate to empty strings
which can produce invalid payloads when persisted to sessionStorage; change
buildOrderRequest so that plan and dates entries are omitted (or set to
undefined) when selectedPlan, firstDate or finalDate are null instead of mapping
to "", e.g. only include plan when selectedPlan != null (using
PLAN_API_MAP[selectedPlan]), only push date objects when firstDate/finalDate !=
null (using toApiDate), and preserve term as-is; update any sessionStorage sync
logic that assumes those keys always exist to handle missing fields safely.
src/data/instructor/home.ts (1)

17-17: ⚡ Quick win

내보내기 상수는 SCREAMING_SNAKE_CASE를 사용해야 합니다.

draftSubmissionStatusData, matchingStatusData, modifyingStatusData는 내보내기된 상수이므로 DRAFT_SUBMISSION_STATUS_DATA, MATCHING_STATUS_DATA, MODIFYING_STATUS_DATA로 명명되어야 합니다.

♻️ 제안하는 수정
-export const draftSubmissionStatusData: DraftSubmissionItem[] = [
+export const DRAFT_SUBMISSION_STATUS_DATA: DraftSubmissionItem[] = [
-export const matchingStatusData: MatchingItem[] = [
+export const MATCHING_STATUS_DATA: MatchingItem[] = [
-export const modifyingStatusData: ModifyingItem[] = [
+export const MODIFYING_STATUS_DATA: ModifyingItem[] = [

사용하는 곳에서도 임포트명을 함께 변경해야 합니다.

Based on learnings: 프로젝트에서 내보내기된 상수는 SCREAMING_SNAKE_CASE (예: BASIC_INFO_FIELDS, PAGE_OPTIONS)를 사용하며, camelCase 명명 가이드라인은 함수와 내보내지 않는 일반 변수에 적용됩니다.

Also applies to: 66-66, 108-108

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/data/instructor/home.ts` at line 17, Exported constant names
draftSubmissionStatusData, matchingStatusData, and modifyingStatusData use
camelCase but must follow SCREAMING_SNAKE_CASE; rename them to
DRAFT_SUBMISSION_STATUS_DATA, MATCHING_STATUS_DATA, and MODIFYING_STATUS_DATA in
their declarations (e.g., the exported arrays in the file) and update all import
sites to use the new names so references to these symbols
(draftSubmissionStatusData, matchingStatusData, modifyingStatusData) are
replaced with DRAFT_SUBMISSION_STATUS_DATA, MATCHING_STATUS_DATA, and
MODIFYING_STATUS_DATA respectively.
src/components/instructor/home/DraftSubmissionStatusRow.tsx (1)

24-29: ⚡ Quick win

제출 개수 유효성 검증 추가를 고려해보세요.

total - submitted가 음수가 될 경우 Array.from에서 문제가 발생할 수 있습니다. API 데이터의 무결성이 보장되지 않는다면 방어 로직을 추가하는 것이 좋습니다.

🛡️ 방어 로직 추가 제안
 const DraftSubmissionStatusRow = ({ item }: { item: DraftSubmissionItem }) => {
   const { title, category, draftSubmission, firstDraftDeadline } = item;
   const { submitted, total } = draftSubmission;
+  const validSubmitted = Math.min(Math.max(0, submitted), total);
+  const remaining = total - validSubmitted;
   const categoryLabel = CATEGORY_DISPLAY_MAP[category] ?? category;

   return (
     ...
           <div className="flex flex-row">
-            {Array.from({ length: submitted }).map((_, i) => (
+            {Array.from({ length: validSubmitted }).map((_, i) => (
               <MatchingOnIcon key={i} className="size-8" />
             ))}
-            {Array.from({ length: total - submitted }).map((_, i) => (
+            {Array.from({ length: remaining }).map((_, i) => (
               <MatchingOffIcon key={i} className="size-8" />
             ))}
           </div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/instructor/home/DraftSubmissionStatusRow.tsx` around lines 24
- 29, The rendering can break if total - submitted is negative; update
DraftSubmissionStatusRow to sanitize counts by computing a safeSubmitted =
Math.max(0, Math.min(submitted, total)) and safeRemaining = Math.max(0, total -
safeSubmitted) (or equivalent clamping) before mapping to icons (refer to
MatchingOnIcon and MatchingOffIcon render blocks), then use those safe values in
the Array.from length arguments so neither map gets a negative length.
src/components/common/PageIndicator.tsx (1)

12-12: 💤 Low value

조건부 클래스 결합 시 cn() 유틸리티 사용 권장

템플릿 리터럴로 조건부 클래스를 직접 이어붙이고 있습니다. 일관성과 가독성을 위해 cn() 유틸리티를 사용하는 것을 권장합니다.

♻️ 제안 수정안
+import { cn } from "`@/lib/utils/cn`";
+
 interface PageIndicatorProps {
   total: number;
   current: number;
 }

 const PageIndicator = ({ total, current }: PageIndicatorProps) => {
   return (
     <div className="flex items-center gap-2">
       {Array.from({ length: total }, (_, i) => (
         <div
           key={i}
-          className={`size-2 rounded-full transition-colors duration-300 ${i === current ? "bg-gray-700" : "bg-gray-300"}`}
+          className={cn(
+            "size-2 rounded-full transition-colors duration-300",
+            i === current ? "bg-gray-700" : "bg-gray-300"
+          )}
         />
       ))}
     </div>
   );
 };

코딩 가이드라인에 따르면, 조건부 클래스 문자열을 직접 이어붙이는 경우 cn() 유틸리티 사용을 제안해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/common/PageIndicator.tsx` at line 12, Replace the
template-literal conditional class construction in the PageIndicator component
(the className currently using `${i === current ? "bg-gray-700" :
"bg-gray-300"}`) with the project's cn() utility: import cn if missing and call
cn("size-2 rounded-full transition-colors duration-300", { "bg-gray-700": i ===
current, "bg-gray-300": i !== current }) so the conditional classes are composed
via cn() rather than string interpolation; update the className prop on the
element that uses variables i and current accordingly.
src/containers/instructor/home/ModifyingCommissionsSection.tsx (1)

34-38: ⚡ Quick win

페이지네이션 버튼에 disabled 상태 추가 권장

현재 페이지가 첫 페이지 또는 마지막 페이지일 때도 이전/다음 버튼이 활성 상태로 표시됩니다. 훅 내부에서 경계 처리는 되지만, 시각적 피드백이 없어 사용자가 버튼을 클릭해도 변화가 없으면 혼란을 겪을 수 있습니다.

♻️ 제안 수정안
 <div className="flex flex-row justify-between">
-  <PrevButton className="size-12 cursor-pointer" onClick={handlePrev} />
+  <PrevButton 
+    className={cn("size-12", current > 0 ? "cursor-pointer" : "cursor-not-allowed opacity-40")} 
+    onClick={current > 0 ? handlePrev : undefined} 
+  />
   <PageIndicator total={totalPages} current={current} />
-  <NextButton className="size-12 cursor-pointer" onClick={handleNext} />
+  <NextButton 
+    className={cn("size-12", current < totalPages - 1 ? "cursor-pointer" : "cursor-not-allowed opacity-40")} 
+    onClick={current < totalPages - 1 ? handleNext : undefined} 
+  />
 </div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/containers/instructor/home/ModifyingCommissionsSection.tsx` around lines
34 - 38, The Prev/Next buttons are always active visually; update the PrevButton
and NextButton JSX to pass a disabled prop when at the list boundaries (use
current === 1 for PrevButton and current === totalPages for NextButton) and
adjust their className or aria-disabled so they visually appear disabled (e.g.,
reduced opacity and pointer-events disabled) and prevent click actions when
disabled so handlePrev/handleNext won't be invoked; reference PrevButton,
NextButton, current, totalPages, handlePrev and handleNext when making the
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/instructor/home/ModifyingCommissionsRow.tsx`:
- Line 11: The row in ModifyingCommissionsRow.tsx uses visual affordances
(cursor-pointer, hover:bg-gray-5) but lacks interaction—fix by either wiring a
real handler or removing affordances: add an onClick prop to the component and
attach it to the div (e.g., implement an onRowClick handler in
ModifyingCommissionsRow and pass it down), ensure keyboard accessibility by
adding role="button", tabIndex={0}, and handling onKeyDown for Enter/Space, and
include an aria-label; alternatively, wrap the content in a Link if navigation
is intended or remove cursor-pointer/hover classes if no interaction should be
available.

In `@src/constants/write.ts`:
- Around line 146-151: The SIZE_DISPLAY_MAP entry for the key "국배판" is currently
set to the API code "KUKBAE", causing the UI (InfoRow in
src/components/instructor/write/PaymentModal/Step1.tsx which renders
SIZE_DISPLAY_MAP[selectedSize]) to show the code instead of a user-friendly
label; update the value for the "국배판" key in the SIZE_DISPLAY_MAP constant (in
src/constants/write.ts) to the appropriate display string (e.g., "국배판") so
selectedSize renders a user-facing label rather than the API code.

In `@src/lib/hooks/usePagination.ts`:
- Around line 8-9: When computing next page, guard against totalPages being 0 to
avoid negative pages: compute a non-negative lastPage (e.g., const lastPage =
Math.max(0, totalPages - 1)) and use it in handleNext (setCurrent(prev =>
Math.min(lastPage, prev + 1))). Keep handlePrev as setCurrent(prev =>
Math.max(0, prev - 1)) so current never goes below 0; update references to
totalPages, lastPage, handleNext, handlePrev, and setCurrent accordingly.

---

Duplicate comments:
In `@src/components/instructor/home/MatchingCommissionsRow.tsx`:
- Line 11: The Tailwind class "border-b-gray-10" in the MatchingCommissionsRow
component is likely invalid; update the class string on the root div in
MatchingCommissionsRow.tsx to use standard Tailwind utility separation (add
"border-b" and keep the color as "border-gray-10") unless "border-b-gray-10" is
a defined custom utility; also check and align the same pattern with
DraftSubmissionStatusRow to keep consistency across components.

---

Nitpick comments:
In `@src/components/common/PageIndicator.tsx`:
- Line 12: Replace the template-literal conditional class construction in the
PageIndicator component (the className currently using `${i === current ?
"bg-gray-700" : "bg-gray-300"}`) with the project's cn() utility: import cn if
missing and call cn("size-2 rounded-full transition-colors duration-300", {
"bg-gray-700": i === current, "bg-gray-300": i !== current }) so the conditional
classes are composed via cn() rather than string interpolation; update the
className prop on the element that uses variables i and current accordingly.

In `@src/components/common/Tag.tsx`:
- Around line 14-22: The Tag component is concatenating classes with a template
literal; switch it to use the cn() utility (importing it where Tag is defined)
to combine the static classes and the dynamic class from tagStyleMap[variant]
(e.g., cn('rounded-4 text-body2-sb flex items-center justify-center',
tagStyleMap[variant], ...)). Also verify and replace the non-standard Tailwind
class h-6.5 with a bracketed value like h-[1.625rem] or h-[26px] if your
Tailwind scale doesn't include 6.5. Update the Tag component and ensure
tagStyleMap usage and cn import are correct.

In `@src/components/instructor/home/DraftSubmissionStatusRow.tsx`:
- Around line 24-29: The rendering can break if total - submitted is negative;
update DraftSubmissionStatusRow to sanitize counts by computing a safeSubmitted
= Math.max(0, Math.min(submitted, total)) and safeRemaining = Math.max(0, total
- safeSubmitted) (or equivalent clamping) before mapping to icons (refer to
MatchingOnIcon and MatchingOffIcon render blocks), then use those safe values in
the Array.from length arguments so neither map gets a negative length.

In `@src/components/instructor/write/PlanChooseCard.tsx`:
- Around line 17-21: Replace the inline conditional template literal used for
className in PlanChooseCard (the expression that switches on isSelected to apply
"border-main-main bg-purple-5" vs "border-gray-40 hover:bg-gray-10 bg-white
hover:border-transparent") with the cn() utility; import cn if it's not already
imported and build the classes by passing the static classes ("rounded-14 w-full
cursor-pointer border p-6 transition-colors duration-150 outline-none") plus an
object or conditional arguments that apply the selected classes when isSelected
is true and the alternate classes when false so behavior remains identical.

In `@src/components/instructor/write/SizeRecommendedCard.tsx`:
- Line 24: The JSX className string in SizeRecommendedCard (the template using
isSelected: className={`rounded-8 w-49.25 cursor-pointer border px-4 py-3
transition-colors duration-150 ${isSelected ? "border-main-main bg-purple-10" :
"border-gray-20 hover:bg-gray-10 bg-white"}`}) should be refactored to use the
cn() utility; replace the inline conditional string concatenation with a cn(...)
call that always includes the static classes and conditionally includes
"border-main-main bg-purple-10" when isSelected is true and "border-gray-20
hover:bg-gray-10 bg-white" when false, referencing the SizeRecommendedCard
component and the isSelected prop to locate the code.

In `@src/containers/instructor/home/ModifyingCommissionsSection.tsx`:
- Around line 34-38: The Prev/Next buttons are always active visually; update
the PrevButton and NextButton JSX to pass a disabled prop when at the list
boundaries (use current === 1 for PrevButton and current === totalPages for
NextButton) and adjust their className or aria-disabled so they visually appear
disabled (e.g., reduced opacity and pointer-events disabled) and prevent click
actions when disabled so handlePrev/handleNext won't be invoked; reference
PrevButton, NextButton, current, totalPages, handlePrev and handleNext when
making the change.

In `@src/data/instructor/home.ts`:
- Line 17: Exported constant names draftSubmissionStatusData,
matchingStatusData, and modifyingStatusData use camelCase but must follow
SCREAMING_SNAKE_CASE; rename them to DRAFT_SUBMISSION_STATUS_DATA,
MATCHING_STATUS_DATA, and MODIFYING_STATUS_DATA in their declarations (e.g., the
exported arrays in the file) and update all import sites to use the new names so
references to these symbols (draftSubmissionStatusData, matchingStatusData,
modifyingStatusData) are replaced with DRAFT_SUBMISSION_STATUS_DATA,
MATCHING_STATUS_DATA, and MODIFYING_STATUS_DATA respectively.

In `@src/store/writeFormStore.ts`:
- Around line 133-140: buildOrderRequest currently maps nullable fields
selectedPlan/firstDate/finalDate to empty strings which can produce invalid
payloads when persisted to sessionStorage; change buildOrderRequest so that plan
and dates entries are omitted (or set to undefined) when selectedPlan, firstDate
or finalDate are null instead of mapping to "", e.g. only include plan when
selectedPlan != null (using PLAN_API_MAP[selectedPlan]), only push date objects
when firstDate/finalDate != null (using toApiDate), and preserve term as-is;
update any sessionStorage sync logic that assumes those keys always exist to
handle missing fields safely.
🪄 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 Plus

Run ID: 193786d8-57c0-4ccb-97bb-9d915e0ba2b6

📥 Commits

Reviewing files that changed from the base of the PR and between 578f8d7 and dbd1478.

⛔ Files ignored due to path filters (2)
  • src/assets/icons/icon_matching_off.svg is excluded by !**/*.svg
  • src/assets/icons/icon_matching_on.svg is excluded by !**/*.svg
📒 Files selected for processing (31)
  • src/app/globals.css
  • src/app/instructor/layout.tsx
  • src/app/instructor/ongoing/page.tsx
  • src/app/instructor/page.tsx
  • src/assets/icons/index.ts
  • src/components/common/Badge.tsx
  • src/components/common/PageIndicator.tsx
  • src/components/common/Tag.tsx
  • src/components/instructor/home/CommissionsHeader.tsx
  • src/components/instructor/home/DraftSubmissionStatusRow.tsx
  • src/components/instructor/home/MatchingCommissionsRow.tsx
  • src/components/instructor/home/ModifyingCommissionsRow.tsx
  • src/components/instructor/write/PaperSizeCard.tsx
  • src/components/instructor/write/PaymentModal/Step2.tsx
  • src/components/instructor/write/PlanChooseCard.tsx
  • src/components/instructor/write/SizeRecommendedCard.tsx
  • src/constants/home.ts
  • src/constants/write.ts
  • src/containers/instructor/home/DraftSubmissionStatusSection.tsx
  • src/containers/instructor/home/MatchingCommissionsSection.tsx
  • src/containers/instructor/home/ModifyingCommissionsSection.tsx
  • src/containers/instructor/write/AttachFileSection.tsx
  • src/containers/instructor/write/ColorChooseSection.tsx
  • src/containers/instructor/write/NecessaryPageChooseSection.tsx
  • src/containers/instructor/write/ReferenceSection.tsx
  • src/containers/instructor/write/Steps/Step2Content.tsx
  • src/data/instructor/home.ts
  • src/lib/hooks/usePagination.ts
  • src/lib/utils/date.ts
  • src/store/writeFormStore.ts
  • src/types/write.ts
💤 Files with no reviewable changes (1)
  • src/app/instructor/ongoing/page.tsx

const { title, finalDeadline, isSubmitted, hasUpdate } = item;

return (
<div className="border-b-gray-10 hover:bg-gray-5 flex h-15 cursor-pointer items-center border-b py-3 transition-colors duration-150">

@coderabbitai coderabbitai Bot Jun 2, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

클릭 가능한 시각적 힌트가 있지만 실제 상호작용 핸들러가 없습니다

cursor-pointerhover:bg-gray-5 스타일이 적용되어 클릭 가능한 요소처럼 보이지만, 실제 onClick 핸들러나 링크가 없습니다. 이는 다음 문제를 야기합니다:

  • 사용자가 클릭해도 아무 일도 일어나지 않아 혼란 발생
  • 키보드 내비게이션 불가능 (접근성 문제)
🔧 제안 수정안

옵션 1: 링크로 래핑 (외주 상세 페이지로 이동)

+import Link from "next/link";
+
 const ModifyingCommissionsRow = ({ item }: { item: ModifyingItem }) => {
   const { title, finalDeadline, isSubmitted, hasUpdate } = item;

   return (
-    <div className="border-b-gray-10 hover:bg-gray-5 flex h-15 cursor-pointer items-center border-b py-3 transition-colors duration-150">
+    <Link 
+      href={`/instructor/commissions/${item.commissionId}`}
+      className="border-b-gray-10 hover:bg-gray-5 flex h-15 cursor-pointer items-center border-b py-3 transition-colors duration-150"
+    >
       <div className="flex w-full flex-row justify-between">
         ...
       </div>
-    </div>
+    </Link>
   );
 };

옵션 2: onClick 핸들러 추가

-const ModifyingCommissionsRow = ({ item }: { item: ModifyingItem }) => {
+const ModifyingCommissionsRow = ({ 
+  item, 
+  onClick 
+}: { 
+  item: ModifyingItem;
+  onClick?: () => void;
+}) => {
   const { title, finalDeadline, isSubmitted, hasUpdate } = item;

   return (
     <div 
       className="border-b-gray-10 hover:bg-gray-5 flex h-15 cursor-pointer items-center border-b py-3 transition-colors duration-150"
+      onClick={onClick}
+      role="button"
+      tabIndex={0}
+      onKeyDown={(e) => e.key === 'Enter' && onClick?.()}
     >

옵션 3: 향후 구현 예정이라면 시각적 힌트 제거

-    <div className="border-b-gray-10 hover:bg-gray-5 flex h-15 cursor-pointer items-center border-b py-3 transition-colors duration-150">
+    <div className="border-b-gray-10 flex h-15 items-center border-b py-3">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/instructor/home/ModifyingCommissionsRow.tsx` at line 11, The
row in ModifyingCommissionsRow.tsx uses visual affordances (cursor-pointer,
hover:bg-gray-5) but lacks interaction—fix by either wiring a real handler or
removing affordances: add an onClick prop to the component and attach it to the
div (e.g., implement an onRowClick handler in ModifyingCommissionsRow and pass
it down), ensure keyboard accessibility by adding role="button", tabIndex={0},
and handling onKeyDown for Enter/Space, and include an aria-label;
alternatively, wrap the content in a Link if navigation is intended or remove
cursor-pointer/hover classes if no interaction should be available.

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.

추후 경로 연결 예정

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@waldls, 알겠습니다! 추후 경로 연결 시 Link 컴포넌트 래핑 또는 onClick 핸들러와 함께 접근성 속성(role="button", tabIndex={0}, onKeyDown)도 함께 추가해 주시면 좋을 것 같습니다.

해당 사항을 추적하기 위해 GitHub 이슈를 생성해 드릴까요?


🧠 Learnings used
Learnt from: waldls
Repo: Ditda-Official/Ditda-Frontend PR: 18
File: src/constants/write.ts:65-82
Timestamp: 2026-05-27T08:26:49.054Z
Learning: In Ditda-Frontend (TypeScript/Next.js), exported constants should be named in SCREAMING_SNAKE_CASE (e.g., BASIC_INFO_FIELDS, PAGE_OPTIONS). The camelCase naming guideline should apply to functions and non-exported/regular variables, but not to exported constants.

Comment thread src/constants/write.ts
Comment on lines 146 to 151
export const SIZE_DISPLAY_MAP: Record<string, string> = {
a4: "A4",
국배판: "국배판",
국배판: "KUKBAE",
b5: "B5",
a5: "A5",
};

@coderabbitai coderabbitai Bot Jun 2, 2026

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check how SIZE_DISPLAY_MAP is used in the codebase
rg -n "SIZE_DISPLAY_MAP" --type=ts -A3 -B1

Repository: Ditda-Official/Ditda-Frontend

Length of output: 1469


SIZE_DISPLAY_MAP의 “국배판” 값이 API 코드(KUKBAE)로 표시될 수 있습니다

  • src/components/instructor/write/PaymentModal/Step1.tsx에서 InfoRow의 사이즈 값이 SIZE_DISPLAY_MAP[selectedSize]로 렌더링됩니다. 그런데 src/constants/write.ts에서 SIZE_DISPLAY_MAP['국배판']"KUKBAE"라서, selectedSize국배판일 경우 화면에 KUKBAE가 노출될 수 있습니다. SIZE_DISPLAY_MAP['국배판']을 사용자 표시값으로 맞춰주세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/constants/write.ts` around lines 146 - 151, The SIZE_DISPLAY_MAP entry
for the key "국배판" is currently set to the API code "KUKBAE", causing the UI
(InfoRow in src/components/instructor/write/PaymentModal/Step1.tsx which renders
SIZE_DISPLAY_MAP[selectedSize]) to show the code instead of a user-friendly
label; update the value for the "국배판" key in the SIZE_DISPLAY_MAP constant (in
src/constants/write.ts) to the appropriate display string (e.g., "국배판") so
selectedSize renders a user-facing label rather than the API code.

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.

ENUM 스펙에 맞춘 문자열임

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@waldls 이해했습니다. SIZE_DISPLAY_MAP의 값이 백엔드 ENUM 스펙에 맞게 의도적으로 "KUKBAE"로 설정된 것이군요. 이 내용을 기억해 두겠습니다.

🐇 (•ω•)


✏️ Learnings added
Learnt from: waldls
Repo: Ditda-Official/Ditda-Frontend PR: 24
File: src/constants/write.ts:146-151
Timestamp: 2026-06-02T09:42:27.963Z
Learning: In Ditda-Frontend (`src/constants/write.ts`), `SIZE_DISPLAY_MAP` intentionally uses backend ENUM code strings (e.g., `국배판: "KUKBAE"`) rather than user-friendly Korean labels. These values are aligned with the backend ENUM specification, not display purposes.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: waldls
Repo: Ditda-Official/Ditda-Frontend PR: 18
File: src/constants/write.ts:65-82
Timestamp: 2026-05-27T08:26:49.054Z
Learning: In Ditda-Frontend (TypeScript/Next.js), exported constants should be named in SCREAMING_SNAKE_CASE (e.g., BASIC_INFO_FIELDS, PAGE_OPTIONS). The camelCase naming guideline should apply to functions and non-exported/regular variables, but not to exported constants.

Comment on lines +8 to +9
const handlePrev = () => setCurrent(prev => Math.max(0, prev - 1));
const handleNext = () => setCurrent(prev => Math.min(totalPages - 1, prev + 1));

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 | ⚡ Quick win

빈 배열 케이스 방어 로직 추가 권장

items가 빈 배열일 때 totalPages가 0이 되면, Line 9의 Math.min(totalPages - 1, prev + 1)Math.min(-1, prev + 1)이 되어 current가 음수가 될 수 있습니다. 실제로는 pageItems가 빈 배열을 반환해 화면에 문제가 없을 수 있지만, 상태가 논리적으로 부정확합니다.

🛡️ 제안 수정안
- const handlePrev = () => setCurrent(prev => Math.max(0, prev - 1));
- const handleNext = () => setCurrent(prev => Math.min(totalPages - 1, prev + 1));
+ const handlePrev = () => setCurrent(prev => (totalPages > 0 ? Math.max(0, prev - 1) : 0));
+ const handleNext = () => setCurrent(prev => (totalPages > 0 ? Math.min(totalPages - 1, prev + 1) : 0));
📝 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 handlePrev = () => setCurrent(prev => Math.max(0, prev - 1));
const handleNext = () => setCurrent(prev => Math.min(totalPages - 1, prev + 1));
const handlePrev = () => setCurrent(prev => (totalPages > 0 ? Math.max(0, prev - 1) : 0));
const handleNext = () => setCurrent(prev => (totalPages > 0 ? Math.min(totalPages - 1, prev + 1) : 0));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/hooks/usePagination.ts` around lines 8 - 9, When computing next page,
guard against totalPages being 0 to avoid negative pages: compute a non-negative
lastPage (e.g., const lastPage = Math.max(0, totalPages - 1)) and use it in
handleNext (setCurrent(prev => Math.min(lastPage, prev + 1))). Keep handlePrev
as setCurrent(prev => Math.max(0, prev - 1)) so current never goes below 0;
update references to totalPages, lastPage, handleNext, handlePrev, and
setCurrent accordingly.

@KOJ50 KOJ50 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

굳굳 좋습니다!! 수고하셨습니다

@waldls waldls merged commit f292f23 into main Jun 3, 2026
2 checks passed
@waldls waldls deleted the feature/#23-instructor-dashboard branch June 3, 2026 03:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 강사 대시보드 페이지 퍼블리싱 및 새 외주 작성 수정사항 반영

2 participants