[FEAT/REFACTOR] 강사 대시보드 API 연동 및 코드 구조 개선#39
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThe PR connects login-response identity fields ( ChangesInstructor Dashboard API Integration & Auth Identity
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 winHandle 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
plansis 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 winPersist signup identity fields when setting auth cookies.
Line 103 stores only token/role, but this flow already has
result.nameandresult.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 winDifferentiate invalid API payloads from true empty plan lists.
Line 11 currently treats missing
result.plansas[], 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
📒 Files selected for processing (48)
next.config.tssrc/app/instructor/my/page.tsxsrc/app/instructor/page.tsxsrc/app/instructor/revision/[commissionId]/page.tsxsrc/features/auth/api/auth.tssrc/features/auth/index.tssrc/features/auth/model/useLogout.tssrc/features/instructor/choose/index.tssrc/features/instructor/choose/model/chooseMock.tssrc/features/instructor/home/api/home.tssrc/features/instructor/home/api/homeTypes.tssrc/features/instructor/home/index.tssrc/features/instructor/home/model/home.tssrc/features/instructor/home/ui/DraftSubmissionStatusRow.tsxsrc/features/instructor/home/ui/MatchingCommissionsRow.tsxsrc/features/instructor/home/ui/ModifyingCommissionsRow.tsxsrc/features/instructor/my/index.tssrc/features/instructor/my/model/myMock.tssrc/features/instructor/my/ui/CommissionsHistoryRow.tsxsrc/features/instructor/write/api/write.tssrc/features/instructor/write/api/writeTypes.tssrc/features/instructor/write/config/write.tssrc/features/instructor/write/index.tssrc/features/instructor/write/model/writeFormStore.tssrc/features/instructor/write/ui/PaymentModal/Step1.tsxsrc/features/instructor/write/ui/PaymentModal/Step2.tsxsrc/features/instructor/write/ui/PlanChooseCard.tsxsrc/features/login/api/login.tssrc/features/login/model/useLoginForm.tssrc/features/signup/api/signup.tssrc/features/signup/api/signupTypes.tssrc/features/signup/index.tssrc/features/signup/model/useSignupStep2Form.tssrc/shared/api/client.tssrc/shared/api/commonType.tssrc/shared/api/server.tssrc/shared/lib/auth/client.tssrc/shared/ui/Header.tsxsrc/widgets/instructor/choose/ui/DraftCheckSection.tsxsrc/widgets/instructor/home/ui/DraftSubmissionStatusSection.tsxsrc/widgets/instructor/home/ui/MatchingCommissionsSection.tsxsrc/widgets/instructor/home/ui/ModifyingCommissionsSection.tsxsrc/widgets/instructor/my/model/my.tssrc/widgets/instructor/my/ui/MyInfoSection.tsxsrc/widgets/instructor/revision/model/revisionMock.tssrc/widgets/instructor/revision/ui/RevisionCategorySection.tsxsrc/widgets/instructor/write/ui/PlanChooseSection.tsxsrc/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
| 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 ?? []; | ||
| }; |
There was a problem hiding this comment.
🎯 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; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
fd -t f "PlanChooseCard.tsx" --type fileRepository: Ditda-Official/Ditda-Frontend
Length of output: 219
🏁 Script executed:
rg -t typescript -t tsx "BadgeVariant" -lRepository: Ditda-Official/Ditda-Frontend
Length of output: 202
🏁 Script executed:
rg -t typescript -t tsx "type Plan" -A 5Repository: Ditda-Official/Ditda-Frontend
Length of output: 202
🏁 Script executed:
rg "BadgeVariant" -A 3 -B 1Repository: Ditda-Official/Ditda-Frontend
Length of output: 167
🏁 Script executed:
rg "type Plan" -A 10 --max-count 5Repository: Ditda-Official/Ditda-Frontend
Length of output: 167
🏁 Script executed:
cat -n src/features/instructor/write/ui/PlanChooseCard.tsx | head -40Repository: Ditda-Official/Ditda-Frontend
Length of output: 1853
🏁 Script executed:
find . -name "Badge*" -type f | head -10Repository: Ditda-Official/Ditda-Frontend
Length of output: 193
🏁 Script executed:
cat -n src/features/instructor/write/api/writeTypes.ts | head -50Repository: Ditda-Official/Ditda-Frontend
Length of output: 485
🏁 Script executed:
cat -n src/shared/ui/Badge.tsx | head -50Repository: Ditda-Official/Ditda-Frontend
Length of output: 1980
🏁 Script executed:
rg "designerCount" -B 2 -A 2 | head -30Repository: Ditda-Official/Ditda-Frontend
Length of output: 167
🏁 Script executed:
rg "getPlan\|getPlans\|plans.*api" -l | head -5Repository: Ditda-Official/Ditda-Frontend
Length of output: 167
🏁 Script executed:
find . -name "*.ts" -o -name "*.tsx" | xargs grep -l "Plan" | grep -i apiRepository: Ditda-Official/Ditda-Frontend
Length of output: 262
🏁 Script executed:
rg "3인|4人|5人" -B 3 -A 3Repository: Ditda-Official/Ditda-Frontend
Length of output: 167
🏁 Script executed:
rg "designerCount" --no-headingRepository: Ditda-Official/Ditda-Frontend
Length of output: 167
🏁 Script executed:
cat -n src/features/instructor/write/api/write.tsRepository: Ditda-Official/Ditda-Frontend
Length of output: 701
🏁 Script executed:
find . -name "write.ts" -path "*/config/*" | head -1Repository: Ditda-Official/Ditda-Frontend
Length of output: 215
🏁 Script executed:
cat -n src/features/instructor/write/config/write.ts | head -60Repository: Ditda-Official/Ditda-Frontend
Length of output: 1999
🏁 Script executed:
cat -n src/features/instructor/write/config/write.ts | tail -80Repository: Ditda-Official/Ditda-Frontend
Length of output: 7909
🏁 Script executed:
rg "PLAN_LABEL_MAP" -B 5 -A 15Repository: Ditda-Official/Ditda-Frontend
Length of output: 167
🏁 Script executed:
rg "PLAN_LABEL_MAP" -A 10Repository: 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 3Repository: 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 -3Repository: Ditda-Official/Ditda-Frontend
Length of output: 167
🏁 Script executed:
rg "3인|4인|5인" --max-count 20Repository: Ditda-Official/Ditda-Frontend
Length of output: 167
🏁 Script executed:
find . -name "*.ts" -o -name "*.tsx" | xargs grep -l "mock\|Mock\|stub\|Stub" | head -5Repository: Ditda-Official/Ditda-Frontend
Length of output: 449
🏁 Script executed:
cat -n src/shared/ui/Badge.tsx | tail -20Repository: Ditda-Official/Ditda-Frontend
Length of output: 772
🏁 Script executed:
cat -n src/features/instructor/choose/model/chooseMock.tsRepository: Ditda-Official/Ditda-Frontend
Length of output: 4689
🏁 Script executed:
grep -r "designerCount" --include="*.ts" --include="*.tsx" | head -20Repository: 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.
| const getPortfolioContentType = (file: File) => { | ||
| if (file.type.length > 0) return file.type; | ||
|
|
||
| return file.name.toLowerCase().endsWith(".pdf") ? "application/pdf" : "image/png"; | ||
| }; |
There was a problem hiding this comment.
🎯 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.
| 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.
| const logoHref = useMemo(() => { | ||
| if (!authState.isLoggedIn) return "/"; | ||
| if (pathname.startsWith("/instructor")) return "/instructor"; | ||
| if (pathname.startsWith("/designer")) return "/designer"; | ||
| return "/"; | ||
| }, [authState.isLoggedIn, pathname]); |
There was a problem hiding this comment.
🎯 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.
| {authState.profileImageUrl != null ? ( | ||
| <Image | ||
| src={authState.profileImageUrl} | ||
| alt="프로필" | ||
| width={32} | ||
| height={32} | ||
| className="size-8 rounded-full object-cover" | ||
| /> |
There was a problem hiding this comment.
🩺 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.tsRepository: 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 expandedRepository: 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.
| useEffect(() => { | ||
| getDraftSubmissions().then(setItems); | ||
| }, []); |
There was a problem hiding this comment.
🩺 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.
| useEffect(() => { | ||
| getMatchingCommissions().then(setItems); | ||
| }, []); |
There was a problem hiding this comment.
🩺 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.
| {pageItems.length === 0 ? ( | ||
| <div className="flex flex-1 items-center justify-center"> | ||
| <span className="text-heading3-m text-gray-60">매칭 중인 외주가 없습니다</span> | ||
| </div> |
There was a problem hiding this comment.
🎯 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.
| {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.
| useEffect(() => { | ||
| getRevisions().then(setItems); | ||
| }, []); |
There was a problem hiding this comment.
🩺 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.
| {profileImageUrl != null ? ( | ||
| <Image | ||
| src={profileImageUrl} | ||
| alt="프로필" | ||
| width={32} | ||
| height={32} | ||
| className="size-8 rounded-full object-cover" | ||
| /> |
There was a problem hiding this comment.
🩺 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.
📢 PR 유형
어떤 변경 사항이 있었나요?
📌 관련 이슈번호
✅ Key Changes
강사 대시보드 API 연동
API 파일 구조 리팩토링
auth.ts,write.ts등)homeTypes.ts,writeTypes.ts,signupTypes.ts,commonType.ts)*.ts→*Mock.ts)공통 기능 개선
📸 스크린샷 or 실행영상
37.mp4
🎸 기타 사항 or 추가 코멘트
Summary by CodeRabbit
Release Notes
New Features
Improvements
Chores