Files
auto-trade/features/trade/components/search/TradeSearchSection.tsx

236 lines
8.2 KiB
TypeScript
Raw Normal View History

2026-02-12 10:24:03 +09:00
import type { FormEvent, KeyboardEvent, FocusEvent, MutableRefObject } from "react";
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 type {
DashboardStockItem,
2026-02-12 10:24:03 +09:00
DashboardStockSearchHistoryItem,
DashboardStockSearchItem,
} from "@/features/trade/types/trade.types";
import { cn } from "@/lib/utils";
2026-02-12 10:24:03 +09:00
interface TradeSearchSectionProps {
canSearch: boolean;
isSearchPanelOpen: boolean;
isSearching: boolean;
keyword: string;
selectedStock: DashboardStockItem | null;
2026-02-12 10:24:03 +09:00
selectedSymbol?: string;
currentPrice?: number;
change?: number;
changeRate?: number;
2026-02-12 10:24:03 +09:00
searchResults: DashboardStockSearchItem[];
searchHistory: DashboardStockSearchHistoryItem[];
searchShellRef: MutableRefObject<HTMLDivElement | null>;
onKeywordChange: (value: string) => void;
onSearchSubmit: (event: FormEvent) => void;
onSearchFocus: () => void;
onSearchShellBlur: (event: FocusEvent<HTMLDivElement>) => void;
onSearchShellKeyDown: (event: KeyboardEvent<HTMLDivElement>) => void;
onSelectStock: (item: DashboardStockSearchItem) => void;
onRemoveHistory: (symbol: string) => void;
onClearHistory: () => void;
}
/**
* @description // .
* @summary UI 흐름: TradeContainer -> TradeSearchSection -> ( /) + ( )
* @see features/trade/components/TradeContainer.tsx - / .
2026-02-12 10:24:03 +09:00
*/
export function TradeSearchSection({
canSearch,
isSearchPanelOpen,
isSearching,
keyword,
selectedStock,
2026-02-12 10:24:03 +09:00
selectedSymbol,
currentPrice,
change,
changeRate,
2026-02-12 10:24:03 +09:00
searchResults,
searchHistory,
searchShellRef,
onKeywordChange,
onSearchSubmit,
onSearchFocus,
onSearchShellBlur,
onSearchShellKeyDown,
onSelectStock,
onRemoveHistory,
onClearHistory,
}: TradeSearchSectionProps) {
return (
<div className="z-30 flex-none border-b bg-background/95 px-3 py-1 backdrop-blur-sm dark:border-brand-800/45 dark:bg-brand-900/22 sm:px-4">
{/* ========== TOP BAR (검색 + 종목 요약 통합) ========== */}
<div className="mx-auto flex max-w-[1800px] items-center gap-2">
{/* ========== SEARCH SHELL ========== */}
<div
ref={searchShellRef}
onBlurCapture={onSearchShellBlur}
onKeyDownCapture={onSearchShellKeyDown}
className="relative min-w-0 flex-1 md:max-w-[480px]"
>
<StockSearchForm
keyword={keyword}
onKeywordChange={onKeywordChange}
onSubmit={onSearchSubmit}
onInputFocus={onSearchFocus}
disabled={!canSearch}
isLoading={isSearching}
/>
{/* ========== SEARCH DROPDOWN ========== */}
{isSearchPanelOpen && canSearch && (
<div className="absolute left-0 right-0 top-full z-50 mt-1 max-h-80 overflow-x-hidden overflow-y-auto rounded-md border bg-background shadow-lg dark:border-brand-800/45 dark:bg-brand-950/95">
{searchResults.length > 0 ? (
<StockSearchResults
items={searchResults}
onSelect={onSelectStock}
selectedSymbol={selectedSymbol}
/>
) : keyword.trim() ? (
<div className="px-3 py-2 text-sm text-muted-foreground">
{isSearching ? "검색 중..." : "검색 결과가 없습니다."}
</div>
) : searchHistory.length > 0 ? (
<StockSearchHistory
items={searchHistory}
onSelect={onSelectStock}
onRemove={onRemoveHistory}
onClear={onClearHistory}
selectedSymbol={selectedSymbol}
/>
) : (
<div className="px-3 py-2 text-sm text-muted-foreground">
.
</div>
)}
</div>
)}
</div>
<InlineStockSummary
stock={selectedStock}
currentPrice={currentPrice}
change={change}
changeRate={changeRate}
2026-02-12 10:24:03 +09:00
/>
</div>
</div>
);
}
2026-02-12 10:24:03 +09:00
/**
* @description / .
* @see features/trade/components/search/TradeSearchSection.tsx - 1 .
*/
function InlineStockSummary({
stock,
currentPrice,
change,
changeRate,
}: {
stock: DashboardStockItem | null;
currentPrice?: number;
change?: number;
changeRate?: number;
}) {
if (!stock) {
return (
<div className="hidden min-w-0 flex-1 items-center justify-end md:flex">
<div className="rounded-md border border-dashed border-border/80 px-3 py-1 text-xs text-muted-foreground dark:border-brand-800/45 dark:text-brand-100/65">
/ .
</div>
2026-02-12 10:24:03 +09:00
</div>
);
}
const displayPrice = currentPrice ?? stock.currentPrice;
const displayChange = change ?? stock.change;
const displayChangeRate = changeRate ?? stock.changeRate;
const isRise = displayChangeRate > 0;
const isFall = displayChangeRate < 0;
const priceToneClass = isRise
? "text-red-600 dark:text-red-400"
: isFall
? "text-blue-600 dark:text-blue-400"
: "text-foreground dark:text-brand-50";
return (
<div className="min-w-0 flex-1">
<div className="flex items-center justify-end gap-2 overflow-hidden rounded-lg border border-brand-200/50 bg-white/70 px-2 py-1 dark:border-brand-700/45 dark:bg-brand-900/30">
<div className="min-w-0">
<p className="truncate text-xs font-semibold text-foreground dark:text-brand-50">
{stock.name}
</p>
<p className="truncate text-[10px] text-muted-foreground dark:text-brand-100/65">
{stock.symbol} · {stock.market}
</p>
</div>
<div className="border-l border-border/65 pl-2 text-right dark:border-brand-700/45">
<p className={cn("text-sm font-bold tabular-nums", priceToneClass)}>
{displayPrice.toLocaleString("ko-KR")}
</p>
<p className={cn("text-[10px] tabular-nums", priceToneClass)}>
{isRise ? "+" : ""}
{displayChange.toLocaleString("ko-KR")} (
{isRise ? "+" : ""}
{displayChangeRate.toFixed(2)}%)
</p>
</div>
<div className="hidden items-center gap-2 border-l border-border/65 pl-2 dark:border-brand-700/45 xl:flex">
<CompactMetric
label="고"
value={stock.high.toLocaleString("ko-KR")}
tone="ask"
/>
<CompactMetric
label="저"
value={stock.low.toLocaleString("ko-KR")}
tone="bid"
/>
<CompactMetric
label="거래량"
value={stock.volume.toLocaleString("ko-KR")}
/>
</div>
</div>
</div>
);
}
/**
* @description 1 (//) .
* @summary UI 흐름: InlineStockSummary -> CompactMetric ->
* @see features/trade/components/search/TradeSearchSection.tsx -
*/
function CompactMetric({
label,
value,
tone,
}: {
label: string;
value: string;
tone?: "ask" | "bid";
}) {
return (
<div className="rounded-md bg-muted/35 px-2 py-1 dark:bg-brand-900/25">
<p className="text-[10px] text-muted-foreground dark:text-brand-100/70">
{label}
</p>
<p
className={cn(
"max-w-[120px] truncate text-[11px] font-semibold tabular-nums",
tone === "ask" && "text-red-600 dark:text-red-400",
tone === "bid" && "text-blue-600 dark:text-blue-400",
!tone && "text-foreground dark:text-brand-50",
)}
>
{value}
</p>
2026-02-12 10:24:03 +09:00
</div>
);
}