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/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.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 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_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): 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"]):