feat(481-2 B08 · B09 저장 후 이동): 단계 이동 = 보이는 화면 저장 성공 뒤 · 실패면 머묾
- B08 = 왼쪽 옵션(옵션 · 이정 캐시) → 지금 탭(자재 · 수리 · 떠난 탭 host.isConnected 거짓이면 안 부름) · 캐시 없으면 요청 없이 - 저장 함수(옵션 · 자재 · 수리 · B09 run) 성공 여부 돌려줌 · B09 = 불러오기 전 · 고침 없음이면 요청 없이 이동 - 시험 test_481_2_save_move 새로 · 121 · 122 원본 줄 기대 갱신 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -42,7 +42,12 @@ function note(text: string): HTMLElement {
|
||||
return p;
|
||||
}
|
||||
|
||||
export async function renderHydro(host: HTMLElement, projectId: string): Promise<void> {
|
||||
/** `onSave` = 페이지 이동 전 저장 등록(481-2) — 고친 측점 없으면 요청 없이 참. */
|
||||
export async function renderHydro(
|
||||
host: HTMLElement,
|
||||
projectId: string,
|
||||
onSave?: (save: () => Promise<boolean>) => void,
|
||||
): Promise<void> {
|
||||
host.replaceChildren(note("홍수량 계산서를 불러오는 중…"));
|
||||
let sheet: HydroPayload;
|
||||
let interval = 20;
|
||||
@@ -133,7 +138,7 @@ export async function renderHydro(host: HTMLElement, projectId: string): Promise
|
||||
};
|
||||
|
||||
/** [저장] — 측점 전부의 지금 입력을 B08 자기 파일로 · 성공하면 캐시를 비우고 저장본으로 다시 엶 */
|
||||
const saveAll = async (): Promise<void> => {
|
||||
const saveAll = async (): Promise<boolean> => {
|
||||
const stations = Object.fromEntries(
|
||||
sheet.stations.map((s) => [keyOf(s), unlinked(s, inputsOf(s))]),
|
||||
);
|
||||
@@ -148,12 +153,15 @@ export async function renderHydro(host: HTMLElement, projectId: string): Promise
|
||||
clearState("b08-hydro", projectId);
|
||||
showToast("수리집수면적표를 저장했습니다.", "success");
|
||||
open(openIndex);
|
||||
return true;
|
||||
} catch (error) {
|
||||
showToast(`저장하지 못했습니다 — ${String(error)}`, "error");
|
||||
return false;
|
||||
} finally {
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
};
|
||||
|
||||
open(0);
|
||||
onSave?.(async () => !Object.keys(drafts).length || saveAll());
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { createDataTable } from "@ui/ui_template_data_table";
|
||||
import { clearState, writeState } from "../A00_Common/b_page_state";
|
||||
import { clearState, readState, writeState } from "../A00_Common/b_page_state";
|
||||
import {
|
||||
blankUnitRows,
|
||||
matched,
|
||||
@@ -117,11 +117,12 @@ function concreteWhy(s: MaterialSheets, source: SummarySource): string {
|
||||
: "";
|
||||
}
|
||||
|
||||
/** 보조 탭 한 장 — 불러오는 동안 한 줄 · 실패하면 까닭 한 줄. */
|
||||
/** 보조 탭 한 장 — 불러오는 동안 한 줄 · 실패하면 까닭 한 줄. `onSave` = 페이지 이동 전 저장 등록(481-2). */
|
||||
export async function renderMaterialSheet(
|
||||
host: HTMLElement,
|
||||
projectId: string,
|
||||
sheet: MaterialSheetId,
|
||||
onSave?: (save: () => Promise<boolean>) => void,
|
||||
): Promise<void> {
|
||||
host.replaceChildren(note("자재 집계표를 불러오는 중…"));
|
||||
let loaded: Awaited<ReturnType<typeof loadMaterials>>;
|
||||
@@ -199,7 +200,7 @@ export async function renderMaterialSheet(
|
||||
});
|
||||
|
||||
/** [저장] — 사용자 값을 서버로 · 서버가 다시 셈한 표로 그림 · 캐시 비움 */
|
||||
const saveAll = async (): Promise<void> => {
|
||||
const saveAll = async (): Promise<boolean> => {
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
payload = await request<Payload>(`/projects/${projectId}/quantity/materials`, {
|
||||
@@ -211,10 +212,13 @@ export async function renderMaterialSheet(
|
||||
clearState("b08-materials", projectId);
|
||||
redraw();
|
||||
showToast("자재 집계표를 저장했습니다.", "success");
|
||||
return true;
|
||||
} catch (error) {
|
||||
showToast(`저장하지 못했습니다 — ${String(error)}`, "error");
|
||||
return false;
|
||||
} finally {
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
};
|
||||
onSave?.(async () => !readState("b08-materials", projectId) || saveAll());
|
||||
}
|
||||
|
||||
@@ -100,8 +100,12 @@ function hint(text: string): HTMLElement {
|
||||
return p;
|
||||
}
|
||||
|
||||
/** 좌측 패널 — 맨 끝 `.ui-sidebar-actions` 줄([저장] · [?] 자리). `onSaved` = 열린 탭 다시 그림. */
|
||||
export function createOptionsPanel(projectId: string | null, onSaved: () => void): HTMLElement {
|
||||
/** 좌측 패널 — 맨 끝 `.ui-sidebar-actions` 줄([저장] · [?] 자리). `onSaved` = 열린 탭 다시 그림.
|
||||
* `save` = 페이지 이동 전 저장(481-2) — 옵션 · 이정 캐시 없으면 요청 없이 참 · 실패면 거짓. */
|
||||
export function createOptionsPanel(
|
||||
projectId: string | null,
|
||||
onSaved: () => void,
|
||||
): { panel: HTMLElement; save: () => Promise<boolean> } {
|
||||
const one = createInputField({
|
||||
label: "배관 1본 길이 (m)",
|
||||
type: "number",
|
||||
@@ -192,12 +196,12 @@ export function createOptionsPanel(projectId: string | null, onSaved: () => void
|
||||
input.addEventListener("change", show);
|
||||
}
|
||||
|
||||
const saveAll = async (): Promise<void> => {
|
||||
if (!projectId) return;
|
||||
const saveAll = async (): Promise<boolean> => {
|
||||
if (!projectId) return true;
|
||||
const draft = readDraft(projectId);
|
||||
if (draft.one_pipe_length_m === null) {
|
||||
showToast("배관 1본 길이를 넣으세요 — 0 보다 큰 값.", "error");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
@@ -215,8 +219,10 @@ export function createOptionsPanel(projectId: string | null, onSaved: () => void
|
||||
show();
|
||||
showToast("수량 산출 옵션을 저장했습니다.", "success");
|
||||
onSaved();
|
||||
return true;
|
||||
} catch (error) {
|
||||
showToast(`저장하지 못했습니다 — ${String(error)}`, "error");
|
||||
return false;
|
||||
} finally {
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
@@ -256,5 +262,8 @@ export function createOptionsPanel(projectId: string | null, onSaved: () => void
|
||||
},
|
||||
(error: unknown) => showToast(`옵션을 불러오지 못했습니다 — ${String(error)}`, "error"),
|
||||
);
|
||||
return panel;
|
||||
const changed = (): boolean =>
|
||||
!!projectId &&
|
||||
(!!readState("b08-options", projectId) || !!readState("b08-milestone", projectId));
|
||||
return { panel, save: async () => !changed() || saveAll() };
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* 자재 아래 보조 탭 = 총괄자재수량집계표 · 콘크리트소요자재집계표 · 자재수량집계표(121-1 `…_UI_Material`).
|
||||
* 옛 수량 산출 코드(`old_code/B08_Quantity/`)는 가져오지 않음 — 새로 씀.
|
||||
* 좌측 패널 = 메인 탭과 같은 컨테이너 셋 + [저장] 줄(`B08_Quantity_UI_Options` · PLAN 122).
|
||||
* 단계 이동(☰ · 진행 단계 · [<] [>]) = 보이는 화면(좌측 옵션 + 지금 탭) 저장 성공 뒤 · 실패면 머묾(481-2).
|
||||
* ========================================================================== */
|
||||
|
||||
import "@ui/ui_template_general_layout.css";
|
||||
@@ -47,6 +48,9 @@ const SHEETS = [
|
||||
|
||||
type SheetId = (typeof SHEETS)[number]["id"];
|
||||
|
||||
/** 지금 탭의 이동 전 저장 — 자재 · 수리 탭이 서면 등록 · 떠난 탭(`host.isConnected` 거짓)은 안 씀(481-2). */
|
||||
type TabSave = (host: HTMLElement, save: () => Promise<boolean>) => void;
|
||||
|
||||
function pendingNote(): HTMLElement {
|
||||
const pending = document.createElement("p");
|
||||
pending.className = "ui-general-block__empty";
|
||||
@@ -55,7 +59,7 @@ function pendingNote(): HTMLElement {
|
||||
}
|
||||
|
||||
/** 자재 탭 — 보조 탭 줄 + 몸통(보조 탭마다 새 몸통 · 늦은 응답이 다른 보조 탭을 덮지 않게). */
|
||||
function renderMaterials(host: HTMLElement, projectId: string | null): void {
|
||||
function renderMaterials(host: HTMLElement, projectId: string | null, onSave: TabSave): void {
|
||||
const tabs = document.createElement("div");
|
||||
tabs.className = "b08-quantity__tabs ui-subtabs b08-structure__tabs";
|
||||
tabs.setAttribute("role", "tablist");
|
||||
@@ -78,17 +82,17 @@ function renderMaterials(host: HTMLElement, projectId: string | null): void {
|
||||
const body = document.createElement("div");
|
||||
view.replaceChildren(body);
|
||||
if (!projectId) body.append(pendingNote());
|
||||
else void renderMaterialSheet(body, projectId, picked);
|
||||
else void renderMaterialSheet(body, projectId, picked, (save) => onSave(body, save));
|
||||
};
|
||||
host.replaceChildren(tabs, view);
|
||||
select("material");
|
||||
}
|
||||
|
||||
/** 탭마다 새 몸통 — 늦게 온 응답이 다른 탭 화면을 덮지 않게(떨어진 몸통에만 그림). */
|
||||
function renderTab(host: HTMLElement, tab: TabId, projectId: string | null): void {
|
||||
function renderTab(host: HTMLElement, tab: TabId, projectId: string | null, onSave: TabSave): void {
|
||||
const view = document.createElement("div");
|
||||
host.replaceChildren(view);
|
||||
if (tab === "material") return renderMaterials(view, projectId);
|
||||
if (tab === "material") return renderMaterials(view, projectId, onSave);
|
||||
if (tab === "summary" && projectId) {
|
||||
// 보조 탭 줄 자리(170-4) — 보이지 않는 sm 단추 한 줄 · 자재 탭과 같은 높이라 탭 바꿀 때 안 튐
|
||||
const blank = document.createElement("div");
|
||||
@@ -101,7 +105,8 @@ function renderTab(host: HTMLElement, tab: TabId, projectId: string | null): voi
|
||||
view.append(blank, body);
|
||||
return void renderStructureSummary(body, projectId);
|
||||
}
|
||||
if (tab === "hydrology" && projectId) return void renderHydro(view, projectId);
|
||||
if (tab === "hydrology" && projectId)
|
||||
return void renderHydro(view, projectId, (save) => onSave(view, save));
|
||||
if (tab === "earthwork" && projectId) return void renderEarthwork(view, projectId);
|
||||
if (tab === "milestone" && projectId) return renderMilestone(view, projectId);
|
||||
view.append(pendingNote());
|
||||
@@ -118,6 +123,8 @@ export function renderB08Quantity(root: HTMLElement): void {
|
||||
panel.setAttribute("role", "tabpanel");
|
||||
|
||||
let current: TabId = "summary";
|
||||
let tabSave: { host: HTMLElement; save: () => Promise<boolean> } | null = null;
|
||||
const onTabSave: TabSave = (host, save) => (tabSave = { host, save });
|
||||
const select = (tab: TabId): void => {
|
||||
current = tab;
|
||||
tabBar.replaceChildren(
|
||||
@@ -133,7 +140,7 @@ export function renderB08Quantity(root: HTMLElement): void {
|
||||
return button;
|
||||
}),
|
||||
);
|
||||
renderTab(panel, tab, projectId);
|
||||
renderTab(panel, tab, projectId, onTabSave);
|
||||
};
|
||||
|
||||
const body = document.createElement("div");
|
||||
@@ -141,16 +148,26 @@ export function renderB08Quantity(root: HTMLElement): void {
|
||||
body.append(tabBar, panel);
|
||||
select("summary");
|
||||
|
||||
// [저장] 뒤 열린 탭을 서버 정본으로 다시 그림
|
||||
const options = createOptionsPanel(projectId, () => select(current));
|
||||
// 왼쪽 → 지금 탭 차례 · 탭 저장은 왼쪽 저장이 탭을 다시 그리기 전에 쥠
|
||||
const saveShown = async (): Promise<boolean> => {
|
||||
const tab = tabSave?.host.isConnected ? tabSave.save : null;
|
||||
return (await options.save()) && (!tab || (await tab()));
|
||||
};
|
||||
|
||||
const layout = createWorkflowLayout({
|
||||
title: L("B08_Quantity_Title"),
|
||||
steps: workflowSteps(),
|
||||
activeStep: 5,
|
||||
mainContent: body,
|
||||
// [저장] 뒤 열린 탭을 서버 정본으로 다시 그림
|
||||
leftPanel: createOptionsPanel(projectId, () => select(current)),
|
||||
leftPanel: options.panel,
|
||||
routes: WORKFLOW_STEP_ROUTES,
|
||||
onStepClick: (stepIndex: number) => {
|
||||
if (projectId) goToWorkflowStage(projectId, WORKFLOW_STEP_ROUTES[stepIndex]);
|
||||
if (!projectId) return;
|
||||
void saveShown().then(
|
||||
(ok) => ok && goToWorkflowStage(projectId, WORKFLOW_STEP_ROUTES[stepIndex]),
|
||||
);
|
||||
},
|
||||
});
|
||||
root.append(layout.root);
|
||||
|
||||
@@ -275,15 +275,15 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
show(current);
|
||||
};
|
||||
|
||||
// keep = 화면 캐시 그대로(296 [업데이트] — 지운 줄 · 고친 칸을 안 되살림)
|
||||
// keep = 화면 캐시 그대로(296 [업데이트] — 지운 줄 · 고친 칸을 안 되살림) · 참 = 성공
|
||||
const run = async (
|
||||
path: string,
|
||||
method: string,
|
||||
done: string,
|
||||
init?: Row,
|
||||
keep = false,
|
||||
): Promise<void> => {
|
||||
if (!projectId) return;
|
||||
): Promise<boolean> => {
|
||||
if (!projectId) return false;
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
const next = await request<Row>(`/projects/${projectId}${path}`, {
|
||||
@@ -293,18 +293,18 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
if (!keep) clearState("b09-estimation", projectId);
|
||||
apply(next, !keep);
|
||||
showToast(done, "success");
|
||||
return true;
|
||||
} catch (error) {
|
||||
showToast(`하지 못했습니다 — ${String(error)}`, "error");
|
||||
return false;
|
||||
} finally {
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
};
|
||||
|
||||
const save = createButton({
|
||||
label: "저장",
|
||||
variant: "filled",
|
||||
onClick: () => void run("/estimation", "PUT", "원가 사용자 값을 저장했습니다.", { user }),
|
||||
});
|
||||
const saveUser = (): Promise<boolean> =>
|
||||
run("/estimation", "PUT", "원가 사용자 값을 저장했습니다.", { user });
|
||||
const save = createButton({ label: "저장", variant: "filled", onClick: () => void saveUser() });
|
||||
save.dataset.b09Save = "";
|
||||
const refresh = createButton({
|
||||
label: "업데이트",
|
||||
@@ -352,8 +352,13 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
mainContent: body,
|
||||
leftPanel: left,
|
||||
routes: WORKFLOW_STEP_ROUTES,
|
||||
// 481-2 단계 이동 = 고친 값 저장 성공 뒤 · 실패면 머묾 · 불러오기 전 · 고침 없음 = 요청 없이
|
||||
onStepClick: (stepIndex: number) => {
|
||||
if (projectId) goToWorkflowStage(projectId, WORKFLOW_STEP_ROUTES[stepIndex]);
|
||||
if (!projectId) return;
|
||||
const changed = payload && readState("b09-estimation", projectId);
|
||||
void (changed ? saveUser() : Promise.resolve(true)).then(
|
||||
(ok) => ok && goToWorkflowStage(projectId, WORKFLOW_STEP_ROUTES[stepIndex]),
|
||||
);
|
||||
},
|
||||
});
|
||||
root.append(layout.root);
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
/* 481-2 B08 · B09 단계 이동 = 저장 성공 뒤 — 진짜 `B08_Quantity_UI_Page.ts` · `B09_Estimation_UI_Shell.ts` 를
|
||||
* 트랜스파일해 가짜 DOM · 가짜 의존 위에서 돌리고 값을 JSON 으로 냄.
|
||||
* 의존은 기록용 가짜 — 지정 안 한 import 는 무엇이든 받아 주는 빈 값(any). */
|
||||
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(),
|
||||
});
|
||||
}
|
||||
|
||||
// 가짜 요소 — isConnected = 뿌리까지 부모 줄이 이어짐
|
||||
class Node {
|
||||
constructor(tag = "div") {
|
||||
this.tagName = tag.toUpperCase();
|
||||
this.children = [];
|
||||
this.parent = null;
|
||||
this.dataset = {};
|
||||
this.style = {};
|
||||
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() {}
|
||||
}
|
||||
global.document = { createElement: (tag) => new Node(tag) };
|
||||
|
||||
function load(rel, stubs) {
|
||||
let 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 = () => new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
const calls = [];
|
||||
let layoutOpts = null;
|
||||
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) => Object.assign(new Node("button"), { onClick: o.onClick }),
|
||||
showToast: (text, kind) => calls.push(`toast:${kind}`),
|
||||
showLoadingOverlay() {},
|
||||
hideLoadingOverlay() {},
|
||||
showConfirmDialog: async () => false,
|
||||
},
|
||||
"@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" };
|
||||
|
||||
async function b08() {
|
||||
let optionsOk = true;
|
||||
let tabOk = true;
|
||||
let registerMaterial = null;
|
||||
let registerHydro = null;
|
||||
const m = load("B08_Quantity/B08_Quantity_UI_Page.ts", {
|
||||
...common,
|
||||
"./B08_Quantity_UI_Options": {
|
||||
createOptionsPanel: () => ({
|
||||
panel: new Node(),
|
||||
save: async () => (calls.push("save:options"), optionsOk),
|
||||
}),
|
||||
},
|
||||
"./B08_Quantity_UI_Material": {
|
||||
renderMaterialSheet: (_h, _p, _s, onSave) => (registerMaterial = onSave),
|
||||
},
|
||||
"./B08_Quantity_UI_Hydro": { renderHydro: (_h, _p, onSave) => (registerHydro = onSave) },
|
||||
});
|
||||
const root = Object.assign(new Node(), { isRoot: true });
|
||||
m.renderB08Quantity(root);
|
||||
const body = layoutOpts.mainContent;
|
||||
const tabBar = body.children[0];
|
||||
const clickTab = (id) => tabBar.children.find((b) => b.dataset.b08Tab === id).onClick();
|
||||
const step = async (i) => {
|
||||
calls.length = 0;
|
||||
layoutOpts.onStepClick(i);
|
||||
await flush();
|
||||
await flush();
|
||||
return [...calls];
|
||||
};
|
||||
const tabSave = (tag) => async () => (calls.push(`save:${tag}`), tabOk);
|
||||
const out = {};
|
||||
out.summaryOk = await step(6);
|
||||
optionsOk = false;
|
||||
out.optionsFail = await step(6);
|
||||
optionsOk = true;
|
||||
clickTab("material");
|
||||
registerMaterial(tabSave("material"));
|
||||
out.materialOk = await step(4);
|
||||
tabOk = false;
|
||||
out.materialFail = await step(4);
|
||||
optionsOk = false;
|
||||
out.optionsFailSkipsTab = await step(4);
|
||||
optionsOk = true;
|
||||
tabOk = false;
|
||||
clickTab("hydrology");
|
||||
registerHydro(tabSave("hydro"));
|
||||
out.hydroFail = await step(6);
|
||||
clickTab("earthwork"); // 수리 탭 떠남 — 떨어진 탭 저장은 안 부름
|
||||
out.leftTab = await step(6);
|
||||
return out;
|
||||
}
|
||||
|
||||
async function b09() {
|
||||
let fail = false;
|
||||
global.fetch = async (url, init) => {
|
||||
calls.push(`fetch:${init?.method ?? "GET"}:${url}`);
|
||||
if (fail && init?.method === "PUT") return { ok: false, status: 500, json: async () => ({}) };
|
||||
return { ok: true, status: 200, json: async () => any(), text: async () => "{}" };
|
||||
};
|
||||
const m = load("B09_Estimation/B09_Estimation_UI_Shell.ts", common);
|
||||
calls.length = 0;
|
||||
m.renderB09Estimation(Object.assign(new Node(), { isRoot: true }));
|
||||
const step = async (i) => {
|
||||
calls.length = 0;
|
||||
layoutOpts.onStepClick(i);
|
||||
for (let k = 0; k < 5; k++) await flush();
|
||||
return calls.filter((c) => !c.startsWith("toast"));
|
||||
};
|
||||
const out = {};
|
||||
out.beforeLoad = await (async () => {
|
||||
store.set("b09-estimation", { x: 1 });
|
||||
return step(5); // 불러오기 전 — 저장 없이 이동 · 캐시는 남음
|
||||
})();
|
||||
for (let k = 0; k < 5; k++) await flush(); // GET 응답 반영
|
||||
store.delete("b09-estimation");
|
||||
out.clean = await step(5);
|
||||
store.set("b09-estimation", { x: 1 });
|
||||
fail = true;
|
||||
out.fail = await step(5);
|
||||
fail = false;
|
||||
out.ok = await step(5);
|
||||
return out;
|
||||
}
|
||||
|
||||
// 진짜 좌측 옵션 `save` — 캐시 없으면 요청 없이 참 · 1본 빈칸 = 거짓 · 요청 실패 = 거짓
|
||||
async function options() {
|
||||
let fail = false;
|
||||
const m = load("B08_Quantity/B08_Quantity_UI_Options.ts", {
|
||||
...common,
|
||||
"@ui/ui_template_elements": {
|
||||
...common["@ui/ui_template_elements"],
|
||||
createInputField: () => ({ input: new Node("input"), root: new Node() }),
|
||||
},
|
||||
"@ui/ui_template_collapsible": { attachCollapsible() {}, createPanelCard: () => new Node() },
|
||||
"./B08_Quantity_UI_Hydro": {
|
||||
request: async (url, init) => {
|
||||
calls.push(`req:${init?.method ?? "GET"}`);
|
||||
if (fail && init?.method === "PUT") 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"),
|
||||
},
|
||||
});
|
||||
const panel = m.createOptionsPanel("P1", () => calls.push("redraw"));
|
||||
await flush();
|
||||
const run = async () => {
|
||||
calls.length = 0;
|
||||
const ok = await panel.save();
|
||||
return [ok, calls.filter((c) => !c.startsWith("toast"))];
|
||||
};
|
||||
const out = {};
|
||||
store.clear();
|
||||
out.clean = await run();
|
||||
store.set("b08-options", { soft_rock_pct: 80 });
|
||||
out.changed = await run();
|
||||
out.after = store.has("b08-options");
|
||||
store.set("b08-milestone", { x: 1 });
|
||||
out.milestoneOnly = await run();
|
||||
store.set("b08-options", { one_pipe_length_m: null });
|
||||
out.blankPipe = await run();
|
||||
store.set("b08-options", { soft_rock_pct: 70 });
|
||||
fail = true;
|
||||
out.fail = await run();
|
||||
out.keptOnFail = store.has("b08-options");
|
||||
store.clear();
|
||||
return out;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const r = { b08: await b08(), b09: await b09(), options: await options() };
|
||||
console.log(JSON.stringify(r));
|
||||
})().catch((e) => {
|
||||
console.error(e && e.stack ? e.stack : e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -224,7 +224,7 @@ def test_저장은_고친_칸만_서버가_다시_셈(tmp_path, monkeypatch):
|
||||
|
||||
def test_화면_연결():
|
||||
page = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Page.ts").read_text(encoding="utf-8")
|
||||
assert "renderMaterialSheet(body, projectId, picked)" in page
|
||||
assert "renderMaterialSheet(body, projectId, picked, (save) => onSave(body, save))" in page
|
||||
ui = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Material.ts").read_text(encoding="utf-8") + (
|
||||
ROOT / "B08_Quantity" / "B08_Quantity_Material_Tables.ts"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
@@ -138,7 +138,8 @@ def test_라우터_등록_응답_칸():
|
||||
def test_좌측_패널_컨테이너_셋과_저장_줄():
|
||||
page = (ROOT / "B08_Quantity/B08_Quantity_UI_Page.ts").read_text(encoding="utf-8")
|
||||
panel = (ROOT / "B08_Quantity/B08_Quantity_UI_Options.ts").read_text(encoding="utf-8")
|
||||
assert "leftPanel: createOptionsPanel(projectId, () => select(current))" in page
|
||||
assert "createOptionsPanel(projectId, () => select(current))" in page
|
||||
assert "leftPanel: options.panel" in page
|
||||
# 160-1 — 메인 탭 차례 = 구조물 집계표 · 자재 · 토공 · 수리 · 카드도 같은 차례(1본 → 집계표 · 할증 → 자재)
|
||||
tabs = ("summary", "material", "earthwork", "hydrology", "milestone") # 244-1 거리이정표
|
||||
ids = re.findall(r'\{ id: "(\w+)", label: "B08_Quantity_Tab_', page)
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""481-2 B08 · B09 단계 이동(☰ · 진행 단계 · [<] [>]) = 보이는 화면 저장 성공 뒤 · 실패면 머묾.
|
||||
|
||||
`helper_481_2_save_move.cjs` 가 진짜 `B08_Quantity_UI_Page.ts` · `B08_Quantity_UI_Options.ts` ·
|
||||
`B09_Estimation_UI_Shell.ts` 를 가짜 DOM · 가짜 의존 위에서 돌림.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def r():
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_481_2_save_move.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):
|
||||
b = r["b08"]
|
||||
assert b["summaryOk"] == ["save:options", "go:b09"]
|
||||
assert b["materialOk"] == ["save:options", "save:material", "go:b07"]
|
||||
|
||||
|
||||
def test_B08_저장_실패면_머묾(r):
|
||||
b = r["b08"]
|
||||
assert b["optionsFail"] == ["save:options"]
|
||||
assert b["materialFail"] == ["save:options", "save:material"]
|
||||
assert b["optionsFailSkipsTab"] == ["save:options"]
|
||||
assert b["hydroFail"] == ["save:options", "save:hydro"]
|
||||
|
||||
|
||||
def test_B08_떠난_탭은_저장_안_부름(r):
|
||||
assert r["b08"]["leftTab"] == ["save:options", "go:b09"]
|
||||
|
||||
|
||||
def test_B08_좌측_옵션_save(r):
|
||||
o = r["options"]
|
||||
assert o["clean"] == [True, []] # 고침 없음 = 요청 없이 참
|
||||
assert o["changed"] == [True, ["req:PUT", "req:milestone", "redraw"]]
|
||||
assert o["after"] is False
|
||||
assert o["milestoneOnly"][0] is True and "req:PUT" in o["milestoneOnly"][1]
|
||||
assert o["blankPipe"] == [False, []] # 1본 빈칸 = 머묾
|
||||
assert o["fail"] == [False, ["req:PUT"]]
|
||||
assert o["keptOnFail"] is True
|
||||
|
||||
|
||||
def test_B09_고친_값_저장_성공_뒤_이동(r):
|
||||
b = r["b09"]
|
||||
assert b["beforeLoad"] == ["go:b08"] # 불러오기 전 = 저장 없이(캐시 남음)
|
||||
assert b["clean"] == ["go:b08"] # 고침 없음 = 요청 없이
|
||||
assert b["fail"] == ["fetch:PUT:/api/projects/P1/estimation"] # 실패 = 머묾
|
||||
assert b["ok"] == ["fetch:PUT:/api/projects/P1/estimation", "go:b08"]
|
||||
|
||||
|
||||
def test_자재_수리_탭_저장_등록():
|
||||
material = (ROOT / "B08_Quantity/B08_Quantity_UI_Material.ts").read_text(encoding="utf-8")
|
||||
hydro = (ROOT / "B08_Quantity/B08_Quantity_UI_Hydro.ts").read_text(encoding="utf-8")
|
||||
assert re.search(
|
||||
r'onSave\?\.\(async \(\) => !readState\("b08-materials", projectId\) \|\| saveAll\(\)\)',
|
||||
material,
|
||||
)
|
||||
assert re.search(
|
||||
r"onSave\?\.\(async \(\) => !Object\.keys\(drafts\)\.length \|\| saveAll\(\)\)", hydro
|
||||
)
|
||||
for src in (material, hydro):
|
||||
assert "const saveAll = async (): Promise<boolean>" in src
|
||||
Reference in New Issue
Block a user