트레이딩창 UI 배치 및 UX 수정 및 기획서 추가
This commit is contained in:
@@ -31,7 +31,7 @@ export function StockSearchForm({
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit} className="flex gap-2">
|
||||
<form onSubmit={onSubmit} className="flex items-center gap-2">
|
||||
{/* ========== SEARCH INPUT ========== */}
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground dark:text-brand-100/65" />
|
||||
@@ -39,9 +39,9 @@ export function StockSearchForm({
|
||||
value={keyword}
|
||||
onChange={(e) => onKeywordChange(e.target.value)}
|
||||
onFocus={onInputFocus}
|
||||
placeholder="종목명 또는 종목코드(6자리)를 입력하세요."
|
||||
placeholder="종목명 또는 코드 검색"
|
||||
autoComplete="off"
|
||||
className="pl-9 pr-9 dark:border-brand-700/55 dark:bg-brand-900/28 dark:text-brand-100 dark:placeholder:text-brand-100/55"
|
||||
className="h-9 pl-9 pr-9 dark:border-brand-700/55 dark:bg-brand-900/28 dark:text-brand-100 dark:placeholder:text-brand-100/55"
|
||||
/>
|
||||
{keyword && (
|
||||
<button
|
||||
@@ -57,7 +57,11 @@ export function StockSearchForm({
|
||||
</div>
|
||||
|
||||
{/* ========== SUBMIT BUTTON ========== */}
|
||||
<Button type="submit" disabled={disabled || isLoading}>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={disabled || isLoading}
|
||||
className="h-9 px-2.5 text-xs sm:px-3 sm:text-sm"
|
||||
>
|
||||
{isLoading ? "검색 중..." : "검색"}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
@@ -3,16 +3,22 @@ import { StockSearchForm } from "@/features/trade/components/search/StockSearchF
|
||||
import { StockSearchHistory } from "@/features/trade/components/search/StockSearchHistory";
|
||||
import { StockSearchResults } from "@/features/trade/components/search/StockSearchResults";
|
||||
import type {
|
||||
DashboardStockItem,
|
||||
DashboardStockSearchHistoryItem,
|
||||
DashboardStockSearchItem,
|
||||
} from "@/features/trade/types/trade.types";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface TradeSearchSectionProps {
|
||||
canSearch: boolean;
|
||||
isSearchPanelOpen: boolean;
|
||||
isSearching: boolean;
|
||||
keyword: string;
|
||||
selectedStock: DashboardStockItem | null;
|
||||
selectedSymbol?: string;
|
||||
currentPrice?: number;
|
||||
change?: number;
|
||||
changeRate?: number;
|
||||
searchResults: DashboardStockSearchItem[];
|
||||
searchHistory: DashboardStockSearchHistoryItem[];
|
||||
searchShellRef: MutableRefObject<HTMLDivElement | null>;
|
||||
@@ -27,16 +33,20 @@ interface TradeSearchSectionProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 트레이드 화면 상단의 검색 입력/결과/히스토리 드롭다운 영역을 렌더링합니다.
|
||||
* @see features/trade/components/TradeContainer.tsx TradeContainer에서 검색 섹션을 분리해 렌더 복잡도를 줄입니다.
|
||||
* @see features/trade/hooks/useTradeSearchPanel.ts 패널 열림/닫힘 및 포커스 핸들러를 전달받습니다.
|
||||
* @description 트레이드 화면 상단의 검색 입력/결과/종목 요약 통합 영역을 렌더링합니다.
|
||||
* @summary UI 흐름: TradeContainer -> TradeSearchSection -> (검색 입력/선택) + (선택 종목 실시간 요약) 반영
|
||||
* @see features/trade/components/TradeContainer.tsx - 검색 상태/선택 종목 실시간 데이터를 전달합니다.
|
||||
*/
|
||||
export function TradeSearchSection({
|
||||
canSearch,
|
||||
isSearchPanelOpen,
|
||||
isSearching,
|
||||
keyword,
|
||||
selectedStock,
|
||||
selectedSymbol,
|
||||
currentPrice,
|
||||
change,
|
||||
changeRate,
|
||||
searchResults,
|
||||
searchHistory,
|
||||
searchShellRef,
|
||||
@@ -50,52 +60,176 @@ export function TradeSearchSection({
|
||||
onClearHistory,
|
||||
}: TradeSearchSectionProps) {
|
||||
return (
|
||||
<div className="z-30 flex-none border-b bg-background/95 px-3 py-2 backdrop-blur-sm dark:border-brand-800/45 dark:bg-brand-900/22 sm:px-4">
|
||||
{/* ========== SEARCH SHELL ========== */}
|
||||
<div
|
||||
ref={searchShellRef}
|
||||
onBlurCapture={onSearchShellBlur}
|
||||
onKeyDownCapture={onSearchShellKeyDown}
|
||||
className="relative mx-auto max-w-2xl"
|
||||
>
|
||||
<StockSearchForm
|
||||
keyword={keyword}
|
||||
onKeywordChange={onKeywordChange}
|
||||
onSubmit={onSearchSubmit}
|
||||
onInputFocus={onSearchFocus}
|
||||
disabled={!canSearch}
|
||||
isLoading={isSearching}
|
||||
/>
|
||||
<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>
|
||||
)}
|
||||
{/* ========== 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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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>
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user