feat(M01): 단가산출 로직 상세 차례 · 오른쪽 흐름도/원문 두 패널
- 좌측 차례 = 기본정보 › 시험 계산 › 단가산출 상세 › 텍스트 수식 - 오른쪽 패널 둘(흐름도 · 원문) — 하나만 열림 · 둘 다 접기 · 머리 토글 단추 · 열린 패널 계정 취향(m01-logic-panel) - 흐름도 패널 접힘을 부르는 쪽이 정하는 길(folded · onFold · fold) - 원문 패널 = 출처 글로 품셈 본문 절을 찾아 보임(GET /api/m01/logic/source) · 못 찾으면 「원문 연결 없음」 + 출처 - 시험 test_m01_logic_source.py · 화면 시나리오 ui_checks/44-4_로직상세_패널.js Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
"""M01 로직 원문 — 출처 글 → 품셈 본문 절(44-4). 원본은 읽기만."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from M01_MasterData import M01_MasterData_Router as router_module
|
||||
from M01_MasterData import M01_MasterData_Store as store
|
||||
from M01_MasterData import M01_MasterData_Store_Make as make
|
||||
|
||||
|
||||
def test_build_civil_section_with_table() -> None:
|
||||
got = make.source("건설품셈 토목 1-1-2")
|
||||
assert len(got["found"]) == 1
|
||||
one = got["found"][0]
|
||||
assert one["matched"] == "1-1-2"
|
||||
assert one["file"].startswith("원가계산/건설공사_표준품셈/본문/02_토목부문/제01장_")
|
||||
assert one["blocks"][0]["h"] == 3 and one["blocks"][0]["text"].startswith("1-1-2 유도선")
|
||||
table = next(b["table"] for b in one["blocks"] if "table" in b)
|
||||
assert table[0][0] == "구분" and ["특별인부", "인", "2", "1,350", "1,560"] in table
|
||||
# 다음 절(1-1-3 …) 앞에서 끊김 · 주석 줄 뺌
|
||||
assert not any("1-1-3" in b.get("text", "") for b in one["blocks"])
|
||||
assert not any(b.get("p", "").startswith("<!--") for b in one["blocks"])
|
||||
|
||||
|
||||
def test_forest_and_many_numbers() -> None:
|
||||
forest = make.source("산림품셈 3-4-2 1")["found"]
|
||||
assert [x["matched"] for x in forest] == ["3-4-2"]
|
||||
many = make.source("건설품셈 공통 8-1-5 · 8-1-6")["found"]
|
||||
assert [x["number"] for x in many] == ["8-1-5", "8-1-6"]
|
||||
|
||||
|
||||
def test_no_link() -> None:
|
||||
assert make.source("현장 자체 단가")["found"] == []
|
||||
assert make.source("건설품셈 토목 99-9-9")["found"] == []
|
||||
|
||||
|
||||
def test_most_master_logics_link() -> None:
|
||||
"""정본 로직 출처 대부분이 원문 절에 닿음."""
|
||||
hit = total = 0
|
||||
for path in store.FOLDER.glob("로직_*품셈_*.json"):
|
||||
for row in json.loads(path.read_text(encoding="utf-8"))["줄"]:
|
||||
total += 1
|
||||
hit += bool(make.source(row.get("출처", ""))["found"])
|
||||
assert total > 1000 and hit / total > 0.9, (hit, total)
|
||||
|
||||
|
||||
def test_route() -> None:
|
||||
app = FastAPI()
|
||||
app.include_router(router_module.router)
|
||||
got = TestClient(app).get("/api/m01/logic/source", params={"src": "산림품셈 3-1"}).json()
|
||||
assert got["found"][0]["matched"] == "3-1"
|
||||
@@ -0,0 +1,103 @@
|
||||
/* 44-4 M01 단가산출 로직 상세 — `orca eval --expression "$(cat 이 파일)"` 한 번에 돎 · JSON 돌려줌.
|
||||
*
|
||||
* 시작 화면: M01 마스터 데이터(#/m01-master-data · 시스템 관리자 로그인) · 아무 화면이나.
|
||||
* 로직 GC000277(건설품셈 토목 1-1-2 유도선 설치 및 해체)을 엶 · 고치지 않음 · 끝에 패널 취향을 처음 값으로 되돌림.
|
||||
* 기대 수치:
|
||||
* order = ["기본정보","시험 계산","단가산출 상세","텍스트 수식"](흐름 로직이면 셋째 · 넷째는 흐름 보기 안)
|
||||
* flow = {flow:true, source:false, pref:"flow"} · 오른쪽 칸 너비 > 300
|
||||
* source = {flow:false, source:true, pref:"source"} · srcHead 에 「1-1-2 유도선」 · srcTable 에 「특별인부」
|
||||
* none = {flow:false, source:false, pref:"none"} · 오른쪽 칸 너비 = 80(띠 둘)
|
||||
* back = {flow:true, source:false, pref:"flow"}(흐름도 띠 단추로 다시 폄)
|
||||
* noLink = 「원문 연결 없음」 + 「출처 · …」(서버가 빈 목록을 줄 때 · fetch 를 잠시 바꿔 봄)
|
||||
* restored = 처음 취향 값
|
||||
*/
|
||||
(async () => {
|
||||
const W = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const $ = (s, root = document) => root.querySelector(s);
|
||||
const $$ = (s, root = document) => [...root.querySelectorAll(s)];
|
||||
const until = async (f, ms = 15000) => {
|
||||
for (let t = 0; t < ms; t += 200) {
|
||||
const v = f();
|
||||
if (v) return v;
|
||||
await W(200);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const PREF = "aislo:pref:m01-logic-panel";
|
||||
const first = localStorage.getItem(PREF);
|
||||
const out = {};
|
||||
const openLogic = async (key) => {
|
||||
sessionStorage.setItem("m01.logic.pending", key);
|
||||
// 왼쪽 「단가산출 로직」 컨테이너 제목 — 펼칠 때 로직 화면으로(펼쳐져 있으면 접었다 폄)
|
||||
const tab = $$(".ui-collapsible__title").find((h) => h.innerText.trim() === "단가산출 로직");
|
||||
if (!tab) return false;
|
||||
if (!tab.closest(".ui-collapsible").classList.contains("is-collapsed")) tab.click();
|
||||
tab.click();
|
||||
return until(() => {
|
||||
const t = $(".m02-dfp__title")?.innerText ?? "";
|
||||
return !$(".m01lab-flow")?.hidden && $(".m01lab-side .m02-dfp") && t && t !== "";
|
||||
});
|
||||
};
|
||||
if (!(await openLogic("GC000277"))) return JSON.stringify({ error: "로직 못 엶" });
|
||||
await W(800);
|
||||
out.order = $$(".m01lab-flow__left .m01lab__box > .m01-logic__section-head h3").map((h) =>
|
||||
h.innerText.trim(),
|
||||
);
|
||||
const slots = () => $$(".m01lab-side__slot");
|
||||
const state = () => ({
|
||||
flow:
|
||||
slots()[0].classList.contains("is-open") &&
|
||||
!$(".m02-dfp", slots()[0]).classList.contains("is-folded"),
|
||||
source: slots()[1].classList.contains("is-open") && !$(".m02-dfp__open", slots()[1]).hidden,
|
||||
pref: localStorage.getItem(PREF),
|
||||
width: Math.round($(".m01lab-flow__right").getBoundingClientRect().width),
|
||||
});
|
||||
const flowToggle = () => $(".m02-dfp__toggle", slots()[0]);
|
||||
const srcToggle = () => $(".m02-dfp__toggle", slots()[1]);
|
||||
// 처음 = 흐름도 열림으로 맞춤
|
||||
if (!state().flow) {
|
||||
if (state().source) srcToggle().click();
|
||||
await W(100);
|
||||
flowToggle().click();
|
||||
await W(400);
|
||||
}
|
||||
out.flow = state();
|
||||
srcToggle().click();
|
||||
await until(() => $(".m01lab-src__head"));
|
||||
out.source = state();
|
||||
out.srcHead = $(".m01lab-src__head")?.innerText;
|
||||
out.srcFile = $(".m01lab-src__file")?.innerText;
|
||||
out.srcTable = $$(".m01lab-src__table td").some((td) => td.innerText === "특별인부");
|
||||
srcToggle().click();
|
||||
await W(400);
|
||||
out.none = state();
|
||||
flowToggle().click();
|
||||
await W(600);
|
||||
out.back = state();
|
||||
// 원문 연결 없음 — 서버가 빈 목록을 줄 때
|
||||
srcToggle().click();
|
||||
const real = window.fetch;
|
||||
window.fetch = (u, o) =>
|
||||
String(u).includes("/m01/logic/source")
|
||||
? Promise.resolve(new Response(JSON.stringify({ found: [] }), { status: 200 }))
|
||||
: real(u, o);
|
||||
try {
|
||||
await openLogic("GC000338");
|
||||
await until(() => $(".m01lab-src__none"));
|
||||
out.noLink = [
|
||||
$(".m01lab-src__none")?.innerText,
|
||||
$(".m01lab-src__body .m01-logic__muted")?.innerText,
|
||||
];
|
||||
} finally {
|
||||
window.fetch = real;
|
||||
}
|
||||
// 되돌림 — 처음 취향(없으면 흐름도)
|
||||
const want = first ?? "flow";
|
||||
if (want !== "source") srcToggle().click();
|
||||
await W(200);
|
||||
if (want === "flow") flowToggle().click();
|
||||
await W(300);
|
||||
out.restored = localStorage.getItem(PREF) ?? "flow";
|
||||
out.first = first;
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
Reference in New Issue
Block a user