diff --git a/A00_Common/design/design_structure_form.ts b/A00_Common/design/design_structure_form.ts index 0d570821d..e6df8ec91 100644 --- a/A00_Common/design/design_structure_form.ts +++ b/A00_Common/design/design_structure_form.ts @@ -212,7 +212,8 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = { const add = document.createElement("button"); add.type = "button"; add.className = "b05-route__irregular-btn b05-structure__spec-add"; - add.textContent = `+ ${containerTitle(c, names, groupNames[c.link])} 추가`; + add.textContent = `+ ${containerTitle(c, names, groupNames[c.link])}`; + add.title = "추가"; add.dataset.specAdd = c.id; add.addEventListener("click", () => { list.push({}); diff --git a/A00_Common/design/design_structure_spec.ts b/A00_Common/design/design_structure_spec.ts index da42af76f..506858418 100644 --- a/A00_Common/design/design_structure_spec.ts +++ b/A00_Common/design/design_structure_spec.ts @@ -225,7 +225,7 @@ export const choiceLabel = ( export function nameTable(spec: StructureSpec): SpecNames { const out: SpecNames = {}; for (const c of spec.containers) { - out[c.id] = c.title; + if (!c.required) out[c.id] = c.title; // 필수 컨테이너 이름 = 정의 그룹 이름(M02 그룹 표에서 바꿈) for (const item of c.items) { out[`${c.id}.${item.id}`] = item.label; const all = [ diff --git a/B05_Profile/B05_Profile_UI_Style_Structures.css b/B05_Profile/B05_Profile_UI_Style_Structures.css index 6b140043f..bb609526d 100644 --- a/B05_Profile/B05_Profile_UI_Style_Structures.css +++ b/B05_Profile/B05_Profile_UI_Style_Structures.css @@ -525,3 +525,22 @@ .b05-structure__stale input[readonly] { opacity: 0.7; } + +/* 구조물 명세 컨테이너(PLAN 52-1) — 상자 차례 · 긴 잇기 이름이 상자를 밀어 넓히지 않게(fieldset 최소 폭) */ +.b05-structure__spec { + display: flex; + flex-direction: column; + gap: 6px; +} + +.b05-structure__spec[hidden] { + display: none; +} + +.b05-structure__spec .b05-structure__group { + min-width: 0; +} + +.b05-structure__spec-add { + align-self: flex-start; +} diff --git a/resources/master_template/구조물정의.json b/resources/master_template/구조물정의.json index 132169e26..5b2e2bf07 100644 --- a/resources/master_template/구조물정의.json +++ b/resources/master_template/구조물정의.json @@ -3234,14 +3234,12 @@ "기슭막이_한벌" ], "이름": { - "pipe": "관", "pipe.diameter": "관경", "pipe.diameter=800": "800", "pipe.diameter=1000": "1000", "pipe.diameter=1200": "1200", "pipe.diameter=1500": "1500", "pipe.length": "관 길이 (m)", - "inlet": "관 보호공 (유입)", "inlet.structure": "구조", "inlet.structure=집수정": "집수정", "inlet.structure=기슭막이": "흙막이", @@ -3259,7 +3257,6 @@ "inlet.height": "높이 (m)", "inlet.before": "기준측점 전 (m)", "inlet.after": "기준측점 후 (m)", - "outlet": "관 보호공 (유출)", "outlet.structure": "구조", "outlet.structure=기슭막이": "흙막이", "outlet.form": "형태", diff --git a/resources/tester/ui_checks/52-1_배관_컨테이너.js b/resources/tester/ui_checks/52-1_배관_컨테이너.js new file mode 100644 index 000000000..02840470e --- /dev/null +++ b/resources/tester/ui_checks/52-1_배관_컨테이너.js @@ -0,0 +1,202 @@ +/* 52-1 — B05 · B06 좌측 「구조물 배치」 배관 = 사용자 구조물 표 01 명세(컨테이너 다섯) + * 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(sub5_검증) · 목록에 「배관 1」 + * 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m521 = "단계";` 를 붙여 + * orca eval --expression "window.__m521='edit'; $(cat 이 파일)" → 고치고 [저장] (원래 관 지점은 sessionStorage 에 둠) + * (orca reload 로 다시 열기) + * orca eval --expression "window.__m521='verify'; $(cat 이 파일)" → 다시 연 값 · B06 같은 그리기 · 관 길이 + * orca eval --expression "window.__m521='restore'; $(cat 이 파일)" → 원래 관 지점으로 되돌림(PUT) · 초안 비움 + * 쓰는 것: 검증 프로젝트 pipe_points.json(되돌림 단계가 원래 값으로 다시 씀) + * 기대 수치(edit): kinds 5 · required 3 늘 보임 · formBasin I형/L형/ㄷ형 · formSoil 6 · split 길이 7 · + * soilAdd 둘 → 하나 빼면 하나(콘크리트) · chute 하나(돌붙임) · 잇기 유입 지정 · 유출 지정 뒤 풂 + * 기대 수치(verify): 저장 값 그대로 · 같은 그리기(B05 = B06) · 관 길이 수(B06) + */ +(async () => { + const MODE = window.__m521 || "edit"; + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const until = async (fn, ms = 20000) => { + for (let t = 0; t < ms; t += 200) { + if (fn()) return true; + await w(200); + } + return false; + }; + const projectId = localStorage.getItem("frd_current_project_id"); + const modUrl = (part) => + performance + .getEntriesByType("resource") + .map((e) => e.name) + .filter((n) => n.includes(part)) + .pop(); + const fetchApi = await import(modUrl("/B04_PreProcess/B04_PreProcess_Api_Fetch.ts")); + const spec = () => document.querySelector(".b05-structure__spec"); + const box = (c, i = 0) => spec().querySelectorAll(`[data-spec-container="${c}"]`)[i]; + const ctl = (c, item, i = 0) => + box(c, i)?.querySelector(`[data-spec-item="${c}.${item}"]`)?.querySelector("select, input"); + const set = async (c, item, v, i = 0) => { + const el = ctl(c, item, i); + el.value = v; + el.dispatchEvent(new Event("change", { bubbles: true })); + await w(200); + }; + const labels = (c, item) => [...ctl(c, item).options].filter((o) => o.value).map((o) => o.text); + const pickPipe = async () => { + await until(() => document.querySelectorAll(".b05-route__irregular-list li").length > 0); + const li = [...document.querySelectorAll(".b05-route__irregular-list li")].find((l) => + /배관 1$/.test(l.textContent.trim()), + ); + // 다시 열면 고른 것이 복원돼 있음 — 누르면 풀리므로 풀렸으면 한 번 더(재선택 = 해제) + (li.querySelector("button") ?? li).click(); + await w(800); + if (!(spec() && spec().offsetParent !== null)) (li.querySelector("button") ?? li).click(); + await until(() => spec() && spec().offsetParent !== null); + await w(500); + }; + const shape = () => + [...spec().querySelectorAll("[data-spec-container]")].map((b) => ({ + c: b.dataset.specContainer, + legend: b.querySelector("legend").firstChild.textContent, + items: [...b.querySelectorAll("[data-spec-item]")].map((n) => n.dataset.specItem), + link: b.querySelector(".b05-structure__summary-name")?.textContent, + })); + const linkName = (c, i = 0) => + box(c, i).querySelector(".b05-structure__summary-name").classList.contains("is-assigned"); + const pickLink = async (c) => { + box(c).querySelector('[data-b05-summary="pick"]').click(); + await until(() => document.querySelector(".ui-item-picker__tree .ui-tree__row")); + // 첫 잎까지 펼침 + for (let k = 0; k < 8; k++) { + const rows = [...document.querySelectorAll(".ui-item-picker__tree .ui-tree__row")]; + const leaf = rows.find((r) => !r.classList.contains("is-branch")); + if (leaf) { + leaf.click(); + break; + } + const closed = rows.find((r) => r.querySelector(".ui-tree__caret")?.textContent === "▸"); + closed?.click(); + await w(300); + } + await w(300); + [...document.querySelectorAll(".ui-item-picker__actions button")] + .find((b) => b.textContent.includes("적용")) + .click(); + await w(500); + }; + const out = { mode: MODE }; + + if (MODE === "edit") { + const before = await fetchApi.fetchDetailPipePoints(projectId); + sessionStorage.setItem("__521_original", JSON.stringify(before.pipe_points)); + await pickPipe(); + const s0 = shape(); + out.kinds = + s0.filter((b) => ["pipe", "inlet", "outlet"].includes(b.c)).length + + spec().querySelectorAll("[data-spec-add]").length; + out.required = ["pipe", "inlet", "outlet"].every((c) => box(c)?.offsetParent !== null); + out.addButtons = [...spec().querySelectorAll("[data-spec-add]")].map((b) => b.textContent); + // 형태 선택지가 구조를 따름 + await set("inlet", "structure", "집수정"); + out.formBasin = labels("inlet", "form"); + await set("inlet", "structure", "기슭막이"); + out.formSoil = labels("inlet", "form"); + await set("inlet", "form", "돌망태"); + // 길이 = 전 + 후 — 길이 10 · 전 3 → 후 7 · 이어서 후 4 → 길이 7(전 · 후를 지킴) + await set("inlet", "length", "10"); + await set("inlet", "before", "3"); + out.splitAfter = ctl("inlet", "after").value; + await set("inlet", "after", "4"); + out.splitLength = ctl("inlet", "length").value; + // 선택 컨테이너 — 흙막이 추가 둘 → 첫째 뺌 · 도수로/돌붙임 하나 + spec().querySelector('[data-spec-add="soil_add"]').click(); + await w(200); + spec().querySelector('[data-spec-add="soil_add"]').click(); + await w(200); + await set("soil_add", "structure", "기슭막이", 0); + await set("soil_add", "form", "바자", 0); + await set("soil_add", "structure", "기슭막이", 1); + await set("soil_add", "form", "콘크리트", 1); + out.soilAddTwo = spec().querySelectorAll('[data-spec-container="soil_add"]').length; + out.soilFormsSoilAdd = labels("soil_add", "form").length; + box("soil_add", 0).querySelector('[data-spec-remove="soil_add"]').click(); + await w(300); + out.soilAddAfterRemove = [...spec().querySelectorAll('[data-spec-container="soil_add"]')].map( + (b, i) => [ + b.querySelector("legend").firstChild.textContent, + ctl("soil_add", "form", i).value, + ], + ); + spec().querySelector('[data-spec-add="chute_add"]').click(); + await w(200); + await set("chute_add", "structure", "돌붙임"); + out.chuteChoices = labels("chute_add", "structure"); + // 구조물 조합 잇기 — 유입 지정 · 유출 지정 뒤 풂 + await pickLink("inlet"); + out.linkInlet = linkName("inlet"); + await pickLink("outlet"); + out.linkOutletPicked = linkName("outlet"); + box("outlet").querySelector('[data-b05-summary="clear"]').click(); + await w(300); + out.linkOutletCleared = !linkName("outlet"); + // [저장] + document.querySelector('[data-dock-action="save"]').click(); + await w(500); + out.saved = await until(() => !document.querySelector(".ui-overlay.is-active"), 60000); + const after = await fetchApi.fetchDetailPipePoints(projectId); + const p = after.pipe_points.find((x) => x.chainage_m > 80 && x.chainage_m < 90); // 배관 1 = 4+5 + out.file = p && { + options: p.options, + group_items: Object.keys(p.group_items ?? {}), + }; + } + + if (MODE === "verify") { + await pickPipe(); + out.reopened = { + inlet: ["structure", "form", "length", "before", "after"].map((k) => ctl("inlet", k).value), + soilAdd: [...spec().querySelectorAll('[data-spec-container="soil_add"]')].map( + (_, i) => ctl("soil_add", "form", i).value, + ), + chute: ctl("chute_add", "structure").value, + linkInlet: linkName("inlet"), + linkOutlet: linkName("outlet"), + }; + const b05 = shape(); + out.b05Length = ctl("pipe", "length").value; + location.hash = "#/b06-section"; + await until(() => location.hash.includes("b06"), 5000); + await w(6000); + await pickPipe(); + const b06 = shape(); + out.sameDrawing = JSON.stringify(b05) === JSON.stringify(b06); + if (!out.sameDrawing) out.diff = { b05, b06 }; + out.b06Length = ctl("pipe", "length").value; + } + + if (MODE === "restore") { + const original = JSON.parse(sessionStorage.getItem("__521_original") || "null"); + if (!original) return JSON.stringify({ error: "원래 값 없음" }); + const keys = [ + "chainage_m", + "source", + "facility", + "start_m", + "end_m", + "options", + "summary_item", + "group_items", + ]; + const points = original.map((p) => + Object.fromEntries( + keys.filter((k) => p[k] !== undefined && p[k] !== null).map((k) => [k, p[k]]), + ), + ); + await fetchApi.saveDetailPipePoints(projectId, points); + for (const k of Object.keys(sessionStorage)) + if (k.startsWith("aislo:draft:pipes:") || k === "__521_original") + sessionStorage.removeItem(k); + const now = await fetchApi.fetchDetailPipePoints(projectId); + const pick = (list) => + JSON.stringify(list.map((p) => [p.chainage_m, p.options ?? {}, p.group_items ?? {}])); + out.restored = pick(now.pipe_points) === pick(original); + } + return JSON.stringify(out); +})();