From 60d2221edcebd3904ad234bb730a75dadc51c734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A8=B8=EB=8B=88=ED=8E=98=EB=8B=88?= Date: Wed, 18 Mar 2026 22:22:56 +0900 Subject: [PATCH] feat: add manual transaction entry UI with modal dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add "거래 추가" button to the transactions tab with a modal dialog for manually entering buy/sell transactions (ticker, type, quantity, price, memo). Refreshes portfolio and transaction list after successful submission. Co-Authored-By: Claude Opus 4.6 --- frontend/src/app/portfolio/[id]/page.tsx | 136 ++++++++++++++++++++++- 1 file changed, 135 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/portfolio/[id]/page.tsx b/frontend/src/app/portfolio/[id]/page.tsx index 69c5917..13c3fc9 100644 --- a/frontend/src/app/portfolio/[id]/page.tsx +++ b/frontend/src/app/portfolio/[id]/page.tsx @@ -10,6 +10,23 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { TradingViewChart } from '@/components/charts/trading-view-chart'; import { DonutChart } from '@/components/charts/donut-chart'; import { Skeleton } from '@/components/ui/skeleton'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; import { api } from '@/lib/api'; import type { AreaData, Time } from 'lightweight-charts'; @@ -93,6 +110,17 @@ export default function PortfolioDetailPage() { const [snapshots, setSnapshots] = useState([]); const [error, setError] = useState(null); + // Transaction modal state + const [txModalOpen, setTxModalOpen] = useState(false); + const [txSubmitting, setTxSubmitting] = useState(false); + const [txForm, setTxForm] = useState({ + ticker: '', + tx_type: 'buy', + quantity: '', + price: '', + memo: '', + }); + const fetchPortfolio = useCallback(async () => { try { setError(null); @@ -173,6 +201,29 @@ export default function PortfolioDetailPage() { })); }; + const handleAddTransaction = async () => { + if (!txForm.ticker || !txForm.quantity || !txForm.price) return; + setTxSubmitting(true); + try { + await api.post(`/api/portfolios/${portfolioId}/transactions`, { + ticker: txForm.ticker, + tx_type: txForm.tx_type, + quantity: parseInt(txForm.quantity, 10), + price: parseFloat(txForm.price), + executed_at: new Date().toISOString(), + memo: txForm.memo || null, + }); + setTxModalOpen(false); + setTxForm({ ticker: '', tx_type: 'buy', quantity: '', price: '', memo: '' }); + await Promise.all([fetchPortfolio(), fetchTransactions()]); + } catch (err) { + const message = err instanceof Error ? err.message : '거래 추가 실패'; + setError(message); + } finally { + setTxSubmitting(false); + } + }; + if (loading) { return ( @@ -450,8 +501,11 @@ export default function PortfolioDetailPage() { {/* Transactions Tab */} - + 거래 내역 +
@@ -632,6 +686,86 @@ export default function PortfolioDetailPage() { )} + + {/* Transaction Add Modal */} + + + + 거래 추가 + 수동으로 매수/매도 거래를 입력합니다. + +
+
+ + setTxForm({ ...txForm, ticker: e.target.value })} + /> +
+
+ + +
+
+
+ + setTxForm({ ...txForm, quantity: e.target.value })} + /> +
+
+ + setTxForm({ ...txForm, price: e.target.value })} + /> +
+
+
+ + setTxForm({ ...txForm, memo: e.target.value })} + /> +
+
+ + + + +
+
); }