From cd824aca251d1da129a225a8780f8f61f2dec0a8 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 27 Sep 2026 20:12:39 +0900 Subject: [PATCH] =?UTF-8?q?feat(spreadsheet):=20F=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EC=9E=AC=EA=B3=84=EC=82=B0=20=C2=B7=20=EC=82=B0=EC=B6=9C?= =?UTF-8?q?=EA=B7=BC=EA=B1=B0=20=EB=94=B0=EB=A1=9C=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=C2=B7=20M02=20=EB=81=BC=EC=9A=B0=EA=B8=B0=20=C2=B7=20=EA=B8=88?= =?UTF-8?q?=EB=B9=9B=20=EC=8B=9C=ED=97=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - common_util_spreadsheet_node.ts · common_util_spreadsheet.py · build:spreadsheet — [저장] 때 화면과 같은 TS 엔진을 Node 로 돌려 계산값 정본(브라우저 값 버림 · 못 풀면 503) - M02 새 종류 basis — resources/master_template/basis/<열 id>.json(구조물집계표 열마다) · 층 복사 · 뼈대 검사 · 옛 구조물 문서 산출근거 49 벌 옮김(migrate_basis) · 새 열도 같이 만듦 - 상세 산출근거 카드 = 새 스프레드시트(import() 로 늦게 받음) · basis 읽기 · 저장 - ⚠ 임시 [스프레드시트 시험] 단추 — 빈 통합문서(시트 셋) · 저장 자리 tmp/spreadsheet_trial.json · 1단계 검증 뒤 지움 - 시험 resources/tester/spreadsheet/ — 저장 · 서버 다리 · 크기 예산 · M02 만 늦게 불러옴 · 실무 구조도 xlsx 세 벌 수식 2,699 칸 엑셀 캐시값 대조(엔진 빈 몸이면 건너뜀) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0168FQuoV7vDh5nhnSowW5cp --- .gitignore | 2 + .../M02_MasterTemplete_Api_Fetch.ts | 15 +- .../M02_MasterTemplete_Router.py | 35 +++++ .../M02_MasterTemplete_Router_Layers.py | 7 + .../M02_MasterTemplete_Spreadsheet_Trial.css | 25 ++++ .../M02_MasterTemplete_Spreadsheet_Trial.ts | 89 +++++++++++ .../M02_MasterTemplete_Store.py | 133 ++++++++++++++--- .../M02_MasterTemplete_Structure.ts | 86 ++++++----- .../M02_MasterTemplete_UI_Main.ts | 19 ++- M02_MasterTemplete/M02_Template_Layers.py | 16 +- common_util/common_util_spreadsheet.py | 56 +++++++ common_util/common_util_spreadsheet_node.ts | 33 +++++ package.json | 3 +- resources/master_template/basis/be.json | 56 +++++++ resources/master_template/basis/bg.json | 56 +++++++ resources/master_template/basis/bm_area.json | 56 +++++++ resources/master_template/basis/bm_len.json | 56 +++++++ resources/master_template/basis/bx_len.json | 56 +++++++ resources/master_template/basis/bx_wing.json | 56 +++++++ resources/master_template/basis/cs.json | 56 +++++++ resources/master_template/basis/ec.json | 56 +++++++ resources/master_template/basis/etc.json | 56 +++++++ resources/master_template/basis/fb.json | 56 +++++++ resources/master_template/basis/fp_l.json | 56 +++++++ resources/master_template/basis/fp_w.json | 56 +++++++ resources/master_template/basis/gr_curb.json | 56 +++++++ resources/master_template/basis/gr_rail.json | 56 +++++++ resources/master_template/basis/gr_sign.json | 56 +++++++ resources/master_template/basis/h_break.json | 56 +++++++ .../master_template/basis/h_ford_basin.json | 56 +++++++ resources/master_template/basis/h_intake.json | 56 +++++++ .../master_template/basis/h_pg_rail.json | 56 +++++++ .../master_template/basis/h_pv_rail.json | 56 +++++++ resources/master_template/basis/h_rip_c.json | 56 +++++++ resources/master_template/basis/h_rip_m.json | 56 +++++++ .../master_template/basis/h_rockfall.json | 56 +++++++ .../master_template/basis/h_soil_ditch.json | 56 +++++++ .../master_template/basis/h_stone_bed.json | 56 +++++++ .../master_template/basis/h_stone_ch.json | 56 +++++++ resources/master_template/basis/h_waste.json | 56 +++++++ resources/master_template/basis/memo.json | 56 +++++++ resources/master_template/basis/mr.json | 56 +++++++ resources/master_template/basis/ms.json | 56 +++++++ resources/master_template/basis/no.json | 56 +++++++ resources/master_template/basis/od_cnt.json | 56 +++++++ resources/master_template/basis/od_len.json | 56 +++++++ resources/master_template/basis/pg_in.json | 56 +++++++ resources/master_template/basis/pp_cp.json | 56 +++++++ resources/master_template/basis/pp_len.json | 56 +++++++ resources/master_template/basis/ps.json | 56 +++++++ resources/master_template/basis/pv_a.json | 56 +++++++ resources/master_template/basis/pv_b.json | 56 +++++++ resources/master_template/basis/pv_jt.json | 56 +++++++ resources/master_template/basis/pv_l.json | 56 +++++++ resources/master_template/basis/pv_w.json | 56 +++++++ resources/master_template/basis/rf.json | 56 +++++++ resources/master_template/basis/rv.json | 56 +++++++ resources/master_template/basis/rw.json | 56 +++++++ resources/master_template/basis/sg.json | 56 +++++++ resources/master_template/basis/sta.json | 56 +++++++ resources/master_template/basis/ta.json | 56 +++++++ resources/master_template/basis/wy.json | 56 +++++++ resources/master_template/structure/be.json | 61 -------- resources/master_template/structure/bg.json | 61 -------- .../master_template/structure/bm_area.json | 61 -------- .../master_template/structure/bm_len.json | 61 -------- .../master_template/structure/bx_len.json | 61 -------- .../master_template/structure/bx_wing.json | 61 -------- resources/master_template/structure/cs.json | 61 -------- resources/master_template/structure/ec.json | 61 -------- resources/master_template/structure/etc.json | 61 -------- resources/master_template/structure/fb.json | 61 -------- resources/master_template/structure/fp_l.json | 61 -------- resources/master_template/structure/fp_w.json | 61 -------- .../master_template/structure/gr_curb.json | 61 -------- .../master_template/structure/gr_rail.json | 61 -------- .../master_template/structure/gr_sign.json | 61 -------- .../master_template/structure/h_break.json | 61 -------- .../structure/h_ford_basin.json | 61 -------- .../master_template/structure/h_intake.json | 61 -------- .../master_template/structure/h_pg_rail.json | 61 -------- .../master_template/structure/h_pv_rail.json | 61 -------- .../master_template/structure/h_rip_c.json | 61 -------- .../master_template/structure/h_rip_m.json | 61 -------- .../master_template/structure/h_rockfall.json | 61 -------- .../structure/h_soil_ditch.json | 61 -------- .../structure/h_stone_bed.json | 61 -------- .../master_template/structure/h_stone_ch.json | 61 -------- .../master_template/structure/h_waste.json | 61 -------- resources/master_template/structure/memo.json | 61 -------- resources/master_template/structure/mr.json | 61 -------- resources/master_template/structure/ms.json | 61 -------- resources/master_template/structure/no.json | 61 -------- .../master_template/structure/od_cnt.json | 61 -------- .../master_template/structure/od_len.json | 61 -------- .../master_template/structure/pg_in.json | 61 -------- .../master_template/structure/pp_cp.json | 61 -------- .../master_template/structure/pp_len.json | 61 -------- resources/master_template/structure/ps.json | 61 -------- resources/master_template/structure/pv_a.json | 61 -------- resources/master_template/structure/pv_b.json | 61 -------- .../master_template/structure/pv_jt.json | 61 -------- resources/master_template/structure/pv_l.json | 61 -------- resources/master_template/structure/pv_w.json | 61 -------- resources/master_template/structure/rf.json | 61 -------- resources/master_template/structure/rv.json | 61 -------- resources/master_template/structure/rw.json | 61 -------- resources/master_template/structure/sg.json | 61 -------- resources/master_template/structure/sta.json | 61 -------- resources/master_template/structure/ta.json | 61 -------- resources/master_template/structure/wy.json | 61 -------- .../tester/spreadsheet/spreadsheet_size.mjs | 56 +++++++ .../spreadsheet/test_spreadsheet_golden.py | 129 ++++++++++++++++ .../spreadsheet/test_spreadsheet_node.py | 37 +++++ .../spreadsheet/test_spreadsheet_size.py | 74 ++++++++++ .../spreadsheet/test_spreadsheet_store.py | 138 ++++++++++++++++++ resources/tester/test_m02_structure.py | 17 +-- resources/tester/test_m02_structure_panel.py | 4 +- 118 files changed, 3642 insertions(+), 3065 deletions(-) create mode 100644 M02_MasterTemplete/M02_MasterTemplete_Spreadsheet_Trial.css create mode 100644 M02_MasterTemplete/M02_MasterTemplete_Spreadsheet_Trial.ts create mode 100644 common_util/common_util_spreadsheet.py create mode 100644 common_util/common_util_spreadsheet_node.ts create mode 100644 resources/master_template/basis/be.json create mode 100644 resources/master_template/basis/bg.json create mode 100644 resources/master_template/basis/bm_area.json create mode 100644 resources/master_template/basis/bm_len.json create mode 100644 resources/master_template/basis/bx_len.json create mode 100644 resources/master_template/basis/bx_wing.json create mode 100644 resources/master_template/basis/cs.json create mode 100644 resources/master_template/basis/ec.json create mode 100644 resources/master_template/basis/etc.json create mode 100644 resources/master_template/basis/fb.json create mode 100644 resources/master_template/basis/fp_l.json create mode 100644 resources/master_template/basis/fp_w.json create mode 100644 resources/master_template/basis/gr_curb.json create mode 100644 resources/master_template/basis/gr_rail.json create mode 100644 resources/master_template/basis/gr_sign.json create mode 100644 resources/master_template/basis/h_break.json create mode 100644 resources/master_template/basis/h_ford_basin.json create mode 100644 resources/master_template/basis/h_intake.json create mode 100644 resources/master_template/basis/h_pg_rail.json create mode 100644 resources/master_template/basis/h_pv_rail.json create mode 100644 resources/master_template/basis/h_rip_c.json create mode 100644 resources/master_template/basis/h_rip_m.json create mode 100644 resources/master_template/basis/h_rockfall.json create mode 100644 resources/master_template/basis/h_soil_ditch.json create mode 100644 resources/master_template/basis/h_stone_bed.json create mode 100644 resources/master_template/basis/h_stone_ch.json create mode 100644 resources/master_template/basis/h_waste.json create mode 100644 resources/master_template/basis/memo.json create mode 100644 resources/master_template/basis/mr.json create mode 100644 resources/master_template/basis/ms.json create mode 100644 resources/master_template/basis/no.json create mode 100644 resources/master_template/basis/od_cnt.json create mode 100644 resources/master_template/basis/od_len.json create mode 100644 resources/master_template/basis/pg_in.json create mode 100644 resources/master_template/basis/pp_cp.json create mode 100644 resources/master_template/basis/pp_len.json create mode 100644 resources/master_template/basis/ps.json create mode 100644 resources/master_template/basis/pv_a.json create mode 100644 resources/master_template/basis/pv_b.json create mode 100644 resources/master_template/basis/pv_jt.json create mode 100644 resources/master_template/basis/pv_l.json create mode 100644 resources/master_template/basis/pv_w.json create mode 100644 resources/master_template/basis/rf.json create mode 100644 resources/master_template/basis/rv.json create mode 100644 resources/master_template/basis/rw.json create mode 100644 resources/master_template/basis/sg.json create mode 100644 resources/master_template/basis/sta.json create mode 100644 resources/master_template/basis/ta.json create mode 100644 resources/master_template/basis/wy.json create mode 100644 resources/tester/spreadsheet/spreadsheet_size.mjs create mode 100644 resources/tester/spreadsheet/test_spreadsheet_golden.py create mode 100644 resources/tester/spreadsheet/test_spreadsheet_node.py create mode 100644 resources/tester/spreadsheet/test_spreadsheet_size.py create mode 100644 resources/tester/spreadsheet/test_spreadsheet_store.py diff --git a/.gitignore b/.gitignore index 0f615447..3853c111 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,8 @@ config/corridor_node/ config/server_calc_node/ # 구조물도 식 풀이 번들 — `npm run build:formula` 산출물(2026-09-13). config/formula_node/ +# 산출근거 스프레드시트 재계산 번들 — `npm run build:spreadsheet` 산출물. +config/spreadsheet_node/ # graphify 위키 산출물 — 창끼리 같은 위키를 찾게 **통째로** git 으로 나름 # (2026-09-09 사용자 확정 「출력물 전체를 공유해도 됨」). diff --git a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts index 68dd9ca7..8a142780 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts @@ -7,8 +7,9 @@ import { API_BASE_URL } from "@config/config_frontend"; /** 층 이름 — 서버와 같은 글 */ export type Layer = "system" | "company" | "personal" | "project"; -/** 서버 종류 — 화면의 「표 양식」 = table · 「도면 양식」 = drawing · 「구조물 도면」 = structure */ -export type Kind = "table" | "drawing" | "structure"; +/** 서버 종류 — 화면의 「표 양식」 = table · 「도면 양식」 = drawing · 「구조물 도면」 = structure · + * 「상세 산출근거」 = basis(구조물집계표 열마다 한 통합문서) */ +export type Kind = "table" | "drawing" | "structure" | "basis"; export interface TemplateInfo { 종류: Kind; @@ -95,7 +96,7 @@ export const deleteLayerTemplate = ( /* --- 구조물 도면(종류 structure · 이름 = 구조물집계표 열 id) --- * 읽기 · 저장 · 지우기는 위 readTemplate · saveTemplate · deleteTemplate 에 "structure" */ -/** 구조물 도면 문서 — `도면` = 도면 양식과 같은 CAD 문서 · `산출근거` = 표 양식과 같은 표 문서 */ +/** 구조물 도면 문서 — `도면` = 도면 양식과 같은 CAD 문서 · 산출근거는 따로(`basis` · 같은 열 id) */ export interface StructureDoc { 양식: "구조물도면"; 종류: "structure"; @@ -103,14 +104,13 @@ export interface StructureDoc { 열: string; 도번: string; 도면: { entities: unknown[] } & Record; - 산출근거: { 열: unknown[]; 줄: unknown[] } & Record; } /** 시스템 층 구조물 도면 목록 */ export const listStructures = async (): Promise => (await listTemplates("system", null)).filter((t) => t.종류 === "structure"); -/** 새로 — 도번 자동 · A1 도각을 깐 빈 도면 + 빈 산출근거 표 · 이미 있으면 StaleError */ +/** 새로 — 도번 자동 · 빈 도면 (+ 없으면 빈 산출근거 통합문서) · 이미 있으면 StaleError */ export const createStructure = (column: string): Promise => call(`/structures/${enc(column)}`, { method: "POST" }); @@ -118,6 +118,11 @@ export const createStructure = (column: string): Promise => export const fetchStructureNumbers = (): Promise> => call("/structures/numbers"); +/* --- ⚠ 임시 — [스프레드시트 시험] 단추(1단계 검증 뒤 지움) · 저장 자리 = 서버 tmp 견본 --- */ +export const readSpreadsheetTrial = (): Promise<{ 문서: unknown }> => call("/spreadsheet-trial"); +export const saveSpreadsheetTrial = (문서: unknown): Promise<{ 문서: unknown }> => + call("/spreadsheet-trial", { method: "PUT", body: JSON.stringify({ 문서 }) }); + /* --- 프로젝트 층 단추 다섯 --- */ const project = (id: string, tail: string): string => `/projects/${enc(id)}/templates/${tail}`; const post = (path: string, body: object = {}): Promise => diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router.py b/M02_MasterTemplete/M02_MasterTemplete_Router.py index 54a4c74f..764fa989 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router.py @@ -5,11 +5,15 @@ from __future__ import annotations +import json +from pathlib import Path from typing import Any from fastapi import APIRouter, HTTPException from pydantic import BaseModel +from common_util import common_util_spreadsheet as spreadsheet +from common_util.common_util_json import atomic_write_json from M02_MasterTemplete import M02_MasterTemplete_Store as store router = APIRouter(prefix="/api/m02", tags=["M02 MasterTemplete"]) @@ -57,3 +61,34 @@ def put_template(kind: str, name: str, body: SaveBody) -> dict: def delete_template(kind: str, name: str, 판: str | None = None) -> dict: _call(store.delete, kind, name, 판) return {"ok": True} + + +# ── ⚠ 임시(스프레드시트 시험 단추 · 사용자 지시 · 1단계 검증 뒤 지움) ────────── +# 저장 자리 = `tmp/spreadsheet_trial.json`(git 밖) · 없으면 시트 셋 빈 통합문서. +TRIAL_PATH = Path(__file__).resolve().parent.parent / "tmp" / "spreadsheet_trial.json" + + +def _trial_doc() -> dict[str, Any]: + sheets = [{"id": f"s{i}", "이름": f"시트{i}", "칸": {}} for i in (1, 2, 3)] + return {"종류": "통합문서", "판": 1, "열": "시험", "서식": [{}], "시트": sheets, "활성": "s1"} + + +@router.get("/spreadsheet-trial") +def get_spreadsheet_trial() -> dict: + if not TRIAL_PATH.is_file(): + return {"문서": _trial_doc()} + return {"문서": json.loads(TRIAL_PATH.read_text(encoding="utf-8"))} + + +class TrialBody(BaseModel): + 문서: dict[str, Any] + + +@router.put("/spreadsheet-trial") +def put_spreadsheet_trial(body: TrialBody) -> dict: + try: + doc = spreadsheet.with_server_values(body.문서) + except spreadsheet.RecalcError as e: + raise HTTPException(status_code=503, detail=str(e)) from e + atomic_write_json(TRIAL_PATH, doc) + return {"문서": doc} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py index a2730e68..8ae50715 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py @@ -22,6 +22,7 @@ from B06_Section.B06_Section_Repository import ( get_cross_section_designs, get_workflow_route_context, ) +from common_util import common_util_spreadsheet as spreadsheet from common_util.common_util_auth import verify_session from common_util.common_util_storage import resolve_stored_project_path from config.config_db import get_db_pool, run_with_connection @@ -260,6 +261,12 @@ async def save_layer_template( if kind == "table" and fill.is_fillable(document): # 채운 표가 와도 설계값은 안 받아 적음 — 양식 + 손 값만(5장 · 브라우저 값을 믿지 않음) document = fill.strip_design(document) + if kind == "basis": + # 산출근거 계산값은 서버가 같은 TS 엔진을 Node 로 돌려 새로 적음(브라우저 값 버림 · 5장 ②) + try: + document = await asyncio.to_thread(spreadsheet.with_server_values, document) + except spreadsheet.RecalcError as error: + raise HTTPException(status_code=503, detail=str(error)) from error try: version = await asyncio.to_thread( layers.write_template, diff --git a/M02_MasterTemplete/M02_MasterTemplete_Spreadsheet_Trial.css b/M02_MasterTemplete/M02_MasterTemplete_Spreadsheet_Trial.css new file mode 100644 index 00000000..aca49b93 --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_Spreadsheet_Trial.css @@ -0,0 +1,25 @@ +/* ⚠ 임시 — [스프레드시트 시험] 화면(1단계 검증 뒤 지움) */ +.m02-trial { + display: flex; + flex-direction: column; + gap: 8px; + width: 100%; +} + +.m02-trial__bar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.m02-trial__note { + color: var(--color-text-muted); + font-size: 0.85rem; +} + +.m02-trial__sheet { + width: 100%; + height: 75vh; + min-height: 420px; +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Spreadsheet_Trial.ts b/M02_MasterTemplete/M02_MasterTemplete_Spreadsheet_Trial.ts new file mode 100644 index 00000000..7d655959 --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_Spreadsheet_Trial.ts @@ -0,0 +1,89 @@ +/* ============================================================================= + * M02_MasterTemplete_Spreadsheet_Trial.ts + * ⚠ 임시 — [스프레드시트 시험] 단추(사용자 지시 · 스프레드시트 1단계 검증 뒤 지움: + * 이 파일 · `M02_MasterTemplete_UI_Main.ts` 의 시험 단추 · Api_Fetch `*SpreadsheetTrial` · + * Router `/spreadsheet-trial` · 저장 자리 `tmp/spreadsheet_trial.json`). + * 빈 통합문서(시트 셋)를 메인 칸에 넓게 엶 · [시험 저장] = 서버가 같은 엔진으로 다시 풀어 tmp 에 적음. + * 글자는 이 파일에 둠. + * ========================================================================== */ + +import { createButton, el, showToast } from "@ui/ui_template_elements"; +import type { SpreadsheetHandle, Workbook } from "../A00_Common/spreadsheet/spreadsheet"; +import { readSpreadsheetTrial, saveSpreadsheetTrial } from "./M02_MasterTemplete_Api_Fetch"; +import "./M02_MasterTemplete_Spreadsheet_Trial.css"; + +const TEXT = { + save: "시험 저장", + note: "임시 시험 화면 — 저장은 서버 tmp 견본 자리(작업 양식과 무관)", + saved: "시험 통합문서를 저장했음(서버 재계산)", + failed: "스프레드시트 시험 실패 — {value}", +}; + +const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); + +export interface TrialHandle { + getDoc: () => unknown; + destroy: () => void; +} + +/** `onDirty` = 고침 있음 · 저장 뒤 없음(페이지의 떠날 때 물음과 이음) */ +export function mountSpreadsheetTrial( + host: HTMLElement, + onDirty: (dirty: boolean) => void, +): TrialHandle { + let sheet: SpreadsheetHandle | null = null; + let alive = true; + const save = createButton({ label: TEXT.save, variant: "filled" }); + const area = el("div", { className: "m02-trial__sheet" }); + const root = el("div", { + className: "m02-trial", + children: [ + el("div", { + className: "m02-trial__bar", + children: [el("span", { className: "m02-trial__note", text: TEXT.note }), save], + }), + area, + ], + }); + host.replaceChildren(root); + const fail = (error: unknown): void => + showToast(TEXT.failed.replace("{value}", why(error)), "error"); + + void (async () => { + try { + const [{ createSpreadsheet }, got] = await Promise.all([ + import("../A00_Common/spreadsheet/spreadsheet"), + readSpreadsheetTrial(), + ]); + if (!alive) return; + sheet = createSpreadsheet(area, got.문서 as Workbook, { onChange: () => onDirty(true) }); + } catch (error) { + if (!alive) return; + area.replaceChildren(el("p", { className: "m02-trial__empty", text: why(error) })); + fail(error); + } + })(); + + save.addEventListener("click", async () => { + if (!sheet) return; + save.disabled = true; + try { + await saveSpreadsheetTrial(sheet.getDoc()); + onDirty(false); + showToast(TEXT.saved, "success"); + } catch (error) { + fail(error); + } finally { + save.disabled = false; + } + }); + + return { + getDoc: () => sheet?.getDoc() ?? null, + destroy: () => { + alive = false; + sheet?.destroy(); + root.remove(); + }, + }; +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Store.py b/M02_MasterTemplete/M02_MasterTemplete_Store.py index 4522430b..7879945c 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Store.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Store.py @@ -14,14 +14,22 @@ from datetime import datetime from pathlib import Path from typing import Any +from common_util import common_util_spreadsheet as spreadsheet from common_util.common_util_json import atomic_write_json from M02_MasterTemplete import M02_Template_Layers as layers FOLDER: Path = Path(__file__).resolve().parent.parent / "resources" / "master_template" KINDS = layers.KINDS # 시험은 FOLDER 를 사본으로 바꿈 # 구조물 도면 = 구조물집계표 열 id 마다 하나(파일 이름 = 열 id) · 도각 없는 전용 화면(그린 만큼이 설계 영역) +# 산출근거 = 같은 열 id 로 따로 한 파일(`basis/<열 id>.json` · 스프레드시트 통합문서) STRUCTURE_TABLE = "구조물집계표" -_BASIS_HEAD = (("work", "공종"), ("spec", "규격"), ("detail", "산출 내역"), ("unit", "단위")) +_BASIS_HEAD = ("공종", "규격", "산출 내역", "단위", "수량(m당)") +_BASIS_HEAD_STYLE = { + "굵게": True, + "가로": "center", + "세로": "center", + "테두리": {side: {"선": "thin"} for side in ("위", "아래", "왼", "오른")}, +} _NUMBER = re.compile(r"^구-(\d+)$") _BAD_NAME = re.compile(r'[\\/:*?"<>|\x00-\x1f]|\.\.') # ponytail: 저장은 한 번에 하나(프로세스 안 잠금) · 서버를 여럿 띄우면 파일 잠금으로 @@ -80,10 +88,14 @@ def _check_skeleton(kind: str, doc: Any) -> None: def write(kind: str, name: str, version: str, doc: Any) -> dict[str, Any]: """`version` 이 빈 글이면 새로 만듦(이미 있으면 409) · 아니면 그 판일 때만 덮어씀. - 구조물집계표를 저장하면 — 새 열의 빈 구조물 도면도 같이 만듦(잠금 밖에서 · `create_structure` - 가 다시 잠금을 잡으므로 안에서 부르면 죽음).""" + 구조물집계표를 저장하면 — 새 열의 빈 구조물 도면 · 산출근거도 같이 만듦(잠금 밖에서 · + `create_structure` 가 다시 잠금을 잡으므로 안에서 부르면 죽음). + 산출근거(basis)는 서버가 같은 TS 엔진을 Node 로 돌려 `계산값` 을 새로 적음 + (브라우저 값 버림 · 5장 ②).""" path = _path(kind, name) _check_skeleton(kind, doc) + if kind == "basis": + doc = _server_values(doc) with _LOCK: have = version_of(path.read_bytes()) if path.is_file() else "" if have != (version or ""): @@ -125,16 +137,17 @@ def _number(doc: Any) -> int: return int(found.group(1)) if found else 0 -def create_structure(column: str) -> dict[str, Any]: - """열 id 하나의 구조물 도면 새로 — 도번 = 있는 것 중 가장 큰 번호 + 1 · 이미 있으면 409.""" - path = _path("structure", column) +def _column_ids() -> list[str]: table = read("table", STRUCTURE_TABLE)["문서"] - if column not in {col.get("id") for col in table.get("열", []) if isinstance(col, dict)}: + return [col["id"] for col in table.get("열", []) if isinstance(col, dict) and col.get("id")] + + +def create_structure(column: str) -> dict[str, Any]: + """열 id 하나의 구조물 도면 새로 — 도번 = 있는 것 중 가장 큰 번호 + 1 · 이미 있으면 409. + 산출근거 파일이 없으면 빈 통합문서도 같이 만듦.""" + path = _path("structure", column) + if column not in _column_ids(): raise StoreError(404, f"{STRUCTURE_TABLE}에 없는 열 「{column}」") - basis = [{"id": key, "머리": [label], "단위": None, "꼴": "글"} for key, label in _BASIS_HEAD] - basis.append({"id": "qty", "머리": ["수량(m당)"], "단위": None, "꼴": "수"}) - # 빈 줄 몇 개를 미리 둠 — 「줄 더하기」 안 눌러도 바로 입력하게 - basis_rows = [{"id": str(i), "값": {}} for i in range(1, 4)] with _LOCK: if path.is_file(): raise StoreError(409, f"이미 있는 구조물 도면 「{column}」") @@ -146,21 +159,107 @@ def create_structure(column: str) -> dict[str, Any]: "열": column, "도번": f"구-{number:02d}", "도면": {"format": 6, "entities": []}, - "산출근거": {"양식": "산출근거", "종류": "표", "판": 1, "열": basis, "줄": basis_rows}, } atomic_write_json(path, doc) + ensure_basis(column) return read("structure", column) +# ── 산출근거 ────────────────────────────────────────── + + +def _col_letters(index: int) -> str: + letters = "" + index += 1 + while index: + index, rest = divmod(index - 1, 26) + letters = chr(65 + rest) + letters + return letters + + +def basis_doc(column: str, old: Any = None) -> dict[str, Any]: + """빈 산출근거 통합문서 — 1 행 머리(공종 · 규격 · 산출 내역 · 단위 · 수량(m당)). + `old` = 옛 구조물 문서 `산출근거` 표(열 · 줄) — 머리 글 · 칸 값 · 열 폭을 옮김 + (옛 열 식은 없어 안 옮김).""" + columns = old.get("열") if isinstance(old, dict) and isinstance(old.get("열"), list) else None + if columns: + heads = [ + " ".join(str(h) for h in (c.get("머리") or []) if h) or c.get("id", "") for c in columns + ] + else: + heads, columns = list(_BASIS_HEAD), [] + cells: dict[str, Any] = { + f"{_col_letters(i)}1": {"값": head, "서식": 1} for i, head in enumerate(heads) if head + } + for r, row in enumerate(old.get("줄", []) if columns else [], start=2): + values = row.get("값", {}) if isinstance(row, dict) else {} + for i, col in enumerate(columns): + value = values.get(col.get("id")) + if value not in (None, ""): + cells[f"{_col_letters(i)}{r}"] = {"값": value} + widths = ((old or {}).get("보기") or {}).get("열너비") or {} if columns else {} + # 옛 폭은 px · 새 폭은 엑셀 글자 단위(숫자 폭 7px 어림) + info = { + _col_letters(i): {"폭": round(widths[c["id"]] / 7, 1)} + for i, c in enumerate(columns) + if isinstance(widths.get(c.get("id")), (int, float)) + } + sheet: dict[str, Any] = {"id": "s1", "이름": "산출근거", "칸": cells} + if info: + sheet["열"] = info + return { + "종류": "통합문서", + "판": 1, + "열": column, + "서식": [{}, _BASIS_HEAD_STYLE], + "시트": [sheet], + "활성": "s1", + } + + +def ensure_basis(column: str, old: Any = None) -> bool: + """산출근거 파일이 없으면 만듦(식 없는 새 문서라 `계산값` 없음 · 첫 [저장] 때 서버가 적음).""" + path = _path("basis", column) + with _LOCK: + if path.is_file(): + return False + atomic_write_json(path, basis_doc(column, old)) + return True + + +def _server_values(doc: Any) -> dict[str, Any]: + try: + return spreadsheet.with_server_values(doc) + except spreadsheet.RecalcError as e: + raise StoreError(503, str(e)) from e + + +def migrate_basis() -> dict[str, list[str]]: + """한 번 — 옛 구조물 문서 안 `산출근거` 표를 `basis/<열 id>.json` 으로 옮기고 + 구조물 문서에서 뺌 · 그 뒤 산출근거가 없는 집계표 열은 빈 통합문서. + 두 번 돌려도 같음(있는 파일은 안 덮음).""" + moved: list[str] = [] + for name, doc in _structures(): + if not isinstance(doc, dict) or "산출근거" not in doc: + continue + ensure_basis(name, doc["산출근거"]) + rest = {key: value for key, value in doc.items() if key != "산출근거"} + with _LOCK: + atomic_write_json(_path("structure", name), rest) + moved.append(name) + made = [column for column in _column_ids() if ensure_basis(column)] + return {"옮김": moved, "새로": made} + + def backfill_missing_structures() -> list[str]: - """구조물집계표 열마다 빈 구조물 도면이 있게 — 없는 열만 열 차례로 새로 만듦(도번 이어서). + """구조물집계표 열마다 빈 구조물 도면 · 산출근거가 있게 — 없는 열만 열 차례로 새로 + 만듦(도번 이어서). 그 사이 남이 만들었거나(409) 열 규칙에 안 맞으면(404) 그 열만 건너뜀.""" - table = read("table", STRUCTURE_TABLE)["문서"] have = {name for name, _ in _structures()} made: list[str] = [] - for col in table.get("열", []): - cid = col.get("id") if isinstance(col, dict) else None - if not cid or cid in have: + for cid in _column_ids(): + if cid in have: + ensure_basis(cid) continue try: create_structure(cid) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts index 0dee58a3..f6d2ac29 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts @@ -1,7 +1,9 @@ /* ============================================================================= * M02_MasterTemplete_Structure.ts - * 구조물집계표 아래 설계 컨테이너 — 고른 열의 구조물 도면 미리보기 · [도면 수정] · 상세 산출근거 표. + * 구조물집계표 아래 설계 컨테이너 — 고른 열의 구조물 도면 미리보기 · [도면 수정] · 상세 산출근거. * 카드 둘(도면 정보 · 상세 산출근거)은 좌측 패널과 같은 공용 ui-sidebar 틀. + * 상세 산출근거 = 스프레드시트(`A00_Common/spreadsheet/`) · 열마다 따로 파일(`basis/<열 id>.json`) · + * 부품은 이 카드를 열 때만 `import()` 로 받음(다른 페이지 번들에 안 섞임) · [산출근거 저장] 때 서버가 다시 풂. * * 구조물 도면은 집계표 열마다 하나(`resources/master_template/structure/<열 id>.json`) · * 서버가 열마다 미리 만들어 둠(도번 자동 · 새 열도 집계표 저장 때 서버가 같이 만듦 — `M02_MasterTemplete_Store.py`). @@ -11,7 +13,7 @@ import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; import { attachCollapsible } from "@ui/ui_template_collapsible"; -import { createSheet, type SheetDoc, type SheetHandle } from "@ui/sheet/ui_template_sheet"; +import type { SpreadsheetHandle, Workbook } from "../A00_Common/spreadsheet/spreadsheet"; import { fetchStructureNumbers, readTemplate, @@ -36,17 +38,20 @@ const TEXT = { basis: "상세 산출근거", noNumber: "도번 없음", noDrawing: "도면 없음", - noBasis: "표 없음 — 도면이 아직 없음", + noBasis: "산출근거 없음", + noSheet: "스프레드시트를 못 띄움 — {value}", saved: "산출근거를 저장했음", dirty: "저장 안 한 산출근거 고침이 있음 — 버리고 넘어갈까?", kept: "열을 바꾸지 않음 — 산출근거를 먼저 저장할 것", - stale: "그 사이 구조물 도면이 바뀜 — 열을 다시 고를 것", + stale: "그 사이 산출근거가 바뀜 — 열을 다시 고를 것", readFailed: "구조물 도면을 못 읽음 — {value}", saveFailed: "저장 못 함 — {value}", }; const why = (error: unknown): string => (error instanceof Error ? error.message : ""); const fill = (text: string, value: string): string => text.replace("{value}", value); +/** 스프레드시트는 산출근거 카드를 열 때만 받음 */ +const loadSpreadsheet = () => import("../A00_Common/spreadsheet/spreadsheet"); /** 집계표 열 — 머리 글만 씀 */ export interface StructureColumn { @@ -83,9 +88,10 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane let column: string | null = null; let columnLabel = ""; let doc: StructureDoc | null = null; - let version = ""; + let basisDoc: Workbook | null = null; + let basisVersion = ""; let preview: DrawingTemplateHandle | null = null; - let basis: SheetHandle | null = null; + let basis: SpreadsheetHandle | null = null; let basisDirty = false; let busy = false; let seq = 0; @@ -145,19 +151,34 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane apply(); }; - /** 미리보기 · 산출근거 표 — 문서가 없으면 없음 글(서버가 열마다 미리 만들어 둬 보통은 없음) */ - const paintBody = (): void => { + /** 산출근거 스프레드시트 — 부품을 늦게 받음 · 그 사이 열이 바뀌면 버림 */ + const paintBasis = async (mine: number): Promise => { + if (!basisDoc) return void basisHost.replaceChildren(note(TEXT.noBasis)); + try { + const { createSpreadsheet } = await loadSpreadsheet(); + if (mine !== seq || !basisDoc) return; + basis = createSpreadsheet(basisHost, basisDoc, { + readOnly: !opts.isAdmin, + onChange: () => (basisDirty = true), + }); + } catch (error) { + if (mine === seq) basisHost.replaceChildren(note(fill(TEXT.noSheet, why(error)))); + } + }; + + /** 미리보기 · 산출근거 — 문서가 없으면 없음 글(서버가 열마다 미리 만들어 둬 보통은 없음) */ + const paintBody = (mine: number): void => { drop(); number.textContent = doc?.도번 || numbers[column ?? ""] || TEXT.noNumber; edit.hidden = !opts.isAdmin; - saveBasis.hidden = !opts.isAdmin || !doc; + saveBasis.hidden = !opts.isAdmin || !basisDoc; + basisHost.replaceChildren(); + void paintBasis(mine); if (!doc) { previewHost.replaceChildren(note(TEXT.noDrawing)); - basisHost.replaceChildren(note(TEXT.noBasis)); return; } previewHost.replaceChildren(); - basisHost.replaceChildren(); // 미리보기 — 도면 양식과 같은 부품을 읽기만으로. 자동백업 칸은 편집 화면과 갈라 둠. preview = mountDrawingTemplate(previewHost, doc.도면 as unknown as DrawingTemplateDoc, { readOnly: true, @@ -166,10 +187,6 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane }); const frame = previewHost.querySelector("iframe"); if (frame) hideToolbar(frame); - basis = createSheet(basisHost, doc.산출근거 as unknown as SheetDoc, { - mode: "master", - onChange: () => (basisDirty = true), - }); }; async function load(col: StructureColumn | null): Promise { @@ -187,20 +204,23 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane columnName.textContent = columnLabel; number.textContent = numbers[col.id] ?? TEXT.noNumber; doc = null; - version = ""; + basisDoc = null; + basisVersion = ""; previewHost.replaceChildren(); basisHost.replaceChildren(); - try { - const got = await readTemplate("system", "structure", col.id, null); - if (mine !== seq) return; - doc = got.문서 as StructureDoc; - version = got.판 ?? ""; - } catch (error) { - if (mine !== seq) return; - // 도번 목록에 있는 열인데 못 읽으면 알림 — 없는 열은 그냥 「도면 없음」 - if (numbers[col.id]) showToast(fill(TEXT.readFailed, why(error)), "error"); + const [drawing, sheet] = await Promise.allSettled([ + readTemplate("system", "structure", col.id, null), + readTemplate("system", "basis", col.id, null), + ]); + if (mine !== seq) return; + if (drawing.status === "fulfilled") doc = drawing.value.문서 as StructureDoc; + // 도번 목록에 있는 열인데 못 읽으면 알림 — 없는 열은 그냥 「도면 없음」 + else if (numbers[col.id]) showToast(fill(TEXT.readFailed, why(drawing.reason)), "error"); + if (sheet.status === "fulfilled") { + basisDoc = sheet.value.문서 as Workbook; + basisVersion = sheet.value.판 ?? ""; } - paintBody(); + paintBody(mine); } const show = (colId: string | null, columns: StructureColumn[]): void => { @@ -216,17 +236,15 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane saveBasis.addEventListener("click", async () => { const at = column; - if (!at || !doc || !basis || busy) return; + if (!at || !basis || busy) return; busy = true; saveBasis.disabled = true; try { - const next: StructureDoc = { - ...doc, - 산출근거: basis.getDoc() as unknown as StructureDoc["산출근거"], - }; - const info = await saveTemplate("system", "structure", at, null, version, next); - version = info.판; - doc = next; + // 계산값은 서버가 같은 엔진으로 다시 풀어 적음(보낸 값은 버림) + const next = basis.getDoc(); + const info = await saveTemplate("system", "basis", at, null, basisVersion, next); + basisVersion = info.판; + basisDoc = next; basisDirty = false; showToast(TEXT.saved, "success"); } catch (error) { diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts index 20328a0f..392e8db2 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -22,6 +22,8 @@ import { type StructureColumn, type StructurePanelHandle, } from "./M02_MasterTemplete_Structure"; +// ⚠ 임시 — [스프레드시트 시험] 단추(1단계 검증 뒤 지움) +import { mountSpreadsheetTrial } from "./M02_MasterTemplete_Spreadsheet_Trial"; export interface Selection { layer: Layer; @@ -77,6 +79,9 @@ export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle { const title = el("h2", { className: "m02-main__title", text: L("M02_PickTemplate") }); const reload = createButton({ label: L("M02_Reload"), variant: "ghost" }); const save = createButton({ label: L("M02_Save"), variant: "filled" }); + // ⚠ 임시 — [스프레드시트 시험] 단추(사용자 지시 · 1단계 검증 뒤 지움) + const trial = createButton({ label: "스프레드시트 시험", variant: "ghost" }); + trial.hidden = !isAdmin; const slot = el("div", { className: "m02-main__slot" }); // 도면 양식의 작도 영역 칸이 들어옴 const notice = el("div", { className: "m02-main__notice", attrs: { hidden: "" } }); const host = el("div", { className: "m02-main__host" }); @@ -84,7 +89,7 @@ export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle { const root = el("div", { className: "m02-main", children: [ - el("div", { className: "m02-main__head", children: [title, slot, reload, save] }), + el("div", { className: "m02-main__head", children: [title, slot, reload, save, trial] }), notice, host, extra, @@ -269,6 +274,18 @@ export function createMain(isAdmin: boolean, onSaved?: () => void): MainHandle { } save.addEventListener("click", () => void doSave()); + // ⚠ 임시 — [스프레드시트 시험] 빈 통합문서(시트 셋)를 메인 칸에 넓게 엶 · 저장은 서버 tmp 견본 자리 + trial.addEventListener("click", async () => { + if (!(await confirmLeave())) return; + seq += 1; + dirty = false; + drop(); + showNotice([]); + sel = null; + bar(); + title.textContent = "스프레드시트 시험"; + editor = mountSpreadsheetTrial(host, (d) => (dirty = d)); + }); reload.addEventListener("click", async () => { if (sel && (await confirmLeave())) void open(sel); }); diff --git a/M02_MasterTemplete/M02_Template_Layers.py b/M02_MasterTemplete/M02_Template_Layers.py index 9c889c25..e3f3060b 100644 --- a/M02_MasterTemplete/M02_Template_Layers.py +++ b/M02_MasterTemplete/M02_Template_Layers.py @@ -1,10 +1,10 @@ """M02 양식 층 — 네 층의 자리 · 읽기 · 쓰기 · 복사 · manifest. 층 넷 (PLAN 10-5): - system `resources/master_template/{table,drawing,structure}/<이름>.json` (git) - company `storage/{회사}/templates/{table,drawing,structure}/` - personal `storage/{회사}/{사용자}/templates/{table,drawing,structure}/` - project `storage/{회사}/{사용자}/{프로젝트}/templates/{table,drawing,structure}/` + system `resources/master_template/{table,drawing,structure,basis}/<이름>.json` (git) + company `storage/{회사}/templates/{table,drawing,structure,basis}/` + personal `storage/{회사}/{사용자}/templates/{table,drawing,structure,basis}/` + project `storage/{회사}/{사용자}/{프로젝트}/templates/{table,drawing,structure,basis}/` + 초기 사본 `templates/_initial/` - 층 폴더마다 `manifest.json` — 그 폴더에 든 양식의 출처 `{"table/이름": {층, 이름, 판, 적용일}}`. @@ -26,7 +26,8 @@ from common_util.common_util_json import atomic_write_json from config import config_system LAYERS = ("system", "company", "personal", "project") -KINDS = ("table", "drawing", "structure") +# basis = 산출근거 통합문서(구조물집계표 열마다 · 스프레드시트 `A00_Common/spreadsheet/`) +KINDS = ("table", "drawing", "structure", "basis") TEMPLATES_DIRNAME = "templates" INITIAL_DIRNAME = "_initial" MANIFEST_NAME = "manifest.json" @@ -165,13 +166,14 @@ def read_template(layer_dir: str | Path, kind: str, name: str) -> dict[str, Any] _SKELETON = { "table": ("열",), "drawing": ("entities",), - "structure": ("도면.entities", "산출근거.열"), + "structure": ("도면.entities",), + "basis": ("시트",), } def check_skeleton(kind: str, document: Any) -> None: """뼈대 없는 문서는 거절 — 표 = `열` 목록 · 도면 = `entities` 목록 · - 구조물 도면 = `도면.entities` · `산출근거.열` 목록(빈 `{}` 저장 막기).""" + 구조물 도면 = `도면.entities` 목록 · 산출근거 = `시트` 목록(빈 `{}` 저장 막기).""" if not isinstance(document, dict): raise ValueError("양식 문서는 JSON 객체여야 합니다.") for key in _SKELETON.get(kind, ()): diff --git a/common_util/common_util_spreadsheet.py b/common_util/common_util_spreadsheet.py new file mode 100644 index 00000000..17044da6 --- /dev/null +++ b/common_util/common_util_spreadsheet.py @@ -0,0 +1,56 @@ +"""산출근거 스프레드시트(통합문서) 서버 재계산 — 화면과 같은 TS 엔진을 Node 로 돌리는 껍데기. + +CLAUDE.md 5장 ② — 계산은 `A00_Common/spreadsheet/` 한 벌. 여기는 번들을 부르기만 함 +(`npm run build:spreadsheet` → `config/spreadsheet_node/`). [저장] 때 브라우저가 보낸 `계산값` 은 +버리고 이 결과를 정본으로 적음. + +결과 한 권 = `{시트 id: {A1: {수|글|참|오류}}}`(식 칸만) · 수는 십진 글. +""" + +from __future__ import annotations + +import copy +from typing import Any + +from common_util.common_util_node_bundle import ROOT, build_bundle, run_bundle_json + +BUNDLE = ROOT / "config" / "spreadsheet_node" / "common_util_spreadsheet_node.js" +NPM_SCRIPT = "build:spreadsheet" +ENTRY = ROOT / "common_util" / "common_util_spreadsheet_node.ts" +SOURCES = (ROOT / "A00_Common" / "spreadsheet", ROOT / "ui_template" / "sheet") + + +class RecalcError(Exception): + """번들 빌드 · 실행 실패 또는 엔진이 그 통합문서를 못 풂 — 저장을 막음(검산 없이 적지 않음).""" + + +def _stale() -> bool: + """번들이 없거나 엔진 TS 원본보다 오래됐으면 참 — 낡으면 화면과 서버가 다른 값을 냄.""" + if not BUNDLE.is_file(): + return True + built_at = BUNDLE.stat().st_mtime + paths = [ENTRY, *(p for folder in SOURCES for p in folder.glob("*.ts"))] + return any(p.stat().st_mtime > built_at for p in paths) + + +def recalc_workbooks(books: list[dict[str, Any]]) -> list[dict[str, Any]]: + """통합문서 여럿 → 입력 차례 그대로 `계산값`. 하나라도 못 풀면 `RecalcError`.""" + if _stale() and not build_bundle(NPM_SCRIPT): + raise RecalcError("스프레드시트 계산 번들을 못 만듦") + out = run_bundle_json(BUNDLE, NPM_SCRIPT, {"books": books}) + if out is None: + raise RecalcError("스프레드시트 계산 번들 실행 실패") + values: list[dict[str, Any]] = [] + for result in out["results"]: + if not result.get("ok"): + raise RecalcError(f"스프레드시트 계산 실패 — {result.get('까닭')}") + values.append(result["계산값"]) + return values + + +def with_server_values(book: dict[str, Any]) -> dict[str, Any]: + """[저장] 직전 — 브라우저 `계산값` 은 버리고 서버 풀이로 새로 적은 사본.""" + clean = copy.deepcopy(book) + clean.pop("계산값", None) + clean["계산값"] = recalc_workbooks([clean])[0] + return clean diff --git a/common_util/common_util_spreadsheet_node.ts b/common_util/common_util_spreadsheet_node.ts new file mode 100644 index 00000000..420f7448 --- /dev/null +++ b/common_util/common_util_spreadsheet_node.ts @@ -0,0 +1,33 @@ +/* ============================================================================= + * common_util_spreadsheet_node.ts + * 산출근거 스프레드시트 풀이를 **서버가** 돌리는 진입점 — [저장] 때 정본 `계산값`. + * + * 풀이는 화면이 쓰는 `A00_Common/spreadsheet/spreadsheet_recalc.ts` 그대로 — 여기에는 계산이 없음(CLAUDE.md 5장 ②). + * 파이썬 껍데기 = `common_util_spreadsheet.py` · 번들 = `npm run build:spreadsheet`. + * + * 실행: node <번들> <입력.json> <출력.json> + * 입력 { books: Workbook[] } + * 출력 { results: ({ ok: true, 계산값 } | { ok: false, 까닭 })[] } — 입력 차례 그대로 · 한 권이 죽어도 나머지는 풂 + * 끝 코드: 0 성공 / 2 인자 오류 + * ⚠ `A00_Common/spreadsheet/` 밖에 둠 — node:fs 가 브라우저 번들로 새지 않게. + * ========================================================================== */ + +import { readFileSync, writeFileSync } from "node:fs"; +import { recalcWorkbook } from "../A00_Common/spreadsheet/spreadsheet_recalc"; +import type { Workbook } from "../A00_Common/spreadsheet/spreadsheet_types"; + +const [inputPath, outputPath] = process.argv.slice(2); +if (!inputPath || !outputPath) { + console.error("사용법: node <번들> <입력.json> <출력.json>"); + process.exit(2); +} + +const input = JSON.parse(readFileSync(inputPath, "utf8")) as { books?: Workbook[] }; +const results = (input.books ?? []).map((book) => { + try { + return { ok: true, 계산값: recalcWorkbook(book) }; + } catch (error) { + return { ok: false, 까닭: error instanceof Error ? error.message : String(error) }; + } +}); +writeFileSync(outputPath, JSON.stringify({ results })); diff --git a/package.json b/package.json index 16499d54..f1f0ca81 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,11 @@ "type": "module", "scripts": { "dev": "node ./config/node_modules/vite/bin/vite.js --configLoader runner", - "build": "node ./config/node_modules/typescript/bin/tsc --noEmit && node ./config/node_modules/vite/bin/vite.js build --configLoader runner && npm run build:corridor && npm run build:server-calc && npm run build:formula && npm run build:b07-cad", + "build": "node ./config/node_modules/typescript/bin/tsc --noEmit && node ./config/node_modules/vite/bin/vite.js build --configLoader runner && npm run build:corridor && npm run build:server-calc && npm run build:formula && npm run build:spreadsheet && npm run build:b07-cad", "build:corridor": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../B05_Profile/B05_Profile_Corridor_Node.ts --outDir ../config/corridor_node", "build:server-calc": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../B06_Section/B06_Section_Server_Calc_Node.ts --outDir ../config/server_calc_node", "build:formula": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../common_util/common_util_sheet_recalc_node.ts --outDir ../config/formula_node", + "build:spreadsheet": "node ./config/node_modules/vite/bin/vite.js build --configLoader runner --ssr ../common_util/common_util_spreadsheet_node.ts --outDir ../config/spreadsheet_node", "install:b07-cad": "npm --prefix B07_DesignDetail/openwebcad install", "build:b07-cad": "npm run install:b07-cad && npm --prefix B07_DesignDetail/openwebcad run build", "preview": "node ./config/node_modules/vite/bin/vite.js preview --configLoader runner", diff --git a/resources/master_template/basis/be.json b/resources/master_template/basis/be.json new file mode 100644 index 00000000..d144a756 --- /dev/null +++ b/resources/master_template/basis/be.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "be", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/bg.json b/resources/master_template/basis/bg.json new file mode 100644 index 00000000..2cfbf5e9 --- /dev/null +++ b/resources/master_template/basis/bg.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "bg", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/bm_area.json b/resources/master_template/basis/bm_area.json new file mode 100644 index 00000000..6f2104cb --- /dev/null +++ b/resources/master_template/basis/bm_area.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "bm_area", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/bm_len.json b/resources/master_template/basis/bm_len.json new file mode 100644 index 00000000..6c485795 --- /dev/null +++ b/resources/master_template/basis/bm_len.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "bm_len", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/bx_len.json b/resources/master_template/basis/bx_len.json new file mode 100644 index 00000000..7f77b27a --- /dev/null +++ b/resources/master_template/basis/bx_len.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "bx_len", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/bx_wing.json b/resources/master_template/basis/bx_wing.json new file mode 100644 index 00000000..abb5866d --- /dev/null +++ b/resources/master_template/basis/bx_wing.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "bx_wing", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/cs.json b/resources/master_template/basis/cs.json new file mode 100644 index 00000000..c7e4c54e --- /dev/null +++ b/resources/master_template/basis/cs.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "cs", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/ec.json b/resources/master_template/basis/ec.json new file mode 100644 index 00000000..6fb922db --- /dev/null +++ b/resources/master_template/basis/ec.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "ec", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/etc.json b/resources/master_template/basis/etc.json new file mode 100644 index 00000000..39b122f6 --- /dev/null +++ b/resources/master_template/basis/etc.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "etc", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/fb.json b/resources/master_template/basis/fb.json new file mode 100644 index 00000000..fdf75c78 --- /dev/null +++ b/resources/master_template/basis/fb.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "fb", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/fp_l.json b/resources/master_template/basis/fp_l.json new file mode 100644 index 00000000..94ea26a3 --- /dev/null +++ b/resources/master_template/basis/fp_l.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "fp_l", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/fp_w.json b/resources/master_template/basis/fp_w.json new file mode 100644 index 00000000..6bef5748 --- /dev/null +++ b/resources/master_template/basis/fp_w.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "fp_w", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/gr_curb.json b/resources/master_template/basis/gr_curb.json new file mode 100644 index 00000000..a7c3407c --- /dev/null +++ b/resources/master_template/basis/gr_curb.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "gr_curb", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/gr_rail.json b/resources/master_template/basis/gr_rail.json new file mode 100644 index 00000000..6599a2fe --- /dev/null +++ b/resources/master_template/basis/gr_rail.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "gr_rail", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/gr_sign.json b/resources/master_template/basis/gr_sign.json new file mode 100644 index 00000000..3d05704a --- /dev/null +++ b/resources/master_template/basis/gr_sign.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "gr_sign", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/h_break.json b/resources/master_template/basis/h_break.json new file mode 100644 index 00000000..ae8cc151 --- /dev/null +++ b/resources/master_template/basis/h_break.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "h_break", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/h_ford_basin.json b/resources/master_template/basis/h_ford_basin.json new file mode 100644 index 00000000..29d20574 --- /dev/null +++ b/resources/master_template/basis/h_ford_basin.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "h_ford_basin", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/h_intake.json b/resources/master_template/basis/h_intake.json new file mode 100644 index 00000000..d072f999 --- /dev/null +++ b/resources/master_template/basis/h_intake.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "h_intake", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/h_pg_rail.json b/resources/master_template/basis/h_pg_rail.json new file mode 100644 index 00000000..ac1d69c2 --- /dev/null +++ b/resources/master_template/basis/h_pg_rail.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "h_pg_rail", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/h_pv_rail.json b/resources/master_template/basis/h_pv_rail.json new file mode 100644 index 00000000..665550f5 --- /dev/null +++ b/resources/master_template/basis/h_pv_rail.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "h_pv_rail", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/h_rip_c.json b/resources/master_template/basis/h_rip_c.json new file mode 100644 index 00000000..ce2cb1b8 --- /dev/null +++ b/resources/master_template/basis/h_rip_c.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "h_rip_c", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/h_rip_m.json b/resources/master_template/basis/h_rip_m.json new file mode 100644 index 00000000..623be3b5 --- /dev/null +++ b/resources/master_template/basis/h_rip_m.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "h_rip_m", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/h_rockfall.json b/resources/master_template/basis/h_rockfall.json new file mode 100644 index 00000000..aaa0e2c1 --- /dev/null +++ b/resources/master_template/basis/h_rockfall.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "h_rockfall", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/h_soil_ditch.json b/resources/master_template/basis/h_soil_ditch.json new file mode 100644 index 00000000..c5b9a2c0 --- /dev/null +++ b/resources/master_template/basis/h_soil_ditch.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "h_soil_ditch", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/h_stone_bed.json b/resources/master_template/basis/h_stone_bed.json new file mode 100644 index 00000000..dafb2577 --- /dev/null +++ b/resources/master_template/basis/h_stone_bed.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "h_stone_bed", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/h_stone_ch.json b/resources/master_template/basis/h_stone_ch.json new file mode 100644 index 00000000..1158d797 --- /dev/null +++ b/resources/master_template/basis/h_stone_ch.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "h_stone_ch", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/h_waste.json b/resources/master_template/basis/h_waste.json new file mode 100644 index 00000000..a1aa8cd2 --- /dev/null +++ b/resources/master_template/basis/h_waste.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "h_waste", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/memo.json b/resources/master_template/basis/memo.json new file mode 100644 index 00000000..76fca18d --- /dev/null +++ b/resources/master_template/basis/memo.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "memo", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/mr.json b/resources/master_template/basis/mr.json new file mode 100644 index 00000000..7d90ba69 --- /dev/null +++ b/resources/master_template/basis/mr.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "mr", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/ms.json b/resources/master_template/basis/ms.json new file mode 100644 index 00000000..81cb2717 --- /dev/null +++ b/resources/master_template/basis/ms.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "ms", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/no.json b/resources/master_template/basis/no.json new file mode 100644 index 00000000..771a4fc4 --- /dev/null +++ b/resources/master_template/basis/no.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "no", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/od_cnt.json b/resources/master_template/basis/od_cnt.json new file mode 100644 index 00000000..00a55396 --- /dev/null +++ b/resources/master_template/basis/od_cnt.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "od_cnt", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/od_len.json b/resources/master_template/basis/od_len.json new file mode 100644 index 00000000..71215e1b --- /dev/null +++ b/resources/master_template/basis/od_len.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "od_len", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/pg_in.json b/resources/master_template/basis/pg_in.json new file mode 100644 index 00000000..44eed2ae --- /dev/null +++ b/resources/master_template/basis/pg_in.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "pg_in", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/pp_cp.json b/resources/master_template/basis/pp_cp.json new file mode 100644 index 00000000..623cae69 --- /dev/null +++ b/resources/master_template/basis/pp_cp.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "pp_cp", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/pp_len.json b/resources/master_template/basis/pp_len.json new file mode 100644 index 00000000..812d3076 --- /dev/null +++ b/resources/master_template/basis/pp_len.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "pp_len", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/ps.json b/resources/master_template/basis/ps.json new file mode 100644 index 00000000..48ca7945 --- /dev/null +++ b/resources/master_template/basis/ps.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "ps", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/pv_a.json b/resources/master_template/basis/pv_a.json new file mode 100644 index 00000000..fe322750 --- /dev/null +++ b/resources/master_template/basis/pv_a.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "pv_a", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/pv_b.json b/resources/master_template/basis/pv_b.json new file mode 100644 index 00000000..71ade0af --- /dev/null +++ b/resources/master_template/basis/pv_b.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "pv_b", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/pv_jt.json b/resources/master_template/basis/pv_jt.json new file mode 100644 index 00000000..ed4e5ef6 --- /dev/null +++ b/resources/master_template/basis/pv_jt.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "pv_jt", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/pv_l.json b/resources/master_template/basis/pv_l.json new file mode 100644 index 00000000..a5d588e3 --- /dev/null +++ b/resources/master_template/basis/pv_l.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "pv_l", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/pv_w.json b/resources/master_template/basis/pv_w.json new file mode 100644 index 00000000..08406dc4 --- /dev/null +++ b/resources/master_template/basis/pv_w.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "pv_w", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/rf.json b/resources/master_template/basis/rf.json new file mode 100644 index 00000000..e57337fd --- /dev/null +++ b/resources/master_template/basis/rf.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "rf", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/rv.json b/resources/master_template/basis/rv.json new file mode 100644 index 00000000..fba5401b --- /dev/null +++ b/resources/master_template/basis/rv.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "rv", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/rw.json b/resources/master_template/basis/rw.json new file mode 100644 index 00000000..047f1e02 --- /dev/null +++ b/resources/master_template/basis/rw.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "rw", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/sg.json b/resources/master_template/basis/sg.json new file mode 100644 index 00000000..b974fcce --- /dev/null +++ b/resources/master_template/basis/sg.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "sg", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/sta.json b/resources/master_template/basis/sta.json new file mode 100644 index 00000000..16bf443b --- /dev/null +++ b/resources/master_template/basis/sta.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "sta", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/ta.json b/resources/master_template/basis/ta.json new file mode 100644 index 00000000..41ddd730 --- /dev/null +++ b/resources/master_template/basis/ta.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "ta", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/basis/wy.json b/resources/master_template/basis/wy.json new file mode 100644 index 00000000..e551e5e8 --- /dev/null +++ b/resources/master_template/basis/wy.json @@ -0,0 +1,56 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "wy", + "서식": [ + {}, + { + "굵게": true, + "가로": "center", + "세로": "center", + "테두리": { + "위": { + "선": "thin" + }, + "아래": { + "선": "thin" + }, + "왼": { + "선": "thin" + }, + "오른": { + "선": "thin" + } + } + } + ], + "시트": [ + { + "id": "s1", + "이름": "산출근거", + "칸": { + "A1": { + "값": "공종", + "서식": 1 + }, + "B1": { + "값": "규격", + "서식": 1 + }, + "C1": { + "값": "산출 내역", + "서식": 1 + }, + "D1": { + "값": "단위", + "서식": 1 + }, + "E1": { + "값": "수량(m당)", + "서식": 1 + } + } + } + ], + "활성": "s1" +} diff --git a/resources/master_template/structure/be.json b/resources/master_template/structure/be.json index 66d85d4c..aeee77e4 100644 --- a/resources/master_template/structure/be.json +++ b/resources/master_template/structure/be.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/bg.json b/resources/master_template/structure/bg.json index 80bb0b4b..30aa8bf5 100644 --- a/resources/master_template/structure/bg.json +++ b/resources/master_template/structure/bg.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/bm_area.json b/resources/master_template/structure/bm_area.json index 46cdf626..9337dc2f 100644 --- a/resources/master_template/structure/bm_area.json +++ b/resources/master_template/structure/bm_area.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/bm_len.json b/resources/master_template/structure/bm_len.json index a1cf80d4..88ef0ab3 100644 --- a/resources/master_template/structure/bm_len.json +++ b/resources/master_template/structure/bm_len.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/bx_len.json b/resources/master_template/structure/bx_len.json index f900a3e7..e4cb34ae 100644 --- a/resources/master_template/structure/bx_len.json +++ b/resources/master_template/structure/bx_len.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/bx_wing.json b/resources/master_template/structure/bx_wing.json index 67f21977..fb99203f 100644 --- a/resources/master_template/structure/bx_wing.json +++ b/resources/master_template/structure/bx_wing.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/cs.json b/resources/master_template/structure/cs.json index 30d6273d..dd24845b 100644 --- a/resources/master_template/structure/cs.json +++ b/resources/master_template/structure/cs.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/ec.json b/resources/master_template/structure/ec.json index affd8114..e119aa89 100644 --- a/resources/master_template/structure/ec.json +++ b/resources/master_template/structure/ec.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/etc.json b/resources/master_template/structure/etc.json index fed2e8fa..c9c9af6f 100644 --- a/resources/master_template/structure/etc.json +++ b/resources/master_template/structure/etc.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/fb.json b/resources/master_template/structure/fb.json index 080bd463..5c7d0911 100644 --- a/resources/master_template/structure/fb.json +++ b/resources/master_template/structure/fb.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/fp_l.json b/resources/master_template/structure/fp_l.json index 764a2ae2..69bb0bc6 100644 --- a/resources/master_template/structure/fp_l.json +++ b/resources/master_template/structure/fp_l.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/fp_w.json b/resources/master_template/structure/fp_w.json index f8ff8360..89831d37 100644 --- a/resources/master_template/structure/fp_w.json +++ b/resources/master_template/structure/fp_w.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/gr_curb.json b/resources/master_template/structure/gr_curb.json index 360487e7..0d631708 100644 --- a/resources/master_template/structure/gr_curb.json +++ b/resources/master_template/structure/gr_curb.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/gr_rail.json b/resources/master_template/structure/gr_rail.json index 066402f1..8c93aedc 100644 --- a/resources/master_template/structure/gr_rail.json +++ b/resources/master_template/structure/gr_rail.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/gr_sign.json b/resources/master_template/structure/gr_sign.json index b47412c5..c3afa1f7 100644 --- a/resources/master_template/structure/gr_sign.json +++ b/resources/master_template/structure/gr_sign.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/h_break.json b/resources/master_template/structure/h_break.json index 981857b3..c78994c2 100644 --- a/resources/master_template/structure/h_break.json +++ b/resources/master_template/structure/h_break.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/h_ford_basin.json b/resources/master_template/structure/h_ford_basin.json index 4be43476..5c7f9ff7 100644 --- a/resources/master_template/structure/h_ford_basin.json +++ b/resources/master_template/structure/h_ford_basin.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/h_intake.json b/resources/master_template/structure/h_intake.json index 13c5fb13..0986d7da 100644 --- a/resources/master_template/structure/h_intake.json +++ b/resources/master_template/structure/h_intake.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/h_pg_rail.json b/resources/master_template/structure/h_pg_rail.json index 93a629e0..ff3fc485 100644 --- a/resources/master_template/structure/h_pg_rail.json +++ b/resources/master_template/structure/h_pg_rail.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/h_pv_rail.json b/resources/master_template/structure/h_pv_rail.json index 44f3fac4..be30d771 100644 --- a/resources/master_template/structure/h_pv_rail.json +++ b/resources/master_template/structure/h_pv_rail.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/h_rip_c.json b/resources/master_template/structure/h_rip_c.json index 2b3268d3..b6bbd7b4 100644 --- a/resources/master_template/structure/h_rip_c.json +++ b/resources/master_template/structure/h_rip_c.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/h_rip_m.json b/resources/master_template/structure/h_rip_m.json index 12564857..d6913c8d 100644 --- a/resources/master_template/structure/h_rip_m.json +++ b/resources/master_template/structure/h_rip_m.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/h_rockfall.json b/resources/master_template/structure/h_rockfall.json index 25195f35..da684b1e 100644 --- a/resources/master_template/structure/h_rockfall.json +++ b/resources/master_template/structure/h_rockfall.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/h_soil_ditch.json b/resources/master_template/structure/h_soil_ditch.json index d05202cc..a906ae5b 100644 --- a/resources/master_template/structure/h_soil_ditch.json +++ b/resources/master_template/structure/h_soil_ditch.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/h_stone_bed.json b/resources/master_template/structure/h_stone_bed.json index f90baf98..bfb2b924 100644 --- a/resources/master_template/structure/h_stone_bed.json +++ b/resources/master_template/structure/h_stone_bed.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/h_stone_ch.json b/resources/master_template/structure/h_stone_ch.json index 3c6e4922..31e9ac8a 100644 --- a/resources/master_template/structure/h_stone_ch.json +++ b/resources/master_template/structure/h_stone_ch.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/h_waste.json b/resources/master_template/structure/h_waste.json index 82add70b..ccfff700 100644 --- a/resources/master_template/structure/h_waste.json +++ b/resources/master_template/structure/h_waste.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/memo.json b/resources/master_template/structure/memo.json index 489e5ef8..51ebf6c0 100644 --- a/resources/master_template/structure/memo.json +++ b/resources/master_template/structure/memo.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/mr.json b/resources/master_template/structure/mr.json index 53369635..482a06df 100644 --- a/resources/master_template/structure/mr.json +++ b/resources/master_template/structure/mr.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/ms.json b/resources/master_template/structure/ms.json index f0272a10..8b828a33 100644 --- a/resources/master_template/structure/ms.json +++ b/resources/master_template/structure/ms.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/no.json b/resources/master_template/structure/no.json index aa348227..ca136814 100644 --- a/resources/master_template/structure/no.json +++ b/resources/master_template/structure/no.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/od_cnt.json b/resources/master_template/structure/od_cnt.json index 2d01ffab..383d961a 100644 --- a/resources/master_template/structure/od_cnt.json +++ b/resources/master_template/structure/od_cnt.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/od_len.json b/resources/master_template/structure/od_len.json index 52ae094a..29f1c329 100644 --- a/resources/master_template/structure/od_len.json +++ b/resources/master_template/structure/od_len.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/pg_in.json b/resources/master_template/structure/pg_in.json index 7293a81e..7b5733fa 100644 --- a/resources/master_template/structure/pg_in.json +++ b/resources/master_template/structure/pg_in.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/pp_cp.json b/resources/master_template/structure/pp_cp.json index 0bb30dfe..73186323 100644 --- a/resources/master_template/structure/pp_cp.json +++ b/resources/master_template/structure/pp_cp.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/pp_len.json b/resources/master_template/structure/pp_len.json index c55eb833..eadad960 100644 --- a/resources/master_template/structure/pp_len.json +++ b/resources/master_template/structure/pp_len.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/ps.json b/resources/master_template/structure/ps.json index ceaafad7..8ba9010c 100644 --- a/resources/master_template/structure/ps.json +++ b/resources/master_template/structure/ps.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/pv_a.json b/resources/master_template/structure/pv_a.json index edb9aed1..30a22f89 100644 --- a/resources/master_template/structure/pv_a.json +++ b/resources/master_template/structure/pv_a.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/pv_b.json b/resources/master_template/structure/pv_b.json index 04938563..ba01319e 100644 --- a/resources/master_template/structure/pv_b.json +++ b/resources/master_template/structure/pv_b.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/pv_jt.json b/resources/master_template/structure/pv_jt.json index ee6a8e75..a0c6b376 100644 --- a/resources/master_template/structure/pv_jt.json +++ b/resources/master_template/structure/pv_jt.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/pv_l.json b/resources/master_template/structure/pv_l.json index 91980abd..363b41ec 100644 --- a/resources/master_template/structure/pv_l.json +++ b/resources/master_template/structure/pv_l.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/pv_w.json b/resources/master_template/structure/pv_w.json index 91a9fa8c..73dcb13b 100644 --- a/resources/master_template/structure/pv_w.json +++ b/resources/master_template/structure/pv_w.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/rf.json b/resources/master_template/structure/rf.json index 50cf9500..580123df 100644 --- a/resources/master_template/structure/rf.json +++ b/resources/master_template/structure/rf.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/rv.json b/resources/master_template/structure/rv.json index 34378219..01aa92db 100644 --- a/resources/master_template/structure/rv.json +++ b/resources/master_template/structure/rv.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/rw.json b/resources/master_template/structure/rw.json index be6470b2..085121ea 100644 --- a/resources/master_template/structure/rw.json +++ b/resources/master_template/structure/rw.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/sg.json b/resources/master_template/structure/sg.json index 1fa04f0c..450dd30f 100644 --- a/resources/master_template/structure/sg.json +++ b/resources/master_template/structure/sg.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/sta.json b/resources/master_template/structure/sta.json index 855f03db..da54464c 100644 --- a/resources/master_template/structure/sta.json +++ b/resources/master_template/structure/sta.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/ta.json b/resources/master_template/structure/ta.json index 577c44a8..64201388 100644 --- a/resources/master_template/structure/ta.json +++ b/resources/master_template/structure/ta.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/master_template/structure/wy.json b/resources/master_template/structure/wy.json index 1b33e090..5eebbb98 100644 --- a/resources/master_template/structure/wy.json +++ b/resources/master_template/structure/wy.json @@ -7,66 +7,5 @@ "도면": { "format": 6, "entities": [] - }, - "산출근거": { - "양식": "산출근거", - "종류": "표", - "판": 1, - "열": [ - { - "id": "work", - "머리": [ - "공종" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "spec", - "머리": [ - "규격" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "detail", - "머리": [ - "산출 내역" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "unit", - "머리": [ - "단위" - ], - "단위": null, - "꼴": "글" - }, - { - "id": "qty", - "머리": [ - "수량(m당)" - ], - "단위": null, - "꼴": "수" - } - ], - "줄": [ - { - "id": "1", - "값": {} - }, - { - "id": "2", - "값": {} - }, - { - "id": "3", - "값": {} - } - ] } } diff --git a/resources/tester/spreadsheet/spreadsheet_size.mjs b/resources/tester/spreadsheet/spreadsheet_size.mjs new file mode 100644 index 00000000..57eff220 --- /dev/null +++ b/resources/tester/spreadsheet/spreadsheet_size.mjs @@ -0,0 +1,56 @@ +// 스프레드시트 크기 재기 — vite 로 줄여 묶은 뒤 gzip 크기(바이트). `test_spreadsheet_size.py` 가 부름. +// node spreadsheet_size.mjs <저장소 뿌리> → {"engine": {js, css}, "total": {js, css}} +// 앱 번들에 이미 있는 `@ui/ui_template_elements*` 는 빼고 잼(스프레드시트가 새로 더하는 몫만). +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import { gzipSync } from "node:zlib"; + +const root = path.resolve(process.argv[2] ?? "."); +const dir = path.join(root, "A00_Common", "spreadsheet"); +const { build } = await import( + pathToFileURL(path.join(root, "config/node_modules/vite/dist/node/index.js")).href +); + +const ENGINE = [ + "spreadsheet_address", + "spreadsheet_parser", + "spreadsheet_eval", + "spreadsheet_graph", + "spreadsheet_refshift", + "spreadsheet_commands", + "spreadsheet_history", + "spreadsheet_recalc", + "spreadsheet_functions", + "spreadsheet_numfmt", + "spreadsheet_fill", +]; + +async function measure(entries) { + const out = await build({ + configFile: false, + logLevel: "silent", + root, + resolve: { alias: { "@ui": path.join(root, "ui_template") } }, + build: { + write: false, + minify: true, + target: "es2022", + cssCodeSplit: false, + lib: { entry: entries, formats: ["es"] }, + rollupOptions: { external: [/ui_template_elements/] }, + }, + }); + const outputs = (Array.isArray(out) ? out : [out]).flatMap((o) => o.output); + const size = { js: 0, css: 0 }; + for (const o of outputs) { + const body = o.type === "chunk" ? o.code : o.source; + const gz = gzipSync(typeof body === "string" ? body : Buffer.from(body)).length; + if (o.fileName.endsWith(".css")) size.css += gz; + else if (o.fileName.endsWith(".js")) size.js += gz; + } + return size; +} + +const engine = await measure(ENGINE.map((name) => path.join(dir, `${name}.ts`))); +const total = await measure([path.join(dir, "spreadsheet.ts")]); +console.log(JSON.stringify({ engine, total })); diff --git a/resources/tester/spreadsheet/test_spreadsheet_golden.py b/resources/tester/spreadsheet/test_spreadsheet_golden.py new file mode 100644 index 00000000..b5d2972e --- /dev/null +++ b/resources/tester/spreadsheet/test_spreadsheet_golden.py @@ -0,0 +1,129 @@ +"""F — 금빛 시험: 실무 구조도 xlsx 세 벌의 수식을 새 엔진으로 풀어 엑셀 캐시값과 대조. + +xlsx 는 **시험에서만** openpyxl 로 읽음(앱에는 가져오기 없음). 식 = 엑셀 글 그대로 · 값 = 칸 값 · +바깥 통합문서 참조(`[172]관집계!…`)는 식을 못 푸니 캐시값을 값으로 굳힘(대조에서 뺌). +갈린 칸은 조용히 맞추지 않음 — 목록을 `tmp/spreadsheet_golden_report.json` 에 적고 시험을 떨굼. +엔진(A · B)이 빈 몸이면 건너뜀 — 엔진이 차는 대로 다시 돌림. +""" + +import datetime as dt +import json +import re +import shutil +from decimal import Decimal, InvalidOperation +from pathlib import Path + +import pytest + +openpyxl = pytest.importorskip("openpyxl") +from openpyxl.utils.datetime import to_excel # noqa: E402 + +from common_util import common_util_spreadsheet as spreadsheet # noqa: E402 + +ROOT = Path(__file__).resolve().parents[3] +PRACTICE = ROOT / "resources" / "knowledge" / "original" / "실무문서" +NAMES = ("07-구조도-소광리.xlsx", "5. 구조도(기번3).xlsx", "구조도(변경).xlsx") +FILES = [next(PRACTICE.rglob(name)) for name in NAMES] +REPORT = ROOT / "tmp" / "spreadsheet_golden_report.json" +EXPECTED_FORMULAS = 2699 +_EXTERNAL = re.compile(r"\[\d+\]") +_ERRORS = {"#NULL!", "#DIV/0!", "#VALUE!", "#REF!", "#NAME?", "#NUM!", "#N/A"} + +pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") + + +def _plain(value): + """칸 값 → JSON(날짜 · 시각 형식 칸은 엑셀 일련수).""" + if isinstance(value, (dt.datetime, dt.date, dt.time, dt.timedelta)): + return to_excel(value) + return value + + +def _book(path: Path): + """xlsx → 통합문서 + 대조할 칸 `[(시트 id, A1, 캐시값)]`.""" + formulas = openpyxl.load_workbook(path, data_only=False) + cached = openpyxl.load_workbook(path, data_only=True) + sheets, checks = [], [] + for i, ws in enumerate(formulas.worksheets, start=1): + sid, cells = f"s{i}", {} + values = cached[ws.title] + for row in ws.iter_rows(): + for c in row: + if c.value is None: + continue + keep = _plain(values[c.coordinate].value) + if c.data_type == "f" and isinstance(c.value, str): + if _EXTERNAL.search(c.value): + if keep is not None: + cells[c.coordinate] = {"값": keep} + continue + cells[c.coordinate] = {"식": c.value[1:]} + checks.append((sid, c.coordinate, keep)) + elif c.data_type != "f": + cells[c.coordinate] = {"값": _plain(c.value)} + sheets.append({"id": sid, "이름": ws.title, "칸": cells}) + book = {"종류": "통합문서", "판": 1, "열": path.stem, "서식": [{}], "시트": sheets} + return book, checks + + +def _same(want, got) -> bool: + if got is None: + return False + if isinstance(want, bool): + return got.get("참") is want + if isinstance(want, (int, float)): + try: + a, b = Decimal(got["수"]), Decimal(repr(float(want))) + except (KeyError, InvalidOperation): + return False + return abs(a - b) <= Decimal("1e-9") * max(Decimal(1), abs(b)) + if want is None or want == "": + return got.get("글") == "" or got.get("수") == "0" + if isinstance(want, str) and want in _ERRORS: + return got.get("오류") == want + return got.get("글") == want + + +def test_실무_구조도_수식_엑셀과_같음(): + loaded = [(path, *_book(path)) for path in FILES] + total = sum(len(checks) for _p, _b, checks in loaded) + external = 6 # 소광리 `[172]관집계` — 값으로 굳혀 대조에서 뺌 + assert total + external == EXPECTED_FORMULAS + try: + results = spreadsheet.recalc_workbooks([book for _p, book, _c in loaded]) + except spreadsheet.RecalcError as error: + if "아직 없음" in str(error): + pytest.skip(f"엔진 아직 — {error}") + raise + mismatches = [] + for (path, book, checks), values in zip(loaded, results, strict=True): + names = {s["id"]: s["이름"] for s in book["시트"]} + for sid, a1, want in checks: + got = values.get(sid, {}).get(a1) + if not _same(want, got): + formula = next(s for s in book["시트"] if s["id"] == sid)["칸"][a1]["식"] + mismatches.append( + { + "파일": path.name, + "시트": names[sid], + "칸": a1, + "식": formula, + "엑셀": want, + "엔진": got, + } + ) + REPORT.parent.mkdir(exist_ok=True) + REPORT.write_text( + json.dumps( + {"대조": total, "갈림": len(mismatches), "목록": mismatches}, + ensure_ascii=False, + indent=1, + default=str, + ), + encoding="utf-8", + ) + head = "\n".join( + f"{m['파일']} {m['시트']}!{m['칸']} ={m['식']} 엑셀 {m['엑셀']!r} · 엔진 {m['엔진']}" + for m in mismatches[:30] + ) + assert not mismatches, f"{len(mismatches)}/{total} 칸 갈림 — 전체 {REPORT}\n{head}" diff --git a/resources/tester/spreadsheet/test_spreadsheet_node.py b/resources/tester/spreadsheet/test_spreadsheet_node.py new file mode 100644 index 00000000..57a1b346 --- /dev/null +++ b/resources/tester/spreadsheet/test_spreadsheet_node.py @@ -0,0 +1,37 @@ +"""F — 서버 재계산 다리: 파이썬 → Node 번들(`build:spreadsheet`) → 화면과 같은 TS 엔진. + +엔진(A · B)이 아직 빈 몸이면 `아직 없음` 까닭이 그대로 올라와 저장을 막는지 봄 · +엔진이 차면 저장 견본(`spreadsheet_sample.json`)의 `계산값` 과 서버 풀이가 같은지 봄. +""" + +import json +import shutil +from pathlib import Path + +import pytest + +from common_util import common_util_spreadsheet as spreadsheet + +ROOT = Path(__file__).resolve().parents[3] +SAMPLE = ROOT / "A00_Common" / "spreadsheet" / "spreadsheet_sample.json" + +pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") + + +def test_견본_계산값_같음(): + sample = json.loads(SAMPLE.read_text(encoding="utf-8")) + try: + saved = spreadsheet.with_server_values({**sample, "계산값": {"s1": {"H4": {"수": "틀림"}}}}) + except spreadsheet.RecalcError as error: + assert "아직 없음" in str(error), error # 엔진이 빈 몸일 때만 봐줌 + pytest.skip(f"엔진 아직 — {error}") + assert saved["계산값"] == sample["계산값"] + assert {k: v for k, v in saved.items() if k != "계산값"} == { + k: v for k, v in sample.items() if k != "계산값" + } + + +def test_못_푸는_문서는_까닭과_함께_막음(): + with pytest.raises(spreadsheet.RecalcError) as caught: + spreadsheet.recalc_workbooks([{"종류": "통합문서", "시트": "깨짐"}]) + assert "스프레드시트 계산 실패" in str(caught.value) diff --git a/resources/tester/spreadsheet/test_spreadsheet_size.py b/resources/tester/spreadsheet/test_spreadsheet_size.py new file mode 100644 index 00000000..4b4d2580 --- /dev/null +++ b/resources/tester/spreadsheet/test_spreadsheet_size.py @@ -0,0 +1,74 @@ +"""F — 스프레드시트 크기 예산 · M02 만 늦게 불러옴(계약 `spreadsheet_types.ts` 머리). + +예산(압축 gzip 뒤) — 엔진 A+B 22 KB · 화면 C+D+E 28 KB · css 4 KB · 합 54 KB. +엔진은 파일마다 따로 묶어 더하므로 실제보다 조금 크게 잼(넘치면 넘친 것). +""" + +import json +import re +import shutil +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[3] +SCRIPT = Path(__file__).with_name("spreadsheet_size.mjs") +KB = 1024 +BUDGET = {"engine": 22 * KB, "view": 28 * KB, "css": 4 * KB, "total": 54 * KB} + +pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") + + +def test_크기_예산(): + out = subprocess.run( + ["node", str(SCRIPT), str(ROOT)], + cwd=ROOT, + capture_output=True, + text=True, + encoding="utf-8", + timeout=300, + check=True, + ) + size = json.loads(out.stdout.strip().splitlines()[-1]) + engine, total = size["engine"]["js"], size["total"] + view = max(0, total["js"] - engine) + print( + f"gzip — 엔진 {engine} · 화면 {view} · css {total['css']} · 합 {total['js'] + total['css']}" + ) + assert engine <= BUDGET["engine"], f"엔진 {engine} B > {BUDGET['engine']}" + assert view <= BUDGET["view"], f"화면 {view} B > {BUDGET['view']}" + assert total["css"] <= BUDGET["css"], f"css {total['css']} B > {BUDGET['css']}" + assert total["js"] + total["css"] <= BUDGET["total"] + + +# 정적 import 금지 — 타입만(`import type`) · 늦게(`import(`) 는 됨 +_STATIC = re.compile( + r"^\s*import\s+(?!type\b)[^;]*?from\s+[\"'][^\"']*spreadsheet/spreadsheet", re.M +) +_SIDE = re.compile(r"^\s*import\s+[\"'][^\"']*spreadsheet/spreadsheet", re.M) + + +def test_M02_만_늦게_불러옴(): + offenders, users = [], [] + listed = subprocess.run( + ["git", "ls-files", "--cached", "--others", "--exclude-standard", "*.ts"], + cwd=ROOT, + capture_output=True, + text=True, + encoding="utf-8", + check=True, + ).stdout.splitlines() + for rel in listed: + if rel.startswith("A00_Common/spreadsheet/") or not (ROOT / rel).is_file(): + continue + text = (ROOT / rel).read_text(encoding="utf-8", errors="replace") + if "spreadsheet/spreadsheet" not in text: + continue + if _STATIC.search(text) or _SIDE.search(text): + offenders.append(rel) + if "import(" in text: + users.append(rel) + # 서버 Node 진입만 정적(브라우저 번들 밖) + assert offenders == ["common_util/common_util_spreadsheet_node.ts"], offenders + assert users and all(u.startswith("M02_MasterTemplete/") for u in users), users diff --git a/resources/tester/spreadsheet/test_spreadsheet_store.py b/resources/tester/spreadsheet/test_spreadsheet_store.py new file mode 100644 index 00000000..4e690eff --- /dev/null +++ b/resources/tester/spreadsheet/test_spreadsheet_store.py @@ -0,0 +1,138 @@ +"""F — 산출근거 저장(basis 종류) · 옛 산출근거 옮김 · [저장] 때 서버 계산값 · 임시 시험 주소. + +서버 Node 풀이는 `test_spreadsheet_node.py` 가 따로 봄 — 여기는 `with_server_values` 를 바꿔 끼워 +「브라우저 값은 버리고 서버 값만 적는다」 · 「못 풀면 저장을 막는다」 만 잼. +""" + +import json +import shutil + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from common_util import common_util_spreadsheet as spreadsheet +from M02_MasterTemplete import M02_MasterTemplete_Router as router_module +from M02_MasterTemplete import M02_MasterTemplete_Store as store +from M02_MasterTemplete import M02_Template_Layers as layers + +REAL = store.FOLDER +SERVER = {"s1": {"A1": {"수": "42"}}} + + +@pytest.fixture() +def client(tmp_path, monkeypatch): + for kind in store.KINDS: + (tmp_path / kind).mkdir() + shutil.copy(REAL / "table/구조물집계표.json", tmp_path / "table") + monkeypatch.setattr(store, "FOLDER", tmp_path) + monkeypatch.setattr(router_module, "TRIAL_PATH", tmp_path / "trial.json") + + def fake(book): + clean = {k: v for k, v in book.items() if k != "계산값"} + return {**clean, "계산값": SERVER} + + monkeypatch.setattr(spreadsheet, "with_server_values", fake) + app = FastAPI() + app.include_router(router_module.router) + return TestClient(app) + + +def _old_structure(folder, column, basis): + doc = {"양식": "구조물도면", "종류": "structure", "판": 1, "열": column, "도번": "구-01"} + doc.update({"도면": {"format": 6, "entities": []}, "산출근거": basis}) + (folder / "structure" / f"{column}.json").write_text( + json.dumps(doc, ensure_ascii=False), encoding="utf-8" + ) + + +def test_옛_산출근거를_따로_파일로_옮김(client, tmp_path): + old = { + "열": [ + {"id": "work", "머리": ["공종"]}, + {"id": "detail", "머리": ["산출", "내역"]}, + {"id": "qty", "머리": ["수량(m당)"]}, + ], + "줄": [{"id": "1", "값": {"work": "터파기", "qty": 1.5}}, {"id": "2", "값": {}}], + "보기": {"열너비": {"detail": 210}}, + } + _old_structure(tmp_path, "pp_len", old) + done = store.migrate_basis() + assert done["옮김"] == ["pp_len"] + columns = store._column_ids() + assert set(done["새로"]) == set(columns) - {"pp_len"} + structure = json.loads((tmp_path / "structure/pp_len.json").read_text(encoding="utf-8")) + assert "산출근거" not in structure and structure["도번"] == "구-01" + basis = store.read("basis", "pp_len")["문서"] + sheet = basis["시트"][0] + assert (basis["종류"], basis["열"], sheet["id"]) == ("통합문서", "pp_len", "s1") + assert sheet["칸"] == { + "A1": {"값": "공종", "서식": 1}, + "B1": {"값": "산출 내역", "서식": 1}, + "C1": {"값": "수량(m당)", "서식": 1}, + "A2": {"값": "터파기"}, + "C2": {"값": 1.5}, + } + assert sheet["열"] == {"B": {"폭": 30.0}} + assert "계산값" not in basis # 식 없는 새 문서 — 첫 [저장] 때 서버가 적음 + # 두 번 돌려도 같음 + before = {p.name: p.read_bytes() for p in (tmp_path / "basis").iterdir()} + assert store.migrate_basis() == {"옮김": [], "새로": []} + assert before == {p.name: p.read_bytes() for p in (tmp_path / "basis").iterdir()} + + +def test_새_구조물_도면은_산출근거_따로(client): + made = client.post("/api/m02/structures/rv").json()["문서"] + assert "산출근거" not in made + basis = client.get("/api/m02/templates/basis/rv").json()["문서"] + heads = [basis["시트"][0]["칸"][f"{c}1"]["값"] for c in "ABCDE"] + assert heads == ["공종", "규격", "산출 내역", "단위", "수량(m당)"] + + +def test_저장은_서버_계산값만_적음(client): + store.ensure_basis("rv") + got = client.get("/api/m02/templates/basis/rv").json() + doc = {**got["문서"], "계산값": {"s1": {"A1": {"수": "브라우저"}}}} + saved = client.put("/api/m02/templates/basis/rv", json={"판": got["판"], "문서": doc}) + assert saved.status_code == 200 + assert client.get("/api/m02/templates/basis/rv").json()["문서"]["계산값"] == SERVER + # 뼈대 없는 문서 · 판이 다르면 막음 + bad = client.put("/api/m02/templates/basis/rv", json={"판": saved.json()["판"], "문서": {}}) + assert bad.status_code == 400 + stale = client.put("/api/m02/templates/basis/rv", json={"판": got["판"], "문서": doc}) + assert stale.status_code == 409 + + +def test_못_풀면_저장을_막음(client, monkeypatch): + store.ensure_basis("rv") + got = client.get("/api/m02/templates/basis/rv").json() + + def broken(_book): + raise spreadsheet.RecalcError("스프레드시트 계산 실패 — 시험") + + monkeypatch.setattr(spreadsheet, "with_server_values", broken) + r = client.put("/api/m02/templates/basis/rv", json={"판": got["판"], "문서": got["문서"]}) + assert r.status_code == 503 + assert client.get("/api/m02/templates/basis/rv").json()["판"] == got["판"] + + +def test_층_규칙과_프로젝트_복사(client, tmp_path, monkeypatch): + store.ensure_basis("rv") + doc = store.read("basis", "rv")["문서"] + layers.check_skeleton("basis", doc) + for bad in ({}, {**doc, "시트": {}}): + with pytest.raises(ValueError): + layers.check_skeleton("basis", bad) + layers.check_skeleton("structure", {"도면": {"entities": []}}) # 산출근거 없어도 됨 + monkeypatch.setattr(layers, "SYSTEM_ROOT", tmp_path) + copied = layers.seed_project(tmp_path / "project") + assert "basis/rv" in copied["작업본"] and "basis/rv" in copied["초기"] + + +def test_임시_시험_주소(client): + first = client.get("/api/m02/spreadsheet-trial").json()["문서"] + assert [s["이름"] for s in first["시트"]] == ["시트1", "시트2", "시트3"] + first["시트"][0]["칸"] = {"A1": {"식": "1+41"}} + saved = client.put("/api/m02/spreadsheet-trial", json={"문서": first}).json()["문서"] + assert saved["계산값"] == SERVER + assert client.get("/api/m02/spreadsheet-trial").json()["문서"] == saved diff --git a/resources/tester/test_m02_structure.py b/resources/tester/test_m02_structure.py index 9876b26b..9fda2eba 100644 --- a/resources/tester/test_m02_structure.py +++ b/resources/tester/test_m02_structure.py @@ -1,4 +1,5 @@ -"""M02 구조물 도면(structure) — 새로 · 도번 자동 · 409 · 저장 · 지운 뒤 번호 · 프로젝트 복사.""" +"""M02 구조물 도면(structure) — 새로 · 도번 자동 · 409 · 저장 · 지운 뒤 번호 · 프로젝트 복사. +산출근거는 따로 파일(basis) — `resources/tester/spreadsheet/test_spreadsheet_store.py`.""" import shutil @@ -36,11 +37,9 @@ def test_새로_도번_자동_409_없는_열(client): ) # 도각 없는 전용 화면 — 그린 만큼이 설계 영역 assert doc["도면"] == {"format": 6, "entities": []} - basis = doc["산출근거"] - assert [c["머리"][0] for c in basis["열"]] == ["공종", "규격", "산출 내역", "단위", "수량(m당)"] - # 빈 줄 몇 개를 미리 둠 — 줄 더하기 안 눌러도 바로 입력 - assert basis["줄"] == [{"id": "1", "값": {}}, {"id": "2", "값": {}}, {"id": "3", "값": {}}] - assert basis["종류"] == "표" + # 산출근거는 구조물 문서 밖 — 같은 열 id 로 따로 한 통합문서 + assert "산출근거" not in doc + assert client.get("/api/m02/templates/basis/pp_len").json()["문서"]["종류"] == "통합문서" assert client.post("/api/m02/structures/rv").json()["문서"]["도번"] == "구-02" assert client.post("/api/m02/structures/pp_len").status_code == 409 @@ -56,10 +55,10 @@ def test_새로_도번_자동_409_없는_열(client): def test_저장_뼈대_거름(client): got = client.post("/api/m02/structures/pp_len").json() doc = got["문서"] - doc["산출근거"]["줄"] = [{"id": "r1", "값": {"work": "관 부설"}}] + doc["도면"]["entities"] = [{"type": "line"}] saved = client.put("/api/m02/templates/structure/pp_len", json={"판": got["판"], "문서": doc}) assert saved.status_code == 200 and saved.json()["판"] != got["판"] - for bad in ({}, {**doc, "도면": {}}, {**doc, "산출근거": {"열": "x"}}, {**doc, "도면": []}): + for bad in ({}, {**doc, "도면": {}}, {**doc, "도면": {"entities": "x"}}, {**doc, "도면": []}): r = client.put( "/api/m02/templates/structure/pp_len", json={"판": saved.json()["판"], "문서": bad} ) @@ -90,4 +89,4 @@ def test_프로젝트_복사와_층_저장에_실림(client, tmp_path, monkeypat assert ("structure", "pp_len") in {(r["종류"], r["이름"]) for r in layers.list_templates(work)} layers.check_skeleton("structure", doc) with pytest.raises(ValueError): - layers.check_skeleton("structure", {**doc, "산출근거": None}) + layers.check_skeleton("structure", {**doc, "도면": None}) diff --git a/resources/tester/test_m02_structure_panel.py b/resources/tester/test_m02_structure_panel.py index e18cd0a5..73be180e 100644 --- a/resources/tester/test_m02_structure_panel.py +++ b/resources/tester/test_m02_structure_panel.py @@ -46,11 +46,11 @@ def test_소스가_700줄을_넘지_않음(): ["createStructure", "fetchStructureNumbers", "readTemplate", "saveTemplate"], ), ("M02_MasterTemplete_Drawing.ts", ["mountDrawingTemplate"]), - ("../ui_template/sheet/ui_template_sheet.ts", ["createSheet"]), + ("../A00_Common/spreadsheet/spreadsheet.ts", ["createSpreadsheet"]), ], ) def test_컨테이너가_쓰는_이름이_내보내진_것(source: str, names: list[str]): - """sub2·sub3 가 이름을 바꾸면 여기서 먼저 걸림.""" + """도면 부품 · 스프레드시트(D) 가 이름을 바꾸면 여기서 먼저 걸림.""" body = _text(M02 / source) panel = _text(PANEL) for name in names: