fix(B08): 자기 감사 — 안 부르던 검사 넷 연결 + 표토 두께 입력 칸
㉘ 자기 감사. B09 에 겨눈 네 축을 내 것에 그대로 겨눔. 안 부르던 검사 — 시험에서만 불리고 있던 것들을 실제 경로에 이음 - verify_no_code_on_materials · verify_bill_flags → build_handoff 결과에 실음 - check_against_plan → 운반 표 옆에 haul_check 로 실음 (계획이 있을 때만) - 바로 위 주석이 「검사는 실제로 부른다」였는데 형제 둘이 놀고 있었음 표토 두께 — 서버·엔진은 받고 있었는데 화면에 넣을 칸이 없었음 - optionalNumberField 신설 (빈 값 = 「안 정함」, 0 과 구별) - null 을 저장에서 버리던 것 수정 (NULLABLE_SETTING_KEYS) — 한 번 넣으면 「안 정함」으로 못 되돌리던 자리 짝 시험 — 규칙은 있는데 시험이 없던 둘 - REFERENCE_MARKS (41건을 지웠던 그 규칙): 기준표는 잡고 「단 위」는 안 잡는 것을 양쪽으로 박음 - attachments_of: 칸이 있을 때만 줄이 서고 부모와 id 가 갈리는 것 시험: 662 passed · 24 skipped (B05 코리도 1건 기존 깨짐, 무관) · tsc 오류 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -775,7 +775,11 @@ def build_handoff(
|
||||
"excluded_row_count": sum(1 for row in work_items if not row["in_bill"]),
|
||||
}
|
||||
# ⚠ 검사는 **실제로 부른다** — 만들어 두고 안 부르면 없는 것과 같다.
|
||||
# 2026-09-08 ㉘ 자기 감사: 아래 줄 하나만 이어져 있고 형제 둘은 **시험에서만** 불리고
|
||||
# 있었다. B09 에서 같은 병(가드 둘이 놀고 있음)을 지적해 놓고 내 쪽도 같았다.
|
||||
result["ratio_math_warnings"] = verify_ratio_math(result)
|
||||
result["material_code_warnings"] = verify_no_code_on_materials(result)
|
||||
result["bill_flag_warnings"] = verify_bill_flags(result)
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import SummaryInput
|
||||
from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import build_table as build_summary_table
|
||||
from B08_Quantity.B08_Quantity_Engine_EarthworkTable import StationArea, build_table
|
||||
from B08_Quantity.B08_Quantity_Engine_HaulSummary import build_table as build_haul_table
|
||||
from B08_Quantity.B08_Quantity_Engine_HaulSummary import check_against_plan
|
||||
from B08_Quantity.B08_Quantity_Engine_Handoff import load_mapping
|
||||
from B08_Quantity.B08_Quantity_Engine_Preparation import build_table as build_preparation_table
|
||||
from B08_Quantity.B08_Quantity_Engine_HaulSummary import summary_input_rows
|
||||
@@ -85,6 +86,17 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse:
|
||||
table["haul"] = haul
|
||||
# 운반계획은 [저장]·[확정]에서 정본에 남는 값이다 — 아직 없으면 빈 표가 정직하다.
|
||||
table["haul_available"] = bool(plan)
|
||||
# ⚠ 검산을 **실제로 부른다** — 무대·도자·덤프 합이 운반계획 총량과 맞는가(8-7 ㉡).
|
||||
# 2026-09-08 ㉘ 자기 감사: 만들어 두고 시험에서만 부르고 있었다. 값을 막지는 않고
|
||||
# 차이만 실어 화면이 띄우게 한다 — 막으면 계획이 없는 정상 상태에서도 멈춘다.
|
||||
if plan:
|
||||
check = check_against_plan(haul, plan)
|
||||
table["haul_check"] = {
|
||||
"hauled_total_m3": check.hauled_total_m3,
|
||||
"plan_total_m3": check.plan_total_m3,
|
||||
"difference_m3": check.difference_m3,
|
||||
"by_equipment": check.details,
|
||||
}
|
||||
|
||||
table["summary"] = build_summary_table(
|
||||
SummaryInput(
|
||||
@@ -189,6 +201,11 @@ class QuantitySettingsBody(BaseModel):
|
||||
topsoil_thickness_m: float | None = None
|
||||
|
||||
|
||||
#: `None` 이 「안 정함」을 뜻하는 칸 — 저장에서 **버리지 않고 그대로 덮어쓴다**.
|
||||
#: 빈 문자열로 되돌리는 칸(시공법·타설 방식)과 달리 숫자 칸은 되돌릴 값이 `None` 뿐이다.
|
||||
NULLABLE_SETTING_KEYS = ("topsoil_thickness_m",)
|
||||
|
||||
|
||||
@router.put("/{project_id}/quantity/settings")
|
||||
async def save_quantity_settings(project_id: UUID, body: QuantitySettingsBody) -> JSONResponse:
|
||||
"""산출 조건을 정본에 남긴다 — [저장]이 부르는 자리.
|
||||
@@ -206,6 +223,12 @@ async def save_quantity_settings(project_id: UUID, body: QuantitySettingsBody) -
|
||||
content={"status": "error", "message": "프로젝트 저장 폴더를 찾지 못했습니다."},
|
||||
)
|
||||
values = {key: value for key, value in body.model_dump().items() if value is not None}
|
||||
# ⚠ `None` 을 통째로 버리면 **「안 정함」으로 되돌릴 길이 없다** — 한 번 넣은 값이
|
||||
# 영영 남는다(2026-09-08 ㉘ 자기 감사). 시공법·타설 방식은 빈 문자열로 되돌리지만
|
||||
# 숫자 칸은 되돌리는 값이 `None` 뿐이라, **화면이 보낸 것**만 골라 살린다.
|
||||
for key in NULLABLE_SETTING_KEYS:
|
||||
if key in body.model_fields_set:
|
||||
values[key] = getattr(body, key)
|
||||
if "concrete_placing_method" in values:
|
||||
method = values["concrete_placing_method"]
|
||||
# 「안 정함」으로 되돌릴 수 있어야 한다 — 빈 값이면 지운다(8-22 ② 와 같은 자리).
|
||||
@@ -225,7 +248,7 @@ async def save_quantity_settings(project_id: UUID, body: QuantitySettingsBody) -
|
||||
_save_quantity,
|
||||
root,
|
||||
values,
|
||||
("rock_methods", "material_supply", "concrete_placing_method"),
|
||||
("rock_methods", "material_supply", "concrete_placing_method") + NULLABLE_SETTING_KEYS,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("B08 설정 저장 실패(쓰기): project_id=%s", project_id)
|
||||
|
||||
@@ -65,6 +65,8 @@ export interface QuantitySettings {
|
||||
material_supply?: Record<string, { supply: string; install_by: string | null }>;
|
||||
/** 콘크리트 타설 방식. `null`·없음이면 **아직 안 정한 것**이고 화면이 기본값 안내를 띄운다. */
|
||||
concrete_placing_method?: string | null;
|
||||
/** 표토 두께(m). `null`·없음이면 **안 정한 것**이라 표토제거 줄이 「근거 없음」으로 선다. */
|
||||
topsoil_thickness_m?: number | null;
|
||||
}
|
||||
|
||||
export interface EarthworkTable {
|
||||
|
||||
@@ -84,6 +84,8 @@ async function saveQuantitySettings(projectId: string, draft: DraftSettings): Pr
|
||||
rock_methods: draft.rock_methods,
|
||||
material_supply: draft.material_supply,
|
||||
concrete_placing_method: draft.concrete_placing_method,
|
||||
// ⚠ `null` 도 그대로 보낸다 — 「안 정함」으로 되돌릴 길이 있어야 한다(시공법과 같은 규칙).
|
||||
topsoil_thickness_m: draft.topsoil_thickness_m,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -134,6 +136,37 @@ function numberField(label: string, value: number, onInput: (value: number) => v
|
||||
return row;
|
||||
}
|
||||
|
||||
/** 비워 둘 수 있는 숫자 칸 — **빈 값은 「안 정함」**이고 0 과 다르다.
|
||||
*
|
||||
* ⚠ `numberField` 로 두면 빈 칸이 0 으로 읽혀 「두께 0m」와 「안 정함」이 같아진다.
|
||||
* 표토제거처럼 **안 정하면 줄이 아예 안 서는** 값은 그 둘을 갈라야 한다.
|
||||
*/
|
||||
function optionalNumberField(
|
||||
label: string,
|
||||
value: number | null,
|
||||
step: string,
|
||||
onInput: (value: number | null) => void,
|
||||
): HTMLElement {
|
||||
const row = document.createElement("label");
|
||||
row.className = "b08-quantity__field";
|
||||
const name = document.createElement("span");
|
||||
name.textContent = label;
|
||||
const input = document.createElement("input");
|
||||
input.type = "number";
|
||||
input.className = "b08-quantity__input";
|
||||
input.min = "0";
|
||||
input.step = step;
|
||||
input.placeholder = L("B08_Quantity_Unset_Placeholder");
|
||||
input.value = value === null || value === undefined ? "" : String(value);
|
||||
// 자동저장은 만들지 않는다 — 입력은 캐시에만 남는다(CLAUDE.md 5장).
|
||||
input.addEventListener("input", () => {
|
||||
const text = input.value.trim();
|
||||
onInput(text === "" ? null : Number(text));
|
||||
});
|
||||
row.append(name, input);
|
||||
return row;
|
||||
}
|
||||
|
||||
/** 타설 방식 표기 — 코드가 아니라 사람이 읽는 이름으로 보인다. */
|
||||
const PLACING_LABELS: Record<string, string> = {
|
||||
ready_mixed: "레디믹스트",
|
||||
@@ -182,6 +215,8 @@ interface DraftSettings {
|
||||
rock_methods: Record<string, string>;
|
||||
// 콘크리트 타설 방식 — `""` 는 「안 정함」이고 저장에서 지워진다.
|
||||
concrete_placing_method: string;
|
||||
// 표토 두께(m) — `null` 은 「안 정함」. 정해야 표토제거 줄이 선다(품셈 9-15 [주]② 의 T).
|
||||
topsoil_thickness_m: number | null;
|
||||
// 자재별 관급/사급 — 표 안에서 줄마다 고른 값.
|
||||
material_supply: Record<string, SupplyChoice>;
|
||||
dirty: boolean;
|
||||
@@ -265,6 +300,22 @@ function buildQuantitySidePanel(
|
||||
}
|
||||
}
|
||||
|
||||
// ── 표토 두께 — 정해야 준비공 표토제거 줄이 선다(품셈 9-15 [주]② 의 「T : 표토두께(m)」) ──
|
||||
// ⚠ 2026-09-08 ㉘ 자기 감사: 서버·엔진은 이 값을 받고 있었는데 **화면에 넣을 칸이 없었다.**
|
||||
// 「죽은 칸」(넣어도 안 쓰임)의 반대 짝이다 — 쓰이는데 넣을 데가 없던 자리.
|
||||
panel.append(field(L("B08_Quantity_Side_Topsoil"), ""));
|
||||
panel.append(
|
||||
optionalNumberField(
|
||||
L("B08_Quantity_Side_Topsoil_Label"),
|
||||
draft.topsoil_thickness_m,
|
||||
"0.01",
|
||||
(value) => {
|
||||
draft.topsoil_thickness_m = value;
|
||||
draft.dirty = true;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// ── 콘크리트 타설 방식 — ⚠ **금액에 바로 걸리는 값**이라 잠정임을 조용히 두지 않는다 ──
|
||||
panel.append(field(L("B08_Quantity_Side_Placing"), ""));
|
||||
panel.append(
|
||||
@@ -503,6 +554,7 @@ export async function renderB08Quantity(root: HTMLElement): Promise<void> {
|
||||
application_ratios_pct: { ...(stored.application_ratios_pct ?? {}) },
|
||||
rock_methods: { ...((stored.rock_methods ?? {}) as Record<string, string>) },
|
||||
concrete_placing_method: (stored.concrete_placing_method as string) ?? "",
|
||||
topsoil_thickness_m: (stored.topsoil_thickness_m as number | null) ?? null,
|
||||
material_supply: { ...((stored.material_supply ?? {}) as Record<string, SupplyChoice>) },
|
||||
dirty: false,
|
||||
};
|
||||
|
||||
@@ -628,6 +628,9 @@ export const ui_locales_b2 = {
|
||||
B08_Quantity_Method_Unset: ["안 정함", "Not set"],
|
||||
B08_Quantity_Method_Ripping: ["긁어내기(암절취)", "Ripping"],
|
||||
B08_Quantity_Method_Blasting: ["터뜨리기(발파암)", "Blasting"],
|
||||
B08_Quantity_Side_Topsoil: ["표토제거", "Topsoil Removal"],
|
||||
B08_Quantity_Side_Topsoil_Label: ["표토 두께(m)", "Topsoil thickness (m)"],
|
||||
B08_Quantity_Unset_Placeholder: ["안 정함", "Not set"],
|
||||
B08_Quantity_Side_Placing: ["콘크리트 타설", "Concrete Placing"],
|
||||
B08_Quantity_Side_Placing_Label: ["타설 방식", "Method"],
|
||||
B08_Quantity_Placing_Unset: ["안 정함(기본값 사용)", "Not set (default)"],
|
||||
|
||||
Reference in New Issue
Block a user