fix(design): 배관 컨테이너 제목 = 정의 그룹 이름 · 상자 폭 · 52-1 화면 시나리오

- 필수 컨테이너 제목은 M02 그룹 이름을 따름(이름 표에서 뺌) · [추가] 단추 글 「+ 컨테이너 이름」
- 긴 잇기 이름이 상자를 패널 밖으로 밀던 것 막음(fieldset 최소 폭 0)
- ui_checks/52-1_배관_컨테이너 — 편집 · 저장 / 다시 열기 · B06 같은 그리기 · 관 길이 / 되돌림 세 단계

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ
This commit is contained in:
2026-10-05 23:24:49 +09:00
co-authored by Claude Opus 5.5
parent 0a095c14b1
commit 11e12b6575
5 changed files with 224 additions and 5 deletions
+2 -1
View File
@@ -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({});
+1 -1
View File
@@ -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 = [
@@ -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;
}
@@ -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": "형태",
@@ -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);
})();