Merge remote-tracking branch 'origin/sub_desktop_1' into main_desktop_1
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
"""표준도(구조물도) 하단표 — **장 나눔**과 **표 조판**.
|
||||
|
||||
실무 원본은 울진소광 `07-구조도-소광리.xlsx`(보이는 22탭 + 숨긴 31탭). **탭 하나 = 장 하나**이고
|
||||
탭 안은 「위 치수조서 + 아래 수량산출서」다. 이 모듈이 만드는 것은 **아래쪽 표**다.
|
||||
|
||||
실무 표의 짜임(돌쌓기 계열 네 탭 실측, 2026-09-09)::
|
||||
|
||||
(단위 표시) m당
|
||||
공종 | 산 출 근 거 | 수량 | 단위
|
||||
1 | 면적 2 × 1 × 1.044 2.088 ㎡
|
||||
...
|
||||
11 | 잔토정리 터파기 − 되메우기 0.165 ㎥
|
||||
|
||||
찰쌓기는 11줄, 메쌓기는 8줄(채움콘크리트·모르터·물구멍이 빠짐)이다. 우리 B08 전개가 이미
|
||||
줄마다 `name · unit · amount · basis` 를 내므로 **여기서 새로 계산하지 않는다** — 접어서 낼 뿐이다.
|
||||
|
||||
⚠ **장 나눔 축은 「제원 조합」이다**(2026-09-09 사용자 확정 ⑨). 같은 종류라도 높이·기울기·
|
||||
뒷길이·돌 종류가 다르면 그림도 수량도 달라지므로 **장이 갈린다**. 기울기가 제원의 한 칸이라
|
||||
「기울기가 바뀌면 장이 갈린다」는 확정이 저절로 지켜진다.
|
||||
|
||||
⚠ **단위당 값**은 성분 수량을 `billing_quantity` 로 나눈 것이다. 실무 시트 머리의 `m당` ·
|
||||
`개소당` · `㎡당` 이 그 단위이고, **구조물마다 다르다**(통일하지 않음 — 4-1).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
#: 장을 가르지 **않는** 제원 칸 — 개소마다 다를 뿐 그림·단위수량을 안 바꾼다.
|
||||
#: 여기 없는 칸은 전부 장 나눔에 들어간다(모르는 칸을 빠뜨려 두 장이 한 장으로 합쳐지는 것보다,
|
||||
#: 장이 하나 더 서는 쪽이 안전하다 — 합쳐지면 값이 조용히 틀린다).
|
||||
PER_PLACE_OPTION_KEYS: frozenset[str] = frozenset(
|
||||
{
|
||||
"start_m",
|
||||
"end_m",
|
||||
"station",
|
||||
"station_m",
|
||||
"side",
|
||||
"length_m",
|
||||
"note",
|
||||
"memo",
|
||||
"label",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def billing_of(structure: dict[str, Any]) -> tuple[str, float]:
|
||||
"""이 장이 설 **단위와 그 수량**.
|
||||
|
||||
⚠ `billing_unit` 이 비어 있으면 「단위가 없다」가 아니라 **「m · 연장」**이라는 뜻이다
|
||||
(`StructureQuantity` 계약). 관측 원단위가 「개소당」·「㎡당」인 종류만 자기 단위를 채운다.
|
||||
이것을 「미정」으로 읽으면 **돌쌓기처럼 흔한 종류가 전부 단위 없는 장**이 된다.
|
||||
"""
|
||||
unit = str(structure.get("billing_unit") or "")
|
||||
if unit:
|
||||
return unit, float(structure.get("billing_quantity") or 0.0)
|
||||
return "m", float(structure.get("length_m") or 0.0)
|
||||
|
||||
|
||||
def _sheet_options(structure: dict[str, Any]) -> dict[str, Any]:
|
||||
"""장 나눔에 쓰는 제원만 남긴다."""
|
||||
options = structure.get("options") or {}
|
||||
return {
|
||||
key: value for key, value in sorted(options.items()) if key not in PER_PLACE_OPTION_KEYS
|
||||
}
|
||||
|
||||
|
||||
def sheet_key(structure: dict[str, Any]) -> str:
|
||||
"""제원 조합 하나를 가리키는 이름. 같은 값이면 같은 장이다."""
|
||||
payload = {
|
||||
"type_id": structure.get("type_id") or "",
|
||||
"height_m": round(float(structure.get("height_m") or 0.0), 3),
|
||||
"options": _sheet_options(structure),
|
||||
}
|
||||
return json.dumps(payload, ensure_ascii=False, sort_keys=True)
|
||||
|
||||
|
||||
def sheet_title(structure: dict[str, Any]) -> str:
|
||||
"""장 제목 — 「이름 H=2.0 1:0.3 뒷길이 45㎝」처럼 **무엇이 갈랐는지**가 보이게."""
|
||||
parts = [str(structure.get("name") or structure.get("type_id") or "구조물")]
|
||||
height = float(structure.get("height_m") or 0.0)
|
||||
if height > 0:
|
||||
parts.append(f"H={height:g}")
|
||||
options = structure.get("options") or {}
|
||||
slope = options.get("face_slope_ratio")
|
||||
if slope is not None:
|
||||
parts.append(f"1:{float(slope):g}")
|
||||
back = options.get("back_len_cm") or options.get("stone_back_length_cm")
|
||||
if back:
|
||||
parts.append(f"뒷길이 {int(back)}㎝")
|
||||
kind = options.get("stone_kind")
|
||||
if kind:
|
||||
parts.append(str(kind))
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def _unit_amount(amount: float, quantity: float) -> float | None:
|
||||
"""단위당 값. 셀 단위를 못 정했으면 **0 으로 나누지 않고 `None`** 을 낸다."""
|
||||
if quantity <= 0:
|
||||
return None
|
||||
return amount / quantity
|
||||
|
||||
|
||||
def _rows_of(structure: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""구조물 하나의 성분을 표 줄로 접는다 — 실무 시트의 `공종 | 산출근거 | 수량 | 단위`."""
|
||||
_unit, quantity = billing_of(structure)
|
||||
rows: list[dict[str, Any]] = []
|
||||
for index, component in enumerate(structure.get("components") or [], start=1):
|
||||
amount = float(component.get("amount") or 0.0)
|
||||
rows.append(
|
||||
{
|
||||
"no": index,
|
||||
"name": component.get("name") or "",
|
||||
# 실무 시트의 「산출근거」 칸 — B08 이 이미 사람이 읽는 문구로 낸다.
|
||||
"basis": component.get("basis") or "",
|
||||
"unit_amount": _unit_amount(amount, quantity),
|
||||
"amount": amount,
|
||||
"unit": component.get("unit") or "",
|
||||
# 값이 식에서 나왔나(derived) 관측 원단위표에서 왔나(observed) — 되짚기용.
|
||||
"basis_kind": component.get("basis_kind") or "",
|
||||
"source": component.get("source") or "",
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def build_standard_sheets(unit_table: dict[str, Any]) -> dict[str, Any]:
|
||||
"""B08 원단위 전개(`build_unit_table` 결과)를 **표준도 장 목록**으로 접는다.
|
||||
|
||||
⚠ 계산을 다시 하지 않는다 — 들어온 전개를 제원 조합으로 묶고 단위당으로 나눌 뿐이다.
|
||||
"""
|
||||
groups: dict[str, dict[str, Any]] = {}
|
||||
for structure in unit_table.get("structures") or []:
|
||||
key = sheet_key(structure)
|
||||
sheet = groups.get(key)
|
||||
if sheet is None:
|
||||
unit, _quantity = billing_of(structure)
|
||||
sheet = {
|
||||
"key": key,
|
||||
"title": sheet_title(structure),
|
||||
"type_id": structure.get("type_id") or "",
|
||||
"height_m": float(structure.get("height_m") or 0.0),
|
||||
"options": _sheet_options(structure),
|
||||
# 실무 시트 머리의 「m당」·「개소당」·「㎡당」. 종류마다 다르다 — 통일하지 않는다.
|
||||
"unit_label": f"{unit}당",
|
||||
"billing_unit": unit,
|
||||
"rows": _rows_of(structure),
|
||||
"members": [],
|
||||
"notes": list(structure.get("notes") or []),
|
||||
}
|
||||
groups[key] = sheet
|
||||
sheet["members"].append(
|
||||
{
|
||||
"structure_id": structure.get("structure_id"),
|
||||
"name": structure.get("name") or "",
|
||||
"start_m": structure.get("start_m"),
|
||||
"end_m": structure.get("end_m"),
|
||||
"length_m": float(structure.get("length_m") or 0.0),
|
||||
"billing_quantity": billing_of(structure)[1],
|
||||
}
|
||||
)
|
||||
for note in structure.get("notes") or []:
|
||||
if note not in sheet["notes"]:
|
||||
sheet["notes"].append(note)
|
||||
|
||||
sheets = sorted(groups.values(), key=lambda sheet: (sheet["type_id"], sheet["height_m"]))
|
||||
for sheet in sheets:
|
||||
sheet["member_count"] = len(sheet["members"])
|
||||
sheet["billing_total"] = sum(member["billing_quantity"] for member in sheet["members"])
|
||||
# ⚠ 단위당을 못 낸 줄 — 「값이 없다」를 숫자 0 으로 때우지 않고 이름으로 드러낸다.
|
||||
sheet["unpriced_rows"] = [
|
||||
row["name"] for row in sheet["rows"] if row["unit_amount"] is None
|
||||
]
|
||||
|
||||
return {
|
||||
"sheets": sheets,
|
||||
"sheet_count": len(sheets),
|
||||
# 왜 안 실렸는지 — 화면이 「구조물이 없다」와 「걸러졌다」를 가릴 수 있어야 한다.
|
||||
"structure_count": int(unit_table.get("structure_count") or 0),
|
||||
"pending_choices": list(unit_table.get("pending_choices") or []),
|
||||
}
|
||||
@@ -240,6 +240,47 @@ async def _designs_by_chainage(route_id: int) -> dict[int, dict[str, Any]]:
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{project_id}/standard-sheets")
|
||||
async def get_standard_sheets(project_id: UUID) -> JSONResponse:
|
||||
"""표준도(구조물도) **장 목록 + 하단표**.
|
||||
|
||||
⚠ 수량을 여기서 새로 셈하지 않는다 — B08 원단위 전개를 그대로 받아 **제원 조합으로 묶고
|
||||
단위당으로 접기만** 한다(계산 자리는 한 곳, CLAUDE.md 5장).
|
||||
"""
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Standard_Sheet import build_standard_sheets
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table as build_unit_table
|
||||
from B08_Quantity.B08_Quantity_Router_Material import _collect_structures
|
||||
|
||||
try:
|
||||
pool = get_db_pool()
|
||||
async with pool.acquire() as connection:
|
||||
stored_path = await get_project_storage_relative_path(connection, project_id)
|
||||
project_root = str(Path(resolve_stored_project_path(stored_path)).resolve())
|
||||
except Exception:
|
||||
logger.exception("B07 표준도 조회 실패(경로): project_id=%s", project_id)
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={"status": "error", "message": "프로젝트 저장 폴더를 찾지 못했습니다."},
|
||||
)
|
||||
|
||||
try:
|
||||
structures, names, skipped = await asyncio.to_thread(_collect_structures, project_root)
|
||||
unit_table = await asyncio.to_thread(build_unit_table, structures, names)
|
||||
except Exception:
|
||||
logger.exception("B07 표준도 전개 실패: project_id=%s", project_id)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"status": "error", "message": "구조물 원단위를 전개하지 못했습니다."},
|
||||
)
|
||||
|
||||
payload = build_standard_sheets(unit_table)
|
||||
payload["status"] = "success"
|
||||
payload["project_id"] = str(project_id)
|
||||
# 왜 안 실렸는지 — 「구조물이 없다」와 「걸러졌다」를 화면이 가릴 수 있어야 한다.
|
||||
payload["skipped_structures"] = skipped
|
||||
return JSONResponse(content=payload)
|
||||
|
||||
|
||||
@router.get("/{project_id}/design-drawings", response_model=DesignDrawingListResponse)
|
||||
async def get_design_drawing_list(
|
||||
project_id: UUID,
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
"""B09 원가계산 — **목록표·집계표** (사용자 확정 12번: 내야 할 표 16개 전체).
|
||||
|
||||
지금까지 안 내던 일곱 표 중 여섯이 여기서 난다.
|
||||
|
||||
A5-1 중기목록표 코드·명칭·규격·단위 · **합계·노무비·재료비·경비** · 비고
|
||||
A6 노무비목록표 코드·명칭·규격·단위 · **단가** · 비고
|
||||
A7 재료비목록표 〃
|
||||
A8 경비목록표 〃 (기계 취득가 `S-` 층이 여기 온다)
|
||||
A11 자원 집계표 코드·명칭·규격 · **수량** · 단위 · 단가 · **금액** · 비고
|
||||
— 노무비·재료비·경비·중기 네 벌
|
||||
|
||||
**서식은 지어내지 않았다** — 실무 내역서(영월 기번6 · 봉화 기번41)의 같은 이름 시트를
|
||||
그대로 옮겼다(2026-09-09 실측). 칸 이름·차례가 그 시트와 같다.
|
||||
|
||||
⚠ **새 계산이 아니다.** 목록표는 `PriceBook` 의 제목을 종류별로 늘어놓는 것이고,
|
||||
집계표는 **내역서에 이미 선 금액을 자원별로 되모으는 것**이다. 값을 여기서 다시 만들면
|
||||
내역서와 어긋난다(CLAUDE.md 5장 「같은 계산을 두 벌로 짜지 않는다」).
|
||||
|
||||
⚠ **집계표는 반올림**이다(단수 규칙 `RESOURCE_SUMMARY`). 내역서 본체는 절사라
|
||||
**두 표의 합이 원 단위로 어긋나는 것이 정상**이다 — 그 사실을 화면에 함께 낸다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from B09_Estimation.B09_Estimation_PriceBook import PriceKind
|
||||
from B09_Estimation.B09_Estimation_Rounding import (
|
||||
SUMMARY_MISMATCH_NOTE,
|
||||
OutputPlace,
|
||||
round_at,
|
||||
)
|
||||
from B09_Estimation.B09_Estimation_UnitPrice import UnitPriceBuild, cached_build
|
||||
|
||||
_ZERO = Decimal(0)
|
||||
|
||||
#: 목록표 한 장이 담는 종류. 실무 시트 이름 그대로 쓴다.
|
||||
LIST_KINDS: tuple[tuple[str, str, PriceKind], ...] = (
|
||||
("labor", "노무비목록표", PriceKind.LABOR),
|
||||
("material", "재료비목록표", PriceKind.MATERIAL),
|
||||
("expense", "경비목록표", PriceKind.MACHINE_BASE),
|
||||
)
|
||||
|
||||
|
||||
def _money(value: Decimal | None) -> str | None:
|
||||
return None if value is None else str(value)
|
||||
|
||||
|
||||
def catalog_list(build: UnitPriceBuild, kind: PriceKind) -> list[dict[str, Any]]:
|
||||
"""목록표 한 장 — 그 종류의 **기초단가 줄**을 코드 차례로 늘어놓는다.
|
||||
|
||||
⚠ 단가가 안 선 줄도 **빼지 않는다.** 빼면 「없는 것」과 「값을 못 구한 것」이 같아 보인다.
|
||||
"""
|
||||
rows: list[dict[str, Any]] = []
|
||||
for code in sorted(build.book.titles):
|
||||
title = build.book.titles[code]
|
||||
if title.kind is not kind:
|
||||
continue
|
||||
try:
|
||||
price: Decimal | None = title.adopted_price()
|
||||
note = ""
|
||||
except Exception as error: # 채택 슬롯이 비었다 — 값을 지어내지 않는다
|
||||
price, note = None, str(error)
|
||||
rows.append(
|
||||
{
|
||||
"code": code,
|
||||
"name": title.name,
|
||||
"spec": title.spec,
|
||||
"unit": title.unit,
|
||||
"unit_price_krw": _money(price),
|
||||
"note": note,
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def machine_base_list() -> list[dict[str, Any]]:
|
||||
"""경비목록표 — **기계 취득가격(천원)** 목록.
|
||||
|
||||
⚠ 내 `S-` 층과 **다른 값**이다. `S-` 는 「취득가 × 시간당 손료계수」라 **원/시간**이고,
|
||||
실무 경비목록표는 **취득가 그 자체를 천원 단위**로 싣는다(영월 실측:
|
||||
`S00104 불도저(무한궤도) 19톤 **천원** 184,499`). 손료를 여기 실으면 자릿수가 세 자리
|
||||
어긋난 채 「경비」로 읽힌다.
|
||||
"""
|
||||
from B09_Estimation.B09_Estimation_MachineCost import load_machine_catalog
|
||||
|
||||
catalog = load_machine_catalog()
|
||||
rows: list[dict[str, Any]] = []
|
||||
for code in sorted(catalog.machines):
|
||||
machine = catalog.machines[code]
|
||||
rows.append(
|
||||
{
|
||||
"code": f"S-{code}",
|
||||
"name": machine.name,
|
||||
"spec": machine.specification,
|
||||
"unit": "천원",
|
||||
"unit_price_krw": _money(machine.price_thousand_krw),
|
||||
"note": "" if machine.loss_coefficient_per_hour is not None else "손료계수 미확보",
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def machine_list(build: UnitPriceBuild) -> list[dict[str, Any]]:
|
||||
"""중기목록표 — 시간당 사용료를 **3분할까지** 보인다 (실무 시트와 같은 칸).
|
||||
|
||||
실무 서식: `X00205 굴삭기(무한궤도) 0.7㎥ 시간 96,843 = 노무 55,700 + 재료 18,015 + 경비 23,128`
|
||||
"""
|
||||
rows: list[dict[str, Any]] = []
|
||||
for code in sorted(build.book.titles):
|
||||
title = build.book.titles[code]
|
||||
if title.kind is not PriceKind.MACHINE_HOURLY:
|
||||
continue
|
||||
try:
|
||||
money = build.book.resolve(code)
|
||||
row = {
|
||||
"total_krw": _money(round_at(money.total, OutputPlace.UNIT_PRICE_ROW)),
|
||||
"labor_krw": _money(round_at(money.labor, OutputPlace.UNIT_PRICE_ROW)),
|
||||
"material_krw": _money(round_at(money.material, OutputPlace.UNIT_PRICE_ROW)),
|
||||
"expense_krw": _money(round_at(money.expense, OutputPlace.UNIT_PRICE_ROW)),
|
||||
"note": "",
|
||||
}
|
||||
except Exception as error: # 층이 덜 섰다 — 0 으로 안 때운다
|
||||
row = {
|
||||
"total_krw": None,
|
||||
"labor_krw": None,
|
||||
"material_krw": None,
|
||||
"expense_krw": None,
|
||||
"note": str(error),
|
||||
}
|
||||
rows.append(
|
||||
{"code": code, "name": title.name, "spec": title.spec, "unit": title.unit, **row}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def resource_summary(
|
||||
quantities: dict[str, Decimal],
|
||||
build: UnitPriceBuild | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""자원 집계표 — 공종 수량을 **자원별로 되모은다**.
|
||||
|
||||
`quantities` = `{공종코드: 수량}` (내역서가 쓰는 것과 같은 모양).
|
||||
한 자원이 여러 공종에 걸리면 **한 줄로 합친다** — 실무 시트가 그 모양이다.
|
||||
|
||||
⚠ **일위대가 안쪽을 한 겹만 편다.** 일위대가 → 자원(노무·자재·기계 사용료)까지가
|
||||
실무 집계표의 깊이다. 기계 사용료(`X-`)를 다시 손료·연료로 쪼개면 **중기 집계표와
|
||||
이중으로 세는 것**이 된다.
|
||||
"""
|
||||
prices = build or cached_build()
|
||||
book = prices.book
|
||||
#: 자원코드 → [수량, 제목]
|
||||
picked: dict[str, list[Any]] = {}
|
||||
missing: list[str] = []
|
||||
|
||||
for raw_code, quantity in quantities.items():
|
||||
code = raw_code if raw_code.startswith("B-") else f"B-{raw_code}"
|
||||
if code not in book.titles:
|
||||
missing.append(raw_code)
|
||||
continue
|
||||
amount = Decimal(str(quantity))
|
||||
for detail in book.details.get(code, []):
|
||||
if detail.percent_of_labor is not None or detail.percent_of_parent is not None:
|
||||
continue # 비율 줄은 자원이 아니다 — 경비로만 붙는다
|
||||
ref = detail.ref_code
|
||||
if ref == code:
|
||||
continue
|
||||
slot = picked.setdefault(ref, [_ZERO, book.titles.get(ref)])
|
||||
slot[0] += detail.quantity * amount
|
||||
|
||||
groups: dict[str, list[dict[str, Any]]] = {
|
||||
"labor": [],
|
||||
"material": [],
|
||||
"expense": [],
|
||||
"machine": [],
|
||||
}
|
||||
for ref, (amount, title) in sorted(picked.items()):
|
||||
if title is None:
|
||||
missing.append(ref)
|
||||
continue
|
||||
bucket = {
|
||||
PriceKind.LABOR: "labor",
|
||||
PriceKind.MATERIAL: "material",
|
||||
PriceKind.MACHINE_BASE: "expense",
|
||||
PriceKind.MACHINE_HOURLY: "machine",
|
||||
}.get(title.kind)
|
||||
if bucket is None:
|
||||
continue
|
||||
try:
|
||||
unit_money = book.resolve(ref)
|
||||
unit_price: Decimal | None = unit_money.total
|
||||
# ⚠ 집계표는 **반올림** — 내역서 본체(절사)와 원 단위로 어긋나는 것이 정상이다.
|
||||
money: Decimal | None = round_at(
|
||||
unit_money.total * amount, OutputPlace.RESOURCE_SUMMARY
|
||||
)
|
||||
note = ""
|
||||
except Exception as error:
|
||||
unit_price, money, note = None, None, str(error)
|
||||
groups[bucket].append(
|
||||
{
|
||||
"code": ref,
|
||||
"name": title.name,
|
||||
"spec": title.spec,
|
||||
"quantity": str(amount),
|
||||
"unit": title.unit,
|
||||
"unit_price_krw": _money(unit_price),
|
||||
"amount_krw": _money(money),
|
||||
"note": note,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"groups": groups,
|
||||
"missing": sorted(set(missing)),
|
||||
"note": SUMMARY_MISMATCH_NOTE,
|
||||
}
|
||||
|
||||
|
||||
def all_lists(build: UnitPriceBuild | None = None) -> dict[str, Any]:
|
||||
"""목록표 넷을 한 번에 — 화면이 탭 하나에서 다 쓴다."""
|
||||
prices = build or cached_build()
|
||||
return {
|
||||
"labor": catalog_list(prices, PriceKind.LABOR),
|
||||
"material": catalog_list(prices, PriceKind.MATERIAL),
|
||||
"expense": machine_base_list(),
|
||||
"machine": machine_list(prices),
|
||||
}
|
||||
@@ -217,6 +217,25 @@ async def list_unit_price_titles(project_id: UUID) -> JSONResponse:
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{project_id}/estimation/base-data")
|
||||
async def get_base_data_lists(project_id: UUID) -> JSONResponse:
|
||||
"""**기초자료 네 표** — 노무비·재료비·경비 목록표 + 중기목록표 (사용자 확정 12번).
|
||||
|
||||
별표2 설계서 구성에 드는 표들이라 **없으면 설계서가 성립하지 않는다.** 서식은
|
||||
실무 내역서(영월 기번6·봉화 기번41) 같은 이름 시트를 그대로 따랐다.
|
||||
"""
|
||||
from B09_Estimation.B09_Estimation_Lists import all_lists
|
||||
|
||||
try:
|
||||
return JSONResponse(content={"status": "success", **all_lists(cached_build())})
|
||||
except Exception:
|
||||
logger.exception("B09 기초자료 목록 실패: project_id=%s", project_id)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"status": "error", "message": "기초자료 목록을 못 만들었습니다."},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{project_id}/estimation/unit-prices/{code}")
|
||||
async def get_unit_price_detail(project_id: UUID, code: str) -> JSONResponse:
|
||||
"""일위대가 **본표** — 「무엇으로 이루어졌나」. 줄마다 원천·파고들기 표시가 붙는다."""
|
||||
|
||||
Reference in New Issue
Block a user