feat(M01): 단가산출 로직 단추 정리 · 로직 상세 두 칸
- 「식으로 새로 만들기」 단추 · 화면 길 지움(LogicLab_New 다섯 파일 — 쓰는 곳 없음) - 「시험판 ▾」 메뉴 · 시험판 단추 둘(CostFlow · DataFlow 입구) · 가져오기 줄 지움 - 「본떠 만들기」 → 「로직 복사/생성」 · 단추 차례 = ← 로직 목록 · 로직 복사/생성 · 로직 지우기 · 고친 것 버리기 · 저장 - 로직 상세 두 칸(LogicLab_Flow) — 왼쪽 지금 내용 · 오른쪽 흐름도 자리 · 끌기 막대 · 너비 비율 기억(localStorage · 25~75%) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_LogicLab.ts
|
||||
* 관리자 화면 — 로직 칸. 가운데 단가산출 상세(호표) 고치기 / 오른쪽 시험 계산
|
||||
* 관리자 화면 — 로직 칸. 로직을 열면 두 칸 — 왼쪽 단가산출 상세(호표) · 시험 계산 / 오른쪽 흐름도
|
||||
* 로직 목록(원문·장 고르기 · 찾기 · 막힘 거름)은 왼쪽 패널 「단가산출 로직」 컨테이너(`listHost`) 안
|
||||
*
|
||||
* 데이터 흐름(CLAUDE.md 5장) — 고친 것은 캐시(sessionStorage)에 쌓고 [저장] 한 번에 `POST /save`.
|
||||
* 409 = 그 사이 파일이 바뀜 · 422 = 검사 걸림(아무것도 안 씀) — 까닭은 서버 글 그대로 보임.
|
||||
* ========================================================================== */
|
||||
|
||||
import { currentLanguageIndex, t as L } from "@ui/ui_template_locale";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import {
|
||||
createButton,
|
||||
el,
|
||||
@@ -37,12 +37,7 @@ import { buildLabDetail, isOwnLogic } from "./M01_MasterData_UI_LogicLab_Detail"
|
||||
import { setLogicOpener } from "./M01_MasterData_UI_LogicLab_Modal";
|
||||
import { openCopyScreen } from "./M01_MasterData_UI_LogicLab_Copy";
|
||||
import { tl } from "./M01_MasterData_UI_LogicLab_Text";
|
||||
import { buildNewLogic } from "./M01_MasterData_UI_LogicLab_New";
|
||||
import { tn } from "./M01_MasterData_UI_LogicLab_New_Text";
|
||||
// 단가산출 다이어그램 시험 길 — 폴더째 지우면 이 두 줄도 지움
|
||||
import { createCostFlowButton } from "../M02_MasterTemplete/CostFlow/M02_CF_Entry";
|
||||
// 단가산출 흐름도 시험 길 — 폴더째 지우면 이 두 줄도 지움
|
||||
import { createDataFlowButton } from "../M02_MasterTemplete/DataFlow/M02_DF_Entry";
|
||||
import { buildSplit } from "./M01_MasterData_UI_LogicLab_Flow";
|
||||
import "./M01_MasterData_UI_Logic_Style.css";
|
||||
|
||||
/** 저장 안 한 로직 하나 — origKey null = 새 로직 · row null = 지움 */
|
||||
@@ -72,49 +67,6 @@ interface Opened extends Draft {
|
||||
const cacheKey = "m01_logic_drafts_lab";
|
||||
const ownHost = (): HTMLElement => el("div", { className: "m01lab__own" });
|
||||
|
||||
/** 시험판 묶음 — 「시험판 ▾」 단추 하나 · 누르면 시험판 단추들이 목록으로(바깥 · Esc · 고르면 닫힘) */
|
||||
function trialMenu(items: HTMLElement[]): HTMLElement {
|
||||
const list = el("div", { attrs: { role: "menu" }, children: items });
|
||||
Object.assign(list.style, {
|
||||
position: "absolute",
|
||||
top: "calc(100% + 4px)",
|
||||
left: "0",
|
||||
zIndex: "20",
|
||||
display: "none",
|
||||
flexDirection: "column",
|
||||
gap: "var(--spacing-4)",
|
||||
padding: "var(--spacing-8)",
|
||||
border: "1px solid var(--color-border)",
|
||||
borderRadius: "var(--radius-lg)",
|
||||
background: "var(--color-surface-raised)",
|
||||
boxShadow: "var(--shadow-lg)",
|
||||
whiteSpace: "nowrap",
|
||||
});
|
||||
const shut = (): void => {
|
||||
list.style.display = "none";
|
||||
button.setAttribute("aria-expanded", "false");
|
||||
document.removeEventListener("click", outside, true);
|
||||
};
|
||||
const outside = (ev: Event): void => {
|
||||
if (!wrap.contains(ev.target as Node)) shut();
|
||||
};
|
||||
const button = createButton({
|
||||
label: ["시험판 ▾", "Trials ▾"][currentLanguageIndex === 1 ? 1 : 0],
|
||||
variant: "ghost",
|
||||
onClick: () => {
|
||||
if (list.style.display !== "none") return shut();
|
||||
list.style.display = "flex";
|
||||
button.setAttribute("aria-expanded", "true");
|
||||
document.addEventListener("click", outside, true);
|
||||
},
|
||||
});
|
||||
button.setAttribute("aria-haspopup", "menu");
|
||||
list.addEventListener("click", () => shut()); // 고른 시험판은 제 단추가 엶
|
||||
const wrap = el("div", { children: [button, list] });
|
||||
Object.assign(wrap.style, { position: "relative", display: "inline-flex" });
|
||||
wrap.addEventListener("keydown", (ev) => ev.key === "Escape" && shut());
|
||||
return wrap;
|
||||
}
|
||||
const clone = <T>(v: T): T => JSON.parse(JSON.stringify(v)) as T;
|
||||
|
||||
function loadDrafts(): Record<string, Draft> {
|
||||
@@ -137,13 +89,13 @@ export async function mountM01LogicLab(
|
||||
): Promise<LogicHandle> {
|
||||
let drafts = loadDrafts();
|
||||
let opened: Opened | null = null;
|
||||
let creating = false; // 「식으로 새로 만들기」 화면이 열려 있음
|
||||
let calcInputs = "";
|
||||
let recalc: number | undefined;
|
||||
const editor = el("div", { className: "m01-logic__editor" });
|
||||
// 로직 상세 두 칸 — 왼쪽 지금 내용 · 오른쪽 흐름도(패널 자리) · 너비 비율 기억
|
||||
const split = buildSplit(editor);
|
||||
const errors = el("div", { className: "m01-logic__reasons", attrs: { hidden: "" } });
|
||||
const calc = el("div", { className: "m01lab__calc" });
|
||||
const newHost = el("div", { className: "m01lab__new", attrs: { hidden: "" } });
|
||||
const filterHost = el("div");
|
||||
const waiting = el("p", { className: "m01-logic__muted", text: tx("Loading") });
|
||||
let loaded = false; // 첫 목록이 오기 전에는 「0 줄」 빈 표를 보이지 않음
|
||||
@@ -233,12 +185,11 @@ export async function mountM01LogicLab(
|
||||
|
||||
const drawEditor = (): void => {
|
||||
// 고른 로직이 없으면 목록 · 있으면 호표 표 + 시험 계산
|
||||
const listing = !opened && !creating;
|
||||
const listing = !opened;
|
||||
list.root.hidden = !listing || !loaded;
|
||||
waiting.hidden = loaded;
|
||||
editor.hidden = !opened;
|
||||
split.root.hidden = !opened;
|
||||
back.hidden = listing;
|
||||
newHost.hidden = !creating;
|
||||
if (!opened) return;
|
||||
if (!opened.row) {
|
||||
editor.replaceChildren(
|
||||
@@ -264,7 +215,6 @@ export async function mountM01LogicLab(
|
||||
|
||||
const show = (next: Opened | null): void => {
|
||||
opened = next;
|
||||
creating = false;
|
||||
errors.hidden = true;
|
||||
drawEditor();
|
||||
drawCalc();
|
||||
@@ -326,7 +276,7 @@ export async function mountM01LogicLab(
|
||||
restore: true,
|
||||
onPick: (sub, detail) => {
|
||||
list.setPick(sub, detail);
|
||||
if (opened || creating) show(null);
|
||||
if (opened) show(null);
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -410,65 +360,36 @@ export async function mountM01LogicLab(
|
||||
}
|
||||
};
|
||||
|
||||
/** 식으로 새로 만들기 — 저장하면 목록을 다시 받아 그 로직의 상세 화면을 엶 */
|
||||
const startNew = (): void => {
|
||||
opened = null;
|
||||
creating = true;
|
||||
newHost.replaceChildren(
|
||||
buildNewLogic({
|
||||
onSaved: (key) => {
|
||||
void (async () => {
|
||||
try {
|
||||
await reload();
|
||||
await openKeyOn(key);
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error");
|
||||
}
|
||||
})();
|
||||
},
|
||||
onClose: () => show(null),
|
||||
}),
|
||||
);
|
||||
drawEditor();
|
||||
drawCalc();
|
||||
};
|
||||
|
||||
const saveButton = createButton({ label: tx("Bar_Save"), onClick: () => void onSave() });
|
||||
const discardButton = createButton({
|
||||
label: tx("Bar_Discard"),
|
||||
variant: "ghost",
|
||||
onClick: () => void onDiscard(),
|
||||
});
|
||||
const copyButton = createButton({
|
||||
label: tl("Copy_Open"),
|
||||
variant: "ghost",
|
||||
onClick: () =>
|
||||
opened?.origKey
|
||||
? void openCopyScreen({
|
||||
key: opened.origKey,
|
||||
afterSave: async (newKey) => {
|
||||
await reload();
|
||||
await open(logicId("자체", newKey), "자체", newKey);
|
||||
},
|
||||
})
|
||||
: showToast(tl("Copy_NeedOpen"), "info"),
|
||||
});
|
||||
const bar = el("div", {
|
||||
className: "m01-logic__bar",
|
||||
children: [
|
||||
back,
|
||||
el("h2", { text: tx("Title") }),
|
||||
pending,
|
||||
createButton({ label: tn("Open"), variant: "filled", onClick: startNew }),
|
||||
trialMenu([
|
||||
// 단가산출 다이어그램 시험 길 — 폴더째 지우면 이 두 줄도 지움
|
||||
createCostFlowButton(),
|
||||
// 단가산출 흐름도 시험 길 — 폴더째 지우면 이 두 줄도 지움
|
||||
createDataFlowButton(),
|
||||
]),
|
||||
copyButton,
|
||||
createButton({ label: tx("Bar_Delete"), variant: "danger", onClick: () => void onDelete() }),
|
||||
discardButton,
|
||||
saveButton,
|
||||
createButton({
|
||||
label: tl("Copy_Open"),
|
||||
variant: "ghost",
|
||||
onClick: () =>
|
||||
opened?.origKey
|
||||
? void openCopyScreen({
|
||||
key: opened.origKey,
|
||||
afterSave: async (newKey) => {
|
||||
await reload();
|
||||
await open(logicId("자체", newKey), "자체", newKey);
|
||||
},
|
||||
})
|
||||
: showToast(tl("Copy_NeedOpen"), "info"),
|
||||
}),
|
||||
],
|
||||
});
|
||||
host.replaceChildren(
|
||||
@@ -477,7 +398,7 @@ export async function mountM01LogicLab(
|
||||
children: [
|
||||
el("main", {
|
||||
className: "m01-logic__main",
|
||||
children: [bar, errors, waiting, list.root, editor, newHost],
|
||||
children: [bar, errors, waiting, list.root, split.root],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user