refactor(785-7): 옛 맞춤 화면 TS 지움 · 옛 [저장] · [모두 고정] 단추 걷음 · 390 줄 판 높이

- 지움(git rm) 15 파일 3,318 줄: Hopyo_Check · CheckRows · Choose · WizardSk · SkApi · SkLines · logic/SkPick · logic/Draft ·
  Left · Lost · Pick · SheetMake · Kids · ToItem · Compare(모두 옛 확인 창 · 마법사 단가산출 · 안 이은 줄 길 몫 · 부르는 곳 0)
- 단추 판단: 엑셀 보기 [저장](체크 호표) · [모두 고정으로 들여오기] 화면 단추 걷음 → 만들기 = [옮기기] 하나 ·
  모두 고정 서버 길(Builder · build_all)은 B09 · 붙여넣기가 써서 남김 · 목록 체크 칸 · 모달 껍데기(openHopyoSheet · raise · lower) 걷음
- Import: 「안 이은 줄」 띠 · [안 이은 줄 고르기] · 남은 줄 셈 걷음 · [끝내기] = 물음(확인하고 안 옮긴 수 = 판정 고름 캐시 moveBody)
- Api: 옛 맞춤 타입 · 길(readHopyoOne · guessHopyo · buildHopyo · buildHopyoAll · leftHopyo · listHopyoPicks · hopyoSiblings ·
  buildHopyoItem …) 걷음 · POLL_MS · HOPYO_BUILT 옮겨 옴 · 목록 꼴 = 1차 판정 줄상태(Judge.view)
- Labor_Pick: withHopyo(실무 호표 덧줄) → withSangeun(찾아 넣기 창 표시만) · Detail · Table 부름 바꿈
- Ledger: unitField 옮겨 옴 · SheetNav: 옛 확인 캐시 칠 걷음 · UI_Page: [구조물 조합으로] 듣기 걷음
- CSS: 옛 확인 창 · 고르기 창 규칙 34 걷음(481 → 287 줄) · 390 폭 = 줄 판 열리면 위 칸 넓힘 · 줄 판 높이 바닥 16rem

시험 정리(까닭 = 지운 화면 파일만 읽음):
- 지움: test_729_3_hopyo_left_screen · test_746_2_hopyo_compare_screen · test_778_5_sk_pick_screen · helper_729_3 · helper_m02_hopyo_check · pick · seam
- test_m02_hopyo_screen = 좌측 판 · 끝내기 둘만 · 627 · helper_778_5 = SheetSk 판 바꿈만 · 785_4 · 721 · 717_4 · 720 · 739_1 = 지금 꼴로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 01:32:17 +09:00
co-authored by Claude Opus 5.5
parent 48848bb287
commit cf4e2a0339
45 changed files with 126 additions and 5791 deletions
@@ -1,57 +0,0 @@
/* =============================================================================
* M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_Draft.ts
* 확인 창 금액 미리 보기 꼴(PLAN 359-3) — 엑셀 줄 하나 + 지금 대상 → 일위대가 문서 한 벌(서버 `/labor/{열}/calc` 에
* 보냄 · 저장 안 함 · 금액은 서버가 셈) · 줄이 들어갈 표(로직 표 · 자재 표).
* 정본 = 서버 `M02_Hopyo_Build.Builder.build` 의 `put`([사전조합 생성] 때 다시 셈) — 같은 규칙:
* 우리 수량 = 엑셀 수량 × 배 · 로직 = {로직, 입력값, 수량 {손}} · 자재 = {코드, 수량 {손}, 별도계상 =
* 갈래 별도계상이면 「별도계상」 아니면 「직접계산」} · 같은 자재 = 수량 더함.
* 「전국」 자재(361-1 · 키 · 단가 없음) = 자재 표 · 문서엔 안 넣음(Build = 본뜬.전국자재 · 금액 없음).
* 미리 보기만 다른 것: 호표 대상(하위 사전조합 — 만든 뒤 셈) · 배 없는 대상은 뺌 ·
* 같은 로직 둘째는 뺌(Build = 한 줄 사전조합으로 감쌈 · 한 문서에 같은 로직 둘 = 서버 400).
* 화면 · fetch · sessionStorage 없음(`CLAUDE.md` 4장).
* ========================================================================== */
import type { HopyoLine, HopyoPart } from "../M02_MasterTemplete_Hopyo_Api";
import type { LaborDoc, LaborMat, LaborRow } from "../M02_MasterTemplete_Labor_Api";
export type CheckTable = "logic" | "mat";
/** 엑셀 갈래 중 자재 표로 가는 것(대상 없을 때) */
const MAT_KINDS = ["자재", "별도계상"];
/** 줄이 들어갈 표 — 대상이 모두 자재(전국 포함)면 자재 표 · 대상 없으면 갈래(자재 · 별도계상) · 그 밖 로직 표 */
export function tableOf(line: Pick<HopyoLine, "갈래">, parts: HopyoPart[]): CheckTable {
if (parts.length) return parts.every((p) => !!p.자재 || !!p.전국) ? "mat" : "logic";
return MAT_KINDS.includes(line.갈래) ? "mat" : "logic";
}
/** 엑셀 줄 + 대상 → 미리 셈 문서 · 셀 것이 없으면 null(호표 대상뿐 · 배 없음 · 대상 없음) */
export function draftDoc(
line: Pick<HopyoLine, "수량" | "갈래">,
parts: HopyoPart[],
): LaborDoc | null {
const rows: LaborRow[] = [];
const mats = new Map<string, LaborMat>();
const apart = line.갈래 === "별도계상" ? "별도계상" : "직접계산";
for (const p of parts) {
if (p.배 === null || p.호표 !== undefined) continue;
const qty = (line.수량 ?? 0) * p.배;
if (p.자재) {
const have = mats.get(p.자재)?.수량;
const hand = (have ? have.손 : 0) + qty;
mats.set(p.자재, {
코드: p.자재,
이름: null,
규격: null,
단위: null,
수량: { 손: hand },
별도계상: apart,
});
} else if (p.로직 && !rows.some((r) => r.로직 === p.로직)) {
const values = (p.입력값 ?? {}) as LaborRow["입력값"];
rows.push({ 로직: p.로직, 별칭: null, 메모: null, 입력값: values, 수량: { 손: qty } });
}
}
if (!rows.length && !mats.size) return null;
return { 종류: "일위대가", 열: "", 단위: null, 담은로직: rows, 자재: [...mats.values()] };
}
@@ -1,11 +1,10 @@
/* =============================================================================
* M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_SheetMap.ts
* 엑셀 보기 창 세 보기 칠 · 뛸 곳(PLAN 779-4 · 요구서 621 3장 4 · 5) — 순수 셈.
* · 재료 = 서버 격자 `보기 · 목록 · 줄 · 호표 · 단산`(779-1 `M02_Hopyo_Grid`) · 목록 `줄상태 · 만든`(779-2 `Match.view`) ·
* · 재료 = 서버 격자 `보기 · 목록 · 줄 · 호표 · 단산`(779-1 `M02_Hopyo_Grid`) · 목록 `줄상태 · 만든`(785-7 `Judge.view` 1차 판정) ·
* 블록 `[{번, 판정, SK, 갈래, 못옮김}]`(779-2 `M02_Hopyo_Marks` — 칠 꼴 표가 그 머리) · 확인 창 캐시 고름
* · 칠 = success 이어짐 · warning 이어짐 · 엑셀과 다름 · info 고를 것 · danger 못 이음 · muted 밖 + mine(내가 고름 띠)
* · 「이어짐」 = 확인 창과 한 규칙(`Hopyo_Check` `isChecked && checkable` · 캐시 고름이 이김) — 캐시에 대상이 없으면
* 서버 체크 제안(서버가 배 확실할 때만 켬)을 그대로 믿음
* · 「이어짐」 = 1차 판정 「제안 있음」 · 판정 화면 확인함 · 만든 SC(옛 확인 창 캐시 `draftOf` 는 785-7 부터 늘 null)
* · 호표 머리 행 = 그 호표 줄 칠 중 가장 나쁜 것 · 블록 = 만든 SK 갈래 › 없으면 부르는 첫 줄 판정 「예상」 · 안 불림 = 밖
* · 블록 안 줄(779-5) = 블록 `줄 [[행, 상태, 차]]`(Marks · D-2 줄 잇기 상태) → 그 출처 행만 칠 · 머리 행 = 줄 가장 나쁜 칠 ·
* 「엑셀과 다름」 = 이어졌는데 금액 참고 차만 있을 때 · 줄 재료 없으면 옛 블록 칠(범위 통째)
@@ -63,7 +62,7 @@ export interface Miss {
줄: { 행: number | null; 이름: string; 묶음: string; 차: number | null; 까닭: string }[];
까닭: string;
}
/** 확인 창 캐시 고름(`Hopyo_Check` `CheckDraft` 중 칠에 쓰는 것) */
/** 옛 확인 창 캐시 고름 꼴(785-7 부터 부르는 쪽이 늘 null) */
export interface PickDraft {
체크: Record<number, boolean>;
대상: Record<number, { 배: number | null; 비율?: unknown }[]>;
@@ -1,242 +0,0 @@
/* =============================================================================
* 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 };
}