From f1f06389025a4df98ac930a3c4bc68501db28b05 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 26 Oct 2026 03:44:49 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix(531-6):=20D04=20=EC=97=85=EC=B2=B4=20?= =?UTF-8?q?=EA=B0=80=EB=A6=BC=20=E2=80=94=20=EC=A7=80=EC=A0=95=20=ED=9A=8C?= =?UTF-8?q?=EC=82=AC=20=EC=9D=91=EB=8B=B5=EC=97=90=EC=84=9C=20=EB=82=A8?= =?UTF-8?q?=EC=9D=98=20=EC=97=85=EC=B2=B4=20=C2=B7=20=EB=8B=A4=EB=A5=B8=20?= =?UTF-8?q?=EC=97=B0=EB=8F=84=20=EC=A4=84=20=EC=84=9C=EB=B2=84=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=BA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - summary · assignees GET: 산림청 · 시스템관리자 = 전부, 지정 회사 = 자기 회사 연도 줄 · 자기 업체 행만 - 시험: 공유 회사 응답에 남의 이름 · 메일 없음 · 관리자 전부 Co-Authored-By: Claude Opus 5.5 --- D04_PlanConfirm/D04_PlanConfirm_Router.py | 37 ++++++++++++++++------- resources/tester/test_531_3_plan_link.py | 26 +++++++++++++--- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/D04_PlanConfirm/D04_PlanConfirm_Router.py b/D04_PlanConfirm/D04_PlanConfirm_Router.py index 276478b99..41897e5af 100644 --- a/D04_PlanConfirm/D04_PlanConfirm_Router.py +++ b/D04_PlanConfirm/D04_PlanConfirm_Router.py @@ -1,9 +1,9 @@ """D 예정노선 확정 · 업체 지정 API (요구 345 · PLAN 531-1 · 531-2). -- `GET /api/projects/{id}/plan/assignees` — 연도별 지정 업체 +- `GET /api/projects/{id}/plan/assignees` — 연도별 지정 업체(지정 회사 = 자기 행만) - `PUT /api/projects/{id}/plan/assignees` — 연도별 지정 업체 저장(등록 회사 또는 업체명 + 메일) - `POST /api/projects/{id}/plan/finalize` — 연도 노선 확정 → 업체별 zip → 메일 첨부 → 발송 시각 -- `GET /api/projects/{id}/plan/summary` — D04 화면 연도 요약(연장 · 시점 · 종점 · 누가 · 확정) +- `GET /api/projects/{id}/plan/summary` — D04 화면 연도 요약(지정 회사 = 자기 연도 · 자기 행만) - `GET /api/projects/{id}/plan/export` — [내보내기] zip(산림청 = 전 연도 + 전체 · 공유 = 자기 연도) 지정 · 확정은 산림청(시스템 관리자 또는 메일 *.go.kr)만. 지정받은 회사는 공용 접근 판정이 @@ -116,10 +116,25 @@ async def _plan_project(project_id: UUID) -> dict[str, Any]: return project +def visible_to( + session: dict[str, Any], years: list[dict[str, Any]], assignees: list[dict[str, Any]] +) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: + """산림청 = 전부 · 지정 회사 = 자기 회사가 지정된 연도 줄 · 자기 업체 행만(남의 이름 · 메일 뺌).""" + if is_forest_service(session): + return years, assignees + company = session.get("company_id") + mine = [a for a in assignees if a["company_id"] is not None and a["company_id"] == company] + own_years = {a["year"] for a in mine} + return [y for y in years if y["year"] in own_years], mine + + @router.get("/{project_id}/plan/assignees", response_model=None) -async def get_plan_assignees(project_id: UUID) -> dict[str, Any]: +async def get_plan_assignees( + project_id: UUID, session: dict = Depends(verify_session) +) -> dict[str, Any]: await _plan_project(project_id) - return {"status": "success", "assignees": await repo.list_assignees(str(project_id))} + _, assignees = visible_to(session, [], await repo.list_assignees(str(project_id))) + return {"status": "success", "assignees": assignees} @router.put("/{project_id}/plan/assignees", response_model=None) @@ -249,15 +264,15 @@ def year_summary(row: dict[str, Any]) -> dict[str, Any]: @router.get("/{project_id}/plan/summary", response_model=None) -async def get_plan_summary(project_id: UUID) -> dict[str, Any]: +async def get_plan_summary( + project_id: UUID, session: dict = Depends(verify_session) +) -> dict[str, Any]: project = await _plan_project(project_id) rows = await repo.list_year_routes(str(project_id)) - return { - "status": "success", - "name": project["name"], - "years": [year_summary(row) for row in rows], - "assignees": await repo.list_assignees(str(project_id)), - } + years, assignees = visible_to( + session, [year_summary(row) for row in rows], await repo.list_assignees(str(project_id)) + ) + return {"status": "success", "name": project["name"], "years": years, "assignees": assignees} def export_zip(bundle: Any, years: list[int], with_all: bool) -> bytes: diff --git a/resources/tester/test_531_3_plan_link.py b/resources/tester/test_531_3_plan_link.py index 4f4869270..20097ac81 100644 --- a/resources/tester/test_531_3_plan_link.py +++ b/resources/tester/test_531_3_plan_link.py @@ -418,8 +418,10 @@ def plan_repo(monkeypatch): async def assignees(_pid): return [ - {"id": 7, "year": 2026, "company_id": 5}, - {"id": 8, "year": 2027, "company_id": 5}, + {"id": 7, "year": 2026, "company_id": 5, "company_name": "가", "email": "a@ga.kr"}, + {"id": 8, "year": 2027, "company_id": 5, "company_name": "가", "email": "a@ga.kr"}, + {"id": 9, "year": 2027, "company_id": None, "company_name": "나", "email": "b@na.kr"}, + {"id": 10, "year": 2028, "company_id": 6, "company_name": "다", "email": "c@da.kr"}, ] monkeypatch.setattr(repo, "get_plan_project", project) @@ -447,13 +449,29 @@ def test_export_shared_gets_own_confirmed_year(plan_repo): def test_summary_year_rows(plan_repo): - out = asyncio.run(d04.get_plan_summary(PLAN_ID)) + forest = {"role": "USER", "email": "kim@forest.go.kr", "company_id": 1} + out = asyncio.run(d04.get_plan_summary(PLAN_ID, forest)) first = out["years"][0] assert first["saved"] and first["start"]["x"] == P2026[0][0] and first["confirmed_at"] == "x" assert 120 < first["start"]["lon"] < 135 and first["end_chainage_m"] == pytest.approx( first["length_m"] ) - assert out["years"][1]["confirmed_at"] is None and len(out["assignees"]) == 2 + assert out["years"][1]["confirmed_at"] is None and len(out["assignees"]) == 4 + + +def test_shared_company_sees_only_own_firm(plan_repo): + """지정 회사 = 자기 연도 줄 · 자기 행만 — 남의 업체 이름 · 메일은 응답에 없음(요구 351).""" + other = {"role": "USER", "email": "x@ga.kr", "company_id": 6} # 2028 만(노선 없음) + for session, years, ids in ((SESSION, [2026, 2027], [7, 8]), (other, [], [10])): + out = asyncio.run(d04.get_plan_summary(PLAN_ID, session)) + assert [y["year"] for y in out["years"]] == years + assert [a["id"] for a in out["assignees"]] == ids + listed = asyncio.run(d04.get_plan_assignees(PLAN_ID, session))["assignees"] + assert [a["id"] for a in listed] == ids + text = str(asyncio.run(d04.get_plan_summary(PLAN_ID, SESSION))) + assert "b@na.kr" not in text and "c@da.kr" not in text and "나" not in text + admin = {"role": "SYSTEM_ADMIN", "email": "root@x.kr", "company_id": 99} + assert len(asyncio.run(d04.get_plan_assignees(PLAN_ID, admin))["assignees"]) == 4 def test_put_assignees_years_clears_empty_year(plan_repo, monkeypatch): From 64e8e10b124099cd5a3934ae2d53e4fbf659e604 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 26 Oct 2026 03:49:29 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix(536-1):=20E06=20=EB=93=A4=EB=A6=BC=20?= =?UTF-8?q?=EC=95=88=EC=A0=84=EC=9C=A8=20-0.0=20=E2=86=92=200=20=C2=B7=20E?= =?UTF-8?q?05=20=EC=95=94=EB=B0=98=EA=B2=80=ED=86=A0=20=EC=B8=A1=EC=A0=90?= =?UTF-8?q?=20=ED=91=9C=20=EB=A8=B8=EB=A6=AC=20=EC=A4=84=20=EC=AA=BD?= =?UTF-8?q?=EB=A7=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- E05_StabReport/E05_StabReport_Rock.py | 3 ++- E06_StabRock/E06_StabRock_Model.py | 2 +- resources/master_template/report/stab_report_slots.md | 2 +- resources/tester/test_536_1_e06_rock.py | 6 ++++++ 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/E05_StabReport/E05_StabReport_Rock.py b/E05_StabReport/E05_StabReport_Rock.py index c7ef1fe90..5c2ef3eae 100644 --- a/E05_StabReport/E05_StabReport_Rock.py +++ b/E05_StabReport/E05_StabReport_Rock.py @@ -59,7 +59,7 @@ def _summary(rock: dict[str, Any]) -> str: def fill_rock(sheet: dict[str, Any], rock: dict[str, Any], labels: dict) -> None: - """s11 — 1. 조건 · 요약(5~9 행) · 2. 절리군(한 줄씩 · 13 행 서식) · 3. 측점(17 행 서식).""" + """s11 — 1. 조건 · 요약(5~9 행) · 2. 절리군(한 줄씩 · 13 행 서식) · 3. 측점(17 행 서식 · 머리 줄 쪽마다).""" s = rock["settings"] d = rock_model.DEFAULT_SETTINGS lateral = (s["lateral_planar_deg"], s["lateral_toppling_deg"]) @@ -143,6 +143,7 @@ def fill_rock(sheet: dict[str, Any], rock: dict[str, Any], labels: dict) -> None # 3. 측점별 판정 r = head(r + 1, 15) + sheet["인쇄"]["머리행"] = [r - 1, r - 1] # 측점 표 머리 줄 = 쪽마다 for entry in rock["stations"]: r = _station_rows(sheet, put_row, r, entry, labels) diff --git a/E06_StabRock/E06_StabRock_Model.py b/E06_StabRock/E06_StabRock_Model.py index 95e57df69..d67907ac5 100644 --- a/E06_StabRock/E06_StabRock_Model.py +++ b/E06_StabRock/E06_StabRock_Model.py @@ -274,7 +274,7 @@ def _gamma_for(resolved: dict | None, materials: dict, override: float | None) - def _fmt(value: float | None) -> float | None: if value is None or not math.isfinite(value): return None - return round(value, 4) + return round(value, 4) + 0.0 # -0.0 → 0.0(들림 · 볼트 없음) def _cases_for(slope: dict, joints: list[dict], kin: dict, settings: dict, gamma: float, conds): diff --git a/resources/master_template/report/stab_report_slots.md b/resources/master_template/report/stab_report_slots.md index f9028a20b..4fdae58dd 100644 --- a/resources/master_template/report/stab_report_slots.md +++ b/resources/master_template/report/stab_report_slots.md @@ -117,7 +117,7 @@ - 6 행 · 10 행 서식을 줄마다 복사 · 저장본 = (측점 · 종류 · 비탈)마다 가장 최근 하나. -## s11 암반검토 (프로그램 몫 · E06 암반 해석 저장본 · 없으면 시트를 뺌) +## s11 암반검토 (프로그램 몫 · E06 암반 해석 저장본 · 없으면 시트를 뺌 · 측점 표 머리 줄 쪽마다 반복) | 칸 | 내용 | | --- | --- | diff --git a/resources/tester/test_536_1_e06_rock.py b/resources/tester/test_536_1_e06_rock.py index f2e37b3a9..9bfdfd844 100644 --- a/resources/tester/test_536_1_e06_rock.py +++ b/resources/tester/test_536_1_e06_rock.py @@ -227,6 +227,11 @@ def test_wedge_gravity_water_and_bolt(): assert bolted["F"] > dry["F"] +def test_fmt_negative_zero(): + assert str(model._fmt(-0.0)) == "0.0" # 들림 · 볼트 없음 = 0(「-0.0」 아님) + assert model._fmt(math.inf) is None + + def test_wedge_not_formed(): # 교선이 사면보다 가파름 → 사면으로 안 나옴 assert _wedge((141, 75), (219, 75), (180, 50), 35) is None @@ -480,6 +485,7 @@ def test_e05_rock_sheet(monkeypatch): assert "기본값 · 사용자 확정 전" in cells["B7"]["값"] and "½" in cells["B7"]["값"] assert cells["B9"]["값"].startswith("깎기 비탈 1") assert cells["A13"]["값"] == doc["sets"][0]["name"] + assert sheet["인쇄"]["머리행"] == [16, 16] and cells["A16"]["값"] == "측점" # 절리 한 줄 rows = [r for r in range(17, 40) if cells.get(f"A{r}", {}).get("값") == "STA"] assert len(rows) == len(stations[0]["cases"]) + len(stations[0]["kinematic"]["toppling"]) for r, case in zip(rows, stations[0]["cases"]): From 60f28f326110542b5c21e5bfcc78659c43cbcfa5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 26 Oct 2026 03:51:23 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix(536-1):=20E05=20=EC=95=94=EB=B0=98?= =?UTF-8?q?=EA=B2=80=ED=86=A0=20=EB=B9=84=ED=83=88=20=EC=97=B4=20=ED=8F=AD?= =?UTF-8?q?=20=E2=80=94=20=EB=86=92=EC=9D=B4=20=EA=B8=80=20=EC=A4=84?= =?UTF-8?q?=EB=B0=94=EB=80=9C=20=EC=97=86=EC=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- resources/master_template/report/stab_report.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/master_template/report/stab_report.json b/resources/master_template/report/stab_report.json index 4128cb5bc..1b9d49ef2 100644 --- a/resources/master_template/report/stab_report.json +++ b/resources/master_template/report/stab_report.json @@ -4071,7 +4071,7 @@ "폭": 14 }, "B": { - "폭": 26 + "폭": 33 }, "C": { "폭": 16