From c6df1839836478cefb9bf843e5c3c7e2138588d6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 1 Nov 2026 15:09:14 +0900 Subject: [PATCH] =?UTF-8?q?feat(718-3):=20M01=20=EB=B3=84=EC=B9=AD=20?= =?UTF-8?q?=EC=9E=A5=EB=B6=80=20=ED=99=94=EB=A9=B4=20=E2=80=94=20=EC=99=BC?= =?UTF-8?q?=EC=AA=BD=20=EC=B9=B8=20=C2=B7=20=EB=AA=A8=EC=9D=B8=20=ED=9B=84?= =?UTF-8?q?=EB=B3=B4=20=ED=91=9C(=EC=9D=B4=EB=A6=84=20=C2=B7=20=EC=A7=9D?= =?UTF-8?q?=20=C2=B7=20=ED=9A=8C=EC=82=AC=20=EC=88=98=20=C2=B7=20=EC=82=AC?= =?UTF-8?q?=EB=9E=8C=20=EC=88=98=20=C2=B7=20=EC=B6=A9=EB=8F=8C)=20=C2=B7?= =?UTF-8?q?=20=EC=8A=B9=EC=9D=B8=20=C2=B7=20=EB=B0=98=EB=A0=A4=20=C2=B7=20?= =?UTF-8?q?=EB=A7=88=EC=8A=A4=ED=84=B0=20=EC=9E=A5=EB=B6=80=20=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=20=C2=B7=20=EC=A7=80=EC=9B=80=20=C2=B7=20=EB=8C=80?= =?UTF-8?q?=EC=8B=9C=EB=B3=B4=EB=93=9C=20=EC=A4=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새 M01_MasterData_UI_Alias — 모인 후보 / 마스터 장부 탭 · 승인 · 반려 · 지움 = 확인 창 뒤 서버(718-2 길) · 마스터 장부 = M02 별칭 장부 부품 그대로(마스터 층) + 마스터 공사 내역 맞춤 - M01 왼쪽 판 = 구조물 옵션 정의 아래 「별칭 장부」 칸(본 칸 = 옵션 정의와 같은 자리) · 주소 ?open=별칭(M01 페이지 한 줄) - 대시보드 설계 기본값 마스터 데이터 묶음에 「별칭 장부」 줄(m_settings_nav 한 줄 · 화면 글 키 하나) - 시험 test_b01_settings_cards · test_197_1 = 줄 하나 늘어난 자리 · 별칭 칸 대조로 고침(낡은 줄 번호) Co-Authored-By: Claude Opus 5.5 --- A00_Common/m_settings_nav.ts | 1 + M01_MasterData/M01_MasterData_UI_Alias.ts | 237 ++++++++++++++++++ M01_MasterData/M01_MasterData_UI_Page.ts | 5 +- M01_MasterData/M01_MasterData_UI_Side.ts | 23 +- .../test_197_1_dashboard_option_card.py | 9 +- resources/tester/test_b01_settings_cards.py | 12 +- ui_template/ui_template_locale_m1.ts | 1 + 7 files changed, 274 insertions(+), 14 deletions(-) create mode 100644 M01_MasterData/M01_MasterData_UI_Alias.ts diff --git a/A00_Common/m_settings_nav.ts b/A00_Common/m_settings_nav.ts index 817dcafc0..fff8b3264 100644 --- a/A00_Common/m_settings_nav.ts +++ b/A00_Common/m_settings_nav.ts @@ -64,6 +64,7 @@ export function settingLinks(user: SettingUser, companyId: number | string | nul m01("m01", L("M01_Title"), read), m01("로직", L("B01_Dashboard_CardLogic"), read, "로직"), m01("옵션", L("B01_Dashboard_CardStructOptionDef"), desc, "옵션"), + m01("별칭", L("B01_Dashboard_CardAliasLedger"), desc, "별칭"), m02(L("B01_Dashboard_CardPrecombo"), "일위대가조합", comboRoot("master"), desc), m02(L("B01_Dashboard_CardStructCombo"), "구조물조합", comboRoot("master"), desc), m02(L("B01_Dashboard_CardDrawing"), "도면", "master", desc), diff --git a/M01_MasterData/M01_MasterData_UI_Alias.ts b/M01_MasterData/M01_MasterData_UI_Alias.ts new file mode 100644 index 000000000..3262b4bb2 --- /dev/null +++ b/M01_MasterData/M01_MasterData_UI_Alias.ts @@ -0,0 +1,237 @@ +/* ============================================================================= + * M01_MasterData_UI_Alias.ts + * M01 별칭 장부(PLAN 718-3 · 요구서 내역12 4장 23 · 4-1 확정 6) — 본 칸 · 시스템 관리자만. + * · 「모인 후보」 = 모든 회사 · 개인 층 별칭 장부(일위대가 맞춤) + 공사 내역 맞춤을 서버가 모음 + * (`/api/m01/alias-ledger` · 718-2) · 이름 · 짝 · 회사 수 · 사람 수(누군지 안 보임) · 충돌 · + * [승인] = 마스터 장부에 넣음(모든 사용자 제안에 쓰임) · [반려] = 다시 안 보임 · 둘 다 확인 창 + * · 「마스터 장부」 = M02 별칭 장부 부품(`buildHopyoLedger` · 마스터 층) 그대로 + 마스터 공사 내역 맞춤 줄 [지움] + * · 자동 반영 없음 — 승인한 것만 모두에게 · 승인 · 반려 · 지움은 누를 때 바로 서버(확인 창 뒤) + * ========================================================================== */ + +import { API_BASE_URL } from "@config/config_frontend"; +import { createBadge } from "@ui/ui_template_badge"; +import { askConfirm, createButton, el, showToast } from "@ui/ui_template_elements"; +import { loadingError, showLoading } from "@ui/ui_template_loading"; +import { createPanelHead } from "@ui/ui_template_panel_head"; +import { createTable, type Column } from "@ui/ui_template_table"; +import { createTabs } from "@ui/ui_template_tabs"; +import "@ui/ui_template_general_layout.css"; // 마스터 장부 부품(M02 Hopyo_Ledger) 표 꼴 — M01 엔 안 실려 있음 +import type { LedgerPair } from "../M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api"; +import { + buildHopyoLedger, + KIND_TEXT, + LEDGER_HINT, + pairText, +} from "../M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger"; + +export const ALIAS_TITLE = "별칭 장부"; +const KIND_NAME: Record = { 맞춤: "일위대가", 내역맞춤: "공사 내역" }; + +interface Origin { + 품명: string; + 규격: string; + 단위: string; +} +export interface AliasCandidate { + 종류: "맞춤" | "내역맞춤"; + 열쇠: string; + 서명: string; + 원래: Origin; + 갈래?: string; + 짝?: LedgerPair[]; + 확정?: string; + 이름?: string; + 회사: number; + 사람: number; + 때: string | null; + 충돌: boolean; + 마스터: "다름" | null; +} +interface MasterBill { + 열쇠: string; + 원래: Origin; + 확정: string; + 이름?: string; +} +export interface AliasBoard { + 후보: AliasCandidate[]; + 모은곳: { 회사: number; 사람: number }; + 반려: number; + 마스터내역?: MasterBill[]; +} + +async function call(path: string, body?: unknown): Promise { + const res = await fetch(`${API_BASE_URL}/m01/alias-ledger${path}`, { + method: body === undefined ? "GET" : "POST", + credentials: "include", + headers: { "Content-Type": "application/json" }, + ...(body === undefined ? {} : { body: JSON.stringify(body) }), + }); + const got = (await res.json()) as T & { detail?: unknown }; + if (!res.ok) throw new Error(String(got.detail ?? `HTTP ${res.status}`)); + return got; +} + +/** 짝 글 — 일위대가 = 갈래 · 우리 로직 · 공사 내역 = 우리 공종 이름 */ +export const candidateText = (c: AliasCandidate): string => + c.종류 === "내역맞춤" + ? `${c.이름 ?? ""} (${c.확정 ?? ""})` + : c.갈래 === "짝" + ? (c.짝 ?? []).map(pairText).join(" + ") + : (KIND_TEXT[c.갈래 ?? ""] ?? c.갈래 ?? ""); + +const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); +const pick = (c: AliasCandidate) => ({ 종류: c.종류, 열쇠: c.열쇠, 서명: c.서명 }); + +const CANDIDATE_COLS: Column[] = [ + { label: "종류", width: "84px" }, + { label: "엑셀 품명" }, + { label: "규격" }, + { label: "단위", width: "64px" }, + { label: "짝" }, + { label: "회사", width: "56px", align: "end" }, + { label: "사람", width: "56px", align: "end" }, + { label: "충돌", width: "96px" }, + { label: "", width: "136px" }, +]; +const BILL_COLS: Column[] = [ + { label: "엑셀 품명" }, + { label: "규격" }, + { label: "단위", width: "64px" }, + { label: "우리 공종" }, + { label: "", width: "72px" }, +]; + +export function buildAliasLedger(): HTMLElement { + const root = el("div", { className: "m01-alias", attrs: { "data-alias-ledger": "" } }); + const body = el("div", { className: "m01-alias__body" }); + let view = "candidates"; + + async function act(c: AliasCandidate, ok: boolean): Promise { + const name = `${c.원래.품명} ${c.원래.규격}`.trim(); + const yes = await askConfirm({ + title: ok ? "마스터 장부에 승인" : "후보 반려", + message: ok + ? `「${name}」 → ${candidateText(c)}\n마스터 장부에 넣으면 모든 사용자 제안에 쓰임(각자 장부가 먼저).` + : `「${name}」 → ${candidateText(c)}\n반려하면 같은 짝은 후보에 다시 안 보임.`, + okLabel: ok ? "승인" : "반려", + danger: !ok, + }); + if (!yes) return; + try { + const got = await call(ok ? "/approve" : "/reject", pick(c)); + showToast(ok ? "마스터 장부에 넣음" : "반려함", "success"); + paintCandidates(got); + } catch (error) { + showToast(`${ok ? "승인" : "반려"} 못 함 — ${why(error)}`, "error"); + } + } + + function candidateRow(c: AliasCandidate): (string | Node)[] { + const clash = c.마스터 === "다름" ? "마스터와 다름" : c.충돌 ? "다른 짝 있음" : ""; + return [ + KIND_NAME[c.종류] ?? c.종류, + c.원래.품명, + c.원래.규격, + c.원래.단위, + candidateText(c), + String(c.회사), + String(c.사람), + clash ? createBadge(clash, "warning") : "", + el("div", { + className: "ui-toolbar", + children: [ + createButton({ + label: "승인", + variant: "filled", + size: "sm", + onClick: () => void act(c, true), + }), + createButton({ + label: "반려", + variant: "ghost", + size: "sm", + onClick: () => void act(c, false), + }), + ], + }), + ]; + } + + function paintCandidates(board: AliasBoard): void { + const head = createPanelHead({ + title: "모인 후보", + sub: `${LEDGER_HINT} · 회사 ${board.모은곳.회사} · 사람 ${board.모은곳.사람}의 장부에서 모음 · 반려 ${board.반려} · 승인한 것만 모두에게 · 누가 적었는지는 안 보임`, + }); + body.replaceChildren( + head.root, + createTable(CANDIDATE_COLS, board.후보.map(candidateRow), { + density: "data", + empty: "모인 후보 없음", + }), + ); + } + + async function forget(b: MasterBill): Promise { + const yes = await askConfirm({ + title: "마스터 공사 내역 맞춤 지움", + message: `「${b.원래.품명} ${b.원래.규격}」 → ${b.이름 ?? b.확정}\n지우면 모든 사용자 제안에서 빠짐.`, + okLabel: "지움", + danger: true, + }); + if (!yes) return; + try { + const got = await call<{ 마스터내역: MasterBill[] }>("/master-bill/forget", { 열쇠: b.열쇠 }); + showToast("지움", "success"); + paintMaster(got.마스터내역); + } catch (error) { + showToast(`지우지 못 함 — ${why(error)}`, "error"); + } + } + + function paintMaster(bill: MasterBill[]): void { + const rows = bill.map((b) => [ + b.원래.품명, + b.원래.규격, + b.원래.단위, + `${b.이름 ?? ""} (${b.확정})`, + createButton({ label: "지움", variant: "danger", size: "sm", onClick: () => void forget(b) }), + ]); + body.replaceChildren( + buildHopyoLedger({ scope: { layer: "system", owner: null }, canWrite: true }), + createPanelHead({ + title: "마스터 공사 내역 맞춤", + sub: "B09 내역 줄 ↔ 우리 공종(각자 기억이 먼저)", + }).root, + createTable(BILL_COLS, rows, { density: "data", empty: "마스터 공사 내역 맞춤 없음" }), + ); + } + + async function load(): Promise { + const stop = showLoading(body); + try { + const got = await call(""); + if (view === "candidates") paintCandidates(got); + else paintMaster(got.마스터내역 ?? []); + } catch (error) { + body.replaceChildren(loadingError(why(error), () => void load())); + } finally { + stop(); + } + } + + const tabs = createTabs({ + tabs: [ + { key: "candidates", label: "모인 후보" }, + { key: "master", label: "마스터 장부" }, + ], + value: view, + onChange: (key) => { + view = key; + void load(); + }, + }); + root.append(tabs.root, body); // 제목 = 본 칸 머리(ALIAS_TITLE · 왼쪽 판이 넘김) + void load(); + return root; +} diff --git a/M01_MasterData/M01_MasterData_UI_Page.ts b/M01_MasterData/M01_MasterData_UI_Page.ts index f4a2ed43b..3b6966ef9 100644 --- a/M01_MasterData/M01_MasterData_UI_Page.ts +++ b/M01_MasterData/M01_MasterData_UI_Page.ts @@ -2,7 +2,8 @@ * M01_MasterData_UI_Page.ts * 마스터 요소 화면 — 좌측 도킹 패널(컨테이너: 인력·재료·기계·소요량·계수·환율·요율·단가산출 로직) * · 일위대가 조합은 M02 마스터 템플릿(PLAN 27-2b) · 구조물 옵션 정의(716-1 · 화면은 M02 `StructOptions/`) - * / 우측 요소 표 · 표 목록 · 로직 편집 · 옵션 정의 · 주소 `?open=로직|옵션` = 그 칸을 고른 채 열림(716-2) + * / 우측 요소 표 · 표 목록 · 로직 편집 · 옵션 정의 · 별칭 장부(718-3) · + * 주소 `?open=로직|옵션|별칭` = 그 칸을 고른 채 열림(716-2) * * 시스템 관리자만. 고친 것은 초안(sessionStorage)에 쌓이고 [저장] 한 번에 서버로 — * 자동저장 없음. @@ -333,6 +334,6 @@ function buildPage(): HTMLElement { if (hasPendingLogicKey()) openLogicTab(); // 주소 `?open=` — 대시보드 설계 기본값 줄 · 옛 M02 `open=옵션` 즐겨찾기가 그 칸을 고른 채 엶 const want = currentQuery().get("open"); - if (want === "로직" || want === "옵션") side.reveal(want); + if (want === "로직" || want === "옵션" || want === "별칭") side.reveal(want); return layout; } diff --git a/M01_MasterData/M01_MasterData_UI_Side.ts b/M01_MasterData/M01_MasterData_UI_Side.ts index 5dc8f06a7..a0ffb87c4 100644 --- a/M01_MasterData/M01_MasterData_UI_Side.ts +++ b/M01_MasterData/M01_MasterData_UI_Side.ts @@ -3,6 +3,7 @@ * 왼쪽 패널 — 공용 접기 컨테이너(`ui-collapsible ui-sidebar-section`) * 인력 · 재료 · 기계 · 소요량 · 계수 · 환율 · 요율 · 단가산출 로직(일위대가 조합은 M02 · PLAN 27-2b) * ─ 구분선 ─ 구조물 옵션 정의(716-1 · 화면 코드는 M02 `StructOptions/` 그대로 · 입구만 여기) + * · 별칭 장부(718-3 · 누르면 본 칸에 모인 후보 · 마스터 장부 · `M01_MasterData_UI_Alias`) * 펼치면 하위 목록(거름 · 파일) · 환율/요율은 누르면 바로 표 · 로직은 안에 로직 목록 * ========================================================================== */ @@ -14,6 +15,7 @@ import { buildOptionsSection, type OptionsSectionOptions, } from "../M02_MasterTemplete/StructOptions/M02_SO_Options"; +import { ALIAS_TITLE, buildAliasLedger } from "./M01_MasterData_UI_Alias"; 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"; @@ -110,7 +112,7 @@ export interface SideHandle { /** 「전체」 + 드롭다운 거름 한 벌 — 로직 컨테이너도 씀 */ filter: (args: FilterArgs) => HTMLElement; /** 주소 `open` — 그 컨테이너를 펼치고 고름(로직 = 로직 목록 · 옵션 = 정의 화면까지) */ - reveal: (target: "로직" | "옵션") => void; + reveal: (target: "로직" | "옵션" | "별칭") => void; /** 옵션 줄 고름 표시 지움(다른 화면을 열었음) · 켬(떠나기를 취소해 머묾) */ setOptionsMark: (on: boolean) => void; } @@ -166,6 +168,7 @@ const LEAF: Group[] = ["환율", "요율"]; /** 거름이 있는 컨테이너 — 펼치면 「전체」(또는 닫기 전 거름) 이 바로 열림 */ const FILTERED: Group[] = ["인력", "기계", "소요량", "계수"]; const LOGIC_ID = "로직|"; +const ALIAS_ID = "별칭|"; export function buildSide( onOpen: (pick: Pick) => void, @@ -500,9 +503,22 @@ ${first.file}` // 구조물 옵션 정의 — 로직 칸 아래 구분선 + 칸 하나(시스템 관리자 화면이라 고침 가능) const options = buildOptionsSection({ ...optionsOpt, isAdmin: true }); const optionsSec = section(options.title, options.body); + // 별칭 장부(718-3) — 옵션 칸 아래 칸 하나 · 누르면 본 칸(옵션 정의와 같은 자리) + const aliasSec = section(ALIAS_TITLE, null); + aliasSec.classList.add("m01-side__leaf"); + buttons.set(ALIAS_ID, aliasSec); + const openAlias = async (): Promise => { + if (!(await optionsOpt.confirmLeave())) return; + optionsOpt.show(ALIAS_TITLE, buildAliasLedger()); + options.clear(); + setActive(ALIAS_ID); + }; + aliasSec + .querySelector(".ui-collapsible__title") + ?.addEventListener("click", () => void openAlias()); const root = el("div", { className: "m01-side ui-sidebar-panel", - children: [...groups, logic, createDivider(), optionsSec], + children: [...groups, logic, createDivider(), optionsSec, aliasSec], }); attachCollapsible(root); void Promise.all(GROUPS.filter((g) => g !== FUEL_GROUP).map(refresh)).catch((error) => @@ -514,7 +530,8 @@ ${first.file}` // 재료는 유가 · 전력 파일까지 다(알약 → 마스터 화면 길이 파일로 찾음) · 유가 · 전력은 그 파일만 const files = (group: string): FileInfo[] => group === FUEL_GROUP ? shown(FUEL_GROUP) : (lists.get(group as Group) ?? []); - const reveal = (target: "로직" | "옵션"): void => { + const reveal = (target: "로직" | "옵션" | "별칭"): void => { + if (target === "별칭") return void openAlias(); if (target === "옵션") { optionsSec.classList.remove("is-collapsed"); options.body.querySelector("button")?.click(); diff --git a/resources/tester/test_197_1_dashboard_option_card.py b/resources/tester/test_197_1_dashboard_option_card.py index 011bbbe1c..12c9b6c4b 100644 --- a/resources/tester/test_197_1_dashboard_option_card.py +++ b/resources/tester/test_197_1_dashboard_option_card.py @@ -49,15 +49,16 @@ def test_줄_해시_옵션은_M01(): from test_b01_settings_cards import _cards cards = _cards({"role": "SYSTEM_ADMIN", "is_master": False}, 3) - heads = [c[2].split("?")[0] + "?" + c[2].split("?")[1].split("&")[0] for c in cards[1:6]] + heads = [c[2].split("?")[0] + "?" + c[2].split("?")[1].split("&")[0] for c in cards[1:7]] assert heads == [ "m01-master-data?open=로직", "m01-master-data?open=옵션", + "m01-master-data?open=별칭", # 718-3 "m02-master-template?open=일위대가조합", "m02-master-template?open=구조물조합", "m02-master-template?open=도면", ] - assert all("&root=master" in c[2] for c in cards[3:6]) + assert all("&root=master" in c[2] for c in cards[4:7]) @needs_node @@ -175,9 +176,9 @@ def test_옛_옵션_주소_M01로_돌려보냄(): 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 'if (want === "로직" || 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 + assert "logic, createDivider(), optionsSec, aliasSec" in side # 718-3 별칭 장부 칸 diff --git a/resources/tester/test_b01_settings_cards.py b/resources/tester/test_b01_settings_cards.py index a32175595..ae486748a 100644 --- a/resources/tester/test_b01_settings_cards.py +++ b/resources/tester/test_b01_settings_cards.py @@ -97,20 +97,22 @@ WRITE_CO = READ_CO.replace("CompanyRead", "CompanyWrite") @pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") -def test_시스템_관리자_줄_칸별_일곱_고른_회사_해시(): - # 716-2 — 대시보드 설계 기본값 = 칸별 줄: M01 3(마스터 요소 · 단가산출 로직 · 구조물 옵션 정의) + +def test_시스템_관리자_줄_칸별_여덟_고른_회사_해시(): + # 716-2 · 718-3 — 대시보드 설계 기본값 = 칸별 줄: M01 4(마스터 요소 · 단가산출 로직 · 구조물 옵션 정의 · + # 별칭 장부) + # M02 3(일위대가 · 구조물 조합 · 도면 양식) + 계산 자리 assert _cards({"role": "SYSTEM_ADMIN", "is_master": False}, 3) == [ ["M01_Title", READ_SRC, M01], ["B01_Dashboard_CardLogic", READ_SRC, f"{M01}?open=로직"], ["B01_Dashboard_CardStructOptionDef", WRITE_M, f"{M01}?open=옵션"], + ["B01_Dashboard_CardAliasLedger", 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})[4][2] == ( + assert _cards({"role": "SYSTEM_ADMIN", "is_master": False})[5][2] == ( f"{M02}?open=구조물조합&root=master" ) @@ -241,7 +243,7 @@ def test_구조물_조합_기본_층_개인(): 134-4 — 층 없는 주소도 개인(회사 없으면 마스터) · 사전조합 기본 층도 같은 규칙.""" combo = f"{M02}?open=구조물조합&root=personal" rows = _cards({"role": "SYSTEM_ADMIN", "is_master": False, "company_id": 1}, 3) - assert rows[3:6] == [ + assert rows[4:7] == [ ["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"], @@ -252,7 +254,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})[4][2] == ( + assert _cards({"role": "SYSTEM_ADMIN", "is_master": False, "company_id": None})[5][2] == ( f"{M02}?open=구조물조합&root=master" ) assert _cards({"role": "ADMIN", "is_master": False, "company_id": None})[0][2] == ( diff --git a/ui_template/ui_template_locale_m1.ts b/ui_template/ui_template_locale_m1.ts index 6c686efd7..7c34ba64d 100644 --- a/ui_template/ui_template_locale_m1.ts +++ b/ui_template/ui_template_locale_m1.ts @@ -173,4 +173,5 @@ export const ui_locales_m1 = { B01_CalcSites_LoadFailed: ["계산 자리를 못 읽음", "Failed to load calc sites"], B01_Dashboard_CardLogic: ["단가산출 로직", "Cost-logic library"], B01_Dashboard_CardStructOptionDef: ["구조물 옵션 정의", "Structure option definition"], + B01_Dashboard_CardAliasLedger: ["별칭 장부", "Alias ledger"], } as const;