/* Shared rendering and recalculation for reviewed shareholder returns. */ function escapeReturnText(value) { return String(value ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } function recalculatePriceFields(row, priceData) { row.current_price = priceData.current_price; // Null means the new price has no matching market cap. Never reuse the old cap. row.market_cap = priceData.market_cap ?? null; row.forward_per = row.current_price && row.est_eps ? row.current_price / row.est_eps : null; row.pbr = row.current_price && row.bps ? row.current_price / row.bps : null; row.est_dividend_yield = row.est_dps != null && row.current_price > 0 ? row.est_dps / row.current_price * 100 : null; row.est_cancellation_yield = row.est_cancellation_amount != null && row.market_cap > 0 ? row.est_cancellation_amount / row.market_cap * 100 : null; row.est_total_shareholder_yield = row.est_dividend_yield != null && row.est_cancellation_yield != null ? row.est_dividend_yield + row.est_cancellation_yield : null; } function passesReturnFilter(row, enabled, rate, spread) { return !enabled || (row.est_total_shareholder_yield != null && rate != null && row.est_total_shareholder_yield > rate + spread); } const RETURN_STATUS_LABELS = { unconfirmed: '미확인', pending_review: '검토 대기', unsupported_scope: '주식 종류·범위 미확인', conflicting_policies: '자료 충돌 검토 필요', missing_inputs: '추정에 필요한 값 미확인', confirmed_none: '소각 계획 없음 확인', reviewed: '자동 반영', }; async function setReturnSourceStatus(id, action) { const response = await fetch(`/api/admin/returns/documents/${id}/status`, { method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action}), }); const data = await response.json(); if (!response.ok) throw new Error(typeof data.detail === 'string' ? data.detail : '반영 상태를 변경하지 못했습니다.'); return data; } function returnDetailHTML(r, options = {}) { if (!r) return ''; const pct = v => v == null ? '—' : v.toFixed(2) + '%'; const asOf = r.return_as_of ? new Date(r.return_as_of.endsWith('Z') ? r.return_as_of : r.return_as_of + 'Z').toLocaleString('ko-KR', {timeZone:'Asia/Seoul',hour12:false}) : ''; const sources = (r.return_sources || []).map(d => { let url = ''; try { const u = new URL(d.source_url); if (['https:', 'http:'].includes(u.protocol)) url = u.href; } catch (_) {} const link = url ? `${escapeReturnText(d.title)}` : escapeReturnText(d.title); const evidence = [...(d.events || []), ...(d.policies || [])].filter(x => x.year === r.year).map(x => x.evidence).join(' · '); let state = {active:'자동 반영',approved:'반영 중',excluded:'반영 취소',reference:'근거만 보관',pending:'근거만 보관'}[d.status] || d.status; if (['active','approved'].includes(d.status) && Array.isArray(r.return_applied_source_ids) && !r.return_applied_source_ids.includes(d.id)) state = '대체·중복 또는 계산에 미사용'; const amounts = [...(d.events || []), ...(d.policies || [])].filter(x => x.year === r.year) .map(x => x.amount != null ? `${Number(x.amount).toLocaleString('ko-KR')}억원` : x.ratio != null ? `${(x.ratio*100).toFixed(1)}% 목표` : '').filter(Boolean).join(' · '); const controls = options.canManage ? `` : ''; return `
${pct(r.est_total_shareholder_yield)} = 예상 배당 ${pct(r.est_dividend_yield)} + 예상 소각 ${pct(r.est_cancellation_yield)}
예상 소각액 ${r.est_cancellation_amount == null ? '—' : r.est_cancellation_amount.toLocaleString('ko-KR', {maximumFractionDigits: 2}) + '억원'} · ${escapeReturnText(RETURN_STATUS_LABELS[r.return_status] || r.return_status)}${r.return_is_estimated ? ' · 환원 목표 기반 추정' : ''}
소각수익률 = 공시·IR 기준 소각 금액 ÷ 현재 시가총액. 실제 현금 배당과는 성격이 다릅니다.
${asOf ? `자료 반영 기준일: ${escapeReturnText(asOf)} (한국시간)
` : ''} ${(r.return_assumptions || []).map(a => `${escapeReturnText(a)}
`).join('')} ${r.return_collection?.error ? '최근 공시 수집 미완료 · 기존 저장 자료 기준
' : ''}