feat(739-1): 별칭 장부 중앙 관리 — 자동 짝 = 마스터(승인) · 이 불러오기에서 이은 짝만 · 회사 · 개인 층 짝 = 내 제안

- 장부 읽는 차례: 자동 정확 열쇠(마스터 · 같은 불러오기 `불러오기` 표시) → 제안 정확 열쇠(`제안` True · 자동 아님) → 품명|단위 느슨
- 만들기 · 단가산출 만들기가 이은 짝에 불러오기 열쇠(`doc_id` 층/주인/HP 키) 남김 · 장부 화면 고침은 그 표시 그대로 둠
- 줄 맞춤: 내 제안 = 숫자 판정 그대로 + 꼬리표 「내 제안」 + `제안`(고르기 창 맨 위 · 한 번 눌러 짝 통째) · 호표 통째 제안 = 체크 안 함
- 장부 화면(회사 · 개인 층) = 「내 제안 목록」 · 승인 칸(대기 · 승인 · 반려 = M01 서명 · 반려 기억) · [삭제] → [거두기] · 마스터 층(M01) 그대로
- M01 별칭 장부 모으기 · 승인 · 반려 길 그대로 · 기존 층 장부 파일 안 지움(제안으로 읽힘)
- 낡은 시험 고침: hopyo ledger(아래 층이 이김 → 마스터만 자동 · 제안 · 승인 상태) · hopyo match(별칭 자동 규칙 = 마스터 층 장부로) · hopyo screen(머리 글 · 거두기 단추)
- 새 시험 test_739_1_alias_central(층 짝 자동 0 · 내 제안 · 이은 짝 → 제안 · M01 후보 · 승인 뒤 자동 · 반려 상태)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-02 18:40:45 +09:00
co-authored by Claude Opus 5.5
parent 88815881da
commit b1b9794946
12 changed files with 319 additions and 44 deletions
+3 -2
View File
@@ -168,6 +168,7 @@ class Builder:
raise _bad(f"없는 호표 「제{no}호표」", 404)
self.src, self.dry, self._sk, self._maker = Path(source), False, None, None
self.strict = True # 남은 줄 있으면 409(붙여넣기 `build_all` = 거짓)
self.doc_id = ledger.doc_id(source, key) # 739-1 이 불러오기에서 이은 짝
# ── 재검사 ───────────────────────────────────────────────────────
def _target(self, raw: dict[str, Any], where: str, line: dict | None = None) -> dict[str, Any]:
@@ -395,11 +396,11 @@ class Builder:
count |= {"빠짐": sum(1 for x in missing if x["까닭"] != TWICE), "이음": links}
count["단가산출"] = sum(1 for x in lines if _is_sk(x))
if not save:
ledger.write(self.folder, book, self.who, file or None)
ledger.write(self.folder, book, self.who, file or None, self.doc_id)
head = {"문서": doc, "빠진줄": missing, "남은줄": left}
return {**head, "줄수": {**count, "이음후보": linker.near}}
ref, again = _save(doc)
wrote = ledger.write(self.folder, book, self.who, file or None)
wrote = ledger.write(self.folder, book, self.who, file or None, self.doc_id)
out = {"참조": ref, "다시씀": again, "빠진줄": missing, "장부": {"씀": wrote}}
return {**out, "줄수": {**count, "이음후보": linker.near}}
+53 -11
View File
@@ -6,8 +6,10 @@
직접 줄 호표 통째 = 열쇠 「호표|품명|규격|단위」.
옛 줄 {로직|자재|빼기, 입력값, 환산, 확정} = 읽을 때 새 꼴로 바꿔 읽음(빼기 = 「안 씀」 ·
파일은 다음 쓰기 때 새 꼴).
읽는 차례 = 같은 층 → 위 층(개인 → 회사 → 마스터) · 아래 층이 이김 · 정확 열쇠가 먼저 · 없으면
품명|단위 느슨 열쇠(`규격다름`).
중앙 관리(739-1) — 자동 짝 = 마스터 층(M01 별칭 장부 승인) · 이 불러오기에서 이은 짝
(줄 `불러오기` = `doc_id`)만 · 회사 · 개인 층 짝 = 「내 제안」(`제안` True · 자동 아님 ·
후보 맨 위 · M01 승인 대기).
읽는 차례 = 자동 정확 열쇠(같은 층 → 위 층) → 제안 정확 열쇠 → 품명|단위 느슨 열쇠(`규격다름`).
쓰는 때 = [확인하고 사전조합 만들기] · 장부 화면 [저장]만 · 쓰기 = 그 층만(권한은 길 몫) ·
짝은 서버가 다시 검사(돈 로직 · 그 로직 입력 이름 · M01 자재품목 · 배 0 넘음 · 몫 (0, 1]).
"""
@@ -101,32 +103,69 @@ def books(folder: str | Path) -> list[tuple[str, str | None, dict[str, dict[str,
return [(*refs.where(one), read(one)) for one in chain(folder)]
def doc_id(folder: str | Path, doc: str) -> str:
"""불러오기 하나 열쇠 — 「층/주인/HP 키」(장부 줄 `불러오기` · 같은 불러오기 안 자동)."""
layer, owner = refs.where(folder)
return f"{layer}/{owner or ''}/{doc}"
def recall(
line: dict[str, Any], folder: str | Path, got: list | None = None, whole: bool = False
line: dict[str, Any],
folder: str | Path,
got: list | None = None,
whole: bool = False,
doc: str | None = None,
) -> dict[str, Any] | None:
"""줄(`whole` = 호표 통째)의 장부 짝 — `{…줄, 층, 주인, 규격다름}` · 없으면 None.
정확 열쇠(아래 층 먼저) → 품명|단위 느슨(규격다름 True · 체크 안 함)."""
"""줄(`whole` = 호표 통째)의 장부 짝 — `{…줄, 층, 주인, 규격다름, 제안}` · 없으면 None.
자동 정확 열쇠(마스터 · `doc` 불러오기에서 이은 짝) → 제안 정확 열쇠(회사 · 개인 층 ·
`제안` True · 자동 아님) → 품명|단위 느슨(규격다름 True · 체크 안 함)."""
full = hopyo_key(line) if whole else key(line)
got = books(folder) if got is None else got
for layer, owner, book in got:
if full in book:
return {**book[full], "층": layer, "주인": owner, "규격다름": False}
hits = [(layer, owner, book[full]) for layer, owner, book in got if full in book]
for layer, owner, row in hits:
if layer == "system" or (doc and row.get("불러오기") == doc):
return {**row, "층": layer, "주인": owner, "규격다름": False, "제안": False}
if hits:
layer, owner, row = hits[0]
return {**row, "층": layer, "주인": owner, "규격다름": False, "제안": True}
loose = _loose(full)
for layer, owner, book in got:
for one, row in book.items():
if one.startswith(HOPYO) == whole and _loose(one) == loose:
return {**row, "층": layer, "주인": owner, "규격다름": True}
return {**row, "층": layer, "주인": owner, "규격다름": True, "제안": False}
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
@@ -239,9 +278,10 @@ def write(
entries: dict[str, dict[str, Any] | None],
who: Any,
file: str | None = None,
doc: str | None = None,
) -> int:
"""검사 끝난 줄을 층 장부에 씀(None = 지움) — 같은 열쇠는 덮음 · 처음파일은 있던 값 둠 ·
옛 꼴 줄은 이때 새 꼴로 → 쓴 줄 수."""
`doc` = 이은 불러오기(`doc_id` · 없으면 있던 값) · 옛 꼴 줄은 이때 새 꼴로 → 쓴 줄 수."""
if not entries:
return 0
have = read(folder)
@@ -254,6 +294,8 @@ def write(
continue
first = (have.get(full) or {}).get("처음파일") or file
out[full] = {**row, "누가": who, "때": stamp, "처음파일": first, "판": version}
if came := doc or (have.get(full) or {}).get("불러오기"):
out[full]["불러오기"] = came
hopyo_store.remember(folder, out)
return len(entries)
+22 -5
View File
@@ -17,6 +17,9 @@
입력값 출처(칸마다) = 별칭 > 블록(옵션 찾기 값이 규격 읽기와 다름) > 규격(`spec_inputs`) > 자동값.
체크 = 처음 제안일 뿐 — 들어감은 [확인하고 사전조합 만들기](다 창)만. 저장 없음 · 셈은 서버 단독.
장부 = `M02_Hopyo_Ledger`(다 창) · 확인된 사전조합 = `M02_Hopyo_Build.confirmed`(다 창).
④ ⑤ 장부 짝 = 자동 짝만(마스터 · 이 불러오기에서 이은 짝 · 739-1) · 회사 · 개인 층 짝 =
「내 제안」 — 숫자 판정 그대로 + 꼬리표 「내 제안」 + `제안 {갈래, 짝, 대상, 층, 때}`
(고르기 창 맨 위 · 한 번 눌러 고름).
"""
from __future__ import annotations
@@ -40,6 +43,7 @@ from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_MasterTemplete_Store as store
SKIP = ("우리에 없음", "안 씀")
MINE = "내 제안"
LOOSE = "별칭 · 규격 다름"
LEDGER_OFF = "장부와 다름"
OUTSIDE = ("%", "노임", "뺌") # 연결 셈 밖(358-3 ②)
@@ -55,6 +59,7 @@ class Ctx:
self.got = hm.targets()
self.units = {int(h["번"]): h for h in self.doc.get("호표") or []}
self.books = ledger.books(self.to)
self.doc_id = ledger.doc_id(self.folder, key)
self.calc = fit.ensure(folder, key)
self.cache = (fit.result(folder, key) or {}).get("블록") or {}
self.done = hopyo_build.confirmed(self.to, refs.where(self.folder))
@@ -255,7 +260,8 @@ def match_line(line: dict[str, Any], ctx: Ctx, full: bool = True) -> dict[str, A
return {**out, "상태": "호표", "체크": ok, "대상": [target], "글": text}
if kind == "노임":
return {**out, "상태": "노임", "글": "노임 · 그릇 없음"}
alias = ledger.recall(line, ctx.to, ctx.books)
alias = ledger.recall(line, ctx.to, ctx.books, doc=ctx.doc_id)
mine, alias = (alias, None) if alias and alias.get("제안") else (None, alias)
loose, alias = (alias, None) if alias and alias.get("규격다름") else (None, alias)
entry = ctx.cache.get(fit.unit_key(line)) if kind not in hm.MATERIAL_KINDS else None
if alias and alias.get("갈래") in SKIP:
@@ -273,9 +279,20 @@ def match_line(line: dict[str, Any], ctx: Ctx, full: bool = True) -> dict[str, A
why = "셈 중" if not ctx.calc["끝"] else fit.NO_NUMBER
got = _names_only(line, ctx, why, full)
got = _loose(got, loose, line, ctx) if loose and full else got
got = _mine(got, mine, line, ctx) if mine else got
return {**out, **(_grouped(got, line, ctx) if full and kind not in hm.MATERIAL_KINDS else got)}
def _mine(got: dict, mine: dict, line: dict, ctx: Ctx) -> dict:
"""내 제안(739-1 · 승인 전 회사 · 개인 층 짝) — 자동 아님 · 꼬리표 + `제안`(고르기 맨 위)."""
pairs = [p for p in mine.get("짝") or [] if p.get("로직") or p.get("자재")]
kind = mine.get("갈래")
word = MINE if kind == "짝" else f"{MINE} — {kind}"
head = {"갈래": kind, "짝": pairs, "층": mine.get("층"), "때": mine.get("때")}
targets = [_pair(p, line, ctx) for p in pairs]
return {**got, "꼬리표": [*(got.get("꼬리표") or []), word], "제안": {**head, "대상": targets}}
def _grouped(got: dict, line: dict, ctx: Ctx) -> dict:
"""묶음 표시(358-1) — 후보 · 이름후보(옛 324 다섯)마다 뜻겹침 · 출처같음(조항 ≥ 0.9) · 책 ·
차례 · 무게 · 상태 그대로(아무것도 숨기지 않음 · 묶음은 화면 몫)."""
@@ -356,12 +373,12 @@ def _aliased(line: dict, ctx: Ctx, alias: dict, entry: dict | None) -> dict:
def whole(hopyo: dict, ctx: Ctx) -> dict | None:
"""직접 줄 호표 통째 제안 — 장부 짝(호표 열쇠) 먼저 · 맞춤셈 「호표:번」."""
head = {k: hopyo.get(k) for k in ("이름", "규격", "단위")}
alias = ledger.recall(head, ctx.to, ctx.books, whole=True)
alias = ledger.recall(head, ctx.to, ctx.books, whole=True, doc=ctx.doc_id)
if alias and alias.get("갈래") == "짝" and alias.get("짝") and not alias.get("규격다름"):
return {
return { # 내 제안(739-1) = 체크 안 함 · 한 번 눌러 고름
"대상": [_pair(p, head, ctx) for p in alias["짝"]],
"글": "별칭 장부 짝",
"체크": True,
"글": MINE if alias.get("제안") else "별칭 장부 짝",
"체크": not alias.get("제안"),
}
entry = ctx.cache.get(f"호표:{hopyo['번']}")
if not entry or not entry.get("차례"):
+3 -1
View File
@@ -306,6 +306,8 @@ def build(folder: Path, key: str, who: Any = None, picks: Any = None) -> dict[st
found = ledger.recall(line, folder, books)
if no in chosen or found is None or found.get("규격다름"):
alias[ledger.key(line)] = _alias(line, plan)
out["별칭"] = ledger.write(folder, alias, who, maker.hp.get("파일") or None)
out["별칭"] = ledger.write(
folder, alias, who, maker.hp.get("파일") or None, ledger.doc_id(folder, key)
)
out["같은것씀"], out["위층같음"] = maker.reused, maker.above
return out
@@ -153,6 +153,8 @@ export interface HopyoMatch {
갈리는칸?: string[];
별칭?: { 층: Layer; 주인: string | null; 규격다름?: boolean; 근거다름?: boolean };
뺌?: string;
/** 739-1 내 제안(승인 전 회사 · 개인 층 장부 짝) — 자동 아님 · 고르기 창 맨 위 */
제안?: { 갈래: string; 짝: LedgerPair[]; 대상: HopyoPart[]; 층: Layer; 때: string | null };
}
/** 숫자 판정 셈 — 끝 · 한 블록 · 모두 · 까닭(우리 로직 준비 중 등) */
@@ -405,6 +407,8 @@ export interface LedgerRow extends LedgerEntry {
층: Layer;
주인: string | null;
고침: boolean;
/** 739-1 M01 승인 상태 — 마스터 줄 = 승인 */
상태?: "승인" | "대기" | "반려";
}
const enc = encodeURIComponent;
@@ -189,7 +189,9 @@ function actsCell(line: HopyoLine, parts: HopyoPart[], ctx: RowsContext): HTMLEl
return el("td", {
className: "m02-hopyo__acts",
children: [
...(ctx.canWrite && m.상태 !== "%" ? [ghost("고르기", () => ctx.choose(line))] : []),
...(ctx.canWrite && m.상태 !== "%"
? [ghost(m.제안?.짝.length ? "고르기 · 내 제안" : "고르기", () => ctx.choose(line))]
: []),
ghost("원문 보기", () => ctx.source(line)),
...(line.근거 ? [ghost("엑셀 보기", () => ctx.sheet(line.근거!.행))] : []),
...options,
@@ -224,6 +224,37 @@ export function openHopyoChoose(opt: ChooseOptions): void {
),
);
const groups = candGroups(line.맞춤, material);
// 739-1 내 제안 — 맨 위 · 한 번 눌러 짝 통째로(입력값 · 배 · 몫 그대로)
const mine = line.맞춤.제안?.짝.length ? line.맞춤.제안 : null;
const mineRow = mine
? el("div", {
attrs: { "data-group": "내 제안" },
children: [
note("내 제안 — 승인 전(M01 별칭 장부 승인 뒤 자동)"),
el("div", {
className: "m02-hopyo__cand",
attrs: { "data-cand": "내 제안" },
children: [
createButton({
label: mine.대상.map((c) => candText(c, new Set())).join(" + "),
variant: "ghost",
size: "sm",
onClick: () =>
finish(
mine.짝.map((p) => ({
...(p.로직 ? { 로직: p.로직 } : { 자재: p.자재 }),
입력값: p.입력값 ?? {},
배: p.배,
몫: p.몫 ?? null,
})),
),
}),
el("span", { className: "m02-hopyo__state is-별칭", text: "내 제안" }),
],
}),
],
})
: null;
const twins = sameNames(groups);
const row = (c: HopyoTarget): HTMLElement => {
const share = el("input", {
@@ -332,6 +363,7 @@ export function openHopyoChoose(opt: ChooseOptions): void {
? "후보 — 하나 고름 · 여럿은 체크(몫 칸) 뒤 [일괄 적용]"
: "가까운 것 없음 — 아래 검색",
),
...(mineRow ? [mineRow] : []),
...sections,
el("div", { className: "ui-toolbar m02-hopyo__bar", children: [together, ...more] }),
);
@@ -7,8 +7,11 @@
* 위 층 줄 [내 층 복사] · 갈래 글 = 확인 창 드롭다운과 같음(짝 · 로직없음 · 사용안함 · 저장 값 그대로)
* · 고친 것 = 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";
@@ -38,6 +41,9 @@ 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 };
@@ -139,6 +145,7 @@ export function buildHopyoLedger(opt: LedgerOptions): HTMLElement {
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", {
@@ -249,7 +256,7 @@ export function buildHopyoLedger(opt: LedgerOptions): HTMLElement {
if (mine)
acts.push(
createButton({
label: "삭제",
label: master ? "삭제" : "거두기",
variant: "danger",
size: "sm",
onClick: () => {
@@ -282,6 +289,10 @@ export function buildHopyoLedger(opt: LedgerOptions): HTMLElement {
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 }),
],
});
@@ -296,7 +307,7 @@ export function buildHopyoLedger(opt: LedgerOptions): HTMLElement {
body.replaceChildren(
...(shown.length
? shown.map(rowOf)
: [el("tr", { children: [el("td", { text: "장부 줄 없음", attrs: { colspan: "8" } })] })]),
: [el("tr", { children: [el("td", { text: "장부 줄 없음", attrs: { colspan: "9" } })] })]),
);
}
search.addEventListener("input", paint);
@@ -332,8 +343,12 @@ export function buildHopyoLedger(opt: LedgerOptions): HTMLElement {
el("div", {
className: "ui-toolbar m02-hopyo__bar",
children: [
el("strong", { text: LEDGER_TITLE }),
note(`${LEDGER_HINT} · ${L("M02_PrecomboTitle")} 생성 때 남음 · 아래 층이 이김`),
el("strong", { text: master ? LEDGER_TITLE : MINE_TITLE }),
note(
master
? `${LEDGER_HINT} · 여기 짝 = 모든 사용자 불러오기에 자동`
: `내 층 짝 = 내 제안(다음 불러오기에서 후보 맨 위 · 자동 아님) · M01 별칭 장부 승인 뒤 자동 · ${L("M02_PrecomboTitle")} 생성 때 남음`,
),
search,
kind,
...(opt.canWrite ? [save, drop] : []),
@@ -356,6 +371,7 @@ export function buildHopyoLedger(opt: LedgerOptions): HTMLElement {
"우리 로직 · 배 · 몫 · 옵션",
"누가 · 때 · 처음 파일",
"층",
"승인",
"",
].map((t) => el("th", { text: t })),
}),
@@ -0,0 +1,147 @@
"""별칭 장부 중앙 관리(PLAN 739-1 · `M02_Hopyo_Ledger` · `M02_Hopyo_Line` ·
`M01_MasterData_Alias`) — 자동 짝 = 마스터(승인) · 이 불러오기에서 이은 짝만 · 회사 · 개인 층 짝 =
「내 제안」(자동 0 · 꼬리표 · 고르기 맨 위 `제안`) · 이은 짝 → 층 장부(불러오기 표시) → M01 후보 ·
승인 뒤 다음 불러오기 자동 · 장부 화면 승인 상태(대기 · 승인 · 반려) · master_data 원본 그대로.
층 · storage 는 임시 폴더.
"""
from M01_MasterData import M01_MasterData_Alias as alias
from M02_MasterTemplete import M02_Hopyo_Fit as fit
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 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)
def test_불러오기_내_제안_후보_이은_짝_승인_흐름(client):
before = master_hash()
assert _post(client).status_code == 200
mine = layers.personal_dir(7, 42)
fit.run_job(mine, "HP-000001")
plain = _lines(client, 3)["구조물터파기(인력100%)"]
assert plain["상태"] not in FROM_LEDGER and "제안" not in plain
# 개인 층 장부 짝(옛 층 장부 · 승인 전) = 자동 0 · 꼬리표 「내 제안」 · 고르기 맨 위 `제안`
row = {"갈래": "짝", "짝": [{"로직": "GC000083", "배": 1}]}
res = client.put(LEDGER.format("personal"), json={"줄": {DIG_KEY: row}})
assert res.status_code == 200, res.text
assert _auto(client, 3) == 0
got = _lines(client, 3)["구조물터파기(인력100%)"]
assert (got["상태"], got["체크"]) == (plain["상태"], plain["체크"]) # 숫자 판정 그대로
assert "내 제안" in got["꼬리표"] and "별칭" not in got
assert got["제안"]["짝"][0]["로직"] == "GC000083" and got["제안"]["층"] == "personal"
assert got["제안"]["대상"][0]["로직"] == "GC000083"
# 확인하고 만들기 = 이은 짝 → 이 불러오기 안에서는 바로 자동 · 층 장부(제안)에 불러오기 표시
hopyo = _hopyo(client, 3)
at = next(x["차례"] for x in hopyo["줄"] if x["이름"] == "구조물터파기(인력100%)")
body = {"층": "personal", "키": "HP-000001", "번": 3}
lines = [{"차례": at, "대상": [{"로직": "GC000083", "입력값": {}, "배": 1}]}]
left = client.post(
"/api/m02/layers/personal/hopyo-unlinked", json={**body, "줄": lines}
).json()["남은줄"]
lines += [{"차례": x["차례"], "뺌": "우리에 없음"} for x in left]
res = client.post("/api/m02/layers/personal/hopyo-build", json={**body, "줄": lines})
assert res.status_code == 200, res.text
book = ledger.read(mine)
assert book[DIG_KEY]["불러오기"] == "personal/7/42/HP-000001"
got = _lines(client, 3)["구조물터파기(인력100%)"]
assert got["상태"] == "별칭" and got["별칭"]["층"] == "personal"
# 다음 불러오기(다른 HP) = 승인 전 자동 0
nxt = {k: {**v, "불러오기": "personal/7/42/HP-000009"} for k, v in book.items()}
hs.remember(mine, nxt) # 장부 파일 그대로 — 다른 불러오기에서 이은 짝
assert _auto(client, 3) == 0
# M01 후보에 보임 → 승인 → 다음 불러오기 자동(마스터)
sig = _candidate(DIG_KEY, "GC000083")["서명"]
alias.approve(alias.ALIAS, DIG_KEY, sig, ADMIN)
got = _lines(client, 3)["구조물터파기(인력100%)"]
assert got["상태"] == "별칭" and got["별칭"]["층"] == "system" and "제안" not in got
assert _auto(client, 3) == 1
assert master_hash() == before
def test_화면_내_제안_글():
"""고르기 창 맨 위 「내 제안」(한 번 눌러 짝 통째) · 장부 화면 = 내 제안 목록 · 승인 칸."""
from pathlib import Path
m02 = Path(__file__).resolve().parents[2] / "M02_MasterTemplete"
choose = (m02 / "M02_MasterTemplete_Hopyo_Choose.ts").read_text(encoding="utf-8")
assert choose.index("...(mineRow ? [mineRow] : [])") < choose.index("...sections")
assert '"data-group": "내 제안"' in choose and "mine.짝.map((p)" in choose
rows = (m02 / "M02_MasterTemplete_Hopyo_CheckRows.ts").read_text(encoding="utf-8")
assert '"고르기 · 내 제안"' in rows
book = (m02 / "M02_MasterTemplete_Hopyo_Ledger.ts").read_text(encoding="utf-8")
assert 'MINE_TITLE = "내 제안 목록"' in book and '"승인",' in book
assert "createBadge(r.상태, STATE_TONE[r.상태])" in book
+18 -14
View File
@@ -1,5 +1,5 @@
"""일위대가 실무자료 별칭 장부(PLAN 338-3 · `M02_Hopyo_Ledger`) — 옛 꼴 바꿔 읽음 · 새 꼴 ·
층 차례(아래 층이 이김) · 느슨 찾기(규격다름) · 우리에 없음 · 안 씀 · 호표 통째 열쇠 ·
층 차례(739-1 자동 = 마스터 · 이 불러오기 짝 · 아래 층 = 제안) · 느슨 찾기(규격다름) · 우리에 없음 · 안 씀 · 호표 통째 열쇠 ·
위 층 읽기만 · 재검사 · 쓰기(처음파일 둠 · 옛 꼴 새로 씀) · master_data 원본 그대로. 층은 임시 폴더.
"""
@@ -65,21 +65,26 @@ def test_옛_꼴_바꿔_읽음(client):
assert got["표지석||개"]["짝"] == [{"자재": "MT004258", "입력값": {}, "배": 1, "몫": None}]
def test_층_차례_아래_층이_이김_느슨_찾기(client):
def test_층_차례_마스터만_자동_아래_층은_제안_느슨_찾기(client):
mine, company, system = ledger.chain(layers.personal_dir(7, 42))
assert (company, system) == (layers.company_dir(7), store.FOLDER)
new = {"갈래": "짝", "짝": [{"로직": "GF000103", "입력값": {}, "배": 1, "몫": None}]}
_put_raw(system, {DIG_KEY: new, "잔토처리(인력100%)||m3": {"갈래": "우리에 없음"}})
_put_raw(company, {DIG_KEY: {**new, "짝": [{"로직": "GF000102", "배": 1}]}})
got = ledger.recall(DIG, mine)
assert (got["층"], got["주인"], got["짝"][0]["로직"], got["규격다름"]) == (
"company",
"7",
"GF000102",
got = ledger.recall(DIG, mine) # 739-1 — 회사 층 짝이 있어도 자동 = 마스터(승인)
assert (got["층"], got["주인"], got["짝"][0]["로직"], got["규격다름"], got["제안"]) == (
"system",
None,
"GF000103",
False,
False,
)
_put_raw(system, {"잔토처리(인력100%)||m3": {"갈래": "우리에 없음"}})
got = ledger.recall(DIG, mine)
assert (got["층"], got["짝"][0]["로직"], got["제안"]) == ("company", "GF000102", True)
_put_raw(mine, {DIG_KEY: {"로직": "GF000101", "환산": 1}})
assert ledger.recall(DIG, mine)["짝"][0]["로직"] == "GF000101" # 같은 층 먼저
got = ledger.recall(DIG, mine)
assert (got["짝"][0]["로직"], got["제안"]) == ("GF000101", True) # 제안 = 같은 층 먼저
rock = {**DIG, "규격": "경암"}
loose = ledger.recall(rock, mine)
assert loose["규격다름"] is True and loose["층"] == "personal" # 품명|단위 느슨
@@ -90,12 +95,11 @@ def test_층_차례_아래_층이_이김_느슨_찾기(client):
books = ledger.books(mine)
assert ledger.recall(DIG, mine, books)["짝"][0]["로직"] == "GF000101" # 한 번 읽어 넘김
rows = ledger.rows(mine, True)
assert [(r["층"], r["고침"]) for r in rows] == [
("personal", True),
("company", False),
("system", False),
("system", False),
] # 위 층 = 읽기만
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))
+11 -4
View File
@@ -157,11 +157,18 @@ def _ledger(client, rows, layer="personal"):
assert res.status_code == 200, res.text
def _master(client, rows):
"""739-1 자동 짝 = 마스터(승인) 장부만 — 시스템 관리자로 마스터 층에 씀."""
client.who.update(role="SYSTEM_ADMIN")
_ledger(client, rows, "system")
client.who.update(role="USER")
def test_별칭_체크_규칙(client):
_ready(client)
dig = "구조물터파기(인력100%)|토사|m3"
remicon = "레미콘타설(장비사용)|철근구조물-소량타설|m3"
_ledger(
_master(
client,
{
dig: {"갈래": "짝", "짝": [{"로직": "GC000083", "배": 1}]},
@@ -172,7 +179,7 @@ def test_별칭_체크_규칙(client):
)
got = _hopyo(client, 3)["구조물터파기(인력100%)"]
assert got["상태"] == "별칭" and got["체크"] is True and got["대상"][0]["출처"] == {}
assert got["별칭"]["층"] == "personal" and not got["별칭"]["근거다름"]
assert got["별칭"]["층"] == "system" and not got["별칭"]["근거다름"]
first = _hopyo(client, 1)
other = first["레미콘타설(장비사용)"] # 숫자 판정 맞음 = GC000198 ≠ 장부 짝 → 판정 먼저(358-3)
assert other["상태"] == "맞음" and other["체크"] is True and "별칭" not in other
@@ -182,14 +189,14 @@ def test_별칭_체크_규칙(client):
assert loose["상태"] == "맞음" and "별칭" not in loose and "별칭 · 규격 다름" in loose["꼬리표"]
assert all("별칭" not in t["출처"].values() for t in loose["대상"])
assert "GC000211" in {c["로직"] for c in loose["후보"]} | {t["로직"] for t in loose["대상"]}
_ledger(
_master(
client, {remicon: {"갈래": "짝", "짝": [{"로직": "GC000198", "입력값": {"소량감률": 50}}]}}
)
plain = _hopyo(client, 7)["레미콘타설(장비사용)"] # 무근구조물 ↔ 장부 철근구조물-소량타설
assert plain["상태"] == "맞음" and plain["체크"] is True and "별칭" not in plain
assert plain["대상"][0]["로직"] == "GC000198" and plain["대상"][0]["입력값"]["소량감률"] == 0
assert "별칭 · 규격 다름" in plain["꼬리표"]
_ledger(client, {dig: {"갈래": "짝", "짝": [{"로직": "GF000101", "배": 1}]}})
_master(client, {dig: {"갈래": "짝", "짝": [{"로직": "GF000101", "배": 1}]}})
off = _hopyo(client, 3)["구조물터파기(인력100%)"] # 산림 로직 ↔ 엑셀 건설 3-2-1
assert off["별칭"]["근거다름"] and off["체크"] is False and "근거 다름" in off["꼬리표"]
+3 -2
View File
@@ -514,10 +514,11 @@ def test_명사형_단추와_별칭_장부_풀이_359():
갈래 글 = 확인 창 드롭다운과 같음(저장 값 그대로)."""
ledger = _text("M02_MasterTemplete_Hopyo_Ledger.ts")
hint = "엑셀 품명 ↔ 우리 로직 · 자재 짝을 기억해 두었다가 같은 품명이 나오면 먼저 제안"
assert hint in ledger and "note(`${LEDGER_HINT}" in ledger
assert hint in ledger and "`${LEDGER_HINT}" in ledger
side = _text("M02_MasterTemplete_Hopyo_Side.ts")
assert "title: LEDGER_HINT" in side
for label in ('"변경 취소"', '"항목 추가"', '"내 층 복사"', '"삭제"'):
assert 'label: master ? "삭제" : "거두기"' in ledger # 739-1 내 제안 = 거두기
for label in ('"변경 취소"', '"항목 추가"', '"내 층 복사"'):
assert f"label: {label}" in ledger, label
assert '"우리에 없음": "로직없음"' in ledger and '"안 씀": "사용안함"' in ledger
mine = ("Hopyo_Side", "Hopyo_Sheet", "Hopyo_Check", "Hopyo_CheckRows", "Hopyo_Ledger")