대시보드 중간 커밋
This commit is contained in:
249
features/dashboard/components/order/OrderForm.tsx
Normal file
249
features/dashboard/components/order/OrderForm.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import type {
|
||||
DashboardStockItem,
|
||||
DashboardOrderSide,
|
||||
} from "@/features/dashboard/types/dashboard.types";
|
||||
import { useOrder } from "@/features/dashboard/hooks/useOrder";
|
||||
import { useKisRuntimeStore } from "@/features/dashboard/store/use-kis-runtime-store";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
interface OrderFormProps {
|
||||
stock?: DashboardStockItem;
|
||||
}
|
||||
|
||||
export function OrderForm({ stock }: OrderFormProps) {
|
||||
const verifiedCredentials = useKisRuntimeStore(
|
||||
(state) => state.verifiedCredentials,
|
||||
);
|
||||
|
||||
const { placeOrder, isLoading, error } = useOrder();
|
||||
|
||||
// Form State
|
||||
// Initial price set from stock current price if available, relying on component remount (key) for updates
|
||||
const [price, setPrice] = useState<string>(
|
||||
stock?.currentPrice.toString() || "",
|
||||
);
|
||||
const [quantity, setQuantity] = useState<string>("");
|
||||
const [activeTab, setActiveTab] = useState("buy");
|
||||
|
||||
const handleOrder = async (side: DashboardOrderSide) => {
|
||||
if (!stock || !verifiedCredentials) return;
|
||||
|
||||
const priceNum = parseInt(price.replace(/,/g, ""), 10);
|
||||
const qtyNum = parseInt(quantity.replace(/,/g, ""), 10);
|
||||
|
||||
if (isNaN(priceNum) || priceNum <= 0) {
|
||||
alert("가격을 올바르게 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
if (isNaN(qtyNum) || qtyNum <= 0) {
|
||||
alert("수량을 올바르게 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!verifiedCredentials.accountNo) {
|
||||
alert(
|
||||
"계좌번호가 설정되지 않았습니다. 설정에서 계좌번호를 입력해주세요.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await placeOrder(
|
||||
{
|
||||
symbol: stock.symbol,
|
||||
side: side,
|
||||
orderType: "limit", // 지정가 고정
|
||||
price: priceNum,
|
||||
quantity: qtyNum,
|
||||
accountNo: verifiedCredentials.accountNo,
|
||||
accountProductCode: "01", // Default to '01' (위탁)
|
||||
},
|
||||
verifiedCredentials,
|
||||
);
|
||||
|
||||
if (response && response.orderNo) {
|
||||
alert(`주문 전송 완료! 주문번호: ${response.orderNo}`);
|
||||
setQuantity("");
|
||||
}
|
||||
};
|
||||
|
||||
const totalPrice =
|
||||
parseInt(price.replace(/,/g, "") || "0", 10) *
|
||||
parseInt(quantity.replace(/,/g, "") || "0", 10);
|
||||
|
||||
const setPercent = (pct: string) => {
|
||||
// Placeholder logic for percent click
|
||||
console.log("Percent clicked:", pct);
|
||||
};
|
||||
|
||||
const isMarketDataAvailable = !!stock;
|
||||
|
||||
return (
|
||||
<div className="h-full bg-background p-4 border-l border-border">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
className="w-full h-full flex flex-col"
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-2 mb-4">
|
||||
<TabsTrigger
|
||||
value="buy"
|
||||
className="data-[state=active]:bg-red-600 data-[state=active]:text-white transition-colors"
|
||||
>
|
||||
매수
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="sell"
|
||||
className="data-[state=active]:bg-blue-600 data-[state=active]:text-white transition-colors"
|
||||
>
|
||||
매도
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent
|
||||
value="buy"
|
||||
className="flex-1 flex flex-col space-y-4 data-[state=inactive]:hidden"
|
||||
>
|
||||
<OrderInputs
|
||||
type="buy"
|
||||
price={price}
|
||||
setPrice={setPrice}
|
||||
quantity={quantity}
|
||||
setQuantity={setQuantity}
|
||||
totalPrice={totalPrice}
|
||||
disabled={!isMarketDataAvailable}
|
||||
hasError={!!error}
|
||||
errorMessage={error}
|
||||
/>
|
||||
|
||||
<PercentButtons onSelect={setPercent} />
|
||||
|
||||
<Button
|
||||
className="w-full bg-red-600 hover:bg-red-700 mt-auto text-lg h-12"
|
||||
disabled={isLoading || !isMarketDataAvailable}
|
||||
onClick={() => handleOrder("buy")}
|
||||
>
|
||||
{isLoading ? <Loader2 className="animate-spin mr-2" /> : "매수하기"}
|
||||
</Button>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent
|
||||
value="sell"
|
||||
className="flex-1 flex flex-col space-y-4 data-[state=inactive]:hidden"
|
||||
>
|
||||
<OrderInputs
|
||||
type="sell"
|
||||
price={price}
|
||||
setPrice={setPrice}
|
||||
quantity={quantity}
|
||||
setQuantity={setQuantity}
|
||||
totalPrice={totalPrice}
|
||||
disabled={!isMarketDataAvailable}
|
||||
hasError={!!error}
|
||||
errorMessage={error}
|
||||
/>
|
||||
|
||||
<PercentButtons onSelect={setPercent} />
|
||||
|
||||
<Button
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 mt-auto text-lg h-12"
|
||||
disabled={isLoading || !isMarketDataAvailable}
|
||||
onClick={() => handleOrder("sell")}
|
||||
>
|
||||
{isLoading ? <Loader2 className="animate-spin mr-2" /> : "매도하기"}
|
||||
</Button>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>주문가능</span>
|
||||
<span>- {type === "buy" ? "KRW" : "주"}</span>
|
||||
</div>
|
||||
|
||||
{hasError && (
|
||||
<div className="p-2 bg-destructive/10 text-destructive text-xs rounded break-keep">
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-4 gap-2 items-center">
|
||||
<span className="text-sm font-medium">
|
||||
{type === "buy" ? "매수가격" : "매도가격"}
|
||||
</span>
|
||||
<Input
|
||||
className="col-span-3 text-right font-mono"
|
||||
placeholder="0"
|
||||
value={price}
|
||||
onChange={(e) => setPrice(e.target.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-2 items-center">
|
||||
<span className="text-sm font-medium">주문수량</span>
|
||||
<Input
|
||||
className="col-span-3 text-right font-mono"
|
||||
placeholder="0"
|
||||
value={quantity}
|
||||
onChange={(e) => setQuantity(e.target.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-2 items-center">
|
||||
<span className="text-sm font-medium">주문총액</span>
|
||||
<Input
|
||||
className="col-span-3 text-right font-mono bg-muted/50"
|
||||
value={totalPrice.toLocaleString()}
|
||||
readOnly
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PercentButtons({ onSelect }: { onSelect: (pct: string) => void }) {
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-2 mt-2">
|
||||
{["10%", "25%", "50%", "100%"].map((pct) => (
|
||||
<Button
|
||||
key={pct}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-xs"
|
||||
onClick={() => onSelect(pct)}
|
||||
>
|
||||
{pct}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user