feat: add manual transaction entry UI with modal dialog

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 <noreply@anthropic.com>
This commit is contained in:
머니페니 2026-03-18 22:22:56 +09:00
parent 4ea744ce62
commit 60d2221edc

View File

@ -10,6 +10,23 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { TradingViewChart } from '@/components/charts/trading-view-chart'; import { TradingViewChart } from '@/components/charts/trading-view-chart';
import { DonutChart } from '@/components/charts/donut-chart'; import { DonutChart } from '@/components/charts/donut-chart';
import { Skeleton } from '@/components/ui/skeleton'; 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 { api } from '@/lib/api';
import type { AreaData, Time } from 'lightweight-charts'; import type { AreaData, Time } from 'lightweight-charts';
@ -93,6 +110,17 @@ export default function PortfolioDetailPage() {
const [snapshots, setSnapshots] = useState<SnapshotListItem[]>([]); const [snapshots, setSnapshots] = useState<SnapshotListItem[]>([]);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(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 () => { const fetchPortfolio = useCallback(async () => {
try { try {
setError(null); 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) { if (loading) {
return ( return (
<DashboardLayout> <DashboardLayout>
@ -450,8 +501,11 @@ export default function PortfolioDetailPage() {
{/* Transactions Tab */} {/* Transactions Tab */}
<TabsContent value="transactions"> <TabsContent value="transactions">
<Card> <Card>
<CardHeader> <CardHeader className="flex flex-row items-center justify-between">
<CardTitle> </CardTitle> <CardTitle> </CardTitle>
<Button size="sm" onClick={() => setTxModalOpen(true)}>
</Button>
</CardHeader> </CardHeader>
<CardContent className="p-0"> <CardContent className="p-0">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
@ -632,6 +686,86 @@ export default function PortfolioDetailPage() {
</Tabs> </Tabs>
</> </>
)} )}
{/* Transaction Add Modal */}
<Dialog open={txModalOpen} onOpenChange={setTxModalOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle> </DialogTitle>
<DialogDescription> / .</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="tx-ticker"></Label>
<Input
id="tx-ticker"
placeholder="예: 069500"
value={txForm.ticker}
onChange={(e) => setTxForm({ ...txForm, ticker: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label> </Label>
<Select
value={txForm.tx_type}
onValueChange={(v) => setTxForm({ ...txForm, tx_type: v })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="buy"></SelectItem>
<SelectItem value="sell"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="tx-quantity"></Label>
<Input
id="tx-quantity"
type="number"
min="1"
placeholder="0"
value={txForm.quantity}
onChange={(e) => setTxForm({ ...txForm, quantity: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="tx-price"></Label>
<Input
id="tx-price"
type="number"
min="1"
placeholder="0"
value={txForm.price}
onChange={(e) => setTxForm({ ...txForm, price: e.target.value })}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="tx-memo"> ()</Label>
<Input
id="tx-memo"
placeholder="메모를 입력하세요"
value={txForm.memo}
onChange={(e) => setTxForm({ ...txForm, memo: e.target.value })}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setTxModalOpen(false)}>
</Button>
<Button
onClick={handleAddTransaction}
disabled={txSubmitting || !txForm.ticker || !txForm.quantity || !txForm.price}
>
{txSubmitting ? '저장 중...' : '저장'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DashboardLayout> </DashboardLayout>
); );
} }