fix(B07): 표준도가 단면유형을 안 넘겨 기울기 판정이 통째로 꺼져 있던 것
표준도가 `build_table` 을 직접 부르면서 `section_modes` 를 안 넘겨, 성절토를 가를 근거가
없다며 **전 구조물이 종전값 1:0.3** 으로 섰음. 값이 없는 게 아니라 **안 넘긴 것**이었음.
`section_modes_from_designs` 한 벌을 그대로 씀(부르는 쪽마다 다시 짜면 B08 과 갈림).
노선은 `get_workflow_route_context` 로 찾고, 못 찾으면 빈 표 — 성토로 눅이지 않음.
⚠ 고치고 나니 **제목·그림이 표와 갈렸음** — 표는 판정된 1:0.35, 제목은 다시 판정하며
근거를 못 받아 1:0.3. 실측으로 잡았음. 그래서 **표를 만든 그 판정을 시트에 실어**
제목·그림이 같은 값을 쓰게 함(`face`·`face_reason`). 자리를 고르는 것도 B08 이 쓰는
`_section_mode_at` 과 판정 한 벌 `structure_face_role` 을 그대로 부름.
실측 (프로젝트 936be972) — 제목과 표가 **같은 값**
```
큰돌쌓기 H=2.5 제목 1:0.3 표 0.3 면적 2.6101
돌쌓기(메) H=2.0 제목 1:0.35 표 0.35 면적 2.1190 ← 메쌓기가 갈림
돌쌓기(찰) H=2.5 제목 1:0.3 표 0.3 면적 2.6101
옹벽 H=2.0 기울기 없음(판정 대상 아님)
```
표 산출근거에 판정 경로가 그대로 뜸 — 「품셈 13-4-4 [주]⑪ 표준경사 · 메쌓기 성토 ·
직고 2m ≤3m → 1:0.35 · right_cut · 자동(성토 쪽) → 성토면」.
앞서 `face_slope_ratio({}, wet=…)` 로 확인한 「근거 없음」은 `face` 를 안 준 호출이라
**정상 동작**이었음 — 함수가 성토로 안 눅이게 짠 결과.
자체검증 — 회귀 580 통과 · 0 실패. 세 라우터 모두 700줄 안(588 · 190 · 692).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -58,7 +58,15 @@ def slope_of(sheet: dict[str, Any]) -> tuple[float, str]:
|
||||
wet = options.get("bond") != "메쌓기"
|
||||
else:
|
||||
wet = True
|
||||
return face_slope_ratio(options, wet=wet, height_m=float(sheet.get("height_m") or 0.0))
|
||||
return face_slope_ratio(
|
||||
options,
|
||||
wet=wet,
|
||||
height_m=float(sheet.get("height_m") or 0.0),
|
||||
# ⚠ 표를 만든 그 판정을 그대로 넘긴다 — 여기서 다시 가르면 근거를 못 받아
|
||||
# 종전값으로 떨어지고 **표와 갈린다**(2026-09-09 실측).
|
||||
face=sheet.get("face"),
|
||||
face_reason=str(sheet.get("face_reason") or ""),
|
||||
)
|
||||
|
||||
|
||||
def section_points(height_m: float, slope_ratio: float) -> list[tuple[float, float]]:
|
||||
|
||||
@@ -204,7 +204,9 @@ def build_standard_drawing(drawing_id: str, label: str, payload: dict[str, Any])
|
||||
}
|
||||
|
||||
|
||||
def standard_payload(project_root: Path) -> dict[str, Any]:
|
||||
def standard_payload(
|
||||
project_root: Path, section_modes: dict[float, str] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""프로젝트 구조물을 제원 조합으로 묶은 장 목록. 실패해도 **빈 목록**을 낸다.
|
||||
|
||||
⚠ 늦게 부른다(함수 안 import) — B08 은 B05 를 부르고 B05 는 다시 B07 을 부를 수 있어
|
||||
@@ -216,18 +218,24 @@ def standard_payload(project_root: Path) -> dict[str, Any]:
|
||||
|
||||
try:
|
||||
structures, names, _skipped = _collect_structures(str(project_root))
|
||||
return build_standard_sheets(build_unit_table(structures, names))
|
||||
# ⚠ 단면유형을 넘겨야 성절토가 갈리고 표준경사 판정이 돈다. 안 넘기면 전 구조물이
|
||||
# 「가를 근거 없음」으로 떨어져 종전값 1:0.3 으로 선다(2026-09-09 실측).
|
||||
return build_standard_sheets(
|
||||
build_unit_table(structures, names, section_modes), section_modes
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("B07 표준도 장 목록 실패 — 빈 목록으로 둔다: %s", project_root)
|
||||
return {"sheets": [], "structure_count": 0}
|
||||
|
||||
|
||||
def sheet_items(project_root: Path) -> list[tuple[str, str]]:
|
||||
def sheet_items(
|
||||
project_root: Path, section_modes: dict[float, str] | None = None
|
||||
) -> list[tuple[str, str]]:
|
||||
"""좌측 목록에 설 `(도면 id, 이름)`. 장이 없으면 **빈 장 하나**를 남긴다.
|
||||
|
||||
구조물이 없다고 단추가 통째로 사라지면 「없어진 것」처럼 보인다 — 눌러서 사유를 읽게 한다.
|
||||
"""
|
||||
sheets = standard_payload(project_root).get("sheets") or []
|
||||
sheets = standard_payload(project_root, section_modes).get("sheets") or []
|
||||
if not sheets:
|
||||
return [(SHEET_ID_PREFIX, "표준도")]
|
||||
total = len(sheets)
|
||||
@@ -237,7 +245,12 @@ def sheet_items(project_root: Path) -> list[tuple[str, str]]:
|
||||
]
|
||||
|
||||
|
||||
def standard_drawing_for(project_root: Path, drawing_id: str, label: str) -> dict[str, Any]:
|
||||
def standard_drawing_for(
|
||||
project_root: Path,
|
||||
drawing_id: str,
|
||||
label: str,
|
||||
section_modes: dict[float, str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""프로젝트 구조물을 읽어 표준도 한 장을 만든다 — 라우터가 부르는 문.
|
||||
|
||||
⚠ 실패해도 도면은 연다 — 구조물 정본을 못 읽었다고 화면이 비면 사용자는 「고장」으로
|
||||
@@ -245,7 +258,7 @@ def standard_drawing_for(project_root: Path, drawing_id: str, label: str) -> dic
|
||||
⚠ 늦게 부른다(함수 안 import) — B08 은 B05 를 부르고 B05 는 다시 B07 을 부를 수 있어
|
||||
모듈 맨 위에서 부르면 맞물린다.
|
||||
"""
|
||||
payload = standard_payload(project_root)
|
||||
payload = standard_payload(project_root, section_modes)
|
||||
sheets = payload.get("sheets") or []
|
||||
# 이 도면 id 가 가리키는 장 하나만 남긴다 — 한 면에 한 장(2026-09-09, 그림이 붙으면서).
|
||||
total = len(sheets)
|
||||
|
||||
@@ -139,13 +139,24 @@ def _rows_of(structure: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
return rows
|
||||
|
||||
|
||||
def build_standard_sheets(unit_table: dict[str, Any]) -> dict[str, Any]:
|
||||
def build_standard_sheets(
|
||||
unit_table: dict[str, Any], section_modes: dict[float, str] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""B08 원단위 전개(`build_unit_table` 결과)를 **표준도 장 목록**으로 접는다.
|
||||
|
||||
⚠ 계산을 다시 하지 않는다 — 들어온 전개를 제원 조합으로 묶고 단위당으로 나눌 뿐이다.
|
||||
"""
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import _section_mode_at
|
||||
from common_util.common_util_structure_face_role import structure_face_role
|
||||
|
||||
groups: dict[str, dict[str, Any]] = {}
|
||||
for structure in unit_table.get("structures") or []:
|
||||
# ⚠ 성절토는 **표를 만든 그 판정**을 그대로 물고 온다 — 제목·그림이 다시 판정하면
|
||||
# 근거를 못 받아 종전값으로 떨어져 **표와 갈린다**(2026-09-09 실측: 표 1:0.35 ·
|
||||
# 제목 1:0.3). 자리를 고르는 것도 B08 이 쓰는 그 함수를 쓴다.
|
||||
mode = _section_mode_at(structure, section_modes)
|
||||
face, face_reason = structure_face_role(mode, (structure.get("options") or {}).get("side"))
|
||||
structure = {**structure, "face": face, "face_reason": face_reason}
|
||||
key = sheet_key(structure)
|
||||
sheet = groups.get(key)
|
||||
if sheet is None:
|
||||
@@ -156,6 +167,9 @@ def build_standard_sheets(unit_table: dict[str, Any]) -> dict[str, Any]:
|
||||
"type_id": structure.get("type_id") or "",
|
||||
"height_m": float(structure.get("height_m") or 0.0),
|
||||
"options": _sheet_options(structure),
|
||||
# 판정 근거 — 제목·그림이 표와 **같은 기울기**를 쓰게 하는 열쇠.
|
||||
"face": structure.get("face"),
|
||||
"face_reason": structure.get("face_reason") or "",
|
||||
# 실무 시트 머리의 「m당」·「개소당」·「㎡당」. 종류마다 다르다 — 통일하지 않는다.
|
||||
"unit_label": f"{unit}당",
|
||||
"billing_unit": unit,
|
||||
|
||||
@@ -31,6 +31,7 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
|
||||
use_company_templates,
|
||||
use_title_fields,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Standard import section_modes_of
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support import (
|
||||
CROSS_STANDARD_ID,
|
||||
LANDUSE_ID,
|
||||
@@ -270,7 +271,10 @@ async def get_design_drawing_list(
|
||||
try:
|
||||
route_id, project_root, longitudinal_path, bypass = await _confirmed_source(project_id)
|
||||
designs = await _designs_by_chainage(route_id)
|
||||
drawings = await asyncio.to_thread(_drawing_list, project_root, longitudinal_path, designs)
|
||||
modes = await section_modes_of(route_id)
|
||||
drawings = await asyncio.to_thread(
|
||||
_drawing_list, project_root, longitudinal_path, designs, modes
|
||||
)
|
||||
return DesignDrawingListResponse(
|
||||
project_id=str(project_id), route_id=route_id, drawings=drawings, dev_bypass=bypass
|
||||
)
|
||||
@@ -363,7 +367,12 @@ async def get_design_drawing(
|
||||
longitudinal = await asyncio.to_thread(_read_json, longitudinal_path)
|
||||
source_design = await asyncio.to_thread(plan_source, context, longitudinal, drawing_id)
|
||||
kind, label, drawing, confirmed, quantity_table = await asyncio.to_thread(
|
||||
_read_drawing, project_root, longitudinal_path, drawing_id, source_design
|
||||
_read_drawing,
|
||||
project_root,
|
||||
longitudinal_path,
|
||||
drawing_id,
|
||||
source_design,
|
||||
await section_modes_of(route_id),
|
||||
)
|
||||
return DesignDrawingResponse(
|
||||
project_id=str(project_id),
|
||||
|
||||
@@ -25,6 +25,40 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/projects", tags=["B07 Design Detail"])
|
||||
|
||||
|
||||
async def section_modes_of_project(project_id: UUID) -> dict[float, str]:
|
||||
"""프로젝트에서 노선을 찾아 단면유형 표를 낸다 — 노선을 모르면 빈 표."""
|
||||
from B06_Section.B06_Section_Repository import get_workflow_route_context
|
||||
from config.config_db import run_with_connection
|
||||
|
||||
try:
|
||||
context = await run_with_connection(get_workflow_route_context, project_id)
|
||||
route_id = int((context or {}).get("route_id") or 0)
|
||||
except Exception:
|
||||
logger.exception("B07 노선 조회 실패: project_id=%s", project_id)
|
||||
return {}
|
||||
return await section_modes_of(route_id) if route_id else {}
|
||||
|
||||
|
||||
async def section_modes_of(route_id: int) -> dict[float, str]:
|
||||
"""측점별 단면유형(`left_cut` 등) — 구조물이 **성토면인가 절토면인가**를 가르는 근거.
|
||||
|
||||
⚠ 이것을 안 넘기면 판정이 통째로 「가를 근거 없음」으로 떨어져 **전 구조물이 종전값
|
||||
1:0.3 으로 선다**(2026-09-09 실측). 값이 없는 것이 아니라 **안 넘긴 것**이었다.
|
||||
⚠ 표를 만드는 셈은 `section_modes_from_designs` 한 벌을 쓴다 — 부르는 쪽마다 다시
|
||||
짜면 B08 과 갈린다.
|
||||
"""
|
||||
from B06_Section.B06_Section_Repository import get_cross_section_designs
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import section_modes_from_designs
|
||||
from config.config_db import run_with_connection
|
||||
|
||||
try:
|
||||
designs = await run_with_connection(get_cross_section_designs, route_id)
|
||||
except Exception:
|
||||
logger.exception("B07 단면유형 조회 실패: route_id=%s", route_id)
|
||||
return {}
|
||||
return section_modes_from_designs(designs)
|
||||
|
||||
|
||||
class StandardSheetSpecRequest(BaseModel):
|
||||
"""표준도 장 하나의 제원. **빈 값(null)은 「정한 적 없음」**이라 그 칸을 지운다."""
|
||||
|
||||
@@ -68,7 +102,9 @@ async def put_standard_sheet_spec(
|
||||
content={"status": "error", "message": "프로젝트 저장 폴더를 찾지 못했습니다."},
|
||||
)
|
||||
|
||||
sheets = (await asyncio.to_thread(standard_payload, project_root)).get("sheets") or []
|
||||
modes = await section_modes_of_project(project_id)
|
||||
payload_sheets = await asyncio.to_thread(standard_payload, project_root, modes)
|
||||
sheets = payload_sheets.get("sheets") or []
|
||||
picked = next((s for s in sheets if s.get("key") == payload.sheet_key), None)
|
||||
if picked is None:
|
||||
return JSONResponse(
|
||||
@@ -135,9 +171,10 @@ async def get_standard_sheets(project_id: UUID) -> JSONResponse:
|
||||
content={"status": "error", "message": "프로젝트 저장 폴더를 찾지 못했습니다."},
|
||||
)
|
||||
|
||||
modes = await section_modes_of_project(project_id)
|
||||
try:
|
||||
structures, names, skipped = await asyncio.to_thread(_collect_structures, project_root)
|
||||
unit_table = await asyncio.to_thread(build_unit_table, structures, names)
|
||||
unit_table = await asyncio.to_thread(build_unit_table, structures, names, modes)
|
||||
except Exception:
|
||||
logger.exception("B07 표준도 전개 실패: project_id=%s", project_id)
|
||||
return JSONResponse(
|
||||
@@ -145,7 +182,7 @@ async def get_standard_sheets(project_id: UUID) -> JSONResponse:
|
||||
content={"status": "error", "message": "구조물 원단위를 전개하지 못했습니다."},
|
||||
)
|
||||
|
||||
payload = build_standard_sheets(unit_table)
|
||||
payload = build_standard_sheets(unit_table, modes)
|
||||
payload["status"] = "success"
|
||||
payload["project_id"] = str(project_id)
|
||||
# 왜 안 실렸는지 — 「구조물이 없다」와 「걸러졌다」를 화면이 가릴 수 있어야 한다.
|
||||
|
||||
@@ -113,6 +113,7 @@ def _drawing_list(
|
||||
project_root: Path,
|
||||
longitudinal_path: Path,
|
||||
designs: dict[int, dict[str, Any]] | None = None,
|
||||
section_modes: dict[float, str] | None = None,
|
||||
) -> list[DesignDrawingItem]:
|
||||
longitudinal = _read_json(longitudinal_path)
|
||||
station_by_chainage = _station_map(longitudinal)
|
||||
@@ -206,7 +207,7 @@ def _drawing_list(
|
||||
)
|
||||
# 표준도 — **제원 조합마다 한 장**이라 장이 나뉜다(2026-09-09). 구조물이 없어도 한 장은
|
||||
# 남긴다: 단추가 사라지면 「없어진 것」처럼 보이고 사유를 읽을 자리도 없어진다.
|
||||
for drawing_id, label in sheet_items(project_root):
|
||||
for drawing_id, label in sheet_items(project_root, section_modes):
|
||||
drawings.append(DesignDrawingItem(id=drawing_id, kind="standard", label=label))
|
||||
_store_drawing_numbers(project_root, drawings)
|
||||
return drawings
|
||||
@@ -409,6 +410,7 @@ def _read_drawing(
|
||||
longitudinal_path: Path,
|
||||
drawing_id: str,
|
||||
stored_design: dict[str, Any] | None = None,
|
||||
section_modes: dict[float, str] | None = None,
|
||||
) -> tuple[str, str, dict[str, Any], bool, dict[str, float | None] | None]:
|
||||
"""(kind, label, drawing, confirmed, quantity_table)를 반환한다.
|
||||
|
||||
@@ -442,8 +444,9 @@ def _read_drawing(
|
||||
return kind, label, saved, True, table
|
||||
if drawing_id == SHEET_ID_PREFIX or drawing_id.startswith(f"{SHEET_ID_PREFIX}_"):
|
||||
# 표준도 — 제원 조합 한 벌이 한 장. 도각은 두르지 않는다(2026-09-08 사용자 지시).
|
||||
label = dict(sheet_items(project_root)).get(drawing_id, "표준도")
|
||||
return "standard", label, standard_drawing_for(project_root, drawing_id, label), False, None
|
||||
label = dict(sheet_items(project_root, section_modes)).get(drawing_id, "표준도")
|
||||
drawing = standard_drawing_for(project_root, drawing_id, label, section_modes)
|
||||
return "standard", label, drawing, False, None
|
||||
|
||||
if drawing_id == COVER_ID:
|
||||
# 표지는 설계 자료를 쓰지 않는다 — 템플릿 한 장이 곧 도면이다.
|
||||
|
||||
Reference in New Issue
Block a user