Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_8

This commit is contained in:
2026-10-10 23:00:52 +09:00
5 changed files with 126 additions and 3 deletions
+5
View File
@@ -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;
+10 -2
View File
@@ -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<PipeSummary> {
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}`,
);
@@ -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: "조합" }}],
@@ -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("<tbody>")[1].split("</tr>");
console.log(JSON.stringify({{ rows: rows.slice(0, 3), tail: html.split("</tbody>")[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 # 밝음 · 어두움
+1
View File
@@ -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"],