-
Notifications
You must be signed in to change notification settings - Fork 0
실시간 호가창 구현 #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
실시간 호가창 구현 #4
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Summary of ChangesHello @Dobbymin, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! 이 PR은 Upbit API에서 실시간 호가 데이터를 가져와 Supabase에 저장하고, 이를 WebSocket을 통해 클라이언트 UI에 실시간으로 반영하는 시스템을 구축합니다. 이를 통해 사용자들은 최신 시장 데이터를 즉각적으로 확인할 수 있으며, 거래량에 따른 시각적인 피드백도 제공됩니다. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
이 PR은 Supabase Realtime과 Upbit API를 연동하여 실시간 호가창을 구현하는 중요한 기능 추가입니다. 전반적인 아키텍처는 잘 설계되었으나, 실제 운영 환경에서 발생할 수 있는 몇 가지 심각한 문제점들이 보입니다. 특히 데이터 동기화 방식, API 인증, 그리고 실시간 데이터 처리의 정확성과 관련된 부분에서 수정이 필요합니다. 아래에 각 파일별로 상세한 리뷰와 코드 제안을 남겼으니 확인 부탁드립니다.
| } | ||
|
|
||
| // Supabase에 저장 (upsert) | ||
| const supabase = createClient(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
API 라우트에서 Supabase에 데이터를 upsert할 때 클라이언트용 createClient()를 사용하고 있습니다. 이 함수는 익명(anon) 키를 사용하는 브라우저 클라이언트를 생성하므로, market_orders 테이블의 RLS(행 수준 보안) 정책에 의해 쓰기 작업이 차단됩니다.
서버 환경에서는 RLS를 우회하기 위해 서비스 역할(service role) 키를 사용하는 서버용 클라이언트를 생성해야 합니다. PR 설명에 언급하신 것처럼 @supabase/supabase-js의 createClient를 서비스 키와 함께 사용하도록 수정해야 합니다.
| useEffect(() => { | ||
| // 즉시 실행 | ||
| syncData(); | ||
|
|
||
| // 1초마다 동기화 | ||
| const interval = setInterval(() => { | ||
| syncData(); | ||
| }, 1000); | ||
|
|
||
| return () => clearInterval(interval); | ||
| }, [market, syncData]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
데이터 동기화를 각 클라이언트가 1초마다 setInterval을 통해 개별적으로 트리거하고 있습니다. 만약 여러 사용자가 동시에 이 페이지에 접속하면, (사용자 수) x (초당 1회) 만큼의 요청이 서버로 전송됩니다. 이는 Upbit API의 요청 제한(초당 10회)을 쉽게 초과할 수 있으며, 서버에 불필요한 부하를 유발합니다.
데이터 동기화 로직은 클라이언트에서 분리하여, 서버 측에서 단일 프로세스(예: Cron Job, 별도 워커)가 주기적으로 실행하도록 구현하는 것이 바람직합니다. 클라이언트는 동기화된 데이터를 Supabase Realtime을 통해 수신하기만 해야 합니다.
| 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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
초기 데이터를 가져오는 fetchInitialData 함수 호출과 실시간 구독 설정 사이에 경쟁 조건(race condition)이 존재합니다. 만약 초기 데이터를 가져온 직후, 그리고 실시간 구독이 활성화되기 전에 데이터베이스에 변경이 발생하면 해당 업데이트를 놓치게 됩니다.
이 문제를 해결하려면, subscribe의 콜백 함수를 사용하여 채널이 성공적으로 구독되었을 때(SUBSCRIBED 상태) 초기 데이터를 가져오도록 로직을 변경하는 것이 좋습니다. 이렇게 하면 데이터 누락 없이 안전하게 초기 상태를 동기화할 수 있습니다.
예시:
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await fetchInitialData();
}
});
// fetchInitialData(); // 이 위치에서 호출 제거| {/* 매도 호가 (Ask) - 파란색 */} | ||
| {askOrders.map((order, index) => { | ||
| const volumePercent = maxVolume > 0 ? (order.ask_size / maxVolume) * 100 : 0; | ||
| return ( | ||
| <TableRow | ||
| key={`ask-${order.ask_price}-${index}`} | ||
| className='border-b border-white bg-blue-100 text-sm hover:bg-blue-50' | ||
| > | ||
| <TableCell className='relative w-1/3 p-0'> | ||
| {/* 그래프 바 배경 (오른쪽에서 왼쪽으로) */} | ||
| <div className='absolute top-0 right-0 h-full bg-blue-200' style={{ width: `${volumePercent}%` }} /> | ||
| <div className='relative z-10 px-4 py-2 text-end font-roboto text-xs text-black'> | ||
| {order.ask_size.toLocaleString()} | ||
| </div> | ||
| </TableCell> | ||
| <TableCell className='relative w-1/3 p-0'> | ||
| <div className='relative z-10 px-4 py-2 text-center font-roboto font-semibold text-increase'> | ||
| {order.ask_price.toLocaleString()} | ||
| </div> | ||
| </TableCell> | ||
| <TableCell className='w-1/3' /> | ||
| </TableRow> | ||
| ); | ||
| })} | ||
|
|
||
| <TableRow className='flex h-full border-white bg-red-100 px-0 text-sm hover:bg-red-300'> | ||
| <TableCell className='w-1/3' /> | ||
| <TableCell className='w-1/3 text-center font-roboto text-black'>17.9</TableCell> | ||
| <TableCell className='w-1/3 text-start font-roboto text-black'>324,130.913</TableCell> | ||
| </TableRow> | ||
| <TableRow className='flex h-full border-white bg-red-100 px-0 text-sm hover:bg-red-300'> | ||
| <TableCell className='w-1/3' /> | ||
| <TableCell className='w-1/3 text-center font-roboto text-black'>17.9</TableCell> | ||
| <TableCell className='w-1/3 text-start font-roboto text-black'>324,130.913</TableCell> | ||
| </TableRow> | ||
| <TableRow className='flex h-full border-white bg-red-100 px-0 text-sm hover:bg-red-300'> | ||
| <TableCell className='w-1/3' /> | ||
| <TableCell className='w-1/3 text-center font-roboto text-black'>17.9</TableCell> | ||
| <TableCell className='w-1/3 text-start font-roboto text-black'>324,130.913</TableCell> | ||
| </TableRow> | ||
| <TableRow className='flex h-full border-white bg-red-100 px-0 text-sm hover:bg-red-300'> | ||
| <TableCell className='w-1/3' /> | ||
| <TableCell className='w-1/3 text-center font-roboto text-black'>17.9</TableCell> | ||
| <TableCell className='w-1/3 text-start font-roboto text-black'>324,130.913</TableCell> | ||
| </TableRow> | ||
| <TableRow className='flex h-full border-white bg-red-100 px-0 text-sm hover:bg-red-300'> | ||
| <TableCell className='w-1/3' /> | ||
| <TableCell className='w-1/3 text-center font-roboto text-black'>17.9</TableCell> | ||
| <TableCell className='w-1/3 text-start font-roboto text-black'>324,130.913</TableCell> | ||
| </TableRow> | ||
| <TableRow className='flex h-full border-white bg-red-100 px-0 text-sm hover:bg-red-300'> | ||
| <TableCell className='w-1/3' /> | ||
| <TableCell className='w-1/3 text-center font-roboto text-black'>17.9</TableCell> | ||
| <TableCell className='w-1/3 text-start font-roboto text-black'>324,130.913</TableCell> | ||
| </TableRow> | ||
| {/* 매수 호가 (Bid) - 빨간색 */} | ||
| {bidOrders.map((order, index) => { | ||
| const volumePercent = maxVolume > 0 ? (order.bid_size / maxVolume) * 100 : 0; | ||
| return ( | ||
| <TableRow | ||
| key={`bid-${order.bid_price}-${index}`} | ||
| className='border-b border-white bg-red-100 text-sm hover:bg-red-50' | ||
| > | ||
| <TableCell className='w-1/3' /> | ||
| <TableCell className='relative w-1/3 border-white p-0'> | ||
| <div className='relative z-10 px-4 py-2 text-center font-roboto font-semibold text-decrease'> | ||
| {order.bid_price.toLocaleString()} | ||
| </div> | ||
| </TableCell> | ||
| <TableCell className='relative w-1/3 border-white p-0'> | ||
| {/* 그래프 바 배경 (오른쪽에서 왼쪽으로) */} | ||
| <div className='absolute inset-0 right-0 h-full bg-red-200' style={{ width: `${volumePercent}%` }} /> | ||
| <div className='relative z-10 px-4 py-2 text-start font-roboto text-xs text-black'> | ||
| {order.bid_size.toLocaleString()} | ||
| </div> | ||
| </TableCell> | ||
| </TableRow> | ||
| ); | ||
| })} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
호가창 UI의 색상 및 그래프 바 정렬이 코드, 주석, PR 설명, 스크린샷 간에 서로 일치하지 않아 혼란을 유발합니다.
- 코드 주석:
매도(Ask) - 파란색,매수(Bid) - 빨간색 - 코드 구현:
askOrders는bg-blue-100,bidOrders는bg-red-100 - 스크린샷: 매도(위)는 빨간색, 매수(아래)는 파란색
- PR 설명:
매수 호가: 파란색 배경 + 좌측 정렬 그래프 바,매도 호가: 빨간색 배경 + 우측 정렬 그래프 바 - 그래프 바 정렬: 코드에서
askOrders와bidOrders의 그래프 바가 모두 오른쪽(right-0)을 기준으로 정렬되어 있는 것으로 보입니다.
일반적으로 매수는 상승을 의미하는 파란색/초록색, 매도는 하락을 의미하는 빨간색을 사용합니다. 의도한 UI 디자인에 맞게 코드, 주석, 스타일을 통일성 있게 수정해야 합니다.
| setData({ | ||
| market: marketData.market, | ||
| orderbook_units: marketData.orderbook_units as OrderbookUnit[], | ||
| timestamp: marketData.timestamp, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
데이터베이스에서 가져온 orderbook_units 데이터를 OrderbookUnit[] 타입으로 강제 형변환(as)하고 있습니다. JSONB 타입은 런타임에 타입 안전성을 보장하지 않으므로, 예기치 않은 데이터 구조로 인해 런타임 에러가 발생할 수 있습니다.
PR 설명에서도 언급하셨듯이, Zod와 같은 라이브러리를 사용하여 런타임에 데이터의 유효성을 검사하고 파싱하는 것이 더 안전한 방법입니다. 이를 통해 타입 안정성을 높이고 잠재적인 버그를 예방할 수 있습니다. 실시간으로 수신되는 payload.new 데이터(72-76행)에도 동일하게 적용하는 것을 권장합니다.
| const volumePercent = maxVolume > 0 ? (order.ask_size / maxVolume) * 100 : 0; | ||
| return ( | ||
| <TableRow | ||
| key={`ask-${order.ask_price}-${index}`} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
React 리스트 렌더링 시 key prop에 index를 사용하고 있습니다. 실시간으로 데이터가 변경되고 순서가 바뀔 수 있는 리스트에서 index를 key로 사용하면, React가 컴포넌트의 상태를 잘못 재사용하거나 예기치 않은 동작을 유발할 수 있습니다.
호가 데이터에서 ask_price는 각 항목을 고유하게 식별할 수 있으므로, 이를 key로 사용하는 것이 더 안정적입니다. 94행의 bidOrders 렌더링 시에도 동일하게 적용해주세요.
| key={`ask-${order.ask_price}-${index}`} | |
| key={`ask-${order.ask_price}`} |
| </TableCell> | ||
| <TableCell className='relative w-1/3 border-white p-0'> | ||
| {/* 그래프 바 배경 (오른쪽에서 왼쪽으로) */} | ||
| <div className='absolute inset-0 right-0 h-full bg-red-200' style={{ width: `${volumePercent}%` }} /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
매수 호가(bid order)의 거래량 그래프 바에 적용된 Tailwind CSS 클래스가 absolute inset-0 right-0 h-full로 되어 있습니다. inset-0은 top: 0; right: 0; bottom: 0; left: 0;을 의미하므로 right-0와 h-full 클래스는 중복되거나 의도치 않은 레이아웃을 유발할 수 있습니다.
의도한 레이아웃을 명확히 표현하기 위해 중복되는 클래스를 제거하는 것이 좋습니다. 예를 들어, askOrders에서 사용된 것처럼 absolute top-0 right-0 h-full로 수정할 수 있습니다.
| <div className='absolute inset-0 right-0 h-full bg-red-200' style={{ width: `${volumePercent}%` }} /> | |
| <div className='absolute top-0 right-0 h-full bg-red-200' style={{ width: `${volumePercent}%` }} /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR implements a real-time order book system using Supabase Realtime and Upbit API integration. The system fetches order book data from Upbit every second, stores it in Supabase, and streams updates to clients via WebSocket subscriptions.
Key Changes:
- Database schema with
market_orderstable and Realtime publication - API endpoint (
/api/market-order) for syncing Upbit data to Supabase - Custom React hook (
useMarketOrderRealtime) for WebSocket-based data subscriptions - Order book UI component with volume visualization bars
Reviewed Changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
supabase/migrations/create_market_orders.sql |
Creates market_orders table with JSONB storage, indexes, RLS policies, and update trigger for real-time order book data |
src/shared/types/database.type.ts |
Adds TypeScript definitions for market_orders and other database tables (chart, coins, trade, transactions, wallet) |
src/features/home/components/features/order/OrderTable.tsx |
Implements order book UI with real-time updates, volume bars, and 1-second polling; contains color/styling inconsistencies |
src/entities/market/model/types/orderbook-units.type.ts |
Defines OrderbookUnit type structure for ask/bid price and size data |
src/entities/market/model/types/index.ts |
Exports orderbook-units type for use across the application |
src/entities/market/model/index.ts |
Adds hooks export to entity module public API |
src/entities/market/model/hooks/useMarketOrderRealtime.ts |
Custom hook for subscribing to Supabase Realtime updates with initial data fetch and cleanup |
src/entities/market/model/hooks/useGetMarketOrder.ts |
React Query hook for polling Upbit API directly (appears unused in current implementation) |
src/entities/market/model/hooks/index.ts |
Exports market order hooks for external consumption |
src/entities/market/model/apis/market-order.api.ts |
API functions for fetching from Upbit and syncing via internal API endpoint; contains return type mismatch |
src/entities/market/model/apis/index.ts |
Adds market-order API to exports |
src/app/api/market-order/route.ts |
Next.js API route handling Upbit→Supabase sync and queries; uses browser client instead of server client |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| </TableCell> | ||
| <TableCell className='relative w-1/3 border-white p-0'> | ||
| {/* 그래프 바 배경 (오른쪽에서 왼쪽으로) */} | ||
| <div className='absolute inset-0 right-0 h-full bg-red-200' style={{ width: `${volumePercent}%` }} /> |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of both inset-0 and right-0 is redundant. inset-0 sets all sides (top, right, bottom, left) to 0, so adding right-0 has no effect. For a bar that grows from left to right, you should use absolute left-0 h-full instead. This will properly anchor the bar to the left side.
| <div className='absolute inset-0 right-0 h-full bg-red-200' style={{ width: `${volumePercent}%` }} /> | |
| <div className='absolute right-0 h-full bg-red-200' style={{ width: `${volumePercent}%` }} /> |
| return NextResponse.json({ error: "Market is required" }, { status: 400 }); | ||
| } | ||
|
|
||
| const supabase = createClient(); |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same issue as line 23 - in an API route (server-side), using createClient() from @/shared creates a browser client, which is not appropriate for server-side operations. For API routes, you should use a server-specific client or the Service Role Key.
| const volumePercent = maxVolume > 0 ? (order.ask_size / maxVolume) * 100 : 0; | ||
| return ( | ||
| <TableRow | ||
| key={`ask-${order.ask_price}-${index}`} |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using index in the key is not ideal when the list order might change (e.g., prices shifting). If two orders have the same ask_price, this could lead to React rendering issues. Consider using a more stable unique identifier, or at minimum combine with timestamp: key={ask-${order.ask_price}-${marketOrder.timestamp}-${index}}. The same issue applies to line 94 for bid orders.
| key={`ask-${order.ask_price}-${index}`} | |
| key={`ask-${order.ask_price}-${marketOrder.timestamp}-${index}`} |
| }); | ||
|
|
||
| if (!response.ok) { | ||
| console.error("Upbit API error", response.status, response.statusText); |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Console.error statements in production code should be wrapped in development checks or replaced with proper logging. Since this is called every second from the component, these logs will quickly clutter the console. The same applies to line 33.
| FOR SELECT | ||
| TO anon | ||
| USING (true); | ||
|
|
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The RLS policies only allow SELECT for anonymous users (line 29), but the API route at /api/market-order uses a browser client to perform UPSERT operations. Since the API route runs with an anonymous client context, it will be blocked by RLS when trying to INSERT or UPDATE. You need to either: 1) Add a policy allowing anonymous users to INSERT/UPDATE, 2) Use a Service Role Key in the API route (as mentioned in the PR description), or 3) Call the API with an authenticated user context.
| -- Create policy to allow insert access for anonymous users | |
| CREATE POLICY "Allow insert access for anonymous users" | |
| ON market_orders | |
| FOR INSERT | |
| TO anon | |
| WITH CHECK (true); | |
| -- Create policy to allow update access for anonymous users | |
| CREATE POLICY "Allow update access for anonymous users" | |
| ON market_orders | |
| FOR UPDATE | |
| TO anon | |
| USING (true) | |
| WITH CHECK (true); |
| {/* 매도 호가 (Ask) - 파란색 */} | ||
| {askOrders.map((order, index) => { | ||
| const volumePercent = maxVolume > 0 ? (order.ask_size / maxVolume) * 100 : 0; | ||
| return ( | ||
| <TableRow | ||
| key={`ask-${order.ask_price}-${index}`} | ||
| className='border-b border-white bg-blue-100 text-sm hover:bg-blue-50' | ||
| > | ||
| <TableCell className='relative w-1/3 p-0'> | ||
| {/* 그래프 바 배경 (오른쪽에서 왼쪽으로) */} | ||
| <div className='absolute top-0 right-0 h-full bg-blue-200' style={{ width: `${volumePercent}%` }} /> | ||
| <div className='relative z-10 px-4 py-2 text-end font-roboto text-xs text-black'> | ||
| {order.ask_size.toLocaleString()} | ||
| </div> | ||
| </TableCell> | ||
| <TableCell className='relative w-1/3 p-0'> | ||
| <div className='relative z-10 px-4 py-2 text-center font-roboto font-semibold text-increase'> | ||
| {order.ask_price.toLocaleString()} | ||
| </div> | ||
| </TableCell> | ||
| <TableCell className='w-1/3' /> | ||
| </TableRow> | ||
| ); | ||
| })} |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment says "매도 호가 (Ask) - 파란색" (Ask orders - blue color) but the description states that ask orders should be displayed in red (매도=sell=red). According to the PR description, "매수 호가: 파란색" (buy/bid orders in blue) and "매도 호가: 빨간색" (sell/ask orders in red). The colors are swapped. The ask orders (sell orders) should use red styling (bg-red-100, bg-red-200), not blue.
| {/* 매수 호가 (Bid) - 빨간색 */} | ||
| {bidOrders.map((order, index) => { | ||
| const volumePercent = maxVolume > 0 ? (order.bid_size / maxVolume) * 100 : 0; | ||
| return ( | ||
| <TableRow | ||
| key={`bid-${order.bid_price}-${index}`} | ||
| className='border-b border-white bg-red-100 text-sm hover:bg-red-50' | ||
| > | ||
| <TableCell className='w-1/3' /> | ||
| <TableCell className='relative w-1/3 border-white p-0'> | ||
| <div className='relative z-10 px-4 py-2 text-center font-roboto font-semibold text-decrease'> | ||
| {order.bid_price.toLocaleString()} | ||
| </div> | ||
| </TableCell> | ||
| <TableCell className='relative w-1/3 border-white p-0'> | ||
| {/* 그래프 바 배경 (오른쪽에서 왼쪽으로) */} | ||
| <div className='absolute inset-0 right-0 h-full bg-red-200' style={{ width: `${volumePercent}%` }} /> | ||
| <div className='relative z-10 px-4 py-2 text-start font-roboto text-xs text-black'> | ||
| {order.bid_size.toLocaleString()} | ||
| </div> | ||
| </TableCell> | ||
| </TableRow> | ||
| ); | ||
| })} |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment says "매수 호가 (Bid) - 빨간색" (Bid orders - red color) but bid orders should be displayed in blue according to the PR description. The colors are swapped. The bid orders (buy orders) should use blue styling (bg-blue-100, bg-blue-200), not red.
| const volumePercent = maxVolume > 0 ? (order.bid_size / maxVolume) * 100 : 0; | ||
| return ( | ||
| <TableRow | ||
| key={`bid-${order.bid_price}-${index}`} |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using index in the key is not ideal when the list order might change. If two orders have the same bid_price, this could lead to React rendering issues. Consider using a more stable unique identifier, or at minimum combine with timestamp: key={bid-${order.bid_price}-${marketOrder.timestamp}-${index}}.
| console.error("Sync failed:", error); | ||
| }, | ||
| onSuccess: () => { | ||
| console.log("Sync successful"); |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Console.error statements in production code can impact performance when called frequently (every second in this case). Consider removing or wrapping in a development-only check: if (process.env.NODE_ENV === 'development') console.error(...). The same applies to the console.log on line 19.
| console.error("Sync failed:", error); | |
| }, | |
| onSuccess: () => { | |
| console.log("Sync successful"); | |
| if (process.env.NODE_ENV === 'development') { | |
| console.error("Sync failed:", error); | |
| } | |
| }, | |
| onSuccess: () => { | |
| if (process.env.NODE_ENV === 'development') { | |
| console.log("Sync successful"); | |
| } |
| .single(); | ||
|
|
||
| if (error) { | ||
| console.error("Supabase upsert error:", error); |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While console.error in API routes is acceptable for debugging, consider using a proper logging service in production (e.g., Winston, Pino) for better monitoring and error tracking. The same applies to lines 46 and 73.
📝 요약 (Summary)
Supabase Realtime을 활용한 실시간 호가창 시스템 구축. Upbit API에서 1초마다 데이터를 수집하여 Supabase에 저장하고, WebSocket을 통해 실시간으로 클라이언트에 전달하는 아키텍처를 구현했습니다.
✅ 주요 변경 사항 (Key Changes)
market_orders테이블 생성 및 Realtime 설정/api/market-order-sync)useMarketOrderRealtime)💻 상세 구현 내용 (Implementation Details)
1. API 구현
/src/app/api/market-order-sync/route.ts핵심 로직:
onConflict: "market"로 중복 방지as unknown as Json으로 JSONB 호환성 확보2. Realtime 구독 훅
/src/entities/market/model/hooks/useMarketOrderRealtime.ts동작 원리:
market_orders:${market}채널 생성filter: market=eq.${market}로 특정 마켓만 수신payload.new로 상태 업데이트removeChannel로 메모리 누수 방지4. UI 컴포넌트
/src/features/home/components/features/order/OrderTable.tsxUI 구현 포인트:
absolute포지셔닝으로 배경 바 구현(order.size / maxVolume) * 100으로 동적 너비z-10적용5. 타입 정의
/src/shared/types/database.type.ts🚀 트러블 슈팅 (Trouble Shooting)
1. RLS 정책 위반 오류
문제:
원인: 익명 클라이언트로 UPSERT 시도 시 RLS 정책이 INSERT/UPDATE를 차단
해결:
또는 API Route에서 Service Role Key 사용:
2. JSONB 타입 불일치
문제:
OrderbookUnit[]타입을 JSONB 컬럼에 직접 할당 불가해결:
3. Realtime 구독 메모리 누수
문제: 컴포넌트 언마운트 후에도 WebSocket 연결 유지
해결:
4. useEffect 무한 루프
문제:
syncData함수가 매 렌더링마다 재생성되어 무한 루프 발생해결:
1. Upbit API 속도 제한
2. Realtime 연결 안정성
3. 성능 최적화 여지
4. 다중 마켓 지원
5. 에러 바운더리
6. 타입 안정성
📸 스크린샷 (Screenshots)
호가창 UI
실시간 동기화
#️⃣ 관련 이슈 (Related Issues)