feat(497-1 B07 단추 줄): 좌측 아래 [?] [<] [>] [저장] [초기화] · [도각 편집] = 윗줄

- [저장] = 이동 없이 고친 탭(●) + 지금 탭 저장 · 고친 것 없으면 요청 0 · 저장 뒤 단계 색 다시 읽음
- [초기화] = danger-ghost · 맨 오른쪽 · 물음 → 오버레이 → POST design-drawings/reset → CAD 자동백업(도면마다) 지움 → 다시 읽음
- [>] = 지금 길 그대로(고친 것 없으면 저장 요청 없이 이동)
- 새 시험 test_298_2_b07_buttons · test_280_5_b07_settings 줄 자리 고쳐 씀

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 20:26:59 +09:00
co-authored by Claude Opus 5.5
parent 1c179ac5e3
commit 878376e295
6 changed files with 304 additions and 7 deletions
@@ -0,0 +1,107 @@
/* 298-2 B07 좌측 [저장] · [초기화] 헬퍼 — `_UI_Save` 의 saveDrawingsNow · resetDrawings 를 Node 로 돌려
* 부른 차례(물음 · 오버레이 · 창구 · 자동백업 지움 · 다시 읽음)를 표준출력 JSON 한 벌로.
* TS 는 그 자리에서 트랜스파일 · 서버 길 · 알림 · 물음은 기록용 가짜.
*/
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const B07 = path.join(__dirname, "..", "..", "B07_DesignDetail");
const calls = [];
let answer = true;
let failReset = false;
const STUBS = {
"@ui/ui_template_elements": {
showToast: (message, kind) => calls.push(["toast", kind]),
showLoadingOverlay: () => calls.push(["overlay+"]),
hideLoadingOverlay: () => calls.push(["overlay-"]),
showConfirmDialog: async () => (calls.push(["ask"]), answer),
},
"./B07_DesignDetail_Api_Fetch": {
confirmDesignDrawing: async (projectId, id) => calls.push(["confirm", id]),
invalidateDesignDrawing: async () => calls.push(["invalidate"]),
resetDesignDrawings: async (projectId) => {
calls.push(["reset", projectId]);
if (failReset) throw new Error("서버 오류");
return { status: "success", released: 1, cleared: [], provisional: 0 };
},
},
};
const storage = new Map();
global.localStorage = {
removeItem: (key) => (calls.push(["drop", key]), storage.delete(key)),
};
global.location = { reload: () => calls.push(["reload"]) };
global.document = { querySelectorAll: () => [] };
function loadTs(file) {
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const box = { exports: {} };
new Function("exports", "module", "require", js)(box.exports, box, (r) => STUBS[r] ?? {});
return box.exports;
}
const Save = loadTs(path.join(B07, "B07_DesignDetail_UI_Save.ts"));
const storeOf = (over = {}) => {
let dirty = !!over.cadDirty;
const tab = { id: "cross_s1", dirty };
return {
projectId: "p1",
drawings: [
{ id: "cover", kind: "cover", label: "표지", confirmed: false },
{ id: "cross_s1", kind: "cross", label: "횡단 1", confirmed: false },
],
tabs: {
tabs: () => [tab],
active: () => tab,
markClean: (t) => ((t ?? tab).dirty = false),
anyDirty: () => tab.dirty,
},
current: () => ({ id: "cross_s1", kind: "cross", label: "횡단 1", confirmed: false }),
cadDirty: () => dirty,
setCadClean: () => (dirty = false),
requestCad: async () => ({ drawing: {}, quantityTable: null }),
isFrameEditing: () => !!over.frame,
forget: () => undefined,
reload: async () => undefined,
};
};
const take = () => calls.splice(0).map((c) => c.join(":"));
(async () => {
const out = {};
let saved = 0;
const onSaved = () => saved++;
await Save.saveDrawingsNow(storeOf(), onSaved);
out.saveNone = { calls: take(), saved };
await Save.saveDrawingsNow(storeOf({ cadDirty: true }), onSaved);
out.saveEdited = { calls: take(), saved };
await Save.resetDrawings(storeOf({ cadDirty: true }));
out.reset = take();
answer = false;
await Save.resetDrawings(storeOf());
answer = true;
out.resetCancel = take();
failReset = true;
await Save.resetDrawings(storeOf());
failReset = false;
out.resetFail = take();
await Save.resetDrawings(storeOf({ frame: true }));
out.resetFrame = take();
process.stdout.write(JSON.stringify(out));
})().catch((error) => {
process.stderr.write(String(error && error.stack ? error.stack : error));
process.exit(1);
});
+1 -1
View File
@@ -141,7 +141,7 @@ def test_페이지_확정_단추_잠금_자동이동_없음():
):
assert gone not in page, gone
assert "confirmed: false, // 잠금 없음(요구 281)" in page
assert "actionRow.append(frameEditor.button);" in page
assert "frameRow.append(frameEditor.button);" in page # 298-2 — [도각 편집] = 윗줄
assert "onStepClick: (stepIndex) => void moveToStage(stepIndex)," in page
move = page[
page.index("const moveToStage = async") : page.index("const layout = createWorkflowLayout")
@@ -0,0 +1,94 @@
# -*- coding: utf-8 -*-
"""298-2 B07 좌측 아래 단추 줄 [?] [<] [>] [저장] [초기화].
못박는 것
- 줄 차례 — [도각 편집] = 윗줄 · 아래 줄(help-row) = [저장] [초기화](앞 [?] [<] [>] 는 공용) ·
[초기화] = danger-ghost(B05 · B06 과 같은 꼴) · 맨 오른쪽.
- [저장] = 이동 없이 고친 탭 저장 · 고친 것 없으면 요청 0 · 저장 뒤 단계 색 다시 읽음.
- [초기화] = 물음 → 오버레이 → POST reset → CAD 자동백업(도면마다) 지움 → 다시 읽음 ·
[취소] = 요청 0 · 실패 = 머묾(다시 읽음 없음) · 도각 편집 중 = 막음.
- 창구 = `POST /projects/{id}/design-drawings/reset` 본문 없음.
Node 헬퍼 `helper_298_2_b07_buttons.cjs` 가 저장 묶음을 돌림.
"""
from __future__ import annotations
import json
import os
import subprocess
from pathlib import Path
import pytest
HERE = os.path.dirname(os.path.abspath(__file__))
B07 = Path(HERE).parents[1] / "B07_DesignDetail"
def _read(name: str) -> str:
return (B07 / name).read_text(encoding="utf-8").replace("\r\n", "\n")
@pytest.fixture(scope="module")
def out():
proc = subprocess.run(
["node", os.path.join(HERE, "helper_298_2_b07_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_줄_차례_꼴():
page = _read("B07_DesignDetail_UI_Page.ts")
assert "frameRow.append(frameEditor.button);" in page
assert "actionRow.dataset.helpRow" in page
assert "actionRow.append(saveButton, resetButton);" in page
assert "actions.append(frameEditor.banner, frameRow, actionRow);" in page
reset = page[page.index("const resetButton = createButton({") :]
reset = reset[: reset.index("});")]
assert 'label: L("Common_Btn_Reset")' in reset
assert 'variant: "danger-ghost"' in reset
assert "resetDrawings(store)" in reset
save = page[page.index("const saveButton = createButton({") :]
save = save[: save.index("});")]
assert 'label: L("Common_Btn_Save")' in save
assert "saveDrawingsNow(store, refreshStages)" in save
assert "layout.setStages(state.stages)" in page
assert len(page.splitlines()) <= 700
def test_창구():
api = _read("B07_DesignDetail_Api_Fetch.ts")
block = api[api.index("export function resetDesignDrawings") :]
block = block[: block.index("}\n")]
assert "/projects/${projectId}/design-drawings/reset" in block
assert '{ method: "POST" }' in block
assert "body" not in block
def test_저장(out):
assert out["saveNone"] == {"calls": ["toast:info"], "saved": 0}
assert out["saveEdited"] == {
"calls": ["overlay+", "confirm:cross_s1", "overlay-", "toast:success"],
"saved": 1,
}
def test_초기화(out):
assert out["reset"] == [
"ask",
"overlay+",
"reset:p1",
"drop:OPEN_WEB_CAD__RECOVERY__cover",
"drop:OPEN_WEB_CAD__RECOVERY__cross_s1",
"toast:success",
"reload",
"overlay-",
]
assert out["resetCancel"] == ["ask"]
assert out["resetFail"] == ["ask", "overlay+", "reset:p1", "toast:error", "overlay-"]
assert out["resetFrame"] == ["toast:warning"]