feat(일위대가실무): 324-4 화면 — 좌측 「일위대가 실무자료」 판 · 본 화면(호표 표 · 줄 맞춤 · 확정 · 빠진 줄 · 값 비교) · [로직 추가] 「실무 호표」 줄
- 좌측 판(Hopyo_Side): 일위대가 사전조합 아래 · 구조물 조합 층 · 쓰기 권한 없으면 [엑셀 올리기] 꺼짐 · 숨은 file 입력 .xls,.xlsx · 파일 줄 → 본 화면
- 본 화면(Hopyo_View): 파일 · 시트 · 호표 수 · 검산 · 맞춤 n/m · 호표 표 + 찾기 · [지우기] / 호표 줄 표(갈래 · 품명 · 규격 · 수량 · 단위 · 엑셀 단가 · 맞춤 · 대상 · 입력값 · 환산) · [고르기](후보 5 + 다른 로직 · 자재 찾기 → 미리 채움) · [확정] · [빼기] · [되살림] · [같음 모두 확정] · 빠진 줄 · [사전조합 만들기] → 값 비교 · 다시 씀이면 [새로 만들기](확인 뒤 덮음)
- [로직 추가](Hopyo_Pick · Labor_Pick 늦은 덧줄 `more` · 종류 고르기): 문서 층 · 위 층 호표 줄(맞춤 n/m) → 고르면 서버가 문서 층 사전조합을 만들고 참조 줄(별칭 = 호표 품명) · 빠진 줄 알림 · 프로젝트 층 없음
- Labor_Detail: 제자리 넷 줄(가져오기 · pick 별칭 받음 · withHopyo) · 693 줄 그대로
- 서버: GET /layers/{층}/hopyo-picks(이 층 · 위 층 · 이 층 기억)
- 색 = 공용 토큰(밝음 · 어두움) · 시험 test_m02_hopyo_screen 4 + hopyo-picks 1 · typecheck 0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
/* M02 일위대가 실무자료(324-4) 헬퍼 — `M02_MasterTemplete_Hopyo_Pick.ts`(고르기 창 「실무 호표」 덧줄) ·
|
||||
* `M02_MasterTemplete_Hopyo_View.ts` 글 도우미를 트랜스파일해 Node 로 돌림 · fetch 대역이 부른 길 · 몸을 모음 ·
|
||||
* stdin = {rows, built} · stdout = {calls, kinds, rows, picked, project, view} JSON. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api" } };
|
||||
// 본 화면이 부르는 고르기 창 · 자재 창은 DOM · 모듈 메타를 씀 — 대역(글 도우미만 시험)
|
||||
const STUB_FILES = ["M02_MasterTemplete_Labor_Pick", "M02_Material_Pick"];
|
||||
const stub = new Proxy({}, { get: (_t, k) => (k === "currentLanguageIndex" ? () => 0 : () => {}) });
|
||||
const cache = new Map();
|
||||
function loadTs(file) {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const localRequire = (request) => {
|
||||
if (STUBS[request]) return STUBS[request];
|
||||
if (STUB_FILES.some((name) => request.endsWith(name))) return stub;
|
||||
if (request.startsWith(".") && !request.endsWith(".css"))
|
||||
return loadTs(`${path.resolve(path.dirname(full), request)}.ts`);
|
||||
return stub;
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const input = JSON.parse(fs.readFileSync(0, "utf8"));
|
||||
const calls = [];
|
||||
global.fetch = async (url, init = {}) => {
|
||||
calls.push({
|
||||
url,
|
||||
method: init.method ?? "GET",
|
||||
body: init.body ? JSON.parse(init.body) : null,
|
||||
});
|
||||
const body = url.includes("/hopyo-picks") ? { 줄: input.rows } : input.built;
|
||||
return { status: 200, ok: true, json: async () => body };
|
||||
};
|
||||
const M02 = path.join(ROOT, "M02_MasterTemplete");
|
||||
const pick = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Pick.ts"));
|
||||
const picked = [];
|
||||
const lib = {
|
||||
layer: "company",
|
||||
owner: "7",
|
||||
pick: (ref, name, alias) => picked.push({ ref, name, alias }),
|
||||
};
|
||||
const wrapped = pick.withHopyo(lib);
|
||||
const rows = (await wrapped.more()).map(({ onPick: _, ...r }) => r);
|
||||
await pick.pickHopyo(input.rows[0], lib);
|
||||
const project = pick.withHopyo({ ...lib, layer: "project" });
|
||||
const view = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_View.ts"));
|
||||
const out = {
|
||||
calls,
|
||||
kinds: wrapped.kindFilter.map((k) => k.value),
|
||||
rows,
|
||||
picked,
|
||||
project: project.more === undefined && project.kindFilter === undefined,
|
||||
view: {
|
||||
target: [
|
||||
view.targetText({ 로직: "GF000101", 이름: "구조물터파기", 단위: "㎥" }),
|
||||
view.targetText({ 자재: "MT1", 이름: "레미콘", 규격: "25-21-120", 단위: "㎥" }),
|
||||
view.targetText({ 호표: 46, 단위: "㎡" }),
|
||||
],
|
||||
inputs: view.inputsText({ 횟수: "4회사용시", 지역: "서울" }, ["보정작업"]),
|
||||
memory: [
|
||||
view.memoryOf({ 로직: "GF000101" }, { 지역: "서울" }, 1),
|
||||
view.memoryOf({ 자재: "MT1" }, {}, 0.5),
|
||||
view.memoryOf({ 로직: "GF000101" }, {}, 0),
|
||||
view.memoryOf(null, {}, 1),
|
||||
],
|
||||
},
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
})().catch((error) => {
|
||||
process.stderr.write(String(error?.stack ?? error));
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -165,3 +165,23 @@ def test_확정_검사와_위_층_기억(client):
|
||||
assert client.post("/api/m02/layers/personal/hopyo-memory", json=mine).status_code == 200
|
||||
assert _hopyo(client, 3)["구조물터파기(인력100%)"]["대상"]["로직"] == "GF000101" # 같은 층 먼저
|
||||
assert hm.chain(layers.personal_dir(7, 42))[1] == layers.company_dir(7)
|
||||
|
||||
|
||||
def test_로직_추가_고르기_줄은_이_층과_위_층(client):
|
||||
client.who.update(role="SYSTEM_ADMIN")
|
||||
assert _post(client, "system").status_code == 200
|
||||
client.who.update(role="USER")
|
||||
assert _post(client).status_code == 200
|
||||
rows = client.get("/api/m02/layers/personal/hopyo-picks").json()["줄"]
|
||||
assert [r["층"] for r in rows].count("personal") == 68
|
||||
assert [r["층"] for r in rows].count("system") == 68
|
||||
three = next(r for r in rows if r["층"] == "system" and r["번"] == 3)
|
||||
assert {k: three[k] for k in ("주인", "키", "이름", "단위")} == {
|
||||
"주인": None,
|
||||
"키": "HP-000001",
|
||||
"이름": "임도표지석",
|
||||
"단위": "개소",
|
||||
}
|
||||
assert three["맞춤"][1] == 4
|
||||
system = client.get("/api/m02/layers/system/hopyo-picks").json()["줄"]
|
||||
assert {r["층"] for r in system} == {"system"} # 마스터 = 마스터만
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
"""일위대가 실무자료 화면(PLAN 324-4) — 좌측 판 · 본 화면 · [로직 추가] 「실무 호표」 줄.
|
||||
|
||||
- 고르기 창 덧줄(Node · fetch 대역): 이 층 · 위 층 호표 줄(맞춤 n/m) · 종류 고르기 ·
|
||||
고르면 문서 층에 만들기 길 → `lib.pick` 참조 줄(별칭 = 호표 품명) · 프로젝트 층엔 없음 ·
|
||||
본 화면 글 도우미
|
||||
- 소스: 좌측 판 자리(사전조합 아래 · 구조물 조합 층 · 쓰기 권한 없으면 올리기 꺼짐 ·
|
||||
숨은 file 입력) · Labor_Detail 줄 수 그대로 · 새 파일 이름 · 700 줄 · 색 = 공용 토큰
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
M02 = Path(HERE).parents[1] / "M02_MasterTemplete"
|
||||
NEW = ("Hopyo_Api", "Hopyo_Pick", "Hopyo_Side", "Hopyo_View")
|
||||
|
||||
|
||||
def _text(name: str) -> str:
|
||||
return (M02 / name).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _run() -> dict:
|
||||
row = {
|
||||
"층": "system",
|
||||
"주인": None,
|
||||
"키": "HP-000001",
|
||||
"파일": "북부청.xls",
|
||||
"번": 3,
|
||||
"이름": "임도표지석",
|
||||
"규격": "910×600×150",
|
||||
"단위": "개소",
|
||||
"맞춤": [2, 4],
|
||||
}
|
||||
built = {
|
||||
"참조": {"층": "company", "주인": "7", "키": "SC-000009"},
|
||||
"이름": "임도표지석",
|
||||
"다시씀": False,
|
||||
"빠진줄": [{"차례": 0}],
|
||||
"비교": {},
|
||||
}
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_m02_hopyo_pick.cjs")],
|
||||
input=json.dumps({"rows": [row], "built": built}, ensure_ascii=False),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
check=True,
|
||||
)
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_고르기_창_실무_호표_줄과_고름():
|
||||
got = _run()
|
||||
assert got["calls"][0] == {
|
||||
"url": "/api/m02/layers/company/hopyo-picks?owner=7",
|
||||
"method": "GET",
|
||||
"body": None,
|
||||
}
|
||||
assert got["kinds"] == ["logic", "precombo", "hopyo"]
|
||||
assert got["rows"] == [
|
||||
{
|
||||
"kind": "hopyo",
|
||||
"key": "system||HP-000001|3",
|
||||
"tag": "실무 호표",
|
||||
"name": "임도표지석 910×600×150",
|
||||
"sub": "북부청.xls · 제3호표 · 마스터",
|
||||
"unit": "개소 · 맞춤 2/4",
|
||||
}
|
||||
]
|
||||
build = got["calls"][1]
|
||||
assert build["url"] == "/api/m02/layers/company/hopyo-build?owner=7" # 만드는 층 = 문서 층
|
||||
assert build["body"] == {
|
||||
"층": "system",
|
||||
"주인": None,
|
||||
"키": "HP-000001",
|
||||
"번": 3,
|
||||
"새로": False,
|
||||
}
|
||||
assert got["picked"] == [
|
||||
{
|
||||
"ref": {"층": "company", "주인": "7", "키": "SC-000009"},
|
||||
"name": "임도표지석",
|
||||
"alias": "임도표지석",
|
||||
}
|
||||
]
|
||||
assert got["project"] is True # 프로젝트 층 = 덧줄 없음
|
||||
|
||||
|
||||
def test_본_화면_글_도우미():
|
||||
view = _run()["view"]
|
||||
assert view["target"] == ["구조물터파기 [㎥]", "레미콘 25-21-120 [㎥]", "제46호표 [㎡]"]
|
||||
assert view["inputs"] == "횟수=4회사용시, 지역=서울 · ⚠ 못 읽음: 보정작업"
|
||||
assert view["memory"] == [
|
||||
{"로직": "GF000101", "입력값": {"지역": "서울"}, "환산": 1},
|
||||
{"자재": "MT1", "입력값": {}, "환산": 0.5},
|
||||
None, # 환산 0 = 확정 못 함
|
||||
None, # 대상 없음
|
||||
]
|
||||
|
||||
|
||||
def test_좌측_판_자리와_권한():
|
||||
side = _text("M02_MasterTemplete_UI_Side.ts")
|
||||
assert 'import { buildHopyoSection } from "./M02_MasterTemplete_Hopyo_Side";' in side
|
||||
host = side.split('className: "m02-side__lists ui-sidebar-panel"')[1]
|
||||
assert host.index("precomboSec,") < host.index("section(hopyo.title, hopyo.body),")
|
||||
assert host.index("section(hopyo.title, hopyo.body),") < host.index("extras ?")
|
||||
assert "canWrite: canEdit(layer, opt.isAdmin)," in side # 구조물 조합 층 쓰기 권한
|
||||
assert "hopyo.paint();" in side.split("function paint(): void {")[1]
|
||||
names = set(re.findall(r"\b\w*[Pp]ractice\w*\b", side))
|
||||
assert names <= {"buildPracticeSection", "practice", "M02_MasterTemplete_Practice"} # 옛 이름만
|
||||
assert "LaborSection" not in side and "labor?." not in side and "picked" not in side
|
||||
hopyo_side = _text("M02_MasterTemplete_Hopyo_Side.ts")
|
||||
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
|
||||
|
||||
|
||||
def test_로직_추가_한_곳과_파일_규칙():
|
||||
detail = _text("M02_MasterTemplete_Labor_Detail.ts")
|
||||
assert len(detail.splitlines()) == 693 # 줄 수 그대로(제자리 고침)
|
||||
assert "withHopyo(library)," in detail
|
||||
assert 'cur.담은로직.push({ 로직: "", 참조: ref, 별칭, 메모: null' in detail
|
||||
pick = _text("M02_MasterTemplete_Labor_Pick.ts")
|
||||
assert "more?: () => Promise<PickExtra[]>;" in pick and "...later" in pick
|
||||
for name in NEW:
|
||||
text = _text(f"M02_MasterTemplete_{name}.ts")
|
||||
assert len(text.splitlines()) <= 700, name
|
||||
css = _text("M02_MasterTemplete_Hopyo.css")
|
||||
assert "var(--color-" in css and not re.search(r"#[0-9a-fA-F]{3,6}\b", css)
|
||||
assert '"./M02_MasterTemplete_Hopyo.css"' in _text("M02_MasterTemplete_Hopyo_View.ts")
|
||||
Reference in New Issue
Block a user