feat(stock): 포트폴리오 매입가/평균단가 분리 + 총 매입 금액 반영
- 기존 카드의 "매입가" → "평균단가" (avg_price) 로 라벨 변경 - 신규 "매입가" (purchase_price) 컬럼 추가. 추가/수정 폼에 입력 필드 노출 (미입력 시 평균단가 값으로 자동 설정) - 브로커별 총 매입 금액은 purchase_price × quantity 합계 기준 - 손익/수익률은 평균단가(avg_price) 기준 유지 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -96,7 +96,7 @@ const PortfolioTab = ({ pf, asset, handleSell, handleSaveSnapshot }) => (
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
평균 매입가 (원)
|
평균단가 (원)
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
@@ -108,6 +108,19 @@ const PortfolioTab = ({ pf, asset, handleSell, handleSaveSnapshot }) => (
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
매입가 (원)
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
value={pf.addForm.purchase_price}
|
||||||
|
onChange={(e) =>
|
||||||
|
pf.setAddForm((p) => ({ ...p, purchase_price: e.target.value }))
|
||||||
|
}
|
||||||
|
placeholder="미입력 시 평균단가로 자동 설정"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
<button
|
<button
|
||||||
className="button primary"
|
className="button primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -435,7 +448,7 @@ const PortfolioTab = ({ pf, asset, handleSell, handleSaveSnapshot }) => (
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
평균매입가
|
평균단가
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
@@ -448,6 +461,20 @@ const PortfolioTab = ({ pf, asset, handleSell, handleSaveSnapshot }) => (
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
매입가
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
value={pf.editForm.purchase_price ?? ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
pf.setEditForm((p) => ({
|
||||||
|
...p,
|
||||||
|
purchase_price: Number(e.target.value),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="pf-edit-actions">
|
<div className="pf-edit-actions">
|
||||||
<button
|
<button
|
||||||
@@ -480,9 +507,13 @@ const PortfolioTab = ({ pf, asset, handleSell, handleSaveSnapshot }) => (
|
|||||||
<strong>{formatNumber(item.quantity)}</strong>
|
<strong>{formatNumber(item.quantity)}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="stock-holdings__metric">
|
<div className="stock-holdings__metric">
|
||||||
<span>매입가</span>
|
<span>평균단가</span>
|
||||||
<strong>{formatNumber(item.avg_price)}</strong>
|
<strong>{formatNumber(item.avg_price)}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="stock-holdings__metric">
|
||||||
|
<span>매입가</span>
|
||||||
|
<strong>{formatNumber(item.purchase_price ?? item.avg_price)}</strong>
|
||||||
|
</div>
|
||||||
<div className="stock-holdings__metric">
|
<div className="stock-holdings__metric">
|
||||||
<span>현재가</span>
|
<span>현재가</span>
|
||||||
<strong className={item.current_price == null ? 'pf-null-price' : ''}>
|
<strong className={item.current_price == null ? 'pf-null-price' : ''}>
|
||||||
|
|||||||
@@ -70,14 +70,19 @@ export default function usePortfolio() {
|
|||||||
}, [brokerGroups]);
|
}, [brokerGroups]);
|
||||||
|
|
||||||
const getBrokerSummary = (items) => {
|
const getBrokerSummary = (items) => {
|
||||||
let totalBuy = 0, totalEvalAmt = 0, hasNullPrice = false;
|
// totalBuy: 요약 표시용 (매입가 purchase_price 기준)
|
||||||
|
// totalCostBasis: 손익 계산용 (평균단가 avg_price 기준)
|
||||||
|
let totalBuy = 0, totalCostBasis = 0, totalEvalAmt = 0, hasNullPrice = false;
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
totalBuy += (item.avg_price ?? 0) * (item.quantity ?? 0);
|
const qty = item.quantity ?? 0;
|
||||||
|
const purchase = item.purchase_price ?? item.avg_price ?? 0;
|
||||||
|
totalBuy += purchase * qty;
|
||||||
|
totalCostBasis += (item.avg_price ?? 0) * qty;
|
||||||
if (item.eval_amount != null) totalEvalAmt += item.eval_amount;
|
if (item.eval_amount != null) totalEvalAmt += item.eval_amount;
|
||||||
else hasNullPrice = true;
|
else hasNullPrice = true;
|
||||||
}
|
}
|
||||||
const totalProfit = totalEvalAmt - totalBuy;
|
const totalProfit = totalEvalAmt - totalCostBasis;
|
||||||
const totalProfitRate = totalBuy > 0 ? (totalProfit / totalBuy) * 100 : 0;
|
const totalProfitRate = totalCostBasis > 0 ? (totalProfit / totalCostBasis) * 100 : 0;
|
||||||
return { totalBuy, totalEval: totalEvalAmt, totalProfit, totalProfitRate, hasNullPrice };
|
return { totalBuy, totalEval: totalEvalAmt, totalProfit, totalProfitRate, hasNullPrice };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -108,6 +113,9 @@ export default function usePortfolio() {
|
|||||||
name: addForm.name.trim(),
|
name: addForm.name.trim(),
|
||||||
quantity: Number(addForm.quantity),
|
quantity: Number(addForm.quantity),
|
||||||
avg_price: Number(addForm.avg_price),
|
avg_price: Number(addForm.avg_price),
|
||||||
|
purchase_price: addForm.purchase_price === '' || addForm.purchase_price == null
|
||||||
|
? Number(addForm.avg_price)
|
||||||
|
: Number(addForm.purchase_price),
|
||||||
});
|
});
|
||||||
setAddForm({ ...emptyPortfolioForm });
|
setAddForm({ ...emptyPortfolioForm });
|
||||||
setAddFormOpen(false);
|
setAddFormOpen(false);
|
||||||
@@ -121,7 +129,13 @@ export default function usePortfolio() {
|
|||||||
|
|
||||||
const handleEditStart = (item) => {
|
const handleEditStart = (item) => {
|
||||||
setEditingId(item.id);
|
setEditingId(item.id);
|
||||||
const data = { quantity: item.quantity, avg_price: item.avg_price, broker: item.broker, name: item.name };
|
const data = {
|
||||||
|
quantity: item.quantity,
|
||||||
|
avg_price: item.avg_price,
|
||||||
|
purchase_price: item.purchase_price ?? item.avg_price,
|
||||||
|
broker: item.broker,
|
||||||
|
name: item.name,
|
||||||
|
};
|
||||||
setEditForm(data);
|
setEditForm(data);
|
||||||
editOrigRef.current = { ...data };
|
editOrigRef.current = { ...data };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ export const emptyPortfolioForm = {
|
|||||||
name: '',
|
name: '',
|
||||||
quantity: '',
|
quantity: '',
|
||||||
avg_price: '',
|
avg_price: '',
|
||||||
|
purchase_price: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ── empty sell-history form ─────────────────────────────────────── */
|
/* ── empty sell-history form ─────────────────────────────────────── */
|
||||||
|
|||||||
Reference in New Issue
Block a user