diff --git a/.env b/.env index 4d9fecafd..2e3d4cb5c 100644 --- a/.env +++ b/.env @@ -89,5 +89,5 @@ AUTH_OTP_DISABLED=True # 대시보드 지식 챗봇(B01_Dashboard_Knowledge_Chat) NVIDIA 키 NVIDIA_API_KEY=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 # 노선분석 산사태위험지도(행안부 생활안전지도 WMS IF_0046 · D03_PlanAnalysis_Landslide) 키 — 비우면 겹 꺼짐 -SAFEMAP_API_KEY= +SAFEMAP_API_KEY=MFO8CY4O-MFO8-MFO8-MFO8-MFO8CY4O6X DATA_GO_KR_API_KEY=0j5WZHeNxSkdPVSLXUsx9kVikXEN%2BDc%2Byv68jz8IpKFffjbZkRiQrJO78y6rxPCTqhiSOW4lDxpid0twkSVQ9g%3D%3D diff --git a/B09_Estimation/B09_Estimation_Bill_Items.py b/B09_Estimation/B09_Estimation_Bill_Items.py index f9b4fa725..3bf6feba6 100644 --- a/B09_Estimation/B09_Estimation_Bill_Items.py +++ b/B09_Estimation/B09_Estimation_Bill_Items.py @@ -16,6 +16,7 @@ from typing import Any from B08_Quantity.B08_Quantity_Haul import bill_key, haul_groups from B08_Quantity.B08_Quantity_Material_Calc import _structure_row +from M02_MasterTemplete.M02_Item_Source import item_name WHY_EARTH = "조합 밖 토공 산근 — 연결 없음" WHY_READY = "준비공 대가 — 연결 없음" @@ -47,6 +48,20 @@ EARTH: list[tuple[str, str, str, tuple[str, str, str | None] | None]] = [ #: 구조물 집계표 열 id 머리 → 공종(그 밖 = 2.2 배수공) · 커플링 = 자재라 뺌 STRUCTURE_SECTION = {"pipe:revet|": "2.1", "pipe:chute|": "2.3", "pipe:wall|": "2.3"} SKIP_COLUMNS = ("pipe:cp|",) +#: 층 이름 — 화면 낱말(번호 · 영문 층 이름은 화면에 안 냄) +LAYER_WORD = {"system": "마스터", "company": "회사", "personal": "내 자료집", "project": "이 공사"} + + +def combo_text(combo: str, names: dict[str, str]) -> str: + """조합 id 「{층}|{주인}|{키}」 → 「내 자료집 · 조합 이름」(이름을 못 읽으면 층 낱말만).""" + layer, _, rest = combo.partition("|") + owner, _, key = rest.partition("|") + if combo not in names: + try: + names[combo] = item_name({"layer": layer, "owner": owner or None, "key": key}) + except (LookupError, KeyError, ValueError, OSError): + names[combo] = "" + return " · ".join(t for t in (LAYER_WORD.get(layer, ""), names[combo]) if t) def _same(row: dict[str, Any], want: tuple[str, str, str | None]) -> bool: @@ -80,13 +95,14 @@ def structure_items(summary: dict[str, Any] | None) -> list[dict[str, Any]]: columns = (summary or {}).get("columns") or [] total = (summary or {}).get("total") or {} out = [] + names: dict[str, str] = {} for column in columns: cid = str(column.get("id") or "") if cid.startswith(SKIP_COLUMNS): continue section = next((s for head, s in STRUCTURE_SECTION.items() if cid.startswith(head)), "2.2") name, spec, unit = _structure_row(column) - combos = [c for c in column.get("combos") or [] if c] + combos = [combo_text(c, names) for c in column.get("combos") or [] if c] out.append( { "key": f"st:{cid}", diff --git a/B09_Estimation/B09_Estimation_Labor_Source.py b/B09_Estimation/B09_Estimation_Labor_Source.py index ba3d269db..7520f426d 100644 --- a/B09_Estimation/B09_Estimation_Labor_Source.py +++ b/B09_Estimation/B09_Estimation_Labor_Source.py @@ -33,6 +33,7 @@ from typing import Any from B09_Estimation import B09_Estimation_Bill_Library as library from B09_Estimation import B09_Estimation_Sangeun as sg from B09_Estimation import B09_Estimation_Sangeun_Text as st +from B09_Estimation.B09_Estimation_Bill_Items import LAYER_WORD from M02_MasterTemplete import M02_Item_Prices as item_prices from M02_MasterTemplete import M02_Labor_Doc as labor_doc from M02_MasterTemplete import M02_Labor_Fixed as fixed @@ -266,7 +267,8 @@ class Builder: { "kind": "사전조합", "name": doc.get("이름") or ref.get("키"), - "spec": str(ref.get("키") or ""), + "spec": LAYER_WORD.get(str(ref.get("층")), ""), # 번호(키)는 풍선 글로만 + "spec_raw": str(ref.get("키") or ""), "unit": doc.get("단위") or "", "cite": "", "origin": {"참조": refs.ref_id(ref)}, diff --git a/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Fold_Menu.ts b/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Fold_Menu.ts index 95b7fef78..c633edc0c 100644 --- a/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Fold_Menu.ts +++ b/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Fold_Menu.ts @@ -38,7 +38,7 @@ async function copyBlock(ctx: TabContext, raw: DataRow[], row: Row, block: Row): note: `「${block.name}」 을 이 공사 안 새 호표로 — 고친 칸까지 복사`, fields: [ { key: "name", label: "이름", value: `${block.name} 복사`, required: true }, - { key: "spec", label: "규격", value: block.spec || "" }, + { key: "spec", label: "규격", value: block.kind === "사전조합" ? "" : block.spec || "" }, ], okLabel: "만들기", closeLabel: "닫기", @@ -106,7 +106,7 @@ async function savePersonal(ctx: TabContext, block: Row): Promise { const tail = left ? ` · 안 담은 고친 칸 ${left}(품 ${skip.품} · 단가 ${skip.단가}) — 품은 고정 줄(PLAN 705) 뒤 담김` : ""; - showToast(`내 자료집에 저장 「${got.이름}」(${got.키})${tail}`, "success", 6000); + showToast(`내 자료집에 저장 「${got.이름}」${tail}`, "success", 6000); return; } if (status !== 409 || !same) { @@ -116,7 +116,7 @@ async function savePersonal(ctx: TabContext, block: Row): Promise { } const over = await askConfirm({ title: "같은 이름", - message: `내 자료집에 「${name.trim()}」(${same}) 이 있음 — 덮어쓰기 · 새 이름`, + message: `내 자료집에 「${name.trim()}」 이 있음 — 덮어쓰기 · 새 이름`, okLabel: "덮어쓰기", cancelLabel: "새 이름", }); diff --git a/M02_MasterTemplete/M02_Logic_Settle.py b/M02_MasterTemplete/M02_Logic_Settle.py new file mode 100644 index 000000000..8900471e7 --- /dev/null +++ b/M02_MasterTemplete/M02_Logic_Settle.py @@ -0,0 +1,115 @@ +"""M02 기준 목록 · 계산서 끝수(PLAN 704-2 딸림) — B09 산근 끝수 규칙으로 셈(③ 서버 단독 · +`B09_Estimation_Sangeun` 과 규칙 짝 · 거울 시험 `test_704_settle`). + +줄 금액 = ROUNDDOWN(단가 × 수량, 1) 비목마다 · 비목 총계 = ROUNDDOWN(Σ 줄, 0) · 계 = 셋 합 · +작업량 Q(중간 이름 Q · Q1 …) = ROUND(식, 2) 뒤 쓰임 · % 덧줄 밑수 = 그때까지 줄 합(줄 끝수 뒤). +근거 = 지식DB `technical_info/01_임도/05_원가정보/단수처리_규칙.md`(행 금액 절사 · 관측 후보 규칙). +M01 엔진(`master_formula.run`)의 구조 그대로 — 끝수 자리만 다름(엔진은 줄 끝수 없이 계에서 한 번). +하위 로직(기계경비 포함)은 같은 규칙으로 다시 셈 · 흐름 · 결과 로직은 엔진 값 그대로. +""" + +from __future__ import annotations + +import re +from decimal import ROUND_DOWN, ROUND_HALF_UP, Decimal +from typing import Any + +from M01_MasterData import M01_MasterData_Store as m01 + +mf = m01.cm.mf +_Q = re.compile(r"Q\d*") + + +def _down(value: Any, places: int) -> Any: + return None if value is None else mf._round(Decimal(value), places, ROUND_DOWN) + + +def _bundle(whole: Any, item: dict, env: dict, depth: int) -> dict: + """로직 호표 줄이 부르는 하위 로직 — 같은 규칙으로 다시 셈(엔진 `logic` 마디 그대로).""" + node = mf.parse(item["요소"]) + if node[0] != "logic": + return mf.evaluate(node, env, whole, depth) + given = {k: mf.evaluate(v, env, whole, depth) for k, v in node[2]} + got = settle(whole, mf.fill(node[1], env), given, depth + 1) + return got["결과"] if "결과" in got else {k: got[k] for k in mf.BUNDLE} + + +def settle(whole: Any, ref: str, given: dict, depth: int = 0) -> dict: + """`master_formula.run` 과 같은 꼴 `{줄, 노무비, 재료비, 경비, 계, 중간}` — 끝수만 B09 규칙.""" + row = whole.logic(ref) + if isinstance(row.get("흐름"), dict) or "결과" in row: + return mf.run(whole, ref, given, depth) + env = mf._inputs(row, given) + for step in row.get("중간", []): + try: + value = mf.evaluate(mf.parse(step["식"]), env, whole, depth) + except mf.EmptyInput as e: + env[step["이름"]] = mf.Blank(str(e)) + continue + if _Q.fullmatch(step["이름"]) and isinstance(value, Decimal): + value = value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) + env[step["이름"]] = value + lines: list[dict] = [] + for n, item in enumerate(row.get("호표", [])): + try: + qty = mf.evaluate(mf.parse(item["수량"]), env, whole, depth) + if item["종류"] == "로직": + bundle = _bundle(whole, item, env, depth) + price = bundle["계"] + split = ( + {k: qty * bundle[k] for k in mf.COST_ITEMS} + if "비목" not in item + else {item["비목"]: qty * price} + ) + else: + ref_ = ( + { + k: mf.fill(v, env) if isinstance(v, str) else v + for k, v in item["요소"].items() + } + if isinstance(item["요소"], dict) + else mf.fill(item["요소"], env) + ) + if isinstance(ref_, str) and qty != 0: + whole.get(ref_) + price = mf.mp.priced(whole, item, ref_, env, n if depth == 0 else None)[0] + if price is None and qty == 0: + price = Decimal(0) + split = {item["비목"]: None if price is None else qty * price} + except mf.EmptyInput as e: + lines.append(mf._blank_line(item, str(e))) + continue + lines.append(_line(item["이름"], split)) + sums = {k: sum((x["비목"].get(k) or 0 for x in lines), Decimal(0)) for k in mf.COST_ITEMS} + env["줄"] = {x["이름"]: x["금액"] for x in lines if x["이름"] and x["금액"] is not None} + for extra in row.get("덧줄", []): + env.update(sums) + try: + value = mf.evaluate(mf.parse(extra["식"]), env, whole, depth) + env[extra["이름"]] = value + except mf.FormulaError: + value = None + sums[extra["비목"]] += _down(value, 1) or 0 + lines.append(_line(extra["이름"], {extra["비목"]: value})) + if value is not None: + env["줄"][extra["이름"]] = lines[-1]["금액"] + sums = {k: mf.cut(row, _down(v, 0), "계") for k, v in sums.items()} + return {"줄": lines, **sums, "계": sum(sums.values()), "중간": mf._shown(env)} + + +def _line(name: str, split: dict) -> dict: + cut = {k: _down(v, 1) for k, v in split.items()} + whole_sum = None if all(v is None for v in cut.values()) else sum(v or 0 for v in cut.values()) + return {"이름": name, "비목": cut, "금액": whole_sum} + + +def apply(groups: list[dict], engine: dict, done: dict) -> None: + """계산서 묶음의 줄 금액 · 소계를 `settle` 값으로 바꿈 — 줄 차례는 엔진과 같음.""" + seq: dict[str, list] = {k: [] for k in mf.COST_ITEMS} + for eng, mine in zip(engine["줄"], done["줄"]): + for part, _ in m01.mtx._shares(eng): + seq[part].append(mine["비목"].get(part)) + for group in groups: + for line, amount in zip(group["줄"], seq.get(group["비목"], [])): + line["금액"] = amount + group["소계"] = done[group["비목"]] diff --git a/M02_MasterTemplete/M02_Logic_Standard.py b/M02_MasterTemplete/M02_Logic_Standard.py index 71eaac2cb..bcfd69de3 100644 --- a/M02_MasterTemplete/M02_Logic_Standard.py +++ b/M02_MasterTemplete/M02_Logic_Standard.py @@ -5,12 +5,13 @@ 위 끝) · 그 밖 연속 값 입력은 견본 자동값(`master_auto.values`) 한 줄 · 지역 = 자동값(전국평균). 펼침 = 바탕(자동값 · 규격 축은 첫 대표 값) 한 줄 + 규격 축마다 값 하나씩 바꾼 줄(곱하지 않음 · 바꾼 줄이 0 원이면 표에 없는 조합으로 보고 뺌). -금액 = M01 엔진(`master_formula.run`) 「계」 — M01 시험 계산과 같은 셈 · 최신 판 노임(마스터 판). +금액 = M01 엔진 식 + B09 산근 끝수(줄마다 버림 · `M02_Logic_Settle` · 근거 지식DB +`technical_info/01_임도/05_원가정보/단수처리_규칙.md`) · 최신 판 노임(마스터 판). 목록은 서버가 뒤에서 한 번 셈(프로세스 `WORKERS` 개 · 수 분) → 프로세스 메모리 + 파생 저장본 (storage `_derived/m02_logic_standard/`) · 열쇠 = 마스터 내용 지문 + `SHAPE` · 같으면 안 셈 · 셈 도중 목록 = 빈 줄 `준비: false`(화면은 로직 줄 그대로 · 금액 빈칸). 실무 낱말(`WORDS`) = 별칭 칸에 덧붙여 찾기(공용 `matchLogic`)가 「석축」 → 돌쌓기를 찾음. -계산서 = `master_text.lines`(줄 「석공 | 268,908 * 0.1 = 26,891」 · 비목 소계 · 계 = 엔진 값). +계산서 = `master_text.lines`(줄 「석공 | 268,908 * 0.1 = 26,891」 · 비목 소계 · 계) + 같은 끝수 값. M01 은 읽기만 · 권한은 부르는 쪽 몫(로그인). """ @@ -27,9 +28,10 @@ from typing import Any from common_util.common_util_derived_store import derived_key, read_derived, write_derived from M01_MasterData import M01_MasterData_Store as m01 from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs +from M02_MasterTemplete import M02_Logic_Settle as settle from M02_MasterTemplete import M02_Template_Layers as layers -SHAPE = 2 # 줄 꼴이 바뀌면 올림 — 옛 저장본을 안 씀 +SHAPE = 3 # 줄 꼴이 바뀌면 올림 — 옛 저장본을 안 씀 KIND = "m02_logic_standard" REGION = "지역" # 지역 = 자동값 한 벌(정함 8) QUIET = ("해당 없음", "없음") # 규격 글에서 뺄 값 @@ -168,7 +170,7 @@ def _money(whole: Any, logic: dict, given: dict) -> Decimal | None: if not str(logic.get("결과단위") or "").startswith("원"): return None try: - got = m01.cm.mf.run(whole, str(logic["키"]), m01.cm.mf.fill_empty(logic, given)) + got = settle.settle(whole, str(logic["키"]), m01.cm.mf.fill_empty(logic, given)) except (m01.cm.mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError): return None return got.get("계", got.get("결과")) @@ -325,7 +327,12 @@ def sheet(key: str, change: dict) -> dict: except m01.cm.mf.FormulaError as e: raise m01.StoreError(404, str(e)) from e given, ax = _given(whole, logic, change) - got = m01.mtx.lines(files, key, m01.cm.mf.fill_empty(logic, given)) + filled = m01.cm.mf.fill_empty(logic, given) + got = m01.mtx.lines(files, key, filled) + done = settle.settle(whole, key, filled) + if "결과" not in done: # 돈 로직 — 줄마다 버림(B09 산근 규칙) + settle.apply(got.get("groups") or [], m01.cm.mf.run(whole, key, filled), done) + got["계"] = done["계"] groups = [ { "비목": g["비목"], diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index 280c3b2d6..c7286f964 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -455,7 +455,7 @@ export function mountLaborDetail( if (!ref) return; void openRefEdit({ ref, - title: `${ref.키} ${lineOf(lineId({ 참조: ref }) ?? "")?.이름 ?? ""}`.trim(), + title: lineOf(lineId({ 참조: ref }) ?? "")?.이름 ?? "", readOnly: !edit || ref.층 !== opt.layer, closed: () => alive && recalc(), }); @@ -466,7 +466,7 @@ export function mountLaborDetail( const row = cur.담은로직[i]; const line = lineOf(lineId(row) ?? ""); openQtyFor( - `${row.참조?.키 ?? row.로직} ${(row.참조 ? line?.이름 : logics.get(row.로직)?.이름) ?? ""}`.trim(), + `${row.참조 ? "" : row.로직} ${(row.참조 ? line?.이름 : logics.get(row.로직)?.이름) ?? ""}`.trim(), row, line, line?.분모 ?? "", @@ -491,7 +491,7 @@ export function mountLaborDetail( pick: (ref: LaborRef, name: string, 별칭: string | null = null) => { const id = lineId({ 참조: ref }); if (cur.담은로직.some((r) => lineId(r) === id)) - return showToast(tc("Labor_AlreadyRef").replace("{value}", `${ref.키} ${name}`), "error"); + return showToast(tc("Labor_AlreadyRef").replace("{value}", name), "error"); const 수량 = lib ? { 손: 1 } : null; // 사전조합 줄 기본 1(51-6) cur.담은로직.push({ 로직: "", 참조: ref, 별칭, 메모: null, 입력값: {}, 수량 }); changed(); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts index b4f6bd348..8c05e65c5 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts @@ -34,6 +34,13 @@ const money = (m: LaborCost | null | undefined, times = 1): HopyoMoney => } : {}; +/** 층 낱말 — 번호(키)는 화면 글에 안 냄 */ +const layerWord = (layer: string): string => + layer === "personal" ? tc("Grid_Mine") : tc(layer === "company" ? "Lib_Company" : "Lib_System"); + +/** 일위대가번 — 번호꼴(SU-000011 · SU01010101)은 사람 글이 아니라 뺌 */ +const humanNo = (no?: string): string => (no && !/^S[A-Z]\d?-?\d{6,8}$/.test(no) ? no : ""); + /** 문서 + calc 답 → 호표 하나 — calc 없으면(계산 중) 금액 빈 칸 · 빈 줄 0 */ export function laborBlock( doc: LaborDoc, @@ -68,7 +75,9 @@ export function laborBlock( unit: line?.분모 ?? (row.참조 ? "" : (logic?.결과단위 ?? "")), prices: money(line?.기준, line?.배 || 1), amounts: money(line?.반영), - note: line?.까닭 ?? (calc && qty === null ? tc("Grid_NoQty") : (row.참조?.키 ?? row.로직)), + note: + line?.까닭 ?? + (calc && qty === null ? tc("Grid_NoQty") : row.참조 ? layerWord(row.참조.층) : row.로직), tag: HOPYO_TAG.pum, }; }); @@ -96,7 +105,7 @@ export function laborBlock( const lines = [...logicLines, ...matLines]; const block: HopyoBlock = { key: doc.열, - name: doc.일위대가번 || doc.열, + name: humanNo(doc.일위대가번) || (doc as { 이름?: string }).이름 || tc("Grid_Head"), unit: doc.단위 ?? "", total: money(calc?.반영합), lines, diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_RefEdit.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_RefEdit.ts index bc08770ce..75916b241 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_RefEdit.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_RefEdit.ts @@ -15,7 +15,7 @@ import { tc } from "./M02_MasterTemplete_Labor_Text"; export interface RefEditOptions { ref: LaborRef; - /** 「SC-000001 돌붙임」 */ + /** 「돌붙임」 */ title: string; readOnly: boolean; closed: () => void; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts index 0d8541919..d64dbdfc9 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts @@ -179,6 +179,8 @@ const TEXT = { Grid_Apart: ["별도계상 · 합계 밖", "Separate · not in total"], Grid_Linked: ["이 공사 단가에 이음", "Linked to project price"], Grid_Hand: ["손 단가(붙인 값)", "Hand price (pasted)"], + Grid_Head: ["일위대가", "Unit price"], + Grid_Mine: ["내 자료집", "My library"], } as const satisfies Record; export type ComboTextKey = keyof typeof TEXT; diff --git a/M02_MasterTemplete/M02_MasterTemplete_ShareRequest.ts b/M02_MasterTemplete/M02_MasterTemplete_ShareRequest.ts index 086a735b3..133e4b0b7 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_ShareRequest.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_ShareRequest.ts @@ -3,7 +3,7 @@ * 회사에 올리기 요청 · 승인(PLAN 706-1) — 서버 `M02_Share_Request.py` 짝(장부 = 회사 층 파일 하나). * 일반 사용자 = 자기 개인 층 구조물 조합 · 사전조합 나무 오른쪽 클릭 「회사에 올리기 요청」 → 글(선택) · * 같은 것 요청이 남아 있으면 「요청 중」(흐림) · 결과는 다음에 M02 열 때 위 띠(닫으면 봄) - * 회사 관리자 = 회사 층 위 띠 「올리기 요청 n」 → 목록(요청자 · 항목 · 날짜 · 글) · [미리 보기] · + * 회사 관리자 = M02 위 띠 「올리기 요청 n」(어느 층이든) → 목록(요청자 · 항목 · 날짜 · 글) · [미리 보기] · * [승인](폴더 고름 = 「회사로 복사…」와 같은 길 · 원본 그대로) · [거절](사유 한 줄) * 관리자(회사 층을 고칠 수 있는 사람)는 「회사로 복사…」가 있어 요청 메뉴를 안 둠. * ========================================================================== */ @@ -50,7 +50,7 @@ export interface ShareOptions { user: SessionUser | null; /** 볼 회사(시스템 관리자는 고른 회사) */ company: string | null; - /** 지금 페이지 층 — 회사 층이면 관리자 띠 */ + /** 지금 페이지 층 — 개인 층이면 요청 메뉴 */ layer: Layer; owner: string | null; showView: (label: string, node: HTMLElement) => void; @@ -77,10 +77,7 @@ export function createShare(opt: ShareOptions): ShareHandle { // 요청할 사람 = 회사가 있고 회사 층을 못 고침(고칠 수 있으면 「회사로 복사…」) const asker = !!ownOwner("personal", opt.user) && !accessFor("company", companyOwner, opt.user).canWrite; - const boss = - opt.layer === "company" && - !!companyOwner && - accessFor("company", companyOwner, opt.user).canWrite; + const boss = !!companyOwner && accessFor("company", companyOwner, opt.user).canWrite; const q = opt.company ? `?company=${encodeURIComponent(opt.company)}` : ""; let rows: ShareRow[] = []; let listHost: HTMLElement | null = null; diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts index 69d869319..17cb6cbc6 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts @@ -157,11 +157,15 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise void go("personal", next, open), }) : panel; - // 704-4 [구조물 조합으로] 만든 항목 → 나무 · 옆 목록 다시(페이지가 내려가면 듣기 끝) + // 704-4 [구조물 조합으로] 만든 항목 → 나무 · 옆 목록 · 열린 표(행렬)를 다시(페이지가 내려가면 듣기 끝) + const afterItem = async (): Promise => { + await Promise.all([side.refresh(), tree.refresh()]); + const now = main.current(); + const shown = now?.kind === "table" && now.name === STRUCTURE_TABLE; + if (shown && (await main.settle()) !== false) await main.openTable(layer, null, true); + }; const onItem = (): void => - tree.root.isConnected - ? void Promise.all([side.refresh(), tree.refresh()]) - : document.removeEventListener(ITEM_BUILT, onItem); + tree.root.isConnected ? void afterItem() : document.removeEventListener(ITEM_BUILT, onItem); document.addEventListener(ITEM_BUILT, onItem); const main = createMain( isAdmin, diff --git a/docs/raw/요구_1025/_조사_517_전국_산림주제도_다른곳.md b/docs/raw/요구_1025/_조사_517_전국_산림주제도_다른곳.md new file mode 100644 index 000000000..8366c2c01 --- /dev/null +++ b/docs/raw/요구_1025/_조사_517_전국_산림주제도_다른곳.md @@ -0,0 +1,131 @@ +# 517 전국 산림 주제도 — 상용 가능한 다른 창구 조사 (2026-10-31 · 웹 공개 페이지만) + +비교 기준 = `docs/raw/요구_1025/_조사_513_산림청_주제도_API.md`(다시 조사 안 함). + +## 결론 + +- **「전국 + 상용 허용 문구 확실」 창구 = 세 주제도 모두 없음.** +- 가장 가까운 곳 = **산림빅데이터거래소(한국임업진흥원)** — 임상도 4차 1:25,000 전국 · 임상도 1:5,000(시도 · 시군구 · 도엽) · 임도망도 전국 상품 페이지에 「라이선스: Creative Commons Attribution」 · 이용료 0원(검색 색인 기준). 그러나 같은 페이지 「Copyright 2019. SEESUNIT CO., LTD. all rights reserved」 · 약관 「자체 사용 목적 외에 외부로 유출 … 구매회원 책임」 · 사이트가 이 조사 환경에서 접속 거부 → **상용 판단 확인 못 함(서면 확인 필요)**. +- 산사태위험지도 전국 파일 — FGIS 일반인 신청(2025년판 · 서약 적용) · 브이월드 = 화면 타일만 · 원본은 「공문 요청 후 방문 수령」 · 옛 국가공간정보포털 오픈마켓 SHP(2019)는 사이트 폐쇄. +- **새 상충** — 산림청 공공데이터 개방 페이지: 임상도 1:25,000 「공공기관 … 비영리공공사업이나 학술·연구목적으로만 제공」(data.go.kr 「제한 없음」과 반대). +- **조건 정정** — 공공누리 상업 허용 = **1 · 3 유형**(2유형 = 「비상업적 이용만 가능」 · 3유형 = 상업 가능 · 변경 금지). +- 견본뿐일 때 공식 길 = 목록별 오류신고 → 제공신청(10일 + 10일) → 거부 · 중단 시 60일 안 분쟁조정(위원회 30일 안 조정안). + +## 1. 임상도 (1:25,000 이상 · 전국) + +| 창구 | 범위 · 축척 · 형식 · 갱신 | 이용 조건 문구 | 로그인 · 신청 | 상용 판단 | 주소 | +|---|---|---|---|---|---| +| data.go.kr 3045619 (기준) | 전국 표기 · 공개 파일 = 제주 견본 · 1:25,000 · SHP · 2025-11-20 | 「이용허락범위 제한 없음」 | 받기 = FGIS 신청 | 문구 OK · 전국 파일 없음 | [D2](https://www.data.go.kr/data/3045619/fileData.do) | +| 산림청 공공데이터 개방 PBD0000059 | 5차 전국 · 견본 = 제주특별자치도 제주시 SHP | 「공공기관(중앙행정기관 및 소속기관, 지방자치단체, 정부투자기관, 학교 및 연구기관)의 비영리공공사업이나 학술·연구목적으로만 제공하고 있음」 · 공공누리 유형 표기 없음 | FGIS 무료신청(일반인 · 공공/연구) | **불가 문구** | [F7](https://www.forest.go.kr/kfsweb/kfi/kfs/trail/fTMap.do?pblicDataId=PBD0000059&tabs=1&mn=NKFS_06_08_02&subTitle=%EC%9E%84%EC%83%81%EB%8F%84) | +| data.go.kr 15093362 (1:5,000) | 전국 · SHP · 2026-10-06 | 「이용허락범위 제한 없음」 · 받는 길 = 「산림공간정보 다운로드 무료신청 > 일반인 신청 > 대축척 임상도 선택 … 신청 목적 입력 > shp 파일 다운로드」 | FGIS(서약) | FGIS 서약 적용 → 확인 못 함 | [D1](https://www.data.go.kr/data/15093362/fileData.do) | +| FGIS 자료유통현황 | 임상도 1:25,000 · 1:5,000 = 「일반 제공용」 · SHP · EPSG:5179 | 서약(업무 한정 · 유출 금지 — 513 조사) | 로그인 · 신청 | 상용 재배포와 충돌 | [F1](https://www.forest.go.kr/newkfsweb/kfi/kfs/fgis/selectAvailMapList.do?mn=KFS_02_04_02_02&orgId=fgis) | +| 브이월드 30136 (기준) · data.go.kr 연계 258662 | 1:5,000 전국 GDB | 브이월드 CC BY-NC-ND · 연계 페이지 이용허락범위 빈칸 | — | 불가 | [V1](https://www.vworld.kr/dtmk/dtmk_ntads_s002.do?svcCde=MK&dsId=30136) [D9](https://www.data.go.kr/data/258662/linkedData.do) | +| **산림빅데이터거래소 FRT001201** 임상도 4차(1:25000)_전국 | 전국 750도엽 · 2018 기준 · 4차(1996~2005) 수치화 · SHP zip 약 22 MB · EPSG:5179 · 「갱신없음」 | 「라이선스: Creative Commons Attribution」 · 「Copyright 2019. SEESUNIT CO., LTD. all rights reserved」 · 이용료 ₩0 · 판매처 ㈜시선아이티 | 회원가입 | CC BY 면 가능 · 저작권 표기 · 약관과 상충 → 확인 못 함 | [B4](https://www.bigdata-forest.kr/product/FRT001201) [D10](https://www.data.go.kr/data/95584/linkedData.do) | +| 산림빅데이터거래소 FRT001002~004 임상도(1:5000) 시도 · 시군구 · 도엽 | 1:5,000 | 「Creative Commons Attribution」 · ₩0 | 회원가입 | 같은 자료가 브이월드 CC BY-NC-ND → 상충 | [B5](https://www.bigdata-forest.kr/product/FRT001002) [B6](https://www.bigdata-forest.kr/product/FRT001004) | +| 산림빅데이터거래소 FRT001101 수종별 임상도 전국 | 1:5,000 기반 | 이용료 400,000원 | 회원가입 · 결제 | 확인 못 함 | [B7](https://www.bigdata-forest.kr/product/FRT001101) | +| 지자체 (data.go.kr) | 「임상도」 파일 검색 = 산림청 2건뿐 · 지자체 등록 0 | — | — | 해당 없음 | [검색](https://www.data.go.kr/tcs/dss/selectDataSetList.do?keyword=%EC%9E%84%EC%83%81%EB%8F%84&dType=FILE) | +| K-Geo(kgeop.go.kr) · egis · AI Hub | K-Geo = 토지 · 건물 정보 · 산림 주제도 언급 없음 · egis · AI Hub = 임상도 제공 문구 못 찾음 | — | — | 확인 못 함 | [K1](https://www.kgeop.go.kr/) | + +## 2. 임도망도 (전국 노선) + +| 창구 | 범위 · 축척 · 형식 · 갱신 | 이용 조건 문구 | 로그인 · 신청 | 상용 판단 | 주소 | +|---|---|---|---|---|---| +| data.go.kr 3045621 (기준) | 「전국 산림 내에 조성된 임도」 · 견본만 · SHP · 2025-11-20 · 산림디지털담당관실 042-481-4285 | 「이용허락범위 제한 없음」 | 기관자체 다운로드 | 문구 OK · 전국 파일 없음 | [D3](https://www.data.go.kr/data/3045621/fileData.do) | +| FGIS 자료유통현황 | 임도망도 1:5,000 = 「공공연구기관 제공용」 | — | 일반 신청 불가 | 불가 | [F1](https://www.forest.go.kr/newkfsweb/kfi/kfs/fgis/selectAvailMapList.do?mn=KFS_02_04_02_02&orgId=fgis) | +| 산림청 임도망도 현황 페이지 | 1:25,000 · 607도엽 · 1999 1차 구축 · 국유임도 중심 · 문의 목재산업과 042-481-4276 | 받는 길 · 조건 표기 없음 | — | 확인 못 함 | [F8](https://www.forest.go.kr/newkfsweb/html/HtmlPage.do?pg=%2Ffgis%2FUI_KFS_5002_020700.html&mn=KFS_02_04_03_04_04&orgId=fgis) | +| **산림빅데이터거래소 FRT002601** 임도망도(산길)_전국 | 전국 · 2019 기준 · 노선 · 시공년도 · 거리 · csv/zip 약 25.71 MB · SHP · EPSG:5179 | CC BY 표기(색인) · 이용료 ₩0 · 판매처 ㈜시선아이티 · data.go.kr 연계 95799 이용허락범위 빈칸 | 회원가입 | 확인 못 함(약관 상충) | [B2](https://www.bigdata-forest.kr/product/FRT002601) [D11](https://www.data.go.kr/data/95799/linkedData.do) | +| 브이월드 | 산림청 12건(임상도 · 토양도 · 경제림 · 맞춤형조림 · 백두대간 · 산림기능 · 등산로) — 임도 없음 · 「임도」 검색 0건 | — | — | 해당 없음 | [V4](https://www.vworld.kr/dtmk/dtmk_ntads_s001.do?svcCde=MK) | +| data.go.kr 임도 표(산림청 · 지자체 CSV) | 예: 거창군 15006041 = 개설년도 · 노선명 · 지번 · 연장 · 산림청 임도시설 현황 15094372 · 산림토목관리 노선정보 15120710 = 노선거리만 · **좌표 없음** | 「이용허락범위 제한 없음」 | 없음 | 가능하나 노선 도형 아님 | [D12](https://www.data.go.kr/data/15006041/fileData.do) [D13](https://www.data.go.kr/data/15120710/fileData.do) | +| data.go.kr 15071663 산림토목사업 대상지정보 | 「대상지 좌표」(점) 포함 표기 · 2026-09-03 | 「이용허락범위 제한 없음」 | 없음 | 가능 · 노선 아님 | [D14](https://www.data.go.kr/data/15071663/fileData.do) | +| 국토지리정보원 연속수치지형도 15059721 | 전국 임의 영역 · 1:5,000 · 1:25,000 · SHP/NGI · 국토정보플랫폼 | 「공공저작물 : 출처표시 (제 1유형)」 | 국토정보플랫폼 | 상용 가능 · **임도 표현 여부 확인 못 함** | [D15](https://www.data.go.kr/data/15059721/fileData.do) | + +## 3. 산사태위험지도 (전국) + +| 창구 | 범위 · 축척 · 형식 · 갱신 | 이용 조건 문구 | 로그인 · 신청 | 상용 판단 | 주소 | +|---|---|---|---|---|---| +| data.go.kr 15074817 (기준) | 전국 표기 · 10 m 격자 · XML · 견본 = 제주시 ZIP · 2026-09-03 · 산사태방지과 042-481-8846 | 「이용허락범위 제한 없음」 | — | 문구 OK · 전국 파일 없음 | [D7](https://www.data.go.kr/data/15074817/fileData.do) [F9](https://www.forest.go.kr/kfsweb/kfi/kfs/trail/sanSaTae.do?mn=NKFS_01_01&subTitle=%EC%82%B0%EC%82%AC%ED%83%9C%EC%9C%84%ED%97%98%EC%A7%80%EB%8F%84) | +| FGIS 일반인 신청 (산림청 답변 2026-01-14) | 구축 2013 · 2019 · 2022~2025 · 「현재 … 2025년 산사태위험지도만 제공이 가능」 · 래스터 제작(2023 답변) | 「map.forest.go.kr -> 산림공간정보 다운로드 -> 무료신청 -> 일반인 신청」 · 자료유통현황 표는 「공공연구기관 제공용」(상충) · 서약 적용 | 로그인 · 신청 | 서약 → 상용 불가 쪽 | [F10](https://www.forest.go.kr/newkfsweb/cop/bbs/selectBoardArticle.do?nttId=3215640&bbsId=BBSMSTR_1351&mn=KFS_02_04_07_02&orgId=fgis) [F11](https://www.forest.go.kr/newkfsweb/cop/bbs/selectBoardArticle.do?nttId=3182126&bbsId=BBSMSTR_1351&mn=KFS_02_04_07_02&orgId=fgis) | +| 정보공개청구(open.go.kr) | 과거판(2013~2024) — 「과거 산사태위험지도는 "정보공개청구"를 통하여 신청 가능」 | 받은 자료의 이용 조건 = 확인 못 함 | 청구 | 확인 못 함 | [F10](https://www.forest.go.kr/newkfsweb/cop/bbs/selectBoardArticle.do?nttId=3215640&bbsId=BBSMSTR_1351&mn=KFS_02_04_07_02&orgId=fgis) | +| 브이월드 DAT_0000000000000096 | 전국 · 2D · 3D **PNG 타일**(지도 보기) · 2024 구축 · 2025-02-05 갱신 · 원본 3.52 GB | 라이선스 표기 없음 · 「대외제공 가능여부: Y」 · 원본 「공문 요청 후 방문 수령」 | 공문 | 확인 못 함 | [V5](https://www.vworld.kr/dtmk/dtmk_ntads_s002.do?svcCde=DT&dsId=DAT_0000000000000096) | +| 옛 국가공간정보포털 오픈마켓 20210708ds00009 | 전국 · SHP(TB_FGDI_FS_FD100.zip) · 2019-05-01 | 「지정된 라이선스 없음」(검색 요약) | — | **사이트 폐쇄**(data.nsdi.go.kr DNS 없음 · 오픈마켓은 2023-12-31 브이월드 통합 — 나무위키) · 브이월드 산림청 목록에 없음 | [N1](http://data.nsdi.go.kr/dataset/20210708ds00009) | +| 생활안전지도 WMS (기준) | 전국 WMS | 공공누리 4유형 | 인증키 | 불가 | [S1](https://www.safemap.go.kr/opna/data/dataViewRenew.do?objtId=125) | +| 산림빅데이터거래소 FRT000301 산사태발생위험등급 | 전국 · 시도 · 시군구 · 1~5 등급 · API | data.go.kr 연계 81177 이용허락범위 빈칸 · 무료 | 회원가입 | 확인 못 함 | [D16](https://www.data.go.kr/data/81177/linkedData.do) | +| 재난안전데이터 공유플랫폼 (data.go.kr 15139681 지역산사태위험) | 지구명 · 법정동코드 · 위험등급 · 고시일자 · 예상면적 · JSON · **도형 없음**(지정 위험지구 목록) | 「이용허락범위 제한 없음」 | 인증키 | 가능 · 위험지도 아님 | [D17](https://www.data.go.kr/data/15139681/openapi.do) | +| 한국지질자원연구원 산사태 취약성도 (85304~85306) | 일부 지역(키워드 강릉 · 보은 · 봉화 · 서울 · 인제 · 평창 · 이천광주) · asc · grid · tiff | 이용허락범위 빈칸 | — | 전국 아님 | [D18](https://www.data.go.kr/data/85304/linkedData.do) | +| 지자체 | 산사태 취약지역 현황 CSV(충남 · 경기 · 인천 연수구 · 창녕) — 지정지역 목록 | 「제한 없음」 등 | — | 위험지도 아님 · 일부 | [검색](https://www.data.go.kr/tcs/dss/selectDataSetList.do?keyword=%EC%82%B0%EC%82%AC%ED%83%9C%EC%9C%84%ED%97%98%EC%A7%80%EB%8F%84) | + +## 4. 산림빅데이터거래소 — 이용 조건 쪽 근거 + +| 항목 | 문구 · 사실 | 출처 | +|---|---|---| +| 운영 | 한국임업진흥원 AI빅데이터실 · 02-6393-2714 · 「유로 또는 무료로 제공되는 데이터」 · 구매회원 = 「웹·모바일 서비스 개발이나 사업 전략」 활용 개인 · 기업 | [K2](https://www.kofpi.or.kr/service/bigdata_01.do) | +| 약관 | 「구매회원은 구매 상품에 대하여 사무국에 허락을 득하지 않고 자체 사용 목적 외에 외부로 유출하여 발생되는 손해에 대하여 모든 책임을 부담하여야 합니다.」(검색 요약 · 전문 미열람) | [B8](https://www.bigdata-forest.kr/tou) | +| 상품 표기 | 임상도 4차 전국 · 임상도 1:5,000 · 임도망도 전국 = 「Creative Commons Attribution」 · ₩0 · 판매처 ㈜시선아이티 · 「Copyright 2019. SEESUNIT CO., LTD. all rights reserved」(검색 색인) | B4 · B2 · B5 | +| 접속 | 이 조사 환경에서 `ECONNREFUSED`(https · http 모두) · 검색 색인에 2026-01-05 공지 있음 | [B9](https://www.bigdata-forest.kr/support/notice/20260105093536) | +| 참고(공식 아님) | 나무신문 칼럼(2021-05-04) 「거의 무료로 다운로드 받을 수 있어, 학술연구 및 사업적인 목적으로 다양하게 이용할 수 있는 장점」 | [W1](https://www.imwood.co.kr/news/articleView.html?idxno=25690) | + +## 5. 「제한 없음」인데 견본뿐일 때 — 공식 길 + +| 길 | 쓰는 때 | 절차 · 기한 | 주소 · 연락 | +|---|---|---|---| +| ① 목록별 오류신고 · 데이터 개선요청 | 등록 목록의 파일 누락 | 포털 약관 「제공기관은 … 회원이 신고한 공공데이터 등록 누락, 오류신고, 이용불편사항 등에 대해서 조치를 취해야 합니다」 · 포털 공통 기한 = 확인 못 함(기관 안내 예: 경찰청 · 해양환경공단 10일 · 법무부 · 중기부 14일) | 각 목록 페이지 · [P1](https://www.data.go.kr/ugs/selectPortalPolicyView.do) · 활용지원센터 1566-0025 | +| ①′ 산림청 데이터 품질오류 신고 게시판 | 같음 | 신고접수 → 담당자 지정 → 담당자 처리 → 결과 안내 · 기한 표기 없음 · 1쪽에 전국 파일 요청 글 없음 | [F12](https://www.forest.go.kr/kfsweb/cop/bbs/selectBoardList.do?bbsId=BBSMSTR_1662&mn=NKFS_06_08_04) | +| ② 공공데이터 제공신청(법 제27조) | 목록에 없는 데이터 · 전국 원본 요청 | 「제공여부 심의 > 공공데이터제공(10일)」 · 10일 안 결정 · 부득이하면 10일 연장 · 불가 시 사유를 신청인 · 행안부에 통보 | [P3](https://www.data.go.kr/tcs/dor/insertDataOfferReqstProcssView.do) [M2](https://www.mois.go.kr/frt/sub/a06/b02/openData_4/screen.do) | +| ③ 공공데이터 제공 분쟁조정(법 제29 · 31조) | 제공 거부 · 제공 중단 처분 | 처분일부터 60일 안 신청 → 사실조사 → 조정 전 합의 권고 → 위원회 30일 안 조정안(의결로 연장) → 「조정결정일부터 15일 이내 … 수락」 = 재판상 화해 · 「공공기관은 특별한 사정이 없으면 조정안을 수락」(행안부) · 사용 중 문제는 다루지 않음 | [P4](https://www.data.go.kr/tcs/dor/insertTrublMdatReqstProcssView.do) [P5](https://www.data.go.kr/odmc/intro/index.do) · odmc@nia.or.kr · 02-6191-2064 | +| ④ 정보공개청구(정보공개법 제11조) | 과거판 산사태위험지도 등 | 10일 안 결정 · 10일 연장 · 이의신청 30일 | [open.go.kr](https://www.open.go.kr) | + +- 법 근거 참고 — 공공데이터법 제3조 ④ 영리 이용 제한 금지(513 조사 L1) · 저작권법 제24조의2 ① 「국가 또는 지방자치단체가 업무상 작성하여 공표한 저작물 … 허락 없이 이용할 수 있다」(제외 4가지 · 영리 여부 문구는 문체부 페이지에 없음)([C1](https://www.mcst.go.kr/site/s_policy/copyright/question/question07.jsp)) · 공공누리 유형([C2](https://www.kogl.or.kr/info/license.do)). + +## 6. 결론 표 + +| 주제도 | 창구 | 범위 | 조건 | 상용 | 주소 | +|---|---|---|---|---|---| +| 임상도 | data.go.kr 3045619 | 견본(제주) | 제한 없음 | 문구 OK · 파일 없음 | [D2](https://www.data.go.kr/data/3045619/fileData.do) | +| 임상도 | 산림청 개방 페이지 | 전국 | 공공기관 비영리 · 학술 전용 | 불가 | [F7](https://www.forest.go.kr/kfsweb/kfi/kfs/trail/fTMap.do?pblicDataId=PBD0000059&tabs=1&mn=NKFS_06_08_02&subTitle=%EC%9E%84%EC%83%81%EB%8F%84) | +| 임상도 | FGIS 신청 | 전국 1:25,000 · 1:5,000 | 서약 | 불가 쪽 | [F1](https://www.forest.go.kr/newkfsweb/kfi/kfs/fgis/selectAvailMapList.do?mn=KFS_02_04_02_02&orgId=fgis) | +| 임상도 | 브이월드 30136 | 전국 1:5,000 | CC BY-NC-ND | 불가 | [V1](https://www.vworld.kr/dtmk/dtmk_ntads_s002.do?svcCde=MK&dsId=30136) | +| 임상도 | 산림빅데이터거래소 4차 1:25,000 · 1:5,000 | 전국 | CC BY 표기 · 약관 외부 유출 제한 | 확인 못 함 | [B4](https://www.bigdata-forest.kr/product/FRT001201) | +| 임도망도 | data.go.kr 3045621 | 견본 | 제한 없음 | 문구 OK · 파일 없음 | [D3](https://www.data.go.kr/data/3045621/fileData.do) | +| 임도망도 | FGIS | 1:5,000 | 공공 · 연구 전용 | 불가 | [F1](https://www.forest.go.kr/newkfsweb/kfi/kfs/fgis/selectAvailMapList.do?mn=KFS_02_04_02_02&orgId=fgis) | +| 임도망도 | 산림빅데이터거래소 FRT002601 | 전국(2019) | CC BY 표기 · 약관 제한 | 확인 못 함 | [B2](https://www.bigdata-forest.kr/product/FRT002601) | +| 임도망도 | 연속수치지형도 | 전국 | 공공누리 1유형 | 가능 · 임도 포함 확인 못 함 | [D15](https://www.data.go.kr/data/15059721/fileData.do) | +| 산사태 | data.go.kr 15074817 | 견본(제주) | 제한 없음 | 문구 OK · 파일 없음 | [D7](https://www.data.go.kr/data/15074817/fileData.do) | +| 산사태 | FGIS 일반인(2025년판) | 전국 | 서약 | 불가 쪽 | [F10](https://www.forest.go.kr/newkfsweb/cop/bbs/selectBoardArticle.do?nttId=3215640&bbsId=BBSMSTR_1351&mn=KFS_02_04_07_02&orgId=fgis) | +| 산사태 | 브이월드 지도 | 전국 타일 · 원본 방문 수령 | 표기 없음 | 확인 못 함 | [V5](https://www.vworld.kr/dtmk/dtmk_ntads_s002.do?svcCde=DT&dsId=DAT_0000000000000096) | +| 산사태 | 생활안전지도 WMS | 전국 | 공공누리 4 | 불가 | [S1](https://www.safemap.go.kr/opna/data/dataViewRenew.do?objtId=125) | +| 산사태 | 거래소 산사태발생위험등급 API | 전국 | 빈칸 | 확인 못 함 | [D16](https://www.data.go.kr/data/81177/linkedData.do) | + +권함 순서: + +1. **data.go.kr 목록별 오류신고(3045619 · 3045621 · 15074817)** — 「제한 없음 표기 · 전국 파일 없음 → 전국 파일 등록 또는 표기 정정」 요청 + 산림청 산림디지털담당관실(042-481-4285 · 4169)에 상용 탑재 서면 질의. 「제한 없음」 = 상용 근거 문구가 가장 또렷한 자리. +2. 답 없음 · 거부 → **제공신청**(전국 원본 · 10일 + 10일) → 거부 처분 → **분쟁조정**(60일 안). +3. 병행 — **산림빅데이터거래소 사무국**(한국임업진흥원 AI빅데이터실 02-6393-2714)에 임상도 4차 전국 · 임도망도 전국 상용 탑재 허락 서면 요청(CC BY 표기 근거 · 약관 「사무국 허락」 조항 근거). 국내 망에서 상품 페이지 · 약관 전문 직접 열람 선행. +4. 임도 보조 후보 — 연속수치지형도(1유형) 도로 레이어에 임도가 들어 있는지 확인. +5. 산사태 — 상용 가능한 전국 파일 창구 없음 · 1 · 2 결과 기다림. + +## 7. 확인 못 함 + +- 산림빅데이터거래소 상품 페이지 · 약관 전문 직접 열람(접속 거부) · CC BY 표기와 「all rights reserved」 · 「자체 사용 목적」 조항의 관계 · 임상도 1:5,000 전국판(FRT001001) 라이선스. +- data.go.kr 목록별 오류신고의 포털 공통 처리 기한. +- 연속수치지형도에 임도 레이어 · 코드 존재 여부. +- 브이월드 산사태 타일의 이용 조건 · 정보공개청구로 받은 자료의 이용 조건. +- egis · AI Hub · 경기 · 강원 · 경북 공간정보포털의 임상도 · 임도 · 산사태 자료(검색에 안 나옴). +- 옛 오픈마켓 산사태 SHP(2019)의 이관 여부. + +## 출처 (513 조사에 없는 것) + +- F7 https://www.forest.go.kr/kfsweb/kfi/kfs/trail/fTMap.do?pblicDataId=PBD0000059&tabs=1&mn=NKFS_06_08_02 — 산림청 공공데이터 개방 임상도 +- F8 https://www.forest.go.kr/newkfsweb/html/HtmlPage.do?pg=%2Ffgis%2FUI_KFS_5002_020700.html&mn=KFS_02_04_03_04_04&orgId=fgis — 임도망도 현황 +- F9 https://www.forest.go.kr/kfsweb/kfi/kfs/trail/sanSaTae.do?mn=NKFS_01_01 — 산사태위험지도 개방 견본 +- F10 https://www.forest.go.kr/newkfsweb/cop/bbs/selectBoardArticle.do?nttId=3215640&bbsId=BBSMSTR_1351&mn=KFS_02_04_07_02&orgId=fgis — 산사태위험지도 다운 문의 답변(2026-01-14) +- F11 https://www.forest.go.kr/newkfsweb/cop/bbs/selectBoardArticle.do?nttId=3182126&bbsId=BBSMSTR_1351&mn=KFS_02_04_07_02&orgId=fgis — 산사태위험지도 SHP 답변(2023-04-13) +- F12 https://www.forest.go.kr/kfsweb/cop/bbs/selectBoardList.do?bbsId=BBSMSTR_1662&mn=NKFS_06_08_04 — 산림청 데이터 품질오류 신고 +- D9~D18 data.go.kr 258662 · 95584 · 95799 · 15006041 · 15120710 · 15071663 · 15059721 · 81177 · 15139681 · 85304 +- V4 https://www.vworld.kr/dtmk/dtmk_ntads_s001.do?svcCde=MK — 브이월드 데이터 목록(산림청 12건) +- V5 https://www.vworld.kr/dtmk/dtmk_ntads_s002.do?svcCde=DT&dsId=DAT_0000000000000096 — 브이월드 산사태위험지도 +- N1 http://data.nsdi.go.kr/dataset/20210708ds00009 — 옛 오픈마켓(접속 불가 · 검색 색인만) +- B2 · B4~B9 https://www.bigdata-forest.kr/… — 산림빅데이터거래소(검색 색인만) +- K1 https://www.kgeop.go.kr/ · K2 https://www.kofpi.or.kr/service/bigdata_01.do +- P3 https://www.data.go.kr/tcs/dor/insertDataOfferReqstProcssView.do · P4 https://www.data.go.kr/tcs/dor/insertTrublMdatReqstProcssView.do · P5 https://www.data.go.kr/odmc/intro/index.do · M2 https://www.mois.go.kr/frt/sub/a06/b02/openData_4/screen.do +- C1 https://www.mcst.go.kr/site/s_policy/copyright/question/question07.jsp · C2 https://www.kogl.or.kr/info/license.do +- W1 https://www.imwood.co.kr/news/articleView.html?idxno=25690 diff --git a/docs/raw/요구_1025/사용자요구_1025.md b/docs/raw/요구_1025/사용자요구_1025.md index e0828c8d3..5c66a1def 100644 --- a/docs/raw/요구_1025/사용자요구_1025.md +++ b/docs/raw/요구_1025/사용자요구_1025.md @@ -1045,3 +1045,27 @@ CPU는 더늘리기는 힘들어보이고 GPU 사용을 위해 부품사용에 ## 요구 519 (10-31 · 사용자) 같은 데이터 인지 확인해보면 될것같은데. / (붙여넣음: 산사태위험지도 WMS — 재난안전 · 제공기관 산림청 · REST · WMS · 활용 신청 258 · 갱신주기 1년 · 수정일 2025-05-30 · 5등급 · 호출 safemap.go.kr/openapi2/IF_0046_WMS · 레이어 A2SM_SANSATAI · serviceKey · srs · bbox · format · width · height · transparent · 범례 lgdInfo?intId=IF_0046) 이거 확인해봐. 그리고 포털 활용신청은 한거야. 이전 채팅에 일반 인증키가 있잖아. + +## 요구 520 (10-31 · 사용자) + +(붙여넣음: 이름 · 이메일 · 발급키 — 값 적지 않음) 이게 행안부 오픈 api키야. + +## 요구 521 (10-31 · 사용자) + +오류 신고는 취소해. + +## 요구 522 (10-31 · 사용자) + +일단 확인은 나중에 한번에 하고 다음으로 찾아야할 데이터는? + +## 요구 523 (10-31 · 사용자) + +지금 받은 3개는 결국 전부 오픈 API야 참고해. 다음으로 임도망도은 산림공간정보에서 받았는데 경상북도 지역만 일단 받았어. 확인해봐. 47.zip이야. + +## 요구 524 (10-31 · 사용자) + +당연히 임도는 국유림에 진행되기 때문에 알수 없는게 맞아. 사유지나 지자체가 하는건 사용자가 직접 넣는게 맞는것같아. 다음으로 지금 전체를 다운받아서 넣었어. 확인해봐. + +## 요구 525 (10-31 · 사용자) + +임상도는 일단안해도 될것같은데 어떻게 생각함? / 좋아 제외하자. 다음 확보데이터는 너가 말했던 1. 보호·규제 구역 묶음 2. 생태자연도 항목이야. 3. 지질도 (암상) 인데 가장 먼저 1번 보호 규제 구역 묶음인데 이중에 벌써 받은 정보가 있었을거 같은데? diff --git a/resources/tester/test_145_2_b09_labor_calc.py b/resources/tester/test_145_2_b09_labor_calc.py index 883a453c8..20e07bddc 100644 --- a/resources/tester/test_145_2_b09_labor_calc.py +++ b/resources/tester/test_145_2_b09_labor_calc.py @@ -311,6 +311,10 @@ def test_견본_사본_대가_블록(project): assert pct["kind"] == "pct" and pct["base"] == "part:material" and pct["qty"] == 20 sc = blocks[b["lines"][3]["key"]] assert sc["kind"] == "사전조합" and b["lines"][3]["qty"] == 4 + assert (sc["spec"], sc["spec_raw"]) == ( + "마스터", + "SC-T", + ) # 규격 = 층 낱말 · 번호는 풍선(spec_raw) assert ( sc["lines"][0]["kind"] == "sangeun" and sc["lines"][0]["key"] == a["lines"][0]["key"] diff --git a/resources/tester/test_701_1_b08_wall_summary.py b/resources/tester/test_701_1_b08_wall_summary.py index c8723be4b..f651ca569 100644 --- a/resources/tester/test_701_1_b08_wall_summary.py +++ b/resources/tester/test_701_1_b08_wall_summary.py @@ -125,4 +125,4 @@ def test_B09_금액_길이_곱하기_대가(out): inputs = {"bill": {"items": structure_items(out)}} sheets = compute_estimation(inputs, {}, {"s-wall": price}.get) row = next(r for r in sheets["bill"]["rows"] if r["key"].endswith("#blk:w")) - assert (row["qty"], row["sum"], row["note"]) == (12.0, 1495128, "대가1 · system||SS000201") + assert (row["qty"], row["sum"], row["note"]) == (12.0, 1495128, "대가1 · 마스터") diff --git a/resources/tester/test_704_3_hopyo_grid.py b/resources/tester/test_704_3_hopyo_grid.py index 935a63bcd..96bc0d304 100644 --- a/resources/tester/test_704_3_hopyo_grid.py +++ b/resources/tester/test_704_3_hopyo_grid.py @@ -58,7 +58,7 @@ def test_머리_구성_합계_줄_꼬리표_빈_줄(): STUBS = r""" -const tc = (k) => ({ Grid_NoQty: "수량 없음", Grid_Apart: "별도계상 · 합계 밖", Labor_Layer: "켜쌓기 {n}%" })[k] ?? k; +const tc = (k) => ({ Grid_NoQty: "수량 없음", Grid_Apart: "별도계상 · 합계 밖", Labor_Layer: "켜쌓기 {n}%", Grid_Head: "일위대가", Grid_Mine: "내 자료집", Lib_Company: "회사", Lib_System: "마스터" })[k] ?? k; const valueLine = (inputs, values) => ({ text: Object.values(values ?? {}).join(" · "), empty: 0 }); """ @@ -108,7 +108,7 @@ def test_M02_문서_calc_를_격자_줄로(): "\nconsole.log(JSON.stringify({ b, foot: hopyoGrid([b]).rows.at(-1), wait: hopyoGrid([wait]).rows.at(-1) }));" ), STUBS) # fmt: skip b = out["b"] - assert b["name"] == "SU-000006" and b["unit"] == "m" and b["total"]["sum"] == 112559.55 + assert b["name"] == "일위대가" and b["unit"] == "m" and b["total"]["sum"] == 112559.55 one, two, mt1, mt2 = b["lines"] assert ( one["name"] == "돌쌓기 찰쌓기" @@ -141,3 +141,19 @@ def test_세_곳_같은_부품(): assert '"@ui/ui_template_hopyo_grid"' in m02 and '"@ui/ui_template_hopyo_grid"' in b09 kit = (ROOT / "ui_template/ui_template_ui_kit_parts.ts").read_text("utf-8") assert 'key: "hopyo-grid"' in kit # 부품 모음 견본 + + +def test_번호는_글에_안_냄_사람_글은_그대로(): + files = [GRID, "M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts", + "M02_MasterTemplete/M02_MasterTemplete_Labor_Spec.ts", + "M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts"] # fmt: skip + ref = {"로직": "", "별칭": None, "입력값": {}, "수량": {"손": 1}, + "참조": {"층": "personal", "주인": "1/3", "키": "SC-000002"}} # fmt: skip + named = {**DOC, "일위대가번": "제5호표", "담은로직": [ref]} + out = _node(files, ( + f"\nconst a = laborBlock({json.dumps(named, ensure_ascii=False)}, null, new Map());" + f"\nconst c = laborBlock({json.dumps({**named, '일위대가번': 'SP3-000015'}, ensure_ascii=False)}, null, new Map());" + "\nconsole.log(JSON.stringify({ a, c }));" + ), STUBS) # fmt: skip + assert out["a"]["name"] == "제5호표" and out["c"]["name"] == "일위대가" + assert out["a"]["lines"][0]["note"] == "내 자료집" diff --git a/resources/tester/test_704_logic_standard.py b/resources/tester/test_704_logic_standard.py index 2cd394958..97a5f2191 100644 --- a/resources/tester/test_704_logic_standard.py +++ b/resources/tester/test_704_logic_standard.py @@ -34,13 +34,14 @@ def _given(key: str, change: dict) -> dict: @pytest.mark.parametrize("key", THREE) -def test_줄_금액은_M01_시험_계산(key: str) -> None: +def test_줄_금액은_M01_시험_계산에_끝수만_버림(key: str) -> None: rows = _rows(key) assert rows for row in rows: got = m01.calc(key, _given(key, row["바꿈"])) assert got["ok"], got - assert row["금액"] == pytest.approx(float(got["sums"]["계"]), abs=0.01), row["규격"] + gap = float(got["sums"]["계"]) - row["금액"] # 줄 · 비목 버림(B09 산근 규칙) — 최대 3 원 + assert -0.01 <= gap < 3.2, row["규격"] def test_대표_규격_펼침() -> None: diff --git a/resources/tester/test_704_settle.py b/resources/tester/test_704_settle.py new file mode 100644 index 000000000..7e85217bd --- /dev/null +++ b/resources/tester/test_704_settle.py @@ -0,0 +1,75 @@ +"""704-2 딸림 — 기준 목록 · 계산서 끝수 = B09 산근 규칙(줄마다 ROUNDDOWN 1 · 비목마다 ROUNDDOWN 0). + +거울: 엔진 줄(수량 · 단가)에 B09 `_money` · `block_totals` 를 그대로 씌운 값 = `settle` 값. +""" + +import re +import sys +from decimal import Decimal +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from B09_Estimation import B09_Estimation_Sangeun as sg # noqa: E402 +from M01_MasterData import M01_MasterData_Store as m01 # noqa: E402 +from M02_MasterTemplete import M02_Logic_Settle as st # noqa: E402 +from M02_MasterTemplete import M02_Logic_Standard as ls # noqa: E402 + +mf = m01.cm.mf +PART_OF = {"노무비": "labor", "재료비": "material", "경비": "expense"} + + +def _plain(row: dict) -> bool: + """로직 줄 · 덧줄 · 작업량 Q 가 없는 돈 로직 — 엔진 줄만으로 B09 값을 낼 수 있음.""" + return ( + str(row.get("결과단위") or "").startswith("원") + and not row.get("덧줄") + and not row.get("끝수") + and not any(i.get("종류") == "로직" for i in row.get("호표") or []) + and not any(re.fullmatch(r"Q\d*", s["이름"]) for s in row.get("중간") or []) + ) + + +def test_줄마다_버림_비목마다_버림_B09와_같음(): + files, whole = m01.loaded() + checked = 0 + for _, data in m01._logic_files(files): + for row in data.get("줄") or []: + if not _plain(row) or checked >= 60: + continue + base, _ = ls.axes(whole, row) + given = mf.fill_empty(row, base) + got = mf.run(whole, row["키"], given) + lines = [] + for line in got["줄"]: + one = dict.fromkeys(PART_OF.values(), 0.0) + for part, money in line["비목"].items(): + one[PART_OF[part]] = sg._money(1.0, float(money)) if money is not None else None + lines.append(one) + if any(v is None for one in lines for v in one.values()): + continue + want = sg.block_totals(lines) + mine = st.settle(whole, row["키"], given) + for name, part in PART_OF.items(): + assert float(mine[name]) == want[part], (row["키"], name) + assert float(mine["계"]) == want["sum"], row["키"] + checked += 1 + assert checked >= 30 + + +def test_계산서_줄_소계_계가_맞물림(): + files, whole = m01.loaded() + key = "GF000089" + got = ls.sheet(key, {}) + assert got["묶음"], key + for group in got["묶음"]: + lines = [Decimal(str(x["금액"] or 0)) for x in group["줄"]] + assert all(v == v.quantize(Decimal("0.1")) for v in lines), group["비목"] + assert group["소계"] == int(group["소계"]) + assert group["소계"] <= sum(lines) < group["소계"] + 1 + Decimal("0.05"), group["비목"] + assert got["계"] == sum(g["소계"] for g in got["묶음"]) + assert got["계"] == ls._money( + whole, whole.logic(key), ls._given(whole, whole.logic(key), {})[0] + )