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

This commit is contained in:
2026-10-29 13:33:25 +09:00
6 changed files with 56 additions and 11 deletions
+11 -7
View File
@@ -126,7 +126,7 @@ async def read_terrain_sources(project_id: UUID) -> dict | JSONResponse:
content={"status": "error", "message": "프로젝트 저장 경로를 찾을 수 없습니다."},
)
params = await get_surface_confirmation_params(connection, str(project_id))
confirmed = await _confirmed_model(connection, project_id)
model = await _design_model(connection, project_id, params)
models_dir = Path(resolve_stored_project_path(stored)) / _MODELS_SUBDIR
sources = await choosable_terrains(connection, project_id, params, models_dir)
return {
@@ -134,7 +134,7 @@ async def read_terrain_sources(project_id: UUID) -> dict | JSONResponse:
"project_id": str(project_id),
"sources": sources,
"design": design_terrain(params), # 지금 설계 지형(PLAN 133-2)
"grid": await asyncio.to_thread(_design_grid, params, models_dir, confirmed),
"grid": await asyncio.to_thread(_design_grid, params, models_dir, model),
}
@@ -156,17 +156,21 @@ async def choosable_terrains(
return sources
async def _confirmed_model(
connection: aiomysql.Connection, project_id: UUID
async def _design_model(
connection: aiomysql.Connection, project_id: UUID, params: dict
) -> tuple[int, str, str] | None:
"""확정 지표면 모델 (id, 표현, 파일 경로) — 없으면 None."""
"""설계 지형 DTM 모델 (id, 표현, 파일 경로) — 서버 표본기처럼 stage 1 params 의 `dtm_{filter}.npz`
(확정 행 먼저). 확정 행이 params 와 어긋나도 서버와 같은 바닥을 받게(578-1) · 없으면 None."""
name = f"dtm_{params.get('source_filter') or ''}.npz"
async with connection.cursor() as cursor:
await cursor.execute(
"SELECT id, model_type, model_file_path FROM surface_models"
" WHERE project_id = %s AND status = 'CONFIRMED' ORDER BY created_at DESC LIMIT 1",
" WHERE project_id = %s AND model_type = 'dtm' AND status IN ('CONFIRMED', 'COMPLETE')"
" ORDER BY status = 'CONFIRMED' DESC, created_at DESC",
(str(project_id),),
)
row = await cursor.fetchone()
rows = await cursor.fetchall()
row = next((r for r in rows if Path(str(r[2] or "")).name == name), None)
return (int(row[0]), str(row[1]), str(row[2] or "")) if row else None
@@ -6,6 +6,8 @@
* 만든다. 여기 있는 것은 전부 **읽어서 붙이는 값**이라 카드 상태(줌·선택)를 건드리지 않는다.
* ========================================================================== */
import { currentProjectId } from "../A00_Common/b_project_switch";
import { designTerrainGridNow } from "../B05_Profile/B05_Profile_Api_TerrainGrid";
import type { CrossSection } from "./B06_Section_Api_Fetch";
import { buildCutSlopeControl, type CutSlopeControl } from "./B06_Section_UI_Cross_CutSlope";
import { buildDesignControls, sectionModeLabel } from "./B06_Section_UI_Cross_Design";
@@ -108,7 +110,15 @@ export function appendCardHeader(
meta.className = "b06-cross-card__meta";
// 사면이 계산 반폭 끝까지 원지반을 못 만난 측점 — 면적이 거기서 잘려 유토곡선까지
// 그 값을 쌓는다. 계산은 손대지 않고 사실만 알린다(2026-09-03 사용자 확정).
if (section.design?.slope_unclosed) {
// 지형 격자가 없으면(DTM 아닌 지형) 브라우저는 받은 표본 끝에서 멈춤 — 값은 [저장] 때 서버가 넓혀 셈(578-1)
if (section.design?.slope_unclosed && !designTerrainGridNow(currentProjectId())) {
const outside = document.createElement("span");
outside.className = "ui-badge ui-badge--pill ui-badge--warning b06-cross-card__outside";
outside.setAttribute("data-size", "20");
outside.textContent = L("B06_Cross_TerrainOutside");
outside.title = L("B06_Cross_TerrainOutside_Tip");
meta.append(outside);
} else if (section.design?.slope_unclosed) {
const openSlope = document.createElement("span");
openSlope.className = "b06-cross-card__warning";
openSlope.textContent = `⚠ ${L("B06_Cross_SlopeUnclosed")}`;
@@ -75,10 +75,10 @@ def _patch_db(monkeypatch, project_root: Path, params: dict) -> None:
monkeypatch.setattr(terrain_router, "get_surface_confirmation_params", confirmed)
monkeypatch.setattr(terrain_router, "resolve_stored_project_path", lambda _s: project_root)
async def no_model(_connection, _project_id): # 격자 받을 자리(344-1)는 따로 시험
async def no_model(_connection, _project_id, _params): # 격자 받을 자리(344-1)는 따로 시험
return None
monkeypatch.setattr(terrain_router, "_confirmed_model", no_model)
monkeypatch.setattr(terrain_router, "_design_model", no_model)
PID = "1bb0f876-983f-4ca5-9cc7-f59ee1f77e0a"
@@ -141,3 +141,28 @@ def test_지반고_자릿수_기본_3_묶음은_6(tmp_path, monkeypatch):
)
assert plain["z"] == [round(5.5 * 0.1234567, 3)]
assert fine["z"] == [round(5.5 * 0.1234567, 6)]
def test_격자_모델은_params_파일_확정_행이_어긋나도(monkeypatch):
"""578-1 — 확정 행(sheet)이 params(csf)와 어긋나도 서버 표본기와 같은 `dtm_{filter}.npz` 행을 고름."""
rows = [
(9, "dtm", "B04_PreProcess/models/dtm_sheet_laplace.npz"), # 확정(어긋남)
(7, "dtm", "B04_PreProcess/models/dtm_csf.npz"),
]
class _Cursor:
async def execute(self, *_args):
return None
async def fetchall(self):
return rows
class _Connection:
@asynccontextmanager
async def cursor(self):
yield _Cursor()
pick = asyncio.run(terrain_router._design_model(_Connection(), PID, PARAMS))
assert pick == (7, "dtm", "B04_PreProcess/models/dtm_csf.npz")
other = {**PARAMS, "source_filter": "pmf"}
assert asyncio.run(terrain_router._design_model(_Connection(), PID, other)) is None
+1 -1
View File
@@ -70,7 +70,7 @@ def sources(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(terrain, "get_db_pool", lambda: _Pool())
monkeypatch.setattr(terrain, "get_project_storage_relative_path", _async("p"))
monkeypatch.setattr(terrain, "get_surface_confirmation_params", _async(dict(LAS)))
monkeypatch.setattr(terrain, "_confirmed_model", _async(None))
monkeypatch.setattr(terrain, "_design_model", _async(None))
monkeypatch.setattr(terrain, "resolve_stored_project_path", lambda stored: tmp_path)
monkeypatch.setattr(terrain, "_design_grid", lambda *args: None)
+6
View File
@@ -318,6 +318,12 @@ export const ui_locales_b2 = {
"사면이 계산 반폭 끝까지 원지반과 만나지 않아 절·성토 면적이 그 자리에서 잘렸습니다. 유토곡선·수량도 잘린 값을 씁니다.",
"The slope never meets existing ground within the computed half-width, so the cut/fill area is truncated there. The mass haul curve and quantities use the truncated value.",
],
/* 지형 격자를 못 받은 지형(DTM 아님)에서 사면이 받은 지반 표본 밖으로 나간 측점(578-1). */
B06_Cross_TerrainOutside: ["지형 범위 밖", "Outside terrain range"],
B06_Cross_TerrainOutside_Tip: [
"사면이 화면이 받은 지반 표본 밖으로 나가 화면 면적은 그 끝에서 잘린 값입니다. [저장] 때 서버가 지형 끝까지 넓혀 다시 셉니다.",
"The slope runs past the ground samples this screen received, so the area shown is cut off there. On [Save] the server extends to the terrain edge and recomputes.",
],
/* 성토사면 경사길이 — 구조물이 사면을 끊기 전 **본래** 길이(2026-09-03 사용자 지시). */
B06_Cross_FillSlope: ["성토사면", "Fill slope"],
B06_Cross_FillSlope_Tip: [