Merge remote-tracking branch 'origin/sub_laptop_7' into sub_laptop_8
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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)},
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
}
|
||||
const over = await askConfirm({
|
||||
title: "같은 이름",
|
||||
message: `내 자료집에 「${name.trim()}」(${same}) 이 있음 — 덮어쓰기 · 새 이름`,
|
||||
message: `내 자료집에 「${name.trim()}」 이 있음 — 덮어쓰기 · 새 이름`,
|
||||
okLabel: "덮어쓰기",
|
||||
cancelLabel: "새 이름",
|
||||
});
|
||||
|
||||
@@ -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["비목"]]
|
||||
@@ -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["비목"],
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -15,7 +15,7 @@ import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
|
||||
export interface RefEditOptions {
|
||||
ref: LaborRef;
|
||||
/** 「SC-000001 돌붙임」 */
|
||||
/** 「돌붙임」 */
|
||||
title: string;
|
||||
readOnly: boolean;
|
||||
closed: () => void;
|
||||
|
||||
@@ -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<string, readonly [string, string]>;
|
||||
|
||||
export type ComboTextKey = keyof typeof TEXT;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -157,11 +157,15 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise<Mounte
|
||||
go: (next) => void go("personal", next, open),
|
||||
})
|
||||
: panel;
|
||||
// 704-4 [구조물 조합으로] 만든 항목 → 나무 · 옆 목록 다시(페이지가 내려가면 듣기 끝)
|
||||
// 704-4 [구조물 조합으로] 만든 항목 → 나무 · 옆 목록 · 열린 표(행렬)를 다시(페이지가 내려가면 듣기 끝)
|
||||
const afterItem = async (): Promise<void> => {
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
@@ -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번 보호 규제 구역 묶음인데 이중에 벌써 받은 정보가 있었을거 같은데?
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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 · 마스터")
|
||||
|
||||
@@ -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"] == "내 자료집"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
Reference in New Issue
Block a user