From deaacc11003e50eed622660dfcac849ff73f0b61 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 25 Oct 2026 20:07:38 +0900 Subject: [PATCH] =?UTF-8?q?fix(532-3):=20E02=20=EC=B8=A1=EC=A0=90(B06=20?= =?UTF-8?q?=EC=95=94=20=EA=B2=BD=EA=B3=84)=EC=9D=84=20E01=20=EB=8F=99?= =?UTF-8?q?=EA=B2=B0=20=EB=B3=B5=EC=82=AC=EB=B3=B8=20db.json=20=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=9D=BD=EC=9D=8C=20=E2=80=94=20=EC=9B=90=EB=B3=B8?= =?UTF-8?q?=20=EC=9E=84=EB=8F=84=20DB=20=EC=95=88=20=EC=9D=BD=EC=9D=8C=20?= =?UTF-8?q?=C2=B7=20=EB=B3=B5=EC=82=AC=EB=B3=B8=20=EC=97=86=EC=9C=BC?= =?UTF-8?q?=EB=A9=B4=20=EB=B9=88=20=EB=AA=A9=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- E02_StabGround/E02_StabGround_Router.py | 84 ++++++++++------------- resources/tester/test_532_3_e02_ground.py | 45 +++++++----- 2 files changed, 64 insertions(+), 65 deletions(-) diff --git a/E02_StabGround/E02_StabGround_Router.py b/E02_StabGround/E02_StabGround_Router.py index aa25b9ace..8b9ad1663 100644 --- a/E02_StabGround/E02_StabGround_Router.py +++ b/E02_StabGround/E02_StabGround_Router.py @@ -5,7 +5,7 @@ · 측점(B06 암 경계)은 서버 값 - `POST …/stab/ground/reset` — [초기화] 초기값을 작업본에 덮어씀(재계산 아님) -초기값 = E 생성 직후 마스터 기본값 + B06 측점(`ensure_initial`) · 그 밖에는 쓰지 않음. +초기값 = E 생성 직후 마스터 기본값 + 복사본 B06 측점(`ensure_initial`) · 그 밖에는 쓰지 않음. 공유 회사(보기만)는 `require_project_access` 가 PUT · POST 를 막음. """ @@ -21,12 +21,13 @@ from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path -from B05_Profile.B05_Profile_Repository import get_latest_route from common_util.common_util_auth import verify_session from common_util.common_util_json import atomic_write_json +from common_util.common_util_project_copy import MANIFEST_NAME from common_util.common_util_storage import resolve_stored_project_path from config.config_db import get_db_pool from config.config_system_design import STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M +from E01_StabRegister.E01_StabRegister_Source import db_snapshot, source_dir from E02_StabGround import E02_StabGround_Model as model router = APIRouter(prefix="/api/projects", tags=["E02 Stab Ground"]) @@ -36,38 +37,30 @@ class GroundRequest(BaseModel): ground: dict[str, Any] -async def _b06_stations(connection: aiomysql.Connection, project_ids: list[str]) -> list[dict]: +def _b06_stations(project_root: Path) -> list[dict]: """측점 · 지반 라벨 · 토층 두께(암 측점 = 암 경계 깊이 · 토사 = 없음). - 자기 복사본(532-1)이 먼저 · 없으면 원본 임도.""" - for project_id in project_ids: - route = await get_latest_route(connection, UUID(project_id)) - if not route: - continue - async with connection.cursor() as cursor: - await cursor.execute( - "SELECT chainage_m, data FROM cross_sections WHERE route_id = %s" - " ORDER BY chainage_m", - (route["id"],), - ) - rows = await cursor.fetchall() - stations = [] - for chainage, data in rows: - data = json.loads(data) if isinstance(data, str) else data or {} - design = data.get("design") or {} - ground_type = design.get("ground_type") or "soil" - offset = design.get("rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M) - rock = ground_type in model.ROCK_LABELS and offset is not None - stations.append( - { - "chainage_m": round(float(chainage), 3), - "ground_type": ground_type, - "soil_thickness_m": round(abs(float(offset)), 3) if rock else None, - } - ) - if stations: - return stations - return [] + E01 동결 복사본(`E01_StabRegister/source/db.json`)만 읽음 — 복사본이 없으면 빈 목록. + """ + root = source_dir(project_root) + if not (root / MANIFEST_NAME).is_file(): + return [] + rows = sorted(db_snapshot(root)["cross_sections"], key=lambda r: float(r["chainage_m"] or 0)) + stations = [] + for row in rows: + data = json.loads(row["data"]) if isinstance(row["data"], str) else row["data"] or {} + design = data.get("design") or {} + ground_type = design.get("ground_type") or "soil" + offset = design.get("rock_boundary_offset_m", STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M) + rock = ground_type in model.ROCK_LABELS and offset is not None + stations.append( + { + "chainage_m": round(float(row["chainage_m"]), 3), + "ground_type": ground_type, + "soil_thickness_m": round(abs(float(offset)), 3) if rock else None, + } + ) + return stations async def _project(project_id: UUID) -> tuple[Path, dict[str, Any]]: @@ -78,7 +71,7 @@ async def _project(project_id: UUID) -> tuple[Path, dict[str, Any]]: raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다.") from error async with connection.cursor(aiomysql.DictCursor) as cursor: await cursor.execute( - "SELECT company_id, project_kind, source_project_id FROM projects WHERE id = %s", + "SELECT company_id, project_kind FROM projects WHERE id = %s", (str(project_id),), ) project = await cursor.fetchone() @@ -87,23 +80,20 @@ async def _project(project_id: UUID) -> tuple[Path, dict[str, Any]]: return Path(resolve_stored_project_path(stored)), project -async def ensure_initial(project_root: Path, project_id: str, source_id: str | None) -> dict: - """초기값이 없을 때만 한 번 — 기본값 + B06 측점(E 생성 직후 · 옛 E 프로젝트는 첫 GET).""" +def ensure_initial(project_root: Path) -> dict: + """초기값이 없을 때만 한 번 — 기본값 + 복사본 B06 측점(E 생성 직후 · 옛 E 프로젝트는 첫 GET).""" path = model.initial_path(project_root) if path.is_file(): return json.loads(path.read_text(encoding="utf-8")) - async with get_db_pool().acquire() as connection: - ids = [project_id] + ([source_id] if source_id else []) - stations = await _b06_stations(connection, ids) - ground = model.build_initial(model.load_defaults(), stations) + ground = model.build_initial(model.load_defaults(), _b06_stations(project_root)) atomic_write_json(path, ground) return ground async def seed_initial(project_id: str) -> None: """E01 생성 직후 부름 — 초기값 한 벌.""" - root, project = await _project(UUID(project_id)) - await ensure_initial(root, project_id, project["source_project_id"]) + root, _ = await _project(UUID(project_id)) + ensure_initial(root) def _read_working(project_root: Path) -> dict[str, Any] | None: @@ -120,7 +110,7 @@ async def get_ground( project_id: UUID, session: dict[str, Any] = Depends(verify_session) ) -> dict[str, Any]: root, project = await _project(project_id) - initial = await ensure_initial(root, str(project_id), project["source_project_id"]) + initial = ensure_initial(root) working = _read_working(root) return { "ground": working or initial, @@ -132,16 +122,14 @@ async def get_ground( @router.put("/{project_id}/stab/ground") async def put_ground(project_id: UUID, payload: GroundRequest) -> dict[str, Any]: - root, project = await _project(project_id) + root, _ = await _project(project_id) defaults = model.load_defaults() errors = model.check_ground(payload.ground, defaults["limits"]) if errors: raise HTTPException(status_code=422, detail={"errors": errors}) ground = model.clean(payload.ground) # 측점(B06 암 경계)은 서버 값 — 브라우저 값을 받아 적지 않음 - base = _read_working(root) or await ensure_initial( - root, str(project_id), project["source_project_id"] - ) + base = _read_working(root) or ensure_initial(root) ground["stations"] = base.get("stations", []) ground["version"] = defaults["version"] atomic_write_json(model.ground_path(root), ground) @@ -150,7 +138,7 @@ async def put_ground(project_id: UUID, payload: GroundRequest) -> dict[str, Any] @router.post("/{project_id}/stab/ground/reset") async def reset_ground(project_id: UUID) -> dict[str, Any]: - root, project = await _project(project_id) - initial = await ensure_initial(root, str(project_id), project["source_project_id"]) + root, _ = await _project(project_id) + initial = ensure_initial(root) atomic_write_json(model.ground_path(root), initial) return {"ground": initial} diff --git a/resources/tester/test_532_3_e02_ground.py b/resources/tester/test_532_3_e02_ground.py index bb04b9a17..ce9b1741d 100644 --- a/resources/tester/test_532_3_e02_ground.py +++ b/resources/tester/test_532_3_e02_ground.py @@ -183,28 +183,33 @@ def test_측점_적용(): assert model.resolve_stations(RESOLVE_CASES[2])[0]["lower"] == "soft_rock" +def _write_copy(root: Path) -> None: + """E01 동결 복사본 꼴(copy_manifest.json + db.json cross_sections) — 측점은 여기서만 읽음.""" + source = root / "E01_StabRegister" / "source" + source.mkdir(parents=True) + (source / "copy_manifest.json").write_text("{}", encoding="utf-8") + rows = [ + {"chainage_m": 20.0, "data": json.dumps({"design": {"ground_type": "ripping_rock"}})}, + {"chainage_m": 0.0, "data": {"design": {"ground_type": "soil"}}}, + { + "chainage_m": 60.0, + "data": {"design": {"ground_type": "ripping_rock", "rock_boundary_offset_m": -0.5}}, + }, + { + "chainage_m": 40.0, + "data": {"design": {"ground_type": "blasting_rock", "rock_boundary_offset_m": -0.8}}, + }, + ] + (source / "db.json").write_text(json.dumps({"cross_sections": rows}), encoding="utf-8") + + @pytest.fixture def project(tmp_path, monkeypatch): async def fake_project(_project_id): - return tmp_path, {"company_id": 1, "project_kind": "STABILITY", "source_project_id": None} - - async def fake_stations(_connection, _ids): - return copy.deepcopy(STATIONS) - - class FakePool: - def acquire(self): - class Ctx: - async def __aenter__(self): - return None - - async def __aexit__(self, *_): - return False - - return Ctx() + return tmp_path, {"company_id": 1, "project_kind": "STABILITY"} monkeypatch.setattr(router, "_project", fake_project) - monkeypatch.setattr(router, "_b06_stations", fake_stations) - monkeypatch.setattr(router, "get_db_pool", lambda: FakePool()) + _write_copy(tmp_path) return tmp_path @@ -276,6 +281,12 @@ def test_저장_다시읽음_초기화(project): assert json.loads(model.ground_path(project).read_text(encoding="utf-8")) == initial +def test_복사본_없으면_측점_없음(tmp_path): + assert router._b06_stations(tmp_path) == [] + _write_copy(tmp_path) + assert router._b06_stations(tmp_path) == STATIONS # 누가 차례 · 암 경계 없으면 −0.5 + + def test_다른_회사는_보기만(project): from uuid import UUID