Skip to content

[FEAT/REFACTOR] 강사 대시보드 API 연동 및 코드 구조 개선#39

Merged
waldls merged 11 commits into
mainfrom
feature/#37-instructor-api
Jun 24, 2026
Merged

[FEAT/REFACTOR] 강사 대시보드 API 연동 및 코드 구조 개선#39
waldls merged 11 commits into
mainfrom
feature/#37-instructor-api

Conversation

@waldls

@waldls waldls commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

📢 PR 유형

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

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

📌 관련 이슈번호


✅ Key Changes

  • 강사 대시보드 API 연동

    • 시안 제출 현황, 매칭 중인 외주, 수정 중인 외주 API 연동
    • 새 외주 작성 시 플랜 조회 API 연동
    • 데이터 없을 때 빈 상태 처리 추가
  • API 파일 구조 리팩토링

    • API 파일을 도메인별로 통합 (auth.ts, write.ts 등)
    • 타입을 별도 파일로 분리 (homeTypes.ts, writeTypes.ts, signupTypes.ts, commonType.ts)
    • Mock 파일 명칭 명확화 (*.ts*Mock.ts)
  • 공통 기능 개선

    • 헤더 로고 클릭 시 역할(강사/학생)에 따라 적절한 홈으로 이동
    • 유저 프로필 이미지 및 이름을 쿠키 기반으로 표시
    • 외부 구글폼 링크 연결

📸 스크린샷 or 실행영상

37.mp4

🎸 기타 사항 or 추가 코멘트

  • 금일 회의록에 진행상황 적어두었습니당
  • 파일명, 파일 위치, 함수명 등등 수정했으니 앞으로 컨벤션 맞춰서 통일시키면 좋을 것 같습니다!

Summary by CodeRabbit

Release Notes

  • New Features

    • User profile images now display in header and instructor profile sections
    • Plans dynamically loaded from server instead of hardcoded
    • Dashboard sections (drafts, commissions, revisions) now fetch live data from server
  • Improvements

    • Plan selection refactored with simplified labeling system
    • Added external links for support (inappropriate draft reporting, revision increase requests)
    • Improved user data persistence across sessions
  • Chores

    • Consolidated API type definitions and imports
    • Updated mock data references for consistency
    • Enhanced server-side API client configuration

@waldls waldls self-assigned this Jun 24, 2026
@waldls waldls added ♻️ Refactor 코드 리팩토링 📢 API 서버 API 연동 ❄️ 유민 labels Jun 24, 2026
@github-actions

Copy link
Copy Markdown

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8bf6522c-487e-4542-aebf-b28cfe32113a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR connects login-response identity fields (name, profileImageUrl) to browser cookies and surfaces them in the header and instructor pages. Three instructor home dashboard sections are refactored from static mock arrays to live API calls. The write-plan model shifts from a Korean-keyed string union to server-supplied Plan objects fetched from a new endpoint. Signup and auth API exports are renamed to a post* convention, and a server-side ky client is introduced.

Changes

Instructor Dashboard API Integration & Auth Identity

Layer / File(s) Summary
Shared API infra: server client, auth cookie identity
src/shared/api/server.ts, src/shared/api/client.ts, src/shared/lib/auth/client.ts, src/features/login/api/login.ts, src/features/auth/api/auth.ts
Adds serverApi ky client that injects Authorization from the accessToken cookie. Extends setClientAuth/clearClientAuth to persist and clear userName/userProfileImageUrl cookies; adds getter helpers. Migrates ApiResponse import to commonType across API files.
Login & signup: persist identity, rename API exports
src/features/login/model/useLoginForm.ts, src/features/signup/api/signup.ts, src/features/signup/api/signupTypes.ts, src/features/signup/index.ts, src/features/signup/model/useSignupStep2Form.ts, src/features/auth/..., src/widgets/signup/ui/SignupFunnel.tsx
useLoginForm passes name and profileImageUrl to setClientAuth on success. Signup API functions renamed to post*; Zod schemas and request body types extracted to new signupTypes.ts. useSignupStep2Form, SignupFunnel, and auth barrel updated to use renamed functions.
Header: profile image, dynamic logo href
src/shared/ui/Header.tsx, next.config.ts
Header reads profileImageUrl cookie, computes logoHref via usePathname, wraps logo in a Link, and conditionally renders next/image avatar. next.config.ts adds S3 remote image pattern.
Instructor pages: identity from cookies
src/app/instructor/page.tsx, src/app/instructor/my/page.tsx
Both pages become async, read userName and userProfileImageUrl cookies, and forward values to their respective widgets (greeting h1, MyInfoSection props).
MyInfoSection: props-driven with avatar
src/widgets/instructor/my/ui/MyInfoSection.tsx, src/features/instructor/my/model/myMock.ts, src/features/instructor/my/index.ts, src/features/instructor/my/ui/CommissionsHistoryRow.tsx
MyInfoSection accepts name/profileImageUrl props and conditionally renders an avatar image. MyInfo mock type and data added to myMock; feature barrel and CommissionsHistoryRow re-pointed to myMock. Widget-level MyInfo/myInfoData removed.
Instructor home: API types, fetch functions, live dashboard sections
src/features/instructor/home/api/homeTypes.ts, src/features/instructor/home/api/home.ts, src/features/instructor/home/index.ts, src/features/instructor/home/ui/..., src/widgets/instructor/home/ui/...
Adds homeTypes.ts with item shapes and result wrappers. Adds getDraftSubmissions, getMatchingCommissions, getRevisions. Dashboard sections (DraftSubmissionStatusSection, MatchingCommissionsSection, ModifyingCommissionsSection) switch to useEffect fetches with empty-state rendering. Row components updated for renamed fields (applicationDeadline, hasUpdated, isViewable).
Write: Plan model from API, PlanChooseSection dynamic fetch
src/features/instructor/write/api/writeTypes.ts, src/features/instructor/write/api/write.ts, src/features/instructor/write/config/write.ts, src/features/instructor/write/model/writeFormStore.ts, src/features/instructor/write/ui/..., src/widgets/instructor/write/ui/PlanChooseSection.tsx
Adds Plan/GetPlansResult types and getPlans API. PlanType becomes BASIC|PLUS|MAX; PLAN_MAP/PLAN_API_MAP replaced with PLAN_LABEL_MAP. Store, PlanChooseCard, Step1, Step2 use Plan object fields directly. PlanChooseSection fetches plans on mount.
Mock ID alignment and Google Form links
src/features/instructor/choose/..., src/widgets/instructor/revision/model/revisionMock.ts, src/app/instructor/revision/.../page.tsx, src/widgets/instructor/choose/ui/DraftCheckSection.tsx, src/widgets/instructor/revision/ui/RevisionCategorySection.tsx
Mock commissionId values aligned to 4/6 across choose and revision mocks. Revision page imports from revisionMock. Two <button> controls replaced with <a> anchors linking to Google Forms.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(173, 216, 230, 0.5)
    note over useLoginForm,Cookies: Login & identity persistence
    useLoginForm->>loginApi: login(credentials)
    loginApi-->>useLoginForm: { accessToken, role, name, profileImageUrl }
    useLoginForm->>setClientAuth: { accessToken, role, name, profileImageUrl }
    setClientAuth->>Cookies: set accessToken, role, userName, userProfileImageUrl
  end

  rect rgba(144, 238, 144, 0.5)
    note over InstructorPage,MyInfoSection: Server-side rendering with cookies
    InstructorPage->>Cookies: cookies().get("userName")
    InstructorPage->>Cookies: cookies().get("userProfileImageUrl")
    Cookies-->>InstructorPage: name, profileImageUrl
    InstructorPage->>MyInfoSection: props { name, profileImageUrl }
    MyInfoSection-->>Browser: renders avatar or fallback icon
  end

  rect rgba(255, 228, 196, 0.5)
    note over Header,getClientProfileImageUrl: Client-side header sync
    Header->>getClientProfileImageUrl: read userProfileImageUrl cookie
    getClientProfileImageUrl-->>Header: profileImageUrl | null
    Header-->>Browser: renders next/image avatar or ProfileCircleIcon
  end
Loading
sequenceDiagram
  rect rgba(221, 160, 221, 0.5)
    note over DashboardSection,BackendAPI: Home dashboard live data fetch
    DashboardSection->>getDraftSubmissions: useEffect on mount
    getDraftSubmissions->>BackendAPI: GET /dashboard/draft-submissions
    BackendAPI-->>getDraftSubmissions: ApiResponse<GetDraftSubmissionsResult>
    getDraftSubmissions-->>DashboardSection: DraftSubmissionItem[]
    DashboardSection->>usePagination: paginate items
    usePagination-->>DashboardSection: pageItems → render rows or empty state
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Ditda-Official/Ditda-Frontend#36: This PR directly renames and rewires the login/signup/logout API functions (logoutpostLogout, signupDesignerpostSignupDesigner, etc.) introduced in PR #36, updating all dependent hooks and pages.
  • Ditda-Official/Ditda-Frontend#35: Both PRs modify src/shared/ui/Header.tsx to drive navigation behavior from cookie-based auth state; this PR extends that work with profile image rendering and dynamic logo href.
  • Ditda-Official/Ditda-Frontend#28: This PR extends MyInfoSection and src/app/instructor/my/page.tsx introduced in PR #28 by converting them to accept props and read identity from cookies.

Poem

🐇 Hop hop, the cookies now hold my name,
Profile pics and plans — no more mock game!
The dashboard calls the server with glee,
postLogout, postSignup — naming is key!
Empty states wave where static arrays stood,
This bunny ships APIs as a good dev should! ✨

🚥 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 The title '[FEAT/REFACTOR] 강사 대시보드 API 연동 및 코드 구조 개선' clearly and specifically describes the main changes: integrating instructor dashboard APIs and improving code structure, which accurately reflects the substantial refactoring and feature work throughout the changeset.
Linked Issues check ✅ Passed The PR successfully implements all completed objectives from issue #37: connecting personal information from login responses with cookies, integrating three instructor dashboard APIs with empty state handling, connecting external Google Form links, and implementing plan query functionality.
Out of Scope Changes check ✅ Passed All changes are aligned with the linked issue #37 objectives. The refactoring of API file structure, type organization, and mock file renaming all support the main feature implementation without introducing unrelated functionality.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/#37-instructor-api

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.

@github-actions

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

Caution

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

⚠️ Outside diff range comments (1)
src/widgets/instructor/write/ui/PlanChooseSection.tsx (1)

16-37: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle plan fetch failure/empty states explicitly instead of rendering a silent blank area.

Line 17 only logs errors; then Line 29 onward renders nothing when plans is empty. That leaves users without guidance or recovery (retry), and can block plan selection.

Suggested fix
 const PlanChooseSection = () => {
   const { selectedPlan, setSelectedPlan } = useWriteFormStore();
   const [plans, setPlans] = useState<Plan[]>([]);
+  const [status, setStatus] = useState<"loading" | "success" | "error">("loading");

   useEffect(() => {
-    getPlans().then(setPlans).catch(console.error);
+    let active = true;
+    getPlans()
+      .then(data => {
+        if (!active) return;
+        setPlans(data);
+        setStatus("success");
+      })
+      .catch(() => {
+        if (!active) return;
+        setStatus("error");
+      });
+    return () => {
+      active = false;
+    };
   }, []);
@@
-      <div className="flex flex-row gap-3">
-        {plans.map(plan => (
-          <PlanChooseCard
-            key={plan.code}
-            plan={plan}
-            isSelected={selectedPlan?.code === plan.code}
-            onClick={() => setSelectedPlan(plan)}
-          />
-        ))}
-      </div>
+      <div className="flex flex-row gap-3">
+        {status === "error" ? (
+          <p className="text-body2-m text-gray-70">플랜 정보를 불러오지 못했어요. 잠시 후 다시 시도해주세요.</p>
+        ) : plans.length === 0 ? (
+          <p className="text-body2-m text-gray-70">현재 선택 가능한 플랜이 없습니다.</p>
+        ) : (
+          plans.map(plan => (
+            <PlanChooseCard
+              key={plan.code}
+              plan={plan}
+              isSelected={selectedPlan?.code === plan.code}
+              onClick={() => setSelectedPlan(plan)}
+            />
+          ))
+        )}
+      </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/write/ui/PlanChooseSection.tsx` around lines 16 - 37,
The component currently only logs fetch errors with console.error and silently
renders nothing when the plans array is empty, providing no feedback or recovery
option to users. Add state variables to track loading and error states in the
useEffect hook alongside getPlans, then modify the rendering logic to show a
loading indicator while fetching, display an error message with a retry button
when getPlans fails, and show a helpful empty state message if plans are
successfully loaded but empty. This ensures users have clear guidance instead of
a blank area when plans fail to load or are unavailable.
🧹 Nitpick comments (2)
src/widgets/signup/ui/SignupFunnel.tsx (1)

92-104: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Persist signup identity fields when setting auth cookies.

Line 103 stores only token/role, but this flow already has result.name and result.profileImageUrl. Persisting them here keeps header/account UI consistent immediately after signup.

Proposed fix
-    setClientAuth({ accessToken: result.accessToken, role: userRole });
+    setClientAuth({
+      accessToken: result.accessToken,
+      role: userRole,
+      name: result.name,
+      profileImageUrl: result.profileImageUrl,
+    });
🤖 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/signup/ui/SignupFunnel.tsx` around lines 92 - 104, The
setClientAuth call in the signup completion flow only persists the accessToken
and role from the signup response, but the result object also contains name and
profileImageUrl fields that should be persisted. Update the setClientAuth
invocation to include result.name and result.profileImageUrl in addition to the
existing accessToken and role parameters so that user identity information is
available immediately in the header and account UI after signup without
requiring additional API calls.
src/features/instructor/write/api/write.ts (1)

7-11: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Differentiate invalid API payloads from true empty plan lists.

Line 11 currently treats missing result.plans as [], which can hide backend/auth/contract failures as a normal empty state. Please fail fast on invalid shape and only return empty when the API explicitly returns an empty array.

Proposed change
 export const getPlans = async (): Promise<Plan[]> => {
   const response = await api
     .get(createApiPath("/api/v1/instructors/commissions/plans"))
     .json<ApiResponse<GetPlansResult>>();
 
-  return response.result?.plans ?? [];
+  const plans = response.result?.plans;
+  if (!Array.isArray(plans)) {
+    throw new Error("Invalid plans response");
+  }
+  return plans;
 };
🤖 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 7 - 11, The current
implementation in the API call that fetches commission plans uses the nullish
coalescing operator to return an empty array when result.plans is undefined,
which masks invalid API responses from being caught. Instead of using
response.result?.plans ?? [], explicitly validate that response.result and
response.result.plans exist with their expected types. Check if result is
defined and if plans is an array before returning it, and throw an error or
handle the failure case if the API response doesn't have the expected shape.
Only return an empty array when plans is explicitly an empty array, not when
it's undefined or missing.
🤖 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/features/instructor/home/api/home.ts`:
- Around line 13-37: The three functions getDraftSubmissions,
getMatchingCommissions, and getRevisions all use the fallback operator to return
an empty array when response.result is undefined, which masks API failures where
success is false. Modify each function to explicitly check the success property
of the ApiResponse before accessing the result. If success is false, throw an
error or handle the failure explicitly instead of defaulting to an empty array,
so that backend errors are distinguishable from cases where data is genuinely
unavailable.

In `@src/features/instructor/write/ui/PlanChooseCard.tsx`:
- Line 13: The badgeVariant assignment in PlanChooseCard.tsx uses an unsafe cast
that assumes plan.designerCount will always produce a valid BadgeVariant, but
since designerCount is typed as a plain number with no constraints, it could
contain values outside the allowed set ("3인", "4인", "5인"). Replace the direct
cast with an explicit validation guard that checks whether the designerCount is
one of the valid values (3, 4, or 5) before creating the badge variant string,
and either skip rendering the Badge component or use a fallback variant if the
value is invalid. Alternatively, update the Plan type definition to constrain
designerCount to only the union of 3 | 4 | 5 at the schema level to ensure type
safety.

In `@src/features/signup/api/signup.ts`:
- Around line 91-95: The getPortfolioContentType function currently defaults to
"image/png" as a fallback for all non-PDF files with missing type information,
which incorrectly labels JPEG and WEBP files. Modify the fallback return
statement to use a safer MIME type such as "application/octet-stream" or
"image/jpeg" instead of hardcoding "image/png", or enhance the logic to detect
additional common image formats from the file extension before falling back to a
generic type.

In `@src/shared/ui/Header.tsx`:
- Around line 71-76: The logoHref calculation in the useMemo hook is deriving
the logo destination from the current pathname instead of the user's actual role
from authState. Replace the pathname-based conditions (the checks for
pathname.startsWith("/instructor") and pathname.startsWith("/designer")) with
checks against the user's role property from authState (likely authState.role).
Update the dependency array of useMemo to include the role value instead of
pathname so the destination is driven by the user's assigned role rather than
their current route location.
- Around line 96-103: The condition checking authState.profileImageUrl at line
96 using the `!= null` operator does not guard against empty strings, which can
be returned from getClientProfileImageUrl(). Replace the null check with a
condition that trims whitespace from authState.profileImageUrl and validates the
result is truthy before rendering the Image component. This ensures both null
values and empty or whitespace-only strings are excluded from being passed as
the src attribute to the Image component.

In `@src/widgets/instructor/home/ui/DraftSubmissionStatusSection.tsx`:
- Around line 19-21: Add error handling to the getDraftSubmissions() promise
chain in the useEffect hook by adding a catch handler to manage API failures.
Create separate state variables to track loading and error states distinct from
the items data. Update the conditional rendering logic that checks pageItems to
instead check these new loading and error states, ensuring that network failures
display an error message rather than being conflated with a valid empty data
state.

In `@src/widgets/instructor/home/ui/MatchingCommissionsSection.tsx`:
- Around line 19-21: The useEffect hook that calls getMatchingCommissions() is
missing error handling on the promise chain, which allows API failures to become
unhandled promise rejections. Add a .catch() handler after the .then(setItems)
call in the useEffect to properly handle API failures. In the catch handler, log
the error appropriately and optionally set an error state or default value for
the items state so the component has a defined state even when the API call
fails.
- Around line 45-48: The empty-state message in MatchingCommissionsSection.tsx
is displayed on first render before data has been fetched, causing a brief flash
of the "매칭 중인 외주가 없습니다" message. Add a loading or fetching state check alongside
the pageItems.length === 0 condition to ensure the empty-state is only rendered
after the initial data fetch has completed and there is genuinely no data. If a
loading state is not already available, introduce one to track whether the first
fetch is in progress, and conditionally render either a loading indicator or
nothing while that fetch is pending before showing the empty state.

In `@src/widgets/instructor/home/ui/ModifyingCommissionsSection.tsx`:
- Around line 19-21: The useEffect hook in ModifyingCommissionsSection component
lacks rejection handling for the getRevisions() promise, and the conditional
render logic around the items state doesn't distinguish between empty results
and failed API calls. Add a .catch() handler to the getRevisions() promise chain
to capture and store error state separately (e.g., setError), and then update
the render logic to check for this error state explicitly before displaying the
"진행중인 외주가 없습니다" message, so that API failures are handled and displayed
differently from genuinely empty data.

In `@src/widgets/instructor/my/ui/MyInfoSection.tsx`:
- Around line 17-24: The condition checking `profileImageUrl != null` in the
MyInfoSection component allows empty strings to pass through, which would render
the Image component with an invalid src attribute. Update the condition to
validate that profileImageUrl is not only non-null but also non-empty by
changing the check to use truthiness evaluation after trimming whitespace, such
as checking `profileImageUrl?.trim()` instead of just `profileImageUrl != null`.
This ensures that empty or whitespace-only strings are treated as invalid and
the Image component is not rendered with an invalid src.

---

Outside diff comments:
In `@src/widgets/instructor/write/ui/PlanChooseSection.tsx`:
- Around line 16-37: The component currently only logs fetch errors with
console.error and silently renders nothing when the plans array is empty,
providing no feedback or recovery option to users. Add state variables to track
loading and error states in the useEffect hook alongside getPlans, then modify
the rendering logic to show a loading indicator while fetching, display an error
message with a retry button when getPlans fails, and show a helpful empty state
message if plans are successfully loaded but empty. This ensures users have
clear guidance instead of a blank area when plans fail to load or are
unavailable.

---

Nitpick comments:
In `@src/features/instructor/write/api/write.ts`:
- Around line 7-11: The current implementation in the API call that fetches
commission plans uses the nullish coalescing operator to return an empty array
when result.plans is undefined, which masks invalid API responses from being
caught. Instead of using response.result?.plans ?? [], explicitly validate that
response.result and response.result.plans exist with their expected types. Check
if result is defined and if plans is an array before returning it, and throw an
error or handle the failure case if the API response doesn't have the expected
shape. Only return an empty array when plans is explicitly an empty array, not
when it's undefined or missing.

In `@src/widgets/signup/ui/SignupFunnel.tsx`:
- Around line 92-104: The setClientAuth call in the signup completion flow only
persists the accessToken and role from the signup response, but the result
object also contains name and profileImageUrl fields that should be persisted.
Update the setClientAuth invocation to include result.name and
result.profileImageUrl in addition to the existing accessToken and role
parameters so that user identity information is available immediately in the
header and account UI after signup without requiring additional API calls.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6e7ced99-0fd4-4315-adaa-da3fcea25385

📥 Commits

Reviewing files that changed from the base of the PR and between 1ab86e4 and 1be878a.

📒 Files selected for processing (48)
  • next.config.ts
  • src/app/instructor/my/page.tsx
  • src/app/instructor/page.tsx
  • src/app/instructor/revision/[commissionId]/page.tsx
  • src/features/auth/api/auth.ts
  • src/features/auth/index.ts
  • src/features/auth/model/useLogout.ts
  • src/features/instructor/choose/index.ts
  • src/features/instructor/choose/model/chooseMock.ts
  • src/features/instructor/home/api/home.ts
  • src/features/instructor/home/api/homeTypes.ts
  • src/features/instructor/home/index.ts
  • src/features/instructor/home/model/home.ts
  • src/features/instructor/home/ui/DraftSubmissionStatusRow.tsx
  • src/features/instructor/home/ui/MatchingCommissionsRow.tsx
  • src/features/instructor/home/ui/ModifyingCommissionsRow.tsx
  • src/features/instructor/my/index.ts
  • src/features/instructor/my/model/myMock.ts
  • src/features/instructor/my/ui/CommissionsHistoryRow.tsx
  • src/features/instructor/write/api/write.ts
  • src/features/instructor/write/api/writeTypes.ts
  • src/features/instructor/write/config/write.ts
  • src/features/instructor/write/index.ts
  • src/features/instructor/write/model/writeFormStore.ts
  • src/features/instructor/write/ui/PaymentModal/Step1.tsx
  • src/features/instructor/write/ui/PaymentModal/Step2.tsx
  • src/features/instructor/write/ui/PlanChooseCard.tsx
  • src/features/login/api/login.ts
  • src/features/login/model/useLoginForm.ts
  • src/features/signup/api/signup.ts
  • src/features/signup/api/signupTypes.ts
  • src/features/signup/index.ts
  • src/features/signup/model/useSignupStep2Form.ts
  • src/shared/api/client.ts
  • src/shared/api/commonType.ts
  • src/shared/api/server.ts
  • src/shared/lib/auth/client.ts
  • src/shared/ui/Header.tsx
  • src/widgets/instructor/choose/ui/DraftCheckSection.tsx
  • src/widgets/instructor/home/ui/DraftSubmissionStatusSection.tsx
  • src/widgets/instructor/home/ui/MatchingCommissionsSection.tsx
  • src/widgets/instructor/home/ui/ModifyingCommissionsSection.tsx
  • src/widgets/instructor/my/model/my.ts
  • src/widgets/instructor/my/ui/MyInfoSection.tsx
  • src/widgets/instructor/revision/model/revisionMock.ts
  • src/widgets/instructor/revision/ui/RevisionCategorySection.tsx
  • src/widgets/instructor/write/ui/PlanChooseSection.tsx
  • src/widgets/signup/ui/SignupFunnel.tsx
💤 Files with no reviewable changes (2)
  • src/features/instructor/home/model/home.ts
  • src/widgets/instructor/my/model/my.ts

Comment on lines +13 to +37
export const getDraftSubmissions = async (): Promise<DraftSubmissionItem[]> => {
const response = await api
.get(createApiPath("/api/v1/instructors/dashboards/draft-submissions"))
.json<ApiResponse<GetDraftSubmissionsResult>>();

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

// 매칭 중인 외주 조회
export const getMatchingCommissions = async (): Promise<MatchingItem[]> => {
const response = await api
.get(createApiPath("/api/v1/instructors/dashboards/matchings"))
.json<ApiResponse<GetMatchingCommissionsResult>>();

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

// 수정 중인 외주 조회
export const getRevisions = async (): Promise<ModifyingItem[]> => {
const response = await api
.get(createApiPath("/api/v1/instructors/dashboards/revisions"))
.json<ApiResponse<GetRevisionsResult>>();

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Handle success: false explicitly instead of returning empty lists.

Line 18, Line 27, and Line 36 currently collapse API failure payloads into [], so real backend errors become indistinguishable from “no data”.

Suggested fix
 export const getDraftSubmissions = async (): Promise<DraftSubmissionItem[]> => {
   const response = await api
     .get(createApiPath("/api/v1/instructors/dashboards/draft-submissions"))
     .json<ApiResponse<GetDraftSubmissionsResult>>();
+  if (!response.success) {
+    throw new Error(response.message);
+  }

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

 export const getMatchingCommissions = async (): Promise<MatchingItem[]> => {
   const response = await api
     .get(createApiPath("/api/v1/instructors/dashboards/matchings"))
     .json<ApiResponse<GetMatchingCommissionsResult>>();
+  if (!response.success) {
+    throw new Error(response.message);
+  }

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

 export const getRevisions = async (): Promise<ModifyingItem[]> => {
   const response = await api
     .get(createApiPath("/api/v1/instructors/dashboards/revisions"))
     .json<ApiResponse<GetRevisionsResult>>();
+  if (!response.success) {
+    throw new Error(response.message);
+  }

   return response.result?.commissions ?? [];
 };
🤖 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/home/api/home.ts` around lines 13 - 37, The three
functions getDraftSubmissions, getMatchingCommissions, and getRevisions all use
the fallback operator to return an empty array when response.result is
undefined, which masks API failures where success is false. Modify each function
to explicitly check the success property of the ApiResponse before accessing the
result. If success is false, throw an error or handle the failure explicitly
instead of defaulting to an empty array, so that backend errors are
distinguishable from cases where data is genuinely unavailable.

const PlanChooseCard = ({ plan, isSelected = false, onClick }: PlanChooseCardProps) => {
const { label, size, price, description } = PLAN_MAP[plan];
const label = PLAN_LABEL_MAP[plan.code];
const badgeVariant = `${plan.designerCount}인` as BadgeVariant;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -t f "PlanChooseCard.tsx" --type file

Repository: Ditda-Official/Ditda-Frontend

Length of output: 219


🏁 Script executed:

rg -t typescript -t tsx "BadgeVariant" -l

Repository: Ditda-Official/Ditda-Frontend

Length of output: 202


🏁 Script executed:

rg -t typescript -t tsx "type Plan" -A 5

Repository: Ditda-Official/Ditda-Frontend

Length of output: 202


🏁 Script executed:

rg "BadgeVariant" -A 3 -B 1

Repository: Ditda-Official/Ditda-Frontend

Length of output: 167


🏁 Script executed:

rg "type Plan" -A 10 --max-count 5

Repository: Ditda-Official/Ditda-Frontend

Length of output: 167


🏁 Script executed:

cat -n src/features/instructor/write/ui/PlanChooseCard.tsx | head -40

Repository: Ditda-Official/Ditda-Frontend

Length of output: 1853


🏁 Script executed:

find . -name "Badge*" -type f | head -10

Repository: Ditda-Official/Ditda-Frontend

Length of output: 193


🏁 Script executed:

cat -n src/features/instructor/write/api/writeTypes.ts | head -50

Repository: Ditda-Official/Ditda-Frontend

Length of output: 485


🏁 Script executed:

cat -n src/shared/ui/Badge.tsx | head -50

Repository: Ditda-Official/Ditda-Frontend

Length of output: 1980


🏁 Script executed:

rg "designerCount" -B 2 -A 2 | head -30

Repository: Ditda-Official/Ditda-Frontend

Length of output: 167


🏁 Script executed:

rg "getPlan\|getPlans\|plans.*api" -l | head -5

Repository: Ditda-Official/Ditda-Frontend

Length of output: 167


🏁 Script executed:

find . -name "*.ts" -o -name "*.tsx" | xargs grep -l "Plan" | grep -i api

Repository: Ditda-Official/Ditda-Frontend

Length of output: 262


🏁 Script executed:

rg "3인|4人|5人" -B 3 -A 3

Repository: Ditda-Official/Ditda-Frontend

Length of output: 167


🏁 Script executed:

rg "designerCount" --no-heading

Repository: Ditda-Official/Ditda-Frontend

Length of output: 167


🏁 Script executed:

cat -n src/features/instructor/write/api/write.ts

Repository: Ditda-Official/Ditda-Frontend

Length of output: 701


🏁 Script executed:

find . -name "write.ts" -path "*/config/*" | head -1

Repository: Ditda-Official/Ditda-Frontend

Length of output: 215


🏁 Script executed:

cat -n src/features/instructor/write/config/write.ts | head -60

Repository: Ditda-Official/Ditda-Frontend

Length of output: 1999


🏁 Script executed:

cat -n src/features/instructor/write/config/write.ts | tail -80

Repository: Ditda-Official/Ditda-Frontend

Length of output: 7909


🏁 Script executed:

rg "PLAN_LABEL_MAP" -B 5 -A 15

Repository: Ditda-Official/Ditda-Frontend

Length of output: 167


🏁 Script executed:

rg "PLAN_LABEL_MAP" -A 10

Repository: Ditda-Official/Ditda-Frontend

Length of output: 167


🏁 Script executed:

cat -n src/features/instructor/write/config/write.ts | grep -A 10 "PLAN_LABEL_MAP"

Repository: Ditda-Official/Ditda-Frontend

Length of output: 518


🏁 Script executed:

rg "designerCount.*=" -B 3 -A 3

Repository: Ditda-Official/Ditda-Frontend

Length of output: 167


🏁 Script executed:

find . -name "*.test.ts" -o -name "*.test.tsx" -o -name "*.spec.ts" -o -name "*.spec.tsx" | xargs grep -l "Plan\|designerCount" 2>/dev/null | head -3

Repository: Ditda-Official/Ditda-Frontend

Length of output: 167


🏁 Script executed:

rg "3인|4인|5인" --max-count 20

Repository: Ditda-Official/Ditda-Frontend

Length of output: 167


🏁 Script executed:

find . -name "*.ts" -o -name "*.tsx" | xargs grep -l "mock\|Mock\|stub\|Stub" | head -5

Repository: Ditda-Official/Ditda-Frontend

Length of output: 449


🏁 Script executed:

cat -n src/shared/ui/Badge.tsx | tail -20

Repository: Ditda-Official/Ditda-Frontend

Length of output: 772


🏁 Script executed:

cat -n src/features/instructor/choose/model/chooseMock.ts

Repository: Ditda-Official/Ditda-Frontend

Length of output: 4689


🏁 Script executed:

grep -r "designerCount" --include="*.ts" --include="*.tsx" | head -20

Repository: Ditda-Official/Ditda-Frontend

Length of output: 357


Avoid unsafe BadgeVariant casting from API data.

Line 13 force-casts \${plan.designerCount}인`toBadgeVariant. The Plantype definesdesignerCountas a plainnumberwith no constraints, butBadgeVariantonly allows"3인" | "4인" | "5인"(plus three unrelated variants). If the API returns a count outside 3–5, this creates an invalid badge variant and causes undefined access in thebadgeStyleMap, breaking rendering. Prefer an explicit mapping with a guard before rendering Badge`.

Suggested fix
+const DESIGNER_COUNT_BADGE_MAP: Record<number, BadgeVariant> = {
+  3: "3인",
+  4: "4인",
+  5: "5인",
+};
+
 const PlanChooseCard = ({ plan, isSelected = false, onClick }: PlanChooseCardProps) => {
   const label = PLAN_LABEL_MAP[plan.code];
-  const badgeVariant = `${plan.designerCount}인` as BadgeVariant;
+  const badgeVariant = DESIGNER_COUNT_BADGE_MAP[plan.designerCount];
   const formattedPrice = `${plan.price.toLocaleString("ko-KR")}원`;
@@
-            <Badge variant={badgeVariant} />
+            {badgeVariant && <Badge variant={badgeVariant} />}

Verify whether the backend API contract guarantees designerCount is only 3, 4, or 5; if yes, encode that constraint in the Plan schema.

🤖 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/ui/PlanChooseCard.tsx` at line 13, The
badgeVariant assignment in PlanChooseCard.tsx uses an unsafe cast that assumes
plan.designerCount will always produce a valid BadgeVariant, but since
designerCount is typed as a plain number with no constraints, it could contain
values outside the allowed set ("3인", "4인", "5인"). Replace the direct cast with
an explicit validation guard that checks whether the designerCount is one of the
valid values (3, 4, or 5) before creating the badge variant string, and either
skip rendering the Badge component or use a fallback variant if the value is
invalid. Alternatively, update the Plan type definition to constrain
designerCount to only the union of 3 | 4 | 5 at the schema level to ensure type
safety.

Comment on lines 91 to 95
const getPortfolioContentType = (file: File) => {
if (file.type.length > 0) return file.type;

return file.name.toLowerCase().endsWith(".pdf") ? "application/pdf" : "image/png";
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a safer MIME fallback for unknown file types.

Line 94 currently labels every non-PDF file with empty file.type as image/png, which can mislabel JPEG/WEBP uploads and cause incorrect object metadata or validation mismatches.

Proposed fix
 const getPortfolioContentType = (file: File) => {
   if (file.type.length > 0) return file.type;
-
-  return file.name.toLowerCase().endsWith(".pdf") ? "application/pdf" : "image/png";
+  const lower = file.name.toLowerCase();
+  if (lower.endsWith(".pdf")) return "application/pdf";
+  if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
+  if (lower.endsWith(".png")) return "image/png";
+  if (lower.endsWith(".webp")) return "image/webp";
+  return "application/octet-stream";
 };
📝 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 getPortfolioContentType = (file: File) => {
if (file.type.length > 0) return file.type;
return file.name.toLowerCase().endsWith(".pdf") ? "application/pdf" : "image/png";
};
const getPortfolioContentType = (file: File) => {
if (file.type.length > 0) return file.type;
const lower = file.name.toLowerCase();
if (lower.endsWith(".pdf")) return "application/pdf";
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
if (lower.endsWith(".png")) return "image/png";
if (lower.endsWith(".webp")) return "image/webp";
return "application/octet-stream";
};
🤖 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/signup/api/signup.ts` around lines 91 - 95, The
getPortfolioContentType function currently defaults to "image/png" as a fallback
for all non-PDF files with missing type information, which incorrectly labels
JPEG and WEBP files. Modify the fallback return statement to use a safer MIME
type such as "application/octet-stream" or "image/jpeg" instead of hardcoding
"image/png", or enhance the logic to detect additional common image formats from
the file extension before falling back to a generic type.

Comment thread src/shared/ui/Header.tsx
Comment on lines +71 to +76
const logoHref = useMemo(() => {
if (!authState.isLoggedIn) return "/";
if (pathname.startsWith("/instructor")) return "/instructor";
if (pathname.startsWith("/designer")) return "/designer";
return "/";
}, [authState.isLoggedIn, pathname]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Drive logo navigation from user role, not current path.

Line 73-Line 75 picks the destination from pathname, so logged-in users on non-/instructor//designer routes are sent to / instead of their role home.

Suggested fix
-  const pathname = usePathname();
-
   const logoHref = useMemo(() => {
-    if (!authState.isLoggedIn) return "/";
-    if (pathname.startsWith("/instructor")) return "/instructor";
-    if (pathname.startsWith("/designer")) return "/designer";
+    if (authState.role === "instructor") return "/instructor";
+    if (authState.role === "designer") return "/designer";
     return "/";
-  }, [authState.isLoggedIn, pathname]);
+  }, [authState.role]);
🤖 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/shared/ui/Header.tsx` around lines 71 - 76, The logoHref calculation in
the useMemo hook is deriving the logo destination from the current pathname
instead of the user's actual role from authState. Replace the pathname-based
conditions (the checks for pathname.startsWith("/instructor") and
pathname.startsWith("/designer")) with checks against the user's role property
from authState (likely authState.role). Update the dependency array of useMemo
to include the role value instead of pathname so the destination is driven by
the user's assigned role rather than their current route location.

Comment thread src/shared/ui/Header.tsx
Comment on lines +96 to +103
{authState.profileImageUrl != null ? (
<Image
src={authState.profileImageUrl}
alt="프로필"
width={32}
height={32}
className="size-8 rounded-full object-cover"
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify all current profile-image guards and empty-string sources.
rg -n 'profileImageUrl\s*!=\s*null|profileImageUrl:\s*""|getClientProfileImageUrl|setClientAuth' \
  src/shared/ui/Header.tsx \
  src/widgets/instructor/my/ui/MyInfoSection.tsx \
  src/features/instructor/my/model/myMock.ts \
  src/shared/lib/auth/client.ts

Repository: Ditda-Official/Ditda-Frontend

Length of output: 804


🏁 Script executed:

# Check the implementation of getClientProfileImageUrl and authState handling
cat -n src/shared/lib/auth/client.ts | sed -n '50,65p'

Repository: Ditda-Official/Ditda-Frontend

Length of output: 864


🏁 Script executed:

# Check the full context of Header.tsx around line 96
cat -n src/shared/ui/Header.tsx | sed -n '50,110p'

Repository: Ditda-Official/Ditda-Frontend

Length of output: 2735


🏁 Script executed:

# Check the auth state initialization and how profileImageUrl gets set
ast-grep outline src/shared/ui/Header.tsx --view expanded

Repository: Ditda-Official/Ditda-Frontend

Length of output: 519


🏁 Script executed:

# Check the mock data to understand if empty strings are intentional
cat -n src/features/instructor/my/model/myMock.ts | sed -n '30,45p'

Repository: Ditda-Official/Ditda-Frontend

Length of output: 567


Guard <Image> against empty-string URLs.

Line 96 uses != null, which treats "" (empty string) as valid. Since getClientProfileImageUrl() can return an empty string from cookies (as shown in mock data), this allows invalid src values to reach next/image at runtime.

The suggested fix using ?.trim() with a truthiness check correctly prevents both null and empty/whitespace-only strings from rendering the Image component.

🤖 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/shared/ui/Header.tsx` around lines 96 - 103, The condition checking
authState.profileImageUrl at line 96 using the `!= null` operator does not guard
against empty strings, which can be returned from getClientProfileImageUrl().
Replace the null check with a condition that trims whitespace from
authState.profileImageUrl and validates the result is truthy before rendering
the Image component. This ensures both null values and empty or whitespace-only
strings are excluded from being passed as the src attribute to the Image
component.

Comment on lines +19 to +21
useEffect(() => {
getDraftSubmissions().then(setItems);
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Differentiate loading/error from true empty data and catch fetch failures.

Line 20 has no rejection handling, and Line 44 treats all empty pageItems as a valid no-data state. Network/API failures will be shown as “제출된 시안이 없습니다”.

Suggested fix
 const DraftSubmissionStatusSection = () => {
   const [items, setItems] = useState<DraftSubmissionItem[]>([]);
+  const [isLoading, setIsLoading] = useState(true);
+  const [isError, setIsError] = useState(false);

   useEffect(() => {
-    getDraftSubmissions().then(setItems);
+    let isMounted = true;
+    (async () => {
+      try {
+        const data = await getDraftSubmissions();
+        if (isMounted) setItems(data);
+      } catch {
+        if (isMounted) setIsError(true);
+      } finally {
+        if (isMounted) setIsLoading(false);
+      }
+    })();
+    return () => {
+      isMounted = false;
+    };
   }, []);
@@
-        {pageItems.length === 0 ? (
+        {isError ? (
+          <div className="flex flex-1 items-center justify-center">
+            <span className="text-heading3-m text-gray-60">시안 현황을 불러오지 못했습니다</span>
+          </div>
+        ) : isLoading ? (
+          <div className="flex flex-1 items-center justify-center">
+            <span className="text-heading3-m text-gray-60">불러오는 중입니다</span>
+          </div>
+        ) : pageItems.length === 0 ? (
           <div className="flex flex-1 items-center justify-center">
             <span className="text-heading3-m text-gray-60">제출된 시안이 없습니다</span>
           </div>
         ) : (

Also applies to: 44-54

🤖 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/home/ui/DraftSubmissionStatusSection.tsx` around lines
19 - 21, Add error handling to the getDraftSubmissions() promise chain in the
useEffect hook by adding a catch handler to manage API failures. Create separate
state variables to track loading and error states distinct from the items data.
Update the conditional rendering logic that checks pageItems to instead check
these new loading and error states, ensuring that network failures display an
error message rather than being conflated with a valid empty data state.

Comment on lines +19 to +21
useEffect(() => {
getMatchingCommissions().then(setItems);
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle API failures in the effect to avoid unhandled rejections.

Line 20 calls the API without a rejection path, so network/API failures can surface as unhandled promise rejections and leave this section in an undefined state.

Suggested fix
 const MatchingCommissionsSection = () => {
   const [items, setItems] = useState<MatchingItem[]>([]);
+  const [isLoading, setIsLoading] = useState(true);

   useEffect(() => {
-    getMatchingCommissions().then(setItems);
+    let mounted = true;
+    getMatchingCommissions()
+      .then(data => {
+        if (mounted) setItems(data);
+      })
+      .catch(() => {
+        if (mounted) setItems([]);
+      })
+      .finally(() => {
+        if (mounted) setIsLoading(false);
+      });
+    return () => {
+      mounted = false;
+    };
   }, []);
🤖 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/home/ui/MatchingCommissionsSection.tsx` around lines
19 - 21, The useEffect hook that calls getMatchingCommissions() is missing error
handling on the promise chain, which allows API failures to become unhandled
promise rejections. Add a .catch() handler after the .then(setItems) call in the
useEffect to properly handle API failures. In the catch handler, log the error
appropriately and optionally set an error state or default value for the items
state so the component has a defined state even when the API call fails.

Comment on lines +45 to +48
{pageItems.length === 0 ? (
<div className="flex flex-1 items-center justify-center">
<span className="text-heading3-m text-gray-60">매칭 중인 외주가 없습니다</span>
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Avoid showing the empty-state before the first fetch resolves.

Lines 45-48 currently render “매칭 중인 외주가 없습니다” on the first paint (before data arrives), which can briefly show incorrect UI.

Suggested fix
-        {pageItems.length === 0 ? (
+        {isLoading ? (
+          <div className="flex flex-1 items-center justify-center">
+            <span className="text-heading3-m text-gray-60">불러오는 중...</span>
+          </div>
+        ) : pageItems.length === 0 ? (
           <div className="flex flex-1 items-center justify-center">
             <span className="text-heading3-m text-gray-60">매칭 중인 외주가 없습니다</span>
           </div>
📝 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
{pageItems.length === 0 ? (
<div className="flex flex-1 items-center justify-center">
<span className="text-heading3-m text-gray-60">매칭 중인 외주가 없습니다</span>
</div>
{isLoading ? (
<div className="flex flex-1 items-center justify-center">
<span className="text-heading3-m text-gray-60">불러오는 중...</span>
</div>
) : pageItems.length === 0 ? (
<div className="flex flex-1 items-center justify-center">
<span className="text-heading3-m text-gray-60">매칭 중인 외주가 없습니다</span>
</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/home/ui/MatchingCommissionsSection.tsx` around lines
45 - 48, The empty-state message in MatchingCommissionsSection.tsx is displayed
on first render before data has been fetched, causing a brief flash of the "매칭
중인 외주가 없습니다" message. Add a loading or fetching state check alongside the
pageItems.length === 0 condition to ensure the empty-state is only rendered
after the initial data fetch has completed and there is genuinely no data. If a
loading state is not already available, introduce one to track whether the first
fetch is in progress, and conditionally render either a loading indicator or
nothing while that fetch is pending before showing the empty state.

Comment on lines +19 to +21
useEffect(() => {
getRevisions().then(setItems);
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add explicit error/loading handling for revisions fetch.

Line 20 lacks rejection handling, and Line 45 maps any empty render state to “진행중인 외주가 없습니다”, including failed API calls.

Suggested fix
 const ModifyingCommissionsSection = () => {
   const [items, setItems] = useState<ModifyingItem[]>([]);
+  const [isLoading, setIsLoading] = useState(true);
+  const [isError, setIsError] = useState(false);

   useEffect(() => {
-    getRevisions().then(setItems);
+    let isMounted = true;
+    (async () => {
+      try {
+        const data = await getRevisions();
+        if (isMounted) setItems(data);
+      } catch {
+        if (isMounted) setIsError(true);
+      } finally {
+        if (isMounted) setIsLoading(false);
+      }
+    })();
+    return () => {
+      isMounted = false;
+    };
   }, []);
@@
-        {pageItems.length === 0 ? (
+        {isError ? (
+          <div className="flex flex-1 items-center justify-center">
+            <span className="text-heading3-m text-gray-60">외주 목록을 불러오지 못했습니다</span>
+          </div>
+        ) : isLoading ? (
+          <div className="flex flex-1 items-center justify-center">
+            <span className="text-heading3-m text-gray-60">불러오는 중입니다</span>
+          </div>
+        ) : pageItems.length === 0 ? (
           <div className="flex flex-1 items-center justify-center">
             <span className="text-heading3-m text-gray-60">진행중인 외주가 없습니다</span>
           </div>
         ) : (

Also applies to: 45-55

🤖 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/home/ui/ModifyingCommissionsSection.tsx` around lines
19 - 21, The useEffect hook in ModifyingCommissionsSection component lacks
rejection handling for the getRevisions() promise, and the conditional render
logic around the items state doesn't distinguish between empty results and
failed API calls. Add a .catch() handler to the getRevisions() promise chain to
capture and store error state separately (e.g., setError), and then update the
render logic to check for this error state explicitly before displaying the
"진행중인 외주가 없습니다" message, so that API failures are handled and displayed
differently from genuinely empty data.

Comment on lines +17 to +24
{profileImageUrl != null ? (
<Image
src={profileImageUrl}
alt="프로필"
width={32}
height={32}
className="size-8 rounded-full object-cover"
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major

Check profileImageUrl is non-empty before rendering <Image>.

The condition profileImageUrl != null allows empty strings to pass through, causing <Image> to render with an invalid src attribute.

Consider normalizing and validating the URL: profileImageUrl?.trim() and updating the condition to check for truthiness rather than null-only checks.

🤖 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/MyInfoSection.tsx` around lines 17 - 24, The
condition checking `profileImageUrl != null` in the MyInfoSection component
allows empty strings to pass through, which would render the Image component
with an invalid src attribute. Update the condition to validate that
profileImageUrl is not only non-null but also non-empty by changing the check to
use truthiness evaluation after trimming whitespace, such as checking
`profileImageUrl?.trim()` instead of just `profileImageUrl != null`. This
ensures that empty or whitespace-only strings are treated as invalid and the
Image component is not rendered with an invalid src.

@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 5c514d2 into main Jun 24, 2026
2 checks passed
@waldls waldls deleted the feature/#37-instructor-api branch June 24, 2026 05:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

♻️ Refactor 코드 리팩토링 ❄️ 유민 📢 API 서버 API 연동

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 내 정보 연결 및 강사 대시보드 API 연동

2 participants