From fa09d2afe3af44912fa443541189bb00ff367899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B3=A0=EB=AF=BC=EA=B7=A0?= <97932282+skyblue1232@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:22:25 +0900 Subject: [PATCH 1/9] api/login --- .../login/_components/KakaoLoginButton.tsx | 29 +++++ .../src/app/login/_components/LoginForm.tsx | 64 ++++++++++ .../login/_components/SignupLinkButton.tsx | 23 ++++ .../app/login/_constants/login.constants.ts | 11 ++ apps/customer/src/app/login/page.tsx | 113 ++++++++++-------- apps/customer/src/shared/api/api.ts | 39 ++++++ .../queries/mutation/auth/useLoginMutation.ts | 10 ++ 7 files changed, 240 insertions(+), 49 deletions(-) create mode 100644 apps/customer/src/app/login/_components/KakaoLoginButton.tsx create mode 100644 apps/customer/src/app/login/_components/LoginForm.tsx create mode 100644 apps/customer/src/app/login/_components/SignupLinkButton.tsx create mode 100644 apps/customer/src/app/login/_constants/login.constants.ts create mode 100644 apps/customer/src/shared/api/api.ts create mode 100644 apps/customer/src/shared/queries/mutation/auth/useLoginMutation.ts diff --git a/apps/customer/src/app/login/_components/KakaoLoginButton.tsx b/apps/customer/src/app/login/_components/KakaoLoginButton.tsx new file mode 100644 index 0000000..ca61d44 --- /dev/null +++ b/apps/customer/src/app/login/_components/KakaoLoginButton.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { Icon } from "@compasser/design-system"; +import { LOGIN_TEXT } from "../_constants/login.constants"; + +interface KakaoLoginButtonProps { + onClick: () => void; +} + +export default function KakaoLoginButton({ + onClick, +}: KakaoLoginButtonProps) { + return ( + + ); +} \ No newline at end of file diff --git a/apps/customer/src/app/login/_components/LoginForm.tsx b/apps/customer/src/app/login/_components/LoginForm.tsx new file mode 100644 index 0000000..b76f96c --- /dev/null +++ b/apps/customer/src/app/login/_components/LoginForm.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { Input, Button } from "@compasser/design-system"; +import type { LoginReqDTO } from "@compasser/api"; +import { LOGIN_TEXT } from "../_constants/login.constants"; + +interface LoginFormProps { + values: LoginReqDTO; + isPending: boolean; + onChangeEmail: (value: string) => void; + onChangePassword: (value: string) => void; + onSubmit: () => void; +} + +export default function LoginForm({ + values, + isPending, + onChangeEmail, + onChangePassword, + onSubmit, +}: LoginFormProps) { + return ( + <> +
+

+ {LOGIN_TEXT.emailLabel} +

+ onChangeEmail(e.target.value)} + /> +
+ +
+

+ {LOGIN_TEXT.passwordLabel} +

+ onChangePassword(e.target.value)} + /> +
+ +
+ +
+ + ); +} \ No newline at end of file diff --git a/apps/customer/src/app/login/_components/SignupLinkButton.tsx b/apps/customer/src/app/login/_components/SignupLinkButton.tsx new file mode 100644 index 0000000..7ceef5f --- /dev/null +++ b/apps/customer/src/app/login/_components/SignupLinkButton.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { LOGIN_TEXT } from "../_constants/login.constants"; + +interface SignupLinkButtonProps { + onClick: () => void; +} + +export default function SignupLinkButton({ + onClick, +}: SignupLinkButtonProps) { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/apps/customer/src/app/login/_constants/login.constants.ts b/apps/customer/src/app/login/_constants/login.constants.ts new file mode 100644 index 0000000..80aa76d --- /dev/null +++ b/apps/customer/src/app/login/_constants/login.constants.ts @@ -0,0 +1,11 @@ +export const LOGIN_TEXT = { + emailLabel: "이메일", + emailPlaceholder: "이메일을 입력해주세요", + passwordLabel: "비밀번호", + passwordPlaceholder: "비밀번호를 입력해주세요", + loginButton: "일반 로그인", + loginPendingButton: "로그인 중...", + kakaoLoginButton: "카카오 로그인", + signupButton: "회원가입", + emptyFieldMessage: "이메일과 비밀번호를 입력해주세요.", +} as const; \ No newline at end of file diff --git a/apps/customer/src/app/login/page.tsx b/apps/customer/src/app/login/page.tsx index 81b1b95..934d9d7 100644 --- a/apps/customer/src/app/login/page.tsx +++ b/apps/customer/src/app/login/page.tsx @@ -1,17 +1,70 @@ "use client"; +import { useState } from "react"; import { useRouter } from "next/navigation"; -import { Input, Button, Icon } from "@compasser/design-system"; +import type { LoginReqDTO } from "@compasser/api"; +import LoginForm from "./_components/LoginForm"; +import KakaoLoginButton from "./_components/KakaoLoginButton"; +import SignupLinkButton from "./_components/SignupLinkButton"; +import { LOGIN_TEXT } from "./_constants/login.constants"; +import { useLoginMutation } from "@/shared/queries/mutation/auth/useLoginMutation"; export default function LoginPage() { const router = useRouter(); + const loginMutation = useLoginMutation(); + + const [formValues, setFormValues] = useState({ + email: "", + password: "", + }); const handleMoveRoleSelect = () => { router.push("/role-select"); }; + const handleChangeEmail = (value: string) => { + setFormValues((prev) => ({ + ...prev, + email: value, + })); + }; + + const handleChangePassword = (value: string) => { + setFormValues((prev) => ({ + ...prev, + password: value, + })); + }; + const handleLogin = () => { - console.log("일반 로그인"); + const email = formValues.email.trim(); + const password = formValues.password.trim(); + + if (!email || !password) { + console.log(LOGIN_TEXT.emptyFieldMessage); + return; + } + + loginMutation.mutate( + { + email, + password, + }, + { + onSuccess: (response) => { + const accessToken = response.data.accessToken; + + if (typeof window !== "undefined") { + localStorage.setItem("accessToken", accessToken); + } + + router.push("/main"); + }, + onError: (error) => { + console.log("일반 로그인 실패", error); + }, + }, + ); }; const handleKakaoLogin = () => { @@ -22,57 +75,19 @@ export default function LoginPage() {
-
-

이메일

- -
- -
-

비밀번호

- -
- -
- -
+
- +
-
- -
+
diff --git a/apps/customer/src/shared/api/api.ts b/apps/customer/src/shared/api/api.ts new file mode 100644 index 0000000..f23758c --- /dev/null +++ b/apps/customer/src/shared/api/api.ts @@ -0,0 +1,39 @@ +"use client"; + +import { + createCompasserApi, + createAuthModule, + type TokenPair, + type TokenStore, +} from "@compasser/api"; + +const tokenStore: TokenStore = { + getAccessToken: () => + typeof window === "undefined" ? null : localStorage.getItem("accessToken"), + + getRefreshToken: () => + typeof window === "undefined" ? null : localStorage.getItem("refreshToken"), + + setTokens: ({ accessToken, refreshToken }: TokenPair) => { + if (typeof window === "undefined") return; + + localStorage.setItem("accessToken", accessToken); + + if (refreshToken) { + localStorage.setItem("refreshToken", refreshToken); + } + }, + + clearTokens: () => { + if (typeof window === "undefined") return; + localStorage.removeItem("accessToken"); + localStorage.removeItem("refreshToken"); + }, +}; + +export const compasserApi = createCompasserApi({ + baseURL: process.env.NEXT_PUBLIC_API_BASE_URL ?? "", + tokenStore, +}); + +export const authModule = createAuthModule(compasserApi); \ No newline at end of file diff --git a/apps/customer/src/shared/queries/mutation/auth/useLoginMutation.ts b/apps/customer/src/shared/queries/mutation/auth/useLoginMutation.ts new file mode 100644 index 0000000..a75cee9 --- /dev/null +++ b/apps/customer/src/shared/queries/mutation/auth/useLoginMutation.ts @@ -0,0 +1,10 @@ +"use client"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { authModule } from "@/shared/api/api"; + +export const useLoginMutation = () => { + const queryClient = useQueryClient(); + + return useMutation(authModule.mutations.login(queryClient)); +}; \ No newline at end of file From 17e4aeeb5c03a53370284a82ffe9ffd8e085eeec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B3=A0=EB=AF=BC=EA=B7=A0?= <97932282+skyblue1232@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:22:37 +0900 Subject: [PATCH 2/9] api/signup --- apps/customer/src/app/signup/page.tsx | 55 ++++++++++++++----- .../mutation/auth/useSignupMutation.ts | 10 ++++ 2 files changed, 52 insertions(+), 13 deletions(-) create mode 100644 apps/customer/src/shared/queries/mutation/auth/useSignupMutation.ts diff --git a/apps/customer/src/app/signup/page.tsx b/apps/customer/src/app/signup/page.tsx index b3b6207..5d5b88f 100644 --- a/apps/customer/src/app/signup/page.tsx +++ b/apps/customer/src/app/signup/page.tsx @@ -3,6 +3,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; import { Input, Button } from "@compasser/design-system"; +import { useSignupMutation } from "@/shared/queries/mutation/auth/useSignupMutation"; export default function SignupPage() { const router = useRouter(); @@ -12,29 +13,52 @@ export default function SignupPage() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [passwordConfirm, setPasswordConfirm] = useState(""); + const [submitError, setSubmitError] = useState(""); + + const { mutateAsync: signUp, isPending } = useSignupMutation(); const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - const isEmailInvalid = - email.length > 0 && !emailRegex.test(email); + const isEmailInvalid = email.length > 0 && !emailRegex.test(email); const isPasswordMismatch = password.length > 0 && passwordConfirm.length > 0 && password !== passwordConfirm; - const handleSignup = () => { - if (isEmailInvalid || isPasswordMismatch) return; + const isFormInvalid = + !name.trim() || + !nickname.trim() || + !email.trim() || + !password.trim() || + !passwordConfirm.trim() || + isEmailInvalid || + isPasswordMismatch; + + const handleSignup = async () => { + if (isFormInvalid || isPending) return; + + setSubmitError(""); - console.log("회원가입", { - name, - nickname, - email, - password, - passwordConfirm, - }); + try { + await signUp({ + memberName: name, + nickname, + email, + password, + passwordConfirm, + }); - router.push("/main"); + router.push("/login"); + } catch (error) { + console.error("회원가입 실패", error); + + setSubmitError( + error instanceof Error + ? error.message + : "회원가입 중 오류가 발생했습니다.", + ); + } }; return ( @@ -96,6 +120,10 @@ export default function SignupPage() { errorMessage="비밀번호가 일치하지 않습니다." /> + + {submitError ? ( +

{submitError}

+ ) : null} @@ -106,8 +134,9 @@ export default function SignupPage() { kind="default" variant="primary" onClick={handleSignup} + disabled={isFormInvalid || isPending} > - 회원가입 + {isPending ? "회원가입 중..." : "회원가입"} diff --git a/apps/customer/src/shared/queries/mutation/auth/useSignupMutation.ts b/apps/customer/src/shared/queries/mutation/auth/useSignupMutation.ts new file mode 100644 index 0000000..82ae4c9 --- /dev/null +++ b/apps/customer/src/shared/queries/mutation/auth/useSignupMutation.ts @@ -0,0 +1,10 @@ +"use client"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { authModule } from "@/shared/api/api"; + +export const useSignupMutation = () => { + const queryClient = useQueryClient(); + + return useMutation(authModule.mutations.signUp(queryClient)); +}; \ No newline at end of file From 5e949c49ef7e3688f77c1fbc6049a88a1fdf13af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B3=A0=EB=AF=BC=EA=B7=A0?= <97932282+skyblue1232@users.noreply.github.com> Date: Fri, 3 Apr 2026 03:11:30 +0900 Subject: [PATCH 3/9] chore/owner-data --- .../signup/register/_constants/register.ts | 62 ------------------- .../app/signup/register/_types/register.ts | 47 -------------- .../signup/register/_utils/business-hours.ts | 29 +++++++++ apps/owner/src/shared/api/api.ts | 47 ++++++++++++++ apps/owner/src/shared/utils/bank.ts | 28 +++++++++ package.json | 3 + pnpm-lock.yaml | 27 ++++++++ 7 files changed, 134 insertions(+), 109 deletions(-) delete mode 100644 apps/owner/src/app/signup/register/_constants/register.ts delete mode 100644 apps/owner/src/app/signup/register/_types/register.ts create mode 100644 apps/owner/src/app/signup/register/_utils/business-hours.ts create mode 100644 apps/owner/src/shared/api/api.ts create mode 100644 apps/owner/src/shared/utils/bank.ts diff --git a/apps/owner/src/app/signup/register/_constants/register.ts b/apps/owner/src/app/signup/register/_constants/register.ts deleted file mode 100644 index 890e3fc..0000000 --- a/apps/owner/src/app/signup/register/_constants/register.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { - BusinessHours, - RandomBoxItem, - DayKey, - BusinessHourFormValue, -} from "../_types/register"; - -export const businessHoursData: BusinessHours = { - fri: "09:00-21:00", - mon: "09:00-21:00", - sat: "10:00-18:00", - sun: "closed", - thu: "09:00-21:00", - tue: "09:00-21:00", - wed: "09:00-21:00", -}; - -export const dayLabelMap: Record = { - mon: "월", - tue: "화", - wed: "수", - thu: "목", - fri: "금", - sat: "토", - sun: "일", -}; - -export const orderedDays: (keyof BusinessHours)[] = [ - "mon", - "tue", - "wed", - "thu", - "fri", - "sat", - "sun", -]; - -export const initialRandomBoxes: RandomBoxItem[] = [ - { id: 1, name: "Level.1", quantity: 5, price: 6000, limit: 1 }, - { id: 2, name: "Level.2", quantity: 3, price: 10000, limit: 1 }, - { id: 3, name: "Level.3", quantity: 1, price: 15000, limit: 1 }, -]; - -export const tagOptions = ["카페", "베이커리", "식당"]; - -export const dayOptions: { key: DayKey; label: string }[] = [ - { key: "mon", label: "월" }, - { key: "tue", label: "화" }, - { key: "wed", label: "수" }, - { key: "thu", label: "목" }, - { key: "fri", label: "금" }, - { key: "sat", label: "토" }, - { key: "sun", label: "일" }, -]; - -export const initialBusinessHourFormValue: BusinessHourFormValue = { - openTime: "", - closeTime: "", - hasBreakTime: null, - breakStartTime: "", - breakEndTime: "", -}; \ No newline at end of file diff --git a/apps/owner/src/app/signup/register/_types/register.ts b/apps/owner/src/app/signup/register/_types/register.ts deleted file mode 100644 index c0ba67c..0000000 --- a/apps/owner/src/app/signup/register/_types/register.ts +++ /dev/null @@ -1,47 +0,0 @@ -export type BusinessHours = { - mon: string; - tue: string; - wed: string; - thu: string; - fri: string; - sat: string; - sun: string; -}; - -export type RandomBoxItem = { - id: number; - name: string; - quantity: number; - price: number; - limit: number; -}; - -export interface RandomBoxFormValue { - name: string; - quantity: number; - limit: number; - pickupStartTime: string; - pickupEndTime: string; - description: string; -} - -export type AccountType = "bank" | "holder"; - -export type DayKey = - | "mon" - | "tue" - | "wed" - | "thu" - | "fri" - | "sat" - | "sun"; - -export type BreakTimeOption = "yes" | "no"; - -export interface BusinessHourFormValue { - openTime: string; - closeTime: string; - hasBreakTime: BreakTimeOption | null; - breakStartTime: string; - breakEndTime: string; -} \ No newline at end of file diff --git a/apps/owner/src/app/signup/register/_utils/business-hours.ts b/apps/owner/src/app/signup/register/_utils/business-hours.ts new file mode 100644 index 0000000..3cd536a --- /dev/null +++ b/apps/owner/src/app/signup/register/_utils/business-hours.ts @@ -0,0 +1,29 @@ +export type DayKey = "mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun"; + +export type BusinessHoursValue = Record; + +export const EMPTY_BUSINESS_HOURS: BusinessHoursValue = { + mon: "", + tue: "", + wed: "", + thu: "", + fri: "", + sat: "", + sun: "", +}; + +export const parseBusinessHours = (value: unknown): BusinessHoursValue => { + if (!value || typeof value !== "object") return EMPTY_BUSINESS_HOURS; + + const obj = value as Partial>; + + return { + mon: typeof obj.mon === "string" ? obj.mon : "", + tue: typeof obj.tue === "string" ? obj.tue : "", + wed: typeof obj.wed === "string" ? obj.wed : "", + thu: typeof obj.thu === "string" ? obj.thu : "", + fri: typeof obj.fri === "string" ? obj.fri : "", + sat: typeof obj.sat === "string" ? obj.sat : "", + sun: typeof obj.sun === "string" ? obj.sun : "", + }; +}; \ No newline at end of file diff --git a/apps/owner/src/shared/api/api.ts b/apps/owner/src/shared/api/api.ts new file mode 100644 index 0000000..37e2264 --- /dev/null +++ b/apps/owner/src/shared/api/api.ts @@ -0,0 +1,47 @@ +"use client"; + +import { + createCompasserApi, + createAuthModule, + type TokenPair, + type TokenStore, + createOwnerModule, + createStoreModule, + createRandomBoxModule, + createStoreImageModule, +} from "@compasser/api"; + +const tokenStore: TokenStore = { + getAccessToken: () => + typeof window === "undefined" ? null : localStorage.getItem("ownerAccessToken"), + + getRefreshToken: () => + typeof window === "undefined" ? null : localStorage.getItem("ownerRefreshToken"), + + setTokens: ({ accessToken, refreshToken }: TokenPair) => { + if (typeof window === "undefined") return; + + localStorage.setItem("ownerAccessToken", accessToken); + + if (refreshToken) { + localStorage.setItem("ownerRefreshToken", refreshToken); + } + }, + + clearTokens: () => { + if (typeof window === "undefined") return; + localStorage.removeItem("ownerAccessToken"); + localStorage.removeItem("ownerRefreshToken"); + }, +}; + +export const compasserApi = createCompasserApi({ + baseURL: process.env.NEXT_PUBLIC_API_BASE_URL ?? "", + tokenStore, +}); + +export const authModule = createAuthModule(compasserApi); +export const ownerModule = createOwnerModule(compasserApi); +export const storeModule = createStoreModule(compasserApi); +export const randomBoxModule = createRandomBoxModule(compasserApi); +export const storeImageModule = createStoreImageModule(compasserApi); \ No newline at end of file diff --git a/apps/owner/src/shared/utils/bank.ts b/apps/owner/src/shared/utils/bank.ts new file mode 100644 index 0000000..209edc4 --- /dev/null +++ b/apps/owner/src/shared/utils/bank.ts @@ -0,0 +1,28 @@ +export const bankNameOptions = [ + "국민은행", + "신한은행", + "우리은행", + "하나은행", + "농협은행", + "기업은행", + "카카오뱅크", + "토스뱅크", + "케이뱅크", + "새마을금고", + "수협은행", + "부산은행", + "대구은행", + "광주은행", + "전북은행", + "경남은행", +] as const; + +export const filterBankNames = (keyword: string) => { + const normalized = keyword.trim(); + if (!normalized) return bankNameOptions; + return bankNameOptions.filter((bank) => bank.includes(normalized)); +}; + +export const normalizeAccountNumber = (value: string) => { + return value.replace(/[^\d-]/g, ""); +}; \ No newline at end of file diff --git a/package.json b/package.json index 48c2603..db1d1af 100644 --- a/package.json +++ b/package.json @@ -22,5 +22,8 @@ "turbo": "^2.0.0", "typescript": "^5", "typescript-eslint": "^8" + }, + "dependencies": { + "zustand": "^5.0.12" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04cf6ad..d314d93 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,10 @@ settings: importers: .: + dependencies: + zustand: + specifier: ^5.0.12 + version: 5.0.12(@types/react@19.2.14)(react@19.2.3) devDependencies: '@eslint/js': specifier: ^9 @@ -3127,6 +3131,24 @@ packages: zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zustand@5.0.12: + resolution: {integrity: sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@adobe/css-tools@4.4.4': {} @@ -6175,3 +6197,8 @@ snapshots: zod: 4.3.6 zod@4.3.6: {} + + zustand@5.0.12(@types/react@19.2.14)(react@19.2.3): + optionalDependencies: + '@types/react': 19.2.14 + react: 19.2.3 From 08935fc7244975c3a78d406590fee4ec8c6089f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B3=A0=EB=AF=BC=EA=B7=A0?= <97932282+skyblue1232@users.noreply.github.com> Date: Fri, 3 Apr 2026 03:13:00 +0900 Subject: [PATCH 4/9] feat/business-page --- apps/owner/src/app/signup/business/page.tsx | 89 ++++++++++++++++++- .../auth/useVerifyBusinessMutation.ts | 12 +++ .../src/shared/stores/ownerSignup.store.ts | 65 ++++++++++++++ .../owner/src/shared/utils/businessLicense.ts | 29 ++++++ 4 files changed, 192 insertions(+), 3 deletions(-) create mode 100644 apps/owner/src/shared/queries/mutation/auth/useVerifyBusinessMutation.ts create mode 100644 apps/owner/src/shared/stores/ownerSignup.store.ts create mode 100644 apps/owner/src/shared/utils/businessLicense.ts diff --git a/apps/owner/src/app/signup/business/page.tsx b/apps/owner/src/app/signup/business/page.tsx index d970e5e..f9974b3 100644 --- a/apps/owner/src/app/signup/business/page.tsx +++ b/apps/owner/src/app/signup/business/page.tsx @@ -1,13 +1,90 @@ "use client"; +import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { Input, Button } from "@compasser/design-system"; +import { useVerifyBusinessMutation } from "@/shared/queries/mutation/auth/useVerifyBusinessMutation"; +import { useOwnerSignupStore } from "@/shared/stores/ownerSignup.store"; +import { + normalizeBusinessNumber, + isValidBusinessNumberFormat, + isValidBusinessNumber, +} from "@/shared/utils/businessLicense"; export default function BusinessSignupPage() { const router = useRouter(); + const verifyMutation = useVerifyBusinessMutation(); + + const signupCompleted = useOwnerSignupStore((s) => s.signupCompleted); + const email = useOwnerSignupStore((s) => s.email); + const setBusinessCompleted = useOwnerSignupStore( + (s) => s.setBusinessCompleted, + ); + + const [value, setValue] = useState(""); + const [error, setError] = useState(""); + + useEffect(() => { + if (!signupCompleted) { + router.replace("/signup"); + } + }, [signupCompleted, router]); + + const handleChange = (v: string) => { + const normalized = normalizeBusinessNumber(v); + + if (normalized.length > 10) return; + + setValue(normalized); + + if (error) setError(""); + }; + + const validate = () => { + if (!value) return "사업자 번호를 입력해주세요."; + + if (!isValidBusinessNumberFormat(value)) + return "사업자 번호는 10자리 숫자입니다."; + + if (!isValidBusinessNumber(value)) + return "유효하지 않은 사업자 번호입니다."; + + return ""; + }; const handleNext = () => { - router.push("/signup/register"); + const message = validate(); + + if (message) { + setError(message); + return; + } + + if (!email) { + router.replace("/signup"); + return; + } + + verifyMutation.mutate( + { + businessLicenseNumber: value, + email, + }, + { + onSuccess: (res) => { + if (res.alreadyUpgraded) { + setError("이미 사업자 등록이 완료된 계정입니다."); + return; + } + + setBusinessCompleted(); + router.push("/signup/register"); + }, + onError: () => { + setError("사업자 번호 인증에 실패했습니다."); + }, + }, + ); }; return ( @@ -18,9 +95,15 @@ export default function BusinessSignupPage() {

사업자 등록번호를 입력해주세요!

+ handleChange(e.target.value)} + error={!!error} + errorMessage={error} /> @@ -30,11 +113,11 @@ export default function BusinessSignupPage() { diff --git a/apps/owner/src/shared/queries/mutation/auth/useVerifyBusinessMutation.ts b/apps/owner/src/shared/queries/mutation/auth/useVerifyBusinessMutation.ts new file mode 100644 index 0000000..7112468 --- /dev/null +++ b/apps/owner/src/shared/queries/mutation/auth/useVerifyBusinessMutation.ts @@ -0,0 +1,12 @@ +"use client"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { ownerModule } from "@/shared/api/api"; + +export const useVerifyBusinessMutation = () => { + const queryClient = useQueryClient(); + + return useMutation( + ownerModule.mutations.verifyBizAndUpgrade(queryClient), + ); +}; \ No newline at end of file diff --git a/apps/owner/src/shared/stores/ownerSignup.store.ts b/apps/owner/src/shared/stores/ownerSignup.store.ts new file mode 100644 index 0000000..f98b39d --- /dev/null +++ b/apps/owner/src/shared/stores/ownerSignup.store.ts @@ -0,0 +1,65 @@ +"use client"; + +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +type OwnerSignupStep = "signup" | "business" | "register" | "done"; + +interface OwnerSignupState { + step: OwnerSignupStep; + signupCompleted: boolean; + businessCompleted: boolean; + registerCompleted: boolean; + email: string | null; + setSignupCompleted: (email: string) => void; + setBusinessCompleted: () => void; + setRegisterCompleted: () => void; + resetSignupFlow: () => void; +} + +export const useOwnerSignupStore = create()( + persist( + (set) => ({ + step: "signup", + signupCompleted: false, + businessCompleted: false, + registerCompleted: false, + email: null, + + setSignupCompleted: (email) => + set({ + step: "business", + signupCompleted: true, + businessCompleted: false, + registerCompleted: false, + email, + }), + + setBusinessCompleted: () => + set((state) => ({ + ...state, + step: "register", + businessCompleted: true, + })), + + setRegisterCompleted: () => + set((state) => ({ + ...state, + step: "done", + registerCompleted: true, + })), + + resetSignupFlow: () => + set({ + step: "signup", + signupCompleted: false, + businessCompleted: false, + registerCompleted: false, + email: null, + }), + }), + { + name: "owner-signup-flow", + }, + ), +); \ No newline at end of file diff --git a/apps/owner/src/shared/utils/businessLicense.ts b/apps/owner/src/shared/utils/businessLicense.ts new file mode 100644 index 0000000..4c8b948 --- /dev/null +++ b/apps/owner/src/shared/utils/businessLicense.ts @@ -0,0 +1,29 @@ +export const normalizeBusinessNumber = (value: string) => { + return value.replace(/\D/g, ""); +}; + +export const isValidBusinessNumberFormat = (value: string) => { + return /^\d{10}$/.test(value); +}; + +// 체크섬 검증 +export const isValidBusinessNumber = (value: string) => { + const num = normalizeBusinessNumber(value); + + if (!/^\d{10}$/.test(num)) return false; + + const digits = num.split("").map(Number); + const weights = [1, 3, 7, 1, 3, 7, 1, 3, 5]; + + let sum = 0; + + for (let i = 0; i < 9; i++) { + sum += digits[i] * weights[i]; + } + + sum += Math.floor((digits[8] * 5) / 10); + + const check = (10 - (sum % 10)) % 10; + + return check === digits[9]; +}; \ No newline at end of file From 52b07f8e81f89ea3c26467a96cf190f07105e13d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B3=A0=EB=AF=BC=EA=B7=A0?= <97932282+skyblue1232@users.noreply.github.com> Date: Fri, 3 Apr 2026 03:13:39 +0900 Subject: [PATCH 5/9] feat/signup-api --- apps/owner/src/app/signup/page.tsx | 100 ++++++++++++++++-- .../mutation/auth/useSignupMutation.ts | 10 ++ 2 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 apps/owner/src/shared/queries/mutation/auth/useSignupMutation.ts diff --git a/apps/owner/src/app/signup/page.tsx b/apps/owner/src/app/signup/page.tsx index b3683b3..9d25c2d 100644 --- a/apps/owner/src/app/signup/page.tsx +++ b/apps/owner/src/app/signup/page.tsx @@ -1,13 +1,73 @@ "use client"; +import { useState } from "react"; import { Input, Button } from "@compasser/design-system"; import { useRouter } from "next/navigation"; +import type { JoinReqDTO } from "@compasser/api"; +import { useSignupMutation } from "@/shared/queries/mutation/auth/useSignupMutation"; +import { useOwnerSignupStore } from "@/shared/stores/ownerSignup.store"; export default function SignupPage() { const router = useRouter(); + const signupMutation = useSignupMutation(); + const setSignupCompleted = useOwnerSignupStore( + (state) => state.setSignupCompleted, + ); + + const [formValues, setFormValues] = useState({ + memberName: "", + nickname: "", + email: "", + password: "", + passwordConfirm: "", + }); + + const handleChange = + (key: keyof JoinReqDTO) => (e: React.ChangeEvent) => { + const value = e.target.value; + + setFormValues((prev) => ({ + ...prev, + [key]: value, + })); + }; const handleSignup = () => { - router.push("/signup/business"); + const memberName = formValues.memberName.trim(); + const nickname = formValues.nickname.trim(); + const email = formValues.email.trim(); + const password = formValues.password.trim(); + const passwordConfirm = formValues.passwordConfirm.trim(); + + if ( + !memberName || + !nickname || + !email || + !password || + !passwordConfirm + ) { + console.log("모든 항목을 입력해주세요."); + return; + } + + signupMutation.mutate( + { + memberName, + nickname, + email, + password, + passwordConfirm, + }, + { + onSuccess: () => { + setSignupCompleted(email); + router.push("/signup/business"); + }, + onError: (error) => { + console.log("회원가입 실패", error); + }, + }, + ); }; return ( @@ -16,27 +76,52 @@ export default function SignupPage() {

이름

- +

닉네임

- +

이메일

- +

비밀번호

- +

비밀번호 확인

- +
@@ -48,8 +133,9 @@ export default function SignupPage() { kind="default" variant="primary" onClick={handleSignup} + disabled={signupMutation.isPending} > - 회원가입 + {signupMutation.isPending ? "회원가입 중..." : "회원가입"} diff --git a/apps/owner/src/shared/queries/mutation/auth/useSignupMutation.ts b/apps/owner/src/shared/queries/mutation/auth/useSignupMutation.ts new file mode 100644 index 0000000..82ae4c9 --- /dev/null +++ b/apps/owner/src/shared/queries/mutation/auth/useSignupMutation.ts @@ -0,0 +1,10 @@ +"use client"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { authModule } from "@/shared/api/api"; + +export const useSignupMutation = () => { + const queryClient = useQueryClient(); + + return useMutation(authModule.mutations.signUp(queryClient)); +}; \ No newline at end of file From e69383bd787f38977e8560062e4d8ba92ea124ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B3=A0=EB=AF=BC=EA=B7=A0?= <97932282+skyblue1232@users.noreply.github.com> Date: Fri, 3 Apr 2026 03:13:57 +0900 Subject: [PATCH 6/9] feat/register-hook --- .../mutation/auth/useCreateRandomBoxMutation.ts | 9 +++++++++ .../mutation/auth/useDeleteRandomBoxMutation.ts | 9 +++++++++ .../mutation/auth/usePatchMyStoreLocationMutation.ts | 9 +++++++++ .../queries/mutation/auth/usePatchMyStoreMutation.ts | 9 +++++++++ .../mutation/auth/useRemoveStoreImageMutation.ts | 9 +++++++++ .../mutation/auth/useUpdateRandomBoxMutation.ts | 9 +++++++++ .../mutation/auth/useUploadStoreImageMutation.ts | 9 +++++++++ .../owner/src/shared/queries/query/useMyStoreQuery.ts | 8 ++++++++ .../src/shared/queries/query/useRandomBoxListQuery.ts | 11 +++++++++++ .../src/shared/queries/query/useStoreImageQuery.ts | 8 ++++++++ 10 files changed, 90 insertions(+) create mode 100644 apps/owner/src/shared/queries/mutation/auth/useCreateRandomBoxMutation.ts create mode 100644 apps/owner/src/shared/queries/mutation/auth/useDeleteRandomBoxMutation.ts create mode 100644 apps/owner/src/shared/queries/mutation/auth/usePatchMyStoreLocationMutation.ts create mode 100644 apps/owner/src/shared/queries/mutation/auth/usePatchMyStoreMutation.ts create mode 100644 apps/owner/src/shared/queries/mutation/auth/useRemoveStoreImageMutation.ts create mode 100644 apps/owner/src/shared/queries/mutation/auth/useUpdateRandomBoxMutation.ts create mode 100644 apps/owner/src/shared/queries/mutation/auth/useUploadStoreImageMutation.ts create mode 100644 apps/owner/src/shared/queries/query/useMyStoreQuery.ts create mode 100644 apps/owner/src/shared/queries/query/useRandomBoxListQuery.ts create mode 100644 apps/owner/src/shared/queries/query/useStoreImageQuery.ts diff --git a/apps/owner/src/shared/queries/mutation/auth/useCreateRandomBoxMutation.ts b/apps/owner/src/shared/queries/mutation/auth/useCreateRandomBoxMutation.ts new file mode 100644 index 0000000..f20982c --- /dev/null +++ b/apps/owner/src/shared/queries/mutation/auth/useCreateRandomBoxMutation.ts @@ -0,0 +1,9 @@ +"use client"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { randomBoxModule } from "@/shared/api/api"; + +export const useCreateRandomBoxMutation = () => { + const queryClient = useQueryClient(); + return useMutation(randomBoxModule.mutations.create(queryClient)); +}; \ No newline at end of file diff --git a/apps/owner/src/shared/queries/mutation/auth/useDeleteRandomBoxMutation.ts b/apps/owner/src/shared/queries/mutation/auth/useDeleteRandomBoxMutation.ts new file mode 100644 index 0000000..01dcf75 --- /dev/null +++ b/apps/owner/src/shared/queries/mutation/auth/useDeleteRandomBoxMutation.ts @@ -0,0 +1,9 @@ +"use client"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { randomBoxModule } from "@/shared/api/api"; + +export const useDeleteRandomBoxMutation = () => { + const queryClient = useQueryClient(); + return useMutation(randomBoxModule.mutations.remove(queryClient)); +}; \ No newline at end of file diff --git a/apps/owner/src/shared/queries/mutation/auth/usePatchMyStoreLocationMutation.ts b/apps/owner/src/shared/queries/mutation/auth/usePatchMyStoreLocationMutation.ts new file mode 100644 index 0000000..9e98019 --- /dev/null +++ b/apps/owner/src/shared/queries/mutation/auth/usePatchMyStoreLocationMutation.ts @@ -0,0 +1,9 @@ +"use client"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { storeModule } from "@/shared/api/api"; + +export const usePatchMyStoreLocationMutation = () => { + const queryClient = useQueryClient(); + return useMutation(storeModule.mutations.patchMyStoreLocation(queryClient)); +}; \ No newline at end of file diff --git a/apps/owner/src/shared/queries/mutation/auth/usePatchMyStoreMutation.ts b/apps/owner/src/shared/queries/mutation/auth/usePatchMyStoreMutation.ts new file mode 100644 index 0000000..395f9be --- /dev/null +++ b/apps/owner/src/shared/queries/mutation/auth/usePatchMyStoreMutation.ts @@ -0,0 +1,9 @@ +"use client"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { storeModule } from "@/shared/api/api"; + +export const usePatchMyStoreMutation = () => { + const queryClient = useQueryClient(); + return useMutation(storeModule.mutations.patchMyStore(queryClient)); +}; \ No newline at end of file diff --git a/apps/owner/src/shared/queries/mutation/auth/useRemoveStoreImageMutation.ts b/apps/owner/src/shared/queries/mutation/auth/useRemoveStoreImageMutation.ts new file mode 100644 index 0000000..5725c14 --- /dev/null +++ b/apps/owner/src/shared/queries/mutation/auth/useRemoveStoreImageMutation.ts @@ -0,0 +1,9 @@ +"use client"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { storeImageModule } from "@/shared/api/api"; + +export const useRemoveStoreImageMutation = () => { + const queryClient = useQueryClient(); + return useMutation(storeImageModule.mutations.remove(queryClient)); +}; \ No newline at end of file diff --git a/apps/owner/src/shared/queries/mutation/auth/useUpdateRandomBoxMutation.ts b/apps/owner/src/shared/queries/mutation/auth/useUpdateRandomBoxMutation.ts new file mode 100644 index 0000000..189959f --- /dev/null +++ b/apps/owner/src/shared/queries/mutation/auth/useUpdateRandomBoxMutation.ts @@ -0,0 +1,9 @@ +"use client"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { randomBoxModule } from "@/shared/api/api"; + +export const useUpdateRandomBoxMutation = () => { + const queryClient = useQueryClient(); + return useMutation(randomBoxModule.mutations.update(queryClient)); +}; \ No newline at end of file diff --git a/apps/owner/src/shared/queries/mutation/auth/useUploadStoreImageMutation.ts b/apps/owner/src/shared/queries/mutation/auth/useUploadStoreImageMutation.ts new file mode 100644 index 0000000..9021e19 --- /dev/null +++ b/apps/owner/src/shared/queries/mutation/auth/useUploadStoreImageMutation.ts @@ -0,0 +1,9 @@ +"use client"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { storeImageModule } from "@/shared/api/api"; + +export const useUploadStoreImageMutation = () => { + const queryClient = useQueryClient(); + return useMutation(storeImageModule.mutations.upload(queryClient)); +}; \ No newline at end of file diff --git a/apps/owner/src/shared/queries/query/useMyStoreQuery.ts b/apps/owner/src/shared/queries/query/useMyStoreQuery.ts new file mode 100644 index 0000000..cbd0da5 --- /dev/null +++ b/apps/owner/src/shared/queries/query/useMyStoreQuery.ts @@ -0,0 +1,8 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { storeModule } from "@/shared/api/api"; + +export const useMyStoreQuery = () => { + return useQuery(storeModule.queries.myStore()); +}; \ No newline at end of file diff --git a/apps/owner/src/shared/queries/query/useRandomBoxListQuery.ts b/apps/owner/src/shared/queries/query/useRandomBoxListQuery.ts new file mode 100644 index 0000000..de7b45c --- /dev/null +++ b/apps/owner/src/shared/queries/query/useRandomBoxListQuery.ts @@ -0,0 +1,11 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { randomBoxModule } from "@/shared/api/api"; + +export const useRandomBoxListQuery = (storeId?: number) => { + return useQuery({ + ...randomBoxModule.queries.list({ storeId: storeId ?? 0 }), + enabled: Boolean(storeId), + }); +}; \ No newline at end of file diff --git a/apps/owner/src/shared/queries/query/useStoreImageQuery.ts b/apps/owner/src/shared/queries/query/useStoreImageQuery.ts new file mode 100644 index 0000000..e01ca55 --- /dev/null +++ b/apps/owner/src/shared/queries/query/useStoreImageQuery.ts @@ -0,0 +1,8 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { storeImageModule } from "@/shared/api/api"; + +export const useStoreImageQuery = () => { + return useQuery(storeImageModule.queries.get()); +}; \ No newline at end of file From b5a3c92268286e4352ca15ae4faedf14c349b91c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B3=A0=EB=AF=BC=EA=B7=A0?= <97932282+skyblue1232@users.noreply.github.com> Date: Fri, 3 Apr 2026 03:14:14 +0900 Subject: [PATCH 7/9] refactor/signup-register --- .../_components/fields/AccountField.tsx | 125 ++++++-- .../_components/fields/EmailField.tsx | 17 +- .../_components/fields/StoreAddressField.tsx | 11 +- .../_components/fields/StoreNameField.tsx | 17 +- .../_components/modals/BusinessHoursModal.tsx | 159 +++++++---- .../_components/modals/RandomBoxModal.tsx | 124 ++++---- .../sections/PhotoUploadSection.tsx | 73 +++-- .../_components/sections/RandomBoxSection.tsx | 16 +- .../_components/sections/TagSection.tsx | 16 +- apps/owner/src/app/signup/register/page.tsx | 266 ++++++++++++------ 10 files changed, 552 insertions(+), 272 deletions(-) diff --git a/apps/owner/src/app/signup/register/_components/fields/AccountField.tsx b/apps/owner/src/app/signup/register/_components/fields/AccountField.tsx index 8a2cc1d..dfccac5 100644 --- a/apps/owner/src/app/signup/register/_components/fields/AccountField.tsx +++ b/apps/owner/src/app/signup/register/_components/fields/AccountField.tsx @@ -1,42 +1,113 @@ "use client"; +import { useMemo, useState } from "react"; import { Input, Tag } from "@compasser/design-system"; -import type { AccountType } from "../../_types/register"; +import { + filterBankNames, + normalizeAccountNumber, +} from "@/shared/utils/bank"; interface AccountFieldProps { - selectedAccountType: AccountType | null; - onSelectAccountType: (type: AccountType) => void; + bankName: string; + depositor: string; + bankAccount: string; + onChangeBankName: (value: string) => void; + onChangeDepositor: (value: string) => void; + onChangeBankAccount: (value: string) => void; } export default function AccountField({ - selectedAccountType, - onSelectAccountType, + bankName, + depositor, + bankAccount, + onChangeBankName, + onChangeDepositor, + onChangeBankAccount, }: AccountFieldProps) { + const [bankKeyword, setBankKeyword] = useState(""); + + const filteredBanks = useMemo(() => { + return filterBankNames(bankKeyword); + }, [bankKeyword]); + + const handleChangeBankName = (nextValue: string) => { + onChangeBankName(nextValue); + setBankKeyword(nextValue); + }; + + const handleChangeBankAccount = (nextValue: string) => { + onChangeBankAccount(normalizeAccountNumber(nextValue)); + }; + return (
-

계좌번호

- -
- onSelectAccountType("bank")} - > - 은행 - - - onSelectAccountType("holder")} - > - 예금주명 - -
+

계좌 정보

+ +
+
+
+ + 은행 + +
- + handleChangeBankName(e.target.value)} + /> + + {bankKeyword.trim() !== "" && ( +
+ {filteredBanks.map((bank) => ( + { + onChangeBankName(bank); + setBankKeyword(bank); + }} + > + {bank} + + ))} +
+ )} +
+ +
+
+ + 예금주명 + +
+ + onChangeDepositor(e.target.value)} + /> +
+ +
+
+ + 계좌번호 + +
+ + handleChangeBankAccount(e.target.value)} + /> +
+
); } \ No newline at end of file diff --git a/apps/owner/src/app/signup/register/_components/fields/EmailField.tsx b/apps/owner/src/app/signup/register/_components/fields/EmailField.tsx index 13cd172..4be028b 100644 --- a/apps/owner/src/app/signup/register/_components/fields/EmailField.tsx +++ b/apps/owner/src/app/signup/register/_components/fields/EmailField.tsx @@ -2,11 +2,24 @@ import { Input } from "@compasser/design-system"; -export default function EmailField() { +interface EmailFieldProps { + value: string; + onChange: (value: string) => void; +} + +export default function EmailField({ + value, + onChange, +}: EmailFieldProps) { return (

이메일

- + onChange(e.target.value)} + />
); } \ No newline at end of file diff --git a/apps/owner/src/app/signup/register/_components/fields/StoreAddressField.tsx b/apps/owner/src/app/signup/register/_components/fields/StoreAddressField.tsx index cca21ce..4fecd15 100644 --- a/apps/owner/src/app/signup/register/_components/fields/StoreAddressField.tsx +++ b/apps/owner/src/app/signup/register/_components/fields/StoreAddressField.tsx @@ -3,10 +3,14 @@ import { Button, Input } from "@compasser/design-system"; interface StoreAddressFieldProps { + value: string; + onChange: (value: string) => void; onSearchAddress: () => void; } export default function StoreAddressField({ + value, + onChange, onSearchAddress, }: StoreAddressFieldProps) { return ( @@ -15,7 +19,12 @@ export default function StoreAddressField({
- + onChange(e.target.value)} + />
); })} @@ -167,8 +228,12 @@ export default function BusinessHoursModal({ updateSelectedDayField("openTime", value)} - onChangeEndTime={(value) => updateSelectedDayField("closeTime", value)} + onChangeStartTime={(value) => + updateSelectedDayField("openTime", value) + } + onChangeEndTime={(value) => + updateSelectedDayField("closeTime", value) + } className="ml-[5.2rem]" />
@@ -185,7 +250,7 @@ export default function BusinessHoursModal({ "flex items-center justify-center rounded-[10px] border px-[1.2rem] py-[1rem]", selectedDayValue.hasBreakTime === "yes" ? "border-primary-variant bg-primary-variant" - : "border-gray-300 bg-white" + : "border-gray-300 bg-white", )} > 유 @@ -207,7 +272,7 @@ export default function BusinessHoursModal({ "flex items-center justify-center rounded-[10px] border px-[1.2rem] py-[1rem]", selectedDayValue.hasBreakTime === "no" ? "border-primary-variant bg-primary-variant" - : "border-gray-300 bg-white" + : "border-gray-300 bg-white", )} > 무 diff --git a/apps/owner/src/app/signup/register/_components/modals/RandomBoxModal.tsx b/apps/owner/src/app/signup/register/_components/modals/RandomBoxModal.tsx index 0664c0d..454caca 100644 --- a/apps/owner/src/app/signup/register/_components/modals/RandomBoxModal.tsx +++ b/apps/owner/src/app/signup/register/_components/modals/RandomBoxModal.tsx @@ -2,41 +2,56 @@ import { useEffect, useState } from "react"; import { Button, Input, Modal, cn } from "@compasser/design-system"; -import TimeRangeField from "./TimeRangeField"; -import CountStepper from "./CountStepper"; -import type { RandomBoxFormValue } from "../../_types/register"; + +interface RandomBoxFormValue { + boxName: string; + stock: number; + price: number; + buyLimit: number; + content: string; + boxId?: number; +} interface RandomBoxModalProps { open: boolean; onClose: () => void; - onSubmit?: (data: RandomBoxFormValue) => void; + onSubmit?: (data: RandomBoxFormValue) => void | Promise; + initialValue?: RandomBoxFormValue | null; } const initialFormValue: RandomBoxFormValue = { - name: "", - quantity: 0, - limit: 0, - pickupStartTime: "00:00", - pickupEndTime: "00:00", - description: "", + boxName: "", + stock: 0, + price: 0, + buyLimit: 0, + content: "", }; export default function RandomBoxModal({ open, onClose, onSubmit, + initialValue, }: RandomBoxModalProps) { const [form, setForm] = useState(initialFormValue); useEffect(() => { if (!open) { setForm(initialFormValue); + return; + } + + if (initialValue) { + setForm(initialValue); + return; } - }, [open]); + + setForm(initialFormValue); + }, [open, initialValue]); const updateField = ( key: K, - value: RandomBoxFormValue[K] + value: RandomBoxFormValue[K], ) => { setForm((prev) => ({ ...prev, @@ -44,8 +59,13 @@ export default function RandomBoxModal({ })); }; - const handleSubmit = () => { - onSubmit?.(form); + const handleSubmit = async () => { + if (!form.boxName.trim()) return; + if (form.stock <= 0) return; + if (form.price < 0) return; + if (form.buyLimit <= 0) return; + + await onSubmit?.(form); onClose(); }; @@ -63,60 +83,68 @@ export default function RandomBoxModal({

랜덤박스 이름

updateField("name", event.target.value)} + value={form.boxName} + onChange={(event) => updateField("boxName", event.target.value)} className="w-[17rem]" containerClassName="border-gray-300" inputClassName="text-gray-600" /> -
+

총 수량

-
- updateField("quantity", value)} - min={0} - /> -
+ + updateField("stock", Number(event.target.value || 0)) + } + className="w-[17rem]" + containerClassName="border-gray-300" + inputClassName="text-gray-600" + />
-
-

구매 제한 개수

+
+

가격

-
- updateField("limit", value)} - min={0} - /> -
+ + updateField("price", Number(event.target.value || 0)) + } + className="w-[17rem]" + containerClassName="border-gray-300" + inputClassName="text-gray-600" + />
-
-

픽업 시간

- -
- updateField("pickupStartTime", value)} - onChangeEndTime={(value) => updateField("pickupEndTime", value)} - /> -
+
+

구매 제한 개수

+ + + updateField("buyLimit", Number(event.target.value || 0)) + } + className="w-[17rem]" + containerClassName="border-gray-300" + inputClassName="text-gray-600" + />

랜덤박스 설명란