feat(735-1): 서고 나무 지우기 — 쓰는 곳 경고 표 뒤 그 줄째 지움 · 되돌림

- 새 M02_Library_Delete: 쓰는 줄 미리 보기(종류 · 이름 · 키 · 줄 · 층) · 권한 밖 409 막힘 목록 ·
  확인 몸이면 쓰는 문서의 그 줄 지움(store.write 정본 다시 셈) 뒤 문서 지움 · _합친것/ 기록
  (지운 문서 · 자리 · 고친 문서 앞판) · 되돌림(그새 고친 문서는 건너뜀) · 단가산출 바탕 · 더한줄 열쇠 당김
- 미리 보기 길 POST {kind}-tree/delete-check · undo 길이 지움 기록도 되돌림 · 합친 것 목록에 「지움」
- 나무 고침 몸 확인 칸 · 세션을 나무 고침에 넘김 · 옛 _refuse_used 걷음
- 화면: 지우기 앞 경고 창(쓰는 곳 N · 표 · 프로젝트 사본 영향 없음 · 되돌림 자리) · 부품 상자
  확인창에 detail 칸(넓은 판 · 안 스크롤)
- 시험: 새 test_735_1 · test_716_4 의 쓰는 곳 409 시험을 확인 없음 409 + 목록 꼴로 고침
  (지움이 막힘에서 경고 뒤 지움으로 바뀜)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-02 15:50:56 +09:00
co-authored by Claude Opus 5.5
parent 1e8d05cea1
commit 1d8642fd76
13 changed files with 712 additions and 68 deletions
+60 -18
View File
@@ -7,7 +7,10 @@ GET /layers/{층}/{kind}-same — 이 층 + 위 층(회사 · 마스터) 같은
POST …/merge {남김, 지움[], 판} — 서버가 지문 다시 셈(다르면 409) · 쓰는 곳 참조를 남김으로
(쓰기 권한 닿는 곳만 · `store.write` 정본 다시 셈) · 남은 쓰는 곳이 있는 문서 = 안 지움
「남김 — 쓰는 곳 N」 · 나머지 = `{층}/_합친것/<키>.json` 으로 옮김 · 나무 자리 걷음
POST …/undo {키} — `_합친것/` 에서 되돌림 · 옮긴 참조도 되돌림
POST …/undo {키} — `_합친것/` 에서 되돌림 · 옮긴 참조도 되돌림 · 나무 지움 기록(735-1 `지움` 칸)이면
`M02_Library_Delete.undo`(문서 · 자리 · 고친 문서 앞판)
POST /layers/{층}/{kind}-tree/delete-check {대상} — 나무 지우기 미리 보기(735-1) · 지울 문서 ·
쓰는 줄
지움 = 이 층 문서만 · 남김 = 이 층 또는 위 층(위 층과 같은 아래 층 문서 = 남김 = 위 층 키) ·
판 = 이 층 서고 나무 판(`{종류}_폴더.json`) · 권한 = 그 층 쓰기 · 프로젝트 사본 = 굳힌 값이라
안 건드림 · 지문 = `M02_Library_Same`(733-1) 읽기만.
@@ -27,7 +30,7 @@ from pydantic import BaseModel
from common_util.common_util_auth import verify_session
from common_util.common_util_json import atomic_write_json
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_Layer_Access as access
from M02_MasterTemplete import M02_Library_Delete as deleting
from M02_MasterTemplete import M02_Library_Same as same
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Precombo_Folders as pre_folders
@@ -37,8 +40,8 @@ from M02_MasterTemplete.M02_MasterTemplete_Router_Items import Layer, Session, _
router = APIRouter(prefix="/api/m02", tags=["M02 MasterTemplete Library Same"])
Kind = Literal["precombo", "sangeun"]
GONE = "_합친것"
USER_KINDS = (("labor", "구조물 조합"), ("precombo", "일위대가"), ("sangeun", "단가산출"))
GONE = deleting.GONE
USER_KINDS = deleting.USER_KINDS
_ORDER = {"system": 0, "company": 1, "personal": 2}
@@ -54,6 +57,12 @@ class UndoBody(BaseModel):
키: str
class CheckBody(BaseModel):
"""나무 지우기 미리 보기 — 나무 고침 `대상` 꼴 `[{경로, 키?}]`."""
대상: list[dict[str, Any]]
def _bad(status: int, text: Any) -> store.StoreError:
return store.StoreError(status, text)
@@ -92,10 +101,7 @@ def _user_index() -> dict[str, list[tuple[Path, str, str]]]:
def _can_write(session: Session, at: refs.Here) -> bool:
try:
return access.grant(session, at[0], at[1]).write
except HTTPException:
return False
return deleting.can_write(session, at)
def _retarget(doc: dict[str, Any], olds: set[str], new: refs.Ref) -> dict[str, Any]:
@@ -144,17 +150,23 @@ def _gone_rows(folder: Path, kind: str) -> list[dict[str, Any]]:
rec = json.loads(path.read_bytes())
except (OSError, ValueError):
continue
if rec.get("종류") == kind:
if rec.get("종류") != kind:
continue
if "지움" in rec: # 나무 지움(735-1) — 남김 없음 · 이름 = 지운 문서들
names = [(d.get("문서") or {}).get("이름") or d.get("키") for d in rec["지움"]]
row = {"이름": " · ".join(map(str, names)), "남김": None, "고친": rec.get("고친곳")}
else:
doc = rec.get("문서") or {}
out.append(
{
"키": rec.get("키"),
"이름": doc.get("이름") or rec.get("키"),
"남김": rec.get("남김"),
"때": rec.get("때"),
"옮긴곳수": len(rec.get("옮긴곳") or []),
}
)
row = {"이름": doc.get("이름") or rec.get("키"), "남김": rec.get("남김"), "고친": None}
out.append(
{
"키": path.stem,
"이름": row["이름"],
"남김": row["남김"],
"때": rec.get("때"),
"옮긴곳수": len(row["고친"] or rec.get("옮긴곳") or []),
}
)
return out
@@ -275,6 +287,10 @@ def undo(session: Session, folder: Path, kind: str, key: str) -> dict[str, Any]:
raise _bad(404, f"합친 것에 없음 「{key}」") from None
if rec.get("종류") != kind:
raise _bad(404, f"합친 것에 없음 「{key}」")
if "지움" in rec:
got = deleting.undo(session, folder, kind, rec)
rec_path.unlink()
return {"되돌림": key, **got, **find(folder, kind)}
path = folder / kind / f"{key}.json"
if path.exists():
raise _bad(409, f"「{key}」 키를 그새 다른 문서가 씀 — 되돌릴 수 없음")
@@ -328,6 +344,32 @@ def library_same_merge(
return _run(merge, session, _root(session, layer, owner, write=True), kind, body)
def check(session: Session, folder: Path, kind: str, picks: list[dict[str, Any]]) -> dict:
"""나무 지우기 미리 보기 — 나무 고침을 셈만(저장 안 함) → 지울 문서 · 쓰는 줄."""
with store.at(folder):
tree_doc, _ = pre_folders._read(kind)
try:
_, note = pre_folders.apply_many(
tree_doc, {"op": "delete", "대상": picks}, pre_folders._keys(kind)
)
except pre_folders.tree.TreeError as e:
raise _bad(e.status, e.detail) from e
gone = note.get("지운문서") or []
return {"문서": gone, "쓰는곳": deleting.users(session, kind, gone)}
@router.post("/layers/{layer}/{kind}-tree/delete-check")
def library_delete_check(
layer: Layer,
kind: Kind,
body: CheckBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""나무 지우기 미리 보기(735-1) — `{문서, 쓰는곳}`(`M02_Library_Delete.users` 줄)."""
return _run(check, session, _root(session, layer, owner, write=True), kind, body.대상)
@router.post("/layers/{layer}/{kind}-same/undo")
def library_same_undo(
layer: Layer,