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:
@@ -0,0 +1,152 @@
|
||||
/* =============================================================================
|
||||
* B09_Estimation_UI_BillRates.ts
|
||||
* 설계내역서 **할증율·일괄보정** 칸 — STmate `wBoqRate`·`wM_Rate` 본 (PLAN 12장 2차 ④)
|
||||
*
|
||||
* - 범위: 선택 항목만(줄마다 한 칸) / 내역서 전체(`*`) · 방식: 같은 비율 / 비목별(재·노·경)
|
||||
* - 비목마다 절사·반올림 × 소수 0~4 자리(안 고르면 원 미만 절사).
|
||||
* - ⚠ 여러 줄을 한꺼번에 바꾸므로 「할증 전부 되돌리기」를 둠 — 줄마다 ↺ 도 있음.
|
||||
* - 화면은 고른 값만 보냄 — 금액은 서버가 다시 셈(내역 한 벌을 새로 받음).
|
||||
* ========================================================================== */
|
||||
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { L, el, userMark } from "./B09_Estimation_UI_Sheet";
|
||||
import { loadEdits, saveEdits, type BillRowDto, type EditChange } from "./B09_Estimation_UI_Store";
|
||||
|
||||
const SECTION = "bill_rates";
|
||||
const PARTS = [
|
||||
["material", "B09_Sheet_Col_Material"],
|
||||
["labor", "B09_Sheet_Col_Labor"],
|
||||
["expense", "B09_Sheet_Col_Expense"],
|
||||
] as const;
|
||||
|
||||
function save(projectId: string, changes: EditChange[], reload: () => void): void {
|
||||
if (changes.length === 0) return;
|
||||
void saveEdits(projectId, changes)
|
||||
.then(() => {
|
||||
showToast(L("B09_Sheet_Saved"), "success");
|
||||
reload();
|
||||
})
|
||||
.catch((error: Error) => showToast(`${L("B09_Sheet_SaveFailed")} ${error.message}`, "error"));
|
||||
}
|
||||
|
||||
function numberBox(placeholder: string): HTMLInputElement {
|
||||
const input = el("input", "b09s-edit-input");
|
||||
input.type = "text";
|
||||
input.inputMode = "decimal";
|
||||
input.placeholder = placeholder;
|
||||
return input;
|
||||
}
|
||||
|
||||
function choice(options: Array<[string, string]>): HTMLSelectElement {
|
||||
const select = el("select");
|
||||
for (const [value, label] of options) {
|
||||
const option = el("option", "", label);
|
||||
option.value = value;
|
||||
select.append(option);
|
||||
}
|
||||
return select;
|
||||
}
|
||||
|
||||
/** 할증·일괄보정 칸 한 벌 — `selected` = 고른 줄의 할증 칸 이름. */
|
||||
export function rateForm(
|
||||
projectId: string,
|
||||
selected: Set<string>,
|
||||
reload: () => void,
|
||||
): HTMLElement {
|
||||
const box = el("div", "b09s-group");
|
||||
const scope = choice([
|
||||
["selected", L("B09_Sheet_Rate_Selected")],
|
||||
["all", L("B09_Sheet_Rate_All")],
|
||||
]);
|
||||
const way = choice([
|
||||
["same", L("B09_Sheet_Rate_Same")],
|
||||
["parts", L("B09_Sheet_Rate_Parts")],
|
||||
]);
|
||||
const same = numberBox("%");
|
||||
const parts = PARTS.map(([key, label]) => {
|
||||
const rate = numberBox("%");
|
||||
const mode = choice([
|
||||
["floor", L("B09_Sheet_Rate_Floor")],
|
||||
["round", L("B09_Sheet_Rate_Round")],
|
||||
]);
|
||||
const digits = choice(
|
||||
[0, 1, 2, 3, 4].map((n): [string, string] => [
|
||||
String(n),
|
||||
`${L("B09_Sheet_Rate_Digits")} ${n}`,
|
||||
]),
|
||||
);
|
||||
const line = el("span", "b09s-inline");
|
||||
line.append(el("span", "b09s-head", L(label)), rate, mode, digits);
|
||||
return { key, rate, mode, digits, line };
|
||||
});
|
||||
|
||||
const apply = el("button", "b09s-undo", L("B09_Sheet_Apply"));
|
||||
apply.type = "button";
|
||||
apply.addEventListener("click", () => {
|
||||
const rounding = Object.fromEntries(
|
||||
parts.map((part) => [part.key, { mode: part.mode.value, digits: Number(part.digits.value) }]),
|
||||
);
|
||||
const value =
|
||||
way.value === "same"
|
||||
? { all: same.value.trim() || "0", rounding }
|
||||
: {
|
||||
...Object.fromEntries(parts.map((part) => [part.key, part.rate.value.trim() || "0"])),
|
||||
rounding,
|
||||
};
|
||||
const keys = scope.value === "all" ? ["*"] : [...selected];
|
||||
if (keys.length === 0) {
|
||||
showToast(L("B09_Sheet_Rate_PickRows"), "info");
|
||||
return;
|
||||
}
|
||||
save(
|
||||
projectId,
|
||||
keys.map((key) => ({ section: SECTION, key, value })),
|
||||
reload,
|
||||
);
|
||||
});
|
||||
|
||||
const clear = el("button", "b09s-undo", L("B09_Sheet_Rate_ClearAll"));
|
||||
clear.type = "button";
|
||||
clear.addEventListener("click", () => {
|
||||
void loadEdits(projectId).then((stored) =>
|
||||
save(
|
||||
projectId,
|
||||
Object.keys(stored.edits[SECTION] ?? {}).map((key) => ({
|
||||
section: SECTION,
|
||||
key,
|
||||
value: null,
|
||||
})),
|
||||
reload,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
const head = el("div", "b09s-bar");
|
||||
head.append(
|
||||
el("span", "b09s-head", L("B09_Sheet_Rate")),
|
||||
scope,
|
||||
way,
|
||||
same,
|
||||
apply,
|
||||
clear,
|
||||
el("span", "b09s-hint", `${L("B09_Sheet_Rate_SelectedCount")} ${selected.size}`),
|
||||
);
|
||||
const partLine = el("div", "b09s-bar");
|
||||
partLine.append(...parts.map((part) => part.line));
|
||||
box.append(head, partLine);
|
||||
return box;
|
||||
}
|
||||
|
||||
/** 줄 비고의 할증 표시 — 그 줄 칸으로 얹혔으면 「사용자」+↺, 내역서 전체로 얹혔으면 표시만. */
|
||||
export function rateMark(
|
||||
projectId: string,
|
||||
row: BillRowDto,
|
||||
reload: () => void,
|
||||
): HTMLElement | null {
|
||||
if (!row.rate) return null;
|
||||
if (row.rate.source === "*")
|
||||
return el("span", "b09s-badge b09s-badge--user", L("B09_Sheet_Rate_AllBadge"));
|
||||
return userMark(() =>
|
||||
save(projectId, [{ section: SECTION, key: row.rate_key, value: null }], reload),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user