Merge remote-tracking branch 'origin/sub_laptop_2' into sub_laptop_4
This commit is contained in:
@@ -11,11 +11,28 @@ const B06 = path.join(ROOT, "B06_Section");
|
||||
const common = fs.readFileSync(path.join(B06, "B06_Section_UI_Section_Common.ts"), "utf8");
|
||||
// 카드 여백은 원본에서 읽음(화면과 같은 값)
|
||||
const CROSS_PAD = Function(`return ${/export const CROSS_PAD = (\{[^}]*\})/.exec(common)[1]}`)();
|
||||
// 축 눈금 간격도 원본 함수 그대로(179-3 · 축 다시 그리기)
|
||||
const niceSrc = /export function niceTickStep[\s\S]*?\n\}/.exec(common.replace(/\r/g, ""))[0];
|
||||
const niceTickStep = ts
|
||||
.transpileModule(niceSrc.replace("export ", ""), { compilerOptions: {} })
|
||||
.outputText.concat("\nreturn niceTickStep;");
|
||||
const svgElement = (tag, attrs = {}) => {
|
||||
const node = el(tag);
|
||||
for (const [name, value] of Object.entries(attrs)) node.setAttribute(name, value);
|
||||
return node;
|
||||
};
|
||||
const svgText = (text, attrs) => Object.assign(svgElement("text", attrs), { textContent: text });
|
||||
const STUBS = {
|
||||
"@ui/ui_template_modal": { openModal: () => {} },
|
||||
"../A00_Common/b_page_state": {},
|
||||
"./B06_Section_Api_Fetch": {},
|
||||
"./B06_Section_UI_Section_Common": { CROSS_PAD, L: (key) => key },
|
||||
"./B06_Section_UI_Section_Common": {
|
||||
CROSS_PAD,
|
||||
L: (key) => key,
|
||||
niceTickStep: Function(niceTickStep)(),
|
||||
svgElement,
|
||||
svgText,
|
||||
},
|
||||
"./B06_Section_UI_Cross_Detail_Pick": { DETAIL_TARGETS: {} },
|
||||
};
|
||||
const resolve = (base) => (fs.existsSync(`${base}.ts`) ? `${base}.ts` : base);
|
||||
@@ -52,6 +69,24 @@ function el(tag) {
|
||||
node.parent = self;
|
||||
self.childNodes.push(node);
|
||||
}),
|
||||
replaceChildren: (...nodes) => {
|
||||
self.childNodes.splice(0).forEach((node) => (node.parent = null));
|
||||
self.append(...nodes);
|
||||
},
|
||||
// 「tag.class」 · 「tag」 만 — 깊이 우선 첫 것
|
||||
querySelector: (selector) => {
|
||||
const [tagName, cls] = selector.split(".");
|
||||
const walk = (node) => {
|
||||
for (const child of node.childNodes) {
|
||||
const classes = (child.attrs.class ?? "").split(" ");
|
||||
if (child.tag === tagName && (!cls || classes.includes(cls))) return child;
|
||||
const found = walk(child);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return walk(self);
|
||||
},
|
||||
addEventListener: (type, fn) => (listeners[type] ??= []).push(fn),
|
||||
fire: (type, event) => {
|
||||
const full = { preventDefault() {}, stopPropagation() {}, pointerId: 1, ...event };
|
||||
@@ -69,16 +104,42 @@ globalThis.document = { createElementNS: (_ns, tag) => el(tag), createElement: e
|
||||
|
||||
const { attachDetailZoom } = loadTs(path.join(B06, "B06_Section_UI_Cross_Detail.ts"));
|
||||
|
||||
const svg = el("svg");
|
||||
svg.setAttribute("viewBox", "0 0 600 300");
|
||||
svg.append(el("rect"), el("g"), el("g"));
|
||||
// 카드 svg 꼴 — 바탕 · 축 층 · clip 그룹 > 도형 층 > 십자(중심 300,150)
|
||||
const AXES = {
|
||||
widthPx: 600,
|
||||
heightPx: 300,
|
||||
pixelsPerMeter: 10,
|
||||
maxOffset: 24,
|
||||
displayMax: 120,
|
||||
elevationMid: 110,
|
||||
exaggeration: 1,
|
||||
};
|
||||
function chart() {
|
||||
const svg = el("svg");
|
||||
svg.setAttribute("viewBox", "0 0 600 300");
|
||||
const axes = svgElement("g", { class: "b06-chart__axes" });
|
||||
const clipGroup = svgElement("g", { "clip-path": "url(#c)" });
|
||||
const plot = svgElement("g", { class: "b06-chart__plot-layer" });
|
||||
const center = svgElement("g", { class: "b06-chart__center" });
|
||||
center.append(svgElement("line", { x1: 300, y1: 132, x2: 300, y2: 168 }));
|
||||
plot.append(center);
|
||||
clipGroup.append(plot);
|
||||
svg.append(svgElement("rect"), axes, clipGroup, svgElement("line", { class: "b06-chart__axis" }));
|
||||
return { svg, axes, plot, center };
|
||||
}
|
||||
const ticks = (axes) =>
|
||||
axes.childNodes.filter((node) => node.tag === "text").map((node) => node.textContent);
|
||||
const { svg, axes, plot: layer, center } = chart();
|
||||
let saved;
|
||||
const handle = attachDetailZoom(svg, undefined, (next) => (saved = next));
|
||||
const layer = svg.childNodes[0];
|
||||
const handle = attachDetailZoom(svg, AXES, undefined, (next) => (saved = next));
|
||||
const ticksBefore = ticks(axes);
|
||||
const out = {
|
||||
wrapped: svg.childNodes.length === 1 && layer.tag === "g" && layer.childNodes.length === 3,
|
||||
// 179-3 — 감싸지 않음 · 자식 넷 그대로
|
||||
wrapped: svg.childNodes.length === 1 && svg.childNodes[0].tag === "g",
|
||||
children: svg.childNodes.length,
|
||||
ratio: svg.attrs["style:--ratio"],
|
||||
before: layer.getAttribute("transform"),
|
||||
ticksBefore: ticksBefore.length,
|
||||
};
|
||||
// 휠 당김(deltaY > 0) = 확대 · 커서(화면 600,300 = svg 300,150) 고정
|
||||
svg.fire("wheel", { clientX: 600, clientY: 300, deltaY: 100 });
|
||||
@@ -97,19 +158,35 @@ svg.fire("pointerdown", { button: 1, clientX: 100, clientY: 100 });
|
||||
svg.fire("pointermove", { clientX: 300, clientY: 140 });
|
||||
svg.fire("pointerup", {});
|
||||
out.middlePan = { ...saved };
|
||||
// 팬 뒤 — 도형 층만 transform · 축 층 · 바깥 그룹 transform 없음 · 눈금 글 바뀜
|
||||
out.plotTransform = layer.getAttribute("transform");
|
||||
out.axesTransform = axes.getAttribute("transform");
|
||||
out.clipTransform = svg.childNodes[2].getAttribute("transform");
|
||||
out.ticksChanged = JSON.stringify(ticks(axes)) !== JSON.stringify(ticksBefore);
|
||||
out.ticksAfter = ticks(axes).length;
|
||||
out.panClickStopped = svg.fire("click", {
|
||||
stopPropagation() {
|
||||
this.stopped = true;
|
||||
},
|
||||
}).stopped;
|
||||
// 다시 떠도 배율 이어 감
|
||||
const again = el("svg");
|
||||
again.setAttribute("viewBox", "0 0 600 300");
|
||||
attachDetailZoom(again, saved, () => {});
|
||||
out.restored = again.childNodes[0].getAttribute("transform");
|
||||
const again = chart();
|
||||
attachDetailZoom(again.svg, AXES, saved, () => {});
|
||||
out.restored = again.plot.getAttribute("transform");
|
||||
// 다시 떠도 축은 이어 간 범위로(1배 원래 눈금과 다름)
|
||||
out.restoredTicksShifted = JSON.stringify(ticks(again.axes)) !== JSON.stringify(ticksBefore);
|
||||
// 축 옵션 없음 · 도형 층 없음 — 줌은 걸고 축은 그대로 / 줌 없음
|
||||
const bare = chart();
|
||||
out.noAxesHandle = attachDetailZoom(bare.svg, null, undefined, () => {}) !== null;
|
||||
const flat = el("svg");
|
||||
flat.setAttribute("viewBox", "0 0 600 300");
|
||||
out.noLayer = attachDetailZoom(flat, AXES, undefined, () => {});
|
||||
// 더블클릭 원복
|
||||
svg.fire("dblclick", {});
|
||||
out.reset = layer.getAttribute("transform");
|
||||
// 확대 — 십자는 중심에서 배율을 되돌림(크기 그대로)
|
||||
svg.fire("wheel", { clientX: 600, clientY: 300, deltaY: 100 });
|
||||
out.centerTransform = center.getAttribute("transform");
|
||||
out.max = (() => {
|
||||
for (let i = 0; i < 60; i++) svg.fire("wheel", { clientX: 600, clientY: 300, deltaY: 100 });
|
||||
return saved.scale;
|
||||
|
||||
@@ -40,10 +40,24 @@ def out() -> dict:
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_복제_자식을_g_하나로_감쌈_비율(out):
|
||||
assert out["wrapped"] is True # 축 · 눈금 · 도형이 한 장으로 움직임
|
||||
def test_감싸지_않음_비율(out):
|
||||
# 179-3 — 복제를 g 하나로 감싸지 않음(축 · 눈금 · 도형 한 장 → 도형 층만)
|
||||
assert out["wrapped"] is False and out["children"] == 4
|
||||
assert out.get("ratio") is None # 173-4 — 칸 = viewBox(칸 크기로 새로 그림) · 비율 변수 없음
|
||||
assert out["before"] is None
|
||||
assert out["ticksBefore"] > 0
|
||||
|
||||
|
||||
def test_축_고정_도형_층만_움직임_눈금_다시(out):
|
||||
# 179-3 — 팬 뒤 도형 층만 transform · 축 층 · clip 그룹 제자리 · 눈금 글은 보이는 범위로
|
||||
assert out["plotTransform"] == "translate(100 20) scale(1)"
|
||||
assert out["axesTransform"] is None and out["clipTransform"] is None
|
||||
assert out["ticksChanged"] is True and out["ticksAfter"] > 0
|
||||
assert out["restoredTicksShifted"] is True # 다시 떠도 이어 간 범위로
|
||||
# 확대 — 십자는 중심(300,150)에서 배율 되돌림
|
||||
assert out["centerTransform"].startswith("translate(300 150) scale(0.869")
|
||||
assert out["noAxesHandle"] is True # 축 옵션 없음 = 줌만
|
||||
assert out["noLayer"] is None # 도형 층 없음 = 줌 없음
|
||||
|
||||
|
||||
def test_휠_당기면_확대_커서_고정(out):
|
||||
@@ -69,8 +83,14 @@ def test_모달_배선_단추_칸_비율():
|
||||
src = _read("B06_Section_UI_Cross_Detail.ts")
|
||||
css = _read("B06_Section_UI_Style_Cross_Detail.css")
|
||||
assert "wheelZoom: true" in src and "attachSvgZoomPan({" in src
|
||||
assert "cad.replaceChildren(clone, buildZoomControls(handle))" in src
|
||||
assert "attachDetailZoom(clone, zoom, (next) => (zoom = next))" in src
|
||||
assert "cad.replaceChildren(clone, ...(handle ? [buildZoomControls(handle)] : []))" in src
|
||||
assert "attachDetailZoom(clone, axes, zoom, (next) => (zoom = next))" in src
|
||||
assert "layer.append(...clone.childNodes)" not in src # 감싸기 지움(179-3)
|
||||
assert 'clone.querySelector<SVGGElement>("g.b06-chart__plot-layer")' in src
|
||||
assert "createCrossAxes(axisLayer, axes)" in src
|
||||
view = _read("B06_Section_UI_Section_View.ts")
|
||||
assert "const plot = fitted && crossPlotFromBase(fitted, heightPx);" in view
|
||||
assert "return { svg, axes };" in view
|
||||
assert "position: relative;" in css
|
||||
# 173-4 — 그림 = 칸 크기로 새로 그림 · svg 칸 가득
|
||||
assert ".b06-detail__cad > svg {\n display: block;\n width: 100%;\n height: 100%;" in css
|
||||
@@ -102,3 +122,27 @@ def test_빈칸_누름_없음_이름_칸_첫_키():
|
||||
assert 'if (!key || el.hasAttribute("data-empty")) return;' in src
|
||||
assert 'cell("th", groupL, { row: 2 }, keyL)' in src
|
||||
assert 'cell("th", groupM, { row: 2 }, keyM)' in src
|
||||
|
||||
|
||||
def test_표_글자_14px_이름_칸_머리색_빈칸_값칸만():
|
||||
# 179-3 — 표 글자 14px · 이름 칸(th) 바탕 = 데이터 표 머리색 · 빈 칸 바탕 지움은 값 칸(td)만
|
||||
css = _read("B06_Section_UI_Style_Cross_Detail.css")
|
||||
table = css.split(".b06-detail__qtable {", 1)[1].split("}", 1)[0]
|
||||
assert "font-size: var(--text-body-sm);" in table
|
||||
assert (
|
||||
"--b06-detail-head: color-mix(in srgb, var(--color-accent) 6%, var(--color-surface));"
|
||||
in table
|
||||
)
|
||||
head = css.split(".b06-detail__qtable th {", 1)[1].split("}", 1)[0]
|
||||
assert "background: var(--b06-detail-head);" in head and "color: var(--color-text);" in head
|
||||
empty = css.split(".b06-detail__qtable [data-key][data-empty] {", 1)[1].split("}", 1)[0]
|
||||
assert "background" not in empty
|
||||
assert ".b06-detail__qtable td[data-key][data-empty] {\n background: none;" in css
|
||||
assert (
|
||||
".b06-detail__qtable th[data-key][data-empty] {\n background: var(--b06-detail-head);"
|
||||
in css
|
||||
)
|
||||
theme = open(
|
||||
os.path.join(ROOT, "ui_template", "ui_template_theme.css"), encoding="utf-8"
|
||||
).read()
|
||||
assert "--text-body-sm: 14px;" in theme
|
||||
|
||||
@@ -87,7 +87,7 @@ def test_상세_테마_토큰_어두운_고정색_없음():
|
||||
def test_상세_칸_크기로_새로_그림_배선():
|
||||
src = _read("B06_Section_UI_Cross_Detail.ts")
|
||||
assert "const { clientWidth: width, clientHeight: height } = cad;" in src
|
||||
assert "const clone = drawChart(state.section, width, height);" in src
|
||||
assert "const drawn = drawChart(state.section, width, height);" in src
|
||||
assert "host?.chart(section.station_id, widthPx, heightPx)" in src
|
||||
assert "new ResizeObserver(() => state.paint())" in src and "resize.disconnect()" in src
|
||||
# 카드 clip id 와 가름 · 그리는 동안 refresh 되먹임 막음
|
||||
@@ -99,7 +99,12 @@ def test_상세_칸_크기로_새로_그림_배선():
|
||||
assert "base && fitPlotBase(base, heightPx)" in view
|
||||
assert "cardZoomStates.delete(stationId);" in view
|
||||
types = _read("B06_Section_UI_Section_View_Types.ts")
|
||||
assert "detailChart: (stationId: string, widthPx: number, heightPx: number) =>" in types
|
||||
# 179-3 — svg + 축 옵션
|
||||
assert " ) => { svg: SVGSVGElement; axes: CrossAxesOptions | null } | null;" in types
|
||||
assert (
|
||||
" detailChart: (\n stationId: string,\n widthPx: number,\n heightPx: number,"
|
||||
in types
|
||||
)
|
||||
sync = _read("B06_Section_UI_Page_Design_Sync.ts")
|
||||
assert "ctx.view().detailChart(stationId, widthPx, heightPx)" in sync
|
||||
|
||||
|
||||
Reference in New Issue
Block a user