"""일위대가 넣을 값(39-3) — 입력 목록 길 `GET /api/m02/logic-lib/inputs` · 값 셈(TS 모델). 못박는 것 - 입력 목록 = 키 차례 · 겹친 키 한 번 · 입력마다 자동값 · 없는 키는 「빠진로직」. - 입력 목록 몸 한 벌(`M02_Labor_Inputs`) · 옛 선택 입력은 없앰(39-7b). - 값 셈 = 칸 글 → 저장 값(빈 글 = 자동값 · 범위 = 수 · 고르기 = 원래 모양) · 범위 밖 · 모르는 이름 버림(사라진 값 알림) · 줄 아래 작은 글 · 빈 값 수(자동값도 없는 입력만). 화면 TS 는 그 자리에서 트랜스파일해 node 로 돌림 · 정본은 읽기만. """ from __future__ import annotations import json import shutil import subprocess from pathlib import Path import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from common_util.common_util_auth import verify_session from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs from M02_MasterTemplete import M02_MasterTemplete_Router_Combos as combos_router ROOT = Path(__file__).resolve().parents[2] A, B = "GF000222", "GF000221" # 찰쌓기 인력 · 장비 @pytest.fixture() def client() -> TestClient: app = FastAPI() app.include_router(combos_router.router) app.dependency_overrides[verify_session] = lambda: {"user_id": 42, "role": "USER"} return TestClient(app) def test_입력_목록은_키_차례_겹침_한_번_자동값_빠진_로직(client: TestClient) -> None: res = client.get("/api/m02/logic-lib/inputs", params={"keys": f"{B},{A},{B},GF999999,"}) assert res.status_code == 200, res.text got = res.json() assert [x["로직"] for x in got["로직"]] == [B, A] assert got["빠진로직"] == ["GF999999"] one = got["로직"][1] assert one["이름"] and one["결과단위"] assert all("출처" in x for x in got["로직"]) # 47-4 원문 패널 assert one["입력"] and all("자동값" in i and "이름" in i for i in one["입력"]) # 입력 목록 몸 한 벌(M02_Labor_Inputs) assert got["로직"] == labor_inputs.logic_inputs([B, A])["로직"] def test_빈_키는_빈_목록(client: TestClient) -> None: got = client.get("/api/m02/logic-lib/inputs").json() assert got == {"로직": [], "빠진로직": []} SCRIPT = r""" const fs = require("fs"); const path = require("path"); const ts = require(path.join(process.argv[1], "config", "node_modules", "typescript")); const file = path.join(process.argv[1], "M02_MasterTemplete", "M02_Labor_Values_Model.ts"); const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const box = { exports: {} }; new Function("exports", "module", "require", js)(box.exports, box, require); const m = box.exports; const height = { 이름: "높이", 단위: "m", 범위: [0.5, 5], 자동값: 1.2 }; const kind = { 이름: "종류", 고르기: ["막돌", "견치돌"] }; const size = { 이름: "규격", 고르기: [25, 40] }; const area = { 이름: "지역" }; const inputs = [height, kind, size, area]; const machine = { 이름: "기계", 고르기: ["0201-0060", "9999"], 고르기이름: { "0201-0060": "굴착기(무한궤도) 0.6㎥" }, }; console.log(JSON.stringify({ stored: [ m.storedOf(height, ""), m.storedOf(height, "2.5"), m.storedOf(kind, "막돌"), m.storedOf(size, "40"), m.storedOf(area, "서울"), ], outside: [ m.isOutside(height, "9"), m.isOutside(height, "0.5"), m.isOutside(height, ""), m.isOutside(height, "abc"), m.isOutside(area, "9"), ], known: m.keepKnown(inputs, { 높이: 2, 종류: "", 없앤것: 3 }), line: m.valueLine(inputs, { 높이: 2, 규격: 40 }), lineEmpty: m.valueLine(inputs, {}), named: [ m.choicesOf(machine).map((c) => [c.값, c.이름]), m.valueHint(machine), m.valueLine([machine], { 기계: "0201-0060" }).text, m.labelOf(machine, "9999"), m.storedOf(machine, "0201-0060"), ], })); """ def test_값_셈() -> None: node = shutil.which("node") if not node: pytest.skip("node 없음") out = subprocess.run( [node, "-e", SCRIPT, str(ROOT)], capture_output=True, text=True, encoding="utf-8" ) assert out.returncode == 0, out.stderr got = json.loads(out.stdout) assert got["stored"] == [None, 2.5, "막돌", 40, "서울"] assert got["outside"] == [True, False, False, True, False] assert got["known"] == {"kept": {"높이": 2}, "lost": ["없앤것"]} assert got["line"] == {"text": "높이 2m · 규격 40", "empty": 2} # 종류 · 지역 = 자동값 없음 assert got["lineEmpty"] == {"text": "", "empty": 3} # 높이는 자동값이 있어 빈 값 아님 # 85-1 — 보이는 글 = 이름 규격 · 모르는 번호는 그대로 · 저장 값은 번호 assert got["named"] == [ [["0201-0060", "굴착기(무한궤도) 0.6㎥"], ["9999", "9999"]], "굴착기(무한궤도) 0.6㎥ / 9999", "기계 굴착기(무한궤도) 0.6㎥", "9999", "0201-0060", ] def test_고르기_원문번호에_이름표(client: TestClient) -> None: """85-1 — 기계 · 집게 = 「이름 규격」 · 직종(입력 이름에 「직종」) = 직종 이름 · 고르기는 번호 그대로.""" got = client.get("/api/m02/logic-lib/inputs", params={"keys": "GC000260"}).json() specs = {i["이름"]: i for i in got["로직"][0]["입력"]} assert specs["기계"]["고르기이름"]["0201-0060"] == "굴착기(무한궤도) 0.6㎥" assert specs["집게"]["고르기이름"]["7206-0020"] == "부착용 집게 0.2㎥" assert "0201-0060" in specs["기계"]["고르기"] assert "고르기이름" not in specs["뒷길이"] and "고르기이름" not in specs["지역"] whole = labor_inputs.m01.loaded()[1] job = {"이름": "조종원직종", "고르기": ["1048", "없는번호"]} assert labor_inputs._labels(whole, job) == {"1048": "건설기계운전사"} ROWS_SCRIPT = r""" const fs = require("fs"); const path = require("path"); const root = process.argv[1]; const ts = require(path.join(root, "config", "node_modules", "typescript")); const stub = new Proxy({}, { get: (_t, k) => (k === "currentLanguageIndex" ? 0 : () => undefined), }); function load(f) { const js = ts.transpileModule(fs.readFileSync(f, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const box = { exports: {} }; const req = (id) => { if (id.startsWith(".") && !id.includes("M01_MasterData")) { const p = path.resolve(path.dirname(f), id); return fs.existsSync(p + ".ts") ? load(p + ".ts") : stub; } return stub; }; new Function("exports", "module", "require", js)(box.exports, box, req); return box.exports; } const m = load(path.join(root, "M02_MasterTemplete", "M02_MasterTemplete_Labor_Rows.ts")); const cells = new Map([["돌쌓기|㎡", "B6"]]); console.log(JSON.stringify([ m.baseQtyText({ 배: 10, 분모: "㎡" }), m.baseQtyText({ 배: 1, 분모: "㎥" }), m.baseQtyText({ 배: 1, 분모: null }), m.qtyShort({ 수량: 5, 수량출처: "손", 알약: "돌쌓기|㎡" }, "㎥", cells), m.qtyShort({ 수량: 12.5, 수량출처: "칸", 알약: "돌쌓기|㎡" }, "㎡", cells), m.qtyShort({ 수량: null, 수량출처: "없음", 알약: "돌쌓기|㎡" }, "㎡", cells), m.qtyShort({ 수량: null, 수량출처: "없음", 알약: "없는|㎡" }, "㎡", cells), m.qtyText({ 수량: 20, 수량출처: "칸", 알약: "돌쌓기|㎡" }, "㎡", cells), m.qtyText({ 수량: 20, 수량출처: "칸", 알약: "돌쌓기|㎡" }, "㎡"), m.qtyText({ 수량: 3, 수량출처: "손", 알약: "돌쌓기|㎡" }, "㎡", cells), m.qtyText({ 수량: null, 수량출처: "없음", 알약: "돌쌓기|㎡" }, "㎡", cells), m.qtyText({ 수량: null, 수량출처: "없음", 알약: "돌쌓기|㎡" }, "㎡"), ])); """ def test_수량_글은_이은_칸_주소를_보임() -> None: """`Labor_View.setQty(수량, 칸)` 의 칸 주소로 「수량 20 ㎡(B6)」 — 없으면 「면적산출 칸」.""" node = shutil.which("node") if not node: pytest.skip("node 없음") out = subprocess.run( [node, "-e", ROWS_SCRIPT, str(ROOT)], capture_output=True, text=True, encoding="utf-8" ) assert out.returncode == 0, out.stderr got = json.loads(out.stdout) # 수량산출 열(40-1) — 위 결과단위 기준 수량 · 아래 지금 수량 짧은 글 assert got[:7] == ["10 ㎡", "1 ㎥", "", "손 5 ㎥", "12.5(B6)", "빈 칸(B6)", "—"] assert got[7:] == [ "수량 20 ㎡(B6)", # 44-6 — 수량(칸 주소) "수량 20 ㎡(면적산출 칸)", "수량 손 입력 3 ㎡", "수량 빈 칸(B6)", # 이은 칸이 비었음 = 수량 없음(39-8 ② · 서버 area_values 와 같음) "", ]