Merge remote-tracking branch 'origin/main' into sub_desktop_1

This commit is contained in:
2026-09-11 08:59:27 +09:00
23 changed files with 1251 additions and 27 deletions
+4
View File
@@ -71,3 +71,7 @@ config/server_calc_node/
# 다만 아래 둘은 PC 마다 다른 값이라 나르면 안 됨 — 나르면 상대 PC 에서 어긋남.
docs/wiki/graphify-out/.graphify_python
docs/wiki/graphify-out/.graphify_incremental.json
# 캐시는 **PC 마다 다시 만들어지는 파생물**이라 git 으로 나르지 않는다(2026-09-10 사용자 확정).
# 까닭 — 코드 창에서 graphify 가 돌 때마다 이 폴더가 바뀌어, 위키 브랜치를 받을 때
# 「덮으면 네 것이 날아간다」로 **병합이 막혔다**. 4.3MB·443파일도 함께 가벼워진다.
docs/wiki/graphify-out/cache/
@@ -64,7 +64,14 @@ ACCEPTED_DESTINATION = "material"
SUPPLY_OWNER = "owner_supplied" # 관급 — 발주처 지급
SUPPLY_CONTRACTOR = "contractor_supplied" # 사급 — 도급자 구입
SUPPLY_UNKNOWN = "unknown" # 아직 안 정함 — 화면에 드러낸다
SUPPLY_LABELS = {SUPPLY_OWNER: "관급", SUPPLY_CONTRACTOR: "사급", SUPPLY_UNKNOWN: "미분류"}
#: ⚠ 「미분류」로만 적으면 **우리가 아직 못 만든 것처럼** 읽힌다 — 실제로는 **발주기관이
#: 정할 자리**라 우리가 지어내면 안 되는 값이다(2026-09-09 화면 확인에서 나옴).
#: 그래서 표기에 **누가 정하는지**를 함께 적는다. 값(`unknown`)과 판정은 그대로다.
SUPPLY_LABELS = {
SUPPLY_OWNER: "관급",
SUPPLY_CONTRACTOR: "사급",
SUPPLY_UNKNOWN: "미정(발주기관 결정)",
}
#: ⚠ 관급 안의 **설치 주체** — 안전관리비 대상액은 관급 전액이 아니라 「도급자설치 관급금액」이다
#: (PLAN 8-10 대상액 정의). 관급/사급 두 갈래로만 두면 B09 가 ⑤를 못 세운다.
+4 -3
View File
@@ -4,7 +4,7 @@
*
* 자재총괄 열은 순수량·할증률·합계 셋이고 **금액이 없다** — 금액은 B09 몫이다.
*
* ⚠ 「모르는 값」을 빈칸으로 두지 않는다. 할증률 미확보·관급구분 미분류·설치주체 미지정은
* ⚠ 「모르는 값」을 빈칸으로 두지 않는다. 할증률 미확보·관급/사급 미정·설치주체 미지정은
* 모두 화면에 **글자로** 뜬다. 0 % 나 빈칸으로 두면 「할증 없음」과 구별이 안 되고,
* 설치 주체를 못 정한 채 넘어가면 B09 안전관리비가 조용히 틀린다.
*
@@ -174,7 +174,8 @@ function warning(title: string, items: string[]): HTMLElement | null {
/** 관급/사급 고르는 칸의 보기. **값은 영문 키, 표기는 한글**(B09 와 같은 낱말). */
const SUPPLY_OPTIONS = [
{ value: "unknown", label: "미분류" },
// ⚠ 「미분류」로만 적으면 미완성으로 읽힌다 — **발주기관이 정할 자리**임을 표기에 담는다.
{ value: "unknown", label: "미정(발주기관 결정)" },
{ value: "contractor_supplied", label: "사급" },
{ value: "owner_supplied", label: "관급" },
];
@@ -259,7 +260,7 @@ export function renderMaterialGrid(
for (const notice of [
warning("⚠ 중복 할증 위험", table.double_count_warnings),
warning("할증률 미확보", table.missing_rate_materials),
warning("관급구분 미분류", table.missing_supply_materials),
warning("관급/사급 미정 — 발주기관이 정하는 자리입니다", table.missing_supply_materials),
warning("설치 주체 미지정(관급)", table.missing_install_by_materials),
]) {
if (notice) wrap.append(notice);
+164
View File
@@ -0,0 +1,164 @@
"""B09 — **산출기초** 한 장 (별표2 (5)(가) 열셋째).
**무엇인가** — 새로 세는 장이 아니다. **이미 줄마다 달려 있는 근거를 한 장으로 접는** 자리다.
법이 설계서 맨 뒤에 「산출기초」를 두라 했는데, 우리는 근거를 **줄에만** 달고 있어
설계서로 묶을 때 그 문구들을 사람이 손으로 모아야 했다.
**넷으로 접는다**
① 어느 판으로 계산했나 — 품셈·요율·노임·유가·기계·자재 데이터의 판과 기준일
② 무엇을 골랐나 — 프로젝트가 고른 값(범위 계수·장비·유가 지역·수송·품 할증 …)
③ 공종마다 무엇을 근거로 했나 — 일위대가 줄에 달린 근거 문구 그대로
④ 못 채운 자리 — 값이 안 선 사유(0 으로 때우지 않은 자리)
⚠ **여기서 값을 다시 계산하지 않는다.** 계산은 일위대가·내역서가 하고 이 장은 **모으기만**
한다 — 두 벌로 세면 설계서와 내역서가 조용히 갈린다.
"""
from __future__ import annotations
import json
import os
from typing import Any
from B09_Estimation.B09_Estimation_PriceBook import PriceKind
_MANIFEST = ("resources", "data_cost_input_value", "_manifest.json")
_MASTER_DIR = ("resources", "data_work_item_master")
SHEET_NOTE = (
"산출기초는 새로 세는 장이 아니라 **줄에 달린 근거를 한 장으로 모은 것**입니다 —"
" 값은 일위대가·내역서가 낸 그대로입니다."
).replace("**", "")
def _project_root() -> str:
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def _read_json(*parts: str) -> Any:
with open(os.path.join(_project_root(), *parts), encoding="utf-8") as handle:
return json.load(handle)
def dataset_versions() -> list[dict[str, str]]:
"""① 어느 판으로 계산했나 — 값층 데이터의 판과 기준일.
⚠ **장부(`_manifest.json`)를 그대로 읽는다** — 여기서 목록을 따로 적으면 판이 바뀔 때
한쪽만 고쳐진다.
"""
rows: list[dict[str, str]] = []
manifest = _read_json(*_MANIFEST)
for entry in manifest.get("files") or []:
rows.append(
{
"dataset_id": str(entry.get("dataset_id") or ""),
"file": str(entry.get("file") or ""),
"effective_date": str(entry.get("effective_date") or ""),
"sha256": str(entry.get("sha256") or "")[:12],
}
)
# 공종 마스터는 다른 폴더의 장부라 함께 싣는다 — 품셈 원문을 정규화한 판이다.
master_dir = os.path.join(_project_root(), *_MASTER_DIR)
if os.path.isdir(master_dir):
for name in sorted(os.listdir(master_dir)):
if name.startswith("work_item_master_") and name.endswith(".json"):
rows.append(
{
"dataset_id": "work_item_master",
"file": name,
"effective_date": name[len("work_item_master_") : -len(".json")],
"sha256": "",
}
)
return rows
def chosen_conditions(settings: dict[str, Any] | None) -> list[dict[str, str]]:
"""② 무엇을 골랐나 — **고른 것만** 싣는다(안 고른 칸은 기본값이라 줄이 안 선다)."""
picked = settings or {}
rows: list[dict[str, str]] = []
for key, label in (
("misc_material_percent", "공구손료·잡재료 (주재료비의 %)"),
("fuel_region", "유가 지역(시도코드)"),
("transport_distance_km", "기계 수송 거리(편도 ㎞)"),
("transport_road", "수송 도로 구분"),
):
value = str(picked.get(key) or "").strip()
if value:
rows.append({"item": label, "value": value})
for code, choice in sorted((picked.get("range_factor_choices") or {}).items()):
rows.append({"item": f"범위 계수 {code}", "value": str(choice)})
for code, choice in sorted((picked.get("machine_choices") or {}).items()):
rows.append({"item": f"장비 규격 {code}", "value": str(choice)})
from B09_Estimation.B09_Estimation_LaborSurcharge import parse_choices, total_percent
surcharge = parse_choices(picked.get("labor_surcharge"))
if surcharge:
percent, reasons = total_percent(surcharge)
rows.append({"item": f"품 할인·할증 합계 {percent:g}%", "value": " · ".join(reasons)})
return rows
def work_item_basis(build: Any) -> list[dict[str, Any]]:
"""③ 공종마다 무엇을 근거로 했나 — **줄에 달린 문구를 그대로** 모은다."""
rows: list[dict[str, Any]] = []
for code, title in sorted(build.book.titles.items()):
if title.kind is not PriceKind.UNIT_PRICE:
continue
notes = [
detail.note.strip()
for detail in build.book.details.get(code, [])
if detail.note and detail.note.strip()
]
work_item_code = code[2:].split("#")[0]
source = build.factor_sources.get(work_item_code)
if source and source not in notes:
notes.append(str(source))
if not notes:
continue
rows.append(
{
"code": code,
"name": title.name,
"unit": title.unit,
"notes": notes,
}
)
return rows
def open_gaps(build: Any) -> list[dict[str, str]]:
"""④ 못 채운 자리 — **0 으로 때우지 않은 자리**를 사유째 모은다."""
rows: list[dict[str, str]] = []
for code, reason in sorted(build.component_gaps.items()):
rows.append({"kind": "성분 못 채움", "code": code, "reason": str(reason)})
for code, section in sorted(build.basis_missing.items()):
rows.append({"kind": "밑수 미확보", "code": code, "reason": str(section)})
for note in build.transport_notes:
rows.append({"kind": "수송비", "code": "FP-10-04", "reason": str(note)})
for machine in build.incomplete_machines:
rows.append({"kind": "기계 층 미완성", "code": machine, "reason": "손료·운전경비 미확보"})
return rows
def basis_sheet(build: Any, settings: dict[str, Any] | None = None) -> dict[str, Any]:
"""산출기초 한 장 — 넷을 접어 낸다."""
versions = dataset_versions()
conditions = chosen_conditions(settings)
items = work_item_basis(build)
gaps = open_gaps(build)
return {
"note": SHEET_NOTE,
"dataset_versions": versions,
"chosen_conditions": conditions,
"work_items": items,
"gaps": gaps,
"summary": (
f"데이터 {len(versions)} 판 · 고른 값 {len(conditions)} · "
f"근거가 달린 공종 {len(items)} · 못 채운 자리 {len(gaps)}"
),
}
@@ -149,12 +149,13 @@ DESIGN_DOC_ITEMS: tuple[dict[str, Any], ...] = (
{
"order": 13,
"name": "산출기초",
"status": STATUS_PARTIAL,
"status": STATUS_READY,
"owner": OWNER_PROGRAM,
"where": "각 줄의 산출근거 문구",
"where": "원가계산 → 산출기초",
"note": (
"마다 어디서 온 값인지는 적혀 있으나 별도의 장으로 묶여 있지 않다 — "
"설계서로 묶으려면 그 문구들을 한 장으로 모으는 자리가 필요하다."
"에 달린 근거를 한 장으로 모은 장이다 — 어느 판으로 계산했나(데이터 기준일) · "
"무엇을 골랐나(산출 조건) · 공종마다 무엇을 근거로 했나 · 못 채운 자리. "
"여기서 값을 다시 계산하지 않는다."
),
},
)
+25
View File
@@ -319,6 +319,28 @@ async def get_price_sources(project_id: UUID) -> JSONResponse:
)
@router.get("/{project_id}/estimation/basis-sheet")
async def get_basis_sheet(project_id: UUID) -> JSONResponse:
"""**산출기초** — 줄에 달린 근거를 한 장으로 모은다(별표2 (5)(가) 열셋째).
⚠ 값을 다시 계산하지 않는다 — 일위대가·내역서가 낸 그대로를 모으기만 한다.
"""
from B09_Estimation.B09_Estimation_BasisSheet import basis_sheet
from common_util.common_util_project_settings import estimation_settings
try:
build = await _build_for(project_id)
root = await _project_root_of(project_id)
settings = estimation_settings(root) if root else {}
return JSONResponse(content={"status": "success", **basis_sheet(build, settings)})
except Exception:
logger.exception("B09 산출기초 실패: project_id=%s", project_id)
return JSONResponse(
status_code=500,
content={"status": "error", "message": "산출기초를 못 만들었습니다."},
)
@router.get("/{project_id}/estimation/machine-expense")
async def get_machine_expense(project_id: UUID) -> JSONResponse:
"""**각종 중기경비계산서** — 기종마다 한 장(별표2 (5)(가) 아홉째).
@@ -486,6 +508,9 @@ async def get_factor_choices(project_id: UUID) -> JSONResponse:
" 별도 계상하되 산정 근거를 명시하여야 한다」",
"⚠ 비워 두면 안 붙습니다 — 지금은 안 붙고 있는 상태입니다"
" (사용자 확정 2026-09-09 「지금은 안 넣되 숫자 넣으면 되게 열어 둘 것」).",
"⚠ 석공사·철골공사처럼 특수공구를 쓰는 자리는 건설품셈 1-2-6 이"
" 「별도 계상」으로 두었습니다 — 일반 비율을 그대로 붙이면 안 되는"
" 자리라, 그런 공종이 섞인 내역에서는 켜기 전에 살펴보십시오.",
],
"base_items": with_material,
"base_note": (
@@ -357,6 +357,91 @@ export function drawMachineExpense(body: HTMLElement, data: MachineExpenseDto):
for (const sheet of data.sheets) body.append(machineExpenseSheet(sheet));
}
/* =============================================================================
* 산출기초 — 줄에 달린 근거를 한 장으로 모은 장(별표2 (5)(가) 열셋째).
* ⚠ 여기서 값을 다시 계산하지 않는다 — 모으기만 한다.
* ========================================================================== */
export interface BasisSheetDto {
status: string;
note: string;
summary: string;
dataset_versions: Array<{
dataset_id: string;
file: string;
effective_date: string;
sha256: string;
}>;
chosen_conditions: Array<{ item: string; value: string }>;
work_items: Array<{ code: string; name: string; unit: string; notes: string[] }>;
gaps: Array<{ kind: string; code: string; reason: string }>;
}
export async function fetchBasisSheet(projectId: string): Promise<BasisSheetDto> {
const response = await fetch(
`${API_BASE_URL}/projects/${encodeURIComponent(projectId)}/estimation/basis-sheet`,
{ credentials: "include" },
);
if (!response.ok) throw new Error(`basis-sheet ${response.status}`);
return (await response.json()) as BasisSheetDto;
}
export function drawBasisSheet(body: HTMLElement, data: BasisSheetDto): void {
body.append(head("산출기초"));
body.append(note(data.summary));
body.append(note(data.note));
body.append(head(`① 어느 판으로 계산했나 (${data.dataset_versions.length})`));
body.append(
table(
["자료", "파일", "기준일", "지문(앞 12)"],
data.dataset_versions.map((row) => [
row.dataset_id,
row.file,
row.effective_date,
row.sha256 || "—",
]),
[0, 1, 2, 3],
),
);
body.append(head(`② 무엇을 골랐나 (${data.chosen_conditions.length})`));
if (data.chosen_conditions.length === 0) {
body.append(note("고른 값이 없습니다 — 전부 확정 기본값으로 돌고 있습니다."));
} else {
body.append(
table(
["항 목", "고른 값"],
data.chosen_conditions.map((row) => [row.item, row.value]),
[0, 1],
),
);
}
body.append(head(`③ 공종마다 무엇을 근거로 했나 (${data.work_items.length})`));
body.append(
table(
["코드", "공 종", "단위", "근 거"],
data.work_items.map((row) => [row.code, row.name, row.unit, row.notes.join(" · ")]),
[0, 1, 2, 3],
),
);
body.append(head(`④ 못 채운 자리 (${data.gaps.length})`));
if (data.gaps.length === 0) {
body.append(note("못 채운 자리가 없습니다."));
} else {
body.append(
table(
["갈 래", "코드", "사 유"],
data.gaps.map((row) => [row.kind, row.code, row.reason]),
[0, 1, 2],
),
);
body.append(note("⚠ 여기 있는 것은 0 으로 때우지 않고 남겨 둔 자리입니다."));
}
}
/** 두 탭이 함께 쓰는 「아직 못 불러왔습니다」 문구. */
export function drawBaseDataError(body: HTMLElement): void {
body.append(note(L("B09_Estimation_Tab_Pending")));
+27
View File
@@ -19,17 +19,20 @@ import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
import {
drawBaseDataTab,
drawFactorChoices,
drawBasisSheet,
drawDesignDocTab,
drawMachineExpense,
drawMachineTab,
drawPriceSourcesPending,
drawPriceSourcesSections,
fetchBaseData,
fetchBasisSheet,
fetchDesignDocIndex,
fetchMachineExpense,
fetchFactorChoices,
fetchPriceSources,
type BaseDataDto,
type BasisSheetDto,
type DesignDocDto,
type MachineExpenseDto,
type FactorChoicesDto,
@@ -602,6 +605,7 @@ const TAB_KEYS: Array<[string, keyof typeof ui_locales, boolean]> = [
["supply", "B09_Estimation_Tab_Supply", true],
["base_data", "B09_Estimation_Tab_BaseData", true],
["design_doc", "B09_Estimation_Tab_DesignDoc", true],
["basis_sheet", "B09_Estimation_Tab_BasisSheet", true],
];
function buildTabs(active: string, onSelect: (key: string) => void): HTMLElement {
@@ -815,6 +819,7 @@ export async function renderB09Estimation(root: HTMLElement): Promise<void> {
let priceSources: PriceSourcesDto | null = null;
let factorChoices: FactorChoicesDto | null = null;
let designDoc: DesignDocDto | null = null;
let basisSheet: BasisSheetDto | null = null;
let machineExpense: MachineExpenseDto | null = null;
let sheet: CostSheetDto | null = null;
let unitPriceList: UnitPriceListDto | null = null;
@@ -1177,6 +1182,28 @@ export async function renderB09Estimation(root: HTMLElement): Promise<void> {
drawMaterialTab();
return;
}
if (activeTab === "basis_sheet") {
// 산출기초 — 줄에 달린 근거를 모아 오는 장이라 조립이 끝나야 뜬다.
if (basisSheet) {
drawBasisSheet(body, basisSheet);
return;
}
const waiting = document.createElement("div");
waiting.className = "b09-empty";
waiting.textContent = L("B09_Estimation_Tab_Pending");
body.append(waiting);
if (projectId) {
void fetchBasisSheet(projectId)
.then((data) => {
basisSheet = data;
drawBody();
})
.catch(() => {
/* 못 받아도 화면을 비우지 않는다. */
});
}
return;
}
if (activeTab === "design_doc") {
// 설계서 구성표 — 프로젝트 값이 아니라 **우리가 무엇을 내는가**의 표다.
if (designDoc) {
@@ -0,0 +1,198 @@
# 계획서 0·3·4장 완료분 근거 (이관 2026-09-09)
> 계획서에는 결과 한두 줄만 남기고 **조사·검증 원문을 여기로 통째로** 옮긴 것
> (2026-09-09 사용자 확정 규칙). 원문 그대로이며 손대지 않았음.
- [x] **재계산 중간 취소 — 만들지 않기로 닫음** (2026-09-09 밤 재판정, 커밋 `5d4473b4`)
**잰 값** — 서버 계측 로그 네 번: **87.3 · 90.0 · 93.9 · 95.4초**(평균 91.7초).
그중 **배수·관·측점 재생성이 80~86초(90 % 넘음)**, 노선 풀이는 3초대임.
293초 → 90초로 이미 3분의 1이 됐음(0-11).
**왜 안 만드나 — 취소가 「반쯤 부순 상태」를 만드는 단추가 되기 때문임.**
· 체인이 돌기 **전에** 관 지점을 이미 지움(`clear_pipe_points`) — 취소해도 안 돌아옴
· 체인이 끊기면 **노선만 새것 · 종횡단은 옛것**으로 어긋남(2026-09-06 실측 사고:
배수관 측점 9 → 0). 지금 코드도 그때는 계획노선을 되돌리라고 사유를 올려 보냄
· 90초는 기다릴 수 있는 길이고, 급하면 **취소보다 그 80초 한 곳을 줄이는 것**이 이득임
**대신 한 것** — 대기 안내가 「몇 분 걸립니다」라는 **옛 문구**였음. **「1분 반쯤 걸립니다
(N초 지남)」** 로 고쳐 **경과 시간이 1초마다 도는 것**을 보임(멈춘 것인지 도는 것인지 알 수 있게).
**다시 열 조건** — 배수·관·측점 재생성이 더 커져 **3분을 넘거나**, 체인을 단계마다
되돌릴 수 있게 되면 그때 다시 볼 것.
---
- [x] **직행 `/files` 가드 — 이미 붙어 있었음** (2026-09-09 확인)
`B03_FileInput_Router.py:212` 가 분석중 차단, `:249` 가 같은 이름 옛 행 내림(`supersede`)을
부름. 주석에 「이 갈래만 빠져 있어 분석 2개가 같은 산출물 경로에서 부딪혔다(2026-09-08)」로
까닭까지 적혀 있음. **세 갈래가 같아졌음.**
~~[잔손질] 두 줄이면 세~~
갈래가 같아짐. **가장 싸고 효과 큼.**
---
- [x] **purge 전 확인 단계 — 이미 있었음** (2026-09-09 확인)
`B03_FileInput_Router_Helpers.py:69` `_confirm_replace_response`**409** 로 「지우고
진행할까?」를 묻고 **무엇이 지워지는지 함께 알림**. 사람이 고르면 `confirm_replace=True`.
⇒ 0-8 은 셋 중 둘이 닫혔고 **남은 것은 업로드 잠금 표시 하나**임.
~~[반나절] 「이 프로젝트에 설계 산출물이 있음. 지우고 새~~
자료로 갈까?」. 지금은 말없이 지움.
---
- [x] **프로젝트 단위 업로드 잠금 표시****2026-09-09 밤 끝냄**(커밋 `018ebe7f`).
막는 것은 이미 있었고(`is_analysis_running`) 없던 것은 **막힌 까닭을 사람에게 보이는 것**이었음.
- 분석을 띄우는 **경로 넷 모두** 시작한 사람을 `params.started_by` 에 담음
- `analysis_lock_owner` 가 그 id 로 이름을 붙임 — 옛 자료는 **이름 없이 잠금만** 돎(막는 것이 먼저)
- `/upload-overview``analysis_lock` 을 실어 B03 화면이 띠로 보임
| 잰 것 | 결과 |
|---|---|
| API 칸이 서나 | ⭕ `analysis_lock` — 도는 분석이 없어 지금은 `null` (두 프로젝트 200 확인) |
| 화면 띠가 뜨나 | ⭕ 「엄상돈 님이 올린 자료를 분석하는 중입니다 — 끝난 뒤에 새 자료를 올릴 수 있습니다」 경고색 |
| 이름이 없으면 | ⭕ 「다른 자리에서 올린 자료를…」 로 뜸(잠금은 그대로) |
**실제 분석이 도는 화면은 아직 못 봄** — 지금 도는 분석이 없어 응답을 잠깐 가로채 확인하고
곧바로 되돌렸음. **다음 업로드 때 실물로 볼 것.** 시험 넷 추가, 전체 **1211 통과 · 실패 0**.
---
- [x] **[답 나옴 · 보류 확정 — 2026-09-09 밤 닫음]** **도수로·절토사면 배수로는 품셈에 그 공종이 없음** (2026-09-08).
「수로」로 전수 검색해도 나오는 것이 위 셋뿐임. 지장목제거와 같은 모양 —
**수량은 서는데 단가 공종이 없는 것**.
⇒ **2026-09-09 사용자 확정 5차 3번 — 「나중에 실무 문서 첨부. 임의로 만드는 건 지금은
무의미」로 보류.** B06 은 **배치만 받고** 단가·일위대가를 만들지 않음. 지금처럼
사유(`pending_user`)로 두는 것이 확정된 동작임 — **더 할 일 없음. 문서가 오면 그때 엶.**
**미체크로 둘 까닭이 없어 닫음**(2026-09-09 밤 재훑기). 문서가 오면 4-12 쪽에서 새로 열 것.
---
- [x] **[잔손질]** 카드에서 측구를 **껐다 켰다** 해서 방금 가른 선택/결과가 화면에서 도는지.
**2026-09-09 밤 화면 실측으로 끝냄 — 안 돌던 까닭을 찾아 고쳤음.**
**⚠ 왜 안 돌았나 — 「칸 하나」가 통로에서 빠져 있었음**
측구 선택(`ditch_choice`)이 **캐시(`CrossDesignChoice`)에 없어** 눌린 값이 그 자리에서
버려졌음. 브라우저 재계산은 저장분 결과만 읽었고, [저장] patch·서버 스키마에도 칸이 없었음.
⇒ 통로 넷 + 스키마를 이었음(`5907de85`). 화면 코드는 리로드, 스키마는 백엔드 재시작으로 반영.
**잰 것 (측점 1+0.0 = 20.0m, 프로젝트 랩탑_보조 route 182)**
| 잰 것 | 고치기 전 | 고친 뒤 |
|---|---|---|
| 캐시에 담기나 | ❌ `ditch_choice` 칸 자체가 없음 | ⭕ `"ditch_choice": false` 로 담김 |
| 끄면 사라지나 | ❌ `aria-pressed``true` 그대로 | ⭕ `false` · **절토 계 3.33 → 1.78㎡** |
| 다시 누르면 켜지나 | ❌ 안 돎 | ⭕ `true` · **절토 계 1.78 → 3.90㎡** |
| 「자동」으로 풀리나 | — | ⭕ **엔진이 자동값과 같은 선택은 `null` 로 되돌림**(시험 `test_자동과_같은_선택은_자동으로_푼다`) |
| 계획고를 바꾸면 자동이 다시 도나 | — | ⭕ **구조로 잠김** — 안 만진 측점은 캐시에 선택이 없어 자동 판정을 그대로 탐 |
시험 `test_b06_ditch_choice.py` 에 통로 넷 + patch 병합 잠금 한 건 추가(전체 **1198 통과 · 실패 0**).
조작으로 바꾼 값은 **캐시를 걷고 리로드해 되돌렸음** — 절토 계 3.33㎡ 로 복귀 확인, [저장]·[확정] 안 눌렀음.
**✅ 곁들여 본 것 — 카드를 만졌다 되돌리면 값이 저장분과 다름 (2026-09-09 밤 끝냄, 커밋 `2eb2c972`)**
**거울 문제가 아니었음.** 같은 입력을 주면 파이썬·TS 가 같은 값을 냄 —
측점 20.0m 에서 **표준단면 측구 상폭 0.69 기준 절토 3.328 · 0.9 기준 3.899** 로 둘 다 같음.
갈린 것은 산식이 아니라 **입력(표준단면)** 이었음.
⚠⚠ **더 큰 것이 나왔음 — 저장분 한 줄 안에 두 표준단면이 섞여 있었음.**
| 잰 것 (측점 20.0m) | 값 |
|---|---|
| 저장분 절토 계 | 3.328㎡ — **옛 상폭 0.69 기준** |
| 저장분 측구 기하·면적 | 상폭 **0.9** · 0.18㎡ — **새 상폭 기준** |
| 저장분 옵션의 표준단면 | 상폭 **0.9** (사용자가 고쳐 저장한 값) |
까닭 — 브라우저는 **만진 측점만** 다시 계산하는데 [저장]은 **전 측점 면적**을 실어 보냄.
그래서 표준단면을 고쳐도 안 만진 측점은 **옛 면적 그대로** 정본이 됨(수량이 갈리는 자리).
고친 것 — ① [저장]·[확정]이 면적을 모으기 **전에** 전 측점을 다시 계산함
② 표준단면 패널에 **「고친 값이 측점에 아직 안 실렸음」 알림**(언제 따라오는지 함께 적음).
화면 실측 — 패널 값을 저장분과 다르게 두면 알림이 뜨고, 같으면 사라짐(초안은 원래대로 되돌려 놓았음).
시험 둘 추가, 전체 **1205 통과 · 실패 0**.
**[저장] 실측은 아직 못 했음** — 사용자가 표토 축 확정 전까지 저장을 멈춰 둔 상태라
저장 버튼을 안 눌렀음. 잠금이 풀리면 「표준단면 고치고 저장 → 전 측점 면적이 따라오나」를 볼 것.
---
- [x] **[잔손질]** 확정 ② 후반 「사용자가 값을 바꿀 수 있게」 — 표준도 입력 칸에 **상부·하부 두께**를
더할 것(비우면 식). ⑨ 야면석 계수 · ⑩ 채움 강도와 같은 자리.
**2026-09-09 밤 끝냄** — 등록부 돌쌓기 계열 다섯에 칸을 두고 표준도 제원 폼·저장·수량까지 이었음.
큰돌쌓기는 두께 식이 달라 뺐음(그 장에는 칸이 안 뜸).
| 잰 것 (랩탑_보조 · 표준도 3장 돌쌓기(찰) H=2.5) | 결과 |
|---|---|
| 칸이 뜨나 | ⭕ 「상부 두께(m)」·「하부 두께(m)」 — 자리표시 「비우면 식」 |
| 저장되나 | ⭕ 상부 0.60 저장 → 「2개소에 반영했습니다」 · 정본 `thickness_top_m: 0.6` |
| 수량에 닿나 | ⭕ **입적 24.375 → 20.625㎥** · 근거 「상부 0.60(사용자 입력) · 하부 1.05(상부 + 0.3×(H1))」 |
| 비우면 식으로 돌아오나 | ⭕ 칸을 비워 저장 → 정본에서 칸이 지워지고 **입적 24.375㎥ 복귀** |
⚠ 빈 칸은 「안 정함」임 — 자동값을 저장에 박지 않음. 상부만 넣으면 하부는 그 위에서 식을 이음.
시험 다섯 추가(`test_b08_coeff_and_strength.py`), 전체 **1203 통과 · 실패 0**. 커밋 `3a64bb5b`.
---
- [x] **끝남(2026-09-09, `0ff26d43`)**`spoil_bank` 두 줄(`side`·`extra_distance_m`)을 뺐음.
**덮여 있었음** — 정본에 `default_basis` 로 까닭이 적혀 있어 목록 없이도 통과.
- 자체검증 — 두 줄을 지우고 `resources/tester/` 전체 실행: **1183 통과 / 22 건너뜀 / 실패 0**.
그 자리에 「앞으로도 목록에 넣지 말 것」을 적어 둠.
- ⚠ **같은 상태가 13 개 더 있음**`revetment`·`soil_guard`·C군 `side` 계열
(`revetment.tiers`·`lift_m`·`shift_m`·`side` · `soil_guard.tiers`·`lift_m`·`shift_m`·
`stone_supply`·`side` · `masonry_wet/dry`·`boulder_masonry`·`retaining_wall`.`side`).
빼도 통과하나 **주석 덩어리가 얽혀 있어** 이번 잔손질 범위를 넘음. 다음에 그 파일을
손댈 때 함께 걷을 자리.
- ⚠ 「혼자 지우지 말 것」 단서는 **풀렸음** — 시험이 `resources/tester`(git 안)로 옮겨져
창끼리 함께 건너감.
---
- [x] **날개벽·집수정 원단위****2026-09-09 밤 끝냄**(커밋 `0747979c`).
**집수정 Ø800 은 이미 표에 있었음**(`pipe_inlet_basin` □형·콘크리트·800) — 다시 안 만듦.
날개벽 다섯 형식을 원본 탭 소계 그대로 옮겼고, **고르는 칸**까지 이었음.
| 형식 | 터파기 | 되메우기 | 잔토 | 기초잡석 | 콘크리트(저판+벽체) | 거푸집(저판+벽체) |
|---|---|---|---|---|---|---|
| Ø800 A-TYPE | 13.104 | 6.504 | 6.61 | 1.152 | 2.0595 | 유로폼 12.5376 |
| Ø800 C-TYPE | 7.7269 | 4.7148 | 3.0122 | 0.6821 | 1.109 | 유로폼 6.2309 |
| Ø1000 A-TYPE | 24.39 | 10.478 | 13.912 | 1.6 | 2.891 | 합판(4회) 18.255 |
| Ø1000 C-TYPE | 13.7179 | 7.9134 | 5.8046 | 1.0271 | 1.9109 | 합판(4회) 10.7220 |
| Ø800 A+집수정 | 16.256 | 8.44 | 7.816 | 1.152 | 2.6395 | 합판(4회) 18.3376 |
- **거푸집 종류를 뭉개지 않았음** — 원본 탭에서 직접 읽어 Ø800 A·C 는 유로폼,
Ø1000 과 A+집수정은 합판(4회). 단가가 갈리는 자리라 이름을 그대로 둠.
- ⚠ **철근·면목은 원본 날개벽 탭에 없음** — 지어내지 않았고 시험이 그것을 지킴.
- ⚠ **겹쳐 세는 자리** — 「A-TYPE+집수정」은 집수정을 품은 형식이라 집수정 형식도 차 있으면
**사유로 알림**(값은 안 고침). 원단위표 `double_count_rules` 에도 규칙으로 적었음.
- 등록부·배치 폼에 「관보호공 날개벽」 칸(A-TYPE·C-TYPE·A-TYPE+집수정, 비우면 안 셈).
검증 — 화면에서 배수관 폼에 칸이 뜨고 선택지 셋 확인. 엔진을 돌려 **날개벽 줄이 따로 서고**
터파기 13.104 · 콘크리트 2.0595 · 유로폼 12.5376 이 나오는 것까지 봄. 시험 일곱 추가,
전체 **1218 통과 · 실패 0**. ⚠ 정본 저장까지는 [저장]을 눌러야 하는데 지금 멈춰 둔 상태라
**화면에서 고른 값이 수량에 실리는 것은 잠금 풀린 뒤에 볼 것.**
---
**⭐ 2026-09-09 밤 — 지금 무엇이 서 있나 (도면 알맹이로 전수 확인, 랩탑 메인)**
| 장 | 표(하단표) | 그림(상단) | 까닭 |
|---|---|---|---|
| 1장 큰돌쌓기 H=2.5 | ⭕ | ❌ | **규격 축이 직경**(`stone_cm` 60~80)인데 그림이 **뒷길이**(`back_len_cm`)를 찾음 |
| 2장 돌쌓기(메) H=2 뒷길이 35㎝ | ⭕ | ⭕ **섬** — 폴리선 2 · 선 1 · 글자 11 | 뒷길이가 정해져 있음 |
| 3장 돌쌓기(찰) H=2.5 | ⭕ | ❌ | **뒷길이를 아직 안 골랐음**(제원이 `기초유`·H 뿐) |
| 4장 옹벽 H=2 | ⭕ | ❌ | 그림 대상 아님 — `FIGURE_TYPE_IDS` 는 돌쌓기 계열 셋뿐 |
**1단계(하단표)는 네 장 다 섬** — V-9 를 ⭕ 로 닫았음(7장 참조).
**2단계(그림)는 이미 절반 돌고 있음** — 코드(`..._Cad_StandardFigure.build_figure`)가 있고
**뒷길이가 정해진 장에서는 실제로 그려짐.** 안 그려지는 것은 코드가 없어서가 아니라
**제원이 없어서**임(뒷길이를 고르면 그 장도 바로 섬).
**⚠ 하나는 코드 자리가 어긋나 있음 — 큰돌쌓기.** `FIGURE_TYPE_IDS` 에는 들어 있는데
그림이 뒷길이를 찾으므로 **직경만 있는 큰돌쌓기는 영영 못 그림**(품셈 13-6 은 직경 축,
13-4 는 뒷길이 축 — 수량 쪽에서 이미 갈라 둔 그 자리와 같음). ⇒ **큰돌쌓기 그림은
직경에서 두께를 내는 다른 식이 필요**하거나, 아니면 목록에서 빼고 사유를 적어야 함.
**만들지 않고 드러내만 둠**(2단계에서 다룰 자리).
@@ -0,0 +1,407 @@
# 계획서 8·9장 완료분 근거 — 원문 이관 (2026-09-09 밤 작업분)
> 규칙(2026-09-09 사용자 확정) — **끝난 항목의 조사·근거·검증 원문은 계획서에 안 남긴다.**
> 계획서에는 결과 한두 줄만 두고, 원문은 여기에 **통째로, 그대로** 옮긴다.
> ⚠ 계획서에 이 파일 경로를 적지 않는다(링크도 쌓인다) — 찾는 일은 위키·graphify 몫.
옮긴 항목 **22건** (B08 8장 · B09 9장의 `- [x]` 완료분).
- [x] **밑수 단위 「인」 6건 중 넷이 오독****이미 고쳐져 있었음**(커밋 `4fc5b38d`, 2026-09-09 11:46).
**자체검증 (2026-09-09 밤, 데스크탑 메인)** — 코드·데이터 대조만 하고 손대지 않음.
· 규칙 = `person_basis_ok`(`B08_Quantity_Build_WorkItemMaster.py`) — 「인」·「공」은
**앞에 숫자가 붙었을 때만** 밑수로 인정. 표 위 본문·표 안 두 갈래 모두에 물려 있음.
· 실측 = 마스터 전수에 **「인」 밑수로 선 공종 0건**. 드론방제·지상방제·야면석 채집·
휘발유는 밑수 「미확보」로 서 있음(0.36㎥ 같은 없는 값이 안 섬).
· 안 사라졌는지 = 2-2-5 천공기는 「천공인부 **1인당** 1대」라 `("인", 1.0)` 그대로 섬.
· 시험 = `test_b08_master_basis_person.py` 5개 통과.
ⓘ 아래 옛 메모는 **무엇을 왜 골랐는지**의 근거라 남겨 둠.
⚠ 2026-09-09 재판정 — **데스크탑 보조의 「참조 따라가기」와 같은 집안**이라 겹칠 수 있음.
`F0228`·`F0229`(드론방제 → **ha당**) · `F0230`(지상방제 → 차량살포) ·
`F0401`(야면석 채집 → **㎡·㎥당**) · `F0046`(휘발유 → **/대/일**) 넷이 오독이고,
`F0068`(천공기 「천공인부 **1인당** 1대」)만 맞음.
**고치는 방향** — 밑수 「인」은 원문에 「<숫자>인당」이 그대로 있을 때만 인정.
**넓게 「인」을 통째로 버리면 2-2-5 가 사라짐** — 좁게 고칠 것.
지금 값에는 안 걸리나 방제·야면석 계열이 서는 날 소요량이 통째로 틀림.
- [x] **안내 문구 둘이 사실과 어긋나던 것** `[잔손질]` — **둘 다 닫힘.**
✅ 타설 문구는 **조건이 붙었음**(`is_default` 가 아닐 때만 뜸 — `UI_Page.ts:598`).
**동바리 문구** — 구조물 수를 함께 보게 고침(2026-09-09 밤, 데스크탑 메인).
**자체검증**`B08_Quantity_UI_MaterialGrid.ts` 의 조건에 `structure_count > 0` 을 더함.
· 화면 실측(공용 브라우저 새 탭, 구조물 원단위 탭) —
**구조물 0개 프로젝트**(`5cff3920`): 머리말 「구조물 0개…」만 뜨고 **동바리 줄 없음**.
**구조물 6개 프로젝트**(`79da822d`): 동바리 줄이 **전과 같이 그대로 뜸**.
· 두 프로젝트 모두 서버 응답은 `shoring.applicable=false`**같음** — 갈린 것은 화면뿐이라
값·금액 변화 없음. 보던 프로젝트는 원래대로 되돌려 놓음.
- [x] **관 줄 밑감 — 연장(m)** `[밑감 들어옴 · 남은 것은 ㉠㉡㉢ 답 대기]` 앞서 「브라우저에만 있으니 서버로 옮기거나 저장 때 남겨야 한다」로
갈래를 냈는데, **그 계산이 이미 서버 Node 로 돌고 있었음**`computeCardCulvert`
`B06_Section_Server_Calc_Node.ts``recompute_server_side` 로 [저장]·[확정] 때 돎
(구조물 폐회로 면적이 그 길로 정본에 실림). ⇒ **그 다리에 값 한 줄 더 실으면 됨.**
랩탑 창이 측점 `design`**`pipe_length_m`**(m 단위 올림 끝난 값)로 넣기로 함.
B08 은 `get_cross_section_designs` 로 그 자리를 이미 읽으므로 **읽는 코드를 새로 안 만듦.**
⚠ 값이 정본에 실리려면 그 프로젝트에서 **[저장] 한 번**이 필요함.
ⓘ 이 줄이 서기 전에 위 ㉠·㉡·㉢ 이 먼저 정해져야 함(2026-09-09 에 한 줄에 뭉쳐 있던 것을 가름).
**밑감은 들어왔음**(2026-09-09 밤 확인) — 랩탑이 측점 `design.pipe_length_m` 자리를 만들었고
(`B06_Section_Schema` · `_Server_Calc_Prebuild` · `_Structure_Layouts`), **B08 이 그 값을 읽고 있음**
(`B08_Quantity_Engine_Pipe`). ⇒ **남은 것은 ㉠·㉡·㉢ 사용자 답뿐임.**
- [x] **밑수 대조 가드 — 내역에 실리는 줄은 이제 전부 대조됨** (2026-09-09 밤 마무리 · 커밋 `8a78927d`)
**자체검증 (2026-09-09 밤, 데스크탑 메인)** — 「반은 놀고 있음」은 **이제 옛말임.**
· 실측 — 프로젝트 `79da822d` 인계에서 **내역에 실리는 25 줄 중 24 줄이 이미 대조되고 있었고**,
못 보던 것은 **`FP-11-01` 가설창고(개소) 한 줄뿐**이었음.
· 그 한 줄을 채움 — 품셈 11-1 은 절 머리에 「(단위: …)」가 없어 **마스터가 못 채우는 자리**라,
매핑 통로에 **원문 근거와 함께 「개소」**를 적음([주]① 「설치 또는 해체 시에 각각」 ·
[주]③ 「트럭크레인 사용시간은 **1개설치당** 2시간」).
· 매핑에 `ancillary`(준비공 부대시설) 절을 만들고 **그 절도 대조에 쓰이게** 함.
· **⇒ 지금 내역 25 줄 = 대조 25 줄.** 일부러 `개소``m` 으로 보내면 가드가
「보내는 단위 「m」가 품셈 밑수 「개소」와 다릅니다」로 잡는 것을 시험으로 못 박음.
· 전체 **1245 통과 · 실패 0**.
**마스터 자체는 여전히 반쯤 빔** — 밑수 단위가 선 공종 **163/477**, 단위 빈 일위대가 **48**.
다만 그 나머지는 **우리가 안 쓰는 공종**(조림·방제 계열)이라 **지금 금액에는 안 걸림**.
쓰기 시작하는 날 그 공종의 원문 줄을 매핑에 적으면 그날 대조가 섬(통로는 이미 있음).
ⓘ 아래 옛 기록은 **무엇을 어떻게 채웠는지**의 근거라 남겨 둠.
- [x] ~~**⚠⚠ 밑수 대조 가드가 반은 놀고 있음 — 마스터가 비어서**~~ `[옛 기록]` (2026-09-08 실측) —
**오늘 「단위가 조용히 어긋난 자리」가 여섯 번** 났고(개소가 미터로 · 준비공 표 통째로 ·
B군 코드 빔 · 성분 줄 안 감 · 돌쌓기 m↔㎡ · **층따기 ㎡↔㎥**) 앞의 다섯은 이제 가드가
잡는데 **여섯째는 마스터에 밑수가 없어 못 잡았음.**
- [x] **⚠ 견적가격은 4순위임 — 화면 문구 이미 있음** (2026-09-09 확인)
`B09_Estimation_Lists_Sources.py:111` 「견적을 넣을 때는 업체명·견적일을 함께 남기십시오 —
국가계약법 시행령 §9 의 4순위(앞의 셋으로 정할 수 없을 때)라 근거가 남아야 합니다.」
~~(국가계약법 시행령 제9조) —~~
①거래실례가격 ②원가계산 ③표준시장단가 ④「①②③에 의할 수 없는 경우 감정가격·
유사 거래실례가격 또는 **견적가격**」.
⇒ **사급 단가에 견적을 쓰려면 「앞의 셋이 없다」는 것이 전제**임. 화면에 그 사유를 남길 것.
- [x] **층따기 — 사전 문구 고침** `[잔손질 — 사전 한 줄]` (2026-09-09 완료)
**자체검증**`원가_출력변수_사전` B2-9 줄의 밑수를 **「㎡·m」 → 「㎥」**로 고치고,
비고에 **왜 두 축이 있는지**를 적었음(화면·집계는 면적이 정본이고 길이 칸을 곱해 ㎥ 로
내보냄 · 확정 2차 ①). 종전 표기가 두 축을 한 칸에 적어 **㎡ 로 내도 되는 것처럼** 읽히던
자리임. 코드는 이미 ㎥ 로 내보내고 있어 **수치 변화 없음** — 전체 시험 1182 통과·실패 0.
**어긋남 자체는 닫혔음** — 확정 ① 로 **면적이 정본**이고 **길이 칸을 곱해 ㎥** 로 냄
(화면 칸 `ab51fa38` · 인계 줄이 그 길로 섬).
**남은 것은 사전 문구 하나**`원가_출력변수_사전 B2-9` 가 아직 **「㎡·m」**로 적혀 있어
품셈 9-18(㎥) 과 어긋난 채임. **다음 사람이 또 ㎡ 로 낼 자리**라 한 줄 고칠 것.
**높이·폭 수치는 전수로도 없음이 확정** — 교본 **세 곳**(6장 4절 · 15장 부록 4-3 ·
성토_비탈면)이 전부 「**설계도서에 명시되어 있는 높이와 폭으로**」 같은 문장임.
- [x] **`_manifest.json``pum_std_drawing` `deferred` 사유 문구** `[잔손질]` — 우리 말로 고침.
**자체검증 (2026-09-09 밤, 데스크탑 메인)**
· 옛 문구 「current technical DB identifies this as an unresolved source gap」 를
**「구조물 표준도 원단위(옹벽·날개벽·집수정 규격별 콘크리트·거푸집·철근량)를 담을 자리.
품셈·교본에 표가 없어 실무 구조물 일람표에서 뽑아야 하는데 그 자료가 아직 안 들어옴
— 자료 확보 대기」** 로 바꾸고, `source_needed`(무엇을 받아야 하나) ·
`knowledge_ref`(지식DB 근거 자리) 두 칸을 함께 붙임.
· 근거 = `원가_입력변수_사전 G6` 「pum_std_drawing … **현행 공백 E1**(실무 일람표 추출 필요)」.
· JSON 다시 읽어 파싱 확인. **이 파일을 읽는 코드는 없음**(장부 파일) — 값·화면 변화 없음.
**문구를 바꾸는 것과 자료를 받는 것은 다른 일임** — 표준도 원단위 자체는 여전히 대기이고,
그 대기 줄은 9-6 「날개벽·집수정 원단위」 쪽에 살아 있음.
⚠ 같은 `deferred` 의 나머지 셋(`mat_rebar`·`mat_remicon`·`asphalt_concrete`)은 **영어 그대로 둠**
— 지시가 이 한 줄이었음. 우리 말로 맞추려면 따로 한 번에 할 것.
- [x] `[반나절]` **돌 무게 인자표** — 「뒷길이 × 0.77 × 2.65」, 야면석은 0.42.
**이미 들어가 있었음**(커밋 `252e6b54`, 2026-09-09 12:08) — 「미착수」는 계획서가 안 지워진 것.
**자체검증 (2026-09-09 밤, 데스크탑 메인)** — 코드·시험 대조만 하고 손대지 않음.
· 자리 = **B08 한 벌**. `B08_Quantity_Engine_UnitQuantity.stone_weight_per_m2` 하나를
돌쌓기·골막이(같은 파일)와 기슭막이·바닥막이(`_Revetment`)가 함께 부름 — 두 벌이 아님.
· 갈래 = **계산식이 기본**(`0.77 × 2.65`), **야면석 계열만 관측표**. 야면석은 공극이 커
같은 식이 안 맞아 원문 값을 그대로 씀(35→0.575 · 45→0.880 · 55→1.100 ton/㎡ ·
돌붙임(메) 0.42 ton/㎡ — 울진 소광 원단위 라이브러리 L103).
· 잠정 표시 = 줄 근거에 「⚠ 잠정 — 실무자 협의 후 명확히 할 값」이 붙어 화면까지 나감.
· 시험 = `test_b08_coeff_and_strength.py` 둘(관측표 갈래 · 계산식 갈래) 통과.
**남는 자리 하나** — 야면석은 뒷길이 **25·30·60·75 에 값이 없어** 그 규격에서는 돌 줄이
안 서고 사유만 남음(원본에 없는 칸을 지어내지 않는 규칙 그대로). 돌붙임 0.42 를 끌어다
메우고 싶어지는 자리인데 **밑수가 다른 표**(돌쌓기 비탈면적 ↔ 돌붙임 평면적)라 안 합침.
- [x] `[잔손질]` **공구손료 칸** — 「지금은 안 넣되 숫자 넣으면 되게 열어 둘 것」.
⇒ B09 **기초자료 탭 「산출 조건」에 칸 하나**를 세움 — `공구손료·잡재료 (주재료비의) [ ]% [적용]`.
근거는 **산림품셈 1-2-6**(주재료비의 2~5%까지, 산정 근거 명시) — 화면에 원문을 그대로 붙임.
**자체검증 (2026-09-09 밤, 데스크탑 메인)**
· 값이 **비면 줄 자체가 안 섬** — 금액 한 원도 안 움직임(시험 `test_비면_한_원도_안_움직인다`).
· 넣으면 **재료비**로 붙고 밑수는 **자재 줄만** — 노무·경비, 하위 일위대가 재료비는 안 듦.
· **상한 5% 초과는 거절**(400) — 조용히 깎아 넣지 않음. 화면에 사유가 그대로 뜸.
· 화면 왕복 실측(공용 브라우저 새 탭, 프로젝트 `79da822d`) — 빈 칸 → 3% [적용] →
「지금 3% 로 붙고 있습니다」 → 6% [적용] → 「⚠ … 5% 까지입니다 … 받은 값 6%」 →
칸 비우고 [적용] → 「비어 있어 안 붙고 있습니다」. **끝나고 빈 칸으로 되돌려 놓음**
(`project_settings.json``misc_material_percent` = `""` 확인).
· 시험 8개 신설(`resources/tester/test_b09_misc_material.py`), 전체 **1189 통과 · 실패 0**.
**지금은 넣어도 붙을 밑수가 없음** — 일위대가에 **주재료비가 선 공종이 0개**임(자재는
자재대 표에서 따로 금액이 섬 · 사급 단가는 확정 5차 큰 것 8 로 대기 중). 그 사실을
**칸 밑에 그대로 띄움** — 안 그러면 값을 넣고도 왜 안 붙는지 모름. 사급 단가가 서는 날
이 칸이 함께 살아남.
**곁다리로 고친 것** — 비율 줄(제잡비·공구손료)이 **저장했다 다시 읽으면 사라지고 있었음**
(`B09_Estimation_Storage``percent_of_parent` 만 적고 있었음). 둘 다 적도록 고침.
- [x] `[반나절]` **유가 지역 단가** (2026-09-09 밤, 데스크탑 메인) — 화면에서 시도를 고르면
그 지역 공시가로 **기계 연료비가 다시 섬**. 근거는 품셈 8-1-7 5호.
**「자료가 다 있음」은 사실이 아니었음** — 받아 둔 것은 **전국평균 한 벌**뿐이었고
시도별 값은 아예 안 받아 와 있었음(`_meta.md` 의 「지역별 조회 **지원**」은 API 가 된다는
말이지 자료가 있다는 말이 아니었음). 그래서 **자료부터 받고** 코드를 이었음.
**자체검증 (2026-09-09 밤)**
· 받은 것 = 오피넷 `avgSidoPrice.do` 시도별 판매가(휘발유·경유) 스냅샷 →
`original/원가계산/유가_오피넷/유가_시도별_2026-09-09.json`,
프로그램용 데이터셋 `oil_regional_2026-09-09.json`(`_manifest.json` 에 등재).
수집 스크립트에도 그 호출을 더해 **다음에 또 받을 수 있게** 함.
· 화면 = 기초자료 탭 「환율및기초자료 ③」의 **「유가 적용 범위」 칸**. 전과 달리 잠겨 있지 않고
**전국 공시가 + 시도 16곳**(값을 함께 보임)을 고름. 고르면 저장되고 프로젝트에만 걸림.
· 화면 실측(공용 브라우저 새 탭, 프로젝트 `79da822d`) — 서울 고름 →
경유 **1,886.15 · 지역 공시가 · 2026-09-09 · oil_regional**, 되돌림 →
**1,846.39 · 전국 공시가 · 2026-08-14 · oil**. **끝나고 전국 공시가로 되돌려 놓음.**
· 값이 실제로 갈리는지 = 기계 시간당 사용료 `X-0101-0019` 전국 148,450.48 → 서울 149,603.52.
· 시험 6개 신설(`test_b09_fuel_region.py`), 전체 **1197 통과 · 실패 0**.
**원천의 시도 가름이 행정구역과 다름** — 17줄 중 `00`이 전국, `20`이 「전남광주」 한 줄이고
`07`(전남)·`16`(광주)은 없음. **원문 이름·코드를 그대로 두고 쪼개지 않음**(자료에 적어 둠).
**현장 소재지 자동 연동은 안 함** — 프로젝트의 「사업 지역」은 자유 글(「울진군 금강송면」)이라
시도 코드로 옮기려면 규칙을 지어내야 함. **사용자가 고르는 칸**으로 두었음.
자동으로 잇고 싶으면 프로젝트 등록에 **시도 칸**이 하나 있어야 함(다음 물음거리).
- [x] `[하루+]` **기계 수송비를 세웠음** (2026-09-09 밤, 데스크탑 메인 · 커밋 `fdc30ecf`)
「기계경비 = 기계손료 + 운전경비 + **수송비**」인데 셋째 몫이 통째로 없었음. `FP-10-04`
중기운반이 그 수송비이고 표가 「미판정」이라 **단가가 아예 안 서고** 있었음.
**세운 방법 — 원문 그대로**
· 산림품셈 10-4 사이클 — 트레일러(20TON) `t1=20·t3=20·t4=0.42` / 트럭(10.5TON)
`t1=10·t3=10·t4=5` · `㎝=t1+t2+t3+t4` · `N=60×0.9/㎝`, **단위는 회당**.
· 속도(t2) — 건설품셈 8-1-6의 2 나 이동속도표(도로 구분별). 원문이 「-」로 둔 칸
(트레일러·고속4차선)은 **지어내지 않고 사유로 냄.**
· 새 파일 `B09_Estimation_Transport.py`(190줄) — `UnitPrice.py` 가 이미 1,106줄이라 안 보탬.
**⚠ 우리가 정한 둘 — 원문이 안 정해 화면 근거에 그대로 적었음**
㉠ t2 속도를 **자주식 이동속도표**에서 가져옴(그 표는 자주로 이동할 때의 표라 쓰임이 꼭
같지는 않음) · ㉡ t2 를 **왕복**으로 봄(건설품셈이 「왕복 수송비」라 못 박음).
⇒ **실무자 확인이 붙으면 좋은 자리.**
**자체검증**
· 거리·도로 구분은 **설계 입력** — 비면 줄이 안 서고 사유만 남음(임의 거리 금지).
· 화면 실측(기초자료 → 산출 조건) — 편도 12㎞ · 사리도로(양호) 넣으니
**트레일러 227,838원/회 · 트럭 149,507원/회**. 비우니 「거리가 안 들어와 줄을 안
세웠습니다」로 되돌아감. **끝나고 두 칸 다 비워 놓음**(설정 파일 확인).
· 시험 9개 신설(`test_b09_transport.py`), 전체 **1225 통과 · 실패 0**.
**⚠⚠ 곁다리로 잡은 큰 것 — 운전경비 표가 페이지에서 잘려 39 기종이 통째로 버려지고 있었음**
8-4-3 표가 페이지를 넘기며 앞자리를 다시 안 적어(`0080 0100 …` 로 시작) 코드 열 판독이
통째로 실패했음. 그 안에 **수송 차량(2702 트레일러)** 이 들어 있었음.
**앞 표의 앞자리를 이어받되 카탈로그에 있는 코드일 때만** 받게 함. 운전경비 줄 **92 → 158**.
ⓘ 지금 서는 일위대가 값은 **하나도 안 바뀜**(그 기종들을 쓰는 임도 공종이 아직 없음) —
수송비를 세울 수 있게 된 것이 이 고침의 값어치임.
**아직 버려지는 표 하나** — 8-4-5 콘크리트기계는 규격 칸이 22, 나머지가 21 로 어긋나
그대로 버려 둠(억지로 맞추면 기종과 연료가 어긋나 붙음).
- [x] `[잔손질]` **미공표 직종 신뢰도 플래그** (2026-09-08 제기 → 2026-09-09 완료)
**자체검증** — 노임 자료의 `reliability` 기호를 읽어 **노임 제목·화면 요약**까지 이었음.
```
읽는 자리 `load_labor_reliability()` — 원문 기호 `*`·`**` 그대로. 정상 직종은 안 담음
싣는 자리 `PriceTitle.reliability` — 노임 제목 전부 + **조종원 줄**에도 같이 붙임
경고 자리 `build_summary``labor_reliability` → 화면 힌트 줄(⚠ 노란 글씨)
```
**금액을 막지 않음** — 값은 그대로 서고 「조사현장이 적다」는 사실만 나름.
**내역에 실제로 실린 것만** 경고함 — 카탈로그 40직종을 다 띄우면 쓰지도 않는 직종이
화면을 채움. 실측 결과 **둘**이 잡힘: `1037 벌목부 [*]` · `1050 일반기계운전사 [*]`
(지식DB 가 예로 든 임도 빈발 직종이 그대로 나옴).
시험 여섯을 새로 세움(`resources/tester/test_b09_labor_reliability.py`) — 원문 기호를
우리 낱말로 바꾸지 않는지 · 정상 직종이 안 담기는지 · 카탈로그 전체를 안 띄우는지까지 검.
전체 시험 **1182 통과 · 실패 0**.
> ⓘ **메모(할 일 아님)** — 불도저 19톤 이상은 암석작업 손료 보정 제외
> (`기계경비_산정 §3 [주]①`). 우리 불도저가 **정확히 19톤**이라 파쇄암·발파암에
> **+25%를 붙이면 안 됨.** 지금 안 붙이고 있어 맞음 — 보정을 넣는 날 걸릴 자리.
- [x] `[다시 셈 끝 · 2026-09-09 밤]` **표를 못 읽어 값이 안 서는 자리 — 다시 센 수**
**⚠ 세는 자리를 바꿨음** — 「표 몇 장이 안 읽히나」는 겁만 주고 할 일을 못 가림(안 읽히는 표
대부분이 조림·방제라 임도에 안 쓰임). **「내역에 실릴 줄인데 금액이 안 서나」로 세는 것이 맞음.**
**표 (마스터 기준)** — 전체 475 · 공종에 붙은 것 **456** · 안 붙은 것 **19**.
형태별로 요구량형 279 · **미판정 76** · 참조 66 · 계수 19 · 시공능력 16.
**일위대가** — 코드 **286** · 공종 기준 **143**, **286 전부 금액이 0 보다 큼**(빈 껍데기 없음).
**⚠⚠ 진짜 구멍 셋 — 내역에 실리기로 돼 있는데 단가가 아예 없음** (프로젝트 `79da822d` 인계 54줄 실측)
| 공종 | 물량 | 왜 안 서나 |
|---|---|---|
| `FP-09-16-01` **성토(노체포설)** | **13,150.2 ㎥** | 원문에 「= ㎥/시간」 식이 셋 다 있는데 **절 머리에 「(단위: …)」가 없어 마스터가 표를 못 붙임** — 그 공종에 붙은 표가 **계수(K·f·E) 한 장뿐** |
| `FP-09-17-01` **성토면다짐** | **12,350.3 ㎡** | 같은 까닭. 원문 L5368 「A = 77.7 ㎡/시간」이 있는데 **붙은 표가 0장** |
| `FP-10-12` **덤프 운반** | 174.6 ㎥ | 운반거리·적재용량·단위중량 — **설계 입력 대기**(아래 사토장 줄과 같은 것) |
**앞의 둘은 같은 병**이고 **자료도 원문도 다 있음**(매핑 파일 `basis_source` 가 원문 줄번호까지
적어 둠: L5322·L5336·L5363 · L5368). **표를 그 공종에 붙이는 일**만 남음 — 아래에 줄로 세움.
ⓘ 함께 나온 것 — 겉으로는 「단가 못 찾음」 열 줄인데 **일곱은 갈래로 서 있는 것**이었음
(`FP-10-11#토사` 처럼 `#갈래`로 섬). **갈래를 셈에 넣지 않으면 있는 금액도 없다고 셈.**
- [x] `[반나절]` **⚠⚠ 성토·성토면다짐에 단가가 서게 함** (2026-09-09 밤, 데스크탑 메인 · 커밋 `4a6254ba`)
**막힌 까닭이 하나가 아니라 둘이었음** — 앞서 「단위 표기」 하나로 짚었던 것은 절반만 맞았음.
| 공종 | 막혔던 까닭 | 푼 방법 |
|---|---|---|
| 9-16-1 **노체포설** | 계수표(K·f·E·㎝)는 붙었는데 **기종이 [주]에만 있어** 못 고름 ⇒ 시공능력 공식이 안 돎 | 흙깎기(9-3-2)와 **같은 장비 칸**을 둠 — 무한궤도 굴착기 0.7㎥([주]① 그대로), 화면에서 바꿀 수 있음 |
| 9-17-1 **비탈면 다짐** | **표가 한 장도 없음** — 값이 절 제목에 있음(「A = 77.7 ㎡/시간」) ⇒ 표만 보는 조립이 그 공종을 아예 안 봄 | **제목·[주]에만 있는 작업량**을 싣는 자리를 두고 원문 문구를 줄에 남김 |
**곁다리로 하나 더 나옴** — 부착 콤팩터 `0240-0007`**카탈로그에 이름이 비어** 있어
손료가 통째로 안 붙고 있었음. 이름 대신 **분류번호로도** 가리게 함(조합 사용 넷은 그대로,
목록을 넓힌 것이 아님). 그래서 본체 굴착기가 **조합 사용 층(잡재료 16%)** 으로 바뀌어 섬.
**자체검증 (프로젝트 `79da822d`)**
· 단가 — 성토 **4,522.9원/㎥** · 성토면다짐 **1,443.4원/㎡**. 단위는 원문 그대로(㎥·㎡).
· 금액 — 성토 **59,477,858원** · 성토면다짐 **17,827,251원** ⇒ **합 77,305,109원**이 새로 섬.
같은 셈으로 **세부 줄 금액 합 31,276,630 → 108,581,739원**(약 3.5배). ⚠ **작은 자리가 아님.**
· 화면 실측 — 설계내역서에 `2-5-1 성토 ㎥ 13,150.19 · 4522.9 · 59,477,858` ·
`2-6-1 성토면다짐 ㎡ 12,350.32 · 1443.4 · 17,827,251` 로 뜸(전에는 단가·금액 칸이 비었음).
· 시험 6개 신설(`test_b09_fill_and_slope_price.py`), 전체 **1214 통과 · 실패 0**.
**노체다짐(9-16-2)은 아래 줄로 옮겼음** — 단가는 세웠고 남은 것은 「셀 것인가」임.
- [x] `[하루+]` **`FP-10-04` 중기운반 · `FP-09-14-02` 다짐(플레이트 콤펙터) — 또 다른 식** — **둘 다 섰음**
(2026-09-09 밤 · 커밋 `fdc30ecf` · `6cf7de9e`)
· **중기운반** = 기계 수송비. `㎝=t1+t2+t3+t4 · N=60×0.9/㎝` 그대로. **거리·도로 구분을 넣으면 섬**
(편도 12㎞·사리도로 양호에서 트레일러 227,838원/회 · 트럭 149,507원/회).
· **플레이트 콤펙터** = 다짐 식 둘째. 롤러가 `V·W·E·D·f/N` 인데 콤펙터는 `A·N·H·f·E/P` 임.
**원문이 답까지 적어 두어**(「∙ = 4.26 ㎥/시간」) 표 계수로 셈한 값과 맞대는 시험을 둠.
단가 **14,509.5원/㎥**(손으로 미는 소형 장비라 ㎥당이 비쌈).
ⓘ 이제 우리가 아는 식은 넷임 — 굴착기 사이클 · 불도저 · **다짐 둘**(롤러·콤펙터) + 수송 사이클.
- [x] `[다시 셈 끝 · 2026-09-09 밤]` **어느 공종에도 안 붙은 표****19 그대로**(읽는 길이 셋 는 것과
무관함: 그 셋은 **붙은 표를 어떻게 읽나**의 이야기이고, 여기는 **붙었나 안 붙았나**의 이야기임).
**19 를 뜯어보니 임도에 쓰일 것은 하나뿐임.**
· **15장 = 조림·숲가꾸기 사례표**(풀베기·덩굴제거·솎아베기·산물수집 등 부록 2-3~2-17) — 임도 밖.
· **2장(`F0248`·`F0257`) = 표가 아니라 「…참조」 한 문장**이 표로 잘린 것.
· **1장(`F0364`) = 12-17-3 「무근진동기 제외」** [주] 조각.
· **⚠ 1장(`F0375`) = 12-27-1 지수판 설치** — 이것만 구조물에 쓰일 수 있음. 다만 **지금 인계에
지수판 줄이 없어** 값이 빠진 자리는 아님. 구조물에 지수판이 서는 날 함께 볼 것.
- [x] `[하루+]` **품의 할인·할증 26계열 — 칸을 세웠음** (2026-09-09 밤, 데스크탑 메인 · 커밋 `f6f51f33`)
**⚠⚠ 앞서 이 줄에 적혀 있던 「임도는 산지경사 할증이 늘 걸릴 자리로 보임」은 추정이었고,
원문을 다시 읽어 뒤집었음.** 다음 창이 또 붙이려 들지 않게 여기 적어 둠.
· 26계열의 **[주]가 거의 다 조림·숲가꾸기·방제 전용**을 지목함 — 1-4-1 「덩굴·뿌리제거 /
**어린나무가꾸기에 한하여**」 · 1-4-2 「**줄베기**」 · 1-4-3 「**소반별**」 ·
1-4-4·1-4-5(평균경사도) **드론 방제** · 1-4-9 「숲가꾸기·병해충방제 **작업로**」 ·
1-4-25 「도서·오지·민통선」.
· ⇒ **임도 토공에 붙이라는 지시가 원문에 없음.** 임의로 붙이면 근거 없는 할증임.
**붙는 자리 = 일위대가의 품(인력) 줄** (「어디서 붙이나」의 답)
근거 — 원문 제목이 「1-4. **품**의 할인·할증」이고 1-6 라 표가 직접노무비를 품셈으로 냄.
**B08 물량에 곱하면 자재·기계까지 부풀어** 그것이 곧 ㉠ 계열 이중계상임.
**자체검증**
· 화면 = 기초자료 → 「품의 할인·할증 (산림품셈 1-4) — 고른 것만 붙습니다」. 계열마다 고르는
칸 하나 + **[주] 원문 그대로**. 기본은 「안 고름」.
· 화면 실측 — 안 고름: 「한 계열도 안 골라 한 원도 안 움직이고 있습니다」 →
1-4-1 「6월∼9월 10%」 고름: 「지금 10% 가 품에 붙고 있습니다」, 일위대가 **육상 암절취(2~3m)
노무 67,664.8 → 74,224.1 · 합계 72,333.4 → 78,892.6** → 되돌리니 원래 문구.
**끝나고 안 고름으로 되돌려 놓음**(설정 파일 확인).
· 품이 늘면 **제잡비 밑수도 함께 늘도록** 줄 차례를 지킴(할증 줄이 제잡비보다 먼저).
· 밑수는 **사람 품만** — 기계 층 안의 조종원 노임은 안 셈(제잡비와 같은 규칙, 시험으로 못 박음).
· 시험 7개 신설(`test_b09_labor_surcharge.py`), 전체 **1234 통과 · 실패 0**.
**율 표는 24개임** — 1-4-24(「**장비품의** -20%」·방제 전용)와 1-4-26(율이 아니라
**적용시공량 규칙** `Q=B/2`)은 성격이 달라 이 칸에 안 담음. 버린 것이 아니라 다른 것임.
- [x] **공구손료·잡재료 2~5 % — 칸을 세웠음** (2026-09-09 데스크탑 메인, `83c842e3`)
9-11 둘째 줄과 **같은 일감이었음**(한 항목이 두 절에 서 있었음). B09 기초자료 탭
「산출 조건」에 칸이 서고 **기본은 빔** — 넣으면 주재료비의 %로 재료비에 붙음
(`B09_Estimation_PriceBook.py:155,209,248`). 몇 %인지는 사용자 몫으로 남음.
~~(2026-09-08) — `품셈_적용기준.md §1`~~
「명시 없을 시 **주재료비의 2~5 %** 별도 계상(산정 근거 명시)」.
**범위값이라 어느 값을 쓸지 확정 대기**임.
**2026-09-09 확인: 지금은 안 붙고 있음**(공구손료 층이 하나도 없음). 이중계상 걱정은
없고, **「2~5% 중 몇 %」 하나만 정하면 붙일 수 있음.**
**2026-09-09 밤: 칸을 세움**(9-11 둘째 줄) — B09 기초자료 탭 「산출 조건」. 기본은 빔.
남은 것은 **사용자가 몇 %인지 정하는 것**과, 붙을 밑수인 **주재료비가 일위대가에 서는 것**
(사급 자재 단가 — 확정 5차 큰 것 8).
- [x] `[하루+]` **설계서 구성표를 세웠음 — 「못 내는 다섯」을 다시 가름** (2026-09-09 밤 · 커밋 `e38b8323`)
**화면** — 원가계산에 **「설계서 구성」 탭**. 법이 적은 차례 그대로 열셋을 놓고 줄마다
**상태(있음·반쪽·없음) · 누가 만드나 · 어디서 나오나 · 못 내면 무엇이 필요한가**를 적음.
**⚠ 다시 가른 결과 — 「우리가 못 내는 다섯」이 아니라 「우리가 더 낼 것 셋」이었음**
```
있음 7 예산내역서 · 일위대가표 · 단가산출서(B09) · 수량계산서 · 자재총괄표 · 토적표(B08) · 목차(이 표)
남 몫 3 일반시방서 · 특별시방서 · 예정공정표 ← 설계자·발주청이 쓰는 문서
우리 3 공사설명서(없음) · 각종 중기경비계산서(반쪽) · 산출기초(반쪽)
```
· **법이 목차를 정했다고 열셋을 다 프로그램이 만들 것은 아님** — 시방서 둘은 표준시방서를
인용하고 현장 특기사항을 적는 문서이고, 예정공정표는 공기를 공종에 나누는 **설계 판단**임.
우리는 공사기간(일)만 받아 요율 구간에 쓰고, 그것으로 공정표를 지어내지 않음.
· **공사설명서는 값이 거의 다 있음** — 공사명·총공사비·유효폭·노견·설계속도·종단구배·
최소곡선반경. **받아야 할 것은 서식과 설계하중 표기**(실무 거창은 「DB-18(32.45T)」).
· ✅ **중기경비계산서 — 세웠음**(2026-09-09 밤 · 커밋 `23f9656e` · `3934bd8d`).
원가계산 → **중기** 탭의 목록표 아래에 **기종마다 한 장**. 취득가·내용시간·손료계수
(상각 750 + 정비 583 + 관리 478 = 1,811 처럼 원문 10⁻⁷ 그대로) → 시간당 손료 ·
주연료와 유가 · 조종원 일당과 시간당 환산 · 시간당 사용료 3분할을 차례로 보임.
**내역에 선 기종 17 장**만 냄(카탈로그 613 을 다 뿌리지 않음).
⚠ **부착 장비는 손료만 드는 것이 정상**이라 결함으로 안 셈(제 엔진이 없어 연료·조종원이
본체에 듦 — 건설품셈 제8장 [주]⑤).
**같은 기종이 두 장일 수 있음** — 조합 사용이면 잡재료가 16% 로 줄어 재료비가 달라짐
(굴착기 0.7 재료비 26,130 → 24,845). 갈래를 적어 두 장임을 밝힘.
**수송비는 이 장에 안 붙임** — 「회당」으로 서는 별개 공종이라 자리만 가리킴.
· 계산서 합계가 **일위대가가 실제로 쓰는 값과 같은지** 시험으로 못 박음(두 벌 방지).
**「설계서 구성」이 반쪽 → 있음**으로 바뀌어 **법이 정한 13 중 8 이 섬**
(우리가 더 낼 것 **둘** — 공사설명서 · 산출기초).
· **산출기초도 반쪽** — 근거 문구는 줄마다 있으나 **한 장으로 묶는 자리**가 없음.
**사방사업은 목록이 다름** — 사방사업법 [별표0] 은 **위치도·관계지적조서·공사원가계산서·
설계도면**을 더 두고 「예산내역서」 대신 「설계내역서」라 적음. 이 표는 임도(별표2) 기준임.
**자체검증** — 화면 실측으로 열셋이 차례대로 뜨는 것과 요약 한 줄을 확인. 시험 6개 신설,
전체 **1258 통과 · 실패 0**.
- [x] ~~`[하루+]` **⚠⚠ 설계서 목차가 법에 통째로 박혀 있었음 — 우리가 못 내는 것 다섯**~~ `[옛 기록]` (2026-09-08 별표2 원문)
**별표2 (5)(가)** — 「설계서는 **목차·공사설명서·일반시방서·특별시방서·예정공정표·예산내역서·
일위대가표·단가산출서·각종중기경비계산서·공종별 수량계산서·각종 소요자재총괄표·토적표·
산출기초** 순으로 작성한다」
- [x] `[반나절]` **제비율 데이터에 밑수·처리 규칙까지 실었음** (2026-09-09 밤, 데스크탑 메인)
**자체검증** — 금액은 한 원도 안 움직임(계산은 그대로, 데이터가 그 계산을 적게 됐을 뿐).
· 빠져 있던 밑수 한 칸(건설기계대여대금 지급보증수수료 = 직접공사비)을 채움.
이제 **밑수 칸이 빈 요율이 없음**(시험이 그것을 셈).
· `processing_rules` 로 실은 것 — 자르는 법(줄 원 단위 버림 · 관급자재대만 천원 올림) ·
안전관리비 A·B 두 식과 **1.2**(B 배수)·**1.1**(관급 부가세 제수) · **조달수수료 차감 없음** ·
이윤 밑수(순공사원가+일반관리비−재료비) · 규모 구간 판정(추정가격, 없으면 반복 수렴).
· **거울 시험 6개**(`test_b09_rate_rules_mirror.py`) — 데이터가 적은 밑수로 실제 금액이 서는지
비목마다 맞대 보고, 두 수(1.2·1.1)와 자르는 법도 코드와 대조함. **한쪽만 바뀌면 멈춤.**
· 전체 **1208 통과 · 실패 0** (랩탑 병합 뒤 한 번 더 돌려 같음).
**계산을 데이터로 옮긴 것이 아님** — 계산은 코드에 그대로 두고, 데이터가 **그 규칙을 적어
두게** 했음. 옮기려면 엔진을 다시 짜야 하고 그 자리에서 금액이 흔들림.
## 곁딸린 고아 근거 두 덩어리 (항목이 이미 닫혀 자리를 잃은 것)
```
마스터 공종 477 · 표 있는 것 364 · 밑수 단위가 선 것 167 ← 나머지 None
우리가 쓰는 공종 28 중 마스터가 아는 것 18 · 매핑이 채운 것 1 · ⚠ 모르는 것 9
FP-05-24 씨앗뿜어붙이기 · FP-09-03-02 기계(흙깎기) · FP-09-04 암절취 · FP-09-05 발파암
FP-09-16 노체 · FP-09-17-01 비탈면 다짐 · FP-10-11 불도저 운반 · FP-10-12 덤프 운반
FP-12-06 콘크리트 포장(인력시공)
```
**특히 운반 둘과 암절취·발파암** — 금액이 큰 자리임.
**통로는 냈음** — 매핑에 원문 밑수를 적을 수 있게 함(`basis_unit` + `basis_source` =
**어느 원문 줄에서 읽었는지**). **원문 줄만 오면 그날 붙음.**
**일곱이 원문 줄과 함께 확정됨**(2026-09-08 B09) — 기계(흙깎기) `㎥` L4704 ·
**암절취 `㎥` L4725·L4738** · **발파암 `㎥` L4744·L4777** · 노체 `㎥` L5322·L5336·L5363 ·
비탈면 다짐 `㎡` L5368(**성토면다짐이 ㎡ 인 근거**) · **불도저 운반 `㎥` L5919** ·
**덤프 운반 `㎥` L5928·L5971·L5985`**. **⇒ 걱정하던 넷(운반 둘·암절취·발파암)이 전부 `㎥` 로 확정.**
남은 둘 — 씨앗뿜어붙이기(원문에 단위 표기 자체가 없음) · 콘크리트 포장
(「(단위: 일당)」 = **품의 단위라 거절**).
**넓히다 규칙이 한 번 샜음** — 상위 절이 하위만 단위를 적는 경우를 채우게 넓혔더니
**「13. 돌공사」가 ton 으로** 나왔음(하위 열여섯 중 하나가 장 전체를 대표). 좁힌 규칙 —
㉠ 장 자체엔 안 줌 ㉡ **말한 하위가 둘 이상**이고 ㉢ 값이 하나로 모일 때만. 짝 시험 있음.
**단위 빈 일위대가 73 → 61.**
```
있음 예산내역서·일위대가표·단가산출서(B09) · 수량계산서·자재총괄표·토적표(B08)
없음 목차·공사설명서·일반시방서·특별시방서·예정공정표 ← 문서류(만들지부터 결정)
없음 ⚠ 각종중기경비계산서 ← 문서가 아니라 **산출물**
반쪽 산출기초 — 근거 문구는 있으나 별도 장이 없음
```
**「무엇을 내야 하나」가 법에 목록으로 있음.** 앞서 B09 가 찾은 **중기경비계산서**가
그 목록 안에 있는 것이 확인됨.
+4 -3
View File
@@ -1,9 +1,9 @@
---
type: completion-summary
status: stable
related_pages: ["[[B06_Section/B06_backend]]", "[[B07_DesignDetail/B07_standard_drawings_2026_09]]", "[[B08_Quantity/B08_overview_2026_09]]", "[[B09_Estimation/B09_overview_2026_09]]", "[[quantity_cost_contract]]"]
last_updated: 2026-09-09
source: ["docs/raw/plans/2026-09-09_plan_완료분_이관.md", "docs/raw/plans/2026-09-09b_plan_완료분_이관.md", "docs/raw/verification/2026-09-09_B06_화면검증_사토장과_V6V7.md", "docs/raw/verification/2026-09-09_완료분_확인으로_닫은것.md", "docs/raw/verification/2026-09-09b_확인으로_닫은것.md"]
related_pages: ["[[B06_Section/B06_backend]]", "[[B07_DesignDetail/B07_standard_drawings_2026_09]]", "[[B08_Quantity/B08_overview_2026_09]]", "[[B08_Quantity/B08_2026_09_09_completion]]", "[[B09_Estimation/B09_overview_2026_09]]", "[[B09_Estimation/B09_2026_09_09_completion]]", "[[quantity_cost_contract]]"]
last_updated: 2026-09-10
source: ["docs/raw/plans/2026-09-09_plan_완료분_이관.md", "docs/raw/plans/2026-09-09b_plan_완료분_이관.md", "docs/raw/verification/2026-09-09_B06_화면검증_사토장과_V6V7.md", "docs/raw/verification/2026-09-09_완료분_확인으로_닫은것.md", "docs/raw/verification/2026-09-09b_확인으로_닫은것.md", "docs/raw/verification/2026-09-10_계획서_8_9장_완료근거_이관.md"]
---
# 2026-09-09 완료 구현·검증
@@ -46,3 +46,4 @@ source: ["docs/raw/plans/2026-09-09_plan_완료분_이관.md", "docs/raw/plans/2
- 사용자가 완료 검증을 승인해 이번 위키 반영에서는 별도 교차검증을 생략했다.
- `two_stage_slope` 선택/결과 분리, 일부 단가·재료·표준도 제원은 후속 또는 미확보 상태다. [[standard_quantity_open_2026-09-09]] 참조.
- 8·9장 밤 작업의 상세 완료 근거와 미확보 경계는 [[B08_Quantity/B08_2026_09_09_completion]], [[B09_Estimation/B09_2026_09_09_completion]]에 분리했다.
File diff suppressed because one or more lines are too long
+3 -1
View File
@@ -1,7 +1,7 @@
---
type: index
status: stable
last_updated: 2026-09-09
last_updated: 2026-09-10
---
# Wiki Index
@@ -32,10 +32,12 @@ last_updated: 2026-09-09
- [[B06_Section/B06_backend|B06 Section]] — 횡단·유토곡선·확정; [[B06_Section/B06_pavement_revetment|물넘이포장]], [[B06_Section/B06_revetment_link_controls|기슭막이 연동·경사·단별 제어]], [[B06_Section/B06_culvert_link_trim|인접 측점 트림]].
- [[B07_DesignDetail/B07_frontend|B07 DesignDetail]] — 상세설계·CAD·도각·설계도면과 [[B07_DesignDetail/B07_standard_drawings_2026_09|구조물 표준도]] 관리.
- [[B08_Quantity/B08_overview_2026_09|B08 Quantity]] — 토공·구조물·자재 수량산출과 B09 인계.
- [[B08_Quantity/B08_2026_09_09_completion|B08 9월 9일 완료]] — 밑수·관 연장·돌 무게와 미확보 경계.
- [[B08_DesignDetail/B08_CAD_commands|B08 CAD 명령]] · [[B08_DesignDetail/B08_CAD_blocks|블록]] · [[B08_DesignDetail/B08_CAD_table_entity|표]] · [[B08_DesignDetail/B08_CAD_title_block|도각·표제란]].
- [[B08_DesignDetail/B08_CAD_usability_2026-09-01|B08 CAD 편의성]] — 편집 손실·확정 잠금·도면별 복구 구현 기록과 재검증 대기 상태.
- [[B08_DesignDetail/B08_cross_structure_sheets|B08 횡단 장]] · [[B08_DesignDetail/B08_drawing_masshaul_watershed|토적·유역도]].
- [[B09_Estimation/B09_overview_2026_09|B09 Estimation]] — 단가산출·예산내역·공사원가계산 5표 구현.
- [[B09_Estimation/B09_2026_09_09_completion|B09 9월 9일 완료]] — 단가·수송비·유가·할증·제비율·설계서 구성.
- [[B10_Payment/B10_frontend|B10 Payment]] · [[B11_Status/B11_frontend|B11 Status]] — 결제·진행상태 목업.
## 로그인 전·관리
+13 -1
View File
@@ -1,7 +1,7 @@
---
type: log
status: stable
last_updated: 2026-09-09
last_updated: 2026-09-10
---
## [2026-09-03] ingest | 상시계획 완료 항목 위치 정리
@@ -1416,3 +1416,15 @@ last_updated: 2026-09-09
- 전체 내부 링크 검사에서 신규 오류는 없고 기존 예시·과거 로그 링크 18건만 남았다.
- `graphify update .`는 Markdown 174개를 코드로 오인해 1,149노드로 팽창했으므로 자동 백업으로 복구했다.
- 기존 정제 그래프 992노드·805관계·197커뮤니티는 보존했으며 신규 문서 3개의 의미 노드 반영은 Graphify Markdown 증분 결함 해소 후 다시 수행해야 한다.
## [2026-09-10] ingest | 계획서 8·9장 완료근거 신규 유입분 위키화
- 상시계획서는 사용자 지시에 따라 조회·수정하지 않았고 `raw/plans/`에는 신규 파일이 없음을 확인했다.
- `raw/verification/2026-09-10_계획서_8_9장_완료근거_이관.md`의 완료 22건을 [[B08_Quantity/B08_2026_09_09_completion]]과 [[B09_Estimation/B09_2026_09_09_completion]]에 반영했다.
- 사용자 검증을 최종 근거로 채택했으며, 관 세부 결정·표준도 원단위·사급 단가·수송 전제·미완성 산출물은 완료 범위와 분리해 남겼다.
## [2026-09-10] lint+graphify | 8·9장 완료근거 위키 점검
- 위키 본문 197개에서 100줄 초과·frontmatter·status·page_id 오류는 모두 0건이다.
- `graphify update .`가 Markdown 8개를 코드로 오인해 1,162노드로 팽창했으므로 자동 백업에서 기존 정제 그래프 992노드·805관계·197커뮤니티를 복구했다.
- 신규 상세 문서 2개의 의미 노드 반영은 Graphify Markdown 증분 결함 해소 후 다시 수행해야 한다.
@@ -0,0 +1,42 @@
---
type: completion-detail
status: stable
page_id: B08_Quantity
related_pages: ["[[B08_Quantity/B08_overview_2026_09]]", "[[B09_Estimation/B09_2026_09_09_completion]]", "[[quantity_cost_contract]]"]
last_updated: 2026-09-10
source: ["docs/raw/verification/2026-09-10_계획서_8_9장_완료근거_이관.md"]
---
# B08 수량산출 — 2026-09-09 완료 근거
사용자 검증 완료 선언에 따라 8장 완료분을 반영했다. 값이 없는 항목은 추정하지 않는 기존 원칙을 유지한다.
## 밑수와 인계
- 밑수 단위 `인`·`공`은 앞에 숫자가 붙을 때만 인정한다. 방제·야면석·휘발유 오독은 제거됐고, `천공인부 1인당 1대`는 유지된다.
- 현재 내역 25줄은 모두 밑수 대조 대상이다. 가설창고에는 원문 근거와 함께 `개소` 밑수를 보강했다.
- 마스터 전체는 밑수가 없는 공종이 남지만 현재 미사용 공종이다. 새 공종을 쓸 때 원문 밑수를 매핑해야 한다.
- 관 연장은 B06 저장 시 측점 `design.pipe_length_m`에 기록되고 B08이 기존 `get_cross_section_designs` 경로로 읽는다.
## 구조물·수량 표시
- 구조물 수가 0이면 동바리 안내를 숨기고, 구조물이 있으면 기존 안내를 유지한다. 서버 수량과 금액은 바뀌지 않는다.
- 층따기 출력 사전의 밑수는 실제 계산과 품셈에 맞게 `㎥`로 바로잡았다. 화면 면적을 정본으로 두고 길이 칸을 곱해 체적을 낸다.
- 돌 무게는 기본적으로 `뒷길이 × 0.77 × 2.65`, 야면석 계열은 관측표, 돌붙임은 0.42 ton/㎡를 쓴다.
- 야면석 뒷길이 25·30·60·75는 원본 값이 없어 돌 수량을 만들지 않고 사유를 남긴다.
## 검증 기록
| 항목 | 기록된 확인 |
|---|---|
| `인` 밑수 | `test_b08_master_basis_person.py` 5건 통과 |
| 밑수 대조 | 실제 내역 25/25 대조, 잘못된 단위 차단 확인 |
| 돌 무게 | 관측표·계산식 갈래 시험 통과 |
| 관 연장 | B06 저장 경로와 B08 읽기 경로 확인 |
## 남은 경계
- 관 연장 정본 반영에는 해당 프로젝트에서 저장이 한 번 필요하다.
- 관 관련 ㉠·㉡·㉢ 사용자 결정은 이 완료 범위에 포함되지 않는다.
- 표준도 원단위 자체는 실무 구조물 일람표 자료 확보 대기다.
@@ -2,9 +2,9 @@
type: page-overview
status: stable
page_id: B08_Quantity
related_pages: ["[[quantity_cost_contract]]", "[[standard_drawing_cost_inputs]]", "[[B09_Estimation/B09_overview_2026_09]]", "[[mass_haul_diagram]]"]
last_updated: 2026-09-09
source: ["docs/raw/plans/2026-09-08_plan_B08_수량산출_완료일감.md", "docs/raw/verification/2026-09-08_V-3_B군_종단배수_금액까지.md", "docs/raw/verification/2026-09-08_V-4_상세제원_다른구조물.md", "docs/raw/verification/2026-09-08_V-4_뒷구간_제원이_갈래를_고르나.md"]
related_pages: ["[[quantity_cost_contract]]", "[[standard_drawing_cost_inputs]]", "[[B09_Estimation/B09_overview_2026_09]]", "[[mass_haul_diagram]]", "[[B08_Quantity/B08_2026_09_09_completion]]"]
last_updated: 2026-09-10
source: ["docs/raw/plans/2026-09-08_plan_B08_수량산출_완료일감.md", "docs/raw/verification/2026-09-08_V-3_B군_종단배수_금액까지.md", "docs/raw/verification/2026-09-08_V-4_상세제원_다른구조물.md", "docs/raw/verification/2026-09-08_V-4_뒷구간_제원이_갈래를_고르나.md", "docs/raw/verification/2026-09-10_계획서_8_9장_완료근거_이관.md"]
---
# B08 Quantity — 2026-09 수량산출
@@ -36,3 +36,4 @@ B08은 물량(㎥·㎡·m·개소)을 산출하고 B09에 공종코드·수량·
- 품셈 476표 공종 축, 자재총괄, B08→B09 인계, 준비공·구조물 원단위·화면 6탭이 완료 기록에 포함된다.
- 검증 기록의 전체 시험은 704 passed·24 skipped이며 기존 B05 코리도 실패 1건은 별도 문제로 분류됐다.
- 미확보 값은 추정하지 않고 `pending_user`, `basis_missing`, `EXPANDER_WITHHELD`로 드러낸다.
- 9월 9일 밑수·관 연장·돌 무게 후속 완료와 남은 결정 경계는 [[B08_Quantity/B08_2026_09_09_completion]]에 정리했다.
@@ -0,0 +1,56 @@
---
type: completion-detail
status: stable
page_id: B09_Estimation
related_pages: ["[[B09_Estimation/B09_overview_2026_09]]", "[[B08_Quantity/B08_2026_09_09_completion]]", "[[standard_quantity_open_2026-09-09]]"]
last_updated: 2026-09-10
source: ["docs/raw/verification/2026-09-10_계획서_8_9장_완료근거_이관.md"]
---
# B09 원가계산 — 2026-09-09 완료 근거
사용자 검증 완료 선언에 따라 9장 완료분을 반영했다. 기본값이 비어 있는 선택 항목은 자동 적용하지 않는다.
## 단가·밑수 완성
- 내역 25줄 모두 밑수 대조가 걸리며, 견적가격은 법정 4순위라는 안내와 업체명·견적일 기록 문구가 표시된다.
- 노체포설과 비탈면 다짐의 생산성 경로를 보강해 각각 4,522.9원/㎥, 1,443.4원/㎡가 섰다. 검증 프로젝트 합계 증가는 77,305,109원이다.
- `FP-10-04` 중기운반은 거리·도로 구분 입력 시 회당 수송비를 계산한다. `FP-09-14-02` 플레이트 콤펙터는 별도 다짐식을 쓴다.
- 어느 공종에도 붙지 않은 표 19개 중 현재 임도에 걸릴 후보는 지수판 설치 1개이며, 현재 인계에는 지수판 줄이 없다.
- 품셈 밑수 없는 미사용 공종은 매핑에 원문 근거가 들어오는 때 대조 대상이 된다.
## 가격·조건 입력
- 공구손료·잡재료는 주재료비의 2~5%를 입력하는 선택 칸으로 제공한다. 비우면 금액이 생기지 않고 5% 초과는 거절한다.
- 품의 할인·할증은 사용자가 고른 계열만 인력 품에 적용한다. 기본은 미선택이며 기계 조종원과 B08 물량에는 곱하지 않는다.
- 유가는 전국 공시가와 오피넷 시도별 스냅샷 중 사용자가 고른 범위를 프로젝트에 저장한다.
- 프로젝트 사업 지역은 자유문자열이므로 시도별 유가와 자동 연동하지 않는다.
- 미공표 직종의 `*`·`**` 신뢰도 기호를 노임 제목과 실제 사용 직종 경고에 그대로 전달하되 금액은 막지 않는다.
## 기계·제비율·산출물
- 기계 수송비는 산림품셈 10-4 사이클과 도로별 속도를 사용한다. 거리·도로 구분이 없으면 단가를 만들지 않는다.
- 운전경비 표의 이어진 분류번호를 카탈로그 코드로 제한해 복원했고 줄 수는 92에서 158로 늘었다.
- 제비율 데이터는 모든 밑수와 절사·안전관리비·이윤·규모구간 처리 규칙을 기록한다. 계산 엔진 자체는 옮기지 않았다.
- 설계서 구성 탭은 법정 13종의 상태·작성 주체·출처를 보여준다. 현재 프로그램 산출물은 8종이다.
- 중기경비계산서는 내역에 쓰인 기종 17종만 기종별로 제공하며 일위대가 사용액과 같은 계산을 쓴다.
## 검증 기록
| 범위 | 기록된 확인 |
|---|---|
| 공구손료·잡재료 | 빈 값 무변화, 3% 적용, 6% 거절, 저장 왕복 |
| 지역 유가 | 전국↔서울 선택과 기계 시간당 사용료 변화 |
| 수송비 | 편도 12㎞·사리도로에서 트레일러 227,838원/회, 트럭 149,507원/회 |
| 노체포설·비탈면 다짐 | 화면 단가·금액과 시험 6건 확인 |
| 할인·할증 | 미선택 무변화, 10% 선택·복원 확인 |
| 제비율 | 거울 시험 6건과 전체 1,208건 실패 0 기록 |
| 설계서 구성 | 법정 13종 순서와 중기경비계산서 합계 확인, 전체 1,258건 실패 0 기록 |
## 남은 경계
- 공구손료율 선택과 주재료비가 서는 사급 단가는 별도 결정·자료가 필요하다.
- 수송시간을 자주식 속도표와 왕복으로 해석한 두 전제는 실무자 재확인 후보다.
- 콘크리트기계 운전경비 표는 열 불일치 때문에 제외 상태다.
- 공사설명서와 산출기초는 아직 프로그램 산출물이 아니다. 일반·특별시방서와 예정공정표는 설계자·발주청 몫이다.
@@ -2,9 +2,9 @@
type: page-overview
status: stable
page_id: B09_Estimation
related_pages: ["[[quantity_cost_contract]]", "[[standard_drawing_cost_inputs]]", "[[B08_Quantity/B08_overview_2026_09]]"]
last_updated: 2026-09-09
source: ["docs/raw/plans/2026-09-08_plan_B09_원가계산_마감.md", "docs/raw/plans/2026-09-08_plan_B08_B09_설계합의.md"]
related_pages: ["[[quantity_cost_contract]]", "[[standard_drawing_cost_inputs]]", "[[B08_Quantity/B08_overview_2026_09]]", "[[B09_Estimation/B09_2026_09_09_completion]]"]
last_updated: 2026-09-10
source: ["docs/raw/plans/2026-09-08_plan_B09_원가계산_마감.md", "docs/raw/plans/2026-09-08_plan_B08_B09_설계합의.md", "docs/raw/verification/2026-09-10_계획서_8_9장_완료근거_이관.md"]
---
# B09 Estimation — 2026-09 원가계산
@@ -37,3 +37,4 @@ B09는 단가산출서·예산내역서·공사원가계산서를 만들며 B08
- 코드 경로는 완료로 기록됐지만 첫 “값이 채워진 B08 인계”와 철근구조물 타설 줄 실데이터 확인은 대기다.
- 사급 단가, 밑수 미확보 68건, 공급 구분·돌 규격·쌓기 방식은 외부 자료나 사용자 입력이 있어야 풀린다.
- 제잡비 밑수, 수량 표시 자릿수, 목재틀 등급, 석재 할증 적용 여부는 사용자 확정 전까지 잠정값 또는 미확정으로 유지한다.
- 9월 9일 생산성식·수송비·유가·할증·제비율·설계서 구성 후속은 [[B09_Estimation/B09_2026_09_09_completion]]에 정리했다.
@@ -215,10 +215,12 @@ def test_합계는_반올림하지_않음() -> None:
# ── 관급/사급 ───────────────────────────────────────────────────────
def test_안_정한_자재는_미분류로_드러남() -> None:
def test_안_정한_자재는_미정으로_드러남() -> None:
"""⚠ 표기에 **누가 정하는지**가 있어야 한다 — 「미분류」로만 적으면 우리가 못 만든 것처럼
읽힌다(2026-09-09 화면 확인). (`unknown`) 판정은 그대로다."""
table = build_table(원단위표(성분("야면석", "", 5.0)))
assert (table, "야면석")["supply"] == SUPPLY_UNKNOWN
assert (table, "야면석")["supply_label"] == "분류"
assert (table, "야면석")["supply_label"] == "정(발주기관 결정)"
assert table["missing_supply_materials"] == ["야면석"]
+87
View File
@@ -0,0 +1,87 @@
"""산출기초 — 줄에 달린 근거를 한 장으로 (2026-09-09).
별표2 (5)() 열셋째. **새로 세는 장이 아니다** 이미 줄마다 달려 있는 근거를 모은다.
겨누는 다섯
어느 판으로 계산했나 **장부(`_manifest.json`) 그대로** 읽는다(목록을 따로 적음)
고른 값만 싣는다 고른 칸은 기본값이라 줄이 선다
근거는 **줄에 달린 문구 그대로** 여기서 새로 짓지 않는다
채운 자리는 **사유째** 모은다(0 으로 때운 자리가 아님)
여기서 **값을 다시 계산하지 않는다** 일위대가가 값과 벌이 되면 된다
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from B09_Estimation.B09_Estimation_BasisSheet import ( # noqa: E402
basis_sheet,
chosen_conditions,
dataset_versions,
work_item_basis,
)
from B09_Estimation.B09_Estimation_UnitPrice import cached_build # noqa: E402
def test_판_목록은_장부를_그대로_읽는다() -> None:
manifest = json.loads(
(ROOT / "resources" / "data_cost_input_value" / "_manifest.json").read_text(
encoding="utf-8"
)
)
ledger = {str(row["file"]) for row in manifest["files"]}
rows = dataset_versions()
assert ledger <= {row["file"] for row in rows}
# 공종 마스터도 함께 실린다 — 품셈 원문을 정규화한 판이다.
assert any(row["dataset_id"] == "work_item_master" for row in rows)
for row in rows:
assert row["effective_date"], row
def test_고른_값만_싣는다() -> None:
assert chosen_conditions({}) == []
assert chosen_conditions({"misc_material_percent": "", "fuel_region": ""}) == []
picked = chosen_conditions({"fuel_region": "01", "transport_distance_km": "12"})
assert [row["value"] for row in picked] == ["01", "12"]
def test_품_할증은_고른_것을_풀어_적는다() -> None:
picked = chosen_conditions({"labor_surcharge": {"1-4-1": "1-4-1:0"}})
assert len(picked) == 1
assert "10%" in picked[0]["item"]
assert "작업시기" in picked[0]["value"]
def test_근거는_줄에_달린_문구_그대로다() -> None:
build = cached_build()
rows = {row["code"]: row for row in work_item_basis(build)}
assert rows, "근거가 달린 공종이 하나도 없습니다"
for code, row in rows.items():
notes = {
detail.note.strip()
for detail in build.book.details.get(code, [])
if detail.note and detail.note.strip()
}
# 줄에 없는 문구를 지어내지 않는다(공종 단위 출처 한 줄만 더 붙을 수 있다).
assert notes <= set(row["notes"])
def test_못_채운_자리는_사유째_모은다() -> None:
sheet = basis_sheet(cached_build(), {})
assert sheet["gaps"], "못 채운 자리가 하나도 없다고 나옵니다 — 셈이 이상합니다"
for row in sheet["gaps"]:
assert row["kind"] and row["code"] and row["reason"]
def test_값을_다시_계산하지_않는다() -> None:
"""⑤ 이 장에는 금액 칸이 없다 — 금액은 일위대가·내역서 몫이다."""
sheet = basis_sheet(cached_build(), {})
text = json.dumps(sheet, ensure_ascii=False)
assert "총액" not in text and "합계 금액" not in text
for row in sheet["work_items"]:
assert set(row) == {"code", "name", "unit", "notes"}
+12 -5
View File
@@ -60,13 +60,20 @@ def test_설계자_몫과_우리_몫을_가른다() -> None:
assert item["status"] == STATUS_MISSING
def test_반쪽은_반쪽이라_적는() -> None:
"""⚠ 2026-09-09 밤 — 중기경비계산서를 세우면서 **반쪽이 하나 줄었다**(기종마다 한 장).
def test_반쪽이_남지_않았() -> None:
"""⚠ 2026-09-09 밤 — 중기경비계산서·산출기초를 차례로 세우며 **반쪽이 다 없어졌다.**
남은 반쪽은 산출기초 하나 근거 문구는 줄마다 있으나 장으로 묶는 자리가 없다.
이제 우리 가운데 없음 공사설명서 하나이고, 그것은 **서식·설계하중 표기를
사용자에게 받아야** 서는 자리다.
"""
partial = {item["name"] for item in DESIGN_DOC_ITEMS if item["status"] == STATUS_PARTIAL}
assert partial == {"산출기초"}
assert partial == set()
ours_missing = {
item["name"]
for item in DESIGN_DOC_ITEMS
if item["owner"] == OWNER_PROGRAM and item["status"] != STATUS_READY
}
assert ours_missing == {"공사설명서"}
def test_서는_것은_어디서_나오는지_적혀_있다() -> None:
@@ -84,7 +91,7 @@ def test_못_내는_것에는_무엇이_필요한지_적혀_있다() -> None:
def test_요약이_셈과_맞는다() -> None:
data = design_doc_index()
assert data["counts"][STATUS_READY] == 8
assert data["counts"][STATUS_READY] == 9
assert sum(data["counts"].values()) == len(DESIGN_DOC_ITEMS)
ours_missing = [
item["name"]
@@ -0,0 +1,93 @@
"""검증 리스트 셋을 시험으로 못 박음 — V-11 · V-16 · V-17 (2026-09-09 밤).
사람이 봤다 두면 다음 사람이 봐야 한다. ** 것을 시험으로 옮긴다.**
V-11 인계 계약 검사가 ** 칸에도** 걸리나 계약 시험이 실제로 무는지(가드의 가드)
V-16 불도저 운반이 **금액으로** 서나 갈래(토사·리핑암)마다 단가가 서는지
V-17 공구손료가 **이중으로 붙나** 제잡비와 밑수가 겹치지 않는지
"""
from __future__ import annotations
import sys
from decimal import Decimal
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from B09_Estimation.B09_Estimation_PriceBook import PriceKind # noqa: E402
from B09_Estimation.B09_Estimation_UnitPrice import build_unit_prices, cached_build # noqa: E402
# ── V-11 ────────────────────────────────────────────────────────────
def test_v11_계약_검사는_양쪽으로_문다() -> None:
"""⚠ 가드의 가드 — 칸을 하나 늘리거나 줄이면 계약 시험이 **반드시** 걸려야 한다.
보내는 쪽은 보냈는데 받는 쪽이 읽는 자리는 양쪽 조용해서, 계약 시험이
무는지 자체를 시험으로 남긴다.
"""
from resources.tester.test_b08_handoff_contract import WORK_ITEM_KEYS
row = {key: None for key in WORK_ITEM_KEYS}
# ① 계약에 없는 칸을 늘리면 잡는다.
grown = dict(row, 새_칸="")
assert set(grown) - WORK_ITEM_KEYS == {"새_칸"}
# ② 계약에 있는 칸을 빠뜨려도 잡는다.
shrunk = dict(row)
shrunk.pop("quantity")
assert WORK_ITEM_KEYS - set(shrunk) == {"quantity"}
# ── V-16 ────────────────────────────────────────────────────────────
def test_v16_불도저_운반이_갈래마다_금액으로_선다() -> None:
"""⚠ 세워 놓고 한 번도 안 돌려 본 채로 두지 않는다 — 갈래마다 단가가 서야 한다."""
build = cached_build()
codes = [code for code in build.book.titles if code.startswith("B-FP-10-11#")]
assert len(codes) >= 2, codes
grounds = {code.split("#", 1)[1] for code in codes}
assert {"토사", "파쇄암"} <= grounds, grounds
for code in codes:
assert build.book.resolve(code).total > 0, code
def test_v16_덤프_운반은_아직_안_선다() -> None:
"""⚠ 안 서는 것도 사실대로 못 박는다 — 운반거리(설계 입력)를 기다리는 자리다."""
build = cached_build()
assert not [code for code in build.book.titles if code.startswith("B-FP-10-12")]
# ── V-17 ────────────────────────────────────────────────────────────
def test_v17_공구손료와_제잡비는_밑수가_겹치지_않는다() -> None:
"""⚠ 둘 다 「%로 붙는 줄」이라 겹치면 조용히 부푼다.
공구손료 = **주재료비** % 재료비 / 제잡비 = **노무비** % 경비.
밑수도 가는 자리도 달라 공종에 있어도 이중계상이 아니다.
"""
build = build_unit_prices(misc_material_percent=Decimal(3))
both = 0
for code, rows in build.book.details.items():
title = build.book.titles.get(code)
if title is None or title.kind is not PriceKind.UNIT_PRICE:
continue
has_overhead = any(row.percent_of_labor is not None for row in rows)
has_tool = any(row.percent_of_material is not None for row in rows)
if not (has_overhead and has_tool):
continue
both += 1
before = build.book.resolve(code)
# 제잡비는 경비로, 공구손료는 재료비로 간다 — 성분이 갈리므로 겹칠 수 없다.
assert before.expense >= 0 and before.material >= 0
assert both > 0, "제잡비와 공구손료가 함께 있는 공종이 하나도 없습니다"
def test_v17_지금은_공구손료가_한_원도_안_붙는다() -> None:
"""⚠ 오늘 상태를 못 박는다 — 칸이 비어 있고, 붙을 주재료비도 아직 0 이다."""
base = cached_build()
with_percent = build_unit_prices(misc_material_percent=Decimal(3))
for code, title in base.book.titles.items():
if title.kind is not PriceKind.UNIT_PRICE:
continue
assert base.book.resolve(code).total == with_percent.book.resolve(code).total, code
+1
View File
@@ -828,6 +828,7 @@ export const ui_locales_b2 = {
B09_Estimation_Tab_Supply: ["관급·사급", "Supplied Materials"],
B09_Estimation_Tab_BaseData: ["기초자료", "Base Data"],
B09_Estimation_Tab_DesignDoc: ["설계서 구성", "Design Document Set"],
B09_Estimation_Tab_BasisSheet: ["산출기초", "Calculation Basis"],
B09_Estimation_Group_Condition: ["공사 조건", "Project Conditions"],
B09_Estimation_Group_RateVersion: ["요율 판", "Rate Edition"],
B09_Estimation_Group_Supplied: ["관급자재", "Owner-Supplied"],