From 36c437d9a1ece40f5940915e5f76225e799c0352 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 4 Oct 2026 17:51:28 +0900 Subject: [PATCH] =?UTF-8?q?fix(M02):=20=EB=82=A8=EC=9D=98=20=EC=B8=B5=20?= =?UTF-8?q?=EC=93=B0=EA=B8=B0=20=EA=B8=B0=EB=A1=9D=20=3D=20=EC=84=B1?= =?UTF-8?q?=EA=B3=B5=ED=95=9C=20=EC=93=B0=EA=B8=B0=EB=A7=8C=20=E2=80=94=20?= =?UTF-8?q?=EA=B8=B8=EC=9D=B4=20=EB=8B=B5=ED=95=9C=20=EB=92=A4=20=EB=82=A8?= =?UTF-8?q?=EA=B9=80=20=C2=B7=20=EB=A7=89=ED=9E=98=20=C2=B7=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=20=EB=B6=80=EB=A6=84=20=EB=BA=8C(39-1d)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP --- .../M02_MasterTemplete_Router_Items.py | 41 +++++++++++++++++-- .../M02_MasterTemplete_Router_Layers.py | 4 +- resources/tester/test_m02_layer_access.py | 2 +- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py index 0a40524d3..cef56af78 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py @@ -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: diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py index f3097f50b..a2e2aa95d 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py @@ -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"] diff --git a/resources/tester/test_m02_layer_access.py b/resources/tester/test_m02_layer_access.py index f89e29ea9..3f0ac4938 100644 --- a/resources/tester/test_m02_layer_access.py +++ b/resources/tester/test_m02_layer_access.py @@ -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]):