From 8aa1c55c374416632560aec6eb9360d3366649e2 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: Tue, 31 Mar 2026 18:48:57 +0900 Subject: [PATCH] setting/packages/query-factory --- apps/customer/package.json | 1 + apps/owner/package.json | 1 + packages/api/package.json | 24 +++ packages/api/src/core/http.ts | 120 +++++++++++++ packages/api/src/core/mutation.ts | 135 ++++++++++++++ packages/api/src/core/query-client.ts | 8 + packages/api/src/core/token-store.ts | 33 ++++ packages/api/src/core/types.ts | 43 +++++ packages/api/src/domains/auth.ts | 123 +++++++++++++ packages/api/src/domains/health.ts | 25 +++ packages/api/src/domains/index.ts | 24 +++ packages/api/src/domains/member.ts | 72 ++++++++ packages/api/src/domains/order.ts | 92 ++++++++++ packages/api/src/domains/owner.ts | 182 +++++++++++++++++++ packages/api/src/domains/payment.ts | 197 +++++++++++++++++++++ packages/api/src/domains/random-box.ts | 150 ++++++++++++++++ packages/api/src/domains/store-image.ts | 85 +++++++++ packages/api/src/domains/store-manager.ts | 63 +++++++ packages/api/src/domains/store.ts | 205 ++++++++++++++++++++++ packages/api/src/index.ts | 25 +++ packages/api/src/models/auth.ts | 29 +++ packages/api/src/models/common.ts | 25 +++ packages/api/src/models/member.ts | 43 +++++ packages/api/src/models/order.ts | 62 +++++++ packages/api/src/models/owner.ts | 43 +++++ packages/api/src/models/payment.ts | 19 ++ packages/api/src/models/random-box.ts | 24 +++ packages/api/src/models/store-image.ts | 7 + packages/api/src/models/store.ts | 70 ++++++++ packages/api/tsconfig.json | 8 + pnpm-lock.yaml | 101 +++++++++++ 31 files changed, 2039 insertions(+) create mode 100644 packages/api/package.json create mode 100644 packages/api/src/core/http.ts create mode 100644 packages/api/src/core/mutation.ts create mode 100644 packages/api/src/core/query-client.ts create mode 100644 packages/api/src/core/token-store.ts create mode 100644 packages/api/src/core/types.ts create mode 100644 packages/api/src/domains/auth.ts create mode 100644 packages/api/src/domains/health.ts create mode 100644 packages/api/src/domains/index.ts create mode 100644 packages/api/src/domains/member.ts create mode 100644 packages/api/src/domains/order.ts create mode 100644 packages/api/src/domains/owner.ts create mode 100644 packages/api/src/domains/payment.ts create mode 100644 packages/api/src/domains/random-box.ts create mode 100644 packages/api/src/domains/store-image.ts create mode 100644 packages/api/src/domains/store-manager.ts create mode 100644 packages/api/src/domains/store.ts create mode 100644 packages/api/src/index.ts create mode 100644 packages/api/src/models/auth.ts create mode 100644 packages/api/src/models/common.ts create mode 100644 packages/api/src/models/member.ts create mode 100644 packages/api/src/models/order.ts create mode 100644 packages/api/src/models/owner.ts create mode 100644 packages/api/src/models/payment.ts create mode 100644 packages/api/src/models/random-box.ts create mode 100644 packages/api/src/models/store-image.ts create mode 100644 packages/api/src/models/store.ts create mode 100644 packages/api/tsconfig.json diff --git a/apps/customer/package.json b/apps/customer/package.json index f3c82db..3de07c4 100644 --- a/apps/customer/package.json +++ b/apps/customer/package.json @@ -8,6 +8,7 @@ "type-check": "tsc --noEmit" }, "dependencies": { + "@compasser/api": "workspace:^", "@compasser/design-system": "workspace:*", "@tanstack/react-query": "^5.90.21", "next": "16.1.6", diff --git a/apps/owner/package.json b/apps/owner/package.json index 7c39b42..4acf822 100644 --- a/apps/owner/package.json +++ b/apps/owner/package.json @@ -8,6 +8,7 @@ "type-check": "tsc --noEmit" }, "dependencies": { + "@compasser/api": "workspace:^", "@compasser/design-system": "workspace:*", "@tanstack/react-query": "^5.90.21", "next": "16.1.6", diff --git a/packages/api/package.json b/packages/api/package.json new file mode 100644 index 0000000..5f4eb6a --- /dev/null +++ b/packages/api/package.json @@ -0,0 +1,24 @@ +{ + "name": "@compasser/api", + "version": "0.1.0", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@tanstack/react-query": "^5.90.21", + "axios": "^1.14.0" + }, + "peerDependencies": { + "react": "^19", + "react-dom": "^19" + }, + "devDependencies": { + "@compasser/typescript-config": "workspace:*", + "typescript": "^5.9.2" + } +} diff --git a/packages/api/src/core/http.ts b/packages/api/src/core/http.ts new file mode 100644 index 0000000..8dcf245 --- /dev/null +++ b/packages/api/src/core/http.ts @@ -0,0 +1,120 @@ +import axios, { AxiosError } from "axios"; +import type { InternalAxiosRequestConfig } from "axios"; +import type { CompasserApi, CreateCompasserApiOptions, TokenPair } from "./types"; + +interface FailedQueueItem { + resolve: (token: string) => void; + reject: (error: unknown) => void; +} + +const processQueue = ( + queue: FailedQueueItem[], + error: unknown, + token: string | null, +) => { + queue.forEach((item) => { + if (error) { + item.reject(error); + return; + } + + item.resolve(token ?? ""); + }); +}; + +export const createCompasserApi = ( + options: CreateCompasserApiOptions, +): CompasserApi => { + const authScheme = options.authScheme ?? "Bearer"; + const withCredentials = options.withCredentials ?? true; + + const publicClient = axios.create({ + baseURL: options.baseURL, + withCredentials, + }); + + const privateClient = axios.create({ + baseURL: options.baseURL, + withCredentials, + }); + + privateClient.interceptors.request.use( + (config) => { + const accessToken = options.tokenStore.getAccessToken(); + if (accessToken) { + config.headers.Authorization = `${authScheme} ${accessToken}`; + } + return config; + }, + (error) => Promise.reject(error), + ); + + let isRefreshing = false; + let failedQueue: FailedQueueItem[] = []; + + privateClient.interceptors.response.use( + (response) => response, + async (error: AxiosError) => { + const originalRequest = error.config as InternalAxiosRequestConfig & { + _retry?: boolean; + }; + + if ( + error.response?.status !== 401 || + originalRequest?._retry || + !options.refreshTokens + ) { + return Promise.reject(error); + } + + if (isRefreshing) { + return new Promise((resolve, reject) => { + failedQueue.push({ + resolve: (token) => { + originalRequest.headers.Authorization = `${authScheme} ${token}`; + resolve(privateClient(originalRequest)); + }, + reject, + }); + }); + } + + originalRequest._retry = true; + isRefreshing = true; + + try { + const nextTokens: TokenPair = await options.refreshTokens({ + publicClient, + refreshToken: options.tokenStore.getRefreshToken(), + originalRequest, + }); + + options.tokenStore.setTokens(nextTokens); + privateClient.defaults.headers.common.Authorization = + `${authScheme} ${nextTokens.accessToken}`; + + processQueue(failedQueue, null, nextTokens.accessToken); + failedQueue = []; + + originalRequest.headers.Authorization = + `${authScheme} ${nextTokens.accessToken}`; + + return privateClient(originalRequest); + } catch (refreshError) { + processQueue(failedQueue, refreshError, null); + failedQueue = []; + options.tokenStore.clearTokens(); + options.onAuthFailure?.(refreshError); + return Promise.reject(refreshError); + } finally { + isRefreshing = false; + } + }, + ); + + return { + publicClient, + privateClient, + tokenStore: options.tokenStore, + }; +}; \ No newline at end of file diff --git a/packages/api/src/core/mutation.ts b/packages/api/src/core/mutation.ts new file mode 100644 index 0000000..816cef4 --- /dev/null +++ b/packages/api/src/core/mutation.ts @@ -0,0 +1,135 @@ +import { + mutationOptions, + type MutationKey, + type QueryClient, + type QueryKey, +} from "@tanstack/react-query"; + +type MaybePromise = T | Promise; + +export type CacheAction = + | { + type: "set"; + queryKey: QueryKey; + value: unknown | ((data: TData, variables: TVariables) => unknown); + } + | { + type: "merge"; + queryKey: QueryKey; + updater: ( + oldData: unknown, + data: TData, + variables: TVariables, + ) => unknown; + } + | { + type: "invalidate"; + queryKey: QueryKey; + } + | { + type: "remove"; + queryKey: QueryKey; + }; + +export interface CreateMutationWithCacheParams< + TData, + TVariables, + TContext = unknown, + TError = unknown, +> { + queryClient: QueryClient; + mutationKey?: MutationKey; + mutationFn: (variables: TVariables) => Promise; + getActions?: ( + data: TData, + variables: TVariables, + ) => CacheAction[]; + onMutate?: (variables: TVariables) => MaybePromise; + onError?: ( + error: TError, + variables: TVariables, + context: TContext | undefined, + ) => MaybePromise; + onSuccess?: ( + data: TData, + variables: TVariables, + context: TContext | undefined, + ) => MaybePromise; + onSettled?: ( + data: TData | undefined, + error: TError | null, + variables: TVariables, + context: TContext | undefined, + ) => MaybePromise; +} + +const runCacheActions = async ( + queryClient: QueryClient, + actions: CacheAction[], + data: TData, + variables: TVariables, +) => { + for (const action of actions) { + if (action.type === "set") { + const nextValue = + typeof action.value === "function" + ? action.value(data, variables) + : action.value; + + queryClient.setQueryData(action.queryKey, nextValue); + continue; + } + + if (action.type === "merge") { + queryClient.setQueryData(action.queryKey, (oldData: unknown) => + action.updater(oldData, data, variables), + ); + continue; + } + + if (action.type === "invalidate") { + await queryClient.invalidateQueries({ + queryKey: action.queryKey, + }); + continue; + } + + if (action.type === "remove") { + queryClient.removeQueries({ + queryKey: action.queryKey, + }); + } + } +}; + +export const createMutationWithCache = < + TData, + TVariables, + TContext = unknown, + TError = unknown, +>({ + queryClient, + mutationKey, + mutationFn, + getActions, + onMutate, + onError, + onSuccess, + onSettled, +}: CreateMutationWithCacheParams) => + mutationOptions({ + ...(mutationKey ? { mutationKey } : {}), + mutationFn, + onMutate, + onError, + onSuccess: async (data, variables, context) => { + const actions = getActions?.(data, variables) ?? []; + + if (actions.length > 0) { + await runCacheActions(queryClient, actions, data, variables); + } + + await onSuccess?.(data, variables, context); + }, + onSettled, + }); \ No newline at end of file diff --git a/packages/api/src/core/query-client.ts b/packages/api/src/core/query-client.ts new file mode 100644 index 0000000..db93bfb --- /dev/null +++ b/packages/api/src/core/query-client.ts @@ -0,0 +1,8 @@ +import type { QueryClient } from "@tanstack/react-query"; + +export const invalidatePrefix = async ( + queryClient: QueryClient, + queryKey: readonly unknown[], +) => { + await queryClient.invalidateQueries({ queryKey }); +}; \ No newline at end of file diff --git a/packages/api/src/core/token-store.ts b/packages/api/src/core/token-store.ts new file mode 100644 index 0000000..d02946c --- /dev/null +++ b/packages/api/src/core/token-store.ts @@ -0,0 +1,33 @@ +import type { TokenPair, TokenStore } from "./types"; + +export interface LocalStorageTokenStoreOptions { + accessTokenKey?: string; + refreshTokenKey?: string; +} + +const isBrowser = () => + typeof window !== "undefined" && typeof window.localStorage !== "undefined"; + +export const createLocalStorageTokenStore = ( + options: LocalStorageTokenStoreOptions = {}, +): TokenStore => { + const accessTokenKey = options.accessTokenKey ?? "accessToken"; + const refreshTokenKey = options.refreshTokenKey ?? "refreshToken"; + + return { + getAccessToken: () => (isBrowser() ? window.localStorage.getItem(accessTokenKey) : null), + getRefreshToken: () => (isBrowser() ? window.localStorage.getItem(refreshTokenKey) : null), + setTokens: ({ accessToken, refreshToken }: TokenPair) => { + if (!isBrowser()) return; + window.localStorage.setItem(accessTokenKey, accessToken); + if (refreshToken) { + window.localStorage.setItem(refreshTokenKey, refreshToken); + } + }, + clearTokens: () => { + if (!isBrowser()) return; + window.localStorage.removeItem(accessTokenKey); + window.localStorage.removeItem(refreshTokenKey); + }, + }; +}; \ No newline at end of file diff --git a/packages/api/src/core/types.ts b/packages/api/src/core/types.ts new file mode 100644 index 0000000..c3ba1b5 --- /dev/null +++ b/packages/api/src/core/types.ts @@ -0,0 +1,43 @@ +import type { AxiosInstance, InternalAxiosRequestConfig } from "axios"; + +export interface ApiResponse { + success: boolean; + code: string; + message: string; + data: T; +} + +export type JsonValue = Record; + +export interface TokenPair { + accessToken: string; + refreshToken?: string; +} + +export interface TokenStore { + getAccessToken: () => string | null; + getRefreshToken: () => string | null; + setTokens: (tokens: TokenPair) => void; + clearTokens: () => void; +} + +export interface RefreshTokensContext { + publicClient: AxiosInstance; + refreshToken: string | null; + originalRequest: InternalAxiosRequestConfig & { _retry?: boolean }; +} + +export interface CreateCompasserApiOptions { + baseURL: string; + tokenStore: TokenStore; + withCredentials?: boolean; + refreshTokens?: (context: RefreshTokensContext) => Promise; + onAuthFailure?: (error: unknown) => void; + authScheme?: string; +} + +export interface CompasserApi { + publicClient: AxiosInstance; + privateClient: AxiosInstance; + tokenStore: TokenStore; +} \ No newline at end of file diff --git a/packages/api/src/domains/auth.ts b/packages/api/src/domains/auth.ts new file mode 100644 index 0000000..81ff149 --- /dev/null +++ b/packages/api/src/domains/auth.ts @@ -0,0 +1,123 @@ +import { type QueryClient } from "@tanstack/react-query"; +import { createMutationWithCache } from "../core/mutation"; +import type { CompasserApi } from "../core/types"; +import type { + JoinReqDTO, + SignUpResponse, + LoginReqDTO, + LoginResponse, + LogoutResponse, + KakaoLoginUrlResponse, + KakaoCallbackResponse, +} from "../models/auth"; + +export const createAuthModule = (api: CompasserApi) => { + const keys = { + all: ["auth"] as const, + session: () => [...keys.all, "session"] as const, + }; + + const requests = { + signUp: async (body: JoinReqDTO) => { + const { data } = await api.publicClient.post( + "/oauth2/sign-up", + body, + ); + return data; + }, + + login: async (body: LoginReqDTO) => { + const { data } = await api.publicClient.post( + "/oauth2/login", + body, + ); + return data; + }, + + logout: async () => { + const { data } = await api.privateClient.post( + "/oauth2/logout", + ); + return data; + }, + + loginKakao: async () => { + const { data } = await api.publicClient.post( + "/oauth2/login-kakao", + ); + return data; + }, + + callbackKakao: async (code: string) => { + const { data } = await api.publicClient.get( + "/oauth2/code/kakao", + { + params: { code }, + }, + ); + return data; + }, + }; + + const mutations = { + signUp: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "sign-up"], + mutationFn: requests.signUp, + }), + + login: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "login"], + mutationFn: requests.login, + getActions: (response) => [ + { + type: "set", + queryKey: keys.session(), + value: response.data, + }, + ], + }), + + logout: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "logout"], + mutationFn: requests.logout, + getActions: () => [ + { + type: "remove", + queryKey: keys.all, + }, + ], + onSuccess: async () => { + api.tokenStore.clearTokens(); + }, + }), + + loginKakao: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "login-kakao"], + mutationFn: requests.loginKakao, + }), + + callbackKakao: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "callback-kakao"], + mutationFn: requests.callbackKakao, + getActions: (response) => [ + { + type: "set", + queryKey: keys.session(), + value: response.data, + }, + ], + }), + }; + + return { keys, requests, mutations }; +}; \ No newline at end of file diff --git a/packages/api/src/domains/health.ts b/packages/api/src/domains/health.ts new file mode 100644 index 0000000..7391f43 --- /dev/null +++ b/packages/api/src/domains/health.ts @@ -0,0 +1,25 @@ +import { queryOptions } from "@tanstack/react-query"; +import type { CompasserApi } from "../core/types"; + +export const createHealthModule = (api: CompasserApi) => { + const keys = { + all: ["health"] as const, + }; + + const requests = { + healthCheck: async () => { + const { data } = await api.publicClient.get("/health"); + return data; + }, + }; + + const queries = { + healthCheck: () => + queryOptions({ + queryKey: keys.all, + queryFn: requests.healthCheck, + }), + }; + + return { keys, requests, queries }; +}; \ No newline at end of file diff --git a/packages/api/src/domains/index.ts b/packages/api/src/domains/index.ts new file mode 100644 index 0000000..3ddc86b --- /dev/null +++ b/packages/api/src/domains/index.ts @@ -0,0 +1,24 @@ +import type { CompasserApi } from "../core/types"; +import { createAuthModule } from "./auth"; +import { createHealthModule } from "./health"; +import { createMemberModule } from "./member"; +import { createOrderModule } from "./order"; +import { createOwnerModule } from "./owner"; +import { createPaymentModule } from "./payment"; +import { createRandomBoxModule } from "./random-box"; +import { createStoreModule } from "./store"; +import { createStoreImageModule } from "./store-image"; +import { createStoreManagerModule } from "./store-manager"; + +export const createCompasserModules = (api: CompasserApi) => ({ + auth: createAuthModule(api), + health: createHealthModule(api), + member: createMemberModule(api), + order: createOrderModule(api), + owner: createOwnerModule(api), + payment: createPaymentModule(api), + randomBox: createRandomBoxModule(api), + store: createStoreModule(api), + storeImage: createStoreImageModule(api), + storeManager: createStoreManagerModule(api), +}); \ No newline at end of file diff --git a/packages/api/src/domains/member.ts b/packages/api/src/domains/member.ts new file mode 100644 index 0000000..740000a --- /dev/null +++ b/packages/api/src/domains/member.ts @@ -0,0 +1,72 @@ +import { queryOptions } from "@tanstack/react-query"; +import type { CompasserApi } from "../core/types"; +import type { + GetMemberRewardResponse, + MyPageResponse, + QRDTO, + RewardListResponse, +} from "../models/member"; + +export const createMemberModule = (api: CompasserApi) => { + const keys = { + all: ["member"] as const, + rewards: () => [...keys.all, "rewards"] as const, + myPage: () => [...keys.all, "my-page"] as const, + qrTest: () => [...keys.all, "qr-test"] as const, + }; + + const requests = { + getRewardList: async () => { + const { data } = await api.privateClient.get( + "/members/reward", + ); + return data; + }, + + getMyPage: async () => { + const { data } = await api.privateClient.get( + "/members/my-page", + ); + return data; + }, + + getRewardQRTest: async () => { + const { data } = await api.privateClient.get("/members/qr/test", { + responseType: "blob", + }); + return data; + }, + + getMemberRewardByQr: async (body: QRDTO) => { + const { data } = await api.privateClient.get( + "/store_manager/qr-check", + { + data: body, + }, + ); + return data; + }, + }; + + const queries = { + rewards: () => + queryOptions({ + queryKey: keys.rewards(), + queryFn: async () => (await requests.getRewardList()).data, + }), + + myPage: () => + queryOptions({ + queryKey: keys.myPage(), + queryFn: async () => (await requests.getMyPage()).data, + }), + + qrTest: () => + queryOptions({ + queryKey: keys.qrTest(), + queryFn: requests.getRewardQRTest, + }), + }; + + return { keys, requests, queries }; +}; \ No newline at end of file diff --git a/packages/api/src/domains/order.ts b/packages/api/src/domains/order.ts new file mode 100644 index 0000000..904cf92 --- /dev/null +++ b/packages/api/src/domains/order.ts @@ -0,0 +1,92 @@ +import { queryOptions, type QueryClient } from "@tanstack/react-query"; +import { createMutationWithCache } from "../core/mutation"; +import { invalidatePrefix } from "../core/query-client"; +import type { CompasserApi } from "../core/types"; +import type { + CancelOrderResponse, + CreateOrderDTO, + CreateOrderResponse, + OrderStatusResponse, +} from "../models/order"; + +export const createOrderModule = (api: CompasserApi) => { + const keys = { + all: ["orders"] as const, + status: (orderId: number) => [...keys.all, orderId, "status"] as const, + }; + + const requests = { + createOrder: async (body: CreateOrderDTO) => { + const { data } = await api.privateClient.post( + "/orders", + body, + ); + return data; + }, + + cancelOrder: async (orderId: number) => { + const { data } = await api.privateClient.patch( + `/orders/${orderId}/cancel`, + ); + return data; + }, + + getOrderStatus: async (orderId: number) => { + const { data } = await api.privateClient.get( + `/orders/${orderId}/status`, + ); + return data; + }, + }; + + const queries = { + status: (orderId: number) => + queryOptions({ + queryKey: keys.status(orderId), + queryFn: async () => (await requests.getOrderStatus(orderId)).data, + enabled: Boolean(orderId), + }), + }; + + const mutations = { + createOrder: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "create"], + mutationFn: requests.createOrder, + getActions: () => [ + { + type: "invalidate", + queryKey: keys.all, + }, + ], + }), + + cancelOrder: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "cancel"], + mutationFn: requests.cancelOrder, + getActions: (_response, orderId) => [ + { + type: "invalidate", + queryKey: keys.status(orderId), + }, + { + type: "invalidate", + queryKey: keys.all, + }, + ], + }), + }; + + const invalidate = { + all: async (queryClient: QueryClient) => + invalidatePrefix(queryClient, keys.all), + + status: async (queryClient: QueryClient, orderId: number) => + invalidatePrefix(queryClient, keys.status(orderId)), + }; + + return { keys, requests, queries, mutations, invalidate }; +}; \ No newline at end of file diff --git a/packages/api/src/domains/owner.ts b/packages/api/src/domains/owner.ts new file mode 100644 index 0000000..5f48b54 --- /dev/null +++ b/packages/api/src/domains/owner.ts @@ -0,0 +1,182 @@ +import { queryOptions, type QueryClient } from "@tanstack/react-query"; +import { createMutationWithCache } from "../core/mutation"; +import { invalidatePrefix } from "../core/query-client"; +import type { CompasserApi } from "../core/types"; +import type { + BusinessLicenseVerifyReqDTO, + OwnerUpgradeRespDTO, + ReservationListResponse, + ReservationReqDTO, + ReservationResponse, +} from "../models/owner"; + +export interface ReservationDecisionParams { + reservationId: number; + body?: ReservationReqDTO; +} + +export const createOwnerModule = (api: CompasserApi) => { + const keys = { + all: ["owner"] as const, + upgrade: () => [...keys.all, "upgrade"] as const, + reservations: () => [...keys.all, "reservations"] as const, + pendingReservations: () => [...keys.reservations(), "pending"] as const, + processedReservations: () => [...keys.reservations(), "processed"] as const, + }; + + const requests = { + verifyBizAndUpgrade: async (body: BusinessLicenseVerifyReqDTO) => { + const { data } = await api.privateClient.post( + "/owners/auth/business-license/verify", + body, + ); + return data; + }, + + upgradeToStoreManager: async () => { + const { data } = await api.privateClient.patch( + "/owners/upgrade", + ); + return data; + }, + + getPendingReservations: async () => { + const { data } = await api.privateClient.get( + "/owners/my-store/reservations", + ); + return data; + }, + + getProcessedReservations: async () => { + const { data } = await api.privateClient.get( + "/owners/my-store/reservations/processed", + ); + return data; + }, + + approveReservation: async ({ reservationId }: ReservationDecisionParams) => { + const { data } = await api.privateClient.patch( + `/owners/my-store/reservations/${reservationId}/approve`, + ); + return data; + }, + + rejectReservation: async ({ + reservationId, + body, + }: ReservationDecisionParams) => { + const { data } = await api.privateClient.patch( + `/owners/my-store/reservations/${reservationId}/reject`, + body ?? {}, + ); + return data; + }, + }; + + const queries = { + pendingReservations: () => + queryOptions({ + queryKey: keys.pendingReservations(), + queryFn: async () => (await requests.getPendingReservations()).data, + }), + + processedReservations: () => + queryOptions({ + queryKey: keys.processedReservations(), + queryFn: async () => (await requests.getProcessedReservations()).data, + }), + }; + + const mutations = { + verifyBizAndUpgrade: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "verify-biz-and-upgrade"], + mutationFn: requests.verifyBizAndUpgrade, + getActions: (response) => [ + { + type: "set", + queryKey: keys.upgrade(), + value: response, + }, + { + type: "invalidate", + queryKey: keys.all, + }, + ], + }), + + upgradeToStoreManager: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "upgrade-to-store-manager"], + mutationFn: requests.upgradeToStoreManager, + getActions: (response) => [ + { + type: "set", + queryKey: keys.upgrade(), + value: response, + }, + { + type: "invalidate", + queryKey: keys.all, + }, + ], + }), + + approveReservation: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "approve-reservation"], + mutationFn: requests.approveReservation, + getActions: () => [ + { + type: "invalidate", + queryKey: keys.reservations(), + }, + { + type: "invalidate", + queryKey: keys.pendingReservations(), + }, + { + type: "invalidate", + queryKey: keys.processedReservations(), + }, + ], + }), + + rejectReservation: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "reject-reservation"], + mutationFn: requests.rejectReservation, + getActions: () => [ + { + type: "invalidate", + queryKey: keys.reservations(), + }, + { + type: "invalidate", + queryKey: keys.pendingReservations(), + }, + { + type: "invalidate", + queryKey: keys.processedReservations(), + }, + ], + }), + }; + + const invalidate = { + reservations: async (queryClient: QueryClient) => + invalidatePrefix(queryClient, keys.reservations()), + + pendingReservations: async (queryClient: QueryClient) => + invalidatePrefix(queryClient, keys.pendingReservations()), + + processedReservations: async (queryClient: QueryClient) => + invalidatePrefix(queryClient, keys.processedReservations()), + }; + + return { keys, requests, queries, mutations, invalidate }; +}; \ No newline at end of file diff --git a/packages/api/src/domains/payment.ts b/packages/api/src/domains/payment.ts new file mode 100644 index 0000000..caba615 --- /dev/null +++ b/packages/api/src/domains/payment.ts @@ -0,0 +1,197 @@ +import { queryOptions, type QueryClient } from "@tanstack/react-query"; +import { createMutationWithCache } from "../core/mutation"; +import type { CompasserApi } from "../core/types"; +import type { + ApproveKakaoPayResponse, + CancelKakaoPayResponse, + ReadyKakaoPayResponse, +} from "../models/payment"; + +export interface ReservationPaymentPath { + reservationId: number; +} + +export interface ApprovePaymentParams extends ReservationPaymentPath { + pg_token: string; +} + +export const createPaymentModule = (api: CompasserApi) => { + const keys = { + all: ["reservation-payment"] as const, + ready: (reservationId: number) => + [...keys.all, reservationId, "ready"] as const, + }; + + const requests = { + readyKakaoPay: async ({ reservationId }: ReservationPaymentPath) => { + const { data } = await api.privateClient.post( + `/reservations/${reservationId}/payment/ready`, + ); + return data; + }, + + cancelKakaoPay: async ({ reservationId }: ReservationPaymentPath) => { + const { data } = await api.privateClient.post( + `/reservations/${reservationId}/payment/cancel`, + ); + return data; + }, + + approveKakaoPay: async ({ + reservationId, + pg_token, + }: ApprovePaymentParams) => { + const { data } = await api.privateClient.post( + `/reservations/${reservationId}/payment/approve`, + undefined, + { + params: { pg_token }, + }, + ); + return data; + }, + + callbackSuccess: async ({ + reservationId, + pg_token, + }: ApprovePaymentParams) => { + const { data } = await api.privateClient.get( + "/payments/kakaopay/success", + { + params: { reservationId, pg_token }, + }, + ); + return data; + }, + + callbackFail: async ({ reservationId }: ReservationPaymentPath) => { + const { data } = await api.privateClient.get("/payments/kakaopay/fail", { + params: { reservationId }, + }); + return data; + }, + + callbackCancel: async ({ reservationId }: ReservationPaymentPath) => { + const { data } = await api.privateClient.get( + "/payments/kakaopay/cancel", + { + params: { reservationId }, + }, + ); + return data; + }, + }; + + const queries = { + ready: ({ reservationId }: ReservationPaymentPath) => + queryOptions({ + queryKey: keys.ready(reservationId), + queryFn: async () => + (await requests.readyKakaoPay({ reservationId })).data, + enabled: Boolean(reservationId), + }), + }; + + const mutations = { + readyKakaoPay: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "ready"], + mutationFn: requests.readyKakaoPay, + getActions: (response, variables) => [ + { + type: "set", + queryKey: keys.ready(variables.reservationId), + value: response.data, + }, + ], + }), + + cancelKakaoPay: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "cancel"], + mutationFn: requests.cancelKakaoPay, + getActions: (_response, variables) => [ + { + type: "remove", + queryKey: keys.ready(variables.reservationId), + }, + { + type: "invalidate", + queryKey: keys.all, + }, + ], + }), + + approveKakaoPay: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "approve"], + mutationFn: requests.approveKakaoPay, + getActions: (_response, variables) => [ + { + type: "remove", + queryKey: keys.ready(variables.reservationId), + }, + { + type: "invalidate", + queryKey: keys.all, + }, + ], + }), + + callbackSuccess: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "callback-success"], + mutationFn: requests.callbackSuccess, + getActions: (_response, variables) => [ + { + type: "remove", + queryKey: keys.ready(variables.reservationId), + }, + { + type: "invalidate", + queryKey: keys.all, + }, + ], + }), + + callbackFail: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "callback-fail"], + mutationFn: requests.callbackFail, + getActions: (_response, variables) => [ + { + type: "remove", + queryKey: keys.ready(variables.reservationId), + }, + { + type: "invalidate", + queryKey: keys.all, + }, + ], + }), + + callbackCancel: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "callback-cancel"], + mutationFn: requests.callbackCancel, + getActions: (_response, variables) => [ + { + type: "remove", + queryKey: keys.ready(variables.reservationId), + }, + { + type: "invalidate", + queryKey: keys.all, + }, + ], + }), + }; + + return { keys, requests, queries, mutations }; +}; \ No newline at end of file diff --git a/packages/api/src/domains/random-box.ts b/packages/api/src/domains/random-box.ts new file mode 100644 index 0000000..e3d12d7 --- /dev/null +++ b/packages/api/src/domains/random-box.ts @@ -0,0 +1,150 @@ +import { queryOptions, type QueryClient } from "@tanstack/react-query"; +import { createMutationWithCache } from "../core/mutation"; +import { invalidatePrefix } from "../core/query-client"; +import type { CompasserApi } from "../core/types"; +import type { + RandomBoxCreateReqDTO, + RandomBoxRespDTO, + RandomBoxUpdateReqDTO, +} from "../models/random-box"; + +export interface RandomBoxPathParams { + storeId: number; +} + +export interface RandomBoxUpdateParams extends RandomBoxPathParams { + boxId: number; + body: RandomBoxUpdateReqDTO; +} + +export interface RandomBoxDeleteParams extends RandomBoxPathParams { + boxId: number; +} + +export interface RandomBoxCreateParams extends RandomBoxPathParams { + body: RandomBoxCreateReqDTO; +} + +export const createRandomBoxModule = (api: CompasserApi) => { + const keys = { + all: ["random-box"] as const, + lists: (storeId: number) => [...keys.all, storeId, "list"] as const, + }; + + const requests = { + list: async ({ storeId }: RandomBoxPathParams) => { + const { data } = await api.privateClient.get( + `/stores/${storeId}/random-box`, + ); + return data; + }, + + create: async ({ storeId, body }: RandomBoxCreateParams) => { + const { data } = await api.privateClient.post( + `/stores/${storeId}/random-box`, + body, + ); + return data; + }, + + update: async ({ storeId, boxId, body }: RandomBoxUpdateParams) => { + const { data } = await api.privateClient.patch( + `/stores/${storeId}/random-box/${boxId}`, + body, + ); + return data; + }, + + remove: async ({ storeId, boxId }: RandomBoxDeleteParams) => { + await api.privateClient.delete(`/stores/${storeId}/random-box/${boxId}`); + }, + }; + + const queries = { + list: ({ storeId }: RandomBoxPathParams) => + queryOptions({ + queryKey: keys.lists(storeId), + queryFn: () => requests.list({ storeId }), + }), + }; + + const mutations = { + create: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "create"], + mutationFn: requests.create, + getActions: (createdBox, variables) => [ + { + type: "merge", + queryKey: keys.lists(variables.storeId), + updater: (oldData) => { + const prev = Array.isArray(oldData) + ? (oldData as RandomBoxRespDTO[]) + : []; + return [...prev, createdBox]; + }, + }, + { + type: "invalidate", + queryKey: keys.lists(variables.storeId), + }, + ], + }), + + update: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "update"], + mutationFn: requests.update, + getActions: (updatedBox, variables) => [ + { + type: "merge", + queryKey: keys.lists(variables.storeId), + updater: (oldData) => { + const prev = Array.isArray(oldData) + ? (oldData as RandomBoxRespDTO[]) + : []; + return prev.map((box) => + box.boxId === variables.boxId ? updatedBox : box, + ); + }, + }, + { + type: "invalidate", + queryKey: keys.lists(variables.storeId), + }, + ], + }), + + remove: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "remove"], + mutationFn: requests.remove, + getActions: (_response, variables) => [ + { + type: "merge", + queryKey: keys.lists(variables.storeId), + updater: (oldData) => { + const prev = Array.isArray(oldData) + ? (oldData as RandomBoxRespDTO[]) + : []; + return prev.filter((box) => box.boxId !== variables.boxId); + }, + }, + { + type: "invalidate", + queryKey: keys.lists(variables.storeId), + }, + ], + }), + }; + + const invalidate = { + list: async (queryClient: QueryClient, storeId: number) => + invalidatePrefix(queryClient, keys.lists(storeId)), + }; + + return { keys, requests, queries, mutations, invalidate }; +}; \ No newline at end of file diff --git a/packages/api/src/domains/store-image.ts b/packages/api/src/domains/store-image.ts new file mode 100644 index 0000000..35d442f --- /dev/null +++ b/packages/api/src/domains/store-image.ts @@ -0,0 +1,85 @@ +import { queryOptions, type QueryClient } from "@tanstack/react-query"; +import { createMutationWithCache } from "../core/mutation"; +import { invalidatePrefix } from "../core/query-client"; +import type { CompasserApi } from "../core/types"; +import type { StoreImageRespDTO } from "../models/store-image"; + +export const createStoreImageModule = (api: CompasserApi) => { + const keys = { + all: ["store-image"] as const, + mine: () => [...keys.all, "mine"] as const, + }; + + const requests = { + get: async () => { + const { data } = await api.privateClient.get( + "/owners/me/store/image", + ); + return data; + }, + + remove: async () => { + await api.privateClient.delete("/owners/me/store/image"); + }, + + upload: async (storeImage: File) => { + const formData = new FormData(); + formData.append("storeImage", storeImage); + + const { data } = await api.privateClient.patch( + "/owners/me/store/image", + formData, + { + headers: { + "Content-Type": "multipart/form-data", + }, + }, + ); + return data; + }, + }; + + const queries = { + get: () => + queryOptions({ + queryKey: keys.mine(), + queryFn: requests.get, + }), + }; + + const mutations = { + remove: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "remove"], + mutationFn: requests.remove, + getActions: () => [ + { + type: "remove", + queryKey: keys.mine(), + }, + ], + }), + + upload: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "upload"], + mutationFn: requests.upload, + getActions: (uploadedImage) => [ + { + type: "set", + queryKey: keys.mine(), + value: uploadedImage, + }, + ], + }), + }; + + const invalidate = { + mine: async (queryClient: QueryClient) => + invalidatePrefix(queryClient, keys.mine()), + }; + + return { keys, requests, queries, mutations, invalidate }; +}; \ No newline at end of file diff --git a/packages/api/src/domains/store-manager.ts b/packages/api/src/domains/store-manager.ts new file mode 100644 index 0000000..722ea85 --- /dev/null +++ b/packages/api/src/domains/store-manager.ts @@ -0,0 +1,63 @@ +import { type QueryClient } from "@tanstack/react-query"; +import { createMutationWithCache } from "../core/mutation"; +import type { CompasserApi } from "../core/types"; +import type { + QRDTO, + WritingRewardDTO, + WritingRewardResponse, + GetMemberRewardResponse, +} from "../models/member"; + +export const createStoreManagerModule = (api: CompasserApi) => { + const keys = { + all: ["store-manager"] as const, + }; + + const requests = { + writingReward: async (body: WritingRewardDTO) => { + const { data } = await api.privateClient.post( + "/store_manager/reward", + body, + ); + return data; + }, + + checkingQr: async (body: QRDTO) => { + const { data } = await api.privateClient.get( + "/store_manager/qr-check", + { + data: body, + }, + ); + return data; + }, + }; + + const mutations = { + writingReward: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "writing-reward"], + mutationFn: requests.writingReward, + getActions: () => [ + { + type: "invalidate", + queryKey: ["member", "rewards"], + }, + { + type: "invalidate", + queryKey: ["member", "my-page"], + }, + ], + }), + + checkingQr: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "checking-qr"], + mutationFn: requests.checkingQr, + }), + }; + + return { keys, requests, mutations }; +}; \ No newline at end of file diff --git a/packages/api/src/domains/store.ts b/packages/api/src/domains/store.ts new file mode 100644 index 0000000..394916a --- /dev/null +++ b/packages/api/src/domains/store.ts @@ -0,0 +1,205 @@ +import { queryOptions, type QueryClient } from "@tanstack/react-query"; +import { createMutationWithCache } from "../core/mutation"; +import { invalidatePrefix } from "../core/query-client"; +import type { CompasserApi } from "../core/types"; +import type { + StoreAddressListParams, + StoreByAddressResponse, + StoreDetailResponse, + StoreListParams, + StoreListResponse, + StoreRespDTO, + StoreSimpleResponse, + StoreTagListParams, + StoreUpdateReqDTO, + StoreLocationUpdateReqDTO, +} from "../models/store"; + +export const createStoreModule = (api: CompasserApi) => { + const keys = { + all: ["stores"] as const, + lists: () => [...keys.all, "list"] as const, + list: (params: StoreListParams) => [...keys.lists(), params] as const, + byTag: (params: StoreTagListParams) => + [...keys.lists(), "tag", params] as const, + byAddress: (params: StoreAddressListParams) => + [...keys.lists(), "address", params] as const, + details: () => [...keys.all, "detail"] as const, + detail: (storeId: number) => [...keys.details(), storeId] as const, + simple: (storeId: number) => [...keys.all, "simple", storeId] as const, + myStore: () => [...keys.all, "my-store"] as const, + }; + + const requests = { + getStoreList: async (params: StoreListParams) => { + const { data } = await api.privateClient.get( + "/stores", + { params }, + ); + return data; + }, + + getStoreDetail: async (storeId: number) => { + const { data } = await api.privateClient.get( + `/stores/${storeId}`, + ); + return data; + }, + + getStoreSimple: async (storeId: number) => { + const { data } = await api.privateClient.get( + `/stores/${storeId}/simple`, + ); + return data; + }, + + getStoreListByTag: async ({ tag, ...params }: StoreTagListParams) => { + const { data } = await api.privateClient.get( + `/stores/tag/${tag}`, + { + params, + }, + ); + return data; + }, + + getStoreListByAddress: async (params: StoreAddressListParams) => { + const { data } = await api.privateClient.get( + "/stores/address", + { + params, + }, + ); + return data; + }, + + getMyStore: async () => { + const { data } = await api.privateClient.get( + "/stores/owners/me/store", + ); + return data; + }, + + patchMyStore: async (body: StoreUpdateReqDTO) => { + const { data } = await api.privateClient.patch( + "/stores/owners/me/store", + body, + ); + return data; + }, + + patchMyStoreLocation: async (body: StoreLocationUpdateReqDTO) => { + const { data } = await api.privateClient.patch( + "/stores/owners/me/store/location", + body, + ); + return data; + }, + }; + + const queries = { + list: (params: StoreListParams) => + queryOptions({ + queryKey: keys.list(params), + queryFn: async () => (await requests.getStoreList(params)).data, + }), + + detail: (storeId: number) => + queryOptions({ + queryKey: keys.detail(storeId), + queryFn: async () => (await requests.getStoreDetail(storeId)).data, + enabled: Boolean(storeId), + }), + + simple: (storeId: number) => + queryOptions({ + queryKey: keys.simple(storeId), + queryFn: async () => (await requests.getStoreSimple(storeId)).data, + enabled: Boolean(storeId), + }), + + byTag: (params: StoreTagListParams) => + queryOptions({ + queryKey: keys.byTag(params), + queryFn: async () => (await requests.getStoreListByTag(params)).data, + }), + + byAddress: (params: StoreAddressListParams) => + queryOptions({ + queryKey: keys.byAddress(params), + queryFn: async () => + (await requests.getStoreListByAddress(params)).data, + enabled: Boolean(params.address), + }), + + myStore: () => + queryOptions({ + queryKey: keys.myStore(), + queryFn: requests.getMyStore, + }), + }; + + const mutations = { + patchMyStore: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "patch-my-store"], + mutationFn: requests.patchMyStore, + getActions: (updatedStore) => [ + { + type: "set", + queryKey: keys.myStore(), + value: updatedStore, + }, + { + type: "set", + queryKey: keys.detail(updatedStore.storeId), + value: updatedStore, + }, + { + type: "invalidate", + queryKey: keys.lists(), + }, + ], + }), + + patchMyStoreLocation: (queryClient: QueryClient) => + createMutationWithCache({ + queryClient, + mutationKey: [...keys.all, "patch-my-store-location"], + mutationFn: requests.patchMyStoreLocation, + getActions: (updatedStore) => [ + { + type: "set", + queryKey: keys.myStore(), + value: updatedStore, + }, + { + type: "set", + queryKey: keys.detail(updatedStore.storeId), + value: updatedStore, + }, + { + type: "invalidate", + queryKey: keys.lists(), + }, + ], + }), + }; + + const invalidate = { + all: async (queryClient: QueryClient) => + invalidatePrefix(queryClient, keys.all), + + lists: async (queryClient: QueryClient) => + invalidatePrefix(queryClient, keys.lists()), + + myStore: async (queryClient: QueryClient) => + invalidatePrefix(queryClient, keys.myStore()), + + detail: async (queryClient: QueryClient, storeId: number) => + invalidatePrefix(queryClient, keys.detail(storeId)), + }; + + return { keys, requests, queries, mutations, invalidate }; +}; \ No newline at end of file diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts new file mode 100644 index 0000000..3ad572a --- /dev/null +++ b/packages/api/src/index.ts @@ -0,0 +1,25 @@ +export * from "./core/http"; +export * from "./core/query-client"; +export * from "./core/token-store"; +export * from "./core/types"; +export * from "./core/mutation"; +export * from "./models/auth"; +export * from "./models/common"; +export * from "./models/member"; +export * from "./models/order"; +export * from "./models/owner"; +export * from "./models/payment"; +export * from "./models/random-box"; +export * from "./models/store"; +export * from "./models/store-image"; +export * from "./domains/auth"; +export * from "./domains/health"; +export * from "./domains/member"; +export * from "./domains/order"; +export * from "./domains/owner"; +export * from "./domains/payment"; +export * from "./domains/random-box"; +export * from "./domains/store"; +export * from "./domains/store-image"; +export * from "./domains/store-manager"; +export * from "./domains/index"; \ No newline at end of file diff --git a/packages/api/src/models/auth.ts b/packages/api/src/models/auth.ts new file mode 100644 index 0000000..4d7278a --- /dev/null +++ b/packages/api/src/models/auth.ts @@ -0,0 +1,29 @@ +import type { ApiResponse } from "../core/types"; + +export interface JoinReqDTO { + memberName: string; + nickname: string; + email: string; + password: string; + passwordConfirm: string; +} + +export interface JoinRespDTO { + memberName: string; + email: string; +} + +export interface LoginReqDTO { + email: string; + password: string; +} + +export interface AccessTokenDTO { + accessToken: string; +} + +export type SignUpResponse = ApiResponse; +export type LoginResponse = ApiResponse; +export type LogoutResponse = ApiResponse>; +export type KakaoLoginUrlResponse = ApiResponse; +export type KakaoCallbackResponse = ApiResponse>; \ No newline at end of file diff --git a/packages/api/src/models/common.ts b/packages/api/src/models/common.ts new file mode 100644 index 0000000..209d902 --- /dev/null +++ b/packages/api/src/models/common.ts @@ -0,0 +1,25 @@ +export type RoleType = "NORMAL" | "STORE_MANAGER" | "NULL"; +export type StoreTag = "CAFE" | "BAKERY" | "RESTAURANT"; +export type ReservationStatus = "REQUESTED" | "APPROVED" | "REJECTED" | "CANCELED"; +export type SaleStatus = "SOLD_OUT" | "READY" | "CANCELED"; +export type BankType = + | "NH" + | "IBK" + | "MG" + | "KB" + | "SC" + | "CU" + | "KAKAO" + | "SHINHAN" + | "DGB" + | "TOSS" + | "HANA" + | "BUSAN" + | "WOORI" + | "GWANGJU" + | "JEONBUK" + | "POST" + | "SUHYUP" + | "KDB" + | "CITI" + | "K_BANK"; \ No newline at end of file diff --git a/packages/api/src/models/member.ts b/packages/api/src/models/member.ts new file mode 100644 index 0000000..14cfe53 --- /dev/null +++ b/packages/api/src/models/member.ts @@ -0,0 +1,43 @@ +import type { ApiResponse } from "../core/types"; + +export interface RewardListDTO { + rewardId: number; + points: number; + storename: string; +} + +export interface MyPageRespDTO { + memberName: string; + nickname: string; + email: string; + profileImageUrl?: string; + totalStampCount: number; + totalUnboxingCount: number; + totalCouponCount: number; +} + +export interface QRDTO { + memberId?: number; + token?: string; +} + +export interface GetMemberRewardDTO { + rewardId: number; + storeId: number; + memberId: number; + nickname: string; + stamp: number; + coupon: number; + createdAt: string; +} + +export interface WritingRewardDTO { + rewardId?: number; + storeId?: number; + memberId?: number; +} + +export type RewardListResponse = ApiResponse; +export type MyPageResponse = ApiResponse; +export type GetMemberRewardResponse = ApiResponse; +export type WritingRewardResponse = ApiResponse>; \ No newline at end of file diff --git a/packages/api/src/models/order.ts b/packages/api/src/models/order.ts new file mode 100644 index 0000000..b59a05e --- /dev/null +++ b/packages/api/src/models/order.ts @@ -0,0 +1,62 @@ +import type { ApiResponse } from "../core/types"; +import type { BankType } from "./common"; + +export interface CreateOrderDTO { + randomBoxId: number; + quantity: number; +} + +export interface CreateOrderResultDTO { + reservationId: number; + storeId: number; + storeName: string; + randomBoxId: number; + randomBoxName: string; + quantity: number; + unitPrice: number; + totalPrice: number; + orderStatus: string; + reservationStatus: string; + paymentStatus: string; + pickupStatus: string; + memberBankType?: BankType; + depositBankType?: BankType; + depositAccountNumber?: string; + depositAccountHolder?: string; + businessHours?: string; + createdAt: string; +} + +export interface CancelOrderResultDTO { + reservationId: number; + orderStatus: string; + reservationStatus: string; + paymentStatus: string; + pickupStatus: string; + message: string; +} + +export interface OrderStatusDTO { + reservationId: number; + storeId: number; + storeName: string; + randomBoxId: number; + randomBoxName: string; + quantity: number; + totalPrice: number; + orderStatus: string; + reservationStatus: string; + paymentStatus: string; + pickupStatus: string; + memberBankType?: BankType; + depositBankType?: BankType; + depositAccountNumber?: string; + depositAccountHolder?: string; + businessHours?: string; + createdAt: string; + updatedAt: string; +} + +export type CreateOrderResponse = ApiResponse; +export type CancelOrderResponse = ApiResponse; +export type OrderStatusResponse = ApiResponse; \ No newline at end of file diff --git a/packages/api/src/models/owner.ts b/packages/api/src/models/owner.ts new file mode 100644 index 0000000..187f4b8 --- /dev/null +++ b/packages/api/src/models/owner.ts @@ -0,0 +1,43 @@ +import type { ApiResponse } from "../core/types"; +import type { ReservationStatus, RoleType } from "./common"; + +export interface BusinessLicenseVerifyReqDTO { + businessLicenseNumber?: string; + email?: string; +} + +export interface OwnerUpgradeRespDTO { + memberId: number; + role: RoleType; + storeId: number; + alreadyUpgraded: boolean; +} + +export interface ReservationReqDTO { + status?: ReservationStatus; + rejectReason?: string; +} + +export interface ReservationDTO { + reservationId: number; + memberId: number; + nickName: string; + storeId: number; + storeName: string; + randomBoxId: number; + randomBoxName: string; + price: number; + status: ReservationStatus; + requestedQuantity: number; + rejectReason?: string; + createdAt: string; + updatedAt: string; +} + +export interface ReservationListDTO { + reservations: ReservationDTO[]; + count: number; +} + +export type ReservationResponse = ApiResponse; +export type ReservationListResponse = ApiResponse; \ No newline at end of file diff --git a/packages/api/src/models/payment.ts b/packages/api/src/models/payment.ts new file mode 100644 index 0000000..e38d9df --- /dev/null +++ b/packages/api/src/models/payment.ts @@ -0,0 +1,19 @@ +import type { ApiResponse } from "../core/types"; + +export interface ReadyResultDTO { + reservationId: number; + tid: string; + redirectUrl: string; + paymentStatus: string; +} + +export interface ApproveResultDTO { + reservationId: number; + paymentMethod: string; + paymentStatus: string; + approvedAt: string; +} + +export type ReadyKakaoPayResponse = ApiResponse; +export type CancelKakaoPayResponse = ApiResponse>; +export type ApproveKakaoPayResponse = ApiResponse; \ No newline at end of file diff --git a/packages/api/src/models/random-box.ts b/packages/api/src/models/random-box.ts new file mode 100644 index 0000000..64d66c5 --- /dev/null +++ b/packages/api/src/models/random-box.ts @@ -0,0 +1,24 @@ +import type { SaleStatus } from "./common"; + +export interface RandomBoxCreateReqDTO { + boxName?: string; + content?: string; + stock?: number; + price?: number; + buyLimit?: number; +} + +export interface RandomBoxUpdateReqDTO extends RandomBoxCreateReqDTO { + saleStatus?: SaleStatus; +} + +export interface RandomBoxRespDTO { + boxId: number; + storeId: number; + boxName: string; + stock: number; + price: number; + buyLimit: number; + content: string; + saleStatus: string; +} \ No newline at end of file diff --git a/packages/api/src/models/store-image.ts b/packages/api/src/models/store-image.ts new file mode 100644 index 0000000..a3d03a7 --- /dev/null +++ b/packages/api/src/models/store-image.ts @@ -0,0 +1,7 @@ +export interface StoreImageRespDTO { + imageId: number; + storeId: number; + imageUrl: string; + createdAt: string; + default: boolean; +} \ No newline at end of file diff --git a/packages/api/src/models/store.ts b/packages/api/src/models/store.ts new file mode 100644 index 0000000..acf42a9 --- /dev/null +++ b/packages/api/src/models/store.ts @@ -0,0 +1,70 @@ +import type { ApiResponse, JsonValue } from "../core/types"; +import type { StoreTag } from "./common"; + +export interface StoreUpdateReqDTO { + storeName?: string; + storeEmail?: string; + bankName?: string; + depositor?: string; + bankAccount?: string; + businessHours?: JsonValue; +} + +export interface StoreLocationUpdateReqDTO { + inputAddress?: string; +} + +export interface StoreRespDTO { + storeId: number; + storeManagerId: number; + storeName: string; + storeDetails: string; + inputAddress: string; + roadAddress: string; + jibunAddress: string; + latitude: number; + longitude: number; + businessHours?: JsonValue; + tag: StoreTag; +} + +export interface GetStoreReqDTO { + storeId: number; + storeManagerId: number; + storeName: string; + storeImage?: string; + tag: StoreTag; + storeDetails: string; + latitude: number; + longitude: number; + businessHours?: JsonValue; +} + +export interface SimpleStoreInfoDTO { + storeId: number; + tag: StoreTag; + storeName: string; + roadAddress: string; + jibunAddress: string; + businessHours?: JsonValue; +} + +export interface StoreListParams { + userLat: number; + userLon: number; + page?: number; +} + +export interface StoreTagListParams extends StoreListParams { + tag: StoreTag; +} + +export interface StoreAddressListParams { + address: string; + page?: number; +} + +export type StoreListResponse = ApiResponse; +export type StoreDetailResponse = ApiResponse; +export type StoreSimpleResponse = ApiResponse; +export type StoreByAddressResponse = ApiResponse>; \ No newline at end of file diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json new file mode 100644 index 0000000..6293d6f --- /dev/null +++ b/packages/api/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@compasser/typescript-config/base.json", + "compilerOptions": { + "baseUrl": "." + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e1b550..04cf6ad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: apps/customer: dependencies: + '@compasser/api': + specifier: workspace:^ + version: link:../../packages/api '@compasser/design-system': specifier: workspace:* version: link:../../packages/design-system @@ -75,6 +78,9 @@ importers: apps/owner: dependencies: + '@compasser/api': + specifier: workspace:^ + version: link:../../packages/api '@compasser/design-system': specifier: workspace:* version: link:../../packages/design-system @@ -98,6 +104,28 @@ importers: specifier: 16.1.6 version: 16.1.6(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.2))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.2) + packages/api: + dependencies: + '@tanstack/react-query': + specifier: ^5.90.21 + version: 5.90.21(react@19.2.3) + axios: + specifier: ^1.14.0 + version: 1.14.0 + react: + specifier: ^19 + version: 19.2.3 + react-dom: + specifier: ^19 + version: 19.2.3(react@19.2.3) + devDependencies: + '@compasser/typescript-config': + specifier: workspace:* + version: link:../typescript-config + typescript: + specifier: ^5.9.2 + version: 5.9.2 + packages/design-system: dependencies: '@compasser/tailwind-config': @@ -1460,6 +1488,9 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -1468,6 +1499,9 @@ packages: resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} engines: {node: '>=4'} + axios@1.14.0: + resolution: {integrity: sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -1552,6 +1586,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@11.1.0: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} @@ -1642,6 +1680,10 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1931,6 +1973,15 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -1939,6 +1990,10 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2366,6 +2421,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -2555,6 +2618,10 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -4178,12 +4245,22 @@ snapshots: async-function@1.0.0: {} + asynckit@0.4.0: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 axe-core@4.11.1: {} + axios@1.14.0: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + axobject-query@4.1.0: {} babel-plugin-react-compiler@1.0.0: @@ -4272,6 +4349,10 @@ snapshots: color-name@1.1.4: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@11.1.0: {} concat-map@0.0.1: {} @@ -4358,6 +4439,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + delayed-stream@1.0.0: {} + dequal@2.0.3: {} detect-libc@2.1.2: {} @@ -4842,6 +4925,8 @@ snapshots: flatted@3.3.3: {} + follow-redirects@1.15.11: {} + for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -4851,6 +4936,14 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + fsevents@2.3.3: optional: true @@ -5249,6 +5342,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + min-indent@1.0.1: {} minimatch@3.1.2: @@ -5442,6 +5541,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + proxy-from-env@2.1.0: {} + punycode@2.3.1: {} queue-microtask@1.2.3: {}