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):