diff --git a/src/features/experience-matching/ui/select-company/select-company.tsx b/src/features/experience-matching/ui/select-company/select-company.tsx
index e1e3cf65..83da168d 100644
--- a/src/features/experience-matching/ui/select-company/select-company.tsx
+++ b/src/features/experience-matching/ui/select-company/select-company.tsx
@@ -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,
@@ -36,23 +37,21 @@ export const SelectCompany = ({ onClick }: { onClick: () => void }) => {
useEffect(() => {
if (data?.totalElements === 0) {
modalStore.open(
- <>
-
- 아직 등록된 경험이 없습니다
- 지금 바로 경험을 등록하러 가볼까요?
-
-
-
-
-
- >,
+
{
+ modalStore.close("NO-EXPERIENCE");
+ navigate(ROUTES.HOME);
+ }}
+ onConfirm={() => {
+ modalStore.close("NO-EXPERIENCE");
+ navigate(ROUTES.EXPERIENCE_CREATE);
+ }}
+ icon={}
+ title="아직 등록된 경험이 없어요"
+ subTitle="경험을 등록하고 AI매칭을 시작해보세요"
+ closeText="나중에 할게요"
+ confirmText="경험 등록하기"
+ />,
undefined,
undefined,
"NO-EXPERIENCE"
@@ -65,13 +64,15 @@ export const SelectCompany = ({ onClick }: { onClick: () => void }) => {
// 기업 선택 후, 대기하는 모달
modalStore.open(
<>
-
-
- {josa(selectedCompany.name, "을/를")} 선택하셨습니다
-
- 기업분석 내용을 불러오는 중입니다.
+
+
+
+
+ {josa(selectedCompany.name, "을/를")} 선택했어요
+
+ 기업분석 내용을 불러오고 있어요
+
-
>,
3000,
() => {
diff --git a/src/features/onboarding/index.ts b/src/features/onboarding/index.ts
index 0ab6a244..9efb4999 100644
--- a/src/features/onboarding/index.ts
+++ b/src/features/onboarding/index.ts
@@ -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";
diff --git a/src/features/onboarding/lib/onboarding-form.validator.ts b/src/features/onboarding/lib/onboarding-form.validator.ts
index eeb57a63..1d3c2a0b 100644
--- a/src/features/onboarding/lib/onboarding-form.validator.ts
+++ b/src/features/onboarding/lib/onboarding-form.validator.ts
@@ -6,8 +6,10 @@ export const isOnboardingFormComplete = (params: {
selectedUniversity: SearchItem | null;
industry: Record;
job: Record;
+ isAgreed: boolean;
}) => {
- const { selectedEducation, selectedUniversity, industry, job } = params;
+ const { selectedEducation, selectedUniversity, industry, job, isAgreed } =
+ params;
const hasEducation = Boolean(selectedEducation);
const hasUniversity = Boolean(selectedUniversity);
@@ -15,5 +17,5 @@ export const isOnboardingFormComplete = (params: {
const hasIndustry1 = Boolean(industry[1]);
const hasJob1 = Boolean(job[1]);
- return hasEducation && hasUniversity && hasIndustry1 && hasJob1;
+ return hasEducation && hasUniversity && hasIndustry1 && hasJob1 && isAgreed;
};
diff --git a/src/features/onboarding/ui/policy-modal/policy-modal.css.ts b/src/features/onboarding/ui/policy-modal/policy-modal.css.ts
new file mode 100644
index 00000000..24cd53a2
--- /dev/null
+++ b/src/features/onboarding/ui/policy-modal/policy-modal.css.ts
@@ -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",
+});
+
+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,
+});
diff --git a/src/features/onboarding/ui/policy-modal/policy-modal.tsx b/src/features/onboarding/ui/policy-modal/policy-modal.tsx
new file mode 100644
index 00000000..414d1732
--- /dev/null
+++ b/src/features/onboarding/ui/policy-modal/policy-modal.tsx
@@ -0,0 +1,46 @@
+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;
+}
+
+const POLICY_MODAL_CONTENT = {
+ USE: {
+ title: "이용약관",
+ Content: UsePolicyContent,
+ },
+ PRIVACY: {
+ title: "개인정보처리방침",
+ Content: PrivacyPolicyContent,
+ },
+};
+
+export const PolicyModal = ({ type, onClose }: PolicyModalProps) => {
+ const { title, Content } = POLICY_MODAL_CONTENT[type]; // 타입에 따른 약관모달 선택
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/features/onboarding/ui/policy-modal/privacy-policy-content.tsx b/src/features/onboarding/ui/policy-modal/privacy-policy-content.tsx
new file mode 100644
index 00000000..dfbe7d1e
--- /dev/null
+++ b/src/features/onboarding/ui/policy-modal/privacy-policy-content.tsx
@@ -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 (
+
+ {/** 개인정보처리방침 타이틀 */}
+
+
+
{TERMS_OF_PRIVACY_INFO.title}
+
{TERMS_OF_PRIVACY_INFO.date}
+
+
{TERMS_OF_PRIVACY_INFO.description}
+
+ {/** 조항 리스트 (ex. 1. 개인정보의 수집 및 이용) */}
+ {TERMS_OF_PRIVACY_INFO.sections.map((section: Section) => (
+
+ {/* 조항 타이틀 및 설명 */}
+
+
+ {section.title}
+
+
{section.description}
+
+ {/** 조항 상세설명 */}
+
+ {section.articles?.map((article: Article) => (
+
+ {/** 조항 상세설명의 타이틀 (ex. 가. 회원가입 및 계정 관리) */}
+ {article.title && (
+
+ {article.title}
+
+ )}
+
+ {article.content && (
+
+ {/* 줄글 형태의 컨텐츠 */}
+ {"text" in article.content && (
+
{article.content.text}
+ )}
+ {/* 테이블 형태의 컨텐츠 */}
+ {"table" in article.content && (
+
+
+
+
+ {article.content.table.header.map(
+ (th, thIdx) => (
+ |
+ {th}
+ |
+ )
+ )}
+
+
+
+ {article.content.table.rows.map((row, rowIdx) => (
+
+ {row.map((td, tdIdx) => (
+ |
+ {td}
+ |
+ ))}
+
+ ))}
+
+
+
+ )}
+
+ )}
+
+
+ ))}
+
+ {/** 조항 추가사항 */}
+ {section.alert && {section.alert}
}
+
+ ))}
+
+ );
+};
diff --git a/src/features/onboarding/ui/policy-modal/use-policy-content.tsx b/src/features/onboarding/ui/policy-modal/use-policy-content.tsx
new file mode 100644
index 00000000..8a60cead
--- /dev/null
+++ b/src/features/onboarding/ui/policy-modal/use-policy-content.tsx
@@ -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 (
+
+ {policy.chapterTitle}
+
+ {policy.chapter.map((chapter: Chapter, idx) => (
+
+ {chapter.title && (
+ {chapter.title}
+ )}
+
+ {chapter.contents.map((content, idx) => (
+
{content}
+ ))}
+
+
+ ))}
+
+ );
+ });
+};
diff --git a/src/pages/login/kakao-login-page.tsx b/src/pages/login/kakao-login-page.tsx
index 6d2aabb2..21d44775 100644
--- a/src/pages/login/kakao-login-page.tsx
+++ b/src/pages/login/kakao-login-page.tsx
@@ -1,4 +1,4 @@
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useAuthStore } from "@/app/store";
@@ -9,7 +9,9 @@ const KakaoLoginPage = () => {
const navigate = useNavigate();
const { actions } = useAuthStore();
- const code = new URL(window.location.href).searchParams.get("code");
+ const [code] = useState(() =>
+ new URL(window.location.href).searchParams.get("code")
+ );
const { data } = useLogin(code ?? "");
diff --git a/src/pages/onboarding/onboarding-page.css.ts b/src/pages/onboarding/onboarding-page.css.ts
index 46937712..006c1c3b 100644
--- a/src/pages/onboarding/onboarding-page.css.ts
+++ b/src/pages/onboarding/onboarding-page.css.ts
@@ -1,4 +1,5 @@
import { style } from "@vanilla-extract/css";
+import { recipe } from "@vanilla-extract/recipes";
import { themeVars } from "@/app/styles";
@@ -106,6 +107,59 @@ export const sectionGroup = style({
gap: "4rem",
});
+export const agreeGroup = style({
+ display: "flex",
+ alignItems: "center",
+ gap: "1.6rem",
+});
+
+export const agreeContent = style({
+ display: "flex",
+ alignItems: "center",
+ color: themeVars.color.gray800,
+ ...themeVars.fontStyles.body_m_16,
+ fontWeight: 500,
+});
+
+export const checkbox = recipe({
+ base: {
+ display: "flex",
+ justifyContent: "center",
+ alignItems: "center",
+ width: "1.8rem",
+ height: "1.8rem",
+ aspectRatio: 1 / 1,
+ borderRadius: "2px",
+ cursor: "pointer",
+ },
+ variants: {
+ isAgreed: {
+ true: {
+ border: `1.6px solid ${themeVars.color.blue400}`,
+ background: themeVars.color.blue500,
+ },
+ false: {
+ border: `1px solid ${themeVars.color.gray400}`,
+ background: themeVars.color.white,
+ },
+ },
+ },
+ defaultVariants: {
+ isAgreed: false,
+ },
+});
+
+export const underlineText = style({
+ textDecoration: "underline",
+ textUnderlinePosition: "under",
+ color: themeVars.color.blue600,
+ selectors: {
+ "&:hover": {
+ cursor: "pointer",
+ },
+ },
+});
+
export const buttonWrap = style({
width: "34rem",
diff --git a/src/pages/onboarding/onboarding-page.tsx b/src/pages/onboarding/onboarding-page.tsx
index 68a1f5e6..c4b196cb 100644
--- a/src/pages/onboarding/onboarding-page.tsx
+++ b/src/pages/onboarding/onboarding-page.tsx
@@ -13,6 +13,7 @@ import { labelToCodeIndustry } from "@/shared/config";
import { Button, Alert } from "@/shared/ui";
import * as s from "./onboarding-page.css";
+import { AgreeSection } from "./ui/agree-section";
import { SelectSection } from "./ui/select-section";
import type { EducationTypeCode } from "@/features/onboarding";
@@ -27,6 +28,7 @@ const OnboardingPage = () => {
useState(null);
const [selectedUniversity, setSelectedUniversity] =
useState(null);
+ const [isAgreed, setIsAgreed] = useState(false); // 이용약관 및 개인정보처리방침 동의 여부
const industry = useInterestSelectStore((s) => s.industry);
const job = useInterestSelectStore((s) => s.job);
@@ -41,8 +43,9 @@ const OnboardingPage = () => {
selectedUniversity,
industry,
job,
+ isAgreed,
}),
- [selectedEducation, selectedUniversity, industry, job]
+ [selectedEducation, selectedUniversity, industry, job, isAgreed]
);
const handleSelectionSubmit = () => {
@@ -98,6 +101,8 @@ const OnboardingPage = () => {
setSelectedUniversity={setSelectedUniversity}
/>
+
+