refactor(815-2): 옛 별칭 장부 걷기 — M02 장부 화면 · 장부 길 · 불러오기 확인 장부 쓰기 · 공사 내역 맞춤 기억
- M02 왼쪽 [별칭 장부] · M02_MasterTemplete_Hopyo_Ledger.ts · 장부 타입 · css 지움
- 길 GET · PUT /api/m02/layers/{층}/hopyo-ledger 지움 · M02_Hopyo_Ledger 의 장부 화면 줄(rows · 승인 상태) 지움
- 일위대가 불러오기 확인(M02_Hopyo_Build) = 장부에 안 적음 · 가져온 식 저장(M02_Doc_Router) = 기계 장부에 안 적음
- B09 공사 내역 맞춤 기억(내역맞춤.json 읽기 · 쓰기 · 이 짝 잊기 길 · 단추) 걷음 — 제안은 짝 표만
- storage 옛 장부 파일(맞춤 · 내역맞춤 · 별칭반려)은 읽지 않게만 · 지우지 않음
- M02_Hopyo_Ledger 모듈은 남김 — 짝 검사 · 열쇠 도우미 · 요소 잇기의 옛 장부 읽기(M02_Machine_Find · M02_Hopyo_Units · M02_Hopyo_Sangeun — 815-1 몫)
- 시험: test_718_2_alias_gather · test_739_1_alias_central · test_m02_hopyo_build 지움(걷은 길 · 화면 지킴) · test_707_4 기억 층 · 잊기 시험을 「걷음」 시험으로 · test_m02_hopyo_ledger rows 줄 · test_m02_hopyo_screen · test_721 장부 화면 줄 고침
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,14 +5,10 @@
|
||||
저절로: 지난번 이음 · 하나 수 · 남은 · 자리 · 이은(`board`)}
|
||||
POST /api/projects/{id}/estimation/link/apply
|
||||
{pairs [{line, target, mode?}], auto, mode, counts?, user?} · 알림 남은 = 안 이은 항목 수
|
||||
잇기(`link`) — `auto` = 지난번 이음 · 하나도 더함 →
|
||||
{user(새 캐시), 알림, payload(그 캐시로 셈)} ·
|
||||
이은 짝 = 맞춤 기억에 씀(회사 층 = 회사 관리자 · 아니면 개인 층)
|
||||
POST …/link/undo {target, user?} → {user, 알림, payload} — 잇기 풀기(맞춤 기억 그대로)
|
||||
POST …/link/forget {key, user?} → {잊음, 기억층, 남음} — 이 짝 잊기(불러온 줄 · 이은 줄 키 →
|
||||
맞춤 열쇠 · 쓸 수 있는 층에 잊음 표시 `{잊음: True}` — 느슨한 기억도 안 불러옴 ·
|
||||
개인 층 표시가 회사 기억을 덮음 · 남음 = 그래도 살아 있는 기억)
|
||||
맞춤 기억 = 층 `실무자료/내역맞춤.json`(실무자료 맞춤 꼴) · 읽기 = 회사 + 개인(개인이 이김).
|
||||
잇기(`link`) → {user(새 캐시), 알림, payload(그 캐시로 셈)}
|
||||
POST …/link/undo {target, user?} → {user, 알림, payload} — 잇기 풀기
|
||||
공사 내역 맞춤 기억(`실무자료/내역맞춤.json` · 지난번 이음 · 잊기)은 걷음(815-2 · 요구 765-2) —
|
||||
제안은 짝 표만 · 옛 기억 파일은 읽지도 쓰지도 않음(지우지 않음).
|
||||
끼워 넣기 = `B09_Estimation_Router`(같은 머리 · 같은 권한) · 화면 = `B09_Estimation_UI_Link`.
|
||||
"""
|
||||
|
||||
@@ -30,42 +26,9 @@ from B08_Quantity.B08_Quantity_Router_Hydro import _project_root
|
||||
from B09_Estimation import B09_Estimation_Bill_Link as linker
|
||||
from B09_Estimation import B09_Estimation_Store as store
|
||||
from common_util.common_util_auth import verify_session
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||||
from M02_MasterTemplete import M02_Layer_Access as access
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as m02_store
|
||||
|
||||
router = APIRouter()
|
||||
PATH = "/{project_id}/estimation/link"
|
||||
LAYERS = ("company", "personal")
|
||||
|
||||
|
||||
def _folders(session: dict[str, Any]) -> dict[str, tuple[Path, bool]]:
|
||||
"""{층: (폴더, 쓰기)} — 회사 없음 등 = 그 층 건너뜀."""
|
||||
out = {}
|
||||
for layer in LAYERS:
|
||||
try:
|
||||
got = access.grant(session, layer)
|
||||
out[layer] = (access.folder(got, create=True), got.write)
|
||||
except HTTPException:
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def recalled(session: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
||||
"""맞춤 기억 — 마스터(시스템 관리자 승인 · 718-2) 위에 회사 · 그 위에 개인을 덮음."""
|
||||
out: dict[str, dict[str, Any]] = dict(hopyo_store.memory(m02_store.FOLDER, linker.MEMORY))
|
||||
for folder, _write in _folders(session).values():
|
||||
out.update(hopyo_store.memory(folder, linker.MEMORY))
|
||||
return out
|
||||
|
||||
|
||||
def keep(session: dict[str, Any], entries: dict[str, dict[str, Any]]) -> str | None:
|
||||
"""이은 짝 → 회사 층(쓸 수 있으면) · 아니면 개인 층 → 쓴 층(없음 = None)."""
|
||||
folders = _folders(session)
|
||||
layer = next((n for n in LAYERS if n in folders and folders[n][1]), None)
|
||||
if entries and layer:
|
||||
hopyo_store.remember(folders[layer][0], entries, linker.MEMORY)
|
||||
return layer if entries else None
|
||||
|
||||
|
||||
def _draft(raw: Any) -> dict[str, Any] | None:
|
||||
@@ -87,11 +50,6 @@ class UndoBody(BaseModel):
|
||||
user: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class ForgetBody(BaseModel):
|
||||
key: str = Field(min_length=1, max_length=200)
|
||||
user: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class ApplyBody(BaseModel):
|
||||
pairs: list[Pair] = Field(default_factory=list, max_length=2000)
|
||||
auto: bool = False
|
||||
@@ -115,8 +73,7 @@ async def suggest(
|
||||
rows = await _rows(project_id, root, draft)
|
||||
board = linker.board(draft, rows)
|
||||
left = {r["key"] for r in board["남은"]}
|
||||
memory = await asyncio.to_thread(recalled, session)
|
||||
lines = [s for s in linker.suggest(rows, memory) if s["key"] in left]
|
||||
lines = [s for s in linker.suggest(rows, {}) if s["key"] in left]
|
||||
return {"줄": lines, "저절로": len(linker.auto_pairs(lines)), **board}
|
||||
|
||||
|
||||
@@ -134,15 +91,14 @@ async def apply(
|
||||
rows = await _rows(project_id, root, draft)
|
||||
pairs = [(p.line, p.target) for p in body.pairs]
|
||||
if body.auto:
|
||||
memory = await asyncio.to_thread(recalled, session)
|
||||
pairs += [p for p in linker.auto_pairs(linker.suggest(rows, memory)) if p not in pairs]
|
||||
pairs += [p for p in linker.auto_pairs(linker.suggest(rows, {})) if p not in pairs]
|
||||
made, report = linker.link(draft, rows, pairs, body.mode, body.counts, modes)
|
||||
made = store.clean_user(made)
|
||||
layer = await asyncio.to_thread(keep, session, report.pop("기억"))
|
||||
report.pop("기억")
|
||||
out = await payload(project_id, root, made, True)
|
||||
left = linker.board(made, out["sheets"]["bill"]["rows"])["남은"]
|
||||
report["남은"] = sum(1 for r in left if r["kind"] == "item")
|
||||
return {"user": made, "알림": {**report, "기억층": layer}, "payload": out}
|
||||
return {"user": made, "알림": report, "payload": out}
|
||||
|
||||
|
||||
@router.post(PATH + "/undo")
|
||||
@@ -162,21 +118,3 @@ async def undo(
|
||||
"알림": {"풀음": 1},
|
||||
"payload": await payload(project_id, root, made, True),
|
||||
}
|
||||
|
||||
|
||||
@router.post(PATH + "/forget")
|
||||
async def forget(
|
||||
project_id: UUID, body: ForgetBody, session: dict[str, Any] = Depends(verify_session)
|
||||
) -> dict[str, Any]:
|
||||
root = await _project_root(project_id)
|
||||
draft = _draft(body.user) or await asyncio.to_thread(store.read_user, root)
|
||||
rows = await _rows(project_id, root, draft)
|
||||
board = linker.board(draft, rows)
|
||||
hit = next((r for r in board["남은"] if r["key"] == body.key), None)
|
||||
memo = linker.key_of(hit) if hit else None
|
||||
memo = memo or next((r["memo"] for r in board["이은"] if r["key"] == body.key), None)
|
||||
if not memo:
|
||||
raise HTTPException(status_code=400, detail="불러온 줄 · 이은 줄이 아님")
|
||||
layer = await asyncio.to_thread(keep, session, {memo: {linker.FORGOT: True}})
|
||||
left = bool((await asyncio.to_thread(recalled, session)).get(memo, {}).get("확정"))
|
||||
return {"잊음": memo, "기억층": layer, "남음": left}
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
* 우리 자동 줄(토공 · 운반 · 구조물)에 이어 B08 수량이 붙게. 저장 안 함(받은 캐시 → [저장] 때 서버 정본).
|
||||
*
|
||||
* 길 = `B09_Estimation_Router_Link.py` link/suggest(제안 · 남은 줄 · 빈 자리 · 이은 줄) ·
|
||||
* link/apply(잇기 · 맞춤 기억 씀) · link/undo(잇기 풀기) · link/forget(이 짝 잊기) — 셈 = 서버 단독
|
||||
* link/apply(잇기) · link/undo(잇기 풀기) — 셈 = 서버 단독 · 맞춤 기억 · 잊기는 걷음(815-2)
|
||||
* 창 = 제안 줄(꼬리표 지난번 이음 · 짝 표 · 이름 같음 · 후보 고름 · 수량 갈래 · 묶음 개수 · [잇기]) ·
|
||||
* 제안 없는 줄([우리 공종으로 잇기] = 단가연결 고르기 창 재사용 · 우리 줄만) · 이은 줄([잇기 풀기] ·
|
||||
* [이 짝 잊기]) · 머리 [모두 제안대로](짧은 길 = 불러오기 → 모두 제안대로 → [저장])
|
||||
* 제안 없는 줄([우리 공종으로 잇기] = 단가연결 고르기 창 재사용 · 우리 줄만) · 이은 줄([잇기 풀기]) · 머리 [모두 제안대로](짧은 길 = 불러오기 → 모두 제안대로 → [저장])
|
||||
* 수량 갈래 기본 = 불러올 때 고른 것(가 넘겨받기 · 나 단가만) · 줄에서 바꿈 ·
|
||||
* 묶음 나 = 개수 칸(빈칸 = B08 수량 · 다르면 수량 칸 붉게)
|
||||
* 고리 = 불러오기 끝(`…_UI_Import`) · 설계내역서 우클릭(`…_UI_Tabs_Bill_Edit`)
|
||||
@@ -185,14 +184,6 @@ export async function openBillLink(
|
||||
.map((s) => [s.key, Number(pickOf(s).count)])
|
||||
.filter(([, n]) => Number.isFinite(n) && (n as number) > 0),
|
||||
);
|
||||
const forget = (key: string): void =>
|
||||
run(async () => {
|
||||
const got = await post("forget", { key, user: cur });
|
||||
showToast(
|
||||
got.남음 ? "이 짝 잊음 — 회사 기억이 남음(회사 관리자만 지움)" : "이 짝 잊음",
|
||||
got.남음 ? "warning" : "success",
|
||||
);
|
||||
});
|
||||
|
||||
const suggestRow = (s: Row): HTMLElement => {
|
||||
const p = pickOf(s);
|
||||
@@ -233,10 +224,6 @@ export async function openBillLink(
|
||||
onClick: () => run(() => linkPairs(cur, done, [pairOf(s)], countsOf([s]))),
|
||||
}),
|
||||
);
|
||||
if (s.꼬리표 === "지난번 이음")
|
||||
items.push(
|
||||
createButton({ label: "이 짝 잊기", variant: "ghost", onClick: () => forget(s.key) }),
|
||||
);
|
||||
return createListRow({
|
||||
label: label(s),
|
||||
sub: s.까닭 || (s.kind === "item" ? "" : "묶음 → 구조물 줄 통째"),
|
||||
@@ -269,7 +256,6 @@ export async function openBillLink(
|
||||
variant: "ghost",
|
||||
onClick: () => run(() => undoLink(cur, done, r.key)),
|
||||
}),
|
||||
createButton({ label: "이 짝 잊기", variant: "ghost", onClick: () => forget(r.key) }),
|
||||
],
|
||||
}),
|
||||
});
|
||||
@@ -326,23 +312,15 @@ async function undoLink(user: Row, done: LinkDone, target: string): Promise<Row>
|
||||
return got.user;
|
||||
}
|
||||
|
||||
/** 설계내역서 우클릭 — 불러온 줄 = 잇기 · 이은 줄(`was`) = 풀기 · 잊기 */
|
||||
/** 설계내역서 우클릭 — 불러온 줄 = 잇기 · 이은 줄(`was`) = 풀기 */
|
||||
export function linkMenu(ctx: TabContext, key: string): MapContextMenuItem[] {
|
||||
const done: LinkDone = (payload, user) => {
|
||||
Object.assign(ctx.payload, payload);
|
||||
ctx.setUser(user);
|
||||
};
|
||||
const entry = ((ctx.user.bill?.rows as Row[]) || []).find((e) => e.key === key);
|
||||
const forget = (): void =>
|
||||
void post("forget", { key, user: ctx.user }).then(
|
||||
(got) => showToast(got.남음 ? "회사 기억이 남음(회사 관리자만 지움)" : "이 짝 잊음", "info"),
|
||||
fail("못 잊음"),
|
||||
);
|
||||
if (entry?.was)
|
||||
return [
|
||||
["잇기 풀기", () => void undoLink(ctx.user, done, key).catch(fail("못 풂"))],
|
||||
["이 짝 잊기", forget],
|
||||
];
|
||||
return [["잇기 풀기", () => void undoLink(ctx.user, done, key).catch(fail("못 풂"))]];
|
||||
if (!key.startsWith("hand:x")) return [];
|
||||
return [["우리 공종으로 잇기", () => void openBillLink(ctx.user, done)]];
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
받아 `본뜬.전국자재 [{이름, 규격, 단위, 수량, 세 칸}]` 에 적고 빠진 줄에 까닭(금액 없음 ·
|
||||
지역은 프로젝트에서) · 장부엔 안 남김.
|
||||
같은 본뜬 사전조합이 있으면 확인한 내용으로 덮음(`다시씀`) · 없으면 같은 내용 SC 재사용(`재사용` · `M02_Hopyo_Kids.save`) ·
|
||||
`하위` = 부르는 하위 호표 `[{번, 키, 새로}]`(부모 줄 = 하위 사본) · 확인한 줄은 별칭 장부에 남음
|
||||
(`M02_Hopyo_Ledger` · 호표 참조 줄 뺌).
|
||||
`하위` = 부르는 하위 호표 `[{번, 키, 새로}]`(부모 줄 = 하위 사본) · 별칭 장부엔 안 남김(815-2 걷음 ·
|
||||
`M02_Hopyo_Ledger` 는 짝 검사 · 열쇠 도우미만).
|
||||
빠짐 0(705-3) — 확인 안 한 줄 · 노임 · % · 「우리에 없음」 줄 = 고정 줄(`M02_Hopyo_Fixed` · 엑셀 숫자
|
||||
그대로) · 확인 안 한 호표 줄 = 확인된 하위 사전조합 참조(단위 같음 · 아니면 고정) · 빠진 줄 = 「안 씀」 ·
|
||||
통째 대신 · 로직 자재 겹침 · 전국만. `본뜬.품셈` = 품셈으로 넣은 엑셀 줄 차례([품셈으로 바꾸기] 첫 체크) ·
|
||||
@@ -248,18 +248,14 @@ class Builder:
|
||||
nation, mat_lines = body.nation, body.mat_lines
|
||||
poomsem, kid_n = [], 0
|
||||
|
||||
book: dict[str, dict[str, Any]] = {}
|
||||
if whole:
|
||||
for one in whole:
|
||||
put({"수량": 1, "단위": hopyo.get("단위"), "이름": hopyo.get("이름")}, one, None)
|
||||
book[ledger.hopyo_key(hopyo)] = self._row(hopyo, "짝", whole)
|
||||
for at, line in enumerate(rows):
|
||||
got = checked.get(at)
|
||||
if whole:
|
||||
missing.append(_missing(line, WHOLE))
|
||||
continue
|
||||
if got and got.get("뺌"):
|
||||
book[ledger.key(line)] = self._row(line, got["뺌"], [])
|
||||
if got and got.get("뺌") == "안 씀":
|
||||
missing.append(_missing(line, f"뺌 — {got['뺌']}"))
|
||||
continue
|
||||
@@ -277,9 +273,6 @@ class Builder:
|
||||
alias = str(line.get("이름") or "")[: labor.ALIAS_MAX].strip() or None
|
||||
for one in got["대상"]:
|
||||
put(line, one, alias)
|
||||
pairs = [p for p in got["대상"] if "호표" not in p and "전국" not in p]
|
||||
if pairs:
|
||||
book[ledger.key(line)] = self._row(line, "짝", pairs)
|
||||
for code in self._inside(lines, mats): # 로직 자재와 같은 품목 = 세 칸만 로직 자재표시로
|
||||
missing.append(_missing(mat_lines[code], TWICE))
|
||||
name = " ".join(x for x in (hopyo["이름"], hopyo.get("규격")) if x)
|
||||
@@ -306,14 +299,11 @@ class Builder:
|
||||
count["단가산출"] = sum(1 for x in lines if _is_sk(x))
|
||||
sub = [{"번": n, "키": self.done.get(mark(self.key, n))} for n in sorted(kids)]
|
||||
if not save:
|
||||
ledger.write(self.folder, book, self.who, file or None, self.doc_id)
|
||||
head = {"문서": doc, "빠진줄": missing, "남은줄": left, "하위": sub}
|
||||
return {**head, "줄수": {**count, "이음후보": linker.near}}
|
||||
ref, again, self.reused = hkids.save(doc, self.same)
|
||||
wrote = ledger.write(self.folder, book, self.who, file or None, self.doc_id)
|
||||
out = {"참조": ref, "다시씀": again, "재사용": self.reused, "빠진줄": missing, "하위": sub}
|
||||
out["남은줄"] = left
|
||||
out["장부"] = {"씀": wrote}
|
||||
return {**out, "줄수": {**count, "이음후보": linker.near}}
|
||||
|
||||
def _kid(self, line: dict[str, Any], kids: set[int]) -> dict[str, Any] | None:
|
||||
@@ -351,17 +341,6 @@ class Builder:
|
||||
mats.pop(code)
|
||||
return moved
|
||||
|
||||
def _row(self, line: dict[str, Any], kind: str, pairs: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
"""장부 한 줄 — 엑셀조항 = 줄 근거 출처(있으면)."""
|
||||
origin = {"품명": line.get("이름"), "규격": line.get("규격"), "단위": line.get("단위")}
|
||||
clauses = (line.get("근거") or {}).get("출처") or []
|
||||
return {
|
||||
"원래": {k: str(v or "") for k, v in origin.items()},
|
||||
"갈래": kind,
|
||||
"짝": pairs,
|
||||
"엑셀조항": clauses if isinstance(clauses, list) else [],
|
||||
}
|
||||
|
||||
|
||||
def _order(units: dict[int, dict[str, Any]], start: int | None = None) -> list[int]:
|
||||
"""하위 호표 먼저(호표 줄 참조) · 고리는 끊음 · `start` = 그 호표와 하위만."""
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
(줄 `불러오기` = `doc_id`)만 · 회사 · 개인 층 짝 = 「내 제안」(`제안` True · 자동 아님 ·
|
||||
후보 맨 위 · M01 승인 대기).
|
||||
읽는 차례 = 자동 정확 열쇠(같은 층 → 위 층) → 제안 정확 열쇠 → 품명|단위 느슨 열쇠(`규격다름`).
|
||||
쓰는 때 = [확인하고 사전조합 만들기] · 장부 화면 [저장]만 · 쓰기 = 그 층만(권한은 길 몫) ·
|
||||
⚠ 815-2 — 장부 화면 · M01 승인 · 불러오기 확인 때 쓰기는 걷음(별칭 = M01 요소 「별칭」 칸) ·
|
||||
남은 것 = 짝 검사 · 열쇠 도우미 · 요소 잇기의 옛 장부 읽기(sub3 815-1 이 뗌) · 쓰기 = 그 층만 ·
|
||||
짝은 서버가 다시 검사(돈 로직 · 그 로직 입력 이름 · M01 자재품목 · 배 0 넘음 · 몫 (0, 1]).
|
||||
"""
|
||||
|
||||
@@ -137,39 +138,6 @@ def recall(
|
||||
return None
|
||||
|
||||
|
||||
def _states(rows_: list[dict[str, Any]]) -> None:
|
||||
"""내 제안 승인 상태(739-1) — 마스터 줄 = 승인 · 아래 층 줄 = 마스터에 같은 짝(서명) 승인 ·
|
||||
M01 반려 기억 반려 · 그 밖 대기(M01 별칭 장부 `sign` · 반려 기억 그대로)."""
|
||||
from M01_MasterData import M01_MasterData_Alias as alias # M01 이 장부를 부름 — 늦게 읽음
|
||||
|
||||
master = read(store.FOLDER)
|
||||
gone = alias.rejected()
|
||||
for row in rows_:
|
||||
if row["층"] == "system":
|
||||
row["상태"] = "승인"
|
||||
continue
|
||||
sig, mine = alias.sign(alias.ALIAS, row), master.get(row["열쇠"])
|
||||
if mine and alias.sign(alias.ALIAS, mine) == sig:
|
||||
row["상태"] = "승인"
|
||||
elif alias._reject_id(alias.ALIAS, row["열쇠"], sig) in gone:
|
||||
row["상태"] = "반려"
|
||||
else:
|
||||
row["상태"] = "대기"
|
||||
|
||||
|
||||
def rows(folder: str | Path, writable: bool) -> list[dict[str, Any]]:
|
||||
"""장부 화면 — 이 층 + 위 층 줄 `{열쇠, …줄, 층, 주인, 고침, 상태}`(고침 = 이 층 · 쓰기 권한 ·
|
||||
상태 = 승인 · 대기 · 반려)."""
|
||||
out = []
|
||||
for at, (layer, owner, book) in enumerate(books(folder)):
|
||||
for full, row in sorted(book.items()):
|
||||
out.append(
|
||||
{"열쇠": full, **row, "층": layer, "주인": owner, "고침": writable and not at}
|
||||
)
|
||||
_states(out)
|
||||
return out
|
||||
|
||||
|
||||
# ── 검사 · 쓰기 ───────────────────────────────────────────────────────
|
||||
def _bad(text: str) -> store.StoreError:
|
||||
return store.StoreError(400, text)
|
||||
@@ -350,9 +318,9 @@ def write(
|
||||
|
||||
|
||||
def put(folder: str | Path, entries: Any, who: Any) -> dict[str, Any]:
|
||||
"""장부 화면 [저장] — `{열쇠: 줄 | None}` 재검사 뒤 이 층에 씀 → `{씀, 줄}`."""
|
||||
"""`{열쇠: 줄 | None}` 재검사 뒤 이 층에 씀 → `{씀}`(장부 화면 · M01 승인은 걷음 · 815-2)."""
|
||||
if not isinstance(entries, dict) or not entries or len(entries) > 2000:
|
||||
raise _bad("저장할 줄이 없음")
|
||||
known = _known()
|
||||
clean = {str(k): None if v is None else check(str(k), v, known) for k, v in entries.items()}
|
||||
return {"씀": write(folder, clean, who), "줄": rows(folder, True)}
|
||||
return {"씀": write(folder, clean, who)}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
엑셀 보기 머리 · 목록 = `View.listing`(줄 표지) · 옮기기 = [내 일위대가로] `hopyo-build-all {번}`(807-1).
|
||||
옛 판정 · 검산 · 옮기기 · 근거 제안 길(785-5 `hopyo-judge` · `check` · `move` · `price` · 793-3a `hopyo-basis`)은
|
||||
809-1 에서 지움(요구 703 — 기초로 바꾸기 없음).
|
||||
그대로 쓰는 길(목록 · 올림 · 엑셀 보기 머리 · 격자 · 지우기 · 장부 · 모두 고정 · 붙여넣기 · 다시 잇기 ·
|
||||
그대로 쓰는 길(목록 · 올림 · 엑셀 보기 머리 · 격자 · 지우기 · 모두 고정 · 붙여넣기 · 다시 잇기 ·
|
||||
구조물 조합 · 칠 = B09 · M01 이 씀) · 옛 맞춤 길(호표 하나 맞춤 · 미리 셈 · 실무 호표 · 확인 만들기 · 안 이은 줄 ·
|
||||
단가산출 만들기)은 785-7 에서 지움.
|
||||
층 권한 = `Router_Items._root` · ⚠ 등록은 `main.py`(로그인만).
|
||||
@@ -25,13 +25,11 @@ from M02_MasterTemplete import M02_Hopyo_Excel as hopyo_excel
|
||||
from M02_MasterTemplete import M02_Hopyo_Forget as hopyo_forget
|
||||
from M02_MasterTemplete import M02_Hopyo_Grid as hopyo_grid
|
||||
from M02_MasterTemplete import M02_Hopyo_Item as hopyo_item
|
||||
from M02_MasterTemplete import M02_Hopyo_Ledger as hopyo_ledger
|
||||
from M02_MasterTemplete import M02_Hopyo_Marks as hopyo_marks
|
||||
from M02_MasterTemplete import M02_Hopyo_View as hopyo_view_
|
||||
from M02_MasterTemplete import M02_Hopyo_Paste as hopyo_paste
|
||||
from M02_MasterTemplete import M02_Hopyo_Relink as hopyo_relink
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||||
from M02_MasterTemplete import M02_Layer_Access as access
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Sangeun_FromHopyo as from_hopyo
|
||||
from M02_MasterTemplete.M02_MasterTemplete_Router_Items import Layer, Session, _call, _root
|
||||
@@ -207,35 +205,6 @@ def hopyo_make_all(
|
||||
return _call(folder, hopyo_build.build_all, *args)
|
||||
|
||||
|
||||
@router.get("/layers/{layer}/hopyo-ledger")
|
||||
def hopyo_ledger_read(
|
||||
layer: Layer, owner: str | None = None, session: Session = Depends(verify_session)
|
||||
) -> dict:
|
||||
"""별칭 장부 — 이 층 + 위 층 줄 `{열쇠, 원래, 갈래, 짝, 누가, 때, 처음파일, 층, 주인, 고침}`
|
||||
(고침 = 이 층 · 쓰기 권한)."""
|
||||
folder = _root(session, layer, owner)
|
||||
writable = access.grant(session, layer, owner).write
|
||||
return {"줄": _call(folder, hopyo_ledger.rows, folder, writable)}
|
||||
|
||||
|
||||
class LedgerBody(_Strict):
|
||||
"""`줄` = {열쇠: 장부 줄 | null(지움)} — 이 층만 · 서버가 짝을 다시 검사."""
|
||||
|
||||
줄: dict[str, Any]
|
||||
|
||||
|
||||
@router.put("/layers/{layer}/hopyo-ledger")
|
||||
def hopyo_ledger_write(
|
||||
layer: Layer,
|
||||
body: LedgerBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""별칭 장부 [저장] — 고친 줄 · 지운 줄(층 쓰기) → `{씀, 줄}`."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
return _call(folder, hopyo_ledger.put, folder, body.줄, _who(session))
|
||||
|
||||
|
||||
@router.delete("/layers/{layer}/hopyo/{key}")
|
||||
def hopyo_delete(
|
||||
layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session)
|
||||
|
||||
@@ -34,14 +34,6 @@
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.m02-hopyo-ledger__table td:nth-child(1) {
|
||||
min-width: 7em;
|
||||
}
|
||||
|
||||
.m02-hopyo-ledger__table td:nth-child(2) {
|
||||
min-width: 6em;
|
||||
}
|
||||
|
||||
/* 우리 로직 칸 — 대상 여럿 = 칸 안 여러 줄(이름 · 키 · 몫 · 배 칸 · [옵션]) */
|
||||
.m02-hopyo__parts {
|
||||
min-width: 11em;
|
||||
@@ -83,15 +75,6 @@
|
||||
margin-left: var(--spacing-4);
|
||||
}
|
||||
|
||||
/* 별칭 장부(338) — 위 층 줄 = 회색 읽기만 · 고친 줄 = 왼쪽 띠 */
|
||||
.m02-hopyo-ledger__row.is-upper {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.m02-hopyo-ledger__row.is-edited > td:first-child {
|
||||
box-shadow: inset 3px 0 0 var(--color-accent);
|
||||
}
|
||||
|
||||
/* 엑셀 보기(329) — 왼쪽 읽기 전용 시트 · 오른쪽 호표 목록 */
|
||||
.m02-hopyo-sheet {
|
||||
display: grid;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* M02_MasterTemplete_Hopyo_Api.ts
|
||||
* 일위대가 실무자료(PLAN 324 · 338) 서버 길 — `/api/m02/layers/{층}/hopyo…` · 층 = 화면 층(`Api_Fetch` 지금 층) ·
|
||||
* 올리기 = FormData(숨은 file 입력) · 그 밖 = 공용 `call`(서버 까닭 글 그대로) ·
|
||||
* 목록 · 올림 · 엑셀 보기 머리(809-1 줄 표지 줄상태) · 격자 · 지우기 · 별칭 장부 · 붙여넣기 · 다시 잇기 —
|
||||
* 목록 · 올림 · 엑셀 보기 머리(809-1 줄 표지 줄상태) · 격자 · 지우기 · 붙여넣기 · 다시 잇기 —
|
||||
* [수정] 보기 = `Hopyo_View`(809-1) · 판정 · 검산 · 옮기기 길(785)은 809-1 에서 지움(요구 703).
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -97,39 +97,6 @@ export interface HopyoBuiltAll {
|
||||
만듦?: { 번: number; 이름: string; 키: string }[];
|
||||
}
|
||||
|
||||
/** 별칭 장부 짝 하나 */
|
||||
export interface LedgerPair {
|
||||
로직?: string;
|
||||
자재?: string;
|
||||
/** 고른 것 꼴(785-3 · 812-9 기계) — 갈래 + 키 */
|
||||
갈래?: string;
|
||||
키?: string;
|
||||
입력값: Record<string, unknown>;
|
||||
배: number;
|
||||
몫: number | null;
|
||||
}
|
||||
|
||||
/** 별칭 장부 한 줄(쓰기 꼴) */
|
||||
export interface LedgerEntry {
|
||||
원래: { 품명: string; 규격: string; 단위: string };
|
||||
갈래: "짝" | "우리에 없음" | "안 씀";
|
||||
짝: LedgerPair[];
|
||||
엑셀조항?: unknown[];
|
||||
}
|
||||
|
||||
/** 별칭 장부 보기 한 줄 — 층 · 고침(이 층 · 쓰기 권한) */
|
||||
export interface LedgerRow extends LedgerEntry {
|
||||
열쇠: string;
|
||||
누가: { id: number; 이름: string | null } | null;
|
||||
때: string | null;
|
||||
처음파일: string | null;
|
||||
층: Layer;
|
||||
주인: string | null;
|
||||
고침: boolean;
|
||||
/** 739-1 M01 승인 상태 — 마스터 줄 = 승인 */
|
||||
상태?: "승인" | "대기" | "반려";
|
||||
}
|
||||
|
||||
const enc = encodeURIComponent;
|
||||
/** 지금 화면 층(구조물 조합 층) */
|
||||
export const pageScope = (): HopyoScope => ({ layer: getActiveLayer(), owner: getActiveOwner() });
|
||||
@@ -149,21 +116,6 @@ export const readHopyo = (scope: HopyoScope, key: string): Promise<HopyoView> =>
|
||||
export const readHopyoGrid = (scope: HopyoScope, key: string): Promise<HopyoGrid> =>
|
||||
call(at(scope, `/hopyo-grid/${enc(key)}`));
|
||||
|
||||
export const readLedger = async (scope: HopyoScope): Promise<LedgerRow[]> =>
|
||||
(await call<{ 줄: LedgerRow[] }>(at(scope, "/hopyo-ledger"))).줄;
|
||||
|
||||
/** 별칭 장부 [저장] — 고친 줄 · null(지움) · 이 층만 */
|
||||
export const saveLedger = async (
|
||||
scope: HopyoScope,
|
||||
줄: Record<string, LedgerEntry | null>,
|
||||
): Promise<LedgerRow[]> =>
|
||||
(
|
||||
await call<{ 줄: LedgerRow[] }>(at(scope, "/hopyo-ledger"), {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ 줄 }),
|
||||
})
|
||||
).줄;
|
||||
|
||||
/** [끝내기] · 화면 떠남(743-1) — `keepalive` = 페이지를 닫아도 요청은 감 */
|
||||
export const deleteHopyo = (scope: HopyoScope, key: string, keepalive = false): Promise<unknown> =>
|
||||
call(at(scope, `/hopyo/${enc(key)}`), { method: "DELETE", keepalive });
|
||||
|
||||
@@ -1,407 +0,0 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Hopyo_Ledger.ts
|
||||
* 일위대가 실무자료 별칭 장부 화면(PLAN 338-3 · 설계 4-4) — 본 칸 · 좌측 판 [별칭 장부] 로 엶.
|
||||
* · 표 = 엑셀 품명 · 규격 · 단위 | 갈래 | 우리 로직(여럿) · 옵션 · 단위 환산 · 몫 | 누가 · 때 · 처음 파일 | 층(위 층 회색 읽기만)
|
||||
* · 뜻(359-4 · 머리 한 줄 · 좌측 단추 title) = 엑셀 품명 ↔ 우리 로직 · 자재 짝을 기억해 두었다가 같은 품명이 나오면 먼저 제안
|
||||
* · 할 일 = 찾기 · 갈래 거름 · 갈래 바꿈 · [로직 바꿈] · [항목 추가] · 단위 환산 · 몫 · [옵션] · [삭제] ·
|
||||
* 위 층 줄 [내 층 복사] · 갈래 글 = 확인 창 드롭다운과 같음(짝 · 로직없음 · 사용안함 · 저장 값 그대로)
|
||||
* · 고친 것 = sessionStorage `m02-hopyo-ledger:{층}:{주인}` → [저장] 때만 `PUT`(서버가 짝을 다시 검사)
|
||||
* · 안 둠 = 발주처 칸 · 자동 올림 · 자동 합침 · 쓰인 횟수 · 회사로 올리기.
|
||||
* · 739-1 중앙 관리 — 회사 · 개인 층 = 「내 제안 목록」(보기 · 고치기 · [거두기] · 승인 상태 대기 · 승인 · 반려) ·
|
||||
* 자동 짝 = 마스터(M01 별칭 장부 승인)만 · 마스터 층(M01 화면) = 별칭 장부 그대로
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge, type BadgeTone } from "@ui/ui_template_badge";
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import { loadingError, showLoading } from "@ui/ui_template_loading";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import {
|
||||
readLedger,
|
||||
saveLedger,
|
||||
type HopyoScope,
|
||||
type LedgerEntry,
|
||||
type LedgerPair,
|
||||
type LedgerRow,
|
||||
} from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import { fetchLogicInputs } from "./M02_MasterTemplete_Labor_Api";
|
||||
import { dateTimeText } from "../common_util/common_util_time";
|
||||
import { openLaborInput } from "./M02_MasterTemplete_Labor_Input";
|
||||
import { openFindPicker } from "./M02_MasterTemplete_Find_Pick";
|
||||
import { formatNumber } from "../common_util/common_util_money";
|
||||
import "./M02_MasterTemplete_Hopyo.css";
|
||||
|
||||
export const LEDGER_TITLE = "별칭 장부";
|
||||
/** 별칭 장부 뜻 한 줄(359-4) — 좌측 단추 title · 화면 머리 */
|
||||
export const LEDGER_HINT =
|
||||
"엑셀 품명 ↔ 우리 로직 · 자재 짝을 기억해 두었다가 같은 품명이 나오면 먼저 제안";
|
||||
const KINDS = ["짝", "우리에 없음", "안 씀"] as const;
|
||||
/** 갈래 화면 글 — 확인 창 드롭다운과 같음(저장 값 그대로) */
|
||||
export const KIND_TEXT: Record<string, string> = {
|
||||
짝: "짝",
|
||||
"우리에 없음": "로직없음",
|
||||
"안 씀": "사용안함",
|
||||
};
|
||||
const LAYER_TEXT: Record<string, string> = { system: "마스터", company: "회사", personal: "개인" };
|
||||
/** 739-1 회사 · 개인 층 장부 화면 제목 */
|
||||
export const MINE_TITLE = "내 제안 목록";
|
||||
const STATE_TONE: Record<string, BadgeTone> = { 승인: "success", 대기: "warning", 반려: "danger" };
|
||||
type Edits = Record<string, LedgerEntry | null>;
|
||||
/** 화면 한 줄 — 서버 줄 위에 고친 것 */
|
||||
export type LedgerView = LedgerRow & { 고친?: boolean };
|
||||
|
||||
export const ledgerKey = (scope: HopyoScope): string =>
|
||||
`m02-hopyo-ledger:${scope.layer}:${scope.owner ?? ""}`;
|
||||
function loadEdits(k: string): Edits {
|
||||
try {
|
||||
return JSON.parse(sessionStorage.getItem(k) ?? "{}") as Edits;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
function saveEdits(k: string, edits: Edits): void {
|
||||
try {
|
||||
if (Object.keys(edits).length) sessionStorage.setItem(k, JSON.stringify(edits));
|
||||
else sessionStorage.removeItem(k);
|
||||
} catch {
|
||||
/* 캐시 못 씀 = 이 화면에만 남음 */
|
||||
}
|
||||
}
|
||||
|
||||
/** 쓰기 꼴 — 보기 칸(층 · 누가 · 때 …) 뺌 */
|
||||
export const entryOf = (row: LedgerEntry): LedgerEntry => ({
|
||||
원래: row.원래,
|
||||
갈래: row.갈래,
|
||||
짝:
|
||||
row.갈래 === "짝"
|
||||
? row.짝.map((p) => ({ ...p, 입력값: p.입력값 ?? {}, 몫: p.몫 ?? null }))
|
||||
: [],
|
||||
엑셀조항: row.엑셀조항 ?? [],
|
||||
});
|
||||
|
||||
/** 서버 줄 + 고친 것 → 화면 줄 — 이 층 줄은 고친 값 · 지운 것 뺌 · 위 층에서 옮긴 줄은 이 층 줄로 더함 */
|
||||
export function mergeRows(rows: LedgerRow[], edits: Edits, scope: HopyoScope): LedgerView[] {
|
||||
const mine = rows.filter((r) => r.고침);
|
||||
const out: LedgerView[] = [];
|
||||
for (const r of mine) {
|
||||
if (!(r.열쇠 in edits)) out.push(r);
|
||||
else if (edits[r.열쇠]) out.push({ ...r, ...edits[r.열쇠]!, 고친: true });
|
||||
}
|
||||
for (const [key, entry] of Object.entries(edits)) {
|
||||
if (!entry || mine.some((r) => r.열쇠 === key)) continue;
|
||||
out.push({
|
||||
...entry,
|
||||
열쇠: key,
|
||||
누가: null,
|
||||
때: null,
|
||||
처음파일: null,
|
||||
층: scope.layer,
|
||||
주인: scope.owner,
|
||||
고침: true,
|
||||
고친: true,
|
||||
});
|
||||
}
|
||||
return [...out, ...rows.filter((r) => !r.고침)];
|
||||
}
|
||||
|
||||
/** 짝 글 — 「GC000198 × 1 · 몫 0.1 · 소량감률=50」 */
|
||||
export const pairText = (p: LedgerPair): string =>
|
||||
[
|
||||
`${p.로직 ?? p.자재 ?? [p.갈래, p.키].filter(Boolean).join(" ")} × ${p.배}`,
|
||||
p.몫 != null ? `몫 ${p.몫}` : "",
|
||||
Object.entries(p.입력값 ?? {})
|
||||
.map(([k, v]) => `${k}=${formatNumber(v)}`)
|
||||
.join(", "),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
|
||||
/** 찾기 · 갈래 거름 */
|
||||
export const filterRows = (rows: LedgerView[], q: string, kind: string): LedgerView[] => {
|
||||
const want = q.trim().toLowerCase();
|
||||
return rows.filter(
|
||||
(r) =>
|
||||
(!kind || r.갈래 === kind) &&
|
||||
(!want ||
|
||||
`${r.원래.품명} ${r.원래.규격} ${r.짝.map(pairText).join(" ")}`
|
||||
.toLowerCase()
|
||||
.includes(want)),
|
||||
);
|
||||
};
|
||||
|
||||
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
||||
const td = (text: string, cls = ""): HTMLElement =>
|
||||
el("td", { text, className: cls, attrs: { title: text } });
|
||||
const note = (text: string): HTMLElement =>
|
||||
el("span", { className: "ui-text ui-text--muted", text, attrs: { "data-text": "14" } });
|
||||
|
||||
export interface LedgerOptions {
|
||||
scope: HopyoScope;
|
||||
/** 이 층 쓰기 권한 — 없으면 보기만 */
|
||||
canWrite: boolean;
|
||||
}
|
||||
|
||||
export function buildHopyoLedger(opt: LedgerOptions): HTMLElement {
|
||||
const root = el("div", {
|
||||
className: "m02-hopyo m02-hopyo-ledger",
|
||||
attrs: { "data-hopyo-ledger": "" },
|
||||
});
|
||||
const k = ledgerKey(opt.scope);
|
||||
const master = opt.scope.layer === "system";
|
||||
let edits = loadEdits(k);
|
||||
let rows: LedgerRow[] = [];
|
||||
const search = el("input", {
|
||||
className: "ui-input ui-input--sm m02-hopyo__search",
|
||||
attrs: { type: "search", placeholder: "품명 · 규격 · 로직 찾기" },
|
||||
}) as HTMLInputElement;
|
||||
const kind = el("select", {
|
||||
className: "ui-input ui-input--sm",
|
||||
children: [
|
||||
el("option", { text: "갈래 전체", attrs: { value: "" } }),
|
||||
...KINDS.map((x) => el("option", { text: KIND_TEXT[x], attrs: { value: x } })),
|
||||
],
|
||||
}) as HTMLSelectElement;
|
||||
const save = createButton({ label: "저장", variant: "filled", size: "sm" });
|
||||
const drop = createButton({ label: "변경 취소", variant: "ghost", size: "sm" });
|
||||
const body = el("tbody");
|
||||
const keep = (): void => {
|
||||
saveEdits(k, edits);
|
||||
paint();
|
||||
};
|
||||
const edit = (r: LedgerView, next: Partial<LedgerEntry>): void => {
|
||||
edits[r.열쇠] = entryOf({ ...entryOf(r), ...next });
|
||||
keep();
|
||||
};
|
||||
|
||||
function pairCell(r: LedgerView, mine: boolean): HTMLElement {
|
||||
const pairs: HTMLElement[] = r.짝.map((p, at) => {
|
||||
const parts: HTMLElement[] = [el("span", { text: pairText(p) })];
|
||||
if (mine) {
|
||||
const times = el("input", {
|
||||
className: "ui-input ui-input--sm m02-hopyo__times",
|
||||
attrs: { type: "number", step: "any", min: "0", value: String(p.배) },
|
||||
}) as HTMLInputElement;
|
||||
times.addEventListener("change", () =>
|
||||
edit(r, { 짝: r.짝.map((x, i) => (i === at ? { ...x, 배: Number(times.value) } : x)) }),
|
||||
);
|
||||
parts.push(unitField(times, { from: r.원래.단위, to: "" }));
|
||||
if (p.로직)
|
||||
parts.push(
|
||||
createButton({
|
||||
label: "옵션",
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () =>
|
||||
void fetchLogicInputs([p.로직!]).then(
|
||||
(got) =>
|
||||
openLaborInput({
|
||||
title: p.로직!,
|
||||
logic: got.로직[0] ?? null,
|
||||
values: p.입력값 as never,
|
||||
memo: null,
|
||||
readOnly: false,
|
||||
done: (res) =>
|
||||
edit(r, {
|
||||
짝: r.짝.map((x, i) => (i === at ? { ...x, 입력값: res.입력값 } : x)),
|
||||
}),
|
||||
}),
|
||||
(error) => showToast(why(error), "error"),
|
||||
),
|
||||
}),
|
||||
createButton({
|
||||
label: "×",
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => edit(r, { 짝: r.짝.filter((_, i) => i !== at) }),
|
||||
}),
|
||||
);
|
||||
}
|
||||
return el("div", { className: "m02-hopyo__part", children: parts });
|
||||
});
|
||||
if (mine && r.갈래 === "짝")
|
||||
pairs.push(
|
||||
createButton({
|
||||
label: "항목 추가",
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () =>
|
||||
// 736-1 별칭 장부가 가리킬 수 있는 것 = 기초 로직 · 재료 둘(탭 막음)
|
||||
openFindPicker({
|
||||
layer: opt.scope.layer,
|
||||
owner: opt.scope.owner,
|
||||
kinds: ["logic", "material"],
|
||||
logic: (x) => edit(r, { 짝: [...r.짝, { 로직: x.키, 입력값: {}, 배: 1, 몫: null }] }),
|
||||
material: (e) =>
|
||||
edit(r, { 짝: [...r.짝, { 자재: e.코드, 입력값: {}, 배: 1, 몫: null }] }),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
return el("td", { className: "m02-hopyo__parts", children: pairs });
|
||||
}
|
||||
|
||||
function rowOf(r: LedgerView): HTMLElement {
|
||||
const mine = opt.canWrite && r.고침;
|
||||
const kindCell = mine
|
||||
? (() => {
|
||||
const s = el("select", {
|
||||
className: "ui-input ui-input--sm",
|
||||
children: KINDS.map((x) => el("option", { text: KIND_TEXT[x], attrs: { value: x } })),
|
||||
}) as HTMLSelectElement;
|
||||
s.value = r.갈래;
|
||||
s.addEventListener("change", () =>
|
||||
edit(r, { 갈래: s.value as LedgerEntry["갈래"], 짝: s.value === "짝" ? r.짝 : [] }),
|
||||
);
|
||||
return el("td", { children: [s] });
|
||||
})()
|
||||
: td(KIND_TEXT[r.갈래] ?? r.갈래);
|
||||
const acts: HTMLElement[] = [];
|
||||
if (mine)
|
||||
acts.push(
|
||||
createButton({
|
||||
label: master ? "삭제" : "거두기",
|
||||
variant: "danger",
|
||||
size: "sm",
|
||||
onClick: () => {
|
||||
edits[r.열쇠] = null;
|
||||
keep();
|
||||
},
|
||||
}),
|
||||
);
|
||||
else if (opt.canWrite && !rows.some((x) => x.고침 && x.열쇠 === r.열쇠) && !edits[r.열쇠])
|
||||
acts.push(
|
||||
createButton({
|
||||
label: "내 층 복사",
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => {
|
||||
edits[r.열쇠] = entryOf(r);
|
||||
keep();
|
||||
},
|
||||
}),
|
||||
);
|
||||
const when = [r.누가?.이름, dateTimeText(r.때, ""), r.처음파일].filter(Boolean).join(" · ");
|
||||
return el("tr", {
|
||||
className: `m02-hopyo-ledger__row${r.고침 ? "" : " is-upper"}${r.고친 ? " is-edited" : ""}`,
|
||||
attrs: { "data-ledger": r.열쇠 },
|
||||
children: [
|
||||
td(r.원래.품명),
|
||||
td(r.원래.규격),
|
||||
td(r.원래.단위),
|
||||
kindCell,
|
||||
pairCell(r, mine),
|
||||
td(when),
|
||||
td(r.고침 ? "내 층" : (LAYER_TEXT[r.층] ?? r.층)),
|
||||
el("td", {
|
||||
attrs: { "data-state": r.상태 ?? "" },
|
||||
children: r.고친 || !r.상태 ? [] : [createBadge(r.상태, STATE_TONE[r.상태])],
|
||||
}),
|
||||
el("td", { className: "m02-hopyo__acts", children: acts }),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function paint(): void {
|
||||
const all = mergeRows(rows, edits, opt.scope);
|
||||
const shown = filterRows(all, search.value, kind.value);
|
||||
const n = Object.keys(edits).length;
|
||||
save.querySelector(".ui-btn__label")!.textContent = n ? `저장 (${n})` : "저장";
|
||||
save.disabled = drop.disabled = !n;
|
||||
body.replaceChildren(
|
||||
...(shown.length
|
||||
? shown.map(rowOf)
|
||||
: [el("tr", { children: [el("td", { text: "장부 줄 없음", attrs: { colspan: "9" } })] })]),
|
||||
);
|
||||
}
|
||||
search.addEventListener("input", paint);
|
||||
kind.addEventListener("change", paint);
|
||||
drop.addEventListener("click", () => {
|
||||
edits = {};
|
||||
keep();
|
||||
});
|
||||
save.addEventListener("click", async () => {
|
||||
save.disabled = true;
|
||||
try {
|
||||
rows = await saveLedger(opt.scope, edits);
|
||||
edits = {};
|
||||
showToast("장부 저장", "success");
|
||||
keep();
|
||||
} catch (error) {
|
||||
showToast(`저장 못 함 — ${why(error)}`, "error");
|
||||
paint();
|
||||
}
|
||||
});
|
||||
|
||||
async function load(): Promise<void> {
|
||||
const stop = showLoading(root);
|
||||
try {
|
||||
rows = await readLedger(opt.scope);
|
||||
} catch (error) {
|
||||
root.replaceChildren(loadingError(why(error), () => void load()));
|
||||
return;
|
||||
} finally {
|
||||
stop();
|
||||
}
|
||||
root.replaceChildren(
|
||||
el("div", {
|
||||
className: "ui-toolbar m02-hopyo__bar",
|
||||
children: [
|
||||
el("strong", { text: master ? LEDGER_TITLE : MINE_TITLE }),
|
||||
note(
|
||||
master
|
||||
? `${LEDGER_HINT} · 여기 짝 = 모든 사용자 불러오기에 자동`
|
||||
: `내 층 짝 = 내 제안(다음 불러오기에서 후보 맨 위 · 자동 아님) · M01 별칭 장부 승인 뒤 자동 · ${L("M02_PrecomboTitle")} 생성 때 남음`,
|
||||
),
|
||||
search,
|
||||
kind,
|
||||
...(opt.canWrite ? [save, drop] : []),
|
||||
],
|
||||
}),
|
||||
el("div", {
|
||||
className: "ui-general-block__table-wrap ui-general-block__table-wrap--sticky-head",
|
||||
children: [
|
||||
el("table", {
|
||||
className: "ui-general-block__table m02-hopyo__table m02-hopyo-ledger__table",
|
||||
children: [
|
||||
el("thead", {
|
||||
children: [
|
||||
el("tr", {
|
||||
children: [
|
||||
"엑셀 품명",
|
||||
"규격",
|
||||
"단위",
|
||||
"갈래",
|
||||
"우리 로직 · 단위 환산 · 몫 · 옵션",
|
||||
"누가 · 때 · 처음 파일",
|
||||
"층",
|
||||
"승인",
|
||||
"",
|
||||
].map((t) => el("th", { text: t })),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
body,
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
paint();
|
||||
}
|
||||
|
||||
void load();
|
||||
return root;
|
||||
}
|
||||
|
||||
/** 단위 환산 칸 — 「엑셀 1 {from} =」 입력 「{to}」(옛 확인 창 `CheckRows.unitField` 옮김 · 785-7) */
|
||||
function unitField(input: HTMLInputElement, v: { from: string; to: string }): HTMLElement {
|
||||
input.title = L("M02_HopyoUnitTip");
|
||||
const word = (text: string): HTMLElement =>
|
||||
el("span", { className: "m02-hopyo__unit-text", text });
|
||||
return el("span", {
|
||||
className: "m02-hopyo__unit",
|
||||
children: [
|
||||
word(L("M02_HopyoUnitLead").replace("{from}", v.from || "?")),
|
||||
input,
|
||||
...(v.to ? [word(v.to)] : []),
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
* · 결과 층 고름 = 작은 고름(쓸 수 있는 층 둘 이상일 때만 · 일반 = 개인 하나라 안 보임)
|
||||
* · 「작업 중」 줄 = 층마다 서버 목록(같은 엑셀 작업 중 이어서) · 누르면 본 칸 불러오기 보기
|
||||
* · 다른 엑셀 [불러오기] · 다른 화면으로 = 지금 보기 떠남(올린 파일 지움 · 743-1) — 고르고 안 만든 확인 캐시가 있으면 물음 한 번
|
||||
* 아래 [별칭 장부](338-3) → 본 칸 장부 화면(`M02_MasterTemplete_Hopyo_Ledger` · 층 = 구조물 조합 층).
|
||||
* 옛 [별칭 장부](338-3)는 걷음(815-2) — 별칭은 M01 요소 「별칭」 칸 · M01 별칭 모아 보기.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements";
|
||||
@@ -16,12 +16,10 @@ import { createSegment } from "@ui/ui_template_segment";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import {
|
||||
listHopyo,
|
||||
pageScope,
|
||||
uploadHopyo,
|
||||
type HopyoFile,
|
||||
type HopyoScope,
|
||||
} from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import { buildHopyoLedger, LEDGER_HINT, LEDGER_TITLE } from "./M02_MasterTemplete_Hopyo_Ledger";
|
||||
import {
|
||||
buildHopyoImport,
|
||||
LAYERS,
|
||||
@@ -32,8 +30,6 @@ import {
|
||||
import type { ManageLayer } from "./M02_MasterTemplete_Layer";
|
||||
|
||||
export interface HopyoSectionOptions {
|
||||
/** 이 층 쓰기 권한 — 별칭 장부 고침(구조물 조합 층이 제자리에서 바뀌어 열 때 읽음 · 720-4) */
|
||||
canWrite: () => boolean;
|
||||
/** 저장 안 한 고침을 버릴지 확인 — 취소면 false */
|
||||
confirmLeave: () => Promise<boolean>;
|
||||
/** 메인 칸에 화면을 띄움 */
|
||||
@@ -72,20 +68,9 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection {
|
||||
const seg = el("div", { className: "m02-side__row" });
|
||||
seg.hidden = true;
|
||||
const jobs = el("div", { className: "m02-hopyo-side", attrs: { "data-hopyo-jobs": "" } });
|
||||
const ledger = el("button", {
|
||||
className: "m02-side__item ui-sidebar-row",
|
||||
text: LEDGER_TITLE,
|
||||
attrs: { type: "button", title: LEDGER_HINT, "data-hopyo-ledger-open": "" },
|
||||
});
|
||||
ledger.addEventListener("click", () => void showLedger());
|
||||
const body = el("div", {
|
||||
className: "m02-hopyo-side",
|
||||
children: [
|
||||
el("div", { className: "m02-side__row", children: [start, input] }),
|
||||
seg,
|
||||
jobs,
|
||||
ledger,
|
||||
],
|
||||
children: [el("div", { className: "m02-side__row", children: [start, input] }), seg, jobs],
|
||||
});
|
||||
let open: { key: string; view: HTMLElement; imp?: HopyoImport } | null = null;
|
||||
|
||||
@@ -177,7 +162,6 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection {
|
||||
file,
|
||||
onEnded: () => {
|
||||
open = null;
|
||||
ledger.classList.remove("is-active");
|
||||
opt.show(title, el("div", { className: "m02-hopyo" })); // 끝냄 — 빈 본 칸
|
||||
void refresh();
|
||||
},
|
||||
@@ -185,19 +169,6 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection {
|
||||
opt.show(`${title} — ${file.파일}`, imp.root);
|
||||
opt.onOpen();
|
||||
open = { key: file.키, view: imp.root, imp };
|
||||
ledger.classList.remove("is-active");
|
||||
void refresh();
|
||||
}
|
||||
|
||||
async function showLedger(): Promise<void> {
|
||||
if (open?.key === LEDGER_TITLE && open.view.isConnected) return;
|
||||
if (!(await leaveOk())) return;
|
||||
open?.imp?.leave();
|
||||
const view = buildHopyoLedger({ scope: pageScope(), canWrite: opt.canWrite() });
|
||||
opt.show(`${title} — ${LEDGER_TITLE}`, view);
|
||||
opt.onOpen();
|
||||
open = { key: LEDGER_TITLE, view };
|
||||
ledger.classList.add("is-active");
|
||||
void refresh();
|
||||
}
|
||||
|
||||
@@ -205,7 +176,6 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection {
|
||||
if (open && !open.view.isConnected) {
|
||||
open.imp?.leave(); // 다른 화면으로 = 불러오기 떠남
|
||||
open = null;
|
||||
ledger.classList.remove("is-active");
|
||||
void refresh();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -56,7 +56,7 @@ const STUB_DRAWING = {
|
||||
export interface SideOptions {
|
||||
/** 시스템 관리자 — 도면 양식(시스템 층에만 있음)을 늘 보임(720-4) */
|
||||
isSystem: boolean;
|
||||
/** 지금 구조물 조합 층을 고칠 수 있나(층이 제자리에서 바뀜) — 일위대가 불러오기 장부 */
|
||||
/** 지금 구조물 조합 층을 고칠 수 있나(층이 제자리에서 바뀜) */
|
||||
canWrite: () => boolean;
|
||||
onOpen: (sel: Selection | null) => void;
|
||||
getOpen: () => Selection | null;
|
||||
@@ -164,7 +164,6 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
const drawing = section(L("M02_KindDrawing"), bodies.get("drawing")!, ...actionRow("drawing"));
|
||||
// 일위대가 불러오기(324-4 · 제목은 718-1) · 층 = 구조물 조합 층 · 쓰기 권한 없으면 올리기 꺼짐
|
||||
const hopyo = buildHopyoSection({
|
||||
canWrite: opt.canWrite,
|
||||
confirmLeave: opt.confirmLeave,
|
||||
show: opt.showView,
|
||||
onOpen: paint,
|
||||
|
||||
@@ -206,25 +206,9 @@ def test_못_잇는_짝(geo):
|
||||
assert lk.link(user, rows, [], "take")[1]["이음"] == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"role, layer", [("USER", "personal"), ("ADMIN", "company")], ids=["일반", "회사관리자"]
|
||||
)
|
||||
def test_기억_층(tmp_path, monkeypatch, role, layer):
|
||||
monkeypatch.setattr(route.access, "folder", lambda got, create=False: tmp_path / got.layer)
|
||||
monkeypatch.setattr(route.m02_store, "FOLDER", tmp_path / "system")
|
||||
master = {"a||": {"확정": "earth:9"}, "m||": {"확정": "earth:1"}} # 시스템 승인(718-2)
|
||||
hopyo_store.remember(tmp_path / "system", master, lk.MEMORY)
|
||||
session = {"company_id": 7, "user_id": 3, "role": role}
|
||||
assert route.keep(session, {"a||": {"확정": "earth:4", "이름": "절토(토사)"}}) == layer
|
||||
assert route.keep(session, {}) is None
|
||||
hopyo_store.remember(
|
||||
tmp_path / "company", {"a||": {"확정": "earth:5"}, "b||": {"확정": "x"}}, lk.MEMORY
|
||||
)
|
||||
got = route.recalled(session)
|
||||
assert got["a||"]["확정"] == ("earth:4" if layer == "personal" else "earth:5")
|
||||
assert got["b||"]["확정"] == "x"
|
||||
assert got["m||"]["확정"] == "earth:1" # 회사 · 개인 줄이 마스터보다 앞섬(a = 아래 층)
|
||||
assert route.recalled({"user_id": 3, "role": role}) == master # 회사 없음 = 마스터만
|
||||
def test_기억_길_걷음():
|
||||
"""공사 내역 맞춤 기억(층 `내역맞춤.json` 읽기 · 쓰기 · 잊기)은 걷음(815-2 · 요구 765-2)."""
|
||||
assert not any(hasattr(route, x) for x in ("recalled", "keep", "forget", "ForgetBody"))
|
||||
|
||||
|
||||
def _same(a, b):
|
||||
@@ -275,38 +259,6 @@ def test_줄마다_갈래_판(geo):
|
||||
assert not inner & left # 구조물 묶음에 딸린 줄 = 남은 줄 아님
|
||||
|
||||
|
||||
def test_이_짝_잊기(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(route.access, "folder", lambda got, create=False: tmp_path / got.layer)
|
||||
user = {"company_id": 7, "user_id": 3, "role": "USER"}
|
||||
gone = {lk.FORGOT: True}
|
||||
route.keep(user, {"a||": {"확정": "earth:4"}, "b||": {"확정": "earth:5"}})
|
||||
hopyo_store.remember(tmp_path / "company", {"c||": {"확정": "x"}}, lk.MEMORY)
|
||||
assert route.keep(user, {"a||": gone}) == "personal"
|
||||
assert route.recalled(user)["a||"] == gone and route.recalled(user)["b||"]["확정"] == "earth:5"
|
||||
route.keep(user, {"c||": gone}) # 일반 = 회사 기억을 개인 층 표시로 덮음
|
||||
assert route.recalled(user)["c||"] == gone
|
||||
|
||||
|
||||
def test_잊은_짝_느슨한_기억에도_안_나옴(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(route.access, "folder", lambda got, create=False: tmp_path / got.layer)
|
||||
user = {"company_id": 7, "user_id": 3, "role": "USER"}
|
||||
row = {"name": "토사절취", "spec": "A", "unit": "m3"}
|
||||
other = {"name": "토사절취", "spec": "B", "unit": "m3"}
|
||||
live = {"확정": "earth:4", "이름": "절토(토사)"}
|
||||
route.keep(user, {lk.key_of(other): live})
|
||||
assert (
|
||||
lk.recall(route.recalled(user), row) == live
|
||||
) # 규격 달라도 품명 · 단위 같음 = 느슨한 기억
|
||||
route.keep(user, {lk.key_of(row): {lk.FORGOT: True}})
|
||||
memory = route.recalled(user)
|
||||
assert lk.recall(memory, row) is None # 잊음 = 느슨한 기억도 끊김
|
||||
assert lk.recall(memory, other) == live # 다른 줄은 그대로
|
||||
third = {"name": "토사절취", "spec": "C", "unit": "m3"}
|
||||
assert lk.recall(memory, third) is None # 잊은 짝 곁에서는 느슨하게 안 좁힘
|
||||
route.keep(user, {lk.key_of(row): live}) # 다시 이음 = 풀림
|
||||
assert lk.recall(route.recalled(user), row) == live
|
||||
|
||||
|
||||
def test_잊은_짝_짝_표_제안에도_안_나옴(geo):
|
||||
_user, rows = _load(geo)
|
||||
now = {s["key"]: s for s in lk.suggest(rows, {})}
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
"""별칭 장부 시스템 모으기(PLAN 718-2 · `M01_MasterData_Alias`) — 회사 · 개인 층 맞춤 +
|
||||
내역맞춤 모음 · 같은 짝 묶기 · 회사 수 · 사람 수(익명) · 충돌 · 승인 → 마스터 줄 ·
|
||||
반려 기억 · 다른 회사 새 사용자가 마스터 줄을 씀
|
||||
· 시스템 관리자만 · master_data 원본 그대로. 층 · storage 는 임시 폴더.
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from B09_Estimation import B09_Estimation_Bill_Link as lk
|
||||
from B09_Estimation import B09_Estimation_Router_Link as route
|
||||
from common_util.common_util_auth import require_system_admin, verify_session
|
||||
from M01_MasterData import M01_MasterData_Alias as alias
|
||||
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hs
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
|
||||
# 올리기 길 시험의 `client` 마당(임시 storage · 마스터 층)을 같이 씀
|
||||
# ruff: noqa: F811
|
||||
from resources.tester.test_m02_hopyo_excel import client, master_hash # noqa: F401
|
||||
|
||||
DIG = {"이름": "구조물터파기(인력100%)", "규격": "토사", "단위": "㎥"}
|
||||
DIG_KEY = "구조물터파기(인력100%)|토사|m3"
|
||||
FORM_KEY = "합판거푸집|4회|m2"
|
||||
CUT_KEY = "토사절취||m3"
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _pair(code):
|
||||
return ledger.check(DIG_KEY, {"갈래": "짝", "짝": [{"로직": code, "배": 1}]})
|
||||
|
||||
|
||||
def _seed():
|
||||
"""회사 7(개인 42 · 43 = 같은 짝) · 회사 8(개인 50 = 다른 짝 · 회사 층 = 안 씀) · 내역맞춤."""
|
||||
for user, who in ((42, "갑"), (43, "을")):
|
||||
mine = layers.personal_dir(7, user)
|
||||
ledger.write(mine, {DIG_KEY: _pair("GF000101")}, {"id": user, "이름": who}, "가공사.xls")
|
||||
ledger.write(layers.personal_dir(8, 50), {DIG_KEY: _pair("GF000103")}, {"id": 50}, "나.xls")
|
||||
skip = ledger.check(FORM_KEY, {"갈래": "안 씀"})
|
||||
ledger.write(layers.company_dir(8), {FORM_KEY: skip}, {"id": 51, "이름": "병"}, "다.xls")
|
||||
live = {"확정": "earth:4", "이름": "절토(토사)"}
|
||||
hs.remember(layers.personal_dir(7, 42), {CUT_KEY: live, "a||": {lk.FORGOT: True}}, lk.MEMORY)
|
||||
hs.remember(layers.company_dir(8), {CUT_KEY: live}, lk.MEMORY)
|
||||
|
||||
|
||||
def _one(got, kind, key, code=None):
|
||||
rows = [x for x in got["후보"] if (x["종류"], x["열쇠"]) == (kind, key)]
|
||||
if code:
|
||||
rows = [x for x in rows if x["짝"][0]["로직"] == code]
|
||||
assert len(rows) == 1, rows
|
||||
return rows[0]
|
||||
|
||||
|
||||
def test_모음_묶기_익명_충돌(client):
|
||||
before = master_hash()
|
||||
_seed()
|
||||
got = alias.gather()
|
||||
assert got["모은곳"] == {"회사": 2, "사람": 5} and got["반려"] == 0
|
||||
assert len(got["후보"]) == 4 # 터파기 둘(충돌) · 거푸집 안 씀 · 토사절취 내역맞춤(같은 짝 하나)
|
||||
same = _one(got, alias.ALIAS, DIG_KEY, "GF000101")
|
||||
assert (same["회사"], same["사람"], same["충돌"], same["마스터"]) == (1, 2, True, None)
|
||||
assert _one(got, alias.ALIAS, DIG_KEY, "GF000103")["사람"] == 1
|
||||
form = _one(got, alias.ALIAS, FORM_KEY)
|
||||
assert (form["갈래"], form["충돌"], form["사람"]) == ("안 씀", False, 1)
|
||||
cut = _one(got, alias.BILL, CUT_KEY)
|
||||
assert (cut["회사"], cut["사람"], cut["확정"], cut["충돌"]) == (2, 2, "earth:4", False)
|
||||
text = json.dumps(got, ensure_ascii=False)
|
||||
for secret in ("갑", "을", "병", "가공사.xls", "누가", "처음파일", "주인"):
|
||||
assert secret not in text # 익명 — 회사 수 · 사람 수만
|
||||
assert master_hash() == before
|
||||
|
||||
|
||||
def test_승인_마스터_줄_반려_기억(client, tmp_path, monkeypatch):
|
||||
before = master_hash()
|
||||
_seed()
|
||||
newbie = layers.personal_dir(9, 99) # 처음 쓰는 다른 회사 사용자
|
||||
assert ledger.recall(DIG, newbie) is None
|
||||
sig = _one(alias.gather(), alias.ALIAS, DIG_KEY, "GF000101")["서명"]
|
||||
got = alias.approve(alias.ALIAS, DIG_KEY, sig, {"id": 1, "이름": "관리자"})
|
||||
hit = ledger.recall(DIG, newbie)
|
||||
assert (hit["층"], hit["짝"][0]["로직"]) == ("system", "GF000101") # 승인 → 모두가 씀
|
||||
master = ledger.read(client.system)
|
||||
assert master[DIG_KEY]["처음파일"] is None # 마스터 줄에도 남의 파일 이름 없음
|
||||
other = _one(got, alias.ALIAS, DIG_KEY) # 승인한 짝 = 마스터와 같음 → 후보에서 빠짐
|
||||
assert other["짝"][0]["로직"] == "GF000103"
|
||||
assert (other["마스터"], other["충돌"]) == ("다름", True)
|
||||
got = alias.reject(alias.ALIAS, DIG_KEY, other["서명"], {"id": 1})
|
||||
assert got["반려"] == 1 and not [x for x in got["후보"] if x["열쇠"] == DIG_KEY]
|
||||
assert alias.gather()["반려"] == 1 # 반려 기억 = 다시 모아도 안 보임
|
||||
with pytest.raises(alias.store.StoreError) as error:
|
||||
alias.approve(alias.ALIAS, DIG_KEY, other["서명"], {"id": 1})
|
||||
assert error.value.status == 404
|
||||
with pytest.raises(alias.store.StoreError):
|
||||
alias.approve("모름", DIG_KEY, sig, {"id": 1})
|
||||
# 내역맞춤 승인 → 마스터 기억 → 다른 회사 새 사용자 B09 잇기 기억에 나옴
|
||||
cut = _one(alias.gather(), alias.BILL, CUT_KEY)
|
||||
alias.approve(alias.BILL, CUT_KEY, cut["서명"], {"id": 1})
|
||||
assert [r["열쇠"] for r in alias.master_bill()] == [CUT_KEY]
|
||||
monkeypatch.setattr(route.access, "folder", lambda got, create=False: tmp_path / got.layer)
|
||||
user = {"company_id": 9, "user_id": 99, "role": "USER"}
|
||||
assert route.recalled(user)[CUT_KEY]["확정"] == "earth:4"
|
||||
assert alias.forget_bill(CUT_KEY) == [] and CUT_KEY not in route.recalled(user)
|
||||
assert master_hash() == before
|
||||
|
||||
|
||||
def test_길_시스템_관리자만(client):
|
||||
_seed()
|
||||
app = FastAPI()
|
||||
app.include_router(alias.router, dependencies=[Depends(require_system_admin)])
|
||||
who = {"user_id": 1, "company_id": None, "role": "USER", "name": "갑"}
|
||||
app.dependency_overrides[verify_session] = lambda: who
|
||||
web = TestClient(app)
|
||||
for role in ("USER", "ADMIN"):
|
||||
who["role"] = role
|
||||
assert web.get("/api/m01/alias-ledger").status_code == 403
|
||||
who["role"] = "SYSTEM_ADMIN"
|
||||
got = web.get("/api/m01/alias-ledger").json()
|
||||
assert len(got["후보"]) == 4 and got["마스터내역"] == []
|
||||
form = _one(got, alias.ALIAS, FORM_KEY)
|
||||
pick = {"종류": alias.ALIAS, "열쇠": FORM_KEY, "서명": form["서명"]}
|
||||
res = web.post("/api/m01/alias-ledger/approve", json=pick)
|
||||
assert res.status_code == 200 and len(res.json()["후보"]) == 3
|
||||
assert ledger.read(client.system)[FORM_KEY]["갈래"] == "안 씀"
|
||||
assert web.post("/api/m01/alias-ledger/approve", json=pick).status_code == 404
|
||||
assert web.post("/api/m01/alias-ledger/reject", json={**pick, "서명": "x"}).status_code == 422
|
||||
gone = web.post("/api/m01/alias-ledger/master-bill/forget", json={"열쇠": CUT_KEY})
|
||||
assert gone.status_code == 404
|
||||
main = (ROOT / "main.py").read_text(encoding="utf-8")
|
||||
at = main.index("M01_MasterData.M01_MasterData_Alias")
|
||||
assert "dependencies=system_admin_only" in main[at : at + 120]
|
||||
@@ -52,9 +52,6 @@ def test_721_2_부르는_곳_셋만_새_창():
|
||||
):
|
||||
text = _text(f)
|
||||
assert "openLogicPicker(" in text and "sangeun:" not in text, f.name
|
||||
for f in ("Hopyo_Ledger",):
|
||||
text = _text(M02 / f"M02_MasterTemplete_{f}.ts")
|
||||
assert "openFindPicker({" in text and "openLogicPicker" not in text.split("*/", 1)[1], f
|
||||
# 탭 셋 · 찾기 칸 하나 · 거름 고르기 없음 · M01 = 로직 · 인력 · 재료(기계 · 호표 없음)
|
||||
for key in ('key: "m01"', 'key: "sk"', 'key: "pre"'):
|
||||
assert key in find
|
||||
@@ -125,8 +122,6 @@ def test_736_1_항목_추가_찾기_창_하나():
|
||||
find = _text(M02 / "M02_MasterTemplete_Find_Pick.ts")
|
||||
assert 'kind === "재료" && opt.material' in find and "? opt.material" in find
|
||||
assert '"logic" | "labor" | "material" | "machine" | "oil" | "sk" | "pre"' in find # 785-3 oil
|
||||
# 별칭 장부 = 기초 · 재료 둘(옛 확인 창 고르기는 785-7 에서 지움)
|
||||
assert 'kinds: ["logic", "material"],' in _text(M02 / "M02_MasterTemplete_Hopyo_Ledger.ts")
|
||||
|
||||
|
||||
def test_736_2_빠진_길_둘_신규_자재_기계_탭():
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
"""별칭 장부 중앙 관리(PLAN 739-1 · `M02_Hopyo_Ledger` ·
|
||||
`M01_MasterData_Alias`) — 자동 짝 = 마스터(승인) · 이 불러오기에서 이은 짝만 · 회사 · 개인 층 짝 =
|
||||
「내 제안」(자동 0 · 꼬리표 · 고르기 맨 위 `제안`) · 이은 짝 → 층 장부(불러오기 표시) → M01 후보 ·
|
||||
승인 뒤 다음 불러오기 자동 · 장부 화면 승인 상태(대기 · 승인 · 반려) · master_data 원본 그대로.
|
||||
층 · storage 는 임시 폴더.
|
||||
"""
|
||||
|
||||
from M01_MasterData import M01_MasterData_Alias as alias
|
||||
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
|
||||
# 올리기 길 시험의 `client` 마당(임시 storage · 마스터 층)을 같이 씀
|
||||
# ruff: noqa: F811
|
||||
from resources.tester.test_m02_hopyo_excel import URL, _post, client, master_hash # noqa: F401
|
||||
|
||||
DIG = {"이름": "구조물터파기(인력100%)", "규격": "토사", "단위": "㎥"}
|
||||
DIG_KEY = "구조물터파기(인력100%)|토사|m3"
|
||||
WHO = {"id": 42, "이름": "갑"}
|
||||
ADMIN = {"id": 1, "이름": "관리자"}
|
||||
LEDGER = "/api/m02/layers/{}/hopyo-ledger"
|
||||
FROM_LEDGER = ("별칭", "뺌") # 장부가 자동으로 정한 줄 상태
|
||||
|
||||
|
||||
def _pair(code):
|
||||
return ledger.check(DIG_KEY, {"갈래": "짝", "짝": [{"로직": code, "배": 1}]})
|
||||
|
||||
|
||||
def _candidate(key, code):
|
||||
got = [
|
||||
x
|
||||
for x in alias.gather()["후보"]
|
||||
if x["열쇠"] == key and x["종류"] == alias.ALIAS and x["짝"][0]["로직"] == code
|
||||
]
|
||||
assert len(got) == 1, got
|
||||
return got[0]
|
||||
|
||||
|
||||
def test_장부_층_짝은_제안_이_불러오기_짝은_자동_승인_뒤_모두_자동(client):
|
||||
before = master_hash()
|
||||
mine = layers.personal_dir(7, 42)
|
||||
here = ledger.doc_id(mine, "HP-000001")
|
||||
assert here == "personal/7/42/HP-000001"
|
||||
ledger.write(mine, {DIG_KEY: _pair("GF000101")}, WHO, "가.xls", here)
|
||||
got = ledger.recall(DIG, mine, doc=here)
|
||||
assert (got["층"], got["제안"]) == ("personal", False) # 이 불러오기에서 이은 짝 = 바로 씀
|
||||
later = ledger.doc_id(mine, "HP-000002") # 다음 불러오기
|
||||
for doc in (later, None):
|
||||
got = ledger.recall(DIG, mine, doc=doc)
|
||||
assert (got["층"], got["짝"][0]["로직"], got["제안"]) == ("personal", "GF000101", True)
|
||||
ledger.put(mine, {DIG_KEY: {**_pair("GF000101"), "엑셀조항": []}}, WHO) # 장부 화면 고침
|
||||
assert ledger.read(mine)[DIG_KEY]["불러오기"] == here # 이은 불러오기 표시는 남음
|
||||
assert [r["상태"] for r in ledger.rows(mine, True)] == ["대기"]
|
||||
# M01 후보에 보임 → 승인 → 마스터 → 다음 불러오기 · 다른 회사 새 사용자도 자동
|
||||
sig = _candidate(DIG_KEY, "GF000101")["서명"]
|
||||
alias.approve(alias.ALIAS, DIG_KEY, sig, ADMIN)
|
||||
for folder, doc in ((mine, later), (layers.personal_dir(9, 99), None)):
|
||||
got = ledger.recall(DIG, folder, doc=doc)
|
||||
assert (got["층"], got["짝"][0]["로직"], got["제안"]) == ("system", "GF000101", False)
|
||||
assert [(r["층"], r["상태"]) for r in ledger.rows(mine, True)] == [
|
||||
("personal", "승인"),
|
||||
("system", "승인"),
|
||||
]
|
||||
# 다른 짝 제안 → 반려 = 상태 반려 · 여전히 내 제안(자동 아님)
|
||||
other = layers.personal_dir(7, 43)
|
||||
ledger.write(other, {DIG_KEY: _pair("GF000103")}, {"id": 43}, "나.xls")
|
||||
alias.reject(alias.ALIAS, DIG_KEY, _candidate(DIG_KEY, "GF000103")["서명"], ADMIN)
|
||||
assert ledger.rows(other, True)[0]["상태"] == "반려"
|
||||
got = ledger.recall(DIG, other)
|
||||
assert (got["층"], got["짝"][0]["로직"]) == ("system", "GF000101") # 자동 = 마스터
|
||||
assert master_hash() == before
|
||||
|
||||
|
||||
def _hopyo(client, no):
|
||||
res = client.get(f"{URL.format('personal')}/HP-000001/{no}")
|
||||
assert res.status_code == 200, res.text
|
||||
return res.json()["호표"]
|
||||
|
||||
|
||||
def _lines(client, no):
|
||||
return {one["이름"]: one["맞춤"] for one in _hopyo(client, no)["줄"]}
|
||||
|
||||
|
||||
def _auto(client, no):
|
||||
"""장부가 자동으로 정한 줄 수."""
|
||||
return sum(1 for m in _lines(client, no).values() if m["상태"] in FROM_LEDGER)
|
||||
@@ -1,52 +0,0 @@
|
||||
"""일위대가 별칭 장부 길(PLAN 324-3 · 338-3 · `M02_Hopyo_Ledger`) — 보기 · 고치기 · 권한 · master_data 원본 그대로.
|
||||
|
||||
옛 확인 만들기 길(`/hopyo-build` · `/hopyo-unlinked`)은 785-7 에서 지움 — 만들기 = 옮기기(`test_642_5_make`).
|
||||
층은 임시 폴더.
|
||||
"""
|
||||
|
||||
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 _post, client, master_hash # noqa: F401
|
||||
|
||||
DIG = "구조물터파기(인력100%)|토사|m3"
|
||||
SOIL = "잔토처리(인력100%)||m3"
|
||||
LEDGER = "/api/m02/layers/{}/hopyo-ledger"
|
||||
ORIGIN = {"품명": "구조물터파기(인력100%)", "규격": "토사", "단위": "m3"}
|
||||
|
||||
|
||||
def _book(layer_dir):
|
||||
return json.loads((layer_dir / hs.DIRNAME / f"{hs.MEMORY}.json").read_bytes())["줄"]
|
||||
|
||||
|
||||
def test_장부_길_보기_고치기_권한(client):
|
||||
before = master_hash()
|
||||
assert _post(client).status_code == 200
|
||||
seed = {"줄": {DIG: {"원래": ORIGIN, "갈래": "짝", "짝": [{"로직": "GF000101"}]}}}
|
||||
assert client.put(LEDGER.format("personal"), json=seed).status_code == 200
|
||||
rows = client.get(LEDGER.format("personal")).json()["줄"]
|
||||
assert (DIG, "personal", True) in [(r["열쇠"], r["층"], r["고침"]) for r in rows]
|
||||
put = {
|
||||
"줄": {DIG: {"갈래": "짝", "짝": [{"로직": "GF000103", "배": 2}]}, SOIL: {"갈래": "안 씀"}}
|
||||
}
|
||||
res = client.put(LEDGER.format("personal"), json=put)
|
||||
assert res.status_code == 200, res.text
|
||||
assert res.json()["씀"] == 2
|
||||
book = _book(layers.personal_dir(7, 42))
|
||||
assert book[DIG]["짝"][0] == {"로직": "GF000103", "입력값": {}, "배": 2, "몫": None}
|
||||
bad = {"줄": {DIG: {"갈래": "짝", "짝": [{"로직": "XX"}]}}}
|
||||
assert client.put(LEDGER.format("personal"), json=bad).status_code == 400
|
||||
assert client.put(LEDGER.format("personal"), json={"줄": {DIG: None}}).status_code == 200
|
||||
assert DIG not in _book(layers.personal_dir(7, 42))
|
||||
assert client.put(LEDGER.format("system"), json=put).status_code == 403 # 위 층 = 읽기만
|
||||
client.who.update(role="SYSTEM_ADMIN")
|
||||
assert client.put(LEDGER.format("system"), json=put).status_code == 200
|
||||
client.who.update(role="USER")
|
||||
rows = client.get(LEDGER.format("personal")).json()["줄"]
|
||||
assert {(r["층"], r["고침"]) for r in rows} == {("personal", True), ("system", False)}
|
||||
assert client.get(LEDGER.format("system")).json()["줄"][0]["고침"] is False
|
||||
assert master_hash() == before
|
||||
@@ -94,13 +94,6 @@ def test_층_차례_마스터만_자동_아래_층은_제안_느슨_찾기(clien
|
||||
assert ledger.recall(DIG, mine, whole=True) is None # 호표 통째 열쇠는 따로
|
||||
books = ledger.books(mine)
|
||||
assert ledger.recall(DIG, mine, books)["짝"][0]["로직"] == "GF000101" # 한 번 읽어 넘김
|
||||
rows = ledger.rows(mine, True)
|
||||
assert [(r["층"], r["고침"], r["상태"]) for r in rows] == [
|
||||
("personal", True, "대기"),
|
||||
("company", False, "대기"),
|
||||
("system", False, "승인"),
|
||||
] # 위 층 = 읽기만 · 739-1 승인 상태
|
||||
assert not any(r["고침"] for r in ledger.rows(mine, False))
|
||||
|
||||
|
||||
def test_재검사(client):
|
||||
|
||||
@@ -8,6 +8,8 @@ from pathlib import Path
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
M02 = Path(HERE).parents[1] / "M02_MasterTemplete"
|
||||
|
||||
|
||||
def _text(name: str) -> str:
|
||||
return (M02 / name).read_text(encoding="utf-8")
|
||||
|
||||
@@ -20,7 +22,7 @@ def test_좌측_판_자리와_권한():
|
||||
order = ("precomboSec,", "summary,", "createDivider(),", "section(hopyo.title, hopyo.body),")
|
||||
assert [host.index(k) for k in order] == sorted(host.index(k) for k in order)
|
||||
assert host.index("section(hopyo.title, hopyo.body),") < host.index("drawing,")
|
||||
assert "canWrite: opt.canWrite," in side # 구조물 조합 층 쓰기 권한(720-4 함수)
|
||||
assert "canWrite: opt.canWrite," not in side # 옛 [별칭 장부] 걷음(815-2)
|
||||
assert "hopyo.paint();" in side.split("function paint(): void {")[1]
|
||||
hopyo_side = _text("M02_MasterTemplete_Hopyo_Side.ts")
|
||||
# 718-1 · 785-4 — 제목 = 화면 글 키 · [불러오기] = 구조물 조합 층 단추 꼴 → 바로 파일 고르기 → 본 칸 불러오기 보기
|
||||
@@ -30,9 +32,8 @@ def test_좌측_판_자리와_권한():
|
||||
assert "buildHopyoImport({" in hopyo_side and "Hopyo_Wizard" not in hopyo_side
|
||||
assert not (M02 / "M02_MasterTemplete_Hopyo_Wizard.ts").exists() # 가운데 상세 페이지 없음
|
||||
assert "uploadHopyo(" in hopyo_side and "openHopyoSheet" not in hopyo_side
|
||||
assert (
|
||||
"buildHopyoLedger({ scope: pageScope(), canWrite: opt.canWrite() })" in hopyo_side
|
||||
) # [별칭 장부]
|
||||
assert "buildHopyoLedger" not in hopyo_side and "data-hopyo-ledger" not in hopyo_side
|
||||
assert not (M02 / "M02_MasterTemplete_Hopyo_Ledger.ts").exists() # 옛 [별칭 장부] 걷음(815-2)
|
||||
imp = _text("M02_MasterTemplete_Hopyo_Import.ts")
|
||||
assert 'accept: ".xls,.xlsx"' in hopyo_side and "input.hidden = true;" in hopyo_side
|
||||
choose = hopyo_side.split("async function choose()")[1].split("input.addEventListener")[0]
|
||||
@@ -54,4 +55,4 @@ def test_372_2_저장은_개인_층에_삭제는_화면_층_권한():
|
||||
assert "a.onEnded();" in imp and "held = false;" in imp
|
||||
assert 'addEventListener("pagehide", leave)' in imp and " leave,\n" in imp
|
||||
panel = _text("M02_MasterTemplete_Hopyo_Side.ts")
|
||||
assert panel.count("open?.imp?.leave()") == 2 and panel.count("open.imp?.leave()") == 1
|
||||
assert panel.count("open?.imp?.leave()") == 1 and panel.count("open.imp?.leave()") == 1
|
||||
|
||||
Reference in New Issue
Block a user