Skip to content

[FEAT] 비밀번호 변경 및 재설정 - #59

Closed
kkhhmm3103 wants to merge 6 commits into
devfrom
feat/password-change
Closed

[FEAT] 비밀번호 변경 및 재설정#59
kkhhmm3103 wants to merge 6 commits into
devfrom
feat/password-change

Conversation

@kkhhmm3103

@kkhhmm3103 kkhhmm3103 commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

📌 PR 설명

설정 페이지에서 비밀번호를 변경할 수 있는 기능을 추가하고,
이메일 인증 기반 비밀번호 재설정 API를 프론트에 연동했습니다.

✅ 완료한 기능 명세

  • 설정 페이지 프로필 탭에 비밀번호 변경 UI 추가
  • 비밀번호 변경 API(/api/auth/password/change) 연동
  • 비밀번호 변경 시 입력값 검증(Zod) 적용
  • 서버 에러 코드 기반 프론트 에러 처리 적용
  • 비밀번호 재설정 API(/api/auth/password/reset) 연동
  • 이메일 인증 흐름 기반 재설정 로직 구조 반영

📸 스크린샷

image

💭 고민과 해결과정

  • AUTH_PASSWORD_MISMATCH, AUTH_PASSWORD_SAME_AS_OLD 등 케이스를 필드별 에러로 매핑하여 UX를 개선했습니다.
  • 비밀번호 재설정은 이메일 인증(validateVerifiedEmail → consumeVerifiedEmail) 흐름을 따르기 때문에, 프론트에서도 인증 이후에만 변경이 가능하도록 구조를 맞췄습니다.

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 설정 페이지에서 비밀번호 변경 기능 추가
    • 비밀번호 재설정 기능 추가
    • 보안 인증(2FA) 섹션 추가 (준비 중)
  • 개선

    • 인증 API 처리 로직 최적화

@kkhhmm3103 kkhhmm3103 self-assigned this Apr 19, 2026
@kkhhmm3103 kkhhmm3103 added the FEAT 새로운 기능 추가 또는 기존 기능 확장 label Apr 19, 2026
@coderabbitai

coderabbitai Bot commented Apr 19, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@kkhhmm3103 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 51 minutes and 54 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 51 minutes and 54 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4ae0cb46-0a2b-4158-ba55-604e0985df7d

📥 Commits

Reviewing files that changed from the base of the PR and between 9327a2d and 28ecfac.

📒 Files selected for processing (1)
  • src/features/game-room/model/use-waiting-room.ts
📝 Walkthrough

Walkthrough

비밀번호 변경 및 재설정 기능을 위한 API 모듈, Zod 검증 스키마, React Query 뮤테이션 훅을 추가했습니다. 설정 프로필 섹션에 비밀번호 변경 폼을 통합하고, API 응답 가로채기 로직을 확장했으며, Toss Payments 타입 정의를 추가했습니다.

Changes

Cohort / File(s) Summary
비밀번호 변경 API 및 검증
src/features/auth-dialog/api/change-password.ts, src/features/auth-dialog/model/change-password.schema.ts
비밀번호 변경 요청 타입, API 함수, 클라이언트 측 Zod 검증 스키마 추가
비밀번호 재설정 API 및 검증
src/features/auth-dialog/api/reset-password.ts, src/features/auth-dialog/model/reset-password.schema.ts
비밀번호 재설정 요청 타입, API 함수, 이메일/코드/새 비밀번호 검증 스키마 추가
뮤테이션 훅
src/features/auth-dialog/model/use-change-password-mutation.ts, src/features/auth-dialog/model/use-reset-password-mutation.ts
TanStack React Query 기반 비밀번호 변경/재설정 뮤테이션 훅 추가
설정 UI 통합
src/features/settings/ui/settings-profile-section.tsx
비밀번호 변경 폼 UI 추가, 클라이언트 검증, 뮤테이션 통합, API 에러 처리, 2FA 섹션 추가
API 설정
src/shared/api/base-api.ts
응답 가로채기 로직에 /auth/password/reset/auth/email-verifications 엔드포인트 제외 목록 추가
설정 뷰 및 타입 정의
src/features/settings/ui/settings-view.tsx, src/shared/type/toss-payments.d.ts
설정 뷰 포맷팅 변경 및 Toss Payments 전역 타입 정의 추가

Sequence Diagram

sequenceDiagram
    participant User
    participant Settings as Settings Profile<br/>Component
    participant Validation as Zod<br/>Validation
    participant Mutation as Change Password<br/>Mutation
    participant API as API Layer<br/>(baseApi)
    participant Server as Auth Server

    User->>Settings: 현재 비밀번호, 새 비밀번호 입력
    User->>Settings: 변경하기 클릭
    
    Settings->>Validation: changePasswordSchema.parse()<br/>(검증)
    
    alt 검증 실패
        Validation-->>Settings: 필드 에러 반환
        Settings->>User: 에러 메시지 표시
    else 검증 성공
        Validation-->>Settings: 통과
        Settings->>Mutation: mutateAsync(request)
        Mutation->>API: POST /auth/password/change
        API->>Server: 요청 전송
        
        alt 성공 (200)
            Server-->>API: 응답 반환
            API-->>Mutation: data 추출
            Mutation-->>Settings: 성공
            Settings->>User: 성공 메시지 표시<br/>폼 초기화
        else 실패 (4xx/5xx)
            Server-->>API: 에러 응답
            API-->>Mutation: ApiError 발생
            Mutation-->>Settings: 에러 코드 반환
            Settings->>User: 매핑된 에러 메시지 표시
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • [FEAT] 로그인 구현 #13: base-api의 인증 요청 제외 목록 확장 로직이 직접 연관되어 있습니다 (/auth/password/reset/auth/email-verifications 엔드포인트 처리).
  • 그룹채팅 구현 및 로그인 적용 #16: 동일한 src/shared/api/base-api.ts의 인증 처리 로직을 수정합니다 (기본 X-User-Id 헤더 제거 vs. 제외 목록 확장).
  • [FEAT] 설정 페이지 그룹 연동 #26: src/shared/api/base-api.ts의 토큰 갱신/제외 로직과 src/features/settings/ui/settings-profile-section.tsx의 설정 프로필 UI를 공통으로 수정합니다.

Suggested labels

ready-for-review

Suggested reviewers

  • gaeunnlee
  • cl-o-lc
  • minseokim0113

Poem

🐰 비밀번호를 바꾸려 하네,
Zod가 검증하니 든든하고,
뮤테이션이 서버로 날아가,
보안이 한층 더 튼튼해졌네!
설정 페이지에 기분 좋은 변화,
토끼도 함께 축하합니다! 🌙✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 PR의 주요 변경 사항을 명확하게 요약합니다. 비밀번호 변경 및 재설정 기능 추가라는 핵심 내용을 잘 나타냅니다.

✏️ 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 feat/password-change

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 and usage tips.

@kkhhmm3103 kkhhmm3103 changed the title Feat/password change [FEAT] 비밀번호 변경 및 재설정 Apr 19, 2026

@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: 9

🧹 Nitpick comments (6)
src/features/auth-dialog/api/change-password.ts (1)

15-18: 성공 응답의 내부 data를 반환하도록 맞추세요.

현재 data는 Axios 응답 본문 전체({ status, code, message, data })라서 이 함수의 성공값이 null이 아니라 응답 envelope입니다. mutation 결과를 사용하는 쪽에서 계약이 어긋나지 않도록 Promise<null>로 명시하고 내부 data를 반환하세요. 가능하면 기존 src/features/watchlist/api/fetch-watchlist.ts:15-19처럼 공통 응답 스키마 파싱도 함께 맞추면 좋습니다.

♻️ 반환 계약 정리 예시
-export async function changePassword(request: ChangePasswordRequest) {
+export async function changePassword(request: ChangePasswordRequest): Promise<null> {
   const { data } = await baseApi.post<ApiResponse<null>>('/auth/password/change', request)
-  return data
+  return data.data
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/auth-dialog/api/change-password.ts` around lines 15 - 18, The
changePassword function currently returns the full Axios envelope
(ApiResponse<null>) instead of the inner payload; update changePassword(request:
ChangePasswordRequest) to return Promise<null> and, after calling
baseApi.post<ApiResponse<null>>('/auth/password/change', request), return the
inner response data (e.g., response.data.data) rather than the whole envelope;
ensure the signature and return value match the pattern used in fetch-watchlist
(parsing ApiResponse<T> and returning its .data field) so callers receive null
on success.
index.html (1)

8-8: 결제 SDK 로드를 초기 렌더링 경로에서 분리해 주세요.

현재 head의 일반 <script>라 Toss CDN이 느리거나 실패하면 앱 파싱/렌더링이 막힐 수 있습니다. 결제 기능은 특정 버튼 클릭 시에만 필요하므로 최소한 defer를 붙이거나, 더 좋게는 결제 요청 시점에 지연 로드하는 방식이 안전합니다.

♻️ 최소 변경 예시
-    <script src="https://js.tosspayments.com/v1/payment"></script>
+    <script defer src="https://js.tosspayments.com/v1/payment"></script>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.html` at line 8, The inline loading of the Toss Payments SDK via
<script src="https://js.tosspayments.com/v1/payment"></script> blocks initial
rendering; change this by either adding defer to that script or, preferably,
remove the immediate script tag and implement a lazy loader that injects that
src only when payment is needed (for example in the payment button handler such
as handlePayClick or payButton click callback). Ensure the loader is idempotent
(check for an existing script element or window.TossPayments), returns a promise
that resolves when the SDK's script load event fires, and only then invokes the
payment initiation logic.
src/features/payment/model/payment.query.ts (1)

28-33: 비로그인 상태에서 useMySubscriptionQuery 자동 실행 가능성

useQueryenabled 조건이 없어 훅이 마운트되는 즉시 /payments/me/subscription을 호출합니다. 비로그인 상태에서도 호출되면 401이 발생하고 base-api.ts의 refresh 플로우가 불필요하게 동작할 수 있습니다. 인증 상태를 확인하는 enabled 옵션을 추가하는 것을 권장합니다.

♻️ 예시 수정
-export function useMySubscriptionQuery() {
+export function useMySubscriptionQuery(options?: { enabled?: boolean }) {
   return useQuery({
     queryKey: PAYMENT_QUERY_KEY.mySubscription,
     queryFn: getMySubscription,
-    retry: false
+    retry: false,
+    enabled: options?.enabled ?? true
   })
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/payment/model/payment.query.ts` around lines 28 - 33,
useMySubscriptionQuery currently calls useQuery immediately causing
/payments/me/subscription to run on mount even when unauthenticated; update the
hook to pass an enabled condition that checks authentication before executing.
Modify useMySubscriptionQuery to add the enabled option (e.g., enabled:
isAuthenticated or isLoggedIn) to the useQuery options so it only runs when the
user is logged in; reference the existing queryKey
PAYMENT_QUERY_KEY.mySubscription and queryFn getMySubscription and ensure any
auth-check utility or state used by base-api.ts (token/refresh logic) is used to
derive the enabled flag.
src/features/payment/model/payment.schema.ts (2)

8-10: 단일값 enum에 대한 의견

provider: z.enum(['TOSS'])는 사실상 리터럴 상수이므로 z.literal('TOSS')가 의도를 더 명확히 드러냅니다. 향후 결제 수단이 추가될 예정이라면 현재 z.enum 형태를 유지해도 무방합니다.

Also applies to: 20-22

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/payment/model/payment.schema.ts` around lines 8 - 10, The
provider field currently uses z.enum(['TOSS']) which is a single constant;
replace it with z.literal('TOSS') to make the intent explicit (update the
provider schema entry and any other identical single-value enums elsewhere in
this file, e.g., the other provider occurrences around the bottom of the
schema). Keep z.enum for billingCycle and other multi-valued enums unchanged; if
you expect multiple providers in the future you may leave z.enum as-is instead
of switching to z.literal.

28-28: 날짜 필드에 ISO datetime 검증 적용 검토

requestedAt, approvedAt, subscriptionStartedAt, subscriptionExpiredAt, startedAt, expiredAt 모두 z.string()으로 선언되어 임의 문자열을 허용합니다. Zod 4의 z.iso.datetime()을 사용하면 포맷 검증이 강화되어 백엔드 변경에 의한 회귀를 조기에 잡을 수 있습니다.

♻️ 예시 수정
-  requestedAt: z.string()
+  requestedAt: z.iso.datetime()

Also applies to: 38-39

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/payment/model/payment.schema.ts` at line 28, Replace the loose
z.string() definitions for the date/time fields in the payment schema with Zod's
datetime validator so formats are enforced: update the properties requestedAt,
approvedAt, subscriptionStartedAt, subscriptionExpiredAt, startedAt, and
expiredAt in the schema (and the other occurrences referenced around the second
block at lines noted in the review) to use z.string().datetime() (or
z.iso.datetime() if using that Zod version) instead of plain z.string(),
ensuring any nullable/optional chaining (e.g., .optional()/.nullable()) is
preserved.
src/features/payment/api/payment.api.ts (1)

19-42: Zod 파싱 실패 시 예외 처리 전략 검토

parse()는 응답 스키마가 맞지 않으면 ZodError를 던져 React Query의 onError로 전달됩니다. 현재 소비자 쪽(subscribe-button.tsx, payment-success-page.tsx)은 일반 alert로만 처리되므로 디버깅이 어렵습니다. 다음 중 하나를 고려해 보세요.

  • safeParse 후 실패 시 로깅을 포함한 명시적 에러를 throw
  • 공통 헬퍼로 감싸서 ZodError → 사용자 친화적 메시지/Sentry 전송

또한 세 함수 모두 apiResponseSchema(...).parse(response.data).data 패턴이 반복되므로 공통 헬퍼(parseApiResponse(schema, response))로 묶으면 중복을 줄일 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/payment/api/payment.api.ts` around lines 19 - 42, The three API
functions (createPayment, confirmPayment, getMySubscription) repeat
apiResponseSchema(...).parse(response.data).data and currently let ZodError
bubble to callers; extract this into a shared helper parseApiResponse(schema,
response) that uses schema.safeParse(response.data) and on failure logs the
ZodError (and send to Sentry/monitoring) and then throws a clearer error (or
rethrows a wrapped Error with context such as function name and endpoint), then
update createPayment, confirmPayment, and getMySubscription to call
parseApiResponse(createPaymentResponseSchema, response) etc.; ensure the helper
returns the .data field on success so callers remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/router/app-router.tsx`:
- Line 119: Wrap the payment success route so only authenticated users can
access it: change the Route for path "payment/success" that currently renders
PaymentSuccessPage to render it inside your authentication wrapper (e.g.,
ProtectedRoute or whatever auth wrapper component your app uses). Specifically,
locate the Route with element={<PaymentSuccessPage />} and replace it so the
element is the auth wrapper component rendering PaymentSuccessPage (e.g.,
<ProtectedRoute><PaymentSuccessPage/></ProtectedRoute>), keeping the
failure/public routes unchanged.

In `@src/features/auth-dialog/api/reset-password.ts`:
- Around line 10-13: ResetPasswordRequest is missing the required verification
code field causing the server to never receive the reset code; add a "code:
string" property to the ResetPasswordRequest type definition (the exported
ResetPasswordRequest) so it matches the schema in
src/features/auth-dialog/model/reset-password.schema.ts and the HTTP payload
includes email, newPassword and code when calling the reset endpoint.
- Around line 15-16: The public password reset endpoint used in
resetPassword(request: ResetPasswordRequest) is missing from the refresh-token
interceptor's isAuthRequest exception list, causing 401s from
/auth/password/reset to trigger token refresh and mask the real error; update
the interceptor's exception list (the isAuthRequest logic in baseApi) to include
'/auth/password/reset' or change resetPassword to call the public API client
instead so the refresh flow is not invoked for this endpoint.

In `@src/features/payment/ui/payment-success-page.tsx`:
- Around line 14-54: The page currently requires a user click to run
handleConfirm; instead, invoke the same confirm flow once on component mount by
calling confirmPaymentMutation.mutate with the existing payload (paymentKey,
orderId, amount) inside a useEffect in the PaymentSuccessPage component and
reuse the validation logic from handleConfirm (check
paymentKey/orderId/amountParam and amount numeric >0), and to prevent double
execution in React Strict Mode add a useRef flag (e.g., confirmStartedRef)
checked/set inside the effect so mutate runs only once; keep the existing
onSuccess/onError handlers as-is and still navigate on failure/success.

In `@src/features/payment/ui/subscribe-button.tsx`:
- Around line 1-35: The button only uses isPending (from
useCreatePaymentMutation) which is cleared after mutateAsync resolves, allowing
clicks while requestTossPayment is still in progress; add a local busy state
(e.g., isBusy via useState) in SubscribeButton, set isBusy = true before calling
mutateAsync and keep it true through the call to requestTossPayment (use
try/finally around mutateAsync and requestTossPayment in handleClick to reset
isBusy on completion/error), and update the button disabled prop to
disabled={isPending || isBusy} so the whole payment flow (mutateAsync +
requestTossPayment) is protected from duplicate clicks.

In `@src/features/settings/ui/settings-profile-section.tsx`:
- Around line 185-235: The three password inputs (bound to currentPassword,
newPassword, newPasswordConfirm) need accessible label associations: add a
unique id to each input (e.g. currentPasswordInput, newPasswordInput,
newPasswordConfirmInput) and set the corresponding label's htmlFor to that id so
clicking the label focuses the matching <input>; update the <label> elements for
the inputs that render currentPassword, newPassword, and newPasswordConfirm to
use htmlFor and ensure each input includes the matching id attribute.
- Around line 23-30: When a user logs out, sensitive form state
(isPasswordFormOpen, currentPassword, newPassword, newPasswordConfirm, errors,
successMessage) must be cleared but avoid calling setState inside useEffect;
instead render with derived values that fallback when isLoggedIn is false.
Update the component to compute effective values (e.g. const
effectiveIsPasswordFormOpen = isLoggedIn ? isPasswordFormOpen : false and const
effectiveCurrentPassword = isLoggedIn ? currentPassword : '' etc.) and use those
effective* identifiers in the JSX and handlers so the UI shows cleared fields
for logged-out state without performing setState in an effect; ensure
changePasswordMutation usage and any submit handlers reference the effective
values or guard by isLoggedIn.

In `@src/features/settings/ui/settings-view.tsx`:
- Line 89: Replace the hardcoded numeric prop planId={1} on SubscribeButton with
a centrally defined named constant; create a constant or enum (e.g.,
SUBSCRIPTION_PLAN_ID or PLAN_ID.PRO) exported from a single settings/constants
module, update the SubscribeButton usage to pass that named constant
(SubscribeButton planId={SUBSCRIPTION_PLAN_ID.PRO} or similar), and import the
constant into settings-view.tsx so future plan ID changes are made in one place
rather than scattered literals.

In `@src/shared/type/toss-payments.d.ts`:
- Line 5: Window 인터페이스의 TossPayments 선언이 현재 필수로 되어 있어 guard 검사와 불일치하므로 이를
선택적(optional)으로 변경하세요: locate the Window.TossPayments declaration (symbol
TossPayments) in the toss-payments type file and change its signature to be
optional (e.g. TossPayments?: (clientKey: string) => { ... }) so TypeScript
reflects that the external SDK may be undefined and callers are forced to check
(if (!window.TossPayments)) before use.

---

Nitpick comments:
In `@index.html`:
- Line 8: The inline loading of the Toss Payments SDK via <script
src="https://js.tosspayments.com/v1/payment"></script> blocks initial rendering;
change this by either adding defer to that script or, preferably, remove the
immediate script tag and implement a lazy loader that injects that src only when
payment is needed (for example in the payment button handler such as
handlePayClick or payButton click callback). Ensure the loader is idempotent
(check for an existing script element or window.TossPayments), returns a promise
that resolves when the SDK's script load event fires, and only then invokes the
payment initiation logic.

In `@src/features/auth-dialog/api/change-password.ts`:
- Around line 15-18: The changePassword function currently returns the full
Axios envelope (ApiResponse<null>) instead of the inner payload; update
changePassword(request: ChangePasswordRequest) to return Promise<null> and,
after calling baseApi.post<ApiResponse<null>>('/auth/password/change', request),
return the inner response data (e.g., response.data.data) rather than the whole
envelope; ensure the signature and return value match the pattern used in
fetch-watchlist (parsing ApiResponse<T> and returning its .data field) so
callers receive null on success.

In `@src/features/payment/api/payment.api.ts`:
- Around line 19-42: The three API functions (createPayment, confirmPayment,
getMySubscription) repeat apiResponseSchema(...).parse(response.data).data and
currently let ZodError bubble to callers; extract this into a shared helper
parseApiResponse(schema, response) that uses schema.safeParse(response.data) and
on failure logs the ZodError (and send to Sentry/monitoring) and then throws a
clearer error (or rethrows a wrapped Error with context such as function name
and endpoint), then update createPayment, confirmPayment, and getMySubscription
to call parseApiResponse(createPaymentResponseSchema, response) etc.; ensure the
helper returns the .data field on success so callers remain unchanged.

In `@src/features/payment/model/payment.query.ts`:
- Around line 28-33: useMySubscriptionQuery currently calls useQuery immediately
causing /payments/me/subscription to run on mount even when unauthenticated;
update the hook to pass an enabled condition that checks authentication before
executing. Modify useMySubscriptionQuery to add the enabled option (e.g.,
enabled: isAuthenticated or isLoggedIn) to the useQuery options so it only runs
when the user is logged in; reference the existing queryKey
PAYMENT_QUERY_KEY.mySubscription and queryFn getMySubscription and ensure any
auth-check utility or state used by base-api.ts (token/refresh logic) is used to
derive the enabled flag.

In `@src/features/payment/model/payment.schema.ts`:
- Around line 8-10: The provider field currently uses z.enum(['TOSS']) which is
a single constant; replace it with z.literal('TOSS') to make the intent explicit
(update the provider schema entry and any other identical single-value enums
elsewhere in this file, e.g., the other provider occurrences around the bottom
of the schema). Keep z.enum for billingCycle and other multi-valued enums
unchanged; if you expect multiple providers in the future you may leave z.enum
as-is instead of switching to z.literal.
- Line 28: Replace the loose z.string() definitions for the date/time fields in
the payment schema with Zod's datetime validator so formats are enforced: update
the properties requestedAt, approvedAt, subscriptionStartedAt,
subscriptionExpiredAt, startedAt, and expiredAt in the schema (and the other
occurrences referenced around the second block at lines noted in the review) to
use z.string().datetime() (or z.iso.datetime() if using that Zod version)
instead of plain z.string(), ensuring any nullable/optional chaining (e.g.,
.optional()/.nullable()) is preserved.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2ff6810e-0b09-4617-ab5c-21e9b3029dee

📥 Commits

Reviewing files that changed from the base of the PR and between 5d84097 and becdf33.

📒 Files selected for processing (19)
  • .env.example
  • index.html
  • src/app/router/app-router.tsx
  • src/features/auth-dialog/api/change-password.ts
  • src/features/auth-dialog/api/reset-password.ts
  • src/features/auth-dialog/model/change-password.schema.ts
  • src/features/auth-dialog/model/reset-password.schema.ts
  • src/features/auth-dialog/model/use-change-password-mutation.ts
  • src/features/auth-dialog/model/use-reset-password-mutation.ts
  • src/features/payment/api/payment.api.ts
  • src/features/payment/lib/request-toss-payment.ts
  • src/features/payment/model/payment.query.ts
  • src/features/payment/model/payment.schema.ts
  • src/features/payment/ui/payment-fail-page.tsx
  • src/features/payment/ui/payment-success-page.tsx
  • src/features/payment/ui/subscribe-button.tsx
  • src/features/settings/ui/settings-profile-section.tsx
  • src/features/settings/ui/settings-view.tsx
  • src/shared/type/toss-payments.d.ts
💤 Files with no reviewable changes (1)
  • .env.example

Comment thread src/app/router/app-router.tsx Outdated
Comment thread src/features/auth-dialog/api/reset-password.ts
Comment thread src/features/auth-dialog/api/reset-password.ts
Comment thread src/features/payment/ui/payment-success-page.tsx Outdated
Comment thread src/features/payment/ui/subscribe-button.tsx Outdated
Comment on lines +23 to +30
const [isPasswordFormOpen, setIsPasswordFormOpen] = useState(false)
const [currentPassword, setCurrentPassword] = useState('')
const [newPassword, setNewPassword] = useState('')
const [newPasswordConfirm, setNewPasswordConfirm] = useState('')
const [errors, setErrors] = useState<PasswordFormErrors>({})
const [successMessage, setSuccessMessage] = useState('')

const changePasswordMutation = useChangePasswordMutation()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

로그아웃 시 비밀번호 입력 상태를 즉시 지우세요.

isLoggedInfalse가 되면 폼은 숨겨지지만 isPasswordFormOpen과 비밀번호 입력값은 그대로 남습니다. 같은 컴포넌트 인스턴스에서 다시 로그인하면 이전 사용자의 비밀번호 입력값이 다시 노출될 수 있습니다.

🛡️ 인증 상태 변경 시 민감 상태 초기화 예시
   const [errors, setErrors] = useState<PasswordFormErrors>({})
   const [successMessage, setSuccessMessage] = useState('')
+  const [previousIsLoggedIn, setPreviousIsLoggedIn] = useState(isLoggedIn)
+
+  if (previousIsLoggedIn !== isLoggedIn) {
+    setPreviousIsLoggedIn(isLoggedIn)
+
+    if (!isLoggedIn) {
+      setIsPasswordFormOpen(false)
+      setCurrentPassword('')
+      setNewPassword('')
+      setNewPasswordConfirm('')
+      setErrors({})
+      setSuccessMessage('')
+    }
+  }

Based on learnings, 이 코드베이스에서는 react-hooks/set-state-in-effect 규칙 때문에 prop/state 동기화를 useEffectsetState로 처리하지 말고 previous value를 state에 두고 render 중 조건부로 조정하는 패턴을 사용해야 합니다.

Also applies to: 181-182

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/settings/ui/settings-profile-section.tsx` around lines 23 - 30,
When a user logs out, sensitive form state (isPasswordFormOpen, currentPassword,
newPassword, newPasswordConfirm, errors, successMessage) must be cleared but
avoid calling setState inside useEffect; instead render with derived values that
fallback when isLoggedIn is false. Update the component to compute effective
values (e.g. const effectiveIsPasswordFormOpen = isLoggedIn ? isPasswordFormOpen
: false and const effectiveCurrentPassword = isLoggedIn ? currentPassword : ''
etc.) and use those effective* identifiers in the JSX and handlers so the UI
shows cleared fields for logged-out state without performing setState in an
effect; ensure changePasswordMutation usage and any submit handlers reference
the effective values or guard by isLoggedIn.

Comment on lines +185 to +235
<label className="mb-1 block text-sm font-medium text-wefin-text">
현재 비밀번호
</label>
<input
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
placeholder="현재 비밀번호 입력"
className={`h-10 w-full max-w-[320px] rounded-lg border bg-white px-3 text-sm text-wefin-text outline-none transition-colors placeholder:text-wefin-subtle focus:border-wefin-mint ${
errors.currentPassword ? 'border-red-400' : 'border-wefin-line'
}`}
/>
{errors.currentPassword && (
<p className="mt-1 text-xs text-red-500">{errors.currentPassword}</p>
)}
</div>

<div>
<label className="mb-1 block text-sm font-medium text-wefin-text">
새 비밀번호
</label>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="새 비밀번호 입력"
className={`h-10 w-full max-w-[320px] rounded-lg border bg-white px-3 text-sm text-wefin-text outline-none transition-colors placeholder:text-wefin-subtle focus:border-wefin-mint ${
errors.newPassword ? 'border-red-400' : 'border-wefin-line'
}`}
/>
<p className="mt-1 text-xs text-wefin-subtle">
8~20자, 영문과 숫자를 포함해야 합니다.
</p>
{errors.newPassword && (
<p className="mt-1 text-xs text-red-500">{errors.newPassword}</p>
)}
</div>

<div>
<label className="mb-1 block text-sm font-medium text-wefin-text">
새 비밀번호 확인
</label>
<input
type="password"
value={newPasswordConfirm}
onChange={(e) => setNewPasswordConfirm(e.target.value)}
placeholder="새 비밀번호 다시 입력"
className={`h-10 w-full max-w-[320px] rounded-lg border bg-white px-3 text-sm text-wefin-text outline-none transition-colors placeholder:text-wefin-subtle focus:border-wefin-mint ${
errors.newPasswordConfirm ? 'border-red-400' : 'border-wefin-line'
}`}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

비밀번호 입력 label을 실제 input과 연결하세요.

현재 <label>htmlFor가 없고 <input>에도 대응되는 id가 없어 보조기술과 label 클릭 포커스가 제대로 동작하지 않습니다. 세 비밀번호 입력 모두 고유 idhtmlFor를 연결해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/settings/ui/settings-profile-section.tsx` around lines 185 -
235, The three password inputs (bound to currentPassword, newPassword,
newPasswordConfirm) need accessible label associations: add a unique id to each
input (e.g. currentPasswordInput, newPasswordInput, newPasswordConfirmInput) and
set the corresponding label's htmlFor to that id so clicking the label focuses
the matching <input>; update the <label> elements for the inputs that render
currentPassword, newPassword, and newPasswordConfirm to use htmlFor and ensure
each input includes the matching id attribute.

{active === 'subscription' && (
<div className="space-y-4">
<SettingsSubscriptionSection />
<SubscribeButton planId={1} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: 결제 플랜 ID가 중앙 상수/설정/플랜 데이터에서 관리되는지 확인합니다.
# Expect: planId 숫자 리터럴이 아닌 명명된 플랜 매핑 또는 서버/상태 기반 선택값 사용.
rg -nP -C3 '\b(planId|createPayment|planName|SubscribeButton)\b'

Repository: S0L-V/wefin-fe

Length of output: 7458


🏁 Script executed:

# 플랜 관련 상수, 설정, 데이터 모델 찾기
rg -i '(plan|subscription)' --type ts --type tsx -B2 -A2 | grep -E '(const|export|PLAN|Plan|config)' | head -40

Repository: S0L-V/wefin-fe

Length of output: 85


🏁 Script executed:

# features/payment 디렉토리 구조 확인
find src/features/payment -type f \( -name "*.ts" -o -name "*.tsx" \) | sort

Repository: S0L-V/wefin-fe

Length of output: 375


🏁 Script executed:

# 다른 곳에서 planId 사용되는 모든 위치
rg 'planId' -B3 -A3 --type ts --type tsx

Repository: S0L-V/wefin-fe

Length of output: 85


🏁 Script executed:

# 모든 planId 관련 코드 확인 (파일타입 필터 제거)
rg 'planId' -B2 -A2

Repository: S0L-V/wefin-fe

Length of output: 3237


🏁 Script executed:

# 플랜 관련 상수나 설정 찾기
rg -i 'plan' src/features/ | grep -iE '(const|export|config|PLAN)' | head -30

Repository: S0L-V/wefin-fe

Length of output: 1849


🏁 Script executed:

# subscribe-button.tsx 전체 내용 확인
cat src/features/payment/ui/subscribe-button.tsx

Repository: S0L-V/wefin-fe

Length of output: 1069


🏁 Script executed:

# settings-view.tsx 전체 내용 확인
cat src/features/settings/ui/settings-view.tsx

Repository: S0L-V/wefin-fe

Length of output: 3580


플랜 ID를 중앙 상수로 분리하세요.

planId={1}이 하드코딩되어 있고, 이 값이 결제 API로 직접 전달됩니다. 플랜이 추가되거나 백엔드 ID가 변경되면 이 값을 여러 위치에서 찾아 수정해야 합니다. 플랜 ID를 명명된 상수(예: PLAN_ID.PRO, SUBSCRIPTION_PLAN_ID)로 정의하여 중앙에서 관리하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/settings/ui/settings-view.tsx` at line 89, Replace the hardcoded
numeric prop planId={1} on SubscribeButton with a centrally defined named
constant; create a constant or enum (e.g., SUBSCRIPTION_PLAN_ID or PLAN_ID.PRO)
exported from a single settings/constants module, update the SubscribeButton
usage to pass that named constant (SubscribeButton
planId={SUBSCRIPTION_PLAN_ID.PRO} or similar), and import the constant into
settings-view.tsx so future plan ID changes are made in one place rather than
scattered literals.

Comment thread src/shared/type/toss-payments.d.ts Outdated

@cl-o-lc cl-o-lc 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.

Zod 스키마로 API 응답을 검증한 부분과, React Query 컨벤션이 통일된 점이 좋았습니다!
payment-fail-page에서 Toss가 보내주는 code/message 쿼리 파라미터를 그대로 노출시켜서, 디버깅이 용이할 것 같아요 👍

구현 수고하셨습니다! 코멘트 확인 부탁드려요 🏃

Comment on lines +10 to +12
export type ResetPasswordRequest = {
email: string
newPassword: string

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.

ResetPasswordRequest 타입에 code 필드가 누락되어 있습니다!
'newPassword: string` 추가를 해야 기능이 작동할 것 같아요 👀

Comment thread src/features/auth-dialog/api/reset-password.ts
Comment on lines +3 to +8
type ApiResponse<T> = {
status: number
code: string
message: string
data: T
}

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.

ApiResponse 타입이 파일마다 중복 정의됩니다.
change-password.ts, reset-password.ts 모두 ApiResponse 타입을 각자 선언하고 있는데, src/shared/api/에 공통 타입을 두고 import하면 어떨까요?
추후 리팩토링 단계에서 적용하셔도 됩니다 👍

@@ -0,0 +1,18 @@
import { z } from 'zod'

export const changePasswordSchema = z

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.

changePasswordSchema와 resetPasswordSchema가 중복입니다.
두 스키마 모두 동일한 newPassword 규칙(min(8).max(20).regex(...))과 newPasswordConfirm + refine을 가지고 있으니,
공통 스키마를 passwordFieldsSchema로 추출하고 .extend()로 합성하면 좋을 것 같습니다!
(이것도 마찬가지로 리팩토링 단계에 적용하셔도 됩니다!)

Comment thread src/features/payment/ui/payment-success-page.tsx Outdated
Comment thread src/features/payment/ui/payment-success-page.tsx Outdated
Comment thread index.html Outdated

@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.

🧹 Nitpick comments (1)
src/shared/api/base-api.ts (1)

110-115: 의도된 변경사항으로 확인되며, 로직상 문제 없음.

/auth/password/reset/auth/email-verifications는 로그인되지 않은 사용자가 호출하는 엔드포인트이므로 401 발생 시 refresh 재시도에서 제외하는 것이 타당합니다. /auth/password/change는 목록에 포함되지 않아 인증이 필요한 비밀번호 변경 요청은 정상적으로 refresh 흐름을 타게 되는 점도 올바릅니다.

다만 includes 기반 문자열 매칭이 5개로 늘어나면서 가독성이 떨어지고, 향후 엔드포인트가 추가될수록 누락/오탐(예: 경로에 유사 문자열이 포함된 다른 API)이 발생하기 쉽습니다. 배열 + some으로 정리해두면 확장이 수월합니다.

♻️ 제안 리팩터링
+const AUTH_REQUEST_PATHS = [
+  '/auth/login',
+  '/auth/signup',
+  '/auth/refresh',
+  '/auth/password/reset',
+  '/auth/email-verifications'
+] as const
+
...
-    const isAuthRequest =
-      requestUrl.includes('/auth/login') ||
-      requestUrl.includes('/auth/signup') ||
-      requestUrl.includes('/auth/refresh') ||
-      requestUrl.includes('/auth/password/reset') ||
-      requestUrl.includes('/auth/email-verifications')
+    const isAuthRequest = AUTH_REQUEST_PATHS.some((path) => requestUrl.includes(path))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/api/base-api.ts` around lines 110 - 115, Replace the long chain of
requestUrl.includes(...) with an array-based check to improve readability and
reduce false positives: define a const publicAuthPaths =
['/auth/login','/auth/signup','/auth/refresh','/auth/password/reset','/auth/email-verifications']
and then set isAuthRequest = publicAuthPaths.some(p => requestUrl.includes(p))
(or, if you can access the URL pathname, compare against pathname with
startsWith/=== for stricter matching); update the code around the isAuthRequest
declaration to use these symbols (publicAuthPaths and isAuthRequest).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/shared/api/base-api.ts`:
- Around line 110-115: Replace the long chain of requestUrl.includes(...) with
an array-based check to improve readability and reduce false positives: define a
const publicAuthPaths =
['/auth/login','/auth/signup','/auth/refresh','/auth/password/reset','/auth/email-verifications']
and then set isAuthRequest = publicAuthPaths.some(p => requestUrl.includes(p))
(or, if you can access the URL pathname, compare against pathname with
startsWith/=== for stricter matching); update the code around the isAuthRequest
declaration to use these symbols (publicAuthPaths and isAuthRequest).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b0475ea8-f3c9-40e4-86b9-54ef0bcdc88a

📥 Commits

Reviewing files that changed from the base of the PR and between becdf33 and 9327a2d.

📒 Files selected for processing (2)
  • src/features/settings/ui/settings-view.tsx
  • src/shared/api/base-api.ts
✅ Files skipped from review due to trivial changes (1)
  • src/features/settings/ui/settings-view.tsx

@S0L-V S0L-V deleted a comment from coderabbitai Bot Apr 19, 2026
@kkhhmm3103
kkhhmm3103 marked this pull request as draft April 19, 2026 16:23
@kkhhmm3103 kkhhmm3103 closed this Apr 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

FEAT 새로운 기능 추가 또는 기존 기능 확장

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants