Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d7e19f3
feat: 온보딩 페이지 이용약관 문구 UI 추가 (#159)
odukong Mar 20, 2026
2b90d5a
feat: 온보딩 유효성 검사 로직, 동의항목 추가 (#159)
odukong Mar 20, 2026
235820f
refactor: 모달 크기(width, height)를 커스텀하기 위한 size 속성 추가 (#159)
odukong Mar 20, 2026
4a403e8
design: 모달 X 버튼 사이즈 수정 (#159)
odukong Mar 20, 2026
2a2fb47
feat: 이용약관 텍스트 데이터 생성 (#159)
odukong Mar 20, 2026
405f84b
feat: 이용약관 모달 UI 구현 (#159)
odukong Mar 20, 2026
7ba628d
refactor: 이용약관&개인정보처리방침 모달 컨텐츠 공유를 위한 Content 컴포넌트 분리 (#159)
odukong Mar 21, 2026
4ad1dda
feat: 개인정보처리방침 UI 구현 (#159)
odukong Mar 21, 2026
4e6ad76
feat: 이용약관&개인정보처리방침 페이지 UI 구현 (#159)
odukong Mar 22, 2026
aa3002f
refactor: policypage 중복 코드 제거 (#159)
odukong Mar 22, 2026
3eb508f
fix: 코드래빗 수정사항 반영 (#159)
odukong Mar 23, 2026
421908a
design: 개인정보처리방침 페이지 gap 수정 (#159)
odukong Mar 24, 2026
dd9b4f9
refactor: 체크박스 이벤트와 약관 모달 이벤트 분리 (#159)
odukong Mar 25, 2026
2ff1fc6
design: 모달 컴포넌트 박스 영역 반응형 적용 (#159)
odukong Mar 25, 2026
06667ad
fix: 누락된 key 추가 (#159)
odukong Mar 25, 2026
0f6e5dd
refactor: 약관 모달 조건부 렌더링 로직을 객체 매핑 방식으로 개선 (#159)
odukong Mar 25, 2026
4658aea
design: 모달 아이콘 추가 (#159)
odukong Mar 26, 2026
6919883
design: medium 버튼 텍스트 크기 body_m_14로 변경 (#159)
odukong Mar 26, 2026
b246e98
design: Modal css 수정사항 반영 (#159)
odukong Mar 26, 2026
6649060
refactor: Modal css수정에 따른 코드 수정 (#159)
odukong Mar 26, 2026
7b0c65a
design: 코드래빗 수정사항 반영 (#159)
odukong Mar 27, 2026
ad92565
design: 모달 텍스트 띄어쓰기 추가 (#159)
odukong Mar 31, 2026
7d20951
fix: 카카오로그인 로직 수정 및 리프레쉬 토큰 오류 해결 (#159)
odukong Mar 31, 2026
c563804
Merge branch 'dev' into feat/#159/onboarding-agree-modal
odukong Mar 31, 2026
9a94bbd
fix: 빌드 에러 해결 (#159)
odukong Mar 31, 2026
e50c4b5
fix: ai-report 생성 중복 요청 에러 해결 (#159)
odukong Mar 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/app/providers/modal-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface ModalItem {
id: string;
content: ReactNode;
autoPlay?: number;
size?: "default" | "auto";
}

export const ModalProvider = () => {
Expand All @@ -33,6 +34,7 @@ export const ModalProvider = () => {
isOpen={true}
autoPlay={modal.autoPlay}
onClose={() => modalStore.close(modal.id)}
size={modal.size}
>
{modal.content}
</Modal>
Expand Down
3 changes: 3 additions & 0 deletions src/app/routes/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@ export const ROUTES = {
EXPERIENCE_DETAIL: (id = ":id") => `/experience/${id}`, // 경험 상세
EXPERIENCE_EDIT: (id = ":id") => `/experience/${id}/edit`, // 경험 수정

POLICY_USE: "/policy/terms", // 이용약관
POLICY_PRIVACY: "/policy/privacy", // 개인정보처리방침

MYPAGE: "/mypage",
};
8 changes: 8 additions & 0 deletions src/app/routes/public-routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,19 @@ const CompanyDetailPage = lazy(() =>
}))
);

const PolicyPage = lazy(() =>
import("@/pages/policy/policy-page").then((module) => ({
default: module.PolicyPage,
}))
);

export const guestRoutes = [{ path: ROUTES.LOGIN, element: <LoginPage /> }];

export const publicRoutes = [
{ path: ROUTES.LOGIN_AUTH, element: <KakaoLoginPage /> },
{ path: ROUTES.LANDING, element: <LandingPage /> },
{ path: ROUTES.HOME, element: <HomePage /> },
{ path: ROUTES.COMPANY(), element: <CompanyDetailPage /> },
{ path: ROUTES.POLICY_USE, element: <PolicyPage mode="USE" /> },
{ path: ROUTES.POLICY_PRIVACY, element: <PolicyPage mode="PRIVACY" /> },
];
2 changes: 2 additions & 0 deletions src/features/onboarding/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ export * from "./store/interest-select/selectors";

export { useGetUniversity } from "./api/use-get-university.query";
export { usePostOnboarding } from "./api/use-post-onboarding.mutation";

export { PolicyModal } from "./ui/policy-modal/policy-modal";
6 changes: 4 additions & 2 deletions src/features/onboarding/lib/onboarding-form.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ export const isOnboardingFormComplete = (params: {
selectedUniversity: SearchItem | null;
industry: Record<number, unknown>;
job: Record<number, unknown>;
isAgreed: boolean;
}) => {
const { selectedEducation, selectedUniversity, industry, job } = params;
const { selectedEducation, selectedUniversity, industry, job, isAgreed } =
params;

const hasEducation = Boolean(selectedEducation);
const hasUniversity = Boolean(selectedUniversity);

const hasIndustry1 = Boolean(industry[1]);
const hasJob1 = Boolean(job[1]);

return hasEducation && hasUniversity && hasIndustry1 && hasJob1;
return hasEducation && hasUniversity && hasIndustry1 && hasJob1 && isAgreed;
};
154 changes: 154 additions & 0 deletions src/features/onboarding/ui/policy-modal/policy-modal.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes";

import { themeVars } from "@/app/styles";

export const wrapper = style({
width: "40rem",
paddingBottom: "1.6rem",
});

export const modalHeader = style({
width: "100%",
position: "relative",
display: "flex",
justifyContent: "center",
alignItems: "center",
padding: "1.6rem",
borderBottom: `1px solid ${themeVars.color.normal}`,
...themeVars.fontStyles.hline_b_18,
});

export const buttonWrapper = style({
position: "absolute",
right: "1.6rem",
width: "2.4rem",
height: "2.4rem",
});

export const modalContent = style({
display: "flex",
flexDirection: "column",
gap: "2.4rem",
textAlign: "left",
height: "28rem",
padding: "2rem 3rem",
marginBottom: "2rem",
overflowY: "auto",

selectors: {
"&::-webkit-scrollbar": {
width: "1.2rem",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: themeVars.color.gray300,
height: "5rem",
borderRadius: "100px",
backgroundClip: "padding-box",
border: `4px solid transparent`,
},
"&::-webkit-scrollbar-track": {
backgroundColor: "transparent",
margin: "0.8rem 1.2rem",
},
},
});

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",
});
Comment on lines +57 to +77
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 및 다른 반복 위치들을 수정하세요.


export const textStyle = recipe({
base: {
color: themeVars.color.gray800,
},
variants: {
type: {
title1: {
...themeVars.fontStyles.body_b_16,
fontWeight: 700,
},
title2: {
...themeVars.fontStyles.body_b_14,
},
title3: {
...themeVars.fontStyles.body_r_14,
fontWeight: 400,
},
},
},
defaultVariants: {
type: "title1",
},
});

export const flexColumn = recipe({
base: {
display: "flex",
flexDirection: "column",
},
variants: {
gap: {
8: { gap: "0.8rem" },
16: { gap: "1.6rem" },
24: { gap: "2.4rem" },
},
},
});

export const tableWrapper = style({
width: "100%",
overflowX: "auto",
selectors: {
"&::-webkit-scrollbar": {
display: "none",
},
},
});

export const table = style({
width: "max-content",
minWidth: "100%",
borderCollapse: "collapse",
});

export const tCell = style({
minWidth: "10rem",
maxWidth: "25rem",
padding: "0.8rem",
border: `1px solid ${themeVars.color.gray200}`,
fontWeight: 400,
verticalAlign: "top",
wordBreak: "keep-all",
});

export const thead = style({
backgroundColor: themeVars.color.gray100,
});

export const th = style({
whiteSpace: "nowrap",
padding: "1rem 0.8rem",
});

export const tableText = style({
fontWeight: 400,
});
33 changes: 33 additions & 0 deletions src/features/onboarding/ui/policy-modal/policy-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Button, Modal } from "@/shared/ui";

import * as styles from "./policy-modal.css";
import { PrivacyPolicyContent } from "./privacy-policy-content";
import { UsePolicyContent } from "./use-policy-content";

interface PolicyModalProps {
type: "USE" | "PRIVACY";
onClose: () => void;
}

export const PolicyModal = ({ type, onClose }: PolicyModalProps) => {
return (
<div className={styles.wrapper}>
<div className={styles.modalHeader}>
<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 대신 컴포넌트 함수 자체를 할당하는 방식으로 디벨롭해두었습니다!

</div>
</Modal.Content>
<Modal.Buttons>
<Button variant="primary" size="full" onClick={onClose}>
확인
</Button>
</Modal.Buttons>
</div>
);
};
94 changes: 94 additions & 0 deletions src/features/onboarding/ui/policy-modal/privacy-policy-content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import {
TERMS_OF_PRIVACY_INFO,
type Article,
type Section,
} from "@/shared/config";

import * as styles from "./policy-modal.css";

export const PrivacyPolicyContent = () => {
return (
<div className={`${styles.content} ${styles.flexColumn({ gap: 24 })}`}>
{/** 개인정보처리방침 타이틀 */}
<div className={styles.flexColumn({ gap: 16 })}>
<div>
<h3 className={styles.textStyle()}>{TERMS_OF_PRIVACY_INFO.title}</h3>
<p>{TERMS_OF_PRIVACY_INFO.date}</p>
</div>
<p>{TERMS_OF_PRIVACY_INFO.description}</p>
</div>
{/** 조항 리스트 (ex. 1. 개인정보의 수집 및 이용) */}
{TERMS_OF_PRIVACY_INFO.sections.map((section: Section) => (
<section key={section.title} className={styles.flexColumn({ gap: 8 })}>
{/* 조항 타이틀 및 설명 */}
<div>
<h4 className={styles.textStyle({ type: "title2" })}>
{section.title}
</h4>
<p>{section.description}</p>
</div>
{/** 조항 상세설명 */}
<div className={styles.flexColumn({ gap: 16 })}>
{section.articles?.map((article: Article) => (
<article
key={article.title}
className={styles.flexColumn({ gap: 8 })}
>
{/** 조항 상세설명의 타이틀 (ex. 가. 회원가입 및 계정 관리) */}
{article.title && (
<p className={styles.textStyle({ type: "title3" })}>
{article.title}
</p>
)}
<div>
{article.content && (
<div>
{/* 줄글 형태의 컨텐츠 */}
{"text" in article.content && (
<div>{article.content.text}</div>
)}
{/* 테이블 형태의 컨텐츠 */}
{"table" in article.content && (
<div className={`${styles.tableWrapper}`}>
<table className={`${styles.table}`}>
<thead className={styles.thead}>
<tr>
{article.content.table.header.map(
(th, thIdx) => (
<th
key={thIdx}
className={`${styles.tCell} ${styles.th}`}
>
{th}
</th>
)
)}
</tr>
</thead>
<tbody>
{article.content.table.rows.map((row, rowIdx) => (
<tr key={rowIdx}>
{row.map((td, tdIdx) => (
<td key={tdIdx} className={styles.tCell}>
{td}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
</div>
</article>
))}
</div>
{/** 조항 추가사항 */}
{section.alert && <div>{section.alert}</div>}
</section>
))}
</div>
);
};
26 changes: 26 additions & 0 deletions src/features/onboarding/ui/policy-modal/use-policy-content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { TERMS_OF_USE, type Chapters, type Chapter } from "@/shared/config";

import * as styles from "./policy-modal.css";

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) => (
<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

</div>
</article>
))}
</section>
);
});
};
Comment on lines +5 to +26
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.

Loading
Loading