Skip to content

회원 가입 약관 매핑 최신화 - #445

Merged
shinwokkang merged 4 commits into
mainfrom
dev
Jun 30, 2026
Merged

shinwokkang merged 4 commits into
mainfrom
dev

Conversation

@shinwokkang

@shinwokkang shinwokkang commented Jun 29, 2026 •

Copy link
Copy Markdown
Contributor

📌 개요 (Summary)

  • 변경 사항에 대한 간략한 요약을 적어주세요.
  • 관련 이슈가 있다면 링크를 걸어주세요 (예: [fix] cicd / build 문제 #123).

🛠️ 변경 사항 (Changes)

  • 새로운 기능 추가
  • 버그 수정
  • 코드 리팩토링
  • 문서 업데이트
  • 기타 (설명: )

📸 스크린샷 (Screenshots)

(UI 변경 사항이 있다면 첨부해주세요)

✅ 체크리스트 (Checklist)

  • 빌드가 성공적으로 수행되었나요? (pnpm build)
  • 린트 에러가 없나요? (pnpm lint)
  • 불필요한 콘솔 로그나 주석을 제거했나요?

Summary by CodeRabbit

  • New Features

    • Added a dedicated terms-agreement flow during signup, including a modal for viewing full terms and a clearer loading/submission state.
    • Users can now review and save agreement selections before continuing, and completed agreements are included in signup requests.
  • Bug Fixes

    • Updated signup routing so the terms step no longer redirects unexpectedly.
    • Adjusted profile-completion routing for social signups to point to the new terms step.

@shinwokkang shinwokkang self-assigned this Jun 29, 2026
@shinwokkang shinwokkang added the ✨ feat 새로운 기능 추가 label Jun 29, 2026
@vercel

vercel Bot commented Jun 29, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
checkmo Ready Ready Preview, Comment Jun 29, 2026 1:06pm

@coderabbitai

coderabbitai Bot commented Jun 29, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Integrates a backend terms API into the signup flow. Adds Term-related TypeScript interfaces, two new MEMBER_ENDPOINTS, and three memberService methods. A new useTermsAgreement hook fetches and manages terms state (including social auto-advance). TermsAgreement is refactored to use the hook; a new TermsDetailModal renders term content. PasswordEntry now passes agreedTermsIds to authService.signup. Profile-completion routing is updated to target /signup/terms.

Changes

Terms Agreement Signup Integration

Layer / File(s) Summary
Terms types, endpoints, and service methods
src/types/auth.ts, src/lib/api/endpoints/member.ts, src/services/memberService.ts, src/contexts/SignupContext.tsx
Adds Term, TermsResponse, MemberTerm, MemberTermsStatus, UpdateAgreementsRequest interfaces; adds TERMS and MY_TERMS endpoint constants; adds getTerms, getMyTerms, saveMyTerms to memberService; updates SignupContext agreements key type from string to number.
useTermsAgreement hook
src/components/base-ui/Join/steps/TermsAgreement/useTermsAgreement.ts
Fetches terms on mount, initializes agreement state for normal and social flows, auto-advances social users when no required terms remain, and exposes handleNext, handleAgreementChange, handleAllAgreementChange, and derived flags.
TermsAgreement component and TermsDetailModal
src/components/base-ui/Join/steps/TermsAgreement/TermsAgreement.tsx, src/components/base-ui/Join/steps/TermsAgreement/TermsDetailModal.tsx
Refactors TermsAgreement to delegate to useTermsAgreement, shows loading state, renders term.title, and opens TermsDetailModal for detail view. TermsDetailModal renders content from a TERMS_CONTENT_MAP, an optional external link, and an agree button.
PasswordEntry + routing adjustments
src/components/base-ui/Join/steps/PasswordEntry/PasswordEntry.tsx, src/utils/profileCompletion.ts, src/app/(public)/signup/[step]/SignupStepPageClient.tsx
PasswordEntry computes and passes agreedTermsIds to authService.signup. Profile completion base route changed to /signup/terms. Step-page redirect no longer triggers on the "terms" step.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • 회원가입 약관 동의 매핑 BE#256: The PR implements the frontend side of the same signup/terms-consent flow described in this issue, including calls to /terms and /members/me/terms and saving agreements before advancing.

Possibly related PRs

  • checkmo2025/FE#388: Both PRs modify TermsAgreement.tsx to change how terms data and agreement selection are rendered.
  • checkmo2025/FE#420: Both PRs modify TermsAgreement.tsx's terms content and agreement rendering logic.
  • checkmo2025/FE#425: Both PRs modify MEMBER_ENDPOINTS in src/lib/api/endpoints/member.ts.

Suggested labels

♻️ refactor

Poem

🐇 Hop hop, the terms now come from the server's burrow,
No more hardcoded scrolls — fetched fresh and thorough.
Social bunnies skip ahead when all is agreed,
The modal opens wide so each term can be read.
IDs fly as numbers, the signup form complete —
A cleaner warren, tidy and neat! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: updating and refactoring signup terms agreement mapping.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

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.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request refactors the signup terms agreement flow to dynamically fetch and save terms from the backend instead of using hardcoded constants. It introduces a custom hook useTermsAgreement for managing state, a new TermsDetailModal component, and updates the signup API integration to pass numeric agreement IDs. While these changes greatly improve flexibility, there is a critical bug in the useTermsAgreement hook where including agreements in the useEffect dependency array can cause the UI to get permanently stuck in a loading state. Additionally, using non-null assertions (!) on API responses in memberService.ts poses a risk of runtime errors if the expected data is missing.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

return () => {
isMounted = false;
};
}, [isSocial, onNext, setAgreements, showToast, agreements]); // 이제 agreements를 의존성 배열에 넣어도 초기 호출 방어가 됨

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.

critical

critical: useEffect 의 의존성 배열에 agreements가 포함되어 있어 심각한 버그가 발생할 수 있습니다.

문제 원인

  1. agreements가 변경될 때마다 (예: 약관 동의 체크박스를 클릭하거나 setAgreements가 호출될 때) useEffect가 다시 실행됩니다.
  2. React는 새 이펙트를 실행하기 전에 이전 이펙트의 정리(cleanup) 함수를 실행합니다. 이 정리 함수는 isMounted = false로 설정합니다.
  3. 하지만 새 이펙트가 실행될 때 hasFetchedRef.current가 true이므로 즉시 return하여 아무 작업도 하지 않고 새로운 정리 함수도 등록하지 않습니다.
  4. 결과적으로, 이미 진행 중이던 비동기 작업(fetchTerms)이 완료되어도 isMounted가 false로 변경되어 있기 때문에 setTermsData, setAgreements, setIsFetching(false) 등의 상태 업데이트가 모두 무시됩니다.
  5. 이로 인해 화면이 "약관을 불러오는 중입니다..." 상태에서 영원히 멈추는 현상이 발생할 수 있습니다.

해결 방법

agreements는 마운트 시점에 초기화 여부를 판단할 때만 필요하므로, useEffect 의 의존성 배열에서 agreements를 제거해야 합니다. 마운트 시점의 최신 agreements 값을 안전하게 참조하기 위해 useRef를 사용하거나, 의존성 배열에서 단순히 제거할 수 있습니다.

Suggested change
}, [isSocial, onNext, setAgreements, showToast, agreements]); // 이제 agreements를 의존성 배열에 넣어도 초기 호출 방어가 됨
}, [isSocial, onNext, setAgreements, showToast]);

Comment on lines +188 to +196
getTerms: async (): Promise<TermsResponse> => {
const response = await apiClient.get<ApiResponse<TermsResponse>>(
MEMBER_ENDPOINTS.TERMS
);
if (!response.isSuccess) {
throw new Error(response.message || "약관을 불러오는 데 실패했습니다.");
}
return response.result!;
},

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.

medium

medium: response.result!와 같이 non-null assertion (!)을 사용하고 있습니다. 만약 API가 성공(isSuccess: true)을 반환했지만 어떤 이유로든 result 필드가 누락되거나 null인 경우, 이를 구조 분해 할당하는 호출부(const { terms } = ...)에서 런타임 에러가 발생할 수 있습니다.

안전한 방어적 프로그래밍을 위해 result 존재 여부를 함께 검증하도록 개선하는 것을 권장합니다.

Suggested change
getTerms: async (): Promise<TermsResponse> => {
const response = await apiClient.get<ApiResponse<TermsResponse>>(
MEMBER_ENDPOINTS.TERMS
);
if (!response.isSuccess) {
throw new Error(response.message || "약관을 불러오는 데 실패했습니다.");
}
return response.result!;
},
getTerms: async (): Promise<TermsResponse> => {
const response = await apiClient.get<ApiResponse<TermsResponse>>(
MEMBER_ENDPOINTS.TERMS
);
if (!response.isSuccess || !response.result) {
throw new Error(response.message || "약관을 불러오는 데 실패했습니다.");
}
return response.result;
},

Comment on lines +197 to +205
getMyTerms: async (): Promise<MemberTermsStatus> => {
const response = await apiClient.get<ApiResponse<MemberTermsStatus>>(
MEMBER_ENDPOINTS.MY_TERMS
);
if (!response.isSuccess) {
throw new Error(response.message || "내 약관 동의 상태를 불러오는 데 실패했습니다.");
}
return response.result!;
},

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.

medium

medium: response.result!와 같이 non-null assertion (!)을 사용하고 있습니다. 만약 API가 성공(isSuccess: true)을 반환했지만 어떤 이유로든 result 필드가 누락되거나 null인 경우, 이를 참조하는 호출부에서 런타임 에러가 발생할 수 있습니다.

안전한 방어적 프로그래밍을 위해 result 존재 여부를 함께 검증하도록 개선하는 것을 권장합니다.

Suggested change
getMyTerms: async (): Promise<MemberTermsStatus> => {
const response = await apiClient.get<ApiResponse<MemberTermsStatus>>(
MEMBER_ENDPOINTS.MY_TERMS
);
if (!response.isSuccess) {
throw new Error(response.message || "내 약관 동의 상태를 불러오는 데 실패했습니다.");
}
return response.result!;
},
getMyTerms: async (): Promise<MemberTermsStatus> => {
const response = await apiClient.get<ApiResponse<MemberTermsStatus>>(
MEMBER_ENDPOINTS.MY_TERMS
);
if (!response.isSuccess || !response.result) {
throw new Error(response.message || "내 약관 동의 상태를 불러오는 데 실패했습니다.");
}
return response.result;
},

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

🧹 Nitpick comments (1)
src/types/auth.ts (1)

46-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use this exported Term as the single contract.

src/components/base-ui/Join/steps/TermsAgreement/TermsDetailModal.tsx:46-53 still defines its own Term shape. Importing this type there will prevent the modal and hook from drifting the next time the backend contract changes.

🤖 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/types/auth.ts` around lines 46 - 53, The modal is duplicating the Term
contract instead of using the shared exported type, which can cause drift.
Update TermsDetailModal to import and use the Term interface from the auth types
module instead of redefining a local shape, and make sure any related props or
hook return values reference that same Term symbol consistently.
🤖 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/app/`(public)/signup/[step]/SignupStepPageClient.tsx:
- Around line 55-57: The terms step is rendering before social mode is known,
which causes `useTermsAgreement` to fetch the wrong path and skip the later
social flow. Update `SignupStepPageClient` so `/signup/terms` is not mounted
until `setIsSocial(true)` has completed, using the existing step handling in
`SignupStepPageClient` and `useTermsAgreement` to gate rendering or redirect
away from `terms` until the social state is resolved.

In `@src/components/base-ui/Join/steps/TermsAgreement/TermsAgreement.tsx`:
- Around line 31-39: `TermsAgreement` currently only handles the `isFetching`
state, so when `useTermsAgreement` finishes after a `getTerms()` failure the
screen renders an empty checklist with a disabled next button. Update
`TermsAgreement` and the `useTermsAgreement` flow to track a recoverable error
state from `getTerms()`, then render an error UI with retry and/or back
navigation instead of falling through. Make sure the loading branch, checklist
rendering, and the `handleNext`/button disabled logic all respect this new error
state so signup can recover from transient API failures.

In `@src/components/base-ui/Join/steps/TermsAgreement/TermsDetailModal.tsx`:
- Around line 9-14: The consent modal currently looks up copy only through
TERMS_CONTENT_MAP by termsType, so it can render stale text when a backend
revision changes selectedTerm.version. Update TermsDetailModal to resolve the
displayed content using both selectedTerm.termsType and selectedTerm.version,
and make sure the versioned backend content is what gets shown for the consent
flow rather than the local constant. Use the existing TermsDetailModal logic and
TERMS_CONTENT_MAP lookup as the place to wire in the version-aware selection.
- Around line 28-31: Add an accessible name to the icon-only close control in
TermsDetailModal by updating the button that uses onClick={onClose} so screen
readers can announce its purpose. Use an aria-label on that close button in the
TermsDetailModal component, keeping the existing visual icon and behavior
unchanged.

In `@src/services/memberService.ts`:
- Around line 188-205: Both memberService methods, getTerms and getMyTerms,
assume ApiResponse.result is always present by forcing it with non-null
assertions, which can crash consumers like useTermsAgreement if the backend
returns success without data. Update these methods to validate response.result
after the isSuccess check and throw a clear error when it is missing, instead of
returning it directly; keep the existing response.message fallback and ensure
the returned value is only used when result is actually defined.

---

Nitpick comments:
In `@src/types/auth.ts`:
- Around line 46-53: The modal is duplicating the Term contract instead of using
the shared exported type, which can cause drift. Update TermsDetailModal to
import and use the Term interface from the auth types module instead of
redefining a local shape, and make sure any related props or hook return values
reference that same Term symbol consistently.
🪄 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

Run ID: dca94228-00de-4c53-a641-4b32b1fb1e63

📥 Commits

Reviewing files that changed from the base of the PR and between 5f2f68c and 480b95d.

📒 Files selected for processing (10)
  • src/app/(public)/signup/[step]/SignupStepPageClient.tsx
  • src/components/base-ui/Join/steps/PasswordEntry/PasswordEntry.tsx
  • src/components/base-ui/Join/steps/TermsAgreement/TermsAgreement.tsx
  • src/components/base-ui/Join/steps/TermsAgreement/TermsDetailModal.tsx
  • src/components/base-ui/Join/steps/TermsAgreement/useTermsAgreement.ts
  • src/contexts/SignupContext.tsx
  • src/lib/api/endpoints/member.ts
  • src/services/memberService.ts
  • src/types/auth.ts
  • src/utils/profileCompletion.ts

Comment on lines +55 to 57
if (step === "email" || step === "password") {
router.replace(PROFILE_COMPLETION_ROUTE);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don’t render the terms step before social mode is resolved.

Removing the redirect here means /signup/terms now mounts once before setIsSocial(true) finishes. useTermsAgreement only fetches once (hasFetchedRef), so that first non-social render prevents the later social path from loading getMyTerms() or auto-advancing. Existing social users can end up redoing agreements that the backend already marks as complete.

🤖 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/app/`(public)/signup/[step]/SignupStepPageClient.tsx around lines 55 -
57, The terms step is rendering before social mode is known, which causes
`useTermsAgreement` to fetch the wrong path and skip the later social flow.
Update `SignupStepPageClient` so `/signup/terms` is not mounted until
`setIsSocial(true)` has completed, using the existing step handling in
`SignupStepPageClient` and `useTermsAgreement` to gate rendering or redirect
away from `terms` until the social state is resolved.

Comment on lines +31 to 39
if (isFetching) {
return (
<JoinLayout title="약관 동의">
<div className="flex flex-col items-center w-full">
{/* Detailed Box: Responsive width and padding */}
<div className="relative flex flex-col items-center justify-between w-[272px] t:w-[654px] h-[580px] t:h-[645px] max-h-[80vh] px-[12px] py-[30px] t:py-[80px] rounded-[8px] border border-[#BBAA9B] bg-white shadow-[2px_4px_4px_0_rgba(0,0,0,0.25)] transition-all">
<button
onClick={() => setSelectedTermId(null)}
className="absolute right-[12px] top-[12px] w-[24px] h-[24px] flex items-center justify-center text-Gray-4 hover:text-Gray-6 transition-colors z-10"
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
<div className="flex flex-col w-full h-full overflow-hidden">
<div className="flex-1 overflow-y-auto px-[8px] t:px-[64px] custom-scrollbar">
<div className="flex flex-col w-full gap-6">
{termData.map((term, i) => (
<div key={i} className="flex flex-col gap-2">
<h3 className="text-[16px] font-semibold text-Gray-7">{term.title}</h3>
{Array.isArray(term.content) ? (
<ul className="flex list-disc flex-col gap-1 pl-5 text-[14px] text-Gray-5">
{term.content.map((item, idx) => (
<li key={idx}>{item}</li>
))}
</ul>
) : (
<div className="flex flex-col gap-1 text-[14px] text-Gray-5">
{term.content.split("\n").map((line, idx) => (
<p key={idx}>{line.trim()}</p>
))}
</div>
)}
</div>
))}
</div>
</div>
</div>
<JoinButton
onClick={() => {
handleAgreementChange(selectedTermId, true);
setSelectedTermId(null);
}}
className="w-full t:w-[526px] h-[48px] px-[16px] py-[12px] mt-[20px] t:mt-[24px] shrink-0 font-sans text-[14px] font-semibold leading-[145%] tracking-[-0.014px]"
>
동의
</JoinButton>
</div>
<p className="text-Gray-5">약관을 불러오는 중입니다...</p>
</div>
</JoinLayout>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Render a recoverable error state when terms loading fails.

useTermsAgreement clears isFetching even after getTerms() throws, so this screen falls through to an empty checklist with a permanently disabled “다음” button. A transient API error now dead-ends signup instead of offering retry/back navigation.

Also applies to: 60-123

🤖 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/components/base-ui/Join/steps/TermsAgreement/TermsAgreement.tsx` around
lines 31 - 39, `TermsAgreement` currently only handles the `isFetching` state,
so when `useTermsAgreement` finishes after a `getTerms()` failure the screen
renders an empty checklist with a disabled next button. Update `TermsAgreement`
and the `useTermsAgreement` flow to track a recoverable error state from
`getTerms()`, then render an error UI with retry and/or back navigation instead
of falling through. Make sure the loading branch, checklist rendering, and the
`handleNext`/button disabled logic all respect this new error state so signup
can recover from transient API failures.

Comment on lines +9 to +14
const TERMS_CONTENT_MAP: Record<string, { title: string; content: string | string[] }[]> = {
PRIVACY_COLLECTION: PRIVACY_DATA,
SERVICE_TERMS: TERMS_OF_USE_DATA,
THIRD_PARTY_PROVISION: THIRD_PARTY_DATA,
MARKETING: MARKETING_DATA,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Don’t key the consent copy only by termsType.

This modal ignores selectedTerm.version, so a backend revision can keep the same termsType while this screen still renders the old local constant. In that case the user is reading stale text but later consenting to the new term id/version, which breaks the consent trail.

Also applies to: 23-24

🤖 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/components/base-ui/Join/steps/TermsAgreement/TermsDetailModal.tsx` around
lines 9 - 14, The consent modal currently looks up copy only through
TERMS_CONTENT_MAP by termsType, so it can render stale text when a backend
revision changes selectedTerm.version. Update TermsDetailModal to resolve the
displayed content using both selectedTerm.termsType and selectedTerm.version,
and make sure the versioned backend content is what gets shown for the consent
flow rather than the local constant. Use the existing TermsDetailModal logic and
TERMS_CONTENT_MAP lookup as the place to wire in the version-aware selection.

Comment on lines +28 to +31
<button
onClick={onClose}
className="absolute right-[12px] top-[12px] w-[24px] h-[24px] flex items-center justify-center text-Gray-4 hover:text-Gray-6 transition-colors z-10"
>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add an accessible name to the close button.

The icon-only button is exposed as just “button” to screen readers. Please add an aria-label.

Proposed fix
         <button
           onClick={onClose}
+          aria-label="약관 상세 닫기"
           className="absolute right-[12px] top-[12px] w-[24px] h-[24px] flex items-center justify-center text-Gray-4 hover:text-Gray-6 transition-colors z-10"
         >
📝 Committable suggestion

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

Suggested change
<button
onClick={onClose}
className="absolute right-[12px] top-[12px] w-[24px] h-[24px] flex items-center justify-center text-Gray-4 hover:text-Gray-6 transition-colors z-10"
>
<button
onClick={onClose}
aria-label="약관 상세 닫기"
className="absolute right-[12px] top-[12px] w-[24px] h-[24px] flex items-center justify-center text-Gray-4 hover:text-Gray-6 transition-colors z-10"
>
🤖 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/components/base-ui/Join/steps/TermsAgreement/TermsDetailModal.tsx` around
lines 28 - 31, Add an accessible name to the icon-only close control in
TermsDetailModal by updating the button that uses onClick={onClose} so screen
readers can announce its purpose. Use an aria-label on that close button in the
TermsDetailModal component, keeping the existing visual icon and behavior
unchanged.

Comment on lines +188 to +205
getTerms: async (): Promise<TermsResponse> => {
const response = await apiClient.get<ApiResponse<TermsResponse>>(
MEMBER_ENDPOINTS.TERMS
);
if (!response.isSuccess) {
throw new Error(response.message || "약관을 불러오는 데 실패했습니다.");
}
return response.result!;
},
getMyTerms: async (): Promise<MemberTermsStatus> => {
const response = await apiClient.get<ApiResponse<MemberTermsStatus>>(
MEMBER_ENDPOINTS.MY_TERMS
);
if (!response.isSuccess) {
throw new Error(response.message || "내 약관 동의 상태를 불러오는 데 실패했습니다.");
}
return response.result!;
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Validate response.result before returning it.

ApiResponse.result is optional, but both methods force it with !. If the backend returns isSuccess: true without result, useTermsAgreement will crash when it destructures the response on the signup step.

Suggested fix
     getTerms: async (): Promise<TermsResponse> => {
         const response = await apiClient.get<ApiResponse<TermsResponse>>(
             MEMBER_ENDPOINTS.TERMS
         );
-        if (!response.isSuccess) {
+        if (!response.isSuccess || !response.result) {
             throw new Error(response.message || "약관을 불러오는 데 실패했습니다.");
         }
-        return response.result!;
+        return response.result;
     },
     getMyTerms: async (): Promise<MemberTermsStatus> => {
         const response = await apiClient.get<ApiResponse<MemberTermsStatus>>(
             MEMBER_ENDPOINTS.MY_TERMS
         );
-        if (!response.isSuccess) {
+        if (!response.isSuccess || !response.result) {
             throw new Error(response.message || "내 약관 동의 상태를 불러오는 데 실패했습니다.");
         }
-        return response.result!;
+        return response.result;
     },
📝 Committable suggestion

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

Suggested change
getTerms: async (): Promise<TermsResponse> => {
const response = await apiClient.get<ApiResponse<TermsResponse>>(
MEMBER_ENDPOINTS.TERMS
);
if (!response.isSuccess) {
throw new Error(response.message || "약관을 불러오는 데 실패했습니다.");
}
return response.result!;
},
getMyTerms: async (): Promise<MemberTermsStatus> => {
const response = await apiClient.get<ApiResponse<MemberTermsStatus>>(
MEMBER_ENDPOINTS.MY_TERMS
);
if (!response.isSuccess) {
throw new Error(response.message || "내 약관 동의 상태를 불러오는 데 실패했습니다.");
}
return response.result!;
},
getTerms: async (): Promise<TermsResponse> => {
const response = await apiClient.get<ApiResponse<TermsResponse>>(
MEMBER_ENDPOINTS.TERMS
);
if (!response.isSuccess || !response.result) {
throw new Error(response.message || "약관을 불러오는 데 실패했습니다.");
}
return response.result;
},
getMyTerms: async (): Promise<MemberTermsStatus> => {
const response = await apiClient.get<ApiResponse<MemberTermsStatus>>(
MEMBER_ENDPOINTS.MY_TERMS
);
if (!response.isSuccess || !response.result) {
throw new Error(response.message || "내 약관 동의 상태를 불러오는 데 실패했습니다.");
}
return response.result;
},
🤖 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/services/memberService.ts` around lines 188 - 205, Both memberService
methods, getTerms and getMyTerms, assume ApiResponse.result is always present by
forcing it with non-null assertions, which can crash consumers like
useTermsAgreement if the backend returns success without data. Update these
methods to validate response.result after the isSuccess check and throw a clear
error when it is missing, instead of returning it directly; keep the existing
response.message fallback and ensure the returned value is only used when result
is actually defined.

@shinwokkang
shinwokkang merged commit 2841e51 into main Jun 30, 2026
3 checks passed

This branch was successfully deployed

1 active deployment
Preview — 480b95da Deployed Jun 29, 2026 by vercel[bot]
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.

1 participant