From 559d445e4ee4fd98b2be4c11b624f4810cfdafdb Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 30 Sep 2026 07:36:32 +0900 Subject: [PATCH 01/18] auto: 2026-09-30 07:36 (ESD_LAPTOP) --- .../B05_Profile_UI_RouteEdit_Initial.ts | 7 ++ .../B05_Profile_UI_RouteEdit_Initial_Model.ts | 62 ++++++++++ .../B05_Profile_UI_RouteEdit_Recommend.ts | 106 ++++++++++++++++-- .../B05_Profile_UI_Style_RouteRecommend.css | 64 ++++++++++- .../tester/helper_b05_routeedit_initial.cjs | 45 ++++++++ .../tester/test_b05_routeedit_initial.py | 33 ++++++ 6 files changed, 304 insertions(+), 13 deletions(-) diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Initial.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Initial.ts index eaa7f8e4..8c440275 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Initial.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Initial.ts @@ -27,6 +27,7 @@ import type { RouteEditSnapshot } from "./B05_Profile_UI_RouteEdit_History"; import { choiceLabel, DEFAULT_WEIGHTS, + NO_WEIGHT_LOCKS, initialRouteSnapshot, normalizeRouteInitial, readInitialChoice, @@ -85,6 +86,11 @@ export function createInitialPanel(params: InitialPanelParams): InitialPanel { let opening: RouteEditSnapshot | null = null; let edited = false; let busy = false; + /** 비중 · 고정 — 모달을 닫았다 열어도 이 편집 창이 사는 동안 남는다. */ + const weightMemory = { + weights: null as RouteInitialWeights | null, + locks: { ...NO_WEIGHT_LOCKS }, + }; /** `/route/plan` 한 번 읽은 것 — 예상노선(지도 바탕) · 초기값 노선([초기화]). 창이 사는 동안 안 바뀐다. */ let planRead: Promise | null = null; @@ -130,6 +136,7 @@ export function createInitialPanel(params: InitialPanelParams): InitialPanel { criteria: choice?.criteriaOverrides ?? base.criteria, defaults, weights: { ...(choice?.weights ?? base.weights ?? DEFAULT_WEIGHTS) }, + weightMemory, planMinRadiusM: params.planMinRadiusM(), expected: (plan?.expected ?? []).map(([x, y]) => [x, y]), current: opening!.planned.map(([x, y]) => [x, y]), diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Initial_Model.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Initial_Model.ts index a8ad81c6..4b5ea6c6 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Initial_Model.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Initial_Model.ts @@ -51,6 +51,68 @@ export function rebalanceWeights( return { expected: out.expected, earthwork: out.earthwork, grade: out.grade }; } +/** 비중마다 고정 표시 — 고정한 값은 다른 비중을 움직여도 안 바뀐다. */ +export type WeightLocks = Record; + +export const NO_WEIGHT_LOCKS: WeightLocks = { expected: false, earthwork: false, grade: false }; + +const lockedKeys = (locks: WeightLocks): WeightKey[] => WEIGHT_KEYS.filter((name) => locks[name]); + +/** 나머지 둘이 고정돼 저절로 정해지는 비중 — 없으면 null. 이 비중은 잠김으로 보인다. */ +export function derivedWeightKey(locks: WeightLocks): WeightKey | null { + const free = WEIGHT_KEYS.filter((name) => !locks[name]); + return free.length === 1 ? free[0] : null; +} + +/** 이 비중의 고정 단추를 새로 누를 수 있나 — 셋 다 고정은 막는다(마지막 하나는 저절로 잠김). + * 이미 고정한 것을 푸는 것은 늘 된다. */ +export function canLockWeight(locks: WeightLocks, key: WeightKey): boolean { + return locks[key] || lockedKeys(locks).length < 2; +} + +/** 슬라이더로 한 비중을 옮긴다 — 고정한 것은 그대로, 안 고정한 것끼리 합 100 을 맞춘다. + * 고정하지 않은 것이 셋이면 나머지 둘이 지금 비율대로(`rebalanceWeights`), 둘이면 다른 하나가 남는 몫을 + * 받고, 이 비중 혼자면(나머지 둘 고정) 값이 정해져 있어 안 움직인다. 값은 남는 몫 안으로 잘린다. */ +export function moveWeight( + weights: RouteInitialWeights, + locks: WeightLocks, + key: WeightKey, + value: number, +): RouteInitialWeights { + if (locks[key]) return { ...weights }; + const fixedSum = lockedKeys(locks).reduce((sum, name) => sum + weights[name], 0); + const room = 100 - fixedSum; + const free = WEIGHT_KEYS.filter((name) => !locks[name]); + if (free.length === WEIGHT_KEYS.length) return rebalanceWeights(weights, key, value); + const out = { ...weights }; + if (free.length === 1) { + out[key] = room; + return out; + } + const other = free.find((name) => name !== key)!; + const next = Math.min(room, Math.max(0, Math.round(Number.isFinite(value) ? value : 0))); + out[key] = next; + out[other] = room - next; + return out; +} + +/** 값 칸에 직접 넣은 숫자 — 0~100 정수이고 고정 규칙 아래 합 100 을 맞출 수 있을 때만 받는다. + * 못 맞추면 `ok: false`(칸에 빨간 테두리 · 서버에 안 보냄). 고정한 비중은 고칠 수 없다. */ +export function inputWeight( + weights: RouteInitialWeights, + locks: WeightLocks, + key: WeightKey, + value: number, +): { ok: boolean; weights: RouteInitialWeights } { + const same = { ok: false, weights: { ...weights } }; + if (locks[key] || !Number.isInteger(value) || value < 0 || value > 100) return same; + const room = 100 - lockedKeys(locks).reduce((sum, name) => sum + weights[name], 0); + const free = WEIGHT_KEYS.filter((name) => !locks[name]); + if (free.length === 1) return value === room ? { ok: true, weights: { ...weights } } : same; + if (value > room) return same; + return { ok: true, weights: moveWeight(weights, locks, key, value) }; +} + /** 결과 비교값 — 서버가 셈해 준 것을 보이기만 한다. 없는 칸은 null. */ export interface InitialMetrics { lengthM: number | null; diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Recommend.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Recommend.ts index bb43b44a..f86fe93d 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Recommend.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Recommend.ts @@ -24,7 +24,10 @@ import { CRITERIA_KEYS, LOGIC_LABEL, metricsLine, - rebalanceWeights, + canLockWeight, + derivedWeightKey, + inputWeight, + moveWeight, relaxedLine, violationsLine, violationText, @@ -33,6 +36,7 @@ import { type InitialChoice, type InitialCriteria, type MapLayer, + type WeightLocks, type WeightKey, } from "./B05_Profile_UI_RouteEdit_Initial_Model"; import { createRecommendMap, RECOMMEND_MAP_HTML } from "./B05_Profile_UI_RouteEdit_Recommend_Map"; @@ -55,6 +59,11 @@ const WEIGHT_NAME: Record = { grade: "기울기", }; +/** 고정 단추 그림 — 자물쇠(글자 색을 따라 밝음 · 어두움에서 보인다). */ +const LOCK_ICON = ``; + /** 공통 제약 칸 — 표시 이름 · 단위 · 서버가 준 값(`InitialCriteria`)에서 그 칸을 꺼내는 법. */ const CRITERIA_FIELD: Record< CriteriaKey, @@ -92,6 +101,8 @@ export interface RecommendParams { /** 칸을 비운 채 처음 물었을 때 서버가 답한 값 — 빈 칸 placeholder. 여기서 채워 간다. */ defaults: Partial>; weights: RouteInitialWeights; + /** 비중 · 고정 상태 — 편집 창이 사는 동안 모달을 닫았다 열어도 남는다. 여기서 고쳐 간다. */ + weightMemory: { weights: RouteInitialWeights | null; locks: WeightLocks }; planMinRadiusM: number; /** 지도 바탕 — 예상노선(원본 점 묶음) · 창을 연 노선(노드). */ expected: Vertex[]; @@ -108,7 +119,10 @@ export interface RecommendParams { export function openRecommendModal(params: RecommendParams): void { let logic: RouteInitialLogic = "follow"; - let weights: RouteInitialWeights = { ...params.weights }; + const { locks } = params.weightMemory; + let weights: RouteInitialWeights = { ...(params.weightMemory.weights ?? params.weights) }; + /** 값 칸에 넣은 숫자가 고정 규칙 아래 합 100 을 못 맞추는 비중 — 빨간 테두리 · 서버에 안 보냄. */ + const invalid = new Set(); let preview: InitialChoice | null = null; let busy = false; /** 마지막 미리 보기 뒤 슬라이더 · 제약 칸을 만졌다 — 미리 본 노선이 칸 값과 다르다. */ @@ -156,12 +170,14 @@ export function openRecommendModal(params: RecommendParams): void {
@@ -299,6 +312,11 @@ export function openRecommendModal(params: RecommendParams): void { const relaxed = relaxedLine(preview); line("relaxed").textContent = relaxed; line("relaxed").hidden = !relaxed; + const similar = similarLine(preview); + line("similar").textContent = similar; + line("similar").hidden = !similar; + line("similar-hint").textContent = similar ? SIMILAR_HINT : ""; + line("similar-hint").hidden = !similar; line("criteria").textContent = preview ? criteriaLine(preview.criteria, params.planMinRadiusM) : ""; diff --git a/B05_Profile/B05_Profile_UI_Style_RouteRecommend.css b/B05_Profile/B05_Profile_UI_Style_RouteRecommend.css index 8d00da23..60b2de8d 100644 --- a/B05_Profile/B05_Profile_UI_Style_RouteRecommend.css +++ b/B05_Profile/B05_Profile_UI_Style_RouteRecommend.css @@ -281,6 +281,13 @@ line-height: 1.5; } +/* 같은 노선 알림 아래 흐린 글. */ +.b05-routerec__lines .is-dim { + color: var(--color-text-secondary); + font-size: var(--text-caption); + opacity: 0.8; +} + /* 상한을 풀었다는 안내 — 눈에 띄게(계획서 20). */ .b05-routerec__lines .is-warn { color: var(--color-warning); diff --git a/resources/tester/helper_b05_routeedit_initial.cjs b/resources/tester/helper_b05_routeedit_initial.cjs index 16573519..bada657b 100644 --- a/resources/tester/helper_b05_routeedit_initial.cjs +++ b/resources/tester/helper_b05_routeedit_initial.cjs @@ -298,6 +298,63 @@ const weightLocks = { }, }; +// ⑨ 추천 갈래 여섯 · 남는 흙 · 같은 노선 알림(계획서 23-4c 화면). +const withExtras = (extra, metricExtra = {}) => + model.normalizeRouteInitial( + { ...plainResponse, ...extra, metrics: { ...plainResponse.metrics, ...metricExtra } }, + "short", + null, + ); +const sixth = { + labels: Object.fromEntries( + ["follow", "earthwork", "gentle", "short", "balance", "weighted"].map((k) => [k, model.LOGIC_LABEL[k]]), + ), + surplus: [122.4, -578.2, 0, 0.4].map((v) => model.surplusText(v)), + metricsPlus: model.metricsLine(withExtras({}, { surplus_m3: 122.4 }).metrics), + metricsMinus: model.metricsLine(withExtras({}, { surplus_m3: -578 }).metrics), + metricsNone: model.metricsLine(withExtras({}).metrics), + similarTwo: model.similarLine( + withExtras({ + band_width_m: 13.5, + similar: [ + { logic: "earthwork", max_gap_m: 0.0 }, + { logic: "gentle", max_gap_m: 6.1 }, + ], + }), + ), + similarOne: model.similarLine( + withExtras({ band_width_m: 13.54, similar: [{ logic: "balance", max_gap_m: 6.1 }] }), + ), + similarNoBand: model.similarLine(withExtras({ similar: [{ logic: "weighted", max_gap_m: 9.1 }] })), + similarEmpty: model.similarLine(withExtras({ band_width_m: 13.5, similar: [] })), + similarMissing: model.similarLine(withExtras({})), + similarNull: model.similarLine(null), + similarUnknown: withExtras({ similar: [{ logic: "nope", max_gap_m: 1 }, { logic: "short", max_gap_m: "x" }] }).similar, + bandParsed: withExtras({ band_width_m: 13.5 }).bandWidthM, + hint: model.SIMILAR_HINT, + weightedViolation: model.violationsLine({ + ...plain.metrics, + violations: [ + { + kind: "grade", + station: "No.9+0.0", + value: 29.1, + limit: 14, + reason: "비중 계산 — 예상노선 몫만큼 제약을 막지 않음(측점 사이 지반이 상한을 넘음)", + }, + ], + }), + weightedViolationReason: model.violationText({ + kind: "grade", + station: "No.9+0.0", + value: 29.1, + limit: 14, + reason: "비중 계산 — 예상노선 몫만큼 제약을 막지 않음", + }), + shortLabel: model.choiceLabel("short", { expected: 0, earthwork: 0, grade: 0 }), + balanceLabel: model.choiceLabel("balance", null), +}; + // ④ 캐시 왕복 — 결과 통 프로젝트 칸. 꼴이 틀린 값은 안 읽는다. model.writeInitialChoice("p-1", plain); const readBack = model.readInitialChoice("p-1"); @@ -341,5 +398,6 @@ process.stdout.write( historyRecs, sweep, weightLocks, + sixth, }), ); diff --git a/resources/tester/test_b05_routeedit_initial.py b/resources/tester/test_b05_routeedit_initial.py index be5cb5e7..ece139ab 100644 --- a/resources/tester/test_b05_routeedit_initial.py +++ b/resources/tester/test_b05_routeedit_initial.py @@ -246,3 +246,59 @@ def test_비중_값_칸_직접_입력(results): "ok": False, "weights": {"expected": 70, "earthwork": 20, "grade": 10}, } + + +def test_추천_갈래_여섯_이름과_가중치_없음(results): + six = results["sixth"] + assert six["labels"] == { + "follow": "예상노선 추적", + "earthwork": "토공 최소", + "gentle": "기울기 순한", + "short": "짧은 노선", + "balance": "흙 균형", + "weighted": "비중 계산", + } + # 짧은 노선 · 흙 균형은 비중이 없다(weights 0/0/0 이 와도 이름만) + assert six["shortLabel"] == "짧은 노선" and six["balanceLabel"] == "흙 균형" + page = os.path.join(HERE, "..", "..", "B05_Profile", "B05_Profile_UI_RouteEdit_Recommend.ts") + with open(page, encoding="utf-8") as handle: + text = handle.read() + order = [ + text.index(f'"{name}"') for name in ("follow", "earthwork", "gentle", "short", "balance") + ] + assert order == sorted(order) # 알약 순서: 추적 · 토공 · 기울기 · 짧은 · 균형 · 비중 + + +def test_비교값_남는_흙(results): + six = results["sixth"] + assert six["surplus"] == ["+122㎥(남음)", "−578㎥(모자람)", "0㎥(균형)", "0㎥(균형)"] + assert six["metricsPlus"].endswith( + "성토 900㎥ · 남는 흙 +122㎥(남음) · 기준 초과 2구간 · 이격 3.3m" + ) + assert "남는 흙 −578㎥(모자람)" in six["metricsMinus"] + # 서버가 안 주면 칸이 없다(옛 답) — 기존 비교값 글 그대로 + assert "남는 흙" not in six["metricsNone"] + + +def test_같은_노선_알림_줄(results): + six = results["sixth"] + assert six["similarTwo"] == ( + "이 지형은 제약 안에서 갈 길이 폭 13.5m 한 띠뿐 — " + "토공 최소 · 기울기 순한과 거의 같은 노선(가장 먼 거리 6.1m)" + ) + assert six["similarOne"] == ( + "이 지형은 제약 안에서 갈 길이 폭 13.5m 한 띠뿐 — 흙 균형과 거의 같은 노선(가장 먼 거리 6.1m)" + ) + # 띠 폭이 없어도 줄은 선다 · 받침 없는 이름은 「와」 + assert "폭" not in six["similarNoBand"] and "비중 계산과 거의" in six["similarNoBand"] + # 없으면 줄 숨김 — 빈 목록 · 칸 없음 · 선택 없음 + assert six["similarEmpty"] == "" and six["similarMissing"] == "" and six["similarNull"] == "" + assert six["similarUnknown"] == [] # 이름 · 거리를 모르는 항목은 버림 + assert six["bandParsed"] == 13.5 + assert "제약 값을 바꾸면" in six["hint"] + + +def test_비중_계산_넘은_구간_사유_그대로(results): + six = results["sixth"] + assert six["weightedViolation"] == "제약 못 지킨 구간 1곳: No.9+0.0 29.1%(비중 계산)" + assert six["weightedViolationReason"] == "No.9+0.0 29.1%(비중 계산)" diff --git a/resources/tester/test_b05_routeedit_initial_roundtrip.py b/resources/tester/test_b05_routeedit_initial_roundtrip.py index a3e9d833..14d7d9a0 100644 --- a/resources/tester/test_b05_routeedit_initial_roundtrip.py +++ b/resources/tester/test_b05_routeedit_initial_roundtrip.py @@ -43,6 +43,8 @@ LOGICS: list[tuple[str, dict[str, float] | None]] = [ ("follow", None), ("earthwork", None), ("gentle", None), + ("short", None), + ("balance", None), ("weighted", {"expected": 50, "earthwork": 25, "grade": 25}), ] From 26eac086a8e0aef3e31d588755e0f1b8d3c58f4f Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 30 Sep 2026 08:15:55 +0900 Subject: [PATCH 08/18] =?UTF-8?q?feat(ui=5Ftemplate):=20=EA=B3=A0=EB=A5=B4?= =?UTF-8?q?=EA=B8=B0=20=EB=AA=A8=EB=8B=AC=20=E2=80=94=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=EB=AC=BC=20=EB=B3=B5=EC=82=AC=EB=B3=B8=20=EA=B8=B0=EC=A4=80=20?= =?UTF-8?q?=C2=B7=20[=EC=9E=AC=EC=A0=81=EC=9A=A9]=20=C2=B7=20[=EA=B0=9C?= =?UTF-8?q?=EC=9D=B8=EC=97=90=20=EC=A0=80=EC=9E=A5]=20=C2=B7=20=EB=82=98?= =?UTF-8?q?=EB=AC=B4=20=EC=98=A4=EB=A5=B8=EC=AA=BD=20=ED=81=B4=EB=A6=AD=20?= =?UTF-8?q?=EC=83=88=20=ED=95=AD=EB=AA=A9=20=C2=B7=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20(PLAN=2026-3b)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 카드 · 편집이 그 구조물의 복사본(폴더 = 구조물 id · 관 = pipe@측점)을 읽고 고침 · 같은 항목을 고른 구조물끼리 값이 따로 - [재적용](덮이는 것을 확인 창으로 알리고 덮임 부분을 알림) · [개인에 저장](경로 · 이름 물음) · 상태 줄에 고침 · 마스터가 바뀜 · 원본 없음 표시 - 나무 개인 그룹 · 폴더 오른쪽 클릭 「새 항목」 → 돌려받은 summary_item 을 폼에 지정 · [적용] 뒤 [저장] 안내 한 줄 - B05 구조물 폼이 구조물 id 를 모달에 넘김 Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01EauA2ATmqNRbeV48s9CcBr --- .../B05_Profile_UI_Structures_Panel.ts | 5 + .../tester/helper_item_picker_project.cjs | 80 ++++++-- resources/tester/test_item_picker_detail.py | 68 +++++-- resources/tester/ui/item-picker-data.test.ts | 41 +++- ui_template/ui_template_item_picker.css | 27 +++ ui_template/ui_template_item_picker.ts | 89 +++++++- ui_template/ui_template_item_picker_ask.ts | 97 +++++++++ ui_template/ui_template_item_picker_detail.ts | 190 ++++++++++++++---- ui_template/ui_template_item_picker_edit.ts | 6 +- .../ui_template_item_picker_project.ts | 102 +++++++--- 10 files changed, 594 insertions(+), 111 deletions(-) create mode 100644 ui_template/ui_template_item_picker_ask.ts diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel.ts b/B05_Profile/B05_Profile_UI_Structures_Panel.ts index d38f48ae..357b6e80 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel.ts @@ -25,6 +25,7 @@ import { type FacilityAttributes } from "./B05_Profile_UI_Drainage_Facility"; import { field } from "./B05_Profile_UI_Structures_Fields"; import { renderOptionFields as renderOptionFieldsInto } from "./B05_Profile_UI_Structures_Panel_Options"; import { openItemPicker } from "@ui/ui_template_item_picker"; +import { pipeStructureId } from "@ui/ui_template_item_picker_project"; import { buildStructuresForm } from "./B05_Profile_UI_Structures_Form"; import { bindStructuresEvents, @@ -501,6 +502,10 @@ export function createStructuresSection( current: summary.get(), canCopy: true, projectId: sectionOptions.projectId ?? null, + // 복사본은 구조물마다 — 고치는 구조물의 id(관 지점 = `pipe@측점`) · 추가 전이면 null(원본 보기만) + structureId: + editingId ?? + (selectedPipeChainage !== null ? pipeStructureId(selectedPipeChainage) : null), }).then((result) => { if (result.action === "cancel") return; summary.set(result.action === "apply" ? result.item : null); diff --git a/resources/tester/helper_item_picker_project.cjs b/resources/tester/helper_item_picker_project.cjs index 25343dbf..55ad7a21 100644 --- a/resources/tester/helper_item_picker_project.cjs +++ b/resources/tester/helper_item_picker_project.cjs @@ -75,8 +75,26 @@ globalThis.fetch = async (url, init = {}) => { const [, , folderEnc, partEnc] = m; if (!folderEnc && method === "GET") return reply(200, { 항목: Object.values(state.folders).map((x) => x.info) }); + if (folderEnc === "new" && method === "POST") { + if (state.noCompany) + return reply(409, { detail: "회사부터 등록 — 개인 층은 회사 연결이 필요합니다." }); + const item = { + layer: "personal", + owner: "1/3", + key: "SS900001", + name: [...body.경로, body.이름].join(" "), + }; + return reply(200, { + key: "SS900001", + 이름: item.name, + 폴더: body.structure, + summary_item: item, + }); + } if (!folderEnc && method === "POST") { - const folder = `${body.layer}_${(body.owner ?? "none").replace(/\//g, "-")}_${body.key}`; + if (!body.structure) + return reply(400, { detail: "미리 가져올 구조물(structure)이 필요합니다." }); + const folder = body.structure.replace("#", "n"); state.folders[folder] = { info: { 폴더: folder, 원본: body, 마스터바뀜: false, 가져온날: "2026-09-30T01:00:00" }, parts: { 면적: { 시트: [{ id: "s", 칸: { A1: { 값: 1 } } }] }, 입력값: null }, @@ -86,9 +104,24 @@ globalThis.fetch = async (url, init = {}) => { const item = state.folders[decodeURIComponent(folderEnc)]; if (!item) return reply(404, { detail: "복사본이 없음" }); const part = partEnc && decodeURIComponent(partEnc); - if (part === "refresh") { + if (part === "reapply") { item.parts.면적 = { 시트: [{ id: "s", 칸: { A1: { 값: 1 } } }] }; - return reply(200, { 폴더: item.info.폴더, 가져온날: "2026-09-30T02:00:00", 원본판: "새판" }); + return reply(200, { + 폴더: item.info.폴더, + 가져온날: "2026-09-30T02:00:00", + 원본판: "새판", + 덮임: ["면적"], + }); + } + if (part === "save-as" && method === "POST") { + if (state.noCompany) + return reply(409, { detail: "회사부터 등록 — 개인 층은 회사 연결이 필요합니다." }); + const name = body.이름 ?? "복사"; + return reply(200, { + key: "SS900002", + 이름: name, + summary_item: { layer: "personal", owner: "1/3", key: "SS900002", name }, + }); } if (part === "calc") return reply(200, { @@ -122,17 +155,24 @@ const row = (over = {}) => ({ key: "SS000001", 열: "rv", layer: "system", owner async function main() { const out = {}; - out.folders = [ - project.folderOf(row()), - project.folderOf(row({ layer: "company", owner: "3" })), - project.folderOf(row({ layer: "personal", owner: "3/12", key: "SS000002" })), - ]; + // 구조물 id → 복사본 폴더(서버 `folder_name`) · 관 지점 id(서버 `pipe@{round(측점, 3)}` 파이썬 float 글) + out.folders = ["st-1", "#3", "pipe@85.05"].map(project.structureFolder); + out.pipeIds = [85.05, 100, 258.12, 0.1234, 12.0004].map(project.pipeStructureId); const items = project.projectItems("P1"); out.listEmpty = await items.list(); - const info = await items.importItem(row({ layer: "personal", owner: "1/3" })); + const info = await items.importItem(row({ layer: "personal", owner: "1/3" }), "pipe@85.05"); out.info = info; out.importCall = state.calls.find((c) => c.method === "POST"); + out.sameItem = [ + project.sameItem(info, row({ layer: "personal", owner: "1/3" })), + project.sameItem(info, row({ layer: "personal", owner: "1/3", key: "SS000002" })), + project.sameItem(info, row()), + ]; + await items.importItem(row(), "").then( + () => (out.noStructure = "성공"), + (error) => (out.noStructure = error.message), + ); out.list = (await items.list()).map((x) => x.폴더); out.read = await items.read(info.폴더, "면적"); @@ -164,11 +204,25 @@ async function main() { ); state.readOnly = false; - const fresh = await items.refresh(info); - out.refresh = fresh; - out.refreshCall = state.calls - .filter((c) => /refresh$/.test(c.url)) + const again = await items.reapply(info); + out.reapply = again; + out.reapplyCall = state.calls + .filter((c) => /reapply$/.test(c.url)) .map((c) => `${c.method} ${c.url}`); + out.saveAs = await items.saveAs(info.폴더, ["sub5", "경로"], "내 항목"); + out.saveAsCall = state.calls.find((c) => /save-as$/.test(c.url)).body; + out.newItem = await items.newItem("pipe@85.05", ["개인", "폴더"], "새 항목", "m"); + out.newCall = state.calls.find((c) => /items\/new$/.test(c.url)).body; + state.noCompany = true; + await items.saveAs(info.폴더, [], "x").then( + () => (out.noCompanySave = "성공"), + (error) => (out.noCompanySave = error.message), + ); + await items.newItem("pipe@85.05", [], "x", "").then( + () => (out.noCompanyNew = "성공"), + (error) => (out.noCompanyNew = error.message), + ); + state.noCompany = false; out.calc = await items.calc(info.폴더); out.calcMissing = await items.calc("없는폴더"); diff --git a/resources/tester/test_item_picker_detail.py b/resources/tester/test_item_picker_detail.py index 63956516..c261a3e4 100644 --- a/resources/tester/test_item_picker_detail.py +++ b/resources/tester/test_item_picker_detail.py @@ -36,26 +36,28 @@ def out(): return json.loads(proc.stdout) -def test_폴더_이름은_서버_snapshot_folder_name_과_같은_글(out): - rows = [ - {"layer": "system", "owner": None, "key": "SS000001"}, - {"layer": "company", "owner": "3", "key": "SS000001"}, - {"layer": "personal", "owner": "3/12", "key": "SS000002"}, +def test_구조물_id_는_복사본_폴더와_관_지점_id_로(out): + assert out["folders"] == ["st-1", "n3", "pipe@85.05"] # `#차례` = `n차례` (서버 folder_name) + # 서버 `pipe@{round(측점, 3)}` — 파이썬 float 글(정수도 `100.0`) + assert out["pipeIds"] == [ + f"pipe@{round(x, 3)}" for x in (85.05, 100.0, 258.12, 0.1234, 12.0004) ] - assert out["folders"] == [snapshot_folder_name(row) for row in rows] - assert out["info"]["폴더"] == snapshot_folder_name( - {"layer": "personal", "owner": "1/3", "key": "SS000001"} - ) -def test_가져오기는_POST_원본_참조_목록은_항목(out): +def test_가져오기는_구조물마다_POST_본문에_structure(out): assert out["listEmpty"] == [] assert out["importCall"] == { "method": "POST", "url": "/api/projects/P1/items", - "body": {"layer": "personal", "owner": "1/3", "key": "SS000001"}, + "body": {"structure": "pipe@85.05", "layer": "personal", "owner": "1/3", "key": "SS000001"}, } - assert out["list"] == [out["info"]["폴더"]] + assert out["info"]["폴더"] == "pipe@85.05" # 폴더 = 구조물 id + assert out["list"] == ["pipe@85.05"] + assert out["noStructure"] == "미리 가져올 구조물(structure)이 필요합니다." # 서버 까닭 그대로 + + +def test_같은_구조물이_다른_항목으로_바뀌었는지_원본_참조로_가림(out): + assert out["sameItem"] == [True, False, False] def test_읽기는_내용만_돌려주고_없으면_null(out): @@ -76,14 +78,31 @@ def test_판이_달라지면_409_까닭_권한_없으면_서버_까닭_그대로 assert out["readOnly"] == "프로젝트 편집 권한이 없음" -def test_새로_가져오기와_계산_한_줄(out): - assert out["refreshCall"][0].startswith("POST /api/projects/P1/items/") - assert out["refreshCall"][0].endswith("/refresh") - assert out["refresh"]["마스터바뀜"] is False and out["refresh"]["원본판"] == "새판" +def test_재적용은_덮임을_돌려주고_계산_한_줄(out): + assert out["reapplyCall"][0] == "POST /api/projects/P1/items/pipe%4085.05/reapply" + assert out["reapply"]["covered"] == ["면적"] # 덮이는 것을 알림으로 + assert out["reapply"]["info"]["마스터바뀜"] is False assert out["calc"] == {"total": 12345.6, "gaps": 2, "stuck": 1} assert out["calcMissing"] is None # 없는 복사본 = 계산 줄 없음 +def test_개인에_저장과_새_항목_본문_summary_item(out): + assert out["saveAsCall"] == {"to": "personal", "경로": ["sub5", "경로"], "이름": "내 항목"} + assert out["saveAs"]["summary_item"]["key"] == "SS900002" + assert out["newCall"] == { + "structure": "pipe@85.05", + "경로": ["개인", "폴더"], + "이름": "새 항목", + "단위": "m", + } + assert out["newItem"]["summary_item"]["name"] == "개인 폴더 새 항목" + + +def test_회사_없음_409_는_서버_까닭_그대로(out): + for key in ("noCompanySave", "noCompanyNew"): + assert out[key].startswith("회사부터 등록"), key + + def test_입력_목록은_한_번만_받고_로직마다_갈라_둠(out): assert out["inputsHits"] == 1 assert out["inputsLogics"] == [["GA1", ["지역", "소량감률"]], ["GA2", ["지역"]]] @@ -164,3 +183,20 @@ def test_소스가_700줄을_넘지_않음(): M02 / "M02_MasterTemplete_Inputs_Model.ts", ): assert len(path.read_text(encoding="utf-8").splitlines()) <= 700, path.name + + +def test_나무_오른쪽_클릭은_메뉴_항목을_밖에서_주는_자리(): + tree = (UI / "ui_template_tree.ts").read_text(encoding="utf-8") + assert "menu?: () => TreeMenuItem[]" in tree and "export function openTreeMenu(" in tree + assert '"contextmenu"' in tree + picker = (UI / "ui_template_item_picker.ts").read_text(encoding="utf-8") + # 새 항목 메뉴는 개인 그룹 · 폴더에만(마스터 · 회사는 메뉴 없음) · 프로젝트 안에서만 + assert 'folder.layer !== "personal" || !opts.projectId' in picker + assert "menu: menuOf(node)" in picker + + +def test_B05_구조물_폼이_구조물_id_를_모달에_넘김(): + panel = (ROOT / "B05_Profile" / "B05_Profile_UI_Structures_Panel.ts").read_text( + encoding="utf-8" + ) + assert "pipeStructureId(selectedPipeChainage)" in panel and "editingId ??" in panel diff --git a/resources/tester/ui/item-picker-data.test.ts b/resources/tester/ui/item-picker-data.test.ts index be8ffa93..37f0c5db 100644 --- a/resources/tester/ui/item-picker-data.test.ts +++ b/resources/tester/ui/item-picker-data.test.ts @@ -10,6 +10,7 @@ import { isCurrent, itemName, pathIds, + pathOf, rowId, rowTip, shown, @@ -89,13 +90,8 @@ assert.equal( "옹벽 H=2m", ); const placeholderTree = buildTree([fresh], labels); -assert.equal(placeholderTree[2].children?.[0].label, "분류 없음"); // 종류 -assert.equal(placeholderTree[2].children?.[0].children?.[0].label, "분류 없음"); // 분류1 -assert.deepEqual(pathIds(fresh, labels), [ - "personal", - "personal|분류 없음", - "personal|분류 없음|분류 없음", -]); +assert.equal(placeholderTree[2].children?.[0].label, "n1"); // 경로가 비면 폴더 없이 그룹 바로 밑 잎 +assert.deepEqual(pathIds(fresh, labels), ["personal"]); // 툴팁 = 층 이름 + 키 assert.equal(rowTip(rows[7], labels), "개인 · SS000201"); @@ -123,14 +119,38 @@ assert.deepEqual( const cat = system.children?.[0].children?.[0]; assert.equal(cat?.label, "메쌓기"); assert.equal(cat?.count, 2); -const leaf = cat?.children?.[0]; +const size = cat?.children?.[0]; // 폴더 깊이 자유 — 경로 앞부분이 모두 폴더 +assert.equal(size?.label, "1.2m"); +assert.deepEqual(size?.folder, { layer: "system", path: ["돌기슭막이", "메쌓기", "1.2m"] }); +assert.deepEqual(tree[2].folder, { layer: "personal", path: [] }); // 그룹 줄 = 빈 경로 +const leaf = size?.children?.[0]; assert.equal(leaf?.label, "돌기슭막이 메쌓기 1.2m 기초유"); assert.equal(leaf?.tip, "마스터 · SS000001"); // 툴팁 = 층 이름 + 키 assert.equal(leaf?.count, null); assert.ok(leaf?.row); -// 분류가 빈 항목은 「분류 없음」 한 칸에 모임 -assert.equal(tree[1].children?.[1].children?.[0].label, "분류 없음"); +// 경로가 짧은 항목은 얕은 폴더에(옹벽 H=2m = 폴더 옹벽 · 잎 「옹벽 H=2m」) +assert.equal(tree[1].children?.[1].label, "옹벽"); +assert.equal(tree[1].children?.[1].children?.[0].label, "옹벽 H=2m"); +assert.equal(tree[1].children?.[1].children?.[0].folder, undefined); // 잎에는 folder 없음 + +// 서버가 준 `경로` 가 종류 · 분류보다 앞 — 이름 = 경로 이음 · 자리표는 뺌 +const deep = row("SS000400", "d1", "무시", ["무시", "", ""], "personal", { + 경로: ["가", "나", "다", "라", "마"], +}); +assert.deepEqual(pathOf(deep), ["가", "나", "다", "라", "마"]); +assert.equal(itemName(deep), "가 나 다 라 마"); +assert.deepEqual(pathIds(deep), [ + "personal", + "personal|가", + "personal|가|나", + "personal|가|나|다", + "personal|가|나|다|라", +]); +assert.deepEqual(pathOf({ key: "k", 열: "x", 경로: ["새 분류1", "옹벽"] }), ["옹벽"]); +const deepTree = buildTree([deep], labels)[2]; +assert.equal(deepTree.children?.[0].children?.[0].children?.[0].children?.[0].label, "라"); +assert.equal(deepTree.count, 1); // 빈 그룹도 한 줄은 남음(회사 없는 사람의 회사 · 개인 그룹) const onlySystem = buildTree( @@ -198,6 +218,7 @@ assert.deepEqual(pathIds(rows[0], labels), [ "system", "system|돌기슭막이", "system|돌기슭막이|메쌓기", + "system|돌기슭막이|메쌓기|1.2m", ]); // 폴더 모양 — 다섯 줄 · 이름은 서버 names · 없으면 키만(작은 글) · 둘 다 없으면 빈 키 diff --git a/ui_template/ui_template_item_picker.css b/ui_template/ui_template_item_picker.css index 4d8d73dd..44852a47 100644 --- a/ui_template/ui_template_item_picker.css +++ b/ui_template/ui_template_item_picker.css @@ -242,3 +242,30 @@ overflow: auto; } } + +/* 구조물 복사본 상태 줄 — 고침 · 마스터가 바뀜 · 원본 없음 표시 */ +.ui-item-picker__badge { + padding: 0 var(--spacing-8); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + color: var(--color-text); +} + +.ui-item-picker__badge.is-warn { + border-color: var(--color-warning); + color: var(--color-text); + font-weight: 600; +} + +/* [적용] 뒤 구조물 [저장] 안내 한 줄 — 단추 왼쪽 */ +.ui-item-picker__saveline { + margin-right: auto; + align-self: center; + color: var(--color-text-secondary); + font-size: var(--text-caption); +} + +/* 글 묻는 작은 창(개인에 저장 · 새 항목) */ +.ui-modal.ui-item-ask { + width: min(28rem, 92vw); +} diff --git a/ui_template/ui_template_item_picker.ts b/ui_template/ui_template_item_picker.ts index 5baec0eb..4062fbd8 100644 --- a/ui_template/ui_template_item_picker.ts +++ b/ui_template/ui_template_item_picker.ts @@ -3,8 +3,11 @@ * 항목 고르기 모달 — 구조물집계표 항목(SS 한 줄)을 마스터 · 회사 · 개인 세 그룹 나무에서 고름. * 키(SS · UA …)는 앞에 내세우지 않고 이름으로 보임 · 키는 마우스를 올리면(PLAN.md 21-0). * - * const result = await openItemPicker({ current, canCopy }); + * const result = await openItemPicker({ current, canCopy, projectId, structureId }); * result.action = "apply"(result.item = 21-1 `summary_item`) | "clear" | "cancel" + * + * 26-3b — 카드 · 편집 · [재적용] · [개인에 저장] 은 **고르는 구조물의 복사본**(`structureId`) 기준. + * 나무 개인 그룹 · 폴더를 오른쪽 클릭하면 「새 항목」(프로젝트에서 새 항목 → 개인 층에도 만들고 폼에 지정). * ========================================================================== */ import "./ui_template_item_picker.css"; @@ -27,7 +30,9 @@ import { } from "./ui_template_item_picker_data"; import { t } from "./ui_template_locale"; import { openModal } from "./ui_template_modal"; -import { createTreeRow, renderTree, type TreeNode } from "./ui_template_tree"; +import { createTreeRow, renderTree, type TreeMenuItem, type TreeNode } from "./ui_template_tree"; +import { askFields } from "./ui_template_item_picker_ask"; +import { projectItems, type CreatedItem } from "./ui_template_item_picker_project"; import { createItemDetail, type EditRequest } from "./ui_template_item_picker_detail"; import { createEditView, type EditView } from "./ui_template_item_picker_edit"; @@ -40,6 +45,8 @@ export interface ItemPickerOptions { canCopy: boolean; /** 프로젝트 안에서 부르면 그 id — 카드 [수정] 이 프로젝트 복사본을 고침(없으면 원본 보기만 · 25-2d) */ projectId?: string | null; + /** 고르는 구조물의 id(`structure_id` · 관 지점 `pipe@측점`) — 복사본이 이 구조물 것 · 없으면(추가 전) 원본 보기만 */ + structureId?: string | null; } export type ItemPickerResult = @@ -88,6 +95,23 @@ async function copyToPersonal(row: PickerRow): Promise { return body?.key ?? null; } +const NEW_TEXT = { + menu: "새 항목", + title: "새 항목", + note: "{value} 안에 새 항목을 만듦 — 개인 층에도 저장되고 이 구조물에 지정됨", + name: "이름", + unit: "단위", + ok: "만들기", + cancel: "취소", + empty: "{label} 을(를) 넣을 것", + made: "새 항목을 만들어 지정했음 — 구조물 [저장]을 눌러야 이 구조물 복사본이 남음", + failed: "새 항목을 만들지 못함 — {value}", + noStructure: "구조물을 먼저 추가해야 새 항목을 이 구조물에 지정할 수 있음", + saveLine: "[적용] 뒤 구조물 [저장]을 눌러야 이 구조물 복사본이 남음", +}; + +const fill = (text: string, value: string): string => text.replace("{value}", value); + const message = (error: unknown): string => error instanceof Error ? error.message : String(error); @@ -106,7 +130,10 @@ export function openItemPicker(opts: ItemPickerOptions): Promise openEdit(request), + // 개인에 저장 — 나무를 다시 받고 새 항목을 고름(개인 그룹에 뜸) + onCreated: (created) => void load(created.key), }); const cols = el("div", { className: "ui-item-picker__cols", @@ -147,11 +174,25 @@ export function openItemPicker(opts: ItemPickerOptions): Promise TreeMenuItem[]) | undefined => { + const folder = node.folder; + if (!folder || folder.layer !== "personal" || !opts.projectId) return undefined; + return () => [ + { + label: NEW_TEXT.menu, + run: () => void createNew(folder.path), + disabledReason: opts.structureId ? undefined : NEW_TEXT.noStructure, + }, + ]; + }; + const toTree = (node: PickNode, searching: boolean): TreeNode => ({ id: node.id, label: node.label, count: node.count, tip: node.tip, + menu: menuOf(node), open: searching || opened.has(node.id), run: node.row ? () => choose(node.row as PickerRow) : undefined, children: node.children?.map((child) => toTree(child, searching)), @@ -234,9 +275,48 @@ export function openItemPicker(opts: ItemPickerOptions): Promise { + if (!opts.projectId || !opts.structureId) return; + const where = [labels().layers.personal, ...path].join(" › "); + const got = await askFields({ + title: NEW_TEXT.title, + note: fill(NEW_TEXT.note, where), + fields: [ + { key: "name", label: NEW_TEXT.name, required: true }, + { key: "unit", label: NEW_TEXT.unit, placeholder: "m · ㎥ · 개소" }, + ], + okLabel: NEW_TEXT.ok, + cancelLabel: NEW_TEXT.cancel, + closeLabel: t("M02_Close"), + emptyText: NEW_TEXT.empty, + }); + if (!got) return; + try { + const made: CreatedItem = await projectItems(opts.projectId).newItem( + opts.structureId, + path, + got.name, + got.unit, + ); + showToast(NEW_TEXT.made, "success"); + finish({ action: "apply", item: made.summary_item as SummaryItem }); + } catch (error) { + showToast(fill(NEW_TEXT.failed, message(error)), "error"); // 회사 없음 409 = 「회사부터 등록」 그대로 + } + } + const actions = el("div", { className: "ui-item-picker__actions", - children: [copyButton, manageButton, clearButton, applyButton], + children: [ + ...(opts.projectId + ? [el("span", { className: "ui-item-picker__saveline", text: NEW_TEXT.saveLine })] + : []), + copyButton, + manageButton, + clearButton, + applyButton, + ], }); /** 카드 [수정] — 모달 안에서 편집 화면으로 바뀜(나무 · 카드 · 단추는 숨김) */ @@ -250,12 +330,13 @@ export function openItemPicker(opts: ItemPickerOptions): Promise | null> { + return new Promise((resolve) => { + let done = false; + const finish = (value: Record | null): void => { + if (done) return; + done = true; + resolve(value); + }; + openModal({ + title: opts.title, + closeLabel: opts.closeLabel, + dialogClass: "ui-item-ask", + backdropClass: "ui-item-ask__backdrop", + beforeClose: () => { + finish(null); + return true; + }, + mount: (body, close) => { + const inputs = opts.fields.map((field) => ({ + field, + input: createInputField({ + label: field.label, + value: field.value ?? "", + placeholder: field.placeholder, + }), + })); + const submit = (): void => { + let bad = false; + for (const { field, input } of inputs) { + const empty = field.required && input.input.value.trim() === ""; + input.setError(empty ? opts.emptyText.replace("{label}", field.label) : undefined); + bad ||= !!empty; + } + if (bad) return; + finish( + Object.fromEntries( + inputs.map(({ field, input }) => [field.key, input.input.value.trim()]), + ), + ); + close(); + }; + for (const { input } of inputs) + input.input.addEventListener("keydown", (event) => { + if (event.key === "Enter") submit(); + }); + body.append( + ...(opts.note ? [el("p", { className: "ui-item-picker__where", text: opts.note })] : []), + ...inputs.map(({ input }) => input.root), + el("div", { + className: "ui-item-picker__actions", + children: [ + createButton({ + label: opts.cancelLabel, + variant: "ghost", + onClick: () => { + finish(null); + close(); + }, + }), + createButton({ label: opts.okLabel, variant: "filled", onClick: submit }), + ], + }), + ); + }, + }); + }); +} diff --git a/ui_template/ui_template_item_picker_detail.ts b/ui_template/ui_template_item_picker_detail.ts index 7abeeda4..482a3b23 100644 --- a/ui_template/ui_template_item_picker_detail.ts +++ b/ui_template/ui_template_item_picker_detail.ts @@ -1,12 +1,14 @@ /* ============================================================================= * ui_template_item_picker_detail.ts - * 항목 고르기 모달 오른쪽 상세(계획서 25-2d) — 글 목록이던 것을 **카드 넷**(도면 · 면적산출 · 재료산출 · - * 선택 입력)으로. 카드는 M02 와 같은 한 벌(`M02_MasterTemplete_ItemCards` · `_Inputs`)을 늦게 받아 씀 + * 항목 고르기 모달 오른쪽 상세(계획서 25-2d · 26-3b) — **카드 넷**(도면 · 면적산출 · 재료산출 · 선택 입력). + * 카드는 M02 와 같은 한 벌(`M02_MasterTemplete_ItemCards` · `_Inputs`)을 늦게 받아 씀 * (B05 · B06 번들에 CAD · 스프레드시트가 안 섞이게 `import()`). * - * 자료 자리 — 프로젝트에 가져온 항목은 **프로젝트 복사본**을 읽고, 아직이면 **원본 층**을 읽음(읽기만). - * [수정] 은 복사본만 고침 — 안 가져온 항목은 「프로젝트로 가져와 고침」(미리 가져오기 뒤 편집). - * 마스터 · 회사 · 개인 원본은 이 모달이 안 바꿈. 편집 화면은 `ui_template_item_picker_edit.ts`. + * 자료 자리 — **그 구조물의 복사본**(폴더 = 구조물 id · 같은 항목을 고른 구조물도 복사본은 따로)을 읽고, + * 그 구조물에 아직 없으면 **원본 층**을 읽음(읽기만). [수정] 은 복사본만 고침 — 아직이면 「이 구조물로 가져와 고침」 + * (미리 가져오기 뒤 편집). [재적용] = 원본을 다시 가져옴(덮이는 것을 확인 창으로 알림) · + * [개인에 저장] = 이 구조물 복사본을 개인 층 새 항목으로. 마스터 · 회사 · 개인 원본은 이 모달이 안 바꿈 + * (개인에 저장 · 새 항목은 개인 층에 **새 항목**을 더할 뿐). 편집 화면은 `ui_template_item_picker_edit.ts`. * ========================================================================== */ import { createButton, el, showConfirmDialog, showToast } from "./ui_template_elements"; @@ -19,20 +21,23 @@ import type { EditKind, ItemCardsHandle } from "../M02_MasterTemplete/M02_Master import type { ItemSource } from "../M02_MasterTemplete/M02_MasterTemplete_ItemSource"; import type { InputsValueHandle } from "../M02_MasterTemplete/M02_MasterTemplete_Inputs"; import type { ValueDoc } from "../M02_MasterTemplete/M02_MasterTemplete_Inputs_Model"; +import { askFields } from "./ui_template_item_picker_ask"; import { folderLines, itemName, layerOf, + pathOf, rowTip, - shown, type PickerLayer, type PickerRow, } from "./ui_template_item_picker_data"; import { - folderOf, loadInputs, PART_OF, projectItems, + sameItem, + structureFolder, + type CreatedItem, type ProjectItemInfo, type ProjectItems, } from "./ui_template_item_picker_project"; @@ -49,10 +54,14 @@ export interface EditRequest { export interface ItemDetailOptions { /** 프로젝트 안에서 열렸으면 그 id — 없으면 원본 보기만(고침 없음) */ projectId: string | null; + /** 고르는 구조물의 id(`structure_id` · 관 지점 `pipe@측점`) — 아직 추가 전이면 null = 복사본 없음 */ + structureId: string | null; layerLabels: Record; hint: string; unit: string; onEdit: (request: EditRequest) => void; + /** [개인에 저장] 으로 개인 층에 새 항목이 생김 — 나무를 다시 받고 그 항목을 고름 */ + onCreated: (created: CreatedItem) => void; } export interface ItemDetail { @@ -60,24 +69,56 @@ export interface ItemDetail { show: (row: PickerRow | null) => void; /** 편집을 마치고 돌아옴 — 지금 항목을 다시 읽음 */ reload: () => void; + /** 이 구조물 복사본에 고침이 생김 — 목록을 다시 받지 않고 표시만 */ + markEdited: () => void; } const TEXT = { - importEdit: "프로젝트로 가져와 고침", - notImported: "프로젝트에 아직 안 가져옴 — 원본을 보고 있음", - imported: "프로젝트에 가져옴 {value}", - changed: "마스터가 바뀜 — 프로젝트 복사본은 가져온 때 그대로", - refresh: "새로 가져오기", - refreshAsk: "프로젝트 복사본의 고친 내용이 원본으로 덮임 — 새로 가져올까요?", - importFailed: "프로젝트로 가져오지 못함 — {value}", - refreshFailed: "새로 가져오지 못함 — {value}", + importEdit: "이 구조물로 가져와 고침", + noStructure: + "구조물을 먼저 추가하면 이 항목을 그 구조물로 가져와 고칠 수 있음 — 지금은 원본을 보고 있음", + notImported: "이 구조물에는 아직 안 가져옴 — 원본을 보고 있음", + otherItem: "이 구조물의 복사본은 「{value}」 — [수정]하면 이 항목으로 바뀜", + otherEdited: + "이 구조물의 복사본 「{value}」 에서 고친 것이 있음 — 이 항목으로 바꾸면 그 고침은 _지난 폴더로 옮겨짐. 바꿀까요?", + imported: "이 구조물 복사본 · 가져옴 {value}", + edited: "고침 있음", + changed: "마스터가 바뀜 — 복사본은 가져온 때 그대로", + gone: "원본이 없어짐 — 복사본은 그대로 씀", + reapply: "재적용", + reapplyTip: + "원본을 다시 가져옴 — 이 구조물 복사본에서 고친 것(도면 · 시트 · 선택 입력 자리)이 원본 것으로 덮임", + reapplyAsk: + "원본을 다시 가져옴 — 이 구조물 복사본에서 고친 도면 · 면적산출 · 재료산출 · 선택 입력 자리가 원본 것으로 덮임(넣은 값은 같은 키면 남음). 재적용할까요?", + reapplied: "재적용했음 — 덮인 것: {value}", + reappliedNone: "재적용했음 — 덮인 것 없음(원본과 같았음)", + reapplyFailed: "재적용하지 못함 — {value}", + saveAs: "개인에 저장", + saveAsTip: "이 구조물 복사본을 내 개인 층의 새 항목으로 저장 — 단가 · 넣은 값은 안 가져감", + saveAsTitle: "개인에 저장", + saveAsNote: "이 구조물 복사본을 개인 층 새 항목으로 저장함 — 경로는 폴더 이름을 「/」 로 이음", + pathLabel: "경로(폴더)", + nameLabel: "이름", + saved: "개인 층에 저장했음", + saveFailed: "개인에 저장하지 못함 — {value}", + ok: "확인", + cancel: "취소", + close: "닫기", + empty: "{label} 을(를) 넣을 것", + importFailed: "이 구조물로 가져오지 못함 — {value}", listFailed: "프로젝트 항목을 못 읽음 — {value}", calc: "복사본 계산 — 계 {total}원 · 빠진 입력 {gaps}개", calcStuck: " · 멈춘 로직 {stuck}개", - loading: "읽는 중…", noCopy: "복사본에 없음", combo: "자재조합", price: "일위대가", + parts: { + 도면: "도면", + 면적: "면적산출", + 재료: "재료산출", + 자재조합: "자재조합", + 입력연결: "선택 입력 자리", + } as Record, }; const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); @@ -97,14 +138,19 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail { let cards: ItemCardsHandle | null = null; let inputs: InputsValueHandle | null = null; let source: ItemSource = { layer: "system", projectId: null, read: () => Promise.reject() }; + /** 이 구조물에 있는 복사본 — 고른 항목의 것일 때만(다른 항목의 것이면 `other`) */ let info: ProjectItemInfo | null = null; + let other: ProjectItemInfo | null = null; const items: ProjectItems | null = opts.projectId ? projectItems(opts.projectId) : null; + const folder = opts.structureId ? structureFolder(opts.structureId) : null; + /** 복사본을 고칠 수 있나 — 프로젝트 안 · 구조물이 있을 때 */ + const canCopy = !!items && !!folder; const small = (text: string): HTMLElement => el("p", { className: "ui-item-picker__where", text }); const paintHead = (row: PickerRow): void => { - const where = [opts.layerLabels[layerOf(row)], shown(row.종류), shown(row.분류1)] + const where = [opts.layerLabels[layerOf(row)], ...pathOf(row).slice(0, -1)] .filter(Boolean) .join(" › "); const pairs = folderLines(row).filter( @@ -126,7 +172,7 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail { ); }; - /** 프로젝트 복사본 또는 원본 층에서 열 하나의 문서를 읽음 — 없으면 던짐(카드가 「없음」) */ + /** 이 구조물 복사본 또는 원본 층에서 열 하나의 문서를 읽음 — 없으면 던짐(카드가 「없음」) */ const makeSource = (row: PickerRow): ItemSource => info && opts.projectId ? { @@ -144,20 +190,28 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail { read: (part, column) => readTemplate(layerOf(row), part, column, null), }; - const paintStatus = async (row: PickerRow, mine: number): Promise => { + const badge = (text: string, className = ""): HTMLElement => + el("span", { className: `ui-item-picker__badge ${className}`.trim(), text }); + + const paintStatus = (row: PickerRow, mine: number): void => { if (!items) return void (status.hidden = true); const nodes: (HTMLElement | string)[] = []; - if (!info) nodes.push(TEXT.notImported); + if (!folder) nodes.push(TEXT.noStructure); + else if (other) nodes.push(fill(TEXT.otherItem, other.이름 ?? other.원본.key)); + else if (!info) nodes.push(TEXT.notImported); else { nodes.push(fill(TEXT.imported, dateOf(info.가져온날))); - if (info.마스터바뀜) { - const button = createButton({ label: TEXT.refresh, variant: "ghost", size: "sm" }); - button.addEventListener("click", () => void refresh(row)); - nodes.push( - el("strong", { className: "ui-item-picker__changed", text: TEXT.changed }), - button, - ); - } + if (info.고침) nodes.push(badge(TEXT.edited)); + if (info.원본없음) nodes.push(badge(TEXT.gone, "is-warn")); + else if (info.마스터바뀜) nodes.push(badge(TEXT.changed, "is-warn")); + const reapply = createButton({ label: TEXT.reapply, variant: "ghost", size: "sm" }); + reapply.title = TEXT.reapplyTip; + reapply.disabled = !!info.원본없음; + reapply.addEventListener("click", () => void reapplyNow(row)); + const saveAs = createButton({ label: TEXT.saveAs, variant: "ghost", size: "sm" }); + saveAs.title = TEXT.saveAsTip; + saveAs.addEventListener("click", () => void saveAsNow(row)); + nodes.push(reapply, saveAs); } status.replaceChildren(...nodes); status.hidden = false; @@ -179,25 +233,66 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail { ); } - async function refresh(row: PickerRow): Promise { - if (!items || !info || !(await showConfirmDialog(TEXT.refreshAsk))) return; + /** 목록은 모달에서 한 번만 받고 바뀐 줄만 갈아 끼움 */ + const upsert = (next: ProjectItemInfo): void => { + infos = [...(infos ?? []).filter((one) => one.폴더 !== next.폴더), next]; + }; + + async function reapplyNow(row: PickerRow): Promise { + if (!items || !info || !(await showConfirmDialog(TEXT.reapplyAsk, TEXT.reapply))) return; try { - const fresh = await items.refresh(info); - infos = (infos ?? []).map((one) => (one.폴더 === fresh.폴더 ? fresh : one)); + const got = await items.reapply(info); + upsert(got.info); + const names = got.covered.map((part) => TEXT.parts[part] ?? part); + showToast( + names.length ? fill(TEXT.reapplied, names.join(" · ")) : TEXT.reappliedNone, + "success", + ); show(row); } catch (error) { - showToast(fill(TEXT.refreshFailed, why(error)), "error"); + showToast(fill(TEXT.reapplyFailed, why(error)), "error"); } } - /** [수정] — 안 가져온 항목이면 먼저 프로젝트로 가져옴 */ + async function saveAsNow(row: PickerRow): Promise { + if (!items || !info) return; + const got = await askFields({ + title: TEXT.saveAsTitle, + note: TEXT.saveAsNote, + fields: [ + { key: "path", label: TEXT.pathLabel, value: pathOf(row).slice(0, -1).join("/") }, + { key: "name", label: TEXT.nameLabel, value: itemName(row), required: true }, + ], + okLabel: TEXT.ok, + cancelLabel: TEXT.cancel, + closeLabel: TEXT.close, + emptyText: TEXT.empty, + }); + if (!got) return; + try { + const path = got.path + .split("/") + .map((part) => part.trim()) + .filter(Boolean); + const created = await items.saveAs(info.폴더, path, got.name); + showToast(TEXT.saved, "success"); + opts.onCreated(created); + } catch (error) { + showToast(fill(TEXT.saveFailed, why(error)), "error"); // 회사 없음 409 등 서버 까닭 그대로 + } + } + + /** [수정] — 이 구조물에 아직 없으면(또는 다른 항목이면) 먼저 이 구조물로 가져옴 */ async function edit(kind: EditKind | "inputs"): Promise { const row = current; - if (!row || !items || !opts.projectId) return; + if (!row || !items || !opts.projectId || !opts.structureId) return; if (!info) { + const ask = other?.고침 ? fill(TEXT.otherEdited, other.이름 ?? other.원본.key) : null; + if (ask && !(await showConfirmDialog(ask))) return; try { - info = await items.importItem(row); - infos = [...(infos ?? []), info]; + info = await items.importItem(row, opts.structureId); + other = null; + upsert(info); } catch (error) { showToast(fill(TEXT.importFailed, why(error)), "error"); return; @@ -234,7 +329,7 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail { inputs = inputModule.createInputsValueCard({ mode: "view", onEdit: () => void edit("inputs") }); cards = cardModule.createItemCards({ source: () => source, - editable: () => !!items, + editable: () => canCopy, onEdit: (kind) => void edit(kind), warnOnFail: () => false, extraCards: [inputs.root], @@ -256,25 +351,34 @@ export function createItemDetail(opts: ItemDetailOptions): ItemDetail { paintHead(row); void (async () => { try { - if (items) infos ??= await items.list(); + if (items && folder) infos ??= await items.list(); } catch (error) { if (mine === seq) showToast(fill(TEXT.listFailed, why(error)), "error"); infos = []; } if (mine !== seq) return; - info = infos?.find((one) => one.폴더 === folderOf(row)) ?? null; + const own = infos?.find((one) => one.폴더 === folder) ?? null; + info = own && sameItem(own, row) ? own : null; + other = own && !info ? own : null; source = makeSource(row); await ensureCards(); if (mine !== seq || !cards || !inputs) return; - const label = info ? null : TEXT.importEdit; + const label = !info && canCopy ? TEXT.importEdit : null; cards.relabel({ structure: label, area: label, material: label }); inputs.setEditLabel(label ?? "선택 입력 수정"); cards.root.hidden = false; - void paintStatus(row, mine); + paintStatus(row, mine); void cards.load({ id: row.열, label: itemName(row) }); void paintInputs(row, mine); })(); } - return { root, show, reload: () => show(current) }; + return { + root, + show, + reload: () => show(current), + markEdited: () => { + if (info) upsert({ ...info, 고침: true }); + }, + }; } diff --git a/ui_template/ui_template_item_picker_edit.ts b/ui_template/ui_template_item_picker_edit.ts index abe7996c..9aa0b4b6 100644 --- a/ui_template/ui_template_item_picker_edit.ts +++ b/ui_template/ui_template_item_picker_edit.ts @@ -48,10 +48,11 @@ const TEXT = { const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); const fill = (text: string, value: string): string => text.replace("{value}", value); -export function createEditView(req: EditRequest, onBack: () => void): EditView { +export function createEditView(req: EditRequest, onBack: (saved: boolean) => void): EditView { const { kind, row, info, items } = req; let dirty = false; let saving = false; + let saved = false; // 한 번이라도 저장했나 — 돌아갈 때 「고침 있음」 표시 let doc: unknown = null; let drawing: DrawingTemplateHandle | null = null; let sheet: SpreadsheetHandle | null = null; @@ -191,6 +192,7 @@ export function createEditView(req: EditRequest, onBack: () => void): EditView { doc = merged; } setDirty(false); + saved = true; showToast(TEXT.saved, "success"); } catch (error) { showToast(fill(TEXT.saveFailed, why(error)), "error"); @@ -211,7 +213,7 @@ export function createEditView(req: EditRequest, onBack: () => void): EditView { save.addEventListener("click", () => void doSave()); back.addEventListener("click", async () => { - if (await leave()) onBack(); + if (await leave()) onBack(saved); }); return { root, start: () => void start(), isDirty: () => dirty, leave, destroy }; diff --git a/ui_template/ui_template_item_picker_project.ts b/ui_template/ui_template_item_picker_project.ts index 7275b48a..27366224 100644 --- a/ui_template/ui_template_item_picker_project.ts +++ b/ui_template/ui_template_item_picker_project.ts @@ -2,11 +2,13 @@ * ui_template_item_picker_project.ts * 항목 고르기 모달의 **자료 창구**(계획서 25-2d) — 프로젝트 복사본 · 입력 목록 · 계산을 한 곳에서 부름. * - * 프로젝트 복사본(sub6 · 25-2b) - * GET /api/projects/{id}/items → {항목: [{폴더, 이름, 원본, 원본판, 가져온날, 마스터바뀜, …}]} - * POST /api/projects/{id}/items {layer, owner, key} 미리 가져오기 → {폴더, 만듦, 가져온날} + * 구조물 복사본(sub6 · 26-1 · 26-2b) — 폴더 = 구조물 id(같은 항목을 고른 구조물도 복사본은 따로) + * GET /api/projects/{id}/items → {항목: [{폴더, 이름, 원본, 원본판, 가져온날, 마스터바뀜, 원본없음, …}]} + * POST /api/projects/{id}/items {structure, layer, owner, key} 미리 가져오기 → {폴더, 만듦, 가져온날} + * POST /api/projects/{id}/items/new {structure, 경로, 이름, 단위} 새 항목(개인 층에도) → {…, summary_item} * GET|PUT /api/projects/{id}/items/{폴더}/{부분} → {종류, 판, 내용} · 고침은 {판, 내용}(판이 다르면 409) - * POST …/{폴더}/refresh 새로 가져오기(입력값은 같은 키면 살림) + * POST …/{폴더}/reapply 재적용 → {폴더, 가져온날, 원본판, 덮임: [부분]} + * POST …/{폴더}/save-as {to, 경로, 이름} 개인에 저장 → {key, 열, 이름, summary_item} * POST …/{폴더}/calc 복사본만으로 계산(계 · 빠진 입력) * 입력 목록 · 항목 연결(sub4 · 25-2a) — `M02_MasterTemplete_Api_Fetch` 의 `fetchItemInputs` · `readInputLinks` * ========================================================================== */ @@ -42,9 +44,27 @@ export interface ProjectItemInfo { 고침?: boolean; } -/** `{층}_{주인 슬러그}_{키}` — 서버 `snapshot_folder_name` 과 같은 글(owner 없음 = none · `/` = `-`) */ -export const folderOf = (row: PickerRow): string => - `${layerOf(row)}_${(ownerOf(row) ?? "none").replace(/\//g, "-")}_${row.key}`; +/** 구조물 id → 복사본 폴더 이름 — 서버 `folder_name` 과 같은 글(id 없는 옛 구조물 `#차례` = `n차례`) */ +export const structureFolder = (structureId: string): string => structureId.replace("#", "n"); + +/** 관 지점의 구조물 id — 서버 `pipe@{round(측점, 3)}`(파이썬 float 글: 정수도 `100.0`) */ +export function pipeStructureId(chainageM: number): string { + const rounded = Math.round(chainageM * 1000) / 1000; + return `pipe@${Number.isInteger(rounded) ? rounded.toFixed(1) : String(rounded)}`; +} + +/** 이 복사본이 그 항목의 것인가 — 같은 구조물이 다른 항목으로 바뀌면 원본 참조가 다름 */ +export const sameItem = (info: ProjectItemInfo, row: PickerRow): boolean => + info.원본.layer === layerOf(row) && + (info.원본.owner ?? null) === ownerOf(row) && + info.원본.key === row.key; + +/** 개인 층에 새로 생긴 항목 — `summary_item` 을 폼에 지정하면 됨 */ +export interface CreatedItem { + key: string; + 이름?: string; + summary_item: { layer: string; owner: string | null; key: string; name: string }; +} /** 복사본 계산 결과에서 모달이 보이는 것 — 계 · 빠진 입력 수 · 멈춘 로직 수 */ export interface ProjectCalc { @@ -94,9 +114,18 @@ export interface ProjectItems { /** 복사본 고침 — 못 하면 서버 까닭 그대로 던짐(권한 · 판이 다름 …) */ write: (folder: string, part: ProjectPart, doc: unknown) => Promise; /** 프로젝트로 미리 가져옴 */ - importItem: (row: PickerRow) => Promise; - /** 새로 가져옴 — 복사본을 원본으로 덮음(넣은 값은 같은 키면 서버가 살림) */ - refresh: (info: ProjectItemInfo) => Promise; + importItem: (row: PickerRow, structureId: string) => Promise; + /** 재적용 — 복사본을 원본으로 덮음(넣은 값은 같은 키면 서버가 살림) · `covered` = 덮인 부분 */ + reapply: (info: ProjectItemInfo) => Promise<{ info: ProjectItemInfo; covered: string[] }>; + /** 이 구조물 복사본을 개인 층 새 항목으로 — 못 하면 서버 까닭(회사 없음 409 …) 그대로 던짐 */ + saveAs: (folder: string, path: string[], name: string) => Promise; + /** 프로젝트에서 새 항목(개인 층에도 만들고 그 구조물 복사본으로) */ + newItem: ( + structureId: string, + path: string[], + name: string, + unit: string, + ) => Promise; /** 복사본만으로 계산 — 계산할 것이 없거나 못 하면 null */ calc: (folder: string) => Promise; } @@ -109,6 +138,10 @@ export function projectItems(projectId: string): ProjectItems { if (kept) return kept; const base = `/projects/${enc(projectId)}/items`; const versions = new Map(); + /** 그 폴더에서 읽어 둔 판을 버림(다시 가져오면 판이 바뀜) */ + const forget = (folder: string): void => { + for (const key of [...versions.keys()]) if (key.startsWith(`${folder}/`)) versions.delete(key); + }; const at = (folder: string, part: string): string => `${base}/${enc(folder)}/${enc(part)}`; const client: ProjectItems = { @@ -136,27 +169,50 @@ export function projectItems(projectId: string): ProjectItems { if (reply.status >= 400) throw new Error(reasonOf(reply)); versions.set(key, (reply.body as { 판?: string } | null)?.판 ?? null); }, - importItem: async (row) => { + importItem: async (row, structureId) => { const ref = { layer: layerOf(row), owner: ownerOf(row), key: row.key }; - const reply = await call(base, post(ref)); + const reply = await call(base, post({ structure: structureId, ...ref })); if (reply.status >= 300) throw new Error(reasonOf(reply)); const got = reply.body as { 폴더: string; 가져온날?: string | null }; + forget(got.폴더); return { 폴더: got.폴더, 원본: ref, 가져온날: got.가져온날, 마스터바뀜: false }; }, - refresh: async (info) => { - const reply = await call(`${base}/${enc(info.폴더)}/refresh`, post({})); + reapply: async (info) => { + const reply = await call(`${base}/${enc(info.폴더)}/reapply`, post({})); if (reply.status >= 300) throw new Error(reasonOf(reply)); - for (const key of [...versions.keys()]) - if (key.startsWith(`${info.폴더}/`)) versions.delete(key); + forget(info.폴더); inputsCache.clear(); // 원본이 바뀌었을 수 있어 입력 목록도 다시 받음 - const got = reply.body as { 가져온날?: string | null; 원본판?: string | null }; - return { - ...info, - 가져온날: got.가져온날, - 원본판: got.원본판, - 마스터바뀜: false, - 고침: false, + const got = reply.body as { + 가져온날?: string | null; + 원본판?: string | null; + 덮임?: string[]; }; + return { + info: { + ...info, + 가져온날: got.가져온날, + 원본판: got.원본판, + 마스터바뀜: false, + 고침: false, + }, + covered: got.덮임 ?? [], + }; + }, + saveAs: async (folder, path, name) => { + const reply = await call( + `${base}/${enc(folder)}/save-as`, + post({ to: "personal", 경로: path, 이름: name }), + ); + if (reply.status >= 300) throw new Error(reasonOf(reply)); + return reply.body as CreatedItem; + }, + newItem: async (structureId, path, name, unit) => { + const reply = await call( + `${base}/new`, + post({ structure: structureId, 경로: path, 이름: name, 단위: unit }), + ); + if (reply.status >= 300) throw new Error(reasonOf(reply)); + return reply.body as CreatedItem; }, calc: async (folder) => { const reply = await call(`${base}/${enc(folder)}/calc`, post({})); From 542c5cc26079454ec2b9c8b8f5172a64ff1829a3 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 30 Sep 2026 08:26:01 +0900 Subject: [PATCH 09/18] =?UTF-8?q?test(spreadsheet):=20=EB=8A=A6=EA=B2=8C?= =?UTF-8?q?=20=EB=B6=88=EB=9F=AC=EC=98=A4=EB=8A=94=20=EA=B3=B3=EC=97=90=20?= =?UTF-8?q?=EA=B3=A0=EB=A5=B4=EA=B8=B0=20=EB=AA=A8=EB=8B=AC=20=ED=8E=B8?= =?UTF-8?q?=EC=A7=91=20=ED=99=94=EB=A9=B4=EC=9D=84=20=EB=8D=94=ED=95=A8=20?= =?UTF-8?q?(PLAN=2026-3b)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01EauA2ATmqNRbeV48s9CcBr --- resources/tester/spreadsheet/test_spreadsheet_size.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/tester/spreadsheet/test_spreadsheet_size.py b/resources/tester/spreadsheet/test_spreadsheet_size.py index 549afd08..57f4c888 100644 --- a/resources/tester/spreadsheet/test_spreadsheet_size.py +++ b/resources/tester/spreadsheet/test_spreadsheet_size.py @@ -76,4 +76,6 @@ def test_M02_만_늦게_불러옴(): 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 + # M02 화면 + 고르기 모달 편집 화면(카드 [수정] 을 누를 때만 `import(`) + later = ("M02_MasterTemplete/", "ui_template/ui_template_item_picker_edit.ts") + assert users and all(u.startswith(later) for u in users), users From 8f553669495e5137cf4fe25722818586ab406e34 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 30 Sep 2026 08:28:42 +0900 Subject: [PATCH 10/18] =?UTF-8?q?feat(M02):=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=ED=95=AD=EB=AA=A9=20=EB=82=98=EB=AC=B4=20?= =?UTF-8?q?=E2=80=94=20=EC=A2=8C=EC=B8=A1=20=ED=8F=B4=EB=8D=94=20=EB=82=98?= =?UTF-8?q?=EB=AC=B4=20=C2=B7=20=ED=91=9C=20=E2=86=94=20=EB=82=98=EB=AC=B4?= =?UTF-8?q?=20=EA=B3=A0=EB=A6=84=20=C2=B7=20=EC=98=A4=EB=A5=B8=EC=AA=BD=20?= =?UTF-8?q?=ED=81=B4=EB=A6=AD=20=EB=8D=94=ED=95=98=EA=B8=B0=20=C2=B7=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B0=94=EA=BE=B8=EA=B8=B0=20=C2=B7=20?= =?UTF-8?q?=EC=98=AE=EA=B8=B0=EA=B8=B0=20=C2=B7=20=EC=A7=80=EC=9A=B0?= =?UTF-8?q?=EA=B8=B0=20(PLAN=2026-3a)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 좌측 「구조물 집계 양식」 아래 「항목 나무」 카드(세 층 공용 · 깊이 자유 · 검색 · 키보드 위아래 · 펼침 · F2 · Delete) - 항목 누름 → 표 그 열 고름 + 카드 넷 · 표에서 열 고름 → 나무 표시 - 나무 고침 POST /api/m02/layers/{층}/tree · 판 409 다시 읽고 알림 · 저장 안 한 표 고침은 저장/버림/취소 물음 - 읽기만 층은 메뉴 · 키 없음 · 나무 메뉴 자리는 sub5 의 ui_template_tree 그대로 - 시험 test_m02_treepanel 8 · test_m02_layer_screen 문구 맞춤 Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01D2nUMUMTsqm93PbQtipo6c --- .../M02_MasterTemplete_Api_Fetch.ts | 36 ++ .../M02_MasterTemplete_TreePanel.css | 40 ++ .../M02_MasterTemplete_TreePanel.ts | 372 ++++++++++++++++++ .../M02_MasterTemplete_TreePanel_Dialogs.ts | 142 +++++++ .../M02_MasterTemplete_TreePanel_Model.ts | 67 ++++ .../M02_MasterTemplete_UI_Main.ts | 29 +- .../M02_MasterTemplete_UI_Page.ts | 13 +- .../M02_MasterTemplete_UI_Side.ts | 14 +- resources/tester/helper_m02_treepanel.cjs | 54 +++ resources/tester/test_m02_layer_screen.py | 2 +- resources/tester/test_m02_treepanel.py | 80 ++++ ui_template/ui_template_locale_m2.ts | 38 ++ 12 files changed, 882 insertions(+), 5 deletions(-) create mode 100644 M02_MasterTemplete/M02_MasterTemplete_TreePanel.css create mode 100644 M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts create mode 100644 M02_MasterTemplete/M02_MasterTemplete_TreePanel_Dialogs.ts create mode 100644 M02_MasterTemplete/M02_MasterTemplete_TreePanel_Model.ts create mode 100644 resources/tester/helper_m02_treepanel.cjs create mode 100644 resources/tester/test_m02_treepanel.py diff --git a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts index 52499b89..3a3d6919 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts @@ -403,3 +403,39 @@ export const saveInputLinks = ( 줄: InputLine[], ): Promise<{ 판: string }> => call(itemPath(layer, key, "input-links"), { method: "PUT", body: JSON.stringify({ 판, 줄 }) }); + +/* --- 항목 나무(PLAN 26-1 · 26-3a) — 폴더 경로 깊이 자유 · 고침은 층 쓰기 권한 --- */ +export interface TreeItem { + key: string; + 열: string; + 경로: string[]; + 이름: string; + 단위?: string | null; + 일위대가?: string | null; +} +export interface TreeNote { + op: string; + 경로?: string[]; + 열?: string; + 지운항목?: string[]; +} +export interface TreeView { + 판: string; + 폴더: string[][]; + items: TreeItem[]; + 알림?: TreeNote; +} +export interface TreeEdit { + op: "add-folder" | "add-item" | "rename" | "move" | "delete"; + 경로?: string[]; + 이름?: string; + 새경로?: string[]; + 키?: string; + 단위?: string; +} + +export const fetchTree = (layer: Layer): Promise => call(`/layers/${layer}/tree`); + +/** 판이 다르면 409(`StaleError`) — 부르는 쪽이 다시 읽음 */ +export const editTree = (layer: Layer, edit: TreeEdit, 판: string): Promise => + call(`/layers/${layer}/tree`, { method: "POST", body: JSON.stringify({ ...edit, 판 }) }); diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.css b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.css new file mode 100644 index 00000000..7efac906 --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.css @@ -0,0 +1,40 @@ +/* 관리 화면 항목 나무 — 줄 모양은 공용 ui-tree · ui-sidebar-row */ +.m02-tree { + padding-top: var(--spacing-8); + display: flex; + flex-direction: column; + gap: var(--spacing-8); + min-width: 0; +} + +.m02-tree__body { + display: flex; + flex-direction: column; + gap: 2px; + max-height: 55vh; + min-height: 4rem; + overflow: auto; + outline: none; +} + +.m02-tree__body .ui-tree__row { + min-width: 0; + overflow-wrap: anywhere; +} + +.m02-tree__hint { + margin: 0; + padding: var(--spacing-8); + color: var(--color-text-muted); + font-size: var(--text-body-sm); +} + +.m02-treedlg { + width: min(24rem, calc(100vw - 2 * var(--spacing-16))); +} + +.m02-treedlg .ui-modal__body { + display: flex; + flex-direction: column; + gap: var(--spacing-12); +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts new file mode 100644 index 00000000..80ece6bb --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts @@ -0,0 +1,372 @@ +/* ============================================================================= + * M02_MasterTemplete_TreePanel.ts + * 관리 화면(마스터 · 회사 · 개인 공용) 좌측 「항목 나무」 — 폴더 → … → 항목(깊이 자유) · 찾기. + * 항목을 누르면 표에서 그 열이 골라짐(카드 넷은 표가 엶) · 표에서 열을 고르면 `select` 로 나무도 그 항목. + * 오른쪽 클릭 · 키보드(F2 · Delete)로 폴더 · 항목 더하기 · 이름 바꾸기 · 옮기기 · 지우기 — + * `POST /api/m02/layers/{층}/tree`(PLAN 26-1). 읽기만 층은 메뉴 · 키 없음. 나무 자료는 `_Model`. + * ========================================================================== */ + +import "./M02_MasterTemplete_TreePanel.css"; +import { createInputField, el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; +import { t as L } from "@ui/ui_template_locale"; +import { + createTreeRow, + openTreeMenu, + renderTree, + type MakeRow, + type TreeMenuItem, + type TreeNode, +} from "@ui/ui_template_tree"; +import { + StaleError, + editTree, + fetchTree, + type Layer, + type TreeEdit, + type TreeItem, + type TreeNote, + type TreeView, +} from "./M02_MasterTemplete_Api_Fetch"; +import { askFolder, askText } from "./M02_MasterTemplete_TreePanel_Dialogs"; +import { + ancestorsOf, + buildFolders, + countItems, + folderIdOf, + folderOf, + isUnder, + itemIdOf, + leafName, + listFolders, + samePath, + type FolderNode, +} from "./M02_MasterTemplete_TreePanel_Model"; + +export interface TreePanelOptions { + layer: Layer; + /** 이 층을 고칠 수 있나 — 아니면 메뉴 · 키 없음 */ + canEdit: boolean; + /** 항목을 누름 — 표에서 그 열을 고름 */ + onPick: (item: TreeItem) => void; + /** 고치기 전에 저장 안 한 표 고침을 처리 — 취소면 false · 저장했으면 "changed" */ + settle: () => Promise; + /** 나무가 바뀜 — 표를 다시 읽고 그 열(`colId`)을 고름 */ + onChanged: (colId: string | null) => Promise; +} + +export interface TreePanelHandle { + root: HTMLElement; + refresh: () => Promise; + /** 표에서 고른 열을 나무에 표시(null = 지움) */ + select: (colId: string | null) => void; +} + +type Target = { kind: "folder"; path: string[] } | { kind: "item"; item: TreeItem }; + +const why = (error: unknown): string => (error instanceof Error ? error.message : ""); +const hint = (text: string): HTMLElement => el("p", { className: "m02-tree__hint", text }); + +export function createTreePanel(opt: TreePanelOptions): TreePanelHandle { + const layerLabel = L( + opt.layer === "company" + ? "M02_PickerCompany" + : opt.layer === "personal" + ? "M02_PickerPersonal" + : "M02_PickerSystem", + ); + let view: TreeView | null = null; + let text = ""; + let activeCol: string | null = null; + let seq = 0; + let busy = false; + const open = new Set([folderIdOf([])]); + const targets = new Map(); + + const search = createInputField({ placeholder: L("M02_TreeSearch"), compact: true }); + const body = el("div", { className: "m02-tree__body", attrs: { tabindex: "-1" } }); + const root = el("div", { className: "m02-tree", children: [search.root, body] }); + + /* --- 그리기 --- */ + const menuOf = (target: Target): TreeMenuItem[] => { + const isTop = target.kind === "folder" && !target.path.length; + return [ + ...(target.kind === "folder" + ? [ + { label: L("M02_TreeAddFolder"), run: () => void addFolder(target.path) }, + { label: L("M02_TreeAddItem"), run: () => void addItem(target.path) }, + ] + : []), + ...(isTop + ? [] + : [ + { label: L("M02_TreeRename"), run: () => void rename(target) }, + { label: L("M02_TreeMove"), run: () => void move(target) }, + { label: L("M02_Delete"), run: () => void remove(target) }, + ]), + ]; + }; + + function node(target: Target, base: Omit, id: string): TreeNode { + targets.set(id, target); + return { ...base, id, ...(opt.canEdit ? { menu: () => menuOf(target) } : {}) }; + } + + function folderNode(folder: FolderNode, searching: boolean): TreeNode { + const id = folderIdOf(folder.path); + const kids = [ + ...folder.folders.map((one) => folderNode(one, searching)), + ...folder.items.map((item) => + node( + { kind: "item", item }, + { + label: leafName(item), + count: null, + tip: `${item.이름} · ${item.key}`, + run: () => opt.onPick(item), + }, + itemIdOf(item), + ), + ), + ]; + return node( + { kind: "folder", path: folder.path }, + { + label: folder.path.length ? folder.path[folder.path.length - 1] : layerLabel, + count: countItems(folder), + children: kids, + open: searching || open.has(id), + run: () => void (open.has(id) ? open.delete(id) : open.add(id)), + }, + id, + ); + } + + const make: MakeRow = (one, caret, depth, onClick) => { + const button = createTreeRow(one, caret, depth, onClick); + button.dataset.node = one.id; + button.classList.toggle("is-active", !!activeCol && one.id === `i|${activeCol}`); + return button; + }; + + function paint(): void { + const focused = (document.activeElement as HTMLElement | null)?.dataset?.node; + targets.clear(); + if (!view) return body.replaceChildren(hint(L("M02_PickerLoading"))); + const searching = text.trim() !== ""; + const tree = buildFolders(view, text); + const none = countItems(tree) === 0 && (searching || !tree.folders.length); + body.replaceChildren( + ...renderTree([folderNode(tree, searching)], make), + ...(none + ? [ + hint( + L(searching ? "M02_TreeNoHit" : opt.canEdit ? "M02_TreeEmptyEdit" : "M02_TreeEmpty"), + ), + ] + : []), + ); + if (focused) rowOf(focused)?.focus({ preventScroll: true }); + } + + const rowOf = (id: string): HTMLElement | null => + [...body.querySelectorAll("[data-node]")].find((b) => b.dataset.node === id) ?? + null; + + async function refresh(): Promise { + const mine = ++seq; + try { + const got = await fetchTree(opt.layer); + if (mine !== seq) return; + view = got; + } catch (error) { + if (mine !== seq) return; + view = null; + body.replaceChildren(hint(L("M02_PickerLoadFailed").replace("{value}", why(error)))); + return; + } + paint(); + } + + function select(colId: string | null): void { + activeCol = colId; + const need = + view && colId ? ancestorsOf(view, colId).filter((p) => !open.has(folderIdOf(p))) : []; + if (need.length) { + need.forEach((p) => open.add(folderIdOf(p))); + paint(); + } else { + body + .querySelectorAll("[data-node]") + .forEach((b) => + b.classList.toggle("is-active", !!colId && b.dataset.node === `i|${colId}`), + ); + } + if (colId) rowOf(`i|${colId}`)?.scrollIntoView({ block: "nearest" }); + } + + /* --- 고치기 --- */ + /** 저장 안 한 표 고침 처리 → (바뀌었으면 나무 다시 읽음) → 참이면 계속 */ + async function ready(): Promise { + const settled = await opt.settle(); + if (!settled) return false; + if (settled === "changed") await refresh(); + return true; + } + + async function edit(body_: TreeEdit, keepCol: string | null = activeCol): Promise { + if (!view || busy) return; + busy = true; + try { + const got = await editTree(opt.layer, body_, view.판); + view = got; + const note: TreeNote | undefined = got.알림; + // 새 항목은 그 열을 고름 · 지운 항목이면 고름 풀림 · 나머지는 지금 고른 열 그대로 + const gone = note?.지운항목 ?? []; + const col = note?.열 ?? (keepCol && !gone.includes(keepCol) ? keepCol : null); + activeCol = col; + const parent = note?.경로 ? note.경로.slice(0, -1) : []; + parent.forEach((_, i) => open.add(folderIdOf(parent.slice(0, i + 1)))); + if (note?.op === "add-folder" || note?.op === "rename" || note?.op === "move") { + if (note.경로) open.add(folderIdOf(note.경로)); + } + paint(); + showToast(L("M02_TreeDone"), "success"); + await opt.onChanged(col); + } catch (error) { + if (error instanceof StaleError) { + await refresh(); + showToast(L("M02_TreeStale"), "error"); + } else { + showToast(L("M02_ActionFailed").replace("{value}", why(error)), "error"); + } + } finally { + busy = false; + } + } + + const nameOf = (target: Target): string => + target.kind === "item" ? leafName(target.item) : target.path[target.path.length - 1]; + + async function addFolder(parent: string[]): Promise { + if (!(await ready())) return; + const got = await askText(L("M02_TreeAddFolder"), [ + { key: "name", label: L("M02_TreeFolderName") }, + ]); + if (got) await edit({ op: "add-folder", 경로: parent, 이름: got.name }); + } + + async function addItem(parent: string[]): Promise { + if (!(await ready())) return; + const got = await askText(L("M02_TreeAddItem"), [ + { key: "name", label: L("M02_TreeItemName") }, + { key: "unit", label: L("M02_TreeUnit"), optional: true }, + ]); + if (got) { + await edit({ + op: "add-item", + 경로: parent, + 이름: got.name, + ...(got.unit ? { 단위: got.unit } : {}), + }); + } + } + + async function rename(target: Target): Promise { + if (!(await ready())) return; + const got = await askText(L("M02_TreeRename"), [ + { + key: "name", + label: L(target.kind === "item" ? "M02_TreeItemName" : "M02_TreeFolderName"), + value: nameOf(target), + }, + ]); + if (!got || got.name === nameOf(target)) return; + await edit( + target.kind === "item" + ? { op: "rename", 경로: folderOf(target.item), 키: target.item.key, 이름: got.name } + : { op: "rename", 경로: target.path, 이름: got.name }, + ); + } + + async function move(target: Target): Promise { + if (!(await ready()) || !view) return; + const from = target.kind === "item" ? folderOf(target.item) : target.path.slice(0, -1); + const to = await askFolder( + L("M02_TreeMove"), + listFolders(buildFolders(view)), + (path) => target.kind === "folder" && isUnder(path, target.path), + from, + ); + if (!to || samePath(to, from)) return; + await edit( + target.kind === "item" + ? { op: "move", 경로: from, 키: target.item.key, 새경로: to } + : { op: "move", 경로: target.path, 새경로: to }, + ); + } + + async function remove(target: Target): Promise { + if (!(await ready()) || !view) return; + const folder = + target.kind === "folder" ? findFolder(buildFolders(view), target.path) : undefined; + const message = + target.kind === "item" + ? L("M02_TreeDeleteItem").replace("{value}", leafName(target.item)) + : L("M02_TreeDeleteFolder") + .replace("{value}", nameOf(target)) + .replace("{n}", String(folder ? countItems(folder) : 0)); + if (!(await showConfirmDialog(message, L("M02_Delete")))) return; + await edit( + target.kind === "item" + ? { op: "delete", 경로: folderOf(target.item), 키: target.item.key } + : { op: "delete", 경로: target.path }, + ); + } + + const findFolder = (from: FolderNode, path: string[]): FolderNode | undefined => + samePath(from.path, path) + ? from + : from.folders.map((one) => findFolder(one, path)).find(Boolean); + + /* --- 이음 --- */ + search.input.addEventListener("input", () => { + text = search.input.value; + paint(); + }); + + // 빈 자리 오른쪽 클릭 = 맨 위 메뉴(폴더 · 항목 더하기) + body.addEventListener("contextmenu", (event) => { + if (!opt.canEdit || (event.target as HTMLElement).closest("[data-node]")) return; + event.preventDefault(); + openTreeMenu(event, menuOf({ kind: "folder", path: [] }).slice(0, 2)); + }); + + body.addEventListener("keydown", (event) => { + const row = (event.target as HTMLElement).closest("[data-node]"); + if (!row) return; + const rows = [...body.querySelectorAll("[data-node]")].filter( + (b) => !b.closest("[hidden]"), + ); + const at = rows.indexOf(row); + const caret = row.querySelector(".ui-tree__caret")?.textContent; + const target = targets.get(row.dataset.node ?? ""); + if (event.key === "ArrowDown" || event.key === "ArrowUp") { + event.preventDefault(); + rows[at + (event.key === "ArrowDown" ? 1 : -1)]?.focus(); + } else if ( + (event.key === "ArrowRight" && caret === "▸") || + (event.key === "ArrowLeft" && caret === "▾") + ) { + event.preventDefault(); + row.click(); + } else if (opt.canEdit && target && event.key === "F2") { + event.preventDefault(); + if (!(target.kind === "folder" && !target.path.length)) void rename(target); + } else if (opt.canEdit && target && event.key === "Delete") { + event.preventDefault(); + if (!(target.kind === "folder" && !target.path.length)) void remove(target); + } + }); + + return { root, refresh, select }; +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Dialogs.ts b/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Dialogs.ts new file mode 100644 index 00000000..962b31c4 --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Dialogs.ts @@ -0,0 +1,142 @@ +/* ============================================================================= + * M02_MasterTemplete_TreePanel_Dialogs.ts + * 항목 나무 오른쪽 클릭 메뉴가 띄우는 작은 창 — 글 받기 · 옮길 폴더 고르기 · 저장/버림 물음. + * 모두 약속(Promise)으로 답함 — 닫기 · Esc · 바깥 누르기 = 취소(null / "cancel"). + * ========================================================================== */ + +import { + createButton, + createInputField, + createSelectField, + el, + showToast, +} from "@ui/ui_template_elements"; +import { t as L } from "@ui/ui_template_locale"; +import { openModal } from "@ui/ui_template_modal"; + +export interface TextField { + key: string; + label: string; + value?: string; + /** 비워도 됨(항목 단위) */ + optional?: boolean; +} + +/** 글 칸 하나 이상을 받음 — 확인이면 칸 이름 → 글(앞뒤 공백 뺌) */ +export function askText( + title: string, + fields: TextField[], + okLabel = L("M02_TreeOk"), +): Promise | null> { + return new Promise((resolve) => { + let done = false; + const finish = (value: Record | null): void => { + if (done) return; + done = true; + resolve(value); + }; + openModal({ + title, + closeLabel: L("M02_Close"), + dialogClass: "m02-modal m02-treedlg", + beforeClose: () => (finish(null), true), + mount: (body, close) => { + const inputs = fields.map((f) => ({ + f, + field: createInputField({ label: f.label, value: f.value }), + })); + const ok = createButton({ label: okLabel, variant: "filled" }); + const submit = (): void => { + const out: Record = {}; + for (const { f, field } of inputs) { + out[f.key] = field.input.value.trim(); + if (!out[f.key] && !f.optional) return showToast(L("M02_NameNeeded"), "error"); + } + finish(out); + close(); + }; + ok.addEventListener("click", submit); + for (const { field } of inputs) { + field.input.addEventListener("keydown", (event) => { + if (event.key === "Enter" && !event.isComposing) submit(); + }); + } + body.append(...inputs.map(({ field }) => field.root), ok); + inputs[0]?.field.input.select(); + }, + }); + }); +} + +/** 옮길 폴더 고르기 — `folders` 에서 `blocked` 인 것은 뺌 · 맨 위(폴더 밖)는 늘 있음 */ +export function askFolder( + title: string, + folders: string[][], + blocked: (path: string[]) => boolean, + current: string[], +): Promise { + return new Promise((resolve) => { + let done = false; + const finish = (value: string[] | null): void => { + if (done) return; + done = true; + resolve(value); + }; + const choices = [[], ...folders.filter((path) => !blocked(path))]; + openModal({ + title, + closeLabel: L("M02_Close"), + dialogClass: "m02-modal m02-treedlg", + beforeClose: () => (finish(null), true), + mount: (body, close) => { + const field = createSelectField({ + label: L("M02_TreeMoveTo"), + options: choices.map((path, i) => ({ + value: String(i), + text: path.length ? path.join(" / ") : L("M02_TreeTop"), + })), + value: String(choices.findIndex((path) => path.join("|") === current.join("|"))), + }); + const ok = createButton({ label: L("M02_TreeOk"), variant: "filled" }); + ok.addEventListener("click", () => { + finish(choices[Number(field.select.value)] ?? []); + close(); + }); + body.append(field.root, ok); + }, + }); + }); +} + +export type Settle = "save" | "discard" | "cancel"; + +/** 저장 안 한 표 고침 — 저장하고 계속 · 버리고 계속 · 취소 */ +export function askSettle(): Promise { + return new Promise((resolve) => { + let done = false; + const finish = (value: Settle): void => { + if (done) return; + done = true; + resolve(value); + }; + openModal({ + title: L("M02_TreeUnsaved"), + closeLabel: L("M02_TreeCancel"), + dialogClass: "m02-modal m02-treedlg", + beforeClose: () => (finish("cancel"), true), + mount: (body, close) => { + const choose = (value: Settle) => () => { + finish(value); + close(); + }; + const save = createButton({ label: L("M02_TreeSaveGo"), variant: "filled" }); + const discard = createButton({ label: L("M02_TreeDiscardGo"), variant: "danger" }); + const cancel = createButton({ label: L("M02_TreeCancel"), variant: "ghost" }); + save.addEventListener("click", choose("save")); + discard.addEventListener("click", choose("discard")); + cancel.addEventListener("click", choose("cancel")); + body.append(el("div", { className: "m02-side__row", children: [save, discard, cancel] })); + }, + }); + }); +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Model.ts b/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Model.ts new file mode 100644 index 00000000..4643cf08 --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Model.ts @@ -0,0 +1,67 @@ +/* ============================================================================= + * M02_MasterTemplete_TreePanel_Model.ts + * 관리 화면 항목 나무의 자료 쪽(화면 없음) — 서버 나무(`폴더` + `items`)를 폴더 나무로 · 찾기 · + * 폴더 목록. 항목 경로는 폴더 이름 배열 + 마지막 마디가 항목 이름(PLAN 26-1 · 깊이 자유). + * ========================================================================== */ + +import type { TreeItem, TreeView } from "./M02_MasterTemplete_Api_Fetch"; + +export interface FolderNode { + /** 이 폴더까지의 경로 — 맨 위 = 빈 배열 */ + path: string[]; + folders: FolderNode[]; + items: TreeItem[]; +} + +export const samePath = (a: string[], b: string[]): boolean => + a.length === b.length && a.every((part, i) => part === b[i]); + +/** `path` 가 `folder` 와 같거나 그 안인가 */ +export const isUnder = (path: string[], folder: string[]): boolean => + folder.length <= path.length && folder.every((part, i) => part === path[i]); + +export const folderIdOf = (path: string[]): string => `f|${path.join("|")}`; +export const itemIdOf = (item: TreeItem): string => `i|${item.열}`; +export const leafName = (item: TreeItem): string => item.경로[item.경로.length - 1] ?? item.이름; +export const folderOf = (item: TreeItem): string[] => item.경로.slice(0, -1); + +/** 찾기 — 경로 · 키 · 일위대가 키에 모든 낱말이 들어 있는 항목(빈 글이면 전부) */ +export function matches(item: TreeItem, text: string): boolean { + const words = text.trim().toLowerCase().split(/\s+/).filter(Boolean); + const hay = [...item.경로, item.key, item.일위대가 ?? ""].join(" ").toLowerCase(); + return words.every((word) => hay.includes(word)); +} + +/** 폴더 나무 — 찾는 글이 있으면 맞는 항목과 그 위 폴더만 · 없으면 빈 폴더까지 전부 */ +export function buildFolders(view: TreeView, text = ""): FolderNode { + const searching = text.trim() !== ""; + const root: FolderNode = { path: [], folders: [], items: [] }; + const at = (path: string[]): FolderNode => + path.reduce((node, name, i) => { + let next = node.folders.find((f) => f.path[i] === name); + if (!next) { + next = { path: path.slice(0, i + 1), folders: [], items: [] }; + node.folders.push(next); + } + return next; + }, root); + if (!searching) view.폴더.forEach(at); + for (const item of view.items) if (matches(item, text)) at(folderOf(item)).items.push(item); + return root; +} + +export const countItems = (node: FolderNode): number => + node.items.length + node.folders.reduce((sum, f) => sum + countItems(f), 0); + +/** 모든 폴더 경로(맨 위 제외) — 옮길 곳 고르기 */ +export function listFolders(root: FolderNode): string[][] { + return root.folders.flatMap((f) => [f.path, ...listFolders(f)]); +} + +/** 그 열이 들어 있는 폴더 길 — 나무를 그 항목까지 펼칠 때 */ +export function ancestorsOf(view: TreeView, colId: string): string[][] { + const item = view.items.find((one) => one.열 === colId); + if (!item) return []; + const folder = folderOf(item); + return folder.map((_, i) => folder.slice(0, i + 1)); +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts index cad56f18..93af24d6 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -24,6 +24,7 @@ import { type StructurePanelHandle, } from "./M02_MasterTemplete_Structure"; import { inputPills } from "./M02_MasterTemplete_Inputs"; +import { askSettle } from "./M02_MasterTemplete_TreePanel_Dialogs"; import { buildComboBar } from "./M02_MasterTemplete_Combo"; import { buildComboPanel, type ComboPanel } from "./M02_MasterTemplete_ComboPanel"; import { @@ -91,6 +92,10 @@ export interface MainHandle { confirmLeave: () => Promise; /** 양식 밖 화면(좌측 자재 조합 상세)을 띄움 — 머리는 제목만 · 고침 표시 손잡이를 돌려줌 */ showView: (label: string, node: HTMLElement) => (dirty: boolean) => void; + /** 구조물집계표를 열고 그 열을 고름 — 이미 표면 다시 안 읽음(`fresh` 면 다시 읽음) */ + openTable: (layer: Layer, colId?: string | null, fresh?: boolean) => Promise; + /** 나무를 고치기 전 — 저장 안 한 고침이 있으면 저장 · 버림 · 취소를 물음 */ + settle: () => Promise; } /** 이 층에서 고칠 수 있나 — `isAdmin` = 이 화면 층의 고칠 권한(페이지가 층별로 정함) */ @@ -501,6 +506,28 @@ export function createMain( return (d) => (dirty = d); } + async function openTable(layer: Layer, colId?: string | null, fresh = false): Promise { + const inTable = sel?.kind === "table" && sel.name === STRUCTURE_TABLE && !fresh; + if (!inTable) { + if (!fresh && !(await confirmLeave())) return; + await open({ layer, projectId: null, kind: "table", name: STRUCTURE_TABLE }); + } + if (colId && sel?.kind === "table") sheet?.selectColumn(colId); + } + + async function settle(): Promise { + if (!dirty) return "clean"; + const choice = await askSettle(); + if (choice === "cancel") return false; + if (choice === "save") { + await doSave(); + return dirty ? false : "changed"; // 저장이 막히면(판 다름 등) 나무도 안 고침 + } + dirty = false; // 버림 — 화면을 서버 그대로 다시 읽음 + if (sel) await open(sel); + return "changed"; + } + save.addEventListener("click", () => void doSave()); back.addEventListener("click", async () => { const column = sel?.name; @@ -518,5 +545,5 @@ export function createMain( }); bar(); empty(""); - return { root, open, current: () => sel, confirmLeave, showView }; + return { root, open, current: () => sel, confirmLeave, showView, openTable, settle }; } diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts index eca6beac..0870ed02 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts @@ -39,7 +39,13 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise isAdmin, () => void side.refresh(), () => side.paint(), - { top: readOnlyBar?.root, onColumn: (column) => readOnlyBar?.setColumn(column) }, + { + top: readOnlyBar?.root, + onColumn: (column) => { + readOnlyBar?.setColumn(column); + side.selectItem(column); + }, + }, ); // 첫 화면 · 닫은 뒤(양식 지움 · 자재 조합 상세 닫음) = 구조물집계표 — 빈 화면 없음 const home: Selection = { @@ -55,6 +61,11 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise getOpen: main.current, confirmLeave: main.confirmLeave, showView: main.showView, + tree: { + onPick: (item) => void main.openTable(layer, item.열), + settle: main.settle, + onChanged: (colId) => main.openTable(layer, colId, true), + }, }); const layout = el("div", { className: "ui-workflow-layout m02-master" }); diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts index 4597dd7a..73b1a840 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts @@ -38,6 +38,7 @@ import { } from "./M02_MasterTemplete_Structure"; import { buildMatsetSection } from "./M02_MasterTemplete_Matset"; import { buildOptionsSection } from "./M02_MasterTemplete_Options"; +import { createTreePanel, type TreePanelOptions } from "./M02_MasterTemplete_TreePanel"; import { canEdit, type Selection } from "./M02_MasterTemplete_UI_Main"; const KINDS: { kind: Kind; label: () => string }[] = [ @@ -71,6 +72,8 @@ export interface SideOptions { confirmLeave: () => Promise; /** 메인 칸에 양식 밖 화면(자재 조합 상세)을 띄움 */ showView: (label: string, node: HTMLElement) => (dirty: boolean) => void; + /** 항목 나무(26-3a) — 항목 누름 · 고치기 전 저장 처리 · 고친 뒤 표 다시 읽기 */ + tree: Pick; } export interface SideHandle { @@ -78,6 +81,8 @@ export interface SideHandle { refresh: () => Promise; /** 고름 표시를 지금 열린 것 한 줄로 — 편집 칸이 무엇을 열든(카드 [도면 수정] 등) 부름 */ paint: () => void; + /** 표에서 고른 열을 항목 나무에 표시 */ + selectItem: (colId: string | null) => void; } /** 서버 이름 규칙과 같게 — 화면에서 먼저 막음 */ @@ -150,10 +155,15 @@ export function buildSide(opt: SideOptions): SideHandle { ); summary.classList.remove("is-collapsed"); summary.classList.add("m02-side__summary"); + // 항목 나무 — 구조물 집계 양식 아래 · 고르기 모달과 같은 폴더 나무(세 층 공용) + const tree = createTreePanel({ layer, canEdit: canEdit(layer, opt.isAdmin), ...opt.tree }); + const treeSection = section(L("M02_TreeTitle"), tree.root); + treeSection.classList.remove("is-collapsed"); const listHost = el("div", { className: "m02-side__lists ui-sidebar-panel", children: [ summary, + treeSection, // 구조물 옵션 보이기 · 도면 양식은 시스템 층(마스터 템플릿)에만 ...(layer === "system" ? [ @@ -279,7 +289,7 @@ export function buildSide(opt: SideOptions): SideHandle { if (mine !== seq) return; paintList(); paintButtons(); - await Promise.all([refreshStructure(), matset.refresh()]); + await Promise.all([refreshStructure(), matset.refresh(), tree.refresh()]); } /* --- 시스템 층 새로 · 본떠 · 지우기(도면 양식만 — 구조물 도면은 집계표 열이 정함) --- */ @@ -360,5 +370,5 @@ export function buildSide(opt: SideOptions): SideHandle { } void refresh(); - return { root, refresh, paint }; + return { root, refresh, paint, selectItem: tree.select }; } diff --git a/resources/tester/helper_m02_treepanel.cjs b/resources/tester/helper_m02_treepanel.cjs new file mode 100644 index 00000000..2b7a3be9 --- /dev/null +++ b/resources/tester/helper_m02_treepanel.cjs @@ -0,0 +1,54 @@ +/* M02 항목 나무 자료(`M02_MasterTemplete_TreePanel_Model.ts`) 단위검증 헬퍼 — 트랜스파일해 Node 로 돌리고 JSON 으로 냄. */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const file = path.join(__dirname, "..", "..", "M02_MasterTemplete", "M02_MasterTemplete_TreePanel_Model.ts"); +const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, +}).outputText; +const box = { exports: {} }; +new Function("exports", "module", "require", js)(box.exports, box, require); +const m = box.exports; + +const item = (열, 경로, key) => ({ key, 열, 경로, 이름: 경로.join(" "), 단위: "m", 일위대가: null }); +const view = { + 판: "v1", + 폴더: [["빈 폴더"], ["돌기슭막이", "빈 아래"]], + items: [ + item("c1", ["돌기슭막이", "메쌓기", "1.2m"], "SS000001"), + item("c2", ["돌기슭막이", "찰쌓기"], "SS000002"), + item("c3", ["다섯", "둘", "셋", "넷", "잎"], "SS000003"), + item("c4", ["최상위 항목"], "SS000004"), + ], +}; +const shape = (n) => ({ + path: n.path, + items: n.items.map((i) => i.열), + folders: n.folders.map(shape), +}); +const all = m.buildFolders(view); +const found = m.buildFolders(view, "찰쌓기"); + +process.stdout.write( + JSON.stringify({ + all: shape(all), + count: m.countItems(all), + found: shape(found), + foundCount: m.countItems(found), + none: shape(m.buildFolders(view, "없는말")), + folders: m.listFolders(all), + ancestors: m.ancestorsOf(view, "c3"), + ancestorsTop: m.ancestorsOf(view, "c4"), + ancestorsNone: m.ancestorsOf(view, "zz"), + under: [ + m.isUnder(["a", "b"], ["a"]), + m.isUnder(["a"], ["a"]), + m.isUnder(["a"], ["a", "b"]), + m.isUnder(["x"], ["a"]), + ], + leaf: m.leafName(view.items[0]), + dir: m.folderOf(view.items[0]), + byKey: m.buildFolders(view, "ss000003").items.length + m.buildFolders(view, "SS000003").folders.length, + }), +); diff --git a/resources/tester/test_m02_layer_screen.py b/resources/tester/test_m02_layer_screen.py index febeedee..6bd2f25f 100644 --- a/resources/tester/test_m02_layer_screen.py +++ b/resources/tester/test_m02_layer_screen.py @@ -104,7 +104,7 @@ def test_층_길_접두어와_고정_풀림(): def test_읽기만_회사용_개인용으로_복사_막대(): page = _text(M02 / "M02_MasterTemplete_UI_Page.ts") assert 'layer === "company" && !isAdmin ? buildReadOnlyBar(user)' in page - assert "onColumn: (column) => readOnlyBar?.setColumn(column)" in page + assert "readOnlyBar?.setColumn(column)" in page # 열 고름 → 막대 · 항목 나무가 함께 받음 assert "access.needCompany" in page and "buildNeedCompany()" in page assert "titleOf(layer)" in page layer = _text(LAYER) diff --git a/resources/tester/test_m02_treepanel.py b/resources/tester/test_m02_treepanel.py new file mode 100644 index 00000000..798f1391 --- /dev/null +++ b/resources/tester/test_m02_treepanel.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +"""M02 관리 화면 항목 나무(PLAN 26-3a) 자료 셈 회귀검증 — 폴더 나무 · 빈 폴더 · 깊이 5 · 찾기 · 폴더 목록. + +항목 경로 = 폴더 이름 배열 + 마지막 마디가 항목 이름 · 찾는 글이 있으면 맞는 항목과 위 폴더만. +Node 헬퍼(`helper_m02_treepanel.cjs`)가 TS 를 트랜스파일해 돌리고, 여기서 값을 판정한다. +""" + +import json +import os +import subprocess + +import pytest + +HERE = os.path.dirname(os.path.abspath(__file__)) + + +@pytest.fixture(scope="module") +def results(): + proc = subprocess.run( + ["node", os.path.join(HERE, "helper_m02_treepanel.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_빈_폴더도_나무에_있음(results): + tops = [f["path"] for f in results["all"]["folders"]] + assert ["빈 폴더"] in tops + dol = next(f for f in results["all"]["folders"] if f["path"] == ["돌기슭막이"]) + assert ["돌기슭막이", "빈 아래"] in [f["path"] for f in dol["folders"]] + + +def test_맨_위_항목은_폴더_밖(results): + assert results["all"]["items"] == ["c4"] + assert results["count"] == 4 + + +def test_깊이_5_항목까지_이어짐(results): + node = next(f for f in results["all"]["folders"] if f["path"] == ["다섯"]) + for depth in range(2, 5): + node = node["folders"][0] + assert len(node["path"]) == depth + assert node["items"] == ["c3"] + + +def test_찾기는_맞는_항목과_위_폴더만(results): + assert results["foundCount"] == 1 + assert [f["path"] for f in results["found"]["folders"]] == [["돌기슭막이"]] + assert results["found"]["folders"][0]["items"] == ["c2"] # 빈 폴더는 안 보임 + assert results["none"] == {"path": [], "items": [], "folders": []} + + +def test_키로도_찾음(results): + assert results["byKey"] == 1 + + +def test_폴더_목록과_위_길(results): + assert ["다섯", "둘", "셋", "넷"] in results["folders"] + assert ["빈 폴더"] in results["folders"] + assert results["ancestors"] == [ + ["다섯"], + ["다섯", "둘"], + ["다섯", "둘", "셋"], + ["다섯", "둘", "셋", "넷"], + ] + assert results["ancestorsTop"] == [] + assert results["ancestorsNone"] == [] + + +def test_안_또는_같음_판정(results): + assert results["under"] == [True, True, False, False] + + +def test_잎_이름과_폴더(results): + assert results["leaf"] == "1.2m" + assert results["dir"] == ["돌기슭막이", "메쌓기"] diff --git a/ui_template/ui_template_locale_m2.ts b/ui_template/ui_template_locale_m2.ts index f9b30d72..e6db41f1 100644 --- a/ui_template/ui_template_locale_m2.ts +++ b/ui_template/ui_template_locale_m2.ts @@ -97,4 +97,42 @@ export const ui_locales_m2 = { M02_PickerManage: ["관리 화면에서 고치기", "Edit in manager"], M02_PickerCopied: ["개인용으로 복사했음", "Copied to personal"], M02_PickerCopyFailed: ["복사 못 함 — {value}", "Copy failed — {value}"], + M02_TreeTitle: ["항목 나무", "Item tree"], + M02_TreeSearch: ["항목 찾기", "Find item"], + M02_TreeEmptyEdit: [ + "항목이 없음 — 맨 위 줄을 오른쪽 클릭해 폴더 · 항목을 더함", + "No items — right-click the top row to add a folder or item", + ], + M02_TreeEmpty: ["항목이 없음", "No items"], + M02_TreeNoHit: ["찾은 항목이 없음", "No matching item"], + M02_TreeAddFolder: ["폴더 더하기", "Add folder"], + M02_TreeAddItem: ["항목 더하기", "Add item"], + M02_TreeRename: ["이름 바꾸기", "Rename"], + M02_TreeMove: ["옮기기", "Move"], + M02_TreeFolderName: ["폴더 이름", "Folder name"], + M02_TreeItemName: ["항목 이름", "Item name"], + M02_TreeUnit: ["단위", "Unit"], + M02_TreeMoveTo: ["옮길 폴더", "Move to"], + M02_TreeTop: ["맨 위 (폴더 밖)", "Top (no folder)"], + M02_TreeOk: ["확인", "OK"], + M02_TreeCancel: ["취소", "Cancel"], + M02_TreeDeleteFolder: [ + "「{value}」 폴더와 안의 항목 {n}개를 지움 — 이미 쓰는 프로젝트에는 영향 없음(프로젝트는 복사본을 씀)", + "Delete folder “{value}” and its {n} item(s) — projects already using them are not affected (they keep their own copies)", + ], + M02_TreeDeleteItem: [ + "「{value}」 항목을 지움 — 이미 쓰는 프로젝트에는 영향 없음(프로젝트는 복사본을 씀)", + "Delete item “{value}” — projects already using it are not affected (they keep their own copies)", + ], + M02_TreeStale: [ + "그 사이 나무가 바뀜 — 다시 읽었음, 한 번 더 해 볼 것", + "The tree changed meanwhile — reloaded, try again", + ], + M02_TreeUnsaved: [ + "저장 안 한 표 고침이 있음 — 나무를 고치기 전에 처리", + "Unsaved table edits — settle them before changing the tree", + ], + M02_TreeSaveGo: ["저장하고 계속", "Save and continue"], + M02_TreeDiscardGo: ["버리고 계속", "Discard and continue"], + M02_TreeDone: ["나무를 고침", "Tree updated"], } as const; From b67a3be75368e8dfc6c4485cc8dd98f2a9cab99d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 30 Sep 2026 08:31:31 +0900 Subject: [PATCH 11/18] =?UTF-8?q?fix(M02):=20=EC=9E=AC=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=20=EB=92=A4=20=EB=B3=B5=EC=82=AC=EB=B3=B8=20=E3=80=8C=EA=B3=A0?= =?UTF-8?q?=EC=B9=A8=E3=80=8D=20=EC=9D=84=20=EB=81=94=20=E2=80=94=20?= =?UTF-8?q?=EC=82=B4=EB=A6=B0=20=EC=9E=85=EB=A0=A5=EA=B0=92=20=C2=B7=20?= =?UTF-8?q?=EA=B3=A0=EB=A5=B8=20=EC=9E=90=EC=9E=AC=EB=8A=94=20=E3=80=8C?= =?UTF-8?q?=EA=B0=92=EC=9E=88=EC=9D=8C=E3=80=8D=20=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=94=B0=EB=A1=9C=20=EC=95=8C=EB=A6=BC=20=C2=B7=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A4=84=EC=97=90=EB=8F=84=20=EB=B0=98=EC=98=81=20?= =?UTF-8?q?(PLAN=2026-2b)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01BU9gbNE3aWFUvE61pS2WGt --- M02_MasterTemplete/M02_Item_Copy.py | 9 +++++++-- resources/tester/test_item_copy_complete.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/M02_MasterTemplete/M02_Item_Copy.py b/M02_MasterTemplete/M02_Item_Copy.py index fc40d21f..7708e664 100644 --- a/M02_MasterTemplete/M02_Item_Copy.py +++ b/M02_MasterTemplete/M02_Item_Copy.py @@ -118,6 +118,7 @@ def list_items(project_root: str | Path) -> list[dict[str, Any]]: "원본없음": changed is None, "구조물": manifest.get("구조물") or [], "고침": bool(manifest.get("고침")), + "값있음": bool(manifest.get("값있음")), } ) return out @@ -257,9 +258,13 @@ def reapply(project_root: str | Path, folder: str) -> dict[str, Any]: ] source = version_of(source_files(files)) stamp = now_text() - fresh = {**manifest, "판": source, "원본판": source, "가져온날": stamp} + # 원본을 다시 가져왔으니 시트 고침은 없음 — 살린 입력값 · 고른 자재는 `값있음` 으로 따로 알림 + kept = bool(files[VALUES_FILE] or files[PICKS_FILE]) + fresh = {**manifest, "판": source, "원본판": source, "가져온날": stamp, "고침": False} + fresh["값있음"] = kept + fresh.pop("고친날", None) atomic_write_json(target / MANIFEST_NAME, fresh) - return {"폴더": folder, "가져온날": stamp, "원본판": source, "덮임": covered} + return {"폴더": folder, "가져온날": stamp, "원본판": source, "덮임": covered, "값있음": kept} refresh = reapply # 25-1b 옛 이름(새로 가져오기) diff --git a/resources/tester/test_item_copy_complete.py b/resources/tester/test_item_copy_complete.py index 7e064363..597fc81b 100644 --- a/resources/tester/test_item_copy_complete.py +++ b/resources/tester/test_item_copy_complete.py @@ -128,6 +128,7 @@ def test_지정하면_새_파일까지_복사(world): "원본없음": False, "구조물": ["pipe@40.0"], "고침": False, + "값있음": False, } ] @@ -522,3 +523,19 @@ def test_새_항목_길_회사_없으면_409(world, monkeypatch): assert client.post("/api/projects/p1/items/new", json=body).status_code == 409 save_as = client.post("/api/projects/p1/items/f1/save-as", json={"경로": ["a"], "이름": "b"}) assert save_as.status_code == 409 + + +def test_재적용_뒤_고침은_꺼지고_값있음만_남음(world): + root = world["root"] + _save(root, [_pipe()]) + _memo(root, FOLDER, "x") + assert copies.list_items(root)[0]["고침"] is True + got = copies.reapply(root, FOLDER) + assert got["값있음"] is False + row = copies.list_items(root)[0] + assert row["고침"] is False and row["값있음"] is False + _fill_values(root) # 입력값 씀 — 고침 켜짐 + assert copies.list_items(root)[0]["고침"] is True + assert copies.reapply(root, FOLDER)["값있음"] is True + row = copies.list_items(root)[0] + assert row["고침"] is False and row["값있음"] is True From 760e43c8df2599137911e852d22c2ae69c5eff99 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 30 Sep 2026 09:04:39 +0900 Subject: [PATCH 12/18] =?UTF-8?q?feat(M02):=20=ED=95=AD=EB=AA=A9=20?= =?UTF-8?q?=ED=82=A4=20=EC=B2=B4=EA=B3=84=20=C2=B7=20=ED=92=88=EB=B2=88=20?= =?UTF-8?q?=EC=83=88=20=EB=AA=A8=EC=96=91(PLAN=2026-4a)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 항목 키 = 마스터 SS + 6자리 · 회사 SC{회사}- + 6자리 · 개인 SP{사용자}- + 6자리(장부 key_head · 층 폴더로 가름) · 한 번 주면 안 바뀜 · 지운 키 다시 안 씀 · 층 사이 복사 · 개인에 저장 · 새 항목은 그 층 새 키 + 본뜬 곳 - 회사 · 개인 층의 옛 SS 키는 장부를 맞출 때(처음 읽을 때 한 번) 새 모양으로 옮김(숫자 여섯 그대로 · 겹치면 다음 번호) · 옛 키는 줄 옛키 에 남김 · 옛 키로도 찾음(store.find_combo · 나무 고침 · 입력 목록 · 항목 복사 · M02_Item_Source.row_of) - summary_item 키 검사 — SC · SP 는 그 층 · 머리 번호 = owner 일 때만 · 옛 SS 는 어느 층이든 받음 - 품번(도번 SD · 면적번 SA · 재료번 SM) 새 일련 = 붙임표 + 6자리(SD-000001) — 옛 8자리(제목행 자리) · 12장 옛 6자리(SD010101)와 모양이 달라 안 겹침 · 옛 번호는 그대로 유효 · 새 모양끼리 셈 · 지운 번호 안 씀 - 시험 test_m02_item_keys 7(층마다 키 모양 · 안 바뀜 · 다시 안 씀 · 복사 본뜬 곳 · 옛 SS 옮김 · 옛 키로 찾음 · 겹친 숫자 · 참조 키 모양 · 120 형제 깊이 6 번호 · 층마다 번호 따로 · 키대장 안 겹침) · 번호 · 키 시험 기대값을 새 모양으로 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL --- M02_MasterTemplete/M02_Item_Source.py | 3 +- M02_MasterTemplete/M02_MasterTemplete_Book.py | 91 ++++++-- .../M02_MasterTemplete_Inputs.py | 2 +- .../M02_MasterTemplete_Items.py | 2 +- .../M02_MasterTemplete_Router_Items.py | 5 +- .../M02_MasterTemplete_Store.py | 42 ++-- M02_MasterTemplete/M02_MasterTemplete_Tree.py | 2 +- common_util/common_util_summary_item.py | 30 ++- .../spreadsheet/test_spreadsheet_store.py | 8 +- resources/tester/test_m02_item_keys.py | 213 ++++++++++++++++++ resources/tester/test_m02_layers_store.py | 6 +- resources/tester/test_m02_structure.py | 52 ++--- resources/tester/test_m02_tree.py | 6 +- 13 files changed, 387 insertions(+), 75 deletions(-) create mode 100644 resources/tester/test_m02_item_keys.py diff --git a/M02_MasterTemplete/M02_Item_Source.py b/M02_MasterTemplete/M02_Item_Source.py index 895dda70..66b4dca9 100644 --- a/M02_MasterTemplete/M02_Item_Source.py +++ b/M02_MasterTemplete/M02_Item_Source.py @@ -49,7 +49,8 @@ def read_json(path: Path) -> Any: def row_of(document: Any, key: str) -> dict[str, Any] | None: rows = document.get("줄") if isinstance(document, dict) else None for row in rows or []: - if isinstance(row, dict) and row.get("키") == key: + # 옛 키로도 — 회사 · 개인 층 SS 키는 새 모양(SC · SP)으로 옮기며 `옛키` 에 남김(PLAN 26-4a) + if isinstance(row, dict) and (row.get("키") == key or key in (row.get("옛키") or [])): return row return None diff --git a/M02_MasterTemplete/M02_MasterTemplete_Book.py b/M02_MasterTemplete/M02_MasterTemplete_Book.py index 11188397..7cab2074 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Book.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Book.py @@ -1,30 +1,72 @@ -"""M02 구조물집계표조합(SS 장부) — 구조물 열마다 줄 하나 · 경로 옮겨 받기(PLAN 26-2a). +"""M02 구조물집계표조합(SS 장부) — 구조물 열마다 줄 하나 · 경로 옮겨 받기(PLAN 26-2a · 26-4a). 제어용 장부 · 관리자는 집계표 화면(또는 나무)으로 고침 · 서버가 집계표 [저장] 때 맞춤 · -키 = SS + 6자리 · 한 번 준 키는 그 열에 남음 · 지운 열의 키는 다시 안 씀(`다음` 이 기억). +키(층마다 머리가 다름 · 폴더 · 종류 뜻 없는 일련번호 6자리) — + 마스터 `SS000001` · 회사 `SC{회사번호}-000001` · 개인 `SP{사용자번호}-000001` +한 번 준 키는 그 열에 남음 · 지운 열의 키는 다시 안 씀(`다음` 이 기억) · 층 사이 복사 · +개인에 저장 · 프로젝트에서 새 항목은 그 층의 새 키(본뜬 곳은 열 `본뜬`). 회사 · 개인 층에 남은 +옛 `SS` 키는 맞출 때 새 모양으로 옮기고(숫자 여섯 그대로 · 겹치면 다음 번호) 옛 키는 줄 `옛키` 에 +남김 — 프로젝트 복사본이 들고 있는 옛 키로도 찾음(`find`). 줄 = `{키, 열, 경로, 이름, 종류, 분류1~3(경로 앞 넷 — 옛 코드가 읽음), 단위, 일위대가, 자재조합, -도번, 면적번, 재료번, 바인딩, 펼침}`. `M02_MasterTemplete_Store.py` 가 커서 떼어 냄 — 저장소는 -`store.sync_combos` · `store.read_combos` 이름으로 이 파일을 부름. +도번, 면적번, 재료번, 바인딩, 펼침, 옛키?}`. `M02_MasterTemplete_Store.py` 가 커서 떼어 냄 — +저장소는 `store.sync_combos` · `store.read_combos` · `store.find_combo` 이름으로 이 파일을 부름. """ from __future__ import annotations import json +import re +from pathlib import Path from typing import Any from common_util.common_util_json import atomic_write_json from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_MasterTemplete_Tree as tree +from M02_MasterTemplete import M02_Template_Layers as layers -HEAD = "SS" +HEAD = "SS" # 마스터(과 회사 · 개인 · 프로젝트 층 밖 모든 층) +_OLD = re.compile(r"^SS\d{6}$") NOTE = ( "구조물집계표 구조물 열(꼴=수)마다 한 줄 · 서버가 집계표 [저장] 때 맞춤(손으로 고치지 않음) · " - "키 SS 는 한 번 주면 안 바뀌고 지운 열의 키는 다시 안 씀 · 경로 = 폴더 이름 배열" + "키(마스터 SS · 회사 SC{회사}- · 개인 SP{사용자}- + 6자리)는 한 번 주면 안 바뀌고 " + "지운 열의 키는 다시 안 씀 · 옛키 = 옮기기 전 키 · 경로 = 폴더 이름 배열" ) -def _row(key: str, col: dict[str, Any], numbers: dict[str, str]) -> dict[str, Any]: +def key_head(root: Path) -> str: + """층 폴더 → 키 머리 — 회사 `SC{회사}-` · 개인 `SP{사용자}-` · + 그 밖(마스터 · 프로젝트 사본) `SS`.""" + try: + parts = Path(root).resolve().relative_to(layers.storage_root()).parts + except ValueError: + return HEAD + if len(parts) == 2 and parts[1] == layers.TEMPLATES_DIRNAME: + return f"SC{parts[0]}-" + if len(parts) == 3 and parts[2] == layers.TEMPLATES_DIRNAME: + return f"SP{parts[1]}-" + return HEAD + + +def _number(head: str, key: str) -> int | None: + """그 머리의 키면 여섯 자리 수 · 아니면 None.""" + found = re.fullmatch(re.escape(head) + r"(\d{6})", str(key)) + return int(found.group(1)) if found else None + + +def find(rows: list[dict[str, Any]], key: str) -> dict[str, Any] | None: + """키로 줄 — 지금 키 또는 옮기기 전 옛 키(프로젝트 복사본 · 구조물 기록이 들고 있는 것).""" + key = str(key) + for row in rows: + if isinstance(row, dict) and (row.get("키") == key or key in (row.get("옛키") or [])): + return row + return None + + +def _row( + key: str, col: dict[str, Any], numbers: dict[str, str], olds: list[str] | None = None +) -> dict[str, Any]: path = tree.path_of(col) + extra = {"옛키": olds} if olds else {} return { "키": key, "열": col["id"], @@ -37,28 +79,40 @@ def _row(key: str, col: dict[str, Any], numbers: dict[str, str]) -> dict[str, An **{field: numbers.get(field, "") for _, field, _, _ in store.PARTS}, "바인딩": col.get("바인딩"), "펼침": col.get("펼침"), + **extra, } def sync() -> dict[str, Any]: - """장부를 집계표에 맞춤 — 새 열 = 새 SS 키 · 지운 열 = 줄 지움 · 고친 열 = 칸 갱신 · - 줄 차례 = 집계표 열 차례. 바뀐 것이 없으면 파일을 안 씀.""" + """장부를 집계표에 맞춤 — 새 열 = 그 층 새 키 · 지운 열 = 줄 지움 · 고친 열 = 칸 갱신 · + 옛 모양 키(회사 · 개인 층의 SS)는 새 모양으로(옛 키는 `옛키`) · 줄 차례 = 집계표 열 차례. + 바뀐 것이 없으면 파일을 안 씀.""" path = store.root() / store.COMBO_FILE + head = key_head(store.root()) with store._LOCK: old = json.loads(path.read_bytes()) if path.is_file() else {} - keys = {row["열"]: row["키"] for row in old.get("줄", [])} - taken = [int(k[len(HEAD) :]) for k in keys.values()] + before = {row["열"]: row for row in old.get("줄", []) if isinstance(row, dict)} + taken = {n for row in before.values() if (n := _number(head, row.get("키"))) is not None} after = max(old.get("다음", 1), max(taken, default=0) + 1) numbers = store.structure_numbers() rows = [] for col in store._structure_columns(): - if col["id"] not in keys: - keys[col["id"]] = f"{HEAD}{after:06d}" + was = before.get(col["id"]) or {} + key, olds = was.get("키"), list(was.get("옛키") or []) + if key is not None and _number(head, key) is None: # 옛 모양 — 숫자 여섯 그대로 옮김 + olds.append(key) + digits = int(key[2:]) if _OLD.match(str(key)) else None + key = f"{head}{digits:06d}" if digits and digits not in taken else None + if key: + taken.add(digits) + if key is None: + key = f"{head}{after:06d}" + taken.add(after) after += 1 - rows.append(_row(keys[col["id"]], col, numbers.get(col["id"], {}))) + rows.append(_row(key, col, numbers.get(col["id"], {}), olds)) doc = {"그룹": "구조물집계표조합", "판": 1, "비고": NOTE, "다음": after} if old.get("지운번호"): - doc["지운번호"] = old["지운번호"] # 지운 열의 도번 8자리 — 다시 안 줌 + doc["지운번호"] = old["지운번호"] # 지운 열의 번호 — 다시 안 줌 doc["줄"] = rows if doc != old: atomic_write_json(path, doc) @@ -66,10 +120,13 @@ def sync() -> dict[str, Any]: def read() -> dict[str, Any]: - """장부 — 파일이 아직 없으면 지금 열 전부로 처음 한 번 만듦 · `경로` 가 없던 옛 줄은 읽을 때 - 종류 · 분류1~3 에서 채움(빈 분류 건너뜀 · 파일은 다음 [저장] 때 새 모양).""" + """장부 — 파일이 아직 없으면 지금 열 전부로 처음 한 번 만듦 · 회사 · 개인 층의 옛 SS 키는 새 + 모양으로 옮김 · `경로` 가 없던 옛 줄은 읽을 때 종류 · 분류1~3 에서 채움(빈 분류 건너뜀).""" path = store.root() / store.COMBO_FILE doc = json.loads(path.read_bytes()) if path.is_file() else sync() + head = key_head(store.root()) + if head != HEAD and any(_number(head, r.get("키")) is None for r in doc.get("줄") or []): + doc = sync() # 회사 · 개인 층의 옛 SS 키 — 처음 읽을 때 한 번 새 모양으로(옛키 남김) for row in doc.get("줄") or []: if "경로" not in row: row["경로"] = tree.row_path(row) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Inputs.py b/M02_MasterTemplete/M02_MasterTemplete_Inputs.py index 92692c6f..348278ef 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Inputs.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Inputs.py @@ -45,7 +45,7 @@ def pill_key(logic: str, name: str) -> str: def _item(key: str) -> dict[str, Any]: """지금 층(`store.at`)의 항목 줄 — 없으면 404.""" - row = next((r for r in store.read_combos().get("줄", []) if r.get("키") == key), None) + row = store.find_combo(key) # 옛 키(회사 · 개인 층 SS)로도 if row is None: raise store.StoreError(404, f"없는 항목 「{key}」") return row diff --git a/M02_MasterTemplete/M02_MasterTemplete_Items.py b/M02_MasterTemplete/M02_MasterTemplete_Items.py index e08613ff..5fbcc4b8 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Items.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Items.py @@ -112,7 +112,7 @@ def copy_item(source: Path, target: Path, key: str, origin: dict[str, Any]) -> d 새 번호 · 새 SS 키 · 열 `본뜬` = `origin`(원래 층 · 주인 · 키). 돌려줌 `{key, 열, 이름}`.""" with store.at(source): - row = next((r for r in store.read_combos().get("줄", []) if r.get("키") == key), None) + row = store.find_combo(key) # 옛 키(회사 · 개인 층 SS)로도 if row is None: raise store.StoreError(404, f"없는 항목 「{key}」") col_id = row["열"] diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py index 86bfd072..4399ecce 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py @@ -230,7 +230,10 @@ def _edit_tree(folder: Path, body: dict[str, Any]) -> dict[str, Any]: got = store.read("table", store.STRUCTURE_TABLE) if got["판"] != body.get("판"): raise store.StoreError(409, {"stale": [store.STRUCTURE_TABLE], "판": got["판"]}) - ids = {str(r.get("키")): str(r.get("열")) for r in store.read_combos().get("줄", [])} + ids = {} # 키 · 옛 키(회사 · 개인 층 SS) → 열 id + for r in store.read_combos().get("줄", []): + for key in [r.get("키"), *(r.get("옛키") or [])]: + ids[str(key)] = str(r.get("열")) try: doc, note = tree.apply(got["문서"], body, ids) except tree.TreeError as e: diff --git a/M02_MasterTemplete/M02_MasterTemplete_Store.py b/M02_MasterTemplete/M02_MasterTemplete_Store.py index 8f1e4a0b..f1bd4860 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Store.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Store.py @@ -47,8 +47,12 @@ def at(folder: str | Path) -> Iterator[Path]: # 구조물집계표 구조물 열(꼴=수) 마다 세 파일(파일 이름 = 열 id) — # 구조물 도면 `structure/` (도각 없는 전용 화면) · 면적산출 `area/` · 재료산출 `material/` # (둘 다 스프레드시트 통합문서) -# 번호 = 도번 SD · 면적번 SA · 재료번 SM + 8자리 — 한 열의 세 번호는 같은 8자리 · 한 번 준 번호는 -# 파일에 남아 안 바뀜(PLAN 13장) · 새 항목은 자리 뜻 없는 일련번호(PLAN 26-1 — 있던 번호는 그대로) +# 번호 = 도번 SD · 면적번 SA · 재료번 SM + 뒷자리 — 한 열의 세 번호는 같은 뒷자리 · 한 번 준 번호는 +# 파일에 남아 안 바뀜(PLAN 13장). 뒷자리 두 모양: +# 옛 8자리 `SD01020300` = [종류][분류1][분류2][분류3] 두 자리씩(그대로 유효 · 더는 새로 안 줌) +# 새 일련 `SD-000001` = 붙임표 + 6자리 · 자리 뜻 없음(PLAN 26-1 · 26-4a) — 옛 8자리 · +# 12장 옛 6자리(`SD010101`)와 글자 모양이 달라 겹치지 않음 · 층마다 따로 셈 +# (파일은 열 id 로 가름 · 번호는 보이는 이름) # 항목 경로(폴더 이름 배열) = 열 `머리` 의 빈 칸 건너뛴 이름(`M02_MasterTemplete_Tree`) STRUCTURE_TABLE = "구조물집계표" # 프로그램이 이름으로 부르는 양식 — 지우면 그 기능이 죽음(B07 이름은 `B07_DesignDetail_Engine_*`) @@ -66,7 +70,7 @@ PARTS = ( ) _KEY = {kind: key for kind, key, _, _ in PARTS} SHEETS = ("area", "material") # 통합문서 종류 — [저장] 때 서버가 계산값을 새로 적음 -_CODE = re.compile(r"^S[DAM](\d{8})$") +_CODE = re.compile(r"^S[DAM](\d{8}|-\d{6})$") _OLD_CODE = re.compile(r"^S[DCA](\d{6})$") # PLAN 12장 6자리 — 끝에 00 을 붙여 읽음 _BAD_NAME = re.compile(r'[\\/:*?"<>|\x00-\x1f]|\.\.') # 도각 없는 빈 CAD 문서의 기본 도면층 — 없으면 B07 CAD 가 못 엶("Cannot read properties @@ -203,7 +207,8 @@ def _docs(kind: str) -> list[tuple[str, Any]]: def _code(doc: Any, key: str) -> str | None: - """문서의 번호 8자리 — 12장 6자리는 끝에 00 · 옛 `구-01` · `표-01` · 없음은 None.""" + """문서 번호의 뒷자리 — 옛 8자리 · 새 `-` + 6자리 · 12장 6자리는 끝에 00 · + 옛 `구-01` · `표-01` · 없음은 None.""" text = str(doc.get(key) or "") if isinstance(doc, dict) else "" if found := _CODE.match(text): return found.group(1) @@ -212,7 +217,7 @@ def _code(doc: Any, key: str) -> str | None: def _codes() -> dict[str, str]: - """`{열 id: 8자리}` — 구조물 도면 도번 먼저 · 없으면 면적번 · 재료번.""" + """`{열 id: 뒷자리}` — 구조물 도면 도번 먼저 · 없으면 면적번 · 재료번.""" codes: dict[str, str] = {} for kind, key, _, _ in reversed(PARTS): codes.update({name: c for name, doc in _docs(kind) if (c := _code(doc, key))}) @@ -248,20 +253,24 @@ def _column_ids() -> list[str]: return [col["id"] for col in _structure_columns()] +_SERIAL = re.compile(r"^-(\d{6})$") + + def _serial(taken: dict[str, str], retired: list[str] | None = None) -> str: - """새 항목 번호 — 자리 뜻 없는 일련번호(있던 번호 · 지운 번호 가운데 가장 큰 수 + 1 · 8자리).""" - codes = [int(c) for c in [*taken.values(), *(retired or [])] if str(c).isdigit()] - return f"{max(codes, default=0) + 1:08d}" + """새 항목 번호 뒷자리 — 붙임표 + 일련 6자리(새 모양 번호 · 지운 번호 가운데 가장 큰 수 + 1) · + 옛 8자리는 셈에 안 넣음(모양이 달라 겹칠 일 없음).""" + found = (_SERIAL.match(str(c)) for c in [*taken.values(), *(retired or [])]) + return f"-{max((int(m.group(1)) for m in found if m), default=0) + 1:06d}" def _new_code(column: str) -> str: - """이 열이 쓸 8자리 — 짝 파일에 이미 있으면 그것 · 없으면 새로(잠금 안에서 부름).""" + """이 열이 쓸 번호 뒷자리 — 짝 파일에 이미 있으면 그것 · 없으면 새로(잠금 안에서 부름).""" taken = _codes() return taken[column] if column in taken else _serial(taken, _retired()) def _retired() -> list[str]: - """지운 열의 8자리 — 조합 장부 `지운번호`(한 번 준 번호는 다시 안 줌).""" + """지운 열의 번호 뒷자리 — 조합 장부 `지운번호`(한 번 준 번호는 다시 안 줌).""" path = root() / COMBO_FILE return json.loads(path.read_bytes()).get("지운번호", []) if path.is_file() else [] @@ -289,7 +298,7 @@ def _blank(kind: str, column: str) -> dict[str, Any]: def _ensure(kind: str, column: str, source: str | None = None) -> bool: - """그 열의 파일이 없으면 빈 문서를 번호(세 파일이 같은 8자리)와 같이 만듦 · 있으면 그대로. + """그 열의 파일이 없으면 빈 문서를 번호(세 파일이 같은 뒷자리)와 같이 만듦 · 있으면 그대로. `source` 열 파일이 있으면 빈 문서 대신 그 내용(알약 `연결` 포함 · 계산값은 서버가 다시 풂).""" path = _path(kind, column) if path.is_file(): @@ -329,8 +338,8 @@ def _take_origins(doc: Any) -> dict[str, str]: def create_structure(column: str) -> dict[str, Any]: - """구조물 열 하나의 구조물 도면 새로 — 도번 SD + 8자리 · 이미 있으면 409. - 면적산출 · 재료산출 파일이 없으면 빈 통합문서도 같이 만듦(같은 8자리).""" + """구조물 열 하나의 구조물 도면 새로 — 도번 SD + 새 일련(`-` + 6자리) · 이미 있으면 409. + 면적산출 · 재료산출 파일이 없으면 빈 통합문서도 같이 만듦(같은 뒷자리).""" if column not in _column_ids(): raise StoreError(404, f"{STRUCTURE_TABLE}에 없는 구조물 열 「{column}」") if not _ensure("structure", column): @@ -471,6 +480,13 @@ def sync_combos() -> dict[str, Any]: return book.sync() +def find_combo(key: str) -> dict[str, Any] | None: + """키(또는 옮기기 전 옛 키)로 장부 줄 — 없으면 None(`Book.find`).""" + from M02_MasterTemplete import M02_MasterTemplete_Book as book + + return book.find(book.read().get("줄", []), key) + + def read_combos() -> dict[str, Any]: """조합 장부 — 파일이 아직 없으면 지금 열 전부로 처음 한 번 만듦(`Book.read`).""" from M02_MasterTemplete import M02_MasterTemplete_Book as book diff --git a/M02_MasterTemplete/M02_MasterTemplete_Tree.py b/M02_MasterTemplete/M02_MasterTemplete_Tree.py index b70e3e55..4bbdd933 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Tree.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Tree.py @@ -142,7 +142,7 @@ def _path(value: Any, what: str = "경로") -> list[str]: def _find(doc: dict[str, Any], key: str | None, ids: dict[str, str]) -> dict[str, Any]: - """SS 키(또는 열 id)로 항목 열 — 없으면 404.""" + """항목 키 · 옛 키(또는 열 id)로 항목 열 — 없으면 404.""" cid = ids.get(str(key), str(key)) if key else None col = next((c for c in _items(doc) if c["id"] == cid), None) if col is None: diff --git a/common_util/common_util_summary_item.py b/common_util/common_util_summary_item.py index 08f58c67..dee3682e 100644 --- a/common_util/common_util_summary_item.py +++ b/common_util/common_util_summary_item.py @@ -3,6 +3,10 @@ summary_item = {layer: "system"|"company"|"personal", owner: null|"{회사}"|"{회사}/{사용자}", key: "SS000004", name: "…"} +항목 키(PLAN 26-4a) — 마스터 `SS000004` · 회사 `SC{회사}-000001` · 개인 `SP{사용자}-000001` +(머리의 번호 = owner 의 회사 · 사용자). 옛 `SS` 키는 어느 층이든 그대로 받음 — 회사 · 개인 층이 새 +모양으로 옮기기 전에 적힌 기록(층 장부의 `옛키` 로 찾음). + 구조물 기록(`structures.json` `StructureInstance`) · 관 지점 기록(`pipe_points.json` 점) 두 곳이 같은 칸을 씀 — 검사는 여기 한 벌. 없으면 null(옛 파일 호환). """ @@ -13,7 +17,11 @@ import re from typing import Any SUMMARY_LAYERS = ("system", "company", "personal") -_KEY = re.compile(r"^SS\d{6}$") +_KEY = re.compile(r"^SS\d{6}$") # 마스터 · 옛 키 +_LAYER_KEY = { + "company": re.compile(r"^SC(\d+)-\d{6}$"), + "personal": re.compile(r"^SP(\d+)-\d{6}$"), +} _OWNER = { "system": None, "company": re.compile(r"^\d+$"), @@ -43,18 +51,32 @@ def parse_summary_item(value: Any) -> dict[str, Any] | None: shape = "{회사}" if layer == "company" else "{회사}/{사용자}" raise ValueError(f"{layer} 층 집계 항목의 owner 는 「{shape}」 숫자 모양입니다.") key = value.get("key") - if not isinstance(key, str) or not _KEY.match(key): - raise ValueError("집계 항목 key 는 SS 와 숫자 6자리입니다(예: SS000004).") + if not isinstance(key, str) or not _key_ok(layer, owner, key): + raise ValueError( + "집계 항목 key 는 마스터 SS000004 · 회사 SC{회사}-000001 · " + "개인 SP{사용자}-000001 모양입니다(옛 SS 키는 어느 층이든 받음)." + ) name = value.get("name") if not isinstance(name, str) or not name.strip() or len(name) > _NAME_MAX: raise ValueError(f"집계 항목 name 은 1~{_NAME_MAX}자 글입니다.") return {"layer": layer, "owner": owner, "key": key, "name": name} +def _key_ok(layer: str, owner: Any, key: str) -> bool: + """옛 · 마스터 SS 키는 어느 층이든 · SC · SP 는 그 층에서 머리 번호가 owner 와 같을 때만.""" + if _KEY.match(key): + return True + rule = _LAYER_KEY.get(layer) + found = rule.match(key) if rule else None + if not found or not isinstance(owner, str): + return False + return found.group(1) == (owner if layer == "company" else owner.split("/")[-1]) + + def snapshot_folder_name(item: dict[str, Any]) -> str: """`{layer}_{owner 슬러그}_{key}` — owner 없음 = `none` · `/` = `-`. - 예: `system_none_SS000004` · `company_3_SS000001` · `personal_3-12_SS000002`. + 예: `system_none_SS000004` · `company_3_SC3-000001` · `personal_3-12_SP12-000002`. """ owner = item.get("owner") slug = str(owner).replace("/", "-") if owner else "none" diff --git a/resources/tester/spreadsheet/test_spreadsheet_store.py b/resources/tester/spreadsheet/test_spreadsheet_store.py index 5be901bb..edac0a18 100644 --- a/resources/tester/spreadsheet/test_spreadsheet_store.py +++ b/resources/tester/spreadsheet/test_spreadsheet_store.py @@ -68,13 +68,13 @@ def test_새_구조물_도면은_면적_재료_진짜_빈_시트_같은_8자리( made1 = client.post("/api/m02/structures/rv").json()["문서"] made2 = client.post("/api/m02/structures/pv_b").json()["문서"] # 새 항목 번호 = 자리 뜻 없는 일련번호(PLAN 26-1) - assert made1["도번"] == "SD00000001" and made2["도번"] == "SD00000002" + assert made1["도번"] == "SD-000001" and made2["도번"] == "SD-000002" for kind, key, head in (("area", "면적번", "SA"), ("material", "재료번", "SM")): doc = client.get(f"/api/m02/templates/{kind}/pv_b").json()["문서"] - assert doc[key] == f"{head}00000002" + assert doc[key] == f"{head}-000002" assert doc["시트"][0]["칸"] == {} and doc["서식"] == [{}] # 머리 줄 없음 numbers = client.get("/api/m02/structures/numbers").json() - assert numbers["pv_b"] == {"도번": "SD00000002", "면적번": "SA00000002", "재료번": "SM00000002"} + assert numbers["pv_b"] == {"도번": "SD-000002", "면적번": "SA-000002", "재료번": "SM-000002"} @pytest.mark.parametrize( @@ -90,7 +90,7 @@ def test_다른_열_내용을_복사해_가져옴_연결도_같이(client, kind, assert r.status_code == 200 ms = client.get(f"/api/m02/templates/{kind}/ms").json()["문서"] assert (ms["시트"][0]["칸"], ms["연결"]) == ({"A1": {"식": "1+1"}}, LINKS) - assert (ms[key], ms["열"]) == (f"{head}00000010", "ms") # 번호 · 열은 받는 쪽 그대로 + assert (ms[key], ms["열"]) == (f"{head}-000010", "ms") # 번호 · 열은 받는 쪽 그대로 assert ms["계산값"] == SERVER # 서버가 다시 풂 assert client.post(f"/api/m02/{kind}/ms/copy", params={"from": "없는열"}).status_code == 404 diff --git a/resources/tester/test_m02_item_keys.py b/resources/tester/test_m02_item_keys.py new file mode 100644 index 00000000..f6c6a82e --- /dev/null +++ b/resources/tester/test_m02_item_keys.py @@ -0,0 +1,213 @@ +"""M02 항목 키 체계 · 품번 겹침 점검(PLAN 26-4a). + +키 — 마스터 `SS` + 6자리 · 회사 `SC{회사}-` + 6자리 · 개인 `SP{사용자}-` + 6자리 · +한 번 주면 안 바뀜 · 지운 키 다시 안 씀 · 층 사이 복사는 새 키 + 본뜬 곳 · +회사 · 개인 층의 옛 SS 키는 새 모양으로 옮기고 옛 키로도 찾음. +번호 — 새 일련 `SD-000001`(붙임표 + 6자리)이 옛 8자리(`SD01020300`) · 12장 6자리(`SD010101`)와 +모양이 달라 겹치지 않음 · 99 넘는 형제 · 깊이 6 에서도 넘침 없음 · 층마다 따로 셈. +""" + +from __future__ import annotations + +import json +import re +from pathlib import Path +from typing import Any + +import pytest + +from common_util.common_util_summary_item import parse_summary_item +from config import config_system +from M02_MasterTemplete import M02_Item_Source as source +from M02_MasterTemplete import M02_MasterTemplete_Book as book +from M02_MasterTemplete import M02_MasterTemplete_Store as store +from M02_MasterTemplete import M02_Template_Layers as layers +from resources.tester.test_m02_structure import write_sample_table +from resources.tester.test_m02_tree import Tree, _app + +ROOT = Path(__file__).resolve().parents[2] +NEW = re.compile(r"^S[DAM]-\d{6}$") + + +@pytest.fixture() +def world(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]: + system = tmp_path / "system" + system.mkdir() + write_sample_table(system) + monkeypatch.setattr(store, "FOLDER", system) + monkeypatch.setattr(layers, "SYSTEM_ROOT", system) + store.renumber() + storage = tmp_path / "storage" + storage.mkdir() + monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(storage)) + session: dict[str, Any] = {"user_id": 42, "company_id": 7, "role": "ADMIN", "is_master": False} + return {"client": _app(session), "session": session, "storage": storage, "system": system} + + +def _numbers(root: Path, cid: str) -> set[str]: + return { + json.loads((root / kind / f"{cid}.json").read_bytes())[key] + for kind, key, _, _ in store.PARTS + } + + +# ── 키 ───────────────────────────────────────────── + + +def test_층마다_키_모양_안_바뀜_다시_안_씀(world): + client = world["client"] + company = Tree(client, "company") + company.do(op="add-item", 경로=["돌쌓기"], 이름="찰쌓기") + company.do(op="add-item", 경로=["돌쌓기"], 이름="메쌓기") + keys = [r["key"] for r in company.now["items"]] + assert keys == ["SC7-000001", "SC7-000002"] + # 이름 바꾸기 · 옮기기 · 표 저장에도 키 그대로(폴더 뜻 없음) + company.do(op="rename", 키="SC7-000001", 이름="찰쌓기2") + company.do(op="move", 키="SC7-000002", 새경로=["다른폴더"]) + assert {r["key"]: r["경로"] for r in company.now["items"]} == { + "SC7-000001": ["돌쌓기", "찰쌓기2"], + "SC7-000002": ["다른폴더", "메쌓기"], + } + # 지운 키는 다시 안 씀 + company.do(op="delete", 키="SC7-000002") + company.do(op="add-item", 경로=[], 이름="새것") + assert [r["key"] for r in company.now["items"]] == ["SC7-000001", "SC7-000003"] + # 개인 층 = SP{사용자} · 층 사이 복사 = 새 키 + 본뜬 곳 + personal = Tree(client, "personal") + personal.do(op="add-item", 경로=[], 이름="내것") + assert [r["key"] for r in personal.now["items"]] == ["SP42-000001"] + body = {"from_layer": "company", "from_owner": "7", "key": "SC7-000001"} + made = client.post("/api/m02/layers/personal/items/copy", json=body).json() + assert made["key"] == "SP42-000002" + table = client.get("/api/m02/layers/personal/templates/table/구조물집계표").json()["문서"] + col = next(c for c in table["열"] if c["id"] == made["열"]) + assert col["본뜬"] == {"layer": "company", "owner": "7", "key": "SC7-000001"} + # 마스터는 SS 그대로 + assert Tree(client, "system").now["items"][0]["key"].startswith("SS") + + +def _legacy_personal(storage: Path, rows: list[tuple[str, str]], after: int) -> Path: + """옛 개인 층 — 집계표 열 + SS 키 장부(26-4a 앞 모양).""" + folder = storage / "7" / "42" / "templates" + for kind in store.KINDS: + (folder / kind).mkdir(parents=True, exist_ok=True) + cols = [{"id": cid, "머리": [cid, None, None, None], "꼴": "수"} for _, cid in rows] + table = {"층": ["종류", "분류1", "분류2", "분류3"], "열": cols, "줄": [], "합계줄": []} + (folder / "table" / "구조물집계표.json").write_text(json.dumps(table), encoding="utf-8") + book_rows = [{"키": key, "열": cid, "종류": cid} for key, cid in rows] + doc = {"그룹": "구조물집계표조합", "다음": after, "줄": book_rows} + (folder / store.COMBO_FILE).write_text(json.dumps(doc), encoding="utf-8") + return folder + + +def test_옛_SS_키는_새_모양으로_옮기고_옛_키로도_찾음(world): + folder = _legacy_personal(world["storage"], [("SS000001", "a"), ("SS000003", "b")], 5) + raw = json.loads((folder / store.COMBO_FILE).read_bytes()) # 복사본 manifest 가 든 옛 줄 + client = world["client"] + rows = client.get("/api/m02/items?layer=personal").json() + assert [r["key"] for r in rows] == ["SP42-000001", "SP42-000003"] # 숫자 여섯 그대로 + migrated = json.loads((folder / store.COMBO_FILE).read_bytes()) + assert [r["옛키"] for r in migrated["줄"]] == [["SS000001"], ["SS000003"]] + assert migrated["다음"] == 5 + # 옛 키로도 찾음 — 입력 목록 · 나무 고침 · 복사본 원본 찾기(M02_Item_Source) + assert client.get("/api/m02/layers/personal/items/SS000003/inputs").status_code == 200 + tree_ = Tree(client, "personal") + tree_.do(op="rename", 키="SS000001", 이름="a2") + assert tree_.item(["a2"])["key"] == "SP42-000001" + assert source.row_of(migrated, "SS000003")["키"] == "SP42-000003" + assert source.row_of(raw, "SS000003")["키"] == "SS000003" # 옮기기 전 파일도 그대로 읽힘 + # 새 항목 = 다음 번호(옛 번호 · 지운 번호 안 씀) + tree_.do(op="add-item", 경로=[], 이름="c") + assert tree_.item(["c"])["key"] == "SP42-000005" + + +def test_옛_숫자가_이미_있으면_다음_번호로(world): + folder = _legacy_personal(world["storage"], [("SP42-000001", "a"), ("SS000001", "b")], 2) + rows = world["client"].get("/api/m02/items?layer=personal").json() + assert [r["key"] for r in rows] == ["SP42-000001", "SP42-000002"] + assert json.loads((folder / store.COMBO_FILE).read_bytes())["줄"][1]["옛키"] == ["SS000001"] + + +def test_집계_항목_참조_키_모양(): + ok = [ + {"layer": "system", "owner": None, "key": "SS000004"}, + {"layer": "company", "owner": "7", "key": "SC7-000001"}, + {"layer": "personal", "owner": "7/42", "key": "SP42-000001"}, + {"layer": "company", "owner": "7", "key": "SS000001"}, # 옛 키 + {"layer": "personal", "owner": "7/42", "key": "SS000002"}, # 옛 키 + ] + for item in ok: + assert parse_summary_item({**item, "name": "x"})["key"] == item["key"] + bad = [ + {"layer": "company", "owner": "8", "key": "SC7-000001"}, # 다른 회사 + {"layer": "personal", "owner": "7/43", "key": "SP42-000001"}, # 다른 사람 + {"layer": "personal", "owner": "7/42", "key": "SC7-000001"}, # 층 머리 틀림 + {"layer": "system", "owner": None, "key": "SC7-000001"}, + {"layer": "company", "owner": "7", "key": "SC7-00001"}, # 자리수 + {"layer": "company", "owner": "7", "key": "SD-000001"}, # 번호는 키가 아님 + ] + for item in bad: + with pytest.raises(ValueError): + parse_summary_item({**item, "name": "x"}) + + +# ── 번호 ──────────────────────────────────────────── + + +def test_새_번호는_옛_번호와_모양이_달라_안_겹침(world): + """마스터 옛 8자리는 그대로 · 새 열은 `-` + 6자리 · 99 넘는 형제 · 깊이 6 에서도 한 모양.""" + system = world["system"] + # 견본 열 둘을 옛 8자리(제목행 자리)로 — 마스터 51 항목이 지금 이 모양 + for cid, code in (("rv", "01010100"), ("c1", "01010200")): + for kind, key, head, _ in store.PARTS: + path = system / kind / f"{cid}.json" + doc = json.loads(path.read_bytes()) + path.write_text(json.dumps({**doc, key: f"{head}{code}"}), encoding="utf-8") + old = {cid: _numbers(system, cid) for cid in store._column_ids()} + assert old["rv"] == {"SD01010100", "SA01010100", "SM01010100"} + first = max(int(n[3:]) for nums in old.values() for n in nums if NEW.match(n)) + 1 + with store.at(system): + got = store.read("table", store.STRUCTURE_TABLE) + doc = got["문서"] + deep = ["a", "b", "c", "d", "e", "f"] + doc["열"] += [ + {"id": f"x{i}", "머리": [*deep[:5], f"항목{i}"], "꼴": "수"} for i in range(120) + ] + store.write("table", store.STRUCTURE_TABLE, got["판"], doc) + new = [_numbers(system, f"x{i}") for i in range(120)] + tails = [{n[2:] for n in nums} for nums in new] + assert all(len(t) == 1 for t in tails) # 세 파일 같은 뒷자리 + assert [t.pop() for t in tails] == [f"-{i:06d}" for i in range(first, first + 120)] + assert all(NEW.match(n) for nums in new for n in nums) + assert not any(store._OLD_CODE.match(n) for nums in new for n in nums) # 12장 6자리로 안 읽힘 + assert {cid: _numbers(system, cid) for cid in old} == old # 옛 번호 그대로 + # 지운 번호 다시 안 줌 · 셈은 새 모양끼리 + with store.at(system): + got = store.read("table", store.STRUCTURE_TABLE) + doc = got["문서"] + doc["열"] = [c for c in doc["열"] if c["id"] != "x119"] + store.write("table", store.STRUCTURE_TABLE, got["판"], doc) + got = store.read("table", store.STRUCTURE_TABLE) + got["문서"]["열"].append({"id": "y", "머리": ["y"], "꼴": "수"}) + store.write("table", store.STRUCTURE_TABLE, got["판"], got["문서"]) + last = f"-{first + 120:06d}" # 지운 x119 번호(first + 119) 다음 + assert _numbers(system, "y") == {f"SD{last}", f"SA{last}", f"SM{last}"} + + +def test_층마다_번호는_따로_파일은_열_id_로(world): + client = world["client"] + Tree(client, "company").do(op="add-item", 경로=[], 이름="회사것") + Tree(client, "personal").do(op="add-item", 경로=[], 이름="내것") + storage = world["storage"] + company = next((storage / "7" / "templates" / "structure").glob("*.json")) + mine = next((storage / "7" / "42" / "templates" / "structure").glob("*.json")) + assert json.loads(company.read_bytes())["도번"] == json.loads(mine.read_bytes())["도번"] + assert company.parent != mine.parent # 같은 번호여도 층 폴더가 달라 안 겹침 + + +def test_키_머리는_M01_키대장과_안_겹침(): + ledger = json.loads((ROOT / "resources/master_data/_키대장.json").read_text("utf-8"))["다음"] + for head in ("SS", "SC", "SP", "SD", "SA", "SM", "UM"): + assert head not in ledger, head + assert "UA" in ledger # 일위대가 조합은 M01 키대장 몫(M02 는 읽기만) + assert book.HEAD == "SS" diff --git a/resources/tester/test_m02_layers_store.py b/resources/tester/test_m02_layers_store.py index 2afbc3c2..eebcdaa1 100644 --- a/resources/tester/test_m02_layers_store.py +++ b/resources/tester/test_m02_layers_store.py @@ -108,7 +108,7 @@ def test_회사_층_첫_진입_뼈대와_쓰기_권한(world): assert client.put(url, json={"판": got["판"], "문서": got["문서"]}).status_code == 403 _add_column(_as(world, "ADMIN"), "company", COL) items = _as(world).get("/api/m02/items?layer=company").json() # 같은 회사 일반 사용자 - assert [(r["key"], r["열"]) for r in items] == [("SS000001", "cw")] + assert [(r["key"], r["열"]) for r in items] == [("SC7-000001", "cw")] # 회사 키(26-4a) assert items[0]["도번"].startswith("SD") and (root / "structure/cw.json").is_file() assert client.get("/api/m02/items?layer=company&owner=9").status_code == 403 # 남의 회사 # 일위대가(UA) 갈라 내기는 M01 시스템 마스터에 쓰므로 회사 관리자도 못 함 @@ -163,7 +163,7 @@ def test_회사_항목을_개인으로_복사(world): got = user.post("/api/m02/layers/personal/items/copy", json=body) assert got.status_code == 200, got.text made = got.json() - assert made["열"] == "cw" and made["key"] == "SS000001" # 응답 = 새 SS 키(바로 고름) + assert made["열"] == "cw" and made["key"] == "SP42-000001" # 응답 = 개인 층 새 키(바로 고름) mine_rows = user.get("/api/m02/items?layer=personal").json() assert mine_rows[0]["key"] == made["key"] and mine_rows[0]["names"]["일위대가"] == ua_name mine = "/api/m02/layers/personal" @@ -175,7 +175,7 @@ def test_회사_항목을_개인으로_복사(world): assert mat["이름"] == "회사 자재" and mat["담은자재"][0]["코드"] == _material()["코드"] # 또 복사하면 새 열 id · 새 SS 키 · 자재 조합 이름 (2) again = user.post("/api/m02/layers/personal/items/copy", json=body).json() - assert (again["열"], again["key"]) == ("cw_2", "SS000002") + assert (again["열"], again["key"]) == ("cw_2", "SP42-000002") names = [r["이름"] for r in user.get(f"{mine}/material-combos").json()["줄"]] assert names == ["회사 자재", "회사 자재 (2)"] # 마스터 항목도 개인으로 · 없는 키 404 diff --git a/resources/tester/test_m02_structure.py b/resources/tester/test_m02_structure.py index 6b28e248..c532c67c 100644 --- a/resources/tester/test_m02_structure.py +++ b/resources/tester/test_m02_structure.py @@ -76,7 +76,7 @@ def test_새로_도번_자동_409_없는_열(client): "구조물도면", "structure", "pp_len", - "SD00000001", + "SD-000001", ) # 도각 없는 전용 화면 — 그린 만큼이 설계 영역 · 기본 도면층 하나(없으면 CAD 가 못 엶) assert doc["도면"]["format"] == 6 @@ -87,13 +87,13 @@ def test_새로_도번_자동_409_없는_열(client): for kind in ("area", "material"): assert client.get(f"/api/m02/templates/{kind}/pp_len").json()["문서"]["종류"] == "통합문서" - assert client.post("/api/m02/structures/rv").json()["문서"]["도번"] == "SD00000002" + assert client.post("/api/m02/structures/rv").json()["문서"]["도번"] == "SD-000002" assert client.post("/api/m02/structures/pp_len").status_code == 409 assert client.post("/api/m02/structures/없는열").status_code == 404 assert client.post("/api/m02/structures/no").status_code == 404 # 글 열은 구조물 아님 assert client.get("/api/m02/structures/numbers").json() == { - "pp_len": _num("00000001"), - "rv": _num("00000002"), + "pp_len": _num("-000001"), + "rv": _num("-000002"), } rows = client.get("/api/m02/templates").json() assert {(r["종류"], r["이름"]) for r in rows if r["종류"] == "structure"} == { @@ -119,7 +119,7 @@ def test_저장_뼈대_거름(client): "/api/m02/templates/structure/pp_len", json={"판": now, "문서": {**doc, "도번": "SD9"}} ) got = client.get("/api/m02/templates/structure/pp_len").json()["문서"] - assert got["도번"] == "SD00000001" # 번호는 서버 것 — 화면 값을 안 받음 + assert got["도번"] == "SD-000001" # 번호는 서버 것 — 화면 값을 안 받음 def test_지워도_번호를_당기지_않음_짝은_같은_번호(client): @@ -127,11 +127,11 @@ def test_지워도_번호를_당기지_않음_짝은_같은_번호(client): client.post(f"/api/m02/structures/{column}") assert client.delete("/api/m02/templates/structure/rv").status_code == 200 numbers = client.get("/api/m02/structures/numbers").json() - assert numbers["rv"] == {**_num("00000002"), "도번": ""} # 면적 · 재료는 남아 번호를 쥠 - assert numbers["ms"] == _num("00000003") # 다른 열 번호는 그대로 + assert numbers["rv"] == {**_num("-000002"), "도번": ""} # 면적 · 재료는 남아 번호를 쥠 + assert numbers["ms"] == _num("-000003") # 다른 열 번호는 그대로 # 새 열 = 다음 일련번호(자리 뜻 없음) · 다시 만든 도면은 짝(면적산출) 번호를 이어 받음 - assert client.post("/api/m02/structures/pp_cp").json()["문서"]["도번"] == "SD00000004" - assert client.post("/api/m02/structures/rv").json()["문서"]["도번"] == "SD00000002" + assert client.post("/api/m02/structures/pp_cp").json()["문서"]["도번"] == "SD-000004" + assert client.post("/api/m02/structures/rv").json()["문서"]["도번"] == "SD-000002" def test_번호_없는_열은_열_차례로_일련번호_한_번_옮김(client, tmp_path): @@ -148,15 +148,15 @@ def test_번호_없는_열은_열_차례로_일련번호_한_번_옮김(client, numbers = client.get("/api/m02/structures/numbers").json() assert set(numbers) == set(store._column_ids()) and "sta" not in numbers expect = { # 구조물 열 차례 그대로 1 부터(제목행 자리 뜻 없음) - "rv": "00000001", - "c1": "00000002", - "pv_b": "00000003", - "pv_l": "00000004", - "pp_len": "00000007", - "fb": "00000009", - "be": "00000011", - "h_rip_c": "00000012", - "h_rip_m": "00000013", + "rv": "-000001", + "c1": "-000002", + "pv_b": "-000003", + "pv_l": "-000004", + "pp_len": "-000007", + "fb": "-000009", + "be": "-000011", + "h_rip_c": "-000012", + "h_rip_m": "-000013", } for column, code in expect.items(): assert numbers[column] == _num(code), column @@ -184,10 +184,10 @@ def test_집계표_저장_때_새_열은_만들고_지운_열은_같이_지움(c for kind_ in ("structure", "area", "material"): assert client.get(f"/api/m02/templates/{kind_}/pv_a").status_code == 404 # 새 열 = 열 차례로 다음 일련번호 · 지운 pv_a(5) 는 비워 둠(다른 번호를 안 당김) - assert numbers["pv_x"] == _num("00000014") - assert numbers["zz"] == _num("00000015") - assert numbers["pv_y"] == _num("00000016") - assert numbers["pv_b"] == _num("00000003") # 있던 번호는 그대로 + assert numbers["pv_x"] == _num("-000014") + assert numbers["zz"] == _num("-000015") + assert numbers["pv_y"] == _num("-000016") + assert numbers["pv_b"] == _num("-000003") # 있던 번호는 그대로 def test_지운_열_번호는_다음_저장에도_다시_안_줌(client): @@ -197,12 +197,12 @@ def test_지운_열_번호는_다음_저장에도_다시_안_줌(client): got = client.get(url).json() got["문서"]["열"] = [c for c in got["문서"]["열"] if c["id"] != "h_rip_m"] assert client.put(url, json={"판": got["판"], "문서": got["문서"]}).status_code == 200 - assert client.get("/api/m02/structure-combos").json()["지운번호"] == ["00000013"] + assert client.get("/api/m02/structure-combos").json()["지운번호"] == ["-000013"] got = client.get(url).json() new = {"id": "pv_z", "머리": ["콘크리트포장", "T=(두께마다)", "Z", None], "꼴": "수"} got["문서"]["열"].append(new) assert client.put(url, json={"판": got["판"], "문서": got["문서"]}).status_code == 200 - assert client.get("/api/m02/structures/numbers").json()["pv_z"] == _num("00000014") + assert client.get("/api/m02/structures/numbers").json()["pv_z"] == _num("-000014") def test_다른_열_도면을_복사해_가져옴(client): @@ -214,8 +214,8 @@ def test_다른_열_도면을_복사해_가져옴(client): assert r.status_code == 200 ms = client.get("/api/m02/templates/structure/ms").json()["문서"] assert ms["도면"]["entities"] == [{"type": "circle"}] - assert (ms["도번"], ms["열"]) == ("SD00000010", "ms") # 번호 · 열은 받는 쪽 그대로 - assert client.get("/api/m02/templates/structure/rv").json()["문서"]["도번"] == "SD00000001" + assert (ms["도번"], ms["열"]) == ("SD-000010", "ms") # 번호 · 열은 받는 쪽 그대로 + assert client.get("/api/m02/templates/structure/rv").json()["문서"]["도번"] == "SD-000001" bad = client.post("/api/m02/structures/ms/copy", params={"from": "없는열"}) assert bad.status_code == 404 diff --git a/resources/tester/test_m02_tree.py b/resources/tester/test_m02_tree.py index 02ed8bb9..de7289ce 100644 --- a/resources/tester/test_m02_tree.py +++ b/resources/tester/test_m02_tree.py @@ -154,7 +154,7 @@ def test_빈_층에서_폴더_항목_깊이_1_2_5_이름_옮기기_지우기(wor assert all(len(c["머리"]) == 5 for c in table["열"]) # 번호 = 자리 뜻 없는 일련번호 · 세 파일 같은 번호 codes = [_numbers(root, t.item(p)["열"]) for p in t.paths()] - assert codes == [{"00000001"}, {"00000002"}, {"00000003"}] + assert codes == [{"-000001"}, {"-000002"}, {"-000003"}] assert ["a", "b", "c", "d"] in t.now["폴더"] and ["돌쌓기"] in t.now["폴더"] # 겹침 409 · 모양 422 · 없는 것 404 t.do(409, op="add-item", 경로=["돌쌓기"], 이름="찰쌓기") @@ -171,7 +171,7 @@ def test_빈_층에서_폴더_항목_깊이_1_2_5_이름_옮기기_지우기(wor key = t.item(["돌쌓기", "찰쌓기"])["key"] t.do(op="rename", 키=key, 이름="메쌓기") moved = t.item(["돌쌓기", "메쌓기"]) - assert moved["key"] == key and _numbers(root, moved["열"]) == {"00000002"} + assert moved["key"] == key and _numbers(root, moved["열"]) == {"-000002"} t.do(op="rename", 경로=["a"], 이름="A") assert ["A", "b", "c", "d", "e"] in t.paths() and ["a"] not in t.now["폴더"] t.do(409, op="rename", 경로=["A"], 이름="돌쌓기") # 있는 폴더로 합치기 막음 @@ -189,7 +189,7 @@ def test_빈_층에서_폴더_항목_깊이_1_2_5_이름_옮기기_지우기(wor for part in ("structure", "area", "material", "inputs"): assert not (root / part / f"{gone['열']}.json").is_file() t.do(op="add-item", 경로=["돌쌓기"], 이름="새것") # 지운 번호는 다시 안 줌 - assert _numbers(root, t.item(["돌쌓기", "새것"])["열"]) == {"00000004"} + assert _numbers(root, t.item(["돌쌓기", "새것"])["열"]) == {"-000004"} t.do(op="delete", 경로=["돌쌓기", "b"]) # 깊은 항목이 빠지면 머리 줄 넷으로 table = json.loads((root / "table/구조물집계표.json").read_bytes()) assert table["층"] == ["종류", "분류1", "분류2", "분류3"] From a718c2d67cb0467af91050a135aaa2a032ac655e Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 30 Sep 2026 09:12:05 +0900 Subject: [PATCH 13/18] =?UTF-8?q?feat(B05):=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=EA=B0=88=EB=9E=98=20=EB=8B=A4=EC=8B=9C=20=E2=80=94=20=EC=98=81?= =?UTF-8?q?=EC=84=A0=20=C2=B7=20=EC=95=88=EC=A0=84=20=EC=9A=B0=EC=84=A0=20?= =?UTF-8?q?=C2=B7=20=EA=B3=84=EB=A5=98=20=ED=9A=A1=EB=8B=A8=20=EC=B5=9C?= =?UTF-8?q?=EC=86=8C=20=C2=B7=20=EA=B3=B5=EC=82=AC=EB=B9=84=20=EC=96=B4?= =?UTF-8?q?=EB=A6=BC=20=EC=B5=9C=EC=86=8C(PLAN=2023-5a)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 갈래 short · balance 뺌(요청 422) · gradeline · safe · crossings · cost 더함 — 각 목표로 제약 탐색한 후보를 묶음에 넣고 제 비교값으로 고름 - 탐색 목표 셋 더함: 목표 기울기 편차(영선 · 등경사선) · 위험(옆경사 35° 이상 · 계류 버퍼 안 통과 길이) · 계류 횡단 링크 · 공사비는 상대 단가 그대로 - 비교값 더함: side_slope_avg_pct · side_slope_max_pct · hazard_m · stream_crossings · stream_offset_avg_m · grade_dev_pct · cost_index - 공사비 상대 단가(절성토 1 ㎥ = 1 · 연장 1 m = 2 · 계류 횡단 1 곳 = 400)는 임시 상대값 · 브레인 임의 판정 - 요청 criteria.target_grade_pct(영선 목표 기울기 · 없으면 상한) · 응답 criteria.target_grade_pct - 시험 test_route_initial 31 · 왕복 시험은 화면이 아는 갈래만(새 넷은 23-5b 뒤) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019ACSGaHdLgnkEkoA4LDtMU --- .../B05_Profile_Engine_RouteInitial.py | 29 +++-- ...B05_Profile_Engine_RouteInitial_Metrics.py | 62 ++++++++++- .../B05_Profile_Engine_RouteInitial_Pool.py | 76 ++++++++----- .../B05_Profile_Engine_RouteInitial_Search.py | 33 ++++-- B05_Profile/B05_Profile_Router_Replan.py | 12 ++- .../B05_Profile_Router_RouteInitial.py | 2 + .../test_b05_routeedit_initial_roundtrip.py | 5 +- resources/tester/test_route_initial.py | 100 ++++++++++++++---- 8 files changed, 246 insertions(+), 73 deletions(-) diff --git a/B05_Profile/B05_Profile_Engine_RouteInitial.py b/B05_Profile/B05_Profile_Engine_RouteInitial.py index 6bdd8fa2..58cda112 100644 --- a/B05_Profile/B05_Profile_Engine_RouteInitial.py +++ b/B05_Profile/B05_Profile_Engine_RouteInitial.py @@ -1,6 +1,7 @@ """초기 계획노선 갈래(PLAN 20 · 23장). -예상노선 추종 · 토공 최소 · 기울기 순한 · 비중 · 짧은 노선 · 흙 균형. +예상노선 추종 · 영선 노선 · 토공 최소 · 기울기 순한 · 안전 우선 · 계류 횡단 최소 · +공사비 어림 최소 · 비중. 계산 자리 = 서버 단독(③). 결과는 화면 캐시용 — 이 모듈은 파일을 쓰지 않는다. 적용은 화면이 받은 노드로 기존 `POST /route/replan`([확인])을 부른다. @@ -10,8 +11,12 @@ 제약 탐색은 하지 않고, 넘은 곳은 `metrics.violations` 에 뜬다. · earthwork — 후보 중 절 · 성토 합(`cut_m3 + fill_m3`) 최소. · gentle — 후보 중 최대 종단기울기 최소(같으면 평균 최소). - · short — 후보 중 연장 최소(PLAN 23-4c). - · balance — 후보 중 흙 남음 |절토 − 성토| 최소 · 같으면 절성토 합 최소(PLAN 23-4c). + · gradeline — 영선 노선: 목표 기울기(`target_grade` · 없으면 상한)로 등고선을 건너가는 + 등경사선(PEGGER 식) · 후보 중 목표 기울기 편차(`grade_dev_pct`) 최소(PLAN 23-5). + · safe — 안전 우선: 후보 중 위험 통과 길이(옆경사 35° 이상 · 계류 버퍼 안) 최소. + · crossings — 계류 횡단 최소: 후보 중 계류 횡단 수 최소 · 같으면 계류에서 먼 쪽. + · cost — 공사비 어림 최소: 후보 중 상대 공사비(토공 + 계류 횡단 구조물 + 연장 · 임시 상대 + 단가 `..._Metrics.COST_*`) 최소. · weighted — 탐색 비용의 가중합 한 번(후보 고르기 아님 · PLAN 23-4a · `..._Pool.weighted`). 끝값은 그 갈래와 같은 선 — 예상 100 = 추적 · 토공 100 = 토공 최소 · 기울기 100 = 기울기 순한. 제약은 (1 − 예상 몫)만큼 벌점으로만 따지고 넘은 곳은 목록에 뜸. @@ -83,15 +88,17 @@ STREAM_CLASSES = ("소하천", "지방하천", "국가하천") PENDING: tuple[str, ...] = () # ── 탐색 · 측정 값 ─────────────────────────────────────────────────────────────── -MODES = ("follow", "earthwork", "gentle", "weighted", "short", "balance") +MODES = ("follow", "gradeline", "earthwork", "gentle", "safe", "crossings", "cost", "weighted") DEFAULT_WEIGHTS = {"expected": 50.0, "earthwork": 25.0, "grade": 25.0} MODE_WEIGHTS = { "follow": {"expected": 100.0, "earthwork": 0.0, "grade": 0.0}, "earthwork": {"expected": 0.0, "earthwork": 100.0, "grade": 0.0}, "gentle": {"expected": 0.0, "earthwork": 0.0, "grade": 100.0}, - # 비중 몫이 없는 갈래 — 연장 · 흙 균형은 비교값으로만 고름 - "short": {"expected": 0.0, "earthwork": 0.0, "grade": 0.0}, - "balance": {"expected": 0.0, "earthwork": 0.0, "grade": 0.0}, + # 비중 몫이 없는 갈래(PLAN 23-5) — 제 목표로 찾은 후보에서 비교값으로 고름 + **{ + mode: {"expected": 0.0, "earthwork": 0.0, "grade": 0.0} + for mode in ("gradeline", "safe", "crossings", "cost") + }, } # 후보 묶음은 갈래와 무관 — 같은 입력이면 다시 짜지 않음(최근 몇 벌만 · 프로세스 메모리) POOL_CACHE_SIZE = 4 @@ -208,6 +215,7 @@ def generate_initial_route( min_radius_m: float, stream_offset_m: float | None = None, grade_smooth_m: float | None = None, + target_grade: float | None = None, ) -> dict[str, Any]: """갈래 하나로 초기 계획노선을 만들고 비교값을 붙여 돌려준다. `max_grade` 는 비율(0.14). @@ -222,6 +230,8 @@ def generate_initial_route( used = resolve_weights(mode, weights) stream_offset = STREAM_OFFSET_M if stream_offset_m is None else float(stream_offset_m) grade_smooth = GRADE_SMOOTH_M if grade_smooth_m is None else float(grade_smooth_m) + # 영선 노선 목표 기울기(비율) — 없으면 상한 · 상한 넘게 넣어도 상한(제약 안에서만) + goal_grade = max_grade if target_grade is None else min(float(target_grade), max_grade) def measure( outline: Any, *, bound: float | None, follow: bool = False, weighted: bool = False @@ -240,6 +250,7 @@ def generate_initial_route( grade_smooth_m=grade_smooth, follow=follow, weighted=weighted, + target_grade=goal_grade, ) def follow_line() -> tuple[Any, dict[str, Any]]: @@ -252,7 +263,7 @@ def generate_initial_route( pool: Pool | None = None target = _weighted_end(used) if mode == "weighted" else mode key = _pool_key( - expected, terrain, streams, max_grade, min_radius_m, stream_offset, grade_smooth + expected, terrain, streams, max_grade, min_radius_m, stream_offset, grade_smooth, goal_grade ) if target == "follow": # 비중 예상 100 = 추적 그 선(PLAN 23-3e) · 묶음은 짜지 않음(빠른 길) outline, metrics = follow_line() @@ -265,6 +276,7 @@ def generate_initial_route( streams, stream_offset_m=stream_offset, grade_smooth_m=grade_smooth, + target_grade=goal_grade, ) graph = RouteGraph(grid, expected, limit=max_grade, min_radius_m=min_radius_m) while len(_POOLS) >= POOL_CACHE_SIZE: @@ -297,6 +309,7 @@ def generate_initial_route( "road_width_m": ROAD_WIDTH_M, "corridor_m": CORRIDOR_M, "grade_smooth_m": round(grade_smooth, 2), + "target_grade_pct": round(goal_grade * 100.0, 2), # 기준값 확정(PLAN 20장, 2026-09-29) — 더는 후보가 아니다. 항상 빈 목록. "pending": list(PENDING), }, diff --git a/B05_Profile/B05_Profile_Engine_RouteInitial_Metrics.py b/B05_Profile/B05_Profile_Engine_RouteInitial_Metrics.py index 0b3196fe..9c74a811 100644 --- a/B05_Profile/B05_Profile_Engine_RouteInitial_Metrics.py +++ b/B05_Profile/B05_Profile_Engine_RouteInitial_Metrics.py @@ -25,6 +25,17 @@ SAMPLE_M = 2.0 # 측정 간격(m) GRADE_SMOOTH_M = 5.0 STATION_M = 20.0 # 종단기울기 측정 간격(m) — 측점 체계 · 타당성평가 20 m 간격 GRADE_EPS = 1e-6 # 상한과 같은 기울기는 지킨 것으로 봄 +# 안전 우선 — 옆경사가 이보다 급한 곳을 위험으로 봄(35° = 70 % · 지식DB 「35° 이상 전폭 절토」 +# `산림과임업기술(임도)/2. 임도/4. 노선측량.md`) +SAFE_SIDE_SLOPE = math.tan(math.radians(35.0)) +# 공사비 어림 상대 단가(절성토 1 ㎥ = 1) — ⚠ 임시 상대값 · 브레인 임의 판정(PLAN 23-5 · 지식DB +# 근거가 붙으면 바꿈). 연장 1 m = 벌개 · 노반 · 측구 등 절성토 밖 공종을 흙 2 ㎥ 로 어림(이 현장 +# 절성토가 1 m 당 3~4 ㎥ 라 두 몫이 같은 자릿수) · 계류 횡단 1 곳 = 횡단 배수 구조물(관 · +# 기슭막이) 400 ㎥. +COST_EARTH_M3 = 1.0 +COST_LENGTH_M = 2.0 +COST_CROSSING = 400.0 +CROSSING_MERGE_M = 10.0 # 이 거리 안의 계류 교점은 한 횡단(갈라진 도엽 선 이음매) REASON_TERRAIN = "지형상 불가능 — 복도 안 어떤 길도 {bound:.1f}% 이상 기울기를 지나야 이어짐" REASON_GRADE_FIT = "곡선 맞춤 뒤 지반 차 — 측점 사이 지반이 상한을 넘음" @@ -116,18 +127,19 @@ def route_metrics( grade_smooth_m: float = GRADE_SMOOTH_M, follow: bool = False, weighted: bool = False, + target_grade: float | None = None, ) -> dict[str, Any]: """비교값과 제약 못 지킨 구간 목록. 종단기울기 = 측점(20 m) 사이 고른 지반 기울기. `bound_pct` 는 제약을 다 지키는 길이 없을 때만 — 복도 안 어떤 길도 넘어야 하는 기울기(%). `follow` 는 예상노선 추적(제약 탐색 없이 예상노선 그대로) · `weighted` 는 비중 계산(제약을 - 벌점으로만 따짐) — 사유가 그 뜻으로 뜬다. + 벌점으로만 따짐) — 사유가 그 뜻으로 뜬다. `target_grade` = 영선 노선 목표 기울기(비율 · 없으면 + 상한) — 기울기 편차(`grade_dev_pct`)의 기준. """ grid = (terrain.y, terrain.x) sampler = RegularGridInterpolator(grid, terrain.z, bounds_error=False, fill_value=None) - smooth = RegularGridInterpolator( - grid, smoothed_ground(terrain, grade_smooth_m), bounds_error=False, fill_value=None - ) + smoothed = smoothed_ground(terrain, grade_smooth_m) + smooth = RegularGridInterpolator(grid, smoothed, bounds_error=False, fill_value=None) points, chain = _resample(vertices, SAMPLE_M) length = float(chain[-1]) lookup = points[:, ::-1] @@ -181,8 +193,12 @@ def route_metrics( # 계류 이격 — 버퍼 안 구간 중 봐줄 길이를 넘는 것. 봐줌 = 건너기(계류에 닿음) 한 번의 # 버퍼 안 길이(45° 로 가로지름) + 기점 · 종점이 버퍼 안이면 45° 로 빠져나가는 길이 route_points = shapely.points(points) + near = np.full(len(points), np.inf) + crossings = 0 if streams: - near = shapely.distance(route_points, MultiLineString(streams)) + network = MultiLineString(streams) + near = shapely.distance(route_points, network) + crossings = _crossings(LineString(vertices), network) for first, last in _runs(near < stream_offset_m): ends_in = int(first == 0) + int(last == len(near) - 1) crosses = float(near[first : last + 1].min()) <= SAMPLE_M @@ -223,6 +239,17 @@ def route_metrics( cut = float((np.maximum(depth, 0.0).mean(axis=1) * road_width_m * weight).sum()) fill = float((np.maximum(-depth, 0.0).mean(axis=1) * road_width_m * weight).sum()) + # 옆경사(고른 지반 · 노선에 수직) · 위험 통과 길이(옆경사 35° 이상 또는 계류 버퍼 안) + sy, sx = np.gradient(smoothed, _spacing(terrain.y), _spacing(terrain.x)) + side = np.abs( + RegularGridInterpolator(grid, sx, bounds_error=False, fill_value=None)(lookup) + * -tangent[:, 1] + + RegularGridInterpolator(grid, sy, bounds_error=False, fill_value=None)(lookup) + * tangent[:, 0] + ) + hazard = float(weight[(side >= SAFE_SIDE_SLOPE) | (near < stream_offset_m)].sum()) + target = max_grade if target_grade is None else target_grade + deviation = np.abs(grades - target) offsets = shapely.distance(route_points, LineString(expected)) counts = { kind: sum(1 for v in violations if v["kind"] == kind) @@ -245,9 +272,34 @@ def route_metrics( "over_limit": {**counts, "total": len(violations)}, "offset_avg_m": round(float(offsets.mean()), 2), "offset_max_m": round(float(offsets.max()), 2), + # PLAN 23-5 — 옆경사 · 위험 통과 길이(안전 우선) · 계류 횡단(계류 횡단 최소) · 계류 평균 + # 거리(계류 없으면 None) · 목표 기울기 편차(영선 노선 · 측점 기울기와 목표의 차 평균) · + # 공사비 어림 + "side_slope_avg_pct": round(float((side * weight).sum() / max(length, 1e-9)) * 100.0, 2), + "side_slope_max_pct": round(float(side.max()) * 100.0, 2), + "hazard_m": round(hazard, 1), + "stream_crossings": crossings, + "stream_offset_avg_m": round(float(near.mean()), 2) if streams else None, + "grade_dev_pct": round( + float((deviation * spans[keep]).sum() / spans[keep].sum()) * 100.0 + if grades.size + else 0.0, + 2, + ), + "cost_index": round( + COST_EARTH_M3 * (cut + fill) + COST_LENGTH_M * length + COST_CROSSING * crossings + ), "violations": violations, } +def _crossings(route: LineString, network: MultiLineString) -> int: + """노선이 계류를 건너는 횟수 — 교점을 노선 따라 세고 `CROSSING_MERGE_M` 안은 한 번.""" + hit = route.intersection(network) + points = [geom for geom in getattr(hit, "geoms", [hit]) if not geom.is_empty] + at = sorted(route.project(point.centroid) for point in points) + return sum(1 for i, value in enumerate(at) if i == 0 or value - at[i - 1] > CROSSING_MERGE_M) + + def _spacing(coords: np.ndarray) -> float: return float((coords[-1] - coords[0]) / (len(coords) - 1)) if len(coords) > 1 else 1.0 diff --git a/B05_Profile/B05_Profile_Engine_RouteInitial_Pool.py b/B05_Profile/B05_Profile_Engine_RouteInitial_Pool.py index 874fbad7..9c4fdcea 100644 --- a/B05_Profile/B05_Profile_Engine_RouteInitial_Pool.py +++ b/B05_Profile/B05_Profile_Engine_RouteInitial_Pool.py @@ -3,15 +3,17 @@ `B05_Profile_Engine_RouteInitial` 이 부름. 후보 묶음(`build_pool`) = 같은 입력(지형 · 예상노선 · 계류 · 제약)이면 한 번만 짠다: - · 제약 탐색 후보 — 목표 하나씩(토공 · 기울기 · 이격 · 연장) × 탐색 상한 단계(지킬 수 있을 + · 제약 탐색 후보 — 목표 하나씩(`TARGETS` · 토공 · 기울기 · 이격 · 목표 기울기 편차 · 위험 · + 계류 횡단 · 공사비) × 탐색 상한 단계(지킬 수 있을 때) 또는 벌점 크기(지형상 불가능할 때 · 막은 기울기 안) — PLAN 23-1 그대로. · 토공 ↔ 기울기 쓸기 — 비중 계산(예상 0)을 토공 100 → 기울기 100 사이 `SWEEP_STEPS` 벌. 갈래가 그 사이 어느 비중보다 제 목표에서 좋게(토공 최소 ≤ 쓸기 절성토 · 기울기 순한 ≤ 쓸기 최대 기울기). · 못 지킨 곳 수가 기준(엄격: 후보 최소 · 풂: 센 벌점 기준 길)보다 많은 후보는 뺀다. -갈래는 **비교값으로** 고른다(`pick`) — 토공 최소 = 절성토 합 · 기울기 순한 = 최대(같으면 평균) · -짧은 노선 = 연장 · 흙 균형 = |절토 − 성토|(같으면 절성토 합). +갈래는 **비교값으로** 고른다(`pick` · `GOALS`) — 토공 최소 = 절성토 합 · 기울기 순한 = 최대(같으면 +평균) · 영선 노선 = 목표 기울기 편차 · 안전 우선 = 위험 통과 길이(옆경사 35° 이상 · 계류 버퍼 안) · +계류 횡단 최소 = 횡단 수(같으면 계류에서 먼 쪽) · 공사비 어림 최소 = 상대 공사비(PLAN 23-5). 비중 계산(`weighted`)은 고르기가 아니라 **탐색 비용의 가중합** 한 번(PLAN 23-4a): · 몫 = 예상노선에서 벗어난 거리 · 토공 가늠 · 기울기 — 각 몫을 끝 노선 셋(추적 · 토공 최소 · @@ -29,15 +31,29 @@ from typing import Any import numpy as np from shapely.geometry import LineString -from B05_Profile.B05_Profile_Engine_RouteInitial_Search import LENGTH_COST, NoRoute, RouteGraph +from B05_Profile.B05_Profile_Engine_RouteInitial_Metrics import ( + COST_CROSSING, + COST_EARTH_M3, + COST_LENGTH_M, +) +from B05_Profile.B05_Profile_Engine_RouteInitial_Search import ( + CROSSING_BAND, + OBJECTIVES, + NoRoute, + RouteGraph, +) from common_util.common_util_route_polyline import build_planned_polyline ZERO = {"expected": 0.0, "earthwork": 0.0, "grade": 0.0} -TARGETS = { +# 갈래 후보를 찾는 탐색 목표(PLAN 23-1 · 23-5) — 공사비는 상대 단가 그대로(`_solve`) +TARGETS: dict[str, dict[str, float]] = { "earthwork": {**ZERO, "earthwork": 100.0}, "gentle": {**ZERO, "grade": 100.0}, "follow": {**ZERO, "expected": 100.0}, - "short": dict(ZERO), # 목표 몫 없음 — 거리 비용만(`SHORT_LENGTH_COST`) + "gradeline": {"gradeline": 100.0}, + "safe": {"risk": 100.0}, + "crossings": {"crossing": 100.0}, + "cost": {}, } # 탐색 상한 = 상한 × 이 값들 차례로 — 곡선 맞춤 뒤 측점 사이 지반이 넘으면 다음 값으로 다시 찾음 GRADE_CAP_STEPS = (1.0, 0.93, 0.85) @@ -45,8 +61,6 @@ GRADE_CAP_STEPS = (1.0, 0.93, 0.85) # 넘긴 길)은 센 값, 갈래 후보는 목표와 겨룰 만한 값들(목표는 1 m 당 평균 1 로 정규화됨) SOFT_GRADE_COST = 500.0 RELAXED_GRADE_COSTS = (50.0, 5.0) -# 짧은 노선 탐색의 1 m 당 거리 비용 — 꺾기 벌점(원호 길이)과 같은 단위라 「연장 최소」가 됨 -SHORT_LENGTH_COST = 1.0 # 토공 100 → 기울기 100 쓸기 벌 수(양 끝 포함 · 10 % 걸음) SWEEP_STEPS = 11 # 끝 노선 사이 범위가 링크 평균의 이 몫보다 작으면(끝 노선끼리 같음) 링크 평균으로 크기를 맞춤 @@ -99,8 +113,21 @@ def _trace( return traced[key] -def _length_cost(weights: dict[str, float]) -> float: - return LENGTH_COST if any(weights.values()) else SHORT_LENGTH_COST +def _solve(graph: RouteGraph, target: str, **constraint: Any) -> list[tuple[float, float]]: + """갈래 후보 탐색 한 번. 공사비 = 토공 가늠 · 계류 횡단 · 연장을 상대 단가 그대로 더한 비용 + (링크 평균이 1 쯤 되게 한 값으로 나눔 — 제약 벌점과 겨루는 크기를 다른 목표와 맞춤).""" + if target != "cost": + return graph.solve(TARGETS[target], **constraint) + crossing = COST_CROSSING / (2.0 * CROSSING_BAND * graph.grid.cell) # 1 m 당(횡단 한 곳 몫) + per_m = COST_EARTH_M3 * float(np.average(graph.terms[1], weights=graph.length)) + COST_LENGTH_M + weights = {"earthwork": COST_EARTH_M3 / per_m, "crossing": crossing / per_m} + total = sum(weights.values()) + return graph.solve( + weights, + scale=np.full(len(OBJECTIVES), 1.0 / total), + length_cost=COST_LENGTH_M / per_m, + **constraint, + ) def _candidates( @@ -116,15 +143,10 @@ def _candidates( 없으면 막은 기울기 안에서 벌점 크기 · 목표를 바꿔 찾은 노선들. """ found: list[Route] = [] - for weights in TARGETS.values(): + for target in TARGETS: for step in GRADE_CAP_STEPS: try: - nodes = graph.solve( - weights, - cap=max_grade * step, - strict_stream=True, - length_cost=_length_cost(weights), - ) + nodes = _solve(graph, target, cap=max_grade * step, strict_stream=True) except NoRoute: break found.append(_trace(nodes, min_radius_m, measure, traced, bound=None)) @@ -153,15 +175,9 @@ def _candidates( bound=bound, ) found = [seed] - for weights in TARGETS.values(): + for target in TARGETS: for cost in RELAXED_GRADE_COSTS: - nodes = graph.solve( - weights, - cap=cap, - strict_stream=False, - grade_cost=cost, - length_cost=_length_cost(weights), - ) + nodes = _solve(graph, target, cap=cap, strict_stream=False, grade_cost=cost) found.append(_trace(nodes, min_radius_m, measure, traced, bound=bound)) most = seed[1]["over_limit"]["total"] return [item for item in found if item[1]["over_limit"]["total"] <= most], bound, most @@ -224,7 +240,7 @@ def _scale( values = np.array([value for value, _ in measured]) reference = max(float(np.hypot(*np.diff(np.asarray(lines[0]), axis=0).T).sum()), 1.0) spread = (values.max(axis=0) - values.min(axis=0)) / reference - per_link = np.vstack([graph.terms, graph.over / graph.limit, graph.along]) + per_link = np.vstack([graph.terms[:3], graph.over / graph.limit, graph.along]) mean = (per_link * graph.length).sum(axis=1) / max(float(graph.length.sum()), 1e-9) return np.where(spread > MIN_RANGE_SHARE * mean, spread, mean), measured[0][1] @@ -241,7 +257,7 @@ def weighted(pool: Pool, weights: dict[str, float], min_radius_m: float, measure strict_stream=False, grade_cost=soft / max(pool.scale[3], 1e-12), stream_cost=soft / max(pool.scale[4], 1e-12), - scale=pool.scale[:3], + scale=np.concatenate([pool.scale[:3], np.ones(len(OBJECTIVES) - 3)]), ) pool.searched[key] = _trace( nodes, min_radius_m, measure, pool.traced, bound=pool.bound, weighted=True @@ -256,8 +272,10 @@ def _earth(metrics: dict[str, Any]) -> float: GOALS: dict[str, Callable[[dict[str, Any]], tuple[float, ...]]] = { "earthwork": lambda m: (_earth(m),), "gentle": lambda m: (m["max_grade_pct"], m["avg_grade_pct"]), - "short": lambda m: (m["length_m"], _earth(m)), - "balance": lambda m: (abs(m["cut_m3"] - m["fill_m3"]), _earth(m)), + "gradeline": lambda m: (m["grade_dev_pct"], m["max_grade_pct"]), + "safe": lambda m: (m["hazard_m"], m["side_slope_avg_pct"]), + "crossings": lambda m: (m["stream_crossings"], -(m["stream_offset_avg_m"] or 0.0)), + "cost": lambda m: (m["cost_index"],), } diff --git a/B05_Profile/B05_Profile_Engine_RouteInitial_Search.py b/B05_Profile/B05_Profile_Engine_RouteInitial_Search.py index 7680ed37..cb97542c 100644 --- a/B05_Profile/B05_Profile_Engine_RouteInitial_Search.py +++ b/B05_Profile/B05_Profile_Engine_RouteInitial_Search.py @@ -19,7 +19,11 @@ from scipy.interpolate import RegularGridInterpolator from scipy.sparse import csr_matrix from scipy.sparse.csgraph import breadth_first_order, connected_components, dijkstra -from B05_Profile.B05_Profile_Engine_RouteInitial_Metrics import GRADE_SMOOTH_M, smoothed_ground +from B05_Profile.B05_Profile_Engine_RouteInitial_Metrics import ( + GRADE_SMOOTH_M, + SAFE_SIDE_SLOPE, + smoothed_ground, +) # 토공 가늠 폭(m) = 유효너비 3 + 길어깨 0.5 × 2(별표2 간선). ROAD_WIDTH_M = 4.0 @@ -31,7 +35,10 @@ TURN_COST = 1.0 # 꺾을 때 벌점 = 최소 곡선반지름 원호 길이(R· SOFT_STREAM_COST = 20.0 # 계류 버퍼 안 나란히 달리기(지형상 불가능할 때만 · 1 m 당) BOUND_BISECT = 7 # 지형상 불가능할 때 막는 기울기를 찾는 이분 탐색 횟수(구간 1/128 까지) STREAM_CROSS_COS = math.cos(math.radians(45.0)) # 버퍼 안 이동 방향 한도(계류 쪽 · 반대쪽 ±45°) -OBJECTIVES = ("expected", "earthwork", "grade") +# 탐색 목표(1 m 당) — 이격 · 토공 가늠 · 기울기 · 목표 기울기 편차(영선) · 위험(급한 옆경사 · 계류 +# 가까움) · 계류 횡단(계류 선 위 링크) — PLAN 23-5 +OBJECTIVES = ("expected", "earthwork", "grade", "gradeline", "risk", "crossing") +CROSSING_BAND = 0.75 # 계류 횡단 링크 = 링크 가운데가 계류에서 칸 × 이 값 안 # 16 방향(행, 열) — +x 에서 반시계. 나이트 걸음(1, 2) 등을 넣어 22.5° 안팎 방향을 곧게 달림. DIRECTIONS = ( (0, 1), @@ -101,8 +108,10 @@ class SearchGrid: *, stream_offset_m: float, grade_smooth_m: float = GRADE_SMOOTH_M, + target_grade: float | None = None, ) -> None: step = max(1, int(round(SEARCH_CELL_M / _cell_size(terrain.x)))) + self.target_grade = target_grade # 영선 노선 목표 기울기(비율) · 없으면 링크 상한 self.xs, self.ys = terrain.x[::step], terrain.y[::step] # 기울기 · 옆경사는 비교값과 같은 고른 지반에서 잰다(`smoothed_ground`) self.z = smoothed_ground(terrain, grade_smooth_m)[::step, ::step] @@ -154,7 +163,17 @@ class SearchGrid: # 반 간격(10 m) 동안 쌓는 높이 차 단면(지형상 불가능할 때만 0 이 아님) earth = ROAD_WIDTH_M**2 * cross / 4.0 + ROAD_WIDTH_M * over * 10.0 offset = 0.5 * (self.d_expected[first][ok] + self.d_expected[second][ok]) - terms = np.stack([offset, earth, (grade / limit) ** 2]) + target = limit if self.target_grade is None else self.target_grade + stream_mid = 0.5 * (self.d_stream[first][ok] + self.d_stream[second][ok]) + # 위험 = 비교값(`hazard_m`)과 같은 뜻 — 옆경사 35° 이상 · 계류 버퍼 안을 지나는 길이(칸 + # 두 끝 가운데 급한 쪽) · 옆경사 몫을 조금 더해 같은 길이면 덜 급한 쪽 + risk = ((cross >= SAFE_SIDE_SLOPE) | (stream_mid < self.stream_offset_m)).astype( + np.float64 + ) + 0.1 * (cross / SAFE_SIDE_SLOPE) ** 2 + crossing = (stream_mid < CROSSING_BAND * self.cell).astype(np.float64) + terms = np.stack( + [offset, earth, (grade / limit) ** 2, ((grade - target) / limit) ** 2, risk, crossing] + ) return a[ok], b[ok], self.cell * norm, terms, over, along, grade def objectives( @@ -328,8 +347,9 @@ class RouteGraph: ) -> list[tuple[float, float]]: """노드(꺾임점) 목록 — 첫 · 끝은 예상노선 양 끝 그대로. - 세 목표는 `scale`(1 m 당 크기)로 나눈 뒤 비중을 곱한다 — 안 주면 쓸 수 있는 링크 평균이 - 1 이 되게(길이 가중). `grade_cost` = 상한을 넘은 기울기 벌점(1 m 당 × 넘은 비율 / 상한) · + 목표(`OBJECTIVES`)는 `scale`(1 m 당 크기)로 나눈 뒤 비중을 곱한다 — 안 주면 쓸 수 있는 + 링크 평균이 1 이 되게(길이 가중). `grade_cost` = 상한을 넘은 기울기 벌점(1 m 당 × 넘은 + 비율 / 상한) · `stream_cost` = 계류 버퍼 안 나란히 벌점(엄격하지 않을 때만) · `length_cost` = 1 m 당 거리 비용. """ @@ -339,7 +359,8 @@ class RouteGraph: if scale is None: length = self.length[usable] scale = (self.terms[:, usable] * length).sum(axis=1) / max(length.sum(), 1e-9) - share = np.array([weights[key] for key in OBJECTIVES]) / max(sum(weights.values()), 1e-9) + share = np.array([weights.get(key, 0.0) for key in OBJECTIVES]) + share = share / max(sum(weights.values()), 1e-9) factor = np.divide(share, scale, out=np.zeros_like(share), where=scale > 1e-12) penalty = grade_cost * self.over / self.limit if not strict_stream: diff --git a/B05_Profile/B05_Profile_Router_Replan.py b/B05_Profile/B05_Profile_Router_Replan.py index a3b76d94..569e6286 100644 --- a/B05_Profile/B05_Profile_Router_Replan.py +++ b/B05_Profile/B05_Profile_Router_Replan.py @@ -105,15 +105,17 @@ class RouteInitialCriteria(BaseModel): min_radius_m: float | None = Field(None, ge=0) stream_offset_m: float | None = Field(None, ge=0) grade_smooth_m: float | None = Field(None, ge=0) + # 영선 노선 목표 종단기울기(%) — 비우면 종단기울기 상한 · 상한보다 크면 상한(PLAN 23-5) + target_grade_pct: float | None = Field(None, ge=0, le=100) class RouteInitialRequest(BaseModel): - """초기 계획노선 갈래 — follow(예상노선 추종) · earthwork · gentle · weighted · - short(연장 최소) · balance(흙 균형 · PLAN 23-4c).""" + """초기 계획노선 갈래 — follow(예상노선 추종) · gradeline(영선) · earthwork · gentle · + safe(안전 우선) · crossings(계류 횡단 최소) · cost(공사비 어림 최소) · weighted(PLAN 23-5).""" - mode: Literal["follow", "earthwork", "gentle", "weighted", "short", "balance"] = Field( - "follow", validation_alias=AliasChoices("logic", "mode") - ) + mode: Literal[ + "follow", "gradeline", "earthwork", "gentle", "safe", "crossings", "cost", "weighted" + ] = Field("follow", validation_alias=AliasChoices("logic", "mode")) weights: RouteInitialWeights | None = None criteria: RouteInitialCriteria | None = None diff --git a/B05_Profile/B05_Profile_Router_RouteInitial.py b/B05_Profile/B05_Profile_Router_RouteInitial.py index b0639479..2dcc75f8 100644 --- a/B05_Profile/B05_Profile_Router_RouteInitial.py +++ b/B05_Profile/B05_Profile_Router_RouteInitial.py @@ -70,6 +70,7 @@ async def initial_route( radius_m = criteria.min_radius_m stream_offset_m = criteria.stream_offset_m if criteria else None grade_smooth_m = criteria.grade_smooth_m if criteria else None + target_pct = criteria.target_grade_pct if criteria else None pool = get_db_pool() async with pool.acquire() as connection: selection = await get_surface_confirmation_params(connection, str(project_id)) @@ -90,6 +91,7 @@ async def initial_route( min_radius_m=radius_m, stream_offset_m=stream_offset_m, grade_smooth_m=grade_smooth_m, + target_grade=None if target_pct is None else target_pct / 100.0, ) ) except (FileNotFoundError, ValueError) as error: diff --git a/resources/tester/test_b05_routeedit_initial_roundtrip.py b/resources/tester/test_b05_routeedit_initial_roundtrip.py index 14d7d9a0..0b77115d 100644 --- a/resources/tester/test_b05_routeedit_initial_roundtrip.py +++ b/resources/tester/test_b05_routeedit_initial_roundtrip.py @@ -43,8 +43,9 @@ LOGICS: list[tuple[str, dict[str, float] | None]] = [ ("follow", None), ("earthwork", None), ("gentle", None), - ("short", None), - ("balance", None), + # PLAN 23-5 — 서버는 짧은 노선 · 흙 균형을 빼고 넷(gradeline · safe · crossings · cost)을 + # 더함. 새 넷은 화면(23-5b)이 알약으로 알게 되면 여기 더함 — 지금 화면은 다시 열 때 모르는 + # 갈래를 추적으로 읽음. 서버 쪽 저장 · 다시 읽기는 `test_route_initial` 이 지킴. ("weighted", {"expected": 50, "earthwork": 25, "grade": 25}), ] diff --git a/resources/tester/test_route_initial.py b/resources/tester/test_route_initial.py index 2f684022..edac8d33 100644 --- a/resources/tester/test_route_initial.py +++ b/resources/tester/test_route_initial.py @@ -25,12 +25,19 @@ from B05_Profile.B05_Profile_Engine_RouteInitial import ( stream_lines, write_initial_choice, ) -from B05_Profile.B05_Profile_Engine_RouteInitial_Metrics import grade_line, station_label +from B05_Profile.B05_Profile_Engine_RouteInitial_Metrics import ( + COST_CROSSING, + COST_EARTH_M3, + COST_LENGTH_M, + grade_line, + station_label, +) +from B05_Profile.B05_Profile_Engine_RouteInitial_Pool import GOALS from common_util.common_util_route_polyline import build_planned_polyline SEARCHED = [mode for mode in MODES if mode != "follow"] # 제약을 지키는 갈래(후보 묶음에서 고름) — 비중은 제약을 벌점으로만(PLAN 23-4a) -BRANCHES = ["earthwork", "gentle", "short", "balance"] +BRANCHES = ["gradeline", "earthwork", "gentle", "safe", "crossings", "cost"] GRADE = 0.14 RADIUS = 12.0 @@ -108,19 +115,18 @@ def _earth(result: dict) -> float: def _check_branch_goals(runs: dict) -> None: - """토공 최소 = 절성토 · 기울기 순한 = 최대 기울기 · 짧은 노선 = 연장 · 흙 균형 = |절토 − 성토| - 가 갈래(같은 후보 묶음) 가운데 가장 좋음 · 흙 남음 = 절토 − 성토.""" - branches = [runs[mode] for mode in BRANCHES] - assert all(_earth(runs["earthwork"]) <= _earth(r) + 1e-6 for r in branches) - top = runs["gentle"]["metrics"]["max_grade_pct"] - assert all(top <= r["metrics"]["max_grade_pct"] + 1e-6 for r in branches) - shortest = runs["short"]["metrics"]["length_m"] - assert all(shortest <= r["metrics"]["length_m"] + 1e-6 for r in branches) - surplus = abs(runs["balance"]["metrics"]["surplus_m3"]) - assert all(surplus <= abs(r["metrics"]["surplus_m3"]) + 0.1 for r in branches) + """갈래마다 제 목표 비교값(`GOALS` — 절성토 · 최대 기울기 · 목표 기울기 편차 · 위험 길이 · + 계류 횡단 · 공사비)이 갈래(같은 후보 묶음) 가운데 1 등 · 새 비교값이 서로 맞음(PLAN 23-5).""" + branches = [runs[mode]["metrics"] for mode in BRANCHES] + for mode, goal in GOALS.items(): + assert all(goal(runs[mode]["metrics"]) <= goal(m) for m in branches), mode for result in runs.values(): - metrics = result["metrics"] - assert abs(metrics["surplus_m3"] - (metrics["cut_m3"] - metrics["fill_m3"])) <= 0.11 + m = result["metrics"] + assert abs(m["surplus_m3"] - (m["cut_m3"] - m["fill_m3"])) <= 0.11 + assert 0.0 <= m["hazard_m"] <= m["length_m"] + 1e-6 + assert m["side_slope_avg_pct"] <= m["side_slope_max_pct"] + 1e-6 + cost = COST_EARTH_M3 * _earth(result) + COST_LENGTH_M * m["length_m"] + assert abs(m["cost_index"] - cost - COST_CROSSING * m["stream_crossings"]) <= 1.0 def test_each_branch_best_at_own_goal(hill: Terrain) -> None: @@ -237,8 +243,17 @@ def test_grade_line_stays_within_limit() -> None: def test_weights_and_stream_filter() -> None: assert resolve_weights("weighted", None) == {"expected": 50.0, "earthwork": 25.0, "grade": 25.0} assert resolve_weights("gentle", {"expected": 99})["grade"] == 100.0 - assert set(MODES) == {"follow", "earthwork", "gentle", "weighted", "short", "balance"} - assert resolve_weights("short", {"expected": 99})["expected"] == 0.0 + assert set(MODES) == { + "follow", + "gradeline", + "earthwork", + "gentle", + "safe", + "crossings", + "cost", + "weighted", + } + assert resolve_weights("cost", {"expected": 99})["expected"] == 0.0 line = {"type": "LineString", "coordinates": [[0, 0], [1, 1]]} features = [ {"properties": {"구분": "세류"}, "geometry": line}, @@ -293,6 +308,10 @@ def test_initial_choice_roundtrip(tmp_path) -> None: "weights": {"expected": 100.0, "earthwork": 0.0, "grade": 0.0}, "criteria": None, } + for logic in ("gradeline", "safe", "crossings", "cost"): # 새 갈래도 적고 다시 읽음(23-5) + write_initial_choice(tmp_path, logic, None, {"target_grade_pct": 10.0}) + assert read_initial_choice(tmp_path)["logic"] == logic + assert read_initial_choice(tmp_path)["criteria"] == {"target_grade_pct": 10.0} write_initial_choice(tmp_path, "weighted", {"expected": 60, "earthwork": 20, "grade": 20}) assert read_initial_choice(tmp_path) == { "logic": "weighted", @@ -318,8 +337,13 @@ def test_request_accepts_logic_name() -> None: assert RouteInitialRequest.model_validate({"logic": "gentle"}).mode == "gentle" assert RouteInitialRequest.model_validate({"mode": "earthwork"}).mode == "earthwork" - assert RouteInitialRequest.model_validate({"logic": "short"}).mode == "short" - assert RouteInitialRequest.model_validate({"logic": "balance"}).mode == "balance" + for logic in ("gradeline", "safe", "crossings", "cost"): + assert RouteInitialRequest.model_validate({"logic": logic}).mode == logic + for gone in ("short", "balance"): # 23-5 에서 뺀 갈래 — 422 + with pytest.raises(ValueError): + RouteInitialRequest.model_validate({"logic": gone}) + criteria = RouteInitialRequest.model_validate({"criteria": {"target_grade_pct": 10}}).criteria + assert criteria is not None and criteria.target_grade_pct == 10 replan = RouteReplanRequest.model_validate({"vertices": [], "initial": {"logic": "weighted"}}) assert replan.initial is not None and replan.initial.mode == "weighted" @@ -446,3 +470,43 @@ def test_project_weight_ends_match_branches(project_runs: dict) -> None: def test_project_weight_moves_monotone(project_runs: dict) -> None: _check_weight_monotone(project_runs["run"]) + + +def test_stream_crossing_counted_once() -> None: + """평지 · 복도를 가로지르는 소하천 한 줄 — 어느 노선이든 한 번 건넘 · 계류 횡단 최소도 한 번.""" + flat = _terrain(lambda xx, yy: np.zeros_like(xx)) + crossing = [[(400.0, 0.0), (400.0, 400.0)]] + for mode in ("follow", "crossings", "cost"): + metrics = _run(flat, mode, streams=crossing)["metrics"] + assert metrics["stream_crossings"] == 1, mode + assert metrics["stream_offset_avg_m"] is not None + assert _run(flat, "follow")["metrics"]["stream_crossings"] == 0 + + +def test_gradeline_target_grade(hill: Terrain) -> None: + """영선 노선 — 목표 기울기(없으면 상한 · 상한보다 크면 상한).""" + default = _run(hill, "gradeline") + assert default["criteria"]["target_grade_pct"] == 14.0 + capped = generate_initial_route( + _line(200.0), + hill, + [], + mode="gradeline", + weights=None, + max_grade=GRADE, + min_radius_m=RADIUS, + target_grade=0.5, + ) + assert capped["criteria"]["target_grade_pct"] == 14.0 + low = generate_initial_route( + _line(200.0), + hill, + [], + mode="gradeline", + weights=None, + max_grade=GRADE, + min_radius_m=RADIUS, + target_grade=0.02, + ) + assert low["criteria"]["target_grade_pct"] == 2.0 + _kept(low) From 58ba4545f203552ef492a812a33ac2369a4deaaf Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 30 Sep 2026 09:24:38 +0900 Subject: [PATCH 14/18] =?UTF-8?q?feat(B05):=20=EB=85=B8=EC=84=A0=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=EC=95=8C=EC=95=BD=20=EC=97=AC=EB=8D=9F(?= =?UTF-8?q?=EC=98=81=EC=84=A0=20=C2=B7=20=EC=95=88=EC=A0=84=20=EC=9A=B0?= =?UTF-8?q?=EC=84=A0=20=C2=B7=20=EA=B3=84=EB=A5=98=20=ED=9A=A1=EB=8B=A8=20?= =?UTF-8?q?=EC=B5=9C=EC=86=8C=20=C2=B7=20=EA=B3=B5=EC=82=AC=EB=B9=84=20?= =?UTF-8?q?=EC=96=B4=EB=A6=BC=20=EC=B5=9C=EC=86=8C)=20=C2=B7=20=EC=98=81?= =?UTF-8?q?=EC=84=A0=20=EB=AA=A9=ED=91=9C=20=EA=B8=B0=EC=9A=B8=EA=B8=B0=20?= =?UTF-8?q?=EC=B9=B8=20=C2=B7=20=EB=B9=84=EA=B5=90=EA=B0=92=20=EB=91=90=20?= =?UTF-8?q?=EB=AC=B6=EC=9D=8C=20(PLAN=2023-5b)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 알약: 예상노선 추적 · 영선 노선 · 토공 최소 · 기울기 순한 · 안전 우선 · 계류 횡단 최소 · 공사비 어림 최소 · 비중 계산 — 짧은 노선 · 흙 균형 뺌 · 네 칸 두 줄(좁으면 두 칸 네 줄) - 영선 노선일 때만 「목표 기울기」 칸(criteria.target_grade_pct · 비우면 상한) · 응답에서 쓴 값을 기준값 줄에 보임 - 비교값 두 묶음: 노선 · 기울기(목표 기울기 편차 · 옆경사) / 흙 · 안전 · 비용(위험 구간 통과 · 계류 횡단 · 이격 · 공사비 어림 상대값) - 옛 저장값 short · balance 는 예상노선 추적으로 읽음 · 시험: 새 이름 · 비교값 · 목표 기울기 · 옛 값 · 왕복에 새 넷 Co-Authored-By: Claude Sonnet 5.5 --- B05_Profile/B05_Profile_Api_RouteInitial.ts | 4 +- .../B05_Profile_UI_RouteEdit_Initial_Model.ts | 88 ++++++++++++++--- .../B05_Profile_UI_RouteEdit_Recommend.ts | 43 +++++--- .../B05_Profile_UI_Style_RouteRecommend.css | 37 +++++-- .../tester/helper_b05_routeedit_initial.cjs | 77 ++++++++++++--- .../helper_b05_routeedit_initial_apply.cjs | 2 +- .../tester/test_b05_routeedit_initial.py | 97 ++++++++++++++----- .../test_b05_routeedit_initial_roundtrip.py | 21 +++- 8 files changed, 298 insertions(+), 71 deletions(-) diff --git a/B05_Profile/B05_Profile_Api_RouteInitial.ts b/B05_Profile/B05_Profile_Api_RouteInitial.ts index 4a658d9c..4fc9e802 100644 --- a/B05_Profile/B05_Profile_Api_RouteInitial.ts +++ b/B05_Profile/B05_Profile_Api_RouteInitial.ts @@ -16,7 +16,7 @@ const INITIAL_TIMEOUT_MS = 5 * 60 * 1000; /** 로직 이름 — 서버와 같은 낱말을 쓴다. `follow` 는 예상노선 추적(처음 초기값). */ export type RouteInitialLogic = - "follow" | "earthwork" | "gentle" | "short" | "balance" | "weighted"; + "follow" | "gradeline" | "earthwork" | "gentle" | "safe" | "crossings" | "cost" | "weighted"; /** 비중 계산의 세 비중(%) — 합 100. */ export interface RouteInitialWeights { @@ -32,6 +32,8 @@ export interface RouteInitialCriteria { min_radius_m?: number; stream_offset_m?: number; grade_smooth_m?: number; + /** 영선 노선(`gradeline`)의 목표 종단기울기(%) — 비우면 종단기울기 상한. */ + target_grade_pct?: number; } export interface RouteInitialRequest { diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Initial_Model.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Initial_Model.ts index d0578760..e60e958c 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Initial_Model.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Initial_Model.ts @@ -17,10 +17,12 @@ type Vertex = [number, number]; export const LOGIC_LABEL: Record = { follow: "예상노선 추적", + gradeline: "영선 노선", earthwork: "토공 최소", gentle: "기울기 순한", - short: "짧은 노선", - balance: "흙 균형", + safe: "안전 우선", + crossings: "계류 횡단 최소", + cost: "공사비 어림 최소", weighted: "비중 계산", }; @@ -38,6 +40,12 @@ export const CRITERIA_KEYS = [ ] as const; export type CriteriaKey = (typeof CRITERIA_KEYS)[number]; +/** 영선 노선 목표 기울기 칸 — 공통 제약 네 칸과 따로(영선을 고를 때만 보이고 보낸다). */ +export const TARGET_GRADE_KEY = "target_grade_pct" as const; + +/** 겹침 값에 담길 수 있는 모든 키 — 캐시 · 서버 기억 견주기에 쓴다. */ +const OVERRIDE_KEYS = [...CRITERIA_KEYS, TARGET_GRADE_KEY] as const; + /** 한 비중을 옮기면 나머지 둘이 **지금 비율대로** 나눠 합 100 을 지킨다(정수 %). */ export function rebalanceWeights( weights: RouteInitialWeights, @@ -122,6 +130,18 @@ export interface InitialMetrics { meanGradePct: number | null; cutM3: number | null; fillM3: number | null; + /** 옆경사(지형이 노선 옆으로 기운 정도, %) 평균 · 최대. */ + sideSlopeAvgPct: number | null; + sideSlopeMaxPct: number | null; + /** 위험 구간(옆경사 35° 이상 · 계류 버퍼 안)을 지나는 길이(m). */ + hazardM: number | null; + /** 계류를 건너는 횟수 · 계류와의 평균 이격(m). */ + streamCrossings: number | null; + streamOffsetAvgM: number | null; + /** 측점 기울기와 영선 목표 기울기 차의 평균(%p). */ + gradeDevPct: number | null; + /** 공사비 어림 — 절성토 · 연장 · 계류 횡단을 섞은 **상대값**(단위 없음 · 실제 공사비 아님). */ + costIndex: number | null; /** 남는 흙(㎥) = 절토 − 성토 — + 남음 / − 모자람. 서버 `surplus_m3`. */ surplusM3: number | null; overLimitCount: number | null; @@ -146,6 +166,8 @@ export interface InitialCriteria { streamOffsetM: number | null; /** 기울기를 잴 지반을 고른 폭(m) — 서버 `grade_smooth_m`. */ gradeSmoothM: number | null; + /** 영선 노선이 실제로 쓴 목표 기울기(%) — 서버 `target_grade_pct`. */ + targetGradePct: number | null; } /** 마지막에 적용한 초기값 한 벌 — 캐시(`routeedit-initial`)에 든 꼴. */ @@ -284,6 +306,13 @@ export function normalizeRouteInitial( meanGradePct: pick(m, "avg_grade_pct"), cutM3: pick(m, "cut_m3"), fillM3: pick(m, "fill_m3"), + sideSlopeAvgPct: pick(m, "side_slope_avg_pct"), + sideSlopeMaxPct: pick(m, "side_slope_max_pct"), + hazardM: pick(m, "hazard_m"), + streamCrossings: pick(m, "stream_crossings"), + streamOffsetAvgM: pick(m, "stream_offset_avg_m"), + gradeDevPct: pick(m, "grade_dev_pct"), + costIndex: pick(m, "cost_index"), surplusM3: pick(m, "surplus_m3"), overLimitCount: pick(over, "total"), offsetM: pick(m, "offset_avg_m"), @@ -307,6 +336,7 @@ export function normalizeRouteInitial( minRadiusM: pick(c, "min_radius_m"), streamOffsetM: pick(c, "stream_offset_m"), gradeSmoothM: pick(c, "grade_smooth_m"), + targetGradePct: pick(c, "target_grade_pct"), } : null, criteriaOverrides, @@ -327,7 +357,7 @@ function parseCriteriaOverrides(raw: unknown): RouteInitialCriteria | null { if (!raw || typeof raw !== "object") return null; const source = raw as Record; const out: RouteInitialCriteria = {}; - for (const key of CRITERIA_KEYS) { + for (const key of OVERRIDE_KEYS) { const value = num(source[key]); if (value !== null) out[key] = value; } @@ -336,7 +366,7 @@ function parseCriteriaOverrides(raw: unknown): RouteInitialCriteria | null { /** 두 겹침 값이 같은가 — 없는 칸은 서로 같다고 본다. */ function criteriaEqual(a: RouteInitialCriteria | null, b: RouteInitialCriteria | null): boolean { - return CRITERIA_KEYS.every((key) => (a?.[key] ?? null) === (b?.[key] ?? null)); + return OVERRIDE_KEYS.every((key) => (a?.[key] ?? null) === (b?.[key] ?? null)); } /** 서버 `current_initial {logic, weights, criteria}` 를 편다. 모르는 값이면 예상노선 추적. */ @@ -414,27 +444,61 @@ export function similarLine(choice: InitialChoice | null): string { export const SIMILAR_HINT = "기울기 · 최소R · 계류 제약 값을 바꾸면 갈래가 서로 벌어질 수 있습니다(다시 셈)."; -/** 결과 비교값 한 줄 — 연장 · 최대/평균 기울기 · 절성토량 · 기준 초과 구간 · 이격. */ -export function metricsLine(metrics: InitialMetrics | null): string { - if (!metrics) return "비교값 — 로직을 누르면 서버가 셈해 보입니다."; - return [ +/** 결과 비교값 — 두 묶음. 노선 · 기울기 / 흙 · 안전 · 비용. 서버가 안 준 칸(옛 답)은 빠진다. */ +export interface MetricsLines { + route: string; + work: string; +} + +const NO_METRICS = "비교값 — 알약을 누르면 서버가 셈해 보입니다."; + +export function metricsLines(metrics: InitialMetrics | null): MetricsLines { + if (!metrics) return { route: NO_METRICS, work: "" }; + const has = (value: number | null): value is number => value !== null; + const route = [ `연장 ${fixed(metrics.lengthM, 1, "m")}`, `기울기 최대 ${fixed(metrics.maxGradePct, 1, "%")} · 평균 ${fixed(metrics.meanGradePct, 1, "%")}`, - `절토 ${fixed(metrics.cutM3, 0, "㎥")} · 성토 ${fixed(metrics.fillM3, 0, "㎥")}`, - ...(metrics.surplusM3 === null ? [] : [`남는 흙 ${surplusText(metrics.surplusM3)}`]), + ...(has(metrics.gradeDevPct) + ? [`목표 기울기 편차 ${fixed(metrics.gradeDevPct, 1, "%p")}`] + : []), + ...(has(metrics.sideSlopeAvgPct) + ? [ + `옆경사 평균 ${fixed(metrics.sideSlopeAvgPct, 1, "%")} · 최대 ${fixed(metrics.sideSlopeMaxPct, 1, "%")}`, + ] + : []), `기준 초과 ${metrics.overLimitCount === null ? "—" : `${metrics.overLimitCount}구간`}`, - `이격 ${fixed(metrics.offsetM, 1, "m")}`, + `예상노선과 이격 ${fixed(metrics.offsetM, 1, "m")}`, ].join(" · "); + const work = [ + `절토 ${fixed(metrics.cutM3, 0, "㎥")} · 성토 ${fixed(metrics.fillM3, 0, "㎥")}`, + ...(has(metrics.surplusM3) ? [`남는 흙 ${surplusText(metrics.surplusM3)}`] : []), + ...(has(metrics.hazardM) ? [`위험 구간 통과 ${fixed(metrics.hazardM, 0, "m")}`] : []), + ...(has(metrics.streamCrossings) ? [`계류 횡단 ${metrics.streamCrossings}회`] : []), + ...(has(metrics.streamOffsetAvgM) + ? [`계류 이격 평균 ${fixed(metrics.streamOffsetAvgM, 1, "m")}`] + : []), + ...(has(metrics.costIndex) + ? [`공사비 어림 ${fixed(metrics.costIndex, 0, "")}(어림 · 상대값)`] + : []), + ].join(" · "); + return { route, work }; } /** 공통 제약 기준값 한 줄 — 서버가 실제로 쓴 값(계획서 20 확정 · 화면 칸에서 바꿀 수 있음). */ -export function criteriaLine(criteria: InitialCriteria | null, planMinRadiusM: number): string { +export function criteriaLine( + criteria: InitialCriteria | null, + planMinRadiusM: number, + /** 영선 노선일 때만 — 서버가 쓴 목표 기울기를 함께 보인다. */ + showTarget = false, +): string { const radius = criteria?.minRadiusM ?? (planMinRadiusM > 0 ? planMinRadiusM : null); const parts = [ `종단기울기 ≤ ${fixed(criteria?.maxGradePct ?? null, 1, "%")}`, `최소 R ${fixed(radius, 0, "m")}`, `계류 이격 ≥ ${fixed(criteria?.streamOffsetM ?? null, 1, "m")}`, ]; + if (showTarget && criteria?.targetGradePct != null) + parts.push(`영선 목표 기울기 ${fixed(criteria.targetGradePct, 1, "%")}`); // 기울기는 고른 지반으로 잰다 — 그 폭을 서버가 주면 그대로 보인다. if (criteria?.gradeSmoothM) parts.push(`기울기 잴 지반 ${fixed(criteria.gradeSmoothM, 0, "m")} 고름`); diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Recommend.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Recommend.ts index e78d004b..6f728f9d 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Recommend.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Recommend.ts @@ -23,7 +23,8 @@ import { criteriaLine, CRITERIA_KEYS, LOGIC_LABEL, - metricsLine, + metricsLines, + TARGET_GRADE_KEY, canLockWeight, derivedWeightKey, inputWeight, @@ -48,10 +49,12 @@ type Vertex = [number, number]; const PILLS: RouteInitialLogic[] = [ "follow", + "gradeline", "earthwork", "gentle", - "short", - "balance", + "safe", + "crossings", + "cost", "weighted", ]; @@ -59,8 +62,10 @@ const PILL_TITLE: Record = { follow: "전처리가 만든 기본 노선 — 예상노선을 따라갑니다", earthwork: "절토 · 성토가 가장 적은 노선", gentle: "종단기울기가 가장 순한 노선", - short: "연장이 가장 짧은 노선(제약 지킴)", - balance: "절토와 성토가 비슷해 남는 흙이 가장 적은 노선", + gradeline: "정한 종단기울기로 등고선을 따라가는 등경사선(영선) — 목표 기울기를 넣을 수 있음", + safe: "급한 옆경사 · 계류 가까운 곳을 피하는 노선", + crossings: "계류를 건너는 횟수가 가장 적은 노선", + cost: "토공 + 연장 + 계류 횡단을 섞은 공사비 어림(상대값)이 가장 낮은 노선", weighted: "예상 · 토공 · 기울기 비중(합 100)으로 셈한 노선", }; @@ -200,11 +205,16 @@ export function openRecommendModal(params: RecommendParams): void { data-criteria="${key}" placeholder="기본값" />${CRITERIA_FIELD[key].unit}`, ).join("")}
+
+
@@ -228,7 +238,8 @@ export function openRecommendModal(params: RecommendParams): void { const criteriaInputs = body.querySelectorAll("[data-criteria]"); criteriaInputs.forEach((input) => { - const value = params.criteria?.[input.dataset.criteria as CriteriaKey]; + const value = + params.criteria?.[input.dataset.criteria as CriteriaKey | typeof TARGET_GRADE_KEY]; input.value = value === undefined ? "" : String(value); }); @@ -236,10 +247,13 @@ export function openRecommendModal(params: RecommendParams): void { function currentCriteria(): RouteInitialCriteria { const out: RouteInitialCriteria = {}; criteriaInputs.forEach((input) => { + const key = input.dataset.criteria as CriteriaKey | typeof TARGET_GRADE_KEY; + // 목표 기울기는 영선 노선을 물을 때만 보낸다. + if (key === TARGET_GRADE_KEY && logic !== "gradeline") return; const raw = input.value.trim(); if (raw === "") return; const value = Number(raw); - if (Number.isFinite(value)) out[input.dataset.criteria as CriteriaKey] = value; + if (Number.isFinite(value)) out[key] = value; }); return out; } @@ -290,19 +304,26 @@ export function openRecommendModal(params: RecommendParams): void { q("[data-weight-sum]").textContent = `합 ${weights.expected + weights.earthwork + weights.grade}`; criteriaInputs.forEach((input) => { - const key = input.dataset.criteria as CriteriaKey; + const key = input.dataset.criteria as CriteriaKey | typeof TARGET_GRADE_KEY; + // 목표 기울기를 비우면 종단기울기 상한을 쓴다 — 그 값이 자리표시. + const known = + key === TARGET_GRADE_KEY ? params.defaults.max_grade_pct : params.defaults[key]; input.placeholder = - params.defaults[key] !== undefined ? String(params.defaults[key]) : "기본값"; + known !== undefined ? String(known) : key === TARGET_GRADE_KEY ? "상한" : "기본값"; input.disabled = busy; }); + q("[data-target-row]").hidden = logic !== "gradeline"; rerun.hidden = !dirty; rerun.disabled = busy || invalid.size > 0; applyButton.disabled = busy || dirty || !preview || invalid.size > 0; + const shown = metricsLines(preview?.metrics ?? null); line("metrics").textContent = preview ? preview.metrics - ? metricsLine(preview.metrics) + ? shown.route : "비교값 — 서버 답에 없습니다." : "비교값 — 알약을 누르면 서버가 셈해 미리 보여 줍니다."; + line("metrics-work").textContent = preview ? shown.work : ""; + line("metrics-work").hidden = !line("metrics-work").textContent; const violations = violationsLine(preview?.metrics ?? null); line("violations").textContent = violations; line("violations").title = (preview?.metrics?.violations ?? []) @@ -318,7 +339,7 @@ export function openRecommendModal(params: RecommendParams): void { line("similar-hint").textContent = similar ? SIMILAR_HINT : ""; line("similar-hint").hidden = !similar; line("criteria").textContent = preview - ? criteriaLine(preview.criteria, params.planMinRadiusM) + ? criteriaLine(preview.criteria, params.planMinRadiusM, preview.logic === "gradeline") : ""; line("criteria").hidden = !preview; drawMap(); diff --git a/B05_Profile/B05_Profile_UI_Style_RouteRecommend.css b/B05_Profile/B05_Profile_UI_Style_RouteRecommend.css index 60b2de8d..a3895d00 100644 --- a/B05_Profile/B05_Profile_UI_Style_RouteRecommend.css +++ b/B05_Profile/B05_Profile_UI_Style_RouteRecommend.css @@ -26,14 +26,32 @@ } /* 알약 — 하나만 고른 상태(라디오 묶음). */ +/* 여덟 알약 — 네 칸 두 줄로 고르게(좁은 폭은 두 칸 네 줄). */ .b05-routerec__pills { - display: flex; - flex-wrap: wrap; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--spacing-8); } +/* 좁은 폭 — 두 줄을 지키려 글자를 줄이고, 글자가 잘릴 만큼 좁으면(560px 이하) 두 칸 네 줄. */ +@media (max-width: 720px) { + .b05-routerec__pill { + padding-inline: 4px; + font-size: var(--text-body-sm); + } +} + +@media (max-width: 560px) { + .b05-routerec__pills { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + .b05-routerec__pill { - padding: 8px var(--spacing-16); + padding: 8px var(--spacing-8); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; border: 1px solid var(--color-border); border-radius: 999px; background: var(--color-surface); @@ -245,7 +263,8 @@ gap: var(--spacing-8); } -.b05-routerec__criteria label { +.b05-routerec__criteria label, +.b05-routerec__target { display: grid; grid-template-columns: minmax(0, 1fr) 5.5em 1.5em; align-items: center; @@ -253,7 +272,8 @@ color: var(--color-text-body); } -.b05-routerec__criteria input { +.b05-routerec__criteria input, +.b05-routerec__target input { width: 100%; padding: 4px 8px; border: 1px solid var(--color-border); @@ -264,7 +284,8 @@ text-align: right; } -.b05-routerec__criteria input::placeholder { +.b05-routerec__criteria input::placeholder, +.b05-routerec__target input::placeholder { color: var(--color-text-secondary); } @@ -318,3 +339,7 @@ height: 300px; } } + +.b05-routerec__target[hidden] { + display: none; +} diff --git a/resources/tester/helper_b05_routeedit_initial.cjs b/resources/tester/helper_b05_routeedit_initial.cjs index bada657b..2e8aca0c 100644 --- a/resources/tester/helper_b05_routeedit_initial.cjs +++ b/resources/tester/helper_b05_routeedit_initial.cjs @@ -162,8 +162,8 @@ try { const labels = { follow: model.choiceLabel("follow", null), weighted: model.choiceLabel("weighted", { expected: 60, earthwork: 20, grade: 20 }), - metrics: model.metricsLine(plain.metrics), - metricsNone: model.metricsLine(null), + metrics: model.metricsLines(plain.metrics), + metricsNone: model.metricsLines(null).route, criteria: model.criteriaLine(plain.criteria, 0), criteriaFallback: model.criteriaLine(null, 15), violations: model.violationsLine(plain.metrics), @@ -298,21 +298,58 @@ const weightLocks = { }, }; -// ⑨ 추천 갈래 여섯 · 남는 흙 · 같은 노선 알림(계획서 23-4c 화면). -const withExtras = (extra, metricExtra = {}) => +// ⑨ 추천 갈래 여덟 · 새 비교값 · 영선 목표 기울기 · 같은 노선 알림(계획서 23-5b 화면). +const newMetrics = { + side_slope_avg_pct: 65.8, + side_slope_max_pct: 104.1, + hazard_m: 458.4, + stream_crossings: 0, + stream_offset_avg_m: 312.44, + grade_dev_pct: 8.53, + cost_index: 9691.4, + surplus_m3: -578.2, +}; +const withExtras = (extra, metricExtra = {}, logic = "gradeline") => model.normalizeRouteInitial( { ...plainResponse, ...extra, metrics: { ...plainResponse.metrics, ...metricExtra } }, - "short", + logic, null, ); +const gradeline = withExtras( + { criteria: { ...plainResponse.criteria, target_grade_pct: 12 } }, + newMetrics, +); const sixth = { labels: Object.fromEntries( - ["follow", "earthwork", "gentle", "short", "balance", "weighted"].map((k) => [k, model.LOGIC_LABEL[k]]), + ["follow", "gradeline", "earthwork", "gentle", "safe", "crossings", "cost", "weighted"].map((k) => [ + k, + model.LOGIC_LABEL[k], + ]), ), + removedLogics: ["short", "balance"].map((k) => k in model.LOGIC_LABEL), + // 옛 저장값(short · balance)은 예상노선 추적으로 읽는다 — 서버 기억 · 캐시 둘 다. + legacyBase: ["short", "balance"].map((k) => model.serverBase({ logic: k })), + legacyCache: undefined, // 아래 ④ 캐시 왕복 뒤에 채운다(저장소를 같이 쓰므로) surplus: [122.4, -578.2, 0, 0.4].map((v) => model.surplusText(v)), - metricsPlus: model.metricsLine(withExtras({}, { surplus_m3: 122.4 }).metrics), - metricsMinus: model.metricsLine(withExtras({}, { surplus_m3: -578 }).metrics), - metricsNone: model.metricsLine(withExtras({}).metrics), + groups: model.metricsLines(gradeline.metrics), + groupsMinus: model.metricsLines(withExtras({}, { surplus_m3: -578 }).metrics), + groupsOld: model.metricsLines(withExtras({}).metrics), + criteriaTarget: model.criteriaLine(gradeline.criteria, 0, true), + criteriaNoTarget: model.criteriaLine(gradeline.criteria, 0, false), + targetParsed: gradeline.criteria.targetGradePct, + targetOverride: model.normalizeRouteInitial(plainResponse, "gradeline", null, { target_grade_pct: 10 }) + .criteriaOverrides, + targetBase: model.serverBase({ logic: "gradeline", criteria: { target_grade_pct: 10 } }), + sameTarget: [ + model.sameBase( + model.normalizeRouteInitial(plainResponse, "gradeline", null, { target_grade_pct: 10 }), + model.serverBase({ logic: "gradeline", criteria: { target_grade_pct: 10 } }), + ), + model.sameBase( + model.normalizeRouteInitial(plainResponse, "gradeline", null, { target_grade_pct: 12 }), + model.serverBase({ logic: "gradeline", criteria: { target_grade_pct: 10 } }), + ), + ], similarTwo: model.similarLine( withExtras({ band_width_m: 13.5, @@ -323,13 +360,19 @@ const sixth = { }), ), similarOne: model.similarLine( - withExtras({ band_width_m: 13.54, similar: [{ logic: "balance", max_gap_m: 6.1 }] }), + withExtras({ band_width_m: 13.54, similar: [{ logic: "safe", max_gap_m: 6.1 }] }), ), - similarNoBand: model.similarLine(withExtras({ similar: [{ logic: "weighted", max_gap_m: 9.1 }] })), + similarNoBand: model.similarLine(withExtras({ similar: [{ logic: "cost", max_gap_m: 9.1 }] })), similarEmpty: model.similarLine(withExtras({ band_width_m: 13.5, similar: [] })), similarMissing: model.similarLine(withExtras({})), similarNull: model.similarLine(null), - similarUnknown: withExtras({ similar: [{ logic: "nope", max_gap_m: 1 }, { logic: "short", max_gap_m: "x" }] }).similar, + similarUnknown: withExtras({ + similar: [ + { logic: "nope", max_gap_m: 1 }, + { logic: "short", max_gap_m: 2 }, + { logic: "cost", max_gap_m: "x" }, + ], + }).similar, bandParsed: withExtras({ band_width_m: 13.5 }).bandWidthM, hint: model.SIMILAR_HINT, weightedViolation: model.violationsLine({ @@ -351,8 +394,6 @@ const sixth = { limit: 14, reason: "비중 계산 — 예상노선 몫만큼 제약을 막지 않음", }), - shortLabel: model.choiceLabel("short", { expected: 0, earthwork: 0, grade: 0 }), - balanceLabel: model.choiceLabel("balance", null), }; // ④ 캐시 왕복 — 결과 통 프로젝트 칸. 꼴이 틀린 값은 안 읽는다. @@ -378,6 +419,14 @@ const badLogic = model.readInitialChoice("p-1"); model.writeInitialChoice("p-1", null); const cleared = model.readInitialChoice("p-1"); +// 옛 저장값(short)이 캐시에 있어도 읽지 않는다 — 서버 기억(추적)을 쓴다. +session._map.clear(); +session.setItem( + "aislo:result:routeedit-initial:p-9", + JSON.stringify({ logic: "short", weights: null, snapshot: { planned: [[0, 0], [1, 1]] } }), +); +sixth.legacyCache = model.readInitialChoice("p-9"); + process.stdout.write( JSON.stringify({ moves, diff --git a/resources/tester/helper_b05_routeedit_initial_apply.cjs b/resources/tester/helper_b05_routeedit_initial_apply.cjs index c7e59008..a1372bef 100644 --- a/resources/tester/helper_b05_routeedit_initial_apply.cjs +++ b/resources/tester/helper_b05_routeedit_initial_apply.cjs @@ -81,7 +81,7 @@ const out = jobs.map((job) => { }, }, label: model.choiceLabel(choice.logic, choice.weights), - metrics: model.metricsLine(choice.metrics), + metrics: model.metricsLines(choice.metrics), violations: model.violationsLine(choice.metrics), relaxed: model.relaxedLine(choice), criteria: model.criteriaLine(choice.criteria, 0), diff --git a/resources/tester/test_b05_routeedit_initial.py b/resources/tester/test_b05_routeedit_initial.py index ece139ab..f5b9c444 100644 --- a/resources/tester/test_b05_routeedit_initial.py +++ b/resources/tester/test_b05_routeedit_initial.py @@ -77,11 +77,12 @@ def test_비교값과_기준값_글(results): labels = results["labels"] assert labels["follow"] == "예상노선 추적" assert labels["weighted"] == "비중 계산 60/20/20" - assert labels["metrics"] == ( - "연장 1,234.6m · 기울기 최대 12.3% · 평균 6.1% · 절토 1,500㎥ · 성토 900㎥" - " · 기준 초과 2구간 · 이격 3.3m" - ) - assert "로직을 누르면" in labels["metricsNone"] + # 두 묶음 — 노선 · 기울기 / 흙 · 안전 · 비용(서버가 안 준 새 칸은 빠짐) + assert labels["metrics"] == { + "route": "연장 1,234.6m · 기울기 최대 12.3% · 평균 6.1% · 기준 초과 2구간 · 예상노선과 이격 3.3m", + "work": "절토 1,500㎥ · 성토 900㎥", + } + assert "알약을 누르면" in labels["metricsNone"] # 기준값 확정(2026-09-29) — 「후보 · 사용자 확정 대기」 꼬리표가 더는 안 붙는다. assert labels["criteria"] == ( "공통 제약 종단기울기 ≤ 14.0% · 최소 R 12m · 계류 이격 ≥ 5.0m · 기울기 잴 지반 5m 고름" @@ -248,36 +249,88 @@ def test_비중_값_칸_직접_입력(results): } -def test_추천_갈래_여섯_이름과_가중치_없음(results): +def test_추천_갈래_여덟_이름_알약_순서(results): six = results["sixth"] assert six["labels"] == { "follow": "예상노선 추적", + "gradeline": "영선 노선", "earthwork": "토공 최소", "gentle": "기울기 순한", - "short": "짧은 노선", - "balance": "흙 균형", + "safe": "안전 우선", + "crossings": "계류 횡단 최소", + "cost": "공사비 어림 최소", "weighted": "비중 계산", } - # 짧은 노선 · 흙 균형은 비중이 없다(weights 0/0/0 이 와도 이름만) - assert six["shortLabel"] == "짧은 노선" and six["balanceLabel"] == "흙 균형" + # 짧은 노선 · 흙 균형은 뺐다 + assert six["removedLogics"] == [False, False] page = os.path.join(HERE, "..", "..", "B05_Profile", "B05_Profile_UI_RouteEdit_Recommend.ts") with open(page, encoding="utf-8") as handle: text = handle.read() + start = text.index("const PILLS") + pills = text[start : text.index("];", start)] order = [ - text.index(f'"{name}"') for name in ("follow", "earthwork", "gentle", "short", "balance") + pills.index(f'"{name}"') + for name in ( + "follow", + "gradeline", + "earthwork", + "gentle", + "safe", + "crossings", + "cost", + "weighted", + ) ] - assert order == sorted(order) # 알약 순서: 추적 · 토공 · 기울기 · 짧은 · 균형 · 비중 + assert order == sorted(order) # 알약 순서 + assert '"short"' not in pills and '"balance"' not in pills + assert ( + "grid-template-columns: repeat(4" + in open( + os.path.join( + HERE, "..", "..", "B05_Profile", "B05_Profile_UI_Style_RouteRecommend.css" + ), + encoding="utf-8", + ).read() + ) # 네 칸 두 줄 -def test_비교값_남는_흙(results): +def test_옛_저장값_short_balance는_추적으로_읽음(results): + six = results["sixth"] + assert six["legacyBase"] == [ + {"logic": "follow", "weights": None, "criteria": None}, + {"logic": "follow", "weights": None, "criteria": None}, + ] + assert six["legacyCache"] is None # 캐시의 옛 갈래는 버림 → 서버 기억(추적)을 씀 + + +def test_비교값_새_칸_두_묶음(results): six = results["sixth"] assert six["surplus"] == ["+122㎥(남음)", "−578㎥(모자람)", "0㎥(균형)", "0㎥(균형)"] - assert six["metricsPlus"].endswith( - "성토 900㎥ · 남는 흙 +122㎥(남음) · 기준 초과 2구간 · 이격 3.3m" + groups = six["groups"] + assert groups["route"] == ( + "연장 1,234.6m · 기울기 최대 12.3% · 평균 6.1% · 목표 기울기 편차 8.5%p" + " · 옆경사 평균 65.8% · 최대 104.1% · 기준 초과 2구간 · 예상노선과 이격 3.3m" ) - assert "남는 흙 −578㎥(모자람)" in six["metricsMinus"] - # 서버가 안 주면 칸이 없다(옛 답) — 기존 비교값 글 그대로 - assert "남는 흙" not in six["metricsNone"] + assert groups["work"] == ( + "절토 1,500㎥ · 성토 900㎥ · 남는 흙 −578㎥(모자람) · 위험 구간 통과 458m" + " · 계류 횡단 0회 · 계류 이격 평균 312.4m · 공사비 어림 9,691(어림 · 상대값)" + ) + assert "남는 흙 −578㎥(모자람)" in six["groupsMinus"]["work"] + # 서버가 안 준 칸(옛 답)은 빠진다 + assert "옆경사" not in six["groupsOld"]["route"] and "공사비" not in six["groupsOld"]["work"] + + +def test_영선_목표_기울기(results): + six = results["sixth"] + assert six["targetParsed"] == 12 + assert six["criteriaTarget"].endswith("영선 목표 기울기 12.0% · 기울기 잴 지반 5m 고름") or ( + "영선 목표 기울기 12.0%" in six["criteriaTarget"] + ) + assert "영선 목표" not in six["criteriaNoTarget"] # 영선이 아니면 안 보임 + # 요청에 실은 값은 겹침으로 남고 · 서버 기억과 견줘 다르면 캐시를 버림 + assert six["targetOverride"] == {"target_grade_pct": 10} + assert six["targetBase"]["criteria"] == {"target_grade_pct": 10} + assert six["sameTarget"] == [True, False] def test_같은_노선_알림_줄(results): @@ -287,13 +340,13 @@ def test_같은_노선_알림_줄(results): "토공 최소 · 기울기 순한과 거의 같은 노선(가장 먼 거리 6.1m)" ) assert six["similarOne"] == ( - "이 지형은 제약 안에서 갈 길이 폭 13.5m 한 띠뿐 — 흙 균형과 거의 같은 노선(가장 먼 거리 6.1m)" + "이 지형은 제약 안에서 갈 길이 폭 13.5m 한 띠뿐 — 안전 우선과 거의 같은 노선(가장 먼 거리 6.1m)" ) - # 띠 폭이 없어도 줄은 선다 · 받침 없는 이름은 「와」 - assert "폭" not in six["similarNoBand"] and "비중 계산과 거의" in six["similarNoBand"] + # 띠 폭이 없어도 줄은 선다 · 받침 있는 이름은 「과」 + assert "폭" not in six["similarNoBand"] and "공사비 어림 최소와 거의" in six["similarNoBand"] # 없으면 줄 숨김 — 빈 목록 · 칸 없음 · 선택 없음 assert six["similarEmpty"] == "" and six["similarMissing"] == "" and six["similarNull"] == "" - assert six["similarUnknown"] == [] # 이름 · 거리를 모르는 항목은 버림 + assert six["similarUnknown"] == [] # 이름 · 거리를 모르는 항목(옛 short 포함)은 버림 assert six["bandParsed"] == 13.5 assert "제약 값을 바꾸면" in six["hint"] diff --git a/resources/tester/test_b05_routeedit_initial_roundtrip.py b/resources/tester/test_b05_routeedit_initial_roundtrip.py index 0b77115d..f30b98b3 100644 --- a/resources/tester/test_b05_routeedit_initial_roundtrip.py +++ b/resources/tester/test_b05_routeedit_initial_roundtrip.py @@ -43,9 +43,11 @@ LOGICS: list[tuple[str, dict[str, float] | None]] = [ ("follow", None), ("earthwork", None), ("gentle", None), - # PLAN 23-5 — 서버는 짧은 노선 · 흙 균형을 빼고 넷(gradeline · safe · crossings · cost)을 - # 더함. 새 넷은 화면(23-5b)이 알약으로 알게 되면 여기 더함 — 지금 화면은 다시 열 때 모르는 - # 갈래를 추적으로 읽음. 서버 쪽 저장 · 다시 읽기는 `test_route_initial` 이 지킴. + # PLAN 23-5 — 새 갈래 넷(영선 · 안전 우선 · 계류 횡단 최소 · 공사비 어림 최소)도 같은 왕복. + ("gradeline", None), + ("safe", None), + ("crossings", None), + ("cost", None), ("weighted", {"expected": 50, "earthwork": 25, "grade": 25}), ] @@ -202,7 +204,18 @@ def test_비교값이_응답에_다_있음(rounds): assert isinstance(metrics["over_limit"]["total"], int) assert isinstance(metrics["offset_avg_m"], (int, float)) # 화면 한 줄에 빈 칸(—)이 없다. - assert "—" not in item["applied"]["metrics"], item["applied"]["metrics"] + shown = item["applied"]["metrics"] + assert "—" not in shown["route"] + shown["work"], shown + # 새 비교값 칸도 화면 글에 다 있음(계획서 23-5) + for label in ( + "옆경사 평균", + "목표 기울기 편차", + "위험 구간 통과", + "계류 횡단", + "계류 이격 평균", + "공사비 어림", + ): + assert label in shown["route"] + shown["work"], (item["logic"], label) def test_제약_못_지킨_구간이_목록과_화면_줄에(rounds): From 2a3051fbcaae13833873647a2275123b51cd79bc Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 30 Sep 2026 09:41:04 +0900 Subject: [PATCH 15/18] =?UTF-8?q?feat(M02):=20=EB=A7=88=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=EC=A7=91=EA=B3=84=ED=91=9C=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=ED=99=94=20=C2=B7=20=EC=8B=9C=ED=97=98=20?= =?UTF-8?q?=EA=B2=AC=EB=B3=B8=ED=99=94(PLAN=2026-5a)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 마스터 층 구조물 항목 51개 지움 — 나무 지우기(tree.apply delete) → 집계표 저장(store.write) 한 줄기 · 집계표 구조물 열 · SS 장부 줄 · structure/area/material 파일 153개 · 남김 = no · sta · 비고 열 · 변수 · 쪽줄 · 합계줄 · 바인딩규칙 · 자재조합 · 구조물옵션표시 · drawing · M01 · 회사 · 개인 층 · 프로젝트 복사본 · 지운 열의 열너비만 뺌 - 키 · 번호 다시 안 씀 — 장부 다음 52(새 항목 SS000052) · 지운번호 51개 기록(새 번호는 SD-000001 모양이라 옛 8자리와 안 겹침) - 지우기 전 사본 tmp/마스터_집계표_초기화전_0930/(git 밖 · 156 파일 = 지우기 전과 같음) - 시험 안 견본 fixtures/m02_master_sample(옛 집계표 · 장부 · 자재조합 · rv · c4 · pp_len · ms 세 파일 · 도면 줄임) + helper_m02_master_sample — 실제 마스터를 읽던 시험 일곱(table_fill · template_layers · structures_summary_snapshot · item_copy_complete · m02_tree · m02_item_inputs · spreadsheet_saved_values)을 견본으로 · M01 쓰는 곳 찾기(users_of) 시험은 이미 임시 표(combo_inputs)라 그대로 - 새 시험 test_m02_empty_master — 빈 마스터에서 항목 0 · 빈 나무 · 첫 화면 길 · 새 폴더 · 새 항목(SS000052 · SD-000001) · 지우기 · 고르기 모달 마스터 0 · 프로젝트 복사본 calc 그대로 · 원본없음 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL --- resources/master_template/area/be.json | 17 - resources/master_template/area/bg.json | 17 - resources/master_template/area/bm_area.json | 17 - resources/master_template/area/bm_len.json | 17 - resources/master_template/area/bx_len.json | 17 - resources/master_template/area/bx_wing.json | 17 - resources/master_template/area/c1.json | 17 - resources/master_template/area/c2.json | 17 - resources/master_template/area/c3.json | 17 - resources/master_template/area/c5.json | 17 - resources/master_template/area/cs.json | 17 - resources/master_template/area/ec.json | 17 - resources/master_template/area/etc.json | 17 - resources/master_template/area/fb.json | 17 - resources/master_template/area/fp_l.json | 17 - resources/master_template/area/fp_w.json | 17 - resources/master_template/area/gr_curb.json | 17 - resources/master_template/area/gr_rail.json | 17 - resources/master_template/area/gr_sign.json | 17 - resources/master_template/area/h_break.json | 17 - .../master_template/area/h_ford_basin.json | 17 - resources/master_template/area/h_intake.json | 17 - resources/master_template/area/h_pg_rail.json | 17 - resources/master_template/area/h_pv_rail.json | 17 - resources/master_template/area/h_rip_c.json | 17 - resources/master_template/area/h_rip_m.json | 17 - .../master_template/area/h_rockfall.json | 17 - .../master_template/area/h_soil_ditch.json | 17 - .../master_template/area/h_stone_bed.json | 17 - .../master_template/area/h_stone_ch.json | 17 - resources/master_template/area/h_waste.json | 17 - resources/master_template/area/mr.json | 17 - resources/master_template/area/od_cnt.json | 17 - resources/master_template/area/od_len.json | 17 - resources/master_template/area/pg_in.json | 18 - resources/master_template/area/pp_cp.json | 17 - resources/master_template/area/ps.json | 17 - resources/master_template/area/pv_a.json | 17 - resources/master_template/area/pv_b.json | 17 - resources/master_template/area/pv_jt.json | 17 - resources/master_template/area/pv_l.json | 17 - resources/master_template/area/pv_w.json | 17 - resources/master_template/area/rf.json | 17 - resources/master_template/area/rw.json | 17 - resources/master_template/area/sg.json | 17 - resources/master_template/area/ta.json | 17 - resources/master_template/area/wy.json | 17 - resources/master_template/material/be.json | 17 - resources/master_template/material/bg.json | 17 - .../master_template/material/bm_area.json | 17 - .../master_template/material/bm_len.json | 17 - .../master_template/material/bx_len.json | 17 - .../master_template/material/bx_wing.json | 17 - resources/master_template/material/c1.json | 17 - resources/master_template/material/c2.json | 17 - resources/master_template/material/c3.json | 17 - resources/master_template/material/c5.json | 17 - resources/master_template/material/cs.json | 17 - resources/master_template/material/ec.json | 17 - resources/master_template/material/etc.json | 17 - resources/master_template/material/fb.json | 17 - resources/master_template/material/fp_l.json | 17 - resources/master_template/material/fp_w.json | 17 - .../master_template/material/gr_curb.json | 17 - .../master_template/material/gr_rail.json | 17 - .../master_template/material/gr_sign.json | 17 - .../master_template/material/h_break.json | 17 - .../material/h_ford_basin.json | 17 - .../master_template/material/h_intake.json | 17 - .../master_template/material/h_pg_rail.json | 17 - .../master_template/material/h_pv_rail.json | 17 - .../master_template/material/h_rip_c.json | 17 - .../master_template/material/h_rip_m.json | 17 - .../master_template/material/h_rockfall.json | 17 - .../material/h_soil_ditch.json | 17 - .../master_template/material/h_stone_bed.json | 17 - .../master_template/material/h_stone_ch.json | 17 - .../master_template/material/h_waste.json | 17 - resources/master_template/material/mr.json | 17 - .../master_template/material/od_cnt.json | 17 - .../master_template/material/od_len.json | 17 - resources/master_template/material/pg_in.json | 17 - resources/master_template/material/pp_cp.json | 17 - resources/master_template/material/ps.json | 17 - resources/master_template/material/pv_a.json | 17 - resources/master_template/material/pv_b.json | 17 - resources/master_template/material/pv_jt.json | 17 - resources/master_template/material/pv_l.json | 17 - resources/master_template/material/pv_w.json | 17 - resources/master_template/material/rf.json | 17 - resources/master_template/material/rw.json | 17 - resources/master_template/material/sg.json | 17 - resources/master_template/material/ta.json | 17 - resources/master_template/material/wy.json | 17 - resources/master_template/structure/be.json | 19 - resources/master_template/structure/bg.json | 19 - .../master_template/structure/bm_area.json | 19 - .../master_template/structure/bm_len.json | 19 - .../master_template/structure/bx_len.json | 19 - .../master_template/structure/bx_wing.json | 19 - resources/master_template/structure/c1.json | 19 - resources/master_template/structure/c2.json | 19 - resources/master_template/structure/c3.json | 19 - resources/master_template/structure/c5.json | 19 - resources/master_template/structure/cs.json | 19 - resources/master_template/structure/ec.json | 19 - resources/master_template/structure/etc.json | 19 - resources/master_template/structure/fb.json | 19 - resources/master_template/structure/fp_l.json | 19 - resources/master_template/structure/fp_w.json | 19 - .../master_template/structure/gr_curb.json | 19 - .../master_template/structure/gr_rail.json | 19 - .../master_template/structure/gr_sign.json | 19 - .../master_template/structure/h_break.json | 19 - .../structure/h_ford_basin.json | 19 - .../master_template/structure/h_intake.json | 19 - .../master_template/structure/h_pg_rail.json | 19 - .../master_template/structure/h_pv_rail.json | 19 - .../master_template/structure/h_rip_c.json | 19 - .../master_template/structure/h_rip_m.json | 19 - .../master_template/structure/h_rockfall.json | 19 - .../structure/h_soil_ditch.json | 19 - .../structure/h_stone_bed.json | 19 - .../master_template/structure/h_stone_ch.json | 19 - .../master_template/structure/h_waste.json | 19 - resources/master_template/structure/mr.json | 19 - .../master_template/structure/od_cnt.json | 19 - .../master_template/structure/od_len.json | 19 - .../master_template/structure/pg_in.json | 19 - .../master_template/structure/pp_cp.json | 19 - resources/master_template/structure/ps.json | 19 - resources/master_template/structure/pv_a.json | 19 - resources/master_template/structure/pv_b.json | 19 - .../master_template/structure/pv_jt.json | 19 - resources/master_template/structure/pv_l.json | 19 - resources/master_template/structure/pv_w.json | 19 - resources/master_template/structure/rf.json | 19 - resources/master_template/structure/rv.json | 8607 ----------------- resources/master_template/structure/rw.json | 19 - resources/master_template/structure/sg.json | 19 - resources/master_template/structure/ta.json | 19 - resources/master_template/structure/wy.json | 19 - .../master_template/table/구조물집계표.json | 1466 +-- .../master_template/구조물집계표조합.json | 1605 +-- .../fixtures/m02_master_sample}/area/c4.json | 0 .../fixtures/m02_master_sample}/area/ms.json | 0 .../m02_master_sample}/area/pp_len.json | 0 .../fixtures/m02_master_sample}/area/rv.json | 0 .../m02_master_sample}/material/c4.json | 0 .../m02_master_sample}/material/ms.json | 0 .../m02_master_sample}/material/pp_len.json | 0 .../m02_master_sample}/material/rv.json | 0 .../m02_master_sample}/structure/c4.json | 17 - .../m02_master_sample}/structure/ms.json | 0 .../m02_master_sample}/structure/pp_len.json | 0 .../m02_master_sample/structure/rv.json | 77 + .../m02_master_sample/table/구조물집계표.json | 1551 +++ .../m02_master_sample/구조물집계표조합.json | 1555 +++ .../fixtures/m02_master_sample/자재조합.json | 7 + resources/tester/helper_m02_master_sample.py | 27 + .../test_spreadsheet_saved_values.py | 8 +- resources/tester/test_item_copy_complete.py | 3 +- resources/tester/test_m02_empty_master.py | 126 + resources/tester/test_m02_item_inputs.py | 12 +- resources/tester/test_m02_table_fill.py | 6 +- resources/tester/test_m02_template_layers.py | 12 +- resources/tester/test_m02_tree.py | 12 +- .../test_structures_summary_snapshot.py | 3 +- 168 files changed, 3425 insertions(+), 14161 deletions(-) delete mode 100644 resources/master_template/area/be.json delete mode 100644 resources/master_template/area/bg.json delete mode 100644 resources/master_template/area/bm_area.json delete mode 100644 resources/master_template/area/bm_len.json delete mode 100644 resources/master_template/area/bx_len.json delete mode 100644 resources/master_template/area/bx_wing.json delete mode 100644 resources/master_template/area/c1.json delete mode 100644 resources/master_template/area/c2.json delete mode 100644 resources/master_template/area/c3.json delete mode 100644 resources/master_template/area/c5.json delete mode 100644 resources/master_template/area/cs.json delete mode 100644 resources/master_template/area/ec.json delete mode 100644 resources/master_template/area/etc.json delete mode 100644 resources/master_template/area/fb.json delete mode 100644 resources/master_template/area/fp_l.json delete mode 100644 resources/master_template/area/fp_w.json delete mode 100644 resources/master_template/area/gr_curb.json delete mode 100644 resources/master_template/area/gr_rail.json delete mode 100644 resources/master_template/area/gr_sign.json delete mode 100644 resources/master_template/area/h_break.json delete mode 100644 resources/master_template/area/h_ford_basin.json delete mode 100644 resources/master_template/area/h_intake.json delete mode 100644 resources/master_template/area/h_pg_rail.json delete mode 100644 resources/master_template/area/h_pv_rail.json delete mode 100644 resources/master_template/area/h_rip_c.json delete mode 100644 resources/master_template/area/h_rip_m.json delete mode 100644 resources/master_template/area/h_rockfall.json delete mode 100644 resources/master_template/area/h_soil_ditch.json delete mode 100644 resources/master_template/area/h_stone_bed.json delete mode 100644 resources/master_template/area/h_stone_ch.json delete mode 100644 resources/master_template/area/h_waste.json delete mode 100644 resources/master_template/area/mr.json delete mode 100644 resources/master_template/area/od_cnt.json delete mode 100644 resources/master_template/area/od_len.json delete mode 100644 resources/master_template/area/pg_in.json delete mode 100644 resources/master_template/area/pp_cp.json delete mode 100644 resources/master_template/area/ps.json delete mode 100644 resources/master_template/area/pv_a.json delete mode 100644 resources/master_template/area/pv_b.json delete mode 100644 resources/master_template/area/pv_jt.json delete mode 100644 resources/master_template/area/pv_l.json delete mode 100644 resources/master_template/area/pv_w.json delete mode 100644 resources/master_template/area/rf.json delete mode 100644 resources/master_template/area/rw.json delete mode 100644 resources/master_template/area/sg.json delete mode 100644 resources/master_template/area/ta.json delete mode 100644 resources/master_template/area/wy.json delete mode 100644 resources/master_template/material/be.json delete mode 100644 resources/master_template/material/bg.json delete mode 100644 resources/master_template/material/bm_area.json delete mode 100644 resources/master_template/material/bm_len.json delete mode 100644 resources/master_template/material/bx_len.json delete mode 100644 resources/master_template/material/bx_wing.json delete mode 100644 resources/master_template/material/c1.json delete mode 100644 resources/master_template/material/c2.json delete mode 100644 resources/master_template/material/c3.json delete mode 100644 resources/master_template/material/c5.json delete mode 100644 resources/master_template/material/cs.json delete mode 100644 resources/master_template/material/ec.json delete mode 100644 resources/master_template/material/etc.json delete mode 100644 resources/master_template/material/fb.json delete mode 100644 resources/master_template/material/fp_l.json delete mode 100644 resources/master_template/material/fp_w.json delete mode 100644 resources/master_template/material/gr_curb.json delete mode 100644 resources/master_template/material/gr_rail.json delete mode 100644 resources/master_template/material/gr_sign.json delete mode 100644 resources/master_template/material/h_break.json delete mode 100644 resources/master_template/material/h_ford_basin.json delete mode 100644 resources/master_template/material/h_intake.json delete mode 100644 resources/master_template/material/h_pg_rail.json delete mode 100644 resources/master_template/material/h_pv_rail.json delete mode 100644 resources/master_template/material/h_rip_c.json delete mode 100644 resources/master_template/material/h_rip_m.json delete mode 100644 resources/master_template/material/h_rockfall.json delete mode 100644 resources/master_template/material/h_soil_ditch.json delete mode 100644 resources/master_template/material/h_stone_bed.json delete mode 100644 resources/master_template/material/h_stone_ch.json delete mode 100644 resources/master_template/material/h_waste.json delete mode 100644 resources/master_template/material/mr.json delete mode 100644 resources/master_template/material/od_cnt.json delete mode 100644 resources/master_template/material/od_len.json delete mode 100644 resources/master_template/material/pg_in.json delete mode 100644 resources/master_template/material/pp_cp.json delete mode 100644 resources/master_template/material/ps.json delete mode 100644 resources/master_template/material/pv_a.json delete mode 100644 resources/master_template/material/pv_b.json delete mode 100644 resources/master_template/material/pv_jt.json delete mode 100644 resources/master_template/material/pv_l.json delete mode 100644 resources/master_template/material/pv_w.json delete mode 100644 resources/master_template/material/rf.json delete mode 100644 resources/master_template/material/rw.json delete mode 100644 resources/master_template/material/sg.json delete mode 100644 resources/master_template/material/ta.json delete mode 100644 resources/master_template/material/wy.json delete mode 100644 resources/master_template/structure/be.json delete mode 100644 resources/master_template/structure/bg.json delete mode 100644 resources/master_template/structure/bm_area.json delete mode 100644 resources/master_template/structure/bm_len.json delete mode 100644 resources/master_template/structure/bx_len.json delete mode 100644 resources/master_template/structure/bx_wing.json delete mode 100644 resources/master_template/structure/c1.json delete mode 100644 resources/master_template/structure/c2.json delete mode 100644 resources/master_template/structure/c3.json delete mode 100644 resources/master_template/structure/c5.json delete mode 100644 resources/master_template/structure/cs.json delete mode 100644 resources/master_template/structure/ec.json delete mode 100644 resources/master_template/structure/etc.json delete mode 100644 resources/master_template/structure/fb.json delete mode 100644 resources/master_template/structure/fp_l.json delete mode 100644 resources/master_template/structure/fp_w.json delete mode 100644 resources/master_template/structure/gr_curb.json delete mode 100644 resources/master_template/structure/gr_rail.json delete mode 100644 resources/master_template/structure/gr_sign.json delete mode 100644 resources/master_template/structure/h_break.json delete mode 100644 resources/master_template/structure/h_ford_basin.json delete mode 100644 resources/master_template/structure/h_intake.json delete mode 100644 resources/master_template/structure/h_pg_rail.json delete mode 100644 resources/master_template/structure/h_pv_rail.json delete mode 100644 resources/master_template/structure/h_rip_c.json delete mode 100644 resources/master_template/structure/h_rip_m.json delete mode 100644 resources/master_template/structure/h_rockfall.json delete mode 100644 resources/master_template/structure/h_soil_ditch.json delete mode 100644 resources/master_template/structure/h_stone_bed.json delete mode 100644 resources/master_template/structure/h_stone_ch.json delete mode 100644 resources/master_template/structure/h_waste.json delete mode 100644 resources/master_template/structure/mr.json delete mode 100644 resources/master_template/structure/od_cnt.json delete mode 100644 resources/master_template/structure/od_len.json delete mode 100644 resources/master_template/structure/pg_in.json delete mode 100644 resources/master_template/structure/pp_cp.json delete mode 100644 resources/master_template/structure/ps.json delete mode 100644 resources/master_template/structure/pv_a.json delete mode 100644 resources/master_template/structure/pv_b.json delete mode 100644 resources/master_template/structure/pv_jt.json delete mode 100644 resources/master_template/structure/pv_l.json delete mode 100644 resources/master_template/structure/pv_w.json delete mode 100644 resources/master_template/structure/rf.json delete mode 100644 resources/master_template/structure/rv.json delete mode 100644 resources/master_template/structure/rw.json delete mode 100644 resources/master_template/structure/sg.json delete mode 100644 resources/master_template/structure/ta.json delete mode 100644 resources/master_template/structure/wy.json rename resources/{master_template => tester/fixtures/m02_master_sample}/area/c4.json (100%) rename resources/{master_template => tester/fixtures/m02_master_sample}/area/ms.json (100%) rename resources/{master_template => tester/fixtures/m02_master_sample}/area/pp_len.json (100%) rename resources/{master_template => tester/fixtures/m02_master_sample}/area/rv.json (100%) rename resources/{master_template => tester/fixtures/m02_master_sample}/material/c4.json (100%) rename resources/{master_template => tester/fixtures/m02_master_sample}/material/ms.json (100%) rename resources/{master_template => tester/fixtures/m02_master_sample}/material/pp_len.json (100%) rename resources/{master_template => tester/fixtures/m02_master_sample}/material/rv.json (100%) rename resources/{master_template => tester/fixtures/m02_master_sample}/structure/c4.json (79%) rename resources/{master_template => tester/fixtures/m02_master_sample}/structure/ms.json (100%) rename resources/{master_template => tester/fixtures/m02_master_sample}/structure/pp_len.json (100%) create mode 100644 resources/tester/fixtures/m02_master_sample/structure/rv.json create mode 100644 resources/tester/fixtures/m02_master_sample/table/구조물집계표.json create mode 100644 resources/tester/fixtures/m02_master_sample/구조물집계표조합.json create mode 100644 resources/tester/fixtures/m02_master_sample/자재조합.json create mode 100644 resources/tester/helper_m02_master_sample.py create mode 100644 resources/tester/test_m02_empty_master.py diff --git a/resources/master_template/area/be.json b/resources/master_template/area/be.json deleted file mode 100644 index f0645118..00000000 --- a/resources/master_template/area/be.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "be", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA14000000" -} diff --git a/resources/master_template/area/bg.json b/resources/master_template/area/bg.json deleted file mode 100644 index 56bd14db..00000000 --- a/resources/master_template/area/bg.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "bg", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA22000000" -} diff --git a/resources/master_template/area/bm_area.json b/resources/master_template/area/bm_area.json deleted file mode 100644 index 96a58246..00000000 --- a/resources/master_template/area/bm_area.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "bm_area", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA12010200" -} diff --git a/resources/master_template/area/bm_len.json b/resources/master_template/area/bm_len.json deleted file mode 100644 index 9e49632f..00000000 --- a/resources/master_template/area/bm_len.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "bm_len", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA12010100" -} diff --git a/resources/master_template/area/bx_len.json b/resources/master_template/area/bx_len.json deleted file mode 100644 index 9d155c3e..00000000 --- a/resources/master_template/area/bx_len.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "bx_len", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA05010100" -} diff --git a/resources/master_template/area/bx_wing.json b/resources/master_template/area/bx_wing.json deleted file mode 100644 index f4133058..00000000 --- a/resources/master_template/area/bx_wing.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "bx_wing", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA05010200" -} diff --git a/resources/master_template/area/c1.json b/resources/master_template/area/c1.json deleted file mode 100644 index 78777103..00000000 --- a/resources/master_template/area/c1.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "c1", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA01010200" -} diff --git a/resources/master_template/area/c2.json b/resources/master_template/area/c2.json deleted file mode 100644 index 8e385b6c..00000000 --- a/resources/master_template/area/c2.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "c2", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA15010000" -} diff --git a/resources/master_template/area/c3.json b/resources/master_template/area/c3.json deleted file mode 100644 index 7c136cea..00000000 --- a/resources/master_template/area/c3.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "c3", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA02010600" -} diff --git a/resources/master_template/area/c5.json b/resources/master_template/area/c5.json deleted file mode 100644 index f4ecac7f..00000000 --- a/resources/master_template/area/c5.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "c5", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA01020000" -} diff --git a/resources/master_template/area/cs.json b/resources/master_template/area/cs.json deleted file mode 100644 index ad66a540..00000000 --- a/resources/master_template/area/cs.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "cs", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA24000000" -} diff --git a/resources/master_template/area/ec.json b/resources/master_template/area/ec.json deleted file mode 100644 index c2232987..00000000 --- a/resources/master_template/area/ec.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "ec", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA13010000" -} diff --git a/resources/master_template/area/etc.json b/resources/master_template/area/etc.json deleted file mode 100644 index 7a32d95f..00000000 --- a/resources/master_template/area/etc.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "etc", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA25010000" -} diff --git a/resources/master_template/area/fb.json b/resources/master_template/area/fb.json deleted file mode 100644 index 11ed8d65..00000000 --- a/resources/master_template/area/fb.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "fb", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA06010000" -} diff --git a/resources/master_template/area/fp_l.json b/resources/master_template/area/fp_l.json deleted file mode 100644 index 8826095f..00000000 --- a/resources/master_template/area/fp_l.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "fp_l", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA07010200" -} diff --git a/resources/master_template/area/fp_w.json b/resources/master_template/area/fp_w.json deleted file mode 100644 index f78fad10..00000000 --- a/resources/master_template/area/fp_w.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "fp_w", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA07010100" -} diff --git a/resources/master_template/area/gr_curb.json b/resources/master_template/area/gr_curb.json deleted file mode 100644 index 17d86c40..00000000 --- a/resources/master_template/area/gr_curb.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "gr_curb", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA19000000" -} diff --git a/resources/master_template/area/gr_rail.json b/resources/master_template/area/gr_rail.json deleted file mode 100644 index 10b65920..00000000 --- a/resources/master_template/area/gr_rail.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "gr_rail", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA18000000" -} diff --git a/resources/master_template/area/gr_sign.json b/resources/master_template/area/gr_sign.json deleted file mode 100644 index 8ad369c8..00000000 --- a/resources/master_template/area/gr_sign.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "gr_sign", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA20000000" -} diff --git a/resources/master_template/area/h_break.json b/resources/master_template/area/h_break.json deleted file mode 100644 index a15a5940..00000000 --- a/resources/master_template/area/h_break.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "h_break", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA35000000" -} diff --git a/resources/master_template/area/h_ford_basin.json b/resources/master_template/area/h_ford_basin.json deleted file mode 100644 index d59671df..00000000 --- a/resources/master_template/area/h_ford_basin.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "h_ford_basin", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA33000000" -} diff --git a/resources/master_template/area/h_intake.json b/resources/master_template/area/h_intake.json deleted file mode 100644 index dbf7ffda..00000000 --- a/resources/master_template/area/h_intake.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "h_intake", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA26000000" -} diff --git a/resources/master_template/area/h_pg_rail.json b/resources/master_template/area/h_pg_rail.json deleted file mode 100644 index d4d9fd17..00000000 --- a/resources/master_template/area/h_pg_rail.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "h_pg_rail", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA31000000" -} diff --git a/resources/master_template/area/h_pv_rail.json b/resources/master_template/area/h_pv_rail.json deleted file mode 100644 index 3802f96d..00000000 --- a/resources/master_template/area/h_pv_rail.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "h_pv_rail", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA30000000" -} diff --git a/resources/master_template/area/h_rip_c.json b/resources/master_template/area/h_rip_c.json deleted file mode 100644 index 9df8f76b..00000000 --- a/resources/master_template/area/h_rip_c.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "h_rip_c", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA27010000" -} diff --git a/resources/master_template/area/h_rip_m.json b/resources/master_template/area/h_rip_m.json deleted file mode 100644 index 6ba6f411..00000000 --- a/resources/master_template/area/h_rip_m.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "h_rip_m", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA27020000" -} diff --git a/resources/master_template/area/h_rockfall.json b/resources/master_template/area/h_rockfall.json deleted file mode 100644 index 442eafdc..00000000 --- a/resources/master_template/area/h_rockfall.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "h_rockfall", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA34000000" -} diff --git a/resources/master_template/area/h_soil_ditch.json b/resources/master_template/area/h_soil_ditch.json deleted file mode 100644 index 3e1baa18..00000000 --- a/resources/master_template/area/h_soil_ditch.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "h_soil_ditch", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA32000000" -} diff --git a/resources/master_template/area/h_stone_bed.json b/resources/master_template/area/h_stone_bed.json deleted file mode 100644 index c728639e..00000000 --- a/resources/master_template/area/h_stone_bed.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "h_stone_bed", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA29000000" -} diff --git a/resources/master_template/area/h_stone_ch.json b/resources/master_template/area/h_stone_ch.json deleted file mode 100644 index a31e6c85..00000000 --- a/resources/master_template/area/h_stone_ch.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "h_stone_ch", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA28000000" -} diff --git a/resources/master_template/area/h_waste.json b/resources/master_template/area/h_waste.json deleted file mode 100644 index daf35b00..00000000 --- a/resources/master_template/area/h_waste.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "h_waste", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA36000000" -} diff --git a/resources/master_template/area/mr.json b/resources/master_template/area/mr.json deleted file mode 100644 index d6d76327..00000000 --- a/resources/master_template/area/mr.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "mr", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA21000000" -} diff --git a/resources/master_template/area/od_cnt.json b/resources/master_template/area/od_cnt.json deleted file mode 100644 index 56e16d8a..00000000 --- a/resources/master_template/area/od_cnt.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "od_cnt", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA08010200" -} diff --git a/resources/master_template/area/od_len.json b/resources/master_template/area/od_len.json deleted file mode 100644 index e5c3362c..00000000 --- a/resources/master_template/area/od_len.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "od_len", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA08010100" -} diff --git a/resources/master_template/area/pg_in.json b/resources/master_template/area/pg_in.json deleted file mode 100644 index cccbea3e..00000000 --- a/resources/master_template/area/pg_in.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "pg_in", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "계산값": {}, - "면적번": "SA04010100" -} diff --git a/resources/master_template/area/pp_cp.json b/resources/master_template/area/pp_cp.json deleted file mode 100644 index fd7ea3e5..00000000 --- a/resources/master_template/area/pp_cp.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "pp_cp", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA03010200" -} diff --git a/resources/master_template/area/ps.json b/resources/master_template/area/ps.json deleted file mode 100644 index 13792fe2..00000000 --- a/resources/master_template/area/ps.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "ps", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA23000000" -} diff --git a/resources/master_template/area/pv_a.json b/resources/master_template/area/pv_a.json deleted file mode 100644 index a4bcc234..00000000 --- a/resources/master_template/area/pv_a.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "pv_a", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA02010400" -} diff --git a/resources/master_template/area/pv_b.json b/resources/master_template/area/pv_b.json deleted file mode 100644 index 156e750b..00000000 --- a/resources/master_template/area/pv_b.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "pv_b", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA02010100" -} diff --git a/resources/master_template/area/pv_jt.json b/resources/master_template/area/pv_jt.json deleted file mode 100644 index a2c7c9a7..00000000 --- a/resources/master_template/area/pv_jt.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "pv_jt", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA02010500" -} diff --git a/resources/master_template/area/pv_l.json b/resources/master_template/area/pv_l.json deleted file mode 100644 index 6f9760ad..00000000 --- a/resources/master_template/area/pv_l.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "pv_l", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA02010200" -} diff --git a/resources/master_template/area/pv_w.json b/resources/master_template/area/pv_w.json deleted file mode 100644 index 421d0309..00000000 --- a/resources/master_template/area/pv_w.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "pv_w", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA02010300" -} diff --git a/resources/master_template/area/rf.json b/resources/master_template/area/rf.json deleted file mode 100644 index 029e40a4..00000000 --- a/resources/master_template/area/rf.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "rf", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA15000000" -} diff --git a/resources/master_template/area/rw.json b/resources/master_template/area/rw.json deleted file mode 100644 index b2f27a4c..00000000 --- a/resources/master_template/area/rw.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "rw", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA09010100" -} diff --git a/resources/master_template/area/sg.json b/resources/master_template/area/sg.json deleted file mode 100644 index 5665034f..00000000 --- a/resources/master_template/area/sg.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "sg", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA11010100" -} diff --git a/resources/master_template/area/ta.json b/resources/master_template/area/ta.json deleted file mode 100644 index 0c8c6061..00000000 --- a/resources/master_template/area/ta.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "ta", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA17000000" -} diff --git a/resources/master_template/area/wy.json b/resources/master_template/area/wy.json deleted file mode 100644 index 69f7c3bb..00000000 --- a/resources/master_template/area/wy.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "wy", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "면적산출", - "칸": {} - } - ], - "활성": "s1", - "면적번": "SA16000000" -} diff --git a/resources/master_template/material/be.json b/resources/master_template/material/be.json deleted file mode 100644 index df25ecf7..00000000 --- a/resources/master_template/material/be.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "be", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM14000000" -} diff --git a/resources/master_template/material/bg.json b/resources/master_template/material/bg.json deleted file mode 100644 index 00a5f9cc..00000000 --- a/resources/master_template/material/bg.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "bg", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM22000000" -} diff --git a/resources/master_template/material/bm_area.json b/resources/master_template/material/bm_area.json deleted file mode 100644 index 6a4793f1..00000000 --- a/resources/master_template/material/bm_area.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "bm_area", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM12010200" -} diff --git a/resources/master_template/material/bm_len.json b/resources/master_template/material/bm_len.json deleted file mode 100644 index 25b5425f..00000000 --- a/resources/master_template/material/bm_len.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "bm_len", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM12010100" -} diff --git a/resources/master_template/material/bx_len.json b/resources/master_template/material/bx_len.json deleted file mode 100644 index 40aa7563..00000000 --- a/resources/master_template/material/bx_len.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "bx_len", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM05010100" -} diff --git a/resources/master_template/material/bx_wing.json b/resources/master_template/material/bx_wing.json deleted file mode 100644 index 22778e84..00000000 --- a/resources/master_template/material/bx_wing.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "bx_wing", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM05010200" -} diff --git a/resources/master_template/material/c1.json b/resources/master_template/material/c1.json deleted file mode 100644 index 4fa69ae5..00000000 --- a/resources/master_template/material/c1.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "c1", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM01010200" -} diff --git a/resources/master_template/material/c2.json b/resources/master_template/material/c2.json deleted file mode 100644 index f9fd0617..00000000 --- a/resources/master_template/material/c2.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "c2", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM15010000" -} diff --git a/resources/master_template/material/c3.json b/resources/master_template/material/c3.json deleted file mode 100644 index f4930d6b..00000000 --- a/resources/master_template/material/c3.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "c3", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM02010600" -} diff --git a/resources/master_template/material/c5.json b/resources/master_template/material/c5.json deleted file mode 100644 index de6f1ca5..00000000 --- a/resources/master_template/material/c5.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "c5", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM01020000" -} diff --git a/resources/master_template/material/cs.json b/resources/master_template/material/cs.json deleted file mode 100644 index d2ce8b8e..00000000 --- a/resources/master_template/material/cs.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "cs", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM24000000" -} diff --git a/resources/master_template/material/ec.json b/resources/master_template/material/ec.json deleted file mode 100644 index c8d3cc42..00000000 --- a/resources/master_template/material/ec.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "ec", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM13010000" -} diff --git a/resources/master_template/material/etc.json b/resources/master_template/material/etc.json deleted file mode 100644 index d7723dee..00000000 --- a/resources/master_template/material/etc.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "etc", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM25010000" -} diff --git a/resources/master_template/material/fb.json b/resources/master_template/material/fb.json deleted file mode 100644 index e3858056..00000000 --- a/resources/master_template/material/fb.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "fb", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM06010000" -} diff --git a/resources/master_template/material/fp_l.json b/resources/master_template/material/fp_l.json deleted file mode 100644 index 3e2f5fd2..00000000 --- a/resources/master_template/material/fp_l.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "fp_l", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM07010200" -} diff --git a/resources/master_template/material/fp_w.json b/resources/master_template/material/fp_w.json deleted file mode 100644 index ff956999..00000000 --- a/resources/master_template/material/fp_w.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "fp_w", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM07010100" -} diff --git a/resources/master_template/material/gr_curb.json b/resources/master_template/material/gr_curb.json deleted file mode 100644 index 3984f870..00000000 --- a/resources/master_template/material/gr_curb.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "gr_curb", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM19000000" -} diff --git a/resources/master_template/material/gr_rail.json b/resources/master_template/material/gr_rail.json deleted file mode 100644 index d81ab597..00000000 --- a/resources/master_template/material/gr_rail.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "gr_rail", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM18000000" -} diff --git a/resources/master_template/material/gr_sign.json b/resources/master_template/material/gr_sign.json deleted file mode 100644 index ae925b79..00000000 --- a/resources/master_template/material/gr_sign.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "gr_sign", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM20000000" -} diff --git a/resources/master_template/material/h_break.json b/resources/master_template/material/h_break.json deleted file mode 100644 index 9433a15c..00000000 --- a/resources/master_template/material/h_break.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "h_break", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM35000000" -} diff --git a/resources/master_template/material/h_ford_basin.json b/resources/master_template/material/h_ford_basin.json deleted file mode 100644 index ef3c8965..00000000 --- a/resources/master_template/material/h_ford_basin.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "h_ford_basin", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM33000000" -} diff --git a/resources/master_template/material/h_intake.json b/resources/master_template/material/h_intake.json deleted file mode 100644 index e3bf1ddf..00000000 --- a/resources/master_template/material/h_intake.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "h_intake", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM26000000" -} diff --git a/resources/master_template/material/h_pg_rail.json b/resources/master_template/material/h_pg_rail.json deleted file mode 100644 index 191bdf94..00000000 --- a/resources/master_template/material/h_pg_rail.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "h_pg_rail", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM31000000" -} diff --git a/resources/master_template/material/h_pv_rail.json b/resources/master_template/material/h_pv_rail.json deleted file mode 100644 index 597b9868..00000000 --- a/resources/master_template/material/h_pv_rail.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "h_pv_rail", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM30000000" -} diff --git a/resources/master_template/material/h_rip_c.json b/resources/master_template/material/h_rip_c.json deleted file mode 100644 index 5f7f6cf5..00000000 --- a/resources/master_template/material/h_rip_c.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "h_rip_c", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM27010000" -} diff --git a/resources/master_template/material/h_rip_m.json b/resources/master_template/material/h_rip_m.json deleted file mode 100644 index 9bd01612..00000000 --- a/resources/master_template/material/h_rip_m.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "h_rip_m", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM27020000" -} diff --git a/resources/master_template/material/h_rockfall.json b/resources/master_template/material/h_rockfall.json deleted file mode 100644 index cbeed72a..00000000 --- a/resources/master_template/material/h_rockfall.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "h_rockfall", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM34000000" -} diff --git a/resources/master_template/material/h_soil_ditch.json b/resources/master_template/material/h_soil_ditch.json deleted file mode 100644 index 56fd98ff..00000000 --- a/resources/master_template/material/h_soil_ditch.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "h_soil_ditch", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM32000000" -} diff --git a/resources/master_template/material/h_stone_bed.json b/resources/master_template/material/h_stone_bed.json deleted file mode 100644 index 1f9e4661..00000000 --- a/resources/master_template/material/h_stone_bed.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "h_stone_bed", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM29000000" -} diff --git a/resources/master_template/material/h_stone_ch.json b/resources/master_template/material/h_stone_ch.json deleted file mode 100644 index 60b74430..00000000 --- a/resources/master_template/material/h_stone_ch.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "h_stone_ch", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM28000000" -} diff --git a/resources/master_template/material/h_waste.json b/resources/master_template/material/h_waste.json deleted file mode 100644 index 8385f64f..00000000 --- a/resources/master_template/material/h_waste.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "h_waste", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM36000000" -} diff --git a/resources/master_template/material/mr.json b/resources/master_template/material/mr.json deleted file mode 100644 index 16896cf1..00000000 --- a/resources/master_template/material/mr.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "mr", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM21000000" -} diff --git a/resources/master_template/material/od_cnt.json b/resources/master_template/material/od_cnt.json deleted file mode 100644 index 08883426..00000000 --- a/resources/master_template/material/od_cnt.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "od_cnt", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM08010200" -} diff --git a/resources/master_template/material/od_len.json b/resources/master_template/material/od_len.json deleted file mode 100644 index 6f5ad0ce..00000000 --- a/resources/master_template/material/od_len.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "od_len", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM08010100" -} diff --git a/resources/master_template/material/pg_in.json b/resources/master_template/material/pg_in.json deleted file mode 100644 index 3e255921..00000000 --- a/resources/master_template/material/pg_in.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "pg_in", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM04010100" -} diff --git a/resources/master_template/material/pp_cp.json b/resources/master_template/material/pp_cp.json deleted file mode 100644 index bb59702a..00000000 --- a/resources/master_template/material/pp_cp.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "pp_cp", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM03010200" -} diff --git a/resources/master_template/material/ps.json b/resources/master_template/material/ps.json deleted file mode 100644 index 770e93d1..00000000 --- a/resources/master_template/material/ps.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "ps", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM23000000" -} diff --git a/resources/master_template/material/pv_a.json b/resources/master_template/material/pv_a.json deleted file mode 100644 index 8d10c10b..00000000 --- a/resources/master_template/material/pv_a.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "pv_a", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM02010400" -} diff --git a/resources/master_template/material/pv_b.json b/resources/master_template/material/pv_b.json deleted file mode 100644 index 426bd0de..00000000 --- a/resources/master_template/material/pv_b.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "pv_b", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM02010100" -} diff --git a/resources/master_template/material/pv_jt.json b/resources/master_template/material/pv_jt.json deleted file mode 100644 index 68199baf..00000000 --- a/resources/master_template/material/pv_jt.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "pv_jt", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM02010500" -} diff --git a/resources/master_template/material/pv_l.json b/resources/master_template/material/pv_l.json deleted file mode 100644 index 80a4f3b7..00000000 --- a/resources/master_template/material/pv_l.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "pv_l", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM02010200" -} diff --git a/resources/master_template/material/pv_w.json b/resources/master_template/material/pv_w.json deleted file mode 100644 index 28ae6384..00000000 --- a/resources/master_template/material/pv_w.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "pv_w", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM02010300" -} diff --git a/resources/master_template/material/rf.json b/resources/master_template/material/rf.json deleted file mode 100644 index fed34d35..00000000 --- a/resources/master_template/material/rf.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "rf", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM15000000" -} diff --git a/resources/master_template/material/rw.json b/resources/master_template/material/rw.json deleted file mode 100644 index b82e55ca..00000000 --- a/resources/master_template/material/rw.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "rw", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM09010100" -} diff --git a/resources/master_template/material/sg.json b/resources/master_template/material/sg.json deleted file mode 100644 index 8e0d103e..00000000 --- a/resources/master_template/material/sg.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "sg", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM11010100" -} diff --git a/resources/master_template/material/ta.json b/resources/master_template/material/ta.json deleted file mode 100644 index 47cd074f..00000000 --- a/resources/master_template/material/ta.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "ta", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM17000000" -} diff --git a/resources/master_template/material/wy.json b/resources/master_template/material/wy.json deleted file mode 100644 index 49fa89f9..00000000 --- a/resources/master_template/material/wy.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "종류": "통합문서", - "판": 1, - "열": "wy", - "서식": [ - {} - ], - "시트": [ - { - "id": "s1", - "이름": "재료산출", - "칸": {} - } - ], - "활성": "s1", - "재료번": "SM16000000" -} diff --git a/resources/master_template/structure/be.json b/resources/master_template/structure/be.json deleted file mode 100644 index f6a47681..00000000 --- a/resources/master_template/structure/be.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "be", - "도번": "SD14000000", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/bg.json b/resources/master_template/structure/bg.json deleted file mode 100644 index c0b4a331..00000000 --- a/resources/master_template/structure/bg.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "bg", - "도번": "SD22000000", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/bm_area.json b/resources/master_template/structure/bm_area.json deleted file mode 100644 index cdc120a0..00000000 --- a/resources/master_template/structure/bm_area.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "bm_area", - "도번": "SD12010200", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/bm_len.json b/resources/master_template/structure/bm_len.json deleted file mode 100644 index ff243ada..00000000 --- a/resources/master_template/structure/bm_len.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "bm_len", - "도번": "SD12010100", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/bx_len.json b/resources/master_template/structure/bx_len.json deleted file mode 100644 index f9daac64..00000000 --- a/resources/master_template/structure/bx_len.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "bx_len", - "도번": "SD05010100", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/bx_wing.json b/resources/master_template/structure/bx_wing.json deleted file mode 100644 index bc90cd8a..00000000 --- a/resources/master_template/structure/bx_wing.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "bx_wing", - "도번": "SD05010200", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/c1.json b/resources/master_template/structure/c1.json deleted file mode 100644 index 8ecd69fa..00000000 --- a/resources/master_template/structure/c1.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "c1", - "도번": "SD01010200", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/c2.json b/resources/master_template/structure/c2.json deleted file mode 100644 index e6635882..00000000 --- a/resources/master_template/structure/c2.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "c2", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - }, - "도번": "SD15010000" -} diff --git a/resources/master_template/structure/c3.json b/resources/master_template/structure/c3.json deleted file mode 100644 index ff1fc685..00000000 --- a/resources/master_template/structure/c3.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "c3", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - }, - "도번": "SD02010600" -} diff --git a/resources/master_template/structure/c5.json b/resources/master_template/structure/c5.json deleted file mode 100644 index 30288930..00000000 --- a/resources/master_template/structure/c5.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "c5", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - }, - "도번": "SD01020000" -} diff --git a/resources/master_template/structure/cs.json b/resources/master_template/structure/cs.json deleted file mode 100644 index a9d3278b..00000000 --- a/resources/master_template/structure/cs.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "cs", - "도번": "SD24000000", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/ec.json b/resources/master_template/structure/ec.json deleted file mode 100644 index 4711b287..00000000 --- a/resources/master_template/structure/ec.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "ec", - "도번": "SD13010000", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/etc.json b/resources/master_template/structure/etc.json deleted file mode 100644 index ab833bcc..00000000 --- a/resources/master_template/structure/etc.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "etc", - "도번": "SD25010000", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/fb.json b/resources/master_template/structure/fb.json deleted file mode 100644 index 43c0f089..00000000 --- a/resources/master_template/structure/fb.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "fb", - "도번": "SD06010000", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/fp_l.json b/resources/master_template/structure/fp_l.json deleted file mode 100644 index c07285da..00000000 --- a/resources/master_template/structure/fp_l.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "fp_l", - "도번": "SD07010200", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/fp_w.json b/resources/master_template/structure/fp_w.json deleted file mode 100644 index c66c6c24..00000000 --- a/resources/master_template/structure/fp_w.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "fp_w", - "도번": "SD07010100", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/gr_curb.json b/resources/master_template/structure/gr_curb.json deleted file mode 100644 index 85e8179c..00000000 --- a/resources/master_template/structure/gr_curb.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "gr_curb", - "도번": "SD19000000", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/gr_rail.json b/resources/master_template/structure/gr_rail.json deleted file mode 100644 index dedda075..00000000 --- a/resources/master_template/structure/gr_rail.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "gr_rail", - "도번": "SD18000000", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/gr_sign.json b/resources/master_template/structure/gr_sign.json deleted file mode 100644 index e9127130..00000000 --- a/resources/master_template/structure/gr_sign.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "gr_sign", - "도번": "SD20000000", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/h_break.json b/resources/master_template/structure/h_break.json deleted file mode 100644 index 9dc97923..00000000 --- a/resources/master_template/structure/h_break.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "h_break", - "도번": "SD35000000", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/h_ford_basin.json b/resources/master_template/structure/h_ford_basin.json deleted file mode 100644 index 39a965a3..00000000 --- a/resources/master_template/structure/h_ford_basin.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "h_ford_basin", - "도번": "SD33000000", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/h_intake.json b/resources/master_template/structure/h_intake.json deleted file mode 100644 index ae614748..00000000 --- a/resources/master_template/structure/h_intake.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "h_intake", - "도번": "SD26000000", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/h_pg_rail.json b/resources/master_template/structure/h_pg_rail.json deleted file mode 100644 index 58769402..00000000 --- a/resources/master_template/structure/h_pg_rail.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "h_pg_rail", - "도번": "SD31000000", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/h_pv_rail.json b/resources/master_template/structure/h_pv_rail.json deleted file mode 100644 index e05d7700..00000000 --- a/resources/master_template/structure/h_pv_rail.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "h_pv_rail", - "도번": "SD30000000", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/h_rip_c.json b/resources/master_template/structure/h_rip_c.json deleted file mode 100644 index 761a230d..00000000 --- a/resources/master_template/structure/h_rip_c.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "h_rip_c", - "도번": "SD27010000", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/h_rip_m.json b/resources/master_template/structure/h_rip_m.json deleted file mode 100644 index f85a0856..00000000 --- a/resources/master_template/structure/h_rip_m.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "h_rip_m", - "도번": "SD27020000", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/h_rockfall.json b/resources/master_template/structure/h_rockfall.json deleted file mode 100644 index e6b6c639..00000000 --- a/resources/master_template/structure/h_rockfall.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "h_rockfall", - "도번": "SD34000000", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/h_soil_ditch.json b/resources/master_template/structure/h_soil_ditch.json deleted file mode 100644 index 1c738722..00000000 --- a/resources/master_template/structure/h_soil_ditch.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "h_soil_ditch", - "도번": "SD32000000", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/h_stone_bed.json b/resources/master_template/structure/h_stone_bed.json deleted file mode 100644 index 4457364a..00000000 --- a/resources/master_template/structure/h_stone_bed.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "h_stone_bed", - "도번": "SD29000000", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/h_stone_ch.json b/resources/master_template/structure/h_stone_ch.json deleted file mode 100644 index 8d25f5e4..00000000 --- a/resources/master_template/structure/h_stone_ch.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "h_stone_ch", - "도번": "SD28000000", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/h_waste.json b/resources/master_template/structure/h_waste.json deleted file mode 100644 index aae8bfe5..00000000 --- a/resources/master_template/structure/h_waste.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "h_waste", - "도번": "SD36000000", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/mr.json b/resources/master_template/structure/mr.json deleted file mode 100644 index e91b5e6e..00000000 --- a/resources/master_template/structure/mr.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "mr", - "도번": "SD21000000", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/od_cnt.json b/resources/master_template/structure/od_cnt.json deleted file mode 100644 index 0d4db232..00000000 --- a/resources/master_template/structure/od_cnt.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "od_cnt", - "도번": "SD08010200", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/od_len.json b/resources/master_template/structure/od_len.json deleted file mode 100644 index 50cac0ad..00000000 --- a/resources/master_template/structure/od_len.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "od_len", - "도번": "SD08010100", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/pg_in.json b/resources/master_template/structure/pg_in.json deleted file mode 100644 index 5353795b..00000000 --- a/resources/master_template/structure/pg_in.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "pg_in", - "도번": "SD04010100", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/pp_cp.json b/resources/master_template/structure/pp_cp.json deleted file mode 100644 index 9130fff7..00000000 --- a/resources/master_template/structure/pp_cp.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "pp_cp", - "도번": "SD03010200", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/ps.json b/resources/master_template/structure/ps.json deleted file mode 100644 index 2b263baa..00000000 --- a/resources/master_template/structure/ps.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "ps", - "도번": "SD23000000", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/pv_a.json b/resources/master_template/structure/pv_a.json deleted file mode 100644 index 3c39c9b3..00000000 --- a/resources/master_template/structure/pv_a.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "pv_a", - "도번": "SD02010400", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/pv_b.json b/resources/master_template/structure/pv_b.json deleted file mode 100644 index 0fff4180..00000000 --- a/resources/master_template/structure/pv_b.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "pv_b", - "도번": "SD02010100", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/pv_jt.json b/resources/master_template/structure/pv_jt.json deleted file mode 100644 index cd9014fd..00000000 --- a/resources/master_template/structure/pv_jt.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "pv_jt", - "도번": "SD02010500", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/pv_l.json b/resources/master_template/structure/pv_l.json deleted file mode 100644 index 569202c5..00000000 --- a/resources/master_template/structure/pv_l.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "pv_l", - "도번": "SD02010200", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/pv_w.json b/resources/master_template/structure/pv_w.json deleted file mode 100644 index 37a5f2f2..00000000 --- a/resources/master_template/structure/pv_w.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "pv_w", - "도번": "SD02010300", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/rf.json b/resources/master_template/structure/rf.json deleted file mode 100644 index 92ada00a..00000000 --- a/resources/master_template/structure/rf.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "rf", - "도번": "SD15000000", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/rv.json b/resources/master_template/structure/rv.json deleted file mode 100644 index 5309d172..00000000 --- a/resources/master_template/structure/rv.json +++ /dev/null @@ -1,8607 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "rv", - "도번": "SD01010100", - "도면": { - "format": 6, - "entities": [ - { - "id": "6f01250e-97dd-4e69-8743-8ae8968b7cea", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4255.194526991341, - "y": 5857.308300811681 - }, - "endPoint": { - "x": 5342.438965520298, - "y": 5857.308300811681 - } - } - }, - { - "id": "408cfd22-a8e3-46da-ae11-d2b50fc97b79", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5842.438965520298, - "y": 5357.308300811681 - }, - "endPoint": { - "x": 5342.438965520298, - "y": 5857.308300811681 - } - } - }, - { - "id": "1865b0b5-c1f2-45ce-8999-05a5deb4bc90", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5842.438965520298, - "y": 5357.308300811681 - }, - "endPoint": { - "x": 6842.438965520298, - "y": 5357.308300811681 - } - } - }, - { - "id": "932f3805-3e58-4fae-9da6-a93363a89247", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8429.683404049312, - "y": 5857.308300811681 - }, - "endPoint": { - "x": 7342.438965520298, - "y": 5857.308300811681 - } - } - }, - { - "id": "62193954-c151-4a21-9ca6-4ce107436498", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 6842.438965520298, - "y": 5357.308300811681 - }, - "endPoint": { - "x": 7342.438965520298, - "y": 5857.308300811681 - } - } - }, - { - "id": "0b7d3991-82a1-4b9d-9c75-e916d1a31b40", - "type": "PolyLine", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": null, - "children": [ - { - "id": "65794e9d-5b53-464f-a1df-3a4de45dd75f", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5842.438965520298, - "y": 5357.308300811681 - }, - "endPoint": { - "x": 5591.823079775961, - "y": 5424.866589938145 - } - } - }, - { - "id": "11329597-4879-4dfd-aca3-70328906ac3f", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5591.823079775961, - "y": 5424.866589938145 - }, - "endPoint": { - "x": 5466.777693556971, - "y": 5459.159273368976 - } - } - }, - { - "id": "e6e6ca5d-bf7f-485f-9d19-e2a9b12f17d6", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5466.777693556971, - "y": 5459.159273368976 - }, - "endPoint": { - "x": 5342.047375321272, - "y": 5494.068203440722 - } - } - }, - { - "id": "d7acba10-0df3-460c-b081-43e0cb5d2980", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5342.047375321272, - "y": 5494.068203440722 - }, - "endPoint": { - "x": 5217.737147730368, - "y": 5529.798795700539 - } - } - }, - { - "id": "a36e1d4f-5de8-4330-b99d-5da4355b5d8d", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5217.737147730368, - "y": 5529.798795700539 - }, - "endPoint": { - "x": 5093.952033445414, - "y": 5566.556465695379 - } - } - }, - { - "id": "0b0b2916-2141-426f-a5fa-728c435510a9", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5093.952033445414, - "y": 5566.556465695379 - }, - "endPoint": { - "x": 4970.797055127507, - "y": 5604.546628972224 - } - } - }, - { - "id": "6348f941-11ac-4363-a67e-1651465b714a", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4970.797055127507, - "y": 5604.546628972224 - }, - "endPoint": { - "x": 4909.488686537836, - "y": 5624.068087949825 - } - } - }, - { - "id": "66954b3f-da4d-4279-ae6e-35dc58525b75", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4909.488686537836, - "y": 5624.068087949825 - }, - "endPoint": { - "x": 4848.37723543786, - "y": 5643.974701078056 - } - } - }, - { - "id": "3b547c5e-5ef8-48d4-b5ca-2909a32a2a5c", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4848.37723543786, - "y": 5643.974701078056 - }, - "endPoint": { - "x": 4735.789354965789, - "y": 5681.869331932277 - } - } - }, - { - "id": "fc65637b-275a-42dc-978d-c309cbf1a6ae", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4735.789354965789, - "y": 5681.869331932277 - }, - "endPoint": { - "x": 4623.708044749044, - "y": 5721.0299459485395 - } - } - }, - { - "id": "75bbaf08-13d4-49e9-9877-6a2f68a6ac8c", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4623.708044749044, - "y": 5721.0299459485395 - }, - "endPoint": { - "x": 4511.92040639068, - "y": 5761.315305741504 - } - } - }, - { - "id": "a3437c82-bf95-43d5-8136-e78b52f39e24", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4511.92040639068, - "y": 5761.315305741504 - }, - "endPoint": { - "x": 4400.213541493809, - "y": 5802.584173926152 - } - } - }, - { - "id": "34d55f2e-7f7e-4e53-a281-f8ef485e8e03", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4400.213541493809, - "y": 5802.584173926152 - }, - "endPoint": { - "x": 4288.374551661662, - "y": 5844.695313117409 - } - } - }, - { - "id": "f0629236-ea65-41c5-bcb6-c8f20840f9d4", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4288.374551661662, - "y": 5844.695313117409 - }, - "endPoint": { - "x": 4176.190538497409, - "y": 5887.507485930051 - } - } - }, - { - "id": "b644421e-ff37-4352-a39d-a4219c575a16", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4176.190538497409, - "y": 5887.507485930051 - }, - "endPoint": { - "x": 3949.9358485848643, - "y": 5974.66998287887 - } - } - }, - { - "id": "52929190-4537-4338-ade5-d998b357e28d", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3949.9358485848643, - "y": 5974.66998287887 - }, - "endPoint": { - "x": 3853.7918536433717, - "y": 6011.631169061206 - } - } - }, - { - "id": "a00e4521-837f-408b-9dbe-0c2a48e5d45a", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3853.7918536433717, - "y": 6011.631169061206 - }, - "endPoint": { - "x": 3757.015221629583, - "y": 6048.277403224143 - } - } - }, - { - "id": "945b05d2-7321-4366-912a-517fa86dc0ea", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3757.015221629583, - "y": 6048.277403224143 - }, - "endPoint": { - "x": 3659.6684827833087, - "y": 6084.0977577585145 - } - } - }, - { - "id": "27b00e87-c4d3-4d51-a29c-4d3b2c569ec7", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3659.6684827833087, - "y": 6084.0977577585145 - }, - "endPoint": { - "x": 3610.800863997778, - "y": 6101.53856528725 - } - } - }, - { - "id": "f610f547-7944-4c90-8f23-c440dfdbbc36", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3610.800863997778, - "y": 6101.53856528725 - }, - "endPoint": { - "x": 3561.814167343662, - "y": 6118.581305055239 - } - } - }, - { - "id": "fab9d022-03b3-4ee1-96ca-7f605c0584c7", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3561.814167343662, - "y": 6118.581305055239 - }, - "endPoint": { - "x": 3512.716209101025, - "y": 6135.162111111422 - } - } - }, - { - "id": "b3250a34-a092-41a3-b0e1-72a33ad6799f", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3512.716209101025, - "y": 6135.162111111422 - }, - "endPoint": { - "x": 3463.514805549872, - "y": 6151.217117504799 - } - } - }, - { - "id": "8a432e1b-2529-4071-913f-11ab2056c5a3", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3463.514805549872, - "y": 6151.217117504799 - }, - "endPoint": { - "x": 3414.2177729698597, - "y": 6166.682458284049 - } - } - }, - { - "id": "5fe28196-212b-44ce-b88e-40dd86b2f3ee", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3414.2177729698597, - "y": 6166.682458284049 - }, - "endPoint": { - "x": 3364.832927641226, - "y": 6181.494267498114 - } - } - }, - { - "id": "9854a0a2-49ed-43ca-be56-b077094d511c", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3364.832927641226, - "y": 6181.494267498114 - }, - "endPoint": { - "x": 3315.3680858436273, - "y": 6195.588679195731 - } - } - }, - { - "id": "f25559da-32ef-40a7-90e5-c0047234e82d", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3315.3680858436273, - "y": 6195.588679195731 - }, - "endPoint": { - "x": 3265.8310638570692, - "y": 6208.901827425812 - } - } - }, - { - "id": "3da510d7-87c3-4773-95ee-a966a0f68c40", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3265.8310638570692, - "y": 6208.901827425812 - }, - "endPoint": { - "x": 3216.2296779614408, - "y": 6221.369846237241 - } - } - }, - { - "id": "e6b6051d-6e85-4cf9-8583-056be3b16d54", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3216.2296779614408, - "y": 6221.369846237241 - }, - "endPoint": { - "x": 3166.5717444367474, - "y": 6232.928869678784 - } - } - } - ] - }, - { - "id": "6a39c14b-b853-40e2-b131-bda6d54c17c5", - "type": "PolyLine", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": null, - "children": [ - { - "id": "8df34d07-ed1a-4530-8eb9-fd53ee933c90", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3166.5717444367474, - "y": 6232.9288696788135 - }, - "endPoint": { - "x": 3091.6470452183858, - "y": 6252.517181223113 - } - } - }, - { - "id": "c407486d-97a3-41aa-bb17-96f0a1ea3da8", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3091.6470452183858, - "y": 6252.517181223113 - }, - "endPoint": { - "x": 3016.7527688441915, - "y": 6272.509237524238 - } - } - }, - { - "id": "d73908fe-0ec2-477e-9ace-882decf6c29d", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3016.7527688441915, - "y": 6272.509237524238 - }, - "endPoint": { - "x": 2941.9193381582736, - "y": 6293.308783338842 - } - } - }, - { - "id": "70e4d2d0-7c04-4783-b9cd-d5d0fe388666", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2941.9193381582736, - "y": 6293.308783338842 - }, - "endPoint": { - "x": 2867.177176004916, - "y": 6315.31956342404 - } - } - }, - { - "id": "2d4daef7-9ac0-47e8-a5a3-b9b0765bee78", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2867.177176004916, - "y": 6315.31956342404 - }, - "endPoint": { - "x": 2803.685612837202, - "y": 6335.209821868193 - } - } - }, - { - "id": "2fd210ce-5e00-4a8e-bd77-9ab8367b6929", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2803.685612837202, - "y": 6335.209821868193 - }, - "endPoint": { - "x": 2740.257035291812, - "y": 6355.986764791596 - } - } - }, - { - "id": "4ba6fe3a-21e7-4d61-a30c-10b9cf64397d", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2740.257035291812, - "y": 6355.986764791596 - }, - "endPoint": { - "x": 2613.488559634716, - "y": 6399.073094457388 - } - } - }, - { - "id": "e22d3b02-a0cb-40c2-a3d1-638dd1b2fa7e", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2613.488559634716, - "y": 6399.073094457388 - }, - "endPoint": { - "x": 2486.6711941683316, - "y": 6442.323333184671 - } - } - }, - { - "id": "6cc9d8d6-c6cd-4cc6-875a-502592481fd2", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2486.6711941683316, - "y": 6442.323333184671 - }, - "endPoint": { - "x": 2423.1815043610404, - "y": 6463.305162434786 - } - } - }, - { - "id": "af9f404a-a3e4-48b8-ae1e-9a1e374145ca", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2423.1815043610404, - "y": 6463.305162434786 - }, - "endPoint": { - "x": 2359.604384026723, - "y": 6483.482261736353 - } - } - }, - { - "id": "f7dcdecc-d893-452d-bb44-bca9f8366cf8", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2359.604384026723, - "y": 6483.482261736353 - }, - "endPoint": { - "x": 2295.914763807319, - "y": 6502.5727286848705 - } - } - }, - { - "id": "c82f2edd-1321-46df-b8e2-65bc234e0ba3", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2295.914763807319, - "y": 6502.5727286848705 - }, - "endPoint": { - "x": 2232.0875743444776, - "y": 6520.294660875632 - } - } - }, - { - "id": "cf1c50fb-7e0b-4e50-9910-e15bd04e1ba0", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2232.0875743444776, - "y": 6520.294660875632 - }, - "endPoint": { - "x": 2200.114556972403, - "y": 6528.554331935418 - } - } - }, - { - "id": "94a2641f-32d3-4435-9c8f-c6d3db7718e3", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2200.114556972403, - "y": 6528.554331935418 - }, - "endPoint": { - "x": 2168.0977462801384, - "y": 6536.3661559040775 - } - } - }, - { - "id": "36c63965-56fa-498e-95de-2ce3de4b259d", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2168.0977462801384, - "y": 6536.3661559040775 - }, - "endPoint": { - "x": 2136.034008597955, - "y": 6543.694894980959 - } - } - }, - { - "id": "d1f00357-a2a9-4903-a612-1db711d3172b", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2136.034008597955, - "y": 6543.694894980959 - }, - "endPoint": { - "x": 2103.920210256125, - "y": 6550.505311365676 - } - } - }, - { - "id": "b667f287-668d-4be4-8019-24e6ad0858f8", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2103.920210256125, - "y": 6550.505311365676 - }, - "endPoint": { - "x": 2071.7532175846864, - "y": 6556.762167257373 - } - } - }, - { - "id": "9e423955-01e3-4594-8509-de674e2cc4e1", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2071.7532175846864, - "y": 6556.762167257373 - }, - "endPoint": { - "x": 2039.5298969139694, - "y": 6562.430224855605 - } - } - }, - { - "id": "216070ad-ac12-4b75-9342-c2c716441ab1", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2039.5298969139694, - "y": 6562.430224855605 - }, - "endPoint": { - "x": 2007.2471145743039, - "y": 6567.474246359809 - } - } - }, - { - "id": "10ab7817-0f83-4d45-81eb-074500c024be", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2007.2471145743039, - "y": 6567.474246359809 - }, - "endPoint": { - "x": 1974.9017368958448, - "y": 6571.85899396945 - } - } - }, - { - "id": "8c3ddf57-96e7-450d-8d1a-e01020a8320e", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 1974.9017368958448, - "y": 6571.85899396945 - }, - "endPoint": { - "x": 1942.490630208631, - "y": 6575.5492298837635 - } - } - }, - { - "id": "30e4baae-de4e-481c-b373-90190b27fb6e", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 1942.490630208631, - "y": 6575.5492298837635 - }, - "endPoint": { - "x": 1910.0106608432252, - "y": 6578.5097163024475 - } - } - }, - { - "id": "6f60f9bf-ed86-4a33-908b-af70d54bbed6", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 1910.0106608432252, - "y": 6578.5097163024475 - }, - "endPoint": { - "x": 1877.4586951296078, - "y": 6580.705215424678 - } - } - }, - { - "id": "ef7484de-a786-4265-bb54-114d36f26c86", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 1877.4586951296078, - "y": 6580.705215424678 - }, - "endPoint": { - "x": 1844.8315993981087, - "y": 6582.100489450037 - } - } - }, - { - "id": "67f84fd1-64b7-48be-a4f3-1b5f51e8bbe0", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 1844.8315993981087, - "y": 6582.100489450037 - }, - "endPoint": { - "x": 1799.8553089759662, - "y": 6582.6958551862335 - } - } - }, - { - "id": "81ff0ea7-abbf-429e-a772-e861dd7add44", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 1799.8553089759662, - "y": 6582.6958551862335 - }, - "endPoint": { - "x": 1754.7496043867432, - "y": 6581.909727944818 - } - } - }, - { - "id": "62cd247f-2225-44db-b4c4-71e5d9eee978", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 1754.7496043867432, - "y": 6581.909727944818 - }, - "endPoint": { - "x": 1709.5329733688268, - "y": 6579.93946386539 - } - } - }, - { - "id": "dacc28df-d679-42d9-a49c-6ee291fbd878", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 1709.5329733688268, - "y": 6579.93946386539 - }, - "endPoint": { - "x": 1664.2239036601386, - "y": 6576.982419087581 - } - } - }, - { - "id": "ab3cddd0-ea9c-43e1-90de-5066f43a02aa", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 1664.2239036601386, - "y": 6576.982419087581 - }, - "endPoint": { - "x": 1618.8408829987748, - "y": 6573.235949751106 - } - } - }, - { - "id": "a4f84b97-ec0c-4835-ba94-dfb72c451034", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 1618.8408829987748, - "y": 6573.235949751106 - }, - "endPoint": { - "x": 1573.4023991230642, - "y": 6568.897411995655 - } - } - }, - { - "id": "6683364e-60bd-4205-b542-4ae783407797", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 1573.4023991230642, - "y": 6568.897411995655 - }, - "endPoint": { - "x": 1482.4329926809296, - "y": 6559.23355578637 - } - } - } - ] - }, - { - "id": "1ea6e70d-7768-4722-9b75-7b22199347a1", - "type": "PolyLine", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": null, - "children": [ - { - "id": "c1985971-c3a2-4f7c-a9ff-b0d70c2615c6", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9518.306186604023, - "y": 6232.928869678872 - }, - "endPoint": { - "x": 9593.230885822384, - "y": 6252.517181223142 - } - } - }, - { - "id": "74329f49-075e-4e05-8a09-90b2e3fec9c6", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9593.230885822384, - "y": 6252.517181223142 - }, - "endPoint": { - "x": 9668.125162196578, - "y": 6272.509237524151 - } - } - }, - { - "id": "a3b0f661-d6fd-402e-8608-ee4727711be3", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9668.125162196578, - "y": 6272.509237524151 - }, - "endPoint": { - "x": 9742.958592882438, - "y": 6293.3087833389 - } - } - }, - { - "id": "131d95f5-51e9-4180-93c8-fc77d8ca2fe6", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9742.958592882438, - "y": 6293.3087833389 - }, - "endPoint": { - "x": 9817.700755035796, - "y": 6315.319563424011 - } - } - }, - { - "id": "e559b71e-924c-4fe9-86c6-762d62b8d25d", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9817.700755035796, - "y": 6315.319563424011 - }, - "endPoint": { - "x": 9881.192318203452, - "y": 6335.209821868106 - } - } - }, - { - "id": "89a198ea-c619-4e7b-8253-2a371353e685", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9881.192318203452, - "y": 6335.209821868106 - }, - "endPoint": { - "x": 9944.620895749016, - "y": 6355.986764791596 - } - } - }, - { - "id": "a6aa8515-4f9e-4c2b-afe6-d6a9d9a4d800", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9944.620895749016, - "y": 6355.986764791596 - }, - "endPoint": { - "x": 10071.389371406054, - "y": 6399.073094457388 - } - } - }, - { - "id": "859ca37a-3835-4ea9-b6b7-c3aade3af25c", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10071.389371406054, - "y": 6399.073094457388 - }, - "endPoint": { - "x": 10198.206736872497, - "y": 6442.323333184642 - } - } - }, - { - "id": "2a536408-5c91-4ff5-b0d1-abc2215ae35a", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10198.206736872497, - "y": 6442.323333184642 - }, - "endPoint": { - "x": 10261.696426679788, - "y": 6463.305162434728 - } - } - }, - { - "id": "deab5431-4493-4e4e-97d9-24844f426a21", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10261.696426679788, - "y": 6463.305162434728 - }, - "endPoint": { - "x": 10325.273547014047, - "y": 6483.482261736324 - } - } - }, - { - "id": "666029a4-3936-4e2e-9466-9300db75a8c5", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10325.273547014047, - "y": 6483.482261736324 - }, - "endPoint": { - "x": 10388.96316723345, - "y": 6502.572728684812 - } - } - }, - { - "id": "481c8bb7-d67c-4608-97e5-107f3aa936f7", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10388.96316723345, - "y": 6502.572728684812 - }, - "endPoint": { - "x": 10452.790356696234, - "y": 6520.294660875603 - } - } - }, - { - "id": "5ef1b1e2-5a21-4d19-9977-431187028c33", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10452.790356696234, - "y": 6520.294660875603 - }, - "endPoint": { - "x": 10484.763374068367, - "y": 6528.554331935447 - } - } - }, - { - "id": "bcad4077-c8ca-46c4-a8f5-87f062f02370", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10484.763374068367, - "y": 6528.554331935447 - }, - "endPoint": { - "x": 10516.780184760515, - "y": 6536.366155904019 - } - } - }, - { - "id": "fa99aaed-f2cf-4220-bbd1-c9e69cde17d1", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10516.780184760515, - "y": 6536.366155904019 - }, - "endPoint": { - "x": 10548.843922442757, - "y": 6543.694894981018 - } - } - }, - { - "id": "0a96c15a-15c4-4f86-ad1d-28187872fc11", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10548.843922442757, - "y": 6543.694894981018 - }, - "endPoint": { - "x": 10580.957720784703, - "y": 6550.505311365647 - } - } - }, - { - "id": "b8161913-3e51-4554-a4d8-7122ac714ad5", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10580.957720784703, - "y": 6550.505311365647 - }, - "endPoint": { - "x": 10613.124713456084, - "y": 6556.762167257344 - } - } - }, - { - "id": "233a4bf7-1af2-4598-aa9a-6547890c68ee", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10613.124713456084, - "y": 6556.762167257344 - }, - "endPoint": { - "x": 10645.3480341268, - "y": 6562.430224855605 - } - } - }, - { - "id": "4ec211ef-ec24-4d31-b2ef-3377e7628bcd", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10645.3480341268, - "y": 6562.430224855605 - }, - "endPoint": { - "x": 10677.630816466524, - "y": 6567.474246359838 - } - } - }, - { - "id": "200fc3c6-fc8c-494c-8eec-237001d76cdf", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10677.630816466524, - "y": 6567.474246359838 - }, - "endPoint": { - "x": 10709.976194144983, - "y": 6571.858993969392 - } - } - }, - { - "id": "95c8ee1f-6406-4105-b1e0-5c882c0d95f6", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10709.976194144983, - "y": 6571.858993969392 - }, - "endPoint": { - "x": 10742.38730083208, - "y": 6575.549229883822 - } - } - }, - { - "id": "2991ce71-828a-44a7-8cf1-e06ad200c61f", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10742.38730083208, - "y": 6575.549229883822 - }, - "endPoint": { - "x": 10774.867270197603, - "y": 6578.509716302506 - } - } - }, - { - "id": "09184757-9f51-4514-9d73-b3d5a4ab48e7", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10774.867270197603, - "y": 6578.509716302506 - }, - "endPoint": { - "x": 10807.419235911162, - "y": 6580.705215424707 - } - } - }, - { - "id": "53c9fe12-290f-4aff-8f1e-1708dc46f16b", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10807.419235911162, - "y": 6580.705215424707 - }, - "endPoint": { - "x": 10840.04633164272, - "y": 6582.100489450095 - } - } - }, - { - "id": "540feaa0-d127-4309-8c66-3fefd7db9b1e", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10840.04633164272, - "y": 6582.100489450095 - }, - "endPoint": { - "x": 10885.022622064804, - "y": 6582.69585518635 - } - } - }, - { - "id": "5b045a71-bee4-4729-b414-0895eece7250", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10885.022622064804, - "y": 6582.69585518635 - }, - "endPoint": { - "x": 10930.128326653969, - "y": 6581.909727944934 - } - } - }, - { - "id": "d3fb8299-040e-4e01-9efd-ba324fc1b374", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10930.128326653969, - "y": 6581.909727944934 - }, - "endPoint": { - "x": 10975.344957671885, - "y": 6579.939463865419 - } - } - }, - { - "id": "b0f49bc0-c851-4ff5-ab2c-0627f3e6953f", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10975.344957671885, - "y": 6579.939463865419 - }, - "endPoint": { - "x": 11020.654027380631, - "y": 6576.982419087668 - } - } - }, - { - "id": "494721fd-c673-44c8-9f9b-e0ec86128289", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 11020.654027380631, - "y": 6576.982419087668 - }, - "endPoint": { - "x": 11066.037048041937, - "y": 6573.235949751193 - } - } - }, - { - "id": "01deddec-8f1f-4774-b103-bfd78fd6d417", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 11066.037048041937, - "y": 6573.235949751193 - }, - "endPoint": { - "x": 11111.475531917647, - "y": 6568.897411995713 - } - } - }, - { - "id": "95f1b9e2-0e75-47f1-92a4-00d666624f47", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 11111.475531917647, - "y": 6568.897411995713 - }, - "endPoint": { - "x": 11202.44493835984, - "y": 6559.233555786399 - } - } - } - ] - }, - { - "id": "891f1d77-5414-49cf-9f51-ee4105f1d8d6", - "type": "PolyLine", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": null, - "children": [ - { - "id": "f7eafcd5-0452-4734-956e-3d611ea34f7a", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 6842.438965520298, - "y": 5357.308300811681 - }, - "endPoint": { - "x": 7093.054851264518, - "y": 5424.866589938145 - } - } - }, - { - "id": "6b44bc5d-c730-4235-a35e-33fd6d2fd4e3", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7093.054851264518, - "y": 5424.866589938145 - }, - "endPoint": { - "x": 7218.100237483799, - "y": 5459.159273368976 - } - } - }, - { - "id": "660faf16-b815-451b-954a-56ad0d907063", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7218.100237483799, - "y": 5459.159273368976 - }, - "endPoint": { - "x": 7342.8305557193235, - "y": 5494.068203440722 - } - } - }, - { - "id": "dcb49652-94f0-43b3-9165-192409675069", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7342.8305557193235, - "y": 5494.068203440722 - }, - "endPoint": { - "x": 7467.140783310228, - "y": 5529.798795700539 - } - } - }, - { - "id": "55d35119-7e74-40b3-9882-222e6f915b6e", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7467.140783310228, - "y": 5529.798795700539 - }, - "endPoint": { - "x": 7590.92589759524, - "y": 5566.556465695379 - } - } - }, - { - "id": "63c6d7be-b2fc-4bc0-ba89-e64787e9647a", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7590.92589759524, - "y": 5566.556465695379 - }, - "endPoint": { - "x": 7714.080875913147, - "y": 5604.546628972224 - } - } - }, - { - "id": "7aec1cce-aca1-46c4-bbfa-7f23bcbc063c", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7714.080875913147, - "y": 5604.546628972224 - }, - "endPoint": { - "x": 7775.389244502818, - "y": 5624.068087949825 - } - } - }, - { - "id": "a9dbe888-8aa2-4426-8cfb-b905205c0beb", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7775.389244502818, - "y": 5624.068087949825 - }, - "endPoint": { - "x": 7836.500695602677, - "y": 5643.974701078056 - } - } - }, - { - "id": "82a002ff-2cfb-49b6-a97f-2b86815c4f9e", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7836.500695602677, - "y": 5643.974701078056 - }, - "endPoint": { - "x": 7949.088576074748, - "y": 5681.869331932277 - } - } - }, - { - "id": "dad2626d-8ce9-4f4e-849b-0094a923e45a", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7949.088576074748, - "y": 5681.869331932277 - }, - "endPoint": { - "x": 8061.169886291551, - "y": 5721.0299459485395 - } - } - }, - { - "id": "410b251e-782e-4de0-a63e-238ed0da6295", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8061.169886291551, - "y": 5721.0299459485395 - }, - "endPoint": { - "x": 8172.957524649915, - "y": 5761.315305741504 - } - } - }, - { - "id": "43859008-17cf-4f6b-9ce5-f26b21d64c55", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8172.957524649915, - "y": 5761.315305741504 - }, - "endPoint": { - "x": 8284.664389546728, - "y": 5802.584173926152 - } - } - }, - { - "id": "d0bd27d7-6e09-49c5-a988-230a9bdbeec3", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8284.664389546728, - "y": 5802.584173926152 - }, - "endPoint": { - "x": 8396.503379378817, - "y": 5844.695313117409 - } - } - }, - { - "id": "38da250b-dd6b-47fc-a9d6-e8daa67c6bd1", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8396.503379378817, - "y": 5844.695313117409 - }, - "endPoint": { - "x": 8508.687392543245, - "y": 5887.507485930051 - } - } - }, - { - "id": "c515b3fe-dcde-4bcb-a33f-31aae9c12275", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8508.687392543245, - "y": 5887.507485930051 - }, - "endPoint": { - "x": 8734.94208245579, - "y": 5974.66998287887 - } - } - }, - { - "id": "2fc9fe55-a771-432b-8988-32bc5320fecd", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8734.94208245579, - "y": 5974.66998287887 - }, - "endPoint": { - "x": 8831.086077397456, - "y": 6011.631169061206 - } - } - }, - { - "id": "d4fac64f-c8aa-4cd6-8fa6-934ac7385c8c", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8831.086077397456, - "y": 6011.631169061206 - }, - "endPoint": { - "x": 8927.862709410954, - "y": 6048.277403224143 - } - } - }, - { - "id": "f600f7e8-3902-4123-9378-c984e7838bfd", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8927.862709410954, - "y": 6048.277403224143 - }, - "endPoint": { - "x": 9025.209448257228, - "y": 6084.0977577585145 - } - } - }, - { - "id": "418751a6-927c-474d-b9b8-fce27667fc87", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9025.209448257228, - "y": 6084.0977577585145 - }, - "endPoint": { - "x": 9074.077067042992, - "y": 6101.53856528725 - } - } - }, - { - "id": "41fe7d3a-0873-4696-abfa-777228e23679", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9074.077067042992, - "y": 6101.53856528725 - }, - "endPoint": { - "x": 9123.063763696991, - "y": 6118.581305055239 - } - } - }, - { - "id": "a1278d9b-bc51-4f0f-aa7c-acd53f128ebc", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9123.063763696991, - "y": 6118.581305055239 - }, - "endPoint": { - "x": 9172.16172193957, - "y": 6135.162111111422 - } - } - }, - { - "id": "4661a342-4c65-433a-ab83-a89bc2c93946", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9172.16172193957, - "y": 6135.162111111422 - }, - "endPoint": { - "x": 9221.363125490781, - "y": 6151.217117504799 - } - } - }, - { - "id": "7c25d6ec-a730-43e9-b6a6-c5b803123cf0", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9221.363125490781, - "y": 6151.217117504799 - }, - "endPoint": { - "x": 9270.660158070736, - "y": 6166.682458284049 - } - } - }, - { - "id": "e091e203-3955-429f-bf93-9b1ae6cb7770", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9270.660158070736, - "y": 6166.682458284049 - }, - "endPoint": { - "x": 9320.045003399428, - "y": 6181.494267498114 - } - } - }, - { - "id": "63b20fed-6604-41e0-b59b-e8334c6d8e76", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9320.045003399428, - "y": 6181.494267498114 - }, - "endPoint": { - "x": 9369.509845196968, - "y": 6195.588679195731 - } - } - }, - { - "id": "9b43bdee-2b8a-461f-9ecc-e14a4731ae15", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9369.509845196968, - "y": 6195.588679195731 - }, - "endPoint": { - "x": 9419.046867183526, - "y": 6208.901827425812 - } - } - }, - { - "id": "977562eb-b2b7-481f-83c7-e41891736e5f", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9419.046867183526, - "y": 6208.901827425812 - }, - "endPoint": { - "x": 9468.648253079155, - "y": 6221.369846237241 - } - } - }, - { - "id": "a86e37a2-be85-4d1f-9af8-f23c614917eb", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9468.648253079155, - "y": 6221.369846237241 - }, - "endPoint": { - "x": 9518.306186603906, - "y": 6232.928869678784 - } - } - } - ] - }, - { - "id": "2db3a5d3-3411-44f5-baee-80d88ff6625a", - "type": "PolyLine", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": null, - "children": [ - { - "id": "e4fef742-369d-4ce8-adb1-7e79e7bcca95", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3166.5717444367474, - "y": 6032.928869678726 - }, - "endPoint": { - "x": 3091.6470452183858, - "y": 6052.517181223026 - } - } - }, - { - "id": "205391cc-c7e3-44be-976c-a78ed686c835", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3091.6470452183858, - "y": 6052.517181223026 - }, - "endPoint": { - "x": 3016.7527688441915, - "y": 6072.50923752418 - } - } - }, - { - "id": "9c03c9f0-6d50-44e0-927f-f103ed150601", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3016.7527688441915, - "y": 6072.50923752418 - }, - "endPoint": { - "x": 2941.9193381582736, - "y": 6093.308783338813 - } - } - }, - { - "id": "a2f65234-507d-4c5d-9957-5a4920b2dccf", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2941.9193381582736, - "y": 6093.308783338813 - }, - "endPoint": { - "x": 2867.177176004916, - "y": 6115.319563423982 - } - } - }, - { - "id": "b62abcea-f6c1-4a8c-ba36-faac9c104d0a", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2867.177176004916, - "y": 6115.319563423982 - }, - "endPoint": { - "x": 2803.685612837202, - "y": 6135.209821868077 - } - } - }, - { - "id": "7621badd-3c51-4460-a231-d86dd04ae73a", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2803.685612837202, - "y": 6135.209821868077 - }, - "endPoint": { - "x": 2740.257035291812, - "y": 6155.98676479148 - } - } - }, - { - "id": "82fd3e94-fb1d-433e-9149-bdffbf4533ac", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2740.257035291812, - "y": 6155.98676479148 - }, - "endPoint": { - "x": 2613.488559634716, - "y": 6199.073094457359 - } - } - }, - { - "id": "a9f2d56a-d4b2-48d2-a349-2ba0a77128f9", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2613.488559634716, - "y": 6199.073094457359 - }, - "endPoint": { - "x": 2486.6711941683316, - "y": 6242.323333184613 - } - } - }, - { - "id": "99e71570-4007-40ad-bc79-385d9f7f5eaa", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2486.6711941683316, - "y": 6242.323333184613 - }, - "endPoint": { - "x": 2423.1815043610404, - "y": 6263.305162434728 - } - } - }, - { - "id": "2565ae06-fce6-476f-b9e4-03c65ba3df57", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2423.1815043610404, - "y": 6263.305162434728 - }, - "endPoint": { - "x": 2359.604384026723, - "y": 6283.482261736324 - } - } - }, - { - "id": "eb27c4f2-c5f8-4348-bc89-a5232cf5efe7", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2359.604384026723, - "y": 6283.482261736324 - }, - "endPoint": { - "x": 2295.914763807319, - "y": 6302.572728684783 - } - } - }, - { - "id": "0009828d-def1-45b4-940a-9cd9eeaf3e39", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2295.914763807319, - "y": 6302.572728684783 - }, - "endPoint": { - "x": 2232.0875743444776, - "y": 6320.294660875574 - } - } - }, - { - "id": "db03dd23-3905-42db-bf92-874d0a6dcf98", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2232.0875743444776, - "y": 6320.294660875574 - }, - "endPoint": { - "x": 2200.114556972403, - "y": 6328.554331935389 - } - } - }, - { - "id": "cc73a647-d026-4899-b03f-abd89308c926", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2200.114556972403, - "y": 6328.554331935389 - }, - "endPoint": { - "x": 2168.0977462801384, - "y": 6336.366155904019 - } - } - }, - { - "id": "9cb98170-7218-4c96-94b1-84fbd9afa7fb", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2168.0977462801384, - "y": 6336.366155904019 - }, - "endPoint": { - "x": 2136.034008597955, - "y": 6343.694894980901 - } - } - }, - { - "id": "605854d8-a774-4ffa-8ed0-be379ae31592", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2136.034008597955, - "y": 6343.694894980901 - }, - "endPoint": { - "x": 2103.920210256125, - "y": 6350.505311365589 - } - } - }, - { - "id": "a426fc74-1407-4da4-b0fc-d92ba9f9ae91", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2103.920210256125, - "y": 6350.505311365589 - }, - "endPoint": { - "x": 2071.7532175846864, - "y": 6356.762167257286 - } - } - }, - { - "id": "9f019def-22b3-49b2-90cd-18704efa4f01", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2071.7532175846864, - "y": 6356.762167257286 - }, - "endPoint": { - "x": 2039.5298969139694, - "y": 6362.430224855547 - } - } - }, - { - "id": "bd8805f7-9cac-4b2d-9911-34eacc7ae102", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2039.5298969139694, - "y": 6362.430224855547 - }, - "endPoint": { - "x": 2007.2471145743039, - "y": 6367.474246359721 - } - } - }, - { - "id": "310b7537-ca21-4154-874e-b0f6d7dcb896", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 2007.2471145743039, - "y": 6367.474246359721 - }, - "endPoint": { - "x": 1974.9017368958448, - "y": 6371.858993969334 - } - } - }, - { - "id": "45561b37-8b18-49fa-9cf7-5bf73252d318", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 1974.9017368958448, - "y": 6371.858993969334 - }, - "endPoint": { - "x": 1942.490630208631, - "y": 6375.549229883705 - } - } - }, - { - "id": "09fabc27-4463-4d4a-ac3c-e318584ed14e", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 1942.490630208631, - "y": 6375.549229883705 - }, - "endPoint": { - "x": 1910.0106608432252, - "y": 6378.50971630236 - } - } - }, - { - "id": "661f58e3-7d5a-431d-a6ad-37ae1f1e42df", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 1910.0106608432252, - "y": 6378.50971630236 - }, - "endPoint": { - "x": 1877.4586951296078, - "y": 6380.70521542459 - } - } - }, - { - "id": "84d0f81f-cc61-41a2-9bde-e7e95b09e061", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 1877.4586951296078, - "y": 6380.70521542459 - }, - "endPoint": { - "x": 1844.8315993981087, - "y": 6382.100489449949 - } - } - }, - { - "id": "80b82fe5-c8ce-45a6-944b-3904a5f2893f", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 1844.8315993981087, - "y": 6382.100489449949 - }, - "endPoint": { - "x": 1799.8553089759662, - "y": 6382.695855186175 - } - } - }, - { - "id": "1e866fed-7d50-44e3-afc0-9bcf5e01b487", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 1799.8553089759662, - "y": 6382.695855186175 - }, - "endPoint": { - "x": 1754.7496043867432, - "y": 6381.909727944731 - } - } - }, - { - "id": "9cb10094-3052-4dcb-8331-5ee007c07523", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 1754.7496043867432, - "y": 6381.909727944731 - }, - "endPoint": { - "x": 1709.5329733688268, - "y": 6379.939463865332 - } - } - }, - { - "id": "79a93ae7-2449-47f6-ac9e-62bc9bb2980f", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 1709.5329733688268, - "y": 6379.939463865332 - }, - "endPoint": { - "x": 1664.2239036601386, - "y": 6376.982419087522 - } - } - }, - { - "id": "6c95801e-1136-45f2-b832-64c378d84b98", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 1664.2239036601386, - "y": 6376.982419087522 - }, - "endPoint": { - "x": 1618.8408829987748, - "y": 6373.235949751106 - } - } - }, - { - "id": "3914d873-5259-44b2-b1bb-b237884f8824", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 1618.8408829987748, - "y": 6373.235949751106 - }, - "endPoint": { - "x": 1573.4023991230642, - "y": 6368.897411995655 - } - } - }, - { - "id": "db51a264-36fe-431a-8aa4-052e1601ba3c", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 1573.4023991230642, - "y": 6368.897411995655 - }, - "endPoint": { - "x": 1482.4329926809296, - "y": 6359.23355578637 - } - } - } - ] - }, - { - "id": "0493c8d2-72be-49c0-80e6-b49be130880d", - "type": "PolyLine", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": null, - "children": [ - { - "id": "61fb5f35-1177-4322-a8ca-dc3d37e1674e", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5842.438965520298, - "y": 5157.308300811681 - }, - "endPoint": { - "x": 5591.823079775961, - "y": 5224.866589938174 - } - } - }, - { - "id": "a6df22e8-e6b3-4fb6-a582-ba0b01139c11", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5591.823079775961, - "y": 5224.866589938174 - }, - "endPoint": { - "x": 5466.777693556971, - "y": 5259.159273369005 - } - } - }, - { - "id": "18297b86-18dd-4df0-9187-335a7d3ec6d4", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5466.777693556971, - "y": 5259.159273369005 - }, - "endPoint": { - "x": 5342.047375321272, - "y": 5294.068203440751 - } - } - }, - { - "id": "6ec540ca-1a33-4da5-bec4-a8cec18ff015", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5342.047375321272, - "y": 5294.068203440751 - }, - "endPoint": { - "x": 5217.737147730368, - "y": 5329.798795700539 - } - } - }, - { - "id": "ca827cd2-e7c8-49d4-8ddb-2ef08a05af40", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5217.737147730368, - "y": 5329.798795700539 - }, - "endPoint": { - "x": 5093.952033445414, - "y": 5366.556465695379 - } - } - }, - { - "id": "a7d5682a-9982-4b7a-b8c2-5cb09a8cf567", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5093.952033445414, - "y": 5366.556465695379 - }, - "endPoint": { - "x": 4970.797055127507, - "y": 5404.546628972224 - } - } - }, - { - "id": "4dcc9947-dc59-459d-9f70-d2ae42f74d4f", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4970.797055127507, - "y": 5404.546628972224 - }, - "endPoint": { - "x": 4909.488686537836, - "y": 5424.068087949825 - } - } - }, - { - "id": "75209cb4-34d2-41b8-ac0c-d634093dd4ee", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4909.488686537836, - "y": 5424.068087949825 - }, - "endPoint": { - "x": 4848.37723543786, - "y": 5443.974701078027 - } - } - }, - { - "id": "47243ec6-be74-4600-8425-b22362dfb3c3", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4848.37723543786, - "y": 5443.974701078027 - }, - "endPoint": { - "x": 4735.789354965789, - "y": 5481.869331932248 - } - } - }, - { - "id": "501dbee6-6edc-432b-aa1c-d56f589804a9", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4735.789354965789, - "y": 5481.869331932248 - }, - "endPoint": { - "x": 4623.708044749044, - "y": 5521.029945948481 - } - } - }, - { - "id": "18d7c357-9f14-4ec3-ad23-cd1243e7fce6", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4623.708044749044, - "y": 5521.029945948481 - }, - "endPoint": { - "x": 4511.92040639068, - "y": 5561.315305741475 - } - } - }, - { - "id": "ccd6e4c8-cdb5-4328-aaae-0ceacebdda46", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4511.92040639068, - "y": 5561.315305741475 - }, - "endPoint": { - "x": 4400.213541493809, - "y": 5602.5841739262105 - } - } - }, - { - "id": "c96ce8a6-04fe-4a52-aa56-755a0dd4c5ec", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4400.213541493809, - "y": 5602.5841739262105 - }, - "endPoint": { - "x": 4288.374551661662, - "y": 5644.695313117438 - } - } - }, - { - "id": "8c093d67-f1cc-4e3c-a3ad-ca7511f717a5", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4288.374551661662, - "y": 5644.695313117438 - }, - "endPoint": { - "x": 4176.190538497409, - "y": 5687.507485930051 - } - } - }, - { - "id": "591b01e2-4671-4c05-a9c3-50f67b49bac1", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4176.190538497409, - "y": 5687.507485930051 - }, - "endPoint": { - "x": 3949.9358485848643, - "y": 5774.66998287887 - } - } - }, - { - "id": "0a9f251a-c60e-4d22-b925-0fa9c12312bc", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3949.9358485848643, - "y": 5774.66998287887 - }, - "endPoint": { - "x": 3853.7918536433717, - "y": 5811.631169061206 - } - } - }, - { - "id": "cb41ba6c-dd1a-4ebb-baf1-21ad6311cdfc", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3853.7918536433717, - "y": 5811.631169061206 - }, - "endPoint": { - "x": 3757.015221629583, - "y": 5848.277403224143 - } - } - }, - { - "id": "e73238c8-ad25-4830-bd86-8d4025f33966", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3757.015221629583, - "y": 5848.277403224143 - }, - "endPoint": { - "x": 3659.6684827833087, - "y": 5884.097757758544 - } - } - }, - { - "id": "72fadaaf-4ffa-41f5-b498-360c891d74cc", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3659.6684827833087, - "y": 5884.097757758544 - }, - "endPoint": { - "x": 3610.800863997778, - "y": 5901.53856528725 - } - } - }, - { - "id": "8d4aead1-9544-43d8-9ff4-b406cc41d4e4", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3610.800863997778, - "y": 5901.53856528725 - }, - "endPoint": { - "x": 3561.814167343662, - "y": 5918.581305055239 - } - } - }, - { - "id": "498dda01-04a7-4a5a-abf8-5d6183c12c5a", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3561.814167343662, - "y": 5918.581305055239 - }, - "endPoint": { - "x": 3512.716209101025, - "y": 5935.162111111451 - } - } - }, - { - "id": "4df3800a-502a-436d-a397-9359fba8211e", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3512.716209101025, - "y": 5935.162111111451 - }, - "endPoint": { - "x": 3463.514805549872, - "y": 5951.217117504799 - } - } - }, - { - "id": "4b63f389-6810-43d1-a4b0-b926a3b4a0a2", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3463.514805549872, - "y": 5951.217117504799 - }, - "endPoint": { - "x": 3414.2177729698597, - "y": 5966.68245828402 - } - } - }, - { - "id": "85049377-6820-4fbd-b39f-190e41e0b88d", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3414.2177729698597, - "y": 5966.68245828402 - }, - "endPoint": { - "x": 3364.832927641226, - "y": 5981.494267498114 - } - } - }, - { - "id": "99c00506-1403-4543-b56d-90ef4bea3e5d", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3364.832927641226, - "y": 5981.494267498114 - }, - "endPoint": { - "x": 3315.3680858436273, - "y": 5995.588679195731 - } - } - }, - { - "id": "cc514f5f-0902-426f-8e21-e9b1af2e8ef5", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3315.3680858436273, - "y": 5995.588679195731 - }, - "endPoint": { - "x": 3265.8310638570692, - "y": 6008.901827425812 - } - } - }, - { - "id": "24133d55-3675-4adb-b5d1-b4fff8cc3177", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3265.8310638570692, - "y": 6008.901827425812 - }, - "endPoint": { - "x": 3216.2296779614408, - "y": 6021.369846237241 - } - } - }, - { - "id": "2d979ed0-ac82-46aa-8e79-49bacfeebf06", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3216.2296779614408, - "y": 6021.369846237241 - }, - "endPoint": { - "x": 3166.5717444367474, - "y": 6032.928869678784 - } - } - } - ] - }, - { - "id": "6613e650-bd6b-4423-9eec-e75a0ae8d573", - "type": "PolyLine", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": null, - "children": [ - { - "id": "2cfaba01-2f65-4603-8024-22881fd231ce", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 6842.438965520298, - "y": 5157.308300811681 - }, - "endPoint": { - "x": 7093.054851264518, - "y": 5224.866589938174 - } - } - }, - { - "id": "e60fa548-2cee-4d2b-820f-9a151cf8f9ad", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7093.054851264518, - "y": 5224.866589938174 - }, - "endPoint": { - "x": 7218.100237483799, - "y": 5259.159273369005 - } - } - }, - { - "id": "74856566-50cb-49d6-afa7-36241e3e9d44", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7218.100237483799, - "y": 5259.159273369005 - }, - "endPoint": { - "x": 7342.8305557193235, - "y": 5294.068203440751 - } - } - }, - { - "id": "603d5e3b-0f85-445e-8c18-87486f8186a1", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7342.8305557193235, - "y": 5294.068203440751 - }, - "endPoint": { - "x": 7467.140783310228, - "y": 5329.798795700539 - } - } - }, - { - "id": "b56646b8-e4b4-4827-b89d-5f49ba2914d4", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7467.140783310228, - "y": 5329.798795700539 - }, - "endPoint": { - "x": 7590.92589759524, - "y": 5366.556465695379 - } - } - }, - { - "id": "873b38a0-9a72-46c9-aa93-8894bc41d207", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7590.92589759524, - "y": 5366.556465695379 - }, - "endPoint": { - "x": 7714.080875913147, - "y": 5404.546628972224 - } - } - }, - { - "id": "dcd77925-72c9-491c-a50e-a3dadeea5793", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7714.080875913147, - "y": 5404.546628972224 - }, - "endPoint": { - "x": 7775.389244502818, - "y": 5424.068087949825 - } - } - }, - { - "id": "4fbd718c-9a9d-4739-a9c9-19d7a74cd1c9", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7775.389244502818, - "y": 5424.068087949825 - }, - "endPoint": { - "x": 7836.500695602677, - "y": 5443.974701078027 - } - } - }, - { - "id": "d25bc27e-1082-46bc-9f2d-e4fbf6018dcd", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7836.500695602677, - "y": 5443.974701078027 - }, - "endPoint": { - "x": 7949.088576074748, - "y": 5481.869331932248 - } - } - }, - { - "id": "8ed746c5-b4ab-4b72-8fae-b3f650f3fdd8", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7949.088576074748, - "y": 5481.869331932248 - }, - "endPoint": { - "x": 8061.169886291551, - "y": 5521.029945948481 - } - } - }, - { - "id": "95ef8545-e4d8-400b-8ad5-3d1ed10b3726", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8061.169886291551, - "y": 5521.029945948481 - }, - "endPoint": { - "x": 8172.957524649915, - "y": 5561.315305741475 - } - } - }, - { - "id": "865c7ebf-81ac-4060-b520-b4565f584bd1", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8172.957524649915, - "y": 5561.315305741475 - }, - "endPoint": { - "x": 8284.664389546728, - "y": 5602.5841739262105 - } - } - }, - { - "id": "4fc353ea-15cb-40df-9618-85e5b158b007", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8284.664389546728, - "y": 5602.5841739262105 - }, - "endPoint": { - "x": 8396.503379378817, - "y": 5644.695313117438 - } - } - }, - { - "id": "d9d05f71-e1d9-424f-8e8a-5df3f2f7216f", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8396.503379378817, - "y": 5644.695313117438 - }, - "endPoint": { - "x": 8508.687392543245, - "y": 5687.507485930051 - } - } - }, - { - "id": "af8bdf53-54a0-4b14-a0d7-459e63142e60", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8508.687392543245, - "y": 5687.507485930051 - }, - "endPoint": { - "x": 8734.94208245579, - "y": 5774.66998287887 - } - } - }, - { - "id": "1451f219-72f7-433a-a4a0-f9c0ed2ea43c", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8734.94208245579, - "y": 5774.66998287887 - }, - "endPoint": { - "x": 8831.086077397456, - "y": 5811.631169061206 - } - } - }, - { - "id": "a6715785-ad41-413f-a4f9-b59508609781", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8831.086077397456, - "y": 5811.631169061206 - }, - "endPoint": { - "x": 8927.862709410954, - "y": 5848.277403224143 - } - } - }, - { - "id": "f72f8fa4-76bc-45d2-b307-7b07b3159a84", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8927.862709410954, - "y": 5848.277403224143 - }, - "endPoint": { - "x": 9025.209448257228, - "y": 5884.097757758544 - } - } - }, - { - "id": "17ea9727-b06c-42d0-a8c0-ecc0e9a87e75", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9025.209448257228, - "y": 5884.097757758544 - }, - "endPoint": { - "x": 9074.077067042992, - "y": 5901.53856528725 - } - } - }, - { - "id": "1ed071b8-dea4-4cab-a341-3df068f852ab", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9074.077067042992, - "y": 5901.53856528725 - }, - "endPoint": { - "x": 9123.063763696991, - "y": 5918.581305055239 - } - } - }, - { - "id": "d743f25c-bc8d-43e2-a0d2-ff55ae6810be", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9123.063763696991, - "y": 5918.581305055239 - }, - "endPoint": { - "x": 9172.16172193957, - "y": 5935.162111111451 - } - } - }, - { - "id": "16fe2e99-bf74-487d-a775-e751cf261033", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9172.16172193957, - "y": 5935.162111111451 - }, - "endPoint": { - "x": 9221.363125490781, - "y": 5951.217117504799 - } - } - }, - { - "id": "0bab9fb9-6d5c-4344-b943-59a2bdad00f1", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9221.363125490781, - "y": 5951.217117504799 - }, - "endPoint": { - "x": 9270.660158070736, - "y": 5966.68245828402 - } - } - }, - { - "id": "07a41a23-2997-4f55-b517-38a8ecb68f5d", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9270.660158070736, - "y": 5966.68245828402 - }, - "endPoint": { - "x": 9320.045003399428, - "y": 5981.494267498114 - } - } - }, - { - "id": "e899f368-8a3b-4ea4-9ac1-d29bf0d5e0e1", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9320.045003399428, - "y": 5981.494267498114 - }, - "endPoint": { - "x": 9369.509845196968, - "y": 5995.588679195731 - } - } - }, - { - "id": "754802f2-81dd-4172-9a35-3c68e988a5cc", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9369.509845196968, - "y": 5995.588679195731 - }, - "endPoint": { - "x": 9419.046867183526, - "y": 6008.901827425812 - } - } - }, - { - "id": "7c6fc6d1-d66d-4c42-892d-c8f4dabbff7d", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9419.046867183526, - "y": 6008.901827425812 - }, - "endPoint": { - "x": 9468.648253079155, - "y": 6021.369846237241 - } - } - }, - { - "id": "a3c055e2-a75e-4c49-a25e-66ba049075dc", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9468.648253079155, - "y": 6021.369846237241 - }, - "endPoint": { - "x": 9518.306186603906, - "y": 6032.928869678784 - } - } - } - ] - }, - { - "id": "51e13f38-8fdf-417d-bf7a-956e7ee8cecf", - "type": "PolyLine", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": null, - "children": [ - { - "id": "a0beae9a-e064-4668-8695-ec038019c48b", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9518.306186604023, - "y": 6032.9288696788135 - }, - "endPoint": { - "x": 9593.230885822384, - "y": 6052.517181223084 - } - } - }, - { - "id": "b230dd44-7a43-4b3f-a0e0-60324da08b47", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9593.230885822384, - "y": 6052.517181223084 - }, - "endPoint": { - "x": 9668.125162196578, - "y": 6072.50923752418 - } - } - }, - { - "id": "43da10e4-c72a-47fd-9d82-80e1123ab319", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9668.125162196578, - "y": 6072.50923752418 - }, - "endPoint": { - "x": 9742.958592882438, - "y": 6093.308783338842 - } - } - }, - { - "id": "506727f7-1d12-4168-947f-0a8140065450", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9742.958592882438, - "y": 6093.308783338842 - }, - "endPoint": { - "x": 9817.700755035796, - "y": 6115.319563423982 - } - } - }, - { - "id": "b0bfe535-5ef8-4d2c-b78b-848872572106", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9817.700755035796, - "y": 6115.319563423982 - }, - "endPoint": { - "x": 9881.192318203452, - "y": 6135.209821868077 - } - } - }, - { - "id": "270bc327-cf76-4c88-b8ca-bff292123aa5", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9881.192318203452, - "y": 6135.209821868077 - }, - "endPoint": { - "x": 9944.620895749016, - "y": 6155.986764791567 - } - } - }, - { - "id": "a851b004-1dd7-4197-afc2-eb5660588417", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9944.620895749016, - "y": 6155.986764791567 - }, - "endPoint": { - "x": 10071.389371406054, - "y": 6199.073094457388 - } - } - }, - { - "id": "e6cd9078-3eb4-420a-9697-2655879469e8", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10071.389371406054, - "y": 6199.073094457388 - }, - "endPoint": { - "x": 10198.206736872497, - "y": 6242.323333184555 - } - } - }, - { - "id": "ca797213-c81a-4283-bc8c-96e53bffb56f", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10198.206736872497, - "y": 6242.323333184555 - }, - "endPoint": { - "x": 10261.696426679788, - "y": 6263.305162434641 - } - } - }, - { - "id": "ed8b31a2-c491-46c4-8b7d-a10bb49d19df", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10261.696426679788, - "y": 6263.305162434641 - }, - "endPoint": { - "x": 10325.273547014047, - "y": 6283.482261736266 - } - } - }, - { - "id": "54c7f546-91d7-41e8-b67d-6e13f341aab0", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10325.273547014047, - "y": 6283.482261736266 - }, - "endPoint": { - "x": 10388.96316723345, - "y": 6302.572728684754 - } - } - }, - { - "id": "147d28cc-734e-494a-9824-d504caaf2b84", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10388.96316723345, - "y": 6302.572728684754 - }, - "endPoint": { - "x": 10452.790356696234, - "y": 6320.294660875516 - } - } - }, - { - "id": "b4121ae1-d894-48f1-bb38-43ffc8cbef70", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10452.790356696234, - "y": 6320.294660875516 - }, - "endPoint": { - "x": 10484.763374068367, - "y": 6328.55433193536 - } - } - }, - { - "id": "d85d8c9f-2633-4e27-b43f-8220b34e7ee8", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10484.763374068367, - "y": 6328.55433193536 - }, - "endPoint": { - "x": 10516.780184760515, - "y": 6336.366155903932 - } - } - }, - { - "id": "634abf90-5fd4-40ba-abe3-ca09d24cfcca", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10516.780184760515, - "y": 6336.366155903932 - }, - "endPoint": { - "x": 10548.843922442757, - "y": 6343.694894980872 - } - } - }, - { - "id": "b3aae130-a692-4ae7-a88b-cfb311f7bb95", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10548.843922442757, - "y": 6343.694894980872 - }, - "endPoint": { - "x": 10580.957720784703, - "y": 6350.50531136553 - } - } - }, - { - "id": "7411508e-d24a-405e-a07c-a100eaca389a", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10580.957720784703, - "y": 6350.50531136553 - }, - "endPoint": { - "x": 10613.124713456084, - "y": 6356.762167257199 - } - } - }, - { - "id": "7b4c5492-7104-4dc8-a5e7-a0c87edb1b76", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10613.124713456084, - "y": 6356.762167257199 - }, - "endPoint": { - "x": 10645.3480341268, - "y": 6362.430224855489 - } - } - }, - { - "id": "49277d76-e3f3-44d9-a77f-c905756efc9d", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10645.3480341268, - "y": 6362.430224855489 - }, - "endPoint": { - "x": 10677.630816466524, - "y": 6367.47424635975 - } - } - }, - { - "id": "cdd70da2-090d-4f93-834e-ede9f0b6ffd5", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10677.630816466524, - "y": 6367.47424635975 - }, - "endPoint": { - "x": 10709.976194144983, - "y": 6371.858993969276 - } - } - }, - { - "id": "dfb27082-fe2f-4fba-9eba-844d8e6cc2de", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10709.976194144983, - "y": 6371.858993969276 - }, - "endPoint": { - "x": 10742.38730083208, - "y": 6375.549229883705 - } - } - }, - { - "id": "d3b91b07-6cb9-49c5-afa7-00cb48dd353a", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10742.38730083208, - "y": 6375.549229883705 - }, - "endPoint": { - "x": 10774.867270197603, - "y": 6378.509716302389 - } - } - }, - { - "id": "32f8adc2-a513-43da-98bb-28e07fd1d5e2", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10774.867270197603, - "y": 6378.509716302389 - }, - "endPoint": { - "x": 10807.419235911162, - "y": 6380.705215424619 - } - } - }, - { - "id": "256b2f8c-dd7a-4ad3-a660-ed28be2a40de", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10807.419235911162, - "y": 6380.705215424619 - }, - "endPoint": { - "x": 10840.04633164272, - "y": 6382.100489449978 - } - } - }, - { - "id": "abbf9d59-1a6b-40e0-9341-ef3f7b7da2f3", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10840.04633164272, - "y": 6382.100489449978 - }, - "endPoint": { - "x": 10885.022622064804, - "y": 6382.6958551862335 - } - } - }, - { - "id": "e9968625-270a-4013-a38b-0b79d2dff7b2", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10885.022622064804, - "y": 6382.6958551862335 - }, - "endPoint": { - "x": 10930.128326653969, - "y": 6381.909727944847 - } - } - }, - { - "id": "ae7dfb70-aeef-4fae-86af-93e51d7b6018", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10930.128326653969, - "y": 6381.909727944847 - }, - "endPoint": { - "x": 10975.344957671885, - "y": 6379.939463865361 - } - } - }, - { - "id": "a2cfd6f0-1584-445d-9d46-dbade8c184ab", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 10975.344957671885, - "y": 6379.939463865361 - }, - "endPoint": { - "x": 11020.654027380631, - "y": 6376.98241908761 - } - } - }, - { - "id": "05ca586d-f782-4915-a21a-1235126b6150", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 11020.654027380631, - "y": 6376.98241908761 - }, - "endPoint": { - "x": 11066.037048041937, - "y": 6373.235949751164 - } - } - }, - { - "id": "30c264ec-b4ad-4cd2-809e-6e16481b29ad", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 11066.037048041937, - "y": 6373.235949751164 - }, - "endPoint": { - "x": 11111.475531917647, - "y": 6368.897411995684 - } - } - }, - { - "id": "004c8a30-b84a-4769-b323-e52da1049013", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 11111.475531917647, - "y": 6368.897411995684 - }, - "endPoint": { - "x": 11202.44493835984, - "y": 6359.233555786399 - } - } - } - ] - }, - { - "id": "c7cbf289-395d-423d-9ae5-4af01b6b6c38", - "type": "PolyLine", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": null, - "children": [ - { - "id": "139fc5ed-de83-4fe2-a439-c59c46821de0", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 6842.438965520356, - "y": 5157.30830081171 - }, - "endPoint": { - "x": 6728.038622369175, - "y": 5147.741626025032 - } - } - }, - { - "id": "5d0b88be-85e8-4480-8da3-b1e5620d8da8", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 6728.038622369175, - "y": 5147.741626025032 - }, - "endPoint": { - "x": 6670.836869562278, - "y": 5143.2973554337805 - } - } - }, - { - "id": "8bd9d7db-3461-48e8-9293-dbfa5a8bb137", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 6670.836869562278, - "y": 5143.2973554337805 - }, - "endPoint": { - "x": 6613.633219278185, - "y": 5139.259965005243 - } - } - }, - { - "id": "9746a22f-211b-4779-8e5c-45f03600f875", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 6613.633219278185, - "y": 5139.259965005243 - }, - "endPoint": { - "x": 6556.427039024071, - "y": 5135.765081459947 - } - } - }, - { - "id": "3d5b22d8-88ec-4484-8701-1ecac2752336", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 6556.427039024071, - "y": 5135.765081459947 - }, - "endPoint": { - "x": 6499.217696307576, - "y": 5132.948331519088 - } - } - }, - { - "id": "4a73405a-f38e-4310-82a5-e9d691c0550e", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 6499.217696307576, - "y": 5132.948331519088 - }, - "endPoint": { - "x": 6442.004558636225, - "y": 5130.945341903309 - } - } - }, - { - "id": "1e96e9df-b385-4f56-ba41-39ac9add0f4e", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 6442.004558636225, - "y": 5130.945341903309 - }, - "endPoint": { - "x": 6384.786993517657, - "y": 5129.891739333543 - } - } - }, - { - "id": "aae51d4e-a672-4025-9fb0-fb4726f4c3c7", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 6384.786993517657, - "y": 5129.891739333543 - }, - "endPoint": { - "x": 6317.00901858724, - "y": 5129.988985007134 - } - } - }, - { - "id": "08e20bea-6c65-408f-8bce-4b6ac1e91316", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 6317.00901858724, - "y": 5129.988985007134 - }, - "endPoint": { - "x": 6249.2248322292, - "y": 5131.418160485249 - } - } - }, - { - "id": "6c6bf01f-a1f7-4e86-aad1-c83a3f112f91", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 6249.2248322292, - "y": 5131.418160485249 - }, - "endPoint": { - "x": 6181.435321790283, - "y": 5133.988990081474 - } - } - }, - { - "id": "f88da8ad-9490-48f4-8bcb-bbc2915b9458", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 6181.435321790283, - "y": 5133.988990081474 - }, - "endPoint": { - "x": 6113.641374617233, - "y": 5137.511198109423 - } - } - }, - { - "id": "37834561-2ea7-4c64-8cd9-56ec4b26cd0d", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 6113.641374617233, - "y": 5137.511198109423 - }, - "endPoint": { - "x": 6045.843878056854, - "y": 5141.794508882769 - } - } - }, - { - "id": "d86164fc-4502-4a8b-8e2e-452d4554ab13", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 6045.843878056854, - "y": 5141.794508882769 - }, - "endPoint": { - "x": 5978.043719456065, - "y": 5146.648646715155 - } - } - }, - { - "id": "68fd336d-ce1d-4cbd-8474-bf38f285e361", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5978.043719456065, - "y": 5146.648646715155 - }, - "endPoint": { - "x": 5842.438965520298, - "y": 5157.308300811681 - } - } - } - ] - }, - { - "id": "f7d82ee3-2712-442e-9f71-4ac58e9756f9", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5342.438965520298, - "y": 5557.308300811681 - }, - "endPoint": { - "x": 5342.438965520298, - "y": 5293.957258093491 - } - } - }, - { - "id": "37026bd2-b4a5-4bac-95a7-8b2e30bc8fbf", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4842.438965520298, - "y": 5557.308300811681 - }, - "endPoint": { - "x": 4842.438965520298, - "y": 5445.9341573001875 - } - } - }, - { - "id": "a096e1c7-5b92-4049-a18a-f8483382f4ee", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4842.438965520298, - "y": 5757.308300811681 - }, - "endPoint": { - "x": 4842.438965520298, - "y": 5557.308300811681 - } - } - }, - { - "id": "7ded1276-3c88-4355-aa68-bfa857a42485", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5342.438965520298, - "y": 5605.331401604984 - }, - "endPoint": { - "x": 5342.438965520298, - "y": 5405.331401604984 - } - } - }, - { - "id": "d35d6df0-44a1-4493-8afa-3103924436de", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7842.438965520298, - "y": 5557.308300811681 - }, - "endPoint": { - "x": 7842.438965520298, - "y": 5445.9341573001875 - } - } - }, - { - "id": "6e832d06-e25e-4c00-a2db-6e095eec0f75", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7842.438965520298, - "y": 5757.308300811681 - }, - "endPoint": { - "x": 7842.438965520298, - "y": 5557.308300811681 - } - } - }, - { - "id": "e08f3508-b28d-4924-864b-687f6dad6438", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7342.438965520298, - "y": 5605.331401604984 - }, - "endPoint": { - "x": 7342.438965520298, - "y": 5405.331401604984 - } - } - }, - { - "id": "8989c81d-17a5-4b13-af3f-a3a5436b37ac", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7342.438965520298, - "y": 5557.308300811681 - }, - "endPoint": { - "x": 7342.438965520298, - "y": 5293.957258093491 - } - } - }, - { - "id": "31cff061-3223-4335-a080-21a2552e8934", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3474.241326984018, - "y": 8754.00813196419 - }, - "endPoint": { - "x": 4092.4389655202976, - "y": 8754.00813196419 - } - } - }, - { - "id": "5754d987-11bd-4fc3-9e5e-35db32a4dba0", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5342.438965520298, - "y": 8754.00813196419 - }, - "endPoint": { - "x": 5342.438965520298, - "y": 8954.00813196419 - } - } - }, - { - "id": "6804a708-86f9-47a7-9dbe-d6cacfac5380", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5842.438965520298, - "y": 8754.00813196419 - }, - "endPoint": { - "x": 5842.438965520298, - "y": 8954.00813196419 - } - } - }, - { - "id": "3e2e2226-be09-412c-838e-f6cc5d21ff3d", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 6842.438965520298, - "y": 8754.00813196419 - }, - "endPoint": { - "x": 6842.438965520298, - "y": 8954.00813196419 - } - } - }, - { - "id": "f0bfe80e-4ae9-4a85-956a-29492c2d0f14", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7342.438965520298, - "y": 8754.00813196419 - }, - "endPoint": { - "x": 7342.438965520298, - "y": 8954.00813196419 - } - } - }, - { - "id": "d3a8a579-8691-406d-a82c-c4ee5bc6effe", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4092.4389655202976, - "y": 8754.00813196419 - }, - "endPoint": { - "x": 4092.4389655202976, - "y": 8954.00813196419 - } - } - }, - { - "id": "9c0e8667-c34a-4796-a36e-a41da1697cec", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8592.438965520298, - "y": 8754.00813196419 - }, - "endPoint": { - "x": 8592.438965520298, - "y": 8954.00813196419 - } - } - }, - { - "id": "537e46d0-7ef5-46d2-bb79-503cec1ebd67", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8592.438965520298, - "y": 8754.00813196419 - }, - "endPoint": { - "x": 8592.438965520298, - "y": 8954.00813196419 - } - } - }, - { - "id": "e0d0de1b-07f5-48e8-809e-72bda27b41a8", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8592.438965520298, - "y": 8954.00813196419 - }, - "endPoint": { - "x": 6842.438965520298, - "y": 8954.00813196419 - } - } - }, - { - "id": "1bd4395b-b46b-4749-a3e1-248b98aa453e", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8592.438965520298, - "y": 9533.763931827678 - }, - "endPoint": { - "x": 3474.241326984018, - "y": 9533.763931827678 - } - } - }, - { - "id": "17a9089b-a2a7-4cc2-9010-7177a5797025", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8592.438965520298, - "y": 9533.763931827678 - }, - "endPoint": { - "x": 9700.645622687414, - "y": 9533.763931827678 - } - } - }, - { - "id": "0ac6e825-a0eb-4752-89c5-2b38081e98f8", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5842.438965520298, - "y": 8954.00813196419 - }, - "endPoint": { - "x": 4092.4389655202976, - "y": 8954.00813196419 - } - } - }, - { - "id": "87e976b9-5227-46ac-933d-53d5967a7ed3", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5088.536087974906, - "y": 5679.5696338159905 - }, - "endPoint": { - "x": 5088.536087974906, - "y": 5479.5696338159905 - } - } - }, - { - "id": "4678d072-c6d8-40c2-8a40-46319156f876", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5088.536087974906, - "y": 5631.546533022687 - }, - "endPoint": { - "x": 5088.536087974906, - "y": 5368.195490304468 - } - } - }, - { - "id": "cc288b9b-d076-4843-821c-af8fa8f9bf74", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5542.316786611744, - "y": 5549.748401477991 - }, - "endPoint": { - "x": 5542.316786611744, - "y": 5349.748401477991 - } - } - }, - { - "id": "aa3b0387-f24a-4d69-9e52-64c5f0d6144b", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5542.316786611744, - "y": 5501.725300684688 - }, - "endPoint": { - "x": 5542.316786611744, - "y": 5238.374257966469 - } - } - }, - { - "id": "2cbb136e-f2ca-4b84-8842-e637e8541ba8", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4612.845350768417, - "y": 5836.268278345466 - }, - "endPoint": { - "x": 4612.845350768417, - "y": 5636.268278345466 - } - } - }, - { - "id": "7e20c6ca-d38e-4785-8ce3-febb239dba51", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4612.845350768417, - "y": 5788.245177552191 - }, - "endPoint": { - "x": 4612.845350768417, - "y": 5524.894134833972 - } - } - }, - { - "id": "5dacca6e-bed8-412f-9644-cdbd320c6cc0", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7114.0738457448315, - "y": 5541.965920789691 - }, - "endPoint": { - "x": 7114.0738457448315, - "y": 5341.965920789691 - } - } - }, - { - "id": "353f28eb-5706-471b-bb1f-5aa0b614a66a", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7114.0738457448315, - "y": 5493.942819996388 - }, - "endPoint": { - "x": 7114.0738457448315, - "y": 5230.591777278285 - } - } - }, - { - "id": "2d43e87e-14da-48d0-99e0-83e6fae65e2e", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7588.367422565352, - "y": 5677.157292607473 - }, - "endPoint": { - "x": 7588.367422565352, - "y": 5477.157292607473 - } - } - }, - { - "id": "f03c078b-ea26-4106-8730-41c92affaf9a", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7588.367422565352, - "y": 5629.13419181417 - }, - "endPoint": { - "x": 7588.367422565352, - "y": 5365.783149096067 - } - } - }, - { - "id": "db35f7c0-608f-466c-993e-c0b100f588e9", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8110.076243293239, - "y": 5849.890115191374 - }, - "endPoint": { - "x": 8110.076243293239, - "y": 5649.890115191374 - } - } - }, - { - "id": "4dcfacee-d534-4cbd-9a98-e3afdd10e4de", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8110.076243293239, - "y": 5801.867014398071 - }, - "endPoint": { - "x": 8110.076243293239, - "y": 5538.515971679881 - } - } - }, - { - "id": "c113c6e1-57e4-4027-b272-f7a26c8e63bd", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4724.560845806904, - "y": 1636.3660789713904 - }, - "endPoint": { - "x": 8406.580789551721, - "y": 1636.3660789713904 - } - } - }, - { - "id": "db0cd893-fc4b-400f-b2e3-a5ddcdedff7b", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4724.560845806904, - "y": 1886.3660789713904 - }, - "endPoint": { - "x": 8406.580789551721, - "y": 1886.3660789713904 - } - } - }, - { - "id": "9c3b9461-c4f6-4108-a986-159d36a792ac", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4724.560845806904, - "y": 2136.3660789713904 - }, - "endPoint": { - "x": 8406.580789551721, - "y": 2136.3660789713904 - } - } - }, - { - "id": "d3dac5a3-526d-4342-844d-8f770b71567b", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4724.560845806904, - "y": 2386.3660789713904 - }, - "endPoint": { - "x": 8406.580789551721, - "y": 2386.3660789713904 - } - } - }, - { - "id": "fb7dbf5a-3782-425c-8a69-37f0b5036d7c", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "공 종", - "basePoint": { - "x": 5117.667635826743, - "y": 2474.317084736278 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#f5f7fa", - "fontSize": 100, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "c3d2156c-e003-4901-b34b-5881ce42f16d", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 6495.3265131632215, - "y": 2636.3660789713904 - }, - "endPoint": { - "x": 6495.3265131632215, - "y": 1636.3660789713904 - } - } - }, - { - "id": "9446eccf-2b17-4cb6-9e62-37dd160365a4", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5838.931579108001, - "y": 2636.3660789713904 - }, - "endPoint": { - "x": 5838.931579108001, - "y": 1636.3660789713904 - } - } - }, - { - "id": "faf76293-9a78-4fe9-82bf-dcde54937e65", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "거 푸 집", - "basePoint": { - "x": 5026.788807597652, - "y": 1723.1852605018648 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#f5f7fa", - "fontSize": 100, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "a1900dab-4160-4dc8-876f-d0b32dbb82ab", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "철 근", - "basePoint": { - "x": 5049.558448077005, - "y": 1972.4529036443855 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#f5f7fa", - "fontSize": 100, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "08a6795a-deaa-47b7-af70-af636a1fd2d1", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "콘크리트", - "basePoint": { - "x": 5004.019167118298, - "y": 2223.185260501865 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#f5f7fa", - "fontSize": 100, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "c6733f8d-c384-44ac-98cf-ef86bd1fcc72", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8406.580789551721, - "y": 2636.3660789713904 - }, - "endPoint": { - "x": 8406.580789551721, - "y": 1636.3660789713904 - } - } - }, - { - "id": "811b0cc2-5b4c-40de-87a3-19ec864842e9", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 7006.580789551721, - "y": 2636.3660789713904 - }, - "endPoint": { - "x": 7006.580789551721, - "y": 1636.3660789713904 - } - } - }, - { - "id": "c37d8952-9ccf-4b53-bafc-66f5de74d6e7", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "재 료 표", - "basePoint": { - "x": 6171.278157050139, - "y": 2926.246386233892 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#f5f7fa", - "fontSize": 162.5, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "eeaa718a-a156-4116-9d08-f620228da1b1", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4724.560845806904, - "y": 2636.3660789713904 - }, - "endPoint": { - "x": 8406.580789551721, - "y": 2636.3660789713904 - } - } - }, - { - "id": "9c94ab33-5042-4366-876f-21a739ebecd3", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4724.560845806904, - "y": 2636.3660789713904 - }, - "endPoint": { - "x": 4724.560845806904, - "y": 1636.3660789713904 - } - } - }, - { - "id": "e4cd4d28-be79-47f9-8bab-5f3fff015bf9", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "수 량", - "basePoint": { - "x": 6008.584931695135, - "y": 2480.5939133914653 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#f5f7fa", - "fontSize": 100, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "47f1ad4b-dca7-4ffe-a428-c03f610578c9", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "0.32", - "basePoint": { - "x": 6027.426476302324, - "y": 2230.5939133914653 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#f5f7fa", - "fontSize": 100, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "953ba152-2cab-44ab-89c1-11701a8a6247", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "7.800", - "basePoint": { - "x": 5988.27867337293, - "y": 1980.6604912875919 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#f5f7fa", - "fontSize": 100, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "4bca5198-fdaf-4c0f-bcf0-199307fa03b2", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "3.20", - "basePoint": { - "x": 6027.426476302324, - "y": 1730.5939133914653 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#f5f7fa", - "fontSize": 100, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "edf98049-5526-4d8d-8136-1422ad80ed45", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "단 위", - "basePoint": { - "x": 6605.460551981407, - "y": 2473.185260501865 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#f5f7fa", - "fontSize": 100, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "8b3cd74d-e7f2-4436-9c14-88bcafcef44b", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "m2", - "basePoint": { - "x": 6666.645638532704, - "y": 1723.1852605018648 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#f5f7fa", - "fontSize": 100, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "31e05f58-1de2-46fe-9b3a-010b446f4910", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "m3", - "basePoint": { - "x": 6666.645638532704, - "y": 2223.185260501865 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#f5f7fa", - "fontSize": 100, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "97bda905-de40-4054-81a3-77809aaa9dcd", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "kg", - "basePoint": { - "x": 6689.148967427551, - "y": 1981.5740754153812 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#f5f7fa", - "fontSize": 100, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "0ff01c50-2823-46ef-9932-24588d0aa138", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "비 고", - "basePoint": { - "x": 7508.109824139217, - "y": 2473.185260501865 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#f5f7fa", - "fontSize": 100, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "31dc275b-5667-4a4b-b2ff-bcc4acce74e2", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "D16mm", - "basePoint": { - "x": 7422.224338120606, - "y": 1973.1852605018648 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#f5f7fa", - "fontSize": 100, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "fb2fd154-0baf-48ba-a1e8-2d6a4f045a41", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "유로폼", - "basePoint": { - "x": 7462.637121076637, - "y": 1723.9176173593733 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#f5f7fa", - "fontSize": 100, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "cbd29065-2971-4e04-858a-24aeb34a5248", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "(개소당)", - "basePoint": { - "x": 7860.603470677044, - "y": 2730.8090505170694 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#f5f7fa", - "fontSize": 100, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "aff62f9c-5c4a-46ee-a627-6f1a8e614977", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "물 넘 이", - "basePoint": { - "x": 5856.929112473503, - "y": 10268.319695265178 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#f5f7fa", - "fontSize": 162.5, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "dfea6352-a41c-4c97-bd6c-54929d9cdf6b", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "평 면 도", - "basePoint": { - "x": 6087.681379433197, - "y": 8235.512656280887 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#f5f7fa", - "fontSize": 120, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "7c5ba9c5-44eb-4532-973b-9db7a4a56aa5", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "노 면", - "basePoint": { - "x": 6150.8795131608495, - "y": 9116.345420375466 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#f5f7fa", - "fontSize": 120, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "26f9fa1f-8565-46d9-888d-16c5cc29ce9e", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "(A)집수부", - "basePoint": { - "x": 7722.742018343939, - "y": 5011.347616029292 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#f5f7fa", - "fontSize": 120, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "3aecbf4e-6c28-433f-963e-2f425861ec9c", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "(A)", - "basePoint": { - "x": 7371.320427476312, - "y": 5726.645132854988 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#f5f7fa", - "fontSize": 100, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "250e9c87-1ac2-4597-a53f-5ae473890d3a", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "(A)", - "basePoint": { - "x": 5179.4078267693985, - "y": 5690.370184635685 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#f5f7fa", - "fontSize": 100, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "cc3c80df-e353-4833-a3e2-a3cead4a49fc", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8592.438965520298, - "y": 8754.00813196419 - }, - "endPoint": { - "x": 9700.645622687414, - "y": 8754.00813196419 - } - } - }, - { - "id": "bb87b27b-6253-4277-b3d1-e0376cf7c18c", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 6842.438965520298, - "y": 8754.00813196419 - }, - "endPoint": { - "x": 8592.438965520298, - "y": 8754.00813196419 - } - } - }, - { - "id": "dc3a5ec6-9a89-4412-b0ce-1d4ff1fcbd49", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5842.438965520298, - "y": 8754.00813196419 - }, - "endPoint": { - "x": 6842.438965520298, - "y": 8754.00813196419 - } - } - }, - { - "id": "6fba818d-5fd9-4d05-ae96-efe80e31f9ae", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4092.4389655202976, - "y": 8754.00813196419 - }, - "endPoint": { - "x": 5842.438965520298, - "y": 8754.00813196419 - } - } - }, - { - "id": "1e52041c-c8ba-4345-8f82-b9cb2ce19e02", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8662.438965520298, - "y": 8754.00813196419 - }, - "endPoint": { - "x": 8876.726452197647, - "y": 8754.00813196419 - } - } - }, - { - "id": "f02c50f5-b68d-475c-83d7-17e542ff0969", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8662.438965520298, - "y": 8954.00813196419 - }, - "endPoint": { - "x": 8876.726452197647, - "y": 8954.00813196419 - } - } - }, - { - "id": "8a6dd846-6c3f-4f49-abaf-c3865cffc1ce", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8846.726452197647, - "y": 8774.00813196419 - }, - "endPoint": { - "x": 8846.726452197647, - "y": 8934.00813196419 - } - } - }, - { - "id": "1e6ec113-0623-4058-879a-19551642653d", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "200", - "basePoint": { - "x": 8806.274324538012, - "y": 8854.008131964278 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#ffffff", - "fontSize": 40, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "2feb2105-f883-4be5-8f30-be39c0eac0ad", - "type": "Point", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "point": { - "x": 8592.438965520298, - "y": 8754.00813196419 - } - } - }, - { - "id": "52fad309-37ba-41a0-a260-a7f5dea5fe79", - "type": "Point", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "point": { - "x": 8592.438965520298, - "y": 8954.00813196419 - } - } - }, - { - "id": "91df594b-5a99-4797-aff0-f3201d40f6f2", - "type": "Point", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "point": { - "x": 8846.726452197647, - "y": 8954.00813196419 - } - } - }, - { - "id": "1d3282bc-b0c7-4751-9ac8-8763835aaf09", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8662.438965520298, - "y": 8954.00813196419 - }, - "endPoint": { - "x": 8876.726452197647, - "y": 8954.00813196419 - } - } - }, - { - "id": "8f0e2d53-cd33-4437-835b-ab7754460eda", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8662.438965520298, - "y": 9533.763931827678 - }, - "endPoint": { - "x": 8876.726452197647, - "y": 9533.763931827678 - } - } - }, - { - "id": "7db482de-3692-41cc-83c2-1f9b0cd0e56c", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 8846.726452197647, - "y": 8974.00813196419 - }, - "endPoint": { - "x": 8846.726452197647, - "y": 9513.763931827678 - } - } - }, - { - "id": "b6e9fa33-ca65-40dd-8893-1b1ca1b51b86", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "3,800", - "basePoint": { - "x": 8803.774324538012, - "y": 9243.886031895992 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#ffffff", - "fontSize": 40, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "ba7667f3-cf46-4974-8bd7-021abf199950", - "type": "Point", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "point": { - "x": 8592.438965520298, - "y": 8954.00813196419 - } - } - }, - { - "id": "ec04ba9f-46dc-48d2-8dfd-380378c3a992", - "type": "Point", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "point": { - "x": 8592.438965520298, - "y": 9533.763931827678 - } - } - }, - { - "id": "71d4559b-a2c5-43b8-ae15-827e856a5e58", - "type": "Point", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "point": { - "x": 8846.726452197647, - "y": 9533.763931827678 - } - } - }, - { - "id": "3d99c44b-ed81-41c6-9e64-2d5d4ccfa399", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4255.194526991341, - "y": 5927.308300811681 - }, - "endPoint": { - "x": 4255.194526991341, - "y": 6110.138199321285 - } - } - }, - { - "id": "b78474b5-347c-489c-96ed-1a4c0679ae2c", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5342.438965520298, - "y": 5927.308300811681 - }, - "endPoint": { - "x": 5342.438965520298, - "y": 6110.138199321285 - } - } - }, - { - "id": "dcc90945-d1fd-4708-aad3-b789f3791206", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4275.194526991341, - "y": 6080.138199321285 - }, - "endPoint": { - "x": 5322.438965520298, - "y": 6080.138199321285 - } - } - }, - { - "id": "f239a321-23a6-4657-8449-41859495e0fa", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "4,000", - "basePoint": { - "x": 4798.816746255849, - "y": 6123.116922725574 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#ffffff", - "fontSize": 40, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "ffa9c182-dbb6-4c49-82eb-e0d0e4c43a43", - "type": "Point", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "point": { - "x": 4255.194526991341, - "y": 5857.308300811681 - } - } - }, - { - "id": "2b7e524e-2d05-42fb-9881-ac74c5ad3076", - "type": "Point", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "point": { - "x": 5342.438965520298, - "y": 5857.308300811681 - } - } - }, - { - "id": "da769cb9-c006-44b4-99b9-f2049c2a173f", - "type": "Point", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "point": { - "x": 5342.438965520298, - "y": 6080.138199321285 - } - } - }, - { - "id": "7540006c-0d87-4ebe-9377-fd5be0360b6e", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5342.438965520298, - "y": 5927.308300811681 - }, - "endPoint": { - "x": 5342.438965520298, - "y": 6110.138199321285 - } - } - }, - { - "id": "d7f3506b-3e50-4724-a53d-e0cef95220ce", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5842.438965520298, - "y": 5927.308300811681 - }, - "endPoint": { - "x": 5842.438965520298, - "y": 6110.138199321285 - } - } - }, - { - "id": "1468ccda-2232-432f-8c29-1bc592d62ccc", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5362.438965520298, - "y": 6080.138199321285 - }, - "endPoint": { - "x": 5822.438965520298, - "y": 6080.138199321285 - } - } - }, - { - "id": "c948238f-642a-42c7-821b-583ef6d0889e", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "400", - "basePoint": { - "x": 5592.438965520298, - "y": 6120.6435184701695 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#ffffff", - "fontSize": 40, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "3104c15f-b432-4109-ae2d-6b5beae0a061", - "type": "Point", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "point": { - "x": 5342.438965520298, - "y": 5857.308300811681 - } - } - }, - { - "id": "5b4c4077-644d-479b-b4be-f21b5a80071c", - "type": "Point", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "point": { - "x": 5842.438965520298, - "y": 5857.308300811681 - } - } - }, - { - "id": "4b06a9a4-263b-4944-a96f-6d5a3f103a28", - "type": "Point", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "point": { - "x": 5842.438965520298, - "y": 6080.138199321285 - } - } - }, - { - "id": "fb9e1666-5097-4472-93ba-ffd66dd79e88", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5912.438965520298, - "y": 5357.308300811681 - }, - "endPoint": { - "x": 6100.861424783943, - "y": 5357.308300811681 - } - } - }, - { - "id": "80ffa0f2-26ee-4903-a840-0058aa703da0", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5912.438965520298, - "y": 5857.308300811681 - }, - "endPoint": { - "x": 6100.861424783943, - "y": 5857.308300811681 - } - } - }, - { - "id": "c0f05f9c-b686-4a40-aed2-87b84d82105e", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 6070.861424783943, - "y": 5377.308300811681 - }, - "endPoint": { - "x": 6070.861424783943, - "y": 5837.308300811681 - } - } - }, - { - "id": "a2a5d699-ab49-45b0-8485-67fbb2016d91", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "400", - "basePoint": { - "x": 6030.356105634943, - "y": 5607.308300811768 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#ffffff", - "fontSize": 40, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "46fe4c4c-06b7-4df0-b5bc-4995292f805b", - "type": "Point", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "point": { - "x": 5842.438965520298, - "y": 5357.308300811681 - } - } - }, - { - "id": "7bc4a312-d87c-4a87-8618-8fc6dc3620bd", - "type": "Point", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "point": { - "x": 5842.438965520298, - "y": 5857.308300811681 - } - } - }, - { - "id": "d77bde7f-1bf7-453e-aacb-79b2e8e9f333", - "type": "Point", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "point": { - "x": 6070.861424783943, - "y": 5857.308300811681 - } - } - }, - { - "id": "743941df-fe64-49dc-82ec-d113debc1be6", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9252.367720227514, - "y": 5938.54050465257 - }, - "endPoint": { - "x": 9402.127321101609, - "y": 5938.54050465257 - } - } - }, - { - "id": "bfc341d0-b718-4ea3-b500-3146494bdd99", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9252.367720227514, - "y": 6138.54050465257 - }, - "endPoint": { - "x": 9402.127321101609, - "y": 6138.54050465257 - } - } - }, - { - "id": "92426a75-2e7b-4ea3-9125-15a1ddc2ca32", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 9372.127321101609, - "y": 5958.54050465257 - }, - "endPoint": { - "x": 9372.127321101609, - "y": 6118.54050465257 - } - } - }, - { - "id": "619a7ed5-4b4d-4bbc-8f90-0495bc6e5c21", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "200", - "basePoint": { - "x": 9331.675193442032, - "y": 6038.54050465257 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#ffffff", - "fontSize": 40, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "741f5bda-9fa0-4fcf-b63d-f28c221094af", - "type": "Point", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "point": { - "x": 9182.367720227514, - "y": 5938.54050465257 - } - } - }, - { - "id": "3df283d1-a669-41c3-9450-fe0a2e38e831", - "type": "Point", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "point": { - "x": 9182.367720227514, - "y": 6138.54050465257 - } - } - }, - { - "id": "e438ddda-61fa-42b5-a45b-0eff0f9650da", - "type": "Point", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "point": { - "x": 9372.127321101609, - "y": 6138.54050465257 - } - } - }, - { - "id": "373c4819-617e-40da-a16a-576c82fb42da", - "type": "PolyLine", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": null, - "children": [ - { - "id": "07719b10-be3a-42db-ad88-e78f4dab0e9a", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5718.21419414843, - "y": 8174.614427795983 - }, - "endPoint": { - "x": 5717.41927127511, - "y": 8188.262726441695 - } - } - }, - { - "id": "9db0253c-4cc1-47b6-939a-060a807f5cee", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5717.41927127511, - "y": 8188.262726441695 - }, - "endPoint": { - "x": 5715.045252623153, - "y": 8201.72645526164 - } - } - }, - { - "id": "4349ede0-1b24-431a-bb61-94cbdba0fb87", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5715.045252623153, - "y": 8201.72645526164 - }, - "endPoint": { - "x": 5711.124242722406, - "y": 8214.823540420359 - } - } - }, - { - "id": "2a9feb97-bee0-4ce0-a3a7-387c738e9eee", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5711.124242722406, - "y": 8214.823540420359 - }, - "endPoint": { - "x": 5705.709266505262, - "y": 8227.376866308943 - } - } - }, - { - "id": "21d42064-5a46-4454-b01b-35ffe01c4f39", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5705.709266505262, - "y": 8227.376866308943 - }, - "endPoint": { - "x": 5698.873552235775, - "y": 8239.216670729744 - } - } - }, - { - "id": "f00623d8-1c90-4b54-b9b2-f75961522cf8", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5698.873552235775, - "y": 8239.216670729744 - }, - "endPoint": { - "x": 5690.709541222837, - "y": 8250.182840638969 - } - } - }, - { - "id": "17f1363b-8022-4e6b-bda6-0a8a8894535d", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5690.709541222837, - "y": 8250.182840638969 - }, - "endPoint": { - "x": 5681.327637710259, - "y": 8260.127077400888 - } - } - }, - { - "id": "6e4257b4-7f8d-424a-9a5e-718e9e39e532", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5681.327637710259, - "y": 8260.127077400888 - }, - "endPoint": { - "x": 5670.854715848458, - "y": 8268.91490227246 - } - } - }, - { - "id": "a4422251-c77d-423f-9c34-d765499b6110", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5670.854715848458, - "y": 8268.91490227246 - }, - "endPoint": { - "x": 5659.432403939543, - "y": 8276.427474997705 - } - } - }, - { - "id": "89e37ed9-3857-4662-aafb-be90f40b919d", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5659.432403939543, - "y": 8276.427474997705 - }, - "endPoint": { - "x": 5647.215169157251, - "y": 8282.563200918172 - } - } - }, - { - "id": "ad43d390-addf-44fd-9c14-d41fa5f98825", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5647.215169157251, - "y": 8282.563200918172 - }, - "endPoint": { - "x": 5634.3682286435505, - "y": 8287.239104866545 - } - } - }, - { - "id": "2249d846-9bf8-4b6a-a35e-2abbfbd55c1b", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5634.3682286435505, - "y": 8287.239104866545 - }, - "endPoint": { - "x": 5621.0653152301675, - "y": 8290.391953263344 - } - } - }, - { - "id": "f96989a9-09ee-4f1b-bc02-5bf921cfcad3", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5621.0653152301675, - "y": 8290.391953263344 - }, - "endPoint": { - "x": 5607.486328000086, - "y": 8291.979109242704 - } - } - }, - { - "id": "0dae4487-9eb9-4e01-8023-3bce0458f6e9", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5607.486328000086, - "y": 8291.979109242704 - }, - "endPoint": { - "x": 5593.814899461111, - "y": 8291.979109242704 - } - } - }, - { - "id": "11d03889-3a31-487b-9dc5-10c2d941dc24", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5593.814899461111, - "y": 8291.979109242704 - }, - "endPoint": { - "x": 5580.23591223103, - "y": 8290.391953263344 - } - } - }, - { - "id": "125dcece-9102-4e17-bad2-dbda66beaf3b", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5580.23591223103, - "y": 8290.391953263344 - }, - "endPoint": { - "x": 5566.932998817647, - "y": 8287.239104866545 - } - } - }, - { - "id": "59bfef36-cc47-47a7-be2c-9125d8161470", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5566.932998817647, - "y": 8287.239104866545 - }, - "endPoint": { - "x": 5554.086058303947, - "y": 8282.563200918172 - } - } - }, - { - "id": "e9b248d8-3498-493c-a94a-81e14120d57e", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5554.086058303947, - "y": 8282.563200918172 - }, - "endPoint": { - "x": 5541.868823521654, - "y": 8276.427474997705 - } - } - }, - { - "id": "cbc9d45d-6015-42a2-af2d-7fcda9c1565d", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5541.868823521654, - "y": 8276.427474997705 - }, - "endPoint": { - "x": 5530.446511612739, - "y": 8268.91490227246 - } - } - }, - { - "id": "b116c195-ab71-4416-a107-3a67c367c164", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5530.446511612739, - "y": 8268.91490227246 - }, - "endPoint": { - "x": 5519.973589750938, - "y": 8260.127077400888 - } - } - }, - { - "id": "a983e606-f5aa-4970-9ac8-b25aa7df43ea", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5519.973589750938, - "y": 8260.127077400888 - }, - "endPoint": { - "x": 5510.59168623836, - "y": 8250.182840638969 - } - } - }, - { - "id": "f3b6099b-1f3d-4206-8988-bebeb3122acd", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5510.59168623836, - "y": 8250.182840638969 - }, - "endPoint": { - "x": 5502.427675225423, - "y": 8239.216670729744 - } - } - }, - { - "id": "d9d17ce6-f122-446c-ac72-8f91b12cc19a", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5502.427675225423, - "y": 8239.216670729744 - }, - "endPoint": { - "x": 5495.591960955935, - "y": 8227.376866308943 - } - } - }, - { - "id": "f44116f6-50ee-455a-ba3e-3a8e83952ef1", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5495.591960955935, - "y": 8227.376866308943 - }, - "endPoint": { - "x": 5490.176984738791, - "y": 8214.823540420359 - } - } - }, - { - "id": "22732ae6-0143-4f9a-b1d1-46515f2df501", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5490.176984738791, - "y": 8214.823540420359 - }, - "endPoint": { - "x": 5486.2559748380445, - "y": 8201.72645526164 - } - } - }, - { - "id": "be97c265-7f3f-4a6c-91b3-98221e92ccc4", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5486.2559748380445, - "y": 8201.72645526164 - }, - "endPoint": { - "x": 5483.881956186087, - "y": 8188.262726441695 - } - } - }, - { - "id": "f9646599-2ba3-4ea9-8e0a-7337001898e4", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5483.881956186087, - "y": 8188.262726441695 - }, - "endPoint": { - "x": 5483.087033312768, - "y": 8174.614427795983 - } - } - }, - { - "id": "293cec21-af0d-4604-b905-3ff9d772248a", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5483.087033312768, - "y": 8174.614427795983 - }, - "endPoint": { - "x": 5483.881956186087, - "y": 8160.966129150271 - } - } - }, - { - "id": "48e0907e-660a-47dd-9f9a-ed7efdcefd3c", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5483.881956186087, - "y": 8160.966129150271 - }, - "endPoint": { - "x": 5486.2559748380445, - "y": 8147.502400330326 - } - } - }, - { - "id": "b65058dd-b032-4675-a61b-8dac6d6bc4c0", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5486.2559748380445, - "y": 8147.502400330326 - }, - "endPoint": { - "x": 5490.176984738791, - "y": 8134.405315171607 - } - } - }, - { - "id": "c507d0ef-0ac5-4ccb-8748-e59f5d1ba204", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5490.176984738791, - "y": 8134.405315171607 - }, - "endPoint": { - "x": 5495.591960955935, - "y": 8121.851989283023 - } - } - }, - { - "id": "d40a0a52-46a6-4bdc-8028-9cbfcb1dbc16", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5495.591960955935, - "y": 8121.851989283023 - }, - "endPoint": { - "x": 5502.427675225423, - "y": 8110.012184862222 - } - } - }, - { - "id": "05afe05e-b9dc-438a-9365-00498dfb507a", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5502.427675225423, - "y": 8110.012184862222 - }, - "endPoint": { - "x": 5510.59168623836, - "y": 8099.046014952997 - } - } - }, - { - "id": "c1580259-ddaa-4a60-83fc-c141bc9d3aa8", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5510.59168623836, - "y": 8099.046014952997 - }, - "endPoint": { - "x": 5519.973589750938, - "y": 8089.101778191078 - } - } - }, - { - "id": "6030fbc9-e72e-4e2a-85b4-f79816944c15", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5519.973589750938, - "y": 8089.101778191078 - }, - "endPoint": { - "x": 5530.446511612739, - "y": 8080.313953319506 - } - } - }, - { - "id": "bb6a97de-9733-4395-9557-93b33ff74986", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5530.446511612739, - "y": 8080.313953319506 - }, - "endPoint": { - "x": 5541.868823521654, - "y": 8072.801380594261 - } - } - }, - { - "id": "a15744df-082b-4584-9de1-db2b34718dea", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5541.868823521654, - "y": 8072.801380594261 - }, - "endPoint": { - "x": 5554.086058303947, - "y": 8066.665654673794 - } - } - }, - { - "id": "b9c821f8-e24b-4588-8fe8-a162241fdfdf", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5554.086058303947, - "y": 8066.665654673794 - }, - "endPoint": { - "x": 5566.932998817647, - "y": 8061.989750725421 - } - } - }, - { - "id": "c39b3026-f95d-4da0-92d8-958f0b318ba1", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5566.932998817647, - "y": 8061.989750725421 - }, - "endPoint": { - "x": 5580.23591223103, - "y": 8058.836902328621 - } - } - }, - { - "id": "988732f5-66d2-41d1-9112-e90a64405878", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5580.23591223103, - "y": 8058.836902328621 - }, - "endPoint": { - "x": 5593.814899461111, - "y": 8057.249746349262 - } - } - }, - { - "id": "9fe422af-be4f-4ffb-96ed-a9b11e7b570b", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5593.814899461111, - "y": 8057.249746349262 - }, - "endPoint": { - "x": 5607.486328000086, - "y": 8057.249746349262 - } - } - }, - { - "id": "eafcb8fb-5c6f-4e51-9a96-3af120a5c45c", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5607.486328000086, - "y": 8057.249746349262 - }, - "endPoint": { - "x": 5621.0653152301675, - "y": 8058.836902328621 - } - } - }, - { - "id": "e82cd8c4-8c42-4fe5-a924-4c20c9083215", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5621.0653152301675, - "y": 8058.836902328621 - }, - "endPoint": { - "x": 5634.3682286435505, - "y": 8061.989750725421 - } - } - }, - { - "id": "04b63c66-d5e0-47bd-be79-f89b27cfd473", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5634.3682286435505, - "y": 8061.989750725421 - }, - "endPoint": { - "x": 5647.215169157251, - "y": 8066.665654673794 - } - } - }, - { - "id": "3a2d09d8-0461-49f2-b169-1604cfc961c8", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5647.215169157251, - "y": 8066.665654673794 - }, - "endPoint": { - "x": 5659.432403939543, - "y": 8072.801380594261 - } - } - }, - { - "id": "9c4be3bc-54b9-4751-9b0f-1f334eba27b6", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5659.432403939543, - "y": 8072.801380594261 - }, - "endPoint": { - "x": 5670.854715848458, - "y": 8080.313953319506 - } - } - }, - { - "id": "f85acb82-00e9-4129-8b70-87801cd33ed4", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5670.854715848458, - "y": 8080.313953319506 - }, - "endPoint": { - "x": 5681.327637710259, - "y": 8089.101778191078 - } - } - }, - { - "id": "1b60ea3b-5193-4f86-9303-55289cb2b664", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5681.327637710259, - "y": 8089.101778191078 - }, - "endPoint": { - "x": 5690.709541222837, - "y": 8099.046014952997 - } - } - }, - { - "id": "1c699a96-6283-4430-88f4-8ac4a3d46b36", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5690.709541222837, - "y": 8099.046014952997 - }, - "endPoint": { - "x": 5698.873552235775, - "y": 8110.012184862222 - } - } - }, - { - "id": "37679986-fcb3-4ec7-b75e-82bd302480c1", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5698.873552235775, - "y": 8110.012184862222 - }, - "endPoint": { - "x": 5705.709266505262, - "y": 8121.851989283023 - } - } - }, - { - "id": "8f4c3f20-4837-46e7-8725-b2747e5b30e6", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5705.709266505262, - "y": 8121.851989283023 - }, - "endPoint": { - "x": 5711.124242722406, - "y": 8134.405315171607 - } - } - }, - { - "id": "60891be9-d076-4120-97fa-ff7d61046449", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5711.124242722406, - "y": 8134.405315171607 - }, - "endPoint": { - "x": 5715.045252623153, - "y": 8147.502400330326 - } - } - }, - { - "id": "b3dc1aa5-ff81-4b0b-9746-9daef1f030a9", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5715.045252623153, - "y": 8147.502400330326 - }, - "endPoint": { - "x": 5717.41927127511, - "y": 8160.966129150271 - } - } - }, - { - "id": "7c14d3bd-cefe-4136-9e62-606d9da12c58", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5717.41927127511, - "y": 8160.966129150271 - }, - "endPoint": { - "x": 5718.21419414843, - "y": 8174.614427795983 - } - } - }, - { - "id": "16a2025d-b843-4e9d-b55c-045175b62f11", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5718.21419414843, - "y": 8174.614427795983 - }, - "endPoint": { - "x": 5718.21419414843, - "y": 8174.614427795983 - } - } - } - ] - }, - { - "id": "7b07fac3-331d-41f0-baa6-ad715b9d201d", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5433.0870333127095, - "y": 8174.614427795983 - }, - "endPoint": { - "x": 7309.487524201104, - "y": 8174.614427795983 - } - } - }, - { - "id": "22af3408-ba2e-4796-8b80-c7302d81da12", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5600.650613730599, - "y": 8174.614427795983 - }, - "endPoint": { - "x": 5600.650613730599, - "y": 8007.050847378181 - } - } - }, - { - "id": "f644cb04-c954-4ed3-abda-e11074ab5664", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5433.0870333127095, - "y": 3730.2742730681784 - }, - "endPoint": { - "x": 7309.487524201104, - "y": 3730.2742730681784 - } - } - }, - { - "id": "6cc5ee6c-c73d-4d80-9a68-1592fd26a8a3", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5600.650613730599, - "y": 3730.2742730681784 - }, - "endPoint": { - "x": 5600.650613730599, - "y": 3562.7106926503766 - } - } - }, - { - "id": "e1b4b339-7d9a-4d80-b43a-2677fb8efc56", - "type": "PolyLine", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": null, - "children": [ - { - "id": "60756684-46d7-4cb1-a60f-0b38a4488799", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5718.21419414843, - "y": 3730.2742730681784 - }, - "endPoint": { - "x": 5717.41927127511, - "y": 3743.9225717138906 - } - } - }, - { - "id": "0a4342bd-88ae-4fa7-8858-18e03a562a56", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5717.41927127511, - "y": 3743.9225717138906 - }, - "endPoint": { - "x": 5715.045252623153, - "y": 3757.3863005338353 - } - } - }, - { - "id": "6b253cc5-8a39-46f5-906f-2f3ce21034a8", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5715.045252623153, - "y": 3757.3863005338353 - }, - "endPoint": { - "x": 5711.124242722406, - "y": 3770.4833856925543 - } - } - }, - { - "id": "95d38aae-02ac-4679-a8e9-6fbd67434b57", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5711.124242722406, - "y": 3770.4833856925543 - }, - "endPoint": { - "x": 5705.709266505262, - "y": 3783.0367115811387 - } - } - }, - { - "id": "e8350021-be8e-42a9-8a8a-837ba5c864ca", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5705.709266505262, - "y": 3783.0367115811387 - }, - "endPoint": { - "x": 5698.873552235775, - "y": 3794.8765160019393 - } - } - }, - { - "id": "cb720ada-86d1-4692-bf8e-b1479871972e", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5698.873552235775, - "y": 3794.8765160019393 - }, - "endPoint": { - "x": 5690.709541222837, - "y": 3805.842685911164 - } - } - }, - { - "id": "c21fcfd2-b6da-4aeb-8b63-b275076619fc", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5690.709541222837, - "y": 3805.842685911164 - }, - "endPoint": { - "x": 5681.327637710259, - "y": 3815.7869226730836 - } - } - }, - { - "id": "3c9efdad-77d8-4ad3-a2a0-03ecbdd27cd8", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5681.327637710259, - "y": 3815.7869226730836 - }, - "endPoint": { - "x": 5670.854715848458, - "y": 3824.574747544655 - } - } - }, - { - "id": "bdaf109d-bc68-4549-8232-5b9827660fa6", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5670.854715848458, - "y": 3824.574747544655 - }, - "endPoint": { - "x": 5659.432403939543, - "y": 3832.0873202699004 - } - } - }, - { - "id": "42bfbaef-20c6-404d-92d4-9042a2adbf40", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5659.432403939543, - "y": 3832.0873202699004 - }, - "endPoint": { - "x": 5647.215169157251, - "y": 3838.2230461903673 - } - } - }, - { - "id": "05fd30c8-852f-430f-984b-0ddeee30a0ca", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5647.215169157251, - "y": 3838.2230461903673 - }, - "endPoint": { - "x": 5634.3682286435505, - "y": 3842.8989501387405 - } - } - }, - { - "id": "cd8ed016-f8ee-49d0-8774-40d76eab47f4", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5634.3682286435505, - "y": 3842.8989501387405 - }, - "endPoint": { - "x": 5621.0653152301675, - "y": 3846.05179853554 - } - } - }, - { - "id": "cf3d1b54-66f0-4aad-8614-86f38311955f", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5621.0653152301675, - "y": 3846.05179853554 - }, - "endPoint": { - "x": 5607.486328000086, - "y": 3847.6389545148995 - } - } - }, - { - "id": "b2bc6cab-b3af-4911-ab1b-72549e9c2489", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5607.486328000086, - "y": 3847.6389545148995 - }, - "endPoint": { - "x": 5593.814899461111, - "y": 3847.6389545148995 - } - } - }, - { - "id": "fcf4f082-d1ed-4060-8037-fd94e927d721", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5593.814899461111, - "y": 3847.6389545148995 - }, - "endPoint": { - "x": 5580.23591223103, - "y": 3846.05179853554 - } - } - }, - { - "id": "1d5d1f5f-21ad-487a-ae6a-b728f9a81b1f", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5580.23591223103, - "y": 3846.05179853554 - }, - "endPoint": { - "x": 5566.932998817647, - "y": 3842.8989501387405 - } - } - }, - { - "id": "b3b721ec-8835-4432-99cf-994bf63ac0b2", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5566.932998817647, - "y": 3842.8989501387405 - }, - "endPoint": { - "x": 5554.086058303947, - "y": 3838.2230461903673 - } - } - }, - { - "id": "523bc5b4-9a87-4249-829c-7d6cddb17a61", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5554.086058303947, - "y": 3838.2230461903673 - }, - "endPoint": { - "x": 5541.868823521654, - "y": 3832.0873202699004 - } - } - }, - { - "id": "8829cce7-66f2-4a55-859f-44353645f1e5", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5541.868823521654, - "y": 3832.0873202699004 - }, - "endPoint": { - "x": 5530.446511612739, - "y": 3824.574747544655 - } - } - }, - { - "id": "df72ee9e-c881-40e3-b8da-af4a7ab07ef5", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5530.446511612739, - "y": 3824.574747544655 - }, - "endPoint": { - "x": 5519.973589750938, - "y": 3815.7869226730836 - } - } - }, - { - "id": "3785b43e-8d52-4657-ba48-5d5360fc1181", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5519.973589750938, - "y": 3815.7869226730836 - }, - "endPoint": { - "x": 5510.59168623836, - "y": 3805.842685911164 - } - } - }, - { - "id": "3d423c96-91af-4b47-9740-a903b40924a6", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5510.59168623836, - "y": 3805.842685911164 - }, - "endPoint": { - "x": 5502.427675225423, - "y": 3794.8765160019393 - } - } - }, - { - "id": "2069985d-bd19-4c4d-8c63-a25198d4832c", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5502.427675225423, - "y": 3794.8765160019393 - }, - "endPoint": { - "x": 5495.591960955935, - "y": 3783.0367115811387 - } - } - }, - { - "id": "6c750d17-c017-447f-8169-9cc0ca3b1891", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5495.591960955935, - "y": 3783.0367115811387 - }, - "endPoint": { - "x": 5490.176984738791, - "y": 3770.4833856925543 - } - } - }, - { - "id": "3ce5f5d4-7a69-49db-a868-64b658f1dd45", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5490.176984738791, - "y": 3770.4833856925543 - }, - "endPoint": { - "x": 5486.2559748380445, - "y": 3757.3863005338353 - } - } - }, - { - "id": "92de1f4a-62a6-49c7-a4cc-abe0e252e63c", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5486.2559748380445, - "y": 3757.3863005338353 - }, - "endPoint": { - "x": 5483.881956186087, - "y": 3743.9225717138906 - } - } - }, - { - "id": "f89b6861-ac96-4042-be58-a1a427e69af2", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5483.881956186087, - "y": 3743.9225717138906 - }, - "endPoint": { - "x": 5483.087033312768, - "y": 3730.2742730681784 - } - } - }, - { - "id": "234656f0-54ca-4131-8f7a-30b84f6b8e44", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5483.087033312768, - "y": 3730.2742730681784 - }, - "endPoint": { - "x": 5483.881956186087, - "y": 3716.6259744224662 - } - } - }, - { - "id": "65e9b18a-c19a-48a3-9030-97db51773f50", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5483.881956186087, - "y": 3716.6259744224662 - }, - "endPoint": { - "x": 5486.2559748380445, - "y": 3703.1622456025216 - } - } - }, - { - "id": "c039b73c-1394-484d-a6c2-740cb4d7868a", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5486.2559748380445, - "y": 3703.1622456025216 - }, - "endPoint": { - "x": 5490.176984738791, - "y": 3690.0651604438026 - } - } - }, - { - "id": "e6f3edd6-727a-4afc-9548-2513a38df948", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5490.176984738791, - "y": 3690.0651604438026 - }, - "endPoint": { - "x": 5495.591960955935, - "y": 3677.5118345552182 - } - } - }, - { - "id": "6c0505b6-c152-4dc2-b7a2-5ce13e038a09", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5495.591960955935, - "y": 3677.5118345552182 - }, - "endPoint": { - "x": 5502.427675225423, - "y": 3665.6720301344176 - } - } - }, - { - "id": "156d5acb-e84c-4e2d-8da6-ea8759e0633d", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5502.427675225423, - "y": 3665.6720301344176 - }, - "endPoint": { - "x": 5510.59168623836, - "y": 3654.7058602251927 - } - } - }, - { - "id": "26b42158-092a-4772-9c8f-221c5037efa0", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5510.59168623836, - "y": 3654.7058602251927 - }, - "endPoint": { - "x": 5519.973589750938, - "y": 3644.7616234632733 - } - } - }, - { - "id": "4e1338e2-8116-45b2-b3b3-27841ebf09d6", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5519.973589750938, - "y": 3644.7616234632733 - }, - "endPoint": { - "x": 5530.446511612739, - "y": 3635.973798591702 - } - } - }, - { - "id": "fdcf9d52-31a7-4f85-9922-e4d0a4d26e39", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5530.446511612739, - "y": 3635.973798591702 - }, - "endPoint": { - "x": 5541.868823521654, - "y": 3628.4612258664565 - } - } - }, - { - "id": "36ba6380-dccc-4a90-a7ef-9438c4d0d34a", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5541.868823521654, - "y": 3628.4612258664565 - }, - "endPoint": { - "x": 5554.086058303947, - "y": 3622.3254999459896 - } - } - }, - { - "id": "baeda2ba-4526-44cc-93e2-b00a716681d6", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5554.086058303947, - "y": 3622.3254999459896 - }, - "endPoint": { - "x": 5566.932998817647, - "y": 3617.6495959976164 - } - } - }, - { - "id": "fff5e652-3d04-4236-97ad-33177e5f34b7", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5566.932998817647, - "y": 3617.6495959976164 - }, - "endPoint": { - "x": 5580.23591223103, - "y": 3614.496747600817 - } - } - }, - { - "id": "5748a4b6-b168-4707-9323-5a308f4865f0", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5580.23591223103, - "y": 3614.496747600817 - }, - "endPoint": { - "x": 5593.814899461111, - "y": 3612.9095916214574 - } - } - }, - { - "id": "f5f46f90-e807-4172-9a3a-10affd7273f9", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5593.814899461111, - "y": 3612.9095916214574 - }, - "endPoint": { - "x": 5607.486328000086, - "y": 3612.9095916214574 - } - } - }, - { - "id": "54e287ac-0ace-4f65-83c2-cd1270238e60", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5607.486328000086, - "y": 3612.9095916214574 - }, - "endPoint": { - "x": 5621.0653152301675, - "y": 3614.496747600817 - } - } - }, - { - "id": "fbe0d6dc-89e6-4d7a-ace7-ab026acf0c1a", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5621.0653152301675, - "y": 3614.496747600817 - }, - "endPoint": { - "x": 5634.3682286435505, - "y": 3617.6495959976164 - } - } - }, - { - "id": "51dc0feb-b03b-44bd-82fd-2ef0abcd8563", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5634.3682286435505, - "y": 3617.6495959976164 - }, - "endPoint": { - "x": 5647.215169157251, - "y": 3622.3254999459896 - } - } - }, - { - "id": "cb5d7056-d456-4487-bfe3-a2ea3c572658", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5647.215169157251, - "y": 3622.3254999459896 - }, - "endPoint": { - "x": 5659.432403939543, - "y": 3628.4612258664565 - } - } - }, - { - "id": "1f53f231-e517-4c09-8ffc-a5f0b6be2ed5", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5659.432403939543, - "y": 3628.4612258664565 - }, - "endPoint": { - "x": 5670.854715848458, - "y": 3635.973798591702 - } - } - }, - { - "id": "6078b620-3bd9-49a0-bd58-c61f0ee149ac", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5670.854715848458, - "y": 3635.973798591702 - }, - "endPoint": { - "x": 5681.327637710259, - "y": 3644.7616234632733 - } - } - }, - { - "id": "9d0f7a7c-cd4b-4029-9423-65b00776a4c5", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5681.327637710259, - "y": 3644.7616234632733 - }, - "endPoint": { - "x": 5690.709541222837, - "y": 3654.7058602251927 - } - } - }, - { - "id": "8c080d18-479b-470d-a893-e9beef8b5907", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5690.709541222837, - "y": 3654.7058602251927 - }, - "endPoint": { - "x": 5698.873552235775, - "y": 3665.6720301344176 - } - } - }, - { - "id": "6dd73348-337f-432b-8d81-ace9357e99c4", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5698.873552235775, - "y": 3665.6720301344176 - }, - "endPoint": { - "x": 5705.709266505262, - "y": 3677.5118345552182 - } - } - }, - { - "id": "d59560a1-7f4b-4b5e-94bc-58ace611c9d9", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5705.709266505262, - "y": 3677.5118345552182 - }, - "endPoint": { - "x": 5711.124242722406, - "y": 3690.0651604438026 - } - } - }, - { - "id": "5b3dcc5d-5b74-4fd7-bbc9-f9ca58a19847", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5711.124242722406, - "y": 3690.0651604438026 - }, - "endPoint": { - "x": 5715.045252623153, - "y": 3703.1622456025216 - } - } - }, - { - "id": "7cabdff4-3779-4658-a9e5-b76a654159c9", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5715.045252623153, - "y": 3703.1622456025216 - }, - "endPoint": { - "x": 5717.41927127511, - "y": 3716.6259744224662 - } - } - }, - { - "id": "62f7b705-f02f-4b85-89c5-c8d66adefb5e", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5717.41927127511, - "y": 3716.6259744224662 - }, - "endPoint": { - "x": 5718.21419414843, - "y": 3730.2742730681784 - } - } - }, - { - "id": "a43dd6fd-7233-47a1-b2a9-848f9497c355", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 5718.21419414843, - "y": 3730.2742730681784 - }, - "endPoint": { - "x": 5718.21419414843, - "y": 3730.2742730681784 - } - } - } - ] - }, - { - "id": "fa680b3a-59c5-4d90-9dfe-cc01d6c3880e", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "정 면 도", - "basePoint": { - "x": 6087.681379433197, - "y": 3791.172501553083 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#f5f7fa", - "fontSize": 120, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "cf4a602a-a552-4b05-ac6b-afcb949fdf7c", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "T=20cm", - "basePoint": { - "x": 4784.830087936833, - "y": 5776.79392950749 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#f5f7fa", - "fontSize": 60, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "14d6d66a-9613-4c58-90c0-01942361e39d", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "T=20cm", - "basePoint": { - "x": 7705.7294778356445, - "y": 5776.79392950749 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#f5f7fa", - "fontSize": 60, - "fontFamily": "sans-serif" - } - } - }, - { - "id": "595c29ce-64ff-46b1-bbb7-a92d69292452", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4822.498914133117, - "y": 5527.7554642618925 - }, - "endPoint": { - "x": 4454.511725482822, - "y": 5114.109492340183 - } - } - }, - { - "id": "eeed58bc-c0ce-4a20-9e8e-214cf6f8e98f", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 4454.511725482822, - "y": 5114.109492340183 - }, - "endPoint": { - "x": 3488.5719559921417, - "y": 5114.109492340183 - } - } - }, - { - "id": "df7c9ee4-869b-4fea-b428-e754f2b1156f", - "type": "Line", - "lineColor": "#fff", - "lineWidth": 1, - "layerId": "b08-frame", - "shapeData": { - "startPoint": { - "x": 3488.5719559921417, - "y": 5114.109492340183 - }, - "endPoint": { - "x": 3585.429944080126, - "y": 5114.109492340183 - } - } - }, - { - "id": "9a4dd1b9-e95c-4369-9673-a4b33674efbc", - "type": "Text", - "lineColor": "#fff", - "lineWidth": 1, - "lineDash": [], - "layerId": "b08-frame", - "shapeData": { - "label": "D16mm, L=500mm", - "basePoint": { - "x": 4272.321739232517, - "y": 5134.109492340183 - }, - "options": { - "textDirection": { - "x": 1, - "y": 0 - }, - "textAlign": "left", - "textColor": "#ffffff", - "fontSize": 70, - "fontFamily": "sans-serif" - } - } - } - ], - "layers": [ - { - "id": "b08-frame", - "name": "도각", - "isVisible": true, - "isLocked": false - } - ], - "drawing_area": [ - 42, - 47, - 812, - 567 - ] - } -} diff --git a/resources/master_template/structure/rw.json b/resources/master_template/structure/rw.json deleted file mode 100644 index 992e0bf6..00000000 --- a/resources/master_template/structure/rw.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "rw", - "도번": "SD09010100", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/sg.json b/resources/master_template/structure/sg.json deleted file mode 100644 index 325b7db6..00000000 --- a/resources/master_template/structure/sg.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "sg", - "도번": "SD11010100", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/ta.json b/resources/master_template/structure/ta.json deleted file mode 100644 index edb845cc..00000000 --- a/resources/master_template/structure/ta.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "ta", - "도번": "SD17000000", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/structure/wy.json b/resources/master_template/structure/wy.json deleted file mode 100644 index fd15a926..00000000 --- a/resources/master_template/structure/wy.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "양식": "구조물도면", - "종류": "structure", - "판": 1, - "열": "wy", - "도번": "SD16000000", - "도면": { - "format": 6, - "entities": [], - "layers": [ - { - "id": "기본", - "name": "기본", - "isVisible": true, - "isLocked": false - } - ] - } -} diff --git a/resources/master_template/table/구조물집계표.json b/resources/master_template/table/구조물집계표.json index 860fb71b..8336b373 100644 --- a/resources/master_template/table/구조물집계표.json +++ b/resources/master_template/table/구조물집계표.json @@ -43,1469 +43,6 @@ "꼴": "글", "설명": "점 NO.x+y · 구간 NO.a~NO.b · 맨 위 전구간" }, - { - "id": "rv", - "머리": [ - "돌기슭막이", - "메쌓기", - "1.2m", - "기초유" - ], - "단위": "m", - "꼴": "수", - "펼침": true, - "설명": "설계값 — 기슭막이 · 관 유입·유출 기슭막이 길이 · 형태 → 높이마다 열", - "일위대가": "UA000004", - "바인딩": { - "종류": "revetment", - "펼침": [ - "form", - "height_m" - ], - "값": "length_m", - "묶음": "rv", - "머리틀": [ - "돌기슭막이", - "{0}", - "{1}" - ], - "값꼴": [ - "{}", - "H={}" - ], - "빈값": [ - "형태 미입력", - "높이 미입력" - ], - "이름": { - "돌쌓기(찰)": "찰쌓기", - "돌쌓기(메)": "메쌓기" - }, - "더함": [ - { - "종류": "pipe", - "펼침": [ - "inlet_revet_form", - "inlet_revet_height_m" - ], - "값": "inlet_revet_length_m", - "조건": { - "inlet_type": "기슭막이" - } - }, - { - "종류": "pipe", - "펼침": [ - "outlet_revet_form", - "outlet_revet_height_m" - ], - "값": "outlet_revet_length_m", - "조건": { - "outlet_type": "기슭막이" - } - } - ] - }, - "경로": [ - "돌기슭막이", - "메쌓기", - "1.2m", - "기초유" - ] - }, - { - "id": "c4", - "머리": [ - "돌기슭막이", - "메쌓기", - "1.2m", - "기초무, 소단" - ], - "단위": "m", - "꼴": "수", - "일위대가": "UA000004", - "경로": [ - "돌기슭막이", - "메쌓기", - "1.2m", - "기초무, 소단" - ] - }, - { - "id": "c1", - "머리": [ - "돌기슭막이", - "메쌓기", - "1.5m", - "기초유" - ], - "단위": "m", - "꼴": "수", - "일위대가": "UA000004", - "경로": [ - "돌기슭막이", - "메쌓기", - "1.5m", - "기초유" - ] - }, - { - "id": "c5", - "머리": [ - "돌기슭막이", - "찰쌓기", - null, - null - ], - "단위": "m", - "꼴": "수", - "일위대가": "UA000004", - "경로": [ - "돌기슭막이", - "찰쌓기" - ] - }, - { - "id": "pv_b", - "머리": [ - "콘크리트포장", - "T=(두께마다)", - "B", - null - ], - "단위": "m", - "꼴": "수", - "펼침": true, - "설명": "설계값 — 포장 폭", - "바인딩": { - "종류": "pavement_concrete", - "펼침": [ - "thickness_cm" - ], - "값": "width_m", - "묶음": "pv", - "머리틀": [ - "콘크리트포장", - "{0}", - "B" - ], - "값꼴": [ - "T={}cm" - ], - "빈값": [ - "두께 미입력" - ] - }, - "경로": [ - "콘크리트포장", - "T=(두께마다)", - "B" - ] - }, - { - "id": "pv_l", - "머리": [ - "콘크리트포장", - "T=(두께마다)", - "L", - null - ], - "단위": "m", - "꼴": "수", - "펼침": true, - "설명": "설계값 — 포장 길이", - "일위대가": "UA000007", - "바인딩": { - "종류": "pavement_concrete", - "펼침": [ - "thickness_cm" - ], - "값": "길이", - "묶음": "pv", - "머리틀": [ - "콘크리트포장", - "{0}", - "L" - ], - "값꼴": [ - "T={}cm" - ], - "빈값": [ - "두께 미입력" - ] - }, - "경로": [ - "콘크리트포장", - "T=(두께마다)", - "L" - ] - }, - { - "id": "pv_w", - "머리": [ - "콘크리트포장", - "T=(두께마다)", - "확폭", - null - ], - "단위": "㎡", - "꼴": "수", - "펼침": true, - "설명": "설계값 — 확폭 면적", - "바인딩": { - "종류": "pavement_concrete", - "펼침": [ - "thickness_cm" - ], - "값": "widening_area_m2", - "묶음": "pv", - "머리틀": [ - "콘크리트포장", - "{0}", - "확폭" - ], - "값꼴": [ - "T={}cm" - ], - "빈값": [ - "두께 미입력" - ] - }, - "경로": [ - "콘크리트포장", - "T=(두께마다)", - "확폭" - ] - }, - { - "id": "pv_a", - "머리": [ - "콘크리트포장", - "T=(두께마다)", - "A", - null - ], - "단위": "㎡", - "꼴": "수", - "펼침": true, - "끝수": { - "자리": 2, - "방법": "반올림" - }, - "설명": "계산 — B × L + 확폭", - "일위대가": "UA000006", - "펼침틀": { - "묶음": "pv", - "머리틀": [ - "콘크리트포장", - "{0}", - "A" - ], - "값꼴": [ - "T={}cm" - ], - "빈값": [ - "두께 미입력" - ] - }, - "경로": [ - "콘크리트포장", - "T=(두께마다)", - "A" - ] - }, - { - "id": "pv_jt", - "머리": [ - "콘크리트포장", - "T=(두께마다)", - "수축줄눈", - null - ], - "단위": "m", - "꼴": "수", - "펼침": true, - "끝수": { - "자리": 2, - "방법": "반올림" - }, - "설명": "계산 — 내림(L ÷ 줄눈 간격) × B · 줄눈 간격 = 설계값(기본 6m · 다르면 그 줄 식에 박음)", - "펼침틀": { - "묶음": "pv", - "머리틀": [ - "콘크리트포장", - "{0}", - "수축줄눈" - ], - "값꼴": [ - "T={}cm" - ], - "빈값": [ - "두께 미입력" - ] - }, - "경로": [ - "콘크리트포장", - "T=(두께마다)", - "수축줄눈" - ] - }, - { - "id": "c3", - "머리": [ - "콘크리트포장", - "T=(두께마다)", - "새 분류2", - null - ], - "단위": "m", - "꼴": "수", - "경로": [ - "콘크리트포장", - "T=(두께마다)", - "새 분류2" - ] - }, - { - "id": "pp_len", - "머리": [ - "관공", - "(관종·관경마다)", - "관매설", - null - ], - "단위": "m", - "꼴": "수", - "펼침": true, - "설명": "설계값 — 횡단 관 연장(B06) · 같은 측점에 관이 둘이면 줄을 나눔", - "일위대가": "UA000036", - "일위대가후보": [ - "UA000036", - "UA000027" - ], - "바인딩": { - "종류": "pipe", - "펼침": [ - "pipe_kind", - "pipe_diameter_mm" - ], - "값": "관연장", - "묶음": "pp", - "머리틀": [ - "관공", - "{0} {1}", - "관매설" - ], - "값꼴": [ - "{}", - "Φ{}" - ], - "빈값": [ - "관종 미입력", - "관경 미입력" - ] - }, - "경로": [ - "관공", - "(관종·관경마다)", - "관매설" - ] - }, - { - "id": "pp_cp", - "머리": [ - "관공", - "(관종·관경마다)", - "커플링밴드", - null - ], - "단위": "개소", - "꼴": "수", - "펼침": true, - "설명": "계산 — 올림(L ÷ 1본 길이) − 1 · 관마다(줄마다) 계산 뒤 합 · 1본 길이 = 변수(관종마다 · 빈칸이면 계산 안 함)", - "펼침틀": { - "묶음": "pp", - "머리틀": [ - "관공", - "{0} {1}", - "커플링밴드" - ], - "값꼴": [ - "{}", - "Φ{}" - ], - "빈값": [ - "관종 미입력", - "관경 미입력" - ] - }, - "경로": [ - "관공", - "(관종·관경마다)", - "커플링밴드" - ] - }, - { - "id": "pg_in", - "머리": [ - "관보호공", - "(형식마다)", - "유입", - null - ], - "단위": "개소", - "꼴": "수", - "펼침": true, - "설명": "설계값 — 관 날개벽 형식 · 유입구가 집수정이면 집수정 형식 · 형식마다 열(유출구는 기슭막이 — 돌기슭막이 열)", - "일위대가": "UA000008", - "일위대가후보": [ - "UA000008", - "UA000025", - "UA000029" - ], - "바인딩": { - "종류": "pipe", - "펼침": [ - "wing_wall_type" - ], - "값": "개소", - "묶음": "pg", - "머리틀": [ - "관보호공", - "{0}", - "유입" - ], - "빈값": [ - "형식 미입력" - ], - "이름": { - "A-TYPE": "날개벽 A형", - "C-TYPE": "날개벽 C형", - "A-TYPE+집수정": "날개벽 A형+집수정" - }, - "더함": [ - { - "종류": "pipe", - "펼침": [ - "inlet_basin_form" - ], - "값": "개소", - "조건": { - "inlet_type": "집수정" - } - } - ] - }, - "경로": [ - "관보호공", - "(형식마다)", - "유입" - ] - }, - { - "id": "bx_len", - "머리": [ - "BOX암거", - "(폭×높이마다)", - "연장", - null - ], - "단위": "m", - "꼴": "수", - "펼침": true, - "설명": "설계값 — BOX 연장(계류 방향)", - "일위대가": "UA000014", - "바인딩": { - "종류": "box_culvert", - "펼침": [ - "body_width_m", - "body_height_m" - ], - "값": "length_m", - "묶음": "bx", - "머리틀": [ - "BOX암거", - "{0}×{1}", - "연장" - ], - "빈값": [ - "폭 미입력", - "높이 미입력" - ] - }, - "경로": [ - "BOX암거", - "(폭×높이마다)", - "연장" - ] - }, - { - "id": "bx_wing", - "머리": [ - "BOX암거", - "(폭×높이마다)", - "날개벽", - null - ], - "단위": "개소", - "꼴": "수", - "펼침": true, - "설명": "설계값 — 날개벽 유입·유출 있음 수", - "일위대가": "UA000025", - "바인딩": { - "종류": "box_culvert", - "펼침": [ - "body_width_m", - "body_height_m" - ], - "값": "개소", - "묶음": "bx", - "머리틀": [ - "BOX암거", - "{0}×{1}", - "날개벽" - ], - "빈값": [ - "폭 미입력", - "높이 미입력" - ], - "조건": { - "wing_in": "있음" - }, - "더함": [ - { - "종류": "box_culvert", - "펼침": [ - "body_width_m", - "body_height_m" - ], - "값": "개소", - "조건": { - "wing_out": "있음" - } - } - ] - }, - "경로": [ - "BOX암거", - "(폭×높이마다)", - "날개벽" - ] - }, - { - "id": "fb", - "머리": [ - "세월교", - "(관경 × 련수마다)", - null, - null - ], - "단위": "개소", - "꼴": "수", - "펼침": true, - "설명": "설계값 — 세월교 개소", - "바인딩": { - "종류": "ford_bridge", - "펼침": [ - "pipe_diameter_mm", - "pipe_count" - ], - "값": "개소", - "묶음": "fb", - "머리틀": [ - "세월교", - "{0}×{1}", - null - ], - "값꼴": [ - "Φ{}", - "{}련" - ], - "빈값": [ - "관경 미입력", - "련수 미입력" - ] - }, - "경로": [ - "세월교", - "(관경 × 련수마다)" - ] - }, - { - "id": "fp_w", - "머리": [ - "물넘이포장", - "T=(두께마다)", - "폭", - null - ], - "단위": "m", - "꼴": "수", - "펼침": true, - "설명": "설계값 — 월류 폭", - "바인딩": { - "종류": "ford_pavement", - "펼침": [ - "thickness_cm" - ], - "값": "ford_width_m", - "묶음": "fp", - "머리틀": [ - "물넘이포장", - "{0}", - "폭" - ], - "값꼴": [ - "T={}cm" - ], - "빈값": [ - "두께 미입력" - ] - }, - "경로": [ - "물넘이포장", - "T=(두께마다)", - "폭" - ] - }, - { - "id": "fp_l", - "머리": [ - "물넘이포장", - "T=(두께마다)", - "길이", - null - ], - "단위": "m", - "꼴": "수", - "펼침": true, - "설명": "설계값 — 포장 길이(노폭 방향)", - "바인딩": { - "종류": "ford_pavement", - "펼침": [ - "thickness_cm" - ], - "값": "length_m", - "묶음": "fp", - "머리틀": [ - "물넘이포장", - "{0}", - "길이" - ], - "값꼴": [ - "T={}cm" - ], - "빈값": [ - "두께 미입력" - ] - }, - "경로": [ - "물넘이포장", - "T=(두께마다)", - "길이" - ] - }, - { - "id": "od_len", - "머리": [ - "횡단개거", - "(규격마다)", - "길이", - null - ], - "단위": "m", - "꼴": "수", - "펼침": true, - "설명": "설계값 — 개거 연장", - "일위대가": "UA000026", - "바인딩": { - "종류": "open_ditch", - "펼침": [ - "ditch_spec" - ], - "값": "length_m", - "묶음": "od", - "머리틀": [ - "횡단개거", - "{0}", - "길이" - ], - "빈값": [ - "규격 미입력" - ] - }, - "경로": [ - "횡단개거", - "(규격마다)", - "길이" - ] - }, - { - "id": "od_cnt", - "머리": [ - "횡단개거", - "(규격마다)", - "개소", - null - ], - "단위": "개소", - "꼴": "수", - "펼침": true, - "설명": "설계값 — 개거 · 노출형 횡단수로 개소", - "일위대가": "UA000003", - "바인딩": { - "종류": "open_ditch", - "펼침": [ - "ditch_spec" - ], - "값": "개소", - "묶음": "od", - "머리틀": [ - "횡단개거", - "{0}", - "개소" - ], - "빈값": [ - "규격 미입력" - ], - "더함": [ - { - "종류": "cross_drain_exposed", - "펼침": [ - "=노출형" - ], - "값": "개소" - } - ] - }, - "경로": [ - "횡단개거", - "(규격마다)", - "개소" - ] - }, - { - "id": "rw", - "머리": [ - "옹벽", - "(형식마다)", - "H=(높이마다)", - null - ], - "단위": "m", - "꼴": "수", - "펼침": true, - "설명": "설계값 — 옹벽 길이", - "바인딩": { - "종류": "retaining_wall", - "펼침": [ - "form", - "height_m" - ], - "값": "길이", - "묶음": "rw", - "머리틀": [ - "옹벽", - "{0}", - "{1}" - ], - "값꼴": [ - "{}", - "H={}" - ], - "빈값": [ - "형식 미입력", - "높이 미입력" - ] - }, - "경로": [ - "옹벽", - "(형식마다)", - "H=(높이마다)" - ] - }, - { - "id": "ms", - "머리": [ - "돌쌓기", - "(찰·메마다)", - "H=(높이마다)", - null - ], - "단위": "m", - "꼴": "수", - "펼침": true, - "설명": "설계값 — 돌쌓기 길이", - "바인딩": { - "종류": "masonry_wet", - "펼침": [ - "=찰쌓기", - "height_m" - ], - "값": "길이", - "묶음": "ms", - "머리틀": [ - "돌쌓기", - "{0}", - "{1}" - ], - "값꼴": [ - "{}", - "H={}" - ], - "빈값": [ - "", - "높이 미입력" - ], - "더함": [ - { - "종류": "masonry_dry", - "펼침": [ - "=메쌓기", - "height_m" - ], - "값": "길이" - } - ] - }, - "경로": [ - "돌쌓기", - "(찰·메마다)", - "H=(높이마다)" - ] - }, - { - "id": "sg", - "머리": [ - "흙막이", - "(형태마다)", - "H=(높이마다)", - null - ], - "단위": "m", - "꼴": "수", - "펼침": true, - "설명": "설계값 — 흙막이 길이 · 일위대가는 큰돌만", - "일위대가": "UA000032", - "바인딩": { - "종류": "soil_guard", - "펼침": [ - "form", - "height_m" - ], - "값": "길이", - "묶음": "sg", - "머리틀": [ - "흙막이", - "{0}", - "{1}" - ], - "값꼴": [ - "{}", - "H={}" - ], - "빈값": [ - "형식 미입력", - "높이 미입력" - ] - }, - "경로": [ - "흙막이", - "(형태마다)", - "H=(높이마다)" - ] - }, - { - "id": "bm_len", - "머리": [ - "큰돌쌓기", - "(찰·메 · 돌 크기 · 높이마다)", - "길이", - null - ], - "단위": "m", - "꼴": "수", - "펼침": true, - "설명": "설계값 — 큰돌쌓기 길이", - "바인딩": { - "종류": "boulder_masonry", - "펼침": [ - "bond", - "stone_cm", - "height_m" - ], - "값": "길이", - "묶음": "bm", - "머리틀": [ - "큰돌쌓기", - "{0} {1} {2}", - "길이" - ], - "값꼴": [ - "{}", - "{}", - "H={}" - ], - "빈값": [ - "쌓기 방식 미입력", - "돌규격 미입력", - "높이 미입력" - ] - }, - "경로": [ - "큰돌쌓기", - "(찰·메 · 돌 크기 · 높이마다)", - "길이" - ] - }, - { - "id": "bm_area", - "머리": [ - "큰돌쌓기", - "(찰·메 · 돌 크기 · 높이마다)", - "면적", - null - ], - "단위": "㎡", - "꼴": "수", - "펼침": true, - "설명": "계산 — L × H (H = 펼친 높이)", - "일위대가": "UA000031", - "펼침틀": { - "묶음": "bm", - "머리틀": [ - "큰돌쌓기", - "{0} {1} {2}", - "면적" - ], - "값꼴": [ - "{}", - "{}", - "H={}" - ], - "빈값": [ - "쌓기 방식 미입력", - "돌규격 미입력", - "높이 미입력" - ], - "식틀": "IF([bm_len]>0,[bm_len]*{2},\"\")" - }, - "경로": [ - "큰돌쌓기", - "(찰·메 · 돌 크기 · 높이마다)", - "면적" - ] - }, - { - "id": "ec", - "머리": [ - "골막이", - "(형태마다)", - null, - null - ], - "단위": "개소", - "꼴": "수", - "펼침": true, - "설명": "설계값 — 골막이 개소", - "바인딩": { - "종류": "erosion_check", - "펼침": [ - "form" - ], - "값": "개소", - "묶음": "ec", - "머리틀": [ - "골막이", - "{0}", - null - ], - "빈값": [ - "형식 미입력" - ] - }, - "경로": [ - "골막이", - "(형태마다)" - ] - }, - { - "id": "be", - "머리": [ - "소단", - null, - null, - null - ], - "단위": "m", - "꼴": "수", - "설명": "설계값 — 소단 길이", - "바인딩": { - "종류": "berm", - "값": "길이" - }, - "경로": [ - "소단" - ] - }, - { - "id": "rf", - "머리": [ - "대피소", - null, - null, - null - ], - "단위": "개소", - "꼴": "수", - "설명": "설계값 — 대피소", - "바인딩": { - "종류": "refuge", - "값": "개소" - }, - "경로": [ - "대피소" - ] - }, - { - "id": "c2", - "머리": [ - "대피소", - "새 분류1", - null, - null - ], - "단위": "개소", - "꼴": "수", - "경로": [ - "대피소", - "새 분류1" - ] - }, - { - "id": "wy", - "머리": [ - "정차·작업장", - null, - null, - null - ], - "단위": "개소", - "꼴": "수", - "설명": "설계값 — 정차·작업장", - "바인딩": { - "종류": "work_yard", - "값": "개소" - }, - "경로": [ - "정차·작업장" - ] - }, - { - "id": "ta", - "머리": [ - "차돌림곳", - null, - null, - null - ], - "단위": "개소", - "꼴": "수", - "설명": "설계값 — 차돌림곳", - "바인딩": { - "종류": "turnaround", - "값": "개소" - }, - "경로": [ - "차돌림곳" - ] - }, - { - "id": "gr_rail", - "머리": [ - "가드레일", - null, - null, - null - ], - "단위": "m", - "꼴": "수", - "설명": "설계값 — 가드레일 길이", - "바인딩": { - "종류": "guardrail", - "값": "길이", - "조건": { - "kind": "가드레일" - } - }, - "경로": [ - "가드레일" - ] - }, - { - "id": "gr_curb", - "머리": [ - "경계석", - null, - null, - null - ], - "단위": "m", - "꼴": "수", - "설명": "설계값 — 경계석 길이", - "바인딩": { - "종류": "guardrail", - "값": "길이", - "조건": { - "kind": "경계석" - } - }, - "경로": [ - "경계석" - ] - }, - { - "id": "gr_sign", - "머리": [ - "위험표지", - null, - null, - null - ], - "단위": "개소", - "꼴": "수", - "설명": "설계값 — 위험표지", - "일위대가": "UA000017", - "바인딩": { - "종류": "guardrail", - "값": "개소", - "조건": { - "kind": "위험표지" - } - }, - "경로": [ - "위험표지" - ] - }, - { - "id": "mr", - "머리": [ - "반사경", - null, - null, - null - ], - "단위": "개소", - "꼴": "수", - "설명": "설계값 — 반사경", - "일위대가": "UA000018", - "바인딩": { - "종류": "mirror", - "값": "개소" - }, - "경로": [ - "반사경" - ] - }, - { - "id": "bg", - "머리": [ - "차단기", - null, - null, - null - ], - "단위": "개소", - "꼴": "수", - "설명": "설계값 — 차단기", - "일위대가": "UA000010", - "바인딩": { - "종류": "barrier_gate", - "값": "개소" - }, - "경로": [ - "차단기" - ] - }, - { - "id": "ps", - "머리": [ - "국가지점번호판", - null, - null, - null - ], - "단위": "개소", - "꼴": "수", - "설명": "설계값 — 번호판 · 전구간 줄은 손 입력", - "일위대가": "UA000016", - "바인딩": { - "종류": "position_sign", - "값": "개소" - }, - "경로": [ - "국가지점번호판" - ] - }, - { - "id": "cs", - "머리": [ - "준공표지석", - null, - null, - null - ], - "단위": "개소", - "꼴": "수", - "설명": "설계값 — 준공표지판", - "일위대가": "UA000002", - "바인딩": { - "종류": "completion_sign", - "값": "개소" - }, - "경로": [ - "준공표지석" - ] - }, - { - "id": "etc", - "머리": [ - "기타", - "(이름마다)", - null, - null - ], - "단위": "개소", - "꼴": "수", - "펼침": true, - "설명": "설계값 — 기타 구조물 개소", - "바인딩": { - "종류": "etc", - "펼침": [ - "name" - ], - "값": "개소", - "묶음": "etc", - "머리틀": [ - "기타", - "{0}", - null - ], - "빈값": [ - "이름 미입력" - ] - }, - "경로": [ - "기타", - "(이름마다)" - ] - }, - { - "id": "h_intake", - "머리": [ - "취수정", - null, - null, - null - ], - "단위": "개소", - "꼴": "수", - "손": true, - "설명": "손 입력", - "일위대가": "UA000005", - "경로": [ - "취수정" - ] - }, - { - "id": "h_rip_c", - "머리": [ - "돌붙임", - "찰붙임", - null, - null - ], - "단위": "㎡", - "꼴": "수", - "손": true, - "설명": "손 입력", - "일위대가": "UA000009", - "경로": [ - "돌붙임", - "찰붙임" - ] - }, - { - "id": "h_rip_m", - "머리": [ - "돌붙임", - "메붙임", - null, - null - ], - "단위": "㎡", - "꼴": "수", - "손": true, - "설명": "손 입력", - "일위대가": "UA000009", - "경로": [ - "돌붙임", - "메붙임" - ] - }, - { - "id": "h_stone_ch", - "머리": [ - "돌수로", - null, - null, - null - ], - "단위": "m", - "꼴": "수", - "손": true, - "설명": "손 입력", - "일위대가": "UA000011", - "경로": [ - "돌수로" - ] - }, - { - "id": "h_stone_bed", - "머리": [ - "돌조공", - null, - null, - null - ], - "단위": "m", - "꼴": "수", - "손": true, - "설명": "손 입력", - "일위대가": "UA000023", - "경로": [ - "돌조공" - ] - }, - { - "id": "h_pv_rail", - "머리": [ - "포장 난간", - null, - null, - null - ], - "단위": "m", - "꼴": "수", - "손": true, - "설명": "손 입력", - "일위대가": "UA000012", - "경로": [ - "포장 난간" - ] - }, - { - "id": "h_pg_rail", - "머리": [ - "관보호공 안전난간", - null, - null, - null - ], - "단위": "개소", - "꼴": "수", - "손": true, - "설명": "손 입력", - "일위대가": "UA000013", - "경로": [ - "관보호공 안전난간" - ] - }, - { - "id": "h_soil_ditch", - "머리": [ - "수로형 토사개거", - null, - null, - null - ], - "단위": "개소", - "꼴": "수", - "손": true, - "설명": "손 입력", - "일위대가": "UA000033", - "경로": [ - "수로형 토사개거" - ] - }, - { - "id": "h_ford_basin", - "머리": [ - "물넘이 집수부", - null, - null, - null - ], - "단위": "개소", - "꼴": "수", - "손": true, - "설명": "손 입력", - "일위대가": "UA000034", - "경로": [ - "물넘이 집수부" - ] - }, - { - "id": "h_rockfall", - "머리": [ - "낙석방지책", - null, - null, - null - ], - "단위": "경간", - "꼴": "수", - "손": true, - "설명": "손 입력", - "일위대가": null, - "경로": [ - "낙석방지책" - ] - }, - { - "id": "h_break", - "머리": [ - "콘크리트·포장 깨기", - null, - null, - null - ], - "단위": "㎥", - "꼴": "수", - "손": true, - "설명": "손 입력", - "일위대가": null, - "경로": [ - "콘크리트·포장 깨기" - ] - }, - { - "id": "h_waste", - "머리": [ - "폐기물처리", - null, - null, - null - ], - "단위": "ton", - "꼴": "수", - "손": true, - "설명": "손 입력", - "일위대가": null, - "경로": [ - "폐기물처리" - ] - }, { "id": "memo", "머리": [ @@ -1543,8 +80,7 @@ "열너비": { "no": 40, "sta": 150, - "memo": 120, - "h_soil_ditch": 32 + "memo": 120 } }, "폴더": [] diff --git a/resources/master_template/구조물집계표조합.json b/resources/master_template/구조물집계표조합.json index e66d5b18..55f6a82d 100644 --- a/resources/master_template/구조물집계표조합.json +++ b/resources/master_template/구조물집계표조합.json @@ -1,1555 +1,60 @@ { "그룹": "구조물집계표조합", "판": 1, - "비고": "구조물집계표 구조물 열(꼴=수)마다 한 줄 · 서버가 집계표 [저장] 때 맞춤(손으로 고치지 않음) · 키 SS 는 한 번 주면 안 바뀌고 지운 열의 키는 다시 안 씀 · 경로 = 폴더 이름 배열", + "비고": "구조물집계표 구조물 열(꼴=수)마다 한 줄 · 서버가 집계표 [저장] 때 맞춤(손으로 고치지 않음) · 키(마스터 SS · 회사 SC{회사}- · 개인 SP{사용자}- + 6자리)는 한 번 주면 안 바뀌고 지운 열의 키는 다시 안 씀 · 옛키 = 옮기기 전 키 · 경로 = 폴더 이름 배열", "다음": 52, - "줄": [ - { - "키": "SS000001", - "열": "rv", - "경로": [ - "돌기슭막이", - "메쌓기", - "1.2m", - "기초유" - ], - "이름": "돌기슭막이 메쌓기 1.2m 기초유", - "종류": "돌기슭막이", - "분류1": "메쌓기", - "분류2": "1.2m", - "분류3": "기초유", - "단위": "m", - "일위대가": "UA000004", - "자재조합": null, - "도번": "SD01010100", - "면적번": "SA01010100", - "재료번": "SM01010100", - "바인딩": { - "종류": "revetment", - "펼침": [ - "form", - "height_m" - ], - "값": "length_m", - "묶음": "rv", - "머리틀": [ - "돌기슭막이", - "{0}", - "{1}" - ], - "값꼴": [ - "{}", - "H={}" - ], - "빈값": [ - "형태 미입력", - "높이 미입력" - ], - "이름": { - "돌쌓기(찰)": "찰쌓기", - "돌쌓기(메)": "메쌓기" - }, - "더함": [ - { - "종류": "pipe", - "펼침": [ - "inlet_revet_form", - "inlet_revet_height_m" - ], - "값": "inlet_revet_length_m", - "조건": { - "inlet_type": "기슭막이" - } - }, - { - "종류": "pipe", - "펼침": [ - "outlet_revet_form", - "outlet_revet_height_m" - ], - "값": "outlet_revet_length_m", - "조건": { - "outlet_type": "기슭막이" - } - } - ] - }, - "펼침": true - }, - { - "키": "SS000002", - "열": "c4", - "경로": [ - "돌기슭막이", - "메쌓기", - "1.2m", - "기초무, 소단" - ], - "이름": "돌기슭막이 메쌓기 1.2m 기초무, 소단", - "종류": "돌기슭막이", - "분류1": "메쌓기", - "분류2": "1.2m", - "분류3": "기초무, 소단", - "단위": "m", - "일위대가": "UA000004", - "자재조합": null, - "도번": "SD01010101", - "면적번": "SA01010101", - "재료번": "SM01010101", - "바인딩": null, - "펼침": null - }, - { - "키": "SS000003", - "열": "c1", - "경로": [ - "돌기슭막이", - "메쌓기", - "1.5m", - "기초유" - ], - "이름": "돌기슭막이 메쌓기 1.5m 기초유", - "종류": "돌기슭막이", - "분류1": "메쌓기", - "분류2": "1.5m", - "분류3": "기초유", - "단위": "m", - "일위대가": "UA000004", - "자재조합": null, - "도번": "SD01010200", - "면적번": "SA01010200", - "재료번": "SM01010200", - "바인딩": null, - "펼침": null - }, - { - "키": "SS000004", - "열": "c5", - "경로": [ - "돌기슭막이", - "찰쌓기" - ], - "이름": "돌기슭막이 찰쌓기", - "종류": "돌기슭막이", - "분류1": "찰쌓기", - "분류2": null, - "분류3": null, - "단위": "m", - "일위대가": "UA000004", - "자재조합": null, - "도번": "SD01020000", - "면적번": "SA01020000", - "재료번": "SM01020000", - "바인딩": null, - "펼침": null - }, - { - "키": "SS000005", - "열": "pv_b", - "경로": [ - "콘크리트포장", - "T=(두께마다)", - "B" - ], - "이름": "콘크리트포장 T=(두께마다) B", - "종류": "콘크리트포장", - "분류1": "T=(두께마다)", - "분류2": "B", - "분류3": null, - "단위": "m", - "일위대가": null, - "자재조합": null, - "도번": "SD02010100", - "면적번": "SA02010100", - "재료번": "SM02010100", - "바인딩": { - "종류": "pavement_concrete", - "펼침": [ - "thickness_cm" - ], - "값": "width_m", - "묶음": "pv", - "머리틀": [ - "콘크리트포장", - "{0}", - "B" - ], - "값꼴": [ - "T={}cm" - ], - "빈값": [ - "두께 미입력" - ] - }, - "펼침": true - }, - { - "키": "SS000006", - "열": "pv_l", - "경로": [ - "콘크리트포장", - "T=(두께마다)", - "L" - ], - "이름": "콘크리트포장 T=(두께마다) L", - "종류": "콘크리트포장", - "분류1": "T=(두께마다)", - "분류2": "L", - "분류3": null, - "단위": "m", - "일위대가": "UA000007", - "자재조합": null, - "도번": "SD02010200", - "면적번": "SA02010200", - "재료번": "SM02010200", - "바인딩": { - "종류": "pavement_concrete", - "펼침": [ - "thickness_cm" - ], - "값": "길이", - "묶음": "pv", - "머리틀": [ - "콘크리트포장", - "{0}", - "L" - ], - "값꼴": [ - "T={}cm" - ], - "빈값": [ - "두께 미입력" - ] - }, - "펼침": true - }, - { - "키": "SS000007", - "열": "pv_w", - "경로": [ - "콘크리트포장", - "T=(두께마다)", - "확폭" - ], - "이름": "콘크리트포장 T=(두께마다) 확폭", - "종류": "콘크리트포장", - "분류1": "T=(두께마다)", - "분류2": "확폭", - "분류3": null, - "단위": "㎡", - "일위대가": null, - "자재조합": null, - "도번": "SD02010300", - "면적번": "SA02010300", - "재료번": "SM02010300", - "바인딩": { - "종류": "pavement_concrete", - "펼침": [ - "thickness_cm" - ], - "값": "widening_area_m2", - "묶음": "pv", - "머리틀": [ - "콘크리트포장", - "{0}", - "확폭" - ], - "값꼴": [ - "T={}cm" - ], - "빈값": [ - "두께 미입력" - ] - }, - "펼침": true - }, - { - "키": "SS000008", - "열": "pv_a", - "경로": [ - "콘크리트포장", - "T=(두께마다)", - "A" - ], - "이름": "콘크리트포장 T=(두께마다) A", - "종류": "콘크리트포장", - "분류1": "T=(두께마다)", - "분류2": "A", - "분류3": null, - "단위": "㎡", - "일위대가": "UA000006", - "자재조합": null, - "도번": "SD02010400", - "면적번": "SA02010400", - "재료번": "SM02010400", - "바인딩": null, - "펼침": true - }, - { - "키": "SS000009", - "열": "pv_jt", - "경로": [ - "콘크리트포장", - "T=(두께마다)", - "수축줄눈" - ], - "이름": "콘크리트포장 T=(두께마다) 수축줄눈", - "종류": "콘크리트포장", - "분류1": "T=(두께마다)", - "분류2": "수축줄눈", - "분류3": null, - "단위": "m", - "일위대가": null, - "자재조합": null, - "도번": "SD02010500", - "면적번": "SA02010500", - "재료번": "SM02010500", - "바인딩": null, - "펼침": true - }, - { - "키": "SS000010", - "열": "c3", - "경로": [ - "콘크리트포장", - "T=(두께마다)", - "새 분류2" - ], - "이름": "콘크리트포장 T=(두께마다) 새 분류2", - "종류": "콘크리트포장", - "분류1": "T=(두께마다)", - "분류2": "새 분류2", - "분류3": null, - "단위": "m", - "일위대가": null, - "자재조합": null, - "도번": "SD02010600", - "면적번": "SA02010600", - "재료번": "SM02010600", - "바인딩": null, - "펼침": null - }, - { - "키": "SS000011", - "열": "pp_len", - "경로": [ - "관공", - "(관종·관경마다)", - "관매설" - ], - "이름": "관공 (관종·관경마다) 관매설", - "종류": "관공", - "분류1": "(관종·관경마다)", - "분류2": "관매설", - "분류3": null, - "단위": "m", - "일위대가": "UA000036", - "자재조합": null, - "도번": "SD03010100", - "면적번": "SA03010100", - "재료번": "SM03010100", - "바인딩": { - "종류": "pipe", - "펼침": [ - "pipe_kind", - "pipe_diameter_mm" - ], - "값": "관연장", - "묶음": "pp", - "머리틀": [ - "관공", - "{0} {1}", - "관매설" - ], - "값꼴": [ - "{}", - "Φ{}" - ], - "빈값": [ - "관종 미입력", - "관경 미입력" - ] - }, - "펼침": true - }, - { - "키": "SS000012", - "열": "pp_cp", - "경로": [ - "관공", - "(관종·관경마다)", - "커플링밴드" - ], - "이름": "관공 (관종·관경마다) 커플링밴드", - "종류": "관공", - "분류1": "(관종·관경마다)", - "분류2": "커플링밴드", - "분류3": null, - "단위": "개소", - "일위대가": null, - "자재조합": null, - "도번": "SD03010200", - "면적번": "SA03010200", - "재료번": "SM03010200", - "바인딩": null, - "펼침": true - }, - { - "키": "SS000013", - "열": "pg_in", - "경로": [ - "관보호공", - "(형식마다)", - "유입" - ], - "이름": "관보호공 (형식마다) 유입", - "종류": "관보호공", - "분류1": "(형식마다)", - "분류2": "유입", - "분류3": null, - "단위": "개소", - "일위대가": "UA000008", - "자재조합": null, - "도번": "SD04010100", - "면적번": "SA04010100", - "재료번": "SM04010100", - "바인딩": { - "종류": "pipe", - "펼침": [ - "wing_wall_type" - ], - "값": "개소", - "묶음": "pg", - "머리틀": [ - "관보호공", - "{0}", - "유입" - ], - "빈값": [ - "형식 미입력" - ], - "이름": { - "A-TYPE": "날개벽 A형", - "C-TYPE": "날개벽 C형", - "A-TYPE+집수정": "날개벽 A형+집수정" - }, - "더함": [ - { - "종류": "pipe", - "펼침": [ - "inlet_basin_form" - ], - "값": "개소", - "조건": { - "inlet_type": "집수정" - } - } - ] - }, - "펼침": true - }, - { - "키": "SS000014", - "열": "bx_len", - "경로": [ - "BOX암거", - "(폭×높이마다)", - "연장" - ], - "이름": "BOX암거 (폭×높이마다) 연장", - "종류": "BOX암거", - "분류1": "(폭×높이마다)", - "분류2": "연장", - "분류3": null, - "단위": "m", - "일위대가": "UA000014", - "자재조합": null, - "도번": "SD05010100", - "면적번": "SA05010100", - "재료번": "SM05010100", - "바인딩": { - "종류": "box_culvert", - "펼침": [ - "body_width_m", - "body_height_m" - ], - "값": "length_m", - "묶음": "bx", - "머리틀": [ - "BOX암거", - "{0}×{1}", - "연장" - ], - "빈값": [ - "폭 미입력", - "높이 미입력" - ] - }, - "펼침": true - }, - { - "키": "SS000015", - "열": "bx_wing", - "경로": [ - "BOX암거", - "(폭×높이마다)", - "날개벽" - ], - "이름": "BOX암거 (폭×높이마다) 날개벽", - "종류": "BOX암거", - "분류1": "(폭×높이마다)", - "분류2": "날개벽", - "분류3": null, - "단위": "개소", - "일위대가": "UA000025", - "자재조합": null, - "도번": "SD05010200", - "면적번": "SA05010200", - "재료번": "SM05010200", - "바인딩": { - "종류": "box_culvert", - "펼침": [ - "body_width_m", - "body_height_m" - ], - "값": "개소", - "묶음": "bx", - "머리틀": [ - "BOX암거", - "{0}×{1}", - "날개벽" - ], - "빈값": [ - "폭 미입력", - "높이 미입력" - ], - "조건": { - "wing_in": "있음" - }, - "더함": [ - { - "종류": "box_culvert", - "펼침": [ - "body_width_m", - "body_height_m" - ], - "값": "개소", - "조건": { - "wing_out": "있음" - } - } - ] - }, - "펼침": true - }, - { - "키": "SS000016", - "열": "fb", - "경로": [ - "세월교", - "(관경 × 련수마다)" - ], - "이름": "세월교 (관경 × 련수마다)", - "종류": "세월교", - "분류1": "(관경 × 련수마다)", - "분류2": null, - "분류3": null, - "단위": "개소", - "일위대가": null, - "자재조합": null, - "도번": "SD06010000", - "면적번": "SA06010000", - "재료번": "SM06010000", - "바인딩": { - "종류": "ford_bridge", - "펼침": [ - "pipe_diameter_mm", - "pipe_count" - ], - "값": "개소", - "묶음": "fb", - "머리틀": [ - "세월교", - "{0}×{1}", - null - ], - "값꼴": [ - "Φ{}", - "{}련" - ], - "빈값": [ - "관경 미입력", - "련수 미입력" - ] - }, - "펼침": true - }, - { - "키": "SS000017", - "열": "fp_w", - "경로": [ - "물넘이포장", - "T=(두께마다)", - "폭" - ], - "이름": "물넘이포장 T=(두께마다) 폭", - "종류": "물넘이포장", - "분류1": "T=(두께마다)", - "분류2": "폭", - "분류3": null, - "단위": "m", - "일위대가": null, - "자재조합": null, - "도번": "SD07010100", - "면적번": "SA07010100", - "재료번": "SM07010100", - "바인딩": { - "종류": "ford_pavement", - "펼침": [ - "thickness_cm" - ], - "값": "ford_width_m", - "묶음": "fp", - "머리틀": [ - "물넘이포장", - "{0}", - "폭" - ], - "값꼴": [ - "T={}cm" - ], - "빈값": [ - "두께 미입력" - ] - }, - "펼침": true - }, - { - "키": "SS000018", - "열": "fp_l", - "경로": [ - "물넘이포장", - "T=(두께마다)", - "길이" - ], - "이름": "물넘이포장 T=(두께마다) 길이", - "종류": "물넘이포장", - "분류1": "T=(두께마다)", - "분류2": "길이", - "분류3": null, - "단위": "m", - "일위대가": null, - "자재조합": null, - "도번": "SD07010200", - "면적번": "SA07010200", - "재료번": "SM07010200", - "바인딩": { - "종류": "ford_pavement", - "펼침": [ - "thickness_cm" - ], - "값": "length_m", - "묶음": "fp", - "머리틀": [ - "물넘이포장", - "{0}", - "길이" - ], - "값꼴": [ - "T={}cm" - ], - "빈값": [ - "두께 미입력" - ] - }, - "펼침": true - }, - { - "키": "SS000019", - "열": "od_len", - "경로": [ - "횡단개거", - "(규격마다)", - "길이" - ], - "이름": "횡단개거 (규격마다) 길이", - "종류": "횡단개거", - "분류1": "(규격마다)", - "분류2": "길이", - "분류3": null, - "단위": "m", - "일위대가": "UA000026", - "자재조합": null, - "도번": "SD08010100", - "면적번": "SA08010100", - "재료번": "SM08010100", - "바인딩": { - "종류": "open_ditch", - "펼침": [ - "ditch_spec" - ], - "값": "length_m", - "묶음": "od", - "머리틀": [ - "횡단개거", - "{0}", - "길이" - ], - "빈값": [ - "규격 미입력" - ] - }, - "펼침": true - }, - { - "키": "SS000020", - "열": "od_cnt", - "경로": [ - "횡단개거", - "(규격마다)", - "개소" - ], - "이름": "횡단개거 (규격마다) 개소", - "종류": "횡단개거", - "분류1": "(규격마다)", - "분류2": "개소", - "분류3": null, - "단위": "개소", - "일위대가": "UA000003", - "자재조합": null, - "도번": "SD08010200", - "면적번": "SA08010200", - "재료번": "SM08010200", - "바인딩": { - "종류": "open_ditch", - "펼침": [ - "ditch_spec" - ], - "값": "개소", - "묶음": "od", - "머리틀": [ - "횡단개거", - "{0}", - "개소" - ], - "빈값": [ - "규격 미입력" - ], - "더함": [ - { - "종류": "cross_drain_exposed", - "펼침": [ - "=노출형" - ], - "값": "개소" - } - ] - }, - "펼침": true - }, - { - "키": "SS000021", - "열": "rw", - "경로": [ - "옹벽", - "(형식마다)", - "H=(높이마다)" - ], - "이름": "옹벽 (형식마다) H=(높이마다)", - "종류": "옹벽", - "분류1": "(형식마다)", - "분류2": "H=(높이마다)", - "분류3": null, - "단위": "m", - "일위대가": null, - "자재조합": null, - "도번": "SD09010100", - "면적번": "SA09010100", - "재료번": "SM09010100", - "바인딩": { - "종류": "retaining_wall", - "펼침": [ - "form", - "height_m" - ], - "값": "길이", - "묶음": "rw", - "머리틀": [ - "옹벽", - "{0}", - "{1}" - ], - "값꼴": [ - "{}", - "H={}" - ], - "빈값": [ - "형식 미입력", - "높이 미입력" - ] - }, - "펼침": true - }, - { - "키": "SS000022", - "열": "ms", - "경로": [ - "돌쌓기", - "(찰·메마다)", - "H=(높이마다)" - ], - "이름": "돌쌓기 (찰·메마다) H=(높이마다)", - "종류": "돌쌓기", - "분류1": "(찰·메마다)", - "분류2": "H=(높이마다)", - "분류3": null, - "단위": "m", - "일위대가": null, - "자재조합": null, - "도번": "SD10010100", - "면적번": "SA10010100", - "재료번": "SM10010100", - "바인딩": { - "종류": "masonry_wet", - "펼침": [ - "=찰쌓기", - "height_m" - ], - "값": "길이", - "묶음": "ms", - "머리틀": [ - "돌쌓기", - "{0}", - "{1}" - ], - "값꼴": [ - "{}", - "H={}" - ], - "빈값": [ - "", - "높이 미입력" - ], - "더함": [ - { - "종류": "masonry_dry", - "펼침": [ - "=메쌓기", - "height_m" - ], - "값": "길이" - } - ] - }, - "펼침": true - }, - { - "키": "SS000023", - "열": "sg", - "경로": [ - "흙막이", - "(형태마다)", - "H=(높이마다)" - ], - "이름": "흙막이 (형태마다) H=(높이마다)", - "종류": "흙막이", - "분류1": "(형태마다)", - "분류2": "H=(높이마다)", - "분류3": null, - "단위": "m", - "일위대가": "UA000032", - "자재조합": null, - "도번": "SD11010100", - "면적번": "SA11010100", - "재료번": "SM11010100", - "바인딩": { - "종류": "soil_guard", - "펼침": [ - "form", - "height_m" - ], - "값": "길이", - "묶음": "sg", - "머리틀": [ - "흙막이", - "{0}", - "{1}" - ], - "값꼴": [ - "{}", - "H={}" - ], - "빈값": [ - "형식 미입력", - "높이 미입력" - ] - }, - "펼침": true - }, - { - "키": "SS000024", - "열": "bm_len", - "경로": [ - "큰돌쌓기", - "(찰·메 · 돌 크기 · 높이마다)", - "길이" - ], - "이름": "큰돌쌓기 (찰·메 · 돌 크기 · 높이마다) 길이", - "종류": "큰돌쌓기", - "분류1": "(찰·메 · 돌 크기 · 높이마다)", - "분류2": "길이", - "분류3": null, - "단위": "m", - "일위대가": null, - "자재조합": null, - "도번": "SD12010100", - "면적번": "SA12010100", - "재료번": "SM12010100", - "바인딩": { - "종류": "boulder_masonry", - "펼침": [ - "bond", - "stone_cm", - "height_m" - ], - "값": "길이", - "묶음": "bm", - "머리틀": [ - "큰돌쌓기", - "{0} {1} {2}", - "길이" - ], - "값꼴": [ - "{}", - "{}", - "H={}" - ], - "빈값": [ - "쌓기 방식 미입력", - "돌규격 미입력", - "높이 미입력" - ] - }, - "펼침": true - }, - { - "키": "SS000025", - "열": "bm_area", - "경로": [ - "큰돌쌓기", - "(찰·메 · 돌 크기 · 높이마다)", - "면적" - ], - "이름": "큰돌쌓기 (찰·메 · 돌 크기 · 높이마다) 면적", - "종류": "큰돌쌓기", - "분류1": "(찰·메 · 돌 크기 · 높이마다)", - "분류2": "면적", - "분류3": null, - "단위": "㎡", - "일위대가": "UA000031", - "자재조합": null, - "도번": "SD12010200", - "면적번": "SA12010200", - "재료번": "SM12010200", - "바인딩": null, - "펼침": true - }, - { - "키": "SS000026", - "열": "ec", - "경로": [ - "골막이", - "(형태마다)" - ], - "이름": "골막이 (형태마다)", - "종류": "골막이", - "분류1": "(형태마다)", - "분류2": null, - "분류3": null, - "단위": "개소", - "일위대가": null, - "자재조합": null, - "도번": "SD13010000", - "면적번": "SA13010000", - "재료번": "SM13010000", - "바인딩": { - "종류": "erosion_check", - "펼침": [ - "form" - ], - "값": "개소", - "묶음": "ec", - "머리틀": [ - "골막이", - "{0}", - null - ], - "빈값": [ - "형식 미입력" - ] - }, - "펼침": true - }, - { - "키": "SS000027", - "열": "be", - "경로": [ - "소단" - ], - "이름": "소단", - "종류": "소단", - "분류1": null, - "분류2": null, - "분류3": null, - "단위": "m", - "일위대가": null, - "자재조합": null, - "도번": "SD14000000", - "면적번": "SA14000000", - "재료번": "SM14000000", - "바인딩": { - "종류": "berm", - "값": "길이" - }, - "펼침": null - }, - { - "키": "SS000028", - "열": "rf", - "경로": [ - "대피소" - ], - "이름": "대피소", - "종류": "대피소", - "분류1": null, - "분류2": null, - "분류3": null, - "단위": "개소", - "일위대가": null, - "자재조합": null, - "도번": "SD15000000", - "면적번": "SA15000000", - "재료번": "SM15000000", - "바인딩": { - "종류": "refuge", - "값": "개소" - }, - "펼침": null - }, - { - "키": "SS000029", - "열": "c2", - "경로": [ - "대피소", - "새 분류1" - ], - "이름": "대피소 새 분류1", - "종류": "대피소", - "분류1": "새 분류1", - "분류2": null, - "분류3": null, - "단위": "개소", - "일위대가": null, - "자재조합": null, - "도번": "SD15010000", - "면적번": "SA15010000", - "재료번": "SM15010000", - "바인딩": null, - "펼침": null - }, - { - "키": "SS000030", - "열": "wy", - "경로": [ - "정차·작업장" - ], - "이름": "정차·작업장", - "종류": "정차·작업장", - "분류1": null, - "분류2": null, - "분류3": null, - "단위": "개소", - "일위대가": null, - "자재조합": null, - "도번": "SD16000000", - "면적번": "SA16000000", - "재료번": "SM16000000", - "바인딩": { - "종류": "work_yard", - "값": "개소" - }, - "펼침": null - }, - { - "키": "SS000031", - "열": "ta", - "경로": [ - "차돌림곳" - ], - "이름": "차돌림곳", - "종류": "차돌림곳", - "분류1": null, - "분류2": null, - "분류3": null, - "단위": "개소", - "일위대가": null, - "자재조합": null, - "도번": "SD17000000", - "면적번": "SA17000000", - "재료번": "SM17000000", - "바인딩": { - "종류": "turnaround", - "값": "개소" - }, - "펼침": null - }, - { - "키": "SS000032", - "열": "gr_rail", - "경로": [ - "가드레일" - ], - "이름": "가드레일", - "종류": "가드레일", - "분류1": null, - "분류2": null, - "분류3": null, - "단위": "m", - "일위대가": null, - "자재조합": null, - "도번": "SD18000000", - "면적번": "SA18000000", - "재료번": "SM18000000", - "바인딩": { - "종류": "guardrail", - "값": "길이", - "조건": { - "kind": "가드레일" - } - }, - "펼침": null - }, - { - "키": "SS000033", - "열": "gr_curb", - "경로": [ - "경계석" - ], - "이름": "경계석", - "종류": "경계석", - "분류1": null, - "분류2": null, - "분류3": null, - "단위": "m", - "일위대가": null, - "자재조합": null, - "도번": "SD19000000", - "면적번": "SA19000000", - "재료번": "SM19000000", - "바인딩": { - "종류": "guardrail", - "값": "길이", - "조건": { - "kind": "경계석" - } - }, - "펼침": null - }, - { - "키": "SS000034", - "열": "gr_sign", - "경로": [ - "위험표지" - ], - "이름": "위험표지", - "종류": "위험표지", - "분류1": null, - "분류2": null, - "분류3": null, - "단위": "개소", - "일위대가": "UA000017", - "자재조합": null, - "도번": "SD20000000", - "면적번": "SA20000000", - "재료번": "SM20000000", - "바인딩": { - "종류": "guardrail", - "값": "개소", - "조건": { - "kind": "위험표지" - } - }, - "펼침": null - }, - { - "키": "SS000035", - "열": "mr", - "경로": [ - "반사경" - ], - "이름": "반사경", - "종류": "반사경", - "분류1": null, - "분류2": null, - "분류3": null, - "단위": "개소", - "일위대가": "UA000018", - "자재조합": null, - "도번": "SD21000000", - "면적번": "SA21000000", - "재료번": "SM21000000", - "바인딩": { - "종류": "mirror", - "값": "개소" - }, - "펼침": null - }, - { - "키": "SS000036", - "열": "bg", - "경로": [ - "차단기" - ], - "이름": "차단기", - "종류": "차단기", - "분류1": null, - "분류2": null, - "분류3": null, - "단위": "개소", - "일위대가": "UA000010", - "자재조합": null, - "도번": "SD22000000", - "면적번": "SA22000000", - "재료번": "SM22000000", - "바인딩": { - "종류": "barrier_gate", - "값": "개소" - }, - "펼침": null - }, - { - "키": "SS000037", - "열": "ps", - "경로": [ - "국가지점번호판" - ], - "이름": "국가지점번호판", - "종류": "국가지점번호판", - "분류1": null, - "분류2": null, - "분류3": null, - "단위": "개소", - "일위대가": "UA000016", - "자재조합": null, - "도번": "SD23000000", - "면적번": "SA23000000", - "재료번": "SM23000000", - "바인딩": { - "종류": "position_sign", - "값": "개소" - }, - "펼침": null - }, - { - "키": "SS000038", - "열": "cs", - "경로": [ - "준공표지석" - ], - "이름": "준공표지석", - "종류": "준공표지석", - "분류1": null, - "분류2": null, - "분류3": null, - "단위": "개소", - "일위대가": "UA000002", - "자재조합": null, - "도번": "SD24000000", - "면적번": "SA24000000", - "재료번": "SM24000000", - "바인딩": { - "종류": "completion_sign", - "값": "개소" - }, - "펼침": null - }, - { - "키": "SS000039", - "열": "etc", - "경로": [ - "기타", - "(이름마다)" - ], - "이름": "기타 (이름마다)", - "종류": "기타", - "분류1": "(이름마다)", - "분류2": null, - "분류3": null, - "단위": "개소", - "일위대가": null, - "자재조합": null, - "도번": "SD25010000", - "면적번": "SA25010000", - "재료번": "SM25010000", - "바인딩": { - "종류": "etc", - "펼침": [ - "name" - ], - "값": "개소", - "묶음": "etc", - "머리틀": [ - "기타", - "{0}", - null - ], - "빈값": [ - "이름 미입력" - ] - }, - "펼침": true - }, - { - "키": "SS000040", - "열": "h_intake", - "경로": [ - "취수정" - ], - "이름": "취수정", - "종류": "취수정", - "분류1": null, - "분류2": null, - "분류3": null, - "단위": "개소", - "일위대가": "UA000005", - "자재조합": null, - "도번": "SD26000000", - "면적번": "SA26000000", - "재료번": "SM26000000", - "바인딩": null, - "펼침": null - }, - { - "키": "SS000041", - "열": "h_rip_c", - "경로": [ - "돌붙임", - "찰붙임" - ], - "이름": "돌붙임 찰붙임", - "종류": "돌붙임", - "분류1": "찰붙임", - "분류2": null, - "분류3": null, - "단위": "㎡", - "일위대가": "UA000009", - "자재조합": null, - "도번": "SD27010000", - "면적번": "SA27010000", - "재료번": "SM27010000", - "바인딩": null, - "펼침": null - }, - { - "키": "SS000042", - "열": "h_rip_m", - "경로": [ - "돌붙임", - "메붙임" - ], - "이름": "돌붙임 메붙임", - "종류": "돌붙임", - "분류1": "메붙임", - "분류2": null, - "분류3": null, - "단위": "㎡", - "일위대가": "UA000009", - "자재조합": null, - "도번": "SD27020000", - "면적번": "SA27020000", - "재료번": "SM27020000", - "바인딩": null, - "펼침": null - }, - { - "키": "SS000043", - "열": "h_stone_ch", - "경로": [ - "돌수로" - ], - "이름": "돌수로", - "종류": "돌수로", - "분류1": null, - "분류2": null, - "분류3": null, - "단위": "m", - "일위대가": "UA000011", - "자재조합": null, - "도번": "SD28000000", - "면적번": "SA28000000", - "재료번": "SM28000000", - "바인딩": null, - "펼침": null - }, - { - "키": "SS000044", - "열": "h_stone_bed", - "경로": [ - "돌조공" - ], - "이름": "돌조공", - "종류": "돌조공", - "분류1": null, - "분류2": null, - "분류3": null, - "단위": "m", - "일위대가": "UA000023", - "자재조합": null, - "도번": "SD29000000", - "면적번": "SA29000000", - "재료번": "SM29000000", - "바인딩": null, - "펼침": null - }, - { - "키": "SS000045", - "열": "h_pv_rail", - "경로": [ - "포장 난간" - ], - "이름": "포장 난간", - "종류": "포장 난간", - "분류1": null, - "분류2": null, - "분류3": null, - "단위": "m", - "일위대가": "UA000012", - "자재조합": null, - "도번": "SD30000000", - "면적번": "SA30000000", - "재료번": "SM30000000", - "바인딩": null, - "펼침": null - }, - { - "키": "SS000046", - "열": "h_pg_rail", - "경로": [ - "관보호공 안전난간" - ], - "이름": "관보호공 안전난간", - "종류": "관보호공 안전난간", - "분류1": null, - "분류2": null, - "분류3": null, - "단위": "개소", - "일위대가": "UA000013", - "자재조합": null, - "도번": "SD31000000", - "면적번": "SA31000000", - "재료번": "SM31000000", - "바인딩": null, - "펼침": null - }, - { - "키": "SS000047", - "열": "h_soil_ditch", - "경로": [ - "수로형 토사개거" - ], - "이름": "수로형 토사개거", - "종류": "수로형 토사개거", - "분류1": null, - "분류2": null, - "분류3": null, - "단위": "개소", - "일위대가": "UA000033", - "자재조합": null, - "도번": "SD32000000", - "면적번": "SA32000000", - "재료번": "SM32000000", - "바인딩": null, - "펼침": null - }, - { - "키": "SS000048", - "열": "h_ford_basin", - "경로": [ - "물넘이 집수부" - ], - "이름": "물넘이 집수부", - "종류": "물넘이 집수부", - "분류1": null, - "분류2": null, - "분류3": null, - "단위": "개소", - "일위대가": "UA000034", - "자재조합": null, - "도번": "SD33000000", - "면적번": "SA33000000", - "재료번": "SM33000000", - "바인딩": null, - "펼침": null - }, - { - "키": "SS000049", - "열": "h_rockfall", - "경로": [ - "낙석방지책" - ], - "이름": "낙석방지책", - "종류": "낙석방지책", - "분류1": null, - "분류2": null, - "분류3": null, - "단위": "경간", - "일위대가": null, - "자재조합": null, - "도번": "SD34000000", - "면적번": "SA34000000", - "재료번": "SM34000000", - "바인딩": null, - "펼침": null - }, - { - "키": "SS000050", - "열": "h_break", - "경로": [ - "콘크리트·포장 깨기" - ], - "이름": "콘크리트·포장 깨기", - "종류": "콘크리트·포장 깨기", - "분류1": null, - "분류2": null, - "분류3": null, - "단위": "㎥", - "일위대가": null, - "자재조합": null, - "도번": "SD35000000", - "면적번": "SA35000000", - "재료번": "SM35000000", - "바인딩": null, - "펼침": null - }, - { - "키": "SS000051", - "열": "h_waste", - "경로": [ - "폐기물처리" - ], - "이름": "폐기물처리", - "종류": "폐기물처리", - "분류1": null, - "분류2": null, - "분류3": null, - "단위": "ton", - "일위대가": null, - "자재조합": null, - "도번": "SD36000000", - "면적번": "SA36000000", - "재료번": "SM36000000", - "바인딩": null, - "펼침": null - } - ] + "지운번호": [ + "01010100", + "01010101", + "01010200", + "01020000", + "02010100", + "02010200", + "02010300", + "02010400", + "02010500", + "02010600", + "03010100", + "03010200", + "04010100", + "05010100", + "05010200", + "06010000", + "07010100", + "07010200", + "08010100", + "08010200", + "09010100", + "10010100", + "11010100", + "12010100", + "12010200", + "13010000", + "14000000", + "15000000", + "15010000", + "16000000", + "17000000", + "18000000", + "19000000", + "20000000", + "21000000", + "22000000", + "23000000", + "24000000", + "25010000", + "26000000", + "27010000", + "27020000", + "28000000", + "29000000", + "30000000", + "31000000", + "32000000", + "33000000", + "34000000", + "35000000", + "36000000" + ], + "줄": [] } diff --git a/resources/master_template/area/c4.json b/resources/tester/fixtures/m02_master_sample/area/c4.json similarity index 100% rename from resources/master_template/area/c4.json rename to resources/tester/fixtures/m02_master_sample/area/c4.json diff --git a/resources/master_template/area/ms.json b/resources/tester/fixtures/m02_master_sample/area/ms.json similarity index 100% rename from resources/master_template/area/ms.json rename to resources/tester/fixtures/m02_master_sample/area/ms.json diff --git a/resources/master_template/area/pp_len.json b/resources/tester/fixtures/m02_master_sample/area/pp_len.json similarity index 100% rename from resources/master_template/area/pp_len.json rename to resources/tester/fixtures/m02_master_sample/area/pp_len.json diff --git a/resources/master_template/area/rv.json b/resources/tester/fixtures/m02_master_sample/area/rv.json similarity index 100% rename from resources/master_template/area/rv.json rename to resources/tester/fixtures/m02_master_sample/area/rv.json diff --git a/resources/master_template/material/c4.json b/resources/tester/fixtures/m02_master_sample/material/c4.json similarity index 100% rename from resources/master_template/material/c4.json rename to resources/tester/fixtures/m02_master_sample/material/c4.json diff --git a/resources/master_template/material/ms.json b/resources/tester/fixtures/m02_master_sample/material/ms.json similarity index 100% rename from resources/master_template/material/ms.json rename to resources/tester/fixtures/m02_master_sample/material/ms.json diff --git a/resources/master_template/material/pp_len.json b/resources/tester/fixtures/m02_master_sample/material/pp_len.json similarity index 100% rename from resources/master_template/material/pp_len.json rename to resources/tester/fixtures/m02_master_sample/material/pp_len.json diff --git a/resources/master_template/material/rv.json b/resources/tester/fixtures/m02_master_sample/material/rv.json similarity index 100% rename from resources/master_template/material/rv.json rename to resources/tester/fixtures/m02_master_sample/material/rv.json diff --git a/resources/master_template/structure/c4.json b/resources/tester/fixtures/m02_master_sample/structure/c4.json similarity index 79% rename from resources/master_template/structure/c4.json rename to resources/tester/fixtures/m02_master_sample/structure/c4.json index b189032d..a1b37243 100644 --- a/resources/master_template/structure/c4.json +++ b/resources/tester/fixtures/m02_master_sample/structure/c4.json @@ -56,23 +56,6 @@ "y": 265.72465372792067 } } - }, - { - "id": "358697a5-1159-4aa4-bc84-100ed5b27f82", - "type": "Line", - "lineColor": "#ffffff", - "lineWidth": 1, - "layerId": "기본", - "shapeData": { - "startPoint": { - "x": 408.7256717135351, - "y": 265.72465372792067 - }, - "endPoint": { - "x": 231.45113982994633, - "y": 277.54295585349314 - } - } } ], "layers": [ diff --git a/resources/master_template/structure/ms.json b/resources/tester/fixtures/m02_master_sample/structure/ms.json similarity index 100% rename from resources/master_template/structure/ms.json rename to resources/tester/fixtures/m02_master_sample/structure/ms.json diff --git a/resources/master_template/structure/pp_len.json b/resources/tester/fixtures/m02_master_sample/structure/pp_len.json similarity index 100% rename from resources/master_template/structure/pp_len.json rename to resources/tester/fixtures/m02_master_sample/structure/pp_len.json diff --git a/resources/tester/fixtures/m02_master_sample/structure/rv.json b/resources/tester/fixtures/m02_master_sample/structure/rv.json new file mode 100644 index 00000000..674a6cb7 --- /dev/null +++ b/resources/tester/fixtures/m02_master_sample/structure/rv.json @@ -0,0 +1,77 @@ +{ + "양식": "구조물도면", + "종류": "structure", + "판": 1, + "열": "rv", + "도번": "SD01010100", + "도면": { + "format": 6, + "entities": [ + { + "id": "6f01250e-97dd-4e69-8743-8ae8968b7cea", + "type": "Line", + "lineColor": "#fff", + "lineWidth": 1, + "layerId": "b08-frame", + "shapeData": { + "startPoint": { + "x": 4255.194526991341, + "y": 5857.308300811681 + }, + "endPoint": { + "x": 5342.438965520298, + "y": 5857.308300811681 + } + } + }, + { + "id": "408cfd22-a8e3-46da-ae11-d2b50fc97b79", + "type": "Line", + "lineColor": "#fff", + "lineWidth": 1, + "layerId": "b08-frame", + "shapeData": { + "startPoint": { + "x": 5842.438965520298, + "y": 5357.308300811681 + }, + "endPoint": { + "x": 5342.438965520298, + "y": 5857.308300811681 + } + } + }, + { + "id": "1865b0b5-c1f2-45ce-8999-05a5deb4bc90", + "type": "Line", + "lineColor": "#fff", + "lineWidth": 1, + "layerId": "b08-frame", + "shapeData": { + "startPoint": { + "x": 5842.438965520298, + "y": 5357.308300811681 + }, + "endPoint": { + "x": 6842.438965520298, + "y": 5357.308300811681 + } + } + } + ], + "layers": [ + { + "id": "b08-frame", + "name": "도각", + "isVisible": true, + "isLocked": false + } + ], + "drawing_area": [ + 42, + 47, + 812, + 567 + ] + } +} diff --git a/resources/tester/fixtures/m02_master_sample/table/구조물집계표.json b/resources/tester/fixtures/m02_master_sample/table/구조물집계표.json new file mode 100644 index 00000000..860fb71b --- /dev/null +++ b/resources/tester/fixtures/m02_master_sample/table/구조물집계표.json @@ -0,0 +1,1551 @@ +{ + "양식": "구조물집계표", + "종류": "표", + "판": 1, + "설명": "측점마다 구조물 수량 한 줄 · 머리 4층(종류 · 공법 · 규격 · 단위) · 설계값 열은 프로젝트 설계에서 채움(화면에서 못 고침 — 설계를 고침) · 손 열은 사용자가 적음 · 계산 열은 식. 종류 정본 = B05_Profile/B05_Profile_Structure_Types.json.", + "바인딩규칙": "종류 = type_id · 펼침 = 값마다 열을 나눌 칸 키(앞에 = 이면 고정 글) · 값 = 칸 키 | 개소(1건 1) | 길이(length_m, 없으면 끝-시작) | 관연장(B06 횡단 pipe_length_m) · 묶음 = 같은 펼침으로 함께 나뉘는 열 · 머리틀 = 펼친 머리({0}{1}… = 펼침 값 차례) · 이름 = 설계 값 → 머리 글 · 조건 = {칸: 값}(맞는 것만) · 더함 = 같은 열에 더하는 다른 출처 · 식틀 = 펼친 열의 식({0}… 펼침 값) · 값꼴 = 자리마다 머리 글 꼴({} = 값) · 빈값 = 설계값이 빈 자리 머리 글 · 펼침틀 = 계산 열이 같은 묶음으로 따라 나뉨(묶음 · 머리틀 · 값꼴 · 빈값 · 식틀 — 설계값을 받지 않아 바인딩 없음)", + "층": [ + "종류", + "분류1", + "분류2", + "분류3" + ], + "변수": { + "파형강관_1본_m": 8, + "흄관_1본_m": "", + "VR관_1본_m": "", + "수축줄눈_간격_m": 6, + "측점간격_m": 20 + }, + "쪽줄": 50, + "열": [ + { + "id": "no", + "머리": [ + "NO", + null, + null, + null + ], + "단위": null, + "꼴": "글", + "설명": "줄 차례(전구간 줄 빼고 1부터)" + }, + { + "id": "sta", + "머리": [ + "측점", + null, + null, + null + ], + "단위": null, + "꼴": "글", + "설명": "점 NO.x+y · 구간 NO.a~NO.b · 맨 위 전구간" + }, + { + "id": "rv", + "머리": [ + "돌기슭막이", + "메쌓기", + "1.2m", + "기초유" + ], + "단위": "m", + "꼴": "수", + "펼침": true, + "설명": "설계값 — 기슭막이 · 관 유입·유출 기슭막이 길이 · 형태 → 높이마다 열", + "일위대가": "UA000004", + "바인딩": { + "종류": "revetment", + "펼침": [ + "form", + "height_m" + ], + "값": "length_m", + "묶음": "rv", + "머리틀": [ + "돌기슭막이", + "{0}", + "{1}" + ], + "값꼴": [ + "{}", + "H={}" + ], + "빈값": [ + "형태 미입력", + "높이 미입력" + ], + "이름": { + "돌쌓기(찰)": "찰쌓기", + "돌쌓기(메)": "메쌓기" + }, + "더함": [ + { + "종류": "pipe", + "펼침": [ + "inlet_revet_form", + "inlet_revet_height_m" + ], + "값": "inlet_revet_length_m", + "조건": { + "inlet_type": "기슭막이" + } + }, + { + "종류": "pipe", + "펼침": [ + "outlet_revet_form", + "outlet_revet_height_m" + ], + "값": "outlet_revet_length_m", + "조건": { + "outlet_type": "기슭막이" + } + } + ] + }, + "경로": [ + "돌기슭막이", + "메쌓기", + "1.2m", + "기초유" + ] + }, + { + "id": "c4", + "머리": [ + "돌기슭막이", + "메쌓기", + "1.2m", + "기초무, 소단" + ], + "단위": "m", + "꼴": "수", + "일위대가": "UA000004", + "경로": [ + "돌기슭막이", + "메쌓기", + "1.2m", + "기초무, 소단" + ] + }, + { + "id": "c1", + "머리": [ + "돌기슭막이", + "메쌓기", + "1.5m", + "기초유" + ], + "단위": "m", + "꼴": "수", + "일위대가": "UA000004", + "경로": [ + "돌기슭막이", + "메쌓기", + "1.5m", + "기초유" + ] + }, + { + "id": "c5", + "머리": [ + "돌기슭막이", + "찰쌓기", + null, + null + ], + "단위": "m", + "꼴": "수", + "일위대가": "UA000004", + "경로": [ + "돌기슭막이", + "찰쌓기" + ] + }, + { + "id": "pv_b", + "머리": [ + "콘크리트포장", + "T=(두께마다)", + "B", + null + ], + "단위": "m", + "꼴": "수", + "펼침": true, + "설명": "설계값 — 포장 폭", + "바인딩": { + "종류": "pavement_concrete", + "펼침": [ + "thickness_cm" + ], + "값": "width_m", + "묶음": "pv", + "머리틀": [ + "콘크리트포장", + "{0}", + "B" + ], + "값꼴": [ + "T={}cm" + ], + "빈값": [ + "두께 미입력" + ] + }, + "경로": [ + "콘크리트포장", + "T=(두께마다)", + "B" + ] + }, + { + "id": "pv_l", + "머리": [ + "콘크리트포장", + "T=(두께마다)", + "L", + null + ], + "단위": "m", + "꼴": "수", + "펼침": true, + "설명": "설계값 — 포장 길이", + "일위대가": "UA000007", + "바인딩": { + "종류": "pavement_concrete", + "펼침": [ + "thickness_cm" + ], + "값": "길이", + "묶음": "pv", + "머리틀": [ + "콘크리트포장", + "{0}", + "L" + ], + "값꼴": [ + "T={}cm" + ], + "빈값": [ + "두께 미입력" + ] + }, + "경로": [ + "콘크리트포장", + "T=(두께마다)", + "L" + ] + }, + { + "id": "pv_w", + "머리": [ + "콘크리트포장", + "T=(두께마다)", + "확폭", + null + ], + "단위": "㎡", + "꼴": "수", + "펼침": true, + "설명": "설계값 — 확폭 면적", + "바인딩": { + "종류": "pavement_concrete", + "펼침": [ + "thickness_cm" + ], + "값": "widening_area_m2", + "묶음": "pv", + "머리틀": [ + "콘크리트포장", + "{0}", + "확폭" + ], + "값꼴": [ + "T={}cm" + ], + "빈값": [ + "두께 미입력" + ] + }, + "경로": [ + "콘크리트포장", + "T=(두께마다)", + "확폭" + ] + }, + { + "id": "pv_a", + "머리": [ + "콘크리트포장", + "T=(두께마다)", + "A", + null + ], + "단위": "㎡", + "꼴": "수", + "펼침": true, + "끝수": { + "자리": 2, + "방법": "반올림" + }, + "설명": "계산 — B × L + 확폭", + "일위대가": "UA000006", + "펼침틀": { + "묶음": "pv", + "머리틀": [ + "콘크리트포장", + "{0}", + "A" + ], + "값꼴": [ + "T={}cm" + ], + "빈값": [ + "두께 미입력" + ] + }, + "경로": [ + "콘크리트포장", + "T=(두께마다)", + "A" + ] + }, + { + "id": "pv_jt", + "머리": [ + "콘크리트포장", + "T=(두께마다)", + "수축줄눈", + null + ], + "단위": "m", + "꼴": "수", + "펼침": true, + "끝수": { + "자리": 2, + "방법": "반올림" + }, + "설명": "계산 — 내림(L ÷ 줄눈 간격) × B · 줄눈 간격 = 설계값(기본 6m · 다르면 그 줄 식에 박음)", + "펼침틀": { + "묶음": "pv", + "머리틀": [ + "콘크리트포장", + "{0}", + "수축줄눈" + ], + "값꼴": [ + "T={}cm" + ], + "빈값": [ + "두께 미입력" + ] + }, + "경로": [ + "콘크리트포장", + "T=(두께마다)", + "수축줄눈" + ] + }, + { + "id": "c3", + "머리": [ + "콘크리트포장", + "T=(두께마다)", + "새 분류2", + null + ], + "단위": "m", + "꼴": "수", + "경로": [ + "콘크리트포장", + "T=(두께마다)", + "새 분류2" + ] + }, + { + "id": "pp_len", + "머리": [ + "관공", + "(관종·관경마다)", + "관매설", + null + ], + "단위": "m", + "꼴": "수", + "펼침": true, + "설명": "설계값 — 횡단 관 연장(B06) · 같은 측점에 관이 둘이면 줄을 나눔", + "일위대가": "UA000036", + "일위대가후보": [ + "UA000036", + "UA000027" + ], + "바인딩": { + "종류": "pipe", + "펼침": [ + "pipe_kind", + "pipe_diameter_mm" + ], + "값": "관연장", + "묶음": "pp", + "머리틀": [ + "관공", + "{0} {1}", + "관매설" + ], + "값꼴": [ + "{}", + "Φ{}" + ], + "빈값": [ + "관종 미입력", + "관경 미입력" + ] + }, + "경로": [ + "관공", + "(관종·관경마다)", + "관매설" + ] + }, + { + "id": "pp_cp", + "머리": [ + "관공", + "(관종·관경마다)", + "커플링밴드", + null + ], + "단위": "개소", + "꼴": "수", + "펼침": true, + "설명": "계산 — 올림(L ÷ 1본 길이) − 1 · 관마다(줄마다) 계산 뒤 합 · 1본 길이 = 변수(관종마다 · 빈칸이면 계산 안 함)", + "펼침틀": { + "묶음": "pp", + "머리틀": [ + "관공", + "{0} {1}", + "커플링밴드" + ], + "값꼴": [ + "{}", + "Φ{}" + ], + "빈값": [ + "관종 미입력", + "관경 미입력" + ] + }, + "경로": [ + "관공", + "(관종·관경마다)", + "커플링밴드" + ] + }, + { + "id": "pg_in", + "머리": [ + "관보호공", + "(형식마다)", + "유입", + null + ], + "단위": "개소", + "꼴": "수", + "펼침": true, + "설명": "설계값 — 관 날개벽 형식 · 유입구가 집수정이면 집수정 형식 · 형식마다 열(유출구는 기슭막이 — 돌기슭막이 열)", + "일위대가": "UA000008", + "일위대가후보": [ + "UA000008", + "UA000025", + "UA000029" + ], + "바인딩": { + "종류": "pipe", + "펼침": [ + "wing_wall_type" + ], + "값": "개소", + "묶음": "pg", + "머리틀": [ + "관보호공", + "{0}", + "유입" + ], + "빈값": [ + "형식 미입력" + ], + "이름": { + "A-TYPE": "날개벽 A형", + "C-TYPE": "날개벽 C형", + "A-TYPE+집수정": "날개벽 A형+집수정" + }, + "더함": [ + { + "종류": "pipe", + "펼침": [ + "inlet_basin_form" + ], + "값": "개소", + "조건": { + "inlet_type": "집수정" + } + } + ] + }, + "경로": [ + "관보호공", + "(형식마다)", + "유입" + ] + }, + { + "id": "bx_len", + "머리": [ + "BOX암거", + "(폭×높이마다)", + "연장", + null + ], + "단위": "m", + "꼴": "수", + "펼침": true, + "설명": "설계값 — BOX 연장(계류 방향)", + "일위대가": "UA000014", + "바인딩": { + "종류": "box_culvert", + "펼침": [ + "body_width_m", + "body_height_m" + ], + "값": "length_m", + "묶음": "bx", + "머리틀": [ + "BOX암거", + "{0}×{1}", + "연장" + ], + "빈값": [ + "폭 미입력", + "높이 미입력" + ] + }, + "경로": [ + "BOX암거", + "(폭×높이마다)", + "연장" + ] + }, + { + "id": "bx_wing", + "머리": [ + "BOX암거", + "(폭×높이마다)", + "날개벽", + null + ], + "단위": "개소", + "꼴": "수", + "펼침": true, + "설명": "설계값 — 날개벽 유입·유출 있음 수", + "일위대가": "UA000025", + "바인딩": { + "종류": "box_culvert", + "펼침": [ + "body_width_m", + "body_height_m" + ], + "값": "개소", + "묶음": "bx", + "머리틀": [ + "BOX암거", + "{0}×{1}", + "날개벽" + ], + "빈값": [ + "폭 미입력", + "높이 미입력" + ], + "조건": { + "wing_in": "있음" + }, + "더함": [ + { + "종류": "box_culvert", + "펼침": [ + "body_width_m", + "body_height_m" + ], + "값": "개소", + "조건": { + "wing_out": "있음" + } + } + ] + }, + "경로": [ + "BOX암거", + "(폭×높이마다)", + "날개벽" + ] + }, + { + "id": "fb", + "머리": [ + "세월교", + "(관경 × 련수마다)", + null, + null + ], + "단위": "개소", + "꼴": "수", + "펼침": true, + "설명": "설계값 — 세월교 개소", + "바인딩": { + "종류": "ford_bridge", + "펼침": [ + "pipe_diameter_mm", + "pipe_count" + ], + "값": "개소", + "묶음": "fb", + "머리틀": [ + "세월교", + "{0}×{1}", + null + ], + "값꼴": [ + "Φ{}", + "{}련" + ], + "빈값": [ + "관경 미입력", + "련수 미입력" + ] + }, + "경로": [ + "세월교", + "(관경 × 련수마다)" + ] + }, + { + "id": "fp_w", + "머리": [ + "물넘이포장", + "T=(두께마다)", + "폭", + null + ], + "단위": "m", + "꼴": "수", + "펼침": true, + "설명": "설계값 — 월류 폭", + "바인딩": { + "종류": "ford_pavement", + "펼침": [ + "thickness_cm" + ], + "값": "ford_width_m", + "묶음": "fp", + "머리틀": [ + "물넘이포장", + "{0}", + "폭" + ], + "값꼴": [ + "T={}cm" + ], + "빈값": [ + "두께 미입력" + ] + }, + "경로": [ + "물넘이포장", + "T=(두께마다)", + "폭" + ] + }, + { + "id": "fp_l", + "머리": [ + "물넘이포장", + "T=(두께마다)", + "길이", + null + ], + "단위": "m", + "꼴": "수", + "펼침": true, + "설명": "설계값 — 포장 길이(노폭 방향)", + "바인딩": { + "종류": "ford_pavement", + "펼침": [ + "thickness_cm" + ], + "값": "length_m", + "묶음": "fp", + "머리틀": [ + "물넘이포장", + "{0}", + "길이" + ], + "값꼴": [ + "T={}cm" + ], + "빈값": [ + "두께 미입력" + ] + }, + "경로": [ + "물넘이포장", + "T=(두께마다)", + "길이" + ] + }, + { + "id": "od_len", + "머리": [ + "횡단개거", + "(규격마다)", + "길이", + null + ], + "단위": "m", + "꼴": "수", + "펼침": true, + "설명": "설계값 — 개거 연장", + "일위대가": "UA000026", + "바인딩": { + "종류": "open_ditch", + "펼침": [ + "ditch_spec" + ], + "값": "length_m", + "묶음": "od", + "머리틀": [ + "횡단개거", + "{0}", + "길이" + ], + "빈값": [ + "규격 미입력" + ] + }, + "경로": [ + "횡단개거", + "(규격마다)", + "길이" + ] + }, + { + "id": "od_cnt", + "머리": [ + "횡단개거", + "(규격마다)", + "개소", + null + ], + "단위": "개소", + "꼴": "수", + "펼침": true, + "설명": "설계값 — 개거 · 노출형 횡단수로 개소", + "일위대가": "UA000003", + "바인딩": { + "종류": "open_ditch", + "펼침": [ + "ditch_spec" + ], + "값": "개소", + "묶음": "od", + "머리틀": [ + "횡단개거", + "{0}", + "개소" + ], + "빈값": [ + "규격 미입력" + ], + "더함": [ + { + "종류": "cross_drain_exposed", + "펼침": [ + "=노출형" + ], + "값": "개소" + } + ] + }, + "경로": [ + "횡단개거", + "(규격마다)", + "개소" + ] + }, + { + "id": "rw", + "머리": [ + "옹벽", + "(형식마다)", + "H=(높이마다)", + null + ], + "단위": "m", + "꼴": "수", + "펼침": true, + "설명": "설계값 — 옹벽 길이", + "바인딩": { + "종류": "retaining_wall", + "펼침": [ + "form", + "height_m" + ], + "값": "길이", + "묶음": "rw", + "머리틀": [ + "옹벽", + "{0}", + "{1}" + ], + "값꼴": [ + "{}", + "H={}" + ], + "빈값": [ + "형식 미입력", + "높이 미입력" + ] + }, + "경로": [ + "옹벽", + "(형식마다)", + "H=(높이마다)" + ] + }, + { + "id": "ms", + "머리": [ + "돌쌓기", + "(찰·메마다)", + "H=(높이마다)", + null + ], + "단위": "m", + "꼴": "수", + "펼침": true, + "설명": "설계값 — 돌쌓기 길이", + "바인딩": { + "종류": "masonry_wet", + "펼침": [ + "=찰쌓기", + "height_m" + ], + "값": "길이", + "묶음": "ms", + "머리틀": [ + "돌쌓기", + "{0}", + "{1}" + ], + "값꼴": [ + "{}", + "H={}" + ], + "빈값": [ + "", + "높이 미입력" + ], + "더함": [ + { + "종류": "masonry_dry", + "펼침": [ + "=메쌓기", + "height_m" + ], + "값": "길이" + } + ] + }, + "경로": [ + "돌쌓기", + "(찰·메마다)", + "H=(높이마다)" + ] + }, + { + "id": "sg", + "머리": [ + "흙막이", + "(형태마다)", + "H=(높이마다)", + null + ], + "단위": "m", + "꼴": "수", + "펼침": true, + "설명": "설계값 — 흙막이 길이 · 일위대가는 큰돌만", + "일위대가": "UA000032", + "바인딩": { + "종류": "soil_guard", + "펼침": [ + "form", + "height_m" + ], + "값": "길이", + "묶음": "sg", + "머리틀": [ + "흙막이", + "{0}", + "{1}" + ], + "값꼴": [ + "{}", + "H={}" + ], + "빈값": [ + "형식 미입력", + "높이 미입력" + ] + }, + "경로": [ + "흙막이", + "(형태마다)", + "H=(높이마다)" + ] + }, + { + "id": "bm_len", + "머리": [ + "큰돌쌓기", + "(찰·메 · 돌 크기 · 높이마다)", + "길이", + null + ], + "단위": "m", + "꼴": "수", + "펼침": true, + "설명": "설계값 — 큰돌쌓기 길이", + "바인딩": { + "종류": "boulder_masonry", + "펼침": [ + "bond", + "stone_cm", + "height_m" + ], + "값": "길이", + "묶음": "bm", + "머리틀": [ + "큰돌쌓기", + "{0} {1} {2}", + "길이" + ], + "값꼴": [ + "{}", + "{}", + "H={}" + ], + "빈값": [ + "쌓기 방식 미입력", + "돌규격 미입력", + "높이 미입력" + ] + }, + "경로": [ + "큰돌쌓기", + "(찰·메 · 돌 크기 · 높이마다)", + "길이" + ] + }, + { + "id": "bm_area", + "머리": [ + "큰돌쌓기", + "(찰·메 · 돌 크기 · 높이마다)", + "면적", + null + ], + "단위": "㎡", + "꼴": "수", + "펼침": true, + "설명": "계산 — L × H (H = 펼친 높이)", + "일위대가": "UA000031", + "펼침틀": { + "묶음": "bm", + "머리틀": [ + "큰돌쌓기", + "{0} {1} {2}", + "면적" + ], + "값꼴": [ + "{}", + "{}", + "H={}" + ], + "빈값": [ + "쌓기 방식 미입력", + "돌규격 미입력", + "높이 미입력" + ], + "식틀": "IF([bm_len]>0,[bm_len]*{2},\"\")" + }, + "경로": [ + "큰돌쌓기", + "(찰·메 · 돌 크기 · 높이마다)", + "면적" + ] + }, + { + "id": "ec", + "머리": [ + "골막이", + "(형태마다)", + null, + null + ], + "단위": "개소", + "꼴": "수", + "펼침": true, + "설명": "설계값 — 골막이 개소", + "바인딩": { + "종류": "erosion_check", + "펼침": [ + "form" + ], + "값": "개소", + "묶음": "ec", + "머리틀": [ + "골막이", + "{0}", + null + ], + "빈값": [ + "형식 미입력" + ] + }, + "경로": [ + "골막이", + "(형태마다)" + ] + }, + { + "id": "be", + "머리": [ + "소단", + null, + null, + null + ], + "단위": "m", + "꼴": "수", + "설명": "설계값 — 소단 길이", + "바인딩": { + "종류": "berm", + "값": "길이" + }, + "경로": [ + "소단" + ] + }, + { + "id": "rf", + "머리": [ + "대피소", + null, + null, + null + ], + "단위": "개소", + "꼴": "수", + "설명": "설계값 — 대피소", + "바인딩": { + "종류": "refuge", + "값": "개소" + }, + "경로": [ + "대피소" + ] + }, + { + "id": "c2", + "머리": [ + "대피소", + "새 분류1", + null, + null + ], + "단위": "개소", + "꼴": "수", + "경로": [ + "대피소", + "새 분류1" + ] + }, + { + "id": "wy", + "머리": [ + "정차·작업장", + null, + null, + null + ], + "단위": "개소", + "꼴": "수", + "설명": "설계값 — 정차·작업장", + "바인딩": { + "종류": "work_yard", + "값": "개소" + }, + "경로": [ + "정차·작업장" + ] + }, + { + "id": "ta", + "머리": [ + "차돌림곳", + null, + null, + null + ], + "단위": "개소", + "꼴": "수", + "설명": "설계값 — 차돌림곳", + "바인딩": { + "종류": "turnaround", + "값": "개소" + }, + "경로": [ + "차돌림곳" + ] + }, + { + "id": "gr_rail", + "머리": [ + "가드레일", + null, + null, + null + ], + "단위": "m", + "꼴": "수", + "설명": "설계값 — 가드레일 길이", + "바인딩": { + "종류": "guardrail", + "값": "길이", + "조건": { + "kind": "가드레일" + } + }, + "경로": [ + "가드레일" + ] + }, + { + "id": "gr_curb", + "머리": [ + "경계석", + null, + null, + null + ], + "단위": "m", + "꼴": "수", + "설명": "설계값 — 경계석 길이", + "바인딩": { + "종류": "guardrail", + "값": "길이", + "조건": { + "kind": "경계석" + } + }, + "경로": [ + "경계석" + ] + }, + { + "id": "gr_sign", + "머리": [ + "위험표지", + null, + null, + null + ], + "단위": "개소", + "꼴": "수", + "설명": "설계값 — 위험표지", + "일위대가": "UA000017", + "바인딩": { + "종류": "guardrail", + "값": "개소", + "조건": { + "kind": "위험표지" + } + }, + "경로": [ + "위험표지" + ] + }, + { + "id": "mr", + "머리": [ + "반사경", + null, + null, + null + ], + "단위": "개소", + "꼴": "수", + "설명": "설계값 — 반사경", + "일위대가": "UA000018", + "바인딩": { + "종류": "mirror", + "값": "개소" + }, + "경로": [ + "반사경" + ] + }, + { + "id": "bg", + "머리": [ + "차단기", + null, + null, + null + ], + "단위": "개소", + "꼴": "수", + "설명": "설계값 — 차단기", + "일위대가": "UA000010", + "바인딩": { + "종류": "barrier_gate", + "값": "개소" + }, + "경로": [ + "차단기" + ] + }, + { + "id": "ps", + "머리": [ + "국가지점번호판", + null, + null, + null + ], + "단위": "개소", + "꼴": "수", + "설명": "설계값 — 번호판 · 전구간 줄은 손 입력", + "일위대가": "UA000016", + "바인딩": { + "종류": "position_sign", + "값": "개소" + }, + "경로": [ + "국가지점번호판" + ] + }, + { + "id": "cs", + "머리": [ + "준공표지석", + null, + null, + null + ], + "단위": "개소", + "꼴": "수", + "설명": "설계값 — 준공표지판", + "일위대가": "UA000002", + "바인딩": { + "종류": "completion_sign", + "값": "개소" + }, + "경로": [ + "준공표지석" + ] + }, + { + "id": "etc", + "머리": [ + "기타", + "(이름마다)", + null, + null + ], + "단위": "개소", + "꼴": "수", + "펼침": true, + "설명": "설계값 — 기타 구조물 개소", + "바인딩": { + "종류": "etc", + "펼침": [ + "name" + ], + "값": "개소", + "묶음": "etc", + "머리틀": [ + "기타", + "{0}", + null + ], + "빈값": [ + "이름 미입력" + ] + }, + "경로": [ + "기타", + "(이름마다)" + ] + }, + { + "id": "h_intake", + "머리": [ + "취수정", + null, + null, + null + ], + "단위": "개소", + "꼴": "수", + "손": true, + "설명": "손 입력", + "일위대가": "UA000005", + "경로": [ + "취수정" + ] + }, + { + "id": "h_rip_c", + "머리": [ + "돌붙임", + "찰붙임", + null, + null + ], + "단위": "㎡", + "꼴": "수", + "손": true, + "설명": "손 입력", + "일위대가": "UA000009", + "경로": [ + "돌붙임", + "찰붙임" + ] + }, + { + "id": "h_rip_m", + "머리": [ + "돌붙임", + "메붙임", + null, + null + ], + "단위": "㎡", + "꼴": "수", + "손": true, + "설명": "손 입력", + "일위대가": "UA000009", + "경로": [ + "돌붙임", + "메붙임" + ] + }, + { + "id": "h_stone_ch", + "머리": [ + "돌수로", + null, + null, + null + ], + "단위": "m", + "꼴": "수", + "손": true, + "설명": "손 입력", + "일위대가": "UA000011", + "경로": [ + "돌수로" + ] + }, + { + "id": "h_stone_bed", + "머리": [ + "돌조공", + null, + null, + null + ], + "단위": "m", + "꼴": "수", + "손": true, + "설명": "손 입력", + "일위대가": "UA000023", + "경로": [ + "돌조공" + ] + }, + { + "id": "h_pv_rail", + "머리": [ + "포장 난간", + null, + null, + null + ], + "단위": "m", + "꼴": "수", + "손": true, + "설명": "손 입력", + "일위대가": "UA000012", + "경로": [ + "포장 난간" + ] + }, + { + "id": "h_pg_rail", + "머리": [ + "관보호공 안전난간", + null, + null, + null + ], + "단위": "개소", + "꼴": "수", + "손": true, + "설명": "손 입력", + "일위대가": "UA000013", + "경로": [ + "관보호공 안전난간" + ] + }, + { + "id": "h_soil_ditch", + "머리": [ + "수로형 토사개거", + null, + null, + null + ], + "단위": "개소", + "꼴": "수", + "손": true, + "설명": "손 입력", + "일위대가": "UA000033", + "경로": [ + "수로형 토사개거" + ] + }, + { + "id": "h_ford_basin", + "머리": [ + "물넘이 집수부", + null, + null, + null + ], + "단위": "개소", + "꼴": "수", + "손": true, + "설명": "손 입력", + "일위대가": "UA000034", + "경로": [ + "물넘이 집수부" + ] + }, + { + "id": "h_rockfall", + "머리": [ + "낙석방지책", + null, + null, + null + ], + "단위": "경간", + "꼴": "수", + "손": true, + "설명": "손 입력", + "일위대가": null, + "경로": [ + "낙석방지책" + ] + }, + { + "id": "h_break", + "머리": [ + "콘크리트·포장 깨기", + null, + null, + null + ], + "단위": "㎥", + "꼴": "수", + "손": true, + "설명": "손 입력", + "일위대가": null, + "경로": [ + "콘크리트·포장 깨기" + ] + }, + { + "id": "h_waste", + "머리": [ + "폐기물처리", + null, + null, + null + ], + "단위": "ton", + "꼴": "수", + "손": true, + "설명": "손 입력", + "일위대가": null, + "경로": [ + "폐기물처리" + ] + }, + { + "id": "memo", + "머리": [ + "비고", + null, + null, + null + ], + "단위": null, + "꼴": "글", + "손": true, + "설명": "손 입력" + } + ], + "줄": [ + { + "id": "all", + "값": { + "sta": "전구간" + }, + "고정": "전구간" + } + ], + "합계줄": [ + { + "id": "sum", + "이름": "합계", + "식": "SUM" + } + ], + "보기": { + "틀고정": { + "열": 2 + }, + "열너비": { + "no": 40, + "sta": 150, + "memo": 120, + "h_soil_ditch": 32 + } + }, + "폴더": [] +} diff --git a/resources/tester/fixtures/m02_master_sample/구조물집계표조합.json b/resources/tester/fixtures/m02_master_sample/구조물집계표조합.json new file mode 100644 index 00000000..e66d5b18 --- /dev/null +++ b/resources/tester/fixtures/m02_master_sample/구조물집계표조합.json @@ -0,0 +1,1555 @@ +{ + "그룹": "구조물집계표조합", + "판": 1, + "비고": "구조물집계표 구조물 열(꼴=수)마다 한 줄 · 서버가 집계표 [저장] 때 맞춤(손으로 고치지 않음) · 키 SS 는 한 번 주면 안 바뀌고 지운 열의 키는 다시 안 씀 · 경로 = 폴더 이름 배열", + "다음": 52, + "줄": [ + { + "키": "SS000001", + "열": "rv", + "경로": [ + "돌기슭막이", + "메쌓기", + "1.2m", + "기초유" + ], + "이름": "돌기슭막이 메쌓기 1.2m 기초유", + "종류": "돌기슭막이", + "분류1": "메쌓기", + "분류2": "1.2m", + "분류3": "기초유", + "단위": "m", + "일위대가": "UA000004", + "자재조합": null, + "도번": "SD01010100", + "면적번": "SA01010100", + "재료번": "SM01010100", + "바인딩": { + "종류": "revetment", + "펼침": [ + "form", + "height_m" + ], + "값": "length_m", + "묶음": "rv", + "머리틀": [ + "돌기슭막이", + "{0}", + "{1}" + ], + "값꼴": [ + "{}", + "H={}" + ], + "빈값": [ + "형태 미입력", + "높이 미입력" + ], + "이름": { + "돌쌓기(찰)": "찰쌓기", + "돌쌓기(메)": "메쌓기" + }, + "더함": [ + { + "종류": "pipe", + "펼침": [ + "inlet_revet_form", + "inlet_revet_height_m" + ], + "값": "inlet_revet_length_m", + "조건": { + "inlet_type": "기슭막이" + } + }, + { + "종류": "pipe", + "펼침": [ + "outlet_revet_form", + "outlet_revet_height_m" + ], + "값": "outlet_revet_length_m", + "조건": { + "outlet_type": "기슭막이" + } + } + ] + }, + "펼침": true + }, + { + "키": "SS000002", + "열": "c4", + "경로": [ + "돌기슭막이", + "메쌓기", + "1.2m", + "기초무, 소단" + ], + "이름": "돌기슭막이 메쌓기 1.2m 기초무, 소단", + "종류": "돌기슭막이", + "분류1": "메쌓기", + "분류2": "1.2m", + "분류3": "기초무, 소단", + "단위": "m", + "일위대가": "UA000004", + "자재조합": null, + "도번": "SD01010101", + "면적번": "SA01010101", + "재료번": "SM01010101", + "바인딩": null, + "펼침": null + }, + { + "키": "SS000003", + "열": "c1", + "경로": [ + "돌기슭막이", + "메쌓기", + "1.5m", + "기초유" + ], + "이름": "돌기슭막이 메쌓기 1.5m 기초유", + "종류": "돌기슭막이", + "분류1": "메쌓기", + "분류2": "1.5m", + "분류3": "기초유", + "단위": "m", + "일위대가": "UA000004", + "자재조합": null, + "도번": "SD01010200", + "면적번": "SA01010200", + "재료번": "SM01010200", + "바인딩": null, + "펼침": null + }, + { + "키": "SS000004", + "열": "c5", + "경로": [ + "돌기슭막이", + "찰쌓기" + ], + "이름": "돌기슭막이 찰쌓기", + "종류": "돌기슭막이", + "분류1": "찰쌓기", + "분류2": null, + "분류3": null, + "단위": "m", + "일위대가": "UA000004", + "자재조합": null, + "도번": "SD01020000", + "면적번": "SA01020000", + "재료번": "SM01020000", + "바인딩": null, + "펼침": null + }, + { + "키": "SS000005", + "열": "pv_b", + "경로": [ + "콘크리트포장", + "T=(두께마다)", + "B" + ], + "이름": "콘크리트포장 T=(두께마다) B", + "종류": "콘크리트포장", + "분류1": "T=(두께마다)", + "분류2": "B", + "분류3": null, + "단위": "m", + "일위대가": null, + "자재조합": null, + "도번": "SD02010100", + "면적번": "SA02010100", + "재료번": "SM02010100", + "바인딩": { + "종류": "pavement_concrete", + "펼침": [ + "thickness_cm" + ], + "값": "width_m", + "묶음": "pv", + "머리틀": [ + "콘크리트포장", + "{0}", + "B" + ], + "값꼴": [ + "T={}cm" + ], + "빈값": [ + "두께 미입력" + ] + }, + "펼침": true + }, + { + "키": "SS000006", + "열": "pv_l", + "경로": [ + "콘크리트포장", + "T=(두께마다)", + "L" + ], + "이름": "콘크리트포장 T=(두께마다) L", + "종류": "콘크리트포장", + "분류1": "T=(두께마다)", + "분류2": "L", + "분류3": null, + "단위": "m", + "일위대가": "UA000007", + "자재조합": null, + "도번": "SD02010200", + "면적번": "SA02010200", + "재료번": "SM02010200", + "바인딩": { + "종류": "pavement_concrete", + "펼침": [ + "thickness_cm" + ], + "값": "길이", + "묶음": "pv", + "머리틀": [ + "콘크리트포장", + "{0}", + "L" + ], + "값꼴": [ + "T={}cm" + ], + "빈값": [ + "두께 미입력" + ] + }, + "펼침": true + }, + { + "키": "SS000007", + "열": "pv_w", + "경로": [ + "콘크리트포장", + "T=(두께마다)", + "확폭" + ], + "이름": "콘크리트포장 T=(두께마다) 확폭", + "종류": "콘크리트포장", + "분류1": "T=(두께마다)", + "분류2": "확폭", + "분류3": null, + "단위": "㎡", + "일위대가": null, + "자재조합": null, + "도번": "SD02010300", + "면적번": "SA02010300", + "재료번": "SM02010300", + "바인딩": { + "종류": "pavement_concrete", + "펼침": [ + "thickness_cm" + ], + "값": "widening_area_m2", + "묶음": "pv", + "머리틀": [ + "콘크리트포장", + "{0}", + "확폭" + ], + "값꼴": [ + "T={}cm" + ], + "빈값": [ + "두께 미입력" + ] + }, + "펼침": true + }, + { + "키": "SS000008", + "열": "pv_a", + "경로": [ + "콘크리트포장", + "T=(두께마다)", + "A" + ], + "이름": "콘크리트포장 T=(두께마다) A", + "종류": "콘크리트포장", + "분류1": "T=(두께마다)", + "분류2": "A", + "분류3": null, + "단위": "㎡", + "일위대가": "UA000006", + "자재조합": null, + "도번": "SD02010400", + "면적번": "SA02010400", + "재료번": "SM02010400", + "바인딩": null, + "펼침": true + }, + { + "키": "SS000009", + "열": "pv_jt", + "경로": [ + "콘크리트포장", + "T=(두께마다)", + "수축줄눈" + ], + "이름": "콘크리트포장 T=(두께마다) 수축줄눈", + "종류": "콘크리트포장", + "분류1": "T=(두께마다)", + "분류2": "수축줄눈", + "분류3": null, + "단위": "m", + "일위대가": null, + "자재조합": null, + "도번": "SD02010500", + "면적번": "SA02010500", + "재료번": "SM02010500", + "바인딩": null, + "펼침": true + }, + { + "키": "SS000010", + "열": "c3", + "경로": [ + "콘크리트포장", + "T=(두께마다)", + "새 분류2" + ], + "이름": "콘크리트포장 T=(두께마다) 새 분류2", + "종류": "콘크리트포장", + "분류1": "T=(두께마다)", + "분류2": "새 분류2", + "분류3": null, + "단위": "m", + "일위대가": null, + "자재조합": null, + "도번": "SD02010600", + "면적번": "SA02010600", + "재료번": "SM02010600", + "바인딩": null, + "펼침": null + }, + { + "키": "SS000011", + "열": "pp_len", + "경로": [ + "관공", + "(관종·관경마다)", + "관매설" + ], + "이름": "관공 (관종·관경마다) 관매설", + "종류": "관공", + "분류1": "(관종·관경마다)", + "분류2": "관매설", + "분류3": null, + "단위": "m", + "일위대가": "UA000036", + "자재조합": null, + "도번": "SD03010100", + "면적번": "SA03010100", + "재료번": "SM03010100", + "바인딩": { + "종류": "pipe", + "펼침": [ + "pipe_kind", + "pipe_diameter_mm" + ], + "값": "관연장", + "묶음": "pp", + "머리틀": [ + "관공", + "{0} {1}", + "관매설" + ], + "값꼴": [ + "{}", + "Φ{}" + ], + "빈값": [ + "관종 미입력", + "관경 미입력" + ] + }, + "펼침": true + }, + { + "키": "SS000012", + "열": "pp_cp", + "경로": [ + "관공", + "(관종·관경마다)", + "커플링밴드" + ], + "이름": "관공 (관종·관경마다) 커플링밴드", + "종류": "관공", + "분류1": "(관종·관경마다)", + "분류2": "커플링밴드", + "분류3": null, + "단위": "개소", + "일위대가": null, + "자재조합": null, + "도번": "SD03010200", + "면적번": "SA03010200", + "재료번": "SM03010200", + "바인딩": null, + "펼침": true + }, + { + "키": "SS000013", + "열": "pg_in", + "경로": [ + "관보호공", + "(형식마다)", + "유입" + ], + "이름": "관보호공 (형식마다) 유입", + "종류": "관보호공", + "분류1": "(형식마다)", + "분류2": "유입", + "분류3": null, + "단위": "개소", + "일위대가": "UA000008", + "자재조합": null, + "도번": "SD04010100", + "면적번": "SA04010100", + "재료번": "SM04010100", + "바인딩": { + "종류": "pipe", + "펼침": [ + "wing_wall_type" + ], + "값": "개소", + "묶음": "pg", + "머리틀": [ + "관보호공", + "{0}", + "유입" + ], + "빈값": [ + "형식 미입력" + ], + "이름": { + "A-TYPE": "날개벽 A형", + "C-TYPE": "날개벽 C형", + "A-TYPE+집수정": "날개벽 A형+집수정" + }, + "더함": [ + { + "종류": "pipe", + "펼침": [ + "inlet_basin_form" + ], + "값": "개소", + "조건": { + "inlet_type": "집수정" + } + } + ] + }, + "펼침": true + }, + { + "키": "SS000014", + "열": "bx_len", + "경로": [ + "BOX암거", + "(폭×높이마다)", + "연장" + ], + "이름": "BOX암거 (폭×높이마다) 연장", + "종류": "BOX암거", + "분류1": "(폭×높이마다)", + "분류2": "연장", + "분류3": null, + "단위": "m", + "일위대가": "UA000014", + "자재조합": null, + "도번": "SD05010100", + "면적번": "SA05010100", + "재료번": "SM05010100", + "바인딩": { + "종류": "box_culvert", + "펼침": [ + "body_width_m", + "body_height_m" + ], + "값": "length_m", + "묶음": "bx", + "머리틀": [ + "BOX암거", + "{0}×{1}", + "연장" + ], + "빈값": [ + "폭 미입력", + "높이 미입력" + ] + }, + "펼침": true + }, + { + "키": "SS000015", + "열": "bx_wing", + "경로": [ + "BOX암거", + "(폭×높이마다)", + "날개벽" + ], + "이름": "BOX암거 (폭×높이마다) 날개벽", + "종류": "BOX암거", + "분류1": "(폭×높이마다)", + "분류2": "날개벽", + "분류3": null, + "단위": "개소", + "일위대가": "UA000025", + "자재조합": null, + "도번": "SD05010200", + "면적번": "SA05010200", + "재료번": "SM05010200", + "바인딩": { + "종류": "box_culvert", + "펼침": [ + "body_width_m", + "body_height_m" + ], + "값": "개소", + "묶음": "bx", + "머리틀": [ + "BOX암거", + "{0}×{1}", + "날개벽" + ], + "빈값": [ + "폭 미입력", + "높이 미입력" + ], + "조건": { + "wing_in": "있음" + }, + "더함": [ + { + "종류": "box_culvert", + "펼침": [ + "body_width_m", + "body_height_m" + ], + "값": "개소", + "조건": { + "wing_out": "있음" + } + } + ] + }, + "펼침": true + }, + { + "키": "SS000016", + "열": "fb", + "경로": [ + "세월교", + "(관경 × 련수마다)" + ], + "이름": "세월교 (관경 × 련수마다)", + "종류": "세월교", + "분류1": "(관경 × 련수마다)", + "분류2": null, + "분류3": null, + "단위": "개소", + "일위대가": null, + "자재조합": null, + "도번": "SD06010000", + "면적번": "SA06010000", + "재료번": "SM06010000", + "바인딩": { + "종류": "ford_bridge", + "펼침": [ + "pipe_diameter_mm", + "pipe_count" + ], + "값": "개소", + "묶음": "fb", + "머리틀": [ + "세월교", + "{0}×{1}", + null + ], + "값꼴": [ + "Φ{}", + "{}련" + ], + "빈값": [ + "관경 미입력", + "련수 미입력" + ] + }, + "펼침": true + }, + { + "키": "SS000017", + "열": "fp_w", + "경로": [ + "물넘이포장", + "T=(두께마다)", + "폭" + ], + "이름": "물넘이포장 T=(두께마다) 폭", + "종류": "물넘이포장", + "분류1": "T=(두께마다)", + "분류2": "폭", + "분류3": null, + "단위": "m", + "일위대가": null, + "자재조합": null, + "도번": "SD07010100", + "면적번": "SA07010100", + "재료번": "SM07010100", + "바인딩": { + "종류": "ford_pavement", + "펼침": [ + "thickness_cm" + ], + "값": "ford_width_m", + "묶음": "fp", + "머리틀": [ + "물넘이포장", + "{0}", + "폭" + ], + "값꼴": [ + "T={}cm" + ], + "빈값": [ + "두께 미입력" + ] + }, + "펼침": true + }, + { + "키": "SS000018", + "열": "fp_l", + "경로": [ + "물넘이포장", + "T=(두께마다)", + "길이" + ], + "이름": "물넘이포장 T=(두께마다) 길이", + "종류": "물넘이포장", + "분류1": "T=(두께마다)", + "분류2": "길이", + "분류3": null, + "단위": "m", + "일위대가": null, + "자재조합": null, + "도번": "SD07010200", + "면적번": "SA07010200", + "재료번": "SM07010200", + "바인딩": { + "종류": "ford_pavement", + "펼침": [ + "thickness_cm" + ], + "값": "length_m", + "묶음": "fp", + "머리틀": [ + "물넘이포장", + "{0}", + "길이" + ], + "값꼴": [ + "T={}cm" + ], + "빈값": [ + "두께 미입력" + ] + }, + "펼침": true + }, + { + "키": "SS000019", + "열": "od_len", + "경로": [ + "횡단개거", + "(규격마다)", + "길이" + ], + "이름": "횡단개거 (규격마다) 길이", + "종류": "횡단개거", + "분류1": "(규격마다)", + "분류2": "길이", + "분류3": null, + "단위": "m", + "일위대가": "UA000026", + "자재조합": null, + "도번": "SD08010100", + "면적번": "SA08010100", + "재료번": "SM08010100", + "바인딩": { + "종류": "open_ditch", + "펼침": [ + "ditch_spec" + ], + "값": "length_m", + "묶음": "od", + "머리틀": [ + "횡단개거", + "{0}", + "길이" + ], + "빈값": [ + "규격 미입력" + ] + }, + "펼침": true + }, + { + "키": "SS000020", + "열": "od_cnt", + "경로": [ + "횡단개거", + "(규격마다)", + "개소" + ], + "이름": "횡단개거 (규격마다) 개소", + "종류": "횡단개거", + "분류1": "(규격마다)", + "분류2": "개소", + "분류3": null, + "단위": "개소", + "일위대가": "UA000003", + "자재조합": null, + "도번": "SD08010200", + "면적번": "SA08010200", + "재료번": "SM08010200", + "바인딩": { + "종류": "open_ditch", + "펼침": [ + "ditch_spec" + ], + "값": "개소", + "묶음": "od", + "머리틀": [ + "횡단개거", + "{0}", + "개소" + ], + "빈값": [ + "규격 미입력" + ], + "더함": [ + { + "종류": "cross_drain_exposed", + "펼침": [ + "=노출형" + ], + "값": "개소" + } + ] + }, + "펼침": true + }, + { + "키": "SS000021", + "열": "rw", + "경로": [ + "옹벽", + "(형식마다)", + "H=(높이마다)" + ], + "이름": "옹벽 (형식마다) H=(높이마다)", + "종류": "옹벽", + "분류1": "(형식마다)", + "분류2": "H=(높이마다)", + "분류3": null, + "단위": "m", + "일위대가": null, + "자재조합": null, + "도번": "SD09010100", + "면적번": "SA09010100", + "재료번": "SM09010100", + "바인딩": { + "종류": "retaining_wall", + "펼침": [ + "form", + "height_m" + ], + "값": "길이", + "묶음": "rw", + "머리틀": [ + "옹벽", + "{0}", + "{1}" + ], + "값꼴": [ + "{}", + "H={}" + ], + "빈값": [ + "형식 미입력", + "높이 미입력" + ] + }, + "펼침": true + }, + { + "키": "SS000022", + "열": "ms", + "경로": [ + "돌쌓기", + "(찰·메마다)", + "H=(높이마다)" + ], + "이름": "돌쌓기 (찰·메마다) H=(높이마다)", + "종류": "돌쌓기", + "분류1": "(찰·메마다)", + "분류2": "H=(높이마다)", + "분류3": null, + "단위": "m", + "일위대가": null, + "자재조합": null, + "도번": "SD10010100", + "면적번": "SA10010100", + "재료번": "SM10010100", + "바인딩": { + "종류": "masonry_wet", + "펼침": [ + "=찰쌓기", + "height_m" + ], + "값": "길이", + "묶음": "ms", + "머리틀": [ + "돌쌓기", + "{0}", + "{1}" + ], + "값꼴": [ + "{}", + "H={}" + ], + "빈값": [ + "", + "높이 미입력" + ], + "더함": [ + { + "종류": "masonry_dry", + "펼침": [ + "=메쌓기", + "height_m" + ], + "값": "길이" + } + ] + }, + "펼침": true + }, + { + "키": "SS000023", + "열": "sg", + "경로": [ + "흙막이", + "(형태마다)", + "H=(높이마다)" + ], + "이름": "흙막이 (형태마다) H=(높이마다)", + "종류": "흙막이", + "분류1": "(형태마다)", + "분류2": "H=(높이마다)", + "분류3": null, + "단위": "m", + "일위대가": "UA000032", + "자재조합": null, + "도번": "SD11010100", + "면적번": "SA11010100", + "재료번": "SM11010100", + "바인딩": { + "종류": "soil_guard", + "펼침": [ + "form", + "height_m" + ], + "값": "길이", + "묶음": "sg", + "머리틀": [ + "흙막이", + "{0}", + "{1}" + ], + "값꼴": [ + "{}", + "H={}" + ], + "빈값": [ + "형식 미입력", + "높이 미입력" + ] + }, + "펼침": true + }, + { + "키": "SS000024", + "열": "bm_len", + "경로": [ + "큰돌쌓기", + "(찰·메 · 돌 크기 · 높이마다)", + "길이" + ], + "이름": "큰돌쌓기 (찰·메 · 돌 크기 · 높이마다) 길이", + "종류": "큰돌쌓기", + "분류1": "(찰·메 · 돌 크기 · 높이마다)", + "분류2": "길이", + "분류3": null, + "단위": "m", + "일위대가": null, + "자재조합": null, + "도번": "SD12010100", + "면적번": "SA12010100", + "재료번": "SM12010100", + "바인딩": { + "종류": "boulder_masonry", + "펼침": [ + "bond", + "stone_cm", + "height_m" + ], + "값": "길이", + "묶음": "bm", + "머리틀": [ + "큰돌쌓기", + "{0} {1} {2}", + "길이" + ], + "값꼴": [ + "{}", + "{}", + "H={}" + ], + "빈값": [ + "쌓기 방식 미입력", + "돌규격 미입력", + "높이 미입력" + ] + }, + "펼침": true + }, + { + "키": "SS000025", + "열": "bm_area", + "경로": [ + "큰돌쌓기", + "(찰·메 · 돌 크기 · 높이마다)", + "면적" + ], + "이름": "큰돌쌓기 (찰·메 · 돌 크기 · 높이마다) 면적", + "종류": "큰돌쌓기", + "분류1": "(찰·메 · 돌 크기 · 높이마다)", + "분류2": "면적", + "분류3": null, + "단위": "㎡", + "일위대가": "UA000031", + "자재조합": null, + "도번": "SD12010200", + "면적번": "SA12010200", + "재료번": "SM12010200", + "바인딩": null, + "펼침": true + }, + { + "키": "SS000026", + "열": "ec", + "경로": [ + "골막이", + "(형태마다)" + ], + "이름": "골막이 (형태마다)", + "종류": "골막이", + "분류1": "(형태마다)", + "분류2": null, + "분류3": null, + "단위": "개소", + "일위대가": null, + "자재조합": null, + "도번": "SD13010000", + "면적번": "SA13010000", + "재료번": "SM13010000", + "바인딩": { + "종류": "erosion_check", + "펼침": [ + "form" + ], + "값": "개소", + "묶음": "ec", + "머리틀": [ + "골막이", + "{0}", + null + ], + "빈값": [ + "형식 미입력" + ] + }, + "펼침": true + }, + { + "키": "SS000027", + "열": "be", + "경로": [ + "소단" + ], + "이름": "소단", + "종류": "소단", + "분류1": null, + "분류2": null, + "분류3": null, + "단위": "m", + "일위대가": null, + "자재조합": null, + "도번": "SD14000000", + "면적번": "SA14000000", + "재료번": "SM14000000", + "바인딩": { + "종류": "berm", + "값": "길이" + }, + "펼침": null + }, + { + "키": "SS000028", + "열": "rf", + "경로": [ + "대피소" + ], + "이름": "대피소", + "종류": "대피소", + "분류1": null, + "분류2": null, + "분류3": null, + "단위": "개소", + "일위대가": null, + "자재조합": null, + "도번": "SD15000000", + "면적번": "SA15000000", + "재료번": "SM15000000", + "바인딩": { + "종류": "refuge", + "값": "개소" + }, + "펼침": null + }, + { + "키": "SS000029", + "열": "c2", + "경로": [ + "대피소", + "새 분류1" + ], + "이름": "대피소 새 분류1", + "종류": "대피소", + "분류1": "새 분류1", + "분류2": null, + "분류3": null, + "단위": "개소", + "일위대가": null, + "자재조합": null, + "도번": "SD15010000", + "면적번": "SA15010000", + "재료번": "SM15010000", + "바인딩": null, + "펼침": null + }, + { + "키": "SS000030", + "열": "wy", + "경로": [ + "정차·작업장" + ], + "이름": "정차·작업장", + "종류": "정차·작업장", + "분류1": null, + "분류2": null, + "분류3": null, + "단위": "개소", + "일위대가": null, + "자재조합": null, + "도번": "SD16000000", + "면적번": "SA16000000", + "재료번": "SM16000000", + "바인딩": { + "종류": "work_yard", + "값": "개소" + }, + "펼침": null + }, + { + "키": "SS000031", + "열": "ta", + "경로": [ + "차돌림곳" + ], + "이름": "차돌림곳", + "종류": "차돌림곳", + "분류1": null, + "분류2": null, + "분류3": null, + "단위": "개소", + "일위대가": null, + "자재조합": null, + "도번": "SD17000000", + "면적번": "SA17000000", + "재료번": "SM17000000", + "바인딩": { + "종류": "turnaround", + "값": "개소" + }, + "펼침": null + }, + { + "키": "SS000032", + "열": "gr_rail", + "경로": [ + "가드레일" + ], + "이름": "가드레일", + "종류": "가드레일", + "분류1": null, + "분류2": null, + "분류3": null, + "단위": "m", + "일위대가": null, + "자재조합": null, + "도번": "SD18000000", + "면적번": "SA18000000", + "재료번": "SM18000000", + "바인딩": { + "종류": "guardrail", + "값": "길이", + "조건": { + "kind": "가드레일" + } + }, + "펼침": null + }, + { + "키": "SS000033", + "열": "gr_curb", + "경로": [ + "경계석" + ], + "이름": "경계석", + "종류": "경계석", + "분류1": null, + "분류2": null, + "분류3": null, + "단위": "m", + "일위대가": null, + "자재조합": null, + "도번": "SD19000000", + "면적번": "SA19000000", + "재료번": "SM19000000", + "바인딩": { + "종류": "guardrail", + "값": "길이", + "조건": { + "kind": "경계석" + } + }, + "펼침": null + }, + { + "키": "SS000034", + "열": "gr_sign", + "경로": [ + "위험표지" + ], + "이름": "위험표지", + "종류": "위험표지", + "분류1": null, + "분류2": null, + "분류3": null, + "단위": "개소", + "일위대가": "UA000017", + "자재조합": null, + "도번": "SD20000000", + "면적번": "SA20000000", + "재료번": "SM20000000", + "바인딩": { + "종류": "guardrail", + "값": "개소", + "조건": { + "kind": "위험표지" + } + }, + "펼침": null + }, + { + "키": "SS000035", + "열": "mr", + "경로": [ + "반사경" + ], + "이름": "반사경", + "종류": "반사경", + "분류1": null, + "분류2": null, + "분류3": null, + "단위": "개소", + "일위대가": "UA000018", + "자재조합": null, + "도번": "SD21000000", + "면적번": "SA21000000", + "재료번": "SM21000000", + "바인딩": { + "종류": "mirror", + "값": "개소" + }, + "펼침": null + }, + { + "키": "SS000036", + "열": "bg", + "경로": [ + "차단기" + ], + "이름": "차단기", + "종류": "차단기", + "분류1": null, + "분류2": null, + "분류3": null, + "단위": "개소", + "일위대가": "UA000010", + "자재조합": null, + "도번": "SD22000000", + "면적번": "SA22000000", + "재료번": "SM22000000", + "바인딩": { + "종류": "barrier_gate", + "값": "개소" + }, + "펼침": null + }, + { + "키": "SS000037", + "열": "ps", + "경로": [ + "국가지점번호판" + ], + "이름": "국가지점번호판", + "종류": "국가지점번호판", + "분류1": null, + "분류2": null, + "분류3": null, + "단위": "개소", + "일위대가": "UA000016", + "자재조합": null, + "도번": "SD23000000", + "면적번": "SA23000000", + "재료번": "SM23000000", + "바인딩": { + "종류": "position_sign", + "값": "개소" + }, + "펼침": null + }, + { + "키": "SS000038", + "열": "cs", + "경로": [ + "준공표지석" + ], + "이름": "준공표지석", + "종류": "준공표지석", + "분류1": null, + "분류2": null, + "분류3": null, + "단위": "개소", + "일위대가": "UA000002", + "자재조합": null, + "도번": "SD24000000", + "면적번": "SA24000000", + "재료번": "SM24000000", + "바인딩": { + "종류": "completion_sign", + "값": "개소" + }, + "펼침": null + }, + { + "키": "SS000039", + "열": "etc", + "경로": [ + "기타", + "(이름마다)" + ], + "이름": "기타 (이름마다)", + "종류": "기타", + "분류1": "(이름마다)", + "분류2": null, + "분류3": null, + "단위": "개소", + "일위대가": null, + "자재조합": null, + "도번": "SD25010000", + "면적번": "SA25010000", + "재료번": "SM25010000", + "바인딩": { + "종류": "etc", + "펼침": [ + "name" + ], + "값": "개소", + "묶음": "etc", + "머리틀": [ + "기타", + "{0}", + null + ], + "빈값": [ + "이름 미입력" + ] + }, + "펼침": true + }, + { + "키": "SS000040", + "열": "h_intake", + "경로": [ + "취수정" + ], + "이름": "취수정", + "종류": "취수정", + "분류1": null, + "분류2": null, + "분류3": null, + "단위": "개소", + "일위대가": "UA000005", + "자재조합": null, + "도번": "SD26000000", + "면적번": "SA26000000", + "재료번": "SM26000000", + "바인딩": null, + "펼침": null + }, + { + "키": "SS000041", + "열": "h_rip_c", + "경로": [ + "돌붙임", + "찰붙임" + ], + "이름": "돌붙임 찰붙임", + "종류": "돌붙임", + "분류1": "찰붙임", + "분류2": null, + "분류3": null, + "단위": "㎡", + "일위대가": "UA000009", + "자재조합": null, + "도번": "SD27010000", + "면적번": "SA27010000", + "재료번": "SM27010000", + "바인딩": null, + "펼침": null + }, + { + "키": "SS000042", + "열": "h_rip_m", + "경로": [ + "돌붙임", + "메붙임" + ], + "이름": "돌붙임 메붙임", + "종류": "돌붙임", + "분류1": "메붙임", + "분류2": null, + "분류3": null, + "단위": "㎡", + "일위대가": "UA000009", + "자재조합": null, + "도번": "SD27020000", + "면적번": "SA27020000", + "재료번": "SM27020000", + "바인딩": null, + "펼침": null + }, + { + "키": "SS000043", + "열": "h_stone_ch", + "경로": [ + "돌수로" + ], + "이름": "돌수로", + "종류": "돌수로", + "분류1": null, + "분류2": null, + "분류3": null, + "단위": "m", + "일위대가": "UA000011", + "자재조합": null, + "도번": "SD28000000", + "면적번": "SA28000000", + "재료번": "SM28000000", + "바인딩": null, + "펼침": null + }, + { + "키": "SS000044", + "열": "h_stone_bed", + "경로": [ + "돌조공" + ], + "이름": "돌조공", + "종류": "돌조공", + "분류1": null, + "분류2": null, + "분류3": null, + "단위": "m", + "일위대가": "UA000023", + "자재조합": null, + "도번": "SD29000000", + "면적번": "SA29000000", + "재료번": "SM29000000", + "바인딩": null, + "펼침": null + }, + { + "키": "SS000045", + "열": "h_pv_rail", + "경로": [ + "포장 난간" + ], + "이름": "포장 난간", + "종류": "포장 난간", + "분류1": null, + "분류2": null, + "분류3": null, + "단위": "m", + "일위대가": "UA000012", + "자재조합": null, + "도번": "SD30000000", + "면적번": "SA30000000", + "재료번": "SM30000000", + "바인딩": null, + "펼침": null + }, + { + "키": "SS000046", + "열": "h_pg_rail", + "경로": [ + "관보호공 안전난간" + ], + "이름": "관보호공 안전난간", + "종류": "관보호공 안전난간", + "분류1": null, + "분류2": null, + "분류3": null, + "단위": "개소", + "일위대가": "UA000013", + "자재조합": null, + "도번": "SD31000000", + "면적번": "SA31000000", + "재료번": "SM31000000", + "바인딩": null, + "펼침": null + }, + { + "키": "SS000047", + "열": "h_soil_ditch", + "경로": [ + "수로형 토사개거" + ], + "이름": "수로형 토사개거", + "종류": "수로형 토사개거", + "분류1": null, + "분류2": null, + "분류3": null, + "단위": "개소", + "일위대가": "UA000033", + "자재조합": null, + "도번": "SD32000000", + "면적번": "SA32000000", + "재료번": "SM32000000", + "바인딩": null, + "펼침": null + }, + { + "키": "SS000048", + "열": "h_ford_basin", + "경로": [ + "물넘이 집수부" + ], + "이름": "물넘이 집수부", + "종류": "물넘이 집수부", + "분류1": null, + "분류2": null, + "분류3": null, + "단위": "개소", + "일위대가": "UA000034", + "자재조합": null, + "도번": "SD33000000", + "면적번": "SA33000000", + "재료번": "SM33000000", + "바인딩": null, + "펼침": null + }, + { + "키": "SS000049", + "열": "h_rockfall", + "경로": [ + "낙석방지책" + ], + "이름": "낙석방지책", + "종류": "낙석방지책", + "분류1": null, + "분류2": null, + "분류3": null, + "단위": "경간", + "일위대가": null, + "자재조합": null, + "도번": "SD34000000", + "면적번": "SA34000000", + "재료번": "SM34000000", + "바인딩": null, + "펼침": null + }, + { + "키": "SS000050", + "열": "h_break", + "경로": [ + "콘크리트·포장 깨기" + ], + "이름": "콘크리트·포장 깨기", + "종류": "콘크리트·포장 깨기", + "분류1": null, + "분류2": null, + "분류3": null, + "단위": "㎥", + "일위대가": null, + "자재조합": null, + "도번": "SD35000000", + "면적번": "SA35000000", + "재료번": "SM35000000", + "바인딩": null, + "펼침": null + }, + { + "키": "SS000051", + "열": "h_waste", + "경로": [ + "폐기물처리" + ], + "이름": "폐기물처리", + "종류": "폐기물처리", + "분류1": null, + "분류2": null, + "분류3": null, + "단위": "ton", + "일위대가": null, + "자재조합": null, + "도번": "SD36000000", + "면적번": "SA36000000", + "재료번": "SM36000000", + "바인딩": null, + "펼침": null + } + ] +} diff --git a/resources/tester/fixtures/m02_master_sample/자재조합.json b/resources/tester/fixtures/m02_master_sample/자재조합.json new file mode 100644 index 00000000..aa84200a --- /dev/null +++ b/resources/tester/fixtures/m02_master_sample/자재조합.json @@ -0,0 +1,7 @@ +{ + "그룹": "자재조합", + "판": "2026", + "비고": ["자재 조합 = 구조물 열 재료산출이 쓰는 자재 목록 묶음 · 수량·계산은 두지 않음(재료산출 몫) · 키 UM 은 「다음」 번호로 매김 · 지운 키는 다시 안 씀"], + "다음": 1, + "줄": [] +} diff --git a/resources/tester/helper_m02_master_sample.py b/resources/tester/helper_m02_master_sample.py new file mode 100644 index 00000000..a1218519 --- /dev/null +++ b/resources/tester/helper_m02_master_sample.py @@ -0,0 +1,27 @@ +"""시험 안 마스터 견본(PLAN 26-5a) — 초기화 전 마스터 층을 줄여 얼린 것 +(`fixtures/m02_master_sample`). + +집계표(바인딩 열 54) · SS 장부(51 줄) · 자재조합 · 세 파일은 rv · c4 · pp_len · ms 넷(도면 줄임) — +시험은 실제 `resources/master_template` 을 안 읽고 이 견본을 임시 폴더에 복사해 씀. +""" + +from __future__ import annotations + +import json +import shutil +from pathlib import Path +from typing import Any + +SAMPLE = Path(__file__).resolve().parent / "fixtures" / "m02_master_sample" +TABLE = SAMPLE / "table" / "구조물집계표.json" + + +def copy_master_sample(target: Path) -> Path: + """견본을 `target` 에 복사(없으면 만듦) · `target` 을 돌려줌.""" + shutil.copytree(SAMPLE, target, dirs_exist_ok=True) + return target + + +def sample_table() -> dict[str, Any]: + """견본 구조물집계표 문서(새 dict).""" + return json.loads(TABLE.read_text(encoding="utf-8")) diff --git a/resources/tester/spreadsheet/test_spreadsheet_saved_values.py b/resources/tester/spreadsheet/test_spreadsheet_saved_values.py index 0b0cc9f8..0d165bb7 100644 --- a/resources/tester/spreadsheet/test_spreadsheet_saved_values.py +++ b/resources/tester/spreadsheet/test_spreadsheet_saved_values.py @@ -1,19 +1,17 @@ """저장 뒤 식 · 값 유지 — M02 면적산출 · 재료산출 저장본을 서버 엔진으로 다시 풀면 저장된 `계산값` 과 같음. -읽기만 함(저장본을 고치지 않음). 저장 때 서버가 적은 값과 지금 엔진 풀이가 어긋나면 화면을 다시 열 때 +시험 안 견본 저장본(`fixtures/m02_master_sample` · PLAN 26-5a)을 읽기만 함. +저장 때 서버가 적은 값과 지금 엔진 풀이가 어긋나면 화면을 다시 열 때 값이 달라 보임. """ 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] -FOLDER = ROOT / "resources" / "master_template" +from resources.tester.helper_m02_master_sample import SAMPLE as FOLDER pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") diff --git a/resources/tester/test_item_copy_complete.py b/resources/tester/test_item_copy_complete.py index 597fc81b..9d1265f5 100644 --- a/resources/tester/test_item_copy_complete.py +++ b/resources/tester/test_item_copy_complete.py @@ -30,8 +30,9 @@ from M02_MasterTemplete import M02_Item_Snapshot as snap from M02_MasterTemplete import M02_MasterTemplete_Router_Layers as layer_routes from M02_MasterTemplete import M02_Project_Items_Router as items_router from M02_MasterTemplete import M02_Template_Layers as layers +from resources.tester.helper_m02_master_sample import SAMPLE -REAL = config_system.PROJECT_ROOT / "resources/master_template" +REAL = SAMPLE # 시험 안 견본(PLAN 26-5a) — 실제 마스터 안 읽음 MASTER = config_system.PROJECT_ROOT / "resources/master_data" PIPE = {"layer": "system", "owner": None, "key": "SS000011", "name": "관공 관매설"} FOLDER = "pipe@40.0" # 관 지점 복사본 폴더 = 구조물 id(26-2b) diff --git a/resources/tester/test_m02_empty_master.py b/resources/tester/test_m02_empty_master.py new file mode 100644 index 00000000..3b28042c --- /dev/null +++ b/resources/tester/test_m02_empty_master.py @@ -0,0 +1,126 @@ +"""빈 마스터(PLAN 26-5a) — 마스터 구조물 항목을 다 지운 뒤에도 M02 가 돎 · 프로젝트 복사본은 그대로. + +마스터 초기화와 같은 줄기(나무 지우기 `tree.apply` → 집계표 저장 `store.write`)를 시험 안 견본 +사본(`fixtures/m02_master_sample`)에 돌림 — 실제 마스터는 안 읽음. +① 지운 뒤 — 항목 0 · 남는 열 no · sta · 비고 · 빈 나무 · 장부 `다음` 유지 · 지운 번호 기록 · + M02 첫 화면 길 · 새 폴더 · 새 항목(다음 키) · 지우기 · 고르기 모달의 마스터 0. +② 프로젝트 복사본 — 마스터가 비어도 계산 그대로 · 목록은 원본없음. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from common_util.common_util_auth import verify_session +from config import config_system +from M02_MasterTemplete import M02_Item_Copy as copies +from M02_MasterTemplete import M02_MasterTemplete_Router as main_router +from M02_MasterTemplete import M02_MasterTemplete_Router_Items as items_router +from M02_MasterTemplete import M02_MasterTemplete_Router_Layers as layers_router +from M02_MasterTemplete import M02_MasterTemplete_Store as store +from M02_MasterTemplete import M02_MasterTemplete_Tree as tree +from M02_MasterTemplete import M02_Template_Layers as layers +from resources.tester.helper_m02_master_sample import copy_master_sample +from resources.tester.test_item_copy_complete import ( # noqa: F401 — 복사본 world 고정물 + FOLDER, + _fill_values, + _pipe, + _save, + master_copy, + world, +) + + +def empty_master(folder: Path) -> list[str]: + """마스터 초기화 — 맨 위 폴더마다 나무 지우기 · 지운 열 너비 뺌 · 집계표 저장 한 번(세 파일 · + 장부가 따라옴).""" + with store.at(folder): + got = store.read("table", store.STRUCTURE_TABLE) + doc = tree.normalize_table(got["문서"]) + tops = {tree.path_of(c)[0] for c in doc["열"] if tree.is_item(c)} + gone: list[str] = [] + for top in sorted(tops | {f[0] for f in tree.folders_of(doc)}): + doc, note = tree.apply(doc, {"op": "delete", "경로": [top]}) + gone += note["지운항목"] + widths = doc.get("보기", {}).get("열너비") or {} + keep = {c["id"] for c in doc["열"]} + if widths: + doc["보기"]["열너비"] = {k: v for k, v in widths.items() if k in keep} + store.write("table", store.STRUCTURE_TABLE, got["판"], doc) + return gone + + +@pytest.fixture() +def empty(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]: + system = copy_master_sample(tmp_path / "master") + monkeypatch.setattr(store, "FOLDER", system) + monkeypatch.setattr(layers, "SYSTEM_ROOT", system) + monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(tmp_path / "storage")) + gone = empty_master(system) + session = {"user_id": 42, "company_id": 7, "role": "SYSTEM_ADMIN", "is_master": True} + app = FastAPI() + for module in (main_router, items_router, layers_router): + app.include_router(module.router) + app.dependency_overrides[verify_session] = lambda: session + return {"client": TestClient(app), "system": system, "gone": gone} + + +def test_지운_뒤_항목_0_틀은_남음(empty): + system, client = empty["system"], empty["client"] + assert len(empty["gone"]) == 51 + table = client.get("/api/m02/templates/table/구조물집계표").json()["문서"] + assert [c["id"] for c in table["열"]] == ["no", "sta", "memo"] and table["폴더"] == [] + assert table["변수"] and table["쪽줄"] == 50 and table["합계줄"] and table["바인딩규칙"] + assert set(table["보기"]["열너비"]) <= {"no", "sta", "memo"} + combos = client.get("/api/m02/structure-combos").json() + assert combos["줄"] == [] and combos["다음"] == 52 + assert len(combos["지운번호"]) == 4 # 견본에 세 파일이 있던 넷(rv · c4 · pp_len · ms) + for kind in ("structure", "area", "material", "inputs"): + assert not list((system / kind).glob("*.json")), kind + assert (system / "자재조합.json").is_file() + + +def test_빈_마스터에서_첫_화면_나무_새_항목_지우기(empty): + client = empty["client"] + # M02 첫 화면 길 — 양식 목록 · 집계표 · 번호 · 장부 · 나무 · 옵션 + listed = client.get("/api/m02/layers/system/templates").json()["양식"] + assert "구조물집계표" in [r["이름"] for r in listed] + assert "구조물집계표" in [r["이름"] for r in client.get("/api/m02/templates").json()] + assert client.get("/api/m02/structures/numbers").json() == {} + assert client.get("/api/m02/structure-options").status_code == 200 + assert client.get("/api/m02/items").json() == [] + groups = client.get("/api/m02/items?all=1").json() # 고르기 모달 — 마스터 0 + assert groups[0]["layer"] == "system" and groups[0]["items"] == [] + url = "/api/m02/layers/system/tree" + now = client.get(url).json() + assert now["폴더"] == [] and now["items"] == [] + + now = client.post(url, json={"판": now["판"], "op": "add-folder", "경로": [], "이름": "옹벽"}) + assert now.status_code == 200, now.text + now = now.json() + assert now["폴더"] == [["옹벽"]] + body = {"판": now["판"], "op": "add-item", "경로": ["옹벽"], "이름": "L형", "단위": "m"} + now = client.post(url, json=body).json() + [item] = now["items"] + assert item["key"] == "SS000052" and item["경로"] == ["옹벽", "L형"] # 지운 키 다시 안 씀 + assert item["도번"] == "SD-000001" # 옛 8자리 지운 번호와 안 겹치는 새 모양 + now = client.post(url, json={"판": now["판"], "op": "delete", "키": item["key"]}).json() + assert now["items"] == [] and now["폴더"] == [["옹벽"]] + now = client.post(url, json={"판": now["판"], "op": "delete", "경로": ["옹벽"]}).json() + assert now["폴더"] == [] and client.get("/api/m02/structure-combos").json()["다음"] == 53 + + +def test_마스터를_비워도_프로젝트_복사본_계산_그대로(world): # noqa: F811 — 고정물 + root, system = world["root"], world["system"] + _save(root, [_pipe()]) + _fill_values(root) + before = copies.calc(root, FOLDER) + assert len(empty_master(system)) == 51 + assert copies.calc(root, FOLDER) == before + row = copies.list_items(root)[0] + assert row["원본없음"] is True and row["마스터바뀜"] is False diff --git a/resources/tester/test_m02_item_inputs.py b/resources/tester/test_m02_item_inputs.py index a72a8557..9e71112f 100644 --- a/resources/tester/test_m02_item_inputs.py +++ b/resources/tester/test_m02_item_inputs.py @@ -1,8 +1,8 @@ """M02 항목 선택 입력(PLAN 25-1b · 25-2a) — 입력 목록 · 항목 연결 파일 · 알약 · 권한. -① 입력 목록은 실제 마스터(시스템 층 · 읽기만)로 — 일위대가 있는 항목 셋에서 로직 수 · 입력 수 · - 자동값이 M01 조합 미리 보기가 쓰는 것(로직 줄 `입력` · `master_auto`)과 같은지 · 로직마다 갈라 - 합치지 않는지 · 일위대가 없는 항목은 빈 목록. +① 입력 목록은 시험 안 마스터 견본(`fixtures/m02_master_sample` 사본 · PLAN 26-5a)으로 — + 일위대가 있는 항목 셋에서 로직 수 · 입력 수 · 자동값이 M01 조합 미리 보기가 쓰는 것(로직 줄 + `입력` · `master_auto`)과 같은지 · 로직마다 갈라 합치지 않는지 · 일위대가 없는 항목은 빈 목록. ② 연결 파일 쓰기는 임시 storage(회사 · 개인 층) + 견본 시스템 집계표로만 — 판 409 · 없는 키 422 · 사라짐 표시 · 알약 · 층 권한 · 열 지우기 · 항목 복사 · 분류 더하기 복사가 파일을 같이 다룸. """ @@ -26,6 +26,7 @@ from M02_MasterTemplete import M02_MasterTemplete_Router_Items as items_router from M02_MasterTemplete import M02_MasterTemplete_Router_Layers as layers_router from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_Template_Layers as layers +from resources.tester.helper_m02_master_sample import copy_master_sample from resources.tester.test_m02_structure import write_sample_table TABLE = "table/구조물집계표" @@ -56,10 +57,11 @@ def _m01_view(ua: str) -> list[tuple[str, list[str], dict]]: return out -# ── ① 입력 목록 — 실제 시스템 층(읽기만) ───────────────────── +# ── ① 입력 목록 — 마스터 견본 사본 ───────────────────── -def test_입력_목록은_M01_조합_미리_보기와_같음(): +def test_입력_목록은_M01_조합_미리_보기와_같음(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(store, "FOLDER", copy_master_sample(tmp_path / "master")) client = _app({"user_id": 1, "company_id": 1, "role": "USER", "is_master": False}) rows = [r for r in json.loads((store.FOLDER / store.COMBO_FILE).read_bytes())["줄"]] with_ua = [r for r in rows if r.get("일위대가")] diff --git a/resources/tester/test_m02_table_fill.py b/resources/tester/test_m02_table_fill.py index ed7b11cb..df9aab5a 100644 --- a/resources/tester/test_m02_table_fill.py +++ b/resources/tester/test_m02_table_fill.py @@ -8,14 +8,12 @@ from typing import Any import pytest -from config import config_system from M02_MasterTemplete import M02_Table_Fill as fill - -MASTER = config_system.PROJECT_ROOT / "resources/master_template/table/구조물집계표.json" +from resources.tester.helper_m02_master_sample import sample_table def _master() -> dict[str, Any]: - return json.loads(MASTER.read_text(encoding="utf-8")) + return sample_table() # 시험 안 견본(PLAN 26-5a) — 실제 마스터 안 읽음 @pytest.fixture diff --git a/resources/tester/test_m02_template_layers.py b/resources/tester/test_m02_template_layers.py index 76b2b013..72d1cc1e 100644 --- a/resources/tester/test_m02_template_layers.py +++ b/resources/tester/test_m02_template_layers.py @@ -15,6 +15,7 @@ from common_util.common_util_auth import verify_session from config import config_system from M02_MasterTemplete import M02_MasterTemplete_Router_Layers as router_module from M02_MasterTemplete import M02_Template_Layers as layers +from resources.tester.helper_m02_master_sample import sample_table P1 = "11111111-1111-1111-1111-111111111111" P2 = "22222222-2222-2222-2222-222222222222" @@ -197,10 +198,7 @@ def test_채운_표는_작업본을_채워_돌려주고_저장_안_함( monkeypatch.setattr(router_module, "_cross_designs", designs) monkeypatch.setattr(router_module.fill, "recalc", lambda document: {"계산": {}}) root = _root(world, P1) - master = config_system.PROJECT_ROOT / "resources/master_template/table/구조물집계표.json" - layers.write_template( - root / "templates", "table", "구조물집계표", json.loads(master.read_text(encoding="utf-8")) - ) + layers.write_template(root / "templates", "table", "구조물집계표", sample_table()) edits = root / "B04_PreProcess/drainage/edits" edits.mkdir(parents=True, exist_ok=True) (edits / "pipe_points.json").write_text( @@ -276,11 +274,7 @@ def test_프로젝트_표_저장은_서버가_설계값을_걸러_씀(world: dic from M02_MasterTemplete import M02_Table_Fill as fill client = world["client"] - master = json.loads( - ( - config_system.PROJECT_ROOT / "resources/master_template/table/구조물집계표.json" - ).read_text(encoding="utf-8") - ) + master = sample_table() filled = fill.fill_document( master, [ diff --git a/resources/tester/test_m02_tree.py b/resources/tester/test_m02_tree.py index de7289ce..16f5e39d 100644 --- a/resources/tester/test_m02_tree.py +++ b/resources/tester/test_m02_tree.py @@ -1,7 +1,7 @@ """M02 집계 항목 폴더 구조(PLAN 26-1 · 26-2a) — 경로 · 옛 자료 읽기 · 일련번호 · 나무 고침 · 권한. -① 옛 마스터(실제 `resources/master_template` · 읽기만) 51 항목이 경로로 읽힘 — 개수 · 이름(옛 종류 · - 분류1~3 이음과 같음) · 키 차례. +① 옛 마스터(시험 안 견본 `fixtures/m02_master_sample` 사본 · PLAN 26-5a) 51 항목이 경로로 + 읽힘 — 개수 · 이름(옛 종류 · 분류1~3 이음과 같음) · 키 차례. ② 나무 고침은 임시 storage(회사 · 개인 층)와 견본 시스템 집계표로만 — 빈 층 첫 폴더 · 항목 · 깊이 1 · 2 · 5 · 이름 바꾸기 · 옮기기 · 지우기(세 파일 · 입력 연결 같이) · 마지막 항목 · 판 409 · 422 · 404 · 권한 403 · 표 저장과 한 줄기(머리를 고치면 경로가 따라옴) · 번호 일련 · 안 되돌림. @@ -25,10 +25,11 @@ from M02_MasterTemplete import M02_MasterTemplete_Router_Layers as layers_router from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_MasterTemplete_Tree as tree from M02_MasterTemplete import M02_Template_Layers as layers +from resources.tester.helper_m02_master_sample import SAMPLE, copy_master_sample from resources.tester.test_m02_structure import write_sample_table TABLE = "table/구조물집계표" -MASTER = Path(__file__).resolve().parents[2] / "resources" / "master_template" +MASTER = SAMPLE def _app(session: dict[str, Any]) -> TestClient: @@ -39,10 +40,11 @@ def _app(session: dict[str, Any]) -> TestClient: return TestClient(app) -# ── ① 옛 마스터 — 읽기만 ───────────────────────────── +# ── ① 옛 마스터 — 견본 사본 ───────────────────────────── -def test_옛_마스터_51_항목이_경로로_읽힘(): +def test_옛_마스터_51_항목이_경로로_읽힘(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(store, "FOLDER", copy_master_sample(tmp_path / "master")) raw = json.loads((MASTER / store.COMBO_FILE).read_bytes())["줄"] old = [ " ".join(str(r[k]) for k in ("종류", "분류1", "분류2", "분류3") if r.get(k)) for r in raw diff --git a/resources/tester/test_structures_summary_snapshot.py b/resources/tester/test_structures_summary_snapshot.py index 260aa92a..261206bc 100644 --- a/resources/tester/test_structures_summary_snapshot.py +++ b/resources/tester/test_structures_summary_snapshot.py @@ -24,8 +24,9 @@ from config import config_system from M02_MasterTemplete import M02_Item_Snapshot as snap from M02_MasterTemplete import M02_Table_Fill as fill from M02_MasterTemplete import M02_Template_Layers as layers +from resources.tester.helper_m02_master_sample import SAMPLE -REAL = config_system.PROJECT_ROOT / "resources/master_template" +REAL = SAMPLE # 시험 안 견본(PLAN 26-5a) — 실제 마스터 안 읽음 MS = {"layer": "system", "owner": None, "key": "SS000022", "name": "돌쌓기"} RV = {"layer": "system", "owner": None, "key": "SS000001", "name": "돌기슭막이 메쌓기 1.2m"} PIPE = {"layer": "system", "owner": None, "key": "SS000011", "name": "관공 관매설"} From 92e27840381176e45d6b887a724ab3617d841b55 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 30 Sep 2026 10:38:16 +0900 Subject: [PATCH 16/18] =?UTF-8?q?docs:=20M01=20=ED=92=88=EC=85=88=20?= =?UTF-8?q?=EB=8C=80=EC=A1=B0=20=EC=A4=91=EA=B0=84=20=EA=B2=B0=EA=B3=BC?= =?UTF-8?q?=EC=99=80=20=EC=9D=B8=EA=B3=84=20=EB=B0=A9=EB=B2=95=20=EB=B3=B4?= =?UTF-8?q?=EC=A1=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 지시에 따라 대조를 중단하고 기존 보고, 줄글 목록, 진행 상태와 대조 방법을 보존한다. --- docs/raw/M01_점검/대조방식.md | 20 ++ docs/raw/M01_점검/보고.md | 529 +++++++++++++++++++++++++++++++++ docs/raw/M01_점검/보고_줄글.md | 437 +++++++++++++++++++++++++++ docs/raw/M01_점검/진행.md | 20 ++ 4 files changed, 1006 insertions(+) create mode 100644 docs/raw/M01_점검/대조방식.md create mode 100644 docs/raw/M01_점검/보고.md create mode 100644 docs/raw/M01_점검/보고_줄글.md create mode 100644 docs/raw/M01_점검/진행.md diff --git a/docs/raw/M01_점검/대조방식.md b/docs/raw/M01_점검/대조방식.md new file mode 100644 index 00000000..5f711f5e --- /dev/null +++ b/docs/raw/M01_점검/대조방식.md @@ -0,0 +1,20 @@ +# M01 원문·자료 표 대조 방식 + +2026-09-30 인계용. 새 판정 없이 앞서 수행한 대조 절차와 확인 사례만 정리한다. `진행.md`의 `끝남` 11표 외에는 숫자나 행 수가 같아도 검증 완료로 보지 않는다. + +1. **원문 찾기:** 로직·소요량·계수 JSON의 `출처`와 절 번호를 기준으로 `resources/knowledge/original/원가계산/` 본문(md)에서 해당 절과 표의 앞뒤 주석까지 찾는다. 제목만 같아도 연도·절·표 번호가 다르면 별개로 취급한다. 원문 인용과 파일·절을 함께 기록한다. +2. **병합 칸 풀기:** 원문의 행 병합·열 병합은 생략된 값을 임의로 빈 칸으로 세지 않는다. 위 행에서 이어지는 규격·조건·단위, 두 줄에 걸친 이름, 소계·주석·계산식 여부를 식별해 비교용 가상 행으로 펼친다. 반복 표 머리말은 데이터 행에서 제외한다. +3. **피벗:** 원문의 한 물리 행에 좌·우 구분이나 여러 값 칸이 있으면 각각 독립 비교 행으로 편다. 역으로 JSON 여러 행이 원문 한 행의 서로 다른 열을 나타내면 같은 원문 행에 짝을 둔다. 따라서 물리 줄 수 차이만으로 누락 판정하지 않는다. +4. **로직 줄 짝짓기:** 원문 절·표·규격·조건·비목·장비/재료명을 복합 키로 삼아 `소요량_*`, `계수_*` 행과 `로직_*.json`의 찾기(QC/QF) 및 호표(LB/EQ/MT/GC/GF)를 연결한다. 순서가 같은 줄이라는 이유만으로 짝짓지 않는다. 중복 규격은 주변 조건·단위와 수치까지 대조한다. +5. **단위와 값:** 원문·자료의 각 수치를 같은 행의 칸 이름과 단위로 비교한다. `㎥/m³`, `㎡/m²` 등 표기만 다른 단위는 정규화하되 `㎾h`, `L`, `kg`, `인`, `시간`, `대`는 서로 바꾸지 않는다. 옆 칸·옆 행의 값이 들어간 경우는 값 자체가 원문 어딘가에 있어도 오류다. +6. **배수:** 원문 기준량(`100㎡ 당`, `10m 당`, `1,000㎡ 당` 등)과 로직 결과단위·입력단위·수량 식을 함께 본다. 원문 원단위를 유지하는 결과라면 배수 나눔이 불필요할 수 있고, 결과를 `㎡`·`m` 등 단위당으로 바꾸면 해당 수량을 100·10·1,000으로 나눠야 한다. 중복 나눔과 누락 모두 검토한다. +7. **비목:** 원문 표의 노무·재료·경비 구분과 호표 요소가 실제 가격표에서 들어가는 비목을 맞춘다. 기계 이름이라고 무조건 경비, 소모품이라고 무조건 재료로 가정하지 않고 원문의 산출 구분과 주석을 우선한다. +8. **판정:** 원문 동일 절·행·칸과 JSON/로직의 대응을 직접 확인한 차이만 `확실`; 병합·피벗·타 절 재사용·판본 차이·조건 해석이 남으면 `의심`으로 둔다. 원문에 수치 표가 있지만 독립 로직이 안 보이는 절은 `누락 확정 / 참고·계수라 로직 불필요 / 다른 로직이 맡음 / 사람 확인`으로 갈라 적는다. 근거 한 줄, 키, 이름, 원문 파일·절, 갈래를 `보고.md`에 넣고 한 흠당 한 줄을 `보고_줄글.md`에 반영한다. + +자주 틀리는 자리는 병합된 마지막 행, 좌·우 피벗을 물리 행 수로 오판하는 자리, 표 머리말·소계의 혼입, 같은 숫자의 옆 열 이동, 단위 칸의 옆 줄 복사, 다른 절의 동명 표, `100㎡ 당` 배수의 이중 적용, 그리고 검색어가 동명 이종 자재를 먼저 찾는 자리다. 재료 검색어는 자재품목표에서 **정확한 품목이 존재하는지**와 **현재 검색어가 무엇을 실제로 찾는지**를 따로 기록한다. + +## 이미 확인한 세 예 + +1. **산림 10-6-1, QF000308:** 원문 한 행의 좌·우 값을 각각 펼쳐 비교했다. 원문 4개 물리 행이 자료 8개 값 행에 대응하므로 `4 ≠ 8`만으로 빠진 줄이라고 판정하지 않았다. 조건·값 짝짓기는 앞선 검수에서 완료로 기록했다. +2. **산림 10-4, QF000307:** 병합·피벗을 풀어도 원문 장비 16행에 자료 15행만 있었다. 끝의 자주식 `크레인(트럭 10TON)`이 빠진 것을 원문 이름과 해당 절로 직접 확인해 `확실` 누락으로 `보고.md`와 `보고_줄글.md`에 기록했다. +3. **GC000943 Tar Epoxy:** 원문의 재료량·동력량 칸을 별개로 보아야 하는데 자료에서 `550`과 `㎾h`가 Tar Epoxy 재료 줄에 들어간 유형이다. 값 존재만으로 합격시키지 않고 원문 열 위치, 재료 단위, 전력 비목 및 호표 참조를 각각 대조해야 한다. 앞선 보고의 확정/의심 표시를 그대로 따른다. diff --git a/docs/raw/M01_점검/보고.md b/docs/raw/M01_점검/보고.md new file mode 100644 index 00000000..5dd7690a --- /dev/null +++ b/docs/raw/M01_점검/보고.md @@ -0,0 +1,529 @@ +# M01 단가산출 로직 읽기 전용 점검 + +- 점검일: 2026-09-30. 대상: `resources/master_data/로직_*.json` 54파일, 1,354줄 및 직접 참조 마스터·원문 본문. +- `check_master.py 전부`: 틀 0, 본문 0, 로직 0, 조합 0, 단위 경고 15. 검사의 `본문`은 **표형 요소의 숫자**를 원문 절과 비교하며, 로직의 의미나 표의 행 조합을 전수 검증하지 않는다. +- 아래 `확실`은 원문 표와 로직 값을 직접 맞댄 것. `의심`은 도구 경고이며 실제 오류 또는 가격 마스터 후보 미비 여부를 더 확인해야 한다. 같은 키의 서로 다른 문제는 한 행에 묶었다. + +## 확인된 차이 + +| 로직 키 | 이름 | 원문 자리(파일 · 절) | 갈래 | 무엇이 다름 | 판정 | 근거 인용 한 줄 | +|---|---|---|---|---|---|---| +| GC000181 | 매트부설(육상) | 건설공사_표준품셈/본문/01_공통부문/제05장_기초공사/5-02_연약지반처리.md · 5-2-1 | 연결 | P.P로프(9㎜) 재료 검색어가 `와이어로프(철심) 9㎜`로, 재질과 품명이 다르다. | 확실 | 원문: `P.P로프(9㎜) (m)`; 로직: `검색어: 와이어로프(철심) 9㎜`. | +| GC000027 | 강관 조립말비계(이동식) 설치 및 해체 | 건설공사_표준품셈/본문/01_공통부문/제02장_가설공사/2-07_비계.md · 2-7-6 | 연결 | `수평띠장 L1829`가 안전발판을 검색한다. | 확실 | 원문: `수평띠장 · L1829 · 개 · 4`; 로직 검색어: `안전발판(PSP대용) 250×3000`. | +| GC000348 | 터널 방수 | 건설공사_표준품셈/본문/02_토목부문/제03장_터널공사/3-04_부대공.md · 3-4-1 | 연결 | 일체식 방수시트가 방수석고보드를 검색한다. | 확실 | 원문: `일체식 방수시트 · ㎡ · 1.15`; 로직 검색어: `방수석고보드 12.5t`. | +| GC000360 | 레일 가스압접 | 건설공사_표준품셈/본문/02_토목부문/제04장_궤도공사/4-05_궤도용접.md · 4-5-1 | 연결 | 바퀴숫돌 단면용의 가격 검색어가 도로청소용 빗자루다. 같은 방식으로 다른 바퀴숫돌 규격도 연결되어 있다. | 확실 | 원문: `바퀴숫돌 · 단면용 A36m B11호… · 개`; 로직 검색어: `빗자루 도로청소용`. | +| GC000560 | 철골 공장용접(기본용접공수) | 건설공사_표준품셈/본문/03_건축부문/제01장_철골공사/1-01_철골_가공_조립(공장생산).md · 1-1-3 | 연결 | CO₂ 와이어·탄산가스·잠호용접와이어의 가격 검색어가 모두 아세틸렌이다. | 확실 | 원문: `CO_2 와이어 · ㎏`, `탄산가스 · ㎏`, `잠호용접와이어 · ㎏`; 로직: `아세틸렌 98%`. | +| GC000575 | 벽돌 쌓기(시멘트벽돌) | 건설공사_표준품셈/본문/03_건축부문/제02장_조적공사/2-01_벽돌.md · 2-1-1 | 연결 | 시멘트벽돌 가격 검색어가 주차라인블록이다. | 확실 | 원문: `본 품은 시멘트 벽돌(19×9×5.7㎝) 쌓기 기준`; 로직 검색어: `주차라인블록 1000X300X150`. | +| GC000604 | 카페트 설치 | 건설공사_표준품셈/본문/03_건축부문/제05장_수장공사/5-01_바닥.md · 5-1-2 | 연결 | 카페트 가격 검색어가 불연천장재(마이톤)다. | 확실 | 원문: `카페트 · ㎡ · 1.1`; 로직 검색어: `불연천장재(마이톤) 12mm`. | +| GC000723 | 동관 용접접합 | 건설공사_표준품셈/본문/04_기계설비부문/제01장_배관공사/1-02_동관.md · 1-2-1 | 연결 | 동 및 동합금용 용접봉과 플럭스가 둘 다 아세틸렌을 검색한다. | 확실 | 원문 표 머리: `용접봉(g) · 플럭스(g) · 산소(ℓ) · 아세틸렌(g)`; 앞의 두 재료 검색어: `아세틸렌 98%`. | +| GC000938 | Stop-Log 설치 | 건설공사_표준품셈/본문/04_기계설비부문/제13장_플랜트설비공사/13-06_수력발전_기계설비.md · 13-6-6 | 연결·단위 | `광명단` 재료가 `목재전용도료 오일스테인`을 검색한다. 아세틸렌 원문은 `병` 수량 1.98인데 호표는 `kg` 1.98로 환산 없이 사용한다. | 확실 | 원문: `아세틸렌 … 병 1.98`, `광명단 … ℓ 2.2`. | +| GC000943 | Roller Gate Guide Metal 제작 | 같은 파일 · 13-6-11 | 연결·단위 | `Tar Epoxy` 호표에 원문의 전기 `550㎾h`를 그대로 넣고 `Tar Epoxy` 원문 `0.75ℓ`를 누락했다. 아세틸렌도 원문 `1.6병`을 `1.6kg`으로 사용한다. | 확실 | 원문: `Tar Epoxy … 0.75ℓ`, `전기 … 550㎾h`, `아세틸렌 … 1.6병`. | +| GC000948 | Tainter Gate Anchorage 제관 | 같은 파일 · 13-6-17 | 연결·단위 | `Tar Epoxy` 호표에 전력 `420㎾h`를 넣고 원문의 `3.0ℓ`를 누락했다. 아세틸렌 원문 `1.5병`도 `1.5kg`으로 사용한다. | 확실 | 원문: `Tar Epoxy … 3.0ℓ`, `전력 … 420㎾h`, `아세틸렌 … 1.5병`. | +| GC000993 | 플랜트 공사별 설치 소모자재(참고) | 건설공사_표준품셈/본문/04_기계설비부문/제13장_플랜트설비공사/13-11_기타_기계설비.md · 13-11-5 | 연결 | 용접봉(전기·산소) 모두 아세틸렌을, 사포는 복사용지를, 걸레는 그리스를, 용접면·용접홀다·용접앞치마는 노면파쇄기 팁을, 페인트붓은 도로청소용 빗자루를 검색한다. 원문 품명과 재료 가격 연결이 다르다. | 확실 | 원문 표의 품명 줄: `용접봉(전기)`, `용접봉(산소)`, `사포`, `걸레`, `용접면`, `용접홀다`, `용접앞치마`, `페인트붓(표기용)`. | + +## 검사 도구의 단위 경고 (15곳, 모두 의심) + +| 로직 키 | 이름 | 원문 자리(파일 · 절) | 갈래 | 무엇이 다름 | 판정 | 근거 인용 한 줄 | +|---|---|---|---|---|---|---| +| GC000027 | 강관 조립말비계(이동식) 설치 및 해체 | 건설공사_표준품셈/본문/01_공통부문/제02장_가설공사/2-07_비계.md · 2-7-6 | 단위 | 가새 L1518-2개 `조` 후보 0 | 의심 | 도구: `단위 「조」 후보 0`. | +| GC000181 | 매트부설(육상) | 5-02_연약지반처리.md · 5-2-1 | 단위 | 철근(19㎜) `m` 후보 0 | 의심 | 도구: `단위 「m」 후보 0`. | +| GC000616 | 걸레받이 설치 | 건설공사_표준품셈/본문/03_건축부문/제05장_수장공사/5-03_벽.md · 5-3-5 | 단위 | 중밀도섬유판 걸레받이 `m` 후보 0 | 의심 | 도구: `단위 「m」 후보 0`. | +| GC000211 | 합판거푸집 설치 및 해체 | 건설공사_표준품셈/본문/01_공통부문/제06장_철근콘크리트공사/6-03_거푸집.md · 6-3-1 | 단위 | 폼타이 12.7·9.5·7.9㎜의 `본` 후보 각각 0 (3곳) | 의심 | 도구: `폼타이 … 단위 「본」 후보 0`. | +| GC000855 | 가스용 강관 나사식 접합 및 배관 | 건설공사_표준품셈/본문/04_기계설비부문/제11장_가스설비공사/11-01_강관.md · 11-1-3 | 단위 | 스레트실테이프 `㎝` 후보 0 | 의심 | 도구: `스레트실테이프 … 단위 「㎝」 후보 0`. | +| GC000859 | 직독식 가스미터 설치 | 건설공사_표준품셈/본문/04_기계설비부문/제11장_가스설비공사/11-03_부속기기.md · 11-3-3 | 단위 | 스레트실테이프 `㎝` 후보 0 | 의심 | 도구: `스레트실테이프 … 단위 「㎝」 후보 0`. | +| GC000938 | Stop-Log 설치 | 13-06_수력발전_기계설비.md · 13-6-6 | 단위 | 함석 `대` 후보와 단위 성격 다름. 원문 역시 `대`; 가격 마스터 문제 가능. | 의심 | 원문: `함석 … 대 0.53`; 도구: `후보 단위가 줄 단위 「대」 과 성격이 다름`. | +| GC000943, GC000948 | 수력발전 게이트 제작 | 같은 파일 · 13-6-11, 13-6-17 | 단위 | 중유 `D/M` 후보 단위가 다름 (2곳) | 의심 | 원문: `중유 … 0.0018D/M`, `중유 … 0.019D/M`. | +| GC000948 | Tainter Gate Anchorage 제관 | 같은 파일 · 13-6-17 | 단위 | Zinc primer `ℓ` 후보 단위가 다름 | 의심 | 원문: `Zinc primer … 0.36ℓ`. | +| GC000993 | 설치 소모자재(참고) | 13-11_기타_기계설비.md · 13-11-5 | 단위 | 용접장갑 `족` 후보 0 | 의심 | 도구: `용접장갑 … 단위 「족」 후보 0`. | +| GF000160 | 합판거푸집 | 산림_표준품셈/본문/제12장_철근콘크리트/12-04_합판거푸집.md · 12-4 | 단위 | 폼타이 `본` 후보 0 | 의심 | 도구: `폼타이(D형 1/2인치) … 단위 「본」 후보 0`. | +| GF000165 | 콘크리트 포장 거푸집 | 산림_표준품셈/본문/제12장_철근콘크리트/12-08_콘크리트_포장_거푸집.md · 12-8 | 단위 | 철재 거푸집 손료 `본` 후보 0 | 의심 | 도구: `철재 거푸집 손료(1본 3m) … 단위 「본」 후보 0`. | + +## 범위와 부문별 요약 + +`점검 수`는 도구가 구조·참조·표 숫자를 검사한 로직 줄 수다. 사람이 원문 표의 모든 규격·조건·비목을 확인한 수가 아니다. `흠 수`는 위 표의 **확실한 문제가 있는 로직 키 수 / 단위 경고 발생 곳 수**이며 같은 키가 양쪽에 있을 수 있다. + +| 부문 | 로직 수 | 점검 수(기계) | 흠 수(확실 / 의심 경고) | +|---|---:|---:|---:| +| 건설품셈 공통 | 276 | 276 | 2 / 5 | +| 건설품셈 토목 | 282 | 282 | 2 / 0 | +| 건설품셈 건축 | 163 | 163 | 3 / 1 | +| 건설품셈 기계설비 | 272 | 272 | 5 / 7 | +| 건설품셈 유지관리 | 110 | 110 | 0 / 0 | +| 산림품셈 | 251 | 251 | 0 / 2 | +| 합계 | 1,354 | 1,354 | 12 / 15 | + +원문 직접 대조한 절: 건설 공통 2-7-6·5-2-1, 토목 3-4-1·4-5-1, 건축 1-1-3·2-1-1·5-1-2, 기계설비 1-2-1·13-6-6·13-6-11·13-6-17·13-11-5, 산림 13-4-1. 각 절에서도 지적 항목 중심으로 대조했다. 이외 절의 로직별 직종·기계·재료, 조건 조합, 비목, 배수 환산은 **미완료**다. 따라서 0건 부문도 무결 판정이 아니다. + +추가 점검: 모든 로직 출처를 `section_of()`로 찾아 1,354줄 중 8줄은 정확한 소제목을 반환하지 않았다(GC000482·483·484·485·487·506·511, GF000183). 원문에 없는 로직이라는 뜻은 아니다. 이 8줄은 제목 표기·하위 번호를 수동 대조해야 한다. 원문 절마다 로직이 필요한지의 판정도 아직 하지 않았다. 산림 본문 `###` 소제목 274개와 산림 로직 고유 원문번호 212개의 단순 집합 비교는 계수·설명 절과 부모 번호가 섞여 누락 건수로 쓸 수 없다. + +검사 도구의 0건은 원문 표의 **숫자 집합**이 맞는다는 뜻이다. 표 행별 규격·조건 조합 누락은 이 검사로 배제할 수 없다. 특히 위에서 확인한 `GC000943`의 550㎾h가 Tar Epoxy에 연결된 문제도 숫자 자체가 원문에 있으므로 통과한다. + +## 2차 전수 대조 · 산림품셈 + +범위: 산림 로직 251줄, 재료 호표 128줄, 소요량·계수 표 634개(저장 줄 4,159개). 재료 128줄의 호표 이름·검색어를 모두 펼쳐 대조했고 아래는 1차 보고와 겹치지 않는 차이다. 원문 절의 수치 포함 여부와 로직 결과단위·배수 식도 251줄을 검색했다. 원문 `100m당`인 `GF000163`의 블레이드 `0.31/100`, 물 `3000/100`은 `원/m`에 맞는다. 표의 숫자 집합 검사는 0건이지만, 원문 Markdown의 병합 표와 마스터의 긴 줄을 행별로 역변환하지 못하므로 **4,159줄의 규격·조건 조합이 모두 있는지는 아직 판정할 수 없다**. + +| 로직 키 | 이름 | 원문 자리(파일 · 절) | 갈래 | 무엇이 다름 | 판정 | 근거 인용 한 줄 | +|---|---|---|---|---|---|---| +| GF000060 | 목재칩 포설 | 산림_표준품셈/본문/제06장_생육보조/6-08_목재칩_포설.md · 6-8 | 연결 | 우드칩 가격 검색어가 미송각재다. | 확실 | 호표 `우드칩`; 검색어 `미송각재 30×30×3600mm 서울`. | +| GF000143 | 케이블 크레인 운반대 설치 | 산림_표준품셈/본문/제10장_자재․장비_운반/10-08_케이블_크레인_운반.md · 10-8-2 | 연결 | 소재(원목)가 제재 각재를 검색한다. | 확실 | 호표 `소재(원목)`; 검색어 `미송각재 30×30×3600mm 서울`. | +| GF000161 | 문양거푸집 설치 및 해체 | 산림_표준품셈/본문/제12장_철근콘크리트/12-05_문양거푸집(0~7m).md · 12-5 | 연결 | 문양 스티로폴이 그라스울 샌드위치판넬을 검색한다. | 확실 | 호표 `문양 스티로폴`; 검색어 `샌드위치판넬(그라스울) T50`. | +| GF000163 | 포장절단(1차 절단) | 산림_표준품셈/본문/제12장_철근콘크리트/12-07_포장절단_및_줄눈설치.md · 12-7-1 | 연결 | 커터 블레이드가 굴착기 치즐을 검색한다. | 확실 | 원문 `100m당 블레이드 0.31개`; 호표 검색어 `치즐(0.2㎥)`. | +| GF000172 | 맹암거 | 산림_표준품셈/본문/제12장_철근콘크리트/12-10_맹암거.md · 12-10 | 연결 | 부직포가 직포를, 잡석이 강모래를 검색한다. | 확실 | 호표 `부직포`, `잡석 구입`; 검색어 `직포 옥상방수용직포`, `모래 강모래`. | +| GF000180 | 집수정 | 산림_표준품셈/본문/제12장_철근콘크리트/12-15_집수정.md · 12-15 | 연결 | 집수정 뚜껑(스틸그레이팅)이 PE빗물받이 뚜껑을 검색한다. | 확실 | 호표 `집수정 뚜껑(스틸그레이팅)`; 검색어 `PE빗물받이스틸뚜껑 495×395×50T`. | +| GF000181 | 맨홀 | 산림_표준품셈/본문/제12장_철근콘크리트/12-16_맨홀.md · 12-16 | 연결 | 원형거푸집 두 줄이 폼타이를 검색한다. | 확실 | 호표 `원형거푸집(PE 10회)` 두 줄; 검색어 `폼타이 D형`. | +| GF000191 | 부직포설치 | 산림_표준품셈/본문/제12장_철근콘크리트/12-23_부직포설치.md · 12-23 | 연결 | 부직포가 직포를 검색한다. | 확실 | 호표 `부직포`; 검색어 `직포 옥상방수용직포`. | +| GF000198 | 실런트(20×25㎜) | 산림_표준품셈/본문/제12장_철근콘크리트/12-27_지수판.md · 12-27-3 | 연결 | 실런트가 수팽창 지수재를 검색한다. | 의심 | 호표 `실런트`; 검색어 `쉬트파일수팽창지수재 DPS-500` — 대체재 동등성 근거 미확인. | +| GF000199 | 신구 BOX접합 | 산림_표준품셈/본문/제12장_철근콘크리트/12-28_신구_BOX접합.md · 12-28 | 연결 | 콘크리트용 에폭시 접착제는 석고본드를, 시너는 목재전용 오일스테인을 검색한다. | 확실 | 호표 `에폭시 접착제(콘크리트용)`, `시너`; 검색어 `석고본드`, `목재전용도료 오일스테인`. | +| GF000201 | 다웰바 설치 신축 이음부 | 산림_표준품셈/본문/제12장_철근콘크리트/12-30_다웰바_설치.md · 12-30-1 | 연결·단위 | 강봉 D32가 트러스를 검색하고, PVC 파이프 Ø35㎜는 D30㎜ 제품을 검색한다. | 확실 | 호표 `강봉(D32㎜)`, `P.V.C 파이프(Ø35㎜)`; 검색어 `트러스`, `D=30mm`. | +| GF000202 | 다웰바 설치 접속스라브부 | 같은 파일 · 12-30-2 | 연결·단위 | 원형철근 Ø29는 D25 가공비를, 탄성고무받침은 익스팬션조인트휠러를, 다웰바캡은 보강재를 검색한다. | 확실 | 호표 `원형철근(∅29㎜)`, `탄성고무받침`, `다웰바캡`; 검색어 `D25mm`, `익스팬션조인트휠러`, `SRR 보강재`. | +| GF000203 | 물끊기 홈(NOTCH) 설치 | 산림_표준품셈/본문/제12장_철근콘크리트/12-31_물끊기_홈(NOTCH)_설치.md · 12-31 | 연결 | 물끊기 홈 재료가 방수석고보드를 검색한다. | 확실 | 호표 `물끊기 홈(NOTCH) 재료`; 검색어 `방수석고보드 12.5t`. | +| GF000220 | 돌쌓기 메쌓기(장비) | 산림_표준품셈/본문/제13장_구조물/13-04_돌쌓기.md · 13-4-2 | 연결 | 채움재 잡석이 강모래를 검색한다. | 확실 | 원문 `잡석을 채움재로 사용하는`; 호표 검색어 `모래 강모래`. | +| GF000224 | 돌붙임 메붙임(장비) | 산림_표준품셈/본문/제13장_구조물/13-05_돌붙임.md · 13-5-2 | 연결 | 채움재 잡석이 강모래를 검색한다. | 확실 | 호표 `잡석(채움재)`; 검색어 `모래 강모래`. | +| GF000235 | 사각형 돌망태 | 산림_표준품셈/본문/제13장_구조물/13-11_돌망태.md · 13-11-4 | 연결 | 철망태가 기초사석을 검색한다. | 확실 | 호표 `철망태`; 검색어 `기초사석 0.015~0.03㎥`. | + +원문에 수치 표가 있고 같은 절의 전용 로직·표의 `용도.로직키`가 모두 없는 절 후보는 **81개**다. 표형 데이터만 보관하려는 절도 섞일 수 있으므로 누락 판정은 `의심`이다. 원문번호별 목록(괄호는 후보 절 수): + +- 4장(3): 4-1-1, 4-2-1, 4-2-2. +- 5장(18): 5-1-1, 5-1-2, 5-1-3, 5-1-5, 5-3-2, 5-3-3, 5-3-4, 5-3-5, 5-16-1, 5-16-2, 5-17-1, 5-17-2, 5-19-1, 5-19-2, 5-26-1, 5-26-2, 5-26-3, 5-28-2. +- 6장(3): 6-4-3, 6-4-4, 6-7-1. +- 7장(7): 7-1-1, 7-5-2, 7-7-2, 7-8-1, 7-8-2, 7-9-1, 7-9-2. +- 8장(9): 8-1-1, 8-1-2, 8-2-1, 8-2-2, 8-2-3, 8-2-4, 8-2-5, 8-6-2, 8-6-3. +- 9장(25): 9-4-1, 9-5-1, 9-5-2, 9-6-1, 9-7-2, 9-8-2, 9-10-1, 9-10-2, 9-11-1, 9-12-2, 9-12-3, 9-13-7, 9-13-8, 9-13-9, 9-13-10, 9-13-11, 9-13-12, 9-13-13, 9-13-14, 9-13-15, 9-13-16, 9-13-17, 9-13-18, 9-16-3, 9-19-3. +- 10장(8): 10-6-1, 10-6-2, 10-6-3, 10-8-1, 10-9-1, 10-12-1, 10-13-1, 10-13-2. +- 12장(2): 12-38-1, 12-38-2. +- 13장(6): 13-4-6, 13-10-2, 13-11-1, 13-11-2, 13-15-1, 13-15-2. + +## 2차 전수 대조 · 건설품셈 공통 + +범위: 로직 276줄, 재료 호표 113줄, 소요량·계수 표 575개(저장 줄 2,828개). 재료 113줄의 호표 이름·검색어를 모두 펼쳤고, 원문 출처별 배수 문구를 로직 결과단위·식과 대조했다. `(10㎡당)` 노무와 `(㎡당)` 재료가 한 절에 같이 있는 2-8-8·2-8-9·2-10-1·2-10-2는 로직이 노무만 10으로 나누는 것이 원문에 맞다. 1차 보고와 겹치지 않는 발견은 아래와 같다. 원문 표의 수치 집합 검사 0건은 조건 조합별 행 수 일치를 증명하지 않는다. + +| 로직 키 | 이름 | 원문 자리(파일 · 절) | 갈래 | 무엇이 다름 | 판정 | 근거 인용 한 줄 | +|---|---|---|---|---|---|---| +| GC000001 | 철제조립식 가설건축물 손료 | 건설공사_표준품셈/본문/01_공통부문/제02장_가설공사/2-02_손율.md · 2-2-3 | 연결 | 외부·내부 PANEL, CANOPY, 박공PANEL, 천장판을 T챤넬로, 중도리를 트러스로, T-BAR를 처마후레싱으로 검색한다. | 확실 | 원문 품명 `PANEL`, `CANOPY`, `중도리`, `T-BAR`; 로직 검색어 `T챤넬`, `트러스`, `처마후레싱`. | +| GC000002 | 콘테이너형 가설건축물 손료 | 같은 파일 · 2-2-3 | 연결·단위 | 3×3·3×6·3×9m 건축물 셋 모두 12×3×2.6m 콘테이너를 검색한다. 규격 차이를 반영하지 않는다. | 확실 | 호표 규격 `3.0x3.0m`·`3.0x6.0m`·`3.0x9.0m`; 검색어 `12.0*3.0*2.6m`. | +| GC000031 | 낙하물 방지망(비계) 설치 및 해체 | 건설공사_표준품셈/본문/01_공통부문/제02장_가설공사/2-08_추락재해_및_낙하물방지시설.md · 2-8-1 | 연결 | 브라켓을 안전발판으로, 그물망을 옥상방수용 직포로 검색한다. | 확실 | 호표 `브라켓`·`그물망`; 검색어 `안전발판`·`직포 옥상방수용직포`. | +| GC000032 | 낙하물 방지망(플라잉넷) 설치 및 해체 | 같은 파일 · 2-8-2 | 연결 | 브라켓을 안전발판으로, 사다리를 강관비계로, 그물망을 직포로 검색한다. | 확실 | 호표 `브라켓`·`사다리`·`그물망`; 검색어 `안전발판`·`강관비계`·`직포`. | +| GC000037 | 철골 안전망 설치 및 해체 | 같은 파일 · 2-8-7 | 연결 | 그물망을 옥상방수용 직포로 검색한다. | 확실 | 원문 `그물망 · ㎡ · 12.4`; 검색어 `직포 옥상방수용직포`. | +| GC000038 | 비계주위 보호망 설치 및 해체 | 같은 파일 · 2-8-8 | 연결 | 보호망을 옥상방수용 직포로 검색한다. | 확실 | 호표 `보호망`; 검색어 `직포 옥상방수용직포`. | +| GC000039 | 갱폼주위 보호망 설치 및 해체 | 같은 파일 · 2-8-9 | 연결 | 보호망을 옥상방수용 직포로 검색한다. | 확실 | 호표 `보호망`; 검색어 `직포 옥상방수용직포`. | +| GC000062 | 방진망 설치 및 해체 | 건설공사_표준품셈/본문/01_공통부문/제02장_가설공사/2-10_피해방지시설.md · 2-10-2 | 연결 | 방진망을 PVC 천막지로 검색한다. | 확실 | 원문 `방진망 · ㎡ · 1.06`; 검색어 `PVC천막지 T-210`. | +| GC000068 | 건축물보양 | 건설공사_표준품셈/본문/01_공통부문/제02장_가설공사/2-11_현장관리.md · 2-11-1 | 연결 | 부직포를 직포로, 풀을 그리스로 검색한다. | 확실 | 호표 `부직포`·`풀`; 검색어 `직포 옥상방수용직포`·`그리스(일반범용)`. | +| GC000127 | 버팀목 설치·해체 | 건설공사_표준품셈/본문/01_공통부문/제03장_토공사/3-08_보강토_옹벽.md · 3-8-3 | 연결·단위 | 각재 10×10㎝가 30×30㎜ 각재를 검색한다. | 확실 | 호표 `각재 10㎝×10㎝`; 검색어 `외송각재 30×30×3600mm`. | +| GC000185 | 연직배수재(PBD) 타설 | 건설공사_표준품셈/본문/01_공통부문/제05장_기초공사/5-02_연약지반처리.md · 5-2-3 | 연결 | 드레인 보드를 고무바로 검색한다. | 확실 | 호표 `드레인 보드`; 검색어 `고무바 2.2cm`. | +| GC000197 | 차수재 부설(1겹) | 건설공사_표준품셈/본문/01_공통부문/제05장_기초공사/5-04_차수.md · 5-4-1 | 연결 | 부직포·지오콤포지트는 옥상방수용 직포로, 벤토나이트매트는 PET 매트로 검색한다. | 확실 | 호표 `부직포`·`지오콤포지트`·`벤토나이트매트`; 검색어 `직포`·`PET 매트`. | +| GC000203 | 에폭시 콘크리트 접착제 바르기 | 건설공사_표준품셈/본문/01_공통부문/제06장_철근콘크리트공사/6-01_콘크리트.md · 6-1-5 | 연결 | 에폭시 접착제 두 종은 석고본드를, 시너는 목재용 오일스테인을 검색한다. | 확실 | 호표 `에폭시 신구콘크리트 접착제`, `시너`; 검색어 `석고본드`, `목재전용도료 오일스테인`. | +| GC000212 | 강재거푸집 인력 설치 및 해체 | 건설공사_표준품셈/본문/01_공통부문/제06장_철근콘크리트공사/6-03_거푸집.md · 6-3-2 | 연결 | 강재거푸집 손료가 K-SPAN 패널 가격을 검색한다. | 확실 | 호표 `강재거푸집 손료`; 검색어 `패널(K-SPAN PANEL) 0.6×305mm`. | +| GC000215 | 문양거푸집(판넬) 설치 및 해체 | 같은 파일 · 6-3-4 | 연결 | 문양거푸집 패널이 K-SPAN 패널을 검색한다. | 확실 | 호표 `문양거푸집(판넬)`; 검색어 `패널(K-SPAN PANEL) 0.6×305mm`. | +| GC000216 | 합성수지(P.E) 원형 맨홀 거푸집 설치 및 해체 | 같은 파일 · 6-3-5 | 연결 | 원형맨홀거푸집 다섯 규격 모두 폼타이를 검색한다. | 확실 | 호표 `원형맨홀거푸집 ø740…ø1800 손료`; 검색어 `폼타이 D형`. | + +원문 수치 표가 있으나 해당 절 전용 로직도 표의 `용도.로직키`도 없는 후보는 **21절(의심)**: 1장 1-3-1, 1-4-3·4·5·6·7; 2장 2-1-1·2, 2-2-2·4; 6장 6-2-1; 7장 7-4-2·3; 8장 8-2-1·3·5·8·9·13·15·21. 적용기준·기계 성능처럼 독립 단가 로직이 필요 없는 표가 섞여 있으므로, 이를 그대로 결손 확정 수로 쓰지 않는다. + +## 2차 전수 대조 · 건설품셈 토목 + +범위: 로직 282줄, 재료 호표 71줄, 소요량·계수 표 765개(저장 줄 7,445개). 재료 71줄을 모두 펼쳐 대조했다. 원문 1-8-9의 10㎡당 재료는 `GC000319`이 `/10`을 적용하고, 노무는 시공량으로 나누므로 이 배수 자체는 맞는다. 원문 표의 수치 집합 검사 0건과 행별 조건 조합의 완전성은 별개다. + +| 로직 키 | 이름 | 원문 자리(파일 · 절) | 갈래 | 무엇이 다름 | 판정 | 근거 인용 한 줄 | +|---|---|---|---|---|---|---| +| GC000319 | 차선도색 융착식 도료 수동식 | 건설공사_표준품셈/본문/02_토목부문/제01장_도로포장공사/1-08_교통시설공.md · 1-8-9 | 연결 | 융착식 도료 프라이머가 신구접착·방청제를 검색한다. | 확실 | 호표 `프라이머(융착식 도료)`; 검색어 `신구접착및방청제 LDS-MP`. | +| GC000337 | 낙석방지망 철망 및 와이어 설치 | 건설공사_표준품셈/본문/02_토목부문/제01장_도로포장공사/1-09_부대공.md · 1-9-4 | 단위 | 결속선 원문은 `m`, 검색어는 철선 #8이다. 가격 후보가 길이 단위인지 확인 필요. | 의심 | 원문 `결속선 · m · 0.3`; 검색어 `보통철선 #8 서울`. | +| GC000374 | 강교 제작 기타재료 | 건설공사_표준품셈/본문/02_토목부문/제05장_강구조공사/5-01_강교제작(공장제작).md · 5-1-3 | 연결 | 용접봉(강교 제작)이 아세틸렌을 검색한다. | 확실 | 호표 `용접봉(강교 제작)`; 검색어 `아세틸렌 98%`. | +| GC000375 | 강교 소재 표면처리 | 건설공사_표준품셈/본문/02_토목부문/제05장_강구조공사/5-02_강교도장.md · 5-2-1 | 연결 | 무기질아연말 샵프라이머가 목재용 오일스테인을 검색한다. | 확실 | 호표 `무기질아연말 샵프라이머`; 검색어 `목재전용도료 오일스테인`. | +| GC000431, GC000432 | 보링 천공(토사·암반) | 건설공사_표준품셈/본문/02_토목부문/제08장_지반조사/8-01_보링.md · 8-1-2·3 | 연결 | 코아바렐·크라운비트·파이프·코아리프터 등 원문 소모품 12줄을 굴착기 치즐로 검색한다. | 확실 | 호표 `싱글코아바렐`, `더블코아바렐`, `다이아몬드비트` 등; 검색어 `치즐(0.2㎥)`. | +| GC000433, GC000434, GC000435 | 표준관입·시료채취·베인전단시험 | 건설공사_표준품셈/본문/02_토목부문/제08장_지반조사/8-02_시험.md · 8-2-1·2·3 | 연결 | 샘플러·신월튜브·베인 블레이드가 굴착기 치즐을 검색한다. | 확실 | 호표 `샘플러`, `신월튜브`, `베인 블레이드`; 검색어 `치즐(0.2㎥)`. | +| GC000442, GC000443, GC000444, GC000445, GC000446 | 대구경 보링 천공 | 건설공사_표준품셈/본문/02_토목부문/제08장_지반조사/8-04_대구경_보링(지하수개발).md · 8-4-1·2 | 연결 | 윙비트가 치즐을, 벤토나이트가 수팽창 지수재를 검색한다. | 확실 | 호표 `윙비트`, `벤토나이트`; 검색어 `치즐(0.2㎥)`, `쉬트파일수팽창지수재 DPS-500`. | +| GC000447, GC000448, GC000449 | 대구경 보링 암반층 천공 | 같은 파일 · 8-4-2 | 연결 | 에어해머·버튼비트가 치즐을 검색한다. | 확실 | 호표 `에어해머`, `버튼비트`; 검색어 `치즐(0.2㎥)`. | + +원문 수치 표가 있으나 해당 절 전용 로직과 표의 `용도.로직키`가 모두 없는 후보는 **13절(의심)**: 3-1-2, 3-2-1·2·3·4, 7-1-1·2·3·4, 7-4-4·5·6, 9-5-8. 계수·참고 표 여부를 확인해야 결손으로 확정할 수 있다. + +## 2차 전수 대조 · 건설품셈 건축 + +범위: 로직 163줄, 재료 호표 92줄, 소요량·계수 표 353개(저장 줄 972개). 재료 92줄 전부의 호표 이름·검색어를 대조했다. 원문에 `10m당`, `100㎡당`, `1,000개당` 등이 명시된 절의 로직 결과단위는 같은 배수로 유지되거나 시공량으로 나눴다. 수치 표가 있는데 해당 절 전용 로직과 `용도.로직키`가 모두 없는 후보는 **0절**이다. 원문 표의 행별 조건 조합 일치는 별도 미판정이다. + +| 로직 키 | 이름 | 원문 자리(파일 · 절) | 갈래 | 무엇이 다름 | 판정 | 근거 인용 한 줄 | +|---|---|---|---|---|---|---| +| GC000565 | 철골 현장용접(CO2 반자동) | 건설공사_표준품셈/본문/03_건축부문/제01장_철골공사/1-02_철골_세우기.md · 1-2-5 | 연결 | CO₂ 와이어와 탄산가스가 모두 아세틸렌을 검색한다. | 확실 | 호표 `CO_2 와이어`, `탄산가스`; 검색어 `아세틸렌 98%`. | +| GC000576 | 치장벽돌 공간쌓기 및 치장줄눈 | 건설공사_표준품셈/본문/03_건축부문/제02장_조적공사/2-01_벽돌.md · 2-1-2 | 연결 | 치장벽돌이 주차라인블록을 검색한다. | 확실 | 호표 `치장벽돌`; 검색어 `주차라인블록 1000X300X150`. | +| GC000604 | 카페트 설치(1차 발견 보강) | 건설공사_표준품셈/본문/03_건축부문/제05장_수장공사/5-01_바닥.md · 5-1-2 | 연결 | 카페트 외에 펠트도 옥상방수용 직포를 검색한다. | 확실 | 원문 `펠트 · ㎡ · 1.1`; 검색어 `직포 옥상방수용직포`. | +| GC000616 | 걸레받이 설치 | 건설공사_표준품셈/본문/03_건축부문/제05장_수장공사/5-03_벽.md · 5-3-5 | 연결 | 테라조와 합성수지 걸레받이 모두 처마후레싱을 검색한다. | 확실 | 호표 `테라조`, `합성수지 걸레받이`; 검색어 `처마후레싱 C/S`. | +| GC000618 | 도배바름(초배 · 정배) | 같은 파일 · 5-3-7 | 연결 | 도배용 풀이 그리스를 검색한다. | 확실 | 호표 `풀`; 검색어 `그리스(일반범용) CROWN`. | +| GC000620, GC000621, GC000622, GC000623, GC000625, GC000626, GC000628 | 단열재 시공 여러 절 | 건설공사_표준품셈/본문/03_건축부문/제05장_수장공사/5-04_단열.md · 5-4-1·2·3·4·6·7·9 | 연결 | 단열재·단열판에 완제품 그라스울 샌드위치판넬 가격을 연결한다. 단열재 종류·두께가 원문 또는 입력에 따라 달라질 수 있어 동등성 확인 필요. | 의심 | 호표 `단열재`/`단열판`; 검색어 `샌드위치판넬(그라스울) T50`. | +| GC000627 | 방습필름 설치 | 같은 파일 · 5-4-8 | 연결 | 방습필름이 PVC 천막지를 검색한다. | 확실 | 호표 `방습필름`; 검색어 `PVC천막지 T-210`. | +| GC000637, GC000638, GC000639 | 시트방수 붙임 3방식 | 건설공사_표준품셈/본문/03_건축부문/제06장_방수공사/6-03_시트_방수.md · 6-3-1·2·3 | 연결 | 방수시트 세 로직 모두 방수석고보드를 검색한다. | 확실 | 호표 `방수시트`; 검색어 `방수석고보드 12.5t`. | +| GC000646 | 벤토나이트방수 붙임 | 건설공사_표준품셈/본문/03_건축부문/제06장_방수공사/6-05_기타방수.md · 6-5-3 | 연결 | 벤토나이트방수재를 석고보드로, PE필름을 PVC천막지로, 카트리지를 화약 카트리지로, 와셔를 핀볼트로 검색한다. | 확실 | 호표 `벤토나이트방수재`·`PE필름`·`카트리지`·`와셔`; 검색어 `방수석고보드`·`PVC천막지`·`카트리지 화약`·`핀볼트`. | +| GC000655 | 폴리카보네이트 설치 | 건설공사_표준품셈/본문/03_건축부문/제07장_지붕_및_홈통공사/7-01_지붕.md · 7-1-6 | 연결 | 폴리카보네이트가 불연천장재(마이톤)를 검색한다. | 확실 | 호표 `폴리카보네이트`; 검색어 `불연천장재(마이톤) 12mm`. | +| GC000665 | 와이어메시 바닥깔기 | 건설공사_표준품셈/본문/03_건축부문/제08장_금속공사/8-01_제품.md · 8-1-3 | 연결 | 와이어메시가 메쉬스크린을 검색한다. | 확실 | 호표 `와이어메시`; 검색어 `메쉬스크린 H=0.9m`. | +| GC000666 | 인서트 설치 | 같은 파일 · 8-1-4 | 연결 | 인서트가 L형앵커볼트를 검색한다. | 확실 | 호표 `인서트`; 검색어 `L형앵커볼트 5/8"`. | +| GC000670 | 앵커고정식 난간 설치(공장제작) | 건설공사_표준품셈/본문/03_건축부문/제08장_금속공사/8-02_시설물.md · 8-2-2 | 연결 | AL리벳이 작은나사(스크류)를 검색한다. | 확실 | 호표 `AL리벳`; 검색어 `작은나사(스크류) ø 4.2㎜`. | + +## 2차 전수 대조 · 건설품셈 기계설비 + +범위: 로직 272줄, 재료 호표 264줄, 소요량·계수 표 509개(저장 줄 8,849개). 재료 264줄의 호표 이름·검색어를 펼쳤고 원문 배수 표기를 검색했다. 100㎡당·10m당 같은 배수 절은 이 부문에 없었다. 수치 표가 있는데 해당 절 전용 로직과 `용도.로직키`가 모두 없는 후보는 **2절(13-4-1, 13-11-4; 의심)**이다. 원문 표의 조건별 행 완전성은 별도 미판정이다. + +| 로직 키 | 이름 | 원문 자리(파일 · 절) | 갈래 | 무엇이 다름 | 판정 | 근거 인용 한 줄 | +|---|---|---|---|---|---|---| +| GC000724 | 스테인리스 강관 용접접합 | 건설공사_표준품셈/본문/04_기계설비부문/제01장_배관공사/1-03_스테인리스_강관.md · 1-3-1 | 연결 | TIG 용접봉과 아르곤가스 모두 아세틸렌을 검색한다. | 확실 | 호표 `TIG 용접봉`, `아르곤가스`; 검색어 `아세틸렌 98%`. | +| GC000752 | 제연댐퍼 설치 | 건설공사_표준품셈/본문/04_기계설비부문/제02장_덕트공사/2-02_덕트기구.md · 2-2-6 | 연결 | 블라인드리벳을 핀볼트로, 철물을 철근부식방지재로 검색한다. | 확실 | 호표 `블라인드리벳`, `철물`; 검색어 `핀볼트 M12`, `철근부식방지재 MT-3`. | +| GC000901 | 플랜트 배관 보온(성형물) | 건설공사_표준품셈/본문/04_기계설비부문/제13장_플랜트설비공사/13-03_배관_및_기기보온.md · 13-3-1 | 연결 | 보온 성형물이 STS 단열바 프레임을, Sheet Metal Screw가 빗자루를 검색한다. | 확실 | 호표 `보온 성형물`, `Sheet Metal Screw`; 검색어 `단열바 STS 프레임`, `빗자루 도로청소용`. | +| GC000931 | 수차 설치(Kaplan · Francis · Propeller) | 건설공사_표준품셈/본문/04_기계설비부문/제13장_플랜트설비공사/13-06_수력발전_기계설비.md · 13-6-1 | 연결 | 걸레가 그리스를, 비닐세트·소창직은 PVC 코팅철선을, 시너·광명단·조합페인트는 목재용 오일스테인을 검색한다. | 확실 | 원문 재료명 `걸레`, `비닐세트`, `시너`, `광명단`; 각 검색어 `그리스`, `PVC코팅철선`, `오일스테인`. | +| GC000932 | 수력 발전기 설치 | 같은 파일 · 13-6-2 | 연결·단위 | 목탄·걸레가 그리스를, 가스용접봉·신주용접봉이 아세틸렌을, 비닐시트가 사포를 검색한다. 산소 원문은 4,500ℓ/병인데 로직은 병 수에 6,000을 곱한다. | 확실 | 원문 `산소 · 4,500ℓ · 병 · 0.109`; 호표 식 `0.109 * 6000`. | +| GC000933, GC000934 | 수문 제작(Tainter·Roller Gate) | 같은 파일 · 13-6-3 | 연결 | 광명단이 목재용 오일스테인을 검색한다. | 확실 | 호표 `광명단`; 검색어 `목재전용도료 오일스테인`. | +| GC000935, GC000936 | 수문 설치(Tainter·Roller Gate) | 같은 파일 · 13-6-4 | 연결 | 광명단이 목재용 오일스테인을 검색한다. | 확실 | 호표 `광명단`; 검색어 `목재전용도료 오일스테인`. | +| GC000937 | Stop-Log 제작 | 같은 파일 · 13-6-5 | 연결 | 광명단·페인트가 신구접착·방청제를 검색한다. | 확실 | 호표 `광명단`, `페인트`; 검색어 `신구접착및방청제 LDS-MP`. | +| GC000941 | Steel Penstock 현장제관 | 같은 파일 · 13-6-9 | 연결 | 시너 두 줄 모두 목재용 오일스테인을 검색한다. | 확실 | 호표 `시너`; 검색어 `목재전용도료 오일스테인`. | +| GC000945 | Tainter Gate Guide Metal 제작 | 같은 파일 · 13-6-13 | 연결 | 전기용접봉이 아세틸렌을 검색한다. | 확실 | 호표 `전기용접봉`; 검색어 `아세틸렌 98%`. | +| GC000947 | Trash Rack 제작 | 같은 파일 · 13-6-15 | 연결 | 연마석·톱날이 도로청소용 빗자루를, Bite가 굴착기 치즐을 검색한다. | 확실 | 호표 `연마석`, `톱날`, `Bite`; 검색어 `빗자루 도로청소용`, `치즐(0.2㎥)`. | +| GC000950 | Trash Rack 설치 | 같은 파일 · 13-6-16 | 연결 | 시너가 목재용 오일스테인을 검색한다. | 확실 | 호표 `시너`; 검색어 `목재전용도료 오일스테인`. | +| GC000972 | 노 기밀시험 | 건설공사_표준품셈/본문/04_기계설비부문/제13장_플랜트설비공사/13-07_제철기계설비.md · 13-7-22 | 연결 | 가루비누가 그리스를 검색한다. | 확실 | 호표 `가루비누`; 검색어 `그리스(일반범용) CROWN`. | +| GC000986 | Over Head Crane 설치 | 건설공사_표준품셈/본문/04_기계설비부문/제13장_플랜트설비공사/13-10_운반기계설비.md · 13-10-2 | 연결 | 걸레가 그리스를 검색한다. | 확실 | 호표 `걸레`; 검색어 `그리스(일반범용) CROWN`. | +| GC000987 | Gantry Crane 설치 | 같은 파일 · 13-10-3 | 연결 | 광명단이 목재용 오일스테인을 검색한다. | 확실 | 호표 `광명단`; 검색어 `목재전용도료 오일스테인`. | +| GC000993 | 플랜트 공사별 설치 소모자재(1차 발견 보강) | 건설공사_표준품셈/본문/04_기계설비부문/제13장_플랜트설비공사/13-11_기타_기계설비.md · 13-11-5 | 연결 | 1차 발견 외에 시너도 목재용 오일스테인을, 비닐시드도 PVC천막지를 검색한다. | 확실 | 호표 `시너`, `비닐시드`; 검색어 `목재전용도료 오일스테인`, `PVC천막지 T-210`. | +| GC000931~GC000950, GC000986, GC000987, GC000993 (23키) | 아세틸렌이 포함된 플랜트·운반기계 로직 | 13-06_수력발전_기계설비.md · 13-6-1~17; 13-10_운반기계설비.md · 13-10-2·3; 13-11_기타_기계설비.md · 13-11-5 | 단위 | 아세틸렌 원문 수량은 병 단위(규격 2,100·2,500·3,500·4,000·4,500ℓ 등)인데 호표는 같은 수를 kg로 사용한다. 해당 23키 모두 병→kg 환산식이 없다. | 확실 | 예: 13-6-11 `아세틸렌 · 2,100ℓ입 · 1.6병`; `GC000943` 호표 `kg · 1.6`. | + +## 2차 전수 대조 · 건설품셈 유지관리 + +범위: 로직 110줄, 재료 호표 7줄, 소요량·계수 표 197개(저장 줄 783개). 재료 7줄을 전부 대조했다. `GC001050`의 10㎡당 재료는 `/10`, `GC001056`의 횡재부 10m당 가스는 `/10`으로 처리한다. 원문 수치 표가 있으나 해당 절 전용 로직과 `용도.로직키`가 모두 없는 후보는 **36절(의심)**: 3-1-1~5, 3-2-1~12, 3-3-1~3, 3-4-1~5, 4-1-1~7, 4-2-1, 4-3-1~3. 표만 필요한 기준인지 별도 확인이 필요하다. 표의 행별 조건 조합 완전성은 미판정이다. + +| 로직 키 | 이름 | 원문 자리(파일 · 절) | 갈래 | 무엇이 다름 | 판정 | 근거 인용 한 줄 | +|---|---|---|---|---|---|---| +| GC001050 | 차선도색 융착식 도료(수동식) | 건설공사_표준품셈/본문/05_유지관리부문/제02장_토목/2-01_도로포장공사.md · 2-1-15 | 연결 | 프라이머가 신구접착·방청제를, 프로판가스가 아세틸렌을 검색한다. | 확실 | 원문 `프라이머 · kg · 2.0`, `프로판가스 · kg · 2.0`; 검색어 `LDS-MP`, `아세틸렌 98%`. | +| GC001055 | 현장가열 표층재생 | 같은 파일 · 2-1-19 | 누락·단위 | 원문 주⑤의 `100㎡당 팁(날) 0.7개`를 계산에 계상하지 않는다. 결과단위 `원/㎡`라면 0.007개/㎡가 필요하지만 호표에 팁 줄이 없다. | 확실 | 원문 `⑤ 100㎡당 팁(날) 0.7개를 계상한다.` | + +## 2차 대조 종합과 남은 한계 + +아래 점검 수는 로직을 구조·참조·수치 검사에 통과시킨 수이며, 사람의 행별 원문 대조 완료 수가 아니다. `새 차이`는 2차 표의 행 수(여러 키를 묶은 행과 1차 발견 보강 포함)로서 고유 결함 키 수가 아니다. 앞의 1차 요약표는 당시 결과이며 이 표가 현재 범위다. + +| 부문 | 로직 수 | 점검 수(기계) | 재료 호표 전수 | 새 차이(표 행) | 누락 절 후보(의심) | 원문 조건·규격 줄 전수 판정 | +|---|---:|---:|---:|---:|---:|---| +| 산림품셈 | 251 | 251 | 128 | 16 | 81 | 미완료 | +| 건설 공통 | 276 | 276 | 113 | 16 | 21 | 미완료 | +| 토목 | 282 | 282 | 71 | 8 | 13 | 미완료 | +| 건축 | 163 | 163 | 92 | 13 | 0 | 미완료 | +| 기계설비 | 272 | 272 | 264 | 17 | 2 | 미완료 | +| 유지관리 | 110 | 110 | 7 | 2 | 36 | 미완료 | +| 합계 | 1,354 | 1,354 | 675 | 72 | 153 | 미완료 | + +검사 대상 표는 3,033개, 저장 줄은 25,036개다. 1차 `check_master.py 본문` 결과 0건은 절 안에 등장하는 **숫자 집합**이 맞는다는 증거일 뿐, 각 숫자가 올바른 규격·조건 줄에 놓였다는 증거가 아니다. 2차에서는 675개 재료 호표 줄의 명칭·검색어를 전부 펼쳐 확인했고, 배수 표기가 나타나는 로직 식을 대조했다. 그러나 원문 Markdown의 병합 셀과 여러 갈래로 분해된 JSON 표를 3,033개 모두 일대일로 재구성하여 25,036줄의 규격·조건 조합 및 비목을 판정하지 못했다. 따라서 이 보고서는 요청한 **완전한 전수 대조의 완료 판정이 아니며**, 위의 미완료 범위는 추후 추가 검증이 필요하다. 원문 절 누락 후보 153개도 참고·계수 표가 포함되므로 확정 결손으로 계산하지 않는다. + +## 3차 장별 대조 · 산림품셈 4장 나무베기 + +원문 `4-1-1`, `4-2-1`, `4-2-2`의 수치 표와 대응 소요량 `QF000041`·`QF000045`·`QF000047`을 조건·수치·단위별로 대조했다. 각각 원문 유효 줄 3·12·9개와 자료 줄 3·12·9개가 맞고, 확인한 값의 자리 밀림은 없다. `4-2-2`의 원문 3직종/장비 × 3나무높이는 자료의 9줄로 펴져 있다. 하지만 세 표 모두 `용도.로직키=[]`이고 산림 로직에서 해당 원문번호가 없다. + +| 로직 키 | 이름 | 원문 자리(파일 · 절) | 갈래 | 무엇이 다름 | 판정 | 근거 인용 한 줄 | +|---|---|---|---|---|---|---| +| 없음 (QF000041) | 임업용 동력기계톱 수확베기 | 산림_표준품셈/본문/제04장_나무베기/4-01_수확베기.md · 4-1-1 | 누락 | 벌도목별 작업량과 벌목부가 저장됐지만 단가 로직이 없다. 후보 분류: 누락 확정. | 확실 | 원문 `단목 20.17㎥/1인/1일 · 벌목부`; `QF000041 용도.로직키=[]`. | +| 없음 (QF000045) | 단목베기 100본당 | 산림_표준품셈/본문/제04장_나무베기/4-02_단목베기.md · 4-2-1 | 누락 | 흉고직경 11구간의 벌목·가지제거·토막내기 및 작업보조 소요인력은 표에 있으나 단가 로직이 없다. 후보 분류: 누락 확정. | 확실 | 원문 `10㎝이하 · 0.2 · 0.1 · 0.1 · 벌목부`, `작업보조 0.6 · 보통인부`; `QF000045 용도.로직키=[]`. | +| 없음 (QF000047) | 단목베기 1,000㎡당 | 같은 파일 · 4-2-2 | 누락 | 나무높이별 벌목부·보통인부·굴착기 소요량은 표에 있으나 단가 로직이 없다. 후보 분류: 누락 확정. | 확실 | 원문 `벌목부 2.14·2.80·3.65인`, `굴착기 2.71·3.54·4.61hr`; `QF000047 용도.로직키=[]`. | + +## 3차 장별 대조 · 산림품셈 5장 누락 후보 18절 + +원문 절의 숫자 표와 산림 로직의 `원문번호`, 표의 `용도.로직키`를 대조했다. 아래 분류는 단가 로직 존재 여부에 대한 것이며, 5장 전체 소요량 표의 조건·규격 줄 전수 검증은 **아직 끝나지 않았다**. + +| 원문 절 | 분류 | 이유와 근거 한 줄 | +|---|---|---| +| 5-1-1 | 누락 확정 | 관목굴취 `특별인부 0.084…0.408인`, `보통인부 0.012…0.072인/10본당`이 있는데 해당 로직 없음. | +| 5-1-2 | 누락 확정 | 교목굴취 나무높이별 특별인부·보통인부 수량이 있는데 해당 로직 없음. | +| 5-1-3 | 누락 확정 | 교목굴취 근원직경별 인력·굴착기·크레인 수량이 있는데 해당 로직 없음. | +| 5-1-5 | 참고·계수 표라 로직 불필요 | 제목부터 `떼운반 적재 기준표`; 지게·리어카 등의 적재 매수와 싣고부리기 시간·인원 기준을 담은 보조 표. 독립 공종 로직 부재만으로 결손으로 보지 않음. | +| 5-3-2 | 누락 확정 | 관목식재(단식) 수고별 특별인부·보통인부 수량이 있는데 해당 로직 없음. | +| 5-3-3 | 누락 확정 | 관목식재(군식) 수고별 특별인부·보통인부 수량이 있는데 해당 로직 없음. | +| 5-3-4 | 누락 확정 | 교목식재 나무높이별 인력·장비 수량이 있는데 해당 로직 없음. | +| 5-3-5 | 누락 확정 | 교목식재 흉고직경별 인력·장비 수량이 있는데 해당 로직 없음. | +| 5-16-1 | 누락 확정 | 단끊기 공정별 보통인부 수량이 있는데 해당 로직 없음. | +| 5-16-2 | 사람 확인 필요 | 선떼붙이기 표는 단끊기 `1m`와 떼붙임 `물량산출`의 합성 기준. 두 하위 로직과 조합하여 처리할 의도인지 확인 필요. | +| 5-17-1 | 사람 확인 필요 | 떼조공은 단끊기·줄떼심기·선떼붙이기 조합 표. 독립 로직 필요성 확인 필요. | +| 5-17-2 | 사람 확인 필요 | 돌조공은 단끊기·돌쌓기 조합 표. 독립 로직 필요성 확인 필요. | +| 5-19-1 | 누락 확정 | 표토절취 깊이별 보통인부 소요량이 있는데 해당 로직 없음. | +| 5-19-2 | 누락 확정 | 표토펴기 ㎥당·100㎡당 보통인부 수량이 있는데 해당 로직 없음. | +| 5-26-1 | 누락 확정 | 막갈이 사토·양토·식토 × 깊이별 `인/992㎡당` 수량이 있는데 해당 로직 없음. | +| 5-26-2 | 누락 확정 | 흙부수기 토성 × 깊이별 `인/992㎡당` 수량이 있는데 해당 로직 없음. | +| 5-26-3 | 누락 확정 | 돌자갈치우기 토성 × 함량별 `인/992㎡당` 수량이 있는데 해당 로직 없음. | +| 5-28-2 | 누락 확정 | 방초·야자섬유매트 포장 `조경공 2인+보통인부 1인`, 90·130㎡/일 시공량이 있는데 해당 로직 없음. | + +5장 18절 중 독립 단가화 가능한 14절을 누락 확정, 보조 기준표 1절을 로직 불필요, 조합 성격 3절을 사람 확인 필요로 나눴다. `5-26`의 밑수는 원문 그대로 **992㎡**이며 1,000㎡로 바꾸어 읽지 않았다. + +## 3차 추가 확인 · 산림품셈 12-38 유로폼 + +후보 `12-38-1` 사용횟수와 `12-38-2` 사용수량은 상위 `GF000210`이 맡는 절의 일부다. 그러나 그 로직의 호표는 형틀목공·보통인부 두 줄뿐이며, 비고에도 `유로폼 자재(12-38-2)는 임대료·손료로 따로 — 요소 없음`, `주자재비가 없어 빠짐`이라고 명시되어 있다. 따라서 **로직 자체가 전혀 없는 절**로 집계하지 않고 아래처럼 분류한다. + +| 로직 키 | 이름 | 원문 자리(파일 · 절) | 갈래 | 무엇이 다름 | 판정 | 근거 인용 한 줄 | +|---|---|---|---|---|---|---| +| GF000210 | 유로폼 설치 및 해체 | 산림_표준품셈/본문/제12장_철근콘크리트/12-38_유로폼.md · 12-38-1·2 | 누락 | `12-38-1`은 유로폼 자재 손료의 사용횟수 기준이라 독립 단가 로직 불필요; `12-38-2`의 패널·부자재 사용수량은 상위 인력 로직이 맡는 공종이지만 실제 호표에 빠져 재료비가 계산되지 않는다. | 확실 | `GF000210` 비고 `유로폼 자재(12-38-2)는 임대료·손료로 따로 — 요소 없음 · 부자재 요율…주자재비가 없어 빠짐`; 호표 인력 2줄뿐. | + +## 3차 자재품목표 대조 · 대표 오류의 대체 후보 유무 + +`재료_자재품목.json`의 이름·규격을 검색했다. 아래 `있음`은 **같은 계열의 후보가 목록에 존재한다는 뜻**이지 규격·단위·가격까지 적합한 최종 대체품을 확정했다는 뜻은 아니다. 반대로 `검색 없음`도 공백·동의어·다른 재료 원천까지 포함한 절대 부재 증명은 아니다. + +| 로직 키 | 호표 재료 줄 | 현재 검색어 | 자재품목표 대체 후보 | 판정 | +|---|---|---|---|---| +| GC000575 | 시멘트벽돌 | 주차라인블록 1000X300X150 | 시멘트벽돌 `190×90×57mm` `MT005760` 등 17줄 있음 | 다른 품목 연결 확실; 후보 있음 | +| GC000604 | 카페트 | 불연천장재(마이톤) | 카페트 `MT008142`, 타일카페트 `MT008140` 있음 | 다른 품목 연결 확실; 후보 있음 | +| GC000319, GC001050 | 융착식 도료 프라이머 | 신구접착및방청제 LDS-MP | 융착식 프라이머 `MT003454` 있음(ℓ; 원문 kg면 환산 검토 필요) | 다른 품목 연결 확실; 후보 있음 | +| GC001050 | 프로판가스 | 아세틸렌 98% | 프로판가스 일반용 `MT003175`, kg 있음 | 다른 품목 연결 확실; 후보 있음 | +| GC000375 | 무기질아연말 샵프라이머 | 목재전용도료 오일스테인 | 무기질 아연말 전처리 프라이머 `MT003460`, 무기질 아연말 프라이머 `MT003461` 있음 | 다른 품목 연결 확실; 후보 있음 | +| GC000665 | 와이어메시 | 메쉬스크린 H=0.9m | 와이어메쉬(용접철망) `MT007128` 등 있음 | 다른 품목 연결 확실; 후보 있음 | +| GC000933~GC000936, GC000987 | 광명단 | 목재전용도료 오일스테인 | 광명단조합페인트 `MT003375`~`MT003377` 있음 | 다른 품목 연결 확실; 후보 있음 | +| GF000220, GF000224 | 잡석(채움재) | 모래 강모래 | 자연자갈·잡석 `MT003848`, 깬잡석 `MT004113` 등 있음 | 다른 품목 연결 확실; 후보 있음 | +| GC000181 | P.P로프 9㎜ | 와이어로프(철심) 9㎜ | P.P로프는 `MT007584` 등 6줄 있으나 9㎜ 규격은 검색 없음 | 다른 품목 연결 확실; 정확 규격 후보는 확인 못 함 | +| GC000348 | 일체식 방수시트 | 방수석고보드 12.5t | ARS아크릴방수시트 `MT006814`·`MT006815` 있음(일체식과 동등성 미확인) | 다른 품목 연결 확실; 후보 동등성 의심 | + +## 3차 산림 소요량 일대일 기계 대조의 범위 + +산림 소요량 중 `출처`가 같은 표가 단 하나이고, 해당 원문 절에 Markdown 표 블록도 하나인 290개 표를 따로 비교했다. 원문 몸줄 수와 자료 줄 수가 같은 경우는 **163표**였다. 그중 같은 순번의 줄마다 숫자 집합까지 같은 것은 **86표**였고, 나머지 **77표**는 숫자 차이가 보여 사람 확인 필요로 남겼다. 나머지 **127표**는 원문 가로·세로 피벗, 병합 셀, 주석·기준표 분리로 원문 물리 줄 수와 JSON 저장 줄 수가 달라 이 방법으로는 합격·불합격을 판정할 수 없다. 숫자 집합 일치는 조건 문자열·비목 일치의 증거가 아니므로 86표도 최종 완료 수로 사용하지 않는다. 전체 산림 소요량·계수 634표 중 위 방법 밖의 표 역시 미완료다. + +## 3차 장별 대조 · 산림품셈 6장 누락 후보 3절 + +| 원문 절 | 분류 | 이유와 근거 한 줄 | +|---|---|---| +| 6-4-3 | 누락 확정 | 소금처리와 제거지점 표시의 `인/100본` 인력 수량이 `QF000167` 5줄에 있으나 로직키가 비어 있다. | +| 6-4-4 | 누락 확정 | 뿌리 고살·덩굴 제거 등의 `인/100본` 수량은 `QF000168`에 있으나 로직키가 비어 있다. 같은 절의 `QF000169` 피복도·본수 환산표는 단독 로직이 필요 없는 보조 기준이다. | +| 6-7-1 | 누락 확정 | 교목 시비의 근원직경별 특별인부·보통인부 수량은 `QF000173` 10줄에 있으나 로직키가 비어 있다. | + +6장 후보 세 절은 모두 직접 소요인력이 있어 누락 확정으로 분류했다. 장 전체의 표 행·비목 대조는 아직 미완료다. + +## 3차 장별 대조 · 산림품셈 7장 누락 후보 7절 + +| 원문 절 | 분류 | 이유와 근거 한 줄 | +|---|---|---| +| 7-1-1 | 누락 확정 | 수확 인력집재의 거리·경사 조건별 작업량 표가 있으나 산림 로직은 이웃 절 `7-1-2`부터 있다. | +| 7-5-2 | 누락 확정 | 숲가꾸기·병해충방제 트랙터 집재의 거리·본당재적별 작업량 및 3인 1조 구성이 있으나 로직 없음. | +| 7-7-2 | 누락 확정 | 숲가꾸기·병해충방제 HAM200 집재의 집재재적·거리별 작업량과 3인 1조 구성이 있으나 로직 없음. | +| 7-8-1 | 누락 확정 | 타워야더 가선설치의 집재방식·가선길이별 소요인력이 있으나 로직 없음. `GF000068` 비고도 `가선설치 · 해체(7-8-1 · 7-8-2) 따로`라고 명시. | +| 7-8-2 | 누락 확정 | 타워야더 가선해체의 집재방식·가선길이별 소요인력이 있으나 로직 없음. 상위 집재 로직에 포함되지 않음. | +| 7-9-1 | 누락 확정 | 수확 스윙야더 집재의 집재재적·거리별 작업량 및 4인 1조 구성이 있으나 로직 없음. | +| 7-9-2 | 누락 확정 | 숲가꾸기·소나무재선충병방제 스윙야더 집재의 집재재적·거리별 작업량 및 4인 1조 구성이 있으나 로직 없음. | + +7장 후보 7절은 모두 직접 작업량·인력 구성이 있어 로직 누락 확정으로 분류했다. 장 전체 표의 규격·조건 줄 및 비목 대조는 아직 미완료다. + +## 3차 장별 대조 · 산림품셈 8장 누락 후보 9절 + +| 원문 절 | 분류 | 이유와 근거 한 줄 | +|---|---|---| +| 8-1-1 | 누락 확정 | 약제주입기 천공수별 `소요인력(ha당)`이 있으나 산림 로직 없음. | +| 8-1-2 | 누락 확정 | 약제주입병 천공수별 `소요인력`이 있으나 산림 로직 없음. | +| 8-2-1 | 참고·계수 표라 로직 불필요 | 소나무재선충병의 가슴높이지름별 천공수·천공당/본당 주입량 기준표이며 독립 시공 품은 아님. 약제 수량 산정에 쓰일 수 있으므로 표 보존은 필요. | +| 8-2-2 | 참고·계수 표라 로직 불필요 | 솔잎혹파리의 선정약제·원액 주입량 기준표. 독립 노무·장비 수량 표 아님. | +| 8-2-3 | 참고·계수 표라 로직 불필요 | 솔껍질깍지벌레의 선정약제·원액 주입량 기준표. 독립 노무·장비 수량 표 아님. | +| 8-2-4 | 참고·계수 표라 로직 불필요 | 솔나방의 선정약제·원액 주입량과 직경별 주입량 기준표. | +| 8-2-5 | 참고·계수 표라 로직 불필요 | 푸사리움가지마름병의 선정약제·원액 주입량과 직경별 주입량 기준표. | +| 8-6-2 | 누락 확정 | 드론방제의 무인헬기 `ha당` 인력 편성·소요량이 있으나 로직 없음(`8-6-1` 유인헬기만 있음). | +| 8-6-3 | 누락 확정 | 지상방제의 차량살포 인력 편성과 물량 조건이 있으나 로직 없음. | + +8장 후보 9절은 누락 확정 4, 약제량 기준표 5로 분류했다. 약제량 표를 다른 로직의 재료 수량에 연결해야 하는지 여부는 별도 검증 대상이며, 8장 전체 행·비목 대조는 미완료다. + +## 3차 장별 대조 · 산림품셈 9장 누락 후보 25절 + +9장 후보 목록의 각 원문번호에 대해 산림 로직의 정확한 `원문번호`와 인접 절 로직을 확인했다. 9-4-2·9-5-3·9-7-1·9-8-1·9-12-1·9-13-1~6·9-16-1~2·9-19-1~2 등 **이웃 절**은 구현됐으나 아래 절을 맡지 않는다. 원문에는 독립 시공량·인력·장비 또는 재료 수량이 있다. + +| 원문 절 | 분류 | 근거 요약 | +|---|---|---| +| 9-4-1 | 누락 확정 | 암파쇄 브레이커 작업능력·치즐소모량 표. | +| 9-5-1 | 누락 확정 | 육상 암석 절취 발파 10%의 폭약·화약공 수량 표. | +| 9-5-2 | 누락 확정 | 암석 절취 깎기 90%의 암종별 작업능력·치즐소모량 표. | +| 9-6-1 | 누락 확정 | 기계소할 15%의 브레이커 작업능력·치즐소모량 표. | +| 9-7-2 | 누락 확정 | 무근콘크리트 깨기 T=30㎝ 이상의 작업능력·치즐·보통인부 표. | +| 9-8-2 | 누락 확정 | 철근콘크리트 깨기 T=30㎝ 이상의 작업능력·치즐·보통인부 표. | +| 9-10-1 | 누락 확정 | 콘크리트 깨기 작업능력·치즐·보통인부 표. | +| 9-10-2 | 누락 확정 | 아스팔트 깨기 작업능력·치즐·보통인부 표. | +| 9-11-1 | 누락 확정 | 콘크리트 커트 인력·장비·절단깊이별 수량 표. | +| 9-12-2 | 누락 확정 | 측구터파기 암절취의 할석공·브레이커 적용량 표. | +| 9-12-3 | 누락 확정 | 측구터파기 발파암의 할석공·브레이커 적용량 표. | +| 9-13-7 | 누락 확정 | 구조물터파기 육상 암절취 0~1m, 할석공·브레이커 적용량. | +| 9-13-8 | 누락 확정 | 구조물터파기 육상 암절취 1~2m, 할석공·브레이커 적용량. | +| 9-13-9 | 누락 확정 | 구조물터파기 육상 암절취 2~3m, 할석공·브레이커 적용량. | +| 9-13-10 | 누락 확정 | 구조물터파기 용수 암절취 0~1m, 인력·브레이커 적용량. | +| 9-13-11 | 누락 확정 | 구조물터파기 용수 암절취 1~2m, 인력·브레이커 적용량. | +| 9-13-12 | 누락 확정 | 구조물터파기 용수 암절취 2~3m, 인력·브레이커 적용량. | +| 9-13-13 | 누락 확정 | 구조물터파기 육상 발파암 0~1m, 할석공·브레이커 적용량. | +| 9-13-14 | 누락 확정 | 구조물터파기 육상 발파암 1~2m, 할석공·브레이커 적용량. | +| 9-13-15 | 누락 확정 | 구조물터파기 육상 발파암 2~3m, 할석공·브레이커 적용량. | +| 9-13-16 | 누락 확정 | 구조물터파기 용수 발파암 0~1m, 할석공·브레이커 적용량. | +| 9-13-17 | 누락 확정 | 구조물터파기 용수 발파암 1~2m, 할석공·브레이커 적용량. | +| 9-13-18 | 누락 확정 | 구조물터파기 용수 발파암 2~3m, 할석공·브레이커 적용량. | +| 9-16-3 | 누락 확정 | 살수의 흡입준비·운반 등 작업시간 기준; `9-16-1~2` 노체포설·다짐 로직과 별개. | +| 9-19-3 | 누락 확정 | 비탈면 고르기 발파암 보통인부·공기압축기 수량; `9-19-1~2` 로직과 별개. | + +이 25절은 소요량·기준 자료만 있고 해당 단가 로직이 없어 누락 확정으로 분류했다. 다만 장 전체 표의 **조건별 수치 배치**는 아직 미완료다. + +## 3차 장별 대조 · 산림품셈 10장 누락 후보 8절 + +| 원문 절 | 분류 | 이유와 근거 한 줄 | +|---|---|---| +| 10-6-1 | 누락 확정 | 토사·석재의 운반거리별 보통인부 `인/㎥당` 표가 있으나 로직 없음. | +| 10-6-2 | 누락 확정 | 돌 지게운반 거리별 보통인부 표가 있으나 로직 없음. | +| 10-6-3 | 누락 확정 | 기타 임업자재 운반거리·자재종류별 보통인부 표가 있으나 로직 없음. | +| 10-8-1 | 누락 확정 | 케이블 크레인 짐내리기의 운반기구별 인부·신호수 수량이 있으나 로직 없음. `10-8-2·3`은 별도 설치·운전 로직. | +| 10-9-1 | 다른 로직이 일부 맡음 / 누락 확정 | 상위 `GF000145(10-9)`는 시간당 기계경비만 계산한다. 비고에 `10-9-1 운반량 쓰는 식 요소 없음`을 명시하므로 자재별 1회 운반량을 반영한 단위 운반비 계산은 빠졌다. | +| 10-12-1 | 누락 확정 | 덤프운반 토사의 굴착기 적재 `Q1=3600×q0×K×f×E0/Cm`와 운반 조건이 있으나 해당 로직 없음. | +| 10-13-1 | 누락 확정 | 드론운반 수평거리 100~600m별 인원 수량이 있으나 로직 없음. | +| 10-13-2 | 누락 확정 | 인력운반 거리별 인원 수량과 드론운반 시간·본수 기준이 있으나 로직 없음. | + +10장 후보는 8절 모두 직접 단가 또는 단위 운반비 산출에 필요한 데이터가 있으나 해당 처리가 없다. `10-9-1`은 상위 로직이 **시간당 경비만** 맡는 일부 구현으로 구분한다. 장 전체의 행별 수치·비목 전수 대조는 미완료다. + +## 3차 장별 대조 · 산림품셈 13장 누락 후보 6절 + +| 원문 절 | 분류 | 이유와 근거 한 줄 | +|---|---|---| +| 13-4-6 | 누락 확정 | 밑돌·천단 돌쌓기 뒷길이별 석공 `0.05·0.08…인` 표가 있으나 `13-4-1·2·4·5` 이웃 로직만 있다. | +| 13-10-2 | 누락 확정 | 나무 말뚝박기 말구·길이별 보통인부 수량이 있으나 `13-10-1` 말뚝 다듬기 로직만 있다. | +| 13-11-1 | 누락 확정 | 원형 돌망태 지름별 조약돌량·작업 수량 표가 있으나 로직은 `13-11-3·4` 다른 형태만 있다. | +| 13-11-2 | 누락 확정 | 타원형 돌망태 지름별 조약돌량·작업 수량 표가 있으나 로직은 `13-11-3·4` 다른 형태만 있다. | +| 13-15-1 | 누락 확정 | 목재 껍질벗기기 말구별 보통인부 수량이 있으나 로직 없음. | +| 13-15-2 | 참고·계수 표라 로직 불필요 | 목책 설치의 `어려운/쉬운` 작업조건(벌도목 직경·가지량·경사도) 구분표는 직접 인력·재료 수량이 아닌 적용조건 기준. 다만 목책 설치 자체의 다른 품이나 로직이 있는지는 추가 확인 필요. | + +13장 후보는 누락 확정 5절, 직접 단가 수량이 아닌 조건 기준표 1절로 분류했다. 장 전체의 행별 수치·비목 대조는 미완료다. + +### 산림 누락 후보 81절 분류 합계 + +독립 단가 로직 누락으로 분류한 절 **69개**(4장 3, 5장 14, 6장 3, 7장 7, 8장 4, 9장 25, 10장 8, 13장 5). 독립 로직이 불필요한 참고·계수·조건 기준표 **8개**(5-1-5, 8-2-1~5, 12-38-1, 13-15-2). 다른 로직이 **일부만 맡아 재료·운반량이 빠진 절 1개**(12-38-2, `GF000210`). 하위 공종의 조합으로 처리할지 **사람 확인 필요 3개**(5-16-2, 5-17-1·2). 분류의 근거는 위 장별 절 목록에 적었다. 이는 **로직 존재 여부의 분류**이지 각 절의 표 줄·값·비목 전수 검증 완료를 뜻하지 않는다. + +## 3차 건설 공통 · 원문 절 누락 후보 21개 분류 + +| 원문 절 | 분류 | 근거 요약 | +|---|---|---| +| 1-3-1 | 참고·계수 표라 로직 불필요 | 시멘트 등 재료의 할증률 표; 독립 공종의 인력·재료량이 아님. | +| 1-4-3 | 참고·계수 표라 로직 불필요 | 작업지연 조건별 품 할증률. | +| 1-4-4 | 참고·계수 표라 로직 불필요 | 지세·지형 등급별 품 할증률. | +| 1-4-5 | 참고·계수 표라 로직 불필요 | 위험 조건별 품 할증률. | +| 1-4-6 | 참고·계수 표라 로직 불필요 | 작업시간 제한 조건별 품 할증률. | +| 1-4-7 | 참고·계수 표라 로직 불필요 | 야간 등 작업환경별 품 할증률. | +| 2-1-1 | 참고·계수 표라 로직 불필요 | 직접노무비에 따른 토목 현장사무소·창고·숙소 규모 기준. 면적만으로 별도 단가가 나오지 않음. | +| 2-1-2 | 참고·계수 표라 로직 불필요 | 건축·기계설비 현장사무소·창고 규모 기준. | +| 2-2-2 | 참고·계수 표라 로직 불필요 | 주요 가설자재 사용기간별 손율. | +| 2-2-4 | 참고·계수 표라 로직 불필요 | 구조물 동바리 기간별 손율. | +| 6-2-1 | 참고·계수 표라 로직 불필요 | 철근가공·조립의 Type-Ⅰ 등 적용범위 설명표. | +| 7-4-2 | 누락 확정 | 석재판 붙임 앵커 긴결공법의 인력 수량·일당 시공량 표 `QC000495`가 있으나 로직 없음. | +| 7-4-3 | 누락 확정 | 석재판 붙임 강제트러스 지지공법의 인력 수량·일당 시공량 표 `QC000496`이 있으나 로직 없음. | +| 8-2-1 | 참고·계수 표라 로직 불필요 | 불도저 삽날용량·작업효율·속도 등 기계 생산성 산정계수. 독립 공종 아님. | +| 8-2-3 | 참고·계수 표라 로직 불필요 | 굴착기 버킷계수·작업효율·싸이클시간 등 생산성 산정계수. | +| 8-2-5 | 참고·계수 표라 로직 불필요 | 로더 적재시간·버킷계수·작업효율 등 생산성 산정계수. | +| 8-2-8 | 참고·계수 표라 로직 불필요 | 덤프트럭 운반속도·적하시간 등 생산성 산정계수. | +| 8-2-9 | 참고·계수 표라 로직 불필요 | 롤러 적합도·다짐폭·속도·다짐횟수 등 생산성 산정계수. | +| 8-2-13 | 참고·계수 표라 로직 불필요 | 대형브레이커 작업능력·치즐 소모량 등 생산성 산정계수. | +| 8-2-15 | 참고·계수 표라 로직 불필요 | 법면다짐기 작업능력 2줄의 기계 생산성 기준. | +| 8-2-21 | 참고·계수 표라 로직 불필요 | 경운기 적재·적하시간과 주행속도 기준. | + +공통 후보는 독립 단가 로직 누락 2절, 참고·계수·적용범위 19절이다. 공통 기계 계수표의 **다른 로직에서의 실제 사용 여부**는 별도 연결 점검이 필요하다. 장 전체 원문 표의 행·값·비목 대조는 미완료다. + +## 3차 건설 토목 · 원문 절 누락 후보 13개 분류 + +| 원문 절 | 분류 | 근거 요약 | +|---|---|---| +| 3-1-2 | 참고·계수 표라 로직 불필요 | 터널 아치·측벽·바닥의 여굴두께 설계기준. | +| 3-2-1 | 참고·계수 표라 로직 불필요 | 터널굴착 1발파당 작업별 싸이클 시간 기준. | +| 3-2-2 | 참고·계수 표라 로직 불필요 | 기계굴착의 암종·기계별 시간당 작업능력. | +| 3-2-3 | 참고·계수 표라 로직 불필요 | 천공기계의 암종별 천공속도. | +| 3-2-4 | 참고·계수 표라 로직 불필요 | 터널 천공·버력처리 장비의 조합 기준. | +| 7-1-1 | 참고·계수 표라 로직 불필요 | 수중공사의 케이슨·블록 여유폭 설계기준. | +| 7-1-2 | 참고·계수 표라 로직 불필요 | 피예인선별 예인선 출력 조합 기준. | +| 7-1-3 | 참고·계수 표라 로직 불필요 | 준설선별 부속 선단·기계 조합 기준. | +| 7-1-4 | 참고·계수 표라 로직 불필요 | 준설선별 취업·운전시간 기준. | +| 7-4-4 | 참고·계수 표라 로직 불필요 | 준설 토질·수심별 여굴 두께 설계기준. | +| 7-4-5 | 참고·계수 표라 로직 불필요 | 펌프준설 매립시 토질별 유보율. | +| 7-4-6 | 참고·계수 표라 로직 불필요 | 펌프준설 매립시 입경별 유실률. | +| 9-5-8 | 참고·계수 표라 로직 불필요 | GPS 측량기 등 상각·정비·관리비율 및 계수 기준. | + +토목 후보 13절은 모두 단독 공종 수량이 아닌 설계·생산성·손율 계수표로 분류했다. 다만 이를 사용하는 로직의 존재·적용 정확성은 별도 문제이고, 토목 전체 원문 표의 행·값·비목 대조는 미완료다. + +## 3차 건설 기계설비 · 원문 절 누락 후보 2개 분류 + +| 원문 절 | 분류 | 근거 요약 | +|---|---|---| +| 13-4-1 | 참고·계수 표라 로직 불필요 | 보통 철골재의 구조별 개산 무게와 주재 대비 부속재 비율 표. 직접 설치 인력·재료 투입량 표 아님. | +| 13-11-4 | 참고·계수 표라 로직 불필요 | IRON WIRE ROPE 등 가설자재의 월별 손료율 표. 개별 단가 산출의 보조 계수. | + +기계설비 후보 2절은 독립 단가 로직 불필요로 분류했다. 272개 기계설비 로직과 509개 소요량·계수 표의 원문 조건·값·비목 전수 대조는 미완료다. + +## 3차 건설 유지관리 · 원문 절 누락 후보 36개 분류 + +| 원문 절 | 분류 | 근거 요약 | +|---|---|---| +| 3-1-1 | 누락 확정 | 콘크리트구조물 인력 헐기 직종·시공량. | +| 3-1-2 | 누락 확정 | 콘크리트구조물 기계 헐기 인력·장비·시공량. | +| 3-1-3 | 누락 확정 | 철골재 인력 철거 직종·시공량. | +| 3-1-4 | 누락 확정 | 철골재 기계 철거 직종·장비·시공량. | +| 3-1-5 | 누락 확정 | 석축 인력 헐기 뒷길이별 할석공·보통인부 수량. | +| 3-2-1 | 누락 확정 | 금속기와 해체 지붕잇기공 수량. | +| 3-2-2 | 누락 확정 | 흡음텍스 해체 내장공 수량. | +| 3-2-3 | 누락 확정 | 경량천장철골틀 해체 내장공 수량. | +| 3-2-4 | 누락 확정 | 조적벽 해체 조적공 수량. | +| 3-2-5 | 누락 확정 | 경량벽체철골틀 해체 내장공 수량. | +| 3-2-6 | 누락 확정 | 석고판 해체 벽·천장별 내장공 수량. | +| 3-2-7 | 누락 확정 | 도배 해체 벽·천장별 도배공 수량. | +| 3-2-8 | 누락 확정 | PVC계바닥재 해체 내장공 수량. | +| 3-2-9 | 누락 확정 | 타일 해체 붙임방식별 타일공 수량. | +| 3-2-10 | 누락 확정 | 기존방수층·보호층 철거 착암공 등 수량. | +| 3-2-11 | 누락 확정 | 기존방수층 제거·바탕처리 방수공 수량. | +| 3-2-12 | 누락 확정 | 석면건축자재 해체 내·외장재별 석면해체공 수량. | +| 3-3-1 | 누락 확정 | 콘크리트·모르타르면 재도장 바탕처리 도장공·시공량. | +| 3-3-2 | 누락 확정 | 철재면 재도장 바탕처리 A·B급별 도장공·시공량. | +| 3-3-3 | 누락 확정 | 목재면 재도장 바탕처리 A·B급별 도장공·시공량. | +| 3-4-1 | 누락 확정 | 지붕 덧씌우기 지붕잇기공·시공량. | +| 3-4-2 | 누락 확정 | 지붕 재설치 지붕잇기공·시공량. | +| 3-4-3 | 누락 확정 | 도배 교체 벽·천장별 인력·시공량. | +| 3-4-4 | 누락 확정 | PVC계바닥재 교체 내장공·시공량. | +| 3-4-5 | 누락 확정 | 타일 교체 붙임방식별 인력·시공량. | +| 4-1-1 | 누락 확정 | 배관 해체 규격·재질별 배관공·보통인부 수량. | +| 4-1-2 | 누락 확정 | 각형덕트 해체 두께별 인력 수량. | +| 4-1-3 | 누락 확정 | 스파이럴덕트 해체 두께·규격별 덕트공·보통인부 수량. | +| 4-1-4 | 누락 확정 | 배관보온 해체 재질·규격별 보온공·보통인부 수량. | +| 4-1-5 | 누락 확정 | 덕트보온 해체 재질별 보온공 수량. | +| 4-1-6 | 누락 확정 | 펌프 해체 출력별 기계설비공·보통인부 수량. | +| 4-1-7 | 참고·계수 표라 로직 불필요 | 일반기계설비 철거·이설의 설치품 대비 적용률 표; 독립 직종·장비 수량 아님. 적용 계산 여부는 별도 점검 필요. | +| 4-2-1 | 참고·계수 표라 로직 불필요 | 자동제어설비 철거·이설의 설치품 대비 적용률 표; 독립 직종·장비 수량 아님. 적용 계산 여부는 별도 점검 필요. | +| 4-3-1 | 누락 확정 | 유량계 교체 인력·규격별 시공량. | +| 4-3-2 | 누락 확정 | 관갱생 규격별 규사·에폭시도료·배관공·장비시간. | +| 4-3-3 | 누락 확정 | 배관누수 검사 배관공·시공량. | + +유지관리 후보 36절 중 독립 단가 로직 누락 **34절**, 설치품을 조정하는 기준표 **2절**로 분류했다. 특히 3·4장의 철거·해체·보수 공종은 다수의 인력 수량이 표로 저장됐지만 로직이 없다. 유지관리 197표 전체의 조건·숫자 자리·비목 대조는 미완료다. + +### 153개 후보 분류 합계 + +산림 81절: 직접 누락 69, 기준표 8, 상위 로직 일부 구현·재료 누락 1, 사람 확인 3. 건설 공통 21절: 직접 누락 2, 기준표 19. 토목 13절: 기준표 13. 건축 0절. 기계설비 2절: 기준표 2. 유지관리 36절: 직접 누락 34, 기준표 2. **총 153절 중 직접 누락 105, 기준표 44, 일부 구현·재료 누락 1, 사람 확인 3.** 이 숫자는 원문 절과 자료 연결을 대조한 분류이며, 원문의 모든 표 행 및 모든 로직의 단위·비목까지 통과했다는 뜻은 아니다. + +확인 보강: 위 직접 누락으로 분류한 절의 소요량·계수 표 키가 1,354개 로직의 `찾기(...)` 식에 간접적으로라도 등장하는지 재검색했으며, **참조된 표 키 0개**였다. 이는 표의 `용도.로직키=[]`뿐 아니라 다른 원문번호의 로직이 표를 가져가는 우회 경로도 찾지 못했다는 근거다. 값이 별도 표 없이 수식에 하드코딩됐을 가능성까지 기계적으로 배제하지는 못한다. + +## 3차 전 부문 표 대응 기계 선별 + +원문 `출처`가 한 표에만 대응하는 경우, 원문 Markdown 표 블록과 JSON 표를 1:1로 잡아 **물리적 줄 수**와 같은 순번 줄의 **숫자 집합**을 비교했다. 아래의 숫자 일치도 문자열 조건·단위·비목까지 일치했다는 뜻은 아니다. 원문 가로 표를 JSON의 세로 줄로 푼 경우에는 물리적 줄 수 불일치가 정상일 수 있다. 같은 출처에 여러 JSON 표가 붙거나 원문에 여러 표 블록이 있는 경우에는 이 단순 비교로 판정하지 않았다. + +| 부문 | 전체 표 | 출처 공유(1:1 불가) | 원문 복수 블록(1:1 불가) | 물리 줄 수 다름(피벗 가능) | 줄 수 같음·숫자 전 줄 일치 | 줄 수 같음·숫자 차이(사람 확인) | +|---|---:|---:|---:|---:|---:|---:| +| 산림 | 634 | 137 | 111 | 184 | 87 | 115 | +| 건설 공통 | 575 | 142 | 224 | 149 | 32 | 27 | +| 토목 | 765 | 370 | 195 | 164 | 18 | 18 | +| 건축 | 353 | 336 | 1 | 9 | 3 | 4 | +| 기계설비 | 509 | 292 | 29 | 132 | 40 | 16 | +| 유지관리 | 197 | 103 | 1 | 82 | 1 | 10 | +| 합계 | 3,033 | 1,380 | 561 | 720 | 181 | 190 | + +공통의 1표는 `출처`가 비교 대상 품셈 본문 문자열이 아니어서 이 표 밖에 남았다. 숫자 차이 **190표**는 원문 머리의 숫자·설명 속 수치, JSON 조건 범위의 재표현이 흔하므로 곧바로 오류로 판정하지 않았다. 위 기계 선별은 3,033표를 **검토 대기 갈래로 나눈 것**이며, 25,036줄의 규격·조건·값 검증을 완료한 것으로 계산하지 않는다. + +## 3차 비목 대조 + +1,354개 로직의 호표를 기계적으로 세면 인력 3,255줄은 모두 `노무비`, 재료 675줄은 모두 `재료비`, 하위 로직 990줄은 비목 고정 없이 하위 결과를 나눠 받는다. **종류와 비목의 직접 불일치는 0줄**이다. 그러나 원문에 노무비·재료비·경비를 한꺼번에 적은 덧줄을 어떤 비목으로 나눌지는 이 검사로 확정되지 않는다. + +| 로직 키 | 이름 | 원문 자리(파일 · 절) | 갈래 | 무엇이 다름 | 판정 | 근거 인용 한 줄 | +|---|---|---|---|---|---|---| +| GC000251, GC000703~GC000707, GC000709~GC000719, GC000823~GC000824 (19키) | 플륨관 및 도장공사의 공구손료·잡재료비 등 | 건설품셈 공통 6-7-1; 건축 11-1-1~11-3-1; 기계설비 9-2-2·3 | 비목 | `공구손료 및 잡재료비` 또는 `공구손료 및 소모재료`라는 결합 덧줄을 전액 `경비`에 계상한다. 원문의 결합률을 공구 경비와 재료비로 분리할 근거가 있는지 확인 필요; 현재 자료만으로 오분류 확정 불가. | 의심 | 예: `GC000703` 덧줄 이름 `공구손료 및 잡재료비`, 비목 `경비`; `_틀.md`는 `잡재료비` 예시를 `재료비`로 둠. | + +원문이 비목을 별도로 지정하지 않는 품까지 3,255+675줄 전체의 원문 비목 일치가 입증된 것은 아니다. 특히 위 19줄은 비목의 해석이 필요하므로 확정 흠 수에 넣지 않았다. + +## 3차 임도 우선 표 묶음 · 산림 9-13-7~18 + +구조물터파기 암절취·발파암 12절의 `QF000276`~`QF000287`을 원문 표 본문 줄과 맞댔다. 각 표의 원문 몸줄 수와 JSON 줄 수가 각각 8·5·5·7·4·4·8·5·5·8·5·5로 같으며, 같은 순번 줄의 `적용` 숫자는 **12표 전부 원문 해당 줄에 있다**. `QF000276`은 할석공 1.6, 보통인부 0.8, 브레이커 3.5㎥/hr, 치즐 0.006본/hr, 백호우 k=0.55·f=0.74·E=0.50·Cm=20초를 원문과 대조했다. 이 묶음에서 수치 자리 밀림은 발견하지 못했다. 다만 병합된 `인력(10%)`·`장비(90%)` 조건과 단위 전체, 비목 연결을 12표 모두 수동 검증한 것은 아니므로 최종 상태는 **사람 확인**으로 유지한다. + +같은 임도 우선 범위 `9-4`~`9-12`의 산림 소요량 표 20개도 선별했다. 원문 표 블록 하나와 JSON 표 하나로 잡히는 **14표**(`QF000250`~`QF000255`, `QF000258`, `QF000261`~`QF000264`, `QF000267`~`QF000269`)는 원문 몸줄 수와 JSON 줄 수가 같고, 같은 순번 줄의 `적용` 또는 `수량` 숫자를 원문 해당 줄에서 찾았다. 나머지 **6표**(`QF000256`·`QF000257`·`QF000259`·`QF000260`·`QF000265`·`QF000266`)는 한 출처 안에 원문 표 블록이 둘이라 표 블록별 대응을 별도로 만들어야 한다. 14표도 조건·단위·비목까지 끝난 것은 아니므로 **사람 확인**, 6표는 **미판정**으로 둔다. + +## 3차 임도 우선 표 묶음 · 산림 10장 + +10장의 소요량 표 42개를 원문 표 블록·저장 줄 수로 선별했다. `10-4 중기운반`의 `QF000307`은 원문 몸 16줄, JSON 15줄이며 마지막 **자주식 운반 `크레인(트럭 10TON)`** 줄이 없다. 앞의 15줄은 운반수단·항목 순서가 원문과 같다. 원문 표에는 같은 묶음의 `물탱크(5500L)`, `콘크리트 믹서(6.0㎥)`, `크레인(트럭 10TON)` 세 장비가 있으나 JSON은 앞의 두 장비까지만 담았다. + +| 로직 키 | 이름 | 원문 자리(파일 · 절) | 갈래 | 무엇이 다름 | 판정 | 근거 인용 한 줄 | +|---|---|---|---|---|---|---| +| GF000137 / QF000307 | 중기운반 트레일러(20ton) / 중기운반 기준표 | 산림_표준품셈/본문/제10장_자재․장비_운반/10-04_중기운반.md · 10-4 | 누락 | 기준표의 자주식 운반 크레인(트럭 10TON) 한 줄이 자료에 없다. `GF000137`은 트레일러만 계산해 직접 영향은 미확인이나, 같은 표를 재사용하는 자주식 운반 계산에는 조건이 빠진다. | 확실 | 원문 `| | | 크레인(트럭 10TON) | |`; `QF000307` 마지막 줄은 `콘크리트 믹서(6.0㎥)`이고 총 15줄. | + +`10-6-1·2·3` 인력운반 표 `QF000308`·`QF000309`·`QF000310`은 원문 가로배치·양쪽배치를 JSON 세로 줄로 펼친 규칙까지 확인했다. 토사·석재 20~90m는 원문 4행×좌우 2조 = JSON 8줄, 돌 지게운반 50~1,000m는 11거리 = 11줄, 기타 임업자재 20~200m는 10거리×10자재 수량 = JSON 10줄의 10값칸이다. 각 거리·자재 칸의 모든 숫자를 원문과 자료 순서대로 비교해 일치했고, `값칸` 단위 `인/㎥`, `인/100속`, `인/100매`, `인/100개`, `인/100본`, `인/1000㎡`, `인/톤`도 원문 머리와 같다. 이 세 표의 규격·조건·값·단위 대조는 **끝남, 흠 0**으로 기록한다. 단가 로직 자체는 앞의 누락 절 분류대로 없다. + +`10-9-1 자재의 1회당 표준 운반량`의 `QF000329`도 원문 7자재×5트럭 규격 = **35칸**, JSON 35줄을 대조했다. 각 칸의 중량(kg), 환산량(개·㎡·㎥), 통나무 본수 숫자를 35칸 전부 순서대로 대조해 차이가 없고, 저장 단위도 원문 괄호 속 단위와 맞는다. 이 표의 조건·값·단위 대조는 **끝남, 흠 0**이다. 그러나 `GF000145`가 표를 읽어 운반비를 계산하지 않는 문제는 앞의 **10-9-1 로직 일부 구현** 판정 그대로 남는다. + +`10-13-1·2` 드론·인력운반의 `QF000340`·`QF000341`은 거리 100~600m의 원문 6칸을 각각 JSON 6줄로 펼친 값이 전부 같다. `QF000342`는 준비시간 60분, 왕복비행시간 `0.0098×수평거리+1.3264`, 1회 운반 100본, 드론운반 작업인원 3명의 원문 4항목과 내용·수치가 같다. 세 표의 조건·값·단위 대조는 **끝남, 흠 0**이다. 산림 10-13-1·2의 **단가 로직 누락** 판정은 별개로 유지한다. `QF000343`의 산출 기준 수 5줄은 부모 10-13절의 계산 설명과 별도 대조가 필요하다. diff --git a/docs/raw/M01_점검/보고_줄글.md b/docs/raw/M01_점검/보고_줄글.md new file mode 100644 index 00000000..9e780499 --- /dev/null +++ b/docs/raw/M01_점검/보고_줄글.md @@ -0,0 +1,437 @@ +# M01 점검 결과 · 줄글 + +보고서 표에서 흠과 미판정 항목을 한 줄씩 옮겼다. 같은 흠이 1차·2차·자재품목 대조에 반복되면 근거 보강을 보존했다. 원문 절 누락 분류의 기준표 항목은 흠이 아니므로 제외했다. + +GC000181 | 매트부설(육상) | 연결 | P.P로프(9㎜) 재료 검색어가 `와이어로프(철심) 9㎜`로, 재질과 품명이 다르다. | 확실 + +GC000027 | 강관 조립말비계(이동식) 설치 및 해체 | 연결 | `수평띠장 L1829`가 안전발판을 검색한다. | 확실 + +GC000348 | 터널 방수 | 연결 | 일체식 방수시트가 방수석고보드를 검색한다. | 확실 + +GC000360 | 레일 가스압접 | 연결 | 바퀴숫돌 단면용의 가격 검색어가 도로청소용 빗자루다. 같은 방식으로 다른 바퀴숫돌 규격도 연결되어 있다. | 확실 + +GC000560 | 철골 공장용접(기본용접공수) | 연결 | CO₂ 와이어·탄산가스·잠호용접와이어의 가격 검색어가 모두 아세틸렌이다. | 확실 + +GC000575 | 벽돌 쌓기(시멘트벽돌) | 연결 | 시멘트벽돌 가격 검색어가 주차라인블록이다. | 확실 + +GC000604 | 카페트 설치 | 연결 | 카페트 가격 검색어가 불연천장재(마이톤)다. | 확실 + +GC000723 | 동관 용접접합 | 연결 | 동 및 동합금용 용접봉과 플럭스가 둘 다 아세틸렌을 검색한다. | 확실 + +GC000938 | Stop-Log 설치 | 연결·단위 | `광명단` 재료가 `목재전용도료 오일스테인`을 검색한다. 아세틸렌 원문은 `병` 수량 1.98인데 호표는 `kg` 1.98로 환산 없이 사용한다. | 확실 + +GC000943 | Roller Gate Guide Metal 제작 | 연결·단위 | `Tar Epoxy` 호표에 원문의 전기 `550㎾h`를 그대로 넣고 `Tar Epoxy` 원문 `0.75ℓ`를 누락했다. 아세틸렌도 원문 `1.6병`을 `1.6kg`으로 사용한다. | 확실 + +GC000948 | Tainter Gate Anchorage 제관 | 연결·단위 | `Tar Epoxy` 호표에 전력 `420㎾h`를 넣고 원문의 `3.0ℓ`를 누락했다. 아세틸렌 원문 `1.5병`도 `1.5kg`으로 사용한다. | 확실 + +GC000993 | 플랜트 공사별 설치 소모자재(참고) | 연결 | 용접봉(전기·산소) 모두 아세틸렌을, 사포는 복사용지를, 걸레는 그리스를, 용접면·용접홀다·용접앞치마는 노면파쇄기 팁을, 페인트붓은 도로청소용 빗자루를 검색한다. 원문 품명과 재료 가격 연결이 다르다. | 확실 + +GC000027 | 강관 조립말비계(이동식) 설치 및 해체 | 단위 | 가새 L1518-2개 `조` 후보 0 | 의심 + +GC000181 | 매트부설(육상) | 단위 | 철근(19㎜) `m` 후보 0 | 의심 + +GC000616 | 걸레받이 설치 | 단위 | 중밀도섬유판 걸레받이 `m` 후보 0 | 의심 + +GC000211 | 합판거푸집 설치 및 해체 | 단위 | 폼타이 12.7·9.5·7.9㎜의 `본` 후보 각각 0 (3곳) | 의심 + +GC000855 | 가스용 강관 나사식 접합 및 배관 | 단위 | 스레트실테이프 `㎝` 후보 0 | 의심 + +GC000859 | 직독식 가스미터 설치 | 단위 | 스레트실테이프 `㎝` 후보 0 | 의심 + +GC000938 | Stop-Log 설치 | 단위 | 함석 `대` 후보와 단위 성격 다름. 원문 역시 `대`; 가격 마스터 문제 가능. | 의심 + +GC000943, GC000948 | 수력발전 게이트 제작 | 단위 | 중유 `D/M` 후보 단위가 다름 (2곳) | 의심 + +GC000948 | Tainter Gate Anchorage 제관 | 단위 | Zinc primer `ℓ` 후보 단위가 다름 | 의심 + +GC000993 | 설치 소모자재(참고) | 단위 | 용접장갑 `족` 후보 0 | 의심 + +GF000160 | 합판거푸집 | 단위 | 폼타이 `본` 후보 0 | 의심 + +GF000165 | 콘크리트 포장 거푸집 | 단위 | 철재 거푸집 손료 `본` 후보 0 | 의심 + +GF000060 | 목재칩 포설 | 연결 | 우드칩 가격 검색어가 미송각재다. | 확실 + +GF000143 | 케이블 크레인 운반대 설치 | 연결 | 소재(원목)가 제재 각재를 검색한다. | 확실 + +GF000161 | 문양거푸집 설치 및 해체 | 연결 | 문양 스티로폴이 그라스울 샌드위치판넬을 검색한다. | 확실 + +GF000163 | 포장절단(1차 절단) | 연결 | 커터 블레이드가 굴착기 치즐을 검색한다. | 확실 + +GF000172 | 맹암거 | 연결 | 부직포가 직포를, 잡석이 강모래를 검색한다. | 확실 + +GF000180 | 집수정 | 연결 | 집수정 뚜껑(스틸그레이팅)이 PE빗물받이 뚜껑을 검색한다. | 확실 + +GF000181 | 맨홀 | 연결 | 원형거푸집 두 줄이 폼타이를 검색한다. | 확실 + +GF000191 | 부직포설치 | 연결 | 부직포가 직포를 검색한다. | 확실 + +GF000198 | 실런트(20×25㎜) | 연결 | 실런트가 수팽창 지수재를 검색한다. | 의심 + +GF000199 | 신구 BOX접합 | 연결 | 콘크리트용 에폭시 접착제는 석고본드를, 시너는 목재전용 오일스테인을 검색한다. | 확실 + +GF000201 | 다웰바 설치 신축 이음부 | 연결·단위 | 강봉 D32가 트러스를 검색하고, PVC 파이프 Ø35㎜는 D30㎜ 제품을 검색한다. | 확실 + +GF000202 | 다웰바 설치 접속스라브부 | 연결·단위 | 원형철근 Ø29는 D25 가공비를, 탄성고무받침은 익스팬션조인트휠러를, 다웰바캡은 보강재를 검색한다. | 확실 + +GF000203 | 물끊기 홈(NOTCH) 설치 | 연결 | 물끊기 홈 재료가 방수석고보드를 검색한다. | 확실 + +GF000220 | 돌쌓기 메쌓기(장비) | 연결 | 채움재 잡석이 강모래를 검색한다. | 확실 + +GF000224 | 돌붙임 메붙임(장비) | 연결 | 채움재 잡석이 강모래를 검색한다. | 확실 + +GF000235 | 사각형 돌망태 | 연결 | 철망태가 기초사석을 검색한다. | 확실 + +GC000001 | 철제조립식 가설건축물 손료 | 연결 | 외부·내부 PANEL, CANOPY, 박공PANEL, 천장판을 T챤넬로, 중도리를 트러스로, T-BAR를 처마후레싱으로 검색한다. | 확실 + +GC000002 | 콘테이너형 가설건축물 손료 | 연결·단위 | 3×3·3×6·3×9m 건축물 셋 모두 12×3×2.6m 콘테이너를 검색한다. 규격 차이를 반영하지 않는다. | 확실 + +GC000031 | 낙하물 방지망(비계) 설치 및 해체 | 연결 | 브라켓을 안전발판으로, 그물망을 옥상방수용 직포로 검색한다. | 확실 + +GC000032 | 낙하물 방지망(플라잉넷) 설치 및 해체 | 연결 | 브라켓을 안전발판으로, 사다리를 강관비계로, 그물망을 직포로 검색한다. | 확실 + +GC000037 | 철골 안전망 설치 및 해체 | 연결 | 그물망을 옥상방수용 직포로 검색한다. | 확실 + +GC000038 | 비계주위 보호망 설치 및 해체 | 연결 | 보호망을 옥상방수용 직포로 검색한다. | 확실 + +GC000039 | 갱폼주위 보호망 설치 및 해체 | 연결 | 보호망을 옥상방수용 직포로 검색한다. | 확실 + +GC000062 | 방진망 설치 및 해체 | 연결 | 방진망을 PVC 천막지로 검색한다. | 확실 + +GC000068 | 건축물보양 | 연결 | 부직포를 직포로, 풀을 그리스로 검색한다. | 확실 + +GC000127 | 버팀목 설치·해체 | 연결·단위 | 각재 10×10㎝가 30×30㎜ 각재를 검색한다. | 확실 + +GC000185 | 연직배수재(PBD) 타설 | 연결 | 드레인 보드를 고무바로 검색한다. | 확실 + +GC000197 | 차수재 부설(1겹) | 연결 | 부직포·지오콤포지트는 옥상방수용 직포로, 벤토나이트매트는 PET 매트로 검색한다. | 확실 + +GC000203 | 에폭시 콘크리트 접착제 바르기 | 연결 | 에폭시 접착제 두 종은 석고본드를, 시너는 목재용 오일스테인을 검색한다. | 확실 + +GC000212 | 강재거푸집 인력 설치 및 해체 | 연결 | 강재거푸집 손료가 K-SPAN 패널 가격을 검색한다. | 확실 + +GC000215 | 문양거푸집(판넬) 설치 및 해체 | 연결 | 문양거푸집 패널이 K-SPAN 패널을 검색한다. | 확실 + +GC000216 | 합성수지(P.E) 원형 맨홀 거푸집 설치 및 해체 | 연결 | 원형맨홀거푸집 다섯 규격 모두 폼타이를 검색한다. | 확실 + +GC000319 | 차선도색 융착식 도료 수동식 | 연결 | 융착식 도료 프라이머가 신구접착·방청제를 검색한다. | 확실 + +GC000337 | 낙석방지망 철망 및 와이어 설치 | 단위 | 결속선 원문은 `m`, 검색어는 철선 #8이다. 가격 후보가 길이 단위인지 확인 필요. | 의심 + +GC000374 | 강교 제작 기타재료 | 연결 | 용접봉(강교 제작)이 아세틸렌을 검색한다. | 확실 + +GC000375 | 강교 소재 표면처리 | 연결 | 무기질아연말 샵프라이머가 목재용 오일스테인을 검색한다. | 확실 + +GC000431, GC000432 | 보링 천공(토사·암반) | 연결 | 코아바렐·크라운비트·파이프·코아리프터 등 원문 소모품 12줄을 굴착기 치즐로 검색한다. | 확실 + +GC000433, GC000434, GC000435 | 표준관입·시료채취·베인전단시험 | 연결 | 샘플러·신월튜브·베인 블레이드가 굴착기 치즐을 검색한다. | 확실 + +GC000442, GC000443, GC000444, GC000445, GC000446 | 대구경 보링 천공 | 연결 | 윙비트가 치즐을, 벤토나이트가 수팽창 지수재를 검색한다. | 확실 + +GC000447, GC000448, GC000449 | 대구경 보링 암반층 천공 | 연결 | 에어해머·버튼비트가 치즐을 검색한다. | 확실 + +GC000565 | 철골 현장용접(CO2 반자동) | 연결 | CO₂ 와이어와 탄산가스가 모두 아세틸렌을 검색한다. | 확실 + +GC000576 | 치장벽돌 공간쌓기 및 치장줄눈 | 연결 | 치장벽돌이 주차라인블록을 검색한다. | 확실 + +GC000604 | 카페트 설치(1차 발견 보강) | 연결 | 카페트 외에 펠트도 옥상방수용 직포를 검색한다. | 확실 + +GC000616 | 걸레받이 설치 | 연결 | 테라조와 합성수지 걸레받이 모두 처마후레싱을 검색한다. | 확실 + +GC000618 | 도배바름(초배 · 정배) | 연결 | 도배용 풀이 그리스를 검색한다. | 확실 + +GC000620, GC000621, GC000622, GC000623, GC000625, GC000626, GC000628 | 단열재 시공 여러 절 | 연결 | 단열재·단열판에 완제품 그라스울 샌드위치판넬 가격을 연결한다. 단열재 종류·두께가 원문 또는 입력에 따라 달라질 수 있어 동등성 확인 필요. | 의심 + +GC000627 | 방습필름 설치 | 연결 | 방습필름이 PVC 천막지를 검색한다. | 확실 + +GC000637, GC000638, GC000639 | 시트방수 붙임 3방식 | 연결 | 방수시트 세 로직 모두 방수석고보드를 검색한다. | 확실 + +GC000646 | 벤토나이트방수 붙임 | 연결 | 벤토나이트방수재를 석고보드로, PE필름을 PVC천막지로, 카트리지를 화약 카트리지로, 와셔를 핀볼트로 검색한다. | 확실 + +GC000655 | 폴리카보네이트 설치 | 연결 | 폴리카보네이트가 불연천장재(마이톤)를 검색한다. | 확실 + +GC000665 | 와이어메시 바닥깔기 | 연결 | 와이어메시가 메쉬스크린을 검색한다. | 확실 + +GC000666 | 인서트 설치 | 연결 | 인서트가 L형앵커볼트를 검색한다. | 확실 + +GC000670 | 앵커고정식 난간 설치(공장제작) | 연결 | AL리벳이 작은나사(스크류)를 검색한다. | 확실 + +GC000724 | 스테인리스 강관 용접접합 | 연결 | TIG 용접봉과 아르곤가스 모두 아세틸렌을 검색한다. | 확실 + +GC000752 | 제연댐퍼 설치 | 연결 | 블라인드리벳을 핀볼트로, 철물을 철근부식방지재로 검색한다. | 확실 + +GC000901 | 플랜트 배관 보온(성형물) | 연결 | 보온 성형물이 STS 단열바 프레임을, Sheet Metal Screw가 빗자루를 검색한다. | 확실 + +GC000931 | 수차 설치(Kaplan · Francis · Propeller) | 연결 | 걸레가 그리스를, 비닐세트·소창직은 PVC 코팅철선을, 시너·광명단·조합페인트는 목재용 오일스테인을 검색한다. | 확실 + +GC000932 | 수력 발전기 설치 | 연결·단위 | 목탄·걸레가 그리스를, 가스용접봉·신주용접봉이 아세틸렌을, 비닐시트가 사포를 검색한다. 산소 원문은 4,500ℓ/병인데 로직은 병 수에 6,000을 곱한다. | 확실 + +GC000933, GC000934 | 수문 제작(Tainter·Roller Gate) | 연결 | 광명단이 목재용 오일스테인을 검색한다. | 확실 + +GC000935, GC000936 | 수문 설치(Tainter·Roller Gate) | 연결 | 광명단이 목재용 오일스테인을 검색한다. | 확실 + +GC000937 | Stop-Log 제작 | 연결 | 광명단·페인트가 신구접착·방청제를 검색한다. | 확실 + +GC000941 | Steel Penstock 현장제관 | 연결 | 시너 두 줄 모두 목재용 오일스테인을 검색한다. | 확실 + +GC000945 | Tainter Gate Guide Metal 제작 | 연결 | 전기용접봉이 아세틸렌을 검색한다. | 확실 + +GC000947 | Trash Rack 제작 | 연결 | 연마석·톱날이 도로청소용 빗자루를, Bite가 굴착기 치즐을 검색한다. | 확실 + +GC000950 | Trash Rack 설치 | 연결 | 시너가 목재용 오일스테인을 검색한다. | 확실 + +GC000972 | 노 기밀시험 | 연결 | 가루비누가 그리스를 검색한다. | 확실 + +GC000986 | Over Head Crane 설치 | 연결 | 걸레가 그리스를 검색한다. | 확실 + +GC000987 | Gantry Crane 설치 | 연결 | 광명단이 목재용 오일스테인을 검색한다. | 확실 + +GC000993 | 플랜트 공사별 설치 소모자재(1차 발견 보강) | 연결 | 1차 발견 외에 시너도 목재용 오일스테인을, 비닐시드도 PVC천막지를 검색한다. | 확실 + +GC000931~GC000950, GC000986, GC000987, GC000993 (23키) | 아세틸렌이 포함된 플랜트·운반기계 로직 | 단위 | 아세틸렌 원문 수량은 병 단위(규격 2,100·2,500·3,500·4,000·4,500ℓ 등)인데 호표는 같은 수를 kg로 사용한다. 해당 23키 모두 병→kg 환산식이 없다. | 확실 + +GC001050 | 차선도색 융착식 도료(수동식) | 연결 | 프라이머가 신구접착·방청제를, 프로판가스가 아세틸렌을 검색한다. | 확실 + +GC001055 | 현장가열 표층재생 | 누락·단위 | 원문 주⑤의 `100㎡당 팁(날) 0.7개`를 계산에 계상하지 않는다. 결과단위 `원/㎡`라면 0.007개/㎡가 필요하지만 호표에 팁 줄이 없다. | 확실 + +없음 (QF000041) | 임업용 동력기계톱 수확베기 | 누락 | 벌도목별 작업량과 벌목부가 저장됐지만 단가 로직이 없다. 후보 분류: 누락 확정. | 확실 + +없음 (QF000045) | 단목베기 100본당 | 누락 | 흉고직경 11구간의 벌목·가지제거·토막내기 및 작업보조 소요인력은 표에 있으나 단가 로직이 없다. 후보 분류: 누락 확정. | 확실 + +없음 (QF000047) | 단목베기 1,000㎡당 | 누락 | 나무높이별 벌목부·보통인부·굴착기 소요량은 표에 있으나 단가 로직이 없다. 후보 분류: 누락 확정. | 확실 + +없음 (산림 5-1-1) | 5-1-1 | 누락 | 관목굴취 `특별인부 0.084…0.408인`, `보통인부 0.012…0.072인/10본당`이 있는데 해당 로직 없음. | 확실 + +없음 (산림 5-1-2) | 5-1-2 | 누락 | 교목굴취 나무높이별 특별인부·보통인부 수량이 있는데 해당 로직 없음. | 확실 + +없음 (산림 5-1-3) | 5-1-3 | 누락 | 교목굴취 근원직경별 인력·굴착기·크레인 수량이 있는데 해당 로직 없음. | 확실 + +없음 (산림 5-3-2) | 5-3-2 | 누락 | 관목식재(단식) 수고별 특별인부·보통인부 수량이 있는데 해당 로직 없음. | 확실 + +없음 (산림 5-3-3) | 5-3-3 | 누락 | 관목식재(군식) 수고별 특별인부·보통인부 수량이 있는데 해당 로직 없음. | 확실 + +없음 (산림 5-3-4) | 5-3-4 | 누락 | 교목식재 나무높이별 인력·장비 수량이 있는데 해당 로직 없음. | 확실 + +없음 (산림 5-3-5) | 5-3-5 | 누락 | 교목식재 흉고직경별 인력·장비 수량이 있는데 해당 로직 없음. | 확실 + +없음 (산림 5-16-1) | 5-16-1 | 누락 | 단끊기 공정별 보통인부 수량이 있는데 해당 로직 없음. | 확실 + +미판정 (5-16-2) | 5-16-2 | 누락 | 선떼붙이기 표는 단끊기 `1m`와 떼붙임 `물량산출`의 합성 기준. 두 하위 로직과 조합하여 처리할 의도인지 확인 필요. | 의심 + +미판정 (5-17-1) | 5-17-1 | 누락 | 떼조공은 단끊기·줄떼심기·선떼붙이기 조합 표. 독립 로직 필요성 확인 필요. | 의심 + +미판정 (5-17-2) | 5-17-2 | 누락 | 돌조공은 단끊기·돌쌓기 조합 표. 독립 로직 필요성 확인 필요. | 의심 + +없음 (산림 5-19-1) | 5-19-1 | 누락 | 표토절취 깊이별 보통인부 소요량이 있는데 해당 로직 없음. | 확실 + +없음 (산림 5-19-2) | 5-19-2 | 누락 | 표토펴기 ㎥당·100㎡당 보통인부 수량이 있는데 해당 로직 없음. | 확실 + +없음 (산림 5-26-1) | 5-26-1 | 누락 | 막갈이 사토·양토·식토 × 깊이별 `인/992㎡당` 수량이 있는데 해당 로직 없음. | 확실 + +없음 (산림 5-26-2) | 5-26-2 | 누락 | 흙부수기 토성 × 깊이별 `인/992㎡당` 수량이 있는데 해당 로직 없음. | 확실 + +없음 (산림 5-26-3) | 5-26-3 | 누락 | 돌자갈치우기 토성 × 함량별 `인/992㎡당` 수량이 있는데 해당 로직 없음. | 확실 + +없음 (산림 5-28-2) | 5-28-2 | 누락 | 방초·야자섬유매트 포장 `조경공 2인+보통인부 1인`, 90·130㎡/일 시공량이 있는데 해당 로직 없음. | 확실 + +GF000210 | 유로폼 설치 및 해체 | 누락 | `12-38-1`은 유로폼 자재 손료의 사용횟수 기준이라 독립 단가 로직 불필요; `12-38-2`의 패널·부자재 사용수량은 상위 인력 로직이 맡는 공종이지만 실제 호표에 빠져 재료비가 계산되지 않는다. | 확실 + +GC000575 | 시멘트벽돌 | 연결 | 검색어 주차라인블록 1000X300X150; 자재품목표 시멘트벽돌 `190×90×57mm` `MT005760` 등 17줄 있음 | 확실 + +GC000604 | 카페트 | 연결 | 검색어 불연천장재(마이톤); 자재품목표 카페트 `MT008142`, 타일카페트 `MT008140` 있음 | 확실 + +GC000319, GC001050 | 융착식 도료 프라이머 | 연결 | 검색어 신구접착및방청제 LDS-MP; 자재품목표 융착식 프라이머 `MT003454` 있음(ℓ; 원문 kg면 환산 검토 필요) | 확실 + +GC001050 | 프로판가스 | 연결 | 검색어 아세틸렌 98%; 자재품목표 프로판가스 일반용 `MT003175`, kg 있음 | 확실 + +GC000375 | 무기질아연말 샵프라이머 | 연결 | 검색어 목재전용도료 오일스테인; 자재품목표 무기질 아연말 전처리 프라이머 `MT003460`, 무기질 아연말 프라이머 `MT003461` 있음 | 확실 + +GC000665 | 와이어메시 | 연결 | 검색어 메쉬스크린 H=0.9m; 자재품목표 와이어메쉬(용접철망) `MT007128` 등 있음 | 확실 + +GC000933~GC000936, GC000987 | 광명단 | 연결 | 검색어 목재전용도료 오일스테인; 자재품목표 광명단조합페인트 `MT003375`~`MT003377` 있음 | 확실 + +GF000220, GF000224 | 잡석(채움재) | 연결 | 검색어 모래 강모래; 자재품목표 자연자갈·잡석 `MT003848`, 깬잡석 `MT004113` 등 있음 | 확실 + +GC000181 | P.P로프 9㎜ | 연결 | 검색어 와이어로프(철심) 9㎜; 자재품목표 P.P로프는 `MT007584` 등 6줄 있으나 9㎜ 규격은 검색 없음 | 확실 + +GC000348 | 일체식 방수시트 | 연결 | 검색어 방수석고보드 12.5t; 자재품목표 ARS아크릴방수시트 `MT006814`·`MT006815` 있음(일체식과 동등성 미확인) | 확실 + +GC000251, GC000703~GC000707, GC000709~GC000719, GC000823~GC000824 (19키) | 공구손료·잡재료비 등 | 비목 | 결합 덧줄 전액을 경비에 계상; 원문이 공구 경비와 재료비 비율을 나누지 않아 오분류 여부는 확인 필요 | 의심 + +없음 (산림 6-4-3) | 6-4-3 | 누락 | 소금처리와 제거지점 표시의 `인/100본` 인력 수량이 `QF000167` 5줄에 있으나 로직키가 비어 있다. | 확실 + +없음 (산림 6-4-4) | 6-4-4 | 누락 | 뿌리 고살·덩굴 제거 등의 `인/100본` 수량은 `QF000168`에 있으나 로직키가 비어 있다. 같은 절의 `QF000169` 피복도·본수 환산표는 단독 로직이 필요 없는 보조 기준이다. | 확실 + +없음 (산림 6-7-1) | 6-7-1 | 누락 | 교목 시비의 근원직경별 특별인부·보통인부 수량은 `QF000173` 10줄에 있으나 로직키가 비어 있다. | 확실 + +없음 (산림 7-1-1) | 7-1-1 | 누락 | 수확 인력집재의 거리·경사 조건별 작업량 표가 있으나 산림 로직은 이웃 절 `7-1-2`부터 있다. | 확실 + +없음 (산림 7-5-2) | 7-5-2 | 누락 | 숲가꾸기·병해충방제 트랙터 집재의 거리·본당재적별 작업량 및 3인 1조 구성이 있으나 로직 없음. | 확실 + +없음 (산림 7-7-2) | 7-7-2 | 누락 | 숲가꾸기·병해충방제 HAM200 집재의 집재재적·거리별 작업량과 3인 1조 구성이 있으나 로직 없음. | 확실 + +없음 (산림 7-8-1) | 7-8-1 | 누락 | 타워야더 가선설치의 집재방식·가선길이별 소요인력이 있으나 로직 없음. `GF000068` 비고도 `가선설치 · 해체(7-8-1 · 7-8-2) 따로`라고 명시. | 확실 + +없음 (산림 7-8-2) | 7-8-2 | 누락 | 타워야더 가선해체의 집재방식·가선길이별 소요인력이 있으나 로직 없음. 상위 집재 로직에 포함되지 않음. | 확실 + +없음 (산림 7-9-1) | 7-9-1 | 누락 | 수확 스윙야더 집재의 집재재적·거리별 작업량 및 4인 1조 구성이 있으나 로직 없음. | 확실 + +없음 (산림 7-9-2) | 7-9-2 | 누락 | 숲가꾸기·소나무재선충병방제 스윙야더 집재의 집재재적·거리별 작업량 및 4인 1조 구성이 있으나 로직 없음. | 확실 + +없음 (산림 8-1-1) | 8-1-1 | 누락 | 약제주입기 천공수별 `소요인력(ha당)`이 있으나 산림 로직 없음. | 확실 + +없음 (산림 8-1-2) | 8-1-2 | 누락 | 약제주입병 천공수별 `소요인력`이 있으나 산림 로직 없음. | 확실 + +없음 (산림 8-6-2) | 8-6-2 | 누락 | 드론방제의 무인헬기 `ha당` 인력 편성·소요량이 있으나 로직 없음(`8-6-1` 유인헬기만 있음). | 확실 + +없음 (산림 8-6-3) | 8-6-3 | 누락 | 지상방제의 차량살포 인력 편성과 물량 조건이 있으나 로직 없음. | 확실 + +없음 (산림 9-4-1) | 9-4-1 | 누락 | 암파쇄 브레이커 작업능력·치즐소모량 표. | 확실 + +없음 (산림 9-5-1) | 9-5-1 | 누락 | 육상 암석 절취 발파 10%의 폭약·화약공 수량 표. | 확실 + +없음 (산림 9-5-2) | 9-5-2 | 누락 | 암석 절취 깎기 90%의 암종별 작업능력·치즐소모량 표. | 확실 + +없음 (산림 9-6-1) | 9-6-1 | 누락 | 기계소할 15%의 브레이커 작업능력·치즐소모량 표. | 확실 + +없음 (산림 9-7-2) | 9-7-2 | 누락 | 무근콘크리트 깨기 T=30㎝ 이상의 작업능력·치즐·보통인부 표. | 확실 + +없음 (산림 9-8-2) | 9-8-2 | 누락 | 철근콘크리트 깨기 T=30㎝ 이상의 작업능력·치즐·보통인부 표. | 확실 + +없음 (산림 9-10-1) | 9-10-1 | 누락 | 콘크리트 깨기 작업능력·치즐·보통인부 표. | 확실 + +없음 (산림 9-10-2) | 9-10-2 | 누락 | 아스팔트 깨기 작업능력·치즐·보통인부 표. | 확실 + +없음 (산림 9-11-1) | 9-11-1 | 누락 | 콘크리트 커트 인력·장비·절단깊이별 수량 표. | 확실 + +없음 (산림 9-12-2) | 9-12-2 | 누락 | 측구터파기 암절취의 할석공·브레이커 적용량 표. | 확실 + +없음 (산림 9-12-3) | 9-12-3 | 누락 | 측구터파기 발파암의 할석공·브레이커 적용량 표. | 확실 + +없음 (산림 9-13-7) | 9-13-7 | 누락 | 구조물터파기 육상 암절취 0~1m, 할석공·브레이커 적용량. | 확실 + +없음 (산림 9-13-8) | 9-13-8 | 누락 | 구조물터파기 육상 암절취 1~2m, 할석공·브레이커 적용량. | 확실 + +없음 (산림 9-13-9) | 9-13-9 | 누락 | 구조물터파기 육상 암절취 2~3m, 할석공·브레이커 적용량. | 확실 + +없음 (산림 9-13-10) | 9-13-10 | 누락 | 구조물터파기 용수 암절취 0~1m, 인력·브레이커 적용량. | 확실 + +없음 (산림 9-13-11) | 9-13-11 | 누락 | 구조물터파기 용수 암절취 1~2m, 인력·브레이커 적용량. | 확실 + +없음 (산림 9-13-12) | 9-13-12 | 누락 | 구조물터파기 용수 암절취 2~3m, 인력·브레이커 적용량. | 확실 + +없음 (산림 9-13-13) | 9-13-13 | 누락 | 구조물터파기 육상 발파암 0~1m, 할석공·브레이커 적용량. | 확실 + +없음 (산림 9-13-14) | 9-13-14 | 누락 | 구조물터파기 육상 발파암 1~2m, 할석공·브레이커 적용량. | 확실 + +없음 (산림 9-13-15) | 9-13-15 | 누락 | 구조물터파기 육상 발파암 2~3m, 할석공·브레이커 적용량. | 확실 + +없음 (산림 9-13-16) | 9-13-16 | 누락 | 구조물터파기 용수 발파암 0~1m, 할석공·브레이커 적용량. | 확실 + +없음 (산림 9-13-17) | 9-13-17 | 누락 | 구조물터파기 용수 발파암 1~2m, 할석공·브레이커 적용량. | 확실 + +없음 (산림 9-13-18) | 9-13-18 | 누락 | 구조물터파기 용수 발파암 2~3m, 할석공·브레이커 적용량. | 확실 + +없음 (산림 9-16-3) | 9-16-3 | 누락 | 살수의 흡입준비·운반 등 작업시간 기준; `9-16-1~2` 노체포설·다짐 로직과 별개. | 확실 + +없음 (산림 9-19-3) | 9-19-3 | 누락 | 비탈면 고르기 발파암 보통인부·공기압축기 수량; `9-19-1~2` 로직과 별개. | 확실 + +없음 (산림 10-6-1) | 10-6-1 | 누락 | 토사·석재의 운반거리별 보통인부 `인/㎥당` 표가 있으나 로직 없음. | 확실 + +없음 (산림 10-6-2) | 10-6-2 | 누락 | 돌 지게운반 거리별 보통인부 표가 있으나 로직 없음. | 확실 + +없음 (산림 10-6-3) | 10-6-3 | 누락 | 기타 임업자재 운반거리·자재종류별 보통인부 표가 있으나 로직 없음. | 확실 + +없음 (산림 10-8-1) | 10-8-1 | 누락 | 케이블 크레인 짐내리기의 운반기구별 인부·신호수 수량이 있으나 로직 없음. `10-8-2·3`은 별도 설치·운전 로직. | 확실 + +없음 (산림 10-9-1) | 10-9-1 | 누락 | 상위 `GF000145(10-9)`는 시간당 기계경비만 계산한다. 비고에 `10-9-1 운반량 쓰는 식 요소 없음`을 명시하므로 자재별 1회 운반량을 반영한 단위 운반비 계산은 빠졌다. | 확실 + +없음 (산림 10-12-1) | 10-12-1 | 누락 | 덤프운반 토사의 굴착기 적재 `Q1=3600×q0×K×f×E0/Cm`와 운반 조건이 있으나 해당 로직 없음. | 확실 + +없음 (산림 10-13-1) | 10-13-1 | 누락 | 드론운반 수평거리 100~600m별 인원 수량이 있으나 로직 없음. | 확실 + +없음 (산림 10-13-2) | 10-13-2 | 누락 | 인력운반 거리별 인원 수량과 드론운반 시간·본수 기준이 있으나 로직 없음. | 확실 + +없음 (산림 13-4-6) | 13-4-6 | 누락 | 밑돌·천단 돌쌓기 뒷길이별 석공 `0.05·0.08…인` 표가 있으나 `13-4-1·2·4·5` 이웃 로직만 있다. | 확실 + +없음 (산림 13-10-2) | 13-10-2 | 누락 | 나무 말뚝박기 말구·길이별 보통인부 수량이 있으나 `13-10-1` 말뚝 다듬기 로직만 있다. | 확실 + +없음 (산림 13-11-1) | 13-11-1 | 누락 | 원형 돌망태 지름별 조약돌량·작업 수량 표가 있으나 로직은 `13-11-3·4` 다른 형태만 있다. | 확실 + +없음 (산림 13-11-2) | 13-11-2 | 누락 | 타원형 돌망태 지름별 조약돌량·작업 수량 표가 있으나 로직은 `13-11-3·4` 다른 형태만 있다. | 확실 + +없음 (산림 13-15-1) | 13-15-1 | 누락 | 목재 껍질벗기기 말구별 보통인부 수량이 있으나 로직 없음. | 확실 + +없음 (건설 공통 7-4-2) | 7-4-2 | 누락 | 석재판 붙임 앵커 긴결공법의 인력 수량·일당 시공량 표 `QC000495`가 있으나 로직 없음. | 확실 + +없음 (건설 공통 7-4-3) | 7-4-3 | 누락 | 석재판 붙임 강제트러스 지지공법의 인력 수량·일당 시공량 표 `QC000496`이 있으나 로직 없음. | 확실 + +없음 (건설 유지관리 3-1-1) | 3-1-1 | 누락 | 콘크리트구조물 인력 헐기 직종·시공량. | 확실 + +없음 (건설 유지관리 3-1-2) | 3-1-2 | 누락 | 콘크리트구조물 기계 헐기 인력·장비·시공량. | 확실 + +없음 (건설 유지관리 3-1-3) | 3-1-3 | 누락 | 철골재 인력 철거 직종·시공량. | 확실 + +없음 (건설 유지관리 3-1-4) | 3-1-4 | 누락 | 철골재 기계 철거 직종·장비·시공량. | 확실 + +없음 (건설 유지관리 3-1-5) | 3-1-5 | 누락 | 석축 인력 헐기 뒷길이별 할석공·보통인부 수량. | 확실 + +없음 (건설 유지관리 3-2-1) | 3-2-1 | 누락 | 금속기와 해체 지붕잇기공 수량. | 확실 + +없음 (건설 유지관리 3-2-2) | 3-2-2 | 누락 | 흡음텍스 해체 내장공 수량. | 확실 + +없음 (건설 유지관리 3-2-3) | 3-2-3 | 누락 | 경량천장철골틀 해체 내장공 수량. | 확실 + +없음 (건설 유지관리 3-2-4) | 3-2-4 | 누락 | 조적벽 해체 조적공 수량. | 확실 + +없음 (건설 유지관리 3-2-5) | 3-2-5 | 누락 | 경량벽체철골틀 해체 내장공 수량. | 확실 + +없음 (건설 유지관리 3-2-6) | 3-2-6 | 누락 | 석고판 해체 벽·천장별 내장공 수량. | 확실 + +없음 (건설 유지관리 3-2-7) | 3-2-7 | 누락 | 도배 해체 벽·천장별 도배공 수량. | 확실 + +없음 (건설 유지관리 3-2-8) | 3-2-8 | 누락 | PVC계바닥재 해체 내장공 수량. | 확실 + +없음 (건설 유지관리 3-2-9) | 3-2-9 | 누락 | 타일 해체 붙임방식별 타일공 수량. | 확실 + +없음 (건설 유지관리 3-2-10) | 3-2-10 | 누락 | 기존방수층·보호층 철거 착암공 등 수량. | 확실 + +없음 (건설 유지관리 3-2-11) | 3-2-11 | 누락 | 기존방수층 제거·바탕처리 방수공 수량. | 확실 + +없음 (건설 유지관리 3-2-12) | 3-2-12 | 누락 | 석면건축자재 해체 내·외장재별 석면해체공 수량. | 확실 + +없음 (건설 유지관리 3-3-1) | 3-3-1 | 누락 | 콘크리트·모르타르면 재도장 바탕처리 도장공·시공량. | 확실 + +없음 (건설 유지관리 3-3-2) | 3-3-2 | 누락 | 철재면 재도장 바탕처리 A·B급별 도장공·시공량. | 확실 + +없음 (건설 유지관리 3-3-3) | 3-3-3 | 누락 | 목재면 재도장 바탕처리 A·B급별 도장공·시공량. | 확실 + +없음 (건설 유지관리 3-4-1) | 3-4-1 | 누락 | 지붕 덧씌우기 지붕잇기공·시공량. | 확실 + +없음 (건설 유지관리 3-4-2) | 3-4-2 | 누락 | 지붕 재설치 지붕잇기공·시공량. | 확실 + +없음 (건설 유지관리 3-4-3) | 3-4-3 | 누락 | 도배 교체 벽·천장별 인력·시공량. | 확실 + +없음 (건설 유지관리 3-4-4) | 3-4-4 | 누락 | PVC계바닥재 교체 내장공·시공량. | 확실 + +없음 (건설 유지관리 3-4-5) | 3-4-5 | 누락 | 타일 교체 붙임방식별 인력·시공량. | 확실 + +없음 (건설 유지관리 4-1-1) | 4-1-1 | 누락 | 배관 해체 규격·재질별 배관공·보통인부 수량. | 확실 + +없음 (건설 유지관리 4-1-2) | 4-1-2 | 누락 | 각형덕트 해체 두께별 인력 수량. | 확실 + +없음 (건설 유지관리 4-1-3) | 4-1-3 | 누락 | 스파이럴덕트 해체 두께·규격별 덕트공·보통인부 수량. | 확실 + +없음 (건설 유지관리 4-1-4) | 4-1-4 | 누락 | 배관보온 해체 재질·규격별 보온공·보통인부 수량. | 확실 + +없음 (건설 유지관리 4-1-5) | 4-1-5 | 누락 | 덕트보온 해체 재질별 보온공 수량. | 확실 + +없음 (건설 유지관리 4-1-6) | 4-1-6 | 누락 | 펌프 해체 출력별 기계설비공·보통인부 수량. | 확실 + +없음 (건설 유지관리 4-3-1) | 4-3-1 | 누락 | 유량계 교체 인력·규격별 시공량. | 확실 + +없음 (건설 유지관리 4-3-2) | 4-3-2 | 누락 | 관갱생 규격별 규사·에폭시도료·배관공·장비시간. | 확실 + +없음 (건설 유지관리 4-3-3) | 4-3-3 | 누락 | 배관누수 검사 배관공·시공량. | 확실 + +GF000137 / QF000307 | 중기운반 기준표 | 누락 | 원문 10-4 자주식 운반 `크레인(트럭 10TON)` 한 줄이 QF000307에 없음(원문 16줄·JSON 15줄); GF000137 직접 영향은 미확인 | 확실 diff --git a/docs/raw/M01_점검/진행.md b/docs/raw/M01_점검/진행.md new file mode 100644 index 00000000..373b2437 --- /dev/null +++ b/docs/raw/M01_점검/진행.md @@ -0,0 +1,20 @@ +# M01 표 행 대조 진행 (2026-09-30) + +읽기 전용 검수. 코드·자료·git은 변경하지 않았고, 이 폴더의 보고 파일만 기록한다. `끝남`은 원문 표의 조건·규격·숫자·단위를 자료 줄과 수동 대응한 것, `사람 확인`은 원문·JSON 물리 줄 수가 같아 기계 비교가 가능했으나 조건·비목 판단이 남은 것, `미판정`은 병합·피벗·여러 표 등으로 아직 일대일 대응표를 만들지 못한 것이다. `사람 확인`과 `미판정`은 합격을 뜻하지 않는다. + +| 부문 | 전체 표 | 끝남 | 사람 확인 | 미판정 | 다음 확인 자리 | +|---|---:|---:|---:|---:|---| +| 산림 | 634 | 11 | 202 | 421 | 10-13-1·2 QF000340~342는 거리·인원 12칸과 시간산출 4항목 완료(로직 누락은 별개); 10-9-1 QF000329 35칸, 10-6-1·2·3 QF000308~310도 완료; QF000307 크레인 1줄 누락. 다음은 산림 10·12·13장 나머지 임도 표. | +| 건설 공통 | 575 | 0 | 59 | 516 | 산림·토목 후 7장 석재판 및 6장 재료. | +| 토목 | 765 | 0 | 36 | 729 | 산림 후 터널·도로·사방에 쓰는 1·2·3장부터. | +| 건축 | 353 | 0 | 7 | 346 | 토목 후. | +| 기계설비 | 509 | 0 | 56 | 453 | 건축 후. | +| 유지관리 | 197 | 0 | 11 | 186 | 마지막. | +| 합계 | 3,033 | 11 | 371 | 2,651 | 원문-JSON 행 일대일 수동 확인 계속. | + +기계 선별 결과는 `보고.md`의 「3차 전 부문 표 대응 기계 선별」에 있음. 출처 공유 1,380표, 복수 원문 표 블록 561표, 피벗 등 물리 줄 수 다른 720표는 단순 비교로 판정 불가(합계 2,661표). 그중 수동 완료 11표를 빼면 2,650표, 여기에 품셈 본문 출처 비교대상 밖인 공통 1표를 더해 위 미판정 2,651표다. 물리 줄 수 같은 371표도 숫자 181표 일치·190표 차이에 그칠 뿐 조건·비목은 사람 확인이 남아 있다. + +153절 원문 누락 후보는 `보고.md` 장별 표와 합계에 모두 분류했고, 흠의 줄글 목록은 `보고_줄글.md`에 있다. 재료 호표 675줄은 2차 때 펼쳐 검토했으나 잘못 연결된 각 줄마다 자재품목표 대체품 존재 유무까지 전부 확인하지는 못했다. 배수 환산도 발견 사례와 기계 선별만 기록했으며 1,354개 전 로직의 완전한 수동 판정은 남았다. 새 흠을 찾으면 두 보고 파일을 먼저 갱신하고 이 진행표를 고친다. +## 다음 볼 자리 (2026-09-30 중단 시점) + +사용자 지시로 새 대조를 멈췄다. 재개 시 산림 10·12·13장 나머지 임도 표부터 확인하고, 건설 공통 → 토목 → 건축 → 기계설비 → 유지관리 순서로 진행한다. 371개 `사람 확인` 표도 조건·수치·단위·비목 확정 전이며 2,651개 `미판정` 표는 병합 칸·피벗 해체가 먼저다. 그 뒤 재료 검색어 675줄의 품목표 실재 여부와 1,354개 로직의 배수 환산을 전수 확인한다. 판정 기준과 세 사례는 `대조방식.md`에 남긴다. From b4100acc6de7e835ea83422a3d169d7c4a7b19d3 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 30 Sep 2026 10:39:59 +0900 Subject: [PATCH 17/18] =?UTF-8?q?feat(M02):=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=E3=80=8C=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=EC=A1=B0=ED=95=A9=E3=80=8D=20=E2=80=94=20=EB=82=98=EB=AC=B4?= =?UTF-8?q?=EA=B0=80=20=EC=A3=BC=20=C2=B7=20=ED=91=9C=EB=8A=94=20=EC=B0=B8?= =?UTF-8?q?=EC=A1=B0=ED=91=9C=20=C2=B7=20=EB=B9=84=EA=B3=A0=20=C2=B7=20?= =?UTF-8?q?=EC=A0=84=EA=B5=AC=EA=B0=84=20=C2=B7=20=ED=95=A9=EA=B3=84=20?= =?UTF-8?q?=EC=88=A8=EA=B9=80(PLAN=2026-7a~e)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 화면 이름 「구조물집계표」 · 「구조물 집계 양식」 → 「구조물 조합」(좌측 줄 · 상세 제목 · 안내글) · 파일 이름 · 양식 키 · API 길 그대로 - 좌측 패널 「항목 나무」 컨테이너 없앰 — 페이지가 나무를 만들어 편집 칸 왼쪽(aside)에 붙임 · 「구조물 조합」일 때만 보임 · 오른쪽 클릭 · 찾기 · 표와 서로 고름 · 고치기 전 저장 물음 그대로 - 표 부품 lockHead — 열 더하기 · 분류N 더하기 · 열 지우기 단추 없음 · 머리 · 층 이름 고치기 없음(단위 · 일위대가 · 조회 후 선택은 그대로) - 비고 열 · 전구간 줄 · 합계 줄은 보일 문서에서만 뺌(comboView) · [저장] 때 저장본 것을 다시 끼움(comboMerge) · 양식 파일 · M02_Table_Fill 그대로 - 서버 — 화면 표 [저장](store.write screen=True)은 있던 항목의 경로 · 층 이름 · 빈 폴더를 저장본 그대로(tree.keep_paths · 브라우저 머리 안 받음) · 머리는 경로에서 다시 만듦 · 나무 고침 줄기 그대로 - 나무 같은 이름 409 는 「이미 있는 항목」 까닭 그대로 보임(판 다름 안내와 가름) - 시험 test_m02_combo_screen 3(숨김 · 되끼움 Node · 잠금 · 나무 자리) · test_m02_tree 표 저장 약속 새로 · 이름 · 잠금 기대값 고침 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL --- .../M02_MasterTemplete_ComboPanel.ts | 2 +- .../M02_MasterTemplete_Inputs.ts | 6 +- .../M02_MasterTemplete_Router.py | 2 +- .../M02_MasterTemplete_Router_Layers.py | 2 +- .../M02_MasterTemplete_Store.py | 8 +- .../M02_MasterTemplete_Structure.ts | 28 ++++- M02_MasterTemplete/M02_MasterTemplete_Tree.py | 14 +++ .../M02_MasterTemplete_TreePanel.ts | 3 +- .../M02_MasterTemplete_UI_Main.ts | 32 +++++- .../M02_MasterTemplete_UI_Page.ts | 30 +++-- .../M02_MasterTemplete_UI_Side.ts | 21 ++-- .../M02_MasterTemplete_UI_Style.css | 50 ++++++++ resources/tester/test_m02_combo_screen.py | 107 ++++++++++++++++++ resources/tester/test_m02_layer_screen.py | 3 +- resources/tester/test_m02_structure_panel.py | 2 +- resources/tester/test_m02_tree.py | 25 ++-- ui_template/sheet/ui_template_sheet.ts | 19 +++- ui_template/sheet/ui_template_sheet_render.ts | 7 +- ui_template/ui_template_locale_m2.ts | 6 +- 19 files changed, 306 insertions(+), 61 deletions(-) create mode 100644 resources/tester/test_m02_combo_screen.py diff --git a/M02_MasterTemplete/M02_MasterTemplete_ComboPanel.ts b/M02_MasterTemplete/M02_MasterTemplete_ComboPanel.ts index f1bc2361..6311a601 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_ComboPanel.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_ComboPanel.ts @@ -38,7 +38,7 @@ const TEXT = { empty: "맞는 조합 없음", none: "조합 없음", use: "이 열에 쓰기", - useAsk: "이 열이 {value} 을 쓰게 바꿈 — 구조물집계표에 바로 적힘 · 조합은 그대로", + useAsk: "이 열이 {value} 을 쓰게 바꿈 — 구조물 조합에 바로 적힘 · 조합은 그대로", useOk: "바꿈", used: "이 열이 {value} 을 씀", loadFailed: "조합 목록을 못 받음 — {value}", diff --git a/M02_MasterTemplete/M02_MasterTemplete_Inputs.ts b/M02_MasterTemplete/M02_MasterTemplete_Inputs.ts index 77a3c59c..586a1378 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Inputs.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Inputs.ts @@ -48,9 +48,9 @@ const TEXT = { count: "로직 {logics} · 입력 {inputs}", chosen: "면적산출 {area} · 재료산출 {material} · 선택 {pick}", auto: "자동값", - noItem: "집계표를 먼저 저장하면 입력 목록이 나옴", - noCombo: "일위대가를 먼저 고름 — 집계표 일위대가 줄에서 고름", - unsavedCombo: "고른 일위대가는 집계표를 저장하면 입력 목록이 나옴", + noItem: "구조물 조합을 먼저 저장하면 입력 목록이 나옴", + noCombo: "일위대가를 먼저 고름 — 구조물 조합 일위대가 줄에서 고름", + unsavedCombo: "고른 일위대가는 구조물 조합을 저장하면 입력 목록이 나옴", noLogic: "이 일위대가에 담긴 로직이 없음", missing: "빠진 로직 {value}", failed: "입력 목록을 못 받음 — {value}", diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router.py b/M02_MasterTemplete/M02_MasterTemplete_Router.py index 8a4ebfe6..e4759adb 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router.py @@ -169,7 +169,7 @@ def get_template(kind: str, name: str) -> dict: @router.put("/templates/{kind}/{name}") def put_template(kind: str, name: str, body: SaveBody) -> dict: - return _call(store.write, kind, name, body.판, body.문서) + return _call(store.write, kind, name, body.판, body.문서, True) # 화면 [저장](26-7e) @router.delete("/templates/{kind}/{name}") diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py index 5743a60b..81a867aa 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py @@ -305,7 +305,7 @@ async def save_layer_template( def _store_save(folder: Path, kind: str, name: str, version: str, doc: Any, layer: str) -> dict: with store.at(folder): try: - info = store.write(kind, name, version, doc) + info = store.write(kind, name, version, doc, screen=True) # 화면 [저장](26-7e) except store.StoreError as error: raise HTTPException(status_code=error.status, detail=error.detail) from error return {"종류": kind, "이름": name, "판": info["판"], "층": layer} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Store.py b/M02_MasterTemplete/M02_MasterTemplete_Store.py index f1bd4860..ced3a7fc 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Store.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Store.py @@ -150,7 +150,7 @@ def _check_skeleton(kind: str, doc: Any) -> None: raise StoreError(400, str(e)) from e -def write(kind: str, name: str, version: str, doc: Any) -> dict[str, Any]: +def write(kind: str, name: str, version: str, doc: Any, screen: bool = False) -> dict[str, Any]: """`version` 이 빈 글이면 새로 만듦(이미 있으면 409) · 아니면 그 판일 때만 덮어씀. 구조물집계표를 저장하면 — 새 구조물 열의 빈 구조물 도면 · 면적산출 · 재료산출을 번호와 같이 만들고 지운 열(또는 구조물이 아니게 된 열)의 세 파일을 지움(잠금 밖에서 · 안에서 부르면 죽음). @@ -158,7 +158,9 @@ def write(kind: str, name: str, version: str, doc: Any) -> dict[str, Any]: (브라우저 값 버림 · 5장 ②) · 알약 `연결` 은 문서와 같이 그대로 적힘. 번호 칸(도번 · 면적번 · 재료번)은 서버 것 — 있던 파일 번호를 그대로 둠(화면 값을 안 받음). 표 열의 `본뜬열`(분류 더하기로 복사한 새 열)은 적지 않음 — 집계표면 아직 파일이 없는 새 열이 - 원본 열의 일위대가 · 자재조합 키와 세 파일 내용을 받음(새 번호 · 그 뒤로는 열마다 따로).""" + 원본 열의 일위대가 · 자재조합 키와 세 파일 내용을 받음(새 번호 · 그 뒤로는 열마다 따로). + `screen` = 화면 표 [저장](PLAN 26-7e) — 있던 항목의 경로 · 층 이름 · 빈 폴더는 저장본 그대로 + (정본 = 나무 · 머리는 경로에서 다시 만듦 · `tree.keep_paths`).""" path = _path(kind, name) _check_skeleton(kind, doc) origins = _take_origins(doc) if kind == "table" else {} @@ -170,6 +172,8 @@ def write(kind: str, name: str, version: str, doc: Any) -> dict[str, Any]: have = version_of(path.read_bytes()) if path.is_file() else "" if have != (version or ""): raise StoreError(409, {"stale": [name], "판": have}) + if screen and have and kind == "table" and name == STRUCTURE_TABLE: + doc = tree.normalize_table(tree.keep_paths(doc, json.loads(path.read_bytes()))) if have and kind in _KEY: number = json.loads(path.read_bytes()).get(_KEY[kind]) doc = {**doc, _KEY[kind]: number} if number else doc diff --git a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts index d7fc619b..8163f1f6 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts @@ -57,12 +57,12 @@ const TEXT = { none: "고를 것이 없음", pickPrice: "일위대가 고르기", combosFailed: "일위대가 목록을 못 받음 — {value}", - saveFirst: "새 열은 집계표를 먼저 저장해야 번호가 생김", + saveFirst: "새 열은 구조물 조합을 먼저 저장해야 번호가 생김", overwrite: "{from} 의 {what} 내용을 {to} 에 덮어씀 — 번호 · 열은 그대로", pickCopy: "{value} 복사해 가져오기", copied: "복사해 가져옴", copyFailed: "복사 실패 — {value}", - noPrice: "이 열에 일위대가가 없어 알약 없음 — 구조물집계표 일위대가 줄에서 고름", + noPrice: "이 열에 일위대가가 없어 알약 없음 — 구조물 조합 일위대가 줄에서 고름", noInputs: "일위대가 {value} 에 입력값이 없어 알약 없음", inputsFailed: "일위대가 입력값을 못 받음 — {value}", }; @@ -180,6 +180,30 @@ export interface StructureColumn { /** 구조물 열 = 꼴=수 — NO · 측점 · 비고 같은 글 열은 도면 · 면적산출 · 재료산출 · 번호가 없음(서버와 같은 규칙) */ export const isStructureColumn = (col: StructureColumn): boolean => col.꼴 === "수"; +/** 관리 화면 「구조물 조합」에서 숨기는 것(PLAN 26-7b) — 비고 열 · 전구간 줄(본문 줄) · 합계 줄. + * 양식 파일은 그대로 — 보일 문서에서만 빼고 [저장] 때 저장본 것을 다시 끼움(`comboMerge`). */ +const HIDDEN_COLUMNS = ["memo"]; +type ComboDoc = { 열: StructureColumn[]; 줄?: unknown[]; 합계줄?: unknown[] }; + +export function comboView(doc: T): T { + const d = doc as unknown as ComboDoc; + const 열 = (d.열 ?? []).filter((c) => !HIDDEN_COLUMNS.includes(c.id)); + return { ...d, 열, 줄: [], 합계줄: [] } as unknown as T; +} + +/** 보인 표 고침 + 저장본의 숨긴 것 — 열 차례는 저장본(표에서 열을 더하거나 지우지 않음 · 26-7d) */ +export function comboMerge(stored: unknown, edited: T): T { + const s = stored as ComboDoc; + const e = edited as unknown as ComboDoc; + const byId = new Map(e.열.map((c) => [c.id, c])); + const kept = (s.열 ?? []).flatMap((c) => + HIDDEN_COLUMNS.includes(c.id) ? [c] : byId.has(c.id) ? [byId.get(c.id)!] : [], + ); + const known = new Set(kept.map((c) => c.id)); + const 열 = [...kept, ...e.열.filter((c) => !known.has(c.id))]; + return { ...e, 열, 줄: s.줄 ?? [], 합계줄: s.합계줄 ?? [] } as unknown as T; +} + /** 구조물 이름 + 변수값 — 제목행 층을 빈칸으로 이음(빈 층은 건너뜀 · 다 비면 열 id) */ export const columnHead = (col: StructureColumn): string => col.머리.filter((part): part is string => !!part && part.trim() !== "").join(" ") || col.id; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Tree.py b/M02_MasterTemplete/M02_MasterTemplete_Tree.py index 4bbdd933..32d16e1a 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Tree.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Tree.py @@ -108,6 +108,20 @@ def normalize_table(doc: dict[str, Any]) -> dict[str, Any]: return out +def keep_paths(doc: dict[str, Any], stored: dict[str, Any]) -> dict[str, Any]: + """화면 표 [저장](PLAN 26-7e) — 정본은 나무: 있던 항목의 경로(머리) · 층 이름 · 빈 폴더는 + 저장본 그대로 · 브라우저가 보낸 머리는 안 받음. 머리는 저장 맞춤(`normalize_table`)이 경로에서 + 다시 만듦.""" + out = copy.deepcopy(doc) + was = {c["id"]: path_of(c) for c in _items(stored)} + for col in _items(out): + if col["id"] in was: + _set_path(col, was[col["id"]]) + out["층"] = copy.deepcopy(stored.get("층") or []) + out["폴더"] = copy.deepcopy(stored.get("폴더") or []) + return out + + def folders_of(doc: dict[str, Any]) -> list[list[str]]: """나무의 폴더 전부 — 적어 둔 빈 폴더 + 항목 경로의 앞마디(차례 = 표 차례).""" out: list[list[str]] = [] diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts index 80ece6bb..0d18ec3f 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts @@ -233,7 +233,8 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle { showToast(L("M02_TreeDone"), "success"); await opt.onChanged(col); } catch (error) { - if (error instanceof StaleError) { + // 409 = 판 다름(서버 detail 이 객체 → 「HTTP 409」) 또는 같은 이름(글 까닭 — 그대로 보임) + if (error instanceof StaleError && error.message === "HTTP 409") { await refresh(); showToast(L("M02_TreeStale"), "error"); } else { diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts index 93af24d6..78b7e7da 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -17,6 +17,8 @@ import { type StructureDoc, } from "./M02_MasterTemplete_Api_Fetch"; import { + comboMerge, + comboView, createStructurePanel, pillsFor, STRUCTURE_TABLE, @@ -103,9 +105,11 @@ export function canEdit(layer: Layer, isAdmin: boolean): boolean { return layer === "project" || isAdmin; } -/** 페이지가 끼우는 것 — `top` = 편집 칸 맨 위 막대(회사용 읽기만 안내) · `onColumn` = 집계표에서 고른 열 */ +/** 페이지가 끼우는 것 — `top` = 편집 칸 맨 위 막대(회사용 읽기만 안내) · `onColumn` = 집계표에서 고른 열 · + * `aside` = 「구조물 조합」을 열 때만 편집 칸 왼쪽에 보임(항목 나무 · PLAN 26-7c) */ export interface MainExtras { top?: HTMLElement; + aside?: HTMLElement; onColumn?: (column: string | null) => void; } @@ -125,7 +129,7 @@ export function createMain( const notice = el("div", { className: "m02-main__notice", attrs: { hidden: "" } }); const host = el("div", { className: "m02-main__host" }); const extra = el("div", { className: "m02-main__extra" }); // 표 밖 형제 자리 — 구조물 설계 컨테이너 - const root = el("div", { + const column = el("div", { className: "m02-main", children: [ ...(extras.top ? [extras.top] : []), @@ -135,6 +139,15 @@ export function createMain( extra, ], }); + // 왼쪽 나무(주) · 오른쪽 편집 칸 — 나무는 「구조물 조합」일 때만(`showAside`) + const root = extras.aside + ? el("div", { className: "m02-shell", children: [extras.aside, column] }) + : column; + const isCombo = (at: Selection | null): boolean => + !!at && at.kind === "table" && at.name === STRUCTURE_TABLE && at.layer !== "project"; + const showAside = (): void => { + if (extras.aside) extras.aside.hidden = !isCombo(sel); + }; let sel: Selection | null = null; let version = ""; @@ -360,9 +373,11 @@ export function createMain( // 그 뒤 .열 을 그대로 읽으면 죽는다(2026-09-28 사용자 보고 — 열 고른 뒤 CAD // 불러오기 오류). getDoc() 뒤에도 옵셔널 체이닝을 이어야 한다. const columns = (): StructureColumn[] => sheet?.getDoc()?.열 ?? []; - editor = create(host, doc, { + // 「구조물 조합」 = 나무를 따라오는 참조표(26-7b · d) — 비고 · 전구간 · 합계 숨김 · 열 · 머리는 나무만 + editor = create(host, struct ? comboView(doc) : doc, { mode: at.layer === "project" ? "project" : "master", readOnly, + lockHead: !!struct, onChange: () => (dirty = !readOnly), ...(struct ? { @@ -382,7 +397,11 @@ export function createMain( } : {}), }); - if (struct) sheet = editor as SheetEditorHandle; + if (!struct) return; + const shown = editor as SheetEditorHandle; + sheet = shown; + // [저장]은 저장본의 숨긴 것을 다시 끼운 문서(서버가 머리를 경로에서 다시 만듦 · 26-7e) + editor = { getDoc: () => comboMerge(loaded, shown.getDoc()), destroy: shown.destroy }; return; } if (at.kind === "area" || at.kind === "material") { @@ -421,11 +440,12 @@ export function createMain( sel = next; onOpened?.(); bar(); + showAside(); if (!next) { title.textContent = L("M02_PickTemplate"); return empty(""); } - title.textContent = next.label ?? next.name; + title.textContent = isCombo(next) ? L("M02_KindTable") : (next.label ?? next.name); // 26-7a try { const got = await readTemplate(next.layer, next.kind, next.name, next.projectId); if (mine !== seq) return; @@ -501,6 +521,7 @@ export function createMain( sel = null; onOpened?.(); // 좌측 양식 · 열 줄 고름 지움 bar(); + showAside(); title.textContent = label; host.replaceChildren(node); return (d) => (dirty = d); @@ -544,6 +565,7 @@ export function createMain( if (sel && (await confirmLeave())) void open(sel); }); bar(); + showAside(); empty(""); return { root, open, current: () => sel, confirmLeave, showView, openTable, settle }; } diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts index 0870ed02..07afe8f7 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts @@ -1,6 +1,7 @@ /* ============================================================================= * M02_MasterTemplete_UI_Page.ts - * 마스터 템플릿 · 구조물 관리 화면 — 좌측 도킹 패널(양식 목록) / 우측 편집 칸(표·도면 부품) + * 마스터 템플릿 · 구조물 관리 화면 — 좌측 도킹 패널(양식 목록) / 우측 편집 칸(표·도면 부품) · + * 「구조물 조합」을 열면 편집 칸 왼쪽에 항목 나무(주 · 고치는 자리) · 오른쪽 참조표(PLAN 26-7c) * * 층 = 주소 인자 `?layer=personal|company`(없으면 시스템) — 같은 화면 한 벌. * 화면은 로그인만 확인 — 고치기는 층별 권한일 때만 켬(서버도 막음 · `M02_MasterTemplete_Layer.ts`). @@ -8,6 +9,7 @@ import "@ui/ui_template_workflow_layout.css"; import { el } from "@ui/ui_template_elements"; +import { t as L } from "@ui/ui_template_locale"; import { createWorkflowOverlays } from "@ui/ui_template_overlay"; import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch"; import { setActiveLayer } from "./M02_MasterTemplete_Api_Fetch"; @@ -19,7 +21,8 @@ import { titleOf, } from "./M02_MasterTemplete_Layer"; import { STRUCTURE_TABLE } from "./M02_MasterTemplete_Structure"; -import { createMain, type Selection } from "./M02_MasterTemplete_UI_Main"; +import { createTreePanel } from "./M02_MasterTemplete_TreePanel"; +import { canEdit, createMain, type Selection } from "./M02_MasterTemplete_UI_Main"; import { buildSide } from "./M02_MasterTemplete_UI_Side"; import "./M02_MasterTemplete_UI_Style.css"; @@ -35,15 +38,28 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise const isAdmin = access.canWrite; // 이 층을 고칠 수 있나(하위 부품의 「관리자」 = 같은 뜻) // 회사용을 읽기만 볼 때 — 안내 + [개인용으로 복사] const readOnlyBar = layer === "company" && !isAdmin ? buildReadOnlyBar(user) : null; + // 항목 나무 — 고르기 모달과 같은 폴더 나무(세 층 공용) · 「구조물 조합」 편집 칸 왼쪽 + const tree = createTreePanel({ + layer, + canEdit: canEdit(layer, isAdmin), + onPick: (item) => void main.openTable(layer, item.열), + settle: () => main.settle(), + onChanged: (colId) => main.openTable(layer, colId, true), + }); + const aside = el("aside", { + className: "m02-shell__tree", + children: [el("h3", { className: "m02-shell__title", text: L("M02_TreeTitle") }), tree.root], + }); const main = createMain( isAdmin, - () => void side.refresh(), + () => void Promise.all([side.refresh(), tree.refresh()]), () => side.paint(), { top: readOnlyBar?.root, + aside, onColumn: (column) => { readOnlyBar?.setColumn(column); - side.selectItem(column); + tree.select(column); }, }, ); @@ -61,11 +77,6 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise getOpen: main.current, confirmLeave: main.confirmLeave, showView: main.showView, - tree: { - onPick: (item) => void main.openTable(layer, item.열), - settle: main.settle, - onChanged: (colId) => main.openTable(layer, colId, true), - }, }); const layout = el("div", { className: "ui-workflow-layout m02-master" }); @@ -85,4 +96,5 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise root.innerHTML = ""; root.append(layout); void main.open(home); + void tree.refresh(); } diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts index 73b1a840..0fae8baf 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts @@ -1,10 +1,12 @@ /* ============================================================================= * M02_MasterTemplete_UI_Side.ts - * 마스터 템플릿 좌측 패널 — 「구조물 집계 양식」(구조물집계표 줄 하나 + 하위 구조물 도면 · 면적산출 · + * 마스터 템플릿 좌측 패널 — 「구조물 조합」(구조물집계표 줄 하나 + 하위 구조물 도면 · 면적산출 · * 재료산출 · 자재 조합) · 「도면 양식」(새로 · 본떠 · 지우기). 서버 길은 계약 `6_계약.md`. * 구조물 도면 · 면적산출 · 재료산출 줄은 집계표 구조물 열(꼴=수)마다 하나씩 — * 서버가 미리 만들어 둠(도번 SD · 면적번 SA · 재료번 SM + 같은 8자리) · * 줄 글은 번호가 맨 앞(「SD01010100 돌기슭막이 …」). 「자재 조합」 컨테이너는 `M02_MasterTemplete_Matset.ts`. + * 화면 이름 「구조물 조합」(PLAN 26-7a) — 파일 이름 · 양식 키 · API 길은 「구조물집계표」 그대로. + * 항목 나무는 좌측이 아니라 「구조물 조합」 상세 화면 왼쪽(PLAN 26-7c · 페이지가 붙임). * ========================================================================== */ import { @@ -38,7 +40,6 @@ import { } from "./M02_MasterTemplete_Structure"; import { buildMatsetSection } from "./M02_MasterTemplete_Matset"; import { buildOptionsSection } from "./M02_MasterTemplete_Options"; -import { createTreePanel, type TreePanelOptions } from "./M02_MasterTemplete_TreePanel"; import { canEdit, type Selection } from "./M02_MasterTemplete_UI_Main"; const KINDS: { kind: Kind; label: () => string }[] = [ @@ -72,8 +73,6 @@ export interface SideOptions { confirmLeave: () => Promise; /** 메인 칸에 양식 밖 화면(자재 조합 상세)을 띄움 */ showView: (label: string, node: HTMLElement) => (dirty: boolean) => void; - /** 항목 나무(26-3a) — 항목 누름 · 고치기 전 저장 처리 · 고친 뒤 표 다시 읽기 */ - tree: Pick; } export interface SideHandle { @@ -81,8 +80,6 @@ export interface SideHandle { refresh: () => Promise; /** 고름 표시를 지금 열린 것 한 줄로 — 편집 칸이 무엇을 열든(카드 [도면 수정] 등) 부름 */ paint: () => void; - /** 표에서 고른 열을 항목 나무에 표시 */ - selectItem: (colId: string | null) => void; } /** 서버 이름 규칙과 같게 — 화면에서 먼저 막음 */ @@ -155,15 +152,10 @@ export function buildSide(opt: SideOptions): SideHandle { ); summary.classList.remove("is-collapsed"); summary.classList.add("m02-side__summary"); - // 항목 나무 — 구조물 집계 양식 아래 · 고르기 모달과 같은 폴더 나무(세 층 공용) - const tree = createTreePanel({ layer, canEdit: canEdit(layer, opt.isAdmin), ...opt.tree }); - const treeSection = section(L("M02_TreeTitle"), tree.root); - treeSection.classList.remove("is-collapsed"); const listHost = el("div", { className: "m02-side__lists ui-sidebar-panel", children: [ summary, - treeSection, // 구조물 옵션 보이기 · 도면 양식은 시스템 층(마스터 템플릿)에만 ...(layer === "system" ? [ @@ -201,7 +193,8 @@ export function buildSide(opt: SideOptions): SideHandle { ? mine.map((info) => { const b = el("button", { className: `m02-side__item ui-sidebar-row${isOpen(info) ? " is-active" : ""}`, - text: info.이름, + // 구조물집계표 = 화면 이름 「구조물 조합」(26-7a) + text: info.이름 === STRUCTURE_TABLE ? L("M02_KindTable") : info.이름, attrs: { type: "button", title: info.수정일 }, }); b.addEventListener("click", () => void select(info)); @@ -289,7 +282,7 @@ export function buildSide(opt: SideOptions): SideHandle { if (mine !== seq) return; paintList(); paintButtons(); - await Promise.all([refreshStructure(), matset.refresh(), tree.refresh()]); + await Promise.all([refreshStructure(), matset.refresh()]); } /* --- 시스템 층 새로 · 본떠 · 지우기(도면 양식만 — 구조물 도면은 집계표 열이 정함) --- */ @@ -370,5 +363,5 @@ export function buildSide(opt: SideOptions): SideHandle { } void refresh(); - return { root, refresh, paint, selectItem: tree.select }; + return { root, refresh, paint }; } diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css index 90f5bc75..a26b107c 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css @@ -215,3 +215,53 @@ color: var(--color-text-muted); font-size: 11px; } + +/* 「구조물 조합」(PLAN 26-7c) — 왼쪽 항목 나무(주 · 고치는 자리) · 오른쪽 편집 칸(참조표 · 카드) */ +.m02-shell { + display: flex; + align-items: stretch; + min-width: 0; +} + +.m02-shell > .m02-main { + flex: 1 1 0; +} + +.m02-shell__tree { + display: flex; + flex: 0 0 16rem; + flex-direction: column; + box-sizing: border-box; + max-height: calc(100vh - var(--spacing-64)); + padding: var(--spacing-16) var(--spacing-12); + border-right: 1px solid var(--color-border); + min-width: 0; + overflow: auto; +} + +.m02-shell__tree[hidden] { + display: none; +} + +.m02-shell__title { + margin: 0; + color: var(--color-text); + font-size: var(--text-body); +} + +.m02-shell__tree .m02-tree__body { + max-height: none; +} + +@media (max-width: 720px) { + .m02-shell { + flex-direction: column; + } + + .m02-shell__tree { + flex-basis: auto; + max-height: 40vh; + border-right: 0; + border-bottom: 1px solid var(--color-border); + } +} diff --git a/resources/tester/test_m02_combo_screen.py b/resources/tester/test_m02_combo_screen.py new file mode 100644 index 00000000..b9ee90e0 --- /dev/null +++ b/resources/tester/test_m02_combo_screen.py @@ -0,0 +1,107 @@ +"""관리 화면 「구조물 조합」(PLAN 26-7a~d) — 이름 · 숨김(비고 · 전구간 · 합계) · 참조표 잠금 · +나무 자리. + +① `comboView` · `comboMerge`(Node) — 보일 문서에서 비고 열 · 줄 · 합계 줄을 빼고, [저장] 때 + 저장본 것을 다시 끼움(열 차례 = 저장본 · 보인 고침은 받음). +② 표 부품 `lockHead` — 열 더하기 · 지우기 · 머리 · 층 이름 고치기 없음(단위 · 조회 후 선택은 + 그대로). +③ 좌측 패널에 나무 없음 · 페이지가 나무를 편집 칸 왼쪽(`aside`)에 · 「구조물 조합」일 때만 보임. +""" + +from __future__ import annotations + +import json +import re +import shutil +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +M02 = ROOT / "M02_MasterTemplete" +SHEET = ROOT / "ui_template" / "sheet" + + +def _text(path: Path) -> str: + return path.read_text(encoding="utf-8") + + +@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") +def test_숨긴_것은_저장_때_저장본에서_다시_끼움(): + src = _text(M02 / "M02_MasterTemplete_Structure.ts") + part = re.search( + r"^const HIDDEN_COLUMNS[\s\S]*?^export function comboMerge[\s\S]*?^}", src, re.M + ) + assert part, "comboView · comboMerge 를 못 찾음" + script = ( + "interface StructureColumn { id: string; 머리: (string | null)[]; 꼴?: string | null;" + " 단위?: string }\n" + + part.group(0).replace("export ", "") + + """ +const stored = { + 열: [{ id: "no", 머리: ["NO"] }, { id: "a", 머리: ["가"], 꼴: "수" }, + { id: "b", 머리: ["나"], 꼴: "수" }, { id: "memo", 머리: ["비고"] }], + 줄: [{ id: "all", 값: { memo: "x" } }], 합계줄: [{ id: "sum" }], 변수: { k: 1 }, +}; +const view = comboView(stored); +const edited = structuredClone(view); +edited.열[2].단위 = "m"; +const saved = comboMerge(stored, edited); +console.log(JSON.stringify([ + view.열.map((c) => c.id), view.줄, view.합계줄, stored.열.length, + saved.열.map((c) => c.id), saved.열[2].단위, saved.줄, saved.합계줄, saved.변수, +])); +""" + ) + result = subprocess.run( + ["node", "--experimental-strip-types", "--no-warnings", + "--input-type=module-typescript", "-e", script], + capture_output=True, text=True, encoding="utf-8", timeout=60, + ) # fmt: skip + assert result.returncode == 0, result.stderr + assert json.loads(result.stdout) == [ + ["no", "a", "b"], + [], + [], + 4, # 저장본은 안 바뀜 + ["no", "a", "b", "memo"], + "m", + [{"id": "all", "값": {"memo": "x"}}], + [{"id": "sum"}], + {"k": 1}, + ] + + +def test_참조표는_열_머리를_못_고침(): + sheet = _text(SHEET / "ui_template_sheet.ts") + assert 'const editCols = opts.mode === "master" && !ro && !opts.lockHead;' in sheet + assert "if (editCols) toolbar.append(colDelete);" in sheet + assert "if (editCols) syncLevels(!!col);" in sheet + assert "if (th && editCols) editHead(th);" in sheet + assert "else if (layer && editCols) editLayer(layer);" in sheet + assert 'ro || opts.lockHead ? { hidden: "" }' in sheet + render = _text(SHEET / "ui_template_sheet_render.ts") + assert 'const heads = mode === "master" && !state.headLocked;' in render + main = _text(M02 / "M02_MasterTemplete_UI_Main.ts") + assert "struct ? comboView(doc) : doc" in main + assert "getDoc: () => comboMerge(loaded, shown.getDoc())" in main + + +def test_나무는_편집_칸_왼쪽_좌측_패널엔_없음(): + side = _text(M02 / "M02_MasterTemplete_UI_Side.ts") + assert "createTreePanel" not in side and "M02_TreeTitle" not in side + assert 'info.이름 === STRUCTURE_TABLE ? L("M02_KindTable")' in side # 좌측 줄 이름 + page = _text(M02 / "M02_MasterTemplete_UI_Page.ts") + assert "createTreePanel({" in page and "aside," in page and "tree.select(column)" in page + main = _text(M02 / "M02_MasterTemplete_UI_Main.ts") + assert 'at.kind === "table" && at.name === STRUCTURE_TABLE && at.layer !== "project"' in main + assert 'isCombo(next) ? L("M02_KindTable")' in main # 상세 제목 + assert main.count("showAside();") == 3 # 열 때 · 양식 밖 화면 · 처음 + locale = _text(ROOT / "ui_template" / "ui_template_locale_m2.ts") + assert 'M02_KindTable: ["구조물 조합"' in locale + assert ".m02-shell__tree[hidden]" in _text(M02 / "M02_MasterTemplete_UI_Style.css") + tree = _text(M02 / "M02_MasterTemplete_TreePanel.ts") + assert ( + 'error instanceof StaleError && error.message === "HTTP 409"' in tree + ) # 같은 이름 = 까닭 글 diff --git a/resources/tester/test_m02_layer_screen.py b/resources/tester/test_m02_layer_screen.py index 6bd2f25f..0da4aa4d 100644 --- a/resources/tester/test_m02_layer_screen.py +++ b/resources/tester/test_m02_layer_screen.py @@ -147,5 +147,6 @@ def test_읽기만_표는_단추_고치기_모두_막음(): assert sheet.count("if (ro") + sheet.count("!ro") + sheet.count("ro ||") >= 7 # 열 0 = 「열 더하기」 하나만 켜짐 · 첫 열 assert "b.disabled = !hasCol && !empty" in sheet and "addFirstColumn(" in sheet - assert re.search(r"readOnly,\s*onChange", _text(M02 / "M02_MasterTemplete_UI_Main.ts")) + main = _text(M02 / "M02_MasterTemplete_UI_Main.ts") + assert re.search(r"readOnly,\s*lockHead: !!struct,\s*onChange", main) assert ".ui-sheet__toolbar[hidden]" in _text(SHEET / "ui_template_sheet.css") diff --git a/resources/tester/test_m02_structure_panel.py b/resources/tester/test_m02_structure_panel.py index fd7e130f..1df1c34d 100644 --- a/resources/tester/test_m02_structure_panel.py +++ b/resources/tester/test_m02_structure_panel.py @@ -153,7 +153,7 @@ def test_좌측_구조물_집계_양식_아래_하위_넷_새로_본떠_지우 # 구조물 도면 · 면적산출 · 재료산출 · 자재 조합 = 구조물 집계 양식 컨테이너 안(같이 접힘) summary = side.split("const summary = section(")[1].split(");")[0] assert "COLUMN_KINDS.map" in summary and "matsetSection" in summary - assert 'M02_KindTable: ["구조물 집계 양식"' in _text( + assert 'M02_KindTable: ["구조물 조합"' in _text( # 화면 이름(26-7a) ROOT / "ui_template" / "ui_template_locale_m2.ts" ) diff --git a/resources/tester/test_m02_tree.py b/resources/tester/test_m02_tree.py index 16f5e39d..90bce3ee 100644 --- a/resources/tester/test_m02_tree.py +++ b/resources/tester/test_m02_tree.py @@ -220,27 +220,32 @@ def test_판_409_권한_403(world): def test_표_저장과_나무가_한_줄기(world): - """집계표 표에서 머리를 고치면 경로가 따라오고, 나무 고침 뒤 표에도 같은 자료가 보임.""" + """정본 = 나무(PLAN 26-7e) — 표 [저장]이 보낸 머리 · 층 이름 · 빈 폴더는 안 받음(저장본 경로 + 그대로) · 나무 고침 뒤 표 머리가 경로에서 다시 만들어짐.""" admin = _as(world, "SYSTEM_ADMIN") t = Tree(admin, "system") rv = next(r for r in t.now["items"] if r["열"] == "rv") + path = rv["경로"] url = f"/api/m02/layers/system/templates/{TABLE}" got = admin.get(url).json() col = next(c for c in got["문서"]["열"] if c["id"] == "rv") - col["머리"] = ["돌기슭막이", None, "찰쌓기", None] # 빈 칸은 건너뜀 + col["머리"] = ["돌기슭막이", None, "찰쌓기", None] + col["단위"] = "㎡" # 머리 밖 칸은 받음 + got["문서"]["층"] = ["가", "나", "다", "라"] + got["문서"]["폴더"] = [["표에서 만든 폴더"]] assert admin.put(url, json=got).status_code == 200 + table = admin.get(url).json()["문서"] + col = next(c for c in table["열"] if c["id"] == "rv") + assert col["경로"] == path and col["단위"] == "㎡" + assert table["층"] == ["종류", "분류1", "분류2", "분류3"] and table["폴더"] == [] t = Tree(admin, "system") - assert t.item(["돌기슭막이", "찰쌓기"])["key"] == rv["key"] + assert t.item(path)["key"] == rv["key"] t.do(op="rename", 키=rv["key"], 이름="메쌓기") table = admin.get(url).json()["문서"] col = next(c for c in table["열"] if c["id"] == "rv") - assert col["경로"] == ["돌기슭막이", "메쌓기"] and col["머리"] == [ - "돌기슭막이", - "메쌓기", - None, - None, - ] - # 표에서 열을 지우면 나무에서도 빠지고 파일도 같이 + assert col["경로"] == [*path[:-1], "메쌓기"] + assert col["머리"] == [*path[:-1], "메쌓기", *[None] * (4 - len(path))] + # (화면엔 단추 없음 · API 호환) 표 문서에서 열을 빼고 저장하면 나무에서도 빠지고 파일도 같이 table["열"] = [c for c in table["열"] if c["id"] != "rv"] assert ( admin.put(url, json={"판": admin.get(url).json()["판"], "문서": table}).status_code == 200 diff --git a/ui_template/sheet/ui_template_sheet.ts b/ui_template/sheet/ui_template_sheet.ts index c91e3704..7cab951e 100644 --- a/ui_template/sheet/ui_template_sheet.ts +++ b/ui_template/sheet/ui_template_sheet.ts @@ -10,6 +10,8 @@ * 치지 않고 조회 후 선택. * `readOnly` = 볼 수만 있음 — 단추 줄 · 안내 글 숨김 · 칸 고치기 · 머리 고치기 · 열 너비 끌기 막음 · * 열이 하나도 없으면 [열 더하기] 가 켜져 첫 열을 만듦(머리 글 바로 고치기). + * `lockHead` = 열 · 머리는 밖(나무)이 정함 — 열 더하기 · 지우기 단추 없음 · 머리 · 층 이름 + * 못 고침(단위 · 일위대가 · 조회 후 선택은 그대로 · PLAN 26-7d). * project = 프로젝트 표 — 바인딩 · 계산 열 잠금 · 손 열만 입력 · 「전구간」 줄 맨 위 · 쪽줄마다 쪽. * 칸을 고치면 문서를 그 자리에서 바꾸고 같은 풀이(`_recalc`)로 즉시 다시 그림 — 서버 왕복 없음. * 저장은 부른 쪽 몫(`onChange` 로 문서를 받음 · 자동저장 없음 · [저장] 때 서버가 Node 로 다시 풂). @@ -49,6 +51,8 @@ export interface SheetOptions { mode: SheetMode; /** 볼 수만 있음(단추 · 고치기 없음) */ readOnly?: boolean; + /** master — 열 더하기 · 지우기 · 머리 · 층 이름 고치기 없음(열 · 머리는 나무가 정함) */ + lockHead?: boolean; onChange?: (doc: SheetDoc) => void; /** 도면 줄 칸 글(마스터 첫 쪽 · 열 id) — 없거나 빈 글이면 「없음」 · 글이 바뀌면 `recalc()` 로 다시 그림. */ drawingLabel?: (colId: string) => string; @@ -84,10 +88,15 @@ export function createSheet(host: HTMLElement, input: SheetDoc, opts: SheetOptio areaLabel: opts.areaLabel, materialLabel: opts.materialLabel, pick: !!opts.onPick, + headLocked: !!opts.lockHead, }; const ro = !!opts.readOnly; + const editCols = opts.mode === "master" && !ro && !opts.lockHead; // 열 · 머리 고치기 const scroll = el("div", { className: "ui-sheet__scroll" }); - const toolbar = el("div", { className: "ui-sheet__toolbar", attrs: ro ? { hidden: "" } : {} }); + const toolbar = el("div", { + className: "ui-sheet__toolbar", + attrs: ro || opts.lockHead ? { hidden: "" } : {}, + }); const root = el("div", { className: `ui-sheet ui-sheet--${opts.mode}`, attrs: { tabindex: "0" }, @@ -133,7 +142,7 @@ export function createSheet(host: HTMLElement, input: SheetDoc, opts: SheetOptio state.sel = state.sel && next ? { r: state.sel.r, c: next.id } : null; changed(); }); - if (opts.mode === "master" && !ro) toolbar.append(colDelete); + if (editCols) toolbar.append(colDelete); // 층마다 열 더하기 단추 — 층이 둘 이상이면 둘째 층부터(「분류1 더하기」 …) · 하나면 「열 더하기」 · // 층 이름 · 층 수가 바뀌면 다시 만듦 @@ -159,7 +168,7 @@ export function createSheet(host: HTMLElement, input: SheetDoc, opts: SheetOptio const syncToolbar = (): void => { const col = state.sel ? colOf(state.sel.c) : undefined; - if (opts.mode === "master" && !ro) syncLevels(!!col); + if (editCols) syncLevels(!!col); colDelete.disabled = !col || state.doc.열.length <= 1; }; @@ -295,8 +304,8 @@ export function createSheet(host: HTMLElement, input: SheetDoc, opts: SheetOptio const th = target.closest("th[data-col]"); const layer = target.closest("th[data-layer]"); if (ro) return; - if (th && state.mode === "master") editHead(th); - else if (layer && state.mode === "master") editLayer(layer); + if (th && editCols) editHead(th); + else if (layer && editCols) editLayer(layer); else if (target.closest("td[data-r]")) edit(); }; const onKey = (event: KeyboardEvent): void => { diff --git a/ui_template/sheet/ui_template_sheet_render.ts b/ui_template/sheet/ui_template_sheet_render.ts index 0ae57807..90e4834c 100644 --- a/ui_template/sheet/ui_template_sheet_render.ts +++ b/ui_template/sheet/ui_template_sheet_render.ts @@ -45,6 +45,8 @@ export interface RenderState { materialLabel?: (colId: string) => string; /** 조회 후 선택 줄(`PICK_ROWS`)을 고름으로 받나 — 칸 겉모양만. */ pick?: boolean; + /** 머리 · 층 이름을 못 고침(`lockHead`) — 칸 겉모양만. */ + headLocked?: boolean; } /** 도면 · 면적산출 · 재료산출 줄 칸 글. */ @@ -194,7 +196,8 @@ export function renderSheet(state: RenderState): HTMLElement { const thead = (): HTMLElement => { const rows = head.map((cells, level) => { - const layer = gutter("th", doc.층?.[level] ?? "", mode === "master" ? "is-editable" : ""); + const heads = mode === "master" && !state.headLocked; + const layer = gutter("th", doc.층?.[level] ?? "", heads ? "is-editable" : ""); layer.dataset.layer = String(level); const tr = el("tr", { children: [layer] }); for (const cell of cells) { @@ -208,7 +211,7 @@ export function renderSheet(state: RenderState): HTMLElement { "data-col": cols[cell.col].id, }, }); - if (mode === "master") th.classList.add("is-editable"); + if (heads) th.classList.add("is-editable"); freeze(th, cell.col, cell.colspan); tr.append(th); } diff --git a/ui_template/ui_template_locale_m2.ts b/ui_template/ui_template_locale_m2.ts index e6db41f1..595678b6 100644 --- a/ui_template/ui_template_locale_m2.ts +++ b/ui_template/ui_template_locale_m2.ts @@ -6,7 +6,7 @@ export const ui_locales_m2 = { B01_Dashboard_MasterTemplate: ["마스터 템플릿", "Master Templates"], M02_Title: ["마스터 템플릿", "Master Templates"], - M02_KindTable: ["구조물 집계 양식", "Structure summary templates"], + M02_KindTable: ["구조물 조합", "Structure combos"], M02_KindDrawing: ["도면 양식", "Drawing templates"], M02_KindStructure: ["구조물 도면", "Structure drawings"], M02_KindArea: ["면적산출", "Area takeoff"], @@ -66,11 +66,11 @@ export const ui_locales_m2 = { "Company templates are read-only — copy the selected column to your own to edit", ], M02_CopyToPersonal: ["개인용으로 복사", "Copy to personal"], - M02_CopyPickFirst: ["복사할 열을 집계표에서 먼저 고름", "Pick a column in the table first"], + M02_CopyPickFirst: ["복사할 열을 구조물 조합에서 먼저 고름", "Pick a column in the table first"], M02_CopyDone: ["개인용으로 복사함 — 구조물 관리(개인)에서 고침", "Copied — edit it in your own"], M02_CopyFailed: ["복사 안 됨 — {value}", "Copy failed — {value}"], M02_NeedCompany: [ - "회사부터 등록해야 구조물 집계표를 만들 수 있음", + "회사부터 등록해야 구조물 조합을 만들 수 있음", "Register a company first to manage structure templates", ], M02_NeedCompanyGo: ["회사 등록하러 가기", "Go to company registration"], From ce4ea7069e2f2184900d719af455db735f6a044e Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 30 Sep 2026 10:48:39 +0900 Subject: [PATCH 18/18] auto: 2026-09-30 10:48 (EOMSANGDON-HOME) --- docs/raw/M01_점검/대조방식.md | 20 ++ docs/raw/M01_점검/보고.md | 529 +++++++++++++++++++++++++++++++++ docs/raw/M01_점검/보고_줄글.md | 437 +++++++++++++++++++++++++++ docs/raw/M01_점검/진행.md | 20 ++ 4 files changed, 1006 insertions(+) create mode 100644 docs/raw/M01_점검/대조방식.md create mode 100644 docs/raw/M01_점검/보고.md create mode 100644 docs/raw/M01_점검/보고_줄글.md create mode 100644 docs/raw/M01_점검/진행.md diff --git a/docs/raw/M01_점검/대조방식.md b/docs/raw/M01_점검/대조방식.md new file mode 100644 index 00000000..5f711f5e --- /dev/null +++ b/docs/raw/M01_점검/대조방식.md @@ -0,0 +1,20 @@ +# M01 원문·자료 표 대조 방식 + +2026-09-30 인계용. 새 판정 없이 앞서 수행한 대조 절차와 확인 사례만 정리한다. `진행.md`의 `끝남` 11표 외에는 숫자나 행 수가 같아도 검증 완료로 보지 않는다. + +1. **원문 찾기:** 로직·소요량·계수 JSON의 `출처`와 절 번호를 기준으로 `resources/knowledge/original/원가계산/` 본문(md)에서 해당 절과 표의 앞뒤 주석까지 찾는다. 제목만 같아도 연도·절·표 번호가 다르면 별개로 취급한다. 원문 인용과 파일·절을 함께 기록한다. +2. **병합 칸 풀기:** 원문의 행 병합·열 병합은 생략된 값을 임의로 빈 칸으로 세지 않는다. 위 행에서 이어지는 규격·조건·단위, 두 줄에 걸친 이름, 소계·주석·계산식 여부를 식별해 비교용 가상 행으로 펼친다. 반복 표 머리말은 데이터 행에서 제외한다. +3. **피벗:** 원문의 한 물리 행에 좌·우 구분이나 여러 값 칸이 있으면 각각 독립 비교 행으로 편다. 역으로 JSON 여러 행이 원문 한 행의 서로 다른 열을 나타내면 같은 원문 행에 짝을 둔다. 따라서 물리 줄 수 차이만으로 누락 판정하지 않는다. +4. **로직 줄 짝짓기:** 원문 절·표·규격·조건·비목·장비/재료명을 복합 키로 삼아 `소요량_*`, `계수_*` 행과 `로직_*.json`의 찾기(QC/QF) 및 호표(LB/EQ/MT/GC/GF)를 연결한다. 순서가 같은 줄이라는 이유만으로 짝짓지 않는다. 중복 규격은 주변 조건·단위와 수치까지 대조한다. +5. **단위와 값:** 원문·자료의 각 수치를 같은 행의 칸 이름과 단위로 비교한다. `㎥/m³`, `㎡/m²` 등 표기만 다른 단위는 정규화하되 `㎾h`, `L`, `kg`, `인`, `시간`, `대`는 서로 바꾸지 않는다. 옆 칸·옆 행의 값이 들어간 경우는 값 자체가 원문 어딘가에 있어도 오류다. +6. **배수:** 원문 기준량(`100㎡ 당`, `10m 당`, `1,000㎡ 당` 등)과 로직 결과단위·입력단위·수량 식을 함께 본다. 원문 원단위를 유지하는 결과라면 배수 나눔이 불필요할 수 있고, 결과를 `㎡`·`m` 등 단위당으로 바꾸면 해당 수량을 100·10·1,000으로 나눠야 한다. 중복 나눔과 누락 모두 검토한다. +7. **비목:** 원문 표의 노무·재료·경비 구분과 호표 요소가 실제 가격표에서 들어가는 비목을 맞춘다. 기계 이름이라고 무조건 경비, 소모품이라고 무조건 재료로 가정하지 않고 원문의 산출 구분과 주석을 우선한다. +8. **판정:** 원문 동일 절·행·칸과 JSON/로직의 대응을 직접 확인한 차이만 `확실`; 병합·피벗·타 절 재사용·판본 차이·조건 해석이 남으면 `의심`으로 둔다. 원문에 수치 표가 있지만 독립 로직이 안 보이는 절은 `누락 확정 / 참고·계수라 로직 불필요 / 다른 로직이 맡음 / 사람 확인`으로 갈라 적는다. 근거 한 줄, 키, 이름, 원문 파일·절, 갈래를 `보고.md`에 넣고 한 흠당 한 줄을 `보고_줄글.md`에 반영한다. + +자주 틀리는 자리는 병합된 마지막 행, 좌·우 피벗을 물리 행 수로 오판하는 자리, 표 머리말·소계의 혼입, 같은 숫자의 옆 열 이동, 단위 칸의 옆 줄 복사, 다른 절의 동명 표, `100㎡ 당` 배수의 이중 적용, 그리고 검색어가 동명 이종 자재를 먼저 찾는 자리다. 재료 검색어는 자재품목표에서 **정확한 품목이 존재하는지**와 **현재 검색어가 무엇을 실제로 찾는지**를 따로 기록한다. + +## 이미 확인한 세 예 + +1. **산림 10-6-1, QF000308:** 원문 한 행의 좌·우 값을 각각 펼쳐 비교했다. 원문 4개 물리 행이 자료 8개 값 행에 대응하므로 `4 ≠ 8`만으로 빠진 줄이라고 판정하지 않았다. 조건·값 짝짓기는 앞선 검수에서 완료로 기록했다. +2. **산림 10-4, QF000307:** 병합·피벗을 풀어도 원문 장비 16행에 자료 15행만 있었다. 끝의 자주식 `크레인(트럭 10TON)`이 빠진 것을 원문 이름과 해당 절로 직접 확인해 `확실` 누락으로 `보고.md`와 `보고_줄글.md`에 기록했다. +3. **GC000943 Tar Epoxy:** 원문의 재료량·동력량 칸을 별개로 보아야 하는데 자료에서 `550`과 `㎾h`가 Tar Epoxy 재료 줄에 들어간 유형이다. 값 존재만으로 합격시키지 않고 원문 열 위치, 재료 단위, 전력 비목 및 호표 참조를 각각 대조해야 한다. 앞선 보고의 확정/의심 표시를 그대로 따른다. diff --git a/docs/raw/M01_점검/보고.md b/docs/raw/M01_점검/보고.md new file mode 100644 index 00000000..5dd7690a --- /dev/null +++ b/docs/raw/M01_점검/보고.md @@ -0,0 +1,529 @@ +# M01 단가산출 로직 읽기 전용 점검 + +- 점검일: 2026-09-30. 대상: `resources/master_data/로직_*.json` 54파일, 1,354줄 및 직접 참조 마스터·원문 본문. +- `check_master.py 전부`: 틀 0, 본문 0, 로직 0, 조합 0, 단위 경고 15. 검사의 `본문`은 **표형 요소의 숫자**를 원문 절과 비교하며, 로직의 의미나 표의 행 조합을 전수 검증하지 않는다. +- 아래 `확실`은 원문 표와 로직 값을 직접 맞댄 것. `의심`은 도구 경고이며 실제 오류 또는 가격 마스터 후보 미비 여부를 더 확인해야 한다. 같은 키의 서로 다른 문제는 한 행에 묶었다. + +## 확인된 차이 + +| 로직 키 | 이름 | 원문 자리(파일 · 절) | 갈래 | 무엇이 다름 | 판정 | 근거 인용 한 줄 | +|---|---|---|---|---|---|---| +| GC000181 | 매트부설(육상) | 건설공사_표준품셈/본문/01_공통부문/제05장_기초공사/5-02_연약지반처리.md · 5-2-1 | 연결 | P.P로프(9㎜) 재료 검색어가 `와이어로프(철심) 9㎜`로, 재질과 품명이 다르다. | 확실 | 원문: `P.P로프(9㎜) (m)`; 로직: `검색어: 와이어로프(철심) 9㎜`. | +| GC000027 | 강관 조립말비계(이동식) 설치 및 해체 | 건설공사_표준품셈/본문/01_공통부문/제02장_가설공사/2-07_비계.md · 2-7-6 | 연결 | `수평띠장 L1829`가 안전발판을 검색한다. | 확실 | 원문: `수평띠장 · L1829 · 개 · 4`; 로직 검색어: `안전발판(PSP대용) 250×3000`. | +| GC000348 | 터널 방수 | 건설공사_표준품셈/본문/02_토목부문/제03장_터널공사/3-04_부대공.md · 3-4-1 | 연결 | 일체식 방수시트가 방수석고보드를 검색한다. | 확실 | 원문: `일체식 방수시트 · ㎡ · 1.15`; 로직 검색어: `방수석고보드 12.5t`. | +| GC000360 | 레일 가스압접 | 건설공사_표준품셈/본문/02_토목부문/제04장_궤도공사/4-05_궤도용접.md · 4-5-1 | 연결 | 바퀴숫돌 단면용의 가격 검색어가 도로청소용 빗자루다. 같은 방식으로 다른 바퀴숫돌 규격도 연결되어 있다. | 확실 | 원문: `바퀴숫돌 · 단면용 A36m B11호… · 개`; 로직 검색어: `빗자루 도로청소용`. | +| GC000560 | 철골 공장용접(기본용접공수) | 건설공사_표준품셈/본문/03_건축부문/제01장_철골공사/1-01_철골_가공_조립(공장생산).md · 1-1-3 | 연결 | CO₂ 와이어·탄산가스·잠호용접와이어의 가격 검색어가 모두 아세틸렌이다. | 확실 | 원문: `CO_2 와이어 · ㎏`, `탄산가스 · ㎏`, `잠호용접와이어 · ㎏`; 로직: `아세틸렌 98%`. | +| GC000575 | 벽돌 쌓기(시멘트벽돌) | 건설공사_표준품셈/본문/03_건축부문/제02장_조적공사/2-01_벽돌.md · 2-1-1 | 연결 | 시멘트벽돌 가격 검색어가 주차라인블록이다. | 확실 | 원문: `본 품은 시멘트 벽돌(19×9×5.7㎝) 쌓기 기준`; 로직 검색어: `주차라인블록 1000X300X150`. | +| GC000604 | 카페트 설치 | 건설공사_표준품셈/본문/03_건축부문/제05장_수장공사/5-01_바닥.md · 5-1-2 | 연결 | 카페트 가격 검색어가 불연천장재(마이톤)다. | 확실 | 원문: `카페트 · ㎡ · 1.1`; 로직 검색어: `불연천장재(마이톤) 12mm`. | +| GC000723 | 동관 용접접합 | 건설공사_표준품셈/본문/04_기계설비부문/제01장_배관공사/1-02_동관.md · 1-2-1 | 연결 | 동 및 동합금용 용접봉과 플럭스가 둘 다 아세틸렌을 검색한다. | 확실 | 원문 표 머리: `용접봉(g) · 플럭스(g) · 산소(ℓ) · 아세틸렌(g)`; 앞의 두 재료 검색어: `아세틸렌 98%`. | +| GC000938 | Stop-Log 설치 | 건설공사_표준품셈/본문/04_기계설비부문/제13장_플랜트설비공사/13-06_수력발전_기계설비.md · 13-6-6 | 연결·단위 | `광명단` 재료가 `목재전용도료 오일스테인`을 검색한다. 아세틸렌 원문은 `병` 수량 1.98인데 호표는 `kg` 1.98로 환산 없이 사용한다. | 확실 | 원문: `아세틸렌 … 병 1.98`, `광명단 … ℓ 2.2`. | +| GC000943 | Roller Gate Guide Metal 제작 | 같은 파일 · 13-6-11 | 연결·단위 | `Tar Epoxy` 호표에 원문의 전기 `550㎾h`를 그대로 넣고 `Tar Epoxy` 원문 `0.75ℓ`를 누락했다. 아세틸렌도 원문 `1.6병`을 `1.6kg`으로 사용한다. | 확실 | 원문: `Tar Epoxy … 0.75ℓ`, `전기 … 550㎾h`, `아세틸렌 … 1.6병`. | +| GC000948 | Tainter Gate Anchorage 제관 | 같은 파일 · 13-6-17 | 연결·단위 | `Tar Epoxy` 호표에 전력 `420㎾h`를 넣고 원문의 `3.0ℓ`를 누락했다. 아세틸렌 원문 `1.5병`도 `1.5kg`으로 사용한다. | 확실 | 원문: `Tar Epoxy … 3.0ℓ`, `전력 … 420㎾h`, `아세틸렌 … 1.5병`. | +| GC000993 | 플랜트 공사별 설치 소모자재(참고) | 건설공사_표준품셈/본문/04_기계설비부문/제13장_플랜트설비공사/13-11_기타_기계설비.md · 13-11-5 | 연결 | 용접봉(전기·산소) 모두 아세틸렌을, 사포는 복사용지를, 걸레는 그리스를, 용접면·용접홀다·용접앞치마는 노면파쇄기 팁을, 페인트붓은 도로청소용 빗자루를 검색한다. 원문 품명과 재료 가격 연결이 다르다. | 확실 | 원문 표의 품명 줄: `용접봉(전기)`, `용접봉(산소)`, `사포`, `걸레`, `용접면`, `용접홀다`, `용접앞치마`, `페인트붓(표기용)`. | + +## 검사 도구의 단위 경고 (15곳, 모두 의심) + +| 로직 키 | 이름 | 원문 자리(파일 · 절) | 갈래 | 무엇이 다름 | 판정 | 근거 인용 한 줄 | +|---|---|---|---|---|---|---| +| GC000027 | 강관 조립말비계(이동식) 설치 및 해체 | 건설공사_표준품셈/본문/01_공통부문/제02장_가설공사/2-07_비계.md · 2-7-6 | 단위 | 가새 L1518-2개 `조` 후보 0 | 의심 | 도구: `단위 「조」 후보 0`. | +| GC000181 | 매트부설(육상) | 5-02_연약지반처리.md · 5-2-1 | 단위 | 철근(19㎜) `m` 후보 0 | 의심 | 도구: `단위 「m」 후보 0`. | +| GC000616 | 걸레받이 설치 | 건설공사_표준품셈/본문/03_건축부문/제05장_수장공사/5-03_벽.md · 5-3-5 | 단위 | 중밀도섬유판 걸레받이 `m` 후보 0 | 의심 | 도구: `단위 「m」 후보 0`. | +| GC000211 | 합판거푸집 설치 및 해체 | 건설공사_표준품셈/본문/01_공통부문/제06장_철근콘크리트공사/6-03_거푸집.md · 6-3-1 | 단위 | 폼타이 12.7·9.5·7.9㎜의 `본` 후보 각각 0 (3곳) | 의심 | 도구: `폼타이 … 단위 「본」 후보 0`. | +| GC000855 | 가스용 강관 나사식 접합 및 배관 | 건설공사_표준품셈/본문/04_기계설비부문/제11장_가스설비공사/11-01_강관.md · 11-1-3 | 단위 | 스레트실테이프 `㎝` 후보 0 | 의심 | 도구: `스레트실테이프 … 단위 「㎝」 후보 0`. | +| GC000859 | 직독식 가스미터 설치 | 건설공사_표준품셈/본문/04_기계설비부문/제11장_가스설비공사/11-03_부속기기.md · 11-3-3 | 단위 | 스레트실테이프 `㎝` 후보 0 | 의심 | 도구: `스레트실테이프 … 단위 「㎝」 후보 0`. | +| GC000938 | Stop-Log 설치 | 13-06_수력발전_기계설비.md · 13-6-6 | 단위 | 함석 `대` 후보와 단위 성격 다름. 원문 역시 `대`; 가격 마스터 문제 가능. | 의심 | 원문: `함석 … 대 0.53`; 도구: `후보 단위가 줄 단위 「대」 과 성격이 다름`. | +| GC000943, GC000948 | 수력발전 게이트 제작 | 같은 파일 · 13-6-11, 13-6-17 | 단위 | 중유 `D/M` 후보 단위가 다름 (2곳) | 의심 | 원문: `중유 … 0.0018D/M`, `중유 … 0.019D/M`. | +| GC000948 | Tainter Gate Anchorage 제관 | 같은 파일 · 13-6-17 | 단위 | Zinc primer `ℓ` 후보 단위가 다름 | 의심 | 원문: `Zinc primer … 0.36ℓ`. | +| GC000993 | 설치 소모자재(참고) | 13-11_기타_기계설비.md · 13-11-5 | 단위 | 용접장갑 `족` 후보 0 | 의심 | 도구: `용접장갑 … 단위 「족」 후보 0`. | +| GF000160 | 합판거푸집 | 산림_표준품셈/본문/제12장_철근콘크리트/12-04_합판거푸집.md · 12-4 | 단위 | 폼타이 `본` 후보 0 | 의심 | 도구: `폼타이(D형 1/2인치) … 단위 「본」 후보 0`. | +| GF000165 | 콘크리트 포장 거푸집 | 산림_표준품셈/본문/제12장_철근콘크리트/12-08_콘크리트_포장_거푸집.md · 12-8 | 단위 | 철재 거푸집 손료 `본` 후보 0 | 의심 | 도구: `철재 거푸집 손료(1본 3m) … 단위 「본」 후보 0`. | + +## 범위와 부문별 요약 + +`점검 수`는 도구가 구조·참조·표 숫자를 검사한 로직 줄 수다. 사람이 원문 표의 모든 규격·조건·비목을 확인한 수가 아니다. `흠 수`는 위 표의 **확실한 문제가 있는 로직 키 수 / 단위 경고 발생 곳 수**이며 같은 키가 양쪽에 있을 수 있다. + +| 부문 | 로직 수 | 점검 수(기계) | 흠 수(확실 / 의심 경고) | +|---|---:|---:|---:| +| 건설품셈 공통 | 276 | 276 | 2 / 5 | +| 건설품셈 토목 | 282 | 282 | 2 / 0 | +| 건설품셈 건축 | 163 | 163 | 3 / 1 | +| 건설품셈 기계설비 | 272 | 272 | 5 / 7 | +| 건설품셈 유지관리 | 110 | 110 | 0 / 0 | +| 산림품셈 | 251 | 251 | 0 / 2 | +| 합계 | 1,354 | 1,354 | 12 / 15 | + +원문 직접 대조한 절: 건설 공통 2-7-6·5-2-1, 토목 3-4-1·4-5-1, 건축 1-1-3·2-1-1·5-1-2, 기계설비 1-2-1·13-6-6·13-6-11·13-6-17·13-11-5, 산림 13-4-1. 각 절에서도 지적 항목 중심으로 대조했다. 이외 절의 로직별 직종·기계·재료, 조건 조합, 비목, 배수 환산은 **미완료**다. 따라서 0건 부문도 무결 판정이 아니다. + +추가 점검: 모든 로직 출처를 `section_of()`로 찾아 1,354줄 중 8줄은 정확한 소제목을 반환하지 않았다(GC000482·483·484·485·487·506·511, GF000183). 원문에 없는 로직이라는 뜻은 아니다. 이 8줄은 제목 표기·하위 번호를 수동 대조해야 한다. 원문 절마다 로직이 필요한지의 판정도 아직 하지 않았다. 산림 본문 `###` 소제목 274개와 산림 로직 고유 원문번호 212개의 단순 집합 비교는 계수·설명 절과 부모 번호가 섞여 누락 건수로 쓸 수 없다. + +검사 도구의 0건은 원문 표의 **숫자 집합**이 맞는다는 뜻이다. 표 행별 규격·조건 조합 누락은 이 검사로 배제할 수 없다. 특히 위에서 확인한 `GC000943`의 550㎾h가 Tar Epoxy에 연결된 문제도 숫자 자체가 원문에 있으므로 통과한다. + +## 2차 전수 대조 · 산림품셈 + +범위: 산림 로직 251줄, 재료 호표 128줄, 소요량·계수 표 634개(저장 줄 4,159개). 재료 128줄의 호표 이름·검색어를 모두 펼쳐 대조했고 아래는 1차 보고와 겹치지 않는 차이다. 원문 절의 수치 포함 여부와 로직 결과단위·배수 식도 251줄을 검색했다. 원문 `100m당`인 `GF000163`의 블레이드 `0.31/100`, 물 `3000/100`은 `원/m`에 맞는다. 표의 숫자 집합 검사는 0건이지만, 원문 Markdown의 병합 표와 마스터의 긴 줄을 행별로 역변환하지 못하므로 **4,159줄의 규격·조건 조합이 모두 있는지는 아직 판정할 수 없다**. + +| 로직 키 | 이름 | 원문 자리(파일 · 절) | 갈래 | 무엇이 다름 | 판정 | 근거 인용 한 줄 | +|---|---|---|---|---|---|---| +| GF000060 | 목재칩 포설 | 산림_표준품셈/본문/제06장_생육보조/6-08_목재칩_포설.md · 6-8 | 연결 | 우드칩 가격 검색어가 미송각재다. | 확실 | 호표 `우드칩`; 검색어 `미송각재 30×30×3600mm 서울`. | +| GF000143 | 케이블 크레인 운반대 설치 | 산림_표준품셈/본문/제10장_자재․장비_운반/10-08_케이블_크레인_운반.md · 10-8-2 | 연결 | 소재(원목)가 제재 각재를 검색한다. | 확실 | 호표 `소재(원목)`; 검색어 `미송각재 30×30×3600mm 서울`. | +| GF000161 | 문양거푸집 설치 및 해체 | 산림_표준품셈/본문/제12장_철근콘크리트/12-05_문양거푸집(0~7m).md · 12-5 | 연결 | 문양 스티로폴이 그라스울 샌드위치판넬을 검색한다. | 확실 | 호표 `문양 스티로폴`; 검색어 `샌드위치판넬(그라스울) T50`. | +| GF000163 | 포장절단(1차 절단) | 산림_표준품셈/본문/제12장_철근콘크리트/12-07_포장절단_및_줄눈설치.md · 12-7-1 | 연결 | 커터 블레이드가 굴착기 치즐을 검색한다. | 확실 | 원문 `100m당 블레이드 0.31개`; 호표 검색어 `치즐(0.2㎥)`. | +| GF000172 | 맹암거 | 산림_표준품셈/본문/제12장_철근콘크리트/12-10_맹암거.md · 12-10 | 연결 | 부직포가 직포를, 잡석이 강모래를 검색한다. | 확실 | 호표 `부직포`, `잡석 구입`; 검색어 `직포 옥상방수용직포`, `모래 강모래`. | +| GF000180 | 집수정 | 산림_표준품셈/본문/제12장_철근콘크리트/12-15_집수정.md · 12-15 | 연결 | 집수정 뚜껑(스틸그레이팅)이 PE빗물받이 뚜껑을 검색한다. | 확실 | 호표 `집수정 뚜껑(스틸그레이팅)`; 검색어 `PE빗물받이스틸뚜껑 495×395×50T`. | +| GF000181 | 맨홀 | 산림_표준품셈/본문/제12장_철근콘크리트/12-16_맨홀.md · 12-16 | 연결 | 원형거푸집 두 줄이 폼타이를 검색한다. | 확실 | 호표 `원형거푸집(PE 10회)` 두 줄; 검색어 `폼타이 D형`. | +| GF000191 | 부직포설치 | 산림_표준품셈/본문/제12장_철근콘크리트/12-23_부직포설치.md · 12-23 | 연결 | 부직포가 직포를 검색한다. | 확실 | 호표 `부직포`; 검색어 `직포 옥상방수용직포`. | +| GF000198 | 실런트(20×25㎜) | 산림_표준품셈/본문/제12장_철근콘크리트/12-27_지수판.md · 12-27-3 | 연결 | 실런트가 수팽창 지수재를 검색한다. | 의심 | 호표 `실런트`; 검색어 `쉬트파일수팽창지수재 DPS-500` — 대체재 동등성 근거 미확인. | +| GF000199 | 신구 BOX접합 | 산림_표준품셈/본문/제12장_철근콘크리트/12-28_신구_BOX접합.md · 12-28 | 연결 | 콘크리트용 에폭시 접착제는 석고본드를, 시너는 목재전용 오일스테인을 검색한다. | 확실 | 호표 `에폭시 접착제(콘크리트용)`, `시너`; 검색어 `석고본드`, `목재전용도료 오일스테인`. | +| GF000201 | 다웰바 설치 신축 이음부 | 산림_표준품셈/본문/제12장_철근콘크리트/12-30_다웰바_설치.md · 12-30-1 | 연결·단위 | 강봉 D32가 트러스를 검색하고, PVC 파이프 Ø35㎜는 D30㎜ 제품을 검색한다. | 확실 | 호표 `강봉(D32㎜)`, `P.V.C 파이프(Ø35㎜)`; 검색어 `트러스`, `D=30mm`. | +| GF000202 | 다웰바 설치 접속스라브부 | 같은 파일 · 12-30-2 | 연결·단위 | 원형철근 Ø29는 D25 가공비를, 탄성고무받침은 익스팬션조인트휠러를, 다웰바캡은 보강재를 검색한다. | 확실 | 호표 `원형철근(∅29㎜)`, `탄성고무받침`, `다웰바캡`; 검색어 `D25mm`, `익스팬션조인트휠러`, `SRR 보강재`. | +| GF000203 | 물끊기 홈(NOTCH) 설치 | 산림_표준품셈/본문/제12장_철근콘크리트/12-31_물끊기_홈(NOTCH)_설치.md · 12-31 | 연결 | 물끊기 홈 재료가 방수석고보드를 검색한다. | 확실 | 호표 `물끊기 홈(NOTCH) 재료`; 검색어 `방수석고보드 12.5t`. | +| GF000220 | 돌쌓기 메쌓기(장비) | 산림_표준품셈/본문/제13장_구조물/13-04_돌쌓기.md · 13-4-2 | 연결 | 채움재 잡석이 강모래를 검색한다. | 확실 | 원문 `잡석을 채움재로 사용하는`; 호표 검색어 `모래 강모래`. | +| GF000224 | 돌붙임 메붙임(장비) | 산림_표준품셈/본문/제13장_구조물/13-05_돌붙임.md · 13-5-2 | 연결 | 채움재 잡석이 강모래를 검색한다. | 확실 | 호표 `잡석(채움재)`; 검색어 `모래 강모래`. | +| GF000235 | 사각형 돌망태 | 산림_표준품셈/본문/제13장_구조물/13-11_돌망태.md · 13-11-4 | 연결 | 철망태가 기초사석을 검색한다. | 확실 | 호표 `철망태`; 검색어 `기초사석 0.015~0.03㎥`. | + +원문에 수치 표가 있고 같은 절의 전용 로직·표의 `용도.로직키`가 모두 없는 절 후보는 **81개**다. 표형 데이터만 보관하려는 절도 섞일 수 있으므로 누락 판정은 `의심`이다. 원문번호별 목록(괄호는 후보 절 수): + +- 4장(3): 4-1-1, 4-2-1, 4-2-2. +- 5장(18): 5-1-1, 5-1-2, 5-1-3, 5-1-5, 5-3-2, 5-3-3, 5-3-4, 5-3-5, 5-16-1, 5-16-2, 5-17-1, 5-17-2, 5-19-1, 5-19-2, 5-26-1, 5-26-2, 5-26-3, 5-28-2. +- 6장(3): 6-4-3, 6-4-4, 6-7-1. +- 7장(7): 7-1-1, 7-5-2, 7-7-2, 7-8-1, 7-8-2, 7-9-1, 7-9-2. +- 8장(9): 8-1-1, 8-1-2, 8-2-1, 8-2-2, 8-2-3, 8-2-4, 8-2-5, 8-6-2, 8-6-3. +- 9장(25): 9-4-1, 9-5-1, 9-5-2, 9-6-1, 9-7-2, 9-8-2, 9-10-1, 9-10-2, 9-11-1, 9-12-2, 9-12-3, 9-13-7, 9-13-8, 9-13-9, 9-13-10, 9-13-11, 9-13-12, 9-13-13, 9-13-14, 9-13-15, 9-13-16, 9-13-17, 9-13-18, 9-16-3, 9-19-3. +- 10장(8): 10-6-1, 10-6-2, 10-6-3, 10-8-1, 10-9-1, 10-12-1, 10-13-1, 10-13-2. +- 12장(2): 12-38-1, 12-38-2. +- 13장(6): 13-4-6, 13-10-2, 13-11-1, 13-11-2, 13-15-1, 13-15-2. + +## 2차 전수 대조 · 건설품셈 공통 + +범위: 로직 276줄, 재료 호표 113줄, 소요량·계수 표 575개(저장 줄 2,828개). 재료 113줄의 호표 이름·검색어를 모두 펼쳤고, 원문 출처별 배수 문구를 로직 결과단위·식과 대조했다. `(10㎡당)` 노무와 `(㎡당)` 재료가 한 절에 같이 있는 2-8-8·2-8-9·2-10-1·2-10-2는 로직이 노무만 10으로 나누는 것이 원문에 맞다. 1차 보고와 겹치지 않는 발견은 아래와 같다. 원문 표의 수치 집합 검사 0건은 조건 조합별 행 수 일치를 증명하지 않는다. + +| 로직 키 | 이름 | 원문 자리(파일 · 절) | 갈래 | 무엇이 다름 | 판정 | 근거 인용 한 줄 | +|---|---|---|---|---|---|---| +| GC000001 | 철제조립식 가설건축물 손료 | 건설공사_표준품셈/본문/01_공통부문/제02장_가설공사/2-02_손율.md · 2-2-3 | 연결 | 외부·내부 PANEL, CANOPY, 박공PANEL, 천장판을 T챤넬로, 중도리를 트러스로, T-BAR를 처마후레싱으로 검색한다. | 확실 | 원문 품명 `PANEL`, `CANOPY`, `중도리`, `T-BAR`; 로직 검색어 `T챤넬`, `트러스`, `처마후레싱`. | +| GC000002 | 콘테이너형 가설건축물 손료 | 같은 파일 · 2-2-3 | 연결·단위 | 3×3·3×6·3×9m 건축물 셋 모두 12×3×2.6m 콘테이너를 검색한다. 규격 차이를 반영하지 않는다. | 확실 | 호표 규격 `3.0x3.0m`·`3.0x6.0m`·`3.0x9.0m`; 검색어 `12.0*3.0*2.6m`. | +| GC000031 | 낙하물 방지망(비계) 설치 및 해체 | 건설공사_표준품셈/본문/01_공통부문/제02장_가설공사/2-08_추락재해_및_낙하물방지시설.md · 2-8-1 | 연결 | 브라켓을 안전발판으로, 그물망을 옥상방수용 직포로 검색한다. | 확실 | 호표 `브라켓`·`그물망`; 검색어 `안전발판`·`직포 옥상방수용직포`. | +| GC000032 | 낙하물 방지망(플라잉넷) 설치 및 해체 | 같은 파일 · 2-8-2 | 연결 | 브라켓을 안전발판으로, 사다리를 강관비계로, 그물망을 직포로 검색한다. | 확실 | 호표 `브라켓`·`사다리`·`그물망`; 검색어 `안전발판`·`강관비계`·`직포`. | +| GC000037 | 철골 안전망 설치 및 해체 | 같은 파일 · 2-8-7 | 연결 | 그물망을 옥상방수용 직포로 검색한다. | 확실 | 원문 `그물망 · ㎡ · 12.4`; 검색어 `직포 옥상방수용직포`. | +| GC000038 | 비계주위 보호망 설치 및 해체 | 같은 파일 · 2-8-8 | 연결 | 보호망을 옥상방수용 직포로 검색한다. | 확실 | 호표 `보호망`; 검색어 `직포 옥상방수용직포`. | +| GC000039 | 갱폼주위 보호망 설치 및 해체 | 같은 파일 · 2-8-9 | 연결 | 보호망을 옥상방수용 직포로 검색한다. | 확실 | 호표 `보호망`; 검색어 `직포 옥상방수용직포`. | +| GC000062 | 방진망 설치 및 해체 | 건설공사_표준품셈/본문/01_공통부문/제02장_가설공사/2-10_피해방지시설.md · 2-10-2 | 연결 | 방진망을 PVC 천막지로 검색한다. | 확실 | 원문 `방진망 · ㎡ · 1.06`; 검색어 `PVC천막지 T-210`. | +| GC000068 | 건축물보양 | 건설공사_표준품셈/본문/01_공통부문/제02장_가설공사/2-11_현장관리.md · 2-11-1 | 연결 | 부직포를 직포로, 풀을 그리스로 검색한다. | 확실 | 호표 `부직포`·`풀`; 검색어 `직포 옥상방수용직포`·`그리스(일반범용)`. | +| GC000127 | 버팀목 설치·해체 | 건설공사_표준품셈/본문/01_공통부문/제03장_토공사/3-08_보강토_옹벽.md · 3-8-3 | 연결·단위 | 각재 10×10㎝가 30×30㎜ 각재를 검색한다. | 확실 | 호표 `각재 10㎝×10㎝`; 검색어 `외송각재 30×30×3600mm`. | +| GC000185 | 연직배수재(PBD) 타설 | 건설공사_표준품셈/본문/01_공통부문/제05장_기초공사/5-02_연약지반처리.md · 5-2-3 | 연결 | 드레인 보드를 고무바로 검색한다. | 확실 | 호표 `드레인 보드`; 검색어 `고무바 2.2cm`. | +| GC000197 | 차수재 부설(1겹) | 건설공사_표준품셈/본문/01_공통부문/제05장_기초공사/5-04_차수.md · 5-4-1 | 연결 | 부직포·지오콤포지트는 옥상방수용 직포로, 벤토나이트매트는 PET 매트로 검색한다. | 확실 | 호표 `부직포`·`지오콤포지트`·`벤토나이트매트`; 검색어 `직포`·`PET 매트`. | +| GC000203 | 에폭시 콘크리트 접착제 바르기 | 건설공사_표준품셈/본문/01_공통부문/제06장_철근콘크리트공사/6-01_콘크리트.md · 6-1-5 | 연결 | 에폭시 접착제 두 종은 석고본드를, 시너는 목재용 오일스테인을 검색한다. | 확실 | 호표 `에폭시 신구콘크리트 접착제`, `시너`; 검색어 `석고본드`, `목재전용도료 오일스테인`. | +| GC000212 | 강재거푸집 인력 설치 및 해체 | 건설공사_표준품셈/본문/01_공통부문/제06장_철근콘크리트공사/6-03_거푸집.md · 6-3-2 | 연결 | 강재거푸집 손료가 K-SPAN 패널 가격을 검색한다. | 확실 | 호표 `강재거푸집 손료`; 검색어 `패널(K-SPAN PANEL) 0.6×305mm`. | +| GC000215 | 문양거푸집(판넬) 설치 및 해체 | 같은 파일 · 6-3-4 | 연결 | 문양거푸집 패널이 K-SPAN 패널을 검색한다. | 확실 | 호표 `문양거푸집(판넬)`; 검색어 `패널(K-SPAN PANEL) 0.6×305mm`. | +| GC000216 | 합성수지(P.E) 원형 맨홀 거푸집 설치 및 해체 | 같은 파일 · 6-3-5 | 연결 | 원형맨홀거푸집 다섯 규격 모두 폼타이를 검색한다. | 확실 | 호표 `원형맨홀거푸집 ø740…ø1800 손료`; 검색어 `폼타이 D형`. | + +원문 수치 표가 있으나 해당 절 전용 로직도 표의 `용도.로직키`도 없는 후보는 **21절(의심)**: 1장 1-3-1, 1-4-3·4·5·6·7; 2장 2-1-1·2, 2-2-2·4; 6장 6-2-1; 7장 7-4-2·3; 8장 8-2-1·3·5·8·9·13·15·21. 적용기준·기계 성능처럼 독립 단가 로직이 필요 없는 표가 섞여 있으므로, 이를 그대로 결손 확정 수로 쓰지 않는다. + +## 2차 전수 대조 · 건설품셈 토목 + +범위: 로직 282줄, 재료 호표 71줄, 소요량·계수 표 765개(저장 줄 7,445개). 재료 71줄을 모두 펼쳐 대조했다. 원문 1-8-9의 10㎡당 재료는 `GC000319`이 `/10`을 적용하고, 노무는 시공량으로 나누므로 이 배수 자체는 맞는다. 원문 표의 수치 집합 검사 0건과 행별 조건 조합의 완전성은 별개다. + +| 로직 키 | 이름 | 원문 자리(파일 · 절) | 갈래 | 무엇이 다름 | 판정 | 근거 인용 한 줄 | +|---|---|---|---|---|---|---| +| GC000319 | 차선도색 융착식 도료 수동식 | 건설공사_표준품셈/본문/02_토목부문/제01장_도로포장공사/1-08_교통시설공.md · 1-8-9 | 연결 | 융착식 도료 프라이머가 신구접착·방청제를 검색한다. | 확실 | 호표 `프라이머(융착식 도료)`; 검색어 `신구접착및방청제 LDS-MP`. | +| GC000337 | 낙석방지망 철망 및 와이어 설치 | 건설공사_표준품셈/본문/02_토목부문/제01장_도로포장공사/1-09_부대공.md · 1-9-4 | 단위 | 결속선 원문은 `m`, 검색어는 철선 #8이다. 가격 후보가 길이 단위인지 확인 필요. | 의심 | 원문 `결속선 · m · 0.3`; 검색어 `보통철선 #8 서울`. | +| GC000374 | 강교 제작 기타재료 | 건설공사_표준품셈/본문/02_토목부문/제05장_강구조공사/5-01_강교제작(공장제작).md · 5-1-3 | 연결 | 용접봉(강교 제작)이 아세틸렌을 검색한다. | 확실 | 호표 `용접봉(강교 제작)`; 검색어 `아세틸렌 98%`. | +| GC000375 | 강교 소재 표면처리 | 건설공사_표준품셈/본문/02_토목부문/제05장_강구조공사/5-02_강교도장.md · 5-2-1 | 연결 | 무기질아연말 샵프라이머가 목재용 오일스테인을 검색한다. | 확실 | 호표 `무기질아연말 샵프라이머`; 검색어 `목재전용도료 오일스테인`. | +| GC000431, GC000432 | 보링 천공(토사·암반) | 건설공사_표준품셈/본문/02_토목부문/제08장_지반조사/8-01_보링.md · 8-1-2·3 | 연결 | 코아바렐·크라운비트·파이프·코아리프터 등 원문 소모품 12줄을 굴착기 치즐로 검색한다. | 확실 | 호표 `싱글코아바렐`, `더블코아바렐`, `다이아몬드비트` 등; 검색어 `치즐(0.2㎥)`. | +| GC000433, GC000434, GC000435 | 표준관입·시료채취·베인전단시험 | 건설공사_표준품셈/본문/02_토목부문/제08장_지반조사/8-02_시험.md · 8-2-1·2·3 | 연결 | 샘플러·신월튜브·베인 블레이드가 굴착기 치즐을 검색한다. | 확실 | 호표 `샘플러`, `신월튜브`, `베인 블레이드`; 검색어 `치즐(0.2㎥)`. | +| GC000442, GC000443, GC000444, GC000445, GC000446 | 대구경 보링 천공 | 건설공사_표준품셈/본문/02_토목부문/제08장_지반조사/8-04_대구경_보링(지하수개발).md · 8-4-1·2 | 연결 | 윙비트가 치즐을, 벤토나이트가 수팽창 지수재를 검색한다. | 확실 | 호표 `윙비트`, `벤토나이트`; 검색어 `치즐(0.2㎥)`, `쉬트파일수팽창지수재 DPS-500`. | +| GC000447, GC000448, GC000449 | 대구경 보링 암반층 천공 | 같은 파일 · 8-4-2 | 연결 | 에어해머·버튼비트가 치즐을 검색한다. | 확실 | 호표 `에어해머`, `버튼비트`; 검색어 `치즐(0.2㎥)`. | + +원문 수치 표가 있으나 해당 절 전용 로직과 표의 `용도.로직키`가 모두 없는 후보는 **13절(의심)**: 3-1-2, 3-2-1·2·3·4, 7-1-1·2·3·4, 7-4-4·5·6, 9-5-8. 계수·참고 표 여부를 확인해야 결손으로 확정할 수 있다. + +## 2차 전수 대조 · 건설품셈 건축 + +범위: 로직 163줄, 재료 호표 92줄, 소요량·계수 표 353개(저장 줄 972개). 재료 92줄 전부의 호표 이름·검색어를 대조했다. 원문에 `10m당`, `100㎡당`, `1,000개당` 등이 명시된 절의 로직 결과단위는 같은 배수로 유지되거나 시공량으로 나눴다. 수치 표가 있는데 해당 절 전용 로직과 `용도.로직키`가 모두 없는 후보는 **0절**이다. 원문 표의 행별 조건 조합 일치는 별도 미판정이다. + +| 로직 키 | 이름 | 원문 자리(파일 · 절) | 갈래 | 무엇이 다름 | 판정 | 근거 인용 한 줄 | +|---|---|---|---|---|---|---| +| GC000565 | 철골 현장용접(CO2 반자동) | 건설공사_표준품셈/본문/03_건축부문/제01장_철골공사/1-02_철골_세우기.md · 1-2-5 | 연결 | CO₂ 와이어와 탄산가스가 모두 아세틸렌을 검색한다. | 확실 | 호표 `CO_2 와이어`, `탄산가스`; 검색어 `아세틸렌 98%`. | +| GC000576 | 치장벽돌 공간쌓기 및 치장줄눈 | 건설공사_표준품셈/본문/03_건축부문/제02장_조적공사/2-01_벽돌.md · 2-1-2 | 연결 | 치장벽돌이 주차라인블록을 검색한다. | 확실 | 호표 `치장벽돌`; 검색어 `주차라인블록 1000X300X150`. | +| GC000604 | 카페트 설치(1차 발견 보강) | 건설공사_표준품셈/본문/03_건축부문/제05장_수장공사/5-01_바닥.md · 5-1-2 | 연결 | 카페트 외에 펠트도 옥상방수용 직포를 검색한다. | 확실 | 원문 `펠트 · ㎡ · 1.1`; 검색어 `직포 옥상방수용직포`. | +| GC000616 | 걸레받이 설치 | 건설공사_표준품셈/본문/03_건축부문/제05장_수장공사/5-03_벽.md · 5-3-5 | 연결 | 테라조와 합성수지 걸레받이 모두 처마후레싱을 검색한다. | 확실 | 호표 `테라조`, `합성수지 걸레받이`; 검색어 `처마후레싱 C/S`. | +| GC000618 | 도배바름(초배 · 정배) | 같은 파일 · 5-3-7 | 연결 | 도배용 풀이 그리스를 검색한다. | 확실 | 호표 `풀`; 검색어 `그리스(일반범용) CROWN`. | +| GC000620, GC000621, GC000622, GC000623, GC000625, GC000626, GC000628 | 단열재 시공 여러 절 | 건설공사_표준품셈/본문/03_건축부문/제05장_수장공사/5-04_단열.md · 5-4-1·2·3·4·6·7·9 | 연결 | 단열재·단열판에 완제품 그라스울 샌드위치판넬 가격을 연결한다. 단열재 종류·두께가 원문 또는 입력에 따라 달라질 수 있어 동등성 확인 필요. | 의심 | 호표 `단열재`/`단열판`; 검색어 `샌드위치판넬(그라스울) T50`. | +| GC000627 | 방습필름 설치 | 같은 파일 · 5-4-8 | 연결 | 방습필름이 PVC 천막지를 검색한다. | 확실 | 호표 `방습필름`; 검색어 `PVC천막지 T-210`. | +| GC000637, GC000638, GC000639 | 시트방수 붙임 3방식 | 건설공사_표준품셈/본문/03_건축부문/제06장_방수공사/6-03_시트_방수.md · 6-3-1·2·3 | 연결 | 방수시트 세 로직 모두 방수석고보드를 검색한다. | 확실 | 호표 `방수시트`; 검색어 `방수석고보드 12.5t`. | +| GC000646 | 벤토나이트방수 붙임 | 건설공사_표준품셈/본문/03_건축부문/제06장_방수공사/6-05_기타방수.md · 6-5-3 | 연결 | 벤토나이트방수재를 석고보드로, PE필름을 PVC천막지로, 카트리지를 화약 카트리지로, 와셔를 핀볼트로 검색한다. | 확실 | 호표 `벤토나이트방수재`·`PE필름`·`카트리지`·`와셔`; 검색어 `방수석고보드`·`PVC천막지`·`카트리지 화약`·`핀볼트`. | +| GC000655 | 폴리카보네이트 설치 | 건설공사_표준품셈/본문/03_건축부문/제07장_지붕_및_홈통공사/7-01_지붕.md · 7-1-6 | 연결 | 폴리카보네이트가 불연천장재(마이톤)를 검색한다. | 확실 | 호표 `폴리카보네이트`; 검색어 `불연천장재(마이톤) 12mm`. | +| GC000665 | 와이어메시 바닥깔기 | 건설공사_표준품셈/본문/03_건축부문/제08장_금속공사/8-01_제품.md · 8-1-3 | 연결 | 와이어메시가 메쉬스크린을 검색한다. | 확실 | 호표 `와이어메시`; 검색어 `메쉬스크린 H=0.9m`. | +| GC000666 | 인서트 설치 | 같은 파일 · 8-1-4 | 연결 | 인서트가 L형앵커볼트를 검색한다. | 확실 | 호표 `인서트`; 검색어 `L형앵커볼트 5/8"`. | +| GC000670 | 앵커고정식 난간 설치(공장제작) | 건설공사_표준품셈/본문/03_건축부문/제08장_금속공사/8-02_시설물.md · 8-2-2 | 연결 | AL리벳이 작은나사(스크류)를 검색한다. | 확실 | 호표 `AL리벳`; 검색어 `작은나사(스크류) ø 4.2㎜`. | + +## 2차 전수 대조 · 건설품셈 기계설비 + +범위: 로직 272줄, 재료 호표 264줄, 소요량·계수 표 509개(저장 줄 8,849개). 재료 264줄의 호표 이름·검색어를 펼쳤고 원문 배수 표기를 검색했다. 100㎡당·10m당 같은 배수 절은 이 부문에 없었다. 수치 표가 있는데 해당 절 전용 로직과 `용도.로직키`가 모두 없는 후보는 **2절(13-4-1, 13-11-4; 의심)**이다. 원문 표의 조건별 행 완전성은 별도 미판정이다. + +| 로직 키 | 이름 | 원문 자리(파일 · 절) | 갈래 | 무엇이 다름 | 판정 | 근거 인용 한 줄 | +|---|---|---|---|---|---|---| +| GC000724 | 스테인리스 강관 용접접합 | 건설공사_표준품셈/본문/04_기계설비부문/제01장_배관공사/1-03_스테인리스_강관.md · 1-3-1 | 연결 | TIG 용접봉과 아르곤가스 모두 아세틸렌을 검색한다. | 확실 | 호표 `TIG 용접봉`, `아르곤가스`; 검색어 `아세틸렌 98%`. | +| GC000752 | 제연댐퍼 설치 | 건설공사_표준품셈/본문/04_기계설비부문/제02장_덕트공사/2-02_덕트기구.md · 2-2-6 | 연결 | 블라인드리벳을 핀볼트로, 철물을 철근부식방지재로 검색한다. | 확실 | 호표 `블라인드리벳`, `철물`; 검색어 `핀볼트 M12`, `철근부식방지재 MT-3`. | +| GC000901 | 플랜트 배관 보온(성형물) | 건설공사_표준품셈/본문/04_기계설비부문/제13장_플랜트설비공사/13-03_배관_및_기기보온.md · 13-3-1 | 연결 | 보온 성형물이 STS 단열바 프레임을, Sheet Metal Screw가 빗자루를 검색한다. | 확실 | 호표 `보온 성형물`, `Sheet Metal Screw`; 검색어 `단열바 STS 프레임`, `빗자루 도로청소용`. | +| GC000931 | 수차 설치(Kaplan · Francis · Propeller) | 건설공사_표준품셈/본문/04_기계설비부문/제13장_플랜트설비공사/13-06_수력발전_기계설비.md · 13-6-1 | 연결 | 걸레가 그리스를, 비닐세트·소창직은 PVC 코팅철선을, 시너·광명단·조합페인트는 목재용 오일스테인을 검색한다. | 확실 | 원문 재료명 `걸레`, `비닐세트`, `시너`, `광명단`; 각 검색어 `그리스`, `PVC코팅철선`, `오일스테인`. | +| GC000932 | 수력 발전기 설치 | 같은 파일 · 13-6-2 | 연결·단위 | 목탄·걸레가 그리스를, 가스용접봉·신주용접봉이 아세틸렌을, 비닐시트가 사포를 검색한다. 산소 원문은 4,500ℓ/병인데 로직은 병 수에 6,000을 곱한다. | 확실 | 원문 `산소 · 4,500ℓ · 병 · 0.109`; 호표 식 `0.109 * 6000`. | +| GC000933, GC000934 | 수문 제작(Tainter·Roller Gate) | 같은 파일 · 13-6-3 | 연결 | 광명단이 목재용 오일스테인을 검색한다. | 확실 | 호표 `광명단`; 검색어 `목재전용도료 오일스테인`. | +| GC000935, GC000936 | 수문 설치(Tainter·Roller Gate) | 같은 파일 · 13-6-4 | 연결 | 광명단이 목재용 오일스테인을 검색한다. | 확실 | 호표 `광명단`; 검색어 `목재전용도료 오일스테인`. | +| GC000937 | Stop-Log 제작 | 같은 파일 · 13-6-5 | 연결 | 광명단·페인트가 신구접착·방청제를 검색한다. | 확실 | 호표 `광명단`, `페인트`; 검색어 `신구접착및방청제 LDS-MP`. | +| GC000941 | Steel Penstock 현장제관 | 같은 파일 · 13-6-9 | 연결 | 시너 두 줄 모두 목재용 오일스테인을 검색한다. | 확실 | 호표 `시너`; 검색어 `목재전용도료 오일스테인`. | +| GC000945 | Tainter Gate Guide Metal 제작 | 같은 파일 · 13-6-13 | 연결 | 전기용접봉이 아세틸렌을 검색한다. | 확실 | 호표 `전기용접봉`; 검색어 `아세틸렌 98%`. | +| GC000947 | Trash Rack 제작 | 같은 파일 · 13-6-15 | 연결 | 연마석·톱날이 도로청소용 빗자루를, Bite가 굴착기 치즐을 검색한다. | 확실 | 호표 `연마석`, `톱날`, `Bite`; 검색어 `빗자루 도로청소용`, `치즐(0.2㎥)`. | +| GC000950 | Trash Rack 설치 | 같은 파일 · 13-6-16 | 연결 | 시너가 목재용 오일스테인을 검색한다. | 확실 | 호표 `시너`; 검색어 `목재전용도료 오일스테인`. | +| GC000972 | 노 기밀시험 | 건설공사_표준품셈/본문/04_기계설비부문/제13장_플랜트설비공사/13-07_제철기계설비.md · 13-7-22 | 연결 | 가루비누가 그리스를 검색한다. | 확실 | 호표 `가루비누`; 검색어 `그리스(일반범용) CROWN`. | +| GC000986 | Over Head Crane 설치 | 건설공사_표준품셈/본문/04_기계설비부문/제13장_플랜트설비공사/13-10_운반기계설비.md · 13-10-2 | 연결 | 걸레가 그리스를 검색한다. | 확실 | 호표 `걸레`; 검색어 `그리스(일반범용) CROWN`. | +| GC000987 | Gantry Crane 설치 | 같은 파일 · 13-10-3 | 연결 | 광명단이 목재용 오일스테인을 검색한다. | 확실 | 호표 `광명단`; 검색어 `목재전용도료 오일스테인`. | +| GC000993 | 플랜트 공사별 설치 소모자재(1차 발견 보강) | 건설공사_표준품셈/본문/04_기계설비부문/제13장_플랜트설비공사/13-11_기타_기계설비.md · 13-11-5 | 연결 | 1차 발견 외에 시너도 목재용 오일스테인을, 비닐시드도 PVC천막지를 검색한다. | 확실 | 호표 `시너`, `비닐시드`; 검색어 `목재전용도료 오일스테인`, `PVC천막지 T-210`. | +| GC000931~GC000950, GC000986, GC000987, GC000993 (23키) | 아세틸렌이 포함된 플랜트·운반기계 로직 | 13-06_수력발전_기계설비.md · 13-6-1~17; 13-10_운반기계설비.md · 13-10-2·3; 13-11_기타_기계설비.md · 13-11-5 | 단위 | 아세틸렌 원문 수량은 병 단위(규격 2,100·2,500·3,500·4,000·4,500ℓ 등)인데 호표는 같은 수를 kg로 사용한다. 해당 23키 모두 병→kg 환산식이 없다. | 확실 | 예: 13-6-11 `아세틸렌 · 2,100ℓ입 · 1.6병`; `GC000943` 호표 `kg · 1.6`. | + +## 2차 전수 대조 · 건설품셈 유지관리 + +범위: 로직 110줄, 재료 호표 7줄, 소요량·계수 표 197개(저장 줄 783개). 재료 7줄을 전부 대조했다. `GC001050`의 10㎡당 재료는 `/10`, `GC001056`의 횡재부 10m당 가스는 `/10`으로 처리한다. 원문 수치 표가 있으나 해당 절 전용 로직과 `용도.로직키`가 모두 없는 후보는 **36절(의심)**: 3-1-1~5, 3-2-1~12, 3-3-1~3, 3-4-1~5, 4-1-1~7, 4-2-1, 4-3-1~3. 표만 필요한 기준인지 별도 확인이 필요하다. 표의 행별 조건 조합 완전성은 미판정이다. + +| 로직 키 | 이름 | 원문 자리(파일 · 절) | 갈래 | 무엇이 다름 | 판정 | 근거 인용 한 줄 | +|---|---|---|---|---|---|---| +| GC001050 | 차선도색 융착식 도료(수동식) | 건설공사_표준품셈/본문/05_유지관리부문/제02장_토목/2-01_도로포장공사.md · 2-1-15 | 연결 | 프라이머가 신구접착·방청제를, 프로판가스가 아세틸렌을 검색한다. | 확실 | 원문 `프라이머 · kg · 2.0`, `프로판가스 · kg · 2.0`; 검색어 `LDS-MP`, `아세틸렌 98%`. | +| GC001055 | 현장가열 표층재생 | 같은 파일 · 2-1-19 | 누락·단위 | 원문 주⑤의 `100㎡당 팁(날) 0.7개`를 계산에 계상하지 않는다. 결과단위 `원/㎡`라면 0.007개/㎡가 필요하지만 호표에 팁 줄이 없다. | 확실 | 원문 `⑤ 100㎡당 팁(날) 0.7개를 계상한다.` | + +## 2차 대조 종합과 남은 한계 + +아래 점검 수는 로직을 구조·참조·수치 검사에 통과시킨 수이며, 사람의 행별 원문 대조 완료 수가 아니다. `새 차이`는 2차 표의 행 수(여러 키를 묶은 행과 1차 발견 보강 포함)로서 고유 결함 키 수가 아니다. 앞의 1차 요약표는 당시 결과이며 이 표가 현재 범위다. + +| 부문 | 로직 수 | 점검 수(기계) | 재료 호표 전수 | 새 차이(표 행) | 누락 절 후보(의심) | 원문 조건·규격 줄 전수 판정 | +|---|---:|---:|---:|---:|---:|---| +| 산림품셈 | 251 | 251 | 128 | 16 | 81 | 미완료 | +| 건설 공통 | 276 | 276 | 113 | 16 | 21 | 미완료 | +| 토목 | 282 | 282 | 71 | 8 | 13 | 미완료 | +| 건축 | 163 | 163 | 92 | 13 | 0 | 미완료 | +| 기계설비 | 272 | 272 | 264 | 17 | 2 | 미완료 | +| 유지관리 | 110 | 110 | 7 | 2 | 36 | 미완료 | +| 합계 | 1,354 | 1,354 | 675 | 72 | 153 | 미완료 | + +검사 대상 표는 3,033개, 저장 줄은 25,036개다. 1차 `check_master.py 본문` 결과 0건은 절 안에 등장하는 **숫자 집합**이 맞는다는 증거일 뿐, 각 숫자가 올바른 규격·조건 줄에 놓였다는 증거가 아니다. 2차에서는 675개 재료 호표 줄의 명칭·검색어를 전부 펼쳐 확인했고, 배수 표기가 나타나는 로직 식을 대조했다. 그러나 원문 Markdown의 병합 셀과 여러 갈래로 분해된 JSON 표를 3,033개 모두 일대일로 재구성하여 25,036줄의 규격·조건 조합 및 비목을 판정하지 못했다. 따라서 이 보고서는 요청한 **완전한 전수 대조의 완료 판정이 아니며**, 위의 미완료 범위는 추후 추가 검증이 필요하다. 원문 절 누락 후보 153개도 참고·계수 표가 포함되므로 확정 결손으로 계산하지 않는다. + +## 3차 장별 대조 · 산림품셈 4장 나무베기 + +원문 `4-1-1`, `4-2-1`, `4-2-2`의 수치 표와 대응 소요량 `QF000041`·`QF000045`·`QF000047`을 조건·수치·단위별로 대조했다. 각각 원문 유효 줄 3·12·9개와 자료 줄 3·12·9개가 맞고, 확인한 값의 자리 밀림은 없다. `4-2-2`의 원문 3직종/장비 × 3나무높이는 자료의 9줄로 펴져 있다. 하지만 세 표 모두 `용도.로직키=[]`이고 산림 로직에서 해당 원문번호가 없다. + +| 로직 키 | 이름 | 원문 자리(파일 · 절) | 갈래 | 무엇이 다름 | 판정 | 근거 인용 한 줄 | +|---|---|---|---|---|---|---| +| 없음 (QF000041) | 임업용 동력기계톱 수확베기 | 산림_표준품셈/본문/제04장_나무베기/4-01_수확베기.md · 4-1-1 | 누락 | 벌도목별 작업량과 벌목부가 저장됐지만 단가 로직이 없다. 후보 분류: 누락 확정. | 확실 | 원문 `단목 20.17㎥/1인/1일 · 벌목부`; `QF000041 용도.로직키=[]`. | +| 없음 (QF000045) | 단목베기 100본당 | 산림_표준품셈/본문/제04장_나무베기/4-02_단목베기.md · 4-2-1 | 누락 | 흉고직경 11구간의 벌목·가지제거·토막내기 및 작업보조 소요인력은 표에 있으나 단가 로직이 없다. 후보 분류: 누락 확정. | 확실 | 원문 `10㎝이하 · 0.2 · 0.1 · 0.1 · 벌목부`, `작업보조 0.6 · 보통인부`; `QF000045 용도.로직키=[]`. | +| 없음 (QF000047) | 단목베기 1,000㎡당 | 같은 파일 · 4-2-2 | 누락 | 나무높이별 벌목부·보통인부·굴착기 소요량은 표에 있으나 단가 로직이 없다. 후보 분류: 누락 확정. | 확실 | 원문 `벌목부 2.14·2.80·3.65인`, `굴착기 2.71·3.54·4.61hr`; `QF000047 용도.로직키=[]`. | + +## 3차 장별 대조 · 산림품셈 5장 누락 후보 18절 + +원문 절의 숫자 표와 산림 로직의 `원문번호`, 표의 `용도.로직키`를 대조했다. 아래 분류는 단가 로직 존재 여부에 대한 것이며, 5장 전체 소요량 표의 조건·규격 줄 전수 검증은 **아직 끝나지 않았다**. + +| 원문 절 | 분류 | 이유와 근거 한 줄 | +|---|---|---| +| 5-1-1 | 누락 확정 | 관목굴취 `특별인부 0.084…0.408인`, `보통인부 0.012…0.072인/10본당`이 있는데 해당 로직 없음. | +| 5-1-2 | 누락 확정 | 교목굴취 나무높이별 특별인부·보통인부 수량이 있는데 해당 로직 없음. | +| 5-1-3 | 누락 확정 | 교목굴취 근원직경별 인력·굴착기·크레인 수량이 있는데 해당 로직 없음. | +| 5-1-5 | 참고·계수 표라 로직 불필요 | 제목부터 `떼운반 적재 기준표`; 지게·리어카 등의 적재 매수와 싣고부리기 시간·인원 기준을 담은 보조 표. 독립 공종 로직 부재만으로 결손으로 보지 않음. | +| 5-3-2 | 누락 확정 | 관목식재(단식) 수고별 특별인부·보통인부 수량이 있는데 해당 로직 없음. | +| 5-3-3 | 누락 확정 | 관목식재(군식) 수고별 특별인부·보통인부 수량이 있는데 해당 로직 없음. | +| 5-3-4 | 누락 확정 | 교목식재 나무높이별 인력·장비 수량이 있는데 해당 로직 없음. | +| 5-3-5 | 누락 확정 | 교목식재 흉고직경별 인력·장비 수량이 있는데 해당 로직 없음. | +| 5-16-1 | 누락 확정 | 단끊기 공정별 보통인부 수량이 있는데 해당 로직 없음. | +| 5-16-2 | 사람 확인 필요 | 선떼붙이기 표는 단끊기 `1m`와 떼붙임 `물량산출`의 합성 기준. 두 하위 로직과 조합하여 처리할 의도인지 확인 필요. | +| 5-17-1 | 사람 확인 필요 | 떼조공은 단끊기·줄떼심기·선떼붙이기 조합 표. 독립 로직 필요성 확인 필요. | +| 5-17-2 | 사람 확인 필요 | 돌조공은 단끊기·돌쌓기 조합 표. 독립 로직 필요성 확인 필요. | +| 5-19-1 | 누락 확정 | 표토절취 깊이별 보통인부 소요량이 있는데 해당 로직 없음. | +| 5-19-2 | 누락 확정 | 표토펴기 ㎥당·100㎡당 보통인부 수량이 있는데 해당 로직 없음. | +| 5-26-1 | 누락 확정 | 막갈이 사토·양토·식토 × 깊이별 `인/992㎡당` 수량이 있는데 해당 로직 없음. | +| 5-26-2 | 누락 확정 | 흙부수기 토성 × 깊이별 `인/992㎡당` 수량이 있는데 해당 로직 없음. | +| 5-26-3 | 누락 확정 | 돌자갈치우기 토성 × 함량별 `인/992㎡당` 수량이 있는데 해당 로직 없음. | +| 5-28-2 | 누락 확정 | 방초·야자섬유매트 포장 `조경공 2인+보통인부 1인`, 90·130㎡/일 시공량이 있는데 해당 로직 없음. | + +5장 18절 중 독립 단가화 가능한 14절을 누락 확정, 보조 기준표 1절을 로직 불필요, 조합 성격 3절을 사람 확인 필요로 나눴다. `5-26`의 밑수는 원문 그대로 **992㎡**이며 1,000㎡로 바꾸어 읽지 않았다. + +## 3차 추가 확인 · 산림품셈 12-38 유로폼 + +후보 `12-38-1` 사용횟수와 `12-38-2` 사용수량은 상위 `GF000210`이 맡는 절의 일부다. 그러나 그 로직의 호표는 형틀목공·보통인부 두 줄뿐이며, 비고에도 `유로폼 자재(12-38-2)는 임대료·손료로 따로 — 요소 없음`, `주자재비가 없어 빠짐`이라고 명시되어 있다. 따라서 **로직 자체가 전혀 없는 절**로 집계하지 않고 아래처럼 분류한다. + +| 로직 키 | 이름 | 원문 자리(파일 · 절) | 갈래 | 무엇이 다름 | 판정 | 근거 인용 한 줄 | +|---|---|---|---|---|---|---| +| GF000210 | 유로폼 설치 및 해체 | 산림_표준품셈/본문/제12장_철근콘크리트/12-38_유로폼.md · 12-38-1·2 | 누락 | `12-38-1`은 유로폼 자재 손료의 사용횟수 기준이라 독립 단가 로직 불필요; `12-38-2`의 패널·부자재 사용수량은 상위 인력 로직이 맡는 공종이지만 실제 호표에 빠져 재료비가 계산되지 않는다. | 확실 | `GF000210` 비고 `유로폼 자재(12-38-2)는 임대료·손료로 따로 — 요소 없음 · 부자재 요율…주자재비가 없어 빠짐`; 호표 인력 2줄뿐. | + +## 3차 자재품목표 대조 · 대표 오류의 대체 후보 유무 + +`재료_자재품목.json`의 이름·규격을 검색했다. 아래 `있음`은 **같은 계열의 후보가 목록에 존재한다는 뜻**이지 규격·단위·가격까지 적합한 최종 대체품을 확정했다는 뜻은 아니다. 반대로 `검색 없음`도 공백·동의어·다른 재료 원천까지 포함한 절대 부재 증명은 아니다. + +| 로직 키 | 호표 재료 줄 | 현재 검색어 | 자재품목표 대체 후보 | 판정 | +|---|---|---|---|---| +| GC000575 | 시멘트벽돌 | 주차라인블록 1000X300X150 | 시멘트벽돌 `190×90×57mm` `MT005760` 등 17줄 있음 | 다른 품목 연결 확실; 후보 있음 | +| GC000604 | 카페트 | 불연천장재(마이톤) | 카페트 `MT008142`, 타일카페트 `MT008140` 있음 | 다른 품목 연결 확실; 후보 있음 | +| GC000319, GC001050 | 융착식 도료 프라이머 | 신구접착및방청제 LDS-MP | 융착식 프라이머 `MT003454` 있음(ℓ; 원문 kg면 환산 검토 필요) | 다른 품목 연결 확실; 후보 있음 | +| GC001050 | 프로판가스 | 아세틸렌 98% | 프로판가스 일반용 `MT003175`, kg 있음 | 다른 품목 연결 확실; 후보 있음 | +| GC000375 | 무기질아연말 샵프라이머 | 목재전용도료 오일스테인 | 무기질 아연말 전처리 프라이머 `MT003460`, 무기질 아연말 프라이머 `MT003461` 있음 | 다른 품목 연결 확실; 후보 있음 | +| GC000665 | 와이어메시 | 메쉬스크린 H=0.9m | 와이어메쉬(용접철망) `MT007128` 등 있음 | 다른 품목 연결 확실; 후보 있음 | +| GC000933~GC000936, GC000987 | 광명단 | 목재전용도료 오일스테인 | 광명단조합페인트 `MT003375`~`MT003377` 있음 | 다른 품목 연결 확실; 후보 있음 | +| GF000220, GF000224 | 잡석(채움재) | 모래 강모래 | 자연자갈·잡석 `MT003848`, 깬잡석 `MT004113` 등 있음 | 다른 품목 연결 확실; 후보 있음 | +| GC000181 | P.P로프 9㎜ | 와이어로프(철심) 9㎜ | P.P로프는 `MT007584` 등 6줄 있으나 9㎜ 규격은 검색 없음 | 다른 품목 연결 확실; 정확 규격 후보는 확인 못 함 | +| GC000348 | 일체식 방수시트 | 방수석고보드 12.5t | ARS아크릴방수시트 `MT006814`·`MT006815` 있음(일체식과 동등성 미확인) | 다른 품목 연결 확실; 후보 동등성 의심 | + +## 3차 산림 소요량 일대일 기계 대조의 범위 + +산림 소요량 중 `출처`가 같은 표가 단 하나이고, 해당 원문 절에 Markdown 표 블록도 하나인 290개 표를 따로 비교했다. 원문 몸줄 수와 자료 줄 수가 같은 경우는 **163표**였다. 그중 같은 순번의 줄마다 숫자 집합까지 같은 것은 **86표**였고, 나머지 **77표**는 숫자 차이가 보여 사람 확인 필요로 남겼다. 나머지 **127표**는 원문 가로·세로 피벗, 병합 셀, 주석·기준표 분리로 원문 물리 줄 수와 JSON 저장 줄 수가 달라 이 방법으로는 합격·불합격을 판정할 수 없다. 숫자 집합 일치는 조건 문자열·비목 일치의 증거가 아니므로 86표도 최종 완료 수로 사용하지 않는다. 전체 산림 소요량·계수 634표 중 위 방법 밖의 표 역시 미완료다. + +## 3차 장별 대조 · 산림품셈 6장 누락 후보 3절 + +| 원문 절 | 분류 | 이유와 근거 한 줄 | +|---|---|---| +| 6-4-3 | 누락 확정 | 소금처리와 제거지점 표시의 `인/100본` 인력 수량이 `QF000167` 5줄에 있으나 로직키가 비어 있다. | +| 6-4-4 | 누락 확정 | 뿌리 고살·덩굴 제거 등의 `인/100본` 수량은 `QF000168`에 있으나 로직키가 비어 있다. 같은 절의 `QF000169` 피복도·본수 환산표는 단독 로직이 필요 없는 보조 기준이다. | +| 6-7-1 | 누락 확정 | 교목 시비의 근원직경별 특별인부·보통인부 수량은 `QF000173` 10줄에 있으나 로직키가 비어 있다. | + +6장 후보 세 절은 모두 직접 소요인력이 있어 누락 확정으로 분류했다. 장 전체의 표 행·비목 대조는 아직 미완료다. + +## 3차 장별 대조 · 산림품셈 7장 누락 후보 7절 + +| 원문 절 | 분류 | 이유와 근거 한 줄 | +|---|---|---| +| 7-1-1 | 누락 확정 | 수확 인력집재의 거리·경사 조건별 작업량 표가 있으나 산림 로직은 이웃 절 `7-1-2`부터 있다. | +| 7-5-2 | 누락 확정 | 숲가꾸기·병해충방제 트랙터 집재의 거리·본당재적별 작업량 및 3인 1조 구성이 있으나 로직 없음. | +| 7-7-2 | 누락 확정 | 숲가꾸기·병해충방제 HAM200 집재의 집재재적·거리별 작업량과 3인 1조 구성이 있으나 로직 없음. | +| 7-8-1 | 누락 확정 | 타워야더 가선설치의 집재방식·가선길이별 소요인력이 있으나 로직 없음. `GF000068` 비고도 `가선설치 · 해체(7-8-1 · 7-8-2) 따로`라고 명시. | +| 7-8-2 | 누락 확정 | 타워야더 가선해체의 집재방식·가선길이별 소요인력이 있으나 로직 없음. 상위 집재 로직에 포함되지 않음. | +| 7-9-1 | 누락 확정 | 수확 스윙야더 집재의 집재재적·거리별 작업량 및 4인 1조 구성이 있으나 로직 없음. | +| 7-9-2 | 누락 확정 | 숲가꾸기·소나무재선충병방제 스윙야더 집재의 집재재적·거리별 작업량 및 4인 1조 구성이 있으나 로직 없음. | + +7장 후보 7절은 모두 직접 작업량·인력 구성이 있어 로직 누락 확정으로 분류했다. 장 전체 표의 규격·조건 줄 및 비목 대조는 아직 미완료다. + +## 3차 장별 대조 · 산림품셈 8장 누락 후보 9절 + +| 원문 절 | 분류 | 이유와 근거 한 줄 | +|---|---|---| +| 8-1-1 | 누락 확정 | 약제주입기 천공수별 `소요인력(ha당)`이 있으나 산림 로직 없음. | +| 8-1-2 | 누락 확정 | 약제주입병 천공수별 `소요인력`이 있으나 산림 로직 없음. | +| 8-2-1 | 참고·계수 표라 로직 불필요 | 소나무재선충병의 가슴높이지름별 천공수·천공당/본당 주입량 기준표이며 독립 시공 품은 아님. 약제 수량 산정에 쓰일 수 있으므로 표 보존은 필요. | +| 8-2-2 | 참고·계수 표라 로직 불필요 | 솔잎혹파리의 선정약제·원액 주입량 기준표. 독립 노무·장비 수량 표 아님. | +| 8-2-3 | 참고·계수 표라 로직 불필요 | 솔껍질깍지벌레의 선정약제·원액 주입량 기준표. 독립 노무·장비 수량 표 아님. | +| 8-2-4 | 참고·계수 표라 로직 불필요 | 솔나방의 선정약제·원액 주입량과 직경별 주입량 기준표. | +| 8-2-5 | 참고·계수 표라 로직 불필요 | 푸사리움가지마름병의 선정약제·원액 주입량과 직경별 주입량 기준표. | +| 8-6-2 | 누락 확정 | 드론방제의 무인헬기 `ha당` 인력 편성·소요량이 있으나 로직 없음(`8-6-1` 유인헬기만 있음). | +| 8-6-3 | 누락 확정 | 지상방제의 차량살포 인력 편성과 물량 조건이 있으나 로직 없음. | + +8장 후보 9절은 누락 확정 4, 약제량 기준표 5로 분류했다. 약제량 표를 다른 로직의 재료 수량에 연결해야 하는지 여부는 별도 검증 대상이며, 8장 전체 행·비목 대조는 미완료다. + +## 3차 장별 대조 · 산림품셈 9장 누락 후보 25절 + +9장 후보 목록의 각 원문번호에 대해 산림 로직의 정확한 `원문번호`와 인접 절 로직을 확인했다. 9-4-2·9-5-3·9-7-1·9-8-1·9-12-1·9-13-1~6·9-16-1~2·9-19-1~2 등 **이웃 절**은 구현됐으나 아래 절을 맡지 않는다. 원문에는 독립 시공량·인력·장비 또는 재료 수량이 있다. + +| 원문 절 | 분류 | 근거 요약 | +|---|---|---| +| 9-4-1 | 누락 확정 | 암파쇄 브레이커 작업능력·치즐소모량 표. | +| 9-5-1 | 누락 확정 | 육상 암석 절취 발파 10%의 폭약·화약공 수량 표. | +| 9-5-2 | 누락 확정 | 암석 절취 깎기 90%의 암종별 작업능력·치즐소모량 표. | +| 9-6-1 | 누락 확정 | 기계소할 15%의 브레이커 작업능력·치즐소모량 표. | +| 9-7-2 | 누락 확정 | 무근콘크리트 깨기 T=30㎝ 이상의 작업능력·치즐·보통인부 표. | +| 9-8-2 | 누락 확정 | 철근콘크리트 깨기 T=30㎝ 이상의 작업능력·치즐·보통인부 표. | +| 9-10-1 | 누락 확정 | 콘크리트 깨기 작업능력·치즐·보통인부 표. | +| 9-10-2 | 누락 확정 | 아스팔트 깨기 작업능력·치즐·보통인부 표. | +| 9-11-1 | 누락 확정 | 콘크리트 커트 인력·장비·절단깊이별 수량 표. | +| 9-12-2 | 누락 확정 | 측구터파기 암절취의 할석공·브레이커 적용량 표. | +| 9-12-3 | 누락 확정 | 측구터파기 발파암의 할석공·브레이커 적용량 표. | +| 9-13-7 | 누락 확정 | 구조물터파기 육상 암절취 0~1m, 할석공·브레이커 적용량. | +| 9-13-8 | 누락 확정 | 구조물터파기 육상 암절취 1~2m, 할석공·브레이커 적용량. | +| 9-13-9 | 누락 확정 | 구조물터파기 육상 암절취 2~3m, 할석공·브레이커 적용량. | +| 9-13-10 | 누락 확정 | 구조물터파기 용수 암절취 0~1m, 인력·브레이커 적용량. | +| 9-13-11 | 누락 확정 | 구조물터파기 용수 암절취 1~2m, 인력·브레이커 적용량. | +| 9-13-12 | 누락 확정 | 구조물터파기 용수 암절취 2~3m, 인력·브레이커 적용량. | +| 9-13-13 | 누락 확정 | 구조물터파기 육상 발파암 0~1m, 할석공·브레이커 적용량. | +| 9-13-14 | 누락 확정 | 구조물터파기 육상 발파암 1~2m, 할석공·브레이커 적용량. | +| 9-13-15 | 누락 확정 | 구조물터파기 육상 발파암 2~3m, 할석공·브레이커 적용량. | +| 9-13-16 | 누락 확정 | 구조물터파기 용수 발파암 0~1m, 할석공·브레이커 적용량. | +| 9-13-17 | 누락 확정 | 구조물터파기 용수 발파암 1~2m, 할석공·브레이커 적용량. | +| 9-13-18 | 누락 확정 | 구조물터파기 용수 발파암 2~3m, 할석공·브레이커 적용량. | +| 9-16-3 | 누락 확정 | 살수의 흡입준비·운반 등 작업시간 기준; `9-16-1~2` 노체포설·다짐 로직과 별개. | +| 9-19-3 | 누락 확정 | 비탈면 고르기 발파암 보통인부·공기압축기 수량; `9-19-1~2` 로직과 별개. | + +이 25절은 소요량·기준 자료만 있고 해당 단가 로직이 없어 누락 확정으로 분류했다. 다만 장 전체 표의 **조건별 수치 배치**는 아직 미완료다. + +## 3차 장별 대조 · 산림품셈 10장 누락 후보 8절 + +| 원문 절 | 분류 | 이유와 근거 한 줄 | +|---|---|---| +| 10-6-1 | 누락 확정 | 토사·석재의 운반거리별 보통인부 `인/㎥당` 표가 있으나 로직 없음. | +| 10-6-2 | 누락 확정 | 돌 지게운반 거리별 보통인부 표가 있으나 로직 없음. | +| 10-6-3 | 누락 확정 | 기타 임업자재 운반거리·자재종류별 보통인부 표가 있으나 로직 없음. | +| 10-8-1 | 누락 확정 | 케이블 크레인 짐내리기의 운반기구별 인부·신호수 수량이 있으나 로직 없음. `10-8-2·3`은 별도 설치·운전 로직. | +| 10-9-1 | 다른 로직이 일부 맡음 / 누락 확정 | 상위 `GF000145(10-9)`는 시간당 기계경비만 계산한다. 비고에 `10-9-1 운반량 쓰는 식 요소 없음`을 명시하므로 자재별 1회 운반량을 반영한 단위 운반비 계산은 빠졌다. | +| 10-12-1 | 누락 확정 | 덤프운반 토사의 굴착기 적재 `Q1=3600×q0×K×f×E0/Cm`와 운반 조건이 있으나 해당 로직 없음. | +| 10-13-1 | 누락 확정 | 드론운반 수평거리 100~600m별 인원 수량이 있으나 로직 없음. | +| 10-13-2 | 누락 확정 | 인력운반 거리별 인원 수량과 드론운반 시간·본수 기준이 있으나 로직 없음. | + +10장 후보는 8절 모두 직접 단가 또는 단위 운반비 산출에 필요한 데이터가 있으나 해당 처리가 없다. `10-9-1`은 상위 로직이 **시간당 경비만** 맡는 일부 구현으로 구분한다. 장 전체의 행별 수치·비목 전수 대조는 미완료다. + +## 3차 장별 대조 · 산림품셈 13장 누락 후보 6절 + +| 원문 절 | 분류 | 이유와 근거 한 줄 | +|---|---|---| +| 13-4-6 | 누락 확정 | 밑돌·천단 돌쌓기 뒷길이별 석공 `0.05·0.08…인` 표가 있으나 `13-4-1·2·4·5` 이웃 로직만 있다. | +| 13-10-2 | 누락 확정 | 나무 말뚝박기 말구·길이별 보통인부 수량이 있으나 `13-10-1` 말뚝 다듬기 로직만 있다. | +| 13-11-1 | 누락 확정 | 원형 돌망태 지름별 조약돌량·작업 수량 표가 있으나 로직은 `13-11-3·4` 다른 형태만 있다. | +| 13-11-2 | 누락 확정 | 타원형 돌망태 지름별 조약돌량·작업 수량 표가 있으나 로직은 `13-11-3·4` 다른 형태만 있다. | +| 13-15-1 | 누락 확정 | 목재 껍질벗기기 말구별 보통인부 수량이 있으나 로직 없음. | +| 13-15-2 | 참고·계수 표라 로직 불필요 | 목책 설치의 `어려운/쉬운` 작업조건(벌도목 직경·가지량·경사도) 구분표는 직접 인력·재료 수량이 아닌 적용조건 기준. 다만 목책 설치 자체의 다른 품이나 로직이 있는지는 추가 확인 필요. | + +13장 후보는 누락 확정 5절, 직접 단가 수량이 아닌 조건 기준표 1절로 분류했다. 장 전체의 행별 수치·비목 대조는 미완료다. + +### 산림 누락 후보 81절 분류 합계 + +독립 단가 로직 누락으로 분류한 절 **69개**(4장 3, 5장 14, 6장 3, 7장 7, 8장 4, 9장 25, 10장 8, 13장 5). 독립 로직이 불필요한 참고·계수·조건 기준표 **8개**(5-1-5, 8-2-1~5, 12-38-1, 13-15-2). 다른 로직이 **일부만 맡아 재료·운반량이 빠진 절 1개**(12-38-2, `GF000210`). 하위 공종의 조합으로 처리할지 **사람 확인 필요 3개**(5-16-2, 5-17-1·2). 분류의 근거는 위 장별 절 목록에 적었다. 이는 **로직 존재 여부의 분류**이지 각 절의 표 줄·값·비목 전수 검증 완료를 뜻하지 않는다. + +## 3차 건설 공통 · 원문 절 누락 후보 21개 분류 + +| 원문 절 | 분류 | 근거 요약 | +|---|---|---| +| 1-3-1 | 참고·계수 표라 로직 불필요 | 시멘트 등 재료의 할증률 표; 독립 공종의 인력·재료량이 아님. | +| 1-4-3 | 참고·계수 표라 로직 불필요 | 작업지연 조건별 품 할증률. | +| 1-4-4 | 참고·계수 표라 로직 불필요 | 지세·지형 등급별 품 할증률. | +| 1-4-5 | 참고·계수 표라 로직 불필요 | 위험 조건별 품 할증률. | +| 1-4-6 | 참고·계수 표라 로직 불필요 | 작업시간 제한 조건별 품 할증률. | +| 1-4-7 | 참고·계수 표라 로직 불필요 | 야간 등 작업환경별 품 할증률. | +| 2-1-1 | 참고·계수 표라 로직 불필요 | 직접노무비에 따른 토목 현장사무소·창고·숙소 규모 기준. 면적만으로 별도 단가가 나오지 않음. | +| 2-1-2 | 참고·계수 표라 로직 불필요 | 건축·기계설비 현장사무소·창고 규모 기준. | +| 2-2-2 | 참고·계수 표라 로직 불필요 | 주요 가설자재 사용기간별 손율. | +| 2-2-4 | 참고·계수 표라 로직 불필요 | 구조물 동바리 기간별 손율. | +| 6-2-1 | 참고·계수 표라 로직 불필요 | 철근가공·조립의 Type-Ⅰ 등 적용범위 설명표. | +| 7-4-2 | 누락 확정 | 석재판 붙임 앵커 긴결공법의 인력 수량·일당 시공량 표 `QC000495`가 있으나 로직 없음. | +| 7-4-3 | 누락 확정 | 석재판 붙임 강제트러스 지지공법의 인력 수량·일당 시공량 표 `QC000496`이 있으나 로직 없음. | +| 8-2-1 | 참고·계수 표라 로직 불필요 | 불도저 삽날용량·작업효율·속도 등 기계 생산성 산정계수. 독립 공종 아님. | +| 8-2-3 | 참고·계수 표라 로직 불필요 | 굴착기 버킷계수·작업효율·싸이클시간 등 생산성 산정계수. | +| 8-2-5 | 참고·계수 표라 로직 불필요 | 로더 적재시간·버킷계수·작업효율 등 생산성 산정계수. | +| 8-2-8 | 참고·계수 표라 로직 불필요 | 덤프트럭 운반속도·적하시간 등 생산성 산정계수. | +| 8-2-9 | 참고·계수 표라 로직 불필요 | 롤러 적합도·다짐폭·속도·다짐횟수 등 생산성 산정계수. | +| 8-2-13 | 참고·계수 표라 로직 불필요 | 대형브레이커 작업능력·치즐 소모량 등 생산성 산정계수. | +| 8-2-15 | 참고·계수 표라 로직 불필요 | 법면다짐기 작업능력 2줄의 기계 생산성 기준. | +| 8-2-21 | 참고·계수 표라 로직 불필요 | 경운기 적재·적하시간과 주행속도 기준. | + +공통 후보는 독립 단가 로직 누락 2절, 참고·계수·적용범위 19절이다. 공통 기계 계수표의 **다른 로직에서의 실제 사용 여부**는 별도 연결 점검이 필요하다. 장 전체 원문 표의 행·값·비목 대조는 미완료다. + +## 3차 건설 토목 · 원문 절 누락 후보 13개 분류 + +| 원문 절 | 분류 | 근거 요약 | +|---|---|---| +| 3-1-2 | 참고·계수 표라 로직 불필요 | 터널 아치·측벽·바닥의 여굴두께 설계기준. | +| 3-2-1 | 참고·계수 표라 로직 불필요 | 터널굴착 1발파당 작업별 싸이클 시간 기준. | +| 3-2-2 | 참고·계수 표라 로직 불필요 | 기계굴착의 암종·기계별 시간당 작업능력. | +| 3-2-3 | 참고·계수 표라 로직 불필요 | 천공기계의 암종별 천공속도. | +| 3-2-4 | 참고·계수 표라 로직 불필요 | 터널 천공·버력처리 장비의 조합 기준. | +| 7-1-1 | 참고·계수 표라 로직 불필요 | 수중공사의 케이슨·블록 여유폭 설계기준. | +| 7-1-2 | 참고·계수 표라 로직 불필요 | 피예인선별 예인선 출력 조합 기준. | +| 7-1-3 | 참고·계수 표라 로직 불필요 | 준설선별 부속 선단·기계 조합 기준. | +| 7-1-4 | 참고·계수 표라 로직 불필요 | 준설선별 취업·운전시간 기준. | +| 7-4-4 | 참고·계수 표라 로직 불필요 | 준설 토질·수심별 여굴 두께 설계기준. | +| 7-4-5 | 참고·계수 표라 로직 불필요 | 펌프준설 매립시 토질별 유보율. | +| 7-4-6 | 참고·계수 표라 로직 불필요 | 펌프준설 매립시 입경별 유실률. | +| 9-5-8 | 참고·계수 표라 로직 불필요 | GPS 측량기 등 상각·정비·관리비율 및 계수 기준. | + +토목 후보 13절은 모두 단독 공종 수량이 아닌 설계·생산성·손율 계수표로 분류했다. 다만 이를 사용하는 로직의 존재·적용 정확성은 별도 문제이고, 토목 전체 원문 표의 행·값·비목 대조는 미완료다. + +## 3차 건설 기계설비 · 원문 절 누락 후보 2개 분류 + +| 원문 절 | 분류 | 근거 요약 | +|---|---|---| +| 13-4-1 | 참고·계수 표라 로직 불필요 | 보통 철골재의 구조별 개산 무게와 주재 대비 부속재 비율 표. 직접 설치 인력·재료 투입량 표 아님. | +| 13-11-4 | 참고·계수 표라 로직 불필요 | IRON WIRE ROPE 등 가설자재의 월별 손료율 표. 개별 단가 산출의 보조 계수. | + +기계설비 후보 2절은 독립 단가 로직 불필요로 분류했다. 272개 기계설비 로직과 509개 소요량·계수 표의 원문 조건·값·비목 전수 대조는 미완료다. + +## 3차 건설 유지관리 · 원문 절 누락 후보 36개 분류 + +| 원문 절 | 분류 | 근거 요약 | +|---|---|---| +| 3-1-1 | 누락 확정 | 콘크리트구조물 인력 헐기 직종·시공량. | +| 3-1-2 | 누락 확정 | 콘크리트구조물 기계 헐기 인력·장비·시공량. | +| 3-1-3 | 누락 확정 | 철골재 인력 철거 직종·시공량. | +| 3-1-4 | 누락 확정 | 철골재 기계 철거 직종·장비·시공량. | +| 3-1-5 | 누락 확정 | 석축 인력 헐기 뒷길이별 할석공·보통인부 수량. | +| 3-2-1 | 누락 확정 | 금속기와 해체 지붕잇기공 수량. | +| 3-2-2 | 누락 확정 | 흡음텍스 해체 내장공 수량. | +| 3-2-3 | 누락 확정 | 경량천장철골틀 해체 내장공 수량. | +| 3-2-4 | 누락 확정 | 조적벽 해체 조적공 수량. | +| 3-2-5 | 누락 확정 | 경량벽체철골틀 해체 내장공 수량. | +| 3-2-6 | 누락 확정 | 석고판 해체 벽·천장별 내장공 수량. | +| 3-2-7 | 누락 확정 | 도배 해체 벽·천장별 도배공 수량. | +| 3-2-8 | 누락 확정 | PVC계바닥재 해체 내장공 수량. | +| 3-2-9 | 누락 확정 | 타일 해체 붙임방식별 타일공 수량. | +| 3-2-10 | 누락 확정 | 기존방수층·보호층 철거 착암공 등 수량. | +| 3-2-11 | 누락 확정 | 기존방수층 제거·바탕처리 방수공 수량. | +| 3-2-12 | 누락 확정 | 석면건축자재 해체 내·외장재별 석면해체공 수량. | +| 3-3-1 | 누락 확정 | 콘크리트·모르타르면 재도장 바탕처리 도장공·시공량. | +| 3-3-2 | 누락 확정 | 철재면 재도장 바탕처리 A·B급별 도장공·시공량. | +| 3-3-3 | 누락 확정 | 목재면 재도장 바탕처리 A·B급별 도장공·시공량. | +| 3-4-1 | 누락 확정 | 지붕 덧씌우기 지붕잇기공·시공량. | +| 3-4-2 | 누락 확정 | 지붕 재설치 지붕잇기공·시공량. | +| 3-4-3 | 누락 확정 | 도배 교체 벽·천장별 인력·시공량. | +| 3-4-4 | 누락 확정 | PVC계바닥재 교체 내장공·시공량. | +| 3-4-5 | 누락 확정 | 타일 교체 붙임방식별 인력·시공량. | +| 4-1-1 | 누락 확정 | 배관 해체 규격·재질별 배관공·보통인부 수량. | +| 4-1-2 | 누락 확정 | 각형덕트 해체 두께별 인력 수량. | +| 4-1-3 | 누락 확정 | 스파이럴덕트 해체 두께·규격별 덕트공·보통인부 수량. | +| 4-1-4 | 누락 확정 | 배관보온 해체 재질·규격별 보온공·보통인부 수량. | +| 4-1-5 | 누락 확정 | 덕트보온 해체 재질별 보온공 수량. | +| 4-1-6 | 누락 확정 | 펌프 해체 출력별 기계설비공·보통인부 수량. | +| 4-1-7 | 참고·계수 표라 로직 불필요 | 일반기계설비 철거·이설의 설치품 대비 적용률 표; 독립 직종·장비 수량 아님. 적용 계산 여부는 별도 점검 필요. | +| 4-2-1 | 참고·계수 표라 로직 불필요 | 자동제어설비 철거·이설의 설치품 대비 적용률 표; 독립 직종·장비 수량 아님. 적용 계산 여부는 별도 점검 필요. | +| 4-3-1 | 누락 확정 | 유량계 교체 인력·규격별 시공량. | +| 4-3-2 | 누락 확정 | 관갱생 규격별 규사·에폭시도료·배관공·장비시간. | +| 4-3-3 | 누락 확정 | 배관누수 검사 배관공·시공량. | + +유지관리 후보 36절 중 독립 단가 로직 누락 **34절**, 설치품을 조정하는 기준표 **2절**로 분류했다. 특히 3·4장의 철거·해체·보수 공종은 다수의 인력 수량이 표로 저장됐지만 로직이 없다. 유지관리 197표 전체의 조건·숫자 자리·비목 대조는 미완료다. + +### 153개 후보 분류 합계 + +산림 81절: 직접 누락 69, 기준표 8, 상위 로직 일부 구현·재료 누락 1, 사람 확인 3. 건설 공통 21절: 직접 누락 2, 기준표 19. 토목 13절: 기준표 13. 건축 0절. 기계설비 2절: 기준표 2. 유지관리 36절: 직접 누락 34, 기준표 2. **총 153절 중 직접 누락 105, 기준표 44, 일부 구현·재료 누락 1, 사람 확인 3.** 이 숫자는 원문 절과 자료 연결을 대조한 분류이며, 원문의 모든 표 행 및 모든 로직의 단위·비목까지 통과했다는 뜻은 아니다. + +확인 보강: 위 직접 누락으로 분류한 절의 소요량·계수 표 키가 1,354개 로직의 `찾기(...)` 식에 간접적으로라도 등장하는지 재검색했으며, **참조된 표 키 0개**였다. 이는 표의 `용도.로직키=[]`뿐 아니라 다른 원문번호의 로직이 표를 가져가는 우회 경로도 찾지 못했다는 근거다. 값이 별도 표 없이 수식에 하드코딩됐을 가능성까지 기계적으로 배제하지는 못한다. + +## 3차 전 부문 표 대응 기계 선별 + +원문 `출처`가 한 표에만 대응하는 경우, 원문 Markdown 표 블록과 JSON 표를 1:1로 잡아 **물리적 줄 수**와 같은 순번 줄의 **숫자 집합**을 비교했다. 아래의 숫자 일치도 문자열 조건·단위·비목까지 일치했다는 뜻은 아니다. 원문 가로 표를 JSON의 세로 줄로 푼 경우에는 물리적 줄 수 불일치가 정상일 수 있다. 같은 출처에 여러 JSON 표가 붙거나 원문에 여러 표 블록이 있는 경우에는 이 단순 비교로 판정하지 않았다. + +| 부문 | 전체 표 | 출처 공유(1:1 불가) | 원문 복수 블록(1:1 불가) | 물리 줄 수 다름(피벗 가능) | 줄 수 같음·숫자 전 줄 일치 | 줄 수 같음·숫자 차이(사람 확인) | +|---|---:|---:|---:|---:|---:|---:| +| 산림 | 634 | 137 | 111 | 184 | 87 | 115 | +| 건설 공통 | 575 | 142 | 224 | 149 | 32 | 27 | +| 토목 | 765 | 370 | 195 | 164 | 18 | 18 | +| 건축 | 353 | 336 | 1 | 9 | 3 | 4 | +| 기계설비 | 509 | 292 | 29 | 132 | 40 | 16 | +| 유지관리 | 197 | 103 | 1 | 82 | 1 | 10 | +| 합계 | 3,033 | 1,380 | 561 | 720 | 181 | 190 | + +공통의 1표는 `출처`가 비교 대상 품셈 본문 문자열이 아니어서 이 표 밖에 남았다. 숫자 차이 **190표**는 원문 머리의 숫자·설명 속 수치, JSON 조건 범위의 재표현이 흔하므로 곧바로 오류로 판정하지 않았다. 위 기계 선별은 3,033표를 **검토 대기 갈래로 나눈 것**이며, 25,036줄의 규격·조건·값 검증을 완료한 것으로 계산하지 않는다. + +## 3차 비목 대조 + +1,354개 로직의 호표를 기계적으로 세면 인력 3,255줄은 모두 `노무비`, 재료 675줄은 모두 `재료비`, 하위 로직 990줄은 비목 고정 없이 하위 결과를 나눠 받는다. **종류와 비목의 직접 불일치는 0줄**이다. 그러나 원문에 노무비·재료비·경비를 한꺼번에 적은 덧줄을 어떤 비목으로 나눌지는 이 검사로 확정되지 않는다. + +| 로직 키 | 이름 | 원문 자리(파일 · 절) | 갈래 | 무엇이 다름 | 판정 | 근거 인용 한 줄 | +|---|---|---|---|---|---|---| +| GC000251, GC000703~GC000707, GC000709~GC000719, GC000823~GC000824 (19키) | 플륨관 및 도장공사의 공구손료·잡재료비 등 | 건설품셈 공통 6-7-1; 건축 11-1-1~11-3-1; 기계설비 9-2-2·3 | 비목 | `공구손료 및 잡재료비` 또는 `공구손료 및 소모재료`라는 결합 덧줄을 전액 `경비`에 계상한다. 원문의 결합률을 공구 경비와 재료비로 분리할 근거가 있는지 확인 필요; 현재 자료만으로 오분류 확정 불가. | 의심 | 예: `GC000703` 덧줄 이름 `공구손료 및 잡재료비`, 비목 `경비`; `_틀.md`는 `잡재료비` 예시를 `재료비`로 둠. | + +원문이 비목을 별도로 지정하지 않는 품까지 3,255+675줄 전체의 원문 비목 일치가 입증된 것은 아니다. 특히 위 19줄은 비목의 해석이 필요하므로 확정 흠 수에 넣지 않았다. + +## 3차 임도 우선 표 묶음 · 산림 9-13-7~18 + +구조물터파기 암절취·발파암 12절의 `QF000276`~`QF000287`을 원문 표 본문 줄과 맞댔다. 각 표의 원문 몸줄 수와 JSON 줄 수가 각각 8·5·5·7·4·4·8·5·5·8·5·5로 같으며, 같은 순번 줄의 `적용` 숫자는 **12표 전부 원문 해당 줄에 있다**. `QF000276`은 할석공 1.6, 보통인부 0.8, 브레이커 3.5㎥/hr, 치즐 0.006본/hr, 백호우 k=0.55·f=0.74·E=0.50·Cm=20초를 원문과 대조했다. 이 묶음에서 수치 자리 밀림은 발견하지 못했다. 다만 병합된 `인력(10%)`·`장비(90%)` 조건과 단위 전체, 비목 연결을 12표 모두 수동 검증한 것은 아니므로 최종 상태는 **사람 확인**으로 유지한다. + +같은 임도 우선 범위 `9-4`~`9-12`의 산림 소요량 표 20개도 선별했다. 원문 표 블록 하나와 JSON 표 하나로 잡히는 **14표**(`QF000250`~`QF000255`, `QF000258`, `QF000261`~`QF000264`, `QF000267`~`QF000269`)는 원문 몸줄 수와 JSON 줄 수가 같고, 같은 순번 줄의 `적용` 또는 `수량` 숫자를 원문 해당 줄에서 찾았다. 나머지 **6표**(`QF000256`·`QF000257`·`QF000259`·`QF000260`·`QF000265`·`QF000266`)는 한 출처 안에 원문 표 블록이 둘이라 표 블록별 대응을 별도로 만들어야 한다. 14표도 조건·단위·비목까지 끝난 것은 아니므로 **사람 확인**, 6표는 **미판정**으로 둔다. + +## 3차 임도 우선 표 묶음 · 산림 10장 + +10장의 소요량 표 42개를 원문 표 블록·저장 줄 수로 선별했다. `10-4 중기운반`의 `QF000307`은 원문 몸 16줄, JSON 15줄이며 마지막 **자주식 운반 `크레인(트럭 10TON)`** 줄이 없다. 앞의 15줄은 운반수단·항목 순서가 원문과 같다. 원문 표에는 같은 묶음의 `물탱크(5500L)`, `콘크리트 믹서(6.0㎥)`, `크레인(트럭 10TON)` 세 장비가 있으나 JSON은 앞의 두 장비까지만 담았다. + +| 로직 키 | 이름 | 원문 자리(파일 · 절) | 갈래 | 무엇이 다름 | 판정 | 근거 인용 한 줄 | +|---|---|---|---|---|---|---| +| GF000137 / QF000307 | 중기운반 트레일러(20ton) / 중기운반 기준표 | 산림_표준품셈/본문/제10장_자재․장비_운반/10-04_중기운반.md · 10-4 | 누락 | 기준표의 자주식 운반 크레인(트럭 10TON) 한 줄이 자료에 없다. `GF000137`은 트레일러만 계산해 직접 영향은 미확인이나, 같은 표를 재사용하는 자주식 운반 계산에는 조건이 빠진다. | 확실 | 원문 `| | | 크레인(트럭 10TON) | |`; `QF000307` 마지막 줄은 `콘크리트 믹서(6.0㎥)`이고 총 15줄. | + +`10-6-1·2·3` 인력운반 표 `QF000308`·`QF000309`·`QF000310`은 원문 가로배치·양쪽배치를 JSON 세로 줄로 펼친 규칙까지 확인했다. 토사·석재 20~90m는 원문 4행×좌우 2조 = JSON 8줄, 돌 지게운반 50~1,000m는 11거리 = 11줄, 기타 임업자재 20~200m는 10거리×10자재 수량 = JSON 10줄의 10값칸이다. 각 거리·자재 칸의 모든 숫자를 원문과 자료 순서대로 비교해 일치했고, `값칸` 단위 `인/㎥`, `인/100속`, `인/100매`, `인/100개`, `인/100본`, `인/1000㎡`, `인/톤`도 원문 머리와 같다. 이 세 표의 규격·조건·값·단위 대조는 **끝남, 흠 0**으로 기록한다. 단가 로직 자체는 앞의 누락 절 분류대로 없다. + +`10-9-1 자재의 1회당 표준 운반량`의 `QF000329`도 원문 7자재×5트럭 규격 = **35칸**, JSON 35줄을 대조했다. 각 칸의 중량(kg), 환산량(개·㎡·㎥), 통나무 본수 숫자를 35칸 전부 순서대로 대조해 차이가 없고, 저장 단위도 원문 괄호 속 단위와 맞는다. 이 표의 조건·값·단위 대조는 **끝남, 흠 0**이다. 그러나 `GF000145`가 표를 읽어 운반비를 계산하지 않는 문제는 앞의 **10-9-1 로직 일부 구현** 판정 그대로 남는다. + +`10-13-1·2` 드론·인력운반의 `QF000340`·`QF000341`은 거리 100~600m의 원문 6칸을 각각 JSON 6줄로 펼친 값이 전부 같다. `QF000342`는 준비시간 60분, 왕복비행시간 `0.0098×수평거리+1.3264`, 1회 운반 100본, 드론운반 작업인원 3명의 원문 4항목과 내용·수치가 같다. 세 표의 조건·값·단위 대조는 **끝남, 흠 0**이다. 산림 10-13-1·2의 **단가 로직 누락** 판정은 별개로 유지한다. `QF000343`의 산출 기준 수 5줄은 부모 10-13절의 계산 설명과 별도 대조가 필요하다. diff --git a/docs/raw/M01_점검/보고_줄글.md b/docs/raw/M01_점검/보고_줄글.md new file mode 100644 index 00000000..9e780499 --- /dev/null +++ b/docs/raw/M01_점검/보고_줄글.md @@ -0,0 +1,437 @@ +# M01 점검 결과 · 줄글 + +보고서 표에서 흠과 미판정 항목을 한 줄씩 옮겼다. 같은 흠이 1차·2차·자재품목 대조에 반복되면 근거 보강을 보존했다. 원문 절 누락 분류의 기준표 항목은 흠이 아니므로 제외했다. + +GC000181 | 매트부설(육상) | 연결 | P.P로프(9㎜) 재료 검색어가 `와이어로프(철심) 9㎜`로, 재질과 품명이 다르다. | 확실 + +GC000027 | 강관 조립말비계(이동식) 설치 및 해체 | 연결 | `수평띠장 L1829`가 안전발판을 검색한다. | 확실 + +GC000348 | 터널 방수 | 연결 | 일체식 방수시트가 방수석고보드를 검색한다. | 확실 + +GC000360 | 레일 가스압접 | 연결 | 바퀴숫돌 단면용의 가격 검색어가 도로청소용 빗자루다. 같은 방식으로 다른 바퀴숫돌 규격도 연결되어 있다. | 확실 + +GC000560 | 철골 공장용접(기본용접공수) | 연결 | CO₂ 와이어·탄산가스·잠호용접와이어의 가격 검색어가 모두 아세틸렌이다. | 확실 + +GC000575 | 벽돌 쌓기(시멘트벽돌) | 연결 | 시멘트벽돌 가격 검색어가 주차라인블록이다. | 확실 + +GC000604 | 카페트 설치 | 연결 | 카페트 가격 검색어가 불연천장재(마이톤)다. | 확실 + +GC000723 | 동관 용접접합 | 연결 | 동 및 동합금용 용접봉과 플럭스가 둘 다 아세틸렌을 검색한다. | 확실 + +GC000938 | Stop-Log 설치 | 연결·단위 | `광명단` 재료가 `목재전용도료 오일스테인`을 검색한다. 아세틸렌 원문은 `병` 수량 1.98인데 호표는 `kg` 1.98로 환산 없이 사용한다. | 확실 + +GC000943 | Roller Gate Guide Metal 제작 | 연결·단위 | `Tar Epoxy` 호표에 원문의 전기 `550㎾h`를 그대로 넣고 `Tar Epoxy` 원문 `0.75ℓ`를 누락했다. 아세틸렌도 원문 `1.6병`을 `1.6kg`으로 사용한다. | 확실 + +GC000948 | Tainter Gate Anchorage 제관 | 연결·단위 | `Tar Epoxy` 호표에 전력 `420㎾h`를 넣고 원문의 `3.0ℓ`를 누락했다. 아세틸렌 원문 `1.5병`도 `1.5kg`으로 사용한다. | 확실 + +GC000993 | 플랜트 공사별 설치 소모자재(참고) | 연결 | 용접봉(전기·산소) 모두 아세틸렌을, 사포는 복사용지를, 걸레는 그리스를, 용접면·용접홀다·용접앞치마는 노면파쇄기 팁을, 페인트붓은 도로청소용 빗자루를 검색한다. 원문 품명과 재료 가격 연결이 다르다. | 확실 + +GC000027 | 강관 조립말비계(이동식) 설치 및 해체 | 단위 | 가새 L1518-2개 `조` 후보 0 | 의심 + +GC000181 | 매트부설(육상) | 단위 | 철근(19㎜) `m` 후보 0 | 의심 + +GC000616 | 걸레받이 설치 | 단위 | 중밀도섬유판 걸레받이 `m` 후보 0 | 의심 + +GC000211 | 합판거푸집 설치 및 해체 | 단위 | 폼타이 12.7·9.5·7.9㎜의 `본` 후보 각각 0 (3곳) | 의심 + +GC000855 | 가스용 강관 나사식 접합 및 배관 | 단위 | 스레트실테이프 `㎝` 후보 0 | 의심 + +GC000859 | 직독식 가스미터 설치 | 단위 | 스레트실테이프 `㎝` 후보 0 | 의심 + +GC000938 | Stop-Log 설치 | 단위 | 함석 `대` 후보와 단위 성격 다름. 원문 역시 `대`; 가격 마스터 문제 가능. | 의심 + +GC000943, GC000948 | 수력발전 게이트 제작 | 단위 | 중유 `D/M` 후보 단위가 다름 (2곳) | 의심 + +GC000948 | Tainter Gate Anchorage 제관 | 단위 | Zinc primer `ℓ` 후보 단위가 다름 | 의심 + +GC000993 | 설치 소모자재(참고) | 단위 | 용접장갑 `족` 후보 0 | 의심 + +GF000160 | 합판거푸집 | 단위 | 폼타이 `본` 후보 0 | 의심 + +GF000165 | 콘크리트 포장 거푸집 | 단위 | 철재 거푸집 손료 `본` 후보 0 | 의심 + +GF000060 | 목재칩 포설 | 연결 | 우드칩 가격 검색어가 미송각재다. | 확실 + +GF000143 | 케이블 크레인 운반대 설치 | 연결 | 소재(원목)가 제재 각재를 검색한다. | 확실 + +GF000161 | 문양거푸집 설치 및 해체 | 연결 | 문양 스티로폴이 그라스울 샌드위치판넬을 검색한다. | 확실 + +GF000163 | 포장절단(1차 절단) | 연결 | 커터 블레이드가 굴착기 치즐을 검색한다. | 확실 + +GF000172 | 맹암거 | 연결 | 부직포가 직포를, 잡석이 강모래를 검색한다. | 확실 + +GF000180 | 집수정 | 연결 | 집수정 뚜껑(스틸그레이팅)이 PE빗물받이 뚜껑을 검색한다. | 확실 + +GF000181 | 맨홀 | 연결 | 원형거푸집 두 줄이 폼타이를 검색한다. | 확실 + +GF000191 | 부직포설치 | 연결 | 부직포가 직포를 검색한다. | 확실 + +GF000198 | 실런트(20×25㎜) | 연결 | 실런트가 수팽창 지수재를 검색한다. | 의심 + +GF000199 | 신구 BOX접합 | 연결 | 콘크리트용 에폭시 접착제는 석고본드를, 시너는 목재전용 오일스테인을 검색한다. | 확실 + +GF000201 | 다웰바 설치 신축 이음부 | 연결·단위 | 강봉 D32가 트러스를 검색하고, PVC 파이프 Ø35㎜는 D30㎜ 제품을 검색한다. | 확실 + +GF000202 | 다웰바 설치 접속스라브부 | 연결·단위 | 원형철근 Ø29는 D25 가공비를, 탄성고무받침은 익스팬션조인트휠러를, 다웰바캡은 보강재를 검색한다. | 확실 + +GF000203 | 물끊기 홈(NOTCH) 설치 | 연결 | 물끊기 홈 재료가 방수석고보드를 검색한다. | 확실 + +GF000220 | 돌쌓기 메쌓기(장비) | 연결 | 채움재 잡석이 강모래를 검색한다. | 확실 + +GF000224 | 돌붙임 메붙임(장비) | 연결 | 채움재 잡석이 강모래를 검색한다. | 확실 + +GF000235 | 사각형 돌망태 | 연결 | 철망태가 기초사석을 검색한다. | 확실 + +GC000001 | 철제조립식 가설건축물 손료 | 연결 | 외부·내부 PANEL, CANOPY, 박공PANEL, 천장판을 T챤넬로, 중도리를 트러스로, T-BAR를 처마후레싱으로 검색한다. | 확실 + +GC000002 | 콘테이너형 가설건축물 손료 | 연결·단위 | 3×3·3×6·3×9m 건축물 셋 모두 12×3×2.6m 콘테이너를 검색한다. 규격 차이를 반영하지 않는다. | 확실 + +GC000031 | 낙하물 방지망(비계) 설치 및 해체 | 연결 | 브라켓을 안전발판으로, 그물망을 옥상방수용 직포로 검색한다. | 확실 + +GC000032 | 낙하물 방지망(플라잉넷) 설치 및 해체 | 연결 | 브라켓을 안전발판으로, 사다리를 강관비계로, 그물망을 직포로 검색한다. | 확실 + +GC000037 | 철골 안전망 설치 및 해체 | 연결 | 그물망을 옥상방수용 직포로 검색한다. | 확실 + +GC000038 | 비계주위 보호망 설치 및 해체 | 연결 | 보호망을 옥상방수용 직포로 검색한다. | 확실 + +GC000039 | 갱폼주위 보호망 설치 및 해체 | 연결 | 보호망을 옥상방수용 직포로 검색한다. | 확실 + +GC000062 | 방진망 설치 및 해체 | 연결 | 방진망을 PVC 천막지로 검색한다. | 확실 + +GC000068 | 건축물보양 | 연결 | 부직포를 직포로, 풀을 그리스로 검색한다. | 확실 + +GC000127 | 버팀목 설치·해체 | 연결·단위 | 각재 10×10㎝가 30×30㎜ 각재를 검색한다. | 확실 + +GC000185 | 연직배수재(PBD) 타설 | 연결 | 드레인 보드를 고무바로 검색한다. | 확실 + +GC000197 | 차수재 부설(1겹) | 연결 | 부직포·지오콤포지트는 옥상방수용 직포로, 벤토나이트매트는 PET 매트로 검색한다. | 확실 + +GC000203 | 에폭시 콘크리트 접착제 바르기 | 연결 | 에폭시 접착제 두 종은 석고본드를, 시너는 목재용 오일스테인을 검색한다. | 확실 + +GC000212 | 강재거푸집 인력 설치 및 해체 | 연결 | 강재거푸집 손료가 K-SPAN 패널 가격을 검색한다. | 확실 + +GC000215 | 문양거푸집(판넬) 설치 및 해체 | 연결 | 문양거푸집 패널이 K-SPAN 패널을 검색한다. | 확실 + +GC000216 | 합성수지(P.E) 원형 맨홀 거푸집 설치 및 해체 | 연결 | 원형맨홀거푸집 다섯 규격 모두 폼타이를 검색한다. | 확실 + +GC000319 | 차선도색 융착식 도료 수동식 | 연결 | 융착식 도료 프라이머가 신구접착·방청제를 검색한다. | 확실 + +GC000337 | 낙석방지망 철망 및 와이어 설치 | 단위 | 결속선 원문은 `m`, 검색어는 철선 #8이다. 가격 후보가 길이 단위인지 확인 필요. | 의심 + +GC000374 | 강교 제작 기타재료 | 연결 | 용접봉(강교 제작)이 아세틸렌을 검색한다. | 확실 + +GC000375 | 강교 소재 표면처리 | 연결 | 무기질아연말 샵프라이머가 목재용 오일스테인을 검색한다. | 확실 + +GC000431, GC000432 | 보링 천공(토사·암반) | 연결 | 코아바렐·크라운비트·파이프·코아리프터 등 원문 소모품 12줄을 굴착기 치즐로 검색한다. | 확실 + +GC000433, GC000434, GC000435 | 표준관입·시료채취·베인전단시험 | 연결 | 샘플러·신월튜브·베인 블레이드가 굴착기 치즐을 검색한다. | 확실 + +GC000442, GC000443, GC000444, GC000445, GC000446 | 대구경 보링 천공 | 연결 | 윙비트가 치즐을, 벤토나이트가 수팽창 지수재를 검색한다. | 확실 + +GC000447, GC000448, GC000449 | 대구경 보링 암반층 천공 | 연결 | 에어해머·버튼비트가 치즐을 검색한다. | 확실 + +GC000565 | 철골 현장용접(CO2 반자동) | 연결 | CO₂ 와이어와 탄산가스가 모두 아세틸렌을 검색한다. | 확실 + +GC000576 | 치장벽돌 공간쌓기 및 치장줄눈 | 연결 | 치장벽돌이 주차라인블록을 검색한다. | 확실 + +GC000604 | 카페트 설치(1차 발견 보강) | 연결 | 카페트 외에 펠트도 옥상방수용 직포를 검색한다. | 확실 + +GC000616 | 걸레받이 설치 | 연결 | 테라조와 합성수지 걸레받이 모두 처마후레싱을 검색한다. | 확실 + +GC000618 | 도배바름(초배 · 정배) | 연결 | 도배용 풀이 그리스를 검색한다. | 확실 + +GC000620, GC000621, GC000622, GC000623, GC000625, GC000626, GC000628 | 단열재 시공 여러 절 | 연결 | 단열재·단열판에 완제품 그라스울 샌드위치판넬 가격을 연결한다. 단열재 종류·두께가 원문 또는 입력에 따라 달라질 수 있어 동등성 확인 필요. | 의심 + +GC000627 | 방습필름 설치 | 연결 | 방습필름이 PVC 천막지를 검색한다. | 확실 + +GC000637, GC000638, GC000639 | 시트방수 붙임 3방식 | 연결 | 방수시트 세 로직 모두 방수석고보드를 검색한다. | 확실 + +GC000646 | 벤토나이트방수 붙임 | 연결 | 벤토나이트방수재를 석고보드로, PE필름을 PVC천막지로, 카트리지를 화약 카트리지로, 와셔를 핀볼트로 검색한다. | 확실 + +GC000655 | 폴리카보네이트 설치 | 연결 | 폴리카보네이트가 불연천장재(마이톤)를 검색한다. | 확실 + +GC000665 | 와이어메시 바닥깔기 | 연결 | 와이어메시가 메쉬스크린을 검색한다. | 확실 + +GC000666 | 인서트 설치 | 연결 | 인서트가 L형앵커볼트를 검색한다. | 확실 + +GC000670 | 앵커고정식 난간 설치(공장제작) | 연결 | AL리벳이 작은나사(스크류)를 검색한다. | 확실 + +GC000724 | 스테인리스 강관 용접접합 | 연결 | TIG 용접봉과 아르곤가스 모두 아세틸렌을 검색한다. | 확실 + +GC000752 | 제연댐퍼 설치 | 연결 | 블라인드리벳을 핀볼트로, 철물을 철근부식방지재로 검색한다. | 확실 + +GC000901 | 플랜트 배관 보온(성형물) | 연결 | 보온 성형물이 STS 단열바 프레임을, Sheet Metal Screw가 빗자루를 검색한다. | 확실 + +GC000931 | 수차 설치(Kaplan · Francis · Propeller) | 연결 | 걸레가 그리스를, 비닐세트·소창직은 PVC 코팅철선을, 시너·광명단·조합페인트는 목재용 오일스테인을 검색한다. | 확실 + +GC000932 | 수력 발전기 설치 | 연결·단위 | 목탄·걸레가 그리스를, 가스용접봉·신주용접봉이 아세틸렌을, 비닐시트가 사포를 검색한다. 산소 원문은 4,500ℓ/병인데 로직은 병 수에 6,000을 곱한다. | 확실 + +GC000933, GC000934 | 수문 제작(Tainter·Roller Gate) | 연결 | 광명단이 목재용 오일스테인을 검색한다. | 확실 + +GC000935, GC000936 | 수문 설치(Tainter·Roller Gate) | 연결 | 광명단이 목재용 오일스테인을 검색한다. | 확실 + +GC000937 | Stop-Log 제작 | 연결 | 광명단·페인트가 신구접착·방청제를 검색한다. | 확실 + +GC000941 | Steel Penstock 현장제관 | 연결 | 시너 두 줄 모두 목재용 오일스테인을 검색한다. | 확실 + +GC000945 | Tainter Gate Guide Metal 제작 | 연결 | 전기용접봉이 아세틸렌을 검색한다. | 확실 + +GC000947 | Trash Rack 제작 | 연결 | 연마석·톱날이 도로청소용 빗자루를, Bite가 굴착기 치즐을 검색한다. | 확실 + +GC000950 | Trash Rack 설치 | 연결 | 시너가 목재용 오일스테인을 검색한다. | 확실 + +GC000972 | 노 기밀시험 | 연결 | 가루비누가 그리스를 검색한다. | 확실 + +GC000986 | Over Head Crane 설치 | 연결 | 걸레가 그리스를 검색한다. | 확실 + +GC000987 | Gantry Crane 설치 | 연결 | 광명단이 목재용 오일스테인을 검색한다. | 확실 + +GC000993 | 플랜트 공사별 설치 소모자재(1차 발견 보강) | 연결 | 1차 발견 외에 시너도 목재용 오일스테인을, 비닐시드도 PVC천막지를 검색한다. | 확실 + +GC000931~GC000950, GC000986, GC000987, GC000993 (23키) | 아세틸렌이 포함된 플랜트·운반기계 로직 | 단위 | 아세틸렌 원문 수량은 병 단위(규격 2,100·2,500·3,500·4,000·4,500ℓ 등)인데 호표는 같은 수를 kg로 사용한다. 해당 23키 모두 병→kg 환산식이 없다. | 확실 + +GC001050 | 차선도색 융착식 도료(수동식) | 연결 | 프라이머가 신구접착·방청제를, 프로판가스가 아세틸렌을 검색한다. | 확실 + +GC001055 | 현장가열 표층재생 | 누락·단위 | 원문 주⑤의 `100㎡당 팁(날) 0.7개`를 계산에 계상하지 않는다. 결과단위 `원/㎡`라면 0.007개/㎡가 필요하지만 호표에 팁 줄이 없다. | 확실 + +없음 (QF000041) | 임업용 동력기계톱 수확베기 | 누락 | 벌도목별 작업량과 벌목부가 저장됐지만 단가 로직이 없다. 후보 분류: 누락 확정. | 확실 + +없음 (QF000045) | 단목베기 100본당 | 누락 | 흉고직경 11구간의 벌목·가지제거·토막내기 및 작업보조 소요인력은 표에 있으나 단가 로직이 없다. 후보 분류: 누락 확정. | 확실 + +없음 (QF000047) | 단목베기 1,000㎡당 | 누락 | 나무높이별 벌목부·보통인부·굴착기 소요량은 표에 있으나 단가 로직이 없다. 후보 분류: 누락 확정. | 확실 + +없음 (산림 5-1-1) | 5-1-1 | 누락 | 관목굴취 `특별인부 0.084…0.408인`, `보통인부 0.012…0.072인/10본당`이 있는데 해당 로직 없음. | 확실 + +없음 (산림 5-1-2) | 5-1-2 | 누락 | 교목굴취 나무높이별 특별인부·보통인부 수량이 있는데 해당 로직 없음. | 확실 + +없음 (산림 5-1-3) | 5-1-3 | 누락 | 교목굴취 근원직경별 인력·굴착기·크레인 수량이 있는데 해당 로직 없음. | 확실 + +없음 (산림 5-3-2) | 5-3-2 | 누락 | 관목식재(단식) 수고별 특별인부·보통인부 수량이 있는데 해당 로직 없음. | 확실 + +없음 (산림 5-3-3) | 5-3-3 | 누락 | 관목식재(군식) 수고별 특별인부·보통인부 수량이 있는데 해당 로직 없음. | 확실 + +없음 (산림 5-3-4) | 5-3-4 | 누락 | 교목식재 나무높이별 인력·장비 수량이 있는데 해당 로직 없음. | 확실 + +없음 (산림 5-3-5) | 5-3-5 | 누락 | 교목식재 흉고직경별 인력·장비 수량이 있는데 해당 로직 없음. | 확실 + +없음 (산림 5-16-1) | 5-16-1 | 누락 | 단끊기 공정별 보통인부 수량이 있는데 해당 로직 없음. | 확실 + +미판정 (5-16-2) | 5-16-2 | 누락 | 선떼붙이기 표는 단끊기 `1m`와 떼붙임 `물량산출`의 합성 기준. 두 하위 로직과 조합하여 처리할 의도인지 확인 필요. | 의심 + +미판정 (5-17-1) | 5-17-1 | 누락 | 떼조공은 단끊기·줄떼심기·선떼붙이기 조합 표. 독립 로직 필요성 확인 필요. | 의심 + +미판정 (5-17-2) | 5-17-2 | 누락 | 돌조공은 단끊기·돌쌓기 조합 표. 독립 로직 필요성 확인 필요. | 의심 + +없음 (산림 5-19-1) | 5-19-1 | 누락 | 표토절취 깊이별 보통인부 소요량이 있는데 해당 로직 없음. | 확실 + +없음 (산림 5-19-2) | 5-19-2 | 누락 | 표토펴기 ㎥당·100㎡당 보통인부 수량이 있는데 해당 로직 없음. | 확실 + +없음 (산림 5-26-1) | 5-26-1 | 누락 | 막갈이 사토·양토·식토 × 깊이별 `인/992㎡당` 수량이 있는데 해당 로직 없음. | 확실 + +없음 (산림 5-26-2) | 5-26-2 | 누락 | 흙부수기 토성 × 깊이별 `인/992㎡당` 수량이 있는데 해당 로직 없음. | 확실 + +없음 (산림 5-26-3) | 5-26-3 | 누락 | 돌자갈치우기 토성 × 함량별 `인/992㎡당` 수량이 있는데 해당 로직 없음. | 확실 + +없음 (산림 5-28-2) | 5-28-2 | 누락 | 방초·야자섬유매트 포장 `조경공 2인+보통인부 1인`, 90·130㎡/일 시공량이 있는데 해당 로직 없음. | 확실 + +GF000210 | 유로폼 설치 및 해체 | 누락 | `12-38-1`은 유로폼 자재 손료의 사용횟수 기준이라 독립 단가 로직 불필요; `12-38-2`의 패널·부자재 사용수량은 상위 인력 로직이 맡는 공종이지만 실제 호표에 빠져 재료비가 계산되지 않는다. | 확실 + +GC000575 | 시멘트벽돌 | 연결 | 검색어 주차라인블록 1000X300X150; 자재품목표 시멘트벽돌 `190×90×57mm` `MT005760` 등 17줄 있음 | 확실 + +GC000604 | 카페트 | 연결 | 검색어 불연천장재(마이톤); 자재품목표 카페트 `MT008142`, 타일카페트 `MT008140` 있음 | 확실 + +GC000319, GC001050 | 융착식 도료 프라이머 | 연결 | 검색어 신구접착및방청제 LDS-MP; 자재품목표 융착식 프라이머 `MT003454` 있음(ℓ; 원문 kg면 환산 검토 필요) | 확실 + +GC001050 | 프로판가스 | 연결 | 검색어 아세틸렌 98%; 자재품목표 프로판가스 일반용 `MT003175`, kg 있음 | 확실 + +GC000375 | 무기질아연말 샵프라이머 | 연결 | 검색어 목재전용도료 오일스테인; 자재품목표 무기질 아연말 전처리 프라이머 `MT003460`, 무기질 아연말 프라이머 `MT003461` 있음 | 확실 + +GC000665 | 와이어메시 | 연결 | 검색어 메쉬스크린 H=0.9m; 자재품목표 와이어메쉬(용접철망) `MT007128` 등 있음 | 확실 + +GC000933~GC000936, GC000987 | 광명단 | 연결 | 검색어 목재전용도료 오일스테인; 자재품목표 광명단조합페인트 `MT003375`~`MT003377` 있음 | 확실 + +GF000220, GF000224 | 잡석(채움재) | 연결 | 검색어 모래 강모래; 자재품목표 자연자갈·잡석 `MT003848`, 깬잡석 `MT004113` 등 있음 | 확실 + +GC000181 | P.P로프 9㎜ | 연결 | 검색어 와이어로프(철심) 9㎜; 자재품목표 P.P로프는 `MT007584` 등 6줄 있으나 9㎜ 규격은 검색 없음 | 확실 + +GC000348 | 일체식 방수시트 | 연결 | 검색어 방수석고보드 12.5t; 자재품목표 ARS아크릴방수시트 `MT006814`·`MT006815` 있음(일체식과 동등성 미확인) | 확실 + +GC000251, GC000703~GC000707, GC000709~GC000719, GC000823~GC000824 (19키) | 공구손료·잡재료비 등 | 비목 | 결합 덧줄 전액을 경비에 계상; 원문이 공구 경비와 재료비 비율을 나누지 않아 오분류 여부는 확인 필요 | 의심 + +없음 (산림 6-4-3) | 6-4-3 | 누락 | 소금처리와 제거지점 표시의 `인/100본` 인력 수량이 `QF000167` 5줄에 있으나 로직키가 비어 있다. | 확실 + +없음 (산림 6-4-4) | 6-4-4 | 누락 | 뿌리 고살·덩굴 제거 등의 `인/100본` 수량은 `QF000168`에 있으나 로직키가 비어 있다. 같은 절의 `QF000169` 피복도·본수 환산표는 단독 로직이 필요 없는 보조 기준이다. | 확실 + +없음 (산림 6-7-1) | 6-7-1 | 누락 | 교목 시비의 근원직경별 특별인부·보통인부 수량은 `QF000173` 10줄에 있으나 로직키가 비어 있다. | 확실 + +없음 (산림 7-1-1) | 7-1-1 | 누락 | 수확 인력집재의 거리·경사 조건별 작업량 표가 있으나 산림 로직은 이웃 절 `7-1-2`부터 있다. | 확실 + +없음 (산림 7-5-2) | 7-5-2 | 누락 | 숲가꾸기·병해충방제 트랙터 집재의 거리·본당재적별 작업량 및 3인 1조 구성이 있으나 로직 없음. | 확실 + +없음 (산림 7-7-2) | 7-7-2 | 누락 | 숲가꾸기·병해충방제 HAM200 집재의 집재재적·거리별 작업량과 3인 1조 구성이 있으나 로직 없음. | 확실 + +없음 (산림 7-8-1) | 7-8-1 | 누락 | 타워야더 가선설치의 집재방식·가선길이별 소요인력이 있으나 로직 없음. `GF000068` 비고도 `가선설치 · 해체(7-8-1 · 7-8-2) 따로`라고 명시. | 확실 + +없음 (산림 7-8-2) | 7-8-2 | 누락 | 타워야더 가선해체의 집재방식·가선길이별 소요인력이 있으나 로직 없음. 상위 집재 로직에 포함되지 않음. | 확실 + +없음 (산림 7-9-1) | 7-9-1 | 누락 | 수확 스윙야더 집재의 집재재적·거리별 작업량 및 4인 1조 구성이 있으나 로직 없음. | 확실 + +없음 (산림 7-9-2) | 7-9-2 | 누락 | 숲가꾸기·소나무재선충병방제 스윙야더 집재의 집재재적·거리별 작업량 및 4인 1조 구성이 있으나 로직 없음. | 확실 + +없음 (산림 8-1-1) | 8-1-1 | 누락 | 약제주입기 천공수별 `소요인력(ha당)`이 있으나 산림 로직 없음. | 확실 + +없음 (산림 8-1-2) | 8-1-2 | 누락 | 약제주입병 천공수별 `소요인력`이 있으나 산림 로직 없음. | 확실 + +없음 (산림 8-6-2) | 8-6-2 | 누락 | 드론방제의 무인헬기 `ha당` 인력 편성·소요량이 있으나 로직 없음(`8-6-1` 유인헬기만 있음). | 확실 + +없음 (산림 8-6-3) | 8-6-3 | 누락 | 지상방제의 차량살포 인력 편성과 물량 조건이 있으나 로직 없음. | 확실 + +없음 (산림 9-4-1) | 9-4-1 | 누락 | 암파쇄 브레이커 작업능력·치즐소모량 표. | 확실 + +없음 (산림 9-5-1) | 9-5-1 | 누락 | 육상 암석 절취 발파 10%의 폭약·화약공 수량 표. | 확실 + +없음 (산림 9-5-2) | 9-5-2 | 누락 | 암석 절취 깎기 90%의 암종별 작업능력·치즐소모량 표. | 확실 + +없음 (산림 9-6-1) | 9-6-1 | 누락 | 기계소할 15%의 브레이커 작업능력·치즐소모량 표. | 확실 + +없음 (산림 9-7-2) | 9-7-2 | 누락 | 무근콘크리트 깨기 T=30㎝ 이상의 작업능력·치즐·보통인부 표. | 확실 + +없음 (산림 9-8-2) | 9-8-2 | 누락 | 철근콘크리트 깨기 T=30㎝ 이상의 작업능력·치즐·보통인부 표. | 확실 + +없음 (산림 9-10-1) | 9-10-1 | 누락 | 콘크리트 깨기 작업능력·치즐·보통인부 표. | 확실 + +없음 (산림 9-10-2) | 9-10-2 | 누락 | 아스팔트 깨기 작업능력·치즐·보통인부 표. | 확실 + +없음 (산림 9-11-1) | 9-11-1 | 누락 | 콘크리트 커트 인력·장비·절단깊이별 수량 표. | 확실 + +없음 (산림 9-12-2) | 9-12-2 | 누락 | 측구터파기 암절취의 할석공·브레이커 적용량 표. | 확실 + +없음 (산림 9-12-3) | 9-12-3 | 누락 | 측구터파기 발파암의 할석공·브레이커 적용량 표. | 확실 + +없음 (산림 9-13-7) | 9-13-7 | 누락 | 구조물터파기 육상 암절취 0~1m, 할석공·브레이커 적용량. | 확실 + +없음 (산림 9-13-8) | 9-13-8 | 누락 | 구조물터파기 육상 암절취 1~2m, 할석공·브레이커 적용량. | 확실 + +없음 (산림 9-13-9) | 9-13-9 | 누락 | 구조물터파기 육상 암절취 2~3m, 할석공·브레이커 적용량. | 확실 + +없음 (산림 9-13-10) | 9-13-10 | 누락 | 구조물터파기 용수 암절취 0~1m, 인력·브레이커 적용량. | 확실 + +없음 (산림 9-13-11) | 9-13-11 | 누락 | 구조물터파기 용수 암절취 1~2m, 인력·브레이커 적용량. | 확실 + +없음 (산림 9-13-12) | 9-13-12 | 누락 | 구조물터파기 용수 암절취 2~3m, 인력·브레이커 적용량. | 확실 + +없음 (산림 9-13-13) | 9-13-13 | 누락 | 구조물터파기 육상 발파암 0~1m, 할석공·브레이커 적용량. | 확실 + +없음 (산림 9-13-14) | 9-13-14 | 누락 | 구조물터파기 육상 발파암 1~2m, 할석공·브레이커 적용량. | 확실 + +없음 (산림 9-13-15) | 9-13-15 | 누락 | 구조물터파기 육상 발파암 2~3m, 할석공·브레이커 적용량. | 확실 + +없음 (산림 9-13-16) | 9-13-16 | 누락 | 구조물터파기 용수 발파암 0~1m, 할석공·브레이커 적용량. | 확실 + +없음 (산림 9-13-17) | 9-13-17 | 누락 | 구조물터파기 용수 발파암 1~2m, 할석공·브레이커 적용량. | 확실 + +없음 (산림 9-13-18) | 9-13-18 | 누락 | 구조물터파기 용수 발파암 2~3m, 할석공·브레이커 적용량. | 확실 + +없음 (산림 9-16-3) | 9-16-3 | 누락 | 살수의 흡입준비·운반 등 작업시간 기준; `9-16-1~2` 노체포설·다짐 로직과 별개. | 확실 + +없음 (산림 9-19-3) | 9-19-3 | 누락 | 비탈면 고르기 발파암 보통인부·공기압축기 수량; `9-19-1~2` 로직과 별개. | 확실 + +없음 (산림 10-6-1) | 10-6-1 | 누락 | 토사·석재의 운반거리별 보통인부 `인/㎥당` 표가 있으나 로직 없음. | 확실 + +없음 (산림 10-6-2) | 10-6-2 | 누락 | 돌 지게운반 거리별 보통인부 표가 있으나 로직 없음. | 확실 + +없음 (산림 10-6-3) | 10-6-3 | 누락 | 기타 임업자재 운반거리·자재종류별 보통인부 표가 있으나 로직 없음. | 확실 + +없음 (산림 10-8-1) | 10-8-1 | 누락 | 케이블 크레인 짐내리기의 운반기구별 인부·신호수 수량이 있으나 로직 없음. `10-8-2·3`은 별도 설치·운전 로직. | 확실 + +없음 (산림 10-9-1) | 10-9-1 | 누락 | 상위 `GF000145(10-9)`는 시간당 기계경비만 계산한다. 비고에 `10-9-1 운반량 쓰는 식 요소 없음`을 명시하므로 자재별 1회 운반량을 반영한 단위 운반비 계산은 빠졌다. | 확실 + +없음 (산림 10-12-1) | 10-12-1 | 누락 | 덤프운반 토사의 굴착기 적재 `Q1=3600×q0×K×f×E0/Cm`와 운반 조건이 있으나 해당 로직 없음. | 확실 + +없음 (산림 10-13-1) | 10-13-1 | 누락 | 드론운반 수평거리 100~600m별 인원 수량이 있으나 로직 없음. | 확실 + +없음 (산림 10-13-2) | 10-13-2 | 누락 | 인력운반 거리별 인원 수량과 드론운반 시간·본수 기준이 있으나 로직 없음. | 확실 + +없음 (산림 13-4-6) | 13-4-6 | 누락 | 밑돌·천단 돌쌓기 뒷길이별 석공 `0.05·0.08…인` 표가 있으나 `13-4-1·2·4·5` 이웃 로직만 있다. | 확실 + +없음 (산림 13-10-2) | 13-10-2 | 누락 | 나무 말뚝박기 말구·길이별 보통인부 수량이 있으나 `13-10-1` 말뚝 다듬기 로직만 있다. | 확실 + +없음 (산림 13-11-1) | 13-11-1 | 누락 | 원형 돌망태 지름별 조약돌량·작업 수량 표가 있으나 로직은 `13-11-3·4` 다른 형태만 있다. | 확실 + +없음 (산림 13-11-2) | 13-11-2 | 누락 | 타원형 돌망태 지름별 조약돌량·작업 수량 표가 있으나 로직은 `13-11-3·4` 다른 형태만 있다. | 확실 + +없음 (산림 13-15-1) | 13-15-1 | 누락 | 목재 껍질벗기기 말구별 보통인부 수량이 있으나 로직 없음. | 확실 + +없음 (건설 공통 7-4-2) | 7-4-2 | 누락 | 석재판 붙임 앵커 긴결공법의 인력 수량·일당 시공량 표 `QC000495`가 있으나 로직 없음. | 확실 + +없음 (건설 공통 7-4-3) | 7-4-3 | 누락 | 석재판 붙임 강제트러스 지지공법의 인력 수량·일당 시공량 표 `QC000496`이 있으나 로직 없음. | 확실 + +없음 (건설 유지관리 3-1-1) | 3-1-1 | 누락 | 콘크리트구조물 인력 헐기 직종·시공량. | 확실 + +없음 (건설 유지관리 3-1-2) | 3-1-2 | 누락 | 콘크리트구조물 기계 헐기 인력·장비·시공량. | 확실 + +없음 (건설 유지관리 3-1-3) | 3-1-3 | 누락 | 철골재 인력 철거 직종·시공량. | 확실 + +없음 (건설 유지관리 3-1-4) | 3-1-4 | 누락 | 철골재 기계 철거 직종·장비·시공량. | 확실 + +없음 (건설 유지관리 3-1-5) | 3-1-5 | 누락 | 석축 인력 헐기 뒷길이별 할석공·보통인부 수량. | 확실 + +없음 (건설 유지관리 3-2-1) | 3-2-1 | 누락 | 금속기와 해체 지붕잇기공 수량. | 확실 + +없음 (건설 유지관리 3-2-2) | 3-2-2 | 누락 | 흡음텍스 해체 내장공 수량. | 확실 + +없음 (건설 유지관리 3-2-3) | 3-2-3 | 누락 | 경량천장철골틀 해체 내장공 수량. | 확실 + +없음 (건설 유지관리 3-2-4) | 3-2-4 | 누락 | 조적벽 해체 조적공 수량. | 확실 + +없음 (건설 유지관리 3-2-5) | 3-2-5 | 누락 | 경량벽체철골틀 해체 내장공 수량. | 확실 + +없음 (건설 유지관리 3-2-6) | 3-2-6 | 누락 | 석고판 해체 벽·천장별 내장공 수량. | 확실 + +없음 (건설 유지관리 3-2-7) | 3-2-7 | 누락 | 도배 해체 벽·천장별 도배공 수량. | 확실 + +없음 (건설 유지관리 3-2-8) | 3-2-8 | 누락 | PVC계바닥재 해체 내장공 수량. | 확실 + +없음 (건설 유지관리 3-2-9) | 3-2-9 | 누락 | 타일 해체 붙임방식별 타일공 수량. | 확실 + +없음 (건설 유지관리 3-2-10) | 3-2-10 | 누락 | 기존방수층·보호층 철거 착암공 등 수량. | 확실 + +없음 (건설 유지관리 3-2-11) | 3-2-11 | 누락 | 기존방수층 제거·바탕처리 방수공 수량. | 확실 + +없음 (건설 유지관리 3-2-12) | 3-2-12 | 누락 | 석면건축자재 해체 내·외장재별 석면해체공 수량. | 확실 + +없음 (건설 유지관리 3-3-1) | 3-3-1 | 누락 | 콘크리트·모르타르면 재도장 바탕처리 도장공·시공량. | 확실 + +없음 (건설 유지관리 3-3-2) | 3-3-2 | 누락 | 철재면 재도장 바탕처리 A·B급별 도장공·시공량. | 확실 + +없음 (건설 유지관리 3-3-3) | 3-3-3 | 누락 | 목재면 재도장 바탕처리 A·B급별 도장공·시공량. | 확실 + +없음 (건설 유지관리 3-4-1) | 3-4-1 | 누락 | 지붕 덧씌우기 지붕잇기공·시공량. | 확실 + +없음 (건설 유지관리 3-4-2) | 3-4-2 | 누락 | 지붕 재설치 지붕잇기공·시공량. | 확실 + +없음 (건설 유지관리 3-4-3) | 3-4-3 | 누락 | 도배 교체 벽·천장별 인력·시공량. | 확실 + +없음 (건설 유지관리 3-4-4) | 3-4-4 | 누락 | PVC계바닥재 교체 내장공·시공량. | 확실 + +없음 (건설 유지관리 3-4-5) | 3-4-5 | 누락 | 타일 교체 붙임방식별 인력·시공량. | 확실 + +없음 (건설 유지관리 4-1-1) | 4-1-1 | 누락 | 배관 해체 규격·재질별 배관공·보통인부 수량. | 확실 + +없음 (건설 유지관리 4-1-2) | 4-1-2 | 누락 | 각형덕트 해체 두께별 인력 수량. | 확실 + +없음 (건설 유지관리 4-1-3) | 4-1-3 | 누락 | 스파이럴덕트 해체 두께·규격별 덕트공·보통인부 수량. | 확실 + +없음 (건설 유지관리 4-1-4) | 4-1-4 | 누락 | 배관보온 해체 재질·규격별 보온공·보통인부 수량. | 확실 + +없음 (건설 유지관리 4-1-5) | 4-1-5 | 누락 | 덕트보온 해체 재질별 보온공 수량. | 확실 + +없음 (건설 유지관리 4-1-6) | 4-1-6 | 누락 | 펌프 해체 출력별 기계설비공·보통인부 수량. | 확실 + +없음 (건설 유지관리 4-3-1) | 4-3-1 | 누락 | 유량계 교체 인력·규격별 시공량. | 확실 + +없음 (건설 유지관리 4-3-2) | 4-3-2 | 누락 | 관갱생 규격별 규사·에폭시도료·배관공·장비시간. | 확실 + +없음 (건설 유지관리 4-3-3) | 4-3-3 | 누락 | 배관누수 검사 배관공·시공량. | 확실 + +GF000137 / QF000307 | 중기운반 기준표 | 누락 | 원문 10-4 자주식 운반 `크레인(트럭 10TON)` 한 줄이 QF000307에 없음(원문 16줄·JSON 15줄); GF000137 직접 영향은 미확인 | 확실 diff --git a/docs/raw/M01_점검/진행.md b/docs/raw/M01_점검/진행.md new file mode 100644 index 00000000..373b2437 --- /dev/null +++ b/docs/raw/M01_점검/진행.md @@ -0,0 +1,20 @@ +# M01 표 행 대조 진행 (2026-09-30) + +읽기 전용 검수. 코드·자료·git은 변경하지 않았고, 이 폴더의 보고 파일만 기록한다. `끝남`은 원문 표의 조건·규격·숫자·단위를 자료 줄과 수동 대응한 것, `사람 확인`은 원문·JSON 물리 줄 수가 같아 기계 비교가 가능했으나 조건·비목 판단이 남은 것, `미판정`은 병합·피벗·여러 표 등으로 아직 일대일 대응표를 만들지 못한 것이다. `사람 확인`과 `미판정`은 합격을 뜻하지 않는다. + +| 부문 | 전체 표 | 끝남 | 사람 확인 | 미판정 | 다음 확인 자리 | +|---|---:|---:|---:|---:|---| +| 산림 | 634 | 11 | 202 | 421 | 10-13-1·2 QF000340~342는 거리·인원 12칸과 시간산출 4항목 완료(로직 누락은 별개); 10-9-1 QF000329 35칸, 10-6-1·2·3 QF000308~310도 완료; QF000307 크레인 1줄 누락. 다음은 산림 10·12·13장 나머지 임도 표. | +| 건설 공통 | 575 | 0 | 59 | 516 | 산림·토목 후 7장 석재판 및 6장 재료. | +| 토목 | 765 | 0 | 36 | 729 | 산림 후 터널·도로·사방에 쓰는 1·2·3장부터. | +| 건축 | 353 | 0 | 7 | 346 | 토목 후. | +| 기계설비 | 509 | 0 | 56 | 453 | 건축 후. | +| 유지관리 | 197 | 0 | 11 | 186 | 마지막. | +| 합계 | 3,033 | 11 | 371 | 2,651 | 원문-JSON 행 일대일 수동 확인 계속. | + +기계 선별 결과는 `보고.md`의 「3차 전 부문 표 대응 기계 선별」에 있음. 출처 공유 1,380표, 복수 원문 표 블록 561표, 피벗 등 물리 줄 수 다른 720표는 단순 비교로 판정 불가(합계 2,661표). 그중 수동 완료 11표를 빼면 2,650표, 여기에 품셈 본문 출처 비교대상 밖인 공통 1표를 더해 위 미판정 2,651표다. 물리 줄 수 같은 371표도 숫자 181표 일치·190표 차이에 그칠 뿐 조건·비목은 사람 확인이 남아 있다. + +153절 원문 누락 후보는 `보고.md` 장별 표와 합계에 모두 분류했고, 흠의 줄글 목록은 `보고_줄글.md`에 있다. 재료 호표 675줄은 2차 때 펼쳐 검토했으나 잘못 연결된 각 줄마다 자재품목표 대체품 존재 유무까지 전부 확인하지는 못했다. 배수 환산도 발견 사례와 기계 선별만 기록했으며 1,354개 전 로직의 완전한 수동 판정은 남았다. 새 흠을 찾으면 두 보고 파일을 먼저 갱신하고 이 진행표를 고친다. +## 다음 볼 자리 (2026-09-30 중단 시점) + +사용자 지시로 새 대조를 멈췄다. 재개 시 산림 10·12·13장 나머지 임도 표부터 확인하고, 건설 공통 → 토목 → 건축 → 기계설비 → 유지관리 순서로 진행한다. 371개 `사람 확인` 표도 조건·수치·단위·비목 확정 전이며 2,651개 `미판정` 표는 병합 칸·피벗 해체가 먼저다. 그 뒤 재료 검색어 675줄의 품목표 실재 여부와 1,354개 로직의 배수 환산을 전수 확인한다. 판정 기준과 세 사례는 `대조방식.md`에 남긴다.