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 { 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user