Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/app/instructor/my/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import { CommissionsHistorySection, MyInfoSection } from "@/widgets/instructor/my";

const page = () => {
return <div>강사 마이페이지</div>;
return (
<div className="mx-auto flex w-212.75 flex-col items-center pt-15">
<h1 className="text-title2-sb w-full pb-10 text-left text-black">마이페이지</h1>
<div className="flex flex-col gap-8">
<MyInfoSection />
<CommissionsHistorySection />
</div>
</div>
);
};

export default page;
8 changes: 8 additions & 0 deletions src/features/instructor/my/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type { CommissionHistoryItem } from "@/features/instructor/my/model/my";
export {
CATEGORY_BADGE_MAP,
commissionHistoryData,
PLAN_DISPLAY_MAP,
} from "@/features/instructor/my/model/my";
export { default as CommissionsHeader } from "@/features/instructor/my/ui/CommissionsHeader";
export { default as CommissionsHistoryRow } from "@/features/instructor/my/ui/CommissionsHistoryRow";
115 changes: 115 additions & 0 deletions src/features/instructor/my/model/my.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { BadgeVariant } from "@/shared/ui/Badge";

// [강사] 마이페이지 외주 내역 조회
export type CommissionHistoryItem = {
commissionId: number;
category: string;
title: string;
createdAt: string;
plan: "BASIC" | "PLUS" | "MAX";
totalAmount: number;
status: "COMPLETED" | "ONGOING";
};

export const PLAN_DISPLAY_MAP: Record<CommissionHistoryItem["plan"], string> = {
BASIC: "기본",
PLUS: "플러스",
MAX: "맥스",
};

export const CATEGORY_BADGE_MAP: Record<string, BadgeVariant> = {
FLYER_TEXTBOOK_COVER_INNER: "교재",
};

export const commissionHistoryData: CommissionHistoryItem[] = [
{
commissionId: 1,
category: "FLYER_TEXTBOOK_COVER_INNER",
title: "YMB 영어교재 표지디자인 외주",
createdAt: "2025-05-05",
plan: "BASIC",
totalAmount: 400000,
status: "COMPLETED",
},
{
commissionId: 2,
category: "FLYER_TEXTBOOK_COVER_INNER",
title: "해커스톡 왕초보 영어 기초 문법편 표지디자인 외주",
createdAt: "2025-04-18",
plan: "PLUS",
totalAmount: 480000,
status: "COMPLETED",
},
{
commissionId: 3,
category: "FLYER_TEXTBOOK_COVER_INNER",
title: "토익 실전 모의고사 Part 5·6 표지디자인 외주",
createdAt: "2025-03-24",
plan: "MAX",
totalAmount: 560000,
status: "COMPLETED",
},
{
commissionId: 4,
category: "FLYER_TEXTBOOK_COVER_INNER",
title: "수능 영어 독해 빈칸추론 완성 표지디자인 외주",
createdAt: "2025-03-02",
plan: "BASIC",
totalAmount: 400000,
status: "COMPLETED",
},
{
commissionId: 5,
category: "FLYER_TEXTBOOK_COVER_INNER",
title: "중학 수학 개념서 1학기 과정 표지디자인 외주",
createdAt: "2025-02-14",
plan: "PLUS",
totalAmount: 480000,
status: "ONGOING",
},
{
commissionId: 6,
category: "FLYER_TEXTBOOK_COVER_INNER",
title: "고등 국어 문학 현대시 집중 표지디자인 외주",
createdAt: "2025-01-28",
plan: "MAX",
totalAmount: 560000,
status: "ONGOING",
},
{
commissionId: 7,
category: "FLYER_TEXTBOOK_COVER_INNER",
title: "중등 과학 탐구 물질과 에너지 표지디자인 외주",
createdAt: "2024-12-19",
plan: "BASIC",
totalAmount: 400000,
status: "COMPLETED",
},
{
commissionId: 8,
category: "FLYER_TEXTBOOK_COVER_INNER",
title: "고등 수학 II 미적분 집중 완성 표지디자인 외주",
createdAt: "2024-11-30",
plan: "PLUS",
totalAmount: 480000,
status: "COMPLETED",
},
{
commissionId: 9,
category: "FLYER_TEXTBOOK_COVER_INNER",
title: "초등 사회 한국사 인물편 표지디자인 외주",
createdAt: "2024-11-12",
plan: "BASIC",
totalAmount: 400000,
status: "ONGOING",
},
{
commissionId: 10,
category: "FLYER_TEXTBOOK_COVER_INNER",
title: "고등 영어 듣기평가 모의고사 표지디자인 외주",
createdAt: "2024-10-27",
plan: "MAX",
totalAmount: 560000,
status: "COMPLETED",
},
];
14 changes: 14 additions & 0 deletions src/features/instructor/my/ui/CommissionsHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const CommissionsHeader = () => {
return (
<div className="text-gray-70 text-caption1-r border-b-gray-20 flex w-full shrink-0 flex-row justify-between border-b px-3 py-2">
<p>외주</p>
<div className="flex gap-16">
<p className="w-25">외주 신청 일자</p>
<p className="w-14">플랜</p>
<p className="w-25">금액</p>
</div>
</div>
);
};

export default CommissionsHeader;
30 changes: 30 additions & 0 deletions src/features/instructor/my/ui/CommissionsHistoryRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {
CATEGORY_BADGE_MAP,
CommissionHistoryItem,
PLAN_DISPLAY_MAP,
} from "@/features/instructor/my/model/my";
import { ArrowRightIcon } from "@/shared/assets/icons";
import Badge from "@/shared/ui/Badge";

const CommissionsHistoryRow = ({ item }: { item: CommissionHistoryItem }) => {
const { category, title, createdAt, plan, totalAmount } = item;

return (
<div className="hover:bg-gray-5 border-b-gray-20 flex h-19.25 w-full shrink-0 cursor-pointer items-center justify-between border-b bg-white px-3 py-5 transition-colors duration-150">
<div className="flex flex-row gap-6">
<Badge variant={CATEGORY_BADGE_MAP[category] ?? "교재"} />
<div className="text-gray-90 flex flex-row items-center gap-1">
<p className="text-heading3-m max-w-70 truncate">{title}</p>
<ArrowRightIcon className="size-6" />
</div>
Comment on lines +13 to +19

@coderabbitai coderabbitai Bot Jun 7, 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

클릭 가능한 UI처럼 보이지만 실제 동작이 없습니다.

cursor-pointer와 화살표 아이콘으로 이동 가능한 행처럼 보이는데, 현재는 클릭 동작이 없어 사용자에게 오해를 줍니다. 상세 이동을 아직 연결하지 않았다면 인터랙티브 스타일을 제거하거나, 지금 연결할 수 있다면 Link/button으로 semantics까지 맞춰주세요.

제안 diff (미구현 상태 유지 시)
-    <div className="hover:bg-gray-5 border-b-gray-20 flex h-19.25 w-full shrink-0 cursor-pointer items-center justify-between border-b bg-white px-3 py-5 transition-colors duration-150">
+    <div className="hover:bg-gray-5 border-b-gray-20 flex h-19.25 w-full shrink-0 items-center justify-between border-b bg-white px-3 py-5 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/features/instructor/my/ui/CommissionsHistoryRow.tsx` around lines 13 -
19, The row in CommissionsHistoryRow renders as clickable (cursor-pointer and
ArrowRightIcon) but has no click handler or navigation; either make it truly
interactive by wrapping the row content in a semantic interactive element (e.g.,
Link or button) and add the onClick/navigation logic to open the detail (ensure
accessibility attributes and keyboard handling) or remove the interactive
affordances (remove "cursor-pointer" class and ArrowRightIcon) so it isn't
misleading; update the JSX around the outer div (the element with className
including "cursor-pointer") and the ArrowRightIcon usage accordingly.

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.

추후 연결 예정 - PR 기타 사항 부분에 작성되어있음

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, PR 기타 사항에 명시되어 있군요 — 확인했습니다. 추후 상세 페이지 이동을 연결할 때 div 대신 Link 또는 button으로 교체하고 키보드 접근성도 함께 맞춰주시면 좋을 것 같습니다.

혹시 이 작업을 추적하기 위한 별도 이슈를 생성해 드릴까요?


🧠 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.

</div>
<div className="flex flex-row items-center gap-16">
<p className="text-gray-70 text-heading2-m w-25">{createdAt.replaceAll("-", ".")}</p>
<p className="text-gray-70 text-heading2-m w-14">{PLAN_DISPLAY_MAP[plan]}</p>
<p className="text-gray-90 text-heading3-m w-25">{totalAmount.toLocaleString()}원</p>
</div>
</div>
);
};

export default CommissionsHistoryRow;
9 changes: 7 additions & 2 deletions src/shared/ui/Badge.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type BadgeVariant = "3인" | "4인" | "5인" | "pass" | "fail" | "waiting";
export type BadgeVariant = "3인" | "4인" | "5인" | "교재" | "pass" | "fail" | "waiting";

const badgeStyleMap: Record<BadgeVariant, { wrapper: string; text: string; label: string }> = {
"3인": {
Expand All @@ -16,6 +16,11 @@ const badgeStyleMap: Record<BadgeVariant, { wrapper: string; text: string; label
text: "text-body2-sb text-red-main",
label: "5인",
},
교재: {
wrapper: "rounded-4 w-fit px-2 bg-purple-20",
text: "text-body2-sb text-purple-60",
label: "교재",
},
pass: {
wrapper: "flex items-center justify-center rounded-32 w-14.5 px-3 py-1 bg-blue-bright",
text: "text-caption1-m text-blue-main",
Expand All @@ -40,7 +45,7 @@ interface BadgeProps {
const Badge = ({ variant }: BadgeProps) => {
const { wrapper, text, label } = badgeStyleMap[variant];
return (
<div className={wrapper}>
<div className={`shrink-0 whitespace-nowrap ${wrapper}`}>
<p className={text}>{label}</p>
</div>
);
Expand Down
27 changes: 19 additions & 8 deletions src/shared/ui/PageIndicator.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
import { cn } from "@/shared/lib/utils/cn";

interface PageIndicatorProps {
total: number;
current: number;
variant?: "home" | "my";
}

const PageIndicator = ({ total, current }: PageIndicatorProps) => {
const PageIndicator = ({ total, current, variant = "home" }: 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"}`}
/>
))}
<div className={cn("flex items-center", variant === "my" ? "gap-3" : "gap-2")}>
{Array.from({ length: total }, (_, i) => {
const active = i === current;

return (
<div
key={i}
className={cn(
"transition-colors duration-300",
variant === "my" ? "rounded-12 size-3" : "size-2 rounded-full",
active ? (variant === "my" ? "bg-gray-90" : "bg-gray-70") : "bg-gray-30",
)}
/>
);
})}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import {
DraftSubmissionStatusRow,
} from "@/features/instructor/home";
import { NextButton, PrevButton } from "@/shared/assets/icons";
import usePagination from "@/shared/lib/hooks/usePagination";
import PageIndicator from "@/shared/ui/PageIndicator";
import { DRAFT_SUBMISSION_ITEMS_PER_PAGE } from "@/widgets/instructor/home/config/home";
import usePagination from "@/widgets/instructor/home/lib/usePagination";

const DraftSubmissionStatusSection = () => {
const { current, totalPages, pageItems, handlePrev, handleNext } = usePagination(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import {
matchingStatusData,
} from "@/features/instructor/home";
import { NextButton, PrevButton } from "@/shared/assets/icons";
import usePagination from "@/shared/lib/hooks/usePagination";
import PageIndicator from "@/shared/ui/PageIndicator";
import { MATCHING_ITEMS_PER_PAGE } from "@/widgets/instructor/home/config/home";
import usePagination from "@/widgets/instructor/home/lib/usePagination";

const MatchingCommissionsSection = () => {
const { current, totalPages, pageItems, handlePrev, handleNext } = usePagination(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import {
modifyingStatusData,
} from "@/features/instructor/home";
import { NextButton, PrevButton } from "@/shared/assets/icons";
import usePagination from "@/shared/lib/hooks/usePagination";
import PageIndicator from "@/shared/ui/PageIndicator";
import { MODIFYING_ITEMS_PER_PAGE } from "@/widgets/instructor/home/config/home";
import usePagination from "@/widgets/instructor/home/lib/usePagination";

const ModifyingCommissionsSection = () => {
const { current, totalPages, pageItems, handlePrev, handleNext } = usePagination(
Expand Down
1 change: 1 addition & 0 deletions src/widgets/instructor/my/config/my.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const COMMISSION_HISTORY_ITEMS_PER_PAGE = 3;
2 changes: 2 additions & 0 deletions src/widgets/instructor/my/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as CommissionsHistorySection } from "@/widgets/instructor/my/ui/CommissionsHistorySection";
export { default as MyInfoSection } from "@/widgets/instructor/my/ui/MyInfoSection";
18 changes: 18 additions & 0 deletions src/widgets/instructor/my/model/my.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// [강사] 마이페이지 본인 정보 + 통계 조회
export type MyInfo = {
name: string;
profileImageUrl: string;
stats: {
totalCommissionCount: number;
ongoingCommissionCount: number;
};
};

export const myInfoData: MyInfo = {
name: "고다현",
profileImageUrl: "",
stats: {
totalCommissionCount: 5,
ongoingCommissionCount: 5,
},
Comment on lines +14 to +17

@coderabbitai coderabbitai Bot Jun 7, 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

통계 목데이터가 외주 내역 데이터와 불일치합니다.

현재 목데이터 기준으로 외주 내역은 총 10건/진행중 3건인데, 여기서는 5/5로 노출됩니다. 동일 페이지에서 함께 보여주므로 수치를 맞춰두는 게 안전합니다.

제안 diff
   stats: {
-    totalCommissionCount: 5,
-    ongoingCommissionCount: 5,
+    totalCommissionCount: 10,
+    ongoingCommissionCount: 3,
   },
📝 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
stats: {
totalCommissionCount: 5,
ongoingCommissionCount: 5,
},
stats: {
totalCommissionCount: 10,
ongoingCommissionCount: 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/widgets/instructor/my/model/my.ts` around lines 14 - 17, The stats mock
in the my model is inconsistent with the commission list; update the stats
object so totalCommissionCount and ongoingCommissionCount reflect the actual
mock data (set totalCommissionCount to 10 and ongoingCommissionCount to 3) in
the stats block (symbols: stats, totalCommissionCount, ongoingCommissionCount)
inside the my.ts model.

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.

mockData임

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 목데이터임을 인지하지 못했습니다, 지적 감사합니다. 해당 코멘트는 무시해 주세요.

🐇


✏️ Learnings added
Learnt from: waldls
Repo: Ditda-Official/Ditda-Frontend PR: 28
File: src/widgets/instructor/my/model/my.ts:14-17
Timestamp: 2026-06-07T17:37:21.449Z
Learning: In Ditda-Frontend, `myInfoData` in `src/widgets/instructor/my/model/my.ts` and `commissionHistoryData` in `src/features/instructor/my/model/my.ts` are independent mock/dummy data objects. Their stat fields (e.g., `totalCommissionCount`, `ongoingCommissionCount`) do not need to match the actual length or contents of the commission history mock array — they are placeholder values only.

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.

};
37 changes: 37 additions & 0 deletions src/widgets/instructor/my/ui/CommissionsHistorySection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use client";

import {
commissionHistoryData,
CommissionsHeader,
CommissionsHistoryRow,
} from "@/features/instructor/my";
import { NextButton, PrevButton } from "@/shared/assets/icons";
import usePagination from "@/shared/lib/hooks/usePagination";
import PageIndicator from "@/shared/ui/PageIndicator";
import { COMMISSION_HISTORY_ITEMS_PER_PAGE } from "@/widgets/instructor/my/config/my";

const CommissionsHistorySection = () => {
const { current, totalPages, pageItems, handlePrev, handleNext } = usePagination(
commissionHistoryData,
COMMISSION_HISTORY_ITEMS_PER_PAGE,
);

return (
<div className="rounded-12 flex h-auto w-212.75 flex-col gap-6 bg-white p-6">
<h1 className="text-heading1-sb text-black">외주 내역 확인</h1>
<div className="flex h-66.25 flex-col">
<CommissionsHeader />
{pageItems.map(item => (
<CommissionsHistoryRow key={item.commissionId} item={item} />
))}
</div>
<div className="flex flex-row items-center justify-center gap-8">
<PrevButton className="size-12 cursor-pointer" onClick={handlePrev} />
<PageIndicator total={totalPages} current={current} variant="my" />
<NextButton className="size-12 cursor-pointer" onClick={handleNext} />
Comment on lines +29 to +31

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

아이콘을 직접 클릭 요소로 쓰지 말고 버튼 시맨틱으로 감싸주세요.

Line 29, Line 31은 키보드 포커스/스크린리더 접근성이 보장되지 않아 페이지네이션 조작이 막힐 수 있습니다. <button>으로 감싸고 aria-label/disabled 상태를 추가하는 쪽이 안전합니다.

수정 예시
-      <div className="flex flex-row items-center justify-center gap-8">
-        <PrevButton className="size-12 cursor-pointer" onClick={handlePrev} />
-        <PageIndicator total={totalPages} current={current} variant="my" />
-        <NextButton className="size-12 cursor-pointer" onClick={handleNext} />
-      </div>
+      <div className="flex flex-row items-center justify-center gap-8">
+        <button
+          type="button"
+          onClick={handlePrev}
+          aria-label="이전 페이지"
+          disabled={current === 0}
+          className="cursor-pointer disabled:cursor-not-allowed disabled:opacity-40"
+        >
+          <PrevButton className="size-12" />
+        </button>
+        <PageIndicator total={totalPages} current={current} variant="my" />
+        <button
+          type="button"
+          onClick={handleNext}
+          aria-label="다음 페이지"
+          disabled={current >= totalPages - 1}
+          className="cursor-pointer disabled:cursor-not-allowed disabled:opacity-40"
+        >
+          <NextButton className="size-12" />
+        </button>
+      </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/widgets/instructor/my/ui/CommissionsHistorySection.tsx` around lines 29 -
31, Wrap the PrevButton and NextButton icon components with semantic <button>
elements (rather than using the icon as the clickable element), move onClick
handlers (handlePrev, handleNext) to those buttons, add descriptive aria-labels
(e.g., "Previous page"/"Next page") and set disabled when at boundaries using
current and totalPages so keyboard and screen-reader users can operate
pagination; ensure PageIndicator usage remains unchanged and that any existing
className/aria props are forwarded to the button wrapper.

</div>
</div>
);
};

export default CommissionsHistorySection;
30 changes: 30 additions & 0 deletions src/widgets/instructor/my/ui/MyInfoSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ProfileCircleIcon } from "@/shared/assets/icons";
import { myInfoData } from "@/widgets/instructor/my/model/my";

const MyInfoSection = () => {
const { name, stats } = myInfoData;

return (
<div className="w-212.75">
<div className="flex flex-row items-center gap-4 rounded-t-xl bg-purple-50 px-6 py-5">
<ProfileCircleIcon className="size-8 text-white" />
<p className="text-heading2-sb text-white">{name}</p>
</div>
<div className="rounded-b-xl bg-white px-30 py-6">
<div className="flex justify-between">
<div className="flex w-fit flex-row gap-14 px-20 whitespace-nowrap">
<p className="text-gray-70 text-body2-m">외주 이용 횟수</p>
<p className="text-gray-80 text-heading3-sb">{stats.totalCommissionCount}회</p>
</div>
<div className="border-gray-30 w-1 border-l" />
<div className="flex w-fit flex-row gap-14 px-20 whitespace-nowrap">
<p className="text-gray-70 text-body2-m">진행 중인 외주 건수</p>
<p className="text-gray-80 text-heading3-sb">{stats.ongoingCommissionCount}건</p>
</div>
</div>
</div>
</div>
);
};

export default MyInfoSection;
Loading