임시커밋

This commit is contained in:
2026-02-11 16:31:28 +09:00
parent f650d51f68
commit 3cea3e66d0
45 changed files with 289 additions and 236 deletions

View File

@@ -1 +1 @@
{"mock:7777b1c958636e65539aadf6c4273a0dfa33c17e55d1beb7dbfd033d49457f6e":{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0b2tlbiIsImF1ZCI6ImRhNWMyYjU5LTA3YTUtNGVhNy1hY2UyLWZlNTMwZTBjM2ZjNCIsInByZHRfY2QiOiIiLCJpc3MiOiJ1bm9ndyIsImV4cCI6MTc3MDc5MzIzOCwiaWF0IjoxNzcwNzA2ODM4LCJqdGkiOiJQUzA2TG12SndjRHl1MDZLVXpPRjVsSHJacWR6ZWJKTXhCVWIifQ.vDnx1Vx-LnzaRETwkR5aQUnl7vV3-KlXG5AVlMRrchjdovJzd5n1vL7YfG_146Swrao2Drw8TwnpdK44-aTrhg","expiresAt":1770793238000},"real:7777b1c958636e65539aadf6c4273a0dfa33c17e55d1beb7dbfd033d49457f6e":{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0b2tlbiIsImF1ZCI6IjQ1ZTBmYTczLWI3ZmEtNDg5Mi1iYmZkLTJkYzdlNWQ2YTFhOCIsInByZHRfY2QiOiIiLCJpc3MiOiJ1bm9ndyIsImV4cCI6MTc3MDg3NDg1NywiaWF0IjoxNzcwNzg4NDU3LCJqdGkiOiJQUzA2TG12SndjRHl1MDZLVXpPRjVsSHJacWR6ZWJKTXhCVWIifQ.f4XsiK4WgzzBNbGEP5bNnJ9r4yAfGBb8SOwEZ-D0knygsFqSOGsj1QfjjVIBo7lG5AxAwyrIUdoC-rjqIVCc3A","expiresAt":1770874857000}} {"mock:7777b1c958636e65539aadf6c4273a0dfa33c17e55d1beb7dbfd033d49457f6e":{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0b2tlbiIsImF1ZCI6ImRhNWMyYjU5LTA3YTUtNGVhNy1hY2UyLWZlNTMwZTBjM2ZjNCIsInByZHRfY2QiOiIiLCJpc3MiOiJ1bm9ndyIsImV4cCI6MTc3MDc5MzIzOCwiaWF0IjoxNzcwNzA2ODM4LCJqdGkiOiJQUzA2TG12SndjRHl1MDZLVXpPRjVsSHJacWR6ZWJKTXhCVWIifQ.vDnx1Vx-LnzaRETwkR5aQUnl7vV3-KlXG5AVlMRrchjdovJzd5n1vL7YfG_146Swrao2Drw8TwnpdK44-aTrhg","expiresAt":1770793238000},"real:7777b1c958636e65539aadf6c4273a0dfa33c17e55d1beb7dbfd033d49457f6e":{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0b2tlbiIsImF1ZCI6ImViN2M3NzBhLWZjMDgtNDI3MS05YjZiLTkxYmM1OGY0NmM0ZiIsInByZHRfY2QiOiIiLCJpc3MiOiJ1bm9ndyIsImV4cCI6MTc3MDg4MTMyNCwiaWF0IjoxNzcwNzk0OTI0LCJqdGkiOiJQUzA2TG12SndjRHl1MDZLVXpPRjVsSHJacWR6ZWJKTXhCVWIifQ.3NvIylftH8PmMvFQu9CmfZUwULMoKQIAIlJHt5zW3sj70h9d4yLIi5WMbvFp-akUwYEAQMZHhFMeD4B58eP7BA","expiresAt":1770881324493}}

View File

@@ -5,12 +5,12 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { createClient } from "@/utils/supabase/server"; import { createClient } from "@/utils/supabase/server";
import { DashboardContainer } from "@/features/dashboard/components/DashboardContainer";
/** /**
* 대시보드 페이지 * 대시보드 페이지 (향후 확장용)
* @returns DashboardContainer UI * @returns 빈 대시보드 안내 UI
* @see features/dashboard/components/DashboardContainer.tsx 클라이언트 상호작용(검색/시세/차트)은 해당 컴포넌트가 담당합니다. * @see app/(main)/trade/page.tsx 트레이딩 기능은 `/trade` 경로에서 제공합니다.
* @see app/(main)/settings/page.tsx KIS 인증 설정은 `/settings` 경로에서 제공합니다.
*/ */
export default async function DashboardPage() { export default async function DashboardPage() {
// 상태 정의: 서버에서 세션을 먼저 확인해 비로그인 접근을 차단합니다. // 상태 정의: 서버에서 세션을 먼저 확인해 비로그인 접근을 차단합니다.
@@ -21,5 +21,17 @@ export default async function DashboardPage() {
if (!user) redirect("/login"); if (!user) redirect("/login");
return <DashboardContainer />; return (
<section className="mx-auto flex h-full w-full max-w-5xl flex-col justify-center p-6">
{/* ========== DASHBOARD PLACEHOLDER ========== */}
<div className="rounded-2xl border border-brand-200 bg-background p-8 shadow-sm dark:border-brand-800/45 dark:bg-brand-900/14">
<h1 className="text-2xl font-semibold tracking-tight text-foreground">
</h1>
<p className="mt-2 text-sm text-muted-foreground">
.
</p>
</div>
</section>
);
} }

View File

@@ -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 <SettingsContainer />;
}

26
app/(main)/trade/page.tsx Normal file
View File

@@ -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 <TradeContainer />;
}

View File

@@ -1,7 +1,7 @@
import type { import type {
DashboardChartTimeframe, DashboardChartTimeframe,
DashboardStockChartResponse, DashboardStockChartResponse,
} from "@/features/dashboard/types/dashboard.types"; } from "@/features/trade/types/trade.types";
import type { KisCredentialInput } from "@/lib/kis/config"; import type { KisCredentialInput } from "@/lib/kis/config";
import { hasKisConfig, normalizeTradingEnv } from "@/lib/kis/config"; import { hasKisConfig, normalizeTradingEnv } from "@/lib/kis/config";
import { getDomesticChart } from "@/lib/kis/domestic"; import { getDomesticChart } from "@/lib/kis/domestic";

View File

@@ -3,7 +3,7 @@ import { executeOrderCash } from "@/lib/kis/trade";
import { import {
DashboardStockCashOrderRequest, DashboardStockCashOrderRequest,
DashboardStockCashOrderResponse, DashboardStockCashOrderResponse,
} from "@/features/dashboard/types/dashboard.types"; } from "@/features/trade/types/trade.types";
import { import {
KisCredentialInput, KisCredentialInput,
hasKisConfig, hasKisConfig,

View File

@@ -3,7 +3,7 @@ import {
getDomesticOrderBook, getDomesticOrderBook,
KisDomesticOrderBookOutput, KisDomesticOrderBookOutput,
} from "@/lib/kis/domestic"; } from "@/lib/kis/domestic";
import { DashboardStockOrderBookResponse } from "@/features/dashboard/types/dashboard.types"; import { DashboardStockOrderBookResponse } from "@/features/trade/types/trade.types";
import { import {
KisCredentialInput, KisCredentialInput,
hasKisConfig, hasKisConfig,

View File

@@ -1,5 +1,5 @@
import { KOREAN_STOCK_INDEX } from "@/features/dashboard/data/korean-stocks"; import { KOREAN_STOCK_INDEX } from "@/features/trade/data/korean-stocks";
import type { DashboardStockOverviewResponse } from "@/features/dashboard/types/dashboard.types"; import type { DashboardStockOverviewResponse } from "@/features/trade/types/trade.types";
import type { KisCredentialInput } from "@/lib/kis/config"; import type { KisCredentialInput } from "@/lib/kis/config";
import { hasKisConfig, normalizeTradingEnv } from "@/lib/kis/config"; import { hasKisConfig, normalizeTradingEnv } from "@/lib/kis/config";
import { getDomesticOverview } from "@/lib/kis/domestic"; import { getDomesticOverview } from "@/lib/kis/domestic";

View File

@@ -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 { import type {
DashboardStockSearchItem, DashboardStockSearchItem,
DashboardStockSearchResponse, DashboardStockSearchResponse,
KoreanStockIndexItem, KoreanStockIndexItem,
} from "@/features/dashboard/types/dashboard.types"; } from "@/features/trade/types/trade.types";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
const SEARCH_LIMIT = 10; const SEARCH_LIMIT = 10;
@@ -15,7 +15,7 @@ const SEARCH_LIMIT = 10;
* - [레이어] API Route * - [레이어] API Route
* - [사용자 행동] 대시보드 검색창 엔터/검색 버튼 클릭 시 호출 * - [사용자 행동] 대시보드 검색창 엔터/검색 버튼 클릭 시 호출
* - [데이터 흐름] dashboard-main.tsx -> /api/kis/domestic/search -> KOREAN_STOCK_INDEX 필터/정렬 -> JSON 응답 * - [데이터 흐름] 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 * @author jihoon87.lee
*/ */
@@ -23,7 +23,7 @@ const SEARCH_LIMIT = 10;
* 국내주식 검색 API * 국내주식 검색 API
* @param request query string의 q(검색어) 사용 * @param request query string의 q(검색어) 사용
* @returns 종목 검색 결과 목록 * @returns 종목 검색 결과 목록
* @see features/dashboard/components/dashboard-main.tsx 검색 폼에서 호출합니다. * @see features/trade/components/dashboard-main.tsx 검색 폼에서 호출합니다.
*/ */
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
// [Step 1] query string에서 검색어(q)를 읽고 공백을 제거합니다. // [Step 1] query string에서 검색어(q)를 읽고 공백을 제거합니다.

View File

@@ -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 { normalizeTradingEnv } from "@/lib/kis/config";
import { import {
parseKisCredentialRequest, parseKisCredentialRequest,
@@ -14,7 +14,7 @@ import { NextRequest, NextResponse } from "next/server";
/** /**
* @description KIS 액세스 토큰 폐기 * @description KIS 액세스 토큰 폐기
* @see features/dashboard/components/auth/KisAuthForm.tsx * @see features/settings/components/KisAuthForm.tsx
*/ */
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
const credentials = await parseKisCredentialRequest(request); const credentials = await parseKisCredentialRequest(request);

View File

@@ -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 { normalizeTradingEnv } from "@/lib/kis/config";
import { import {
parseKisCredentialRequest, parseKisCredentialRequest,
@@ -14,7 +14,7 @@ import { NextRequest, NextResponse } from "next/server";
/** /**
* @description 액세스 토큰 발급 성공 여부로 API 키를 검증합니다. * @description 액세스 토큰 발급 성공 여부로 API 키를 검증합니다.
* @see features/dashboard/components/auth/KisAuthForm.tsx * @see features/settings/components/KisAuthForm.tsx
*/ */
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
const credentials = await parseKisCredentialRequest(request); const credentials = await parseKisCredentialRequest(request);

View File

@@ -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 { getKisApprovalKey, resolveKisWebSocketUrl } from "@/lib/kis/approval";
import { normalizeTradingEnv } from "@/lib/kis/config"; import { normalizeTradingEnv } from "@/lib/kis/config";
import { import {
@@ -14,7 +14,7 @@ import { NextRequest, NextResponse } from "next/server";
/** /**
* @description 실시간 웹소켓 연결 정보를 발급합니다. * @description 실시간 웹소켓 연결 정보를 발급합니다.
* @see features/dashboard/hooks/useKisTradeWebSocket.ts * @see features/trade/hooks/useKisTradeWebSocket.ts
*/ */
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
const credentials = await parseKisCredentialRequest(request); const credentials = await parseKisCredentialRequest(request);

View File

@@ -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 { import type {
DashboardKisRevokeResponse, DashboardKisRevokeResponse,
DashboardKisValidateResponse, DashboardKisValidateResponse,
DashboardKisWsApprovalResponse, DashboardKisWsApprovalResponse,
} from "@/features/dashboard/types/dashboard.types"; } from "@/features/trade/types/trade.types";
interface KisApiBaseResponse { interface KisApiBaseResponse {
ok: boolean; ok: boolean;

View File

@@ -3,11 +3,11 @@ import { useShallow } from "zustand/react/shallow";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; 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 { import {
revokeKisCredentials, revokeKisCredentials,
validateKisCredentials, validateKisCredentials,
} from "@/features/dashboard/apis/kis-auth.api"; } from "@/features/settings/apis/kis-auth.api";
import { import {
KeyRound, KeyRound,
Shield, Shield,

View File

@@ -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 (
<section className="mx-auto flex w-full max-w-5xl flex-col gap-5 p-4 md:p-6">
{/* ========== STATUS CARD ========== */}
<article className="rounded-2xl border border-brand-200 bg-muted/35 p-4 dark:border-brand-800/45 dark:bg-brand-900/20">
<h1 className="text-xl font-semibold tracking-tight text-foreground">
KIS API
</h1>
<div className="mt-3 flex flex-wrap items-center gap-2 text-sm">
<span className="font-medium text-foreground"> :</span>
{isKisVerified ? (
<span className="inline-flex items-center rounded-full bg-brand-100 px-2.5 py-1 text-xs font-semibold text-brand-700 dark:bg-brand-900/45 dark:text-brand-200">
<span className="mr-1.5 h-2 w-2 rounded-full bg-brand-500" />
({verifiedCredentials?.tradingEnv === "real" ? "실전" : "모의"})
</span>
) : (
<span className="inline-flex items-center rounded-full bg-brand-50 px-2.5 py-1 text-xs font-semibold text-brand-700 dark:bg-brand-900/30 dark:text-brand-200">
<span className="mr-1.5 h-2 w-2 rounded-full bg-brand-300 dark:bg-brand-500/70" />
</span>
)}
</div>
</article>
{/* ========== AUTH FORM CARD ========== */}
<article className="rounded-2xl border border-brand-200 bg-background p-4 shadow-sm dark:border-brand-800/45 dark:bg-brand-900/14">
<KisAuthForm />
</article>
</section>
);
}

View File

@@ -1,14 +1,14 @@
"use client"; "use client";
import { fetchKisWebSocketApproval } from "@/features/dashboard/apis/kis-auth.api"; import { fetchKisWebSocketApproval } from "@/features/settings/apis/kis-auth.api";
import type { KisTradingEnv } from "@/features/dashboard/types/dashboard.types"; import type { KisTradingEnv } from "@/features/trade/types/trade.types";
import { createJSONStorage, persist } from "zustand/middleware"; import { createJSONStorage, persist } from "zustand/middleware";
import { create } from "zustand"; 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. * @description Stores KIS input, verification, and websocket connection state.
* @see features/dashboard/hooks/useKisTradeWebSocket.ts * @see features/trade/hooks/useKisTradeWebSocket.ts
*/ */
export interface KisRuntimeCredentials { export interface KisRuntimeCredentials {
appKey: string; appKey: string;
@@ -73,7 +73,7 @@ let wsConnectionPromise: Promise<KisWsConnection | null> | null = null;
/** /**
* @description Runtime store for KIS session. * @description Runtime store for KIS session.
* @see features/dashboard/components/auth/KisAuthForm.tsx * @see features/settings/components/KisAuthForm.tsx
*/ */
export const useKisRuntimeStore = create< export const useKisRuntimeStore = create<
KisRuntimeStoreState & KisRuntimeStoreActions KisRuntimeStoreState & KisRuntimeStoreActions

View File

@@ -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 { import type {
DashboardChartTimeframe, DashboardChartTimeframe,
DashboardStockCashOrderRequest, DashboardStockCashOrderRequest,
@@ -7,7 +7,7 @@ import type {
DashboardStockOrderBookResponse, DashboardStockOrderBookResponse,
DashboardStockOverviewResponse, DashboardStockOverviewResponse,
DashboardStockSearchResponse, DashboardStockSearchResponse,
} from "@/features/dashboard/types/dashboard.types"; } from "@/features/trade/types/trade.types";
import { import {
DOMESTIC_KIS_SESSION_OVERRIDE_HEADER, DOMESTIC_KIS_SESSION_OVERRIDE_HEADER,
DOMESTIC_KIS_SESSION_OVERRIDE_STORAGE_KEY, DOMESTIC_KIS_SESSION_OVERRIDE_STORAGE_KEY,

View File

@@ -1,44 +1,41 @@
"use client"; "use client";
import Link from "next/link";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { ChevronDown, ChevronUp } from "lucide-react";
import { useShallow } from "zustand/react/shallow"; import { useShallow } from "zustand/react/shallow";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useKisRuntimeStore } from "@/features/dashboard/store/use-kis-runtime-store"; import { useKisRuntimeStore } from "@/features/settings/store/use-kis-runtime-store";
import { KisAuthForm } from "@/features/dashboard/components/auth/KisAuthForm"; import { StockSearchForm } from "@/features/trade/components/search/StockSearchForm";
import { StockSearchForm } from "@/features/dashboard/components/search/StockSearchForm"; import { StockSearchHistory } from "@/features/trade/components/search/StockSearchHistory";
import { StockSearchHistory } from "@/features/dashboard/components/search/StockSearchHistory"; import { StockSearchResults } from "@/features/trade/components/search/StockSearchResults";
import { StockSearchResults } from "@/features/dashboard/components/search/StockSearchResults"; import { useStockSearch } from "@/features/trade/hooks/useStockSearch";
import { useStockSearch } from "@/features/dashboard/hooks/useStockSearch"; import { useOrderBook } from "@/features/trade/hooks/useOrderBook";
import { useOrderBook } from "@/features/dashboard/hooks/useOrderBook"; import { useKisTradeWebSocket } from "@/features/trade/hooks/useKisTradeWebSocket";
import { useKisTradeWebSocket } from "@/features/dashboard/hooks/useKisTradeWebSocket"; import { useStockOverview } from "@/features/trade/hooks/useStockOverview";
import { useStockOverview } from "@/features/dashboard/hooks/useStockOverview"; import { useCurrentPrice } from "@/features/trade/hooks/useCurrentPrice";
import { useCurrentPrice } from "@/features/dashboard/hooks/useCurrentPrice"; import { DashboardLayout } from "@/features/trade/components/layout/DashboardLayout";
import { DashboardLayout } from "@/features/dashboard/components/layout/DashboardLayout"; import { StockHeader } from "@/features/trade/components/header/StockHeader";
import { StockHeader } from "@/features/dashboard/components/header/StockHeader"; import { OrderBook } from "@/features/trade/components/orderbook/OrderBook";
import { OrderBook } from "@/features/dashboard/components/orderbook/OrderBook"; import { OrderForm } from "@/features/trade/components/order/OrderForm";
import { OrderForm } from "@/features/dashboard/components/order/OrderForm"; import { StockLineChart } from "@/features/trade/components/chart/StockLineChart";
import { StockLineChart } from "@/features/dashboard/components/chart/StockLineChart";
import type { import type {
DashboardStockOrderBookResponse, DashboardStockOrderBookResponse,
DashboardStockSearchItem, DashboardStockSearchItem,
} from "@/features/dashboard/types/dashboard.types"; } from "@/features/trade/types/trade.types";
/** /**
* @description * @description .
* @see app/(main)/dashboard/page.tsx . * @see app/(main)/trade/page.tsx .
* @see features/dashboard/hooks/useStockSearch.ts // . * @see app/(main)/settings/page.tsx .
* @see features/dashboard/hooks/useStockOverview.ts . * @see features/trade/hooks/useStockSearch.ts // .
* @see features/trade/hooks/useStockOverview.ts .
*/ */
export function DashboardContainer() { export function TradeContainer() {
const skipNextAutoSearchRef = useRef(false); const skipNextAutoSearchRef = useRef(false);
const hasInitializedAuthPanelRef = useRef(false);
const searchShellRef = useRef<HTMLDivElement | null>(null); const searchShellRef = useRef<HTMLDivElement | null>(null);
// 모바일에서는 초기 진입 시 API 패널을 접어 본문(차트/호가)을 먼저 보이게 합니다. // 상태 정의: 검색 패널 열림 상태를 관리합니다.
const [isMobileViewport, setIsMobileViewport] = useState(false);
const [isAuthPanelExpanded, setIsAuthPanelExpanded] = useState(true);
const [isSearchPanelOpen, setIsSearchPanelOpen] = useState(false); const [isSearchPanelOpen, setIsSearchPanelOpen] = useState(false);
const { verifiedCredentials, isKisVerified } = useKisRuntimeStore( const { verifiedCredentials, isKisVerified } = useKisRuntimeStore(
@@ -112,11 +109,12 @@ export function DashboardContainer() {
orderBook, orderBook,
}); });
const canSearch = isKisVerified && !!verifiedCredentials; const canTrade = isKisVerified && !!verifiedCredentials;
const canSearch = canTrade;
/** /**
* @description API . * @description API .
* @see features/dashboard/components/search/StockSearchForm.tsx . * @see features/trade/components/search/StockSearchForm.tsx .
*/ */
const ensureSearchReady = useCallback(() => { const ensureSearchReady = useCallback(() => {
if (canSearch) return true; if (canSearch) return true;
@@ -135,7 +133,7 @@ export function DashboardContainer() {
/** /**
* @description (/) . * @description (/) .
* @see features/dashboard/components/search/StockSearchForm.tsx . * @see features/trade/components/search/StockSearchForm.tsx .
*/ */
const handleSearchShellBlur = useCallback( const handleSearchShellBlur = useCallback(
(event: React.FocusEvent<HTMLDivElement>) => { (event: React.FocusEvent<HTMLDivElement>) => {
@@ -155,35 +153,6 @@ export function DashboardContainer() {
[closeSearchPanel], [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(() => { useEffect(() => {
if (skipNextAutoSearchRef.current) { if (skipNextAutoSearchRef.current) {
skipNextAutoSearchRef.current = false; skipNextAutoSearchRef.current = false;
@@ -210,7 +179,7 @@ export function DashboardContainer() {
/** /**
* @description ( ) . * @description ( ) .
* @see features/dashboard/components/search/StockSearchForm.tsx onSubmit . * @see features/trade/components/search/StockSearchForm.tsx onSubmit .
*/ */
const handleSearchSubmit = useCallback( const handleSearchSubmit = useCallback(
(event: React.FormEvent) => { (event: React.FormEvent) => {
@@ -223,8 +192,8 @@ export function DashboardContainer() {
/** /**
* @description / . * @description / .
* @see features/dashboard/components/search/StockSearchResults.tsx onSelect * @see features/trade/components/search/StockSearchResults.tsx onSelect
* @see features/dashboard/components/search/StockSearchHistory.tsx onSelect * @see features/trade/components/search/StockSearchHistory.tsx onSelect
*/ */
const handleSelectStock = useCallback( const handleSelectStock = useCallback(
(item: DashboardStockSearchItem) => { (item: DashboardStockSearchItem) => {
@@ -257,65 +226,30 @@ export function DashboardContainer() {
], ],
); );
if (!canTrade) {
return ( return (
<div className="relative h-full flex flex-col"> <div className="flex h-full items-center justify-center p-6">
{/* ========== AUTH STATUS ========== */} <section className="w-full max-w-xl rounded-2xl border border-brand-200 bg-background p-6 shadow-sm dark:border-brand-800/45 dark:bg-brand-900/18">
<div className="flex-none border-b bg-muted/40 transition-all duration-300 ease-in-out dark:border-brand-800/45 dark:bg-brand-900/28"> {/* ========== UNVERIFIED NOTICE ========== */}
<div className="flex items-center justify-between gap-2 px-3 py-2 text-xs sm:px-4"> <h2 className="text-lg font-semibold text-foreground">
<div className="flex items-center gap-2"> KIS API .
<span className="font-semibold">KIS API :</span> </h2>
{isKisVerified ? ( <p className="mt-2 text-sm text-muted-foreground">
<span className="flex items-center font-medium text-brand-700 dark:text-brand-200"> App Key/App Secret을
<span className="mr-1.5 h-2 w-2 rounded-full bg-brand-500 ring-2 ring-brand-100 dark:ring-brand-900" /> .
({verifiedCredentials?.tradingEnv === "real" ? "실전" : "모의"}) </p>
</span> <div className="mt-4">
) : ( <Button asChild className="bg-brand-600 hover:bg-brand-700">
<span className="text-muted-foreground flex items-center"> <Link href="/settings"> </Link>
<span className="mr-1.5 h-2 w-2 rounded-full bg-brand-200 dark:bg-brand-500/60" />
</span>
)}
</div>
<Button
type="button"
size="sm"
variant="outline"
onClick={() => setIsAuthPanelExpanded((prev) => !prev)}
className={cn(
"h-8 shrink-0 gap-1.5 px-2.5 text-[11px] font-semibold",
"border-brand-200 bg-brand-50 text-brand-700 hover:bg-brand-100 dark:border-brand-700/60 dark:bg-brand-900/30 dark:text-brand-200 dark:hover:bg-brand-900/45",
!isAuthPanelExpanded &&
isMobileViewport &&
"ring-2 ring-brand-200 dark:ring-brand-600/60",
)}
>
{isAuthPanelExpanded ? (
<>
<ChevronUp className="h-3.5 w-3.5" />
API
</>
) : (
<>
<ChevronDown className="h-3.5 w-3.5" />
API
</>
)}
</Button> </Button>
</div> </div>
</section>
</div>
);
}
<div return (
className={cn( <div className="relative h-full flex flex-col">
"overflow-hidden transition-[max-height,opacity] duration-300 ease-in-out",
isAuthPanelExpanded ? "max-h-[560px] opacity-100" : "max-h-0 opacity-0",
)}
>
<div className="border-t bg-background p-4 dark:border-brand-800/45 dark:bg-brand-900/14">
<KisAuthForm />
</div>
</div>
</div>
{/* ========== SEARCH ========== */} {/* ========== SEARCH ========== */}
<div className="z-30 flex-none border-b bg-background/95 p-4 backdrop-blur-sm dark:border-brand-800/45 dark:bg-brand-900/22"> <div className="z-30 flex-none border-b bg-background/95 p-4 backdrop-blur-sm dark:border-brand-800/45 dark:bg-brand-900/22">
<div <div

View File

@@ -13,13 +13,13 @@ import {
import { ChevronDown } from "lucide-react"; import { ChevronDown } from "lucide-react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { toast } from "sonner"; import { toast } from "sonner";
import { fetchStockChart } from "@/features/dashboard/apis/kis-stock.api"; import { fetchStockChart } from "@/features/trade/apis/kis-stock.api";
import type { KisRuntimeCredentials } from "@/features/dashboard/store/use-kis-runtime-store"; import type { KisRuntimeCredentials } from "@/features/settings/store/use-kis-runtime-store";
import type { import type {
DashboardChartTimeframe, DashboardChartTimeframe,
DashboardRealtimeTradeTick, DashboardRealtimeTradeTick,
StockCandlePoint, StockCandlePoint,
} from "@/features/dashboard/types/dashboard.types"; } from "@/features/trade/types/trade.types";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { import {
type ChartBar, type ChartBar,
@@ -119,7 +119,7 @@ interface StockLineChartProps {
/** /**
* @description TradingView , timeframe별 KIS API를 . * @description TradingView , timeframe별 KIS API를 .
* @see features/dashboard/apis/kis-stock.api.ts fetchStockChart * @see features/trade/apis/kis-stock.api.ts fetchStockChart
* @see lib/kis/domestic.ts getDomesticChart * @see lib/kis/domestic.ts getDomesticChart
*/ */
export function StockLineChart({ export function StockLineChart({
@@ -199,7 +199,7 @@ export function StockLineChart({
/** /**
* @description lightweight-charts OHLCV . * @description lightweight-charts OHLCV .
* @see features/dashboard/components/chart/StockLineChart.tsx renderableBars useMemo * @see features/trade/components/chart/StockLineChart.tsx renderableBars useMemo
*/ */
const setSeriesData = useCallback((nextBars: ChartBar[]) => { const setSeriesData = useCallback((nextBars: ChartBar[]) => {
const candleSeries = candleSeriesRef.current; const candleSeries = candleSeriesRef.current;
@@ -524,8 +524,8 @@ export function StockLineChart({
/** /**
* @description WebSocket timeframe . * @description WebSocket timeframe .
* @see features/dashboard/hooks/useKisTradeWebSocket.ts latestTick * @see features/trade/hooks/useKisTradeWebSocket.ts latestTick
* @see features/dashboard/components/chart/chart-utils.ts toRealtimeTickBar * @see features/trade/components/chart/chart-utils.ts toRealtimeTickBar
*/ */
useEffect(() => { useEffect(() => {
if (!latestTick) return; if (!latestTick) return;
@@ -544,7 +544,7 @@ export function StockLineChart({
/** /**
* @description (1m/30m/1h) WS . * @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 * @see lib/kis/domestic.ts getDomesticChart
*/ */
useEffect(() => { useEffect(() => {

View File

@@ -12,7 +12,7 @@ import type {
DashboardChartTimeframe, DashboardChartTimeframe,
DashboardRealtimeTradeTick, DashboardRealtimeTradeTick,
StockCandlePoint, StockCandlePoint,
} from "@/features/dashboard/types/dashboard.types"; } from "@/features/trade/types/trade.types";
const KRW_FORMATTER = new Intl.NumberFormat("ko-KR"); const KRW_FORMATTER = new Intl.NumberFormat("ko-KR");
const KST_TIME_ZONE = "Asia/Seoul"; const KST_TIME_ZONE = "Asia/Seoul";
@@ -228,8 +228,8 @@ export function upsertRealtimeBar(
/** /**
* @description ChartBar로 . (KST + tickTime ) * @description ChartBar로 . (KST + tickTime )
* @see features/dashboard/hooks/useKisTradeWebSocket.ts latestTick * @see features/trade/hooks/useKisTradeWebSocket.ts latestTick
* @see features/dashboard/components/chart/StockLineChart.tsx * @see features/trade/components/chart/StockLineChart.tsx
*/ */
export function toRealtimeTickBar( export function toRealtimeTickBar(
tick: DashboardRealtimeTradeTick, tick: DashboardRealtimeTradeTick,
@@ -260,7 +260,7 @@ export function toRealtimeTickBar(
/** /**
* @description lightweight-charts X축 KST . * @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) { export function formatKstTickMark(time: Time, tickMarkType: TickMarkType) {
const date = toDateFromChartTime(time); const date = toDateFromChartTime(time);
@@ -275,7 +275,7 @@ export function formatKstTickMark(time: Time, tickMarkType: TickMarkType) {
/** /**
* @description crosshair KST로 . * @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) { export function formatKstCrosshairTime(time: Time) {
const date = toDateFromChartTime(time); const date = toDateFromChartTime(time);

View File

@@ -6,13 +6,13 @@ import {
CardTitle, CardTitle,
CardDescription, CardDescription,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { StockLineChart } from "@/features/dashboard/components/chart/StockLineChart"; import { StockLineChart } from "@/features/trade/components/chart/StockLineChart";
import { StockPriceBadge } from "@/features/dashboard/components/details/StockPriceBadge"; import { StockPriceBadge } from "@/features/trade/components/details/StockPriceBadge";
import type { import type {
DashboardStockItem, DashboardStockItem,
DashboardPriceSource, DashboardPriceSource,
DashboardMarketPhase, DashboardMarketPhase,
} from "@/features/dashboard/types/dashboard.types"; } from "@/features/trade/types/trade.types";
const PRICE_FORMATTER = new Intl.NumberFormat("ko-KR"); const PRICE_FORMATTER = new Intl.NumberFormat("ko-KR");
function formatVolume(value: number) { function formatVolume(value: number) {

View File

@@ -1,6 +1,6 @@
// import { Badge } from "@/components/ui/badge"; // import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator"; 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"; import { cn } from "@/lib/utils";
interface StockHeaderProps { interface StockHeaderProps {

View File

@@ -3,12 +3,12 @@ import { Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useOrder } from "@/features/dashboard/hooks/useOrder"; import { useOrder } from "@/features/trade/hooks/useOrder";
import { useKisRuntimeStore } from "@/features/dashboard/store/use-kis-runtime-store"; import { useKisRuntimeStore } from "@/features/settings/store/use-kis-runtime-store";
import type { import type {
DashboardOrderSide, DashboardOrderSide,
DashboardStockItem, DashboardStockItem,
} from "@/features/dashboard/types/dashboard.types"; } from "@/features/trade/types/trade.types";
interface OrderFormProps { interface OrderFormProps {
stock?: DashboardStockItem; stock?: DashboardStockItem;
@@ -16,8 +16,8 @@ interface OrderFormProps {
/** /**
* @description / . * @description / .
* @see features/dashboard/hooks/useOrder.ts placeOrder - API * @see features/trade/hooks/useOrder.ts placeOrder - API
* @see features/dashboard/components/DashboardContainer.tsx OrderForm - * @see features/trade/components/TradeContainer.tsx OrderForm -
*/ */
export function OrderForm({ stock }: OrderFormProps) { export function OrderForm({ stock }: OrderFormProps) {
const verifiedCredentials = useKisRuntimeStore( const verifiedCredentials = useKisRuntimeStore(
@@ -161,7 +161,7 @@ export function OrderForm({ stock }: OrderFormProps) {
/** /**
* @description (//) . * @description (//) .
* @see features/dashboard/components/order/OrderForm.tsx OrderForm - / * @see features/trade/components/order/OrderForm.tsx OrderForm - /
*/ */
function OrderInputs({ function OrderInputs({
type, type,
@@ -236,7 +236,7 @@ function OrderInputs({
/** /**
* @description (10/25/50/100%) . * @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 }) { function PercentButtons({ onSelect }: { onSelect: (pct: string) => void }) {
return ( return (

View File

@@ -5,7 +5,7 @@ import { Skeleton } from "@/components/ui/skeleton";
import type { import type {
DashboardRealtimeTradeTick, DashboardRealtimeTradeTick,
DashboardStockOrderBookResponse, DashboardStockOrderBookResponse,
} from "@/features/dashboard/types/dashboard.types"; } from "@/features/trade/types/trade.types";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { AnimatedQuantity } from "./AnimatedQuantity"; import { AnimatedQuantity } from "./AnimatedQuantity";

View File

@@ -14,7 +14,7 @@ interface StockSearchFormProps {
/** /**
* @description / . * @description / .
* @see features/dashboard/components/DashboardContainer.tsx . * @see features/trade/components/TradeContainer.tsx .
*/ */
export function StockSearchForm({ export function StockSearchForm({
keyword, keyword,

View File

@@ -1,7 +1,7 @@
import { Clock3, Trash2, X } from "lucide-react"; import { Clock3, Trash2, X } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils"; 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 { interface StockSearchHistoryProps {
items: DashboardStockSearchHistoryItem[]; items: DashboardStockSearchHistoryItem[];
@@ -13,8 +13,8 @@ interface StockSearchHistoryProps {
/** /**
* @description , // . * @description , // .
* @see features/dashboard/components/DashboardContainer.tsx UI로 . * @see features/trade/components/TradeContainer.tsx UI로 .
* @see features/dashboard/hooks/useStockSearch.ts searchHistory . * @see features/trade/hooks/useStockSearch.ts searchHistory .
*/ */
export function StockSearchHistory({ export function StockSearchHistory({
items, items,

View File

@@ -1,7 +1,7 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
// import { Activity, TrendingDown, TrendingUp } from "lucide-react"; // import { Activity, TrendingDown, TrendingUp } from "lucide-react";
import { cn } from "@/lib/utils"; 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 { interface StockSearchResultsProps {
items: DashboardStockSearchItem[]; items: DashboardStockSearchItem[];

View File

@@ -1,5 +1,5 @@
import rawStocks from "@/features/dashboard/data/korean-stocks.json"; import rawStocks from "@/features/trade/data/korean-stocks.json";
import type { KoreanStockIndexItem } from "@/features/dashboard/types/dashboard.types"; import type { KoreanStockIndexItem } from "@/features/trade/types/trade.types";
/** /**
* (KOSPI + KOSDAQ) * (KOSPI + KOSDAQ)

View File

@@ -1,5 +1,5 @@
/** /**
* @file features/dashboard/data/mock-stocks.ts * @file features/trade/data/mock-stocks.ts
* @description 1 UI * @description 1 UI
* @remarks * @remarks
* - API . * - 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 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[] = [ export const MOCK_STOCKS: DashboardStockItem[] = [
{ {

View File

@@ -3,7 +3,7 @@ import type {
DashboardRealtimeTradeTick, DashboardRealtimeTradeTick,
DashboardStockItem, DashboardStockItem,
DashboardStockOrderBookResponse, DashboardStockOrderBookResponse,
} from "@/features/dashboard/types/dashboard.types"; } from "@/features/trade/types/trade.types";
interface UseCurrentPriceParams { interface UseCurrentPriceParams {
stock?: DashboardStockItem | null; stock?: DashboardStockItem | null;

View File

@@ -2,16 +2,16 @@ import { useEffect, useRef, useState } from "react";
import { import {
type KisRuntimeCredentials, type KisRuntimeCredentials,
useKisRuntimeStore, useKisRuntimeStore,
} from "@/features/dashboard/store/use-kis-runtime-store"; } from "@/features/settings/store/use-kis-runtime-store";
import type { import type {
DashboardRealtimeTradeTick, DashboardRealtimeTradeTick,
DashboardStockOrderBookResponse, DashboardStockOrderBookResponse,
} from "@/features/dashboard/types/dashboard.types"; } from "@/features/trade/types/trade.types";
import { import {
buildKisRealtimeMessage, buildKisRealtimeMessage,
parseKisRealtimeOrderbook, parseKisRealtimeOrderbook,
parseKisRealtimeTickBatch, parseKisRealtimeTickBatch,
} from "@/features/dashboard/utils/kis-realtime.utils"; } from "@/features/trade/utils/kis-realtime.utils";
import { import {
DOMESTIC_KIS_SESSION_OVERRIDE_STORAGE_KEY, DOMESTIC_KIS_SESSION_OVERRIDE_STORAGE_KEY,
resolveDomesticKisSession, resolveDomesticKisSession,
@@ -32,8 +32,8 @@ function resolveTradeTrId(
session: DomesticKisSession, session: DomesticKisSession,
) { ) {
if (env === "mock") return TRADE_TR_ID; if (env === "mock") return TRADE_TR_ID;
if (shouldUseAfterHoursSinglePriceTr(session)) return TRADE_TR_ID_OVERTIME; if (shouldUseAfterHoursSinglePriceTr(session)) return TRADE_TR_ID;
if (shouldUseExpectedExecutionTr(session)) return TRADE_TR_ID_EXPECTED; if (shouldUseExpectedExecutionTr(session)) return TRADE_TR_ID;
return TRADE_TR_ID; return TRADE_TR_ID;
} }
@@ -42,13 +42,14 @@ function resolveOrderBookTrId(
session: DomesticKisSession, session: DomesticKisSession,
) { ) {
if (env === "mock") return ORDERBOOK_TR_ID; 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; return ORDERBOOK_TR_ID;
} }
/** /**
* @description Subscribes trade ticks and orderbook over one websocket. * @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 * @see lib/kis/domestic-market-session.ts
*/ */
export function useKisTradeWebSocket( export function useKisTradeWebSocket(

View File

@@ -1,10 +1,10 @@
import { useState, useCallback } from "react"; 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 { import type {
DashboardStockCashOrderRequest, DashboardStockCashOrderRequest,
DashboardStockCashOrderResponse, DashboardStockCashOrderResponse,
} from "@/features/dashboard/types/dashboard.types"; } from "@/features/trade/types/trade.types";
import { fetchOrderCash } from "@/features/dashboard/apis/kis-stock.api"; import { fetchOrderCash } from "@/features/trade/apis/kis-stock.api";
export function useOrder() { export function useOrder() {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);

View File

@@ -1,15 +1,15 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } 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 { DashboardStockOrderBookResponse } from "@/features/dashboard/types/dashboard.types"; import type { DashboardStockOrderBookResponse } from "@/features/trade/types/trade.types";
import { fetchStockOrderBook } from "@/features/dashboard/apis/kis-stock.api"; import { fetchStockOrderBook } from "@/features/trade/apis/kis-stock.api";
import { toast } from "sonner"; import { toast } from "sonner";
/** /**
* @description REST , . * @description REST , .
* DashboardContainer에서 useKisTradeWebSocket을 * TradeContainer에서 useKisTradeWebSocket을
* WebSocket으로 externalRealtimeOrderBook으로 . * WebSocket으로 externalRealtimeOrderBook으로 .
* @see features/dashboard/components/DashboardContainer.tsx * @see features/trade/components/TradeContainer.tsx
* @see features/dashboard/components/orderbook/OrderBook.tsx * @see features/trade/components/orderbook/OrderBook.tsx
*/ */
export function useOrderBook( export function useOrderBook(
symbol: string | undefined, symbol: string | undefined,

View File

@@ -1,13 +1,13 @@
import { useCallback, useState, useTransition } from "react"; 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 { import type {
DashboardMarketPhase, DashboardMarketPhase,
DashboardPriceSource, DashboardPriceSource,
DashboardRealtimeTradeTick, DashboardRealtimeTradeTick,
DashboardStockSearchItem, DashboardStockSearchItem,
DashboardStockItem, DashboardStockItem,
} from "@/features/dashboard/types/dashboard.types"; } from "@/features/trade/types/trade.types";
import { fetchStockOverview } from "@/features/dashboard/apis/kis-stock.api"; import { fetchStockOverview } from "@/features/trade/apis/kis-stock.api";
interface OverviewMeta { interface OverviewMeta {
priceSource: DashboardPriceSource; priceSource: DashboardPriceSource;
@@ -60,8 +60,8 @@ export function useStockOverview() {
/** /**
* / . * / .
* StockLineChart API . * StockLineChart API .
* @see features/dashboard/components/DashboardContainer.tsx useKisTradeWebSocket onTick * @see features/trade/components/TradeContainer.tsx useKisTradeWebSocket onTick
* @see features/dashboard/components/chart/StockLineChart.tsx fetchStockChart * @see features/trade/components/chart/StockLineChart.tsx fetchStockChart
*/ */
const updateRealtimeTradeTick = useCallback( const updateRealtimeTradeTick = useCallback(
(tick: DashboardRealtimeTradeTick) => { (tick: DashboardRealtimeTradeTick) => {

View File

@@ -1,10 +1,10 @@
import { useCallback, useRef, useState } from "react"; import { useCallback, useRef, useState } from "react";
import { fetchStockSearch } from "@/features/dashboard/apis/kis-stock.api"; import { fetchStockSearch } from "@/features/trade/apis/kis-stock.api";
import type { KisRuntimeCredentials } from "@/features/dashboard/store/use-kis-runtime-store"; import type { KisRuntimeCredentials } from "@/features/settings/store/use-kis-runtime-store";
import type { import type {
DashboardStockSearchHistoryItem, DashboardStockSearchHistoryItem,
DashboardStockSearchItem, 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_STORAGE_KEY = "jurini:stock-search-history:v1";
const SEARCH_HISTORY_LIMIT = 12; const SEARCH_HISTORY_LIMIT = 12;
@@ -44,8 +44,8 @@ function writeSearchHistory(items: DashboardStockSearchHistoryItem[]) {
/** /**
* @description (//) (localStorage) . * @description (//) (localStorage) .
* @see features/dashboard/components/DashboardContainer.tsx // . * @see features/trade/components/TradeContainer.tsx // .
* @see features/dashboard/components/search/StockSearchHistory.tsx . * @see features/trade/components/search/StockSearchHistory.tsx .
*/ */
export function useStockSearch() { export function useStockSearch() {
// ========== SEARCH STATE ========== // ========== SEARCH STATE ==========
@@ -92,7 +92,7 @@ export function useStockSearch() {
/** /**
* @description API를 . * @description API를 .
* @see features/dashboard/components/DashboardContainer.tsx handleSearchSubmit / . * @see features/trade/components/TradeContainer.tsx handleSearchSubmit / .
*/ */
const search = useCallback( const search = useCallback(
(query: string, credentials: KisRuntimeCredentials | null) => { (query: string, credentials: KisRuntimeCredentials | null) => {
@@ -119,7 +119,7 @@ export function useStockSearch() {
/** /**
* @description . * @description .
* @see features/dashboard/components/DashboardContainer.tsx . * @see features/trade/components/TradeContainer.tsx .
*/ */
const clearSearch = useCallback(() => { const clearSearch = useCallback(() => {
abortRef.current?.abort(); abortRef.current?.abort();
@@ -130,7 +130,7 @@ export function useStockSearch() {
/** /**
* @description API setter입니다. * @description API setter입니다.
* @see features/dashboard/components/DashboardContainer.tsx ensureSearchReady . * @see features/trade/components/TradeContainer.tsx ensureSearchReady .
*/ */
const setSearchError = useCallback((message: string | null) => { const setSearchError = useCallback((message: string | null) => {
setError(message); setError(message);
@@ -138,7 +138,7 @@ export function useStockSearch() {
/** /**
* @description ( ). * @description ( ).
* @see features/dashboard/components/DashboardContainer.tsx handleSelectStock . * @see features/trade/components/TradeContainer.tsx handleSelectStock .
*/ */
const appendSearchHistory = useCallback((item: DashboardStockSearchItem) => { const appendSearchHistory = useCallback((item: DashboardStockSearchItem) => {
setSearchHistory((prev) => { setSearchHistory((prev) => {
@@ -155,7 +155,7 @@ export function useStockSearch() {
/** /**
* @description . * @description .
* @see features/dashboard/components/search/StockSearchHistory.tsx . * @see features/trade/components/search/StockSearchHistory.tsx .
*/ */
const removeSearchHistory = useCallback((symbol: string) => { const removeSearchHistory = useCallback((symbol: string) => {
setSearchHistory((prev) => { setSearchHistory((prev) => {
@@ -167,7 +167,7 @@ export function useStockSearch() {
/** /**
* @description . * @description .
* @see features/dashboard/components/search/StockSearchHistory.tsx . * @see features/trade/components/search/StockSearchHistory.tsx .
*/ */
const clearSearchHistory = useCallback(() => { const clearSearchHistory = useCallback(() => {
setSearchHistory([]); setSearchHistory([]);

View File

@@ -1,5 +1,5 @@
/** /**
* @file features/dashboard/types/dashboard.types.ts * @file features/trade/types/trade.types.ts
* @description (//) * @description (//)
*/ */
@@ -75,7 +75,7 @@ export interface DashboardStockSearchItem {
/** /**
* 1 * 1
* @see features/dashboard/hooks/useStockSearch.ts localStorage에 / . * @see features/trade/hooks/useStockSearch.ts localStorage에 / .
*/ */
export interface DashboardStockSearchHistoryItem export interface DashboardStockSearchHistoryItem
extends DashboardStockSearchItem { extends DashboardStockSearchItem {

View File

@@ -1,7 +1,7 @@
import type { import type {
DashboardRealtimeTradeTick, DashboardRealtimeTradeTick,
DashboardStockOrderBookResponse, DashboardStockOrderBookResponse,
} from "@/features/dashboard/types/dashboard.types"; } from "@/features/trade/types/trade.types";
const REALTIME_SIGN_NEGATIVE = new Set(["4", "5"]); const REALTIME_SIGN_NEGATIVE = new Set(["4", "5"]);
const ALLOWED_REALTIME_TRADE_TR_IDS = new Set([ const ALLOWED_REALTIME_TRADE_TR_IDS = new Set([
@@ -32,7 +32,8 @@ const TICK_FIELD_INDEX = {
} as const; } as const;
/** /**
* KIS ??щ/? ??붿€????? * @description KIS / .
* @see features/trade/hooks/useKisTradeWebSocket.ts / payload .
*/ */
export function buildKisRealtimeMessage( export function buildKisRealtimeMessage(
approvalKey: string, approvalKey: string,
@@ -57,9 +58,10 @@ export function buildKisRealtimeMessage(
} }
/** /**
* ?? ??raw)?? ?????? * @description (raw) .
* - ?( ????? ? 붿 * - ( ) .
* - ?щ ?€?0 ? ?? ? * - 0 .
* @see features/trade/hooks/useKisTradeWebSocket.ts onmessage .
*/ */
export function parseKisRealtimeTickBatch(raw: string, expectedSymbol: string) { export function parseKisRealtimeTickBatch(raw: string, expectedSymbol: string) {
if (!/^([01])\|/.test(raw)) return [] as DashboardRealtimeTradeTick[]; 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( export function parseKisRealtimeOrderbook(
raw: string, raw: string,
@@ -224,8 +227,8 @@ export function parseKisRealtimeOrderbook(
} }
/** /**
* @description ? ?? ? ??? 6? ????. * @description 6 .
* @see features/dashboard/utils/kis-realtime.utils.ts parseKisRealtimeOrderbook * @see features/trade/utils/kis-realtime.utils.ts parseKisRealtimeOrderbook .
*/ */
function normalizeDomesticSymbol(value: string) { function normalizeDomesticSymbol(value: string) {
const trimmed = value.trim(); const trimmed = value.trim();

View File

@@ -90,7 +90,7 @@ function tryParseApprovalResponse(rawText: string): KisApprovalResponse {
/** /**
* @description 승인키를 캐시에서 반환하거나 새로 발급합니다. * @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) { export async function getKisApprovalKey(credentials?: KisCredentialInput) {
const cacheKey = getApprovalCacheKey(credentials); const cacheKey = getApprovalCacheKey(credentials);

View File

@@ -29,7 +29,7 @@ const AFTER_HOURS_SINGLE_END_MINUTES = 18 * 60; // 18:00
/** /**
* @description Converts external string to strict session enum. * @description Converts external string to strict session enum.
* @see lib/kis/domestic.ts getDomesticOrderBook * @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) { export function parseDomesticKisSession(value?: string | null) {
if (!value) return null; if (!value) return null;
@@ -53,7 +53,7 @@ export function parseDomesticKisSession(value?: string | null) {
/** /**
* @description Returns current session in KST. * @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 * @see lib/kis/domestic.ts REST orderbook source switching
*/ */
export function getDomesticKisSessionInKst(now = new Date()): DomesticKisSession { 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. * @description If override is valid, use it. Otherwise use real KST time.
* @see app/api/kis/domestic/orderbook/route.ts session override header * @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( export function resolveDomesticKisSession(
override?: string | null, override?: string | null,
@@ -143,7 +143,7 @@ export function shouldUseOvertimeOrderBookApi(session: DomesticKisSession) {
/** /**
* @description Whether trade tick should use expected-execution TR. * @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) { export function shouldUseExpectedExecutionTr(session: DomesticKisSession) {
return session === "openAuction" || session === "closeAuction"; 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. * @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) { export function shouldUseAfterHoursSinglePriceTr(session: DomesticKisSession) {
return session === "afterHoursSinglePrice"; return session === "afterHoursSinglePrice";

View File

@@ -2,7 +2,7 @@ import type {
DashboardChartTimeframe, DashboardChartTimeframe,
DashboardStockItem, DashboardStockItem,
StockCandlePoint, StockCandlePoint,
} from "@/features/dashboard/types/dashboard.types"; } from "@/features/trade/types/trade.types";
import type { KisCredentialInput } from "@/lib/kis/config"; import type { KisCredentialInput } from "@/lib/kis/config";
import { kisGet } from "@/lib/kis/client"; import { kisGet } from "@/lib/kis/client";
import { import {

View File

@@ -3,7 +3,7 @@ import { KisCredentialInput } from "@/lib/kis/config";
import { import {
DashboardOrderSide, DashboardOrderSide,
DashboardOrderType, DashboardOrderType,
} from "@/features/dashboard/types/dashboard.types"; } from "@/features/trade/types/trade.types";
/** /**
* @file lib/kis/trade.ts * @file lib/kis/trade.ts