From 855c705e31db12e58a6c97e0b40ee30cf4abd493 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Tue, 2 Dec 2025 13:52:11 +0900 Subject: [PATCH 01/16] =?UTF-8?q?chore:=20baseline-browser-mapping=20?= =?UTF-8?q?=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 ++- pnpm-lock.yaml | 11 +++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 2bdac41..8f0df93 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "next dev --webpack", "build": "next build", "start": "next start", "lint": "eslint", @@ -45,6 +45,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "babel-plugin-react-compiler": "1.0.0", + "baseline-browser-mapping": "^2.8.32", "eslint": "^9", "eslint-config-next": "16.0.1", "immer": "^10.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a0c85c..0a0fe59 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,6 +108,9 @@ importers: babel-plugin-react-compiler: specifier: 1.0.0 version: 1.0.0 + baseline-browser-mapping: + specifier: ^2.8.32 + version: 2.8.32 eslint: specifier: ^9 version: 9.39.1(jiti@2.6.1) @@ -1407,8 +1410,8 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - baseline-browser-mapping@2.8.25: - resolution: {integrity: sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==} + baseline-browser-mapping@2.8.32: + resolution: {integrity: sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==} hasBin: true bin-links@6.0.0: @@ -4162,7 +4165,7 @@ snapshots: balanced-match@1.0.2: {} - baseline-browser-mapping@2.8.25: {} + baseline-browser-mapping@2.8.32: {} bin-links@6.0.0: dependencies: @@ -4187,7 +4190,7 @@ snapshots: browserslist@4.27.0: dependencies: - baseline-browser-mapping: 2.8.25 + baseline-browser-mapping: 2.8.32 caniuse-lite: 1.0.30001754 electron-to-chromium: 1.5.249 node-releases: 2.0.27 From 4fca6b831ac12f8476412e29c60c311bc456bc3d Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Tue, 2 Dec 2025 14:09:50 +0900 Subject: [PATCH 02/16] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=A7=80=EA=B0=91=20=EC=B4=9D=EC=95=A1=20api=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/index.ts | 1 + src/entities/wallet/index.ts | 2 ++ src/entities/wallet/model/apis/index.ts | 1 + .../wallet/model/apis/user-balance.api.ts | 26 +++++++++++++++++++ src/entities/wallet/model/index.ts | 1 + src/features/wallet/hooks/index.ts | 1 + .../wallet/hooks/useGetUserBalance.ts | 26 +++++++++++++++++++ 7 files changed, 58 insertions(+) create mode 100644 src/entities/wallet/index.ts create mode 100644 src/entities/wallet/model/apis/index.ts create mode 100644 src/entities/wallet/model/apis/user-balance.api.ts create mode 100644 src/entities/wallet/model/index.ts create mode 100644 src/features/wallet/hooks/useGetUserBalance.ts diff --git a/src/entities/index.ts b/src/entities/index.ts index f19112a..43c35c7 100644 --- a/src/entities/index.ts +++ b/src/entities/index.ts @@ -2,3 +2,4 @@ export * from "./auth"; export * from "./market"; export * from "./user"; export * from "./news"; +export * from "./wallet"; diff --git a/src/entities/wallet/index.ts b/src/entities/wallet/index.ts new file mode 100644 index 0000000..7bdc80d --- /dev/null +++ b/src/entities/wallet/index.ts @@ -0,0 +1,2 @@ +export * from "./model"; +export * from "./server"; diff --git a/src/entities/wallet/model/apis/index.ts b/src/entities/wallet/model/apis/index.ts new file mode 100644 index 0000000..446f942 --- /dev/null +++ b/src/entities/wallet/model/apis/index.ts @@ -0,0 +1 @@ +export * from "./user-balance.api"; diff --git a/src/entities/wallet/model/apis/user-balance.api.ts b/src/entities/wallet/model/apis/user-balance.api.ts new file mode 100644 index 0000000..80b0a38 --- /dev/null +++ b/src/entities/wallet/model/apis/user-balance.api.ts @@ -0,0 +1,26 @@ +import { APIResponse } from "@/shared"; + +interface UserBalanceResponse { + id: number; + user_id: string; + coin_id: string; + amount: number; +} + +export const userBalanceAPI = async (coinId?: string) => { + const queryParams = coinId ? `?coin_id=${coinId}` : ""; + + const response = await fetch(`/api/wallet/balance${queryParams}`); + + if (!response.ok) { + throw new Error("Failed to fetch user balance"); + } + + const result: APIResponse = await response.json(); + + if (result.status === "error") { + throw new Error(result.error); + } + + return result.data; +}; diff --git a/src/entities/wallet/model/index.ts b/src/entities/wallet/model/index.ts new file mode 100644 index 0000000..08440a6 --- /dev/null +++ b/src/entities/wallet/model/index.ts @@ -0,0 +1 @@ +export * from "./apis"; diff --git a/src/features/wallet/hooks/index.ts b/src/features/wallet/hooks/index.ts index b6c63bb..7002ae4 100644 --- a/src/features/wallet/hooks/index.ts +++ b/src/features/wallet/hooks/index.ts @@ -1,2 +1,3 @@ export * from "./useGetHoldDeposit"; export * from "./useRealtimeWallet"; +export * from "./useGetUserBalance"; diff --git a/src/features/wallet/hooks/useGetUserBalance.ts b/src/features/wallet/hooks/useGetUserBalance.ts new file mode 100644 index 0000000..b0ad145 --- /dev/null +++ b/src/features/wallet/hooks/useGetUserBalance.ts @@ -0,0 +1,26 @@ +import { userBalanceAPI } from "@/entities"; +import { useQuery } from "@tanstack/react-query"; + +import { WALLET_KEYS } from "../constants"; + +export const useGetUserBalance = (coinId?: string) => { + let queryKey: readonly unknown[]; + + if (coinId === "KRW") { + queryKey = WALLET_KEYS.balance.krw(); + } else if (coinId) { + queryKey = WALLET_KEYS.balance.coin(coinId); + } else { + queryKey = WALLET_KEYS.balance.all; + } + + return useQuery({ + queryKey: queryKey, + + queryFn: () => userBalanceAPI(coinId), + + refetchInterval: 1000, + + retry: 1, + }); +}; From c414229e06d4d15733cc26e334ba297f4783beed Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Tue, 2 Dec 2025 14:10:26 +0900 Subject: [PATCH 03/16] =?UTF-8?q?feat:=20=EC=A7=80=EA=B0=91=EC=9D=98=20use?= =?UTF-8?q?r=20type=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/wallet/types/index.ts | 1 + src/entities/wallet/types/wallet.type.ts | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 src/entities/wallet/types/index.ts create mode 100644 src/entities/wallet/types/wallet.type.ts diff --git a/src/entities/wallet/types/index.ts b/src/entities/wallet/types/index.ts new file mode 100644 index 0000000..f2c5e95 --- /dev/null +++ b/src/entities/wallet/types/index.ts @@ -0,0 +1 @@ +export * from "./wallet.type"; diff --git a/src/entities/wallet/types/wallet.type.ts b/src/entities/wallet/types/wallet.type.ts new file mode 100644 index 0000000..dcbbaeb --- /dev/null +++ b/src/entities/wallet/types/wallet.type.ts @@ -0,0 +1,11 @@ +import { User } from "@supabase/supabase-js"; + +export type UserMetadata = { + nickname?: string; + full_name?: string; + avatar_url?: string; +}; + +export type WalletUser = Pick & { + user_metadata: UserMetadata; +}; From 63a4940f241671bb449cceed9e7da937dd92f5d2 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Tue, 2 Dec 2025 14:32:37 +0900 Subject: [PATCH 04/16] =?UTF-8?q?refactor:=20=EC=9E=85=EA=B8=88=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20api=20=EA=B5=AC=EC=A1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/wallet/deposit/route.ts | 165 +++--------------- src/entities/wallet/model/apis/deposit.api.ts | 28 +++ src/entities/wallet/server/deposit.helpers.ts | 33 ++++ src/entities/wallet/server/deposit.service.ts | 51 ++++++ 4 files changed, 141 insertions(+), 136 deletions(-) create mode 100644 src/entities/wallet/model/apis/deposit.api.ts create mode 100644 src/entities/wallet/server/deposit.helpers.ts create mode 100644 src/entities/wallet/server/deposit.service.ts diff --git a/src/app/api/wallet/deposit/route.ts b/src/app/api/wallet/deposit/route.ts index caab4bc..eeb2076 100644 --- a/src/app/api/wallet/deposit/route.ts +++ b/src/app/api/wallet/deposit/route.ts @@ -1,158 +1,51 @@ import { cookies } from "next/headers"; import { NextResponse } from "next/server"; +import { depositService } from "@/entities"; +import { APIResponse } from "@/shared"; + import { createClient as createServerClient } from "@/shared/utils/supabase/server"; export async function POST(request: Request) { try { const supabase = createServerClient(cookies()); - const { data: userData, error: userError } = await supabase.auth.getUser(); - if (userError || !userData.user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + const { + data: { user }, + error: userError, + } = await supabase.auth.getUser(); + if (userError || !user) { + return NextResponse.json>( + { status: "error", data: null, error: "Unauthorized" }, + { status: 401 }, + ); } const body = await request.json(); const { amount } = body; - // 필수 파라미터 검증 - if (!amount || amount <= 0) { - return NextResponse.json({ error: "Invalid amount" }, { status: 400 }); - } - - const coin_id = "KRW"; // KRW 입금 - coins.market_id를 참조 - - // FK 제약사항 대응: coins 테이블에 KRW가 없으면 생성 - const { error: coinSelectError } = await supabase - .from("coins") - .select("market_id") - .eq("market_id", coin_id) - .single(); - - if (coinSelectError) { - // 데이터 없음(PGRST116)이면 KRW를 생성 시도 - if ((coinSelectError as { code?: string }).code === "PGRST116") { - const { error: coinInsertError } = await supabase - .from("coins") - .insert({ market_id: coin_id, korean_name: "원화", english_name: "Korean Won" }); - if (coinInsertError) { - console.error("KRW coin insert error:", coinInsertError); - return NextResponse.json({ error: coinInsertError.message }, { status: 500 }); - } - } else { - console.error("KRW coin select error:", coinSelectError); - return NextResponse.json({ error: coinSelectError.message }, { status: 500 }); - } - } - - // FK 제약사항 대응: users 테이블에 사용자가 없으면 생성 (Supabase Auth에는 있지만 public.users 테이블에 없을 경우) - const { error: userSelectError } = await supabase.from("profiles").select("id").eq("id", userData.user.id).single(); - - if (userSelectError) { - if ((userSelectError as { code?: string }).code === "PGRST116") { - // users 테이블에 사용자 정보가 없으면 생성 - const { error: userInsertError } = await supabase.from("profiles").insert({ - id: userData.user.id, - email: userData.user.email || "", - nickname: userData.user.user_metadata?.nickname || "User", - user_name: userData.user.user_metadata?.full_name || "User", // profiles 테이블의 user_name 필드 추가 - }); - if (userInsertError) { - console.error("User insert error:", userInsertError); - return NextResponse.json({ error: `Failed to sync user: ${userInsertError.message}` }, { status: 500 }); - } - } else { - console.error("User check error:", userSelectError); - return NextResponse.json({ error: userSelectError.message }, { status: 500 }); - } + if (!amount || typeof amount !== "number" || amount <= 0) { + return NextResponse.json>( + { status: "error", data: null, error: "Invalid amount" }, + { status: 400 }, + ); } - // 기존 KRW 지갑 조회 - const { data: existingWallet, error: walletError } = await supabase - .from("wallet") - .select("*") - .eq("user_id", userData.user.id) - .eq("coin_id", coin_id) - .single(); - - if (walletError && walletError.code !== "PGRST116") { - // PGRST116은 데이터 없음 오류 - console.error("Wallet query error:", walletError); - return NextResponse.json({ error: walletError.message }, { status: 500 }); - } - - let result; - - if (existingWallet) { - // 기존 지갑이 있으면 금액 증가 - const newAmount = existingWallet.amount + amount; - const { data, error } = await supabase - .from("wallet") - .update({ amount: newAmount }) - .eq("id", existingWallet.id) - .select() - .single(); - - if (error) { - console.error("Wallet update error:", error); - return NextResponse.json({ error: error.message }, { status: 500 }); - } - result = data; - } else { - // 새 지갑 생성 - const { data, error } = await supabase - .from("wallet") - .insert({ - user_id: userData.user.id, - coin_id, - amount, - }) - .select() - .single(); - - if (error) { - console.error("Wallet insert error:", error); - return NextResponse.json({ error: error.message }, { status: 500 }); - } - result = data; - } + const result = await depositService({ + client: supabase, + user: { + id: user.id, + email: user.email, + user_metadata: user.user_metadata, + }, + amount, + }); - return NextResponse.json({ success: true, data: result }, { status: 200 }); + return NextResponse.json>({ status: "success", data: result }, { status: 200 }); } catch (error) { console.error("Deposit API error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); - } -} - -// 사용자의 지갑 조회 -export async function GET(request: Request) { - try { - const supabase = createServerClient(cookies()); - const { data: userData, error: userError } = await supabase.auth.getUser(); - - if (userError || !userData.user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - const { searchParams } = new URL(request.url); - const coin_id = searchParams.get("coin_id"); + const errorMessage = error instanceof Error ? error.message : "Internal server error"; - let query = supabase.from("wallet").select("*").eq("user_id", userData.user.id); - - if (coin_id) { - query = query.eq("coin_id", coin_id); - } - - const { data, error } = await query; - - if (error) { - console.error("Wallet query error:", error); - return NextResponse.json({ error: error.message }, { status: 500 }); - } - - return NextResponse.json({ success: true, data }, { status: 200 }); - } catch (error) { - console.error("Wallet GET API error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + return NextResponse.json>({ status: "error", data: null, error: errorMessage }, { status: 500 }); } } diff --git a/src/entities/wallet/model/apis/deposit.api.ts b/src/entities/wallet/model/apis/deposit.api.ts new file mode 100644 index 0000000..104bdcb --- /dev/null +++ b/src/entities/wallet/model/apis/deposit.api.ts @@ -0,0 +1,28 @@ +import { WalletEntity } from "../../types"; + +export interface DepositParams { + amount: number; +} + +export interface DepositResponse { + success: boolean; + data?: WalletEntity; + error?: string; +} + +export const depositAPI = async (amount: number): Promise => { + const response = await fetch("/api/wallet/deposit", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ amount }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Deposit failed"); + } + + return response.json(); +}; diff --git a/src/entities/wallet/server/deposit.helpers.ts b/src/entities/wallet/server/deposit.helpers.ts new file mode 100644 index 0000000..ecce2f3 --- /dev/null +++ b/src/entities/wallet/server/deposit.helpers.ts @@ -0,0 +1,33 @@ +import { Database } from "@/shared"; +import { SupabaseClient } from "@supabase/supabase-js"; + +import { WalletUser } from "../types"; + +export const ensureKrwCoinExists = async (client: SupabaseClient) => { + const COIN_ID = "KRW"; + + const { error: selectError } = await client.from("coins").select("market_id").eq("market_id", COIN_ID).single(); + + if (selectError?.code === "PGRST116") { + const { error: insertError } = await client + .from("coins") + .insert({ market_id: COIN_ID, korean_name: "원화", english_name: "Korean Won" }); + + if (insertError) throw new Error(`Failed to create KRW coin: ${insertError.message}`); + } +}; + +export const ensureProfileExists = async (client: SupabaseClient, user: WalletUser) => { + const { error: selectError } = await client.from("profiles").select("id").eq("id", user.id).single(); + + if (selectError?.code === "PGRST116") { + const { error: insertError } = await client.from("profiles").insert({ + id: user.id, + email: user.email || "", + nickname: user.user_metadata?.nickname || "User", + user_name: user.user_metadata?.full_name || "User", + }); + + if (insertError) throw new Error(`Failed to sync user profile: ${insertError.message}`); + } +}; diff --git a/src/entities/wallet/server/deposit.service.ts b/src/entities/wallet/server/deposit.service.ts new file mode 100644 index 0000000..48050a1 --- /dev/null +++ b/src/entities/wallet/server/deposit.service.ts @@ -0,0 +1,51 @@ +import { SupabaseClient } from "@supabase/supabase-js"; + +import { Database } from "@/shared/types/database.type"; + +import { WalletUser } from "../types"; + +import { ensureKrwCoinExists, ensureProfileExists } from "./deposit.helpers"; + +type DepositParams = { + client: SupabaseClient; + user: WalletUser; + amount: number; +}; + +export const depositService = async ({ client, user, amount }: DepositParams) => { + const COIN_ID = "KRW"; + + await Promise.all([ensureKrwCoinExists(client), ensureProfileExists(client, user)]); + + const { data: existingWallet, error: walletError } = await client + .from("wallet") + .select("*") + .eq("user_id", user.id) + .eq("coin_id", COIN_ID) + .single(); + + if (walletError && walletError.code !== "PGRST116") { + throw new Error(walletError.message); + } + + if (existingWallet) { + const { data, error } = await client + .from("wallet") + .update({ amount: existingWallet.amount + amount }) + .eq("id", existingWallet.id) + .select() + .single(); + + if (error) throw new Error(error.message); + return data; + } else { + const { data, error } = await client + .from("wallet") + .insert({ user_id: user.id, coin_id: COIN_ID, amount }) + .select() + .single(); + + if (error) throw new Error(error.message); + return data; + } +}; From 72262047cfaa23e3c47c1785eb40f9e91d81bfa8 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Tue, 2 Dec 2025 14:34:14 +0900 Subject: [PATCH 05/16] =?UTF-8?q?refactor:=20=EB=B3=B4=EC=9C=A0=20?= =?UTF-8?q?=EC=9E=90=EC=82=B0=20=EA=B4=80=EB=A6=AC=20api=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/wallet/balance/route.ts | 42 +++++++++++++++++++ src/entities/wallet/model/apis/index.ts | 1 + src/entities/wallet/server/index.ts | 3 ++ .../wallet/server/user-balance.service.ts | 24 +++++++++++ src/entities/wallet/types/wallet.type.ts | 3 ++ src/features/wallet/apis/index.ts | 1 - src/features/wallet/constants/index.ts | 1 + .../wallet/constants/wallet-queries.ts | 7 ++++ src/features/wallet/types/index.ts | 1 - 9 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 src/app/api/wallet/balance/route.ts create mode 100644 src/entities/wallet/server/index.ts create mode 100644 src/entities/wallet/server/user-balance.service.ts create mode 100644 src/features/wallet/constants/index.ts create mode 100644 src/features/wallet/constants/wallet-queries.ts diff --git a/src/app/api/wallet/balance/route.ts b/src/app/api/wallet/balance/route.ts new file mode 100644 index 0000000..4bf718c --- /dev/null +++ b/src/app/api/wallet/balance/route.ts @@ -0,0 +1,42 @@ +import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; + +import { getUserBalanceService } from "@/entities"; +import { APIResponse } from "@/shared"; + +import { createClient as createServerClient } from "@/shared/utils/supabase/server"; + +export async function GET(request: Request) { + try { + const supabase = createServerClient(cookies()); + + const { + data: { user }, + error: userError, + } = await supabase.auth.getUser(); + + if (userError || !user) { + return NextResponse.json>( + { status: "error", data: null, error: "Unauthorized" }, + { status: 401 }, + ); + } + + const { searchParams } = new URL(request.url); + const coin_id = searchParams.get("coin_id"); + + const walletData = await getUserBalanceService({ + client: supabase, + userId: user.id, + coinId: coin_id, + }); + + return NextResponse.json>({ status: "success", data: walletData }, { status: 200 }); + } catch (error) { + console.error("Wallet GET API error:", error); + + const errorMessage = error instanceof Error ? error.message : "Internal server error"; + + return NextResponse.json>({ status: "error", data: null, error: errorMessage }, { status: 500 }); + } +} diff --git a/src/entities/wallet/model/apis/index.ts b/src/entities/wallet/model/apis/index.ts index 446f942..8d4177e 100644 --- a/src/entities/wallet/model/apis/index.ts +++ b/src/entities/wallet/model/apis/index.ts @@ -1 +1,2 @@ export * from "./user-balance.api"; +export * from "./deposit.api"; diff --git a/src/entities/wallet/server/index.ts b/src/entities/wallet/server/index.ts new file mode 100644 index 0000000..82f3819 --- /dev/null +++ b/src/entities/wallet/server/index.ts @@ -0,0 +1,3 @@ +export * from "./user-balance.service"; +export * from "./deposit.service"; +export * from "./deposit.helpers"; diff --git a/src/entities/wallet/server/user-balance.service.ts b/src/entities/wallet/server/user-balance.service.ts new file mode 100644 index 0000000..b7de91e --- /dev/null +++ b/src/entities/wallet/server/user-balance.service.ts @@ -0,0 +1,24 @@ +import { Database } from "@/shared"; +import { SupabaseClient } from "@supabase/supabase-js"; + +type WalletParams = { + client: SupabaseClient; + userId: string; + coinId?: string | null; +}; + +export const getUserBalanceService = async ({ client, userId, coinId }: WalletParams) => { + let query = client.from("wallet").select("*").eq("user_id", userId); + + if (coinId) { + query = query.eq("coin_id", coinId); + } + + const { data, error } = await query; + + if (error) { + throw new Error(error.message); + } + + return data; +}; diff --git a/src/entities/wallet/types/wallet.type.ts b/src/entities/wallet/types/wallet.type.ts index dcbbaeb..9c6ac16 100644 --- a/src/entities/wallet/types/wallet.type.ts +++ b/src/entities/wallet/types/wallet.type.ts @@ -1,3 +1,4 @@ +import { Database } from "@/shared"; import { User } from "@supabase/supabase-js"; export type UserMetadata = { @@ -9,3 +10,5 @@ export type UserMetadata = { export type WalletUser = Pick & { user_metadata: UserMetadata; }; + +export type WalletEntity = Database["public"]["Tables"]["wallet"]["Row"]; diff --git a/src/features/wallet/apis/index.ts b/src/features/wallet/apis/index.ts index ad65524..06ce309 100644 --- a/src/features/wallet/apis/index.ts +++ b/src/features/wallet/apis/index.ts @@ -1,4 +1,3 @@ -export * from "./deposit.api"; export * from "./hold-deposit.api"; export * from "./wallet.api"; export * from "./trades.api"; diff --git a/src/features/wallet/constants/index.ts b/src/features/wallet/constants/index.ts new file mode 100644 index 0000000..3026a8f --- /dev/null +++ b/src/features/wallet/constants/index.ts @@ -0,0 +1 @@ +export * from "./wallet-queries"; diff --git a/src/features/wallet/constants/wallet-queries.ts b/src/features/wallet/constants/wallet-queries.ts new file mode 100644 index 0000000..39c357c --- /dev/null +++ b/src/features/wallet/constants/wallet-queries.ts @@ -0,0 +1,7 @@ +export const WALLET_KEYS = { + balance: { + all: ["user-balance"], + krw: () => [...WALLET_KEYS.balance.all, "krw"], + coin: (coinId: string) => [...WALLET_KEYS.balance.all, "coin", coinId], + }, +}; diff --git a/src/features/wallet/types/index.ts b/src/features/wallet/types/index.ts index e7412dd..bdf5d2d 100644 --- a/src/features/wallet/types/index.ts +++ b/src/features/wallet/types/index.ts @@ -1,2 +1 @@ export * from "./tab.type"; -export * from "./wallet.type"; From 5e812c38b086fa520c3c6335d851bb8555b27558 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Tue, 2 Dec 2025 14:34:53 +0900 Subject: [PATCH 06/16] =?UTF-8?q?refactor:=20=EC=BF=BC=EB=A6=AC=EB=AC=B4?= =?UTF-8?q?=ED=9A=A8=ED=99=94=EB=A1=9C=20=EB=B3=B4=EC=9C=A0=20=EC=9E=90?= =?UTF-8?q?=EC=82=B0=20=EC=B4=88=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/wallet/ui/CashSection.tsx | 29 +++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/features/wallet/ui/CashSection.tsx b/src/features/wallet/ui/CashSection.tsx index 11e2296..e6c1c6c 100644 --- a/src/features/wallet/ui/CashSection.tsx +++ b/src/features/wallet/ui/CashSection.tsx @@ -2,24 +2,39 @@ import { useState } from "react"; -import { Button, Dialog, DialogContent, DialogDescription, DialogTitle, DialogTrigger, Input } from "@/shared"; +import { depositAPI } from "@/entities"; +import { + Button, + Dialog, + DialogContent, + DialogDescription, + DialogTitle, + DialogTrigger, + Input, + queryClient, +} from "@/shared"; import { useMutation } from "@tanstack/react-query"; import { toast } from "sonner"; -import { depositAPI } from "../apis/deposit.api"; +import { WALLET_KEYS } from "../constants"; export const CashSection = () => { const [amount, setAmount] = useState(""); const [isOpen, setIsOpen] = useState(false); + const onSuccess = () => { + toast.success("입금이 완료되었습니다!"); + setAmount(""); + setIsOpen(false); + queryClient.invalidateQueries({ + queryKey: WALLET_KEYS.balance.krw(), + }); + }; + const { mutate: deposit, isPending } = useMutation({ mutationFn: (amount: number) => depositAPI(amount), onSuccess: () => { - toast.success(`${amount.toLocaleString()}원이 입금되었습니다!`); - setAmount(""); - setIsOpen(false); - // 페이지 새로고침으로 지갑 데이터 업데이트 - window.location.reload(); + onSuccess(); }, onError: (error: Error) => { toast.error(`입금 실패: ${error.message}`); From b31f8f6050b8e96605521c1e0ffa7bb015f9e9e4 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Tue, 2 Dec 2025 14:35:07 +0900 Subject: [PATCH 07/16] =?UTF-8?q?feat:=20=EC=BF=BC=EB=A6=AC=EB=AC=B4?= =?UTF-8?q?=ED=9A=A8=ED=99=94=20=EC=82=AC=EC=9A=A9=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/shared/index.ts b/src/shared/index.ts index c544940..444c1c3 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -5,3 +5,4 @@ export * from "./provider"; export * from "./types"; export * from "./hooks"; export * from "./store"; +export * from "./libs"; From ab342230aa848aaf4b92ac10d1d6097befd3e83d Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Tue, 2 Dec 2025 14:35:17 +0900 Subject: [PATCH 08/16] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/wallet/apis/deposit.api.ts | 28 ------------------------ src/features/wallet/types/wallet.type.ts | 3 --- 2 files changed, 31 deletions(-) delete mode 100644 src/features/wallet/apis/deposit.api.ts delete mode 100644 src/features/wallet/types/wallet.type.ts diff --git a/src/features/wallet/apis/deposit.api.ts b/src/features/wallet/apis/deposit.api.ts deleted file mode 100644 index 3303e61..0000000 --- a/src/features/wallet/apis/deposit.api.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { WalletEntity } from "../types"; - -export interface DepositParams { - amount: number; -} - -export interface DepositResponse { - success: boolean; - data?: WalletEntity; - error?: string; -} - -export const depositAPI = async (amount: number): Promise => { - const response = await fetch("/api/wallet/deposit", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ amount }), - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || "Deposit failed"); - } - - return response.json(); -}; diff --git a/src/features/wallet/types/wallet.type.ts b/src/features/wallet/types/wallet.type.ts deleted file mode 100644 index d81758f..0000000 --- a/src/features/wallet/types/wallet.type.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Database } from "@/shared"; - -export type WalletEntity = Database["public"]["Tables"]["wallet"]["Row"]; From 5964bf560eafe802b6c9d72699c31dc06e007c35 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Tue, 2 Dec 2025 15:02:15 +0900 Subject: [PATCH 09/16] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=20=EB=B3=B4=EC=9C=A0=20=EC=9E=90=EC=82=B0=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/trade/route.ts | 2 +- src/entities/wallet/index.ts | 1 + src/features/wallet/apis/wallet.api.ts | 2 +- .../wallet/hooks/useRealtimeWallet.ts | 70 ++++++++++++------- src/features/wallet/store/useWalletStore.ts | 3 +- 5 files changed, 51 insertions(+), 27 deletions(-) diff --git a/src/app/api/trade/route.ts b/src/app/api/trade/route.ts index 467af68..5faf350 100644 --- a/src/app/api/trade/route.ts +++ b/src/app/api/trade/route.ts @@ -1,7 +1,7 @@ import { cookies } from "next/headers"; import { NextResponse } from "next/server"; -import { toDisplayMarket, toUpbitMarket } from "@/entities"; +import { toDisplayMarket } from "@/entities"; import { createClient as createServerClient } from "@/shared/utils/supabase/server"; diff --git a/src/entities/wallet/index.ts b/src/entities/wallet/index.ts index 7bdc80d..403aa6a 100644 --- a/src/entities/wallet/index.ts +++ b/src/entities/wallet/index.ts @@ -1,2 +1,3 @@ export * from "./model"; export * from "./server"; +export * from "./types"; diff --git a/src/features/wallet/apis/wallet.api.ts b/src/features/wallet/apis/wallet.api.ts index f1c0f95..11e60c4 100644 --- a/src/features/wallet/apis/wallet.api.ts +++ b/src/features/wallet/apis/wallet.api.ts @@ -1,4 +1,4 @@ -import { WalletEntity } from "../types"; +import { WalletEntity } from "@/entities/wallet"; export interface WalletResponse { success: boolean; diff --git a/src/features/wallet/hooks/useRealtimeWallet.ts b/src/features/wallet/hooks/useRealtimeWallet.ts index 75ab138..a85b2c7 100644 --- a/src/features/wallet/hooks/useRealtimeWallet.ts +++ b/src/features/wallet/hooks/useRealtimeWallet.ts @@ -1,42 +1,45 @@ "use client"; -import { useEffect } from "react"; +import { useEffect, useMemo } from "react"; -import { useQuery } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { getTickerAPI } from "@/features/home/apis"; import { createClient as createSupabaseBrowserClient } from "@/shared/utils/supabase"; -import { getWalletAPI } from "../apis"; import { getTradesAPI } from "../apis/trades.api"; +import { WALLET_KEYS } from "../constants"; import { useWalletStore } from "../store"; import { computeCostBasis } from "../utils/aggregate-trades"; +import { useGetUserBalance } from "./useGetUserBalance"; + /** * 지갑과 시세를 실시간으로 동기화하는 hook */ export const useRealtimeWallet = () => { const { setWallets, setTickerMap, setCostBasisMap } = useWalletStore(); + const queryClient = useQueryClient(); // 지갑 정보 조회 - const { data: walletData, refetch: refetchWallet } = useQuery({ - queryKey: ["wallet"], - queryFn: () => getWalletAPI(), - }); + const { data: walletData, refetch: refetchWallet } = useGetUserBalance(); - // 지갑에 있는 모든 코인의 마켓 ID 추출 (KRW 제외) - const markets = walletData?.data - ?.filter((w) => w.coin_id !== "KRW") - .map((w) => w.coin_id) - .join(","); + // 지갑에 있는 모든 코인의 마켓 ID 추출 (KRW 제외) - 문자열로 변환 + const marketIds = useMemo(() => { + if (!walletData) return ""; + return walletData + .filter((w) => w.coin_id !== "KRW") + .map((w) => w.coin_id) + .join(","); + }, [walletData]); // 실시간 시세 조회 (1초마다) const { data: tickerData } = useQuery({ - queryKey: ["ticker", markets], - queryFn: () => getTickerAPI(markets || ""), + queryKey: ["ticker", marketIds], + queryFn: () => getTickerAPI(marketIds), refetchInterval: 1000, - enabled: !!markets, // markets가 있을 때만 실행 + enabled: !!marketIds, // marketIds가 있을 때만 실행 }); // 거래 내역 조회 (1초마다 - 주문 시 즉시 반영) @@ -48,8 +51,8 @@ export const useRealtimeWallet = () => { // 지갑 데이터를 store에 동기화 useEffect(() => { - if (walletData?.data) { - setWallets(walletData.data); + if (walletData) { + setWallets(walletData); } }, [walletData, setWallets]); @@ -72,25 +75,43 @@ export const useRealtimeWallet = () => { } }, [tradesData, setCostBasisMap]); - // Supabase Realtime 구독: wallet 테이블의 사용자 레코드 변경 시 즉시 반영 + // Supabase Realtime 구독: wallet 및 trade 테이블 변경 시 즉시 반영 useEffect(() => { const supabase = createSupabaseBrowserClient(); let subscribed = true; - let channel: ReturnType | null = null; + let walletChannel: ReturnType | null = null; + let tradeChannel: ReturnType | null = null; const setup = async () => { const { data: auth } = await supabase.auth.getUser(); const userId = auth.user?.id; if (!userId || !subscribed) return; - channel = supabase + // wallet 테이블 구독 + walletChannel = supabase .channel(`wallet-changes-${userId}`) .on( "postgres_changes", { event: "*", schema: "public", table: "wallet", filter: `user_id=eq.${userId}` }, () => { - // 변경 감지 시 최신 지갑 데이터로 갱신 + // 변경 감지 시 모든 지갑 관련 쿼리 무효화 + queryClient.invalidateQueries({ queryKey: WALLET_KEYS.balance.all }); + refetchWallet(); + }, + ) + .subscribe(); + + // trade 테이블 구독 (거래 발생 시 지갑 데이터도 갱신) + tradeChannel = supabase + .channel(`trade-changes-${userId}`) + .on( + "postgres_changes", + { event: "*", schema: "public", table: "trade", filter: `user_id=eq.${userId}` }, + () => { + // 거래 발생 시 지갑 및 거래 내역 갱신 + queryClient.invalidateQueries({ queryKey: WALLET_KEYS.balance.all }); + queryClient.invalidateQueries({ queryKey: ["trades"] }); refetchWallet(); }, ) @@ -101,12 +122,13 @@ export const useRealtimeWallet = () => { return () => { subscribed = false; - channel?.unsubscribe(); + walletChannel?.unsubscribe(); + tradeChannel?.unsubscribe(); }; - }, [refetchWallet]); + }, [refetchWallet, queryClient]); return { - wallets: walletData?.data || [], + wallets: walletData || [], refetchWallet, }; }; diff --git a/src/features/wallet/store/useWalletStore.ts b/src/features/wallet/store/useWalletStore.ts index fb7d292..c0a76fb 100644 --- a/src/features/wallet/store/useWalletStore.ts +++ b/src/features/wallet/store/useWalletStore.ts @@ -2,7 +2,8 @@ import { create } from "zustand"; import { TickerData } from "@/features/home/apis"; -import { WalletEntity } from "../types"; +import { WalletEntity } from "@/entities/wallet"; + import { CostBasis } from "../utils/aggregate-trades"; interface WalletState { From fc9787508f4f34341a7e1da104a6f34a068bbd89 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Tue, 2 Dec 2025 15:23:04 +0900 Subject: [PATCH 10/16] =?UTF-8?q?refactor:=20=EC=8B=A4=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=20=EC=BD=94=EC=9D=B8=20=EC=8B=9C=EC=84=B8=20=EB=B0=98=EC=98=81?= =?UTF-8?q?=ED=95=9C=20=EB=B3=B4=EC=9C=A0=20=EC=9E=90=EC=82=B0=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/wallet/ui/ChartAreaSection.tsx | 2 +- src/features/wallet/ui/StatsAreaSection.tsx | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/features/wallet/ui/ChartAreaSection.tsx b/src/features/wallet/ui/ChartAreaSection.tsx index 920c195..d0e3ee7 100644 --- a/src/features/wallet/ui/ChartAreaSection.tsx +++ b/src/features/wallet/ui/ChartAreaSection.tsx @@ -20,7 +20,7 @@ export const ChartAreaSection = () => { return (