From 7c6a6764a9bddb89d379e0b261a0f7c2ff969802 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, 1 May 2026 03:33:37 +0900 Subject: [PATCH 01/10] feat/owner-upgrade-apis --- .../customer/src/app/payment/success/page.tsx | 30 ++++++++-- apps/customer/src/shared/api/api.ts | 4 +- .../useCompleteAdminSettlementMutation.ts | 12 ++++ apps/owner/src/app/signup/business/page.tsx | 38 +++++------- .../auth/useUpgradeToStoreManagerMutation.ts | 12 ++++ .../auth/useVerifyBusinessMutation.ts | 6 +- packages/api/src/domains/admin-settlement.ts | 59 +++++++++++++++++++ packages/api/src/domains/owner.ts | 16 ----- packages/api/src/index.ts | 2 + packages/api/src/models/admin-settlement.ts | 16 +++++ packages/api/src/models/owner.ts | 21 ++----- 11 files changed, 153 insertions(+), 63 deletions(-) create mode 100644 apps/customer/src/shared/queries/mutation/admin-settlement/useCompleteAdminSettlementMutation.ts create mode 100644 apps/owner/src/shared/queries/mutation/auth/useUpgradeToStoreManagerMutation.ts create mode 100644 packages/api/src/domains/admin-settlement.ts create mode 100644 packages/api/src/models/admin-settlement.ts diff --git a/apps/customer/src/app/payment/success/page.tsx b/apps/customer/src/app/payment/success/page.tsx index b0027d3..f1c979e 100644 --- a/apps/customer/src/app/payment/success/page.tsx +++ b/apps/customer/src/app/payment/success/page.tsx @@ -1,9 +1,10 @@ "use client"; -import { Suspense, useEffect, useMemo, useState } from "react"; +import { Suspense, useEffect, useMemo, useRef, useState } from "react"; import { useSearchParams } from "next/navigation"; import type { StoreRandomBoxRespDTO, StoreRespDTO } from "@compasser/api"; import { useApproveKakaoPayMutation } from "@/shared/queries/mutation/payment/useApproveKakaoPayMutation"; +import { useCompleteAdminSettlementMutation } from "@/shared/queries/mutation/admin-settlement/useCompleteAdminSettlementMutation"; import PurchaseCompleteModal from "@/app/(tabs)/main/store/[id]/purchase/_components/PurchaseCompleteModal"; interface PendingPayment { @@ -15,7 +16,12 @@ interface PendingPayment { function PaymentSuccessContent() { const searchParams = useSearchParams(); + const approveKakaoPayMutation = useApproveKakaoPayMutation(); + const completeAdminSettlementMutation = useCompleteAdminSettlementMutation(); + + const hasRequestedRef = useRef(false); + const [pendingPayment, setPendingPayment] = useState( null, ); @@ -29,10 +35,14 @@ function PaymentSuccessContent() { const pgToken = searchParams.get("pg_token"); useEffect(() => { + if (hasRequestedRef.current) return; + const savedPayment = sessionStorage.getItem("pendingPayment"); if (!savedPayment || !reservationId || !pgToken) return; + hasRequestedRef.current = true; + const parsedPayment = JSON.parse(savedPayment) as PendingPayment; setPendingPayment(parsedPayment); @@ -44,12 +54,24 @@ function PaymentSuccessContent() { }, { onSuccess: () => { - sessionStorage.removeItem("pendingPayment"); - setIsCompleteModalOpen(true); + completeAdminSettlementMutation.mutate( + { + storeId: parsedPayment.store.storeId, + body: { + reservationIds: [reservationId], + }, + }, + { + onSettled: () => { + sessionStorage.removeItem("pendingPayment"); + setIsCompleteModalOpen(true); + }, + }, + ); }, }, ); - }, [reservationId, pgToken, approveKakaoPayMutation]); + }, [reservationId, pgToken]); if (approveKakaoPayMutation.isPending) { return
결제 승인 처리 중...
; diff --git a/apps/customer/src/shared/api/api.ts b/apps/customer/src/shared/api/api.ts index e643a7f..3f6c68f 100644 --- a/apps/customer/src/shared/api/api.ts +++ b/apps/customer/src/shared/api/api.ts @@ -9,6 +9,7 @@ import { createMemberModule, createOrderModule, createPaymentModule, + createAdminSettlementModule, } from "@compasser/api"; const tokenStore: TokenStore = { @@ -44,4 +45,5 @@ export const authModule = createAuthModule(compasserApi); export const memberModule = createMemberModule(compasserApi); export const storeModule = createStoreModule(compasserApi); export const orderModule = createOrderModule(compasserApi); -export const paymentModule = createPaymentModule(compasserApi); \ No newline at end of file +export const paymentModule = createPaymentModule(compasserApi); +export const adminSettlementModule = createAdminSettlementModule(compasserApi); \ No newline at end of file diff --git a/apps/customer/src/shared/queries/mutation/admin-settlement/useCompleteAdminSettlementMutation.ts b/apps/customer/src/shared/queries/mutation/admin-settlement/useCompleteAdminSettlementMutation.ts new file mode 100644 index 0000000..d4fb747 --- /dev/null +++ b/apps/customer/src/shared/queries/mutation/admin-settlement/useCompleteAdminSettlementMutation.ts @@ -0,0 +1,12 @@ +"use client"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { adminSettlementModule } from "@/shared/api/api"; + +export const useCompleteAdminSettlementMutation = () => { + const queryClient = useQueryClient(); + + return useMutation({ + ...adminSettlementModule.mutations.completeSettlement(queryClient), + }); +}; \ No newline at end of file diff --git a/apps/owner/src/app/signup/business/page.tsx b/apps/owner/src/app/signup/business/page.tsx index f9974b3..b4ea32e 100644 --- a/apps/owner/src/app/signup/business/page.tsx +++ b/apps/owner/src/app/signup/business/page.tsx @@ -3,7 +3,7 @@ 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 { useUpgradeToStoreManagerMutation } from "@/shared/queries/mutation/auth/useUpgradeToStoreManagerMutation"; import { useOwnerSignupStore } from "@/shared/stores/ownerSignup.store"; import { normalizeBusinessNumber, @@ -13,7 +13,7 @@ import { export default function BusinessSignupPage() { const router = useRouter(); - const verifyMutation = useVerifyBusinessMutation(); + const upgradeMutation = useUpgradeToStoreManagerMutation(); const signupCompleted = useOwnerSignupStore((s) => s.signupCompleted); const email = useOwnerSignupStore((s) => s.email); @@ -65,26 +65,20 @@ export default function BusinessSignupPage() { return; } - verifyMutation.mutate( - { - businessLicenseNumber: value, - email, + upgradeMutation.mutate(undefined, { + onSuccess: (res) => { + if (res.alreadyUpgraded) { + setError("이미 사업자 등록이 완료된 계정입니다."); + return; + } + + setBusinessCompleted(); + router.push("/signup/register"); }, - { - onSuccess: (res) => { - if (res.alreadyUpgraded) { - setError("이미 사업자 등록이 완료된 계정입니다."); - return; - } - - setBusinessCompleted(); - router.push("/signup/register"); - }, - onError: () => { - setError("사업자 번호 인증에 실패했습니다."); - }, + onError: () => { + setError("점장 승격 처리에 실패했습니다."); }, - ); + }); }; return ( @@ -115,9 +109,9 @@ export default function BusinessSignupPage() { size="lg" variant="primary" onClick={handleNext} - disabled={verifyMutation.isPending} + disabled={upgradeMutation.isPending} > - {verifyMutation.isPending ? "확인 중..." : "다음으로"} + {upgradeMutation.isPending ? "확인 중..." : "다음으로"} diff --git a/apps/owner/src/shared/queries/mutation/auth/useUpgradeToStoreManagerMutation.ts b/apps/owner/src/shared/queries/mutation/auth/useUpgradeToStoreManagerMutation.ts new file mode 100644 index 0000000..0cb54a8 --- /dev/null +++ b/apps/owner/src/shared/queries/mutation/auth/useUpgradeToStoreManagerMutation.ts @@ -0,0 +1,12 @@ +"use client"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { ownerModule } from "@/shared/api/api"; + +export const useUpgradeToStoreManagerMutation = () => { + const queryClient = useQueryClient(); + + return useMutation({ + ...ownerModule.mutations.upgradeToStoreManager(queryClient), + }); +}; \ No newline at end of file diff --git a/apps/owner/src/shared/queries/mutation/auth/useVerifyBusinessMutation.ts b/apps/owner/src/shared/queries/mutation/auth/useVerifyBusinessMutation.ts index 7112468..e667621 100644 --- a/apps/owner/src/shared/queries/mutation/auth/useVerifyBusinessMutation.ts +++ b/apps/owner/src/shared/queries/mutation/auth/useVerifyBusinessMutation.ts @@ -6,7 +6,7 @@ import { ownerModule } from "@/shared/api/api"; export const useVerifyBusinessMutation = () => { const queryClient = useQueryClient(); - return useMutation( - ownerModule.mutations.verifyBizAndUpgrade(queryClient), - ); + return useMutation({ + ...ownerModule.mutations.verifyBizAndUpgrade(queryClient), + }); }; \ No newline at end of file diff --git a/packages/api/src/domains/admin-settlement.ts b/packages/api/src/domains/admin-settlement.ts new file mode 100644 index 0000000..a4868c8 --- /dev/null +++ b/packages/api/src/domains/admin-settlement.ts @@ -0,0 +1,59 @@ +// packages/api/src/domains/admin-settlement.ts + +import type { QueryClient } from "@tanstack/react-query"; + +import { createMutationWithCache } from "../core/mutation"; +import type { CompasserApi } from "../core/types"; +import type { + AdminSettlementCompleteReqDTO, + AdminSettlementCompleteResponse, +} from "../models/admin-settlement"; + +export interface CompleteAdminSettlementParams { + storeId: number; + body: AdminSettlementCompleteReqDTO; +} + +export const createAdminSettlementModule = (api: CompasserApi) => { + const keys = { + all: ["admin-settlement"] as const, + complete: (storeId: number) => [...keys.all, "complete", storeId] as const, + }; + + const requests = { + completeSettlement: async ({ + storeId, + body, + }: CompleteAdminSettlementParams) => { + const { data } = + await api.privateClient.post( + `/admin/settlements/${storeId}/complete`, + body, + ); + + return data; + }, + }; + + const mutations = { + completeSettlement: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "complete"], + mutationFn: requests.completeSettlement, + getActions: (response, variables) => [ + { + type: "set", + queryKey: keys.complete(variables.storeId), + value: response, + }, + { + type: "invalidate", + queryKey: keys.all, + }, + ], + }), + }; + + return { keys, requests, mutations }; +}; \ No newline at end of file diff --git a/packages/api/src/domains/owner.ts b/packages/api/src/domains/owner.ts index 11102ad..5f48b54 100644 --- a/packages/api/src/domains/owner.ts +++ b/packages/api/src/domains/owner.ts @@ -8,7 +8,6 @@ import type { ReservationListResponse, ReservationReqDTO, ReservationResponse, - SettlementPreviewDTO, } from "../models/owner"; export interface ReservationDecisionParams { @@ -23,8 +22,6 @@ export const createOwnerModule = (api: CompasserApi) => { reservations: () => [...keys.all, "reservations"] as const, pendingReservations: () => [...keys.reservations(), "pending"] as const, processedReservations: () => [...keys.reservations(), "processed"] as const, - settlements: () => [...keys.all, "settlements"] as const, - settlementPreview: () => [...keys.settlements(), "preview"] as const, }; const requests = { @@ -74,13 +71,6 @@ export const createOwnerModule = (api: CompasserApi) => { ); return data; }, - - getSettlementPreview: async (): Promise => { - const { data } = await api.privateClient.get( - "/owners/my-store/settlements/preview", - ); - return data; - }, }; const queries = { @@ -95,12 +85,6 @@ export const createOwnerModule = (api: CompasserApi) => { queryKey: keys.processedReservations(), queryFn: async () => (await requests.getProcessedReservations()).data, }), - - settlementPreview: () => - queryOptions({ - queryKey: keys.settlementPreview(), - queryFn: async () => (await requests.getSettlementPreview()), - }), }; const mutations = { diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 25399c9..32d5350 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -13,6 +13,7 @@ export * from "./models/random-box"; export * from "./models/store"; export * from "./models/storeManager"; export * from "./models/store-image"; +export * from "./models/admin-settlement"; export * from "./domains/auth"; export * from "./domains/health"; export * from "./domains/member"; @@ -23,4 +24,5 @@ export * from "./domains/random-box"; export * from "./domains/store"; export * from "./domains/store-image"; export * from "./domains/store-manager"; +export * from "./domains/admin-settlement"; export * from "./domains/index"; \ No newline at end of file diff --git a/packages/api/src/models/admin-settlement.ts b/packages/api/src/models/admin-settlement.ts new file mode 100644 index 0000000..225fd04 --- /dev/null +++ b/packages/api/src/models/admin-settlement.ts @@ -0,0 +1,16 @@ +import type { ApiResponse } from "../core/types"; + +export interface AdminSettlementCompleteReqDTO { + reservationIds: number[]; +} + +export interface AdminSettlementCompleteRespDTO { + storeId: number; + storeName: string; + count: number; + totalAmount: number; + message: string; +} + +export type AdminSettlementCompleteResponse = + ApiResponse; \ No newline at end of file diff --git a/packages/api/src/models/owner.ts b/packages/api/src/models/owner.ts index c9a1c24..92842b4 100644 --- a/packages/api/src/models/owner.ts +++ b/packages/api/src/models/owner.ts @@ -2,8 +2,10 @@ import type { ApiResponse } from "../core/types"; import type { ReservationStatus, RoleType } from "./common"; export interface BusinessLicenseVerifyReqDTO { - businessLicenseNumber?: string; - email?: string; + businessLicenseNumber: string; + ownerName: string; + startDate: string; + businessName: string; } export interface OwnerUpgradeRespDTO { @@ -39,20 +41,5 @@ export interface ReservationListDTO { count: number; } -export interface SettlementPreviewReservationDTO { - reservationId: number; - memberId: number; - totalPrice: number; - createdAt: string; -} - -export interface SettlementPreviewDTO { - storeId: number; - storeName: string; - count: number; - totalAmount: number; - reservations: SettlementPreviewReservationDTO[]; -} - export type ReservationResponse = ApiResponse; export type ReservationListResponse = ApiResponse; \ No newline at end of file From 0a16f811c43bb807e5281a6db5a21671320c6ada 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, 1 May 2026 03:42:44 +0900 Subject: [PATCH 02/10] Update pr-automation.yml --- .github/workflows/pr-automation.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/pr-automation.yml b/.github/workflows/pr-automation.yml index d4f0624..2d7c318 100644 --- a/.github/workflows/pr-automation.yml +++ b/.github/workflows/pr-automation.yml @@ -37,7 +37,6 @@ jobs: question: 'question' }; - const branchName = context.payload.pull_request.head.ref; const parts = branchName.split('/'); if (parts.length < 3) { From f52f69813c791038564b3e6b303b051658dcb680 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, 1 May 2026 03:48:02 +0900 Subject: [PATCH 03/10] chore/owner-apis --- packages/api/src/domains/owner.ts | 16 ++++++++++++++++ packages/api/src/models/owner.ts | 21 +++++++++++++++++---- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/packages/api/src/domains/owner.ts b/packages/api/src/domains/owner.ts index 5f48b54..11102ad 100644 --- a/packages/api/src/domains/owner.ts +++ b/packages/api/src/domains/owner.ts @@ -8,6 +8,7 @@ import type { ReservationListResponse, ReservationReqDTO, ReservationResponse, + SettlementPreviewDTO, } from "../models/owner"; export interface ReservationDecisionParams { @@ -22,6 +23,8 @@ export const createOwnerModule = (api: CompasserApi) => { reservations: () => [...keys.all, "reservations"] as const, pendingReservations: () => [...keys.reservations(), "pending"] as const, processedReservations: () => [...keys.reservations(), "processed"] as const, + settlements: () => [...keys.all, "settlements"] as const, + settlementPreview: () => [...keys.settlements(), "preview"] as const, }; const requests = { @@ -71,6 +74,13 @@ export const createOwnerModule = (api: CompasserApi) => { ); return data; }, + + getSettlementPreview: async (): Promise => { + const { data } = await api.privateClient.get( + "/owners/my-store/settlements/preview", + ); + return data; + }, }; const queries = { @@ -85,6 +95,12 @@ export const createOwnerModule = (api: CompasserApi) => { queryKey: keys.processedReservations(), queryFn: async () => (await requests.getProcessedReservations()).data, }), + + settlementPreview: () => + queryOptions({ + queryKey: keys.settlementPreview(), + queryFn: async () => (await requests.getSettlementPreview()), + }), }; const mutations = { diff --git a/packages/api/src/models/owner.ts b/packages/api/src/models/owner.ts index 92842b4..c9a1c24 100644 --- a/packages/api/src/models/owner.ts +++ b/packages/api/src/models/owner.ts @@ -2,10 +2,8 @@ import type { ApiResponse } from "../core/types"; import type { ReservationStatus, RoleType } from "./common"; export interface BusinessLicenseVerifyReqDTO { - businessLicenseNumber: string; - ownerName: string; - startDate: string; - businessName: string; + businessLicenseNumber?: string; + email?: string; } export interface OwnerUpgradeRespDTO { @@ -41,5 +39,20 @@ export interface ReservationListDTO { count: number; } +export interface SettlementPreviewReservationDTO { + reservationId: number; + memberId: number; + totalPrice: number; + createdAt: string; +} + +export interface SettlementPreviewDTO { + storeId: number; + storeName: string; + count: number; + totalAmount: number; + reservations: SettlementPreviewReservationDTO[]; +} + export type ReservationResponse = ApiResponse; export type ReservationListResponse = ApiResponse; \ No newline at end of file From e2d240b0b2f6dbd197330ad45ebe0ce07dc91000 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, 1 May 2026 18:34:05 +0900 Subject: [PATCH 04/10] feat/owner-reservation-apis --- .../mutation/owner/useApproveReservationMutation.ts | 8 ++++++++ .../mutation/owner/useRejectReservationMutation.ts | 8 ++++++++ .../query/owner/usePendingReservationsQuery.ts | 6 ++++++ .../query/owner/useProcessedReservationsQuery.ts | 6 ++++++ packages/api/src/domains/owner.ts | 4 ++-- packages/api/src/models/owner.ts | 11 ++++------- 6 files changed, 34 insertions(+), 9 deletions(-) create mode 100644 apps/owner/src/shared/queries/mutation/owner/useApproveReservationMutation.ts create mode 100644 apps/owner/src/shared/queries/mutation/owner/useRejectReservationMutation.ts create mode 100644 apps/owner/src/shared/queries/query/owner/usePendingReservationsQuery.ts create mode 100644 apps/owner/src/shared/queries/query/owner/useProcessedReservationsQuery.ts diff --git a/apps/owner/src/shared/queries/mutation/owner/useApproveReservationMutation.ts b/apps/owner/src/shared/queries/mutation/owner/useApproveReservationMutation.ts new file mode 100644 index 0000000..1c67ce1 --- /dev/null +++ b/apps/owner/src/shared/queries/mutation/owner/useApproveReservationMutation.ts @@ -0,0 +1,8 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { ownerModule } from "@/shared/api/api"; + +export const useApproveReservationMutation = () => { + const queryClient = useQueryClient(); + + return useMutation(ownerModule.mutations.approveReservation(queryClient)); +}; \ No newline at end of file diff --git a/apps/owner/src/shared/queries/mutation/owner/useRejectReservationMutation.ts b/apps/owner/src/shared/queries/mutation/owner/useRejectReservationMutation.ts new file mode 100644 index 0000000..2960b00 --- /dev/null +++ b/apps/owner/src/shared/queries/mutation/owner/useRejectReservationMutation.ts @@ -0,0 +1,8 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { ownerModule } from "@/shared/api/api"; + +export const useRejectReservationMutation = () => { + const queryClient = useQueryClient(); + + return useMutation(ownerModule.mutations.rejectReservation(queryClient)); +}; \ No newline at end of file diff --git a/apps/owner/src/shared/queries/query/owner/usePendingReservationsQuery.ts b/apps/owner/src/shared/queries/query/owner/usePendingReservationsQuery.ts new file mode 100644 index 0000000..fb1800a --- /dev/null +++ b/apps/owner/src/shared/queries/query/owner/usePendingReservationsQuery.ts @@ -0,0 +1,6 @@ +import { useQuery } from "@tanstack/react-query"; +import { ownerModule } from "@/shared/api/api"; + +export const usePendingReservationsQuery = () => { + return useQuery(ownerModule.queries.pendingReservations()); +}; \ No newline at end of file diff --git a/apps/owner/src/shared/queries/query/owner/useProcessedReservationsQuery.ts b/apps/owner/src/shared/queries/query/owner/useProcessedReservationsQuery.ts new file mode 100644 index 0000000..52fd953 --- /dev/null +++ b/apps/owner/src/shared/queries/query/owner/useProcessedReservationsQuery.ts @@ -0,0 +1,6 @@ +import { useQuery } from "@tanstack/react-query"; +import { ownerModule } from "@/shared/api/api"; + +export const useProcessedReservationsQuery = () => { + return useQuery(ownerModule.queries.processedReservations()); +}; \ No newline at end of file diff --git a/packages/api/src/domains/owner.ts b/packages/api/src/domains/owner.ts index 11102ad..2a14494 100644 --- a/packages/api/src/domains/owner.ts +++ b/packages/api/src/domains/owner.ts @@ -6,14 +6,14 @@ import type { BusinessLicenseVerifyReqDTO, OwnerUpgradeRespDTO, ReservationListResponse, - ReservationReqDTO, + ReservationRejectReqDTO, ReservationResponse, SettlementPreviewDTO, } from "../models/owner"; export interface ReservationDecisionParams { reservationId: number; - body?: ReservationReqDTO; + body?: ReservationRejectReqDTO; } export const createOwnerModule = (api: CompasserApi) => { diff --git a/packages/api/src/models/owner.ts b/packages/api/src/models/owner.ts index c9a1c24..6c5ce80 100644 --- a/packages/api/src/models/owner.ts +++ b/packages/api/src/models/owner.ts @@ -13,30 +13,27 @@ export interface OwnerUpgradeRespDTO { alreadyUpgraded: boolean; } -export interface ReservationReqDTO { +export interface ReservationRejectReqDTO { status?: ReservationStatus; - rejectReason?: string; + rejectReason: string; } export interface ReservationDTO { reservationId: number; memberId: number; - nickName: string; + customerName: string; storeId: number; storeName: string; randomBoxId: number; randomBoxName: string; - price: number; + totalPrice: number; status: ReservationStatus; requestedQuantity: number; rejectReason?: string; - createdAt: string; - updatedAt: string; } export interface ReservationListDTO { reservations: ReservationDTO[]; - count: number; } export interface SettlementPreviewReservationDTO { From 8eb6b98e7dcb5c2541a198b7f4dfaecefa00386f 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, 1 May 2026 18:34:20 +0900 Subject: [PATCH 05/10] chore/owner-reservation-apis --- .../_components/modal/RejectOrderModal.tsx | 19 ++- .../src/app/(tabs)/order/_types/order.ts | 7 +- .../app/(tabs)/order/_utils/orderStatus.ts | 36 +++-- apps/owner/src/app/(tabs)/order/page.tsx | 151 ++++++++++++------ 4 files changed, 148 insertions(+), 65 deletions(-) diff --git a/apps/owner/src/app/(tabs)/order/_components/modal/RejectOrderModal.tsx b/apps/owner/src/app/(tabs)/order/_components/modal/RejectOrderModal.tsx index 75a3f49..70f79f1 100644 --- a/apps/owner/src/app/(tabs)/order/_components/modal/RejectOrderModal.tsx +++ b/apps/owner/src/app/(tabs)/order/_components/modal/RejectOrderModal.tsx @@ -16,12 +16,20 @@ export default function RejectOrderModal({ }: RejectOrderModalProps) { const [reason, setReason] = useState(""); + const trimmedReason = reason.trim(); + const isValidReason = trimmedReason.length >= 10; + useEffect(() => { if (!open) { setReason(""); } }, [open]); + const handleConfirm = () => { + if (!isValidReason) return; + onConfirm(trimmedReason); + }; + return ( onConfirm(reason)} + onClick={handleConfirm} + disabled={!isValidReason} > 거절하기 @@ -58,7 +67,7 @@ export default function RejectOrderModal({ setReason(e.target.value)} - placeholder="거절 사유를 입력해주세요." + placeholder="거절 사유를 입력해주세요. (10자 이상)" className=" w-full rounded-[8px] border border-primary px-[1rem] py-[0.6rem] @@ -67,6 +76,12 @@ export default function RejectOrderModal({ outline-none " /> + + {!isValidReason && reason.length > 0 && ( +

+ 거절 사유는 10자 이상 입력해주세요. +

+ )}
); diff --git a/apps/owner/src/app/(tabs)/order/_types/order.ts b/apps/owner/src/app/(tabs)/order/_types/order.ts index 2d48126..e69633d 100644 --- a/apps/owner/src/app/(tabs)/order/_types/order.ts +++ b/apps/owner/src/app/(tabs)/order/_types/order.ts @@ -1,6 +1,10 @@ export type OrderTabKey = "reservation" | "order"; -export type ReservationStatus = "pending" | "completed" | "cancelled"; +export type ReservationStatus = + | "pending" + | "completed" + | "cancelled" + | "refunded"; export interface ReservationItem { id: number; @@ -10,6 +14,7 @@ export interface ReservationItem { quantity: string; status: ReservationStatus; processedAt?: string; + rejectReason?: string; } export interface AcceptModalState { diff --git a/apps/owner/src/app/(tabs)/order/_utils/orderStatus.ts b/apps/owner/src/app/(tabs)/order/_utils/orderStatus.ts index d5bf8a4..bd99808 100644 --- a/apps/owner/src/app/(tabs)/order/_utils/orderStatus.ts +++ b/apps/owner/src/app/(tabs)/order/_utils/orderStatus.ts @@ -2,26 +2,30 @@ import type { ReservationStatus } from "../_types/order"; export const getStatusLabel = (status: ReservationStatus) => { switch (status) { - case "pending": - return "확인 대기중"; - case "completed": - return "거래완료"; - case "cancelled": - return "거래취소"; - default: - return ""; + case "pending": + return "확인 대기중"; + case "completed": + return "거래완료"; + case "cancelled": + return "거래취소"; + case "refunded": + return "환불"; + default: + return ""; } }; export const getStatusClassName = (status: ReservationStatus) => { switch (status) { - case "pending": - return "body1-m text-secondary"; - case "completed": - return "body1-m text-primary"; - case "cancelled": - return "body1-m text-gray-500"; - default: - return ""; + case "pending": + return "body1-m text-secondary"; + case "completed": + return "body1-m text-primary"; + case "cancelled": + return "body1-m text-gray-500"; + case "refunded": + return "body1-m text-secondary"; + default: + return ""; } }; \ No newline at end of file diff --git a/apps/owner/src/app/(tabs)/order/page.tsx b/apps/owner/src/app/(tabs)/order/page.tsx index 0bd8071..d84c112 100644 --- a/apps/owner/src/app/(tabs)/order/page.tsx +++ b/apps/owner/src/app/(tabs)/order/page.tsx @@ -5,20 +5,51 @@ import { Header, TopTabBar } from "@compasser/design-system"; import OrderList from "./_components/OrderList"; import AcceptOrderModal from "./_components/modal/AcceptOrderModal"; import RejectOrderModal from "./_components/modal/RejectOrderModal"; -import { INITIAL_RESERVATIONS } from "./_constants/mockOrders"; -import { formatProcessedAt } from "./_utils/formatProcessAt"; import type { AcceptModalState, OrderTabKey, RejectModalState, ReservationItem, } from "./_types/order"; +import { usePendingReservationsQuery } from "@/shared/queries/query/owner/usePendingReservationsQuery"; +import { useProcessedReservationsQuery } from "@/shared/queries/query/owner/useProcessedReservationsQuery"; +import { useApproveReservationMutation } from "@/shared/queries/mutation/owner/useApproveReservationMutation"; +import { useRejectReservationMutation } from "@/shared/queries/mutation/owner/useRejectReservationMutation"; +import type { ReservationDTO } from "@compasser/api"; + +const formatPrice = (price: number) => `${price.toLocaleString()}원`; + +const mapReservationStatus = ( + status: ReservationDTO["status"], +): ReservationItem["status"] => { + switch (status) { + case "REQUESTED": + return "pending"; + case "APPROVED": + return "completed"; + case "REJECTED": + return "cancelled"; + case "CANCELED": + return "refunded"; + default: + return "pending"; + } +}; + +const mapReservationToItem = ( + reservation: ReservationDTO, +): ReservationItem => ({ + id: reservation.reservationId, + customerName: reservation.customerName, + orderDetail: reservation.randomBoxName, + price: formatPrice(reservation.totalPrice), + quantity: `${reservation.requestedQuantity}개`, + status: mapReservationStatus(reservation.status), + rejectReason: reservation.rejectReason, +}); export default function OrderStatusPage() { const [activeTab, setActiveTab] = useState("reservation"); - const [orders, setOrders] = useState(INITIAL_RESERVATIONS); - const isOrderTabKey = (key: string): key is OrderTabKey => - key === "reservation" || key === "order"; const [acceptModal, setAcceptModal] = useState({ isOpen: false, @@ -30,18 +61,44 @@ export default function OrderStatusPage() { orderId: null, }); + const { + data: pendingReservationData, + isLoading: isPendingLoading, + isError: isPendingError, + } = usePendingReservationsQuery(); + + const { + data: processedReservationData, + isLoading: isProcessedLoading, + isError: isProcessedError, + } = useProcessedReservationsQuery(); + + const approveMutation = useApproveReservationMutation(); + const rejectMutation = useRejectReservationMutation(); + + const isOrderTabKey = (key: string): key is OrderTabKey => + key === "reservation" || key === "order"; + const reservationOrders = useMemo( - () => orders.filter((order) => order.status === "pending"), - [orders] + () => + pendingReservationData?.reservations.map(mapReservationToItem) ?? [], + [pendingReservationData], ); - const completedOrders = useMemo( - () => orders.filter((order) => order.status !== "pending"), - [orders] + const processedOrders = useMemo( + () => + processedReservationData?.reservations.map(mapReservationToItem) ?? [], + [processedReservationData], ); const currentOrders = - activeTab === "reservation" ? reservationOrders : completedOrders; + activeTab === "reservation" ? reservationOrders : processedOrders; + + const isLoading = + activeTab === "reservation" ? isPendingLoading : isProcessedLoading; + + const isError = + activeTab === "reservation" ? isPendingError : isProcessedError; const openAcceptModal = (orderId: number) => { setAcceptModal({ @@ -74,41 +131,31 @@ export default function OrderStatusPage() { const handleAcceptOrder = () => { if (acceptModal.orderId === null) return; - const now = formatProcessedAt(new Date()); - - setOrders((prev) => - prev.map((order) => - order.id === acceptModal.orderId - ? { - ...order, - status: "completed", - processedAt: now, - } - : order - ) + approveMutation.mutate( + { + reservationId: acceptModal.orderId, + }, + { + onSuccess: closeAcceptModal, + }, ); - - closeAcceptModal(); }; - const handleRejectOrder = (_reason: string) => { + const handleRejectOrder = (reason: string) => { if (rejectModal.orderId === null) return; - const now = formatProcessedAt(new Date()); - - setOrders((prev) => - prev.map((order) => - order.id === rejectModal.orderId - ? { - ...order, - status: "cancelled", - processedAt: now, - } - : order - ) + rejectMutation.mutate( + { + reservationId: rejectModal.orderId, + body: { + status: "REQUESTED", + rejectReason: reason, + }, + }, + { + onSuccess: closeRejectModal, + }, ); - - closeRejectModal(); }; return ( @@ -128,12 +175,24 @@ export default function OrderStatusPage() { />
- + {isLoading ? ( +
+

불러오는 중이에요.

+
+ ) : isError ? ( +
+

+ 주문 내역을 불러오지 못했어요. +

+
+ ) : ( + + )}
From bb3717613183ccac4e8a4f7c0a6e16776cf41a72 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: Mon, 4 May 2026 21:48:57 +0900 Subject: [PATCH 06/10] chore/store-apis --- packages/api/src/models/random-box.ts | 5 ++++- packages/api/src/models/store.ts | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/api/src/models/random-box.ts b/packages/api/src/models/random-box.ts index 64d66c5..2428930 100644 --- a/packages/api/src/models/random-box.ts +++ b/packages/api/src/models/random-box.ts @@ -1,3 +1,4 @@ +import { JsonValue } from "../core/types"; import type { SaleStatus } from "./common"; export interface RandomBoxCreateReqDTO { @@ -6,6 +7,7 @@ export interface RandomBoxCreateReqDTO { stock?: number; price?: number; buyLimit?: number; + pickupTimeInfo?: JsonValue; } export interface RandomBoxUpdateReqDTO extends RandomBoxCreateReqDTO { @@ -20,5 +22,6 @@ export interface RandomBoxRespDTO { price: number; buyLimit: number; content: string; - saleStatus: string; + saleStatus: SaleStatus | string; + pickupTimeInfo: string; } \ No newline at end of file diff --git a/packages/api/src/models/store.ts b/packages/api/src/models/store.ts index 96de341..80cf4c4 100644 --- a/packages/api/src/models/store.ts +++ b/packages/api/src/models/store.ts @@ -4,10 +4,11 @@ import type { StoreTag } from "./common"; export interface StoreUpdateReqDTO { storeName?: string; storeEmail?: string; - bankName?: string; + bankType?: string; depositor?: string; bankAccount?: string; businessHours?: JsonValue; + tag?: StoreTag; } export interface StoreLocationUpdateReqDTO { @@ -66,9 +67,9 @@ export interface SimpleStoreInfoDTO { storeId: number; tag: StoreTag; storeName: string; - storeEmail: string; roadAddress: string; jibunAddress: string; + storeEmail: string; businessHours?: JsonValue; } From 39b79fc5ee0a29e03c7711d5b1c0ffe611bd6dfa 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: Mon, 4 May 2026 21:49:17 +0900 Subject: [PATCH 07/10] chore/store-types --- .../signup/register/_types/address-search.ts | 8 ++ .../signup/register/_utils/business-hours.ts | 99 ++++++++++++--- apps/owner/src/shared/types/kakao.d.ts | 116 ++++++++++++++++++ apps/owner/src/shared/utils/bank.ts | 48 +++++--- 4 files changed, 235 insertions(+), 36 deletions(-) create mode 100644 apps/owner/src/app/signup/register/_types/address-search.ts create mode 100644 apps/owner/src/shared/types/kakao.d.ts diff --git a/apps/owner/src/app/signup/register/_types/address-search.ts b/apps/owner/src/app/signup/register/_types/address-search.ts new file mode 100644 index 0000000..6f8a069 --- /dev/null +++ b/apps/owner/src/app/signup/register/_types/address-search.ts @@ -0,0 +1,8 @@ +export interface AddressSearchItem { + id: string; + label: string; + lotNumberAddress: string; + roadAddress: string; + longitude: number; + latitude: number; +} \ 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 index 3cd536a..640cb1f 100644 --- a/apps/owner/src/app/signup/register/_utils/business-hours.ts +++ b/apps/owner/src/app/signup/register/_utils/business-hours.ts @@ -1,29 +1,96 @@ export type DayKey = "mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun"; -export type BusinessHoursValue = Record; +export type ServerDayKey = "MON" | "TUE" | "WED" | "THU" | "FRI" | "SAT" | "SUN"; + +export interface BreakTime { + start: string; + end: string; +} + +export interface BusinessDayValue { + open: string | null; + close: string | null; + "break-time": BreakTime | null; + closed: boolean; +} + +export type BusinessHoursValue = { + weekly: Record; +} & Record; + +const createBusinessDay = (closed = false): BusinessDayValue => ({ + open: closed ? null : "", + close: closed ? null : "", + "break-time": null, + closed, +}); export const EMPTY_BUSINESS_HOURS: BusinessHoursValue = { - mon: "", - tue: "", - wed: "", - thu: "", - fri: "", - sat: "", - sun: "", + weekly: { + MON: createBusinessDay(), + TUE: createBusinessDay(), + WED: createBusinessDay(), + THU: createBusinessDay(), + FRI: createBusinessDay(), + SAT: createBusinessDay(), + SUN: createBusinessDay(true), + }, +}; + +const normalizeBreakTime = (value: unknown): BreakTime | null => { + if (!value || typeof value !== "object") return null; + + const breakTime = value as Partial; + + if (typeof breakTime.start !== "string" || typeof breakTime.end !== "string") { + return null; + } + + return { + start: breakTime.start, + end: breakTime.end, + }; +}; + +const normalizeBusinessDay = ( + value: unknown, + defaultClosed = false, +): BusinessDayValue => { + if (!value || typeof value !== "object") { + return createBusinessDay(defaultClosed); + } + + const day = value as Partial; + + const closed = + typeof day.closed === "boolean" ? day.closed : defaultClosed; + + return { + open: closed ? null : typeof day.open === "string" ? day.open : "", + close: closed ? null : typeof day.close === "string" ? day.close : "", + "break-time": normalizeBreakTime(day["break-time"]), + closed, + }; }; export const parseBusinessHours = (value: unknown): BusinessHoursValue => { if (!value || typeof value !== "object") return EMPTY_BUSINESS_HOURS; - const obj = value as Partial>; + const obj = value as Partial; + + if (!obj.weekly || typeof obj.weekly !== "object") { + return EMPTY_BUSINESS_HOURS; + } 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 : "", + weekly: { + MON: normalizeBusinessDay(obj.weekly.MON), + TUE: normalizeBusinessDay(obj.weekly.TUE), + WED: normalizeBusinessDay(obj.weekly.WED), + THU: normalizeBusinessDay(obj.weekly.THU), + FRI: normalizeBusinessDay(obj.weekly.FRI), + SAT: normalizeBusinessDay(obj.weekly.SAT), + SUN: normalizeBusinessDay(obj.weekly.SUN, true), + }, }; }; \ No newline at end of file diff --git a/apps/owner/src/shared/types/kakao.d.ts b/apps/owner/src/shared/types/kakao.d.ts new file mode 100644 index 0000000..47cbb48 --- /dev/null +++ b/apps/owner/src/shared/types/kakao.d.ts @@ -0,0 +1,116 @@ +export {}; + +declare global { + interface Window { + kakao: { + maps: { + load: (callback: () => void) => void; + + LatLng: new (latitude: number, longitude: number) => KakaoLatLng; + + Map: new ( + container: HTMLElement, + options: { + center: KakaoLatLng; + level: number; + } + ) => KakaoMapInstance; + + Marker: new (options: { + map?: KakaoMapInstance | null; + position: KakaoLatLng; + title?: string; + image?: KakaoMarkerImage; + }) => KakaoMarker; + + MarkerImage: new ( + src: string, + size: KakaoSize, + options?: { + offset?: KakaoPoint; + alt?: string; + shape?: string; + coords?: string; + spriteOrigin?: KakaoPoint; + spriteSize?: KakaoSize; + } + ) => KakaoMarkerImage; + + Size: new (width: number, height: number) => KakaoSize; + + Point: new (x: number, y: number) => KakaoPoint; + + event: { + addListener: ( + target: object, + type: string, + handler: (...args: unknown[]) => void + ) => void; + removeListener: ( + target: object, + type: string, + handler: (...args: unknown[]) => void + ) => void; + }; + + services: { + Geocoder: new () => { + addressSearch: ( + address: string, + callback: ( + result: KakaoAddressSearchResult[], + status: string + ) => void, + options?: { + page?: number; + size?: number; + analyze_type?: string; + } + ) => void; + }; + + Status: { + OK: string; + ZERO_RESULT: string; + ERROR: string; + }; + + AnalyzeType: { + SIMILAR: string; + EXACT: string; + }; + }; + }; + }; + } + + interface KakaoLatLng {} + + interface KakaoMapInstance { + relayout: () => void; + setCenter: (latlng: KakaoLatLng) => void; + } + + interface KakaoMarker { + setMap: (map: KakaoMapInstance | null) => void; + setPosition: (position: KakaoLatLng) => void; + } + + interface KakaoMarkerImage {} + + interface KakaoSize {} + + interface KakaoPoint {} + + interface KakaoAddressSearchResult { + address_name: string; + x: string; + y: string; + address?: { + address_name: string; + }; + road_address?: { + address_name: string; + }; + } +} \ No newline at end of file diff --git a/apps/owner/src/shared/utils/bank.ts b/apps/owner/src/shared/utils/bank.ts index 209edc4..b8cb52d 100644 --- a/apps/owner/src/shared/utils/bank.ts +++ b/apps/owner/src/shared/utils/bank.ts @@ -1,26 +1,34 @@ -export const bankNameOptions = [ - "국민은행", - "신한은행", - "우리은행", - "하나은행", - "농협은행", - "기업은행", - "카카오뱅크", - "토스뱅크", - "케이뱅크", - "새마을금고", - "수협은행", - "부산은행", - "대구은행", - "광주은행", - "전북은행", - "경남은행", +export const bankOptions = [ + { label: "국민은행", value: "KB" }, + { label: "신한은행", value: "SHINHAN" }, + { label: "우리은행", value: "WOORI" }, + { label: "하나은행", value: "HANA" }, + { label: "농협은행", value: "NH" }, + { label: "기업은행", value: "IBK" }, + { label: "카카오뱅크", value: "KAKAO" }, + { label: "토스뱅크", value: "TOSS" }, + { label: "케이뱅크", value: "K" }, + { label: "새마을금고", value: "SAEMAUL" }, + { label: "수협은행", value: "SUHYUP" }, + { label: "부산은행", value: "BUSAN" }, + { label: "대구은행", value: "DAEGU" }, + { label: "광주은행", value: "GWANGJU" }, + { label: "전북은행", value: "JEONBUK" }, + { label: "경남은행", value: "KYONGNAM" }, ] as const; -export const filterBankNames = (keyword: string) => { +export type BankLabel = (typeof bankOptions)[number]["label"]; +export type BankValue = (typeof bankOptions)[number]["value"]; + +export const filterBanks = (keyword: string) => { const normalized = keyword.trim(); - if (!normalized) return bankNameOptions; - return bankNameOptions.filter((bank) => bank.includes(normalized)); + if (!normalized) return bankOptions; + + return bankOptions.filter((bank) => bank.label.includes(normalized)); +}; + +export const getBankLabel = (value: string) => { + return bankOptions.find((bank) => bank.value === value)?.label ?? ""; }; export const normalizeAccountNumber = (value: string) => { From 6ca0fc5dc6727e6ac0bb32b82bf5b7b6844716e2 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: Mon, 4 May 2026 21:49:33 +0900 Subject: [PATCH 08/10] feat/store-register-apis --- .../register/_apis/searchAddressBykakao.ts | 68 ++++++++++ .../_components/AddressSearchBottomSheet.tsx | 127 ++++++++++++++++++ .../_components/fields/AccountField.tsx | 31 ++--- .../_components/modals/RandomBoxModal.tsx | 48 +++++++ apps/owner/src/app/signup/register/page.tsx | 126 +++++++++++------ 5 files changed, 347 insertions(+), 53 deletions(-) create mode 100644 apps/owner/src/app/signup/register/_apis/searchAddressBykakao.ts create mode 100644 apps/owner/src/app/signup/register/_components/AddressSearchBottomSheet.tsx diff --git a/apps/owner/src/app/signup/register/_apis/searchAddressBykakao.ts b/apps/owner/src/app/signup/register/_apis/searchAddressBykakao.ts new file mode 100644 index 0000000..3da000e --- /dev/null +++ b/apps/owner/src/app/signup/register/_apis/searchAddressBykakao.ts @@ -0,0 +1,68 @@ +"use client"; + +import type { AddressSearchItem } from "../_types/address-search"; + +export function searchAddressByKakao( + keyword: string, + size = 10, +): Promise { + return new Promise((resolve, reject) => { + const trimmedKeyword = keyword.trim(); + + if (!trimmedKeyword) { + resolve([]); + return; + } + + if ( + typeof window === "undefined" || + !window.kakao?.maps?.services + ) { + reject(new Error("Kakao services library is not loaded.")); + return; + } + + const geocoder = new window.kakao.maps.services.Geocoder(); + + geocoder.addressSearch( + trimmedKeyword, + (result: KakaoAddressSearchResult[], status: string) => { + const { kakao } = window; + + if (status === kakao.maps.services.Status.ZERO_RESULT) { + resolve([]); + return; + } + + if (status !== kakao.maps.services.Status.OK) { + reject(new Error("주소 검색 중 오류가 발생했습니다.")); + return; + } + + const mapped = result.slice(0, size).map((item, index) => { + const lotNumberAddress = + item.address?.address_name || item.address_name || ""; + + const roadAddress = + item.road_address?.address_name || item.address_name || ""; + + return { + id: `${item.x}-${item.y}-${index}`, + label: roadAddress || lotNumberAddress, + lotNumberAddress, + roadAddress, + longitude: Number(item.x), + latitude: Number(item.y), + }; + }); + + resolve(mapped); + }, + { + page: 1, + size, + analyze_type: window.kakao.maps.services.AnalyzeType.SIMILAR, + }, + ); + }); +} \ No newline at end of file diff --git a/apps/owner/src/app/signup/register/_components/AddressSearchBottomSheet.tsx b/apps/owner/src/app/signup/register/_components/AddressSearchBottomSheet.tsx new file mode 100644 index 0000000..b5c5586 --- /dev/null +++ b/apps/owner/src/app/signup/register/_components/AddressSearchBottomSheet.tsx @@ -0,0 +1,127 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { BottomSheet, Input } from "@compasser/design-system"; +import type { AddressSearchItem } from "../_types/address-search"; +import { searchAddressByKakao } from "../_apis/searchAddressBykakao"; + +interface AddressSearchBottomSheetProps { + open: boolean; + onClose: () => void; + onSelectAddress: (item: AddressSearchItem) => void; +} + +export default function AddressSearchBottomSheet({ + open, + onClose, + onSelectAddress, +}: AddressSearchBottomSheetProps) { + const [keyword, setKeyword] = useState(""); + const [results, setResults] = useState([]); + const [isSearching, setIsSearching] = useState(false); + + const debounceRef = useRef(null); + + useEffect(() => { + if (!open) { + setKeyword(""); + setResults([]); + setIsSearching(false); + } + }, [open]); + + useEffect(() => { + if (!open) return; + + const trimmedKeyword = keyword.trim(); + + if (!trimmedKeyword) { + setResults([]); + setIsSearching(false); + return; + } + + if (debounceRef.current) { + window.clearTimeout(debounceRef.current); + } + + debounceRef.current = window.setTimeout(async () => { + try { + setIsSearching(true); + const searched = await searchAddressByKakao(trimmedKeyword, 10); + setResults(searched); + } catch (error) { + console.error(error); + setResults([]); + } finally { + setIsSearching(false); + } + }, 250); + + return () => { + if (debounceRef.current) { + window.clearTimeout(debounceRef.current); + } + }; + }, [keyword, open]); + + const handleSelectItem = (item: AddressSearchItem) => { + onSelectAddress(item); + onClose(); + }; + + return ( + + setKeyword(event.target.value)} + placeholder="주소를 입력해주세요" + /> + + {results.length > 0 && ( +
+ {results.map((item) => ( + + ))} +
+ )} + + {keyword.trim() && isSearching && ( +

+ 검색 중... +

+ )} + + {keyword.trim() && !isSearching && results.length === 0 && ( +

+ 검색 결과가 없습니다. +

+ )} +
+ ); +} \ No newline at end of file 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 fc612e2..dac4e5d 100644 --- a/apps/owner/src/app/signup/register/_components/fields/AccountField.tsx +++ b/apps/owner/src/app/signup/register/_components/fields/AccountField.tsx @@ -3,39 +3,40 @@ import { useMemo, useState } from "react"; import { Input, Tag } from "@compasser/design-system"; import { - filterBankNames, + filterBanks, + getBankLabel, normalizeAccountNumber, } from "@/shared/utils/bank"; type AccountStep = "bank" | "holder" | "account"; interface AccountFieldProps { - bankName: string; + bankType: string; depositor: string; bankAccount: string; - onChangeBankName: (value: string) => void; + onChangeBankType: (value: string) => void; onChangeDepositor: (value: string) => void; onChangeBankAccount: (value: string) => void; } export default function AccountField({ - bankName, + bankType, depositor, bankAccount, - onChangeBankName, + onChangeBankType, onChangeDepositor, onChangeBankAccount, }: AccountFieldProps) { const [step, setStep] = useState("bank"); - const [bankKeyword, setBankKeyword] = useState(bankName); + const [bankKeyword, setBankKeyword] = useState(getBankLabel(bankType)); const filteredBanks = useMemo(() => { - return filterBankNames(bankKeyword); + return filterBanks(bankKeyword); }, [bankKeyword]); const currentValue = step === "bank" - ? bankName + ? bankKeyword : step === "holder" ? depositor : bankAccount; @@ -49,8 +50,8 @@ export default function AccountField({ const handleChangeInput = (nextValue: string) => { if (step === "bank") { - onChangeBankName(nextValue); setBankKeyword(nextValue); + onChangeBankType(""); return; } @@ -62,9 +63,9 @@ export default function AccountField({ onChangeBankAccount(normalizeAccountNumber(nextValue)); }; - const handleSelectBank = (bank: string) => { - onChangeBankName(bank); - setBankKeyword(bank); + const handleSelectBank = (bank: { label: string; value: string }) => { + setBankKeyword(bank.label); + onChangeBankType(bank.value); }; return ( @@ -111,13 +112,13 @@ export default function AccountField({
{filteredBanks.map((bank) => ( handleSelectBank(bank)} > - {bank} + {bank.label} ))}
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 454caca..000220b 100644 --- a/apps/owner/src/app/signup/register/_components/modals/RandomBoxModal.tsx +++ b/apps/owner/src/app/signup/register/_components/modals/RandomBoxModal.tsx @@ -3,12 +3,18 @@ import { useEffect, useState } from "react"; import { Button, Input, Modal, cn } from "@compasser/design-system"; +interface PickupTimeInfo { + start: string; + end: string; +} + interface RandomBoxFormValue { boxName: string; stock: number; price: number; buyLimit: number; content: string; + pickupTimeInfo: PickupTimeInfo; boxId?: number; } @@ -25,6 +31,10 @@ const initialFormValue: RandomBoxFormValue = { price: 0, buyLimit: 0, content: "", + pickupTimeInfo: { + start: "", + end: "", + }, }; export default function RandomBoxModal({ @@ -59,11 +69,23 @@ export default function RandomBoxModal({ })); }; + const updatePickupTime = (key: keyof PickupTimeInfo, value: string) => { + setForm((prev) => ({ + ...prev, + pickupTimeInfo: { + ...prev.pickupTimeInfo, + [key]: value, + }, + })); + }; + const handleSubmit = async () => { if (!form.boxName.trim()) return; if (form.stock <= 0) return; if (form.price < 0) return; if (form.buyLimit <= 0) return; + if (!form.pickupTimeInfo.start) return; + if (!form.pickupTimeInfo.end) return; await onSubmit?.(form); onClose(); @@ -136,6 +158,32 @@ export default function RandomBoxModal({ /> +
+

픽업 시작 시간

+ + updatePickupTime("start", event.target.value)} + className="w-[17rem]" + containerClassName="border-gray-300" + inputClassName="text-gray-600" + /> +
+ +
+

픽업 종료 시간

+ + updatePickupTime("end", event.target.value)} + className="w-[17rem]" + containerClassName="border-gray-300" + inputClassName="text-gray-600" + /> +
+

랜덤박스 설명란

diff --git a/apps/owner/src/app/signup/register/page.tsx b/apps/owner/src/app/signup/register/page.tsx index 0ce9159..67af166 100644 --- a/apps/owner/src/app/signup/register/page.tsx +++ b/apps/owner/src/app/signup/register/page.tsx @@ -6,6 +6,8 @@ import { useRouter } from "next/navigation"; import type { StoreUpdateReqDTO, StoreLocationUpdateReqDTO, + StoreTag, + BankType, } from "@compasser/api"; import StoreNameField from "./_components/fields/StoreNameField"; @@ -19,13 +21,14 @@ import TagSection from "./_components/sections/TagSection"; import RegisterSubmitButton from "./_components/RegisterSubmitButton"; import BusinessHoursModal from "./_components/modals/BusinessHoursModal"; import RandomBoxModal from "./_components/modals/RandomBoxModal"; - +import AddressSearchBottomSheet from "./_components/AddressSearchBottomSheet"; +import type { AddressSearchItem } from "./_types/address-search"; 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 { 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"; @@ -33,6 +36,22 @@ import { useRemoveStoreImageMutation } from "@/shared/queries/mutation/auth/useR import { parseBusinessHours, EMPTY_BUSINESS_HOURS } from "./_utils/business-hours"; +const tagOptions = ["카페", "베이커리", "식당"] as const; + +type TagLabel = (typeof tagOptions)[number]; + +const tagMap: Record = { + 카페: "CAFE", + 베이커리: "BAKERY", + 식당: "RESTAURANT", +}; + +const reverseTagMap: Record = { + CAFE: "카페", + BAKERY: "베이커리", + RESTAURANT: "식당", +}; + export default function StoreRegisterPage() { const router = useRouter(); @@ -45,7 +64,7 @@ export default function StoreRegisterPage() { const patchMyStoreMutation = usePatchMyStoreMutation(); const patchMyStoreLocationMutation = usePatchMyStoreLocationMutation(); const createRandomBoxMutation = useCreateRandomBoxMutation(); - const updateRandomBoxMutation = useUpdateRandomBoxMutation(); + // const updateRandomBoxMutation = useUpdateRandomBoxMutation(); const deleteRandomBoxMutation = useDeleteRandomBoxMutation(); const uploadStoreImageMutation = useUploadStoreImageMutation(); const removeStoreImageMutation = useRemoveStoreImageMutation(); @@ -53,13 +72,13 @@ export default function StoreRegisterPage() { const [storeName, setStoreName] = useState(""); const [storeEmail, setStoreEmail] = useState(""); const [inputAddress, setInputAddress] = useState(""); - const [bankName, setBankName] = useState(""); + const [bankType, setBankType] = useState(""); + const [isAddressSearchOpen, setIsAddressSearchOpen] = useState(false); const [depositor, setDepositor] = useState(""); const [bankAccount, setBankAccount] = useState(""); const [businessHours, setBusinessHours] = useState(EMPTY_BUSINESS_HOURS); - const tagOptions = ["카페", "베이커리", "식당"] as const; - const [selectedTag, setSelectedTag] = useState<"" | "카페" | "베이커리" | "식당">(""); + const [selectedTag, setSelectedTag] = useState<"" | TagLabel>(""); const [selectedRandomBoxIds, setSelectedRandomBoxIds] = useState([]); const [isBusinessHoursModalOpen, setIsBusinessHoursModalOpen] = useState(false); const [isRandomBoxModalOpen, setIsRandomBoxModalOpen] = useState(false); @@ -69,12 +88,16 @@ export default function StoreRegisterPage() { const [imageRemoved, setImageRemoved] = useState(false); useEffect(() => { - if (!myStore) return; + if (!myStore) return; setStoreName(myStore.storeName ?? ""); - setStoreEmail(""); + setStoreEmail(myStore.storeEmail ?? ""); setInputAddress(myStore.inputAddress ?? ""); setBusinessHours(parseBusinessHours(myStore.businessHours)); + + if (myStore.tag) { + setSelectedTag(reverseTagMap[myStore.tag as StoreTag] ?? ""); + } }, [myStore]); useEffect(() => { @@ -83,6 +106,16 @@ export default function StoreRegisterPage() { }, [storeImage, imageRemoved]); const businessHoursRows = useMemo(() => { + const dayKeyMap = { + mon: "MON", + tue: "TUE", + wed: "WED", + thu: "THU", + fri: "FRI", + sat: "SAT", + sun: "SUN", + } as const; + const dayLabelMap = { mon: "월", tue: "화", @@ -96,8 +129,11 @@ export default function StoreRegisterPage() { const orderedDays = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] as const; return orderedDays.map((day) => { - const value = businessHours[day]; - const formatted = value === "closed" ? "휴무" : value || "-"; + const value = businessHours.weekly[dayKeyMap[day]]; + + const formatted = value.closed + ? "휴무" + : `${value.open || "-"} - ${value.close || "-"}`; return { dayLabel: dayLabelMap[day], @@ -106,9 +142,14 @@ export default function StoreRegisterPage() { }); }, [businessHours]); + const handleSelectAddress = (item: AddressSearchItem) => { + setInputAddress(item.roadAddress || item.lotNumberAddress || item.label); + setIsAddressSearchOpen(false); + }; + 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], ); }; @@ -117,8 +158,8 @@ export default function StoreRegisterPage() { await Promise.all( selectedRandomBoxIds.map((boxId) => - deleteRandomBoxMutation.mutateAsync({ storeId, boxId }) - ) + deleteRandomBoxMutation.mutateAsync({ storeId, boxId }), + ), ); setSelectedRandomBoxIds([]); @@ -130,34 +171,35 @@ export default function StoreRegisterPage() { price: number; buyLimit: number; content: string; + pickupTimeInfo: { + start: string; + end: string; + }; boxId?: number; }) => { if (!storeId) return; - 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 body = { + boxName: form.boxName, + stock: form.stock, + price: form.price, + buyLimit: form.buyLimit, + content: form.content, + pickupTimeInfo: form.pickupTimeInfo, + }; + + // if (form.boxId) { + // await updateRandomBoxMutation.mutateAsync({ + // storeId, + // boxId: form.boxId, + // body, + // }); + // return; + // } await createRandomBoxMutation.mutateAsync({ storeId, - body: { - boxName: form.boxName, - stock: form.stock, - price: form.price, - buyLimit: form.buyLimit, - content: form.content, - }, + body, }); }; @@ -188,10 +230,11 @@ export default function StoreRegisterPage() { const storePayload: StoreUpdateReqDTO = { storeName, storeEmail, - bankName, + bankType: bankType || undefined, depositor, bankAccount, businessHours, + tag: selectedTag ? tagMap[selectedTag] : undefined, }; const locationPayload: StoreLocationUpdateReqDTO = { @@ -222,13 +265,14 @@ export default function StoreRegisterPage() { {}} + onSearchAddress={() => setIsAddressSearchOpen(true)} /> + setBankType(value as BankType)} onChangeDepositor={setDepositor} onChangeBankAccount={setBankAccount} /> @@ -275,6 +319,12 @@ export default function StoreRegisterPage() { onClose={() => setIsRandomBoxModalOpen(false)} onSubmit={handleSubmitRandomBox} /> + + setIsAddressSearchOpen(false)} + onSelectAddress={handleSelectAddress} + /> ); } \ No newline at end of file From 7e37399fe9c3f44bf9015d9ea6a4bc587719037f 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: Mon, 4 May 2026 21:51:32 +0900 Subject: [PATCH 09/10] refactor/store-info --- .../src/app/(tabs)/mypage/store-info/page.tsx | 127 ++++++++++-------- 1 file changed, 68 insertions(+), 59 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 d987eba..3dcf797 100644 --- a/apps/owner/src/app/(tabs)/mypage/store-info/page.tsx +++ b/apps/owner/src/app/(tabs)/mypage/store-info/page.tsx @@ -6,6 +6,8 @@ import { Button, Header } from "@compasser/design-system"; import type { StoreUpdateReqDTO, StoreLocationUpdateReqDTO, + StoreTag, + BankType, } from "@compasser/api"; import StoreNameField from "@/app/signup/register/_components/fields/StoreNameField"; @@ -18,13 +20,14 @@ 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 AddressSearchBottomSheet from "@/app/signup/register/_components/AddressSearchBottomSheet"; +import type { AddressSearchItem } from "@/app/signup/register/_types/address-search"; 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"; @@ -35,7 +38,21 @@ import { EMPTY_BUSINESS_HOURS, } from "@/app/signup/register/_utils/business-hours"; -type FixedTag = "카페" | "베이커리" | "식당"; +const tagOptions = ["카페", "베이커리", "식당"] as const; + +type TagLabel = (typeof tagOptions)[number]; + +const tagMap: Record = { + 카페: "CAFE", + 베이커리: "BAKERY", + 식당: "RESTAURANT", +}; + +const reverseTagMap: Record = { + CAFE: "카페", + BAKERY: "베이커리", + RESTAURANT: "식당", +}; export default function StoreInfoEditPage() { const router = useRouter(); @@ -49,7 +66,6 @@ export default function StoreInfoEditPage() { const patchMyStoreMutation = usePatchMyStoreMutation(); const patchMyStoreLocationMutation = usePatchMyStoreLocationMutation(); const createRandomBoxMutation = useCreateRandomBoxMutation(); - const updateRandomBoxMutation = useUpdateRandomBoxMutation(); const deleteRandomBoxMutation = useDeleteRandomBoxMutation(); const uploadStoreImageMutation = useUploadStoreImageMutation(); const removeStoreImageMutation = useRemoveStoreImageMutation(); @@ -57,46 +73,32 @@ export default function StoreInfoEditPage() { const [storeName, setStoreName] = useState(""); const [storeEmail, setStoreEmail] = useState(""); const [inputAddress, setInputAddress] = useState(""); - const [bankName, setBankName] = useState(""); + const [bankType, setBankType] = 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 [selectedTag, setSelectedTag] = useState<"" | TagLabel>(""); + const [selectedRandomBoxIds, setSelectedRandomBoxIds] = useState([]); + const [isBusinessHoursModalOpen, setIsBusinessHoursModalOpen] = useState(false); const [isRandomBoxModalOpen, setIsRandomBoxModalOpen] = useState(false); + const [isAddressSearchOpen, setIsAddressSearchOpen] = useState(false); const [photoFile, setPhotoFile] = useState(null); 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(""); + setStoreEmail(myStore.storeEmail ?? ""); setInputAddress(myStore.inputAddress ?? ""); setBusinessHours(parseBusinessHours(myStore.businessHours)); - setSelectedTag(mapStoreTagToLabel(myStore.tag)); + + if (myStore.tag) { + setSelectedTag(reverseTagMap[myStore.tag as StoreTag] ?? ""); + } }, [myStore]); useEffect(() => { @@ -105,6 +107,16 @@ export default function StoreInfoEditPage() { }, [storeImage, imageRemoved]); const businessHoursRows = useMemo(() => { + const dayKeyMap = { + mon: "MON", + tue: "TUE", + wed: "WED", + thu: "THU", + fri: "FRI", + sat: "SAT", + sun: "SUN", + } as const; + const dayLabelMap = { mon: "월", tue: "화", @@ -115,19 +127,14 @@ export default function StoreInfoEditPage() { sun: "일", } as const; - const orderedDays = [ - "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 = businessHours[day]; - const formatted = value === "closed" ? "휴무" : value || "-"; + const value = businessHours.weekly[dayKeyMap[day]]; + + const formatted = value.closed + ? "휴무" + : `${value.open || "-"} - ${value.close || "-"}`; return { dayLabel: dayLabelMap[day], @@ -136,6 +143,11 @@ export default function StoreInfoEditPage() { }); }, [businessHours]); + const handleSelectAddress = (item: AddressSearchItem) => { + setInputAddress(item.roadAddress || item.lotNumberAddress || item.label); + setIsAddressSearchOpen(false); + }; + const toggleRandomBoxSelection = (id: number) => { setSelectedRandomBoxIds((prev) => prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id], @@ -160,25 +172,13 @@ export default function StoreInfoEditPage() { price: number; buyLimit: number; content: string; - boxId?: number; + pickupTimeInfo: { + start: string; + end: string; + }; }) => { if (!storeId) return; - 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; - } - await createRandomBoxMutation.mutateAsync({ storeId, body: { @@ -187,6 +187,7 @@ export default function StoreInfoEditPage() { price: form.price, buyLimit: form.buyLimit, content: form.content, + pickupTimeInfo: form.pickupTimeInfo, }, }); }; @@ -218,10 +219,11 @@ export default function StoreInfoEditPage() { const storePayload: StoreUpdateReqDTO = { storeName, storeEmail, - bankName, + bankType: bankType || undefined, depositor, bankAccount, businessHours, + tag: selectedTag ? tagMap[selectedTag] : undefined, }; const locationPayload: StoreLocationUpdateReqDTO = { @@ -256,17 +258,18 @@ export default function StoreInfoEditPage() {
+ {}} + onSearchAddress={() => setIsAddressSearchOpen(true)} /> setBankType(value as BankType)} onChangeDepositor={setDepositor} onChangeBankAccount={setBankAccount} /> @@ -293,7 +296,7 @@ export default function StoreInfoEditPage() { /> @@ -326,6 +329,12 @@ export default function StoreInfoEditPage() { onClose={() => setIsRandomBoxModalOpen(false)} onSubmit={handleSubmitRandomBox} /> + + setIsAddressSearchOpen(false)} + onSelectAddress={handleSelectAddress} + /> ); } \ No newline at end of file From e6c2add67b6cdb44b4c1409615ceeee864e90f9f 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: Mon, 4 May 2026 22:00:05 +0900 Subject: [PATCH 10/10] chore/business-hour-modal --- .../_components/modals/BusinessHoursModal.tsx | 317 ++++++++++++------ 1 file changed, 213 insertions(+), 104 deletions(-) diff --git a/apps/owner/src/app/signup/register/_components/modals/BusinessHoursModal.tsx b/apps/owner/src/app/signup/register/_components/modals/BusinessHoursModal.tsx index 013de88..7d1dcbe 100644 --- a/apps/owner/src/app/signup/register/_components/modals/BusinessHoursModal.tsx +++ b/apps/owner/src/app/signup/register/_components/modals/BusinessHoursModal.tsx @@ -6,8 +6,10 @@ import TimeRangeField from "./TimeRangeField"; import { EMPTY_BUSINESS_HOURS, parseBusinessHours, + type BusinessDayValue, type BusinessHoursValue, type DayKey, + type ServerDayKey, } from "../../_utils/business-hours"; type BreakTimeOption = "yes" | "no" | null; @@ -15,6 +17,7 @@ type BreakTimeOption = "yes" | "no" | null; interface BusinessHourFormValue { openTime: string; closeTime: string; + closed: boolean; hasBreakTime: BreakTimeOption; breakStartTime: string; breakEndTime: string; @@ -27,19 +30,24 @@ interface BusinessHoursModalProps { onSubmit?: (data: BusinessHoursValue) => void; } -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: "일" }, +const dayOptions: { + key: DayKey; + serverKey: ServerDayKey; + label: string; +}[] = [ + { key: "mon", serverKey: "MON", label: "월" }, + { key: "tue", serverKey: "TUE", label: "화" }, + { key: "wed", serverKey: "WED", label: "수" }, + { key: "thu", serverKey: "THU", label: "목" }, + { key: "fri", serverKey: "FRI", label: "금" }, + { key: "sat", serverKey: "SAT", label: "토" }, + { key: "sun", serverKey: "SUN", label: "일" }, ]; const initialBusinessHourFormValue: BusinessHourFormValue = { openTime: "", closeTime: "", + closed: false, hasBreakTime: null, breakStartTime: "", breakEndTime: "", @@ -52,37 +60,64 @@ const getEmptyBusinessHoursForm = (): Record => ( thu: { ...initialBusinessHourFormValue }, fri: { ...initialBusinessHourFormValue }, sat: { ...initialBusinessHourFormValue }, - sun: { ...initialBusinessHourFormValue }, + sun: { + ...initialBusinessHourFormValue, + closed: true, + }, }); +const getDayOption = (day: DayKey) => { + return dayOptions.find((option) => option.key === day); +}; + const toFormValue = ( source?: BusinessHoursValue, ): Record => { const normalized = parseBusinessHours(source); const base = getEmptyBusinessHoursForm(); - (Object.keys(base) as DayKey[]).forEach((day) => { - const value = normalized[day]; - - if (!value || value === "closed") { - return; - } - - const [open, close] = value.split("-"); + dayOptions.forEach(({ key, serverKey }) => { + const value = normalized.weekly[serverKey]; - base[day] = { - ...base[day], - openTime: open ?? "", - closeTime: close ?? "", - hasBreakTime: "no", - breakStartTime: "", - breakEndTime: "", + base[key] = { + openTime: value.open ?? "", + closeTime: value.close ?? "", + closed: value.closed, + hasBreakTime: value["break-time"] ? "yes" : "no", + breakStartTime: value["break-time"]?.start ?? "", + breakEndTime: value["break-time"]?.end ?? "", }; }); return base; }; +const toBusinessDayValue = ( + value: BusinessHourFormValue, +): BusinessDayValue => { + if (value.closed) { + return { + open: null, + close: null, + "break-time": null, + closed: true, + }; + } + + return { + open: value.openTime, + close: value.closeTime, + "break-time": + value.hasBreakTime === "yes" + ? { + start: value.breakStartTime, + end: value.breakEndTime, + } + : null, + closed: false, + }; +}; + export default function BusinessHoursModal({ open, onClose, @@ -105,7 +140,7 @@ export default function BusinessHoursModal({ const updateSelectedDayField = ( key: keyof BusinessHourFormValue, - value: string | BreakTimeOption, + value: string | boolean | BreakTimeOption, ) => { setBusinessHoursForm((prev) => ({ ...prev, @@ -116,6 +151,25 @@ export default function BusinessHoursModal({ })); }; + const handleChangeClosed = (closed: boolean) => { + setBusinessHoursForm((prev) => ({ + ...prev, + [selectedDay]: { + ...prev[selectedDay], + closed, + ...(closed + ? { + openTime: "", + closeTime: "", + hasBreakTime: "no" as BreakTimeOption, + breakStartTime: "", + breakEndTime: "", + } + : {}), + }, + })); + }; + const handleChangeBreakTimeOption = (value: BreakTimeOption) => { setBusinessHoursForm((prev) => { const current = prev[selectedDay]; @@ -137,6 +191,8 @@ export default function BusinessHoursModal({ }; const isDayCompleted = (value: BusinessHourFormValue) => { + if (value.closed) return true; + const hasOpenClose = value.openTime.trim() !== "" && value.closeTime.trim() !== ""; @@ -157,26 +213,26 @@ export default function BusinessHoursModal({ }, [businessHoursForm]); const isRegisterButtonEnabled = useMemo(() => { - return dayOptions.some((day) => completedDays[day.key]); + return dayOptions.every((day) => completedDays[day.key]); }, [completedDays]); const handleSubmit = () => { if (!isRegisterButtonEnabled) return; - const formatted = (Object.keys(businessHoursForm) as DayKey[]).reduce( - (acc, day) => { - const value = businessHoursForm[day]; + const formatted: BusinessHoursValue = { + weekly: { + ...EMPTY_BUSINESS_HOURS.weekly, + }, + }; - if (!isDayCompleted(value)) { - acc[day] = ""; - return acc; - } + (Object.keys(businessHoursForm) as DayKey[]).forEach((day) => { + const dayOption = getDayOption(day); + if (!dayOption) return; - acc[day] = `${value.openTime}-${value.closeTime}`; - return acc; - }, - { ...EMPTY_BUSINESS_HOURS }, - ); + formatted.weekly[dayOption.serverKey] = toBusinessDayValue( + businessHoursForm[day], + ); + }); onSubmit?.(formatted); onClose(); @@ -195,6 +251,7 @@ export default function BusinessHoursModal({
{dayOptions.map((day) => { const isSelected = selectedDay === day.key; + const isCompleted = completedDays[day.key]; return ( - - + + -
+ 휴무 + +
+
+ + {!selectedDayValue.closed && ( + <> +
+

영업시간

- {selectedDayValue.hasBreakTime === "yes" && ( -
- updateSelectedDayField("breakStartTime", value) + updateSelectedDayField("openTime", value) } onChangeEndTime={(value) => - updateSelectedDayField("breakEndTime", value) + updateSelectedDayField("closeTime", value) } + className="ml-[5.2rem]" />
- )} -
+ +
+
+

브레이크타임

+ +
+ + + +
+
+ + {selectedDayValue.hasBreakTime === "yes" && ( +
+ + updateSelectedDayField("breakStartTime", value) + } + onChangeEndTime={(value) => + updateSelectedDayField("breakEndTime", value) + } + /> +
+ )} +
+ + )}