From e313a3f6045a9d54276cb5987a8034711f7af9b7 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 18:32:57 +0900 Subject: [PATCH 1/3] =?UTF-8?q?chore(sync):=20=EA=B3=B5=EC=9C=A0=20docs=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EB=8B=B4=EC=9D=8C=20=E2=80=94=20=EA=B8=B0?= =?UTF-8?q?=EC=A4=80=20=EC=BB=A4=EB=B0=8B=EA=B3=BC=20=EA=B0=99=EC=9D=80=20?= =?UTF-8?q?=EB=82=B4=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- docs/raw/사용자요구/2026-10-05_수정요구.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index 023a682e3..c8dbf6477 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -72,3 +72,19 @@ 추가로 원문에서 표는 스크롤로 매우 낮은 공간만 보이고 스크롤 표현되어서 보기가 힘듬. 그냥 표가 길더라도 보여줄것. 어차피 상세페이지와 원문의 우측 패널의 영역은 별도의 스크롤 영역이라 무관함. 원문은 M01과 M02에 표현되고 있는데 일원화 할것. M02가 더 가독성이 좋음. + +## 일위대가 조합 · 면적산출 알약 (10-5 · 원문 그대로) + +다시 돌아가서 M02 페이지에 +1-1. 구조물 조합의 면적산출에서 도면으로 보내진 셀의 영역을 표현이 되게하기 +1-2. 구조물 조합의 면적산출에서 알약을 선택을 했을때 셀이 반짝임은 있지만 반대로 셀을 선택했을때는 잘 안보임. +1-3. 구조물 조합의 면적산출에서 일위대가의 로직을 삭제했을때 기 지정된 알약이 잔존하고 있음. 삭제가 힘들다면 느낌표라도 보여지게 하고 삭제가능한지 검토 +2. 원문 표기의 '산림품셈 3-4-2 1' 항목 확인 +3. 구조물 조합의 일위대가 상세 화면에서 단가산출 로직에 들어가는 로직항목들은 단가산출 로직 뿐만아니라 단일 단가산출 로직을 일위대가로 승격시킨 선택지와 다른 일위대가로 만들어진 조합도 선택이 되어야함. +4. 방법 : 구조물 조합 컨테이너 하단에 일위대가 조합 컨테이너를 추가하고 여기에 마스터, 회사, 개인의 권한을 구현, 이때는 두가지의 선택지가 있음. 하나는 일위대가로 승격시킨 단가산출로직의 리스트와 일위대가에 사용하기 전 사전 조합으로 만들어진 조합으로 구분함. +5. 단가산출 승격은 단가산출 로직의 항목들을 검색하여 선택하면 일위대가 형태로 저장이 됨. (도면 X, 면적산출 X) +6. 일위대가 사전 조합은 단가산출 승격 + 다른 일위대가의 조합으로 그룹(도면 X, 면적산출 X) +7. 일위대가 조합은 구조물 조합의 일위대가 상세화면에서 선택할 수 있어야함. +8. 구조물 조합의 일위대가 상세화면에서의 로직테이블은 일위대가 목록표임. 여기서 행을 눌렀을때 하단에 상세 내역이 보였으면 함. 상세내역은 M01페이지의 단가산출 로직을 보면 시험계산 컨테이너 내부에 기준단위에 대한 테이블 리스트가 있음. 이걸 B02 구조물 조합의 일위대가 조합에 맞게 넣어주면 됨. 행을 선택했을때만 보여주면 되고 다른 행을 선택하면 닫히면 됨. +9. 구조물 조합의 일위대가 상세화면에 단가산출 로직 테이블의 이름 좌측 열을 추가하고 구분을 추가함. 구분은 출처로 단가산출인지, 일위대가 조합인지, 단가산출을 승격한 정보인지 표기 항목은 4음절로 표기, +일단 상세히 반영 가능성에 대해 상세히 검토후 계획서에 반영 From b1a5f38a1655bca483b02bbdee9bc52842985718 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 18:42:50 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat(M02):=2050-3=E2=91=A2=20=EC=9D=BC?= =?UTF-8?q?=EC=9C=84=EB=8C=80=EA=B0=80=20=EC=A1=B0=ED=95=A9=20=EC=84=9C?= =?UTF-8?q?=EA=B3=A0=20=ED=99=94=EB=A9=B4=20=ED=8B=80=20=E2=80=94=20?= =?UTF-8?q?=EC=A2=8C=EC=B8=A1=20=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=20?= =?UTF-8?q?=C2=B7=20=EB=8B=A8=EA=B0=80=EC=8A=B9=EA=B2=A9=20=C2=B7=20?= =?UTF-8?q?=EC=82=AC=EC=A0=84=EC=A1=B0=ED=95=A9=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=C2=B7=20=EC=83=81=EC=84=B8=20=EC=84=9C=EA=B3=A0=20=EB=AA=A8?= =?UTF-8?q?=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 구조물 조합 아래 「일위대가 조합」 컨테이너 · 층 단추(진입 open=일위대가조합) · 새로 · 복사 · 지우기 - 단가승격 새로 = 로직 고르기 → SP- 키 저장 · 사전조합 새로 = 이름만 → SC- 키 - 상세 = 일위대가 상세 서고 모드(이름 칸 · 로직 표 · 원문만 · 자재 · 면적 · 다이어그램 없음) - 낡은 글 찾기 시험 셋을 지금 코드에 맞춤 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- .../M02_MasterTemplete_Api_Fetch.ts | 5 +- .../M02_MasterTemplete_Labor_Detail.ts | 57 ++++- .../M02_MasterTemplete_Labor_Text.ts | 1 + .../M02_MasterTemplete_Layer.ts | 4 +- .../M02_MasterTemplete_UI_Main.ts | 20 +- .../M02_MasterTemplete_UI_Page.ts | 6 +- .../M02_MasterTemplete_UI_Side.ts | 26 +- .../M02_MasterTemplete_UI_Side_Precombo.ts | 240 ++++++++++++++++++ .../M02_MasterTemplete_UI_Style.css | 14 + resources/tester/test_m02_card_shell.py | 4 +- resources/tester/test_m02_labor_tree.py | 6 +- resources/tester/test_m02_layer_screen.py | 2 +- 12 files changed, 350 insertions(+), 35 deletions(-) create mode 100644 M02_MasterTemplete/M02_MasterTemplete_UI_Side_Precombo.ts diff --git a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts index e0569ac5c..a56806e1f 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts @@ -9,8 +9,9 @@ import { API_BASE_URL } from "@config/config_frontend"; export type Layer = "system" | "company" | "personal" | "project"; /** 서버 종류 — 화면의 「표 양식」 = table · 「도면 양식」 = drawing · 「구조물 도면」 = structure · * 「면적산출」 = area(구조물집계표 열마다 한 통합문서) · - * 「일위대가」 = labor(열마다 문서 · 39장) */ -export type Kind = "table" | "drawing" | "structure" | "area" | "labor"; + * 「일위대가」 = labor(열마다 문서 · 39장) · + * 「단가승격」 = promote · 「사전조합」 = precombo(일위대가 조합 서고 · 50-3 · 키가 이름) */ +export type Kind = "table" | "drawing" | "structure" | "area" | "labor" | "promote" | "precombo"; export interface TemplateInfo { 종류: Kind; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index 0e34d21ee..c3069f8b5 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -7,6 +7,8 @@ * · 금액 = 서버 calc(기준 ③ · 최신 응답만 · 저장 안 함) · 고칠 때마다 뒤에서 다시 셈 * · 같은 로직 두 번 · 같은 자재 두 번 막음 · 빈 일위대가도 저장 됨 * · 오른쪽 「원문」 접는 패널(47-4 · 상세만 · 공용 `ui_template_side_panels`) = 고른 줄 로직의 출처 원문 + * · 서고 모드(50-3 · `mode`) = 단가승격 · 사전조합 — 이름 칸 · 단가산출 로직 표 · 원문만(자재 · 면적 · 다이어그램 없음) · + * 승격은 로직 한 줄(입력값 고정 · 더하기 · 순서 · 수량 없음) * ========================================================================== */ import { createButton, createInputField, el, showToast } from "@ui/ui_template_elements"; @@ -64,6 +66,8 @@ export interface LaborDetailOptions { /** 구조물 조합 카드 갈래(43-4) — 단가산출 로직 표 + 자재목록만 · 위로 · 아래로 · 로직 추가 · * 자재 추가 · 정보(단위 · 본뜬) · 다이어그램 없음 · 옵션 · 삭제 · 줄 · 수량 모달은 상세와 같음 */ card?: boolean; + /** 일위대가 조합 서고(50-3) — 단가승격 · 사전조합 문서(`column` = 키) */ + mode?: "promote" | "precombo"; } export interface LaborDetailHandle { @@ -82,6 +86,8 @@ export function mountLaborDetail( const cur = normalizeLabor(doc, opt.column); const saved = JSON.stringify(cur); const edit = !opt.readOnly; + const lib = opt.mode ?? null; + const promote = lib === "promote"; const logics = new Map(); let calc: LaborCalc | null = null; let fail: string | null = null; @@ -135,7 +141,7 @@ export function mountLaborDetail( const matOf = (code: string) => calc?.자재.find((m) => m.코드 === code); const drawDg = (): void => { - if (opt.card || !dgBox.open) return; // 카드 갈래엔 다이어그램 없음 + if (opt.card || lib || !dgBox.open) return; // 카드 갈래 · 서고 모드엔 다이어그램 없음 const data = buildLaborDiagram({ rows: cur.담은로직, logics: [...logics.values()], @@ -161,7 +167,7 @@ export function mountLaborDetail( logicTable( { rows: cur.담은로직, logics, calc, fail, cells: cells(), selected: picked }, // 읽기만이면 순서 · 삭제 없음([옵션] = 보기 모달은 둠) - edit + edit && !promote ? { open: pick, option: openRow, @@ -415,6 +421,16 @@ export function mountLaborDetail( ], }); + // 서고 모드 이름 칸(문서 `이름` · 목록에 보이는 이름) + const nameField = createInputField({ type: "text", label: tc("Lib_Name") }); + nameField.root.dataset.laborField = "이름"; + nameField.input.value = String((cur as { 이름?: unknown }).이름 ?? ""); + nameField.input.readOnly = !edit; + nameField.input.addEventListener("input", () => { + (cur as { 이름?: string }).이름 = nameField.input.value.trim(); + opt.onChanged(dirty()); + }); + const editor = (body: HTMLElement): HTMLElement => side ? el("div", { className: "ui-side-panels-row", children: [body, side.root] }) : body; host.replaceChildren( @@ -429,13 +445,17 @@ export function mountLaborDetail( el("section", { className: "m01lab__box m01-logic__section", children: [ - el("div", { className: "m01lab__info", children: [unit.root, origin.root] }), + el("div", { + className: "m01lab__info", + children: [...(lib ? [nameField.root] : []), unit.root, origin.root], + }), ], }), ]), - section(tc("Logics_Title"), opt.card ? null : addLogic, logicHost), - section(tc("Labor_Mats"), opt.card ? null : addMat, matHost), - ...(opt.card ? [] : [dgBox]), + // 승격 = 로직 한 줄(새로 만들 때 고름) · 사전조합 고르기는 고르기 창 탭(50-3 ④) + section(tc("Logics_Title"), opt.card || lib ? null : addLogic, logicHost), + ...(lib ? [] : [section(tc("Labor_Mats"), opt.card ? null : addMat, matHost)]), + ...(opt.card || lib ? [] : [dgBox]), ], }), ), @@ -444,19 +464,28 @@ export function mountLaborDetail( // 로직 입력 목록 · 면적산출 이은 칸(없으면 빈 것 — 새 구조물)을 받은 뒤 첫 계산 void Promise.all([ loadLogics(), - readTemplate(opt.layer, "area", opt.column, null).then( - (got) => { - area = { doc: got.문서 as Workbook, 판: got.판 ?? "" }; - links = areaLinks(got.문서); - }, - () => undefined, - ), + lib // 서고 모드엔 면적산출 없음 + ? undefined + : readTemplate(opt.layer, "area", opt.column, null).then( + (got) => { + area = { doc: got.문서 as Workbook, 판: got.판 ?? "" }; + links = areaLinks(got.문서); + }, + () => undefined, + ), ]).then(() => { if (alive) recalc(); }); return { - getDoc: () => cur, + // 서고 모드 = 문서 종류 그대로 · 열 · 자재 없음 + getDoc: () => { + if (!lib) return cur; + const out: Record = { ...cur, 종류: promote ? "단가승격" : "사전조합" }; + delete out.열; + delete out.자재; + return out as unknown as LaborDoc; + }, saveArea: async () => { if (!area || !areaDirty) return false; const info = await saveTemplate(opt.layer, "area", opt.column, null, area.판, area.doc); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts index db14ee6bc..16f8cb5c2 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts @@ -105,6 +105,7 @@ const TEXT = { In_Ok: ["확인", "OK"], In_Cancel: ["취소", "Cancel"], In_Close: ["닫기", "Close"], + Lib_Name: ["이름", "Name"], } as const satisfies Record; export type ComboTextKey = keyof typeof TEXT; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Layer.ts b/M02_MasterTemplete/M02_MasterTemplete_Layer.ts index 47fd72a35..43a7b6b86 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Layer.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Layer.ts @@ -16,7 +16,7 @@ import { copyItemToPersonal, fetchStructureCombos } from "./M02_MasterTemplete_A export type ManageLayer = "system" | "company" | "personal"; /** 해시 · 화면의 뿌리 이름 — 서버 층 이름은 `layerOfRoot` */ export type Root = "master" | "company" | "personal"; -export type OpenTarget = "구조물조합" | "옵션" | "도면"; +export type OpenTarget = "구조물조합" | "옵션" | "도면" | "일위대가조합"; export interface Entry { root: Root; @@ -27,7 +27,7 @@ export interface Entry { company: string | null; } -const OPENS: OpenTarget[] = ["구조물조합", "옵션", "도면"]; +const OPENS: OpenTarget[] = ["구조물조합", "옵션", "도면", "일위대가조합"]; export const layerOfRoot = (root: Root): ManageLayer => (root === "master" ? "system" : root); diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts index 37867bab2..6ebe70b4c 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -31,6 +31,7 @@ import { buildAreaPanel, type AreaPanel } from "./M02_MasterTemplete_AreaPanel"; import { createAreaToDrawing } from "./M02_MasterTemplete_AreaToDrawing"; import { detailHelp, HELP_TITLE } from "./M02_MasterTemplete_CardHelp"; import { helpParts } from "./M02_MasterTemplete_CardShell"; +import { isPrecombo } from "./M02_MasterTemplete_UI_Side_Precombo"; import type { SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet"; import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet_links"; export interface Selection { @@ -378,19 +379,24 @@ export function createMain( editor = { getDoc: () => comboMerge(loaded, shown.getDoc()), destroy: shown.destroy }; return; } - if (at.kind === "labor") { - // 일위대가 상세(39-3) — 구조물 열마다 하나 · 고침은 이 화면 [저장]으로 + if (at.kind === "labor" || isPrecombo(at.kind)) { + // 일위대가 상세(39-3) — 구조물 열마다 하나 · 고침은 이 화면 [저장]으로 · + // 단가승격 · 사전조합(50-3) = 같은 상세 모드(도면 · 면적 · 자재 없음 · 이름 = 키) const { mountLaborDetail } = await import("./M02_MasterTemplete_Labor_Detail"); + const mode = isPrecombo(at.kind) ? at.kind : undefined; const labor = mountLaborDetail(host, doc, { layer: at.layer, column: at.name, readOnly, onChanged: (d) => (dirty = d && !readOnly), - goArea: async () => { - if (!(await confirmLeave())) return false; // 취소 — 입력 모달 그대로 - void open({ ...at, kind: "area" }); - return true; - }, + goArea: mode + ? null + : async () => { + if (!(await confirmLeave())) return false; // 취소 — 입력 모달 그대로 + void open({ ...at, kind: "area" }); + return true; + }, + ...(mode ? { mode } : {}), }); // [저장] = 엑셀에서 고른 칸 연결(면적 문서) 먼저 · 그다음 일위대가(서버가 새 칸 값으로 계산값) editor = { diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts index f3a44130f..7eebb2002 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts @@ -105,13 +105,13 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise void): HTMLElement => + const layerBar = (open: OpenTarget, add: HTMLElement | null, reselect: () => void): HTMLElement => createLayerBar({ user, active: { root: entry.root, owner }, company, go: (next, nextOwner) => void go(next, nextOwner, open), - add, + ...(add ? { add } : {}), reselect, }); // 남의 회사 「개인」 — 고른 사람 줄 아래에 그 사람 나무(사람 줄 → 라이브러리 · 29-6) @@ -184,6 +184,8 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise void main.openTable(layer)), peopleTree("구조물조합", tree.root), ], + // 일위대가 조합(50-3) — 같은 층 단추 · 누르면 그 층 화면에서 이 컨테이너를 펼침 + precomboBar: [layerBar("일위대가조합", null, () => undefined)], }); const layout = el("div", { className: "ui-workflow-layout m02-master" }); diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts index 12561432e..0fa16606e 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts @@ -1,6 +1,7 @@ /* ============================================================================= * M02_MasterTemplete_UI_Side.ts - * 마스터 템플릿 좌측 패널 — 「구조물 조합」(층 단추 · 나무) · 「도면 양식」(새로 · 본떠 · 지우기). + * 마스터 템플릿 좌측 패널 — 「구조물 조합」(층 단추 · 나무) · 「일위대가 조합」(층 단추 · 단가승격 · 사전조합 · + * `UI_Side_Precombo` · 50-3) · 「도면 양식」(새로 · 본떠 · 지우기). * 서버 길은 계약 `6_계약.md`. * 구조물 도면 · 일위대가 · 면적산출 상세는 좌측 줄이 아니라 항목 카드 [수정]으로(PLAN 45-2). * 화면 이름 「구조물 조합」(PLAN 26-7a) — 파일 이름 · 양식 키 · API 길은 「구조물집계표」 그대로. @@ -31,6 +32,7 @@ import { STRUCTURE_TABLE } from "./M02_MasterTemplete_Structure"; import { buildOptionsSection } from "./StructOptions/M02_SO_Options"; import { buildPracticeSection } from "./M02_MasterTemplete_Practice"; import { canEdit, type Selection } from "./M02_MasterTemplete_UI_Main"; +import { buildPrecomboSection, PRECOMBO_TITLE } from "./M02_MasterTemplete_UI_Side_Precombo"; const KINDS: { kind: Kind; label: () => string }[] = [ { kind: "table", label: () => L("M02_KindTable") }, @@ -58,6 +60,8 @@ export interface SideOptions { showView: (label: string, node: HTMLElement) => (dirty: boolean) => void; /** 「구조물 조합」 줄 아래 — 층 단추 · 고른 층의 구조물 라이브러리 나무(PLAN 29-2 · 페이지가 만듦) */ tree?: HTMLElement[]; + /** 「일위대가 조합」 줄 아래 층 단추(50-3 · 페이지가 만듦) */ + precomboBar?: HTMLElement[]; } export interface SideHandle { @@ -66,7 +70,7 @@ export interface SideHandle { /** 고름 표시를 지금 열린 것 한 줄로 — 편집 칸이 무엇을 열든(카드 [도면 수정] 등) 부름 */ paint: () => void; /** 진입 해시 `open`(PLAN 27-1d) — 그 컨테이너를 펼침(옵션은 열기까지) · 옵션 · 도면은 시스템 층에만 */ - reveal: (target: "옵션" | "도면") => void; + reveal: (target: "옵션" | "도면" | "일위대가조합") => void; } /** 서버 이름 규칙과 같게 — 화면에서 먼저 막음 */ @@ -130,6 +134,17 @@ export function buildSide(opt: SideOptions): SideHandle { openOnExpand(summary, async () => { if (!isPicked("table", STRUCTURE_TABLE) && (await opt.confirmLeave())) opt.onOpen(null); }); + // 일위대가 조합 서고(50-3) — 구조물 조합 바로 아래 · 층마다(마스터 · 회사 · 개인) + const precombo = buildPrecomboSection({ + layer, + canEdit: canEdit(layer, opt.isAdmin), + bar: opt.precomboBar ?? [], + confirmLeave: opt.confirmLeave, + onOpen: opt.onOpen, + isOpen: (kind, name) => isPicked(kind, name), + refresh: () => refresh(), + }); + const precomboSec = section(PRECOMBO_TITLE, precombo.body); // 구조물 옵션 정의 · 도면 양식은 시스템 층(마스터 템플릿)에만 · 정의 고쳐 씀은 시스템 관리자만 · 다른 역할은 읽기만(PLAN 48-2) const extras = layer === "system" @@ -151,6 +166,7 @@ export function buildSide(opt: SideOptions): SideHandle { className: "m02-side__lists ui-sidebar-panel", children: [ summary, + precomboSec, ...(extras ? [extras.옵션, extras.도면].filter((s): s is HTMLElement => !!s) : []), ...(practice ? [section(practice.title, practice.body)] : []), ], @@ -205,6 +221,7 @@ export function buildSide(opt: SideOptions): SideHandle { * (양식이 열려 있으면 옵션 · 일위대가 조합 고름은 지움) */ function paint(): void { paintList(); + precombo.paint(items); paintButtons(); if (opt.getOpen()) options.clear(); if (opt.getOpen()) practice?.clear(); @@ -228,6 +245,7 @@ export function buildSide(opt: SideOptions): SideHandle { if (mine !== seq) return; paintList(); paintButtons(); + precombo.paint(items); } /* --- 시스템 층 새로 · 본떠 · 지우기(도면 양식만 — 구조물 도면은 집계표 열이 정함) --- */ @@ -307,8 +325,8 @@ export function buildSide(opt: SideOptions): SideHandle { } } - const reveal = (target: "옵션" | "도면"): void => { - const sec = extras?.[target]; + const reveal = (target: "옵션" | "도면" | "일위대가조합"): void => { + const sec = target === "일위대가조합" ? precomboSec : extras?.[target]; if (!sec) return; // 옵션 보이기는 시스템 관리자만 sec.classList.remove("is-collapsed"); if (target === "옵션") options.body.querySelector("button")?.click(); diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Precombo.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Precombo.ts new file mode 100644 index 000000000..67c0f220f --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Precombo.ts @@ -0,0 +1,240 @@ +/* ============================================================================= + * M02_MasterTemplete_UI_Side_Precombo.ts + * 좌측 「일위대가 조합」 컨테이너(50-3 ③) — 구조물 조합 아래 · 층 단추(페이지가 만듦) · + * 하위 둘 「단가승격」(promote · 단가산출 로직 하나를 입력값 고정해 일위대가 꼴로) · + * 「사전조합」(precombo · 승격 + 다른 사전조합 묶음) — 목록 · 새로 · 복사 · 지우기. + * 상세는 편집 칸(UI_Main 종류 promote · precombo = 일위대가 상세 모드 · 도면 · 면적 · 자재 없음). + * 파일 이름 = 키(SP- · SC-) · 화면 이름 = 문서 `이름`(목록 `제목`) · 쓰는 곳 있으면 서버가 지우기를 막음(409 까닭). + * ========================================================================== */ + +import { + createButton, + createInputField, + el, + showConfirmDialog, + showToast, +} from "@ui/ui_template_elements"; +import { t as L } from "@ui/ui_template_locale"; +import { openModal } from "@ui/ui_template_modal"; +import { + deleteLayerTemplate, + deleteTemplate, + getActiveOwner, + readTemplate, + saveTemplate, + type Kind, + type Layer, + type TemplateInfo, +} from "./M02_MasterTemplete_Api_Fetch"; +import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick"; +import type { Selection } from "./M02_MasterTemplete_UI_Main"; + +export type PrecomboKind = Extract; + +/** 종류 · 키 머리 · 화면 이름(= 문서 `종류` 글) */ +const PARTS: { kind: PrecomboKind; prefix: string; label: string }[] = [ + { kind: "promote", prefix: "SP", label: "단가승격" }, + { kind: "precombo", prefix: "SC", label: "사전조합" }, +]; +export const PRECOMBO_TITLE = "일위대가 조합"; +export const isPrecombo = (kind: Kind): kind is PrecomboKind => + kind === "promote" || kind === "precombo"; + +/** 목록 줄 — 서버가 문서 이름을 `제목` 으로 실어 보냄(없으면 키만) */ +type Info = TemplateInfo & { 제목?: string }; +export const precomboLabel = (info: Info): string => + info.제목 ? `${info.이름} ${info.제목}` : info.이름; + +/** 그 층 다음 키 — 지금 목록의 가장 큰 수 + 1(서버가 같은 키면 409 로 막음) */ +const nextKey = (prefix: string, items: Info[]): string => { + const top = items.reduce((max, i) => { + const m = new RegExp(`^${prefix}-(\\d+)$`).exec(i.이름); + return m ? Math.max(max, Number(m[1])) : max; + }, 0); + return `${prefix}-${String(top + 1).padStart(6, "0")}`; +}; + +export interface PrecomboOptions { + layer: Layer; + canEdit: boolean; + /** 층 단추 줄(페이지가 만듦 · 누르면 그 층 화면 + 이 컨테이너 펼침) */ + bar: HTMLElement[]; + confirmLeave: () => Promise; + onOpen: (sel: Selection | null) => void; + isOpen: (kind: Kind, name: string) => boolean; + /** 목록 다시 받음(좌측 전체) — 받은 뒤 `paint` 가 불림 */ + refresh: () => Promise; +} + +export interface PrecomboHandle { + body: HTMLElement; + /** 좌측이 받은 층 목록 중 승격 · 사전조합만 그림 */ + paint: (items: TemplateInfo[]) => void; +} + +const why = (error: unknown): string => (error instanceof Error ? error.message : ""); +const failed = (error: unknown): void => + showToast(L("M02_ActionFailed").replace("{value}", why(error)), "error"); + +export function buildPrecomboSection(opt: PrecomboOptions): PrecomboHandle { + let items: Info[] = []; + const lists = new Map(); + const buttons = new Map(); + const mine = (kind: PrecomboKind): Info[] => items.filter((i) => i.종류 === kind); + const current = (kind: PrecomboKind): Info | undefined => + mine(kind).find((i) => opt.isOpen(kind, i.이름)); + + const select = async (info: Info): Promise => { + if (!opt.isOpen(info.종류, info.이름) && !(await opt.confirmLeave())) return; + opt.onOpen({ + layer: opt.layer, + projectId: null, + kind: info.종류, + name: info.이름, + label: precomboLabel(info), + }); + }; + + /** 새로 · 복사 — 다음 키로 저장 후 엶 */ + const create = async (kind: PrecomboKind, doc: Record): Promise => { + const key = nextKey(PARTS.find((p) => p.kind === kind)!.prefix, mine(kind)); + await saveTemplate(opt.layer, kind, key, null, "", doc); + showToast(L("M02_Created"), "success"); + await opt.refresh(); + const info = mine(kind).find((i) => i.이름 === key); + if (info) await select(info); + }; + + const nameDialog = (title: string, start: string, onOk: (name: string) => Promise) => { + const name = createInputField({ label: L("M02_Name"), placeholder: L("M02_Name") }); + name.input.value = start; + openModal({ + title, + closeLabel: L("M02_Close"), + dialogClass: "m02-modal", + mount: (body, close) => { + const ok = createButton({ label: L("M02_Create"), variant: "filled" }); + ok.addEventListener("click", async () => { + const text = name.input.value.trim(); + if (!text) return showToast(L("M02_NameNeeded"), "error"); + ok.disabled = true; // 연달아 눌러도 한 번만 + try { + await onOk(text); + close(); + } catch (error) { + failed(error); + } finally { + ok.disabled = false; + } + }); + name.input.addEventListener("keydown", (event) => { + if (event.key === "Enter" && !event.isComposing) ok.click(); + }); + body.append(name.root, ok); + }, + }); + }; + + /** 단가승격 새로 = 단가산출 로직 고르기 → 이름 · 단위는 로직 것(입력값은 상세에서 고정) */ + const newPromote = (): void => + openLogicPicker((x) => { + void opt.confirmLeave().then((go) => + !go + ? undefined + : create("promote", { + 종류: "단가승격", + 이름: x.이름, + 단위: x.결과단위 || null, + 담은로직: [{ 로직: x.키, 입력값: {} }], + }).catch(failed), + ); + }); + /** 사전조합 새로 = 이름만 · 빈 묶음(승격 · 사전조합은 상세 [로직 추가]로) */ + const newPrecombo = (): void => + nameDialog(L("M02_NewTitle"), "", async (name) => { + if (!(await opt.confirmLeave())) return; + await create("precombo", { 종류: "사전조합", 이름: name, 단위: null, 담은로직: [] }); + }); + + /** 복사 = 같은 층 새 키 · 이름 「(2)」 · 본뜬 = 원래 키 */ + const copy = (kind: PrecomboKind): void => { + const from = current(kind); + if (!from) return; + nameDialog(L("M02_CopyTitle"), `${from.제목 ?? from.이름} (2)`, async (name) => { + if (!(await opt.confirmLeave())) return; + const got = await readTemplate(opt.layer, kind, from.이름, null); + const doc: Record = { ...(got.문서 as object), 이름: name }; + doc.본뜬 = { 층: opt.layer, 주인: getActiveOwner(), 키: from.이름 }; + delete doc.판; + await create(kind, doc); + }); + }; + + const remove = async (kind: PrecomboKind): Promise => { + await opt.refresh(); // 저장 직후여도 최신 판으로 + const info = current(kind); + if (!info) return; + const name = precomboLabel(info); + if (!(await showConfirmDialog(L("M02_DeleteConfirm").replace("{value}", name)))) return; + try { + if (opt.layer === "system") await deleteTemplate(kind, info.이름, info.판); + else await deleteLayerTemplate(opt.layer, kind, info.이름, info.판); + showToast(L("M02_Deleted"), "success"); + opt.onOpen(null); + await opt.refresh(); + } catch (error) { + // 쓰는 곳이 있으면 서버가 막음 — 까닭(쓰는 곳 이름) 그대로 + showToast(why(error) || L("M02_ActionFailed").replace("{value}", ""), "error"); + } + }; + + const block = (part: (typeof PARTS)[number]): HTMLElement => { + const list = el("div", { className: "m02-side__items ui-sidebar-list" }); + lists.set(part.kind, list); + const row: HTMLElement[] = []; + if (opt.canEdit) { + const btnNew = createButton({ label: L("M02_New"), variant: "filled", size: "sm" }); + const btnCopy = createButton({ label: L("M02_Copy"), variant: "ghost", size: "sm" }); + const del = createButton({ label: L("M02_Delete"), variant: "danger", size: "sm" }); + buttons.set(part.kind, { copy: btnCopy, del }); + btnNew.addEventListener("click", part.kind === "promote" ? newPromote : newPrecombo); + btnCopy.addEventListener("click", () => copy(part.kind)); + del.addEventListener("click", () => void remove(part.kind)); + row.push(el("div", { className: "m02-side__row", children: [btnNew, btnCopy, del] })); + } + return el("div", { + className: "m02-side__sub", + attrs: { "data-precombo": part.kind }, + children: [el("h4", { className: "m02-side__subtitle", text: part.label }), ...row, list], + }); + }; + + const body = el("div", { + className: "m02-side__items ui-sidebar-list", + children: [...opt.bar, ...PARTS.map(block)], + }); + + function paint(all: TemplateInfo[]): void { + items = all.filter((i) => isPrecombo(i.종류)); + for (const part of PARTS) { + const found = mine(part.kind); + lists.get(part.kind)?.replaceChildren( + ...(found.length + ? found.map((info) => { + const b = el("button", { + className: `m02-side__item ui-sidebar-row${opt.isOpen(part.kind, info.이름) ? " is-active" : ""}`, + text: precomboLabel(info), + attrs: { type: "button", title: info.수정일, "data-precombo-key": info.이름 }, + }); + b.addEventListener("click", () => void select(info)); + return b; + }) + : [el("p", { className: "m02-side__empty", text: L("M02_NoTemplates") })]), + ); + const b = buttons.get(part.kind); + if (b) b.copy.disabled = b.del.disabled = !current(part.kind); + } + } + + return { body, paint }; +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css index 6791fc2a9..c232dcff0 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css @@ -56,6 +56,20 @@ gap: var(--spacing-4); } +/* 일위대가 조합 서고(50-3) — 하위 단가승격 · 사전조합 */ +.m02-side__sub { + display: flex; + flex-direction: column; + gap: var(--spacing-4); + margin-top: var(--spacing-8); +} + +.m02-side__subtitle { + margin: 0; + color: var(--color-text-secondary); + font-size: var(--text-body-sm); +} + .m02-main { display: flex; flex-direction: column; diff --git a/resources/tester/test_m02_card_shell.py b/resources/tester/test_m02_card_shell.py index 0cd36812d..57faaba79 100644 --- a/resources/tester/test_m02_card_shell.py +++ b/resources/tester/test_m02_card_shell.py @@ -79,7 +79,9 @@ def test_일위대가_카드는_상세_카드_갈래(): assert "Labor_View" not in cards # 한 줄 표(40-2) 안 씀 detail = _text("M02_MasterTemplete_Labor_Detail.ts") assert "...(opt.card ? {} : { move })" in detail # 순서 단추 없음 · 빼기는 그대로 - assert "opt.card ? null : addLogic" in detail and "...(opt.card ? [] : [dgBox])," in detail + # 서고 모드(50-3 · lib)도 더하기 · 다이어그램 없음 + assert "opt.card || lib ? null : addLogic" in detail + assert "...(opt.card || lib ? [] : [dgBox])," in detail assert "opt.card ? null : addMat" in detail # 49-6 — 카드엔 [자재 추가] 없음 · 상세는 그대로 rows = _text("M02_MasterTemplete_Labor_Rows.ts") assert "const tools = !!(move || remove || option);" in rows and "compactTable" not in rows diff --git a/resources/tester/test_m02_labor_tree.py b/resources/tester/test_m02_labor_tree.py index 4675902fc..ff2f7bb97 100644 --- a/resources/tester/test_m02_labor_tree.py +++ b/resources/tester/test_m02_labor_tree.py @@ -46,7 +46,7 @@ def test_좌측_하위_컨테이너와_전체_항목_길_없음(): assert "buildAllItems" not in page and "onAll" not in page and "ALL_TITLE" not in page assert "buildAllItems" not in _text("M02_MasterTemplete_ItemList.ts") api = _text("M02_MasterTemplete_Api_Fetch.ts") - assert '| "labor";' in api and "일위대가번: string;" in api + assert '| "labor" |' in api and "일위대가번: string;" in api locale = (ROOT / "ui_template" / "ui_template_locale_m2.ts").read_text("utf-8") for gone in ("M02_ItemsAll", "M02_KindLabor", "M02_KindStructure", "M02_KindArea"): assert gone not in locale, gone @@ -57,7 +57,9 @@ def test_좌측_하위_컨테이너와_전체_항목_길_없음(): def test_층_단추는_구조물_조합_한_벌(): page = _text("M02_MasterTemplete_UI_Page.ts") - assert page.count('layerBar("') == 1 # 구조물 조합 줄 아래 하나 + # 구조물 조합 줄 아래 하나 + 일위대가 조합 서고(50-3) 하나 + assert page.count('layerBar("') == 2 + assert 'layerBar("일위대가조합", null, () => undefined)' in page assert 'layerBar("구조물조합", tree.add, () => void main.openTable(layer)),' in page diff --git a/resources/tester/test_m02_layer_screen.py b/resources/tester/test_m02_layer_screen.py index 3892125c6..963adb174 100644 --- a/resources/tester/test_m02_layer_screen.py +++ b/resources/tester/test_m02_layer_screen.py @@ -87,7 +87,7 @@ def test_주소_인자를_라우트에서_뗌(): # 진입 해시 `?open=&root=&owner=`(PLAN 27-1d) · 옛 `?layer=` 도 root 로 assert 'query.get("root") ?? query.get("layer")' in layer assert 'raw === "company" || raw === "personal" ? raw : "master"' in layer - assert '["구조물조합", "옵션", "도면"]' in layer # 일위대가 = 구조물 조합 안(44-7) + assert '["구조물조합", "옵션", "도면", "일위대가조합"]' in layer # 일위대가 조합 서고 = 50-3 picker = _text(ROOT / "ui_template" / "ui_template_item_picker.ts") assert 'open: "구조물조합", root: "personal"' in picker and "layer=personal" not in picker From 027ad3da9833ea3d6922db5c918dbdd106b7dd1e Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 18:45:01 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat(M02):=2050-3=E2=91=A2=20=EC=84=9C?= =?UTF-8?q?=EA=B3=A0=20=EC=83=88=EB=A1=9C=20=C2=B7=20=EB=B3=B5=EC=82=AC?= =?UTF-8?q?=EB=A5=BC=20=EC=84=9C=EB=B2=84=20=ED=82=A4=20=EA=B8=B8=EB=A1=9C?= =?UTF-8?q?=20=E2=80=94=20POST=20/layers/{=EC=B8=B5}/library/{=EC=A2=85?= =?UTF-8?q?=EB=A5=98}=20=C2=B7=20=EC=B0=B8=EC=A1=B0=20=EC=A4=84=20?= =?UTF-8?q?=EB=B3=B4=EC=A1=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새로 = 서버가 그 층 다음 SP · SC 키 · 복사 = 본뜬 원본 → 고른 이름으로 고쳐 씀 - 일위대가 문서 줄의 참조 {층, 주인, 키} 를 화면 캐시에서 안 버림 · 로직 입력 받기에서 참조 줄 건너뜀 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- .../M02_MasterTemplete_Api_Fetch.ts | 13 +++++ .../M02_MasterTemplete_Labor_Api.ts | 3 + .../M02_MasterTemplete_Labor_Detail.ts | 2 +- .../M02_MasterTemplete_UI_Side_Precombo.ts | 56 +++++++++---------- 4 files changed, 45 insertions(+), 29 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts index a56806e1f..a76619d33 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts @@ -126,6 +126,19 @@ export const deleteLayerTemplate = ( method: "DELETE", }); +/** 서고 새로 · 복사(50-3) — 새로 = `문서` · 복사 = `본뜬` {층, 주인, 키} · 키는 서버가 줌(답 `이름`) */ +export type LibraryBody = + { 문서: Record } | { 본뜬: { 층: Layer; 주인: string | null; 키: string } }; +export const newLibrary = ( + layer: Layer, + kind: "promote" | "precombo", + body: LibraryBody, +): Promise => + call(owned(layer, `/layers/${layer}/library/${kind}`), { + method: "POST", + body: JSON.stringify(body), + }); + /* --- 구조물 도면(종류 structure · 이름 = 구조물집계표 열 id) --- * 읽기 · 저장 · 지우기는 위 readTemplate · saveTemplate · deleteTemplate 에 "structure" */ diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts index 5e7b50172..21cfd6835 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts @@ -126,6 +126,8 @@ export interface LaborRow { 메모: string | null; 입력값: Record; 수량: LaborQty; + /** 서고 참조 줄(50-3 · 사전조합 · 로직 없음) — 단가승격 · 사전조합 {층, 주인, 키} */ + 참조?: { 층: string; 주인: string | null; 키: string }; } export interface LaborMat { 코드: string; @@ -159,6 +161,7 @@ export function normalizeLabor(doc: unknown, column: string): LaborDoc { 메모: r.메모 ?? null, 입력값: r.입력값 ?? {}, 수량: r.수량 ?? null, + ...(r.참조 ? { 참조: r.참조 } : {}), })), 자재: (d.자재 ?? []).map((m) => ({ 코드: m.코드, diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index c3069f8b5..8176a4bed 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -283,7 +283,7 @@ export function mountLaborDetail( /** 아직 안 받은 로직 입력 목록만 받음 */ const loadLogics = async (): Promise => { - const want = cur.담은로직.map((r) => r.로직).filter((k) => !logics.has(k)); + const want = cur.담은로직.map((r) => r.로직).filter((k) => k && !logics.has(k)); // 참조 줄 = 로직 없음 if (!want.length) return; try { const got = await fetchLogicInputs(want); diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Precombo.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Precombo.ts index 67c0f220f..8773635dd 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Precombo.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Precombo.ts @@ -20,10 +20,12 @@ import { deleteLayerTemplate, deleteTemplate, getActiveOwner, + newLibrary, readTemplate, saveTemplate, type Kind, type Layer, + type LibraryBody, type TemplateInfo, } from "./M02_MasterTemplete_Api_Fetch"; import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick"; @@ -31,10 +33,10 @@ import type { Selection } from "./M02_MasterTemplete_UI_Main"; export type PrecomboKind = Extract; -/** 종류 · 키 머리 · 화면 이름(= 문서 `종류` 글) */ -const PARTS: { kind: PrecomboKind; prefix: string; label: string }[] = [ - { kind: "promote", prefix: "SP", label: "단가승격" }, - { kind: "precombo", prefix: "SC", label: "사전조합" }, +/** 종류 · 화면 이름(= 문서 `종류` 글 · 키 SP- · SC- 는 서버가 줌) */ +const PARTS: { kind: PrecomboKind; label: string }[] = [ + { kind: "promote", label: "단가승격" }, + { kind: "precombo", label: "사전조합" }, ]; export const PRECOMBO_TITLE = "일위대가 조합"; export const isPrecombo = (kind: Kind): kind is PrecomboKind => @@ -45,15 +47,6 @@ type Info = TemplateInfo & { 제목?: string }; export const precomboLabel = (info: Info): string => info.제목 ? `${info.이름} ${info.제목}` : info.이름; -/** 그 층 다음 키 — 지금 목록의 가장 큰 수 + 1(서버가 같은 키면 409 로 막음) */ -const nextKey = (prefix: string, items: Info[]): string => { - const top = items.reduce((max, i) => { - const m = new RegExp(`^${prefix}-(\\d+)$`).exec(i.이름); - return m ? Math.max(max, Number(m[1])) : max; - }, 0); - return `${prefix}-${String(top + 1).padStart(6, "0")}`; -}; - export interface PrecomboOptions { layer: Layer; canEdit: boolean; @@ -95,10 +88,9 @@ export function buildPrecomboSection(opt: PrecomboOptions): PrecomboHandle { }); }; - /** 새로 · 복사 — 다음 키로 저장 후 엶 */ - const create = async (kind: PrecomboKind, doc: Record): Promise => { - const key = nextKey(PARTS.find((p) => p.kind === kind)!.prefix, mine(kind)); - await saveTemplate(opt.layer, kind, key, null, "", doc); + /** 새로 — 서버가 그 층 다음 키로 씀 → 엶 */ + const create = async (kind: PrecomboKind, body: LibraryBody): Promise => { + const key = (await newLibrary(opt.layer, kind, body)).이름; showToast(L("M02_Created"), "success"); await opt.refresh(); const info = mine(kind).find((i) => i.이름 === key); @@ -142,10 +134,11 @@ export function buildPrecomboSection(opt: PrecomboOptions): PrecomboHandle { !go ? undefined : create("promote", { - 종류: "단가승격", - 이름: x.이름, - 단위: x.결과단위 || null, - 담은로직: [{ 로직: x.키, 입력값: {} }], + 문서: { + 이름: x.이름, + 단위: x.결과단위 || null, + 담은로직: [{ 로직: x.키, 입력값: {} }], + }, }).catch(failed), ); }); @@ -153,20 +146,27 @@ export function buildPrecomboSection(opt: PrecomboOptions): PrecomboHandle { const newPrecombo = (): void => nameDialog(L("M02_NewTitle"), "", async (name) => { if (!(await opt.confirmLeave())) return; - await create("precombo", { 종류: "사전조합", 이름: name, 단위: null, 담은로직: [] }); + await create("precombo", { 문서: { 이름: name, 단위: null, 담은로직: [] } }); }); - /** 복사 = 같은 층 새 키 · 이름 「(2)」 · 본뜬 = 원래 키 */ + /** 복사 = 같은 층 새 키(서버 · 본뜬 = 원래 키) → 고른 이름으로 고쳐 씀 */ const copy = (kind: PrecomboKind): void => { const from = current(kind); if (!from) return; nameDialog(L("M02_CopyTitle"), `${from.제목 ?? from.이름} (2)`, async (name) => { if (!(await opt.confirmLeave())) return; - const got = await readTemplate(opt.layer, kind, from.이름, null); - const doc: Record = { ...(got.문서 as object), 이름: name }; - doc.본뜬 = { 층: opt.layer, 주인: getActiveOwner(), 키: from.이름 }; - delete doc.판; - await create(kind, doc); + const made = await newLibrary(opt.layer, kind, { + 본뜬: { 층: opt.layer, 주인: getActiveOwner(), 키: from.이름 }, + }); + const got = await readTemplate(opt.layer, kind, made.이름, null); + await saveTemplate(opt.layer, kind, made.이름, null, made.판, { + ...(got.문서 as object), + 이름: name, + }); + showToast(L("M02_Created"), "success"); + await opt.refresh(); + const info = mine(kind).find((i) => i.이름 === made.이름); + if (info) await select(info); }); };