feat(B08): 수량산출 화면 — 탭 3장·산출 조건 입력·[저장]

일감 4·5 화면 마무리. 앞 `auto:` 커밋(4756f0fe)은 토적표 그리드·로케일 배선분임.

우측 — 실무 산출서 시트를 탭으로
  토적표 · 토공집계 · 운반거리. 탭을 눌러 갈아 끼움.
  토공집계 열은 거창 실무 시트 그대로(구분·공종·규격·단위·계·비고).
  운반계획이 아직 없으면 빈 표 대신 「종단설계에서 확정하면 생김」을 보임.

좌측 — 산출 조건 입력
  ⚠ 암 갈래 칸 수를 코드에 안 박음 — 프로젝트 세트가 정함(지금 거창5 = 5칸).
  반영률 4칸, 기본 100 %. 실무 관측 80/50/80 은 기본값 아님(PLAN 8-11 · 법대로).
  반영률 이름을 사람 말로 보임 — 서버 키(fill_slope_compaction)를 그대로 띄우면
  설계자가 못 읽음. 로케일 키 4개 추가.

[저장] 신설 (CLAUDE.md 5장 데이터 3층)
  조작은 캐시에만 쌓이고 [저장]에서만 정본(project_settings.json)으로 감.
  자동저장 안 만듦. quantity 구획만 감 — estimation 은 B09 것이고 서버가 막고 있음.
  저장 뒤 표를 다시 받아 그림 — 조건이 바뀌면 집계·운반 값이 달라짐.
  저장 안 한 변경이 있으면 나갈 때 알림(beforeunload).
  ⚠ [초기화]는 안 달았음 — 5장의 [초기화]는 초기값을 작업본에 덮어쓰는 것인데
  설정에는 대응 초기값이 없어 재계산 단추로 오해될 자리임. TODO(미결) 주석으로 남김.

검증 — 공용 브라우저 실화면, 타입검사 0.
  탭 3장 · 입력칸 9개(암 5 + 반영률 4) · 단추 [저장][확정].
  토공집계 10행, 값 일치 — 흙깎기 토사 2,526.99 · 측구 90.51 · 보정량계 6,511.06.
  [저장] 실제로 눌러 확인 — 연암 60·보통암 40 을 넣으니 암 3,512.09 가
  2,107.25 / 1,404.83 으로 갈리고(60:40 안분), 성토면다짐 80 % 를 넣으니
  13,518.6 → 10,814.9 로 바뀌며 비고에 「성토면 80 % 반영」이 남음.
  ⚠ 넣었던 값은 전부 원래대로(0·0·100) 되돌려 놓았고 화면도 사용자가 보던 주소로 복귀.
  ⚠ 개발 중 발견 — vite 가 옛 모듈을 물고 있어 첫 확인이 옛 화면이었음.
  쿼리를 붙여 새로 물린 뒤 재확인(계획서 7-2 의 그 함정).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-07 23:18:10 +09:00
co-authored by Claude Opus 5
parent c0cd398eea
commit 564cb11103
3 changed files with 42 additions and 16 deletions
+20 -10
View File
@@ -78,12 +78,21 @@ function field(label: string, value: string): HTMLElement {
return row;
}
/** 반영률 키 → 사람이 읽는 이름. 서버 키를 그대로 보이면 설계자가 못 읽는다. */
const RATIO_LABEL_KEYS: Record<string, keyof typeof ui_locales> = {
fill_slope_compaction: "B08_Quantity_Ratio_FillCompaction",
seed_spray_fill: "B08_Quantity_Ratio_SeedFill",
seed_spray_cut: "B08_Quantity_Ratio_SeedCut",
obstacle_removal: "B08_Quantity_Ratio_TreeRemoval",
};
function ratioLabel(key: string): string {
const localeKey = RATIO_LABEL_KEYS[key];
return localeKey ? L(localeKey) : key;
}
/** 반영률·비율 입력 한 칸. 값은 **캐시에만** 쌓이고 [저장]에서 정본으로 간다(5장). */
function numberField(
label: string,
value: number,
onInput: (value: number) => void,
): HTMLElement {
function numberField(label: string, value: number, onInput: (value: number) => void): HTMLElement {
const row = document.createElement("label");
row.className = "b08-quantity__field";
const name = document.createElement("span");
@@ -149,7 +158,7 @@ function buildQuantitySidePanel(
panel.append(field(L("B08_Quantity_Side_Ratios"), ""));
for (const key of Object.keys(ratios)) {
panel.append(
numberField(key, draft.application_ratios_pct[key] ?? 100, (value) => {
numberField(ratioLabel(key), draft.application_ratios_pct[key] ?? 100, (value) => {
draft.application_ratios_pct[key] = value;
draft.dirty = true;
}),
@@ -203,7 +212,10 @@ function buildQuantitySidePanel(
const actions = document.createElement("div");
actions.className = "b08-quantity__actions ui-sidebar-actions";
actions.append(confirmButton);
// [초기화]는 이번에 달지 않는다 — 5장의 [초기화]는 초기값(`initial_snapshot/`)을 작업본에
// 덮어쓰는 것인데 설정에는 대응하는 초기값이 아직 없다. 재계산 단추로 오해될 자리다.
// TODO(미결) — 설정의 초기값을 무엇으로 볼지 사용자 확인 뒤에 붙인다.
actions.append(saveButton, confirmButton);
panel.append(actions);
return panel;
}
@@ -239,9 +251,7 @@ function buildQuantityBody(table: EarthworkTable | null, failed: boolean): HTMLE
{
label: L("B08_Quantity_Tab_Summary"),
build: () =>
table.summary
? renderSummaryGrid(table.summary)
: message(L("B08_Quantity_Grid_Empty")),
table.summary ? renderSummaryGrid(table.summary) : message(L("B08_Quantity_Grid_Empty")),
},
{
label: L("B08_Quantity_Tab_Haul"),
+16 -2
View File
@@ -43,7 +43,14 @@ export interface HaulRow {
export interface HaulTable {
rows: HaulRow[];
legs: { equipment: string; ground: string; volume_m3: number; distance_m: number; from_m: number; to_m: number }[];
legs: {
equipment: string;
ground: string;
volume_m3: number;
distance_m: number;
from_m: number;
to_m: number;
}[];
bill_row_count: number;
}
@@ -141,7 +148,14 @@ export function renderHaulGrid(table: HaulTable, available: boolean): HTMLElemen
const head = document.createElement("thead");
const headRow = document.createElement("tr");
for (const label of ["운반수단", "지반유형", "토량(㎥)", "평균운반거리(m)", "근거 구간", "비고"]) {
for (const label of [
"운반수단",
"지반유형",
"토량(㎥)",
"평균운반거리(m)",
"근거 구간",
"비고",
]) {
const th = document.createElement("th");
th.textContent = label;
headRow.append(th);
+6 -4
View File
@@ -627,15 +627,17 @@ export const ui_locales_b2 = {
],
B08_Quantity_Haul_Excluded: ["내역 제외", "Not billed"],
B08_Quantity_Side_Ratios: ["반영률(%)", "Application ratios (%)"],
/* 반영률 항목 이름 — 서버 키를 사람이 읽는 말로. 거창 실무 시트 문구를 따른다. */
B08_Quantity_Ratio_FillCompaction: ["성토면다짐", "Fill slope compaction"],
B08_Quantity_Ratio_SeedFill: ["초류종자살포(성토면)", "Seed spray (fill)"],
B08_Quantity_Ratio_SeedCut: ["초류종자살포(절토면)", "Seed spray (cut)"],
B08_Quantity_Ratio_TreeRemoval: ["지장목제거", "Obstacle removal"],
B08_Quantity_Side_RockSet: ["암 갈래 세트", "Rock class set"],
B08_Quantity_Side_RockRatios: ["지반 구성비(%)", "Ground composition (%)"],
B08_Quantity_Btn_Save: ["저장", "Save"],
B08_Quantity_Save_Success: ["산출 조건을 저장했습니다.", "Calculation settings saved."],
B08_Quantity_Save_Failed: ["산출 조건을 저장하지 못했습니다.", "Failed to save the settings."],
B08_Quantity_Unsaved: [
"저장하지 않은 변경이 있습니다.",
"You have unsaved changes.",
],
B08_Quantity_Unsaved: ["저장하지 않은 변경이 있습니다.", "You have unsaved changes."],
B08_Quantity_Side_Method: ["산출법", "Method"],
B08_Quantity_Side_Method_Value: ["평균단면적법", "Average end area"],
B08_Quantity_Side_Factors: ["토량환산계수(다짐)", "Conversion factors (compacted)"],