Merge remote-tracking branch 'origin/dev' into sub_laptop_5
This commit is contained in:
@@ -4,6 +4,6 @@
|
||||
|
||||
## 작업 순서
|
||||
|
||||
## 단추
|
||||
## 버튼
|
||||
|
||||
## 주의할 점
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
|
||||
## 작업 순서
|
||||
|
||||
## 단추
|
||||
## 버튼
|
||||
|
||||
## 주의할 점
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
|
||||
## 작업 순서
|
||||
|
||||
## 단추
|
||||
## 버튼
|
||||
|
||||
## 주의할 점
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
|
||||
## 작업 순서
|
||||
|
||||
## 단추
|
||||
## 버튼
|
||||
|
||||
## 주의할 점
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
|
||||
## 작업 순서
|
||||
|
||||
## 단추
|
||||
## 버튼
|
||||
|
||||
## 주의할 점
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
|
||||
## 작업 순서
|
||||
|
||||
## 단추
|
||||
## 버튼
|
||||
|
||||
## 주의할 점
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
|
||||
## 작업 순서
|
||||
|
||||
## 단추
|
||||
## 버튼
|
||||
|
||||
## 주의할 점
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
|
||||
## 작업 순서
|
||||
|
||||
## 단추
|
||||
## 버튼
|
||||
|
||||
## 주의할 점
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* 224-1b 유토 판 「붙어 있음」 판정 — 진짜 높이 나눔(`createHeightCascade`)과 방향키 판정
|
||||
/* 224-1b 유토 판 높이 나눔 · 방향키 판정 — 진짜 높이 나눔(`createHeightCascade`)과 방향키 판정
|
||||
* (`arrowsBelongToProfile` 본문)을 가짜 DOM 위에서 돌려 JSON 으로 냄.
|
||||
* · 열림 && 뿌리 안 = 자리 차지 · 열림 && 새 창(뿌리 밖) = 자리 0 · 인라인 높이 안 씀
|
||||
* · 유토곡선 = 종단 열 아래 자기 판(354-1) — 본문 높이에서 이미 빠져 나눔에 안 낌(입력에도 없음)
|
||||
* · 방향키 = 마지막 누름이 종단 뿌리 · 뗀 유토 · 뗀 배수 판 안이면 종단도 몫 */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
@@ -44,33 +44,27 @@ new Function(
|
||||
)(
|
||||
heightsExports,
|
||||
(id) =>
|
||||
id.includes("MassHaul")
|
||||
? { MASSHAUL_MIN_HEIGHT: 160 }
|
||||
: id.includes("TableOverlay")
|
||||
? { TABLE_OVERLAY_MIN_HEIGHT: 120 }
|
||||
: id.includes("Structures_Marks")
|
||||
? { STRUCTURE_LANE_HEIGHT_PX: 40 }
|
||||
: { frameOf: () => 0 },
|
||||
id.includes("TableOverlay")
|
||||
? { TABLE_OVERLAY_MIN_HEIGHT: 120 }
|
||||
: id.includes("Structures_Marks")
|
||||
? { STRUCTURE_LANE_HEIGHT_PX: 40 }
|
||||
: { frameOf: () => 0 },
|
||||
{ innerHeight: 900 },
|
||||
);
|
||||
|
||||
function run(docked) {
|
||||
function run(tableDesired) {
|
||||
const root = el("b05-route-profile");
|
||||
root.offsetHeight = 500;
|
||||
const body = root.append(el());
|
||||
body.clientHeight = 400;
|
||||
const overlay = el("b05-profile__masshaul-overlay");
|
||||
if (docked) body.append(overlay);
|
||||
else el("popup-body").append(overlay); // 새 창 body
|
||||
const overlay = el("b05-profile__table-overlay");
|
||||
const cascade = heightsExports.createHeightCascade({
|
||||
root,
|
||||
body,
|
||||
massHaul: { overlay, desiredHeight: () => 300, syncHandle() {} },
|
||||
tableOverlay: {
|
||||
overlay: el(),
|
||||
isOpen: () => false,
|
||||
desiredHeight: () => 0,
|
||||
setBottomOffset() {},
|
||||
overlay,
|
||||
isOpen: () => tableDesired > 0,
|
||||
desiredHeight: () => tableDesired,
|
||||
},
|
||||
isMainDragging: () => false,
|
||||
isSubDragging: () => false,
|
||||
@@ -99,8 +93,9 @@ const card = el("b06-cross-card");
|
||||
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
docked: run(true),
|
||||
popped: run(false),
|
||||
tableClosed: run(0),
|
||||
tableFits: run(200),
|
||||
tableShrinks: run(300),
|
||||
arrows: {
|
||||
profile: arrows(profileChild),
|
||||
mass: arrows(massChild),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* 224-1c 유토곡선 · 배수유역 [창 분할] — 진짜 `attachSubPanelPopouts` · `attachProfilePopout`
|
||||
* (design_profile_popout.ts)를 가짜 DOM · 가짜 창 위에서 돌려 JSON 으로 냄.
|
||||
* · 단추 자리 · 글 · 클래스 · 툴팁 · 창 이름 셋 다름
|
||||
* · 뗌 = 새 창 body · 유토 손잡이 숨김 · 인라인 높이 비움 · 배수 rehome
|
||||
* · 붙임(단추 · 새 창 닫힘 · dispose) = 제자리(유토 손잡이 앞 · content 끝)
|
||||
* · 단추 = 두 판 머리줄 맨 앞(유토는 접어도 남는 머리줄 · 354-1)
|
||||
* · 뗌 = 새 창 body · 배수 rehome · 붙임(단추 · 새 창 닫힘 · dispose) = 제자리(종단 열 끝 · content 끝)
|
||||
* · 종단 새 창 안의 단추 = 그 창에서 엶 */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
@@ -122,21 +122,25 @@ new Function("exports", "require", "window", "document", js)(
|
||||
mainDoc,
|
||||
);
|
||||
|
||||
// ---- 종단 뿌리 = [content = [bodyWrap = [유토 판, 유토 손잡이], 배수 판 = [머리줄 = [제목]]]] ----
|
||||
// ---- 종단 뿌리 = [content = [열 = [종단 칸, 유토 판 = [머리줄 = [손잡이]]], 배수 판 = [머리줄 = [제목]]]] ----
|
||||
const slot = el("div");
|
||||
mainDoc.body.append(slot);
|
||||
const root = el("div", "b05-route-profile");
|
||||
slot.append(root);
|
||||
const content = el("div", "b05-route-profile__content");
|
||||
const bodyWrap = el("div");
|
||||
const column = el("div", "b05-route-profile__column");
|
||||
const bodyWrap = el("div", "b05-route-profile__body-wrap");
|
||||
const massOverlay = el("div", "b05-profile__masshaul-overlay");
|
||||
const massHead = el("div", "b05-profile__masshaul-head");
|
||||
const massHandle = el("div", "b05-profile__masshaul-handle");
|
||||
bodyWrap.append(massOverlay, massHandle);
|
||||
massHead.append(massHandle);
|
||||
massOverlay.append(el("div", "ui-resizer"), massHead);
|
||||
column.append(bodyWrap, massOverlay);
|
||||
const drainageRoot = el("aside", "b05-drainage");
|
||||
const header = el("div", "b05-drainage__header");
|
||||
header.append(el("h3"));
|
||||
drainageRoot.append(header);
|
||||
content.append(bodyWrap, drainageRoot);
|
||||
content.append(column, drainageRoot);
|
||||
root.append(content);
|
||||
let rehomes = 0;
|
||||
let disposed = 0;
|
||||
@@ -150,20 +154,18 @@ const profileButton = el("button");
|
||||
mod.attachProfilePopout(profile, slot, profileButton);
|
||||
mod.attachSubPanelPopouts(profile);
|
||||
|
||||
const massButton = massOverlay.children.find((c) => c.tagName === "BUTTON");
|
||||
const massButton = massHead.children[0];
|
||||
const drainButton = header.children[0];
|
||||
const btn = (b) => ({ text: b.textContent, title: b.title, className: b.className, type: b.type });
|
||||
const out = { mass: btn(massButton), drainage: btn(drainButton), profile: btn(profileButton) };
|
||||
|
||||
out.massButtonInHead = massButton.tagName === "BUTTON" && massHead.children[1] === massHandle;
|
||||
// 유토 뗌
|
||||
massOverlay.style.height = "123px";
|
||||
massButton.click();
|
||||
const massPopup = massOverlay.parent?.doc?.defaultView;
|
||||
out.massOpened = {
|
||||
inPopupBody: massOverlay.parent === massPopup?.document.body,
|
||||
isPopout: massOverlay.classList.contains("is-popout"),
|
||||
handleHidden: massHandle.hidden,
|
||||
inline: massOverlay.style.height,
|
||||
text: massButton.textContent,
|
||||
docTitle: massPopup?.document.title,
|
||||
};
|
||||
@@ -175,11 +177,10 @@ out.drainOpened = {
|
||||
rehomes,
|
||||
text: drainButton.textContent,
|
||||
};
|
||||
// 유토 새 창 닫힘 → 제자리(손잡이 바로 앞)
|
||||
// 유토 새 창 닫힘 → 제자리(종단 열 끝)
|
||||
massPopup.fire("pagehide");
|
||||
out.massClosed = {
|
||||
order: bodyWrap.children.map((c) => c.className),
|
||||
handleHidden: massHandle.hidden,
|
||||
order: column.children.map((c) => c.className),
|
||||
isPopout: massOverlay.classList.contains("is-popout"),
|
||||
text: massButton.textContent,
|
||||
};
|
||||
@@ -205,10 +206,8 @@ profile.dispose();
|
||||
out.disposed = {
|
||||
closed: [profilePopup.closed, massPopup2.closed, drainPopup2.closed],
|
||||
profileInSlot: root.parent === slot,
|
||||
massBeforeHandle:
|
||||
bodyWrap.children.indexOf(massOverlay) === bodyWrap.children.indexOf(massHandle) - 1,
|
||||
massInColumnEnd: column.children[column.children.length - 1] === massOverlay,
|
||||
drainInContent: drainageRoot.parent === content,
|
||||
handleHidden: massHandle.hidden,
|
||||
original: disposed,
|
||||
};
|
||||
out.opened = opened;
|
||||
|
||||
@@ -65,6 +65,17 @@ const design = {
|
||||
{ offset_m: -7, elevation_m: 97.2 },
|
||||
],
|
||||
};
|
||||
// 356-1 양측절토 — 우(낮은 쪽) 노면 끝도 절토 → 노면을 늘여 원지반과 만난 점(tilt_reach) · 그 너머 원지반 오름
|
||||
const bothCut = {
|
||||
...design,
|
||||
design_line: [
|
||||
...design.design_line.slice(0, 3),
|
||||
{ offset_m: -4, elevation_m: 99.8 },
|
||||
{ offset_m: -4.5, elevation_m: 99.775 },
|
||||
{ offset_m: -6, elevation_m: 100.5 },
|
||||
],
|
||||
tilt_reach: { side: "right", offset_m: -4.5, elevation_m: 99.775 },
|
||||
};
|
||||
const svg = new Node("g");
|
||||
const group = draw.appendUnderdrainOverlay(
|
||||
svg,
|
||||
@@ -96,6 +107,7 @@ process.stdout.write(
|
||||
attached,
|
||||
sections: sections.map((s) => s.underdrain ?? null),
|
||||
outline: draw.underdrainOutline(design, 1),
|
||||
bothCut: draw.underdrainOutline(bothCut, 1),
|
||||
noHeight: draw.underdrainOutline(design, null),
|
||||
drew: !!group,
|
||||
bodyId: body?.attrs["data-structure-id"] ?? null,
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/* 354-2 종단 기준 숨김 — 진짜 `readVisible`(B05 유토곡선) · `normalizeVisibleBasis` ·
|
||||
* `computeMassHaulSeries`(공용)를 잘라 돌려 JSON 으로 냄.
|
||||
* · 저장된 범례 = 종단 → 횡단으로 맞춤 · 분배 켬은 그대로
|
||||
* · 기준 ["cross"] = 횡단 계열 하나 · 기본(묶음 단계) = 두 기준 그대로 */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), "utf8");
|
||||
const transpile = (code) =>
|
||||
ts.transpileModule(code, {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const block = (src, head) => {
|
||||
const at = src.indexOf(head);
|
||||
if (at < 0) throw new Error(`없음: ${head}`);
|
||||
return src.slice(at, src.indexOf("\n}", at) + 2).replace(/^export /, "");
|
||||
};
|
||||
|
||||
const util = read("common_util", "common_util_mass_haul.ts");
|
||||
const mass = read("B05_Profile", "B05_Profile_UI_Profile_MassHaul.ts");
|
||||
let stored = null;
|
||||
const src = [
|
||||
'const SERIES_BASES = ["cross", "longitudinal"];',
|
||||
'const DEFAULT_VISIBLE = ["cross"];',
|
||||
block(util, "export function normalizeVisibleBasis("),
|
||||
block(mass, "function readVisible(): Set<string> {"),
|
||||
block(util, "export function computeMassHaulSeries("),
|
||||
].join("\n");
|
||||
const api = new Function(
|
||||
"readStateRaw",
|
||||
"designedSections",
|
||||
"crossAreaSamples",
|
||||
"longitudinalAreaSamples",
|
||||
"integrate",
|
||||
`${transpile(src)}; return { readVisible, computeMassHaulSeries };`,
|
||||
)(
|
||||
() => stored,
|
||||
(sections) => sections,
|
||||
() => [{ basis: "cross" }],
|
||||
() => [{ basis: "longitudinal" }],
|
||||
(samples) => ({ from: samples[0].basis }),
|
||||
);
|
||||
|
||||
const visible = (raw) => ((stored = raw), [...api.readVisible()].sort());
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
longitudinal: visible('["longitudinal"]'),
|
||||
longitudinalBalance: visible('["longitudinal","balance"]'),
|
||||
cross: visible('["cross"]'),
|
||||
empty: visible(null),
|
||||
broken: visible("{"),
|
||||
crossOnly: api.computeMassHaulSeries({}, [], {}, undefined, ["cross"]).map((s) => s.key),
|
||||
both: api.computeMassHaulSeries({}, [], {}).map((s) => s.key),
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,121 @@
|
||||
/* 354-3 스크롤 동기화 메아리 재현 — 진짜 휠 이동(`attachWheelHorizontalScroll`)과 진짜 잇기
|
||||
* (`linkScrollLeft`)를 가짜 창 · 가짜 스크롤러 위에서 돌려 JSON 으로 냄.
|
||||
* 가짜 창은 브라우저 차례 그대로 — scrollLeft 가 바뀌면 scroll 은 **다음 틀**에 오고(틀 앞머리),
|
||||
* 그 뒤 화면 틀 콜백이 돎. 옛 잇기(동기 플래그)와 새 잇기를 같은 휠로 견줌.
|
||||
* 경우 넷 = 표 켬/끔 × 분배(유토 전체 맞춤) 켬/끔 · 휠을 굴리는 쪽 = 종단 · 유토 둘. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const js = ts.transpileModule(
|
||||
fs.readFileSync(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Profile_Wheel.ts"), "utf8"),
|
||||
{ compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 } },
|
||||
).outputText;
|
||||
|
||||
function fakeWindow() {
|
||||
const win = { now: 0, frames: [], seq: 0, pending: new Set() };
|
||||
win.requestAnimationFrame = (fn) => (win.frames.push({ id: ++win.seq, fn }), win.seq);
|
||||
win.cancelAnimationFrame = (id) => (win.frames = win.frames.filter((f) => f.id !== id));
|
||||
win.getSelection = () => ({ removeAllRanges() {} });
|
||||
win.frame = () => {
|
||||
win.now += 16;
|
||||
const targets = [...win.pending];
|
||||
win.pending.clear();
|
||||
targets.forEach((el) => el.fire("scroll"));
|
||||
const due = win.frames;
|
||||
win.frames = [];
|
||||
due.forEach((f) => f.fn(win.now));
|
||||
};
|
||||
return win;
|
||||
}
|
||||
|
||||
function scroller(win, name, { hidden = false, scrollWidth = 6000 } = {}) {
|
||||
const calls = {};
|
||||
let left = 0;
|
||||
const el = {
|
||||
name,
|
||||
ownerDocument: { defaultView: win },
|
||||
clientWidth: hidden ? 0 : 1000,
|
||||
scrollWidth: hidden ? 0 : scrollWidth,
|
||||
scrollTop: 0,
|
||||
style: {},
|
||||
addEventListener: (type, fn) => (calls[type] ??= []).push(fn),
|
||||
removeEventListener() {},
|
||||
hasPointerCapture: () => false,
|
||||
fire: (type, ev = {}) => {
|
||||
const e = { preventDefault() {}, ...ev };
|
||||
(calls[type] ?? []).forEach((fn) => fn(e));
|
||||
},
|
||||
};
|
||||
Object.defineProperty(el, "scrollLeft", {
|
||||
get: () => left,
|
||||
set: (v) => {
|
||||
const next = Math.min(Math.max(0, v), Math.max(0, el.scrollWidth - el.clientWidth));
|
||||
if (next !== left) win.pending.add(el);
|
||||
left = next;
|
||||
},
|
||||
});
|
||||
return el;
|
||||
}
|
||||
|
||||
/** 옛 잇기 — 유토곡선 · 표가 각자 들고 있던 동기 플래그 꼴 그대로. */
|
||||
function oldLink(a, b, paused = () => false) {
|
||||
let syncing = false;
|
||||
const mirror = (from, to) =>
|
||||
from.addEventListener("scroll", () => {
|
||||
if (syncing || paused()) return;
|
||||
syncing = true;
|
||||
to.scrollLeft = from.scrollLeft;
|
||||
syncing = false;
|
||||
});
|
||||
mirror(a, b);
|
||||
mirror(b, a);
|
||||
}
|
||||
|
||||
function run(kind, { table, balance, driver }) {
|
||||
const win = fakeWindow();
|
||||
const wheel = { exports: {} };
|
||||
new Function("exports", "module", "require", "window", js)(
|
||||
wheel.exports,
|
||||
wheel,
|
||||
() => ({ viewOf: () => win }),
|
||||
win,
|
||||
);
|
||||
const body = scroller(win, "body");
|
||||
// 분배 켬 = 유토곡선 전체 맞춤(스크롤 없음) · 잇기는 멈춤
|
||||
const mass = scroller(win, "mass", { scrollWidth: balance ? 1000 : 6000 });
|
||||
const tableEl = scroller(win, "table", { hidden: !table });
|
||||
const link = kind === "old" ? oldLink : wheel.exports.linkScrollLeft;
|
||||
link(body, mass, () => balance);
|
||||
link(body, tableEl);
|
||||
const lead = driver === "mass" && !balance ? mass : body;
|
||||
[body, mass, tableEl].forEach((el) => wheel.exports.attachWheelHorizontalScroll(el));
|
||||
const trace = [];
|
||||
for (let i = 0; i < 60; i += 1) {
|
||||
if (i < 20 && i % 2 === 0) lead.fire("wheel", { deltaY: 120 });
|
||||
win.frame();
|
||||
trace.push(body.scrollLeft);
|
||||
}
|
||||
let back = 0;
|
||||
for (let i = 1; i < trace.length; i += 1) if (trace[i] < trace[i - 1] - 0.01) back += 1;
|
||||
return {
|
||||
back,
|
||||
end: Math.round(body.scrollLeft),
|
||||
lead: Math.round(lead.scrollLeft),
|
||||
mass: Math.round(mass.scrollLeft),
|
||||
table: Math.round(tableEl.scrollLeft),
|
||||
};
|
||||
}
|
||||
|
||||
const out = {};
|
||||
for (const kind of ["old", "new"])
|
||||
for (const driver of ["body", "mass"])
|
||||
for (const table of [true, false])
|
||||
for (const balance of [false, true])
|
||||
out[`${kind}|${driver}|table=${table}|balance=${balance}`] = run(kind, {
|
||||
table,
|
||||
balance,
|
||||
driver,
|
||||
});
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -185,6 +185,12 @@ let out0 = null;
|
||||
{ 번: 7, 아래: [8] },
|
||||
{ 번: 8, 아래: [7] },
|
||||
]),
|
||||
linked: [
|
||||
[2, 2],
|
||||
[1, 2],
|
||||
[0, 0],
|
||||
[3, 2],
|
||||
].map(sheet.allLinked),
|
||||
},
|
||||
};
|
||||
// 길 — 만들기(줄 · 통째) · 호표 하나(만들 층) · 미리 셈 · 장부
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""224-1b(PLAN 325-2) 유토곡선 판 「붙어 있음」 판정 · 방향키.
|
||||
"""224-1b(PLAN 325-2 · 354-1) 유토곡선 판 높이 나눔 · 방향키.
|
||||
|
||||
- 높이 나눔 · 끌기는 「열림 && 종단 뿌리 안」만 자리를 줌 — 새 창으로 뗀 유토 판은 메인 자리 0 ·
|
||||
임시 축소 인라인 높이도 안 씀(뗀 창 높이가 굳지 않게).
|
||||
- 유토곡선 = 종단 열 아래 자기 판(354-1) — 높이 나눔 · 끌기에 안 낌(본문 높이에서 이미 빠짐) ·
|
||||
인라인 임시 축소도 안 씀. 나눔은 그래프 · 표 둘.
|
||||
- 뗀 유토 판(`is-popout`)은 메인 보이는 구간 세로 범위(viewRange)를 안 씀 — 노선 전체 세로.
|
||||
- 방향키 ←→ 판정은 마지막 누름이 종단 뿌리 · 뗀 유토 · 뗀 배수 판 안이면 종단도 몫.
|
||||
|
||||
@@ -34,13 +34,12 @@ def r():
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_붙은_유토는_자리를_차지(r):
|
||||
"""본문 400 − 알약 40 = 360 · 유토 300 은 예산 247 넘어 247 로 줄고 그래프 100."""
|
||||
assert r["docked"] == {"chartHeight": 100, "inline": "247px"}
|
||||
|
||||
|
||||
def test_뗀_유토는_자리_0_인라인_없음(r):
|
||||
assert r["popped"] == {"chartHeight": 347, "inline": ""}
|
||||
def test_유토_판은_나눔에_안_낌(r):
|
||||
"""본문 400 − 알약 40 = 360 · 손잡이 띠 13 → 표 없음 그래프 347."""
|
||||
assert r["tableClosed"] == {"chartHeight": 347, "inline": ""}
|
||||
assert r["tableFits"] == {"chartHeight": 147, "inline": ""}
|
||||
# 표 300 은 예산 247 넘어 247 로 줄고 그래프 100
|
||||
assert r["tableShrinks"] == {"chartHeight": 100, "inline": "247px"}
|
||||
|
||||
|
||||
def test_방향키_판정(r):
|
||||
@@ -53,12 +52,15 @@ def test_방향키_판정(r):
|
||||
}
|
||||
|
||||
|
||||
def test_끌기도_같은_판정():
|
||||
def test_끌기도_유토_판_안_셈():
|
||||
drag = (B05 / "B05_Profile_UI_Profile_Drag.ts").read_text(encoding="utf-8")
|
||||
assert "massHaul.overlay.hidden" in drag # 판정 함수 한 곳에만
|
||||
assert drag.count("massHaul.overlay.hidden") == 1
|
||||
assert drag.count("massDocked()") == 3
|
||||
assert "root.contains(massHaul.overlay)" in drag
|
||||
heights = (B05 / "B05_Profile_UI_Profile_Heights.ts").read_text(encoding="utf-8")
|
||||
for src in (drag, heights):
|
||||
assert "massDocked" not in src and "MASSHAUL_MIN_HEIGHT" not in src
|
||||
assert "mass:" not in src
|
||||
# 유토 판 리사이저 끌기 출처만 봄
|
||||
assert drag.count("massHaul.overlay") == 1
|
||||
assert "massHaul" not in heights
|
||||
|
||||
|
||||
def test_뗀_유토는_노선_전체_세로():
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""224-1c(PLAN 325-3) 유토곡선 · 배수유역 [창 분할] — 종단 [창 분할]과 같은 양식 · 따로 새 창.
|
||||
|
||||
- 단추 = 유토 판 좌상단 · 배수 머리줄 맨 앞 · 글 [창 분할]/[창 결합] · 툴팁에 판 이름.
|
||||
- 단추 = 두 판 머리줄 맨 앞(유토는 접어도 남는 머리줄 · 354-1) · 글 [창 분할]/[창 결합] · 툴팁에 판 이름.
|
||||
- 창 이름 셋 다름(같으면 앞 창 body 를 덮어 조각이 사라짐).
|
||||
- 뗌 = 새 창 body · 유토 손잡이 숨김 · 인라인 높이 비움 · 배수 지도 관찰 rehome.
|
||||
- 붙임(단추 · 새 창 닫힘 · 페이지 떠남) = 제자리 — 유토 손잡이 바로 앞 · 배수 content 끝.
|
||||
- 뗌 = 새 창 body · 뗀 유토 판 손잡이 · 리사이저는 CSS 로 감춤 · 배수 지도 관찰 rehome.
|
||||
- 붙임(단추 · 새 창 닫힘 · 페이지 떠남) = 제자리 — 종단 열 끝(종단 칸 아래) · 배수 content 끝.
|
||||
- 종단 새 창 안에서 다시 떼면 그 창이 새 창을 엶(메인 창 팝업 차단 피함).
|
||||
|
||||
`helper_224_1c_subpanel_popout.cjs` 가 진짜 TS 를 가짜 DOM · 가짜 창 위에서 돌림.
|
||||
@@ -51,14 +51,13 @@ def test_단추_글_툴팁_클래스(r):
|
||||
"type": "button",
|
||||
}
|
||||
assert "b05-route-profile__popout" in r["profile"]["className"]
|
||||
assert r["massButtonInHead"] is True
|
||||
|
||||
|
||||
def test_유토_뗌(r):
|
||||
assert r["massOpened"] == {
|
||||
"inPopupBody": True,
|
||||
"isPopout": True,
|
||||
"handleHidden": True,
|
||||
"inline": "",
|
||||
"text": "창 결합",
|
||||
"docTitle": "유토곡선 — Aislo",
|
||||
}
|
||||
@@ -70,8 +69,7 @@ def test_배수_뗌은_rehome(r):
|
||||
|
||||
def test_새_창_닫힘과_단추로_제자리(r):
|
||||
assert r["massClosed"] == {
|
||||
"order": ["b05-profile__masshaul-overlay", "b05-profile__masshaul-handle"],
|
||||
"handleHidden": False,
|
||||
"order": ["b05-route-profile__body-wrap", "b05-profile__masshaul-overlay"],
|
||||
"isPopout": False,
|
||||
"text": "창 분할",
|
||||
}
|
||||
@@ -90,9 +88,8 @@ def test_페이지_떠나면_셋_다_닫고_제자리(r):
|
||||
assert r["disposed"] == {
|
||||
"closed": [True, True, True],
|
||||
"profileInSlot": True,
|
||||
"massBeforeHandle": True,
|
||||
"massInColumnEnd": True,
|
||||
"drainInContent": True,
|
||||
"handleHidden": False,
|
||||
"original": 1,
|
||||
}
|
||||
|
||||
@@ -104,4 +101,5 @@ def test_배선과_양식():
|
||||
block = re.search(r"\n\.b05-subpanel__popout \{([^}]*)\}", css).group(1)
|
||||
assert "var(--btn-bar-padding)" in block and "var(--btn-bar-font-size)" in block
|
||||
assert ".b05-profile__masshaul-overlay.is-popout," in css
|
||||
assert ".b05-profile__masshaul-overlay.is-popout .b05-profile__masshaul-handle," in css
|
||||
assert ".b05-drainage.is-popout {" in css
|
||||
|
||||
@@ -67,7 +67,8 @@ def test_높이_맞춤(r):
|
||||
|
||||
def test_표_유토곡선_크기_끌기_다시_그림(r):
|
||||
assert r["table"] == {"changed": 1, "handles": 2, "mainTimers": 0}
|
||||
assert r["massHaul"] == {"changed": 1, "handles": 2, "mainTimers": 0}
|
||||
# 유토 판 손잡이는 머리줄에 앉아 끌기 때 따라 옮길 것이 없음(354-1)
|
||||
assert r["massHaul"] == {"changed": 1, "handles": 0, "mainTimers": 0}
|
||||
|
||||
|
||||
def test_유토곡선_세로창_미끄러짐(r):
|
||||
|
||||
@@ -131,6 +131,22 @@ def test_형상_옵셋선_성토_절토와_만남(ts):
|
||||
assert ts["drew"] is True and ts["stones"] > 0
|
||||
|
||||
|
||||
def test_양측절토_늘인_노면_원지반_교점에서_끊음(ts):
|
||||
"""356-1 — 낮은 쪽 노면 끝도 절토(tilt_reach) = reach 에서 수직으로 끊음 · 너머 원지반 따라가지 않음."""
|
||||
outline = [(p["offset"], p["elevation"]) for p in ts["bothCut"]]
|
||||
expected = [
|
||||
(3, 100.15),
|
||||
(-3, 99.85),
|
||||
(-4, 99.8),
|
||||
(-4.5, 99.775), # 늘인 노면 ∩ 원지반(tilt_reach)
|
||||
(-4.5, 98.775), # reach 아래 옵셋선
|
||||
(1.846154, 99.092308),
|
||||
]
|
||||
assert len(outline) == len(expected)
|
||||
for got, want in zip(outline, expected):
|
||||
assert got == pytest.approx(want, abs=1e-5)
|
||||
|
||||
|
||||
def test_횡단도_B07_이_맹암거를_그림():
|
||||
view = (ROOT / "B06_Section" / "B06_Section_UI_Cross_View.ts").read_text(encoding="utf-8")
|
||||
assert (
|
||||
|
||||
@@ -249,7 +249,7 @@ def test_창_열_제목_강조_줄_자리():
|
||||
)
|
||||
assert out["boxes"] == ["bill"] and out["n"] == 1
|
||||
assert out["cls"] == "b09-estimation b09-popup ui-popup" # 창 안 = b09 표 꼴(빨강 테두리)
|
||||
assert out["head"] == ["설계내역서", "탭으로", "×"]
|
||||
assert out["head"] == ["설계내역서", "탭 이동", "×"]
|
||||
assert out["rows"] == ["b1", "b2", "b3"] # 그 줄 + 자손
|
||||
assert out["rowCls"] == ["is-title is-pick is-pick-top", "is-pick", "is-pick is-pick-end"]
|
||||
assert out["columns"] is True and out["caption"] == "설계내역서" # 열 제목(머리 여러 줄) 그대로
|
||||
@@ -291,7 +291,7 @@ def test_닫기_탭으로_새_누름_주소():
|
||||
"\nconst a = boxes();"
|
||||
"\nopen(); tables[2].onCellClick(0, 'name'); open('b4');" # 화면 새 누름 = 있던 창 모두 닫고 새로
|
||||
"\nconst b = [boxes(), rows(4)];"
|
||||
"\nhead(0)[1].click();" # [탭으로] = 옮기고 모두 닫음
|
||||
"\nhead(0)[1].click();" # [탭 이동] = 옮기고 모두 닫음
|
||||
"\nconst c = [boxes(), [...gone]];"
|
||||
"\nopen(); hashFns.forEach((f) => f());"
|
||||
"\nconst d = boxes();"
|
||||
|
||||
@@ -297,8 +297,11 @@ def test_추천_저장본_종류당_최근_두_벌(tmp_path: Path) -> None:
|
||||
assert read_stored(tmp_path, "a") is not None # 꺼내면 최근으로
|
||||
time.sleep(0.02)
|
||||
write_stored(tmp_path, "c", "p", request, {"n": 3}, False)
|
||||
time.sleep(0.02)
|
||||
write_stored(tmp_path, "d", "p", request, {"n": 4}, False)
|
||||
names = sorted(path.stem for path in store_dir(tmp_path).glob("*.json"))
|
||||
assert names == ["a", "c", "g"]
|
||||
# 기본값(a)은 따로 셈 — 사용자 시도 b · c · d 중 옛 b 만 지움(351-1)
|
||||
assert names == ["a", "c", "d", "g"]
|
||||
|
||||
|
||||
# ── 횡단 상세 — GET 은 안 지움 · ETag(R26) ───────────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
"""351-1 노선 추천 저장 빈틈 — 재확정 체인 · 기본값 지킴 · 저장된 제약 · B01 사용 범위.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_351_1_recommend_gaps.py -q
|
||||
|
||||
체인 뒤 첫 누름 = 저장본(기본값 · 저장된 제약 둘 다) · 「최근 2벌」 정리가 기본값을 안 밀어냄 ·
|
||||
사용 범위를 바꾸면 예상노선을 원본에서 다시 자르고 미리 셈.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from B05_Profile import B05_Profile_Router_RouteInitial as router
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial import write_initial_choice
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Store import (
|
||||
DEFAULT_MODES,
|
||||
is_default,
|
||||
store_dir,
|
||||
write_stored,
|
||||
)
|
||||
from common_util.common_util_route_geometry import (
|
||||
expected_route_csv_path,
|
||||
load_design_route,
|
||||
planned_route_working_path,
|
||||
write_route_csv,
|
||||
)
|
||||
from test_224_3_recommend_store import _ask, wired # noqa: F401 — 픽스처
|
||||
|
||||
|
||||
def _saved(root: Path) -> list[dict]:
|
||||
return [json.loads(p.read_text(encoding="utf-8")) for p in store_dir(root).glob("*.json")]
|
||||
|
||||
|
||||
def test_저장된_제약으로도_미리_셈_첫_누름_저장본(wired) -> None: # noqa: F811
|
||||
criteria = {"min_radius_m": 15.0, "max_grade_pct": 12.0}
|
||||
write_initial_choice(
|
||||
wired.root, "weighted", {"expected": 40, "earthwork": 30, "grade": 30}, criteria
|
||||
)
|
||||
asyncio.run(router.precompute_defaults(uuid4()))
|
||||
assert wired.count == 2 * len(DEFAULT_MODES)
|
||||
assert all(item["default"] for item in _saved(wired.root))
|
||||
# 모달은 저장된 제약 · 비중으로 열림 — 셈 없이 저장본
|
||||
hit = _ask(
|
||||
mode="weighted", weights={"expected": 40, "earthwork": 30, "grade": 30}, criteria=criteria
|
||||
)
|
||||
gentle = _ask(mode="gentle", criteria=criteria)
|
||||
plain = _ask(mode="earthwork")
|
||||
assert hit["stored"] and gentle["stored"] and plain["stored"]
|
||||
assert wired.count == 10
|
||||
# 다시 불러도 셈 0
|
||||
asyncio.run(router.precompute_defaults(uuid4()))
|
||||
assert wired.count == 10
|
||||
|
||||
|
||||
def test_저장된_제약_없으면_기본값만(wired) -> None: # noqa: F811
|
||||
write_initial_choice(wired.root, "gentle", None, None)
|
||||
asyncio.run(router.precompute_defaults(uuid4()))
|
||||
assert wired.count == len(DEFAULT_MODES)
|
||||
|
||||
|
||||
def test_기본값은_최근_두_벌_정리에서_안_밀림(tmp_path: Path) -> None:
|
||||
request = {"mode": "earthwork", "terrain": None}
|
||||
write_stored(tmp_path, "d", "p", request, {}, True)
|
||||
for name in ("a", "b", "c"):
|
||||
time.sleep(0.02)
|
||||
write_stored(tmp_path, name, "p", request, {}, False)
|
||||
names = sorted(path.stem for path in store_dir(tmp_path).glob("*.json"))
|
||||
assert names == ["b", "c", "d"]
|
||||
|
||||
|
||||
def test_기본값_판정() -> None:
|
||||
saved = {
|
||||
"criteria": {"min_radius_m": 15.0},
|
||||
"weights": {"expected": 40.0, "earthwork": 30.0, "grade": 30.0},
|
||||
}
|
||||
assert is_default("gentle", None, None, None)
|
||||
assert is_default("gentle", None, {"min_radius_m": None}, None)
|
||||
assert not is_default("gentle", None, None, "las")
|
||||
assert not is_default("gentle", None, {"min_radius_m": 15.0}, None)
|
||||
assert is_default("gentle", None, {"min_radius_m": 15.0}, None, saved)
|
||||
assert not is_default("gentle", None, {"min_radius_m": 16.0}, None, saved)
|
||||
weights = {"expected": 40.0, "earthwork": 30.0, "grade": 30.0}
|
||||
assert is_default("weighted", weights, {"min_radius_m": 15.0}, None, saved)
|
||||
assert not is_default("weighted", None, {"min_radius_m": 15.0}, None, saved)
|
||||
|
||||
|
||||
def test_두_체인_끝에서_미리_셈_예약() -> None:
|
||||
source = Path("B03_FileInput/B03_FileInput_Service_Chain.py").read_text(encoding="utf-8")
|
||||
redesign = source[source.index("async def run_redesign_chain(") :]
|
||||
upload = source[: source.index("async def run_redesign_chain(")]
|
||||
assert "schedule_precompute(project_id)" in upload
|
||||
# 재확정 체인 성공 끝(완료 로그 앞)
|
||||
done = redesign.index("재확정 체인 완료")
|
||||
assert "schedule_precompute(project_id)" in redesign[:done]
|
||||
b01 = Path("B01_Dashboard/B01_Dashboard_Router.py").read_text(encoding="utf-8")
|
||||
assert "refresh_expected_route(" in b01
|
||||
|
||||
|
||||
def test_사용_범위_비교() -> None:
|
||||
from decimal import Decimal
|
||||
|
||||
from B01_Dashboard.B01_Dashboard_Router import _range_of
|
||||
|
||||
assert _range_of({"route_start_m": Decimal("10.0"), "route_end_m": None}) == (10.0, None)
|
||||
assert _range_of({"route_start_m": 10.0, "route_end_m": None}) == _range_of(
|
||||
{"route_start_m": Decimal("10"), "route_end_m": None}
|
||||
)
|
||||
assert _range_of({"route_start_m": 10.0}) != _range_of({"route_start_m": 20.0})
|
||||
|
||||
|
||||
def _source_project(root: Path) -> None:
|
||||
write_route_csv(
|
||||
root / "B03_FileInput/input/route.csv",
|
||||
[{"x": float(x), "y": 0.0} for x in range(0, 101, 10)],
|
||||
)
|
||||
# 설계 정본(수정본) · 예상노선이 따로 있음 — 원본에서 다시 읽어야 함
|
||||
write_route_csv(planned_route_working_path(root), [{"x": 0.0, "y": 5.0}, {"x": 9.0, "y": 5.0}])
|
||||
write_route_csv(expected_route_csv_path(root), [{"x": 0.0, "y": 0.0}, {"x": 100.0, "y": 0.0}])
|
||||
|
||||
|
||||
def test_원본에서_다시_읽기(tmp_path: Path) -> None:
|
||||
_source_project(tmp_path)
|
||||
surface = {"source_filter": "csf", "method": "dtm", "smooth": False}
|
||||
stored = load_design_route(tmp_path, surface, (20.0, 80.0))
|
||||
assert [v.y for v in stored.vertices] == [5.0, 5.0] # 정본이 먼저
|
||||
fresh = load_design_route(tmp_path, surface, (20.0, 80.0), from_source=True)
|
||||
xs = [round(v.x, 3) for v in fresh.vertices]
|
||||
assert xs[0] == 20.0 and xs[-1] == 80.0
|
||||
|
||||
|
||||
def test_사용_범위_바꾸면_예상노선_다시_쓰고_미리_셈(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
import B03_FileInput.B03_FileInput_Repository as repo
|
||||
import common_util.common_util_storage as storage
|
||||
import common_util.common_util_surface_confirmation as surface
|
||||
|
||||
_source_project(tmp_path)
|
||||
calls: list[str] = []
|
||||
|
||||
class _Cursor:
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_):
|
||||
return None
|
||||
|
||||
async def execute(self, *_):
|
||||
return None
|
||||
|
||||
async def fetchone(self):
|
||||
return (30.0, 70.0)
|
||||
|
||||
class _Connection:
|
||||
def cursor(self):
|
||||
return _Cursor()
|
||||
|
||||
class _Acquire:
|
||||
async def __aenter__(self):
|
||||
return _Connection()
|
||||
|
||||
async def __aexit__(self, *_):
|
||||
return None
|
||||
|
||||
class _Pool:
|
||||
def acquire(self):
|
||||
return _Acquire()
|
||||
|
||||
async def storage_path(*_):
|
||||
return "x"
|
||||
|
||||
async def surface_params(*_):
|
||||
return {"source_filter": "csf", "method": "dtm", "smooth": False}
|
||||
|
||||
async def precompute(_pid):
|
||||
calls.append("precompute")
|
||||
|
||||
monkeypatch.setattr(router, "get_db_pool", lambda: _Pool())
|
||||
monkeypatch.setattr(repo, "get_project_storage_relative_path", storage_path)
|
||||
monkeypatch.setattr(storage, "resolve_stored_project_path", lambda _p: str(tmp_path))
|
||||
monkeypatch.setattr(surface, "get_surface_confirmation_params", surface_params)
|
||||
monkeypatch.setattr(router, "precompute_defaults", precompute)
|
||||
asyncio.run(router.refresh_expected_route(uuid4()))
|
||||
rows = expected_route_csv_path(tmp_path).read_text(encoding="utf-8").splitlines()
|
||||
assert rows[1].split(",")[1:] == ["30.0", "0.0"] and rows[-1].split(",")[1:] == ["70.0", "0.0"]
|
||||
assert calls == ["precompute"]
|
||||
# 체인 전(예상노선 없음)은 건너뜀
|
||||
expected_route_csv_path(tmp_path).unlink()
|
||||
asyncio.run(router.refresh_expected_route(uuid4()))
|
||||
assert calls == ["precompute"] and not expected_route_csv_path(tmp_path).exists()
|
||||
@@ -0,0 +1,85 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""354-1 유토곡선 = 배수유역도처럼 자기 판(요구 235-7).
|
||||
|
||||
- 종단 열(`b05-route-profile__column`) = [종단 칸(bodyWrap), 유토 판] · content 안이라 종단 [창 분할]
|
||||
(`.is-popout > :not(content)` 숨김)에도 같이 감 · B05 · B06 이 같은 `design_profile` 하나.
|
||||
- 유토 판은 흐름 칸(겹침 아님) · 자기 리사이저 높이 · 접으면 머리줄만(손잡이 · [창 분할] 닿음).
|
||||
- 높이 나눔 · 표 쌓기에서 빠짐(표 bottomOffset 없음) · X 동기화는 그대로.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = Path(HERE).parents[1]
|
||||
|
||||
|
||||
def read(rel: str) -> str:
|
||||
return (ROOT / rel).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def css_block(css: str, selector: str) -> str:
|
||||
m = re.search(r"(?:^|\n)" + re.escape(selector) + r" \{([^}]*)\}", css)
|
||||
assert m, selector
|
||||
return m.group(1)
|
||||
|
||||
|
||||
def test_종단_열_아래_자기_칸():
|
||||
overlays = read("A00_Common/design/design_profile_overlays.ts")
|
||||
assert 'column.className = "b05-route-profile__column";' in overlays
|
||||
assert "column.append(bodyWrap, massHaul.overlay);" in overlays
|
||||
assert "bodyWrap.append(massHaul.overlay" not in overlays
|
||||
assert "massHaul.attachScrollSync(body);" in overlays
|
||||
profile = read("A00_Common/design/design_profile.ts")
|
||||
assert "content.append(column);" in profile
|
||||
assert sum(1 for _ in profile.splitlines()) <= 700
|
||||
|
||||
|
||||
def test_종단_창_분할에도_안_숨음():
|
||||
page = read("A00_Common/design/design_page.css")
|
||||
assert ".b05-route-profile.is-popout > :not(.b05-route-profile__content)" in page
|
||||
# 유토 판은 뿌리 바로 아래가 아니라 content > column 안
|
||||
overlays = read("A00_Common/design/design_profile_overlays.ts")
|
||||
assert "root.append" not in overlays
|
||||
|
||||
|
||||
def test_흐름_칸_자기_높이_접으면_머리줄():
|
||||
css = read("B05_Profile/B05_Profile_UI_Style_MassHaul.css")
|
||||
panel = css_block(css, ".b05-profile__masshaul-overlay")
|
||||
assert "position: relative;" in panel and "absolute" not in panel
|
||||
assert "height: var(--b05-masshaul-height, 280px);" in panel
|
||||
assert "flex: none;" in panel
|
||||
column = css_block(css, ".b05-route-profile__column")
|
||||
assert "flex-direction: column;" in column
|
||||
hide = css_block(
|
||||
css,
|
||||
".b05-profile__masshaul-overlay.is-collapsed:not(.is-popout) > :not(.b05-profile__masshaul-head)",
|
||||
)
|
||||
assert "display: none;" in hide
|
||||
assert "display: none;" in css_block(
|
||||
css, ".b05-route-profile.is-collapsed .b05-profile__masshaul-overlay"
|
||||
)
|
||||
mass = read("B05_Profile/B05_Profile_UI_Profile_MassHaul.ts")
|
||||
assert "head.append(handle);" in mass
|
||||
assert "overlay.append(resizer.root, head, bar, scroll, legendLayer, pendingChip);" in mass
|
||||
assert 'overlay.classList.toggle("is-collapsed", !next);' in mass
|
||||
assert "overlay.hidden" not in mass
|
||||
# 새 창으로 뗀 판은 접힘이어도 그림
|
||||
assert 'const shown = (): boolean => open || overlay.classList.contains("is-popout");' in mass
|
||||
|
||||
|
||||
def test_창_분할_단추는_머리줄_맨_앞():
|
||||
popout = read("A00_Common/design/design_profile_popout.ts")
|
||||
assert "massHead.prepend(button);" in popout
|
||||
assert "home: () => column?.append(massOverlay)," in popout
|
||||
page = read("A00_Common/design/design_page.css")
|
||||
assert ".b05-profile__masshaul-overlay > .b05-subpanel__popout" not in page
|
||||
|
||||
|
||||
def test_높이_나눔_표_쌓기에서_빠짐():
|
||||
heights = read("B05_Profile/B05_Profile_UI_Profile_Heights.ts")
|
||||
table = read("B05_Profile/B05_Profile_UI_Profile_TableOverlay.ts")
|
||||
assert "massHaul" not in heights
|
||||
assert "setBottomOffset" not in heights and "setBottomOffset" not in table
|
||||
assert "bottomOffset" not in table
|
||||
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""354-2 유토곡선 종단 기준 숨김 — 안 셈 · 안 그림 · 범례 단추 없음(코드 남김).
|
||||
|
||||
- 그리기 = 횡단 기준만(`["cross"]`) · 범례는 낸 계열마다라 종단 단추가 안 섬.
|
||||
- 저장된 범례가 종단(`longitudinal`)이면 횡단으로 맞춤 · 분배 켬은 그대로.
|
||||
- 계산 묶음 단계(MassHaul_Stage)는 두 기준 그대로 · 공용 계산 코드 남김.
|
||||
|
||||
`helper_354_2_masshaul_cross_only.cjs` 가 진짜 TS 를 잘라 돌림.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = Path(HERE).parents[1]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def r():
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_354_2_masshaul_cross_only.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_저장된_종단_범례는_횡단으로(r):
|
||||
assert r["longitudinal"] == ["cross"]
|
||||
assert r["longitudinalBalance"] == ["balance", "cross"]
|
||||
assert r["cross"] == ["cross"]
|
||||
assert r["empty"] == ["cross"] and r["broken"] == ["cross"]
|
||||
|
||||
|
||||
def test_기준_횡단만_계산(r):
|
||||
assert r["crossOnly"] == ["cross"]
|
||||
assert r["both"] == ["cross", "longitudinal"] # 공용 기본값 = 묶음 단계 그대로
|
||||
|
||||
|
||||
def test_그리기는_횡단만_부름():
|
||||
mass = (ROOT / "B05_Profile/B05_Profile_UI_Profile_MassHaul.ts").read_text(encoding="utf-8")
|
||||
calls = re.findall(r"computeMassHaulSeries\(([^;]*?)\)", mass, re.S)
|
||||
assert calls and all('["cross"]' in call for call in calls)
|
||||
|
||||
|
||||
def test_묶음_단계와_공용_코드는_그대로():
|
||||
stage = (ROOT / "B05_Profile/logic/B05_Profile_MassHaul_Stage.ts").read_text(encoding="utf-8")
|
||||
assert '["cross"]' not in stage
|
||||
util = (ROOT / "common_util/common_util_mass_haul.ts").read_text(encoding="utf-8")
|
||||
assert 'const SERIES_BASES: MassHaulBasis[] = ["cross", "longitudinal"];' in util
|
||||
assert "longitudinalAreaSamples(longitudinal, sections)" in util
|
||||
@@ -0,0 +1,87 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""354-3 종단 그래프 스크롤 끊김 — 동기화 메아리 · 스크롤마다 유토 다시 셈.
|
||||
|
||||
- 메아리: scroll 은 다음 틀에 옴 → 동기 플래그로 못 막아 한 틀 전 값을 되씀 → 뒤로 튐 →
|
||||
휠 이동이 접힘. 공용 `linkScrollLeft`(Wheel.ts)가 써 넣은 값으로 온 scroll 을 무시.
|
||||
가짜 창(다음 틀 scroll) 재현 — 옛 잇기는 뒤로 튐 · 멈춤, 새 잇기는 4 경우 × 휠 쪽 둘 모두 튐 0.
|
||||
- 유토 스크롤 그리기 = 열림 · 분배 맞춤 아님 · 붙음일 때만 · 한 틀 한 번 · 다시 셈 없음.
|
||||
|
||||
`helper_354_3_scroll_echo.cjs` 가 진짜 휠 · 잇기 TS 를 가짜 창 위에서 돌림.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
B05 = Path(HERE).parents[1] / "B05_Profile"
|
||||
CASES = [
|
||||
f"{driver}|table={table}|balance={balance}"
|
||||
for driver in ("body", "mass")
|
||||
for table in ("true", "false")
|
||||
for balance in ("false", "true")
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def r():
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_354_3_scroll_echo.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_옛_잇기는_메아리로_뒤로_튐(r):
|
||||
# 표 · 분배 끔(사용자 보고 경우)에서 휠 다섯 칸(1,200px) 중 210px 에서 멈춤
|
||||
assert r["old|body|table=false|balance=false"]["back"] > 0
|
||||
assert r["old|body|table=false|balance=false"]["end"] < 1200
|
||||
# 분배 켜고 표 끔 = 잇기 없음 — 사용자 관찰(분배 켜면 매끄러움)과 같음
|
||||
assert r["old|body|table=false|balance=true"] == {
|
||||
"back": 0,
|
||||
"end": 1200,
|
||||
"lead": 1200,
|
||||
"mass": 0,
|
||||
"table": 0,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("case", CASES)
|
||||
def test_새_잇기는_뒤로_튐_0_끝까지(r, case):
|
||||
got = r[f"new|{case}"]
|
||||
assert got["back"] == 0
|
||||
assert got["end"] == 1200 and got["lead"] == 1200
|
||||
table, balance = "table=true" in case, "balance=true" in case
|
||||
assert got["table"] == (1200 if table else 0)
|
||||
assert got["mass"] == (0 if balance else 1200)
|
||||
|
||||
|
||||
def test_유토_표_같은_도우미():
|
||||
for name in ("MassHaul", "TableOverlay"):
|
||||
src = (B05 / f"B05_Profile_UI_Profile_{name}.ts").read_text(encoding="utf-8")
|
||||
assert "linkScrollLeft(main, scroll" in src
|
||||
assert "syncing" not in src
|
||||
|
||||
|
||||
def test_유토_스크롤_그리기는_창_따를_때만_한_틀_한_번():
|
||||
mass = (B05 / "B05_Profile_UI_Profile_MassHaul.ts").read_text(encoding="utf-8")
|
||||
body = mass[mass.index("function drawWindow(") :]
|
||||
body = body[: body.index("\n }\n")]
|
||||
assert '!open || fitted || overlay.classList.contains("is-popout") || !lastSeries' in body
|
||||
assert "windowState.frame" in body and "frameOf(overlay" in body
|
||||
assert "draw(lastParams, true)" in body
|
||||
# 다시 셈 없음 · 총괄값 · 범례는 계열이 바뀔 때만
|
||||
assert re.search(r"reuse && lastSeries\s*\? lastSeries", mass)
|
||||
assert "if (series !== lastSeries) emitSummary(series);" in mass
|
||||
# 다시 그려도 제 자리 — 종단 값으로 되쓰는 것은 처음 · 맞춤 뒤 · 창 옮김 뒤만
|
||||
assert "scroll.scrollLeft = syncedMain.scrollLeft" not in mass
|
||||
render = (B05 / "B05_Profile_UI_Profile_Render.ts").read_text(encoding="utf-8")
|
||||
assert "massHaul.drawWindow({" in render
|
||||
@@ -0,0 +1,19 @@
|
||||
"""356 B06 횡단 카드 — 키 강조 · 중심점 좌우(요구 235-17 · 18).
|
||||
|
||||
못박는 것
|
||||
- 356-2 카드 초점 없음 — 마우스로 고른 뒤 키를 눌러도 초점 테두리(다른 색) 안 뜸 · ▲▼ 계획고는 창 듣개.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
B06 = ROOT / "B06_Section"
|
||||
|
||||
|
||||
def test_카드_초점_없음():
|
||||
view = (B06 / "B06_Section_UI_Cross_View.ts").read_text(encoding="utf-8")
|
||||
assert "card.tabIndex" not in view
|
||||
assert 'card.addEventListener("keydown"' not in view
|
||||
assert 'card.addEventListener("click", () => onSelect(section.station_id));' in view
|
||||
@@ -140,16 +140,16 @@ def test_설정_창_탭_저장_물음():
|
||||
page.click(".ui-confirm button >> nth=0") # 취소
|
||||
assert page.locator(".app-settings").count() == 1 and _writes(page) == []
|
||||
|
||||
# [저장] = 한 번 씀 · 읽기 층 방향 뒤집힘 · 닫을 때 안 물음
|
||||
# [저장] = 한 번 씀 · 바로 닫힘(물음 없음) · 읽기 층 방향 뒤집힘
|
||||
_save(page)
|
||||
assert page.locator(".app-settings").count() == 0
|
||||
assert page.locator(".ui-confirm").count() == 0
|
||||
assert len(_writes(page)) == 1
|
||||
assert json.loads(_writes(page)[0]) == {"wheelZoom": "pull", "cadAliases": {}}
|
||||
assert page.evaluate(WHEEL_IN, 100) is True and page.evaluate(WHEEL_IN, -100) is False
|
||||
_save(page) # 고친 것 없음 — 또 안 씀
|
||||
assert len(_writes(page)) == 1
|
||||
_close(page)
|
||||
assert page.locator(".app-settings").count() == 0
|
||||
assert page.locator(".ui-confirm").count() == 0
|
||||
_open_settings(page)
|
||||
_save(page) # 고친 것 없음 — 또 안 씀 · 닫힘
|
||||
assert len(_writes(page)) == 1 and page.locator(".app-settings").count() == 0
|
||||
|
||||
# CAD 별칭 — 잘못이면 안 씀 · 칸 표시
|
||||
_open_settings(page)
|
||||
@@ -163,25 +163,26 @@ def test_설정_창_탭_저장_물음():
|
||||
_save(page)
|
||||
assert page.inner_text(".app-settings__error").strip(), (line_v, circle_v)
|
||||
assert page.locator(".app-settings input[aria-invalid=true]").count() >= 1
|
||||
assert len(_writes(page)) == 1
|
||||
assert len(_writes(page)) == 1 and page.locator(".app-settings").count() == 1 # 창 남음
|
||||
page.fill(line, "ll")
|
||||
page.fill(circle, "")
|
||||
_save(page)
|
||||
assert page.inner_text(".app-settings__error").strip() == ""
|
||||
assert page.locator(".app-settings").count() == 0 # 저장 성공 = 닫힘
|
||||
assert len(_writes(page)) == 2
|
||||
assert json.loads(_writes(page)[1]) == {"wheelZoom": "pull", "cadAliases": {"LINE": ["LL"]}}
|
||||
|
||||
# 닫을 때 물음 — 버림 = 안 씀 · 저장 = 한 번 더
|
||||
_open_settings(page)
|
||||
page.click("[data-settings-tab=mouse]")
|
||||
page.check("input[name=app-settings-wheel][value=push]")
|
||||
_close(page)
|
||||
page.click(".ui-confirm button >> nth=1") # 버리고 넘어감
|
||||
page.click(".ui-confirm button >> nth=1") # 폐기
|
||||
assert page.locator(".app-settings").count() == 0 and len(_writes(page)) == 2
|
||||
assert page.evaluate(WHEEL_IN, 100) is True
|
||||
_open_settings(page)
|
||||
page.check("input[name=app-settings-wheel][value=push]")
|
||||
_close(page)
|
||||
page.click(".ui-confirm button >> nth=2") # 저장 후 넘어감
|
||||
page.click(".ui-confirm button >> nth=2") # 저장
|
||||
assert page.locator(".app-settings").count() == 0 and len(_writes(page)) == 3
|
||||
assert json.loads(_writes(page)[2])["wheelZoom"] == "push"
|
||||
assert page.evaluate(WHEEL_IN, 100) is False
|
||||
|
||||
@@ -24,7 +24,6 @@ NEW = (
|
||||
"Hopyo_Api",
|
||||
"Hopyo_Pick",
|
||||
"Hopyo_Side",
|
||||
"Hopyo_View",
|
||||
"Hopyo_Sheet",
|
||||
"Hopyo_Check",
|
||||
"Hopyo_Choose",
|
||||
@@ -192,7 +191,9 @@ def test_로직_추가_확인된_것은_바로_참조_아니면_확인_창():
|
||||
|
||||
|
||||
def test_엑셀_보기_창_하위_호표_먼저():
|
||||
assert _check()["sheet"]["order"] == [46, 45, 3, 7, 8] # 순환은 남은 차례
|
||||
got = _check()["sheet"]
|
||||
assert got["order"] == [46, 45, 3, 7, 8] # 순환은 남은 차례
|
||||
assert got["linked"] == [True, False, False, False] # 다 연결만 녹색 · 0/0 색 없음(359-2)
|
||||
|
||||
|
||||
def _pick() -> dict:
|
||||
@@ -279,18 +280,27 @@ def test_옛_즉시_확정과_바로_만들기_없음():
|
||||
):
|
||||
assert label not in every, label
|
||||
users = sorted(name for name, text in texts.items() if "buildHopyo(" in text)
|
||||
assert users == ["Hopyo_Check", "Hopyo_Sheet"] # 확인 창 · 펼침 뒤 한 번
|
||||
assert users == ["Hopyo_Check", "Hopyo_Sheet"] # 확인 창 · 엑셀 보기 [저장] 물음 뒤
|
||||
check = texts["Hopyo_Check"]
|
||||
assert '"확인하고 사전조합 만들기"' in check and "맞음 · 별칭 모두 체크" in check
|
||||
assert "showLoadingOverlay()" in check and "dropDraft(k)" in check
|
||||
assert "m02-hopyo-check:" in check and "POLL_MS" in check
|
||||
sheet = texts["Hopyo_Sheet"]
|
||||
assert "확인하고 만들기" in sheet and "이대로 ${ready.length}개 만들기" in sheet
|
||||
assert "확인 창 열기" in sheet and "buildOrder(" in sheet
|
||||
# 359-2 — 펼침 표 · [확인 창 열기] · [이대로 n개 만들기] 없음 · [저장] = 물음 한 번 뒤 체크 줄로 만들기
|
||||
for gone in ("확인 창 열기", "이대로", "review(", "확인하고 만들기"):
|
||||
assert gone not in sheet, gone
|
||||
save = sheet.split("async function save(")[1].split("async function makeAll(")[0]
|
||||
assert 'showConfirmDialog(ask, "저장")' in save and "hasDraft(" in save
|
||||
assert "건너뜀 — 체크 줄 없음" in save and "await makeAll(ready, skipped);" in save
|
||||
assert "buildOrder(" in sheet and "error instanceof StaleError" in sheet # 하위 먼저 · 409 안내
|
||||
edit = sheet.split("const editButton =")[1].split("function paintList(")[0]
|
||||
assert '"수정"' in edit and "e.stopPropagation();" in edit and "openHopyoCheck({" in edit
|
||||
row = sheet.split("function paintList(")[1]
|
||||
assert row.index("m02-hopyo-sheet__no") < row.index("editButton(h)") < row.index("__name")
|
||||
assert 'linked.classList.toggle("is-linked", allLinked(h.맞춤));' in row
|
||||
pick = texts["Hopyo_Pick"]
|
||||
assert "if (row.확인)" in pick and 'import("./M02_MasterTemplete_Hopyo_Check")' in pick
|
||||
view = texts["Hopyo_View"]
|
||||
assert "buildHopyoCheck(" in view and "choose(" not in view
|
||||
assert not (M02 / "M02_MasterTemplete_Hopyo_View.ts").exists() # 본 칸 표 없음(359-1)
|
||||
ledger = texts["Hopyo_Ledger"]
|
||||
assert "m02-hopyo-ledger:" in ledger and "saveLedger(" in ledger
|
||||
for gone in ("발주처", "회사로 올리기", "쓰인 횟수"):
|
||||
@@ -306,6 +316,13 @@ def test_좌측_판_자리와_권한():
|
||||
assert "canWrite: canEdit(layer, opt.isAdmin)," in side # 구조물 조합 층 쓰기 권한
|
||||
assert "hopyo.paint();" in side.split("function paint(): void {")[1]
|
||||
hopyo_side = _text("M02_MasterTemplete_Hopyo_Side.ts")
|
||||
# 359-1 — [파일 업로드] = 구조물 조합 층 단추 꼴(보통 크기 · m02-layers__btn) · 파일 줄 = 바로 엑셀 보기
|
||||
up = hopyo_side.split("const upload = createButton({")[1].split("upload.title")[0]
|
||||
assert 'label: "파일 업로드"' in up and 'upload.classList.add("m02-layers__btn");' in up
|
||||
assert "size:" not in up and "엑셀 올리기" not in hopyo_side
|
||||
assert "buildHopyoView" not in hopyo_side
|
||||
assert 'row.addEventListener("click", () => void showSheet(f));' in hopyo_side
|
||||
assert "onClose:" in hopyo_side and "onDeleted: () => void refresh()," in hopyo_side
|
||||
assert 'accept: ".xls,.xlsx"' in hopyo_side and "input.hidden = true;" in hopyo_side
|
||||
assert "disabled: !opt.canWrite," in hopyo_side and "pageScope()" in hopyo_side
|
||||
assert "buildHopyoLedger({ scope, canWrite: opt.canWrite })" in hopyo_side # [별칭 장부]
|
||||
@@ -332,16 +349,15 @@ def test_로직_추가_한_곳과_파일_규칙():
|
||||
def test_엑셀_보기_창_여는_자리와_늦게_불러옴():
|
||||
lazy = 'import("./M02_MasterTemplete_Hopyo_Sheet")'
|
||||
side = _text("M02_MasterTemplete_Hopyo_Side.ts")
|
||||
assert lazy in side.split("await show(got);")[1].split("})")[0] # 올린 직후 자동
|
||||
view = _text("M02_MasterTemplete_Hopyo_View.ts")
|
||||
assert lazy in view and '"엑셀로 보기"' in view
|
||||
assert "await showSheet(got);" in side # 올린 직후 자동
|
||||
assert lazy in side.split("async function showSheet(")[1].split("async function showLedger(")[0]
|
||||
check = _text("M02_MasterTemplete_Hopyo_Check.ts")
|
||||
assert lazy in check and '"엑셀에서 보기"' in check and "호표: opt.no" in check
|
||||
assert "단산행: 행" in check
|
||||
pick = _text("M02_MasterTemplete_Hopyo_Pick.ts")
|
||||
assert lazy in pick
|
||||
static = r'^import (?!type )[^(;]*"\./M02_MasterTemplete_Hopyo_Sheet"'
|
||||
for text in (side, view, pick, check):
|
||||
for text in (side, pick, check):
|
||||
assert not re.search(static, text, re.M) # 정적 import 없음(타입만)
|
||||
sheet = _text("M02_MasterTemplete_Hopyo_Sheet.ts")
|
||||
assert 'import("../A00_Common/spreadsheet/spreadsheet")' in sheet
|
||||
@@ -400,15 +416,18 @@ def test_확인_창_손질_339():
|
||||
sheet = _text("M02_MasterTemplete_Hopyo_Sheet.ts")
|
||||
assert "poll(file);" in sheet and "poll(next);" in sheet and "POLL_MS" in sheet
|
||||
assert "if (ok) document.dispatchEvent(new Event(HOPYO_BUILT));" in sheet
|
||||
assert '`맞음 ${tally("맞음")} · 다른 점 ${tally("다른 점")}`' in sheet
|
||||
|
||||
|
||||
def test_본_칸_호표_목록_셈_끝까지_다시_읽음_339():
|
||||
"""339 덧고침 2 — 본 칸 호표 표 「제안 n / 줄 m」 · 머리 「셈 중」 — 셈 중이면 3 초마다 다시 읽음."""
|
||||
view = _text("M02_MasterTemplete_Hopyo_View.ts")
|
||||
assert "poll(file);" in view and "poll(next);" in view and "POLL_MS" in view
|
||||
assert "hopyos = next.호표;" in view and "...hopyos" in view
|
||||
assert (
|
||||
"const live = (): boolean => my === nav && view.isConnected;" in view
|
||||
) # 호표 하나로 가면 멈춤
|
||||
assert "숫자 판정 셈 중" in view
|
||||
def test_엑셀_보기_머리_막대_셈_끝까지_다시_읽음_359():
|
||||
"""339 덧고침 2 → 359-1 — 옛 본 칸 표 몫(시트 · 호표 수 · 검산 · 「제안 n / 줄 m」 · 「셈 중」 · [삭제])은
|
||||
엑셀 보기 창 머리 · 셈 중이면 3 초마다 다시 읽음(머리 · 목록 함께)."""
|
||||
sheet = _text("M02_MasterTemplete_Hopyo_Sheet.ts")
|
||||
head = sheet.split("function paintHead(")[1].split("const dropButton")[0]
|
||||
for word in ("시트 ", "호표 ", "검산 어긋남", "검산 맞음", "제안 ", "숫자 판정 셈 중"):
|
||||
assert word in head, word
|
||||
assert "opt.canWrite && opt.onDeleted ? [dropButton()]" in head
|
||||
drop = sheet.split("const dropButton =")[1].split("/** 목록 강조")[0]
|
||||
assert 'label: "삭제"' in drop and 'showConfirmDialog(ask, "삭제")' in drop
|
||||
assert drop.index("deleteHopyo(") < drop.index("modal.close();") < drop.index("opt.onDeleted")
|
||||
poll = sheet.split("function poll(")[1].split("async function load(")[0]
|
||||
assert "paintHead(next);" in poll and "paintList();" in poll and "poll(next);" in poll
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""일위대가 실무자료 이음매(PLAN 329 · 330) — 실제 서버 응답(북부청 격자 · 호표 맞춤)을
|
||||
화면 도우미(`helper_m02_hopyo_seam.cjs` → Hopyo_Sheet · Hopyo_View)에 먹임.
|
||||
화면 도우미(`helper_m02_hopyo_seam.cjs` → Hopyo_Sheet)에 먹임.
|
||||
|
||||
행 22 → 제3호표 · 25 빈 줄 · 26 → 제4호표 · 영역 B20:O24 · 단산93 행 4229:4244 ·
|
||||
엑셀 근거 줄 · 근거 꼬리표 · 근거 맞음 · 도우미는 고르기 창 `Hopyo_Choose`.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""저장 안 한 고침 3단추 물음(PLAN 86-1 · `ui_template_leave_dialog.ts`) — Node 로 TS 를 돌려 판정.
|
||||
|
||||
못박는 것
|
||||
- 단추 순서 [취소] [버리고 넘어감] [저장 후 넘어감] · 기본 초점 = 취소 · Esc = 취소.
|
||||
- 저장 후 넘어감: 저장 성공 + 고침 없음 = saved / 저장 던짐 · 고침 남음 = stay(머묾).
|
||||
- 버리고 넘어감: discard 갈고리 호출 · 취소: 아무것도 안 부름.
|
||||
- 단추 순서 [취소] [폐기] [저장] · 기본 초점 = 취소 · Esc = 취소.
|
||||
- 저장: 저장 성공 + 고침 없음 = saved / 저장 던짐 · 고침 남음 = stay(머묾).
|
||||
- 폐기: discard 갈고리 호출 · 취소: 아무것도 안 부름.
|
||||
- M02 자리(UI_Main · Structure · ItemCards)가 이 물음을 씀.
|
||||
"""
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
① `delayedShow` 를 Node 로 가짜 시계로 돌림 — 0.3초 전에 끝나면 안 보임(깜빡임 없음) · 넘으면 보이고
|
||||
끝나면 걷힘 · 끝낸 뒤 시계가 와도 안 보임.
|
||||
② 실패 자리 = 까닭 글(「불러오지 못함 — 까닭」) + [다시 불러오기] · 화면 전체 잠금 안 씀.
|
||||
② 실패 자리 = 까닭 글(「불러오지 못함 — 까닭」) + [새로고침] · 화면 전체 잠금 안 씀.
|
||||
③ M02 나무 · 상세(표 · 도면) · 조합 목록 · 조합 상세 · 항목 카드가 이 부품을 씀.
|
||||
"""
|
||||
|
||||
@@ -70,7 +70,7 @@ def test_실패_글과_다시_불러오기_M02_자리():
|
||||
assert "clear: (id) => clearTimeout(id)," in src
|
||||
locale = _text(ROOT / "ui_template" / "ui_template_locale_common.ts")
|
||||
assert 'Common_Load_Failed: ["불러오지 못함"' in locale
|
||||
assert 'Common_Reload: ["다시 불러오기"' in locale
|
||||
assert 'Common_Reload: ["새로고침"' in locale
|
||||
tree = _text(M02 / "M02_MasterTemplete_TreePanel.ts")
|
||||
assert "const stop = showLoading(body);" in tree
|
||||
assert "loadingError(why(error), () => void refresh())" in tree
|
||||
|
||||
Reference in New Issue
Block a user