Merge branch 'sub_laptop_2' of https://gitea.chemifactory.com/eomsangdon/Aislo into main_laptop_1
This commit is contained in:
@@ -12,12 +12,15 @@
|
||||
* [저장] 한 번에 같이 PUT(`…_UI_Milestone_State`)
|
||||
* 조작 = 캐시 `b08-options` 즉시 + 열린 표가 같은 식으로 바로 다시 셈(`…_Options_Calc`) ·
|
||||
* [저장] = PUT …/quantity/options → 캐시 비움 · 열린 탭을 서버 정본으로 다시 그림 · 자동 저장 없음.
|
||||
* [초기화](요구 298-2) = 확인 창 → POST …/quantity/reset(사용자 값 · 설정 지움 · 1본 · 이정 그림 남김) →
|
||||
* 초안 넷 비움 → 열린 탭을 지금 B05 · B06 저장본 셈 값으로 다시 그림.
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
createButton,
|
||||
createInputField,
|
||||
hideLoadingOverlay,
|
||||
showConfirmDialog,
|
||||
showLoadingOverlay,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
@@ -27,7 +30,7 @@ import { clearState, readState, writeState } from "../A00_Common/b_page_state";
|
||||
import { request } from "./B08_Quantity_UI_Hydro";
|
||||
import { clampPct } from "./B08_Quantity_Options_Calc";
|
||||
import { PANEL_SURCHARGE, type Cells } from "./B08_Quantity_Material_Calc";
|
||||
import { saveMilestone } from "./B08_Quantity_UI_Milestone_State";
|
||||
import { loadMilestone, saveMilestone } from "./B08_Quantity_UI_Milestone_State";
|
||||
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
return ui_locales[key][currentLanguageIndex];
|
||||
@@ -228,15 +231,50 @@ export function createOptionsPanel(
|
||||
}
|
||||
};
|
||||
|
||||
// [초기화] — 연암 · 할증 · 재료 칸 · 수리 · 이정 지움 → 기준 = 지금 앞 단계 셈 값(떠 두지 않음)
|
||||
const resetAll = async (): Promise<void> => {
|
||||
if (!projectId) return;
|
||||
const message =
|
||||
"수량 산출에서 고친 값과 설정(연암 · 할증 · 자재 표 칸 · 수리 · 거리이정표)을 모두 지우고 지금 노선 · 횡단 설계로 다시 셈합니다. 배관 1본 길이 · 올린 이정 그림은 남습니다.";
|
||||
if (!(await showConfirmDialog(message, "초기화"))) return;
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
const body = await request<SavedOptions>(`/projects/${projectId}/quantity/reset`, {
|
||||
method: "POST",
|
||||
});
|
||||
saved = {
|
||||
one: body.one_pipe_length_m,
|
||||
soft: body.rock_ratios_pct.연암,
|
||||
surcharge: body.material_surcharge_pct,
|
||||
};
|
||||
for (const name of ["b08-options", "b08-materials", "b08-hydro", "b08-milestone"] as const)
|
||||
clearState(name, projectId);
|
||||
await loadMilestone(projectId, true);
|
||||
show();
|
||||
showToast("수량 산출을 초기값으로 되돌렸습니다.", "success");
|
||||
onSaved();
|
||||
} catch (error) {
|
||||
showToast(`초기화하지 못했습니다 — ${String(error)}`, "error");
|
||||
} finally {
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
};
|
||||
|
||||
const save = createButton({
|
||||
label: L("B08_Quantity_Btn_Save"),
|
||||
variant: "filled",
|
||||
onClick: () => void saveAll(),
|
||||
});
|
||||
save.dataset.b08OptionsSave = "";
|
||||
const reset = createButton({
|
||||
label: L("Common_Btn_Reset"),
|
||||
variant: "danger-ghost",
|
||||
onClick: () => void resetAll(),
|
||||
});
|
||||
reset.dataset.b08OptionsReset = "";
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "ui-sidebar-actions";
|
||||
actions.append(save);
|
||||
actions.append(save, reset);
|
||||
|
||||
const panel = document.createElement("div");
|
||||
panel.className = "ui-sidebar-panel b08-options";
|
||||
|
||||
@@ -8,19 +8,20 @@
|
||||
* 옛 수량 산출 코드(`old_code/B08_Quantity/`)는 가져오지 않음 — 새로 씀.
|
||||
* 좌측 패널 = 메인 탭과 같은 컨테이너 셋 + [저장] 줄(`B08_Quantity_UI_Options` · PLAN 122).
|
||||
* 단계 이동(☰ · 진행 단계 · [<] [>]) = 보이는 화면(좌측 옵션 + 지금 탭) 저장 성공 뒤 · 실패면 머묾(481-2).
|
||||
* B09 쪽 이동 = 저장 뒤 POST …/quantity/complete(단계 5 완료 · 요구 298-2 · 300 B1) 성공 뒤.
|
||||
* ========================================================================== */
|
||||
|
||||
import "@ui/ui_template_general_layout.css";
|
||||
import "./B08_Quantity_UI_Page.css";
|
||||
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { createButton, showToast } from "@ui/ui_template_elements";
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
||||
import { workflowSteps } from "../A00_Common/b_page_scaffold";
|
||||
import { goToWorkflowStage, WORKFLOW_STEP_ROUTES } from "../A00_Common/b_workflow_nav";
|
||||
import { renderStructureSummary } from "./B08_Quantity_UI_Summary";
|
||||
import { renderMaterialSheet } from "./B08_Quantity_UI_Material";
|
||||
import { renderHydro } from "./B08_Quantity_UI_Hydro";
|
||||
import { renderHydro, request } from "./B08_Quantity_UI_Hydro";
|
||||
import { renderEarthwork } from "./B08_Quantity_UI_Earthwork";
|
||||
import { createOptionsPanel } from "./B08_Quantity_UI_Options";
|
||||
import { renderMilestone } from "./B08_Quantity_UI_Milestone";
|
||||
@@ -165,9 +166,17 @@ export function renderB08Quantity(root: HTMLElement): void {
|
||||
routes: WORKFLOW_STEP_ROUTES,
|
||||
onStepClick: (stepIndex: number) => {
|
||||
if (!projectId) return;
|
||||
void saveShown().then(
|
||||
(ok) => ok && goToWorkflowStage(projectId, WORKFLOW_STEP_ROUTES[stepIndex]),
|
||||
);
|
||||
void (async () => {
|
||||
if (!(await saveShown())) return;
|
||||
// B09 쪽 = 단계 5 완료 뒤(요구 300 B1) · 실패면 머묾
|
||||
if (stepIndex > 5)
|
||||
try {
|
||||
await request(`/projects/${projectId}/quantity/complete`, { method: "POST" });
|
||||
} catch (error) {
|
||||
return showToast(`단계를 마치지 못했습니다 — ${String(error)}`, "error");
|
||||
}
|
||||
goToWorkflowStage(projectId, WORKFLOW_STEP_ROUTES[stepIndex]);
|
||||
})();
|
||||
},
|
||||
});
|
||||
root.append(layout.root);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* ① [박스보기|호표보기] 묶음 탭 셋 보기(304-1 · 한 값)
|
||||
* ② [하이퍼링크|팝업] 행 누름(303 · 기본 팝업)
|
||||
* ③ `DRAFT.actions` 단추(초안 · 비면 줄 없음)
|
||||
* ④ [?][저장][내역 초기화](도움말 줄 `data-help-row`)
|
||||
* ④ [?][<][>][초기화](도움말 줄 `data-help-row` · 요구 298-2 · 301)
|
||||
*
|
||||
* 스위치 값 = pref global(계정 공용 · [저장] 필요 없음) · 바닥 고정 = 공용 `.ui-sidebar-actions` 한 벌
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
*
|
||||
* 원본 = 기번3 공통 원가산출 엑셀 · 탭 하나 = 화면 표 하나(`…_UI_Tabs` 차례) · 옛 원가 코드 안 봄
|
||||
* 자료 = GET …/estimation(입력 · 사용자 값 · 서버가 셈한 표) · 조작 = 캐시(`b09-estimation`) +
|
||||
* 짝 계산(`…_Calc_*.ts`)으로 바로 다시 셈 · [저장] = PUT → 서버 정본 · [업데이트] = POST …/prices
|
||||
* 짝 계산(`…_Calc_*.ts`)으로 바로 다시 셈 · 저장(PUT → 서버 정본) = 단계 이동 · [>] 때(481-2 · [저장] 단추 없음 · 요구 301)
|
||||
* [>] = 고친 값 저장 → POST …/estimation/complete(단계 6 완료) → `DRAFT.next`(인쇄 미리보기 · 초안) · 실패면 머묾
|
||||
* [초기화](요구 298-2) = 확인 창 → POST …/estimation/reset(사용자 값 · 단가 책 지움 · 견적서 남김) → 셈 값 · [업데이트] = POST …/prices
|
||||
* 호표 칸 누름 = 그 탭 그 줄로 이동 · 자동 저장 없음
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -302,10 +304,11 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
}
|
||||
};
|
||||
|
||||
const saveUser = (): Promise<boolean> =>
|
||||
run("/estimation", "PUT", "원가 사용자 값을 저장했습니다.", { user });
|
||||
const save = createButton({ label: "저장", variant: "filled", onClick: () => void saveUser() });
|
||||
save.dataset.b09Save = "";
|
||||
// 481-2 고친 값만 저장 · 불러오기 전 · 고침 없음 = 요청 없이 참
|
||||
const saveChanged = (): Promise<boolean> =>
|
||||
payload && projectId && readState("b09-estimation", projectId)
|
||||
? run("/estimation", "PUT", "원가 사용자 값을 저장했습니다.", { user })
|
||||
: Promise.resolve(true);
|
||||
const refresh = createButton({
|
||||
label: "업데이트",
|
||||
variant: "ghost",
|
||||
@@ -320,26 +323,24 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
});
|
||||
refresh.dataset.b09Refresh = "";
|
||||
const check = createCheckPanel(refresh, (link) => show(link.tab, link.key));
|
||||
// 184-3 내역 초기화 — 설계내역서 덮개(user.bill) 지우고 바로 저장 → 기본 로직 내역
|
||||
// 298-2 [초기화] — 내역 · 단가 · 공기 · 요율 등 사용자 값 · 설정 모두 지움 → B08 수량 셈 값(견적서 파일 남김)
|
||||
const reset = createButton({
|
||||
label: "내역 초기화",
|
||||
variant: "ghost",
|
||||
label: L("Common_Btn_Reset"),
|
||||
variant: "danger-ghost",
|
||||
onClick: () =>
|
||||
void showConfirmDialog(
|
||||
"설계내역서에서 고친 칸 · 더한 줄 · 지운 줄 · 차례를 모두 지우고 기본 로직 내역으로 되돌립니다. 바로 저장됩니다.",
|
||||
"원가 계산에서 고친 값과 설정(설계내역서 · 단가 · 공기 · 요율 등)을 모두 지우고 지금 수량 산출 값으로 다시 셈합니다. 올린 견적서 파일은 남습니다.",
|
||||
"초기화",
|
||||
).then((ok) => {
|
||||
if (!ok) return;
|
||||
const { bill: _bill, ...rest } = user;
|
||||
void run("/estimation", "PUT", "설계내역서를 초기값으로 되돌렸습니다.", { user: rest });
|
||||
if (ok) void run("/estimation/reset", "POST", "원가 계산을 초기값으로 되돌렸습니다.");
|
||||
}),
|
||||
});
|
||||
reset.dataset.b09BillReset = "";
|
||||
reset.dataset.b09Reset = "";
|
||||
// 카드 = 좌측 패널 공용 카드(269 · 「구조물 배치」 양식)
|
||||
const section = createPanelCard("데이터 점검", true); // 311 기본 접힘
|
||||
section.append(refresh, check.list);
|
||||
// 303-1 · 304-1 하단 고정 줄 — [박스보기|호표보기] · [하이퍼링크|팝업] · 초안 단추 · [?][저장][내역 초기화]
|
||||
const actions = createDock([save, reset], () => isGroupTab(current) && show(current));
|
||||
// 303-1 · 304-1 하단 고정 줄 — [박스보기|호표보기] · [하이퍼링크|팝업] · 초안 단추 · [?][<][>][초기화]
|
||||
const actions = createDock([reset], () => isGroupTab(current) && show(current));
|
||||
const left = document.createElement("div");
|
||||
left.className = "ui-sidebar-panel b09-options";
|
||||
left.append(section, ...DRAFT.panels.map((make) => make(() => user, setUser)), actions);
|
||||
@@ -355,12 +356,28 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
// 481-2 단계 이동 = 고친 값 저장 성공 뒤 · 실패면 머묾 · 불러오기 전 · 고침 없음 = 요청 없이
|
||||
onStepClick: (stepIndex: number) => {
|
||||
if (!projectId) return;
|
||||
const changed = payload && readState("b09-estimation", projectId);
|
||||
void (changed ? saveUser() : Promise.resolve(true)).then(
|
||||
void saveChanged().then(
|
||||
(ok) => ok && goToWorkflowStage(projectId, WORKFLOW_STEP_ROUTES[stepIndex]),
|
||||
);
|
||||
},
|
||||
});
|
||||
// [>] = 다음 단계 없음 → 공용 틀 대신 여기서 — 저장 → 단계 6 완료 → 초안 다음 길 · 초안 없으면 흐림 그대로
|
||||
const next = left.querySelector<HTMLButtonElement>('[data-step-nav="1"]');
|
||||
const goNext = DRAFT.next;
|
||||
if (next && goNext) next.disabled = false;
|
||||
left.addEventListener("ui-step-nav", (event) => {
|
||||
if ((event as CustomEvent<number>).detail !== 1 || !goNext || !projectId) return;
|
||||
event.stopPropagation();
|
||||
void (async () => {
|
||||
if (!(await saveChanged())) return;
|
||||
try {
|
||||
await request(`/projects/${projectId}/estimation/complete`, { method: "POST" });
|
||||
} catch (error) {
|
||||
return showToast(`단계를 마치지 못했습니다 — ${String(error)}`, "error");
|
||||
}
|
||||
goNext();
|
||||
})();
|
||||
});
|
||||
root.append(layout.root);
|
||||
show(current);
|
||||
|
||||
|
||||
@@ -124,12 +124,14 @@ export type TabBuild = (ctx: TabContext) => TabTable | string;
|
||||
/** 초안 탭(252 · `draft/*_UI.ts` 가 TABS · TAB_GROUPS 와 함께 넣음 · 폴더째 지우면 빈 채로) —
|
||||
* builders = 탭 짜기 · extend = 브라우저 셈 곁(셈 결과에 더함) ·
|
||||
* panels = 왼쪽 패널 카드(264 · 「데이터 점검」 아래 · 지금 사용자 값 · 고침 → 캐시 · 다시 셈) ·
|
||||
* actions = 좌측 하단 고정 줄 단추(303-1 · [저장] 줄 위 한 줄 · 비면 줄 없음) */
|
||||
* actions = 좌측 하단 고정 줄 단추(303-1 · [?] 줄 위 한 줄 · 비면 줄 없음) ·
|
||||
* next = [>] 저장 · 단계 완료 뒤 갈 길(298-2 · 없으면 [>] 흐림) */
|
||||
export const DRAFT: {
|
||||
builders: Record<string, TabBuild>;
|
||||
extend: Array<(payload: Row, user: Row, sheets: Row) => void>;
|
||||
panels: Array<(user: () => Row, setUser: (next: Row) => void) => HTMLElement>;
|
||||
actions: Array<() => HTMLElement>;
|
||||
next?: () => void;
|
||||
} = { builders: {}, extend: [], panels: [], actions: [] };
|
||||
|
||||
/** 201-1 호표 묶음 레벨(보기 전용) — 머리 · 합계(제목 줄) = 1 · 그 밖 = 2 · key 열을 들여씀 */
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/* =============================================================================
|
||||
* B09_Draft_Print_UI.ts
|
||||
* 인쇄 미리보기 등록 입구(PLAN 302-1 · 초안) — B09 셸이 `import.meta.glob("./draft/*_UI.ts")` 로 불러올 때
|
||||
* 좌측 하단 고정 줄(`DRAFT.actions`)에 [인쇄 미리보기] 단추 → #/b09-print(`…_Print_Route`)
|
||||
* 좌측 하단 고정 줄(`DRAFT.actions`)에 [인쇄 미리보기] 단추 → #/b09-print(`…_Print_Route`) ·
|
||||
* [>] 다음 길(`DRAFT.next` · 저장 · 단계 6 완료 뒤 · 요구 298-2)
|
||||
* draft 폴더째 지우면 단추 · 길 · 서버 길이 함께 사라짐
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -11,6 +12,8 @@ import { navigateTo } from "../../A00_Common/router";
|
||||
import { DRAFT } from "../B09_Estimation_UI_Tabs";
|
||||
import { ROUTE } from "./B09_Draft_Print_Route";
|
||||
|
||||
DRAFT.next = () => navigateTo(ROUTE.path as RoutePath);
|
||||
|
||||
DRAFT.actions.push(() => {
|
||||
const button = createButton({
|
||||
label: "인쇄 미리보기",
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
/* 298-2 B08 · B09 좌측 아래 단추 줄 — 진짜 `B08_Quantity_UI_Options.ts` · `B08_Quantity_UI_Page.ts` ·
|
||||
* `B09_Estimation_UI_Shell.ts` 를 트랜스파일해 가짜 DOM · 가짜 의존 위에서 돌리고 값을 JSON 으로 냄.
|
||||
* [초기화] = 확인 창 → POST reset · [>] = 저장 → POST complete → 다음 길 · 실패면 머묾. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
|
||||
// 무엇이든 받아 주는 빈 값 — 부르면 또 빈 값 · 펼치면 비어 있음 · 글자로는 ""
|
||||
function any() {
|
||||
const store = {};
|
||||
return new Proxy(function () {}, {
|
||||
get(_t, k) {
|
||||
if (k in store) return store[k];
|
||||
if (k === "then") return undefined;
|
||||
if (k === Symbol.iterator) return function* () {};
|
||||
if (k === Symbol.toPrimitive) return () => "";
|
||||
return (store[k] = any());
|
||||
},
|
||||
set(_t, k, v) {
|
||||
store[k] = v;
|
||||
return true;
|
||||
},
|
||||
apply: () => any(),
|
||||
construct: () => any(),
|
||||
});
|
||||
}
|
||||
|
||||
class Node {
|
||||
constructor(tag = "div") {
|
||||
this.tagName = tag.toUpperCase();
|
||||
this.children = [];
|
||||
this.parent = null;
|
||||
this.dataset = {};
|
||||
this.style = {};
|
||||
this.listeners = {};
|
||||
this.classList = { add() {}, toggle() {}, remove() {}, contains: () => false };
|
||||
}
|
||||
get isConnected() {
|
||||
let n = this;
|
||||
while (n.parent) n = n.parent;
|
||||
return n.isRoot === true;
|
||||
}
|
||||
append(...kids) {
|
||||
kids.forEach((k) => {
|
||||
if (k instanceof Node) k.parent = this;
|
||||
this.children.push(k);
|
||||
});
|
||||
}
|
||||
replaceChildren(...kids) {
|
||||
this.children.forEach((k) => k instanceof Node && (k.parent = null));
|
||||
this.children = [];
|
||||
this.append(...kids);
|
||||
}
|
||||
setAttribute() {}
|
||||
querySelector() {
|
||||
return null;
|
||||
}
|
||||
querySelectorAll() {
|
||||
return [];
|
||||
}
|
||||
addEventListener(name, fn) {
|
||||
(this.listeners[name] ??= []).push(fn);
|
||||
}
|
||||
}
|
||||
global.document = { createElement: (tag) => new Node(tag) };
|
||||
|
||||
function load(rel, stubs) {
|
||||
const src = fs
|
||||
.readFileSync(path.join(ROOT, rel), "utf8")
|
||||
.replace(/import\.meta\.glob\(/g, "(() => ({}))(");
|
||||
const js = ts.transpileModule(src, {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const mod = { exports: {} };
|
||||
const req = (id) => (id in stubs ? stubs[id] : id.endsWith(".css") ? {} : any());
|
||||
new Function("exports", "module", "require", js)(mod.exports, mod, req);
|
||||
return mod.exports;
|
||||
}
|
||||
const flush = async () => {
|
||||
for (let k = 0; k < 8; k++) await new Promise((r) => setTimeout(r, 0));
|
||||
};
|
||||
|
||||
const calls = [];
|
||||
const buttons = []; // 만든 단추 — { variant, node }
|
||||
let layoutOpts = null;
|
||||
let confirmAnswer = false;
|
||||
const store = new Map(); // 가짜 세션 캐시
|
||||
const common = {
|
||||
"@ui/ui_template_workflow_layout": {
|
||||
createWorkflowLayout: (opts) => {
|
||||
layoutOpts = opts;
|
||||
const root = new Node();
|
||||
root.append(opts.leftPanel, opts.mainContent);
|
||||
return { root };
|
||||
},
|
||||
},
|
||||
"@ui/ui_template_elements": {
|
||||
createButton: (o) => {
|
||||
const node = Object.assign(new Node("button"), { onClick: o.onClick, variant: o.variant });
|
||||
buttons.push(node);
|
||||
return node;
|
||||
},
|
||||
createInputField: () => ({ input: new Node("input"), root: new Node() }),
|
||||
showToast: (_text, kind) => calls.push(`toast:${kind}`),
|
||||
showLoadingOverlay: () => calls.push("lock"),
|
||||
hideLoadingOverlay: () => calls.push("unlock"),
|
||||
showConfirmDialog: async () => (calls.push("confirm"), confirmAnswer),
|
||||
},
|
||||
"@ui/ui_template_collapsible": { attachCollapsible() {}, createPanelCard: () => new Node() },
|
||||
"@ui/ui_template_locale": { ui_locales: any(), currentLanguageIndex: 0 },
|
||||
"@config/config_frontend": { CURRENT_PROJECT_ID_KEY: "pid", API_BASE_URL: "/api" },
|
||||
"../A00_Common/b_page_scaffold": { workflowSteps: () => [] },
|
||||
"../A00_Common/b_workflow_nav": {
|
||||
WORKFLOW_STEP_ROUTES: ["b03", "b04", "b05", "b06", "b07", "b08", "b09"],
|
||||
goToWorkflowStage: (_p, route) => calls.push(`go:${route}`),
|
||||
},
|
||||
"../A00_Common/b_page_state": {
|
||||
readState: (name) => store.get(name) ?? null,
|
||||
writeState: (name, v) => store.set(name, v),
|
||||
clearState: (name) => store.delete(name),
|
||||
},
|
||||
};
|
||||
global.localStorage = { getItem: () => "P1" };
|
||||
const take = () => calls.splice(0).filter((c) => !c.startsWith("toast:success"));
|
||||
|
||||
// B08 좌측 옵션 [저장] [초기화]
|
||||
async function b08Options() {
|
||||
let fail = false;
|
||||
const m = load("B08_Quantity/B08_Quantity_UI_Options.ts", {
|
||||
...common,
|
||||
"./B08_Quantity_UI_Hydro": {
|
||||
request: async (url, init) => {
|
||||
const method = init?.method ?? "GET";
|
||||
if (method !== "GET") calls.push(`req:${method}:${url}`);
|
||||
if (fail && method === "POST") throw new Error("500");
|
||||
return { one_pipe_length_m: 6, rock_ratios_pct: { 연암: 90 }, material_surcharge_pct: {} };
|
||||
},
|
||||
},
|
||||
"./B08_Quantity_UI_Milestone_State": {
|
||||
saveMilestone: async () => calls.push("req:milestone"),
|
||||
loadMilestone: async (_p, force) => calls.push(`milestone:${force}`),
|
||||
},
|
||||
});
|
||||
buttons.length = 0;
|
||||
const panel = m.createOptionsPanel("P1", () => calls.push("redraw"));
|
||||
await flush();
|
||||
const actions = panel.panel.children.find((c) => c.className === "ui-sidebar-actions");
|
||||
const out = { order: actions.children.map((b) => b.variant) };
|
||||
const reset = actions.children[1];
|
||||
const drafts = ["b08-options", "b08-materials", "b08-hydro", "b08-milestone"];
|
||||
const fill = () => drafts.forEach((d) => store.set(d, { x: 1 }));
|
||||
const press = async () => {
|
||||
take();
|
||||
reset.onClick();
|
||||
await flush();
|
||||
return take();
|
||||
};
|
||||
fill();
|
||||
confirmAnswer = false;
|
||||
out.cancel = [await press(), drafts.filter((d) => store.has(d))];
|
||||
confirmAnswer = true;
|
||||
out.ok = [await press(), drafts.filter((d) => store.has(d))];
|
||||
fill();
|
||||
fail = true;
|
||||
out.fail = [await press(), drafts.filter((d) => store.has(d))];
|
||||
store.clear();
|
||||
return out;
|
||||
}
|
||||
|
||||
// B08 진행 단계 · [>] — B09 쪽 = 저장 → POST complete → 이동
|
||||
async function b08Page() {
|
||||
let completeFail = false;
|
||||
const m = load("B08_Quantity/B08_Quantity_UI_Page.ts", {
|
||||
...common,
|
||||
"./B08_Quantity_UI_Options": {
|
||||
createOptionsPanel: () => ({ panel: new Node(), save: async () => true }),
|
||||
},
|
||||
"./B08_Quantity_UI_Hydro": {
|
||||
renderHydro() {},
|
||||
request: async (url, init) => {
|
||||
calls.push(`req:${init?.method}:${url}`);
|
||||
if (completeFail) throw new Error("500");
|
||||
},
|
||||
},
|
||||
});
|
||||
m.renderB08Quantity(Object.assign(new Node(), { isRoot: true }));
|
||||
const step = async (i) => {
|
||||
take();
|
||||
layoutOpts.onStepClick(i);
|
||||
await flush();
|
||||
return take();
|
||||
};
|
||||
const out = { next: await step(6), back: await step(4) };
|
||||
completeFail = true;
|
||||
out.fail = await step(6);
|
||||
return out;
|
||||
}
|
||||
|
||||
// B09 [초기화] · [>](공용 틀 대신 셸) — draft = DRAFT.next 있음 / 없음
|
||||
async function b09(withNext) {
|
||||
let failOn = null; // "PUT" | "complete"
|
||||
global.fetch = async (url, init) => {
|
||||
const method = init?.method ?? "GET";
|
||||
if (method !== "GET") calls.push(`fetch:${method}:${url}`);
|
||||
const bad = failOn === method || (failOn === "complete" && url.endsWith("/complete"));
|
||||
if (bad) return { ok: false, status: 500, json: async () => ({}) };
|
||||
return { ok: true, status: 200, json: async () => any() };
|
||||
};
|
||||
const tabs = any();
|
||||
tabs.DRAFT = { builders: {}, extend: [], panels: [], actions: [] };
|
||||
if (withNext) tabs.DRAFT.next = () => calls.push("next");
|
||||
const nextBtn = Object.assign(new Node("button"), { disabled: true });
|
||||
const m = load("B09_Estimation/B09_Estimation_UI_Shell.ts", {
|
||||
...common,
|
||||
"./B09_Estimation_UI_Tabs": tabs,
|
||||
"./B09_Estimation_UI_Dock": {
|
||||
linkMode: () => "popup",
|
||||
createDock: (list) => {
|
||||
const dock = new Node();
|
||||
dock.append(...list);
|
||||
return dock;
|
||||
},
|
||||
},
|
||||
"@ui/ui_template_workflow_layout": {
|
||||
createWorkflowLayout: (opts) => {
|
||||
layoutOpts = opts;
|
||||
// 공용 [?] [<] [>] 자리 흉내 — B09 [>] = 흐림
|
||||
opts.leftPanel.querySelector = (sel) => (sel === '[data-step-nav="1"]' ? nextBtn : null);
|
||||
const root = new Node();
|
||||
root.append(opts.leftPanel, opts.mainContent);
|
||||
return { root };
|
||||
},
|
||||
},
|
||||
});
|
||||
buttons.length = 0;
|
||||
m.renderB09Estimation(Object.assign(new Node(), { isRoot: true }));
|
||||
await flush(); // GET 응답 반영
|
||||
const left = layoutOpts.leftPanel;
|
||||
const dock = left.children[left.children.length - 1];
|
||||
const out = { dock: dock.children.map((b) => b.variant), nextEnabled: !nextBtn.disabled };
|
||||
const nav = async (detail) => {
|
||||
take();
|
||||
let stopped = false;
|
||||
const event = { detail, stopPropagation: () => (stopped = true) };
|
||||
(left.listeners["ui-step-nav"] ?? []).forEach((fn) => fn(event));
|
||||
await flush();
|
||||
return [stopped, take()];
|
||||
};
|
||||
if (!withNext) {
|
||||
out.nav = await nav(1);
|
||||
return out;
|
||||
}
|
||||
out.clean = await nav(1);
|
||||
store.set("b09-estimation", { x: 1 });
|
||||
out.changed = await nav(1);
|
||||
out.cleared = !store.has("b09-estimation");
|
||||
store.set("b09-estimation", { x: 1 });
|
||||
failOn = "PUT";
|
||||
out.saveFail = await nav(1);
|
||||
failOn = "complete";
|
||||
store.delete("b09-estimation");
|
||||
out.completeFail = await nav(1);
|
||||
failOn = null;
|
||||
out.prev = await nav(-1);
|
||||
// [초기화]
|
||||
const reset = dock.children[0];
|
||||
const press = async () => {
|
||||
take();
|
||||
reset.onClick();
|
||||
await flush();
|
||||
return take();
|
||||
};
|
||||
store.set("b09-estimation", { x: 1 });
|
||||
confirmAnswer = false;
|
||||
out.resetCancel = [await press(), store.has("b09-estimation")];
|
||||
confirmAnswer = true;
|
||||
out.resetOk = [await press(), store.has("b09-estimation")];
|
||||
store.clear();
|
||||
return out;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const r = {
|
||||
options: await b08Options(),
|
||||
page: await b08Page(),
|
||||
b09: await b09(true),
|
||||
b09NoDraft: await b09(false),
|
||||
};
|
||||
console.log(JSON.stringify(r));
|
||||
})().catch((e) => {
|
||||
console.error(e && e.stack ? e.stack : e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -127,7 +127,10 @@ async function b08() {
|
||||
"./B08_Quantity_UI_Material": {
|
||||
renderMaterialSheet: (_h, _p, _s, onSave) => (registerMaterial = onSave),
|
||||
},
|
||||
"./B08_Quantity_UI_Hydro": { renderHydro: (_h, _p, onSave) => (registerHydro = onSave) },
|
||||
"./B08_Quantity_UI_Hydro": {
|
||||
renderHydro: (_h, _p, onSave) => (registerHydro = onSave),
|
||||
request: async (url) => calls.push(`req:${url.split("/").pop()}`), // 298-2 B09 쪽 = 단계 5 완료
|
||||
},
|
||||
});
|
||||
const root = Object.assign(new Node(), { isRoot: true });
|
||||
m.renderB08Quantity(root);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""184-3 · 246-1 B09 설계내역서 자유 편집 화면 — 모든 줄 우클릭 메뉴 · 칸 고침 → 덮개 · 삭제 → gone · 옮김 · 새 공종(←) ·
|
||||
자동값으로 · 관급 옮김 막힘 · 깊이 5 · 고친 칸 강조 · [내역 초기화] 배선.
|
||||
자동값으로 · 관급 옮김 막힘 · 깊이 5 · 고친 칸 강조 · [초기화] 배선(298-2).
|
||||
|
||||
- 셈 출력 줄 = 계약 꼴(`Calc_Bill.py` 머리 · parent · auto · auto_{칸} · level · no · cost) 가짜 줄
|
||||
- 저장 = `user.bill` v2 {v: 2, rows: 표 차례 {key · parent · 고친 칸}, gone}
|
||||
@@ -278,12 +278,12 @@ def test_표_칸_강조_번호_연결_칸():
|
||||
assert out["renamed"] == "절토공" # 번호는 떼고 이름만
|
||||
|
||||
|
||||
def test_내역_초기화_배선():
|
||||
def test_초기화_배선():
|
||||
src = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Shell.ts").read_text("utf-8")
|
||||
block = src[src.index('label: "내역 초기화"') :]
|
||||
block = block[: block.index("b09BillReset")]
|
||||
# 확인 창 → bill 뺀 사용자 값으로 바로 PUT(덮개 빔) · 좌측 단추 줄
|
||||
assert "showConfirmDialog(" in block and "if (!ok) return;" in block
|
||||
assert "const { bill: _bill, ...rest } = user;" in block
|
||||
assert 'run("/estimation", "PUT"' in block and "{ user: rest }" in block
|
||||
assert "createDock([save, reset]," in src # 303-1 하단 고정 줄 맨 아래 줄
|
||||
block = src[src.index('label: L("Common_Btn_Reset")') :]
|
||||
block = block[: block.index("b09Reset")]
|
||||
# 298-2 [내역 초기화] → [초기화] 하나 — 확인 창 → POST …/estimation/reset(내역 + 그 페이지 값 · 설정)
|
||||
assert 'variant: "danger-ghost"' in block and "showConfirmDialog(" in block
|
||||
assert 'if (ok) void run("/estimation/reset", "POST"' in block
|
||||
assert "내역 초기화" not in src and "b09BillReset" not in src
|
||||
assert "createDock([reset]," in src # 303-1 하단 고정 줄 맨 아래 줄 · [저장] 없음(요구 301)
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""298-2 B08 · B09 좌측 아래 단추 줄 — [초기화] · [>] (PLAN 497-2 · 요구 300 C1 · C3 · C4 · C5 · 301).
|
||||
|
||||
`helper_298_2_b08_b09_buttons.cjs` 가 진짜 `B08_Quantity_UI_Options.ts` · `B08_Quantity_UI_Page.ts` ·
|
||||
`B09_Estimation_UI_Shell.ts` 를 가짜 DOM · 가짜 의존 위에서 돌림.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
DRAFTS = ["b08-options", "b08-materials", "b08-hydro", "b08-milestone"]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def r():
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_298_2_b08_b09_buttons.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_B08_단추_줄_저장_다음_초기화(r):
|
||||
assert r["options"]["order"] == ["filled", "danger-ghost"] # [?][<][>] 은 공용이 앞에
|
||||
|
||||
|
||||
def test_B08_초기화_취소면_그대로(r):
|
||||
assert r["options"]["cancel"] == [["confirm"], DRAFTS]
|
||||
|
||||
|
||||
def test_B08_초기화_확인_잠금_요청_초안_비움_다시_그림(r):
|
||||
calls, left = r["options"]["ok"]
|
||||
assert calls == [
|
||||
"confirm",
|
||||
"lock",
|
||||
"req:POST:/projects/P1/quantity/reset",
|
||||
"milestone:true", # 이정 저장본 다시 읽음
|
||||
"redraw",
|
||||
"unlock",
|
||||
]
|
||||
assert left == []
|
||||
|
||||
|
||||
def test_B08_초기화_실패면_초안_남음(r):
|
||||
calls, left = r["options"]["fail"]
|
||||
assert "toast:error" in calls and "redraw" not in calls and left == DRAFTS
|
||||
|
||||
|
||||
def test_B08_다음_단계_완료_뒤_이동_실패면_머묾(r):
|
||||
p = r["page"]
|
||||
assert p["next"] == ["req:POST:/projects/P1/quantity/complete", "go:b09"]
|
||||
assert p["back"] == ["go:b07"] # 앞 단계 = 완료 없음
|
||||
assert p["fail"] == ["req:POST:/projects/P1/quantity/complete", "toast:error"]
|
||||
|
||||
|
||||
def test_B09_단추_줄_초기화_하나_저장_없음(r):
|
||||
assert r["b09"]["dock"] == ["danger-ghost"]
|
||||
assert r["b09"]["nextEnabled"] is True
|
||||
|
||||
|
||||
def test_B09_다음_저장_완료_인쇄(r):
|
||||
b = r["b09"]
|
||||
assert b["clean"] == [True, ["fetch:POST:/api/projects/P1/estimation/complete", "next"]]
|
||||
assert b["changed"] == [
|
||||
True,
|
||||
[
|
||||
"lock",
|
||||
"fetch:PUT:/api/projects/P1/estimation",
|
||||
"unlock",
|
||||
"fetch:POST:/api/projects/P1/estimation/complete",
|
||||
"next",
|
||||
],
|
||||
]
|
||||
assert b["cleared"] is True
|
||||
assert "next" not in b["saveFail"][1] and "toast:error" in b["saveFail"][1]
|
||||
assert b["completeFail"][1] == [
|
||||
"fetch:POST:/api/projects/P1/estimation/complete",
|
||||
"toast:error",
|
||||
]
|
||||
assert b["prev"] == [False, []] # [<] = 공용 틀 몫
|
||||
|
||||
|
||||
def test_B09_초기화(r):
|
||||
b = r["b09"]
|
||||
assert b["resetCancel"] == [["confirm"], True]
|
||||
assert b["resetOk"] == [
|
||||
["confirm", "lock", "fetch:POST:/api/projects/P1/estimation/reset", "unlock"],
|
||||
False,
|
||||
]
|
||||
|
||||
|
||||
def test_B09_초안_없으면_다음_흐림(r):
|
||||
assert r["b09NoDraft"] == {"dock": ["danger-ghost"], "nextEnabled": False, "nav": [False, []]}
|
||||
|
||||
|
||||
def test_초안_다음_길_등록():
|
||||
src = (ROOT / "B09_Estimation/draft/B09_Draft_Print_UI.ts").read_text("utf-8")
|
||||
assert "DRAFT.next = () => navigateTo(ROUTE.path as RoutePath);" in src
|
||||
@@ -98,6 +98,6 @@ def test_잇기():
|
||||
groups = (ROOT / GROUPS).read_text("utf-8")
|
||||
assert "toggle" not in groups and "redraw" not in groups and "createButton" not in groups
|
||||
shell = (ROOT / "B09_Estimation/B09_Estimation_UI_Shell.ts").read_text("utf-8")
|
||||
assert "createDock([save, reset], () => isGroupTab(current) && show(current))" in shell
|
||||
assert "createDock([reset], () => isGroupTab(current) && show(current))" in shell
|
||||
state = (ROOT / "A00_Common/b_page_state.ts").read_text("utf-8")
|
||||
assert '"b09-group-view": { bucket: "pref", scope: "global" },' in state
|
||||
|
||||
@@ -32,7 +32,7 @@ def r():
|
||||
|
||||
def test_B08_저장_성공_뒤_이동_왼쪽_다음_지금_탭(r):
|
||||
b = r["b08"]
|
||||
assert b["summaryOk"] == ["save:options", "go:b09"]
|
||||
assert b["summaryOk"] == ["save:options", "req:complete", "go:b09"] # 298-2 단계 5 완료
|
||||
assert b["materialOk"] == ["save:options", "save:material", "go:b07"]
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ def test_B08_저장_실패면_머묾(r):
|
||||
|
||||
|
||||
def test_B08_떠난_탭은_저장_안_부름(r):
|
||||
assert r["b08"]["leftTab"] == ["save:options", "go:b09"]
|
||||
assert r["b08"]["leftTab"] == ["save:options", "req:complete", "go:b09"]
|
||||
|
||||
|
||||
def test_B08_좌측_옵션_save(r):
|
||||
|
||||
Reference in New Issue
Block a user