Merge remote-tracking branch 'origin/dev' into main_laptop_1

This commit is contained in:
2026-09-13 15:11:46 +09:00
10 changed files with 24 additions and 270 deletions
+2 -50
View File
@@ -15,7 +15,6 @@ export interface DesignDrawingItem {
| "landuse"
| "plan_lidar"
| "cross_standard"
| "standard"
| "blank";
label: string;
chainage_m: number | null;
@@ -103,7 +102,6 @@ export interface DesignDrawingResponse {
| "landuse"
| "plan_lidar"
| "cross_standard"
| "standard"
| "blank";
label: string;
drawing: CadDrawing;
@@ -246,51 +244,5 @@ export function resetFrameTemplate(projectId: string): Promise<void> {
});
}
/** 표준도 장 목록 — 제원 입력 칸이 쓰는 것만 추린 꼴. */
export interface StandardSheetsResponse {
status: string;
sheet_count: number;
structure_count: number;
sheets: {
key: string;
title: string;
type_id: string;
member_count: number;
options: Record<string, unknown>;
}[];
}
// 창구는 B08 구조물도 — 2026-09-13 이관(PLAN 3장). 탭 배선 뒤 B07 표준도와 함께 지움.
export function fetchStandardSheets(projectId: string): Promise<StandardSheetsResponse> {
return requestJson(`/projects/${projectId}/quantity/structure-sheets`);
}
/** 장 하나의 제원 저장 — 빈 값(null)은 **그 칸을 지우라**는 뜻이다. */
export function putStandardSheetSpec(
projectId: string,
body: {
sheet_key: string;
base_revision: number;
stone_kind: string | null;
stone_supply: string | null;
back_len_cm: string | null;
face_slope_ratio: string | null;
foundation: string | null;
stone_coeff_basis: string | null;
fill_concrete_mpa: string | null;
thickness_top_m: string | null;
thickness_bottom_m: string | null;
blinding_concrete: string | null;
},
): Promise<{ status: string; revision: number; changed: number; notes: string[] }> {
return requestJson(`/projects/${projectId}/quantity/structure-sheets/spec`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
}
/** 구조물 정본 판번호 — 제원을 저장할 때 함께 보내야 다른 창 덮어쓰기를 막는다. */
export function fetchStructureRevision(projectId: string): Promise<{ revision: number }> {
return requestJson(`/projects/${projectId}/route/structures`);
}
// 표준도(구조물도) 장 목록·제원 저장 창구는 2026-09-13 B08 「구조물도」 탭으로 이관
// (`B08_Quantity_UI_StructureSheet.ts`).
@@ -12,12 +12,13 @@
⚠ 값을 여기서 셈하지 않는다 — `B08_Quantity_Engine_StructureSheet.build_standard_sheets` 가 낸 것을
글자로 옮길 뿐이다. 셈이 두 벌이 되면 도면과 수량서가 갈린다(CLAUDE.md 5장).
⚠ **2026-09-13 B07 도면 목록에서 빠짐**(사용자 확정 ③ — 구조물도는 B08 탭, PLAN 3장 ④).
목록·라우터 배선은 걷어냈고 표·그림 조립만 남김 — B08 구조물도 상단 그림 일감이 다시 쓸 자리.
"""
from __future__ import annotations
import logging
from pathlib import Path
from typing import Any
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
@@ -36,18 +37,8 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad_StandardFigure import (
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_StandardFigure import build_figure, figure_reason
from B07_DesignDetail.B07_DesignDetail_Engine_Template import usable_bbox
logger = logging.getLogger(__name__)
STANDARD_LAYER_ID = "standard"
#: 장 하나가 도면 하나 — 계획평면도와 같은 본을 따른다(`plan_lidar` / `plan_lidar_2`).
SHEET_ID_PREFIX = "standard_sheet"
def sheet_drawing_id(number: int, total: int) -> str:
"""장이 하나면 `standard_sheet`, 여럿이면 `standard_sheet_2` 처럼 번호를 붙인다."""
return SHEET_ID_PREFIX if total <= 1 and number <= 1 else f"{SHEET_ID_PREFIX}_{number}"
# 종이 밀리미터. 실무 시트가 「공종 | 산출근거 | 수량 | 단위」 넉 줄이라 그대로 간다.
# ⚠ **산출근거 칸을 넓게 잡는다** — 처음 176㎜ 로 냈더니 물구멍관·버림콘크리트처럼 단서가
@@ -223,65 +214,3 @@ def build_standard_drawing(drawing_id: str, label: str, payload: dict[str, Any])
"entities": entities,
"layers": [_layer(STANDARD_LAYER_ID, "표준도")],
}
def standard_payload(
project_root: Path, section_modes: dict[float, str] | None = None
) -> dict[str, Any]:
"""프로젝트 구조물을 제원 조합으로 묶은 장 목록. 실패해도 **빈 목록**을 낸다.
⚠ 늦게 부른다(함수 안 import) — B08 은 B05 를 부르고 B05 는 다시 B07 을 부를 수 있어
모듈 맨 위에서 부르면 맞물린다.
⚠ 장 목록은 **B08 구조물도 창구와 한 벌**(2026-09-13 이관) — 따로 전개하면 기초잡석 두께가 갈림.
"""
from B08_Quantity.B08_Quantity_Router_StructureSheet import project_structure_sheets
try:
# ⚠ 단면유형을 넘겨야 성절토가 갈리고 표준경사 판정이 돈다. 안 넘기면 전 구조물이
# 「가를 근거 없음」으로 떨어져 종전값 1:0.3 으로 선다(2026-09-09 실측).
return project_structure_sheets(str(project_root), section_modes)
except Exception:
logger.exception("B07 표준도 장 목록 실패 — 빈 목록으로 둔다: %s", project_root)
return {"sheets": [], "structure_count": 0}
def sheet_items(
project_root: Path, section_modes: dict[float, str] | None = None
) -> list[tuple[str, str]]:
"""좌측 목록에 설 `(도면 id, 이름)`. 장이 없으면 **빈 장 하나**를 남긴다.
구조물이 없다고 단추가 통째로 사라지면 「없어진 것」처럼 보인다 — 눌러서 사유를 읽게 한다.
"""
sheets = standard_payload(project_root, section_modes).get("sheets") or []
if not sheets:
return [(SHEET_ID_PREFIX, "표준도")]
total = len(sheets)
return [
(sheet_drawing_id(index, total), f"표준도 {index}장 ({sheet.get('title') or ''})".strip())
for index, sheet in enumerate(sheets, start=1)
]
def standard_drawing_for(
project_root: Path,
drawing_id: str,
label: str,
section_modes: dict[float, str] | None = None,
) -> dict[str, Any]:
"""프로젝트 구조물을 읽어 표준도 한 장을 만든다 — 라우터가 부르는 문.
⚠ 실패해도 도면은 연다 — 구조물 정본을 못 읽었다고 화면이 비면 사용자는 「고장」으로
읽는다. 그때는 빈 표 대신 사유가 적힌 한 줄이 뜬다.
⚠ 늦게 부른다(함수 안 import) — B08 은 B05 를 부르고 B05 는 다시 B07 을 부를 수 있어
모듈 맨 위에서 부르면 맞물린다.
"""
payload = standard_payload(project_root, section_modes)
sheets = payload.get("sheets") or []
# 이 도면 id 가 가리키는 장 하나만 남긴다 — 한 면에 한 장(2026-09-09, 그림이 붙으면서).
total = len(sheets)
picked = [
sheet
for index, sheet in enumerate(sheets, start=1)
if sheet_drawing_id(index, total) == drawing_id
]
return build_standard_drawing(drawing_id, label, {**payload, "sheets": picked})
+1 -6
View File
@@ -31,7 +31,6 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
use_company_templates,
use_title_fields,
)
from B08_Quantity.B08_Quantity_Router_StructureSheet import section_modes_of
from B07_DesignDetail.B07_DesignDetail_Router_Support import (
CROSS_STANDARD_ID,
LANDUSE_ID,
@@ -271,10 +270,7 @@ 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)
modes = await section_modes_of(route_id)
drawings = await asyncio.to_thread(
_drawing_list, project_root, longitudinal_path, designs, modes
)
drawings = await asyncio.to_thread(_drawing_list, project_root, longitudinal_path, designs)
return DesignDrawingListResponse(
project_id=str(project_id), route_id=route_id, drawings=drawings, dev_bypass=bypass
)
@@ -372,7 +368,6 @@ async def get_design_drawing(
longitudinal_path,
drawing_id,
source_design,
await section_modes_of(route_id),
)
return DesignDrawingResponse(
project_id=str(project_id),
@@ -48,11 +48,6 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Standard import (
STANDARD_LABEL,
build_standard_cross_drawing,
)
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_StandardSheet import (
SHEET_ID_PREFIX,
sheet_items,
standard_drawing_for,
)
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Table import (
QUANTITY_VALUE_KEYS,
)
@@ -113,7 +108,6 @@ 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)
@@ -205,10 +199,7 @@ def _drawing_list(
confirmed=bool(manifest_drawings.get(drawing_id, {}).get("confirmed")),
)
)
# 표준도 — **제원 조합마다 한 장**이라 장이 나뉜다(2026-09-09). 구조물이 없어도 한 장은
# 남긴다: 단추가 사라지면 「없어진 것」처럼 보이고 사유를 읽을 자리도 없어진다.
for drawing_id, label in sheet_items(project_root, section_modes):
drawings.append(DesignDrawingItem(id=drawing_id, kind="standard", label=label))
# 표준도(구조물도)는 2026-09-13 B08 「구조물도」 탭으로 이관 — 사용자 확정 ③, PLAN 3장 ④.
_store_drawing_numbers(project_root, drawings)
return drawings
@@ -410,7 +401,6 @@ 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,11 +432,6 @@ def _read_drawing(
stored_table = manifest_entry.get("quantity_table")
table = stored_table if kind == "cross" and isinstance(stored_table, dict) else None
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, 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:
# 표지는 설계 자료를 쓰지 않는다 — 템플릿 한 장이 곧 도면이다.
+2 -4
View File
@@ -20,8 +20,7 @@ class DesignDrawingItem(BaseModel):
"landuse",
"plan_lidar",
"cross_standard",
# standard: 표준도(구조물도) — 제원 조합 하나가 한 장.
"standard",
# 표준도(구조물도)는 2026-09-13 B08 「구조물도」 탭으로 이관 — kind 도 뺌.
"blank",
]
label: str
@@ -59,8 +58,7 @@ class DesignDrawingResponse(BaseModel):
"landuse",
"plan_lidar",
"cross_standard",
# standard: 표준도(구조물도) — 제원 조합 하나가 한 장.
"standard",
# 표준도(구조물도)는 2026-09-13 B08 「구조물도」 탭으로 이관 — kind 도 뺌.
"blank",
]
label: str
+2 -88
View File
@@ -38,23 +38,14 @@ import {
exportDrawing,
fetchDesignDrawing,
fetchDesignDrawingList,
fetchStandardSheets,
fetchStructureRevision,
invalidateDesignDrawing,
putStandardSheetSpec,
type CadDrawing,
type DesignDrawingItem,
type DesignDrawingResponse,
type QuantityTable,
type StandardSheetsResponse,
} from "./B07_DesignDetail_Api_Fetch";
import { appendStructureEntities } from "./B07_DesignDetail_UI_Cad_Structures";
import { createFrameTemplateEditor } from "./B07_DesignDetail_UI_FrameEdit";
// 제원 칸은 B08 구조물도로 이관(2026-09-13, PLAN 3장) — 탭 배선 뒤 B07 표준도와 함께 뺌.
import {
buildStandardSpecPanel,
type StandardSpecResult,
} from "../B08_Quantity/B08_Quantity_UI_StructureSheet_Spec";
/** CAD 앱 수량 패널로 넘기는 설계 컨텍스트 (openwebcad DesignMeta와 동일 형식). */
interface DesignMeta {
@@ -103,10 +94,6 @@ const CAD_TOAST_ACTION_MESSAGE = "aislo:b08:toast-action";
* -------------------------------------------------------------------------- */
export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
/** 표준도 장 목록 — 제원 폼이 쓰는 값. 진입 때 한 번 받고 저장 뒤 다시 받는다. */
let standardSheets: StandardSheetsResponse["sheets"] = [];
/** 저장 뒤 폼이 다시 그려질 때 이어서 보여 줄 안내. */
let specNotes: string[] = [];
let workflowState: WorkflowState | undefined;
let drawings: DesignDrawingItem[] = [];
let drawingError: string | undefined;
@@ -120,14 +107,6 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
if (drawingResult.status === "fulfilled") {
drawings = drawingResult.value.drawings;
devBypass = drawingResult.value.dev_bypass === true;
if (drawings.some((item) => item.kind === "standard") && projectId) {
// 표준도 제원 폼이 쓸 장 목록 — 실패해도 도면은 열려야 하므로 조용히 넘어간다.
try {
standardSheets = (await fetchStandardSheets(projectId)).sheets;
} catch {
standardSheets = [];
}
}
} else
drawingError =
drawingResult.reason instanceof Error
@@ -214,73 +193,8 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
const infoPanelHost = document.createElement("div");
infoPanelHost.className = "b07-info-host";
/**
* [여기서 고치기] — 도면 하단표에서 **값을 정하는 자리**로 데려간다(확정 6 ㉮
* 「한 곳에서 정하고 도면은 비추기만」). 표는 CAD 안에 그려져 단추를 못 달고, 제원 폼은
* 도면 목록 아래라 장이 많으면 화면 밖으로 밀린다 — 그래서 **고정 액션 줄**에 둔다
* (떠 있는 띠는 CAD 리본과 겹쳐 2026-09-02 에 걷어낸 자리라 다시 띄우지 않는다).
* ⚠ 값을 여기로 복사하지 않는다 — 폼 한 벌이 그대로 정본 창구다.
*/
const specJumpButton = createButton({
label: "여기서 고치기",
variant: "ghost",
onClick: () => {
infoPanelHost.scrollIntoView({ behavior: "smooth", block: "nearest" });
infoPanelHost.querySelector<HTMLElement>("select, input")?.focus();
},
});
specJumpButton.title = "이 표의 값을 정하는 제원 칸으로 갑니다.";
specJumpButton.hidden = true;
const showStandardSpec = (drawing: DesignDrawingItem): void => {
const index = Number(/_(\d+)$/.exec(drawing.id)?.[1] ?? "1") - 1;
const sheet = standardSheets[index];
if (!projectId || !sheet) {
infoPanelHost.replaceChildren();
specJumpButton.hidden = true;
return;
}
specJumpButton.hidden = false;
// 판정된 기울기는 **칸에 적지 않고 도움말로만** 비춘다 — 적어 두면 「안 정함」이 사라진다.
const judged = /1:([\d.]+)/.exec(drawing.label)?.[1] ?? null;
infoPanelHost.replaceChildren(
buildStandardSpecPanel(
sheet,
judged,
async (result: StandardSpecResult) => {
const { revision } = await fetchStructureRevision(projectId);
const saved = await putStandardSheetSpec(projectId, {
...result,
base_revision: revision,
});
// 정본이 바뀌었으니 표·그림을 **다시 받아** 그린다 — 화면이 두 번째 정본이 되면 안 된다.
drawingCache.delete(drawing.id);
standardSheets = (await fetchStandardSheets(projectId)).sheets;
// ⚠ 장 제목에 제원이 들어 있다 — 고쳤으면 **좌측 단추 글자도 따라가야** 한다.
// 목록을 통째로 다시 받지 않고 표준도 줄만 갈아 끼운다(2026-09-09 실화면에서 잡음).
for (const [order, item] of drawings.filter((d) => d.kind === "standard").entries()) {
const fresh = standardSheets[order];
if (!fresh) continue;
item.label = `표준도 ${order + 1}장 (${fresh.title})`;
const name = findButton(item.id)?.querySelector(".b07-drawing-button__name");
if (name) name.textContent = item.label;
}
specNotes = [`${saved.changed}개소에 반영했습니다.`, ...(saved.notes ?? [])];
await loadDrawing(drawing, currentIndex);
return specNotes;
},
specNotes,
),
);
specNotes = [];
};
// 표준도 제원 칸·[여기서 고치기]는 2026-09-13 B08 「구조물도」 탭으로 이관(PLAN 3장 ④).
const updateInfoPanel = (drawing: DesignDrawingItem, response: DesignDrawingResponse): void => {
if (drawing.kind === "standard") {
showStandardSpec(drawing);
return;
}
specJumpButton.hidden = true; // 표준도 장이 아니면 갈 곳이 없다
if (drawing.kind !== "cross") {
infoPanelHost.replaceChildren();
return;
@@ -680,7 +594,7 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
// (2026-09-02 사용자 지시 — CAD 리본과 겹치던 떠 있는 띠를 여기로 옮김).
const confirmButtonRow = document.createElement("div");
confirmButtonRow.className = "b07-drawing-actions__row";
confirmButtonRow.append(specJumpButton, frameEditor.button, confirmButton);
confirmButtonRow.append(frameEditor.button, confirmButton);
confirmActions.append(frameEditor.banner, confirmButtonRow);
drawingPanel.append(infoPanelHost, confirmActions);
@@ -37,8 +37,6 @@ export const DRAWING_GROUPS: readonly {
{ label: "횡단면도", kind: "cross" },
{ label: "토적도(유토곡선)", kind: "mass_haul" },
{ label: "유역도(배수 유역도)", kind: "watershed" },
// 표준도는 **제원 조합마다 한 장**이라 장이 나뉜다(2026-09-09) — 계획평면도와 같은 묶음.
{ label: "표준도", idPrefix: "standard_sheet" },
{ label: "용지도", idPrefix: "landuse" },
];
@@ -39,27 +39,6 @@ async def _project_root(project_id: UUID) -> str | None:
return None
async def section_modes_of(route_id: int) -> dict[float, str]:
"""측점별 단면유형(`left_cut` 등) — 구조물이 **성토면인가 절토면인가**를 가르는 근거.
⚠ 이것을 안 넘기면 판정이 통째로 「가를 근거 없음」으로 떨어져 **전 구조물이 종전값
1:0.3 으로 선다**(2026-09-09 실측). 값이 없는 것이 아니라 **안 넘긴 것**이었다.
⚠ 표를 만드는 셈은 `section_modes_from_designs` 한 벌을 쓴다 — 부르는 쪽마다 다시
짜면 B08 과 갈린다.
⚠ 과도기 — 노선 id 로 부르는 쪽은 B07 표준도 도면뿐임.
탭 배선 뒤 B07 에서 표준도를 빼면 함께 지움.
"""
from B06_Section.B06_Section_Repository import get_cross_section_designs
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import section_modes_from_designs
try:
designs = await run_with_connection(get_cross_section_designs, route_id)
except Exception:
logger.exception("B08 구조물도 단면유형 조회 실패: route_id=%s", route_id)
return {}
return section_modes_from_designs(designs)
def project_structure_sheets(
project_root: str,
section_modes: dict[float, str] | None,
@@ -3,7 +3,7 @@
* 표준도 **제원 입력 칸** — 장 하나가 곧 제원 조합 하나이므로 여기서 고치면 그 조합 전부.
*
* ⚠ 2026-09-13 B07 에서 이관(PLAN 3장) — 옛 자리 `B07_DesignDetail_UI_StandardSpec.ts`.
* B07 표준도 목록이 과도기 동안 이 파일을 함께 씀 — 그래서 모양(CSS)을 **이 파일이 스스로 넣음**.
* 모양(CSS)은 옛 B07 페이지 CSS 에서 옮겨 **이 파일이 스스로 넣음**.
*
* 왜 여기인가 (PLAN 4-5b) — `phase: "detail"` 칸(돌 종류·조달·뒷길이·전면 기울기)을 그리는
* 화면이 없었다(2026-09-09 실측: B06 구조물 폼은 b05 phase 열 칸만 그림). B06 은
@@ -58,7 +58,7 @@ const THICKNESS_TYPES = new Set(["masonry_wet", "masonry_dry"]);
const STYLE_ID = "b08-spec-style";
/* 옛 B07 페이지 CSS(`B07_DesignDetail_UI_Style.css`)에서 옮긴 것 — 두 페이지가 같은 모양을 보게. */
/* 옛 B07 페이지 CSS(`B07_DesignDetail_UI_Style.css`)에서 옮긴 것. */
const CSS = `
.b08-spec { display: flex; flex-direction: column; gap: var(--spacing-8); padding: var(--spacing-12); border-radius: var(--radius-lg); }
.b08-spec__title { margin: 0; font-size: var(--text-body-sm); font-weight: 600; }
@@ -101,14 +101,18 @@ def test_안_정하면_국가기준_기본이_뜬다() -> None:
assert 물구멍 and "Ø50" in 물구멍[0] and "2㎡당" in 물구멍[0], 물구멍
def test_여기서_고치기_단추가_고정_액션_줄에_있다() -> None:
"""확정 6 ㉮ — 표는 CAD 안이라 단추를 못 달아 **고정 액션 줄**에 둔다(값 복사본 없음)."""
def test_제원_칸이_구조물도_탭_표_옆에_있다() -> None:
"""확정 6 ㉮ 「한 곳에서 정하고 표는 비추기만」.
ⓘ 2026-09-13 구조물도가 B08 탭으로 옮겨(PLAN 3장 ④) 표가 CAD 밖 HTML 로 서면서 폼이
**표 바로 옆**에 붙음 — 옛 B07 [여기서 고치기] 단추(표가 CAD 안이라 필요했던 것)는 걷어냄.
"""
page = (ROOT / "B07_DesignDetail" / "B07_DesignDetail_UI_Page.ts").read_text(encoding="utf-8")
assert '"여기서 고치기"' in page
assert "confirmButtonRow.append(specJumpButton" in page
# 표준도 장에서만 뜬다 — 다른 도면에는 갈 곳이 없다.
assert "specJumpButton.hidden = false" in page
assert "specJumpButton.hidden = true" in page
assert "specJumpButton" not in page, "B07 에 표준도 제원 칸 흔적이 남았다"
sheet = (ROOT / "B08_Quantity" / "B08_Quantity_UI_StructureSheet.ts").read_text(
encoding="utf-8"
)
assert "buildStandardSpecPanel(" in sheet
# 폼에 버림 칸이 섰다 — 끄는 자리가 화면에 있다(확정 8-2).
form = (ROOT / "B08_Quantity" / "B08_Quantity_UI_StructureSheet_Spec.ts").read_text(
encoding="utf-8"