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
11 changes: 5 additions & 6 deletions src/app/instructor/(withSidebar)/my/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { cookies } from "next/headers";

import { getMyInfo } from "@/features/instructor/my/api/my";
import { CommissionsHistorySection, MyInfoSection } from "@/widgets/instructor/my";

export const dynamic = "force-dynamic";

const page = async () => {
const cookieStore = await cookies();
const name = cookieStore.get("userName")?.value ?? "";
const profileImageUrl = cookieStore.get("userProfileImageUrl")?.value;
const myInfo = await getMyInfo();

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 name={name} profileImageUrl={profileImageUrl} />
{myInfo != null && <MyInfoSection {...myInfo} />}
<CommissionsHistorySection />
</div>
</div>
Expand Down
10 changes: 10 additions & 0 deletions src/features/instructor/my/api/my.ts
Original file line number Diff line number Diff line change
@@ -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<MyInfo | null> => {
const response = await serverApi.get("/api/v1/instructors/me").json<ApiResponse<MyInfo>>();

return response.result;
};
38 changes: 38 additions & 0 deletions src/features/instructor/my/api/myTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { BadgeVariant } from "@/shared/ui/Badge";

export type MyInfoStats = {
totalCommissionCount: number;
ongoingCommissionCount: number;
};

export type MyInfo = {
name: string;
profileImageUrl: string;
stats: MyInfoStats;
};

export type CommissionHistoryItem = {
commissionId: number;
category: string;
title: string;
createdAt: string;
plan: "BASIC" | "PLUS" | "MAX";
paidAmount: number | null;
status: string;
};

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 PLAN_PAID_AMOUNT_MAP: Record<CommissionHistoryItem["plan"], number> = {
BASIC: 400000,
PLUS: 450000,
MAX: 480000,
};
4 changes: 2 additions & 2 deletions src/features/instructor/my/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
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, PLAN_PAID_AMOUNT_MAP } from "./api/myTypes";
export { default as CommissionsHeader } from "./ui/CommissionsHeader";
export { default as CommissionsHistoryRow } from "./ui/CommissionsHistoryRow";
134 changes: 0 additions & 134 deletions src/features/instructor/my/model/myMock.ts

This file was deleted.

9 changes: 6 additions & 3 deletions src/features/instructor/my/ui/CommissionsHistoryRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import {
CATEGORY_BADGE_MAP,
CommissionHistoryItem,
PLAN_DISPLAY_MAP,
} from "@/features/instructor/my/model/myMock";
PLAN_PAID_AMOUNT_MAP,
} 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 (
<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">
Expand All @@ -21,7 +22,9 @@ const CommissionsHistoryRow = ({ item }: { item: CommissionHistoryItem }) => {
<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>
<p className="text-gray-90 text-heading3-m w-25">
{(paidAmount ?? PLAN_PAID_AMOUNT_MAP[plan]).toLocaleString()}원
</p>
</div>
</div>
);
Expand Down
65 changes: 63 additions & 2 deletions src/features/instructor/write/api/write.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
import type { GetPlansResult, Plan } from "@/features/instructor/write/api/writeTypes";
import { api, createApiPath } from "@/shared/api/client";
import type {
CreateCommissionResult,
GetPlansResult,
Plan,
} from "@/features/instructor/write/api/writeTypes";
import type { CommissionFileTarget } from "@/features/instructor/write/config/write";
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";

// 플랜 조회
export const getPlans = async (): Promise<Plan[]> => {
Expand All @@ -10,3 +23,51 @@ export const getPlans = async (): Promise<Plan[]> => {

return response.result?.plans ?? [];
};

// 외주 생성(결제) 요청
export const postCommission = async (body: WriteOrderRequest): Promise<CreateCommissionResult> => {
try {
const response = await api
.post(createApiPath("/api/v1/instructors/commissions"), { json: body })
.json<ApiResponse<CreateCommissionResult>>();

if (!response.success || !response.result) {
throw new ApiError(getApiResponseMessage(response), {
code: response.code,
response,
});
}

return response.result;
} catch (error) {
throw await toApiError(error);
}
};

// 입금 통보
export const postNotifyDeposit = async (commissionId: number): Promise<void> => {
try {
const response = await api
.post(createApiPath(`/api/v1/instructors/commissions/${commissionId}/notify-deposit`))
.json<ApiResponse<unknown>>();

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";
const { key, presignedUrl } = await postFilePresignedUrl({ target, contentType });

await uploadFileToPresignedUrl({ file, presignedUrl, contentType });
Comment on lines +66 to +70

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

MIME 타입 fallback을 이미지로 고정하지 마세요.

File.type이 비어 있는 PDF/문서도 image/png으로 presigned URL을 발급·업로드하게 되어 서버 메타데이터나 검증이 실제 파일과 달라질 수 있습니다. 알 수 없는 타입은 일반 바이너리로 처리하는 편이 안전합니다.

🐛 제안 수정
 export const uploadCommissionFile = async (file: File, target: CommissionFileTarget) => {
-  const contentType = file.type || "image/png";
+  const contentType = file.type || "application/octet-stream";
   const { key, presignedUrl } = await postFilePresignedUrl({ target, contentType });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const uploadCommissionFile = async (file: File, target: CommissionFileTarget) => {
const contentType = file.type || "image/png";
const { key, presignedUrl } = await postFilePresignedUrl({ target, contentType });
await uploadFileToPresignedUrl({ file, presignedUrl, contentType });
export const uploadCommissionFile = async (file: File, target: CommissionFileTarget) => {
const contentType = file.type || "application/octet-stream";
const { key, presignedUrl } = await postFilePresignedUrl({ target, contentType });
await uploadFileToPresignedUrl({ file, presignedUrl, contentType });
🤖 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/write/api/write.ts` around lines 66 - 70, The MIME
fallback in uploadCommissionFile is incorrectly hardcoded to an image type,
which can mislabel non-image files when File.type is empty. Update the
contentType handling in uploadCommissionFile so unknown types are treated as a
generic binary type instead of image/png, while still passing the resolved
contentType through postFilePresignedUrl and uploadFileToPresignedUrl.


return key;
};
12 changes: 12 additions & 0 deletions src/features/instructor/write/api/writeTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,15 @@ export type Plan = {
export type GetPlansResult = {
plans: Plan[];
};

export type CreateCommissionResult = {
commissionId: number;
title: string;
category: string;
status: string;
applicationDeadline: string;
firstDraftDeadline: string;
finalDeadline: string;
maxRevision: number;
createdAt: string;
};
8 changes: 8 additions & 0 deletions src/features/instructor/write/config/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: "강사명을 작성해주세요" },
Expand Down
6 changes: 3 additions & 3 deletions src/features/instructor/write/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export { getPlans } from "./api/write";
export type { Plan } from "./api/writeTypes";
export { getPlans, postCommission, postNotifyDeposit, uploadCommissionFile } from "./api/write";
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";
Expand Down
17 changes: 12 additions & 5 deletions src/features/instructor/write/lib/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Loading
Loading