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 (
);
}
\ 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)}
+ />
-
+
총 수량
-
- 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"
+ />
@@ -129,7 +157,7 @@ export default function RandomBoxModal({
kind="default"
onClick={handleSubmit}
>
- 추가하기
+ {form.boxId ? "수정하기" : "추가하기"}
diff --git a/apps/owner/src/app/signup/register/_components/sections/PhotoUploadSection.tsx b/apps/owner/src/app/signup/register/_components/sections/PhotoUploadSection.tsx
index e744560..f8f2df6 100644
--- a/apps/owner/src/app/signup/register/_components/sections/PhotoUploadSection.tsx
+++ b/apps/owner/src/app/signup/register/_components/sections/PhotoUploadSection.tsx
@@ -1,64 +1,57 @@
"use client";
-import { ChangeEvent, useRef } from "react";
-import Image from "next/image";
-import { Icon } from "@compasser/design-system";
-
interface PhotoUploadSectionProps {
- previewUrl?: string;
+ previewUrl: string;
onChangePhoto: (file: File) => void;
+ onRemovePhoto: () => void | Promise
;
}
export default function PhotoUploadSection({
previewUrl,
onChangePhoto,
+ onRemovePhoto,
}: PhotoUploadSectionProps) {
- const fileInputRef = useRef(null);
-
- const handleOpenFilePicker = () => {
- fileInputRef.current?.click();
- };
-
- const handleChangeFile = (event: ChangeEvent) => {
- const file = event.target.files?.[0];
+ const handleChangeFile = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
if (!file) return;
-
onChangePhoto(file);
-
- event.target.value = "";
};
return (
사진 첨부
-
- 상점의 대표 이미지를 추가해주세요!
-
+
상점의 대표이미지를 추가해주세요!
-
+
{previewUrl ? (
-
+
+

+
+
+ ×
+
+
) : (
-
+
)}
-
-
-
+
);
}
\ No newline at end of file
diff --git a/apps/owner/src/app/signup/register/_components/sections/RandomBoxSection.tsx b/apps/owner/src/app/signup/register/_components/sections/RandomBoxSection.tsx
index 57b3957..225a64e 100644
--- a/apps/owner/src/app/signup/register/_components/sections/RandomBoxSection.tsx
+++ b/apps/owner/src/app/signup/register/_components/sections/RandomBoxSection.tsx
@@ -1,10 +1,10 @@
"use client";
import { Button, Card } from "@compasser/design-system";
-import type { RandomBoxItem } from "../../_types/register";
+import type { RandomBoxRespDTO } from "@compasser/api";
interface RandomBoxSectionProps {
- randomBoxes: RandomBoxItem[];
+ randomBoxes: RandomBoxRespDTO[];
selectedRandomBoxIds: number[];
onToggleRandomBoxSelection: (id: number) => void;
onDeleteRandomBoxes: () => void;
@@ -65,23 +65,23 @@ export default function RandomBoxSection({
}
>
{randomBoxes.map((item) => {
- const isSelected = selectedRandomBoxIds.includes(item.id);
+ const isSelected = selectedRandomBoxIds.includes(item.boxId);
return (
onToggleRandomBoxSelection(item.id)}
+ onClick={() => onToggleRandomBoxSelection(item.boxId)}
className={`grid w-full grid-cols-4 px-[1rem] py-[1rem] text-center ${
isSelected ? "bg-background" : "bg-white"
}`}
>
- {item.name}
- {item.quantity}
+ {item.boxName}
+ {item.stock}
{item.price.toLocaleString()}
- {item.limit}
+ {item.buyLimit}
);
})}
diff --git a/apps/owner/src/app/signup/register/_components/sections/TagSection.tsx b/apps/owner/src/app/signup/register/_components/sections/TagSection.tsx
index e00296c..e6a4fb7 100644
--- a/apps/owner/src/app/signup/register/_components/sections/TagSection.tsx
+++ b/apps/owner/src/app/signup/register/_components/sections/TagSection.tsx
@@ -2,16 +2,18 @@
import { Tag } from "@compasser/design-system";
+type FixedTag = "카페" | "베이커리" | "식당";
+
interface TagSectionProps {
- tagOptions: string[];
- selectedTags: string[];
- onToggleTag: (tag: string) => void;
+ tagOptions: FixedTag[];
+ selectedTag: FixedTag | "";
+ onSelectTag: (tag: FixedTag) => void;
}
export default function TagSection({
tagOptions,
- selectedTags,
- onToggleTag,
+ selectedTag,
+ onSelectTag,
}: TagSectionProps) {
return (
@@ -23,8 +25,8 @@ export default function TagSection({
onToggleTag(tag)}
+ selected={selectedTag === tag}
+ onClick={() => onSelectTag(tag)}
>
{tag}
diff --git a/apps/owner/src/app/signup/register/page.tsx b/apps/owner/src/app/signup/register/page.tsx
index 7ab9cf1..0ce9159 100644
--- a/apps/owner/src/app/signup/register/page.tsx
+++ b/apps/owner/src/app/signup/register/page.tsx
@@ -1,7 +1,13 @@
"use client";
-import { useMemo, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation";
+
+import type {
+ StoreUpdateReqDTO,
+ StoreLocationUpdateReqDTO,
+} from "@compasser/api";
+
import StoreNameField from "./_components/fields/StoreNameField";
import EmailField from "./_components/fields/EmailField";
import StoreAddressField from "./_components/fields/StoreAddressField";
@@ -14,144 +20,222 @@ import RegisterSubmitButton from "./_components/RegisterSubmitButton";
import BusinessHoursModal from "./_components/modals/BusinessHoursModal";
import RandomBoxModal from "./_components/modals/RandomBoxModal";
-import {
- businessHoursData,
- dayLabelMap,
- orderedDays,
- initialRandomBoxes,
- tagOptions,
-} from "./_constants/register";
+import { useMyStoreQuery } from "@/shared/queries/query/useMyStoreQuery";
+import { usePatchMyStoreMutation } from "@/shared/queries/mutation/auth/usePatchMyStoreMutation";
+import { usePatchMyStoreLocationMutation } from "@/shared/queries/mutation/auth/usePatchMyStoreLocationMutation";
+import { useRandomBoxListQuery } from "@/shared/queries/query/useRandomBoxListQuery";
+import { useCreateRandomBoxMutation } from "@/shared/queries/mutation/auth/useCreateRandomBoxMutation";
+import { useUpdateRandomBoxMutation } from "@/shared/queries/mutation/auth/useUpdateRandomBoxMutation";
+import { useDeleteRandomBoxMutation } from "@/shared/queries/mutation/auth/useDeleteRandomBoxMutation";
+import { useStoreImageQuery } from "@/shared/queries/query/useStoreImageQuery";
+import { useUploadStoreImageMutation } from "@/shared/queries/mutation/auth/useUploadStoreImageMutation";
+import { useRemoveStoreImageMutation } from "@/shared/queries/mutation/auth/useRemoveStoreImageMutation";
-import type {
- AccountType,
- RandomBoxItem,
- RandomBoxFormValue,
-} from "./_types/register";
+import { parseBusinessHours, EMPTY_BUSINESS_HOURS } from "./_utils/business-hours";
export default function StoreRegisterPage() {
const router = useRouter();
- const [selectedAccountType, setSelectedAccountType] =
- useState
(null);
- const [selectedTags, setSelectedTags] = useState([]);
- const [randomBoxes, setRandomBoxes] =
- useState(initialRandomBoxes);
+ const { data: myStore, isLoading: isMyStoreLoading } = useMyStoreQuery();
+ const storeId = myStore?.storeId;
+
+ const { data: randomBoxes = [] } = useRandomBoxListQuery(storeId);
+ const { data: storeImage } = useStoreImageQuery();
+
+ const patchMyStoreMutation = usePatchMyStoreMutation();
+ const patchMyStoreLocationMutation = usePatchMyStoreLocationMutation();
+ const createRandomBoxMutation = useCreateRandomBoxMutation();
+ const updateRandomBoxMutation = useUpdateRandomBoxMutation();
+ const deleteRandomBoxMutation = useDeleteRandomBoxMutation();
+ const uploadStoreImageMutation = useUploadStoreImageMutation();
+ const removeStoreImageMutation = useRemoveStoreImageMutation();
+
+ const [storeName, setStoreName] = useState("");
+ const [storeEmail, setStoreEmail] = useState("");
+ const [inputAddress, setInputAddress] = useState("");
+ const [bankName, setBankName] = useState("");
+ const [depositor, setDepositor] = useState("");
+ const [bankAccount, setBankAccount] = useState("");
+ const [businessHours, setBusinessHours] = useState(EMPTY_BUSINESS_HOURS);
+
+ const tagOptions = ["카페", "베이커리", "식당"] as const;
+ const [selectedTag, setSelectedTag] = useState<"" | "카페" | "베이커리" | "식당">("");
const [selectedRandomBoxIds, setSelectedRandomBoxIds] = useState([]);
- const [isBusinessHoursModalOpen, setIsBusinessHoursModalOpen] =
- useState(false);
+ const [isBusinessHoursModalOpen, setIsBusinessHoursModalOpen] = useState(false);
const [isRandomBoxModalOpen, setIsRandomBoxModalOpen] = useState(false);
const [photoFile, setPhotoFile] = useState(null);
- const [photoPreviewUrl, setPhotoPreviewUrl] = useState("");
+ const [photoPreviewUrl, setPhotoPreviewUrl] = useState("");
+ const [imageRemoved, setImageRemoved] = useState(false);
+
+ useEffect(() => {
+ if (!myStore) return;
+
+ setStoreName(myStore.storeName ?? "");
+ setStoreEmail("");
+ setInputAddress(myStore.inputAddress ?? "");
+ setBusinessHours(parseBusinessHours(myStore.businessHours));
+ }, [myStore]);
+
+ useEffect(() => {
+ if (!storeImage || imageRemoved) return;
+ setPhotoPreviewUrl(storeImage.imageUrl);
+ }, [storeImage, imageRemoved]);
const businessHoursRows = useMemo(() => {
+ const dayLabelMap = {
+ mon: "월",
+ tue: "화",
+ wed: "수",
+ thu: "목",
+ fri: "금",
+ sat: "토",
+ sun: "일",
+ } as const;
+
+ const orderedDays = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] as const;
+
return orderedDays.map((day) => {
- const value = businessHoursData[day];
- const formatted = value === "closed" ? "휴무" : value.replace("-", " ~ ");
+ const value = businessHours[day];
+ const formatted = value === "closed" ? "휴무" : value || "-";
return {
dayLabel: dayLabelMap[day],
time: formatted,
};
});
- }, []);
-
- const toggleTag = (tag: string) => {
- setSelectedTags((prev) =>
- prev.includes(tag)
- ? prev.filter((item) => item !== tag)
- : [...prev, tag]
- );
- };
+ }, [businessHours]);
const toggleRandomBoxSelection = (id: number) => {
setSelectedRandomBoxIds((prev) =>
- prev.includes(id)
- ? prev.filter((item) => item !== id)
- : [...prev, id]
+ prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
);
};
- const handleDeleteRandomBoxes = () => {
- setRandomBoxes((prev) =>
- prev.filter((item) => !selectedRandomBoxIds.includes(item.id))
+ const handleDeleteRandomBoxes = async () => {
+ if (!storeId || selectedRandomBoxIds.length === 0) return;
+
+ await Promise.all(
+ selectedRandomBoxIds.map((boxId) =>
+ deleteRandomBoxMutation.mutateAsync({ storeId, boxId })
+ )
);
+
setSelectedRandomBoxIds([]);
};
- const handleOpenRandomBoxModal = () => {
- setIsRandomBoxModalOpen(true);
- };
+ const handleSubmitRandomBox = async (form: {
+ boxName: string;
+ stock: number;
+ price: number;
+ buyLimit: number;
+ content: string;
+ boxId?: number;
+ }) => {
+ if (!storeId) return;
- const handleCloseRandomBoxModal = () => {
- setIsRandomBoxModalOpen(false);
- };
+ if (form.boxId) {
+ await updateRandomBoxMutation.mutateAsync({
+ storeId,
+ boxId: form.boxId,
+ body: {
+ boxName: form.boxName,
+ stock: form.stock,
+ price: form.price,
+ buyLimit: form.buyLimit,
+ content: form.content,
+ },
+ });
+ return;
+ }
- const handleSubmitRandomBox = (data: RandomBoxFormValue) => {
- const nextId =
- randomBoxes.length > 0
- ? Math.max(...randomBoxes.map((item) => item.id)) + 1
- : 1;
-
- setRandomBoxes((prev) => [
- ...prev,
- {
- id: nextId,
- name: data.name,
- quantity: data.quantity,
- price: 0,
- limit: data.limit,
- pickupStartTime: data.pickupStartTime,
- pickupEndTime: data.pickupEndTime,
- description: data.description,
+ await createRandomBoxMutation.mutateAsync({
+ storeId,
+ body: {
+ boxName: form.boxName,
+ stock: form.stock,
+ price: form.price,
+ buyLimit: form.buyLimit,
+ content: form.content,
},
- ]);
- };
-
- const handleOpenBusinessHoursModal = () => {
- setIsBusinessHoursModalOpen(true);
+ });
};
- const handleCloseBusinessHoursModal = () => {
+ const handleSubmitBusinessHours = (value: typeof businessHours) => {
+ setBusinessHours(value);
setIsBusinessHoursModalOpen(false);
};
- const handleSubmitBusinessHours = (data: any) => {
- console.log("영업시간 등록", data);
+ const handleChangePhoto = (file: File) => {
+ setPhotoFile(file);
+ setImageRemoved(false);
+ setPhotoPreviewUrl(URL.createObjectURL(file));
};
- const handleSearchAddress = () => {
- console.log("주소 검색");
+ const handleRemovePhoto = async () => {
+ setPhotoFile(null);
+ setPhotoPreviewUrl("");
+ setImageRemoved(true);
+
+ if (storeImage) {
+ await removeStoreImageMutation.mutateAsync(undefined);
+ }
};
- const handleChangePhoto = (file: File) => {
- setPhotoFile(file);
+ const handleCompleteRegister = async () => {
+ if (!myStore) return;
- const objectUrl = URL.createObjectURL(file);
- setPhotoPreviewUrl(objectUrl);
- };
+ const storePayload: StoreUpdateReqDTO = {
+ storeName,
+ storeEmail,
+ bankName,
+ depositor,
+ bankAccount,
+ businessHours,
+ };
+
+ const locationPayload: StoreLocationUpdateReqDTO = {
+ inputAddress,
+ };
+
+ await patchMyStoreMutation.mutateAsync(storePayload);
+ await patchMyStoreLocationMutation.mutateAsync(locationPayload);
+
+ if (photoFile) {
+ await uploadStoreImageMutation.mutateAsync(photoFile);
+ }
- const handleCompleteRegister = () => {
- console.log("업로드 파일", photoFile);
router.push("/main");
};
+ if (isMyStoreLoading) {
+ return 불러오는 중...
;
+ }
+
return (
<>
-
-
-
+
+
+
{}}
+ />
setIsBusinessHoursModalOpen(true)}
/>
setIsRandomBoxModalOpen(true)}
/>
@@ -180,13 +265,14 @@ export default function StoreRegisterPage() {
setIsBusinessHoursModalOpen(false)}
+ initialValue={businessHours}
onSubmit={handleSubmitBusinessHours}
/>
setIsRandomBoxModalOpen(false)}
onSubmit={handleSubmitRandomBox}
/>
>
From dba4df59fd7257f14e76cf603cf4b3020d68eb76 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:31:38 +0900
Subject: [PATCH 8/9] chore/account-field
---
.../_components/fields/AccountField.tsx | 148 ++++++++++--------
.../sections/PhotoUploadSection.tsx | 72 ++++++---
.../owner/src/shared/utils/businessLicense.ts | 22 +--
3 files changed, 132 insertions(+), 110 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 dfccac5..fc612e2 100644
--- a/apps/owner/src/app/signup/register/_components/fields/AccountField.tsx
+++ b/apps/owner/src/app/signup/register/_components/fields/AccountField.tsx
@@ -7,6 +7,8 @@ import {
normalizeAccountNumber,
} from "@/shared/utils/bank";
+type AccountStep = "bank" | "holder" | "account";
+
interface AccountFieldProps {
bankName: string;
depositor: string;
@@ -24,90 +26,102 @@ export default function AccountField({
onChangeDepositor,
onChangeBankAccount,
}: AccountFieldProps) {
- const [bankKeyword, setBankKeyword] = useState("");
+ const [step, setStep] = useState("bank");
+ const [bankKeyword, setBankKeyword] = useState(bankName);
const filteredBanks = useMemo(() => {
return filterBankNames(bankKeyword);
}, [bankKeyword]);
- const handleChangeBankName = (nextValue: string) => {
- onChangeBankName(nextValue);
- setBankKeyword(nextValue);
- };
+ const currentValue =
+ step === "bank"
+ ? bankName
+ : step === "holder"
+ ? depositor
+ : bankAccount;
+
+ const currentPlaceholder =
+ step === "bank"
+ ? "은행명을 입력해주세요"
+ : step === "holder"
+ ? "예금주명을 입력해주세요"
+ : "계좌번호를 입력해주세요";
+
+ const handleChangeInput = (nextValue: string) => {
+ if (step === "bank") {
+ onChangeBankName(nextValue);
+ setBankKeyword(nextValue);
+ return;
+ }
+
+ if (step === "holder") {
+ onChangeDepositor(nextValue);
+ return;
+ }
- const handleChangeBankAccount = (nextValue: string) => {
onChangeBankAccount(normalizeAccountNumber(nextValue));
};
+ const handleSelectBank = (bank: string) => {
+ onChangeBankName(bank);
+ setBankKeyword(bank);
+ };
+
return (
계좌 정보
-
-
-
-
- 은행
-
-
-
-
handleChangeBankName(e.target.value)}
- />
-
- {bankKeyword.trim() !== "" && (
-
- {filteredBanks.map((bank) => (
- {
- onChangeBankName(bank);
- setBankKeyword(bank);
- }}
- >
- {bank}
-
- ))}
-
- )}
-
+
+
setStep("bank")}
+ >
+ 은행
+
-
-
-
- 예금주명
-
-
-
-
onChangeDepositor(e.target.value)}
- />
-
+
setStep("holder")}
+ >
+ 예금주명
+
-
-
-
- 계좌번호
+ setStep("account")}
+ >
+ 계좌번호
+
+
+
+
handleChangeInput(e.target.value)}
+ />
+
+ {step === "bank" && bankKeyword.trim() !== "" && (
+
+ {filteredBanks.map((bank) => (
+ handleSelectBank(bank)}
+ >
+ {bank}
-
-
-
handleChangeBankAccount(e.target.value)}
- />
+ ))}
-
+ )}
);
}
\ No newline at end of file
diff --git a/apps/owner/src/app/signup/register/_components/sections/PhotoUploadSection.tsx b/apps/owner/src/app/signup/register/_components/sections/PhotoUploadSection.tsx
index f8f2df6..3ae4daf 100644
--- a/apps/owner/src/app/signup/register/_components/sections/PhotoUploadSection.tsx
+++ b/apps/owner/src/app/signup/register/_components/sections/PhotoUploadSection.tsx
@@ -1,7 +1,11 @@
"use client";
+import { ChangeEvent, useRef } from "react";
+import Image from "next/image";
+import { Icon } from "@compasser/design-system";
+
interface PhotoUploadSectionProps {
- previewUrl: string;
+ previewUrl?: string;
onChangePhoto: (file: File) => void;
onRemovePhoto: () => void | Promise
;
}
@@ -11,47 +15,69 @@ export default function PhotoUploadSection({
onChangePhoto,
onRemovePhoto,
}: PhotoUploadSectionProps) {
- const handleChangeFile = (e: React.ChangeEvent) => {
- const file = e.target.files?.[0];
+ const fileInputRef = useRef(null);
+
+ const handleOpenFilePicker = () => {
+ fileInputRef.current?.click();
+ };
+
+ const handleChangeFile = (event: ChangeEvent) => {
+ const file = event.target.files?.[0];
if (!file) return;
+
onChangePhoto(file);
+ event.target.value = "";
+ };
+
+ const handleClickRemove = async (
+ event: React.MouseEvent,
+ ) => {
+ event.stopPropagation();
+ await onRemovePhoto();
};
return (
사진 첨부
-
상점의 대표이미지를 추가해주세요!
+
+ 상점의 대표 이미지를 추가해주세요!
+
-
+
{previewUrl ? (
-
-
![]()
+
- ×
+
-
+ >
) : (
-
+
)}
-
+
+
+
);
}
\ No newline at end of file
diff --git a/apps/owner/src/shared/utils/businessLicense.ts b/apps/owner/src/shared/utils/businessLicense.ts
index 4c8b948..303ec8f 100644
--- a/apps/owner/src/shared/utils/businessLicense.ts
+++ b/apps/owner/src/shared/utils/businessLicense.ts
@@ -3,27 +3,9 @@ export const normalizeBusinessNumber = (value: string) => {
};
export const isValidBusinessNumberFormat = (value: string) => {
- return /^\d{10}$/.test(value);
+ return /^\d{10}$/.test(normalizeBusinessNumber(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];
+ return /^\d{10}$/.test(normalizeBusinessNumber(value));
};
\ No newline at end of file
From 94924b8f9e3c731df5e101b2bf688dde77529bb6 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:38:18 +0900
Subject: [PATCH 9/9] refactor/store-info
---
.../src/app/(tabs)/mypage/store-info/page.tsx | 295 ++++++++++++------
1 file changed, 207 insertions(+), 88 deletions(-)
diff --git a/apps/owner/src/app/(tabs)/mypage/store-info/page.tsx b/apps/owner/src/app/(tabs)/mypage/store-info/page.tsx
index cd2ffdc..d987eba 100644
--- a/apps/owner/src/app/(tabs)/mypage/store-info/page.tsx
+++ b/apps/owner/src/app/(tabs)/mypage/store-info/page.tsx
@@ -1,7 +1,13 @@
"use client";
-import { useMemo, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation";
+import { Button, Header } from "@compasser/design-system";
+import type {
+ StoreUpdateReqDTO,
+ StoreLocationUpdateReqDTO,
+} from "@compasser/api";
+
import StoreNameField from "@/app/signup/register/_components/fields/StoreNameField";
import EmailField from "@/app/signup/register/_components/fields/EmailField";
import StoreAddressField from "@/app/signup/register/_components/fields/StoreAddressField";
@@ -12,130 +18,230 @@ import PhotoUploadSection from "@/app/signup/register/_components/sections/Photo
import TagSection from "@/app/signup/register/_components/sections/TagSection";
import BusinessHoursModal from "@/app/signup/register/_components/modals/BusinessHoursModal";
import RandomBoxModal from "@/app/signup/register/_components/modals/RandomBoxModal";
-import { Button, Header } from "@compasser/design-system";
+
+import { useMyStoreQuery } from "@/shared/queries/query/useMyStoreQuery";
+import { usePatchMyStoreMutation } from "@/shared/queries/mutation/auth/usePatchMyStoreMutation";
+import { usePatchMyStoreLocationMutation } from "@/shared/queries/mutation/auth/usePatchMyStoreLocationMutation";
+import { useRandomBoxListQuery } from "@/shared/queries/query/useRandomBoxListQuery";
+import { useCreateRandomBoxMutation } from "@/shared/queries/mutation/auth/useCreateRandomBoxMutation";
+import { useUpdateRandomBoxMutation } from "@/shared/queries/mutation/auth/useUpdateRandomBoxMutation";
+import { useDeleteRandomBoxMutation } from "@/shared/queries/mutation/auth/useDeleteRandomBoxMutation";
+import { useStoreImageQuery } from "@/shared/queries/query/useStoreImageQuery";
+import { useUploadStoreImageMutation } from "@/shared/queries/mutation/auth/useUploadStoreImageMutation";
+import { useRemoveStoreImageMutation } from "@/shared/queries/mutation/auth/useRemoveStoreImageMutation";
import {
- businessHoursData,
- dayLabelMap,
- orderedDays,
- initialRandomBoxes,
- tagOptions,
-} from "@/app/signup/register/_constants/register";
+ parseBusinessHours,
+ EMPTY_BUSINESS_HOURS,
+} from "@/app/signup/register/_utils/business-hours";
-import type {
- AccountType,
- RandomBoxItem,
- RandomBoxFormValue,
-} from "@/app/signup/register/_types/register";
+type FixedTag = "카페" | "베이커리" | "식당";
export default function StoreInfoEditPage() {
const router = useRouter();
- const [selectedAccountType, setSelectedAccountType] =
- useState(null);
- const [selectedTags, setSelectedTags] = useState([]);
- const [randomBoxes, setRandomBoxes] =
- useState(initialRandomBoxes);
- const [selectedRandomBoxIds, setSelectedRandomBoxIds] = useState([]);
+ const { data: myStore, isLoading: isMyStoreLoading } = useMyStoreQuery();
+ const storeId = myStore?.storeId;
+
+ const { data: randomBoxes = [] } = useRandomBoxListQuery(storeId);
+ const { data: storeImage } = useStoreImageQuery();
+
+ const patchMyStoreMutation = usePatchMyStoreMutation();
+ const patchMyStoreLocationMutation = usePatchMyStoreLocationMutation();
+ const createRandomBoxMutation = useCreateRandomBoxMutation();
+ const updateRandomBoxMutation = useUpdateRandomBoxMutation();
+ const deleteRandomBoxMutation = useDeleteRandomBoxMutation();
+ const uploadStoreImageMutation = useUploadStoreImageMutation();
+ const removeStoreImageMutation = useRemoveStoreImageMutation();
+
+ const [storeName, setStoreName] = useState("");
+ const [storeEmail, setStoreEmail] = useState("");
+ const [inputAddress, setInputAddress] = useState("");
+ const [bankName, setBankName] = useState("");
+ const [depositor, setDepositor] = useState("");
+ const [bankAccount, setBankAccount] = useState("");
+ const [businessHours, setBusinessHours] = useState(EMPTY_BUSINESS_HOURS);
+
+ const tagOptions: FixedTag[] = ["카페", "베이커리", "식당"];
+ const [selectedTag, setSelectedTag] = useState("");
+
+ const [selectedRandomBoxIds, setSelectedRandomBoxIds] = useState(
+ [],
+ );
const [isBusinessHoursModalOpen, setIsBusinessHoursModalOpen] =
useState(false);
const [isRandomBoxModalOpen, setIsRandomBoxModalOpen] = useState(false);
const [photoFile, setPhotoFile] = useState(null);
- const [photoPreviewUrl, setPhotoPreviewUrl] = useState("");
+ const [photoPreviewUrl, setPhotoPreviewUrl] = useState("");
+ const [imageRemoved, setImageRemoved] = useState(false);
+
+ const mapStoreTagToLabel = (tag: string | undefined): FixedTag | "" => {
+ switch (tag) {
+ case "CAFE":
+ return "카페";
+ case "BAKERY":
+ return "베이커리";
+ case "RESTAURANT":
+ return "식당";
+ default:
+ return "";
+ }
+ };
+
+ useEffect(() => {
+ if (!myStore) return;
+
+ setStoreName(myStore.storeName ?? "");
+ setStoreEmail("");
+ setInputAddress(myStore.inputAddress ?? "");
+ setBusinessHours(parseBusinessHours(myStore.businessHours));
+ setSelectedTag(mapStoreTagToLabel(myStore.tag));
+ }, [myStore]);
+
+ useEffect(() => {
+ if (!storeImage || imageRemoved) return;
+ setPhotoPreviewUrl(storeImage.imageUrl);
+ }, [storeImage, imageRemoved]);
const businessHoursRows = useMemo(() => {
+ const dayLabelMap = {
+ mon: "월",
+ tue: "화",
+ wed: "수",
+ thu: "목",
+ fri: "금",
+ sat: "토",
+ sun: "일",
+ } as const;
+
+ const orderedDays = [
+ "mon",
+ "tue",
+ "wed",
+ "thu",
+ "fri",
+ "sat",
+ "sun",
+ ] as const;
+
return orderedDays.map((day) => {
- const value = businessHoursData[day];
- const formatted = value === "closed" ? "휴무" : value.replace("-", " ~ ");
+ const value = businessHours[day];
+ const formatted = value === "closed" ? "휴무" : value || "-";
return {
dayLabel: dayLabelMap[day],
time: formatted,
};
});
- }, []);
-
- const toggleTag = (tag: string) => {
- setSelectedTags((prev) =>
- prev.includes(tag)
- ? prev.filter((item) => item !== tag)
- : [...prev, tag]
- );
- };
+ }, [businessHours]);
const toggleRandomBoxSelection = (id: number) => {
setSelectedRandomBoxIds((prev) =>
- prev.includes(id)
- ? prev.filter((item) => item !== id)
- : [...prev, id]
+ prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id],
);
};
- const handleDeleteRandomBoxes = () => {
- setRandomBoxes((prev) =>
- prev.filter((item) => !selectedRandomBoxIds.includes(item.id))
+ const handleDeleteRandomBoxes = async () => {
+ if (!storeId || selectedRandomBoxIds.length === 0) return;
+
+ await Promise.all(
+ selectedRandomBoxIds.map((boxId) =>
+ deleteRandomBoxMutation.mutateAsync({ storeId, boxId }),
+ ),
);
+
setSelectedRandomBoxIds([]);
};
- const handleOpenRandomBoxModal = () => {
- setIsRandomBoxModalOpen(true);
- };
+ const handleSubmitRandomBox = async (form: {
+ boxName: string;
+ stock: number;
+ price: number;
+ buyLimit: number;
+ content: string;
+ boxId?: number;
+ }) => {
+ if (!storeId) return;
- const handleCloseRandomBoxModal = () => {
- setIsRandomBoxModalOpen(false);
- };
+ if (form.boxId) {
+ await updateRandomBoxMutation.mutateAsync({
+ storeId,
+ boxId: form.boxId,
+ body: {
+ boxName: form.boxName,
+ stock: form.stock,
+ price: form.price,
+ buyLimit: form.buyLimit,
+ content: form.content,
+ },
+ });
+ return;
+ }
- const handleSubmitRandomBox = (data: RandomBoxFormValue) => {
- const nextId =
- randomBoxes.length > 0
- ? Math.max(...randomBoxes.map((item) => item.id)) + 1
- : 1;
-
- setRandomBoxes((prev) => [
- ...prev,
- {
- id: nextId,
- name: data.name,
- quantity: data.quantity,
- price: 0,
- limit: data.limit,
- pickupStartTime: data.pickupStartTime,
- pickupEndTime: data.pickupEndTime,
- description: data.description,
+ await createRandomBoxMutation.mutateAsync({
+ storeId,
+ body: {
+ boxName: form.boxName,
+ stock: form.stock,
+ price: form.price,
+ buyLimit: form.buyLimit,
+ content: form.content,
},
- ]);
- };
-
- const handleOpenBusinessHoursModal = () => {
- setIsBusinessHoursModalOpen(true);
+ });
};
- const handleCloseBusinessHoursModal = () => {
+ const handleSubmitBusinessHours = (value: typeof businessHours) => {
+ setBusinessHours(value);
setIsBusinessHoursModalOpen(false);
};
- const handleSubmitBusinessHours = (data: any) => {
- console.log("영업시간 수정", data);
+ const handleChangePhoto = (file: File) => {
+ setPhotoFile(file);
+ setImageRemoved(false);
+ setPhotoPreviewUrl(URL.createObjectURL(file));
};
- const handleSearchAddress = () => {
- console.log("주소 검색");
+ const handleRemovePhoto = async () => {
+ setPhotoFile(null);
+ setPhotoPreviewUrl("");
+ setImageRemoved(true);
+
+ if (storeImage) {
+ await removeStoreImageMutation.mutateAsync(undefined);
+ }
};
- const handleChangePhoto = (file: File) => {
- setPhotoFile(file);
+ const handleCompleteEdit = async () => {
+ if (!myStore) return;
- const objectUrl = URL.createObjectURL(file);
- setPhotoPreviewUrl(objectUrl);
- };
+ const storePayload: StoreUpdateReqDTO = {
+ storeName,
+ storeEmail,
+ bankName,
+ depositor,
+ bankAccount,
+ businessHours,
+ };
+
+ const locationPayload: StoreLocationUpdateReqDTO = {
+ inputAddress,
+ };
+
+ await patchMyStoreMutation.mutateAsync(storePayload);
+ await patchMyStoreLocationMutation.mutateAsync(locationPayload);
+
+ if (photoFile) {
+ await uploadStoreImageMutation.mutateAsync(photoFile);
+ }
- const handleCompleteEdit = () => {
- console.log("수정 파일", photoFile);
router.push("/mypage");
};
+ if (isMyStoreLoading) {
+ return 불러오는 중...
;
+ }
+
return (
<>
@@ -148,17 +254,28 @@ export default function StoreInfoEditPage() {
-
-
-
+
+
+
{}}
+ />
+
+ setIsBusinessHoursModalOpen(true)
+ }
/>
setIsRandomBoxModalOpen(true)}
/>
@@ -198,13 +316,14 @@ export default function StoreInfoEditPage() {
setIsBusinessHoursModalOpen(false)}
+ initialValue={businessHours}
onSubmit={handleSubmitBusinessHours}
/>
setIsRandomBoxModalOpen(false)}
onSubmit={handleSubmitRandomBox}
/>
>