Files
Aislo/M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_SkPick.ts
T
eomsangdonandClaude Opus 5.5 8a5689c400 feat(780-2): 엑셀 보기 단가산출 탭 오른쪽 = 단가산출 목록 — 판 바꾸기 · 거르기 · 목록 ↔ 시트 · 두 벌 맞춤(요구 627)
- 새 Hopyo_SheetSk: 탭(역할) 따라 오른쪽 판 바꿈 · 일위대가표 = 호표 목록 그대로 · 단가산출표 · 상세 = 마법사 단가산출 칸 좁은 꼴 한 벌(처음 올 때 지음 · 다른 층 · 권한 없음 = 보기만) · 시트 칸 → 목록 강조 · 목록 줄 → 지금 탭 블록 자리
- WizardSk: 좁은 꼴 옵션 · 부름 칸(일위대가 n 곳 · 일위대가 밖) · 거르기(전체 · 안 · 밖 · 고를 줄 있음) · 같은 캐시 두 벌 맞춤 알림 · 옮김 뒤 HOPYO_BUILT(열린 엑셀 보기 칠 다시)
- SkPick: 줄 0 블록 = 「줄 없음」 확인 막음 · 거르기 · 끝내기 글 재료에 일위대가 밖 수
- SheetNav: role · blockAt · spotOf 내보냄 · SheetMap: 판정 없음 글 → 「일위대가 밖」
- Wizard [끝내기]: 막지 않음 · 물음창에 안 옮긴 단가산출 n(일위대가 밖 m)
- 시험: 새 test_627_sheet_sk(판 바꿈 · 목록 지음 · 줄 0 막음 · 캐시 공유) · test_778_5 끝내기 재료 꼴에 밖 칸 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 18:01:06 +09:00

243 lines
9.8 KiB
TypeScript

/* =============================================================================
* M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_SkPick.ts
* 마법사 단가산출 줄 고름(PLAN 778-5 D-5 · 요구 621 · 622 · 623 · 624) — 순수 셈.
* · 재료 = 서버 `POST …/hopyo/{키}/sangeun` 답 `판정 [{번 … 줄 […]}]`(D-3 `M02_Hopyo_Sangeun.build`)
* · 고름 캐시 `{고름 {번: {차례: 고름}}, 확인 [번]}` — 줄 고름이 서버 상태를 덮음(꼬리표 「내가 고름」)
* · 블록 [확인] = 남은 고를 줄 0 일 때만 · 몸 = `{확인: [{번, 고름?}]}`(확인한 블록만 · 자동 확인 없음)
* · 금액 · 판정 정본은 서버 — 여기는 캐시를 겹쳐 보이기 · 몸 짓기만
* · 780-2(요구 627) — 블록 `부름 [호표 번]`(빈 = 일위대가 밖 · 칸 없음 = 옛 서버 = 안) · 줄 0 = 「줄 없음」 확인 막음 ·
* 거르기(모두 · 안 · 밖 · 고를 줄 있음)
* 화면 · fetch · sessionStorage 없음(`CLAUDE.md` 4장).
* ========================================================================== */
export type SkGroup = "L" | "X" | "M" | "R" | "S" | "E";
/** 줄 고름 — 노임 LB · 기계 EQ · 자재 MT · 율 기준 · 뺌 + 까닭 */
export type SkPick =
| { 노임: string }
| { 기계: string }
| { 자재: string; 별도계상?: boolean }
| { 기준: SkBase; 비목?: string }
| { 뺌: string };
/** 율 기준 — 묶음 · 비목 · 줄 목록(그 엑셀 행 줄) */
export type SkBase = string | { 행: number[] };
/** 서버 후보 — 노임 · 기계 = `키` · 자재 = `자재` · 율 = `기준` */
export interface SkCand {
키?: string;
자재?: string;
기준?: SkBase;
비목?: string;
갈래?: string;
이름?: string | null;
규격?: string;
단위?: string;
구분?: string;
단가?: number | null;
글?: string;
}
export interface SkLine {
차례: number;
행: number | null;
묶음: SkGroup;
이름: string | null;
규격: string;
매개값: Record<string, number>;
엑셀금액: number | null;
상태: string;
방법: string;
단위: unknown;
후보: SkCand[];
꼬리표: string[];
글: string;
우리금액?: number | null;
차?: number | null;
}
export interface SkBlock {
번: number;
이름: string | null;
단위: string | null;
판정: string;
고를줄: number;
우리금액: number | null;
엑셀금액: number | null;
근거: { 로직: string; 이름: string | null; 판정: string | null } | null;
만든: string | null;
줄: SkLine[];
/** 부르는 일위대가 호표 번(780-1) — [] = 일위대가 밖 · 없음 = 옛 서버(안) */
부름?: number[];
}
export interface SkJudged {
블록: number;
판정: SkBlock[];
만듦: { 번: number; 이름: string | null; 키: string; 갈래: string; 못옮김: unknown[] }[];
못만듦: { 번: number; 이름: string | null; 까닭: string }[];
그대로: number;
같은것씀: { 번: number; 키: string }[];
위층같음: { 번: number; 층: string; 주인: string | null; 키: string }[];
}
/** 확인 목록 한 칸(서버 몸) */
export interface SkSure {
번: number;
고름?: Record<number, SkPick>;
}
/** 캐시 꼴 — 블록 번 · 줄 차례는 JSON 열쇠라 글 */
export interface SkDraft {
고름: Record<string, Record<string, SkPick>>;
확인: number[];
}
export const MINE = "내가 고름";
export const GOOD = "옮길 수 있음";
export const NO_LINES = "줄 없음";
export const LINKED = "이어짐";
export const PICK = "고를 것";
export const OUT = "밖";
export const DROP = "뺌";
export const GROUP: Record<SkGroup, string> = {
L: "노무",
X: "기계",
M: "자재",
R: "율",
S: "하위",
E: "값",
};
/** 율 기준 고르기 칸 */
export const BASES = ["인력", "재료", "노무비", "재료비", "경비"];
const DONE = new Set([LINKED, OUT, DROP]);
export const emptyDraft = (): SkDraft => ({ 고름: {}, 확인: [] });
/** 캐시 글 → 꼴(틀리면 빈 것) */
export function readDraft(raw: string | null): SkDraft {
try {
const got = JSON.parse(raw ?? "") as Partial<SkDraft> | null;
const picks = got?.고름 && typeof got.고름 === "object" ? got.고름 : {};
const sure = Array.isArray(got?.확인) ? got.확인.filter((n) => Number.isInteger(n)) : [];
return { 고름: picks, 확인: sure };
} catch {
return emptyDraft();
}
}
/** 고른 것 글 — 키 · 율 기준 · 뺌 까닭 */
export function pickText(p: SkPick): string {
if ("뺌" in p) return `${DROP} · ${p.뺌}`;
if ("기준" in p) {
const base = typeof p.기준 === "string" ? p.기준 : `${p.기준.행.join(" · ")} 행`;
return `기준 ${base}${p.비목 ? ` · ${p.비목}` : ""}`;
}
return "노임" in p ? p.노임 : "기계" in p ? p.기계 : p.자재;
}
/** 줄 보이기 — 캐시 고름이 서버 상태를 덮음 */
export function lineView(
line: SkLine,
pick: SkPick | undefined,
): { 상태: string; 꼬리표: string[]; 글: string; mine: boolean } {
if (!pick) return { 상태: line.상태, 꼬리표: line.꼬리표, 글: line.글, mine: false };
return { 상태: "뺌" in pick ? DROP : LINKED, 꼬리표: [MINE], 글: pickText(pick), mine: true };
}
/** 그 블록 캐시 고름 `{차례: 고름}` */
export const picksOf = (d: SkDraft, 번: number): Record<string, SkPick> => d.고름[String(번)] ?? {};
/** 남은 고를 줄 수 — 이어짐 · 밖 · 뺌 아닌 줄(캐시 고름 겹침) · 줄 0 블록 = 1(서버 「줄 없음」 고를줄 1) */
export const leftLines = (b: SkBlock, picks: Record<string, SkPick>): number =>
b.줄.length ? b.줄.filter((l) => !DONE.has(lineView(l, picks[String(l.차례)]).상태)).length : 1;
/** [확인] 켜짐 — 고를 줄 0 */
export const canConfirm = (b: SkBlock, picks: Record<string, SkPick>): boolean =>
leftLines(b, picks) === 0;
/** 블록 판정 글 — 캐시 겹친 뒤 「옮길 수 있음」 · 「고를 줄 n」 · 「줄 없음」 */
export function verdictOf(b: SkBlock, picks: Record<string, SkPick>): string {
if (!b.줄.length) return NO_LINES;
const n = leftLines(b, picks);
return n ? `고를 줄 ${n}` : GOOD;
}
/** 줄 고를 수 있나 — 밖 줄 = 아님 · 하위 · 값 = 뺌만 */
export const pickable = (l: SkLine): boolean => l.상태 !== OUT;
export const dropOnly = (l: SkLine): boolean => l.묶음 === "S" || l.묶음 === "E";
/** 서버 후보 → 고름(그 줄 묶음 꼴 · 아니면 null) */
export function candPick(group: SkGroup, c: SkCand): SkPick | null {
if (group === "L" && c.키?.startsWith("LB")) return { 노임: c.키 };
if (group === "X" && c.키?.startsWith("EQ")) return { 기계: c.키 };
if (group === "M" && c.자재) return { 자재: c.자재 };
if (group === "R" && c.기준 !== undefined && c.기준 !== null)
return { 기준: c.기준, ...(c.비목 ? { 비목: c.비목 } : {}) };
return null;
}
/** 후보 글 — 「이름 규격 [단위]」 · 율 = 기준 */
export function candText(c: SkCand): string {
if (c.기준 !== undefined) return pickText({ 기준: c.기준, ...(c.비목 ? { 비목: c.비목 } : {}) });
const spec = c.규격 ? ` ${c.규격}` : "";
const unit = c.단위 ? ` [${c.단위}]` : "";
return `${c.이름 ?? c.키 ?? c.자재 ?? ""}${spec}${unit}${c.구분 ? ` · ${c.구분}` : ""}`;
}
/** 캐시 정리 — 고를 줄 남은 블록 · 이미 옮겨 판정 없는 블록은 확인에서 뺌 */
export function tidy(d: SkDraft, blocks: SkBlock[]): SkDraft {
const by = new Map(blocks.map((b) => [b.번, b]));
const sure = d.확인.filter((n) => {
const b = by.get(n);
return !!b && canConfirm(b, picksOf(d, n));
});
return { ...d, 확인: [...new Set(sure)].sort((a, b) => a - b) };
}
/** [확인한 것 옮기기] 몸 — 확인한 블록만 · 고름은 그 블록 줄에 있는 차례만 */
export function sureBody(d: SkDraft, blocks: SkBlock[]): SkSure[] {
const by = new Map(blocks.map((b) => [b.번, b]));
return tidy(d, blocks).확인.map((n) => {
const have = new Set(by.get(n)!.줄.map((l) => l.차례));
const picks = Object.entries(picksOf(d, n)).filter(([at]) => have.has(Number(at)));
return picks.length
? { 번: n, 고름: Object.fromEntries(picks.map(([at, p]) => [Number(at), p])) }
: { 번: n };
});
}
/** 일위대가 밖 블록 — `부름` 빈 배열 */
export const outside = (b: SkBlock): boolean => !!b.부름 && !b.부름.length;
/** 마법사 [끝내기] 글 재료 — 아직 안 옮긴 블록 수 · 그중 일위대가 밖 · 그 블록 고를 줄 합 */
export function waiting(d: SkDraft, blocks: SkBlock[]): { 블록: number; 밖: number; 줄: number } {
const open = blocks.filter((b) => !b.만든);
const 줄 = open.reduce((n, b) => n + leftLines(b, picksOf(d, b.번)), 0);
return { 블록: open.length, 밖: open.filter(outside).length, 줄 };
}
export type SkFilter = "all" | "in" | "out" | "open";
/** 거르기 — 모두 · 일위대가 안 · 일위대가 밖 · 고를 줄 있음(안 옮긴 블록 · 캐시 겹침) */
export function filterBlocks(blocks: SkBlock[], d: SkDraft, f: SkFilter): SkBlock[] {
if (f === "in") return blocks.filter((b) => !outside(b));
if (f === "out") return blocks.filter(outside);
if (f === "open") return blocks.filter((b) => !b.만든 && leftLines(b, picksOf(d, b.번)) > 0);
return blocks;
}
/** 줄 고름 넣기 · 지움(null) — 새 캐시 */
export function setPick(d: SkDraft, 번: number, 차례: number, p: SkPick | null): SkDraft {
const mine = { ...picksOf(d, 번) };
if (p) mine[String(차례)] = p;
else delete mine[String(차례)];
return { ...d, 고름: { ...d.고름, [String(번)]: mine } };
}
/** [확인] 켬 · 끔 — 고를 줄 남으면 안 켬 */
export function setSure(d: SkDraft, b: SkBlock, on: boolean): SkDraft {
const rest = d.확인.filter((n) => n !== b.번);
const ok = on && canConfirm(b, picksOf(d, b.번));
return { ...d, 확인: ok ? [...rest, b.번].sort((x, y) => x - y) : rest };
}