/** * TeamOps handlers 공통 헬퍼 — 4인 팀 운영 슬래시 명령 클러스터가 공유하는 함수·상수. * * 이전 위치: `src/features/datacollect/slashRouter.ts` 내부 module-local 함수 * (v2.2.196 에서 분리). 핸들러 도메인별 분할 시 공통 항목만 여기에. * * 추후 datacollect / system 핸들러도 비슷한 공유 helpers 가 필요해지면 * `src/features/_shared/` 로 promote 고려. */ /** 한국식 KRW 숫자 포맷 — 만/억 단위 자동. 마이너스 부호 보존. */ export function fmtKrw(n: number): string { const sign = n < 0 ? '-' : ''; const abs = Math.abs(n); if (abs >= 100_000_000) return `${sign}${(abs / 100_000_000).toFixed(2)}억`; if (abs >= 10_000) return `${sign}${(abs / 10_000).toFixed(0)}만`; return `${sign}${abs.toLocaleString('ko-KR')}`; } /** 한국식 금액 토큰 파싱 — "5000만", "1.5억", "300000", "1,500,000", "10k", "5m" 모두 인식. */ export function parseAmount(token: string): number | null { if (!token) return null; const s = token.replace(/[,_]/g, '').trim(); const m = s.match(/^(-?[\d.]+)\s*(억|만|k|m|b)?$/i); if (!m) return null; const base = parseFloat(m[1]); if (!Number.isFinite(base)) return null; let mul = 1; const unit = (m[2] || '').toLowerCase(); if (unit === '억') mul = 100_000_000; else if (unit === '만') mul = 10_000; else if (unit === 'k') mul = 1_000; else if (unit === 'm') mul = 1_000_000; else if (unit === 'b') mul = 1_000_000_000; return base * mul; } /** D-day 계산 — ISO 날짜 (YYYY-MM-DD) 받아 오늘부터 며칠 후인지. 음수면 지난 일수. */ export function daysUntil(isoDate: string | undefined, now: Date = new Date()): number | null { if (!isoDate) return null; const t = Date.parse(isoDate); if (!Number.isFinite(t)) return null; return Math.ceil((t - now.getTime()) / (24 * 60 * 60 * 1000)); } export interface ParsedTaskOwner { owner: string | undefined; displayTitle: string; } /** * Task 제목·notes 에서 owner 추출. 두 패턴 지원: * 1. 제목 prefix `[멤버] 실제 제목` — /task, /meet 가 등록한 형식 * 2. notes 의 `담당: @이름` 또는 `담당: 이름` — 일부 외부 등록 호환 */ export function parseTaskOwner(title: string, notes?: string): ParsedTaskOwner { const titlePrefix = title.match(/^\[([^\]]+)\]\s*(.+)$/); if (titlePrefix) return { owner: titlePrefix[1].trim(), displayTitle: titlePrefix[2].trim() }; const notesMatch = (notes || '').match(/담당:\s*(?:@)?([\S]+)/); if (notesMatch) return { owner: notesMatch[1].trim(), displayTitle: title }; return { owner: undefined, displayTitle: title }; } /** /hire 파이프라인 단계 정렬 가중치. inbox 1, hired 7, terminal 99. */ export const STAGE_ORDER: Record = { inbox: 1, screened: 2, interview: 3, final: 4, offer: 5, accepted: 6, hired: 7, rejected: 99, declined: 99, }; /** 종료된 후보 단계. 검색·통계에서 active 와 구분. */ export const TERMINAL_STAGES = new Set(['hired', 'rejected', 'declined']); /** 단계별 emoji — UI 표시. */ export function stageEmoji(stage: string): string { switch (stage) { case 'inbox': return '📥'; case 'screened': return '🔍'; case 'interview': return '💬'; case 'final': return '🎯'; case 'offer': return '📨'; case 'accepted': return '🤝'; case 'hired': return '🎉'; case 'rejected': return '❌'; case 'declined': return '🚪'; default: return '•'; } }