refactor(portfolio): 예수금 패널 CashPanel 컴포넌트 추출

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHXzpsZQxKG9hQmNRfZjRS
This commit is contained in:
2026-07-10 10:20:56 +09:00
parent 6f98c0a84d
commit ca5adbf4a4
2 changed files with 134 additions and 125 deletions

View File

@@ -5,6 +5,7 @@ import PriceSessionBadge from './portfolio/PriceSessionBadge';
import AddHoldingForm from './portfolio/AddHoldingForm'; import AddHoldingForm from './portfolio/AddHoldingForm';
import PortfolioSummary from './portfolio/PortfolioSummary'; import PortfolioSummary from './portfolio/PortfolioSummary';
import AssetHistoryChart from './portfolio/AssetHistoryChart'; import AssetHistoryChart from './portfolio/AssetHistoryChart';
import CashPanel from './portfolio/CashPanel';
const PortfolioTab = ({ pf, asset, handleSell, handleSaveSnapshot }) => ( const PortfolioTab = ({ pf, asset, handleSell, handleSaveSnapshot }) => (
<> <>
@@ -53,131 +54,7 @@ const PortfolioTab = ({ pf, asset, handleSell, handleSaveSnapshot }) => (
</section> </section>
{/* 예수금 패널 */} {/* 예수금 패널 */}
<section className="stock-panel stock-panel--wide"> <CashPanel pf={pf} />
<div className="stock-panel__head">
<div>
<p className="stock-panel__eyebrow">예수금 관리</p>
<h3>증권사별 예수금</h3>
<p className="stock-panel__sub">
증권사별 예수금을 입력하면 자산에 자동 반영됩니다.
</p>
</div>
</div>
{pf.cashList.length > 0 && (
<div className="pf-cash-table">
{pf.cashList.map((item) => {
const isEditing = pf.cashEditingBroker === item.broker;
return (
<div key={item.id ?? item.broker} className="pf-cash-row">
<span className="pf-cash-broker">{item.broker}</span>
{isEditing ? (
<input
className="pf-cash-edit-input"
type="number"
min={0}
step={1}
value={pf.cashEditingValue}
onChange={(e) => pf.setCashEditingValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') pf.handleCashInlineSave(item.broker);
if (e.key === 'Escape') pf.handleCashInlineCancel();
}}
autoFocus
/>
) : (
<strong className="pf-cash-amount">
{formatNumber(item.cash)}
</strong>
)}
<span className="pf-cash-date">
{item.updated_at
? new Date(item.updated_at).toLocaleDateString('ko-KR')
: ''}
</span>
{isEditing ? (
<>
<button
className="button primary small"
onClick={() => pf.handleCashInlineSave(item.broker)}
disabled={pf.cashEditSaving}
>
{pf.cashEditSaving ? '저장 중' : '저장'}
</button>
<button
className="button ghost small"
onClick={pf.handleCashInlineCancel}
disabled={pf.cashEditSaving}
>
취소
</button>
</>
) : (
<>
<button
className="button ghost small"
onClick={() => pf.handleCashInlineEdit(item)}
title="수정"
>
</button>
<button
className="button ghost small pf-btn-danger"
onClick={() => pf.handleCashDelete(item.broker)}
title="삭제"
>
🗑
</button>
</>
)}
</div>
);
})}
</div>
)}
{pf.cashList.length === 0 && (
<p className="stock-empty" style={{ fontSize: 13 }}>
등록된 예수금이 없습니다.
</p>
)}
<form className="pf-cash-form" onSubmit={pf.handleCashSave}>
<label>
증권사명
<input
type="text"
value={pf.cashForm.broker}
onChange={(e) =>
pf.setCashForm((p) => ({ ...p, broker: e.target.value }))
}
placeholder="KB증권"
required
/>
</label>
<label>
예수금 ()
<input
type="number"
min={0}
step={1}
value={pf.cashForm.cash}
onChange={(e) =>
pf.setCashForm((p) => ({ ...p, cash: e.target.value }))
}
placeholder="1500000"
required
/>
</label>
<button
className="button primary"
type="submit"
disabled={pf.cashSaving}
>
{pf.cashSaving ? '저장 중...' : '저장'}
</button>
{pf.cashError && <p className="stock-error">{pf.cashError}</p>}
</form>
</section>
{/* Broker cards stacked */} {/* Broker cards stacked */}
{pf.brokerGroups.map(([broker, items]) => { {pf.brokerGroups.map(([broker, items]) => {

View File

@@ -0,0 +1,132 @@
import React from 'react';
import { formatNumber } from '../../stockUtils';
const CashPanel = ({ pf }) => (
<section className="stock-panel stock-panel--wide">
<div className="stock-panel__head">
<div>
<p className="stock-panel__eyebrow">예수금 관리</p>
<h3>증권사별 예수금</h3>
<p className="stock-panel__sub">
증권사별 예수금을 입력하면 자산에 자동 반영됩니다.
</p>
</div>
</div>
{pf.cashList.length > 0 && (
<div className="pf-cash-table">
{pf.cashList.map((item) => {
const isEditing = pf.cashEditingBroker === item.broker;
return (
<div key={item.id ?? item.broker} className="pf-cash-row">
<span className="pf-cash-broker">{item.broker}</span>
{isEditing ? (
<input
className="pf-cash-edit-input"
type="number"
min={0}
step={1}
value={pf.cashEditingValue}
onChange={(e) => pf.setCashEditingValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') pf.handleCashInlineSave(item.broker);
if (e.key === 'Escape') pf.handleCashInlineCancel();
}}
autoFocus
/>
) : (
<strong className="pf-cash-amount">
{formatNumber(item.cash)}
</strong>
)}
<span className="pf-cash-date">
{item.updated_at
? new Date(item.updated_at).toLocaleDateString('ko-KR')
: ''}
</span>
{isEditing ? (
<>
<button
className="button primary small"
onClick={() => pf.handleCashInlineSave(item.broker)}
disabled={pf.cashEditSaving}
>
{pf.cashEditSaving ? '저장 중' : '저장'}
</button>
<button
className="button ghost small"
onClick={pf.handleCashInlineCancel}
disabled={pf.cashEditSaving}
>
취소
</button>
</>
) : (
<>
<button
className="button ghost small"
onClick={() => pf.handleCashInlineEdit(item)}
title="수정"
>
</button>
<button
className="button ghost small pf-btn-danger"
onClick={() => pf.handleCashDelete(item.broker)}
title="삭제"
>
🗑
</button>
</>
)}
</div>
);
})}
</div>
)}
{pf.cashList.length === 0 && (
<p className="stock-empty" style={{ fontSize: 13 }}>
등록된 예수금이 없습니다.
</p>
)}
<form className="pf-cash-form" onSubmit={pf.handleCashSave}>
<label>
증권사명
<input
type="text"
value={pf.cashForm.broker}
onChange={(e) =>
pf.setCashForm((p) => ({ ...p, broker: e.target.value }))
}
placeholder="KB증권"
required
/>
</label>
<label>
예수금 ()
<input
type="number"
min={0}
step={1}
value={pf.cashForm.cash}
onChange={(e) =>
pf.setCashForm((p) => ({ ...p, cash: e.target.value }))
}
placeholder="1500000"
required
/>
</label>
<button
className="button primary"
type="submit"
disabled={pf.cashSaving}
>
{pf.cashSaving ? '저장 중...' : '저장'}
</button>
{pf.cashError && <p className="stock-error">{pf.cashError}</p>}
</form>
</section>
);
export default CashPanel;