refactor(785-7): 옛 맞춤 화면 TS 지움 · 옛 [저장] · [모두 고정] 단추 걷음 · 390 줄 판 높이

- 지움(git rm) 15 파일 3,318 줄: Hopyo_Check · CheckRows · Choose · WizardSk · SkApi · SkLines · logic/SkPick · logic/Draft ·
  Left · Lost · Pick · SheetMake · Kids · ToItem · Compare(모두 옛 확인 창 · 마법사 단가산출 · 안 이은 줄 길 몫 · 부르는 곳 0)
- 단추 판단: 엑셀 보기 [저장](체크 호표) · [모두 고정으로 들여오기] 화면 단추 걷음 → 만들기 = [옮기기] 하나 ·
  모두 고정 서버 길(Builder · build_all)은 B09 · 붙여넣기가 써서 남김 · 목록 체크 칸 · 모달 껍데기(openHopyoSheet · raise · lower) 걷음
- Import: 「안 이은 줄」 띠 · [안 이은 줄 고르기] · 남은 줄 셈 걷음 · [끝내기] = 물음(확인하고 안 옮긴 수 = 판정 고름 캐시 moveBody)
- Api: 옛 맞춤 타입 · 길(readHopyoOne · guessHopyo · buildHopyo · buildHopyoAll · leftHopyo · listHopyoPicks · hopyoSiblings ·
  buildHopyoItem …) 걷음 · POLL_MS · HOPYO_BUILT 옮겨 옴 · 목록 꼴 = 1차 판정 줄상태(Judge.view)
- Labor_Pick: withHopyo(실무 호표 덧줄) → withSangeun(찾아 넣기 창 표시만) · Detail · Table 부름 바꿈
- Ledger: unitField 옮겨 옴 · SheetNav: 옛 확인 캐시 칠 걷음 · UI_Page: [구조물 조합으로] 듣기 걷음
- CSS: 옛 확인 창 · 고르기 창 규칙 34 걷음(481 → 287 줄) · 390 폭 = 줄 판 열리면 위 칸 넓힘 · 줄 판 높이 바닥 16rem

시험 정리(까닭 = 지운 화면 파일만 읽음):
- 지움: test_729_3_hopyo_left_screen · test_746_2_hopyo_compare_screen · test_778_5_sk_pick_screen · helper_729_3 · helper_m02_hopyo_check · pick · seam
- test_m02_hopyo_screen = 좌측 판 · 끝내기 둘만 · 627 · helper_778_5 = SheetSk 판 바꿈만 · 785_4 · 721 · 717_4 · 720 · 739_1 = 지금 꼴로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 01:32:17 +09:00
co-authored by Claude Opus 5.5
parent 48848bb287
commit cf4e2a0339
45 changed files with 126 additions and 5791 deletions
@@ -1,94 +0,0 @@
/* 729-3 안 이은 줄 화면 헬퍼 — `Hopyo_Left` · `Hopyo_Check` · `Api_Fetch` · `Labor_Unlinked` 도우미를
* 트랜스파일해 Node 로 돌림 · fetch 대역(부른 길 · 몸 모음 · 409 몸) · sessionStorage 대역 ·
* stdin = {one, made, left, fixes} · stdout = JSON. */
const fs = require("fs");
const path = require("path");
const { makeLoader, memoryStorage } = require("./helper_ts_stub_loader.cjs");
const ROOT = path.join(__dirname, "..", "..");
const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api", ROUTES: {} } };
const STUB_FILES = [
"M02_MasterTemplete_Labor_Pick",
"M02_Material_Pick",
"M02_MasterTemplete_Find_Pick",
"M02_MasterTemplete_Labor_Api",
"M02_MasterTemplete_Labor_Input",
"M01_MasterData_UI_LogicLab_Source",
"M01_MasterData_UI_Logic_Api",
"M02_MasterTemplete_Hopyo_CheckRows",
"M02_MasterTemplete_Find_Pick",
];
const stub = new Proxy({}, { get: (_t, k) => (k === "currentLanguageIndex" ? () => 0 : () => {}) });
const { loadTs } = makeLoader({
root: ROOT,
requireFor: (full) => (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;
},
});
global.sessionStorage = memoryStorage();
global.document = { dispatchEvent: () => true }; // 캐시 고침 알림(779-4) 대역
(async () => {
const input = JSON.parse(fs.readFileSync(0, "utf8"));
const calls = [];
global.fetch = async (url, init = {}) => {
const body = init.body ? JSON.parse(init.body) : null;
calls.push({ url, method: init.method ?? "GET", body });
if (url.includes("/hopyo-build"))
return { status: 409, ok: false, json: async () => ({ detail: { 까닭: "남음", 남은줄: input.left } }) }; // prettier-ignore
if (url.includes("/hopyo-unlinked")) {
const done = new Set((body.줄 ?? []).map((x) => x.차례));
const left = input.left.filter((x) => !done.has(x.차례));
return { status: 200, ok: true, json: async () => ({ 남은줄: left, 수: left.length }) };
}
const no = Number(url.split("?")[0].split("/").pop());
const one = { ...input.one, 호표: { ...input.one.호표, 번: no, 만든: input.made.includes(no) ? { 키: "SC-1", 확인: true } : null } }; // prettier-ignore
return { status: 200, ok: true, json: async () => one };
};
const M02 = path.join(ROOT, "M02_MasterTemplete");
const check = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Check.ts"));
const left = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Left.ts"));
const api = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Api.ts"));
const grid = loadTs(path.join(M02, "M02_MasterTemplete_Labor_Unlinked.ts"));
const scope = { layer: "personal", owner: null };
const at = { scope, target: scope, key: "HP-000001" };
// 만들기 409 → 남은 줄 · 까닭 글
let stale = null;
try {
await api.buildHopyo(scope, { 층: "personal", 주인: null, 키: "HP-000001", 번: 2 }, []);
} catch (error) {
stale = { message: error.message, left: api.leftOf(error), name: error.constructor.name };
}
const before = await left.countLeft(at, [1, 2]);
// 줄마다 고름 — 노임 · 기계 · 비율 · 우리에 없음 · 로직
const [a, b, c, d, e] = input.left;
left.put(at, 2, a.차례, [{ 노임: "LB000002", 이름: "보통인부", 입력값: {}, 배: 1 }]);
left.put(at, 2, b.차례, [{ 기계: "EQ000069", 이름: "덤프트럭", 입력값: {}, 배: 1 }]);
left.put(at, 2, c.차례, [{ 비율: "노무비", 이름: "노무비", 입력값: {}, 배: 1 }]);
left.put(at, 2, d.차례, null);
const half = await left.countLeft(at, [1, 2]);
left.put(at, 2, e.차례, [{ 로직: "GF000113", 이름: "되메우기", 입력값: {}, 배: 1 }]);
const after = await left.countLeft(at, [1, 2]);
const body = left.draftLines(at, after.get(2).one);
const count = (m) => left.leftCount(m);
process.stdout.write(
JSON.stringify({
stale,
before: count(before),
half: count(half),
after: count(after),
body,
unlinkedCalls: calls.filter((x) => x.url.includes("/hopyo-unlinked")).map((x) => x.url),
lastBody: calls.filter((x) => x.url.includes("/hopyo-unlinked")).pop().body,
fixes: grid.unlinkedFixes({ 담은로직: input.fixes }).map((x) => x.이름),
}),
);
})().catch((error) => {
process.stderr.write(String(error?.stack ?? error));
process.exit(1);
});
+4 -97
View File
@@ -1,12 +1,10 @@
/* 778-5 D-5 마법사 단가산출 줄 고름 헬퍼 — `logic/Hopyo_SkPick`(순수) · `Hopyo_SkApi`(몸) 를 트랜스파일해 Node 로 돌림 ·
* fetch 대역(부른 길 · 몸 모음) · stdin = {blocks} · stdout = JSON.
* 780-2 — stdin `{sheetSk: {...}}` = 엑셀 보기 오른쪽 판 바꾸기(`Hopyo_SheetSk`) · 단가산출 칸 · 시트 막대 · 판은 가짜. */
/* 780-2 · 785-6 엑셀 보기 오른쪽 판 바꾸기 헬퍼 — stdin `{sheetSk: {...}}` = `Hopyo_SheetSk` 를 트랜스파일해 Node 로 돌림 ·
* 단가산출 칸 · 시트 막대 · 판은 가짜 · stdout = JSON. 옛 마법사 줄 고름(`SkPick` · `SkApi`)은 785-7 에서 지움. */
const fs = require("fs");
const path = require("path");
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
const ROOT = path.join(__dirname, "..", "..");
const made = []; // 가짜 단가산출 칸 — 지은 인자 · 강조
const fakeEl = () => ({
hidden: false,
attrs: {},
@@ -64,7 +62,7 @@ const STUBS = {
},
},
"./M02_MasterTemplete_Hopyo_Block": { createJudgeBlock: (o) => ({ ...o, destroy() {} }) },
"./M02_MasterTemplete_Hopyo_Check": { POLL_MS: 3000 },
"./M02_MasterTemplete_Hopyo_Api": { POLL_MS: 3000 },
"./M02_MasterTemplete_Hopyo_Rows": { createJudgeMove: () => ({ root: fakeEl(), destroy() {} }) },
"@config/config_frontend": { API_BASE_URL: "/api", ROUTES: {} },
"@ui/ui_template_elements": {
@@ -78,17 +76,6 @@ const STUBS = {
},
}),
},
"./M02_MasterTemplete_Hopyo_WizardSk": {
sangeunStep: (scope, key, file, onChange, o) => {
const step = {
args: { scope, key, file, o: { ...o, onRow: !!o.onRow } },
marks: [],
onRow: o.onRow,
};
made.push(step);
return { root: fakeEl(), mark: (n) => step.marks.push(n) };
},
},
};
const stub = new Proxy({}, { get: () => () => {} });
const { loadTs } = makeLoader({
@@ -104,87 +91,7 @@ global.sessionStorage = { getItem: () => null, setItem: () => {}, removeItem: ()
(async () => {
const input = JSON.parse(fs.readFileSync(0, "utf8"));
if (input.sheetSk) return process.stdout.write(JSON.stringify(await sheetSk(input.sheetSk)));
const { blocks } = input;
const calls = [];
global.fetch = async (url, init = {}) => {
calls.push({
url,
method: init.method ?? "GET",
body: init.body ? JSON.parse(init.body) : null,
});
return { status: 200, ok: true, json: async () => ({ 판정: blocks, 만듦: [], 못만듦: [] }) };
};
const M02 = path.join(ROOT, "M02_MasterTemplete");
const sk = loadTs(path.join(M02, "logic", "M02_MasterTemplete_Hopyo_SkPick.ts"));
const api = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_SkApi.ts"));
const [a, b, c] = blocks; // a = 고를 줄 2 · b = 옮길 수 있음 · c = 못 이음(하위)
const out = {};
let d = sk.emptyDraft();
out.start = blocks.map((x) => ({
번: x.번,
판정: sk.verdictOf(x, {}),
확인켜짐: sk.canConfirm(x, {}),
}));
out.emptyBody = sk.sureBody(d, blocks); // 아무것도 안 눌렀으면 몸 비움(자동 확인 없음)
d = sk.setSure(d, a, true); // 고를 줄 남은 블록 = 안 켜짐
out.sureBlocked = d.확인;
d = sk.setPick(d, a.번, 1, { 노임: "LB000099" }); // 작업반장 → 다른 노임
out.half = {
판정: sk.verdictOf(a, sk.picksOf(d, a.번)),
켜짐: sk.canConfirm(a, sk.picksOf(d, a.번)),
};
d = sk.setPick(d, a.번, 2, { 자재: "MT000123" });
d = sk.setPick(d, a.번, 3, { 뺌: "우리에 없음" });
out.view = a.줄.map((l) => sk.lineView(l, sk.picksOf(d, a.번)[String(l.차례)]));
out.full = {
판정: sk.verdictOf(a, sk.picksOf(d, a.번)),
켜짐: sk.canConfirm(a, sk.picksOf(d, a.번)),
};
d = sk.setSure(d, a, true);
d = sk.setSure(d, b, true);
d = sk.setSure(d, c, true); // 못 이음 남음 = 안 켜짐
out.sure = d.확인;
out.body = sk.sureBody(d, blocks);
out.wait = sk.waiting(d, blocks);
// 고름 지우면 확인에서 빠짐(tidy)
out.tidy = sk.tidy(sk.setPick(d, a.번, 2, null), blocks).확인;
// 캐시 글 왕복 · 틀린 글
out.round = sk.readDraft(JSON.stringify(d));
out.bad = sk.readDraft("{엉터리");
out.cand = {
L: sk.candPick("L", { 키: "LB000001", 이름: "작업반장" }),
X: sk.candPick("X", { 키: "EQ000019", 이름: "굴착기" }),
M: sk.candPick("M", { 자재: "MT000001", 이름: "시멘트" }),
R: sk.candPick("R", { 기준: "노무비", 비목: "경비" }),
Mbad: sk.candPick("M", { 키: "LB000001" }),
text: sk.candText({ 이름: "합판", 규격: "12mm", 단위: "장" }),
};
await api.judgeSangeun({ layer: "personal", owner: null }, "HP-000001");
await api.judgeSangeun({ layer: "personal", owner: null }, "HP-000001", out.body);
out.calls = calls;
// 780-2 일위대가 안 · 밖 · 줄 0 블록 — 거르기 · 판정 · 확인 막음 · 끝내기 글 재료
const more = [
{ ...a, 부름: [3, 7] },
{ ...b, 부름: [] },
{ ...b, 번: 9, 부름: [], 줄: [] },
{ ...c, 부름: [12], 만든: "SK-000001" },
];
const e = more[2];
let d2 = sk.setSure(sk.emptyDraft(), e, true);
out.empty = {
판정: sk.verdictOf(e, {}),
켜짐: sk.canConfirm(e, {}),
확인: d2.확인,
몸: sk.sureBody({ 고름: {}, 확인: [9] }, more),
};
d2 = sk.emptyDraft();
out.filter = Object.fromEntries(
["all", "in", "out", "open"].map((f) => [f, sk.filterBlocks(more, d2, f).map((x) => x.번)]),
);
out.old = sk.filterBlocks(blocks, d2, "out").length; // 옛 서버(부름 없음) = 모두 안
out.wait2 = sk.waiting(d2, more);
process.stdout.write(JSON.stringify(out));
process.stdout.write(JSON.stringify(await sheetSk(input.sheetSk)));
})().catch((error) => {
process.stderr.write(String(error?.stack ?? error));
process.exit(1);
-315
View File
@@ -1,315 +0,0 @@
/* M02 일위대가 실무자료 확인 창(338-3) 헬퍼 — `Hopyo_Check` · `Hopyo_Choose` · `Hopyo_Ledger` · `Hopyo_Sheet`
* 도우미와 `Hopyo_Api` 길을 트랜스파일해 Node 로 돌림 · fetch 대역이 부른 길 · 몸을 모음 · sessionStorage 대역 ·
* stdin = {lines, rows, ledger} · stdout = {check, choose, ledger, sheet, calls, pick} JSON. */
const fs = require("fs");
const path = require("path");
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
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",
"M02_MasterTemplete_Find_Pick",
"M02_MasterTemplete_Labor_Api",
"M02_MasterTemplete_Labor_Input",
"M01_MasterData_UI_LogicLab_Source",
"M02_MasterTemplete_Hopyo_Left", // 남은 줄 창(729-3 · DOM · 찾기 창)
"M02_MasterTemplete_Hopyo_CheckRows", // 표 그리기(DOM · Labor_Rows) — 359-3
];
const opened = [];
const stub = new Proxy(
{},
{
get: (_t, k) =>
k === "currentLanguageIndex"
? () => 0
: k === "openModal"
? (o) => opened.push(o.title)
: () => {},
},
);
const { loadTs } = makeLoader({
root: ROOT,
requireFor: (full) => (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;
},
});
const store = new Map();
global.sessionStorage = {
getItem: (k) => (store.has(k) ? store.get(k) : null),
setItem: (k, v) => store.set(k, String(v)),
removeItem: (k) => store.delete(k),
};
global.document = { dispatchEvent: () => true }; // 캐시 고침 알림(779-4) 대역
let out0 = null;
(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") || url.includes("/hopyo-ledger") ? { 줄: [] } : {};
return { status: 200, ok: true, json: async () => body };
};
const M02 = path.join(ROOT, "M02_MasterTemplete");
const check = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Check.ts"));
const choose = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Choose.ts"));
const ledger = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Ledger.ts"));
const sheet = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Sheet.ts"));
const api = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Api.ts"));
const pick = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Pick.ts"));
const draft = loadTs(path.join(M02, "logic", "M02_MasterTemplete_Hopyo_Draft.ts"));
if (input.drafts) {
// 359-3 — 실물 줄 → 미리 셈 문서 · 표(서버 calc 에 그대로 보냄)
const d = check.loadDraft("drafts");
out0 = input.drafts.map((l) => {
const parts = check.partsOf(l, d);
return { 차례: l.차례, table: draft.tableOf(l, parts), doc: draft.draftDoc(l, parts) };
});
process.stdout.write(JSON.stringify(out0));
return;
}
if (input.real) {
// 실물 맞춤 응답(338-4) — 서버 줄을 그대로 먹여 체크 · 몸 · 빠질 줄 · 품 비교 · 글
const lines = input.real;
const d = check.loadDraft("real");
out0 = {
body: check.bodyOf(lines, d),
leftOut: check.leftOut(lines, d),
sure: check.sureLines(lines, d).map((l) => l.차례),
lines: lines.map((l) => {
const p = check.partsOf(l, d)[0];
return {
이름: l.이름,
상태: l.맞춤.상태,
checkable: check.checkable(l, d),
checked: check.isChecked(l, d),
compare: check.compareText(l.맞춤.품),
qty: choose.qtyText(l.맞춤.품?.엑셀),
target: choose.targetText(p),
inputs: p ? choose.inputsText(p.입력값 ?? {}, p.못읽음 ?? [], p.출처 ?? {}) : "",
cands: (l.맞춤.후보 ?? []).map((c) => [
choose.targetText(c),
c.판정 ?? "",
choose.basisTag(c.근거),
]),
};
}),
};
process.stdout.write(JSON.stringify(out0));
return;
}
const { lines } = input;
const scope = { layer: "personal", owner: null };
const key = check.draftKey(scope, "HP-000001", 3);
// 서버 제안 그대로 → 사용자가 만짐(체크 끔 · 뺌 · 고른 대상) → 캐시 다시 읽음
const fresh = check.loadDraft(key);
const suggested = check.bodyOf(lines, fresh);
const d = check.loadDraft(key);
d.체크[1] = false; // 맞음 줄 체크 끔
d.뺌[3] = "우리에 없음";
d.체크[3] = true;
d.대상[4] = [{ 로직: "GF000211", 이름: "고임돌", 입력값: { 유형: "보통" }, 배: 0.1, 몫: 0.1 }];
d.체크[4] = true;
check.saveDraft(key, d);
const again = check.loadDraft(key);
const ask = check.newest();
const a = ask(1);
const b = ask(1);
const c = ask(2);
const out = {
check: {
key,
suggested,
edited: check.bodyOf(lines, again),
leftOut: check.leftOut(lines, again),
checkable: lines.map((l) => check.checkable(l, fresh)),
sure: check.sureLines(lines, fresh).map((l) => l.차례),
compare: check.compareText(lines[1].맞춤.품),
compareMany: check.compareText(lines[3].맞춤.품),
toTarget: [
check.toTarget({ 호표: 46, 이름: "고화매트", 입력값: {}, 배: 2.3 }),
check.toTarget({ 자재: "MT1", 이름: "레미콘", 입력값: {}, 배: 1, 몫: null }),
check.toTarget({ 전국: true, 이름: "레미콘(전국)-관급", 입력값: {}, 배: 1, 몫: null }),
],
newest: [a(), b(), c()],
dropped: (check.dropDraft(key), store.has(key)),
},
choose: {
hint: [
choose.searchHint({
이름: "레미콘타설(장비사용)",
근거: { 출처: [["건설품셈", "6-1-1"]], 직종: [], 행: [1, 1] },
}),
choose.searchHint({ 이름: "(소)고임돌 채집 및 운반" }),
],
picked: choose.pickedTargets([
{ c: { 로직: "GC000206" }, 몫: null },
{ c: { 로직: "GF000211" }, 몫: 0.1 },
]),
inputs: choose.inputsText({ 소량감률: 50, 지역: "서울" }, ["측압"], {
소량감률: "블록",
지역: "자동값",
}),
word: [choose.builtWord({ 다시씀: false }), choose.builtWord({ 다시씀: true })],
qty: choose.qtyText(lines[1].맞춤.품.엑셀),
target: choose.targetText({ 호표: 46, 단위: "㎡" }),
// 358-1 후보 묶음 — 1위 / 출처 같음 / 이름 닮은 로직 / 숫자만 닮음(약한 후보 끝) · 같은 이름 책
groups: (() => {
const m = {
후보: [
{ 로직: "A", 이름: "되메우기", 판정: "맞음", 뜻겹침: true, 책: "산림품셈" },
{ 로직: "B", 이름: "야면석 채집", 판정: "후보", 뜻겹침: false },
{
로직: "C",
이름: "되메우기",
판정: "후보",
뜻겹침: true,
출처같음: true,
책: "건설품셈",
},
{ 로직: "D", 이름: "표토펴기", 판정: "다른 점", 뜻겹침: false },
],
이름후보: [
{ 로직: "A", 이름: "되메우기" },
{ 로직: "E", 이름: "되메우기(노상재)" },
],
};
const g = choose.candGroups(m);
const twins = choose.sameNames(g);
return {
groups: g.map((x) => [x.제목, x.접힘, x.후보.map((c) => c.로직)]),
text: g.flatMap((x) => x.후보).map((c) => choose.candText(c, twins)),
material: choose
.candGroups({ 후보: [{ 자재: "M1" }, { 자재: "M2" }] }, true)
.map((x) => x.제목),
// 361-1 지역만 다른 자재 — 1위 전국 · 지역 변형 접힘 · 고르면 그 지역 · 전국 되돌림
national: (() => {
const all = [
{ 전국: true, 이름: "레미콘(전국)-관급", 규격: "25-21-120", 단위: "㎥" },
{ 자재: "M1", 이름: "레미콘(서울)-사급", 변형: true },
{ 자재: "M2", 이름: "레미콘(관급)-강원도(강릉시)", 변형: true },
];
const g = choose.candGroups({ 후보: all }, true);
const id = (c) => c.자재 ?? (c.전국 ? "전국" : "");
return {
groups: g.map((x) => [x.제목, x.접힘, x.후보.map(id)]),
text: choose.candText(all[0], new Set()),
region: choose.pickedTargets([{ c: all[2], 몫: null }]),
back: choose.pickedTargets([{ c: all[0], 몫: null }]),
hit: choose
.candGroups({ 후보: [all[2], all[0], all[1]] }, true)
.map((x) => [x.제목, x.후보.map(id)]),
};
})(),
pick: [
choose.canPickMaterial({ 갈래: "단가산출", 단가: { 재료비: 10 } }),
choose.canPickMaterial({ 갈래: "단가산출", 단가: { 재료비: null } }),
choose.canPickMaterial({ 갈래: "별도계상", 단가: {} }),
],
};
})(),
},
ledger: (() => {
const rows = input.ledger;
const edits = {
"가|b|m3": null, // 지움
"나||m3": { 원래: { 품명: "나", 규격: "", 단위: "m3" }, 갈래: "안 씀", 짝: [] },
"다||개": ledger.entryOf(rows[2]), // 위 층 → 내 층
};
const merged = ledger.mergeRows(rows, edits, { layer: "personal", owner: "7/42" });
return {
key: ledger.ledgerKey({ layer: "company", owner: "7" }),
merged: merged.map((r) => [r.열쇠, r.층, r.고침, !!r.고친, r.갈래]),
filter: ledger.filterRows(merged, "MT1", "").map((r) => r.열쇠),
kind: ledger.filterRows(merged, "", "안 씀").map((r) => r.열쇠),
pair: ledger.pairText({ 로직: "GC000198", 입력값: { 소량감률: 50 }, 배: 1, 몫: 0.1 }),
entry: ledger.entryOf({ ...rows[0], 갈래: "우리에 없음" }),
};
})(),
draft: (() => {
// 359-3 — 미리 셈 문서(우리 수량 = 엑셀 × 배 · 별도계상 · 같은 자재 더함 · 같은 로직 둘째 · 호표 · 배 없음 뺌)
const L = { 로직: "GC000198", 입력값: { 소량감률: 50 }, 배: 2 };
const M = { 자재: "MT1", 입력값: {}, 배: 1 };
return {
logic: draft.draftDoc({ 수량: 0.5, 갈래: "단가산출" }, [
L,
{ ...L, 배: 3 },
{ 호표: 46, 입력값: {}, 배: 1 },
{ 로직: "GF000101", 입력값: {}, 배: null },
]),
mat: draft.draftDoc({ 수량: 2, 갈래: "별도계상" }, [M, { ...M, 배: 0.5 }]),
direct: draft.draftDoc({ 수량: null, 갈래: "자재" }, [M]).자재[0],
none: [
draft.draftDoc({ 수량: 1, 갈래: "단가산출" }, []),
draft.draftDoc({ 수량: 1, 갈래: "호표" }, [{ 호표: 3, 입력값: {}, 배: 1 }]),
draft.draftDoc({ 수량: 1, 갈래: "별도계상" }, [{ 전국: true, 입력값: {}, 배: 1 }]),
],
table: [
draft.tableOf({ 갈래: "단가산출" }, [M]),
draft.tableOf({ 갈래: "자재" }, [L]),
draft.tableOf({ 갈래: "자재" }, [L, M]),
draft.tableOf({ 갈래: "별도계상" }, []),
draft.tableOf({ 갈래: "%" }, []),
draft.tableOf({ 갈래: "별도계상" }, [{ 전국: true, 입력값: {}, 배: 1 }]),
],
};
})(),
sheet: {
order: sheet.buildOrder([
{ 번: 45, 아래: [46] },
{ 번: 46, 아래: [] },
{ 번: 3, 아래: [] },
{ 번: 7, 아래: [8] },
{ 번: 8, 아래: [7] },
]),
linked: [
{ 맞춤: [2, 2] },
{ 맞춤: [1, 2] },
{ 맞춤: [0, 0] },
{ 맞춤: [3, 2] },
{ 맞춤: [1, 4], 연결할: 3, 연결된: 3 }, // 358-3 두 칸이 앞섬(% · 노임 줄 밖)
{ 맞춤: [3, 3], 연결할: 3, 연결된: 2 },
{ 맞춤: [0, 1], 연결할: 0, 연결된: 0 },
].map(sheet.allLinked),
},
};
// 길 — 만들기(줄 · 통째) · 호표 하나(만들 층) · 미리 셈 · 장부
const from = { 층: "system", 주인: null, 키: "HP-000001", 번: 3 };
const target = { layer: "company", owner: "7" };
await api.buildHopyo(target, from, suggested);
await api.buildHopyo(target, from, [], { 대상: [{ 로직: "GF000225", 배: 1 }] });
await api.readHopyoOne({ layer: "system", owner: null }, "HP-000001", 3, target);
await api.guessHopyo(scope, "HP-000001", 3, 4, [
{ 로직: "GF000211", 입력값: {}, 배: 0.1, 몫: 0.1 },
]);
await api.readLedger(target);
await api.saveLedger(target, { "가|b|m3": null });
out.calls = calls.splice(0);
// [로직 추가] 실무 호표 — 확인된 것 = 바로 참조 · 아닌 것 = 확인 창(만들기 길 안 부름)
const picked = [];
const lib = {
layer: "company",
owner: "7",
pick: (ref, name, alias) => picked.push({ ref, name, alias }),
};
for (const row of input.rows) await pick.pickHopyo(row, lib);
await new Promise((r) => setTimeout(r, 0));
out.pick = { picked, calls: calls.splice(0), opened };
process.stdout.write(JSON.stringify(out));
})().catch((error) => {
process.stderr.write(String(error?.stack ?? error));
process.exit(1);
});
-115
View File
@@ -1,115 +0,0 @@
/* M02 일위대가 실무자료(324-4 · 338-3) 헬퍼 — `M02_MasterTemplete_Hopyo_Pick.ts`(고르기 창 「실무 호표」 덧줄) ·
* `M02_MasterTemplete_Hopyo_Choose.ts` 글 도우미 · `M02_MasterTemplete_Hopyo_Sheet.ts` 엑셀 보기 창 도우미(329 · 행 → 호표)를
* 트랜스파일해 Node 로 돌림 · fetch 대역이 부른 길 · 몸을 모음 ·
* stdin = {rows, built} · stdout = {calls, kinds, rows, picked, project, view, sheet} JSON. */
const fs = require("fs");
const path = require("path");
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
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",
"M02_MasterTemplete_Find_Pick",
"M02_MasterTemplete_Labor_Api",
"M02_MasterTemplete_Labor_Input",
"M01_MasterData_UI_LogicLab_Source",
"M02_MasterTemplete_Hopyo_Left", // 남은 줄 창(729-3 · DOM · 찾기 창)
];
const stub = new Proxy({}, { get: (_t, k) => (k === "currentLanguageIndex" ? () => 0 : () => {}) });
const { loadTs } = makeLoader({
root: ROOT,
requireFor: (full) => (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;
},
});
(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_Choose.ts"));
const sheet = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Sheet.ts"));
const api = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Api.ts"));
await api.readHopyoGrid({ layer: "company", owner: "7" }, "HP-000001");
const spans = [
{ 번: 3, 이름: "임도표지석", 행: [20, 24] },
{ 번: 4, 이름: "배수관", 행: [25, 30] },
];
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회사용시", 지역: "서울" }, ["보정작업"]),
},
basis: (() => {
const hit = {
조항: ["건설 공통 3-2-1"],
직종: ["특별인부"],
단위: true,
점수: 1.65,
까닭: "조항 같음 · 직종 특별인부 · 단위 맞음",
};
const half = { ...hit, 직종: [] };
const trade = { 출처: [["건설품셈", "3-2-1"]], 직종: ["특별인부", "보통인부"], 행: [1, 1] };
const plain = { ...trade, 직종: ["보통인부"] };
return {
ok: [hit, half, { ...hit, 단위: false }, { ...hit, 까닭: "앞 마디 같음" }, undefined].map(
(b) => view.basisOk(b, trade),
),
// 엑셀 직종이 보통인부뿐 · 없음(기계) = 직종 안 봄
plain: [view.basisOk(half, plain), view.basisOk(half, { ...trade, 직종: [] })],
tag: [hit, half, undefined].map(view.basisTag),
source: [
view.sourceText({ 출처: [["건설표준품셈", "3-2-1"]], 직종: ["특별인부"], 행: [93, 101] }),
view.sourceText({ 출처: [], 직종: [], 행: [7, 7] }),
view.sourceText(undefined),
],
};
})(),
sheet: {
at: [19, 20, 22, 24, 25, 30, 31].map((row) => sheet.hopyoAtRow(spans, row)),
span: sheet.spanText([20, 24], sheet.lastColumn({ 칸: { A1: {}, N24: {} }, 열: { O: {} } })),
last: [sheet.lastColumn({ 칸: { Z9: {}, AA1: {} } }), sheet.lastColumn({ 칸: {} })],
rows: [sheet.rowsText(4345), sheet.rowsText([93, 101])],
},
};
process.stdout.write(JSON.stringify(out));
})().catch((error) => {
process.stderr.write(String(error?.stack ?? error));
process.exit(1);
});
@@ -1,58 +0,0 @@
/* M02 일위대가 실무자료 이음매(329 · 330) 헬퍼 — 실제 서버 응답(격자 · 호표 맞춤)을
* `M02_MasterTemplete_Hopyo_Sheet.ts`(행 → 호표 · 영역 A1) · `M02_MasterTemplete_Hopyo_Choose.ts`
* (근거 꼬리표 · 엑셀 근거 줄 · 근거 맞음)에 먹임 ·
* stdin = {grid, lines, all} · stdout = {at, span, rows, lines: [{이름, source, tag, ok}],
* count: {top}} JSON — count = 「후보」 줄 1순위 근거 맞음. */
const fs = require("fs");
const path = require("path");
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
const ROOT = path.join(__dirname, "..", "..");
const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api" } };
const STUB_FILES = [
"M02_MasterTemplete_Labor_Pick",
"M02_Material_Pick",
"M02_MasterTemplete_Find_Pick",
"M02_MasterTemplete_Labor_Api",
"M02_MasterTemplete_Labor_Input",
"M01_MasterData_UI_LogicLab_Source",
"M02_MasterTemplete_Hopyo_Left", // 남은 줄 창(729-3 · DOM · 찾기 창)
];
const stub = new Proxy({}, { get: (_t, k) => (k === "currentLanguageIndex" ? () => 0 : () => {}) });
const { loadTs } = makeLoader({
root: ROOT,
requireFor: (full) => (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;
},
});
const input = JSON.parse(fs.readFileSync(0, "utf8"));
const M02 = path.join(ROOT, "M02_MasterTemplete");
const sheet = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Sheet.ts"));
const view = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Choose.ts"));
const { grid } = input;
const ilwi = grid.책.시트.find((s) => s.이름 === "일위대가");
const three = grid.호표.find((h) => h.번 === 3);
const out = {
at: [19, 20, 22, 24, 25, 26].map((row) => sheet.hopyoAtRow(grid.호표, row)),
span: sheet.spanText(three.행, sheet.lastColumn(ilwi)),
rows: sheet.rowsText(grid.단산.find((b) => b.번 === 93).행),
lines: input.lines.map((line) => {
const top = (line.맞춤.후보 ?? [])[0];
return {
이름: line.이름,
source: view.sourceText(line.근거),
tag: view.basisTag(top?.근거),
ok: view.basisOk(top?.근거, line.근거),
};
}),
};
const open = (input.all ?? []).filter((line) => line.맞춤.상태 === "후보");
out.count = {
top: open.filter((line) => view.basisOk((line.맞춤.후보 ?? [])[0]?.근거, line.근거)).length,
};
process.stdout.write(JSON.stringify(out));
+4 -18
View File
@@ -2,7 +2,7 @@
서버 없이 잡음: 탭(역할) 따라 판 바꿈(일위대가표 = 호표 목록 · 단가산출 탭 = 단가산출 목록 · 칸은 처음 한 번만 지음) ·
시트 행 → 목록 강조 · 목록 줄 → 지금 탭의 블록 자리 · 만들 층 ≠ 파일 층 = 보기만 · 거르기(전체 · 안 · 밖 · 고를 줄 있음) ·
줄 0 블록 = 「줄 없음」 확인 막음 · 마법사 칸과 같은 캐시 열쇠(두 벌 고름 맞춤) · [끝내기] = 막지 않고 물음창 글만 ·
[끝내기] = 막지 않고 물음창 글만 ·
화면 = 부품 상자만 · 700 줄.
"""
@@ -14,7 +14,6 @@ from pathlib import Path
HERE = os.path.dirname(__file__)
M02 = Path(__file__).resolve().parents[2] / "M02_MasterTemplete"
from test_778_5_sk_pick_screen import BLOCKS as sys_blocks # noqa: E402
def _node(payload: dict) -> dict:
@@ -71,24 +70,11 @@ def test_다른_층_권한_없음_보기만():
assert got["blocks"][0]["readOnly"] is True
def test_거르기_줄0_막음_끝내기_글():
got = _node({"blocks": sys_blocks})
assert got["empty"] == {"판정": "줄 없음", "켜짐": False, "확인": [], "몸": []}
assert got["filter"] == {"all": [2, 5, 9, 7], "in": [2, 7], "out": [5, 9], "open": [2, 9]}
assert got["old"] == 0 # 옛 서버(부름 없음) = 모두 일위대가 안
assert got["wait2"] == {"블록": 3, "밖": 2, "줄": 3} # 만든 블록 7 뺌 · 줄 0 블록 = 고를 줄 1
def _src(name: str) -> str:
return (M02 / f"M02_MasterTemplete_Hopyo_{name}.ts").read_text(encoding="utf-8")
def test_캐시_공유_알림_칠_다시():
wiz = _src("WizardSk")
assert wiz.count("skDraftKey(scope, key)") == 1 # 두 벌 모두 같은 열쇠(층 · 주인 · 파일 키)
assert "SK_SYNC" in wiz and "tell({ got: next })" in wiz # 옮김 = 받은 판정 그대로 다른 벌에
assert wiz.count("tell();") == 2 # [확인] · 줄 고름 바뀜
assert "new Event(HOPYO_BUILT)" in wiz # 옮김 뒤 열린 엑셀 보기 칠 다시
sheet = _src("Sheet")
assert "createSheetSk(" in sheet and "sk?.at(s);" in sheet
side = _src("SheetSk")
@@ -96,17 +82,17 @@ def test_캐시_공유_알림_칠_다시():
nav = _src("SheetNav")
assert "blockAt(sheet: string, row: number)" in nav and "spotOf(no: number, role: Role)" in nav
wizard = _src("Import")
assert 'L("M02_SkUnmoved")' in wizard and "finish.disabled = !!n?.줄;" in wizard # 밖 = 안 막음
assert 'L("M02_SkUnmoved")' in wizard and "leftCount" not in wizard # 밖 = 안 막음 · 옛 남은 줄 막음 걷음
tone = (M02 / "logic" / "M02_MasterTemplete_Hopyo_SheetMap.ts").read_text(encoding="utf-8")
assert '글: "일위대가 밖"' in tone
def test_부품_상자만_700줄():
for name in ("SheetSk", "Sheet", "SheetMake", "SheetNav", "WizardSk", "Import", "Side"):
for name in ("SheetSk", "Sheet", "SheetNav", "Import", "Side"):
src = _src(name)
assert len(src.splitlines()) <= 700, name
assert not re.search(r"#[0-9a-fA-F]{3,6}\b|rgb\(", src), name
for name in ("SheetSk", "WizardSk", "Import"):
for name in ("SheetSk", "Import"):
assert not re.search(r'el\("(button|input|select)"|<button|<input', _src(name)), name
css = (M02 / "M02_MasterTemplete_Hopyo.css").read_text(encoding="utf-8")
block = css.split(".m02-hopyo-sheet.is-sk", 1)[1].split("/*", 1)[0]
+3 -3
View File
@@ -33,9 +33,9 @@ def test_sk_list_layer_order_and_ref_shape():
# 721-2 — SK · 일위대가 탭은 찾아 넣기 창(Find_Pick) · 줄 누름 = 참조
find = _text(M02 / "M02_MasterTemplete_Find_Pick.ts")
assert "go: take(() => opt.ref?.(x.ref, x.제목 || x.이름))" in find
assert "sangeun: true," in _text(
M02 / "M02_MasterTemplete_Hopyo_Pick.ts"
) # 일위대가 [로직 추가] → 찾아 넣기 창
assert "sangeun: true }" in _text(
M02 / "M02_MasterTemplete_Labor_Pick.ts"
) # 일위대가 [로직 추가] → 찾아 넣기 창(785-7 `withSangeun`)
store_ts = _text(ROOT / "B09_Estimation" / "B09_Estimation_UI_Store.ts")
assert '["sangeun", "단가산출"],' in store_ts and "sangeun: sangeunItems," in store_ts
# 더한줄 고르기 — 715 이음 꼴 셋(인력 · 재료 = 요소 키)
@@ -89,7 +89,7 @@ def test_구조물_조합_층_제자리_바꿈():
assert "topHost.replaceChildren(" in build and "treeHost.replaceChildren(" in build
assert "tree: [treeHost]," in page and "top: topHost," in page
assert "() => isAdmin," in page and 'isSystem: user?.role === "SYSTEM_ADMIN",' in page
assert "main.root.isConnected ? void afterItem()" in page # 나무를 갈아도 듣기 그대로
assert "afterItem" not in page # 785-7 [구조물 조합으로](옛 확인 창 몫) 듣기 걷음
side = _text("M02_MasterTemplete_UI_Side.ts")
assert "const drawing = section(" in side and "practice" not in side # 732-1
# 도면 양식 칸 = 누구나 · 단추만 관리자(720-4 덧)
+3 -15
View File
@@ -44,7 +44,7 @@ def test_721_2_부르는_곳_셋만_새_창():
assert "logicOnly" not in _text(M02 / "M02_MasterTemplete_Sangeun_Detail.ts") # 727-2
assert "logicOnly" not in find
assert "openFindPicker({" in _text(M02 / "M02_MasterTemplete_Sangeun_Pick.ts")
assert "sangeun: true," in _text(M02 / "M02_MasterTemplete_Hopyo_Pick.ts") # 일위대가
assert "sangeun: true }" in labor_pick # 일위대가(785-7 `withSangeun`)
# 옛 창 그대로 — B09 단가 연결은 sangeun 없이 부름(736-1 불러오기 · 별칭 장부는 찾아 넣기 창으로)
for f in (
ROOT / "B09_Estimation" / "B09_Estimation_UI_Link.ts",
@@ -52,7 +52,7 @@ def test_721_2_부르는_곳_셋만_새_창():
):
text = _text(f)
assert "openLogicPicker(" in text and "sangeun:" not in text, f.name
for f in ("Hopyo_Choose", "Hopyo_Ledger"):
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 = 로직 · 인력 · 재료(기계 · 호표 없음)
@@ -126,11 +126,7 @@ 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
# 불러오기 엑셀 연결 = 기초(자재 아닌 줄) · 재료(자재 · 재료비 줄) · 별칭 장부 = 기초 · 재료 둘
choose = _text(M02 / "M02_MasterTemplete_Hopyo_Choose.ts")
assert "다른 로직 검색" not in choose and "다른 자재 검색" not in choose
assert 'MATERIAL.has(line.갈래) ? [] : (["logic"] as const)' in choose
assert 'canPickMaterial(line) ? (["material"] as const) : []' in choose
# 별칭 장부 = 기초 · 재료 둘(옛 확인 창 고르기는 785-7 에서 지움)
assert 'kinds: ["logic", "material"],' in _text(M02 / "M02_MasterTemplete_Hopyo_Ledger.ts")
@@ -143,14 +139,6 @@ def test_736_2_빠진_길_둘_신규_자재_기계_탭():
assert 'opt.machine && ok("machine")' in find
pick = _text(M02 / "M02_Material_Pick.ts")
assert "export function openNewMaterial(" in pick and "{ owner }," in pick
choose = _text(M02 / "M02_MasterTemplete_Hopyo_Choose.ts")
assert 'line.갈래 === "노임"' in choose and '["labor"]' in choose and '["machine"]' in choose
assert "element: (e) => opt.done([{ 노임: e.코드, 배: 1 }])," in choose
assert "machine: (e) => opt.done([{ 기계: e.코드, 배: 1 }])," in choose
left = _text(M02 / "M02_MasterTemplete_Hopyo_Left.ts")
assert "kinds: kindsOf(line)" in left and "pickMachine" not in left
lost = _text(M02 / "M02_MasterTemplete_Hopyo_Lost.ts")
assert '"machine"' in lost and "pickMachine" not in lost
unlinked = _text(M02 / "M02_MasterTemplete_Labor_Unlinked.ts")
assert 'machine: (e) => done("기계", e.코드)' in unlinked and "pickMachine" not in unlinked
@@ -1,138 +0,0 @@
"""일위대가 불러오기 마법사 안 이은 줄 화면(PLAN 729-3 · 요구서 내역19 1장 8 · 2장 4).
헬퍼(`helper_729_3_hopyo_left.cjs` · fetch · sessionStorage 대역)로 진짜 TS 를 돌림 —
만들기 409 `{까닭, 남은줄}` = StaleError 까닭 글 + 남은 줄 · 남은 수 = 안 만든 호표만(만든 호표는 안 물음) ·
줄마다 고름(노임 · 기계 · 비율 · 우리에 없음 · 로직) = 확인 창 캐시 → 만들기 몸 그대로 · 0 될 때까지 ·
일위대가표 안 이은 고정 줄(비율 · 이음 있는 줄 뺌) · 고름 대기 후보 글.
소스: [끝내기] = 남은 줄 다시 셈 → 있으면 막음(띠 · 표) · 단가산출 칸 · 목록 열 · 화면 요소 = 부품 상자 · 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"
def _line(no: int, kind: str = "자재", state: str = "후보") -> dict:
match = {"열쇠": f"줄{no}", "상태": state, "체크": False, "대상": []}
return {"차례": no, "이름": f"줄{no}", "규격": "", "수량": 1, "단위": "㎥", "갈래": kind,
"단가": {}, "금액": {}, "비고": "", "맞춤": match} # fmt: skip
def _left(no: int, kind: str) -> dict:
return {"차례": no, "이름": f"줄{no}", "규격": "", "단위": "㎥", "수량": 1, "갈래": kind,
"까닭": "이을 것 없음", "후보": []} # fmt: skip
LINES = [_line(0, "노임"), _line(1, "기계"), _line(2, "%", "%"), _line(3), _line(4, "단가산출")]
LEFT = [_left(0, "노임"), _left(1, "기계"), _left(2, "%"), _left(3, "자재"), _left(4, "단가산출")]
FIXES = [
{"고정": {"이름": "안이음", "단가": {}, "이음": {"노임": None, "자재": None, "기계": None}}},
{
"고정": {
"이름": "이음",
"단가": {},
"이음": {"노임": "LB000002", "자재": None, "기계": None},
}
},
{"고정": {"이름": "비율", "단가": {}, "비율": {"기준": "노무비", "비목": "경비"}}},
{"로직": "GF000113"},
]
def _run() -> dict:
payload = {
"one": {"키": "HP-000001", "파일": "북부청.xls", "호표": {"이름": "x", "줄": LINES}},
"made": [1],
"left": LEFT,
"fixes": FIXES,
}
proc = subprocess.run(
["node", os.path.join(HERE, "helper_729_3_hopyo_left.cjs")],
input=json.dumps(payload, ensure_ascii=False),
capture_output=True,
text=True,
encoding="utf-8",
check=False,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
def test_남은_줄_고름_몸_막힘_판정():
got = _run()
# 만들기 409 = StaleError · 까닭 글 · 남은 줄 그대로
assert got["stale"]["name"] == "StaleError" and got["stale"]["message"] == "남음"
assert got["stale"]["left"] == LEFT
# 만든 호표(1) 는 안 물음 · 안 만든 호표(2) 만 남은 줄
assert got["before"] == {"줄": 5, "호표": 1}
assert all(u.endswith("/layers/personal/hopyo-unlinked") for u in got["unlinkedCalls"])
assert len(got["unlinkedCalls"]) == 3 # 셈 세 번 × 안 만든 호표 하나
assert got["half"] == {"줄": 1, "호표": 1}
assert got["after"] == {"줄": 0, "호표": 0}
# 고른 것 = 만들기 몸(서버 이음 대상 꼴 · 입력값 없음 · 배 1)
body = {x["차례"]: x for x in got["body"]}
assert body[0] == {"차례": 0, "대상": [{"노임": "LB000002", "배": 1}]}
assert body[1] == {"차례": 1, "대상": [{"기계": "EQ000069", "배": 1}]}
assert body[2] == {"차례": 2, "대상": [{"비율": "노무비", "배": 1}]} # % 줄 = 밑수 고른 것만
assert body[3] == {"차례": 3, "뺌": "우리에 없음"}
assert body[4] == {"차례": 4, "대상": [{"로직": "GF000113", "입력값": {}, "배": 1}]}
assert got["lastBody"]["줄"] == got["body"] and got["lastBody"]["번"] == 2
# 일위대가표 띠 = 이음 · 비율 없는 고정 줄만
assert got["fixes"] == ["안이음"]
def _text(name: str) -> str:
return (M02 / name).read_text(encoding="utf-8")
def test_불러오기_끝내기_막음_단가산출_칸_목록_열():
wizard = _text("M02_MasterTemplete_Hopyo_Import.ts")
end = wizard.split("async function end()")[1]
assert end.index("await recount()") < end.index("showConfirmDialog(") # 끝내기 전 다시 셈
assert "if (leftCount(got).줄)" in end and "openLeft(" in end # 남으면 막음 · 표
assert "finish.disabled = !!n?.줄;" in wizard # 배지 · 남은 수 · [끝내기] 막음
assert (
"body?.skCount()" in wizard and "/sangeun`" not in wizard
) # 단가산출 칸 = 엑셀 보기 오른쪽 판
# 785-4 흠 — [안 이은 줄 고르기] · [끝내기] 모두 엑셀 보기가 읽은 호표 번으로 셈(빈 번 「0」 오표시 없음)
pick = wizard.split("async function openPick()")[1].split("async function ensureNos")[0]
assert "await ensureNos();" in pick and "openLeft(" in pick
assert "nos = got.호표.map((h) => h.번)" in wizard
assert "ensureNos" in wizard.split("async function recount()")[1]
sk = _text(
"M02_MasterTemplete_Hopyo_WizardSk.ts"
) # 778-5 D-5 — 줄 고름 · 확인한 것만(옛 기초 고름 걷음)
assert "judgeSangeun(scope, key, body)" in sk
lost = _text("M02_MasterTemplete_Hopyo_Lost.ts") # 옛 못옮김 창 = 보기만(옛 `바탕` 몸 걷음)
assert "makeSangeun" not in lost and "loadLinks" not in lost
assert "row.못옮김?.length && row.단산 !== undefined && row.SK" in _text(
"M02_MasterTemplete_Hopyo_Left.ts"
)
left = _text(
"M02_MasterTemplete_Hopyo_Left.ts"
) # 778-5 D-3 — 기초 고르기 · SK 만들기 걷고 걸음 이동
assert "makeSangeun" not in left and 'L("M02_LeftSkGo")' in left and "at.onSk" in left
lib = _text("M02_MasterTemplete_Library_List.ts")
assert 'L("M02_LibUnlinked"), (r) => (r.안이은줄' in lib
assert "unlinkedBar(doc, edit)" in _text("M02_MasterTemplete_Labor_Grid.ts")
assert "unlinkedBar(cur, edit &&" in _text("M02_MasterTemplete_Labor_Table.ts") # 일위대가표 띠
left = _text("M02_MasterTemplete_Hopyo_Left.ts")
# 일반 사용자 길 — 기계 고름은 찾아 넣기 창(736-2)이 `/logic-lib/elements` 로
assert "M01_MasterData_UI_Logic_Api" not in left
assert "/logic-lib/elements?group=" in _text("M02_MasterTemplete_Find_Pick.ts")
for name in (
"M02_MasterTemplete_Hopyo_Left.ts",
"M02_MasterTemplete_Hopyo_WizardSk.ts",
"M02_MasterTemplete_Hopyo_Import.ts",
"M02_MasterTemplete_Labor_Unlinked.ts",
"M02_MasterTemplete_Hopyo_Lost.ts",
):
src = _text(name)
assert len(src.splitlines()) <= 700, name
assert 'el("button"' not in src and 'el("input"' not in src, name # 부품 상자만
assert not re.search(r"#[0-9a-fA-F]{3,6}|rgb\(", src), name # 색 = 공통 값
@@ -83,18 +83,3 @@ def _lines(client, no):
def _auto(client, no):
"""장부가 자동으로 정한 줄 수."""
return sum(1 for m in _lines(client, no).values() if m["상태"] in FROM_LEDGER)
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
@@ -1,33 +0,0 @@
"""746-2 지킴 — 불러오기 고르기 창 후보 블록 줄 비교 · 요소 후보 화면(요구 585).
서버 없이 소스로 잡음: 안 이은 줄 창이 요소 후보 단추를 붙임(고름 대기 줄 비교 · 못옮김 잇기는 778-5 D-5 줄 고름으로 걷음) ·
안 이은 줄 창의 기초 후보 칸에 요소 후보(`키`)가 섞여 들어가지 않음 · 서버 응답 칸 이름 · 부품 상자만 · 700 줄.
"""
import re
from pathlib import Path
M02 = Path(__file__).resolve().parents[2] / "M02_MasterTemplete"
def _src(name: str) -> str:
return (M02 / f"M02_MasterTemplete_Hopyo_{name}.ts").read_text(encoding="utf-8")
def test_안_이은_줄_창이_요소_후보_단추를_붙임():
left = _src("Left")
assert "elemChoices(row.후보.filter(isElem)" in left
assert "SangeunCand" not in left # 778-5 D-3 — 기초 고르기 걷음
def test_서버_응답_칸_이름_그대로():
api = _src("Api")
assert "못옮김: SangeunLost[]" in api and "export interface ElemCand" in api
assert "SangeunCompare" not in api and "export function compareView" not in _src("Compare")
def test_부품_상자만_날_요소_없음_700줄_안():
src = _src("Compare")
assert not re.search(r'createElement\("(button|input)"|<button|<input', src)
for name in ("Compare", "WizardSk", "Lost", "Left"):
assert len(_src(name).splitlines()) <= 700
@@ -1,132 +0,0 @@
"""778-5 D-5 지킴 — 마법사 단가산출 줄 고름(제안 · 확인 뒤 옮김 · 요구 621 · 622 · 623 · 624).
서버 없이 잡음: 줄 고름 캐시가 서버 상태를 덮음(「내가 고름」) · [확인] = 고를 줄 0 일 때만 · 몸 = 이음 줄
`{확인: [{번, 고름?: {차례: 고름}}]}`(확인한 블록만) · 자동 확인 없음(빈 캐시 = 빈 몸 · 「모두 확인」 단추 없음) ·
화면 = 부품 상자만 · 700 줄.
"""
import json
import os
import re
import subprocess
from pathlib import Path
HERE = os.path.dirname(__file__)
M02 = Path(__file__).resolve().parents[2] / "M02_MasterTemplete"
def _line(at, kind, state, **more):
row = {"차례": at, "행": 100 + at, "묶음": kind, "이름": f"줄{at}", "규격": "", "매개값": {}}
row |= {"엑셀금액": 1000.0, "상태": state, "방법": "", "단위": None, "후보": [], "꼬리표": []}
return {**row, "글": "", "우리금액": None, "차": None, **more}
def _block(no, lines, made=None):
head = {"번": no, "이름": f"블록{no}", "단위": "㎥", "판정": "", "고를줄": 0, "우리금액": None}
return head | {"엑셀금액": 5000.0, "근거": None, "만든": made, "줄": lines}
BOSS = ["기본 — 건설업 작업반장"]
BLOCKS = [
_block(
2,
[
_line(0, "L", "이어짐", 매개값={"품": 0.2}),
_line(1, "L", "이어짐", 꼬리표=BOSS, 후보=[{"키": "LB000099"}]),
_line(2, "M", "고를 것", 후보=[{"자재": "MT000123"}]),
_line(3, "M", "못 이음"),
_line(4, "R", "밖"),
],
),
_block(5, [_line(0, "X", "이어짐"), _line(1, "R", "이어짐")]),
_block(7, [_line(0, "S", "못 이음")], made=None),
]
def _run() -> dict:
proc = subprocess.run(
["node", os.path.join(HERE, "helper_778_5_sk_pick.cjs")],
input=json.dumps({"blocks": BLOCKS}, ensure_ascii=False),
capture_output=True,
text=True,
encoding="utf-8",
check=False,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
def test_판정_확인_켜짐_조건():
got = _run()
assert got["start"] == [
{"번": 2, "판정": "고를 줄 2", "확인켜짐": False},
{"번": 5, "판정": "옮길 수 있음", "확인켜짐": True},
{"번": 7, "판정": "고를 줄 1", "확인켜짐": False},
]
assert got["sureBlocked"] == [] # 고를 줄 남은 블록 = [확인] 안 켜짐
assert got["half"] == {"판정": "고를 줄 2", "켜짐": False} # 이어진 줄 바꿈은 고를 줄 안 줄임
assert got["full"] == {"판정": "옮길 수 있음", "켜짐": True}
assert got["sure"] == [2, 5] # 하위 못 이음 블록 7 = 안 켜짐
def test_줄_표_꼴_내가_고름이_덮음():
view = _run()["view"]
assert view[0] == {"상태": "이어짐", "꼬리표": [], "글": "", "mine": False}
assert view[1] == {"상태": "이어짐", "꼬리표": ["내가 고름"], "글": "LB000099", "mine": True}
assert view[2]["상태"] == "이어짐" and view[2]["글"] == "MT000123"
assert view[3] == {
"상태": "뺌",
"꼬리표": ["내가 고름"],
"글": "뺌 · 우리에 없음",
"mine": True,
}
assert view[4]["상태"] == "밖"
def test_몸_꼴_이음_줄_자동_확인_없음():
got = _run()
assert got["emptyBody"] == [] # 누른 것 없음 = 몸 비움
picks = {"1": {"노임": "LB000099"}, "2": {"자재": "MT000123"}, "3": {"뺌": "우리에 없음"}}
assert got["body"] == [{"번": 2, "고름": picks}, {"번": 5}]
first, second = got["calls"]
assert first["method"] == "POST" and first["body"] == {"확인": []} # 판정만
assert first["url"].endswith("/layers/personal/hopyo/HP-000001/sangeun")
assert second["body"] == {"확인": got["body"]}
assert got["wait"] == {
"블록": 3,
"밖": 0,
"줄": 1,
} # 안 옮긴 블록 셋 · 블록 7 하위 줄 · 옛 서버 = 밖 없음
assert got["tidy"] == [5] # 고름 지우면 그 블록 확인 빠짐
assert got["round"]["확인"] == [2, 5] and got["bad"] == {"고름": {}, "확인": []}
def test_후보_고름_꼴():
cand = _run()["cand"]
assert cand["L"] == {"노임": "LB000001"} and cand["X"] == {"기계": "EQ000019"}
assert cand["M"] == {"자재": "MT000001"} and cand["R"] == {"기준": "노무비", "비목": "경비"}
assert cand["Mbad"] is None and cand["text"] == "합판 12mm [장]"
def _src(name: str) -> str:
return (M02 / f"M02_MasterTemplete_Hopyo_{name}.ts").read_text(encoding="utf-8")
def test_화면_자동_없음_부품_상자만():
wiz = _src("WizardSk")
assert "judgeSangeun(scope, key)" in wiz # 열 때 = 판정만
assert "judgeSangeun(scope, key, body)" in wiz and "sureBody(load(), got.판정)" in wiz
assert "모두" not in wiz.split("*/", 1)[1] and "setSure(" in wiz # 확인은 블록마다만
assert 'kind: "chip"' in wiz and "disabled: !ok" in wiz # [확인] 칩 · 고를 줄 0 일 때만
assert "단산행: 행" in wiz # 엑셀 보기 뛰기
lines = _src("SkLines")
assert "lineFind(" in lines and "unitTone(" in lines # 찾기 창 · 엑셀 칠과 한 규칙
wizard = _src("Import")
assert 'L("M02_SkLeft")' in wizard and "body?.skCount()" in wizard # [끝내기] 막힘 글
for name in ("WizardSk", "SkLines", "SkApi", "Lost", "Compare"):
src = _src(name)
assert len(src.splitlines()) <= 700, name
assert not re.search(r'el\("(button|input|select)"|<button|<input', src), name
assert not re.search(r"#[0-9a-fA-F]{3,6}\b|rgb\(", src), name
logic = (M02 / "logic" / "M02_MasterTemplete_Hopyo_SkPick.ts").read_text(encoding="utf-8")
assert not re.search(r"sessionStorage\.|fetch\(|document\.", logic) # 순수
+15 -22
View File
@@ -1,5 +1,5 @@
"""785-4 불러오기 화면 껍데기 — 가운데 상세 페이지(마법사) 없음 · 왼쪽 [불러오기] → 바로 본 칸 엑셀 + 오른쪽 목록 ·
머리 막대 [끝내기] · 흠 셋(안 이은 줄 0 오표시 · 「맞춤 셈 중」 멈춤 · 좁은 판 단가산출 줄 짓눌림) · 부품 상자만 · 700 줄.
머리 막대 [끝내기] · 좁은 판 줄 판 자리 · 부품 상자만 · 700 줄(785-7 — 안 이은 줄 띠 · 마법사 단가산출 걸음 · 옛 캐시 걷음 반영).
소스 글만 읽음(서버 · 화면 없이 돎)."""
import re
@@ -33,7 +33,7 @@ def test_누름_하나_바로_엑셀():
show = side.split("async function show(")[1].split("async function showLedger")[0]
assert "buildHopyoImport({" in show and "opt.show(" in show # 올림 뒤 본 칸 바로
imp = _src("Import")
assert "m.buildHopyoSheet({" in imp and "headExtra: [layerNote, badge, pickBtn, finish]" in imp
assert "m.buildHopyoSheet({" in imp and "headExtra: [layerNote, finish]" in imp
def test_결과_층_고름_둘_이상일_때만():
@@ -43,28 +43,21 @@ def test_결과_층_고름_둘_이상일_때만():
assert 'let pick: ManageLayer = "personal";' in side
def test_흠_셋_고침():
imp = _src("Import")
# ① 안 이은 줄 0 오표시 — 빈 호표 번으로 안 부름 · 엑셀 보기가 읽은 번을 씀
pick = imp.split("async function openPick()")[1].split("async function ensureNos")[0]
assert "await ensureNos();" in pick
assert "nos = got.호표.map((h) => h.번)" in imp
assert "ensureNos" in imp.split("async function recount()")[1]
# ② 「맞춤 셈 중」(409) = 3 초마다 저절로 다시 판정
sk = _src("WizardSk")
assert "error instanceof StaleError" in sk and "void judge()" in sk and "POLL_MS" in sk
# ③ 좁은 판 줄 짓눌림 — [줄 n] = 시트 아래 줄 판(넓은 줄 표) · 판 안 펼침 없음
assert "skLineTable({" in sk.split("function drawLines()")[1]
assert "is-sub" not in sk and "o.narrow" not in sk
def test_줄_판_자리_좁은_폭():
# 셈 중(블록 풀이) = 3 초마다 다시 읽음 · 줄 판 = 시트 아래 넓은 자리 · 좁은 폭 = 줄 판 높이 바닥
assert "POLL_MS" in _src("SheetSk") and "POLL_MS" in _src("Sheet")
sheet = _src("Sheet")
assert 'attrs: { "data-hopyo-lines": "" }' in sheet and "openLines," in sheet
assert "openLines: (make: (host: HTMLElement)" in _src("SheetSk") # 785-6 줄 판 자리 한 손잡이
css = (M02 / "M02_MasterTemplete_Hopyo.css").read_text(encoding="utf-8")
narrow = css.split("/* 좁은 폭(785-7 · 390)")[1].split("/*")[0]
assert ".m02-hopyo-sheet__lines:not([hidden])" in narrow and "min-height: 16rem;" in narrow
def test_끝내기_머리_막대_끝냄_뒤_빈_본_칸():
imp = _src("Import")
end = imp.split("async function end()")[1]
assert end.index("await recount()") < end.index("showConfirmDialog(")
assert "unmoved()" in end.split("showConfirmDialog(")[0] # 확인하고 안 옮긴 수 = 물음 글만(막지 않음)
assert "deleteHopyo(scope, file.키)" in end and "a.onEnded();" in end
side = _src("Side")
assert "onEnded: () => {" in side and "void refresh();" in side
@@ -73,19 +66,19 @@ def test_끝내기_머리_막대_끝냄_뒤_빈_본_칸():
def test_부품_상자만_700줄_늘리지_않음():
for name in ("Import", "Side", "Sheet", "SheetMake", "SheetSk", "WizardSk"):
for name in ("Import", "Side", "Sheet", "SheetSk"):
src = _src(name)
assert len(src.splitlines()) <= 700, name
assert not re.search(r"#[0-9a-fA-F]{3,6}\b|rgb\(", src), name # 색 = 공통 값
for name in ("Import", "SheetMake", "SheetSk", "WizardSk"):
for name in ("Import", "SheetSk"):
assert not re.search(r'el\("(button|input|select)"|<button|<input', _src(name)), name
assert len(_src("Check").splitlines()) <= 662 and len(_src("Api").splitlines()) <= 632
assert len(_src("Api").splitlines()) <= 632
css = (M02 / "M02_MasterTemplete_Hopyo.css").read_text(encoding="utf-8")
assert not re.search(r"#[0-9a-fA-F]{3,6}\b|rgb\(", css) and ".m02-hopyo-import" in css
def test_해시_캐시_열쇠_그대로():
check = _src("Check")
assert "m02-hopyo-check:" in check # 확인 창 캐시 열쇠
assert "m02-hopyo-sk:" in _src("WizardSk") # 단가산출 캐시 열쇠
picks = (M02 / "logic" / "M02_MasterTemplete_Hopyo_Picks.ts").read_text(encoding="utf-8")
assert "m02-hopyo-judge:" in picks # 판정 고름 캐시 열쇠 하나(옛 확인 · 단가산출 캐시 걷음)
assert "draftKey(layer, null, file.키)" in _src("Import")
assert "location.hash" not in _src("Import") + _src("Side") # 새 해시 열쇠 없음
+2 -583
View File
@@ -1,348 +1,17 @@
"""일위대가 실무자료 화면(PLAN 324-4 · 338-3) — 좌측 판 · 본 화면 · 확인 창 · 고르기 창 ·
별칭 장부 · [로직 추가] 「실무 호표」 줄 · 엑셀 보기 창.
"""일위대가 실무자료 화면(PLAN 324-4 · 718-1 · 785-4) — 좌측 판 자리 · 권한 · 불러오기 본 칸 · [끝내기] · 떠남.
- 확인 창 도우미(Node · fetch · sessionStorage 대역 · `helper_m02_hopyo_check.cjs`):
체크 → 몸(체크한 줄만 · 뺌 · 고른 대상 · 몫 · 배 · 호표) · 빠질 줄 까닭 · 캐시 키 ·
최신 응답만 · 품 비교 · 만들기 · 통째 · 만들 층 · 미리 셈 · 장부 길 · 장부 화면 합침 ·
하위 호표 먼저 · 확인된 것 = 바로 참조
- 고르기 창 덧줄(`helper_m02_hopyo_pick.cjs`): 이 층 · 위 층 호표 줄 · 종류 고르기 ·
프로젝트 층엔 없음 · 글 도우미 · 근거 꼬리표 · 엑셀 보기 창 행 → 호표
- 소스: 옛 즉시 확정 · 바로 만들기 없음(saveHopyoMemory · [확정] · [같음 모두 확정] ·
[근거 맞음 모두 확정] · [사전조합 만들기]) · `buildHopyo(` 는 확인 창 · 엑셀 보기 창 펼침에서만 ·
좌측 판 자리 · 700 줄 · 색 = 공용 토큰
옛 확인 창 · 고르기 창 · [로직 추가] 「실무 호표」 · 엑셀 보기 모달 시험은 785-7 에서 지움(판정 화면 = `test_642_6_screen`).
"""
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_Sheet",
"Hopyo_Check",
"Hopyo_CheckRows",
"Hopyo_Choose",
"Hopyo_Ledger",
"Hopyo_Import",
"Hopyo_SheetMake",
)
ROW = {
"층": "system",
"주인": None,
"키": "HP-000001",
"파일": "북부청.xls",
"번": 3,
"이름": "임도표지석",
"규격": "910×600×150",
"단위": "개소",
"맞춤": [2, 4],
}
SURE = {"로직": "GC000198", "이름": "장비사용타설", "입력값": {"소량감률": 50}, "배": 1, "몫": None}
QTY = {
"엑셀": {"인력": {"콘크리트공": 0.1091}, "장비": [{"갈래": "굴착기", "시간": 0.29}]},
"우리": {"인력": {"콘크리트공": 0.1091}, "장비": [{"갈래": "굴착기", "시간": 0.31}]},
"같음": {"콘크리트공": True, "굴착기": False},
}
QTY_MANY = { # 같은 장비 여러 줄(229-4 ②) — 합쳐 한 줄로 견줌
"엑셀": {"장비": [{"갈래": "굴착기", "시간": t} for t in (0.0334, 0.0371, 0.0889)]},
"우리": {"장비": [{"갈래": "굴착기", "시간": 0.0889}]},
"같음": {"굴착기": False},
}
def _text(name: str) -> str:
return (M02 / name).read_text(encoding="utf-8")
def _node(helper: str, payload: dict) -> dict:
proc = subprocess.run(
["node", os.path.join(HERE, helper)],
input=json.dumps(payload, ensure_ascii=False),
capture_output=True,
text=True,
encoding="utf-8",
check=False,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
def _line(no, state, check, parts, **extra):
match = {"열쇠": f"줄{no}||m3", "상태": state, "체크": check, "대상": parts, **extra}
return {"차례": no, "이름": f"줄{no}", "규격": "", "수량": 1, "단위": "㎥", "갈래": "단가산출",
"맞춤": match} # fmt: skip
def _check() -> dict:
lines = [
_line(0, "후보", False, [{"자재": "MT004258", "입력값": {}, "배": 1}]),
_line(1, "맞음", True, [SURE], 품=QTY),
_line(2, "별칭", True, [{"로직": "GF000101", "입력값": {}, "배": 1}]),
_line(3, "다른 점", False, [{"로직": "GF000103", "입력값": {}, "배": 1}], 품=QTY_MANY),
_line(4, "후보", False, []),
_line(5, "%", False, []),
_line(6, "호표", True, [{"호표": 46, "입력값": {}, "배": 2.3}]),
_line(7, "노임", False, [{"로직": "GF000300", "입력값": {}, "배": None}]),
]
ledger = [
{"열쇠": "가|b|m3", "원래": {"품명": "가", "규격": "b", "단위": "m3"}, "갈래": "짝",
"짝": [{"로직": "GC000198", "입력값": {}, "배": 1, "몫": None}], "층": "personal",
"주인": "7/42", "고침": True},
{"열쇠": "나||m3", "원래": {"품명": "나", "규격": "", "단위": "m3"}, "갈래": "짝",
"짝": [{"로직": "GF000101", "입력값": {}, "배": 1, "몫": None}], "층": "personal",
"주인": "7/42", "고침": True},
{"열쇠": "다||개", "원래": {"품명": "다", "규격": "", "단위": "개"}, "갈래": "짝",
"짝": [{"자재": "MT1", "입력값": {}, "배": 1, "몫": None}], "층": "system",
"주인": None, "고침": False},
] # fmt: skip
rows = [
{**ROW, "확인": {"층": "company", "주인": "7", "키": "SC-000009"}},
{**ROW, "번": 4, "이름": "배수관", "확인": None},
]
return _node("helper_m02_hopyo_check.cjs", {"lines": lines, "ledger": ledger, "rows": rows})
def test_확인_창_체크한_줄만_몸():
got = _check()["check"]
assert got["key"] == "m02-hopyo-check:personal::HP-000001:3"
assert got["suggested"] == [ # 서버 체크 제안 그대로 · 배 없는 줄 · % 줄 · 후보는 안 감
{"차례": 1, "대상": [{"로직": "GC000198", "입력값": {"소량감률": 50}, "배": 1}]},
{"차례": 2, "대상": [{"로직": "GF000101", "입력값": {}, "배": 1}]},
{"차례": 6, "대상": [{"호표": 46, "배": 2.3}]},
]
assert got["edited"] == [ # 캐시 — 맞음 줄 체크 끔 · 뺌 · 고른 대상(몫 · 배)
{"차례": 2, "대상": [{"로직": "GF000101", "입력값": {}, "배": 1}]},
{"차례": 3, "뺌": "우리에 없음"},
{
"차례": 4,
"대상": [{"로직": "GF000211", "입력값": {"유형": "보통"}, "배": 0.1, "몫": 0.1}],
},
{"차례": 6, "대상": [{"호표": 46, "배": 2.3}]},
]
assert got["leftOut"] == {"확인 안 함": 3, "뺌": 1, "% 줄": 1}
assert got["checkable"] == [True, True, True, True, False, False, True, False]
assert got["sure"] == [1, 2] # [맞음 · 별칭 일괄 체크]
assert got["compare"] == "콘크리트공 0.1091 → 0.1091 ✓ · 굴착기 0.29 → 0.31 ≠"
assert got["compareMany"] == "굴착기 0.1594 → 0.0889 ≠" # 같은 장비 여러 줄 = 합쳐 한 줄
assert got["toTarget"] == [
{"호표": 46, "배": 2.3},
{"자재": "MT1", "배": 1},
{"전국": True, "배": 1}, # 361-1 — 키 없음 · 이름은 서버가
]
assert got["newest"] == [False, True, True] # 같은 줄 늦은 응답 버림 · 다른 줄은 따로
assert got["dropped"] is False # 만들기 성공 = 캐시 지움
def test_고르기_창_도우미():
got = _check()["choose"]
assert got["hint"] == ["레미콘타설", "고임돌"] # 품명 첫 뜻 낱말(358-1 · 조항 번호 아님)
assert got["groups"]["groups"] == [
["1위", False, ["A"]],
["출처 같음", False, ["C"]],
["이름 닮은 로직", False, ["E"]],
["숫자만 닮음", True, ["D", "B"]], # 뜻 낱말 안 겹치는 「후보」 = 끝
]
assert got["groups"]["text"][:2] == ["되메우기 · A · 산림", "되메우기 · C · 건설"]
assert got["groups"]["material"] == ["1위", "그 밖 후보"]
nat = got["groups"]["national"] # 361-1
assert nat["groups"] == [["1위", False, ["전국"]], ["지역 변형", True, ["M1", "M2"]]]
assert nat["text"] == "레미콘(전국)-관급 25-21-120 [㎥] · 키 없음"
assert nat["region"] == [{"자재": "M2", "입력값": {}, "배": 1, "몫": None}]
assert nat["back"] == [{"전국": True, "입력값": {}, "배": 1, "몫": None}]
assert nat["hit"] == [ # 줄 글 지역 = 1위 · 전국 = 되돌릴 자리 · 남은 변형 접힘
["1위", ["M2"]],
["전국", ["전국"]],
["지역 변형", ["M1"]],
]
assert got["groups"]["pick"] == [True, False, True] # 재료비 있는 단가산출 줄도 자재 검색
assert got["picked"] == [
{"로직": "GC000206", "입력값": {}, "배": 1, "몫": None},
{"로직": "GF000211", "입력값": {}, "배": 0.1, "몫": 0.1},
]
assert got["inputs"] == "소량감률=50(블록), 지역=서울 · ⚠ 못 가름: 측압"
assert got["word"] == ["만듦", "있던 것을 확인한 내용으로 덮음"]
assert got["qty"] == "콘크리트공 0.1091 · 굴착기 0.29hr"
assert got["target"] == "제46호표 [㎡]"
def test_별칭_장부_화면_합침():
got = _check()["ledger"]
assert got["key"] == "m02-hopyo-ledger:company:7"
assert got["merged"] == [
["나||m3", "personal", True, True, "안 씀"], # 고친 줄
["다||개", "personal", True, True, "짝"], # 위 층 → 내 층으로 고쳐 씀
["다||개", "system", False, False, "짝"], # 위 층 = 그대로 읽기만
] # 지운 줄(가) 없음
assert got["filter"] == ["다||개", "다||개"] and got["kind"] == ["나||m3"]
assert got["pair"] == "GC000198 × 1 · 몫 0.1 · 소량감률=50"
assert got["entry"]["짝"] == [] and "층" not in got["entry"]
def test_길_만들기_통째_만들_층_미리_셈_장부():
calls = _check()["calls"]
build = "/api/m02/layers/company/hopyo-build?owner=7"
head = {"층": "system", "주인": None, "키": "HP-000001", "번": 3}
assert calls[0]["url"] == build and calls[0]["body"]["줄"][0]["차례"] == 1
assert {k: calls[0]["body"][k] for k in head} == head and "새로" not in calls[0]["body"]
assert calls[1]["body"] == {**head, "통째": {"대상": [{"로직": "GF000225", "배": 1}]}}
assert calls[2] == {
"url": "/api/m02/layers/system/hopyo/HP-000001/3?to=company&to_owner=7",
"method": "GET",
"body": None,
}
assert calls[3]["url"] == "/api/m02/layers/personal/hopyo/HP-000001/3/guess"
assert calls[3]["body"]["대상"][0]["몫"] == 0.1
assert calls[4]["url"] == "/api/m02/layers/company/hopyo-ledger?owner=7"
assert calls[5]["method"] == "PUT" and calls[5]["body"] == {"줄": {"가|b|m3": None}}
def test_로직_추가_확인된_것은_바로_참조_아니면_확인_창():
got = _check()["pick"]
assert got["picked"] == [
{
"ref": {"층": "company", "주인": "7", "키": "SC-000009"},
"name": "임도표지석",
"alias": "임도표지석",
}
]
assert got["calls"] == [] # 확인 없이 만들지 않음
assert got["opened"] == ["확인 — 북부청.xls 제4호표 배수관"]
def test_엑셀_보기_창_하위_호표_먼저():
got = _check()["sheet"]
assert got["order"] == [46, 45, 3, 7, 8] # 순환은 남은 차례
# 다 연결만 녹색 · 0/0 색 없음(359-2) · 358-3 연결할 · 연결된 두 칸이 있으면 그것
assert got["linked"] == [True, False, False, False, True, False, False]
def _pick() -> dict:
row = {**ROW, "확인": {"층": "company", "주인": "7", "키": "SC-000009"}}
return _node("helper_m02_hopyo_pick.cjs", {"rows": [row], "built": {}})
def test_고르기_창_실무_호표_줄():
got = _pick()
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": "개소 · 확인 SC-000009",
},
{
"kind": "hopyo",
"key": "system||HP-000001|엑셀",
"tag": "실무 호표",
"name": "엑셀로 보기 · 여러 호표",
"sub": "북부청.xls · 마스터",
"unit": "호표 1",
},
]
assert got["picked"][0]["ref"]["키"] == "SC-000009"
assert got["project"] is True # 프로젝트 층 = 덧줄 없음
def test_글_도우미와_근거_꼬리표():
got = _pick()
view = got["view"]
assert view["target"] == ["구조물터파기 [㎥]", "레미콘 25-21-120 [㎥]", "제46호표 [㎡]"]
assert view["inputs"] == "횟수=4회사용시, 지역=서울 · ⚠ 못 가름: 보정작업"
basis = got["basis"]
assert basis["ok"] == [True, False, False, False, False] # 조항 같음 · 직종 · 단위 모두
assert basis["plain"] == [True, True] # 줄 직종이 보통인부뿐 · 없음 = 직종 안 봄
assert basis["tag"] == [
"조항 건설 공통 3-2-1 · 직종 특별인부 · 단위 같음",
"조항 건설 공통 3-2-1 · 단위 같음",
"",
]
assert basis["source"] == [
"출처 건설표준품셈 3-2-1 · 직종 특별인부 · 단가산출 93–101행",
"단가산출 7행",
"",
]
def test_엑셀_보기_창_행과_호표():
got = _pick()
assert got["calls"][1] == {
"url": "/api/m02/layers/company/hopyo-grid/HP-000001?owner=7",
"method": "GET",
"body": None,
}
sheet = got["sheet"]
assert sheet["at"] == [None, 3, 3, 3, 4, 4, None] # 22행 → 제3호표 · 범위 밖 = 없음
assert sheet["span"] == "B20:O24" # 제3호표 영역 = B열 ~ 끝 열(열 폭 정보 O)
assert sheet["last"] == ["AA", "A"]
assert sheet["rows"] == ["B4345:F4345", "B93:F101"] # 단가산출 행 B~F 열
# ── 소스 ──────────────────────────────────────────────────────────────
def test_옛_즉시_확정과_바로_만들기_없음():
texts = {name: _text(f"M02_MasterTemplete_{name}.ts") for name in NEW}
every = "\n".join(texts.values())
for gone in ("saveHopyoMemory", "hopyo-memory", "HopyoMemory", "memoryOf", "basisPick"):
assert gone not in every, gone
for label in (
"같음 모두 확정",
"근거 맞음 모두 확정",
'"확정"',
'"사전조합 만들기"',
'"불러오기" 옛 단추', # 718-1 — 「불러오기」 = 칸 이름 · 마법사 단추라 낱말은 뺌
):
assert label not in every, label
users = sorted(name for name, text in texts.items() if "buildHopyo(" in text)
assert users == [
"Hopyo_Check",
"Hopyo_SheetMake",
] # 확인 창 · 엑셀 보기 [저장] 물음 뒤(785-4 만들기 셋 = SheetMake)
check = texts["Hopyo_Check"]
assert '`${L("M02_PrecomboTitle")} 생성`' in check and "맞음 · 별칭 일괄 체크" in check
assert "확인하고" not in check # 359-4 명사형
assert "showLoadingOverlay()" in check and "dropDraft(k)" in check
assert "m02-hopyo-check:" in check and "POLL_MS" in check
sheet = texts["Hopyo_SheetMake"]
# 359-2 — 펼침 표 · [확인 창 열기] · [이대로 n개 만들기] 없음 · [저장] = 물음 한 번 뒤 체크 줄로 만들기
for gone in ("확인 창 열기", "이대로", "review(", "확인하고 만들기"):
assert gone not in sheet, gone
save = sheet.split("async function save(")[1].split("async function makeAll(")[0]
assert 'showConfirmDialog(ask, "저장")' in save and "hasDraft(" in save
assert "건너뜀 — 체크 줄 없음" in save and "await makeAll(ready, skipped);" in save
assert (
"buildOrder(" in sheet and "unmadeKids(" in sheet and "leftAt(" in sheet
) # 하위 먼저 · 함께(777-1)
sheet = texts["Hopyo_Sheet"] # 목록 · [수정] = 엑셀 보기 몸
edit = sheet.split("const editButton =")[1].split("function paintList(")[0]
assert (
'"수정"' in edit and "e.stopPropagation();" in edit and "createJudgeRows({" in edit
) # 785-6 줄 판
row = sheet.split("function paintList(")[1]
assert row.index("m02-hopyo-sheet__no") < row.index("editButton(h)") < row.index("__name")
assert 'linked.classList.toggle("is-linked", allLinked(h));' in row
pick = texts["Hopyo_Pick"]
assert "if (row.확인)" in pick and 'import("./M02_MasterTemplete_Hopyo_Check")' in pick
assert not (M02 / "M02_MasterTemplete_Hopyo_View.ts").exists() # 본 칸 표 없음(359-1)
ledger = texts["Hopyo_Ledger"]
assert "m02-hopyo-ledger:" in ledger and "saveLedger(" in ledger
for gone in ("발주처", "회사로 올리기", "쓰인 횟수"):
assert gone not in ledger.split("*/", 1)[1] # 머리 글 「안 둠」 밖
def test_좌측_판_자리와_권한():
side = _text("M02_MasterTemplete_UI_Side.ts")
assert 'import { buildHopyoSection } from "./M02_MasterTemplete_Hopyo_Side";' in side
@@ -375,182 +44,6 @@ def test_좌측_판_자리와_권한():
assert gone not in imp + hopyo_side, gone # 중간 걸음 · 단가산출 체크 없음
def test_로직_추가_한_곳과_파일_규칙():
detail = _text("M02_MasterTemplete_Labor_Detail.ts")
assert len(detail.splitlines()) <= 700 # 700 줄 넘지 않음(717-3 나눔으로 줄어듦)
assert "withHopyo(library)," in detail
pick = _text("M02_MasterTemplete_Labor_Pick.ts")
assert "more?: () => Promise<PickExtra[]>;" in pick and "...later" in pick
assert "if (pre?.query) finder.search.value = pre.query;" 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)
for state in ("맞음", "별칭", "다른점", "후보"):
assert f".m02-hopyo__state.is-{state}" in css
assert '"./M02_MasterTemplete_Hopyo.css"' in _text("M02_MasterTemplete_Hopyo_Check.ts")
def test_엑셀_보기_창_여는_자리와_늦게_불러옴():
lazy = 'import("./M02_MasterTemplete_Hopyo_Sheet")'
side = _text("M02_MasterTemplete_Hopyo_Import.ts")
assert "return show(pick, got, true);" in _text(
"M02_MasterTemplete_Hopyo_Side.ts"
) # 올린 직후 바로 본 칸
assert lazy in side and "m.buildHopyoSheet({" in side # 본 칸 몸 = 늦게 불러옴
check = _text("M02_MasterTemplete_Hopyo_Check.ts")
assert lazy in check and 'label: "엑셀 보기"' in check and "호표: opt.no" in check
assert "엑셀에서 보기" not in check
assert "단산행: 행" in check
pick = _text("M02_MasterTemplete_Hopyo_Pick.ts")
assert lazy in pick
static = r'^import (?!type )[^(;]*"\./M02_MasterTemplete_Hopyo_Sheet"'
for text in (side, pick, check):
assert not re.search(static, text, re.M) # 정적 import 없음(타입만)
sheet = _text("M02_MasterTemplete_Hopyo_Sheet.ts")
assert 'import("../A00_Common/spreadsheet/spreadsheet")' in sheet
assert not re.search(
r'^import \{[^}]*\} from "\.\./A00_Common/spreadsheet/spreadsheet"', sheet, re.M
)
assert "readOnly: true" in sheet and "wide: true" in sheet
def test_실물_응답_대역_확인_창():
"""338-4 — 가 창 실물 응답(`fixtures/m02_hopyo_view_sample.json`)을 확인 창 · 고르기 창 도우미에
그대로 먹임 — 체크 = 서버 제안 · 몸 꼴 · 품 비교 · 옵션 출처 · 후보 판정 꼬리표."""
sample = json.loads((Path(HERE) / "fixtures" / "m02_hopyo_view_sample.json").read_bytes())
lines = sample["호표"]["호표"]["줄"]
got = _node("helper_m02_hopyo_check.cjs", {"real": lines})
by = {x["이름"]: x for x in got["lines"]}
remicon = by["레미콘타설(장비사용)"]
assert remicon["상태"] == "맞음" and remicon["checked"] and remicon["checkable"]
assert (
remicon["compare"]
== "콘크리트공 0.1091 → 0.1091 ✓ · 보통인부 0.03636 → 0.03636 ✓ · 굴착기 0.29 → 0.2909 ✓"
)
assert (
remicon["inputs"]
== "타설방법=장비사용타설(규격), 구조물=철근구조물(규격), 소량감률=50(블록)"
)
assert remicon["qty"] == "콘크리트공 0.1091 · 보통인부 0.03636 · 굴착기 0.29hr"
assert remicon["cands"][0][1] == "맞음" and remicon["cands"][0][2].startswith("조항 건설품셈")
rebar = by["철근가공조립(현장)"]
assert rebar["상태"] == "다른 점" and not rebar["checked"] and rebar["checkable"]
assert [x["차례"] for x in got["body"]] == [x["차례"] for x in lines if x["맞춤"]["체크"]]
assert all({"로직", "입력값", "배"} <= set(t) for x in got["body"] for t in x["대상"])
assert got["sure"] == [x["차례"] for x in lines if x["맞춤"]["상태"] == "맞음"]
def test_확인_창_손질_339():
"""339 덧고침 — ① 줄 표 꼴(한글 낱말 줄바꿈 · 옵션 · 품 비교 한 줄씩 · 단추 세로) · 장부 품명 · 규격 폭
② 만들기 뒤 좌측 사전조합 목록 다시 ③ 엑셀 보기 창 목록 셈 끝까지 다시 읽음 · 펼침 표 숫자 판정 칸."""
css = _text("M02_MasterTemplete_Hopyo.css")
assert "word-break: keep-all;" in css and "white-space: pre-line;" in css
assert ".m02-hopyo__lines .m02-hopyo__acts > *" in css
assert "nth-child(9)" not in css # 359-3 — 칸 차례 대신 칸 이름
assert ".m02-hopyo-ledger__table td:nth-child(1)" in css
assert "m02-hopyo-ledger__table" in _text("M02_MasterTemplete_Hopyo_Ledger.ts")
rows = _text("M02_MasterTemplete_Hopyo_CheckRows.ts")
assert r'text: compare.split(" · ").join("\n")' in rows # 품 비교 = 항목마다 한 줄
assert "m02-hopyo__opts" in rows # 옵션 = 항목마다 한 줄
check = _text("M02_MasterTemplete_Hopyo_Check.ts")
make = check.split("async function make(")[1].split("function builtBlock(")[0]
assert make.index("document.dispatchEvent(new Event(HOPYO_BUILT));") < make.index("opt.onBuilt")
side = _text("M02_MasterTemplete_UI_Side.ts")
assert "document.addEventListener(HOPYO_BUILT, onBuilt);" in side
onb = side.split("const onBuilt")[1].split(";")[0]
assert "precombo.refresh()" in onb and "sangeun.refresh()" in onb # 717-2a — 둘 다 새로고침
sheet = _text("M02_MasterTemplete_Hopyo_Sheet.ts")
assert "poll(file);" in sheet and "poll(next);" in sheet and "POLL_MS" in sheet
assert "if (ok) document.dispatchEvent(new Event(HOPYO_BUILT));" in _text(
"M02_MasterTemplete_Hopyo_SheetMake.ts"
)
def test_엑셀_보기_머리_막대_셈_끝까지_다시_읽음_359():
"""339 덧고침 2 → 359-1 · 785-4 — 옛 본 칸 표 몫(시트 · 호표 수 · 검산 · 「제안 n / 줄 m」 · 「셈 중」)은
엑셀 보기 몸 머리(부르는 쪽 덧붙임 `headExtra` — 층 · 안 이은 줄 · [끝내기]) · 셈 중이면 3 초마다 다시 읽음(머리 · 목록 함께)."""
sheet = _text("M02_MasterTemplete_Hopyo_Sheet.ts")
head = sheet.split("function paintHead(")[1].split("/** 목록 강조")[0]
for word in ("시트 ", "호표 ", "검산 어긋남", "검산 맞음", "제안 ", "숫자 판정 셈 중"):
assert word in head, word
assert "...(opt.headExtra ?? [])" in head and "opt.onRead?.(file);" in head
assert "dropButton" not in sheet and "onDeleted" not in sheet # [삭제] 걷음 = [끝내기] 한 길
poll = sheet.split("function poll(")[1].split("async function load(")[0]
assert "paintHead(next);" in poll and "paintList();" in poll and "poll(next);" in poll
def test_확인_창_표_일위대가_상세_꼴_359():
"""359-3 — 로직 표 · 자재 표(일위대가 상세 꼴) + 품 비교 · 단추 · 드롭다운 글(저장 값 그대로) ·
금액 = 줄마다 서버 calc 미리 셈(`logic/` 문서 꼴 · Build 와 같은 수량 규칙)."""
got = _check()["draft"]
assert got["logic"] == {
"종류": "일위대가",
"열": "",
"단위": None,
"담은로직": [ # 같은 로직 둘째 · 호표 · 배 없음 = 뺌
{"로직": "GC000198", "별칭": None, "메모": None, "입력값": {"소량감률": 50},
"수량": {"손": 1}},
],
"자재": [],
} # fmt: skip
assert got["mat"]["자재"] == [ # 같은 자재 = 2 × 1 + 2 × 0.5
{"코드": "MT1", "이름": None, "규격": None, "단위": None, "수량": {"손": 3},
"별도계상": "별도계상"},
] # fmt: skip
assert got["direct"]["별도계상"] == "직접계산" and got["direct"]["수량"] == {"손": 0}
assert got["none"] == [None, None, None] # 전국 자재(361-1) = 키 없어 미리 셈 없음
assert got["table"] == ["mat", "logic", "logic", "mat", "logic", "mat"]
rows = _text("M02_MasterTemplete_Hopyo_CheckRows.ts")
assert '["", "추가"],' in rows and '["우리에 없음", "로직없음"],' in rows
assert '["안 씀", "사용안함"],' in rows and "뺌 — " not in rows
for word in ('"고르기"', '"원문 보기"', '"엑셀 보기"', '"옵션"', '"품 비교"'):
assert word in rows, word
assert 'import { moneyCell, PRICE_SLOTS } from "./M02_MasterTemplete_Labor_Rows";' in rows
assert "...markCells(p?.세칸 ?? { 별도계상:" in rows # 361-2 서버 세 칸 먼저
assert "...markHeads()," in rows
for head in ("Col_Order", "Sub", "Col_Name", "Col_Alias", "Labor_QtyCol", "Col_Unit"):
assert f'tc("{head}")' in rows, head
check = _text("M02_MasterTemplete_Hopyo_Check.ts")
assert "calcLabor(target.layer, DRAFT_COLUMN, doc, {}, target.owner)" in check
assert "checkTables(h.줄, ctx())" in check and "draftDoc(line, partsOf(line, d))" in check
assert "function lineRow(" not in check and "function partCell(" not in check
draft = _text("logic/M02_MasterTemplete_Hopyo_Draft.ts")
assert "M02_Hopyo_Build" in draft # 정본 짝 표시
code = draft.split("*/", 1)[1] # 머리 글 밖
for gone in ("document", "fetch(", "sessionStorage", "@ui/"):
assert gone not in code, gone
def test_명사형_단추와_별칭_장부_풀이_359():
"""359-4 — 별칭 장부 뜻 한 줄(좌측 단추 title · 화면 머리) · M02 실무자료 단추 = 명사형 ·
갈래 글 = 확인 창 드롭다운과 같음(저장 값 그대로)."""
ledger = _text("M02_MasterTemplete_Hopyo_Ledger.ts")
hint = "엑셀 품명 ↔ 우리 로직 · 자재 짝을 기억해 두었다가 같은 품명이 나오면 먼저 제안"
assert hint in ledger and "`${LEDGER_HINT}" in ledger
side = _text("M02_MasterTemplete_Hopyo_Side.ts")
assert "title: LEDGER_HINT" in side
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")
mine += ("Hopyo_Import", "Hopyo_SheetMake")
every = "\n".join(_text(f"M02_MasterTemplete_{n}.ts") for n in mine)
for gone in (
"고친 것 버림",
"로직 더함",
"내 층으로 고쳐 씀",
"확인하고 사전조합 만들기",
"엑셀 올리기",
"엑셀에서 보기",
"모두 체크 (",
'label: "지우기"',
):
assert gone not in every, gone
def test_372_2_저장은_개인_층에_삭제는_화면_층_권한():
"""718-1 — 만들 층 = 올린 층 · [끝내기] 로 올린 파일 지움 → 빈 본 칸(785-4: 엑셀 보기 [삭제] 걷음) ·
743-1 — 떠남(화면 바뀜 · 페이지 닫음) = 쥔 파일 지움."""
@@ -562,77 +55,3 @@ def test_372_2_저장은_개인_층에_삭제는_화면_층_권한():
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
def test_779_세_보기_칠_뛰기_창_다시_씀():
"""779-4 — 탭 = 서버 역할 id(이름으로 안 찾음) · 칠 = 공용 격자 `칠` · 범례 부품 · 색은 공통 값 ·
확인 창 캐시 고침 = 다시 칠 알림 · 779-6 — 같은 파일 창은 다시 씀(앞으로 · 제자리) · 격자 다시 안 받음."""
sheet = _text("M02_MasterTemplete_Hopyo_Sheet.ts")
nav = _text("M02_MasterTemplete_Hopyo_SheetNav.ts")
check = _text("M02_MasterTemplete_Hopyo_Check.ts")
assert "Nav.nameTabs(book, grid)" in sheet and "칠: nav?.fill" in sheet
assert "nav?.at(s);" in sheet and "nav?.marks(briefs);" in sheet
assert "[detail, ...DANSAN]" in sheet # 단산행 = 역할 id 먼저(STmate 「단가산출근거」)
assert "if (opened.has(id)) return opened.get(id)!(opt);" in sheet
assert "Nav.gridOf(id, opt.scope, opt.key)" in sheet and "readHopyoGrid" not in sheet
assert "above = Nav.raise(left);" in sheet and "Nav.lower(left, above.splice(0))" in sheet
assert "createLegend([" in nav and 'kind: "line"' in nav
assert re.findall(r'color: "([^"]+)"', nav) == [
f"var(--color-{x})"
for x in ("success", "warning", "info", "danger", "text-muted", "accent")
]
assert 'HOPYO_DRAFT = "m02-hopyo-draft"' in nav
save = check.split("export function saveDraft(")[1].split("export function dropDraft(")[0]
assert 'document.dispatchEvent(new Event("m02-hopyo-draft"));' in save
for name in (
"M02_MasterTemplete_Hopyo_SheetNav.ts",
"logic/M02_MasterTemplete_Hopyo_SheetMap.ts",
):
text = _text(name)
assert (
'el("button"' not in text
and 'el("input"' not in text
and "#" not in text.split("*/", 1)[1]
)
pure = _text("logic/M02_MasterTemplete_Hopyo_SheetMap.ts").split("*/", 1)[1]
for word in ("document", "fetch", "sessionStorage", "import "):
assert word not in pure, word
assert (
len(check.splitlines()) <= 663
and len(_text("M02_MasterTemplete_Hopyo_Api.ts").splitlines()) <= 676
)
def test_단위_환산_칸_보이기_784_2():
"""784-2 — 배 1 · 단위 같음 = 칸 숨김 · 몫 줄 = 몫만 · 빈칸 · 추정 · 단위 다름 = 입력 칸 + 두 단위 글 재료 ·
화면(CheckRows · Ledger)은 「배」 대신 「단위 환산」 · 「엑셀 1 ㎡ = 우리 [ ] 본」 꼴."""
cases = [
{"part": {"배": 1, "환산": {"배": 1, "갈래": "같음"}}, "from": "㎡", "to": "㎡"},
{"part": {"배": 1, "환산": {"배": 1, "갈래": "두 단위"}}, "from": "공/㎥", "to": "공㎥"},
{"part": {"배": 0.1, "몫": 0.1, "환산": {"배": 1, "갈래": "같음"}}, "from": "㎥", "to": "㎥"},
{"part": {"배": None, "환산": {"배": None, "갈래": "입력"}}, "from": "㎡", "to": "본"},
{"part": {"배": 1, "환산": {"배": None, "갈래": "입력"}}, "from": "m", "to": "본"},
{"part": {"배": 0.2, "환산": {"배": 0.2, "갈래": "추정"}}, "from": "㎡", "to": "㎥"},
{"part": {"배": 100, "환산": {"배": 100, "갈래": "단위표"}}, "from": "m", "to": "cm"},
{"part": {"배": 0.01, "몫": 0.1, "환산": {"배": 0.1, "갈래": "단위표"}}, "from": "a", "to": "b"},
{"part": {"배": 1}, "from": "m2", "to": "㎡"},
{"part": {"배": 1}, "from": "m", "to": "본"},
{"part": {"배": 1, "몫": 1, "환산": {"배": 1, "갈래": "같음"}}, "from": "㎡", "to": "㎡"},
] # fmt: skip
got = _node("helper_m02_hopyo_unit.cjs", {"cases": cases})
assert [g["mode"] for g in got] == [
"same", "same", "quota", "input", "input", "input", "input", "input", "same", "input", "same",
] # fmt: skip
assert (got[3]["from"], got[3]["to"]) == ("㎡", "본") # 「엑셀 1 ㎡ = 우리 [ ] 본」
assert got[5]["estimated"] and got[5]["thick"] == 0.2 # 「두께 0.2m 로 추정」
assert not got[3]["estimated"] and got[3]["thick"] is None
rows = _text("M02_MasterTemplete_Hopyo_CheckRows.ts")
assert "unitView(p, line.단위, p.단위)" in rows and "M02_HopyoUnitSame" in rows
assert "배 — 우리 수량" not in rows
ledger = _text("M02_MasterTemplete_Hopyo_Ledger.ts")
assert "unitField(times, { from: r.원래.단위" in ledger and 'title: "배"' not in ledger
pure = _text("logic/M02_MasterTemplete_Hopyo_Unit.ts").split("*/", 1)[1]
for word in ("document", "fetch", "sessionStorage", "import "):
assert word not in pure, word
for name in ("CheckRows", "Ledger"):
assert len(_text(f"M02_MasterTemplete_Hopyo_{name}.ts").splitlines()) <= 700