Merge remote-tracking branches 'origin/sub_laptop_1' and 'origin/sub_laptop_2' into main_laptop_1

This commit is contained in:
2026-09-19 16:02:57 +09:00
7 changed files with 2053 additions and 9 deletions
+7
View File
@@ -38,6 +38,7 @@
- 값이 여럿인 요소(기계)는 `값` 을 묶음으로 — `{ "가격": …, "상각비계수": …, "정비비계수": …, "관리비계수": …, "손료계수": …, "주연료": …, "주연료종류": …, "잡재료율": …, "조종원": …, "조종원직종": … }` · `손료계수` = 원문 「계」.
- 묶음 요소는 `단위` · `출처` 도 같은 모양의 묶음.
- 원문에 없는 값을 지어 넣지 않음. 원문 「-」 = 칸을 비움(`null`).
- 원문 이름 대신 번호로 적은 값(`조종원직종` = 노임 번호 「1048」)은 원문 이름을 `비고` 칸에.
## 5. 표형 요소 (소요량 · 계수)
@@ -64,6 +65,8 @@
- 값 칸에 범위(「80 ∼ 100」)는 `[아래, 위]` — 로직은 설계자 입력 칸으로 받음(기본값 두지 않음).
- 수와 글이 섞인 칸(「20(135°)」)은 값 칸에 수 · `<값 칸>원문` 칸에 원문 글.
- 줄마다 단위가 다르면 `단위` 칸을 줄에 둠.
- 분수·식으로 적힌 값(「L/V×2×60」)은 값 칸에 식 글자 그대로 · `<값 칸>원문` 칸에 원문.
- 원문에 짝 표시 없이 나란히 놓인 줄은 `짝` 칸으로 묶음.
- 수는 원문 그대로(쉼표만 뺌). 단위 환산하지 않음.
- `주` 는 원문 글 그대로 — 계산에 쓰는 수가 들어 있으면 따로 계수 표로도 올림.
@@ -114,3 +117,7 @@
- 입력 · 중간 · 비목 합은 이름 그대로.
- 쓸 수 있는 것 — `+ - * /` · 괄호 · 비교 `< <= > >= == !=` · `그리고` `또는` · `찾기` · `로직` · `만약(조건, 참, 거짓)` · `올림` `버림` `반올림`(자리) · `최소` `최대`. 그 밖은 쓰지 않음.
- 로직이 돌고 도는 참조를 만들지 않음.
## 8. 검사
- 고친 뒤 `scripts/check_master.py` — 틀 · 본문 대조 · 로직 검사. 로직은 `계산` 으로 손 계산과 맞댐.
@@ -0,0 +1,305 @@
# -*- coding: utf-8 -*-
"""마스터 데이터 검사 도구 (`resources/master_data/_틀.md`).
돌리기: ./venv/Scripts/python.exe resources/master_data/scripts/check_master.py [틀|본문|로직] [파일 …]
./venv/Scripts/python.exe resources/master_data/scripts/check_master.py 계산 "산림품셈:13-4-1 메쌓기" 뒷길이=35 돌=견치돌 …
(1) 틀 — 파일 이름 · 머리 칸 · 줄 칸 · 출처 모양
(2) 본문 — 표형 요소의 출처 절을 본문 md 에서 찾아 수를 맞댐 · 결손(본문 표에 있고 요소에 없음)·허구(요소에 있고 본문 절에 없음)
(3) 로직 — 변수가 가리키는 요소·표·칸 · 찾기 조건 이름 · 로직 입력 · 돌고 도는 참조
(4) 계산 — 로직 열쇠와 입력값으로 호표 줄별 수량·단가·금액 · 비목 합 (`master_formula.py`)
수 뽑기는 `resources/knowledge/original/_pipeline/pum_md_tool.py` 의 것을 씀.
"""
from __future__ import annotations
import json
import re
import sys
from decimal import Decimal
from pathlib import Path
HERE = Path(__file__).resolve().parent
MASTER = HERE.parent
ROOT = MASTER.parents[1]
COST = ROOT / "resources/knowledge/original/원가계산"
sys.path.insert(0, str(HERE))
sys.path.insert(0, str(ROOT / "resources/knowledge/original/_pipeline"))
import master_formula as mf # noqa: E402
from pum_md_tool import _NUM, md_lines # noqa: E402
BOOKS = (
"산림품셈",
"건설품셈",
"건설노임",
"제조노임",
"나라장터자재",
"오피넷유가",
"한국은행환율",
"조달청제비율",
"자체",
)
DIVISIONS = {
"공통": "01_공통부문",
"토목": "02_토목부문",
"건축": "03_건축부문",
"기계설비": "04_기계설비부문",
"유지관리": "05_유지관리부문",
}
_FILE = re.compile(rf"^({'|'.join(mf.GROUPS)})_({'|'.join(BOOKS)})(?:_(\d+장|[^_]+))?\.json$")
_SOURCE = re.compile(
rf"^(?:자체|(?:{'|'.join(b for b in BOOKS if b not in ('건설품셈', '자체'))})(?:\s\S.*)?|건설품셈 (?:{'|'.join(DIVISIONS)}) \S.*)$"
)
_IDENT = re.compile(r"\d+-\d+(?:-\d+)*")
ROW_KEYS = {
"요소": ("열쇠", "이름", "", "출처"),
"": ("열쇠", "이름", "기준", "출처", "조건", "값칸", "", ""),
"로직": ("열쇠", "이름", "결과단위", "출처", "소유", "입력", "중간", "끝수"),
}
def load(names: list[str] | None = None) -> dict[str, dict]:
"""첫 층 JSON 모두(밑줄 파일 빼고) — 수는 Decimal."""
out = {}
for path in sorted(MASTER.glob("*.json")):
if path.name.startswith("_") or (
names and path.name not in names and path.stem not in names
):
continue
out[path.name] = json.loads(
path.read_text(encoding="utf-8"), parse_float=Decimal, parse_int=Decimal
)
return out
def master() -> mf.Master:
return mf.Master(load())
# ── (1) 틀 ────────────────────────────────────────────────────────────
def _source_ok(source) -> bool:
if isinstance(source, dict):
return all(_source_ok(v) for v in source.values())
return isinstance(source, str) and bool(_SOURCE.match(source))
def check_form(name: str, data: dict) -> list[str]:
out = []
m = _FILE.match(name)
if not m:
out.append(f"{name} · 파일 이름이 「그룹_원문[_장].json」 아님")
elif (data.get("그룹"), data.get("원문")) != (m.group(1), m.group(2)):
out.append(f"{name} · 머리 그룹·원문이 파일 이름과 다름")
for head in ("그룹", "원문", ""):
if head not in data:
out.append(f"{name} · 머리 「{head}」 없음")
table = data.get("그룹") in mf.TABLE_GROUPS
kind = "" if table else "로직" if data.get("그룹") == "로직" else "요소"
rows = data.get("" if table else "")
if not isinstance(rows, list):
return out + [f"{name} · 머리 「{'' if table else ''}」 목록 없음"]
seen = set()
for row in rows:
key = str(row.get("열쇠"))
where = f"{name} · {key}"
if key in seen:
out.append(f"{where} · 열쇠 겹침")
seen.add(key)
for need in ROW_KEYS[kind]:
if need not in row:
out.append(f"{where} · 칸 「{need}」 없음")
if "출처" in row and not _source_ok(row["출처"]):
out.append(f"{where} · 출처 모양 「{row['출처']}")
if kind == "요소" and isinstance(row.get(""), dict):
for bundle in ("단위", "출처"):
if isinstance(row.get(bundle), dict) and set(row[bundle]) != set(row[""]):
out.append(f"{where} · 「{bundle}」 묶음 칸이 값 묶음과 다름")
if kind == "":
out += _check_table(where, row)
if kind == "로직" and "결과" not in row and "호표" not in row:
out.append(f"{where} · 「호표」 도 「결과」 도 없음")
return out
def _check_table(where: str, table: dict) -> list[str]:
out, conds, cols = [], table.get("조건", {}), table.get("값칸", {})
allowed = set(conds) | set(cols) | {"단위", ""} | {c + "원문" for c in cols}
for kind in conds.values():
if kind not in ("", "고르기", "범위"):
out.append(f"{where} · 조건 종류 「{kind}")
if "범위" in conds.values() and "범위규칙" not in table:
out.append(f"{where} · 범위 조건에 「범위규칙」 없음")
for number, row in enumerate(table.get("", []), start=1):
extra = set(row) - allowed
if extra:
out.append(f"{where}{number} · 모르는 칸 {sorted(extra)}")
if not set(row) & set(cols):
out.append(f"{where}{number} · 값 칸 없음")
for cond, kind in conds.items():
if cond not in row:
continue
value = row[cond]
if kind == "범위" and not (isinstance(value, list) and len(value) == 2):
out.append(f"{where}{number} · 범위 조건 「{cond}」 이 [아래, 위] 아님")
if kind == "" and not isinstance(value, Decimal):
out.append(f"{where}{number} · 수 조건 「{cond}」 이 수 아님")
for col in cols:
value = row.get(col)
if isinstance(value, list) and len(value) != 2:
out.append(f"{where}{number} · 값 칸 「{col}」 범위가 [아래, 위] 아님")
return out
# ── (2) 본문 ──────────────────────────────────────────────────────────
def _key(parts: str) -> tuple[int, ...]:
return tuple(int(p) for p in parts.split("-"))
def section_of(source: str) -> tuple[Path | None, str, str]:
"""(본문 md, 절 번호, 절 글) — 못 찾으면 md 나 글이 빔."""
ident = _IDENT.search(source)
if not ident:
return None, "", ""
ident = ident.group()
parts = _key(ident)
if source.startswith("산림품셈"):
chapters = COST / "산림_표준품셈/본문"
elif source.startswith("건설품셈"):
chapters = COST / "건설공사_표준품셈/본문" / DIVISIONS.get(source.split()[1], "")
else:
return None, ident, ""
best = None
for md in chapters.glob(f"{parts[0]:02d}장_*/*.md"):
head = md.name.split("_", 1)[0]
if not _IDENT.fullmatch(head):
continue
own = _key(head)
if (
own[0]
and own == parts[: len(own)]
and (best is None or len(own) > len(_key(best.name.split("_", 1)[0])))
):
best = md
if best is None:
return None, ident, ""
lines = best.read_text(encoding="utf-8").split("\n")
heading = re.compile(rf"^(#+)\s*{re.escape(ident)}\.?(\s|$)")
for at, line in enumerate(lines):
m = heading.match(line)
if m:
level, end = len(m.group(1)), len(lines)
for later in range(at + 1, len(lines)):
h = re.match(r"^(#+)\s", lines[later])
if h and len(h.group(1)) <= level:
end = later
break
return best, ident, "\n".join(lines[at:end])
return best, ident, ""
def _canon(token: str) -> str:
return format(Decimal(token.replace(",", "")).normalize(), "f")
def numbers_in(text: str) -> set[str]:
return {_canon(t) for _, line in md_lines(text) for t in _NUM.findall(line)}
def _flat(value) -> set[str]:
if isinstance(value, Decimal):
return {_canon(str(abs(value)))}
if isinstance(value, str):
return {_canon(t) for t in _NUM.findall(value)}
if isinstance(value, list):
return set().union(*map(_flat, value)) if value else set()
if isinstance(value, dict):
return set().union(*map(_flat, value.values())) if value else set()
return set()
def check_body(files: dict[str, dict]) -> list[str]:
"""표마다 허구 · 절마다 결손."""
out, by_section = [], {}
for name, data in files.items():
for table in (data.get("") or []) if data.get("그룹") in mf.TABLE_GROUPS else []:
where = f"{name} · {table.get('열쇠')}"
md, ident, text = section_of(str(table.get("출처", "")))
if not text:
out.append(f"{where} · 본문 절 못 찾음 「{table.get('출처')}")
continue
mine = _flat(table.get("")) | _flat(table.get("기준")) | _flat(table.get(""))
fake = mine - numbers_in(text)
if fake:
out.append(f"{where} · 허구 {len(fake)} {sorted(fake, key=Decimal)}")
slot = by_section.setdefault((md, ident), [text, set(), []])
slot[1] |= mine
slot[2].append(where)
for (md, ident), (text, mine, tables) in by_section.items():
table_text = "\n".join(line for line in text.split("\n") if line.lstrip().startswith("|"))
lack = numbers_in(table_text) - mine
if lack:
out.append(
f"{md.name} {ident} · 결손 {len(lack)} {sorted(lack, key=Decimal)}{' · '.join(tables)}"
)
return out
# ── (3) 로직 ──────────────────────────────────────────────────────────
def check_logics(files: dict[str, dict], whole: mf.Master) -> list[str]:
out, graph = [], {}
for name, data in files.items():
if data.get("그룹") != "로직":
continue
for row in data.get("", []):
found, calls = mf.check_logic(whole, data.get("원문"), row)
out += [f"{name} · {x}" for x in found]
graph[f"{data.get('원문')}:{row.get('열쇠')}"] = calls
out += [f"돌고 도는 참조 · {''.join(loop)}" for loop in mf.find_loops(graph)]
return out
# ── (4) 계산 ──────────────────────────────────────────────────────────
def show(result: dict) -> str:
if "결과" in result:
return f"결과 {result['결과']}"
rows = [
f" {r['이름']} | {r['단위']} | 수량 {r['수량']:.6f} | 단가 {r['단가']} | 금액 {r['금액']:.2f}"
for r in result[""]
]
rows += [f" {k} {result[k]:.2f}" for k in (*mf.COST_ITEMS, "")]
return "\n".join(rows)
def _value(text: str):
try:
return Decimal(text)
except ArithmeticError:
return text
def main(argv: list[str]) -> int:
mode = argv[0] if argv else "전부"
if mode == "계산":
given = dict(arg.split("=", 1) for arg in argv[2:])
print(show(mf.run(master(), argv[1], {k: _value(v) for k, v in given.items()})))
return 0
names = argv[1:] or None
files, whole = load(names), master()
report = {}
if mode in ("", "전부"):
report[""] = [x for name, data in files.items() for x in check_form(name, data)]
if mode in ("본문", "전부"):
report["본문"] = check_body(files)
if mode in ("로직", "전부"):
report["로직"] = check_logics(files, whole)
for title, found in report.items():
print(f"({title}) {len(found)}")
for line in found:
print(" " + line)
return 1 if any(report.values()) else 0
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
sys.exit(main(sys.argv[1:]))
@@ -0,0 +1,562 @@
# -*- coding: utf-8 -*-
"""마스터 로직 식 — 읽기 · 검사 · 시험 계산 (`_틀.md` 6~7장).
식은 틀 7장 범위만 읽음 — eval 쓰지 않음. 수는 Decimal(끝수 사사오입이 흔들리지 않게).
`check_master.py` 가 부름.
"""
from __future__ import annotations
import itertools
import re
from decimal import ROUND_DOWN, ROUND_HALF_UP, ROUND_UP, Decimal
GROUPS = ("인력", "재료", "기계", "소요량", "계수", "환율", "요율", "로직")
TABLE_GROUPS = ("소요량", "계수")
COST_ITEMS = ("노무비", "재료비", "경비")
BUNDLE = (*COST_ITEMS, "")
FUNCS = ("만약", "올림", "버림", "반올림", "최소", "최대")
_NAME = re.compile(r"[\w가-힣]+")
_NUMBER = re.compile(r"\d+(?:\.\d+)?")
_HOLE = re.compile(r"\{([^{}]+)\}")
_OPS = ("<=", ">=", "==", "!=", "<", ">")
class FormulaError(ValueError):
pass
# ── 읽기 ──────────────────────────────────────────────────────────────
class _Parser:
def __init__(self, text: str):
self.s, self.i = text, 0
def fail(self, why: str):
raise FormulaError(f"{why} — 「{self.s}{self.i}")
def ws(self):
while self.i < len(self.s) and self.s[self.i].isspace():
self.i += 1
def peek(self, word: str) -> bool:
self.ws()
return self.s.startswith(word, self.i)
def take(self, word: str) -> bool:
if self.peek(word):
self.i += len(word)
return True
return False
def need(self, word: str):
if not self.take(word):
self.fail(f"{word}」 없음")
def parse(self):
node = self.expr()
self.ws()
if self.i != len(self.s):
self.fail("식 끝에 남은 글자")
return node
def expr(self):
node = self.both()
while self.take("또는"):
node = ("bin", "또는", node, self.both())
return node
def both(self):
node = self.compare()
while self.take("그리고"):
node = ("bin", "그리고", node, self.compare())
return node
def compare(self):
node = self.add()
for op in _OPS:
if self.take(op):
return ("bin", op, node, self.add())
return node
def add(self):
node = self.mul()
while True:
if self.take("+"):
node = ("bin", "+", node, self.mul())
elif self.take("-"):
node = ("bin", "-", node, self.mul())
else:
return node
def mul(self):
node = self.unary()
while True:
if self.take("*"):
node = ("bin", "*", node, self.unary())
elif self.take("/"):
node = ("bin", "/", node, self.unary())
else:
return node
def unary(self):
if self.take("-"):
return ("neg", self.unary())
node = self.primary()
while self.take("."):
node = ("field", node, self.name())
return node
def name(self) -> str:
self.ws()
m = _NAME.match(self.s, self.i)
if not m:
self.fail("이름 없음")
self.i = m.end()
return m.group()
def ref(self) -> str:
"""열쇠 글자 — 괄호 밖의 `,` · `)` · `.칸` · ` 연산자 ` 앞에서 멈춤(열쇠에 빈칸·괄호가 들어감)."""
start, depth = self.i, 0
while self.i < len(self.s):
ch = self.s[self.i]
if ch in "({":
depth += 1
elif ch in ")}":
if depth == 0:
break
depth -= 1
elif depth == 0 and ch == ",":
break
elif depth == 0 and ch == "." and not self.s[self.i + 1 : self.i + 2].isdigit():
break
elif depth == 0 and re.match(r"\s[-+*/<>=!]", self.s[self.i : self.i + 2]):
break
self.i += 1
return self.s[start : self.i].strip()
def args(self) -> list[tuple[str, object]]:
out = []
while self.take(","):
key = self.name()
self.need("=")
out.append((key, self.expr()))
self.need(")")
return out
def primary(self):
self.ws()
if self.take("("):
node = self.expr()
self.need(")")
return node
if self.take("'"):
end = self.s.find("'", self.i)
if end < 0:
self.fail("작은따옴표 안 닫힘")
text, self.i = self.s[self.i : end], end + 1
return ("str", text)
m = _NUMBER.match(self.s, self.i)
if m:
self.i = m.end()
return ("num", Decimal(m.group()))
word = self.name()
if word == "찾기":
self.need("(")
head = self.name()
self.need(":")
return ("find", f"{head}:{self.ref()}", self.args())
if word == "로직":
self.need("(")
return ("logic", self.ref(), self.args())
if word in FUNCS:
self.need("(")
items = [self.expr()]
while self.take(","):
items.append(self.expr())
self.need(")")
return ("call", word, items)
if word in GROUPS and self.s.startswith(":", self.i):
self.i += 1
return ("ref", f"{word}:{self.ref()}")
return ("name", word)
def parse(text) -> tuple:
if isinstance(text, (int, float, Decimal)):
return ("num", Decimal(str(text)))
return _Parser(str(text)).parse()
def walk(node):
"""식 나무의 모든 마디."""
yield node
kind = node[0]
if kind == "bin":
yield from walk(node[2])
yield from walk(node[3])
elif kind in ("neg",):
yield from walk(node[1])
elif kind == "field":
yield from walk(node[1])
elif kind == "call":
for item in node[2]:
yield from walk(item)
elif kind in ("find", "logic"):
for _, item in node[2]:
yield from walk(item)
# ── 마스터 모음 ────────────────────────────────────────────────────────
class Master:
"""요소·표·로직 찾기 — `files` = {파일 이름: 읽은 JSON}(수는 Decimal)."""
def __init__(self, files: dict[str, dict]):
self.index: dict[tuple[str, str], dict[str, dict]] = {}
for data in files.values():
rows = data.get("") if data.get("그룹") in TABLE_GROUPS else data.get("")
book = self.index.setdefault((data.get("그룹"), data.get("원문")), {})
for row in rows or []:
book.setdefault(str(row.get("열쇠")), row)
def get(self, ref: str) -> dict:
group, book, key = (ref.split(":", 2) + ["", ""])[:3]
row = self.index.get((group, book), {}).get(key)
if row is None:
raise FormulaError(f"없는 요소 「{ref}")
return row
def logic(self, ref: str) -> dict:
return self.get(f"로직:{ref}")
def fill(template: str, env: dict) -> str:
def one(m):
if m.group(1) not in env:
raise FormulaError(f"열쇠의 {{{m.group(1)}}} 값 없음")
value = env[m.group(1)]
return format(value.normalize(), "f") if isinstance(value, Decimal) else str(value)
return _HOLE.sub(one, template)
# ── 계산 ──────────────────────────────────────────────────────────────
def _round(x: Decimal, places, mode) -> Decimal:
return x.quantize(Decimal(1).scaleb(-int(places)), rounding=mode)
def _match(table: dict, row: dict, cond: str, want) -> bool:
if cond not in row: # 빈 조건 칸은 무엇이든 맞음
return True
have = row[cond]
if table.get("조건", {}).get(cond) == "범위":
low, high = have
if table.get("범위규칙", "초과·이하") != "초과·이하":
raise FormulaError(f"모르는 범위규칙 「{table.get('범위규칙')}")
return (low is None or want > low) and (high is None or want <= high)
return have == want
def find(table: dict, conds: dict) -> dict:
for cond in conds:
if cond not in table.get("조건", {}):
raise FormulaError(f"{table['열쇠']}」 에 없는 조건 「{cond}")
hits = [row for row in table[""] if all(_match(table, row, c, v) for c, v in conds.items())]
if len(hits) != 1:
raise FormulaError(f"{table['열쇠']}{conds} 맞는 줄 {len(hits)}")
return hits[0]
def evaluate(node, env: dict, master: Master, depth: int = 0):
kind = node[0]
if kind in ("num", "str"):
return node[1]
if kind == "name":
if node[1] not in env:
raise FormulaError(f"모르는 이름 「{node[1]}")
return env[node[1]]
if kind == "ref":
value = master.get(fill(node[1], env)).get("")
if isinstance(value, list):
raise FormulaError(f"{node[1]}」 값이 범위 — 입력으로 받아야 함")
return value
if kind == "field":
base = evaluate(node[1], env, master, depth)
if not isinstance(base, dict) or node[2] not in base:
raise FormulaError(f"칸 「{node[2]}」 없음")
value = base[node[2]]
if isinstance(value, list):
raise FormulaError(f"칸 「{node[2]}」 값이 범위 — 입력으로 받아야 함")
return value
if kind == "neg":
return -evaluate(node[1], env, master, depth)
if kind == "bin":
op, a = node[1], evaluate(node[2], env, master, depth)
if op == "그리고":
return a and evaluate(node[3], env, master, depth)
if op == "또는":
return a or evaluate(node[3], env, master, depth)
b = evaluate(node[3], env, master, depth)
return {
"+": lambda: a + b,
"-": lambda: a - b,
"*": lambda: a * b,
"/": lambda: a / b,
"<": lambda: a < b,
"<=": lambda: a <= b,
">": lambda: a > b,
">=": lambda: a >= b,
"==": lambda: a == b,
"!=": lambda: a != b,
}[op]()
if kind == "call":
name, items = node[1], node[2]
if name == "만약":
test = evaluate(items[0], env, master, depth)
return evaluate(items[1] if test else items[2], env, master, depth)
values = [evaluate(x, env, master, depth) for x in items]
if name in ("최소", "최대"):
return (min if name == "최소" else max)(values)
mode = {"올림": ROUND_UP, "버림": ROUND_DOWN, "반올림": ROUND_HALF_UP}[name]
return _round(values[0], values[1] if len(values) > 1 else 0, mode)
if kind == "find":
table = master.get(fill(node[1], env))
return find(table, {k: evaluate(v, env, master, depth) for k, v in node[2]})
if kind == "logic":
if depth > 20:
raise FormulaError("로직 부르기가 너무 깊음 — 돌고 도는 참조")
given = {k: evaluate(v, env, master, depth) for k, v in node[2]}
result = run(master, fill(node[1], env), given, depth + 1)
return result["결과"] if "결과" in result else {k: result[k] for k in BUNDLE}
raise FormulaError(f"모르는 마디 {kind}")
def _inputs(row: dict, given: dict) -> dict:
env = {}
for spec in row.get("입력", []):
name = spec["이름"]
if name not in given:
raise FormulaError(f"{row['열쇠']}」 입력 「{name}」 없음")
value = given[name]
if isinstance(value, (int, float)):
value = Decimal(str(value))
if "고르기" in spec and value not in spec["고르기"]:
raise FormulaError(f"입력 「{name}」={value} 고르기 밖")
if "범위" in spec:
low, high = spec["범위"]
if not low <= value <= high:
raise FormulaError(f"입력 「{name}」={value} 범위 [{low}, {high}] 밖")
env[name] = value
unknown = set(given) - set(env)
if unknown:
raise FormulaError(f"{row['열쇠']}」 모르는 입력 {sorted(unknown)}")
return env
def run(master: Master, ref: str, given: dict, depth: int = 0) -> dict:
"""로직 한 줄 계산 — 돈 로직은 호표 줄 · 비목 합 · 계, 돈 아닌 로직은 결과 수 하나."""
row = master.logic(ref)
env = _inputs(row, given)
for step in row.get("중간", []):
env[step["이름"]] = evaluate(parse(step[""]), env, master, depth)
if "결과" in row:
return {"결과": evaluate(parse(row["결과"][""]), env, master, depth), "중간": env}
sums = dict.fromkeys(COST_ITEMS, Decimal(0))
lines = []
for item in row.get("호표", []):
qty = evaluate(parse(item["수량"]), env, master, depth)
if item["종류"] == "로직":
bundle = evaluate(parse(item["요소"]), env, master, depth)
price = bundle[""]
split = (
{k: qty * bundle[k] for k in COST_ITEMS}
if "비목" not in item
else {item["비목"]: qty * price}
)
else:
price = master.get(fill(item["요소"], env))[""]
split = {item["비목"]: qty * price}
for k, v in split.items():
sums[k] += v
lines.append(
{
"이름": item.get("이름", ""),
"단위": item.get("단위", ""),
"수량": qty,
"단가": price,
"금액": qty * price,
"비목": split,
}
)
for extra in row.get("덧줄", []):
env.update(sums)
value = evaluate(parse(extra[""]), env, master, depth)
env[extra["이름"]] = value
sums[extra["비목"]] += value
lines.append(
{
"이름": extra["이름"],
"단위": "",
"수량": Decimal(1),
"단가": value,
"금액": value,
"비목": {extra["비목"]: value},
}
)
return {"": lines, **sums, "": sum(sums.values()), "중간": env}
# ── 검사 ──────────────────────────────────────────────────────────────
def _choices(row: dict, name: str, master: Master):
"""이름이 가질 수 있는 값 — 고르기 입력, 또는 고르기 입력만 끼운 요소 칸인 중간. 모르면 None."""
for spec in row.get("입력", []):
if spec["이름"] == name:
return spec.get("고르기")
for step in row.get("중간", []):
if step["이름"] == name:
node = parse(step[""])
if node[0] == "field" and node[1][0] == "ref":
out = set()
for ref in _expand(row, node[1][1], master) or []:
try:
out.add(str(master.get(ref)[""][node[2]]))
except (FormulaError, KeyError, TypeError):
pass
return sorted(out)
return None
def _expand(row: dict, template: str, master: Master):
"""열쇠 틀의 {이름} 을 고를 수 있는 값으로 모두 끼움 — 못 정하면 None."""
holes = _HOLE.findall(template)
options = [_choices(row, h, master) for h in holes]
if any(o is None for o in options):
return None
return [
fill(template, {h: v for h, v in zip(holes, combo)})
for combo in itertools.product(*options)
]
def _check_node(row, node, known: set, master: Master, where: str, out: list, calls: set):
for part in walk(node):
kind = part[0]
if kind == "name" and part[1] not in known:
out.append(f"{where} · 모르는 이름 「{part[1]}")
if kind in ("ref", "find", "logic"):
for hole in _HOLE.findall(part[1]):
if hole not in known:
out.append(f"{where} · 열쇠 {{{hole}}} 가 입력·중간 아님")
if kind == "ref":
refs = _expand(row, part[1], master) or []
for ref in refs:
try:
master.get(ref)
except FormulaError as e:
out.append(f"{where} · {e}")
if kind == "field" and part[1][0] == "ref":
for ref in _expand(row, part[1][1], master) or []:
try:
value = master.get(ref).get("")
except FormulaError:
continue
if not isinstance(value, dict) or part[2] not in value:
out.append(f"{where} · 「{ref}」 에 칸 「{part[2]}」 없음")
if kind == "find":
if part[1].split(":")[0] not in TABLE_GROUPS:
out.append(f"{where} · 찾기 대상 「{part[1]}」 이 표 아님")
continue
try:
table = master.get(part[1])
except FormulaError as e:
out.append(f"{where} · {e}")
continue
for cond, _ in part[2]:
if cond not in table.get("조건", {}):
out.append(f"{where} · 「{part[1]}」 에 조건 「{cond}」 없음")
if kind == "field" and part[1][0] == "find":
try:
table = master.get(part[1][1])
except FormulaError:
continue
if part[2] not in table.get("값칸", {}):
out.append(f"{where} · 「{part[1][1]}」 에 값 칸 「{part[2]}」 없음")
if kind == "logic":
calls.add(part[1])
try:
target = master.logic(part[1])
except FormulaError as e:
out.append(f"{where} · {e}")
continue
names = {s["이름"] for s in target.get("입력", [])}
given = {k for k, _ in part[2]}
for extra in sorted(given - names):
out.append(f"{where} · 「{part[1]}」 에 입력 「{extra}」 없음")
for lack in sorted(names - given):
out.append(f"{where} · 「{part[1]}」 입력 「{lack}」 안 줌")
if kind == "field" and part[1][0] == "logic":
try:
target = master.logic(part[1][1])
except FormulaError:
continue
if "결과" in target:
out.append(
f"{where} · 돈 아닌 로직 「{part[1][1]}」 은 수 하나 — 칸 「{part[2]}」 못 씀"
)
elif part[2] not in BUNDLE:
out.append(f"{where} · 로직 결과 칸 「{part[2]}」 은 {BUNDLE} 가운데 하나여야 함")
def check_logic(master: Master, book: str, row: dict) -> tuple[list[str], set]:
"""(문제 목록, 부르는 로직 열쇠) — 변수 · 찾기 조건 · 로직 입력."""
out, calls, key = [], set(), f"{book}:{row.get('열쇠')}"
known = {s["이름"] for s in row.get("입력", [])}
def one(text, where, names):
try:
_check_node(row, parse(text), names, master, where, out, calls)
except FormulaError as e:
out.append(f"{where} · 식 못 읽음: {e}")
for step in row.get("중간", []):
one(step[""], f"{key} 중간 {step['이름']}", known)
known = known | {step["이름"]}
if "결과" in row:
one(row["결과"][""], f"{key} 결과", known)
for item in row.get("호표", []):
where = f"{key} 호표 {item.get('이름', '')}"
one(item["수량"], where + " 수량", known)
if item.get("종류") not in ("인력", "재료", "기계", "로직"):
out.append(f"{where} · 종류 「{item.get('종류')}")
if item.get("비목") not in (*COST_ITEMS, None) or (
item.get("종류") != "로직" and "비목" not in item
):
out.append(f"{where} · 비목 「{item.get('비목')}")
one(item["요소"], where + " 요소", known)
if item.get("종류") != "로직" and not str(item["요소"]).startswith(f"{item.get('종류')}:"):
out.append(f"{where} · 요소 그룹이 종류와 다름")
names = known | set(COST_ITEMS)
for extra in row.get("덧줄", []):
one(extra[""], f"{key} 덧줄 {extra['이름']}", names)
names = names | {extra["이름"]}
if extra.get("비목") not in COST_ITEMS:
out.append(f"{key} 덧줄 {extra['이름']} · 비목 「{extra.get('비목')}")
return out, calls
def find_loops(graph: dict[str, set]) -> list[list[str]]:
"""로직 부르기의 돌고 도는 길."""
loops, state = [], {}
def visit(node, path):
state[node] = 1
for nxt in sorted(graph.get(node, ())):
if state.get(nxt) == 1:
loops.append(path[path.index(nxt) :] + [nxt])
elif nxt not in state:
visit(nxt, path + [nxt])
state[node] = 2
for node in sorted(graph):
if node not in state:
visit(node, [node])
return loops
@@ -0,0 +1,536 @@
{
"그룹": "계수",
"원문": "건설품셈",
"부문": "공통",
"판": "2026",
"줄": [
{
"열쇠": "1-3-1 콘크리트 및 포장용 재료",
"이름": "재료 할증 — 콘크리트 및 포장용 재료",
"출처": "건설품셈 공통 1-3-1 1",
"조건": {"종류": "고르기"},
"값칸": {"정치식": "%", "기타": "%"},
"줄": [
{"종류": "시멘트", "정치식": 2, "기타": 3},
{"종류": "잔골재・채움재", "정치식": 10, "기타": 12},
{"종류": "굵은골재", "정치식": 3, "기타": 5},
{"종류": "아스팔트", "정치식": 2, "기타": 3},
{"종류": "석분", "정치식": 2, "기타": 3},
{"종류": "혼화재", "정치식": 2}
],
"주": ["속채움 재료의 경우에도 이 값을 준용한다."]
},
{
"열쇠": "1-3-1 노상 및 노반재료",
"이름": "재료 할증 — 노상 및 노반재료(선택층, 보조기층, 기층 등)",
"출처": "건설품셈 공통 1-3-1 2",
"조건": {"종류": "고르기"},
"값칸": {"할증률": "%"},
"줄": [
{"종류": "모래", "할증률": 6},
{"종류": "부순돌・자갈・막자갈", "할증률": 4},
{"종류": "점질토", "할증률": 6}
]
},
{
"열쇠": "1-3-1 관 및 구조물기초 부설재료",
"이름": "재료 할증 — 관 및 구조물기초 부설재료",
"출처": "건설품셈 공통 1-3-1 3",
"조건": {"종류": "고르기"},
"값칸": {"할증률": "%"},
"줄": [
{"종류": "모래", "할증률": 4}
]
},
{
"열쇠": "1-3-1 토사(해상)",
"이름": "재료 할증 — 토사(해상)",
"출처": "건설품셈 공통 1-3-1 4",
"조건": {"종류": "고르기"},
"값칸": {"할증률": "%", "비고": "글"},
"줄": [
{"종류": "치환모래(置換砂)", "할증률": 20, "비고": "표면건조포화상태의 모래에 대한 할증률"},
{"종류": "깔모래(敷砂)", "할증률": 30},
{"종류": "사항용모래(砂抗用砂)", "할증률": 20},
{"종류": "압입모래(壓入砂)", "할증률": 40}
]
},
{
"열쇠": "1-3-1 사석(해상)",
"이름": "재료 할증 — 사석(해상)",
"출처": "건설품셈 공통 1-3-1 5",
"조건": {"종류": "고르기", "지반": "고르기", "사석두께": "범위"},
"값칸": {"할증률": "%"},
"범위규칙": "사석두께(m) 이상·미만 — 「2m미만」 = [null, 2] · 「2m이상」 = [2, null]",
"줄": [
{"종류": "기초사석", "지반": "보통지반", "사석두께": [null, 2], "할증률": 25},
{"종류": "기초사석", "지반": "보통지반", "사석두께": [2, null], "할증률": 20},
{"종류": "기초사석", "지반": "모래치환지반", "사석두께": [null, 2], "할증률": 30},
{"종류": "기초사석", "지반": "모래치환지반", "사석두께": [2, null], "할증률": 25},
{"종류": "기초사석", "지반": "연약지반", "사석두께": [null, 2], "할증률": 50},
{"종류": "기초사석", "지반": "연약지반", "사석두께": [2, null], "할증률": 40},
{"종류": "피복석(被覆石)", "지반": "보통지반", "사석두께": [null, 2], "할증률": 15},
{"종류": "피복석(被覆石)", "지반": "보통지반", "사석두께": [2, null], "할증률": 15},
{"종류": "피복석(被覆石)", "지반": "모래치환지반", "사석두께": [null, 2], "할증률": 15},
{"종류": "피복석(被覆石)", "지반": "모래치환지반", "사석두께": [2, null], "할증률": 15},
{"종류": "피복석(被覆石)", "지반": "연약지반", "사석두께": [null, 2], "할증률": 20},
{"종류": "피복석(被覆石)", "지반": "연약지반", "사석두께": [2, null], "할증률": 20},
{"종류": "뒤채움사석", "지반": "보통지반", "사석두께": [null, 2], "할증률": 20},
{"종류": "뒤채움사석", "지반": "보통지반", "사석두께": [2, null], "할증률": 20},
{"종류": "뒤채움사석", "지반": "모래치환지반", "사석두께": [null, 2], "할증률": 20},
{"종류": "뒤채움사석", "지반": "모래치환지반", "사석두께": [2, null], "할증률": 20},
{"종류": "뒤채움사석", "지반": "연약지반", "사석두께": [null, 2], "할증률": 25},
{"종류": "뒤채움사석", "지반": "연약지반", "사석두께": [2, null], "할증률": 25}
],
"주": ["사석의 재료할증률은 공사의 위치, 자연조건(수심, 조류, 파랑, 조위, 해저지질 등)과 제체의 규모 및 공사의 종류 등 현장조건에 적합하게 적용할 수 있다."]
},
{
"열쇠": "1-3-1 속채움(해상)",
"이름": "재료 할증 — 속채움(해상)",
"출처": "건설품셈 공통 1-3-1 6",
"조건": {"종류": "고르기"},
"값칸": {"할증률": "%", "비고": "글"},
"줄": [
{"종류": "모래", "할증률": 10, "비고": "케이슨 또는 세라 블록 등의 속채움시"},
{"종류": "사석", "할증률": 10, "비고": "단, 블록 또는 콘크리트의 속채움재는 제외"}
]
},
{
"열쇠": "1-3-1 강재류",
"이름": "재료 할증 — 강재류",
"출처": "건설품셈 공통 1-3-1 7",
"조건": {"종류": "고르기"},
"값칸": {"할증률": "%"},
"줄": [
{"종류": "원형철근", "할증률": 5},
{"종류": "이형철근", "할증률": 3},
{"종류": "이형철근(교량・지하철 및 이와 유사한 복잡한 구조물의 주철근)", "할증률": "67"},
{"종류": "일반볼트", "할증률": 5},
{"종류": "고장력볼트(H.T.B)", "할증률": 3},
{"종류": "강판(板)", "할증률": 10},
{"종류": "강관", "할증률": 5},
{"종류": "대형형강(形鋼)", "할증률": 7},
{"종류": "소형형강", "할증률": 5},
{"종류": "봉강(棒鋼)", "할증률": 5},
{"종류": "평강대강", "할증률": 5},
{"종류": "경량형강, 각파이프", "할증률": 5},
{"종류": "리벳(제품)", "할증률": 5},
{"종류": "스테인리스강판", "할증률": 10},
{"종류": "스테인리스강관", "할증률": 5},
{"종류": "동판", "할증률": 10},
{"종류": "동관", "할증률": 5},
{"종류": "덕트용금속판", "할증률": 28},
{"종류": "프레스접합식스테인리스강관", "할증률": 5},
{"종류": "이음부속류", "할증률": 5}
],
"주": ["① 이형철근의 경우, 해당 공사 또는 구조물의 시공실적에 따라 조정하여 적용할 수 있다.", "② 강관, 스테인리스강관의 할증률(%)은 옥외공사를 기준한 것이며 옥내공사용 재료의 할증률은 10% 이내로 한다.", "③ 형강(形鋼)의 대형구분은 100㎜ 이상을 말한다.", "④ 현장 여건상 절단 및 가공 등이 불필요한 경우, 상기 할증률을 조정하여 적용할 수 있다."]
},
{
"열쇠": "1-3-1 기타재료",
"이름": "재료 할증 — 기타재료",
"출처": "건설품셈 공통 1-3-1 8",
"조건": {"재료별": "고르기", "세부": "고르기"},
"값칸": {"할증률": "%", "비고": "글"},
"줄": [
{"재료별": "목재", "세부": "각재", "할증률": 5},
{"재료별": "목재", "세부": "판재", "할증률": 10},
{"재료별": "합판", "세부": "일반용합판", "할증률": 3},
{"재료별": "합판", "세부": "수장용합판", "할증률": 5},
{"재료별": "쉬즈관", "세부": "", "할증률": 8},
{"재료별": "쉬즈판", "세부": "", "할증률": 8},
{"재료별": "PVC관/PE관", "세부": "", "할증률": 5, "비고": "'23 신설"},
{"재료별": "원심력철근콘크리트관", "세부": "", "할증률": 3},
{"재료별": "조립식구조물(U형플륨관 등)", "세부": "", "할증률": 3, "비고": "'92 신설"},
{"재료별": "도료", "세부": "", "할증률": 2},
{"재료별": "벽돌", "세부": "붉은벽돌", "할증률": 3},
{"재료별": "벽돌", "세부": "시멘트벽돌", "할증률": 5},
{"재료별": "벽돌", "세부": "내화벽돌", "할증률": 3},
{"재료별": "벽돌", "세부": "경계블록", "할증률": 3},
{"재료별": "벽돌", "세부": "콘크리트블록", "할증률": 4},
{"재료별": "벽돌", "세부": "호안블록", "할증률": 5},
{"재료별": "원석(마름돌용)", "세부": "", "할증률": 30},
{"재료별": "석재판붙임용재", "세부": "정형돌", "할증률": 10},
{"재료별": "석재판붙임용재", "세부": "부정형돌", "할증률": 30},
{"재료별": "조경용수목", "세부": "", "할증률": 10},
{"재료별": "잔디 및 초화류", "세부": "", "할증률": 10},
{"재료별": "래디믹스트 콘크리트 타설(현장플랜트포함)", "세부": "무근구조물", "할증률": 2},
{"재료별": "래디믹스트 콘크리트 타설(현장플랜트포함)", "세부": "철근구조물", "할증률": 1},
{"재료별": "래디믹스트 콘크리트 타설(현장플랜트포함)", "세부": "철골구조물", "할증률": 1},
{"재료별": "현장 혼합 콘크리트 타설(인력 및 믹서)", "세부": "무근구조물", "할증률": 3},
{"재료별": "현장 혼합 콘크리트 타설(인력 및 믹서)", "세부": "철근구조물", "할증률": 2},
{"재료별": "현장 혼합 콘크리트 타설(인력 및 믹서)", "세부": "소형구조물", "할증률": 5},
{"재료별": "콘크리트포장혼합물의 포설", "세부": "", "할증률": 4},
{"재료별": "아스팔트콘크리트포설(현장플랜트포함)", "세부": "", "할증률": 2},
{"재료별": "졸대", "세부": "", "할증률": 20},
{"재료별": "텍스", "세부": "", "할증률": 5},
{"재료별": "석고판(못붙임용)", "세부": "", "할증률": 5},
{"재료별": "석고판(본드붙임용)", "세부": "", "할증률": 8},
{"재료별": "콜크판", "세부": "", "할증률": 5},
{"재료별": "단열재", "세부": "", "할증률": 10},
{"재료별": "유리", "세부": "", "할증률": 1},
{"재료별": "테라콧타", "세부": "", "할증률": 3},
{"재료별": "블록", "세부": "", "할증률": 4},
{"재료별": "기와", "세부": "", "할증률": 5},
{"재료별": "슬레이트", "세부": "", "할증률": 3},
{"재료별": "타일", "세부": "모자이크", "할증률": 3},
{"재료별": "타일", "세부": "도기", "할증률": 3},
{"재료별": "타일", "세부": "자기", "할증률": 3},
{"재료별": "타일", "세부": "아스팔트", "할증률": 5},
{"재료별": "타일", "세부": "리노륨", "할증률": 5},
{"재료별": "타일", "세부": "비닐", "할증률": 5},
{"재료별": "타일", "세부": "비닐렉스", "할증률": 5},
{"재료별": "타일", "세부": "크링카", "할증률": 3},
{"재료별": "테라죠판", "세부": "", "할증률": 6, "비고": "'18 신설"},
{"재료별": "위생기구(도기, 자기류)", "세부": "", "할증률": 2}
],
"주": ["① 거푸집 및 동바리, 가건축물 또는 품셈에 할증률이 포함 또는 표시되어 있는 것에 대하여는 본 할증률을 적용하지 아니한다.", "② 개별 부재의 설계조건에 의해 제작이 완료된 상태의 PC부재(PC암거, 건축용 구조부재 등)는 할증수량을 적용하지 않는다.", "③ PVC, PE관의 할증률(%)은 옥외공사 기준이며 옥내공사용 재료의 할증률은 10% 이내로 한다.", "④ 현장 여건상 절단 및 가공 등이 불필요한 경우, 상기 할증률을 조정하여 적용할 수 있다."]
},
{
"열쇠": "1-4-3 현장조건",
"이름": "작업지연 — 현장조건",
"출처": "건설품셈 공통 1-4-3 1",
"조건": {"구분": "고르기"},
"값칸": {"할증률": "%", "적용조건": "글"},
"줄": [
{"구분": "통제보안지역", "할증률": 20, "적용조건": "- 보안구역 등 작업인력의 출입통제로 작업에 지장을 받는 경우"},
{"구분": "군(軍)통제지역", "할증률": 50, "적용조건": "- 인근 사격훈련 등 군(軍) 관련 지역 내 출입통제 등으로 작업에 지장을 받는 경우"},
{"구분": "도서지역", "할증률": 50, "적용조건": "- 본토와 도서지구간 인력의 이동(출퇴근) 발생으로 작업에 지장을 받는 경우"},
{"구분": "공항지역", "할증률": 50, "적용조건": "- 공항 내 이착륙(1일 20회 이상)발생으로 작업에 지장을 받는 경우"}
],
"주": ["① 본 할증은 인력의 출입 및 작업 통제에 의해 실 작업시간이 줄어드는 경우에 적용한다.", "② 도서지역에서 자원(인력, 자재, 건설기계)의 수급에 영향을 받는 경우는 본 할증과 무관하며, 별도 반영하여야 한다."]
},
{
"열쇠": "1-4-3 열차의 운행빈도",
"이름": "작업지연 — 열차의 운행빈도",
"출처": "건설품셈 공통 1-4-3 2",
"조건": {"구분": "고르기", "열차운행횟수(8시간)": "범위"},
"값칸": {"할증률": "%"},
"범위규칙": "열차운행횟수(회) 이상·이하 — 「13회 이하」 = [null, 13] · 「19회 이상」 = [19, null]",
"줄": [
{"구분": "본선상작업", "열차운행횟수(8시간)": [null, 13], "할증률": 14},
{"구분": "본선상작업", "열차운행횟수(8시간)": [14, 18], "할증률": 25},
{"구분": "본선상작업", "열차운행횟수(8시간)": [19, null], "할증률": 37},
{"구분": "열차운행선 인접작업", "열차운행횟수(8시간)": [null, 13], "할증률": 3},
{"구분": "열차운행선 인접작업", "열차운행횟수(8시간)": [14, 18], "할증률": 5},
{"구분": "열차운행선 인접작업", "열차운행횟수(8시간)": [19, null], "할증률": 7}
],
"주": ["① 열차 통과에 따라 작업이 중단(지장 또는 대피)되는 경우에 적용한다.", "② 열차운행선 인접공사시 열차통과에 따라 작업이 중단되어 작업능률이 저하되는 경우 대피 할증율을 적용하며, 선로와의 이격거리는 철도안전법 기준을 적용한다."]
},
{
"열쇠": "1-4-3 건물 층수",
"이름": "작업지연 — 건물 층수",
"출처": "건설품셈 공통 1-4-3 3",
"조건": {"구분": "고르기", "층": "범위"},
"값칸": {"할증률": "%", "가산단위": "층", "가산할증": "%", "원문": "글"},
"범위규칙": "지상층 — 줄 순서대로 처음 맞는 줄(「10층 이하」 = [null, 10]) · 「30층 초과」 = (30, null) · 지하층 — 「지하 2층 초과」 = (2, null)",
"줄": [
{"구분": "지상층", "층": [2, 5], "할증률": 1},
{"구분": "지상층", "층": [null, 10], "할증률": 3},
{"구분": "지상층", "층": [null, 15], "할증률": 4},
{"구분": "지상층", "층": [null, 20], "할증률": 5},
{"구분": "지상층", "층": [null, 25], "할증률": 6},
{"구분": "지상층", "층": [null, 30], "할증률": 7},
{"구분": "지상층", "층": [30, null], "가산단위": 5, "가산할증": 1, "원문": "5층마다 1%씩 가산"},
{"구분": "지하층", "층": [1, 1], "할증률": 1},
{"구분": "지하층", "층": [2, 2], "할증률": 2},
{"구분": "지하층", "층": [2, null], "가산단위": 1, "가산할증": 1, "원문": "1층마다 1%씩 가산"}
],
"주": ["① 시설(건물 등) 내부에서 작업자의 이동에 따라 작업능률이 저하되는 경우에 적용한다.", "② 층의 구분을 할 수 없는 경우 층고를 3.6m로 기준하여 환산한다."]
},
{
"열쇠": "1-4-3 층고 환산",
"이름": "건물 층수 — 층 구분 불가 시 환산 층고",
"출처": "건설품셈 공통 1-4-3 3 주②",
"조건": {},
"값칸": {"층고": "m"},
"줄": [
{"층고": 3.6}
]
},
{
"열쇠": "1-4-4 지세",
"이름": "지세/지형 — 지세",
"출처": "건설품셈 공통 1-4-4 1",
"조건": {"구분": "고르기", "등급": "고르기"},
"값칸": {"할증률": "%", "적용조건": "글"},
"줄": [
{"구분": "산지", "등급": "산지 A", "할증률": 15, "적용조건": "- ‘산지의 등급 구분’ 참조"},
{"구분": "산지", "등급": "산지 B", "할증률": 25, "적용조건": "- ‘산지의 등급 구분’ 참조"},
{"구분": "산지", "등급": "산지 C", "할증률": 50, "적용조건": "- ‘산지의 등급 구분’ 참조"},
{"구분": "경사지", "등급": "경사지 A", "할증률": 10, "적용조건": "- 비탈면 등 경사면 작업으로 작업에 지장을 받는 경우"},
{"구분": "경사지", "등급": "경사지 B", "할증률": 20, "적용조건": "- ‘경사지의 등급 구분’ 참조"},
{"구분": "습지/해안지", "등급": "", "할증률": 20, "적용조건": "- 습지(물이 있는 논 등) 또는 해안지역(갯벌, 간척지, 모래사장 등)에서 직접 작업하는 경우"}
],
"주": ["① 시공위치의 형상 변화(간섭, 경사 등)로 인해 작업에 지장을 받는 경우에 적용한다.", "② 작업조건의 개선(지형 평탄화, 탑승장비 활용 등)으로 본 작업의 영향을 받지 않는 경우 적용하지 않는다."]
},
{
"열쇠": "1-4-4 산지의 등급 구분",
"이름": "지세/지형 — 산지의 등급 구분",
"출처": "건설품셈 공통 1-4-4 1 주③",
"조건": {"구분": "고르기"},
"값칸": {"적용대상": "글"},
"줄": [
{"구분": "산지 A", "적용대상": "-국도 주변 야산지\n-지방도 주변 야산지\n-시가지 주변 야산지\n-마을 주변 야산지"},
{"구분": "산지 B", "적용대상": "-순수 야산지\n-해안 야산지"},
{"구분": "산지 C", "적용대상": "- 산악지"}
]
},
{
"열쇠": "1-4-4 경사지의 등급 구분",
"이름": "지세/지형 — 경사지의 등급 구분",
"출처": "건설품셈 공통 1-4-4 1 주④",
"조건": {"수평각": "범위"},
"값칸": {"등급": "글"},
"범위규칙": "수평각(도) 이상·미만 — 「15도 ~ 30도 미만」 = [15, 30] · 「30도 이상」 = [30, null]",
"줄": [
{"수평각": [15, 30], "등급": "경사지 A"},
{"수평각": [30, null], "등급": "경사지 B"}
]
},
{
"열쇠": "1-4-4 지세구분",
"이름": "지세/지형 — [참고] 지세구분",
"출처": "건설품셈 공통 1-4-4 1 참고",
"조건": {"구분": "고르기", "세부": "고르기"},
"값칸": {"평탄지": "글", "산지 A": "글", "산지 B": "글", "산지 C": "글"},
"줄": [
{"구분": "지형", "세부": "", "평탄지": "평지 또는 보통 야산으로 교통이 편리한 곳", "산지 A": "험한 야산지대 및 수목이 우거진 보통 산악지대", "산지 B": "험한 야산지대 및 수목이 우거진 보통 산악지대로서 교통이 불편한 곳", "산지 C": "산림이 우거진 험준한 산악지대로서 교통이 극히 불편한 곳"},
{"구분": "지세", "세부": "", "평탄지": "평지 또는 보통 야산", "산지 A": "험한 야산 또는 보통 산악", "산지 B": "험한 야산 또는 보통 산악", "산지 C": "험한 산악"},
{"구분": "높이 기준", "세부": "해발", "평탄지": "100m 미만", "산지 A": "300m 미만", "산지 B": "300m 미만", "산지 C": "400m 미만"},
{"구분": "높이 기준", "세부": "표고", "평탄지": "50m 미만", "산지 A": "150m 미만", "산지 B": "150m 미만", "산지 C": "200m 미만"},
{"구분": "통행 조건", "세부": "도로", "평탄지": "대소로(유)", "산지 A": "대소로(유)", "산지 B": "대로(무)", "산지 C": "대소로(무)"},
{"구분": "통행 조건", "세부": "구배", "평탄지": "완만", "산지 A": "완만", "산지 B": "완급", "산지 C": "극급"},
{"구분": "통행 조건", "세부": "통행", "평탄지": "양호", "산지 A": "양호", "산지 B": "불편", "산지 C": "극히불량"},
{"구분": "자연 환경", "세부": "지세", "평탄지": "양호", "산지 A": "불편", "산지 B": "불편", "산지 C": "불량"},
{"구분": "자연 환경", "세부": "수목", "평탄지": "소수 또는 소목", "산지 A": "보통 또는 약간울창", "산지 B": "보통 또는 약간울창", "산지 C": "울창"},
{"구분": "자연 환경", "세부": "기상", "평탄지": "보통", "산지 A": "불편", "산지 B": "불편", "산지 C": "불편"},
{"구분": "기타 조건", "세부": "교통", "평탄지": "도로에서 500m 이내", "산지 A": "도로에서 500m 이내", "산지 B": "도로에서 1㎞ 이내", "산지 C": "도로에서 1㎞ 이상"},
{"구분": "기타 조건", "세부": "숙소", "평탄지": "편리", "산지 A": "편리", "산지 B": "불편", "산지 C": "극히 불편"},
{"구분": "기타 조건", "세부": "통신", "평탄지": "편리", "산지 A": "편리", "산지 B": "불편", "산지 C": "불가"},
{"구분": "기타 조건", "세부": "인력동원", "평탄지": "편리", "산지 A": "편리", "산지 B": "불편", "산지 C": "불가"}
],
"주": ["① 교통\n- 도로 : 도시․군계획시설의 결정․구조 및 설치기준에 관한 규칙 제9조 참고\n- 편리 : 대형차의 통행가능\n- 불편 : 소형차 또는 리어카 정도의 통행가능\n- 극히불편 : 사람 이외의 통행불가", "② 표고 : 활동 중심구역에서의 거리 300m 기준", "③ 구배\n- 완만 : 사거리 100m 미만으로 수평각 15도 미만 정도\n- 완급 : 사거리 100m 이상의 수평각 30도 미만 정도\n- 극급 : 사거리 100m 이상으로 수평각 30도 이상 정도", "④ 선정기준 : 상기 구분기준 중 4개 이상에 해당되는 경우를 대상으로 함"]
},
{
"열쇠": "1-4-4 지세구분 표고 기준거리",
"이름": "지세구분 — 표고 기준 활동 중심구역 거리",
"출처": "건설품셈 공통 1-4-4 1 참고 주②",
"조건": {},
"값칸": {"거리": "m"},
"줄": [
{"거리": 300}
]
},
{
"열쇠": "1-4-4 지세구분 구배",
"이름": "지세구분 — 구배 구분",
"출처": "건설품셈 공통 1-4-4 1 참고 주③",
"조건": {"구배": "고르기"},
"값칸": {"사거리": "글", "수평각": "글"},
"줄": [
{"구배": "완만", "사거리": "100m 미만", "수평각": "15도 미만 정도"},
{"구배": "완급", "사거리": "100m 이상", "수평각": "30도 미만 정도"},
{"구배": "극급", "사거리": "100m 이상", "수평각": "30도 이상 정도"}
]
},
{
"열쇠": "1-4-4 지세구분 선정기준",
"이름": "지세구분 — 선정기준(구분기준 중 해당 개수)",
"출처": "건설품셈 공통 1-4-4 1 참고 주④",
"조건": {},
"값칸": {"해당 개수 이상": "개"},
"줄": [
{"해당 개수 이상": 4}
]
},
{
"열쇠": "1-4-4 도심지",
"이름": "지세/지형 — 도심지",
"출처": "건설품셈 공통 1-4-4 2",
"조건": {"구분": "고르기", "세부": "고르기"},
"값칸": {"할증률": "%", "적용조건": "글"},
"줄": [
{"구분": "도로점유 차도공사", "세부": "차도 A(2차로)", "할증률": 30, "적용조건": "- 교행불가 발생으로 인해 작업에 영향을 받는 경우"},
{"구분": "도로점유 차도공사", "세부": "차도 B(4차로 이하)", "할증률": 25, "적용조건": "- 통행제한 또는 저속통행으로 인해 작업에 영향을 받는 경우"},
{"구분": "도로점유 차도공사", "세부": "차도 C(4차로 초과)", "할증률": 20, "적용조건": "- 교통량 과다로 인한 차량통행에 영향을 받는 경우"},
{"구분": "주거지 및 상업지공사", "세부": "보행자 및 차량통행", "할증률": 15, "적용조건": "- 보행자 또는 차량통행으로 인해 작업에 영향을 받는 경우"},
{"구분": "주거지 및 상업지공사", "세부": "주거환경영향", "할증률": 15, "적용조건": "- 주변환경 영향으로 인해 작업에 영향을 받는 경우"},
{"구분": "주거지 및 상업지공사", "세부": "현장협소", "할증률": 15, "적용조건": "- 현장내 자재 적치 또는 장비의 설치/운전이 어려운 경우"},
{"구분": "주거지 및 상업지공사", "세부": "지하매설물", "할증률": 15, "적용조건": "- 지하매설물의 간섭으로 인해 작업에 영향을 받는 경우"},
{"구분": "지하/지반 공사", "세부": "고층/초고층 건축물", "할증률": 10, "적용조건": "- 장시간 연속타설이 필요한 기초공사 등\n- 초대형 장비(대구경 천공기/대형 크레인 등)의 설치/운전이 어려운 경우"},
{"구분": "지하/지반 공사", "세부": "대심도굴착 A", "할증률": 20, "적용조건": "- 대심도 수직구 굴착공사에서 도심지 작업으로 인해 작업에 영향을 받는 경우(자재반입, 버력반출 제약 등)\n- 수직구 깊이 40m 60m 이하"},
{"구분": "지하/지반 공사", "세부": "대심도굴착 B", "할증률": 30, "적용조건": "- 대심도 수직구 굴착공사에서 도심지 작업으로 인해 작업에 영향을 받는 경우(자재반입, 버력반출 제약 등)\n- 수직구 깊이 60m 초과"}
],
"주": ["① 도로점유 차도 공사는 도로를 점유하여 작업하는 공종을 기준으로 한다.", "② 주거 및 상업지 공사는 ‘국토의 계획 및 이용에 관한 법률’에 따른 주거지역 및 상업지역과 공업지역 중 준공업지역을 기준으로 하며, 그 밖의 지역에서 시공환경이 유사한 경우 이를 준용하여 적용할 수 있다.", "③ 지하/지반 공사는 도시지역 내 현장부지가 협소하거나 보행자 및 차량통행 등으로 현장진입이 원활하지 않은 경우에 적용한다.", "④ 고층 및 초고층 건축물의 구분은 ‘건축법’ 및 ‘건축법 시행령’을 기준으로 한다."]
},
{
"열쇠": "1-4-5 고소작업",
"이름": "위험 — 고소작업",
"출처": "건설품셈 공통 1-4-5 1",
"조건": {"구분": "고르기", "높이": "범위"},
"값칸": {"할증률": "%", "가산단위": "m", "가산할증": "%", "원문": "글"},
"범위규칙": "높이(m) 이상·미만 — 「10m 이상 ~ 20m 미만」 = [10, 20] · 「60m 초과」 = (60, null) · 「10m 미만」 줄은 원문 「-」 라 뺌",
"줄": [
{"구분": "비계사용", "높이": [10, 20], "할증률": 5},
{"구분": "비계사용", "높이": [20, 30], "할증률": 8},
{"구분": "비계사용", "높이": [30, 40], "할증률": 12},
{"구분": "비계사용", "높이": [40, 50], "할증률": 16},
{"구분": "비계사용", "높이": [50, 60], "할증률": 20},
{"구분": "비계사용", "높이": [60, null], "가산단위": 10, "가산할증": 4, "원문": "10m마다 4%씩 가산"},
{"구분": "고소작업차 사용", "높이": [10, 20], "할증률": 4},
{"구분": "고소작업차 사용", "높이": [20, 30], "할증률": 6},
{"구분": "고소작업차 사용", "높이": [30, 40], "할증률": 8},
{"구분": "고소작업차 사용", "높이": [40, 50], "할증률": 10},
{"구분": "고소작업차 사용", "높이": [50, 60], "할증률": 12},
{"구분": "고소작업차 사용", "높이": [60, null], "가산단위": 10, "가산할증": 2, "원문": "10m마다 2%씩 가산"}
],
"주": ["① 비계 사용은 기설치 된 비계(강관비계, 시스템비계 등)위에서 작업하는 기준이며, 고소작업차 사용은 고소작업차에 탑승하여 작업하는 기준이다.", "② 굴착 등 지하에서 작업할 경우 본 표의 높이별 할증율을 동일하게 적용하며 비계 또는 고소작업차의 설치 위치를 기준으로 한다.", "③ 특수 조건의 고소작업(비계틀 불사용 등)은 별도 계상한다."]
},
{
"열쇠": "1-4-5 교량상 작업",
"이름": "위험 — 교량상 작업",
"출처": "건설품셈 공통 1-4-5 2",
"조건": {"구분": "고르기"},
"값칸": {"할증률": "%", "적용조건": "글"},
"줄": [
{"구분": "슬래브(도상)위", "할증률": 15, "적용조건": "- 작업자의 추락 위험이 비교적 낮은 작업"},
{"구분": "무도상 교량/난간 설치 및 철거", "할증률": 30, "적용조건": "- 작업자의 추락 위험이 높은 작업"}
],
"주": ["교량상 작업은 교량위에서 작업자의 안전시설(안전로프 등) 착용이 필요한 작업 기준이다."]
},
{
"열쇠": "1-4-5 터널내 작업",
"이름": "위험 — 터널내 작업",
"출처": "건설품셈 공통 1-4-5 3",
"조건": {"구분": "고르기"},
"값칸": {"할증률": "%", "적용조건": "글"},
"줄": [
{"구분": "도로/보행터널", "할증률": 15, "적용조건": "- 작업자의 대피가 용이한 터널"},
{"구분": "철도터널", "할증률": 30, "적용조건": "- 작업자의 대피거리가 길고, 별도의 대피공간이 필요한 터널"}
],
"주": ["터널내 작업은 완공되어 운영 중인 터널의 입구에서 25m이상 진입하여 보수 및 보강, 유지보수 등의 작업 시에 적용한다.", "비고 : 터널내 사다리작업으로 작업능률이 현저하게 저하될 시는 위 할증율에 10%까지 가산할 수 있다."]
},
{
"열쇠": "1-4-5 터널내 작업 진입거리",
"이름": "터널내 작업 — 적용 최소 진입거리(입구에서)",
"출처": "건설품셈 공통 1-4-5 3 주",
"조건": {},
"값칸": {"진입거리": "m 이상"},
"줄": [
{"진입거리": 25}
]
},
{
"열쇠": "1-4-5 터널내 작업 사다리 가산",
"이름": "터널내 작업 — 사다리작업 가산 상한",
"출처": "건설품셈 공통 1-4-5 3 비고",
"조건": {},
"값칸": {"가산 상한": "%"},
"줄": [
{"가산 상한": 10}
]
},
{
"열쇠": "1-4-5 유해 작업",
"이름": "위험 — 유해 작업",
"출처": "건설품셈 공통 1-4-5 4",
"조건": {"구분": "고르기"},
"값칸": {"할증률": "%", "적용조건": "글"},
"줄": [
{"구분": "활선근접", "할증률": 30, "적용조건": "- 고온·고압기기 접근작업 [참고] AC140㎸급 이상(4m이내), 60㎸급 이상(3m이내), 7㎸급 이상(2m이내), 600V이상(1m이내)"},
{"구분": "기타", "할증률": 20, "적용조건": "- 고열·위험물·극독물의 보관실내 작업"},
{"구분": "기타", "할증률": 10, "적용조건": "- 정화조, 축전지실, 제방실내 등 유해가스 발생장소"}
],
"주": ["유해작업은 유해시설과 인접하여 작업하는 경우에 적용한다."]
},
{
"열쇠": "1-4-5 활선근접 이격거리",
"이름": "유해 작업 — 활선근접 접근 이격거리",
"출처": "건설품셈 공통 1-4-5 4 참고",
"조건": {"전압": "고르기"},
"값칸": {"이격거리": "m 이내"},
"줄": [
{"전압": "AC140㎸급 이상", "이격거리": 4},
{"전압": "60㎸급 이상", "이격거리": 3},
{"전압": "7㎸급 이상", "이격거리": 2},
{"전압": "600V이상", "이격거리": 1}
]
},
{
"열쇠": "1-4-6 작업시간 제한",
"이름": "작업제한 — 작업시간 제한",
"기준": "1일 8시간",
"출처": "건설품셈 공통 1-4-6 1",
"조건": {"작업가능시간": "수"},
"값칸": {"할증률": "%"},
"범위규칙": "작업가능시간(시간) 이하 — 「3시간 〃」 = 3시간 이하",
"줄": [
{"작업가능시간": 2, "할증률": 50},
{"작업가능시간": 3, "할증률": 35},
{"작업가능시간": 4, "할증률": 25},
{"작업가능시간": 5, "할증률": 20},
{"작업가능시간": 6, "할증률": 15}
],
"주": ["① 휴전, 단수, 선로사용중지 등 일일 작업시간이 제한되는 경우에 적용한다.", "② 작업가능시간은 작업준비, 대기 등을 제외한 실질적인 시공위치의 점유가 가능한 시간이다."]
},
{
"열쇠": "1-4-6 폭염시 휴게시간 할증",
"이름": "작업제한 — 폭염시 휴게시간 할증",
"출처": "건설품셈 공통 1-4-6 2",
"조건": {"체감온도": "범위", "작업시간": "수"},
"값칸": {"할증률": "%"},
"범위규칙": "체감온도(도) 이상 · 작업시간(시간) 이상 — 「4시간 〃」 = 4시간 이상일 경우 작업시",
"줄": [
{"체감온도": [33, null], "작업시간": 2, "할증률": 5},
{"체감온도": [33, null], "작업시간": 4, "할증률": 10},
{"체감온도": [33, null], "작업시간": 6, "할증률": 15},
{"체감온도": [33, null], "작업시간": 8, "할증률": 20}
],
"주": ["산업안전보건기준에 관한 규칙에 따른 휴식시간을 부여할 경우를 적용한다."]
},
{
"열쇠": "1-4-7 야간",
"이름": "작업환경 — 야간",
"출처": "건설품셈 공통 1-4-7 1",
"조건": {"구분": "고르기"},
"값칸": {"할증률": "%", "적용조건": "글"},
"줄": [
{"구분": "야간", "할증률": 25, "적용조건": "- 정상작업시간에 추가하여 야간공사 수행(돌관공사)\n- 공사성격에 따라 야간작업으로 계획"}
],
"주": ["공정계획에 의해 정상작업(정상공기)에 의한 작업이 불가능한 경우 또는 공사성격 상 야간작업을 수행하는 경우에 적용한다."]
},
{
"열쇠": "1-4-7 특수작업",
"이름": "작업환경 — 특수작업",
"출처": "건설품셈 공통 1-4-7 2",
"조건": {"구분": "고르기"},
"값칸": {"할증률": "%", "적용조건": "글"},
"줄": [
{"구분": "특수작업", "할증률": "5~10", "적용조건": "- 중요기기 및 설비의 분해, 가공 또는 조립작업\n- 특별한 사양 및 공법에 의한 작업\n- 기타 중요한 기기 및 설비를 취급하는 작업"}
],
"주": ["비고 : 원자력 발전소와 같이 작업단계별 품질 및 안전도 검사 등이 엄격히 적용되는 공정의 경우에는 각 공정에 따라 품 할증을 별도 가산한다.", "작업의 중요도가 높거나 특별 시방에 따라 특수한 기술과 안전관리가 필요한 작업(원자력 발전소 등)에 적용한다."]
},
{
"열쇠": "1-4-7 기타",
"이름": "작업환경 — 기타",
"출처": "건설품셈 공통 1-4-7 3",
"조건": {"구분": "고르기"},
"값칸": {"할증률": "%", "적용조건": "글"},
"줄": [
{"구분": "기타", "할증률": 50, "적용조건": "- 작업공간의 협소(작업간섭)\n- 동일장소에서 수종의 장비가동\n- 소음·진동 발생\n- 위험 발생"},
{"구분": "기타", "할증률": 50, "적용조건": "- 원거리, 계속이동작업, 분산작업 등 이동시간 과다발생"}
],
"주": ["① 현장 조건에 따라 작업능력 저하가 발생하는 경우에 적용한다.", "② 1개 이상의 적용조건이 발생하는 경우 개별 할증을 중복 가산하지 않으며, 현장 전반의 작업환경을 종합적으로 고려하여 할증율을 적용한다.", "③ 이동으로 인한 작업시간 손실이 1시간 이내의 경우는 할증을 적용하지 않는다.", "④ 작업환경에 따라 작업시간 감소가 예상되는 경우 ‘1-4-6 작업제한/작업시간제한’ 할증율 참고하여 적용한다."]
},
{
"열쇠": "1-4-7 이동시간 손실 무할증",
"이름": "작업환경 기타 — 이동 작업시간 손실 무할증 한도",
"출처": "건설품셈 공통 1-4-7 3 주③",
"조건": {},
"값칸": {"이동 작업시간 손실": "시간 이내"},
"줄": [
{"이동 작업시간 손실": 1}
]
}
]
}
@@ -0,0 +1,505 @@
{
"그룹": "계수",
"원문": "산림품셈",
"판": "산림청고시 제2025-82호",
"줄": [
{
"열쇠": "1-3-1 콘크리트 및 포장용 재료",
"이름": "재료 할증 — 콘크리트 및 포장용 재료",
"출처": "산림품셈 1-3-1 2",
"조건": {"종류": "고르기"},
"값칸": {"정치식": "%", "기타": "%"},
"줄": [
{"종류": "시멘트", "정치식": 2, "기타": 3},
{"종류": "잔골재ㆍ채움재", "정치식": 10, "기타": 12},
{"종류": "굵은골재", "정치식": 3, "기타": 5},
{"종류": "아스팔트", "정치식": 2, "기타": 3},
{"종류": "석분", "정치식": 2, "기타": 3},
{"종류": "혼화재", "정치식": 2}
],
"주": ["속채움 재료의 경우에도 이 값을 적용한다."]
},
{
"열쇠": "1-3-1 노상 및 노반재료",
"이름": "재료 할증 — 노상 및 노반재료(선택층, 보조기층, 기층 등)",
"출처": "산림품셈 1-3-1 3",
"조건": {"종류": "고르기"},
"값칸": {"할증률": "%"},
"줄": [
{"종류": "모래", "할증률": 6},
{"종류": "부순돌ㆍ자갈ㆍ막자갈", "할증률": 4},
{"종류": "석분", "할증률": 0},
{"종류": "점질토", "할증률": 6}
]
},
{
"열쇠": "1-3-1 관류 및 구조물 기초 부설재료",
"이름": "재료 할증 — 관류 및 구조물 기초 부설재료",
"출처": "산림품셈 1-3-1 4",
"조건": {"종류": "고르기"},
"값칸": {"할증률": "%"},
"줄": [
{"종류": "모래", "할증률": 4}
]
},
{
"열쇠": "1-3-1 강재류",
"이름": "재료 할증 — 강재류",
"출처": "산림품셈 1-3-1 5",
"조건": {"종류": "고르기"},
"값칸": {"할증률": "%"},
"줄": [
{"종류": "원형철근", "할증률": 5},
{"종류": "이형철근", "할증률": 3},
{"종류": "이형철근(교량·지하철 및 이와 유사한 복잡한 구조물의 주철근)", "할증률": "6-7"},
{"종류": "강판", "할증률": 10},
{"종류": "강관(옥외 수도용 강관 제외)", "할증률": 5},
{"종류": "대형형강 (形鋼)", "할증률": 7},
{"종류": "소형형강", "할증률": 5},
{"종류": "봉강 (棒鋼)", "할증률": 5},
{"종류": "평강대강", "할증률": 5},
{"종류": "경량형 강각(角) 파이프", "할증률": 5},
{"종류": "리벳 (제품)", "할증률": 5}
],
"주": ["이형철근의 경우, 해당 공사 또는 구조물의 시공실적에 따라 조정하여 적용할 수 있다."]
},
{
"열쇠": "1-3-1 기타재료",
"이름": "재료 할증 — 기타재료",
"출처": "산림품셈 1-3-1 6",
"조건": {"재료별": "고르기", "세부": "고르기"},
"값칸": {"할증률": "%"},
"줄": [
{"재료별": "목재", "세부": "각재", "할증률": 5},
{"재료별": "목재", "세부": "판재", "할증률": 10},
{"재료별": "합판", "세부": "일반용 합판", "할증률": 3},
{"재료별": "합판", "세부": "수장용 합판", "할증률": 5},
{"재료별": "조립식구조물(U형플륨관 등)", "세부": "", "할증률": 3},
{"재료별": "레디믹스트콘크리트타설(현장 플랜트 포함)", "세부": "무근 구조물", "할증률": 2},
{"재료별": "레디믹스트콘크리트타설(현장 플랜트 포함)", "세부": "철근 구조물", "할증률": 1},
{"재료별": "레디믹스트콘크리트타설(현장 플랜트 포함)", "세부": "철골 구조물", "할증률": 1},
{"재료별": "원석 (마름돌용)", "세부": "", "할증률": 30},
{"재료별": "사방용 수목", "세부": "", "할증률": 10},
{"재료별": "떼 및 초화류", "세부": "", "할증률": 10},
{"재료별": "현장콘크리트 타설(인력 및 믹서)", "세부": "무근구조물", "할증률": 3},
{"재료별": "현장콘크리트 타설(인력 및 믹서)", "세부": "철근구조물", "할증률": 2},
{"재료별": "현장콘크리트 타설(인력 및 믹서)", "세부": "소형구조물", "할증률": 5},
{"재료별": "아스팔트콘크리트 포설(현장플랜트 포함)", "세부": "", "할증률": 2},
{"재료별": "콘크리트포장 포설", "세부": "", "할증률": 4},
{"재료별": "원심력철근콘크리트관", "세부": "", "할증률": 3}
]
},
{
"열쇠": "1-3-2 작업반장 배치",
"이름": "노임 할증 — 작업반장 배치(산지사방, 야계사방, 해안사방)",
"출처": "산림품셈 1-3-2 2",
"조건": {},
"값칸": {"작업반장": "인", "기준인원": "인"},
"줄": [
{"작업반장": 1, "기준인원": 20}
]
},
{
"열쇠": "1-4-1 작업시기",
"이름": "품의 할증 — 작업시기",
"출처": "산림품셈 1-4-1",
"조건": {"작업시기": "범위"},
"값칸": {"할증률": "%"},
"범위규칙": "작업시기(월) 이상·이하 — 「6월∼9월」 = [6, 9]",
"줄": [
{"작업시기": [6, 9], "할증률": 10},
{"작업시기": [10, 12], "할증률": 5},
{"작업시기": [1, 5], "할증률": 0}
],
"주": ["① 계절에 따라 덩굴 및 뿌리제거 작업조건이 달라지므로 작업시기에 따라 할증을 적용한다.", "② 어린나무가꾸기에 한하여 작업시기가 6~9월인 경우 10% 할증하고, 이외의 작업시기에 대해서는 0%로 적용한다."]
},
{
"열쇠": "1-4-2 조림 후 경과연수",
"이름": "품의 할증 — 조림 후 경과연수",
"출처": "산림품셈 1-4-2",
"조건": {"조림 후 경과연수": "고르기"},
"값칸": {"할증률": "%"},
"줄": [
{"조림 후 경과연수": "3년차 이상", "할증률": 10},
{"조림 후 경과연수": "2년차", "할증률": 5},
{"조림 후 경과연수": "당해 연도(전년도 추기조림 포함)", "할증률": 0}
],
"주": ["줄베기 작업 시 조림 경과연도에 따라 발생하는 식생량이 증가하여 작업조건이 어려워지므로 조림 후 경과연수에 따라 할증 적용한다. 다만, 연 2회차 작업 시는 경과연수 할증률을 적용하지 않는다."]
},
{
"열쇠": "1-4-3 집단화정도",
"이름": "품의 할증 — 집단화정도",
"출처": "산림품셈 1-4-3",
"조건": {"개소당 평균면적": "범위"},
"값칸": {"할증률": "%"},
"범위규칙": "개소당 평균면적(ha) 이상·미만 — 「1ha 미만」 = [null, 1] · 「3ha 이상」 = [3, null]",
"줄": [
{"개소당 평균면적": [null, 1], "할증률": 10},
{"개소당 평균면적": [1, 3], "할증률": 5},
{"개소당 평균면적": [3, null], "할증률": 0}
],
"주": ["① 대상지의 집단화 정도에 따라 이동거리가 증가함으로 할증을 적용한다.", "② 개소는 한 개의 연결된 구역을 기준으로 적용한다.", "③ 집단화 정도는 소반별로 적용한다. 즉, 소반별 사업면적 ÷ 개소수로 산출한다.", "④ 소반 내 사업지와 사업지 간격이 50m 이내의 경우에는 동일 개소로 적용한다.\n* A, B 사업지 간격은 A구역 경계에서 B구역 경계까지 거리를 말함\n- 예) 소반면적 20ha, 10개소일 경우, 20ha÷10개소 = 2.0ha/개소로 +5% 할증 적용"]
},
{
"열쇠": "1-4-3 동일 개소 판정 간격",
"이름": "집단화정도 — 사업지 간격 동일 개소 판정",
"출처": "산림품셈 1-4-3 주④",
"조건": {},
"값칸": {"간격": "m 이내"},
"줄": [
{"간격": 50}
]
},
{
"열쇠": "1-4-4 작업구역",
"이름": "품의 할인·할증 — 작업구역",
"출처": "산림품셈 1-4-4",
"조건": {"구분": "고르기", "기준": "고르기"},
"값칸": {"할인·할증률": "%"},
"줄": [
{"구분": "작업구역", "기준": "개소 평균 1ha 미만", "할인·할증률": 10},
{"구분": "작업구역", "기준": "개소 평균 13ha 미만", "할인·할증률": 5},
{"구분": "작업구역", "기준": "개소 평균 35ha 미만", "할인·할증률": 0},
{"구분": "작업구역", "기준": "개소당 평균 5ha 이상", "할인·할증률": -5},
{"구분": "벌채구역 크기", "기준": "개벌지 크기가 700㎡(또는 벌채폭 30m) 이하", "할인·할증률": 10},
{"구분": "벌채구역 크기", "기준": "개벌지 크기가 3,000㎡(또는 벌채폭 60m) 미만", "할인·할증률": 5},
{"구분": "벌채구역 크기", "기준": "개벌지 크기가 3,000㎡ 이상", "할인·할증률": 0}
],
"주": ["① 작업구역이 소면적 산재할 경우 동일한 이착륙장에서 방재할 수 있는 면적이 감소하므로 할증율을 적용한다.", "② 평균 작업구역 면적은 개소 간 이격거리 50m 이내일 경우 동일 작업구역으로 판단한다."]
},
{
"열쇠": "1-4-4 동일 작업구역 판정 이격거리",
"이름": "작업구역 — 개소 간 이격거리 동일 작업구역 판정",
"출처": "산림품셈 1-4-4 주②",
"조건": {},
"값칸": {"이격거리": "m 이내"},
"줄": [
{"이격거리": 50}
]
},
{
"열쇠": "1-4-5 경사도",
"이름": "품의 할인·할증 — 경사도",
"출처": "산림품셈 1-4-5",
"조건": {"적용기준": "고르기", "경사도": "고르기"},
"값칸": {"할인·할증률": "%"},
"줄": [
{"적용기준": "산지경사", "경사도": "급 (30° 초과)", "할인·할증률": 10},
{"적용기준": "산지경사", "경사도": "중 (1530°)", "할인·할증률": 5},
{"적용기준": "산지경사", "경사도": "완 (15° 미만)", "할인·할증률": 0},
{"적용기준": "평균경사도", "경사도": "평균경사도 50%이상", "할인·할증률": 20},
{"적용기준": "평균경사도", "경사도": "평균경사도 20%50%미만", "할인·할증률": 10},
{"적용기준": "평균경사도", "경사도": "평균경사도 10%20%미만", "할인·할증률": 5},
{"적용기준": "평균경사도", "경사도": "평균경사도 10%미만", "할인·할증률": 0}
],
"주": ["① 산지경사는 표준지를 실측하거나 수치지형도를 이용하여 산출한다.", "② 평균경사도는 드론(무인 헬리콥터)를 활용한 소나무재선충병방제의 약제살포 시 적용한다.\n- 평지의 경우는 가시거리 일부 불량, 완경사지의 경우 사면적의 증가 없이 가시거리가 양호한 것으로 적용\n- 중경사지, 급경사지의 경우 사면적의 증가에 따른 방제면적 증가분을 할증으로 반영"]
},
{
"열쇠": "1-4-6 작업장까지의 이동거리",
"이름": "품의 할증 — 작업장까지의 이동거리",
"출처": "산림품셈 1-4-6",
"조건": {"구분": "고르기", "작업장까지 이동거리": "범위"},
"값칸": {"할증률": "%"},
"범위규칙": "이동거리(㎞) 이상·미만 — 「2.5㎞ 이상」 = [2.5, null] · 「1.3㎞ 미만」 = [null, 1.3]",
"줄": [
{"구분": "도보", "작업장까지 이동거리": [2.5, null], "할증률": 10},
{"구분": "도보", "작업장까지 이동거리": [1.3, 2.5], "할증률": 5},
{"구분": "도보", "작업장까지 이동거리": [null, 1.3], "할증률": 0},
{"구분": "차량", "작업장까지 이동거리": [2.5, null], "할증률": 10},
{"구분": "차량", "작업장까지 이동거리": [1.3, 2.5], "할증률": 5},
{"구분": "차량", "작업장까지 이동거리": [null, 1.3], "할증률": 0}
],
"주": ["작업장까지 차량 이동거리란, 「도로법」상 도로에서 작업장 시작지점까지의 거리로 1/5,000 지형도에서 산출한다."]
},
{
"열쇠": "1-4-7 하층식생",
"이름": "품의 할증 — 하층식생",
"출처": "산림품셈 1-4-7",
"조건": {"하층식생": "고르기"},
"값칸": {"할증률": "%"},
"줄": [
{"하층식생": "식생을 제거하지 않고는 보행이 곤란하다", "할증률": 10},
{"하층식생": "보행하는데 약간의 어려움이 있다", "할증률": 5},
{"하층식생": "보행이 용이하다", "할증률": 0}
]
},
{
"열쇠": "1-4-8 장애물의 정도",
"이름": "품의 할증 — 장애물의 정도",
"출처": "산림품셈 1-4-8",
"조건": {"장애물의 정도": "고르기"},
"값칸": {"할증률": "%"},
"줄": [
{"장애물의 정도": "가슴높이 이상의 초본․관목", "할증률": 10},
{"장애물의 정도": "가슴높이 미만의 초본․관목", "할증률": 5},
{"장애물의 정도": "무릎높이 이하의 초본․관목", "할증률": 0}
],
"주": ["소나무재선충병방제의 장애물의 정도에 따른 할증은 남해안과 도서지역에 한한다."]
},
{
"열쇠": "1-4-9 제거대상 식생",
"이름": "품의 할증 — 제거대상 식생",
"출처": "산림품셈 1-4-9",
"조건": {"제거대상 식생": "고르기"},
"값칸": {"할증률": "%"},
"줄": [
{"제거대상 식생": "어렵다(높이 1.2m 이상이고, 직경이 46cm 이상)", "할증률": 10},
{"제거대상 식생": "보통이다(높이 1.2m 이상이고, 직경이 46cm 미만)", "할증률": 5},
{"제거대상 식생": "쉽다(높이 1.2m 미만이고, 낫으로도 제거가 용이)", "할증률": 0}
],
"주": ["숲가꾸기 및 산림병해충방제 작업로 설치 시 반영한다."]
},
{
"열쇠": "1-4-10 토양상태",
"이름": "품의 할증 — 토양상태",
"출처": "산림품셈 1-4-10",
"조건": {"토양조건": "고르기"},
"값칸": {"할증률": "%"},
"줄": [
{"토양조건": "돌 등 장애물 함량이 30% 이상이고,\n나무뿌리가 밀하게 분포할 경우", "할증률": 10},
{"토양조건": "돌 등 장애물 함량이 10~30%이고,\n나무뿌리가 보통정도로 분포할 경우", "할증률": 5},
{"토양조건": "식혈시 장애물이 거의 없음", "할증률": 0}
],
"주": ["돌이나 나무뿌리 등 장애물에 따라 구덩이 파기 작업조건이 달라지므로 토양조건 할증 적용한다."]
},
{
"열쇠": "1-4-11 덩굴피복도",
"이름": "품의 할인·할증 — 덩굴피복도",
"출처": "산림품셈 1-4-11",
"조건": {"덩굴 피복도": "고르기"},
"값칸": {"할인·할증률": "%"},
"줄": [
{"덩굴 피복도": "과밀(80%초과 피복)", "할인·할증률": 50},
{"덩굴 피복도": "밀(6080%미만 피복)", "할인·할증률": 20},
{"덩굴 피복도": "보통(4060%미만 피복)", "할인·할증률": 0},
{"덩굴 피복도": "소(2040%미만 피복)", "할인·할증률": -20},
{"덩굴 피복도": "과소(20%미만 피복)", "할인·할증률": -50}
],
"주": ["덩굴 피복도는 지표면을 덩굴로 덮고 있는 정도를 육안으로 조사하여 적용한다."]
},
{
"열쇠": "1-4-12 제거대상 피복도",
"이름": "품의 할인·할증 — 제거대상 피복도",
"출처": "산림품셈 1-4-12",
"조건": {"제거대상 피복도": "고르기"},
"값칸": {"할인·할증률": "%"},
"줄": [
{"제거대상 피복도": "대 (임지의 71% 이상 분포)", "할인·할증률": 20},
{"제거대상 피복도": "중 (임지의 41%70% 분포)", "할인·할증률": 0},
{"제거대상 피복도": "소 (임지의 40% 이하 분포)", "할인·할증률": -20}
],
"주": ["① 제거대상 피복도는 조림목에 방해되는 천연발생목의 피복도를 말한다.", "② 제거대상목은 조림목 수고의 70% 이상인 천연발생 교목성 수목을 말한다.", "③ 제거대상 피복도는 제거할 대상이 사업 면적을 덮고 있는 비율로 육안으로 판정한다."]
},
{
"열쇠": "1-4-12 제거대상목 기준 수고",
"이름": "제거대상 피복도 — 제거대상목 기준(조림목 수고 대비)",
"출처": "산림품셈 1-4-12 주②",
"조건": {},
"값칸": {"수고 비율": "% 이상"},
"줄": [
{"수고 비율": 70}
]
},
{
"열쇠": "1-4-13 주행 장애물 상태",
"이름": "품의 할증 — 주행 장애물 상태",
"출처": "산림품셈 1-4-13",
"조건": {"주행 장애물 상태": "고르기"},
"값칸": {"할증률": "%"},
"줄": [
{"주행 장애물 상태": "돌, 도랑, 그루터기 등으로 주행이 매우 힘들다", "할증률": 10},
{"주행 장애물 상태": "돌, 도랑, 그루터기 등으로 주행이 다소 힘들다", "할증률": 5},
{"주행 장애물 상태": "주행에 어려움이 없다", "할증률": 0}
]
},
{
"열쇠": "1-4-14 집재방향",
"이름": "품의 할증 — 집재방향",
"출처": "산림품셈 1-4-14",
"조건": {"구분": "고르기", "집재방향": "고르기", "경사": "고르기"},
"값칸": {"할증률": "%"},
"줄": [
{"구분": "가선을 이용한 기계장비의 원목․생산재 집재", "집재방향": "하향집재", "경사": "", "할증률": 10},
{"구분": "가선을 이용한 기계장비의 원목․생산재 집재", "집재방향": "상향집재", "경사": "", "할증률": 0},
{"구분": "소나무재선충병 인력에 의한 원목 및 재해산물 수집", "집재방향": "상향집재", "경사": "급(30°초과)", "할증률": 10},
{"구분": "소나무재선충병 인력에 의한 원목 및 재해산물 수집", "집재방향": "상향집재", "경사": "중(1530°)", "할증률": 5},
{"구분": "소나무재선충병 인력에 의한 원목 및 재해산물 수집", "집재방향": "상향집재", "경사": "완(15° 미만)", "할증률": 0},
{"구분": "소나무재선충병 방제 훈증더미 제거", "집재방향": "상향집재", "경사": "", "할증률": 10},
{"구분": "소나무재선충병 방제 훈증더미 제거", "집재방향": "수평 또는 하향집재", "경사": "", "할증률": 0}
],
"주": ["소나무재선충병방제 사업의 기계장비를 활용한 산물수집의 할증은 원목․생산재 집재를 적용한다."]
},
{
"열쇠": "1-4-15 횡단운반거리",
"이름": "품의 할증 — 횡단운반거리",
"출처": "산림품셈 1-4-15",
"조건": {"횡단운반거리": "범위"},
"값칸": {"할증률": "%"},
"범위규칙": "횡단운반거리(m) 이상·이하 — 「1120m」 = [11, 20]",
"줄": [
{"횡단운반거리": [11, 20], "할증률": 10},
{"횡단운반거리": [6, 10], "할증률": 5},
{"횡단운반거리": [0, 5], "할증률": 0}
],
"주": ["횡단운반거리는 벌채 및 숲가꾸기사업 기계장비 산물수집의 측방집재거리와 소나무재선충병 방제의 기계장비 산물수집의 가로집재거리와 동일하다."]
},
{
"열쇠": "1-4-16 규격재 생산",
"이름": "품의 할증 — 규격재 생산",
"출처": "산림품셈 1-4-16",
"조건": {"적용 조건": "고르기"},
"값칸": {"할증률": "%"},
"줄": [
{"적용 조건": "규격재 생산을 위한 조재", "할증률": 20}
]
},
{
"열쇠": "1-4-17 방제대상목 분포",
"이름": "품의 할인·할증 — 방제대상목 분포",
"출처": "산림품셈 1-4-17",
"조건": {"구분": "고르기", "대상목 분포": "고르기"},
"값칸": {"할인·할증률": "%"},
"줄": [
{"구분": "산림병해충방제(단목베기, 나무주사)", "대상목 분포": "10본/ha 미만", "할인·할증률": 30},
{"구분": "산림병해충방제(단목베기, 나무주사)", "대상목 분포": "1029본/ha", "할인·할증률": 20},
{"구분": "산림병해충방제(단목베기, 나무주사)", "대상목 분포": "3049본/ha", "할인·할증률": 10},
{"구분": "산림병해충방제(단목베기, 나무주사)", "대상목 분포": "50본/ha 이상", "할인·할증률": 0},
{"구분": "소나무재선충병방제(소각, 매몰, 훈증, 박피)", "대상목 분포": "15본/ha 이하", "할인·할증률": 20},
{"구분": "소나무재선충병방제(소각, 매몰, 훈증, 박피)", "대상목 분포": "15본~25본/ha", "할인·할증률": 10},
{"구분": "소나무재선충병방제(소각, 매몰, 훈증, 박피)", "대상목 분포": "25본~35본/ha", "할인·할증률": 0},
{"구분": "소나무재선충병방제(소각, 매몰, 훈증, 박피)", "대상목 분포": "35본~45본/ha", "할인·할증률": -10},
{"구분": "소나무재선충병방제(소각, 매몰, 훈증, 박피)", "대상목 분포": "45본/ha 이상", "할인·할증률": -20}
]
},
{
"열쇠": "1-4-18 방제대상목 평균 경급",
"이름": "품의 할인·할증 — 방제대상목 평균 경급",
"출처": "산림품셈 1-4-18",
"조건": {"구분": "고르기", "평균경급": "고르기"},
"값칸": {"할인·할증률": "%"},
"줄": [
{"구분": "산림병해충방제(산물수집-기계장비 집재)", "평균경급": "2226cm", "할인·할증률": 10},
{"구분": "산림병해충방제(산물수집-기계장비 집재)", "평균경급": "1620cm", "할인·할증률": 0},
{"구분": "소나무재선충병방제(그물망 피복)", "평균경급": "16cm 이하", "할인·할증률": 10},
{"구분": "소나무재선충병방제(그물망 피복)", "평균경급": "18cm", "할인·할증률": 0},
{"구분": "소나무재선충병방제(그물망 피복)", "평균경급": "20cm 이상", "할인·할증률": -10}
],
"주": ["산림병해충방제(산물수집-기계장비 집재)는 아키야윈치 및 2드럼 케이블 윈치 집재작업에 적용한다."]
},
{
"열쇠": "1-4-19 매개충나무주사",
"이름": "품의 할증 — 매개충나무주사",
"출처": "산림품셈 1-4-19",
"조건": {"구분": "고르기", "내용": "고르기"},
"값칸": {"할증률": "%"},
"줄": [
{"구분": "매개충나무주사", "내용": "매개충나무주사 실행", "할증률": 30}
],
"주": ["소나무재선충병방제의 예방․매개충․합제 나무주사에 적용한다."]
},
{
"열쇠": "1-4-20 방제지 사면",
"이름": "품의 할인·할증 — 방제지 사면",
"출처": "산림품셈 1-4-20",
"조건": {"구분": "고르기", "내용": "고르기"},
"값칸": {"할인·할증률": "%"},
"줄": [
{"구분": "사면형", "내용": "종․횡방향 복합사면(가시거리 매우불량, 고소작업차 이동 필요)", "할인·할증률": 10},
{"구분": "사면형", "내용": "종방향 복합사면(가시거리 불량, 고소작업차 사용 필요)", "할인·할증률": 5},
{"구분": "사면형", "내용": "횡방향 복합사면(가시거리 보통)", "할인·할증률": 0},
{"구분": "사면형", "내용": "단순사면(가시거리 양호)", "할인·할증률": -5}
],
"주": ["① 등고선방향 횡방향 굴곡의 경우 표준사면으로 결정한다.", "② 단순사면의 경우 가시거리가 양호하여 할인을 적용. 산록에서 산정부 방향의 복합사면은 경사도의 변화폭이 20% 이상의 경우를 할증한다."]
},
{
"열쇠": "1-4-20 복합사면 경사도 변화폭",
"이름": "방제지 사면 — 산록에서 산정부 방향 복합사면 할증 기준 경사도 변화폭",
"출처": "산림품셈 1-4-20 주②",
"조건": {},
"값칸": {"경사도 변화폭": "% 이상"},
"줄": [
{"경사도 변화폭": 20}
]
},
{
"열쇠": "1-4-21 방제지 접근성",
"이름": "품의 할증 — 방제지 접근성",
"출처": "산림품셈 1-4-21",
"조건": {"구분": "고르기", "내용": "고르기"},
"값칸": {"할증률": "%"},
"줄": [
{"구분": "접근성", "내용": "이착륙장~방재사업지까지의 이동거리\n200m 이상 거리까지 차량접근 가능", "할증률": 20},
{"구분": "접근성", "내용": "이착륙장~방재사업지까지의 이동거리\n200m 이내 까지 차량접근 가능", "할증률": 10},
{"구분": "접근성", "내용": "이착륙장~방재사업지까지의 이동거리\n100m 이내 까지 차량접근 가능", "할증률": 0}
],
"주": ["① 방제사업지와 이착륙장(차량 도달 가능 장소)이 이격된 경우 할증율을 적용한다.", "② 이격거리는 방제사업지와 이착륙장의 거리를 수평거리로 계상한다.", "③ 작업효율에 영향을 미치는 인자에 대한 할인․할증의 합계 값을 노무비, 기계경비에 반영한다.", "④ 작업구역별(소반) 단가를 산출 후 할인․할증요소를 반영한다."]
},
{
"열쇠": "1-4-22 방제 수종",
"이름": "품의 할증 — 방제 수종",
"출처": "산림품셈 1-4-22",
"조건": {"구분": "고르기", "내용": "고르기"},
"값칸": {"할증률": "%"},
"줄": [
{"구분": "방제 수종", "내용": "잣나무", "할증률": 20},
{"구분": "방제 수종", "내용": "소나무, 해송", "할증률": 0}
]
},
{
"열쇠": "1-4-23 방제목 운반거리",
"이름": "품의 할증 — 방제목 운반거리",
"출처": "산림품셈 1-4-23",
"조건": {"구분": "고르기", "내용": "고르기"},
"값칸": {"할증률": "%"},
"줄": [
{"구분": "그물망 피복 시 임내 운반거리", "내용": "200m 초과", "할증률": 20},
{"구분": "그물망 피복 시 임내 운반거리", "내용": "101200m", "할증률": 10},
{"구분": "그물망 피복 시 임내 운반거리", "내용": "100m 이하", "할증률": 0},
{"구분": "수라집재 시 목재 운반거리", "내용": "1120m", "할증률": 10},
{"구분": "수라집재 시 목재 운반거리", "내용": "610m", "할증률": 5},
{"구분": "수라집재 시 목재 운반거리", "내용": "05m", "할증률": 0}
],
"주": ["방제목 운반거리는 소나무재선충병방제에 적용한다."]
},
{
"열쇠": "1-4-24 방제장비 규격",
"이름": "품의 할인 — 방제장비 규격",
"출처": "산림품셈 1-4-24",
"조건": {"구분": "고르기", "내용": "고르기"},
"값칸": {"할인률": "%", "비고": "글"},
"줄": [
{"구분": "장비의 규격(굴삭기)", "내용": "0.2㎥", "할인률": 0},
{"구분": "장비의 규격(굴삭기)", "내용": "0.4㎥ 이상", "할인률": -20, "비고": "장비품의 -20%"}
],
"주": ["방제장비 규격은 소나무재선충병방제의 기계장비를 활용한 훈증더미 제거에 한하여 적용한다."]
},
{
"열쇠": "1-4-25 작업시간 제한",
"이름": "품의 할증 — 작업시간 제한",
"출처": "산림품셈 1-4-25",
"조건": {"작업 가능 시간": "수"},
"값칸": {"할증률": "%"},
"범위규칙": "작업 가능 시간(시간) 이하",
"줄": [
{"작업 가능 시간": 2, "할증률": 50},
{"작업 가능 시간": 3, "할증률": 35},
{"작업 가능 시간": 4, "할증률": 25},
{"작업 가능 시간": 5, "할증률": 20},
{"작업 가능 시간": 6, "할증률": 15}
],
"주": ["도서(제주도포함), 오지지역, 민간군사통제구역 사업 등으로 인하여 작업시간이 줄어들 때 적용한다."]
},
{
"열쇠": "1-4-26 소규모 작업물량 제한",
"이름": "품의 할증 — 소규모 작업물량 제한",
"출처": "산림품셈 1-4-26",
"조건": {"구분": "수"},
"값칸": {"적용조건": "글", "적용시공량": "식"},
"줄": [
{"구분": 1, "적용조건": "A ≦ B/2 일 경우", "적용시공량": "Q = B/2"},
{"구분": 2, "적용조건": "B/2 < A ≦ B 일 경우", "적용시공량": "Q = B"}
],
"주": ["시공량(A), 1일시공량(표준품셈)(B), 적용시공량(Q)\n※ 시공량(A)은 일반적으로 총 시공량을 적용한다. 다만, 외부환경(교통통제 및 발주물량 제한으로 “시공량/일”이 제한되는 경우 등)으로 인해 “시공량/일” 미만이 발생되는 경우 해당 시공량으로 적용한다"]
}
]
}
+18 -9
View File
@@ -42,7 +42,8 @@
"잡재료율": "건설품셈 공통 8-4-1",
"조종원": "건설품셈 공통 8-4-1",
"조종원직종": "건설품셈 공통 8-1-3 5 · 건설노임 1048"
}
},
"비고": "조종원직종 원문 이름: 건설기계운전사"
},
{
"열쇠": "0201-0020",
@@ -83,7 +84,8 @@
"잡재료율": "건설품셈 공통 8-4-1",
"조종원": "건설품셈 공통 8-4-1",
"조종원직종": "건설품셈 공통 8-1-3 5 · 건설노임 1048"
}
},
"비고": "조종원직종 원문 이름: 건설기계운전사"
},
{
"열쇠": "0201-0040",
@@ -124,7 +126,8 @@
"잡재료율": "건설품셈 공통 8-4-1",
"조종원": "건설품셈 공통 8-4-1",
"조종원직종": "건설품셈 공통 8-1-3 5 · 건설노임 1048"
}
},
"비고": "조종원직종 원문 이름: 건설기계운전사"
},
{
"열쇠": "0201-0060",
@@ -165,7 +168,8 @@
"잡재료율": "건설품셈 공통 8-4-1",
"조종원": "건설품셈 공통 8-4-1",
"조종원직종": "건설품셈 공통 8-1-3 5 · 건설노임 1048"
}
},
"비고": "조종원직종 원문 이름: 건설기계운전사"
},
{
"열쇠": "0201-0070",
@@ -206,7 +210,8 @@
"잡재료율": "건설품셈 공통 8-4-1",
"조종원": "건설품셈 공통 8-4-1",
"조종원직종": "건설품셈 공통 8-1-3 5 · 건설노임 1048"
}
},
"비고": "조종원직종 원문 이름: 건설기계운전사"
},
{
"열쇠": "0201-0080",
@@ -247,7 +252,8 @@
"잡재료율": "건설품셈 공통 8-4-1",
"조종원": "건설품셈 공통 8-4-1",
"조종원직종": "건설품셈 공통 8-1-3 5 · 건설노임 1048"
}
},
"비고": "조종원직종 원문 이름: 건설기계운전사"
},
{
"열쇠": "0201-0100",
@@ -288,7 +294,8 @@
"잡재료율": "건설품셈 공통 8-4-1",
"조종원": "건설품셈 공통 8-4-1",
"조종원직종": "건설품셈 공통 8-1-3 5 · 건설노임 1048"
}
},
"비고": "조종원직종 원문 이름: 건설기계운전사"
},
{
"열쇠": "0201-0120",
@@ -329,7 +336,8 @@
"잡재료율": "건설품셈 공통 8-4-1",
"조종원": "건설품셈 공통 8-4-1",
"조종원직종": "건설품셈 공통 8-1-3 5 · 건설노임 1048"
}
},
"비고": "조종원직종 원문 이름: 건설기계운전사"
},
{
"열쇠": "0201-0200",
@@ -370,7 +378,8 @@
"잡재료율": "건설품셈 공통 8-4-1",
"조종원": "건설품셈 공통 8-4-1",
"조종원직종": "건설품셈 공통 8-1-3 5 · 건설노임 1048"
}
},
"비고": "조종원직종 원문 이름: 건설기계운전사"
}
]
}
+120
View File
@@ -0,0 +1,120 @@
"""마스터 데이터 검사 도구 — 견본 셋 손 계산 · 견본 파일 검사 통과 (`resources/master_data/scripts/check_master.py`)."""
import sys
from decimal import ROUND_HALF_UP, Decimal
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "master_data/scripts"))
import check_master as cm # noqa: E402
import master_formula as mf # noqa: E402
SAMPLES = [
"계수_건설품셈_공통8장.json",
"계수_산림품셈_9장.json",
"기계_건설품셈.json",
"로직_건설품셈_공통8장.json",
"로직_산림품셈_13장.json",
"로직_산림품셈_9장.json",
"소요량_산림품셈_13장.json",
]
EXCAVATOR = "건설품셈:8 굴착기(무한궤도) 시간당 경비"
@pytest.fixture(scope="module")
def whole():
return cm.master()
def cents(x) -> str:
return str(Decimal(x).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))
def test_메쌓기(whole):
got = mf.run(
whole,
"산림품셈:13-4-1 메쌓기",
{"뒷길이": 35, "": "견치돌", "쌓기": "골쌓기", "높이": 2, "초과증가율": 80},
)
assert cents(got[""]) == "203281.20"
def test_메쌓기_높이_증가율(whole):
got = mf.run(
whole,
"산림품셈:13-4-1 메쌓기",
{"뒷길이": 35, "": "견치돌", "쌓기": "골쌓기", "높이": 5, "초과증가율": 80},
)
assert [line["수량"] for line in got[""]] == [Decimal("0.70"), Decimal("0.56")]
def test_굴착기(whole):
got = mf.run(
whole, EXCAVATOR, {"기계": "0201-0070", "지역": "전국평균", "보정작업": "해당 없음"}
)
assert [cents(got[k]) for k in ("노무비", "재료비", "경비", "")] == [
"35412.13",
"26130.11",
"24554.00",
"86096.24",
]
def test_굴착기_암석_보정(whole):
got = mf.run(
whole,
EXCAVATOR,
{"기계": "0201-0070", "지역": "전국평균", "보정작업": "암석작업(연암・보통암・경암)"},
)
assert got["중간"]["손료"] == 28322
assert cents(got[""]) == "89864.24"
def test_노체포설(whole):
assert mf.run(whole, "산림품셈:9-16-1 노체포설 작업량", {})["결과"] == Decimal("24.255")
got = mf.run(whole, "산림품셈:9-16-1 노체포설", {"지역": "전국평균", "보정작업": "해당 없음"})
assert [cents(got[k]) for k in ("노무비", "재료비", "경비", "")] == [
"1459.99",
"1077.31",
"1012.33",
"3549.63",
]
def test_범위_입력은_막음(whole):
with pytest.raises(mf.FormulaError):
mf.run(
whole,
"산림품셈:13-4-1 메쌓기",
{"뒷길이": 35, "": "견치돌", "쌓기": "골쌓기", "높이": 8, "초과증가율": 120},
)
def test_견본_파일_검사_통과(whole):
files = cm.load(SAMPLES)
assert len(files) == len(SAMPLES)
assert [x for name, data in files.items() for x in cm.check_form(name, data)] == []
assert cm.check_body(files) == []
assert cm.check_logics(files, whole) == []
def test_로직_검사가_틀린_열쇠를_잡음(whole):
row = {
"열쇠": "시험",
"입력": [{"이름": "지역", "고르기": ["전국평균", "없는곳"]}],
"중간": [{"이름": "x", "": "찾기(계수:산림품셈:9-16-1 노체포설, 없는조건='K').적용"}],
"호표": [
{"종류": "재료", "요소": "재료:오피넷유가:경유:{지역}", "수량": "y", "비목": "재료비"}
],
"덧줄": [],
}
found, _ = mf.check_logic(whole, "산림품셈", row)
assert any("없는조건" in x for x in found)
assert any("경유:없는곳" in x for x in found)
assert any("「y」" in x for x in found)
def test_돌고_도는_참조():
assert mf.find_loops({"": {""}, "": {""}}) == [["", "", ""]]