PLAN 8-4b 열 명세를 화면에 세움. 일감 2 의 마지막 덩어리(엔진 → API → 화면). 왜 실무 서식 그대로인가 이 화면의 첫 사용자는 「프로그램이 맞나」를 확인하려는 설계자임. 보기 좋게 재배치하면 실무 산출서와 눈으로 대조를 못 함. 열 순서·머리글 문구를 실무 토적표(= 오솔길 1.BOM 36열)에 맞춤. 소수 자리는 표기 규칙일 뿐 (PLAN 8-16) 서버는 전정밀 값을 주고 자르는 것은 화면뿐임. 실무 시트 관측대로 단면적·체적 2자리 · 보정량계·유용토·차인·누가 1자리 · 거리 정수. 원가 쪽(줄마다 원 단위 절사)과 규칙이 반대라 그 코드를 옮기지 말 것. 구성 좌측 = 산출 조건 읽기 전용(산출법·토량환산계수). 계수는 서버 상수가 유일 정의처라 화면이 값을 다시 적지 않음. 우측 = 시트 탭 + 표. 지금 서는 장은 토적표 하나, 나머지 6장은 차례로 붙임. 표는 한 번만 받아 좌측·우측이 함께 씀(중복 호출 없음). 측점 열·머리글은 sticky — 가로로 굴러도 어느 줄인지 보임. locale 은 B08 키 8개만 추가 (다른 창과 겹치는 파일이라 기존 줄 무접촉). 검증 — 공용 브라우저 실화면. 타입검사 오류 0. 탭 「토적표」, 3단 머리글 3행(9/5/14셀), 본문 65행 x 20열, 합계행 표시. 둘째 줄이 API 값과 일치: NO.1 거리 20 · 절토토사 1.96/22.34/20.10 · 암 2.54/25.43/29.24 · 측구 0.08/2.58/2.33 + 0.10/1.02/1.17 · 보정량계 52.8 · 성토 4.82/48.79 · 유용 48.8 · 차인 4.0 · 누가 4.0. ⚠ 어두운 테마에서 머리글이 묻히던 것을 프로젝트 테마 변수로 바꿔 고침 — 대비 실측 머리글 10.51:1 · 본문 11.32:1 (기준 4.5:1). 조작으로 바꾼 화면은 사용자가 보던 주소로 되돌려 놓음. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
182 lines
6.8 KiB
TypeScript
182 lines
6.8 KiB
TypeScript
/* =============================================================================
|
|
* B08_Quantity_UI_Page.ts
|
|
* 로그인 후 08: 5차 워크플로우 (수량 산출)
|
|
*
|
|
* 우측 = 실무 수량산출서의 시트를 탭으로 옮긴 것. 지금은 **토적표** 한 장이 서 있고
|
|
* 나머지(토적집계·구조물위치·수량집계표·총괄집계·수리계산·운반거리)는 차례로 붙인다.
|
|
* 확정 = 워크플로 stage 5(QUANTITY) 완료 처리 후 B09 설계도서로 이동.
|
|
* ========================================================================== */
|
|
|
|
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
|
import { createButton, showToast } from "@ui/ui_template_elements";
|
|
import { API_BASE_URL, CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
|
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
|
import { workflowSteps } from "../A00_Common/b_page_scaffold";
|
|
import {
|
|
fetchWorkflowState,
|
|
goToWorkflowStage,
|
|
WORKFLOW_STEP_ROUTES,
|
|
} from "../A00_Common/b_workflow_nav";
|
|
import { renderEarthworkGrid, type EarthworkTable } from "./B08_Quantity_UI_EarthworkGrid";
|
|
import { injectEarthworkGridStyles } from "./B08_Quantity_UI_EarthworkGrid_Style";
|
|
|
|
/** locale 헬퍼 */
|
|
function L(key: keyof typeof ui_locales): string {
|
|
return ui_locales[key][currentLanguageIndex];
|
|
}
|
|
|
|
/** stage 5(QUANTITY) 완료 요청 — 본문 미구현 상태의 유일한 백엔드 연동. */
|
|
async function confirmQuantityStage(projectId: string): Promise<void> {
|
|
const response = await fetch(
|
|
`${API_BASE_URL}/projects/${encodeURIComponent(projectId)}/quantity/confirm`,
|
|
{ method: "POST", credentials: "include" },
|
|
);
|
|
if (!response.ok) {
|
|
throw new Error(`quantity confirm failed: ${response.status}`);
|
|
}
|
|
}
|
|
|
|
/** 토적표를 받아 온다. 노선을 안 주면 워크플로가 보고 있는 최신 노선으로 나온다. */
|
|
async function fetchEarthworkTable(projectId: string): Promise<EarthworkTable> {
|
|
const response = await fetch(
|
|
`${API_BASE_URL}/projects/${encodeURIComponent(projectId)}/quantity/earthwork-table`,
|
|
{ credentials: "include" },
|
|
);
|
|
if (!response.ok) throw new Error(`earthwork table failed: ${response.status}`);
|
|
return (await response.json()) as EarthworkTable;
|
|
}
|
|
|
|
/** 좌측 패널의 한 줄 — 이름과 값. 산출 조건을 읽기 전용으로 보인다. */
|
|
function field(label: string, value: string): HTMLElement {
|
|
const row = document.createElement("div");
|
|
row.className = "b08-quantity__field";
|
|
const name = document.createElement("span");
|
|
name.textContent = label;
|
|
const amount = document.createElement("span");
|
|
amount.className = "b08-quantity__field-value";
|
|
amount.textContent = value;
|
|
row.append(name, amount);
|
|
return row;
|
|
}
|
|
|
|
/** 좌측 패널: 산출 조건(읽기 전용) + 하단 [확정] 액션 행. */
|
|
function buildQuantitySidePanel(
|
|
projectId: string | null,
|
|
table: EarthworkTable | null,
|
|
): HTMLElement {
|
|
const panel = document.createElement("div");
|
|
panel.className = "b08-quantity__panel";
|
|
|
|
// 계수는 서버 상수가 유일한 정의처다 — 화면은 보여 주기만 하고 값을 다시 적지 않는다.
|
|
panel.append(field(L("B08_Quantity_Side_Method"), L("B08_Quantity_Side_Method_Value")));
|
|
const entries = Object.entries(table?.conversion_factors ?? {});
|
|
if (entries.length) {
|
|
panel.append(field(L("B08_Quantity_Side_Factors"), ""));
|
|
for (const [kind, value] of entries) {
|
|
panel.append(field(kind, String((value as { compacted: number }).compacted)));
|
|
}
|
|
}
|
|
|
|
const confirmButton = createButton({
|
|
label: L("B08_Quantity_Btn_Confirm"),
|
|
variant: "filled",
|
|
onClick: () => {
|
|
if (!projectId) {
|
|
showToast(L("B08_Quantity_Confirm_Failed"), "error");
|
|
return;
|
|
}
|
|
confirmButton.disabled = true;
|
|
confirmQuantityStage(projectId)
|
|
.then(() => {
|
|
showToast(L("B08_Quantity_Confirm_Success"), "success");
|
|
goToWorkflowStage(projectId, WORKFLOW_STEP_ROUTES[6]);
|
|
})
|
|
.catch(() => {
|
|
showToast(L("B08_Quantity_Confirm_Failed"), "error");
|
|
confirmButton.disabled = false;
|
|
});
|
|
},
|
|
});
|
|
|
|
const actions = document.createElement("div");
|
|
actions.className = "b08-quantity__actions ui-sidebar-actions";
|
|
actions.append(confirmButton);
|
|
panel.append(actions);
|
|
return panel;
|
|
}
|
|
|
|
/** 우측 본문 — 시트 탭 + 그 장의 표. 지금 서 있는 장은 토적표 하나다. */
|
|
function buildQuantityBody(table: EarthworkTable | null, failed: boolean): HTMLElement {
|
|
const body = document.createElement("div");
|
|
body.className = "b08-quantity__body";
|
|
|
|
const tabs = document.createElement("div");
|
|
tabs.className = "b08-quantity__tabs";
|
|
const tab = document.createElement("button");
|
|
tab.type = "button";
|
|
tab.className = "b08-quantity__tab is-active";
|
|
tab.textContent = L("B08_Quantity_Tab_Earthwork");
|
|
tabs.append(tab);
|
|
body.append(tabs);
|
|
|
|
if (failed) {
|
|
const message = document.createElement("p");
|
|
message.className = "b08-quantity__message";
|
|
message.textContent = L("B08_Quantity_Grid_Failed");
|
|
body.append(message);
|
|
return body;
|
|
}
|
|
if (!table || !table.rows?.length) {
|
|
const message = document.createElement("p");
|
|
message.className = "b08-quantity__message";
|
|
message.textContent = L("B08_Quantity_Grid_Empty");
|
|
body.append(message);
|
|
return body;
|
|
}
|
|
body.append(renderEarthworkGrid(table));
|
|
return body;
|
|
}
|
|
|
|
/* -----------------------------------------------------------------------------
|
|
* 페이지 진입점
|
|
* -------------------------------------------------------------------------- */
|
|
export async function renderB08Quantity(root: HTMLElement): Promise<void> {
|
|
injectEarthworkGridStyles();
|
|
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
|
|
|
// 표는 한 번만 받아 좌측 패널(계수 표시)과 우측 그리드가 함께 쓴다.
|
|
let table: EarthworkTable | null = null;
|
|
let failed = false;
|
|
if (projectId) {
|
|
try {
|
|
table = await fetchEarthworkTable(projectId);
|
|
} catch {
|
|
failed = true;
|
|
}
|
|
}
|
|
|
|
let workflowState: Awaited<ReturnType<typeof fetchWorkflowState>> | undefined;
|
|
if (projectId) {
|
|
try {
|
|
workflowState = await fetchWorkflowState(projectId);
|
|
} catch {
|
|
/* 조회 실패 시 stages 미전달 → 전체 이동 허용 (다른 워크플로 페이지와 같음) */
|
|
}
|
|
}
|
|
|
|
const layout = createWorkflowLayout({
|
|
title: L("B08_Quantity_Title"),
|
|
steps: workflowSteps(),
|
|
activeStep: 5,
|
|
leftPanel: buildQuantitySidePanel(projectId, table),
|
|
mainContent: buildQuantityBody(table, failed),
|
|
stages: workflowState?.stages,
|
|
currentStage: workflowState?.current_stage,
|
|
routes: WORKFLOW_STEP_ROUTES,
|
|
onStepClick: (stepIndex: number) => {
|
|
if (projectId) goToWorkflowStage(projectId, WORKFLOW_STEP_ROUTES[stepIndex]);
|
|
},
|
|
});
|
|
root.append(layout.root);
|
|
}
|