Skip to content

[Feat] 이용약관 및 개인정보처리방침 모달, 페이지 구현#162

Merged
odukong merged 26 commits intodevfrom
feat/#159/onboarding-agree-modal
Mar 31, 2026
Merged

[Feat] 이용약관 및 개인정보처리방침 모달, 페이지 구현#162
odukong merged 26 commits intodevfrom
feat/#159/onboarding-agree-modal

Conversation

@odukong
Copy link
Copy Markdown
Collaborator

@odukong odukong commented Mar 22, 2026

✏️ Summary

📑 Tasks

이용약관과 개인정보처리방침의 텍스트는 온보딩 페이지, 약관 페이지 양쪽에서 공통으로 사용됩니다. 각각의 페이지에서 공통적으로 사용되는 내용이기 때문에, shared/config 하위에 policy-privacy-info.ts, policy-use-info.ts 파일로 분리하여 객체 형태로 구조화했습니다.

온보딩의 약관 모달 (Modal 컴포넌트 개선)

기존의 공통 컴포넌트 모달은 width, height가 고정된 상태로 공통 컴포넌트를 만들어두었기 때문에, 원하는 온보딩의 모달 UI를 구현하는데 <Modal>컴포넌트를 그대로 사용할 수 없다는 문제가 존재하였습니다. (온보딩의 모달은 상단 제목이 존재하고 모달 컨텐츠가 스크롤이 가능하다는 점도 차이가 있었습니다.)

스크린샷 2026-03-22 210616

이러한 문제점을 해결하기 위해 Modal 컴포넌트의 매개변수로 size 를 추가하였습니다.

interface ModalProps {
  children: ReactNode;
  autoPlay?: number;
  isOpen: boolean;
  onClose: () => void;
  size?: "default" | "auto";   
}

// 온보딩 약관 모달 호출 예시 (auto 속성이 추가됨)
modalStore.open(
  <PolicyModal type={type} onClose={() => modalStore.close(MODAL_ID)} />,
  undefined,
  undefined,
  MODAL_ID,
  "auto"
);

size 속성은 default, auto 두 가지 옵션을 적용할 수 있습니다.

  • default를 선택한 경우에는 기존 모달 사이즈(공통 컴포넌트 모달)인 width(60rem), height(46rem) 이 적용되며,
  • auto를 선택하면 width(auto), height(auto) 이 적용되어 내부 컨텐츠의 크기에 따라 유동적으로 사이즈를 조절할 수 있도록 css값을 설정하였습니다.
export const modalContent = recipe({
  variants: {
    size: {
      default: {
        width: "60rem",
        height: "46rem",
        padding: "1.5rem 1.6rem 4.8rem 1.6rem",
      },
      auto: {
        width: "auto",
        height: "auto",
        padding: "0",
      },
    },
  },
});

온보딩의 두 가지 약관 모달은 내부 텍스트 데이터를 제외하면, UI 구조가 모두 동일합니다.
따라서 type props (클릭된 약관 텍스트)을 받아 조건부로 컨텐츠(<UsePolicyContent /> 또는 <PrivacyPolicyContent />)를 렌더링하는 PolicyModal을 구현하여 중복 코드를 줄였습니다.

이용약관 개인정보처리방침
스크린샷 1 스크린샷 2
export const PolicyModal = ({ type, onClose }: modalProps) => {
  return (
    <div className={styles.wrapper}>
      <div className={styles.modalHeader}>
        <div>{type === "USE" ? "이용약관" : "개인정보처리방침"}</div>
        <div className={styles.buttonWrapper}>
          <Modal.XButton />
        </div>
      </div>
      <Modal.Content>
        <div className={styles.modalCotent}>
          {type === "USE" ? <UsePolicyContent /> : <PrivacyPolicyContent />}
        </div>
      </Modal.Content>
      <Modal.Buttons>
        <Button variant="primary" size="full" onClick={onClose}>
          확인
        </Button>
      </Modal.Buttons>
    </div>
  );
};

별도의 이용약관 및 개인정보처리방침 페이지 (policy-page.tsx)

온보딩에 사용되는 약관 컨텐츠(<UsePolicyContent />,<PrivacyPolicyContent />)가 페이지에서도 비슷한 형식으로 구현해야 하기 때문에, 해당 컨텐츠를 widget component으로 분리하여 단독 약관 페이지에서도 재활용할까 고민했습니다.

하지만, 약관 텍스트 뷰 자체가 범용적인 “컴포넌트”의 성격에 가깝지 않다고 생각이 들었고, 1. 온보딩의 약관 컨텐츠2. 페이지의 약관 컨텐츠에 적용되는 세부 스타일(gap, fontWeigth)에 있어서도 차이가 있었습니다.

따라서 억지로 UI widget 컴포넌트를 공유하기보다는, shared/config의 데이터만 공유하고 각 뷰의 목적과 디자인에 맞게 별도의 컴포넌트로 분리하여 렌더링하도록 구현했습니다.

스크린샷 2026-03-22 212747 스크린샷 2026-03-22 212727

👀 To Reviewer

Modal 컴포넌트 디자인 수정사항 반영

  • 큰 변경사항은 아니다보니, Modal 컴포넌트의 디자인 수정사항을 해당 이슈에서 함께 반영하였습니다.
    (현재 모달을 사용하는 작업 페이지들이 많다 보니 빠르게 merge되는 것도 중요하다고 생각했습니다!)
  • 디자인 요구사항으로, button medium 사이즈의 폰트 크기를 body_m_14로 조정하였습니다.
  • 또한 아이콘 요소가 모달에 추가되어 Icon 컴포넌트를 모달 구성 컴포넌트로 추가하였습니다.
image

📸 Screenshot

🔔 ETC

  • 가능하다면 해당 이슈에서 리프레쉬토큰 관련 문제도 수정할 예정입니다. (생각한 대로라면,, 크게 수정할 부분이 없어서..! 근데 생각한 해결안이 아니다..? 그럼 다른 이슈에서... )
    ▶️ 액세스 토큰 (/re-issued) 재발급 로직 수정하였습니다!!!!!!! 액세스 토큰 만료되면 쿠키에 있는 리프레쉬 토큰을 통해 새로운 액세스 토큰이 재발급되고, 토큰 에러로 실패한 요청이 재요청가게 됩니다!
image

Summary by CodeRabbit

  • 새로운 기능

    • 이용약관·개인정보처리방침 페이지 및 모달형 정책 열람 추가
    • 온보딩에 약관 동의 체크박스와 정책 모달 연결(모달에서 열람·확인 가능)
    • 정책 내용을 표·섹션 형태로 페이지와 모달에서 확인 가능
  • 개선

    • 모달에 크기 옵션(기본 / 자동) 추가로 표시 유연성 향상
    • 모달·버튼·아이콘·문구 업데이트로 안내성 및 일관성 개선

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 22, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

온보딩 동의 UI와 정책 전용 페이지(이용약관/개인정보처리방침)를 추가하고, 모달에 선택 가능한 크기 옵션("default" | "auto")을 도입했습니다. 정책 내용은 타입화된 상수로 중앙화되어 재사용됩니다.

Changes

Cohort / File(s) Summary
라우팅 및 정책 페이지
src/app/routes/paths.ts, src/app/routes/public-routes.tsx, src/pages/policy/policy-page.tsx, src/pages/policy/*
정책 경로(POLICY_USE, POLICY_PRIVACY) 추가 및 PolicyPage 라우트 등록. 모드별(USE/PRIVACY) 콘텐츠 분기와 페이지 및 스타일 컴포넌트 추가.
정책 콘텐츠 데이터
src/shared/config/index.ts, src/shared/config/policy-use-info.ts, src/shared/config/policy-privacy-info.ts
이용약관/개인정보처리방침을 타입 정의와 함께 상수로 추가하고 config에 재수출을 추가함.
온보딩 정책 모달 UI
src/features/onboarding/index.ts, src/features/onboarding/ui/policy-modal/*
PolicyModal, UsePolicyContent, PrivacyPolicyContent 및 관련 스타일 추가. 모달 헤더/본문/버튼 구조 구현.
온보딩 동의 섹션 및 검증
src/pages/onboarding/ui/agree-section.tsx, src/pages/onboarding/onboarding-page.tsx, src/features/onboarding/lib/onboarding-form.validator.ts, src/pages/onboarding/onboarding-page.css.ts
동의 체크박스 UI와 스타일 추가. isAgreed 상태 도입 및 온보딩 완료 검사에 포함. 클릭 시 정책 모달 오픈 연결.
모달 크기 옵션(공유)
src/app/providers/modal-provider.tsx, src/shared/model/store/modal.store.ts, src/shared/ui/modal/modal.css.ts, src/shared/ui/modal/modal.tsx
ModalItem/스토어/프로바이더에 `size?: "default"
모달 API/구성 변경
src/shared/ui/index.ts, src/shared/ui/modal/use-modal.ts
useModal 훅 제거 및 관련 re-export 삭제(중앙 modalStore로 제어 이동).
공용 모달·기본 컴포넌트 변경
src/shared/ui/modal/modal-basic.tsx, src/shared/ui/modal/modal.tsx
ModalBasicicon?: ReactNode 추가, Modalsize prop 추가, Modal.TitleGroup/Modal.Icon 등 서브컴포넌트 추가 및 Content API 단순화.
아이콘 리소스
src/shared/assets/icons/index.ts
AgreeCheckIcon, IconPen, IconWarn, IconTrash 등 아이콘 재내보내기 추가.
모달 관련 스타일·타이포 조정
src/shared/ui/button/button.css.ts, src/shared/ui/modal/modal.css.ts, src/pages/policy/policy-page.css.ts, src/features/onboarding/ui/policy-modal/policy-modal.css.ts
버튼 폰트 스타일 조정, 모달 CSS 구조 변경(TitleGroup, Icon 스타일, Content/Buttons padding 등), 정책 페이지/모달 전용 스타일 추가.
경험 관련 모달 카피·아이콘 변경
src/features/experience-detail/..., src/features/experience-matching/...
여러 확인/삭제/종료 모달의 카피, 버튼 레이블, 아이콘 추가 및 모달 콜백 흐름 조정(내부 동작 변경).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User as 사용자
  participant Agree as AgreeSection
  participant Store as modalStore
  participant Provider as ModalProvider
  participant Modal as Modal 컴포넌트
  participant Policy as PolicyModalContent

  User->>Agree: 약관/개인정보 링크 클릭
  Agree->>Store: open(content=PolicyModal(type), id="ONBOARD_MODAL", size="default")
  Store->>Provider: modal 상태 업데이트 (open)
  Provider->>Modal: render(isOpen=true, size="default", autoPlay=?, onClose=?)
  Modal->>Policy: 렌더 정책 콘텐츠 (Use/Privacy)
  User->>Modal: 확인 버튼 클릭
  Modal->>Store: close("ONBOARD_MODAL")
  Store->>Provider: modal 상태 업데이트 (closed)
  Provider->>Modal: 언마운트
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • hummingbbird
  • qowjdals23
  • u-zzn
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed 제목은 PR의 주요 변경사항(이용약관 및 개인정보처리방침 모달/페이지 구현)을 명확하게 요약합니다.
Linked Issues check ✅ Passed PR이 #159의 모든 체크리스트 항목을 완료했습니다: 온보딩 내 이용약관/개인정보처리방침 모달 추가, 별도 페이지 구현.
Out of Scope Changes check ✅ Passed PR의 모든 변경사항이 #159의 요구사항과 관련이 있으며, Modal 컴포넌트 개선도 이용약관 모달 구현을 지원하는 범위 내입니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed PR 설명이 템플릿의 모든 필수 섹션을 충족하며, 작업 내용, 기술적 결정사항, 스크린샷이 포함되어 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 22, 2026

🚀 빌드 결과

린트 검사 완료
빌드 성공

로그 확인하기
Actions 탭에서 자세히 보기

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 18

Caution

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

⚠️ Outside diff range comments (2)
src/app/providers/modal-provider.tsx (1)

8-13: ⚠️ Potential issue | 🟠 Major

모달 종료 시 저장된 onClose 콜백이 실행되지 않습니다.

현재 Provider의 ModalItem 타입에서 onClose가 빠져 있고(Line 8-13), 닫기 핸들러도 modalStore.close만 호출합니다(Line 36-37). modalStore.open(..., onClose, ...)로 전달한 콜백이 무시되어 정리 로직/후속 동작이 누락될 수 있습니다.

🐛 제안 수정안
 interface ModalItem {
   id: string;
   content: ReactNode;
+  onClose?: () => void;
   autoPlay?: number;
   size?: "default" | "auto";
 }
@@
-            onClose={() => modalStore.close(modal.id)}
+            onClose={() => {
+              modal.onClose?.();
+              modalStore.close(modal.id);
+            }}
             size={modal.size}

Also applies to: 36-37

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

In `@src/app/providers/modal-provider.tsx` around lines 8 - 13, The ModalItem type
is missing an onClose callback and the provider only calls modalStore.close, so
any onClose supplied to modalStore.open is ignored; add an optional onClose?: ()
=> void to the ModalItem interface and update the close logic in the modal
provider to invoke the stored item's onClose (if present) when closing (ensure
you retrieve the current ModalItem from the store/state and call its onClose
before or after calling modalStore.close so cleanup/side-effects run).
src/shared/model/store/modal.store.ts (1)

30-37: 🛠️ Refactor suggestion | 🟠 Major

open의 위치 기반 파라미터 확장은 실수 유발 가능성이 큽니다.

size를 전달하려고 중간 인자를 undefined로 채우는 호출 패턴이 생겨 API 사용성이 떨어집니다. 옵션 객체 형태로 바꾸면 확장 시점에도 안전합니다.

♻️ 제안하는 개선안
-  open(
-    content: ReactNode,
-    autoPlay?: number,
-    onClose?: () => void,
-    id: string = new Date().toString(),
-    size?: "default" | "auto"
-  ) {
-    const newModal = { id, content, autoPlay, onClose, size }; // 새로 열고자 하는 모달
+  open({
+    content,
+    autoPlay,
+    onClose,
+    id = new Date().toString(),
+    size,
+  }: {
+    content: ReactNode;
+    autoPlay?: number;
+    onClose?: () => void;
+    id?: string;
+    size?: "default" | "auto";
+  }) {
+    const newModal = { id, content, autoPlay, onClose, size }; // 새로 열고자 하는 모달
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/model/store/modal.store.ts` around lines 30 - 37, The positional
parameter list for open (function open) is error-prone—change it to accept a
single options object (e.g., open({ content, autoPlay?, onClose?, id?, size? }))
to avoid filling undefined middle args; update the function signature and the
newModal creation (replace the positional parameters with destructured
properties and preserve default id generation), then update all callers to pass
an object with named fields matching the new shape (or provide a small
compatibility wrapper if needed).
🤖 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/features/onboarding/ui/policy-modal/policy-modal.css.ts`:
- Line 29: Rename the misspelled style identifier modalCotent to modalContent in
the CSS module and update all references to it across the codebase (components,
tests, imports) so the symbol matches consistently; locate the definition in
policy-modal.css.ts (export const modalCotent) and change the identifier to
modalContent, then search for and replace any usages of modalCotent with
modalContent to avoid breaking imports or runtime styling.
- Line 45: The CSS uses a px value for the height property in policy-modal
(height: "50px") which violates the project's rule to prefer rem units; change
the value to the equivalent rem (e.g., "5rem") in
src/features/onboarding/ui/policy-modal/policy-modal.css.ts by updating the
height property, and scan the same file for any other non-letter-spacing
measurements using px to convert them to rem as well.
- Around line 57-77: 현재 title, subTitle, content 스타일에서 themeVars.fontStyles를
사용하면서 별도로 fontWeight를 덮어쓰고 있으니(fontWeight 사용 사례: title, subTitle, content 및 지적된
다른 위치들), 각 컴포넌트에서는 fontWeight 속성을 제거하고 대신 적절한 themeVars.fontStyles 토큰(예: 기존
body_b_14, cap_m_12 같은 weight가 포함된 변형)을 사용하도록 교체하세요; 만약 필요한 weight 조합을 표현하는 토큰이
없으면 themeVars.fontStyles에 새 토큰을 추가(예: body_semibold_14 등 명확한 이름)하고 해당 토큰을 참조하도록
title, subTitle, content 및 다른 반복 위치들을 수정하세요.

In `@src/features/onboarding/ui/policy-modal/policy-modal.tsx`:
- Around line 7-12: Rename the props interface from modalProps to
PolicyModalProps and update the PolicyModal component signature to use
PolicyModalProps instead of modalProps; specifically locate the interface
declaration named modalProps and the component export const PolicyModal = ({
type, onClose }: modalProps) and change both to PolicyModalProps to follow
PascalCase and the "ComponentNameProps" convention.
- Around line 15-17: The modal header title is rendered as a non-semantic div
(inside the PolicyModal component using styles.modalHeader) which weakens screen
reader context; replace the title div that outputs {type === "USE" ? "이용약관" :
"개인정보처리방침"} with an appropriate semantic heading element (e.g., h2) and keep the
surrounding structure and styles (styles.modalHeader / styles.buttonWrapper)
intact so styling and layout are unchanged while improving accessibility.

In `@src/features/onboarding/ui/policy-modal/privacy-policy-content.tsx`:
- Around line 49-50: The table element incorrectly includes the cell-only class
styles.tCell; locate the JSX where a div has className `${styles.tableWrapper}`
and the table is rendered with className `${styles.table} ${styles.tCell}` and
remove styles.tCell from the table, applying styles.tCell only to cell elements
(td/th) inside the table (also mirror the same fix in privacy-policy.tsx where
the same pattern appears).
- Around line 21-33: Add unique React key props to the elements rendered in the
maps to remove warnings: for the outer map that renders <section> (iterating
TERMS_OF_PRIVACY_INFO.sections) add a key (e.g., section.title or a fallback
index) on the <section> element, and for the inner map that renders <article>
(iterating section.articles) add a key (e.g., article.title or a fallback index)
on the <article> element; ensure you reference the existing section and article
variables used in those callbacks when assigning the keys.

In `@src/features/onboarding/ui/policy-modal/use-policy-content.tsx`:
- Around line 11-19: The map over policy.chapter uses chapter.title as the
article key which can be empty/duplicate (e.g., "부칙") and the inner map over
chapter.contents omits a key on the <p> elements; fix by using stable, unique
keys: when rendering policy.chapter in the component (the map that creates
<article key={...}>), replace chapter.title with a fallback that includes the
chapter index or an id (e.g., use the chapter index or a unique chapter.id:
key={`chapter-${index}`} or key={`chapter-${chapter.id||index}`}); likewise, add
a key to the inner map over chapter.contents using the content index or a unique
content id (e.g., key={`content-${cIndex}`} or
key={content.id||`content-${cIndex}`}) to avoid React warnings and potential
collisions.

In `@src/pages/onboarding/onboarding-page.css.ts`:
- Around line 115-122: The agreeContent style overrides fontWeight (fontWeight:
500) which conflicts with the project rule to only use themeVars.fontStyles for
font sizing/weight; remove the individual fontWeight override from agreeContent
and, if a different weight is required, replace the spread
...themeVars.fontStyles.body_m_16 with the correct theme token (e.g.,
themeVars.fontStyles.<appropriate_token>) so all fontSize/lineHeight/fontWeight
come solely from themeVars.fontStyles.body_m_16 or the chosen token.

In `@src/pages/onboarding/ui/agree-section.tsx`:
- Around line 41-53: The clickable spans for "이용약관" and "개인정보처리방침" lack keyboard
accessibility; update the elements in agree-section.tsx to be accessible by
either replacing each <span> with a semantic <button type="button"> using the
same className (styles.underlineText) and onClick handler (handleModal), or by
adding role="button", tabIndex={0}, and an onKeyDown that triggers handleModal
for Enter/Space when keeping the span; ensure you reference handleModal and
styles.underlineText so the modal opens the same way.
- Around line 13-24: Extract the MODAL_ID string out of the handleModal function
and declare it as a module-level constant in UPPER_SNAKE_CASE (e.g.,
ONBOARD_MODAL_ID) so it follows the same pattern as LEAVE_MODAL_ID in
use-leave-confirm.tsx; update handleModal to reference the new ONBOARD_MODAL_ID
when calling modalStore.open and modalStore.close (keep PolicyModal and
modalStore.open call unchanged otherwise).

In `@src/pages/policy/policy-page.css.ts`:
- Around line 19-29: The exported style objects (e.g., title and subTitle) are
combining themeVars.fontStyles with an individual fontWeight, which violates the
token-only font rule; remove the explicit fontWeight properties from title,
subTitle and the other style exports in this file (the blocks around lines
44–66) and instead pick or add the correct token in themeVars.fontStyles that
already encodes size/line-height/weight (or create a new fontStyles token if the
desired weight/size combo doesn't exist) so each style uses only
themeVars.fontStyles without specifying fontSize/lineHeight/fontWeight directly.
- Around line 44-118: Duplicate styles (textStyle, flexColumn, tableWrapper,
table, tCell, thead, th) exist in policy-page.css.ts and the onboarding
policy-modal CSS; extract these shared definitions into a new shared module
(e.g., src/shared/styles/policy.css.ts) and replace the local exports in both
files with imports from that module. Concretely: create the shared file
exporting the same named recipes/styles (textStyle, flexColumn, tableWrapper,
table, tCell, thead, th), move themeVars-based implementations there, update
src/pages/policy/policy-page.css.ts and
src/features/onboarding/ui/policy-modal/policy-modal.css.ts to import those
symbols instead of declaring them locally, and remove the duplicated
declarations to keep a single source of truth for policy styles.

In `@src/pages/policy/policy-page.tsx`:
- Around line 25-29: Remove the unnecessary null-guard around currentPolicy in
PolicyPage: since PolicyPageProps.mode is strongly typed to "USE" | "PRIVACY"
and POLICY_CONTENT[mode] is guaranteed, delete the if (!currentPolicy) return
<></>; branch and use currentPolicy directly in the component body (update any
references to currentPolicy accordingly in PolicyPage). Ensure no other code
assumes a possible undefined value and run TypeScript checks to confirm no
errors remain.

In `@src/pages/policy/ui/privacy-policy.tsx`:
- Around line 52-53: The table element is incorrectly receiving the styles.tCell
class (which is meant for individual cells); remove styles.tCell from the
table's className (leave styles.table on the <table>) and apply styles.tCell to
the cell elements instead (add className={styles.tCell} to the <td> and/or <th>
elements inside the <table>, e.g., in the render of rows/cells or the map that
creates them) so that maxWidth, padding and border styles only affect cells.

In `@src/pages/policy/ui/use-policy.tsx`:
- Around line 19-23: 요약: Chapter 타입에서 title이 필수이므로 중복된 조건부 렌더링을 제거하세요; 수정방법:
use-policy.tsx의 해당 JSX에서 조건문 "{chapter.title && ( <h3 ...>{chapter.title}</h3>
)}"을 단순히 "<h3 className={styles.textStyle({ type: 'title3'
})}>{chapter.title}</h3>"로 바꿔 불필요한 분기 제거(참조 심볼: chapter.title, styles.textStyle,
h3 렌더링).

In `@src/shared/config/policy-use-info.ts`:
- Around line 190-198: The "부칙" chapter object uses title: "" which can produce
duplicate/invalid React keys in components like use-policy-content.tsx that use
chapter.title as a key; update the data so the chapter object either omits the
title property or sets it to undefined (instead of an empty string) and adjust
any code that assumes a string (e.g., when mapping chapters in
use-policy-content.tsx) to safely handle undefined by falling back to a stable
key (like chapterTitle or index) to avoid empty-string keys.

In `@src/shared/ui/modal/modal.css.ts`:
- Around line 21-28: The modalContent recipe uses a rem value for borderRadius;
change modalContent.base.borderRadius from "1.2rem" to an equivalent px value
(e.g., "19.2px") so it follows the guideline to use px for border and
border-radius—update the borderRadius property inside the modalContent recipe
accordingly.

---

Outside diff comments:
In `@src/app/providers/modal-provider.tsx`:
- Around line 8-13: The ModalItem type is missing an onClose callback and the
provider only calls modalStore.close, so any onClose supplied to modalStore.open
is ignored; add an optional onClose?: () => void to the ModalItem interface and
update the close logic in the modal provider to invoke the stored item's onClose
(if present) when closing (ensure you retrieve the current ModalItem from the
store/state and call its onClose before or after calling modalStore.close so
cleanup/side-effects run).

In `@src/shared/model/store/modal.store.ts`:
- Around line 30-37: The positional parameter list for open (function open) is
error-prone—change it to accept a single options object (e.g., open({ content,
autoPlay?, onClose?, id?, size? })) to avoid filling undefined middle args;
update the function signature and the newModal creation (replace the positional
parameters with destructured properties and preserve default id generation),
then update all callers to pass an object with named fields matching the new
shape (or provide a small compatibility wrapper if needed).
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 3704e7d8-8725-4b8c-9085-0b66648d640e

📥 Commits

Reviewing files that changed from the base of the PR and between 55858a8 and aa3002f.

⛔ Files ignored due to path filters (2)
  • src/shared/assets/icons/check3.svg is excluded by !**/*.svg and included by src/**
  • src/shared/assets/icons/x.svg is excluded by !**/*.svg and included by src/**
📒 Files selected for processing (23)
  • src/app/providers/modal-provider.tsx
  • src/app/routes/paths.ts
  • src/app/routes/public-routes.tsx
  • src/features/onboarding/index.ts
  • src/features/onboarding/lib/onboarding-form.validator.ts
  • src/features/onboarding/ui/policy-modal/policy-modal.css.ts
  • src/features/onboarding/ui/policy-modal/policy-modal.tsx
  • src/features/onboarding/ui/policy-modal/privacy-policy-content.tsx
  • src/features/onboarding/ui/policy-modal/use-policy-content.tsx
  • src/pages/onboarding/onboarding-page.css.ts
  • src/pages/onboarding/onboarding-page.tsx
  • src/pages/onboarding/ui/agree-section.tsx
  • src/pages/policy/policy-page.css.ts
  • src/pages/policy/policy-page.tsx
  • src/pages/policy/ui/privacy-policy.tsx
  • src/pages/policy/ui/use-policy.tsx
  • src/shared/assets/icons/index.ts
  • src/shared/config/index.ts
  • src/shared/config/policy-privacy-info.ts
  • src/shared/config/policy-use-info.ts
  • src/shared/model/store/modal.store.ts
  • src/shared/ui/modal/modal.css.ts
  • src/shared/ui/modal/modal.tsx

height: "2.4rem",
});

export const modalCotent = style({
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 | 🔵 Trivial

스타일 식별자 오탈자(modalCotent) 정리 권장

modalCotentmodalContent 오탈자로 보입니다. 사용처까지 함께 정리하면 검색성과 유지보수성이 좋아집니다.

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

In `@src/features/onboarding/ui/policy-modal/policy-modal.css.ts` at line 29,
Rename the misspelled style identifier modalCotent to modalContent in the CSS
module and update all references to it across the codebase (components, tests,
imports) so the symbol matches consistently; locate the definition in
policy-modal.css.ts (export const modalCotent) and change the identifier to
modalContent, then search for and replace any usages of modalCotent with
modalContent to avoid breaking imports or runtime styling.

Comment on lines +57 to +77
export const title = style({
color: themeVars.color.gray800,
...themeVars.fontStyles.body_b_14,
fontWeight: 600,
marginBottom: "0.8rem",
});

export const subTitle = style({
color: themeVars.color.gray800,
...themeVars.fontStyles.cap_m_12,
fontWeight: 500,
});

export const content = style({
display: "flex",
flexDirection: "column",
color: themeVars.color.gray500,
...themeVars.fontStyles.cap_m_12,
fontWeight: 500,
whiteSpace: "pre-wrap",
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

fontStyles와 개별 fontWeight를 혼용하고 있습니다.

themeVars.fontStyles를 이미 사용하고 있는데 fontWeight를 별도로 덮어쓰는 패턴(Line 60, 67, 75, 87, 94, 153)이 반복됩니다. 해당 값들은 토큰으로 통일해 주세요.

As per coding guidelines "src/**/*.css.ts: font 스타일은 themeVars.fontStyles만 사용 ... fontSize / lineHeight / fontWeight를 개별 속성으로 작성하지 않음".

Also applies to: 79-101, 152-154

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

In `@src/features/onboarding/ui/policy-modal/policy-modal.css.ts` around lines 57
- 77, 현재 title, subTitle, content 스타일에서 themeVars.fontStyles를 사용하면서 별도로
fontWeight를 덮어쓰고 있으니(fontWeight 사용 사례: title, subTitle, content 및 지적된 다른 위치들), 각
컴포넌트에서는 fontWeight 속성을 제거하고 대신 적절한 themeVars.fontStyles 토큰(예: 기존 body_b_14,
cap_m_12 같은 weight가 포함된 변형)을 사용하도록 교체하세요; 만약 필요한 weight 조합을 표현하는 토큰이 없으면
themeVars.fontStyles에 새 토큰을 추가(예: body_semibold_14 등 명확한 이름)하고 해당 토큰을 참조하도록
title, subTitle, content 및 다른 반복 위치들을 수정하세요.

Comment on lines +190 to +198
{
chapterTitle: "부칙",
chapter: [
{
title: "",
contents: ["본 약관은 2026년 3월 8일부터 시행합니다."],
},
],
},
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 | 🔵 Trivial

title 처리에 대한 고려가 필요해요.

"부칙" 챕터의 경우 title: ""로 빈 문자열이 설정되어 있어요. 이 데이터를 소비하는 컴포넌트(use-policy-content.tsx)에서 chapter.title을 React key로 사용하고 있어서 주의가 필요해요. 선택 사항이지만, 빈 문자열 대신 title을 optional로 만들거나 undefined를 사용하는 것도 고려해 볼 수 있어요.

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

In `@src/shared/config/policy-use-info.ts` around lines 190 - 198, The "부칙"
chapter object uses title: "" which can produce duplicate/invalid React keys in
components like use-policy-content.tsx that use chapter.title as a key; update
the data so the chapter object either omits the title property or sets it to
undefined (instead of an empty string) and adjust any code that assumes a string
(e.g., when mapping chapters in use-policy-content.tsx) to safely handle
undefined by falling back to a stable key (like chapterTitle or index) to avoid
empty-string keys.

- use-modal도 함께 삭제
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (3)
src/features/onboarding/ui/policy-modal/privacy-policy-content.tsx (1)

21-35: ⚠️ Potential issue | 🟡 Minor

article key를 optional 필드 단독에 의존하지 않도록 바꿔주세요.

현재 key={article.title}title이 비어 있거나 중복될 때 충돌할 수 있습니다. 섹션/인덱스를 조합한 fallback key를 같이 두는 편이 안전합니다.

🔧 제안하는 수정
-      {TERMS_OF_PRIVACY_INFO.sections.map((section: Section) => (
-        <section key={section.title} className={styles.flexColumn({ gap: 8 })}>
+      {TERMS_OF_PRIVACY_INFO.sections.map((section: Section, sectionIdx) => (
+        <section
+          key={`${section.title}-${sectionIdx}`}
+          className={styles.flexColumn({ gap: 8 })}
+        >
@@
-            {section.articles?.map((article: Article) => (
+            {section.articles?.map((article: Article, articleIdx) => (
               <article
-                key={article.title}
+                key={article.title ?? `article-${sectionIdx}-${articleIdx}`}
                 className={styles.flexColumn({ gap: 8 })}
               >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/onboarding/ui/policy-modal/privacy-policy-content.tsx` around
lines 21 - 35, The article list currently uses key={article.title} which can be
empty or duplicate; update the mapping over TERMS_OF_PRIVACY_INFO.sections and
section.articles to use a stable composite fallback key (e.g., combine section
index and article index and only use article.title when truthy) so each
<article> has a unique key; locate the map over TERMS_OF_PRIVACY_INFO.sections
and the inner map over section.articles in privacy-policy-content.tsx (types
Section and Article) and change the key to something like
`${sectionIndex}-${articleIndex}-${article.title || 'article'}` to avoid
collisions.
src/features/onboarding/ui/policy-modal/use-policy-content.tsx (1)

11-18: ⚠️ Potential issue | 🟡 Minor

리스트 렌더링 key 누락/안정성 문제를 정리해주세요.

chapter.contents.map<p>key가 없어 React 경고가 발생합니다. 또한 article에 index key만 쓰면 추후 데이터 순서 변경 시 재조정 비용/의도치 않은 재사용이 생길 수 있어 fallback 포함 key가 더 안전합니다.

🔧 제안하는 수정
-        {policy.chapter.map((chapter: Chapter, idx) => (
-          <article key={idx}>
+        {policy.chapter.map((chapter: Chapter, idx) => (
+          <article key={chapter.title || `chapter-${idx}`}>
             {chapter.title && (
               <h3 className={styles.subTitle}>{chapter.title}</h3>
             )}
             <div className={styles.content}>
-              {chapter.contents.map((content) => (
-                <p>{content}</p>
+              {chapter.contents.map((content, contentIdx) => (
+                <p key={`content-${idx}-${contentIdx}`}>{content}</p>
               ))}
             </div>
           </article>
         ))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/onboarding/ui/policy-modal/use-policy-content.tsx` around lines
11 - 18, The list rendering is missing stable keys: add a key to the <p>
elements produced by chapter.contents.map (use a stable identifier such as
content.id or, if not available, a combination like
`${content}-${contentIndex}`) and replace the article's index-only key
(currently key={idx}) with a fallback that prefers a stable id on Chapter (e.g.,
chapter.id || idx) to avoid re-use issues when order changes; update the map
callbacks and key assignments in the use-policy-content.tsx component
(chapter.contents.map and the article element) to use these stable keys.
src/features/onboarding/ui/policy-modal/policy-modal.css.ts (1)

57-60: 🛠️ Refactor suggestion | 🟠 Major

폰트 토큰 사용 규칙 위반(fontStyles + 개별 fontWeight)을 정리해주세요.

현재 여러 스타일에서 themeVars.fontStyles를 사용하면서 fontWeight를 별도로 덮어쓰고 있습니다. 이 파일 규칙상 폰트 속성은 토큰으로만 표현되어야 하므로, 개별 fontWeight를 제거하고 필요한 경우 적절한 font 토큰으로 대체해 주세요.

🔧 최소 수정 예시(중복 fontWeight 제거)
 export const title = style({
   color: themeVars.color.gray800,
   ...themeVars.fontStyles.body_b_14,
-  fontWeight: 600,
   marginBottom: "0.8rem",
 });

 export const subTitle = style({
   color: themeVars.color.gray800,
   ...themeVars.fontStyles.cap_m_12,
-  fontWeight: 500,
 });

 export const content = style({
   display: "flex",
   flexDirection: "column",
   color: themeVars.color.gray500,
   ...themeVars.fontStyles.cap_m_12,
-  fontWeight: 500,
   whiteSpace: "pre-wrap",
 });

 export const textStyle = recipe({
@@
       title1: {
         ...themeVars.fontStyles.body_b_16,
-        fontWeight: 700,
       },
@@
       title3: {
         ...themeVars.fontStyles.body_r_14,
-        fontWeight: 400,
       },

As per coding guidelines "src/**/*.css.ts: font 스타일은 themeVars.fontStyles만 사용 ... fontSize / lineHeight / fontWeight를 개별 속성으로 작성하지 않음".

Also applies to: 64-67, 70-75, 85-95, 133-139, 152-154

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

In `@src/features/onboarding/ui/policy-modal/policy-modal.css.ts` around lines 57
- 60, The style export "title" (export const title) violates the font token rule
by combining themeVars.fontStyles.body_b_14 with an overriding fontWeight;
remove the inline fontWeight and instead pick the appropriate predefined font
token from themeVars.fontStyles that already encodes weight (or replace
body_b_14 with the correct token that matches the desired weight). Apply the
same change pattern to the other style exports in this file that set
themeVars.fontStyles and then override fontWeight (the blocks flagged in the
review): remove individual fontSize/lineHeight/fontWeight overrides and use the
correct themeVars.fontStyles token consistently.
🤖 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/pages/policy/policy-page.tsx`:
- Around line 9-23: POLICY_CONTENT currently stores instantiated JSX (<UsePolicy
/> and <PrivacyPolicy />) which causes components to be created at module load;
change the Component fields to hold the component references (UsePolicy and
PrivacyPolicy) instead of JSX, update any code that renders them to instantiate
at render time (e.g., render <Component /> or React.createElement(Component,
props)), and adjust the POLICY_CONTENT type if needed so Component expects a
React.ComponentType or similar to allow passing props at render time.

In `@src/pages/policy/ui/privacy-policy.tsx`:
- Around line 52-53: The className uses unnecessary template literals for single
classes; replace className={`${styles.tableWrapper}`} and
className={`${styles.table}`} with plain expressions using the class identifiers
(styles.tableWrapper and styles.table) in privacy-policy.tsx so the div and
table use className={styles.tableWrapper} and className={styles.table}
respectively.
- Around line 32-35: The map over section.articles? in the JSX uses an
index-based key (`article-${idx}`) which is brittle; change the key to a stable
identifier such as a title or id from the Article object (e.g., use
article.title or article.id if present) in the map callback where Article is
referenced and the <article ... key=...> prop is set so that the key becomes
something like `article-${article.title}` (or `article-${article.id}`) instead
of using idx.

In `@src/pages/policy/ui/use-policy.tsx`:
- Around line 5-32: The UsePolicy component currently returns the raw array from
TERMS_OF_USE.map; wrap the mapped sections in a React fragment so the component
returns a single JSX node (e.g., return (<>{TERMS_OF_USE.map(... )}</>)); locate
the map inside UsePolicy (references: UsePolicy, TERMS_OF_USE, Chapters,
Chapter) and enclose the existing returned array in a fragment (<>...</>) to
make the component structure explicit and ready for future wrappers.

---

Duplicate comments:
In `@src/features/onboarding/ui/policy-modal/policy-modal.css.ts`:
- Around line 57-60: The style export "title" (export const title) violates the
font token rule by combining themeVars.fontStyles.body_b_14 with an overriding
fontWeight; remove the inline fontWeight and instead pick the appropriate
predefined font token from themeVars.fontStyles that already encodes weight (or
replace body_b_14 with the correct token that matches the desired weight). Apply
the same change pattern to the other style exports in this file that set
themeVars.fontStyles and then override fontWeight (the blocks flagged in the
review): remove individual fontSize/lineHeight/fontWeight overrides and use the
correct themeVars.fontStyles token consistently.

In `@src/features/onboarding/ui/policy-modal/privacy-policy-content.tsx`:
- Around line 21-35: The article list currently uses key={article.title} which
can be empty or duplicate; update the mapping over
TERMS_OF_PRIVACY_INFO.sections and section.articles to use a stable composite
fallback key (e.g., combine section index and article index and only use
article.title when truthy) so each <article> has a unique key; locate the map
over TERMS_OF_PRIVACY_INFO.sections and the inner map over section.articles in
privacy-policy-content.tsx (types Section and Article) and change the key to
something like `${sectionIndex}-${articleIndex}-${article.title || 'article'}`
to avoid collisions.

In `@src/features/onboarding/ui/policy-modal/use-policy-content.tsx`:
- Around line 11-18: The list rendering is missing stable keys: add a key to the
<p> elements produced by chapter.contents.map (use a stable identifier such as
content.id or, if not available, a combination like
`${content}-${contentIndex}`) and replace the article's index-only key
(currently key={idx}) with a fallback that prefers a stable id on Chapter (e.g.,
chapter.id || idx) to avoid re-use issues when order changes; update the map
callbacks and key assignments in the use-policy-content.tsx component
(chapter.contents.map and the article element) to use these stable keys.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: c73e945a-73a7-4351-82ee-0513345d988a

📥 Commits

Reviewing files that changed from the base of the PR and between aa3002f and 3eb508f.

📒 Files selected for processing (11)
  • src/features/onboarding/ui/policy-modal/policy-modal.css.ts
  • src/features/onboarding/ui/policy-modal/policy-modal.tsx
  • src/features/onboarding/ui/policy-modal/privacy-policy-content.tsx
  • src/features/onboarding/ui/policy-modal/use-policy-content.tsx
  • src/pages/policy/policy-page.css.ts
  • src/pages/policy/policy-page.tsx
  • src/pages/policy/ui/privacy-policy.tsx
  • src/pages/policy/ui/use-policy.tsx
  • src/shared/ui/index.ts
  • src/shared/ui/modal/modal.css.ts
  • src/shared/ui/modal/use-modal.ts
💤 Files with no reviewable changes (2)
  • src/shared/ui/index.ts
  • src/shared/ui/modal/use-modal.ts

Comment on lines +9 to +23
const POLICY_CONTENT = {
USE: {
title: "이용약관",
subTitle:
"컴핏 관련 제반 서비스의 이용과 관련하여 필요한 사항을 규정합니다.",
gap: 24 as const,
Component: <UsePolicy />,
},
PRIVACY: {
title: "개인정보처리방침",
subTitle: "컴핏은 개인정보 보호 등에 관한 법률을 준수합니다.",
gap: 40 as const,
Component: <PrivacyPolicy />,
},
};
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 | 🔵 Trivial

POLICY_CONTENT에서 JSX를 미리 인스턴스화하는 것은 권장되지 않아요.

현재 Component 필드에 <UsePolicy /><PrivacyPolicy />가 미리 생성되어 저장되어 있어요. 이 방식은 모듈 로드 시점에 컴포넌트가 생성되므로 React의 렌더링 최적화를 활용하기 어렵고, 향후 props 전달이 필요할 때 수정이 복잡해질 수 있어요.

컴포넌트 참조만 저장하고 렌더링 시점에 인스턴스화하는 것을 권장해요.

♻️ 제안하는 수정
+import type { ComponentType } from "react";
+
 const POLICY_CONTENT = {
   USE: {
     title: "이용약관",
     subTitle:
       "컴핏 관련 제반 서비스의 이용과 관련하여 필요한 사항을 규정합니다.",
     gap: 24 as const,
-    Component: <UsePolicy />,
+    Component: UsePolicy,
   },
   PRIVACY: {
     title: "개인정보처리방침",
     subTitle: "컴핏은 개인정보 보호 등에 관한 법률을 준수합니다.",
     gap: 40 as const,
-    Component: <PrivacyPolicy />,
+    Component: PrivacyPolicy,
   },
 };

 const PolicyPage = ({ mode }: PolicyPageProps) => {
   const currentPolicy = POLICY_CONTENT[mode];
+  const ContentComponent = currentPolicy.Component;

   return (
     ...
         {/** 약관 컨텐츠 */}
-        {currentPolicy.Component}
+        <ContentComponent />
     ...
   );
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/policy/policy-page.tsx` around lines 9 - 23, POLICY_CONTENT
currently stores instantiated JSX (<UsePolicy /> and <PrivacyPolicy />) which
causes components to be created at module load; change the Component fields to
hold the component references (UsePolicy and PrivacyPolicy) instead of JSX,
update any code that renders them to instantiate at render time (e.g., render
<Component /> or React.createElement(Component, props)), and adjust the
POLICY_CONTENT type if needed so Component expects a React.ComponentType or
similar to allow passing props at render time.

Comment on lines +32 to +35
{section.articles?.map((article: Article, idx) => (
<article
key={`article-${idx}`}
className={styles.flexColumn({ gap: 8 })}
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 | 🔵 Trivial

article key에 의미 있는 식별자 사용을 고려해 주세요.

현재 article-${idx} 형태의 인덱스 기반 key를 사용하고 있어요. 정적 콘텐츠라 당장 문제는 없지만, article.title이 있는 경우 이를 활용하면 더 안정적이에요.

♻️ 제안하는 수정
-              <article
-                key={`article-${idx}`}
-                className={styles.flexColumn({ gap: 8 })}
-              >
+              <article
+                key={article.title ?? `article-${idx}`}
+                className={styles.flexColumn({ gap: 8 })}
+              >
📝 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
{section.articles?.map((article: Article, idx) => (
<article
key={`article-${idx}`}
className={styles.flexColumn({ gap: 8 })}
{section.articles?.map((article: Article, idx) => (
<article
key={article.title ?? `article-${idx}`}
className={styles.flexColumn({ gap: 8 })}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/policy/ui/privacy-policy.tsx` around lines 32 - 35, The map over
section.articles? in the JSX uses an index-based key (`article-${idx}`) which is
brittle; change the key to a stable identifier such as a title or id from the
Article object (e.g., use article.title or article.id if present) in the map
callback where Article is referenced and the <article ... key=...> prop is set
so that the key becomes something like `article-${article.title}` (or
`article-${article.id}`) instead of using idx.

Comment on lines +52 to +53
<div className={`${styles.tableWrapper}`}>
<table className={`${styles.table}`}>
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 | 🔵 Trivial

불필요한 템플릿 리터럴 사용이에요.

단일 className만 사용할 때는 템플릿 리터럴이 필요 없어요. 더 간결하게 작성할 수 있어요.

♻️ 제안하는 수정
-                        <div className={`${styles.tableWrapper}`}>
-                          <table className={`${styles.table}`}>
+                        <div className={styles.tableWrapper}>
+                          <table className={styles.table}>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/policy/ui/privacy-policy.tsx` around lines 52 - 53, The className
uses unnecessary template literals for single classes; replace
className={`${styles.tableWrapper}`} and className={`${styles.table}`} with
plain expressions using the class identifiers (styles.tableWrapper and
styles.table) in privacy-policy.tsx so the div and table use
className={styles.tableWrapper} and className={styles.table} respectively.

Comment on lines +5 to +32
export const UsePolicy = () => {
return TERMS_OF_USE.map((policy: Chapters) => {
return (
<section
key={policy.chapterTitle}
className={styles.flexColumn({ gap: 8 })}
>
<h2 className={styles.textStyle({ type: "title1" })}>
{policy.chapterTitle}
</h2>

<div>
{policy.chapter.map((chapter: Chapter) => (
<article key={chapter.title}>
<h3 className={styles.textStyle({ type: "title3" })}>
{chapter?.title}
</h3>
<div className={styles.content}>
{chapter.contents.map((content, idx) => (
<p key={idx}>{content}</p>
))}
</div>
</article>
))}
</div>
</section>
);
});
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 | 🔵 Trivial

컴포넌트가 배열을 직접 반환하고 있어요.

UsePolicy 컴포넌트가 map의 결과 배열을 직접 반환하고 있어요. 동작에는 문제없지만, React Fragment로 감싸면 컴포넌트 구조가 더 명확해지고 향후 래퍼 요소 추가 시 수정이 용이해요.

♻️ Fragment로 감싸는 제안
 export const UsePolicy = () => {
-  return TERMS_OF_USE.map((policy: Chapters) => {
-    return (
+  return (
+    <>
+      {TERMS_OF_USE.map((policy: Chapters) => (
         <section
           key={policy.chapterTitle}
           className={styles.flexColumn({ gap: 8 })}
         >
           ...
         </section>
-    );
-  });
+      ))}
+    </>
+  );
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/policy/ui/use-policy.tsx` around lines 5 - 32, The UsePolicy
component currently returns the raw array from TERMS_OF_USE.map; wrap the mapped
sections in a React fragment so the component returns a single JSX node (e.g.,
return (<>{TERMS_OF_USE.map(... )}</>)); locate the map inside UsePolicy
(references: UsePolicy, TERMS_OF_USE, Chapters, Chapter) and enclose the
existing returned array in a fragment (<>...</>) to make the component structure
explicit and ready for future wrappers.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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/pages/policy/policy-page.css.ts`:
- Around line 94-99: The table style sets conflicting width rules (width:
"max-content", minWidth: "100%", maxWidth: "25rem") which can produce unintended
layout when the parent is larger than 25rem; update the CSS in the table style
to remove or adjust the conflicting constraint (e.g., remove maxWidth or change
minWidth/width logic) so it behaves correctly inside the tableWrapper scrolling
container—locate the exported const table in src/pages/policy/policy-page.css.ts
and either drop maxWidth: "25rem" or replace minWidth: "100%" with a responsive
rule that matches the intended scrolling behavior.

In `@src/pages/policy/ui/privacy-policy.tsx`:
- Around line 1-7: Move the type-only imports off the combined import and place
them in a separate import statement after the value imports: keep
TERMS_OF_PRIVACY_INFO in the first import and create a new import that only
imports type Article and type Section (using the type-only import form) below
it; update the import block so that type Article and type Section are imported
in their own "import type" statement as required by the coding guideline.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 341def0e-7f0a-4fce-96ea-99f88eb4ece0

📥 Commits

Reviewing files that changed from the base of the PR and between 3eb508f and 421908a.

📒 Files selected for processing (2)
  • src/pages/policy/policy-page.css.ts
  • src/pages/policy/ui/privacy-policy.tsx

Comment on lines +94 to +99
export const table = style({
width: "max-content",
minWidth: "100%",
borderCollapse: "collapse",
maxWidth: "25rem",
});
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

table 스타일의 width 속성들이 서로 충돌할 수 있어요.

width: "max-content", minWidth: "100%", maxWidth: "25rem"이 동시에 적용되면 의도치 않은 레이아웃이 발생할 수 있어요:

  • minWidth: 100%는 부모 너비 이상을 보장하지만
  • maxWidth: 25rem은 최대 너비를 제한해요
  • 부모가 25rem보다 크면 minWidthmaxWidth가 충돌해요

의도된 동작인지 확인해 주세요. 테이블이 스크롤되는 컨테이너(tableWrapper)안에 있으므로, maxWidth 제한이 정말 필요한지 검토가 필요해요.

🔧 maxWidth 제거 제안
 export const table = style({
   width: "max-content",
   minWidth: "100%",
   borderCollapse: "collapse",
-  maxWidth: "25rem",
 });
📝 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
export const table = style({
width: "max-content",
minWidth: "100%",
borderCollapse: "collapse",
maxWidth: "25rem",
});
export const table = style({
width: "max-content",
minWidth: "100%",
borderCollapse: "collapse",
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/policy/policy-page.css.ts` around lines 94 - 99, The table style
sets conflicting width rules (width: "max-content", minWidth: "100%", maxWidth:
"25rem") which can produce unintended layout when the parent is larger than
25rem; update the CSS in the table style to remove or adjust the conflicting
constraint (e.g., remove maxWidth or change minWidth/width logic) so it behaves
correctly inside the tableWrapper scrolling container—locate the exported const
table in src/pages/policy/policy-page.css.ts and either drop maxWidth: "25rem"
or replace minWidth: "100%" with a responsive rule that matches the intended
scrolling behavior.

Comment on lines +1 to +7
import {
TERMS_OF_PRIVACY_INFO,
type Article,
type Section,
} from "@/shared/config";

import * as styles from "../policy-page.css";
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 | 🔵 Trivial

type import는 import 구문 하단에 배치해 주세요.

코딩 가이드라인에 따르면 "Place type imports at the bottom of the import statement"이에요. type Articletype Section을 별도 import 문으로 분리해 주세요.

♻️ 제안하는 수정
-import {
-  TERMS_OF_PRIVACY_INFO,
-  type Article,
-  type Section,
-} from "@/shared/config";
+import { TERMS_OF_PRIVACY_INFO } from "@/shared/config";
 
 import * as styles from "../policy-page.css";
+
+import type { Article, Section } from "@/shared/config";

As per coding guidelines "src/**/*.{ts,tsx}: Place type imports at the bottom of the import statement".

📝 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
import {
TERMS_OF_PRIVACY_INFO,
type Article,
type Section,
} from "@/shared/config";
import * as styles from "../policy-page.css";
import { TERMS_OF_PRIVACY_INFO } from "@/shared/config";
import * as styles from "../policy-page.css";
import type { Article, Section } from "@/shared/config";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/policy/ui/privacy-policy.tsx` around lines 1 - 7, Move the
type-only imports off the combined import and place them in a separate import
statement after the value imports: keep TERMS_OF_PRIVACY_INFO in the first
import and create a new import that only imports type Article and type Section
(using the type-only import form) below it; update the import block so that type
Article and type Section are imported in their own "import type" statement as
required by the coding guideline.

@u-zzn
Copy link
Copy Markdown
Collaborator

u-zzn commented Mar 25, 2026

항상 빠르게 작업해주셔서 감사합니다 수빈님 ☺️👍

전체적으로 구조 잘 잡아서 작업해주신 것 같습니다 :)
약관/개인정보처리방침 데이터를 shared/config로 분리하여 온보딩 모달과 별도 정책 페이지 양쪽에서 공통으로 사용할 수 있도록 구성해주신 점, 기존 공통 Modal을 새로 만들기보다 size 옵션으로 확장하여 요구사항을 반영해주신 흐름 좋다고 느꼈습니다 👍

추가로 약관 페이지를 별도 라우트로 분리해주셔서, 온보딩 내부에서만 사용하는 것이 아니라 외부에서도 접근 가능하도록 설계해주신 점이 좋았습니다. 만약 추후 footer 등이 새로 생긴다면 약관 페이지 링크를 연결하는 작업도 덕분에 수월하게 진행할 수 있을 것 같습니다! 🙂

몇 가지 보완하면 좋을 것 같은 부분만 아래에 세부 코멘트로 남겨두겠습니다! 수고 많으셨어요오 :)

Comment on lines +35 to +56
<label htmlFor="agree" className={styles.agreeContent}>
<div className={styles.checkbox({ isAgreed })}>
<AgreeCheckIcon />
</div>
<p>
Comfit{" "}
<span
className={styles.underlineText}
onClick={(e) => handleModal(e, "USE")}
>
이용약관
</span>{" "}
및{" "}
<span
className={styles.underlineText}
onClick={(e) => handleModal(e, "PRIVACY")}
>
개인정보처리방침
</span>
에 동의합니다.
</p>
</label>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

약관 열기 액션이 현재 label 내부 span 클릭으로 들어가 있어서 “체크박스 토글” 역할과 “약관 열기” 역할이 한 영역 안에 같이 묶여 있는 것 같아요!

지금처럼 preventDefault()로 제어할 수도 있긴 한데, 구조적으로는 체크박스 라벨과 약관 열기 액션을 분리해두는 쪽이 의미상 더 명확하지 않을까 .. 하는 개인적인 의견입니다! :)

예를 들어 약관 텍스트는 button이나 Link로 분리해두면 접근성 측면에서 역할이 더 분명해지고, 추후 키보드 탐색이나 focus 처리도 더 자연스러워질 것 같습니다 🙂

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

체크박스 요소에 대한 설명을 돕기 위한 접근성 목적으로, label 태그 하위에 실제 보이는 체크박스와 캡션을 함께
위치 시키고 preventDefault()를 추가하게 된 것인데
생각해보니!!!!!!! 말씀하신대로 체크박스 라벨과 텍스트 영역을 분리하고 aria-label을 추가로 설정하면 접근성과 클릭 이벤트에 대한 문제도 해결이 될 것 같아 해당 방향으로 수정하겠습니다!!!!!

Comment on lines +36 to +40
auto: {
width: "auto",
height: "auto",
padding: "0",
},
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

size: "auto"로 분기한 방향 너무 좋은 것 같습니다! ☺️

다만 현재는 width/height: auto만 들어가 있는 상태라 약관처럼 내용이 긴 모달은 작은 화면에서 viewport를 넘길 가능성도 있을 것 같아서 maxWidth, maxHeight 설정도 같이 해주면 좋을 것 같아요 :)

`maxWidth: "90vw"`
`maxHeight: "90vh"`

정도로 제한을 두고, 내부 컨텐츠 영역에서 스크롤이 돌도록 맞추면 모바일(물론 지금은 모바일 화면이 따로 없긴 하지만)이나 작은 노트북 화면에서도 안정적으로 동작할 것 같은데, 어떨까요 ?? 🙂

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

우선은 Modal 컴포넌트의 modalContent css에 maxWidth와 maxHeight속성을 기본적으로 추가해두었습니다!
완전한 반응형을 위해서는 내부 컨텐츠에 대해서도 디자인 수정이 들어가야 하는데,
이 부분은 임의로 수정해두기보다(수정이 수정을 일으킬 수 있다는.. 그런 느낌..?)..
추후 반응형을 위한 스프린트 (지극히 저의 개인적인 바램//)에서 함께 수정하면 좋을 것 같아요!

Comment on lines +17 to +19
{chapter.contents.map((content) => (
<p>{content}</p>
))}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

여기 chapter.contents.map() 안쪽 <p>key가 없어서 간단하게 idx라도 같이 넣어두면 어떨까요? ☺️

{chapter.contents.map((content, idx) => (
  <p key={idx}>{content}</p>
))}

작은 부분이긴 한데 map 렌더링은 한 번씩 다 맞춰두면 이후에 warning 잡기가 편한 것 같습니다!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

꺄호 key를 넣는다고 다 넣었는데 누락된 부분이 있었네요!!!!!!!!!!!!!
꼼꼼코리 감사합니다~~~ 바로 반영하겠습니다!!!!! o(^@^)o

Comment on lines +21 to +22
{TERMS_OF_PRIVACY_INFO.sections.map((section: Section) => (
<section key={section.title} className={styles.flexColumn({ gap: 8 })}>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

pr에서 설명해주신 것처럼 “UI 컴포넌트 자체를 억지로 공용화하지 않았다”는 판단은 좋은 것 같습니다!

다만 지금은 페이지/모달이 스타일은 달라도 sections -> articles -> table/text를 순회하는 렌더링 구조 자체는 거의 동일해서, 추후에는 'UI 컴포넌트 공용화보다는 데이터 렌더링 레벨만 한 번 분리해볼 수 있지 않을까?' 하는 개인적인 의견이 있습니닷 🙂

예를 들면

`renderArticleContent`
`renderPrivacySection`

처럼 순회/분기 로직만 분리해두고, 스타일 클래스만 각 화면에서 주입받는 식으로 정리하면 정책 내용 수정 시 두 군데를 같이 수정해야 하는 부담이 줄어들 것 같습니다!

이번 스프린트 기간에 변경이 필요하다기보다는, 추후 만약 약관 내용이 자주 변경될 가능성이 있다면 리팩토링 포인트로 고려해봐도 좋을 것 같다는 생각이 들었습니다!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

오 UI적으로 동일하게 사용되었을 때 공통요소로 분리해야한다고 생각했는데, 데이터 렌더링 측면에서 분리를 진행해볼 수도 있겠네요 오호 생각의 전환이..

((다음 스프린트에서 한 번 염두해두고 진행해보록 할게요 // (귀찮은거 진짜 아님. 좋은 아이디어라고 생각함 진짜임 진짜. 단지 다른 이슈들도 뒤에 기다리고 있기 때문에..)
랏코_짱

Comment on lines +16 to +23
<h2>{type === "USE" ? "이용약관" : "개인정보처리방침"}</h2>
<div className={styles.buttonWrapper}>
<Modal.XButton />
</div>
</div>
<Modal.Content>
<div className={styles.modalContent}>
{type === "USE" ? <UsePolicyContent /> : <PrivacyPolicyContent />}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

type === "USE" ? ... : ... 분기가 제목과 본문 양쪽에 각각 들어가 있는데, 이 부분 따로 객체로 분리해두는거 어떨까요? 🙂

const POLICY_MODAL_CONTENT = {
  USE: {
    title: "이용약관",
    Content: <UsePolicyContent />,
  },
  PRIVACY: {
    title: "개인정보처리방침",
    Content: <PrivacyPolicyContent />,
  },
};

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

컨텐츠가 두 개뿐이라 분기처리 형태와 객체 형태로 빼는 방식 중에 고민했는데 그래도 확실히 가독성 면에서 객체 형태로 빼는 방식이 더 깔끔한 것 같네요 ㅎㅅㅎ
제안하신 방향으로 수정하면서, 렌더링 시점에 type에 해당하는 컴포넌트만 만들어질 수 있게 JSX 대신 컴포넌트 함수 자체를 할당하는 방식으로 디벨롭해두었습니다!

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
src/shared/ui/modal/modal.tsx (1)

80-86: 🧹 Nitpick | 🔵 Trivial

시맨틱 HTML 태그 사용을 고려해 주세요.

TitleSubTitle 컴포넌트에서 div 대신 시맨틱 태그(h2, p 등)를 사용하면 접근성이 향상돼요. 물론 범용 모달 컴포넌트라 컨텍스트에 따라 다를 수 있지만, 대부분의 사용 사례에서 제목은 heading 역할을 해요.

♻️ 제안하는 수정 사항
 const Title = ({ children }: { children: ReactNode }) => {
-  return <div className={styles.Title}>{children}</div>;
+  return <h2 className={styles.Title}>{children}</h2>;
 };

 const SubTitle = ({ children }: { children: ReactNode }) => {
-  return <div className={styles.SubTitle}>{children}</div>;
+  return <p className={styles.SubTitle}>{children}</p>;
 };

Based on learnings, "Use semantic HTML tags instead of generic div elements".

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

In `@src/shared/ui/modal/modal.tsx` around lines 80 - 86, Replace non-semantic
divs in the Title and SubTitle components with appropriate semantic elements:
render Title using a heading element (e.g., h2) and SubTitle using a paragraph
(p) to improve accessibility, keeping existing className={styles.Title} and
className={styles.SubTitle}; optionally make Title accept an optional "as" prop
or "level" prop to customize heading level (h1–h6) if consumers need different
semantics while preserving current typing for children: ReactNode and existing
styling.
🤖 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/features/experience-detail/model/use-leave-confirm.tsx`:
- Around line 68-76: The modal's XButton currently triggers ModalBasic.onClose
which only closes the modal (leaving blocker.state "blocked"), causing the modal
to immediately re-open; to fix, either hide the XButton or wire the XButton
behavior to the "continue editing" action: change ModalBasic's onClose prop to
call cancelLeave (same as onConfirm) instead of confirmLeave, or ensure onClose
invokes the same unblock/reset logic as cancelLeave so blocker.proceed/reset
runs; update the ModalBasic usage (props onClose/onConfirm and functions
confirmLeave/cancelLeave) accordingly to keep labels and behavior consistent.

In `@src/features/onboarding/ui/policy-modal/use-policy-content.tsx`:
- Around line 5-26: In UsePolicyContent, avoid shadowing the same idx name in
nested iterators: in the policy.chapter.map callback
(policy.chapter.map((chapter: Chapter, idx) => ...)) rename idx to chapterIdx
and in chapter.contents.map((content, idx) => ...) rename idx to contentIdx (or
similar) so each key variable is distinct; update the corresponding key props
(e.g., key={chapterIdx} and key={contentIdx}) and any references to those
variables, locating them via TERMS_OF_USE, policy.chapter.map, and
chapter.contents.map.

In `@src/shared/ui/modal/modal.css.ts`:
- Around line 50-55: Replace the hardcoded color in the XButton style with the
project theme token: change the color: "black" usage inside the XButton style
definition to reference themeVars.color (e.g., the appropriate key like
themeVars.color.text or themeVars.color.primary per your theme API) so the
component uses the design tokens instead of a literal string; update any imports
to ensure themeVars is imported into the file if not already.

In `@src/shared/ui/modal/modal.tsx`:
- Around line 72-74: Modal.Content (implemented as the Content component) no
longer accepts a type prop, but the Storybook story still passes type="auto";
remove the type prop from the Modal.Content usage in the story (the story that
renders Modal.Content with type="auto") so it matches the new Content signature,
and verify there are no other occurrences of Modal.Content with a type prop in
stories or tests.

---

Outside diff comments:
In `@src/shared/ui/modal/modal.tsx`:
- Around line 80-86: Replace non-semantic divs in the Title and SubTitle
components with appropriate semantic elements: render Title using a heading
element (e.g., h2) and SubTitle using a paragraph (p) to improve accessibility,
keeping existing className={styles.Title} and className={styles.SubTitle};
optionally make Title accept an optional "as" prop or "level" prop to customize
heading level (h1–h6) if consumers need different semantics while preserving
current typing for children: ReactNode and existing styling.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 2b5647b2-c369-46da-a0bb-f307bc1038a7

📥 Commits

Reviewing files that changed from the base of the PR and between 421908a and 6649060.

⛔ Files ignored due to path filters (3)
  • src/shared/assets/icons/icon_pen.svg is excluded by !**/*.svg and included by src/**
  • src/shared/assets/icons/icon_trash_on.svg is excluded by !**/*.svg and included by src/**
  • src/shared/assets/icons/icon_warning.svg is excluded by !**/*.svg and included by src/**
📒 Files selected for processing (12)
  • src/features/experience-detail/model/use-leave-confirm.tsx
  • src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx
  • src/features/experience-matching/ui/select-company/select-company.tsx
  • src/features/onboarding/ui/policy-modal/policy-modal.tsx
  • src/features/onboarding/ui/policy-modal/use-policy-content.tsx
  • src/pages/onboarding/onboarding-page.css.ts
  • src/pages/onboarding/ui/agree-section.tsx
  • src/shared/assets/icons/index.ts
  • src/shared/ui/button/button.css.ts
  • src/shared/ui/modal/modal-basic.tsx
  • src/shared/ui/modal/modal.css.ts
  • src/shared/ui/modal/modal.tsx

Comment on lines 68 to 76
<ModalBasic
title={`작성중인 내용이 있습니다.\n정말 나가시겠습니까?`}
subTitle="저장하지 않으면 내용이 사라져요."
closeText="이어서 작성"
confirmText="나가기"
onClose={cancelLeave}
onConfirm={confirmLeave}
icon={<IconWarn width={48} height={48} />}
title={`작성 중인 내용이 있어요`}
subTitle="저장하지 않으면 내용이 모두 사라져요."
closeText="나가기"
confirmText="계속 작성하기"
onClose={confirmLeave}
onConfirm={cancelLeave}
/>,
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

모달 버튼 의미와 XButton 동작 간 불일치 가능성이 있어요.

onCloseconfirmLeave(나가기)를, onConfirmcancelLeave(계속 작성)를 매핑한 것은 버튼 라벨과 일치하지만, Modal.XButton 클릭 시 동작이 예상과 다를 수 있어요.

modal-provider.tsx에서 ModalonClosemodalStore.close(modal.id)만 호출해요. 이 경우:

  1. XButton 클릭 → 모달만 닫힘 (blocker.proceed/reset 미호출)
  2. blocker.state가 여전히 "blocked" 상태
  3. useEffect가 다시 트리거되어 모달이 재오픈됨

기능적으로는 작동하지만, 사용자가 XButton으로 모달을 닫으려 할 때 즉시 다시 열리는 UX가 혼란스러울 수 있어요. XButton을 숨기거나, 명시적으로 "계속 작성하기" 동작과 연결하는 것을 고려해 보세요.

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

In `@src/features/experience-detail/model/use-leave-confirm.tsx` around lines 68 -
76, The modal's XButton currently triggers ModalBasic.onClose which only closes
the modal (leaving blocker.state "blocked"), causing the modal to immediately
re-open; to fix, either hide the XButton or wire the XButton behavior to the
"continue editing" action: change ModalBasic's onClose prop to call cancelLeave
(same as onConfirm) instead of confirmLeave, or ensure onClose invokes the same
unblock/reset logic as cancelLeave so blocker.proceed/reset runs; update the
ModalBasic usage (props onClose/onConfirm and functions
confirmLeave/cancelLeave) accordingly to keep labels and behavior consistent.

Comment on lines +5 to +26
export const UsePolicyContent = () => {
return TERMS_OF_USE.map((policy: Chapters) => {
return (
<section key={policy.chapterTitle}>
<h2 className={styles.title}>{policy.chapterTitle}</h2>

{policy.chapter.map((chapter: Chapter, idx) => (
<article key={idx}>
{chapter.title && (
<h3 className={styles.subTitle}>{chapter.title}</h3>
)}
<div className={styles.content}>
{chapter.contents.map((content, idx) => (
<p key={idx}>{content}</p>
))}
</div>
</article>
))}
</section>
);
});
};
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 | 🔵 Trivial

컴포넌트 구조가 잘 구현되어 있어요!

시맨틱 HTML 태그(section, article, h2, h3)를 적절히 사용하고, 이전 리뷰에서 지적된 key prop 누락 문제도 잘 해결되었어요.

한 가지 작은 개선 포인트: Line 11과 Line 17에서 idx 변수명이 중복 사용되고 있어요. 내부 스코프에서 외부 변수를 가리는(shadowing) 것은 가독성을 떨어뜨릴 수 있으니, 구분되는 이름을 사용하는 것을 권장해요.

♻️ 변수명 구분 제안
-        {policy.chapter.map((chapter: Chapter, idx) => (
-          <article key={idx}>
+        {policy.chapter.map((chapter: Chapter, chapterIdx) => (
+          <article key={chapterIdx}>
             {chapter.title && (
               <h3 className={styles.subTitle}>{chapter.title}</h3>
             )}
             <div className={styles.content}>
-              {chapter.contents.map((content, idx) => (
-                <p key={idx}>{content}</p>
+              {chapter.contents.map((content, contentIdx) => (
+                <p key={contentIdx}>{content}</p>
               ))}
             </div>
           </article>
         ))}
📝 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
export const UsePolicyContent = () => {
return TERMS_OF_USE.map((policy: Chapters) => {
return (
<section key={policy.chapterTitle}>
<h2 className={styles.title}>{policy.chapterTitle}</h2>
{policy.chapter.map((chapter: Chapter, idx) => (
<article key={idx}>
{chapter.title && (
<h3 className={styles.subTitle}>{chapter.title}</h3>
)}
<div className={styles.content}>
{chapter.contents.map((content, idx) => (
<p key={idx}>{content}</p>
))}
</div>
</article>
))}
</section>
);
});
};
export const UsePolicyContent = () => {
return TERMS_OF_USE.map((policy: Chapters) => {
return (
<section key={policy.chapterTitle}>
<h2 className={styles.title}>{policy.chapterTitle}</h2>
{policy.chapter.map((chapter: Chapter, chapterIdx) => (
<article key={chapterIdx}>
{chapter.title && (
<h3 className={styles.subTitle}>{chapter.title}</h3>
)}
<div className={styles.content}>
{chapter.contents.map((content, contentIdx) => (
<p key={contentIdx}>{content}</p>
))}
</div>
</article>
))}
</section>
);
});
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/onboarding/ui/policy-modal/use-policy-content.tsx` around lines
5 - 26, In UsePolicyContent, avoid shadowing the same idx name in nested
iterators: in the policy.chapter.map callback (policy.chapter.map((chapter:
Chapter, idx) => ...)) rename idx to chapterIdx and in
chapter.contents.map((content, idx) => ...) rename idx to contentIdx (or
similar) so each key variable is distinct; update the corresponding key props
(e.g., key={chapterIdx} and key={contentIdx}) and any references to those
variables, locating them via TERMS_OF_USE, policy.chapter.map, and
chapter.contents.map.

Comment on lines +50 to +55
export const XButton = style({
maxWidth: "2.4rem",
maxHeight: "2.4rem",
alignSelf: "flex-end",
color: "black",
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

하드코딩된 색상 값을 themeVars로 변경해 주세요.

XButtoncolor: "black"이 하드코딩되어 있어요. 디자인 토큰 일관성을 위해 themeVars.color를 사용하는 것이 좋아요.

♻️ 제안하는 수정 사항
 export const XButton = style({
   maxWidth: "2.4rem",
   maxHeight: "2.4rem",
   alignSelf: "flex-end",
-  color: "black",
+  color: themeVars.color.gray900, // 또는 적절한 themeVars 색상
 });

As per coding guidelines, "color, typography, shadow, zIndex 은 반드시 themeVars를 통해 사용 - 하드코딩된 값(hex, rgb, fontSize, fontWeight 등) 사용 지양".

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

In `@src/shared/ui/modal/modal.css.ts` around lines 50 - 55, Replace the hardcoded
color in the XButton style with the project theme token: change the color:
"black" usage inside the XButton style definition to reference themeVars.color
(e.g., the appropriate key like themeVars.color.text or themeVars.color.primary
per your theme API) so the component uses the design tokens instead of a literal
string; update any imports to ensure themeVars is imported into the file if not
already.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
src/shared/ui/modal/modal.stories.tsx (1)

95-113: ⚠️ Potential issue | 🟡 Minor

WithImage 스토리에서 size prop 전달이 필요한지 확인해 주세요.

이전에 Modal.Content에 있던 type="auto"가 제거되었는데, Modal 컴포넌트의 size prop이 이를 대체하고 있습니다. 다만 문제는 ModalTemplate이 현재 size prop을 받거나 Modal에 전달하지 않다는 점이에요.

// 현재 ModalTemplate (line 43)
<Modal isOpen={isOpen} onClose={() => setIsOpen(false)}>
  {children}
</Modal>

이는 WithImage 스토리가 기본값(size의 기본값)으로 렌더링될 수 밖에 없다는 뜻입니다. 이미지를 포함한 모달에서 이전의 자동 사이징이 필요하다면, ModalTemplate을 수정해서 size prop을 지원하도록 업데이트하거나, WithImage 스토리에서 직접 Modal을 사용하는 방식을 고려해 봐도 좋을 것 같습니다.

의도된 변경이고 기본 사이징으로 충분하다면 무시해 주세요!

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

In `@src/shared/ui/modal/modal.stories.tsx` around lines 95 - 113, The WithImage
story now needs to ensure the Modal receives the intended size (previously
handled by Modal.Content type="auto"); update ModalTemplate to accept a size
prop and pass it through to the underlying <Modal> (i.e., change ModalTemplate
to accept size and call <Modal isOpen={isOpen} size={size} onClose=...>) or
alternatively change the WithImage story to bypass ModalTemplate and render
<Modal size="auto"> with its children directly; reference ModalTemplate,
WithImage, Modal, Modal.Content, and Modal.Image when making the change so the
image modal uses the correct sizing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/shared/ui/modal/modal.stories.tsx`:
- Around line 95-113: The WithImage story now needs to ensure the Modal receives
the intended size (previously handled by Modal.Content type="auto"); update
ModalTemplate to accept a size prop and pass it through to the underlying
<Modal> (i.e., change ModalTemplate to accept size and call <Modal
isOpen={isOpen} size={size} onClose=...>) or alternatively change the WithImage
story to bypass ModalTemplate and render <Modal size="auto"> with its children
directly; reference ModalTemplate, WithImage, Modal, Modal.Content, and
Modal.Image when making the change so the image modal uses the correct sizing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2185b2a1-8477-4c52-9ae7-d1193dd94ba2

📥 Commits

Reviewing files that changed from the base of the PR and between 6649060 and 7b0c65a.

📒 Files selected for processing (2)
  • src/shared/ui/modal/modal.css.ts
  • src/shared/ui/modal/modal.stories.tsx

Copy link
Copy Markdown
Collaborator

@qowjdals23 qowjdals23 left a comment

Choose a reason for hiding this comment

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

이용약관 모달 / 페이지 구현뿐만 아니라 온보딩 흐름 안에 자연스럽게 연결해주셔서 너무 좋네용
두 종류의 약관 모달도 type 기반으로 하나의 PolicyModal 안에서 정리해주셔서 중복을 잘 줄여주신 것 같아요 !!!

작업 많으셨을 것 같은데 수고 많으셨습니다 ~~~ !!!! 전반적으로 흐름이 이미 잘 잡혀 있어서 바로 어푸 할게요 !!!! 👍 👍 🏊
(유진님이 중요한 포인트들 꼼꼼하게 잘 봐주셔서 편하게 따라가며 확인할 수 있었습니다 !! ㅎㅎ)

icon={<IconPen width={48} height={48} />}
title="아직 등록된 경험이 없어요"
subTitle="경험을 등록하고 AI매칭을 시작해보세요"
closeText="나중에할게요"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

띄어쓰기 추가하면 좋을 것 같아요 !!!! 💫 😄

Suggested change
closeText="나중에할게요"
closeText="나중에 할게요"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

저 초등학교 때 받아쓰기를 못했습니다 .... 우리 팀에도 맞춤법 선생님이 있어 다행이다.. ((수정완완

odukong and others added 5 commits March 31, 2026 17:08
- 카카오로그인 시에 두 번씩 요청되던 문제를 ref를 통해 한 번만 요청이 보내지도록 수정하였습니다
- refreshToken에 대한 요청을 cookie로 통신함에 따라 관련 axios-instance.ts 로직을 수정하였습니다.
@odukong odukong merged commit 94896a2 into dev Mar 31, 2026
3 checks passed
@odukong odukong deleted the feat/#159/onboarding-agree-modal branch March 31, 2026 17:11
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.

[Feat] 온보딩 내 이용약관 및 개인정보처리방침 모달 추가

3 participants