Files
Aislo/B08_Quantity/B08_Quantity_UI_StructureSheet_UnitPrice.ts
T
eomsangdonandClaude Opus 5 475ad7875e feat(b08·b05·b09): 10-A 우리가 정한 SW 규칙을 화면에 드러냄 — 훑기 21건 중 안 보이거나 반만 보이던 13건 채움 + 9-21 제근 굴착기 크기 칸
- 문구 못박음 셋: ① 옹벽 높이 칸 밑 「기본값 · 소광리 도면 H=2.0 · 바꿀 수 있음」(등록부 default_basis 를 모든 칸 밑 회색 한 줄로) · ② 일반관리비 「임도가 어느 쪽인지 규정이 없어 (주)공사 기본 · 칸에서 바꿀 수 있음」 · ③ 모르타르 배합 「산림품셈에 배합 절이 없어 건설품셈 [건축] 9-1-1 을 씀(교차 참조)」
- B08: 막자갈 비고 「칸을 새로 두는 것은 B05 등록부 몫」 · 일위대가 머리 5단 한도 · 야면석 계수 칸 밑 「돌 종류는 그대로」 · 식 저장 전 상태 줄 「제원을 바꿔도 따라감」 · 양식 장 머리 「반올림은 m당 값에 걸고 수량 = m당 × 연장」
- 산출 조건 제근 굴착기 크기 칸(임목축적 등급 밑 · 「안 정함」 · 회색 제안 0.7㎥ + 근거 + [제안값 넣기] 누른 때만) — 936be972 소림 + 0.7 → 뿌리뽑기 55원/㎡ × 17,873.80㎡ = 983,057원(되돌림)
- 라이브러리 이름표: [내 라이브러리에 저장]·[발행] 창이 장 이름(종류 + 제원 요약)을 채워 묻고 고친 이름으로 저장 · 취소면 안 씀 · 비우면 양식 이름
- B05 계곡 통과 시설 폼 머리 「[저장]은 이 폼에 있는 칸만 바꿈 — 폼에 없는 칸은 그대로 둠」 · B09 폐기물처리비 자리 칸 밑 「법정경비 밑수에는 안 넣음」 · 내역서 「금액을 못 세운 줄」 첫 줄 「사유는 아래 단계가 낸 것을 그대로 옮김」
- 남은 하나(표 형태 66표 까닭 form_basis 화면)는 자원 축 파일을 거쳐야 해 브레인 물음

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
2026-09-14 19:44:18 +09:00

191 lines
6.6 KiB
TypeScript

/* =============================================================================
* B08_Quantity_UI_StructureSheet_UnitPrice.ts
* 구조물도 장 아래 **일위대가 표**(미리보기) — PLAN 3장 하단 ①②③.
*
* ⚠ 값을 셈하지 않음 — 서버(`…/structure-sheets/unit-price`)가 B09 단가표로 낸 금액을 적기만.
* ⚠ 장 조회와 따로 받음 — 단가표 첫 조립이 십여 초라 표가 늦게 차도 위 수량표는 먼저 보임.
* ⚠ 막힌 줄은 0 이 아니라 까닭을 적고, 하나라도 있으면 합계 앞에 「미완」.
* ⚠ 수동 단가 줄은 금액 칸 빨간 테두리 + 머리 「미확정 N건」 배지(PLAN 확정 ⑦).
* ========================================================================== */
import { API_BASE_URL } from "@config/config_frontend";
import { el, num } from "./B08_Quantity_UI_StructureSheet_Formula";
import {
unitPriceEditor,
type UnitPriceEditorData,
} from "./B08_Quantity_UI_StructureSheet_UnitPriceEdit";
interface UnitPriceRow {
seq: number;
name: string;
spec: string;
ref_code: string;
unit: string;
quantity: number | null;
skipped: boolean;
reason: string;
material?: number;
labor?: number;
expense?: number;
total?: number;
manual?: boolean;
manual_source?: string;
manual_entered_at?: string;
}
interface UnitPriceTable {
code: string;
name: string;
unit: string;
rows: UnitPriceRow[];
material: number;
labor: number;
expense: number;
total: number;
blocked: number;
complete: boolean;
unconfirmed: number;
}
/** 금액 칸 — 0.1원 자리까지(금액란 규칙). 못 푼 줄은 빈칸. */
function won(value: number | undefined): string {
return value === undefined ? "" : num(value, 1);
}
/** 장 아래 일위대가 칸 — 받는 동안 안내를 두고, 오면 표로 갈음. 저장 뒤엔 다시 받음. */
export function unitPriceSection(
projectId: string,
sheetKey: string,
sheetRows: { no: number; name: string; unit: string }[],
): HTMLElement {
const wrap = el("div", "b08-grid");
const load = async (): Promise<void> => {
wrap.replaceChildren(
el("p", "b08-grid__caption", "일위대가(미리보기) 불러오는 중… 단가표 첫 조립은 십여 초"),
);
try {
const response = await fetch(
`${API_BASE_URL}/projects/${encodeURIComponent(projectId)}/quantity/structure-sheets/unit-price?sheet_key=${encodeURIComponent(sheetKey)}`,
{ credentials: "include" },
);
const payload = (await response.json().catch(() => ({}))) as {
unit_price?: UnitPriceTable | null;
editor?: UnitPriceEditorData;
message?: string;
};
if (!response.ok) throw new Error(payload.message ?? `HTTP ${response.status}`);
const table = payload.unit_price;
const editor = payload.editor;
if (!table || !editor) {
wrap.replaceChildren(el("p", "b08-grid__caption", "이 양식에는 일위대가 줄이 아직 없음"));
return;
}
const open = el("button", "", editor.edited ? "줄 고치기 (고친 조합)" : "줄 고치기");
open.type = "button";
open.style.alignSelf = "flex-start";
const view = table.rows.length
? render(table)
: [el("p", "b08-grid__caption", "일위대가 줄이 아직 없음 — [줄 고치기]로 더함")];
open.addEventListener("click", () => {
open.disabled = true;
const panel = unitPriceEditor({
projectId,
sheetKey,
sheetRows,
data: editor,
onSaved: () => void load(),
onClose: () => {
panel.remove();
open.disabled = false;
},
});
wrap.append(panel);
});
wrap.replaceChildren(...view, open);
} catch (error) {
wrap.replaceChildren(
el(
"p",
"b08-grid__caption b08-grid__caption--warn",
`일위대가를 불러오지 못함 — ${error instanceof Error ? error.message : ""}`,
),
);
}
};
void load();
return wrap;
}
function render(table: UnitPriceTable): HTMLElement[] {
const total = table.complete
? `${num(table.total, 0)}원`
: `미완 — 막힌 줄 ${table.blocked} · 선 줄만 ${num(table.total, 0)}원`;
const head = el(
"p",
"b08-sheet__head",
`일위대가 ${table.code} · ${table.unit}${total} (미리보기 — 내역 금액은 원가계산이 셈 · 하위 일위대가는 5단까지 풀고 더 깊거나 돌면 막힘)`,
);
if (table.unconfirmed) {
head.append(el("span", "b08-unit__badge", `미확정 ${table.unconfirmed}건`));
}
const scroller = el("div", "b08-grid__scroll");
const grid = el("table", "b08-grid__table b08-grid__table--summary");
const headRow = document.createElement("tr");
for (const label of [
"공종·자원",
"코드",
"수량",
"단위",
"재료비",
"노무비",
"경비",
"합계",
"비고",
]) {
headRow.append(el("th", "", label));
}
const thead = document.createElement("thead");
thead.append(headRow);
const tbody = document.createElement("tbody");
for (const row of table.rows) {
const tr = document.createElement("tr");
const manual = row.manual
? `수동 단가 — ${row.manual_source || "출처 없음"} · ${row.manual_entered_at ?? ""}`
: "";
const note = row.skipped ? `안 섬 — ${row.reason}` : row.reason ? `⚠ ${row.reason}` : manual;
// 단가표 이름에 갈래가 이미 들었거나 규격 칸이 코드 자체면 겹쳐 적지 않음.
const spec =
row.spec && !row.name.includes(row.spec) && !row.ref_code.includes(row.spec) ? row.spec : "";
const money = [row.material, row.labor, row.expense, row.total].map((value) => {
const cell = el("td", row.manual ? "b08-unit__manual" : "", won(value));
if (row.manual) cell.title = manual;
return cell;
});
tr.append(
el("td", "", spec ? `${row.name} (${spec})` : row.name),
el("td", "", row.ref_code),
el("td", "", row.quantity === null ? "" : num(row.quantity, 3)),
el("td", "", row.unit),
...money,
el("td", "", note),
);
tbody.append(tr);
}
const foot = document.createElement("tr");
foot.append(
el("td", "", "계"),
el("td", "", ""),
el("td", "", ""),
el("td", "", ""),
el("td", "", won(table.material)),
el("td", "", won(table.labor)),
el("td", "", won(table.expense)),
el("td", "", table.complete ? num(table.total, 0) : `미완 ${num(table.total, 0)}`),
el("td", "", ""),
);
tbody.append(foot);
grid.append(thead, tbody);
scroller.append(grid);
return [head, scroller];
}