From e1b17a523c7e49fbfe676f67677c71d47ceb1fa2 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 2 Nov 2026 13:17:24 +0900 Subject: [PATCH] =?UTF-8?q?feat(732-1):=20M02=20=EC=8B=A4=EB=AC=B4?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=B9=B8=20=C2=B7=20=EB=B3=B5=EC=82=AC=20?= =?UTF-8?q?=ED=83=AD=20=C2=B7=20=EC=84=9C=EB=B2=84=20=EA=B8=B8=20=EA=B1=B7?= =?UTF-8?q?=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 왼쪽 「실무문서」 칸(Practice.ts) · 일위대가 가져오기 「실무문서」 탭 · 복사 원본 고르기 · 낱 CSS · locale 열쇠 걷음 - 서버 `from_layer=practice`(_copy_practice) · `DELETE /api/m02/practice`(drop_practice) · 조합 목록 · 키 하나 읽기 길 걷음 · 조합 길 시스템 관리자 문 걷음(담을 로직 읽기는 그대로) - 옛 마스터 조합 파일 `일위대가조합.json`(UA 37) = 쓰는 곳 있음(구조물집계표 일위대가 칸 · labor 본뜬키 · 프로젝트 초기값 사본 · 불러오기 맞춤 씨앗) → 파일 · 읽기 길 유지 · 지우지 않음 - 낡은 시험: test_m02_practice 지움(화면 · 길 없음) · 조합 길 읽던 시험(combo · combo_inputs · combo_layers)은 저장소 함수로 · 실무문서 복사 시험(labor_copy · pair_copy) 줄 지움 · 탭 · 차례 단정(layer_screen · structure_options · structure_panel · 720 · precombo_scope · ui_loading) 고침 Co-Authored-By: Claude Sonnet 5.5 --- M02_MasterTemplete/M02_Labor_Doc.py | 2 +- M02_MasterTemplete/M02_Labor_Migrate.py | 2 +- .../M02_MasterTemplete_Api_Fetch.ts | 9 +- .../M02_MasterTemplete_Combo_Store.py | 15 -- .../M02_MasterTemplete_ItemPick.ts | 37 +-- .../M02_MasterTemplete_Labor_Api.ts | 78 +----- .../M02_MasterTemplete_Layer.ts | 17 +- .../M02_MasterTemplete_Practice.ts | 232 ------------------ .../M02_MasterTemplete_Router_Combos.py | 57 +---- .../M02_MasterTemplete_Router_Items.py | 22 +- .../M02_MasterTemplete_Store.py | 9 +- .../M02_MasterTemplete_Structure.ts | 14 +- .../M02_MasterTemplete_UI_Side.ts | 22 +- .../M02_MasterTemplete_UI_Style.css | 34 --- resources/tester/test_720_m02_folder_layer.py | 6 +- resources/tester/test_m01_combo.py | 9 +- resources/tester/test_m01_combo_inputs.py | 17 +- resources/tester/test_m02_combo_layers.py | 53 +--- resources/tester/test_m02_labor_copy.py | 14 -- resources/tester/test_m02_layer_screen.py | 16 +- resources/tester/test_m02_pair_copy.py | 13 - resources/tester/test_m02_practice.py | 59 ----- resources/tester/test_m02_precombo_scope.py | 1 - .../tester/test_m02_structure_options.py | 8 +- resources/tester/test_m02_structure_panel.py | 2 +- resources/tester/test_ui_loading.py | 5 - ui_template/ui_template_locale_m2.ts | 27 -- 27 files changed, 79 insertions(+), 701 deletions(-) delete mode 100644 M02_MasterTemplete/M02_MasterTemplete_Practice.ts delete mode 100644 resources/tester/test_m02_practice.py diff --git a/M02_MasterTemplete/M02_Labor_Doc.py b/M02_MasterTemplete/M02_Labor_Doc.py index 55a731192..2ae60b5a7 100644 --- a/M02_MasterTemplete/M02_Labor_Doc.py +++ b/M02_MasterTemplete/M02_Labor_Doc.py @@ -324,7 +324,7 @@ def layered(got: dict[str, Any], rate: Any) -> dict[str, Any]: def from_combo(key: str) -> dict[str, Any]: - """옛 일위대가 조합(마스터 UA = 실무문서 · 회사 UC · 개인 UP) → 일위대가 문서 몸 — + """옛 일위대가 조합(마스터 UA · 회사 UC · 개인 UP) → 일위대가 문서 몸 — 담은로직 · 단위 복사 · 입력값 {} · 수량 null · 자재 [] · `본뜬` = 그 조합(층 · 주인 · 키). 없는 조합은 404.""" from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos diff --git a/M02_MasterTemplete/M02_Labor_Migrate.py b/M02_MasterTemplete/M02_Labor_Migrate.py index 37527b31b..8359bfd31 100644 --- a/M02_MasterTemplete/M02_Labor_Migrate.py +++ b/M02_MasterTemplete/M02_Labor_Migrate.py @@ -5,7 +5,7 @@ 수량 null · 자재 []) — 여러 열이 쓰던 조합도 열마다 따로 복사 - 키 없는 열 = 빈 일위대가 · 번호 SU 는 저장소가 줌(도번과 같은 뒷자리) - 몇 번 돌려도 같음 — 이미 그 키를 본뜬 문서 · 사람이 채운 문서(담은로직 · 자재가 있음)는 그대로 -옛 조합 파일 · 집계표 열 `일위대가` 칸은 손대지 않음(실무문서 · 은퇴는 39-1b B9). +옛 조합 파일 · 집계표 열 `일위대가` 칸은 손대지 않음. ./venv/Scripts/python.exe M02_MasterTemplete/M02_Labor_Migrate.py # 시스템 층 ./venv/Scripts/python.exe M02_MasterTemplete/M02_Labor_Migrate.py --storage # 회사 · 개인 층 diff --git a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts index 8f1da31b9..b2b7cb714 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts @@ -312,13 +312,12 @@ export const fetchStructureCombos = (): Promise => scoped("/str /** 조합 갈라 내기 결과 — `새로만듦` false = 같은 담은 로직의 조합이 이미 있어 그 키를 가리킴 */ /** 항목 하나 복사해 가져오기(39-5 · 39-1b) — 받는 열(`column`)에 덮어씀 · 번호 · 열은 그대로 · - * `source` = 원본 열 id(실무문서면 조합 키) · `from` 없음 = 지금 층 · 층 · 주인 = 다른 층(원본 읽기 권한) · - * `practice` = 실무문서 조합 → 일위대가(시스템 관리자만) */ + * `source` = 원본 열 id · `from` 없음 = 지금 층 · 층 · 주인 = 다른 층(원본 읽기 권한) */ export const copyPartFrom = async ( kind: "structure" | "labor" | "area", column: string, source: string, - from: { layer: Layer | "practice"; owner: string | null } | null, + from: { layer: Layer; owner: string | null } | null, pair = false, ): Promise => { const q = new URLSearchParams({ from: source }); @@ -333,11 +332,11 @@ export const copyPartFrom = async ( /** 일위대가 · 면적산출 함께 가져오기(43-9a · 카드 [가져오기]) — 원본 열의 면적산출(시트 · 알약 연결)과 * 일위대가(담은로직 · 입력값 · 수량 · 자재)로 둘 다 덮어씀 · 도면은 안 건드림 · - * 원본에 면적산출이 없으면(실무문서) 일위대가만 · 인자는 `copyPartFrom` 과 같음 */ + * 원본에 면적산출이 없으면 일위대가만 · 인자는 `copyPartFrom` 과 같음 */ export const copyLaborAreaFrom = ( column: string, source: string, - from: { layer: Layer | "practice"; owner: string | null } | null, + from: { layer: Layer; owner: string | null } | null, ): Promise => copyPartFrom("labor", column, source, from, true); /** 항목 하나 비우기(40-4 · 카드 [삭제]) — 새 열과 같은 빈 문서 · 번호 · 열 그대로 · 지금 층 */ diff --git a/M02_MasterTemplete/M02_MasterTemplete_Combo_Store.py b/M02_MasterTemplete/M02_MasterTemplete_Combo_Store.py index ce991de1a..28847f8e0 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Combo_Store.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Combo_Store.py @@ -277,21 +277,6 @@ def edit(key: str, row: Any, version: str) -> dict[str, Any]: return {"키": key, "판": _save(doc, version), "combo": fixed} -def drop_practice(version: str) -> dict[str, Any]: - """실무문서(마스터 조합 표 = 옛 일위대가 조합 · PLAN 39-2) 통째 지우기 — 줄을 모두 비움 · - 구조물이 쓰는 조합이 있으면 409(아무것도 안 지움) · 판 다르면 409 · 마스터 층에서만.""" - if heads()[-1] != MASTER: - raise store.StoreError(400, "실무문서는 마스터 층에만 있음") - doc, have = _load() - if have != (version or ""): - raise store.StoreError(409, {"stale": [FILE], "판": have}) - keys = [str(r.get("키")) for r in doc.get("줄") or []] - _refuse_used(keys) - if keys: - _m01(combo_store._write, [{"op": "delete", "key": k} for k in keys], have) - return {**listing(), "지운조합": keys} - - def delete(key: str, version: str) -> dict[str, Any]: """지우기 — 이 층 구조물 조합이 쓰면 409 + 항목 이름들(지우지 않음).""" if heads()[-1] == MASTER: diff --git a/M02_MasterTemplete/M02_MasterTemplete_ItemPick.ts b/M02_MasterTemplete/M02_MasterTemplete_ItemPick.ts index 956036f75..0e6956413 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_ItemPick.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_ItemPick.ts @@ -1,10 +1,10 @@ /* ============================================================================= * M02_MasterTemplete_ItemPick.ts - * 가져올 곳 고르기(PLAN 39-5) — 탭(마스터 · 회사 · 개인 · 다른 사람 · 실무문서) + 항목 목록(`ItemList`). + * 가져올 곳 고르기(PLAN 39-5) — 탭(마스터 · 회사 · 개인 · 다른 사람) + 항목 목록(`ItemList`). * 복사 생성(상세 칸 안 · `buildPicker`) · 복사해 가져오기(창 · `pickFrom`)가 같이 씀. * 탭 목록 · 기본 탭 = `readableSources`(권한 몫 · `M02_MasterTemplete_Layer.ts`) · 탭은 목록만 바꿈 * (좌측 층 단추와 달리 화면을 다시 안 세움) · 모양은 층 단추(`m02-layers__btn`) 그대로. - * 다른 사람 = 사람 고르기 하나(`fetchOwners`) · 실무문서 = 마스터 일위대가 조합 줄(일위대가 고를 때만). + * 다른 사람 = 사람 고르기 하나(`fetchOwners`). * ========================================================================== */ import { createButton, createSelectField, el, showConfirmDialog } from "@ui/ui_template_elements"; @@ -16,11 +16,9 @@ import { ITEM_COLS, loadItems, missingPart, - type Col, type ItemRow, type ListKind, } from "./M02_MasterTemplete_ItemList"; -import { fetchCombos, type ComboSummary } from "./M02_MasterTemplete_Labor_Api"; import { entryFromHash, ownOwner, @@ -42,28 +40,17 @@ const TEXT = { makeCopy: "복사 생성", }; -/** 고른 곳 — `practice` = 실무문서(마스터 일위대가 조합) */ +/** 고른 곳 */ export interface From { layer: Layer; owner: string | null; label: string; - practice: boolean; } -export type Picked = { from: From; row: ItemRow } | { from: From; combo: ComboSummary }; +export type Picked = { from: From; row: ItemRow }; /** 고른 것 이름 — 확인 글에 씀 */ -export const pickedName = (p: Picked): string => - "row" in p ? leafName(p.row) : `${p.combo.키} ${p.combo.이름}`; - -const PRACTICE_COLS: Col[] = [ - { head: "키", get: (c) => c.키 }, - { head: "이름", get: (c) => c.이름 }, - { head: "구분", get: (c) => c.구분 ?? "" }, - { head: "상세구분", get: (c) => c.상세구분 ?? "" }, - { head: "단위", get: (c) => c.단위 ?? "" }, - { head: "단가산출 기초 수", get: (c) => String(c.count) }, -]; +export const pickedName = (p: Picked): string => leafName(p.row); const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); /** 층 폴더가 아직 없는 사람(서버 사람 목록 `비어있음` · 39-6) */ @@ -73,7 +60,7 @@ export interface PickerOptions { user: SessionUser | null; /** 볼 회사(`viewCompany`) */ company: string | null; - /** 칸 종류 — 항목 통째(복사 생성) · 도면 · 면적산출 · 일위대가(실무문서 탭이 붙음) */ + /** 칸 종류 — 항목 통째(복사 생성) · 도면 · 면적산출 · 일위대가 */ kind: ListKind; /** 고를 수 없는 줄(받는 열 자신 등) — 목록에서 뺌 */ skip?: (from: From, row: ItemRow) => boolean; @@ -82,9 +69,7 @@ export interface PickerOptions { /** 탭 + 목록 — 부르는 쪽이 칸 안 · 창에 붙임 */ export function buildPicker(opt: PickerOptions): { root: HTMLElement } { - const { tabs, first } = readableSources(opt.user, opt.company, { - practice: opt.kind === "labor", - }); + const { tabs, first } = readableSources(opt.user, opt.company); const list = buildItemList(); const people = createSelectField({ options: [], compact: true, label: TEXT.person }); people.root.hidden = true; @@ -109,12 +94,6 @@ export function buildPicker(opt: PickerOptions): { root: HTMLElement } { people.root.hidden = tab.id !== "people"; list.message(); try { - if (tab.id === "practice") { - const combos = await fetchCombos(); - if (mine !== seq) return; - const from = { layer: tab.layer, owner: null, label: tab.label, practice: true }; - return list.show(combos, PRACTICE_COLS, (combo) => opt.onPick({ from, combo })); - } let owner = tab.owner; let label = tab.label; if (tab.id === "people") { @@ -139,7 +118,7 @@ export function buildPicker(opt: PickerOptions): { root: HTMLElement } { } const rows = await loadItems(tab.layer, owner); if (mine !== seq) return; - const from = { layer: tab.layer, owner, label, practice: false }; + const from = { layer: tab.layer, owner, label }; list.show( rows.filter((row) => !opt.skip?.(from, row)), ITEM_COLS[opt.kind], diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts index 67d1e1d51..6a2519765 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts @@ -1,8 +1,6 @@ /* ============================================================================= * M02_MasterTemplete_Labor_Api.ts - * 일위대가 조합 서버 길 — 층(마스터 UA · 회사 UC · 개인 UP) 조합 표 `일위대가조합.json` - * 길 = `/api/m02/layers/{layer}/combos`(PLAN 27-2a) · 키 하나 = `/api/m02/combos/key/{키}` · - * 담을 로직 읽기 = `/api/m02/logic-lib/…` · 모양 = `_화면_계약.md` 9장 + * 일위대가 서버 길 — 담을 로직 읽기 = `/api/m02/logic-lib/…` · 모양 = `_화면_계약.md` 9장 * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; @@ -21,61 +19,6 @@ import { type Layer, } from "./M02_MasterTemplete_Api_Fetch"; -/** 담은 로직 한 줄 — 로직 키와 메모뿐(수량·계산은 마스터에 두지 않음 · 계약 9장) */ -export interface ComboLogic { - 로직: string; - 메모: string | null; -} -/** 조합 한 줄 통째 — `_틀.md` 10장 · 키·소유는 서버 것이 이김 */ -export interface Combo { - 키: string; - 이름: string; - 구분: string | null; - 상세구분: string | null; - 단위: string | null; - 담은로직: ComboLogic[]; - 출처: string | null; - 소유: string | null; - /** 이 조합을 본떠 만든 원본 키 — 비면 원본(M02 가 갈라 낸 조합에 채워짐) */ - 본뜬키: string | null; - 비고: string | null; -} -export interface ComboSummary extends Combo { - count: number; - blocked: boolean; - reasons: string[]; -} -/** `GET /combo` 의 `줄` — 담은 로직에 이름을 붙인 것 */ -export interface ComboLine { - 차례: number; - 로직: string; - 메모: string | null; - ok: boolean; - 까닭?: string; - 이름?: string; - 구분?: string; - 상세구분?: string; - 결과단위?: string; -} -export interface ComboOne { - file: string; - version: string; - combo: Combo; - 줄: ComboLine[]; -} -/** 조합 층 — 층 + 주인(회사 `7` · 개인 `7/42` · 없으면 내 것) */ -export interface LaborScope { - layer: Exclude; - owner: string | null; -} -export const SYSTEM_SCOPE: LaborScope = { layer: "system", owner: null }; - -/** 폴더 = [구분] · [구분, 상세구분] — 깊이 2(PLAN 27-2a) */ -export interface ComboList { - 판: string; - 폴더: string[][]; - 줄: ComboSummary[]; -} async function request(url: string, method = "GET", body?: unknown): Promise { const response = await fetch(`${API_BASE_URL}/m02${url}`, { method, @@ -89,25 +32,6 @@ async function request(url: string, method = "GET", body?: unknown): Promise< } const enc = encodeURIComponent; -/** 층 길 — `path` 뒤에 주인 · 더 붙일 물음 */ -const at = (scope: LaborScope, path = "", more: Record = {}): string => { - const params = new URLSearchParams(more); - if (scope.owner) params.set("owner", scope.owner); - const q = params.toString(); - return `/layers/${scope.layer}/combos${path}${q ? `?${q}` : ""}`; -}; - -export const fetchComboList = (scope: LaborScope = SYSTEM_SCOPE): Promise => - request(at(scope)); - -export const fetchCombos = (scope: LaborScope = SYSTEM_SCOPE): Promise => - fetchComboList(scope).then((d) => d.줄); - -/** 키 하나 — 어느 층이든(그 층 읽기 권한) · `version` = 그 층 조합 표 판 */ -export const fetchCombo = (key: string): Promise => - request & LaborScope & { 판: string }>( - `/combos/key/${enc(key)}`, - ).then((d) => ({ ...d, file: "일위대가조합.json", version: d.판 })); /* --- 담을 로직(읽기만 · 로그인) — M01 로직 길과 같은 모양 --- */ export const fetchLibLogics = (): Promise => diff --git a/M02_MasterTemplete/M02_MasterTemplete_Layer.ts b/M02_MasterTemplete/M02_MasterTemplete_Layer.ts index 721f67c2a..cee791ad5 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Layer.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Layer.ts @@ -131,19 +131,18 @@ export function manages(company: string | null, user: SessionUser | null): boole /** 가져올 곳 탭 하나 — `people` = 다른 사람(주인은 부르는 쪽이 `fetchOwners` 로 고름) */ export interface Source { - id: "master" | "company" | "personal" | "people" | "practice"; + id: "master" | "company" | "personal" | "people"; layer: ManageLayer; - /** 층 주인 — 자기 층 · 마스터 · 실무문서 · 다른 사람 = null */ + /** 층 주인 — 자기 층 · 마스터 · 다른 사람 = null */ owner: string | null; label: string; } /** 복사 생성 · 복사해 가져오기의 탭(39-6 · 답 2) — 읽을 수 있는 곳 + 기본 탭. - * 기본 = 시스템 관리자 마스터 · 그 밖 내 개인(회사 없으면 마스터) · `practice` = 일위대가 고를 때만 */ + * 기본 = 시스템 관리자 마스터 · 그 밖 내 개인(회사 없으면 마스터) */ export function readableSources( user: SessionUser | null, company: string | null, - opt: { practice?: boolean } = {}, ): { tabs: Source[]; first: Source["id"] } { const isSystem = user?.role === "SYSTEM_ADMIN"; const own = ownOwner("personal", user); @@ -179,16 +178,6 @@ export function readableSources( }, ] : []), - ...(opt.practice && isSystem - ? [ - { - id: "practice" as const, - layer: "system" as const, - owner: null, - label: L("M02_SourcePractice"), - }, - ] - : []), ]; return { tabs, first: isSystem || !own ? "master" : "personal" }; } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Practice.ts b/M02_MasterTemplete/M02_MasterTemplete_Practice.ts deleted file mode 100644 index 23ffa1afb..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Practice.ts +++ /dev/null @@ -1,232 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Practice.ts - * 좌측 「실무문서」 컨테이너(구조물 조합 밖 · 도면 양식 아래 · 마스터 층 · 시스템 관리자만 · PLAN 39-2) — - * 줄 하나를 누르면 메인 칸에 옛 마스터 일위대가 조합(`일위대가조합.json` 통째) 목록 · 상세(읽기만) · - * [전체 삭제](물음 두 번 → `DELETE /api/m02/practice` · 서버도 시스템 관리자 · 쓰는 곳 0 확인). - * 읽기 = 마스터 조합 길(`Labor_Api`) · 복사 원본 고르기는 목록 화면(ItemList)의 실무문서 탭 몫. - * ========================================================================== */ - -import { API_BASE_URL } from "@config/config_frontend"; -import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; -import { loadingError, showLoading } from "@ui/ui_template_loading"; -import { t as L } from "@ui/ui_template_locale"; -import { - fetchCombo, - fetchComboList, - type ComboList, - type ComboOne, -} from "./M02_MasterTemplete_Labor_Api"; -import { createEmpty } from "@ui/ui_template_empty"; - -export interface PracticeSectionOptions { - /** 저장 안 한 고침을 버릴지 확인 — 취소면 false */ - confirmLeave: () => Promise; - /** 메인 칸에 화면을 띄움 */ - show: (label: string, node: HTMLElement) => (dirty: boolean) => void; - /** 화면을 열었음 — 좌측 다른 고름 표시를 지움 */ - onOpen: () => void; -} - -export interface PracticeSection { - title: string; - body: HTMLElement; - /** 다른 화면을 열었음 — 고름 표시만 지움 */ - clear: () => void; -} - -const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); - -/** 서버 까닭 글 — `{errors}` · 판 다름 · 글 */ -function reasonOf(detail: unknown, status: number): string { - if (typeof detail === "string") return detail; - const d = detail as { errors?: string[]; stale?: unknown } | null; - if (d?.errors?.length) return d.errors.join(" · "); - return d?.stale ? L("M02_PracticeStale") : `HTTP ${status}`; -} - -/** 통째 지우기 — 줄을 모두 비움(판 = 읽은 표 판) */ -async function dropPractice(판: string): Promise { - const res = await fetch(`${API_BASE_URL}/m02/practice?판=${encodeURIComponent(판)}`, { - method: "DELETE", - credentials: "include", - }); - if (res.ok) return; - const data = (await res.json().catch(() => ({}))) as { detail?: unknown }; - throw new Error(reasonOf(data.detail, res.status)); -} - -const table = (heads: string[], rows: HTMLElement[]): HTMLElement => - el("div", { - className: "ui-general-block__table-wrap ui-general-block__table-wrap--sticky-head", - children: [ - el("table", { - className: "ui-general-block__table m02-practice__table", - children: [ - el("thead", { - children: [el("tr", { children: heads.map((t) => el("th", { text: t })) })], - }), - el("tbody", { children: rows }), - ], - }), - ], - }); - -const cells = (values: string[]): HTMLElement[] => - values.map((v) => el("td", { text: v, attrs: { title: v } })); - -export function buildPracticeSection(opt: PracticeSectionOptions): PracticeSection { - const row = el("button", { - className: "m02-side__item ui-sidebar-row", - text: L("M02_PracticeRow"), - attrs: { type: "button", "data-practice-row": "" }, - }); - const body = el("div", { className: "m02-side__items ui-sidebar-list", children: [row] }); - const view = el("div", { className: "m02-practice" }); - let opened = false; - let nav = 0; // 늦게 온 응답 버림 - const mark = (on: boolean): void => { - opened = on; - row.classList.toggle("is-active", on); - }; - row.addEventListener("click", () => void open()); - - async function open(): Promise { - if (opened || !(await opt.confirmLeave())) return; - opt.show(L("M02_PracticeTitle"), view); - opt.onOpen(); - mark(true); - await drawList(); - } - - async function drawList(): Promise { - const my = ++nav; - const stop = showLoading(view); - let list: ComboList; - try { - list = await fetchComboList(); - } catch (error) { - stop(); - if (my === nav) view.replaceChildren(loadingError(why(error), () => void drawList())); - return; - } finally { - stop(); - } - if (my !== nav) return; - const drop = createButton({ label: L("M02_PracticeDrop"), variant: "danger", size: "sm" }); - drop.disabled = !list.줄.length; - drop.addEventListener("click", () => void dropAll(list, drop)); - const rows = list.줄.map((c) => { - const tr = el("tr", { - className: "m02-practice__row", - attrs: { "data-practice-key": c.키 }, - children: cells([c.키, c.이름, c.구분 ?? "", c.단위 ?? "", String(c.count)]), - }); - tr.addEventListener("click", () => void drawOne(c.키)); - return tr; - }); - view.replaceChildren( - el("div", { - className: "ui-toolbar m02-practice__bar", - children: [ - el("span", { - className: "ui-text ui-text--muted", - attrs: { "data-text": "14" }, - text: L("M02_PracticeCount").replace("{n}", String(list.줄.length)), - }), - drop, - ], - }), - rows.length - ? table( - [ - L("M02_PracticeKey"), - L("M02_Name"), - L("M02_PracticeGroup"), - L("M02_PracticeUnit"), - L("M02_PracticeLogics"), - ], - rows, - ) - : createEmpty({ text: L("M02_PracticeEmpty"), small: true }), - ); - } - - /** 상세 — 읽기만(이름 · 키 · 구분 · 단위 · 비고 + 담은 로직 표) */ - async function drawOne(key: string): Promise { - const my = ++nav; - const stop = showLoading(view); - let one: ComboOne; - try { - one = await fetchCombo(key); - } catch (error) { - stop(); - if (my === nav) view.replaceChildren(loadingError(why(error), () => void drawOne(key))); - return; - } finally { - stop(); - } - if (my !== nav) return; - const back = createButton({ label: L("M02_PracticeBack"), variant: "ghost", size: "sm" }); - back.addEventListener("click", () => void drawList()); - const c = one.combo; - const info = [ - [L("M02_PracticeKey"), c.키], - [L("M02_PracticeGroup"), c.구분 ?? ""], - [L("M02_PracticeUnit"), c.단위 ?? ""], - ].map(([k, v]) => - el("span", { - className: "ui-text ui-text--muted", - attrs: { "data-text": "14" }, - text: `${k} ${v}`, - }), - ); - const lines = one.줄.map((x) => - el("tr", { - children: cells([ - String(x.차례), - x.로직, - x.ok ? (x.이름 ?? "") : (x.까닭 ?? ""), - x.결과단위 ?? "", - x.메모 ?? "", - ]), - }), - ); - view.replaceChildren( - el("div", { - className: "ui-toolbar m02-practice__bar", - children: [back, el("strong", { text: c.이름 }), ...info], - }), - ...(c.비고 ? [el("p", { className: "m02-practice__memo", text: c.비고 })] : []), - lines.length - ? table( - [ - L("M02_PracticeOrder"), - L("M02_PracticeKey"), - L("M02_Name"), - L("M02_PracticeUnit"), - L("M02_PracticeMemo"), - ], - lines, - ) - : createEmpty({ text: L("M02_PracticeNoLogic"), small: true }), - ); - } - - /** 통째 지우기 — 물음 두 번 · 서버가 쓰는 곳 · 판을 다시 봄 */ - async function dropAll(list: ComboList, button: HTMLButtonElement): Promise { - const n = String(list.줄.length); - if (!(await showConfirmDialog(L("M02_PracticeDropAsk").replace("{n}", n), L("M02_Delete")))) - return; - if (!(await showConfirmDialog(L("M02_PracticeDropAsk2"), L("M02_PracticeDrop")))) return; - button.disabled = true; - try { - await dropPractice(list.판); - showToast(L("M02_PracticeDropped").replace("{n}", n), "success"); - } catch (error) { - showToast(L("M02_ActionFailed").replace("{value}", why(error)), "error"); - } - await drawList(); - } - - return { title: L("M02_PracticeTitle"), body, clear: () => mark(false) }; -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router_Combos.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Combos.py index 011163f40..40882864f 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router_Combos.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Combos.py @@ -1,12 +1,8 @@ -"""M02 일위대가 조합 층 API(PLAN 27-2a) — `/api/m02/layers/{layer}/combos`(옛 `/api/m01/combo*`). +"""M02 담을 로직 읽기 API — `/api/m02/logic-lib/…`(로그인만). -⚠ 등록은 `main.py`(로그인만) · 층 권한은 `M02_Layer_Access`(`Router_Items._root`) 한 규칙 · -읽기만(목록 · 키 하나) + 실무문서 통째 지우기 — 갈라 내기 · 키 적기 · 만들기 · 고치기 · 지우기 · -알약 길은 은퇴(39-1d · 정본 = 열마다 `labor/<열>.json`). -저장소 = `M02_MasterTemplete_Combo_Store`. +⚠ 등록은 `main.py`(로그인만). 조합 목록 · 키 하나 · 통째 지우기 길은 없앰(732-1). 담을 로직 고르기 · 일위대가 입력 모달이 읽는 단가산출 로직(M01 길은 시스템 관리자 전용)은 `/api/m02/logic-lib/…` 읽기만(로그인) — 모양은 M01 길과 같음. -39-6 — 일위대가 조합 = 실무문서 · 조합 길 전부 시스템 관리자만(`_door` · 담을 로직 읽기 뺌). 85-4 — 원문 패널 읽기(`/logic-lib/source` · M01 길과 같은 몸) · 원문 pdf 통째 (`/logic-lib/source-pdf` · 책 이름만 받음 · 경로 안 받음)도 로그인만. 704 — 기준 목록(`/logic-lib/standard`) · 계산서(`/logic-lib/sheet`) 로그인만(`M02_Logic_Standard`). @@ -26,61 +22,14 @@ from common_util.common_util_http_cache import cached_file_response from M01_MasterData import M01_MasterData_Store as m01 from M01_MasterData import M01_MasterData_Store_Make as make from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs -from M02_MasterTemplete import M02_Layer_Access as access from M02_MasterTemplete import M02_Logic_Standard as standard -from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos -from M02_MasterTemplete.M02_MasterTemplete_Router_Items import Layer, Session, _call, _root - -def _door(request: Request, session: Session = Depends(verify_session)) -> None: - """일위대가 조합 = 실무문서(39-6) — 조합 길 전부 시스템 관리자만 · 담을 로직 읽기는 로그인만.""" - if "/logic-lib/" not in request.url.path and not access.is_system_admin(session): - raise HTTPException(status_code=403, detail="실무문서(일위대가 조합)는 시스템 관리자만 봄.") - - -router = APIRouter( - prefix="/api/m02", tags=["M02 MasterTemplete Combos"], dependencies=[Depends(_door)] -) +router = APIRouter(prefix="/api/m02", tags=["M02 MasterTemplete Combos"]) if "pytest" not in sys.modules: # 기준 목록을 뒤에서 한 번 셈(저장본이 있으면 읽기만) standard.warm() -def _where(key: str) -> tuple[str, str | None]: - got = combos.where(key) - if got is None: - raise HTTPException(status_code=404, detail=f"없는 조합 「{key}」") - return got - - -@router.get("/combos/key/{key}") -def get_by_key(key: str, session: Session = Depends(verify_session)) -> dict: - """키 하나(어느 층이든 · 그 층 읽기 권한) — `{layer, owner, 판, combo, 줄, …}`.""" - layer, owner = _where(key) - return {"layer": layer, "owner": owner, **_call(_root(session, layer, owner), combos.one, key)} - - -@router.get("/layers/{layer}/combos") -def get_combos(layer: Layer, owner: str | None = None, session: Session = Depends(verify_session)): - return _call(_root(session, layer, owner), combos.listing) - - -@router.get("/layers/{layer}/combos/{key}") -def get_combo( - layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session) -): - return _call(_root(session, layer, owner), combos.one, key) - - -@router.delete("/practice") -def delete_practice(판: str = "", session: Session = Depends(verify_session)) -> dict: - """실무문서 통째 지우기(PLAN 39-2) — 시스템 관리자만 · 마스터 조합 표 줄을 모두 비움 · - 구조물이 쓰는 조합이 있으면 409.""" - if not access.is_system_admin(session): - raise HTTPException(status_code=403, detail="실무문서는 시스템 관리자만") - return _call(_root(session, "system", None, write=True), combos.drop_practice, 판) - - # ── 담을 로직(읽기만) — M01 `/logics` · `/subs?group=로직` · `/logic` · `/logic/auto` 모양 ── def _read(fn, *args): try: diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py index 873991b11..0ec01999b 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py @@ -301,15 +301,6 @@ def post_structure( return _call(_root(session, layer, owner, write=True), store.create_structure, column) -def _copy_practice(column: str, key: str) -> dict[str, Any]: - """실무문서 조합(마스터 UA) → 이 열 일위대가 문서(입력값 빈 값 · 39-1b).""" - from M02_MasterTemplete import M02_Labor_Doc as labor - - if not key.startswith("UA"): - raise store.StoreError(400, f"실무문서 조합 키가 아님 「{key}」") - return store.copy_from("labor", column, key, body=labor.from_combo(key)) - - class AreaTableBody(BaseModel): """면적산출 범위 → 도면 표(41-1) — `시트` = 시트 id · 이름 · `범위` · `이름`(104-1) 이 있으면 그 이름이 지금 가리키는 범위로 품(연결표 다시 읽을 때) · `글자크기` 기본 2.5.""" @@ -358,23 +349,16 @@ def post_copy( column: str, source: str = Query(alias="from"), owner: str | None = None, - from_layer: Literal["system", "company", "personal", "practice"] | None = None, + from_layer: Literal["system", "company", "personal"] | None = None, from_owner: str | None = None, pair: bool = False, session: Session = Depends(verify_session), ): """항목 하나 복사해 가져오기(덮어씀 · 번호 · 열 그대로) — 원본 = 같은 층(`from_layer` 없음) · - 다른 층(원본 읽기 권한) · 실무문서 조합(`practice` · 일위대가만 · 시스템 관리자만) · - `pair` = 일위대가 · 면적산출 함께(43-9a · 답 `{area?, labor}` · 실무문서는 일위대가만).""" + 다른 층(원본 읽기 권한) · + `pair` = 일위대가 · 면적산출 함께(43-9a · 답 `{area?, labor}`).""" folder = _root(session, layer, owner, write=True) part = "structure" if kind == "structures" else kind - if from_layer == "practice": - if part not in (("labor", "area") if pair else ("labor",)) or not access.is_system_admin( - session - ): - raise HTTPException(status_code=403, detail="실무문서는 시스템 관리자가 일위대가로만") - got = _call(folder, _copy_practice, column, source) - return {"labor": got} if pair else got origin = _root(session, from_layer, from_owner) if from_layer else None if pair and part != "structure": return _call(folder, store.copy_pair, column, source, origin) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Store.py b/M02_MasterTemplete/M02_MasterTemplete_Store.py index 99b52145d..561310534 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Store.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Store.py @@ -449,7 +449,7 @@ def copy_from( """다른 열(`source`) 내용을 받는 열(`column`)로 가져옴 — 번호 · 열은 받는 쪽 그대로(두 열은 따로 움직임) · 통합문서는 알약 `연결` 도 같이 오고 계산값은 서버가 다시 풂 · `source_root` = 원본 층 폴더(없으면 지금 층 · 권한은 부르는 쪽) · `body` = 원본 문서를 바로 줌 - (실무문서 조합 → 일위대가 · 39-1b).""" + (예: 문서를 만들어 바로 담음).""" if kind not in _KEY: raise StoreError(404, f"없는 종류 「{kind}」") target = read(kind, column)["문서"] @@ -472,18 +472,17 @@ def copy_pair( column: str, source: str, source_root: Path | None = None, - labor_body: dict[str, Any] | None = None, ) -> dict[str, Any]: """일위대가 ↔ 면적산출 함께 가져오기(43-9a) — 같은 원본 열의 면적산출(시트 · 알약 `연결`)과 일위대가(담은로직 · 입력값 · 수량 · 자재)로 둘 다 덮어씀 · 도면은 안 건드림 · - 원본에 면적산출이 없으면(실무문서 `labor_body` 포함) 일위대가만 · 면적 먼저(일위대가 계산값이 + 원본에 면적산출이 없으면 일위대가만 · 면적 먼저(일위대가 계산값이 새 면적 칸 값을 읽음 · 면적 계산이 막히면 일위대가도 안 씀).""" with at(source_root or root()): - has_area = labor_body is None and _path("area", source).is_file() + has_area = _path("area", source).is_file() out: dict[str, Any] = {} if has_area: out["area"] = copy_from("area", column, source, source_root) - out["labor"] = copy_from("labor", column, source, source_root, labor_body) + out["labor"] = copy_from("labor", column, source, source_root) return out diff --git a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts index ba755f81c..50396c26a 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts @@ -304,7 +304,7 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane /** 다른 항목의 도면 · 면적산출(…)을 골라 내용만 복사(39-5) — 탭 = 읽을 수 있는 곳 · 덮어쓰기 확인 · * 번호 · 열은 그대로 · 보고 있던 열이면 다시 읽음 · 집계표 줄 「조회 후 선택」 · 카드 「가져오기」가 씀 · - * 일위대가 · 면적산출은 함께(43-9b · 알약 연결이 이어져 있음 · 실무문서면 서버가 일위대가만) · 도면은 그 칸만 */ + * 일위대가 · 면적산출은 함께(43-9b · 알약 연결이 이어져 있음) · 도면은 그 칸만 */ const pickCopy = async (kind: CardKind, to: StructureColumn | undefined): Promise => { const pair = kind !== "structure"; const word = PART_WORD[kind]; @@ -335,13 +335,11 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane const copy = (source: string, from: Parameters[3]) => pair ? copyLaborAreaFrom(to.id, source, from) : copyPartFrom(kind, to.id, source, from); try { - // 같은 곳 = 다른 열 · 다른 층 · 사람 = 원본 층 · 실무문서 = 조합 키 → 일위대가(39-1b) - if ("combo" in picked) await copy(picked.combo.키, { layer: "practice", owner: null }); - else - await copy( - picked.row.열, - same(picked.from) ? null : { layer: picked.from.layer, owner: picked.from.owner }, - ); + // 같은 곳 = 다른 열 · 다른 층 · 사람 = 원본 층 + await copy( + picked.row.열, + same(picked.from) ? null : { layer: picked.from.layer, owner: picked.from.owner }, + ); showToast(TEXT.copied, "success"); if (column === to.id) void load(shown); } catch (error) { diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts index fd83c0998..e06795503 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts @@ -2,9 +2,9 @@ * M02_MasterTemplete_UI_Side.ts * 마스터 템플릿 좌측 패널 차례(716-3 · 717-2a) — 「단가산출」(층 단추 따로 · 기본 개인 · `UI_Side_Sangeun`) › 「일위대가」(층 단추 따로 · 2단 폴더 나무 · `UI_Side_Precombo` · * 50-3 · 134-3) › 「구조물 조합」(층 단추 · 나무) › 수평 구분선 › 「일위대가 불러오기」(구조물 조합 층 · - * `Hopyo_Side` · 324-4) › 「도면 양식」(새로 · 본떠 · 지우기) › 실무문서. + * `Hopyo_Side` · 324-4) › 「도면 양식」(새로 · 본떠 · 지우기). * 구조물 옵션 정의는 M01 로 옮김(716-1). - * 도면 양식 · 실무문서 = 시스템 관리자면 늘(마스터 층 것 · 구조물 조합 층과 무관 · 720-4). + * 도면 양식 = 시스템 관리자면 늘(마스터 층 것 · 구조물 조합 층과 무관 · 720-4). * 서버 길은 계약 `6_계약.md`. * 구조물 도면 · 일위대가 · 면적산출 상세는 좌측 줄이 아니라 항목 카드 [수정]으로(PLAN 45-2). * 화면 이름 「구조물 조합」(PLAN 26-7a) — 파일 이름 · 양식 키 · API 길은 「구조물집계표」 그대로. @@ -32,7 +32,6 @@ import { type TemplateInfo, } from "./M02_MasterTemplete_Api_Fetch"; 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 type { Selection } from "./M02_MasterTemplete_UI_Main"; @@ -55,7 +54,7 @@ const STUB_DRAWING = { }; export interface SideOptions { - /** 시스템 관리자 — 도면 양식(시스템 층에만 있음) · 실무문서 칸을 늘 보임(720-4) */ + /** 시스템 관리자 — 도면 양식(시스템 층에만 있음)을 늘 보임(720-4) */ isSystem: boolean; /** 지금 구조물 조합 층을 고칠 수 있나(층이 제자리에서 바뀜) — 일위대가 불러오기 장부 */ canWrite: () => boolean; @@ -163,20 +162,12 @@ export function buildSide(opt: SideOptions): SideHandle { document.addEventListener(HOPYO_BUILT, onBuilt); // 도면 양식 칸 = 누구나 늘(읽기 · 고르기) · 새로 · 본떠 · 지우기 단추만 시스템 관리자 const drawing = section(L("M02_KindDrawing"), bodies.get("drawing")!, ...actionRow("drawing")); - // 실무문서(옛 마스터 일위대가 조합 · 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: opt.canWrite, confirmLeave: opt.confirmLeave, show: opt.showView, - onOpen: () => (practice?.clear(), paint()), + onOpen: paint, }); const listHost = el("div", { className: "m02-side__lists ui-sidebar-panel", @@ -187,7 +178,6 @@ export function buildSide(opt: SideOptions): SideHandle { createDivider(), section(hopyo.title, hopyo.body), drawing, - ...(practice ? [section(practice.title, practice.body)] : []), ], }); attachCollapsible(listHost); @@ -236,15 +226,13 @@ export function buildSide(opt: SideOptions): SideHandle { for (const [kind, b] of editButtons) b.copy.disabled = b.del.disabled = cur?.종류 !== kind; } - /** 고름 표시는 여기서만 — 목록 · 실무문서 줄을 함께 그려야 두 줄이 동시에 안 남음 - * (양식이 열려 있으면 실무문서 고름은 지움) */ + /** 고름 표시는 여기서만 */ function paint(): void { paintList(); sangeun.paint(); precombo.paint(); hopyo.paint(); paintButtons(); - if (opt.getOpen()) practice?.clear(); } // 여는 것은 편집 칸(`onOpen`) — 편집 칸이 열 때마다 `paint` 를 부름(페이지가 이음) diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css index e74257d20..277727da2 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css @@ -10,40 +10,6 @@ font-size: var(--text-body-sm); } -/* 실무문서 화면(PLAN 39-2) — 위 막대(개수 · 통째 지우기 / 목록으로 · 이름 · 정보) + 표 */ -.m02-practice { - display: flex; - flex-direction: column; - gap: var(--spacing-8); - min-width: 0; -} - -.m02-practice__bar { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: var(--spacing-8); -} - -.m02-practice__bar > button:last-child:not(:first-child) { - margin-left: auto; -} - -.m02-practice__memo { - margin: 0; - color: var(--color-text-secondary); - font-size: var(--text-body-sm); - white-space: pre-wrap; -} - -.m02-practice__row { - cursor: pointer; -} - -.m02-practice__row:hover { - background: var(--color-accent-soft); -} - .m02-side__row { display: flex; flex-wrap: wrap; diff --git a/resources/tester/test_720_m02_folder_layer.py b/resources/tester/test_720_m02_folder_layer.py index a22252dab..d8b759f7e 100644 --- a/resources/tester/test_720_m02_folder_layer.py +++ b/resources/tester/test_720_m02_folder_layer.py @@ -78,7 +78,7 @@ def test_화면_층_단추_최상위_폴더_이름_바꾸기(): 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] @@ -91,10 +91,10 @@ def test_구조물_조합_층_제자리_바꿈(): 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 = section(" in side and "const practice = opt.isSystem" in side + assert "const drawing = section(" in side and "practice" not in side # 732-1 # 도면 양식 칸 = 누구나 · 단추만 관리자(720-4 덧) assert "const drawing = opt.isSystem" not in side - assert 'if (!opt.isSystem) return [];' in side + assert "if (!opt.isSystem) return [];" in side assert "items = await listTemplates(layer, null);" in side hopyo = _text("M02_MasterTemplete_Hopyo_Side.ts") assert "buildHopyoLedger({ scope: pageScope(), canWrite: opt.canWrite() })" in hopyo diff --git a/resources/tester/test_m01_combo.py b/resources/tester/test_m01_combo.py index 8cf31f4f4..cfb0c76f9 100644 --- a/resources/tester/test_m01_combo.py +++ b/resources/tester/test_m01_combo.py @@ -1,6 +1,6 @@ """일위대가 조합(PLAN 4-2 · 27-2a) — 담은 단가산출 로직 목록만 두고 미리 보기로 비목별 합계. -길 = M02 층 길 마스터(`/api/m02/layers/system/combos` · 옛 `/api/m01/combo*` 를 옮김). +읽기 = M02 조합 저장소(`combos.one` · `combos.listing` · 서버 길은 없앰 732-1). 계약 `resources/master_data/_화면_계약.md` 9장 · 틀 `_틀.md` 10장 · 엔진 `scripts/master_combo.py`. 못박는 것 @@ -39,7 +39,6 @@ STACK = "GF000219" # 13-4-1 돌쌓기 메쌓기(인력) LAYER = "GF000220" # 13-4-2 돌쌓기 메쌓기(장비) GATHER = "GF000211" # 13-2-1 모래·자갈·약돌 채집(인력) SAMPLE = (STACK, LAYER, GATHER) # 견본 조합 — 로직 셋 -SYS = "/api/m02/layers/system/combos" # 마스터 층 조합 길 ADMIN = {"user_id": 1, "company_id": 1, "role": "SYSTEM_ADMIN", "is_master": False} @@ -87,12 +86,12 @@ def _new(client: TestClient, body: dict, want: int = 200) -> dict: def _one(client: TestClient, key: str) -> dict: - got = _get(client, f"{SYS}/{key}") + got = combos.one(key) return {"version": got["판"], **got} def _list(client: TestClient) -> list[dict]: - return _get(client, SYS)["줄"] + return combos.listing()["줄"] def _edit(client: TestClient, key: str, version: str, row: dict, want: int = 200) -> dict: @@ -177,7 +176,7 @@ def test_목록은_소유_담은_수_폴더를_준다(client: TestClient) -> Non """거르기(구분 · 소유 · 찾기)는 화면 몫 — 층 목록은 통째 · 폴더 = [구분, 상세구분].""" _make(client, 이름="돌쌓기 한 벌") _make(client, keys=(GATHER,), 이름="채집만", owner="공용") - got = _get(client, SYS) + got = combos.listing() assert len(got["줄"]) == 2 and got["판"] only = [c for c in got["줄"] if c["소유"] == "공용"] assert [c["이름"] for c in only] == ["채집만"] and only[0]["count"] == 1 diff --git a/resources/tester/test_m01_combo_inputs.py b/resources/tester/test_m01_combo_inputs.py index f59dc3b0a..d4b0e997e 100644 --- a/resources/tester/test_m01_combo_inputs.py +++ b/resources/tester/test_m01_combo_inputs.py @@ -1,4 +1,4 @@ -"""조합 알약(`GET /api/m02/layers/system/combos/{키}/inputs`) — 결과단위 분모가 물량 하나. +"""조합 알약(저장소 `combo_inputs`) — 결과단위 분모가 물량 하나. 못박는 것 - 물량 = 로직 결과단위 분모(원/㎥ → ㎥) · 설계 조건(고르기 · 범위)은 안 넣음(`조건` 은 빈 자리). @@ -26,7 +26,6 @@ from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos from M02_MasterTemplete import M02_MasterTemplete_Store as m02 from M02_MasterTemplete.M02_MasterTemplete_Router_Combos import router as combos_router -SYS = "/api/m02/layers/system/combos" ADMIN = {"user_id": 1, "company_id": 1, "role": "SYSTEM_ADMIN", "is_master": False} @@ -118,22 +117,24 @@ def _use(sheet: Path, key: str) -> None: def test_쓰는_열이_있으면_조합을_못_지우고_구조물_이름을_알린다(own) -> None: key = _new(own, "쓰는 조합", ["GF000222"]) _use(combo_store.USERS[0], key) - version = own.get(f"{SYS}/{key}").json()["판"] + version = combos.one(key)["판"] with pytest.raises(m02.StoreError) as caught: # 지우기 길은 은퇴(39-1d) — 저장소 함수로 combos.delete(key, version) assert caught.value.status == 409 detail = caught.value.detail assert detail["users"] == ["돌기슭막이 찰쌓기"] assert "돌기슭막이 찰쌓기" in detail["errors"][0] - assert own.get(f"{SYS}/{key}").status_code == 200 # 그대로 남음 + assert combos.one(key)["combo"]["키"] == key # 그대로 남음 def test_안_쓰는_조합은_지워진다(own) -> None: key = _new(own, "안 쓰는 조합", ["GF000222"]) _use(combo_store.USERS[0], "UA000009") # 다른 조합을 씀 - version = own.get(f"{SYS}/{key}").json()["판"] + version = combos.one(key)["판"] combos.delete(key, version) - assert own.get(f"{SYS}/{key}").status_code == 404 + with pytest.raises(m02.StoreError) as gone: + combos.one(key) + assert gone.value.status == 404 def test_본뜬키는_목록에_나오고_없는_키_자기_키는_400(own) -> None: @@ -141,13 +142,13 @@ def test_본뜬키는_목록에_나오고_없는_키_자기_키는_400(own) -> N combo = {"이름": "본뜬 것", "본뜬키": base, "담은로직": [{"로직": "GF000222", "메모": None}]} got = combos.create(combo) assert got["combo"]["본뜬키"] == base - rows = {r["키"]: r for r in own.get(SYS).json()["줄"]} + rows = {r["키"]: r for r in combos.listing()["줄"]} assert rows[got["키"]]["본뜬키"] == base and rows[base]["본뜬키"] is None bad = {**combo, "이름": "없는 원본", "본뜬키": "UA999999"} with pytest.raises(m02.StoreError) as caught: combos.create(bad) assert caught.value.status == 400 - version = own.get(f"{SYS}/{base}").json()["판"] + version = combos.one(base)["판"] me = {"이름": "원본", "본뜬키": base, "담은로직": combo["담은로직"]} with pytest.raises(store.StoreError) as caught: # 조합 고치기 길(PUT) 은퇴 — 저장소 함수로 combo_store.combo_edit(base, version, me) diff --git a/resources/tester/test_m02_combo_layers.py b/resources/tester/test_m02_combo_layers.py index 18b877a24..3a898aa10 100644 --- a/resources/tester/test_m02_combo_layers.py +++ b/resources/tester/test_m02_combo_layers.py @@ -8,6 +8,7 @@ - 만들기 · 고치기 · 지우기 · 알약 · 고르기(use-combo) · 갈라 내기 길은 은퇴(39-1d) — 만들기 · 지우기는 층 문(`_root`) + 저장소 함수로 봄. - 세 층 목록 · 키 하나 읽기 · 조합 이름 · 알약(저장소) · 스냅샷 원본이 회사 · 개인 조합도 읽음. + - 조합 목록 · 키 하나 · 통째 지우기 서버 길은 없앰(732-1) — 읽기는 저장소 함수로 봄. 정본(마스터 · 마스터 템플릿)은 읽기만 · 쓰기는 임시 storage 와 마스터 사본. """ @@ -52,27 +53,9 @@ def world(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]: app.include_router(items_router.router) app.include_router(combos_router.router) app.dependency_overrides[verify_session] = lambda: session - # 문(39-6 · 시스템 관리자만)은 `test_조합_길은_시스템_관리자만` 이 봄 — 여기는 그 안의 층 규칙 - app.dependency_overrides[combos_router._door] = lambda: None return {"client": TestClient(app), "session": session, "storage": storage} -def test_조합_길은_시스템_관리자만(world): - """39-6 — 일위대가 조합 = 실무문서 · 조합 길 전부 시스템 관리자만 · 담을 로직은 로그인만.""" - world["client"].app.dependency_overrides.pop(combos_router._door) - for role in ("USER", "ADMIN"): - client = _as(world, role) - assert client.get(URL.format("system")).status_code == 403 - assert client.get("/api/m02/combos/key/UA000001").status_code == 403 - assert client.get("/api/m02/logic-lib/logics").status_code == 200 - world["session"]["is_master"] = True - assert _as(world, "USER").get(URL.format("system")).status_code == 403 # 회사 마스터도 - world["session"]["is_master"] = False - admin = _as(world, "SYSTEM_ADMIN") - assert admin.get(URL.format("system")).status_code == 200 - assert admin.get(URL.format("company")).status_code == 200 - - def _as(world: dict, role: str = "USER", user: int = 42) -> TestClient: world["session"].update(role=role, user_id=user) return world["client"] @@ -99,12 +82,6 @@ def _new(client: TestClient, layer: str, combo: dict, want: int = 200, **params) return got if code == 200 else {"detail": detail} -def _list(client: TestClient, layer: str, **params) -> dict: - res = client.get(URL.format(layer), params=params) - assert res.status_code == 200, res.text - return res.json() - - # ── 키 · 자리 ───────────────────────────────────────────────────────── def test_회사_개인_키_장부_지운_번호는_다시_안_줌(world): boss = _as(world, "ADMIN") @@ -120,7 +97,8 @@ def test_회사_개인_키_장부_지운_번호는_다시_안_줌(world): with store.at(layers.personal_dir(7, 42)): # 지우기 길은 은퇴(39-1d) — 저장소 함수로 combos.delete(mine["키"], mine["판"]) assert _new(me, "personal", _combo("다음 것"))["키"] == "UP42-000002" - got = me.get(f"{URL.format('personal')}/UP42-000002").json() + with store.at(layers.personal_dir(7, 42)): + got = combos.one("UP42-000002") assert [x["로직"] for x in got["줄"]] == [A] and got["blocked"] is False @@ -129,9 +107,6 @@ def test_권한_층_규칙(world): _new(user, "system", _combo("마스터"), 403) _new(user, "company", _combo("회사"), 403) _new(user, "personal", _combo("남의 것"), 403, owner="7/43") - assert user.get(URL.format("system")).status_code == 200 # 마스터 읽기 = 누구나 - assert user.get(URL.format("company")).status_code == 200 # 같은 회사 읽기 - assert user.get(URL.format("personal"), params={"owner": "8/9"}).status_code == 403 _as(world, "ADMIN") _new(user, "company", _combo("회사")) world["session"]["company_id"] = None @@ -151,16 +126,15 @@ def test_검사_빈_조합_이름_겹침_본뜬키_판(world): def test_조합_쓰기_알약_길은_은퇴(world): - """39-1d — 만들기 · 고치기 · 지우기 · 알약 길 없음(읽기 · 실무문서 통째 지우기만 남음).""" + """39-1d — 만들기 · 고치기 · 지우기 · 알약 길 없음(조합 서버 길 자체가 없음 · 732-1).""" me = _as(world, "SYSTEM_ADMIN") key = _new(me, "personal", _combo("원본"))["키"] url = f"{URL.format('personal')}/{key}" - assert me.post(URL.format("personal"), json={"combo": _combo("새")}).status_code == 405 - assert me.put(url, json={"combo": _combo("고침")}).status_code == 405 - assert me.delete(url, params={"판": ""}).status_code == 405 - assert me.get(f"{url}/inputs").status_code == 404 + assert me.post(URL.format("personal"), json={"combo": _combo("새")}).status_code in (404, 405) + assert me.put(url, json={"combo": _combo("고침")}).status_code in (404, 405) + assert me.delete(url, params={"판": ""}).status_code in (404, 405) + assert me.get(url).status_code in (404, 405) assert me.get(f"/api/m02/combos/key/{key}/inputs").status_code == 404 - assert me.get(url).status_code == 200 # 읽기는 남음 # ── 마스터 사본 ─────────────────────────────────────────────────────── @@ -184,7 +158,7 @@ def master_copy(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: # ── 세 층 고르기 · 갈라 내기 · 읽는 곳 ───────────────────────────────── -def test_세_층_목록_고르기_남의_층_403_갈라_낸_것은_그_층에(world): +def test_세_층_조합_찾기_갈라_낸_것은_그_층에(world): boss = _as(world, "ADMIN") uc = _new(boss, "company", _combo("회사 조합", (A, B)))["키"] me = _as(world) @@ -192,13 +166,8 @@ def test_세_층_목록_고르기_남의_층_403_갈라_낸_것은_그_층에(wo assert me.get("/api/m02/combos/all").status_code == 404 # 세 층 고르기 길 은퇴(39-1d) assert combos.find(uc)["이름"] == "회사 조합" and combos.find(up)["이름"] == "내 조합" assert combos.find("UP42-999999") is None and combos.find("엉터리") is None - by_key = me.get(f"/api/m02/combos/key/{up}").json() - assert (by_key["layer"], by_key["owner"], by_key["combo"]["키"]) == ("personal", "7/42", up) - assert me.get(f"/api/m02/combos/key/{uc}").json()["owner"] == "7" - assert me.get("/api/m02/combos/key/UP42-999999").status_code == 404 - world["session"]["company_id"] = 8 # 다른 회사 사람 — 남의 회사 조합은 못 봄 - assert me.get(f"/api/m02/combos/key/{uc}").status_code == 403 - world["session"]["company_id"] = 7 + assert combos.where(up) == ("personal", "7/42") and combos.where(uc) == ("company", "7") + assert me.get(f"/api/m02/combos/key/{up}").status_code == 404 # 키 하나 읽기 길 없앰(732-1) # 고르기(use-combo) · 갈라 내기 길은 은퇴(39-1d) — 열마다 labor 문서 base = "/api/m02/layers/personal/structures/c1" assert me.post(f"{base}/use-combo", json={"종류": "일위대가", "키": up}).status_code == 404 diff --git a/resources/tester/test_m02_labor_copy.py b/resources/tester/test_m02_labor_copy.py index b2cc4269f..bfd70d32f 100644 --- a/resources/tester/test_m02_labor_copy.py +++ b/resources/tester/test_m02_labor_copy.py @@ -96,20 +96,6 @@ def test_권한_없는_원본_층_403_없는_원본_404(world): assert client.post(URL.format("system", "labor", "c1") + "?from=rv").status_code == 403 -def test_실무문서_조합에서_일위대가로_시스템_관리자만(world): - url = URL.format("system", "labor", "c1") + "?from=UA000008&from_layer=practice" - assert _as(world).post(url).status_code == 403 - admin = _as(world, "SYSTEM_ADMIN") - assert admin.post(url).status_code == 200 - c1 = _labor(world["system"], "c1") - assert c1["본뜬"] == {"층": "system", "주인": None, "키": "UA000008"} - assert len(c1["담은로직"]) == 14 and all(x["입력값"] == {} for x in c1["담은로직"]) - area = URL.format("system", "area", "c1") + "?from=UA000008&from_layer=practice" - assert admin.post(area).status_code == 403 # 실무문서는 일위대가로만 - bad = URL.format("system", "labor", "c1") + "?from=UP1-000001&from_layer=practice" - assert admin.post(bad).status_code == 400 - - def test_구조물_조합_복사는_새_일위대가번_조합_키는_안_받음(world): system, personal = world["system"], world["personal"] with store.at(system): diff --git a/resources/tester/test_m02_layer_screen.py b/resources/tester/test_m02_layer_screen.py index 744e73968..fdcb13461 100644 --- a/resources/tester/test_m02_layer_screen.py +++ b/resources/tester/test_m02_layer_screen.py @@ -97,8 +97,8 @@ def test_주소_인자를_라우트에서_뗌(): @pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") def test_가져올_곳_탭(): - """39-6 · 답 2 — 탭 = 마스터 · 볼 회사 · 내 개인 · 다른 사람(맡은 회사) · 실무문서(시스템 관리자 · - 일위대가) · 기본 = 시스템 관리자 마스터 · 그 밖 내 개인(회사 없으면 마스터).""" + """39-6 · 답 2 — 탭 = 마스터 · 볼 회사 · 내 개인 · 다른 사람(맡은 회사) · + 기본 = 시스템 관리자 마스터 · 그 밖 내 개인(회사 없으면 마스터).""" text = _text(LAYER) parts = [ re.search(r"^export const ownOwner[\s\S]*?;\n", text, re.M), @@ -114,8 +114,8 @@ def test_가져올_곳_탭(): "const L = (key: string): string => key;\n" + "\n".join(p.group(0).replace("export ", "") for p in parts if p) + "\nconst cases = JSON.parse(process.argv[1]);" - + "\nconsole.log(JSON.stringify(cases.map(([u, c, practice]) => {" - + "\n const got = readableSources(u, c, { practice });" + + "\nconsole.log(JSON.stringify(cases.map(([u, c]) => {" + + "\n const got = readableSources(u, c);" + "\n return [got.first, got.tabs.map((t) => t.id + (t.owner ? ':' + t.owner : ''))];" + "\n})));" ) @@ -125,8 +125,8 @@ def test_가져올_곳_탭(): user = {"id": "7", "role": "USER", "company_id": 3} orphan = {"id": "8", "role": "USER", "company_id": None} cases = [ - [sys1, "3", True], [sys1, "1", False], [admin, "3", True], [master, "3", False], - [user, "3", True], [orphan, None, False], + [sys1, "3"], [sys1, "1"], [admin, "3"], [master, "3"], + [user, "3"], [orphan, None], ] # fmt: skip result = subprocess.run( ["node", "--experimental-strip-types", "--no-warnings", @@ -135,9 +135,9 @@ def test_가져올_곳_탭(): ) # fmt: skip assert result.returncode == 0, result.stderr assert json.loads(result.stdout) == [ - ["master", ["master", "company:3", "personal", "people", "practice"]], # 고객 회사 봄 + ["master", ["master", "company:3", "personal", "people"]], # 고객 회사 봄 ["master", ["master", "company", "personal", "people"]], # 자기 회사 · 일위대가 아님 - ["personal", ["master", "company", "personal", "people"]], # 회사 관리자 · 실무문서 없음 + ["personal", ["master", "company", "personal", "people"]], # 회사 관리자 ["personal", ["master", "company", "personal", "people"]], # is_master = 회사 관리자 ["personal", ["master", "company", "personal"]], # 일반 = 다른 사람 없음 ["master", ["master"]], # 회사 없음 diff --git a/resources/tester/test_m02_pair_copy.py b/resources/tester/test_m02_pair_copy.py index de344497f..4e3758d6e 100644 --- a/resources/tester/test_m02_pair_copy.py +++ b/resources/tester/test_m02_pair_copy.py @@ -68,19 +68,6 @@ def test_pair_없으면_예전처럼_한_칸만(world): # noqa: F811 assert "연결" not in _doc(personal, "area", "a")["시트"][0] -def test_실무문서는_일위대가만_면적_그대로(world): # noqa: F811 - system = world["system"] - _link_rv(system) - area = _doc(system, "area", "rv") - for kind in ("labor", "area"): - url = URL.format("system", kind, "rv") + "?from=UA000008&from_layer=practice&pair=true" - assert _as(world).post(url).status_code == 403 # 시스템 관리자만 - got = _as(world, "SYSTEM_ADMIN").post(url) - assert got.status_code == 200 and set(got.json()) == {"labor"} - assert len(_doc(system, "labor", "rv")["담은로직"]) == 14 - assert _doc(system, "area", "rv") == area - - def test_원본에_면적산출_없으면_일위대가만(world): # noqa: F811 system, personal = world["system"], world["personal"] (system / "area" / "rv.json").unlink() diff --git a/resources/tester/test_m02_practice.py b/resources/tester/test_m02_practice.py deleted file mode 100644 index fd303e582..000000000 --- a/resources/tester/test_m02_practice.py +++ /dev/null @@ -1,59 +0,0 @@ -"""실무문서(PLAN 39-2) — 옛 마스터 일위대가 조합 표 통째 · 시스템 관리자만 통째 지우기. - -못박는 것 - - `DELETE /api/m02/practice?판=` = 시스템 관리자만(그 밖 403) · 판 다르면 409 · - 구조물이 쓰는 조합이 있으면 409(아무것도 안 지움) · 지우면 줄 0 · 마스터 틀 검사 그대로. - - 화면(`Practice.ts`) = 마스터 층 · 시스템 관리자만 · 물음 두 번 · 상세 읽기만. -정본(마스터)은 읽기만 · 쓰기는 임시 사본. -""" - -from __future__ import annotations - -import json -from pathlib import Path - -from M01_MasterData import M01_MasterData_Store as m01 -from M01_MasterData import M01_MasterData_Store_Combo as combo_store -from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos -from resources.tester.test_m02_combo_layers import _as, _list, master_copy, world # noqa: F401 - -ROOT = Path(__file__).resolve().parents[2] -M02 = ROOT / "M02_MasterTemplete" - - -def _drop(client, 판: str, want: int) -> dict: - res = client.delete("/api/m02/practice", params={"판": 판}) - assert res.status_code == want, res.text - return res.json() - - -def test_통째_지우기_시스템_관리자만_판_쓰는_곳(world, master_copy, monkeypatch): # noqa: F811 - admin = _as(world, "SYSTEM_ADMIN") - before = _list(admin, "system") - assert before["줄"] # 사본에 옛 조합이 있음 - for role in ("USER", "ADMIN"): - _drop(_as(world, role), before["판"], 403) - admin = _as(world, "SYSTEM_ADMIN") - _drop(admin, "옛판", 409) - used = before["줄"][0]["키"] - monkeypatch.setattr(combo_store, "combo_users", lambda k: ["항목 하나"] if k == used else []) - got = _drop(admin, before["판"], 409) - assert "항목 하나" in json.dumps(got, ensure_ascii=False) - assert len(_list(admin, "system")["줄"]) == len(before["줄"]) # 아무것도 안 지움 - monkeypatch.setattr(combo_store, "combo_users", lambda k: []) - got = _drop(admin, before["판"], 200) - assert got["줄"] == [] and len(got["지운조합"]) == len(before["줄"]) - doc = json.loads((master_copy / combos.FILE).read_text(encoding="utf-8")) - assert doc["줄"] == [] and m01.cm.check_form(combos.FILE, doc) == [] - again = _list(admin, "system") - assert _drop(admin, again["판"], 200)["지운조합"] == [] # 빈 표 = 그대로 - - -def test_화면_시스템_관리자_마스터만_물음_두_번_읽기만(): - side = (M02 / "M02_MasterTemplete_UI_Side.ts").read_text("utf-8") - 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("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 ab7e412bc..f3233f657 100644 --- a/resources/tester/test_m02_precombo_scope.py +++ b/resources/tester/test_m02_precombo_scope.py @@ -155,7 +155,6 @@ def test_왼쪽_칸_차례_일위대가_구조물조합_구분선_불러오기_7 "createDivider()", "hopyo.title", "drawing,", - "practice ?", ] at = [order.index(m) for m in marks] assert at == sorted(at) diff --git a/resources/tester/test_m02_structure_options.py b/resources/tester/test_m02_structure_options.py index 4b91cad56..39b7902b6 100644 --- a/resources/tester/test_m02_structure_options.py +++ b/resources/tester/test_m02_structure_options.py @@ -109,12 +109,12 @@ def test_없는_종류_키_참거짓_아님은_422_낡은_판은_409(client) -> def test_좌측_줄은_구조물_집계_양식_아래_한_줄_같은_한_곳에서_지움() -> None: side = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts").read_text("utf-8") - # 716-3 — 옵션 줄은 M01 로 옮겨 좌측 칸에서 뺌(차례 = 일위대가 → 구조물 조합 → 불러오기 → 도면 양식 → 실무문서) + # 716-3 — 옵션 줄은 M01 로 옮겨 좌측 칸에서 뺌(차례 = 일위대가 → 구조물 조합 → 도면 양식) assert "options.title" not in side and "options.clear" not in side order = side.split('className: "m02-side__lists ui-sidebar-panel",')[1].split("});")[0] - assert order.index("summary") < order.index("drawing,") < order.index("practice") - # 실무문서를 열면 이 줄 고름 지움(자재 조합 · 「전체 항목」 없앰) - assert "onOpen: () => (practice?.clear(), paint())" in side + assert order.index("summary") < order.index("drawing,") + # 실무문서 칸은 걷음(732-1) + assert "practice" not in side assert "matset" not in side # 화면은 정의 한 벌로 읽고 씀(PLAN 48-2) · 보이기 체크는 정의 화면 옵션 줄 api = (ROOT / "M02_MasterTemplete/StructOptions/M02_SO_Api.ts").read_text("utf-8") diff --git a/resources/tester/test_m02_structure_panel.py b/resources/tester/test_m02_structure_panel.py index 337f7f6e9..ca6e145a3 100644 --- a/resources/tester/test_m02_structure_panel.py +++ b/resources/tester/test_m02_structure_panel.py @@ -194,7 +194,7 @@ def test_좌측_고름_표시는_열린_것_한_줄뿐(): # 다른 판도 같은 한 곳 — 열면 양식 · 열 줄 고름 지움 · 양식을 열면 실무자료 판 고름 지움 # (716-3 옵션 줄 뺌 — 좌측 판 = 실무자료) assert re.search(r"sel = null;\s*onOpened\?\.\(\);", _text(MAIN)) - assert "if (opt.getOpen()) practice?.clear();" in side.split("function paint(): void {")[1] + assert "practice" not in side # 실무문서 칸은 걷음(732-1) # 편집 칸이 열 때마다 좌측 `paint`(일위대가 화면 해시 맞춤은 39-2 에서 지움) assert "() => side.paint()," in _text(M02 / "M02_MasterTemplete_UI_Page.ts") diff --git a/resources/tester/test_ui_loading.py b/resources/tester/test_ui_loading.py index f29688c0f..edb67cc94 100644 --- a/resources/tester/test_ui_loading.py +++ b/resources/tester/test_ui_loading.py @@ -77,11 +77,6 @@ def test_실패_글과_다시_불러오기_M02_자리(): main = _text(M02 / "M02_MasterTemplete_UI_Main.ts") assert "const stop = showLoading(host);" in main assert "loadingError(why, () => void open(next))" in main - # 옛 조합 나무 · 상세(Labor.ts · Labor_Edit.ts)는 39 에 지움 — 그 자리는 실무문서(같은 꼴) - practice = _text(M02 / "M02_MasterTemplete_Practice.ts") - assert "const stop = showLoading(view);" in practice - assert "loadingError(why(error), () => void drawList())" in practice - assert "loadingError(why(error), () => void drawOne(key))" in practice cards = _text(M02 / "M02_MasterTemplete_ItemCards.ts") # 일위대가 카드는 뺌 — 몸(Labor_View)을 덮었다 지우면 빈 카드(40 D1) assert "cards.filter((c) => c !== laborCard).map((c) => c.shell.loading())" in cards diff --git a/ui_template/ui_template_locale_m2.ts b/ui_template/ui_template_locale_m2.ts index f69ab4cb0..8af169773 100644 --- a/ui_template/ui_template_locale_m2.ts +++ b/ui_template/ui_template_locale_m2.ts @@ -98,7 +98,6 @@ export const ui_locales_m2 = { M02_RootReadOnly: ["읽기", "read-only"], M02_RootNone: ["볼 수 있는 층이 없음", "Nothing to show"], M02_SourcePeople: ["다른 사람", "Others"], - M02_SourcePractice: ["실무문서", "Practice documents"], M02_PeopleEmpty: ["비어 있음", "empty"], M02_TreeEmptyEdit: [ "항목이 없음 — 빈 자리 오른쪽 클릭 또는 층 버튼 옆 「+」 로 폴더 · 항목을 더함", @@ -163,32 +162,6 @@ export const ui_locales_m2 = { "나무를 고침 — 같은 이름이 있어 「{value}」 로 저장", "Tree updated — name already taken, saved as “{value}”", ], - M02_PracticeTitle: ["실무문서", "Practice documents"], - M02_PracticeRow: ["실무문서 목록", "Practice document list"], - M02_PracticeCount: [ - "옛 마스터 일위대가 조합 {n}개 — 보기 · 복사 원본만(고칠 수 없음)", - "{n} old master unit-cost combos — view and copy source only", - ], - M02_PracticeEmpty: ["실무문서 없음", "No practice documents"], - M02_PracticeKey: ["키", "Key"], - M02_PracticeGroup: ["구분", "Group"], - M02_PracticeUnit: ["단위", "Unit"], - M02_PracticeLogics: ["로직 수", "Logics"], - M02_PracticeOrder: ["차례", "No."], - M02_PracticeMemo: ["메모", "Memo"], - M02_PracticeNoLogic: ["단가산출 기초 없음", "No logics"], - M02_PracticeBack: ["목록", "List"], - M02_PracticeDrop: ["전체 삭제", "Delete all"], - M02_PracticeDropAsk: ["실무문서 {n}개를 모두 지울까요?", "Delete all {n} practice documents?"], - M02_PracticeDropAsk2: [ - "되돌릴 수 없음 — 정말 통째로 지울까요?", - "This cannot be undone — really delete all?", - ], - M02_PracticeDropped: ["실무문서 {n}개를 지움", "Deleted {n} practice documents"], - M02_PracticeStale: [ - "그 사이 실무문서가 바뀜 — 목록을 다시 받음", - "Practice documents changed — reloaded", - ], M02_HopyoImport: ["일위대가 불러오기", "Import unit-price sheets"], B01_Dashboard_CardPrecombo: ["일위대가", "Unit-cost items"], M02_PrecomboTitle: ["일위대가", "Unit-cost items"],