From f8b3f807aeaf2abee439b48f5a805e3517b426c7 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Thu, 13 Nov 2025 18:37:46 +0900 Subject: [PATCH 01/15] =?UTF-8?q?feat:=20=ED=98=B8=EA=B0=80=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EB=B6=88=EB=9F=AC=EC=98=A4=EA=B8=B0=20api=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/market/model/apis/index.ts | 1 + .../market/model/apis/market-order.api.ts | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 src/entities/market/model/apis/market-order.api.ts diff --git a/src/entities/market/model/apis/index.ts b/src/entities/market/model/apis/index.ts index d62e325..3112ea3 100644 --- a/src/entities/market/model/apis/index.ts +++ b/src/entities/market/model/apis/index.ts @@ -1,2 +1,3 @@ export * from "./market-all.api"; +export * from "./market-order.api"; export * from "./ticker.api"; diff --git a/src/entities/market/model/apis/market-order.api.ts b/src/entities/market/model/apis/market-order.api.ts new file mode 100644 index 0000000..f911c8f --- /dev/null +++ b/src/entities/market/model/apis/market-order.api.ts @@ -0,0 +1,23 @@ +import { UPBIT_URL } from "@/shared"; + +import { OrderbookUnit } from "../types"; + +export interface MarketOrderResponse { + market: string; + orderbook_units: OrderbookUnit[]; + timestamp: number; +} + +export const marketOrderAPI = async (markets: string): Promise => { + const response = await fetch(`${UPBIT_URL}/orderbook?markets=${markets}`, { + cache: "no-store", + }); + + if (!response.ok) { + console.error("Upbit API error", response.status, response.statusText); + throw new Error("Failed to fetch market order data"); + } + + const data = await response.json(); + return data; +}; From 735ff20cbf6699106c720e55595bd89ba6c07b64 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Thu, 13 Nov 2025 18:46:04 +0900 Subject: [PATCH 02/15] =?UTF-8?q?feat:=20=ED=98=B8=EA=B0=80=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EB=B6=88=EB=9F=AC=EC=98=A4=EA=B8=B0=20hook=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/market/model/hooks/index.ts | 1 + src/entities/market/model/hooks/useGetMarketOrder.ts | 10 ++++++++++ src/entities/market/model/index.ts | 1 + 3 files changed, 12 insertions(+) create mode 100644 src/entities/market/model/hooks/index.ts create mode 100644 src/entities/market/model/hooks/useGetMarketOrder.ts diff --git a/src/entities/market/model/hooks/index.ts b/src/entities/market/model/hooks/index.ts new file mode 100644 index 0000000..a58b0f2 --- /dev/null +++ b/src/entities/market/model/hooks/index.ts @@ -0,0 +1 @@ +export * from "./useGetMarketOrder"; diff --git a/src/entities/market/model/hooks/useGetMarketOrder.ts b/src/entities/market/model/hooks/useGetMarketOrder.ts new file mode 100644 index 0000000..38f77ad --- /dev/null +++ b/src/entities/market/model/hooks/useGetMarketOrder.ts @@ -0,0 +1,10 @@ +import { useQuery } from "@tanstack/react-query"; + +import { marketOrderAPI } from "../apis"; + +export const useGetMarketOrder = (markets: string) => { + return useQuery({ + queryKey: ["market-order", markets], + queryFn: () => marketOrderAPI(markets), + }); +}; diff --git a/src/entities/market/model/index.ts b/src/entities/market/model/index.ts index 1063c8d..a1c1e7c 100644 --- a/src/entities/market/model/index.ts +++ b/src/entities/market/model/index.ts @@ -1,2 +1,3 @@ export * from "./apis"; export * from "./types"; +export * from "./hooks"; From 597e7abbac0775402e403d5d324699cbf651b872 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Thu, 13 Nov 2025 18:46:41 +0900 Subject: [PATCH 03/15] =?UTF-8?q?feat:=20order=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/market/model/types/index.ts | 1 + src/entities/market/model/types/orderbook-units.type.ts | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 src/entities/market/model/types/orderbook-units.type.ts diff --git a/src/entities/market/model/types/index.ts b/src/entities/market/model/types/index.ts index 0406812..ece66b6 100644 --- a/src/entities/market/model/types/index.ts +++ b/src/entities/market/model/types/index.ts @@ -1 +1,2 @@ export * from "./market-info.type"; +export * from "./orderbook-units.type"; diff --git a/src/entities/market/model/types/orderbook-units.type.ts b/src/entities/market/model/types/orderbook-units.type.ts new file mode 100644 index 0000000..474b4f4 --- /dev/null +++ b/src/entities/market/model/types/orderbook-units.type.ts @@ -0,0 +1,6 @@ +export type OrderbookUnit = { + ask_price: number; + bid_price: number; + ask_size: number; + bid_size: number; +}; From cb41d5fd7dae3eedff3d875837d23346688fbae6 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Thu, 13 Nov 2025 18:48:48 +0900 Subject: [PATCH 04/15] =?UTF-8?q?feat:=20order=20data=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/features/order/OrderTable.tsx | 123 +++++++++--------- 1 file changed, 59 insertions(+), 64 deletions(-) diff --git a/src/features/home/components/features/order/OrderTable.tsx b/src/features/home/components/features/order/OrderTable.tsx index 23a81dc..6eb1375 100644 --- a/src/features/home/components/features/order/OrderTable.tsx +++ b/src/features/home/components/features/order/OrderTable.tsx @@ -1,6 +1,31 @@ +"use client"; + +import { useGetMarketOrder } from "@/entities"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/shared"; export const OrderTable = () => { + const { data: orderData } = useGetMarketOrder("KRW-WAXP"); + + // 배열의 첫 번째 요소 가져오기 + const marketOrder = orderData?.[0]; + + console.log("Market Order Data:", marketOrder); + + if (!marketOrder) { + return
Loading...
; + } + + // 호가 데이터를 매도/매수로 분리 (최대 15개씩) + const orderbookUnits = marketOrder.orderbook_units || []; + const askOrders = orderbookUnits.slice(0, 6); // 매도 호가 + const bidOrders = orderbookUnits.slice(0, 6); // 매수 호가 + + // 현재가 판단: 첫 번째 호가의 매수/매도 가격 비교 + const firstOrder = orderbookUnits[0]; + const currentPrice = firstOrder?.ask_price || firstOrder?.bid_price || 0; + const isBidPrice = firstOrder?.bid_price === currentPrice; + const currentVolume = isBidPrice ? firstOrder?.bid_size : firstOrder?.ask_size; + return ( @@ -11,73 +36,43 @@ export const OrderTable = () => { - - 324,130.913 - 17.9 - - - - 324,130.913 - 17.9 - - - - 324,130.913 - 17.9 - - - - 324,130.913 - 17.9 - - - - 324,130.913 - 17.9 - - - - 324,130.913 - 17.9 - - + {/* 매수 호가 (Bid) - 파란색 */} + {bidOrders.reverse().map((order, index) => ( + + {order.bid_size.toLocaleString()} + + {order.bid_price.toLocaleString()} + + + + ))} - - - 17.9 - 324,130.913 + {/* 현재가 구분선 */} + + + {isBidPrice ? currentVolume?.toLocaleString() : ""} + + + {currentPrice.toLocaleString()} + + + {!isBidPrice ? currentVolume?.toLocaleString() : ""} + - - - 17.9 - 324,130.913 - - - - 17.9 - 324,130.913 - - - - 17.9 - 324,130.913 - - - - 17.9 - 324,130.913 - - - - 17.9 - 324,130.913 - - - - 17.9 - 324,130.913 - + {/* 매도 호가 (Ask) - 빨간색 */} + {askOrders.map((order, index) => ( + + + + {order.ask_price.toLocaleString()} + + {order.ask_size.toLocaleString()} + + ))}
); From a0946b9cfabc15c2bd32255d59abbb39fb814979 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Thu, 13 Nov 2025 18:59:21 +0900 Subject: [PATCH 05/15] =?UTF-8?q?feat:=20order=20api=205=EC=B4=88=EB=A7=88?= =?UTF-8?q?=EB=8B=A4=20=ED=98=B8=EC=B6=9C=EB=A1=9C=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/market/model/hooks/useGetMarketOrder.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/entities/market/model/hooks/useGetMarketOrder.ts b/src/entities/market/model/hooks/useGetMarketOrder.ts index 38f77ad..a7f5659 100644 --- a/src/entities/market/model/hooks/useGetMarketOrder.ts +++ b/src/entities/market/model/hooks/useGetMarketOrder.ts @@ -6,5 +6,7 @@ export const useGetMarketOrder = (markets: string) => { return useQuery({ queryKey: ["market-order", markets], queryFn: () => marketOrderAPI(markets), + + refetchInterval: 5000, }); }; From 5b07fc39d88e89d9ae84cf384647988835dedd68 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Thu, 13 Nov 2025 19:19:22 +0900 Subject: [PATCH 06/15] =?UTF-8?q?feat:=20=EA=B1=B0=EB=9E=98=EB=9F=89?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EA=B7=B8=EB=9E=98=ED=94=84=20?= =?UTF-8?q?=ED=91=9C=ED=98=84=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/features/order/OrderTable.tsx | 84 +++++++++++++------ 1 file changed, 59 insertions(+), 25 deletions(-) diff --git a/src/features/home/components/features/order/OrderTable.tsx b/src/features/home/components/features/order/OrderTable.tsx index 6eb1375..6c00edd 100644 --- a/src/features/home/components/features/order/OrderTable.tsx +++ b/src/features/home/components/features/order/OrderTable.tsx @@ -26,6 +26,11 @@ export const OrderTable = () => { const isBidPrice = firstOrder?.bid_price === currentPrice; const currentVolume = isBidPrice ? firstOrder?.bid_size : firstOrder?.ask_size; + // 최대 거래량 계산 (그래프 바 비율 계산용) + const maxBidSize = Math.max(...bidOrders.map((o) => o.bid_size), 0); + const maxAskSize = Math.max(...askOrders.map((o) => o.ask_size), 0); + const maxVolume = Math.max(maxBidSize, maxAskSize); + return ( @@ -37,42 +42,71 @@ export const OrderTable = () => { {/* 매수 호가 (Bid) - 파란색 */} - {bidOrders.reverse().map((order, index) => ( - - {order.bid_size.toLocaleString()} - - {order.bid_price.toLocaleString()} - - - - ))} + {bidOrders.reverse().map((order, index) => { + const volumePercent = maxVolume > 0 ? (order.bid_size / maxVolume) * 100 : 0; + return ( + + {/* 그래프 바 배경 */} +
+ + + {order.bid_size.toLocaleString()} + + + {order.bid_price.toLocaleString()} + + + + ); + })} {/* 현재가 구분선 */} - - + + {/* 현재가 그래프 바 (매수/매도에 따라 방향 다름) */} + {currentVolume && ( +
0 ? ((currentVolume / maxVolume) * 100) / 3 : 0}%`, + }} + /> + )} + + {isBidPrice ? currentVolume?.toLocaleString() : ""} - + {currentPrice.toLocaleString()} - + {!isBidPrice ? currentVolume?.toLocaleString() : ""} {/* 매도 호가 (Ask) - 빨간색 */} - {askOrders.map((order, index) => ( - - - - {order.ask_price.toLocaleString()} - - {order.ask_size.toLocaleString()} - - ))} + {askOrders.map((order, index) => { + const volumePercent = maxVolume > 0 ? (order.ask_size / maxVolume) * 100 : 0; + return ( + + {/* 그래프 바 배경 (오른쪽에서 시작) */} +
+ + + + {order.ask_price.toLocaleString()} + + + {order.ask_size.toLocaleString()} + + + ); + })}
); From 9111e46e179e247bf0063f1a1d845b9144cb8c0c Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Thu, 13 Nov 2025 21:20:09 +0900 Subject: [PATCH 07/15] =?UTF-8?q?chore:=20supabase=20db=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/types/database.type.ts | 202 ++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) diff --git a/src/shared/types/database.type.ts b/src/shared/types/database.type.ts index 46b8fe6..8a94570 100644 --- a/src/shared/types/database.type.ts +++ b/src/shared/types/database.type.ts @@ -14,6 +14,92 @@ export type Database = { } public: { Tables: { + chart: { + Row: { + candle_timestamp: string + close: number + coin_id: string + high: number + id: number + low: number + open: number + volume: number + } + Insert: { + candle_timestamp?: string + close: number + coin_id: string + high: number + id?: number + low: number + open: number + volume: number + } + Update: { + candle_timestamp?: string + close?: number + coin_id?: string + high?: number + id?: number + low?: number + open?: number + volume?: number + } + Relationships: [ + { + foreignKeyName: "chart_coin_id_fkey" + columns: ["coin_id"] + isOneToOne: false + referencedRelation: "coins" + referencedColumns: ["market_id"] + }, + ] + } + coins: { + Row: { + english_name: string | null + korean_name: string + market_id: string + } + Insert: { + english_name?: string | null + korean_name: string + market_id: string + } + Update: { + english_name?: string | null + korean_name?: string + market_id?: string + } + Relationships: [] + } + market_orders: { + Row: { + created_at: string | null + id: number + market: string + orderbook_units: Json + timestamp: number + updated_at: string | null + } + Insert: { + created_at?: string | null + id?: number + market: string + orderbook_units: Json + timestamp: number + updated_at?: string | null + } + Update: { + created_at?: string | null + id?: number + market?: string + orderbook_units?: Json + timestamp?: number + updated_at?: string | null + } + Relationships: [] + } profiles: { Row: { avatar_url: string | null @@ -41,6 +127,122 @@ export type Database = { } Relationships: [] } + trade: { + Row: { + amount: number + coin_id: string + created_at: string + id: number + price: number + total_krw: number + trade_type: string + user_id: string + } + Insert: { + amount: number + coin_id: string + created_at: string + id?: number + price: number + total_krw: number + trade_type: string + user_id?: string + } + Update: { + amount?: number + coin_id?: string + created_at?: string + id?: number + price?: number + total_krw?: number + trade_type?: string + user_id?: string + } + Relationships: [ + { + foreignKeyName: "trade_coin_id_fkey" + columns: ["coin_id"] + isOneToOne: false + referencedRelation: "coins" + referencedColumns: ["market_id"] + }, + { + foreignKeyName: "trade_user_id_fkey" + columns: ["user_id"] + isOneToOne: false + referencedRelation: "profiles" + referencedColumns: ["id"] + }, + ] + } + transactions: { + Row: { + amount: number + created_at: string + id: number + transaction_type: string + user_id: string + } + Insert: { + amount: number + created_at?: string + id?: number + transaction_type: string + user_id?: string + } + Update: { + amount?: number + created_at?: string + id?: number + transaction_type?: string + user_id?: string + } + Relationships: [ + { + foreignKeyName: "transactions_user_id_fkey" + columns: ["user_id"] + isOneToOne: false + referencedRelation: "profiles" + referencedColumns: ["id"] + }, + ] + } + wallet: { + Row: { + amount: number + coin_id: string + id: number + user_id: string + } + Insert: { + amount: number + coin_id: string + id?: number + user_id?: string + } + Update: { + amount?: number + coin_id?: string + id?: number + user_id?: string + } + Relationships: [ + { + foreignKeyName: "wallet_coin_id_fkey" + columns: ["coin_id"] + isOneToOne: false + referencedRelation: "coins" + referencedColumns: ["market_id"] + }, + { + foreignKeyName: "wallet_user_id_fkey" + columns: ["user_id"] + isOneToOne: false + referencedRelation: "profiles" + referencedColumns: ["id"] + }, + ] + } } Views: { [_ in never]: never From 258d8d0b33e3bf640d6cfb093cedf114861d1c31 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Thu, 13 Nov 2025 21:22:55 +0900 Subject: [PATCH 08/15] =?UTF-8?q?feat:=20=ED=98=B8=EA=B0=80=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20sync=20api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../market/model/apis/market-order.api.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/entities/market/model/apis/market-order.api.ts b/src/entities/market/model/apis/market-order.api.ts index f911c8f..d8dbd4d 100644 --- a/src/entities/market/model/apis/market-order.api.ts +++ b/src/entities/market/model/apis/market-order.api.ts @@ -21,3 +21,19 @@ export const marketOrderAPI = async (markets: string): Promise => { + const response = await fetch("/api/market-order", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ market }), + }); + + if (!response.ok) { + console.error("Sync failed:", response.status, response.statusText); + throw new Error("Failed to fetch sync market order data"); + } + + const data = await response.json(); + return data; +}; From 8b7949f16d84bcf1d880815aa020b9af421fc6e2 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Thu, 13 Nov 2025 21:24:11 +0900 Subject: [PATCH 09/15] =?UTF-8?q?fix:=20refetch=20=EC=8B=9C=EA=B0=84=201?= =?UTF-8?q?=EC=B4=88=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/market/model/hooks/useGetMarketOrder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/entities/market/model/hooks/useGetMarketOrder.ts b/src/entities/market/model/hooks/useGetMarketOrder.ts index a7f5659..e0f94c8 100644 --- a/src/entities/market/model/hooks/useGetMarketOrder.ts +++ b/src/entities/market/model/hooks/useGetMarketOrder.ts @@ -7,6 +7,6 @@ export const useGetMarketOrder = (markets: string) => { queryKey: ["market-order", markets], queryFn: () => marketOrderAPI(markets), - refetchInterval: 5000, + refetchInterval: 1000, }); }; From d109061054b343a032c9d62cdc40212dd51ffbee Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Thu, 13 Nov 2025 21:25:43 +0900 Subject: [PATCH 10/15] =?UTF-8?q?feat:=20realtime=20=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EA=B0=80=EC=A0=B8=EC=98=A4?= =?UTF-8?q?=EA=B8=B0=20hook=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/market/model/hooks/index.ts | 1 + .../model/hooks/useMarketOrderRealtime.ts | 91 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 src/entities/market/model/hooks/useMarketOrderRealtime.ts diff --git a/src/entities/market/model/hooks/index.ts b/src/entities/market/model/hooks/index.ts index a58b0f2..50b35e8 100644 --- a/src/entities/market/model/hooks/index.ts +++ b/src/entities/market/model/hooks/index.ts @@ -1 +1,2 @@ export * from "./useGetMarketOrder"; +export * from "./useMarketOrderRealtime"; diff --git a/src/entities/market/model/hooks/useMarketOrderRealtime.ts b/src/entities/market/model/hooks/useMarketOrderRealtime.ts new file mode 100644 index 0000000..c3486b4 --- /dev/null +++ b/src/entities/market/model/hooks/useMarketOrderRealtime.ts @@ -0,0 +1,91 @@ +"use client"; + +import { useEffect, useState } from "react"; + +import { createClient } from "@/shared"; + +import { OrderbookUnit } from "../types"; + +export interface MarketOrderRealtimeData { + market: string; + orderbook_units: OrderbookUnit[]; + timestamp: number; +} + +export const useMarketOrderRealtime = (market: string) => { + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const supabase = createClient(); + + // 초기 데이터 로드 + const fetchInitialData = async () => { + try { + const { data: marketData, error: fetchError } = await supabase + .from("market_orders") + .select("*") + .eq("market", market) + .single(); + + if (fetchError) { + if (fetchError.code !== "PGRST116") { + // 데이터가 없는 경우가 아니라면 에러 처리 + throw fetchError; + } + } else if (marketData) { + setData({ + market: marketData.market, + orderbook_units: marketData.orderbook_units as OrderbookUnit[], + timestamp: marketData.timestamp, + }); + } + setIsLoading(false); + } catch (err) { + setError(err as Error); + setIsLoading(false); + } + }; + + fetchInitialData(); + + // Realtime 구독 설정 + const channel = supabase + .channel(`market_orders:${market}`) + .on( + "postgres_changes", + { + event: "*", + schema: "public", + table: "market_orders", + filter: `market=eq.${market}`, + }, + (payload) => { + console.log("Realtime update:", payload); + + if (payload.eventType === "INSERT" || payload.eventType === "UPDATE") { + const newData = payload.new as { + market: string; + orderbook_units: unknown; + timestamp: number; + }; + + setData({ + market: newData.market, + orderbook_units: newData.orderbook_units as OrderbookUnit[], + timestamp: newData.timestamp, + }); + } + }, + ) + .subscribe(); + + // Cleanup + return () => { + supabase.removeChannel(channel); + }; + }, [market]); + + return { data, isLoading, error }; +}; From 9a36f494fd9a657c9b09dde2f0654676208a2829 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Thu, 13 Nov 2025 21:26:02 +0900 Subject: [PATCH 11/15] =?UTF-8?q?chore:=20market=20order=20db=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supabase/migrations/create_market_orders.sql | 48 ++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 supabase/migrations/create_market_orders.sql diff --git a/supabase/migrations/create_market_orders.sql b/supabase/migrations/create_market_orders.sql new file mode 100644 index 0000000..b3acd4d --- /dev/null +++ b/supabase/migrations/create_market_orders.sql @@ -0,0 +1,48 @@ +-- Create market_orders table for real-time orderbook data +CREATE TABLE IF NOT EXISTS market_orders ( + id BIGSERIAL PRIMARY KEY, + market VARCHAR(20) NOT NULL UNIQUE, + orderbook_units JSONB NOT NULL, + timestamp BIGINT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Create index for faster market lookup +CREATE INDEX IF NOT EXISTS idx_market_orders_market ON market_orders(market); +CREATE INDEX IF NOT EXISTS idx_market_orders_timestamp ON market_orders(timestamp); + +-- Enable Row Level Security +ALTER TABLE market_orders ENABLE ROW LEVEL SECURITY; + +-- Create policy to allow all operations for authenticated users +CREATE POLICY "Allow all operations for authenticated users" + ON market_orders + FOR ALL + TO authenticated + USING (true) + WITH CHECK (true); + +-- Create policy to allow read access for anonymous users +CREATE POLICY "Allow read access for anonymous users" + ON market_orders + FOR SELECT + TO anon + USING (true); + +-- Enable Realtime +ALTER PUBLICATION supabase_realtime ADD TABLE market_orders; + +-- Create updated_at trigger +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER update_market_orders_updated_at + BEFORE UPDATE ON market_orders + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); From 1924b0ccb0d8993935840d5dbcf273336683599c Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Thu, 13 Nov 2025 21:27:59 +0900 Subject: [PATCH 12/15] =?UTF-8?q?feat:=20supabase=20db=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EA=B0=80=EC=A0=B8=EC=98=A4?= =?UTF-8?q?=EA=B8=B0=20route=20handler=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/market-order/route.ts | 76 +++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/app/api/market-order/route.ts diff --git a/src/app/api/market-order/route.ts b/src/app/api/market-order/route.ts new file mode 100644 index 0000000..2b9a4fe --- /dev/null +++ b/src/app/api/market-order/route.ts @@ -0,0 +1,76 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { marketOrderAPI } from "@/entities"; +import { Json, createClient } from "@/shared"; + +export async function POST(request: NextRequest) { + try { + const { market } = await request.json(); + + if (!market) { + return NextResponse.json({ error: "Market is required" }, { status: 400 }); + } + + // Upbit API에서 데이터 가져오기 + const upbitData = await marketOrderAPI(market); + const marketData = upbitData[0]; + + if (!marketData) { + return NextResponse.json({ error: "No data from Upbit API" }, { status: 404 }); + } + + // Supabase에 저장 (upsert) + const supabase = createClient(); + const { data, error } = await supabase + .from("market_orders") + .upsert( + { + market: marketData.market, + orderbook_units: marketData.orderbook_units as unknown as Json, + timestamp: marketData.timestamp, + }, + { + onConflict: "market", + }, + ) + .select() + .single(); + + if (error) { + console.error("Supabase upsert error:", error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } + + return NextResponse.json({ success: true, data }); + } catch (error) { + console.error("Sync error:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} + +// GET 요청으로 특정 마켓 조회 +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const market = searchParams.get("market"); + + if (!market) { + return NextResponse.json({ error: "Market is required" }, { status: 400 }); + } + + const supabase = createClient(); + const { data, error } = await supabase.from("market_orders").select("*").eq("market", market).single(); + + if (error) { + if (error.code === "PGRST116") { + return NextResponse.json({ error: "Market not found" }, { status: 404 }); + } + return NextResponse.json({ error: error.message }, { status: 500 }); + } + + return NextResponse.json(data); + } catch (error) { + console.error("Get error:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} From 12706516719b4e2a9b38a62226f81cdd731e1aff Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Thu, 13 Nov 2025 21:29:25 +0900 Subject: [PATCH 13/15] =?UTF-8?q?feat(order):=20=ED=98=B8=EA=B0=80=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=EA=B8=B0=EB=8A=A5=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 --- .../components/features/order/OrderTable.tsx | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/src/features/home/components/features/order/OrderTable.tsx b/src/features/home/components/features/order/OrderTable.tsx index 6c00edd..1e8f50f 100644 --- a/src/features/home/components/features/order/OrderTable.tsx +++ b/src/features/home/components/features/order/OrderTable.tsx @@ -1,17 +1,43 @@ "use client"; -import { useGetMarketOrder } from "@/entities"; +import { useEffect } from "react"; + +import { syncMarketOrderAPI, useMarketOrderRealtime } from "@/entities"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/shared"; +import { useMutation } from "@tanstack/react-query"; export const OrderTable = () => { - const { data: orderData } = useGetMarketOrder("KRW-WAXP"); + const market = "KRW-WAXP"; + const { data: marketOrder, isLoading, error } = useMarketOrderRealtime(market); + + const { mutate: syncData } = useMutation({ + mutationFn: () => syncMarketOrderAPI(market), + onError: (error) => { + console.error("Sync failed:", error); + }, + onSuccess: () => { + console.log("Sync successful"); + }, + }); + + // 주기적으로 Upbit 데이터를 Supabase에 동기화 + useEffect(() => { + // 즉시 실행 + syncData(); - // 배열의 첫 번째 요소 가져오기 - const marketOrder = orderData?.[0]; + // 1초마다 동기화 + const interval = setInterval(() => { + syncData(); + }, 1000); - console.log("Market Order Data:", marketOrder); + return () => clearInterval(interval); + }, [market, syncData]); + + if (error) { + return
Error: {error.message}
; + } - if (!marketOrder) { + if (isLoading || !marketOrder) { return
Loading...
; } From e5c24808ce10d24b45d2fc864529c2059074aacf Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Thu, 13 Nov 2025 22:50:05 +0900 Subject: [PATCH 14/15] =?UTF-8?q?fix(order):=20=ED=98=B8=EA=B0=80=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B8=B0=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 --- .../components/features/order/OrderTable.tsx | 95 +++++++------------ 1 file changed, 36 insertions(+), 59 deletions(-) diff --git a/src/features/home/components/features/order/OrderTable.tsx b/src/features/home/components/features/order/OrderTable.tsx index 1e8f50f..f6f3366 100644 --- a/src/features/home/components/features/order/OrderTable.tsx +++ b/src/features/home/components/features/order/OrderTable.tsx @@ -43,14 +43,8 @@ export const OrderTable = () => { // 호가 데이터를 매도/매수로 분리 (최대 15개씩) const orderbookUnits = marketOrder.orderbook_units || []; - const askOrders = orderbookUnits.slice(0, 6); // 매도 호가 - const bidOrders = orderbookUnits.slice(0, 6); // 매수 호가 - - // 현재가 판단: 첫 번째 호가의 매수/매도 가격 비교 - const firstOrder = orderbookUnits[0]; - const currentPrice = firstOrder?.ask_price || firstOrder?.bid_price || 0; - const isBidPrice = firstOrder?.bid_price === currentPrice; - const currentVolume = isBidPrice ? firstOrder?.bid_size : firstOrder?.ask_size; + const askOrders = orderbookUnits.slice(0, 15).reverse(); // 매도 호가 + const bidOrders = orderbookUnits.slice(0, 15); // 매수 호가 // 최대 거래량 계산 (그래프 바 비율 계산용) const maxBidSize = Math.max(...bidOrders.map((o) => o.bid_size), 0); @@ -60,75 +54,58 @@ export const OrderTable = () => { return ( - - + + 호가 - {/* 매수 호가 (Bid) - 파란색 */} - {bidOrders.reverse().map((order, index) => { - const volumePercent = maxVolume > 0 ? (order.bid_size / maxVolume) * 100 : 0; + {/* 매도 호가 (Ask) - 파란색 */} + {askOrders.map((order, index) => { + const volumePercent = maxVolume > 0 ? (order.ask_size / maxVolume) * 100 : 0; return ( - {/* 그래프 바 배경 */} -
- - - {order.bid_size.toLocaleString()} + + {/* 그래프 바 배경 (오른쪽에서 왼쪽으로) */} +
+
+ {order.ask_size.toLocaleString()} +
- - {order.bid_price.toLocaleString()} + +
+ {order.ask_price.toLocaleString()} +
- + ); })} - {/* 현재가 구분선 */} - - {/* 현재가 그래프 바 (매수/매도에 따라 방향 다름) */} - {currentVolume && ( -
0 ? ((currentVolume / maxVolume) * 100) / 3 : 0}%`, - }} - /> - )} - - - {isBidPrice ? currentVolume?.toLocaleString() : ""} - - - {currentPrice.toLocaleString()} - - - {!isBidPrice ? currentVolume?.toLocaleString() : ""} - - - - {/* 매도 호가 (Ask) - 빨간색 */} - {askOrders.map((order, index) => { - const volumePercent = maxVolume > 0 ? (order.ask_size / maxVolume) * 100 : 0; + {/* 매수 호가 (Bid) - 빨간색 */} + {bidOrders.map((order, index) => { + const volumePercent = maxVolume > 0 ? (order.bid_size / maxVolume) * 100 : 0; return ( - {/* 그래프 바 배경 (오른쪽에서 시작) */} -
- - - - {order.ask_price.toLocaleString()} + + +
+ {order.bid_price.toLocaleString()} +
- - {order.ask_size.toLocaleString()} + + {/* 그래프 바 배경 (오른쪽에서 왼쪽으로) */} +
+
+ {order.bid_size.toLocaleString()} +
); From 974f54c0efa0511f8743bf5f199612b69c68a843 Mon Sep 17 00:00:00 2001 From: Dobbymin Date: Thu, 13 Nov 2025 23:20:35 +0900 Subject: [PATCH 15/15] =?UTF-8?q?fix:=20=EB=94=94=EB=B2=84=EA=B9=85?= =?UTF-8?q?=EC=9A=A9=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/market/model/hooks/useMarketOrderRealtime.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/entities/market/model/hooks/useMarketOrderRealtime.ts b/src/entities/market/model/hooks/useMarketOrderRealtime.ts index c3486b4..09b1d4c 100644 --- a/src/entities/market/model/hooks/useMarketOrderRealtime.ts +++ b/src/entities/market/model/hooks/useMarketOrderRealtime.ts @@ -62,8 +62,6 @@ export const useMarketOrderRealtime = (market: string) => { filter: `market=eq.${market}`, }, (payload) => { - console.log("Realtime update:", payload); - if (payload.eventType === "INSERT" || payload.eventType === "UPDATE") { const newData = payload.new as { market: string;