fix(box): 본체 「사용자 입력」 빈 칸 저장 뒤 다시 열어도 사용자 입력 · B06 다리가 유입·유출 구조 글 유지(52-19)
- 규격 판정(bodyOf): 가로 · 세로 중 손 값 하나라도 있으면 사용자 입력 + 빈 칸은 빈 채 - 둘 다 비면 표시 키 body_custom 만 저장 · 폼 칸 목록에 넣어 숫자 채우면 지워짐 - B06 폼→조정창 다리: 흙막이 · 날개벽 글을 있음/없음으로 바꿔 적던 것 → 글 그대로 - 시험: 52-2 헬퍼 한 칸 · 두 칸 빈 경우 · ui_checks 52-19 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4uKaV4ZBhsJN4GHG7yRj4
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
* 컨테이너 셋(모두 필수): 본체 · 유입 구조 · 유출 구조. 구조 · 정본 = 이 명세 · 이름만 정의(M02)에서 바꿈.
|
||||
* 저장 키 = pipe_points.json 옛 키 그대로(B06 횡단 · B07 · B08 이 읽는 자리):
|
||||
* · 본체 = `body_width_m` · `body_height_m` 수 — 규격 칸은 저장 안 함(폭 · 높이로 되찾음 · `bodyOf`)
|
||||
* · 사용자 입력에 두 칸 다 비면 `body_custom` = 「사용자 입력」 표시만
|
||||
* · 유입 · 유출 구조 = `wing_in` · `wing_out`(없음 · 날개벽 · 흙막이 · 옛 「있음」 = 날개벽)
|
||||
* — 엔진은 날개벽만 세움(`_wing_spec` ↔ `wingSpec` 짝)
|
||||
* 표 02 에 없는 옛 날개벽 치수 칸은 화면에서 빼고 값은 남김(지난 정의 값).
|
||||
@@ -14,6 +15,8 @@
|
||||
import type { Instance, SpecItem, StructureSpec } from "./design_structure_spec";
|
||||
|
||||
export const BOX_CUSTOM = "사용자 입력";
|
||||
/** 사용자 입력에 가로 · 세로 둘 다 빈 채 저장했다는 표시(폭 · 높이 수가 없을 때만 적음). */
|
||||
export const BOX_CUSTOM_KEY = "body_custom";
|
||||
/** 본체 규격 → [가로, 세로](m) — 실무 관측 규격(2026-08-17 사용자 확정). */
|
||||
const BOX_PRESETS: Record<string, readonly [number, number]> = {
|
||||
"2.0×2.0": [2, 2],
|
||||
@@ -97,21 +100,25 @@ export function boxOptionsIn(
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 저장 폭 · 높이 → 본체 칸 값 — 규격과 맞으면 그 규격 · 아니면 사용자 입력 · 없으면 빈 값. */
|
||||
/** 저장 폭 · 높이 → 본체 칸 값 — 규격과 맞으면 그 규격 · 손 값이 하나라도 있거나 사용자 입력 표시면
|
||||
* 사용자 입력(빈 칸은 빈 채) · 아니면 빈 값. */
|
||||
export function bodyOf(options: Record<string, string | number>): Instance {
|
||||
const width = Number(options.body_width_m);
|
||||
const height = Number(options.body_height_m);
|
||||
if (!(width > 0 && height > 0)) return { size: "" };
|
||||
const preset = Object.entries(BOX_PRESETS).find(
|
||||
([, [w, h]]) => Math.abs(w - width) < 0.05 && Math.abs(h - height) < 0.05,
|
||||
);
|
||||
return preset
|
||||
? { size: preset[0] }
|
||||
: { size: BOX_CUSTOM, width: String(width), height: String(height) };
|
||||
if (preset) return { size: preset[0] };
|
||||
if (!(width > 0) && !(height > 0) && options[BOX_CUSTOM_KEY] !== BOX_CUSTOM) return { size: "" };
|
||||
const side = (n: number): string => (n > 0 ? String(n) : "");
|
||||
return { size: BOX_CUSTOM, width: side(width), height: side(height) };
|
||||
}
|
||||
|
||||
/** 본체 칸 값 → 저장 폭 · 높이 — 규격이면 규격 수(사용자 입력은 명세 키가 적음). */
|
||||
export function bodyOptions(body: Instance | undefined): Record<string, number> {
|
||||
/** 본체 칸 값 → 저장 폭 · 높이 — 규격이면 규격 수(사용자 입력은 명세 키가 적음) · 사용자 입력에 두 칸 다
|
||||
* 비면 표시 키만(다시 열 때 「사용자 입력」). */
|
||||
export function bodyOptions(body: Instance | undefined): Record<string, string | number> {
|
||||
const preset = BOX_PRESETS[body?.size ?? ""];
|
||||
return preset ? { body_width_m: preset[0], body_height_m: preset[1] } : {};
|
||||
if (preset) return { body_width_m: preset[0], body_height_m: preset[1] };
|
||||
const blank = !(body?.width ?? "").trim() && !(body?.height ?? "").trim();
|
||||
return body?.size === BOX_CUSTOM && blank ? { [BOX_CUSTOM_KEY]: BOX_CUSTOM } : {};
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export const FORM_OPTION_KEYS: Readonly<Record<string, readonly string[]>> = {
|
||||
...revetKeys("outlet"),
|
||||
],
|
||||
// BOX = 표 02 명세 키(PLAN 52-2) — 옛 날개벽 치수는 폼에서 빠져 이어 붙임(지난 정의 값)
|
||||
box_culvert: ["body_width_m", "body_height_m", "wing_in", "wing_out"],
|
||||
box_culvert: ["body_width_m", "body_height_m", "body_custom", "wing_in", "wing_out"],
|
||||
// 물넘이 포장 = 표 03 명세 키(PLAN 52-3 · `design_structure_ford_pavement`) — 옛 월류 · 경사 · 두께 · 길이는
|
||||
// 폼에서 빠져 이어 붙임 · 도수로/돌붙임 키는 `FORM_OPTION_PREFIXES`
|
||||
ford_pavement: ["ford_slope_deg"],
|
||||
|
||||
@@ -25,6 +25,8 @@ import type { BoxControl } from "./B06_Section_UI_Cross_Box_Panel";
|
||||
/** 날개벽 한 벌의 저장 키 — 세월교·BOX암거가 같은 옵션 이름을 쓴다(`wing_in*`/`wing_out*`). */
|
||||
type WingPatch = Partial<{
|
||||
installed: boolean;
|
||||
/** 폼이 낸 설치 글(날개벽 · 흙막이 …) — 있으면 저장에 그 글 그대로(있음/없음으로 안 바꿈). */
|
||||
text: string;
|
||||
height_m: number;
|
||||
length_m: number;
|
||||
angle_deg: number;
|
||||
@@ -34,7 +36,8 @@ type WingPatch = Partial<{
|
||||
function wingOptions(role: "inlet" | "outlet", patch: WingPatch): Record<string, number | string> {
|
||||
const prefix = role === "inlet" ? "wing_in" : "wing_out";
|
||||
const options: Record<string, number | string> = {};
|
||||
if (patch.installed !== undefined) options[prefix] = patch.installed ? "있음" : "없음";
|
||||
if (patch.installed !== undefined)
|
||||
options[prefix] = patch.text ?? (patch.installed ? "있음" : "없음");
|
||||
if (patch.height_m !== undefined) options[`${prefix}_height_m`] = patch.height_m;
|
||||
if (patch.length_m !== undefined) options[`${prefix}_length_m`] = patch.length_m;
|
||||
if (patch.angle_deg !== undefined) options[`${prefix}_angle_deg`] = patch.angle_deg;
|
||||
@@ -82,7 +85,10 @@ function wingPatchFrom(patch: Record<string, number | string>, prefix: string):
|
||||
};
|
||||
const wing: WingPatch = {};
|
||||
// 설치 값 글(있음 · 없음 · BOX 날개벽 · 흙막이) — 백엔드 `_wing_spec` 과 같은 판정.
|
||||
if (patch[prefix] !== undefined) wing.installed = wingInstalled(patch[prefix]);
|
||||
if (patch[prefix] !== undefined) {
|
||||
wing.installed = wingInstalled(patch[prefix]);
|
||||
wing.text = String(patch[prefix]);
|
||||
}
|
||||
const heightM = num(`${prefix}_height_m`);
|
||||
if (heightM !== undefined) wing.height_m = heightM;
|
||||
const lengthM = num(`${prefix}_length_m`);
|
||||
|
||||
@@ -55,7 +55,15 @@ const out = {
|
||||
st.body[0].height = "1.8";
|
||||
}),
|
||||
blank: roundTrip({}),
|
||||
// 52-19 — 사용자 입력에서 한 칸 · 두 칸 비우고 저장 → 다시 열기
|
||||
oneBlank: roundTrip({ body_height_m: 2.5 }),
|
||||
bothBlank: roundTrip({ body_width_m: 2, body_height_m: 2 }, (st) => {
|
||||
st.body[0].size = "사용자 입력";
|
||||
st.body[0].width = "";
|
||||
st.body[0].height = "";
|
||||
}),
|
||||
keys: s.specKeys(SPEC),
|
||||
retired: SPEC.retired,
|
||||
};
|
||||
out.bothBlankAgain = roundTrip(out.bothBlank.out);
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
|
||||
@@ -76,6 +76,24 @@ def test_빈_값은_빈_채로(out):
|
||||
assert out["blank"]["state"]["body"][0]["size"] == ""
|
||||
|
||||
|
||||
def test_사용자_입력_빈_칸은_다시_열어도_사용자_입력(out):
|
||||
# 52-19 — 가로만 비움: 세로 손 값이 남아 사용자 입력 + 빈 가로
|
||||
assert out["oneBlank"]["state"]["body"][0] == {
|
||||
"size": "사용자 입력",
|
||||
"width": "",
|
||||
"height": "2.5",
|
||||
}
|
||||
assert out["oneBlank"]["out"] == {"body_height_m": 2.5}
|
||||
# 둘 다 비움: 표시 키만 적고 다시 열면 사용자 입력 + 빈 칸 둘
|
||||
assert out["bothBlank"]["out"] == {"body_custom": "사용자 입력"}
|
||||
assert out["bothBlankAgain"]["state"]["body"][0] == {
|
||||
"size": "사용자 입력",
|
||||
"width": "",
|
||||
"height": "",
|
||||
}
|
||||
assert out["bothBlankAgain"]["out"] == {"body_custom": "사용자 입력"}
|
||||
|
||||
|
||||
def test_정의_파일에_이름_표_그룹_선택지가_올라_있음():
|
||||
names = _node("names")
|
||||
with open(
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/* 52-19 — BOX암거 본체 「사용자 입력」 빈 칸 저장 → 다시 열기
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트 · 목록에 「BOX암거」(52-2 edit 뒤 2.4 × 1.8 · 흙막이)
|
||||
* 부르는 법: `window.__m5219 = "단계";` 를 붙여 — 단계 사이 orca reload
|
||||
* one → 가로 비우고 [저장] (reload) view → 사용자 입력 · 가로 빈 칸 · 세로 1.8
|
||||
* both → 세로도 비우고 [저장] (reload) view → 사용자 입력 · 빈 칸 둘 · 파일 body_custom
|
||||
* fill → 2.2 × 1.6 [저장] → 파일에 body_custom 없음
|
||||
* 기대: 유입 · 유출 구조 글(흙막이 · 없음) 그대로 · 되돌림은 52-2 restore
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m5219;
|
||||
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 pid = localStorage.getItem("frd_current_project_id");
|
||||
const api = await import(
|
||||
performance
|
||||
.getEntriesByType("resource")
|
||||
.map((e) => e.name)
|
||||
.filter((n) => n.includes("/B04_PreProcess/B04_PreProcess_Api_Fetch.ts"))
|
||||
.pop()
|
||||
);
|
||||
const spec = () =>
|
||||
[...document.querySelectorAll(".b05-structure__spec")].find((n) => n.offsetParent !== null);
|
||||
const box = (c) => spec()?.querySelector(`[data-spec-container="${c}"]`);
|
||||
const ctl = (c, i) =>
|
||||
box(c)?.querySelector(`[data-spec-item="${c}.${i}"]`)?.querySelector("select, input");
|
||||
const set = async (c, i, v) => {
|
||||
const el = ctl(c, i);
|
||||
el.value = v;
|
||||
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await w(200);
|
||||
};
|
||||
await until(() =>
|
||||
[...document.querySelectorAll(".b05-route__irregular-list li")].some((l) =>
|
||||
/BOX암거$/.test(l.textContent.trim()),
|
||||
),
|
||||
);
|
||||
const li = [...document.querySelectorAll(".b05-route__irregular-list li")].find((l) =>
|
||||
/BOX암거$/.test(l.textContent.trim()),
|
||||
);
|
||||
(li.querySelector("button") ?? li).click();
|
||||
await w(800);
|
||||
if (!box("body")) (li.querySelector("button") ?? li).click();
|
||||
await until(() => !!box("body"));
|
||||
await w(500);
|
||||
const read = () => ({
|
||||
size: ctl("body", "size")?.value,
|
||||
width: ctl("body", "width")?.value ?? null,
|
||||
height: ctl("body", "height")?.value ?? null,
|
||||
});
|
||||
const file = async () =>
|
||||
(await api.fetchDetailPipePoints(pid)).pipe_points.find((p) => p.facility === "box_culvert")
|
||||
?.options;
|
||||
const save = async () => {
|
||||
document.querySelector('[data-dock-action="save"]').click();
|
||||
await w(500);
|
||||
return until(() => !document.querySelector(".ui-overlay.is-active"), 60000);
|
||||
};
|
||||
const wings = () => ["inlet", "outlet"].map((c) => ctl(c, "structure")?.value);
|
||||
const out = { mode: MODE, reopened: read(), wings: wings() };
|
||||
if (MODE === "one") {
|
||||
await set("body", "width", "");
|
||||
out.saved = await save();
|
||||
out.file = await file();
|
||||
}
|
||||
if (MODE === "both") {
|
||||
await set("body", "height", "");
|
||||
out.saved = await save();
|
||||
out.file = await file();
|
||||
}
|
||||
if (MODE === "fill") {
|
||||
await set("body", "width", "2.2");
|
||||
await set("body", "height", "1.6");
|
||||
out.saved = await save();
|
||||
out.file = await file();
|
||||
}
|
||||
out.wingsAfter = wings();
|
||||
out.after = read();
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
Reference in New Issue
Block a user