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:
parent
4ea744ce62
commit
60d2221edc
@ -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<SnapshotListItem[]>([]);
|
||||
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 () => {
|
||||
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 (
|
||||
<DashboardLayout>
|
||||
@ -450,8 +501,11 @@ export default function PortfolioDetailPage() {
|
||||
{/* Transactions Tab */}
|
||||
<TabsContent value="transactions">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>거래 내역</CardTitle>
|
||||
<Button size="sm" onClick={() => setTxModalOpen(true)}>
|
||||
거래 추가
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<div className="overflow-x-auto">
|
||||
@ -632,6 +686,86 @@ export default function PortfolioDetailPage() {
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user