Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 3 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/src/assets/logo/symbol-color.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>whereyouad_fe</title>
<title>whereyouad</title>
</head>
<body>
<div id="root"></div>
<div id="modal-root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-hook-form": "^7.71.1",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.12.0",
"sonner": "^2.0.7",
"tailwindcss": "^4.1.18",
Expand Down
673 changes: 673 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions src/assets/docs/marketing-agreement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
## 마케팅 정보 수신 동의 (선택)

1. 마케팅 정보 전송 방법
마케팅 정보는 회사의 각 개별 서비스에서 정한 방식에 따라 전송됩니다.

2. 마케팅 목적의 개인정보 이용
회사는 이용자의 개인정보를 다음과 같은 마케팅 목적으로 이용할 수 있습니다.

- 이벤트, 프로모션, 혜택 정보 안내
- 신규 서비스 및 기능 소개
- 맞춤형 서비스 및 콘텐츠 제공

3. 광고성 정보 전송 수단
- 이메일
- 문자메시지(SMS/LMS)
- 앱 푸시 알림

> ※ 본 동의는 선택 사항이며, 동의하지 않아도 서비스 이용에는 제한이 없습니다.
24 changes: 24 additions & 0 deletions src/assets/docs/privacy-collection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## 개인정보 수집 및 이용 동의 (필수)

1. 개인정보 수집 및 이용 목적
회사는 WhereYouAd 서비스 제공을 위해 아래와 같은 목적으로 개인정보를 수집하고 이용합니다.

- 서비스 제공: 광고 매체 데이터 연동 및 통합 대시보드 제공, 리포트 생성, 맞춤형 광고 성과 분석 서비스 제공
- 회원 관리: 회원제 서비스 이용에 따른 본인 확인, 개인 식별, 불량 회원의 부정 이용 방지, 가입 의사 확인, 민원 처리 및 고지 사항 전달
- 서비스 개선: 신규 서비스 개발 및 맞춤 서비스 제공, 서비스 이용 기록 분석 및 통계

2. 수집하는 개인정보 항목
회사는 회원가입, 상담, 서비스 신청 등을 위해 아래와 같은 개인정보를 수집하고 있습니다.

- **필수 항목**: 이메일 주소(ID), 비밀번호, 이름, 휴대전화 번호
- 자동 수집 정보: 서비스 이용 기록, 접속 로그, 쿠키, 접속 IP 정보, 기기 정보, 브라우저 유저 에이전트

3. 개인정보 보유 및 이용 기간
이용자의 개인정보는 원칙적으로 개인정보 수집 및 이용 목적이 달성된 후(회원 탈퇴 시) 지체 없이 파기합니다. 단, 관계 법령에 의하여 보존할 필요가 있는 경우에는 해당 기간 동안 보관합니다.

- 계약 또는 청약철회 등에 관한 기록: 5년
- 대금결제 및 재화 등의 공급에 관한 기록: 5년
- 소비자의 불만 또는 분쟁처리에 관한 기록: 3년
- 로그기록 (통신비밀보호법): 3개월

> ※ 귀하는 개인정보 수집 및 이용에 대한 동의를 거부할 권리가 있습니다. 단, 동의를 거부할 경우 회원가입 및 주요 서비스 이용이 제한될 수 있습니다.
3 changes: 3 additions & 0 deletions src/assets/icon/x-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 32 additions & 5 deletions src/components/auth/signupStep/Step03Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,24 @@ import { step03Schema } from "@/utils/validation";

import CommonAuthInput from "@/components/auth/CommonAuthInput";
import Button from "@/components/common/Button";
import { MODAL_TYPES } from "@/components/modal/ModalProvider";

import useAuthStore from "@/store/useAuthStore";
import useModalStore from "@/store/useModalStore";

type TStep03FormValues = z.infer<typeof step03Schema>;

export default function SignupProfile() {
const navigate = useNavigate();
const { email, password } = useAuthStore();
const { openModal } = useModalStore();

const {
register,
handleSubmit,
control,
setValue,
watch,
formState: { errors, isValid },
} = useForm<TStep03FormValues>({
mode: "onChange",
Expand Down Expand Up @@ -75,22 +80,44 @@ export default function SignupProfile() {
)}
/>

<div className="flex items-center mt-3 pl-1 w-full justify-between">
<div
className="flex items-center mt-3 pl-1 w-full justify-between cursor-pointer"
onClick={() => {
openModal({
modalType: MODAL_TYPES.PRIVACY,
modalProps: {
initialState: {
privacy: watch("terms") || false,
marketing: watch("marketing") || false,
},
onAgree: (agreements) => {
setValue("marketing", agreements.marketing);
if (agreements.privacy) {
setValue("terms", true, { shouldValidate: true });
toast.success("약관에 동의하였습니다.");
} else {
setValue("terms", false, { shouldValidate: true });
}
},
},
});
}}
>
<div className="flex items-center gap-3">
<input
type="checkbox"
className="checkbox"
{...register("terms")}
className="checkbox pointer-events-none"
checked={watch("terms")}
readOnly
/>
<span className="font-body2 text-text-main flex items-center gap-2">
<span className="text-brand-700 font-body2">(필수)</span>
개인정보 수집 및 이용 동의
</span>
</div>
<button
type="button" // form submit 방지
type="button"
className="text-text-sub underline hover:text-text-main font-body2 whitespace-nowrap"
onClick={() => toast.info("개인정보 처리방침 내용입니다.")}
>
내용 보기
</button>
Expand Down
40 changes: 40 additions & 0 deletions src/components/modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { ReactNode } from "react";
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";

import CloseIcon from "@/assets/icon/x-icon.svg?react";

type TModalprops = {
isOpen?: boolean;
children: ReactNode;
onClose: () => void;
};

export default function Modal({
isOpen = true,
children,
onClose,
}: TModalprops) {
const [isVisible, setIsVisible] = useState(isOpen);

useEffect(() => {
setIsVisible(isOpen);
}, [isOpen]);

return createPortal(
isVisible && (
<div className="z-1000 fixed top-0 left-0 w-screen h-screen bg-black/30 flex items-center justify-center">
<div className="relative bg-white p-5 flex flex-col rounding-15 shadow-Medium min-w-[320px]">
<div
className="absolute top-4 right-4 cursor-pointer"
onClick={onClose}
>
<CloseIcon className="w-5 h-auto" />
</div>
<div className="flex w-full">{children}</div>
</div>
</div>
),
document.getElementById("modal-root")!,
);
}
31 changes: 31 additions & 0 deletions src/components/modal/ModalProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useEffect } from "react";
import { useLocation } from "react-router-dom";

import PrivacyModal from "./privacyModal/PrivacyModal";

import useModalStore from "@/store/useModalStore";

export const MODAL_TYPES = {
PRIVACY: "PRIVACY",
} as const;

// 모달 등록
export const MODAL_COMPONENTS = {
[MODAL_TYPES.PRIVACY]: PrivacyModal,
};

export default function ModalProvider() {
const { modalType, closeModal, modalProps } = useModalStore();
const location = useLocation();
useEffect(() => {
closeModal();
}, [location]);

if (!modalType) {
return null;
}

const ModalComponent = MODAL_COMPONENTS[modalType];

return <ModalComponent onClose={closeModal} {...modalProps} />;
}
100 changes: 100 additions & 0 deletions src/components/modal/privacyModal/AgreementItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import ReactMarkdown, { type Components } from "react-markdown";

type TAgreementItemProps = {
label: string;
required?: boolean;
checked: boolean;
expanded: boolean;
onToggleCheck: () => void;
onToggleExpand: () => void;
content: string;
};

const markdownComponents: Components = {
h2: ({ ...props }) => (
<h2
className="font-heading3 text-text-main font-bold mb-2 mt-4 first:mt-0"
{...props}
/>
),
h3: ({ ...props }) => (
<h3 className="font-body1 text-text-sub font-bold mb-1 mt-3" {...props} />
),
p: ({ ...props }) => (
<p className="mb-3 last:mb-0 leading-relaxed" {...props} />
),
ul: ({ ...props }) => (
<ul className="list-disc pl-5 mb-3 space-y-1" {...props} />
),
ol: ({ ...props }) => (
<ol className="list-decimal pl-5 mb-3 space-y-1" {...props} />
),
li: ({ ...props }) => (
<li className="pl-1 marker:text-text-disabled" {...props} />
),
};

export default function AgreementItem({
label,
required,
checked,
expanded,
onToggleCheck,
onToggleExpand,
content,
}: TAgreementItemProps) {
return (
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between">
<div
className="flex items-center gap-3 cursor-pointer"
onClick={onToggleCheck}
role="checkbox"
aria-checked={checked}
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onToggleCheck();
}
}}
>
<input
type="checkbox"
className="checkbox"
checked={checked}
readOnly
tabIndex={-1}
aria-hidden="true"
/>
<span className="font-body2 text-text-main flex items-center gap-1">
{label}
<span
className={`text-xs ${
required ? "text-color-brand-600" : "text-text-sub"
}`}
>
({required ? "필수" : "선택"})
</span>
</span>
</div>
<button
onClick={onToggleExpand}
className="text-text-sub text-sm underline hover:text-text-main"
aria-expanded={expanded}
aria-label={`${label} 상세 내용 ${expanded ? "접기" : "보기"}`}
>
{expanded ? "접기" : "보기"}
</button>
</div>

{expanded && (
<div className="bg-brand-300 p-5 rounded-15 text-sm text-text-sub leading-relaxed max-h-50 overflow-y-auto">
<ReactMarkdown components={markdownComponents}>
{content}
</ReactMarkdown>
</div>
)}
</div>
);
}
Loading