Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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.
32 changes: 27 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,39 @@ 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: {
onAgree: (agreements) => {
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