Merge remote-tracking branch 'origin/sub_laptop_1' into main_desktop_1

This commit is contained in:
2026-09-08 21:50:45 +09:00
5 changed files with 66 additions and 19 deletions
@@ -274,6 +274,7 @@ export function putStandardSheetSpec(
stone_supply: string | null; stone_supply: string | null;
back_len_cm: string | null; back_len_cm: string | null;
face_slope_ratio: string | null; face_slope_ratio: string | null;
foundation: string | null;
}, },
): Promise<{ status: string; revision: number; changed: number; notes: string[] }> { ): Promise<{ status: string; revision: number; changed: number; notes: string[] }> {
return requestJson(`/projects/${projectId}/standard-sheets/spec`, { return requestJson(`/projects/${projectId}/standard-sheets/spec`, {
@@ -11,12 +11,14 @@
⚠ **수량이 쓰는 그 상수로 그린다** — `B08_Quantity_Engine_UnitQuantity.STONE_MASONRY` 를 ⚠ **수량이 쓰는 그 상수로 그린다** — `B08_Quantity_Engine_UnitQuantity.STONE_MASONRY` 를
직접 읽는다. 치수를 여기서 다시 적으면 **그림과 표가 갈린다**(CLAUDE.md 5장). 직접 읽는다. 치수를 여기서 다시 적으면 **그림과 표가 갈린다**(CLAUDE.md 5장).
상부 두께 = 0.45 + 0.10·H 하부 두께 = 0.45 + 0.40·H 상부 두께 = 뒷길이 + 0.30 하부 두께 = 상부 + 0.30 × (H 1.0)
터파기 폭 = 평균두께 + 0.2 되메우기 두께 = 0.2 터파기 폭 = 평균두께 + 0.2 되메우기 두께 = 0.2
ⓘ 이 상수는 **뒷면이 수직인 1:0.3 벽**을 뜻한다 — 하부−상부 = 0.30·H 이고 전면이 뒤로 ⚠ **뒷길이가 두께에 들어간다**(확정 2차 ②, 실무 구조물도 식). 뒷길이가 다르면 벽이
기우는 양도 0.3·H 라 딱 맞는다. 기울기가 0.3 이 아니면 그 차이만큼 **뒷면이 기운다**. 두꺼워지고 그림도 그만큼 넓어진다 — 예전 식(0.45+0.10H / 0.45+0.40H)은 뒷길이를
지어낸 것이 아니라 상수에서 따라 나오는 결과라 그대로 그린다. 아예 안 봐서 35㎝ 든 45㎝ 든 같은 그림이 나왔다.
⚠ 뒷길이를 안 정한 장은 두께를 못 낸다 — 그때는 **그리지 않는다**(0 으로 때우면
거짓 그림이 된다).
⚠ **치수가 없는 것은 그리되 치수를 적지 않는다** — 막자갈(뒷채움)은 우리 식이 「입적에서 ⚠ **치수가 없는 것은 그리되 치수를 적지 않는다** — 막자갈(뒷채움)은 우리 식이 「입적에서
몸통·고임돌을 뺀 것」이라 폭·높이로 정의된 도형이 아니다. 자리만 보이고 값은 표를 가리킨다. 몸통·고임돌을 뺀 것」이라 폭·높이로 정의된 도형이 아니다. 자리만 보이고 값은 표를 가리킨다.
@@ -69,12 +71,30 @@ def slope_of(sheet: dict[str, Any]) -> tuple[float, str]:
) )
def section_points(height_m: float, slope_ratio: float) -> list[tuple[float, float]]: def wall_thickness(height_m: float, back_cm: float) -> tuple[float, float]:
"""벽 단면 네 점(m 단위, 밑면 앞끝이 원점). 앞면이 뒤로 `n·H` 기운다.""" """(상부, 하부) 두께 — **수량이 쓰는 그 식**(`stone_masonry`)과 같은 상수를 읽는다."""
top_t = STONE_MASONRY["thickness_base_m"] + STONE_MASONRY["thickness_top_coeff"] * height_m top_t = back_cm / 100.0 + STONE_MASONRY["thickness_top_add_m"]
bottom_t = ( bottom_t = top_t + STONE_MASONRY["thickness_slope_per_m"] * max(
STONE_MASONRY["thickness_base_m"] + STONE_MASONRY["thickness_bottom_coeff"] * height_m height_m - STONE_MASONRY["thickness_height_base_m"], 0.0
) )
return top_t, bottom_t
def back_length_cm(sheet: dict[str, Any]) -> float | None:
"""장의 뒷길이(㎝). 안 정했으면 `None` — 그때는 두께를 못 내므로 그리지 않는다."""
options = sheet.get("options") or {}
raw = options.get("back_len_cm") or options.get("stone_back_length_cm")
try:
return float(raw) if raw not in (None, "") else None
except (TypeError, ValueError):
return None
def section_points(
height_m: float, slope_ratio: float, back_cm: float
) -> list[tuple[float, float]]:
"""벽 단면 네 점(m 단위, 밑면 앞끝이 원점). 앞면이 뒤로 `n·H` 기운다."""
top_t, bottom_t = wall_thickness(height_m, back_cm)
lean = slope_ratio * height_m lean = slope_ratio * height_m
return [ return [
(0.0, 0.0), (0.0, 0.0),
@@ -101,7 +121,10 @@ def build_figure(
if height_m <= 0: if height_m <= 0:
return [], 0.0 return [], 0.0
options = sheet.get("options") or {} back_cm = back_length_cm(sheet)
if back_cm is None:
# 뒷길이가 없으면 두께를 못 낸다 — 0 으로 때우지 않고 그리지 않는다.
return [], 0.0
slope, slope_note = slope_of(sheet) slope, slope_note = slope_of(sheet)
scale = SCALE_MM_PER_M scale = SCALE_MM_PER_M
ox, oy = origin ox, oy = origin
@@ -109,11 +132,8 @@ def build_figure(
def mm(point: tuple[float, float]) -> tuple[float, float]: def mm(point: tuple[float, float]) -> tuple[float, float]:
return (ox + point[0] * scale, oy + point[1] * scale) return (ox + point[0] * scale, oy + point[1] * scale)
points = section_points(height_m, slope) points = section_points(height_m, slope, back_cm)
top_t = STONE_MASONRY["thickness_base_m"] + STONE_MASONRY["thickness_top_coeff"] * height_m top_t, bottom_t = wall_thickness(height_m, back_cm)
bottom_t = (
STONE_MASONRY["thickness_base_m"] + STONE_MASONRY["thickness_bottom_coeff"] * height_m
)
average_t = (top_t + bottom_t) / 2.0 average_t = (top_t + bottom_t) / 2.0
dig_width = average_t + STONE_MASONRY["excavation_extra_m"] dig_width = average_t + STONE_MASONRY["excavation_extra_m"]
backfill_t = STONE_MASONRY["backfill_thickness_m"] backfill_t = STONE_MASONRY["backfill_thickness_m"]
@@ -177,9 +197,7 @@ def build_figure(
), ),
("막자갈(뒷채움) — 수량은 아래 표", (dig_width + 0.16, height_m * 0.55), "left"), ("막자갈(뒷채움) — 수량은 아래 표", (dig_width + 0.16, height_m * 0.55), "left"),
] ]
back = options.get("back_len_cm") or options.get("stone_back_length_cm") labels.append((f"뒷길이 ℓ₃ = {int(back_cm)}", (dig_width + 0.16, height_m * 0.72), "left"))
if back:
labels.append((f"뒷길이 ℓ₃ = {int(back)}", (dig_width + 0.16, height_m * 0.72), "left"))
for index, (text, point, align) in enumerate(labels): for index, (text, point, align) in enumerate(labels):
x, y = mm(point) x, y = mm(point)
@@ -25,7 +25,18 @@ from typing import Any
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance from B05_Profile.B05_Profile_Structures_Schema import StructureInstance
#: 표준도에서 받는 칸 — `키 → (이름, 검사)`. 여기 없는 칸은 표준도가 안 만진다. #: 표준도에서 받는 칸 — `키 → (이름, 검사)`. 여기 없는 칸은 표준도가 안 만진다.
EDITABLE_KEYS: tuple[str, ...] = ("stone_kind", "stone_supply", "back_len_cm", "face_slope_ratio") EDITABLE_KEYS: tuple[str, ...] = (
"stone_kind",
"stone_supply",
"back_len_cm",
"face_slope_ratio",
"foundation",
)
#: 기초 갈래 — 정본 xls 탭 제목 그대로(`04.구조도(기슭막이).xls`).
#: ⚠ **물량과 그림이 같은 칸을 본다** — 터파기 기초 몫이 0.5×(0.7+0.2)=0.45 대
#: 0.1×(0.7+0.0)=0.07 ㎥/m 로 갈리고, 횡단도 터파기 선도 이 값으로 그려진다.
FOUNDATION_CHOICES: tuple[str, ...] = ("기초유", "기초버림")
#: 품셈 13-4-3·13-4-4 [주]① 의 일곱 규격. 그 밖의 값은 계수가 없어 물량이 안 선다. #: 품셈 13-4-3·13-4-4 [주]① 의 일곱 규격. 그 밖의 값은 계수가 없어 물량이 안 선다.
BACK_LENGTH_CHOICES: tuple[int, ...] = (25, 30, 35, 45, 55, 60, 75) BACK_LENGTH_CHOICES: tuple[int, ...] = (25, 30, 35, 45, 55, 60, 75)
@@ -39,6 +50,7 @@ FIELD_LABELS: dict[str, str] = {
"stone_supply": "조달", "stone_supply": "조달",
"back_len_cm": "뒷길이", "back_len_cm": "뒷길이",
"face_slope_ratio": "전면 기울기", "face_slope_ratio": "전면 기울기",
"foundation": "기초",
} }
@@ -95,6 +107,15 @@ def clean_spec(spec: dict[str, Any]) -> tuple[dict[str, Any], list[str]]:
"물량이 서지 않습니다." "물량이 서지 않습니다."
) )
if "foundation" in spec:
found = spec.get("foundation")
cleaned["foundation"] = str(found) if found not in (None, "") else None
if cleaned["foundation"] and cleaned["foundation"] not in FOUNDATION_CHOICES:
notes.append(
f"기초 「{cleaned['foundation']}」는 정본에 없는 갈래입니다 "
f"(있는 것: {' · '.join(FOUNDATION_CHOICES)})."
)
if "face_slope_ratio" in spec: if "face_slope_ratio" in spec:
ratio, note = _clean_slope(spec.get("face_slope_ratio")) ratio, note = _clean_slope(spec.get("face_slope_ratio"))
cleaned["face_slope_ratio"] = ratio cleaned["face_slope_ratio"] = ratio
@@ -70,6 +70,7 @@ class StandardSheetSpecRequest(BaseModel):
stone_supply: str | None = None stone_supply: str | None = None
back_len_cm: int | str | None = None back_len_cm: int | str | None = None
face_slope_ratio: float | str | None = None face_slope_ratio: float | str | None = None
foundation: str | None = None
@router.put("/{project_id}/standard-sheets/spec") @router.put("/{project_id}/standard-sheets/spec")
@@ -15,6 +15,8 @@
const STONE_KINDS = ["야면석·호박돌", "깬잡석", "깬돌", "견치돌"] as const; const STONE_KINDS = ["야면석·호박돌", "깬잡석", "깬돌", "견치돌"] as const;
const SUPPLIES = ["채집", "구입"] as const; const SUPPLIES = ["채집", "구입"] as const;
const BACK_LENGTHS = ["25", "30", "35", "45", "55", "60", "75"] as const; const BACK_LENGTHS = ["25", "30", "35", "45", "55", "60", "75"] as const;
/** 정본 xls 탭 제목 그대로 — 터파기 기초 몫 0.45 대 0.07 을 가르는 축. */
const FOUNDATIONS = ["기초유", "기초버림"] as const;
/** 표준도 장 하나 — 서버가 낸 것 중 이 폼이 쓰는 것만. */ /** 표준도 장 하나 — 서버가 낸 것 중 이 폼이 쓰는 것만. */
export interface StandardSheetSpec { export interface StandardSheetSpec {
@@ -31,6 +33,7 @@ export interface StandardSpecResult {
stone_supply: string | null; stone_supply: string | null;
back_len_cm: string | null; back_len_cm: string | null;
face_slope_ratio: string | null; face_slope_ratio: string | null;
foundation: string | null;
} }
/** 이 종류가 돌쌓기 계열인가 — 옹벽·집수정에는 이 칸들이 뜻이 없다. */ /** 이 종류가 돌쌓기 계열인가 — 옹벽·집수정에는 이 칸들이 뜻이 없다. */
@@ -106,6 +109,7 @@ export function buildStandardSpecPanel(
const kind = select(STONE_KINDS, options.stone_kind, "— 안 정함 —"); const kind = select(STONE_KINDS, options.stone_kind, "— 안 정함 —");
const supply = select(SUPPLIES, options.stone_supply, "— 안 정함(기본 채집) —"); const supply = select(SUPPLIES, options.stone_supply, "— 안 정함(기본 채집) —");
const back = select(BACK_LENGTHS, options.back_len_cm, "— 안 정함 —"); const back = select(BACK_LENGTHS, options.back_len_cm, "— 안 정함 —");
const foundation = select(FOUNDATIONS, options.foundation, "— 안 정함 —");
const slope = document.createElement("input"); const slope = document.createElement("input");
slope.className = "b07-spec__input"; slope.className = "b07-spec__input";
@@ -118,6 +122,7 @@ export function buildStandardSpecPanel(
field("돌 종류", kind), field("돌 종류", kind),
field("조달", supply, "비우면 「캔다」로 봅니다."), field("조달", supply, "비우면 「캔다」로 봅니다."),
field("뒷길이 (㎝)", back, "품셈 일곱 규격 밖이면 물량이 서지 않습니다."), field("뒷길이 (㎝)", back, "품셈 일곱 규격 밖이면 물량이 서지 않습니다."),
field("기초", foundation, "터파기 몫이 갈립니다 — 기초유 0.45 · 기초버림 0.07 ㎥/m."),
field( field(
"전면 기울기 1:n", "전면 기울기 1:n",
slope, slope,
@@ -157,6 +162,7 @@ export function buildStandardSpecPanel(
stone_supply: supply.value || null, stone_supply: supply.value || null,
back_len_cm: back.value || null, back_len_cm: back.value || null,
face_slope_ratio: slope.value.trim() || null, face_slope_ratio: slope.value.trim() || null,
foundation: foundation.value || null,
}), }),
); );
} catch (error) { } catch (error) {