From d7e19f368c2d7ff1892686012538946a7910899f Mon Sep 17 00:00:00 2001 From: odukong Date: Fri, 20 Mar 2026 14:22:50 +0900 Subject: [PATCH 01/25] =?UTF-8?q?feat:=20=EC=98=A8=EB=B3=B4=EB=94=A9=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9D=B4=EC=9A=A9=EC=95=BD?= =?UTF-8?q?=EA=B4=80=20=EB=AC=B8=EA=B5=AC=20UI=20=EC=B6=94=EA=B0=80=20(#15?= =?UTF-8?q?9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/onboarding/onboarding-page.css.ts | 49 +++++++++++++++++++++ src/pages/onboarding/onboarding-page.tsx | 4 ++ src/pages/onboarding/ui/agree-section.tsx | 34 ++++++++++++++ src/shared/assets/icons/check3.svg | 3 ++ src/shared/assets/icons/index.ts | 1 + 5 files changed, 91 insertions(+) create mode 100644 src/pages/onboarding/ui/agree-section.tsx create mode 100644 src/shared/assets/icons/check3.svg diff --git a/src/pages/onboarding/onboarding-page.css.ts b/src/pages/onboarding/onboarding-page.css.ts index 46937712..689c2a10 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,54 @@ export const sectionGroup = style({ gap: "4rem", }); +export const agreeGroup = style({ + display: "flex", + alignItems: "center", +}); + +export const agreeContent = style({ + display: "flex", + alignItems: "center", + gap: "1.6rem", + 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, +}); + export const buttonWrap = style({ width: "34rem", diff --git a/src/pages/onboarding/onboarding-page.tsx b/src/pages/onboarding/onboarding-page.tsx index 68a1f5e6..159ea531 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); @@ -98,6 +100,8 @@ const OnboardingPage = () => { setSelectedUniversity={setSelectedUniversity} /> + +
+ +
+ ); +}; diff --git a/src/pages/onboarding/ui/agree-section.tsx b/src/pages/onboarding/ui/agree-section.tsx index 206f4f2c..25493b95 100644 --- a/src/pages/onboarding/ui/agree-section.tsx +++ b/src/pages/onboarding/ui/agree-section.tsx @@ -1,4 +1,6 @@ +import { UsePolicyModal } from "@/features/onboarding"; import { AgreeCheckIcon } from "@/shared/assets/icons"; +import { modalStore } from "@/shared/model/store"; import * as styles from "../onboarding-page.css"; @@ -8,6 +10,19 @@ interface AgreeSectionProps { } const AgreeSection = ({ isAgreed, setIsAgreed }: AgreeSectionProps) => { + const handleModal = (e: React.MouseEvent, type: "USE" | "PRIVACY") => { + e.preventDefault(); + const MODAL_ID = "ONBOARD_MODAL"; + let content = <>; + + if (type === "USE") { + content = modalStore.close(MODAL_ID)} />; + } else { + content =
; + } + modalStore.open(content, undefined, undefined, MODAL_ID, "auto"); + }; + return (
{

- Comfit 이용약관 및{" "} - 개인정보처리방침에 - 동의합니다. + Comfit{" "} + handleModal(e, "USE")} + > + 이용약관 + {" "} + 및{" "} + handleModal(e, "PRIVACY")} + > + 개인정보처리방침 + + 에 동의합니다.

From 7ba628d7b3c53a9ddece631a465ff8ea5059816a Mon Sep 17 00:00:00 2001 From: odukong Date: Sun, 22 Mar 2026 00:31:00 +0900 Subject: [PATCH 07/25] =?UTF-8?q?refactor:=20=EC=9D=B4=EC=9A=A9=EC=95=BD?= =?UTF-8?q?=EA=B4=80&=EA=B0=9C=EC=9D=B8=EC=A0=95=EB=B3=B4=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=EB=B0=A9=EC=B9=A8=20=EB=AA=A8=EB=8B=AC=20=EC=BB=A8?= =?UTF-8?q?=ED=85=90=EC=B8=A0=20=EA=B3=B5=EC=9C=A0=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20Content=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20(#159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 온보딩에 사용되는 이용약관 모달과 개인정보처리방침 모달은 각 컨텐츠를 제외하고 동일한 형태의 모달을 사용합니다. - 별도의 모달(use-policy-modal, privacy-policy-modal)로 선언하기 보다 모달의 형태는 공유해서 쓸 수 있되, content만 교체해서 쓸 수 있도록 구조를 변경하였습니다. --- src/features/onboarding/index.ts | 2 +- ...olicy-modal.css.ts => policy-modal.css.ts} | 0 .../ui/policy-modal/policy-modal.tsx | 32 +++++++++++++ .../ui/policy-modal/use-policy-content.tsx | 26 +++++++++++ .../ui/policy-modal/use-policy-modal.tsx | 46 ------------------- src/pages/onboarding/ui/agree-section.tsx | 16 +++---- src/shared/config/index.ts | 2 +- .../config/{policy.ts => policy-use-info.ts} | 0 8 files changed, 68 insertions(+), 56 deletions(-) rename src/features/onboarding/ui/policy-modal/{use-policy-modal.css.ts => policy-modal.css.ts} (100%) create mode 100644 src/features/onboarding/ui/policy-modal/policy-modal.tsx create mode 100644 src/features/onboarding/ui/policy-modal/use-policy-content.tsx delete mode 100644 src/features/onboarding/ui/policy-modal/use-policy-modal.tsx rename src/shared/config/{policy.ts => policy-use-info.ts} (100%) diff --git a/src/features/onboarding/index.ts b/src/features/onboarding/index.ts index 9d027ca4..9efb4999 100644 --- a/src/features/onboarding/index.ts +++ b/src/features/onboarding/index.ts @@ -8,4 +8,4 @@ export * from "./store/interest-select/selectors"; export { useGetUniversity } from "./api/use-get-university.query"; export { usePostOnboarding } from "./api/use-post-onboarding.mutation"; -export { UsePolicyModal } from "./ui/policy-modal/use-policy-modal"; +export { PolicyModal } from "./ui/policy-modal/policy-modal"; diff --git a/src/features/onboarding/ui/policy-modal/use-policy-modal.css.ts b/src/features/onboarding/ui/policy-modal/policy-modal.css.ts similarity index 100% rename from src/features/onboarding/ui/policy-modal/use-policy-modal.css.ts rename to src/features/onboarding/ui/policy-modal/policy-modal.css.ts 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..0a43f12c --- /dev/null +++ b/src/features/onboarding/ui/policy-modal/policy-modal.tsx @@ -0,0 +1,32 @@ +import { Button, Modal } from "@/shared/ui"; + +import * as styles from "./policy-modal.css"; +import { UsePolicyContent } from "./use-policy-content"; + +interface modalProps { + type: "USE" | "PRIVACY"; + onClose: () => void; +} + +export const PolicyModal = ({ type, onClose }: modalProps) => { + return ( +
+
+
{type === "USE" ? "이용약관" : "개인정보처리방침"}
+
+ +
+
+ +
+ {type === "USE" ? : <>} +
+
+ + + +
+ ); +}; 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..b15ccd5f --- /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) => ( +
+ {chapter.title && ( +

{chapter.title}

+ )} +
+ {chapter.contents.map((content) => ( +

{content}

+ ))} +
+
+ ))} +
+ ); + }); +}; diff --git a/src/features/onboarding/ui/policy-modal/use-policy-modal.tsx b/src/features/onboarding/ui/policy-modal/use-policy-modal.tsx deleted file mode 100644 index 399db3fc..00000000 --- a/src/features/onboarding/ui/policy-modal/use-policy-modal.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { TERMS_OF_USE, type Chapter, type Chapters } from "@/shared/config"; -import { Button, Modal } from "@/shared/ui"; - -import * as styles from "./use-policy-modal.css"; - -interface modalProps { - onClose: () => void; -} - -export const UsePolicyModal = ({ onClose }: modalProps) => { - return ( -
-
-
이용약관
-
- -
-
- -
- {TERMS_OF_USE.map((policy: Chapters) => { - return ( -
-

{policy.chapterTitle}

- - {policy.chapter.map((chapter: Chapter) => ( -
- {chapter.title && ( -

{chapter.title}

- )} -
{chapter.contents}
-
- ))} -
- ); - })} -
-
- - - -
- ); -}; diff --git a/src/pages/onboarding/ui/agree-section.tsx b/src/pages/onboarding/ui/agree-section.tsx index 25493b95..666e8f75 100644 --- a/src/pages/onboarding/ui/agree-section.tsx +++ b/src/pages/onboarding/ui/agree-section.tsx @@ -1,4 +1,4 @@ -import { UsePolicyModal } from "@/features/onboarding"; +import { PolicyModal } from "@/features/onboarding/ui/policy-modal/policy-modal"; import { AgreeCheckIcon } from "@/shared/assets/icons"; import { modalStore } from "@/shared/model/store"; @@ -13,14 +13,14 @@ const AgreeSection = ({ isAgreed, setIsAgreed }: AgreeSectionProps) => { const handleModal = (e: React.MouseEvent, type: "USE" | "PRIVACY") => { e.preventDefault(); const MODAL_ID = "ONBOARD_MODAL"; - let content = <>; - if (type === "USE") { - content = modalStore.close(MODAL_ID)} />; - } else { - content =
; - } - modalStore.open(content, undefined, undefined, MODAL_ID, "auto"); + modalStore.open( + modalStore.close(MODAL_ID)} />, + undefined, + undefined, + MODAL_ID, + "auto" + ); }; return ( diff --git a/src/shared/config/index.ts b/src/shared/config/index.ts index f27f0d23..bf3be1ab 100644 --- a/src/shared/config/index.ts +++ b/src/shared/config/index.ts @@ -1,3 +1,3 @@ export * from "./company"; export * from "./experience"; -export * from "./policy"; +export * from "./policy-use-info"; diff --git a/src/shared/config/policy.ts b/src/shared/config/policy-use-info.ts similarity index 100% rename from src/shared/config/policy.ts rename to src/shared/config/policy-use-info.ts From 4ad1ddadadbc874f3572c89d5bd65637ad284515 Mon Sep 17 00:00:00 2001 From: odukong Date: Sun, 22 Mar 2026 02:47:48 +0900 Subject: [PATCH 08/25] =?UTF-8?q?feat:=20=EA=B0=9C=EC=9D=B8=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EC=B2=98=EB=A6=AC=EB=B0=A9=EC=B9=A8=20UI=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/policy-modal/policy-modal.css.ts | 81 ++++ .../ui/policy-modal/policy-modal.tsx | 3 +- .../policy-modal/privacy-policy-content.tsx | 91 +++++ src/pages/onboarding/onboarding-page.css.ts | 5 + src/shared/config/index.ts | 1 + src/shared/config/policy-privacy-info.ts | 348 ++++++++++++++++++ 6 files changed, 528 insertions(+), 1 deletion(-) create mode 100644 src/features/onboarding/ui/policy-modal/privacy-policy-content.tsx create mode 100644 src/shared/config/policy-privacy-info.ts diff --git a/src/features/onboarding/ui/policy-modal/policy-modal.css.ts b/src/features/onboarding/ui/policy-modal/policy-modal.css.ts index 4ffcfac2..8efd7fc4 100644 --- a/src/features/onboarding/ui/policy-modal/policy-modal.css.ts +++ b/src/features/onboarding/ui/policy-modal/policy-modal.css.ts @@ -1,4 +1,5 @@ import { style } from "@vanilla-extract/css"; +import { recipe } from "@vanilla-extract/recipes"; import { themeVars } from "@/app/styles"; @@ -67,7 +68,87 @@ export const subTitle = style({ }); 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 index 0a43f12c..6fa55e55 100644 --- a/src/features/onboarding/ui/policy-modal/policy-modal.tsx +++ b/src/features/onboarding/ui/policy-modal/policy-modal.tsx @@ -1,6 +1,7 @@ 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 modalProps { @@ -19,7 +20,7 @@ export const PolicyModal = ({ type, onClose }: modalProps) => {
- {type === "USE" ? : <>} + {type === "USE" ? : }
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..9a56b5cf --- /dev/null +++ b/src/features/onboarding/ui/policy-modal/privacy-policy-content.tsx @@ -0,0 +1,91 @@ +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) => ( + + ) + )} + + + + {article.content.table.rows.map((row, rowIdx) => ( + + {row.map((td, tdIdx) => ( + + ))} + + ))} + +
+ {th} +
+ {td} +
+
+ )} +
+ )} +
+
+ ))} +
+ {/** 조항 추가사항 */} + {section.alert &&
{section.alert}
} +
+ ))} +
+ ); +}; diff --git a/src/pages/onboarding/onboarding-page.css.ts b/src/pages/onboarding/onboarding-page.css.ts index 689c2a10..ee916e65 100644 --- a/src/pages/onboarding/onboarding-page.css.ts +++ b/src/pages/onboarding/onboarding-page.css.ts @@ -153,6 +153,11 @@ export const underlineText = style({ textDecoration: "underline", textUnderlinePosition: "under", color: themeVars.color.blue600, + selectors: { + "&:hover": { + cursor: "pointer", + }, + }, }); export const buttonWrap = style({ diff --git a/src/shared/config/index.ts b/src/shared/config/index.ts index bf3be1ab..16fe3245 100644 --- a/src/shared/config/index.ts +++ b/src/shared/config/index.ts @@ -1,3 +1,4 @@ export * from "./company"; export * from "./experience"; export * from "./policy-use-info"; +export * from "./policy-privacy-info"; diff --git a/src/shared/config/policy-privacy-info.ts b/src/shared/config/policy-privacy-info.ts new file mode 100644 index 00000000..da4bb82d --- /dev/null +++ b/src/shared/config/policy-privacy-info.ts @@ -0,0 +1,348 @@ +export interface Table { + header: string[]; + rows: string[][]; +} + +export interface Article { + title?: string; + content?: { text: string } | { table: Table }; +} + +export interface Section { + title: string; + description: string; + articles?: Article[]; + alert?: string; +} + +export interface PrivacyPolicy { + title: string; + date: string; + description: string; + sections: Section[]; +} + +export const TERMS_OF_PRIVACY_INFO: PrivacyPolicy = { + title: "개인 정보 처리 방침", + date: "시행일자: 2026년 3월 8일", + description: + "Comfit(이하 “회사”)는 「개인정보 보호법」 등 관련 법령을 준수하며, 이용자의 개인정보를 안전하게 보호하기 위해 최선을 다하고 있습니다. 회사는 본 개인정보 처리방침을 통하여 이용자가 제공한 개인정보가 어떤 목적과 방식으로 이용되며, 이를 보호하기 위해 어떤 조치를 취하고 있는지 안내드립니다. 본 개인정보 처리방침은 회사가 제공하는 Comfit 웹 서비스 및 관련 서비스에 적용됩니다.", + sections: [ + { + title: "1. 개인정보의 수집 및 이용", + description: + "회사는 서비스 제공에 필요한 최소한의 개인정보만 수집하며, 수집한 개인정보는 고지한 목적 범위 내에서만 이용합니다.", + articles: [ + { + title: "가. 회원가입 및 계정 관리", + content: { + table: { + header: [ + "구분", + "수집 항목", + "수집 및 이용 목적", + "보유 기간", + "법적 근거", + ], + rows: [ + [ + "간편 로그인", + "소셜 로그인 제공자로부터 제공받는 식별정보, 이메일, 이름", + "간편 회원가입 및 로그인 연동", + "회원 탈퇴 시까지", + "개인정보 보호법 제15조 제1항 제4호", + ], + ], + }, + }, + }, + { + title: "나. 온보딩 및 프로필 설정", + content: { + table: { + header: [ + "구분", + "수집 항목", + "수집 및 이용 목적", + "보유 기간", + "법적 근거", + ], + rows: [ + [ + "온보딩 정보", + "최종학력, 학교명, 관심 산업, 관심 직무", + "간편 회원가입 및 로그인 연동", + "회원 탈퇴 시까지", + "개인정보 보호법 제15조 제1항 제1호", + ], + [ + "프로필 정보", + "프로필 이미지", + "마이페이지 구성, 서비스 내 사용자 식별 및 편의 제공", + "회원 탈퇴 시까지 또는 해당 정보 삭제 시까지", + "개인정보 보호법 제15조 제1항 제4호", + ], + ], + }, + }, + }, + { + title: "다. 경험 등록 및 AI 분석 서비스 이용", + content: { + table: { + header: [ + "구분", + "수집 항목", + "수집 및 이용 목적", + "보유 기간", + "법적 근거", + ], + rows: [ + [ + "경험등록", + "이용자가 직접 입력한 경험 정보(예: 활동명, 기간, 역할, 상황, 과제, 행동, 결과, 성과, 느낀 점 등)", + "경험 저장 및 관리, 기업 맞춤 경험 매칭, 분석 결과 생성", + "회원 탈퇴 시까지 또는 이용자 삭제 시까지", + "개인정보 보호법 제15조 제1항 제1호(정보주체의 동의) 또는 제4호(계약의 이행)", + ], + [ + "AI 분석 이용", + "경험 정보, 선택 기업 정보, 분석 요청 내역, 생성 결과", + "기업-경험 매칭 결과 제공, 결과 리포트 생성, 서비스 개선 및 오류 대응", + "회원 탈퇴 시까지 또는 분석 결과 삭제 시까지", + "개인정보 보호법 제15조 제1항 제4호(계약의 이행)", + ], + ], + }, + }, + }, + ], + }, + { + title: "2. 개인정보의 보유 및 이용기간", + description: + "회사는 원칙적으로 개인정보의 수집 및 이용 목적이 달성되면 지체 없이 해당 정보를 파기합니다. 다만, 관련 법령에 따라 일정 기간 보관이 필요한 경우에는 해당 법령에 따라 분리 보관합니다.", + articles: [ + { + title: "가. 서비스 이용을 위해 수집한 정보", + content: { + text: `- 회원정보: 회원 탈퇴 시까지 +- 경험 등록 정보 및 분석 결과: 회원 탈퇴 시까지 또는 이용자가 직접 삭제할 때까지 +- 고객문의 정보: 문의 처리 완료 후 3년 +- 마케팅 수신 정보: 동의 철회 시까지`, + }, + }, + { + title: "나. 관련 법령에 따른 보관", + content: { + text: `- 회원정보: 회원 탈퇴 시까지 +- 경험 등록 정보 및 분석 결과: 회원 탈퇴 시까지 또는 이용자가 직접 삭제할 때까지 +- 고객문의 정보: 문의 처리 완료 후 3년 +- 마케팅 수신 정보: 동의 철회 시까지`, + }, + }, + ], + alert: + "※ 현재 Comfit는 유료 서비스 및 결제 기능을 운영하고 있지 않습니다. 향후 결제, 환불, 청약철회 등이 포함된 유료 서비스가 도입되는 경우, 관련 법령에 따라 별도의 보관 항목이 추가될 수 있으며, 개인정보처리방침을 통해 사전에 안내합니다.", + }, + { + title: "3. 개인정보의 파기절차 및 방법", + description: + "회사는 개인정보 보유기간의 경과, 처리 목적 달성 등 개인정보가 불필요하게 되었을 때에는 지체 없이 해당 개인정보를 파기합니다.", + articles: [ + { + title: "가. 파기절차", + content: { + text: "이용자가 입력한 개인정보는 목적 달성 후 내부 방침 및 관련 법령에 따라 일정 기간 분리 보관된 후 파기됩니다.", + }, + }, + { + title: "나. 파기방법", + content: { + text: `- 전자적 파일 형태의 정보는 복구 및 재생이 불가능한 기술적 방법으로 삭제합니다. +- 종이 문서에 출력된 개인정보는 분쇄 또는 소각하여 파기합니다.`, + }, + }, + ], + }, + { + title: "4. 개인정보의 제3자 제공", + description: `회사는 원칙적으로 이용자의 개인정보를 외부에 제공하지 않습니다. 다만, 아래의 경우에는 예외로 합니다. +1. 이용자가 사전에 동의한 경우 +2. 법령에 특별한 규정이 있거나 수사기관 등이 적법한 절차에 따라 요청한 경우`, + alert: + "※ 현재 Comfit는 이용자의 개인정보를 제3자에게 제공하는 방식의 서비스를 기본적으로 운영하지 않으며, 향후 제3자 제공이 필요한 서비스가 도입될 경우 제공받는 자, 제공 목적, 제공 항목, 보유 기간 등을 별도로 고지하고 동의를 받습니다.", + }, + { + title: "5. 개인정보 처리위탁", + description: + "회사는 서비스 제공 및 운영을 위해 다음과 같이 개인정보 처리 업무의 일부를 외부 전문업체에 위탁할 수 있으며, 관련 법령에 따라 수탁자에 대한 관리·감독을 실시합니다.", + articles: [ + { + content: { + table: { + header: ["수탁업체", "위탁업무 내용"], + rows: [ + [ + "Amazon Web Services (AWS)", + "클라우드 서버 인프라 운영 및 데이터 보관", + ], + ["Vercel", "프론트엔드 애플리케이션 배포 및 운영"], + ["OpenAI", "AI 기반 분석 서비스 제공을 위한 API 처리"], + ], + }, + }, + }, + ], + alert: + "※ 위탁 업무 내용 및 수탁업체는 서비스 운영 환경에 따라 변경될 수 있으며, 변경 시 본 개인정보처리방침을 통해 안내합니다.", + }, + { + title: "6. 개인정보의 국외 이전에 관한 사항", + description: + "회사는 안정적인 서비스 제공을 위해 개인정보를 해외 클라우드 서버 및 외부 AI API 서비스에 저장·처리할 수 있습니다.", + articles: [ + { + content: { + table: { + header: [ + "이전 받는 자", + "이전 국가", + "이전 항목", + "이전 목적", + "보유 기간", + ], + rows: [ + [ + "Amazon Web Services, Inc.", + "미국", + "회원가입 정보, 서비스 이용 정보", + "클라우드 인프라 운영 및 데이터 보관", + "회원 탈퇴 시까지", + ], + [ + "OpenAI", + "미국", + "AI 분석 요청 시 입력된 텍스트 데이터", + "AI 기반 경험 분석 및 리포트 생성", + "처리 후 즉시 폐기 또는 서비스 정책에 따름", + ], + ], + }, + }, + }, + ], + alert: "이전 방법: 서비스 이용 시 네트워크를 통한 전송", + }, + { + title: "7. 정보주체의 권리 및 행사방법", + description: `이용자는 언제든지 자신의 개인정보에 대해 다음 권리를 행사할 수 있습니다. +1. 개인정보 조회 +2. 개인정보 수정 +3. 개인정보 삭제 +4. 동의 철회 +5. 회원 탈퇴 요청 +이용자는 서비스 내 마이페이지 또는 고객문의 채널을 통해 위 권리를 행사할 수 있습니다. 회사는 관련 법령에 따라 지체 없이 조치합니다. +단, 다음의 경우 일부 권리 행사가 제한될 수 있습니다. +- 법령상 보관의무가 있는 경우 +- 다른 사람의 권리나 이익을 침해할 우려가 있는 경우 +- 서비스 제공에 필수적인 정보 삭제를 요구하는 경우`, + }, + { + title: "8. 민감정보 처리 여부", + description: `회사는 원칙적으로 민감정보를 수집하지 않습니다. +다만 이용자가 경험 등록, 자기소개 작성, 파일 첨부 등 자유 입력 영역에 건강정보, 정치적 견해, 종교, 노동조합 가입 여부 등 민감정보를 기재하는 경우, 해당 정보는 이용자 본인의 선택에 의해 제공되는 것입니다. 회사는 서비스 제공에 불필요한 민감정보의 입력을 권장하지 않습니다. +이용자는 자유 입력란에 민감정보를 기재하지 않도록 주의해 주시기 바랍니다`, + }, + { + title: "9. 자동 수집되는 개인정보 및 거부에 관한 사항", + description: `회사는 서비스 제공 과정에서 아래와 같은 정보가 자동으로 생성·수집될 수 있습니다. +- IP 주소 +- 쿠키 +- 접속 일시 +- 서비스 이용 기록 +- 브라우저 정보 +- 운영체제 정보 +- 디바이스 정보 +- 오류 로그 +가. 수집 목적 +- 로그인 상태 유지 +- 서비스 이용환경 최적화 +- 접속 통계 및 이용 패턴 분석 +- 보안 및 이상 행위 탐지 +- 서비스 개선 +나. 쿠키의 설치 및 거부 +이용자는 웹브라우저 설정을 통해 쿠키 저장을 거부할 수 있습니다. 다만 쿠키 저장을 거부하는 경우 로그인 유지, 맞춤형 화면 제공 등 일부 기능 이용이 제한될 수 있습니다. +브라우저별 설정 예시는 다음과 같습니다. +- Chrome: 설정 > 개인정보 보호 및 보안 > 인터넷 사용 기록 삭제 / 쿠키 설정 +- Edge: 설정 > 쿠키 및 사이트 권한 > 쿠키 및 사이트 데이터 관리 +- Safari: 설정 > Safari > 모든 쿠키 차단`, + }, + { + title: "10. 온라인 맞춤형 광고 및 행태정보 처리", + description: `회사는 이용자에게 더 적합한 콘텐츠와 서비스 정보를 제공하기 위해 서비스 이용 기록을 바탕으로 행태정보를 처리할 수 있습니다. +가. 처리 항목 +- 방문 페이지 +- 클릭 이력 +- 유입 경로 +- 사용 기능 +- 체류 시간 +- 기기 및 브라우저 정보 +나. 처리 목적 +- 서비스 개선 +- 사용자 경험 최적화 +- 알림 및 콘텐츠 개인화 +- 마케팅 성과 측정 +이용자는 브라우저 또는 운영체제 설정을 통해 일부 맞춤형 정보 제공을 제한할 수 있습니다. +※ 현재 회사는 현재 온라인 맞춤형 광고를 직접 운영하지 않으나, 서비스 분석 및 개선을 위해 유사한 형태의 이용 기록을 처리할 수 있습니다.`, + }, + { + title: "11. 개인정보의 안전성 확보조치", + description: `회사는 이용자의 개인정보를 안전하게 보호하기 위하여 다음과 같은 조치를 취하고 있습니다. +가. 관리적 조치 +- 개인정보 처리 내부관리계획 수립 및 시행 +- 개인정보 접근 권한 관리 +- 임직원 대상 개인정보 보호 교육 실시 +나. 기술적 조치 +- 비밀번호 등 중요 정보 암호화 +- 접근통제 시스템 운영 +- 보안 프로그램 설치 및 운영 +- 접속기록 보관 및 이상행위 모니터링 +다. 물리적 조치 +- 개인정보 보관 시스템에 대한 접근 제한 +- 문서 및 저장매체 보관 통제`, + }, + { + title: "12. 개인정보 보호책임자 및 문의처", + description: `회사는 개인정보 처리에 관한 업무를 총괄해서 책임지고, 개인정보 관련 문의 및 불만처리를 위해 아래와 같이 개인정보 보호책임자 또는 담당 부서를 지정하고 있습니다. +개인정보 보호책임자 +- 성명: Comfit +- 이메일: comfit0125@gmail.com +고객문의 +- 문의 채널: 이메일 문의 +이용자는 회사의 서비스를 이용하며 발생한 모든 개인정보 관련 문의, 열람청구, 불만처리, 피해구제 등에 관한 사항을 위 연락처로 문의할 수 있습니다. +또한 개인정보 침해에 대한 신고나 상담이 필요한 경우 아래 기관에 문의할 수 있습니다. +- 개인정보침해신고센터: (국번없이) 118 +- 개인정보분쟁조정위원회: 1833-6972 +- 대검찰청: 1301 +- 경찰청 사이버범죄 신고시스템: 182`, + }, + { + title: "13. 아동의 개인정보 처리", + description: `회사는 원칙적으로 만 15세 미만 아동의 개인정보를 수집하지 않습니다. +만 15세 미만 아동이 회사에 개인정보를 제공한 사실이 확인될 경우, 해당 정보는 지체 없이 삭제하거나 필요한 조치를 취합니다.`, + }, + { + title: "14. AI 서비스 이용과 관련한 추가 안내", + description: `Comfit는 이용자가 입력한 경험 정보와 선택한 기업 정보 등을 바탕으로 AI 기반 분석 결과를 제공할 수 있습니다. +회사는 AI 기능 제공을 위해 필요한 범위 내에서 입력 데이터를 처리할 수 있으며, 해당 결과는 참고용으로 제공됩니다. 이용자는 자유 입력란에 타인의 개인정보, 민감정보, 불필요한 개인정보를 입력하지 않도록 주의해야 합니다. +회사는 AI 서비스 품질 개선, 오류 대응, 부정 이용 방지 등을 위해 분석 요청 내역 및 생성 결과를 저장할 수 있습니다.`, + }, + { + title: "15. 개인정보 처리방침의 변경", + description: `회사는 관련 법령, 서비스 내용, 내부 운영 정책의 변경에 따라 본 개인정보 처리방침을 수정할 수 있습니다. 개인정보 처리방침이 변경되는 경우 서비스 내 공지사항 또는 별도의 안내를 통해 사전 공지합니다. +- 개인정보 처리방침 버전: Ver 1.0 +- 시행일자: 2026년 3월 8일`, + }, + ], +}; From 4e6ad76cab019bcd11aee5089b1e13bad97f373d Mon Sep 17 00:00:00 2001 From: odukong Date: Sun, 22 Mar 2026 16:20:16 +0900 Subject: [PATCH 09/25] =?UTF-8?q?feat:=20=EC=9D=B4=EC=9A=A9=EC=95=BD?= =?UTF-8?q?=EA=B4=80&=EA=B0=9C=EC=9D=B8=EC=A0=95=EB=B3=B4=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=EB=B0=A9=EC=B9=A8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20UI?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20(#159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/routes/paths.ts | 3 + src/app/routes/public-routes.tsx | 8 ++ src/pages/policy/policy-page.css.ts | 118 +++++++++++++++++++++++++ src/pages/policy/policy-page.tsx | 41 +++++++++ src/pages/policy/ui/privacy-policy.tsx | 94 ++++++++++++++++++++ src/pages/policy/ui/use-policy.tsx | 35 ++++++++ 6 files changed, 299 insertions(+) create mode 100644 src/pages/policy/policy-page.css.ts create mode 100644 src/pages/policy/policy-page.tsx create mode 100644 src/pages/policy/ui/privacy-policy.tsx create mode 100644 src/pages/policy/ui/use-policy.tsx diff --git a/src/app/routes/paths.ts b/src/app/routes/paths.ts index 9007da92..ca1af694 100644 --- a/src/app/routes/paths.ts +++ b/src/app/routes/paths.ts @@ -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", }; diff --git a/src/app/routes/public-routes.tsx b/src/app/routes/public-routes.tsx index 47a882e6..aaec6742 100644 --- a/src/app/routes/public-routes.tsx +++ b/src/app/routes/public-routes.tsx @@ -32,6 +32,12 @@ const CompanyDetailPage = lazy(() => })) ); +const PolicyPage = lazy(() => + import("@/pages/policy/policy-page").then((module) => ({ + default: module.PolicyPage, + })) +); + export const guestRoutes = [{ path: ROUTES.LOGIN, element: }]; export const publicRoutes = [ @@ -39,4 +45,6 @@ export const publicRoutes = [ { path: ROUTES.LANDING, element: }, { path: ROUTES.HOME, element: }, { path: ROUTES.COMPANY(), element: }, + { path: ROUTES.POLICY_USE, element: }, + { path: ROUTES.POLICY_PRIVACY, element: }, ]; diff --git a/src/pages/policy/policy-page.css.ts b/src/pages/policy/policy-page.css.ts new file mode 100644 index 00000000..d2417270 --- /dev/null +++ b/src/pages/policy/policy-page.css.ts @@ -0,0 +1,118 @@ +import { style } from "@vanilla-extract/css"; +import { recipe } from "@vanilla-extract/recipes"; + +import { themeVars } from "@/app/styles"; + +export const background = style({ + display: "flex", + justifyContent: "center", + marginTop: themeVars.height.header, + background: themeVars.color.gray100, +}); + +export const wrapper = style({ + width: "106rem", + padding: "8rem 5.2rem", + background: themeVars.color.white, +}); + +export const title = style({ + color: themeVars.color.gray800, + ...themeVars.fontStyles.title_b_36, + fontWeight: 700, +}); + +export const subTitle = style({ + color: themeVars.color.gray800, + ...themeVars.fontStyles.hline_m_18, + fontWeight: 500, +}); + +export const divider = style({ + height: "0.15rem", + backgroundColor: themeVars.color.gray200, +}); + +export const content = style({ + display: "flex", + flexDirection: "column", + color: themeVars.color.gray500, + ...themeVars.fontStyles.body_r_14, + 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" }, + 40: { gap: "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({ + maxWidth: "25rem", + padding: "0.8rem", + border: `1px solid ${themeVars.color.gray200}`, + + verticalAlign: "top", + textAlign: "left", + wordBreak: "keep-all", + + ...themeVars.fontStyles.body_r_14, +}); + +export const thead = style({ + backgroundColor: themeVars.color.gray100, +}); + +export const th = style({ + whiteSpace: "nowrap", + padding: "1rem 0.8rem", +}); diff --git a/src/pages/policy/policy-page.tsx b/src/pages/policy/policy-page.tsx new file mode 100644 index 00000000..38cff144 --- /dev/null +++ b/src/pages/policy/policy-page.tsx @@ -0,0 +1,41 @@ +import * as styles from "./policy-page.css"; +import { PrivacyPolicy } from "./ui/privacy-policy"; +import { UsePolicy } from "./ui/use-policy"; + +interface PolicyPageProps { + mode: "USE" | "PRIVACY"; +} + +const PolicyPage = ({ mode }: PolicyPageProps) => { + return ( +
+ {mode === "USE" ? ( +
+
+

이용약관

+

+ 컴핏 관련 제반 서비스의 이용과 관련하여 필요한 사항을 규정합니다. +

+
+
+ +
+ ) : mode === "PRIVACY" ? ( +
+
+

개인정보처리방침

+

+ 컴핏은 개인정보 보호 등에 관한 법률을 준수합니다. +

+
+
+ +
+ ) : ( + <> + )} +
+ ); +}; + +export { PolicyPage }; diff --git a/src/pages/policy/ui/privacy-policy.tsx b/src/pages/policy/ui/privacy-policy.tsx new file mode 100644 index 00000000..03e11b4d --- /dev/null +++ b/src/pages/policy/ui/privacy-policy.tsx @@ -0,0 +1,94 @@ +import { + TERMS_OF_PRIVACY_INFO, + type Article, + type Section, +} from "@/shared/config"; + +import * as styles from "../policy-page.css"; + +export const PrivacyPolicy = () => { + 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, idx) => ( +
+ {/** 조항 상세설명의 타이틀 (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) => ( + + ) + )} + + + + {article.content.table.rows.map((row, rowIdx) => ( + + {row.map((td, tdIdx) => ( + + ))} + + ))} + +
+ {th} +
+ {td} +
+
+ )} +
+ )} +
+
+ ))} +
+ {/** 조항 추가사항 */} + {section.alert &&
{section.alert}
} +
+ ))} +
+ ); +}; diff --git a/src/pages/policy/ui/use-policy.tsx b/src/pages/policy/ui/use-policy.tsx new file mode 100644 index 00000000..32aa18ca --- /dev/null +++ b/src/pages/policy/ui/use-policy.tsx @@ -0,0 +1,35 @@ +import { TERMS_OF_USE, type Chapter, type Chapters } from "@/shared/config"; + +import * as styles from "../policy-page.css"; + +export const UsePolicy = () => { + return TERMS_OF_USE.map((policy: Chapters) => { + return ( +
+

+ {policy.chapterTitle} +

+ +
+ {policy.chapter.map((chapter: Chapter) => ( +
+ {chapter.title && ( +

+ {chapter.title} +

+ )} +
+ {chapter.contents.map((content, idx) => ( +

{content}

+ ))} +
+
+ ))} +
+
+ ); + }); +}; From aa3002f87f6e39e4e86eaf37754bb8937f58ffaf Mon Sep 17 00:00:00 2001 From: odukong Date: Sun, 22 Mar 2026 16:32:58 +0900 Subject: [PATCH 10/25] =?UTF-8?q?refactor:=20policypage=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0=20(#159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/policy/policy-page.tsx | 55 ++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/src/pages/policy/policy-page.tsx b/src/pages/policy/policy-page.tsx index 38cff144..2243cc4d 100644 --- a/src/pages/policy/policy-page.tsx +++ b/src/pages/policy/policy-page.tsx @@ -6,34 +6,41 @@ interface PolicyPageProps { mode: "USE" | "PRIVACY"; } +const POLICY_CONTENT = { + USE: { + title: "이용약관", + subTitle: + "컴핏 관련 제반 서비스의 이용과 관련하여 필요한 사항을 규정합니다.", + gap: 24 as const, + Component: , + }, + PRIVACY: { + title: "개인정보처리방침", + subTitle: "컴핏은 개인정보 보호 등에 관한 법률을 준수합니다.", + gap: 40 as const, + Component: , + }, +}; + const PolicyPage = ({ mode }: PolicyPageProps) => { + const currentPolicy = POLICY_CONTENT[mode]; + + if (!currentPolicy) return <>; + return (
- {mode === "USE" ? ( -
-
-

이용약관

-

- 컴핏 관련 제반 서비스의 이용과 관련하여 필요한 사항을 규정합니다. -

-
-
- -
- ) : mode === "PRIVACY" ? ( -
-
-

개인정보처리방침

-

- 컴핏은 개인정보 보호 등에 관한 법률을 준수합니다. -

-
-
- +
+ {/** 약관 타이틀 */} +
+

{currentPolicy.title}

+

{currentPolicy.subTitle}

- ) : ( - <> - )} +
+ {/** 약관 컨텐츠 */} + {currentPolicy.Component} +
); }; From 3eb508f896736ecd9a6fb45fb7a6a0964c3fb788 Mon Sep 17 00:00:00 2001 From: odukong Date: Mon, 23 Mar 2026 21:32:00 +0900 Subject: [PATCH 11/25] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EB=B9=97=20=EC=88=98=EC=A0=95=EC=82=AC=ED=95=AD=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20(#159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - use-modal도 함께 삭제 --- .../ui/policy-modal/policy-modal.css.ts | 4 ++-- .../ui/policy-modal/policy-modal.tsx | 8 +++---- .../policy-modal/privacy-policy-content.tsx | 9 +++++--- .../ui/policy-modal/use-policy-content.tsx | 4 ++-- src/pages/policy/policy-page.css.ts | 2 +- src/pages/policy/policy-page.tsx | 2 -- src/pages/policy/ui/privacy-policy.tsx | 2 +- src/pages/policy/ui/use-policy.tsx | 8 +++---- src/shared/ui/index.ts | 1 - src/shared/ui/modal/modal.css.ts | 2 +- src/shared/ui/modal/use-modal.ts | 23 ------------------- 11 files changed, 20 insertions(+), 45 deletions(-) delete mode 100644 src/shared/ui/modal/use-modal.ts diff --git a/src/features/onboarding/ui/policy-modal/policy-modal.css.ts b/src/features/onboarding/ui/policy-modal/policy-modal.css.ts index 8efd7fc4..24cd53a2 100644 --- a/src/features/onboarding/ui/policy-modal/policy-modal.css.ts +++ b/src/features/onboarding/ui/policy-modal/policy-modal.css.ts @@ -26,7 +26,7 @@ export const buttonWrapper = style({ height: "2.4rem", }); -export const modalCotent = style({ +export const modalContent = style({ display: "flex", flexDirection: "column", gap: "2.4rem", @@ -42,7 +42,7 @@ export const modalCotent = style({ }, "&::-webkit-scrollbar-thumb": { backgroundColor: themeVars.color.gray300, - height: "50px", + height: "5rem", borderRadius: "100px", backgroundClip: "padding-box", border: `4px solid transparent`, diff --git a/src/features/onboarding/ui/policy-modal/policy-modal.tsx b/src/features/onboarding/ui/policy-modal/policy-modal.tsx index 6fa55e55..409a9d15 100644 --- a/src/features/onboarding/ui/policy-modal/policy-modal.tsx +++ b/src/features/onboarding/ui/policy-modal/policy-modal.tsx @@ -4,22 +4,22 @@ import * as styles from "./policy-modal.css"; import { PrivacyPolicyContent } from "./privacy-policy-content"; import { UsePolicyContent } from "./use-policy-content"; -interface modalProps { +interface PolicyModalProps { type: "USE" | "PRIVACY"; onClose: () => void; } -export const PolicyModal = ({ type, onClose }: modalProps) => { +export const PolicyModal = ({ type, onClose }: PolicyModalProps) => { return (
-
{type === "USE" ? "이용약관" : "개인정보처리방침"}
+

{type === "USE" ? "이용약관" : "개인정보처리방침"}

-
+
{type === "USE" ? : }
diff --git a/src/features/onboarding/ui/policy-modal/privacy-policy-content.tsx b/src/features/onboarding/ui/policy-modal/privacy-policy-content.tsx index 9a56b5cf..dfbe7d1e 100644 --- a/src/features/onboarding/ui/policy-modal/privacy-policy-content.tsx +++ b/src/features/onboarding/ui/policy-modal/privacy-policy-content.tsx @@ -19,7 +19,7 @@ export const PrivacyPolicyContent = () => {
{/** 조항 리스트 (ex. 1. 개인정보의 수집 및 이용) */} {TERMS_OF_PRIVACY_INFO.sections.map((section: Section) => ( -
+
{/* 조항 타이틀 및 설명 */}

@@ -30,7 +30,10 @@ export const PrivacyPolicyContent = () => { {/** 조항 상세설명 */}
{section.articles?.map((article: Article) => ( -
+
{/** 조항 상세설명의 타이틀 (ex. 가. 회원가입 및 계정 관리) */} {article.title && (

@@ -47,7 +50,7 @@ export const PrivacyPolicyContent = () => { {/* 테이블 형태의 컨텐츠 */} {"table" in article.content && (

- +
{article.content.table.header.map( diff --git a/src/features/onboarding/ui/policy-modal/use-policy-content.tsx b/src/features/onboarding/ui/policy-modal/use-policy-content.tsx index b15ccd5f..c3a8a4e1 100644 --- a/src/features/onboarding/ui/policy-modal/use-policy-content.tsx +++ b/src/features/onboarding/ui/policy-modal/use-policy-content.tsx @@ -8,8 +8,8 @@ export const UsePolicyContent = () => {

{policy.chapterTitle}

- {policy.chapter.map((chapter: Chapter) => ( -
+ {policy.chapter.map((chapter: Chapter, idx) => ( +
{chapter.title && (

{chapter.title}

)} diff --git a/src/pages/policy/policy-page.css.ts b/src/pages/policy/policy-page.css.ts index d2417270..335274a2 100644 --- a/src/pages/policy/policy-page.css.ts +++ b/src/pages/policy/policy-page.css.ts @@ -94,10 +94,10 @@ export const table = style({ width: "max-content", minWidth: "100%", borderCollapse: "collapse", + maxWidth: "25rem", }); export const tCell = style({ - maxWidth: "25rem", padding: "0.8rem", border: `1px solid ${themeVars.color.gray200}`, diff --git a/src/pages/policy/policy-page.tsx b/src/pages/policy/policy-page.tsx index 2243cc4d..36cd4019 100644 --- a/src/pages/policy/policy-page.tsx +++ b/src/pages/policy/policy-page.tsx @@ -25,8 +25,6 @@ const POLICY_CONTENT = { const PolicyPage = ({ mode }: PolicyPageProps) => { const currentPolicy = POLICY_CONTENT[mode]; - if (!currentPolicy) return <>; - return (
{ {/* 테이블 형태의 컨텐츠 */} {"table" in article.content && (
-
+
{article.content.table.header.map( diff --git a/src/pages/policy/ui/use-policy.tsx b/src/pages/policy/ui/use-policy.tsx index 32aa18ca..79569155 100644 --- a/src/pages/policy/ui/use-policy.tsx +++ b/src/pages/policy/ui/use-policy.tsx @@ -16,11 +16,9 @@ export const UsePolicy = () => {
{policy.chapter.map((chapter: Chapter) => (
- {chapter.title && ( -

- {chapter.title} -

- )} +

+ {chapter?.title} +

{chapter.contents.map((content, idx) => (

{content}

diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts index c0ee1b7d..85fb7ab2 100644 --- a/src/shared/ui/index.ts +++ b/src/shared/ui/index.ts @@ -6,7 +6,6 @@ export { usePagination } from "./pagination/use-pagination"; export { Modal } from "./modal/modal"; export { ModalBasic } from "./modal/modal-basic"; -export { useModal } from "./modal/use-modal"; export { Button } from "./button/button"; diff --git a/src/shared/ui/modal/modal.css.ts b/src/shared/ui/modal/modal.css.ts index 1988ff3b..7ae93301 100644 --- a/src/shared/ui/modal/modal.css.ts +++ b/src/shared/ui/modal/modal.css.ts @@ -23,7 +23,7 @@ export const modalContent = recipe({ display: "flex", flexDirection: "column", alignItems: "center", - borderRadius: "1.2rem", + borderRadius: "12px", backgroundColor: themeVars.color.white, }, variants: { diff --git a/src/shared/ui/modal/use-modal.ts b/src/shared/ui/modal/use-modal.ts deleted file mode 100644 index 883669e6..00000000 --- a/src/shared/ui/modal/use-modal.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useEffect, useRef, useState } from "react"; - -export const useModal = (autoPlay?: number) => { - const [isOpen, setIsOpen] = useState(false); - const timerRef = useRef(null); // 자동 모달 닫힘을 위한 타이머 ref - - const handleModal = () => setIsOpen((prev) => !prev); - - const openModal = () => setIsOpen(true); - const closeModal = () => setIsOpen(false); - useEffect(() => { - if (autoPlay && autoPlay > 0 && isOpen) { - if (timerRef.current) clearTimeout(timerRef.current); - timerRef.current = setTimeout(() => setIsOpen(false), autoPlay); - - return () => { - if (timerRef.current) clearTimeout(timerRef.current); - }; - } - }, [autoPlay, isOpen]); - - return { autoPlay, isOpen, handleModal, openModal, closeModal }; -}; From 421908af2635d14fde89561d6c1252268754400a Mon Sep 17 00:00:00 2001 From: odukong Date: Wed, 25 Mar 2026 00:14:47 +0900 Subject: [PATCH 12/25] =?UTF-8?q?design:=20=EA=B0=9C=EC=9D=B8=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EC=B2=98=EB=A6=AC=EB=B0=A9=EC=B9=A8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20gap=20=EC=88=98=EC=A0=95=20(#159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/policy/policy-page.css.ts | 1 + src/pages/policy/ui/privacy-policy.tsx | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/policy/policy-page.css.ts b/src/pages/policy/policy-page.css.ts index 335274a2..60d5325a 100644 --- a/src/pages/policy/policy-page.css.ts +++ b/src/pages/policy/policy-page.css.ts @@ -72,6 +72,7 @@ export const flexColumn = recipe({ }, variants: { gap: { + 0: { gap: "0rem" }, 8: { gap: "0.8rem" }, 16: { gap: "1.6rem" }, 24: { gap: "2.4rem" }, diff --git a/src/pages/policy/ui/privacy-policy.tsx b/src/pages/policy/ui/privacy-policy.tsx index b2cccc41..e088c7d2 100644 --- a/src/pages/policy/ui/privacy-policy.tsx +++ b/src/pages/policy/ui/privacy-policy.tsx @@ -19,7 +19,7 @@ export const PrivacyPolicy = () => {
{/** 조항 리스트 (ex. 1. 개인정보의 수집 및 이용) */} {TERMS_OF_PRIVACY_INFO.sections.map((section: Section) => ( -
+
{/* 조항 타이틀 및 설명 */}

@@ -28,7 +28,7 @@ export const PrivacyPolicy = () => {

{section.description}

{/** 조항 상세설명 */} -
+
{section.articles?.map((article: Article, idx) => (
Date: Thu, 26 Mar 2026 00:43:10 +0900 Subject: [PATCH 13/25] =?UTF-8?q?refactor:=20=EC=B2=B4=ED=81=AC=EB=B0=95?= =?UTF-8?q?=EC=8A=A4=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=EC=99=80=20=EC=95=BD?= =?UTF-8?q?=EA=B4=80=20=EB=AA=A8=EB=8B=AC=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20(#159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 체크박스와 약관 모달 클릭 이벤트를 분리하도록 구조 변경 - 대신, 접근성 향상을 위해 aria-label태그 활용 --- src/pages/onboarding/onboarding-page.css.ts | 2 +- src/pages/onboarding/ui/agree-section.tsx | 39 ++++++++++----------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/pages/onboarding/onboarding-page.css.ts b/src/pages/onboarding/onboarding-page.css.ts index ee916e65..006c1c3b 100644 --- a/src/pages/onboarding/onboarding-page.css.ts +++ b/src/pages/onboarding/onboarding-page.css.ts @@ -110,12 +110,12 @@ export const sectionGroup = style({ export const agreeGroup = style({ display: "flex", alignItems: "center", + gap: "1.6rem", }); export const agreeContent = style({ display: "flex", alignItems: "center", - gap: "1.6rem", color: themeVars.color.gray800, ...themeVars.fontStyles.body_m_16, fontWeight: 500, diff --git a/src/pages/onboarding/ui/agree-section.tsx b/src/pages/onboarding/ui/agree-section.tsx index 666e8f75..dbe406f3 100644 --- a/src/pages/onboarding/ui/agree-section.tsx +++ b/src/pages/onboarding/ui/agree-section.tsx @@ -10,8 +10,7 @@ interface AgreeSectionProps { } const AgreeSection = ({ isAgreed, setIsAgreed }: AgreeSectionProps) => { - const handleModal = (e: React.MouseEvent, type: "USE" | "PRIVACY") => { - e.preventDefault(); + const handleModal = (type: "USE" | "PRIVACY") => { const MODAL_ID = "ONBOARD_MODAL"; modalStore.open( @@ -32,28 +31,28 @@ const AgreeSection = ({ isAgreed, setIsAgreed }: AgreeSectionProps) => { onChange={() => setIsAgreed(!isAgreed)} /> {/** 실제 눈에 보이는 체크박스, 텍스트 */} -
); }; From 2ff1fc6a6a51401000be3e439eaf4308a784f369 Mon Sep 17 00:00:00 2001 From: odukong Date: Thu, 26 Mar 2026 00:47:10 +0900 Subject: [PATCH 14/25] =?UTF-8?q?design:=20=EB=AA=A8=EB=8B=AC=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B0=95=EC=8A=A4=20=EC=98=81?= =?UTF-8?q?=EC=97=AD=20=EB=B0=98=EC=9D=91=ED=98=95=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?(#159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/modal/modal.css.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/shared/ui/modal/modal.css.ts b/src/shared/ui/modal/modal.css.ts index 7ae93301..5f1a6ff6 100644 --- a/src/shared/ui/modal/modal.css.ts +++ b/src/shared/ui/modal/modal.css.ts @@ -36,6 +36,8 @@ export const modalContent = recipe({ auto: { width: "auto", height: "auto", + maxWidth: "90vw", + maxHeight: "60vh", padding: "0", }, }, From 06667adfa7fc342a7d2987fe1275328acf818c3e Mon Sep 17 00:00:00 2001 From: odukong Date: Thu, 26 Mar 2026 00:48:36 +0900 Subject: [PATCH 15/25] =?UTF-8?q?fix:=20=EB=88=84=EB=9D=BD=EB=90=9C=20key?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(#159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../onboarding/ui/policy-modal/use-policy-content.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/onboarding/ui/policy-modal/use-policy-content.tsx b/src/features/onboarding/ui/policy-modal/use-policy-content.tsx index c3a8a4e1..8a60cead 100644 --- a/src/features/onboarding/ui/policy-modal/use-policy-content.tsx +++ b/src/features/onboarding/ui/policy-modal/use-policy-content.tsx @@ -14,8 +14,8 @@ export const UsePolicyContent = () => {

{chapter.title}

)}
- {chapter.contents.map((content) => ( -

{content}

+ {chapter.contents.map((content, idx) => ( +

{content}

))}
From 0f6e5dd6f3da27b3a5e75b9460b378b4ae2ce5c3 Mon Sep 17 00:00:00 2001 From: odukong Date: Thu, 26 Mar 2026 00:56:30 +0900 Subject: [PATCH 16/25] =?UTF-8?q?refactor:=20=EC=95=BD=EA=B4=80=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EC=A1=B0=EA=B1=B4=EB=B6=80=20=EB=A0=8C?= =?UTF-8?q?=EB=8D=94=EB=A7=81=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=20=EB=A7=A4=ED=95=91=20=EB=B0=A9=EC=8B=9D=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EA=B0=9C=EC=84=A0=20(#159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../onboarding/ui/policy-modal/policy-modal.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/features/onboarding/ui/policy-modal/policy-modal.tsx b/src/features/onboarding/ui/policy-modal/policy-modal.tsx index 409a9d15..414d1732 100644 --- a/src/features/onboarding/ui/policy-modal/policy-modal.tsx +++ b/src/features/onboarding/ui/policy-modal/policy-modal.tsx @@ -9,18 +9,31 @@ interface PolicyModalProps { 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 (
-

{type === "USE" ? "이용약관" : "개인정보처리방침"}

+

{title}

- {type === "USE" ? : } +
From 4658aeaba837478ac8b980bdb9ac8a072cea44bc Mon Sep 17 00:00:00 2001 From: odukong Date: Thu, 26 Mar 2026 22:40:44 +0900 Subject: [PATCH 17/25] =?UTF-8?q?design:=20=EB=AA=A8=EB=8B=AC=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EC=BD=98=20=EC=B6=94=EA=B0=80=20(#159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/assets/icons/icon_pen.svg | 4 ++++ src/shared/assets/icons/icon_trash_on.svg | 6 ++++++ src/shared/assets/icons/icon_warning.svg | 3 +++ src/shared/assets/icons/index.ts | 4 ++++ src/shared/ui/modal/modal.css.ts | 9 +++++++++ src/shared/ui/modal/modal.tsx | 5 +++++ 6 files changed, 31 insertions(+) create mode 100644 src/shared/assets/icons/icon_pen.svg create mode 100644 src/shared/assets/icons/icon_trash_on.svg create mode 100644 src/shared/assets/icons/icon_warning.svg diff --git a/src/shared/assets/icons/icon_pen.svg b/src/shared/assets/icons/icon_pen.svg new file mode 100644 index 00000000..c2e37652 --- /dev/null +++ b/src/shared/assets/icons/icon_pen.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/shared/assets/icons/icon_trash_on.svg b/src/shared/assets/icons/icon_trash_on.svg new file mode 100644 index 00000000..74bec63d --- /dev/null +++ b/src/shared/assets/icons/icon_trash_on.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/shared/assets/icons/icon_warning.svg b/src/shared/assets/icons/icon_warning.svg new file mode 100644 index 00000000..3f9274bb --- /dev/null +++ b/src/shared/assets/icons/icon_warning.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/shared/assets/icons/index.ts b/src/shared/assets/icons/index.ts index c7fe508d..02e4b8d4 100644 --- a/src/shared/assets/icons/index.ts +++ b/src/shared/assets/icons/index.ts @@ -53,3 +53,7 @@ export { default as IconJob } from "./icon_job.svg?react"; export { default as IconCopy } from "./icon_copy.svg?react"; export { default as IconCheckOn } from "./icon_check_on.svg?react"; + +export { default as IconPen } from "./icon_pen.svg?react"; +export { default as IconWarn } from "./icon_warning.svg?react"; +export { default as IconTrash } from "./icon_trash_on.svg?react"; diff --git a/src/shared/ui/modal/modal.css.ts b/src/shared/ui/modal/modal.css.ts index 5f1a6ff6..a4258fe2 100644 --- a/src/shared/ui/modal/modal.css.ts +++ b/src/shared/ui/modal/modal.css.ts @@ -58,6 +58,7 @@ export const Content = recipe({ base: { display: "flex", flexDirection: "column", + alignItems: "center", flex: 1, textAlign: "center", }, @@ -90,6 +91,14 @@ export const SubTitle = style({ ...themeVars.fontStyles.hline_m_18, }); +export const Icon = style({ + width: "8rem", + height: "8rem", + padding: "1.6rem", + borderRadius: "40px", + backgroundColor: themeVars.color.blue100, +}); + export const Image = style({ alignItems: "flex-end", width: "28rem", diff --git a/src/shared/ui/modal/modal.tsx b/src/shared/ui/modal/modal.tsx index a3295378..ac3bb3a3 100644 --- a/src/shared/ui/modal/modal.tsx +++ b/src/shared/ui/modal/modal.tsx @@ -87,6 +87,10 @@ const SubTitle = ({ children }: { children: ReactNode }) => { return
{children}
; }; +const Icon = ({ children }: { children: ReactNode }) => { + return
{children}
; +}; + const Image = () => { return 모달 이미지; }; @@ -100,6 +104,7 @@ Modal.XButton = XButton; Modal.Content = Content; Modal.Title = Title; Modal.SubTitle = SubTitle; +Modal.Icon = Icon; Modal.Image = Image; Modal.Buttons = Buttons; From 69198836ea9245efab8f8c72ab31520a4e2116c3 Mon Sep 17 00:00:00 2001 From: odukong Date: Thu, 26 Mar 2026 22:42:50 +0900 Subject: [PATCH 18/25] =?UTF-8?q?design:=20medium=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=ED=85=8D=EC=8A=A4=ED=8A=B8=20=ED=81=AC=EA=B8=B0=20body=5Fm=5F1?= =?UTF-8?q?4=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20(#159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/button/button.css.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/ui/button/button.css.ts b/src/shared/ui/button/button.css.ts index e5b62557..fd3affb8 100644 --- a/src/shared/ui/button/button.css.ts +++ b/src/shared/ui/button/button.css.ts @@ -77,7 +77,7 @@ export const buttonSizes = styleVariants({ medium: { width: "12rem", height: "4.8rem", - ...themeVars.fontStyles.body_m_16, + ...themeVars.fontStyles.body_m_14, }, small: { width: "8rem", From b246e980e0de099b3540e146159ac589da5f9177 Mon Sep 17 00:00:00 2001 From: odukong Date: Thu, 26 Mar 2026 23:33:54 +0900 Subject: [PATCH 19/25] =?UTF-8?q?design:=20Modal=20css=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81=20(#159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/modal/modal.css.ts | 60 ++++++++++++++------------------ src/shared/ui/modal/modal.tsx | 15 ++++---- 2 files changed, 33 insertions(+), 42 deletions(-) diff --git a/src/shared/ui/modal/modal.css.ts b/src/shared/ui/modal/modal.css.ts index a4258fe2..7c59b50e 100644 --- a/src/shared/ui/modal/modal.css.ts +++ b/src/shared/ui/modal/modal.css.ts @@ -29,9 +29,9 @@ export const modalContent = recipe({ variants: { size: { default: { - width: "60rem", - height: "46rem", - padding: "1.5rem 1.6rem 4.8rem 1.6rem", + width: "52rem", + height: "36.2rem", + padding: "1.6rem 1.6rem 4rem 1.6rem", }, auto: { width: "auto", @@ -48,47 +48,39 @@ export const modalContent = recipe({ }); export const XButton = style({ - maxWidth: "4.4rem", - maxHeight: "4.4rem", + maxWidth: "2.4rem", + maxHeight: "2.4rem", alignSelf: "flex-end", color: "black", }); -export const Content = recipe({ - base: { - display: "flex", - flexDirection: "column", - alignItems: "center", - flex: 1, - textAlign: "center", - }, - variants: { - type: { - default: { - justifyContent: "center", - gap: "1.6rem", - }, - auto: { - justifyContent: "flex-end", - gap: "0.8rem", - }, - }, - }, - defaultVariants: { - type: "default", - }, +export const Content = style({ + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "flex-end", + gap: "1.6rem", + flex: 1, + textAlign: "center", + padding: "1.6rem", +}); + +export const TitleGroup = style({ + display: "flex", + flexDirection: "column", + gap: "0.4rem", }); export const Title = style({ whiteSpace: "pre-wrap", - color: themeVars.color.blue600, - ...themeVars.fontStyles.title_b_28, + color: themeVars.color.gray700, + ...themeVars.fontStyles.hding_b_22, }); export const SubTitle = style({ whiteSpace: "pre-wrap", color: themeVars.color.gray500, - ...themeVars.fontStyles.hline_m_18, + ...themeVars.fontStyles.body_m_14, }); export const Icon = style({ @@ -101,10 +93,9 @@ export const Icon = style({ export const Image = style({ alignItems: "flex-end", - width: "28rem", - height: "28rem", + width: "17.5rem", + height: "17.5rem", aspectRatio: 1 / 1, - marginBottom: "-2rem", }); export const Buttons = style({ @@ -112,4 +103,5 @@ export const Buttons = style({ display: "flex", justifyContent: "center", gap: "1.6rem", + padding: "1.6rem", }); diff --git a/src/shared/ui/modal/modal.tsx b/src/shared/ui/modal/modal.tsx index ac3bb3a3..fd41b821 100644 --- a/src/shared/ui/modal/modal.tsx +++ b/src/shared/ui/modal/modal.tsx @@ -69,14 +69,12 @@ const XButton = () => { ); }; -const Content = ({ - children, - type, -}: { - children: ReactNode; - type?: "default" | "auto"; -}) => { - return
{children}
; +const Content = ({ children }: { children: ReactNode }) => { + return
{children}
; +}; + +const TitleGroup = ({ children }: { children: ReactNode }) => { + return
{children}
; }; const Title = ({ children }: { children: ReactNode }) => { @@ -102,6 +100,7 @@ const Buttons = ({ children }: { children: ReactNode }) => { // 내보내기 Modal.XButton = XButton; Modal.Content = Content; +Modal.TitleGroup = TitleGroup; Modal.Title = Title; Modal.SubTitle = SubTitle; Modal.Icon = Icon; From 6649060290fbe6a370476fea24daa1a80fe4e24a Mon Sep 17 00:00:00 2001 From: odukong Date: Thu, 26 Mar 2026 23:45:55 +0900 Subject: [PATCH 20/25] =?UTF-8?q?refactor:=20Modal=20css=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 모달 컴포넌트 css와 아이콘 추가에 따른 모달을 사용하는 파일 코드 수정 --- .../model/use-leave-confirm.tsx | 14 +++--- .../experience-viewer/experience-viewer.tsx | 16 +++--- .../ui/select-company/select-company.tsx | 49 ++++++++++--------- src/shared/ui/modal/modal-basic.tsx | 15 ++++-- 4 files changed, 54 insertions(+), 40 deletions(-) diff --git a/src/features/experience-detail/model/use-leave-confirm.tsx b/src/features/experience-detail/model/use-leave-confirm.tsx index a5cfee1a..266b1c1d 100644 --- a/src/features/experience-detail/model/use-leave-confirm.tsx +++ b/src/features/experience-detail/model/use-leave-confirm.tsx @@ -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"; @@ -65,12 +66,13 @@ export const useLeaveConfirm = () => { if (blocker.state === "blocked") { modalStore.open( } + title={`작성 중인 내용이 있어요`} + subTitle="저장하지 않으면 내용이 모두 사라져요." + closeText="나가기" + confirmText="계속 작성하기" + onClose={confirmLeave} + onConfirm={cancelLeave} />, 0, undefined, diff --git a/src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx b/src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx index 42ca1d30..385fc9cd 100644 --- a/src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx +++ b/src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx @@ -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"; @@ -42,15 +43,18 @@ const ExperienceViewer = () => { const handleOpenDeleteModal = () => { modalStore.open( modalStore.reset()} // 취소 시 닫기 - onConfirm={() => { + icon={} + title="이 경험을 삭제할까요?" + subTitle="삭제하면 다시 복구할 수 없어요" + closeText="삭제하기" + confirmText="취소하기" + onClose={() => { onClickDelete(); // 실제 삭제 동작 modalStore.reset(); // 모달 닫기 }} + onConfirm={() => { + modalStore.reset(); // 취소 시 닫기 + }} /> ); }; 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..dfc67f5e 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/shared/ui/modal/modal-basic.tsx b/src/shared/ui/modal/modal-basic.tsx index cefdd5b3..54bbcacf 100644 --- a/src/shared/ui/modal/modal-basic.tsx +++ b/src/shared/ui/modal/modal-basic.tsx @@ -2,9 +2,12 @@ import { Button } from "../button/button"; import { Modal } from "./modal"; +import type { ReactNode } from "react"; + interface ModalBasicProps { onClose: () => void; // 모달 닫기 액션 onConfirm: () => void; // 모달 작업 확인 액션 + icon?: ReactNode; // 아이콘 title: string; // 모달 제목 subTitle?: string; // 모달 소제목 closeText?: string; // 모달 닫기 버튼 텍스트 @@ -18,6 +21,7 @@ interface ModalBasicProps { export const ModalBasic = ({ onClose, onConfirm, + icon, title, subTitle, closeText = "나가기", @@ -27,14 +31,17 @@ export const ModalBasic = ({ <> - {title} - {subTitle && {subTitle}} + {icon && {icon}} + + {title} + {subTitle && {subTitle}} + - - From 7b0c65a877478d2b79688f6fd1bad718c6dd1678 Mon Sep 17 00:00:00 2001 From: odukong Date: Fri, 27 Mar 2026 18:22:14 +0900 Subject: [PATCH 21/25] =?UTF-8?q?design:=20=EC=BD=94=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EB=B9=97=20=EC=88=98=EC=A0=95=EC=82=AC=ED=95=AD=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20(#159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/modal/modal.css.ts | 1 - src/shared/ui/modal/modal.stories.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/shared/ui/modal/modal.css.ts b/src/shared/ui/modal/modal.css.ts index 7c59b50e..eb4ad4bd 100644 --- a/src/shared/ui/modal/modal.css.ts +++ b/src/shared/ui/modal/modal.css.ts @@ -51,7 +51,6 @@ export const XButton = style({ maxWidth: "2.4rem", maxHeight: "2.4rem", alignSelf: "flex-end", - color: "black", }); export const Content = style({ diff --git a/src/shared/ui/modal/modal.stories.tsx b/src/shared/ui/modal/modal.stories.tsx index 2ba928f1..411b7a57 100644 --- a/src/shared/ui/modal/modal.stories.tsx +++ b/src/shared/ui/modal/modal.stories.tsx @@ -95,7 +95,7 @@ export const Confirm: Story = { export const WithImage: Story = { render: () => ( - + CJ ENM을 선택하셨습니다 기업 내용 분석 내용을 불러오는 중입니다. From ad92565537239e82ee99007c216593827d199387 Mon Sep 17 00:00:00 2001 From: odukong Date: Tue, 31 Mar 2026 17:08:44 +0900 Subject: [PATCH 22/25] =?UTF-8?q?design:=20=EB=AA=A8=EB=8B=AC=20=ED=85=8D?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=9D=84=EC=96=B4=EC=93=B0=EA=B8=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../experience-matching/ui/select-company/select-company.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 dfc67f5e..83da168d 100644 --- a/src/features/experience-matching/ui/select-company/select-company.tsx +++ b/src/features/experience-matching/ui/select-company/select-company.tsx @@ -49,7 +49,7 @@ export const SelectCompany = ({ onClick }: { onClick: () => void }) => { icon={} title="아직 등록된 경험이 없어요" subTitle="경험을 등록하고 AI매칭을 시작해보세요" - closeText="나중에할게요" + closeText="나중에 할게요" confirmText="경험 등록하기" />, undefined, From 7d20951ee2c9d149cdbf37956584fc489ef057f0 Mon Sep 17 00:00:00 2001 From: odukong Date: Tue, 31 Mar 2026 17:20:22 +0900 Subject: [PATCH 23/25] =?UTF-8?q?fix:=20=EC=B9=B4=EC=B9=B4=EC=98=A4?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EB=A6=AC=ED=94=84=EB=A0=88=EC=89=AC=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0=20?= =?UTF-8?q?(#159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 카카오로그인 시에 두 번씩 요청되던 문제를 ref를 통해 한 번만 요청이 보내지도록 수정하였습니다 - refreshToken에 대한 요청을 cookie로 통신함에 따라 관련 axios-instance.ts 로직을 수정하였습니다. --- src/pages/login/kakao-login-page.tsx | 6 ++++-- src/shared/api/axios-instance.ts | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pages/login/kakao-login-page.tsx b/src/pages/login/kakao-login-page.tsx index 6d2aabb2..d7ba1ec1 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, useRef, 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/shared/api/axios-instance.ts b/src/shared/api/axios-instance.ts index 1dd03bb9..adeab521 100644 --- a/src/shared/api/axios-instance.ts +++ b/src/shared/api/axios-instance.ts @@ -61,13 +61,13 @@ axiosInstance.interceptors.response.use( ); // 새로 발급 받은 액세스 토큰 저장 - const newAccessToken = data.accessToken; + const newAccessToken = data.result.accessToken; tokenStorage.set(newAccessToken); originalRequest.headers["Authorization"] = `Bearer ${newAccessToken}`; return axiosInstance(originalRequest); // 이전 요청 재시도 } catch (refreshError) { - alert("리프레쉬 토큰 요청에 실패했습니다."); + alert("리프레쉬 토큰 요청에 실패했습니다. 다시 로그인해주세요."); tokenStorage.clear(); window.location.replace("/login"); From 9a94bbd5d3663c6526b256dee765028c4e1037ac Mon Sep 17 00:00:00 2001 From: odukong Date: Tue, 31 Mar 2026 17:30:53 +0900 Subject: [PATCH 24/25] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=ED=95=B4=EA=B2=B0=20(#159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/experience-viewer/experience-viewer.tsx | 1 - src/pages/login/kakao-login-page.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx b/src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx index 093cb541..743890d8 100644 --- a/src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx +++ b/src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx @@ -1,5 +1,4 @@ import { IconTrash } from "@/shared/assets/icons"; -import { EXPERIENCE_TYPE } from "@/shared/config/experience"; import { EXPERIENCE_TYPE, type ExperienceTypeCode, diff --git a/src/pages/login/kakao-login-page.tsx b/src/pages/login/kakao-login-page.tsx index d7ba1ec1..21d44775 100644 --- a/src/pages/login/kakao-login-page.tsx +++ b/src/pages/login/kakao-login-page.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { useAuthStore } from "@/app/store"; From e50c4b545146283a8d696d03987ca7059494cfb1 Mon Sep 17 00:00:00 2001 From: odukong Date: Wed, 1 Apr 2026 02:05:26 +0900 Subject: [PATCH 25/25] =?UTF-8?q?fix:=20ai-report=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EC=9A=94=EC=B2=AD=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0=20(#159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/analyzing/analyzing.tsx | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/src/features/experience-matching/ui/analyzing/analyzing.tsx b/src/features/experience-matching/ui/analyzing/analyzing.tsx index 6c32a332..598586e4 100644 --- a/src/features/experience-matching/ui/analyzing/analyzing.tsx +++ b/src/features/experience-matching/ui/analyzing/analyzing.tsx @@ -10,38 +10,44 @@ import * as styles from "./analyzing.css"; import type { CustomErrorResponse } from "@/shared/api/generate/http-client"; +let isRequesting = false; + export const Analyzing = ({ nextStep }: { nextStep: () => void }) => { const { company, experience, jobDescription, setReportId } = useReportStore(); - const { mutate } = useCreateReport(); + const { mutateAsync } = useCreateReport(); // 에러 핸들링 (임시) const [open, setOpen] = useState(false); const [errorMsg, setErrorMsg] = useState(""); useEffect(() => { - mutate( - { - companyId: company?.id ?? 0, - experienceId: experience?.id ?? 0, - jobDescription: jobDescription, - }, - { - onSuccess: (response) => { - setReportId(response?.id ?? 0); - nextStep(); - }, - onError: (error: CustomErrorResponse) => { - const serverMessage = - error.message || "리포트 생성 중 에러가 발생했습니다"; - setErrorMsg(serverMessage); - setOpen(true); + if (isRequesting) return; + isRequesting = true; + + const handleRequest = async () => { + try { + const response = await mutateAsync({ + companyId: company?.id ?? 0, + experienceId: experience?.id ?? 0, + jobDescription: jobDescription, + }); - setTimeout(() => setOpen(false), 3000); - }, + setReportId(response?.id ?? 0); + nextStep(); + } catch (err) { + const error = err as CustomErrorResponse; + const serverMessage = + error.message || "리포트 생성 중 에러가 발생했습니다"; + setErrorMsg(serverMessage); + setOpen(true); + setTimeout(() => setOpen(false), 3000); + } finally { + isRequesting = false; } - ); - }, [nextStep, setReportId, mutate]); + }; + handleRequest(); + }, []); return ( <>