fix(531-6): D04 업체 가림 — 지정 회사 응답에서 남의 업체 · 다른 연도 줄 서버에서 뺌
- summary · assignees GET: 산림청 · 시스템관리자 = 전부, 지정 회사 = 자기 회사 연도 줄 · 자기 업체 행만 - 시험: 공유 회사 응답에 남의 이름 · 메일 없음 · 관리자 전부 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user