From 74ce7a319099078f4acd46ace0997f82cf720a3e Mon Sep 17 00:00:00 2001 From: YuminPark Date: Wed, 1 Jul 2026 15:19:49 +0900 Subject: [PATCH 01/13] =?UTF-8?q?#47=20[FEAT]=20=EA=B0=95=EC=82=AC=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=86=B5?= =?UTF-8?q?=EA=B3=84=20=EC=A1=B0=ED=9A=8C=20API=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/instructor/(withSidebar)/my/page.tsx | 9 +++------ src/features/instructor/my/api/my.ts | 10 ++++++++++ src/features/instructor/my/api/myTypes.ts | 10 ++++++++++ src/features/instructor/my/index.ts | 1 + src/widgets/instructor/my/ui/MyInfoSection.tsx | 11 +++-------- 5 files changed, 27 insertions(+), 14 deletions(-) create mode 100644 src/features/instructor/my/api/my.ts create mode 100644 src/features/instructor/my/api/myTypes.ts diff --git a/src/app/instructor/(withSidebar)/my/page.tsx b/src/app/instructor/(withSidebar)/my/page.tsx index a31f1b5..a8df343 100644 --- a/src/app/instructor/(withSidebar)/my/page.tsx +++ b/src/app/instructor/(withSidebar)/my/page.tsx @@ -1,17 +1,14 @@ -import { cookies } from "next/headers"; - +import { getMyInfo } from "@/features/instructor/my/api/my"; import { CommissionsHistorySection, MyInfoSection } from "@/widgets/instructor/my"; const page = async () => { - const cookieStore = await cookies(); - const name = cookieStore.get("userName")?.value ?? ""; - const profileImageUrl = cookieStore.get("userProfileImageUrl")?.value; + const myInfo = await getMyInfo(); return (

마이페이지

- + {myInfo != null && }
diff --git a/src/features/instructor/my/api/my.ts b/src/features/instructor/my/api/my.ts new file mode 100644 index 0000000..1c58c7b --- /dev/null +++ b/src/features/instructor/my/api/my.ts @@ -0,0 +1,10 @@ +import type { MyInfo } from "@/features/instructor/my/api/myTypes"; +import type { ApiResponse } from "@/shared/api/commonType"; +import { serverApi } from "@/shared/api/server"; + +// 강사 마이페이지 통계 조회 +export const getMyInfo = async (): Promise => { + const response = await serverApi.get("/api/v1/instructors/me").json>(); + + return response.result; +}; diff --git a/src/features/instructor/my/api/myTypes.ts b/src/features/instructor/my/api/myTypes.ts new file mode 100644 index 0000000..99d29f2 --- /dev/null +++ b/src/features/instructor/my/api/myTypes.ts @@ -0,0 +1,10 @@ +export type MyInfoStats = { + totalCommissionCount: number; + ongoingCommissionCount: number; +}; + +export type MyInfo = { + name: string; + profileImageUrl: string; + stats: MyInfoStats; +}; diff --git a/src/features/instructor/my/index.ts b/src/features/instructor/my/index.ts index 64097c1..ee6afd2 100644 --- a/src/features/instructor/my/index.ts +++ b/src/features/instructor/my/index.ts @@ -1,3 +1,4 @@ +export type { MyInfo } from "./api/myTypes"; export type { CommissionHistoryItem } from "./model/myMock"; export { CATEGORY_BADGE_MAP, commissionHistoryData, PLAN_DISPLAY_MAP } from "./model/myMock"; export { default as CommissionsHeader } from "./ui/CommissionsHeader"; diff --git a/src/widgets/instructor/my/ui/MyInfoSection.tsx b/src/widgets/instructor/my/ui/MyInfoSection.tsx index aa0b033..5033d97 100644 --- a/src/widgets/instructor/my/ui/MyInfoSection.tsx +++ b/src/widgets/instructor/my/ui/MyInfoSection.tsx @@ -1,16 +1,11 @@ import Image from "next/image"; -import { myInfoData } from "@/features/instructor/my/model/myMock"; +import type { MyInfo } from "@/features/instructor/my"; import { ProfileCircleIcon } from "@/shared/assets/icons"; -interface MyInfoSectionProps { - name: string; - profileImageUrl?: string; -} - -const MyInfoSection = ({ name, profileImageUrl }: MyInfoSectionProps) => { - const { stats } = myInfoData; +type MyInfoSectionProps = MyInfo; +const MyInfoSection = ({ name, profileImageUrl, stats }: MyInfoSectionProps) => { return (
From c7e93464d225110ff51e094c2c40be1cffefd545 Mon Sep 17 00:00:00 2001 From: YuminPark Date: Wed, 1 Jul 2026 15:23:16 +0900 Subject: [PATCH 02/13] =?UTF-8?q?#47=20[FEAT]=20=EA=B0=95=EC=82=AC=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=97=86=EC=9D=84=20=EB=95=8C=20=ED=85=8D?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../my/ui/CommissionsHistorySection.tsx | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/widgets/instructor/my/ui/CommissionsHistorySection.tsx b/src/widgets/instructor/my/ui/CommissionsHistorySection.tsx index 3ff6100..9fef168 100644 --- a/src/widgets/instructor/my/ui/CommissionsHistorySection.tsx +++ b/src/widgets/instructor/my/ui/CommissionsHistorySection.tsx @@ -21,15 +21,25 @@ const CommissionsHistorySection = () => {

외주 내역 확인

- {pageItems.map(item => ( - - ))} -
-
- - - + {commissionHistoryData.length === 0 ? ( +
+

진행된 외주가 없습니다

+
+ ) : ( + <> + {pageItems.map(item => ( + + ))} + + )}
+ {commissionHistoryData.length > 0 && ( +
+ + + +
+ )}
); }; From 61f32838aaac9de54de24f03dca4f84fb7657aba Mon Sep 17 00:00:00 2001 From: YuminPark Date: Wed, 1 Jul 2026 15:32:56 +0900 Subject: [PATCH 03/13] =?UTF-8?q?#47=20[FEAT]=20=EA=B0=95=EC=82=AC=20?= =?UTF-8?q?=EC=99=B8=EC=A3=BC=20=EB=82=B4=EC=97=AD=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/instructor/my/api/myTypes.ts | 10 +++++ src/features/instructor/my/index.ts | 5 +-- .../my/ui/CommissionsHistoryRow.tsx | 8 ++-- src/widgets/instructor/my/api/my.ts | 25 +++++++++++++ src/widgets/instructor/my/api/myTypes.ts | 9 +++++ .../my/ui/CommissionsHistorySection.tsx | 37 +++++++++++-------- 6 files changed, 73 insertions(+), 21 deletions(-) create mode 100644 src/widgets/instructor/my/api/my.ts create mode 100644 src/widgets/instructor/my/api/myTypes.ts diff --git a/src/features/instructor/my/api/myTypes.ts b/src/features/instructor/my/api/myTypes.ts index 99d29f2..81f2fb1 100644 --- a/src/features/instructor/my/api/myTypes.ts +++ b/src/features/instructor/my/api/myTypes.ts @@ -8,3 +8,13 @@ export type MyInfo = { profileImageUrl: string; stats: MyInfoStats; }; + +export type CommissionHistoryItem = { + commissionId: number; + category: string; + title: string; + createdAt: string; + plan: "BASIC" | "PLUS" | "MAX"; + paidAmount: number | null; + status: string; +}; diff --git a/src/features/instructor/my/index.ts b/src/features/instructor/my/index.ts index ee6afd2..47870f9 100644 --- a/src/features/instructor/my/index.ts +++ b/src/features/instructor/my/index.ts @@ -1,5 +1,4 @@ -export type { MyInfo } from "./api/myTypes"; -export type { CommissionHistoryItem } from "./model/myMock"; -export { CATEGORY_BADGE_MAP, commissionHistoryData, PLAN_DISPLAY_MAP } from "./model/myMock"; +export type { CommissionHistoryItem, MyInfo } from "./api/myTypes"; +export { CATEGORY_BADGE_MAP, PLAN_DISPLAY_MAP } from "./model/myMock"; export { default as CommissionsHeader } from "./ui/CommissionsHeader"; export { default as CommissionsHistoryRow } from "./ui/CommissionsHistoryRow"; diff --git a/src/features/instructor/my/ui/CommissionsHistoryRow.tsx b/src/features/instructor/my/ui/CommissionsHistoryRow.tsx index 298975a..2bf60fe 100644 --- a/src/features/instructor/my/ui/CommissionsHistoryRow.tsx +++ b/src/features/instructor/my/ui/CommissionsHistoryRow.tsx @@ -2,12 +2,12 @@ import { CATEGORY_BADGE_MAP, CommissionHistoryItem, PLAN_DISPLAY_MAP, -} from "@/features/instructor/my/model/myMock"; +} from "@/features/instructor/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; + const { category, title, createdAt, plan, paidAmount } = item; return (
@@ -21,7 +21,9 @@ const CommissionsHistoryRow = ({ item }: { item: CommissionHistoryItem }) => {

{createdAt.replaceAll("-", ".")}

{PLAN_DISPLAY_MAP[plan]}

-

{totalAmount.toLocaleString()}원

+

+ {paidAmount != null ? `${paidAmount.toLocaleString()}원` : "-"} +

); diff --git a/src/widgets/instructor/my/api/my.ts b/src/widgets/instructor/my/api/my.ts new file mode 100644 index 0000000..2cffef1 --- /dev/null +++ b/src/widgets/instructor/my/api/my.ts @@ -0,0 +1,25 @@ +import type { CommissionHistoryItem } from "@/features/instructor/my"; +import { api } from "@/shared/api/client"; +import type { ApiResponse } from "@/shared/api/commonType"; +import type { GetCommissionsResult } from "@/widgets/instructor/my/api/myTypes"; + +const COMMISSIONS_SIZE = 3; + +// 외주 내역 조회 +export const getCommissions = async (page: number): Promise => { + const response = await api + .get("/api/v1/instructors/commissions", { + searchParams: { page, size: COMMISSIONS_SIZE }, + }) + .json>(); + + return ( + response.result ?? { + items: [] as CommissionHistoryItem[], + page, + size: COMMISSIONS_SIZE, + totalElements: 0, + totalPages: 0, + } + ); +}; diff --git a/src/widgets/instructor/my/api/myTypes.ts b/src/widgets/instructor/my/api/myTypes.ts new file mode 100644 index 0000000..590f0d1 --- /dev/null +++ b/src/widgets/instructor/my/api/myTypes.ts @@ -0,0 +1,9 @@ +import type { CommissionHistoryItem } from "@/features/instructor/my"; + +export type GetCommissionsResult = { + items: CommissionHistoryItem[]; + page: number; + size: number; + totalElements: number; + totalPages: number; +}; diff --git a/src/widgets/instructor/my/ui/CommissionsHistorySection.tsx b/src/widgets/instructor/my/ui/CommissionsHistorySection.tsx index 9fef168..b920dd2 100644 --- a/src/widgets/instructor/my/ui/CommissionsHistorySection.tsx +++ b/src/widgets/instructor/my/ui/CommissionsHistorySection.tsx @@ -1,42 +1,49 @@ "use client"; -import { - commissionHistoryData, - CommissionsHeader, - CommissionsHistoryRow, -} from "@/features/instructor/my"; +import { useEffect, useState } from "react"; + +import type { CommissionHistoryItem } from "@/features/instructor/my"; +import { 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"; +import { getCommissions } from "@/widgets/instructor/my/api/my"; const CommissionsHistorySection = () => { - const { current, totalPages, pageItems, handlePrev, handleNext } = usePagination( - commissionHistoryData, - COMMISSION_HISTORY_ITEMS_PER_PAGE, - ); + const [page, setPage] = useState(0); + const [items, setItems] = useState([]); + const [totalPages, setTotalPages] = useState(0); + + useEffect(() => { + getCommissions(page).then(result => { + setItems(result.items); + setTotalPages(result.totalPages); + }); + }, [page]); + + const handlePrev = () => setPage(prev => Math.max(0, prev - 1)); + const handleNext = () => setPage(prev => Math.min(totalPages - 1, prev + 1)); return (

외주 내역 확인

- {commissionHistoryData.length === 0 ? ( + {items.length === 0 ? (

진행된 외주가 없습니다

) : ( <> - {pageItems.map(item => ( + {items.map(item => ( ))} )}
- {commissionHistoryData.length > 0 && ( + {totalPages > 0 && (
- +
)} From fd43345a1a4ed3b63d55efab6939ba0d4b280fff Mon Sep 17 00:00:00 2001 From: YuminPark Date: Wed, 1 Jul 2026 15:36:01 +0900 Subject: [PATCH 04/13] =?UTF-8?q?#47=20[DEL]=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=AA=A9=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/instructor/my/api/myTypes.ts | 10 ++ src/features/instructor/my/index.ts | 2 +- src/features/instructor/my/model/myMock.ts | 134 --------------------- 3 files changed, 11 insertions(+), 135 deletions(-) delete mode 100644 src/features/instructor/my/model/myMock.ts diff --git a/src/features/instructor/my/api/myTypes.ts b/src/features/instructor/my/api/myTypes.ts index 81f2fb1..4095d87 100644 --- a/src/features/instructor/my/api/myTypes.ts +++ b/src/features/instructor/my/api/myTypes.ts @@ -18,3 +18,13 @@ export type CommissionHistoryItem = { paidAmount: number | null; status: string; }; + +export const PLAN_DISPLAY_MAP: Record = { + BASIC: "기본", + PLUS: "플러스", + MAX: "맥스", +}; + +export const CATEGORY_BADGE_MAP: Record = { + FLYER_TEXTBOOK_COVER_INNER: "교재", +}; diff --git a/src/features/instructor/my/index.ts b/src/features/instructor/my/index.ts index 47870f9..f0269e9 100644 --- a/src/features/instructor/my/index.ts +++ b/src/features/instructor/my/index.ts @@ -1,4 +1,4 @@ export type { CommissionHistoryItem, MyInfo } from "./api/myTypes"; -export { CATEGORY_BADGE_MAP, PLAN_DISPLAY_MAP } from "./model/myMock"; +export { CATEGORY_BADGE_MAP, PLAN_DISPLAY_MAP } from "./api/myTypes"; export { default as CommissionsHeader } from "./ui/CommissionsHeader"; export { default as CommissionsHistoryRow } from "./ui/CommissionsHistoryRow"; diff --git a/src/features/instructor/my/model/myMock.ts b/src/features/instructor/my/model/myMock.ts deleted file mode 100644 index 5a94bcc..0000000 --- a/src/features/instructor/my/model/myMock.ts +++ /dev/null @@ -1,134 +0,0 @@ -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 = { - BASIC: "기본", - PLUS: "플러스", - MAX: "맥스", -}; - -export const CATEGORY_BADGE_MAP: Record = { - FLYER_TEXTBOOK_COVER_INNER: "교재", -}; - -// [강사] 마이페이지 본인 정보 + 통계 조회 -export type MyInfo = { - name: string; - profileImageUrl: string; - stats: { - totalCommissionCount: number; - ongoingCommissionCount: number; - }; -}; - -export const myInfoData: MyInfo = { - name: "고다현", - profileImageUrl: "", - stats: { - totalCommissionCount: 5, - ongoingCommissionCount: 5, - }, -}; - -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", - }, -]; From babd6c580038df85795c9886cc384cff92c8ee51 Mon Sep 17 00:00:00 2001 From: YuminPark Date: Wed, 1 Jul 2026 17:42:12 +0900 Subject: [PATCH 05/13] =?UTF-8?q?#47=20[MOD]=20=EB=9D=BC=EC=9D=B4=ED=8C=85?= =?UTF-8?q?=20=ED=8F=BC=20=EB=8B=A4=EC=9D=8C=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=ED=99=9C=EC=84=B1=ED=99=94=20=EC=A1=B0=EA=B1=B4=20=EB=B3=B4?= =?UTF-8?q?=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../instructor/write/ui/Step1Content.tsx | 17 +++++++++++------ .../instructor/write/ui/Step2Content.tsx | 14 ++++++++++++-- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/widgets/instructor/write/ui/Step1Content.tsx b/src/widgets/instructor/write/ui/Step1Content.tsx index fcfa311..0911873 100644 --- a/src/widgets/instructor/write/ui/Step1Content.tsx +++ b/src/widgets/instructor/write/ui/Step1Content.tsx @@ -8,15 +8,20 @@ import DesignConceptSection from "@/widgets/instructor/write/ui/DesignConceptSec import SizeSection from "@/widgets/instructor/write/ui/SizeSection"; const Step1Content = () => { - const { selectedCategory, selectedSize, selectedKeywords, colorMode, colors, setCurrentStep } = - useWriteFormStore(); + const { + selectedCategory, + selectedSize, + selectedKeywords, + additionalConcept, + colorMode, + colors, + setCurrentStep, + } = useWriteFormStore(); const isColorReady = colorMode === "designer" || colors.every(c => c !== null); + const isConceptReady = selectedKeywords.length >= 1 || additionalConcept.trim().length > 0; const isAllSelected = - selectedCategory !== null && - selectedSize !== null && - selectedKeywords.length >= 1 && - isColorReady; + selectedCategory !== null && selectedSize !== null && isConceptReady && isColorReady; return (
diff --git a/src/widgets/instructor/write/ui/Step2Content.tsx b/src/widgets/instructor/write/ui/Step2Content.tsx index 9996eb4..7a44291 100644 --- a/src/widgets/instructor/write/ui/Step2Content.tsx +++ b/src/widgets/instructor/write/ui/Step2Content.tsx @@ -8,13 +8,23 @@ import NecessaryPageChooseSection from "@/widgets/instructor/write/ui/NecessaryP import ReferenceSection from "@/widgets/instructor/write/ui/ReferenceSection"; const Step2Content = () => { - const { basicInfo, selectedPages, setCurrentStep } = useWriteFormStore(); + const { + basicInfo, + selectedPages, + materialFiles, + materialDescription, + referenceFiles, + referenceDescription, + setCurrentStep, + } = useWriteFormStore(); const isAllFilled = basicInfo.교재명.trim() !== "" && basicInfo.강사명.trim() !== "" && basicInfo.과목명.trim() !== "" && - selectedPages.length >= 1; + selectedPages.length >= 1 && + (materialFiles.length === 0 || materialDescription.trim() !== "") && + (referenceFiles.length === 0 || referenceDescription.trim() !== ""); return (
From 73edc6cd30f6a7594e1784a546fea934fd7dee3f Mon Sep 17 00:00:00 2001 From: YuminPark Date: Wed, 1 Jul 2026 18:30:30 +0900 Subject: [PATCH 06/13] =?UTF-8?q?#47=20[FEAT]=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20URL=20=EB=B0=9C=EA=B8=89=20API=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/instructor/write/api/write.ts | 12 ++++ src/features/instructor/write/config/write.ts | 8 +++ src/features/instructor/write/index.ts | 2 +- src/shared/api/file.ts | 56 +++++++++++++++++++ src/shared/api/fileTypes.ts | 13 +++++ src/shared/lib/hooks/useUploadedFiles.ts | 21 ++++++- src/shared/types/file.ts | 1 + .../instructor/write/ui/AttachFileSection.tsx | 12 +++- .../instructor/write/ui/ReferenceSection.tsx | 12 +++- 9 files changed, 129 insertions(+), 8 deletions(-) create mode 100644 src/shared/api/file.ts create mode 100644 src/shared/api/fileTypes.ts diff --git a/src/features/instructor/write/api/write.ts b/src/features/instructor/write/api/write.ts index 432e9d2..9c5680f 100644 --- a/src/features/instructor/write/api/write.ts +++ b/src/features/instructor/write/api/write.ts @@ -1,6 +1,8 @@ import type { GetPlansResult, Plan } from "@/features/instructor/write/api/writeTypes"; +import type { CommissionFileTarget } from "@/features/instructor/write/config/write"; import { api, createApiPath } from "@/shared/api/client"; import type { ApiResponse } from "@/shared/api/commonType"; +import { postFilePresignedUrl, uploadFileToPresignedUrl } from "@/shared/api/file"; // 플랜 조회 export const getPlans = async (): Promise => { @@ -10,3 +12,13 @@ export const getPlans = async (): Promise => { return response.result?.plans ?? []; }; + +// 외주 첨부 파일 업로드 (presigned URL 발급 후 업로드, 반환된 key를 외주 작성 요청에 사용) +export const uploadCommissionFile = async (file: File, target: CommissionFileTarget) => { + const contentType = file.type || "image/png"; + const { key, presignedUrl } = await postFilePresignedUrl({ target, contentType }); + + await uploadFileToPresignedUrl({ file, presignedUrl, contentType }); + + return key; +}; diff --git a/src/features/instructor/write/config/write.ts b/src/features/instructor/write/config/write.ts index 7b26e66..e90bc08 100644 --- a/src/features/instructor/write/config/write.ts +++ b/src/features/instructor/write/config/write.ts @@ -64,6 +64,14 @@ export const MAX_CONCEPT_SELECT = 5; /* ========================= STEP2 ========================= */ +export const COMMISSION_FILE_TARGET = { + MATERIAL: "COMMISSION_MATERIAL", + REFERENCE: "COMMISSION_REFERENCE", +} as const; + +export type CommissionFileTarget = + (typeof COMMISSION_FILE_TARGET)[keyof typeof COMMISSION_FILE_TARGET]; + export const BASIC_INFO_FIELDS = [ { label: "교재명", placeholder: "교재명을 작성해주세요" }, { label: "강사명", placeholder: "강사명을 작성해주세요" }, diff --git a/src/features/instructor/write/index.ts b/src/features/instructor/write/index.ts index 74cbda0..a8976fd 100644 --- a/src/features/instructor/write/index.ts +++ b/src/features/instructor/write/index.ts @@ -1,4 +1,4 @@ -export { getPlans } from "./api/write"; +export { getPlans, uploadCommissionFile } from "./api/write"; export type { Plan } from "./api/writeTypes"; export * from "./config/write"; export type { RgbaColor } from "./lib/color"; diff --git a/src/shared/api/file.ts b/src/shared/api/file.ts new file mode 100644 index 0000000..41ff179 --- /dev/null +++ b/src/shared/api/file.ts @@ -0,0 +1,56 @@ +import { + api, + ApiError, + createApiPath, + getApiResponseMessage, + toApiError, +} from "@/shared/api/client"; +import type { ApiResponse } from "@/shared/api/commonType"; +import { + type FilePresignedUrlResult, + filePresignedUrlResultSchema, + type PostFilePresignedUrlBody, +} from "@/shared/api/fileTypes"; + +// 파일 업로드 presigned URL 발급 +export const postFilePresignedUrl = async ( + body: PostFilePresignedUrlBody, +): Promise => { + try { + const response = await api + .post(createApiPath("/api/v1/files/presigned-url"), { json: body }) + .json>(); + + if (!response.success) { + throw new ApiError(getApiResponseMessage(response), { + code: response.code, + response, + }); + } + + return filePresignedUrlResultSchema.parse(response.result); + } catch (error) { + throw await toApiError(error); + } +}; + +// presigned URL로 파일 업로드 +export const uploadFileToPresignedUrl = async ({ + file, + presignedUrl, + contentType, +}: { + file: File; + presignedUrl: string; + contentType: string; +}) => { + const response = await fetch(presignedUrl, { + body: file, + headers: { "Content-Type": contentType }, + method: "PUT", + }); + + if (!response.ok) { + throw new ApiError("파일 업로드에 실패했습니다", { status: response.status }); + } +}; diff --git a/src/shared/api/fileTypes.ts b/src/shared/api/fileTypes.ts new file mode 100644 index 0000000..429e679 --- /dev/null +++ b/src/shared/api/fileTypes.ts @@ -0,0 +1,13 @@ +import { z } from "zod"; + +export const filePresignedUrlResultSchema = z.object({ + key: z.string(), + presignedUrl: z.string(), +}); + +export type FilePresignedUrlResult = z.infer; + +export type PostFilePresignedUrlBody = { + target: Target; + contentType: string; +}; diff --git a/src/shared/lib/hooks/useUploadedFiles.ts b/src/shared/lib/hooks/useUploadedFiles.ts index 5b3dd3e..b4c9944 100644 --- a/src/shared/lib/hooks/useUploadedFiles.ts +++ b/src/shared/lib/hooks/useUploadedFiles.ts @@ -3,9 +3,13 @@ import { useEffect, useRef, useState } from "react"; import { formatFileSize } from "@/shared/lib/utils/file"; import { UploadedFile } from "@/shared/types/file"; +type UploadFile = (file: File) => Promise; + export const useUploadedFiles = ( externalFiles?: UploadedFile[], setExternalFiles?: (files: UploadedFile[]) => void, + uploadFile?: UploadFile, + onUploadError?: (file: File) => void, ) => { const [localFiles, setLocalFiles] = useState([]); const externalFilesRef = useRef(externalFiles); @@ -34,8 +38,23 @@ export const useUploadedFiles = ( setFiles(prev => [...prev, ...newEntries]); - // 임시 업로드 시뮬레이션 (실제 API 연동 시 교체) newEntries.forEach(entry => { + if (uploadFile) { + uploadFile(entry.file) + .then(key => { + setFiles(prev => + prev.map(f => (f.id === entry.id ? { ...f, isUploading: false, key } : f)), + ); + }) + .catch(() => { + setFiles(prev => prev.filter(f => f.id !== entry.id)); + onUploadError?.(entry.file); + }); + + return; + } + + // 임시 업로드 시뮬레이션 (실제 API 연동 시 교체) setTimeout(() => { setFiles(prev => prev.map(f => (f.id === entry.id ? { ...f, isUploading: false } : f))); }, 2000); diff --git a/src/shared/types/file.ts b/src/shared/types/file.ts index 68bcdb8..543f3d4 100644 --- a/src/shared/types/file.ts +++ b/src/shared/types/file.ts @@ -4,4 +4,5 @@ export interface UploadedFile { fileName: string; fileSize: string; isUploading: boolean; + key?: string; } diff --git a/src/widgets/instructor/write/ui/AttachFileSection.tsx b/src/widgets/instructor/write/ui/AttachFileSection.tsx index 3595c8e..c358734 100644 --- a/src/widgets/instructor/write/ui/AttachFileSection.tsx +++ b/src/widgets/instructor/write/ui/AttachFileSection.tsx @@ -2,7 +2,11 @@ import { useState } from "react"; -import { useWriteFormStore } from "@/features/instructor/write"; +import { + COMMISSION_FILE_TARGET, + uploadCommissionFile, + useWriteFormStore, +} from "@/features/instructor/write"; import { useUploadedFiles } from "@/shared/lib/hooks/useUploadedFiles"; import { isAllowedFileType, MAX_FILE_SIZE_BYTES } from "@/shared/lib/utils/file"; import FileDragAndDrop from "@/shared/ui/FileDragAndDrop"; @@ -15,12 +19,14 @@ const MAX_FILE_COUNT = 5; const AttachFileSection = () => { const { materialFiles, setMaterialFiles, materialDescription, setMaterialDescription } = useWriteFormStore(); + const [isInvalidFileModalOpen, setIsInvalidFileModalOpen] = useState(false); + const [isFileCountExceededModalOpen, setIsFileCountExceededModalOpen] = useState(false); const { uploadedFiles, handleFilesAdded, handleRemove } = useUploadedFiles( materialFiles, setMaterialFiles, + file => uploadCommissionFile(file, COMMISSION_FILE_TARGET.MATERIAL), + () => setIsInvalidFileModalOpen(true), ); - const [isInvalidFileModalOpen, setIsInvalidFileModalOpen] = useState(false); - const [isFileCountExceededModalOpen, setIsFileCountExceededModalOpen] = useState(false); const handleValidatedFilesAdded = (files: File[]) => { const validFiles = files.filter( diff --git a/src/widgets/instructor/write/ui/ReferenceSection.tsx b/src/widgets/instructor/write/ui/ReferenceSection.tsx index 1d565e8..3c35d48 100644 --- a/src/widgets/instructor/write/ui/ReferenceSection.tsx +++ b/src/widgets/instructor/write/ui/ReferenceSection.tsx @@ -2,7 +2,11 @@ import { useState } from "react"; -import { useWriteFormStore } from "@/features/instructor/write"; +import { + COMMISSION_FILE_TARGET, + uploadCommissionFile, + useWriteFormStore, +} from "@/features/instructor/write"; import { useUploadedFiles } from "@/shared/lib/hooks/useUploadedFiles"; import { isAllowedFileType, MAX_FILE_SIZE_BYTES } from "@/shared/lib/utils/file"; import FileDragAndDrop from "@/shared/ui/FileDragAndDrop"; @@ -15,12 +19,14 @@ const MAX_FILE_COUNT = 3; const ReferenceSection = () => { const { referenceFiles, setReferenceFiles, referenceDescription, setReferenceDescription } = useWriteFormStore(); + const [isInvalidFileModalOpen, setIsInvalidFileModalOpen] = useState(false); + const [isFileCountExceededModalOpen, setIsFileCountExceededModalOpen] = useState(false); const { uploadedFiles, handleFilesAdded, handleRemove } = useUploadedFiles( referenceFiles, setReferenceFiles, + file => uploadCommissionFile(file, COMMISSION_FILE_TARGET.REFERENCE), + () => setIsInvalidFileModalOpen(true), ); - const [isInvalidFileModalOpen, setIsInvalidFileModalOpen] = useState(false); - const [isFileCountExceededModalOpen, setIsFileCountExceededModalOpen] = useState(false); const handleValidatedFilesAdded = (files: File[]) => { const validFiles = files.filter( From 03251ae402ec431ca8f2c964e8f4a6be03d5d94e Mon Sep 17 00:00:00 2001 From: YuminPark Date: Wed, 1 Jul 2026 18:54:04 +0900 Subject: [PATCH 07/13] =?UTF-8?q?#47=20[FEAT]=20=EC=83=88=20=EC=99=B8?= =?UTF-8?q?=EC=A3=BC=20=EC=83=9D=EC=84=B1=20API=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/instructor/write/api/write.ts | 27 ++++++++- src/features/instructor/write/index.ts | 2 +- src/features/instructor/write/model/write.ts | 29 +++++++--- .../instructor/write/model/writeFormStore.ts | 57 +++++++++++++------ .../write/ui/PaymentModal/PaymentModal.tsx | 27 ++++++++- .../write/ui/PaymentModal/Step1.tsx | 24 ++++++-- 6 files changed, 131 insertions(+), 35 deletions(-) diff --git a/src/features/instructor/write/api/write.ts b/src/features/instructor/write/api/write.ts index 9c5680f..9bf8129 100644 --- a/src/features/instructor/write/api/write.ts +++ b/src/features/instructor/write/api/write.ts @@ -1,6 +1,13 @@ import type { GetPlansResult, Plan } from "@/features/instructor/write/api/writeTypes"; import type { CommissionFileTarget } from "@/features/instructor/write/config/write"; -import { api, createApiPath } from "@/shared/api/client"; +import type { WriteOrderRequest } from "@/features/instructor/write/model/write"; +import { + api, + ApiError, + createApiPath, + getApiResponseMessage, + toApiError, +} from "@/shared/api/client"; import type { ApiResponse } from "@/shared/api/commonType"; import { postFilePresignedUrl, uploadFileToPresignedUrl } from "@/shared/api/file"; @@ -13,6 +20,24 @@ export const getPlans = async (): Promise => { return response.result?.plans ?? []; }; +// 외주 생성(결제) 요청 +export const postCommission = async (body: WriteOrderRequest): Promise => { + try { + const response = await api + .post(createApiPath("/api/v1/instructors/commissions"), { json: body }) + .json>(); + + if (!response.success) { + throw new ApiError(getApiResponseMessage(response), { + code: response.code, + response, + }); + } + } catch (error) { + throw await toApiError(error); + } +}; + // 외주 첨부 파일 업로드 (presigned URL 발급 후 업로드, 반환된 key를 외주 작성 요청에 사용) export const uploadCommissionFile = async (file: File, target: CommissionFileTarget) => { const contentType = file.type || "image/png"; diff --git a/src/features/instructor/write/index.ts b/src/features/instructor/write/index.ts index a8976fd..c1ad8e9 100644 --- a/src/features/instructor/write/index.ts +++ b/src/features/instructor/write/index.ts @@ -1,4 +1,4 @@ -export { getPlans, uploadCommissionFile } from "./api/write"; +export { getPlans, postCommission, uploadCommissionFile } from "./api/write"; export type { Plan } from "./api/writeTypes"; export * from "./config/write"; export type { RgbaColor } from "./lib/color"; diff --git a/src/features/instructor/write/model/write.ts b/src/features/instructor/write/model/write.ts index a4346fb..f0d64ee 100644 --- a/src/features/instructor/write/model/write.ts +++ b/src/features/instructor/write/model/write.ts @@ -6,7 +6,7 @@ export interface WriteOrderColorRequest { } export interface WriteOrderDesignInfoRequest { - size: string; + pageSize: string; concepts: string[]; additionalConcept?: string; colorSelectionMode: "DESIGNER_DELEGATED" | "USER_SELECTED"; @@ -18,6 +18,21 @@ export interface WriteOrderPageRequest { description: string | null; } +export interface WriteOrderTextbookRequest { + textbookName: string; + instructorName: string; + subject: string; + requiredPages: WriteOrderPageRequest[]; +} + +export type CommissionFileKind = "MATERIAL" | "REFERENCE"; + +export interface WriteOrderFileRequest { + fileKind: CommissionFileKind; + keys: string[]; + description?: string; +} + export interface WriteOrderDateRequest { firstDraftDeadline: string; finalDeadline: string; @@ -32,13 +47,9 @@ export interface WriteOrderTermRequest { export interface WriteOrderRequest { category: string; designInfo: WriteOrderDesignInfoRequest; - textbookName: string; - instructorName: string; - subject: string; - requiredPages: WriteOrderPageRequest[]; - materialDescription?: string; - referenceDescription?: string; + textbook: WriteOrderTextbookRequest; + files?: WriteOrderFileRequest[]; plan: string; - dates: WriteOrderDateRequest[]; - term: WriteOrderTermRequest[]; + date: WriteOrderDateRequest; + term: WriteOrderTermRequest; } diff --git a/src/features/instructor/write/model/writeFormStore.ts b/src/features/instructor/write/model/writeFormStore.ts index 7114b9d..8675e91 100644 --- a/src/features/instructor/write/model/writeFormStore.ts +++ b/src/features/instructor/write/model/writeFormStore.ts @@ -69,6 +69,8 @@ interface WriteFormState { setFinalDate: (value: Date | null) => void; setIsTermsAgreed: (value: boolean) => void; + // 현재 상태를 외주 생성 요청 포맷으로 변환 + getOrderRequest: () => WriteOrderRequest; // API 제출 후 호출 → 스토어 초기화 + sessionStorage 키 삭제 clearAfterSubmit: () => void; } @@ -111,38 +113,57 @@ const buildOrderRequest = (state: WriteFormState): WriteOrderRequest => { if (result.length > 0) mappedColors = result; } + const files: WriteOrderRequest["files"] = []; + const materialKeys = state.materialFiles.map(f => f.key).filter((key): key is string => !!key); + if (materialKeys.length > 0) { + files.push({ + fileKind: "MATERIAL", + keys: materialKeys, + ...(state.materialDescription ? { description: state.materialDescription } : {}), + }); + } + const referenceKeys = state.referenceFiles.map(f => f.key).filter((key): key is string => !!key); + if (referenceKeys.length > 0) { + files.push({ + fileKind: "REFERENCE", + keys: referenceKeys, + ...(state.referenceDescription ? { description: state.referenceDescription } : {}), + }); + } + return { category: state.selectedCategory ? CATEGORY_API_MAP[state.selectedCategory.item] : "", designInfo: { - size: state.selectedSize ? SIZE_API_MAP[state.selectedSize] : "", + pageSize: state.selectedSize ? SIZE_API_MAP[state.selectedSize] : "", concepts: state.selectedKeywords.map(k => KEYWORD_API_MAP[k]), ...(state.additionalConcept ? { additionalConcept: state.additionalConcept } : {}), colorSelectionMode: state.colorMode === "designer" ? "DESIGNER_DELEGATED" : "USER_SELECTED", ...(mappedColors ? { colors: mappedColors } : {}), }, - textbookName: state.basicInfo.교재명, - instructorName: state.basicInfo.강사명, - subject: state.basicInfo.과목명, - requiredPages: state.selectedPages.map(page => ({ - pageType: PAGE_API_MAP[page], - description: state.pageDescriptions[page] || null, - })), - ...(state.materialDescription ? { materialDescription: state.materialDescription } : {}), - ...(state.referenceDescription ? { referenceDescription: state.referenceDescription } : {}), + textbook: { + textbookName: state.basicInfo.교재명, + instructorName: state.basicInfo.강사명, + subject: state.basicInfo.과목명, + requiredPages: state.selectedPages.map(page => ({ + pageType: PAGE_API_MAP[page], + description: state.pageDescriptions[page] || null, + })), + }, + ...(files.length > 0 ? { files } : {}), plan: state.selectedPlan?.code ?? "", - dates: [ - { - firstDraftDeadline: state.firstDate ? toApiDate(state.firstDate) : "", - finalDeadline: state.finalDate ? toApiDate(state.finalDate) : "", - }, - ], - term: [{ type: "SETTLEMENT", version: "V1.0", isAgreed: state.isTermsAgreed }], + date: { + firstDraftDeadline: state.firstDate ? toApiDate(state.firstDate) : "", + finalDeadline: state.finalDate ? toApiDate(state.finalDate) : "", + }, + term: { type: "SETTLEMENT", version: "V1.0", isAgreed: state.isTermsAgreed }, }; }; -export const useWriteFormStore = create()(set => ({ +export const useWriteFormStore = create()((set, get) => ({ ...initialState, + getOrderRequest: () => buildOrderRequest(get()), + setCurrentStep: value => set({ currentStep: value }), setSelectedCategory: value => set({ selectedCategory: value }), setSelectedSize: value => set({ selectedSize: value }), diff --git a/src/features/instructor/write/ui/PaymentModal/PaymentModal.tsx b/src/features/instructor/write/ui/PaymentModal/PaymentModal.tsx index f1905cf..d573980 100644 --- a/src/features/instructor/write/ui/PaymentModal/PaymentModal.tsx +++ b/src/features/instructor/write/ui/PaymentModal/PaymentModal.tsx @@ -2,14 +2,18 @@ import { useEffect, useState } from "react"; +import { postCommission } from "@/features/instructor/write/api/write"; import { useWriteFormStore } from "@/features/instructor/write/model/writeFormStore"; import Step1 from "@/features/instructor/write/ui/PaymentModal/Step1"; import Step2 from "@/features/instructor/write/ui/PaymentModal/Step2"; +import { getApiErrorMessage } from "@/shared/api/client"; import { CloseIcon } from "@/shared/assets/icons"; const PaymentModalContent = ({ onClose }: { onClose?: () => void }) => { const [step, setStep] = useState<1 | 2>(1); - const { basicInfo } = useWriteFormStore(); + const [isSubmitting, setIsSubmitting] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + const { basicInfo, getOrderRequest } = useWriteFormStore(); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -19,6 +23,21 @@ const PaymentModalContent = ({ onClose }: { onClose?: () => void }) => { return () => window.removeEventListener("keydown", handleKeyDown); }, [onClose]); + const handlePay = async () => { + if (isSubmitting) return; + + setIsSubmitting(true); + setErrorMessage(null); + try { + await postCommission(getOrderRequest()); + setStep(2); + } catch (error) { + setErrorMessage(await getApiErrorMessage(error)); + } finally { + setIsSubmitting(false); + } + }; + return (
void }) => {

- {step === 1 ? setStep(2)} /> : setStep(1)} />} + {step === 1 ? ( + + ) : ( + setStep(1)} /> + )}
); diff --git a/src/features/instructor/write/ui/PaymentModal/Step1.tsx b/src/features/instructor/write/ui/PaymentModal/Step1.tsx index 6c2591c..95894b0 100644 --- a/src/features/instructor/write/ui/PaymentModal/Step1.tsx +++ b/src/features/instructor/write/ui/PaymentModal/Step1.tsx @@ -109,11 +109,20 @@ const TermsSection = ({ Step1 ───────────────────────────────────────────── */ -const Step1 = ({ onNext, errorMessage }: { onNext: () => void; errorMessage?: string | null }) => { +const Step1 = ({ + onNext, + isSubmitting, + errorMessage, +}: { + onNext: () => void; + isSubmitting?: boolean; + errorMessage?: string | null; +}) => { const { selectedCategory, selectedSize, selectedKeywords, + additionalConcept, colorMode, colors, selectedPages, @@ -153,11 +162,16 @@ const Step1 = ({ onNext, errorMessage }: { onNext: () => void; errorMessage?: st ))} )} - {selectedKeywords.length > 0 && ( + {(selectedKeywords.length > 0 || additionalConcept.trim() !== "") && ( {selectedKeywords.map(k => ( ))} + {additionalConcept.trim() !== "" && ( +

+ {additionalConcept} +

+ )}
)} {colorMode === "designer" && } @@ -195,8 +209,10 @@ const Step1 = ({ onNext, errorMessage }: { onNext: () => void; errorMessage?: st className="absolute -top-4 left-1/2 w-fit shrink-0 -translate-x-1/2 -translate-y-full whitespace-nowrap" />
diff --git a/src/features/instructor/write/ui/PaymentModal/Step2.tsx b/src/features/instructor/write/ui/PaymentModal/Step2.tsx index 668e8d5..197bc98 100644 --- a/src/features/instructor/write/ui/PaymentModal/Step2.tsx +++ b/src/features/instructor/write/ui/PaymentModal/Step2.tsx @@ -1,14 +1,45 @@ "use client"; import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import { postNotifyDeposit } from "@/features/instructor/write/api/write"; import { useWriteFormStore } from "@/features/instructor/write/model/writeFormStore"; +import { getApiErrorMessage } from "@/shared/api/client"; import { ArrowLeftIcon, ExclamationMarkCircleIcon } from "@/shared/assets/icons"; import Button from "@/shared/ui/Button"; +import Toast from "@/shared/ui/Toast"; -const Step2 = ({ onBack }: { onBack: () => void }) => { +const Step2 = ({ onBack, commissionId }: { onBack: () => void; commissionId: number | null }) => { const router = useRouter(); const { selectedPlan } = useWriteFormStore(); + const [isSubmitting, setIsSubmitting] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + const [autoHide, setAutoHide] = useState(false); + const showError = !!errorMessage && !autoHide; + + useEffect(() => { + if (!showError) return; + const timeout = setTimeout(() => setAutoHide(true), 2500); + return () => clearTimeout(timeout); + }, [showError]); + + const handleComplete = async () => { + if (isSubmitting || commissionId == null) return; + + setIsSubmitting(true); + setAutoHide(false); + setErrorMessage(null); + try { + await postNotifyDeposit(commissionId); + router.push("/instructor"); + } catch (error) { + setErrorMessage(await getApiErrorMessage(error)); + } finally { + setIsSubmitting(false); + } + }; + return (
@@ -48,9 +79,20 @@ const Step2 = ({ onBack }: { onBack: () => void }) => {

이체를 완료하신 후, 결제 완료 버튼을 눌러주세요

- +
+ + +
); From d02ce41394e94967d05c7dfbf6a7d15bc2c5593b Mon Sep 17 00:00:00 2001 From: YuminPark Date: Wed, 1 Jul 2026 19:23:24 +0900 Subject: [PATCH 09/13] =?UTF-8?q?#47=20[MOD]=201=EC=B0=A8=20=EC=8B=9C?= =?UTF-8?q?=EC=95=88=20=EC=88=98=EB=A0=B9=EC=9D=BC=20=EC=B5=9C=EC=86=8C=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EA=B0=80=EB=8A=A5=EC=9D=BC=20=EC=98=A4?= =?UTF-8?q?=EB=8A=98=EB=A1=9C=EB=B6=80=ED=84=B0=2012=EC=9D=BC=20=EC=9D=B4?= =?UTF-8?q?=ED=9B=84=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/instructor/write/index.ts | 2 +- src/features/instructor/write/lib/date.ts | 17 ++++++++++++----- .../write/ui/DeadlineChooseSection.tsx | 12 +++++++----- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/features/instructor/write/index.ts b/src/features/instructor/write/index.ts index 66516fd..92a1b7f 100644 --- a/src/features/instructor/write/index.ts +++ b/src/features/instructor/write/index.ts @@ -2,7 +2,7 @@ export { getPlans, postCommission, postNotifyDeposit, uploadCommissionFile } fro export type { CreateCommissionResult, Plan } from "./api/writeTypes"; export * from "./config/write"; export type { RgbaColor } from "./lib/color"; -export { formatDate, getMinFinalDate, getYesterday } from "./lib/date"; +export { formatDate, getFirstAvailableDate, getMinFinalDate, getMinFirstDate } from "./lib/date"; export type { BasicInfo } from "./model/writeFormStore"; export { useWriteFormStore } from "./model/writeFormStore"; export { default as ColorChooseCard } from "./ui/ColorChooseCard"; diff --git a/src/features/instructor/write/lib/date.ts b/src/features/instructor/write/lib/date.ts index 73f069d..744d29b 100644 --- a/src/features/instructor/write/lib/date.ts +++ b/src/features/instructor/write/lib/date.ts @@ -5,11 +5,18 @@ export const formatDate = (date: Date) => { return `${year}년 ${month}월 ${day}일`; }; -export const getYesterday = () => { - const yesterday = new Date(); - yesterday.setDate(yesterday.getDate() - 1); - yesterday.setHours(0, 0, 0, 0); - return yesterday; +export const getFirstAvailableDate = () => { + const date = new Date(); + date.setDate(date.getDate() + 12); + date.setHours(0, 0, 0, 0); + return date; +}; + +export const getMinFirstDate = () => { + const date = new Date(); + date.setDate(date.getDate() + 11); + date.setHours(0, 0, 0, 0); + return date; }; export const getMinFinalDate = (date: Date) => { diff --git a/src/widgets/instructor/write/ui/DeadlineChooseSection.tsx b/src/widgets/instructor/write/ui/DeadlineChooseSection.tsx index a77a2c2..c8c501e 100644 --- a/src/widgets/instructor/write/ui/DeadlineChooseSection.tsx +++ b/src/widgets/instructor/write/ui/DeadlineChooseSection.tsx @@ -4,8 +4,9 @@ import { useEffect, useRef, useState } from "react"; import { formatDate, + getFirstAvailableDate, getMinFinalDate, - getYesterday, + getMinFirstDate, useWriteFormStore, } from "@/features/instructor/write"; import DateDropdownBox from "@/shared/ui/dropdown/DateDropdownBox"; @@ -15,7 +16,8 @@ const DeadlineChooseSection = () => { const { firstDate, setFirstDate, finalDate, setFinalDate } = useWriteFormStore(); const [openMenu, setOpenMenu] = useState<"first" | "final" | null>(null); - const yesterday = getYesterday(); + const minFirstDate = getMinFirstDate(); + const firstAvailableDate = getFirstAvailableDate(); const containerRef = useRef(null); @@ -61,9 +63,9 @@ const DeadlineChooseSection = () => {
)} From 3e653d6d78504579a13e5036510fa273e31986f4 Mon Sep 17 00:00:00 2001 From: YuminPark Date: Thu, 2 Jul 2026 01:42:52 +0900 Subject: [PATCH 10/13] =?UTF-8?q?#47=20[CHORE]=20=ED=85=8D=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=B0=8F=20placeholder=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/widgets/instructor/write/ui/DesignConceptSection.tsx | 2 +- .../instructor/write/ui/NecessaryPageChooseSection.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/widgets/instructor/write/ui/DesignConceptSection.tsx b/src/widgets/instructor/write/ui/DesignConceptSection.tsx index 4e00c68..28448ce 100644 --- a/src/widgets/instructor/write/ui/DesignConceptSection.tsx +++ b/src/widgets/instructor/write/ui/DesignConceptSection.tsx @@ -86,7 +86,7 @@ const DesignConceptSection = () => { >
setAdditionalConcept(e.target.value)} /> diff --git a/src/widgets/instructor/write/ui/NecessaryPageChooseSection.tsx b/src/widgets/instructor/write/ui/NecessaryPageChooseSection.tsx index 50108ef..ebf735d 100644 --- a/src/widgets/instructor/write/ui/NecessaryPageChooseSection.tsx +++ b/src/widgets/instructor/write/ui/NecessaryPageChooseSection.tsx @@ -65,7 +65,9 @@ const NecessaryPageChooseSection = () => { ))}
) : ( -

상단에서 필수페이지를 우선 선택해주세요

+

+ 상단에서 외주 맡길 페이지를 우선 선택해주세요 +

)} ); From b66a82a3cad363e4d91c4da4c8b8e2ed26480bf0 Mon Sep 17 00:00:00 2001 From: YuminPark Date: Thu, 2 Jul 2026 01:48:54 +0900 Subject: [PATCH 11/13] =?UTF-8?q?#47=20[FIX]=20=EC=99=B8=EC=A3=BC=20?= =?UTF-8?q?=EB=82=B4=EC=97=AD=20=EB=B1=83=EC=A7=80=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EB=AF=B8?= =?UTF-8?q?=EA=B2=B0=EC=A0=9C=20=EA=B8=88=EC=95=A1=20=ED=94=8C=EB=9E=9C?= =?UTF-8?q?=EB=B3=84=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/instructor/my/api/myTypes.ts | 10 +++++++++- src/features/instructor/my/index.ts | 2 +- .../instructor/my/ui/CommissionsHistoryRow.tsx | 3 ++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/features/instructor/my/api/myTypes.ts b/src/features/instructor/my/api/myTypes.ts index 4095d87..66b18db 100644 --- a/src/features/instructor/my/api/myTypes.ts +++ b/src/features/instructor/my/api/myTypes.ts @@ -1,3 +1,5 @@ +import type { BadgeVariant } from "@/shared/ui/Badge"; + export type MyInfoStats = { totalCommissionCount: number; ongoingCommissionCount: number; @@ -25,6 +27,12 @@ export const PLAN_DISPLAY_MAP: Record = { MAX: "맥스", }; -export const CATEGORY_BADGE_MAP: Record = { +export const CATEGORY_BADGE_MAP: Record = { FLYER_TEXTBOOK_COVER_INNER: "교재", }; + +export const PLAN_PAID_AMOUNT_MAP: Record = { + BASIC: 400000, + PLUS: 450000, + MAX: 480000, +}; diff --git a/src/features/instructor/my/index.ts b/src/features/instructor/my/index.ts index f0269e9..bf305be 100644 --- a/src/features/instructor/my/index.ts +++ b/src/features/instructor/my/index.ts @@ -1,4 +1,4 @@ export type { CommissionHistoryItem, MyInfo } from "./api/myTypes"; -export { CATEGORY_BADGE_MAP, PLAN_DISPLAY_MAP } from "./api/myTypes"; +export { CATEGORY_BADGE_MAP, PLAN_DISPLAY_MAP, PLAN_PAID_AMOUNT_MAP } from "./api/myTypes"; export { default as CommissionsHeader } from "./ui/CommissionsHeader"; export { default as CommissionsHistoryRow } from "./ui/CommissionsHistoryRow"; diff --git a/src/features/instructor/my/ui/CommissionsHistoryRow.tsx b/src/features/instructor/my/ui/CommissionsHistoryRow.tsx index 2bf60fe..f124fe5 100644 --- a/src/features/instructor/my/ui/CommissionsHistoryRow.tsx +++ b/src/features/instructor/my/ui/CommissionsHistoryRow.tsx @@ -2,6 +2,7 @@ import { CATEGORY_BADGE_MAP, CommissionHistoryItem, PLAN_DISPLAY_MAP, + PLAN_PAID_AMOUNT_MAP, } from "@/features/instructor/my"; import { ArrowRightIcon } from "@/shared/assets/icons"; import Badge from "@/shared/ui/Badge"; @@ -22,7 +23,7 @@ const CommissionsHistoryRow = ({ item }: { item: CommissionHistoryItem }) => {

{createdAt.replaceAll("-", ".")}

{PLAN_DISPLAY_MAP[plan]}

- {paidAmount != null ? `${paidAmount.toLocaleString()}원` : "-"} + {(paidAmount ?? PLAN_PAID_AMOUNT_MAP[plan]).toLocaleString()}원

From c19a0c10cb53847688435657f7d2ae06e8b52888 Mon Sep 17 00:00:00 2001 From: YuminPark Date: Thu, 2 Jul 2026 01:55:06 +0900 Subject: [PATCH 12/13] =?UTF-8?q?#47=20[MOD]=20=EA=B2=B0=EC=A0=9C=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EC=BB=A8=EC=85=89=20=ED=91=9C=EC=8B=9C=20?= =?UTF-8?q?=EC=8B=9C=20Chip=20=EC=9E=88=EC=9C=BC=EB=A9=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=88=A8=EA=B9=80?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/instructor/write/ui/PaymentModal/Step1.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/instructor/write/ui/PaymentModal/Step1.tsx b/src/features/instructor/write/ui/PaymentModal/Step1.tsx index 95894b0..af957b6 100644 --- a/src/features/instructor/write/ui/PaymentModal/Step1.tsx +++ b/src/features/instructor/write/ui/PaymentModal/Step1.tsx @@ -167,7 +167,7 @@ const Step1 = ({ {selectedKeywords.map(k => ( ))} - {additionalConcept.trim() !== "" && ( + {selectedKeywords.length === 0 && additionalConcept.trim() !== "" && (

{additionalConcept}

From 6d1c6313eb9b7c9ba541143dbf4d5eaadfd1ca55 Mon Sep 17 00:00:00 2001 From: YuminPark Date: Thu, 2 Jul 2026 02:23:08 +0900 Subject: [PATCH 13/13] =?UTF-8?q?#47=20[FIX]=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=A0=95=EC=A0=81=20=ED=94=84=EB=A6=AC?= =?UTF-8?q?=EB=A0=8C=EB=8D=94=EB=A7=81=EC=9C=BC=EB=A1=9C=20=EC=9D=B8?= =?UTF-8?q?=ED=95=9C=20=EB=B9=8C=EB=93=9C=20=EC=97=90=EB=9F=AC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/instructor/(withSidebar)/my/page.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/instructor/(withSidebar)/my/page.tsx b/src/app/instructor/(withSidebar)/my/page.tsx index a8df343..1ff2da3 100644 --- a/src/app/instructor/(withSidebar)/my/page.tsx +++ b/src/app/instructor/(withSidebar)/my/page.tsx @@ -1,6 +1,8 @@ import { getMyInfo } from "@/features/instructor/my/api/my"; import { CommissionsHistorySection, MyInfoSection } from "@/widgets/instructor/my"; +export const dynamic = "force-dynamic"; + const page = async () => { const myInfo = await getMyInfo();