From 559d445e4ee4fd98b2be4c11b624f4810cfdafdb Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 30 Sep 2026 07:36:32 +0900 Subject: [PATCH 01/15] 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/15] =?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/15] =?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/15] =?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/15] =?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/15] =?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/15] =?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/15] =?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/15] =?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": "관공 관매설"}