diff --git a/A00_Common/b_page_state.ts b/A00_Common/b_page_state.ts index 9f9e3ae48..3e3f493ea 100644 --- a/A00_Common/b_page_state.ts +++ b/A00_Common/b_page_state.ts @@ -59,7 +59,7 @@ export const STATE_REGISTRY = { "drainage-width": { bucket: "pref", scope: "global", legacy: () => "b05-route-drainage-width" }, /** 종단도 오른쪽 판(배수유역도) 켬 · 끔 — 두 보기가 같은 값(계획서 24-2c · 24-2e). */ "drainage-visible": { bucket: "pref", scope: "global" }, - /** M02 면적산출 · 재료산출 오른쪽 조합 패널 — 접힘 · 폭 */ + /** M02 면적산출 오른쪽 일위대가 패널(`AreaPanel`) — 접힘 · 폭(옛 조합 패널 키 그대로) */ "m02-combo-panel-collapsed": { bucket: "pref", scope: "global" }, "m02-combo-panel-width": { bucket: "pref", scope: "global" }, /** M02 면적산출 카드 미리보기 아래 알약 줄 보임(기본 숨김 · 상세는 늘 보임) */ diff --git a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts index 63d2a3671..4c34f3e76 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts @@ -187,33 +187,6 @@ export interface StructureCombos { export const fetchStructureCombos = (): Promise => scoped("/structure-combos"); /** 조합 갈라 내기 결과 — `새로만듦` false = 같은 담은 로직의 조합이 이미 있어 그 키를 가리킴 */ -export interface BranchedCombo { - 키: string; - 이름: string; - 새로만듦: boolean; - 집계표판: string; -} - -/** 이 열이 가리키는 일위대가 조합을 고친 담은 로직으로 갈라 냄 — 새 UA · 집계표 그 열만 새 키 · - * `판`(집계표 판)을 주면 어긋날 때 StaleError · 원래 조합은 그대로 */ -export const branchCombo = ( - column: string, - body: { - 본뜬키: string; - 담은로직: { 로직: string; 메모?: string | null }[]; - 이름: string; - 판?: string; - }, -): Promise => - scoped(`/structures/${enc(column)}/combo`, { method: "POST", body: JSON.stringify(body) }); - -/** 있는 조합을 이 열에 씀 — 집계표 그 열 일위대가 키만 바꿈(조합은 그대로 · `새로만듦` false) */ -export const setColumnCombo = ( - column: string, - body: { 종류: "일위대가"; 키: string; 판?: string }, -): Promise => - scoped(`/structures/${enc(column)}/use-combo`, { method: "POST", body: JSON.stringify(body) }); - /* --- 구조물 옵션 보이기(마스터 템플릿 `구조물옵션표시.json`) — 좌측 「구조물 옵션 보이기」 줄 --- */ export interface OptionKind { type_id: string; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Combo.css b/M02_MasterTemplete/M02_MasterTemplete_Combo.css deleted file mode 100644 index e51b4b0db..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Combo.css +++ /dev/null @@ -1,75 +0,0 @@ -/* M02 조합 줄(면적산출) · 조합 고치기 창 · 자재 목록 옆 판(재료산출) */ - -/* 알약 막대 위 한 줄 */ -.m02-combo { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: var(--spacing-4) var(--spacing-12); - font-size: var(--text-body-sm); -} - -.m02-combo__key { - padding: 0 var(--spacing-8); - border-radius: var(--radius-md); - background: color-mix(in srgb, var(--color-accent) 14%, transparent); - color: var(--color-text); - font-variant-numeric: tabular-nums; - white-space: nowrap; -} - -.m02-combo__name { - color: var(--color-text); - overflow-wrap: anywhere; -} - -.m02-combo__meta, -.m02-combo__origin { - color: var(--color-text-secondary); - white-space: nowrap; -} - -.m02-combo__origin { - font-size: var(--text-caption); -} - -.m02-combo > .ui-btn { - margin-left: auto; -} - -/* 조합 고치기 창 — 담은 로직 표(M01 조합 화면과 같은 줄) */ -.ui-modal.m02-combo-edit { - width: min(980px, 94vw); -} - -.m02-combo-edit__row { - display: grid; - grid-template-columns: 3rem 7rem minmax(10rem, 1.4fr) 6rem minmax(8rem, 1fr) auto; - align-items: center; - gap: var(--spacing-8); - padding: var(--spacing-4) 0; - border-bottom: 1px solid var(--color-border); -} - -.m02-combo-edit__row--head { - color: var(--color-text-secondary); - font-size: var(--text-caption); - font-weight: 600; -} - -.m02-combo-edit__acts { - display: flex; - gap: var(--spacing-4); -} - -.m02-combo-edit__foot, -.m02-branch__foot { - display: flex; - gap: var(--spacing-8); - justify-content: flex-end; -} - -.m02-branch__msg { - margin: 0; - line-height: 1.5; -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Combo.ts b/M02_MasterTemplete/M02_MasterTemplete_Combo.ts deleted file mode 100644 index 02b0cc559..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Combo.ts +++ /dev/null @@ -1,309 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Combo.ts - * 면적산출 편집 화면 — 오른쪽 조합 패널 「이 열의 조합」 칸의 조합 줄 + [조합 고치기](시스템 관리자만). - * 일위대가 조합(UA)은 여러 열 · 모든 프로젝트가 같이 씀 → 제자리 고치지 않고 고친 내용으로 새 조합을 만들어 - * 이 열만 가리킴(서버 `branchCombo`) · 원래 조합은 그대로. 담은 로직 표는 M01 조합 화면과 같은 줄 모양. - * ========================================================================== */ - -import { - createButton, - createInputField, - el, - showConfirmDialog, - showToast, -} from "@ui/ui_template_elements"; -import { openModal } from "@ui/ui_template_modal"; -import { fetchCombo, type ComboLine } from "./M02_MasterTemplete_Labor_Api"; -import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick"; -import { branchCombo, type BranchedCombo } from "./M02_MasterTemplete_Api_Fetch"; -import { readColumnBase, reasonOf } from "./M02_MasterTemplete_Combo_Api"; -import "./M02_MasterTemplete_Combo.css"; - -const TEXT = { - combo: "일위대가", - logics: "담은 로직 {n}", - shared: "다른 열 {n} 이 같이 씀", - alone: "이 열만 씀", - branched: "본뜬 조합 {value}", - edit: "조합 고치기", - editTitle: "일위대가 조합 고치기 — {value}", - no: "차례", - key: "키", - name: "이름", - unit: "결과 단위", - memo: "메모", - up: "위", - down: "아래", - remove: "빼기", - add: "로직 추가", - apply: "적용", - close: "닫기", - empty: "담은 로직이 없음", - unchanged: "고친 것이 없음", - discard: "고친 것을 버리고 닫음", - discardOk: "버리고 닫음", - nameEmpty: "이름을 넣음", - noLines: "담은 로직이 없어 적용 안 됨 — 로직을 하나 이상 담음", - nameLong: "이름은 {n}자까지", - loadFailed: "조합을 못 읽음 — {value}", - askTitle: "새 조합으로 갈라 냄", - askName: "새 조합 이름", - askOk: "새로 만듦", - askCancel: "취소", - askBody: - "이 조합은 열 {n} 개와 모든 프로젝트가 같이 씀 — 고친 내용으로 새 조합을 만들고 이 열만 새 조합을 가리킴 · 같은 내용의 조합이 있으면 그것을 씀 · 원래 조합은 그대로", - askAlone: - "이 조합은 모든 프로젝트가 같이 씀 — 고친 내용으로 새 조합을 만들고 이 열만 새 조합을 가리킴 · 같은 내용의 조합이 있으면 그것을 씀 · 원래 조합은 그대로", - made: "새 {kind} {key} 「{name}」 을 만듦 — 이 열이 가리킴", - reused: "같은 내용의 {kind} {key} 「{name}」 을 씀 — 새로 안 만듦", - failed: "조합을 못 고침 — {value}", -}; - -/** 조합 이름 글자 수 한도 — 서버 `master_combo.MAX_이름` 과 같음 */ -const MAX_NAME = 60; - -const fill = (text: string, value: string | number): string => - text.replace("{n}", String(value)).replace("{value}", String(value)); - -/** 갈라 내기 확인 모달 — 이름 칸을 미리 채워 두고 고칠 수 있음 · 취소면 null */ -export function askBranch(message: string, defaultName: string): Promise { - return new Promise((resolve) => { - let done = false; - const finish = (value: string | null): void => { - if (!done) resolve(value); - done = true; - }; - openModal({ - title: TEXT.askTitle, - closeLabel: TEXT.askCancel, - dialogClass: "m02-modal m02-branch", - mount: (body, close) => { - const name = createInputField({ label: TEXT.askName, type: "text" }); - name.input.value = defaultName.slice(0, MAX_NAME); - name.input.maxLength = MAX_NAME; - const ok = createButton({ - label: TEXT.askOk, - variant: "filled", - onClick: () => { - const value = name.input.value.trim(); - if (!value || value.length > MAX_NAME) { - name.setError(value ? fill(TEXT.nameLong, MAX_NAME) : TEXT.nameEmpty); - return name.input.focus(); - } - finish(value); - close(); - }, - }); - name.input.addEventListener("input", () => name.setError(undefined)); - name.input.addEventListener("keydown", (ev) => ev.key === "Enter" && ok.click()); - body.append( - el("p", { className: "m02-branch__msg", text: message }), - name.root, - el("div", { className: "m02-branch__foot", children: [ok] }), - ); - name.input.focus(); - }, - }); - // 닫기 단추 · Esc · 바깥 누르기 — 창이 빠지면 취소 - const watch = new MutationObserver(() => { - if (document.querySelector(".m02-branch")) return; - watch.disconnect(); - finish(null); - }); - watch.observe(document.body, { childList: true }); - }); -} - -/** 새 조합 이름 자동 — 원래 이름(끝 「 (n)」 뗌 · 서버가 새로 붙임) · 구조물 이름(한쪽이 다른 쪽을 - * 이미 품으면 긴 쪽 하나만) */ -export const branchName = ( - named: string, - label: string, - original = named.replace(/ \(\d+\)$/, ""), -): string => - !original || label.includes(original) - ? label || original - : !label || original.includes(label) - ? original - : `${original} · ${label}`; - -/** 결과 알림 — 새로 만듦 / 같은 내용의 있는 조합을 씀(`kind` = 일위대가 조합 · 자재 조합) */ -export const branchToast = (r: BranchedCombo, kind = "일위대가 조합"): void => - showToast( - (r.새로만듦 ? TEXT.made : TEXT.reused) - .replace("{kind}", kind) - .replace("{key}", r.키) - .replace("{name}", r.이름), - "success", - ); - -export interface ComboBarOptions { - /** 집계표 열 id */ - column: string; - /** 화면 제목에 쓰는 구조물 이름(집계표 머리 층을 이은 글) — 새 이름 자동 미리 채움에 씀 */ - label: string; - isAdmin: boolean; - /** 고치기 전 — 저장 안 한 시트 고침이 있으면 물음 · false 면 그만둠 */ - beforeEdit: () => Promise; - /** 갈라 낸 뒤 — 알약 다시 받기 */ - onApplied: () => Promise | void; -} - -interface Line { - 로직: string; - 메모: string | null; - 이름: string; - 결과단위: string; -} - -/** 조합 줄 — 이 열에 일위대가가 없으면 null */ -export async function buildComboBar(opts: ComboBarOptions): Promise { - const base = await readColumnBase(opts.column); - if (!base.일위대가) return null; - const key = base.일위대가; - let one; - try { - one = await fetchCombo(key); - } catch (error) { - showToast(fill(TEXT.loadFailed, reasonOf(error)), "error"); - return null; - } - const combo = one.combo as typeof one.combo & { 본뜬키?: string | null }; - const same = base.다른열일위대가; - const parts = [ - el("span", { className: "m02-combo__key", text: `${TEXT.combo} ${key}` }), - el("span", { className: "m02-combo__name", text: combo.이름 }), - el("span", { className: "m02-combo__meta", text: fill(TEXT.logics, combo.담은로직.length) }), - el("span", { - className: "m02-combo__meta", - text: same ? fill(TEXT.shared, same) : TEXT.alone, - }), - ]; - if (opts.isAdmin && combo.본뜬키) { - parts.push( - el("span", { className: "m02-combo__origin", text: fill(TEXT.branched, combo.본뜬키) }), - ); - } - const bar = el("div", { className: "m02-combo", children: parts }); - if (!opts.isAdmin) return bar; - - const edit = createButton({ label: TEXT.edit, variant: "ghost", size: "sm" }); - edit.addEventListener("click", async () => { - if (!(await opts.beforeEdit())) return; - openEditor(one.줄, combo.이름, same); - }); - bar.append(edit); - return bar; - - function openEditor(from: ComboLine[], comboName: string, others: number): void { - const original = JSON.stringify(from.map((l) => [l.로직, l.메모 ?? null])); - const changed = (): boolean => - JSON.stringify(lines.map((l) => [l.로직, l.메모 ?? null])) !== original; - const lines: Line[] = from.map((l) => ({ - 로직: l.로직, - 메모: l.메모, - 이름: l.이름 ?? "", - 결과단위: l.결과단위 ?? "", - })); - openModal({ - title: fill(TEXT.editTitle, opts.label), - closeLabel: TEXT.close, - dialogClass: "m02-modal m02-combo-edit", - // 고친 채 닫기 · Esc · 바깥 누르기 — 버릴지 물음 - beforeClose: () => !changed() || showConfirmDialog(TEXT.discard, TEXT.discardOk), - mount: (body, close) => { - const table = el("div", { className: "m02-combo-edit__table" }); - const paint = (): void => { - const head = el("div", { - className: "m02-combo-edit__row m02-combo-edit__row--head", - children: [TEXT.no, TEXT.key, TEXT.name, TEXT.unit, TEXT.memo, ""].map((t) => - el("span", { text: t }), - ), - }); - const rows = lines.map((line, i) => { - const memo = el("input", { - className: "ui-input", - attrs: { type: "text", value: line.메모 ?? "", title: line.메모 ?? "" }, - }); - memo.addEventListener("input", () => { - line.메모 = memo.value || null; - memo.title = memo.value; - }); - const move = (to: number): void => { - if (to < 0 || to >= lines.length) return; - [lines[i], lines[to]] = [lines[to], lines[i]]; - paint(); - }; - const up = createButton({ label: "▲", variant: "ghost", size: "sm" }); - up.title = TEXT.up; - up.disabled = i === 0; - up.addEventListener("click", () => move(i - 1)); - const down = createButton({ label: "▼", variant: "ghost", size: "sm" }); - down.title = TEXT.down; - down.disabled = i === lines.length - 1; - down.addEventListener("click", () => move(i + 1)); - const del = createButton({ label: TEXT.remove, variant: "ghost", size: "sm" }); - del.addEventListener("click", () => { - lines.splice(i, 1); - paint(); - }); - return el("div", { - className: "m02-combo-edit__row", - attrs: { "data-combo-line": line.로직 }, - children: [ - el("span", { text: String(i + 1) }), - el("span", { text: line.로직 }), - el("span", { text: line.이름 }), - el("span", { text: line.결과단위 }), - memo, - el("span", { className: "m02-combo-edit__acts", children: [up, down, del] }), - ], - }); - }); - table.replaceChildren( - head, - ...(rows.length - ? rows - : [el("p", { className: "m02-struct__empty", text: TEXT.empty })]), - ); - }; - paint(); - const add = createButton({ label: TEXT.add, variant: "ghost", size: "sm" }); - add.addEventListener("click", () => - openLogicPicker((logic) => { - lines.push({ 로직: logic.키, 메모: null, 이름: logic.이름, 결과단위: logic.결과단위 }); - paint(); - }), - ); - const apply = createButton({ label: TEXT.apply, variant: "filled" }); - apply.addEventListener("click", async () => { - if (!changed()) { - return showToast(TEXT.unchanged, "info"); - } - if (!lines.length) return showToast(TEXT.noLines, "error"); - const message = others ? fill(TEXT.askBody, others + 1) : TEXT.askAlone; - const name = await askBranch(message, branchName(comboName, opts.label)); - if (name === null) return; - apply.disabled = true; - try { - const result = await branchCombo(opts.column, { - 본뜬키: key, - 담은로직: lines.map((l) => ({ 로직: l.로직, 메모: l.메모 })), - 이름: name, - }); - branchToast(result); - close(); - await opts.onApplied(); - } catch (error) { - apply.disabled = false; - showToast(fill(TEXT.failed, reasonOf(error)), "error"); - } - }); - body.append( - table, - el("div", { className: "m02-combo-edit__foot", children: [add, apply] }), - ); - }, - }); - } -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_ComboPanel.css b/M02_MasterTemplete/M02_MasterTemplete_ComboPanel.css deleted file mode 100644 index 6b2071651..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_ComboPanel.css +++ /dev/null @@ -1,91 +0,0 @@ -/* M02 면적산출 · 재료산출 오른쪽 조합 패널 — 틀은 공용 사이드바 카드 · 접기 · 폭 손잡이 · 접기 손잡이 */ - -/* 편집 칸 = 시트 | 패널 — 패널 폭은 손잡이가 `--m02-cpanel-width` 로 줌 */ -.m02-cpanel { - position: relative; - display: flex; - flex: 0 0 auto; - flex-direction: column; - width: var(--m02-cpanel-width, 24rem); - min-width: 280px; - max-width: 60%; - min-height: 0; - border-left: 1px solid var(--color-border); -} - -.m02-cpanel.is-resizing { - transition: none; -} - -.m02-cpanel.is-collapsed { - width: 0; - min-width: 0; -} - -.m02-cpanel.is-collapsed > .m02-cpanel__body, -.m02-cpanel.is-collapsed > .ui-resizer { - display: none; -} - -/* 접기 손잡이 — 패널 왼쪽 밖 세로 가운데(시트 위로) */ -.m02-cpanel > .ui-workflow-overlay__handle--side { - z-index: 6; - right: auto; - left: calc(-1 * var(--spacing-24)); - border-right: 0; - border-radius: var(--radius-buttons) 0 0 var(--radius-buttons); -} - -/* 안쪽만 굴림 — 페이지 스크롤 안 생김 */ -.m02-cpanel__body { - flex: 1 1 0; - min-height: 0; - overflow: auto; - padding: var(--spacing-8); -} - -.m02-cpanel__list { - max-height: 24rem; - overflow: auto; -} - -.m02-cpanel__row { - display: grid; - grid-template-columns: auto 1fr; - gap: 0 var(--spacing-8); - text-align: left; -} - -.m02-cpanel__key { - font-variant-numeric: tabular-nums; - font-weight: 600; -} - -.m02-cpanel__name { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.m02-cpanel__meta { - grid-column: 1 / -1; - color: var(--color-text-secondary); - font-size: var(--text-caption); -} - -/* 고른 줄 + 바로 아래 [이 열에 쓰기] */ -.m02-cpanel__pick { - display: flex; - flex-direction: column; - gap: var(--spacing-4); - padding-bottom: var(--spacing-4); -} - -.m02-cpanel__pick > .ui-btn { - align-self: flex-end; -} - -/* 이 열이 쓰는 조합 — 테두리로 늘 보임(고름 is-active 와 따로) */ -.m02-cpanel__row.is-current { - box-shadow: inset 3px 0 0 var(--color-primary); -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_ComboPanel.ts b/M02_MasterTemplete/M02_MasterTemplete_ComboPanel.ts deleted file mode 100644 index 05c6613c2..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_ComboPanel.ts +++ /dev/null @@ -1,227 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_ComboPanel.ts - * 면적산출 편집 화면 오른쪽 안쪽 패널 — 좌측 패널과 같은 공용 틀(사이드바 카드 · 접기) · - * 공용 폭 손잡이(`ui_template_resizer` · 폭 기억 · 최소 폭) · 공용 접기 손잡이(`ui_template_overlay`). - * · 「이 열의 조합」 — 일위대가 조합 줄 + [조합 고치기](`M02_MasterTemplete_Combo.ts`) · - * 넣는 쪽이 `setMine` 으로 갈아 끼움 - * · 조합 목록 — 일위대가 조합(이 층이 쓸 수 있는 마스터 UA · 회사 UC · 개인 UP · 27-2c) · - * 찾기 · 이 열 조합 강조 · - * 하나 골라 [이 열에 쓰기] = 집계표 그 열 키만 바꿈(서버 `use-combo` · 조합은 그대로) - * ========================================================================== */ - -import { - createButton, - createInputField, - el, - showConfirmDialog, - showToast, -} from "@ui/ui_template_elements"; -import { attachCollapsible } from "@ui/ui_template_collapsible"; -import { createWorkflowPanelHandle } from "@ui/ui_template_overlay"; -import { createPanelResizer } from "@ui/ui_template_resizer"; -import { readStateRaw, stateKey, writeStateRaw } from "../A00_Common/b_page_state"; -import { fetchUsableCombos } from "./M02_MasterTemplete_Labor_Api"; -import { getActiveLayer, setColumnCombo } from "./M02_MasterTemplete_Api_Fetch"; -import { readColumnBase, reasonOf } from "./M02_MasterTemplete_Combo_Api"; -import "./M02_MasterTemplete_ComboPanel.css"; - -const TEXT = { - handle: "조합", - mine: "이 열의 조합", - noneArea: "이 열에 일위대가 조합 없음", - listArea: "일위대가 조합 목록", - search: "키 · 이름으로 찾기", - logics: "로직 {n}", - origin: "본뜬 {value}", - current: "이 열", - empty: "맞는 조합 없음", - none: "조합 없음", - use: "이 열에 쓰기", - useAsk: "이 열이 {value} 을 쓰게 바꿈 — 구조물 조합에 바로 적힘 · 조합은 그대로", - useOk: "바꿈", - used: "이 열이 {value} 을 씀", - loadFailed: "조합 목록을 못 받음 — {value}", - failed: "이 열에 못 씀 — {value}", -}; - -const fill = (text: string, value: string | number): string => - text.replace("{n}", String(value)).replace("{value}", String(value)); - -/** 최소 폭(px) · 최대 = 편집 칸 폭의 비율 */ -const MIN_WIDTH = 280; -const MAX_RATIO = 0.6; - -interface Row { - 키: string; - 이름: string; - 수: number; - 본뜬키: string | null; -} - -export interface ComboPanelOptions { - kind: "area"; - /** 집계표 열 id */ - column: string; - isAdmin: boolean; - /** [이 열에 쓰기] 뒤 — 알약 · 「이 열의 조합」을 새 조합으로 다시 그림 */ - onUsed: () => Promise | void; -} - -export interface ComboPanel { - root: HTMLElement; - /** 「이 열의 조합」 부분을 갈아 끼움(조합 줄) */ - setMine: (node: HTMLElement | null) => void; - /** 목록 · 이 열 조합 키를 다시 받음(갈라 낸 뒤 등) */ - refresh: () => Promise; - destroy: () => void; -} - -export function buildComboPanel(opts: ComboPanelOptions): ComboPanel { - let rows: Row[] = []; - let current: string | null = null; - let picked: string | null = null; - let q = ""; - let seq = 0; - - const card = (title: string, ...children: HTMLElement[]): HTMLElement => - el("section", { - className: "ui-collapsible ui-sidebar-section ui-sidebar-card", - children: [el("h3", { className: "ui-collapsible__title", text: title }), ...children], - }); - const mine = el("div", { className: "m02-cpanel__mine" }); - const find = createInputField({ type: "search", placeholder: TEXT.search }); - const list = el("div", { className: "ui-sidebar-list m02-cpanel__list" }); - const use = createButton({ label: TEXT.use, variant: "filled", size: "sm" }); - const body = el("div", { - className: "m02-cpanel__body ui-sidebar-panel", - children: [card(TEXT.mine, mine), card(TEXT.listArea, find.root, list)], - }); - attachCollapsible(body); - - const root = el("aside", { className: "m02-cpanel", attrs: { "data-kind": opts.kind } }); - const handle = createWorkflowPanelHandle("side", "right", TEXT.handle); - // 왼쪽 경계를 끌어 폭 조절 — 폭은 브라우저에 기억 · 더블클릭 = 기본 폭 - const resizer = createPanelResizer({ - axis: "horizontal", - target: root, - cssVar: "--m02-cpanel-width", - direction: -1, - min: MIN_WIDTH, - max: () => (root.parentElement?.clientWidth ?? window.innerWidth) * MAX_RATIO, - storageKey: stateKey("m02-combo-panel-width") ?? undefined, - }); - root.append(handle.root, resizer.root, body); - const setCollapsed = (collapsed: boolean): void => { - root.classList.toggle("is-collapsed", collapsed); - handle.setOpen(!collapsed); - writeStateRaw("m02-combo-panel-collapsed", String(collapsed)); - }; - handle.root.addEventListener("click", () => - setCollapsed(!root.classList.contains("is-collapsed")), - ); - setCollapsed(readStateRaw("m02-combo-panel-collapsed") === "true"); - - find.input.addEventListener("input", () => { - q = find.input.value.trim().toLowerCase(); - paint(); - }); - - function paint(): void { - const shown = rows.filter((r) => !q || `${r.키} ${r.이름}`.toLowerCase().includes(q)); - list.replaceChildren( - ...(shown.length - ? shown.map((r) => { - const meta = [ - fill(TEXT.logics, r.수), - r.본뜬키 ? fill(TEXT.origin, r.본뜬키) : "", - r.키 === current ? TEXT.current : "", - ]; - const b = el("button", { - className: - "m02-side__item ui-sidebar-row m02-cpanel__row" + - (r.키 === picked ? " is-active" : "") + - (r.키 === current ? " is-current" : ""), - attrs: { type: "button", "data-combo-key": r.키, title: `${r.키} ${r.이름}` }, - children: [ - el("span", { className: "m02-cpanel__key", text: r.키 }), - el("span", { className: "m02-cpanel__name", text: r.이름 }), - el("span", { - className: "m02-cpanel__meta", - text: meta.filter(Boolean).join(" · "), - }), - ], - }); - b.addEventListener("click", () => { - picked = r.키 === picked ? null : r.키; - paint(); - }); - // [이 열에 쓰기]는 고른 줄 바로 아래(목록 안) — 목록 밖 단추가 스크롤된 줄 글과 겹치지 않게 - return opts.isAdmin && r.키 === picked && r.키 !== current - ? el("div", { className: "m02-cpanel__pick", children: [b, use] }) - : b; - }) - : [el("p", { className: "m02-side__empty", text: q ? TEXT.empty : TEXT.none })]), - ); - use.disabled = false; - if (use.isConnected) use.scrollIntoView({ block: "nearest" }); - } - - async function refresh(): Promise { - const mineSeq = ++seq; - try { - const [base, got] = await Promise.all([ - readColumnBase(opts.column), - fetchUsableCombos(getActiveLayer()).then((cs) => - cs.map((c) => ({ - 키: c.키, - 이름: c.이름, - 수: c.count, // 목록 요약엔 담은로직이 없음 — 수만 옴 - 본뜬키: c.본뜬키 ?? null, - })), - ), - ]); - if (mineSeq !== seq) return; - current = base.일위대가 ?? null; - rows = got; - } catch (error) { - if (mineSeq !== seq) return; - showToast(fill(TEXT.loadFailed, reasonOf(error)), "error"); - } - if (picked === current) picked = null; - paint(); - } - - use.addEventListener("click", async () => { - const row = rows.find((r) => r.키 === picked); - if (!row || row.키 === current) return; - if (!(await showConfirmDialog(fill(TEXT.useAsk, `${row.키} ${row.이름}`), TEXT.useOk))) return; - use.disabled = true; - try { - const got = await setColumnCombo(opts.column, { - 종류: "일위대가", - 키: row.키, - }); - showToast(fill(TEXT.used, `${got.키} ${got.이름}`), "success"); - picked = null; - await opts.onUsed(); - await refresh(); - } catch (error) { - showToast(fill(TEXT.failed, reasonOf(error)), "error"); - paint(); - } - }); - - paint(); - void refresh(); - return { - root, - setMine: (node) => - mine.replaceChildren(node ?? el("p", { className: "m02-side__empty", text: TEXT.noneArea })), - refresh, - destroy: () => { - ++seq; - resizer.dispose(); - root.remove(); - }, - }; -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Combo_Api.ts b/M02_MasterTemplete/M02_MasterTemplete_Combo_Api.ts deleted file mode 100644 index a352532ad..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Combo_Api.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Combo_Api.ts - * 조합 고치기 화면이 쓰는 읽기 — 집계표 열이 가리키는 조합 키 · 같이 쓰는 다른 열 수 · - * 서버 까닭 글. 갈라 내기 길(`branchCombo`)은 `M02_MasterTemplete_Api_Fetch.ts`. - * ========================================================================== */ - -import { getActiveLayer, readTemplate } from "./M02_MasterTemplete_Api_Fetch"; - -/** 구조물집계표 이름 — `M02_MasterTemplete_Structure.STRUCTURE_TABLE` 과 같은 글 */ -const STRUCTURE_TABLE = "구조물집계표"; - -/** 집계표 열이 가리키는 조합 키 + 같은 조합을 쓰는 다른 열 수 */ -export interface ColumnBase { - 일위대가: string | null; - 다른열일위대가: number; -} - -/** 서버가 준 까닭 글 */ -export const reasonOf = (error: unknown): string => (error instanceof Error ? error.message : ""); - -/** 집계표(시스템 층)에서 이 열의 일위대가 키와 같이 쓰는 다른 열 수 */ -export async function readColumnBase(column: string): Promise { - type Col = { id: string; 일위대가?: string | null }; - const table = await readTemplate(getActiveLayer(), "table", STRUCTURE_TABLE, null).catch( - () => null, - ); - const cols = ((table?.문서 as { 열?: Col[] } | null)?.열 ?? []) as Col[]; - const mine = cols.find((c) => c.id === column); - const same = (key: string | null | undefined, pick: (c: Col) => string | null | undefined) => - key ? cols.filter((c) => c.id !== column && pick(c) === key).length : 0; - return { - 일위대가: mine?.일위대가 || null, - 다른열일위대가: same(mine?.일위대가, (c) => c.일위대가), - }; -} diff --git a/resources/tester/test_m02_structure_panel.py b/resources/tester/test_m02_structure_panel.py index 99585c6a5..67678a9d9 100644 --- a/resources/tester/test_m02_structure_panel.py +++ b/resources/tester/test_m02_structure_panel.py @@ -170,12 +170,16 @@ def test_좌측_구조물_집계_양식_아래_하위_넷_새로_본떠_지우 ) -def test_면적_재료_오른쪽_조합_패널은_공용_틀_손잡이(): - """좌측과 같은 사이드바 카드 · 접기 · 공용 폭 손잡이 · 접기 손잡이 — 새로 그리지 않음.""" - cpanel = _text(M02 / "M02_MasterTemplete_ComboPanel.ts") +def test_면적_오른쪽_일위대가_패널은_공용_틀_손잡이(): + """좌측과 같은 사이드바 카드 · 접기 · 공용 폭 손잡이 · 접기 손잡이 — 새로 그리지 않음(39-4 · 옛 조합 패널 지움).""" + apanel = _text(M02 / "M02_MasterTemplete_AreaPanel.ts") for name in ("createPanelResizer", "createWorkflowPanelHandle", "attachCollapsible"): - assert name in cpanel, name - assert "ui-sidebar-card" in cpanel and "setColumnCombo" in cpanel + assert name in apanel, name + assert "ui-sidebar-card" in apanel and "setColumnCombo" not in apanel + for gone in ("ComboPanel.ts", "ComboPanel.css", "Combo.ts", "Combo.css", "Combo_Api.ts"): + assert not (M02 / f"M02_MasterTemplete_{gone}").exists(), gone + api = _text(M02 / "M02_MasterTemplete_Api_Fetch.ts") + assert "branchCombo" not in api and "setColumnCombo" not in api main = _text(MAIN) assert "comboSlot" not in main # 조합 줄은 패널 「이 열의 조합」 칸으로 assert "matPanel" not in main # 자재 조합 판은 없앰