- 탭 셋 일위대가표 · 단가산출표 · 단가산출 상세 = 서버 역할 id(이름으로 안 찾음 · STmate 단가산출근거 뛰기 풀림) · 두 형식 같은 탭 글 · 원본 시트 이름은 막대 - 새 logic/M02_MasterTemplete_Hopyo_SheetMap.ts(순수 — 줄 칠 · 블록 칠 · 머리 행 = 가장 나쁜 칠 · 뛸 곳) · 「이어짐」 = 확인 창 체크 규칙(캐시가 이김) - 새 M02_MasterTemplete_Hopyo_SheetNav.ts — 범례 createLegend · 맥락 단추(상세 · 목록 · 하위 호표 · 부르는 줄 차례로) · 뒤로 · 두 번 누름 = 첫 단추 · 다시 칠(목록 다시 읽음 · 캐시 고침 · 만들기) · 칠은 A 열 ~ 끝 열 · 띠는 A 열 - Hopyo_Sheet.ts = 잇는 줄만(580 → 609) · 줄 행 누름 = 그 줄 그대로 · 머리 행 = 호표 영역 - 779-6 — 같은 파일 창은 새 모달 대신 그 창을 앞으로 꺼내 그 자리로 · 닫기 = 제자리로 내림 · 마지막 격자 다시 씀(확인 · 고르기 창 [엑셀 보기] 모두 openSheet 한 곳으로 지나감) - Hopyo_Check.ts saveDraft = 다시 칠 알림 한 줄(663 → 662) · Hopyo_Api.ts at 내보냄(676 그대로) - 서버 Match.view — 목록 「후보」 중 이름 후보 안 찾은 줄만 다시 셈(뒤 셈 끝 뒤) → 확인 창 「없음」과 같음(빨강) · 북부청 임도 목록 응답 2.81 → 3.49 초(다시 셈 48 줄 · 없음 11) - 시험: 새 test_779_4_sheet_map(꼴 표 · 블록 칠 · 실물 EST · STmate 칠 행 수 · 뛸 곳) · test_m02_hopyo_screen 779 지킴 더함 - 낡은 시험 고침: test_779_2 기대 「후보 ≠ 없음 허용」 → 「모두 같음」(까닭 = 이번 덧 셈) · 도우미 helper_m02_hopyo_check · helper_729_3_hopyo_left 에 document 대역(까닭 = saveDraft 알림이 document 를 씀) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
226 lines
9.7 KiB
TypeScript
226 lines
9.7 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_SheetMap.ts
|
|
* 엑셀 보기 창 세 보기 칠 · 뛸 곳(PLAN 779-4 · 요구서 621 3장 4 · 5) — 순수 셈.
|
|
* · 재료 = 서버 격자 `보기 · 목록 · 줄 · 호표 · 단산`(779-1 `M02_Hopyo_Grid`) · 목록 `줄상태 · 만든`(779-2 `Match.view`) ·
|
|
* 블록 `[{번, 판정, SK, 갈래, 못옮김}]`(779-2 `M02_Hopyo_Marks` — 칠 꼴 표가 그 머리) · 확인 창 캐시 고름
|
|
* · 칠 = success 이어짐 · warning 이어짐 · 엑셀과 다름 · info 고를 것 · danger 못 이음 · muted 밖 + mine(내가 고름 띠)
|
|
* · 「이어짐」 = 확인 창과 한 규칙(`Hopyo_Check` `isChecked && checkable` · 캐시 고름이 이김) — 캐시에 대상이 없으면
|
|
* 서버 체크 제안(서버가 배 확실할 때만 켬)을 그대로 믿음
|
|
* · 호표 머리 행 = 그 호표 줄 칠 중 가장 나쁜 것 · 블록 = 만든 SK 갈래 › 없으면 부르는 첫 줄 판정 「예상」 · 안 불림 = 밖
|
|
* · 뛸 곳 = 일위대가 단산 줄 → 상세 블록 · 목록 행 · (여럿이면) 다른 부르는 줄 / 일위 줄 → 하위 호표 / 목록 행 → 상세 · 부르는 줄 / 상세 → 부르는 줄 · 목록
|
|
* 화면 · fetch · sessionStorage 없음(`CLAUDE.md` 4장).
|
|
* ========================================================================== */
|
|
|
|
export type Role = "일위대가표" | "단가산출표" | "단가산출상세";
|
|
export const ROLES: Role[] = ["일위대가표", "단가산출표", "단가산출상세"];
|
|
export type Tone = "success" | "warning" | "info" | "danger" | "muted";
|
|
type Rows = [number, number];
|
|
|
|
/** 격자 응답 덧 칸(779-1) — 옛 파일엔 없음 */
|
|
export interface GridLine {
|
|
번: number;
|
|
차례: number;
|
|
행: number | null;
|
|
단산?: { 번: number; 행: Rows };
|
|
일위?: { 번: number; 행: Rows };
|
|
}
|
|
export interface GridMore {
|
|
호표: { 번: number; 행: Rows }[];
|
|
단산: { 번: number; 행: Rows }[];
|
|
보기?: Partial<Record<Role, string>>;
|
|
원본?: Partial<Record<Role, string>>;
|
|
목록?: { 호표: { 번: number; 행: number }[]; 단산: { 번: number; 행: number }[] };
|
|
줄?: GridLine[];
|
|
}
|
|
/** 목록 호표 덧 칸(779-2) — `줄상태 [[차례, 상태, 체크 제안]]` · `만든` SC 키 */
|
|
export interface BriefMarks {
|
|
번: number;
|
|
줄상태?: [number, string, boolean][];
|
|
만든?: string | null;
|
|
}
|
|
export interface BlockMark {
|
|
번: number;
|
|
판정: string | null;
|
|
SK: string | null;
|
|
갈래: string | null;
|
|
못옮김: unknown[];
|
|
}
|
|
/** 확인 창 캐시 고름(`Hopyo_Check` `CheckDraft` 중 칠에 쓰는 것) */
|
|
export interface PickDraft {
|
|
체크: Record<number, boolean>;
|
|
대상: Record<number, { 배: number | null; 비율?: unknown }[]>;
|
|
뺌: Record<number, string>;
|
|
}
|
|
export interface Mark {
|
|
tone: string;
|
|
글: string;
|
|
}
|
|
/** 뛸 곳 — 시트 id · 행 [시작, 끝] · 부름 = 여럿(차례로 돌림) */
|
|
export interface Spot {
|
|
시트: string;
|
|
행: Rows;
|
|
}
|
|
export interface Jump {
|
|
갈래: "상세" | "목록" | "호표" | "부름";
|
|
번: number;
|
|
곳: Spot[];
|
|
}
|
|
|
|
const RANK: Record<Tone, number> = { muted: 0, success: 1, warning: 2, info: 3, danger: 4 };
|
|
export const WORD: Record<Tone, string> = {
|
|
success: "이어짐",
|
|
warning: "이어짐 · 엑셀과 다름",
|
|
info: "고를 것",
|
|
danger: "못 이음",
|
|
muted: "밖",
|
|
};
|
|
|
|
/** 줄 칠 — `state` 서버 상태 · `check` 서버 체크 제안 · `d` 캐시 고름 · `at` 줄 차례 */
|
|
export function lineTone(
|
|
state: string,
|
|
check: boolean,
|
|
d: PickDraft | null,
|
|
at: number,
|
|
): { tone: Tone; mine: boolean } {
|
|
const parts = d?.대상[at];
|
|
const mine = parts !== undefined || !!d?.뺌[at];
|
|
if (d?.뺌[at] || state === "뺌") return { tone: "muted", mine };
|
|
const ok = !parts
|
|
? true // 캐시 대상 없음 = 서버 제안 대상(체크 제안은 배 확실할 때만)
|
|
: state === "%"
|
|
? !!parts[0]?.비율
|
|
: parts.length > 0 && parts.every((p) => p.배 !== null && p.배 > 0);
|
|
const on = (d?.체크[at] ?? check) && ok;
|
|
if (state === "%") return { tone: on ? "success" : "muted", mine };
|
|
if (on) return { tone: state === "다른 점" && !parts ? "warning" : "success", mine };
|
|
return { tone: state === "없음" || state === "노임" ? "danger" : "info", mine };
|
|
}
|
|
|
|
/** 가장 나쁜 칠(밖만이면 밖) */
|
|
export const worst = (tones: Tone[]): Tone =>
|
|
tones.reduce<Tone>((a, b) => (RANK[b] > RANK[a] ? b : a), "muted");
|
|
|
|
/** 블록 칠 — 만든 SK 갈래 › 없으면 부르는 첫 줄 판정 「예상」 · 판정 없음 = 밖 */
|
|
export function blockTone(m: BlockMark | undefined): { tone: Tone; 글: string } {
|
|
if (!m) return { tone: "muted", 글: "안 불림" };
|
|
if (m.SK) {
|
|
const lost = m.못옮김.length;
|
|
const tone: Tone = lost ? "danger" : m.갈래 === "SK" ? "warning" : "success";
|
|
return { tone, 글: `${m.SK} · ${m.갈래 ?? ""}${lost ? ` · 못옮김 ${lost}` : ""}` };
|
|
}
|
|
if (!m.판정) return { tone: "muted", 글: "판정 없음" };
|
|
const tone: Tone = m.판정 === "맞음" ? "success" : m.판정 === "다른 점" ? "warning" : "info";
|
|
return { tone, 글: `예상 — 부르는 첫 줄 ${m.판정}` };
|
|
}
|
|
|
|
/** 보기 셋 찾기표 */
|
|
export interface SheetIndex {
|
|
ids: Partial<Record<Role, string>>;
|
|
lineAt: Map<number, GridLine>;
|
|
hopyo: Map<number, Rows>;
|
|
block: Map<number, Rows>;
|
|
listRow: Map<number, number>;
|
|
listAt: Map<number, number>;
|
|
callers: Map<number, GridLine[]>;
|
|
}
|
|
|
|
export function indexOf(g: GridMore): SheetIndex {
|
|
const callers = new Map<number, GridLine[]>();
|
|
for (const l of g.줄 ?? [])
|
|
if (l.단산 && l.행) callers.set(l.단산.번, [...(callers.get(l.단산.번) ?? []), l]);
|
|
const list = g.목록?.단산 ?? [];
|
|
return {
|
|
ids: g.보기 ?? {},
|
|
lineAt: new Map((g.줄 ?? []).flatMap((l) => (l.행 ? [[l.행, l] as const] : []))),
|
|
hopyo: new Map(g.호표.map((h) => [h.번, h.행])),
|
|
block: new Map(g.단산.map((b) => [b.번, b.행])),
|
|
listRow: new Map(list.map((x) => [x.번, x.행])),
|
|
listAt: new Map(list.map((x) => [x.행, x.번])),
|
|
callers,
|
|
};
|
|
}
|
|
|
|
/** 행 범위 안 번(호표 · 블록) */
|
|
export const numAt = (spans: Map<number, Rows>, row: number): number | null => {
|
|
for (const [no, [a, b]] of spans) if (a <= row && row <= b) return no;
|
|
return null;
|
|
};
|
|
|
|
/** 시트 · 행에서 뛸 곳 — 없으면 [] */
|
|
export function jumpsAt(ix: SheetIndex, sheet: string, row: number): Jump[] {
|
|
const { 일위대가표: ilwi, 단가산출표: list, 단가산출상세: detail } = ix.ids;
|
|
const detailOf = (no: number): Jump[] => {
|
|
const rows = ix.block.get(no);
|
|
return detail && rows ? [{ 갈래: "상세", 번: no, 곳: [{ 시트: detail, 행: rows }] }] : [];
|
|
};
|
|
const listOf = (no: number): Jump[] => {
|
|
const at = ix.listRow.get(no);
|
|
return list && at ? [{ 갈래: "목록", 번: no, 곳: [{ 시트: list, 행: [at, at] }] }] : [];
|
|
};
|
|
const callersOf = (no: number): Jump[] => {
|
|
const lines = ix.callers.get(no) ?? [];
|
|
const 곳 = lines.map((l): Spot => ({ 시트: ilwi!, 행: [l.행!, l.행!] }));
|
|
return ilwi && 곳.length ? [{ 갈래: "부름", 번: no, 곳 }] : [];
|
|
};
|
|
if (sheet === ilwi) {
|
|
const l = ix.lineAt.get(row);
|
|
if (l?.단산) {
|
|
const more = (ix.callers.get(l.단산.번)?.length ?? 0) > 1 ? callersOf(l.단산.번) : [];
|
|
return [...detailOf(l.단산.번), ...listOf(l.단산.번), ...more]; // 같은 블록 다른 부르는 줄
|
|
}
|
|
if (l?.일위) return [{ 갈래: "호표", 번: l.일위.번, 곳: [{ 시트: ilwi, 행: l.일위.행 }] }];
|
|
return [];
|
|
}
|
|
if (sheet === list) {
|
|
const no = ix.listAt.get(row);
|
|
return no === undefined ? [] : [...detailOf(no), ...callersOf(no)];
|
|
}
|
|
if (sheet === detail) {
|
|
const no = numAt(ix.block, row);
|
|
return no === null ? [] : [...callersOf(no), ...listOf(no)];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/** 칠 표 — 시트 id → 행(1부터) → {꼴, 글} · `draftOf(번)` = 그 호표 캐시 고름(없으면 null) */
|
|
export function paintOf(
|
|
ix: SheetIndex,
|
|
briefs: BriefMarks[],
|
|
blocks: BlockMark[],
|
|
draftOf: (no: number) => PickDraft | null,
|
|
): Map<string, Map<number, Mark>> {
|
|
const out = new Map<string, Map<number, Mark>>();
|
|
const put = (sheet: string | undefined, row: number, m: Mark): void => {
|
|
if (sheet) (out.get(sheet) ?? out.set(sheet, new Map()).get(sheet)!).set(row, m);
|
|
};
|
|
const { 일위대가표: ilwi, 단가산출표: list, 단가산출상세: detail } = ix.ids;
|
|
const rowOf = new Map<string, number>(); // 「번:차례」 → 일위대가 행
|
|
for (const l of ix.lineAt.values()) rowOf.set(`${l.번}:${l.차례}`, l.행!);
|
|
for (const h of briefs) {
|
|
if (!h.줄상태) continue;
|
|
const d = draftOf(h.번);
|
|
const tones: Tone[] = [];
|
|
for (const [at, state, check] of h.줄상태) {
|
|
const { tone, mine } = lineTone(state, check, d, at);
|
|
tones.push(tone);
|
|
const row = rowOf.get(`${h.번}:${at}`);
|
|
const 글 = `제${h.번}호표 줄 ${at + 1} · ${state} → ${WORD[tone]}${mine ? " · 내가 고름" : ""}`;
|
|
if (row) put(ilwi, row, { tone: mine ? `${tone} mine` : tone, 글 });
|
|
}
|
|
const head = ix.hopyo.get(h.번)?.[0];
|
|
const tone = worst(tones);
|
|
const made = h.만든 ? ` · 만듦 ${h.만든}` : "";
|
|
if (head && !out.get(ilwi ?? "")?.has(head))
|
|
put(ilwi, head, { tone, 글: `제${h.번}호표 · 가장 나쁜 줄 ${WORD[tone]}${made}` });
|
|
}
|
|
const by = new Map(blocks.map((b) => [b.번, b]));
|
|
for (const [no, [a, b]] of ix.block) {
|
|
const { tone, 글 } = blockTone(by.get(no));
|
|
const m = { tone, 글: `단산${no} · ${WORD[tone]} · ${글}` };
|
|
for (let r = a; r <= b; r += 1) put(detail, r, m);
|
|
const at = ix.listRow.get(no);
|
|
if (at) put(list, at, m);
|
|
}
|
|
return out;
|
|
}
|