From 3cea3e66d0719fcbd41cf3308bff70115059d061 Mon Sep 17 00:00:00 2001 From: "jihoon87.lee" Date: Wed, 11 Feb 2026 16:31:28 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9E=84=EC=8B=9C=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .tmp/kis-token-cache.json | 2 +- app/(main)/dashboard/page.tsx | 22 ++- app/(main)/settings/page.tsx | 26 +++ app/(main)/trade/page.tsx | 26 +++ app/api/kis/domestic/chart/route.ts | 2 +- app/api/kis/domestic/order-cash/route.ts | 2 +- app/api/kis/domestic/orderbook/route.ts | 2 +- app/api/kis/domestic/overview/route.ts | 4 +- app/api/kis/domestic/search/route.ts | 8 +- app/api/kis/revoke/route.ts | 4 +- app/api/kis/validate/route.ts | 4 +- app/api/kis/ws/approval/route.ts | 4 +- .../apis/kis-auth.api.ts | 4 +- .../components}/KisAuthForm.tsx | 4 +- .../settings/components/SettingsContainer.tsx | 51 ++++++ .../store/use-kis-runtime-store.ts | 10 +- .../apis/kis-stock.api.ts | 4 +- .../components/TradeContainer.tsx} | 170 ++++++------------ .../components/chart/StockLineChart.tsx | 16 +- .../components/chart/chart-utils.ts | 10 +- .../components/details/StockOverviewCard.tsx | 6 +- .../components/details/StockPriceBadge.tsx | 0 .../components/header/StockHeader.tsx | 2 +- .../components/layout/DashboardLayout.tsx | 0 .../components/order/OrderForm.tsx | 14 +- .../components/orderbook/AnimatedQuantity.tsx | 0 .../components/orderbook/OrderBook.tsx | 2 +- .../components/search/StockSearchForm.tsx | 2 +- .../components/search/StockSearchHistory.tsx | 6 +- .../components/search/StockSearchResults.tsx | 2 +- .../data/korean-stocks.json | 0 .../data/korean-stocks.ts | 4 +- .../{dashboard => trade}/data/mock-stocks.ts | 6 +- .../hooks/useCurrentPrice.ts | 2 +- .../hooks/useKisTradeWebSocket.ts | 15 +- .../{dashboard => trade}/hooks/useOrder.ts | 6 +- .../hooks/useOrderBook.ts | 12 +- .../hooks/useStockOverview.ts | 10 +- .../hooks/useStockSearch.ts | 22 +-- .../types/trade.types.ts} | 4 +- .../utils/kis-realtime.utils.ts | 19 +- lib/kis/approval.ts | 2 +- lib/kis/domestic-market-session.ts | 10 +- lib/kis/domestic.ts | 2 +- lib/kis/trade.ts | 2 +- 45 files changed, 289 insertions(+), 236 deletions(-) create mode 100644 app/(main)/settings/page.tsx create mode 100644 app/(main)/trade/page.tsx rename features/{dashboard => settings}/apis/kis-auth.api.ts (93%) rename features/{dashboard/components/auth => settings/components}/KisAuthForm.tsx (99%) create mode 100644 features/settings/components/SettingsContainer.tsx rename features/{dashboard => settings}/store/use-kis-runtime-store.ts (93%) rename features/{dashboard => trade}/apis/kis-stock.api.ts (97%) rename features/{dashboard/components/DashboardContainer.tsx => trade/components/TradeContainer.tsx} (61%) rename features/{dashboard => trade}/components/chart/StockLineChart.tsx (97%) rename features/{dashboard => trade}/components/chart/chart-utils.ts (95%) rename features/{dashboard => trade}/components/details/StockOverviewCard.tsx (95%) rename features/{dashboard => trade}/components/details/StockPriceBadge.tsx (100%) rename features/{dashboard => trade}/components/header/StockHeader.tsx (97%) rename features/{dashboard => trade}/components/layout/DashboardLayout.tsx (100%) rename features/{dashboard => trade}/components/order/OrderForm.tsx (93%) rename features/{dashboard => trade}/components/orderbook/AnimatedQuantity.tsx (100%) rename features/{dashboard => trade}/components/orderbook/OrderBook.tsx (99%) rename features/{dashboard => trade}/components/search/StockSearchForm.tsx (91%) rename features/{dashboard => trade}/components/search/StockSearchHistory.tsx (92%) rename features/{dashboard => trade}/components/search/StockSearchResults.tsx (93%) rename features/{dashboard => trade}/data/korean-stocks.json (100%) rename features/{dashboard => trade}/data/korean-stocks.ts (67%) rename features/{dashboard => trade}/data/mock-stocks.ts (93%) rename features/{dashboard => trade}/hooks/useCurrentPrice.ts (96%) rename features/{dashboard => trade}/hooks/useKisTradeWebSocket.ts (95%) rename features/{dashboard => trade}/hooks/useOrder.ts (85%) rename features/{dashboard => trade}/hooks/useOrderBook.ts (85%) rename features/{dashboard => trade}/hooks/useStockOverview.ts (86%) rename features/{dashboard => trade}/hooks/useStockSearch.ts (80%) rename features/{dashboard/types/dashboard.types.ts => trade/types/trade.types.ts} (96%) rename features/{dashboard => trade}/utils/kis-realtime.utils.ts (87%) diff --git a/.tmp/kis-token-cache.json b/.tmp/kis-token-cache.json index 31c1d03..8475705 100644 --- a/.tmp/kis-token-cache.json +++ b/.tmp/kis-token-cache.json @@ -1 +1 @@ -{"mock:7777b1c958636e65539aadf6c4273a0dfa33c17e55d1beb7dbfd033d49457f6e":{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0b2tlbiIsImF1ZCI6ImRhNWMyYjU5LTA3YTUtNGVhNy1hY2UyLWZlNTMwZTBjM2ZjNCIsInByZHRfY2QiOiIiLCJpc3MiOiJ1bm9ndyIsImV4cCI6MTc3MDc5MzIzOCwiaWF0IjoxNzcwNzA2ODM4LCJqdGkiOiJQUzA2TG12SndjRHl1MDZLVXpPRjVsSHJacWR6ZWJKTXhCVWIifQ.vDnx1Vx-LnzaRETwkR5aQUnl7vV3-KlXG5AVlMRrchjdovJzd5n1vL7YfG_146Swrao2Drw8TwnpdK44-aTrhg","expiresAt":1770793238000},"real:7777b1c958636e65539aadf6c4273a0dfa33c17e55d1beb7dbfd033d49457f6e":{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0b2tlbiIsImF1ZCI6IjQ1ZTBmYTczLWI3ZmEtNDg5Mi1iYmZkLTJkYzdlNWQ2YTFhOCIsInByZHRfY2QiOiIiLCJpc3MiOiJ1bm9ndyIsImV4cCI6MTc3MDg3NDg1NywiaWF0IjoxNzcwNzg4NDU3LCJqdGkiOiJQUzA2TG12SndjRHl1MDZLVXpPRjVsSHJacWR6ZWJKTXhCVWIifQ.f4XsiK4WgzzBNbGEP5bNnJ9r4yAfGBb8SOwEZ-D0knygsFqSOGsj1QfjjVIBo7lG5AxAwyrIUdoC-rjqIVCc3A","expiresAt":1770874857000}} \ No newline at end of file +{"mock:7777b1c958636e65539aadf6c4273a0dfa33c17e55d1beb7dbfd033d49457f6e":{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0b2tlbiIsImF1ZCI6ImRhNWMyYjU5LTA3YTUtNGVhNy1hY2UyLWZlNTMwZTBjM2ZjNCIsInByZHRfY2QiOiIiLCJpc3MiOiJ1bm9ndyIsImV4cCI6MTc3MDc5MzIzOCwiaWF0IjoxNzcwNzA2ODM4LCJqdGkiOiJQUzA2TG12SndjRHl1MDZLVXpPRjVsSHJacWR6ZWJKTXhCVWIifQ.vDnx1Vx-LnzaRETwkR5aQUnl7vV3-KlXG5AVlMRrchjdovJzd5n1vL7YfG_146Swrao2Drw8TwnpdK44-aTrhg","expiresAt":1770793238000},"real:7777b1c958636e65539aadf6c4273a0dfa33c17e55d1beb7dbfd033d49457f6e":{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0b2tlbiIsImF1ZCI6ImViN2M3NzBhLWZjMDgtNDI3MS05YjZiLTkxYmM1OGY0NmM0ZiIsInByZHRfY2QiOiIiLCJpc3MiOiJ1bm9ndyIsImV4cCI6MTc3MDg4MTMyNCwiaWF0IjoxNzcwNzk0OTI0LCJqdGkiOiJQUzA2TG12SndjRHl1MDZLVXpPRjVsSHJacWR6ZWJKTXhCVWIifQ.3NvIylftH8PmMvFQu9CmfZUwULMoKQIAIlJHt5zW3sj70h9d4yLIi5WMbvFp-akUwYEAQMZHhFMeD4B58eP7BA","expiresAt":1770881324493}} \ No newline at end of file diff --git a/app/(main)/dashboard/page.tsx b/app/(main)/dashboard/page.tsx index 60fe5b7..d315e98 100644 --- a/app/(main)/dashboard/page.tsx +++ b/app/(main)/dashboard/page.tsx @@ -5,12 +5,12 @@ import { redirect } from "next/navigation"; import { createClient } from "@/utils/supabase/server"; -import { DashboardContainer } from "@/features/dashboard/components/DashboardContainer"; /** - * 대시보드 페이지 - * @returns DashboardContainer UI - * @see features/dashboard/components/DashboardContainer.tsx 클라이언트 상호작용(검색/시세/차트)은 해당 컴포넌트가 담당합니다. + * 대시보드 페이지 (향후 확장용) + * @returns 빈 대시보드 안내 UI + * @see app/(main)/trade/page.tsx 트레이딩 기능은 `/trade` 경로에서 제공합니다. + * @see app/(main)/settings/page.tsx KIS 인증 설정은 `/settings` 경로에서 제공합니다. */ export default async function DashboardPage() { // 상태 정의: 서버에서 세션을 먼저 확인해 비로그인 접근을 차단합니다. @@ -21,5 +21,17 @@ export default async function DashboardPage() { if (!user) redirect("/login"); - return ; + return ( +
+ {/* ========== DASHBOARD PLACEHOLDER ========== */} +
+

+ 대시보드 +

+

+ 이 페이지는 향후 포트폴리오 요약과 리포트 기능을 위한 확장 영역입니다. +

+
+
+ ); } diff --git a/app/(main)/settings/page.tsx b/app/(main)/settings/page.tsx new file mode 100644 index 0000000..4cbda44 --- /dev/null +++ b/app/(main)/settings/page.tsx @@ -0,0 +1,26 @@ +/** + * @file app/(main)/settings/page.tsx + * @description 로그인 사용자 전용 설정 페이지(Server Component) + */ + +import { redirect } from "next/navigation"; +import { SettingsContainer } from "@/features/settings/components/SettingsContainer"; +import { createClient } from "@/utils/supabase/server"; + +/** + * 설정 페이지 + * @returns SettingsContainer UI + * @see features/settings/components/SettingsContainer.tsx KIS 인증 설정 UI를 제공합니다. + */ +export default async function SettingsPage() { + // 상태 정의: 서버에서 세션을 먼저 확인해 비로그인 접근을 차단합니다. + const supabase = await createClient(); + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (!user) redirect("/login"); + + return ; +} + diff --git a/app/(main)/trade/page.tsx b/app/(main)/trade/page.tsx new file mode 100644 index 0000000..b2c5041 --- /dev/null +++ b/app/(main)/trade/page.tsx @@ -0,0 +1,26 @@ +/** + * @file app/(main)/trade/page.tsx + * @description 로그인 사용자 전용 트레이딩 페이지(Server Component) + */ + +import { redirect } from "next/navigation"; +import { TradeContainer } from "@/features/trade/components/TradeContainer"; +import { createClient } from "@/utils/supabase/server"; + +/** + * 트레이딩 페이지 + * @returns TradeContainer UI + * @see features/trade/components/TradeContainer.tsx 종목 검색/차트/호가/주문 기능을 제공합니다. + */ +export default async function TradePage() { + // 상태 정의: 서버에서 세션을 먼저 확인해 비로그인 접근을 차단합니다. + const supabase = await createClient(); + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (!user) redirect("/login"); + + return ; +} + diff --git a/app/api/kis/domestic/chart/route.ts b/app/api/kis/domestic/chart/route.ts index a190bef..ab3999b 100644 --- a/app/api/kis/domestic/chart/route.ts +++ b/app/api/kis/domestic/chart/route.ts @@ -1,7 +1,7 @@ import type { DashboardChartTimeframe, DashboardStockChartResponse, -} from "@/features/dashboard/types/dashboard.types"; +} from "@/features/trade/types/trade.types"; import type { KisCredentialInput } from "@/lib/kis/config"; import { hasKisConfig, normalizeTradingEnv } from "@/lib/kis/config"; import { getDomesticChart } from "@/lib/kis/domestic"; diff --git a/app/api/kis/domestic/order-cash/route.ts b/app/api/kis/domestic/order-cash/route.ts index 50f97d9..236f3d2 100644 --- a/app/api/kis/domestic/order-cash/route.ts +++ b/app/api/kis/domestic/order-cash/route.ts @@ -3,7 +3,7 @@ import { executeOrderCash } from "@/lib/kis/trade"; import { DashboardStockCashOrderRequest, DashboardStockCashOrderResponse, -} from "@/features/dashboard/types/dashboard.types"; +} from "@/features/trade/types/trade.types"; import { KisCredentialInput, hasKisConfig, diff --git a/app/api/kis/domestic/orderbook/route.ts b/app/api/kis/domestic/orderbook/route.ts index f6f10cf..7b60e3d 100644 --- a/app/api/kis/domestic/orderbook/route.ts +++ b/app/api/kis/domestic/orderbook/route.ts @@ -3,7 +3,7 @@ import { getDomesticOrderBook, KisDomesticOrderBookOutput, } from "@/lib/kis/domestic"; -import { DashboardStockOrderBookResponse } from "@/features/dashboard/types/dashboard.types"; +import { DashboardStockOrderBookResponse } from "@/features/trade/types/trade.types"; import { KisCredentialInput, hasKisConfig, diff --git a/app/api/kis/domestic/overview/route.ts b/app/api/kis/domestic/overview/route.ts index 0156520..1656acb 100644 --- a/app/api/kis/domestic/overview/route.ts +++ b/app/api/kis/domestic/overview/route.ts @@ -1,5 +1,5 @@ -import { KOREAN_STOCK_INDEX } from "@/features/dashboard/data/korean-stocks"; -import type { DashboardStockOverviewResponse } from "@/features/dashboard/types/dashboard.types"; +import { KOREAN_STOCK_INDEX } from "@/features/trade/data/korean-stocks"; +import type { DashboardStockOverviewResponse } from "@/features/trade/types/trade.types"; import type { KisCredentialInput } from "@/lib/kis/config"; import { hasKisConfig, normalizeTradingEnv } from "@/lib/kis/config"; import { getDomesticOverview } from "@/lib/kis/domestic"; diff --git a/app/api/kis/domestic/search/route.ts b/app/api/kis/domestic/search/route.ts index 5e0945e..6df3c5e 100644 --- a/app/api/kis/domestic/search/route.ts +++ b/app/api/kis/domestic/search/route.ts @@ -1,9 +1,9 @@ -import { KOREAN_STOCK_INDEX } from "@/features/dashboard/data/korean-stocks"; +import { KOREAN_STOCK_INDEX } from "@/features/trade/data/korean-stocks"; import type { DashboardStockSearchItem, DashboardStockSearchResponse, KoreanStockIndexItem, -} from "@/features/dashboard/types/dashboard.types"; +} from "@/features/trade/types/trade.types"; import { NextRequest, NextResponse } from "next/server"; const SEARCH_LIMIT = 10; @@ -15,7 +15,7 @@ const SEARCH_LIMIT = 10; * - [레이어] API Route * - [사용자 행동] 대시보드 검색창 엔터/검색 버튼 클릭 시 호출 * - [데이터 흐름] dashboard-main.tsx -> /api/kis/domestic/search -> KOREAN_STOCK_INDEX 필터/정렬 -> JSON 응답 - * - [연관 파일] features/dashboard/data/korean-stocks.ts, features/dashboard/components/dashboard-main.tsx + * - [연관 파일] features/trade/data/korean-stocks.ts, features/trade/components/dashboard-main.tsx * @author jihoon87.lee */ @@ -23,7 +23,7 @@ const SEARCH_LIMIT = 10; * 국내주식 검색 API * @param request query string의 q(검색어) 사용 * @returns 종목 검색 결과 목록 - * @see features/dashboard/components/dashboard-main.tsx 검색 폼에서 호출합니다. + * @see features/trade/components/dashboard-main.tsx 검색 폼에서 호출합니다. */ export async function GET(request: NextRequest) { // [Step 1] query string에서 검색어(q)를 읽고 공백을 제거합니다. diff --git a/app/api/kis/revoke/route.ts b/app/api/kis/revoke/route.ts index 1069a56..00d9522 100644 --- a/app/api/kis/revoke/route.ts +++ b/app/api/kis/revoke/route.ts @@ -1,4 +1,4 @@ -import type { DashboardKisRevokeResponse } from "@/features/dashboard/types/dashboard.types"; +import type { DashboardKisRevokeResponse } from "@/features/trade/types/trade.types"; import { normalizeTradingEnv } from "@/lib/kis/config"; import { parseKisCredentialRequest, @@ -14,7 +14,7 @@ import { NextRequest, NextResponse } from "next/server"; /** * @description KIS 액세스 토큰 폐기 - * @see features/dashboard/components/auth/KisAuthForm.tsx + * @see features/settings/components/KisAuthForm.tsx */ export async function POST(request: NextRequest) { const credentials = await parseKisCredentialRequest(request); diff --git a/app/api/kis/validate/route.ts b/app/api/kis/validate/route.ts index 9df871a..4b29660 100644 --- a/app/api/kis/validate/route.ts +++ b/app/api/kis/validate/route.ts @@ -1,4 +1,4 @@ -import type { DashboardKisValidateResponse } from "@/features/dashboard/types/dashboard.types"; +import type { DashboardKisValidateResponse } from "@/features/trade/types/trade.types"; import { normalizeTradingEnv } from "@/lib/kis/config"; import { parseKisCredentialRequest, @@ -14,7 +14,7 @@ import { NextRequest, NextResponse } from "next/server"; /** * @description 액세스 토큰 발급 성공 여부로 API 키를 검증합니다. - * @see features/dashboard/components/auth/KisAuthForm.tsx + * @see features/settings/components/KisAuthForm.tsx */ export async function POST(request: NextRequest) { const credentials = await parseKisCredentialRequest(request); diff --git a/app/api/kis/ws/approval/route.ts b/app/api/kis/ws/approval/route.ts index 407a920..da7443e 100644 --- a/app/api/kis/ws/approval/route.ts +++ b/app/api/kis/ws/approval/route.ts @@ -1,4 +1,4 @@ -import type { DashboardKisWsApprovalResponse } from "@/features/dashboard/types/dashboard.types"; +import type { DashboardKisWsApprovalResponse } from "@/features/trade/types/trade.types"; import { getKisApprovalKey, resolveKisWebSocketUrl } from "@/lib/kis/approval"; import { normalizeTradingEnv } from "@/lib/kis/config"; import { @@ -14,7 +14,7 @@ import { NextRequest, NextResponse } from "next/server"; /** * @description 실시간 웹소켓 연결 정보를 발급합니다. - * @see features/dashboard/hooks/useKisTradeWebSocket.ts + * @see features/trade/hooks/useKisTradeWebSocket.ts */ export async function POST(request: NextRequest) { const credentials = await parseKisCredentialRequest(request); diff --git a/features/dashboard/apis/kis-auth.api.ts b/features/settings/apis/kis-auth.api.ts similarity index 93% rename from features/dashboard/apis/kis-auth.api.ts rename to features/settings/apis/kis-auth.api.ts index e858458..f28dcbe 100644 --- a/features/dashboard/apis/kis-auth.api.ts +++ b/features/settings/apis/kis-auth.api.ts @@ -1,9 +1,9 @@ -import type { KisRuntimeCredentials } from "@/features/dashboard/store/use-kis-runtime-store"; +import type { KisRuntimeCredentials } from "@/features/settings/store/use-kis-runtime-store"; import type { DashboardKisRevokeResponse, DashboardKisValidateResponse, DashboardKisWsApprovalResponse, -} from "@/features/dashboard/types/dashboard.types"; +} from "@/features/trade/types/trade.types"; interface KisApiBaseResponse { ok: boolean; diff --git a/features/dashboard/components/auth/KisAuthForm.tsx b/features/settings/components/KisAuthForm.tsx similarity index 99% rename from features/dashboard/components/auth/KisAuthForm.tsx rename to features/settings/components/KisAuthForm.tsx index feaa666..8bf53f0 100644 --- a/features/dashboard/components/auth/KisAuthForm.tsx +++ b/features/settings/components/KisAuthForm.tsx @@ -3,11 +3,11 @@ import { useShallow } from "zustand/react/shallow"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { useKisRuntimeStore } from "@/features/dashboard/store/use-kis-runtime-store"; +import { useKisRuntimeStore } from "@/features/settings/store/use-kis-runtime-store"; import { revokeKisCredentials, validateKisCredentials, -} from "@/features/dashboard/apis/kis-auth.api"; +} from "@/features/settings/apis/kis-auth.api"; import { KeyRound, Shield, diff --git a/features/settings/components/SettingsContainer.tsx b/features/settings/components/SettingsContainer.tsx new file mode 100644 index 0000000..338713c --- /dev/null +++ b/features/settings/components/SettingsContainer.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useShallow } from "zustand/react/shallow"; +import { KisAuthForm } from "@/features/settings/components/KisAuthForm"; +import { useKisRuntimeStore } from "@/features/settings/store/use-kis-runtime-store"; + +/** + * @description 설정 페이지 컨테이너입니다. KIS 연결 상태와 인증 폼을 카드 UI로 제공합니다. + * @see app/(main)/settings/page.tsx 로그인 확인 후 이 컴포넌트를 렌더링합니다. + * @see features/settings/components/KisAuthForm.tsx 실제 인증 입력/검증/해제를 담당합니다. + */ +export function SettingsContainer() { + // 상태 정의: 연결 상태 표시용 전역 인증 상태를 구독합니다. + const { verifiedCredentials, isKisVerified } = useKisRuntimeStore( + useShallow((state) => ({ + verifiedCredentials: state.verifiedCredentials, + isKisVerified: state.isKisVerified, + })), + ); + + return ( +
+ {/* ========== STATUS CARD ========== */} +
+

+ KIS API 설정 +

+
+ 연결 상태: + {isKisVerified ? ( + + + 연결됨 ({verifiedCredentials?.tradingEnv === "real" ? "실전" : "모의"}) + + ) : ( + + + 미연결 + + )} +
+
+ + {/* ========== AUTH FORM CARD ========== */} +
+ +
+
+ ); +} + diff --git a/features/dashboard/store/use-kis-runtime-store.ts b/features/settings/store/use-kis-runtime-store.ts similarity index 93% rename from features/dashboard/store/use-kis-runtime-store.ts rename to features/settings/store/use-kis-runtime-store.ts index 399ade8..ecf16f7 100644 --- a/features/dashboard/store/use-kis-runtime-store.ts +++ b/features/settings/store/use-kis-runtime-store.ts @@ -1,14 +1,14 @@ "use client"; -import { fetchKisWebSocketApproval } from "@/features/dashboard/apis/kis-auth.api"; -import type { KisTradingEnv } from "@/features/dashboard/types/dashboard.types"; +import { fetchKisWebSocketApproval } from "@/features/settings/apis/kis-auth.api"; +import type { KisTradingEnv } from "@/features/trade/types/trade.types"; import { createJSONStorage, persist } from "zustand/middleware"; import { create } from "zustand"; /** - * @file features/dashboard/store/use-kis-runtime-store.ts + * @file features/settings/store/use-kis-runtime-store.ts * @description Stores KIS input, verification, and websocket connection state. - * @see features/dashboard/hooks/useKisTradeWebSocket.ts + * @see features/trade/hooks/useKisTradeWebSocket.ts */ export interface KisRuntimeCredentials { appKey: string; @@ -73,7 +73,7 @@ let wsConnectionPromise: Promise | null = null; /** * @description Runtime store for KIS session. - * @see features/dashboard/components/auth/KisAuthForm.tsx + * @see features/settings/components/KisAuthForm.tsx */ export const useKisRuntimeStore = create< KisRuntimeStoreState & KisRuntimeStoreActions diff --git a/features/dashboard/apis/kis-stock.api.ts b/features/trade/apis/kis-stock.api.ts similarity index 97% rename from features/dashboard/apis/kis-stock.api.ts rename to features/trade/apis/kis-stock.api.ts index 7d3bb55..21c8ce2 100644 --- a/features/dashboard/apis/kis-stock.api.ts +++ b/features/trade/apis/kis-stock.api.ts @@ -1,4 +1,4 @@ -import type { KisRuntimeCredentials } from "@/features/dashboard/store/use-kis-runtime-store"; +import type { KisRuntimeCredentials } from "@/features/settings/store/use-kis-runtime-store"; import type { DashboardChartTimeframe, DashboardStockCashOrderRequest, @@ -7,7 +7,7 @@ import type { DashboardStockOrderBookResponse, DashboardStockOverviewResponse, DashboardStockSearchResponse, -} from "@/features/dashboard/types/dashboard.types"; +} from "@/features/trade/types/trade.types"; import { DOMESTIC_KIS_SESSION_OVERRIDE_HEADER, DOMESTIC_KIS_SESSION_OVERRIDE_STORAGE_KEY, diff --git a/features/dashboard/components/DashboardContainer.tsx b/features/trade/components/TradeContainer.tsx similarity index 61% rename from features/dashboard/components/DashboardContainer.tsx rename to features/trade/components/TradeContainer.tsx index d158282..cdd80b6 100644 --- a/features/dashboard/components/DashboardContainer.tsx +++ b/features/trade/components/TradeContainer.tsx @@ -1,44 +1,41 @@ "use client"; +import Link from "next/link"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ChevronDown, ChevronUp } from "lucide-react"; import { useShallow } from "zustand/react/shallow"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; -import { useKisRuntimeStore } from "@/features/dashboard/store/use-kis-runtime-store"; -import { KisAuthForm } from "@/features/dashboard/components/auth/KisAuthForm"; -import { StockSearchForm } from "@/features/dashboard/components/search/StockSearchForm"; -import { StockSearchHistory } from "@/features/dashboard/components/search/StockSearchHistory"; -import { StockSearchResults } from "@/features/dashboard/components/search/StockSearchResults"; -import { useStockSearch } from "@/features/dashboard/hooks/useStockSearch"; -import { useOrderBook } from "@/features/dashboard/hooks/useOrderBook"; -import { useKisTradeWebSocket } from "@/features/dashboard/hooks/useKisTradeWebSocket"; -import { useStockOverview } from "@/features/dashboard/hooks/useStockOverview"; -import { useCurrentPrice } from "@/features/dashboard/hooks/useCurrentPrice"; -import { DashboardLayout } from "@/features/dashboard/components/layout/DashboardLayout"; -import { StockHeader } from "@/features/dashboard/components/header/StockHeader"; -import { OrderBook } from "@/features/dashboard/components/orderbook/OrderBook"; -import { OrderForm } from "@/features/dashboard/components/order/OrderForm"; -import { StockLineChart } from "@/features/dashboard/components/chart/StockLineChart"; +import { useKisRuntimeStore } from "@/features/settings/store/use-kis-runtime-store"; +import { StockSearchForm } from "@/features/trade/components/search/StockSearchForm"; +import { StockSearchHistory } from "@/features/trade/components/search/StockSearchHistory"; +import { StockSearchResults } from "@/features/trade/components/search/StockSearchResults"; +import { useStockSearch } from "@/features/trade/hooks/useStockSearch"; +import { useOrderBook } from "@/features/trade/hooks/useOrderBook"; +import { useKisTradeWebSocket } from "@/features/trade/hooks/useKisTradeWebSocket"; +import { useStockOverview } from "@/features/trade/hooks/useStockOverview"; +import { useCurrentPrice } from "@/features/trade/hooks/useCurrentPrice"; +import { DashboardLayout } from "@/features/trade/components/layout/DashboardLayout"; +import { StockHeader } from "@/features/trade/components/header/StockHeader"; +import { OrderBook } from "@/features/trade/components/orderbook/OrderBook"; +import { OrderForm } from "@/features/trade/components/order/OrderForm"; +import { StockLineChart } from "@/features/trade/components/chart/StockLineChart"; import type { DashboardStockOrderBookResponse, DashboardStockSearchItem, -} from "@/features/dashboard/types/dashboard.types"; +} from "@/features/trade/types/trade.types"; /** - * @description 대시보드 메인 컨테이너 - * @see app/(main)/dashboard/page.tsx 로그인 완료 후 이 컴포넌트를 렌더링합니다. - * @see features/dashboard/hooks/useStockSearch.ts 검색 입력/요청/히스토리 상태를 관리합니다. - * @see features/dashboard/hooks/useStockOverview.ts 선택 종목 상세 상태를 관리합니다. + * @description 트레이딩 페이지 메인 컨테이너입니다. + * @see app/(main)/trade/page.tsx 로그인 완료 후 이 컴포넌트를 렌더링합니다. + * @see app/(main)/settings/page.tsx 미인증 상태일 때 설정 페이지로 이동하도록 안내합니다. + * @see features/trade/hooks/useStockSearch.ts 검색 입력/요청/히스토리 상태를 관리합니다. + * @see features/trade/hooks/useStockOverview.ts 선택 종목 상세 상태를 관리합니다. */ -export function DashboardContainer() { +export function TradeContainer() { const skipNextAutoSearchRef = useRef(false); - const hasInitializedAuthPanelRef = useRef(false); const searchShellRef = useRef(null); - // 모바일에서는 초기 진입 시 API 패널을 접어 본문(차트/호가)을 먼저 보이게 합니다. - const [isMobileViewport, setIsMobileViewport] = useState(false); - const [isAuthPanelExpanded, setIsAuthPanelExpanded] = useState(true); + // 상태 정의: 검색 패널 열림 상태를 관리합니다. const [isSearchPanelOpen, setIsSearchPanelOpen] = useState(false); const { verifiedCredentials, isKisVerified } = useKisRuntimeStore( @@ -112,11 +109,12 @@ export function DashboardContainer() { orderBook, }); - const canSearch = isKisVerified && !!verifiedCredentials; + const canTrade = isKisVerified && !!verifiedCredentials; + const canSearch = canTrade; /** * @description 검색 전 API 인증 여부를 확인합니다. - * @see features/dashboard/components/search/StockSearchForm.tsx 검색 제출 전 공통 가드로 사용합니다. + * @see features/trade/components/search/StockSearchForm.tsx 검색 제출 전 공통 가드로 사용합니다. */ const ensureSearchReady = useCallback(() => { if (canSearch) return true; @@ -135,7 +133,7 @@ export function DashboardContainer() { /** * @description 검색 영역 포커스가 완전히 빠지면 드롭다운(검색결과/히스토리)을 닫습니다. - * @see features/dashboard/components/search/StockSearchForm.tsx 입력 포커스 이벤트에서 열림 제어를 함께 사용합니다. + * @see features/trade/components/search/StockSearchForm.tsx 입력 포커스 이벤트에서 열림 제어를 함께 사용합니다. */ const handleSearchShellBlur = useCallback( (event: React.FocusEvent) => { @@ -155,35 +153,6 @@ export function DashboardContainer() { [closeSearchPanel], ); - useEffect(() => { - const mediaQuery = window.matchMedia("(max-width: 767px)"); - - const applyViewportMode = (matches: boolean) => { - setIsMobileViewport(matches); - - // 최초 1회: 모바일이면 접힘, 데스크탑이면 펼침. - if (!hasInitializedAuthPanelRef.current) { - setIsAuthPanelExpanded(!matches); - hasInitializedAuthPanelRef.current = true; - return; - } - - // 데스크탑으로 돌아오면 항상 펼쳐 사용성을 유지합니다. - if (!matches) { - setIsAuthPanelExpanded(true); - } - }; - - applyViewportMode(mediaQuery.matches); - - const onViewportChange = (event: MediaQueryListEvent) => { - applyViewportMode(event.matches); - }; - mediaQuery.addEventListener("change", onViewportChange); - - return () => mediaQuery.removeEventListener("change", onViewportChange); - }, []); - useEffect(() => { if (skipNextAutoSearchRef.current) { skipNextAutoSearchRef.current = false; @@ -210,7 +179,7 @@ export function DashboardContainer() { /** * @description 수동 검색 버튼(엔터 포함) 제출 이벤트를 처리합니다. - * @see features/dashboard/components/search/StockSearchForm.tsx onSubmit 이벤트로 호출됩니다. + * @see features/trade/components/search/StockSearchForm.tsx onSubmit 이벤트로 호출됩니다. */ const handleSearchSubmit = useCallback( (event: React.FormEvent) => { @@ -223,8 +192,8 @@ export function DashboardContainer() { /** * @description 검색 결과/히스토리에서 종목을 선택하면 종목 상세를 로드하고 히스토리를 갱신합니다. - * @see features/dashboard/components/search/StockSearchResults.tsx onSelect 이벤트 - * @see features/dashboard/components/search/StockSearchHistory.tsx onSelect 이벤트 + * @see features/trade/components/search/StockSearchResults.tsx onSelect 이벤트 + * @see features/trade/components/search/StockSearchHistory.tsx onSelect 이벤트 */ const handleSelectStock = useCallback( (item: DashboardStockSearchItem) => { @@ -257,65 +226,30 @@ export function DashboardContainer() { ], ); + if (!canTrade) { + return ( +
+
+ {/* ========== UNVERIFIED NOTICE ========== */} +

+ 트레이딩을 시작하려면 KIS API 인증이 필요합니다. +

+

+ 설정 페이지에서 App Key/App Secret을 입력하고 연결 상태를 확인해 + 주세요. +

+
+ +
+
+
+ ); + } + return (
- {/* ========== AUTH STATUS ========== */} -
-
-
- KIS API 연결 상태: - {isKisVerified ? ( - - - 연결됨 ({verifiedCredentials?.tradingEnv === "real" ? "실전" : "모의"}) - - ) : ( - - - 미연결 - - )} -
- - -
- -
-
- -
-
-
- {/* ========== SEARCH ========== */}
{ const candleSeries = candleSeriesRef.current; @@ -524,8 +524,8 @@ export function StockLineChart({ /** * @description WebSocket 체결 틱을 현재 timeframe 캔들에 반영합니다. - * @see features/dashboard/hooks/useKisTradeWebSocket.ts latestTick - * @see features/dashboard/components/chart/chart-utils.ts toRealtimeTickBar + * @see features/trade/hooks/useKisTradeWebSocket.ts latestTick + * @see features/trade/components/chart/chart-utils.ts toRealtimeTickBar */ useEffect(() => { if (!latestTick) return; @@ -544,7 +544,7 @@ export function StockLineChart({ /** * @description 분봉(1m/30m/1h)에서는 WS 공백 상황을 대비해 최신 페이지를 주기적으로 동기화합니다. - * @see features/dashboard/apis/kis-stock.api.ts fetchStockChart + * @see features/trade/apis/kis-stock.api.ts fetchStockChart * @see lib/kis/domestic.ts getDomesticChart */ useEffect(() => { diff --git a/features/dashboard/components/chart/chart-utils.ts b/features/trade/components/chart/chart-utils.ts similarity index 95% rename from features/dashboard/components/chart/chart-utils.ts rename to features/trade/components/chart/chart-utils.ts index 5766ba0..122af67 100644 --- a/features/dashboard/components/chart/chart-utils.ts +++ b/features/trade/components/chart/chart-utils.ts @@ -12,7 +12,7 @@ import type { DashboardChartTimeframe, DashboardRealtimeTradeTick, StockCandlePoint, -} from "@/features/dashboard/types/dashboard.types"; +} from "@/features/trade/types/trade.types"; const KRW_FORMATTER = new Intl.NumberFormat("ko-KR"); const KST_TIME_ZONE = "Asia/Seoul"; @@ -228,8 +228,8 @@ export function upsertRealtimeBar( /** * @description 실시간 체결 틱을 차트용 ChartBar로 변환합니다. (KST 날짜 + tickTime 기준) - * @see features/dashboard/hooks/useKisTradeWebSocket.ts latestTick - * @see features/dashboard/components/chart/StockLineChart.tsx 실시간 캔들 반영 + * @see features/trade/hooks/useKisTradeWebSocket.ts latestTick + * @see features/trade/components/chart/StockLineChart.tsx 실시간 캔들 반영 */ export function toRealtimeTickBar( tick: DashboardRealtimeTradeTick, @@ -260,7 +260,7 @@ export function toRealtimeTickBar( /** * @description lightweight-charts X축 라벨을 KST 기준으로 강제 포맷합니다. - * @see features/dashboard/components/chart/StockLineChart.tsx createChart options.timeScale.tickMarkFormatter + * @see features/trade/components/chart/StockLineChart.tsx createChart options.timeScale.tickMarkFormatter */ export function formatKstTickMark(time: Time, tickMarkType: TickMarkType) { const date = toDateFromChartTime(time); @@ -275,7 +275,7 @@ export function formatKstTickMark(time: Time, tickMarkType: TickMarkType) { /** * @description crosshair 시간 라벨을 KST로 포맷합니다. - * @see features/dashboard/components/chart/StockLineChart.tsx createChart options.localization.timeFormatter + * @see features/trade/components/chart/StockLineChart.tsx createChart options.localization.timeFormatter */ export function formatKstCrosshairTime(time: Time) { const date = toDateFromChartTime(time); diff --git a/features/dashboard/components/details/StockOverviewCard.tsx b/features/trade/components/details/StockOverviewCard.tsx similarity index 95% rename from features/dashboard/components/details/StockOverviewCard.tsx rename to features/trade/components/details/StockOverviewCard.tsx index 53b730b..08be86f 100644 --- a/features/dashboard/components/details/StockOverviewCard.tsx +++ b/features/trade/components/details/StockOverviewCard.tsx @@ -6,13 +6,13 @@ import { CardTitle, CardDescription, } from "@/components/ui/card"; -import { StockLineChart } from "@/features/dashboard/components/chart/StockLineChart"; -import { StockPriceBadge } from "@/features/dashboard/components/details/StockPriceBadge"; +import { StockLineChart } from "@/features/trade/components/chart/StockLineChart"; +import { StockPriceBadge } from "@/features/trade/components/details/StockPriceBadge"; import type { DashboardStockItem, DashboardPriceSource, DashboardMarketPhase, -} from "@/features/dashboard/types/dashboard.types"; +} from "@/features/trade/types/trade.types"; const PRICE_FORMATTER = new Intl.NumberFormat("ko-KR"); function formatVolume(value: number) { diff --git a/features/dashboard/components/details/StockPriceBadge.tsx b/features/trade/components/details/StockPriceBadge.tsx similarity index 100% rename from features/dashboard/components/details/StockPriceBadge.tsx rename to features/trade/components/details/StockPriceBadge.tsx diff --git a/features/dashboard/components/header/StockHeader.tsx b/features/trade/components/header/StockHeader.tsx similarity index 97% rename from features/dashboard/components/header/StockHeader.tsx rename to features/trade/components/header/StockHeader.tsx index 7f2a44d..8de02da 100644 --- a/features/dashboard/components/header/StockHeader.tsx +++ b/features/trade/components/header/StockHeader.tsx @@ -1,6 +1,6 @@ // import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; -import { DashboardStockItem } from "@/features/dashboard/types/dashboard.types"; +import { DashboardStockItem } from "@/features/trade/types/trade.types"; import { cn } from "@/lib/utils"; interface StockHeaderProps { diff --git a/features/dashboard/components/layout/DashboardLayout.tsx b/features/trade/components/layout/DashboardLayout.tsx similarity index 100% rename from features/dashboard/components/layout/DashboardLayout.tsx rename to features/trade/components/layout/DashboardLayout.tsx diff --git a/features/dashboard/components/order/OrderForm.tsx b/features/trade/components/order/OrderForm.tsx similarity index 93% rename from features/dashboard/components/order/OrderForm.tsx rename to features/trade/components/order/OrderForm.tsx index 2e9d3c1..52b5189 100644 --- a/features/dashboard/components/order/OrderForm.tsx +++ b/features/trade/components/order/OrderForm.tsx @@ -3,12 +3,12 @@ import { Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { useOrder } from "@/features/dashboard/hooks/useOrder"; -import { useKisRuntimeStore } from "@/features/dashboard/store/use-kis-runtime-store"; +import { useOrder } from "@/features/trade/hooks/useOrder"; +import { useKisRuntimeStore } from "@/features/settings/store/use-kis-runtime-store"; import type { DashboardOrderSide, DashboardStockItem, -} from "@/features/dashboard/types/dashboard.types"; +} from "@/features/trade/types/trade.types"; interface OrderFormProps { stock?: DashboardStockItem; @@ -16,8 +16,8 @@ interface OrderFormProps { /** * @description 대시보드 주문 패널에서 매수/매도 주문을 입력하고 전송합니다. - * @see features/dashboard/hooks/useOrder.ts placeOrder - 주문 API 호출 - * @see features/dashboard/components/DashboardContainer.tsx OrderForm - 우측 주문 패널 렌더링 + * @see features/trade/hooks/useOrder.ts placeOrder - 주문 API 호출 + * @see features/trade/components/TradeContainer.tsx OrderForm - 우측 주문 패널 렌더링 */ export function OrderForm({ stock }: OrderFormProps) { const verifiedCredentials = useKisRuntimeStore( @@ -161,7 +161,7 @@ export function OrderForm({ stock }: OrderFormProps) { /** * @description 주문 입력 영역(가격/수량/총액)을 렌더링합니다. - * @see features/dashboard/components/order/OrderForm.tsx OrderForm - 매수/매도 탭에서 공용 호출 + * @see features/trade/components/order/OrderForm.tsx OrderForm - 매수/매도 탭에서 공용 호출 */ function OrderInputs({ type, @@ -236,7 +236,7 @@ function OrderInputs({ /** * @description 주문 비율(10/25/50/100%) 단축 버튼을 표시합니다. - * @see features/dashboard/components/order/OrderForm.tsx setPercent - 버튼 클릭 이벤트 처리 + * @see features/trade/components/order/OrderForm.tsx setPercent - 버튼 클릭 이벤트 처리 */ function PercentButtons({ onSelect }: { onSelect: (pct: string) => void }) { return ( diff --git a/features/dashboard/components/orderbook/AnimatedQuantity.tsx b/features/trade/components/orderbook/AnimatedQuantity.tsx similarity index 100% rename from features/dashboard/components/orderbook/AnimatedQuantity.tsx rename to features/trade/components/orderbook/AnimatedQuantity.tsx diff --git a/features/dashboard/components/orderbook/OrderBook.tsx b/features/trade/components/orderbook/OrderBook.tsx similarity index 99% rename from features/dashboard/components/orderbook/OrderBook.tsx rename to features/trade/components/orderbook/OrderBook.tsx index 484b55b..ad7ef36 100644 --- a/features/dashboard/components/orderbook/OrderBook.tsx +++ b/features/trade/components/orderbook/OrderBook.tsx @@ -5,7 +5,7 @@ import { Skeleton } from "@/components/ui/skeleton"; import type { DashboardRealtimeTradeTick, DashboardStockOrderBookResponse, -} from "@/features/dashboard/types/dashboard.types"; +} from "@/features/trade/types/trade.types"; import { cn } from "@/lib/utils"; import { AnimatedQuantity } from "./AnimatedQuantity"; diff --git a/features/dashboard/components/search/StockSearchForm.tsx b/features/trade/components/search/StockSearchForm.tsx similarity index 91% rename from features/dashboard/components/search/StockSearchForm.tsx rename to features/trade/components/search/StockSearchForm.tsx index 3b32896..313b6fd 100644 --- a/features/dashboard/components/search/StockSearchForm.tsx +++ b/features/trade/components/search/StockSearchForm.tsx @@ -14,7 +14,7 @@ interface StockSearchFormProps { /** * @description 종목 검색 입력/제출 폼을 렌더링합니다. - * @see features/dashboard/components/DashboardContainer.tsx 검색 패널에서 키워드 입력 이벤트를 전달합니다. + * @see features/trade/components/TradeContainer.tsx 검색 패널에서 키워드 입력 이벤트를 전달합니다. */ export function StockSearchForm({ keyword, diff --git a/features/dashboard/components/search/StockSearchHistory.tsx b/features/trade/components/search/StockSearchHistory.tsx similarity index 92% rename from features/dashboard/components/search/StockSearchHistory.tsx rename to features/trade/components/search/StockSearchHistory.tsx index 381bc1c..202ea7c 100644 --- a/features/dashboard/components/search/StockSearchHistory.tsx +++ b/features/trade/components/search/StockSearchHistory.tsx @@ -1,7 +1,7 @@ import { Clock3, Trash2, X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; -import type { DashboardStockSearchHistoryItem } from "@/features/dashboard/types/dashboard.types"; +import type { DashboardStockSearchHistoryItem } from "@/features/trade/types/trade.types"; interface StockSearchHistoryProps { items: DashboardStockSearchHistoryItem[]; @@ -13,8 +13,8 @@ interface StockSearchHistoryProps { /** * @description 최근 검색 종목 목록을 보여주고, 재검색/개별삭제/전체삭제를 제공합니다. - * @see features/dashboard/components/DashboardContainer.tsx 검색 패널에서 종목 재선택 UI로 사용합니다. - * @see features/dashboard/hooks/useStockSearch.ts searchHistory 상태를 화면에 렌더링합니다. + * @see features/trade/components/TradeContainer.tsx 검색 패널에서 종목 재선택 UI로 사용합니다. + * @see features/trade/hooks/useStockSearch.ts searchHistory 상태를 화면에 렌더링합니다. */ export function StockSearchHistory({ items, diff --git a/features/dashboard/components/search/StockSearchResults.tsx b/features/trade/components/search/StockSearchResults.tsx similarity index 93% rename from features/dashboard/components/search/StockSearchResults.tsx rename to features/trade/components/search/StockSearchResults.tsx index d19983c..adeac6e 100644 --- a/features/dashboard/components/search/StockSearchResults.tsx +++ b/features/trade/components/search/StockSearchResults.tsx @@ -1,7 +1,7 @@ import { Button } from "@/components/ui/button"; // import { Activity, TrendingDown, TrendingUp } from "lucide-react"; import { cn } from "@/lib/utils"; -import type { DashboardStockSearchItem } from "@/features/dashboard/types/dashboard.types"; +import type { DashboardStockSearchItem } from "@/features/trade/types/trade.types"; interface StockSearchResultsProps { items: DashboardStockSearchItem[]; diff --git a/features/dashboard/data/korean-stocks.json b/features/trade/data/korean-stocks.json similarity index 100% rename from features/dashboard/data/korean-stocks.json rename to features/trade/data/korean-stocks.json diff --git a/features/dashboard/data/korean-stocks.ts b/features/trade/data/korean-stocks.ts similarity index 67% rename from features/dashboard/data/korean-stocks.ts rename to features/trade/data/korean-stocks.ts index f160789..9667f0c 100644 --- a/features/dashboard/data/korean-stocks.ts +++ b/features/trade/data/korean-stocks.ts @@ -1,5 +1,5 @@ -import rawStocks from "@/features/dashboard/data/korean-stocks.json"; -import type { KoreanStockIndexItem } from "@/features/dashboard/types/dashboard.types"; +import rawStocks from "@/features/trade/data/korean-stocks.json"; +import type { KoreanStockIndexItem } from "@/features/trade/types/trade.types"; /** * 국내주식 검색 인덱스(KOSPI + KOSDAQ) diff --git a/features/dashboard/data/mock-stocks.ts b/features/trade/data/mock-stocks.ts similarity index 93% rename from features/dashboard/data/mock-stocks.ts rename to features/trade/data/mock-stocks.ts index bba31b5..d74850f 100644 --- a/features/dashboard/data/mock-stocks.ts +++ b/features/trade/data/mock-stocks.ts @@ -1,5 +1,5 @@ /** - * @file features/dashboard/data/mock-stocks.ts + * @file features/trade/data/mock-stocks.ts * @description 대시보드 1단계 UI 검증용 목업 종목 데이터 * @remarks * - 한국투자증권 API 연동 전까지 화면 동작 검증에 사용합니다. @@ -7,12 +7,12 @@ * - 현재는 레거시/비교용 샘플 데이터로만 남겨둔 상태입니다. */ -import type { DashboardStockItem } from "@/features/dashboard/types/dashboard.types"; +import type { DashboardStockItem } from "@/features/trade/types/trade.types"; /** * 대시보드 목업 종목 목록 * @see app/(main)/dashboard/page.tsx DashboardPage가 DashboardMain을 통해 이 데이터를 조회합니다. - * @see features/dashboard/components/dashboard-main.tsx 검색/차트/지표 카드의 기본 데이터 소스입니다. + * @see features/trade/components/dashboard-main.tsx 검색/차트/지표 카드의 기본 데이터 소스입니다. */ export const MOCK_STOCKS: DashboardStockItem[] = [ { diff --git a/features/dashboard/hooks/useCurrentPrice.ts b/features/trade/hooks/useCurrentPrice.ts similarity index 96% rename from features/dashboard/hooks/useCurrentPrice.ts rename to features/trade/hooks/useCurrentPrice.ts index 62a8532..102e164 100644 --- a/features/dashboard/hooks/useCurrentPrice.ts +++ b/features/trade/hooks/useCurrentPrice.ts @@ -3,7 +3,7 @@ import type { DashboardRealtimeTradeTick, DashboardStockItem, DashboardStockOrderBookResponse, -} from "@/features/dashboard/types/dashboard.types"; +} from "@/features/trade/types/trade.types"; interface UseCurrentPriceParams { stock?: DashboardStockItem | null; diff --git a/features/dashboard/hooks/useKisTradeWebSocket.ts b/features/trade/hooks/useKisTradeWebSocket.ts similarity index 95% rename from features/dashboard/hooks/useKisTradeWebSocket.ts rename to features/trade/hooks/useKisTradeWebSocket.ts index 661b778..3151b26 100644 --- a/features/dashboard/hooks/useKisTradeWebSocket.ts +++ b/features/trade/hooks/useKisTradeWebSocket.ts @@ -2,16 +2,16 @@ import { useEffect, useRef, useState } from "react"; import { type KisRuntimeCredentials, useKisRuntimeStore, -} from "@/features/dashboard/store/use-kis-runtime-store"; +} from "@/features/settings/store/use-kis-runtime-store"; import type { DashboardRealtimeTradeTick, DashboardStockOrderBookResponse, -} from "@/features/dashboard/types/dashboard.types"; +} from "@/features/trade/types/trade.types"; import { buildKisRealtimeMessage, parseKisRealtimeOrderbook, parseKisRealtimeTickBatch, -} from "@/features/dashboard/utils/kis-realtime.utils"; +} from "@/features/trade/utils/kis-realtime.utils"; import { DOMESTIC_KIS_SESSION_OVERRIDE_STORAGE_KEY, resolveDomesticKisSession, @@ -32,8 +32,8 @@ function resolveTradeTrId( session: DomesticKisSession, ) { if (env === "mock") return TRADE_TR_ID; - if (shouldUseAfterHoursSinglePriceTr(session)) return TRADE_TR_ID_OVERTIME; - if (shouldUseExpectedExecutionTr(session)) return TRADE_TR_ID_EXPECTED; + if (shouldUseAfterHoursSinglePriceTr(session)) return TRADE_TR_ID; + if (shouldUseExpectedExecutionTr(session)) return TRADE_TR_ID; return TRADE_TR_ID; } @@ -42,13 +42,14 @@ function resolveOrderBookTrId( session: DomesticKisSession, ) { if (env === "mock") return ORDERBOOK_TR_ID; - if (shouldUseAfterHoursSinglePriceTr(session)) return ORDERBOOK_TR_ID_OVERTIME; + if (shouldUseAfterHoursSinglePriceTr(session)) + return ORDERBOOK_TR_ID_OVERTIME; return ORDERBOOK_TR_ID; } /** * @description Subscribes trade ticks and orderbook over one websocket. - * @see features/dashboard/components/DashboardContainer.tsx + * @see features/trade/components/TradeContainer.tsx * @see lib/kis/domestic-market-session.ts */ export function useKisTradeWebSocket( diff --git a/features/dashboard/hooks/useOrder.ts b/features/trade/hooks/useOrder.ts similarity index 85% rename from features/dashboard/hooks/useOrder.ts rename to features/trade/hooks/useOrder.ts index 27c4167..58ed26c 100644 --- a/features/dashboard/hooks/useOrder.ts +++ b/features/trade/hooks/useOrder.ts @@ -1,10 +1,10 @@ import { useState, useCallback } from "react"; -import type { KisRuntimeCredentials } from "@/features/dashboard/store/use-kis-runtime-store"; +import type { KisRuntimeCredentials } from "@/features/settings/store/use-kis-runtime-store"; import type { DashboardStockCashOrderRequest, DashboardStockCashOrderResponse, -} from "@/features/dashboard/types/dashboard.types"; -import { fetchOrderCash } from "@/features/dashboard/apis/kis-stock.api"; +} from "@/features/trade/types/trade.types"; +import { fetchOrderCash } from "@/features/trade/apis/kis-stock.api"; export function useOrder() { const [isLoading, setIsLoading] = useState(false); diff --git a/features/dashboard/hooks/useOrderBook.ts b/features/trade/hooks/useOrderBook.ts similarity index 85% rename from features/dashboard/hooks/useOrderBook.ts rename to features/trade/hooks/useOrderBook.ts index d2b4c11..f74efa8 100644 --- a/features/dashboard/hooks/useOrderBook.ts +++ b/features/trade/hooks/useOrderBook.ts @@ -1,15 +1,15 @@ import { useEffect, useRef, useState } from "react"; -import type { KisRuntimeCredentials } from "@/features/dashboard/store/use-kis-runtime-store"; -import type { DashboardStockOrderBookResponse } from "@/features/dashboard/types/dashboard.types"; -import { fetchStockOrderBook } from "@/features/dashboard/apis/kis-stock.api"; +import type { KisRuntimeCredentials } from "@/features/settings/store/use-kis-runtime-store"; +import type { DashboardStockOrderBookResponse } from "@/features/trade/types/trade.types"; +import { fetchStockOrderBook } from "@/features/trade/apis/kis-stock.api"; import { toast } from "sonner"; /** * @description 초기 REST 호가를 한 번 조회하고, 이후에는 웹소켓 호가를 우선 사용합니다. - * 웹소켓 호가 데이터는 DashboardContainer에서 useKisTradeWebSocket을 통해 + * 웹소켓 호가 데이터는 TradeContainer에서 useKisTradeWebSocket을 통해 * 단일 WebSocket으로 수신되어 externalRealtimeOrderBook으로 주입됩니다. - * @see features/dashboard/components/DashboardContainer.tsx 호가 데이터 흐름 - * @see features/dashboard/components/orderbook/OrderBook.tsx 호가창 렌더링 데이터 공급 + * @see features/trade/components/TradeContainer.tsx 호가 데이터 흐름 + * @see features/trade/components/orderbook/OrderBook.tsx 호가창 렌더링 데이터 공급 */ export function useOrderBook( symbol: string | undefined, diff --git a/features/dashboard/hooks/useStockOverview.ts b/features/trade/hooks/useStockOverview.ts similarity index 86% rename from features/dashboard/hooks/useStockOverview.ts rename to features/trade/hooks/useStockOverview.ts index 157c853..32ac816 100644 --- a/features/dashboard/hooks/useStockOverview.ts +++ b/features/trade/hooks/useStockOverview.ts @@ -1,13 +1,13 @@ import { useCallback, useState, useTransition } from "react"; -import type { KisRuntimeCredentials } from "@/features/dashboard/store/use-kis-runtime-store"; +import type { KisRuntimeCredentials } from "@/features/settings/store/use-kis-runtime-store"; import type { DashboardMarketPhase, DashboardPriceSource, DashboardRealtimeTradeTick, DashboardStockSearchItem, DashboardStockItem, -} from "@/features/dashboard/types/dashboard.types"; -import { fetchStockOverview } from "@/features/dashboard/apis/kis-stock.api"; +} from "@/features/trade/types/trade.types"; +import { fetchStockOverview } from "@/features/trade/apis/kis-stock.api"; interface OverviewMeta { priceSource: DashboardPriceSource; @@ -60,8 +60,8 @@ export function useStockOverview() { /** * 실시간 체결 수신 시 헤더/주요 시세 상태만 갱신합니다. * 차트 캔들은 StockLineChart 내부 API 응답을 기준으로 유지합니다. - * @see features/dashboard/components/DashboardContainer.tsx useKisTradeWebSocket onTick 전달 - * @see features/dashboard/components/chart/StockLineChart.tsx 차트 데이터 fetchStockChart 기준 렌더링 + * @see features/trade/components/TradeContainer.tsx useKisTradeWebSocket onTick 전달 + * @see features/trade/components/chart/StockLineChart.tsx 차트 데이터 fetchStockChart 기준 렌더링 */ const updateRealtimeTradeTick = useCallback( (tick: DashboardRealtimeTradeTick) => { diff --git a/features/dashboard/hooks/useStockSearch.ts b/features/trade/hooks/useStockSearch.ts similarity index 80% rename from features/dashboard/hooks/useStockSearch.ts rename to features/trade/hooks/useStockSearch.ts index 61ffb1d..e9ead1f 100644 --- a/features/dashboard/hooks/useStockSearch.ts +++ b/features/trade/hooks/useStockSearch.ts @@ -1,10 +1,10 @@ import { useCallback, useRef, useState } from "react"; -import { fetchStockSearch } from "@/features/dashboard/apis/kis-stock.api"; -import type { KisRuntimeCredentials } from "@/features/dashboard/store/use-kis-runtime-store"; +import { fetchStockSearch } from "@/features/trade/apis/kis-stock.api"; +import type { KisRuntimeCredentials } from "@/features/settings/store/use-kis-runtime-store"; import type { DashboardStockSearchHistoryItem, DashboardStockSearchItem, -} from "@/features/dashboard/types/dashboard.types"; +} from "@/features/trade/types/trade.types"; const SEARCH_HISTORY_STORAGE_KEY = "jurini:stock-search-history:v1"; const SEARCH_HISTORY_LIMIT = 12; @@ -44,8 +44,8 @@ function writeSearchHistory(items: DashboardStockSearchHistoryItem[]) { /** * @description 종목 검색 상태(키워드/결과/에러)와 검색 히스토리(localStorage)를 함께 관리합니다. - * @see features/dashboard/components/DashboardContainer.tsx 검색 제출/자동검색/히스토리 클릭 이벤트에서 호출합니다. - * @see features/dashboard/components/search/StockSearchHistory.tsx 히스토리 목록 렌더링 데이터로 사용합니다. + * @see features/trade/components/TradeContainer.tsx 검색 제출/자동검색/히스토리 클릭 이벤트에서 호출합니다. + * @see features/trade/components/search/StockSearchHistory.tsx 히스토리 목록 렌더링 데이터로 사용합니다. */ export function useStockSearch() { // ========== SEARCH STATE ========== @@ -92,7 +92,7 @@ export function useStockSearch() { /** * @description 검색어를 받아 종목 검색 API를 호출합니다. - * @see features/dashboard/components/DashboardContainer.tsx handleSearchSubmit 자동/수동 검색에 사용합니다. + * @see features/trade/components/TradeContainer.tsx handleSearchSubmit 자동/수동 검색에 사용합니다. */ const search = useCallback( (query: string, credentials: KisRuntimeCredentials | null) => { @@ -119,7 +119,7 @@ export function useStockSearch() { /** * @description 검색 결과를 지우고 진행 중인 검색 요청을 중단합니다. - * @see features/dashboard/components/DashboardContainer.tsx 종목 선택 직후 검색 패널 정리에 사용합니다. + * @see features/trade/components/TradeContainer.tsx 종목 선택 직후 검색 패널 정리에 사용합니다. */ const clearSearch = useCallback(() => { abortRef.current?.abort(); @@ -130,7 +130,7 @@ export function useStockSearch() { /** * @description API 검증 전 같은 상황에서 공통 에러 메시지를 표시하기 위한 setter입니다. - * @see features/dashboard/components/DashboardContainer.tsx ensureSearchReady 인증 가드에서 사용합니다. + * @see features/trade/components/TradeContainer.tsx ensureSearchReady 인증 가드에서 사용합니다. */ const setSearchError = useCallback((message: string | null) => { setError(message); @@ -138,7 +138,7 @@ export function useStockSearch() { /** * @description 선택한 종목을 검색 히스토리 맨 위에 추가(중복 제거)합니다. - * @see features/dashboard/components/DashboardContainer.tsx handleSelectStock 종목 선택 이벤트에서 호출합니다. + * @see features/trade/components/TradeContainer.tsx handleSelectStock 종목 선택 이벤트에서 호출합니다. */ const appendSearchHistory = useCallback((item: DashboardStockSearchItem) => { setSearchHistory((prev) => { @@ -155,7 +155,7 @@ export function useStockSearch() { /** * @description 종목코드 기준으로 히스토리 항목을 삭제합니다. - * @see features/dashboard/components/search/StockSearchHistory.tsx 삭제 버튼 클릭 이벤트에서 호출합니다. + * @see features/trade/components/search/StockSearchHistory.tsx 삭제 버튼 클릭 이벤트에서 호출합니다. */ const removeSearchHistory = useCallback((symbol: string) => { setSearchHistory((prev) => { @@ -167,7 +167,7 @@ export function useStockSearch() { /** * @description 저장된 검색 히스토리를 전체 삭제합니다. - * @see features/dashboard/components/search/StockSearchHistory.tsx 전체 삭제 버튼 이벤트에서 호출합니다. + * @see features/trade/components/search/StockSearchHistory.tsx 전체 삭제 버튼 이벤트에서 호출합니다. */ const clearSearchHistory = useCallback(() => { setSearchHistory([]); diff --git a/features/dashboard/types/dashboard.types.ts b/features/trade/types/trade.types.ts similarity index 96% rename from features/dashboard/types/dashboard.types.ts rename to features/trade/types/trade.types.ts index 7ed6d8c..861e72f 100644 --- a/features/dashboard/types/dashboard.types.ts +++ b/features/trade/types/trade.types.ts @@ -1,5 +1,5 @@ /** - * @file features/dashboard/types/dashboard.types.ts + * @file features/trade/types/trade.types.ts * @description 대시보드(검색/시세/차트)에서 공통으로 쓰는 타입 모음 */ @@ -75,7 +75,7 @@ export interface DashboardStockSearchItem { /** * 검색 히스토리 1개 항목 - * @see features/dashboard/hooks/useStockSearch.ts localStorage에 저장/복원할 때 사용합니다. + * @see features/trade/hooks/useStockSearch.ts localStorage에 저장/복원할 때 사용합니다. */ export interface DashboardStockSearchHistoryItem extends DashboardStockSearchItem { diff --git a/features/dashboard/utils/kis-realtime.utils.ts b/features/trade/utils/kis-realtime.utils.ts similarity index 87% rename from features/dashboard/utils/kis-realtime.utils.ts rename to features/trade/utils/kis-realtime.utils.ts index 659f214..1c821af 100644 --- a/features/dashboard/utils/kis-realtime.utils.ts +++ b/features/trade/utils/kis-realtime.utils.ts @@ -1,7 +1,7 @@ import type { DashboardRealtimeTradeTick, DashboardStockOrderBookResponse, -} from "@/features/dashboard/types/dashboard.types"; +} from "@/features/trade/types/trade.types"; const REALTIME_SIGN_NEGATIVE = new Set(["4", "5"]); const ALLOWED_REALTIME_TRADE_TR_IDS = new Set([ @@ -32,7 +32,8 @@ const TICK_FIELD_INDEX = { } as const; /** - * KIS ?ㅼ떆媛?援щ룆/?댁젣 ?뱀냼耳?硫붿떆吏€瑜??앹꽦?⑸땲?? + * @description KIS 실시간 구독/해제 소켓 메시지를 생성합니다. + * @see features/trade/hooks/useKisTradeWebSocket.ts 구독/해제 요청 payload 생성에 사용됩니다. */ export function buildKisRealtimeMessage( approvalKey: string, @@ -57,9 +58,10 @@ export function buildKisRealtimeMessage( } /** - * ?ㅼ떆媛?泥닿껐 ?ㅽ듃由?raw)??諛곗뿴 ?⑥쐞濡??뚯떛?⑸땲?? - * - 諛곗튂 ?꾩넚(蹂듭닔 ?????뚮룄 紐⑤뱺 ?깆쓣 異붿텧 - * - ?щ낵 遺덉씪移?媛€寃?0 ?댄븯 ?곗씠?곕뒗 ?쒖쇅 + * @description 실시간 체결 스트림(raw)을 배열 단위로 파싱합니다. + * - 배치 전송(복수 체결) 데이터를 모두 추출합니다. + * - 종목 불일치 또는 가격 0 이하 데이터는 제외합니다. + * @see features/trade/hooks/useKisTradeWebSocket.ts onmessage 이벤트에서 체결 패킷 파싱에 사용됩니다. */ export function parseKisRealtimeTickBatch(raw: string, expectedSymbol: string) { if (!/^([01])\|/.test(raw)) return [] as DashboardRealtimeTradeTick[]; @@ -152,7 +154,8 @@ export function parseKisRealtimeTickBatch(raw: string, expectedSymbol: string) { } /** - * KIS ?ㅼ떆媛??멸?(H0STASP0/H0UNASP0/H0STOAA0)瑜?OrderBook 援ъ“濡??뚯떛?⑸땲?? + * @description KIS 실시간 호가(H0STASP0/H0UNASP0/H0STOAA0)를 OrderBook 구조로 파싱합니다. + * @see features/trade/hooks/useKisTradeWebSocket.ts onOrderBookMessage 콜백 데이터 생성에 사용됩니다. */ export function parseKisRealtimeOrderbook( raw: string, @@ -224,8 +227,8 @@ export function parseKisRealtimeOrderbook( } /** - * @description 援?궡 醫낅ぉ肄붾뱶 鍮꾧탳瑜??꾪빐 ?묐몢 臾몄옄瑜??쒓굅?섍퀬 6?먮━ 肄붾뱶濡??뺢퇋?뷀빀?덈떎. - * @see features/dashboard/utils/kis-realtime.utils.ts parseKisRealtimeOrderbook 醫낅ぉ 留ㅼ묶 鍮꾧탳 + * @description 국내 종목코드 비교를 위해 접두 문자를 제거하고 6자리 코드로 정규화합니다. + * @see features/trade/utils/kis-realtime.utils.ts parseKisRealtimeOrderbook 종목 매칭 비교에 사용됩니다. */ function normalizeDomesticSymbol(value: string) { const trimmed = value.trim(); diff --git a/lib/kis/approval.ts b/lib/kis/approval.ts index 12ad503..36211a8 100644 --- a/lib/kis/approval.ts +++ b/lib/kis/approval.ts @@ -90,7 +90,7 @@ function tryParseApprovalResponse(rawText: string): KisApprovalResponse { /** * @description 승인키를 캐시에서 반환하거나 새로 발급합니다. - * @see features/dashboard/store/use-kis-runtime-store.ts + * @see features/settings/store/use-kis-runtime-store.ts */ export async function getKisApprovalKey(credentials?: KisCredentialInput) { const cacheKey = getApprovalCacheKey(credentials); diff --git a/lib/kis/domestic-market-session.ts b/lib/kis/domestic-market-session.ts index d81dd31..e2fc056 100644 --- a/lib/kis/domestic-market-session.ts +++ b/lib/kis/domestic-market-session.ts @@ -29,7 +29,7 @@ const AFTER_HOURS_SINGLE_END_MINUTES = 18 * 60; // 18:00 /** * @description Converts external string to strict session enum. * @see lib/kis/domestic.ts getDomesticOrderBook - * @see features/dashboard/hooks/useKisTradeWebSocket.ts resolveSessionInClient + * @see features/trade/hooks/useKisTradeWebSocket.ts resolveSessionInClient */ export function parseDomesticKisSession(value?: string | null) { if (!value) return null; @@ -53,7 +53,7 @@ export function parseDomesticKisSession(value?: string | null) { /** * @description Returns current session in KST. - * @see features/dashboard/hooks/useKisTradeWebSocket.ts WebSocket TR switching + * @see features/trade/hooks/useKisTradeWebSocket.ts WebSocket TR switching * @see lib/kis/domestic.ts REST orderbook source switching */ export function getDomesticKisSessionInKst(now = new Date()): DomesticKisSession { @@ -104,7 +104,7 @@ export function getDomesticKisSessionInKst(now = new Date()): DomesticKisSession /** * @description If override is valid, use it. Otherwise use real KST time. * @see app/api/kis/domestic/orderbook/route.ts session override header - * @see features/dashboard/hooks/useKisTradeWebSocket.ts localStorage override + * @see features/trade/hooks/useKisTradeWebSocket.ts localStorage override */ export function resolveDomesticKisSession( override?: string | null, @@ -143,7 +143,7 @@ export function shouldUseOvertimeOrderBookApi(session: DomesticKisSession) { /** * @description Whether trade tick should use expected-execution TR. - * @see features/dashboard/hooks/useKisTradeWebSocket.ts resolveTradeTrId + * @see features/trade/hooks/useKisTradeWebSocket.ts resolveTradeTrId */ export function shouldUseExpectedExecutionTr(session: DomesticKisSession) { return session === "openAuction" || session === "closeAuction"; @@ -151,7 +151,7 @@ export function shouldUseExpectedExecutionTr(session: DomesticKisSession) { /** * @description Whether trade tick/orderbook should use after-hours single-price TR. - * @see features/dashboard/hooks/useKisTradeWebSocket.ts resolveTradeTrId + * @see features/trade/hooks/useKisTradeWebSocket.ts resolveTradeTrId */ export function shouldUseAfterHoursSinglePriceTr(session: DomesticKisSession) { return session === "afterHoursSinglePrice"; diff --git a/lib/kis/domestic.ts b/lib/kis/domestic.ts index 6373d3b..ef38d31 100644 --- a/lib/kis/domestic.ts +++ b/lib/kis/domestic.ts @@ -2,7 +2,7 @@ import type { DashboardChartTimeframe, DashboardStockItem, StockCandlePoint, -} from "@/features/dashboard/types/dashboard.types"; +} from "@/features/trade/types/trade.types"; import type { KisCredentialInput } from "@/lib/kis/config"; import { kisGet } from "@/lib/kis/client"; import { diff --git a/lib/kis/trade.ts b/lib/kis/trade.ts index 714a2ba..f704fb8 100644 --- a/lib/kis/trade.ts +++ b/lib/kis/trade.ts @@ -3,7 +3,7 @@ import { KisCredentialInput } from "@/lib/kis/config"; import { DashboardOrderSide, DashboardOrderType, -} from "@/features/dashboard/types/dashboard.types"; +} from "@/features/trade/types/trade.types"; /** * @file lib/kis/trade.ts