[FEAT] 온보딩 페이지 API 연동 #36
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:
📝 WalkthroughWalkthrough
Changes온보딩 API 연동 (로그인·회원가입·로그아웃)
Sequence Diagram(s)sequenceDiagram
participant User as 사용자
participant LoginPage as LoginPage
participant useLoginForm
participant loginAPI as login()
participant APIServer as API 서버
participant AuthCookie as 클라이언트 쿠키
User->>LoginPage: 아이디/비밀번호 입력 후 제출
LoginPage->>useLoginForm: onSubmit(formValues)
useLoginForm->>loginAPI: login({ username, password })
loginAPI->>APIServer: POST /api/v1/auth/login
APIServer-->>loginAPI: { success, result: { accessToken, userType, ... } }
loginAPI-->>useLoginForm: LoginResult
useLoginForm->>AuthCookie: setClientAuth({ accessToken, role })
useLoginForm->>LoginPage: router.push(getClientUserHomePath(role))
sequenceDiagram
participant SignupFunnel
participant TermsProfileStep
participant AccountStep
participant DesignerAdditionalStep
participant signupDesigner
participant AuthCookie as 클라이언트 쿠키
SignupFunnel->>TermsProfileStep: terms, initialData
TermsProfileStep-->>SignupFunnel: handleProfileNext(SignupProfileData)
SignupFunnel->>AccountStep: role, profileData, initialData
AccountStep-->>SignupFunnel: handleAccountNext(SignupAccountData)
SignupFunnel->>DesignerAdditionalStep: initialData
DesignerAdditionalStep-->>SignupFunnel: handleDesignerSubmit(SignupDesignerAdditionalData)
SignupFunnel->>signupDesigner: { profile, account, additional }
signupDesigner-->>SignupFunnel: DesignerSignupResult
SignupFunnel->>AuthCookie: setClientAuth({ accessToken, role })
SignupFunnel->>SignupFunnel: router.push(getClientUserHomePath(role))
Estimated code review effort🎯 5 (Critical) | ⏱️ ~100 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 5 | ❌ 1❌ Failed checks (1 warning)
✅ 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: 7
🧹 Nitpick comments (8)
src/features/signup/api/signup.ts (2)
18-34: 🧹 Nitpick | 🔵 Trivial | 💤 Low value
instructorSignupResultSchema와designerSignupResultSchema가 동일합니다.두 스키마의 구조가 완전히 같으므로 공통 스키마로 추출하여 중복을 제거할 수 있습니다.
♻️ 스키마 통합 제안
-const instructorSignupResultSchema = z.object({ - userId: z.number(), - userType: z.string(), - name: z.string(), - profileImageUrl: z.string(), - accessToken: z.string(), -}); - -export type InstructorSignupResult = z.infer<typeof instructorSignupResultSchema>; - -const designerSignupResultSchema = z.object({ - userId: z.number(), - userType: z.string(), - name: z.string(), - profileImageUrl: z.string(), - accessToken: z.string(), -}); +const signupResultSchema = z.object({ + userId: z.number(), + userType: z.string(), + name: z.string(), + profileImageUrl: z.string(), + accessToken: z.string(), +}); + +export type InstructorSignupResult = z.infer<typeof signupResultSchema>; +export type DesignerSignupResult = z.infer<typeof signupResultSchema>;🤖 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 18 - 34, The instructorSignupResultSchema and designerSignupResultSchema have identical field definitions which creates unnecessary code duplication. Extract a single shared schema containing the common fields (userId, userType, name, profileImageUrl, accessToken), then use this shared schema to define both InstructorSignupResult and DesignerSignupResult types. This eliminates the duplicate schema definitions while maintaining the same type exports.
141-159: 🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoffpresigned URL 업로드 실패 시 재시도 로직이 없습니다.
S3 presigned URL 업로드는 네트워크 일시적 오류로 실패할 수 있습니다. 현재 구현은 단일 시도 후 바로 실패합니다. 중요한 파일 업로드이므로 재시도 로직 추가를 고려해 보세요.
🤖 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 141 - 159, The uploadPortfolioFile function lacks retry logic for handling transient network failures during the S3 presigned URL upload. Implement exponential backoff retry logic in the uploadPortfolioFile function to automatically retry the fetch request when network errors occur or when the response is not ok. Define a reasonable retry count (typically 2-3 attempts) and only throw the ApiError after all retry attempts have been exhausted. This ensures the upload has a better chance of succeeding despite temporary network hiccups without changing the function signature or error handling contract.src/features/signup/model/useSignupStep2Form.ts (2)
240-246: 🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff
validateAndGetAccountData가 validation 실패 원인을 구분하지 않습니다.
isFormValid,isUserIdAvailable,isEmailVerified중 어느 조건이 실패했는지 알 수 없어 호출자가 적절한 피드백을 제공하기 어렵습니다. 현재 UI에서는 버튼 활성화 상태로 미리 제어하므로 문제없지만, 향후 개선 시 고려해 보세요.🤖 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/model/useSignupStep2Form.ts` around lines 240 - 246, The validateAndGetAccountData function returns null without distinguishing which validation condition failed among isFormValid, isUserIdAvailable, or isEmailVerified, making it difficult for callers to provide specific feedback. Refactor the return type of validateAndGetAccountData to return an object that includes both the account data and validation status details (such as which condition failed), rather than just returning null on any failure, so the caller can determine the specific reason for validation failure.
71-71: 🧹 Nitpick | 🔵 Trivial
useWatch호출 시 명시적defaultValue지정 권장
useWatch({ control })에서 타입 단언을 통해SignupAccountFormValues로 처리하고 있습니다.useForm의defaultValues가 모든 필드를 제공하므로 실무에서는 안전하지만, 스키마 변경 시 타입 안전성을 보장하기 위해 다음과 같이 개선하는 것이 좋습니다:const values = useWatch({ control, defaultValue: { email: "", password: "", passwordConfirm: "", username: "", verificationCode: "", } }) as SignupAccountFormValues;또는 타입 단언 없이 더 안전하게:
const values = useWatch<SignupAccountFormValues>({ control });🤖 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/model/useSignupStep2Form.ts` at line 71, The useWatch hook call on the values variable is using a type assertion as SignupAccountFormValues without explicitly specifying a defaultValue parameter. To improve type safety and handle potential schema changes, replace the type assertion with either an explicit defaultValue object containing all required fields (email, password, passwordConfirm, username, verificationCode) in the useWatch configuration, or use a generic type parameter approach by specifying useWatch<SignupAccountFormValues>({ control }) without the type assertion. Choose the generic type parameter approach for cleaner code and better type inference.src/features/signup/model/signupSchemas.ts (2)
29-33: 🧹 Nitpick | 🔵 Trivial | 💤 Low valuephone 스키마의
.max()제약이 중복됩니다.regex
/^\d{10,11}$/가 이미 10-11자리로 길이를 제한하므로.max(SIGNUP_MAX_PHONE_NUMBER_LENGTH)는 불필요합니다. 단, 가독성 또는 명시적 문서화 목적이라면 유지해도 무방합니다.🤖 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/model/signupSchemas.ts` around lines 29 - 33, The phone schema in the signupSchemas has a redundant `.max()` constraint that duplicates validation already performed by the regex pattern. The regex `/^\d{10,11}$/` already restricts the phone number to 10-11 digits, making the `.max(SIGNUP_MAX_PHONE_NUMBER_LENGTH)` call unnecessary. Remove the redundant `.max()` method call from the phone field validation chain to clean up the schema, unless explicit documentation or readability justifies keeping it.
58-67: 🧹 Nitpick | 🔵 Trivial | 💤 Low value
z.custom사용이 적절하나, 에러 메시지 커스터마이징을 고려해 보세요.
z.custom은 validation 실패 시 기본 에러 메시지가 불명확합니다. Zod 4에서는error옵션으로 명확한 메시지를 제공할 수 있습니다.♻️ 에러 메시지 추가 예시
export const signupDesignerAdditionalSchema = z.object({ bankCode: z.custom<(typeof BANK_OPTIONS)[number]["code"]>( value => typeof value === "string" && BANK_OPTIONS.some(({ code }) => code === value), + { error: "유효한 은행을 선택해주세요" }, ), accountNumber: z.string().trim().min(1), accountHolder: z.string().trim().min(1), portfolioFiles: z .array(z.custom<File>(value => typeof File !== "undefined" && value instanceof File)) .max(3), });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/signup/model/signupSchemas.ts` around lines 58 - 67, The z.custom validators in the signupDesignerAdditionalSchema schema lack clear error messages which makes validation failures confusing. Add the error option parameter to both z.custom calls in the bankCode field and the portfolioFiles array validation to provide descriptive error messages. For the bankCode validation, specify an error message indicating it must be a valid bank code from the available options. For the portfolioFiles File instance check, specify an error message indicating that each item must be a valid File object. This will help users understand validation failures more clearly.src/widgets/signup/ui/SignupFunnel.tsx (1)
67-73: 🧹 Nitpick | 🔵 Trivial | 💤 Low value디자이너 회원가입 완료 시
moveNext의/login경로가 도달하지 않습니다.
handleDesignerSubmit에서router.push(getClientUserHomePath(userRole))로 직접 이동하므로,moveNext의/login폴백은 실행되지 않습니다. 이 코드가 의도된 것인지 확인이 필요합니다.현재 흐름:
- 강사:
AccountStep→handleInstructorSignup성공 →onNext→moveNext→/instructor✓- 디자이너:
DesignerAdditionalStep→handleDesignerSubmit→router.push(homePath)(moveNext 호출 안함)만약
moveNext의 디자이너 경로가 불필요하다면 제거하거나, 의도된 폴백이라면 주석을 추가해주세요.🤖 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 67 - 73, The routing logic in the `moveNext` function currently defaults to `/login` for non-instructor roles, but the designer signup flow bypasses this by calling `router.push(homePath)` directly in `handleDesignerSubmit`, making the `/login` fallback unreachable for designers. Update the condition in `moveNext` where `router.push` is called to explicitly handle all roles (instructor should navigate to "/instructor" and designer should navigate to its appropriate home path using `getClientUserHomePath`), or alternatively add a clear comment explaining why designers intentionally skip this `moveNext` logic. This ensures the routing behavior is explicit and intentional for all user roles.src/shared/ui/SidebarMenu.tsx (1)
39-40: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win조건부
className결합은cn()유틸로 통일해주세요.현재 문자열 덧붙이기 방식은 Tailwind 조건 확장 시 가독성과 유지보수성이 떨어집니다. 팀 규칙대로
cn()기반 조합으로 맞추는 게 좋습니다.리팩터링 예시
- const className = - "bg-gray-5 rounded-8 hover:bg-gray-20 group block w-58 cursor-pointer px-5 py-3 text-left transition-colors duration-150 disabled:cursor-not-allowed disabled:opacity-60" + - (isSelected ? " bg-gray-20" : ""); + const className = cn( + "bg-gray-5 rounded-8 hover:bg-gray-20 group block w-58 cursor-pointer px-5 py-3 text-left transition-colors duration-150 disabled:cursor-not-allowed disabled:opacity-60", + isSelected && "bg-gray-20", + );As per coding guidelines, "TailwindCSS - 조건부 클래스 문자열을 직접 이어붙이는 경우 cn() 유틸리티 사용을 제안해주세요."
🤖 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/SidebarMenu.tsx` around lines 39 - 40, The className assignment in SidebarMenu.tsx uses string concatenation with the + operator to combine base Tailwind classes with a conditional class based on the isSelected state. Replace this string concatenation approach with the cn() utility function by passing the base class string and the conditional class (using a ternary operator or object syntax) as separate arguments to cn(). This improves code readability and maintainability by following the team's coding guidelines for conditional className combining.Source: Coding guidelines
🤖 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/signup/api/signup.ts`:
- Around line 43-62: The `fallbackMessage` parameter in the `unwrapApiResponse`
function is declared but never used in the function body, making it an unused
parameter. Either remove the `fallbackMessage` parameter from the function
signature and all call sites where it is passed, or utilize it by passing it to
the `getApiResponseMessage` call or using it as a fallback in error message
construction when an API response error occurs.
In `@src/features/signup/config/signup.ts`:
- Around line 100-104: The USERINFO configuration object in the signup
configuration has an inconsistency between the label property (which is "강사 약관
2") and the modalTitle property (which is "강사 약관 제목 3"). Update the modalTitle
value from "강사 약관 제목 3" to "강사 약관 제목 2" to match the corresponding label and
maintain consistency across the configuration.
In `@src/features/signup/ui/AccountStep.tsx`:
- Around line 34-54: The handleNext function silently returns without user
feedback when profileData is null or when userRole normalization fails (when
role is "instructor"). Add error messaging or toast notifications to inform the
user when these validation failures occur. Specifically, display an appropriate
error message when profileData is null before calling
form.handleInstructorSignup and when normalizeClientUserRole returns null, so
users understand why the signup flow is not proceeding.
In `@src/features/signup/ui/DesignerAdditionalStep.tsx`:
- Around line 40-43: The useUploadedFiles hook on line 43 is not being passed
the initialData to restore previously uploaded portfolio files, while other form
fields like bankCode, accountNumber, and accountHolder are properly initialized
from initialData on lines 40-42. Modify the useUploadedFiles hook invocation to
pass initialData?.portfolioFiles as a parameter to restore the files when users
navigate back to this step, ensuring consistency with how other form fields are
being initialized.
In `@src/shared/api/client.ts`:
- Around line 76-86: The reissueAccessToken function and related token refresh
logic has a race condition where multiple simultaneous 401 requests can trigger
duplicate token reissue operations, and one failed reissue can call
clearClientAuth() to invalidate a successful reissue from a parallel request.
Implement a single-flight pattern by storing the token reissue Promise in a
module-level variable. When reissueAccessToken is called, check if a reissue is
already in progress and return the existing Promise instead of starting a new
one. Clear the stored Promise only after the reissue completes (either
successfully or with error). This ensures all concurrent requests share a single
reissue operation, preventing the clearClientAuth() call from invalidating a
successful reissue completed by another request.
- Around line 171-174: The fallback case in the error handler is returning an
empty string as the ApiError message, which provides no guidance to users when
an unknown exception occurs. Replace the empty string with a meaningful fallback
message such as a common error phrase like "An unknown error occurred" or
similar contextual text. This ensures that even when the error is not an
instance of Error, users receive helpful error guidance instead of seeing
nothing.
In `@src/shared/ui/Header.tsx`:
- Line 56: The header's login authentication check is inconsistent with the
middleware authentication logic in src/proxy.ts. Currently, the isLoggedIn
property only verifies that accessToken is not null, but the middleware also
validates that the user's role can be successfully interpreted. When role
interpretation fails in the middleware, the user is treated as unauthenticated,
which causes a mismatch where the header shows the user as logged in while the
middleware rejects the request, breaking the recovery flow. Update the
isLoggedIn check in the Header component to include the same role/permissions
validation logic that the proxy middleware uses, ensuring both checks use
identical authentication criteria. This applies to all authentication checks in
the Header component.
---
Nitpick comments:
In `@src/features/signup/api/signup.ts`:
- Around line 18-34: The instructorSignupResultSchema and
designerSignupResultSchema have identical field definitions which creates
unnecessary code duplication. Extract a single shared schema containing the
common fields (userId, userType, name, profileImageUrl, accessToken), then use
this shared schema to define both InstructorSignupResult and
DesignerSignupResult types. This eliminates the duplicate schema definitions
while maintaining the same type exports.
- Around line 141-159: The uploadPortfolioFile function lacks retry logic for
handling transient network failures during the S3 presigned URL upload.
Implement exponential backoff retry logic in the uploadPortfolioFile function to
automatically retry the fetch request when network errors occur or when the
response is not ok. Define a reasonable retry count (typically 2-3 attempts) and
only throw the ApiError after all retry attempts have been exhausted. This
ensures the upload has a better chance of succeeding despite temporary network
hiccups without changing the function signature or error handling contract.
In `@src/features/signup/model/signupSchemas.ts`:
- Around line 29-33: The phone schema in the signupSchemas has a redundant
`.max()` constraint that duplicates validation already performed by the regex
pattern. The regex `/^\d{10,11}$/` already restricts the phone number to 10-11
digits, making the `.max(SIGNUP_MAX_PHONE_NUMBER_LENGTH)` call unnecessary.
Remove the redundant `.max()` method call from the phone field validation chain
to clean up the schema, unless explicit documentation or readability justifies
keeping it.
- Around line 58-67: The z.custom validators in the
signupDesignerAdditionalSchema schema lack clear error messages which makes
validation failures confusing. Add the error option parameter to both z.custom
calls in the bankCode field and the portfolioFiles array validation to provide
descriptive error messages. For the bankCode validation, specify an error
message indicating it must be a valid bank code from the available options. For
the portfolioFiles File instance check, specify an error message indicating that
each item must be a valid File object. This will help users understand
validation failures more clearly.
In `@src/features/signup/model/useSignupStep2Form.ts`:
- Around line 240-246: The validateAndGetAccountData function returns null
without distinguishing which validation condition failed among isFormValid,
isUserIdAvailable, or isEmailVerified, making it difficult for callers to
provide specific feedback. Refactor the return type of validateAndGetAccountData
to return an object that includes both the account data and validation status
details (such as which condition failed), rather than just returning null on any
failure, so the caller can determine the specific reason for validation failure.
- Line 71: The useWatch hook call on the values variable is using a type
assertion as SignupAccountFormValues without explicitly specifying a
defaultValue parameter. To improve type safety and handle potential schema
changes, replace the type assertion with either an explicit defaultValue object
containing all required fields (email, password, passwordConfirm, username,
verificationCode) in the useWatch configuration, or use a generic type parameter
approach by specifying useWatch<SignupAccountFormValues>({ control }) without
the type assertion. Choose the generic type parameter approach for cleaner code
and better type inference.
In `@src/shared/ui/SidebarMenu.tsx`:
- Around line 39-40: The className assignment in SidebarMenu.tsx uses string
concatenation with the + operator to combine base Tailwind classes with a
conditional class based on the isSelected state. Replace this string
concatenation approach with the cn() utility function by passing the base class
string and the conditional class (using a ternary operator or object syntax) as
separate arguments to cn(). This improves code readability and maintainability
by following the team's coding guidelines for conditional className combining.
In `@src/widgets/signup/ui/SignupFunnel.tsx`:
- Around line 67-73: The routing logic in the `moveNext` function currently
defaults to `/login` for non-instructor roles, but the designer signup flow
bypasses this by calling `router.push(homePath)` directly in
`handleDesignerSubmit`, making the `/login` fallback unreachable for designers.
Update the condition in `moveNext` where `router.push` is called to explicitly
handle all roles (instructor should navigate to "/instructor" and designer
should navigate to its appropriate home path using `getClientUserHomePath`), or
alternatively add a clear comment explaining why designers intentionally skip
this `moveNext` logic. This ensures the routing behavior is explicit and
intentional for all user roles.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: de0c4fdc-c731-4954-989a-fa5e5808528c
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (28)
package.jsonsrc/app/designer/layout.tsxsrc/app/instructor/layout.tsxsrc/app/login/page.tsxsrc/features/auth/api/logout.tssrc/features/auth/index.tssrc/features/auth/model/useLogout.tssrc/features/auth/ui/LogoutSidebarMenu.tsxsrc/features/login/api/login.tssrc/features/login/index.tssrc/features/login/model/loginSchemas.tssrc/features/login/model/useLoginForm.tssrc/features/signup/api/signup.tssrc/features/signup/config/signup.tssrc/features/signup/index.tssrc/features/signup/model/signup.tssrc/features/signup/model/signupSchemas.tssrc/features/signup/model/useSignupStep2Form.tssrc/features/signup/ui/AccountStep.tsxsrc/features/signup/ui/DesignerAdditionalStep.tsxsrc/features/signup/ui/TermsProfileStep.tsxsrc/proxy.tssrc/shared/api/client.tssrc/shared/api/types.tssrc/shared/lib/auth/client.tssrc/shared/ui/Header.tsxsrc/shared/ui/SidebarMenu.tsxsrc/widgets/signup/ui/SignupFunnel.tsx
| const unwrapApiResponse = async <T>( | ||
| request: Promise<ApiResponse<unknown>>, | ||
| resultSchema: z.ZodType<T>, | ||
| fallbackMessage?: string, | ||
| ) => { | ||
| try { | ||
| const response = await request; | ||
|
|
||
| if (!response.success) { | ||
| throw new ApiError(getApiResponseMessage(response), { | ||
| code: response.code, | ||
| response, | ||
| }); | ||
| } | ||
|
|
||
| return resultSchema.parse(response.result); | ||
| } catch (error) { | ||
| throw await toApiError(error); | ||
| } | ||
| }; |
There was a problem hiding this comment.
fallbackMessage 매개변수가 선언되었지만 사용되지 않습니다.
unwrapApiResponse의 세 번째 매개변수 fallbackMessage가 함수 내부에서 사용되지 않아 Line 207에서 전달된 값이 무시됩니다.
🐛 fallbackMessage 활용 제안
const unwrapApiResponse = async <T>(
request: Promise<ApiResponse<unknown>>,
resultSchema: z.ZodType<T>,
fallbackMessage?: string,
) => {
try {
const response = await request;
if (!response.success) {
throw new ApiError(getApiResponseMessage(response), {
code: response.code,
response,
});
}
return resultSchema.parse(response.result);
} catch (error) {
- throw await toApiError(error);
+ throw await toApiError(error, fallbackMessage);
}
};또는 fallbackMessage를 사용하지 않는다면 매개변수와 Line 207의 인자를 제거하세요.
🤖 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 43 - 62, The
`fallbackMessage` parameter in the `unwrapApiResponse` function is declared but
never used in the function body, making it an unused parameter. Either remove
the `fallbackMessage` parameter from the function signature and all call sites
where it is passed, or utilize it by passing it to the `getApiResponseMessage`
call or using it as a fallback in error message construction when an API
response error occurs.
| id: "USERINFO", | ||
| label: "강사 약관 2", | ||
| modalTitle: "강사 약관 제목 3", | ||
| content: "강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2", | ||
| version: "V1.0", |
There was a problem hiding this comment.
USERINFO 항목의 modalTitle이 "강사 약관 제목 3"으로 되어 있습니다.
label은 "강사 약관 2"인데 modalTitle은 "강사 약관 제목 3"으로 되어 있어 불일치합니다. "강사 약관 제목 2"가 되어야 할 것 같습니다.
🔧 수정 제안
{
id: "USERINFO",
label: "강사 약관 2",
- modalTitle: "강사 약관 제목 3",
+ modalTitle: "강사 약관 제목 2",
content: "강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2",
version: "V1.0",
},📝 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.
| id: "USERINFO", | |
| label: "강사 약관 2", | |
| modalTitle: "강사 약관 제목 3", | |
| content: "강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2", | |
| version: "V1.0", | |
| id: "USERINFO", | |
| label: "강사 약관 2", | |
| modalTitle: "강사 약관 제목 2", | |
| content: "강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2강사약관2", | |
| version: "V1.0", |
🤖 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/config/signup.ts` around lines 100 - 104, The USERINFO
configuration object in the signup configuration has an inconsistency between
the label property (which is "강사 약관 2") and the modalTitle property (which is
"강사 약관 제목 3"). Update the modalTitle value from "강사 약관 제목 3" to "강사 약관 제목 2" to
match the corresponding label and maintain consistency across the configuration.
| const handleNext = async () => { | ||
| const accountData = await form.validateAndGetAccountData(); | ||
|
|
||
| if (accountData == null) return; | ||
|
|
||
| if (role === "instructor") { | ||
| if (profileData == null) return; | ||
|
|
||
| const result = await form.handleInstructorSignup(profileData, accountData); | ||
|
|
||
| if (result == null) return; | ||
|
|
||
| const userRole = normalizeClientUserRole(result.userType); | ||
|
|
||
| if (userRole == null) return; | ||
|
|
||
| setClientAuth({ accessToken: result.accessToken, role: userRole }); | ||
| } | ||
|
|
||
| onNext(accountData); | ||
| }; |
There was a problem hiding this comment.
profileData가 없거나 userRole 정규화 실패 시 사용자에게 피드백이 없습니다.
role === "instructor"일 때 profileData == null이거나 userRole == null인 경우 조용히 반환되어 사용자가 왜 진행되지 않는지 알 수 없습니다. 에러 메시지를 표시하는 것이 좋겠습니다.
🛡️ 개선 제안
const handleNext = async () => {
const accountData = await form.validateAndGetAccountData();
if (accountData == null) return;
if (role === "instructor") {
- if (profileData == null) return;
+ if (profileData == null) {
+ form.setSubmitErrorMessage?.("프로필 정보를 확인할 수 없습니다");
+ return;
+ }
const result = await form.handleInstructorSignup(profileData, accountData);
if (result == null) return;
const userRole = normalizeClientUserRole(result.userType);
- if (userRole == null) return;
+ if (userRole == null) {
+ form.setSubmitErrorMessage?.("사용자 유형을 확인할 수 없습니다");
+ return;
+ }
setClientAuth({ accessToken: result.accessToken, role: userRole });
}
onNext(accountData);
};📝 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 handleNext = async () => { | |
| const accountData = await form.validateAndGetAccountData(); | |
| if (accountData == null) return; | |
| if (role === "instructor") { | |
| if (profileData == null) return; | |
| const result = await form.handleInstructorSignup(profileData, accountData); | |
| if (result == null) return; | |
| const userRole = normalizeClientUserRole(result.userType); | |
| if (userRole == null) return; | |
| setClientAuth({ accessToken: result.accessToken, role: userRole }); | |
| } | |
| onNext(accountData); | |
| }; | |
| const handleNext = async () => { | |
| const accountData = await form.validateAndGetAccountData(); | |
| if (accountData == null) return; | |
| if (role === "instructor") { | |
| if (profileData == null) { | |
| form.setSubmitErrorMessage?.("프로필 정보를 확인할 수 없습니다"); | |
| return; | |
| } | |
| const result = await form.handleInstructorSignup(profileData, accountData); | |
| if (result == null) return; | |
| const userRole = normalizeClientUserRole(result.userType); | |
| if (userRole == null) { | |
| form.setSubmitErrorMessage?.("사용자 유형을 확인할 수 없습니다"); | |
| return; | |
| } | |
| setClientAuth({ accessToken: result.accessToken, role: userRole }); | |
| } | |
| onNext(accountData); | |
| }; |
🤖 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/ui/AccountStep.tsx` around lines 34 - 54, The handleNext
function silently returns without user feedback when profileData is null or when
userRole normalization fails (when role is "instructor"). Add error messaging or
toast notifications to inform the user when these validation failures occur.
Specifically, display an appropriate error message when profileData is null
before calling form.handleInstructorSignup and when normalizeClientUserRole
returns null, so users understand why the signup flow is not proceeding.
| const [selectedBank, setSelectedBank] = useState<BankCode | null>(initialData?.bankCode ?? null); | ||
| const [accountNumber, setAccountNumber] = useState(initialData?.accountNumber ?? ""); | ||
| const [accountHolder, setAccountHolder] = useState(initialData?.accountHolder ?? ""); | ||
| const { uploadedFiles, handleFilesAdded, handleRemove } = useUploadedFiles(); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# useUploadedFiles 훅의 초기값 지원 여부 확인
ast-grep --pattern $'const useUploadedFiles = ($$$) => {
$$$
}'Repository: Ditda-Official/Ditda-Frontend
Length of output: 3364
🏁 Script executed:
# Check the DesignerAdditionalStep.tsx file to see initialData structure and hook call
head -60 src/features/signup/ui/DesignerAdditionalStep.tsxRepository: Ditda-Official/Ditda-Frontend
Length of output: 2128
🏁 Script executed:
# Find and check the SignupDesignerAdditionalData type definition
rg "type SignupDesignerAdditionalData" -A 15Repository: Ditda-Official/Ditda-Frontend
Length of output: 2488
🏁 Script executed:
# Check the signupDesignerAdditionalSchema definition
rg "signupDesignerAdditionalSchema" -B 5 -A 15 src/features/signup/model/signupSchemas.tsRepository: Ditda-Official/Ditda-Frontend
Length of output: 1047
🏁 Script executed:
# Check the handleSubmit and how portfolioFiles are handled in submission
sed -n '60,120p' src/features/signup/ui/DesignerAdditionalStep.tsxRepository: Ditda-Official/Ditda-Frontend
Length of output: 2159
useUploadedFiles() 호출 시 initialData?.portfolioFiles를 전달하지 않아 파일 상태가 복원되지 않습니다.
다른 필드들(bankCode, accountNumber, accountHolder)은 initialData로부터 초기값이 설정되지만(40-42줄), 포트폴리오 파일은 복원되지 않습니다. useUploadedFiles 훅이 externalFiles 매개변수를 지원하므로, 다음과 같이 수정하세요:
const { uploadedFiles, handleFilesAdded, handleRemove } = useUploadedFiles(initialData?.portfolioFiles);
사용자가 "이전" 버튼으로 돌아갔다가 다시 이 단계로 오면 업로드한 파일이 사라집니다.
🤖 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/ui/DesignerAdditionalStep.tsx` around lines 40 - 43, The
useUploadedFiles hook on line 43 is not being passed the initialData to restore
previously uploaded portfolio files, while other form fields like bankCode,
accountNumber, and accountHolder are properly initialized from initialData on
lines 40-42. Modify the useUploadedFiles hook invocation to pass
initialData?.portfolioFiles as a parameter to restore the files when users
navigate back to this step, ensuring consistency with how other form fields are
being initialized.
| const reissueAccessToken = async () => { | ||
| try { | ||
| const response = await tokenRefreshApi | ||
| .post(createApiPath(TOKEN_REISSUE_PATH)) | ||
| .json<ApiResponse<unknown>>(); | ||
|
|
||
| return extractAccessToken(response); | ||
| } catch (error) { | ||
| throw await toApiError(error); | ||
| } | ||
| }; |
There was a problem hiding this comment.
토큰 재발급 경쟁 상태로 정상 세션이 강제 로그아웃될 수 있습니다.
동시에 여러 요청이 401을 받으면 재발급이 중복 실행되고, 한 요청의 실패가 clearClientAuth()를 호출해 이미 갱신된 세션까지 지울 수 있습니다. 재발급 Promise를 공유해 단일 비행으로 직렬화해 주세요.
🔧 제안 수정안
+let refreshPromise: Promise<string> | null = null;
+
+const getOrRefreshAccessToken = () => {
+ if (refreshPromise == null) {
+ refreshPromise = reissueAccessToken().finally(() => {
+ refreshPromise = null;
+ });
+ }
+
+ return refreshPromise;
+};
+
export const api = ky.create({
@@
afterResponse: [
async ({ request, response, retryCount }) => {
@@
try {
- const accessToken = await reissueAccessToken();
+ const accessToken = await getOrRefreshAccessToken();
setClientAccessToken(accessToken);
@@
} catch {
clearClientAuth();
}
},
],
},
});Also applies to: 101-126
🤖 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/api/client.ts` around lines 76 - 86, The reissueAccessToken
function and related token refresh logic has a race condition where multiple
simultaneous 401 requests can trigger duplicate token reissue operations, and
one failed reissue can call clearClientAuth() to invalidate a successful reissue
from a parallel request. Implement a single-flight pattern by storing the token
reissue Promise in a module-level variable. When reissueAccessToken is called,
check if a reissue is already in progress and return the existing Promise
instead of starting a new one. Clear the stored Promise only after the reissue
completes (either successfully or with error). This ensures all concurrent
requests share a single reissue operation, preventing the clearClientAuth() call
from invalidating a successful reissue completed by another request.
| if (error instanceof Error) return new ApiError(error.message); | ||
|
|
||
| return new ApiError(""); | ||
| }; |
There was a problem hiding this comment.
알 수 없는 예외에서 빈 메시지를 반환하면 에러 안내가 사라집니다.
기본 메시지를 빈 문자열로 두지 말고 공통 fallback 문구를 사용해 주세요.
🔧 제안 수정안
- return new ApiError("");
+ return new ApiError("요청 처리 중 문제가 발생했습니다");🤖 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/api/client.ts` around lines 171 - 174, The fallback case in the
error handler is returning an empty string as the ApiError message, which
provides no guidance to users when an unknown exception occurs. Replace the
empty string with a meaningful fallback message such as a common error phrase
like "An unknown error occurred" or similar contextual text. This ensures that
even when the error is not an instance of Error, users receive helpful error
guidance instead of seeing nothing.
|
|
||
| setAuthState({ | ||
| isLoggedIn: accessToken != null && role != null, | ||
| isLoggedIn: accessToken != null, |
There was a problem hiding this comment.
헤더의 로그인 판별 기준이 미들웨어 권한 판별과 불일치합니다.
현재는 accessToken만 있으면 로그인 UI를 노출하지만, src/proxy.ts는 역할 해석 실패 시 비인증으로 처리합니다. 이 상태에선 로그인/회원가입 CTA가 숨겨지고 내 계정이 /로 고정되어 복구 동선이 끊깁니다. 역할 해석 기준을 동일하게 맞춰 주세요.
🔧 제안 수정안
+import { getClientUserRoleFromAccessToken } from "`@/shared/lib/auth/client`";
@@
const syncAuthState = () => {
const accessToken = getCookieValue(ACCESS_TOKEN_COOKIE_NAME);
- const role = normalizeRole(getCookieValue(USER_ROLE_COOKIE_NAME));
+ const roleFromCookie = normalizeRole(getCookieValue(USER_ROLE_COOKIE_NAME));
+ const role =
+ roleFromCookie ?? (accessToken != null ? getClientUserRoleFromAccessToken(accessToken) : null);
setAuthState({
- isLoggedIn: accessToken != null,
+ isLoggedIn: accessToken != null && role != null,
role,
});
};
@@
- if (authState.role == null) return "/";
+ if (authState.role == null) return "/login";Also applies to: 68-68
🤖 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` at line 56, The header's login authentication check
is inconsistent with the middleware authentication logic in src/proxy.ts.
Currently, the isLoggedIn property only verifies that accessToken is not null,
but the middleware also validates that the user's role can be successfully
interpreted. When role interpretation fails in the middleware, the user is
treated as unauthenticated, which causes a mismatch where the header shows the
user as logged in while the middleware rejects the request, breaking the
recovery flow. Update the isLoggedIn check in the Header component to include
the same role/permissions validation logic that the proxy middleware uses,
ensuring both checks use identical authentication criteria. This applies to all
authentication checks in the Header component.
📢 PR 유형
어떤 변경 사항이 있었나요?
📌 관련 이슈번호
✅ Key Changes
/login,/signup에 접근 제한📸 스크린샷 or 실행영상
2026-06-22.18.15.25.mov
🎸 기타 사항 or 추가 코멘트
기존에 요청했던 플로우는 계속 api를 호출해서 문제가 발생할 가능성이 있어 확인 버튼으로 반영(디자인 팀 노티 필요)

Summary by CodeRabbit
릴리스 노트
New Features
Refactor