diff --git a/A00_Common/main.ts b/A00_Common/main.ts index 26d914b8..537077f4 100644 --- a/A00_Common/main.ts +++ b/A00_Common/main.ts @@ -27,6 +27,7 @@ const FOOTERLESS_ROUTES: readonly RoutePath[] = [ ROUTES.B10_PAYMENT, ROUTES.B11_STATUS, ROUTES.Z01_MASTER_DATA, + ROUTES.M01_MASTER_DATA, ]; function bootstrap(): void { diff --git a/A00_Common/router.ts b/A00_Common/router.ts index b6c99652..547dc50f 100644 --- a/A00_Common/router.ts +++ b/A00_Common/router.ts @@ -61,6 +61,8 @@ const routeTable: Partial Promise>> = { (await import("../B11_Status/B11_Status_UI_Loading")).renderB11Loading, [ROUTES.Z01_MASTER_DATA]: async () => (await import("../Z01_MasterData/Z01_MasterData_UI_Page")).renderZ01MasterData, + [ROUTES.M01_MASTER_DATA]: async () => + (await import("../M01_MasterData/M01_MasterData_UI_Page")).renderM01MasterData, }; /** 로그인 여부 (토큰 존재 확인) */ diff --git a/B01_Dashboard/B01_Dashboard_UI_Page.ts b/B01_Dashboard/B01_Dashboard_UI_Page.ts index 9f675b88..6e47a210 100644 --- a/B01_Dashboard/B01_Dashboard_UI_Page.ts +++ b/B01_Dashboard/B01_Dashboard_UI_Page.ts @@ -139,6 +139,11 @@ function buildSystemSettingsPanel(): HTMLElement { variant: "ghost", onClick: () => navigateTo(ROUTES.Z01_MASTER_DATA), }), + createButton({ + label: L("B01_Dashboard_MasterElements"), + variant: "ghost", + onClick: () => navigateTo(ROUTES.M01_MASTER_DATA), + }), ); return wrap; } diff --git a/M01_MasterData/M01_MasterData_Api_Fetch.ts b/M01_MasterData/M01_MasterData_Api_Fetch.ts new file mode 100644 index 00000000..76e11db9 --- /dev/null +++ b/M01_MasterData/M01_MasterData_Api_Fetch.ts @@ -0,0 +1,109 @@ +/* ============================================================================= + * M01_MasterData_Api_Fetch.ts + * 마스터 요소 화면 서버 호출 — 계약 `resources/master_data/_화면_계약.md` · 시스템 관리자만 + * ========================================================================== */ + +import { API_BASE_URL } from "@config/config_frontend"; + +/** 줄·표 한 개 — 파일 그대로(한글 칸). */ +export type Row = Record; + +export interface GroupInfo { + group: string; + files: number; + rows: number; +} + +export interface FileInfo { + file: string; + book: string | null; + chapter: string; + edition: string | null; + rows: number; + version: string; +} + +export interface RowsPage { + file: string; + version: string; + total: number; + page: number; + size: number; + rows: Row[]; +} + +export interface TableHead { + 열쇠: string; + 이름: string; + 기준: string; + 출처: string; + 조건: Record; + 값칸: Record; + count: number; +} + +export interface Change { + op: "edit" | "add" | "delete"; + key?: string; + row?: Row; +} + +export interface FileChanges { + file: string; + version: string; + changes: Change[]; +} + +/** 저장 결과 — 200 은 새 판본 · 나머지는 화면이 그대로 안내할 상태. */ +export type SaveResult = + | { status: 200; files: { file: string; version: string }[] } + | { status: 409; stale: string[] } + | { status: 422; errors: string[] } + | { status: number; detail: string }; + +async function call(path: string, init: RequestInit = {}): Promise<{ status: number; body: T }> { + const response = await fetch(`${API_BASE_URL}/m01${path}`, { + credentials: "include", + headers: { "Content-Type": "application/json" }, + ...init, + }); + return { status: response.status, body: (await response.json()) as T }; +} + +async function get(path: string, params: Record): Promise { + const query = new URLSearchParams(Object.entries(params).map(([k, v]) => [k, String(v)])); + const { status, body } = await call(`${path}?${query}`); + if (status !== 200) throw new Error(String(body.detail ?? `HTTP ${status}`)); + return body; +} + +export const fetchGroups = async (): Promise => + (await get<{ groups: GroupInfo[] }>("/groups", {})).groups; + +export const fetchFiles = async (group: string): Promise => + (await get<{ files: FileInfo[] }>(`/groups/${encodeURIComponent(group)}/files`, {})).files; + +export const fetchRows = (file: string, page: number, size: number, q: string): Promise => + get("/rows", { file, page, size, q }); + +export const fetchTables = ( + file: string, + q: string, +): Promise<{ version: string; tables: TableHead[] }> => get("/tables", { file, q }); + +export const fetchTable = ( + file: string, + key: string, +): Promise<{ version: string; table: Row & { 줄: Row[] } }> => get("/table", { file, key }); + +export async function saveFiles(files: FileChanges[]): Promise { + const { status, body } = await call>("/save", { + method: "POST", + body: JSON.stringify({ files }), + }); + const detail = body.detail as { stale?: string[]; errors?: string[] } | string | undefined; + if (status === 200) return { status, files: body.files as { file: string; version: string }[] }; + if (status === 409 && typeof detail === "object") return { status, stale: detail.stale ?? [] }; + if (status === 422 && typeof detail === "object") return { status, errors: detail.errors ?? [] }; + return { status, detail: typeof detail === "string" ? detail : JSON.stringify(detail) }; +} diff --git a/M01_MasterData/M01_MasterData_Draft.ts b/M01_MasterData/M01_MasterData_Draft.ts new file mode 100644 index 00000000..4201d541 --- /dev/null +++ b/M01_MasterData/M01_MasterData_Draft.ts @@ -0,0 +1,161 @@ +/* ============================================================================= + * M01_MasterData_Draft.ts + * 고친 것 초안 — sessionStorage 에만 쌓임(자동저장 없음) · [저장] 때만 서버로. + * + * 요소 파일 = 줄 단위(edits · adds · deletes) · 표형 파일 = 표 하나 통째(tables). + * ========================================================================== */ + +import type { FileChanges, Row } from "./M01_MasterData_Api_Fetch"; + +/** o = 원래 줄 번호(새 줄은 null) — 노랑 칠은 이 번호의 원래 줄과 견줌. */ +export interface TableItem { + o: number | null; + r: Row; +} + +interface TableDraft { + base: Row; + items: TableItem[]; +} + +interface FileDraft { + version: string; + edits: Record; + adds: Row[]; + deletes: string[]; + tables: Record; +} + +const STORAGE_KEY = "m01_master_draft"; +const listeners = new Set<() => void>(); + +function readStore(): Record { + try { + return JSON.parse(sessionStorage.getItem(STORAGE_KEY) ?? "{}") as Record; + } catch { + return {}; + } +} + +let store = readStore(); + +export const same = (a: unknown, b: unknown): boolean => JSON.stringify(a) === JSON.stringify(b); + +/** 구독 — 돌려받은 함수를 부르면 해제. */ +export function onDraftChange(fn: () => void): () => void { + listeners.add(fn); + return () => void listeners.delete(fn); +} + +function touch(file: string): void { + const d = store[file]; + if ( + d && + !Object.keys(d.edits).length && + !d.adds.length && + !d.deletes.length && + !Object.keys(d.tables).length + ) { + delete store[file]; + } + try { + sessionStorage.setItem(STORAGE_KEY, JSON.stringify(store)); + } catch { + /* 저장소 막힘 — 이 탭 메모리에서만 유지 */ + } + listeners.forEach((fn) => fn()); +} + +function slot(file: string, version: string): FileDraft { + return (store[file] ??= { version, edits: {}, adds: [], deletes: [], tables: {} }); +} + +export function peek(file: string): FileDraft | undefined { + return store[file]; +} + +const countOf = (d: FileDraft): number => + Object.keys(d.edits).length + d.adds.length + d.deletes.length + Object.keys(d.tables).length; + +export const fileCount = (file: string): number => (store[file] ? countOf(store[file]) : 0); +export const totalCount = (): number => Object.values(store).reduce((n, d) => n + countOf(d), 0); + +/** 그 파일을 초안이 받은 판본과 다른 판본으로 읽었나 — 저장하면 409 로 막힐 초안. */ +export const isStale = (file: string, version: string): boolean => + store[file] !== undefined && store[file].version !== version; + +/* --- 요소 파일 --- */ +export function editRow(file: string, version: string, key: string, orig: Row, next: Row): void { + const d = slot(file, version); + if (same(orig, next)) delete d.edits[key]; + else d.edits[key] = next; + touch(file); +} + +export function addRow(file: string, version: string, row: Row): void { + slot(file, version).adds.push(row); + touch(file); +} + +export function setAdd(file: string, index: number, row: Row): void { + store[file].adds[index] = row; + touch(file); +} + +export function removeAdd(file: string, index: number): void { + store[file].adds.splice(index, 1); + touch(file); +} + +export function toggleDelete(file: string, version: string, key: string): void { + const d = slot(file, version); + d.deletes = d.deletes.includes(key) ? d.deletes.filter((k) => k !== key) : [...d.deletes, key]; + touch(file); +} + +/* --- 표형 파일 --- */ +export function setTable( + file: string, + version: string, + key: string, + base: Row, + items: TableItem[], + orig: Row[], +): void { + const d = slot(file, version); + if (items.length === orig.length && items.every((it, i) => it.o === i && same(it.r, orig[i]))) { + delete d.tables[key]; + } else { + d.tables[key] = { base, items }; + } + touch(file); +} + +export const tableItems = (file: string, key: string): TableItem[] | undefined => + store[file]?.tables[key]?.items; + +/* --- 저장·버리기 --- */ +export function payload(): FileChanges[] { + return Object.entries(store).map(([file, d]) => ({ + file, + version: d.version, + changes: [ + ...Object.entries(d.edits) + .filter(([key]) => !d.deletes.includes(key)) + .map(([key, row]) => ({ op: "edit" as const, key, row })), + ...Object.entries(d.tables).map(([key, t]) => ({ + op: "edit" as const, + key, + row: { ...t.base, 줄: t.items.map((it) => it.r) }, + })), + ...d.adds.map((row) => ({ op: "add" as const, row })), + ...d.deletes.map((key) => ({ op: "delete" as const, key })), + ], + })); +} + +/** files 를 안 주면 전부 버림. */ +export function discard(files?: string[]): void { + for (const file of files ?? Object.keys(store)) delete store[file]; + touch(""); +} diff --git a/M01_MasterData/M01_MasterData_Router.py b/M01_MasterData/M01_MasterData_Router.py new file mode 100644 index 00000000..0746de12 --- /dev/null +++ b/M01_MasterData/M01_MasterData_Router.py @@ -0,0 +1,108 @@ +"""M01 마스터 데이터 관리자 API — 계약 `resources/master_data/_화면_계약.md`. + +⚠ 권한은 등록하는 쪽(`main.py`)이 `system_admin_only` 로 붙임. +""" + +from __future__ import annotations + +from decimal import Decimal +from typing import Any, Literal + +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel + +from M01_MasterData import M01_MasterData_Store as store + +router = APIRouter(prefix="/api/m01", tags=["M01 MasterData"]) + + +class CalcBody(BaseModel): + book: str + key: str + inputs: dict[str, Any] = {} + row: dict[str, Any] | None = None # 저장 전 고친 로직 줄(없으면 저장된 파일로) + file: str | None = None # 새 로직이면 더할 로직 파일 + + +class Change(BaseModel): + op: Literal["edit", "add", "delete"] + key: str | None = None + row: dict[str, Any] | None = None + + +class FileChanges(BaseModel): + file: str + version: str + changes: list[Change] + + +class SaveBody(BaseModel): + files: list[FileChanges] + + +def _num(v): + """Decimal → JSON 수(그냥 두면 글자로 나감).""" + if isinstance(v, Decimal): + return int(v) if v == v.to_integral_value() else float(v) + if isinstance(v, list): + return [_num(x) for x in v] + if isinstance(v, dict): + return {k: _num(x) for k, x in v.items()} + return v + + +def _call(fn, *args): + try: + return _num(fn(*args)) + except store.StoreError as e: + raise HTTPException(status_code=e.status, detail=e.detail) from e + + +@router.get("/groups") +def get_groups() -> dict: + return {"groups": _call(store.groups)} + + +@router.get("/groups/{group}/files") +def get_files(group: str) -> dict: + return {"files": _call(store.files_of, group)} + + +@router.get("/rows") +def get_rows(file: str, page: int = 1, size: int = 50, q: str = "") -> dict: + return _call(store.rows, file, page, size, q) + + +@router.get("/tables") +def get_tables(file: str, q: str = "") -> dict: + return _call(store.tables, file, q) + + +@router.get("/table") +def get_table(file: str, key: str) -> dict: + return _call(store.table, file, key) + + +@router.get("/logics") +def get_logics(book: str = "", chapter: str = "", q: str = "", blocked: int | None = None) -> dict: + return {"logics": _call(store.logics, book, chapter, q, blocked)} + + +@router.get("/logic") +def get_logic(book: str, key: str) -> dict: + return _call(store.logic, book, key) + + +@router.get("/elements") +def get_elements(group: str, q: str = "", limit: int = 50) -> dict: + return _call(store.elements, group, q, limit) + + +@router.post("/calc") +def post_calc(body: CalcBody) -> dict: + return _call(store.calc, body.book, body.key, body.inputs, body.row, body.file) + + +@router.post("/save") +def post_save(body: SaveBody) -> dict: + return {"files": _call(store.save, [f.model_dump() for f in body.files])} diff --git a/M01_MasterData/M01_MasterData_Store.py b/M01_MasterData/M01_MasterData_Store.py new file mode 100644 index 00000000..9968fd86 --- /dev/null +++ b/M01_MasterData/M01_MasterData_Store.py @@ -0,0 +1,353 @@ +"""M01 마스터 데이터 파일 저장소 — `resources/master_data/` 첫 층 JSON 읽기·검사·쓰기. + +계약: `resources/master_data/_화면_계약.md` · 파일 모양: `_틀.md` +검사·계산 엔진: `scripts/check_master.py`. +""" + +from __future__ import annotations + +import hashlib +import json +import os +import re +import sys +import threading +from decimal import Decimal +from pathlib import Path + +SCRIPTS = Path(__file__).resolve().parents[1] / "resources/master_data/scripts" +sys.path.insert(0, str(SCRIPTS)) + +import check_master as cm # noqa: E402 + +FOLDER: Path = cm.MASTER # 시험은 사본 폴더로 바꿈 +_CHAPTER = re.compile(r"_([^_]*\d+장)\.json$") # 「공통3장」 · 「13장」 +# ponytail: 저장은 한 번에 하나(프로세스 안 잠금) · 서버를 여럿 띄우면 파일 잠금으로 +_LOCK = threading.Lock() + + +class StoreError(Exception): + def __init__(self, status: int, detail): + super().__init__(detail) + self.status, self.detail = status, detail + + +# ── 읽기 ────────────────────────────────────────────────────────────── +def _path(file: str) -> Path: + path = FOLDER / file + if "/" in file or "\\" in file or file.startswith("_") or not path.is_file(): + raise StoreError(404, f"없는 파일 「{file}」") + return path + + +def version_of(raw: bytes) -> str: + return hashlib.sha256(raw).hexdigest()[:16] + + +def read(file: str) -> tuple[dict, str]: + raw = _path(file).read_bytes() + return json.loads(raw, parse_float=Decimal, parse_int=Decimal), version_of(raw) + + +def items_key(data: dict) -> str: + return "표" if data.get("그룹") in cm.mf.TABLE_GROUPS else "줄" + + +def chapter_of(file: str) -> str: + m = _CHAPTER.search(file) + return m.group(1) if m else "" + + +def _hit(row: dict, q: str) -> bool: + q = q.lower() + return not q or q in str(row.get("열쇠", "")).lower() or q in str(row.get("이름", "")).lower() + + +def groups() -> list[dict]: + out = {g: {"group": g, "files": 0, "rows": 0} for g in cm.mf.GROUPS} + for data in cm.load(folder=FOLDER).values(): + slot = out.get(data.get("그룹")) + if slot: + slot["files"] += 1 + slot["rows"] += len(data.get(items_key(data)) or []) + return list(out.values()) + + +def files_of(group: str) -> list[dict]: + if group not in cm.mf.GROUPS: + raise StoreError(404, f"없는 그룹 「{group}」") + out = [] + for path in sorted(FOLDER.glob(f"{group}_*.json")): + data, version = read(path.name) + out.append( + { + "file": path.name, + "book": data.get("원문"), + "chapter": chapter_of(path.name), + "edition": data.get("판"), + "rows": len(data.get(items_key(data)) or []), + "version": version, + } + ) + return out + + +def rows(file: str, page: int, size: int, q: str) -> dict: + data, version = read(file) + hits = [r for r in data.get(items_key(data)) or [] if _hit(r, q)] + page, size = max(page, 1), min(max(size, 1), 500) + return { + "file": file, + "version": version, + "total": len(hits), + "page": page, + "size": size, + "rows": hits[(page - 1) * size : page * size], + } + + +def tables(file: str, q: str) -> dict: + data, version = read(file) + if items_key(data) != "표": + raise StoreError(400, f"표형 파일 아님 「{file}」") + heads = ("열쇠", "이름", "기준", "출처", "조건", "값칸") + return { + "file": file, + "version": version, + "tables": [ + {**{k: t.get(k) for k in heads}, "count": len(t.get("줄") or [])} + for t in data["표"] + if _hit(t, q) + ], + } + + +def _one(data: dict, key: str) -> dict: + for row in data.get(items_key(data)) or []: + if str(row.get("열쇠")) == key: + return row + raise StoreError(404, f"없는 열쇠 「{key}」") + + +def table(file: str, key: str) -> dict: + data, version = read(file) + return {"file": file, "version": version, "table": _one(data, key)} + + +def _logic_files(files: dict[str, dict]): + for name, data in files.items(): + if data.get("그룹") == "로직": + yield name, data + + +def logics(book: str, chapter: str, q: str, blocked: int | None) -> list[dict]: + files = cm.load(folder=FOLDER) + whole = cm.mf.Master(files) + out = [] + for name, data in _logic_files(files): + if (book and data.get("원문") != book) or (chapter and chapter_of(name) != chapter): + continue + for row in data.get("줄") or []: + if not _hit(row, q): + continue + reasons = cm.mf.check_logic(whole, data.get("원문"), row)[0] + if blocked is not None and bool(reasons) != bool(blocked): + continue + out.append( + { + "file": name, + "book": data.get("원문"), + "chapter": chapter_of(name), + **{k: row.get(k) for k in ("열쇠", "이름", "결과단위", "출처")}, + "blocked": bool(reasons), + "reasons": reasons, + } + ) + return out + + +def logic(book: str, key: str) -> dict: + files = cm.load(folder=FOLDER) + for name, data in _logic_files(files): + if data.get("원문") != book: + continue + for row in data.get("줄") or []: + if str(row.get("열쇠")) == key: + whole = cm.mf.Master(files) + reasons = cm.mf.check_logic(whole, book, row)[0] + _, version = read(name) + return { + "file": name, + "version": version, + "logic": row, + "blocked": bool(reasons), + "reasons": reasons, + "prices": _prices(whole, row), + } + raise StoreError(404, f"없는 로직 「{book}:{key}」") + + +def _brief(ref: str, row: dict) -> dict: + return {"ref": ref, **{k: row.get(k) for k in ("이름", "규격", "단위", "값", "값칸")}} + + +def _prices(whole, row: dict) -> dict: + """호표 요소 → 요소 줄 요약(단가 자동 칸) · `{이름}` 낀 열쇠·하위 로직은 계산 때 정해짐.""" + out = {} + for item in row.get("호표") or []: + ref = str(item.get("요소", "")) + if item.get("종류") != "로직" and "{" not in ref: + try: + out[ref] = _brief(ref, whole.get(ref)) + except cm.mf.FormulaError: + out[ref] = None + return out + + +def elements(group: str, q: str, limit: int) -> dict: + """요소 찾기 창 — 그룹 전체에서 이름·열쇠 찾기 · `ref` 는 식에 그대로 넣는 `그룹:원문:열쇠`.""" + if group not in cm.mf.GROUPS: + raise StoreError(404, f"없는 그룹 「{group}」") + hits = [] + for name, data in cm.load(folder=FOLDER).items(): + if data.get("그룹") != group: + continue + for row in data.get(items_key(data)) or []: + if _hit(row, q): + ref = f"{group}:{data.get('원문')}:{row.get('열쇠')}" + hits.append({**_brief(ref, row), "file": name}) + return {"total": len(hits), "items": hits[: min(max(limit, 1), 200)]} + + +# ── 시험 계산 ────────────────────────────────────────────────────────── +def _swap(files: dict, book: str, key: str, row: dict, file: str | None) -> None: + """저장 전 시험 — 메모리 사본에서 그 로직을 고친 줄로 바꿈(없으면 `file` 에 더함).""" + row = _dec(row) + for name, data in _logic_files(files): + if data.get("원문") == book: + items = data.get("줄") or [] + for i, old in enumerate(items): + if str(old.get("열쇠")) == key: + items[i] = row + return + if file not in files or files[file].get("그룹") != "로직": + raise StoreError(404, f"없는 로직 파일 「{file}」") + files[file].setdefault("줄", []).append(row) + + +def calc(book: str, key: str, inputs: dict, row: dict | None = None, file: str | None = None): + given = {k: _dec(v) for k, v in inputs.items()} + files = cm.load(folder=FOLDER) + if row is not None: + _swap(files, book, key, row, file) + key = str(row.get("열쇠", key)) + try: + result = cm.calc(files, f"{book}:{key}", given) + except (cm.mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError) as e: + return {"ok": False, "reason": str(e) or type(e).__name__} + if "결과" in result: + return {"ok": True, "result": result["결과"], "middle": result["중간"]} + return { + "ok": True, + "lines": result["줄"], + "sums": {k: result[k] for k in cm.mf.BUNDLE}, + "middle": result["중간"], + } + + +# ── 저장 ────────────────────────────────────────────────────────────── +def _dec(v): + """화면이 보낸 수 → Decimal(파일을 읽은 모양과 같게).""" + if isinstance(v, bool) or v is None: + return v + if isinstance(v, (int, float)): + return Decimal(str(v)) + if isinstance(v, list): + return [_dec(x) for x in v] + if isinstance(v, dict): + return {k: _dec(x) for k, x in v.items()} + return v + + +def _apply(data: dict, changes: list[dict], file: str) -> None: + items = data.setdefault(items_key(data), []) + for ch in changes: + op, key, row = ch.get("op"), ch.get("key"), _dec(ch.get("row")) + if op not in ("edit", "add", "delete"): + raise StoreError(400, f"모르는 op 「{op}」") + if op != "add": + at = next((i for i, r in enumerate(items) if str(r.get("열쇠")) == key), None) + if at is None: + raise StoreError(404, f"{file} · 없는 열쇠 「{key}」") + if op != "delete" and not (isinstance(row, dict) and "열쇠" in row): + raise StoreError(400, f"{file} · row 에 열쇠 없음") + new_key = str(row["열쇠"]) if op != "delete" else None + taken = {str(r.get("열쇠")) for i, r in enumerate(items) if op == "add" or i != at} + if new_key in taken: + raise StoreError(400, f"{file} · 열쇠 겹침 「{new_key}」") + if op == "add": + items.append(row) + elif op == "edit": + items[at] = row + else: + del items[at] + + +def save(batch: list[dict]) -> list[dict]: + with _LOCK: + stale = [p.get("file", "") for p in batch if read(p.get("file", ""))[1] != p.get("version")] + if stale: + raise StoreError(409, {"stale": stale}) + before, after = cm.load(folder=FOLDER), cm.load(folder=FOLDER) # after = 고칠 사본 + names = [part["file"] for part in batch] + for part in batch: + _apply(after[part["file"]], part.get("changes") or [], part["file"]) + old = set(cm.check_saved(before, names)) + new = [x for x in cm.check_saved(after, names) if x not in old] + if new: + raise StoreError(422, {"errors": new}) + out = [] + for name in dict.fromkeys(names): + text = dump(after[name]) + tmp = FOLDER / f".{name}.tmp" + tmp.write_text(text, encoding="utf-8", newline="\n") + os.replace(tmp, FOLDER / name) + out.append({"file": name, "version": version_of(text.encode("utf-8"))}) + return out + + +# ── 쓰기 모양 ───────────────────────────────────────────────────────── +def _atom(v) -> str: + if isinstance(v, Decimal): + return str(v) + return json.dumps(v, ensure_ascii=False) + + +def _flat(v) -> str: + if isinstance(v, dict): + return ( + "{ " + ", ".join(f"{_atom(k)}: {_flat(x)}" for k, x in v.items()) + " }" if v else "{}" + ) + if isinstance(v, list): + return "[" + ", ".join(_flat(x) for x in v) + "]" + return _atom(v) + + +def dump(v, indent: int = 0, width: int = 160) -> str: + """UTF-8 · 들여쓰기 2칸 · 한 줄에 들어가는 묶음은 한 줄로.""" + flat = _flat(v) + if indent and len(flat) + indent <= width or not isinstance(v, (dict, list)) or not v: + return flat + ("" if indent else "\n") + pad = " " * (indent + 2) + if isinstance(v, dict): + body = [f"{pad}{_atom(k)}: {dump(x, indent + 2, width)}" for k, x in v.items()] + text = "{\n" + ",\n".join(body) + "\n" + " " * indent + "}" + else: + text = ( + "[\n" + + ",\n".join(pad + dump(x, indent + 2, width) for x in v) + + "\n" + + " " * indent + + "]" + ) + return text + ("" if indent else "\n") diff --git a/M01_MasterData/M01_MasterData_UI_Cells.ts b/M01_MasterData/M01_MasterData_UI_Cells.ts new file mode 100644 index 00000000..f1e4441b --- /dev/null +++ b/M01_MasterData/M01_MasterData_UI_Cells.ts @@ -0,0 +1,121 @@ +/* ============================================================================= + * M01_MasterData_UI_Cells.ts + * 칸 다루기 — 값 → 글 · 글 → 값 · 눌러 고치기 · 쪽 나눔 띠 (요소·표 화면 공용) + * ========================================================================== */ + +import { createButton, el } from "@ui/ui_template_elements"; +import { t as L } from "@ui/ui_template_locale"; +import type { Row } from "./M01_MasterData_Api_Fetch"; + +/** 묶음 값(`값 = {가격: …}`)을 「값›가격」 칸으로 펼칠 때의 구분자. */ +export const SEP = "›"; + +export const isScalar = (v: unknown): boolean => + v === null || v === undefined || ["string", "number", "boolean"].includes(typeof v); + +/** 줄 → 칸 이름별 값. 묶음(dict) 칸은 한 단계 펼침. */ +export function flatten(row: Row): Record { + const out: Record = {}; + for (const [k, v] of Object.entries(row)) { + if (v && typeof v === "object" && !Array.isArray(v)) { + for (const [k2, v2] of Object.entries(v)) out[`${k}${SEP}${k2}`] = v2; + } else { + out[k] = v; + } + } + return out; +} + +/** 칸 이름(펼친 것 포함)에 값을 넣은 새 줄. */ +export function withCell(row: Row, path: string, value: unknown): Row { + const [head, sub] = path.split(SEP); + if (sub === undefined) return { ...row, [head]: value }; + return { ...row, [head]: { ...(row[head] as Row), [sub]: value } }; +} + +export function show(v: unknown): string { + if (v === null || v === undefined) return ""; + if (Array.isArray(v)) return v.map((x) => (x === null ? "" : String(x))).join(" ~ "); + return typeof v === "object" ? JSON.stringify(v) : String(v); +} + +const NUMBER = /^-?\d+(\.\d+)?$/; + +/** 글 → 수(수 꼴이면) 아니면 글. 빈 글 = null. */ +export function toNumberish(text: string): number | string | null { + const s = text.replace(/,/g, "").trim(); + if (s === "") return null; + return NUMBER.test(s) ? Number(s) : text.trim(); +} + +/** 원래 값의 꼴을 따라 글을 값으로 — 글이었으면 글 · 수·빈 칸이었으면 수 꼴이면 수. */ +export const coerce = (text: string, orig: unknown): unknown => + typeof orig === "string" ? text : toNumberish(text); + +/** 「아래 ~ 위」 → [아래, 위] (빈 쪽 = null · 「~」 없으면 한 값). */ +export function toRange(text: string): unknown { + if (!text.includes("~")) return toNumberish(text); + return text.split("~").map((s) => (s.trim() === "" ? null : toNumberish(s))); +} + +/** 칸을 눌러 고침 — Enter·칸 밖 누름 = 확정 · Esc = 취소. */ +export function startEdit(td: HTMLElement, text: string, commit: (value: string) => void): void { + if (td.querySelector("input")) return; + const input = el("input", { className: "m01-master__cell-input", attrs: { type: "text" } }); + input.value = text; + td.textContent = ""; + td.append(input); + input.focus(); + input.select(); + let done = false; + const finish = (ok: boolean): void => { + if (done) return; + done = true; + if (ok && input.value !== text) commit(input.value); + else td.textContent = text; + }; + input.addEventListener("keydown", (e) => { + if (e.key === "Enter") finish(true); + else if (e.key === "Escape") finish(false); + }); + input.addEventListener("blur", () => finish(true)); +} + +/** 쪽 나눔 띠 — 이전·다음 · 「n / N 쪽 · 전체 줄」. */ +export function buildPager( + total: number, + size: number, + page: number, + goto: (page: number) => void, +): HTMLElement { + const last = Math.max(1, Math.ceil(total / size)); + const prev = createButton({ label: L("M01_Prev"), variant: "ghost", disabled: page <= 1 }); + const next = createButton({ label: L("M01_Next"), variant: "ghost", disabled: page >= last }); + prev.addEventListener("click", () => goto(page - 1)); + next.addEventListener("click", () => goto(page + 1)); + return el("div", { + className: "m01-master__pager", + children: [ + el("span", { + text: `${page} / ${last} ${L("M01_Page")} · ${total.toLocaleString("ko-KR")} ${L("M01_Rows")}`, + }), + prev, + next, + ], + }); +} + +/** 칸 하나 — 노랑(고침) · 눌러 고침. */ +export function buildCell( + text: string, + opts: { changed: boolean; onEdit?: (value: string) => void }, +): HTMLTableCellElement { + const td = el("td", { text, attrs: { title: text } }); + td.classList.toggle("is-changed", opts.changed); + if (opts.onEdit) { + const onEdit = opts.onEdit; + td.classList.add("is-editable"); + td.addEventListener("click", () => startEdit(td, text, onEdit)); + } + return td; +} diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Api.ts b/M01_MasterData/M01_MasterData_UI_Logic_Api.ts new file mode 100644 index 00000000..4f559904 --- /dev/null +++ b/M01_MasterData/M01_MasterData_UI_Logic_Api.ts @@ -0,0 +1,164 @@ +/* ============================================================================= + * M01_MasterData_UI_Logic_Api.ts + * 로직 화면이 부르는 서버 길 — 계약 `resources/master_data/_화면_계약.md` · `/api/m01` + * ========================================================================== */ + +import { API_BASE_URL } from "@config/config_frontend"; + +/** 로직 한 줄 — 파일 그대로(한글 칸) · 틀 `_틀.md` 6장 */ +export interface LogicInput { + 이름: string; + 단위?: string; + 고르기?: string[]; + 범위?: [number, number]; +} +export interface HoLine { + 종류: string; + 요소: string; + 이름?: string; + 규격?: string; + 단위?: string; + 수량: string; + 비목?: string; +} +export interface NamedFormula { + 이름: string; + 식: string; + 비목?: string; + 출처?: string; +} +export interface LogicRow { + 열쇠: string; + 이름: string; + 결과단위: string; + 출처: string; + 소유?: string; + 입력: LogicInput[]; + 중간: NamedFormula[]; + 호표?: HoLine[]; + 결과?: { 식: string }; + 덧줄?: NamedFormula[]; + 끝수?: string | null; + 비고?: string; + [extra: string]: unknown; +} + +export interface LogicSummary { + file: string; + book: string; + chapter: string; + 열쇠: string; + 이름: string; + 결과단위: string; + 출처: string; + blocked: boolean; + reasons: string[]; +} + +export interface ElementBrief { + ref: string; + file?: string; + 이름?: string; + 규격?: string; + 단위?: unknown; + 값?: unknown; + 값칸?: Record | null; +} + +export interface LogicOne { + file: string; + version: string; + logic: LogicRow; + blocked: boolean; + reasons: string[]; + prices: Record; +} + +export interface CalcLine { + 이름: string; + 단위: string; + 수량: number; + 단가: number; + 금액: number; + 비목: Record; +} +export type CalcAnswer = + | { + ok: true; + lines?: CalcLine[]; + sums?: Record; + result?: unknown; + middle: Record; + } + | { ok: false; reason: string }; + +export interface LogicFile { + file: string; + book: string; + chapter: string; + version: string; +} + +export interface SaveChange { + op: "edit" | "add" | "delete"; + key?: string; + row?: LogicRow; +} +export interface SaveFile { + file: string; + version: string; + changes: SaveChange[]; +} + +/** 서버가 준 몸과 상태 번호 — 409·422 는 몸(`detail`)을 화면이 풀어 보임 */ +export class ApiError extends Error { + constructor( + public status: number, + public detail: unknown, + ) { + super(typeof detail === "string" ? detail : JSON.stringify(detail)); + } +} + +async function request(path: string, body?: unknown): Promise { + const response = await fetch(`${API_BASE_URL}/m01${path}`, { + method: body === undefined ? "GET" : "POST", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: body === undefined ? undefined : JSON.stringify(body), + }); + const data = (await response.json().catch(() => ({}))) as { detail?: unknown } & T; + if (!response.ok) throw new ApiError(response.status, data.detail ?? response.statusText); + return data; +} + +const query = (params: Record): string => new URLSearchParams(params).toString(); + +export const fetchLogics = (): Promise => + request<{ logics: LogicSummary[] }>("/logics").then((d) => d.logics); + +export const fetchLogic = (book: string, key: string): Promise => + request(`/logic?${query({ book, key })}`); + +export const fetchLogicFiles = (): Promise => + request<{ files: LogicFile[] }>(`/groups/${encodeURIComponent("로직")}/files`).then( + (d) => d.files, + ); + +export const searchElements = ( + group: string, + q: string, +): Promise<{ total: number; items: ElementBrief[] }> => + request(`/elements?${query({ group, q, limit: "100" })}`); + +export const runCalc = (body: { + book: string; + key: string; + inputs: Record; + row?: LogicRow; + file?: string; +}): Promise => request("/calc", body); + +export const saveFiles = ( + files: SaveFile[], +): Promise<{ files: { file: string; version: string }[] }> => request("/save", { files }); diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts b/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts new file mode 100644 index 00000000..56bcb9b8 --- /dev/null +++ b/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts @@ -0,0 +1,164 @@ +/* ============================================================================= + * M01_MasterData_UI_Logic_Calc.ts + * 시험 계산 — 받을 값을 넣고 [계산] → 줄별 금액 · 비목 합 · 멈춘 까닭(엔진 글 그대로) + * 고친 로직은 저장 전 줄(`row`)을 같이 보내 메모리에서 셈(`POST /calc`) — 파일에 안 씀 + * ========================================================================== */ + +import { createButton, el, showToast } from "@ui/ui_template_elements"; +import { + runCalc, + type CalcAnswer, + type CalcLine, + type LogicRow, +} from "./M01_MasterData_UI_Logic_Api"; +import { formatNumber } from "./M01_MasterData_UI_Logic_Edit"; +import { tx } from "./M01_MasterData_UI_Logic_Text"; + +export interface CalcContext { + book: string; + /** 저장된 열쇠 — 새 로직은 null */ + savedKey: string | null; + file: string; + row: LogicRow; + dirty: () => boolean; + /** 로직마다 넣은 값 — 다시 그려도 남음 */ + values: Record; + onLines: (lines: CalcLine[] | null) => void; +} + +const SUMS = ["노무비", "재료비", "경비", "계"]; + +export function buildCalc(host: HTMLElement, ctx: CalcContext): void { + const fields = (ctx.row.입력 ?? []).map((spec) => { + const name = spec.이름; + let control: HTMLInputElement | HTMLSelectElement; + if (spec.고르기?.length) { + control = el("select", { className: "m01-logic__input" }); + for (const option of ["", ...spec.고르기]) { + control.append(el("option", { text: option, attrs: { value: option } })); + } + } else { + control = el("input", { + className: "m01-logic__input", + attrs: { type: "text", inputmode: "decimal" }, + }); + if (spec.범위) control.placeholder = `${spec.범위[0]} ∼ ${spec.범위[1]}`; + } + control.value = ctx.values[name] ?? ""; + control.addEventListener("input", () => (ctx.values[name] = control.value)); + control.addEventListener("change", () => (ctx.values[name] = control.value)); + const label = spec.단위 ? `${name} (${spec.단위})` : name; + return el("label", { + className: "m01-logic__field", + children: [el("span", { text: label }), control], + }); + }); + const out = el("div", { className: "m01-logic__calc-out" }); + const run = async (): Promise => { + const inputs: Record = {}; + for (const spec of ctx.row.입력 ?? []) { + const raw = (ctx.values[spec.이름] ?? "").trim(); + if (raw === "") continue; // 빈 칸은 안 보냄 — 엔진이 「입력 없음」 으로 멈춤 + inputs[spec.이름] = spec.고르기?.length || Number.isNaN(Number(raw)) ? raw : Number(raw); + } + const draft = ctx.dirty() || ctx.savedKey === null; + try { + const answer = await runCalc({ + book: ctx.book, + key: ctx.savedKey ?? ctx.row.열쇠, + inputs, + ...(draft ? { row: ctx.row, file: ctx.file } : {}), + }); + ctx.onLines(answer.ok ? (answer.lines ?? null) : null); + out.replaceChildren(...answerView(answer, draft)); + } catch (error) { + showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error"); + } + }; + host.replaceChildren( + el("h3", { text: tx("Calc_Title") }), + ...(fields.length + ? fields + : [el("p", { className: "m01-logic__muted", text: tx("Calc_NoInputs") })]), + createButton({ label: tx("Calc_Run"), onClick: () => void run() }), + out, + ); +} + +function answerView(answer: CalcAnswer, draft: boolean): HTMLElement[] { + const note = draft ? [el("p", { className: "m01-logic__muted", text: tx("Calc_Draft") })] : []; + if (!answer.ok) { + return [ + ...note, + el("div", { + className: "m01-logic__reasons", + children: [el("strong", { text: tx("Calc_Stopped") }), el("div", { text: answer.reason })], + }), + ]; + } + const parts: HTMLElement[] = [...note]; + if (answer.lines) { + const rows = answer.lines.map((line) => + el("tr", { + children: [ + el("td", { text: line.이름 }), + el("td", { text: `${formatNumber(line.수량)} ${line.단위}` }), + el("td", { className: "m01-logic__money", text: formatNumber(line.금액) }), + ], + }), + ); + parts.push( + el("table", { + className: "m01-logic__grid", + children: [ + el("thead", { + children: [ + el("tr", { + children: [tx("Head_Name"), tx("Ho_Qty"), tx("Ho_Amount")].map((h) => + el("th", { text: h }), + ), + }), + ], + }), + el("tbody", { children: rows }), + ], + }), + ); + } + if (answer.sums) { + parts.push( + el("dl", { + className: "m01-logic__sums", + children: SUMS.flatMap((k) => [ + el("dt", { text: k === "계" ? tx("Calc_Sum") : k }), + el("dd", { className: "m01-logic__money", text: formatNumber(answer.sums?.[k] ?? 0) }), + ]), + }), + ); + } + if (answer.result !== undefined) { + parts.push( + el("dl", { + className: "m01-logic__sums", + children: [ + el("dt", { text: tx("Calc_Result") }), + el("dd", { text: formatNumber(answer.result) }), + ], + }), + ); + } + const middle = Object.entries(answer.middle ?? {}); + if (middle.length) { + parts.push( + el("h4", { text: tx("Middle_Title") }), + el("dl", { + className: "m01-logic__sums", + children: middle.flatMap(([k, v]) => [ + el("dt", { text: k }), + el("dd", { text: formatNumber(v) }), + ]), + }), + ); + } + return parts; +} diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Edit.ts b/M01_MasterData/M01_MasterData_UI_Logic_Edit.ts new file mode 100644 index 00000000..38fd879f --- /dev/null +++ b/M01_MasterData/M01_MasterData_UI_Logic_Edit.ts @@ -0,0 +1,423 @@ +/* ============================================================================= + * M01_MasterData_UI_Logic_Edit.ts + * 로직 한 줄 고치기 — 실무 일위대가표(호표) 모양 + * 머리(이름·결과단위·출처·소유) · 설계에서 받을 값 · 호표 줄 · 중간 값 · 결과 식 · 덧줄 · 끝수 + * + * 칸을 고치면 `row` 를 그 자리에서 바꾸고 `onChange` 만 부름(다시 그리지 않음). + * 줄 더하기·지우기 · 요소 고르기처럼 모양이 바뀔 때만 `rerender`. + * ========================================================================== */ + +import { createButton, el } from "@ui/ui_template_elements"; +import type { + CalcLine, + ElementBrief, + HoLine, + LogicInput, + LogicRow, + NamedFormula, +} from "./M01_MasterData_UI_Logic_Api"; +import { tx } from "./M01_MasterData_UI_Logic_Text"; + +export type PickMode = "element" | "table"; +export type PickDone = (item: ElementBrief, column?: string) => void; + +export interface EditContext { + row: LogicRow; + file: string; + isNew: boolean; + files: string[]; + reasons: string[]; + prices: Record; + /** 마지막 시험 계산의 줄 — 호표 차례와 같음(덧줄은 뒤에 붙음) */ + lines: CalcLine[] | null; + onChange: () => void; + onFile: (file: string) => void; + onPick: (mode: PickMode, group: string, done: PickDone) => void; +} + +const KINDS = ["인력", "재료", "기계", "로직"]; +const COSTS = ["노무비", "재료비", "경비"]; +const COST_OF: Record = { 인력: "노무비", 재료: "재료비", 기계: "경비" }; + +export function formatNumber(value: unknown): string { + if (typeof value === "number") return value.toLocaleString("ko-KR", { maximumFractionDigits: 4 }); + if (value === null || value === undefined) return ""; + return typeof value === "object" ? JSON.stringify(value) : String(value); +} + +function textBox(value: unknown, onInput: (v: string) => void, className = ""): HTMLInputElement { + const input = el("input", { + className: `m01-logic__input ${className}`, + attrs: { type: "text" }, + }); + input.value = value === null || value === undefined ? "" : String(value); + input.addEventListener("input", () => onInput(input.value)); + return input; +} + +function choice( + options: string[], + value: string | undefined, + onPick: (v: string) => void, +): HTMLSelectElement { + const select = el("select", { className: "m01-logic__input" }); + for (const option of value && !options.includes(value) ? [value, ...options] : options) { + select.append(el("option", { text: option, attrs: { value: option } })); + } + select.value = value ?? options[0]; + select.addEventListener("change", () => onPick(select.value)); + return select; +} + +function dropButton(onClick: () => void): HTMLButtonElement { + const button = el("button", { + className: "m01-logic__drop", + text: "×", + attrs: { type: "button", title: tx("DeleteRow") }, + }); + button.addEventListener("click", onClick); + return button; +} + +function section(title: string, body: HTMLElement, onAdd?: () => void): HTMLElement { + const head = el("div", { + className: "m01-logic__section-head", + children: [el("h3", { text: title })], + }); + if (onAdd) head.append(createButton({ label: tx("AddRow"), variant: "ghost", onClick: onAdd })); + return el("section", { className: "m01-logic__section", children: [head, body] }); +} + +function grid(headers: string[], rows: HTMLElement[][], className: string): HTMLElement { + const table = el("table", { className: `m01-logic__grid ${className}` }); + table.append( + el("thead", { children: [el("tr", { children: headers.map((h) => el("th", { text: h })) })] }), + ); + const body = el("tbody"); + for (const cells of rows) + body.append(el("tr", { children: cells.map((c) => el("td", { children: [c] })) })); + table.append(body); + return el("div", { className: "m01-logic__scroll", children: [table] }); +} + +export function qtyKind(qty: string): string { + const text = qty.trim(); + if (/^-?\d+(\.\d+)?$/.test(text)) return tx("Ho_QtyNumber"); + if (text.startsWith("찾기(")) return tx("Ho_QtyTable"); + return tx("Ho_QtyFormula"); +} + +export function buildEditor(host: HTMLElement, ctx: EditContext): void { + const rerender = (): void => { + ctx.onChange(); + buildEditor(host, ctx); + }; + const row = ctx.row; + const set = (patch: () => void): void => { + patch(); + ctx.onChange(); + }; + host.innerHTML = ""; + host.append(head(ctx, set, rerender)); + if (ctx.reasons.length) { + host.append( + el("div", { + className: "m01-logic__reasons", + children: [ + el("strong", { text: tx("Head_Blocked") }), + ...ctx.reasons.map((r) => el("div", { text: r })), + ], + }), + ); + } + host.append(inputsSection(row, set, rerender)); + const money = !("결과" in row) && (row.결과단위 ?? "").startsWith("원"); + if (money) host.append(hoSection(ctx, set, rerender)); + host.append(middleSection(row, set, rerender)); + if (!money) { + const formula = textBox(row.결과?.식 ?? "", (v) => + set(() => { + row.결과 = { 식: v }; + }), + ); + host.append(section(tx("Result_Title"), formula)); + } else { + host.append(extraSection(row, set, rerender)); + const rounding = textBox(row.끝수 ?? "", (v) => + set(() => { + row.끝수 = v.trim() ? v : null; + }), + ); + host.append(section(tx("Rounding"), rounding)); + } +} + +function field(label: string, control: HTMLElement, wide = false): HTMLElement { + return el("label", { + className: `m01-logic__field${wide ? " m01-logic__field--wide" : ""}`, + children: [el("span", { text: label }), control], + }); +} + +function head(ctx: EditContext, set: (p: () => void) => void, rerender: () => void): HTMLElement { + const row = ctx.row; + const file = ctx.isNew + ? choice(ctx.files, ctx.file, (v) => ctx.onFile(v)) + : el("span", { className: "m01-logic__file", text: ctx.file }); + const unit = textBox(row.결과단위, (v) => set(() => (row.결과단위 = v))); + unit.addEventListener("change", rerender); // 돈 로직 ↔ 돈 아닌 로직 칸이 바뀜 + const note = el("textarea", { className: "m01-logic__input m01-logic__note" }); + note.value = row.비고 ?? ""; + note.addEventListener("input", () => + set(() => { + if (note.value.trim()) row.비고 = note.value; + else delete row.비고; + }), + ); + return el("div", { + className: "m01-logic__head", + children: [ + field(tx("Head_File"), file, true), + field( + tx("Head_Key"), + textBox(row.열쇠, (v) => set(() => (row.열쇠 = v))), + ), + field( + tx("Head_Name"), + textBox(row.이름, (v) => set(() => (row.이름 = v))), + ), + field(tx("Head_Unit"), unit), + field( + tx("Head_Source"), + textBox(row.출처, (v) => set(() => (row.출처 = v))), + ), + field( + tx("Head_Owner"), + choice(["공용", "개인"], row.소유, (v) => set(() => (row.소유 = v))), + ), + field(tx("Head_Note"), note, true), + ], + }); +} + +function inputsSection( + row: LogicRow, + set: (p: () => void) => void, + rerender: () => void, +): HTMLElement { + const list = row.입력 ?? []; + const rows = list.map((spec: LogicInput, i) => { + const low = textBox(spec.범위?.[0] ?? "", () => range(), "m01-logic__num"); + const high = textBox(spec.범위?.[1] ?? "", () => range(), "m01-logic__num"); + const range = (): void => + set(() => { + if (low.value.trim() === "" && high.value.trim() === "") delete spec.범위; + else spec.범위 = [Number(low.value), Number(high.value)]; + }); + return [ + textBox(spec.이름, (v) => set(() => (spec.이름 = v))), + textBox(spec.단위 ?? "", (v) => + set(() => { + if (v.trim()) spec.단위 = v; + else delete spec.단위; + }), + ), + textBox((spec.고르기 ?? []).join(", "), (v) => + set(() => { + const items = v + .split(",") + .map((x) => x.trim()) + .filter(Boolean); + if (items.length) spec.고르기 = items; + else delete spec.고르기; + }), + ), + el("div", { + className: "m01-logic__range", + children: [low, el("span", { text: "∼" }), high], + }), + dropButton(() => { + list.splice(i, 1); + rerender(); + }), + ]; + }); + const headers = [ + tx("Head_Name"), + tx("Inputs_Unit"), + tx("Inputs_Choices"), + tx("Inputs_Range"), + "", + ]; + return section(tx("Inputs_Title"), grid(headers, rows, "m01-logic__grid--inputs"), () => { + (row.입력 ??= []).push({ 이름: "" }); + rerender(); + }); +} + +function priceCell(ctx: EditContext, item: HoLine, i: number): HTMLElement { + const line = ctx.lines?.[i]; + if (line) return el("span", { className: "m01-logic__money", text: formatNumber(line.단가) }); + if (item.종류 === "로직" || item.요소.includes("{")) { + return el("span", { className: "m01-logic__muted", text: tx("Ho_PriceLater") }); + } + const brief = ctx.prices[item.요소]; + if (brief === undefined) return el("span", { className: "m01-logic__muted", text: "" }); + if (brief === null) return el("span", { className: "m01-logic__bad", text: tx("Ho_Missing") }); + return el("span", { className: "m01-logic__money", text: formatNumber(brief.값) }); +} + +function hoSection( + ctx: EditContext, + set: (p: () => void) => void, + rerender: () => void, +): HTMLElement { + const list = ctx.row.호표 ?? []; + const rows = list.map((item: HoLine, i) => { + const element = textBox(item.요소, (v) => set(() => (item.요소 = v)), "m01-logic__ref"); + const pick = createButton({ + label: tx("Ho_Pick"), + variant: "ghost", + onClick: () => + ctx.onPick("element", item.종류, (found) => { + const [group, book] = found.ref.split(":"); + const key = found.ref.slice(group.length + book.length + 2); + item.요소 = group === "로직" ? `로직(${book}:${key})` : found.ref; + if (KINDS.includes(group)) item.종류 = group; + item.이름 = found.이름 ?? item.이름; + if (found.규격) item.규격 = found.규격; + else delete item.규격; + if (!item.비목 && COST_OF[item.종류]) item.비목 = COST_OF[item.종류]; + if (group !== "로직") ctx.prices[found.ref] = found; + rerender(); + }), + }); + const brief = ctx.prices[item.요소]; + const spec = item.규격 ?? brief?.규격 ?? ""; + const qty = textBox(item.수량, (v) => { + set(() => (item.수량 = v)); + tag.textContent = qtyKind(v); + }); + const tag = el("span", { className: "m01-logic__tag", text: qtyKind(item.수량) }); + const table = createButton({ + label: tx("Ho_PickTable"), + variant: "ghost", + onClick: () => + ctx.onPick("table", "소요량", (found, column) => { + const ref = found.ref; + item.수량 = `찾기(${ref})${column ? `.${column}` : ""}`; + rerender(); + }), + }); + const cost = + item.종류 === "로직" && !item.비목 + ? choice(["", ...COSTS], "", (v) => set(() => (item.비목 = v || undefined))) + : choice(COSTS, item.비목, (v) => set(() => (item.비목 = v))); + return [ + choice(KINDS, item.종류, (v) => { + item.종류 = v; + rerender(); + }), + el("div", { className: "m01-logic__pair", children: [element, pick] }), + el("div", { + className: "m01-logic__stack", + children: [ + textBox(item.이름 ?? "", (v) => set(() => (item.이름 = v))), + el("span", { className: "m01-logic__muted", text: spec }), + ], + }), + textBox(item.단위 ?? "", (v) => set(() => (item.단위 = v)), "m01-logic__unit"), + el("div", { className: "m01-logic__pair", children: [tag, qty, table] }), + priceCell(ctx, item, i), + el("span", { className: "m01-logic__money", text: formatNumber(ctx.lines?.[i]?.금액 ?? "") }), + cost, + dropButton(() => { + list.splice(i, 1); + rerender(); + }), + ]; + }); + const headers = [ + tx("Ho_Kind"), + tx("Ho_Element"), + tx("Ho_Name"), + tx("Ho_Unit"), + tx("Ho_Qty"), + tx("Ho_Price"), + tx("Ho_Amount"), + tx("Ho_Cost"), + "", + ]; + return section(tx("Ho_Title"), grid(headers, rows, "m01-logic__grid--ho"), () => { + (ctx.row.호표 ??= []).push({ + 종류: "인력", + 요소: "", + 이름: "", + 단위: "인", + 수량: "", + 비목: "노무비", + }); + rerender(); + }); +} + +function formulaRows( + list: NamedFormula[], + set: (p: () => void) => void, + rerender: () => void, + withCost: boolean, +): HTMLElement[][] { + return list.map((item, i) => { + const cells: HTMLElement[] = [ + textBox(item.이름, (v) => set(() => (item.이름 = v))), + textBox(item.식, (v) => set(() => (item.식 = v)), "m01-logic__formula"), + ]; + if (withCost) { + cells.push( + choice(COSTS, item.비목, (v) => set(() => (item.비목 = v))), + textBox(item.출처 ?? "", (v) => set(() => (item.출처 = v))), + ); + } + cells.push( + dropButton(() => { + list.splice(i, 1); + rerender(); + }), + ); + return cells; + }); +} + +function middleSection( + row: LogicRow, + set: (p: () => void) => void, + rerender: () => void, +): HTMLElement { + const list = row.중간 ?? []; + const table = grid( + [tx("Head_Name"), tx("Formula"), ""], + formulaRows(list, set, rerender, false), + "", + ); + return section(tx("Middle_Title"), table, () => { + (row.중간 ??= []).push({ 이름: "", 식: "" }); + rerender(); + }); +} + +function extraSection( + row: LogicRow, + set: (p: () => void) => void, + rerender: () => void, +): HTMLElement { + const list = row.덧줄 ?? []; + const headers = [tx("Head_Name"), tx("Formula"), tx("Ho_Cost"), tx("Head_Source"), ""]; + return section( + tx("Extra_Title"), + grid(headers, formulaRows(list, set, rerender, true), ""), + () => { + (row.덧줄 ??= []).push({ 이름: "", 식: "", 비목: "재료비", 출처: "" }); + rerender(); + }, + ); +} diff --git a/M01_MasterData/M01_MasterData_UI_Logic_List.ts b/M01_MasterData/M01_MasterData_UI_Logic_List.ts new file mode 100644 index 00000000..738436e0 --- /dev/null +++ b/M01_MasterData/M01_MasterData_UI_Logic_List.ts @@ -0,0 +1,155 @@ +/* ============================================================================= + * M01_MasterData_UI_Logic_List.ts + * 왼쪽 로직 목록 — 원문 · 장 · 이름 찾기 · 막힘만 · 막힘(⛔)·고침(●) 표시 + * 거르기는 화면에서(목록은 한 번 받음 · 391 줄 남짓) + * ========================================================================== */ + +import { el } from "@ui/ui_template_elements"; +import type { LogicSummary } from "./M01_MasterData_UI_Logic_Api"; +import { tx } from "./M01_MasterData_UI_Logic_Text"; + +export type ListMark = "edited" | "new" | "deleted"; + +export interface ListItem { + id: string; + book: string; + chapter: string; + key: string; + name: string; + blocked: boolean; + reasons: string[]; +} + +export interface ListHandle { + root: HTMLElement; + setItems: (items: LogicSummary[]) => void; + /** 저장 안 한 새 로직도 목록 맨 위에 */ + setMarks: (marks: Map, extra: ListItem[]) => void; + setActive: (id: string | null) => void; +} + +export const logicId = (book: string, key: string): string => `${book}\n${key}`; + +export function buildList(onOpen: (item: ListItem) => void): ListHandle { + let items: ListItem[] = []; + let extra: ListItem[] = []; + let marks = new Map(); + let active: string | null = null; + const book = el("select", { className: "m01-logic__input" }); + const chapter = el("select", { className: "m01-logic__input" }); + const search = el("input", { + className: "m01-logic__input", + attrs: { type: "search", placeholder: tx("List_Search") }, + }); + const blockedOnly = el("input", { attrs: { type: "checkbox" } }); + const count = el("span", { className: "m01-logic__muted" }); + const list = el("ul", { className: "m01-logic__list" }); + + const options = (select: HTMLSelectElement, all: string, values: string[]): void => { + const keep = select.value; + select.replaceChildren( + el("option", { text: all, attrs: { value: "" } }), + ...values.map((v) => el("option", { text: v, attrs: { value: v } })), + ); + select.value = values.includes(keep) ? keep : ""; + }; + const chapters = (): void => + options(chapter, tx("List_AllChapters"), [ + ...new Set(items.filter((x) => !book.value || x.book === book.value).map((x) => x.chapter)), + ]); + + const draw = (): void => { + const q = search.value.trim().toLowerCase(); + const shown = [...extra, ...items].filter( + (x) => + (!book.value || x.book === book.value) && + (!chapter.value || x.chapter === chapter.value) && + (!blockedOnly.checked || x.blocked) && + (!q || x.key.toLowerCase().includes(q) || x.name.toLowerCase().includes(q)), + ); + count.textContent = tx("List_Count", { n: shown.length }); + list.replaceChildren(...shown.slice(0, 500).map(line)); + }; + const line = (item: ListItem): HTMLElement => { + const mark = marks.get(item.id); + const badges: HTMLElement[] = []; + if (item.blocked) { + badges.push( + el("span", { + className: "m01-logic__badge m01-logic__badge--blocked", + text: `⛔ ${tx("List_Blocked")}`, + attrs: { title: item.reasons.join("\n") }, + }), + ); + } + if (mark) { + const label = { edited: "List_Edited", new: "List_New", deleted: "List_Deleted" } as const; + badges.push(el("span", { className: "m01-logic__badge", text: `● ${tx(label[mark])}` })); + } + const button = el("button", { + className: `m01-logic__item${item.id === active ? " is-active" : ""}`, + attrs: { type: "button", title: item.key }, + children: [ + el("span", { className: "m01-logic__muted", text: `${item.book} ${item.chapter}` }), + el("span", { text: item.name || item.key }), + el("span", { className: "m01-logic__badges", children: badges }), + ], + }); + button.addEventListener("click", () => onOpen(item)); + return el("li", { children: [button] }); + }; + + book.addEventListener("change", () => { + chapters(); + draw(); + }); + chapter.addEventListener("change", draw); + search.addEventListener("input", draw); + blockedOnly.addEventListener("change", draw); + + const root = el("aside", { + className: "m01-logic__side", + children: [ + el("div", { + className: "m01-logic__filters", + children: [ + book, + chapter, + search, + el("label", { + className: "m01-logic__check", + children: [blockedOnly, el("span", { text: tx("List_BlockedOnly") })], + }), + count, + ], + }), + list, + ], + }); + return { + root, + setItems: (logics) => { + items = logics.map((x) => ({ + id: logicId(x.book, x.열쇠), + book: x.book, + chapter: x.chapter, + key: x.열쇠, + name: x.이름, + blocked: x.blocked, + reasons: x.reasons, + })); + options(book, tx("List_AllBooks"), [...new Set(items.map((x) => x.book))]); + chapters(); + draw(); + }, + setMarks: (next, more) => { + marks = next; + extra = more; + draw(); + }, + setActive: (id) => { + active = id; + draw(); + }, + }; +} diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Page.ts b/M01_MasterData/M01_MasterData_UI_Logic_Page.ts new file mode 100644 index 00000000..78ba6bc9 --- /dev/null +++ b/M01_MasterData/M01_MasterData_UI_Logic_Page.ts @@ -0,0 +1,360 @@ +/* ============================================================================= + * M01_MasterData_UI_Logic_Page.ts + * 관리자 화면 — 로직 칸. 왼쪽 목록 / 가운데 일위대가표(호표) 고치기 / 오른쪽 시험 계산 + * 진입: 공용 진입 파일이 `mountM01Logic(host)` 한 줄로 붙임(요소 칸은 sub_laptop_3 몫) + * + * 데이터 흐름(CLAUDE.md 5장) — 고친 것은 캐시(sessionStorage)에 쌓고 [저장] 한 번에 `POST /save`. + * 409 = 그 사이 파일이 바뀜 · 422 = 검사 걸림(아무것도 안 씀) — 까닭은 서버 글 그대로 보임. + * ========================================================================== */ + +import { + createButton, + el, + hideLoadingOverlay, + showConfirmDialog, + showLoadingOverlay, + showToast, +} from "@ui/ui_template_elements"; +import { + ApiError, + fetchLogic, + fetchLogicFiles, + fetchLogics, + saveFiles, + type CalcLine, + type ElementBrief, + type LogicFile, + type LogicRow, + type SaveFile, +} from "./M01_MasterData_UI_Logic_Api"; +import { buildCalc } from "./M01_MasterData_UI_Logic_Calc"; +import { buildEditor } from "./M01_MasterData_UI_Logic_Edit"; +import { buildList, logicId, type ListItem, type ListMark } from "./M01_MasterData_UI_Logic_List"; +import { openPicker } from "./M01_MasterData_UI_Logic_Pick"; +import { tx } from "./M01_MasterData_UI_Logic_Text"; +import "./M01_MasterData_UI_Logic_Style.css"; + +/** 저장 안 한 로직 하나 — origKey null = 새 로직 · row null = 지움 */ +interface Draft { + book: string; + file: string; + version: string; + origKey: string | null; + row: LogicRow | null; +} + +interface Opened extends Draft { + id: string; + row: LogicRow | null; + original: string | null; + prices: Record; + reasons: string[]; + lines: CalcLine[] | null; + values: Record; +} + +const CACHE_KEY = "m01_logic_drafts"; +const clone = (v: T): T => JSON.parse(JSON.stringify(v)) as T; + +function loadDrafts(): Record { + try { + return JSON.parse(sessionStorage.getItem(CACHE_KEY) ?? "{}") as Record; + } catch { + return {}; + } +} + +export async function mountM01Logic(host: HTMLElement): Promise { + let drafts = loadDrafts(); + let files: LogicFile[] = []; + let opened: Opened | null = null; + let calcInputs = ""; + + const editor = el("div", { className: "m01-logic__editor" }); + const errors = el("div", { className: "m01-logic__reasons", attrs: { hidden: "" } }); + const calc = el("aside", { className: "m01-logic__calc" }); + const pending = el("span", { className: "m01-logic__muted" }); + const list = buildList((item) => void open(item.id, item.book, item.key)); + + const persist = (): void => { + try { + sessionStorage.setItem(CACHE_KEY, JSON.stringify(drafts)); + } catch { + /* 캐시가 막혀도 화면 안의 고친 것은 남음 */ + } + const marks = new Map(); + const extra: ListItem[] = []; + for (const [id, d] of Object.entries(drafts)) { + marks.set(id, d.origKey === null ? "new" : d.row === null ? "deleted" : "edited"); + if (d.origKey === null && d.row) { + extra.push({ + id, + book: d.book, + chapter: "", + key: d.row.열쇠, + name: d.row.이름, + blocked: false, + reasons: [], + }); + } + } + list.setMarks(marks, extra); + const n = Object.keys(drafts).length; + pending.textContent = n ? tx("Bar_Pending", { n }) : ""; + saveButton.disabled = discardButton.disabled = n === 0; + }; + + const touch = (): void => { + if (!opened?.row) return; + const now = JSON.stringify(opened.row); + if (opened.origKey !== null && now === opened.original) delete drafts[opened.id]; + else drafts[opened.id] = pick(opened, clone(opened.row)); + persist(); + const inputs = JSON.stringify(opened.row.입력 ?? []); + if (inputs !== calcInputs) drawCalc(); + }; + + const pick = (o: Opened, row: LogicRow | null): Draft => ({ + book: o.book, + file: o.file, + version: o.version, + origKey: o.origKey, + row, + }); + + const drawCalc = (): void => { + if (!opened?.row) { + calc.replaceChildren(); + return; + } + const current = opened; + calcInputs = JSON.stringify(current.row?.입력 ?? []); + buildCalc(calc, { + book: current.book, + savedKey: current.origKey, + file: current.file, + row: current.row as LogicRow, + dirty: () => current.id in drafts, + values: current.values, + onLines: (lines) => { + current.lines = lines; + drawEditor(); + }, + }); + }; + + const drawEditor = (): void => { + list.setActive(opened?.id ?? null); + if (!opened) { + editor.replaceChildren(el("p", { className: "m01-logic__empty", text: tx("Empty") })); + return; + } + if (!opened.row) { + editor.replaceChildren( + el("p", { className: "m01-logic__empty", text: `● ${tx("List_Deleted")}` }), + ); + return; + } + const current = opened; + buildEditor(editor, { + row: current.row as LogicRow, + file: current.file, + isNew: current.origKey === null, + files: files.map((f) => f.file), + reasons: current.reasons, + prices: current.prices, + lines: current.lines, + onChange: touch, + onFile: (file) => { + const f = files.find((x) => x.file === file); + if (!f) return; + Object.assign(current, { file: f.file, book: f.book, version: f.version }); + touch(); + }, + onPick: openPicker, + }); + }; + + const show = (next: Opened | null): void => { + opened = next; + errors.hidden = true; + drawEditor(); + drawCalc(); + }; + + const open = async (id: string, book: string, key: string): Promise => { + const draft = drafts[id]; + if (draft?.origKey === null) { + show({ ...draft, id, original: null, prices: {}, reasons: [], lines: null, values: {} }); + return; + } + try { + const one = await fetchLogic(book, key); + show({ + id, + book, + file: draft?.file ?? one.file, + version: draft?.version ?? one.version, + origKey: key, + row: draft ? (draft.row === null ? null : clone(draft.row)) : clone(one.logic), + original: JSON.stringify(one.logic), + prices: one.prices, + reasons: one.reasons, + lines: null, + values: {}, + }); + } catch (error) { + showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error"); + } + }; + + const reload = async (): Promise => { + const [logics, logicFiles] = await Promise.all([fetchLogics(), fetchLogicFiles()]); + files = logicFiles; + list.setItems(logics); + persist(); + }; + + const onNew = (): void => { + const f = files.find((x) => x.file === opened?.file) ?? files[0]; + if (!f) return; + const id = `new:${Date.now()}`; + const row: LogicRow = { + 열쇠: `${tx("New_Key")} ${Object.keys(drafts).length + 1}`, + 이름: "", + 결과단위: "원/", + 출처: "", + 소유: "공용", + 입력: [], + 중간: [], + 호표: [], + 덧줄: [], + 끝수: null, + }; + const next: Opened = { + id, + book: f.book, + file: f.file, + version: f.version, + origKey: null, + row, + original: null, + prices: {}, + reasons: [], + lines: null, + values: {}, + }; + drafts[id] = pick(next, clone(row)); + persist(); + show(next); + }; + + const onDelete = async (): Promise => { + if (!opened?.row) return; + if (!(await showConfirmDialog(tx("Confirm_Delete", { v: opened.row.열쇠 }), tx("Bar_Delete")))) + return; + if (opened.origKey === null) { + delete drafts[opened.id]; + persist(); + show(null); + return; + } + drafts[opened.id] = pick(opened, null); + opened.row = null; + persist(); + show(opened); + }; + + const onDiscard = async (): Promise => { + if (!(await showConfirmDialog(tx("Confirm_Discard"), tx("Bar_Discard")))) return; + drafts = {}; + persist(); + const was = opened; + if (was && was.origKey !== null) + await open(logicId(was.book, was.origKey), was.book, was.origKey); + else show(null); + }; + + const onSave = async (): Promise => { + const byFile = new Map(); + for (const d of Object.values(drafts)) { + const part = byFile.get(d.file) ?? { file: d.file, version: d.version, changes: [] }; + if (d.origKey === null) { + if (d.row) part.changes.push({ op: "add", row: d.row }); + } else if (d.row === null) part.changes.push({ op: "delete", key: d.origKey }); + else part.changes.push({ op: "edit", key: d.origKey, row: d.row }); + byFile.set(d.file, part); + } + if (!byFile.size) { + showToast(tx("Save_Nothing"), "info"); + return; + } + showLoadingOverlay(); + try { + await saveFiles([...byFile.values()]); + const was = opened; + drafts = {}; + await reload(); + showToast(tx("Save_Done"), "success"); + if (was?.row) await open(logicId(was.book, was.row.열쇠), was.book, was.row.열쇠); + else show(null); + } catch (error) { + failed(error); + } finally { + hideLoadingOverlay(); + } + }; + + const failed = (error: unknown): void => { + const detail = + error instanceof ApiError ? (error.detail as { stale?: string[]; errors?: string[] }) : null; + if (error instanceof ApiError && error.status === 409) { + showToast(tx("Save_Stale", { v: (detail?.stale ?? []).join(", ") }), "error"); + } else if (error instanceof ApiError && error.status === 422) { + errors.replaceChildren( + el("strong", { text: tx("Save_Errors") }), + ...(detail?.errors ?? []).map((x) => el("div", { text: x })), + ); + errors.hidden = false; + } else { + showToast(error instanceof Error ? error.message : tx("Save_Failed"), "error"); + } + }; + + const saveButton = createButton({ label: tx("Bar_Save"), onClick: () => void onSave() }); + const discardButton = createButton({ + label: tx("Bar_Discard"), + variant: "ghost", + onClick: () => void onDiscard(), + }); + const bar = el("div", { + className: "m01-logic__bar", + children: [ + el("h2", { text: tx("Title") }), + pending, + createButton({ label: tx("Bar_New"), variant: "ghost", onClick: onNew }), + createButton({ label: tx("Bar_Delete"), variant: "danger", onClick: () => void onDelete() }), + discardButton, + saveButton, + ], + }); + host.replaceChildren( + el("div", { + className: "m01-logic", + children: [ + list.root, + el("main", { className: "m01-logic__main", children: [bar, errors, editor] }), + calc, + ], + }), + ); + show(null); + showLoadingOverlay(); + try { + await reload(); + } catch (error) { + showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error"); + } finally { + hideLoadingOverlay(); + } +} diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Pick.ts b/M01_MasterData/M01_MasterData_UI_Logic_Pick.ts new file mode 100644 index 00000000..a49d6f7e --- /dev/null +++ b/M01_MasterData/M01_MasterData_UI_Logic_Pick.ts @@ -0,0 +1,109 @@ +/* ============================================================================= + * M01_MasterData_UI_Logic_Pick.ts + * 요소 찾기 창 — 그룹 전체에서 이름·열쇠로 찾아 호표 줄에 넣음(`GET /elements`) + * 표 고르기(소요량·계수)는 값칸 단추를 눌러 `찾기(…).칸` 으로 수량에 넣음 + * ========================================================================== */ + +import { createButton, el, showToast } from "@ui/ui_template_elements"; +import { searchElements, type ElementBrief } from "./M01_MasterData_UI_Logic_Api"; +import { formatNumber, type PickDone, type PickMode } from "./M01_MasterData_UI_Logic_Edit"; +import { tx } from "./M01_MasterData_UI_Logic_Text"; + +const GROUPS: Record = { + element: ["인력", "재료", "기계", "로직"], + table: ["소요량", "계수"], +}; + +export function openPicker(mode: PickMode, group: string, done: PickDone): void { + const groups = GROUPS[mode]; + const select = el("select", { className: "m01-logic__input" }); + for (const g of groups) select.append(el("option", { text: g, attrs: { value: g } })); + select.value = groups.includes(group) ? group : groups[0]; + const search = el("input", { + className: "m01-logic__input", + attrs: { type: "search", placeholder: tx("Pick_Search") }, + }); + const count = el("span", { className: "m01-logic__muted" }); + const list = el("div", { className: "m01-logic__pick-list" }); + const close = (): void => backdrop.remove(); + const dialog = el("div", { + className: "m01-logic__pick", + attrs: { role: "dialog", "aria-label": tx("Pick_Title") }, + children: [ + el("div", { + className: "m01-logic__section-head", + children: [ + el("h3", { text: tx("Pick_Title") }), + createButton({ label: tx("Pick_Close"), variant: "ghost", onClick: close }), + ], + }), + el("div", { className: "m01-logic__pick-bar", children: [select, search, count] }), + list, + ], + }); + const backdrop = el("div", { className: "m01-logic__backdrop", children: [dialog] }); + backdrop.addEventListener("click", (ev) => ev.target === backdrop && close()); + backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && close()); + + const choose = (item: ElementBrief, column?: string): void => { + close(); + done(item, column); + }; + let seq = 0; + const load = async (): Promise => { + const mine = ++seq; + try { + const found = await searchElements(select.value, search.value.trim()); + if (mine !== seq) return; // 최신 응답만 + count.textContent = tx("Pick_Total", { n: found.total }); + list.replaceChildren(...found.items.map((item) => itemRow(item, mode, choose))); + } catch (error) { + showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error"); + } + }; + let timer = 0; + search.addEventListener("input", () => { + window.clearTimeout(timer); + timer = window.setTimeout(load, 250); + }); + select.addEventListener("change", load); + document.body.append(backdrop); + search.focus(); + void load(); +} + +function itemRow( + item: ElementBrief, + mode: PickMode, + choose: (item: ElementBrief, column?: string) => void, +): HTMLElement { + const text = el("div", { + className: "m01-logic__stack", + children: [ + el("strong", { text: `${item.이름 ?? ""} ${item.규격 ?? ""}`.trim() }), + el("span", { className: "m01-logic__muted", text: item.ref }), + ], + }); + if (mode === "table") { + const columns = Object.keys(item.값칸 ?? {}); + const chips = columns.map((column) => + createButton({ label: column, variant: "pill", onClick: () => choose(item, column) }), + ); + return el("div", { + className: "m01-logic__pick-row", + children: [text, el("div", { className: "m01-logic__chips", children: chips })], + }); + } + const unit = typeof item.단위 === "string" ? item.단위 : ""; + const value = typeof item.값 === "object" && item.값 !== null ? "{…}" : formatNumber(item.값); + const row = el("button", { + className: "m01-logic__pick-row m01-logic__pick-row--button", + attrs: { type: "button" }, + children: [ + text, + el("span", { className: "m01-logic__money", text: `${value} ${unit}`.trim() }), + ], + }); + row.addEventListener("click", () => choose(item)); + return row; +} diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Style.css b/M01_MasterData/M01_MasterData_UI_Logic_Style.css new file mode 100644 index 00000000..0bbf5961 --- /dev/null +++ b/M01_MasterData/M01_MasterData_UI_Logic_Style.css @@ -0,0 +1,388 @@ +/* M01 로직 칸 — 왼쪽 목록 / 가운데 일위대가표 / 오른쪽 시험 계산 */ +.m01-logic [hidden] { + display: none !important; +} + +.m01-logic { + display: grid; + grid-template-columns: 240px minmax(0, 1fr) 280px; + gap: var(--spacing-12); + height: 100%; + min-height: 0; + padding: var(--spacing-12); + box-sizing: border-box; + color: var(--color-text-body); + font-size: var(--text-body-sm); +} + +.m01-logic__side, +.m01-logic__main, +.m01-logic__calc { + display: flex; + flex-direction: column; + gap: var(--spacing-8); + min-height: 0; + overflow: auto; +} + +.m01-logic__side, +.m01-logic__calc { + padding: var(--spacing-8); + border-radius: var(--radius-cards); + background: var(--color-surface); +} + +.m01-logic__filters { + display: flex; + flex-direction: column; + gap: var(--spacing-4); +} + +.m01-logic__check { + display: flex; + align-items: center; + gap: var(--spacing-4); +} + +.m01-logic__list { + margin: 0; + padding: 0; + list-style: none; + overflow: auto; +} + +.m01-logic__item { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2px; + width: 100%; + padding: var(--spacing-4) var(--spacing-8); + border: 0; + border-radius: var(--radius-buttons); + background: none; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} + +.m01-logic__item:hover { + background: var(--color-paper); +} + +.m01-logic__item.is-active { + background: var(--color-mist-violet); + color: var(--color-accent); +} + +.m01-logic__badges { + display: flex; + gap: var(--spacing-4); +} + +.m01-logic__badge { + font-size: var(--text-caption); + color: var(--color-warning); +} + +.m01-logic__badge--blocked { + color: var(--color-danger); +} + +.m01-logic__bar { + display: flex; + align-items: center; + gap: var(--spacing-8); + flex-wrap: wrap; +} + +.m01-logic__bar h2 { + margin: 0 auto 0 0; + font-size: 18px; + color: var(--color-text); +} + +.m01-logic__empty { + color: var(--color-text-muted); +} + +.m01-logic__head { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: var(--spacing-8); +} + +.m01-logic__field { + display: flex; + flex-direction: column; + gap: 2px; + color: var(--color-text-secondary); + font-size: var(--text-caption); +} + +.m01-logic__field--wide { + grid-column: 1 / -1; +} + +.m01-logic__file { + color: var(--color-text-body); + font-size: var(--text-body-sm); +} + +.m01-logic__input { + box-sizing: border-box; + width: 100%; + min-width: 0; + padding: 3px 6px; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + background: var(--color-canvas); + color: var(--color-text-body); + font: inherit; + font-size: var(--text-body-sm); +} + +.m01-logic__note { + min-height: 48px; + resize: vertical; +} + +.m01-logic__section { + display: flex; + flex-direction: column; + gap: var(--spacing-4); +} + +.m01-logic__section-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--spacing-8); +} + +.m01-logic__section-head h3, +.m01-logic__calc h3 { + margin: 0; + font-size: var(--text-body-sm); + color: var(--color-text); +} + +.m01-logic__grid { + width: 100%; + border-collapse: collapse; +} + +.m01-logic__grid th, +.m01-logic__grid td { + padding: 2px 4px; + border-bottom: 1px solid var(--color-border); + vertical-align: top; + text-align: left; +} + +.m01-logic__grid th { + color: var(--color-text-secondary); + font-size: var(--text-caption); + font-weight: 600; +} + +.m01-logic__grid th, +.m01-logic__grid .ui-btn { + white-space: nowrap; +} + +.m01-logic__scroll { + overflow-x: auto; +} + +.m01-logic__grid--ho { + min-width: 1124px; + table-layout: fixed; +} + +.m01-logic__grid--ho th:nth-child(1) { + width: 76px; +} + +.m01-logic__grid--ho th:nth-child(2) { + width: 220px; +} + +.m01-logic__grid--ho th:nth-child(3) { + width: 150px; +} + +.m01-logic__grid--ho th:nth-child(4) { + width: 56px; +} + +.m01-logic__grid--ho th:nth-child(5) { + width: 300px; +} + +.m01-logic__grid--ho th:nth-child(6), +.m01-logic__grid--ho th:nth-child(7) { + width: 100px; +} + +.m01-logic__grid--ho th:nth-child(8) { + width: 90px; +} + +.m01-logic__grid--ho th:nth-child(9) { + width: 32px; +} + +.m01-logic__pair { + display: flex; + align-items: center; + gap: var(--spacing-4); +} + +.m01-logic__stack { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.m01-logic__range { + display: flex; + align-items: center; + gap: 2px; +} + +.m01-logic__num { + width: 64px; +} + +.m01-logic__tag { + flex: none; + padding: 0 6px; + border-radius: var(--radius-pills); + background: var(--color-mist-violet); + color: var(--color-accent); + font-size: var(--text-caption); +} + +.m01-logic__money { + display: block; + text-align: right; + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.m01-logic__muted { + color: var(--color-text-muted); + font-size: var(--text-caption); +} + +.m01-logic__bad { + color: var(--color-danger); + font-size: var(--text-caption); +} + +.m01-logic__drop { + border: 0; + background: none; + color: var(--color-text-muted); + font-size: 16px; + cursor: pointer; +} + +.m01-logic__drop:hover { + color: var(--color-danger); +} + +.m01-logic__reasons { + padding: var(--spacing-8); + border-left: 3px solid var(--color-danger); + border-radius: var(--radius-sm); + background: var(--color-canvas); + color: var(--color-danger); +} + +.m01-logic__sums { + display: grid; + grid-template-columns: auto 1fr; + gap: 2px var(--spacing-8); + margin: 0; +} + +.m01-logic__sums dt { + color: var(--color-text-secondary); +} + +.m01-logic__sums dd { + margin: 0; + text-align: right; +} + +.m01-logic__calc-out { + display: flex; + flex-direction: column; + gap: var(--spacing-8); +} + +.m01-logic__backdrop { + position: fixed; + inset: 0; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + background: color-mix(in srgb, var(--color-ink) 30%, transparent); +} + +.m01-logic__pick { + display: flex; + flex-direction: column; + gap: var(--spacing-8); + width: min(720px, 92vw); + max-height: 80vh; + padding: var(--spacing-16); + border-radius: var(--radius-xl); + background: var(--color-surface-raised); + font-size: var(--text-body-sm); +} + +.m01-logic__pick-bar { + display: grid; + grid-template-columns: 120px 1fr auto; + align-items: center; + gap: var(--spacing-8); +} + +.m01-logic__pick-list { + overflow: auto; +} + +.m01-logic__pick-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--spacing-8); + width: 100%; + padding: var(--spacing-4) var(--spacing-8); + border: 0; + border-bottom: 1px solid var(--color-border); + background: none; + color: inherit; + font: inherit; + text-align: left; +} + +.m01-logic__pick-row--button { + cursor: pointer; +} + +.m01-logic__pick-row--button:hover { + background: var(--color-paper); +} + +.m01-logic__chips { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-4); +} diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Text.ts b/M01_MasterData/M01_MasterData_UI_Logic_Text.ts new file mode 100644 index 00000000..05020d41 --- /dev/null +++ b/M01_MasterData/M01_MasterData_UI_Logic_Text.ts @@ -0,0 +1,99 @@ +/* ============================================================================= + * M01_MasterData_UI_Logic_Text.ts + * 로직 화면 글자 — [한국어, 영어] · 공용 사전과 같은 모양 · 창끼리 공용 사전 줄이 겹치지 않게 따로 둠 + * ========================================================================== */ + +import { currentLanguageIndex } from "@ui/ui_template_locale"; + +const TEXT = { + Title: ["일위대가 로직", "Unit-cost logic"], + List_Search: ["이름 찾기", "Search name"], + List_AllBooks: ["원문 모두", "All sources"], + List_AllChapters: ["장 모두", "All chapters"], + List_BlockedOnly: ["막힘만", "Blocked only"], + List_Count: ["{n} 개", "{n} items"], + List_Blocked: ["막힘", "Blocked"], + List_Edited: ["고침", "Edited"], + List_New: ["새것", "New"], + List_Deleted: ["지움", "Deleted"], + Bar_New: ["로직 새로 만들기", "New logic"], + Bar_Delete: ["로직 지우기", "Delete logic"], + Bar_Save: ["저장", "Save"], + Bar_Discard: ["고친 것 버리기", "Discard edits"], + Bar_Pending: ["저장 안 한 로직 {n} 개", "{n} unsaved logics"], + Empty: ["왼쪽 목록에서 로직을 고르세요", "Pick a logic from the list"], + Head_File: ["파일", "File"], + Head_Key: ["열쇠", "Key"], + Head_Name: ["이름", "Name"], + Head_Unit: ["결과단위", "Result unit"], + Head_Source: ["출처", "Source"], + Head_Owner: ["소유", "Owner"], + Head_Note: ["비고", "Note"], + Head_Blocked: ["막힌 까닭", "Why blocked"], + Inputs_Title: ["설계에서 받을 값", "Values from design"], + Inputs_Unit: ["단위", "Unit"], + Inputs_Choices: ["고르기(쉼표로)", "Choices (comma)"], + Inputs_Range: ["범위", "Range"], + Ho_Title: ["호표", "Unit-cost lines"], + Ho_Kind: ["종류", "Kind"], + Ho_Element: ["요소", "Element"], + Ho_Name: ["이름·규격", "Name · spec"], + Ho_Unit: ["단위", "Unit"], + Ho_Qty: ["수량", "Qty"], + Ho_Price: ["단가", "Unit price"], + Ho_Amount: ["금액", "Amount"], + Ho_Cost: ["비목", "Cost item"], + Ho_Pick: ["찾기", "Find"], + Ho_PickTable: ["표", "Table"], + Ho_QtyNumber: ["수", "Number"], + Ho_QtyTable: ["표 찾기", "Table lookup"], + Ho_QtyFormula: ["식", "Formula"], + Ho_PriceLater: ["계산 때", "At calc"], + Ho_Missing: ["없는 요소", "Missing element"], + Middle_Title: ["중간 값", "Intermediate values"], + Result_Title: ["결과 식(돈 아닌 로직)", "Result formula (non-money)"], + Extra_Title: ["덧줄", "Extra lines"], + Rounding: ["끝수", "Rounding"], + Formula: ["식", "Formula"], + AddRow: ["줄 더하기", "Add line"], + DeleteRow: ["줄 지우기", "Delete line"], + Calc_Title: ["시험 계산", "Test calculation"], + Calc_Run: ["계산", "Calculate"], + Calc_NoInputs: ["받을 값 없음", "No inputs"], + Calc_Draft: ["고친 줄로 계산(저장 전)", "Using unsaved edits"], + Calc_Stopped: ["멈춤", "Stopped"], + Calc_Result: ["결과", "Result"], + Calc_Sum: ["계", "Total"], + Pick_Title: ["요소 찾기", "Find element"], + Pick_Search: ["이름·열쇠", "Name · key"], + Pick_Total: ["{n} 개 중 앞 100 개", "first 100 of {n}"], + Pick_Close: ["닫기", "Close"], + Save_Done: ["저장했음", "Saved"], + Save_Stale: [ + "그 사이 파일이 바뀜 — 고친 것을 버리고 다시 여세요: {v}", + "Files changed meanwhile — discard and reopen: {v}", + ], + Save_Errors: ["검사에 걸려 저장 안 함", "Check failed — nothing saved"], + Save_Failed: ["저장 못 함", "Save failed"], + Save_Nothing: ["고친 것 없음", "Nothing to save"], + Load_Failed: ["불러오지 못함", "Load failed"], + Confirm_Delete: [ + "로직 「{v}」 을 지울까요? [저장] 때 파일에서 빠짐", + "Delete logic “{v}”? Removed on save", + ], + Confirm_Discard: ["고친 것을 모두 버릴까요?", "Discard all edits?"], + Confirm_Leave: [ + "저장 안 한 로직이 있음 — 다른 로직을 열어도 고친 것은 남음", + "Unsaved logics stay in the draft", + ], + New_Key: ["새 로직", "New logic"], +} as const satisfies Record; + +export type TextKey = keyof typeof TEXT; + +/** 공용 `t()` 와 같은 뜻 — `{n}`·`{v}` 는 채움 */ +export function tx(key: TextKey, fill: Record = {}): string { + const entry = TEXT[key]; + const text: string = entry[currentLanguageIndex as 0 | 1] ?? entry[0]; + return text.replace(/\{(\w+)\}/g, (m, k: string) => (k in fill ? String(fill[k]) : m)); +} diff --git a/M01_MasterData/M01_MasterData_UI_Page.ts b/M01_MasterData/M01_MasterData_UI_Page.ts new file mode 100644 index 00000000..69f178ac --- /dev/null +++ b/M01_MasterData/M01_MasterData_UI_Page.ts @@ -0,0 +1,226 @@ +/* ============================================================================= + * M01_MasterData_UI_Page.ts + * 마스터 요소 화면 — 좌측 도킹 패널(그룹 탭 7 · 파일 고르기) / 우측 요소 표 · 표 목록 + * + * 시스템 관리자만. 고친 것은 초안(sessionStorage)에 쌓이고 [저장] 한 번에 서버로 — + * 자동저장 없음. 로직 그룹은 다음 화면. + * ========================================================================== */ + +import "@ui/ui_template_workflow_layout.css"; +import { ROUTES } from "@config/config_frontend"; +import { + createButton, + el, + hideLoadingOverlay, + showLoadingOverlay, + showToast, +} from "@ui/ui_template_elements"; +import { t as L } from "@ui/ui_template_locale"; +import { createWorkflowOverlays } from "@ui/ui_template_overlay"; +import { navigateTo } from "../A00_Common/router"; +import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch"; +import { + fetchFiles, + fetchGroups, + saveFiles, + type FileInfo, + type GroupInfo, +} from "./M01_MasterData_Api_Fetch"; +import { discard, isStale, onDraftChange, payload, totalCount } from "./M01_MasterData_Draft"; +import { renderRows } from "./M01_MasterData_UI_Rows"; +import { renderTables } from "./M01_MasterData_UI_Tables"; +import "./M01_MasterData_UI_Style.css"; + +const TABLE_GROUPS = ["소요량", "계수"]; +const fileLabel = (file: string): string => file.replace(/^[^_]+_/, "").replace(/\.json$/, ""); + +export async function renderM01MasterData(root: HTMLElement): Promise { + const user = await fetchSessionUser().catch(() => null); + if (user?.role !== "SYSTEM_ADMIN") { + showToast(L("M01_AdminOnly"), "error"); + navigateTo(ROUTES.B01_ACCOUNT); + return; + } + showLoadingOverlay(); + let groups: GroupInfo[] = []; + try { + groups = (await fetchGroups()).filter((g) => g.group !== "로직"); + } catch (error) { + showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error"); + } finally { + hideLoadingOverlay(); + } + root.innerHTML = ""; + root.append(buildPage(groups)); +} + +function buildPage(groups: GroupInfo[]): HTMLElement { + let group = ""; + let current: FileInfo | null = null; + let query = ""; + let dispose = (): void => {}; + + /* --- 우측: 머리(제목·찾기·저장) + 알림 + 본문 --- */ + const title = el("h2", { className: "m01-master__title", text: L("M01_PickFile") }); + const search = el("input", { + className: "m01-master__search", + attrs: { type: "search", placeholder: L("M01_Search") }, + }); + const summary = el("span", { className: "m01-master__summary" }); + const save = createButton({ label: L("M01_Save"), variant: "filled" }); + const drop = createButton({ label: L("M01_Discard"), variant: "ghost" }); + const notice = el("div", { className: "m01-master__notice", attrs: { hidden: "" } }); + const body = el("div", { className: "m01-master__body" }); + const head = el("div", { + className: "m01-master__head", + children: [title, search, summary, drop, save], + }); + + const showNotice = (nodes: (HTMLElement | string)[]): void => { + notice.replaceChildren(...nodes); + notice.hidden = nodes.length === 0; + }; + const refreshBar = (): void => { + const n = totalCount(); + summary.textContent = n ? L("M01_Changed").replace("{value}", String(n)) : ""; + save.disabled = drop.disabled = n === 0; + }; + onDraftChange(refreshBar); + refreshBar(); + + const openFile = (file: FileInfo): void => { + dispose(); + current = file; + title.textContent = `${group} · ${fileLabel(file.file)}`; + const view = TABLE_GROUPS.includes(group) ? renderTables : renderRows; + dispose = view(body, file.file, query); + showNotice(isStale(file.file, file.version) ? [L("M01_FileStale")] : []); + }; + + let searchTimer: number | undefined; + search.addEventListener("input", () => { + window.clearTimeout(searchTimer); + searchTimer = window.setTimeout(() => { + query = search.value.trim(); + if (current) openFile(current); + }, 300); + }); + + drop.addEventListener("click", () => { + if (!window.confirm(L("M01_DiscardConfirm"))) return; + discard(); + showNotice([]); + }); + + save.addEventListener("click", async () => { + save.disabled = true; + try { + const result = await saveFiles(payload()); + if (result.status === 200 && "files" in result) { + discard(result.files.map((f) => f.file)); + showNotice([]); + showToast(L("M01_Saved"), "success"); + await reloadFiles(); + } else if (result.status === 409 && "stale" in result) { + showNotice(staleNotice(result.stale)); + } else if (result.status === 422 && "errors" in result) { + showNotice([ + el("strong", { text: L("M01_CheckErrors") }), + ...result.errors.map((e) => el("p", { text: e })), + ]); + } else if ("detail" in result) { + showNotice([L("M01_SaveFailed").replace("{value}", result.detail)]); + } + } catch (error) { + showNotice([ + L("M01_SaveFailed").replace("{value}", error instanceof Error ? error.message : ""), + ]); + } finally { + refreshBar(); + } + }); + + const staleNotice = (stale: string[]): (HTMLElement | string)[] => { + const again = createButton({ label: L("M01_StaleReload"), variant: "ghost" }); + again.addEventListener("click", async () => { + discard(stale); + showNotice([]); + await reloadFiles(); + }); + return [L("M01_Stale").replace("{value}", stale.map(fileLabel).join(", ")), again]; + }; + + /* --- 좌측: 그룹 탭 + 파일 --- */ + const tabs = el("div", { className: "m01-master__tabs" }); + const files = el("div", { className: "m01-master__files" }); + + const reloadFiles = async (): Promise => { + if (!group) return; + let list: FileInfo[] = []; + try { + list = await fetchFiles(group); + } catch (error) { + showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error"); + } + files.replaceChildren( + ...list.map((f) => { + const button = el("button", { + className: "m01-master__file", + attrs: { type: "button" }, + children: [ + el("span", { text: fileLabel(f.file) }), + el("span", { className: "m01-master__muted", text: f.rows.toLocaleString("ko-KR") }), + ], + }); + button.classList.toggle("is-active", f.file === current?.file); + button.addEventListener("click", () => { + files.querySelector(".is-active")?.classList.remove("is-active"); + button.classList.add("is-active"); + openFile(f); + }); + return button; + }), + ); + const again = list.find((f) => f.file === current?.file); + if (again) openFile(again); + }; + + for (const g of groups) { + const tab = el("button", { + className: "m01-master__tab", + attrs: { type: "button" }, + children: [ + el("span", { text: g.group }), + el("span", { className: "m01-master__muted", text: String(g.files) }), + ], + }); + tab.addEventListener("click", () => { + tabs.querySelector(".is-active")?.classList.remove("is-active"); + tab.classList.add("is-active"); + group = g.group; + current = null; + dispose(); + body.replaceChildren(); + title.textContent = L("M01_PickFile"); + void reloadFiles(); + }); + tabs.append(tab); + } + + const layout = el("div", { className: "ui-workflow-layout m01-master" }); + const main = el("main", { + className: "ui-workflow-layout__main", + children: [el("div", { className: "m01-master__panel", children: [head, notice, body] })], + }); + const overlays = createWorkflowOverlays({ + title: L("M01_Title"), + optionsContent: el("div", { className: "m01-master__side", children: [tabs, files] }), + showProjectName: false, + onOptionsOpenChange: (isOpen) => layout.classList.toggle("is-options-open", isOpen), + }); + layout.append( + el("div", { className: "ui-workflow-layout__body", children: [main] }), + overlays.root, + ); + return layout; +} diff --git a/M01_MasterData/M01_MasterData_UI_Rows.ts b/M01_MasterData/M01_MasterData_UI_Rows.ts new file mode 100644 index 00000000..93f346b9 --- /dev/null +++ b/M01_MasterData/M01_MasterData_UI_Rows.ts @@ -0,0 +1,153 @@ +/* ============================================================================= + * M01_MasterData_UI_Rows.ts + * 요소 파일(인력·재료·기계·환율·요율) 표 — 한 줄 = 요소 · 이름 찾기 · 쪽 나눔 · 눌러 고침(노랑) + * ========================================================================== */ + +import { createButton, el, showToast } from "@ui/ui_template_elements"; +import { t as L } from "@ui/ui_template_locale"; +import { fetchRows, type Row, type RowsPage } from "./M01_MasterData_Api_Fetch"; +import { + addRow, + editRow, + onDraftChange, + peek, + removeAdd, + same, + setAdd, + toggleDelete, +} from "./M01_MasterData_Draft"; +import { + buildCell, + buildPager, + coerce, + flatten, + isScalar, + show, + withCell, +} from "./M01_MasterData_UI_Cells"; + +const SIZE = 50; +const FALLBACK: Row = { 열쇠: "", 이름: "", 규격: "", 단위: "", 값: null, 출처: "" }; + +/** 새 줄 바탕 — 본 줄과 같은 칸 · 글 칸은 "" · 수·값 칸은 null. */ +function blank(sample: Row): Row { + const out: Row = {}; + for (const [k, v] of Object.entries(sample)) { + if (v && typeof v === "object" && !Array.isArray(v)) out[k] = blank(v as Row); + else out[k] = typeof v === "string" && k !== "값" ? "" : null; + } + return out; +} + +/** 돌려받은 함수 = 이 화면을 걷을 때 부를 해제. */ +export function renderRows(host: HTMLElement, file: string, q: string): () => void { + let page = 1; + let data: RowsPage | null = null; + + const paint = (): void => { + if (!data) return; + const d = data; + const draft = peek(file); + const adds = draft?.adds ?? []; + const cols: string[] = []; + for (const row of [...adds, ...d.rows]) { + for (const k of Object.keys(flatten(row))) if (!cols.includes(k)) cols.push(k); + } + const head = el("tr", { children: [el("th"), ...cols.map((c) => el("th", { text: c }))] }); + const body = el("tbody"); + + adds.forEach((row, i) => { + const flat = flatten(row); + const tr = el("tr", { className: "is-new" }); + tr.append(actionCell(L("M01_RowRemove"), () => removeAdd(file, i))); + for (const c of cols) { + tr.append( + buildCell(show(flat[c]), { + changed: true, + onEdit: (text) => setAdd(file, i, withCell(row, c, coerce(text, flat[c]))), + }), + ); + } + body.append(tr); + }); + + for (const row of d.rows) { + const key = String(row["열쇠"]); + const cur = draft?.edits[key] ?? row; + const deleted = draft?.deletes.includes(key) ?? false; + const orig = flatten(row); + const now = flatten(cur); + const tr = el("tr"); + tr.classList.toggle("is-deleted", deleted); + tr.append( + actionCell(deleted ? L("M01_RowRestore") : L("M01_RowDelete"), () => + toggleDelete(file, d.version, key), + ), + ); + for (const c of cols) { + const editable = !deleted && isScalar(orig[c]); + tr.append( + buildCell(show(now[c]), { + changed: !same(now[c], orig[c]), + onEdit: editable + ? (text) => + editRow(file, d.version, key, row, withCell(cur, c, coerce(text, orig[c]))) + : undefined, + }), + ); + } + body.append(tr); + } + + const add = createButton({ label: L("M01_RowAdd"), variant: "ghost" }); + add.addEventListener("click", () => addRow(file, d.version, blank(d.rows[0] ?? FALLBACK))); + const empty = + d.rows.length || adds.length + ? [] + : [el("p", { className: "m01-master__empty", text: L("M01_NoRows") })]; + host.replaceChildren( + el("div", { + className: "m01-master__bar", + children: [add, buildPager(d.total, SIZE, d.page, go)], + }), + el("div", { + className: "m01-master__grid-wrap", + children: [ + el("table", { + className: "m01-master__grid", + children: [el("thead", { children: [head] }), body], + }), + ], + }), + ...empty, + ); + }; + + const go = (next: number): void => { + page = next; + void load(); + }; + + const load = async (): Promise => { + try { + data = await fetchRows(file, page, SIZE, q); + paint(); + } catch (error) { + showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error"); + } + }; + + const off = onDraftChange(paint); + void load(); + return off; +} + +function actionCell(label: string, onClick: () => void): HTMLTableCellElement { + const button = el("button", { + className: "m01-master__row-btn", + text: label, + attrs: { type: "button" }, + }); + button.addEventListener("click", onClick); + return el("td", { children: [button] }); +} diff --git a/M01_MasterData/M01_MasterData_UI_Style.css b/M01_MasterData/M01_MasterData_UI_Style.css new file mode 100644 index 00000000..ef501483 --- /dev/null +++ b/M01_MasterData/M01_MasterData_UI_Style.css @@ -0,0 +1,219 @@ +/* M01 마스터 요소 — 좌측 도킹 패널(그룹 탭 · 파일) / 우측 표 */ +.m01-master [hidden] { + display: none !important; +} + +.m01-master__side { + display: flex; + flex-direction: column; + gap: var(--spacing-8); + padding-top: var(--spacing-8); +} + +.m01-master__tabs { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--spacing-4); +} + +.m01-master__files { + display: flex; + flex-direction: column; + gap: 2px; +} + +.m01-master__tab, +.m01-master__file { + display: flex; + justify-content: space-between; + gap: var(--spacing-8); + padding: var(--spacing-4) var(--spacing-8); + border: 1px solid var(--color-border); + border-radius: var(--radius-buttons); + background: var(--color-surface); + color: var(--color-text-body); + font: inherit; + font-size: var(--text-body-sm); + text-align: left; + cursor: pointer; +} + +.m01-master__file { + border-color: transparent; + background: transparent; +} + +.m01-master__tab:hover, +.m01-master__file:hover { + background: var(--color-paper); +} + +.m01-master__tab.is-active, +.m01-master__file.is-active { + background: var(--color-mist-violet); + color: var(--color-accent); +} + +.m01-master__muted { + color: var(--color-text-muted); + font-size: var(--text-caption); +} + +/* ── 우측 ── */ +.m01-master__panel { + display: flex; + flex-direction: column; + gap: var(--spacing-12); + min-width: 0; + padding: var(--spacing-16) var(--spacing-24); +} + +.m01-master__head, +.m01-master__bar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--spacing-12); +} + +.m01-master__title { + flex: 1 1 auto; + margin: 0; + color: var(--color-text); + font-size: var(--text-body); +} + +.m01-master__summary { + color: var(--color-accent); + font-size: var(--text-body-sm); + font-weight: var(--font-weight-bold); +} + +.m01-master__notice { + display: flex; + flex-direction: column; + gap: var(--spacing-4); + padding: var(--spacing-8) var(--spacing-12); + max-height: 30vh; + overflow: auto; + border-left: 4px solid var(--color-danger); + background: color-mix(in srgb, var(--color-danger) 10%, var(--color-canvas)); + color: var(--color-danger); + font-size: var(--text-body-sm); +} + +.m01-master__notice p { + margin: 0; +} + +.m01-master__body { + display: flex; + flex-direction: column; + gap: var(--spacing-12); + min-width: 0; +} + +.m01-master__grid-wrap { + max-height: calc(100vh - 300px); + overflow: auto; + border: 1px solid var(--color-border); + border-radius: var(--radius-cards); +} + +.m01-master__grid { + border-collapse: collapse; + font-size: var(--text-body-sm); +} + +.m01-master__grid th, +.m01-master__grid td { + max-width: 320px; + padding: var(--spacing-8) var(--spacing-12); + overflow: hidden; + border-bottom: 1px solid var(--color-border); + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; +} + +.m01-master__grid th { + position: sticky; + top: 0; + z-index: 1; + background: var(--color-surface); + color: var(--color-text-secondary); + font-weight: var(--font-weight-medium); +} + +.m01-master__grid td.is-editable { + cursor: text; +} + +/* 고친 칸 — 노랑 */ +.m01-master__grid td.is-changed { + background: color-mix(in srgb, var(--color-warning) 28%, var(--color-canvas)); +} + +.m01-master__grid tr.is-deleted td { + color: var(--color-text-muted); + text-decoration: line-through; + background: color-mix(in srgb, var(--color-danger) 8%, var(--color-canvas)); +} + +.m01-master__cell-input { + width: 100%; + min-width: 6em; + padding: 2px var(--spacing-4); +} + +.m01-master__row-btn { + padding: 2px var(--spacing-8); + border: 1px solid var(--color-border); + border-radius: var(--radius-buttons); + background: var(--color-surface); + color: var(--color-text-body); + font: inherit; + font-size: var(--text-caption); + cursor: pointer; +} + +.m01-master__pager { + display: flex; + align-items: center; + gap: var(--spacing-12); + margin-left: auto; + color: var(--color-text-secondary); + font-size: var(--text-body-sm); +} + +.m01-master__tables { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: var(--spacing-8); +} + +.m01-master__table-card { + display: flex; + flex-direction: column; + gap: 2px; + padding: var(--spacing-8) var(--spacing-12); + border: 1px solid var(--color-border); + border-radius: var(--radius-cards); + background: var(--color-surface); + color: var(--color-text-body); + font: inherit; + text-align: left; + cursor: pointer; +} + +.m01-master__table-card:hover { + background: var(--color-paper); +} + +.m01-master__table-card.is-changed { + background: color-mix(in srgb, var(--color-warning) 28%, var(--color-canvas)); +} + +.m01-master__empty { + color: var(--color-text-muted); +} diff --git a/M01_MasterData/M01_MasterData_UI_Tables.ts b/M01_MasterData/M01_MasterData_UI_Tables.ts new file mode 100644 index 00000000..2915f944 --- /dev/null +++ b/M01_MasterData/M01_MasterData_UI_Tables.ts @@ -0,0 +1,200 @@ +/* ============================================================================= + * M01_MasterData_UI_Tables.ts + * 표형 파일(소요량·계수) — 표 목록 → 표 하나(조건 칸 + 값 칸의 긴 줄) · 눌러 고침(노랑) · 줄 더하기·지우기 + * ========================================================================== */ + +import { createButton, el, showToast } from "@ui/ui_template_elements"; +import { t as L } from "@ui/ui_template_locale"; +import { fetchTable, fetchTables, type Row, type TableHead } from "./M01_MasterData_Api_Fetch"; +import { + onDraftChange, + peek, + same, + setTable, + tableItems, + type TableItem, +} from "./M01_MasterData_Draft"; +import { + buildCell, + buildPager, + coerce, + show, + toNumberish, + toRange, +} from "./M01_MasterData_UI_Cells"; + +const LIST_SIZE = 30; + +/** 돌려받은 함수 = 이 화면을 걷을 때 부를 해제. */ +export function renderTables(host: HTMLElement, file: string, q: string): () => void { + let stop = (): void => {}; + const list = (page = 1): void => { + stop(); + stop = (): void => {}; + void showList(host, file, q, page, (key) => { + stop = openTable(host, file, key, () => list(page)); + }); + }; + list(); + const off = onDraftChange( + () => host.querySelector(".m01-master__tables") && markChanged(host, file), + ); + return () => { + stop(); + off(); + }; +} + +/** 목록 위 「고침」 표시만 다시 — 목록을 다시 받지 않음. */ +function markChanged(host: HTMLElement, file: string): void { + const changed = peek(file)?.tables ?? {}; + host.querySelectorAll(".m01-master__table-card").forEach((card) => { + card.classList.toggle("is-changed", card.dataset.key! in changed); + }); +} + +async function showList( + host: HTMLElement, + file: string, + q: string, + page: number, + open: (key: string) => void, +): Promise { + let tables: TableHead[]; + try { + tables = (await fetchTables(file, q)).tables; + } catch (error) { + showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error"); + return; + } + const cards = tables.slice((page - 1) * LIST_SIZE, page * LIST_SIZE).map((t) => { + const card = el("button", { + className: "m01-master__table-card", + attrs: { type: "button", "data-key": t.열쇠 }, + children: [ + el("strong", { text: t.이름 || t.열쇠 }), + el("span", { className: "m01-master__muted", text: `${t.열쇠} · ${t.기준 || "—"}` }), + el("span", { + className: "m01-master__muted", + text: `${Object.keys(t.조건 ?? {}).join(" · ") || "—"} → ${Object.keys(t.값칸 ?? {}).join(" · ")} · ${t.count} ${L("M01_Rows")}`, + }), + ], + }); + card.classList.toggle("is-changed", t.열쇠 in (peek(file)?.tables ?? {})); + card.addEventListener("click", () => open(t.열쇠)); + return card; + }); + host.replaceChildren( + buildPager(tables.length, LIST_SIZE, page, (p) => void showList(host, file, q, p, open)), + el("div", { className: "m01-master__tables", children: cards }), + ); +} + +/** 표 하나. 돌려받은 함수 = 해제. */ +function openTable(host: HTMLElement, file: string, key: string, back: () => void): () => void { + let off = (): void => {}; + void (async () => { + let version: string; + let table: Row & { 줄: Row[] }; + try { + ({ version, table } = await fetchTable(file, key)); + } catch (error) { + showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error"); + return; + } + const { 줄: orig, ...base } = table; + const conds = (table.조건 ?? {}) as Record; + const values = (table.값칸 ?? {}) as Record; + const cols = [...Object.keys(conds), ...Object.keys(values)]; + for (const row of orig) for (const k of Object.keys(row)) if (!cols.includes(k)) cols.push(k); + const items = (): TableItem[] => tableItems(file, key) ?? orig.map((r, o) => ({ o, r })); + const commit = (next: TableItem[]): void => setTable(file, version, key, base, next, orig); + + const parse = (col: string, text: string, prev: unknown): unknown => { + if (conds[col] === "범위") return toRange(text); + if (conds[col] === "고르기") return text; + return col in values || conds[col] === "수" ? toNumberish(text) : coerce(text, prev); + }; + const headText = (c: string): string => + c in conds ? `${c} (${conds[c]})` : c in values ? `${c} (${values[c]})` : c; + + const paint = (): void => { + const now = items(); + const body = el("tbody"); + now.forEach((item, i) => { + const tr = el("tr"); + tr.classList.toggle("is-new", item.o === null); + const del = el("button", { + className: "m01-master__row-btn", + text: L("M01_RowRemove"), + attrs: { type: "button" }, + }); + del.addEventListener("click", () => commit(now.filter((_, j) => j !== i))); + tr.append(el("td", { children: [del] })); + for (const c of cols) { + const was = item.o === null ? undefined : orig[item.o][c]; + tr.append( + buildCell(show(item.r[c]), { + changed: item.o === null || !same(item.r[c], was), + onEdit: (text) => { + const value = parse(c, text, item.r[c]); + const r = { ...item.r }; + if (value === null || value === "") + delete r[c]; // 원문 「-」 = 칸을 뺌 + else r[c] = value; + commit(now.map((it, j) => (j === i ? { ...it, r } : it))); + }, + }), + ); + } + body.append(tr); + }); + const add = createButton({ label: L("M01_RowAdd"), variant: "ghost" }); + add.addEventListener("click", () => commit([...now, { o: null, r: {} }])); + const backButton = createButton({ label: L("M01_BackToTables"), variant: "ghost" }); + backButton.addEventListener("click", () => { + off(); + back(); + }); + const notes = ((table.주 ?? []) as string[]).map((n) => + el("p", { className: "m01-master__muted", text: n }), + ); + host.replaceChildren( + el("div", { + className: "m01-master__bar", + children: [ + backButton, + el("strong", { text: String(table.이름 ?? key) }), + el("span", { + className: "m01-master__muted", + text: `${key} · ${table.기준 || "—"} · ${table.출처 || ""}`, + }), + add, + ], + }), + el("div", { + className: "m01-master__grid-wrap", + children: [ + el("table", { + className: "m01-master__grid", + children: [ + el("thead", { + children: [ + el("tr", { + children: [el("th"), ...cols.map((c) => el("th", { text: headText(c) }))], + }), + ], + }), + body, + ], + }), + ], + }), + ...notes, + ); + }; + off = onDraftChange(paint); + paint(); + })(); + return () => off(); +} diff --git a/config/config_frontend.ts b/config/config_frontend.ts index 277a9238..a5738a7f 100644 --- a/config/config_frontend.ts +++ b/config/config_frontend.ts @@ -118,6 +118,8 @@ export const ROUTES = { B11_LOADING: "b11-loading", // 시스템 관리자 전용 — 로직·기초값 마스터 보기 (2026-09-15 브레인 Z01). Z01_MASTER_DATA: "z01-master-data", + // 시스템 관리자 전용 — 마스터 요소(파일 첫 층 JSON) 보기·고치기. + M01_MASTER_DATA: "m01-master-data", } as const; export type RouteKey = keyof typeof ROUTES; @@ -141,6 +143,7 @@ export const PROTECTED_ROUTES: readonly RoutePath[] = [ ROUTES.B11_STATUS, ROUTES.B11_LOADING, ROUTES.Z01_MASTER_DATA, + ROUTES.M01_MASTER_DATA, ]; /* ----------------------------------------------------------------------------- diff --git a/main.py b/main.py index 85f2344a..916efeaa 100644 --- a/main.py +++ b/main.py @@ -76,6 +76,7 @@ from B09_Estimation.B09_Estimation_Router_MaterialPrices import ( router as b09_material_prices_router, ) from Z01_MasterData.Z01_MasterData_Router import router as z01_master_data_router +from M01_MasterData.M01_MasterData_Router import router as m01_master_data_router from common_util.common_util_audit import note_api_call, record_call_burst from common_util.common_util_auth import ( require_company, @@ -659,6 +660,7 @@ app.include_router(b09_material_prices_router, dependencies=protected_with_compa # Z01 마스터 데이터 — 회사·프로젝트가 아니라 시스템 관리자만 본다(2026-09-15 브레인 Z01). system_admin_only = [Depends(verify_session), Depends(require_system_admin)] app.include_router(z01_master_data_router, dependencies=system_admin_only) +app.include_router(m01_master_data_router, dependencies=system_admin_only) # 개발 전용 잠금 해제 — 다른 라우터와 **같은 보호**를 받는다(로그인·회사·프로젝트 접근). # 그 위에 서버가 환경까지 한 번 더 본다. app.include_router(dev_unlock_router, dependencies=protected_with_company) diff --git a/resources/master_data/_화면_계약.md b/resources/master_data/_화면_계약.md new file mode 100644 index 00000000..90c3c2ab --- /dev/null +++ b/resources/master_data/_화면_계약.md @@ -0,0 +1,62 @@ +# 관리자 화면 계약 (M01) + +`resources/master_data/` 첫 층 JSON 을 보고 고치는 화면과 서버(`M01_MasterData/`) 사이의 약속. 파일 모양은 `_틀.md`. + +## 1. 공통 + +- 길 머리 `/api/m01` · 시스템 관리자만(아니면 403). +- 봉투 칸 이름은 영문 · 줄·표·로직 알맹이는 파일 그대로(한글 칸). +- 수는 JSON 수로 옴. +- `version` = 파일 판본(내용 해시). 읽을 때 받아 두고 저장 때 그대로 돌려줌. +- `file` = 파일 이름(`소요량_건설품셈_기계설비7장.json`). 밑줄 파일·하위 폴더는 없음. +- 없는 파일·열쇠 = 404 · 틀린 요청 = 400. + +## 2. 읽기 + +| 길 | 받음 | 줌 | +|---|---|---| +| `GET /groups` | | `{groups: [{group, files, rows}]}` — 그룹 차례는 `_틀.md` 2장 | +| `GET /groups/{group}/files` | | `{files: [{file, book, chapter, edition, rows, version}]}` | +| `GET /rows` | `file` · `page`(1) · `size`(50) · `q` | `{file, version, total, page, size, rows: [줄…]}` | +| `GET /tables` | `file` · `q` | `{file, version, tables: [{열쇠, 이름, 기준, 출처, 조건, 값칸, count}]}` | +| `GET /table` | `file` · `key` | `{file, version, table: {표 하나 통째}}` | +| `GET /logics` | `book` · `chapter` · `q` · `blocked`(0/1) | `{logics: [{file, book, chapter, 열쇠, 이름, 결과단위, 출처, blocked, reasons}]}` | +| `GET /logic` | `book` · `key` | `{file, version, logic: {로직 한 줄 통째}, blocked, reasons, prices: {요소: 요약 \| null}}` | +| `GET /elements` | `group` · `q` · `limit`(50 · 200까지) | `{total, items: [{ref, file, 이름, 규격, 단위, 값, 값칸}]}` | + +- `rows` 는 요소·로직 파일은 `줄`, 표형 파일(소요량·계수)은 `표` 를 줄로 봄. +- `q` = 이름 찾기 — `열쇠`·`이름` 에 든 글(대소문자 무시). +- `chapter` = 파일 이름의 장(`공통3장` · `13장` · `01장`) · 없으면 "". +- `blocked` = 로직 검사(`check_master.py 로직`)에 걸림 · `reasons` = 걸린 까닭 글. +- `prices` = 호표 `요소` → `{ref, 이름, 규격, 단위, 값}`(단가 자동 칸) · 없는 요소는 `null` · `{이름}` 이 낀 열쇠·하위 로직 줄은 빠짐(계산 때 정해짐). +- `elements` = 요소 찾기 창 — 그룹 전체에서 `q` 찾기 · `ref` = 식에 넣는 `그룹:원문:열쇠` · 표형 그룹은 `값칸` 이 옴. + +## 3. 시험 계산 + +`POST /calc` 받음 `{book, key, inputs: {이름: 값}, row?, file?}` + +- 됨 — `{ok: true, lines: [{이름, 단위, 수량, 단가, 금액, 비목: {비목: 금액}}], sums: {노무비, 재료비, 경비, 계}, middle: {이름: 값}}` +- 돈 아닌 로직 — `{ok: true, result, middle}` +- 멈춤 — `{ok: false, reason}` (200). 까닭 = 엔진 글 그대로(「입력 「돌」 없음」 · 「값 없음 — 관리자가 채울 값」 …). +- `row` 없음 = 저장된 파일로 계산 · `row` 있음 = 그 로직만 고친 줄로 바꿔(없던 로직이면 `file` 에 더해) 메모리에서 계산 — 파일에 안 씀. 요소 값은 늘 저장된 파일. + +## 4. 저장 + +`POST /save` 받음 `{files: [{file, version, changes: [{op, key, row}]}]}` + +- `op` — `edit`(열쇠 `key` 줄을 `row` 로 바꿈) · `add`(`row` 더함 · `key` 안 씀) · `delete`(`key` 줄 지움). +- 요소 파일 = `줄` 한 줄 · 표형 파일 = `표` 하나 · 로직 파일 = 로직 한 줄. `row` 는 통째(칸 일부만 보내지 않음). +- 화면은 조작을 캐시에 쌓고 [저장] 한 번에 보냄 — 서버는 이때만 파일에 씀. +- 순서: 모든 `version` 대조 → 모두 적용(메모리) → 검사 → 모두 씀. 하나라도 걸리면 아무것도 안 씀. + +| 답 | 뜻 | 몸 | +|---|---|---| +| 200 | 씀 | `{files: [{file, version}]}` — 새 판본 | +| 409 | 낡은 화면 — 그 사이 파일이 바뀜 | `{detail: {stale: [file…]}}` → 다시 읽고 다시 고침 | +| 422 | 검사 걸림 | `{detail: {errors: [글…]}}` | +| 404 | 없는 파일·열쇠 | `{detail}` | +| 400 | 틀린 `op` · 더할 열쇠가 이미 있음 · 고친 줄의 열쇠가 딴 줄과 겹침 | `{detail}` | + +- 검사 = `check_master.py` 의 틀(고친 파일) + 로직 변수(모든 로직 파일, 고친 뒤 마스터 기준). **고친 뒤 새로 생긴 걸림**만 막음 — 원래 있던 걸림은 막지 않음. +- 요소를 지워 어떤 로직이 가리키던 것이 사라지면 로직 변수 검사에 걸림(422). +- 쓰기는 UTF-8 · LF · 들여쓰기 2칸. diff --git a/resources/master_data/scripts/check_master.py b/resources/master_data/scripts/check_master.py index 0bee4a7b..84224b7a 100644 --- a/resources/master_data/scripts/check_master.py +++ b/resources/master_data/scripts/check_master.py @@ -59,10 +59,10 @@ ROW_KEYS = { } -def load(names: list[str] | None = None) -> dict[str, dict]: - """첫 층 JSON 모두(밑줄 파일 빼고) — 수는 Decimal.""" +def load(names: list[str] | None = None, folder: Path = MASTER) -> dict[str, dict]: + """첫 층 JSON 모두(밑줄 파일 빼고) — 수는 Decimal. `folder` = 마스터 폴더(M01 시험은 사본).""" out = {} - for path in sorted(MASTER.glob("*.json")): + for path in sorted(Path(folder).glob("*.json")): if path.name.startswith("_") or ( names and path.name not in names and path.stem not in names ): @@ -73,8 +73,19 @@ def load(names: list[str] | None = None) -> dict[str, dict]: return out -def master() -> mf.Master: - return mf.Master(load()) +def master(folder: Path = MASTER) -> mf.Master: + return mf.Master(load(folder=folder)) + + +def check_saved(files: dict[str, dict], changed: list[str]) -> list[str]: + """저장 전 검사(M01) — 고친 파일의 틀 + 모든 로직의 변수. `files` = 폴더 전부(고친 뒤).""" + out = [x for name in changed for x in check_form(name, files[name])] + return out + check_logics(files, mf.Master(files)) + + +def calc(files: dict[str, dict], ref: str, given: dict) -> dict: + """시험 계산(M01) — `ref` = 「원문:로직 열쇠」 · 멈추면 FormulaError.""" + return mf.run(mf.Master(files), ref, given) # ── (1) 틀 ──────────────────────────────────────────────────────────── @@ -228,7 +239,12 @@ def check_body(files: dict[str, dict]) -> list[str]: if not text: out.append(f"{where} · 본문 절 못 찾음 「{table.get('출처')}」") continue - mine = _flat(table.get("줄")) | _flat(table.get("기준")) | _flat(table.get("주")) | _flat(table.get("값칸")) + mine = ( + _flat(table.get("줄")) + | _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)}") diff --git a/resources/master_data/기계_산림품셈.json b/resources/master_data/기계_산림품셈.json index 19808200..df96ebe6 100644 --- a/resources/master_data/기계_산림품셈.json +++ b/resources/master_data/기계_산림품셈.json @@ -184,7 +184,7 @@ "단위": { "손료계수": "1일 1대당 4시간 가동", "취득가액": "원" }, "값": { "손료계수": 0.0040, "취득가액": 9894500 }, "출처": { "손료계수": "산림품셈 2-2-8", "취득가액": "산림품셈 2-2-8" } }, - { "열쇠": "2-2-8 리튬폴리머 배터리(평균가)", "이름": "리튬폴리머 배터리(평균가)", "규격": "16,000mA, 32,000mA", + { "열쇠": "2-2-8 리튬폴리머 배터리(평균가)", "이름": "리튬폴리머 배터리(평균가)", "규격": "16,000mA, 32000mA", "단위": { "손료계수": "1일 1대당 4시간 가동", "취득가액": "원" }, "값": { "손료계수": 0.0028, "취득가액": 7104000 }, "출처": { "손료계수": "산림품셈 2-2-8", "취득가액": "산림품셈 2-2-8" }, diff --git a/resources/master_data/로직_건설품셈_공통8장.json b/resources/master_data/로직_건설품셈_공통8장.json index 7fabd947..4e66cf88 100644 --- a/resources/master_data/로직_건설품셈_공통8장.json +++ b/resources/master_data/로직_건설품셈_공통8장.json @@ -69,7 +69,7 @@ "출처": "건설품셈 공통 8-1-5 · 8-1-6", "소유": "공용", "입력": [ - { "이름": "기계", "고르기": ["1730-0015", "7204-0055", "0602-0150", "2702-0020", "0101-0007", "0101-0010", "0101-0012", "0101-0019", "0101-0032", "2105-0005", "2104-0010", "2104-0015", "2104-0020", "2104-0025", "2104-0030", "2104-0035", "2104-0040", "2104-0045", "2104-0050", "2104-0060", "2104-0070", "2104-0080", "2104-0100", "2104-0130", "2104-0160", "2104-0200", "2104-0220", "2104-0250", "2104-0300", "4504-0032", "4504-0036", "4504-0041", "4504-0043", "4504-0047", "4504-0052", "4205-0010", "4205-0017", "4205-0020", "4205-0030", "4205-0040", "4205-0045", "2105-0002", "2105-0003", "2105-0010", "2105-0015", "2105-0018", "2502-0025", "0602-0025", "2502-0050", "2101-0010", "2101-0015", "2101-0020", "2101-0025", "2101-0030", "2101-0035", "2101-0040", "2101-0050", "2101-0070", "2101-0080", "2101-0100", "2101-0150", "2101-0220", "2101-0280", "2101-0300", "7204-0018", "7204-0160", "5205-0210", "5405-0110", "7505-0450", "5205-0255", "7505-0150", "5205-0103", "6701-0147", "6701-0184", "5205-0170", "7505-0350", "0302-0134", "7505-0100", "7505-0050", "5205-0071", "7505-0025", "0502-0036", "1306-0100", "1506-0032", "0121-0004", "1305-0007", "7204-0038", "7204-0065", "0302-0057", "5701-0020", "0302-0095", "3201-0003", "1106-0012", "1406-0015", "1206-0008", "3601-0102", "1306-0025", "0211-0018", "0602-0045", "3901-0300", "7505-0250", "2502-0035"] }, + { "이름": "기계", "고르기": ["1730-0015", "7204-0055", "0602-0150", "2702-0020", "0101-0007", "0101-0010", "0101-0012", "0101-0019", "0101-0032", "2105-0005", "2104-0010", "2104-0015", "2104-0020", "2104-0025", "2104-0030", "2104-0035", "2104-0040", "2104-0045", "2104-0050", "2104-0060", "2104-0070", "2104-0080", "2104-0100", "2104-0130", "2104-0160", "2104-0200", "2104-0220", "2104-0250", "2104-0300", "4504-0032", "4504-0036", "4504-0041", "4504-0043", "4504-0047", "4504-0052", "4205-0010", "4205-0017", "4205-0020", "4205-0030", "4205-0040", "4205-0045", "2105-0002", "2105-0003", "2105-0010", "2105-0015", "2105-0018", "2502-0025", "0602-0025", "2502-0050", "2101-0010", "2101-0015", "2101-0020", "2101-0025", "2101-0030", "2101-0035", "2101-0040", "2101-0050", "2101-0070", "2101-0080", "2101-0100", "2101-0150", "2101-0220", "2101-0280", "2101-0300", "7204-0018", "7204-0160", "5205-0210", "5405-0110", "7505-0450", "5205-0255", "7505-0150", "5205-0103", "6701-0147", "6701-0184", "5205-0170", "7505-0350", "0302-0134", "7505-0100", "7505-0050", "5205-0071", "7505-0025", "0502-0036", "1306-0100", "1506-0032", "0121-0004", "1305-0007", "7204-0038", "7204-0065", "0302-0057", "5701-0020", "0302-0095", "3201-0003", "1106-0012", "1406-0015", "1206-0008", "3601-0102", "1306-0025", "0211-0018", "0602-0045", "3901-0300", "7505-0250", "2502-0035", "1306-0120", "1106-0010"] }, { "이름": "지역", "고르기": [ @@ -309,7 +309,7 @@ "입력": [ { "이름": "기계", - "고르기": ["4611-0350", "7812-0015", "4506-0200", "4506-0400", "1730-0015", "1305-0007", "2106-0002", "2106-0003", "2106-0005", "7210-0485", "7360-0090", "6540-0131"] + "고르기": ["4611-0350", "7812-0015", "4506-0200", "4506-0400", "1730-0015", "1305-0007", "2106-0002", "2106-0003", "2106-0005", "7210-0485", "7360-0090", "6540-0131", "3302-0038", "4430-0400"] }, { "이름": "지역", diff --git a/resources/master_data/로직_건설품셈_토목1장.json b/resources/master_data/로직_건설품셈_토목1장.json new file mode 100644 index 00000000..22b6fa28 --- /dev/null +++ b/resources/master_data/로직_건설품셈_토목1장.json @@ -0,0 +1,3623 @@ +{ + "그룹": "로직", + "원문": "건설품셈", + "판": "2026", + "줄": [ + { + "열쇠": "1-1-2 유도선 설치 및 해체", + "이름": "유도선 설치 및 해체", + "결과단위": "원/m", + "출처": "건설품셈 토목 1-1-2", + "소유": "공용", + "입력": [{ "이름": "설치간격", "고르기": ["6m이하", "10m이하"] }], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-1-2 유도선 설치 및 해체, 설치간격=설치간격).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-1-2 유도선 설치 및 해체, 설치간격=설치간격).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-1-2 유도선 설치 및 해체, 설치간격=설치간격).보통인부 / 시공량", + "비목": "노무비" + } + ], + "덧줄": [], + "끝수": null, + "비고": "스틱 설치 천공 별도(주③)" + }, + { + "열쇠": "1-2-1 인력식 소규모장비 포설", + "이름": "동상방지층 인력식 소규모장비 포설", + "결과단위": "원/㎥", + "출처": "건설품셈 토목 1-2-1", + "소유": "공용", + "입력": [ + { "이름": "살수", "고르기": ["살수차", "인력 살수"], "출처": "건설품셈 토목 1-2-1 비고" }, + { "이름": "두께", "단위": "㎝", "출처": "건설품셈 토목 1-2-1 비고" }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + }, + { "이름": "보정작업", "고르기": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "출처": "건설품셈 공통 8-1-7 1" } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-2-1 인력식 소규모장비 포설).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1018", + "이름": "포설공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-2-1 인력식 소규모장비 포설).포설공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-2-1 인력식 소규모장비 포설).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 굴착기(무한궤도) 시간당 경비, 기계='0201-0060', 지역=지역, 보정작업=보정작업)", + "이름": "굴착기 0.6㎥", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-2-1 인력식 소규모장비 포설).굴착기 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='7204-0055', 지역=지역)", + "이름": "살수차 5,500ℓ", + "단위": "시간", + "수량": "만약(살수 == '살수차', 찾기(소요량:건설품셈:1-2-1 인력식 소규모장비 포설).살수차 * 8 / 시공량, 0)" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "만약(살수 == '인력 살수', 1 / 두께, 0)", + "비목": "노무비", + "출처": "건설품셈 토목 1-2-1 비고" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 해석: 인력 살수 = 살수차 빼고 보통인부 100㎡당 1인 · 100㎡ × 두께 = ㎥ 로 바꿈(두께 ㎝ 입력) · 살수량(주④ 두께 20㎝·100㎡당 2ton) 물값은 계산에 안 넣음 · 장비조합·규격 현장여건·시험포장 따라 바꿀 수 있음(주③) · 막힘: 진동롤러(핸드가이드식) 빠짐 — 칸 이름 「진동롤러(핸드가이드식)」 식에서 못 부름" + }, + { + "열쇠": "1-2-2 기계포설(길어깨)", + "이름": "동상방지층 기계포설(길어깨)", + "결과단위": "원/㎥", + "출처": "건설품셈 토목 1-2-2", + "소유": "공용", + "입력": [ + { "이름": "살수", "고르기": ["살수차", "인력 살수"], "출처": "건설품셈 토목 1-2-2 비고" }, + { "이름": "두께", "단위": "㎝", "출처": "건설품셈 토목 1-2-2 비고" }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + }, + { "이름": "보정작업", "고르기": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "출처": "건설품셈 공통 8-1-7 1" } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-2-2 기계포설(길어깨)).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1018", + "이름": "포설공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-2-2 기계포설(길어깨)).포설공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-2-2 기계포설(길어깨)).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 굴착기(무한궤도) 시간당 경비, 기계='0201-0100', 지역=지역, 보정작업=보정작업)", + "이름": "굴착기 1.0㎥", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-2-2 기계포설(길어깨)).굴착기 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='1306-0120', 지역=지역)", + "이름": "진동롤러 12ton", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-2-2 기계포설(길어깨)).진동롤러 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='7204-0160', 지역=지역)", + "이름": "살수차 16,000ℓ", + "단위": "시간", + "수량": "만약(살수 == '살수차', 찾기(소요량:건설품셈:1-2-2 기계포설(길어깨)).살수차 * 8 / 시공량, 0)" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "만약(살수 == '인력 살수', 1 / 두께, 0)", + "비목": "노무비", + "출처": "건설품셈 토목 1-2-2 비고" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 해석: 인력 살수 = 살수차 빼고 보통인부 100㎡당 1인 · 100㎡ × 두께 = ㎥ 로 바꿈(두께 ㎝ 입력) · 살수량(주④ 두께 20㎝·100㎡당 2ton) 물값은 계산에 안 넣음 · 장비조합·규격 현장여건·시험포장 따라 바꿀 수 있음(주③)" + }, + { + "열쇠": "1-2-3 기계포설(본선)", + "이름": "동상방지층 기계포설(본선)", + "결과단위": "원/㎥", + "출처": "건설품셈 토목 1-2-3", + "소유": "공용", + "입력": [ + { "이름": "살수", "고르기": ["살수차", "인력 살수"], "출처": "건설품셈 토목 1-2-3 비고" }, + { "이름": "두께", "단위": "㎝", "출처": "건설품셈 토목 1-2-3 비고" }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-2-3 기계포설(본선)).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1018", + "이름": "포설공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-2-3 기계포설(본선)).포설공 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='0502-0036', 지역=지역)", + "이름": "모터그레이더 3.6m", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-2-3 기계포설(본선)).모터그레이더 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='1306-0120', 지역=지역)", + "이름": "진동롤러 12ton", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-2-3 기계포설(본선)).진동롤러 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='7204-0160', 지역=지역)", + "이름": "살수차 16,000ℓ", + "단위": "시간", + "수량": "만약(살수 == '살수차', 찾기(소요량:건설품셈:1-2-3 기계포설(본선)).살수차 * 8 / 시공량, 0)" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "만약(살수 == '인력 살수', 1 / 두께, 0)", + "비목": "노무비", + "출처": "건설품셈 토목 1-2-3 비고" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 해석: 인력 살수 = 살수차 빼고 보통인부 100㎡당 1인 · 100㎡ × 두께 = ㎥ 로 바꿈(두께 ㎝ 입력) · 살수량(주④ 두께 20㎝·100㎡당 2ton) 물값은 계산에 안 넣음 · 장비조합·규격 현장여건·시험포장 따라 바꿀 수 있음(주③)" + }, + { + "열쇠": "1-3-1 인력식 소규모장비 포설", + "이름": "보조기층 인력식 소규모장비 포설", + "결과단위": "원/㎥", + "출처": "건설품셈 토목 1-3-1", + "소유": "공용", + "입력": [ + { "이름": "살수", "고르기": ["살수차", "인력 살수"], "출처": "건설품셈 토목 1-3-1 비고" }, + { "이름": "두께", "단위": "㎝", "출처": "건설품셈 토목 1-3-1 비고" }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + }, + { "이름": "보정작업", "고르기": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "출처": "건설품셈 공통 8-1-7 1" } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-3-1 인력식 소규모장비 포설).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1018", + "이름": "포설공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-3-1 인력식 소규모장비 포설).포설공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-3-1 인력식 소규모장비 포설).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 굴착기(무한궤도) 시간당 경비, 기계='0201-0060', 지역=지역, 보정작업=보정작업)", + "이름": "굴착기 0.6㎥", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-3-1 인력식 소규모장비 포설).굴착기 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='7204-0055', 지역=지역)", + "이름": "살수차 5,500ℓ", + "단위": "시간", + "수량": "만약(살수 == '살수차', 찾기(소요량:건설품셈:1-3-1 인력식 소규모장비 포설).살수차 * 8 / 시공량, 0)" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "만약(살수 == '인력 살수', 1 / 두께, 0)", + "비목": "노무비", + "출처": "건설품셈 토목 1-3-1 비고" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 해석: 인력 살수 = 살수차 빼고 보통인부 100㎡당 1인 · 100㎡ × 두께 = ㎥ 로 바꿈(두께 ㎝ 입력) · 살수량(주④ 두께 20㎝·100㎡당 2ton) 물값은 계산에 안 넣음 · 장비조합·규격 현장여건·시험포장 따라 바꿀 수 있음(주③) · 막힘: 진동롤러(핸드가이드식) 빠짐 — 칸 이름 「진동롤러(핸드가이드식)」 식에서 못 부름" + }, + { + "열쇠": "1-3-2 기계포설(길어깨)", + "이름": "보조기층 기계포설(길어깨)", + "결과단위": "원/㎥", + "출처": "건설품셈 토목 1-3-2", + "소유": "공용", + "입력": [ + { "이름": "살수", "고르기": ["살수차", "인력 살수"], "출처": "건설품셈 토목 1-3-2 비고" }, + { "이름": "두께", "단위": "㎝", "출처": "건설품셈 토목 1-3-2 비고" }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + }, + { "이름": "보정작업", "고르기": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "출처": "건설품셈 공통 8-1-7 1" } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-3-2 기계포설(길어깨)).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1018", + "이름": "포설공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-3-2 기계포설(길어깨)).포설공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-3-2 기계포설(길어깨)).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 굴착기(무한궤도) 시간당 경비, 기계='0201-0100', 지역=지역, 보정작업=보정작업)", + "이름": "굴착기 1.0㎥", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-3-2 기계포설(길어깨)).굴착기 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='1306-0120', 지역=지역)", + "이름": "진동롤러 12ton", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-3-2 기계포설(길어깨)).진동롤러 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='7204-0160', 지역=지역)", + "이름": "살수차 16,000ℓ", + "단위": "시간", + "수량": "만약(살수 == '살수차', 찾기(소요량:건설품셈:1-3-2 기계포설(길어깨)).살수차 * 8 / 시공량, 0)" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "만약(살수 == '인력 살수', 1 / 두께, 0)", + "비목": "노무비", + "출처": "건설품셈 토목 1-3-2 비고" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 해석: 인력 살수 = 살수차 빼고 보통인부 100㎡당 1인 · 100㎡ × 두께 = ㎥ 로 바꿈(두께 ㎝ 입력) · 살수량(주④ 두께 20㎝·100㎡당 2ton) 물값은 계산에 안 넣음 · 장비조합·규격 현장여건·시험포장 따라 바꿀 수 있음(주③)" + }, + { + "열쇠": "1-3-3 기계포설(본선)", + "이름": "보조기층 기계포설(본선)", + "결과단위": "원/㎥", + "출처": "건설품셈 토목 1-3-3", + "소유": "공용", + "입력": [ + { "이름": "살수", "고르기": ["살수차", "인력 살수"], "출처": "건설품셈 토목 1-3-3 비고" }, + { "이름": "두께", "단위": "㎝", "출처": "건설품셈 토목 1-3-3 비고" }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-3-3 기계포설(본선)).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1018", + "이름": "포설공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-3-3 기계포설(본선)).포설공 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='0502-0036', 지역=지역)", + "이름": "모터그레이더 3.6m", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-3-3 기계포설(본선)).모터그레이더 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='1306-0120', 지역=지역)", + "이름": "진동롤러 12ton", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-3-3 기계포설(본선)).진동롤러 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='7204-0160', 지역=지역)", + "이름": "살수차 16,000ℓ", + "단위": "시간", + "수량": "만약(살수 == '살수차', 찾기(소요량:건설품셈:1-3-3 기계포설(본선)).살수차 * 8 / 시공량, 0)" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "만약(살수 == '인력 살수', 1 / 두께, 0)", + "비목": "노무비", + "출처": "건설품셈 토목 1-3-3 비고" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 해석: 인력 살수 = 살수차 빼고 보통인부 100㎡당 1인 · 100㎡ × 두께 = ㎥ 로 바꿈(두께 ㎝ 입력) · 살수량(주④ 두께 20㎝·100㎡당 2ton) 물값은 계산에 안 넣음 · 장비조합·규격 현장여건·시험포장 따라 바꿀 수 있음(주③)" + }, + { + "열쇠": "1-4-1 인력식 소규모장비 포설", + "이름": "입도조정기층 인력식 소규모장비 포설", + "결과단위": "원/㎥", + "출처": "건설품셈 토목 1-4-1", + "소유": "공용", + "입력": [ + { "이름": "살수", "고르기": ["살수차", "인력 살수"], "출처": "건설품셈 토목 1-4-1 비고" }, + { "이름": "두께", "단위": "㎝", "출처": "건설품셈 토목 1-4-1 비고" }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + }, + { "이름": "보정작업", "고르기": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "출처": "건설품셈 공통 8-1-7 1" } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-4-1 인력식 소규모장비 포설).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1018", + "이름": "포설공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-4-1 인력식 소규모장비 포설).포설공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-4-1 인력식 소규모장비 포설).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 굴착기(무한궤도) 시간당 경비, 기계='0201-0060', 지역=지역, 보정작업=보정작업)", + "이름": "굴착기 0.6㎥", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-4-1 인력식 소규모장비 포설).굴착기 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='7204-0055', 지역=지역)", + "이름": "살수차 5,500ℓ", + "단위": "시간", + "수량": "만약(살수 == '살수차', 찾기(소요량:건설품셈:1-4-1 인력식 소규모장비 포설).살수차 * 8 / 시공량, 0)" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "만약(살수 == '인력 살수', 1 / 두께, 0)", + "비목": "노무비", + "출처": "건설품셈 토목 1-4-1 비고" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 해석: 인력 살수 = 살수차 빼고 보통인부 100㎡당 1인 · 100㎡ × 두께 = ㎥ 로 바꿈(두께 ㎝ 입력) · 살수량(주④ 두께 20㎝·100㎡당 2ton) 물값은 계산에 안 넣음 · 장비조합·규격 현장여건·시험포장 따라 바꿀 수 있음(주③) · 막힘: 진동롤러(핸드가이드식) 빠짐 — 칸 이름 「진동롤러(핸드가이드식)」 식에서 못 부름" + }, + { + "열쇠": "1-4-2 기계포설(길어깨)", + "이름": "입도조정기층 기계포설(길어깨)", + "결과단위": "원/㎥", + "출처": "건설품셈 토목 1-4-2", + "소유": "공용", + "입력": [ + { "이름": "살수", "고르기": ["살수차", "인력 살수"], "출처": "건설품셈 토목 1-4-2 비고" }, + { "이름": "두께", "단위": "㎝", "출처": "건설품셈 토목 1-4-2 비고" }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + }, + { "이름": "보정작업", "고르기": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "출처": "건설품셈 공통 8-1-7 1" } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-4-2 기계포설(길어깨)).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1018", + "이름": "포설공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-4-2 기계포설(길어깨)).포설공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-4-2 기계포설(길어깨)).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 굴착기(무한궤도) 시간당 경비, 기계='0201-0100', 지역=지역, 보정작업=보정작업)", + "이름": "굴착기 1.0㎥", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-4-2 기계포설(길어깨)).굴착기 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='1306-0120', 지역=지역)", + "이름": "진동롤러 12ton", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-4-2 기계포설(길어깨)).진동롤러 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='7204-0160', 지역=지역)", + "이름": "살수차 16,000ℓ", + "단위": "시간", + "수량": "만약(살수 == '살수차', 찾기(소요량:건설품셈:1-4-2 기계포설(길어깨)).살수차 * 8 / 시공량, 0)" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "만약(살수 == '인력 살수', 1 / 두께, 0)", + "비목": "노무비", + "출처": "건설품셈 토목 1-4-2 비고" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 해석: 인력 살수 = 살수차 빼고 보통인부 100㎡당 1인 · 100㎡ × 두께 = ㎥ 로 바꿈(두께 ㎝ 입력) · 살수량(주④ 두께 20㎝·100㎡당 2ton) 물값은 계산에 안 넣음 · 장비조합·규격 현장여건·시험포장 따라 바꿀 수 있음(주③)" + }, + { + "열쇠": "1-4-3 기계포설(본선)", + "이름": "입도조정기층 기계포설(본선)", + "결과단위": "원/㎥", + "출처": "건설품셈 토목 1-4-3", + "소유": "공용", + "입력": [ + { "이름": "살수", "고르기": ["살수차", "인력 살수"], "출처": "건설품셈 토목 1-4-3 비고" }, + { "이름": "두께", "단위": "㎝", "출처": "건설품셈 토목 1-4-3 비고" }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-4-3 기계포설(본선)).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1018", + "이름": "포설공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-4-3 기계포설(본선)).포설공 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='0502-0036', 지역=지역)", + "이름": "모터그레이더 3.6m", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-4-3 기계포설(본선)).모터그레이더 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='1306-0120', 지역=지역)", + "이름": "진동롤러 12ton", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-4-3 기계포설(본선)).진동롤러 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='7204-0160', 지역=지역)", + "이름": "살수차 16,000ℓ", + "단위": "시간", + "수량": "만약(살수 == '살수차', 찾기(소요량:건설품셈:1-4-3 기계포설(본선)).살수차 * 8 / 시공량, 0)" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "만약(살수 == '인력 살수', 1 / 두께, 0)", + "비목": "노무비", + "출처": "건설품셈 토목 1-4-3 비고" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 해석: 인력 살수 = 살수차 빼고 보통인부 100㎡당 1인 · 100㎡ × 두께 = ㎥ 로 바꿈(두께 ㎝ 입력) · 살수량(주④ 두께 20㎝·100㎡당 2ton) 물값은 계산에 안 넣음 · 장비조합·규격 현장여건·시험포장 따라 바꿀 수 있음(주③)" + }, + { + "열쇠": "1-5-1 텍코팅 및 프라임 코팅 살포", + "이름": "텍코팅 및 프라임 코팅 살포", + "결과단위": "원/㎡", + "출처": "건설품셈 토목 1-5-1", + "소유": "공용", + "입력": [ + { "이름": "구분", "고르기": ["인력식", "기계식"] }, + { "이름": "비산방지살포량", "단위": "ℓ/㎡", "출처": "건설품셈 토목 1-5-1 비고" }, + { "이름": "양생모래량", "단위": "㎥/㎡", "출처": "건설품셈 토목 1-5-1 비고" }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-5-1 텍코팅 및 프라임 코팅 살포, 구분=구분).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-5-1 텍코팅 및 프라임 코팅 살포, 구분=구분).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "비산방지살포량 / 2000", + "비목": "노무비", + "출처": "건설품셈 토목 1-5-1 비고" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "양생모래량 / 2", + "비목": "노무비", + "출처": "건설품셈 토목 1-5-1 비고" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비(조종원 없음), 기계='3302-0038', 지역=지역)", + "이름": "아스팔트 디스트리뷰터 3,800ℓ", + "단위": "시간", + "수량": "만약(구분 == '기계식', 찾기(소요량:건설품셈:1-5-1 텍코팅 및 프라임 코팅 살포, 구분=구분).아스팔트디스트리뷰터 * 8 / 시공량, 0)" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 비산 방지 보통인부 2,000ℓ당 1인 · 양생 모래 2㎥당 1인(비고) — 살포량·모래량 설계자 입력(없으면 0) · 해석: 아스팔트 디스트리뷰터는 기계 요소에 조종원 직종이 비어 조종원 없는 경비 로직으로 부름(조종원 빠짐) · 역청재 재료 원문 표에 없음 · 막힘: 아스팔트스프레어 빠짐 — 칸 이름 「아스팔트스프레어(수동식 살포기)」 식에서 못 부름" + }, + { + "열쇠": "1-5-2 아스팔트 기층 소규모포설", + "이름": "아스팔트 기층 소규모포설", + "결과단위": "원/㎡", + "출처": "건설품셈 토목 1-5-2", + "소유": "공용", + "입력": [ + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-5-2 아스팔트 기층 소규모포설).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1019", + "이름": "포장공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-5-2 아스팔트 기층 소규모포설).포장공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-5-2 아스팔트 기층 소규모포설).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='1730-0015', 지역=지역)", + "이름": "플레이트 콤팩터 1.5ton", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-5-2 아스팔트 기층 소규모포설).플레이트콤팩터 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='7204-0055', 지역=지역)", + "이름": "살수차 5,500ℓ", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-5-2 아스팔트 기층 소규모포설).살수차 * 8 / 시공량" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 1층 포설두께 7.5㎝ 이하(주②) · 아스콘 재료 원문 표에 없음 · 막힘: 진동롤러(핸드가이드식) 빠짐 — 칸 이름 「진동롤러(핸드가이드식)」 식에서 못 부름 · 막힘: 로더(타이어) 빠짐 — 칸 이름 「로더(타이어)」 식에서 못 부름" + }, + { + "열쇠": "1-5-5 아스팔트 표층 소규모포설", + "이름": "아스팔트 표층 소규모포설", + "결과단위": "원/㎡", + "출처": "건설품셈 토목 1-5-5", + "소유": "공용", + "입력": [ + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-5-5 아스팔트 표층 소규모포설).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1019", + "이름": "포장공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-5-5 아스팔트 표층 소규모포설).포장공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-5-5 아스팔트 표층 소규모포설).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='1730-0015', 지역=지역)", + "이름": "플레이트 콤팩터 1.5ton", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-5-5 아스팔트 표층 소규모포설).플레이트콤팩터 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='7204-0055', 지역=지역)", + "이름": "살수차 5,500ℓ", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-5-5 아스팔트 표층 소규모포설).살수차 * 8 / 시공량" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 1층 포설두께 7.5㎝ 이하(주②) · 아스콘 재료 원문 표에 없음 · 막힘: 진동롤러(핸드가이드식) 빠짐 — 칸 이름 「진동롤러(핸드가이드식)」 식에서 못 부름 · 막힘: 로더(타이어) 빠짐 — 칸 이름 「로더(타이어)」 식에서 못 부름" + }, + { + "열쇠": "1-5-6 아스팔트 표층 기계포설(소형장비)", + "이름": "아스팔트 표층 기계포설(소형장비)", + "결과단위": "원/㎡", + "출처": "건설품셈 토목 1-5-6", + "소유": "공용", + "입력": [ + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + }, + { "이름": "보정작업", "고르기": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "출처": "건설품셈 공통 8-1-7 1" } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-5-6 아스팔트 표층 기계포설(소형장비)).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1019", + "이름": "포장공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-5-6 아스팔트 표층 기계포설(소형장비)).포장공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-5-6 아스팔트 표층 기계포설(소형장비)).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 굴착기(무한궤도) 시간당 경비, 기계='0201-0060', 지역=지역, 보정작업=보정작업)", + "이름": "굴착기 0.6㎥", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-5-6 아스팔트 표층 기계포설(소형장비)).굴착기 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='1106-0010', 지역=지역)", + "이름": "머캐덤롤러 8~10ton", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-5-6 아스팔트 표층 기계포설(소형장비)).머캐덤롤러 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='1206-0008', 지역=지역)", + "이름": "탠덤롤러 5∼8ton", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-5-6 아스팔트 표층 기계포설(소형장비)).탠덤롤러 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='7204-0055', 지역=지역)", + "이름": "살수차 5,500ℓ", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-5-6 아스팔트 표층 기계포설(소형장비)).살수차 * 8 / 시공량" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 1층 포설두께 5~7㎝(주②) · 아스콘 재료 원문 표에 없음 · 막힘: 아스팔트 피니셔 빠짐 — 칸 이름 「아스팔트 피니셔」 식에서 못 부름 · 막힘: 타이어롤러 줄 빠짐 — 기계 요소에 타이어롤러 없음" + }, + { + "열쇠": "1-5-7 아스팔트 표층 기계포설(대형장비)", + "이름": "아스팔트 표층 기계포설(대형장비)", + "결과단위": "원/㎡", + "출처": "건설품셈 토목 1-5-7", + "소유": "공용", + "입력": [ + { "이름": "시공폭", "고르기": ["2m≤시공폭<3m", "3m≤시공폭"] }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-5-7 아스팔트 표층 기계포설(대형장비), 시공폭=시공폭).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1019", + "이름": "포장공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-5-7 아스팔트 표층 기계포설(대형장비), 시공폭=시공폭).포장공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-5-7 아스팔트 표층 기계포설(대형장비), 시공폭=시공폭).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='1106-0012', 지역=지역)", + "이름": "머캐덤롤러 10∼12ton", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-5-7 아스팔트 표층 기계포설(대형장비), 시공폭=시공폭).머캐덤롤러 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='1206-0008', 지역=지역)", + "이름": "탠덤롤러 5∼8ton", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-5-7 아스팔트 표층 기계포설(대형장비), 시공폭=시공폭).탠덤롤러 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='7204-0160', 지역=지역)", + "이름": "살수차 16,000ℓ", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-5-7 아스팔트 표층 기계포설(대형장비), 시공폭=시공폭).살수차 * 8 / 시공량" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 추가 장비(아스팔트온도조절장비 등) 기계경비 별도(주⑤) · 아스콘 재료 원문 표에 없음 · 막힘: 아스팔트 피니셔 빠짐 — 칸 이름 「아스팔트 피니셔」 식에서 못 부름 · 막힘: 타이어롤러 줄 빠짐 — 기계 요소에 타이어롤러 없음" + }, + { + "열쇠": "1-5-8 쇄석 매스틱 아스팔트(SMA) 표층 포설", + "이름": "쇄석 매스틱 아스팔트(SMA) 표층 포설", + "결과단위": "원/㎡", + "출처": "건설품셈 토목 1-5-8", + "소유": "공용", + "입력": [ + { "이름": "시공폭", "고르기": ["2m≤시공폭<3m", "3m≤시공폭"] }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-5-8 쇄석 매스틱 아스팔트(SMA) 표층 포설, 시공폭=시공폭).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1019", + "이름": "포장공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-5-8 쇄석 매스틱 아스팔트(SMA) 표층 포설, 시공폭=시공폭).포장공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-5-8 쇄석 매스틱 아스팔트(SMA) 표층 포설, 시공폭=시공폭).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='1106-0012', 지역=지역)", + "이름": "머캐덤롤러 10∼12ton", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-5-8 쇄석 매스틱 아스팔트(SMA) 표층 포설, 시공폭=시공폭).머캐덤롤러 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='1206-0008', 지역=지역)", + "이름": "탠덤롤러 5∼8ton", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-5-8 쇄석 매스틱 아스팔트(SMA) 표층 포설, 시공폭=시공폭).탠덤롤러 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='7204-0160', 지역=지역)", + "이름": "살수차 16,000ℓ", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-5-8 쇄석 매스틱 아스팔트(SMA) 표층 포설, 시공폭=시공폭).살수차 * 8 / 시공량" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 시공폭 2m 미만은 1-5-6(주③) · 추가 장비(아스팔트온도조절장비 등) 기계경비 별도(주⑤) · 아스콘 재료 원문 표에 없음 · 막힘: 아스팔트 피니셔 빠짐 — 칸 이름 「아스팔트 피니셔」 식에서 못 부름" + }, + { + "열쇠": "1-5-9 배수성·저소음 아스팔트 표층 포설", + "이름": "배수성·저소음 아스팔트 표층 포설", + "결과단위": "원/㎡", + "출처": "건설품셈 토목 1-5-9", + "소유": "공용", + "입력": [ + { "이름": "시공폭", "고르기": ["2m≤시공폭<3m", "3m≤시공폭"] }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-5-9 배수성·저소음 아스팔트 표층 포설, 시공폭=시공폭).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1019", + "이름": "포장공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-5-9 배수성·저소음 아스팔트 표층 포설, 시공폭=시공폭).포장공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-5-9 배수성·저소음 아스팔트 표층 포설, 시공폭=시공폭).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='1106-0012', 지역=지역)", + "이름": "머캐덤롤러 10∼12ton", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-5-9 배수성·저소음 아스팔트 표층 포설, 시공폭=시공폭).머캐덤롤러 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='1206-0008', 지역=지역)", + "이름": "탠덤롤러 5∼8ton", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-5-9 배수성·저소음 아스팔트 표층 포설, 시공폭=시공폭).탠덤롤러 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='7204-0160', 지역=지역)", + "이름": "살수차 16,000ℓ", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-5-9 배수성·저소음 아스팔트 표층 포설, 시공폭=시공폭).살수차 * 8 / 시공량" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 시공폭 2m 미만은 1-5-6(주③) · 추가 장비(아스팔트온도조절장비 등) 기계경비 별도(주⑤) · 아스콘 재료 원문 표에 없음 · 막힘: 아스팔트 피니셔 빠짐 — 칸 이름 「아스팔트 피니셔」 식에서 못 부름" + }, + { + "열쇠": "1-6-1 린 콘크리트 기층 포설", + "이름": "린 콘크리트 기층 포설", + "결과단위": "원/㎥", + "출처": "건설품셈 토목 1-6-1", + "소유": "공용", + "입력": [ + { "이름": "포장", "고르기": ["일반포장", "터널포장"] }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-6-1 린 콘크리트 기층 포설, 포장=포장).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1019", + "이름": "포장공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-6-1 린 콘크리트 기층 포설, 포장=포장).포장공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-6-1 린 콘크리트 기층 포설, 포장=포장).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='3201-0003', 지역=지역)", + "이름": "아스팔트 피니셔 3m", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-6-1 린 콘크리트 기층 포설, 포장=포장).아스팔트피니셔 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='1306-0100', 지역=지역)", + "이름": "진동롤러 10ton", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-6-1 린 콘크리트 기층 포설, 포장=포장).진동롤러 * 8 / 시공량" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 린 콘크리트 재료 원문 표에 없음 · 막힘: 타이어롤러 줄 빠짐 — 기계 요소에 타이어롤러 없음" + }, + { + "열쇠": "1-6-2 표층 인력포설", + "이름": "콘크리트 표층 인력포설", + "결과단위": "원/㎥", + "출처": "건설품셈 토목 1-6-2", + "소유": "공용", + "입력": [{ "이름": "형식", "고르기": ["A-Type", "B-Type"] }, { "이름": "두께", "고르기": ["20㎝", "30㎝", "40㎝"] }], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-6-2 표층 인력포설, 형식=형식, 두께=두께).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1019", + "이름": "포장공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-6-2 표층 인력포설, 형식=형식, 두께=두께).포장공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-6-2 표층 인력포설, 형식=형식, 두께=두께).보통인부 / 시공량", + "비목": "노무비" + } + ], + "덧줄": [], + "끝수": null, + "비고": "형식 A-Type = 믹서트럭 직접 타설 · B-Type = 후진 진입·경운기 운반 타설(적용기준 표) · 거푸집·줄눈 별도(주③) · 콘크리트 재료 원문 표에 없음" + }, + { + "열쇠": "1-6-3 콘크리트 표층 기계포설(소형장비)", + "이름": "콘크리트 표층 기계포설(소형장비)", + "결과단위": "원/㎥", + "출처": "건설품셈 토목 1-6-3", + "소유": "공용", + "입력": [ + { "이름": "포장", "고르기": ["일반포장", "터널포장", "공항포장"] }, + { "이름": "지장물", "고르기": ["해당 없음", "연속 포설 불가"], "출처": "건설품셈 토목 1-6-3 비고" }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + }, + { "이름": "보정작업", "고르기": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "출처": "건설품셈 공통 8-1-7 1" } + ], + "중간": [ + { + "이름": "시공량", + "식": "찾기(소요량:건설품셈:1-6-3 콘크리트 표층 기계포설(소형장비), 포장=포장).시공량 * (1 - 만약(포장 == '공항포장', 만약(지장물 == '연속 포설 불가', 찾기(소요량:건설품셈:1-6-3 콘크리트 표층 기계포설(소형장비) 계수 비고, 구분='공항포장에서 집수정, 기초 등 지장물에 의해 이동이 빈번하게 발생하여 연속적인 포설이 불가능할 경우 시공량의 15%를 감한다.').값, 0), 0) / 100)", + "출처": "건설품셈 토목 1-6-3 비고" + } + ], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1019", + "이름": "포장공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-6-3 콘크리트 표층 기계포설(소형장비), 포장=포장).포장공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-6-3 콘크리트 표층 기계포설(소형장비), 포장=포장).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-6-3 콘크리트 표층 기계포설(소형장비), 포장=포장).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 굴착기(무한궤도) 시간당 경비, 기계='0201-0100', 지역=지역, 보정작업=보정작업)", + "이름": "굴착기 1.0㎥", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-6-3 콘크리트 표층 기계포설(소형장비), 포장=포장).굴착기 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='7204-0160', 지역=지역)", + "이름": "살수차 16,000ℓ", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-6-3 콘크리트 표층 기계포설(소형장비), 포장=포장).살수차 * 8 / 시공량" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 공항포장에서 지장물로 연속 포설 불가 시 시공량 15% 감(비고) — 공항포장에만 · 해석: 감은 시공량에 곱함 · 양생제·마대·잡품 재료비 별도(주⑤) · 막힘: 콘크리트 페이버 빠짐 — 칸 이름 「콘크리트 페이버」 식에서 못 부름" + }, + { + "열쇠": "1-6-4 콘크리트 표층 기계포설(대형장비)", + "이름": "콘크리트 표층 기계포설(대형장비)", + "결과단위": "원/㎥", + "출처": "건설품셈 토목 1-6-4", + "소유": "공용", + "입력": [ + { "이름": "포장", "고르기": ["일반포장", "터널포장", "공항포장"] }, + { "이름": "지장물", "고르기": ["해당 없음", "연속 포설 불가"], "출처": "건설품셈 토목 1-6-4 비고" }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + }, + { "이름": "보정작업", "고르기": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "출처": "건설품셈 공통 8-1-7 1" } + ], + "중간": [ + { + "이름": "시공량", + "식": "찾기(소요량:건설품셈:1-6-4 콘크리트 표층 기계포설(대형장비), 포장=포장).시공량 * (1 - 만약(포장 == '공항포장', 만약(지장물 == '연속 포설 불가', 찾기(소요량:건설품셈:1-6-4 콘크리트 표층 기계포설(대형장비) 계수 비고, 구분='공항포장에서 집수정, 기초 등 지장물에 의해 이동이 빈번하게 발생하여 연속적인 포설이 불가능할 경우 시공량의 15%를 감한다.').값, 0), 0) / 100)", + "출처": "건설품셈 토목 1-6-4 비고" + } + ], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1019", + "이름": "포장공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-6-4 콘크리트 표층 기계포설(대형장비), 포장=포장).포장공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-6-4 콘크리트 표층 기계포설(대형장비), 포장=포장).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-6-4 콘크리트 표층 기계포설(대형장비), 포장=포장).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 굴착기(무한궤도) 시간당 경비, 기계='0201-0100', 지역=지역, 보정작업=보정작업)", + "이름": "굴착기 1.0㎥", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-6-4 콘크리트 표층 기계포설(대형장비), 포장=포장).굴착기 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='7204-0160', 지역=지역)", + "이름": "살수차 16,000ℓ", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-6-4 콘크리트 표층 기계포설(대형장비), 포장=포장).살수차 * 8 / 시공량" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 공항포장에서 지장물로 연속 포설 불가 시 시공량 15% 감(비고) — 공항포장에만 · 해석: 감은 시공량에 곱함 · 양생제·마대·잡품 재료비 별도(주⑤) · 막힘: 콘크리트 페이버 빠짐 — 칸 이름 「콘크리트 페이버」 식에서 못 부름" + }, + { + "열쇠": "1-6-5 기계포설 장비조립 및 해체", + "이름": "콘크리트 페이버 조립 및 해체", + "결과단위": "원/회", + "출처": "건설품셈 토목 1-6-5", + "소유": "공용", + "입력": [ + { "이름": "구분", "고르기": ["외부 반출/반입", "작업구간 이동"] }, + { "이름": "작업", "고르기": ["조립", "해체"] }, + { + "이름": "크레인", + "고르기": [ + "2105-0005", + "2104-0010", + "2104-0015", + "2104-0020", + "2104-0025", + "2104-0030", + "2104-0035", + "2104-0040", + "2104-0045", + "2104-0050", + "2104-0060", + "2104-0070", + "2104-0080", + "2104-0100", + "2104-0130", + "2104-0160", + "2104-0200", + "2104-0220", + "2104-0250", + "2104-0300", + "2105-0002", + "2105-0003", + "2105-0010", + "2105-0015", + "2105-0018" + ] + }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + } + ], + "중간": [{ "이름": "소요일수", "식": "찾기(소요량:건설품셈:1-6-5 기계포설 장비조립 및 해체, 구분=구분, 작업=작업).소요일수" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1051", + "이름": "기계설비공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-6-5 기계포설 장비조립 및 해체, 구분=구분, 작업=작업).기계설비공 * 소요일수", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1009", + "이름": "철공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-6-5 기계포설 장비조립 및 해체, 구분=구분, 작업=작업).철공 * 소요일수", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-6-5 기계포설 장비조립 및 해체, 구분=구분, 작업=작업).특별인부 * 소요일수", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계=크레인, 지역=지역)", + "이름": "크레인", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-6-5 기계포설 장비조립 및 해체, 구분=구분, 작업=작업).크레인 * 8 * 소요일수" + } + ], + "덧줄": [ + { + "이름": "공구손료 및 경장비", + "식": "((찾기(소요량:건설품셈:1-6-5 기계포설 장비조립 및 해체, 구분=구분, 작업=작업).기계설비공 * 소요일수) * 인력:건설노임:1051 + (찾기(소요량:건설품셈:1-6-5 기계포설 장비조립 및 해체, 구분=구분, 작업=작업).철공 * 소요일수) * 인력:건설노임:1009 + (찾기(소요량:건설품셈:1-6-5 기계포설 장비조립 및 해체, 구분=구분, 작업=작업).특별인부 * 소요일수) * 인력:건설노임:1003) * 찾기(소요량:건설품셈:1-6-5 기계포설 장비조립 및 해체 계수 주⑤, 구분='공구손료 및 경장비(소형브레이커, 용접기 등)의 기계경비').값 / 100", + "비목": "경비", + "출처": "건설품셈 토목 1-6-5 주⑤" + } + ], + "끝수": null, + "비고": "해석: 인력품 = 인력 호표 줄 금액 합(기계 조종원 제외) · 해석: 편성 인원·장비는 1일 편성 — 소요일수를 곱함 · 크레인 1대 × 1일 8시간 · 크레인 종류·규격 설계자 고름 — 원문 규격 없음(주④ 현장여건)" + }, + { + "열쇠": "1-6-6 포장줄눈 절단", + "이름": "포장줄눈 절단", + "결과단위": "원/m", + "출처": "건설품셈 토목 1-6-6", + "소유": "공용", + "입력": [ + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-6-6 포장줄눈 절단).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-6-6 포장줄눈 절단).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-6-6 포장줄눈 절단).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비(조종원 없음), 기계='4430-0400', 지역=지역)", + "이름": "커터 320~400㎜", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-6-6 포장줄눈 절단).커터 * 8 / 시공량" + } + ], + "덧줄": [ + { + "이름": "공구손료 및 경장비", + "식": "((찾기(소요량:건설품셈:1-6-6 포장줄눈 절단).특별인부 / 시공량) * 인력:건설노임:1003 + (찾기(소요량:건설품셈:1-6-6 포장줄눈 절단).보통인부 / 시공량) * 인력:건설노임:1002) * 찾기(소요량:건설품셈:1-6-6 포장줄눈 절단 계수 주③, 구분='공구손료 및 경장비(동력분무기 등)의 기계경비').값 / 100", + "비목": "경비", + "출처": "건설품셈 토목 1-6-6 주③" + } + ], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 해석: 인력품 = 인력 호표 줄 금액 합(기계 조종원 제외) · 해석: 커터는 기계 요소에 조종원 직종이 비어 조종원 없는 경비 로직으로 부름 · 블레이드·물 별도(주④)" + }, + { + "열쇠": "1-6-7 포장줄눈 설치", + "이름": "포장줄눈 설치", + "결과단위": "원/m", + "출처": "건설품셈 토목 1-6-7", + "소유": "공용", + "입력": [], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-6-7 포장줄눈 설치).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-6-7 포장줄눈 설치).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-6-7 포장줄눈 설치).보통인부 / 시공량", + "비목": "노무비" + } + ], + "덧줄": [], + "끝수": null, + "비고": "줄눈재·백업재 재료비 별도(주③)" + }, + { + "열쇠": "1-7-1 보도용 블록 설치(소형)", + "이름": "보도용 블록 설치(소형)", + "결과단위": "원/㎡", + "출처": "건설품셈 토목 1-7-1", + "소유": "공용", + "입력": [ + { "이름": "형식", "고르기": ["A-Type", "B-Type"] }, + { "이름": "유도블록", "고르기": ["아님", "유도・점자블록"], "출처": "건설품셈 토목 1-7-1 비고" }, + { "이름": "정밀절단", "고르기": ["전동절단기", "아님"], "출처": "건설품셈 토목 1-7-1 비고" }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + }, + { "이름": "보정작업", "고르기": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "출처": "건설품셈 공통 8-1-7 1" } + ], + "중간": [ + { + "이름": "시공량", + "식": "찾기(소요량:건설품셈:1-7-1 보도용 블록 설치(소형), 형식=형식).시공량 * (1 - 만약(유도블록 == '아님', 0, 찾기(소요량:건설품셈:1-7-1 보도용 블록 설치(소형) 계수 비고, 구분='유도・점자블록을 설치하는 경우 시공량의 10%를 감하여 적용한다.').값) / 100)", + "출처": "건설품셈 토목 1-7-1 비고" + }, + { "이름": "굴착기", "식": "만약(형식 == 'A-Type', '0201-0060', '0201-0040')", "출처": "건설품셈 토목 1-7-1 「굴착기규격」 0.6㎥ · 0.4㎥" } + ], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1019", + "이름": "포장공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-7-1 보도용 블록 설치(소형), 형식=형식).포장공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "(찾기(소요량:건설품셈:1-7-1 보도용 블록 설치(소형), 형식=형식).특별인부 - 만약(정밀절단 == '아님', 찾기(소요량:건설품셈:1-7-1 보도용 블록 설치(소형) 계수 비고, 구분='블록 정밀절단(전동절단기)에 의한 시공이 아닌 경우, 특별인부 1인을 감하여 적용한다.').값, 0)) / 시공량", + "비목": "노무비", + "출처": "건설품셈 토목 1-7-1 비고" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-7-1 보도용 블록 설치(소형), 형식=형식).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 굴착기(무한궤도) 시간당 경비, 기계=굴착기, 지역=지역, 보정작업=보정작업)", + "이름": "굴착기", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-7-1 보도용 블록 설치(소형), 형식=형식).굴착기 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='1730-0015', 지역=지역)", + "이름": "플레이트 콤팩터 1.5ton", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-7-1 보도용 블록 설치(소형), 형식=형식).플레이트콤팩터 * 8 / 시공량" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 형식 A = 장비 이동·적재 쉬운 구간 · B = 협소 구간(적용기준 표) · 해석: 감은 시공량에 곱함 · 블록·모래 재료 원문 표에 없음" + }, + { + "열쇠": "1-7-2 보도용 블록 설치(대형)", + "이름": "보도용 블록 설치(대형)", + "결과단위": "원/㎡", + "출처": "건설품셈 토목 1-7-2", + "소유": "공용", + "입력": [ + { "이름": "형식", "고르기": ["A-Type", "B-Type"] }, + { "이름": "유도블록", "고르기": ["아님", "유도・점자블록"], "출처": "건설품셈 토목 1-7-2 비고" }, + { "이름": "정밀절단", "고르기": ["전동절단기", "아님"], "출처": "건설품셈 토목 1-7-2 비고" }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + }, + { "이름": "보정작업", "고르기": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "출처": "건설품셈 공통 8-1-7 1" } + ], + "중간": [ + { + "이름": "시공량", + "식": "찾기(소요량:건설품셈:1-7-2 보도용 블록 설치(대형), 형식=형식).시공량 * (1 - 만약(유도블록 == '아님', 0, 찾기(소요량:건설품셈:1-7-2 보도용 블록 설치(대형) 계수 비고, 구분='유도・점자블록을 설치하는 경우 시공량의 10%를 감하여 적용한다.').값) / 100)", + "출처": "건설품셈 토목 1-7-2 비고" + }, + { "이름": "굴착기", "식": "만약(형식 == 'A-Type', '0201-0060', '0201-0040')", "출처": "건설품셈 토목 1-7-2 「굴착기규격」 0.6㎥ · 0.4㎥" } + ], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1019", + "이름": "포장공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-7-2 보도용 블록 설치(대형), 형식=형식).포장공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "(찾기(소요량:건설품셈:1-7-2 보도용 블록 설치(대형), 형식=형식).특별인부 - 만약(정밀절단 == '아님', 찾기(소요량:건설품셈:1-7-2 보도용 블록 설치(대형) 계수 비고, 구분='블록 정밀절단(전동절단기)에 의한 시공이 아닌 경우, 특별인부 1인을 감하여 적용한다.').값, 0)) / 시공량", + "비목": "노무비", + "출처": "건설품셈 토목 1-7-2 비고" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-7-2 보도용 블록 설치(대형), 형식=형식).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 굴착기(무한궤도) 시간당 경비, 기계=굴착기, 지역=지역, 보정작업=보정작업)", + "이름": "굴착기", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-7-2 보도용 블록 설치(대형), 형식=형식).굴착기 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='1730-0015', 지역=지역)", + "이름": "플레이트 콤팩터 1.5ton", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-7-2 보도용 블록 설치(대형), 형식=형식).플레이트콤팩터 * 8 / 시공량" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 형식 A = 장비 이동·적재 쉬운 구간 · B = 협소 구간(적용기준 표) · 해석: 감은 시공량에 곱함 · 블록·모래 재료 원문 표에 없음" + }, + { + "열쇠": "1-7-3 투수아스팔트 표층 소규모포설", + "이름": "투수아스팔트 표층 소규모포설", + "결과단위": "원/㎡", + "출처": "건설품셈 토목 1-7-3", + "소유": "공용", + "입력": [ + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-7-3 투수아스팔트 표층 소규모포설).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1019", + "이름": "포장공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-7-3 투수아스팔트 표층 소규모포설).포장공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-7-3 투수아스팔트 표층 소규모포설).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='7204-0055', 지역=지역)", + "이름": "살수차 5,500ℓ", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-7-3 투수아스팔트 표층 소규모포설).살수차 * 8 / 시공량" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 필터층·보조기층·기층 별도(주④) · 아스콘 재료 원문 표에 없음 · 막힘: 로더(타이어) 빠짐 — 칸 이름 「로더(타이어)」 식에서 못 부름 · 막힘: 진동롤러(핸드가이드식) 빠짐 — 칸 이름 「진동롤러(핸드가이드식)」 식에서 못 부름 · 막힘: 플레이트 콤팩터 빠짐 — 칸 이름 「플레이트 콤팩터」 식에서 못 부름" + }, + { + "열쇠": "1-7-4 투수아스팔트 표층 기계포설(소형장비)", + "이름": "투수아스팔트 표층 기계포설(소형장비)", + "결과단위": "원/㎡", + "출처": "건설품셈 토목 1-7-4", + "소유": "공용", + "입력": [ + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + }, + { "이름": "보정작업", "고르기": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "출처": "건설품셈 공통 8-1-7 1" } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-7-4 투수아스팔트 표층 기계포설(소형장비)).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1019", + "이름": "포장공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-7-4 투수아스팔트 표층 기계포설(소형장비)).포장공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-7-4 투수아스팔트 표층 기계포설(소형장비)).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 굴착기(무한궤도) 시간당 경비, 기계='0201-0060', 지역=지역, 보정작업=보정작업)", + "이름": "굴착기 0.6㎥", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-7-4 투수아스팔트 표층 기계포설(소형장비)).굴착기 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='1106-0010', 지역=지역)", + "이름": "머캐덤롤러 8∼10ton", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-7-4 투수아스팔트 표층 기계포설(소형장비)).머캐덤롤러 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='1206-0008', 지역=지역)", + "이름": "탠덤롤러 5∼8ton", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-7-4 투수아스팔트 표층 기계포설(소형장비)).탠덤롤러 * 8 / 시공량" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='7204-0160', 지역=지역)", + "이름": "살수차 16,000ℓ", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-7-4 투수아스팔트 표층 기계포설(소형장비)).살수차 * 8 / 시공량" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 필터층·보조기층·기층 별도(주④) · 아스콘 재료 원문 표에 없음 · 막힘: 아스팔트 피니셔 빠짐 — 칸 이름 「아스팔트 피니셔」 식에서 못 부름" + }, + { + "열쇠": "1-7-5 탄성포장재 포설", + "이름": "탄성포장재 포설", + "결과단위": "원/㎡", + "출처": "건설품셈 토목 1-7-5", + "소유": "공용", + "입력": [ + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-7-5 탄성포장재 포설).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-7-5 탄성포장재 포설).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-7-5 탄성포장재 포설).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='4205-0020', 지역=지역)", + "이름": "콘크리트 믹서 0.20㎥", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-7-5 탄성포장재 포설).믹서 * 8 / 시공량" + } + ], + "덧줄": [ + { + "이름": "공구손료 및 경장비", + "식": "((찾기(소요량:건설품셈:1-7-5 탄성포장재 포설).특별인부 / 시공량) * 인력:건설노임:1003 + (찾기(소요량:건설품셈:1-7-5 탄성포장재 포설).보통인부 / 시공량) * 인력:건설노임:1002) * 찾기(소요량:건설품셈:1-7-5 탄성포장재 포설 계수 주④, 구분='공구손료 및 경장비(발전기, 다짐롤러 등)의 기계경비').값 / 100", + "비목": "경비", + "출처": "건설품셈 토목 1-7-5 주④" + } + ], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 해석: 인력품 = 인력 호표 줄 금액 합(기계 조종원 제외) · 해석: 믹서 0.2㎥ = 기계 요소 콘크리트 믹서 0.20㎥ · 무늬 포설 별도(주③) · 탄성포장재 재료 원문 표에 없음" + }, + { + "열쇠": "1-8-1 교통 안전표지판 설치", + "이름": "교통 안전표지판 설치", + "결과단위": "원/개소", + "출처": "건설품셈 토목 1-8-1", + "소유": "공용", + "입력": [ + { "이름": "작업", "고르기": ["지주", "표지판"] }, + { + "이름": "크레인", + "고르기": [ + "2105-0005", + "2104-0010", + "2104-0015", + "2104-0020", + "2104-0025", + "2104-0030", + "2104-0035", + "2104-0040", + "2104-0045", + "2104-0050", + "2104-0060", + "2104-0070", + "2104-0080", + "2104-0100", + "2104-0130", + "2104-0160", + "2104-0200", + "2104-0220", + "2104-0250", + "2104-0300", + "2105-0002", + "2105-0003", + "2105-0010", + "2105-0015", + "2105-0018" + ] + }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-8-1 교통 안전표지판 설치, 작업=작업).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-1 교통 안전표지판 설치, 작업=작업).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-1 교통 안전표지판 설치, 작업=작업).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계=크레인, 지역=지역)", + "이름": "크레인", + "단위": "시간", + "수량": "만약(작업 == '지주', 찾기(소요량:건설품셈:1-8-1 교통 안전표지판 설치, 작업=작업).크레인 * 8 / 시공량, 0)" + } + ], + "덧줄": [ + { + "이름": "공구손료 및 경장비", + "식": "((찾기(소요량:건설품셈:1-8-1 교통 안전표지판 설치, 작업=작업).특별인부 / 시공량) * 인력:건설노임:1003 + (찾기(소요량:건설품셈:1-8-1 교통 안전표지판 설치, 작업=작업).보통인부 / 시공량) * 인력:건설노임:1002) * 찾기(소요량:건설품셈:1-8-1 교통 안전표지판 설치 계수 주⑤, 구분='공구손료 및 경장비(드릴, 발전기 등)의 기계경비').값 / 100", + "비목": "경비", + "출처": "건설품셈 토목 1-8-1 주⑤" + } + ], + "끝수": null, + "비고": "해석: 인력품 = 인력 호표 줄 금액 합(기계 조종원 제외) · 해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 크레인 종류·규격 설계자 고름 — 원문 규격 5ton(지주) · 기초·폐자재 운반 별도(주③)" + }, + { + "열쇠": "1-8-2 도로 표지판 설치", + "이름": "도로 표지판 설치", + "결과단위": "원/개소", + "출처": "건설품셈 토목 1-8-2", + "소유": "공용", + "입력": [ + { "이름": "형식", "고르기": ["복주식 + 표지판 (8㎡이하 1개)", "편지식 + 표지판 (12㎡이하 1개)", "문형식 + 표지판 (8㎡이하 2개)"] }, + { + "이름": "크레인", + "고르기": [ + "2105-0005", + "2104-0010", + "2104-0015", + "2104-0020", + "2104-0025", + "2104-0030", + "2104-0035", + "2104-0040", + "2104-0045", + "2104-0050", + "2104-0060", + "2104-0070", + "2104-0080", + "2104-0100", + "2104-0130", + "2104-0160", + "2104-0200", + "2104-0220", + "2104-0250", + "2104-0300", + "2105-0002", + "2105-0003", + "2105-0010", + "2105-0015", + "2105-0018" + ] + }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-8-2 도로 표지판 설치, 형식=형식).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-2 도로 표지판 설치, 형식=형식).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-2 도로 표지판 설치, 형식=형식).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계=크레인, 지역=지역)", + "이름": "크레인", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-8-2 도로 표지판 설치, 형식=형식).크레인 * 8 / 시공량" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 크레인 종류·규격 설계자 고름 — 원문 규격 복주식 5ton · 편지식 25ton · 문형식 50ton(크레인 규격 표) · 기초·폐자재 운반 별도(주③) · 막힘: 표지판 추가 설치(주④) 빠짐 — 조건 칸 이름 「표지판 설치 규격(개소당)」 식에서 못 부름" + }, + { + "열쇠": "1-8-3 도로반사경 설치", + "이름": "도로반사경 설치", + "결과단위": "원/본", + "출처": "건설품셈 토목 1-8-3", + "소유": "공용", + "입력": [{ "이름": "면", "고르기": ["1면", "2면"] }], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-8-3 도로반사경 설치, 면=면).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-3 도로반사경 설치, 면=면).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-3 도로반사경 설치, 면=면).보통인부 / 시공량", + "비목": "노무비" + } + ], + "덧줄": [ + { + "이름": "공구손료 및 경장비", + "식": "((찾기(소요량:건설품셈:1-8-3 도로반사경 설치, 면=면).특별인부 / 시공량) * 인력:건설노임:1003 + (찾기(소요량:건설품셈:1-8-3 도로반사경 설치, 면=면).보통인부 / 시공량) * 인력:건설노임:1002) * 찾기(소요량:건설품셈:1-8-3 도로반사경 설치 계수 주③, 구분='공구손료 및 경장비(전동드릴, 발전기 등)의 기계경비').값 / 100", + "비목": "경비", + "출처": "건설품셈 토목 1-8-3 주③" + } + ], + "끝수": null, + "비고": "해석: 인력품 = 인력 호표 줄 금액 합(기계 조종원 제외) · 재료 원문 표에 없음" + }, + { + "열쇠": "1-8-5 시선유도표지 설치", + "이름": "시선유도표지 설치", + "결과단위": "원/개소", + "출처": "건설품셈 토목 1-8-5", + "소유": "공용", + "입력": [{ "이름": "설치위치", "고르기": ["흙속매설용", "가드레일용", "옹벽용"] }], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-8-5 시선유도표지 설치, 설치위치=설치위치).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-5 시선유도표지 설치, 설치위치=설치위치).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-5 시선유도표지 설치, 설치위치=설치위치).보통인부 / 시공량", + "비목": "노무비" + } + ], + "덧줄": [ + { + "이름": "공구손료 및 경장비", + "식": "((찾기(소요량:건설품셈:1-8-5 시선유도표지 설치, 설치위치=설치위치).특별인부 / 시공량) * 인력:건설노임:1003 + (찾기(소요량:건설품셈:1-8-5 시선유도표지 설치, 설치위치=설치위치).보통인부 / 시공량) * 인력:건설노임:1002) * 찾기(소요량:건설품셈:1-8-5 시선유도표지 설치 계수 주③, 구분='공구손료 및 경장비(전동드릴 등)의 기계경비').값 / 100", + "비목": "경비", + "출처": "건설품셈 토목 1-8-5 주③" + } + ], + "끝수": null, + "비고": "해석: 인력품 = 인력 호표 줄 금액 합(기계 조종원 제외) · 재료 원문 표에 없음" + }, + { + "열쇠": "1-8-6 볼라드 설치", + "이름": "볼라드 설치", + "결과단위": "원/개소", + "출처": "건설품셈 토목 1-8-6", + "소유": "공용", + "입력": [], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-8-6 볼라드 설치).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-6 볼라드 설치).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-6 볼라드 설치).보통인부 / 시공량", + "비목": "노무비" + } + ], + "덧줄": [ + { + "이름": "공구손료 및 경장비", + "식": "((찾기(소요량:건설품셈:1-8-6 볼라드 설치).특별인부 / 시공량) * 인력:건설노임:1003 + (찾기(소요량:건설품셈:1-8-6 볼라드 설치).보통인부 / 시공량) * 인력:건설노임:1002) * 찾기(소요량:건설품셈:1-8-6 볼라드 설치 계수 주③, 구분='공구손료 및 경장비(코어드릴, 발전기 등)의 기계경비').값 / 100", + "비목": "경비", + "출처": "건설품셈 토목 1-8-6 주③" + } + ], + "끝수": null, + "비고": "해석: 인력품 = 인력 호표 줄 금액 합(기계 조종원 제외) · 재료 원문 표에 없음" + }, + { + "열쇠": "1-8-7 주차 블록 설치", + "이름": "주차 블록 설치", + "결과단위": "원/개소", + "출처": "건설품셈 토목 1-8-7", + "소유": "공용", + "입력": [], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-8-7 주차 블록 설치).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-7 주차 블록 설치).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-7 주차 블록 설치).보통인부 / 시공량", + "비목": "노무비" + } + ], + "덧줄": [ + { + "이름": "공구손료 및 경장비", + "식": "((찾기(소요량:건설품셈:1-8-7 주차 블록 설치).특별인부 / 시공량) * 인력:건설노임:1003 + (찾기(소요량:건설품셈:1-8-7 주차 블록 설치).보통인부 / 시공량) * 인력:건설노임:1002) * 찾기(소요량:건설품셈:1-8-7 주차 블록 설치 계수 주③, 구분='공구손료 및 경장비(전동드릴, 발전기 등)의 기계경비').값 / 100", + "비목": "경비", + "출처": "건설품셈 토목 1-8-7 주③" + } + ], + "끝수": null, + "비고": "해석: 인력품 = 인력 호표 줄 금액 합(기계 조종원 제외) · 재료 원문 표에 없음" + }, + { + "열쇠": "1-8-8 차선규제봉 설치", + "이름": "차선규제봉 설치", + "결과단위": "원/개소", + "출처": "건설품셈 토목 1-8-8", + "소유": "공용", + "입력": [], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-8-8 차선규제봉 설치).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-8 차선규제봉 설치).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-8 차선규제봉 설치).보통인부 / 시공량", + "비목": "노무비" + } + ], + "덧줄": [ + { + "이름": "공구손료 및 경장비", + "식": "((찾기(소요량:건설품셈:1-8-8 차선규제봉 설치).특별인부 / 시공량) * 인력:건설노임:1003 + (찾기(소요량:건설품셈:1-8-8 차선규제봉 설치).보통인부 / 시공량) * 인력:건설노임:1002) * 찾기(소요량:건설품셈:1-8-8 차선규제봉 설치 계수 주③, 구분='공구손료 및 경장비(전동드릴, 발전기 등)의 기계경비').값 / 100", + "비목": "경비", + "출처": "건설품셈 토목 1-8-8 주③" + } + ], + "끝수": null, + "비고": "해석: 인력품 = 인력 호표 줄 금액 합(기계 조종원 제외) · 재료 원문 표에 없음" + }, + { + "열쇠": "1-8-4 도로표지병 설치", + "이름": "도로표지병 설치", + "결과단위": "원/개소", + "출처": "건설품셈 토목 1-8-4", + "소유": "공용", + "입력": [{ "이름": "주재료비", "단위": "원/개소", "출처": "건설품셈 토목 1-8-4 주④" }], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-8-4 도로표지병 설치).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-4 도로표지병 설치).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-4 도로표지병 설치).보통인부 / 시공량", + "비목": "노무비" + } + ], + "덧줄": [ + { + "이름": "공구손료 및 경장비", + "식": "((찾기(소요량:건설품셈:1-8-4 도로표지병 설치).특별인부 / 시공량) * 인력:건설노임:1003 + (찾기(소요량:건설품셈:1-8-4 도로표지병 설치).보통인부 / 시공량) * 인력:건설노임:1002) * 찾기(소요량:건설품셈:1-8-4 도로표지병 설치 계수 주③, 구분='공구손료 및 경장비(전동드릴 등)의 기계경비').값 / 100", + "비목": "경비", + "출처": "건설품셈 토목 1-8-4 주③" + }, + { + "이름": "잡재료", + "식": "주재료비 * 찾기(소요량:건설품셈:1-8-4 도로표지병 설치 계수 주④, 구분='잡재료비(접착제 등)').값 / 100", + "비목": "재료비", + "출처": "건설품셈 토목 1-8-4 주④" + } + ], + "끝수": null, + "비고": "해석: 인력품 = 인력 호표 줄 금액 합(기계 조종원 제외) · 주재료(표지병) 값 설계자 입력 — 잡재료 = 주재료비 5%" + }, + { + "열쇠": "1-8-9 차선도색 차선 밑그림", + "이름": "차선 밑그림", + "결과단위": "원/㎡", + "출처": "건설품셈 토목 1-8-9", + "소유": "공용", + "입력": [ + { "이름": "도색", "고르기": ["실선", "파선", "횡단보도, 주차장", "문자, 기호"] }, + { "이름": "주재료비", "단위": "원/㎡", "출처": "건설품셈 토목 1-8-9 주⑤" } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-8-9 차선도색 차선 밑그림, 도색=도색).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-9 차선도색 차선 밑그림, 도색=도색).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-9 차선도색 차선 밑그림, 도색=도색).보통인부 / 시공량", + "비목": "노무비" + } + ], + "덧줄": [ + { + "이름": "잡재료 및 소모재료", + "식": "주재료비 * 찾기(소요량:건설품셈:1-8-9 차선도색 차선 밑그림 계수 주⑤, 구분='잡재료 및 소모재료').값 / 100", + "비목": "재료비", + "출처": "건설품셈 토목 1-8-9 주⑤" + } + ], + "끝수": null, + "비고": "주재료(페인트·유리알) 값 설계자 입력 — 잡재료 = 주재료비 1% · 사전 청소 별도 · 페인트·유리알 재료량 별도 · 막힘: 트럭 줄 빠짐 — 기계 요소에 트럭(화물) 없음(덤프트럭만)" + }, + { + "열쇠": "1-8-9 차선도색 수용성형 페인트 수동식", + "이름": "차선도색 수용성형 페인트 수동식", + "결과단위": "원/㎡", + "출처": "건설품셈 토목 1-8-9", + "소유": "공용", + "입력": [ + { "이름": "도색", "고르기": ["실선", "파선", "횡단보도, 주차장", "문자, 기호"] }, + { "이름": "표지병감", "단위": "%", "범위": [0, 10], "출처": "건설품셈 토목 1-8-9 비고" }, + { "이름": "주재료비", "단위": "원/㎡", "출처": "건설품셈 토목 1-8-9 주⑥" } + ], + "중간": [ + { "이름": "시공량", "식": "찾기(소요량:건설품셈:1-8-9 차선도색 수용성형 페인트 수동식, 도색=도색).시공량 * (1 - 표지병감 / 100)", "출처": "건설품셈 토목 1-8-9 비고" } + ], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-9 차선도색 수용성형 페인트 수동식, 도색=도색).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-9 차선도색 수용성형 페인트 수동식, 도색=도색).보통인부 / 시공량", + "비목": "노무비" + } + ], + "덧줄": [ + { + "이름": "공구손료 및 경장비", + "식": "((찾기(소요량:건설품셈:1-8-9 차선도색 수용성형 페인트 수동식, 도색=도색).특별인부 / 시공량) * 인력:건설노임:1003 + (찾기(소요량:건설품셈:1-8-9 차선도색 수용성형 페인트 수동식, 도색=도색).보통인부 / 시공량) * 인력:건설노임:1002) * 찾기(소요량:건설품셈:1-8-9 차선도색 수용성형 페인트 수동식 계수 주⑤, 구분='공구손료 및 경장비(라인마커 등)의 기계경비').값 / 100", + "비목": "경비", + "출처": "건설품셈 토목 1-8-9 주⑤" + }, + { + "이름": "잡재료 및 소모재료", + "식": "주재료비 * 찾기(소요량:건설품셈:1-8-9 차선도색 수용성형 페인트 수동식 계수 주⑥, 구분='잡재료 및 소모재료').값 / 100", + "비목": "재료비", + "출처": "건설품셈 토목 1-8-9 주⑥" + } + ], + "끝수": null, + "비고": "해석: 인력품 = 인력 호표 줄 금액 합(기계 조종원 제외) · 주재료(페인트·유리알) 값 설계자 입력 — 잡재료 = 주재료비 1% · 표지병 등으로 능률 저하 시 시공량 10%까지 감(비고) — 설계자 입력(아니면 0) · 해석: 감은 시공량에 곱함 · 사전 청소 별도 · 페인트·유리알 재료량 별도 · 막힘: 트럭 줄 빠짐 — 기계 요소에 트럭(화물) 없음(덤프트럭만)" + }, + { + "열쇠": "1-8-9 차선도색 수용성형 페인트 기계식", + "이름": "차선도색 수용성형 페인트 기계식", + "결과단위": "원/㎡", + "출처": "건설품셈 토목 1-8-9", + "소유": "공용", + "입력": [ + { "이름": "도색", "고르기": ["실선", "파선"] }, + { "이름": "표지병감", "단위": "%", "범위": [0, 10], "출처": "건설품셈 토목 1-8-9 비고" }, + { "이름": "주재료비", "단위": "원/㎡", "출처": "건설품셈 토목 1-8-9 주⑤" } + ], + "중간": [ + { "이름": "시공량", "식": "찾기(소요량:건설품셈:1-8-9 차선도색 수용성형 페인트 기계식, 도색=도색).시공량 * (1 - 표지병감 / 100)", "출처": "건설품셈 토목 1-8-9 비고" } + ], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-9 차선도색 수용성형 페인트 기계식, 도색=도색).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-9 차선도색 수용성형 페인트 기계식, 도색=도색).보통인부 / 시공량", + "비목": "노무비" + } + ], + "덧줄": [ + { + "이름": "잡재료 및 소모재료", + "식": "주재료비 * 찾기(소요량:건설품셈:1-8-9 차선도색 수용성형 페인트 기계식 계수 주⑤, 구분='잡재료 및 소모재료').값 / 100", + "비목": "재료비", + "출처": "건설품셈 토목 1-8-9 주⑤" + } + ], + "끝수": null, + "비고": "주재료(페인트·유리알) 값 설계자 입력 — 잡재료 = 주재료비 1% · 표지병 등으로 능률 저하 시 시공량 10%까지 감(비고) — 설계자 입력(아니면 0) · 해석: 감은 시공량에 곱함 · 사전 청소 별도 · 페인트·유리알 재료량 별도 · 막힘: 라인마커트럭 줄 빠짐 — 기계 요소에 라인마커트럭 없음 · 막힘: 트럭 줄 빠짐 — 기계 요소에 트럭(화물) 없음(덤프트럭만)" + }, + { + "열쇠": "1-8-9 차선도색 융착식 도료 수동식", + "이름": "차선도색 융착식 도료 수동식", + "결과단위": "원/㎡", + "출처": "건설품셈 토목 1-8-9", + "소유": "공용", + "입력": [ + { "이름": "도색", "고르기": ["실선", "파선", "횡단보도, 주차장", "문자, 기호"] }, + { "이름": "표지병감", "단위": "%", "범위": [0, 10], "출처": "건설품셈 토목 1-8-9 비고" } + ], + "중간": [ + { "이름": "시공량", "식": "찾기(소요량:건설품셈:1-8-9 차선도색 융착식 도료 수동식, 도색=도색).시공량 * (1 - 표지병감 / 100)", "출처": "건설품셈 토목 1-8-9 비고" } + ], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-9 차선도색 융착식 도료 수동식, 도색=도색).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-9 차선도색 융착식 도료 수동식, 도색=도색).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "재료", + "요소": "재료:자체:차선도색 프라이머", + "이름": "프라이머(융착식 도료)", + "단위": "kg", + "수량": "찾기(소요량:건설품셈:1-8-9 융착식 도료 수동식 재료).프라이머 / 10", + "비목": "재료비", + "출처": "건설품셈 토목 1-8-9 재료(10㎡당)" + }, + { + "종류": "재료", + "요소": "재료:나라장터자재:20204251", + "이름": "프로판가스(건설용 LPG)", + "단위": "kg", + "수량": "찾기(소요량:건설품셈:1-8-9 융착식 도료 수동식 재료).프로판가스 / 10", + "비목": "재료비", + "출처": "건설품셈 토목 1-8-9 재료(10㎡당)" + } + ], + "덧줄": [], + "끝수": null, + "비고": "표지병 등으로 능률 저하 시 시공량 10%까지 감(비고) — 설계자 입력(아니면 0) · 해석: 감은 시공량에 곱함 · 해석: 프로판가스 = 나라장터 액화석유가스 건설용LPG · 공시에 없는 재료(프라이머) = 재료_자체 · 트럭 4.5ton = 용해기 운반(트럭 작업 표) · 사전 청소 별도 · 페인트·유리알 재료량 별도 · 막힘: 트럭 2.5ton 줄 빠짐 — 칸 이름 「트럭 2.5ton」 식에서 못 부름 · 기계 요소에 트럭(화물) 없음 · 막힘: 트럭 줄 빠짐 — 기계 요소에 트럭(화물) 없음(덤프트럭만)" + }, + { + "열쇠": "1-8-9 차선도색 상온경화형 플라스틱 도료 구동식", + "이름": "차선도색 상온경화형 플라스틱 도료 구동식", + "결과단위": "원/㎡", + "출처": "건설품셈 토목 1-8-9", + "소유": "공용", + "입력": [ + { "이름": "도색", "고르기": ["실선", "파선", "횡단보도, 주차장", "문자, 기호"] }, + { "이름": "표지병감", "단위": "%", "범위": [0, 10], "출처": "건설품셈 토목 1-8-9 비고" }, + { "이름": "주재료비", "단위": "원/㎡", "출처": "건설품셈 토목 1-8-9 주⑥" } + ], + "중간": [ + { + "이름": "시공량", + "식": "찾기(소요량:건설품셈:1-8-9 차선도색 상온경화형 플라스틱 도료 구동식, 도색=도색).시공량 * (1 - 표지병감 / 100)", + "출처": "건설품셈 토목 1-8-9 비고" + } + ], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-9 차선도색 상온경화형 플라스틱 도료 구동식, 도색=도색).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-9 차선도색 상온경화형 플라스틱 도료 구동식, 도색=도색).보통인부 / 시공량", + "비목": "노무비" + } + ], + "덧줄": [ + { + "이름": "공구손료 및 경장비", + "식": "((찾기(소요량:건설품셈:1-8-9 차선도색 상온경화형 플라스틱 도료 구동식, 도색=도색).특별인부 / 시공량) * 인력:건설노임:1003 + (찾기(소요량:건설품셈:1-8-9 차선도색 상온경화형 플라스틱 도료 구동식, 도색=도색).보통인부 / 시공량) * 인력:건설노임:1002) * 찾기(소요량:건설품셈:1-8-9 차선도색 상온경화형 플라스틱 도료 구동식 계수 주⑤, 구분='공구손료 및 경장비(핸드믹서 등)의 기계경비').값 / 100", + "비목": "경비", + "출처": "건설품셈 토목 1-8-9 주⑤" + }, + { + "이름": "잡재료 및 소모재료", + "식": "주재료비 * 찾기(소요량:건설품셈:1-8-9 차선도색 상온경화형 플라스틱 도료 구동식 계수 주⑥, 구분='잡재료 및 소모재료').값 / 100", + "비목": "재료비", + "출처": "건설품셈 토목 1-8-9 주⑥" + } + ], + "끝수": null, + "비고": "해석: 인력품 = 인력 호표 줄 금액 합(기계 조종원 제외) · 주재료(페인트·유리알) 값 설계자 입력 — 잡재료 = 주재료비 1% · 표지병 등으로 능률 저하 시 시공량 10%까지 감(비고) — 설계자 입력(아니면 0) · 해석: 감은 시공량에 곱함 · 라인마커 기계경비 별도(주⑤) · 사전 청소 별도 · 페인트·유리알 재료량 별도 · 막힘: 트럭 줄 빠짐 — 기계 요소에 트럭(화물) 없음(덤프트럭만)" + }, + { + "열쇠": "1-8-10 가드레일 설치 지주 설치", + "이름": "가드레일 지주 설치", + "결과단위": "원/m", + "출처": "건설품셈 토목 1-8-10", + "소유": "공용", + "입력": [{ "이름": "지주간격", "고르기": ["2m", "4m"] }], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-8-10 가드레일 설치 지주 설치, 지주간격=지주간격).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-10 가드레일 설치 지주 설치, 지주간격=지주간격).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-10 가드레일 설치 지주 설치, 지주간격=지주간격).보통인부 / 시공량", + "비목": "노무비" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 막힘: 굴착기+대형브레이커 빠짐 — 칸 이름 「굴착기+대형브레이커」 식에서 못 부름 · 막힘: 트럭 줄 빠짐 — 기계 요소에 트럭(화물) 없음(덤프트럭만)" + }, + { + "열쇠": "1-8-10 가드레일 설치 판 설치", + "이름": "가드레일 설치 판 설치", + "결과단위": "원/m", + "출처": "건설품셈 토목 1-8-10", + "소유": "공용", + "입력": [ + { "이름": "지주간격", "고르기": ["2m", "4m"] }, + { "이름": "W", "고르기": ["2W", "3W"] }, + { "이름": "곡선감", "단위": "%", "범위": [0, 40], "출처": "건설품셈 토목 1-8-10 주④" } + ], + "중간": [ + { + "이름": "시공량", + "식": "찾기(소요량:건설품셈:1-8-10 가드레일 설치 판 설치, 지주간격=지주간격, W=W).시공량 * (1 - 곡선감 / 100)", + "출처": "건설품셈 토목 1-8-10 주④" + } + ], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-10 가드레일 설치 판 설치, 지주간격=지주간격, W=W).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-10 가드레일 설치 판 설치, 지주간격=지주간격, W=W).보통인부 / 시공량", + "비목": "노무비" + } + ], + "덧줄": [ + { + "이름": "공구손료 및 경장비", + "식": "((찾기(소요량:건설품셈:1-8-10 가드레일 설치 판 설치, 지주간격=지주간격, W=W).특별인부 / 시공량) * 인력:건설노임:1003 + (찾기(소요량:건설품셈:1-8-10 가드레일 설치 판 설치, 지주간격=지주간격, W=W).보통인부 / 시공량) * 인력:건설노임:1002) * 찾기(소요량:건설품셈:1-8-10 가드레일 설치 판 설치 계수 주⑤, 구분='공구손료 및 경장비(전동드릴 등)의 기계경비').값 / 100", + "비목": "경비", + "출처": "건설품셈 토목 1-8-10 주⑤" + } + ], + "끝수": null, + "비고": "해석: 인력품 = 인력 호표 줄 금액 합(기계 조종원 제외) · 램프 등 곡선구간 시공량 40% 범위 감 — 설계자 입력(아니면 0) · 해석: 감은 시공량에 곱함 · 가드레일 재료 원문 표에 없음 · 막힘: 트럭 줄 빠짐 — 기계 요소에 트럭(화물) 없음(덤프트럭만)" + }, + { + "열쇠": "1-8-11 중앙분리대 설치(가드레일식) 판 설치", + "이름": "중앙분리대 설치(가드레일식) 판 설치", + "결과단위": "원/m", + "출처": "건설품셈 토목 1-8-11", + "소유": "공용", + "입력": [{ "이름": "지주간격", "고르기": ["2m", "4m"] }, { "이름": "W", "고르기": ["2W", "3W"] }], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-8-11 중앙분리대 설치(가드레일식) 판 설치, 지주간격=지주간격, W=W).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-11 중앙분리대 설치(가드레일식) 판 설치, 지주간격=지주간격, W=W).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-11 중앙분리대 설치(가드레일식) 판 설치, 지주간격=지주간격, W=W).보통인부 / 시공량", + "비목": "노무비" + } + ], + "덧줄": [ + { + "이름": "공구손료 및 경장비", + "식": "((찾기(소요량:건설품셈:1-8-11 중앙분리대 설치(가드레일식) 판 설치, 지주간격=지주간격, W=W).특별인부 / 시공량) * 인력:건설노임:1003 + (찾기(소요량:건설품셈:1-8-11 중앙분리대 설치(가드레일식) 판 설치, 지주간격=지주간격, W=W).보통인부 / 시공량) * 인력:건설노임:1002) * 찾기(소요량:건설품셈:1-8-11 중앙분리대 설치(가드레일식) 판 설치 계수 주④, 구분='공구손료 및 경장비(전동드릴 등)의 기계경비').값 / 100", + "비목": "경비", + "출처": "건설품셈 토목 1-8-11 주④" + } + ], + "끝수": null, + "비고": "해석: 인력품 = 인력 호표 줄 금액 합(기계 조종원 제외) · 가드레일 재료 원문 표에 없음 · 막힘: 트럭 줄 빠짐 — 기계 요소에 트럭(화물) 없음(덤프트럭만)" + }, + { + "열쇠": "1-8-11 중앙분리대 설치(가드레일식) 지주 설치", + "이름": "중앙분리대(가드레일식) 지주 설치", + "결과단위": "원/m", + "출처": "건설품셈 토목 1-8-11", + "소유": "공용", + "입력": [ + { "이름": "지주간격", "고르기": ["2m", "4m"] }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-8-11 중앙분리대 설치(가드레일식) 지주 설치, 지주간격=지주간격).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-11 중앙분리대 설치(가드레일식) 지주 설치, 지주간격=지주간격).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-11 중앙분리대 설치(가드레일식) 지주 설치, 지주간격=지주간격).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='5205-0170', 지역=지역)", + "이름": "공기압축기 17.0㎥/min", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-8-11 중앙분리대 설치(가드레일식) 지주 설치, 지주간격=지주간격).공기압축기 * 8 / 시공량" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 장비 규격 현장여건 따라 바꿀 수 있음(주④) · 막힘: 굴착기+대형브레이커 빠짐 — 칸 이름 「굴착기+대형브레이커」 식에서 못 부름 · 막힘: 크롤러드릴(공기식) 빠짐 — 칸 이름 「크롤러드릴(공기식)」 식에서 못 부름 · 막힘: 트럭 줄 빠짐 — 기계 요소에 트럭(화물) 없음(덤프트럭만)" + }, + { + "열쇠": "1-8-12 중앙분리대 설치(콘크리트포설식)", + "이름": "중앙분리대 설치(콘크리트포설식)", + "결과단위": "원/m", + "출처": "건설품셈 토목 1-8-12", + "소유": "공용", + "입력": [ + { "이름": "높이", "고르기": ["0.81m", "1.27m"] }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + }, + { "이름": "보정작업", "고르기": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "출처": "건설품셈 공통 8-1-7 1" } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-8-12 중앙분리대 설치(콘크리트포설식), 높이=높이).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1019", + "이름": "포장공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-12 중앙분리대 설치(콘크리트포설식), 높이=높이).포장공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1008", + "이름": "철근공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-12 중앙분리대 설치(콘크리트포설식), 높이=높이).철근공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-12 중앙분리대 설치(콘크리트포설식), 높이=높이).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 굴착기(무한궤도) 시간당 경비, 기계='0201-0100', 지역=지역, 보정작업=보정작업)", + "이름": "굴착기 1.0㎥", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-8-12 중앙분리대 설치(콘크리트포설식), 높이=높이).굴착기 * 8 / 시공량" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 유도선·수축줄눈 별도(주③) · 콘크리트·철망 재료 원문 표에 없음 · 막힘: 콘크리트 피니셔 빠짐 — 칸 이름 「콘크리트 피니셔」 식에서 못 부름" + }, + { + "열쇠": "1-8-13 유색포장(미끄럼방지)", + "이름": "유색포장(미끄럼방지)", + "결과단위": "원/㎡", + "출처": "건설품셈 토목 1-8-13", + "소유": "공용", + "입력": [{ "이름": "형식", "고르기": ["A-Type", "B-Type"] }], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-8-13 유색포장(미끄럼방지), 형식=형식).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1029", + "이름": "도장공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-13 유색포장(미끄럼방지), 형식=형식).도장공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-13 유색포장(미끄럼방지), 형식=형식).보통인부 / 시공량", + "비목": "노무비" + } + ], + "덧줄": [], + "끝수": null, + "비고": "형식 A-Type = 전면처리 · B-Type = 이격식(적용기준 표) · 아크릴수지·규사 재료 원문 표에 없음 · 막힘: 트럭 줄 빠짐 — 기계 요소에 트럭(화물) 없음(덤프트럭만)" + }, + { + "열쇠": "1-8-14 표시못 설치", + "이름": "표시못 설치", + "결과단위": "원/개소", + "출처": "건설품셈 토목 1-8-14", + "소유": "공용", + "입력": [{ "이름": "형식", "고르기": ["A-Type", "B-Type"] }], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-8-14 표시못 설치, 형식=형식).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-14 표시못 설치, 형식=형식).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-14 표시못 설치, 형식=형식).보통인부 / 시공량", + "비목": "노무비" + } + ], + "덧줄": [], + "끝수": null, + "비고": "형식 A-Type = 산재 구간 · B-Type = 밀집 구간(적용기준 표) · 요율 표(A-Type 2% · B-Type 4%)는 무엇에 곱하는지 원문 글 없음 — 계산에 안 넣음 · 표시못 재료 원문 표에 없음 · 막힘: 트럭 줄 빠짐 — 기계 요소에 트럭(화물) 없음(덤프트럭만)" + }, + { + "열쇠": "1-8-15 L형측구 설치(포설식)", + "이름": "L형측구 설치(포설식)", + "결과단위": "원/m", + "출처": "건설품셈 토목 1-8-15", + "소유": "공용", + "입력": [ + { "이름": "높이", "고르기": ["H=0.5m이하", "H=1.2m"] }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + }, + { "이름": "보정작업", "고르기": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "출처": "건설품셈 공통 8-1-7 1" } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-8-15 L형측구 설치(포설식), 높이=높이).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1019", + "이름": "포장공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-15 L형측구 설치(포설식), 높이=높이).포장공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-8-15 L형측구 설치(포설식), 높이=높이).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 굴착기(무한궤도) 시간당 경비, 기계='0201-0060', 지역=지역, 보정작업=보정작업)", + "이름": "굴착기 0.6㎥", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-8-15 L형측구 설치(포설식), 높이=높이).굴착기 * 8 / 시공량" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · H=1.2m 는 2회 포설 기준(주①) · 유도선·터파기·되메우기·수축줄눈 별도(주③) · 콘크리트 재료 원문 표에 없음 · 막힘: 콘크리트 페이버 빠짐 — 칸 이름 「콘크리트 페이버」 식에서 못 부름" + }, + { + "열쇠": "1-9-1 방음벽 설치 앵커볼트 설치", + "이름": "방음벽 앵커볼트 설치", + "결과단위": "원/개", + "출처": "건설품셈 토목 1-9-1", + "소유": "공용", + "입력": [], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-9-1 방음벽 설치 앵커볼트 설치).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1009", + "이름": "철공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-9-1 방음벽 설치 앵커볼트 설치).철공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-9-1 방음벽 설치 앵커볼트 설치).보통인부 / 시공량", + "비목": "노무비" + } + ], + "덧줄": [ + { + "이름": "공구손료 및 경장비", + "식": "((찾기(소요량:건설품셈:1-9-1 방음벽 설치 앵커볼트 설치).철공 / 시공량) * 인력:건설노임:1009 + (찾기(소요량:건설품셈:1-9-1 방음벽 설치 앵커볼트 설치).보통인부 / 시공량) * 인력:건설노임:1002) * 찾기(소요량:건설품셈:1-9-1 방음벽 설치 앵커볼트 설치 계수 주③, 구분='공구손료 및 경장비(용접기 등)의 기계경비').값 / 100", + "비목": "경비", + "출처": "건설품셈 토목 1-9-1 주③" + } + ], + "끝수": null, + "비고": "해석: 인력품 = 인력 호표 줄 금액 합(기계 조종원 제외) · 앵커볼트 재료 원문 표에 없음" + }, + { + "열쇠": "1-9-3 낙석방지책 설치 지주 설치", + "이름": "낙석방지책 지주 설치", + "결과단위": "원/개", + "출처": "건설품셈 토목 1-9-3", + "소유": "공용", + "입력": [ + { + "이름": "크레인", + "고르기": [ + "2105-0005", + "2104-0010", + "2104-0015", + "2104-0020", + "2104-0025", + "2104-0030", + "2104-0035", + "2104-0040", + "2104-0045", + "2104-0050", + "2104-0060", + "2104-0070", + "2104-0080", + "2104-0100", + "2104-0130", + "2104-0160", + "2104-0200", + "2104-0220", + "2104-0250", + "2104-0300", + "2105-0002", + "2105-0003", + "2105-0010", + "2105-0015", + "2105-0018" + ] + }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-9-3 낙석방지책 설치 지주 설치).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1012", + "이름": "용접공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-9-3 낙석방지책 설치 지주 설치).용접공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-9-3 낙석방지책 설치 지주 설치).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-9-3 낙석방지책 설치 지주 설치).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계=크레인, 지역=지역)", + "이름": "크레인", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-9-3 낙석방지책 설치 지주 설치).크레인 * 8 / 시공량" + } + ], + "덧줄": [ + { + "이름": "공구손료 및 경장비", + "식": "((찾기(소요량:건설품셈:1-9-3 낙석방지책 설치 지주 설치).용접공 / 시공량) * 인력:건설노임:1012 + (찾기(소요량:건설품셈:1-9-3 낙석방지책 설치 지주 설치).특별인부 / 시공량) * 인력:건설노임:1003 + (찾기(소요량:건설품셈:1-9-3 낙석방지책 설치 지주 설치).보통인부 / 시공량) * 인력:건설노임:1002) * 찾기(소요량:건설품셈:1-9-3 낙석방지책 설치 지주 설치 계수 주④, 구분='공구손료 및 경장비(용접기 등)의 기계경비').값 / 100", + "비목": "경비", + "출처": "건설품셈 토목 1-9-3 주④" + } + ], + "끝수": null, + "비고": "해석: 인력품 = 인력 호표 줄 금액 합(기계 조종원 제외) · 해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 크레인 종류·규격 설계자 고름 — 원문 규격 10ton · 터파기·기초·되메우기 별도(주③)" + }, + { + "열쇠": "1-9-3 낙석방지책 설치 와이어 설치", + "이름": "낙석방지책 와이어 설치", + "결과단위": "원/m", + "출처": "건설품셈 토목 1-9-3", + "소유": "공용", + "입력": [], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-9-3 낙석방지책 설치 와이어 설치).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-9-3 낙석방지책 설치 와이어 설치).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-9-3 낙석방지책 설치 와이어 설치).보통인부 / 시공량", + "비목": "노무비" + } + ], + "덧줄": [ + { + "이름": "공구손료 및 경장비", + "식": "((찾기(소요량:건설품셈:1-9-3 낙석방지책 설치 와이어 설치).특별인부 / 시공량) * 인력:건설노임:1003 + (찾기(소요량:건설품셈:1-9-3 낙석방지책 설치 와이어 설치).보통인부 / 시공량) * 인력:건설노임:1002) * 찾기(소요량:건설품셈:1-9-3 낙석방지책 설치 와이어 설치 계수 주④, 구분='공구손료 및 경장비(절단기 등)의 기계경비').값 / 100", + "비목": "경비", + "출처": "건설품셈 토목 1-9-3 주④" + } + ], + "끝수": null, + "비고": "해석: 인력품 = 인력 호표 줄 금액 합(기계 조종원 제외) · 비계 별도(주③) · 재료 원문 표에 없음" + }, + { + "열쇠": "1-9-3 낙석방지책 설치 철망 설치", + "이름": "낙석방지책 철망 설치", + "결과단위": "원/㎡", + "출처": "건설품셈 토목 1-9-3", + "소유": "공용", + "입력": [], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-9-3 낙석방지책 설치 철망 설치).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-9-3 낙석방지책 설치 철망 설치).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-9-3 낙석방지책 설치 철망 설치).보통인부 / 시공량", + "비목": "노무비" + } + ], + "덧줄": [ + { + "이름": "공구손료 및 경장비", + "식": "((찾기(소요량:건설품셈:1-9-3 낙석방지책 설치 철망 설치).특별인부 / 시공량) * 인력:건설노임:1003 + (찾기(소요량:건설품셈:1-9-3 낙석방지책 설치 철망 설치).보통인부 / 시공량) * 인력:건설노임:1002) * 찾기(소요량:건설품셈:1-9-3 낙석방지책 설치 철망 설치 계수 주④, 구분='공구손료 및 경장비(절단기 등)의 기계경비').값 / 100", + "비목": "경비", + "출처": "건설품셈 토목 1-9-3 주④" + } + ], + "끝수": null, + "비고": "해석: 인력품 = 인력 호표 줄 금액 합(기계 조종원 제외) · 비계 별도(주③) · 재료 원문 표에 없음" + }, + { + "열쇠": "1-9-4 낙석방지망 설치 기초 착암", + "이름": "낙석방지망 기초 착암", + "결과단위": "원/㎡", + "출처": "건설품셈 토목 1-9-4", + "소유": "공용", + "입력": [ + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-9-4 낙석방지망 설치 기초 착암).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1015", + "이름": "착암공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-9-4 낙석방지망 설치 기초 착암).착암공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1006", + "이름": "비계공", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-9-4 낙석방지망 설치 기초 착암).비계공 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-9-4 낙석방지망 설치 기초 착암).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계='5205-0103', 지역=지역)", + "이름": "공기압축기 10.3㎥/min", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:1-9-4 낙석방지망 설치 기초 착암).공기압축기 * 8 / 시공량" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 재료량 설계수량(주④) · 비탈면 고르기 별도(주③) · 막힘: 소형브레이커 2.7㎥/min 줄 빠짐 — 기계 요소 5210-0027 손료 계수(상각·정비·관리) 빈칸" + }, + { + "열쇠": "1-9-4 낙석방지망 설치 철망 및 와이어 설치", + "이름": "낙석방지망 철망 및 와이어 설치", + "결과단위": "원/㎡", + "출처": "건설품셈 토목 1-9-4", + "소유": "공용", + "입력": [ + { "이름": "구분", "고르기": ["기계식", "인력식"] }, + { + "이름": "크레인", + "고르기": [ + "2105-0005", + "2104-0010", + "2104-0015", + "2104-0020", + "2104-0025", + "2104-0030", + "2104-0035", + "2104-0040", + "2104-0045", + "2104-0050", + "2104-0060", + "2104-0070", + "2104-0080", + "2104-0100", + "2104-0130", + "2104-0160", + "2104-0200", + "2104-0220", + "2104-0250", + "2104-0300", + "2105-0002", + "2105-0003", + "2105-0010", + "2105-0015", + "2105-0018" + ] + }, + { "이름": "방지망", "고르기": ["포켓식", "비포켓식"], "출처": "건설품셈 토목 1-9-4 재료(참고자료)" }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + } + ], + "중간": [{ "이름": "시공량", "식": "찾기(소요량:건설품셈:1-9-4 낙석방지망 설치 철망 및 와이어 설치, 구분=구분).시공량" }], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-9-4 낙석방지망 설치 철망 및 와이어 설치, 구분=구분).특별인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:1-9-4 낙석방지망 설치 철망 및 와이어 설치, 구분=구분).보통인부 / 시공량", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계=크레인, 지역=지역)", + "이름": "크레인", + "단위": "시간", + "수량": "만약(구분 == '기계식', 찾기(소요량:건설품셈:1-9-4 낙석방지망 설치 철망 및 와이어 설치, 구분=구분).크레인 * 8 / 시공량, 0)" + }, + { + "종류": "재료", + "요소": "재료:자체:낙석방지망 철망", + "이름": "철망(낙석방지망)", + "단위": "㎡", + "수량": "찾기(소요량:건설품셈:1-9-4 낙석방지망 설치 재료, 구분='철망').수량", + "비목": "재료비", + "출처": "건설품셈 토목 1-9-4 재료(참고자료)" + }, + { + "종류": "재료", + "요소": "재료:자체:결속선", + "이름": "결속선", + "단위": "m", + "수량": "찾기(소요량:건설품셈:1-9-4 낙석방지망 설치 재료, 구분='결속선').수량", + "비목": "재료비", + "출처": "건설품셈 토목 1-9-4 재료(참고자료)" + }, + { + "종류": "재료", + "요소": "재료:자체:에폭시", + "이름": "에폭시", + "단위": "kg", + "수량": "만약(방지망 == '포켓식', 찾기(소요량:건설품셈:1-9-4 낙석방지망 설치 재료, 구분='에폭시').수량, 0)", + "비목": "재료비", + "출처": "건설품셈 토목 1-9-4 재료(참고자료)" + } + ], + "덧줄": [], + "끝수": null, + "비고": "해석: 일당 편성 장비 1대 × 1일 8시간 ÷ 일당 시공량(산림품셈 12장 로직과 같음) · 크레인 종류·규격 설계자 고름 — 원문 규격 50ton(기계식) · 재료량은 참고자료(㎡당) — 지주·고정핀·클립·모르타르·와이어로프 설계 따라 별도 · 에폭시는 포켓식만 · 공시에 없는 재료 = 재료_자체" + } + ] +} diff --git a/resources/master_data/로직_건설품셈_토목2장.json b/resources/master_data/로직_건설품셈_토목2장.json new file mode 100644 index 00000000..526f2278 --- /dev/null +++ b/resources/master_data/로직_건설품셈_토목2장.json @@ -0,0 +1,394 @@ +{ + "그룹": "로직", + "원문": "건설품셈", + "판": "2026", + "줄": [ + { + "열쇠": "2-1-1 사석부설", + "이름": "사석부설", + "결과단위": "원/㎥", + "출처": "건설품셈 토목 2-1-1", + "소유": "공용", + "입력": [ + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + }, + { "이름": "보정작업", "고르기": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "출처": "건설품셈 공통 8-1-7 1" } + ], + "중간": [], + "호표": [ + { "종류": "인력", "요소": "인력:건설노임:1002", "이름": "보통인부", "단위": "인", "수량": "찾기(소요량:건설품셈:2-1-1 사석부설).보통인부", "비목": "노무비" }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 굴착기(무한궤도) 시간당 경비, 기계='0201-0100', 지역=지역, 보정작업=보정작업)", + "이름": "굴착기 1.0㎥", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:2-1-1 사석부설).굴삭기" + } + ], + "덧줄": [], + "끝수": null, + "비고": "필터매트는 건설품셈 공통 5-2-1 매트부설 · 사석 재료 원문 표에 없음" + }, + { + "열쇠": "2-1-2 사석고르기", + "이름": "사석고르기", + "결과단위": "원/㎡", + "출처": "건설품셈 토목 2-1-2", + "소유": "공용", + "입력": [], + "중간": [], + "호표": [ + { "종류": "인력", "요소": "인력:건설노임:1002", "이름": "보통인부", "단위": "인", "수량": "찾기(소요량:건설품셈:2-1-2 사석고르기).보통인부", "비목": "노무비" } + ], + "덧줄": [], + "끝수": null, + "비고": "잡석 채움 포함(주②) · 막힘: 굴삭기+부착용집게 빠짐 — 칸 이름 「굴삭기+부착용집게」 식에서 못 부름" + }, + { + "열쇠": "2-2-2 매트리스형 돌망태 설치", + "이름": "매트리스형 돌망태 설치", + "결과단위": "원/㎡", + "출처": "건설품셈 토목 2-2-2", + "소유": "공용", + "입력": [ + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + }, + { "이름": "보정작업", "고르기": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "출처": "건설품셈 공통 8-1-7 1" } + ], + "중간": [], + "호표": [ + { "종류": "인력", "요소": "인력:건설노임:1033", "이름": "석공", "단위": "인", "수량": "찾기(소요량:건설품셈:2-2-2 매트리스형 돌망태 설치).석공", "비목": "노무비" }, + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:2-2-2 매트리스형 돌망태 설치).특별인부", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:2-2-2 매트리스형 돌망태 설치).보통인부", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 굴착기(무한궤도) 시간당 경비, 기계='0201-0100', 지역=지역, 보정작업=보정작업)", + "이름": "굴착기 1.0㎥", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:2-2-2 매트리스형 돌망태 설치).굴착기" + } + ], + "덧줄": [], + "끝수": null, + "비고": "폭 200㎝ · 높이 30㎝ 기준(주①) · 필터매트는 건설품셈 공통 5-2-1 매트부설 · 망태·망태석 재료 원문 표에 없음" + }, + { + "열쇠": "2-2-3 돌망태형옹벽 설치", + "이름": "돌망태형옹벽 설치", + "결과단위": "원/㎥", + "출처": "건설품셈 토목 2-2-3", + "소유": "공용", + "입력": [ + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + }, + { "이름": "보정작업", "고르기": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "출처": "건설품셈 공통 8-1-7 1" } + ], + "중간": [], + "호표": [ + { "종류": "인력", "요소": "인력:건설노임:1033", "이름": "석공", "단위": "인", "수량": "찾기(소요량:건설품셈:2-2-3 돌망태형옹벽 설치).석공", "비목": "노무비" }, + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:2-2-3 돌망태형옹벽 설치).특별인부", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:2-2-3 돌망태형옹벽 설치).보통인부", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 굴착기(무한궤도) 시간당 경비, 기계='0201-0060', 지역=지역, 보정작업=보정작업)", + "이름": "굴착기 0.6㎥", + "단위": "시간", + "수량": "찾기(소요량:건설품셈:2-2-3 돌망태형옹벽 설치).굴착기" + } + ], + "덧줄": [], + "끝수": null, + "비고": "높이 5m 이하(주①) · 터파기·지반고르기 별도(주③) · 필터매트는 건설품셈 공통 5-2-1 매트부설 · 철망태·망태석 재료 원문 표에 없음" + }, + { + "열쇠": "2-3-1 식생매트 설치", + "이름": "식생매트 설치", + "결과단위": "원/㎡", + "출처": "건설품셈 토목 2-3-1", + "소유": "공용", + "입력": [ + { "이름": "작업", "고르기": ["식생매트설치", "복토"] }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + }, + { "이름": "보정작업", "고르기": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "출처": "건설품셈 공통 8-1-7 1" } + ], + "중간": [], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "만약(작업 == '식생매트설치', 찾기(소요량:건설품셈:2-3-1 식생매트 설치, 작업=작업).특별인부, 0)", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:2-3-1 식생매트 설치, 작업=작업).보통인부", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 굴착기(무한궤도) 시간당 경비, 기계='0201-0060', 지역=지역, 보정작업=보정작업)", + "이름": "굴착기 0.6㎥", + "단위": "시간", + "수량": "만약(작업 == '복토', 찾기(소요량:건설품셈:2-3-1 식생매트 설치, 작업=작업).굴삭기, 0)" + } + ], + "덧줄": [], + "끝수": null, + "비고": "작업(식생매트설치 · 복토)마다 따로 계상 — 둘 다 하면 두 줄 · 종자살포·잔디·관수·시비 별도(주③) · 식생매트 재료 원문 표에 없음" + }, + { + "열쇠": "2-3-2 블록 붙이기(인력)", + "이름": "블록 붙이기(인력)", + "결과단위": "원/㎡", + "출처": "건설품셈 토목 2-3-2", + "소유": "공용", + "입력": [], + "중간": [], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:2-3-2 블록 붙이기(인력)).특별인부", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:2-3-2 블록 붙이기(인력)).보통인부", + "비목": "노무비" + } + ], + "덧줄": [], + "끝수": null, + "비고": "비탈면 고르기·흙 채움·잔디심기 별도(주③) · 호안블록 재료 원문 표에 없음" + }, + { + "열쇠": "2-3-3 블록 붙이기(기계)", + "이름": "블록 붙이기(기계)", + "결과단위": "원/㎡", + "출처": "건설품셈 토목 2-3-3", + "소유": "공용", + "입력": [ + { "이름": "장비", "고르기": ["크레인", "굴착기"], "출처": "건설품셈 토목 2-3-3 주④" }, + { + "이름": "크레인", + "고르기": [ + "2105-0005", + "2104-0010", + "2104-0015", + "2104-0020", + "2104-0025", + "2104-0030", + "2104-0035", + "2104-0040", + "2104-0045", + "2104-0050", + "2104-0060", + "2104-0070", + "2104-0080", + "2104-0100", + "2104-0130", + "2104-0160", + "2104-0200", + "2104-0220", + "2104-0250", + "2104-0300", + "2105-0002", + "2105-0003", + "2105-0010", + "2105-0015", + "2105-0018" + ] + }, + { + "이름": "지역", + "고르기": [ + "전국평균", + "전국", + "서울", + "경기", + "강원", + "충북", + "충남", + "전북", + "경북", + "경남", + "부산", + "제주", + "대구", + "인천", + "대전", + "울산", + "세종", + "전남광주" + ], + "출처": "건설품셈 공통 8-1-7 5" + }, + { "이름": "보정작업", "고르기": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "출처": "건설품셈 공통 8-1-7 1" } + ], + "중간": [], + "호표": [ + { + "종류": "인력", + "요소": "인력:건설노임:1003", + "이름": "특별인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:2-3-3 블록 붙이기(기계)).특별인부", + "비목": "노무비" + }, + { + "종류": "인력", + "요소": "인력:건설노임:1002", + "이름": "보통인부", + "단위": "인", + "수량": "찾기(소요량:건설품셈:2-3-3 블록 붙이기(기계)).보통인부", + "비목": "노무비" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 기계 시간당 경비, 기계=크레인, 지역=지역)", + "이름": "크레인", + "단위": "시간", + "수량": "만약(장비 == '크레인', 찾기(소요량:건설품셈:2-3-3 블록 붙이기(기계)).크레인, 0)" + }, + { + "종류": "로직", + "요소": "로직(건설품셈:8 굴착기(무한궤도) 시간당 경비, 기계='0201-0020', 지역=지역, 보정작업=보정작업)", + "이름": "굴착기 0.2㎥", + "단위": "시간", + "수량": "만약(장비 == '굴착기', 찾기(소요량:건설품셈:2-3-3 블록 붙이기(기계) 계수 주④, 구분='크레인 대체 굴착기 사용시간').값, 0)", + "출처": "건설품셈 토목 2-3-3 주④" + } + ], + "덧줄": [], + "끝수": null, + "비고": "크레인 종류·규격 설계자 고름 — 원문 규격 10톤 · 현장여건 따라 크레인 대신 굴착기 0.2㎥ 0.063hr(주④) · 비탈면 고르기·흙 채움·잔디심기 별도(주③) · 호안블록 재료 원문 표에 없음" + } + ] +} diff --git a/resources/master_data/소요량_건설품셈_공통6장.json b/resources/master_data/소요량_건설품셈_공통6장.json index b4b7ed89..185abf8c 100644 --- a/resources/master_data/소요량_건설품셈_공통6장.json +++ b/resources/master_data/소요량_건설품셈_공통6장.json @@ -501,16 +501,16 @@ "조건": { "사용횟수": "고르기" }, "값칸": { "합판": "%", "소모자재": "%" }, "줄": [ - { "사용횟수": "1회", "합판": 55.0, "소모자재": 4.0 }, - { "사용횟수": "2회", "합판": 44.3, "소모자재": 7.0 }, - { "사용횟수": "3회", "합판": 38.0, "소모자재": 8.0 }, - { "사용횟수": "4회", "합판": 35.0, "소모자재": 9.0 }, - { "사용횟수": "5회", "합판": 32.7, "소모자재": 10.0 }, - { "사용횟수": "6회", "소모자재": 11.0 } + { "사용횟수": "1회", "소모자재": 4.0 }, + { "사용횟수": "2회", "합판": 55.0, "소모자재": 7.0 }, + { "사용횟수": "3회", "합판": 44.3, "소모자재": 8.0 }, + { "사용횟수": "4회", "합판": 38.0, "소모자재": 9.0 }, + { "사용횟수": "5회", "합판": 35.0, "소모자재": 10.0 }, + { "사용횟수": "6회", "합판": 32.7, "소모자재": 11.0 } ], "주": [ "소모자재(박리재 등)는 주자재비의 %", - "합판 6회 값은 원문 「-」(빈칸)" + "합판 1회는 수량 칸(1.03㎡)이라 %가 없음" ] }, { diff --git a/resources/master_data/소요량_건설품셈_유지관리3장.json b/resources/master_data/소요량_건설품셈_유지관리3장.json new file mode 100644 index 00000000..7251a2ea --- /dev/null +++ b/resources/master_data/소요량_건설품셈_유지관리3장.json @@ -0,0 +1,501 @@ +{ + "그룹": "소요량", + "원문": "건설품셈", + "부문": "유지관리", + "판": "2026", + "표": [ + { + "열쇠": "3-1-1 콘크리트구조물 헐기(인력)", + "이름": "콘크리트구조물 헐기(인력)", + "기준": "일당", + "출처": "건설품셈 유지관리 3-1-1", + "조건": { "철근유무": "고르기" }, + "값칸": { "착암공": "인", "보통인부": "인", "소형브레이커(1.5kW)": "대", "시공량": "㎥" }, + "줄": [ + { "철근유무": "무근", "착암공": 2, "보통인부": 1, "소형브레이커(1.5kW)": 2, "시공량": 2.7 }, + { "철근유무": "철근", "착암공": 2, "보통인부": 1, "소형브레이커(1.5kW)": 2, "시공량": 2.3 } + ], + "주": [ + "규격 — 소형브레이커 1.5kW.", + "① 본 품은 소형브레이커(전기식)를 사용하여 콘크리트구조물을 철거하는 기준이다.", + "② 본 품은 콘크리트 헐기, 발생재 정리 작업을 포함한다.", + "③ 장애물 제거(철근, 파이프 등)가 필요한 경우 별도 계상한다.", + "④ 잡재료비(치즐 등)는 인력품의 1%로 계상한다." + ] + }, + { + "열쇠": "3-1-2 콘크리트구조물 헐기(기계)", + "이름": "콘크리트구조물 헐기(기계)", + "기준": "일당", + "출처": "건설품셈 유지관리 3-1-2", + "조건": { "장애물": "고르기" }, + "값칸": { "용접공": "인", "특별인부": "인", "보통인부": "인", "굴착기+압쇄기(1.0㎥)": "대", "굴착기(0.6㎥)": "대", "시공량": "㎥" }, + "줄": [ + { "장애물": "미제거", "특별인부": 2, "보통인부": 1, "굴착기+압쇄기(1.0㎥)": 1, "굴착기(0.6㎥)": 1, "시공량": 50 }, + { "장애물": "제거", "용접공": 1, "특별인부": 2, "보통인부": 1, "굴착기+압쇄기(1.0㎥)": 1, "굴착기(0.6㎥)": 1, "시공량": 45 } + ], + "주": [ + "규격 — 굴착기+압쇄기 1.0㎥ · 굴착기 0.6㎥.", + "① 본 품은 장비(굴착기+압쇄기)를 사용한 철근콘크리트 구조물을 해체하는 기준이다.", + "② 본 품은 콘크리트 헐기 및 부수기, 발생재 정리 작업을 포함한다.", + "③ 본 품은 높이 10m이하 기준이며, 특수조건(하부구조보강 필요 등)에 대한 비용은 별도 계상한다.", + "④ 공사장의 보호 및 안전시설 설치비, 폐기물 상차 및 운반, 폐기물 처리비용은 별도 계상한다.", + "⑤ 장비는 현장여건에 따라 규격을 변경하여 적용할 수 있다.", + "⑥ 대형브레이커가 필요한 경우 '[공통부문] 8-2-13 대형브레이커'를 참조하여 별도 계상한다.", + "⑦ 공구손료 및 경장비(살수장비 등)의 기계경비는 인력품의 4%로 계상한다.", + "⑧ 장애물 제거(철근, 파이프 등) 시 재료량은 재료량 표를 참고한다." + ] + }, + { + "열쇠": "3-1-2 콘크리트구조물 헐기(기계) 재료량", + "이름": "콘크리트구조물 헐기(기계) 장애물 제거 재료량(㎥당)", + "기준": "㎥당", + "출처": "건설품셈 유지관리 3-1-2", + "조건": { "구분": "고르기" }, + "값칸": { "단위": "글", "수량": "수" }, + "줄": [ + { "구분": "산소(대기압상태기준)", "단위": "L", "수량": 135 }, + { "구분": "아세틸렌", "단위": "㎏", "수량": 0.05 } + ], + "주": [ "※ 산소량은 대기압상태의 기준량이며, 압축산소는 35℃에서 150기압으로 압축용기에 넣어 사용하는 것을 기준한다." ] + }, + { + "열쇠": "3-1-3 철골재 철거(인력)", + "이름": "철골재 철거(인력)", + "기준": "일당", + "출처": "건설품셈 유지관리 3-1-3", + "조건": {}, + "값칸": { "용접공": "인", "보통인부": "인", "시공량": "ton" }, + "줄": [ { "용접공": 3, "보통인부": 2, "시공량": 1.4 } ], + "주": [ + "① 본 품은 산소용접기를 사용하여 철골재 구조물을 해체하는 기준이다.", + "② 본 품은 철골재 철거, 발생재 정리 작업을 포함한다.", + "③ 공사장의 보호 및 안전시설 설치비, 폐기물 상차 및 운반, 폐기물 처리비용은 별도 계상한다.", + "④ 재료량은 재료량 표를 참고하여 적용한다." + ] + }, + { + "열쇠": "3-1-3 철골재 철거(인력) 재료량", + "이름": "철골재 철거(인력) 재료량(ton당)", + "기준": "ton당", + "출처": "건설품셈 유지관리 3-1-3", + "조건": { "구분": "고르기" }, + "값칸": { "단위": "글", "수량": "수" }, + "줄": [ + { "구분": "산소", "단위": "병", "수량": 0.7 }, + { "구분": "아세틸렌", "단위": "㎏", "수량": 2.5 }, + { "구분": "LPG", "단위": "㎏", "수량": 2.0 } + ], + "주": [ + "※ 아세틸렌(산소포함) 또는 L.P.G 중 한가지만 선택 사용한다.", + "※ 산소량은 대기압상태의 기준량이며, 압축산소는 35℃에서 150기압으로 압축용기에 넣어 사용하는 것을 기준한다." + ] + }, + { + "열쇠": "3-1-4 철골재 철거(기계)", + "이름": "철골재 철거(기계)", + "기준": "일당", + "출처": "건설품셈 유지관리 3-1-4", + "조건": {}, + "값칸": { "특별인부": "인", "보통인부": "인", "굴착기+빔커터기(1.0㎥)": "대", "굴착기(1.0㎥)": "대", "크레인": "대", "시공량": "ton" }, + "줄": [ { "특별인부": 2, "보통인부": 1, "굴착기+빔커터기(1.0㎥)": 1, "굴착기(1.0㎥)": 1, "크레인": 1, "시공량": 22 } ], + "주": [ + "① 본 품은 장비(굴착기+빔커터기 등)를 사용하여 철골재 구조물을 해체하는 기준이다.", + "② 본 품은 철골재 철거, 발생재 정리 작업을 포함한다.", + "③ 높이 10m이하(지상에서 철거) 기준이며, 특수조건(소형장비 추가투입 등)에 대한 비용은 별도 계상한다.", + "④ 공사장의 보호 및 안전시설 설치비, 폐기물 상차 및 운반, 폐기물 처리비용은 별도 계상한다.", + "⑤ 크레인의 규격은 작업여건(시공높이, 시공위치 등) 및 안전율(적정하중, 작업반경 등)을 고려하여 적합한 규격을 적용한다.", + "⑥ 장비는 현장여건에 따라 규격을 변경하여 적용할 수 있다.", + "⑦ 공구손료 및 경장비(살수장비 등)의 기계경비는 인력품의 4%로 계상한다." + ] + }, + { + "열쇠": "3-1-5 석축 헐기(인력)", + "이름": "석축 헐기(인력)", + "기준": "정의별", + "출처": "건설품셈 유지관리 3-1-5", + "조건": { "공종": "고르기" }, + "값칸": { "기준": "글", "할석공": "인", "보통인부": "인" }, + "줄": [ + { "공종": "메쌓기 뒷길이 45∼60㎝", "기준": "㎡당", "보통인부": 0.2 }, + { "공종": "메쌓기 뒷길이 60∼90㎝", "기준": "㎡당", "보통인부": 0.3 }, + { "공종": "찰쌓기", "기준": "㎡당", "보통인부": 0.6 }, + { "공종": "절석(마름돌) 쌓기", "기준": "㎥당", "할석공": 0.1, "보통인부": 1.1 } + ], + "주": [ + "원문 그대로 — 이 표는 밑줄(칸 경계선) 없이 찍힘.", + "① 본 품은 기준높이 3.6m일 때의 인력헐기를 기준한 것이며, 그 이상일 때의 작업 안전설비 및 특수 조건에 대한 품은 별도 계상한다.", + "② 발생품을 재사용코자 할 때나 제자리 고르기를 할 경우는 별도 계상한다.", + "③ 본 품은 부수기내의 장애물 제거(철근, 파이프 등) 및 공구손료가 포함되어 있다.", + "④ 잡재료는 인력품의 5%이내에서 계상한다." + ] + }, + { + "열쇠": "3-2-1 금속기와 해체", + "이름": "금속기와 해체", + "기준": "㎡당", + "출처": "건설품셈 유지관리 3-2-1", + "조건": {}, + "값칸": { "지붕잇기공": "인", "보통인부": "인" }, + "줄": [ { "지붕잇기공": 0.018, "보통인부": 0.012 } ], + "주": [ + "① 본 품은 금속기와 지붕을 재사용하지 아니하는 때의 절단하여 해체하는 기준이다.", + "② 본 품은 지붕재 및 후레싱 해체 작업을 포함한다.", + "③ 비산방지, 보호 및 안전시설 등의 설치비는 별도 계상한다.", + "④ 폐기물 처리비용은 별도 계상한다.", + "⑤ 공구손료 및 경장비(절단기 등)의 기계경비는 인력품의 3%로 계상한다." + ] + }, + { + "열쇠": "3-2-2 흡음텍스 해체", + "이름": "흡음텍스 해체", + "기준": "㎡당", + "출처": "건설품셈 유지관리 3-2-2", + "조건": {}, + "값칸": { "내장공": "인", "보통인부": "인" }, + "줄": [ { "내장공": 0.016, "보통인부": 0.011 } ], + "주": [ + "① 본 품은 흡음텍스를 재사용하지 아니하는 때의 해체하는 기준이다.", + "② 비산방지, 보호 및 안전시설 등의 설치비는 별도 계상한다.", + "③ 폐기물 처리비용은 별도 계상한다." + ] + }, + { + "열쇠": "3-2-3 경량천장철골틀 해체", + "이름": "경량천장철골틀 해체", + "기준": "㎡당", + "출처": "건설품셈 유지관리 3-2-3", + "조건": {}, + "값칸": { "내장공": "인", "보통인부": "인" }, + "줄": [ { "내장공": 0.018, "보통인부": 0.012 } ], + "주": [ + "① 본 품은 경량천장철골틀을 재사용하지 아니하는 때의 절단하여 해체하는 기준이다.", + "② 본 품은 천장틀(채널, BAR 등) 해체, 달대 및 행거 해체 작업을 포함한다.", + "③ 비산방지, 보호 및 안전시설 등의 설치비는 별도 계상한다.", + "④ 폐기물 처리비용은 별도 계상한다.", + "⑤ 공구손료 및 경장비(절단기 등)의 기계경비는 인력품의 2%로 계상한다." + ] + }, + { + "열쇠": "3-2-4 조적벽 해체", + "이름": "조적벽 해체", + "기준": "㎥당", + "출처": "건설품셈 유지관리 3-2-4", + "조건": {}, + "값칸": { "조적공": "인", "보통인부": "인" }, + "줄": [ { "조적공": 0.380, "보통인부": 0.252 } ], + "주": [ + "① 본 품은 조적벽(높이 3.6m이하)을 재사용하지 아니하는 때의 해체하는 기준이다.", + "② 본 품은 조적벽 해체, 고정철물 해체 작업을 포함한다.", + "③ 비산방지, 보호 및 안전시설 등의 설치비는 별도 계상한다.", + "④ 폐기물 처리비용은 별도 계상한다.", + "⑤ 공구손료 및 경장비(함마 등)의 기계경비는 인력품의 2%로 계상한다." + ] + }, + { + "열쇠": "3-2-5 경량벽체철골틀 해체", + "이름": "경량벽체철골틀 해체", + "기준": "㎡당", + "출처": "건설품셈 유지관리 3-2-5", + "조건": {}, + "값칸": { "내장공": "인", "보통인부": "인" }, + "줄": [ { "내장공": 0.016, "보통인부": 0.011 } ], + "주": [ + "① 본 품은 경량벽체철골틀을 재사용하지 아니하는 때의 절단하여 해체하는 기준이다.", + "② 본 품은 러너 및 스터드 해체 작업을 포함한다.", + "③ 비산방지, 보호 및 안전시설 등의 설치비는 별도 계상한다.", + "④ 폐기물 처리비용은 별도 계상한다.", + "⑤ 공구손료 및 경장비(절단기 등)의 기계경비는 인력품의 2%로 계상한다." + ] + }, + { + "열쇠": "3-2-6 석고판 해체", + "이름": "석고판 해체", + "기준": "㎡당", + "출처": "건설품셈 유지관리 3-2-6", + "조건": { "부위": "고르기" }, + "값칸": { "내장공": "인", "보통인부": "인" }, + "줄": [ + { "부위": "벽", "내장공": 0.014, "보통인부": 0.010 }, + { "부위": "천장", "내장공": 0.016, "보통인부": 0.012 } + ], + "주": [ + "① 본 품은 석고판을 재사용하지 아니하는 때의 절단하여 해체하는 기준이다.", + "② 비산방지, 보호 및 안전시설 등의 설치비는 별도 계상한다.", + "③ 폐기물 처리비용은 별도 계상한다.", + "④ 공구손료 및 경장비(절단기 등)의 기계경비는 인력품의 2%로 계상한다." + ] + }, + { + "열쇠": "3-2-7 도배 해체", + "이름": "도배 해체", + "기준": "㎡당", + "출처": "건설품셈 유지관리 3-2-7", + "조건": { "부위": "고르기" }, + "값칸": { "도배공": "인", "보통인부": "인" }, + "줄": [ + { "부위": "벽", "도배공": 0.008, "보통인부": 0.005 }, + { "부위": "천장", "도배공": 0.010, "보통인부": 0.007 } + ], + "주": [ + "① 본 품은 도배지를 재사용하지 아니하는 때의 해체하는 기준이다.", + "② 본 품은 정배지 및 초배지 해체 작업을 포함한다.", + "③ 비산방지, 보호 및 안전시설 등의 설치비는 별도 계상한다.", + "④ 폐기물 처리비용은 별도 계상한다." + ] + }, + { + "열쇠": "3-2-8 PVC계바닥재 해체", + "이름": "PVC계바닥재 해체", + "기준": "㎡당", + "출처": "건설품셈 유지관리 3-2-8", + "조건": {}, + "값칸": { "내장공": "인", "보통인부": "인" }, + "줄": [ { "내장공": 0.006, "보통인부": 0.004 } ], + "주": [ + "① 본 품은 PVC계 바닥재(시트)를 재사용하지 아니하는 때의 해체하는 기준이다.", + "② 비산방지, 보호 및 안전시설 등의 설치비는 별도 계상한다.", + "③ 폐기물 처리비용은 별도 계상한다." + ] + }, + { + "열쇠": "3-2-9 타일 해체", + "이름": "타일 해체", + "기준": "㎡당", + "출처": "건설품셈 유지관리 3-2-9", + "조건": { "공법": "고르기" }, + "값칸": { "타일공": "인", "보통인부": "인" }, + "줄": [ + { "공법": "떠붙이기", "타일공": 0.037, "보통인부": 0.024 }, + { "공법": "압착붙이기,접착붙이기", "타일공": 0.041, "보통인부": 0.027 } + ], + "주": [ + "① 본 품은 타일을 재사용하지 아니하는 때의 해체하는 기준이다.", + "② 본 품은 타일 및 접착제 깨기 작업을 포함한다.", + "③ 비산방지, 보호 및 안전시설 등의 설치비는 별도 계상한다.", + "④ 폐기물 처리비용은 별도 계상한다.", + "⑤ 공구손료 및 경장비(절단기 등)의 기계경비는 인력품의 6%로 계상한다." + ] + }, + { + "열쇠": "3-2-10 기존방수층 및 보호층 철거", + "이름": "기존방수층 및 보호층 철거", + "기준": "㎡당", + "출처": "건설품셈 유지관리 3-2-10", + "조건": {}, + "값칸": { "착암공": "인", "보통인부": "인", "소형브레이커(1.3㎥/min)": "시간", "공기압축기(3.5㎥/min)": "시간" }, + "줄": [ { "착암공": 0.06, "보통인부": 0.22, "소형브레이커(1.3㎥/min)": 0.10, "공기압축기(3.5㎥/min)": 0.05 } ], + "주": [ + "규격 — 소형브레이커 1.3㎥/min · 공기압축기 3.5㎥/min.", + "① 본 품은 아스팔트 8층 방수를 보수하기 위하여 방수층을 철거하는 품으로 누름 콘크리트층의 파쇄, 방수층 철거, 폐자재 소운반 및 정리품이 포함되어 있다.", + "② 소규모공사(개소당 작업면적 40㎡미만)인 경우는 장비 사용기간 및 품을 40% 범위 내에서 가산하여 적용한다.", + "③ 누름 콘크리트 두께 8㎝ 기준이다." + ] + }, + { + "열쇠": "3-2-11 기존방수층 제거 및 바탕처리", + "이름": "기존방수층 제거 및 바탕처리", + "기준": "㎡당", + "출처": "건설품셈 유지관리 3-2-11", + "조건": { "부위": "고르기" }, + "값칸": { "방수공": "인", "보통인부": "인" }, + "줄": [ + { "부위": "바닥", "방수공": 0.037, "보통인부": 0.015 }, + { "부위": "수직부", "방수공": 0.041, "보통인부": 0.017 } + ], + "주": [ + "① 본 품은 재방수를 하기 위하여 기존방수층(도막방수)을 제거하고 바탕처리하는 기준이다.", + "② 본 품은 방수층 제거, 홈메우기, 불순물 청소, 퍼티 작업을 포함한다.", + "③ 공구손료 및 경장비(엔진송풍기, 연마기 등)의 기계경비는 인력품의 6%로 계상한다.", + "④ 바탕처리에 사용되는 재료(퍼티, 방수테이프 등)는 별도 계상한다." + ] + }, + { + "열쇠": "3-2-12 석면건축자재 해체", + "이름": "석면건축자재 해체", + "기준": "㎡당", + "출처": "건설품셈 유지관리 3-2-12", + "조건": { "부위": "고르기" }, + "값칸": { "석면해체공": "인", "보통인부": "인" }, + "줄": [ + { "부위": "내장재", "석면해체공": 0.120, "보통인부": 0.017 }, + { "부위": "외장재", "석면해체공": 0.045, "보통인부": 0.011 }, + { "부위": "뿜칠재", "석면해체공": 0.5 } + ], + "주": [ + "① 본 품은 석면이 함유된 자재를 해체하는 품으로 적용기준은 다음과 같다.", + "㉮ 내장재는 건축물의 내부 천장재, 내벽체, 간막이재 철거를 기준한 것이다.", + "㉯ 외장재는 슬레이트 지붕재 해체를 기준한 것이다.", + "㉰ 뿜칠재는 철골내화피복재를 기준으로 한 것으로 철골면의 하부면, 측면부, 상부면 등의 해체공사와 철재로 시공된 천장면에 부착되어 있는 뿜칠재의 해체를 기준한 것이다.", + "② 뿜칠재의 경우, 콘크리트면에 부착된 석면 뿜칠재의 해체는 본 품의 20%를 할증하여 적용한다.", + "③ 본 품은 비닐보양재, 오염제거구역 설치 및 해체가 포함된 것이며, 보양막(외장재) 설치 및 해체품은 제외되어 있다.", + "④ 본 품은 일일 작업시간 6시간을 기준한 것이다.", + "⑤ 석면자재의 해체 작업 시 소요되는 기기경비 및 재료비, 소모품비는 별도 계상한다.", + "⑥ 실내 고소작업 및 실외 비계설치를 위한 가설재의 설치는 별도 계상한다." + ] + }, + { + "열쇠": "3-3-1 재도장 시 바탕처리(콘크리트·모르타르면)", + "이름": "재도장 시 바탕처리(콘크리트·모르타르면)", + "기준": "일당", + "출처": "건설품셈 유지관리 3-3-1", + "조건": {}, + "값칸": { "도장공": "인", "보통인부": "인", "시공량": "㎡" }, + "줄": [ { "도장공": 2, "보통인부": 1, "시공량": 230 } ], + "주": [ + "① 본 품은 콘크리트·모르타르면 재도장 시 바탕처리하는 기준이다.", + "② 본 품은 기존 도장면을 제거하지 않고, 곰팡이 등 오염, 균열 부위에 부분적으로 퍼티 및 연마하는 작업 기준이다.", + "③ 공구손료 및 잡재료비(연마지 등)는 인력품의 3%로 계상한다." + ] + }, + { + "열쇠": "3-3-2 재도장 시 바탕처리(철재면)", + "이름": "재도장 시 바탕처리(철재면)", + "기준": "일당", + "출처": "건설품셈 유지관리 3-3-2", + "조건": { "등급": "고르기" }, + "값칸": { "도장공": "인", "보통인부": "인", "시공량": "㎡" }, + "줄": [ + { "등급": "A급", "도장공": 2, "보통인부": 1, "시공량": 20 }, + { "등급": "B급", "도장공": 2, "보통인부": 1, "시공량": 60 } + ], + "주": [ + "① 본 품은 철재면 재도장 시 바탕처리하는 기준이다.", + "② 본 품은 오염(기름때 등) 및 부착물 제거, 도장면 연마 및 청소 작업을 포함한다.", + "③ 대상면의 상태에 따른 적용기준은 적용기준 표와 같다.", + "④ 공구손료 및 잡재료비(연마지 등)는 인력품의 3%로 계상한다." + ] + }, + { + "열쇠": "3-3-2 재도장 시 바탕처리(철재면) 적용기준", + "이름": "재도장 시 바탕처리(철재면) 대상면 적용기준", + "기준": "정의", + "출처": "건설품셈 유지관리 3-3-2", + "조건": { "구분": "고르기" }, + "값칸": { "적용기준": "글" }, + "줄": [ + { "구분": "A급", "적용기준": "재래도장의 탈락이 심하고 부분적으로 부식되어 약품을 사용하여 도막 및 기타 부착물의 완전 제거를 요하는 정도" }, + { "구분": "B급", "적용기준": "재래도장의 부출되어 있는 녹을 제거하고 와이어 브러쉬로 청소할 정도" } + ], + "주": [ "③ 대상면의 상태에 따른 적용기준은 다음과 같다." ] + }, + { + "열쇠": "3-3-3 재도장 시 바탕처리(목재면)", + "이름": "재도장 시 바탕처리(목재면)", + "기준": "일당", + "출처": "건설품셈 유지관리 3-3-3", + "조건": { "등급": "고르기" }, + "값칸": { "도장공": "인", "보통인부": "인", "시공량": "㎡" }, + "줄": [ + { "등급": "A급", "도장공": 2, "보통인부": 1, "시공량": 110 }, + { "등급": "B급", "도장공": 2, "보통인부": 1, "시공량": 270 } + ], + "주": [ + "① 본 품은 목재면 재도장 시 바탕처리하는 기준이다.", + "② 본 품은 오염 및 부착물 제거, 틈새 및 구멍 충진, 퍼티 및 연마 작업을 포함한다.", + "③ 대상면의 상태에 따른 적용기준은 적용기준 표와 같다.", + "④ 공구손료 및 잡재료비(연마지 등)는 인력품의 3%로 계상한다." + ] + }, + { + "열쇠": "3-3-3 재도장 시 바탕처리(목재면) 적용기준", + "이름": "재도장 시 바탕처리(목재면) 대상면 적용기준", + "기준": "정의", + "출처": "건설품셈 유지관리 3-3-3", + "조건": { "구분": "고르기" }, + "값칸": { "적용기준": "글" }, + "줄": [ + { "구분": "A급", "적용기준": "재래도장의 탈락 및 목재의 손상이 심하여 갈라진틈, 구멍 땜 등을 충진하고, 평탄하게 연마해야하는 정도" }, + { "구분": "B급", "적용기준": "재래도장의 탈락 및 목재의 손상이 거의 없으며, 부착물 제거, 부분적으로 퍼티 및 연마를 요하는 정도" } + ], + "주": [ "③ 대상면의 상태에 따른 적용기준은 다음과 같다." ] + }, + { + "열쇠": "3-4-1 지붕 덧씌우기", + "이름": "지붕 덧씌우기", + "기준": "일당", + "출처": "건설품셈 유지관리 3-4-1", + "조건": {}, + "값칸": { "지붕잇기공": "인", "보통인부": "인", "시공량": "㎡" }, + "줄": [ { "지붕잇기공": 4, "보통인부": 2, "시공량": 85 } ], + "주": [ + "비고 — 맞배지붕(경사를 짓는 지붕면이 2개소)은 시공량을 20% 가산하여 적용한다.", + "① 본 품은 기존의 지붕 위에 신규 지붕을 덧씌워 보수하는 기준이다.", + "② 본 품은 바탕정리, 지붕틀 설치, 지붕재(금속기와) 설치, 용마루 및 후레싱 마감 작업을 포함한다.", + "③ 홈통 및 빗물받이 설치는 '[건축부문] 7-2 홈통'을 따른다.", + "④ 비계매기, 비산방지, 보호 및 안전시설의 설치비는 별도 계상한다.", + "⑤ 공구손료 및 경장비(에어콤프, 절단기 등)의 기계경비는 인력품의 2%로 계상한다." + ] + }, + { + "열쇠": "3-4-2 지붕 재설치", + "이름": "지붕 재설치", + "기준": "일당", + "출처": "건설품셈 유지관리 3-4-2", + "조건": {}, + "값칸": { "지붕잇기공": "인", "보통인부": "인", "시공량": "㎡" }, + "줄": [ { "지붕잇기공": 6, "보통인부": 2, "시공량": 50 } ], + "주": [ + "비고 — 맞배지붕(경사를 짓는 지붕면이 2개소)은 시공량을 20% 가산하여 적용한다.", + "① 본 품은 기존의 지붕재가 철거된 상태에서 신규 지붕을 재설치하는 기준이다.", + "② 본 품은 바탕정리, 지붕틀 및 바탕합판 설치, 방수시트 및 단열재 설치, 지붕재(금속기와) 설치, 용마루 및 후레싱 마감 작업을 포함한다.", + "③ 홈통 및 빗물받이 설치는 '[건축부문] 7-2 홈통'을 따른다.", + "④ 지붕재 철거는 별도 계상한다.", + "⑤ 비계매기, 비산방지, 보호 및 안전시설(비계 등)의 설치비는 별도 계상한다.", + "⑥ 공구손료 및 경장비(에어콤프, 절단기 등)의 기계경비는 인력품의 2%로 계상한다." + ] + }, + { + "열쇠": "3-4-3 도배 교체", + "이름": "도배 교체", + "기준": "일당", + "출처": "건설품셈 유지관리 3-4-3", + "조건": { "부위": "고르기" }, + "값칸": { "내장공": "인", "시공량": "㎡" }, + "줄": [ + { "부위": "벽", "내장공": 2, "시공량": 46 }, + { "부위": "천장", "내장공": 2, "시공량": 35 } + ], + "주": [ + "비고 — 사용중인 세대로 가구 등의 지장물이 있는 경우 시공량의 15%를 감한다.", + "① 본 품은 도배지를 해체(재사용하지 아니하는 때)하고 재설치하는 기준이다.", + "② 본 품은 도배지 해체, 바탕정리, 풀먹임, 초배 및 정배 바름 작업을 포함한다.", + "③ 가구 등 지장물의 운반은 별도 계상한다." + ] + }, + { + "열쇠": "3-4-4 PVC계바닥재 교체", + "이름": "PVC계바닥재 교체", + "기준": "일당", + "출처": "건설품셈 유지관리 3-4-4", + "조건": {}, + "값칸": { "내장공": "인", "시공량": "㎡" }, + "줄": [ { "내장공": 2, "시공량": 61 } ], + "주": [ + "비고 — 사용중인 세대로 가구 등의 지장물이 있는 경우 시공량의 15%를 감한다.", + "① 본 품은 PVC계 바닥재(시트)를 해체(재사용하지 아니하는 때)하고 재설치하는 기준이다.", + "② 본 품은 바닥재 해체, 바탕정리, 접착제(부분접합 방식) 바름, 바닥재 설치 작업을 포함한다.", + "③ 가구 등 지장물의 운반은 별도 계상한다." + ] + }, + { + "열쇠": "3-4-5 타일 교체", + "이름": "타일 교체", + "기준": "일당", + "출처": "건설품셈 유지관리 3-4-5", + "조건": { "공법": "고르기" }, + "값칸": { "타일공": "인", "시공량": "㎡" }, + "줄": [ + { "공법": "떠붙이기(벽)", "타일공": 2, "시공량": 7 }, + { "공법": "압착붙이기(바닥)", "타일공": 2, "시공량": 8 } + ], + "주": [ + "비고 — 사용중인 세대로 가구 등의 지장물이 있는 경우 시공량의 15%를 감한다.", + "① 본 품은 타일을 해체(재사용하지 아니하는 때)하고 재설치하는 기준이다.", + "② 본 품은 타일 해체, 바탕정리, 타일 붙임, 줄눈 설치 및 마무리 작업을 포함한다.", + "③ 방수 작업은 별도 계상한다.", + "④ 가구 등 지장물의 운반은 별도 계상한다." + ] + } + ] +} diff --git a/resources/master_data/소요량_건설품셈_유지관리4장.json b/resources/master_data/소요량_건설품셈_유지관리4장.json new file mode 100644 index 00000000..4004e49b --- /dev/null +++ b/resources/master_data/소요량_건설품셈_유지관리4장.json @@ -0,0 +1,353 @@ +{ + "그룹": "소요량", + "원문": "건설품셈", + "부문": "유지관리", + "판": "2026", + "표": [ + { + "열쇠": "4-1-1 배관 해체", + "이름": "배관 해체", + "기준": "m당", + "출처": "건설품셈 유지관리 4-1-1", + "조건": { "관종": "고르기", "규격(㎜)": "고르기" }, + "값칸": { "배관공": "인", "보통인부": "인" }, + "줄": [ + { "관종": "강관", "규격(㎜)": "ø15이하", "배관공": 0.012, "보통인부": 0.008 }, + { "관종": "강관", "규격(㎜)": "20", "배관공": 0.013, "보통인부": 0.009 }, + { "관종": "강관", "규격(㎜)": "25", "배관공": 0.017, "보통인부": 0.011 }, + { "관종": "강관", "규격(㎜)": "32", "배관공": 0.019, "보통인부": 0.013 }, + { "관종": "강관", "규격(㎜)": "40", "배관공": 0.021, "보통인부": 0.014 }, + { "관종": "강관", "규격(㎜)": "50", "배관공": 0.027, "보통인부": 0.018 }, + { "관종": "강관", "규격(㎜)": "65", "배관공": 0.031, "보통인부": 0.021 }, + { "관종": "강관", "규격(㎜)": "80", "배관공": 0.039, "보통인부": 0.026 }, + { "관종": "강관", "규격(㎜)": "100", "배관공": 0.053, "보통인부": 0.035 }, + { "관종": "강관", "규격(㎜)": "125", "배관공": 0.067, "보통인부": 0.045 }, + { "관종": "강관", "규격(㎜)": "150", "배관공": 0.079, "보통인부": 0.053 }, + { "관종": "강관", "규격(㎜)": "200", "배관공": 0.121, "보통인부": 0.080 }, + { "관종": "강관", "규격(㎜)": "250", "배관공": 0.161, "보통인부": 0.107 }, + { "관종": "강관", "규격(㎜)": "300", "배관공": 0.208, "보통인부": 0.139 }, + { "관종": "강관", "규격(㎜)": "350", "배관공": 0.250, "보통인부": 0.167 }, + { "관종": "강관", "규격(㎜)": "400", "배관공": 0.296, "보통인부": 0.197 }, + { "관종": "동관", "규격(㎜)": "ø15이하", "배관공": 0.010, "보통인부": 0.007 }, + { "관종": "동관", "규격(㎜)": "20", "배관공": 0.012, "보통인부": 0.008 }, + { "관종": "동관", "규격(㎜)": "25", "배관공": 0.015, "보통인부": 0.010 }, + { "관종": "동관", "규격(㎜)": "32", "배관공": 0.018, "보통인부": 0.012 }, + { "관종": "동관", "규격(㎜)": "40", "배관공": 0.020, "보통인부": 0.014 }, + { "관종": "동관", "규격(㎜)": "50", "배관공": 0.027, "보통인부": 0.018 }, + { "관종": "동관", "규격(㎜)": "65", "배관공": 0.031, "보통인부": 0.021 }, + { "관종": "동관", "규격(㎜)": "80", "배관공": 0.039, "보통인부": 0.026 }, + { "관종": "동관", "규격(㎜)": "100", "배관공": 0.053, "보통인부": 0.035 }, + { "관종": "동관", "규격(㎜)": "125", "배관공": 0.066, "보통인부": 0.044 }, + { "관종": "동관", "규격(㎜)": "150", "배관공": 0.078, "보통인부": 0.052 }, + { "관종": "동관", "규격(㎜)": "200", "배관공": 0.116, "보통인부": 0.077 }, + { "관종": "동관", "규격(㎜)": "250", "배관공": 0.153, "보통인부": 0.102 } + ], + "주": [ + "원문 그대로 — 동관 300~400 규격 칸은 빈 칸(글자층 전각 빈칸).", + "① 본 품은 배관을 재사용하지 아니하는 때의 절단하여 해체하는 기준이다.", + "② 본 품은 지지철물, 배관 해체를 포함한다.", + "③ 비산방지, 보호 및 안전시설 등의 설치비는 별도 계상한다.", + "④ 폐기물 처리비용은 별도 계상한다.", + "⑤ 공구손료 및 경장비(절단기 등)의 기계경비는 인력품의 2%로 계상한다." + ] + }, + { + "열쇠": "4-1-2 각형덕트 해체", + "이름": "각형덕트 해체", + "기준": "㎡당", + "출처": "건설품셈 유지관리 4-1-2", + "조건": { "호칭두께(mm)": "고르기" }, + "값칸": { "덕트공": "인", "보통인부": "인" }, + "줄": [ + { "호칭두께(mm)": "0.5", "덕트공": 0.064, "보통인부": 0.043 }, + { "호칭두께(mm)": "0.6", "덕트공": 0.060, "보통인부": 0.040 }, + { "호칭두께(mm)": "0.8", "덕트공": 0.063, "보통인부": 0.042 }, + { "호칭두께(mm)": "1.0", "덕트공": 0.077, "보통인부": 0.051 }, + { "호칭두께(mm)": "1.2", "덕트공": 0.089, "보통인부": 0.059 }, + { "호칭두께(mm)": "1.6", "덕트공": 0.111, "보통인부": 0.074 } + ], + "주": [ + "① 본 품은 각형덕트(아연도금강판, 스테인리스)를 재사용하지 아니하는 때의 절단하여 해체하는 기준이다.", + "② 본 품은 지지철물, 덕트 절단 및 해체를 포함한다.", + "③ 비산방지, 보호 및 안전시설 등의 설치비는 별도 계상한다.", + "④ 폐기물 처리비용은 별도 계상한다.", + "⑤ 공구손료 및 경장비(절단기 등)의 기계경비는 인력품의 2%로 계상한다." + ] + }, + { + "열쇠": "4-1-3 스파이럴덕트 해체", + "이름": "스파이럴덕트 해체", + "기준": "m당", + "출처": "건설품셈 유지관리 4-1-3", + "조건": { "철판두께(㎜)": "고르기", "규격(㎜)": "고르기" }, + "값칸": { "덕트공": "인", "보통인부": "인" }, + "줄": [ + { "철판두께(㎜)": "0.5", "규격(㎜)": "ø150이하", "덕트공": 0.036, "보통인부": 0.024 }, + { "철판두께(㎜)": "0.5", "규격(㎜)": "160", "덕트공": 0.037, "보통인부": 0.025 }, + { "철판두께(㎜)": "0.5", "규격(㎜)": "180", "덕트공": 0.041, "보통인부": 0.028 }, + { "철판두께(㎜)": "0.5", "규격(㎜)": "200", "덕트공": 0.045, "보통인부": 0.030 }, + { "철판두께(㎜)": "0.6", "규격(㎜)": "225", "덕트공": 0.050, "보통인부": 0.033 }, + { "철판두께(㎜)": "0.6", "규격(㎜)": "250", "덕트공": 0.055, "보통인부": 0.036 }, + { "철판두께(㎜)": "0.6", "규격(㎜)": "275", "덕트공": 0.059, "보통인부": 0.040 }, + { "철판두께(㎜)": "0.6", "규격(㎜)": "300", "덕트공": 0.064, "보통인부": 0.043 }, + { "철판두께(㎜)": "0.6", "규격(㎜)": "350", "덕트공": 0.074, "보통인부": 0.049 }, + { "철판두께(㎜)": "0.6", "규격(㎜)": "400", "덕트공": 0.084, "보통인부": 0.056 }, + { "철판두께(㎜)": "0.6", "규격(㎜)": "450", "덕트공": 0.104, "보통인부": 0.069 }, + { "철판두께(㎜)": "0.6", "규격(㎜)": "500", "덕트공": 0.114, "보통인부": 0.076 }, + { "철판두께(㎜)": "0.6", "규격(㎜)": "550", "덕트공": 0.123, "보통인부": 0.082 }, + { "철판두께(㎜)": "0.6", "규격(㎜)": "600", "덕트공": 0.132, "보통인부": 0.088 } + ], + "주": [ + "① 본 품은 스파이럴덕트(아연도금강판)를 재사용하지 아니하는 때의 절단하여 해체하는 기준이다.", + "② 본 품은 지지철물, 덕트 절단 및 해체를 포함한다.", + "③ 비산방지, 보호 및 안전시설 등의 설치비는 별도 계상한다.", + "④ 폐기물 처리비용은 별도 계상한다.", + "⑤ 공구손료 및 경장비(절단기 등)의 기계경비는 인력품의 2%로 계상한다." + ] + }, + { + "열쇠": "4-1-4 배관보온 해체", + "이름": "배관보온 해체", + "기준": "m당", + "출처": "건설품셈 유지관리 4-1-4", + "조건": { "보온재종류": "고르기", "규격(㎜)": "고르기" }, + "값칸": { "보온공": "인", "보통인부": "인" }, + "줄": [ + { "보온재종류": "고무발포보온재", "규격(㎜)": "ø15", "보온공": 0.014, "보통인부": 0.010 }, + { "보온재종류": "고무발포보온재", "규격(㎜)": "20", "보온공": 0.016, "보통인부": 0.011 }, + { "보온재종류": "고무발포보온재", "규격(㎜)": "25", "보온공": 0.017, "보통인부": 0.011 }, + { "보온재종류": "고무발포보온재", "규격(㎜)": "32", "보온공": 0.020, "보통인부": 0.013 }, + { "보온재종류": "고무발포보온재", "규격(㎜)": "40", "보온공": 0.023, "보통인부": 0.016 }, + { "보온재종류": "고무발포보온재", "규격(㎜)": "50", "보온공": 0.027, "보통인부": 0.018 }, + { "보온재종류": "고무발포보온재", "규격(㎜)": "65", "보온공": 0.029, "보통인부": 0.020 }, + { "보온재종류": "고무발포보온재", "규격(㎜)": "80", "보온공": 0.033, "보통인부": 0.022 }, + { "보온재종류": "고무발포보온재", "규격(㎜)": "100", "보온공": 0.038, "보통인부": 0.025 }, + { "보온재종류": "고무발포보온재", "규격(㎜)": "125", "보온공": 0.046, "보통인부": 0.030 }, + { "보온재종류": "고무발포보온재", "규격(㎜)": "150", "보온공": 0.053, "보통인부": 0.035 }, + { "보온재종류": "고무발포보온재", "규격(㎜)": "200", "보온공": 0.064, "보통인부": 0.042 }, + { "보온재종류": "고무발포보온재", "규격(㎜)": "250", "보온공": 0.073, "보통인부": 0.049 }, + { "보온재종류": "고무발포보온재", "규격(㎜)": "300", "보온공": 0.083, "보통인부": 0.055 }, + { "보온재종류": "발포폴리에틸렌보온재", "규격(㎜)": "ø15", "보온공": 0.010, "보통인부": 0.007 }, + { "보온재종류": "발포폴리에틸렌보온재", "규격(㎜)": "20", "보온공": 0.012, "보통인부": 0.008 }, + { "보온재종류": "발포폴리에틸렌보온재", "규격(㎜)": "25", "보온공": 0.012, "보통인부": 0.008 }, + { "보온재종류": "발포폴리에틸렌보온재", "규격(㎜)": "32", "보온공": 0.014, "보통인부": 0.010 }, + { "보온재종류": "발포폴리에틸렌보온재", "규격(㎜)": "40", "보온공": 0.017, "보통인부": 0.011 }, + { "보온재종류": "발포폴리에틸렌보온재", "규격(㎜)": "50", "보온공": 0.019, "보통인부": 0.013 }, + { "보온재종류": "발포폴리에틸렌보온재", "규격(㎜)": "65", "보온공": 0.021, "보통인부": 0.014 }, + { "보온재종류": "발포폴리에틸렌보온재", "규격(㎜)": "80", "보온공": 0.024, "보통인부": 0.016 }, + { "보온재종류": "발포폴리에틸렌보온재", "규격(㎜)": "100", "보온공": 0.027, "보통인부": 0.018 }, + { "보온재종류": "발포폴리에틸렌보온재", "규격(㎜)": "125", "보온공": 0.033, "보통인부": 0.022 }, + { "보온재종류": "발포폴리에틸렌보온재", "규격(㎜)": "150", "보온공": 0.038, "보통인부": 0.025 }, + { "보온재종류": "발포폴리에틸렌보온재", "규격(㎜)": "200", "보온공": 0.045, "보통인부": 0.030 }, + { "보온재종류": "발포폴리에틸렌보온재", "규격(㎜)": "250", "보온공": 0.052, "보통인부": 0.035 }, + { "보온재종류": "발포폴리에틸렌보온재", "규격(㎜)": "300", "보온공": 0.059, "보통인부": 0.039 }, + { "보온재종류": "유리면보온재(글라스울)", "규격(㎜)": "ø15", "보온공": 0.016, "보통인부": 0.011 }, + { "보온재종류": "유리면보온재(글라스울)", "규격(㎜)": "20", "보온공": 0.018, "보통인부": 0.012 }, + { "보온재종류": "유리면보온재(글라스울)", "규격(㎜)": "25", "보온공": 0.019, "보통인부": 0.013 }, + { "보온재종류": "유리면보온재(글라스울)", "규격(㎜)": "32", "보온공": 0.023, "보통인부": 0.015 }, + { "보온재종류": "유리면보온재(글라스울)", "규격(㎜)": "40", "보온공": 0.026, "보통인부": 0.017 }, + { "보온재종류": "유리면보온재(글라스울)", "규격(㎜)": "50", "보온공": 0.031, "보통인부": 0.020 }, + { "보온재종류": "유리면보온재(글라스울)", "규격(㎜)": "65", "보온공": 0.033, "보통인부": 0.022 }, + { "보온재종류": "유리면보온재(글라스울)", "규격(㎜)": "80", "보온공": 0.038, "보통인부": 0.025 }, + { "보온재종류": "유리면보온재(글라스울)", "규격(㎜)": "100", "보온공": 0.043, "보통인부": 0.028 }, + { "보온재종류": "유리면보온재(글라스울)", "규격(㎜)": "125", "보온공": 0.051, "보통인부": 0.034 }, + { "보온재종류": "유리면보온재(글라스울)", "규격(㎜)": "150", "보온공": 0.060, "보통인부": 0.040 }, + { "보온재종류": "유리면보온재(글라스울)", "규격(㎜)": "200", "보온공": 0.072, "보통인부": 0.048 }, + { "보온재종류": "유리면보온재(글라스울)", "규격(㎜)": "250", "보온공": 0.082, "보통인부": 0.055 }, + { "보온재종류": "유리면보온재(글라스울)", "규격(㎜)": "300", "보온공": 0.093, "보통인부": 0.062 } + ], + "주": [ + "① 본 품은 배관보온재(보온두께 50mm이하)를 재사용하지 아니하는 때의 절단하여 해체하는 기준이다.", + "② 비산방지, 보호 및 안전시설 등의 설치비는 별도 계상한다.", + "③ 폐기물 처리비용은 별도 계상한다." + ] + }, + { + "열쇠": "4-1-5 덕트보온 해체", + "이름": "덕트보온 해체", + "기준": "㎡당", + "출처": "건설품셈 유지관리 4-1-5", + "조건": { "보온재종류": "고르기" }, + "값칸": { "보온공": "인", "보통인부": "인" }, + "줄": [ + { "보온재종류": "고무발포보온재·발포폴리에틸렌보온재", "보온공": 0.081, "보통인부": 0.054 }, + { "보온재종류": "유리면보온재(글라스울)", "보온공": 0.096, "보통인부": 0.064 } + ], + "주": [ + "① 본 품은 재사용하지 아니하는 때의 보온재를 절단하여 해체하는 기준이다.", + "② 비산방지, 보호 및 안전시설 등의 설치비는 별도 계상한다.", + "③ 폐기물 처리비용은 별도 계상한다." + ] + }, + { + "열쇠": "4-1-6 펌프 해체", + "이름": "펌프 해체", + "기준": "대당", + "출처": "건설품셈 유지관리 4-1-6", + "조건": { "규격(kW)": "고르기" }, + "값칸": { "기계설비공": "인", "보통인부": "인" }, + "줄": [ + { "규격(kW)": "0.75 이하", "기계설비공": 0.245, "보통인부": 0.163 }, + { "규격(kW)": "1.5 이하", "기계설비공": 0.271, "보통인부": 0.181 }, + { "규격(kW)": "2.2 이하", "기계설비공": 0.312, "보통인부": 0.208 }, + { "규격(kW)": "3.7 이하", "기계설비공": 0.359, "보통인부": 0.239 }, + { "규격(kW)": "5.5 이하", "기계설비공": 0.432, "보통인부": 0.288 }, + { "규격(kW)": "7.5 이하", "기계설비공": 0.545, "보통인부": 0.363 }, + { "규격(kW)": "11 이하", "기계설비공": 0.685, "보통인부": 0.457 }, + { "규격(kW)": "15 이하", "기계설비공": 0.727, "보통인부": 0.485 }, + { "규격(kW)": "22 이하", "기계설비공": 1.175, "보통인부": 0.783 }, + { "규격(kW)": "37 이하", "기계설비공": 1.517, "보통인부": 1.011 }, + { "규격(kW)": "55 이하", "기계설비공": 2.440, "보통인부": 1.627 }, + { "규격(kW)": "75 이하", "기계설비공": 2.989, "보통인부": 1.993 } + ], + "주": [ + "① 본 품은 일반펌프(급수 및 소방펌프)를 재사용하지 아니하는 때의 절단하여 해체하는 기준이다.", + "② 본 품은 방진가대 해체, 펌프 절단 및 해체를 포함한다.", + "③ 비산방지, 보호 및 안전시설 등의 설치비는 별도 계상한다.", + "④ 폐기물 처리비용은 별도 계상한다.", + "⑤ 공구손료 및 경장비(절단기 등)의 기계경비는 인력품의 2%로 계상한다." + ] + }, + { + "열쇠": "4-1-7 일반기계설비 철거 및 이설", + "이름": "일반기계설비 철거 및 이설 요율", + "기준": "단위:%", + "출처": "건설품셈 유지관리 4-1-7", + "조건": { "구분": "고르기" }, + "값칸": { "철거(재사용고려)": "%", "철거(재사용안함)": "%", "동일구내(인접장소)이설": "%" }, + "줄": [ + { "구분": "1. 기기류", "철거(재사용고려)": 80, "철거(재사용안함)": 60, "동일구내(인접장소)이설": 160 }, + { "구분": "2. 철골류", "철거(재사용고려)": 70, "철거(재사용안함)": 50, "동일구내(인접장소)이설": 150 }, + { "구분": "3. 배관류", "철거(재사용고려)": 60, "철거(재사용안함)": 40, "동일구내(인접장소)이설": 140 }, + { "구분": "4. BELT CONVEYOR류", "철거(재사용고려)": 80, "철거(재사용안함)": 60, "동일구내(인접장소)이설": 160 }, + { "구분": "5. 보온재", "철거(재사용고려)": 60, "철거(재사용안함)": 40, "동일구내(인접장소)이설": 140 }, + { "구분": "6. HEATER & TANK류", "철거(재사용고려)": 70, "철거(재사용안함)": 50, "동일구내(인접장소)이설": 150 }, + { "구분": "7. PUMP & FAN류", "철거(재사용고려)": 60, "철거(재사용안함)": 40, "동일구내(인접장소)이설": 140 }, + { "구분": "8. CRANE류", "철거(재사용고려)": 70, "철거(재사용안함)": 50, "동일구내(인접장소)이설": 150 } + ], + "주": [ + "① '4-1-1 배관 해체~4-1-6 펌프 해체'의 각 항목을 우선 적용하며, 외의 항목은 상기류 유사폼목에 적용할 수 있다.(원문 그대로 「유사폼목」)", + "② 공구손료 및 소모재료는 별도 계상한다.", + "③ 상기의 율은 설치를 100%로 볼 때이다.", + "④ 특수기기에 대하여는 별도 계상한다.", + "⑤ 철거한 설비를 동일구내 또한 인접한 장소가 아닌 곳에 재 설치할 경우에는 설치품+철거품(재사용을 고려할 경우)으로 계상한다.", + "⑥ 다음 항목의 철거는 신설의 50%(재사용을 고려치 않을 경우)로 계상한다 — 적용항목 표를 참고한다." + ] + }, + { + "열쇠": "4-1-7 일반기계설비 철거 및 이설 적용항목", + "이름": "일반기계설비 철거 신설 50% 적용항목", + "기준": "정의", + "출처": "건설품셈 유지관리 4-1-7", + "조건": {}, + "값칸": { "항목": "글" }, + "줄": [ { "항목": "1-4-1 주철관 기계식 접합 및 배관 · 4-2-1 송풍기 설치 · 5-1-1 일반밸브 및 콕류 설치 · 5-1-2 감압밸브장치 설치 · 5-2-1 스팀트랩 장치 설치 · 5-3-1 익스팬션조인트 설치 · 5-3-2 플랙시블커넥터 설치 · 8-1-2 냉동기 설치 · 8-1-3 냉각탑 설치 · 8-2-1 공기가열기, 공기냉각기, 공기여과기 설치 · 8-2-3 공기조화기(Air Handling Unit) 설치 · 8-3-6 방열기 설치 · 10-1-1 옥내소화전함설치 · 10-1-2 소화용구 격납상자설치 · 10-3-1 지하식설치 · 10-3-2 지상식설치 · 10-4-1 일반송수구설치 · 10-4-2 방수구설치" } ], + "주": [ "⑥ 다음 항목의 철거는 신설의 50%(재사용을 고려치 않을 경우)로 계상한다." ] + }, + { + "열쇠": "4-2-1 자동제어설비 철거 및 이설", + "이름": "자동제어설비 철거 및 이설", + "기준": "적용내용", + "출처": "건설품셈 유지관리 4-2-1", + "조건": {}, + "값칸": { "항목": "글", "철거요율": "%", "이설요율": "%" }, + "줄": [ + { + "항목": "12-1-1 계기반 설치 · 12-1-2 플랜트계기 설치 · 12-2-2 계량기 설치 · 12-2-3 도압배관 · 12-2-4 Control Air 배관 · 12-2-5 압축공기 발생장치 및 공기관 배관", + "철거요율": 40, + "이설요율": 140 + } + ], + "주": [ + "적용내용 — 철거는 본 품의 40%(재사용)를 계상한다. 이설은 본 품의 140%를 계상한다." + ] + }, + { + "열쇠": "4-3-1 유량계 교체", + "이름": "유량계 교체", + "기준": "일당", + "출처": "건설품셈 유지관리 4-3-1", + "조건": { "규격(mm)": "고르기" }, + "값칸": { "배관공": "인", "보통인부": "인", "시공량(보호통)": "개", "시공량(유량계)": "개" }, + "줄": [ + { "규격(mm)": "ø13~15", "배관공": 1, "보통인부": 1, "시공량(보호통)": 6.0, "시공량(유량계)": 8.0 }, + { "규격(mm)": "ø20~32", "배관공": 1, "보통인부": 1, "시공량(보호통)": 5.0, "시공량(유량계)": 7.0 }, + { "규격(mm)": "ø40~50", "배관공": 1, "보통인부": 1, "시공량(보호통)": 4.0, "시공량(유량계)": 6.0 }, + { "규격(mm)": "ø65~80", "배관공": 1, "보통인부": 1, "시공량(유량계)": 2.0 }, + { "규격(mm)": "ø100~150", "배관공": 1, "보통인부": 1, "시공량(유량계)": 1.5 }, + { "규격(mm)": "ø200~300", "배관공": 1, "보통인부": 1, "시공량(유량계)": 1.0 } + ], + "주": [ + "비고 — 동일장소에서 수도미터, 온수미터를 병행 교체 시(해체 후 재부착)에는 유량계 교체 시공량에 30%를 감한다.", + "① 본 품은 수도미터(급수용), 온수미터(급탕용, 난방용)의 옥내배관 교체(해체 후 재부착) 기준이다.", + "② 보호통·뚜껑철거 및 재설치가 요구되는 경우에 보호통을 적용한다.", + "③ 본 품은 유량계 해체 및 재부착, 작동시험 및 마무리 작업을 포함한다.", + "④ 공구손료 및 경장비의 기계경비는 인력품의 1%로 계상한다." + ] + }, + { + "열쇠": "4-3-2 관갱생공", + "이름": "관갱생공", + "기준": "m당", + "출처": "건설품셈 유지관리 4-3-2", + "조건": { "규격(㎜)": "고르기" }, + "값칸": { "규사": "㎏", "에폭시도료": "㎏", "배관공": "인", "특별인부": "인", "장비사용시간": "시간" }, + "줄": [ + { "규격(㎜)": "ø15", "규사": 0.520, "에폭시도료": 0.060, "배관공": 0.072, "특별인부": 0.036, "장비사용시간": 0.053 }, + { "규격(㎜)": "20", "규사": 0.590, "에폭시도료": 0.107, "배관공": 0.072, "특별인부": 0.036, "장비사용시간": 0.053 }, + { "규격(㎜)": "25", "규사": 0.707, "에폭시도료": 0.127, "배관공": 0.072, "특별인부": 0.036, "장비사용시간": 0.053 }, + { "규격(㎜)": "32", "규사": 0.880, "에폭시도료": 0.173, "배관공": 0.072, "특별인부": 0.036, "장비사용시간": 0.053 }, + { "규격(㎜)": "40", "규사": 1.083, "에폭시도료": 0.203, "배관공": 0.072, "특별인부": 0.036, "장비사용시간": 0.053 }, + { "규격(㎜)": "50", "규사": 1.343, "에폭시도료": 0.260, "배관공": 0.072, "특별인부": 0.036, "장비사용시간": 0.053 }, + { "규격(㎜)": "65", "규사": 1.687, "에폭시도료": 0.330, "배관공": 0.081, "특별인부": 0.039, "장비사용시간": 0.064 }, + { "규격(㎜)": "80", "규사": 2.083, "에폭시도료": 0.387, "배관공": 0.081, "특별인부": 0.039, "장비사용시간": 0.064 }, + { "규격(㎜)": "100", "규사": 2.580, "에폭시도료": 0.513, "배관공": 0.081, "특별인부": 0.039, "장비사용시간": 0.064 }, + { "규격(㎜)": "125", "규사": 3.177, "에폭시도료": 0.647, "배관공": 0.101, "특별인부": 0.050, "장비사용시간": 0.080 }, + { "규격(㎜)": "150", "규사": 3.977, "에폭시도료": 0.777, "배관공": 0.101, "특별인부": 0.050, "장비사용시간": 0.080 }, + { "규격(㎜)": "200", "규사": 5.030, "에폭시도료": 1.027, "배관공": 0.101, "특별인부": 0.050, "장비사용시간": 0.080 }, + { "규격(㎜)": "250", "규사": 6.297, "에폭시도료": 1.277, "배관공": 0.111, "특별인부": 0.056, "장비사용시간": 0.089 }, + { "규격(㎜)": "300", "규사": 7.610, "에폭시도료": 1.650, "배관공": 0.111, "특별인부": 0.056, "장비사용시간": 0.089 } + ], + "주": [ + "① 본 품은 에어샌드공법을 기준한 것이다.", + "② 도장두께는 0.3∼1㎜일 때를 기준한 것이다.", + "③ 본 품에는 강관 갱생을 위한 관내부세척, 열풍건조, 관내부 피복코팅 및 소운반 품이 포함되어 있다.", + "④ 입상관의 경우는 본 품에 30%를 가산한다.", + "⑤ 검사구 설치, 밸브 및 보온 해체 복구, 가설급수 배관 및 해체에 대한 비용은 별도 계상한다.", + "⑥ 관세척 공사시 발생되는 폐기물을 폐기물관리법 등의 규정에 따라 적정하게 처리하는데 소요되는 비용은 별도 계상한다.", + "⑦ 사용장비중 공기압축기는 규격 25.5㎥/min를 기준한 것이며, 라이닝기(1set)에 대한 기계경비는 별도 계상한다.", + "⑧ 장비조합은 장비조합 표를 기준한다." + ] + }, + { + "열쇠": "4-3-2 관갱생공 장비조합", + "이름": "관갱생공 장비조합", + "기준": "정의", + "출처": "건설품셈 유지관리 4-3-2", + "조건": { "규격(㎜)범위": "고르기" }, + "값칸": { "라이닝기": "글", "공기압축기": "대" }, + "줄": [ + { "규격(㎜)범위": "ø15∼50", "라이닝기": "1set", "공기압축기": 1 }, + { "규격(㎜)범위": "ø65∼100", "라이닝기": "1set", "공기압축기": 2 }, + { "규격(㎜)범위": "ø125∼200", "라이닝기": "1set", "공기압축기": 5 }, + { "규격(㎜)범위": "ø250∼300", "라이닝기": "1set", "공기압축기": 6 } + ], + "주": [ "⑧ 장비조합은 다음을 기준한다." ] + }, + { + "열쇠": "4-3-3 배관누수 검사", + "이름": "배관누수 검사", + "기준": "일당", + "출처": "건설품셈 유지관리 4-3-3", + "조건": {}, + "값칸": { "배관공": "인", "시공량": "회" }, + "줄": [ { "배관공": 2, "시공량": 2.8 } ], + "주": [ + "① 본 품은 급수용, 급탕용, 난방용 옥내배관(∅50mm이하)의 누수보수를 위해 배관을 검사하는 기준이다.", + "② 본 품은 작업준비, 수도검침 및 기록, 미터기 해체 및 재설치, 공기압시험 및 누수탐지, 정리 작업을 포함한다.", + "③ 누수부위에 대한 해체 및 복구, 누수배관 교체 작업은 별도 계상한다.", + "④ 공구손료 및 경장비(공기압축기, 압력계 등)의 기계경비는 인력품의 3%로 계상한다." + ] + } + ] +} diff --git a/resources/master_data/소요량_건설품셈_토목9장.json b/resources/master_data/소요량_건설품셈_토목9장.json index 23685f1f..87c79ac1 100644 --- a/resources/master_data/소요량_건설품셈_토목9장.json +++ b/resources/master_data/소요량_건설품셈_토목9장.json @@ -17487,6 +17487,19908 @@ } ], "주": [] + }, + { + "열쇠": "9-5-1 디지털항공사진 GSD별 제원", + "이름": "디지털항공사진 지상표본거리(GSD)별 제원", + "기준": "초점거리 11.2㎝ 기준", + "출처": "건설품셈 토목 9-5-1", + "조건": { + "GSD(㎝)": "고르기" + }, + "값칸": { + "비행고도(m)": "m", + "1변실거리_종": "㎞", + "1변실거리_횡": "㎞", + "촬영면적": "㎢", + "촬영기선장": "㎞", + "코스간격": "㎞", + "스테레오면적": "㎢" + }, + "줄": [ + { + "GSD(㎝)": 8, + "비행고도(m)": 1600, + "1변실거리_종": 1.12, + "1변실거리_횡": 1.34, + "촬영면적": 1.5, + "촬영기선장": 0.45, + "코스간격": 0.94, + "스테레오면적": 0.42 + }, + { + "GSD(㎝)": 10, + "비행고도(m)": 2000, + "1변실거리_종": 1.4, + "1변실거리_횡": 1.68, + "촬영면적": 2.35, + "촬영기선장": 0.56, + "코스간격": 1.17, + "스테레오면적": 0.66 + }, + { + "GSD(㎝)": 12, + "비행고도(m)": 2400, + "1변실거리_종": 1.68, + "1변실거리_횡": 2.012, + "촬영면적": 3.38, + "촬영기선장": 0.67, + "코스간격": 1.41, + "스테레오면적": 0.95 + }, + { + "GSD(㎝)": 15, + "비행고도(m)": 3000, + "1변실거리_종": 2.1, + "1변실거리_횡": 2.52, + "촬영면적": 5.29, + "촬영기선장": 0.84, + "코스간격": 1.76, + "스테레오면적": 1.48 + }, + { + "GSD(㎝)": 20, + "비행고도(m)": 4000, + "1변실거리_종": 2.8, + "1변실거리_횡": 3.35, + "촬영면적": 9.4, + "촬영기선장": 1.12, + "코스간격": 2.35, + "스테레오면적": 2.63 + }, + { + "GSD(㎝)": 25, + "비행고도(m)": 5000, + "1변실거리_종": 3.5, + "1변실거리_횡": 4.19, + "촬영면적": 14.69, + "촬영기선장": 1.4, + "코스간격": 2.93, + "스테레오면적": 4.11 + }, + { + "GSD(㎝)": 42, + "비행고도(m)": 8400, + "1변실거리_종": 5.89, + "1변실거리_횡": 7.043, + "촬영면적": 41.46, + "촬영기선장": 2.36, + "코스간격": 4.93, + "스테레오면적": 11.61 + }, + { + "GSD(㎝)": 80, + "비행고도(m)": 16000, + "1변실거리_종": 11.21, + "1변실거리_횡": 13.41, + "촬영면적": 150.41, + "촬영기선장": 4.49, + "코스간격": 9.39, + "스테레오면적": 42.12 + } + ], + "주": [ + "비행고도 = 지상표본거리(GSD)×초점거리/CCD크기.", + "1변 실거리(종·횡) = 영상크기(종·횡)×지상표본거리(GSD).", + "촬영면적 = 1변 실거리(종)×1변 실거리(횡).", + "촬영기선장 = 1변 실거리(종)×(1-종중복도).", + "코스간격 = 1변 실거리(횡)×(1-횡중복도).", + "스테레오면적 = 촬영기선장×코스간격.", + "사진 중복도는 비행방향으로 60%, 스트립 사이 30%를 기준으로 한 것이다.", + "항공사진 촬영은 각 촬영 노선마다 양단에서의 여유는 각각 2매 이상으로 하고, 촬영축척이나 지형에 따라 조정하며 촬영구역 경계에 접한 촬영노선에서는 사진 폭의 약 30%를 여유 있게 촬영한다.", + "촬영기준면의 변화 또는 산악지대의 촬영에서 중복도를 변경할 경우에는 별도 계산한다.", + "항공사진축척 및 지상표본거리(GSD)는 최종도면의 축척, 최고비행고도, 등고선 간격, 도화기의 정밀도 및 사진의 사용목적에 따라 결정한다.", + "측량용 카메라의 초점거리는 1/100m단위까지 정밀측정 한다." + ] + }, + { + "열쇠": "9-5-1 적용예 카메라제원1", + "이름": "항공사진촬영 적용예(카메라 제원1: 영상크기 14,016×16,768pixel, CCD 5.6㎛, 초점거리 11.2㎝)", + "기준": "", + "출처": "건설품셈 토목 9-5-1 적용예", + "조건": { + "GSD(㎝)": "고르기" + }, + "값칸": { + "비행고도(m)": "m", + "1변실거리_종": "㎞", + "1변실거리_횡": "㎞", + "촬영면적": "㎢", + "촬영기선장": "㎞", + "코스간격": "㎞", + "스테레오면적": "㎢" + }, + "줄": [ + { + "GSD(㎝)": 8, + "비행고도(m)": 1600, + "1변실거리_종": 1.12, + "1변실거리_횡": 1.34, + "촬영면적": 1.5, + "촬영기선장": 0.45, + "코스간격": 0.94, + "스테레오면적": 0.42 + }, + { + "GSD(㎝)": 10, + "비행고도(m)": 2000, + "1변실거리_종": 1.4, + "1변실거리_횡": 1.68, + "촬영면적": 2.35, + "촬영기선장": 0.56, + "코스간격": 1.17, + "스테레오면적": 0.66 + }, + { + "GSD(㎝)": 12, + "비행고도(m)": 2400, + "1변실거리_종": 1.68, + "1변실거리_횡": 2.01, + "촬영면적": 3.38, + "촬영기선장": 0.67, + "코스간격": 1.41, + "스테레오면적": 0.95 + }, + { + "GSD(㎝)": 15, + "비행고도(m)": 3000, + "1변실거리_종": 2.1, + "1변실거리_횡": 2.52, + "촬영면적": 5.29, + "촬영기선장": 0.84, + "코스간격": 1.76, + "스테레오면적": 1.48 + }, + { + "GSD(㎝)": 20, + "비행고도(m)": 4000, + "1변실거리_종": 2.8, + "1변실거리_횡": 3.35, + "촬영면적": 9.4, + "촬영기선장": 1.12, + "코스간격": 2.35, + "스테레오면적": 2.63 + }, + { + "GSD(㎝)": 25, + "비행고도(m)": 5000, + "1변실거리_종": 3.5, + "1변실거리_횡": 4.19, + "촬영면적": 14.69, + "촬영기선장": 1.4, + "코스간격": 2.93, + "스테레오면적": 4.11 + }, + { + "GSD(㎝)": 42, + "비행고도(m)": 8400, + "1변실거리_종": 5.89, + "1변실거리_횡": 7.04, + "촬영면적": 41.46, + "촬영기선장": 2.35, + "코스간격": 4.93, + "스테레오면적": 11.61 + }, + { + "GSD(㎝)": 80, + "비행고도(m)": 16000, + "1변실거리_종": 11.21, + "1변실거리_횡": 13.41, + "촬영면적": 150.41, + "촬영기선장": 4.49, + "코스간격": 9.39, + "스테레오면적": 42.12 + } + ], + "주": [] + }, + { + "열쇠": "9-5-1 적용예 카메라제원2", + "이름": "항공사진촬영 적용예(카메라 제원2: 영상크기 14,790×23,010pixel, CCD 4.6㎛, 초점거리 12㎝)", + "기준": "", + "출처": "건설품셈 토목 9-5-1 적용예", + "조건": { + "GSD(㎝)": "고르기" + }, + "값칸": { + "비행고도(m)": "m", + "1변실거리_종": "㎞", + "1변실거리_횡": "㎞", + "촬영면적": "㎢", + "촬영기선장": "㎞", + "코스간격": "㎞", + "스테레오면적": "㎢" + }, + "줄": [ + { + "GSD(㎝)": 8, + "비행고도(m)": 2087, + "1변실거리_종": 1.18, + "1변실거리_횡": 1.84, + "촬영면적": 2.18, + "촬영기선장": 0.47, + "코스간격": 1.29, + "스테레오면적": 0.61 + }, + { + "GSD(㎝)": 10, + "비행고도(m)": 2609, + "1변실거리_종": 1.48, + "1변실거리_횡": 2.3, + "촬영면적": 3.4, + "촬영기선장": 0.59, + "코스간격": 1.61, + "스테레오면적": 0.95 + }, + { + "GSD(㎝)": 12, + "비행고도(m)": 3130, + "1변실거리_종": 1.77, + "1변실거리_횡": 2.76, + "촬영면적": 4.9, + "촬영기선장": 0.71, + "코스간격": 1.93, + "스테레오면적": 1.37 + }, + { + "GSD(㎝)": 15, + "비행고도(m)": 3913, + "1변실거리_종": 2.22, + "1변실거리_횡": 3.45, + "촬영면적": 7.66, + "촬영기선장": 0.89, + "코스간격": 2.42, + "스테레오면적": 2.14 + }, + { + "GSD(㎝)": 20, + "비행고도(m)": 5217, + "1변실거리_종": 2.96, + "1변실거리_횡": 4.6, + "촬영면적": 13.61, + "촬영기선장": 1.18, + "코스간격": 3.22, + "스테레오면적": 3.81 + }, + { + "GSD(㎝)": 25, + "비행고도(m)": 6522, + "1변실거리_종": 3.7, + "1변실거리_횡": 5.75, + "촬영면적": 21.27, + "촬영기선장": 1.48, + "코스간격": 4.03, + "스테레오면적": 5.96 + }, + { + "GSD(㎝)": 42, + "비행고도(m)": 10957, + "1변실거리_종": 6.21, + "1변실거리_횡": 9.66, + "촬영면적": 60.03, + "촬영기선장": 2.48, + "코스간격": 6.76, + "스테레오면적": 16.81 + }, + { + "GSD(㎝)": 80, + "비행고도(m)": 20870, + "1변실거리_종": 11.83, + "1변실거리_횡": 18.41, + "촬영면적": 217.8, + "촬영기선장": 4.73, + "코스간격": 12.89, + "스테레오면적": 60.98 + } + ], + "주": [] + }, + { + "열쇠": "9-5-1 월별 천후표", + "이름": "항공사진촬영 월별 천후표(쾌청일수)", + "기준": "", + "출처": "건설품셈 토목 9-5-1", + "조건": { + "지역별": "고르기", + "월": "고르기" + }, + "값칸": { + "쾌청일수": "일" + }, + "줄": [ + { + "지역별": "춘천", + "월": "1월", + "쾌청일수": "(7)" + }, + { + "지역별": "춘천", + "월": "2월", + "쾌청일수": "(5)" + }, + { + "지역별": "춘천", + "월": "3월", + "쾌청일수": 6 + }, + { + "지역별": "춘천", + "월": "4월", + "쾌청일수": 4 + }, + { + "지역별": "춘천", + "월": "5월", + "쾌청일수": 4 + }, + { + "지역별": "춘천", + "월": "6월", + "쾌청일수": 2 + }, + { + "지역별": "춘천", + "월": "7월", + "쾌청일수": 0 + }, + { + "지역별": "춘천", + "월": "8월", + "쾌청일수": 0 + }, + { + "지역별": "춘천", + "월": "9월", + "쾌청일수": 2 + }, + { + "지역별": "춘천", + "월": "10월", + "쾌청일수": 5 + }, + { + "지역별": "춘천", + "월": "11월", + "쾌청일수": 3 + }, + { + "지역별": "춘천", + "월": "12월", + "쾌청일수": "(7)" + }, + { + "지역별": "춘천", + "월": "계", + "쾌청일수": 45 + }, + { + "지역별": "강릉", + "월": "1월", + "쾌청일수": "(11)" + }, + { + "지역별": "강릉", + "월": "2월", + "쾌청일수": "(6)" + }, + { + "지역별": "강릉", + "월": "3월", + "쾌청일수": "(6)" + }, + { + "지역별": "강릉", + "월": "4월", + "쾌청일수": 4 + }, + { + "지역별": "강릉", + "월": "5월", + "쾌청일수": 3 + }, + { + "지역별": "강릉", + "월": "6월", + "쾌청일수": 2 + }, + { + "지역별": "강릉", + "월": "7월", + "쾌청일수": 0 + }, + { + "지역별": "강릉", + "월": "8월", + "쾌청일수": 1 + }, + { + "지역별": "강릉", + "월": "9월", + "쾌청일수": 1 + }, + { + "지역별": "강릉", + "월": "10월", + "쾌청일수": 5 + }, + { + "지역별": "강릉", + "월": "11월", + "쾌청일수": 6 + }, + { + "지역별": "강릉", + "월": "12월", + "쾌청일수": "(10)" + }, + { + "지역별": "강릉", + "월": "계", + "쾌청일수": 55 + }, + { + "지역별": "서울", + "월": "1월", + "쾌청일수": "(8)" + }, + { + "지역별": "서울", + "월": "2월", + "쾌청일수": "(6)" + }, + { + "지역별": "서울", + "월": "3월", + "쾌청일수": 6 + }, + { + "지역별": "서울", + "월": "4월", + "쾌청일수": 5 + }, + { + "지역별": "서울", + "월": "5월", + "쾌청일수": 6 + }, + { + "지역별": "서울", + "월": "6월", + "쾌청일수": 2 + }, + { + "지역별": "서울", + "월": "7월", + "쾌청일수": 0 + }, + { + "지역별": "서울", + "월": "8월", + "쾌청일수": 1 + }, + { + "지역별": "서울", + "월": "9월", + "쾌청일수": 4 + }, + { + "지역별": "서울", + "월": "10월", + "쾌청일수": 7 + }, + { + "지역별": "서울", + "월": "11월", + "쾌청일수": 4 + }, + { + "지역별": "서울", + "월": "12월", + "쾌청일수": "(6)" + }, + { + "지역별": "서울", + "월": "계", + "쾌청일수": 55 + }, + { + "지역별": "인천", + "월": "1월", + "쾌청일수": "(7)" + }, + { + "지역별": "인천", + "월": "2월", + "쾌청일수": "(6)" + }, + { + "지역별": "인천", + "월": "3월", + "쾌청일수": 7 + }, + { + "지역별": "인천", + "월": "4월", + "쾌청일수": 5 + }, + { + "지역별": "인천", + "월": "5월", + "쾌청일수": 5 + }, + { + "지역별": "인천", + "월": "6월", + "쾌청일수": 1 + }, + { + "지역별": "인천", + "월": "7월", + "쾌청일수": 0 + }, + { + "지역별": "인천", + "월": "8월", + "쾌청일수": 1 + }, + { + "지역별": "인천", + "월": "9월", + "쾌청일수": 3 + }, + { + "지역별": "인천", + "월": "10월", + "쾌청일수": 6 + }, + { + "지역별": "인천", + "월": "11월", + "쾌청일수": 5 + }, + { + "지역별": "인천", + "월": "12월", + "쾌청일수": "(6)" + }, + { + "지역별": "인천", + "월": "계", + "쾌청일수": 52 + }, + { + "지역별": "울릉도", + "월": "1월", + "쾌청일수": 0 + }, + { + "지역별": "울릉도", + "월": "2월", + "쾌청일수": 0 + }, + { + "지역별": "울릉도", + "월": "3월", + "쾌청일수": "(2)" + }, + { + "지역별": "울릉도", + "월": "4월", + "쾌청일수": 3 + }, + { + "지역별": "울릉도", + "월": "5월", + "쾌청일수": 3 + }, + { + "지역별": "울릉도", + "월": "6월", + "쾌청일수": 1 + }, + { + "지역별": "울릉도", + "월": "7월", + "쾌청일수": 1 + }, + { + "지역별": "울릉도", + "월": "8월", + "쾌청일수": 0 + }, + { + "지역별": "울릉도", + "월": "9월", + "쾌청일수": 0 + }, + { + "지역별": "울릉도", + "월": "10월", + "쾌청일수": 1 + }, + { + "지역별": "울릉도", + "월": "11월", + "쾌청일수": 0 + }, + { + "지역별": "울릉도", + "월": "12월", + "쾌청일수": 0 + }, + { + "지역별": "울릉도", + "월": "계", + "쾌청일수": 11 + }, + { + "지역별": "수원", + "월": "1월", + "쾌청일수": "(7)" + }, + { + "지역별": "수원", + "월": "2월", + "쾌청일수": "(5)" + }, + { + "지역별": "수원", + "월": "3월", + "쾌청일수": 6 + }, + { + "지역별": "수원", + "월": "4월", + "쾌청일수": 5 + }, + { + "지역별": "수원", + "월": "5월", + "쾌청일수": 5 + }, + { + "지역별": "수원", + "월": "6월", + "쾌청일수": 2 + }, + { + "지역별": "수원", + "월": "7월", + "쾌청일수": 0 + }, + { + "지역별": "수원", + "월": "8월", + "쾌청일수": 0 + }, + { + "지역별": "수원", + "월": "9월", + "쾌청일수": 4 + }, + { + "지역별": "수원", + "월": "10월", + "쾌청일수": 6 + }, + { + "지역별": "수원", + "월": "11월", + "쾌청일수": 4 + }, + { + "지역별": "수원", + "월": "12월", + "쾌청일수": "(6)" + }, + { + "지역별": "수원", + "월": "계", + "쾌청일수": 50 + }, + { + "지역별": "청주", + "월": "1월", + "쾌청일수": "(4)" + }, + { + "지역별": "청주", + "월": "2월", + "쾌청일수": "(4)" + }, + { + "지역별": "청주", + "월": "3월", + "쾌청일수": 6 + }, + { + "지역별": "청주", + "월": "4월", + "쾌청일수": 5 + }, + { + "지역별": "청주", + "월": "5월", + "쾌청일수": 5 + }, + { + "지역별": "청주", + "월": "6월", + "쾌청일수": 1 + }, + { + "지역별": "청주", + "월": "7월", + "쾌청일수": 0 + }, + { + "지역별": "청주", + "월": "8월", + "쾌청일수": 0 + }, + { + "지역별": "청주", + "월": "9월", + "쾌청일수": 2 + }, + { + "지역별": "청주", + "월": "10월", + "쾌청일수": 6 + }, + { + "지역별": "청주", + "월": "11월", + "쾌청일수": 4 + }, + { + "지역별": "청주", + "월": "12월", + "쾌청일수": "(3)" + }, + { + "지역별": "청주", + "월": "계", + "쾌청일수": 40 + }, + { + "지역별": "추풍령", + "월": "1월", + "쾌청일수": "(5)" + }, + { + "지역별": "추풍령", + "월": "2월", + "쾌청일수": "(3)" + }, + { + "지역별": "추풍령", + "월": "3월", + "쾌청일수": "(6)" + }, + { + "지역별": "추풍령", + "월": "4월", + "쾌청일수": 3 + }, + { + "지역별": "추풍령", + "월": "5월", + "쾌청일수": 5 + }, + { + "지역별": "추풍령", + "월": "6월", + "쾌청일수": 3 + }, + { + "지역별": "추풍령", + "월": "7월", + "쾌청일수": 0 + }, + { + "지역별": "추풍령", + "월": "8월", + "쾌청일수": 0 + }, + { + "지역별": "추풍령", + "월": "9월", + "쾌청일수": 1 + }, + { + "지역별": "추풍령", + "월": "10월", + "쾌청일수": 6 + }, + { + "지역별": "추풍령", + "월": "11월", + "쾌청일수": 6 + }, + { + "지역별": "추풍령", + "월": "12월", + "쾌청일수": "(4)" + }, + { + "지역별": "추풍령", + "월": "계", + "쾌청일수": 42 + }, + { + "지역별": "포항", + "월": "1월", + "쾌청일수": 11 + }, + { + "지역별": "포항", + "월": "2월", + "쾌청일수": 6 + }, + { + "지역별": "포항", + "월": "3월", + "쾌청일수": 7 + }, + { + "지역별": "포항", + "월": "4월", + "쾌청일수": 5 + }, + { + "지역별": "포항", + "월": "5월", + "쾌청일수": 5 + }, + { + "지역별": "포항", + "월": "6월", + "쾌청일수": 1 + }, + { + "지역별": "포항", + "월": "7월", + "쾌청일수": 1 + }, + { + "지역별": "포항", + "월": "8월", + "쾌청일수": 1 + }, + { + "지역별": "포항", + "월": "9월", + "쾌청일수": 1 + }, + { + "지역별": "포항", + "월": "10월", + "쾌청일수": 5 + }, + { + "지역별": "포항", + "월": "11월", + "쾌청일수": 7 + }, + { + "지역별": "포항", + "월": "12월", + "쾌청일수": 9 + }, + { + "지역별": "포항", + "월": "계", + "쾌청일수": 59 + }, + { + "지역별": "대구", + "월": "1월", + "쾌청일수": "(8)" + }, + { + "지역별": "대구", + "월": "2월", + "쾌청일수": 5 + }, + { + "지역별": "대구", + "월": "3월", + "쾌청일수": 7 + }, + { + "지역별": "대구", + "월": "4월", + "쾌청일수": 5 + }, + { + "지역별": "대구", + "월": "5월", + "쾌청일수": 5 + }, + { + "지역별": "대구", + "월": "6월", + "쾌청일수": 1 + }, + { + "지역별": "대구", + "월": "7월", + "쾌청일수": 0 + }, + { + "지역별": "대구", + "월": "8월", + "쾌청일수": 1 + }, + { + "지역별": "대구", + "월": "9월", + "쾌청일수": 1 + }, + { + "지역별": "대구", + "월": "10월", + "쾌청일수": 5 + }, + { + "지역별": "대구", + "월": "11월", + "쾌청일수": 6 + }, + { + "지역별": "대구", + "월": "12월", + "쾌청일수": 6 + }, + { + "지역별": "대구", + "월": "계", + "쾌청일수": 50 + }, + { + "지역별": "전주", + "월": "1월", + "쾌청일수": "(3)" + }, + { + "지역별": "전주", + "월": "2월", + "쾌청일수": 3 + }, + { + "지역별": "전주", + "월": "3월", + "쾌청일수": 6 + }, + { + "지역별": "전주", + "월": "4월", + "쾌청일수": 5 + }, + { + "지역별": "전주", + "월": "5월", + "쾌청일수": 5 + }, + { + "지역별": "전주", + "월": "6월", + "쾌청일수": 1 + }, + { + "지역별": "전주", + "월": "7월", + "쾌청일수": 0 + }, + { + "지역별": "전주", + "월": "8월", + "쾌청일수": 0 + }, + { + "지역별": "전주", + "월": "9월", + "쾌청일수": 2 + }, + { + "지역별": "전주", + "월": "10월", + "쾌청일수": 6 + }, + { + "지역별": "전주", + "월": "11월", + "쾌청일수": 3 + }, + { + "지역별": "전주", + "월": "12월", + "쾌청일수": "(3)" + }, + { + "지역별": "전주", + "월": "계", + "쾌청일수": 37 + }, + { + "지역별": "울산", + "월": "1월", + "쾌청일수": 10 + }, + { + "지역별": "울산", + "월": "2월", + "쾌청일수": 5 + }, + { + "지역별": "울산", + "월": "3월", + "쾌청일수": 7 + }, + { + "지역별": "울산", + "월": "4월", + "쾌청일수": 5 + }, + { + "지역별": "울산", + "월": "5월", + "쾌청일수": 5 + }, + { + "지역별": "울산", + "월": "6월", + "쾌청일수": 1 + }, + { + "지역별": "울산", + "월": "7월", + "쾌청일수": 1 + }, + { + "지역별": "울산", + "월": "8월", + "쾌청일수": 2 + }, + { + "지역별": "울산", + "월": "9월", + "쾌청일수": 1 + }, + { + "지역별": "울산", + "월": "10월", + "쾌청일수": 4 + }, + { + "지역별": "울산", + "월": "11월", + "쾌청일수": 6 + }, + { + "지역별": "울산", + "월": "12월", + "쾌청일수": 9 + }, + { + "지역별": "울산", + "월": "계", + "쾌청일수": 56 + }, + { + "지역별": "광주", + "월": "1월", + "쾌청일수": "(3)" + }, + { + "지역별": "광주", + "월": "2월", + "쾌청일수": 4 + }, + { + "지역별": "광주", + "월": "3월", + "쾌청일수": 5 + }, + { + "지역별": "광주", + "월": "4월", + "쾌청일수": 4 + }, + { + "지역별": "광주", + "월": "5월", + "쾌청일수": 4 + }, + { + "지역별": "광주", + "월": "6월", + "쾌청일수": 0 + }, + { + "지역별": "광주", + "월": "7월", + "쾌청일수": 0 + }, + { + "지역별": "광주", + "월": "8월", + "쾌청일수": 1 + }, + { + "지역별": "광주", + "월": "9월", + "쾌청일수": 2 + }, + { + "지역별": "광주", + "월": "10월", + "쾌청일수": 6 + }, + { + "지역별": "광주", + "월": "11월", + "쾌청일수": 3 + }, + { + "지역별": "광주", + "월": "12월", + "쾌청일수": "(2)" + }, + { + "지역별": "광주", + "월": "계", + "쾌청일수": 34 + }, + { + "지역별": "부산", + "월": "1월", + "쾌청일수": 12 + }, + { + "지역별": "부산", + "월": "2월", + "쾌청일수": 6 + }, + { + "지역별": "부산", + "월": "3월", + "쾌청일수": 7 + }, + { + "지역별": "부산", + "월": "4월", + "쾌청일수": 5 + }, + { + "지역별": "부산", + "월": "5월", + "쾌청일수": 5 + }, + { + "지역별": "부산", + "월": "6월", + "쾌청일수": 1 + }, + { + "지역별": "부산", + "월": "7월", + "쾌청일수": 0 + }, + { + "지역별": "부산", + "월": "8월", + "쾌청일수": 3 + }, + { + "지역별": "부산", + "월": "9월", + "쾌청일수": 2 + }, + { + "지역별": "부산", + "월": "10월", + "쾌청일수": 5 + }, + { + "지역별": "부산", + "월": "11월", + "쾌청일수": 7 + }, + { + "지역별": "부산", + "월": "12월", + "쾌청일수": 9 + }, + { + "지역별": "부산", + "월": "계", + "쾌청일수": 62 + }, + { + "지역별": "목포", + "월": "1월", + "쾌청일수": "(2)" + }, + { + "지역별": "목포", + "월": "2월", + "쾌청일수": "(2)" + }, + { + "지역별": "목포", + "월": "3월", + "쾌청일수": 5 + }, + { + "지역별": "목포", + "월": "4월", + "쾌청일수": 4 + }, + { + "지역별": "목포", + "월": "5월", + "쾌청일수": 4 + }, + { + "지역별": "목포", + "월": "6월", + "쾌청일수": 0 + }, + { + "지역별": "목포", + "월": "7월", + "쾌청일수": 0 + }, + { + "지역별": "목포", + "월": "8월", + "쾌청일수": 1 + }, + { + "지역별": "목포", + "월": "9월", + "쾌청일수": 3 + }, + { + "지역별": "목포", + "월": "10월", + "쾌청일수": 5 + }, + { + "지역별": "목포", + "월": "11월", + "쾌청일수": 2 + }, + { + "지역별": "목포", + "월": "12월", + "쾌청일수": "(2)" + }, + { + "지역별": "목포", + "월": "계", + "쾌청일수": 30 + }, + { + "지역별": "여수", + "월": "1월", + "쾌청일수": 6 + }, + { + "지역별": "여수", + "월": "2월", + "쾌청일수": 5 + }, + { + "지역별": "여수", + "월": "3월", + "쾌청일수": 7 + }, + { + "지역별": "여수", + "월": "4월", + "쾌청일수": 5 + }, + { + "지역별": "여수", + "월": "5월", + "쾌청일수": 4 + }, + { + "지역별": "여수", + "월": "6월", + "쾌청일수": 0 + }, + { + "지역별": "여수", + "월": "7월", + "쾌청일수": 0 + }, + { + "지역별": "여수", + "월": "8월", + "쾌청일수": 4 + }, + { + "지역별": "여수", + "월": "9월", + "쾌청일수": 2 + }, + { + "지역별": "여수", + "월": "10월", + "쾌청일수": 5 + }, + { + "지역별": "여수", + "월": "11월", + "쾌청일수": 6 + }, + { + "지역별": "여수", + "월": "12월", + "쾌청일수": 6 + }, + { + "지역별": "여수", + "월": "계", + "쾌청일수": 50 + }, + { + "지역별": "제주", + "월": "1월", + "쾌청일수": 0 + }, + { + "지역별": "제주", + "월": "2월", + "쾌청일수": 0 + }, + { + "지역별": "제주", + "월": "3월", + "쾌청일수": 3 + }, + { + "지역별": "제주", + "월": "4월", + "쾌청일수": 4 + }, + { + "지역별": "제주", + "월": "5월", + "쾌청일수": 4 + }, + { + "지역별": "제주", + "월": "6월", + "쾌청일수": 0 + }, + { + "지역별": "제주", + "월": "7월", + "쾌청일수": 0 + }, + { + "지역별": "제주", + "월": "8월", + "쾌청일수": 1 + }, + { + "지역별": "제주", + "월": "9월", + "쾌청일수": 0 + }, + { + "지역별": "제주", + "월": "10월", + "쾌청일수": 2 + }, + { + "지역별": "제주", + "월": "11월", + "쾌청일수": 1 + }, + { + "지역별": "제주", + "월": "12월", + "쾌청일수": 0 + }, + { + "지역별": "제주", + "월": "계", + "쾌청일수": 15 + }, + { + "지역별": "서귀포", + "월": "1월", + "쾌청일수": "(1)" + }, + { + "지역별": "서귀포", + "월": "2월", + "쾌청일수": 0 + }, + { + "지역별": "서귀포", + "월": "3월", + "쾌청일수": 3 + }, + { + "지역별": "서귀포", + "월": "4월", + "쾌청일수": 5 + }, + { + "지역별": "서귀포", + "월": "5월", + "쾌청일수": 3 + }, + { + "지역별": "서귀포", + "월": "6월", + "쾌청일수": 0 + }, + { + "지역별": "서귀포", + "월": "7월", + "쾌청일수": 0 + }, + { + "지역별": "서귀포", + "월": "8월", + "쾌청일수": 0 + }, + { + "지역별": "서귀포", + "월": "9월", + "쾌청일수": 2 + }, + { + "지역별": "서귀포", + "월": "10월", + "쾌청일수": 4 + }, + { + "지역별": "서귀포", + "월": "11월", + "쾌청일수": 1 + }, + { + "지역별": "서귀포", + "월": "12월", + "쾌청일수": 0 + }, + { + "지역별": "서귀포", + "월": "계", + "쾌청일수": 19 + }, + { + "지역별": "속초", + "월": "1월", + "쾌청일수": "(11)" + }, + { + "지역별": "속초", + "월": "2월", + "쾌청일수": "(6)" + }, + { + "지역별": "속초", + "월": "3월", + "쾌청일수": "(6)" + }, + { + "지역별": "속초", + "월": "4월", + "쾌청일수": 4 + }, + { + "지역별": "속초", + "월": "5월", + "쾌청일수": 4 + }, + { + "지역별": "속초", + "월": "6월", + "쾌청일수": 2 + }, + { + "지역별": "속초", + "월": "7월", + "쾌청일수": 0 + }, + { + "지역별": "속초", + "월": "8월", + "쾌청일수": 0 + }, + { + "지역별": "속초", + "월": "9월", + "쾌청일수": 2 + }, + { + "지역별": "속초", + "월": "10월", + "쾌청일수": 6 + }, + { + "지역별": "속초", + "월": "11월", + "쾌청일수": 6 + }, + { + "지역별": "속초", + "월": "12월", + "쾌청일수": "(10)" + }, + { + "지역별": "속초", + "월": "계", + "쾌청일수": 57 + }, + { + "지역별": "철원", + "월": "1월", + "쾌청일수": "(10)" + }, + { + "지역별": "철원", + "월": "2월", + "쾌청일수": "(4)" + }, + { + "지역별": "철원", + "월": "3월", + "쾌청일수": 6 + }, + { + "지역별": "철원", + "월": "4월", + "쾌청일수": 4 + }, + { + "지역별": "철원", + "월": "5월", + "쾌청일수": 4 + }, + { + "지역별": "철원", + "월": "6월", + "쾌청일수": 2 + }, + { + "지역별": "철원", + "월": "7월", + "쾌청일수": 0 + }, + { + "지역별": "철원", + "월": "8월", + "쾌청일수": 0 + }, + { + "지역별": "철원", + "월": "9월", + "쾌청일수": 4 + }, + { + "지역별": "철원", + "월": "10월", + "쾌청일수": 6 + }, + { + "지역별": "철원", + "월": "11월", + "쾌청일수": 5 + }, + { + "지역별": "철원", + "월": "12월", + "쾌청일수": "(8)" + }, + { + "지역별": "철원", + "월": "계", + "쾌청일수": 53 + }, + { + "지역별": "원주", + "월": "1월", + "쾌청일수": "(9)" + }, + { + "지역별": "원주", + "월": "2월", + "쾌청일수": "(4)" + }, + { + "지역별": "원주", + "월": "3월", + "쾌청일수": 5 + }, + { + "지역별": "원주", + "월": "4월", + "쾌청일수": 4 + }, + { + "지역별": "원주", + "월": "5월", + "쾌청일수": 5 + }, + { + "지역별": "원주", + "월": "6월", + "쾌청일수": 1 + }, + { + "지역별": "원주", + "월": "7월", + "쾌청일수": 0 + }, + { + "지역별": "원주", + "월": "8월", + "쾌청일수": 0 + }, + { + "지역별": "원주", + "월": "9월", + "쾌청일수": 3 + }, + { + "지역별": "원주", + "월": "10월", + "쾌청일수": 6 + }, + { + "지역별": "원주", + "월": "11월", + "쾌청일수": 4 + }, + { + "지역별": "원주", + "월": "12월", + "쾌청일수": "(7)" + }, + { + "지역별": "원주", + "월": "계", + "쾌청일수": 48 + }, + { + "지역별": "서산", + "월": "1월", + "쾌청일수": "(3)" + }, + { + "지역별": "서산", + "월": "2월", + "쾌청일수": "(3)" + }, + { + "지역별": "서산", + "월": "3월", + "쾌청일수": "(5)" + }, + { + "지역별": "서산", + "월": "4월", + "쾌청일수": 5 + }, + { + "지역별": "서산", + "월": "5월", + "쾌청일수": 4 + }, + { + "지역별": "서산", + "월": "6월", + "쾌청일수": 2 + }, + { + "지역별": "서산", + "월": "7월", + "쾌청일수": 0 + }, + { + "지역별": "서산", + "월": "8월", + "쾌청일수": 0 + }, + { + "지역별": "서산", + "월": "9월", + "쾌청일수": 4 + }, + { + "지역별": "서산", + "월": "10월", + "쾌청일수": 6 + }, + { + "지역별": "서산", + "월": "11월", + "쾌청일수": 2 + }, + { + "지역별": "서산", + "월": "12월", + "쾌청일수": "(3)" + }, + { + "지역별": "서산", + "월": "계", + "쾌청일수": 37 + }, + { + "지역별": "울진", + "월": "1월", + "쾌청일수": "(10)" + }, + { + "지역별": "울진", + "월": "2월", + "쾌청일수": "(5)" + }, + { + "지역별": "울진", + "월": "3월", + "쾌청일수": 6 + }, + { + "지역별": "울진", + "월": "4월", + "쾌청일수": 5 + }, + { + "지역별": "울진", + "월": "5월", + "쾌청일수": 4 + }, + { + "지역별": "울진", + "월": "6월", + "쾌청일수": 1 + }, + { + "지역별": "울진", + "월": "7월", + "쾌청일수": 0 + }, + { + "지역별": "울진", + "월": "8월", + "쾌청일수": 1 + }, + { + "지역별": "울진", + "월": "9월", + "쾌청일수": 2 + }, + { + "지역별": "울진", + "월": "10월", + "쾌청일수": 5 + }, + { + "지역별": "울진", + "월": "11월", + "쾌청일수": 7 + }, + { + "지역별": "울진", + "월": "12월", + "쾌청일수": 9 + }, + { + "지역별": "울진", + "월": "계", + "쾌청일수": 55 + }, + { + "지역별": "대전", + "월": "1월", + "쾌청일수": "(4)" + }, + { + "지역별": "대전", + "월": "2월", + "쾌청일수": "(4)" + }, + { + "지역별": "대전", + "월": "3월", + "쾌청일수": 6 + }, + { + "지역별": "대전", + "월": "4월", + "쾌청일수": 5 + }, + { + "지역별": "대전", + "월": "5월", + "쾌청일수": 5 + }, + { + "지역별": "대전", + "월": "6월", + "쾌청일수": 1 + }, + { + "지역별": "대전", + "월": "7월", + "쾌청일수": 0 + }, + { + "지역별": "대전", + "월": "8월", + "쾌청일수": 0 + }, + { + "지역별": "대전", + "월": "9월", + "쾌청일수": 3 + }, + { + "지역별": "대전", + "월": "10월", + "쾌청일수": 6 + }, + { + "지역별": "대전", + "월": "11월", + "쾌청일수": 3 + }, + { + "지역별": "대전", + "월": "12월", + "쾌청일수": "(3)" + }, + { + "지역별": "대전", + "월": "계", + "쾌청일수": 40 + }, + { + "지역별": "안동", + "월": "1월", + "쾌청일수": "(9)" + }, + { + "지역별": "안동", + "월": "2월", + "쾌청일수": "(6)" + }, + { + "지역별": "안동", + "월": "3월", + "쾌청일수": 7 + }, + { + "지역별": "안동", + "월": "4월", + "쾌청일수": 5 + }, + { + "지역별": "안동", + "월": "5월", + "쾌청일수": 5 + }, + { + "지역별": "안동", + "월": "6월", + "쾌청일수": 1 + }, + { + "지역별": "안동", + "월": "7월", + "쾌청일수": 0 + }, + { + "지역별": "안동", + "월": "8월", + "쾌청일수": 1 + }, + { + "지역별": "안동", + "월": "9월", + "쾌청일수": 1 + }, + { + "지역별": "안동", + "월": "10월", + "쾌청일수": 3 + }, + { + "지역별": "안동", + "월": "11월", + "쾌청일수": 5 + }, + { + "지역별": "안동", + "월": "12월", + "쾌청일수": "(7)" + }, + { + "지역별": "안동", + "월": "계", + "쾌청일수": 50 + }, + { + "지역별": "군산", + "월": "1월", + "쾌청일수": "(5)" + }, + { + "지역별": "군산", + "월": "2월", + "쾌청일수": 2 + }, + { + "지역별": "군산", + "월": "3월", + "쾌청일수": 5 + }, + { + "지역별": "군산", + "월": "4월", + "쾌청일수": 4 + }, + { + "지역별": "군산", + "월": "5월", + "쾌청일수": 6 + }, + { + "지역별": "군산", + "월": "6월", + "쾌청일수": 1 + }, + { + "지역별": "군산", + "월": "7월", + "쾌청일수": 2 + }, + { + "지역별": "군산", + "월": "8월", + "쾌청일수": 0 + }, + { + "지역별": "군산", + "월": "9월", + "쾌청일수": 4 + }, + { + "지역별": "군산", + "월": "10월", + "쾌청일수": 6 + }, + { + "지역별": "군산", + "월": "11월", + "쾌청일수": 2 + }, + { + "지역별": "군산", + "월": "12월", + "쾌청일수": "(2)" + }, + { + "지역별": "군산", + "월": "계", + "쾌청일수": 39 + }, + { + "지역별": "통영", + "월": "1월", + "쾌청일수": 12 + }, + { + "지역별": "통영", + "월": "2월", + "쾌청일수": 5 + }, + { + "지역별": "통영", + "월": "3월", + "쾌청일수": 7 + }, + { + "지역별": "통영", + "월": "4월", + "쾌청일수": 6 + }, + { + "지역별": "통영", + "월": "5월", + "쾌청일수": 3 + }, + { + "지역별": "통영", + "월": "6월", + "쾌청일수": 0 + }, + { + "지역별": "통영", + "월": "7월", + "쾌청일수": 0 + }, + { + "지역별": "통영", + "월": "8월", + "쾌청일수": 1 + }, + { + "지역별": "통영", + "월": "9월", + "쾌청일수": 3 + }, + { + "지역별": "통영", + "월": "10월", + "쾌청일수": 6 + }, + { + "지역별": "통영", + "월": "11월", + "쾌청일수": 7 + }, + { + "지역별": "통영", + "월": "12월", + "쾌청일수": 9 + }, + { + "지역별": "통영", + "월": "계", + "쾌청일수": 59 + }, + { + "지역별": "완도", + "월": "1월", + "쾌청일수": "(4)" + }, + { + "지역별": "완도", + "월": "2월", + "쾌청일수": 3 + }, + { + "지역별": "완도", + "월": "3월", + "쾌청일수": 5 + }, + { + "지역별": "완도", + "월": "4월", + "쾌청일수": 5 + }, + { + "지역별": "완도", + "월": "5월", + "쾌청일수": 5 + }, + { + "지역별": "완도", + "월": "6월", + "쾌청일수": 1 + }, + { + "지역별": "완도", + "월": "7월", + "쾌청일수": 2 + }, + { + "지역별": "완도", + "월": "8월", + "쾌청일수": 1 + }, + { + "지역별": "완도", + "월": "9월", + "쾌청일수": 3 + }, + { + "지역별": "완도", + "월": "10월", + "쾌청일수": 6 + }, + { + "지역별": "완도", + "월": "11월", + "쾌청일수": 3 + }, + { + "지역별": "완도", + "월": "12월", + "쾌청일수": "(3)" + }, + { + "지역별": "완도", + "월": "계", + "쾌청일수": 41 + }, + { + "지역별": "진주", + "월": "1월", + "쾌청일수": 8 + }, + { + "지역별": "진주", + "월": "2월", + "쾌청일수": 4 + }, + { + "지역별": "진주", + "월": "3월", + "쾌청일수": 5 + }, + { + "지역별": "진주", + "월": "4월", + "쾌청일수": 3 + }, + { + "지역별": "진주", + "월": "5월", + "쾌청일수": 3 + }, + { + "지역별": "진주", + "월": "6월", + "쾌청일수": 0 + }, + { + "지역별": "진주", + "월": "7월", + "쾌청일수": 0 + }, + { + "지역별": "진주", + "월": "8월", + "쾌청일수": 1 + }, + { + "지역별": "진주", + "월": "9월", + "쾌청일수": 1 + }, + { + "지역별": "진주", + "월": "10월", + "쾌청일수": 4 + }, + { + "지역별": "진주", + "월": "11월", + "쾌청일수": 4 + }, + { + "지역별": "진주", + "월": "12월", + "쾌청일수": 5 + }, + { + "지역별": "진주", + "월": "계", + "쾌청일수": 38 + } + ], + "주": [ + "( )는 월간 3일 이상 적설이 있는 달의 쾌청일수를 말한다.", + "이 표의 숫자는 쾌청일수를 말하며 단지 구름의 양이 1.0(구름양 10%)이하를 기준한 기상 통계이므로 사진촬영에 크게 영향을 끼치는 겨울철의 적설, 도심지역의 연무 현상 및 산악지대의 태양각 등의 특수 지상조건을 고려하여 증감할 수 있다.", + "사진축척에 따른 실제 비행고도 및 비행기의 종류를 고려하여 증감할 수 있다.", + "이 표의 쾌청일수는 1일 8회의 관측치를 평균한 2008년∼2018년의 기상청 통계이며, 운항체류일수의 계산에 활용한다.", + "이 표에 명시되지 않은 지역은 가장 가까운 지역의 자료를 활용할 수 있다.", + "여러 개월에 걸쳐 항공촬영을 행하는 경우 해당 개월의 쾌청일수 산술평균을 적용한다." + ] + }, + { + "열쇠": "9-5-1 운항속도", + "이름": "항공사진촬영 운항속도", + "기준": "", + "출처": "건설품셈 토목 9-5-1", + "조건": { + "GSD구분": "고르기" + }, + "값칸": { + "기지이동운항속도": "㎞/hr", + "운항속도": "㎞/hr", + "비고": "" + }, + "줄": [ + { + "GSD구분": "GSD ≤ 65cm", + "기지이동운항속도": 240, + "운항속도": 200, + "비고": "FMC사용" + }, + { + "GSD구분": "GSD > 65cm", + "기지이동운항속도": 240, + "운항속도": 220, + "비고": "FMC사용" + } + ], + "주": [ + "본 제원은 항공사진촬영이 가능한 경비행기를 기준한 것이다." + ] + }, + { + "열쇠": "9-5-1 예비운항시간", + "이름": "항공사진촬영 예비운항시간", + "기준": "", + "출처": "건설품셈 토목 9-5-1", + "조건": { + "항목": "고르기" + }, + "값칸": { + "시간(분)": "분" + }, + "줄": [ + { + "항목": "시운전", + "시간(분)": 25 + }, + { + "항목": "편류측정", + "시간(분)": 15 + }, + { + "항목": "코스진입", + "시간(분)": 5 + }, + { + "항목": "이착륙", + "시간(분)": 20 + } + ], + "주": [ + "편류측정 횟수는 총 코스 연장 100km마다 1회로 하며, 노선측량의 촬영에서는 별도 가산할 수 있다.", + "본 제원은 항공사진촬영이 가능한 경비행기를 기준한 것이다.", + "항공기의 종류, 최대운항속도 및 기상조건에 따라 조정 적용할 수 있다.", + "코스진입은 매 코스당 1회, 시운전 및 이착륙은 운항 1일당 1회로 한다." + ] + }, + { + "열쇠": "9-5-1 촬영기준 계산식 상수 — 그룹 계수", + "이름": "항공사진촬영기준 계산식의 비율·기준 상수", + "기준": "", + "출처": "건설품셈 토목 9-5-1", + "조건": { + "항목": "고르기" + }, + "값칸": { + "값": "" + }, + "줄": [ + { + "항목": "천후장애시간(왕복운항시간 대비)", + "값": "200%" + }, + { + "항목": "보완촬영시간(촬영운항시간 대비)", + "값": "50%" + }, + { + "항목": "순촬영소요일수 산정 기준시간", + "값": "5시간" + } + ], + "주": [] + }, + { + "열쇠": "9-5-1 항공사진촬영(작업일수·인원)", + "이름": "항공사진촬영 작업구분별 작업일수 및 인원", + "기준": "촬영거리 200km", + "출처": "건설품셈 토목 9-5-1", + "조건": { + "작업구분": "고르기", + "GSD구간": "고르기", + "직종": "고르기" + }, + "값칸": { + "작업일수": "일", + "인원": "" + }, + "줄": [ + { + "작업구분": "계획준비", + "GSD구간": "GSD≤25cm", + "작업일수": 1 + }, + { + "작업구분": "계획준비", + "GSD구간": "25cm TestClient: + for path in REAL.glob("*.json"): + if not path.name.startswith("_"): + shutil.copy(path, tmp_path / path.name) + monkeypatch.setattr(store, "FOLDER", tmp_path) + app = FastAPI() + app.include_router(router_module.router) + return TestClient(app) + + +def _get(client: TestClient, url: str, **params) -> dict: + res = client.get(url, params=params) + assert res.status_code == 200, res.text + return res.json() + + +def _calc(client: TestClient, **inputs) -> dict: + res = client.post( + "/api/m01/calc", json={"book": LOGIC_BOOK, "key": LOGIC_KEY, "inputs": inputs} + ) + assert res.status_code == 200, res.text + return res.json() + + +def _labor_row(client: TestClient, key: str) -> tuple[dict, str]: + got = _get(client, "/api/m01/rows", file=LABOR, q="", size=500) + return next(r for r in got["rows"] if r["열쇠"] == key), got["version"] + + +def test_그룹_파일_줄_쪽나눔_찾기(client: TestClient) -> None: + groups = {g["group"]: g for g in _get(client, "/api/m01/groups")["groups"]} + assert list(groups) == ["인력", "재료", "기계", "소요량", "계수", "환율", "요율", "로직"] + files = _get(client, "/api/m01/groups/인력/files")["files"] + assert LABOR in [f["file"] for f in files] + assert groups["인력"]["files"] == len(files) + page2 = _get(client, "/api/m01/rows", file=LABOR, page=2, size=10) + assert page2["page"] == 2 and len(page2["rows"]) == 10 and page2["total"] > 20 + hit = _get(client, "/api/m01/rows", file=LABOR, q="보통인부") + assert [r["열쇠"] for r in hit["rows"]] == ["1002"] + assert client.get("/api/m01/rows", params={"file": "없음.json"}).status_code == 404 + assert client.get("/api/m01/rows", params={"file": "../main.py"}).status_code == 404 + + +def test_표형_요소_표_목록과_표_하나(client: TestClient) -> None: + file = "소요량_건설품셈_공통3장.json" + listed = _get(client, "/api/m01/tables", file=file, q="암발파") + one = next(t for t in listed["tables"] if t["열쇠"] == LOGIC_KEY) + assert one["count"] >= 1 and "줄" not in one + table = _get(client, "/api/m01/table", file=file, key=LOGIC_KEY)["table"] + assert len(table["줄"]) == one["count"] and table["값칸"] == one["값칸"] + assert client.get("/api/m01/tables", params={"file": LABOR}).status_code == 400 + + +def test_로직_목록_하나_시험계산(client: TestClient) -> None: + listed = _get(client, "/api/m01/logics", book=LOGIC_BOOK, chapter="공통3장", q="암발파")[ + "logics" + ] + assert LOGIC_KEY in [x["열쇠"] for x in listed] + assert all(x["chapter"] == "공통3장" and not x["blocked"] for x in listed) + assert _get(client, "/api/m01/logics", blocked=1)["logics"] == [] + one = _get(client, "/api/m01/logic", book=LOGIC_BOOK, key=LOGIC_KEY) + assert one["file"] == "로직_건설품셈_공통3장.json" and one["blocked"] is False + got = _calc(client) + assert got["ok"] is True + lines = {x["이름"]: x for x in got["lines"]} + assert Decimal(str(lines["화약취급공"]["금액"])) == Decimal( + str(lines["화약취급공"]["수량"]) + ) * Decimal(str(lines["화약취급공"]["단가"])) + assert got["sums"]["계"] == pytest.approx(sum(x["금액"] for x in got["lines"])) + stop = _calc(client, 없는입력=1) + assert stop["ok"] is False and "모르는 입력" in stop["reason"] + + +def test_저장_고침은_파일에_쓰고_낡은_판본은_409(client: TestClient) -> None: + row, version = _labor_row(client, "1016") + before = _calc(client)["sums"]["계"] + edited = {**row, "값": row["값"] + 1000} + body = { + "files": [ + { + "file": LABOR, + "version": version, + "changes": [{"op": "edit", "key": "1016", "row": edited}], + } + ] + } + res = client.post("/api/m01/save", json=body) + assert res.status_code == 200, res.text + new_version = res.json()["files"][0]["version"] + assert new_version != version + assert _labor_row(client, "1016") == (edited, new_version) + assert _calc(client)["sums"]["계"] == pytest.approx(before + 1000 * 0.04) # 화약취급공 0.040 인 + data = json.loads((store.FOLDER / LABOR).read_text(encoding="utf-8")) + assert ( + data["그룹"] == "인력" + and len(data["줄"]) == _get(client, "/api/m01/rows", file=LABOR)["total"] + ) + again = client.post("/api/m01/save", json=body) # 옛 판본 그대로 + assert again.status_code == 409 and again.json()["detail"]["stale"] == [LABOR] + + +def test_저장_검사에_걸리면_아무것도_안_씀(client: TestClient) -> None: + _, version = _labor_row(client, "1016") + raw = (store.FOLDER / LABOR).read_bytes() + gone = {"file": LABOR, "version": version, "changes": [{"op": "delete", "key": "1016"}]} + res = client.post("/api/m01/save", json={"files": [gone]}) + assert res.status_code == 422 and any("1016" in x for x in res.json()["detail"]["errors"]) + bad_form = {**_labor_row(client, "1002")[0], "열쇠": "9999"} + del bad_form["출처"] + res = client.post( + "/api/m01/save", json={"files": [{**gone, "changes": [{"op": "add", "row": bad_form}]}]} + ) + assert res.status_code == 422 and any("출처" in x for x in res.json()["detail"]["errors"]) + dup = {**gone, "changes": [{"op": "add", "row": _labor_row(client, "1002")[0]}]} + assert client.post("/api/m01/save", json={"files": [dup]}).status_code == 400 + assert (store.FOLDER / LABOR).read_bytes() == raw + + +def test_더함_지움_로직_파일도_같은_길(client: TestClient) -> None: + one = _get(client, "/api/m01/logic", book=LOGIC_BOOK, key=LOGIC_KEY) + copy = {**one["logic"], "열쇠": LOGIC_KEY + " 사본"} + add = {"file": one["file"], "version": one["version"], "changes": [{"op": "add", "row": copy}]} + res = client.post("/api/m01/save", json={"files": [add]}) + assert res.status_code == 200, res.text + assert _get(client, "/api/m01/logic", book=LOGIC_BOOK, key=copy["열쇠"])["blocked"] is False + drop = { + "file": one["file"], + "version": res.json()["files"][0]["version"], + "changes": [{"op": "delete", "key": copy["열쇠"]}], + } + assert client.post("/api/m01/save", json={"files": [drop]}).status_code == 200 + assert ( + client.get("/api/m01/logic", params={"book": LOGIC_BOOK, "key": copy["열쇠"]}).status_code + == 404 + ) + + +def test_단가_자동_요소_찾기_저장전_시험계산(client: TestClient) -> None: + one = _get(client, "/api/m01/logic", book=LOGIC_BOOK, key=LOGIC_KEY) + labor = one["prices"]["인력:건설노임:1016"] + assert labor["이름"] == "화약취급공" and labor["값"] > 0 + found = _get(client, "/api/m01/elements", group="인력", q="보통인부") + assert "인력:건설노임:1002" in [x["ref"] for x in found["items"]] + tables = _get(client, "/api/m01/elements", group="소요량", q="암발파", limit=5) + assert tables["items"] and all(x["값칸"] for x in tables["items"]) + raw = (store.FOLDER / one["file"]).read_bytes() + before = _calc(client)["sums"]["계"] + half = {**one["logic"], "호표": one["logic"]["호표"][:1]} # 보통인부 줄 뺌 + res = client.post( + "/api/m01/calc", json={"book": LOGIC_BOOK, "key": LOGIC_KEY, "inputs": {}, "row": half} + ) + lines = res.json()["lines"] + assert [x["이름"] for x in lines] == ["화약취급공"] and res.json()["sums"]["계"] < before + new = {**half, "열쇠": "새 로직"} + body = {"book": LOGIC_BOOK, "key": "새 로직", "inputs": {}, "row": new, "file": one["file"]} + assert client.post("/api/m01/calc", json=body).json()["ok"] is True + assert (store.FOLDER / one["file"]).read_bytes() == raw # 시험 계산은 안 씀 + + +def test_쓰기_모양은_읽은_값과_같음() -> None: + for name in ("소요량_건설품셈_기계설비13장.json", "로직_건설품셈_토목2장.json", LABOR): + data = json.loads((REAL / name).read_text(encoding="utf-8"), parse_float=Decimal) + text = store.dump(data) + assert json.loads(text, parse_float=Decimal) == data + assert "\r" not in text and text.endswith("}\n") + + +def test_main_은_시스템관리자만() -> None: + main = (Path(__file__).resolve().parents[2] / "main.py").read_text(encoding="utf-8") + assert "app.include_router(m01_master_data_router, dependencies=system_admin_only)" in main diff --git a/resources/tester/test_masterdata_check.py b/resources/tester/test_masterdata_check.py index 04108a5b..a0a1b473 100644 --- a/resources/tester/test_masterdata_check.py +++ b/resources/tester/test_masterdata_check.py @@ -19,6 +19,8 @@ SAMPLES = [ "로직_건설품셈_공통4장.json", "로직_건설품셈_공통7장.json", "로직_건설품셈_공통8장.json", + "로직_건설품셈_토목1장.json", + "로직_건설품셈_토목2장.json", "로직_산림품셈_03장.json", "로직_산림품셈_04장.json", "로직_산림품셈_05장.json", @@ -241,6 +243,36 @@ def test_건설_흙깎기_암_치즐(whole): assert got["줄"][-1]["금액"] == brk["금액"] / 100 +def test_토목_동상방지층_인력_살수(whole): + """토목 1-2-1 — 165㎥/일 · 인력 살수면 살수차 0 · 보통인부 100㎡당 1인 = 두께 20㎝ 에서 1/20 인/㎥.""" + given = {"살수": "인력 살수", "두께": 20, "지역": "전국평균", "보정작업": "해당 없음"} + got = mf.run(whole, "건설품셈:1-2-1 인력식 소규모장비 포설", given) + qty = {line["이름"]: line["수량"] for line in got["줄"]} + assert qty["굴착기 0.6㎥"] == Decimal(8) / 165 + assert qty["살수차 5,500ℓ"] == 0 + assert got["줄"][-1]["수량"] == Decimal(1) / 20 + + +def test_토목_보도블록_감(whole): + """토목 1-7-1 — B-Type 190㎡/일 × 0.9(유도블록) · 정밀절단 아니면 특별인부 2 - 1.""" + given = { + "형식": "B-Type", + "유도블록": "유도・점자블록", + "정밀절단": "아님", + "지역": "전국평균", + "보정작업": "해당 없음", + } + got = mf.run(whole, "건설품셈:1-7-1 보도용 블록 설치(소형)", given) + assert got["줄"][1]["수량"] == Decimal(1) / Decimal(171) + + +def test_토목_블록붙이기_굴착기_대체(whole): + """토목 2-3-3 주④ — 크레인 대신 굴착기 0.2㎥ 0.063hr · 크레인 0.""" + given = {"장비": "굴착기", "크레인": "2104-0010", "지역": "전국평균", "보정작업": "해당 없음"} + got = mf.run(whole, "건설품셈:2-3-3 블록 붙이기(기계)", given) + assert [line["수량"] for line in got["줄"][2:]] == [0, Decimal("0.063")] + + def test_범위_입력은_막음(whole): with pytest.raises(mf.FormulaError): mf.run( diff --git a/ui_template/ui_template_locale.ts b/ui_template/ui_template_locale.ts index df2909ad..a36c79d4 100644 --- a/ui_template/ui_template_locale.ts +++ b/ui_template/ui_template_locale.ts @@ -24,6 +24,7 @@ import { ui_locales_a } from "./ui_template_locale_a"; import { ui_locales_b1 } from "./ui_template_locale_b1"; import { ui_locales_b2 } from "./ui_template_locale_b2"; import { ui_locales_b3 } from "./ui_template_locale_b3"; +import { ui_locales_m1 } from "./ui_template_locale_m1"; /** 지원 언어 인덱스: 0 = 한국어, 1 = 영어 */ export const LANGUAGES = ["ko", "en"] as const; @@ -58,6 +59,7 @@ export const ui_locales = { ...ui_locales_b1, ...ui_locales_b2, ...ui_locales_b3, + ...ui_locales_m1, } as const; export type LocaleKey = keyof typeof ui_locales; diff --git a/ui_template/ui_template_locale_m1.ts b/ui_template/ui_template_locale_m1.ts new file mode 100644 index 00000000..1429d66d --- /dev/null +++ b/ui_template/ui_template_locale_m1.ts @@ -0,0 +1,39 @@ +/* ============================================================================= + * ui_template_locale_m1.ts + * M01 마스터 요소 화면(시스템 관리자) 사전 — 신규 문구는 최하단에 추가. + * ========================================================================== */ + +export const ui_locales_m1 = { + B01_Dashboard_MasterElements: ["마스터 요소", "Master Elements"], + M01_Title: ["마스터 요소", "Master Elements"], + M01_AdminOnly: ["시스템 관리자만 볼 수 있음", "System administrators only"], + M01_LoadFailed: ["마스터 요소를 못 읽음", "Failed to load master elements"], + M01_PickFile: ["왼쪽에서 그룹과 파일을 고를 것", "Pick a group and a file on the left"], + M01_Search: ["이름 찾기", "Find by name"], + M01_Prev: ["이전", "Prev"], + M01_Next: ["다음", "Next"], + M01_Page: ["쪽", "pages"], + M01_Rows: ["줄", "rows"], + M01_NoRows: ["줄 없음", "No rows"], + M01_RowAdd: ["줄 더하기", "Add row"], + M01_RowDelete: ["지우기", "Delete"], + M01_RowRestore: ["되살리기", "Restore"], + M01_RowRemove: ["빼기", "Remove"], + M01_BackToTables: ["← 표 목록", "← Tables"], + M01_Changed: ["고친 곳 {value}", "{value} changes"], + M01_Save: ["저장", "Save"], + M01_Discard: ["고친 것 버리기", "Discard changes"], + M01_DiscardConfirm: ["고친 것을 모두 버릴까?", "Discard all changes?"], + M01_Saved: ["저장했음", "Saved"], + M01_SaveFailed: ["저장 못 함 — {value}", "Save failed — {value}"], + M01_Stale: [ + "그 사이 파일이 바뀜({value}) — 고친 것을 버리고 다시 읽은 뒤 다시 고칠 것", + "Files changed meanwhile ({value}) — discard and reload, then edit again", + ], + M01_StaleReload: ["버리고 다시 읽기", "Discard and reload"], + M01_FileStale: [ + "고치기 시작한 뒤 이 파일이 바뀜 — 저장하면 막힘", + "This file changed after you started editing — saving will be refused", + ], + M01_CheckErrors: ["검사에 걸림", "Validation failed"], +} as const;