임시커밋
This commit is contained in:
@@ -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}}
|
||||
@@ -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 <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>
|
||||
);
|
||||
}
|
||||
|
||||
26
app/(main)/settings/page.tsx
Normal file
26
app/(main)/settings/page.tsx
Normal 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
26
app/(main)/trade/page.tsx
Normal 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 />;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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)를 읽고 공백을 제거합니다.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
51
features/settings/components/SettingsContainer.tsx
Normal file
51
features/settings/components/SettingsContainer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<KisWsConnection | null> | 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
|
||||
@@ -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,
|
||||
@@ -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<HTMLDivElement | null>(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<HTMLDivElement>) => {
|
||||
@@ -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 (
|
||||
<div className="flex h-full items-center justify-center p-6">
|
||||
<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">
|
||||
{/* ========== UNVERIFIED NOTICE ========== */}
|
||||
<h2 className="text-lg font-semibold text-foreground">
|
||||
트레이딩을 시작하려면 KIS API 인증이 필요합니다.
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
설정 페이지에서 App Key/App Secret을 입력하고 연결 상태를 확인해
|
||||
주세요.
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<Button asChild className="bg-brand-600 hover:bg-brand-700">
|
||||
<Link href="/settings">설정 페이지로 이동</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative h-full flex flex-col">
|
||||
{/* ========== AUTH STATUS ========== */}
|
||||
<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">
|
||||
<div className="flex items-center justify-between gap-2 px-3 py-2 text-xs sm:px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold">KIS API 연결 상태:</span>
|
||||
{isKisVerified ? (
|
||||
<span className="flex items-center font-medium text-brand-700 dark:text-brand-200">
|
||||
<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" ? "실전" : "모의"})
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground flex items-center">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"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 ========== */}
|
||||
<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
|
||||
@@ -13,13 +13,13 @@ import {
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { toast } from "sonner";
|
||||
import { fetchStockChart } from "@/features/dashboard/apis/kis-stock.api";
|
||||
import type { KisRuntimeCredentials } from "@/features/dashboard/store/use-kis-runtime-store";
|
||||
import { fetchStockChart } from "@/features/trade/apis/kis-stock.api";
|
||||
import type { KisRuntimeCredentials } from "@/features/settings/store/use-kis-runtime-store";
|
||||
import type {
|
||||
DashboardChartTimeframe,
|
||||
DashboardRealtimeTradeTick,
|
||||
StockCandlePoint,
|
||||
} from "@/features/dashboard/types/dashboard.types";
|
||||
} from "@/features/trade/types/trade.types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
type ChartBar,
|
||||
@@ -119,7 +119,7 @@ interface StockLineChartProps {
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
export function StockLineChart({
|
||||
@@ -199,7 +199,7 @@ export function StockLineChart({
|
||||
|
||||
/**
|
||||
* @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 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(() => {
|
||||
@@ -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);
|
||||
@@ -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) {
|
||||
@@ -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 {
|
||||
@@ -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 (
|
||||
@@ -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";
|
||||
|
||||
@@ -14,7 +14,7 @@ interface StockSearchFormProps {
|
||||
|
||||
/**
|
||||
* @description 종목 검색 입력/제출 폼을 렌더링합니다.
|
||||
* @see features/dashboard/components/DashboardContainer.tsx 검색 패널에서 키워드 입력 이벤트를 전달합니다.
|
||||
* @see features/trade/components/TradeContainer.tsx 검색 패널에서 키워드 입력 이벤트를 전달합니다.
|
||||
*/
|
||||
export function StockSearchForm({
|
||||
keyword,
|
||||
@@ -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,
|
||||
@@ -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[];
|
||||
@@ -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)
|
||||
@@ -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[] = [
|
||||
{
|
||||
@@ -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;
|
||||
@@ -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(
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
@@ -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) => {
|
||||
@@ -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([]);
|
||||
@@ -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 {
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user