diff --git a/src/pages/portfoliio/PortfolioPage.tsx b/src/pages/portfoliio/PortfolioPage.tsx index 6580b75..30c9dd7 100644 --- a/src/pages/portfoliio/PortfolioPage.tsx +++ b/src/pages/portfoliio/PortfolioPage.tsx @@ -1,6 +1,7 @@ import React from "react"; -import PortfolioHeader from "@/pages/portfoliio/components/PortfolioHeader"; +import PortfolioHeader from "@/pages/portfoliio/components/PortfolioHeader"; import PortfolioSummary from "@/pages/portfoliio/components/PortfolioSummary"; +import TransactionHistory from "@/pages/portfoliio/components/Transaction/TransactionHistory"; const PortfolioPage = () => { return ( @@ -8,6 +9,7 @@ const PortfolioPage = () => {
+
); diff --git a/src/pages/portfoliio/components/PortfolioSummary.tsx b/src/pages/portfoliio/components/PortfolioSummary.tsx index d919268..aaa1556 100644 --- a/src/pages/portfoliio/components/PortfolioSummary.tsx +++ b/src/pages/portfoliio/components/PortfolioSummary.tsx @@ -23,7 +23,7 @@ const PortfolioSummary = () => { -
+
+ + {open && ( +
+
+ 종목 + +
+
+ {categories.map((category) => ( + + ))} +
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/pages/portfoliio/components/Transaction/TransactionHistory.tsx b/src/pages/portfoliio/components/Transaction/TransactionHistory.tsx new file mode 100644 index 0000000..c552e30 --- /dev/null +++ b/src/pages/portfoliio/components/Transaction/TransactionHistory.tsx @@ -0,0 +1,186 @@ +import React, { useState, useMemo } from "react"; +import { ChevronDown, ChevronUp } from "lucide-react"; +import { cn } from "@/lib/utils"; +import TransactionTable from "./TransactionTable"; +import TransactionFilters from "./TransactionFilters"; +import { + Transaction, + SortKey, + SortDir, + Category, + OrderType, + TradeType, +} from "./types/transaction"; + +/** ---- Mock Data (예시) ---- */ +const mockTx: Transaction[] = [ + { + id: "T-2025-01-01-001", + date: "2025-01-01", + time: "00:00:00", + category: "AI 딥러닝 트레이딩", + division: "시장가", + type: "체결", + quantity: 1, + price: 850_000, + pnl: 533_840, + side: "매도", + }, + { + id: "T-2025-01-01-002", + date: "2025-01-01", + time: "00:10:00", + category: "AI 딥러닝 트레이딩", + division: "지정가", + type: "체결", + quantity: 1, + price: 850_000, + pnl: 533_840, + side: "매도", + }, + { + id: "T-2025-01-01-003", + date: "2025-01-01", + time: "00:24:01", + category: "AI 딥러닝 트레이딩", + division: "시장가", + type: "체결", + quantity: 1, + price: 850_000, + pnl: -12_000, + side: "매수", + }, + { + id: "T-2025-01-01-004", + date: "2025-01-01", + time: "00:00:8", + category: "AI 딥러닝 트레이딩", + division: "시장가", + type: "미체결", + quantity: 1, + price: 850_000, + pnl: 300, + side: "매수", + }, + { + id: "T-2025-01-01-005", + date: "2025-01-01", + time: "00:00:00", + category: "AI 딥러닝 트레이딩", + division: "시장가", + type: "체결", + quantity: 1, + price: 850_000, + pnl: 533_840, + side: "매도", + }, +]; + +export default function TransactionHistory() { + const [data] = useState(mockTx); + const [isExpanded, setIsExpanded] = useState(false); + const [selectedFilter, setSelectedFilter] = useState<"stock" | "period">("stock"); + + const [fCategory, setFCategory] = useState([]); + const [sortKey, setSortKey] = useState("date"); + const [sortDir, setSortDir] = useState("desc"); + const [page, setPage] = useState(1); + const PAGE_SIZE = 10; + + const filtered = useMemo(() => { + if (selectedFilter === "stock" && fCategory.length > 0) { + return data.filter((d) => fCategory.includes(d.category)); + } + return data; + }, [data, fCategory, selectedFilter]); + + const sorted = useMemo(() => { + const arr = [...filtered]; + arr.sort((a, b) => { + const va = String(a[sortKey]); + const vb = String(b[sortKey]); + if (va === vb) return 0; + return sortDir === "asc" ? (va < vb ? -1 : 1) : va > vb ? -1 : 1; + }); + return arr; + }, [filtered, sortKey, sortDir]); + + const total = sorted.length; + const paged = sorted.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE); + + const toggleLabel = isExpanded ? "간단히 보기" : "자세히 보기"; + const Icon = isExpanded ? ChevronUp : ChevronDown; + + const toggleSort = (key: SortKey) => { + if (sortKey !== key) { + setSortKey(key); + setSortDir("asc"); + } else { + setSortDir((d) => (d === "asc" ? "desc" : "asc")); + } + }; + + const handleCategoryFilterChange = (type: "category", value: Category[]) => { + setFCategory(value); + }; + + const handleTopFilterChange = (filter: "stock" | "period") => { + setSelectedFilter(filter); + if (filter === "period") { + setFCategory([]); + } + }; + + return ( +
+

거래내역

+ +
+
+

총 {total}회

+
+ +
+
+ + + {isExpanded && ( + <> +
+ handleTopFilterChange("stock")} + selectedFilter={selectedFilter} + /> + +
+ + + )} +
+
+ ); +} \ No newline at end of file diff --git a/src/pages/portfoliio/components/Transaction/TransactionTable.tsx b/src/pages/portfoliio/components/Transaction/TransactionTable.tsx new file mode 100644 index 0000000..dc54978 --- /dev/null +++ b/src/pages/portfoliio/components/Transaction/TransactionTable.tsx @@ -0,0 +1,108 @@ +import React from "react"; +import { cn } from "@/lib/utils"; +import { Transaction, SortKey, SortDir } from "./types/transaction"; +import { ChevronDown, ChevronUp } from "lucide-react"; + +interface TransactionTableProps { + data: Transaction[]; + sortKey: SortKey; + sortDir: SortDir; + onSort: (key: SortKey) => void; +} + +// ₩ 기호와 숫자를 통일된 형식으로 변환 +const nfmt = (n: number) => `₩ ${n.toLocaleString()}`; + +export default function TransactionTable({ + data, + sortKey, + sortDir, + onSort, +}: TransactionTableProps) { + return ( + + + + {[ + ["date", "날짜"] as const, + ["time", "시간"] as const, + ["category", "종목"] as const, + ["side", "구분"] as const, + ["type", "체결여부"] as const, + ["division", "유형"] as const, + ["quantity", "주문량"] as const, + ["price", "체결가"] as const, + ["pnl", "손익"] as const, + ].map(([key, label]) => ( + + ))} + + + + {data.length === 0 ? ( + + + + ) : ( + data.map((tx) => ( + + + + + + + + + + + + )) + )} + +
onSort(key as SortKey)} + className={cn( + "px-4 py-3 text-left select-none cursor-pointer", + sortKey === key ? "text-black" : "" + )} + > +
+ {label} + {sortKey === key && ( + + {sortDir === "asc" ? "▲" : "▼"} + + )} +
+
+ 조건에 맞는 거래 내역이 없습니다. +
{tx.date}{tx.time}{tx.category} + {tx.side} + + + {tx.type} + + {tx.division}{tx.quantity.toLocaleString()}₩ {tx.price.toLocaleString()}= 0 ? "text-system-positive" : "text-system-negative" + )} + > + {/* 손익(pnl) 렌더링 로직 수정 */} + {tx.pnl >= 0 ? `+${nfmt(tx.pnl)}` : `-${nfmt(Math.abs(tx.pnl))}`} +
+ ); +} \ No newline at end of file diff --git a/src/pages/portfoliio/components/Transaction/types/transaction.ts b/src/pages/portfoliio/components/Transaction/types/transaction.ts new file mode 100644 index 0000000..4cf74a9 --- /dev/null +++ b/src/pages/portfoliio/components/Transaction/types/transaction.ts @@ -0,0 +1,25 @@ +// TransactionHistory 컴포넌트에서 사용되는 모든 타입 정의 +export type Side = "매수" | "매도"; +export type OrderType = "시장가" | "지정가"; +export type TradeType = "체결" | "미체결"; +export type Category = "AI 딥러닝 트레이딩" | "알고리즘 트레이딩" | "Test 트레이딩"; + +export interface Transaction { + id: string; + date: string; // YYYY-MM-DD + time: string; // HH:mm:ss + category: Category; // 트레이딩 종류 + side: Side; // 매수/매도 + type: TradeType; // 체결여부 + division: OrderType; // 주문방식 + quantity: number; // 주문량 + price: number; // 체결가(원) + pnl: number; // 순익(원) +} + +// 정렬에 사용되는 키와 방향 +export type SortKey = keyof Pick< + Transaction, + "date" | "time" | "category" | "division" | "type" | "quantity" | "price" | "pnl" +>; +export type SortDir = "asc" | "desc"; \ No newline at end of file diff --git a/src/pages/portfoliio/components/common/DropFilter.tsx b/src/pages/portfoliio/components/common/DropFilter.tsx new file mode 100644 index 0000000..bec942f --- /dev/null +++ b/src/pages/portfoliio/components/common/DropFilter.tsx @@ -0,0 +1,69 @@ +import React, { useState } from "react"; +import { ChevronDown, ChevronUp, X } from "lucide-react"; +import { cn } from "@/lib/utils"; // 경로는 프로젝트 구조에 맞게 수정 + +interface DropFilterProps { + label: string; + value?: T; + options: T[]; + onChange: (v?: T) => void; +} + +export default function DropFilter({ + label, + value, + options, + onChange, +}: DropFilterProps) { + const [open, setOpen] = useState(false); + return ( +
+ + + {open && ( +
+
+ {label} + +
+
+ {options.map((op) => ( + + ))} +
+
+ )} +
+ ); +} \ No newline at end of file