feat(718-1): 일위대가 불러오기 = 한시 마법사 — 결과 층 고름 · [끝내기] 때 올린 파일 지움 · 같은 엑셀 겹침 막음
- 좌측 「일위대가 실무자료」 → 「일위대가 불러오기」(locale M02_HopyoImport) · 옛 파일 목록 걷고 [불러오기] → 본 칸 마법사(새 M02_MasterTemplete_Hopyo_Wizard.ts) - 마법사 = ① 결과 층(쓸 수 있는 층만 · 기본 개인) · 결과 체크(일위대가 · 단가산출은 715-2 뒤) · 엑셀 고르기 · 작업 중 이어서 → ② 엑셀 보기 창(모두 고정으로 들여오기 · 구조물 조합으로 그대로) → ③ 끝내기 - 서버: 올림 지문(sha256) · 작업 중 같은 엑셀 = 이어서 · 끝낸 같은 엑셀 409 · 지우기 = 끝내기(HP · 격자 · 맞춤셈 캐시 지움 · 만든 일위대가 있으면 지문 기록 · 별칭 장부 남음) · HP 번 다시 안 씀 · 목록 = 지문 있는 것만(옛 HP · 붙여넣기 HP 파일 그대로 숨김) - 시험: 새 지킴 test_718_1_hopyo_import(임시 storage) · 낡은 시험 고침 — 같은 엑셀 다시 올림이 새 키를 내던 가정(excel · grid · flow) · 붙여넣기 HP 목록 가정(705_2) · 좌측 판 글 대조(screen) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,10 +4,14 @@
|
||||
파일 = 읽은 결과만(원본 엑셀 안 둠) `{키, 파일, 올린때, 올린이, 시트, 호표, 검산}` · 키 `HP-######`
|
||||
(층마다 다음 번호) · 맞춤 기억 = `{줄: {줄 열쇠: {로직?|자재?|빼기?, 입력값, 환산, 확정}}}`.
|
||||
층 종류(`layers.KINDS`) 밖 폴더 — 양식 목록 · 복사 · 프로젝트 씨앗에 안 섞임 · 권한은 길 몫.
|
||||
한시 불러오기(718-1) — 파일에 `지문`(올린 엑셀 sha256) · 목록 = 지문 있는 것(작업 중)만 · 옛 HP 는
|
||||
파일 그대로 목록에서만 숨김 · 끝낸 기록 `실무자료/불러옴.json`
|
||||
`{번, 지문 {지문: {파일, 키, 끝낸때, 만든}}}` · 번 = 낸 키 가장 큰 번(지운 뒤에도 키를 다시 안 씀).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import threading
|
||||
@@ -21,6 +25,7 @@ from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
DIRNAME = "실무자료"
|
||||
MEMORY = "맞춤"
|
||||
GRID = "격자"
|
||||
DONE = "불러옴"
|
||||
NO_GRID = "격자 없음 — 다시 올리면 보임"
|
||||
_KEY = re.compile(r"^HP-(\d{6})$")
|
||||
# ponytail: 프로세스 안 잠금 — 서버를 여럿 띄우면 파일 잠금으로
|
||||
@@ -58,16 +63,33 @@ def brief(doc: dict[str, Any]) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def fingerprint(data: bytes) -> str:
|
||||
"""올린 엑셀 지문 — 같은 파일 다시 올림을 알아봄."""
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def rows(folder: str | Path) -> list[dict[str, Any]]:
|
||||
"""층 실무자료 목록(키 차례)."""
|
||||
"""층 실무자료 목록(키 차례) — 작업 중(지문 있음)만 · 옛 HP 는 숨김."""
|
||||
out = []
|
||||
for path in sorted(_dir(folder).glob("HP-*.json")):
|
||||
doc = _load(path)
|
||||
if isinstance(doc, dict) and _KEY.match(path.stem):
|
||||
out.append(brief(doc))
|
||||
if isinstance(doc, dict) and _KEY.match(path.stem) and doc.get("지문"):
|
||||
out.append({**brief(doc), "지문": doc["지문"]})
|
||||
return out
|
||||
|
||||
|
||||
def done(folder: str | Path) -> dict[str, Any]:
|
||||
"""끝낸 기록 `{번, 지문 {지문: {파일, 키, 끝낸때, 만든}}}`."""
|
||||
doc = _load(_dir(folder) / f"{DONE}.json")
|
||||
doc = doc if isinstance(doc, dict) else {}
|
||||
marks = doc.get("지문")
|
||||
return {"번": int(doc.get("번") or 0), "지문": marks if isinstance(marks, dict) else {}}
|
||||
|
||||
|
||||
def _put_done(folder: str | Path, got: dict[str, Any]) -> None:
|
||||
atomic_write_json(_dir(folder) / f"{DONE}.json", got)
|
||||
|
||||
|
||||
def read(folder: str | Path, key: str) -> dict[str, Any]:
|
||||
doc = _load(_path(folder, key))
|
||||
if not isinstance(doc, dict):
|
||||
@@ -81,25 +103,30 @@ def save(
|
||||
parsed: dict[str, Any],
|
||||
who: Any,
|
||||
grid: dict[str, Any] | None = None,
|
||||
mark: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""읽은 결과(+ 격자) → 새 키로 씀 → 목록 한 줄."""
|
||||
"""읽은 결과(+ 격자) → 새 키로 씀 → 목록 한 줄 · `mark` = 올린 엑셀 지문."""
|
||||
with _LOCK:
|
||||
have = [
|
||||
int(m.group(1)) for p in _dir(folder).glob("HP-*.json") if (m := _KEY.match(p.stem))
|
||||
]
|
||||
key = f"HP-{max(have, default=0) + 1:06d}"
|
||||
log = done(folder)
|
||||
log["번"] = max(*have, log["번"], 0) + 1
|
||||
key = f"HP-{log['번']:06d}"
|
||||
doc = {
|
||||
"키": key,
|
||||
"파일": name,
|
||||
"올린때": utc_now_text(),
|
||||
"올린이": who,
|
||||
**({"지문": mark} if mark else {}),
|
||||
**parsed,
|
||||
}
|
||||
if grid is not None:
|
||||
book = {**grid["책"], "열": key}
|
||||
atomic_write_json(_grid_path(folder, key), {**grid, "책": book})
|
||||
atomic_write_json(_path(folder, key), doc)
|
||||
return brief(doc)
|
||||
_put_done(folder, log)
|
||||
return {**brief(doc), **({"지문": mark} if mark else {})}
|
||||
|
||||
|
||||
def grid(folder: str | Path, key: str) -> dict[str, Any]:
|
||||
@@ -110,11 +137,22 @@ def grid(folder: str | Path, key: str) -> dict[str, Any]:
|
||||
return doc
|
||||
|
||||
|
||||
def delete(folder: str | Path, key: str) -> None:
|
||||
def delete(folder: str | Path, key: str, made: list[str] | None = None) -> None:
|
||||
"""HP · 격자 지움 · 만든 것(`made` SC 키)이 있으면 지문을 끝낸 기록에 남김(다시 올림 막음)."""
|
||||
path = _path(folder, key)
|
||||
with _LOCK:
|
||||
if not path.is_file():
|
||||
doc = _load(path)
|
||||
if not isinstance(doc, dict):
|
||||
raise store.StoreError(404, f"없는 실무자료 「{key}」")
|
||||
if made and doc.get("지문"):
|
||||
log = done(folder)
|
||||
log["지문"][doc["지문"]] = {
|
||||
"파일": doc.get("파일"),
|
||||
"키": key,
|
||||
"끝낸때": utc_now_text(),
|
||||
"만든": made,
|
||||
}
|
||||
_put_done(folder, log)
|
||||
path.unlink()
|
||||
_grid_path(folder, key).unlink(missing_ok=True)
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
사전조합 나무 자유 폴더(158-1 · `M02_Precombo_Folders` · 읽기 = 층 읽기 · 고침 = 층 쓰기) +
|
||||
일위대가 실무자료(324 · `M02_Hopyo_*` · 올리기 · 지우기 = 층 쓰기 · 목록 · 읽기 = 층 읽기) ·
|
||||
338 = 확인한 줄만 만들기(몸 없음 400 · 옛 즉시 확정 길 없앰) · 별칭 장부 보기 · 고치기 ·
|
||||
올린 뒤 맞춤 뒤 셈(`M02_Hopyo_Fit.ensure`) · 시동 때 우리 지문 데움(`M02_Hopyo_Ours.warm`).
|
||||
올린 뒤 맞춤 뒤 셈(`M02_Hopyo_Fit.ensure`) · 시동 때 우리 지문 데움(`M02_Hopyo_Ours.warm`) ·
|
||||
718-1 한시 불러오기 = 올림 지문(끝낸 같은 엑셀 409 · 작업 중 같은 엑셀 = 그 파일 `이어서`) ·
|
||||
지우기 = [끝내기](HP · 격자 · 맞춤셈 캐시 지움 · 만든 일위대가가 있으면 지문 기록 · 별칭 장부 남음).
|
||||
층 권한 = `Router_Items._root`(`M02_Layer_Access` 한 벌).
|
||||
⚠ 등록은 `main.py`(로그인만).
|
||||
"""
|
||||
@@ -198,6 +200,16 @@ def _who(session: Session) -> dict[str, Any]:
|
||||
|
||||
|
||||
def _upload(name: str, data: bytes, who: dict[str, Any]) -> dict[str, Any]:
|
||||
mark = hopyo_store.fingerprint(data)
|
||||
if old := hopyo_store.done(store.root())["지문"].get(mark):
|
||||
made = len(old.get("만든") or [])
|
||||
when = str(old.get("끝낸때") or "")[:10]
|
||||
raise store.StoreError(
|
||||
409,
|
||||
f"이미 불러온 엑셀 「{old.get('파일')}」({when} · 일위대가 {made}) — 다시 만들지 않음",
|
||||
)
|
||||
if same := next((r for r in hopyo_store.rows(store.root()) if r.get("지문") == mark), None):
|
||||
return {**same, "이어서": True}
|
||||
try:
|
||||
parsed = hopyo_excel.read_book(name, data)
|
||||
except hopyo_excel.ExcelError as error:
|
||||
@@ -206,7 +218,7 @@ def _upload(name: str, data: bytes, who: dict[str, Any]) -> dict[str, Any]:
|
||||
grid = hopyo_grid.read(name, data, parsed)
|
||||
except Exception: # 격자는 보기용 덤 — 못 만들어도 올리기는 됨(보기 창 404)
|
||||
grid = None
|
||||
row = hopyo_store.save(store.root(), name, parsed, who, grid)
|
||||
row = hopyo_store.save(store.root(), name, parsed, who, grid, mark)
|
||||
if (ensure := _later("M02_Hopyo_Fit", "ensure")) is not None:
|
||||
ensure(store.root(), row["키"]) # 맞춤 뒤 셈(일감 줄 · 화면 안 기다림)
|
||||
return row
|
||||
@@ -441,10 +453,28 @@ def hopyo_ledger_write(
|
||||
def hopyo_delete(
|
||||
layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session)
|
||||
) -> dict:
|
||||
"""실무자료 지우기(만든 사전조합 · 맞춤 기억은 그대로)."""
|
||||
"""실무자료 지우기 = 불러오기 [끝내기](718-1) — HP · 격자 · 맞춤셈 캐시 지움 · 만든 일위대가
|
||||
(이 층 확인된 본뜬 「키 제N호표」)는 그대로 · 있으면 지문 기록(같은 엑셀 다시 올림 막음) ·
|
||||
별칭 장부 그대로 → `{지움, 만든}`."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
_call(folder, hopyo_store.delete, folder, key)
|
||||
return {"지움": key}
|
||||
return _call(folder, _finish, folder, key)
|
||||
|
||||
|
||||
def _finish(folder: Path, key: str) -> dict[str, Any]:
|
||||
head = f"{key} 제"
|
||||
made = sorted(
|
||||
{sc for mark, sc in hopyo_build.confirmed(folder).items() if mark.startswith(head)}
|
||||
)
|
||||
if (cache_path := _later("M02_Hopyo_Fit", "cache_path")) is not None:
|
||||
try:
|
||||
got = cache_path(folder, key)
|
||||
except (OSError, store.StoreError):
|
||||
got = None
|
||||
# ponytail: 뒤 셈이 도는 중이면 끝낸 뒤 캐시를 다시 쓸 수 있음 — tmp 캐시라 둠
|
||||
for old in got.parent.glob(f"{'_'.join(got.name.split('_')[:2])}_*.json") if got else []:
|
||||
old.unlink(missing_ok=True)
|
||||
hopyo_store.delete(folder, key, made)
|
||||
return {"지움": key, "만든": len(made)}
|
||||
|
||||
|
||||
class PasteBody(_Strict):
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Hopyo_Side.ts
|
||||
* 좌측 「일위대가 실무자료」 컨테이너(PLAN 324-4 · 359-1 · 일위대가 사전조합 아래) — 층 = 구조물 조합 층(화면 층) ·
|
||||
* [파일 업로드](구조물 조합 층 단추 꼴 `m02-layers__btn` · 숨은 file 입력 .xls · .xlsx · 쓰기 권한 없으면 꺼짐) +
|
||||
* 파일 줄 → 누르면 바로 엑셀 보기 창(329 · `M02_MasterTemplete_Hopyo_Sheet` 늦게 불러옴 · 지우기 · 검산 · 셈 중은 창 머리) ·
|
||||
* 고름 표시 = 그 파일 창이 열린 동안 · 올린 직후도 엑셀 보기 창 ·
|
||||
* 파일 목록 아래 [별칭 장부](338-3) → 본 칸 장부 화면(`M02_MasterTemplete_Hopyo_Ledger`).
|
||||
* 좌측 「일위대가 불러오기」 컨테이너(PLAN 324-4 · 718-1 한시) — [불러오기](구조물 조합 층 단추 꼴 `m02-layers__btn`) →
|
||||
* 본 칸 마법사(`M02_MasterTemplete_Hopyo_Wizard` · 결과 층 고름 · 작업 중 이어서 · [끝내기] 때 올린 파일 지움) ·
|
||||
* 옛 파일 목록 없음(옛 HP 는 서버 목록에서 숨김) ·
|
||||
* 아래 [별칭 장부](338-3) → 본 칸 장부 화면(`M02_MasterTemplete_Hopyo_Ledger` · 층 = 구조물 조합 층).
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import { listHopyo, pageScope, uploadHopyo, type HopyoFile } from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { pageScope } from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import { buildHopyoLedger, LEDGER_HINT, LEDGER_TITLE } from "./M02_MasterTemplete_Hopyo_Ledger";
|
||||
import { createEmpty } from "@ui/ui_template_empty";
|
||||
|
||||
export const HOPYO_TITLE = "일위대가 실무자료";
|
||||
import { buildHopyoWizard } from "./M02_MasterTemplete_Hopyo_Wizard";
|
||||
|
||||
export interface HopyoSectionOptions {
|
||||
/** 이 층 쓰기 권한 — 없으면 올리기 꺼짐 · 엑셀 보기 창 보기만 */
|
||||
/** 이 층 쓰기 권한 — 별칭 장부 고침 */
|
||||
canWrite: boolean;
|
||||
/** 저장 안 한 고침을 버릴지 확인 — 취소면 false */
|
||||
confirmLeave: () => Promise<boolean>;
|
||||
@@ -30,122 +28,48 @@ export interface HopyoSection {
|
||||
body: HTMLElement;
|
||||
/** 다른 화면이 열렸으면 고름 표시를 지움 */
|
||||
paint: () => void;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
||||
const WIZARD = "불러오기";
|
||||
|
||||
export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection {
|
||||
const title = L("M02_HopyoImport");
|
||||
const scope = pageScope();
|
||||
const list = el("div", { className: "m02-side__items ui-sidebar-list" });
|
||||
const input = el("input", {
|
||||
attrs: { type: "file", accept: ".xls,.xlsx", "data-hopyo-file": "" },
|
||||
}) as HTMLInputElement;
|
||||
input.hidden = true;
|
||||
// 구조물 조합 층 단추(`TreePanel_Roots` [마스터])와 같은 꼴 — 보통 크기 · 같은 클래스
|
||||
const upload = createButton({
|
||||
label: "파일 업로드",
|
||||
const start = createButton({
|
||||
label: "불러오기",
|
||||
variant: "filled",
|
||||
disabled: !opt.canWrite,
|
||||
onClick: () => input.click(),
|
||||
onClick: () => void showView(WIZARD),
|
||||
});
|
||||
upload.classList.add("m02-layers__btn");
|
||||
upload.title = opt.canWrite ? "설계내역서 엑셀(일위대가 시트)" : "이 층 쓰기 권한 없음";
|
||||
start.classList.add("m02-layers__btn");
|
||||
start.title = "다른 프로그램 일위대가 엑셀 → 우리 일위대가(한시 · 끝내면 올린 파일 지움)";
|
||||
start.dataset.hopyoWizardOpen = "";
|
||||
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());
|
||||
ledger.addEventListener("click", () => void showView(LEDGER_TITLE));
|
||||
const body = el("div", {
|
||||
className: "m02-hopyo-side",
|
||||
children: [el("div", { className: "m02-side__row", children: [upload, input] }), list, ledger],
|
||||
children: [el("div", { className: "m02-side__row", children: [start] }), ledger],
|
||||
});
|
||||
let files: HopyoFile[] = [];
|
||||
let open: { key: string; view: HTMLElement } | null = null;
|
||||
let sheetKey: string | null = null; // 엑셀 보기 창이 열린 파일
|
||||
let seq = 0;
|
||||
|
||||
input.addEventListener("change", () => {
|
||||
const file = input.files?.[0];
|
||||
input.value = "";
|
||||
if (!file) return;
|
||||
upload.disabled = true;
|
||||
uploadHopyo(scope, file)
|
||||
.then(async (got) => {
|
||||
const wrong = got.어긋남 ? ` · 검산 어긋남 ${got.어긋남}` : "";
|
||||
showToast(`올림 — 호표 ${got.호표수}${wrong}`, got.어긋남 ? "warning" : "success");
|
||||
await refresh();
|
||||
await showSheet(got);
|
||||
})
|
||||
.catch((error) => showToast(why(error), "error"))
|
||||
.finally(() => (upload.disabled = !opt.canWrite));
|
||||
});
|
||||
|
||||
function paintList(): void {
|
||||
const paintList = (): void => {
|
||||
ledger.classList.toggle("is-active", open?.key === LEDGER_TITLE);
|
||||
list.replaceChildren(
|
||||
...(files.length
|
||||
? files.map((f) => {
|
||||
const row = el("button", {
|
||||
className: `m02-side__item ui-sidebar-row${sheetKey === f.키 ? " is-active" : ""}`,
|
||||
text: `${f.파일} · 호표 ${f.호표수}`,
|
||||
attrs: { type: "button", title: `${f.키} · ${f.올린때}`, "data-hopyo-key": f.키 },
|
||||
});
|
||||
row.addEventListener("click", () => void showSheet(f));
|
||||
return row;
|
||||
})
|
||||
: [createEmpty({ text: "올린 실무자료 없음", small: true })]),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/** 파일 줄 누름 · 올린 직후 = 바로 엑셀 보기 창(본 칸은 그대로 · 열 때 불러옴) */
|
||||
async function showSheet(file: HopyoFile): Promise<void> {
|
||||
const sheet = await import("./M02_MasterTemplete_Hopyo_Sheet").catch((error: unknown) => {
|
||||
showToast(why(error), "error");
|
||||
return null;
|
||||
});
|
||||
if (!sheet) return;
|
||||
sheetKey = file.키;
|
||||
paintList();
|
||||
sheet.openHopyoSheet({
|
||||
scope,
|
||||
key: file.키,
|
||||
파일: file.파일,
|
||||
// 사전조합은 어느 화면 층에서든 로그인한 사람 개인 층에 만듦 — 회사 · 마스터는 구조물 카드 [가져오기]
|
||||
target: { layer: "personal", owner: null },
|
||||
canWrite: true,
|
||||
onClose: () => {
|
||||
if (sheetKey === file.키) sheetKey = null;
|
||||
paintList();
|
||||
},
|
||||
// [삭제]는 실무자료 파일이 있는 화면 층 쓰기 권한 때만
|
||||
onDeleted: opt.canWrite ? () => void refresh() : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
async function showLedger(): Promise<void> {
|
||||
if (open?.key === LEDGER_TITLE && open.view.isConnected) return;
|
||||
async function showView(key: string): Promise<void> {
|
||||
if (open?.key === key && open.view.isConnected) return;
|
||||
if (!(await opt.confirmLeave())) return;
|
||||
const view = buildHopyoLedger({ scope, canWrite: opt.canWrite });
|
||||
opt.show(`${HOPYO_TITLE} — ${LEDGER_TITLE}`, view);
|
||||
const view =
|
||||
key === WIZARD ? buildHopyoWizard() : buildHopyoLedger({ scope, canWrite: opt.canWrite });
|
||||
opt.show(key === WIZARD ? title : `${title} — ${LEDGER_TITLE}`, view);
|
||||
opt.onOpen();
|
||||
open = { key: LEDGER_TITLE, view };
|
||||
open = { key, view };
|
||||
paintList();
|
||||
}
|
||||
|
||||
async function refresh(): Promise<void> {
|
||||
const mine = ++seq;
|
||||
try {
|
||||
const got = await listHopyo(scope);
|
||||
if (mine === seq) files = got;
|
||||
} catch (error) {
|
||||
if (mine === seq) files = [];
|
||||
showToast(why(error), "error");
|
||||
}
|
||||
if (mine === seq) paintList();
|
||||
}
|
||||
|
||||
const paint = (): void => {
|
||||
if (open && !open.view.isConnected) {
|
||||
open = null;
|
||||
@@ -153,6 +77,5 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection {
|
||||
}
|
||||
};
|
||||
|
||||
void refresh();
|
||||
return { title: HOPYO_TITLE, body, paint, refresh };
|
||||
return { title, body, paint };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Hopyo_Wizard.ts
|
||||
* 일위대가 불러오기 = 한시 마법사(PLAN 718-1 · 요구서 내역12 4장 22) — 본 칸 한 화면 · 걸음 셋:
|
||||
* ① 결과 층(쓸 수 있는 층만 · 기본 개인) · 결과(일위대가 · 단가산출 = 715-2 뒤 켬) · 엑셀 고르기 → 올림
|
||||
* (그 층에 둠 · 같은 엑셀 작업 중 = 그 파일 이어서 · 끝낸 같은 엑셀 = 서버 409 안내) · 작업 중 파일 이어서
|
||||
* ② 보기 · 확인 · 만들기 = 엑셀 보기 창(`Hopyo_Sheet` — [모두 고정으로 들여오기] · 호표 [수정] →
|
||||
* 확인 창 만들기 · [구조물 조합으로] 그대로) · 만들 층 = 고른 층
|
||||
* ③ [끝내기] = 물음 → 서버가 이번 HP · 격자 · 맞춤셈 캐시 지움 · 만든 일위대가 · 별칭 장부 남음 · 지문 기록
|
||||
* 작업 중 = 서버에 보관(떠났다 와도 ① 목록에서 이어서) · 옛 HP(지문 없음)는 서버 목록에서 숨김.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements";
|
||||
import { createCheckbox } from "@ui/ui_template_checkbox";
|
||||
import { createEmpty } from "@ui/ui_template_empty";
|
||||
import { createPanelHead } from "@ui/ui_template_panel_head";
|
||||
import { createSegment } from "@ui/ui_template_segment";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
|
||||
import {
|
||||
deleteHopyo,
|
||||
listHopyo,
|
||||
uploadHopyo,
|
||||
type HopyoFile,
|
||||
type HopyoScope,
|
||||
} from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import { accessFor, type ManageLayer } from "./M02_MasterTemplete_Layer";
|
||||
|
||||
const LAYERS: { layer: ManageLayer; text: string }[] = [
|
||||
{ layer: "personal", text: "개인" },
|
||||
{ layer: "company", text: "회사" },
|
||||
{ layer: "system", text: "마스터" },
|
||||
];
|
||||
const STEPS = [
|
||||
{ value: "1", label: "① 층 · 엑셀" },
|
||||
{ value: "2", label: "② 보기 · 확인 · 만들기" },
|
||||
{ value: "3", label: "③ 끝내기" },
|
||||
];
|
||||
/** 단가산출 결과 — M02 단가산출 서고(715-2)가 들어오면 켬 */
|
||||
export const SANGEUN_READY = false;
|
||||
const SANGEUN_WHY = "M02 단가산출 칸(715-2)이 들어온 뒤 켬";
|
||||
|
||||
type Got = HopyoFile & { 이어서?: boolean };
|
||||
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
||||
const textOf = (layer: string): string => LAYERS.find((x) => x.layer === layer)?.text ?? layer;
|
||||
|
||||
/** 쓸 수 있는 자기 층(개인 · 회사 · 마스터 차례) */
|
||||
export async function writableLayers(): Promise<ManageLayer[]> {
|
||||
const user = await fetchSessionUser().catch(() => null);
|
||||
return LAYERS.map((x) => x.layer).filter((layer) => accessFor(layer, null, user).canWrite);
|
||||
}
|
||||
|
||||
export function buildHopyoWizard(): HTMLElement {
|
||||
const steps = createSegment({
|
||||
options: STEPS.map((s) => ({ ...s, disabled: true })),
|
||||
value: "1",
|
||||
});
|
||||
steps.root.dataset.hopyoSteps = "";
|
||||
const body = el("div", { className: "m02-hopyo" });
|
||||
const root = el("div", {
|
||||
className: "m02-hopyo",
|
||||
attrs: { "data-hopyo-wizard": "" },
|
||||
children: [
|
||||
createText(
|
||||
"한시 — 다른 프로그램 일위대가 엑셀을 우리 일위대가로 옮김 · 끝내면 올린 파일 지움 · 별칭 장부는 남음",
|
||||
"hint",
|
||||
),
|
||||
el("div", { className: "ui-toolbar", children: [steps.root] }),
|
||||
body,
|
||||
],
|
||||
});
|
||||
let layers: ManageLayer[] = [];
|
||||
let pick: ManageLayer = "personal";
|
||||
const scopeOf = (layer: ManageLayer): HopyoScope => ({ layer, owner: null });
|
||||
|
||||
async function first(): Promise<void> {
|
||||
steps.set("1");
|
||||
layers = await writableLayers();
|
||||
if (!layers.includes(pick)) pick = layers[0] ?? "personal";
|
||||
const input = el("input", {
|
||||
attrs: { type: "file", accept: ".xls,.xlsx", "data-hopyo-file": "" },
|
||||
}) as HTMLInputElement;
|
||||
input.hidden = true;
|
||||
const upload = createButton({
|
||||
label: "엑셀 고르기",
|
||||
variant: "filled",
|
||||
disabled: !layers.length,
|
||||
onClick: () => input.click(),
|
||||
});
|
||||
upload.title = layers.length ? "설계내역서 엑셀(일위대가 시트)" : "쓸 수 있는 층 없음";
|
||||
input.addEventListener("change", () => {
|
||||
const file = input.files?.[0];
|
||||
input.value = "";
|
||||
if (!file) return;
|
||||
upload.disabled = true;
|
||||
uploadHopyo(scopeOf(pick), file)
|
||||
.then((got: Got) => {
|
||||
if (got.이어서) showToast(`같은 엑셀 작업 중 — ${got.키} 이어서`, "info");
|
||||
else showToast(`올림 — 호표 ${got.호표수}`, got.어긋남 ? "warning" : "success");
|
||||
second(pick, got);
|
||||
})
|
||||
.catch((error) => showToast(why(error), "error"))
|
||||
.finally(() => (upload.disabled = !layers.length));
|
||||
});
|
||||
const seg = createSegment({
|
||||
options: LAYERS.map((x) => ({
|
||||
value: x.layer,
|
||||
label: x.text,
|
||||
disabled: !layers.includes(x.layer),
|
||||
})),
|
||||
value: pick,
|
||||
onChange: (v) => (pick = v as ManageLayer),
|
||||
});
|
||||
seg.root.dataset.hopyoLayer = "";
|
||||
const ilwi = createCheckbox("일위대가(고정 줄)", { checked: true, disabled: true });
|
||||
const sangeun = createCheckbox("단가산출(산근 블록 · 바탕 없음)", { disabled: !SANGEUN_READY });
|
||||
sangeun.root.title = SANGEUN_READY ? "" : SANGEUN_WHY;
|
||||
const busy = el("div", { className: "m02-hopyo" });
|
||||
body.replaceChildren(
|
||||
createText("결과 층", "muted", 14),
|
||||
el("div", { className: "ui-toolbar", children: [seg.root] }),
|
||||
createText("결과", "muted", 14),
|
||||
el("div", { className: "ui-toolbar", children: [ilwi.root, sangeun.root] }),
|
||||
...(SANGEUN_READY ? [] : [createText(`단가산출 — ${SANGEUN_WHY}`, "hint")]),
|
||||
el("div", { className: "ui-toolbar", children: [upload, input] }),
|
||||
createPanelHead({ title: "작업 중", level: 4, divider: true }).root,
|
||||
busy,
|
||||
);
|
||||
const rows = (
|
||||
await Promise.all(
|
||||
layers.map((layer) =>
|
||||
listHopyo(scopeOf(layer))
|
||||
.then((list) => list.map((f) => ({ layer, f })))
|
||||
.catch(() => []),
|
||||
),
|
||||
)
|
||||
).flat();
|
||||
busy.replaceChildren(
|
||||
...(rows.length
|
||||
? rows.map(({ layer, f }) => {
|
||||
const row = createButton({
|
||||
label: `${f.파일} · 호표 ${f.호표수} · ${textOf(layer)} — 이어서`,
|
||||
variant: "ghost",
|
||||
onClick: () => second(layer, f),
|
||||
});
|
||||
row.dataset.hopyoKey = f.키;
|
||||
return row;
|
||||
})
|
||||
: [createEmpty({ text: "작업 중인 불러오기 없음", small: true })]),
|
||||
);
|
||||
}
|
||||
|
||||
function second(layer: ManageLayer, file: HopyoFile): void {
|
||||
steps.set("2");
|
||||
const scope = scopeOf(layer);
|
||||
const view = createButton({
|
||||
label: "엑셀 보기 · 확인 · 만들기",
|
||||
variant: "filled",
|
||||
onClick: () =>
|
||||
void import("./M02_MasterTemplete_Hopyo_Sheet")
|
||||
.then((m) =>
|
||||
m.openHopyoSheet({
|
||||
scope,
|
||||
key: file.키,
|
||||
파일: file.파일,
|
||||
target: scope,
|
||||
canWrite: true,
|
||||
onDeleted: () => void first(),
|
||||
}),
|
||||
)
|
||||
.catch((error: unknown) => showToast(why(error), "error")),
|
||||
});
|
||||
const finish = createButton({ label: "끝내기", variant: "ghost", onClick: () => void end() });
|
||||
finish.dataset.hopyoFinish = "";
|
||||
const back = createButton({ label: "처음으로", variant: "link", onClick: () => void first() });
|
||||
back.title = "작업은 그대로 보관 — 처음 화면 「작업 중」에서 이어서";
|
||||
const wrong = file.어긋남 ? ` · 검산 어긋남 ${file.어긋남}` : "";
|
||||
body.replaceChildren(
|
||||
createText(`${file.파일} · ${textOf(layer)} 층 · 호표 ${file.호표수}${wrong}`, "muted", 14),
|
||||
createText(
|
||||
"엑셀 보기 창에서 [모두 고정으로 들여오기] 또는 호표 [수정] → 확인하고 만들기 · [구조물 조합으로] · 만든 일위대가는 고른 층에 들어감",
|
||||
"hint",
|
||||
),
|
||||
el("div", { className: "ui-toolbar", children: [view, finish, back] }),
|
||||
);
|
||||
|
||||
async function end(): Promise<void> {
|
||||
steps.set("3");
|
||||
const ok = await showConfirmDialog(
|
||||
`「${file.파일}」 불러오기를 끝냄 — 올린 파일(엑셀 결과 · 격자 · 맞춤 캐시)을 지움 · 만든 일위대가와 별칭 장부는 남음 · 일위대가를 만들었으면 같은 엑셀은 다시 올리지 못함`,
|
||||
"끝내기",
|
||||
);
|
||||
if (!ok) return steps.set("2");
|
||||
try {
|
||||
const got = (await deleteHopyo(scope, file.키)) as { 만든?: number };
|
||||
showToast(`끝냄 — 만든 일위대가 ${got.만든 ?? 0} · 올린 파일 지움`, "success");
|
||||
} catch (error) {
|
||||
showToast(why(error), "error");
|
||||
return steps.set("2");
|
||||
}
|
||||
await first();
|
||||
}
|
||||
}
|
||||
|
||||
void first();
|
||||
return root;
|
||||
}
|
||||
@@ -94,8 +94,10 @@ def test_실무_xlsx_넷_붙여넣기_만듦_합계_같음(client, tag):
|
||||
assert got["같음"] == FILES[tag][1] and all(abs(x["차이"]) <= 1 for x in got["다름"])
|
||||
assert len(got["만듦"]) == got["호표"] and got["고정"] + got["호표참조"] == got["줄"]
|
||||
assert master_hash() == before
|
||||
rows = client.get("/api/m02/layers/personal/hopyo").json()["줄"]
|
||||
assert rows[0]["파일"].startswith("붙여넣기") and rows[0]["시트"] == "붙여넣기"
|
||||
# 718-1 — 붙여넣기 실무자료 = 지문 없음 → 불러오기 목록엔 숨김 · 파일은 남음
|
||||
assert client.get("/api/m02/layers/personal/hopyo").json()["줄"] == []
|
||||
doc = client.get(f"/api/m02/layers/personal/hopyo/{got['키']}").json()
|
||||
assert doc["파일"].startswith("붙여넣기") and doc["시트"] == "붙여넣기"
|
||||
|
||||
|
||||
def _block(rows, no):
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""일위대가 불러오기 한시(PLAN 718-1) — 올림 지문 · [끝내기] 지움 · 겹침 막음.
|
||||
|
||||
같은 엑셀 작업 중 다시 올림 = 그 파일 이어서(새 키 없음) · [끝내기] = HP · 격자 · 맞춤셈 캐시 0 ·
|
||||
만든 일위대가 · 별칭 장부 그대로 · 끝낸 같은 엑셀 다시 올림 409 · 만든 것 없이 지움 = 다시 올림 됨 ·
|
||||
옛 HP(지문 없음) = 파일 그대로 목록에서만 숨김 · 키 번 = 지운 뒤에도 다시 안 씀. 층 = 임시 폴더.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Fit as fit
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hs
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
from resources.tester.test_m02_hopyo_excel import NORTH, URL, _post, client # noqa: F401
|
||||
|
||||
ALL = "/api/m02/layers/personal/hopyo-build-all"
|
||||
|
||||
|
||||
def _keys(client):
|
||||
return [r["키"] for r in client.get(URL.format("personal")).json()["줄"]]
|
||||
|
||||
|
||||
def test_이어서_끝내기_지움_겹침_막음(client):
|
||||
folder = layers.personal_dir(7, 42)
|
||||
first = _post(client).json()
|
||||
assert first["키"] == "HP-000001" and len(first["지문"]) == 64
|
||||
again = _post(client).json()
|
||||
assert again["키"] == "HP-000001" and again["이어서"] is True # 작업 중 같은 엑셀 = 새 키 없음
|
||||
assert _keys(client) == ["HP-000001"]
|
||||
made = client.post(ALL, json={"층": "personal", "키": "HP-000001"}).json()
|
||||
assert len(made["만듦"]) == 68
|
||||
hs.remember(folder, {"장부 줄": {"갈래": "안 씀"}}) # 별칭 장부 = 끝내기 뒤 남음
|
||||
cache = fit.cache_path(folder, "HP-000001")
|
||||
cache.write_text("{}", encoding="utf-8")
|
||||
old_cache = cache.with_name("_".join(cache.name.split("_")[:2]) + "_옛판_1.json")
|
||||
old_cache.write_text("{}", encoding="utf-8")
|
||||
sc_before = sorted((folder / "precombo").glob("SC-*.json"))
|
||||
|
||||
res = client.delete(URL.format("personal") + "/HP-000001")
|
||||
assert res.status_code == 200 and res.json() == {"지움": "HP-000001", "만든": 68}
|
||||
assert not list((folder / hs.DIRNAME).glob("HP-*.json")) # 이번 HP 0
|
||||
assert not list((folder / hs.DIRNAME / hs.GRID).glob("HP-*.json"))
|
||||
assert not cache.exists() and not old_cache.exists()
|
||||
assert hs.memory(folder) == {"장부 줄": {"갈래": "안 씀"}}
|
||||
assert sorted((folder / "precombo").glob("SC-*.json")) == sc_before # 만든 일위대가 그대로
|
||||
|
||||
block = _post(client) # 끝낸 같은 엑셀 = 막음 · 안내
|
||||
assert block.status_code == 409 and "이미 불러온 엑셀" in block.json()["detail"]
|
||||
assert "일위대가 68" in block.json()["detail"] and _keys(client) == []
|
||||
assert sorted((folder / "precombo").glob("SC-*.json")) == sc_before
|
||||
|
||||
|
||||
def test_만든_것_없이_지움_다시_올림_키_번_안_되씀(client):
|
||||
assert _post(client).json()["키"] == "HP-000001"
|
||||
assert client.delete(URL.format("personal") + "/HP-000001").json()["만든"] == 0
|
||||
got = _post(client) # 만든 것 없음 = 막지 않음 · 번은 이어서
|
||||
assert got.status_code == 200 and got.json()["키"] == "HP-000002"
|
||||
|
||||
|
||||
def test_옛_HP_목록에서만_숨김(client):
|
||||
folder = layers.personal_dir(7, 42) / hs.DIRNAME
|
||||
folder.mkdir(parents=True)
|
||||
old = folder / "HP-000007.json"
|
||||
old.write_text(json.dumps({"키": "HP-000007", "파일": "옛.xls", "호표": []}), encoding="utf-8")
|
||||
assert _keys(client) == []
|
||||
assert client.get("/api/m02/layers/personal/hopyo-picks").json()["줄"] == []
|
||||
assert _post(client).json()["키"] == "HP-000008" # 옛 번 뒤로
|
||||
assert old.is_file() and _keys(client) == ["HP-000008"]
|
||||
@@ -179,8 +179,10 @@ def test_올리기_목록_읽기_지우기(client):
|
||||
assert (folder / hs.DIRNAME / "HP-000001.json").is_file()
|
||||
assert all(r["종류"] in layers.KINDS for r in layers.list_templates(folder)) # 양식에 안 섞임
|
||||
assert not any("실무자료" in str(p) for _, p in layers.layer_files("precombo"))
|
||||
assert _post(client).json()["키"] == "HP-000002"
|
||||
again = _post(client).json() # 718-1 — 작업 중 같은 엑셀 = 그 파일 이어서
|
||||
assert again["키"] == "HP-000001" and again["이어서"] is True
|
||||
assert client.delete(URL.format("personal") + "/HP-000001").status_code == 200
|
||||
assert _post(client).json()["키"] == "HP-000002" # 지운 번 다시 안 씀
|
||||
assert [r["키"] for r in client.get(URL.format("personal")).json()["줄"]] == ["HP-000002"]
|
||||
assert client.get(URL.format("personal") + "/HP-000001").status_code == 404
|
||||
assert client.get(URL.format("personal") + "/../x").status_code == 404
|
||||
|
||||
@@ -105,8 +105,8 @@ def test_끝까지_확인한_줄만_별칭(client):
|
||||
keys = {r["열쇠"]: r for r in rows}
|
||||
assert {p["로직"] for p in keys[rebar["맞춤"]["열쇠"]]["짝"]} == {"GC000206", "GC000207"}
|
||||
# 다시 올림 = 확인한 줄은 별칭 · 체크 · 만든 것이 보임
|
||||
_up(client, "HP-000002")
|
||||
again = {x["이름"]: x["맞춤"] for x in _one(client, "HP-000002", no)["줄"]}
|
||||
assert _post(client).json()["이어서"] is True # 718-1 — 같은 엑셀 = 그 파일 이어서(새 키 없음)
|
||||
again = {x["이름"]: x["맞춤"] for x in _one(client, "HP-000001", no)["줄"]}
|
||||
assert again["레미콘타설(장비사용)"]["상태"] == "별칭" and again["레미콘타설(장비사용)"]["체크"]
|
||||
assert again["철근가공조립(현장)"]["상태"] == "별칭"
|
||||
assert again["철근가공조립(현장)"]["대상"][0]["출처"] == {
|
||||
@@ -138,7 +138,9 @@ def test_막는_길(client):
|
||||
"/api/m02/layers/personal/hopyo-build",
|
||||
json={"층": "personal", "키": "HP-000001", "번": 1, "줄": []},
|
||||
)
|
||||
assert empty.status_code == 200 and empty.json()["줄수"]["품셈"] == 0 # 빈 몸 = 모두 고정(705-3)
|
||||
assert (
|
||||
empty.status_code == 200 and empty.json()["줄수"]["품셈"] == 0
|
||||
) # 빈 몸 = 모두 고정(705-3)
|
||||
money = client.post(
|
||||
"/api/m02/layers/personal/hopyo-build",
|
||||
json={"층": "personal", "키": "HP-000001", "번": 1, "줄": [{"차례": 1, "금액": 1}]},
|
||||
|
||||
@@ -84,13 +84,15 @@ def test_올리기_격자_200_404_지우기(client):
|
||||
path.unlink() # 옛 파일(324) = 격자 없음
|
||||
old = client.get(GRID.format("personal", "HP-000001"))
|
||||
assert old.status_code == 404 and old.json()["detail"] == "격자 없음 — 다시 올리면 보임"
|
||||
assert _post(client).status_code == 200
|
||||
assert client.delete(URL.format("personal") + "/HP-000001").status_code == 200
|
||||
assert _post(client).status_code == 200 # 718-1 — 같은 엑셀은 지운 뒤 새 번
|
||||
assert client.get(GRID.format("personal", "HP-000002")).status_code == 200
|
||||
assert client.delete(URL.format("personal") + "/HP-000002").status_code == 200
|
||||
assert not (folder / hs.DIRNAME / hs.GRID / "HP-000002.json").exists()
|
||||
assert client.get(GRID.format("personal", "HP-000002")).status_code == 404
|
||||
assert client.get(GRID.format("personal", "../x")).status_code == 404
|
||||
assert client.get(URL.format("personal") + "/HP-000001/3").status_code == 200 # 길 안 부딪힘
|
||||
gone = client.get(URL.format("personal") + "/HP-000001/3") # 길 안 부딪힘(718-1 지운 뒤 = 없음)
|
||||
assert gone.status_code == 404 and "없는 실무자료" in gone.json()["detail"]
|
||||
assert master_hash() == before
|
||||
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ NEW = (
|
||||
"Hopyo_CheckRows",
|
||||
"Hopyo_Choose",
|
||||
"Hopyo_Ledger",
|
||||
"Hopyo_Wizard",
|
||||
)
|
||||
ROW = {
|
||||
"층": "system",
|
||||
@@ -301,7 +302,7 @@ def test_옛_즉시_확정과_바로_만들기_없음():
|
||||
"근거 맞음 모두 확정",
|
||||
'"확정"',
|
||||
'"사전조합 만들기"',
|
||||
"불러오기",
|
||||
'"불러오기" 옛 단추', # 718-1 — 「불러오기」 = 칸 이름 · 마법사 단추라 낱말은 뺌
|
||||
):
|
||||
assert label not in every, label
|
||||
users = sorted(name for name, text in texts.items() if "buildHopyo(" in text)
|
||||
@@ -342,20 +343,18 @@ def test_좌측_판_자리와_권한():
|
||||
assert "canWrite: canEdit(layer, opt.isAdmin)," in side # 구조물 조합 층 쓰기 권한
|
||||
assert "hopyo.paint();" in side.split("function paint(): void {")[1]
|
||||
hopyo_side = _text("M02_MasterTemplete_Hopyo_Side.ts")
|
||||
# 359-1 — [파일 업로드] = 구조물 조합 층 단추 꼴(보통 크기 · m02-layers__btn) · 파일 줄 = 바로 엑셀 보기
|
||||
up = hopyo_side.split("const upload = createButton({")[1].split("upload.title")[0]
|
||||
assert 'label: "파일 업로드"' in up and 'upload.classList.add("m02-layers__btn");' in up
|
||||
assert "size:" not in up and "엑셀 올리기" not in hopyo_side
|
||||
assert "buildHopyoView" not in hopyo_side
|
||||
assert 'row.addEventListener("click", () => void showSheet(f));' in hopyo_side
|
||||
assert (
|
||||
"onClose:" in hopyo_side
|
||||
and "onDeleted: opt.canWrite ? () => void refresh() : undefined," in hopyo_side
|
||||
)
|
||||
assert 'accept: ".xls,.xlsx"' in hopyo_side and "input.hidden = true;" in hopyo_side
|
||||
assert "disabled: !opt.canWrite," in hopyo_side and "pageScope()" in hopyo_side
|
||||
# 718-1 — 제목 = 화면 글 키 · [불러오기] = 구조물 조합 층 단추 꼴 → 본 칸 마법사 · 옛 파일 목록 없음
|
||||
assert 'L("M02_HopyoImport")' in hopyo_side and "일위대가 실무자료" not in hopyo_side
|
||||
start = hopyo_side.split("const start = createButton({")[1].split("start.title")[0]
|
||||
assert 'label: "불러오기"' in start and 'start.classList.add("m02-layers__btn");' in start
|
||||
assert "buildHopyoWizard()" in hopyo_side and "listHopyo" not in hopyo_side
|
||||
assert "uploadHopyo" not in hopyo_side and "openHopyoSheet" not in hopyo_side
|
||||
assert "buildHopyoLedger({ scope, canWrite: opt.canWrite })" in hopyo_side # [별칭 장부]
|
||||
assert hopyo_side.index("list, ledger]") > 0
|
||||
wizard = _text("M02_MasterTemplete_Hopyo_Wizard.ts")
|
||||
assert 'accept: ".xls,.xlsx"' in wizard and "input.hidden = true;" in wizard
|
||||
assert "accessFor(layer, null, user).canWrite" in wizard # 결과 층 = 쓸 수 있는 층만
|
||||
assert 'let pick: ManageLayer = "personal";' in wizard # 기본 개인
|
||||
assert "export const SANGEUN_READY = false;" in wizard # 단가산출 결과 = 715-2 뒤 켬
|
||||
|
||||
|
||||
def test_로직_추가_한_곳과_파일_규칙():
|
||||
@@ -379,9 +378,9 @@ def test_로직_추가_한_곳과_파일_규칙():
|
||||
|
||||
def test_엑셀_보기_창_여는_자리와_늦게_불러옴():
|
||||
lazy = 'import("./M02_MasterTemplete_Hopyo_Sheet")'
|
||||
side = _text("M02_MasterTemplete_Hopyo_Side.ts")
|
||||
assert "await showSheet(got);" in side # 올린 직후 자동
|
||||
assert lazy in side.split("async function showSheet(")[1].split("async function showLedger(")[0]
|
||||
side = _text("M02_MasterTemplete_Hopyo_Wizard.ts")
|
||||
assert "second(pick, got);" in side # 올린 직후 ② 걸음
|
||||
assert lazy in side.split("function second(")[1].split("async function end(")[0]
|
||||
check = _text("M02_MasterTemplete_Hopyo_Check.ts")
|
||||
assert lazy in check and 'label: "엑셀 보기"' in check and "호표: opt.no" in check
|
||||
assert "엑셀에서 보기" not in check
|
||||
@@ -517,6 +516,7 @@ def test_명사형_단추와_별칭_장부_풀이_359():
|
||||
assert f"label: {label}" in ledger, label
|
||||
assert '"우리에 없음": "로직없음"' in ledger and '"안 씀": "사용안함"' in ledger
|
||||
mine = ("Hopyo_Side", "Hopyo_Sheet", "Hopyo_Check", "Hopyo_CheckRows", "Hopyo_Ledger")
|
||||
mine += ("Hopyo_Wizard",)
|
||||
every = "\n".join(_text(f"M02_MasterTemplete_{n}.ts") for n in mine)
|
||||
for gone in (
|
||||
"고친 것 버림",
|
||||
@@ -532,8 +532,8 @@ def test_명사형_단추와_별칭_장부_풀이_359():
|
||||
|
||||
|
||||
def test_372_2_저장은_개인_층에_삭제는_화면_층_권한():
|
||||
side = _text("M02_MasterTemplete_Hopyo_Side.ts")
|
||||
open_ = side.split("sheet.openHopyoSheet({")[1].split("});")[0]
|
||||
assert 'target: { layer: "personal", owner: null },' in open_
|
||||
assert "canWrite: true," in open_ # 만들기 = 개인 층 · 화면 층 무관
|
||||
assert "onDeleted: opt.canWrite ? () => void refresh() : undefined," in open_
|
||||
"""718-1 — 만들 층 = 마법사에서 고른 층(올린 층과 같음) · [삭제] = 끝내기 → 처음 걸음."""
|
||||
side = _text("M02_MasterTemplete_Hopyo_Wizard.ts")
|
||||
open_ = side.split("m.openHopyoSheet({")[1].split("}),")[0]
|
||||
assert "target: scope," in open_ and "canWrite: true," in open_
|
||||
assert "onDeleted: () => void first()," in open_
|
||||
|
||||
@@ -188,4 +188,5 @@ export const ui_locales_m2 = {
|
||||
"그 사이 실무문서가 바뀜 — 목록을 다시 받음",
|
||||
"Practice documents changed — reloaded",
|
||||
],
|
||||
M02_HopyoImport: ["일위대가 불러오기", "Import unit-price sheets"],
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user