diff --git a/package.json b/package.json index 45e2f17..058eb5b 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ ] }, "dependencies": { - "@toss/use-funnel": "^1.4.2", "axios": "^1.13.2", "next": "16.0.10", "react": "19.2.1", diff --git a/public/images/onboarding-step-1.svg b/public/images/onboarding-step-1.svg new file mode 100644 index 0000000..4275a77 --- /dev/null +++ b/public/images/onboarding-step-1.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/onboarding-step-2.svg b/public/images/onboarding-step-2.svg new file mode 100644 index 0000000..d2c097b --- /dev/null +++ b/public/images/onboarding-step-2.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/onboarding-step-3.svg b/public/images/onboarding-step-3.svg new file mode 100644 index 0000000..2fdb266 --- /dev/null +++ b/public/images/onboarding-step-3.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index c96a610..f746d0d 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -10,7 +10,7 @@ const AuthStartPage = () => { }; return ( -
+
{/* 상단 로고 영역 */}
diff --git a/src/app/login/phone/page.tsx b/src/app/login/phone/page.tsx index 6c3ef77..efa7882 100644 --- a/src/app/login/phone/page.tsx +++ b/src/app/login/phone/page.tsx @@ -27,12 +27,12 @@ const LoginPage = () => { const handlePhoneChange = phoneChangeHandler(setPhone); return ( -
-
+
+
diff --git a/src/app/onboarding/page.tsx b/src/app/onboarding/page.tsx new file mode 100644 index 0000000..d31738a --- /dev/null +++ b/src/app/onboarding/page.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { useRouter } from "next/navigation"; + +import { useState } from "react"; + +import OnboardingStep from "@/components/onboarding/OnboardingStep"; +import ProgressDots from "@/components/onboarding/ProgressDots"; + +import { + ONBOARDING_STEPS, + type OnboardingStepName, +} from "@/constants/onboardingSteps"; + +const OnboardingPage = () => { + const router = useRouter(); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + + const currentStepName = ONBOARDING_STEPS[ + currentStepIndex + ] as OnboardingStepName; + const isLastStep = currentStepIndex === ONBOARDING_STEPS.length - 1; + + const goNext = () => { + const nextIndex = currentStepIndex + 1; + + if (nextIndex >= ONBOARDING_STEPS.length) { + if (typeof window !== "undefined") { + localStorage.setItem("onboardingDone", "true"); + } + router.push("/onboarding/profile"); + return; + } + + setCurrentStepIndex(nextIndex); + }; + + return ( +
+
+
+ +
+ + +
+
+ ); +}; + +export default OnboardingPage; diff --git a/src/app/onboarding/profile/page.tsx b/src/app/onboarding/profile/page.tsx new file mode 100644 index 0000000..ab57adf --- /dev/null +++ b/src/app/onboarding/profile/page.tsx @@ -0,0 +1,6 @@ +import OnboardingProfileClient from "@/components/onboarding/OnboardingProfile"; + +const OnboardingProfilePage = () => { + return ; +}; +export default OnboardingProfilePage; diff --git a/src/app/page.tsx b/src/app/page.tsx index a170921..6b3cc57 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,3 +1,3 @@ -export default function Home() { - return
홈화면
; +export default function HomePage() { + return
홈 화면
; } diff --git a/src/app/password/find/page.tsx b/src/app/password/find/page.tsx index 927c7c2..2514ddc 100644 --- a/src/app/password/find/page.tsx +++ b/src/app/password/find/page.tsx @@ -4,7 +4,7 @@ import { PasswordForm } from "@/components/password/PasswordForm"; const PasswordPage = () => { return ( -
+
비밀번호를 찾아볼까요? diff --git a/src/app/password/find/result/page.tsx b/src/app/password/find/result/page.tsx index 51951b6..515bb7d 100644 --- a/src/app/password/find/result/page.tsx +++ b/src/app/password/find/result/page.tsx @@ -13,8 +13,8 @@ const FindPage = () => { const router = useRouter(); return ( -
-
+
+
기존에 사용하셨던 비밀번호예요. diff --git a/src/app/signup/agree/page.tsx b/src/app/signup/agree/page.tsx index f14f87e..50d05de 100644 --- a/src/app/signup/agree/page.tsx +++ b/src/app/signup/agree/page.tsx @@ -9,39 +9,17 @@ import { Checkbox } from "@/components/common/Checkbox"; import { FullButton } from "@/components/common/FullButton"; import { PageTitle } from "@/components/common/PageTitle"; -type AgreementKey = - | "service" // 심톡 이용약관 동의 (필수) - | "finance" // 전자금융거래 이용약관 동의 (필수) - | "personalReq" // 개인정보 수집 이용 동의 (필수) - | "personalOpt1" // 개인정보 수집 이용 동의 (선택) - | "personalOpt2" // 개인정보 수집 이용 동의 (선택) - | "marketing"; // 마케팅 정보 수신 동의 (선택) +import { AGREEMENTS, INITIAL_AGREEMENTS } from "@/constants/agreement"; -const AGREEMENTS: { key: AgreementKey; label: string }[] = [ - { key: "service", label: "심톡 이용약관 동의 (필수)" }, - { key: "finance", label: "전자금융거래 이용약관 동의 (필수)" }, - { key: "personalReq", label: "개인정보 수집 이용 동의 (필수)" }, - { key: "personalOpt1", label: "개인정보 수집 이용 동의 (선택)" }, - { key: "personalOpt2", label: "개인정보 수집 이용 동의 (선택)" }, - { key: "marketing", label: "마케팅 정보 메일, SNS수신동의 (선택)" }, -]; - -const initialAgreements: Record = { - service: false, - finance: false, - personalReq: false, - personalOpt1: false, - personalOpt2: false, - marketing: false, -}; +import type { AgreementKey } from "@/types/agreement.type"; const AgreePage = () => { const router = useRouter(); const [agreements, setAgreements] = - useState>(initialAgreements); + useState>(INITIAL_AGREEMENTS); const isConfirmActive = - agreements.service && agreements.finance && agreements.personalReq; + agreements.service && agreements.finance && agreements.personalRequired; const allChecked = Object.values(agreements).every(Boolean); diff --git a/src/app/signup/password/page.tsx b/src/app/signup/password/page.tsx index 6f263a5..2cbbb4c 100644 --- a/src/app/signup/password/page.tsx +++ b/src/app/signup/password/page.tsx @@ -134,7 +134,7 @@ const SettingPage = () => {
router.push("/login")} + onClick={() => router.push("/onboarding")} > 완료 diff --git a/src/assets/onboarding_profile.svg b/src/assets/onboarding_profile.svg new file mode 100644 index 0000000..76a5642 --- /dev/null +++ b/src/assets/onboarding_profile.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/photo.svg b/src/assets/photo.svg new file mode 100644 index 0000000..f4b7873 --- /dev/null +++ b/src/assets/photo.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/components/common/FullButton.tsx b/src/components/common/FullButton.tsx index 0d39032..85f83d9 100644 --- a/src/components/common/FullButton.tsx +++ b/src/components/common/FullButton.tsx @@ -27,8 +27,8 @@ export const FullButton = ({ disabled={!isActive} {...props} > -
- {children} +
+ {children}
); diff --git a/src/components/common/AlertModal.tsx b/src/components/common/LoadingModal.tsx similarity index 58% rename from src/components/common/AlertModal.tsx rename to src/components/common/LoadingModal.tsx index bdb6b87..7bbc03a 100644 --- a/src/components/common/AlertModal.tsx +++ b/src/components/common/LoadingModal.tsx @@ -1,25 +1,33 @@ "use client"; -type AlertModalProps = { +import { ALERT_BACKDROP_CLASS, AlertBackdrop } from "@/constants/alert"; + +interface LoadingModalProps { isOpen: boolean; title: string; - message: string; + message?: string; confirmLabel?: string; onClose: () => void; -}; + backdrop?: AlertBackdrop; + isLoading?: boolean; +} -export const AlertModal = ({ +const LoadingModal = ({ isOpen, title, message, confirmLabel = "확인", onClose, -}: AlertModalProps) => { + backdrop = "default", + isLoading = false, +}: LoadingModalProps) => { if (!isOpen) return null; return (
-

{message}

+ {isLoading ? ( + + ) : ( + message && ( +

+ {message} +

+ ) + )}
@@ -47,3 +63,5 @@ export const AlertModal = ({
); }; + +export default LoadingModal; diff --git a/src/components/common/NameInput.tsx b/src/components/common/NameInput.tsx new file mode 100644 index 0000000..6d99d4c --- /dev/null +++ b/src/components/common/NameInput.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; + +interface NameInputProps { + value: string; + onChange: (value: string) => void; +} + +const PLACEHOLDER = "이름을 입력해주세요"; +const INITIAL_WIDTH = 204; + +export const NameInput = ({ value, onChange }: NameInputProps) => { + const spanRef = useRef(null); + const [inputWidth, setInputWidth] = useState(INITIAL_WIDTH); + + useEffect(() => { + if (!spanRef.current) return; + + const spanWidth = spanRef.current.offsetWidth; + setInputWidth(spanWidth + 36); + }, [value]); + + return ( +
+ + {value || PLACEHOLDER} + + + onChange(e.target.value)} + placeholder={PLACEHOLDER} + style={{ width: inputWidth }} + className="border-mint-01 text-d3 text-neutral-01 placeholder:text-neutral-07 rounded-2xl border px-4 py-2 text-center transition-[width] duration-150 ease-out outline-none" + /> +
+ ); +}; diff --git a/src/components/dailyRecord/MissionFeed.tsx b/src/components/dailyRecord/MissionFeed.tsx index c32df5d..393fc06 100644 --- a/src/components/dailyRecord/MissionFeed.tsx +++ b/src/components/dailyRecord/MissionFeed.tsx @@ -49,13 +49,13 @@ export const RecordMissionFeed = () => {
{otherRecords.map(item => ( -
+
{item.userName}

{item.userName}

diff --git a/src/components/onboarding/OnboardingProfile.tsx b/src/components/onboarding/OnboardingProfile.tsx new file mode 100644 index 0000000..957640e --- /dev/null +++ b/src/components/onboarding/OnboardingProfile.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { useRouter } from "next/navigation"; + +import { useState } from "react"; + +import { FullButton } from "@/components/common/FullButton"; +import LoadingModal from "@/components/common/LoadingModal"; +import { NameInput } from "@/components/common/NameInput"; +import ProfileImagePicker from "@/components/onboarding/ProfileImagePicker"; +import UploadButton from "@/components/onboarding/UploadButton"; + +import { useProfileImageUpload } from "@/hooks/useProfileImageUpload"; + +const OnboardingProfileClient = () => { + const router = useRouter(); + + const [name, setName] = useState(""); + const [isUploadOpen, setIsUploadOpen] = useState(false); + + const isNameValid = name.trim().length > 0; + + const { profileImage, isLoading, uploadImage, resetImage, cancelUpload } = + useProfileImageUpload(); + + const handleCreateProfile = async () => { + if (!isNameValid) return; + + // 1) 프로필 데이터 저장 (지금은 임시로 localStorage 예시) + const profile = { + name: name.trim(), + imageUrl: profileImage ?? null, + }; + try { + // TODO: 실제 API로 교체 + // await fetch("/api/profile", { method: "POST", body: JSON.stringify(profile) }); + localStorage.setItem("onboardingProfile", JSON.stringify(profile)); + } catch (e) { + // 에러 처리 + console.error(e); + return; + } + // 2) 저장이 끝난 뒤에만 페이지 이동 + router.replace("/login"); + }; + + return ( + <> +
+
+
+

+ 가족들에게 보여줄{"\n"}내 프로필을 만들어주세요 +

+
+
+
+ setIsUploadOpen(true)} + /> +
+ +
+
+
+ +
+ {!isUploadOpen && ( + + 프로필생성하기 + + )} +
+
+
+ setIsUploadOpen(false)} + onSelectAlbum={async file => { + try { + await uploadImage(file); + setIsUploadOpen(false); + } catch (error) { + console.error("이미지 업로드 실패:", error); + } + }} + onSelectDefault={() => { + resetImage(); + setIsUploadOpen(false); + }} + /> + + + + ); +}; + +export default OnboardingProfileClient; diff --git a/src/components/onboarding/OnboardingStep.tsx b/src/components/onboarding/OnboardingStep.tsx new file mode 100644 index 0000000..879cf52 --- /dev/null +++ b/src/components/onboarding/OnboardingStep.tsx @@ -0,0 +1,55 @@ +"use client"; + +import Image from "next/image"; + +import { FullButton } from "@/components/common/FullButton"; + +import type { OnboardingStepName } from "@/constants/onboardingSteps"; +import { ONBOARDING_CONTENTS } from "@/constants/onboardingSteps"; + +interface Props { + stepName: OnboardingStepName; + isLastStep: boolean; + onNext: () => void; +} + +const OnboardingStep = ({ stepName, isLastStep, onNext }: Props) => { + const { title, background } = ONBOARDING_CONTENTS[stepName]; + const [firstLine, secondLine] = title.split("\n"); + + return ( +
+ {background.type === "image" && ( + + )} + + {background.type === "class" && ( +
+ )} + +
+
+

+ {firstLine} + {secondLine} +

+ + + {isLastStep ? "시작하기" : "다음"} + +
+
+
+ ); +}; + +export default OnboardingStep; diff --git a/src/components/onboarding/ProfileImagePicker.tsx b/src/components/onboarding/ProfileImagePicker.tsx new file mode 100644 index 0000000..6e3fb5e --- /dev/null +++ b/src/components/onboarding/ProfileImagePicker.tsx @@ -0,0 +1,37 @@ +"use client"; + +import ProfileIcon from "@/assets/onboarding_profile.svg"; +import PhotoIcon from "@/assets/photo.svg"; + +type ProfileImagePickerProps = { + imageUrl: string | null; + onClick: () => void; +}; + +const ProfileImagePicker = ({ imageUrl, onClick }: ProfileImagePickerProps) => { + return ( +
+ {imageUrl ? ( + 프로필 이미지 + ) : ( + + )} + + +
+ ); +}; + +export default ProfileImagePicker; diff --git a/src/components/onboarding/ProgressDots.tsx b/src/components/onboarding/ProgressDots.tsx new file mode 100644 index 0000000..71501a9 --- /dev/null +++ b/src/components/onboarding/ProgressDots.tsx @@ -0,0 +1,23 @@ +interface ProgressDotsProps { + total: number; + current: number; +} + +export default function ProgressDots({ total, current }: ProgressDotsProps) { + return ( +
+
+ {Array.from({ length: total }).map((_, i) => { + const isActive = i <= current; + + return ( + + ); + })} +
+
+ ); +} diff --git a/src/components/onboarding/UploadButton.tsx b/src/components/onboarding/UploadButton.tsx new file mode 100644 index 0000000..f5760d8 --- /dev/null +++ b/src/components/onboarding/UploadButton.tsx @@ -0,0 +1,116 @@ +"use client"; + +import React, { useEffect, useRef } from "react"; + +import { FullButton } from "@/components/common/FullButton"; + +interface UploadButtonProps { + isOpen: boolean; + onClose: () => void; + onSelectAlbum: (file: File) => void; + onSelectDefault: () => void; +} + +const UploadButton = ({ + isOpen, + onClose, + onSelectAlbum, + onSelectDefault, +}: UploadButtonProps) => { + const fileInputRef = useRef(null); + const modalRef = useRef(null); + + useEffect(() => { + if (!isOpen) return; + + const handleEscape = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + + document.addEventListener("keydown", handleEscape); + modalRef.current?.focus(); + + return () => { + document.removeEventListener("keydown", handleEscape); + }; + }, [isOpen, onClose]); + + if (!isOpen) return null; + + const handleAlbumClick = () => { + if (fileInputRef.current) { + fileInputRef.current.value = ""; + fileInputRef.current.click(); + } + }; + + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + onSelectAlbum(file); + e.target.value = ""; + onClose(); + }; + + return ( +
+
e.stopPropagation()} + tabIndex={-1} + > +
+ +
+ + + +
+ + + + +
+ +
+ + 취소하기 + +
+
+
+ ); +}; + +export default UploadButton; diff --git a/src/components/password/PasswordForm.tsx b/src/components/password/PasswordForm.tsx index 5a26f4f..453b786 100644 --- a/src/components/password/PasswordForm.tsx +++ b/src/components/password/PasswordForm.tsx @@ -7,8 +7,8 @@ import { useState } from "react"; import PhoneIcon from "@/assets/phone.svg"; import ProfileIcon from "@/assets/profile.svg"; -import { AlertModal } from "@/components/common/AlertModal"; import { FullButton } from "@/components/common/FullButton"; +import LoadingModal from "@/components/common/LoadingModal"; import { useCountdown } from "@/hooks/useCountdown"; import { usePhoneValidation } from "@/hooks/usePhoneValidation"; @@ -214,7 +214,7 @@ export const PasswordForm = () => {
{modalType && ( - { ? "인증이 완료되었습니다." : "인증번호를 확인할 수 없습니다." } + backdrop="default" onClose={handleModalConfirm} /> )} diff --git a/src/components/signup/ProfileForm.tsx b/src/components/signup/ProfileForm.tsx index 89aedad..36fb778 100644 --- a/src/components/signup/ProfileForm.tsx +++ b/src/components/signup/ProfileForm.tsx @@ -8,8 +8,8 @@ import DateIcon from "@/assets/date.svg"; import PhoneIcon from "@/assets/phone.svg"; import ProfileIcon from "@/assets/profile.svg"; -import { AlertModal } from "@/components/common/AlertModal"; import { FullButton } from "@/components/common/FullButton"; +import LoadingModal from "@/components/common/LoadingModal"; import { useCountdown } from "@/hooks/useCountdown"; import { usePhoneValidation } from "@/hooks/usePhoneValidation"; @@ -245,7 +245,7 @@ export const ProfileForm = () => {
{modalType && ( - = { + service: false, + finance: false, + personalRequired: false, + personalOption1: false, + personalOption2: false, + marketing: false, +}; diff --git a/src/constants/alert.ts b/src/constants/alert.ts new file mode 100644 index 0000000..99ba3d8 --- /dev/null +++ b/src/constants/alert.ts @@ -0,0 +1,7 @@ +export type AlertBackdrop = "default" | "light" | "blur"; + +export const ALERT_BACKDROP_CLASS: Record = { + default: "bg-neutral-01/30", + light: "bg-neutral-01/50", + blur: "bg-neutral-01/50 backdrop-blur-[7px]", +}; diff --git a/src/constants/onboardingSteps.ts b/src/constants/onboardingSteps.ts new file mode 100644 index 0000000..b2021b5 --- /dev/null +++ b/src/constants/onboardingSteps.ts @@ -0,0 +1,43 @@ +export const ONBOARDING_STEPS = ["전체", "커뮤니티", "정원"] as const; + +export type OnboardingStepName = (typeof ONBOARDING_STEPS)[number]; + +export type OnboardingFunnelSteps = { + 전체: object; + 커뮤니티: object; + 정원: object; +}; + +type OnboardingBackground = + | { type: "image"; src: string } + | { type: "class"; className: string }; + +export const ONBOARDING_CONTENTS: Record< + OnboardingStepName, + { + title: string; + background: OnboardingBackground; + } +> = { + 전체: { + title: "시니어와 가족이 조금 편하게\n연결될 수 있도록", + background: { + type: "image", + src: "/images/onboarding-step-1.svg", + }, + }, + 커뮤니티: { + title: "작은 순간도 반가운 소식이\n될 수 있도록", + background: { + type: "image", + src: "/images/onboarding-step-2.svg", + }, + }, + 정원: { + title: "소중한 마음을 모아 정원을\n꾸며볼까요?", + background: { + type: "image", + src: "/images/onboarding-step-3.svg", + }, + }, +}; diff --git a/src/hooks/usePasswordValidation.ts b/src/hooks/usePasswordValidation.ts index ed8a983..bc75ede 100644 --- a/src/hooks/usePasswordValidation.ts +++ b/src/hooks/usePasswordValidation.ts @@ -1,9 +1,11 @@ export const usePasswordValidation = () => { + // const isValidPassword = (pwd: string) => { + // const hasLetter = /[A-Za-z]/.test(pwd); + // const hasNumber = /\d/.test(pwd); + // const hasSpecial = /[^A-Za-z0-9]/.test(pwd); + // return pwd.length >= 8 && hasLetter && hasNumber && hasSpecial; const isValidPassword = (pwd: string) => { - const hasLetter = /[A-Za-z]/.test(pwd); - const hasNumber = /\d/.test(pwd); - const hasSpecial = /[^A-Za-z0-9]/.test(pwd); - return pwd.length >= 8 && hasLetter && hasNumber && hasSpecial; + return pwd.length >= 8; }; const getState = (value: string, isValid: boolean) => { diff --git a/src/hooks/useProfileImageUpload.ts b/src/hooks/useProfileImageUpload.ts new file mode 100644 index 0000000..ccc4285 --- /dev/null +++ b/src/hooks/useProfileImageUpload.ts @@ -0,0 +1,53 @@ +"use client"; + +import { useState } from "react"; + +//import { uploadImageFile } from "@/utils/upload"; + +export const useProfileImageUpload = () => { + const [profileImage, setProfileImage] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const uploadImage = async (file: File) => { + setIsLoading(true); + + await new Promise(res => setTimeout(res, 2000)); + + const imageUrl = URL.createObjectURL(file); + setProfileImage(imageUrl); + + setIsLoading(false); + }; + + const cancelUpload = () => { + setIsLoading(false); + }; + + // const uploadImage = async (file: File) => { + // try { + // setIsLoading(true); + // const { url } = await uploadImageFile(file); + // setProfileImage(url); + // } finally { + // setIsLoading(false); + // } + // }; + + const resetImage = () => { + setProfileImage(null); + }; + + // return { + // profileImage, + // isLoading, + // uploadImage, + // resetImage, + // }; + return { + profileImage, + isLoading, + uploadImage, + resetImage, + cancelUpload, + } as const; +}; diff --git a/src/styles/globals.css b/src/styles/globals.css index c9b4367..788865a 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -40,8 +40,6 @@ --color-neutral-10: #f3f3f4; --color-neutral-11: #f9f9fb; --color-white: #fefefe; - - --color-dim-02: #242628D4; } @layer utilities { @@ -66,13 +64,6 @@ ); } - .bg-dim02-gradient { - background: linear-gradient( - 0deg, - rgba(36, 38, 40, 0.83) 0%, - rgba(36, 38, 40, 0.83) 100% - ) - } .bg-onboarding-circle-mint { border-radius: 146px; background: linear-gradient( @@ -115,3 +106,24 @@ } } +@layer utilities{ + .loader { + width: 48px; + height: 48px; + border: 5px solid var(--color-white); + border-bottom-color: var(--color-mint-01); + border-radius: 50%; + display: inline-block; + box-sizing: border-box; + animation: rotation 1s linear infinite; + } +} +@keyframes rotation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + diff --git a/src/types/agreement.type.ts b/src/types/agreement.type.ts new file mode 100644 index 0000000..d65639e --- /dev/null +++ b/src/types/agreement.type.ts @@ -0,0 +1,7 @@ +export type AgreementKey = + | "service" // 심톡 이용약관 동의 (필수) + | "finance" // 전자금융거래 이용약관 동의 (필수) + | "personalRequired" // 개인정보 수집 이용 동의 (필수) + | "personalOption1" // 개인정보 수집 이용 동의 (선택) + | "personalOption2" // 개인정보 수집 이용 동의 (선택) + | "marketing"; // 마케팅 정보 수신 동의 (선택) diff --git a/src/utils/upload.ts b/src/utils/upload.ts new file mode 100644 index 0000000..75a0b65 --- /dev/null +++ b/src/utils/upload.ts @@ -0,0 +1,27 @@ +// export interface UploadImageResult { +// url: string; +// fileName: string; +// } + +// export const uploadImageFile = async ( +// file: File, +// ): Promise => { +// const formData = new FormData(); +// formData.append("file", file); + +// const res = await fetch("/api/uploads/image", { +// method: "POST", +// body: formData, +// }); + +// if (!res.ok) { +// throw new Error("이미지 업로드에 실패했습니다."); +// } + +// const data = (await res.json()) as { url: string; fileName: string }; + +// return { +// url: data.url, +// fileName: data.fileName, +// }; +// }; diff --git a/yarn.lock b/yarn.lock index 4a6fba9..552b8aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -847,7 +847,7 @@ "@babel/plugin-transform-modules-commonjs" "^7.27.1" "@babel/plugin-transform-typescript" "^7.28.5" -"@babel/runtime@^7.14.8", "@babel/runtime@^7.19.0", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.8", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2": +"@babel/runtime@^7.23.8", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2": version "7.28.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326" integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== @@ -1507,63 +1507,6 @@ postcss "^8.4.41" tailwindcss "4.1.18" -"@toss/assert@^1.2.2": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@toss/assert/-/assert-1.2.2.tgz#78a96586e134edd38947553b20a18bc24a692c52" - integrity sha512-qK2G1LzI2ghY0aUOsz9mFiy2v/eNlMHG5qXdSogfGFLxHqFZ6KQWJQpnb9eN+dyHYIudBVWPZhbkljqnT8R3/g== - dependencies: - "@toss/utils" "^1.6.1" - -"@toss/react@^1.8.1": - version "1.8.1" - resolved "https://registry.yarnpkg.com/@toss/react/-/react-1.8.1.tgz#d69d9e5dfed21c8f9fa791473293239e31ac5dda" - integrity sha512-jH3oo/7yctexuutj/YgQrddaK1bU2s5659dkJIXOe23bEjkY+lbhvEz2FLEhRjSo6k6ktPagpxO4AcdhCi5k5A== - dependencies: - "@babel/runtime" "^7.14.8" - "@toss/storage" "^1.4.1" - "@toss/utils" "^1.6.1" - classnames "^2.3.1" - lodash.debounce "^4.0.8" - lodash.throttle "^4.1.1" - -"@toss/storage@^1.4.1": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@toss/storage/-/storage-1.4.1.tgz#c7f03946244a35631dbd4f40a7c513c314750f86" - integrity sha512-jvnBXAQ/Fqqdt+gqYKeHYk7SzR2LX/FC50JIoVI0RldG1mDh1tebOqD7XkrZ89q77t/RwTuh60+8fjZDgend/g== - dependencies: - "@babel/runtime" "^7.19.0" - -"@toss/use-funnel@^1.4.2": - version "1.4.2" - resolved "https://registry.yarnpkg.com/@toss/use-funnel/-/use-funnel-1.4.2.tgz#08e7d681e4f200fb83159100c037fb195dd43e8e" - integrity sha512-qgfYhdoJh07D4+kyRgRE3Du5wEdecFOR9Ht3MjgFMYAJyGplNbD++hpD/FcXrGT5oa14KG0bB5bpp60KLmPkFw== - dependencies: - "@toss/assert" "^1.2.2" - "@toss/react" "^1.8.1" - "@toss/storage" "^1.4.1" - "@toss/use-query-param" "^1.3.1" - "@toss/utils" "^1.6.1" - fast-deep-equal "^3.1.3" - -"@toss/use-query-param@^1.3.1": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@toss/use-query-param/-/use-query-param-1.3.1.tgz#a9bc67692972f300b8d011e00f0af17f6fa3d7d0" - integrity sha512-GRA+6st46/88KgmP9PGx8mV9sxxkewwLMzBl25TpXPjnqHz+tiZdybVZMQ5UHj0Kzf7bZnHGF3kd5oko492vxA== - -"@toss/utility-types@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@toss/utility-types/-/utility-types-1.2.1.tgz#3c67bea23b7aaffd4b4e7c2a518773ae22a496f6" - integrity sha512-1y8s1bvmuhuMX/d6qR9mmvcgFZIKYIQqJbAIshlGArXkjk/ec67gXc5uByEV1Y7in9ZhrGNRmjD8DTH0988vpQ== - -"@toss/utils@^1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@toss/utils/-/utils-1.6.1.tgz#e1226b1274b3d7e04b5b648ef11accdc013568c3" - integrity sha512-x6m8jLKWtAmCbxTLXbgTzJ5wZyRSUQPLpR/oLJP1ZK9ytXcRf03oA46W/+78kErUkEw/yQz2L+t2xFDHSeZ6IQ== - dependencies: - "@babel/runtime" "^7.14.8" - "@toss/utility-types" "^1.2.1" - date-fns "^2.25.0" - "@trivago/prettier-plugin-sort-imports@^6.0.0": version "6.0.1" resolved "https://registry.yarnpkg.com/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-6.0.1.tgz#b71c020c4069c0b7a75953227ad6c803cea3f568" @@ -2345,11 +2288,6 @@ chrome-trace-event@^1.0.2: resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== -classnames@^2.3.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" - integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== - cli-cursor@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-5.0.0.tgz#24a4831ecf5a6b01ddeb32fb71a4b2088b0dce38" @@ -2521,13 +2459,6 @@ data-view-byte-offset@^1.0.1: es-errors "^1.3.0" is-data-view "^1.0.1" -date-fns@^2.25.0: - version "2.30.0" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" - integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== - dependencies: - "@babel/runtime" "^7.21.0" - debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -3875,11 +3806,6 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash.throttle@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" - integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ== - log-update@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/log-update/-/log-update-6.1.0.tgz#1a04ff38166f94647ae1af562f4bd6a15b1b7cd4"