Merge remote-tracking branch 'origin/sub_laptop_8' into main_laptop_1
This commit is contained in:
@@ -189,6 +189,20 @@ export function invalidateDesignDrawing(projectId: string, drawingId: string): P
|
||||
);
|
||||
}
|
||||
|
||||
/** [초기화](298-2) 답 — 풀린 장 수 · 지운 설정 파일 · 잠정으로 돌린 측점 수. */
|
||||
export interface DesignDrawingResetResponse {
|
||||
status: string;
|
||||
project_id: string;
|
||||
released: number;
|
||||
cleared: string[];
|
||||
provisional: number;
|
||||
}
|
||||
|
||||
/** [초기화](298-2) — 저장본 확정 풂 · 설정 지움(서버 `_Router_Reset.py`) · 본문 없음. */
|
||||
export function resetDesignDrawings(projectId: string): Promise<DesignDrawingResetResponse> {
|
||||
return requestJson(`/projects/${projectId}/design-drawings/reset`, { method: "POST" });
|
||||
}
|
||||
|
||||
/** B07 단계(4) 완료(281) — 페이지 이동 저장 뒤 B08 · B09 로 갈 때(확정 잠금 없음). */
|
||||
export function completeDesignStage(projectId: string): Promise<void> {
|
||||
return requestJson(`/projects/${projectId}/design-drawings-complete`, { method: "POST" });
|
||||
|
||||
@@ -12,7 +12,12 @@
|
||||
|
||||
import "./B07_DesignDetail_UI_Style.css";
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { hideLoadingOverlay, showLoadingOverlay, showToast } from "@ui/ui_template_elements";
|
||||
import {
|
||||
createButton,
|
||||
hideLoadingOverlay,
|
||||
showLoadingOverlay,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
||||
import { CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
||||
// 좌측 패널(도면 목록·설계 정보)은 파일이 700줄을 넘어 떼어냈다(2026-09-04).
|
||||
@@ -24,6 +29,7 @@ import {
|
||||
} from "./B07_DesignDetail_UI_Panels";
|
||||
import { workflowSteps } from "../A00_Common/b_page_scaffold";
|
||||
import {
|
||||
clearWorkflowStateCache,
|
||||
fetchWorkflowState,
|
||||
goToWorkflowStage,
|
||||
WORKFLOW_STEP_ROUTES,
|
||||
@@ -48,7 +54,13 @@ import {
|
||||
} from "./B07_DesignDetail_Api_Fetch";
|
||||
import { appendStructureEntities } from "./B07_DesignDetail_UI_Cad_Structures";
|
||||
import { buildGuideToggle, guideShown } from "./B07_DesignDetail_UI_GuideToggle";
|
||||
import { hasRedrawable, redrawFromSource, saveEditedDrawings } from "./B07_DesignDetail_UI_Save";
|
||||
import {
|
||||
hasRedrawable,
|
||||
redrawFromSource,
|
||||
resetDrawings,
|
||||
saveDrawingsNow,
|
||||
saveEditedDrawings,
|
||||
} from "./B07_DesignDetail_UI_Save";
|
||||
import { openDrawingSettings } from "./B07_DesignDetail_UI_Settings";
|
||||
// CAD iframe 부모 흐름 · 도각 편집은 M02 와 한 벌로 공용에 둔다(PLAN 10-3).
|
||||
import { createCadHost, type CadSaveResult } from "@ui/cad_host/cad_host";
|
||||
@@ -451,13 +463,35 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
// 하단 고정은 공용 ui-sidebar-actions로 통일 — 사이드 본문이 [스크롤 영역][액션 줄]로
|
||||
// 쪼개지고 액션 줄은 스크롤 밖에 남는다(2026-08-18 사용자 지시, B04~B07 공통).
|
||||
actions.className = "b07-drawing-actions ui-sidebar-actions";
|
||||
// 액션 칸은 두 줄이다 — 1행 도각 편집 띠(평소 숨김), 2행 [도각 편집]
|
||||
// (2026-09-02 사용자 지시 — CAD 리본과 겹치던 떠 있는 띠를 여기로 옮김 · 확정 단추는 281 에 지움).
|
||||
// 액션 칸은 세 줄이다 — 1행 도각 편집 띠(평소 숨김), 2행 [도각 편집]
|
||||
// (2026-09-02 사용자 지시 — CAD 리본과 겹치던 떠 있는 띠를 여기로 옮김 · 확정 단추는 281 에 지움),
|
||||
// 3행 [?] [<] [>] [저장] [초기화](298-2 · B05 · B06 과 같은 차례 · [?] [<] [>] 는 공용이 앞에 붙임).
|
||||
const frameRow = document.createElement("div");
|
||||
frameRow.className = "b07-drawing-actions__row";
|
||||
frameRow.append(frameEditor.button);
|
||||
const actionRow = document.createElement("div");
|
||||
actionRow.className = "b07-drawing-actions__row";
|
||||
actionRow.dataset.helpRow = "";
|
||||
actionRow.append(frameEditor.button);
|
||||
actions.append(frameEditor.banner, actionRow);
|
||||
const refreshStages = (): void => {
|
||||
if (!projectId) return;
|
||||
clearWorkflowStateCache();
|
||||
void fetchWorkflowState(projectId)
|
||||
.then((state) => layout.setStages(state.stages))
|
||||
.catch(() => undefined);
|
||||
};
|
||||
const saveButton = createButton({
|
||||
label: L("Common_Btn_Save"),
|
||||
variant: "ghost",
|
||||
onClick: () => void saveDrawingsNow(store, refreshStages),
|
||||
});
|
||||
const resetButton = createButton({
|
||||
label: L("Common_Btn_Reset"),
|
||||
variant: "danger-ghost",
|
||||
onClick: () => void resetDrawings(store),
|
||||
});
|
||||
saveButton.disabled = resetButton.disabled = !projectId;
|
||||
actionRow.append(saveButton, resetButton);
|
||||
actions.append(frameEditor.banner, frameRow, actionRow);
|
||||
|
||||
drawingPanel.append(infoPanelHost, actions);
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
* 고친 탭(●) + 지금 탭을 `confirm` 길로 · 안 고친 도면은 보내지 않음(늘 원본에서 새로 그림).
|
||||
* 잠금 없음 — 저장본도 그대로 고침 · 다시 옮길 때 다시 저장.
|
||||
* 원본 되돌림 = ⚙ 모달 [원본에서 다시 그리기] — 저장본이면 `invalidate` · 고친 것 버리고 다시 실음.
|
||||
* 좌측 [저장](298-2) = 이동 없이 같은 저장 · [초기화] = 물음 → `reset`(저장본 풂 · 설정 지움) →
|
||||
* CAD 자동백업 지움 → 페이지 다시 읽음(기준 = 지금 앞 단계로 그린 원본).
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
@@ -19,6 +21,7 @@ import type { CadTabs } from "@ui/cad_host/cad_host_tabs";
|
||||
import {
|
||||
confirmDesignDrawing,
|
||||
invalidateDesignDrawing,
|
||||
resetDesignDrawings,
|
||||
type CadDrawing,
|
||||
type DesignDrawingItem,
|
||||
type QuantityTable,
|
||||
@@ -145,3 +148,48 @@ export async function redrawFromSource(store: DrawingStore): Promise<boolean> {
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
/** 좌측 [저장](298-2) — 이동 없이 고친 탭 저장 · 저장했으면 `onSaved`(단계 색 다시 읽음). */
|
||||
export async function saveDrawingsNow(store: DrawingStore, onSaved: () => void): Promise<void> {
|
||||
if (!store.cadDirty() && !store.tabs.anyDirty()) {
|
||||
showToast("고친 도면이 없습니다.", "info");
|
||||
return;
|
||||
}
|
||||
if (!(await saveEditedDrawings(store))) return;
|
||||
showToast("저장했습니다.", "success");
|
||||
onSaved();
|
||||
}
|
||||
|
||||
/** CAD 자동백업 칸(`openwebcad/src/helpers/autosave.ts`) — 칸 이름 = 도면 id. */
|
||||
const CAD_RECOVERY_KEY = "OPEN_WEB_CAD__RECOVERY";
|
||||
|
||||
/**
|
||||
* 좌측 [초기화](298-2 · 300 C1 · C3) — 물은 뒤 서버가 저장본을 풀고 설정을 지움 → 고친 것 · CAD 자동백업 버림 →
|
||||
* 페이지 다시 읽음(다음 GET 이 지금 앞 단계 저장본으로 원본을 그림). [취소] = 그대로.
|
||||
*/
|
||||
export async function resetDrawings(store: DrawingStore): Promise<void> {
|
||||
if (store.isFrameEditing()) {
|
||||
showToast("도각 편집을 마친 뒤 초기화하세요.", "warning");
|
||||
return;
|
||||
}
|
||||
const ok = await showConfirmDialog(
|
||||
"이 페이지에서 저장한 도면 · 고친 것 · 설정(축척 · 횡단 배치 · 평면도 종류 · 표식)을 모두 지우고 " +
|
||||
"지금 앞 단계 값으로 다시 그릴까요?",
|
||||
"초기화",
|
||||
);
|
||||
if (!ok) return;
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
await resetDesignDrawings(store.projectId);
|
||||
store.drawings.forEach((item) => localStorage.removeItem(`${CAD_RECOVERY_KEY}__${item.id}`));
|
||||
showToast("초기화했습니다.", "success");
|
||||
location.reload();
|
||||
} catch (error) {
|
||||
showToast(
|
||||
error instanceof Error ? error.message : "상세 설계를 초기화하지 못했습니다.",
|
||||
"error",
|
||||
);
|
||||
} finally {
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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"]
|
||||
Reference in New Issue
Block a user