diff --git a/A00_Common/spreadsheet/spreadsheet.css b/A00_Common/spreadsheet/spreadsheet.css index 067f5f1a..1949871e 100644 --- a/A00_Common/spreadsheet/spreadsheet.css +++ b/A00_Common/spreadsheet/spreadsheet.css @@ -7,8 +7,8 @@ height: 100%; min-height: 0; font-size: 13px; - color: #1f2328; - background: #fff; + color: var(--color-text-body); + background: var(--color-surface-raised); } .ss-grid-host { @@ -21,26 +21,26 @@ flex: none; align-items: stretch; height: 26px; - border-bottom: 1px solid #d0d7de; + border-bottom: 1px solid var(--color-border); } .ss-addr { width: 96px; padding: 0 6px; border: none; - border-right: 1px solid #d0d7de; + border-right: 1px solid var(--color-border); font: inherit; outline: none; } .ss-addr:focus { - box-shadow: inset 0 0 0 2px #217346; + box-shadow: inset 0 0 0 2px var(--color-focus-ring); } .ss-fx { align-self: center; padding: 0 8px; - color: #6e7781; + color: var(--color-text-muted); font-style: italic; user-select: none; } @@ -49,7 +49,7 @@ flex: 1; padding: 4px 6px; border: none; - border-left: 1px solid #d0d7de; + border-left: 1px solid var(--color-border); font: inherit; line-height: 18px; white-space: pre; @@ -59,7 +59,7 @@ } .ss--readonly .ss-formula { - color: #57606a; + color: var(--color-text-secondary); } /* 칸 편집기 = 숨은 textarea — 쉴 때는 활성 칸 자리에 투명하게(한글 조합 창 자리) */ @@ -69,9 +69,9 @@ box-sizing: border-box; margin: 0; padding: 1px 3px; - border: 2px solid #217346; - background: #fff; - color: #000; + border: 2px solid var(--color-focus-ring); + background: var(--color-surface-raised); + color: var(--color-text-body); font: inherit; line-height: 1.25; white-space: pre; @@ -95,7 +95,7 @@ position: absolute; z-index: 4; box-sizing: border-box; - border: 1px dashed #57606a; + border: 1px dashed var(--color-text-secondary); pointer-events: none; } @@ -106,8 +106,8 @@ z-index: 20; padding: 6px 10px; border-radius: 4px; - background: #24292f; - color: #fff; + background: var(--color-primary); + color: var(--color-primary-text); } /* 2단계 잇기 — 서식 붓 · 찾기 패널 · 메모 · 골라 붙여넣기 */ @@ -115,15 +115,15 @@ flex: none; padding: 0 8px; border: none; - border-right: 1px solid #d0d7de; + border-right: 1px solid var(--color-border); background: none; font: inherit; cursor: pointer; } .ss-brush.is-on { - background: #dafbe1; - box-shadow: inset 0 0 0 2px #217346; + background: color-mix(in srgb, var(--color-focus-ring) 20%, var(--color-surface-raised)); + box-shadow: inset 0 0 0 2px var(--color-focus-ring); } .ss--painting .ss-grid-host { @@ -139,10 +139,10 @@ flex-direction: column; gap: 4px; padding: 8px; - border: 1px solid #d0d7de; + border: 1px solid var(--color-border); border-radius: 6px; - background: #fff; - box-shadow: 0 4px 12px rgb(0 0 0 / 15%); + background: var(--color-surface-raised); + box-shadow: var(--shadow-lg); } .ss-find[hidden], @@ -162,15 +162,15 @@ .ss-find__input { width: 180px; padding: 2px 6px; - border: 1px solid #d0d7de; + border: 1px solid var(--color-border); font: inherit; } .ss-find__btn { padding: 2px 8px; - border: 1px solid #d0d7de; + border: 1px solid var(--color-border); border-radius: 4px; - background: #f6f8fa; + background: var(--color-surface); font: inherit; cursor: pointer; } @@ -184,7 +184,7 @@ .ss-find__status { font-size: 12px; - color: #57606a; + color: var(--color-text-secondary); } .ss-memo, @@ -194,11 +194,11 @@ min-width: 140px; max-width: 260px; padding: 4px 6px; - border: 1px solid #bf8700; - background: #fff8c5; + border: 1px solid var(--color-warning); + background: color-mix(in srgb, var(--color-warning) 15%, var(--color-surface-raised)); font: inherit; white-space: pre-wrap; - box-shadow: 0 2px 6px rgb(0 0 0 / 15%); + box-shadow: var(--shadow-sm); } .ss-memo { @@ -213,15 +213,15 @@ display: flex; gap: 2px; padding: 4px; - border: 1px solid #d0d7de; - background: #fff; - box-shadow: 0 4px 12px rgb(0 0 0 / 15%); + border: 1px solid var(--color-border); + background: var(--color-surface-raised); + box-shadow: var(--shadow-lg); } .ss-pick > button { padding: 2px 8px; - border: 1px solid #d0d7de; - background: #f6f8fa; + border: 1px solid var(--color-border); + background: var(--color-surface); font: inherit; cursor: pointer; } @@ -232,10 +232,10 @@ z-index: 6; min-width: 200px; max-width: 360px; - border: 1px solid #d0d7de; - background: #fff; + border: 1px solid var(--color-border); + background: var(--color-surface-raised); font-size: 12px; - box-shadow: 0 4px 12px rgb(0 0 0 / 15%); + box-shadow: var(--shadow-lg); } .ss-hint[hidden] { @@ -255,7 +255,7 @@ } .ss-hint__item.is-on { - background: #ddf4ff; + background: color-mix(in srgb, var(--color-accent) 18%, var(--color-surface-raised)); } /* 데이터 메뉴 — 패널 창 · 유효성 목록 ▼ */ @@ -266,10 +266,10 @@ z-index: 21; max-height: calc(100% - 8px); overflow: auto; - border: 1px solid #d0d7de; + border: 1px solid var(--color-border); border-radius: 6px; - background: #fff; - box-shadow: 0 4px 12px rgb(0 0 0 / 15%); + background: var(--color-surface-raised); + box-shadow: var(--shadow-lg); } .ss-dialog[hidden], @@ -283,7 +283,7 @@ justify-content: space-between; gap: 8px; padding: 6px 8px; - border-bottom: 1px solid #d0d7de; + border-bottom: 1px solid var(--color-border); font-weight: bold; } @@ -296,8 +296,8 @@ z-index: 8; width: 16px; padding: 0; - border: 1px solid #8c959f; - background: #f6f8fa; + border: 1px solid var(--color-border); + background: var(--color-surface); font-size: 9px; cursor: pointer; } diff --git a/A00_Common/spreadsheet/spreadsheet_cellstyle.ts b/A00_Common/spreadsheet/spreadsheet_cellstyle.ts index 4ba194bb..842bca41 100644 --- a/A00_Common/spreadsheet/spreadsheet_cellstyle.ts +++ b/A00_Common/spreadsheet/spreadsheet_cellstyle.ts @@ -94,7 +94,7 @@ export function resolveBoxBorders( /** 빈 글 = 인라인 테두리를 안 그림(css `aislo-grid--gridlines` 눈금선이 비쳐 보임). `scale` = 확대 · 축소 배율(선 굵기도 같이). */ function borderCss(side: BorderSide | undefined, scale: number): string { if (!side) return ""; - const color = side.색 ?? "#000000"; + const color = side.색 ?? "currentColor"; const w = (px: number): number => Math.max(1, Math.round(px * scale)); switch (side.선) { case "hair": diff --git a/A00_Common/spreadsheet/spreadsheet_grid.css b/A00_Common/spreadsheet/spreadsheet_grid.css index 9998e5b2..9dd2d789 100644 --- a/A00_Common/spreadsheet/spreadsheet_grid.css +++ b/A00_Common/spreadsheet/spreadsheet_grid.css @@ -4,12 +4,16 @@ * ========================================================================== */ .aislo-grid { - --aislo-grid-line: #d9d9d9; - --aislo-grid-head-bg: #f3f3f3; - --aislo-grid-head-fg: #444; - --aislo-grid-head-border: #cfcfcf; + /* 앱 전역 테마 토큰(ui_template_theme.css · :root[data-theme])을 그대로 물려받음 — + * 여기서 값을 다시 안 적음(다크 전환 시 앱 토큰이 바뀌면 같이 바뀜). */ + --aislo-grid-bg: var(--color-surface-raised); + --aislo-grid-fg: var(--color-text-body); + --aislo-grid-line: var(--color-border); + --aislo-grid-head-bg: var(--color-surface); + --aislo-grid-head-fg: var(--color-text-secondary); + --aislo-grid-head-border: var(--color-border); --aislo-grid-freeze-shadow: rgba(0, 0, 0, 0.12); - --aislo-grid-selected: #1a73e8; + --aislo-grid-selected: var(--color-accent); position: relative; overflow: hidden; @@ -18,7 +22,8 @@ font: 10pt "굴림", sans-serif; - background: #fff; + background: var(--aislo-grid-bg); + color: var(--aislo-grid-fg); user-select: none; -webkit-user-select: none; } @@ -94,7 +99,7 @@ .aislo-grid__colhead--hidden-before, .aislo-grid__rowhead--hidden-before { - border-left: 3px double #888; + border-left: 3px double var(--aislo-grid-head-fg); } .aislo-grid__head-label { @@ -122,7 +127,7 @@ box-sizing: border-box; padding: 0; border: 1px solid var(--aislo-grid-head-border); - background: #fff; + background: var(--aislo-grid-head-bg); color: var(--aislo-grid-head-fg); font-size: 0.7em; line-height: 1; @@ -160,7 +165,7 @@ flex-direction: column; overflow: hidden; padding: 0 2px; - background: #fff; + background: var(--aislo-grid-bg); cursor: cell; } @@ -228,7 +233,7 @@ width: 6px; height: 6px; background: var(--aislo-grid-selected); - border: 1px solid #fff; + border: 1px solid var(--aislo-grid-bg); cursor: crosshair; z-index: 3; } diff --git a/A00_Common/spreadsheet/spreadsheet_grid.ts b/A00_Common/spreadsheet/spreadsheet_grid.ts index 23dc5c68..e17fd02d 100644 --- a/A00_Common/spreadsheet/spreadsheet_grid.ts +++ b/A00_Common/spreadsheet/spreadsheet_grid.ts @@ -815,7 +815,7 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle { node.style.top = `${screen.y}px`; node.style.width = `${screen.w}px`; node.style.height = `${screen.h}px`; - node.style.borderColor = "#1a73e8"; + node.style.borderColor = "var(--aislo-grid-selected)"; node.style.borderWidth = i === 0 ? "2px" : "1px"; selLayer!.append(node); } diff --git a/A00_Common/spreadsheet/spreadsheet_stage3.css b/A00_Common/spreadsheet/spreadsheet_stage3.css index 549d2588..453ab197 100644 --- a/A00_Common/spreadsheet/spreadsheet_stage3.css +++ b/A00_Common/spreadsheet/spreadsheet_stage3.css @@ -75,7 +75,7 @@ background: var(--color-surface); border: 1px solid var(--color-border); border-radius: 4px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12); + box-shadow: var(--shadow-sm); max-height: 160px; overflow-y: auto; } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Structure.css b/M02_MasterTemplete/M02_MasterTemplete_Structure.css index 35b74d67..a8de11bc 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Structure.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Structure.css @@ -78,3 +78,37 @@ padding: var(--spacing-16); color: var(--color-text-muted); } + +/* 조회 후 선택 창 — 찾기 칸 + 목록(번호 · 이름) */ +.m02-pick__list { + display: flex; + flex-direction: column; + max-height: 55vh; + overflow: auto; + margin: var(--spacing-8) 0; +} + +.m02-pick__item { + display: flex; + gap: var(--spacing-12); + padding: var(--spacing-8); + border: none; + border-radius: var(--radius-sm, 4px); + background: none; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} + +.m02-pick__item:hover, +.m02-pick__item:focus-visible { + background: var(--color-surface-sunken); +} + +.m02-pick__key { + flex: none; + min-width: 6rem; + color: var(--color-text-muted); + font-variant-numeric: tabular-nums; +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts index 4e07473a..fdd451e2 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts @@ -9,13 +9,25 @@ * 구조물 도면은 집계표 열마다 하나(`resources/master_template/structure/<열 id>.json`) · * 서버가 열마다 미리 만들어 둠(도번 자동 · 새 열도 집계표 저장 때 서버가 같이 만듦 — `M02_MasterTemplete_Store.py`). * 이 칸은 표 부품 host 밖 형제 자리에 붙음 — 집계표는 그대로 두고 여기만 열마다 다시 그림. + * 집계표 일위대가 · 도면 · 산출근거 줄의 조회 후 선택(`pick`)도 여기 — 일위대가 = M01 일위대가 목록에서 고름 · + * 도면 · 산출근거 = 다른 열 것을 골라 내용만 복사해 가져옴(번호 · 열은 그대로 · 서버가 복사). * 글자는 이 파일에 둠(도면 부품 `M02_MasterTemplete_Drawing.ts` 와 같은 방식). * ========================================================================== */ -import { createButton, el, showToast } from "@ui/ui_template_elements"; -import { attachCollapsible } from "@ui/ui_template_collapsible"; -import type { SpreadsheetHandle, Workbook } from "../A00_Common/spreadsheet/spreadsheet"; import { + createButton, + createInputField, + el, + showConfirmDialog, + showToast, +} from "@ui/ui_template_elements"; +import { attachCollapsible } from "@ui/ui_template_collapsible"; +import { openModal } from "@ui/ui_template_modal"; +import type { SpreadsheetHandle, Workbook } from "../A00_Common/spreadsheet/spreadsheet"; +import { type ComboSummary, fetchCombos } from "../M01_MasterData/M01_MasterData_UI_Combo_Api"; +import { + copyBasis, + copyStructure, fetchStructureNumbers, readTemplate, type StructureDoc, @@ -42,6 +54,20 @@ const TEXT = { noBasis: "산출근거 없음", noSheet: "스프레드시트를 못 띄움 — {value}", readFailed: "구조물 도면을 못 읽음 — {value}", + close: "닫기", + find: "찾기", + clear: "비움", + none: "고를 것이 없음", + pickPrice: "일위대가 고르기", + pickDrawing: "도면 복사해 가져오기", + pickBasis: "산출근거 복사해 가져오기", + combosFailed: "일위대가 목록을 못 받음 — {value}", + saveFirst: "새 열은 집계표를 먼저 저장해야 번호가 생김", + overwrite: "{from} 의 {what} 내용을 {to} 에 덮어씀 — 번호 · 열은 그대로", + drawing: "도면", + basisWord: "산출근거", + copied: "복사해 가져옴", + copyFailed: "복사 실패 — {value}", }; const why = (error: unknown): string => (error instanceof Error ? error.message : ""); @@ -49,6 +75,93 @@ const fill = (text: string, value: string): string => text.replace("{value}", va /** 스프레드시트는 산출근거 카드를 열 때만 받음 */ const loadSpreadsheet = () => import("../A00_Common/spreadsheet/spreadsheet"); +/** `{열 id: {도번, 표번}}` — 집계표 도면 · 산출근거 줄(없는 열은 키 없음 = 「없음」) */ +export type ColumnNumbers = Record; + +/** 일위대가 목록 — 처음 한 번만 받음(못 받으면 다음에 다시) */ +let combos: Promise | null = null; +const loadCombos = (): Promise => + (combos ??= fetchCombos().catch((error: unknown) => { + combos = null; + throw error; + })); + +interface Choice { + value: string; + key: string; + label: string; +} + +/** 찾기 칸 + 목록 창 — 고른 값(「비움」 = 빈 글) · 그냥 닫으면 null */ +function choose(title: string, items: Choice[], clearable: boolean): Promise { + return new Promise((resolve) => { + let done = false; + const finish = (value: string | null): void => { + if (!done) resolve(value); + done = true; + }; + openModal({ + title, + closeLabel: TEXT.close, + dialogClass: "m02-modal m02-pick", + mount: (body, close) => { + const search = createInputField({ placeholder: TEXT.find, type: "search" }); + const list = el("div", { className: "m02-pick__list" }); + const paint = (): void => { + const words = search.input.value.trim().toLowerCase().split(/\s+/).filter(Boolean); + const hits = items.filter((it) => + words.every((w) => `${it.key} ${it.label}`.toLowerCase().includes(w)), + ); + list.replaceChildren( + ...(hits.length + ? hits.map((it) => + el("button", { + className: "m02-pick__item", + attrs: { type: "button", "data-value": it.value }, + children: [el("span", { className: "m02-pick__key", text: it.key }), it.label], + }), + ) + : [el("p", { className: "m02-struct__empty", text: TEXT.none })]), + ); + }; + list.addEventListener("click", (event) => { + const item = (event.target as HTMLElement).closest("[data-value]"); + if (!item) return; + finish(item.dataset.value!); + close(); + }); + search.input.addEventListener("input", paint); + search.input.addEventListener("keydown", (event) => { + if (event.key !== "Enter") return; + list.querySelector("[data-value]")?.click(); // 첫 줄 + }); + const foot = clearable + ? [ + createButton({ + label: TEXT.clear, + variant: "ghost", + onClick: () => { + finish(""); + close(); + }, + }), + ] + : []; + body.append(search.root, list, ...foot); + paint(); + search.input.focus(); + }, + }); + // 닫기 단추 · Esc · 바깥 누르기 — 창이 빠지면 null + const watch = new MutationObserver(() => { + if (document.querySelector(".m02-pick")) return; + watch.disconnect(); + finish(null); + }); + watch.observe(document.body, { childList: true }); + }); +} + /** 집계표 열 — 머리 글(제목행 종류 · 분류1 · 분류2) · 꼴만 씀 */ export interface StructureColumn { id: string; @@ -79,17 +192,21 @@ export interface StructurePanelOptions { export interface StructurePanelHandle { root: HTMLElement; - /** `{열 id: 도번}` — 집계표 `drawingLabel` 이 씀 */ - numbers: () => Record; - /** 도번 목록 다시 받기 */ + /** `{열 id: {도번, 표번}}` — 집계표 `drawingLabel` · `basisLabel` 이 씀 */ + numbers: () => ColumnNumbers; + /** 도번 · 표번 목록 다시 받기 */ reload: () => Promise; /** 고른 열이 바뀜 — 같은 열이면 아무것도 안 함(CAD 를 다시 안 띄움) */ show: (colId: string | null, columns: StructureColumn[]) => void; + /** 집계표 조회 후 선택(`onPick`) — 일위대가 = 고른 키(「비움」 = 빈 글 · 닫으면 null) · + * 도면 · 산출근거 = 다른 열 것을 복사해 가져옴(돌려주는 글 없음) */ + pick: (row: string, colId: string | null, columns: StructureColumn[]) => Promise; destroy: () => void; } export function createStructurePanel(opts: StructurePanelOptions): StructurePanelHandle { - let numbers: Record = {}; + let numbers: ColumnNumbers = {}; + let shown: StructureColumn | null = null; let column: string | null = null; let columnLabel = ""; let doc: StructureDoc | null = null; @@ -173,7 +290,7 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane drop(); number.textContent = doc?.도번 || numbers[column ?? ""]?.도번 || TEXT.noNumber; edit.hidden = !opts.isAdmin; - basisNo.textContent = basisNumber || TEXT.noTableNumber; + basisNo.textContent = basisNumber || numbers[column ?? ""]?.표번 || TEXT.noTableNumber; editBasis.hidden = !opts.isAdmin; basisHost.replaceChildren(); void paintBasis(mine); @@ -195,6 +312,7 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane async function load(col: StructureColumn | null): Promise { const mine = ++seq; drop(); + shown = col; if (!col) { root.hidden = true; column = null; @@ -233,6 +351,72 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane void load(col); }; + const pickPrice = async (colId: string, columns: StructureColumn[]): Promise => { + let list: ComboSummary[]; + try { + list = await loadCombos(); + } catch (error) { + showToast(fill(TEXT.combosFailed, why(error)), "error"); + return null; + } + const col = columns.find((c) => c.id === colId); + const items = list.map((c) => ({ + value: c.키, + key: c.키, + // 이름에 단위가 이미 붙은 조합이 있음 — 겹치면 뺌 + label: [c.이름, c.단위 && !c.이름.includes(`(${c.단위})`) && `(${c.단위})`, c.구분] + .filter(Boolean) + .join(" "), + })); + return choose(col ? `${TEXT.pickPrice} — ${columnHead(col)}` : TEXT.pickPrice, items, true); + }; + + /** 다른 열의 도면 · 산출근거를 골라 내용만 복사 — 덮어쓰기 확인 · 보고 있던 열이면 다시 읽음 */ + const pickCopy = async ( + kind: "structure" | "basis", + colId: string, + columns: StructureColumn[], + ): Promise => { + const field = kind === "structure" ? "도번" : "표번"; + const to = columns.find((c) => c.id === colId); + const toNo = numbers[colId]?.[field]; + if (!to || !toNo) return void showToast(TEXT.saveFirst, "error"); + const items = columns + .filter((c) => c.id !== colId && numbers[c.id]?.[field]) + .map((c) => ({ value: c.id, key: numbers[c.id][field]!, label: columnHead(c) })); + const from = await choose( + `${kind === "structure" ? TEXT.pickDrawing : TEXT.pickBasis} — ${toNo} ${columnHead(to)}`, + items, + false, + ); + const src = columns.find((c) => c.id === from); + if (!from || !src) return; + const ask = TEXT.overwrite + .replace("{from}", `${numbers[from][field]} ${columnHead(src)}`) + .replace("{what}", kind === "structure" ? TEXT.drawing : TEXT.basisWord) + .replace("{to}", `${toNo} ${columnHead(to)}`); + if (!(await showConfirmDialog(ask))) return; + try { + await (kind === "structure" ? copyStructure : copyBasis)(colId, from); + showToast(TEXT.copied, "success"); + if (column === colId) void load(shown); + } catch (error) { + showToast(fill(TEXT.copyFailed, why(error)), "error"); + } + }; + + const pick = async ( + row: string, + colId: string | null, + columns: StructureColumn[], + ): Promise => { + if (!opts.isAdmin || !colId) return null; + if (row === "s:price") return pickPrice(colId, columns); + if (row === "s:drawing" || row === "s:basis") + await pickCopy(row === "s:drawing" ? "structure" : "basis", colId, columns); + return null; + }; + edit.addEventListener("click", async () => { const at = column; if (!at) return; @@ -249,11 +433,12 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane return { root, - numbers: () => Object.fromEntries(Object.entries(numbers).map(([id, n]) => [id, n.도번])), + numbers: () => numbers, reload: async () => { numbers = await fetchStructureNumbers().catch(() => ({})); }, show, + pick, destroy: () => { seq += 1; drop(); diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts index 30d0576a..8d121507 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -161,22 +161,30 @@ export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle { "createSheet", ); if (!create) return empty(L("M02_NoSheet")); - // 구조물집계표(시스템 층)만 — 도면 줄에 도번 · 칸을 고르면 아래 컨테이너 + // 구조물집계표(시스템 층)만 — 도면 · 산출근거 줄에 도번 · 표번 · 칸을 고르면 아래 컨테이너 · + // 고칠 수 있으면 일위대가 · 도면 · 산출근거 칸은 조회 후 선택 const struct = at.layer === "system" && at.name === STRUCTURE_TABLE ? buildPanel() : null; if (struct) await struct.reload(); panel = struct; + // ⚠ sheet 는 바깥(다시 열기 등으로 drop() 이 null 로 되돌리는) 값 — 표 부품이 + // 헐린 뒤에도 안 떼인 이벤트가 이 줄을 부르면 sheet?.getDoc() 가 undefined 라 + // 그 뒤 .열 을 그대로 읽으면 죽는다(2026-09-28 사용자 보고 — 열 고른 뒤 CAD + // 불러오기 오류). getDoc() 뒤에도 옵셔널 체이닝을 이어야 한다. + const columns = (): StructureColumn[] => sheet?.getDoc()?.열 ?? []; editor = create(host, doc, { mode: at.layer === "project" ? "project" : "master", onChange: () => (dirty = !readOnly), ...(struct ? { - drawingLabel: (colId: string) => struct.numbers()[colId] ?? "", - // ⚠ sheet 는 바깥(다시 열기 등으로 drop() 이 null 로 되돌리는) 값 — 표 부품이 - // 헐린 뒤에도 안 떼인 이벤트가 이 줄을 부르면 sheet?.getDoc() 가 undefined 라 - // 그 뒤 .열 을 그대로 읽으면 죽는다(2026-09-28 사용자 보고 — 열 고른 뒤 CAD - // 불러오기 오류). getDoc() 뒤에도 옵셔널 체이닝을 이어야 한다. - onSelect: (s: { colId: string | null }) => - struct.show(s.colId, sheet?.getDoc()?.열 ?? []), + drawingLabel: (colId: string) => struct.numbers()[colId]?.도번 ?? "", + basisLabel: (colId: string) => struct.numbers()[colId]?.표번 ?? "", + onSelect: (s: { colId: string | null }) => struct.show(s.colId, columns()), + ...(readOnly + ? {} + : { + onPick: (s: { row: string; colId: string | null }) => + struct.pick(s.row, s.colId, columns()), + }), } : {}), }); diff --git a/resources/master_template/basis/c1.json b/resources/master_template/basis/c1.json index d01ebfe7..19a2f61b 100644 --- a/resources/master_template/basis/c1.json +++ b/resources/master_template/basis/c1.json @@ -3,55 +3,15 @@ "판": 1, "열": "c1", "서식": [ - {}, - { - "굵게": true, - "가로": "center", - "세로": "center", - "테두리": { - "위": { - "선": "thin" - }, - "아래": { - "선": "thin" - }, - "왼": { - "선": "thin" - }, - "오른": { - "선": "thin" - } - } - } + {} ], "시트": [ { "id": "s1", "이름": "산출근거", - "칸": { - "A1": { - "값": "공종", - "서식": 1 - }, - "B1": { - "값": "규격", - "서식": 1 - }, - "C1": { - "값": "산출 내역", - "서식": 1 - }, - "D1": { - "값": "단위", - "서식": 1 - }, - "E1": { - "값": "수량(m당)", - "서식": 1 - } - } + "칸": {} } ], "활성": "s1", - "표번": "표-50" + "표번": "SC010102" } diff --git a/resources/master_template/structure/c1.json b/resources/master_template/structure/c1.json index 43947f80..a198523e 100644 --- a/resources/master_template/structure/c1.json +++ b/resources/master_template/structure/c1.json @@ -3,7 +3,7 @@ "종류": "structure", "판": 1, "열": "c1", - "도번": "구-50", + "도번": "SD010102", "도면": { "format": 6, "entities": [], diff --git a/resources/master_template/table/구조물집계표.json b/resources/master_template/table/구조물집계표.json index 35ab0983..8315bd8c 100644 --- a/resources/master_template/table/구조물집계표.json +++ b/resources/master_template/table/구조물집계표.json @@ -4,11 +4,7 @@ "판": 1, "설명": "측점마다 구조물 수량 한 줄 · 머리 4층(종류 · 공법 · 규격 · 단위) · 설계값 열은 프로젝트 설계에서 채움(화면에서 못 고침 — 설계를 고침) · 손 열은 사용자가 적음 · 계산 열은 식. 종류 정본 = B05_Profile/B05_Profile_Structure_Types.json.", "바인딩규칙": "종류 = type_id · 펼침 = 값마다 열을 나눌 칸 키(앞에 = 이면 고정 글) · 값 = 칸 키 | 개소(1건 1) | 길이(length_m, 없으면 끝-시작) | 관연장(B06 횡단 pipe_length_m) · 묶음 = 같은 펼침으로 함께 나뉘는 열 · 머리틀 = 펼친 머리({0}{1}… = 펼침 값 차례) · 이름 = 설계 값 → 머리 글 · 조건 = {칸: 값}(맞는 것만) · 더함 = 같은 열에 더하는 다른 출처 · 식틀 = 펼친 열의 식({0}… 펼침 값) · 값꼴 = 자리마다 머리 글 꼴({} = 값) · 빈값 = 설계값이 빈 자리 머리 글 · 펼침틀 = 계산 열이 같은 묶음으로 따라 나뉨(묶음 · 머리틀 · 값꼴 · 빈값 · 식틀 — 설계값을 받지 않아 바인딩 없음)", - "층": [ - "종류", - "공법", - "규격" - ], + "층": ["종류", "분류1", "분류2"], "변수": { "파형강관_1본_m": 8, "흄관_1본_m": "", @@ -18,1229 +14,217 @@ }, "쪽줄": 50, "열": [ + { "id": "no", "머리": ["NO", null, null], "단위": null, "꼴": "글", "설명": "줄 차례(전구간 줄 빼고 1부터)" }, + { "id": "sta", "머리": ["측점", null, null], "단위": null, "꼴": "글", "설명": "점 NO.x+y · 구간 NO.a~NO.b · 맨 위 전구간" }, + { - "id": "no", - "머리": [ - "NO", - null, - null - ], - "단위": null, - "꼴": "글", - "설명": "줄 차례(전구간 줄 빼고 1부터)" - }, - { - "id": "sta", - "머리": [ - "측점", - null, - null - ], - "단위": null, - "꼴": "글", - "설명": "점 NO.x+y · 구간 NO.a~NO.b · 맨 위 전구간" - }, - { - "id": "rv", - "머리": [ - "돌기슭막이", - "(형태마다)", - "H=(높이마다)" - ], - "단위": "m", - "꼴": "수", - "펼침": true, + "id": "rv", "머리": ["돌기슭막이", "(형태마다)", "H=(높이마다)"], "단위": "m", "꼴": "수", "펼침": true, "설명": "설계값 — 기슭막이 · 관 유입·유출 기슭막이 길이 · 형태 → 높이마다 열", "일위대가": "UA000004", "바인딩": { - "종류": "revetment", - "펼침": [ - "form", - "height_m" - ], - "값": "length_m", - "묶음": "rv", - "머리틀": [ - "돌기슭막이", - "{0}", - "{1}" - ], - "값꼴": [ - "{}", - "H={}" - ], - "빈값": [ - "형태 미입력", - "높이 미입력" - ], - "이름": { - "돌쌓기(찰)": "찰쌓기", - "돌쌓기(메)": "메쌓기" - }, + "종류": "revetment", "펼침": ["form", "height_m"], "값": "length_m", "묶음": "rv", + "머리틀": ["돌기슭막이", "{0}", "{1}"], "값꼴": ["{}", "H={}"], "빈값": ["형태 미입력", "높이 미입력"], + "이름": { "돌쌓기(찰)": "찰쌓기", "돌쌓기(메)": "메쌓기" }, "더함": [ - { - "종류": "pipe", - "펼침": [ - "inlet_revet_form", - "inlet_revet_height_m" - ], - "값": "inlet_revet_length_m", - "조건": { - "inlet_type": "기슭막이" - } - }, - { - "종류": "pipe", - "펼침": [ - "outlet_revet_form", - "outlet_revet_height_m" - ], - "값": "outlet_revet_length_m", - "조건": { - "outlet_type": "기슭막이" - } - } + { "종류": "pipe", "펼침": ["inlet_revet_form", "inlet_revet_height_m"], "값": "inlet_revet_length_m", "조건": { "inlet_type": "기슭막이" } }, + { "종류": "pipe", "펼침": ["outlet_revet_form", "outlet_revet_height_m"], "값": "outlet_revet_length_m", "조건": { "outlet_type": "기슭막이" } } ] } }, + + { "id": "c1", "머리": ["돌기슭막이", "(형태마다)", "새 열"], "단위": "m", "꼴": "수" }, + { - "id": "c1", - "머리": [ - "돌기슭막이", - "(형태마다)", - "새 열" - ], - "단위": "m", - "꼴": "수" - }, - { - "id": "pv_b", - "머리": [ - "콘크리트포장", - "T=(두께마다)", - "B" - ], - "단위": "m", - "꼴": "수", - "펼침": true, + "id": "pv_b", "머리": ["콘크리트포장", "T=(두께마다)", "B"], "단위": "m", "꼴": "수", "펼침": true, "설명": "설계값 — 포장 폭", - "바인딩": { - "종류": "pavement_concrete", - "펼침": [ - "thickness_cm" - ], - "값": "width_m", - "묶음": "pv", - "머리틀": [ - "콘크리트포장", - "{0}", - "B" - ], - "값꼴": [ - "T={}cm" - ], - "빈값": [ - "두께 미입력" - ] - } + "바인딩": { "종류": "pavement_concrete", "펼침": ["thickness_cm"], "값": "width_m", "묶음": "pv", "머리틀": ["콘크리트포장", "{0}", "B"], "값꼴": ["T={}cm"], "빈값": ["두께 미입력"] } }, { - "id": "pv_l", - "머리": [ - "콘크리트포장", - "T=(두께마다)", - "L" - ], - "단위": "m", - "꼴": "수", - "펼침": true, + "id": "pv_l", "머리": ["콘크리트포장", "T=(두께마다)", "L"], "단위": "m", "꼴": "수", "펼침": true, "설명": "설계값 — 포장 길이", "일위대가": "UA000007", - "바인딩": { - "종류": "pavement_concrete", - "펼침": [ - "thickness_cm" - ], - "값": "길이", - "묶음": "pv", - "머리틀": [ - "콘크리트포장", - "{0}", - "L" - ], - "값꼴": [ - "T={}cm" - ], - "빈값": [ - "두께 미입력" - ] - } + "바인딩": { "종류": "pavement_concrete", "펼침": ["thickness_cm"], "값": "길이", "묶음": "pv", "머리틀": ["콘크리트포장", "{0}", "L"], "값꼴": ["T={}cm"], "빈값": ["두께 미입력"] } }, { - "id": "pv_w", - "머리": [ - "콘크리트포장", - "T=(두께마다)", - "확폭" - ], - "단위": "㎡", - "꼴": "수", - "펼침": true, + "id": "pv_w", "머리": ["콘크리트포장", "T=(두께마다)", "확폭"], "단위": "㎡", "꼴": "수", "펼침": true, "설명": "설계값 — 확폭 면적", - "바인딩": { - "종류": "pavement_concrete", - "펼침": [ - "thickness_cm" - ], - "값": "widening_area_m2", - "묶음": "pv", - "머리틀": [ - "콘크리트포장", - "{0}", - "확폭" - ], - "값꼴": [ - "T={}cm" - ], - "빈값": [ - "두께 미입력" - ] - } + "바인딩": { "종류": "pavement_concrete", "펼침": ["thickness_cm"], "값": "widening_area_m2", "묶음": "pv", "머리틀": ["콘크리트포장", "{0}", "확폭"], "값꼴": ["T={}cm"], "빈값": ["두께 미입력"] } }, { - "id": "pv_a", - "머리": [ - "콘크리트포장", - "T=(두께마다)", - "A" - ], - "단위": "㎡", - "꼴": "수", - "펼침": true, + "id": "pv_a", "머리": ["콘크리트포장", "T=(두께마다)", "A"], "단위": "㎡", "꼴": "수", "펼침": true, "식": "IF([pv_l]>0,[pv_b]*[pv_l]+[pv_w],\"\")", - "끝수": { - "자리": 2, - "방법": "반올림" - }, + "끝수": { "자리": 2, "방법": "반올림" }, "설명": "계산 — B × L + 확폭", "일위대가": "UA000006", - "펼침틀": { - "묶음": "pv", - "머리틀": [ - "콘크리트포장", - "{0}", - "A" - ], - "값꼴": [ - "T={}cm" - ], - "빈값": [ - "두께 미입력" - ] - } + "펼침틀": { "묶음": "pv", "머리틀": ["콘크리트포장", "{0}", "A"], "값꼴": ["T={}cm"], "빈값": ["두께 미입력"] } }, { - "id": "pv_jt", - "머리": [ - "콘크리트포장", - "T=(두께마다)", - "수축줄눈" - ], - "단위": "m", - "꼴": "수", - "펼침": true, + "id": "pv_jt", "머리": ["콘크리트포장", "T=(두께마다)", "수축줄눈"], "단위": "m", "꼴": "수", "펼침": true, "식": "IF([pv_l]>0,INT([pv_l]/[$수축줄눈_간격_m])*[pv_b],\"\")", - "끝수": { - "자리": 2, - "방법": "반올림" - }, + "끝수": { "자리": 2, "방법": "반올림" }, "설명": "계산 — 내림(L ÷ 줄눈 간격) × B · 줄눈 간격 = 설계값(기본 6m · 다르면 그 줄 식에 박음)", - "펼침틀": { - "묶음": "pv", - "머리틀": [ - "콘크리트포장", - "{0}", - "수축줄눈" - ], - "값꼴": [ - "T={}cm" - ], - "빈값": [ - "두께 미입력" - ] - } + "펼침틀": { "묶음": "pv", "머리틀": ["콘크리트포장", "{0}", "수축줄눈"], "값꼴": ["T={}cm"], "빈값": ["두께 미입력"] } }, + { - "id": "pp_len", - "머리": [ - "관공", - "(관종·관경마다)", - "관매설" - ], - "단위": "m", - "꼴": "수", - "펼침": true, + "id": "pp_len", "머리": ["관공", "(관종·관경마다)", "관매설"], "단위": "m", "꼴": "수", "펼침": true, "설명": "설계값 — 횡단 관 연장(B06) · 같은 측점에 관이 둘이면 줄을 나눔", "일위대가": "UA000036", - "일위대가후보": [ - "UA000036", - "UA000027" - ], - "바인딩": { - "종류": "pipe", - "펼침": [ - "pipe_kind", - "pipe_diameter_mm" - ], - "값": "관연장", - "묶음": "pp", - "머리틀": [ - "관공", - "{0} {1}", - "관매설" - ], - "값꼴": [ - "{}", - "Φ{}" - ], - "빈값": [ - "관종 미입력", - "관경 미입력" - ] - } + "일위대가후보": ["UA000036", "UA000027"], + "바인딩": { "종류": "pipe", "펼침": ["pipe_kind", "pipe_diameter_mm"], "값": "관연장", "묶음": "pp", "머리틀": ["관공", "{0} {1}", "관매설"], "값꼴": ["{}", "Φ{}"], "빈값": ["관종 미입력", "관경 미입력"] } }, { - "id": "pp_cp", - "머리": [ - "관공", - "(관종·관경마다)", - "커플링밴드" - ], - "단위": "개소", - "꼴": "수", - "펼침": true, + "id": "pp_cp", "머리": ["관공", "(관종·관경마다)", "커플링밴드"], "단위": "개소", "꼴": "수", "펼침": true, "식": "IF([pp_len]>0,ROUNDUP([pp_len]/[$파형강관_1본_m],0)-1,\"\")", "설명": "계산 — 올림(L ÷ 1본 길이) − 1 · 관마다(줄마다) 계산 뒤 합 · 1본 길이 = 변수(관종마다 · 빈칸이면 계산 안 함)", "펼침틀": { - "묶음": "pp", - "머리틀": [ - "관공", - "{0} {1}", - "커플링밴드" - ], - "값꼴": [ - "{}", - "Φ{}" - ], - "빈값": [ - "관종 미입력", - "관경 미입력" - ], + "묶음": "pp", "머리틀": ["관공", "{0} {1}", "커플링밴드"], "값꼴": ["{}", "Φ{}"], "빈값": ["관종 미입력", "관경 미입력"], "식틀": "IF([pp_len]>0,ROUNDUP([pp_len]/[${0}_1본_m],0)-1,\"\")" } }, + { - "id": "pg_in", - "머리": [ - "관보호공", - "(형식마다)", - "유입" - ], - "단위": "개소", - "꼴": "수", - "펼침": true, + "id": "pg_in", "머리": ["관보호공", "(형식마다)", "유입"], "단위": "개소", "꼴": "수", "펼침": true, "설명": "설계값 — 관 날개벽 형식 · 유입구가 집수정이면 집수정 형식 · 형식마다 열(유출구는 기슭막이 — 돌기슭막이 열)", "일위대가": "UA000008", - "일위대가후보": [ - "UA000008", - "UA000025", - "UA000029" - ], + "일위대가후보": ["UA000008", "UA000025", "UA000029"], "바인딩": { - "종류": "pipe", - "펼침": [ - "wing_wall_type" - ], - "값": "개소", - "묶음": "pg", - "머리틀": [ - "관보호공", - "{0}", - "유입" - ], - "빈값": [ - "형식 미입력" - ], - "이름": { - "A-TYPE": "날개벽 A형", - "C-TYPE": "날개벽 C형", - "A-TYPE+집수정": "날개벽 A형+집수정" - }, - "더함": [ - { - "종류": "pipe", - "펼침": [ - "inlet_basin_form" - ], - "값": "개소", - "조건": { - "inlet_type": "집수정" - } - } - ] + "종류": "pipe", "펼침": ["wing_wall_type"], "값": "개소", "묶음": "pg", "머리틀": ["관보호공", "{0}", "유입"], "빈값": ["형식 미입력"], + "이름": { "A-TYPE": "날개벽 A형", "C-TYPE": "날개벽 C형", "A-TYPE+집수정": "날개벽 A형+집수정" }, + "더함": [ { "종류": "pipe", "펼침": ["inlet_basin_form"], "값": "개소", "조건": { "inlet_type": "집수정" } } ] } }, + { - "id": "bx_len", - "머리": [ - "BOX암거", - "(폭×높이마다)", - "연장" - ], - "단위": "m", - "꼴": "수", - "펼침": true, + "id": "bx_len", "머리": ["BOX암거", "(폭×높이마다)", "연장"], "단위": "m", "꼴": "수", "펼침": true, "설명": "설계값 — BOX 연장(계류 방향)", "일위대가": "UA000014", - "바인딩": { - "종류": "box_culvert", - "펼침": [ - "body_width_m", - "body_height_m" - ], - "값": "length_m", - "묶음": "bx", - "머리틀": [ - "BOX암거", - "{0}×{1}", - "연장" - ], - "빈값": [ - "폭 미입력", - "높이 미입력" - ] - } + "바인딩": { "종류": "box_culvert", "펼침": ["body_width_m", "body_height_m"], "값": "length_m", "묶음": "bx", "머리틀": ["BOX암거", "{0}×{1}", "연장"], "빈값": ["폭 미입력", "높이 미입력"] } }, { - "id": "bx_wing", - "머리": [ - "BOX암거", - "(폭×높이마다)", - "날개벽" - ], - "단위": "개소", - "꼴": "수", - "펼침": true, + "id": "bx_wing", "머리": ["BOX암거", "(폭×높이마다)", "날개벽"], "단위": "개소", "꼴": "수", "펼침": true, "설명": "설계값 — 날개벽 유입·유출 있음 수", "일위대가": "UA000025", "바인딩": { - "종류": "box_culvert", - "펼침": [ - "body_width_m", - "body_height_m" - ], - "값": "개소", - "묶음": "bx", - "머리틀": [ - "BOX암거", - "{0}×{1}", - "날개벽" - ], - "빈값": [ - "폭 미입력", - "높이 미입력" - ], - "조건": { - "wing_in": "있음" - }, - "더함": [ - { - "종류": "box_culvert", - "펼침": [ - "body_width_m", - "body_height_m" - ], - "값": "개소", - "조건": { - "wing_out": "있음" - } - } - ] + "종류": "box_culvert", "펼침": ["body_width_m", "body_height_m"], "값": "개소", "묶음": "bx", "머리틀": ["BOX암거", "{0}×{1}", "날개벽"], "빈값": ["폭 미입력", "높이 미입력"], + "조건": { "wing_in": "있음" }, + "더함": [ { "종류": "box_culvert", "펼침": ["body_width_m", "body_height_m"], "값": "개소", "조건": { "wing_out": "있음" } } ] } }, + { - "id": "fb", - "머리": [ - "세월교", - "(관경 × 련수마다)", - null - ], - "단위": "개소", - "꼴": "수", - "펼침": true, + "id": "fb", "머리": ["세월교", "(관경 × 련수마다)", null], "단위": "개소", "꼴": "수", "펼침": true, "설명": "설계값 — 세월교 개소", - "바인딩": { - "종류": "ford_bridge", - "펼침": [ - "pipe_diameter_mm", - "pipe_count" - ], - "값": "개소", - "묶음": "fb", - "머리틀": [ - "세월교", - "{0}×{1}", - null - ], - "값꼴": [ - "Φ{}", - "{}련" - ], - "빈값": [ - "관경 미입력", - "련수 미입력" - ] - } + "바인딩": { "종류": "ford_bridge", "펼침": ["pipe_diameter_mm", "pipe_count"], "값": "개소", "묶음": "fb", "머리틀": ["세월교", "{0}×{1}", null], "값꼴": ["Φ{}", "{}련"], "빈값": ["관경 미입력", "련수 미입력"] } }, + { - "id": "fp_w", - "머리": [ - "물넘이포장", - "T=(두께마다)", - "폭" - ], - "단위": "m", - "꼴": "수", - "펼침": true, + "id": "fp_w", "머리": ["물넘이포장", "T=(두께마다)", "폭"], "단위": "m", "꼴": "수", "펼침": true, "설명": "설계값 — 월류 폭", - "바인딩": { - "종류": "ford_pavement", - "펼침": [ - "thickness_cm" - ], - "값": "ford_width_m", - "묶음": "fp", - "머리틀": [ - "물넘이포장", - "{0}", - "폭" - ], - "값꼴": [ - "T={}cm" - ], - "빈값": [ - "두께 미입력" - ] - } + "바인딩": { "종류": "ford_pavement", "펼침": ["thickness_cm"], "값": "ford_width_m", "묶음": "fp", "머리틀": ["물넘이포장", "{0}", "폭"], "값꼴": ["T={}cm"], "빈값": ["두께 미입력"] } }, { - "id": "fp_l", - "머리": [ - "물넘이포장", - "T=(두께마다)", - "길이" - ], - "단위": "m", - "꼴": "수", - "펼침": true, + "id": "fp_l", "머리": ["물넘이포장", "T=(두께마다)", "길이"], "단위": "m", "꼴": "수", "펼침": true, "설명": "설계값 — 포장 길이(노폭 방향)", - "바인딩": { - "종류": "ford_pavement", - "펼침": [ - "thickness_cm" - ], - "값": "length_m", - "묶음": "fp", - "머리틀": [ - "물넘이포장", - "{0}", - "길이" - ], - "값꼴": [ - "T={}cm" - ], - "빈값": [ - "두께 미입력" - ] - } + "바인딩": { "종류": "ford_pavement", "펼침": ["thickness_cm"], "값": "length_m", "묶음": "fp", "머리틀": ["물넘이포장", "{0}", "길이"], "값꼴": ["T={}cm"], "빈값": ["두께 미입력"] } }, + { - "id": "od_len", - "머리": [ - "횡단개거", - "(규격마다)", - "길이" - ], - "단위": "m", - "꼴": "수", - "펼침": true, + "id": "od_len", "머리": ["횡단개거", "(규격마다)", "길이"], "단위": "m", "꼴": "수", "펼침": true, "설명": "설계값 — 개거 연장", "일위대가": "UA000026", - "바인딩": { - "종류": "open_ditch", - "펼침": [ - "ditch_spec" - ], - "값": "length_m", - "묶음": "od", - "머리틀": [ - "횡단개거", - "{0}", - "길이" - ], - "빈값": [ - "규격 미입력" - ] - } + "바인딩": { "종류": "open_ditch", "펼침": ["ditch_spec"], "값": "length_m", "묶음": "od", "머리틀": ["횡단개거", "{0}", "길이"], "빈값": ["규격 미입력"] } }, { - "id": "od_cnt", - "머리": [ - "횡단개거", - "(규격마다)", - "개소" - ], - "단위": "개소", - "꼴": "수", - "펼침": true, + "id": "od_cnt", "머리": ["횡단개거", "(규격마다)", "개소"], "단위": "개소", "꼴": "수", "펼침": true, "설명": "설계값 — 개거 · 노출형 횡단수로 개소", "일위대가": "UA000003", "바인딩": { - "종류": "open_ditch", - "펼침": [ - "ditch_spec" - ], - "값": "개소", - "묶음": "od", - "머리틀": [ - "횡단개거", - "{0}", - "개소" - ], - "빈값": [ - "규격 미입력" - ], - "더함": [ - { - "종류": "cross_drain_exposed", - "펼침": [ - "=노출형" - ], - "값": "개소" - } - ] + "종류": "open_ditch", "펼침": ["ditch_spec"], "값": "개소", "묶음": "od", "머리틀": ["횡단개거", "{0}", "개소"], "빈값": ["규격 미입력"], + "더함": [ { "종류": "cross_drain_exposed", "펼침": ["=노출형"], "값": "개소" } ] } }, + { - "id": "rw", - "머리": [ - "옹벽", - "(형식마다)", - "H=(높이마다)" - ], - "단위": "m", - "꼴": "수", - "펼침": true, + "id": "rw", "머리": ["옹벽", "(형식마다)", "H=(높이마다)"], "단위": "m", "꼴": "수", "펼침": true, "설명": "설계값 — 옹벽 길이", - "바인딩": { - "종류": "retaining_wall", - "펼침": [ - "form", - "height_m" - ], - "값": "길이", - "묶음": "rw", - "머리틀": [ - "옹벽", - "{0}", - "{1}" - ], - "값꼴": [ - "{}", - "H={}" - ], - "빈값": [ - "형식 미입력", - "높이 미입력" - ] - } + "바인딩": { "종류": "retaining_wall", "펼침": ["form", "height_m"], "값": "길이", "묶음": "rw", "머리틀": ["옹벽", "{0}", "{1}"], "값꼴": ["{}", "H={}"], "빈값": ["형식 미입력", "높이 미입력"] } }, { - "id": "ms", - "머리": [ - "돌쌓기", - "(찰·메마다)", - "H=(높이마다)" - ], - "단위": "m", - "꼴": "수", - "펼침": true, + "id": "ms", "머리": ["돌쌓기", "(찰·메마다)", "H=(높이마다)"], "단위": "m", "꼴": "수", "펼침": true, "설명": "설계값 — 돌쌓기 길이", "바인딩": { - "종류": "masonry_wet", - "펼침": [ - "=찰쌓기", - "height_m" - ], - "값": "길이", - "묶음": "ms", - "머리틀": [ - "돌쌓기", - "{0}", - "{1}" - ], - "값꼴": [ - "{}", - "H={}" - ], - "빈값": [ - "", - "높이 미입력" - ], - "더함": [ - { - "종류": "masonry_dry", - "펼침": [ - "=메쌓기", - "height_m" - ], - "값": "길이" - } - ] + "종류": "masonry_wet", "펼침": ["=찰쌓기", "height_m"], "값": "길이", "묶음": "ms", "머리틀": ["돌쌓기", "{0}", "{1}"], "값꼴": ["{}", "H={}"], "빈값": ["", "높이 미입력"], + "더함": [ { "종류": "masonry_dry", "펼침": ["=메쌓기", "height_m"], "값": "길이" } ] } }, { - "id": "sg", - "머리": [ - "흙막이", - "(형태마다)", - "H=(높이마다)" - ], - "단위": "m", - "꼴": "수", - "펼침": true, + "id": "sg", "머리": ["흙막이", "(형태마다)", "H=(높이마다)"], "단위": "m", "꼴": "수", "펼침": true, "설명": "설계값 — 흙막이 길이 · 일위대가는 큰돌만", "일위대가": "UA000032", - "바인딩": { - "종류": "soil_guard", - "펼침": [ - "form", - "height_m" - ], - "값": "길이", - "묶음": "sg", - "머리틀": [ - "흙막이", - "{0}", - "{1}" - ], - "값꼴": [ - "{}", - "H={}" - ], - "빈값": [ - "형식 미입력", - "높이 미입력" - ] - } + "바인딩": { "종류": "soil_guard", "펼침": ["form", "height_m"], "값": "길이", "묶음": "sg", "머리틀": ["흙막이", "{0}", "{1}"], "값꼴": ["{}", "H={}"], "빈값": ["형식 미입력", "높이 미입력"] } }, { - "id": "bm_len", - "머리": [ - "큰돌쌓기", - "(찰·메 · 돌 크기 · 높이마다)", - "길이" - ], - "단위": "m", - "꼴": "수", - "펼침": true, + "id": "bm_len", "머리": ["큰돌쌓기", "(찰·메 · 돌 크기 · 높이마다)", "길이"], "단위": "m", "꼴": "수", "펼침": true, "설명": "설계값 — 큰돌쌓기 길이", - "바인딩": { - "종류": "boulder_masonry", - "펼침": [ - "bond", - "stone_cm", - "height_m" - ], - "값": "길이", - "묶음": "bm", - "머리틀": [ - "큰돌쌓기", - "{0} {1} {2}", - "길이" - ], - "값꼴": [ - "{}", - "{}", - "H={}" - ], - "빈값": [ - "쌓기 방식 미입력", - "돌규격 미입력", - "높이 미입력" - ] - } + "바인딩": { "종류": "boulder_masonry", "펼침": ["bond", "stone_cm", "height_m"], "값": "길이", "묶음": "bm", "머리틀": ["큰돌쌓기", "{0} {1} {2}", "길이"], "값꼴": ["{}", "{}", "H={}"], "빈값": ["쌓기 방식 미입력", "돌규격 미입력", "높이 미입력"] } }, { - "id": "bm_area", - "머리": [ - "큰돌쌓기", - "(찰·메 · 돌 크기 · 높이마다)", - "면적" - ], - "단위": "㎡", - "꼴": "수", - "펼침": true, + "id": "bm_area", "머리": ["큰돌쌓기", "(찰·메 · 돌 크기 · 높이마다)", "면적"], "단위": "㎡", "꼴": "수", "펼침": true, "설명": "계산 — L × H (H = 펼친 높이)", "일위대가": "UA000031", "펼침틀": { - "묶음": "bm", - "머리틀": [ - "큰돌쌓기", - "{0} {1} {2}", - "면적" - ], - "값꼴": [ - "{}", - "{}", - "H={}" - ], - "빈값": [ - "쌓기 방식 미입력", - "돌규격 미입력", - "높이 미입력" - ], + "묶음": "bm", "머리틀": ["큰돌쌓기", "{0} {1} {2}", "면적"], "값꼴": ["{}", "{}", "H={}"], "빈값": ["쌓기 방식 미입력", "돌규격 미입력", "높이 미입력"], "식틀": "IF([bm_len]>0,[bm_len]*{2},\"\")" } }, + { - "id": "ec", - "머리": [ - "골막이", - "(형태마다)", - null - ], - "단위": "개소", - "꼴": "수", - "펼침": true, + "id": "ec", "머리": ["골막이", "(형태마다)", null], "단위": "개소", "꼴": "수", "펼침": true, "설명": "설계값 — 골막이 개소", - "바인딩": { - "종류": "erosion_check", - "펼침": [ - "form" - ], - "값": "개소", - "묶음": "ec", - "머리틀": [ - "골막이", - "{0}", - null - ], - "빈값": [ - "형식 미입력" - ] - } + "바인딩": { "종류": "erosion_check", "펼침": ["form"], "값": "개소", "묶음": "ec", "머리틀": ["골막이", "{0}", null], "빈값": ["형식 미입력"] } }, { - "id": "be", - "머리": [ - "소단", - null, - null - ], - "단위": "m", - "꼴": "수", + "id": "be", "머리": ["소단", null, null], "단위": "m", "꼴": "수", "설명": "설계값 — 소단 길이", - "바인딩": { - "종류": "berm", - "값": "길이" - } + "바인딩": { "종류": "berm", "값": "길이" } }, + { "id": "rf", "머리": ["대피소", null, null], "단위": "개소", "꼴": "수", "설명": "설계값 — 대피소", "바인딩": { "종류": "refuge", "값": "개소" } }, + { "id": "wy", "머리": ["정차·작업장", null, null], "단위": "개소", "꼴": "수", "설명": "설계값 — 정차·작업장", "바인딩": { "종류": "work_yard", "값": "개소" } }, + { "id": "ta", "머리": ["차돌림곳", null, null], "단위": "개소", "꼴": "수", "설명": "설계값 — 차돌림곳", "바인딩": { "종류": "turnaround", "값": "개소" } }, + { "id": "gr_rail", "머리": ["가드레일", null, null], "단위": "m", "꼴": "수", "설명": "설계값 — 가드레일 길이", "바인딩": { "종류": "guardrail", "값": "길이", "조건": { "kind": "가드레일" } } }, + { "id": "gr_curb", "머리": ["경계석", null, null], "단위": "m", "꼴": "수", "설명": "설계값 — 경계석 길이", "바인딩": { "종류": "guardrail", "값": "길이", "조건": { "kind": "경계석" } } }, + { "id": "gr_sign", "머리": ["위험표지", null, null], "단위": "개소", "꼴": "수", "설명": "설계값 — 위험표지", "일위대가": "UA000017", "바인딩": { "종류": "guardrail", "값": "개소", "조건": { "kind": "위험표지" } } }, + { "id": "mr", "머리": ["반사경", null, null], "단위": "개소", "꼴": "수", "설명": "설계값 — 반사경", "일위대가": "UA000018", "바인딩": { "종류": "mirror", "값": "개소" } }, + { "id": "bg", "머리": ["차단기", null, null], "단위": "개소", "꼴": "수", "설명": "설계값 — 차단기", "일위대가": "UA000010", "바인딩": { "종류": "barrier_gate", "값": "개소" } }, + { "id": "ps", "머리": ["국가지점번호판", null, null], "단위": "개소", "꼴": "수", "설명": "설계값 — 번호판 · 전구간 줄은 손 입력", "일위대가": "UA000016", "바인딩": { "종류": "position_sign", "값": "개소" } }, + { "id": "cs", "머리": ["준공표지석", null, null], "단위": "개소", "꼴": "수", "설명": "설계값 — 준공표지판", "일위대가": "UA000002", "바인딩": { "종류": "completion_sign", "값": "개소" } }, { - "id": "rf", - "머리": [ - "대피소", - null, - null - ], - "단위": "개소", - "꼴": "수", - "설명": "설계값 — 대피소", - "바인딩": { - "종류": "refuge", - "값": "개소" - } - }, - { - "id": "wy", - "머리": [ - "정차·작업장", - null, - null - ], - "단위": "개소", - "꼴": "수", - "설명": "설계값 — 정차·작업장", - "바인딩": { - "종류": "work_yard", - "값": "개소" - } - }, - { - "id": "ta", - "머리": [ - "차돌림곳", - null, - null - ], - "단위": "개소", - "꼴": "수", - "설명": "설계값 — 차돌림곳", - "바인딩": { - "종류": "turnaround", - "값": "개소" - } - }, - { - "id": "gr_rail", - "머리": [ - "가드레일", - null, - null - ], - "단위": "m", - "꼴": "수", - "설명": "설계값 — 가드레일 길이", - "바인딩": { - "종류": "guardrail", - "값": "길이", - "조건": { - "kind": "가드레일" - } - } - }, - { - "id": "gr_curb", - "머리": [ - "경계석", - null, - null - ], - "단위": "m", - "꼴": "수", - "설명": "설계값 — 경계석 길이", - "바인딩": { - "종류": "guardrail", - "값": "길이", - "조건": { - "kind": "경계석" - } - } - }, - { - "id": "gr_sign", - "머리": [ - "위험표지", - null, - null - ], - "단위": "개소", - "꼴": "수", - "설명": "설계값 — 위험표지", - "일위대가": "UA000017", - "바인딩": { - "종류": "guardrail", - "값": "개소", - "조건": { - "kind": "위험표지" - } - } - }, - { - "id": "mr", - "머리": [ - "반사경", - null, - null - ], - "단위": "개소", - "꼴": "수", - "설명": "설계값 — 반사경", - "일위대가": "UA000018", - "바인딩": { - "종류": "mirror", - "값": "개소" - } - }, - { - "id": "bg", - "머리": [ - "차단기", - null, - null - ], - "단위": "개소", - "꼴": "수", - "설명": "설계값 — 차단기", - "일위대가": "UA000010", - "바인딩": { - "종류": "barrier_gate", - "값": "개소" - } - }, - { - "id": "ps", - "머리": [ - "국가지점번호판", - null, - null - ], - "단위": "개소", - "꼴": "수", - "설명": "설계값 — 번호판 · 전구간 줄은 손 입력", - "일위대가": "UA000016", - "바인딩": { - "종류": "position_sign", - "값": "개소" - } - }, - { - "id": "cs", - "머리": [ - "준공표지석", - null, - null - ], - "단위": "개소", - "꼴": "수", - "설명": "설계값 — 준공표지판", - "일위대가": "UA000002", - "바인딩": { - "종류": "completion_sign", - "값": "개소" - } - }, - { - "id": "etc", - "머리": [ - "기타", - "(이름마다)", - null - ], - "단위": "개소", - "꼴": "수", - "펼침": true, + "id": "etc", "머리": ["기타", "(이름마다)", null], "단위": "개소", "꼴": "수", "펼침": true, "설명": "설계값 — 기타 구조물 개소", - "바인딩": { - "종류": "etc", - "펼침": [ - "name" - ], - "값": "개소", - "묶음": "etc", - "머리틀": [ - "기타", - "{0}", - null - ], - "빈값": [ - "이름 미입력" - ] - } + "바인딩": { "종류": "etc", "펼침": ["name"], "값": "개소", "묶음": "etc", "머리틀": ["기타", "{0}", null], "빈값": ["이름 미입력"] } }, - { - "id": "h_intake", - "머리": [ - "취수정", - null, - null - ], - "단위": "개소", - "꼴": "수", - "손": true, - "설명": "손 입력", - "일위대가": "UA000005" - }, - { - "id": "h_rip_c", - "머리": [ - "돌붙임", - "찰붙임", - null - ], - "단위": "㎡", - "꼴": "수", - "손": true, - "설명": "손 입력", - "일위대가": "UA000009" - }, - { - "id": "h_rip_m", - "머리": [ - "돌붙임", - "메붙임", - null - ], - "단위": "㎡", - "꼴": "수", - "손": true, - "설명": "손 입력", - "일위대가": "UA000009" - }, - { - "id": "h_stone_ch", - "머리": [ - "돌수로", - null, - null - ], - "단위": "m", - "꼴": "수", - "손": true, - "설명": "손 입력", - "일위대가": "UA000011" - }, - { - "id": "h_stone_bed", - "머리": [ - "돌조공", - null, - null - ], - "단위": "m", - "꼴": "수", - "손": true, - "설명": "손 입력", - "일위대가": "UA000023" - }, - { - "id": "h_pv_rail", - "머리": [ - "포장 난간", - null, - null - ], - "단위": "m", - "꼴": "수", - "손": true, - "설명": "손 입력", - "일위대가": "UA000012" - }, - { - "id": "h_pg_rail", - "머리": [ - "관보호공 안전난간", - null, - null - ], - "단위": "개소", - "꼴": "수", - "손": true, - "설명": "손 입력", - "일위대가": "UA000013" - }, - { - "id": "h_soil_ditch", - "머리": [ - "수로형 토사개거", - null, - null - ], - "단위": "개소", - "꼴": "수", - "손": true, - "설명": "손 입력", - "일위대가": "UA000033" - }, - { - "id": "h_ford_basin", - "머리": [ - "물넘이 집수부", - null, - null - ], - "단위": "개소", - "꼴": "수", - "손": true, - "설명": "손 입력", - "일위대가": "UA000034" - }, - { - "id": "h_rockfall", - "머리": [ - "낙석방지책", - null, - null - ], - "단위": "경간", - "꼴": "수", - "손": true, - "설명": "손 입력", - "일위대가": null - }, - { - "id": "h_break", - "머리": [ - "콘크리트·포장 깨기", - null, - null - ], - "단위": "㎥", - "꼴": "수", - "손": true, - "설명": "손 입력", - "일위대가": null - }, - { - "id": "h_waste", - "머리": [ - "폐기물처리", - null, - null - ], - "단위": "ton", - "꼴": "수", - "손": true, - "설명": "손 입력", - "일위대가": null - }, - { - "id": "memo", - "머리": [ - "비고", - null, - null - ], - "단위": null, - "꼴": "글", - "손": true, - "설명": "손 입력" - } + + { "id": "h_intake", "머리": ["취수정", null, null], "단위": "개소", "꼴": "수", "손": true, "설명": "손 입력", "일위대가": "UA000005" }, + { "id": "h_rip_c", "머리": ["돌붙임", "찰붙임", null], "단위": "㎡", "꼴": "수", "손": true, "설명": "손 입력", "일위대가": "UA000009" }, + { "id": "h_rip_m", "머리": ["돌붙임", "메붙임", null], "단위": "㎡", "꼴": "수", "손": true, "설명": "손 입력", "일위대가": "UA000009" }, + { "id": "h_stone_ch", "머리": ["돌수로", null, null], "단위": "m", "꼴": "수", "손": true, "설명": "손 입력", "일위대가": "UA000011" }, + { "id": "h_stone_bed", "머리": ["돌조공", null, null], "단위": "m", "꼴": "수", "손": true, "설명": "손 입력", "일위대가": "UA000023" }, + { "id": "h_pv_rail", "머리": ["포장 난간", null, null], "단위": "m", "꼴": "수", "손": true, "설명": "손 입력", "일위대가": "UA000012" }, + { "id": "h_pg_rail", "머리": ["관보호공 안전난간", null, null], "단위": "개소", "꼴": "수", "손": true, "설명": "손 입력", "일위대가": "UA000013" }, + { "id": "h_soil_ditch", "머리": ["수로형 토사개거", null, null], "단위": "개소", "꼴": "수", "손": true, "설명": "손 입력", "일위대가": "UA000033" }, + { "id": "h_ford_basin", "머리": ["물넘이 집수부", null, null], "단위": "개소", "꼴": "수", "손": true, "설명": "손 입력", "일위대가": "UA000034" }, + { "id": "h_rockfall", "머리": ["낙석방지책", null, null], "단위": "경간", "꼴": "수", "손": true, "설명": "손 입력", "일위대가": null }, + { "id": "h_break", "머리": ["콘크리트·포장 깨기", null, null], "단위": "㎥", "꼴": "수", "손": true, "설명": "손 입력", "일위대가": null }, + { "id": "h_waste", "머리": ["폐기물처리", null, null], "단위": "ton", "꼴": "수", "손": true, "설명": "손 입력", "일위대가": null }, + + { "id": "memo", "머리": ["비고", null, null], "단위": null, "꼴": "글", "손": true, "설명": "손 입력" } ], "줄": [ - { - "id": "all", - "값": { - "sta": "전구간" - }, - "고정": "전구간" - } + { "id": "all", "값": { "sta": "전구간" }, "고정": "전구간" } ], "합계줄": [ - { - "id": "sum", - "이름": "합계", - "식": "SUM" - } + { "id": "sum", "이름": "합계", "식": "SUM" } ], - "보기": { - "틀고정": { - "열": 2 - }, - "열너비": { - "no": 40, - "sta": 150, - "memo": 120 - } - } + "보기": { "틀고정": { "열": 2 }, "열너비": { "no": 40, "sta": 150, "memo": 120 } } } diff --git a/resources/tester/spreadsheet/spreadsheet_grid_test.html b/resources/tester/spreadsheet/spreadsheet_grid_test.html index f393d1d4..4d3a65df 100644 --- a/resources/tester/spreadsheet/spreadsheet_grid_test.html +++ b/resources/tester/spreadsheet/spreadsheet_grid_test.html @@ -1,8 +1,9 @@ - + C 격자 시험 틀 +