-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add chart websocket connection #5
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
base: main
Are you sure you want to change the base?
Conversation
📝 WalkthroughWalkthrough개발 환경 변수 추가, SockJS/STOMP 의존성 도입, STOMP over SockJS 기반 WebSocketService 구현, StockChart에 실시간 구독·업데이트 통합 및 타입·설정 변경을 적용합니다. (50단어 이내) 변경 사항
시퀀스 다이어그램sequenceDiagram
participant UI as StockChart (UI)
participant WS as WebSocketService
participant Client as STOMP Client
participant Server as Backend Server
UI->>WS: subscribe(stockCode)
WS->>WS: isConnected? → connect() if needed
WS->>Client: SockJS 연결 시도
Client->>Server: WebSocket 핸드셰이크
Server-->>Client: 연결 완료
Client-->>WS: 연결 콜백
WS->>Client: subscribe /topic/stock/askbid/{code}
WS->>Client: subscribe /topic/stock/execution/{code}
WS->>Server: publish /app/stock/subscribe (payload)
loop 실시간 ask/bid
Server->>Client: ask/bid 메시지
Client->>WS: onAskBid 콜백
WS->>UI: onAskBid 전달
UI->>UI: UI/먹스테이트 갱신
end
loop 실시간 execution
Server->>Client: execution 메시지
Client->>WS: onExecution 콜백
WS->>UI: onExecution 전달
UI->>UI: 캔들 생성/증분 업데이트
end
UI->>WS: unsubscribe(stockCode)
WS->>Server: publish /app/stock/unsubscribe (payload)
WS->>Client: 토픽 구독 해제
예상 코드 리뷰 노력🎯 3 (중간) | ⏱️ ~25분 시
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
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.
Actionable comments posted: 9
🤖 Fix all issues with AI agents
In @src/components/StockChart.tsx:
- Line 684: The WebSocket useEffect (the one whose dependency array is currently
[shortCode, selectedOption.interval, basePrice]) uses chartType inside its
message/callback (seen around the callback lines), but chartType is missing from
the dependency array so the callback will capture a stale value; fix by either
adding chartType to that useEffect dependency array or convert chartType to a
ref (e.g., chartTypeRef with useRef and keep it in sync in a separate useEffect)
and use chartTypeRef.current inside the WebSocket callback so it always reads
the latest value.
- Around line 271-279: Comments for maxVisibleBars in StockChart.tsx are
inconsistent with the assigned values; update the comments to match the actual
numbers or adjust the values to match the comments. Locate the block that
branches on selectedOption.interval and sets maxVisibleBars (the branches for
'min', 'day:', 'week:', 'month:', and the fallback) and either change the
numeric literals to the intended limits in the comments or edit the inline
Korean comments to reflect 300, 180, 104, 110, and 30 respectively so the
comment text matches the values.
- Around line 528-537: The fallback uses new Date() (local timezone) causing
timezone mismatch; update the fallback to produce a KST timestamp instead by
creating a KST Date (e.g., const kstNow = new Date(Date.now() + 9 * 60 * 60 *
1000)) and return its ISO datetime portion (e.g.,
kstNow.toISOString().slice(0,19)) so the returned string is in KST; update the
code inside the parser fallback where now/getFullYear/getMonth/... are used to
use this KST computation and formatting instead.
- Around line 612-646: You are mutating the React state array chartData directly
(using chartData[index]= and chartData.push), which breaks React updates;
instead keep mutable realtime data in a ref (e.g., chartDataRef) or always
produce a new array via the state setter: copy chartData, modify the copy
(update existingIndex or push new realtimeCandle), sort the copy, then call
setChartData(prev => newCopy) using the functional updater; ensure
realtimeCandle creation logic (existingIndex, normalizedDateTime,
accumulatedAmount, volume, basePrice, currentPrice) stays the same but applied
to the copied array or the ref and never mutate the original chartData state
in-place.
In @src/pages/StockDetail.tsx:
- Line 110: The current usage of a fallback empty string (code || '') when
rendering StockChart may pass an invalid stock code into StockChart's WebSocket
logic; either remove the fallback and render StockChart only when code is truthy
(e.g., guard with code && <StockChart ...>) so that StockChart always receives a
valid code, or if you must keep the fallback, update the StockChart subscription
logic to explicitly handle/ignore empty strings; update references in
StockDetail where StockChart is rendered and in StockChart's WebSocket
subscription code to implement the chosen fix.
In @src/services/chartService.ts:
- Line 3: chartService.ts defines API_BASE_URL from
import.meta.env.VITE_API_BASE_URL falling back to localhost; create a
.env.production at the repo root and set VITE_API_BASE_URL to your production
API endpoint so the production build picks up the correct URL, then verify your
CI/build uses the .env.production and that the variable is available during the
Vite build (and avoid committing any sensitive secrets).
- Line 3: The constant migration is incomplete: replace the use of
LOCAL_API_BASE_URL in src/services/stockService.ts with
import.meta.env.VITE_API_BASE_URL (matching the pattern used in chartService.ts,
e.g., set API_BASE_URL or equivalent to import.meta.env.VITE_API_BASE_URL ||
"http://localhost:8080") and update any references to LOCAL_API_BASE_URL to use
that new variable; additionally add VITE_API_BASE_URL to your production
environment config (e.g., .env.production) to ensure the variable is defined at
runtime to avoid errors.
In @src/services/websocketService.ts:
- Around line 231-235: The subscription check using if
(this.subscriptions.has(`askbid-${stockCode}`) ||
this.subscriptions.has(`execution-${stockCode}`)) incorrectly blocks retries
when only one of the two channels is subscribed; update the logic in the
subscription routine (the block referencing this.subscriptions.has and
stockCode) to either use AND so both must exist before returning, or better:
check each channel independently and clean up or complete any partial
subscription (e.g., if askbid- exists but execution- missing, attempt execution
subscribe or remove the stale askbid- entry) so partial subscription states do
not prevent retrying the missing channel.
- Around line 137-142: onDisconnect currently clears all subscriptions
(this.subscriptions.clear() and this.replySubscription = null) which prevents
restoring them after automatic reconnect; preserve the subscription info and
implement a restoration step on successful reconnect (e.g., add a
restoreSubscriptions or onConnect handler) that iterates over the stored
this.subscriptions entries and re-sends subscription messages and reattaches
reply handlers (and reinstates this.replySubscription if present), or
alternatively stop clearing the Map in onDisconnect and only mark this.connected
= false so reconnect logic (with reconnectDelay) can call the re-subscribe
routine to re-establish real-time updates.
🧹 Nitpick comments (2)
src/types/chart.ts (1)
24-41: 타입 정의 중복 문제
RealtimeStockData와WebSocketSubscriptionResponse인터페이스가src/services/websocketService.ts(라인 7-23)에도 동일하게 정의되어 있습니다. 이는 DRY 원칙 위반이며, 향후 한쪽만 수정될 경우 타입 불일치가 발생할 수 있습니다.한 곳에서만 타입을 정의하고 다른 곳에서 import하여 사용하는 것을 권장합니다.
♻️ 권장 수정안
src/types/chart.ts에서 WebSocket 관련 타입을 export하고,websocketService.ts에서 import하여 사용:// src/services/websocketService.ts +import { RealtimeStockData, WebSocketSubscriptionResponse } from '@/types/chart'; import { Client, IMessage, StompSubscription } from '@stomp/stompjs'; import SockJS from 'sockjs-client'; const WS_BASE_URL = import.meta.env.VITE_API_BASE_URL || "http://localhost:8080"; const WS_ENDPOINT = "/stock"; -export interface WebSocketSubscriptionResponse { - status: string; - message: string; - stockCode: string; - dataEndpoint?: string; -} - -export interface RealtimeStockData { - dateTime: string; - base: number; - open: number; - high: number; - low: number; - close: number; - volume: number; - accumulatedAmount: number; -} +export type { RealtimeStockData, WebSocketSubscriptionResponse };src/services/websocketService.ts (1)
183-190:onConnect콜백 덮어쓰기 문제
initializeClient()(라인 132-136)에서 설정한onConnect콜백이connect()호출 시 (라인 183-190) 덮어써집니다. 현재는 두 콜백이 동일한 로직을 수행하지만, 코드 유지보수 시 불일치가 발생할 수 있습니다.
onConnect로직을 한 곳에서만 정의하는 것을 권장합니다.♻️ 리팩토링 제안
async connect(): Promise<void> { if (this.connected) { return Promise.resolve(); } if (this.connectPromise) { return this.connectPromise; } this.connectPromise = new Promise((resolve, reject) => { if (!this.client) { reject(new Error('Client not initialized')); return; } const timeoutId = setTimeout(() => { reject(new Error('Connection timeout')); this.connectPromise = null; }, 10000); + // 기존 onConnect를 래핑하여 Promise resolve 추가 + const originalOnConnect = this.client.onConnect; - this.client.onConnect = () => { + this.client.onConnect = (frame) => { clearTimeout(timeoutId); - console.log('[WebSocket] Connected'); - this.connected = true; - this.setupReplySubscription(); + if (originalOnConnect) { + originalOnConnect(frame); + } this.connectPromise = null; resolve(); }; this.client.activate(); }); return this.connectPromise; }
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (8)
.env.developmentpackage.jsonsrc/components/StockChart.tsxsrc/pages/StockDetail.tsxsrc/services/chartService.tssrc/services/websocketService.tssrc/types/chart.tsvite.config.ts
🧰 Additional context used
🧬 Code graph analysis (2)
src/components/StockChart.tsx (2)
src/types/chart.ts (1)
ChartData(1-10)src/services/websocketService.ts (3)
websocketService(351-351)StockAskBidData(39-56)StockExecutionData(58-105)
src/types/chart.ts (1)
src/services/websocketService.ts (2)
RealtimeStockData(14-23)WebSocketSubscriptionResponse(7-12)
🔇 Additional comments (6)
vite.config.ts (1)
18-20: LGTM! 브라우저 호환성을 위한 올바른 설정SockJS와 같은 Node.js 스타일 라이브러리가 브라우저 환경에서 작동하도록
global을globalThis로 매핑하는 올바른 설정입니다..env.development (1)
1-1: LGTM! 개발 환경 설정 추가Vite 규칙을 따르는 올바른 환경 변수 정의입니다.
package.json (1)
42-42: WebSocket 의존성 버전 및 보안 검증 완료추가된 WebSocket 라이브러리들(@stomp/stompjs@7.2.1, sockjs-client@1.6.1, @types/sockjs-client@1.5.4)은 모두 최신 버전이며, 알려진 보안 취약점이 없습니다.
src/components/StockChart.tsx (1)
490-510: LGTM - WebSocket 구독 구조WebSocket 구독 로직이 잘 구성되어 있습니다.
shortCode가 없을 때 조기 반환하고, cleanup 함수에서 구독을 해제하는 패턴이 적절합니다.src/services/websocketService.ts (2)
112-147: WebSocketService 클래스 구조 승인클래스 구조가 잘 설계되어 있습니다:
- Client 초기화와 연결이 분리되어 있어 lazy connection 지원
- Heartbeat 설정으로 연결 상태 유지
- 에러 핸들링 및 로깅 포함
299-339: LGTM - 구독 해제 로직구독 해제 로직이 적절하게 구현되어 있습니다. 서버에 먼저 알리고 로컬 구독을 정리하는 순서가 합리적입니다.
| import { ChartResponse, ApiResponse } from "@/types/chart"; | ||
|
|
||
| const API_BASE_URL = import.meta.env.LOCAL_API_BASE_URL || "http://localhost:8080"; | ||
| const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "http://localhost:8080"; |
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.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check if VITE_API_BASE_URL is defined in production environment
echo "=== Listing all .env files ==="
fd -H "^\.env" --type f
echo "=== Checking VITE_API_BASE_URL in all .env files ==="
fd -H "^\.env" --type f --exec grep -H "VITE_API_BASE_URL" {}Repository: SimuDAQ/Simudaq-client
Length of output: 225
프로덕션 환경 설정 파일 누락
.env.production 파일이 없어서 프로덕션 빌드 시 VITE_API_BASE_URL이 정의되지 않으며, 결과적으로 로컬호스트 폴백값(http://localhost:8080)이 사용됩니다. .env.production 파일을 생성하고 적절한 프로덕션 API 엔드포인트를 설정해야 합니다.
🤖 Prompt for AI Agents
In @src/services/chartService.ts at line 3, chartService.ts defines API_BASE_URL
from import.meta.env.VITE_API_BASE_URL falling back to localhost; create a
.env.production at the repo root and set VITE_API_BASE_URL to your production
API endpoint so the production build picks up the correct URL, then verify your
CI/build uses the .env.production and that the variable is available during the
Vite build (and avoid committing any sensitive secrets).
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for LOCAL_API_BASE_URL in all source files and config files
echo "=== Searching for LOCAL_API_BASE_URL in entire codebase ==="
rg -i "LOCAL_API_BASE_URL" --type-add 'env:*.env*' -t env -t ts -t tsx -t js -t jsx -t json -t yaml -t yml -C2
echo -e "\n=== Checking for VITE_API_BASE_URL definition ==="
rg "VITE_API_BASE_URL" --type-add 'env:*.env*' -t env -t json -C2
echo -e "\n=== Listing all .env files ==="
fd -e env -e "env.*" | head -20Repository: SimuDAQ/Simudaq-client
Length of output: 310
🏁 Script executed:
#!/bin/bash
# List all .env files and their content
echo "=== All .env files in repository ==="
find . -maxdepth 2 -name ".env*" -type f 2>/dev/null | sort
echo -e "\n=== Content of .env files ==="
find . -maxdepth 2 -name ".env*" -type f 2>/dev/null | while read file; do
echo "--- $file ---"
cat "$file"
echo ""
done
echo "=== Search for API_BASE_URL or api references ==="
rg -i "api.*url|base.*url" --type ts --type js --type json -C1 | head -50
echo "=== Check for LOCAL in entire codebase ==="
rg "LOCAL_API" . --type ts --type js 2>/dev/nullRepository: SimuDAQ/Simudaq-client
Length of output: 1859
VITE_API_BASE_URL 마이그레이션이 불완전합니다
src/services/stockService.ts에서 여전히 LOCAL_API_BASE_URL을 사용하고 있습니다. 이 파일도 VITE_API_BASE_URL로 변경해야 합니다. 추가로 프로덕션 환경 설정 파일(.env.production)이 없어서, 프로덕션 환경에서 VITE_API_BASE_URL이 정의되지 않으면 런타임 오류가 발생할 수 있습니다.
영향을 받는 파일
src/services/stockService.ts:LOCAL_API_BASE_URL→VITE_API_BASE_URL로 변경 필요.env.production(또는 프로덕션 환경 설정):VITE_API_BASE_URL정의 추가 필요
🤖 Prompt for AI Agents
In @src/services/chartService.ts at line 3, The constant migration is
incomplete: replace the use of LOCAL_API_BASE_URL in
src/services/stockService.ts with import.meta.env.VITE_API_BASE_URL (matching
the pattern used in chartService.ts, e.g., set API_BASE_URL or equivalent to
import.meta.env.VITE_API_BASE_URL || "http://localhost:8080") and update any
references to LOCAL_API_BASE_URL to use that new variable; additionally add
VITE_API_BASE_URL to your production environment config (e.g., .env.production)
to ensure the variable is defined at runtime to avoid errors.
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.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/StockChart.tsx (1)
419-419: 차트 초기화 effect의 의존성 배열을 최적화하세요.
chartData를 의존성 배열에 포함하면 실시간 데이터 업데이트마다 차트가 재생성됩니다. 이는 성능 저하와 함께 사용자의 줌/팬 상태를 초기화하여 UX를 저하시킵니다.차트 인스턴스는
stockCode,chartType,selectedGroupIndex,selectedOptionIndex와 같은 구조적 변경 시에만 재생성되어야 하며,chartData업데이트는 별도 effect(line 422-488)에서 처리하는 것이 적절합니다.♻️ 의존성 배열 최적화 제안
- }, [stockCode, selectedGroupIndex, selectedOptionIndex, isPositive, chartType, chartData, basePrice]); + }, [stockCode, selectedGroupIndex, selectedOptionIndex, isPositive, chartType]);
🤖 Fix all issues with AI agents
In @src/components/StockChart.tsx:
- Around line 516-531: The parseExecutionDateTime function currently falls back
to the current KST timestamp on parse failure which hides data issues; change
parseExecutionDateTime to stop silently returning KST and instead surface the
failure by either (a) accepting an optional onError callback parameter and
invoking onError(new Error(...)) with a descriptive message then returning null,
or (b) logging the error via console.error/processLogger and throwing a specific
Error; update all callers of parseExecutionDateTime to handle a null/exception
result (skip the datapoint or mark it invalid) so malformed timestamps are not
silently visualized.
In @src/services/websocketService.ts:
- Around line 4-5: The code uses a fallback to localhost when VITE_API_BASE_URL
is missing (WS_BASE_URL in websocketService.ts and the equivalent in
chartService.ts), which risks production misconfiguration; fix by ensuring a
production environment variable is provided—add a .env.production template with
VITE_API_BASE_URL=https://api.production.com, or add a build script like
"build:prod" that runs Vite with --mode production and document it, and/or
configure your CI/CD to inject VITE_API_BASE_URL into the production build so
WS_BASE_URL never silently falls back to localhost.
🧹 Nitpick comments (3)
src/components/StockChart.tsx (1)
155-157: 변수 네이밍 일관성 개선을 고려하세요.API 응답의
stockCode를shortCode로 저장하고 있습니다. 주석에서 언급한 것처럼 실제로는 shortCode이므로, API 응답 타입이나 변수명을 일관되게 맞추는 것을 고려해보세요.src/services/websocketService.ts (2)
190-223: 연결 핸들러 설정의 일관성을 개선하세요.
initializeClient()에서 설정한onConnect핸들러(lines 139-144)와connect()메서드에서 오버라이드하는 핸들러(lines 210-217) 간에 중복이 있습니다.
connect()가 호출되면 초기 핸들러가 교체되지만, 자동 재연결 시에는initializeClient()의 핸들러가 사용됩니다. 두 핸들러가 동일한 로직(setupReplySubscription,restoreSubscriptions)을 수행하지만, 이런 중복은 유지보수 시 불일치를 야기할 수 있습니다.♻️ 핸들러 통합 제안
연결 성공 시 실행할 로직을 별도 메서드로 추출하고, 두 곳에서 동일한 메서드를 호출하도록 리팩토링하세요:
private handleConnect() { console.log('[WebSocket] Connected'); this.connected = true; this.setupReplySubscription(); this.restoreSubscriptions(); }그리고
initializeClient의onConnect와connect()의 오버라이드에서 모두 이 메서드를 호출하세요.
400-402:isSubscribed()메서드의 의도를 명확히 하세요.현재
isSubscribed()는 askbid 또는 execution 중 하나라도 구독되어 있으면true를 반환합니다.subscribe()메서드가 부분 구독 상태를 정리하므로(lines 268-279) 정상 상태에서는 둘 다 존재하거나 둘 다 없어야 합니다.완전한 구독 상태만
true로 반환하도록 수정하는 것을 고려하세요:♻️ 두 구독 모두 확인하는 로직 제안
isSubscribed(stockCode: string): boolean { - return this.subscriptions.has(`askbid-${stockCode}`) || this.subscriptions.has(`execution-${stockCode}`); + return this.subscriptions.has(`askbid-${stockCode}`) && this.subscriptions.has(`execution-${stockCode}`); }
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/components/StockChart.tsxsrc/pages/StockDetail.tsxsrc/services/websocketService.ts
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/StockChart.tsx (2)
src/types/chart.ts (1)
ChartData(1-10)src/services/websocketService.ts (3)
websocketService(406-406)StockAskBidData(39-56)StockExecutionData(58-105)
🔇 Additional comments (8)
src/pages/StockDetail.tsx (1)
109-113: LGTM!
code가 존재할 때만 차트를 렌더링하는 조건부 처리가 적절합니다. WebSocket 구독에 필요한stockCodeprop을 정확하게 전달하고 있습니다.src/components/StockChart.tsx (3)
58-58: LGTM!실시간 데이터 관리를 위한 상태와 ref 추가가 적절합니다.
realtimeCandleRef를 Map으로 사용하여 메모리 내 캔들 데이터를 효율적으로 관리하고,lastUpdateTimeRef로 마지막 업데이트 시간을 추적하는 설계가 합리적입니다.Also applies to: 68-69
167-173: LGTM!차트 옵션 변경 시 실시간 데이터를 초기화하는 로직이 적절합니다. 이전 구간의 실시간 캔들 데이터가 새로운 차트에 잘못 표시되는 것을 방지합니다.
271-271: LGTM!interval별 최대 줌 아웃 제한 값 조정이 합리적입니다. 분봉(300개), 월봉(110개), 년봉(30개)의 제한은 각 시간 단위에 적절한 데이터 범위를 제공합니다.
Also applies to: 277-277, 279-279
src/services/websocketService.ts (4)
7-110: LGTM!WebSocket 데이터 구조에 대한 타입 정의가 포괄적이고 명확합니다. 서버 응답의 숫자 값들을 문자열 타입으로 정의한 것이 적절하며, 콜백 타입 별칭도 잘 정의되어 있습니다.
244-349: LGTM!구독 로직이 견고하게 구현되어 있습니다:
- 중복 구독 방지 (lines 258-265)
- 부분 구독 상태 정리 (lines 268-279)
- 재연결을 위한 구독 정보 저장 (lines 328-333)
- 토픽 구독 후 서버에 구독 요청을 전송하는 올바른 순서
에러 처리와 로깅도 적절합니다.
351-394: LGTM!구독 해제 로직이 올바르게 구현되어 있습니다. 두 토픽 모두에서 구독을 해제하고,
pendingSubscriptions에서도 제거하여 재연결 시 복원되지 않도록 합니다.
130-156: SockJS WebSocket 타입 캐스팅은 @stomp/stompjs 공식 문서에서 명시된 표준 패턴입니다.Line 132의 타입 캐스팅은 안전하며, 이는 @stomp/stompjs에서 SockJS를 사용하기 위한 권장 방식입니다. SockJS는 런타임에 WebSocket API를 구현하지만 TypeScript 타입 정의가 DOM WebSocket과 일치하지 않아 캐스팅이 필요합니다. 설치된 버전(@stomp/stompjs ^7.2.1, sockjs-client ^1.6.1)은 호환되며, 이 패턴에 대한 추가 검증이 필요하지 않습니다.
Likely an incorrect or invalid review comment.
| // WebSocket 구독 관리 (모든 차트 타입에서 실시간 데이터 구독) | ||
| useEffect(() => { | ||
| // shortCode가 없으면 구독하지 않음 | ||
| if (!shortCode) { | ||
| return; | ||
| } | ||
|
|
||
| console.log('[StockChart] Subscribing to WebSocket for:', shortCode); | ||
|
|
||
| // 선택된 차트 간격 파싱 | ||
| const [intervalType, intervalValueStr] = selectedOption.interval.split(':'); | ||
| const intervalValue = parseInt(intervalValueStr); | ||
|
|
||
| // WebSocket 구독 | ||
| websocketService.subscribe( | ||
| shortCode, | ||
| // onAskBid - 호가 데이터 (차트에는 사용하지 않음) | ||
| (askBidData: StockAskBidData) => { | ||
| console.log('[StockChart] AskBid data received:', askBidData); | ||
| // 호가 데이터는 차트에 반영하지 않음 | ||
| }, | ||
| // onExecution - 체결 데이터 (차트에 반영) | ||
| (executionData: StockExecutionData) => { | ||
| console.log('[StockChart] Execution data received:', executionData); | ||
|
|
||
| // businessDate(YYYYMMDD)와 executionTime(HHmmss)을 결합 | ||
| const parseExecutionDateTime = (businessDate: string, executionTime: string): string => { | ||
| if (businessDate.length === 8 && executionTime.length === 6) { | ||
| const year = businessDate.substring(0, 4); | ||
| const month = businessDate.substring(4, 6); | ||
| const day = businessDate.substring(6, 8); | ||
| const hour = executionTime.substring(0, 2); | ||
| const minute = executionTime.substring(2, 4); | ||
| const second = executionTime.substring(4, 6); | ||
|
|
||
| return `${year}-${month}-${day}T${hour}:${minute}:${second}`; | ||
| } | ||
|
|
||
| // 파싱 실패 시 현재 KST 시간 반환 | ||
| const kstNow = new Date(Date.now() + 9 * 60 * 60 * 1000); | ||
| return kstNow.toISOString().slice(0, 19); | ||
| }; | ||
|
|
||
| const executionDateTime = parseExecutionDateTime(executionData.businessDate, executionData.executionTime); | ||
| console.log('[StockChart] Parsed execution dateTime:', executionDateTime); | ||
|
|
||
| // 차트 간격에 맞춰 시간을 정규화 | ||
| const normalizeDateTime = (dateTimeStr: string, type: string, value: number): string => { | ||
| const date = new Date(dateTimeStr); | ||
|
|
||
| switch (type) { | ||
| case 'min': { | ||
| // 분봉: 분 단위로 정규화 | ||
| const minutes = date.getMinutes(); | ||
| const normalizedMinutes = Math.floor(minutes / value) * value; | ||
| date.setMinutes(normalizedMinutes); | ||
| date.setSeconds(0); | ||
| break; | ||
| } | ||
| case 'day': { | ||
| // 일봉: 날짜 단위로 정규화 (시간/분/초 = 0) | ||
| date.setHours(0, 0, 0, 0); | ||
| break; | ||
| } | ||
| case 'week': { | ||
| // 주봉: 주의 시작일(월요일)로 정규화 | ||
| const dayOfWeek = date.getDay(); | ||
| const diff = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // 일요일이면 6일 전, 아니면 요일-1 | ||
| date.setDate(date.getDate() - diff); | ||
| date.setHours(0, 0, 0, 0); | ||
| break; | ||
| } | ||
| case 'month': { | ||
| // 월봉: 월의 1일로 정규화 | ||
| date.setDate(1); | ||
| date.setHours(0, 0, 0, 0); | ||
| break; | ||
| } | ||
| case 'year': { | ||
| // 년봉: 연도의 1월 1일로 정규화 | ||
| date.setMonth(0, 1); | ||
| date.setHours(0, 0, 0, 0); | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| const year = date.getFullYear(); | ||
| const month = String(date.getMonth() + 1).padStart(2, '0'); | ||
| const day = String(date.getDate()).padStart(2, '0'); | ||
| const hour = String(date.getHours()).padStart(2, '0'); | ||
| const minute = String(date.getMinutes()).padStart(2, '0'); | ||
| const second = String(date.getSeconds()).padStart(2, '0'); | ||
|
|
||
| return `${year}-${month}-${day}T${hour}:${minute}:${second}`; | ||
| }; | ||
|
|
||
| const normalizedDateTime = normalizeDateTime(executionDateTime, intervalType, intervalValue); | ||
|
|
||
| if (!seriesRef.current) return; | ||
|
|
||
| const currentPrice = parseFloat(executionData.currentPrice); | ||
| const volume = parseInt(executionData.executionVolume); | ||
| const accumulatedAmount = parseFloat(executionData.accumulatedTradeAmount); | ||
|
|
||
| // timestamp로 변환 | ||
| const parseKSTtoTimestamp = (dateTimeStr: string): UTCTimestamp => { | ||
| const [datePart, timePart] = dateTimeStr.split('T'); | ||
| const [year, month, day] = datePart.split('-').map(Number); | ||
| const [hour, minute, second] = timePart.split(':').map(Number); | ||
| const timestamp = Date.UTC(year, month - 1, day, hour, minute, second); | ||
| return Math.floor(timestamp / 1000) as UTCTimestamp; | ||
| }; | ||
|
|
||
| const timestamp = parseKSTtoTimestamp(normalizedDateTime); | ||
|
|
||
| // chartData에서 기존 캔들 찾기 | ||
| const existingIndex = chartData.findIndex((item) => item.dateTime === normalizedDateTime); | ||
|
|
||
| let realtimeCandle: ChartData; | ||
| let updatedChartData: ChartData[]; | ||
|
|
||
| if (existingIndex >= 0) { | ||
| // chartData에 이미 존재하는 캔들 업데이트 | ||
| const existingCandle = chartData[existingIndex]; | ||
| realtimeCandle = { | ||
| ...existingCandle, | ||
| high: Math.max(existingCandle.high, currentPrice), | ||
| low: Math.min(existingCandle.low, currentPrice), | ||
| close: currentPrice, | ||
| volume: existingCandle.volume + volume, | ||
| accumulatedAmount: accumulatedAmount, | ||
| }; | ||
|
|
||
| // chartData 업데이트 (불변성 유지) | ||
| updatedChartData = [...chartData]; | ||
| updatedChartData[existingIndex] = realtimeCandle; | ||
| setChartData(updatedChartData); | ||
| } else { | ||
| // 새로운 캔들 생성 | ||
| realtimeCandle = { | ||
| dateTime: normalizedDateTime, | ||
| base: basePrice, | ||
| open: currentPrice, | ||
| high: currentPrice, | ||
| low: currentPrice, | ||
| close: currentPrice, | ||
| volume: volume, | ||
| accumulatedAmount: accumulatedAmount, | ||
| }; | ||
|
|
||
| // chartData에 추가 (불변성 유지) | ||
| updatedChartData = [...chartData, realtimeCandle]; | ||
| updatedChartData.sort((a, b) => new Date(a.dateTime).getTime() - new Date(b.dateTime).getTime()); | ||
| setChartData(updatedChartData); | ||
| } | ||
|
|
||
| // 실시간 캔들 저장 | ||
| realtimeCandleRef.current.set(normalizedDateTime, realtimeCandle); | ||
|
|
||
| // 차트에 직접 update (setData가 아닌 update 사용) | ||
| if (chartType === 'candlestick') { | ||
| seriesRef.current.update({ | ||
| time: timestamp, | ||
| open: realtimeCandle.open, | ||
| high: realtimeCandle.high, | ||
| low: realtimeCandle.low, | ||
| close: realtimeCandle.close, | ||
| }); | ||
| } else { | ||
| seriesRef.current.update({ | ||
| time: timestamp, | ||
| value: realtimeCandle.close, | ||
| }); | ||
| } | ||
|
|
||
| lastUpdateTimeRef.current = normalizedDateTime; | ||
| }, | ||
| // onReply | ||
| (response) => { | ||
| console.log('[StockChart] Subscription response:', response); | ||
| }, | ||
| // onError | ||
| (error) => { | ||
| console.error('[StockChart] WebSocket error:', error); | ||
| } | ||
| ); | ||
|
|
||
| // 컴포넌트 언마운트 또는 stockCode 변경 시 구독 해제 | ||
| return () => { | ||
| console.log('[StockChart] Unsubscribing from WebSocket for:', shortCode); | ||
| websocketService.unsubscribe(shortCode); | ||
| }; | ||
| }, [shortCode, selectedOption.interval, basePrice, chartType, chartData]); |
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.
WebSocket 구독 effect의 의존성 배열이 재구독 루프를 발생시킵니다.
Line 682의 의존성 배열에 chartData가 포함되어 있어 심각한 문제가 발생합니다:
- 실시간 데이터가 도착하면
chartData가 업데이트됩니다 (lines 626, 643) chartData변경으로 effect가 재실행됩니다- 재실행 시 이전 구독이 해제되고 새로운 구독이 생성됩니다 (line 680)
- 이 과정이 모든 데이터 업데이트마다 반복됩니다
이는 불필요한 네트워크 오버헤드를 발생시키고, 구독 해제/재구독 과정에서 데이터 손실 가능성이 있습니다.
🐛 의존성 배열 수정 제안
- }, [shortCode, selectedOption.interval, basePrice, chartType, chartData]);
+ }, [shortCode, selectedOption.interval, chartType]);chartData를 의존성에서 제거하고, 대신 콜백 내부에서 setChartData의 함수형 업데이트를 사용하여 최신 상태에 접근하세요. basePrice는 새 캔들 생성 시에만 필요하므로 ref로 관리하거나 stock 정보에서 직접 조회하는 것을 고려하세요.
Committable suggestion skipped: line range outside the PR's diff.
| const parseExecutionDateTime = (businessDate: string, executionTime: string): string => { | ||
| if (businessDate.length === 8 && executionTime.length === 6) { | ||
| const year = businessDate.substring(0, 4); | ||
| const month = businessDate.substring(4, 6); | ||
| const day = businessDate.substring(6, 8); | ||
| const hour = executionTime.substring(0, 2); | ||
| const minute = executionTime.substring(2, 4); | ||
| const second = executionTime.substring(4, 6); | ||
|
|
||
| return `${year}-${month}-${day}T${hour}:${minute}:${second}`; | ||
| } | ||
|
|
||
| // 파싱 실패 시 현재 KST 시간 반환 | ||
| const kstNow = new Date(Date.now() + 9 * 60 * 60 * 1000); | ||
| return kstNow.toISOString().slice(0, 19); | ||
| }; |
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.
타임스탬프 파싱 실패 시 에러 처리를 개선하세요.
Lines 528-530에서 파싱 실패 시 현재 KST 시간으로 폴백하는 로직이 데이터 이슈를 은폐할 수 있습니다. 잘못된 타임스탬프로 차트 데이터가 생성되면 시각화가 왜곡될 수 있습니다.
파싱 실패 시 명시적으로 에러를 로깅하거나, onError 콜백을 통해 상위로 전파하는 것을 고려하세요.
🔍 에러 로깅 추가 제안
const parseExecutionDateTime = (businessDate: string, executionTime: string): string => {
if (businessDate.length === 8 && executionTime.length === 6) {
const year = businessDate.substring(0, 4);
const month = businessDate.substring(4, 6);
const day = businessDate.substring(6, 8);
const hour = executionTime.substring(0, 2);
const minute = executionTime.substring(2, 4);
const second = executionTime.substring(4, 6);
return `${year}-${month}-${day}T${hour}:${minute}:${second}`;
}
// 파싱 실패 시 현재 KST 시간 반환
+ console.error('[StockChart] Failed to parse execution time:', { businessDate, executionTime });
const kstNow = new Date(Date.now() + 9 * 60 * 60 * 1000);
return kstNow.toISOString().slice(0, 19);
};🤖 Prompt for AI Agents
In @src/components/StockChart.tsx around lines 516 - 531, The
parseExecutionDateTime function currently falls back to the current KST
timestamp on parse failure which hides data issues; change
parseExecutionDateTime to stop silently returning KST and instead surface the
failure by either (a) accepting an optional onError callback parameter and
invoking onError(new Error(...)) with a descriptive message then returning null,
or (b) logging the error via console.error/processLogger and throwing a specific
Error; update all callers of parseExecutionDateTime to handle a null/exception
result (skip the datapoint or mark it invalid) so malformed timestamps are not
silently visualized.
| const WS_BASE_URL = import.meta.env.VITE_API_BASE_URL || "http://localhost:8080"; | ||
| const WS_ENDPOINT = "/stock"; |
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.
🧩 Analysis chain
🏁 Script executed:
# First, explore the repository structure and find relevant files
echo "=== Repository structure ==="
git ls-files | head -20
echo ""
echo "=== Looking for websocketService.ts ==="
fd websocketService.ts
echo ""
echo "=== Looking for environment configuration files ==="
fd -e env -e env.example -e env.production -e env.development -e env.localRepository: SimuDAQ/Simudaq-client
Length of output: 590
🏁 Script executed:
# Search for vite configuration
echo "=== Looking for vite config ==="
fd "vite.config"
echo ""
echo "=== Looking for package.json ==="
fd -e "package.json"
echo ""
echo "=== Looking for deployment/build configs ==="
fd "\.github" -o fd "\.gitlab" -o fd "\.circleci" -o fd "dockerfile" -o fd "docker-compose"Repository: SimuDAQ/Simudaq-client
Length of output: 333
🏁 Script executed:
# Read websocketService.ts
echo "=== Content of websocketService.ts ==="
cat -n src/services/websocketService.ts
echo ""
echo "=== Content of .env.development ==="
cat -n .env.development
echo ""
echo "=== All .env* files ==="
ls -la | grep envRepository: SimuDAQ/Simudaq-client
Length of output: 14974
🏁 Script executed:
# Check vite config
echo "=== Content of vite.config.ts ==="
cat -n vite.config.ts
echo ""
echo "=== Search for VITE_API_BASE_URL usage ==="
rg "VITE_API_BASE_URL" -n
echo ""
echo "=== Check for deployment/CI configs ==="
find . -name ".github" -o -name "Dockerfile" -o -name "docker-compose.yml" -o -name ".gitlab-ci.yml" | head -20Repository: SimuDAQ/Simudaq-client
Length of output: 1059
🏁 Script executed:
# Check if there are any build scripts or deployment documentation
echo "=== Checking package.json for build scripts ==="
grep -A 10 '"scripts"' package.json
echo ""
echo "=== Check for README or deployment docs ==="
rg "production|deploy|environment" README.md -i
echo ""
echo "=== Check for any CI workflow examples ==="
find . -type f \( -name "*.md" -o -name "*.yml" \) | xargs grep -l "VITE_API_BASE_URL" 2>/dev/null || echo "No CI/documentation found for VITE_API_BASE_URL"Repository: SimuDAQ/Simudaq-client
Length of output: 569
환경 변수 설정 누락 위험: 프로덕션 배포 전 필수 확인
VITE_API_BASE_URL 환경 변수가 설정되지 않을 경우 websocketService.ts와 chartService.ts 모두 localhost:8080으로 연결되어 프로덕션 환경에서 정상 작동하지 않습니다. 현재 .env.production 파일이 없고 프로덕션 배포 가이드가 없어 배포 시 환경 변수 설정을 누락할 위험이 있습니다.
다음 중 하나를 수행하세요:
.env.production템플릿 파일 추가 (예:VITE_API_BASE_URL=https://api.production.com)package.json에"build:prod": "vite build --mode production"스크립트 추가 및 프로덕션 배포 문서 작성- CI/CD 파이프라인에 VITE_API_BASE_URL 환경 변수 설정 자동화
🤖 Prompt for AI Agents
In @src/services/websocketService.ts around lines 4 - 5, The code uses a
fallback to localhost when VITE_API_BASE_URL is missing (WS_BASE_URL in
websocketService.ts and the equivalent in chartService.ts), which risks
production misconfiguration; fix by ensuring a production environment variable
is provided—add a .env.production template with
VITE_API_BASE_URL=https://api.production.com, or add a build script like
"build:prod" that runs Vite with --mode production and document it, and/or
configure your CI/CD to inject VITE_API_BASE_URL into the production build so
WS_BASE_URL never silently falls back to localhost.
상세페이지 Chart websocket 연결
Summary by CodeRabbit
릴리스 노트
새로운 기능
Chores
✏️ Tip: You can customize this high-level summary in your review settings.