feat(M02 실무자료 빠짐 0): 확인 안 한 줄 · 노임 · % = 고정 줄 · [모두 고정으로 들여오기] · [품셈으로 바꾸기] (PLAN 705-3)

- 만들기 — 체크 안 한 줄 · 노임 · 「우리에 없음」 = 고정 줄(엑셀 숫자 그대로 · 서버가 파일에서) · % 줄 = 비율 고정 줄(밑수 = 다른 줄 합일 때 · 아니면 금액 그대로) · 합계만 = 재료비 · 같은 이름 = 차례 가름 · 하위 호표 = 확인된 사전조합 참조
- 이음 = 이름 · 규격 · 단위 · 단가 다 같은 노임 · 자재만 저절로 · 다른 단가 = 이음 후보 수
- 시트 통째 — /hopyo-build-all(하위 먼저 · 확인된 호표 그대로) · 알림 「호표 · 줄 · 품셈 줄 · 고정 줄 · 빠짐 · 엑셀 합계 = 우리 합계」 · 비교 = 엑셀 끝수 규칙(EST 내림 · STmate 반올림)
- 비율 고정 줄 셈 — M02 settle 짝(파이썬 · TS ratioLines) · 사전조합 셈 · B09 pct 줄
- 확인 창 — 만든 사전조합 품셈 줄 = 첫 체크 · [품셈으로 바꾸기] · [모두 고정으로] · 생성 물음에 전후 금액 · 품 다른 줄 수
- 낡은 시험 고침 — 빈 몸 400 · 빠진 줄(확인 안 함 · %) 단정 = 고정 줄로 바뀐 규칙

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 15:10:16 +09:00
co-authored by Claude Opus 5.5
parent 4290edb8bc
commit 2789afb102
15 changed files with 825 additions and 51 deletions
@@ -9,6 +9,9 @@
* · 대상 · 옵션을 바꾸면 서버 `guess`(저장 안 함 · 줄마다 최신 응답만)가 우리 품을 다시 셈
* · 캐시 = sessionStorage `m02-hopyo-check:{층}:{주인}:{키}:{번}`(조작 즉시 · 만들기 성공하면 지움) · 자동 저장 없음
* · 화면 잠금은 [사전조합 생성] 때만 · 쓰기 단추 = 만들 층 쓰기 권한만.
* · 705-3 — 체크 안 한 줄 = 고정 줄(엑셀 숫자 그대로) · 만든 사전조합이 있으면 첫 체크 = 그 품셈 줄(`만든.품셈`) ·
* [품셈으로 바꾸기 (n)] = 맞음 · 별칭 줄 체크 + 생성 · [모두 고정으로] = 체크 모두 끔 + 생성(되돌리기) ·
* 생성 물음에 전후 금액(품셈 = 우리 미리 셈 · 고정 = 엑셀) · 품 다른 줄 수.
* 계산은 서버(`M02_Hopyo_Match` · `M02_Hopyo_Build` · `/labor/{열}/calc`) — 화면은 보이기 · 체크 · 고름만.
* ========================================================================== */
@@ -240,6 +243,8 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement {
const one = await readHopyoOne(opt.scope, opt.key, opt.no, opt.target);
if (my !== seq) return;
got = one;
const made = one.호표.만든?.품셈; // 705-3 — 만든 사전조합의 품셈 줄 = 첫 체크
if (made && !hasDraft(k)) for (const l of one.호표.줄) d.체크[l.차례] = made.includes(l.차례);
draw();
} catch (error) {
if (my === seq && !quiet) root.replaceChildren(loadingError(why(error), () => void load()));
@@ -391,21 +396,38 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement {
단산행: 행,
});
/** 1 단위 금액(705-3) — 품셈 줄 = 우리 미리 셈 · 고정 줄 = 엑셀 · 셈 중 = null */
function sumOf(lines: HopyoLine[], poom: (l: HopyoLine) => boolean): number | null {
let total = 0;
for (const l of lines) {
const p = poom(l) ? price(l) : null;
if (p && !p.calc && !p.fail) return null;
total += p?.calc?.반영합?.계 ?? l.금액.계 ?? 0;
}
return total;
}
async function make(h: HopyoOne["호표"]): Promise<void> {
const whole = !!h.통째 && (d.통째 ?? h.통째.체크);
const lines = whole ? [] : bodyOf(h.줄, d);
const out = leftOut(h.줄, d);
const put = lines.filter((x) => x.대상).length;
const outs = lines.length - put;
const poom = (l: HopyoLine): boolean => isChecked(l, d) && checkable(l, d) && !d.뺌[l.차례];
const now = (l: HopyoLine): boolean => (h.만든?.품셈 ?? []).includes(l.차례);
const outs = h.줄.filter((l) => isChecked(l, d) && d.뺌[l.차례] === "안 씀").length;
const put = h.줄.filter(poom);
const diff = put.filter((l) => {
const q = d.품[l.차례] ?? l.맞춤.품;
return !!q && Object.values(q.같음).some((v) => !v);
}).length;
const after = sumOf(h.줄, poom);
const known = !!h.만든?.품셈; // 옛 사전조합(품셈 줄 기록 없음) = 지금 금액 모름
const before = known ? sumOf(h.줄, now) : null;
const money =
`1 단위 ${known ? `지금 ${before === null ? "셈 중" : won(Math.round(before))} → ` : ""}` +
`바뀐 뒤 ${after === null ? "셈 중" : won(Math.round(after))} · 엑셀 ${won(h.금액.계)}`;
const ask = whole
? `호표 통째로 만듦 — ${h.통째!.대상.map(targetText).join(" · ")}`
: `넣을 줄 ${put} · 뺌 ${outs} · 빠질 줄 ${Object.values(out).reduce((a, b) => a + b, 0)}` +
(Object.keys(out).length
? `(${Object.entries(out)
.map(([a, b]) => `${a} ${b}`)
.join(" · ")})`
: "") +
" — 사전조합 생성";
: `품셈 줄 ${put.length}${diff ? `(품 다름 ${diff})` : ""} · 고정 줄 ${h.줄.length - put.length - outs}` +
` · 뺌 ${outs} — ${money}(미리 셈 · 끝수 차 있음) — 사전조합 생성`;
if (!(await showConfirmDialog(ask, "생성"))) return;
const from = { 층: opt.scope.layer, 주인: opt.scope.owner, 키: opt.key, 번: opt.no };
showLoadingOverlay();
@@ -498,10 +520,32 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement {
});
const whole = !!h.통째 && (d.통째 ?? h.통째.체크);
const go = createButton({ label: "사전조합 생성", variant: "filled", size: "sm" });
go.disabled = !whole && checked < 1;
go.setAttribute("data-hopyo-make", "");
go.addEventListener("click", () => void make(h));
tools.push(all, go);
tools.push(all);
if (h.만든 && !whole) {
// 705-3 — 고정 ↔ 품셈 바꾸기(줄 하나 = 체크 하나 + [사전조합 생성])
const poom = createButton({ label: `품셈으로 바꾸기 (${sure.length})`, size: "sm" });
poom.setAttribute("data-hopyo-to-poom", "");
poom.disabled = !sure.length;
poom.addEventListener("click", () => {
for (const l of sure) d.체크[l.차례] = true;
keep();
draw();
void make(h);
});
const back = createButton({ label: "모두 고정으로", size: "sm" });
back.setAttribute("data-hopyo-to-fixed", "");
back.disabled = !h.만든.품셈?.length;
back.addEventListener("click", () => {
for (const l of h.줄) d.체크[l.차례] = false;
keep();
draw();
void make(h);
});
tools.push(poom, back);
}
tools.push(go);
}
const head = el("div", {
className: "ui-toolbar m02-hopyo__bar",
@@ -515,7 +559,16 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement {
...(calc && !calc.끝
? [note(`숫자 판정 셈 중 ${calc.한}/${calc.모두}${calc.까닭 ? ` · ${calc.까닭}` : ""}`)]
: []),
...(h.만든 ? [note(`만든 사전조합 ${h.만든.키}${h.만든.확인 ? " · 확인함" : ""}`)] : []),
...(h.만든
? [
note(
`만든 사전조합 ${h.만든.키}${h.만든.확인 ? " · 확인함" : ""}` +
(h.만든.품셈
? ` · 품셈 줄 ${h.만든.품셈.length} · 고정 줄 ${h.만든.고정 ?? 0}`
: ""),
),
]
: []),
createButton({
label: "엑셀 보기",
size: "sm",