refactor(B08,B09): 폴더째 old_code 로 옮기고 빈 화면 둘만 남김 (PLAN 7-3)

B08_Quantity 86 · B09_Estimation 111 파일을 old_code/ 로 옮김(지우지 않음).
화면은 메뉴·주소·단계 막대만 남은 빈 틀 둘 — main.py 라우터 13 개는 끊음.
B07 이 빌려 쓰던 비탈 길이·면적은 필요한 함수만 B07_DesignDetail_Engine_SlopeGeometry
로 옮겨 적음(면적 적분·노면 면적·측점 묶음은 안 옮김) · 시험 하나를 새로 둠.
B07 구조물도 조립(Cad_StandardSheet)은 2026-09-13 에 이미 도면 목록에서 빠져
부르는 곳이 없어 old_code 로 같이 보냄 — 구조물 그림은 되살리지 않음.
B06 구조물 몫 조회는 빈 값으로 두어 화면이 그대로 서게 함.
B08·B09 를 부르던 시험 25 개도 old_code/resources/tester 로 옮김.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAdA5ThqmtVSsbzjusk1cJ
This commit is contained in:
2026-09-22 12:27:45 +09:00
co-authored by Claude Opus 5
parent 185f800d23
commit 8472fc9f40
231 changed files with 1617 additions and 1216 deletions
@@ -0,0 +1,207 @@
/* =============================================================================
* B09_Estimation_UI_Detail.ts
* 호표 본표(일위대가표·단가산출근거·시간당 중기) 한 장 + **들어가기 자취** (PLAN 12장 1차)
*
* - ⭐ STmate 를 익숙하게 만드는 동작 = 타고 들어가기(브레인 판정). 내역 줄 → 제 N 호표 →
* 호표 안 줄(일위대가·단산·중기) → … 을 누를 때마다 자취가 쌓이고, 자취 글을 누르면 그 자리로 되돌아감.
* - 표 모양은 실무 `일위대가표`·`단가산출근거` 시트: 명칭·규격·수량·단위 · 합계/노무/재료/경비(단가·금액) · 비고.
* - ⚠ 합계 줄은 서버 값(호표 성분 소계 원 미만 절사) — 여기서 안 더함.
* ========================================================================== */
import type { B09TabContext } from "./B09_Estimation_UI_Shell_Types";
import {
L,
el,
hint,
linkButton,
moneyCells,
numberCell,
quantity,
sheetHead,
sheetTable,
} from "./B09_Estimation_UI_Sheet";
import { loadDetail, type DetailDto, type DetailRowDto } from "./B09_Estimation_UI_Store";
import {
addRowForm,
qFormulaInput,
quantityInput,
rowControls,
} from "./B09_Estimation_UI_DetailEdit";
interface Crumb {
tab: string;
code: string;
label: string;
}
const crumbs: Crumb[] = [];
let drilling = false;
/** 본표 안 줄을 눌러 들어갈 때 — 자취를 이어 쌓음. 목록·내역에서 들어가면 자취가 새로 시작. */
export function drill(ctx: B09TabContext, tab: string, code: string): void {
drilling = true;
ctx.open(tab, code);
}
function visit(tab: string, code: string, label: string): void {
if (!drilling) crumbs.length = 0;
drilling = false;
const at = crumbs.findIndex((crumb) => crumb.tab === tab && crumb.code === code);
if (at >= 0) crumbs.length = at;
crumbs.push({ tab, code, label });
}
function trail(ctx: B09TabContext): HTMLElement {
const box = el("div", "b09s-trail");
crumbs.forEach((crumb, index) => {
if (index > 0) box.append(el("span", "b09s-hint", "›"));
if (index === crumbs.length - 1) {
box.append(el("span", "b09s-title", crumb.label));
return;
}
box.append(linkButton(crumb.label, () => drill(ctx, crumb.tab, crumb.code)));
});
return box;
}
/** 줄이 가리키는 곳 — 단산(D)은 단가산출근거 탭, 시간당 중기(X)는 중기 탭, 일위대가는 일위대가 탭. */
function targetTab(row: DetailRowDto): string | null {
if (!row.drillable || !row.ref_code) return null;
if (row.kind === "price_basis") return "price_basis";
return row.kind === "machine_hourly" ? "machine" : "unit_price";
}
/** 편집 칸을 켰는가 — 탭을 오가도 남음(본표 한 장 단위가 아니라 화면 단위). */
let editing = false;
interface EditHooks {
projectId: string;
reload: () => void;
}
function detailRow(row: DetailRowDto, ctx: B09TabContext, hooks: EditHooks | null): HTMLElement {
const tr = el(
"tr",
row.edit?.removed ? "is-removed" : row.edit?.user || row.edit?.q_formula ? "is-user" : "",
);
const edit = row.edit;
const quantityCell =
hooks && edit && editing && row.unit !== "%" ? el("td") : numberCell(quantity(row.quantity));
if (hooks && edit && editing && row.unit !== "%") {
quantityCell.append(quantityInput(hooks.projectId, edit, row.quantity, hooks.reload));
}
// 비율 줄(잡품·공구손료·제잡비)은 단가 칸에 **밑수**가 옴 — 「21 % × 주연료비 6,365」 꼴(실무 표).
const unit: [string, string, string, string] = [
row.unit_total ?? "",
row.unit_labor ?? "",
row.unit_material ?? "",
row.unit_expense ?? "",
];
tr.append(
el("td", "", row.name),
el("td", "", row.spec),
quantityCell,
el("td", "", row.unit),
...moneyCells(unit, [row.total, row.labor, row.material, row.expense]),
);
const note = el("td", "b09s-note");
if (hooks && edit && (editing || edit.user)) {
note.append(rowControls(hooks.projectId, edit, hooks.reload, editing));
}
if (hooks && edit?.output && (editing || edit.q_formula)) {
note.append(qFormulaInput(hooks.projectId, edit, hooks.reload));
}
const source = row.source_label || row.source || "";
if (source) note.append(el("span", "b09s-hint", `[${source}] `));
if (row.note) note.append(el("span", "b09s-formula", row.note));
tr.append(note);
const tab = targetTab(row);
if (tab && row.ref_code) {
const code = row.ref_code;
tr.classList.add("is-clickable");
tr.title = L("B09_Sheet_Drill");
tr.addEventListener("click", () => drill(ctx, tab, code));
}
return tr;
}
function drawDetail(
ctx: B09TabContext,
box: HTMLElement,
detail: DetailDto,
label: string,
reload: () => void,
): void {
const title = el(
"div",
"b09s-title b09s-inline",
`${label} ${detail.name}${detail.spec ? ` · ${detail.spec}` : ""}`,
);
if (detail.unit) title.append(el("span", "b09s-hint", ` (${detail.unit})`));
const hooks = detail.editable && ctx.projectId ? { projectId: ctx.projectId, reload } : null;
if (hooks) {
// 2차 편집(PLAN 12장) — 구성행 수정 · Q 식 편집. 프로젝트 단위로 저장, 금액은 서버가 다시 셈.
const toggle = el(
"button",
"b09s-undo",
editing ? L("B09_Sheet_EditDone") : L("B09_Sheet_Edit"),
);
toggle.type = "button";
toggle.addEventListener("click", () => {
editing = !editing;
reload();
});
title.append(toggle);
}
const { wrap, tbody } = sheetTable(
sheetHead([
L("B09_Sheet_Col_Name"),
L("B09_Sheet_Col_Spec"),
L("B09_Sheet_Col_Quantity"),
L("B09_Sheet_Col_Unit"),
]),
);
for (const row of detail.rows) tbody.append(detailRow(row, ctx, hooks));
const sum = el("tr", "is-sum");
sum.append(el("td", "", L("B09_Sheet_Sum")), el("td"), el("td"), el("td"));
sum.append(
...moneyCells(null, [detail.total, detail.labor, detail.material, detail.expense]),
el("td"),
);
tbody.append(sum);
box.append(title, wrap);
if (hooks && editing && detail.next_add_key) {
box.append(addRowForm(hooks.projectId, detail.code, detail.next_add_key, reload));
}
if (detail.unattached_note) box.append(hint(detail.unattached_note.replace(/\*\*/g, ""), true));
if (detail.known_gap_note) box.append(hint(detail.known_gap_note, true));
if (detail.form_basis_note) box.append(hint(detail.form_basis_note, true));
}
/** 본표 한 장을 `box` 에 — 자취를 쌓고 서버 본표를 받아 그림. */
export function renderDetail(
ctx: B09TabContext,
box: HTMLElement,
tab: string,
code: string,
label: string,
): void {
if (!ctx.projectId) return;
visit(tab, code, label);
const projectId = ctx.projectId;
const load = (): void => {
box.replaceChildren(trail(ctx), hint(L("B09_Sheet_Loading")));
loadDetail(projectId, code)
.then((detail) => {
box.replaceChildren(trail(ctx));
drawDetail(ctx, box, detail, label, load);
})
.catch((error: Error) => {
box.replaceChildren(
trail(ctx),
hint(`${L("B09_Sheet_LoadFailed")} ${error.message}`, true),
);
});
};
load();
}