From c91d5aa6c9ec9a4b5d6b93a6d3ae36ac362b0c83 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 10 Oct 2026 22:58:50 +0900 Subject: [PATCH] =?UTF-8?q?feat(b08):=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=EC=A7=91=EA=B3=84=ED=91=9C=20=EB=AA=A8=EB=91=90=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20=EC=B8=A1=EC=A0=90=20=EC=A4=84=20=EC=B4=88=EB=A1=9D?= =?UTF-8?q?=20=EB=9E=A8=ED=94=84=20(247-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 측점 줄 시설이 모두 조합에 연결 = 측점 칸 왼쪽 초록 램프(노랑과 같은 자리 · 크기) · 호버 「모두 연결됨」 - 미연결 = 노랑 그대로 · 시설 없는 줄 · 합계 줄 = 램프 없음 · 색 = 성공 토큰(밝음 · 어두움) - 새 시험 test_247_1 · 198_1 시험 정규식 맞춤 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P --- B08_Quantity/B08_Quantity_UI_Page.css | 5 + B08_Quantity/B08_Quantity_UI_Summary.ts | 12 +- .../tester/test_198_1_b08_summary_lamp.py | 4 +- .../test_247_1_b08_summary_green_lamp.py | 107 ++++++++++++++++++ ui_template/ui_template_locale_b4.ts | 1 + 5 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 resources/tester/test_247_1_b08_summary_green_lamp.py diff --git a/B08_Quantity/B08_Quantity_UI_Page.css b/B08_Quantity/B08_Quantity_UI_Page.css index 8de5e6c50..e07c12821 100644 --- a/B08_Quantity/B08_Quantity_UI_Page.css +++ b/B08_Quantity/B08_Quantity_UI_Page.css @@ -66,6 +66,11 @@ vertical-align: middle; } +/* 247-1 — 모두 연결 = 같은 램프 초록 */ +[data-b08-summary="table"] td.b08-summary__lamp.is-linked::before { + background: var(--color-success); +} + /* 좌측 패널 옵션(122) — 연암 · 경암 두 칸 나란히 · 설명 한 줄 */ .b08-options__row { display: grid; diff --git a/B08_Quantity/B08_Quantity_UI_Summary.ts b/B08_Quantity/B08_Quantity_UI_Summary.ts index a8a13ef89..4d74c1927 100644 --- a/B08_Quantity/B08_Quantity_UI_Summary.ts +++ b/B08_Quantity/B08_Quantity_UI_Summary.ts @@ -8,6 +8,7 @@ * 조합 · 측점 = 프로젝트 안 사본이 있는 것만(`linkedOnly` · PLAN 143-1) — 미연결 시설은 그 측점 * 줄만 측점 칸 왼쪽 노랑 램프(값 빈칸 · 서버 `unlinked` · PLAN 154-1 · 198-1) * · 램프 호버(공용 `ui_template_hover_info`) = 그 측점 빈 조합 칸(160-2 · 화면만) + * · 시설이 모두 조합에 이은 측점 줄 = 같은 자리 초록 램프(호버 「모두 연결됨」 · 247-1) * 횡단배수 열 · 칸 = GET …/quantity/summary(서버 단독 · PLAN 98 · 120) — 머리 구조물 · 형식 · 규격 * 3단 + 단위 줄 · 「관 공」 맨 윗 머리 = 관에 이은 조합 경로(있으면) · 서버가 펼친 종류(`types`)는 * 조합 열에서 뺌 · 다른 구조물 열은 조합 그대로 @@ -101,6 +102,8 @@ async function fetchPipeSummary(projectId: string): Promise { const PIPE_TOP = "관 공"; /** 미연결 램프 칸 class(`B08_Quantity_UI_Page.css` · 198-1) · 램프 자리 폭(12px 글 기준) */ const LAMP_CLASS = "b08-summary__lamp"; +/** 모두 연결 = 같은 램프 초록(247-1) */ +const LINKED_CLASS = `${LAMP_CLASS} is-linked`; const LAMP_W = 14; const stationKey = (station: number): number => Math.round(station * 100); /** 열 소수 자리 — 칸 · 계 값이 쓰는 만큼(최대 2). */ @@ -159,6 +162,8 @@ function buildTable( const missing = stations.map((station) => unassignedText(unlinkedAt(unlinked, station), L("B08_Quantity_Link_Body")), ); + // 247-1 — 시설이 있고 빈 칸 없음 = 초록 램프 + const linked = stations.map((station) => unlinkedAt(pipe?.facilities ?? [], station).length > 0); const table = createDataTable({ columns: [ { key: "station", label: stationHead, width: widthOf(stationHead, ...labels) + LAMP_W }, @@ -179,7 +184,8 @@ function buildTable( })), total, onRowClick: (i) => onRowClick(stations[i], labels[i]), - cellClass: (i, key) => (key === "station" && missing[i] ? LAMP_CLASS : null), + cellClass: (i, key) => + key !== "station" ? null : missing[i] ? LAMP_CLASS : linked[i] ? LINKED_CLASS : null, frozen: 1, empty: "", caption: L("B08_Quantity_Tab_StructureSummary"), @@ -191,7 +197,9 @@ function buildTable( table.root, (cell) => { const why = missing[Number(cell.closest("tr")?.dataset.i)]; - return why && `${L("B08_Quantity_Summary_LampTip")} — ${why}`; + return why + ? `${L("B08_Quantity_Summary_LampTip")} — ${why}` + : L("B08_Quantity_Summary_LampOk"); }, `td.${LAMP_CLASS}`, ); diff --git a/resources/tester/test_198_1_b08_summary_lamp.py b/resources/tester/test_198_1_b08_summary_lamp.py index 184e9396d..2088141f8 100644 --- a/resources/tester/test_198_1_b08_summary_lamp.py +++ b/resources/tester/test_198_1_b08_summary_lamp.py @@ -39,7 +39,7 @@ def _lamp_class() -> str: def test_미연결_줄_측점_칸만_램프(): - cell = re.search(r"cellClass: (\(i, key\) => .*?),\n", UI) + cell = re.search(r"cellClass: (\(i, key\) =>.*?),\n\s*frozen", UI, re.S) assert cell table = json.dumps(str(ROOT / "ui_template" / "ui_template_data_table.ts")) got = _node( @@ -50,6 +50,8 @@ new Function("exports", "module", "require", js(fs.readFileSync({table}, "utf8") box.exports, box, (p) => (p.endsWith(".css") ? {{}} : require(p))); const LAMP_CLASS = {json.dumps(_lamp_class())}; const missing = ["", "BOX", ""]; +const linked = [false, false, false]; +const LINKED_CLASS = LAMP_CLASS + " is-linked"; const cellClass = {cell.group(1)}; const html = box.exports.renderDataTableHtml({{ columns: [{{ key: "station", label: "측점" }}, {{ key: "c1", label: "조합" }}], diff --git a/resources/tester/test_247_1_b08_summary_green_lamp.py b/resources/tester/test_247_1_b08_summary_green_lamp.py new file mode 100644 index 000000000..a490adb49 --- /dev/null +++ b/resources/tester/test_247_1_b08_summary_green_lamp.py @@ -0,0 +1,107 @@ +"""247-1 B08 구조물 집계표 — 시설이 모두 조합에 이은 측점 줄 = 초록 램프. + +- 칸 class: 화면 소스의 `missing` · `linked` · `cellClass` 를 떼어 공용 표로 그림(Node) + 모두 연결 = 초록 · 하나라도 빈 칸 = 노랑 · 시설 없음 · 합계 줄 = 램프 없음 +- 호버 글: 초록 = 「모두 연결됨」 +- CSS: 초록 = 성공 토큰(밝음 · 어두움) +""" + +import json +import re + +from test_198_1_b08_summary_lamp import PRELUDE, ROOT, UI, _lamp_class, _node + +DATA = (ROOT / "B08_Quantity" / "B08_Quantity_Summary_Data.ts").read_text(encoding="utf-8") + + +def _fac(fid: str, at: float, *linked: bool) -> dict: + slots = [ + {"copy": f"{fid}{i}", "group": "칸", "item": None, "linked": v} + for i, v in enumerate(linked) + ] + return { + "id": fid, + "type_id": "box", + "name": "BOX", + "facility": None, + "chainage_m": at, + "slots": slots, + } + + +def test_모두_연결_초록_빈_칸_노랑_없음_없음(): + grab = lambda pat: re.search(pat, UI, re.S).group(1) # noqa: E731 + missing = grab(r"(const missing = stations\.map\(.*?\);\n)") + linked = grab(r"(const linked = stations\.map\(.*?\);\n)") + cell = grab(r"cellClass: (\(i, key\) =>.*?),\n\s*frozen") + line = re.search(r"(const LINKED_CLASS = .*?;)", UI).group(1) + facilities = [_fac("a", 0, True, True), _fac("b", 20, True, False), _fac("c", 20, True)] + unlinked = [f for f in facilities if not all(s["linked"] for s in f["slots"])] + table = json.dumps(str(ROOT / "ui_template" / "ui_template_data_table.ts")) + # 두 함수만 떼어 씀(모듈 import 없이) + data = json.dumps( + "".join( + re.search(rf"export (const {name} = .*?;\n)\n", DATA, re.S).group(1) + for name in ("unlinkedAt", "unassignedText") + ) + ) + got = _node( + PRELUDE + + f""" +const load = (src) => {{ + const box = {{ exports: {{}} }}; + new Function("exports", "module", "require", js(src))( + box.exports, box, (p) => (p.endsWith(".css") ? {{}} : require(p))); + return box.exports; +}}; +const t = load(fs.readFileSync({table}, "utf8")); +const {{ unlinkedAt, unassignedText }} = new Function( + js({data}) + "; return {{ unlinkedAt, unassignedText }};")(); +const L = (k) => k; +const LAMP_CLASS = {json.dumps(_lamp_class())}; +{line} +const stations = [0, 20, 40]; +const unlinked = {json.dumps(unlinked, ensure_ascii=False)}; +const pipe = {{ facilities: {json.dumps(facilities, ensure_ascii=False)} }}; +{missing}{linked}const cellClass = {cell}; +const html = t.renderDataTableHtml({{ + columns: [{{ key: "station", label: "측점" }}, {{ key: "c1", label: "조합" }}], + rows: stations.map((s) => ({{ station: String(s) }})), + total: {{ station: "계", c1: 1 }}, + frozen: 1, empty: "", cellClass }}); +const rows = html.split("")[1].split(""); +console.log(JSON.stringify({{ rows: rows.slice(0, 3), tail: html.split("")[1] ?? "", lamps: html.split(LAMP_CLASS).length - 1 }})); +""" + ) + lamp = _lamp_class() + assert f'class="is-frozen is-edge {lamp} is-linked"' in got["rows"][0] # 0 = 모두 연결 + assert f'class="is-frozen is-edge {lamp}"' in got["rows"][1] # 20 = 빈 칸 하나 = 노랑 + assert lamp not in got["rows"][2] # 40 = 시설 없음 + assert got["lamps"] == 2 # 합계 줄 없음 + + +def test_초록_호버_모두_연결됨(): + hover = re.search( + r"attachHoverInfo\(\n\s*table\.root,\n\s*(\(cell\) => \{.*?\n\s*\}),\n", UI, re.S + ) + got = _node( + PRELUDE + + f""" +const L = (k) => ({{ B08_Quantity_Summary_LampTip: "구조물 조합 미연결", B08_Quantity_Summary_LampOk: "모두 연결됨" }})[k]; +const missing = ["", "BOX: 칸"]; +const text = eval(js("(" + {json.dumps(hover.group(1))} + ")")); +const cell = (i) => ({{ closest: () => ({{ dataset: {{ i: String(i) }} }}) }}); +console.log(JSON.stringify([text(cell(0)), text(cell(1))])); +""" + ) + assert got == ["모두 연결됨", "구조물 조합 미연결 — BOX: 칸"] + locale = (ROOT / "ui_template" / "ui_template_locale_b4.ts").read_text(encoding="utf-8") + assert 'B08_Quantity_Summary_LampOk: ["모두 연결됨",' in locale + + +def test_초록_성공_토큰(): + css = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Page.css").read_text(encoding="utf-8") + m = re.search(r"td\.b08-summary__lamp\.is-linked::before\s*\{([^}]*)\}", css) + assert m and re.search(r"background:\s*var\(--color-success\)", m.group(1)) + theme = (ROOT / "ui_template" / "ui_template_theme.css").read_text(encoding="utf-8") + assert len(re.findall(r"--color-success:\s*#", theme)) >= 2 # 밝음 · 어두움 diff --git a/ui_template/ui_template_locale_b4.ts b/ui_template/ui_template_locale_b4.ts index b8433c381..4c0e3ff0e 100644 --- a/ui_template/ui_template_locale_b4.ts +++ b/ui_template/ui_template_locale_b4.ts @@ -65,6 +65,7 @@ export const ui_locales_b4 = { B08_Quantity_Summary_Total: ["계", "Total"], B08_Quantity_Summary_Unlinked: ["미연결", "Unlinked"], B08_Quantity_Summary_LampTip: ["구조물 조합 미연결", "Structure combination unlinked"], + B08_Quantity_Summary_LampOk: ["모두 연결됨", "All linked"], B08_Quantity_Link_Title: ["조합 연결", "Link combinations"], B08_Quantity_Link_Body: ["본체", "Body"], B08_Quantity_Link_Linked: ["연결됨", "Linked"],