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
+42 -15
View File
@@ -1,7 +1,7 @@
/* =============================================================================
* m_settings_nav.ts
* 시스템 설정 바로가기(334 · 요구 225-2) — 대시보드 「시스템 설정」 카드 · M01 · M02 ☰ 가 같이 쓰는
* 역할 규칙 한 벌(`settingLinks`) · ☰ 진행 단계 자리에 🏠 대시보드 + 카드(`mountSettingsDrawer`).
* 시스템 설정 바로가기(334 · 요구 225-2 · 716-2) — 대시보드 설계 기본값 줄(`settingLinks` · 칸마다 한 줄 ·
* `?open=`) / M01 · M02 ☰ 메뉴(`pageLinks` · 페이지 둘 · `mountSettingsDrawer`).
* ========================================================================== */
import { ROUTES, type RoutePath } from "@config/config_frontend";
@@ -16,52 +16,63 @@ interface SettingUser {
company_id?: number | null;
}
/** key = M01 은 「m01」 · 계산 자리 창은 「calcSites」(대시보드 카드만 · 504-2) · M02 는 진입 인자 `open` */
/** key = 페이지 열쇠(☰ · 「m01」 「m02」) 또는 칸 열쇠(대시보드 줄 · 계산 자리 「calcSites」 = 창 504-2) ·
* group = 대시보드 설계 기본값 묶음 제목 */
export interface SettingLink {
key: string;
title: string;
desc: string;
route: RoutePath;
query?: Record<string, string>;
group?: string;
}
function L(key: keyof typeof ui_locales): string {
return ui_locales[key][currentLanguageIndex];
}
const ICONS: Record<string, string> = { m01: "🗄️", 구조물조합: "🧱", 옵션: "🎛️", 도면: "📑" };
const ICONS: Record<string, string> = { m01: "🗄️", m02: "🧱" };
/** 시스템 설정 — 역할로 카드 숨김 · M02 카드는 `?open=&root=` 로 진입(27-1d) */
/** 설계 기본값 줄(대시보드) — 칸마다 한 줄 · 역할로 숨김 · M01 줄 `?open=`(칸 고름) · M02 줄 `?open=&root=`(27-1d).
* 칸을 더할 때는 아래 표에 한 줄(m01 / m02 호출)만 더함. */
export function settingLinks(user: SettingUser, companyId: number | string | null): SettingLink[] {
// 시스템 관리자는 고른 회사를 해시에 실음(29-1)
const company: Record<string, string> =
user.role === "SYSTEM_ADMIN" && companyId ? { company: String(companyId) } : {};
const m01 = (key: string, title: string, desc: string, open?: string): SettingLink => ({
key,
title,
desc,
route: ROUTES.M01_MASTER_DATA,
query: open ? { open } : undefined,
group: "마스터 데이터",
});
const m02 = (title: string, open: string, root: string, desc: string): SettingLink => ({
key: open,
title,
desc,
route: ROUTES.M02_MASTER_TEMPLATE,
query: { open, root, ...company },
group: "마스터 템플릿",
});
// 구조물 조합 기본 층 = 개인(123-1 · 누구든) · 개인 층 없음(회사 없음) = 아래 역할 규칙
const comboRoot = (fallback: string): string => (user.company_id ? "personal" : fallback);
if (user.role === "SYSTEM_ADMIN") {
const desc = L("B01_Dashboard_PermMasterWrite");
const read = L("B01_Dashboard_PermSourceRead");
return [
{
key: "m01",
title: L("B01_Dashboard_MasterElements"),
desc: L("B01_Dashboard_PermSourceRead"),
route: ROUTES.M01_MASTER_DATA,
},
m01("m01", L("M01_Title"), read),
m01("로직", L("B01_Dashboard_CardLogic"), read, "로직"),
m01("옵션", L("B01_Dashboard_CardStructOptionDef"), desc, "옵션"),
m02(L("B01_Dashboard_CardPrecombo"), "일위대가조합", comboRoot("master"), desc),
m02(L("B01_Dashboard_CardStructCombo"), "구조물조합", comboRoot("master"), desc),
m02(L("B01_Dashboard_CardStructOption"), "옵션", "master", desc),
m02(L("B01_Dashboard_CardDrawing"), "도면", "master", desc),
{
key: "calcSites",
title: L("B01_CalcSites_Title"),
desc: L("B01_CalcSites_Desc"),
route: ROUTES.B01_ACCOUNT,
group: "계산",
},
];
}
@@ -76,14 +87,30 @@ export function settingLinks(user: SettingUser, companyId: number | string | nul
return [m02(L("B01_Dashboard_CardStructCombo"), "구조물조합", comboRoot(root), desc)];
}
/** ☰ 진행 단계 자리 — 🏠 대시보드 + 카드 · 지금 칸 강조 · 같은 길(M02 → M02)은 머리 가드가 안 물어 여기서 먼저 */
/** ☰ 메뉴 — 페이지 둘만(M01 = 시스템 관리자 · M02 = 누구나) · 열쇠 「m01」 「m02」 */
export function pageLinks(user: SettingUser, companyId: number | string | null): SettingLink[] {
const combo = settingLinks(user, companyId).find((link) => link.key === "구조물조합");
const links: SettingLink[] = [];
if (user.role === "SYSTEM_ADMIN")
links.push({
key: "m01",
title: L("B01_Dashboard_MasterElements"),
desc: "",
route: ROUTES.M01_MASTER_DATA,
});
if (combo)
links.push({ ...combo, key: "m02", title: L("M02_Title"), desc: "", group: undefined });
return links;
}
/** ☰ 진행 단계 자리 — 🏠 대시보드 + 페이지 둘 · 지금 페이지 강조(`activeKey` = 「m01」 「m02」) ·
* 같은 길(M02 → M02)은 머리 가드가 안 물어 여기서 먼저 */
export function mountSettingsDrawer(
user: SettingUser,
activeKey: string,
companyId: number | string | null,
): void {
// 계산 자리 = 대시보드 창(504-2) — 페이지가 아니라 ☰ 에 안 둠
const links = settingLinks(user, companyId).filter((link) => link.key !== "calcSites");
const links = pageLinks(user, companyId);
const bar = createStepBar(
links.map((link) => link.title),
links.findIndex((link) => link.key === activeKey),
@@ -1,7 +1,8 @@
/* =============================================================================
* B01_Dashboard_UI_View_SysDefaults.ts
* 시스템 관리 › 설계 기본값(549-2) — 마스터 데이터 · 구조물 조합 · 구조물 옵션 · 도면 양식 · 계산 자리.
* 카드 규칙은 `settingLinks` 한 벌(334) — 여기는 모양만(묶음 제목 + 둥근 상자 안 줄) · 계산 자리 = 창(504-2).
* 시스템 관리 › 설계 기본값(549-2 · 716-2) — 마스터 데이터(마스터 요소 · 단가산출 로직 · 구조물 옵션 정의) ·
* 마스터 템플릿(일위대가 · 구조물 조합 · 도면 양식) · 계산 자리. 칸마다 한 줄(누르면 그 칸을 고른 채 열림 · `?open=`).
* 줄 · 묶음 규칙은 `settingLinks` 한 벌(334) — 여기는 모양만(묶음 제목 + 둥근 상자 안 줄) · 계산 자리 = 창(504-2).
* ========================================================================== */
import { createButton } from "@ui/ui_template_elements";
@@ -12,22 +13,13 @@ import { pageFrame, settingCard, settingRow } from "./B01_Dashboard_UI_PageKit";
import { group } from "./B01_Dashboard_UI_View_Parts";
import type { ViewCtx } from "./B01_Dashboard_UI_Views";
/** 카드 열쇠 → 묶음 제목 — 모르는 열쇠는 「그 밖」 */
const GROUP_OF: Record<string, string> = {
m01: "마스터 데이터",
구조물조합: "구조물 · 도면",
옵션: "구조물 · 도면",
도면: "구조물 · 도면",
calcSites: "계산",
};
const open = (link: SettingLink): void =>
link.key === "calcSites" ? void openCalcSitesDialog() : navigateTo(link.route, link.query);
export function mountSysDefaults(host: HTMLElement, ctx: ViewCtx): void {
const groups = new Map<string, HTMLElement[]>();
for (const link of settingLinks(ctx.me, ctx.companyId)) {
const title = GROUP_OF[link.key] ?? "그 밖";
const title = link.group ?? "그 밖";
const openBtn = createButton({ label: "열기", variant: "ghost", onClick: () => open(link) });
groups.set(title, [...(groups.get(title) ?? []), settingRow(link.title, link.desc, openBtn)]);
}
+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,
};
}
@@ -51,6 +51,14 @@ export function entryFromHash(user?: SessionUser | null): Entry {
return { root, owner, open, company: query.get("company") || null, pre };
}
/** 옛 주소 `open=옵션`(구조물 옵션 정의는 M01 로 옮김 · 716-1) — 시스템 관리자면 M01 그 칸으로 돌려보냄 ·
* 돌려보냈으면 참(그 밖 역할은 읽기 화면이 없어져 구조물 조합으로 열림) */
export function redirectOldOptions(user: SessionUser | null): boolean {
if (user?.role !== "SYSTEM_ADMIN" || currentQuery().get("open") !== "옵션") return false;
navigateTo(ROUTES.M01_MASTER_DATA, { open: "옵션" });
return true;
}
/** 진입 해시 인자 — `navigateTo(ROUTES.M02_MASTER_TEMPLATE, entryQuery(…))` */
export const entryQuery = (entry: Entry): Record<string, string> => ({
open: entry.open,
@@ -31,6 +31,7 @@ import {
entryQuery,
layerOfRoot,
ownOwner,
redirectOldOptions,
viewCompany,
type OpenTarget,
type Root,
@@ -61,6 +62,8 @@ const NEW_ASK =
export async function renderM02MasterTemplate(root: HTMLElement): Promise<MountedPage> {
const user = await fetchSessionUser().catch(() => null);
// 구조물 옵션 정의는 M01 로 옮김(716-1) — 옛 주소는 M01 그 칸으로
if (redirectOldOptions(user)) return { covers: () => false, show: () => {}, dispose: () => {} };
const entry = entryFromHash(user); // 134-4 — 해시에 층 없음 = 개인(회사 없으면 마스터)
const company = viewCompany(entry, user); // 볼 회사(시스템 관리자는 고른 회사)
let layer = layerOfRoot(entry.root);
@@ -283,7 +286,7 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise<Mounte
// 진입 해시 `open` — 옵션 · 도면 컨테이너는 마스터에만 ·
// 옵션 화면이 뒤에 열리면 편집 칸 차례(`seq`)가 늦게 온 집계표를 버림
if (entry.open !== "구조물조합") side.reveal(entry.open);
mountSettingsDrawer(user ?? {}, entry.open, entry.company); // ☰ 시스템 설정 바로가기(334)
mountSettingsDrawer(user ?? {}, "m02", entry.company); // ☰ 시스템 설정 바로가기(334)
void tree.refresh();
// 이 화면을 떠나면 층 길을 시스템 · 자기 층으로 되돌림(다른 화면의 항목 고르기가 남의 층을 안 부르게)
return { covers: () => false, show: () => {}, dispose: () => setActiveLayer("system") };
@@ -404,3 +404,11 @@ table.m02-sod__table--wrap {
word-break: keep-all;
overflow-wrap: anywhere;
}
/* M01 본 칸(716-1) — 정의 화면을 얹는 틀: 머리 줄 아래 화면 */
.m02-sod__host {
display: flex;
flex-direction: column;
gap: var(--spacing-12);
min-width: 0;
}
@@ -1,10 +1,14 @@
"""197-1 대시보드 「구조물 옵션」 카드 → M02 「구조물 옵션 정의」 화면이 바로 열림 시험 — 조작은 사용자.
"""197-1 · 716-1 대시보드 「구조물 옵션 정의」 줄 → M01 「구조물 옵션 정의」 화면이 바로 열림 시험 — 조작은 ORCA.
716-1 — 화면 입구가 M02 에서 M01 로 옮겨감(화면 코드 · 서버 길은 M02 폴더 그대로) · 옛 M02 `open=옵션` 주소는
시스템 관리자면 M01 `?open=옵션` 으로 돌려보냄.
원인 — 진입 해시 `open=옵션` 이 옵션 줄을 눌러 정의 화면을 띄우는 사이 첫 화면(집계표)의 `mount` 가
모듈 · 번호를 기다리다 뒤늦게 끝나 정의 화면 위에 집계표를 덮음(`mount` 에 낡음 검사 없었음).
· 카드 셋 → 해시(open=구조물조합 · 옵션 · 도면) · 해시 → 진입(`entryFromHash`) Node 로 바로 돌림
· `reveal` 가 옵션만 줄을 누르고 도면은 펼치기만 · 구조물조합은 아무것도 안 함
· 줄 → 해시(M01 open=로직 · 옵션 / M02 open=일위대가조합 · 구조물조합 · 도면) · 해시 → 진입(`entryFromHash`) Node
· M02 `reveal` 가 옵션만 줄을 누르고 도면은 펼치기만 · 구조물조합은 아무것도 안 함 · M01 `reveal` 도 같은 꼴
· 옛 `open=옵션` 돌려보냄(`redirectOldOptions`)
· `mount` 집계표 갈래(진짜 소스 조각)를 Node 로 돌림 — 기다리는 사이 다른 화면이 열리면 그리지 않음
"""
@@ -41,16 +45,19 @@ def _run(script: str, *args: str) -> object:
@needs_node
def test_카드_셋_해시():
def test_줄_해시_옵션은_M01():
from test_b01_settings_cards import _cards
cards = _cards({"role": "SYSTEM_ADMIN", "is_master": False}, 3)
assert [c[2].split("?")[1].split("&")[0] for c in cards[1:4]] == [
"open=구조물조합",
"open=옵션",
"open=도면",
heads = [c[2].split("?")[0] + "?" + c[2].split("?")[1].split("&")[0] for c in cards[1:6]]
assert heads == [
"m01-master-data?open=로직",
"m01-master-data?open=옵션",
"m02-master-template?open=일위대가조합",
"m02-master-template?open=구조물조합",
"m02-master-template?open=도면",
]
assert all("&root=master" in c[2] for c in cards[1:4])
assert all("&root=master" in c[2] for c in cards[3:6])
@needs_node
@@ -69,7 +76,7 @@ def test_해시_진입_셋():
+ fn.group(0).replace("export function", "function")
+ "\nconsole.log(JSON.stringify(entryFromHash(null)));"
)
for want in ("구조물조합", "옵션", "도면"):
for want in ("구조물조합", "옵션", "도면", "일위대가조합"):
got = _run(script, f"open={want}&root=master&company=3")
assert got["open"] == want and got["root"] == "master" and got["company"] == "3"
@@ -139,3 +146,38 @@ def test_집계표_갈래_기다리는_사이_다른_화면이_열리면_그리
assert got["none"] == ["create", "panel:set"] # 안 끼어들면 그대로 그림
assert got["mod"] == ["panel:null"] # 모듈 기다리는 사이 다른 화면 — 그리지 않음
assert got["num"] == ["panel.destroy", "panel:null"] # 번호 기다리는 사이 — 패널도 버림
@needs_node
def test_옛_옵션_주소_M01로_돌려보냄():
layer = _text(M02 / "M02_MasterTemplete_Layer.ts")
fn = re.search(r"^export function redirectOldOptions\([\s\S]*?^}", layer, re.M)
assert fn, "redirectOldOptions 를 못 찾음"
script = (
"const currentQuery = () => new URLSearchParams(process.argv[1]);\n"
"const ROUTES = { M01_MASTER_DATA: 'm01' }; const sent: string[] = [];\n"
"const navigateTo = (r: string, q: any) => sent.push(r + JSON.stringify(q));\n"
"type SessionUser = any;\n"
+ fn.group(0).replace("export function", "function")
+ "\nconst a = redirectOldOptions({ role: 'SYSTEM_ADMIN' });"
+ "\nconst b = redirectOldOptions({ role: 'USER' });"
+ "\nconst c = redirectOldOptions(null);"
+ "\nconsole.log(JSON.stringify([a, b, c, sent]));"
)
assert _run(script, "open=옵션&root=master") == [True, False, False, ['m01{"open":"옵션"}']]
# 옵션이 아니면 안 돌려보냄
assert _run(script, "open=도면&root=master") == [False, False, False, []]
page = _text(M02 / "M02_MasterTemplete_UI_Page.ts")
assert "if (redirectOldOptions(user))" in page
assert 'mountSettingsDrawer(user ?? {}, "m02", entry.company);' in page
def test_M01_open_인자_로직_옵션():
m01 = ROOT / "M01_MasterData"
page = _text(m01 / "M01_MasterData_UI_Page.ts")
assert 'if (want === "로직" || want === "옵션") side.reveal(want);' in page
assert "setLeaveGuard(leaveOptions)" in page # 저장 안 한 정의 고침 — 떠날 때 물음
side = _text(m01 / "M01_MasterData_UI_Side.ts")
assert "buildOptionsSection({ ...optionsOpt, isAdmin: true })" in side
# 로직 칸 아래 구분선 부품 + 옵션 칸
assert "logic, createDivider(), optionsSec" in side
+1 -1
View File
@@ -263,7 +263,7 @@ def test_카드_시스템_관리자만_글_다_있음():
links = nav.split("export function mountSettingsDrawer")[0]
admin, rest = links.split('if (user.role === "SYSTEM_ADMIN") {')[1].split("// 회사 수정 =")
assert 'key: "calcSites"' in admin and "calcSites" not in rest
assert 'settingLinks(user, companyId).filter((link) => link.key !== "calcSites")' in nav
assert "calcSites" not in nav.split("export function pageLinks")[1] # ☰ 는 페이지 둘만(716-2)
page = _text("B01_Dashboard/B01_Dashboard_UI_View_SysDefaults.ts")
assert 'link.key === "calcSites" ? void openCalcSitesDialog()' in page
locale = _text("ui_template/ui_template_locale_m1.ts")
+61 -15
View File
@@ -58,7 +58,36 @@ def _cards(user: dict, company_id: int | None = None) -> list:
return json.loads(result.stdout)
def _pages(user: dict, company_id: int | None = None) -> list:
# ☰ 메뉴 = pageLinks(settingLinks 의 구조물 조합 줄을 바탕으로 페이지 둘만)
script = (
"const ROUTES = { M01_MASTER_DATA: 'm01-master-data',"
" M02_MASTER_TEMPLATE: 'm02-master-template', B01_ACCOUNT: 'b01-account' };\n"
"const L = (k: string) => k;\n"
"let went = '';\n"
"const navigateTo = (r: string, q?: Record<string, string>) =>"
" { went = r + (q ? '?' + new URLSearchParams(q) : ''); };\n"
+ _fn(_text(NAV), "settingLinks")
+ "\n"
+ _fn(_text(NAV), "pageLinks")
+ "\nconst user = JSON.parse(process.argv[1]);"
+ "\nconst links = pageLinks(user, JSON.parse(process.argv[2]));"
+ "\nconsole.log(JSON.stringify(links.map((l: any) => {"
+ " navigateTo(l.route, l.query); return [l.key, l.title, decodeURIComponent(went)]; })));"
)
result = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings",
"--input-type=module-typescript", "-e", script, json.dumps(user), json.dumps(company_id)],
capture_output=True, text=True, encoding="utf-8", timeout=60,
) # fmt: skip
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
M01 = "m01-master-data"
M02 = "m02-master-template"
READ_SRC = "B01_Dashboard_PermSourceRead"
WRITE_M = "B01_Dashboard_PermMasterWrite"
# 계산 자리(504-2) — 시스템 관리자만 · 이동 대신 창
CALC_SITES = ["B01_CalcSites_Title", "B01_CalcSites_Desc", "calc-sites-dialog"]
READ_CO = (
@@ -68,21 +97,40 @@ WRITE_CO = READ_CO.replace("CompanyRead", "CompanyWrite")
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
def test_시스템_관리자_카드_다섯_고른_회사_해시():
m = "B01_Dashboard_PermMasterWrite"
def test_시스템_관리자_줄_칸별_일곱_고른_회사_해시():
# 716-2 — 대시보드 설계 기본값 = 칸별 줄: M01 3(마스터 요소 · 단가산출 로직 · 구조물 옵션 정의) +
# M02 3(일위대가 · 구조물 조합 · 도면 양식) + 계산 자리
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False}, 3) == [
["B01_Dashboard_MasterElements", "B01_Dashboard_PermSourceRead", "m01-master-data"],
["B01_Dashboard_CardStructCombo", m, f"{M02}?open=구조물조합&root=master&company=3"],
["B01_Dashboard_CardStructOption", m, f"{M02}?open=옵션&root=master&company=3"],
["B01_Dashboard_CardDrawing", m, f"{M02}?open=도면&root=master&company=3"],
["M01_Title", READ_SRC, M01],
["B01_Dashboard_CardLogic", READ_SRC, f"{M01}?open=로직"],
["B01_Dashboard_CardStructOptionDef", WRITE_M, f"{M01}?open=옵션"],
["B01_Dashboard_CardPrecombo", WRITE_M, f"{M02}?open=일위대가조합&root=master&company=3"],
["B01_Dashboard_CardStructCombo", WRITE_M, f"{M02}?open=구조물조합&root=master&company=3"],
["B01_Dashboard_CardDrawing", WRITE_M, f"{M02}?open=도면&root=master&company=3"],
CALC_SITES,
]
# 회사가 없으면 해시에 안 실음
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False})[1][2] == (
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False})[4][2] == (
f"{M02}?open=구조물조합&root=master"
)
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
def test_메뉴_페이지_둘만_역할별():
# 716-2 — ☰ = M01 · M02 두 페이지(시스템 관리자) / M02 한 페이지(그 밖) · 열쇠 m01 · m02
admin = _pages({"role": "SYSTEM_ADMIN", "is_master": False, "company_id": 1}, 3)
assert admin == [
["m01", "B01_Dashboard_MasterElements", M01],
["m02", "M02_Title", f"{M02}?open=구조물조합&root=personal&company=3"],
]
assert _pages({"role": "ADMIN", "is_master": False, "company_id": None}) == [
["m02", "M02_Title", f"{M02}?open=구조물조합&root=company"],
]
assert _pages({"role": "USER", "is_master": False, "company_id": 1}) == [
["m02", "M02_Title", f"{M02}?open=구조물조합&root=personal"],
]
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
def test_회사_관리자_개인_카드_하나():
company = [
@@ -191,14 +239,12 @@ def test_회사_찾기_거르기():
def test_구조물_조합_기본_층_개인():
"""123-1 — 회사 있는 사람은 역할 무관 구조물 조합 = 개인 · 회사 없으면 지금 규칙 · 다른 곳 그대로 ·
134-4 — 층 없는 주소도 개인(회사 없으면 마스터) · 사전조합 기본 층도 같은 규칙."""
m = "B01_Dashboard_PermMasterWrite"
combo = f"{M02}?open=구조물조합&root=personal"
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False, "company_id": 1}, 3) == [
["B01_Dashboard_MasterElements", "B01_Dashboard_PermSourceRead", "m01-master-data"],
["B01_Dashboard_CardStructCombo", m, f"{combo}&company=3"],
["B01_Dashboard_CardStructOption", m, f"{M02}?open=옵션&root=master&company=3"],
["B01_Dashboard_CardDrawing", m, f"{M02}?open=도면&root=master&company=3"],
CALC_SITES,
rows = _cards({"role": "SYSTEM_ADMIN", "is_master": False, "company_id": 1}, 3)
assert rows[3:6] == [
["B01_Dashboard_CardPrecombo", WRITE_M, f"{M02}?open=일위대가조합&root=personal&company=3"],
["B01_Dashboard_CardStructCombo", WRITE_M, f"{combo}&company=3"],
["B01_Dashboard_CardDrawing", WRITE_M, f"{M02}?open=도면&root=master&company=3"],
]
assert _cards({"role": "ADMIN", "is_master": False, "company_id": 1}) == [
["B01_Dashboard_CardStructCombo", WRITE_CO, combo],
@@ -206,7 +252,7 @@ def test_구조물_조합_기본_층_개인():
assert _cards({"role": "USER", "is_master": True, "company_id": 1})[0][2] == combo
assert _cards({"role": "USER", "is_master": False, "company_id": 1})[0][2] == combo
# 개인 층 없음(회사 없음) = 지금 규칙
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False, "company_id": None})[1][2] == (
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False, "company_id": None})[4][2] == (
f"{M02}?open=구조물조합&root=master"
)
assert _cards({"role": "ADMIN", "is_master": False, "company_id": None})[0][2] == (
+2
View File
@@ -171,4 +171,6 @@ export const ui_locales_m1 = {
B01_CalcSites_Saved: ["계산 자리 저장함", "Calc sites saved"],
B01_CalcSites_Leave: ["저장 안 한 계산 자리가 있음", "You have unsaved calc sites"],
B01_CalcSites_LoadFailed: ["계산 자리를 못 읽음", "Failed to load calc sites"],
B01_Dashboard_CardLogic: ["단가산출 로직", "Cost-logic library"],
B01_Dashboard_CardStructOptionDef: ["구조물 옵션 정의", "Structure option definition"],
} as const;
+1
View File
@@ -188,4 +188,5 @@ export const ui_locales_m2 = {
"그 사이 실무문서가 바뀜 — 목록을 다시 받음",
"Practice documents changed — reloaded",
],
B01_Dashboard_CardPrecombo: ["일위대가", "Unit-cost items"],
} as const;