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 diff --git a/src/app/api/trade/route.ts b/src/app/api/trade/route.ts index 467af68..293f211 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"; @@ -97,7 +97,8 @@ export async function POST(request: Request) { .eq("market_id", coin_id) .single(); - if ((coinSelectError as { code?: string }).code === "PGRST116") { + // PGRST116: 데이터가 없을 때 발생하는 에러 코드 + if (coinSelectError && coinSelectError.code === "PGRST116") { const { error: coinInsertError } = await supabase .from("coins") .insert({ market_id: coin_id, korean_name: coin_id, english_name: coin_id }); 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/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/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..403aa6a --- /dev/null +++ b/src/entities/wallet/index.ts @@ -0,0 +1,3 @@ +export * from "./model"; +export * from "./server"; +export * from "./types"; diff --git a/src/features/wallet/apis/deposit.api.ts b/src/entities/wallet/model/apis/deposit.api.ts similarity index 92% rename from src/features/wallet/apis/deposit.api.ts rename to src/entities/wallet/model/apis/deposit.api.ts index 3303e61..104bdcb 100644 --- a/src/features/wallet/apis/deposit.api.ts +++ b/src/entities/wallet/model/apis/deposit.api.ts @@ -1,4 +1,4 @@ -import { WalletEntity } from "../types"; +import { WalletEntity } from "../../types"; export interface DepositParams { amount: number; diff --git a/src/entities/wallet/model/apis/index.ts b/src/entities/wallet/model/apis/index.ts new file mode 100644 index 0000000..8d4177e --- /dev/null +++ b/src/entities/wallet/model/apis/index.ts @@ -0,0 +1,2 @@ +export * from "./user-balance.api"; +export * from "./deposit.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/entities/wallet/model/types/wallet-stats.type.ts b/src/entities/wallet/model/types/wallet-stats.type.ts new file mode 100644 index 0000000..2c0adaa --- /dev/null +++ b/src/entities/wallet/model/types/wallet-stats.type.ts @@ -0,0 +1,12 @@ +export interface WalletStatsEntity { + id: string; + user_id: string; + total_assets: number; + total_purchase: number; + total_evaluation: number; + total_profit_loss: number; + total_profit_rate: number; + held_krw: number; + updated_at: string; + created_at: string; +} 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; + } +}; 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/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..9c6ac16 --- /dev/null +++ b/src/entities/wallet/types/wallet.type.ts @@ -0,0 +1,14 @@ +import { Database } from "@/shared"; +import { User } from "@supabase/supabase-js"; + +export type UserMetadata = { + nickname?: string; + full_name?: string; + avatar_url?: string; +}; + +export type WalletUser = Pick & { + user_metadata: UserMetadata; +}; + +export type WalletEntity = Database["public"]["Tables"]["wallet"]["Row"]; diff --git a/src/features/home/components/features/order/OrderForm.tsx b/src/features/home/components/features/order/OrderForm.tsx index 0aa5eae..032a1b5 100644 --- a/src/features/home/components/features/order/OrderForm.tsx +++ b/src/features/home/components/features/order/OrderForm.tsx @@ -5,12 +5,12 @@ import { useEffect, useState } from "react"; import { OrderSchemaType, orderSchema, tradeAPI, useGetMarket } from "@/entities"; import { Button, Form } from "@/shared"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { RotateCw } from "lucide-react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; -import { getWalletAPI } from "@/features/wallet/apis"; +import { useGetUserBalance } from "@/features/wallet"; import { getTickerAPI } from "../../../apis"; import { TabType } from "../../../types"; @@ -19,7 +19,6 @@ import { AmountField, PriceField, ToggleButtonGroup, TotalField } from "../../co export const OrderForm = () => { const { market } = useGetMarket(); const [activeTab, setActiveTab] = useState("매수"); - const queryClient = useQueryClient(); // 실시간 시세 조회 (1초마다) const { data: tickerData } = useQuery({ @@ -31,15 +30,12 @@ export const OrderForm = () => { const currentPrice = tickerData?.data?.[0]?.trade_price || 0; - // 지갑 정보 조회 - const { data: walletData } = useQuery({ - queryKey: ["wallet"], - queryFn: () => getWalletAPI(), - }); + // 지갑 정보 조회 (FSD 구조에 맞게 useGetUserBalance 훅 사용) + const { data: walletData } = useGetUserBalance(); // KRW 잔고와 선택된 코인 잔고 계산 - const krwBalance = walletData?.data?.find((w) => w.coin_id === "KRW")?.amount || 0; - const coinBalance = walletData?.data?.find((w) => w.coin_id === market)?.amount || 0; + const krwBalance = walletData?.find((w) => w.coin_id === "KRW")?.amount || 0; + const coinBalance = walletData?.find((w) => w.coin_id === market)?.amount || 0; const form = useForm({ resolver: zodResolver(orderSchema), @@ -92,10 +88,7 @@ export const OrderForm = () => { const onSuccess = () => { toast.success("주문이 완료되었습니다."); handleReset(); - // 모든 wallet 관련 쿼리 무효화 (다른 컴포넌트도 동기화) - queryClient.invalidateQueries({ queryKey: ["wallet"] }); - // 거래 내역도 무효화 - queryClient.invalidateQueries({ queryKey: ["trades"] }); + // 지갑 정보는 useRealtimeWallet의 Supabase Realtime 구독으로 자동 갱신 }; const { mutate: tradeMutate, isPending } = useMutation({ diff --git a/src/features/home/hooks/useRealtimeTicker.ts b/src/features/home/hooks/useRealtimeTicker.ts index e0c134e..5d8ea3f 100644 --- a/src/features/home/hooks/useRealtimeTicker.ts +++ b/src/features/home/hooks/useRealtimeTicker.ts @@ -9,7 +9,7 @@ import { TickerData, getTickerAPI } from "../apis"; * @param markets 조회할 마켓 (예: "KRW-BTC,KRW-ETH") * @param refetchInterval 갱신 주기 (ms), 기본값 1000ms (1초) */ -export const useRealtimeTicker = (markets: string, refetchInterval: number = 1000) => { +export const useRealtimeTicker = (markets: string, refetchInterval: number = 500) => { const { data } = useQuery({ queryKey: ["ticker", markets], queryFn: () => getTickerAPI(markets), diff --git a/src/features/wallet/apis/index.ts b/src/features/wallet/apis/index.ts index ad65524..55ecaba 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"; +export * from "./wallet-stats.api"; diff --git a/src/features/wallet/apis/wallet-stats.api.ts b/src/features/wallet/apis/wallet-stats.api.ts new file mode 100644 index 0000000..262f7a7 --- /dev/null +++ b/src/features/wallet/apis/wallet-stats.api.ts @@ -0,0 +1,77 @@ +import { createClient } from "@/shared/utils/supabase/client"; + +export interface WalletStats { + total_assets: number; + total_purchase: number; + total_evaluation: number; + total_profit_loss: number; + total_profit_rate: number; + held_krw: number; +} + +/** + * 사용자의 지갑 통계 정보를 조회합니다. + */ +export const getWalletStatsAPI = async (): Promise => { + const supabase = createClient(); + + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (!user) { + throw new Error("User not authenticated"); + } + + const { data, error } = await supabase.from("wallet_stats").select("*").eq("user_id", user.id).single(); + + if (error) { + // 레코드가 없으면 기본값 반환 + if (error.code === "PGRST116") { + return { + total_assets: 0, + total_purchase: 0, + total_evaluation: 0, + total_profit_loss: 0, + total_profit_rate: 0, + held_krw: 0, + }; + } + throw error; + } + + return { + total_assets: data.total_assets ?? 0, + total_purchase: data.total_purchase ?? 0, + total_evaluation: data.total_evaluation ?? 0, + total_profit_loss: data.total_profit_loss ?? 0, + total_profit_rate: data.total_profit_rate ?? 0, + held_krw: data.held_krw ?? 0, + }; +}; + +/** + * 지갑 통계를 업데이트합니다. (프론트엔드에서 계산된 값) + */ +export const updateWalletStatsAPI = async (stats: Partial): Promise => { + const supabase = createClient(); + + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (!user) { + throw new Error("User not authenticated"); + } + + const { error } = await supabase + .from("wallet_stats") + .upsert({ + user_id: user.id, + ...stats, + updated_at: new Date().toISOString(), + }) + .eq("user_id", user.id); + + if (error) throw error; +}; diff --git a/src/features/wallet/apis/wallet.api.ts b/src/features/wallet/apis/wallet.api.ts deleted file mode 100644 index f1c0f95..0000000 --- a/src/features/wallet/apis/wallet.api.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { WalletEntity } from "../types"; - -export interface WalletResponse { - success: boolean; - data: WalletEntity[]; -} - -export const getWalletAPI = async (coinId?: string): Promise => { - const url = coinId ? `/api/wallet/deposit?coin_id=${coinId}` : "/api/wallet/deposit"; - - const response = await fetch(url, { - method: "GET", - cache: "no-store", - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || "Failed to fetch wallet"); - } - - return response.json(); -}; 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..d2df442 --- /dev/null +++ b/src/features/wallet/constants/wallet-queries.ts @@ -0,0 +1,7 @@ +export const WALLET_KEYS = { + balance: { + all: ["user-balance"] as const, + krw: () => ["user-balance", "krw"] as const, + coin: (coinId: string) => ["user-balance", "coin", coinId] as const, + }, +}; diff --git a/src/features/wallet/hooks/index.ts b/src/features/wallet/hooks/index.ts index b6c63bb..a295e44 100644 --- a/src/features/wallet/hooks/index.ts +++ b/src/features/wallet/hooks/index.ts @@ -1,2 +1,5 @@ export * from "./useGetHoldDeposit"; export * from "./useRealtimeWallet"; +export * from "./useGetUserBalance"; +export * from "./useAssetList"; +export * from "./useWalletStats"; diff --git a/src/features/wallet/hooks/useAssetList.ts b/src/features/wallet/hooks/useAssetList.ts new file mode 100644 index 0000000..5107304 --- /dev/null +++ b/src/features/wallet/hooks/useAssetList.ts @@ -0,0 +1,74 @@ +import { useMemo } from "react"; + +import { useWalletStore } from "../store"; + +export type AssetItem = { + name: string; + symbol: string; + quantity: string; + buyPrice: string; + currentPrice: string; + profitRate: string; + profitAmount: string; + isProfit: boolean; +}; + +/** + * Display 형식을 Upbit API 형식으로 변환 + * @example "COMP/KRW" -> "KRW-COMP" + */ +const toUpbitMarketFormat = (coinId: string): string => { + if (!coinId.includes("/")) return coinId; + const [coin, currency] = coinId.split("/"); + return `${currency}-${coin}`; +}; + +/** + * 보유 자산 목록 데이터를 가공하는 훅 + * 지갑 데이터를 UI에 표시할 형식으로 변환 + */ +export const useAssetList = (): AssetItem[] => { + // Zustand store에서 개별 값을 구독 (반응성 향상) + const wallets = useWalletStore((state) => state.wallets); + const tickerMap = useWalletStore((state) => state.tickerMap); + const costBasisMap = useWalletStore((state) => state.costBasisMap); + const getCoinEvaluation = useWalletStore((state) => state.getCoinEvaluation); + + const assetList = useMemo(() => { + // KRW를 제외한 코인만 필터링 + const coinWallets = wallets.filter((w) => w.coin_id !== "KRW"); + + return coinWallets.map((wallet) => { + // Ticker 조회 시 Upbit 형식으로 변환하여 검색 + const upbitMarket = toUpbitMarketFormat(wallet.coin_id); + const ticker = tickerMap[upbitMarket] || tickerMap[wallet.coin_id]; + + const evaluation = getCoinEvaluation(wallet.coin_id, wallet.amount); + + // 현재 보유 수량 × 평단가 = 매수 원가 + const costInfo = costBasisMap[wallet.coin_id]; + const avgPrice = costInfo?.avgPrice ?? 0; + const buyTotalAmount = wallet.amount * avgPrice; + + // 손익 계산 + const profitAmount = evaluation - buyTotalAmount; + const profitRate = buyTotalAmount > 0 ? (profitAmount / buyTotalAmount) * 100 : 0; + + // 마켓 코드에서 코인 심볼 추출 (예: "COMP/KRW" -> "COMP") + const symbol = wallet.coin_id.includes("/") ? wallet.coin_id.split("/")[0] : wallet.coin_id; + + return { + name: ticker?.market || wallet.coin_id, + symbol, + quantity: wallet.amount.toFixed(8), + buyPrice: buyTotalAmount.toLocaleString("ko-KR"), + currentPrice: evaluation.toLocaleString("ko-KR"), + profitRate: `${profitRate >= 0 ? "+" : ""}${profitRate.toFixed(2)}`, + profitAmount: `${profitAmount >= 0 ? "+" : ""}${profitAmount.toLocaleString("ko-KR")}`, + isProfit: profitAmount >= 0, + }; + }); + }, [wallets, tickerMap, costBasisMap, getCoinEvaluation]); + + return assetList; +}; 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, + }); +}; 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/hooks/useWalletStats.ts b/src/features/wallet/hooks/useWalletStats.ts new file mode 100644 index 0000000..1d1327b --- /dev/null +++ b/src/features/wallet/hooks/useWalletStats.ts @@ -0,0 +1,118 @@ +"use client"; + +import { useEffect, useMemo } from "react"; + +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + +import { createClient as createSupabaseBrowserClient } from "@/shared/utils/supabase"; + +import { getWalletStatsAPI, updateWalletStatsAPI } from "../apis"; +import { useWalletStore } from "../store"; + +/** + * 지갑 통계를 실시간으로 관리하는 훅 + * 1. Supabase Realtime으로 wallet_stats 구독 + * 2. 시세 변동 시 프론트에서 계산하여 업데이트 + */ +export const useWalletStats = () => { + const queryClient = useQueryClient(); + + // Store에서 필요한 값들 가져오기 (선택자 패턴) + const wallets = useWalletStore((state) => state.wallets); + const tickerMap = useWalletStore((state) => state.tickerMap); + const costBasisMap = useWalletStore((state) => state.costBasisMap); + const getTotalEvaluation = useWalletStore((state) => state.getTotalEvaluation); + const getTotalCoinEvaluation = useWalletStore((state) => state.getTotalCoinEvaluation); + const getTotalPurchase = useWalletStore((state) => state.getTotalPurchase); + const getTotalProfitLoss = useWalletStore((state) => state.getTotalProfitLoss); + const getTotalProfitRate = useWalletStore((state) => state.getTotalProfitRate); + + // wallet_stats 조회 + const { refetch } = useQuery({ + queryKey: ["wallet-stats"], + queryFn: getWalletStatsAPI, + refetchInterval: false, // Realtime으로만 업데이트 + }); + + // wallet_stats 업데이트 mutation + const { mutate: updateStats } = useMutation({ + mutationFn: updateWalletStatsAPI, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["wallet-stats"] }); + }, + }); + + // 계산된 통계 값 (useMemo로 최적화) + const calculatedStats = useMemo(() => { + const krwWallet = wallets.find((w) => w.coin_id === "KRW"); + const heldKRW = krwWallet?.amount || 0; + const totalCoinEval = getTotalCoinEvaluation(); + const totalAssets = heldKRW + totalCoinEval; + const totalPurchase = getTotalPurchase(); + const totalEvaluation = getTotalEvaluation(); + const totalProfitLoss = getTotalProfitLoss(); + const totalProfitRate = getTotalProfitRate(); + + return { + held_krw: heldKRW, + total_assets: totalAssets, + total_purchase: totalPurchase, + total_evaluation: totalEvaluation, + total_profit_loss: totalProfitLoss, + total_profit_rate: totalProfitRate, + }; + }, [wallets, getTotalEvaluation, getTotalCoinEvaluation, getTotalPurchase, getTotalProfitLoss, getTotalProfitRate]); + + // 시세 변동 시 통계 업데이트 (debounce 없이 즉시) + useEffect(() => { + // tickerMap이 업데이트되고 wallets가 있을 때만 실행 + if (Object.keys(tickerMap).length > 0 && wallets.length > 0) { + updateStats(calculatedStats); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tickerMap]); // tickerMap 변경 시에만 실행 + + // Supabase Realtime 구독 + useEffect(() => { + const supabase = createSupabaseBrowserClient(); + + let subscribed = true; + let channel: ReturnType | null = null; + + const setup = async () => { + const { data: auth } = await supabase.auth.getUser(); + const userId = auth.user?.id; + if (!userId || !subscribed) return; + + // wallet_stats 테이블 구독 + channel = supabase + .channel(`wallet-stats-${userId}`) + .on( + "postgres_changes", + { + event: "*", + schema: "public", + table: "wallet_stats", + filter: `user_id=eq.${userId}`, + }, + () => { + // 변경 감지 시 refetch + refetch(); + }, + ) + .subscribe(); + }; + + setup(); + + return () => { + subscribed = false; + channel?.unsubscribe(); + }; + }, [refetch]); + + return { + stats: calculatedStats, // 항상 계산된 최신 값 반환 + isLoaded: Object.keys(costBasisMap).length > 0, + }; +}; diff --git a/src/features/wallet/index.ts b/src/features/wallet/index.ts index 2ef5ae9..a165947 100644 --- a/src/features/wallet/index.ts +++ b/src/features/wallet/index.ts @@ -1,2 +1,3 @@ export * from "./ui"; export * from "./types"; +export * from "./hooks"; diff --git a/src/features/wallet/store/useWalletStore.ts b/src/features/wallet/store/useWalletStore.ts index fb7d292..3cd4f94 100644 --- a/src/features/wallet/store/useWalletStore.ts +++ b/src/features/wallet/store/useWalletStore.ts @@ -1,88 +1,113 @@ import { create } from "zustand"; +import { devtools } from "zustand/middleware"; import { TickerData } from "@/features/home/apis"; -import { WalletEntity } from "../types"; +import { WalletEntity } from "@/entities/wallet"; + import { CostBasis } from "../utils/aggregate-trades"; interface WalletState { + // State wallets: WalletEntity[]; - tickerMap: Record; - costBasisMap: Record; // 코인별 평균매수가/원가 + tickerMap: Record; // Key: "KRW-COMP" (Upbit format) + costBasisMap: Record; // Key: "COMP/KRW" (Display format) + + // Actions setWallets: (wallets: WalletEntity[]) => void; setTickerMap: (tickerMap: Record) => void; setCostBasisMap: (map: Record) => void; - getTotalEvaluation: () => number; - getTotalPurchase: () => number; - getTotalProfitLoss: () => number; - getTotalProfitRate: () => number; - getCoinEvaluation: (coinId: string, amount: number) => number; - getTotalCoinEvaluation: () => number; // KRW 제외 코인 평가액 합계 - getCoinCostBasis: (coinId: string) => number; // 해당 코인의 총 원가 + + // Getters (Computeds) + getCoinEvaluation: (coinId: string, amount: number) => number; // 개별 코인 평가액 + getTotalCoinEvaluation: () => number; // 코인만 평가액 합계 + getTotalEvaluation: () => number; // 전체 자산 (KRW + 코인) + getTotalPurchase: () => number; // 총 매수금액 + getTotalProfitLoss: () => number; // 총 평가손익 + getTotalProfitRate: () => number; // 총 수익률 } -export const useWalletStore = create((set, get) => ({ - wallets: [], - tickerMap: {}, - costBasisMap: {}, - - setWallets: (wallets) => set({ wallets }), - - setTickerMap: (tickerMap) => set({ tickerMap }), - setCostBasisMap: (map) => set({ costBasisMap: map }), - - // 특정 코인의 평가액 계산 - getCoinEvaluation: (coinId, amount) => { - const { tickerMap } = get(); - if (coinId === "KRW") return amount; - - const ticker = tickerMap[coinId]; - if (!ticker) return 0; - - return ticker.trade_price * amount; - }, - - // 코인 평가액 총합 (KRW 제외) - getTotalCoinEvaluation: () => { - const { wallets, getCoinEvaluation } = get(); - return wallets - .filter((w) => w.coin_id !== "KRW") - .reduce((total, w) => total + getCoinEvaluation(w.coin_id, w.amount), 0); - }, - - // 특정 코인의 총 원가 - getCoinCostBasis: (coinId) => { - const { costBasisMap } = get(); - return costBasisMap[coinId]?.totalCost ?? 0; - }, - - // 총 평가액 (모든 자산의 현재 가치 합계) - getTotalEvaluation: () => { - const { wallets, getCoinEvaluation } = get(); - return wallets.reduce((total, wallet) => { - return total + getCoinEvaluation(wallet.coin_id, wallet.amount); - }, 0); - }, - - // 총 매수액 (현재 보유 코인의 원가 합계) - getTotalPurchase: () => { - const { wallets, getCoinCostBasis } = get(); - return wallets - .filter((w) => w.coin_id !== "KRW" && w.amount > 0) - .reduce((sum, w) => sum + getCoinCostBasis(w.coin_id), 0); - }, - - // 총 평가 손익 - getTotalProfitLoss: () => { - const { getTotalCoinEvaluation, getTotalPurchase } = get(); - return getTotalCoinEvaluation() - getTotalPurchase(); - }, - - // 총 평가 수익률 - getTotalProfitRate: () => { - const { getTotalProfitLoss, getTotalPurchase } = get(); - const purchase = getTotalPurchase(); - if (purchase === 0) return 0; - return (getTotalProfitLoss() / purchase) * 100; - }, -})); +export const useWalletStore = create()( + devtools( + (set, get) => ({ + // --- Initial State --- + wallets: [], + tickerMap: {}, + costBasisMap: {}, + + // --- Actions --- + setWallets: (wallets) => set({ wallets }), + setTickerMap: (tickerMap) => set({ tickerMap }), + setCostBasisMap: (map) => set({ costBasisMap: map }), + + // --- Getters --- + + // 1. 특정 코인의 현재 평가금액 (현재가 * 수량) + getCoinEvaluation: (coinId, amount) => { + if (coinId === "KRW") return amount; + + const { tickerMap } = get(); + + // Display 형식("COMP/KRW")을 Upbit 형식("KRW-COMP")으로 변환 + const upbitFormat = coinId.includes("/") ? `${coinId.split("/")[1]}-${coinId.split("/")[0]}` : coinId; + + // 두 형식 모두 시도 + const ticker = tickerMap[upbitFormat] || tickerMap[coinId]; + + if (!ticker) return 0; + return ticker.trade_price * amount; + }, + + // 2. 코인들의 평가금액 총합 (보유 KRW 제외) + getTotalCoinEvaluation: () => { + const { wallets, getCoinEvaluation } = get(); + return wallets + .filter((w) => w.coin_id !== "KRW") + .reduce((total, w) => total + getCoinEvaluation(w.coin_id, w.amount), 0); + }, + + // 3. 총 보유자산 (보유 KRW + 코인 평가금액) + getTotalEvaluation: () => { + const { wallets, getCoinEvaluation } = get(); + return wallets.reduce((total, wallet) => { + return total + getCoinEvaluation(wallet.coin_id, wallet.amount); + }, 0); + }, + + // 4. 총 매수금액 (보유 수량 * 평단가) + // *중요: 단순히 costBasisMap의 totalCost를 쓰면 안 됩니다. + // 매도 후 남은 잔량에 대한 원가만 계산해야 하므로 (수량 * 평단가) 공식을 씁니다. + getTotalPurchase: () => { + const { wallets, costBasisMap } = get(); + + return wallets + .filter((w) => w.coin_id !== "KRW" && w.amount > 0) + .reduce((sum, w) => { + const costInfo = costBasisMap[w.coin_id]; + + // 평단가가 없으면 0원으로 처리 (혹은 현재가로 처리할 수도 있으나 보수적으로 0) + const avgPrice = costInfo?.avgPrice ?? 0; + + return sum + w.amount * avgPrice; + }, 0); + }, + + // 5. 총 평가손익 (총 평가금액 - 총 매수금액) + getTotalProfitLoss: () => { + const totalEval = get().getTotalCoinEvaluation(); // 코인만 계산 + const totalBuy = get().getTotalPurchase(); // 코인만 계산 + return totalEval - totalBuy; + }, + + // 6. 총 수익률 ((평가손익 / 매수금액) * 100) + getTotalProfitRate: () => { + const profitLoss = get().getTotalProfitLoss(); + const totalBuy = get().getTotalPurchase(); + + if (totalBuy === 0) return 0; // 0으로 나누기 방지 + return (profitLoss / totalBuy) * 100; + }, + }), + { name: "WalletStore" }, + ), +); 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"; 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"]; 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}`); 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 (