/* ============================================================================= * M01_MasterData_UI_RateSheet_Map.ts * 요율 화면 시트 칸 → RT 줄 찾기 (805-1) — 순수 계산 · 화면 · fetch 없음. * 책의 `대응`(RT 키 → 시트 · 첫 칸 · 칠할 범위)을 거꾸로 읽음. 겹치면 가장 좁은 범위(RT000007 ⊂ RT000006). * 병합 칸 안쪽은 병합의 왼쪽 위 칸으로 바꿔 찾음(예 고용보험 7등급 AH79:AT82). * ========================================================================== */ export interface RateSpot { 시트: string; 칸: string; 범위: string; } export interface RateArea { r0: number; c0: number; r1: number; c1: number; } export type ParseArea = (a1: string) => RateArea | null; const inside = (a: RateArea, r: number, c: number): boolean => r >= a.r0 && r <= a.r1 && c >= a.c0 && c <= a.c1; /** 시트 id · 행 · 열(0 부터) → RT 키 · 대응 없으면 null. `merges` = 그 시트의 병합 A1 목록. */ export function keyAt( spots: Record, parse: ParseArea, merges: readonly string[] | undefined, sheetId: string, row: number, col: number, ): string | null { let [r, c] = [row, col]; for (const text of merges ?? []) { const m = parse(text); if (m && inside(m, r, c)) { [r, c] = [m.r0, m.c0]; break; } } let best: string | null = null; let bestSize = Infinity; for (const [key, spot] of Object.entries(spots)) { const a = spot.시트 === sheetId ? parse(spot.범위) : null; if (!a || !inside(a, r, c)) continue; const size = (a.r1 - a.r0 + 1) * (a.c1 - a.c0 + 1); if (size < bestSize) [best, bestSize] = [key, size]; } return best; }