diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts index ff2948e83..cd670718c 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts @@ -13,8 +13,8 @@ import { buildHopyoLedger, LEDGER_HINT, LEDGER_TITLE } from "./M02_MasterTemplet import { buildHopyoWizard } from "./M02_MasterTemplete_Hopyo_Wizard"; export interface HopyoSectionOptions { - /** 이 층 쓰기 권한 — 별칭 장부 고침 */ - canWrite: boolean; + /** 이 층 쓰기 권한 — 별칭 장부 고침(구조물 조합 층이 제자리에서 바뀌어 열 때 읽음 · 720-4) */ + canWrite: () => boolean; /** 저장 안 한 고침을 버릴지 확인 — 취소면 false */ confirmLeave: () => Promise; /** 메인 칸에 화면을 띄움 */ @@ -34,7 +34,6 @@ const WIZARD = "불러오기"; export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection { const title = L("M02_HopyoImport"); - const scope = pageScope(); const start = createButton({ label: "불러오기", variant: "filled", @@ -63,7 +62,9 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection { if (open?.key === key && open.view.isConnected) return; if (!(await opt.confirmLeave())) return; const view = - key === WIZARD ? buildHopyoWizard() : buildHopyoLedger({ scope, canWrite: opt.canWrite }); + key === WIZARD + ? buildHopyoWizard() + : buildHopyoLedger({ scope: pageScope(), canWrite: opt.canWrite() }); opt.show(key === WIZARD ? title : `${title} — ${LEDGER_TITLE}`, view); opt.onOpen(); open = { key, view }; diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts index 33105a4ce..d68699dba 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -130,9 +130,10 @@ export function newItemButton(run: () => void, label = L("M02_TreeNewItem")): HT return button; } -/** `onOpened` — 무엇을 열든(좌측 · 카드 [도면 수정] · 다시 읽기) 연 바로 뒤 부름(좌측 고름 표시) */ +/** `onOpened` — 무엇을 열든(좌측 · 카드 [도면 수정] · 다시 읽기) 연 바로 뒤 부름(좌측 고름 표시) · + * `isAdmin` = 지금 구조물 조합 층 고칠 권한(층을 제자리에서 바꾸므로 함수 · 720-4) */ export function createMain( - isAdmin: boolean, + isAdmin: () => boolean, onSaved?: () => void, onOpened?: () => void, extras: MainExtras = {}, @@ -185,7 +186,7 @@ export function createMain( // 「새 항목」 · 표 접기(제목)는 「구조물 조합」일 때만 const showAside = (): void => { const combo = isCombo(sel); - if (extras.newItem) extras.newItem.hidden = !combo; + if (extras.newItem) extras.newItem.hidden = !combo || !sel || !writable(sel); title.classList.toggle("m02-main__title--fold", combo); title.title = combo ? L("M02_TableFold") : ""; if (!combo) { @@ -251,7 +252,7 @@ export function createMain( host.replaceChildren(createEmpty({ text })); }; /** 고를 것마다 고칠 권한 — 사전조합 층은 고름에 실림(134-3) · 그 밖 페이지 층 */ - const writable = (at: Selection): boolean => at.canWrite ?? canEdit(at.layer, isAdmin); + const writable = (at: Selection): boolean => at.canWrite ?? canEdit(at.layer, isAdmin()); const bar = (): void => { reload.hidden = !sel; save.hidden = !sel || !writable(sel); @@ -282,7 +283,7 @@ export function createMain( /** 구조물집계표 아래 설계 컨테이너 — 고른 열의 도면 정보 · 면적산출 카드 */ function buildPanel(layer: Layer): StructurePanelHandle { const made = createStructurePanel({ - isAdmin, + isAdmin: isAdmin(), confirmLeave, keepColumn: (colId) => sheet?.selectColumn(colId), // 카드 고침을 안 버림 — 고른 열 되돌림 onNumbers: () => sheet?.recalc(), // 카드 [저장] 뒤 번호 · 일위대가 합계 줄을 새로(86-3) diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts index d56440cb8..24f9d606c 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts @@ -4,7 +4,8 @@ * 우측 편집 칸(표·도면 부품 · 「구조물 조합」이면 나무는 좌측 패널 · PLAN 29-2) * * 한 화면에 층 셋(마스터 · 회사 · 개인 · PLAN 27-1) — 고른 층 = 진입 해시 `?open=&root=&owner=&company=` - * (`M02_MasterTemplete_Layer.ts`) · 층 단추 · 「사람 고르기」를 누르면 해시를 바꿔 이 화면을 다시 세움. + * (`M02_MasterTemplete_Layer.ts`) · 구조물 조합 층 단추 · 사람 나무를 누르면 제자리에서 그 층으로 갈아 끼움 + * (720-4 · 해시는 replaceState · 왼쪽 칸 펼침 · 단가산출 · 일위대가 층 그대로). * 화면은 로그인만 확인 — 고치기는 층별 권한일 때만 켬(서버도 막음 · `accessFor`). * ========================================================================== */ @@ -20,7 +21,7 @@ import { el } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { createWorkflowOverlays } from "@ui/ui_template_overlay"; import { mountSettingsDrawer } from "../A00_Common/m_settings_nav"; -import { navigateTo, type MountedPage } from "../A00_Common/router"; +import type { MountedPage } from "../A00_Common/router"; import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch"; import { fetchOwners, setActiveLayer } from "./M02_MasterTemplete_Api_Fetch"; import { @@ -33,7 +34,7 @@ import { ownOwner, redirectOldOptions, viewCompany, - type OpenTarget, + type ManageLayer, type Root, } from "./M02_MasterTemplete_Layer"; import { @@ -67,100 +68,141 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise false, show: () => {}, dispose: () => {} }; const entry = entryFromHash(user); // 134-4 — 해시에 층 없음 = 개인(회사 없으면 마스터) const company = viewCompany(entry, user); // 볼 회사(시스템 관리자는 고른 회사) - let layer = layerOfRoot(entry.root); - // 회사 층 주인 = 볼 회사 · 자기 층 주인을 적어도 자기 층 · 회사 없는 사람의 회사 · 개인 = 마스터(단추 자리에 안내) - let owner = entry.owner ?? (entry.root === "company" ? companyOwner(company, user) : null); - if (owner === ownOwner(layer, user)) owner = null; - // 시스템 관리자가 남의 회사 「개인」 — 그 회사 관리자처럼 사람 나무(PLAN 29-6) · 사람을 안 골랐으면 첫 사람 · - // 사람이 없으면 마스터 - const people = entry.root === "personal" && seesOthers(company, user); - if (people && !owner) { - owner = (await fetchOwners("personal", company).catch(() => []))[0]?.owner ?? null; - entry.owner = owner; - if (!owner) [entry.root, layer] = ["master", "system"]; - } - if (accessFor(layer, owner, user).needCompany) { - [entry.root, layer, owner] = ["master", "system", null]; - } - setActiveLayer(layer, owner); - const isAdmin = accessFor(layer, owner, user).canWrite; // 이 층을 고칠 수 있나(하위 부품의 「관리자」) - // 자기 회사용을 읽기만 볼 때 — 안내 + [개인용 복사] - const readOnlyBar = layer === "company" && !owner && !isAdmin ? buildReadOnlyBar(user) : null; - // 고른 층의 나무 — 고르기 모달과 같은 폴더 나무 · 좌측 「구조물 조합」 줄 아래 + // 구조물 조합 층(720-4) — 층 단추 · 사람 나무 · 공유 승인 띠가 바꿈 · 페이지는 그대로 두고 제자리에서 갈아 끼움 + // (`switchStructure` · 해시는 replaceState) · 단가산출 · 일위대가 층 · 왼쪽 칸 펼침은 그대로 + let layer: ManageLayer = "system"; + let owner: string | null = null; + let people = false; + let isAdmin = false; // 이 층을 고칠 수 있나(하위 부품의 「관리자」) + let readOnlyBar: ReturnType | null = null; + let share: ReturnType; + let tree: ReturnType; + let turn = 0; + /** 고른 층 → 실제 층 — 회사 층 주인 = 볼 회사 · 자기 층 주인을 적어도 자기 층 · + * 시스템 관리자가 남의 회사 「개인」 = 사람 나무(29-6 · 안 골랐으면 첫 사람 · 사람이 없으면 마스터) · + * 회사 없는 사람의 회사 · 개인 = 마스터(단추 자리에 안내) */ + const resolve = async (want: Root, wantOwner: string | null) => { + let [at, lay] = [want, layerOfRoot(want)]; + let who = wantOwner ?? (want === "company" ? companyOwner(company, user) : null); + if (who === ownOwner(lay, user)) who = null; + const many = want === "personal" && seesOthers(company, user); + if (many && !who) { + who = (await fetchOwners("personal", company).catch(() => []))[0]?.owner ?? null; + if (!who) [at, lay] = ["master", "system"]; + } + if (accessFor(lay, who, user).needCompany) [at, lay, who] = ["master", "system", null]; + return { root: at, layer: lay, owner: who, people: many && at === "personal" }; + }; // 「〇〇로 복사…」(30-2 · 39-6) — 쓸 수 있는 층 전부(마스터 · 볼 회사 · 내 개인 · 지금 보는 층은 뺌) · // 대상 폴더는 고르는 창 - const here = owner === ownOwner(layer, user) ? null : owner; - const copyTo = (["master", "company", "personal"] as Root[]).flatMap((one) => { - const to = layerOfRoot(one); - const toOwner = one === "company" ? companyOwner(company, user) : null; - if (!accessFor(to, toOwner, user).canWrite || (to === layer && toOwner === here)) return []; - const key = one === "master" ? "Master" : one === "company" ? "Company" : "Personal"; - return [{ layer: to, owner: toOwner, label: L(`M02_TreeCopyTo${key}`) }]; - }); - // 회사에 올리기 요청 · 승인(706-1) — 요청 = 개인 층 나무 덧메뉴 · 승인 = 회사 층 위 띠 · 결과 띠 - const share = createShare({ - user, - company, - layer, - owner, - showView: (label, node) => - void main.confirmLeave().then((ok) => ok && main.showView(label, node)), - openPrecombo: (r) => - void main.open({ - layer: "personal", - owner: r.주인, - canWrite: false, - projectId: null, - kind: "precombo", - name: r.키, - label: r.이름, - }), - openItem: (r) => void go("personal", r.주인, "구조물조합"), - changed: () => void Promise.all([side.refresh(), tree.refresh()]), - }); - const tree = createTreePanel({ - layer, - owner, - copyTo, - more: share.itemMenu, - canEdit: canEdit(layer, isAdmin), - onPick: (item) => void main.openTable(layer, item.열), - settle: () => main.settle(), - // 나무가 바뀜(새 항목 · 복사 생성 · 붙여넣기 · 지우기 · 이름 · 옮기기) — 집계표 - onChanged: async (colId) => { - await main.openTable(layer, colId, true); - }, - }); - const go = async (next: Root, nextOwner: string | null, open: OpenTarget): Promise => { - // 같은 주소 안 이동은 머리 가드가 안 물음 — 저장 안 한 고침은 여기서 - if (!(await main.confirmLeave())) return; - navigateTo( - ROUTES.M02_MASTER_TEMPLATE, - entryQuery({ ...entry, open, root: next, owner: nextOwner }), + const copyTargets = () => { + const here = owner === ownOwner(layer, user) ? null : owner; + return (["master", "company", "personal"] as Root[]).flatMap((one) => { + const to = layerOfRoot(one); + const toOwner = one === "company" ? companyOwner(company, user) : null; + if (!accessFor(to, toOwner, user).canWrite || (to === layer && toOwner === here)) return []; + const key = one === "master" ? "Master" : one === "company" ? "Company" : "Personal"; + return [{ layer: to, owner: toOwner, label: L(`M02_TreeCopyTo${key}`) }]; + }); + }; + /** 지금 층의 띠 · 나무 · 층 줄을 새로 만들어 감싼 칸에 끼움(처음 · 층 바꿈 한 길) */ + const build = (): void => { + // 자기 회사용을 읽기만 볼 때 — 안내 + [개인용 복사] + readOnlyBar = layer === "company" && !owner && !isAdmin ? buildReadOnlyBar(user) : null; + // 회사에 올리기 요청 · 승인(706-1) — 요청 = 개인 층 나무 덧메뉴 · 승인 = 회사 층 위 띠 · 결과 띠 + share = createShare({ + user, + company, + layer, + owner, + showView: (label, node) => + void main.confirmLeave().then((ok) => ok && main.showView(label, node)), + openPrecombo: (r) => + void main.open({ + layer: "personal", + owner: r.주인, + canWrite: false, + projectId: null, + kind: "precombo", + name: r.키, + label: r.이름, + }), + openItem: (r) => void go("personal", r.주인), + changed: () => void Promise.all([side.refresh(), tree.refresh()]), + }); + // 고른 층의 나무 — 고르기 모달과 같은 폴더 나무 · 좌측 「구조물 조합」 줄 아래 + tree = createTreePanel({ + layer, + owner, + copyTo: copyTargets(), + more: share.itemMenu, + canEdit: canEdit(layer, isAdmin), + onPick: (item) => void main.openTable(layer, item.열), + settle: () => main.settle(), + // 나무가 바뀜(새 항목 · 복사 생성 · 붙여넣기 · 지우기 · 이름 · 옮기기) — 집계표 + onChanged: async (colId) => { + await main.openTable(layer, colId, true); + }, + }); + topHost.replaceChildren(...(readOnlyBar ? [readOnlyBar.root] : []), share.top); + treeHost.replaceChildren( + layerBar(tree.add, () => void main.openTable(layer)), + peopleTree(tree.root), ); + void tree.refresh(); + }; + /** 층을 정해 페이지 층 길 · 권한을 맞춤 */ + const settleLayer = (got: Awaited>): void => { + [entry.root, entry.owner, layer, owner, people] = [ + got.root, + got.owner, + got.layer, + got.owner, + got.people, + ]; + setActiveLayer(layer, owner); + isAdmin = accessFor(layer, owner, user).canWrite; + }; + /** 구조물 조합 층 바꿈(720-4) — 제자리 · 주소 replaceState · 새 층 구조물 표 */ + const switchStructure = async (want: Root, wantOwner: string | null): Promise => { + const mine = ++turn; + const got = await resolve(want, wantOwner); + if (mine !== turn) return; + settleLayer(got); + const query = new URLSearchParams(entryQuery({ ...entry, open: "구조물조합" })); + history.replaceState(null, "", `#/${ROUTES.M02_MASTER_TEMPLATE}?${query}`); + build(); + await main.openTable(layer, null, true); + }; + const go = async (next: Root, nextOwner: string | null): Promise => { + // 저장 안 한 고침은 먼저 물음(취소 = 층 그대로) + if (!(await main.confirmLeave())) return; + await switchStructure(next, nextOwner); }; // 층 단추 셋(PLAN 29-2) — 「구조물 조합」 줄 아래 한 벌 // 단추 옆 「+」 = 그 나무 맨 위 더하기(뿌리 줄을 갈음 · 29-6) // 켜진 단추를 다시 누르면 그 층 목록 화면(30-4 · 집계표) - const layerBar = (open: OpenTarget, add: HTMLElement | null, reselect: () => void): HTMLElement => + const layerBar = (add: HTMLElement, reselect: () => void): HTMLElement => createLayerBar({ user, active: { root: entry.root, owner }, company, - go: (next, nextOwner) => void go(next, nextOwner, open), - ...(add ? { add } : {}), + go: (next, nextOwner) => void go(next, nextOwner), + add, reselect, }); // 남의 회사 「개인」 — 고른 사람 줄 아래에 그 사람 나무(사람 줄 → 라이브러리 · 29-6) - const peopleTree = (open: OpenTarget, panel: HTMLElement): HTMLElement => + const peopleTree = (panel: HTMLElement): HTMLElement => people && owner && company ? createPeopleTree({ company, active: owner, panel, - go: (next) => void go("personal", next, open), + go: (next) => void go("personal", next), }) : panel; + const topHost = el("div"); + const treeHost = el("div"); // 704-4 [구조물 조합으로] 만든 항목 → 나무 · 옆 목록 · 열린 표(행렬)를 다시(페이지가 내려가면 듣기 끝) const afterItem = async (): Promise => { await Promise.all([side.refresh(), tree.refresh()]); @@ -169,29 +211,27 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise - tree.root.isConnected ? void afterItem() : document.removeEventListener(ITEM_BUILT, onItem); + main.root.isConnected ? void afterItem() : document.removeEventListener(ITEM_BUILT, onItem); document.addEventListener(ITEM_BUILT, onItem); const main = createMain( - isAdmin, + () => isAdmin, () => void Promise.all([side.refresh(), tree.refresh()]), () => side.paint(), { - top: el("div", { children: [...(readOnlyBar ? [readOnlyBar.root] : []), share.top] }), - // 「새 항목」(30-5) — 제목 줄 오른쪽 끝 · 쓸 수 있는 층만 · 상세 칸에 [새로 만들기] [복사 생성](39-5) - ...(canEdit(layer, isAdmin) ? { newItem: newItemButton(() => void openNew()) } : {}), + top: topHost, + // 「새 항목」(30-5) — 제목 줄 오른쪽 끝 · 쓸 수 있는 층만(편집 칸이 가림) · [새로 만들기] [복사 생성](39-5) + newItem: newItemButton(() => void openNew()), onColumn: (column) => { readOnlyBar?.setColumn(column); tree.select(column); }, - // 표 머리 이름 칸 고침 = 나무 이름 바꾸기(31-1) · 쓸 수 있는 층만 - ...(canEdit(layer, isAdmin) - ? { - onHeadRename: (col: StructureColumn, level: number, name: string) => - void tree.editFromTable(headRenameEdit(col, level, name), col.id), - } - : {}), + // 표 머리 이름 칸 고침 = 나무 이름 바꾸기(31-1) · 쓸 수 있는 층만(나무가 가림) + onHeadRename: (col: StructureColumn, level: number, name: string) => + void tree.editFromTable(headRenameEdit(col, level, name), col.id), }, ); + settleLayer(await resolve(entry.root, entry.owner ?? null)); + build(); /** 새 구조물 조합(39-5 · 답 2) — 새로 만들기 = 나무 「항목 추가」 창 · 복사 생성 = 탭 + 목록 → * 지금 보는 층 · 나무에서 고른 폴더(없으면 맨 위) · 원래 이름(겹치면 「(2)」) · 새 키 · 새 번호 */ const openNew = async (): Promise => { @@ -214,12 +254,7 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise ({ layer, projectId: null, kind: "table", name: STRUCTURE_TABLE }); // 일위대가 사전조합 층(134-3) — 페이지 층(해시 root)과 따로 · 마스터 · 볼 회사 · 내 개인(사람 나무 없음) · // 못 가는 층(회사 없음) = 마스터 · 고른 층 = 주소 `pre`(페이지를 다시 세우지 않음 · replaceState) const preScope = (want: Root): PrecomboScope => { @@ -273,16 +308,13 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise void main.confirmLeave().then((ok) => ok && void main.open(sangeunList(sk))), }); const side = buildSide({ - layer, - isAdmin, - onOpen: (sel) => void main.open(sel ?? home), + isSystem: user?.role === "SYSTEM_ADMIN", + canWrite: () => canEdit(layer, isAdmin), + onOpen: (sel) => void main.open(sel ?? home()), getOpen: main.current, confirmLeave: main.confirmLeave, showView: main.showView, - tree: [ - layerBar("구조물조합", tree.add, () => void main.openTable(layer)), - peopleTree("구조물조합", tree.root), - ], + tree: [treeHost], precombo: { scope: () => pre, bar: preBar, @@ -307,12 +339,11 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise false, show: () => {}, dispose: () => setActiveLayer("system") }; } diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts index 5a86c8d5a..53f3955cc 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts @@ -4,6 +4,7 @@ * 50-3 · 134-3) › 「구조물 조합」(층 단추 · 나무) › 수평 구분선 › 「일위대가 불러오기」(구조물 조합 층 · * `Hopyo_Side` · 324-4) › 「도면 양식」(새로 · 본떠 · 지우기) › 실무문서. * 구조물 옵션 정의는 M01 로 옮김(716-1). + * 도면 양식 · 실무문서 = 시스템 관리자면 늘(마스터 층 것 · 구조물 조합 층과 무관 · 720-4). * 서버 길은 계약 `6_계약.md`. * 구조물 도면 · 일위대가 · 면적산출 상세는 좌측 줄이 아니라 항목 카드 [수정]으로(PLAN 45-2). * 화면 이름 「구조물 조합」(PLAN 26-7a) — 파일 이름 · 양식 키 · API 길은 「구조물집계표」 그대로. @@ -34,7 +35,7 @@ import { STRUCTURE_TABLE } from "./M02_MasterTemplete_Structure"; import { buildPracticeSection } from "./M02_MasterTemplete_Practice"; import { HOPYO_BUILT } from "./M02_MasterTemplete_Hopyo_Check"; import { buildHopyoSection } from "./M02_MasterTemplete_Hopyo_Side"; -import { canEdit, type Selection } from "./M02_MasterTemplete_UI_Main"; +import type { Selection } from "./M02_MasterTemplete_UI_Main"; import { buildPrecomboSection, type PrecomboOptions } from "./M02_MasterTemplete_UI_Side_Precombo"; import { buildSangeunSection, type SangeunOptions } from "./M02_MasterTemplete_UI_Side_Sangeun"; import { createEmpty } from "@ui/ui_template_empty"; @@ -54,10 +55,10 @@ const STUB_DRAWING = { }; export interface SideOptions { - /** 이 화면의 층 — 도면 양식 목록은 시스템 층에만 있음 */ - layer: Layer; - /** 이 층을 고칠 수 있나 */ - isAdmin: boolean; + /** 시스템 관리자 — 도면 양식(시스템 층에만 있음) · 실무문서 칸을 늘 보임(720-4) */ + isSystem: boolean; + /** 지금 구조물 조합 층을 고칠 수 있나(층이 제자리에서 바뀜) — 일위대가 불러오기 장부 */ + canWrite: () => boolean; onOpen: (sel: Selection | null) => void; getOpen: () => Selection | null; /** 저장 안 한 고침을 버릴지 확인 — 취소면 false */ @@ -97,7 +98,7 @@ const failed = (error: unknown): void => showToast(L("M02_ActionFailed").replace("{value}", why(error)), "error"); export function buildSide(opt: SideOptions): SideHandle { - const layer = opt.layer; + const layer: Layer = "system"; // 도면 양식 목록 · 고름의 층 let items: TemplateInfo[] = []; let seq = 0; @@ -112,7 +113,7 @@ export function buildSide(opt: SideOptions): SideHandle { /** 새로 · 본떠 만들기 · 지우기 — 도면 양식 컨테이너 안(구조물 집계 양식은 집계표 하나라 없음) · 관리자만 */ const editButtons = new Map(); const actionRow = (kind: Kind): HTMLElement[] => { - if (!canEdit(layer, opt.isAdmin)) return []; + if (!opt.isSystem) return []; const btnNew = createButton({ label: L("M02_New"), variant: "filled", size: "sm" }); const copy = createButton({ label: L("M02_Copy"), variant: "ghost", size: "sm" }); const del = createButton({ label: L("M02_Delete"), variant: "danger", size: "sm" }); @@ -160,23 +161,21 @@ export function buildSide(opt: SideOptions): SideHandle { ? void Promise.all([precombo.refresh(), sangeun.refresh()]) : document.removeEventListener(HOPYO_BUILT, onBuilt); document.addEventListener(HOPYO_BUILT, onBuilt); - // 도면 양식은 시스템 층(마스터 템플릿)에만 - const drawing = - layer === "system" - ? section(L("M02_KindDrawing"), bodies.get("drawing")!, ...actionRow("drawing")) - : null; - // 실무문서(옛 마스터 일위대가 조합 · PLAN 39-2) — 구조물 조합 밖 · 도면 양식 아래 · 마스터 층 · 시스템 관리자만 - const practice = - layer === "system" && opt.isAdmin - ? buildPracticeSection({ - confirmLeave: opt.confirmLeave, - show: opt.showView, - onOpen: paint, - }) - : null; + // 도면 양식은 시스템 층(마스터 템플릿)에만 — 시스템 관리자면 늘(720-4) + const drawing = opt.isSystem + ? section(L("M02_KindDrawing"), bodies.get("drawing")!, ...actionRow("drawing")) + : null; + // 실무문서(옛 마스터 일위대가 조합 · PLAN 39-2) — 구조물 조합 밖 · 도면 양식 아래 · 시스템 관리자만 + const practice = opt.isSystem + ? buildPracticeSection({ + confirmLeave: opt.confirmLeave, + show: opt.showView, + onOpen: paint, + }) + : null; // 일위대가 불러오기(324-4 · 제목은 718-1) · 층 = 구조물 조합 층 · 쓰기 권한 없으면 올리기 꺼짐 const hopyo = buildHopyoSection({ - canWrite: canEdit(layer, opt.isAdmin), + canWrite: opt.canWrite, confirmLeave: opt.confirmLeave, show: opt.showView, onOpen: () => (practice?.clear(), paint()), @@ -197,10 +196,10 @@ export function buildSide(opt: SideOptions): SideHandle { const root = el("div", { className: "m02-side ui-sidebar-panel", children: [listHost] }); - /** 고름 표시 — 편집 칸에 지금 열린 것 하나뿐 */ + /** 고름 표시 — 편집 칸에 지금 열린 것 하나뿐(도면 양식 = 시스템 층뿐 · 구조물 조합 표 = 어느 층이든) */ const isPicked = (kind: Kind, name: string): boolean => { const cur = opt.getOpen(); - return !!cur && cur.layer === layer && cur.kind === kind && cur.name === name; + return !!cur && cur.kind === kind && cur.name === name; }; const isOpen = (info: TemplateInfo): boolean => isPicked(info.종류, info.이름); @@ -260,7 +259,7 @@ export function buildSide(opt: SideOptions): SideHandle { const mine = ++seq; paintButtons(); try { - items = await listTemplates(layer, null); + items = opt.isSystem ? await listTemplates(layer, null) : []; // 도면 양식 목록 — 시스템 관리자만 } catch (error) { items = []; showToast(L("M02_LoadFailed").replace("{value}", why(error)), "error"); diff --git a/resources/tester/test_720_m02_folder_layer.py b/resources/tester/test_720_m02_folder_layer.py index a3b9fddbc..d22012be0 100644 --- a/resources/tester/test_720_m02_folder_layer.py +++ b/resources/tester/test_720_m02_folder_layer.py @@ -62,7 +62,7 @@ def test_화면_층_단추_최상위_폴더_이름_바꾸기(): assert "setPre(next), void main.open(precomboList(pre))" in page assert "void main.open(sangeunList(sk))" in page.split("const skBar")[1].split("reselect:")[0] # 720-2 — 층 줄 「+」 = 서고 칸 맨 위 메뉴 · 메뉴 줄 = 같은 addFolder([]) - assert page.count(" add,\n") == 2 + assert page.count(" add,\n") == 3 # 구조물 조합 · 일위대가 · 단가산출 assert "onClick: (event) => openTreeMenu(event, menuAt([]))" in lib assert "add.hidden = !at.canWrite;" in lib and "opt.bar(add)" in lib assert lib.count('L("M02_TreeAddTopFolder"), () => void addFolder([])') == 1 # 폴더 줄 @@ -74,3 +74,23 @@ def test_화면_층_단추_최상위_폴더_이름_바꾸기(): assert "이름: name }" in lib and "saveTemplate(at.layer, kind, row.이름, null" in lib main = _text("M02_MasterTemplete_UI_Main.ts") assert "title.textContent = at.label = named ? `${at.name} ${named}` : at.name;" in main + + +def test_구조물_조합_층_제자리_바꿈(): + """720-4 — 해시 이동 없음 · replaceState · 나무 · 층 줄 · 띠를 감싼 칸에 갈아 끼움 · 권한 = 함수 · + 도면 양식 · 실무문서 = 시스템 관리자면 늘 · 불러오기 장부 = 열 때 층 · 권한.""" + page = _text("M02_MasterTemplete_UI_Page.ts") + assert "navigateTo" not in page + switch = page.split("const switchStructure = async")[1].split("const go =")[0] + assert "history.replaceState(" in switch and "build();" in switch + assert "await main.openTable(layer, null, true);" in switch + assert "if (mine !== turn) return;" in switch # 늦게 온 옛 고름 버림 + build = page.split("const build = (): void => {")[1].split("const settleLayer")[0] + assert "topHost.replaceChildren(" in build and "treeHost.replaceChildren(" in build + assert "tree: [treeHost]," in page and "top: topHost," in page + assert "() => isAdmin," in page and 'isSystem: user?.role === "SYSTEM_ADMIN",' in page + assert "main.root.isConnected ? void afterItem()" in page # 나무를 갈아도 듣기 그대로 + side = _text("M02_MasterTemplete_UI_Side.ts") + assert "const drawing = opt.isSystem" in side and "const practice = opt.isSystem" in side + hopyo = _text("M02_MasterTemplete_Hopyo_Side.ts") + assert "buildHopyoLedger({ scope: pageScope(), canWrite: opt.canWrite() })" in hopyo diff --git a/resources/tester/test_m02_combo_screen.py b/resources/tester/test_m02_combo_screen.py index dcf3d396d..4d10e8016 100644 --- a/resources/tester/test_m02_combo_screen.py +++ b/resources/tester/test_m02_combo_screen.py @@ -106,7 +106,7 @@ def test_나무는_좌측_패널_찾기_칸_없음(): assert 'info.이름 === STRUCTURE_TABLE ? L("M02_KindTable")' in side # 좌측 줄 이름 page = _text(M02 / "M02_MasterTemplete_UI_Page.ts") assert "createTreePanel({" in page and "tree.select(column)" in page - assert 'layerBar("구조물조합", tree.add, () => void main.openTable(layer)),' in page + assert "layerBar(tree.add, () => void main.openTable(layer))," in page main = _text(M02 / "M02_MasterTemplete_UI_Main.ts") assert 'at.kind === "table" && at.name === STRUCTURE_TABLE && at.layer !== "project"' in main assert 'isCombo(next) ? L("M02_KindTable")' in main # 상세 제목 diff --git a/resources/tester/test_m02_combo_table.py b/resources/tester/test_m02_combo_table.py index 11802c88e..c991c446f 100644 --- a/resources/tester/test_m02_combo_table.py +++ b/resources/tester/test_m02_combo_table.py @@ -87,7 +87,9 @@ def test_머리_고침은_쓸_수_있는_층만_나무로(): assert "readOnly || !extras.onHeadRename" in main page = _text(M02 / "M02_MasterTemplete_UI_Page.ts") assert "tree.editFromTable(headRenameEdit(col, level, name), col.id)" in page - assert page.index("...(canEdit(layer, isAdmin)\n ? {\n onHeadRename") > 0 + assert ( + "onHeadRename: (col: StructureColumn" in page + ) # 720-4 — 가림은 편집 칸 readOnly · 나무 canEdit tree = _text(M02 / "M02_MasterTemplete_TreePanel.ts") assert "if (opt.canEdit && (await ready())) await edit(body, colId);" in tree diff --git a/resources/tester/test_m02_hopyo_screen.py b/resources/tester/test_m02_hopyo_screen.py index dfd6fc510..ae2f60164 100644 --- a/resources/tester/test_m02_hopyo_screen.py +++ b/resources/tester/test_m02_hopyo_screen.py @@ -342,7 +342,7 @@ def test_좌측_판_자리와_권한(): order = ("precomboSec,", "summary,", "createDivider(),", "section(hopyo.title, hopyo.body),") assert [host.index(k) for k in order] == sorted(host.index(k) for k in order) assert host.index("section(hopyo.title, hopyo.body),") < host.index("drawing ?") - assert "canWrite: canEdit(layer, opt.isAdmin)," in side # 구조물 조합 층 쓰기 권한 + assert "canWrite: opt.canWrite," in side # 구조물 조합 층 쓰기 권한(720-4 함수) assert "hopyo.paint();" in side.split("function paint(): void {")[1] hopyo_side = _text("M02_MasterTemplete_Hopyo_Side.ts") # 718-1 — 제목 = 화면 글 키 · [불러오기] = 구조물 조합 층 단추 꼴 → 본 칸 마법사 · 옛 파일 목록 없음 @@ -351,7 +351,9 @@ def test_좌측_판_자리와_권한(): assert 'label: "불러오기"' in start and 'start.classList.add("m02-layers__btn");' in start assert "buildHopyoWizard()" in hopyo_side and "listHopyo" not in hopyo_side assert "uploadHopyo" not in hopyo_side and "openHopyoSheet" not in hopyo_side - assert "buildHopyoLedger({ scope, canWrite: opt.canWrite })" in hopyo_side # [별칭 장부] + assert ( + "buildHopyoLedger({ scope: pageScope(), canWrite: opt.canWrite() })" in hopyo_side + ) # [별칭 장부] wizard = _text("M02_MasterTemplete_Hopyo_Wizard.ts") assert 'accept: ".xls,.xlsx"' in wizard and "input.hidden = true;" in wizard assert "accessFor(layer, null, user).canWrite" in wizard # 결과 층 = 쓸 수 있는 층만 diff --git a/resources/tester/test_m02_layer_screen.py b/resources/tester/test_m02_layer_screen.py index 8e58b5371..24ab18297 100644 --- a/resources/tester/test_m02_layer_screen.py +++ b/resources/tester/test_m02_layer_screen.py @@ -181,7 +181,10 @@ def test_층_단추_셋_남의_회사_사람_나무(): page = _text(M02 / "M02_MasterTemplete_UI_Page.ts") assert "createLayerBar({" in page and "setActiveLayer(layer, owner)" in page assert "const company = viewCompany(entry, user);" in page - assert "await main.confirmLeave()" in page.split("const go =")[1].split("navigateTo(")[0] + # 720-4 — 다른 층 = 저장 안 한 고침 물음 뒤 제자리 바꿈(해시 이동 · 페이지 다시 세움 없음) + go = page.split("const go =")[1].split("};")[0] + assert "await main.confirmLeave()" in go and "await switchStructure(next, nextOwner);" in go + assert "navigateTo" not in page assert 'dispose: () => setActiveLayer("system")' in page assert "side.reveal(entry.open)" in page roots = _text(M02 / "M02_MasterTemplete_TreePanel_Roots.ts") @@ -192,9 +195,9 @@ def test_층_단추_셋_남의_회사_사람_나무(): assert "manages(company, user);" in roots.split("export const seesOthers")[1] # 맡은 회사(39-6) assert "const readOnly = !people && !accessFor(" in roots # 사람 나무 = 고침 assert 'L("M02_PeopleEmpty")' in roots - assert 'const people = entry.root === "personal" && seesOthers(company, user);' in page + assert 'const many = want === "personal" && seesOthers(company, user);' in page assert '(await fetchOwners("personal", company).catch(() => []))[0]?.owner' in page - assert 'peopleTree("구조물조합", tree.root)' in page + assert "peopleTree(tree.root)" in page assert 'peopleTree("일위대가"' not in page # 일위대가 사람 나무 벌은 39-2 에서 지움 panel = _text(M02 / "M02_MasterTemplete_TreePanel.ts") assert ( @@ -223,8 +226,8 @@ def test_층_길_접두어와_고정_풀림(): in labor ) side = _text(M02 / "M02_MasterTemplete_UI_Side.ts") - assert "const layer = opt.layer;" in side - assert 'layer === "system"' in side # 도면 양식은 시스템 층에만 + assert 'const layer: Layer = "system";' in side # 도면 양식은 시스템 층에만 + assert "const drawing = opt.isSystem" in side # 720-4 — 시스템 관리자면 늘 main = _text(M02 / "M02_MasterTemplete_UI_Main.ts") assert 'sel.layer !== "system"' not in main assert 'return layer === "project" || isAdmin;' in main @@ -236,7 +239,7 @@ def test_읽기만_회사용_개인용으로_복사_막대(): assert ( "readOnlyBar?.setColumn(column)" in page ) # 열 고름 → 막대 · 구조물 라이브러리가 함께 받음 - assert "accessFor(layer, owner, user).needCompany" in page # 회사 없음 = 마스터로 + assert "if (accessFor(lay, who, user).needCompany)" in page # 회사 없음 = 마스터로 assert "buildNeedCompany()" in _text(M02 / "M02_MasterTemplete_TreePanel_Roots.ts") assert 'title: L("M02_Title")' in page # 한 화면 한 제목 layer = _text(LAYER) @@ -290,7 +293,7 @@ def test_켜진_층_단추는_목록으로_복사_끌기(): roots = _text(M02 / "M02_MasterTemplete_TreePanel_Roots.ts") assert "if (!current) opt.go(root, owner);\n else opt.reselect?.();" in roots page = _text(M02 / "M02_MasterTemplete_UI_Page.ts") - assert 'layerBar("구조물조합", tree.add, () => void main.openTable(layer))' in page + assert "layerBar(tree.add, () => void main.openTable(layer))" in page # 39-6 — 쓸 수 있는 층 전부(볼 회사 주인까지 · 지금 층 뺌) · 대상 폴더 고르는 창 assert 'const toOwner = one === "company" ? companyOwner(company, user) : null;' in page assert "(to === layer && toOwner === here)) return [];" in page @@ -316,12 +319,10 @@ def test_구조물_조합_제목_줄_새_항목(): r"\s*\.\.\.\(extras\.newItem \? \[extras\.newItem\] : \[\]\),?\s*\]", main, ) - assert "if (extras.newItem) extras.newItem.hidden = !combo;" in main + # 720-4 — 쓸 수 있는 층만 = 편집 칸이 고른 것 권한으로 가림(층이 제자리에서 바뀜) + assert "extras.newItem.hidden = !combo || !sel || !writable(sel);" in main page = _text(M02 / "M02_MasterTemplete_UI_Page.ts") - assert ( - "...(canEdit(layer, isAdmin) ? { newItem: newItemButton(() => void openNew()) } : {})" - in page - ) + assert "newItem: newItemButton(() => void openNew())," in page panel = _text(M02 / "M02_MasterTemplete_TreePanel.ts") assert 'return at?.kind === "folder" ? at.path : [];' in panel # 고른 폴더(39-5 folder()) assert "const newItem = (): void => void addItem(folder());" in panel diff --git a/resources/tester/test_m02_practice.py b/resources/tester/test_m02_practice.py index 4466a0ac5..68a5284e9 100644 --- a/resources/tester/test_m02_practice.py +++ b/resources/tester/test_m02_practice.py @@ -51,9 +51,9 @@ def test_통째_지우기_시스템_관리자만_판_쓰는_곳(world, master_co def test_화면_시스템_관리자_마스터만_물음_두_번_읽기만(): side = (M02 / "M02_MasterTemplete_UI_Side.ts").read_text("utf-8") - assert 'layer === "system" && opt.isAdmin' in side + assert "const practice = opt.isSystem" in side # 720-4 — 시스템 관리자면 늘 order = side.split('className: "m02-side__lists ui-sidebar-panel",')[1].split("});")[0] - assert order.index("extras.도면") < order.index("practice") # 도면 양식 아래 + assert order.index("drawing ?") < order.index("practice ?") # 도면 양식 아래 view = (M02 / "M02_MasterTemplete_Practice.ts").read_text("utf-8") assert view.count("showConfirmDialog(") == 2 assert "dropPractice(" in view and "createInputField" not in view # 고칠 칸 없음 diff --git a/resources/tester/test_m02_precombo_scope.py b/resources/tester/test_m02_precombo_scope.py index 660859a49..9aeeab33c 100644 --- a/resources/tester/test_m02_precombo_scope.py +++ b/resources/tester/test_m02_precombo_scope.py @@ -72,15 +72,17 @@ def test_사전조합_층_단추는_페이지를_다시_안_세움(): assert "history.replaceState(" in pre and "side.refreshPrecombo()" in pre assert "navigateTo" not in pre and "setActiveLayer" not in pre # 페이지 층 · 해시 이동 없음 assert "entry.pre = pre.root;" in pre # 구조물 조합 층 이동(go) 때도 주소에 남음 - bar = page.split("const preBar = (add: HTMLElement): HTMLElement =>")[1].split("const side = buildSide(")[0] + bar = page.split("const preBar = (add: HTMLElement): HTMLElement =>")[1].split( + "const side = buildSide(" + )[0] # 720-1 — 다른 층 = 저장 안 한 고침 물음 → 그 층 나무 + 목록표 go = bar.split("go: (next) =>")[1].split("reselect:")[0] assert ".confirmLeave()" in go and "setPre(next), void main.open(precomboList(pre))" in go sk = page.split("const skBar = (")[1].split("const side = buildSide(")[0] assert ".confirmLeave()" in sk and "void main.open(sangeunList(sk))" in sk.split("reselect:")[0] - # 구조물 조합 층 단추는 그대로 해시 이동 한 벌 - assert page.count('layerBar("') == 1 - assert 'layerBar("구조물조합", tree.add, () => void main.openTable(layer)),' in page + # 구조물 조합 층 단추 한 벌(720-4 제자리 바꿈) + assert page.count("layerBar(") == 1 + assert "layerBar(tree.add, () => void main.openTable(layer))," in page assert "precombo: {\n scope: () => pre,\n bar: preBar," in page # 706-1 more 덧붙음 @@ -92,7 +94,7 @@ def test_고름에_owner_권한_서고_길에_명시_owner(): assert "listTemplates(at.layer, null, at.owner)" in lib_side main = _text("M02_MasterTemplete_UI_Main.ts") assert ( - "const writable = (at: Selection): boolean => at.canWrite ?? canEdit(at.layer, isAdmin);" + "const writable = (at: Selection): boolean => at.canWrite ?? canEdit(at.layer, isAdmin());" in main ) assert "readTemplate(next.layer, next.kind, next.name, next.projectId, next.owner)" in main diff --git a/resources/tester/test_m02_structure_panel.py b/resources/tester/test_m02_structure_panel.py index 67fafe82f..337f7f6e9 100644 --- a/resources/tester/test_m02_structure_panel.py +++ b/resources/tester/test_m02_structure_panel.py @@ -180,7 +180,7 @@ def test_면적_오른쪽_일위대가_패널은_공용_틀_손잡이(): def test_첫_화면과_닫은_뒤는_구조물집계표(): page = _text(M02 / "M02_MasterTemplete_UI_Page.ts") assert re.search(r'kind: "table",\s*name: STRUCTURE_TABLE', page) - assert "main.open(sel ?? home)" in page and "void main.open(home);" in page + assert "main.open(sel ?? home())" in page and "void main.open(home());" in page def test_좌측_고름_표시는_열린_것_한_줄뿐():