Merge remote-tracking branch 'origin/sub_laptop_6' into main_laptop_1

This commit is contained in:
2026-11-01 14:53:03 +09:00
12 changed files with 281 additions and 64 deletions
+53 -6
View File
@@ -1,8 +1,8 @@
/* =============================================================================
* M01_MasterData_UI_Page.ts
* 마스터 요소 화면 — 좌측 도킹 패널(컨테이너: 인력·재료·기계·소요량·계수·환율·요율·단가산출 로직)
* · 일위대가 조합은 M02 마스터 템플릿(PLAN 27-2b)
* / 우측 요소 표 · 표 목록 · 로직 편집
* · 일위대가 조합은 M02 마스터 템플릿(PLAN 27-2b) · 구조물 옵션 정의(716-1 · 화면은 M02 `StructOptions/`)
* / 우측 요소 표 · 표 목록 · 로직 편집 · 옵션 정의 · 주소 `?open=로직|옵션` = 그 칸을 고른 채 열림(716-2)
*
* 시스템 관리자만. 고친 것은 초안(sessionStorage)에 쌓이고 [저장] 한 번에 서버로 —
* 자동저장 없음.
@@ -26,7 +26,7 @@ import { t as L } from "@ui/ui_template_locale";
import { createWorkflowOverlays } from "@ui/ui_template_overlay";
import { ROUTES } from "@config/config_frontend";
import { mountSettingsDrawer } from "../A00_Common/m_settings_nav";
import { navigateTo } from "../A00_Common/router";
import { currentQuery, navigateTo, setLeaveGuard } from "../A00_Common/router";
import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
import { saveFiles, type FileInfo } from "./M01_MasterData_Api_Fetch";
import { discard, isStale, onDraftChange, payload, totalCount } from "./M01_MasterData_Draft";
@@ -89,6 +89,7 @@ function buildPage(): HTMLElement {
children: [headBar.root, buildHeadFilters([searchField.root])],
});
const logicHost = el("div", { className: "m01-master__logic", attrs: { hidden: "" } });
const optionsHost = el("div", { className: "m02-sod__host", attrs: { hidden: "" } });
const elementView = el("div", {
className: "m01-master__elements",
children: [
@@ -112,12 +113,50 @@ function buildPage(): HTMLElement {
onDraftChange(refreshBar);
refreshBar();
const showMode = (mode: "elements" | "logic"): void => {
const showMode = (mode: "elements" | "logic" | "options"): void => {
logicHost.hidden = mode !== "logic";
elementView.hidden = mode !== "elements";
optionsHost.hidden = mode !== "options";
if (mode !== "options") side.setOptionsMark(false);
};
/* --- 구조물 옵션 정의(716-1) — 저장 안 한 고침은 칸을 바꿀 때 · 페이지를 떠날 때 물음 --- */
let optionsDirty = false;
const leaveOptions = async (): Promise<boolean> => {
if (!optionsDirty || optionsHost.hidden) return true;
if (!(await showConfirmDialog(L("M02_DiscardConfirm")))) return false;
optionsDirty = false;
return true;
};
/** 옵션 정의에 고침이 있으면 물은 뒤에 `run` — 취소면 옵션 줄 표시를 되돌려 머묾 */
const afterLeave = (run: () => void): boolean => {
if (!optionsDirty || optionsHost.hidden) return true;
void leaveOptions().then((ok) => {
if (ok) return run();
side.setActive(null);
side.setOptionsMark(true);
});
return false;
};
const showOptions = (label: string, node: HTMLElement): ((dirty: boolean) => void) => {
dispose();
dispose = (): void => {};
current = null;
optionsDirty = false;
optionsHost.replaceChildren(
el("div", {
className: "m01-master__headbox",
children: [buildHeadBar({ title: label, buttons: [] }).root],
}),
node,
);
showMode("options");
setLeaveGuard(leaveOptions);
return (dirty) => (optionsDirty = dirty);
};
const openFile = (pick: Pick): void => {
if (!afterLeave(() => openFile(pick))) return;
dispose();
showMode("elements");
if (pick.view) {
@@ -203,6 +242,7 @@ function buildPage(): HTMLElement {
/** 「단가산출 로직」 탭으로 전환 — `key` 가 있으면 그 로직을 바로 엶. */
const openLogicTab = (key?: string): void => {
if (key) setPendingLogicKey(key);
if (!afterLeave(() => openLogicTab())) return;
dispose();
dispose = (): void => {};
current = null;
@@ -259,7 +299,11 @@ function buildPage(): HTMLElement {
setMasterJump((target) => void jump(target));
/* --- 좌측: 컨테이너 --- */
const side = buildSide(openFile, () => openLogicTab());
const side = buildSide(openFile, () => openLogicTab(), {
confirmLeave: leaveOptions,
show: showOptions,
onOpen: () => side.setActive(null),
});
const layout = el("div", { className: "ui-workflow-layout m01-master" });
const main = el("main", {
@@ -267,7 +311,7 @@ function buildPage(): HTMLElement {
children: [
el("div", {
className: "m01-master__panel",
children: [elementView, logicHost],
children: [elementView, logicHost, optionsHost],
}),
],
});
@@ -287,5 +331,8 @@ function buildPage(): HTMLElement {
);
// 다른 화면(일위대가 조합 다이어그램 등)이 로직 키를 들고 왔으면 로직 탭으로 바로 엶
if (hasPendingLogicKey()) openLogicTab();
// 주소 `?open=` — 대시보드 설계 기본값 줄 · 옛 M02 `open=옵션` 즐겨찾기가 그 칸을 고른 채 엶
const want = currentQuery().get("open");
if (want === "로직" || want === "옵션") side.reveal(want);
return layout;
}
+45 -4
View File
@@ -1,13 +1,19 @@
/* =============================================================================
* M01_MasterData_UI_Side.ts
* 왼쪽 패널 — 공용 접기 컨테이너(`ui-collapsible ui-sidebar-section`) 아홉 개
* 왼쪽 패널 — 공용 접기 컨테이너(`ui-collapsible ui-sidebar-section`)
* 인력 · 재료 · 기계 · 소요량 · 계수 · 환율 · 요율 · 단가산출 로직(일위대가 조합은 M02 · PLAN 27-2b)
* ─ 구분선 ─ 구조물 옵션 정의(716-1 · 화면 코드는 M02 `StructOptions/` 그대로 · 입구만 여기)
* 펼치면 하위 목록(거름 · 파일) · 환율/요율은 누르면 바로 표 · 로직은 안에 로직 목록
* ========================================================================== */
import { createSelectField, el, showToast } from "@ui/ui_template_elements";
import { attachCollapsible } from "@ui/ui_template_collapsible";
import { createDivider } from "@ui/ui_template_divider";
import { currentLanguageIndex, t as L } from "@ui/ui_template_locale";
import {
buildOptionsSection,
type OptionsSectionOptions,
} from "../M02_MasterTemplete/StructOptions/M02_SO_Options";
import { fetchFiles, fetchSubsAll, type FileInfo, type SubInfo } from "./M01_MasterData_Api_Fetch";
import { tx } from "./M01_MasterData_UI_Logic_Text";
import { renderTree, type MakeRow } from "./M01_MasterData_UI_Tree";
@@ -103,6 +109,10 @@ export interface SideHandle {
setOnPicked: (fn: () => void) => void;
/** 「전체」 + 드롭다운 거름 한 벌 — 로직 컨테이너도 씀 */
filter: (args: FilterArgs) => HTMLElement;
/** 주소 `open` — 그 컨테이너를 펼치고 고름(로직 = 로직 목록 · 옵션 = 정의 화면까지) */
reveal: (target: "로직" | "옵션") => void;
/** 옵션 줄 고름 표시 지움(다른 화면을 열었음) · 켬(떠나기를 취소해 머묾) */
setOptionsMark: (on: boolean) => void;
}
export interface FilterArgs {
@@ -157,7 +167,11 @@ const LEAF: Group[] = ["환율", "요율"];
const FILTERED: Group[] = ["인력", "기계", "소요량", "계수"];
const LOGIC_ID = "로직|";
export function buildSide(onOpen: (pick: Pick) => void, onLogic: () => void): SideHandle {
export function buildSide(
onOpen: (pick: Pick) => void,
onLogic: () => void,
optionsOpt: Omit<OptionsSectionOptions, "isAdmin">,
): SideHandle {
const lists = new Map<Group, FileInfo[]>();
const bodies = new Map<Group, HTMLElement>();
const buttons = new Map<string, HTMLElement>();
@@ -483,9 +497,12 @@ ${first.file}`
onLogic();
});
// 구조물 옵션 정의 — 로직 칸 아래 구분선 + 칸 하나(시스템 관리자 화면이라 고침 가능)
const options = buildOptionsSection({ ...optionsOpt, isAdmin: true });
const optionsSec = section(options.title, options.body);
const root = el("div", {
className: "m01-side ui-sidebar-panel",
children: [...groups, logic],
children: [...groups, logic, createDivider(), optionsSec],
});
attachCollapsible(root);
void Promise.all(GROUPS.filter((g) => g !== FUEL_GROUP).map(refresh)).catch((error) =>
@@ -497,5 +514,29 @@ ${first.file}`
// 재료는 유가 · 전력 파일까지 다(알약 → 마스터 화면 길이 파일로 찾음) · 유가 · 전력은 그 파일만
const files = (group: string): FileInfo[] =>
group === FUEL_GROUP ? shown(FUEL_GROUP) : (lists.get(group as Group) ?? []);
return { root, logicHost, refresh, files, setActive, setOnPicked, filter };
const reveal = (target: "로직" | "옵션"): void => {
if (target === "옵션") {
optionsSec.classList.remove("is-collapsed");
options.body.querySelector<HTMLElement>("button")?.click();
return;
}
logic.classList.remove("is-collapsed");
setActive(buttons.has(LOGIC_ID) ? LOGIC_ID : null);
onLogic();
};
const setOptionsMark = (on: boolean): void => {
if (!on) return options.clear();
options.body.querySelector("button")?.classList.add("is-active");
};
return {
root,
logicHost,
refresh,
files,
setActive,
setOnPicked,
filter,
reveal,
setOptionsMark,
};
}