Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 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" /> },
];
14 changes: 8 additions & 6 deletions src/features/experience-detail/model/use-leave-confirm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useCallback } from "react";
import { useBlocker } from "react-router-dom";

import { IconWarn } from "@/shared/assets/icons";
import { modalStore } from "@/shared/model/store";
import { ModalBasic } from "@/shared/ui";

Expand Down Expand Up @@ -65,12 +66,13 @@ export const useLeaveConfirm = () => {
if (blocker.state === "blocked") {
modalStore.open(
<ModalBasic
title={`작성중인 내용이 있습니다.\n정말 나가시겠습니까?`}
subTitle="저장하지 않으면 내용이 사라져요."
closeText="이어서 작성"
confirmText="나가기"
onClose={cancelLeave}
onConfirm={confirmLeave}
icon={<IconWarn width={48} height={48} />}
title={`작성 중인 내용이 있어요`}
subTitle="저장하지 않으면 내용이 모두 사라져요."
closeText="나가기"
confirmText="계속 작성하기"
onClose={confirmLeave}
onConfirm={cancelLeave}
/>,
Comment on lines 68 to 76
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.

0,
undefined,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { IconTrash } from "@/shared/assets/icons";
import { EXPERIENCE_TYPE } from "@/shared/config/experience";
import { parseYMD } from "@/shared/lib/format-date";
import { modalStore } from "@/shared/model/store";
Expand Down Expand Up @@ -42,15 +43,18 @@ const ExperienceViewer = () => {
const handleOpenDeleteModal = () => {
modalStore.open(
<ModalBasic
title="이 경험을 삭제하시겠습니까?"
subTitle="작성한 내용은 즉시 제거되며, 복구할 수 없습니다."
closeText="취소"
confirmText="삭제"
onClose={() => modalStore.reset()} // 취소 시 닫기
onConfirm={() => {
icon={<IconTrash width={48} height={48} />}
title="이 경험을 삭제할까요?"
subTitle="삭제하면 다시 복구할 수 없어요"
closeText="삭제하기"
confirmText="취소하기"
onClose={() => {
onClickDelete(); // 실제 삭제 동작
modalStore.reset(); // 모달 닫기
}}
onConfirm={() => {
modalStore.reset(); // 취소 시 닫기
}}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";

import { ROUTES } from "@/app/routes/paths";
import { IconPen } from "@/shared/assets/icons";
import { modalStore } from "@/shared/model/store";
import { Button, Modal } from "@/shared/ui";
import { Modal, ModalBasic } from "@/shared/ui";
import {
useGetExperience,
useGetCompanyList,
Expand Down Expand Up @@ -36,23 +37,21 @@ export const SelectCompany = ({ onClick }: { onClick: () => void }) => {
useEffect(() => {
if (data?.totalElements === 0) {
modalStore.open(
<>
<Modal.Content>
<Modal.Title>아직 등록된 경험이 없습니다</Modal.Title>
<Modal.SubTitle>지금 바로 경험을 등록하러 가볼까요?</Modal.SubTitle>
</Modal.Content>
<Modal.Buttons>
<Button variant="secondary" onClick={() => navigate(ROUTES.HOME)}>
나가기
</Button>
<Button
variant="primary"
onClick={() => navigate(ROUTES.EXPERIENCE_CREATE)}
>
이동하기
</Button>
</Modal.Buttons>
</>,
<ModalBasic
onClose={() => {
modalStore.close("NO-EXPERIENCE");
navigate(ROUTES.HOME);
}}
onConfirm={() => {
modalStore.close("NO-EXPERIENCE");
navigate(ROUTES.EXPERIENCE_CREATE);
}}
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.

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

confirmText="경험 등록하기"
/>,
undefined,
undefined,
"NO-EXPERIENCE"
Expand All @@ -65,13 +64,15 @@ export const SelectCompany = ({ onClick }: { onClick: () => void }) => {
// 기업 선택 후, 대기하는 모달
modalStore.open(
<>
<Modal.Content type="auto">
<Modal.Title>
{josa(selectedCompany.name, "을/를")} 선택하셨습니다
</Modal.Title>
<Modal.SubTitle>기업분석 내용을 불러오는 중입니다.</Modal.SubTitle>
<Modal.Content>
<Modal.Image />
<Modal.TitleGroup>
<Modal.Title>
{josa(selectedCompany.name, "을/를")} 선택했어요
</Modal.Title>
<Modal.SubTitle>기업분석 내용을 불러오고 있어요</Modal.SubTitle>
</Modal.TitleGroup>
</Modal.Content>
<Modal.Image />
</>,
3000,
() => {
Expand Down
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,
});
Loading