fix(M02): 남의 층 쓰기 기록 = 성공한 쓰기만 — 길이 답한 뒤 남김 · 막힘 · 실패 부름 뺌(39-1d)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP
This commit is contained in:
2026-10-04 17:51:28 +09:00
co-authored by Claude Opus 5.5
parent 3db416fc7b
commit 36c437d9a1
3 changed files with 41 additions and 6 deletions
@@ -11,11 +11,14 @@ PLAN 27-1c) · `owner` = 회사 `{C}` · 개인 `{C}/{U}`(없으면 자기 층)
from __future__ import annotations
import logging
from collections.abc import Callable, Coroutine
from contextvars import ContextVar
from pathlib import Path
from typing import Any, Literal
import anyio.from_thread
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response
from fastapi.routing import APIRoute
from pydantic import BaseModel
from common_util.common_util_audit import record_audit
@@ -37,7 +40,32 @@ from M02_MasterTemplete.M02_MasterTemplete_Router import (
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/m02", tags=["M02 MasterTemplete Items"])
#: 이 요청이 쓴 남의 층(`others_write`) — 길이 성공해 답한 뒤에만 시스템 로그(`NotedRoute`)
_WRITES: ContextVar[list[tuple[dict, access.Grant]] | None] = ContextVar("m02_writes", default=None)
class NotedRoute(APIRoute):
"""남의 층 쓰기 기록 = 성공한 쓰기만(39-1d) — 막히거나 실패한 부름(예외 · 4xx · 5xx)은 뺌."""
def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]:
handler = super().get_route_handler()
async def run(request: Request) -> Response:
notes: list[tuple[dict, access.Grant]] = []
token = _WRITES.set(notes)
try:
response = await handler(request)
finally:
_WRITES.reset(token)
if response.status_code < 400:
for session, got in notes:
await _note(session, got)
return response
return run
router = APIRouter(prefix="/api/m02", tags=["M02 MasterTemplete Items"], route_class=NotedRoute)
Layer = Literal["system", "company", "personal"]
Session = dict[str, Any]
@@ -114,12 +142,17 @@ async def _note(session: Session, got: access.Grant) -> None:
async def others_write(session: Session, got: access.Grant, folder: Path) -> None:
"""남의 층 쓰기 — 처음(집계표 없음)이면 주인이 있는지 본 뒤 뼈대 · 쓸 때마다 시스템 로그."""
"""남의 층 쓰기 — 처음(집계표 없음)이면 주인이 있는지 본 뒤 뼈대 · 시스템 로그는 길이 성공한
뒤(`NotedRoute` · 그 밖의 길은 바로)."""
if not (folder / "table" / f"{store.STRUCTURE_TABLE}.json").is_file():
if not await _member(str(got.owner)):
raise HTTPException(status_code=403, detail="그 회사 · 그 회사 사람이 아님")
items_store.ensure(folder)
await _note(session, got)
pending = _WRITES.get()
if pending is None:
await _note(session, got)
else:
pending.append((session, got))
def _root(session: Session, layer: str, owner: str | None = None, write: bool = False) -> Path:
@@ -34,7 +34,9 @@ from M02_MasterTemplete import M02_Template_Layers as layers
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/m02", tags=["M02 MasterTemplete Layers"])
router = APIRouter(
prefix="/api/m02", tags=["M02 MasterTemplete Layers"], route_class=items_router.NotedRoute
)
Layer = Literal["system", "company", "personal", "project"]
+1 -1
View File
@@ -160,7 +160,7 @@ def test_남의_층_첫_쓰기는_소속_확인_뒤_뼈대(world: dict[str, Any]
add["판"] = ""
assert client.post("/api/m02/layers/company/tree?owner=9", json=add).status_code == 409
assert (storage / "9/templates/table/구조물집계표.json").is_file()
assert [n[2] for n in world["notes"]] == ["7/44", "7/44", "9"]
assert [n[2] for n in world["notes"]] == ["7/44"] # 성공한 쓰기만(39-1d) · 409 · 403 은 안 남김
def test_남의_층_목록(world: dict[str, Any]):