feat(777-1): 호표가 다른 호표를 부를 때 = 일위대가 둘 — 하위 함께 만들기 · 같은 SC 재사용 · 결과 글 · 납작 길 막음

- 부모 저장 때 확인 안 된 하위 호표(깊이 끝까지)를 하위 먼저 그 체크대로 함께 만듦(409 「먼저 확인」 걷음)
  · 서버 = 체크 제안 몸(M02_Hopyo_Kids.proposal ↔ 화면 bodyFor 짝) · 화면 = 확인 창 캐시 고름(Hopyo_Kids.ts)
  · 하위 실패 = 409 「하위 제N호표 실패 — …」 · 하위 · 하위남은줄 → 결과 표 [고르기]가 그 하위를 엶
  · [모두 고정] 연쇄 실패 까닭 따로 · 미리 보기는 하위 호표 줄을 이음으로 셈
- 같은 엑셀 다시 불러오기 = 같은 이름 · 같은 내용(수량까지) SC 재사용 — 본뜬.또(SK 꼴) · 끝내기는 그 몫만 뺌
- 결과 표 글 「SC-12 · 하위 제4호표 SC-11(사본 담음)」 · 확인 창 호표 줄 글 「하위 일위대가 SC-11 사본」
- [구조물 조합으로] 도 하위 먼저(못 만들면 지금처럼 고정 줄) · 붙여넣기 · B09 통째 = 이미 하위 먼저(안 고침)
- 엑셀 대조(CUTS · _excel_round · excel_given · compare) → 새 M02_Hopyo_Compare.py(Build 608 → 540)
- M02_Labor_Router 병행 흔적 가드 걷음(_later · inspect · 몸 칸 새로)
- STmate [엑셀 보기] 단가산출근거로 감(형식별 블록 시트 이름)
- 낡은 시험 고침: 「제46호표 먼저 확인」 409 · 글 → 하위 함께(777-1 정함) · 718-1 실패 = 남은줄 또는 하위 ·
  화면 시험 StaleError 안내 → unmadeKids · leftAt · 새 지킴 시험 test_777_1_hopyo_kids

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-04 09:24:48 +09:00
co-authored by Claude Opus 5.5
parent 4e0ea28aec
commit eaa8064f1a
17 changed files with 740 additions and 171 deletions
@@ -6,8 +6,8 @@
* 오른쪽 = 호표 목록(체크 · 제N호표 [수정] · 찾기 · 제안 n/m — 다 연결이면 녹색 · 뒤 셈 중이면 3 초마다 다시) + [저장].
* · 목록 누름 → 시트에서 그 호표 영역(B열 ~ 끝 열) 고름 · 칸 누름 → 그 행 호표를 목록에서 강조 · 영역 고름
* · [수정] = 그 호표 확인 창(`openHopyoCheck` · 줄 누름과 따로)
* · [저장] = 체크한 호표마다 체크 줄(확인 창 캐시 · 없으면 서버 제안)로 하위 호표부터 차례로 `/hopyo-build` —
* 물음 한 번(확인 창 안 연 호표 수) · 체크 줄 없는 호표 건너뜀 · 하위 확인 전(409)은 안내 ·
* · [저장] = 체크한 호표 + 확인 안 된 하위 호표(777-1 · `Hopyo_Kids`)마다 체크 줄(확인 창 캐시 · 없으면 서버 제안)로
* 하위 호표부터 차례로 `/hopyo-build` — 물음 한 번 · 체크 줄 없는 고른 호표 건너뜀 · 하위 실패 = 그 하위 [고르기] ·
* 결과 표(사전조합 키 · 빠진 줄 · 값 차이) · `onImport` 가 있으면 호표마다 알림([로직 추가] 참조 줄)
* · [모두 고정으로 들여오기](705-3) = 물음 한 번 → 서버 `/hopyo-build-all`(호표 모두 · 하위 먼저 · 엑셀 숫자 그대로 ·
* 이미 만든 호표 그대로) → 알림 「호표 n · 줄 m · 품셈 줄 a · 고정 줄 b · 빠짐 0 · 엑셀 합계 = 우리 합계 n/n」 + 다음 걸음
@@ -27,7 +27,6 @@ import { openModal } from "@ui/ui_template_modal";
import { t as L } from "@ui/ui_template_locale";
import type { Sheet } from "../A00_Common/spreadsheet/spreadsheet_types";
import type { SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet";
import { StaleError } from "./M02_MasterTemplete_Api_Fetch";
import {
buildHopyo,
buildHopyoAll,
@@ -45,7 +44,6 @@ import {
type HopyoView,
} from "./M02_MasterTemplete_Hopyo_Api";
import {
bodyOf,
draftKey,
hasDraft,
HOPYO_BUILT,
@@ -53,8 +51,8 @@ import {
openHopyoCheck,
partsOf,
POLL_MS,
toTarget,
} from "./M02_MasterTemplete_Hopyo_Check";
import { bodyPair, kidsText, leftAt, readyFor, unmadeKids } from "./M02_MasterTemplete_Hopyo_Kids";
import { builtWord } from "./M02_MasterTemplete_Hopyo_Choose";
import { openRelink, relinkKeys } from "./M02_MasterTemplete_Hopyo_Relink";
import { leftButton } from "./M02_MasterTemplete_Hopyo_Left";
@@ -83,7 +81,7 @@ export interface HopyoSheetOptions {
}
const ILWI = "일위대가";
const DANSAN = "단가산출";
const DANSAN = ["단가산출", "단가산출근거"]; // 형식별 블록 시트(EST · STmate)
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
const won = (v: number | null | undefined): string =>
@@ -392,7 +390,8 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void {
poll(file);
if (grid instanceof Error) return void left.replaceChildren(note(grid.message));
await mountSheet(grid);
if (opt.단산행 !== undefined) sheet?.select(rowsText(opt.단산행), DANSAN);
const rows = opt.단산행 === undefined ? null : rowsText(opt.단산행);
if (rows) DANSAN.some((name) => sheet?.select(rows, name));
else if (current !== null) focus(current);
} catch (error) {
if (!closed) {
@@ -413,17 +412,7 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void {
],
});
/** 호표 하나의 몸 — 확인 창 캐시 위 체크 제안 · 통째면 줄 없음 */
function bodyFor(one: HopyoOne): Parameters<typeof buildHopyo>[2] {
const h = one.호표;
const d = loadDraft(draftKey(opt.scope, opt.key, h.번));
return h.통째 && (d.통째 ?? h.통째.체크) ? [] : bodyOf(h.줄, d);
}
function wholeFor(one: HopyoOne): Parameters<typeof buildHopyo>[3] {
const h = one.호표;
const d = loadDraft(draftKey(opt.scope, opt.key, h.번));
return h.통째 && (d.통째 ?? h.통째.체크) ? { 대상: h.통째.대상.map(toTarget) } : null;
}
const body = (one: HopyoOne) => bodyPair(opt.scope, opt.key, one);
const headCell = (no: number, name: string): HTMLElement =>
el("td", { text: `제${no}호표 ${name}`.trim() });
@@ -448,11 +437,12 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void {
skipped.push(el("tr", { children: [headCell(no, ""), el("td", { text: why(error) })] }));
}
}
ones.push(...(await unmadeKids(opt.scope, opt.key, target, ones))); // 777-1 하위 함께
} finally {
hideLoadingOverlay();
paintGo();
}
const ready = ones.filter((one) => bodyFor(one).length || wholeFor(one));
const ready = ones.filter((one) => readyFor(opt.scope, opt.key, one, nos)); // 하위 함께
for (const one of ones)
if (!ready.includes(one))
skipped.push(
@@ -488,7 +478,7 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void {
const cell = headCell(no, h.이름);
try {
const from = { 층: opt.scope.layer, 주인: opt.scope.owner, 키: opt.key, 번: no };
const got = await buildHopyo(target, from, bodyFor(one), wholeFor(one));
const got = await buildHopyo(target, from, ...body(one));
ok += 1;
opt.onImport?.(got, h);
rows.push(
@@ -496,18 +486,16 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void {
attrs: { "data-built": got.참조.키 },
children: [
cell,
el("td", { text: `${got.참조.키} · ${builtWord(got)}` }),
el("td", { text: `${got.참조.키} · ${builtWord(got)}${kidsText(got)}` }),
el("td", { text: String(got.빠진줄.length), className: "m02-hopyo__num" }),
el("td", { text: won(got.비교.차이), className: "m02-hopyo__num" }),
],
}),
);
} catch (error) {
const left = leftButton(error, { scope: opt.scope, target, key: opt.key, 파일: opt.파일, 번: no }); // prettier-ignore
const text =
left || !(error instanceof StaleError)
? `실패 — ${why(error)}`
: `안내 — ${why(error)} · 하위 호표를 함께 고르거나 먼저 저장`;
const [err, kid] = leftAt(error, no); // 하위 실패 = 그 하위 남은 줄
const left = leftButton(err, { scope: opt.scope, target, key: opt.key, 파일: opt.파일, 번: kid }); // prettier-ignore
const text = `실패 — ${why(error)}`;
rows.push(el("tr", { children: [cell, el("td", { text }), ...(left ? [el("td", { children: [left] })] : [])] })); // prettier-ignore
}
}