8개 커스텀 훅으로 state/handler 로직 추출: - usePortfolio: 포트폴리오 CRUD, 예수금, 브로커 그룹 - useSellHistory: 매도 내역 CRUD, 드로어/폼 상태 - useAiCoach: AI 코치 분석 + 캐시 - useAssetHistory: 자산 추이 차트 데이터 - useMarketContext: VIX/F&G/국채/WTI 시장 데이터 - useAiBalance: AI 모의투자 잔고, 수동 주문 - useReportData: 리포트 정렬, 차트, 집중도 분석 - useAdvisor: 어드바이저 프롬프트 빌더 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
112 lines
4.6 KiB
JavaScript
112 lines
4.6 KiB
JavaScript
import { useState, useMemo } from 'react';
|
|
import { toNumeric } from '../stockUtils';
|
|
|
|
export default function useReportData({ portfolioHoldings, portfolioSummary, brokerGroups, getBrokerSummary }) {
|
|
const [reportSortField, setReportSortField] = useState('profit_rate');
|
|
const [reportSortDir, setReportSortDir] = useState('desc');
|
|
|
|
const handleReportSort = (field) => {
|
|
if (reportSortField === field) {
|
|
setReportSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
|
|
} else {
|
|
setReportSortField(field);
|
|
setReportSortDir('desc');
|
|
}
|
|
};
|
|
|
|
const brokerPieData = useMemo(() =>
|
|
brokerGroups
|
|
.map(([broker, items]) => ({ name: broker, value: getBrokerSummary(items).totalEval }))
|
|
.filter((d) => d.value > 0),
|
|
[brokerGroups, getBrokerSummary]
|
|
);
|
|
|
|
const profitBarData = useMemo(() =>
|
|
portfolioHoldings
|
|
.filter((item) => item.profit_rate != null)
|
|
.map((item) => ({
|
|
name: item.ticker ?? (item.name ?? 'N/A').slice(0, 5),
|
|
fullName: item.name ?? item.ticker ?? 'N/A',
|
|
rate: toNumeric(item.profit_rate) ?? 0,
|
|
}))
|
|
.sort((a, b) => b.rate - a.rate),
|
|
[portfolioHoldings]
|
|
);
|
|
|
|
const maxAbsRate = useMemo(() =>
|
|
Math.max(1, ...portfolioHoldings.map((h) => Math.abs(toNumeric(h.profit_rate) ?? 0))),
|
|
[portfolioHoldings]
|
|
);
|
|
|
|
const brokerConcentration = useMemo(() => {
|
|
const totalEval = toNumeric(portfolioSummary.total_eval);
|
|
if (!totalEval || totalEval === 0) return [];
|
|
return brokerGroups
|
|
.map(([broker, items]) => {
|
|
const { totalEval: brokerEval } = getBrokerSummary(items);
|
|
const ratio = Math.round((brokerEval / totalEval) * 1000) / 10;
|
|
return { broker, eval: brokerEval, ratio };
|
|
})
|
|
.sort((a, b) => b.ratio - a.ratio);
|
|
}, [brokerGroups, portfolioSummary.total_eval, getBrokerSummary]);
|
|
|
|
const stockConcentration = useMemo(() => {
|
|
const totalEval = toNumeric(portfolioSummary.total_eval);
|
|
if (!totalEval || totalEval === 0) return [];
|
|
return portfolioHoldings
|
|
.map((item) => {
|
|
const evalAmt = item.eval_amount != null
|
|
? toNumeric(item.eval_amount)
|
|
: (item.current_price != null && item.quantity != null)
|
|
? toNumeric(item.current_price) * toNumeric(item.quantity)
|
|
: null;
|
|
if (!evalAmt) return null;
|
|
return {
|
|
name: item.name ?? item.ticker ?? 'N/A',
|
|
ticker: item.ticker ?? '',
|
|
eval: evalAmt,
|
|
ratio: Math.round((evalAmt / totalEval) * 1000) / 10,
|
|
};
|
|
})
|
|
.filter(Boolean)
|
|
.sort((a, b) => b.ratio - a.ratio)
|
|
.slice(0, 5);
|
|
}, [portfolioHoldings, portfolioSummary.total_eval]);
|
|
|
|
const sortedHoldings = useMemo(() => {
|
|
const getVal = (item) => {
|
|
switch (reportSortField) {
|
|
case 'profit_rate': return toNumeric(item.profit_rate) ?? -Infinity;
|
|
case 'profit_amount': return toNumeric(item.profit_amount) ?? -Infinity;
|
|
case 'eval_amount': {
|
|
const ea = toNumeric(item.eval_amount);
|
|
if (ea != null) return ea;
|
|
const cp = toNumeric(item.current_price);
|
|
const qty = toNumeric(item.quantity);
|
|
return cp != null && qty != null ? cp * qty : -Infinity;
|
|
}
|
|
default: return 0;
|
|
}
|
|
};
|
|
return [...portfolioHoldings].sort((a, b) => {
|
|
if (reportSortField === 'name')
|
|
return reportSortDir === 'asc'
|
|
? (a.name ?? '').localeCompare(b.name ?? '')
|
|
: (b.name ?? '').localeCompare(a.name ?? '');
|
|
if (reportSortField === 'broker')
|
|
return reportSortDir === 'asc'
|
|
? (a.broker ?? '').localeCompare(b.broker ?? '')
|
|
: (b.broker ?? '').localeCompare(a.broker ?? '');
|
|
const av = getVal(a);
|
|
const bv = getVal(b);
|
|
return reportSortDir === 'asc' ? av - bv : bv - av;
|
|
});
|
|
}, [portfolioHoldings, reportSortField, reportSortDir]);
|
|
|
|
return {
|
|
reportSortField, reportSortDir, handleReportSort,
|
|
brokerPieData, profitBarData, maxAbsRate,
|
|
brokerConcentration, stockConcentration, sortedHoldings,
|
|
};
|
|
}
|