diff --git a/src/frontend/apps/web/app/stock/[stockSlug]/page.tsx b/src/frontend/apps/web/app/stock/[stockSlug]/page.tsx index d1353627..2d1ec17d 100644 --- a/src/frontend/apps/web/app/stock/[stockSlug]/page.tsx +++ b/src/frontend/apps/web/app/stock/[stockSlug]/page.tsx @@ -28,7 +28,7 @@ export default function StockDetailsPage({ params }) { > - +
diff --git a/src/frontend/apps/web/src/features/stock/lib/stock-chart.util.ts b/src/frontend/apps/web/src/features/stock/lib/stock-chart.util.ts index 0fd46050..132220fa 100644 --- a/src/frontend/apps/web/src/features/stock/lib/stock-chart.util.ts +++ b/src/frontend/apps/web/src/features/stock/lib/stock-chart.util.ts @@ -30,15 +30,17 @@ const formatChartData = ( formatFn: (item: StockChartAPIResponse, time: Time) => T, ): T[] => { return response - .map((item) => { - const formattedTime = formatTimeForChart( - item.businessDate, - item.tradingTime, - ); - if (!formattedTime) return null; - return formatFn(item, formattedTime); - }) - .filter((item): item is T => item !== null); + ? response + .map((item) => { + const formattedTime = formatTimeForChart( + item.businessDate, + item.tradingTime, + ); + if (!formattedTime) return null; + return formatFn(item, formattedTime); + }) + .filter((item): item is T => item !== null) + : []; }; export const formatCandleChart = ( @@ -46,7 +48,7 @@ export const formatCandleChart = ( ): CandleChart[] => formatChartData(response, (item, time) => ({ time, - open: parseFloat(item.openPrice), + open: parseFloat(item.openPrice ?? item.openingPrice), high: parseFloat(item.highPrice), low: parseFloat(item.lowPrice), close: parseFloat(item.currentPrice), diff --git a/src/frontend/apps/web/src/features/stock/model/chart.types.ts b/src/frontend/apps/web/src/features/stock/model/chart.types.ts new file mode 100644 index 00000000..5f802dcc --- /dev/null +++ b/src/frontend/apps/web/src/features/stock/model/chart.types.ts @@ -0,0 +1,20 @@ +import { Time } from 'lightweight-charts'; + +export enum ChartType { + Candlestick = 'candlestick', + Line = 'line', + Histogram = 'histogram', +} + +export type CandleChart = { + time: Time; + open: number; + high: number; + low: number; + close: number; +}; + +export type DefaultChart = { + time: Time; + value: number; +}; diff --git a/src/frontend/apps/web/src/features/stock/model/index.ts b/src/frontend/apps/web/src/features/stock/model/index.ts index ae99d1a6..011fd6ec 100644 --- a/src/frontend/apps/web/src/features/stock/model/index.ts +++ b/src/frontend/apps/web/src/features/stock/model/index.ts @@ -1,10 +1,6 @@ -export { - type Stock, - type StockChartAPIResponse, - type CandleChart, - type DefaultChart, - ChartType, -} from './stock.types'; +export type { StockTable, StockChartAPIResponse, StockWS } from './stock.types'; +export { type CandleChart, type DefaultChart, ChartType } from './chart.types'; export { columns } from './stocks-table.columns'; export { dummyStockData, dummyRealTime } from './stock.mock'; export { useStockChart } from './use-stock-chart'; +export { useStockWebSocket } from './use-stock-websocket'; diff --git a/src/frontend/apps/web/src/features/stock/model/stock.constants.ts b/src/frontend/apps/web/src/features/stock/model/stock.constants.ts new file mode 100644 index 00000000..b0c03bc8 --- /dev/null +++ b/src/frontend/apps/web/src/features/stock/model/stock.constants.ts @@ -0,0 +1,7 @@ +export const STOCKS = { + 'samsung-electronics': { code: '005930', name: '삼성전자' }, + 'sk-hynix': { code: '000660', name: 'SK하이닉스' }, + kakao: { code: '035720', name: '카카오' }, + naver: { code: '035420', name: 'NAVER' }, + 'hanwha-aerospace': { code: '012450', name: '한화에어로스페이스' }, +} as const; diff --git a/src/frontend/apps/web/src/features/stock/model/stock.types.ts b/src/frontend/apps/web/src/features/stock/model/stock.types.ts index aefc3c0e..fb217dfb 100644 --- a/src/frontend/apps/web/src/features/stock/model/stock.types.ts +++ b/src/frontend/apps/web/src/features/stock/model/stock.types.ts @@ -1,6 +1,4 @@ -import { Time } from 'lightweight-charts'; - -export type Stock = { +export type StockTable = { id: string; name: string; currPrice: string; @@ -13,30 +11,25 @@ export type StockChartAPIResponse = { businessDate: string; tradingTime: string; currentPrice: string; - openPrice: string; + openPrice?: string; + openingPrice?: string; highPrice: string; lowPrice: string; tradingVolume: string; totalTradeAmount: string; }; -export enum ChartType { - Candlestick = 'candlestick', - Line = 'line', - Histogram = 'histogram', -} - -export type CandleChart = { - time: Time; - open: number; - high: number; - low: number; - close: number; -}; - -export type DefaultChart = { - time: Time; - value: number; +export type StockWS = { + code: string; + htsKorIsnm: string; + stckBsopDate: string; + stckCntgHour: string; + stckPrpr: string; + stckOprc: string; + stckHgpr: string; + stckLwpr: string; + cntgVol: string; + acmlTrPbmn: string; }; export type RealTimeStock = { diff --git a/src/frontend/apps/web/src/features/stock/model/use-stock-chart.ts b/src/frontend/apps/web/src/features/stock/model/use-stock-chart.ts index 7a10c8bd..cd8b0ad1 100644 --- a/src/frontend/apps/web/src/features/stock/model/use-stock-chart.ts +++ b/src/frontend/apps/web/src/features/stock/model/use-stock-chart.ts @@ -9,7 +9,7 @@ import { LineSeries, HistogramSeries, } from 'lightweight-charts'; -import { CandleChart, ChartType, DefaultChart } from './stock.types'; +import { CandleChart, ChartType, DefaultChart } from './chart.types'; export const useStockChart = ( chartType: ChartType, diff --git a/src/frontend/apps/web/src/features/stock/model/use-stock-websocket.tsx b/src/frontend/apps/web/src/features/stock/model/use-stock-websocket.tsx new file mode 100644 index 00000000..d84e60de --- /dev/null +++ b/src/frontend/apps/web/src/features/stock/model/use-stock-websocket.tsx @@ -0,0 +1,44 @@ +import { useCallback } from 'react'; + +import { useQueryClient } from '@tanstack/react-query'; + +import { useStompWebSocket } from '@/src/shared/providers'; +import { QUERY_KEYS } from '@/src/shared/services'; + +export const useStockWebSocket = () => { + const queryClient = useQueryClient(); + const { client, isConnected } = useStompWebSocket(); + + const subscribe = useCallback(() => { + if (!client) { + console.error(' WebSocket Client가 없습니다.'); + return; + } + + if (!isConnected) { + console.warn('WebSocket이 아직 연결되지 않았습니다. 구독을 대기합니다.'); + return; + } + + console.log(`Subscribing to /subscribe/stock`); + const subscription = client.subscribe(`/subscribe/stock`, (message) => { + try { + const payload = JSON.parse(message.body); + console.log('Received:', payload); + + queryClient.setQueryData( + QUERY_KEYS.stock(payload.code), + (prev: any[] = []) => [...prev, payload], + ); + } catch (error) { + console.error('메시지 파싱 실패:', error); + } + }); + + return () => { + subscription.unsubscribe(); + }; + }, [client, isConnected, queryClient]); + + return { subscribe }; +}; diff --git a/src/frontend/apps/web/src/features/stock/ui/stock-chart-container.tsx b/src/frontend/apps/web/src/features/stock/ui/stock-chart-container.tsx index 7aad4501..ca3d1fe8 100644 --- a/src/frontend/apps/web/src/features/stock/ui/stock-chart-container.tsx +++ b/src/frontend/apps/web/src/features/stock/ui/stock-chart-container.tsx @@ -1,13 +1,28 @@ +'use client'; + import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from '@workspace/ui/components'; import StockChartItem from './stock-chart-item'; -import { ChartType, dummyStockData } from '../model'; +import { ChartType, StockChartAPIResponse, useStockWebSocket } from '../model'; +import { useEffect } from 'react'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { QUERY_KEYS } from '@/src/shared/services'; + +const StockChartContainer = ({ stockCode }: { stockCode: string }) => { + const queryClient = useQueryClient(); + const { subscribe } = useStockWebSocket(); + + useEffect(() => { + return subscribe(); + }, [subscribe]); -const StockChartContainer = () => { - const data = dummyStockData; + const { data: stockData } = useQuery({ + queryKey: QUERY_KEYS.stock(stockCode), + queryFn: () => queryClient.getQueryData(QUERY_KEYS.stock(stockCode)), + }); return ( { className="flex items-center justify-center bg-gray-200" > @@ -29,7 +44,7 @@ const StockChartContainer = () => { className="flex items-center justify-center bg-gray-300" > diff --git a/src/frontend/apps/web/src/features/stock/ui/stock-detail-layout.tsx b/src/frontend/apps/web/src/features/stock/ui/stock-detail-layout.tsx index 25cbf242..0a5a1bc9 100644 --- a/src/frontend/apps/web/src/features/stock/ui/stock-detail-layout.tsx +++ b/src/frontend/apps/web/src/features/stock/ui/stock-detail-layout.tsx @@ -1,14 +1,21 @@ +import { STOCKS } from '../model/stock.constants'; import StockChartContainer from './stock-chart-container'; import StockInfoContainer from './stock-info-container'; -const StockDetailLayout = () => { +const StockDetailLayout = ({ stockSlug }: { stockSlug: string }) => { + const stockCode = STOCKS[stockSlug].code; + const stockName = STOCKS[stockSlug].name; + return (
- +
- +
); diff --git a/src/frontend/apps/web/src/features/stock/ui/stock-info-container.tsx b/src/frontend/apps/web/src/features/stock/ui/stock-info-container.tsx index e1c1f510..a3b18ddf 100644 --- a/src/frontend/apps/web/src/features/stock/ui/stock-info-container.tsx +++ b/src/frontend/apps/web/src/features/stock/ui/stock-info-container.tsx @@ -1,7 +1,13 @@ import StockInfo from './stock-info'; import StockLogo from './stock-logo'; -const StockInfoContainer = () => { +const StockInfoContainer = ({ + stockCode, + stockName, +}: { + stockCode: string; + stockName: string; +}) => { return (
diff --git a/src/frontend/apps/web/src/features/stock/ui/stocks-list-table.tsx b/src/frontend/apps/web/src/features/stock/ui/stocks-list-table.tsx index 556e915f..63276b53 100644 --- a/src/frontend/apps/web/src/features/stock/ui/stocks-list-table.tsx +++ b/src/frontend/apps/web/src/features/stock/ui/stocks-list-table.tsx @@ -11,7 +11,7 @@ import { useReactTable, } from '@tanstack/react-table'; import { useState } from 'react'; -import { columns } from '../model'; +import { StockTable, columns } from '../model'; import { Button, DropdownMenu, diff --git a/src/frontend/apps/web/src/shared/services/apis/querykey.ts b/src/frontend/apps/web/src/shared/services/apis/querykey.ts index 74d51252..76bc649f 100644 --- a/src/frontend/apps/web/src/shared/services/apis/querykey.ts +++ b/src/frontend/apps/web/src/shared/services/apis/querykey.ts @@ -1,6 +1,7 @@ export const QUERY_KEYS = { messages: (channelId: number) => ['messages', `/subscribe/chat.${channelId}`] as const, + stock: (stockCode: string) => ['stock', stockCode] as const, forwardHistory: (channelId: number) => ['forwardHistory', channelId] as const, reverseHistory: (channelId: number) => ['reverseHistory', channelId] as const, workspaceList: (workspaceId: number) =>