StockTrade 컴포넌트 훅 분리 (Phase 4): 2,788→1,932줄

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>
This commit is contained in:
2026-04-03 07:31:10 +09:00
parent 314702cb66
commit 1b16b40251
10 changed files with 1341 additions and 1306 deletions

View File

@@ -0,0 +1,125 @@
/* ── helpers ─────────────────────────────────────────────────────── */
export const formatNumber = (value) => {
if (value === null || value === undefined || value === '') return '-';
const numeric = Number(value);
if (Number.isNaN(numeric)) return value;
return new Intl.NumberFormat('ko-KR').format(numeric);
};
export const formatPercent = (value) => {
if (value === null || value === undefined || value === '') return '-';
if (typeof value === 'string' && value.includes('%')) return value;
const numeric = Number(value);
if (Number.isNaN(numeric)) return value;
return `${numeric >= 0 ? '+' : ''}${numeric.toFixed(2)}%`;
};
export const pickFirst = (...values) =>
values.find((value) => value !== undefined && value !== null && value !== '');
export const getQty = (item) =>
pickFirst(item?.qty, item?.quantity, item?.holding, item?.hold_qty);
export const getBuyPrice = (item) =>
pickFirst(
item?.buy_price,
item?.avg_price,
item?.avg,
item?.purchase_price,
item?.buyPrice,
item?.price
);
export const getCurrentPrice = (item) =>
pickFirst(
item?.current_price,
item?.current,
item?.cur_price,
item?.now_price,
item?.market_price
);
export const getProfitRate = (item) =>
pickFirst(
item?.profit_rate,
item?.profitRate,
item?.profit_pct,
item?.profitPercent,
item?.pnl_rate,
item?.return_rate,
item?.yield
);
export const getProfitLoss = (item) =>
pickFirst(item?.profit_loss, item?.pnl, item?.profitLoss);
export const toNumeric = (value) => {
if (value === null || value === undefined || value === '') return null;
const numeric = Number(String(value).replace(/[^0-9.-]/g, ''));
return Number.isNaN(numeric) ? null : numeric;
};
/* ── Chart colors ──────────────────────────────────────────────── */
export const CHART_COLORS = ['#818cf8', '#fbbf24', '#34d399', '#f472b6', '#fb923c', '#a78bfa', '#38bdf8', '#4ade80'];
export const profitColorClass = (numericValue) => {
if (numericValue > 0) return 'is-up';
if (numericValue < 0) return 'is-down';
if (numericValue === 0) return 'is-flat';
return '';
};
export const getVixLabel = (vix) => {
if (vix < 12) return '극히 낮음 (안일 주의)';
if (vix < 20) return '정상 (안정적)';
if (vix < 30) return '주의 (불확실성 증가)';
if (vix < 40) return '높음 (극도의 공포)';
return '극단 (패닉)';
};
export const getFgLabel = (score) => {
if (score <= 25) return '극단적 공포';
if (score <= 45) return '공포';
if (score <= 55) return '중립';
if (score <= 75) return '탐욕';
return '극단적 탐욕';
};
/* ── empty portfolio form ────────────────────────────────────────── */
export const emptyPortfolioForm = {
broker: '',
ticker: '',
name: '',
quantity: '',
avg_price: '',
};
/* ── empty sell-history form ─────────────────────────────────────── */
export const toLocalDatetimeValue = (isoStr) => {
if (!isoStr) return '';
const d = new Date(isoStr);
const pad = (n) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
};
export const emptySellForm = () => ({
broker: '',
ticker: '',
name: '',
quantity: '',
avg_price: '',
sell_price: '',
commission: '',
sold_at: toLocalDatetimeValue(new Date().toISOString()),
});
/* ── TAB IDs ─────────────────────────────────────────────────────── */
export const TAB_PORTFOLIO = 'portfolio';
export const TAB_AI = 'ai';
export const TAB_REPORT = 'report';
export const TAB_ADVISOR = 'advisor';