feat(일위대가실무): 324-3 실무 호표 → 우리 일위대가 사전조합 만들기 · 사전조합 줄 별칭 살림
- M02_Hopyo_Build: 호표 = 사전조합 하나(이름 = 품명 + 규격 · 엑셀 단위 · 구분 실무자료 · 상세구분 파일 · 본뜬 실무자료 표시) · 로직 줄 {별칭 = 엑셀 품명 40 자 · 메모 「실무 0.14㎥ × 1 · 규격」 · 입력값 · 수량 손 = 엑셀 × 환산} · 자재 줄(값 없음 = 별도계상 · 아니면 직접계산 · 같은 자재 수량 더함) · 같은 로직 둘째 = 한 줄 사전조합으로 감쌈 · 일위N = 먼저 만들어 참조 · 순환 = 빠진 줄 · 같은 층 · 파일 · 호표 = 다시 씀 · 새로 = 그 키에 덮음 · 빠진 줄 + 엑셀 금액 · 만든 값 ↔ 엑셀 단가
- M02_Labor_Ref._combo_lines: 별칭 있으면 남김(한 줄)
- 길 POST /layers/{층}/hopyo-build(이 층 쓰기 · 파일 층 읽기)
- 시험 test_m02_hopyo_build 5 통과 · 곁 시험 84 통과
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
"""M02 일위대가 실무자료 → 우리 일위대가 사전조합(PLAN 324-3).
|
||||
|
||||
호표 하나 = 사전조합 하나 — 이름 = 품명 + 규격 · 단위 = 엑셀 단위 · 구분 「실무자료」 ·
|
||||
상세구분 = 파일 · 본뜬 = {층 「실무자료」, 키 「HP-###### 제N호표」, 파일} ·
|
||||
만드는 층 = 부르는 층(일위대가 문서 층). 들어가는 줄(`M02_Hopyo_Match` 의 `들어감`)만:
|
||||
로직 = {로직, 별칭 = 엑셀 품명 40 자, 메모 = 「실무 0.14㎥ × 1 · 규격」, 입력값,
|
||||
수량 {손 = 엑셀 수량 × 환산}} · 같은 로직 둘째부터 = 그 줄 하나짜리 사전조합으로 감싸 참조
|
||||
(같은 로직 두 번 금지) · 자재 = {코드, 수량 {손}, 별도계상 = 엑셀 값 없음 「별도계상」 ·
|
||||
아니면 「직접계산」}(같은 자재 = 수량 더함) · 일위N = 그 호표 사전조합을 먼저 만들어 참조
|
||||
(순환 = 빠진 줄).
|
||||
같은 층 · 같은 파일 · 같은 호표 = 있던 것을 다시 씀 · `새로` = 그 키에 다시 만들어 덮음(감싼 것도).
|
||||
빠진 줄(% · 못 맞춤 · 빼기 · 환산 입력 · 추정) = 엑셀 금액과 함께 보고 · 만든 값 ↔ 엑셀 호표 단가.
|
||||
엔진 · 일위대가 계산은 그대로(`M02_Labor_Ref.Expand`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as match
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
|
||||
ORIGIN = "실무자료"
|
||||
WHY = {
|
||||
"%": "% 줄 — 우리 로직 덧줄이 셈",
|
||||
"빼기": "뺌(확정)",
|
||||
"후보": "못 맞춤 — 후보 확정 필요",
|
||||
"없음": "못 맞춤",
|
||||
}
|
||||
|
||||
|
||||
def mark(key: str, no: int, at: int | None = None) -> str:
|
||||
"""본뜬 키 — 호표 「HP-000001 제3호표」 · 감싼 줄 「… 2째 줄」."""
|
||||
head = f"{key} 제{no}호표"
|
||||
return head if at is None else f"{head} {at + 1}째 줄"
|
||||
|
||||
|
||||
def _found(origin: str) -> tuple[str, str] | None:
|
||||
"""지금 층 사전조합 가운데 본뜬 = 이 실무 표시 → (키, 판)."""
|
||||
for path in sorted((store.root() / "precombo").glob("SC-*.json")):
|
||||
try:
|
||||
doc = json.loads(path.read_bytes())
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
got = doc.get("본뜬") if isinstance(doc, dict) else None
|
||||
if isinstance(got, dict) and got.get("층") == ORIGIN and got.get("키") == origin:
|
||||
return path.stem, store.version_of(path.read_bytes())
|
||||
return None
|
||||
|
||||
|
||||
def _save(doc: dict[str, Any], fresh: bool) -> tuple[dict[str, Any], bool]:
|
||||
"""(참조, 다시씀) — 있으면 그대로(`fresh` 면 그 키에 덮음) · 없으면 새 키."""
|
||||
at = refs.here()
|
||||
have = _found(doc["본뜬"]["키"])
|
||||
if have and not fresh:
|
||||
return {"층": at[0], "주인": at[1], "키": have[0]}, True
|
||||
if have:
|
||||
store.write("precombo", have[0], have[1], doc)
|
||||
key = have[0]
|
||||
else:
|
||||
key = refs.create("precombo", doc)["이름"]
|
||||
return {"층": at[0], "주인": at[1], "키": key}, bool(have)
|
||||
|
||||
|
||||
def _num(value: Decimal) -> int | float:
|
||||
value = value.normalize()
|
||||
return int(value) if value == value.to_integral_value() else float(value)
|
||||
|
||||
|
||||
def _memo(line: dict[str, Any], times: Any) -> str:
|
||||
qty = line.get("수량")
|
||||
spec = str(line.get("규격") or "").strip()
|
||||
head = f"실무 {qty if qty is not None else ''}{line.get('단위') or ''} × {times}"
|
||||
return f"{head} · {spec}" if spec else head
|
||||
|
||||
|
||||
def _missing(line: dict[str, Any], why: str) -> dict[str, Any]:
|
||||
keep = ("차례", "이름", "규격", "수량", "단위")
|
||||
return {
|
||||
**{k: line.get(k) for k in keep},
|
||||
"금액": (line.get("금액") or {}).get("계"),
|
||||
"까닭": why,
|
||||
}
|
||||
|
||||
|
||||
class Builder:
|
||||
"""한 번 만들기 — 파일 · 층 기억 · 우리 쪽 목록을 한 번 읽고 호표마다 만든 참조를 기억."""
|
||||
|
||||
def __init__(self, source: Path, folder: Path, key: str, fresh: bool) -> None:
|
||||
self.doc = hopyo_store.read(source, key)
|
||||
self.key, self.fresh = key, fresh
|
||||
self.memo, self.units = match.memories(folder), match.units_of(self.doc)
|
||||
self.got = match.targets()
|
||||
self.made: dict[int, dict[str, Any]] = {}
|
||||
self.missing: dict[int, list[dict[str, Any]]] = {}
|
||||
|
||||
def hopyo(self, no: int) -> dict[str, Any]:
|
||||
found = next((h for h in self.doc.get("호표") or [] if int(h["번"]) == int(no)), None)
|
||||
if found is None:
|
||||
raise store.StoreError(404, f"없는 호표 「제{no}호표」")
|
||||
return found
|
||||
|
||||
def build(self, no: int, trail: tuple[int, ...] = ()) -> dict[str, Any]:
|
||||
"""호표 → 사전조합 참조 — 일위N 은 먼저(순환은 빠진 줄)."""
|
||||
if no in self.made:
|
||||
return self.made[no]
|
||||
hopyo = match.match_hopyo(self.hopyo(no), self.memo, self.units, True, self.got)
|
||||
lines, mats, missing = [], {}, []
|
||||
used: set[str] = set()
|
||||
for line in hopyo["줄"]:
|
||||
got = line["맞춤"]
|
||||
if not got["들어감"]:
|
||||
conv = got.get("환산") or {}
|
||||
why = WHY.get(got["상태"]) or (
|
||||
"추정 환산 — 확정 필요" if conv.get("갈래") == "추정" else "환산 입력 필요"
|
||||
)
|
||||
missing.append(_missing(line, why))
|
||||
continue
|
||||
times = got["환산"]["배"]
|
||||
qty = Decimal(str(line.get("수량") or 0)) * Decimal(str(times))
|
||||
target = got["대상"]
|
||||
alias = str(line.get("이름") or "")[: labor.ALIAS_MAX].strip() or None
|
||||
memo = _memo(line, times)
|
||||
if "자재" in target:
|
||||
code = target["자재"]
|
||||
apart = "별도계상" if line.get("갈래") == "별도계상" else "직접계산"
|
||||
have = mats.get(code)
|
||||
hand = qty + Decimal(str(have["수량"]["손"])) if have else qty
|
||||
mats[code] = {"코드": code, "수량": {"손": _num(hand)}, "별도계상": apart}
|
||||
continue
|
||||
if "호표" in target:
|
||||
child = int(target["호표"])
|
||||
if child in (*trail, no):
|
||||
missing.append(_missing(line, "순환 — 호표가 서로 담음"))
|
||||
continue
|
||||
ref = self.build(child, (*trail, no))
|
||||
lines.append({"참조": ref, "별칭": alias, "메모": memo, "수량": {"손": _num(qty)}})
|
||||
continue
|
||||
one = {"로직": target["로직"], "입력값": got.get("입력값") or {}, "메모": memo}
|
||||
if target["로직"] not in used:
|
||||
used.add(target["로직"])
|
||||
lines.append({**one, "별칭": alias, "수량": {"손": _num(qty)}})
|
||||
continue
|
||||
wrap = { # 같은 로직 둘째 — 한 줄 사전조합으로 감쌈
|
||||
"이름": f"{hopyo['이름']} — {line.get('이름')}",
|
||||
"단위": target.get("단위"),
|
||||
"구분": ORIGIN,
|
||||
"상세구분": self.doc.get("파일") or "",
|
||||
"담은로직": [{**one, "별칭": alias, "수량": {"손": 1}}],
|
||||
"자재": [],
|
||||
"본뜬": self._origin(mark(self.key, no, line["차례"])),
|
||||
}
|
||||
ref, _ = _save(wrap, self.fresh)
|
||||
lines.append({"참조": ref, "별칭": alias, "메모": memo, "수량": {"손": _num(qty)}})
|
||||
name = " ".join(x for x in (hopyo["이름"], hopyo.get("규격")) if x)
|
||||
doc = {
|
||||
"이름": name,
|
||||
"단위": hopyo.get("단위") or None,
|
||||
"구분": ORIGIN,
|
||||
"상세구분": self.doc.get("파일") or "",
|
||||
"담은로직": lines,
|
||||
"자재": list(mats.values()),
|
||||
"본뜬": self._origin(mark(self.key, no)),
|
||||
}
|
||||
ref, again = _save(doc, self.fresh)
|
||||
self.made[no] = {**ref, "다시씀": again}
|
||||
self.missing[no] = missing
|
||||
return self.made[no]
|
||||
|
||||
def _origin(self, origin: str) -> dict[str, Any]:
|
||||
return {"층": ORIGIN, "주인": None, "키": origin, "파일": self.doc.get("파일")}
|
||||
|
||||
|
||||
def compare(ref: dict[str, Any], hopyo: dict[str, Any]) -> dict[str, Any]:
|
||||
"""만든 사전조합 1 단위 값 ↔ 엑셀 호표 단가."""
|
||||
got = refs.Expand(m01.loaded()[1])({k: ref[k] for k in ("층", "주인", "키")})
|
||||
base = got.get("기준") or {}
|
||||
ours = float(base["계"]) if "계" in base else None
|
||||
excel = (hopyo.get("금액") or {}).get("계")
|
||||
gap = round(ours - excel, 1) if ours is not None and excel is not None else None
|
||||
return {"엑셀": excel, "우리": ours, "차이": gap, "까닭": got.get("까닭")}
|
||||
|
||||
|
||||
def build(folder: Path, source: Path, key: str, no: int, fresh: bool = False) -> dict[str, Any]:
|
||||
"""실무 호표(`source` 층 파일) → 지금 층(`folder`) 사전조합 —
|
||||
`{참조, 이름, 다시씀, 빠진줄, 비교}`(부르는 쪽이 `store.at(folder)`)."""
|
||||
maker = Builder(source, folder, key, fresh)
|
||||
made = maker.build(int(no))
|
||||
hopyo = maker.hopyo(int(no))
|
||||
ref = {k: made[k] for k in ("층", "주인", "키")}
|
||||
return {
|
||||
"참조": ref,
|
||||
"이름": hopyo.get("이름"),
|
||||
"다시씀": made["다시씀"],
|
||||
"빠진줄": maker.missing.get(int(no), []),
|
||||
"비교": compare(ref, hopyo),
|
||||
}
|
||||
@@ -210,6 +210,7 @@ def _combo_lines(doc: dict, key: str) -> list[dict[str, Any]]:
|
||||
if "로직" in one:
|
||||
head["입력값"] = one["입력값"]
|
||||
head.update({k: one[k] for k in ("켜쌓기율", "고름", "자재표시") if k in one})
|
||||
head.update({"별칭": one["별칭"]} if one["별칭"] else {}) # 실무 품명(324-3)
|
||||
lines.append({**head, "수량": one["수량"] or {"손": 1}, "메모": one["메모"]})
|
||||
at_ = here()
|
||||
me = {"층": at_[0], "주인": at_[1], "키": key}
|
||||
|
||||
@@ -18,6 +18,7 @@ from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||
from pydantic import BaseModel
|
||||
|
||||
from common_util.common_util_auth import verify_session
|
||||
from M02_MasterTemplete import M02_Hopyo_Build as hopyo_build
|
||||
from M02_MasterTemplete import M02_Hopyo_Excel as hopyo_excel
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||||
@@ -266,6 +267,30 @@ def hopyo_memory(
|
||||
return _call(folder, hopyo_match.confirm, folder, body.줄)
|
||||
|
||||
|
||||
class BuildBody(BaseModel):
|
||||
"""실무 호표 → 이 층 사전조합 — 파일 층 `층` · `주인` · `키` + 호표 `번` · `새로` = 있던 것을 덮음."""
|
||||
|
||||
층: Layer
|
||||
주인: str | None = None
|
||||
키: str
|
||||
번: int
|
||||
새로: bool = False
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/hopyo-build")
|
||||
def hopyo_make(
|
||||
layer: Layer,
|
||||
body: BuildBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""실무 호표 → 우리 일위대가 사전조합(이 층 쓰기 · 파일 층 읽기) —
|
||||
`{참조, 이름, 다시씀, 빠진줄, 비교}`."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
source = _root(session, body.층, body.주인)
|
||||
return _call(folder, hopyo_build.build, folder, source, body.키, body.번, body.새로)
|
||||
|
||||
|
||||
@router.delete("/layers/{layer}/hopyo/{key}")
|
||||
def hopyo_delete(
|
||||
layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session)
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
"""일위대가 실무자료 → 사전조합 만들기(PLAN 324-3 · `M02_Hopyo_Build`) — 별칭 남음 ·
|
||||
수량 = 엑셀 × 환산 · 자재 · 같은 로직 둘 = 감쌈 · 일위N 참조 · 순환 ·
|
||||
같은 호표 다시 = 같은 사전조합 · 새로 = 덮음 ·
|
||||
빠진 줄 · 값 비교 · 길 권한 · master_data 원본 그대로. 층은 임시 폴더.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hs
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
|
||||
# 올리기 길 시험의 `client` 마당을 같이 씀
|
||||
# ruff: noqa: F811
|
||||
from resources.tester.test_m02_hopyo_excel import URL, _post, client, master_hash # noqa: F401
|
||||
|
||||
DIG = "구조물터파기(인력100%)|토사|m3"
|
||||
FILL = "구조물되메우기(인력100%)||m3"
|
||||
SOIL = "잔토처리(인력100%)||m3"
|
||||
STONE = "임도표지석(받침,글새김포함)|900x600x200x150|m3"
|
||||
MEMORY = "/api/m02/layers/{}/hopyo-memory"
|
||||
BUILD = "/api/m02/layers/{}/hopyo-build"
|
||||
|
||||
|
||||
def _remember(client, layer="personal", **lines):
|
||||
res = client.post(MEMORY.format(layer), json={"줄": lines})
|
||||
assert res.status_code == 200, res.text
|
||||
|
||||
|
||||
def _build(client, no, layer="personal", want=200, **extra):
|
||||
body = {"층": "personal", "키": "HP-000001", "번": no, **extra}
|
||||
res = client.post(BUILD.format(layer), json=body)
|
||||
assert res.status_code == want, res.text
|
||||
return res.json()
|
||||
|
||||
|
||||
def _doc(layer_dir, key):
|
||||
return json.loads((layer_dir / "precombo" / f"{key}.json").read_bytes())
|
||||
|
||||
|
||||
def test_만들기_별칭_수량_메모_빠진_줄_비교(client):
|
||||
before = master_hash()
|
||||
assert _post(client).status_code == 200
|
||||
_remember(
|
||||
client,
|
||||
**{
|
||||
DIG: {"로직": "GF000101", "입력값": {"지역": "서울"}, "환산": 1},
|
||||
FILL: {"로직": "GF000113", "환산": 2},
|
||||
},
|
||||
)
|
||||
got = _build(client, 3)
|
||||
mine = layers.personal_dir(7, 42)
|
||||
doc = _doc(mine, got["참조"]["키"])
|
||||
assert (doc["이름"], doc["단위"], doc["구분"]) == ("임도표지석 910×600×150", "개소", "실무자료")
|
||||
assert doc["상세구분"].endswith("(공통단가).xls")
|
||||
assert doc["본뜬"]["층"] == "실무자료" and doc["본뜬"]["키"] == "HP-000001 제3호표"
|
||||
assert doc["담은로직"] == [
|
||||
{
|
||||
"로직": "GF000101",
|
||||
"입력값": {"지역": "서울"},
|
||||
"별칭": "구조물터파기(인력100%)",
|
||||
"수량": {"손": 0.14},
|
||||
"메모": "실무 0.14㎥ × 1 · 토사",
|
||||
},
|
||||
{
|
||||
"로직": "GF000113",
|
||||
"입력값": {},
|
||||
"별칭": "구조물되메우기(인력100%)",
|
||||
"수량": {"손": 0.1}, # 엑셀 0.05 × 환산 2
|
||||
"메모": "실무 0.05㎥ × 2",
|
||||
},
|
||||
]
|
||||
assert [(x["이름"], x["금액"]) for x in got["빠진줄"]] == [
|
||||
("임도표지석(받침,글새김포함)", 1400000),
|
||||
("잔토처리(인력100%)", 3097.1),
|
||||
]
|
||||
assert got["비교"]["엑셀"] == 1415169 and got["비교"]["우리"] > 0
|
||||
assert got["비교"]["차이"] == round(got["비교"]["우리"] - 1415169, 1)
|
||||
assert master_hash() == before
|
||||
|
||||
|
||||
def test_자재_같은_로직_감쌈_다시씀_새로(client):
|
||||
assert _post(client).status_code == 200
|
||||
_remember(
|
||||
client,
|
||||
**{
|
||||
DIG: {"로직": "GF000101", "환산": 1},
|
||||
SOIL: {"로직": "GF000101", "환산": 1},
|
||||
STONE: {"자재": "MT004258", "환산": 1},
|
||||
},
|
||||
)
|
||||
got = _build(client, 3)
|
||||
mine = layers.personal_dir(7, 42)
|
||||
doc = _doc(mine, got["참조"]["키"])
|
||||
first, wrapped = doc["담은로직"]
|
||||
assert first["로직"] == "GF000101" and first["별칭"] == "구조물터파기(인력100%)"
|
||||
assert wrapped["별칭"] == "잔토처리(인력100%)" and wrapped["수량"] == {"손": 0.09}
|
||||
inner = _doc(mine, wrapped["참조"]["키"])
|
||||
assert inner["본뜬"]["키"] == "HP-000001 제3호표 4째 줄"
|
||||
assert inner["담은로직"][0]["로직"] == "GF000101" and inner["담은로직"][0]["수량"] == {"손": 1}
|
||||
assert [(m["코드"], m["수량"], m["별도계상"]) for m in doc["자재"]] == [
|
||||
("MT004258", {"손": 1}, "직접계산")
|
||||
]
|
||||
files = sorted(p.name for p in (mine / "precombo").glob("*.json"))
|
||||
again = _build(client, 3)
|
||||
assert again["다시씀"] is True and again["참조"] == got["참조"]
|
||||
assert sorted(p.name for p in (mine / "precombo").glob("*.json")) == files
|
||||
_remember(client, **{SOIL: {"빼기": True}})
|
||||
fresh = _build(client, 3, 새로=True)
|
||||
assert fresh["참조"] == got["참조"] and fresh["다시씀"] is True
|
||||
assert len(_doc(mine, got["참조"]["키"])["담은로직"]) == 1 # 덮음
|
||||
assert sorted(p.name for p in (mine / "precombo").glob("*.json")) == files
|
||||
|
||||
|
||||
def test_일위N_참조_먼저_만듦(client):
|
||||
assert _post(client).status_code == 200
|
||||
got = _build(client, 45)
|
||||
mine = layers.personal_dir(7, 42)
|
||||
lines = _doc(mine, got["참조"]["키"])["담은로직"]
|
||||
ref = lines[0]
|
||||
assert ref["별칭"] == "고화매트재료비" and ref["수량"] == {"손": 2.3}
|
||||
child = _doc(mine, ref["참조"]["키"])
|
||||
assert child["이름"] == "고화매트재료비" and child["본뜬"]["키"] == "HP-000001 제46호표"
|
||||
assert any(x["까닭"].startswith("% 줄") for x in _build(client, 46)["빠진줄"])
|
||||
|
||||
|
||||
def test_순환은_빠진_줄(client):
|
||||
folder = layers.personal_dir(7, 42)
|
||||
_post(client) # 층 뼈대
|
||||
line = {"차례": 0, "이름": "남", "규격": "", "수량": 1, "단위": "식", "갈래": "호표"}
|
||||
money = {"계": 1, "노무비": 1, "재료비": None, "경비": None}
|
||||
hopyos = [
|
||||
{"번": 1, "이름": "가", "규격": "", "단위": "식", "금액": money, "줄": [
|
||||
{**line, "참조": {"종류": "일위", "번": 2}, "금액": money}]},
|
||||
{"번": 2, "이름": "나", "규격": "", "단위": "식", "금액": money, "줄": [
|
||||
{**line, "참조": {"종류": "일위", "번": 1}, "금액": money}]},
|
||||
] # fmt: skip
|
||||
key = hs.save(folder, "loop.xls", {"시트": "일위대가", "호표": hopyos, "검산": {}}, None)["키"]
|
||||
got = _build(client, 1, 키=key)
|
||||
assert got["빠진줄"] == []
|
||||
inner = _doc(folder, _doc(folder, got["참조"]["키"])["담은로직"][0]["참조"]["키"])
|
||||
assert inner["담은로직"] == [] # 제2호표 안 제1호표 = 순환 → 빠짐
|
||||
|
||||
|
||||
def test_길_권한과_위_층_파일(client):
|
||||
client.who.update(role="SYSTEM_ADMIN")
|
||||
assert _post(client, "system").status_code == 200
|
||||
client.who.update(role="USER")
|
||||
_build(client, 3, layer="system", 층="system", want=403) # 마스터 쓰기 = 시스템 관리자
|
||||
got = _build(client, 3, 층="system") # 마스터 파일 → 내 층 사전조합
|
||||
assert got["참조"]["층"] == "personal" and got["참조"]["주인"] == "7/42"
|
||||
assert _build(client, 99, 층="system", want=404)
|
||||
assert client.get(URL.format("personal")).json()["줄"] == [] # 파일은 마스터에만
|
||||
Reference in New Issue
Block a user