"use client"; import { useState } from "react"; import type { DashboardHoldingItem } from "@/features/dashboard/types/dashboard.types"; 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/trade/hooks/useOrder"; import { useKisRuntimeStore } from "@/features/settings/store/use-kis-runtime-store"; import type { DashboardOrderSide, DashboardStockItem, } from "@/features/trade/types/trade.types"; import { cn } from "@/lib/utils"; interface OrderFormProps { stock?: DashboardStockItem; matchedHolding?: DashboardHoldingItem | null; } /** * @description 대시보드 주문 패널에서 매수/매도 주문을 입력하고 전송합니다. * @see features/trade/hooks/useOrder.ts - placeOrder 주문 API 호출 * @see features/trade/components/layout/TradeDashboardContent.tsx - orderForm prop으로 DashboardLayout에 전달 */ export function OrderForm({ stock, matchedHolding }: OrderFormProps) { const verifiedCredentials = useKisRuntimeStore( (state) => state.verifiedCredentials, ); const { placeOrder, isLoading, error } = useOrder(); // ========== FORM STATE ========== const [price, setPrice] = useState( stock?.currentPrice.toString() || "", ); const [quantity, setQuantity] = useState(""); const [activeTab, setActiveTab] = useState<"buy" | "sell">("buy"); // ========== ORDER HANDLER ========== /** * UI 흐름: 매수하기/매도하기 버튼 클릭 -> handleOrder -> placeOrder API 호출 -> 주문번호 반환 -> alert */ const handleOrder = async (side: DashboardOrderSide) => { if (!stock || !verifiedCredentials) return; const priceNum = parseInt(price.replace(/,/g, ""), 10); const qtyNum = parseInt(quantity.replace(/,/g, ""), 10); if (Number.isNaN(priceNum) || priceNum <= 0) { alert("가격을 올바르게 입력해 주세요."); return; } if (Number.isNaN(qtyNum) || qtyNum <= 0) { alert("수량을 올바르게 입력해 주세요."); return; } if (!verifiedCredentials.accountNo) { alert("계좌번호가 없습니다. 설정에서 계좌번호를 입력해 주세요."); return; } const response = await placeOrder( { symbol: stock.symbol, side, orderType: "limit", price: priceNum, quantity: qtyNum, accountNo: verifiedCredentials.accountNo, accountProductCode: "01", }, verifiedCredentials, ); if (response?.orderNo) { alert(`주문 전송 완료: ${response.orderNo}`); setQuantity(""); } }; const totalPrice = parseInt(price.replace(/,/g, "") || "0", 10) * parseInt(quantity.replace(/,/g, "") || "0", 10); const setPercent = (pct: string) => { // TODO: 계좌 잔고 연동 시 퍼센트 자동 계산으로 교체 console.log("Percent clicked:", pct); }; const isMarketDataAvailable = Boolean(stock); const isBuy = activeTab === "buy"; return (
setActiveTab(value as "buy" | "sell")} className="flex h-full w-full flex-col" > {/* ========== ORDER SIDE TABS ========== */} 매수 매도 {/* ========== CURRENT PRICE INFO ========== */} {stock && (
현재가 {stock.currentPrice.toLocaleString()}원
)} {/* ========== BUY TAB ========== */}
{/* ========== SELL TAB ========== */}
); } /** * @description 주문 입력 영역(가격/수량/총액)을 렌더링합니다. * @see features/trade/components/order/OrderForm.tsx - OrderForm 매수/매도 탭에서 공용 호출 */ function OrderInputs({ type, price, setPrice, quantity, setQuantity, totalPrice, disabled, hasError, errorMessage, }: { type: "buy" | "sell"; price: string; setPrice: (v: string) => void; quantity: string; setQuantity: (v: string) => void; totalPrice: number; disabled: boolean; hasError: boolean; errorMessage: string | null; }) { const labelClass = "text-xs font-medium text-foreground/80 dark:text-brand-100/80 min-w-[56px]"; const inputClass = "col-span-3 h-9 text-right font-mono text-sm dark:border-brand-700/55 dark:bg-black/25 dark:text-brand-100"; return (
{/* 주문 가능 */}
주문가능 - {type === "buy" ? "KRW" : "주"}
{hasError && (
{errorMessage}
)} {/* 가격 입력 */}
{type === "buy" ? "매수가격" : "매도가격"} setPrice(e.target.value)} disabled={disabled} />
{/* 수량 입력 */}
주문수량 setQuantity(e.target.value)} disabled={disabled} />
{/* 총액 */}
주문총액 0 ? `${totalPrice.toLocaleString()}원` : ""} readOnly disabled={disabled} placeholder="0원" />
); } /** * @description 주문 비율(10/25/50/100%) 단축 버튼을 표시합니다. * @see features/trade/components/order/OrderForm.tsx - OrderForm setPercent 이벤트 처리 */ function PercentButtons({ onSelect }: { onSelect: (pct: string) => void }) { return (
{["10%", "25%", "50%", "100%"].map((pct) => ( ))}
); } /** * @description 선택 종목이 보유 상태일 때 주문 패널 하단에 보유 요약을 표시합니다. * @summary UI 흐름: TradeContainer(matchedHolding 계산) -> TradeDashboardContent -> OrderForm -> HoldingInfoPanel 렌더링 * @see features/trade/components/TradeContainer.tsx - selectedSymbol 기준으로 보유종목 매칭 값을 전달합니다. */ function HoldingInfoPanel({ holding, }: { holding?: DashboardHoldingItem | null; }) { if (!holding) return null; const profitToneClass = getHoldingProfitToneClass(holding.profitLoss); return (

보유 정보

= 0 ? "+" : ""}${holding.profitLoss.toLocaleString("ko-KR")}원`} toneClass={profitToneClass} /> = 0 ? "+" : ""}${holding.profitRate.toFixed(2)}%`} toneClass={profitToneClass} />
); } /** * @description 보유정보 카드의 단일 라벨/값 행을 렌더링합니다. * @see features/trade/components/order/OrderForm.tsx HoldingInfoPanel */ function HoldingInfoRow({ label, value, toneClass, }: { label: string; value: string; toneClass?: string; }) { return (
{label} {value}
); } /** * @description 보유 손익 부호에 따른 색상 클래스를 반환합니다. * @see features/trade/components/order/OrderForm.tsx HoldingInfoPanel */ function getHoldingProfitToneClass(value: number) { if (value > 0) return "text-red-500 dark:text-red-400"; if (value < 0) return "text-blue-600 dark:text-blue-400"; return "text-foreground dark:text-brand-50"; }