fix(b06): 횡단도 구조물 고르기 → 좌측 패널 연결 점검 · 고침(53-1)
- 다른 측점 부재를 누를 때 옛 카드 선택 풀기가 폼 · 선택 칸을 비우던 것 막음(첫 누름부터 잡힘) - 컨테이너 상자 강조가 폼 다시 그리기에 지워지던 것 · 옛 상자 강조 남던 것 · 유입 · 유출도 보이게 스크롤 - 고른 구조물 기준 측점 고침 = 바로 반영 · 횡단 카드 · 알약 · 선택 칸이 따라감 - 부재키는 같은 것을 다시 적을 때만 지킴 · 풀면 지움(B05 ↔ B06 오갈 때 옛 조정창 안 열림) - 카드 고름이 일반 측점 · 이음 카드 알약을 지우던 것 → 연 구조물 알약 지킴 - B05 되살리기 = 구조물 id 가 같은 측점 관보다 먼저 · B06 다시 들어올 때 넘김값 다시 적용 - 시험: ui_checks/53-1_횡단_고르기.js(점검 116 · 흠 0) · 선택 시험 새 규칙으로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4uKaV4ZBhsJN4GHG7yRj4
This commit is contained in:
@@ -136,8 +136,9 @@ export function subscribeSelection(projectId: string | null, fn: SelectionListen
|
||||
}
|
||||
|
||||
/**
|
||||
* 고름을 적고 구독자에게 알린다. `partKey` 를 안 주면 이미 적힌 부재키를 지킨다 — 3D 로
|
||||
* 부재를 집은 직후 선택 동기화가 측점만 다시 적어도 조정창이 그대로 열려야 한다.
|
||||
* 고름을 적고 구독자에게 알린다. `partKey` 를 안 주면 **같은 것**을 다시 적을 때만 부재키를 지킨다 —
|
||||
* 3D 로 부재를 집은 직후 선택 동기화가 측점만 다시 적어도 조정창이 그대로 열려야 한다.
|
||||
* 다른 구조물로 옮기면 버리고, 빈 값("")을 주면 지운다(53-1).
|
||||
* `from` 은 낸 쪽 이름 — 구독자가 제 것을 되받아 다시 그리지 않게 거른다.
|
||||
* 값이 그대로면 알리지 않고, 알림 도중 다시 낸 값은 적기만 한다(되울림 방지).
|
||||
*/
|
||||
@@ -157,7 +158,17 @@ export function selectDesign(
|
||||
before.structureId === selection.structureId &&
|
||||
(options.partKey === undefined || options.partKey === before.partKey);
|
||||
if (same) return;
|
||||
const key = options.partKey ?? before?.partKey;
|
||||
// 부재키는 같은 것을 다시 적을 때만 지킴 — 다른 구조물로 옮기면 옛 부재(조정창)가 따라오지 않음(53-1)
|
||||
const sameTarget =
|
||||
before !== null &&
|
||||
before.chainageM !== null &&
|
||||
isNear(before.chainageM, selection.chainageM) &&
|
||||
before.structureId === selection.structureId;
|
||||
// 빈 부재키("") = 부재를 풂
|
||||
const key =
|
||||
options.partKey === ""
|
||||
? undefined
|
||||
: (options.partKey ?? (sameTarget ? before?.partKey : undefined));
|
||||
writeState(
|
||||
"structure-pick",
|
||||
{ at: selection.chainageM, key, sid: selection.structureId ?? undefined },
|
||||
|
||||
@@ -176,7 +176,18 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
|
||||
return wrap;
|
||||
});
|
||||
box.body.append(grid(...fields));
|
||||
if (c.required) box.body.append(slotOf(c.id));
|
||||
if (c.required) {
|
||||
const slot = slotOf(c.id);
|
||||
// B06 조정 강조(테두리 · 이름표)는 칸 상자에 붙음 — 다시 그려도 이어 감(53-1)
|
||||
const prev = slot.closest("fieldset");
|
||||
if (prev?.classList.contains("is-active")) {
|
||||
const legend = box.root.querySelector("legend")!;
|
||||
legend.dataset.baseLabel = legend.textContent ?? "";
|
||||
legend.textContent = prev.querySelector("legend")?.textContent ?? legend.textContent;
|
||||
box.root.classList.add("is-active");
|
||||
}
|
||||
box.body.append(slot);
|
||||
}
|
||||
if (links) {
|
||||
const key = linkOf(c, index + 1);
|
||||
const row = buildSummaryRow();
|
||||
|
||||
@@ -355,6 +355,11 @@ export async function mountCrossView(
|
||||
const opened = structuresPanel.showAtChainage(owner?.chainage_m ?? target.chainage_m);
|
||||
// 카드만 고른 것도 남긴다 — 종단 보기로 가면 그 시설이 그대로 열린다.
|
||||
if (!isWallSelecting()) selectDesign(projectId, opened, { from: "b06" });
|
||||
// 카드 고름은 그 카드 측점선으로 알약을 맞춤 — 일반 측점 구조물 · 옆 측점 이음 카드면
|
||||
// 알약이 꺼지므로 연 것으로 다시 세움(관 알약 id = pipe-측점 · 53-1)
|
||||
const pipeAt = structuresPanel.currentChainage();
|
||||
const mark = opened.structureId ?? (pipeAt === null ? null : `pipe-${pipeAt.toFixed(2)}`);
|
||||
if (mark) sectionView.selectMark(mark);
|
||||
structureSelection.syncInletStructure();
|
||||
// 카드만 고른 경우엔 구조물(벽·구체) 선택을 비운다 — 조정창이 저절로 뜨면 안 된다
|
||||
// (2026-08-29 사용자). 벽을 찍어 카드가 딸려 선택된 경우는 건드리지 않는다.
|
||||
@@ -433,6 +438,10 @@ export async function mountCrossView(
|
||||
structuresPanel.prefer(handoff?.structureId ?? null);
|
||||
sectionView.focusStation(stationId);
|
||||
structuresPanel.prefer(null);
|
||||
// 그 카드가 이미 골라져 있으면 카드 고름이 안 돌아 폼이 옛 것으로 남음 — id 로 직접 엶(53-1)
|
||||
const sid = handoff?.structureId;
|
||||
if (sid && structuresPanel.getStructures().some((entry) => entry.structure_id === sid))
|
||||
structuresPanel.selectStructure(sid);
|
||||
},
|
||||
refreshCard: (chainageM) => sectionView.refreshCard(chainageM),
|
||||
revetSelect: (chainageM, key) => stationControls.revetOffset.select(chainageM, key),
|
||||
@@ -492,6 +501,8 @@ export async function mountCrossView(
|
||||
// 계획고를 가게에서 다시 읽어 폼 · 카드 · 종단도를 그린다. 떠나면 카드 칸 관찰을 뗀다.
|
||||
host.onShow(() => {
|
||||
if (currentRouteId === null) return;
|
||||
// 떠난 사이 종단 보기가 폼을 다른 것으로 바꿨을 수 있음 — 같은 넘김값도 다시 적용(53-1)
|
||||
appliedPick = null;
|
||||
void structuresPanel.load().then(restoreStructurePick);
|
||||
void refreshDetailForStructures().then(() => reconcileStaleDesigns());
|
||||
});
|
||||
|
||||
@@ -88,7 +88,8 @@ export function restoreStructurePick(
|
||||
selectStation(handoff.at);
|
||||
// 구조물 id 가 적혀 있을 때만 구조물로 연다 — 관 목록이 아직 안 실렸을 때 측점만으로 구조물을
|
||||
// 짐작하면 그 측점을 덮는 다른 구조물이 열린다(관은 목록이 실릴 때 위 `selectStation` 이 연다).
|
||||
if (!structures.hasSelection() && handoff.structureId) {
|
||||
// id 가 있으면 측점이 연 관보다 먼저다(resolveSelection ① · 53-1)
|
||||
if (handoff.structureId) {
|
||||
structures.selectAt({ chainageM: handoff.at, structureId: handoff.structureId });
|
||||
}
|
||||
return structures.hasSelection();
|
||||
|
||||
@@ -326,6 +326,8 @@ export function createStructuresSection(
|
||||
commitTempPipe();
|
||||
syncOptionLock();
|
||||
syncRangeDisplay();
|
||||
// 고르던 항목의 측점도 값 — 다른 칸처럼 바로 반영(53-1)
|
||||
if (positionConfirmed) liveCommit();
|
||||
}
|
||||
|
||||
/** 타입의 옵션 스키마대로 입력 칸을 다시 그린다 — 그리는 규칙은 `_Structures_Panel_Options`. */
|
||||
|
||||
@@ -109,6 +109,13 @@ function clearSideSlots(): void {
|
||||
// 추가 기슭막이 칸은 고른 다단 벽이 있을 때만 서 있는다 — 비면 그룹째 내린다.
|
||||
if (target === sideSlots?.extra) group.hidden = true;
|
||||
}
|
||||
// 칸이 다른 명세로 옮겨 간 뒤 옛 상자에 남은 강조까지 끔(53-1)
|
||||
document
|
||||
.querySelectorAll<HTMLElement>("fieldset.is-active > legend[data-base-label]")
|
||||
.forEach((legend) => {
|
||||
legend.parentElement!.classList.remove("is-active");
|
||||
setGroupLabel(legend.parentElement!, "");
|
||||
});
|
||||
}
|
||||
|
||||
/** 행 DOM을 제자리 표식과 함께 옮긴다 — 숨은 행은 그대로 둔다. */
|
||||
@@ -156,8 +163,8 @@ function relocateSideRows(panelRoot: HTMLElement): void {
|
||||
// 이름표에 지금 만지는 구조물을 적는다 — "유입구 (기슭막이)", "유출구 (기슭막이(독립))".
|
||||
// 추가 기슭막이 칸은 이름 자체가 그 단이라 괄호 안에 단 번호가 온다(2026-08-30 지시 1).
|
||||
setGroupLabel(group, panelRoot.dataset.panelTitle ?? "");
|
||||
// 다단은 칸이 새로 서는 것이라 화면 밖일 수 있다 — 그 자리로 스크롤한다(지시 2).
|
||||
if (side === "extra") group.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
||||
// 고른 칸이 화면 밖일 수 있다 — 그 자리로 스크롤한다(지시 2 · 유입 · 유출도 53-1).
|
||||
group.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
|
||||
function ensureDock(): void {
|
||||
|
||||
@@ -38,8 +38,10 @@ import {
|
||||
} from "../common_util/common_util_summary_item";
|
||||
import { readStructurePick } from "../B05_Profile/B05_Profile_UI_Structure_Pick_Session";
|
||||
import {
|
||||
coversChainage,
|
||||
NO_SELECTION,
|
||||
PIPE_MATCH_M,
|
||||
readSelection,
|
||||
resolveSelection,
|
||||
selectDesign,
|
||||
type DesignSelection,
|
||||
@@ -170,6 +172,23 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
pushMarks();
|
||||
if (deps.projectId) writePendingStructures(deps.projectId, structures);
|
||||
deps.onStructuresChanged?.();
|
||||
// 고른 구조물을 다른 측점으로 옮기면 횡단 카드도 따라감(53-1)
|
||||
const picked = readSelection(deps.projectId);
|
||||
const moved = picked?.structureId
|
||||
? structures.find((entry) => entry.structure_id === picked.structureId)
|
||||
: undefined;
|
||||
if (moved && picked?.chainageM != null && !coversChainage(moved, picked.chainageM)) {
|
||||
const id = moved.structure_id ?? null;
|
||||
preferredId = id;
|
||||
deps.focusChainage(structureAnchorM(moved));
|
||||
preferredId = null;
|
||||
deps.markStructure?.(id); // 카드 고름이 옛 자리로 알약을 풀었으면 다시 세움
|
||||
selectDesign(
|
||||
deps.projectId,
|
||||
{ chainageM: structureAnchorM(moved), structureId: id },
|
||||
{ from: "b06" },
|
||||
);
|
||||
}
|
||||
},
|
||||
onSelect: (structure) => {
|
||||
if (!structure) return;
|
||||
@@ -552,11 +571,17 @@ export function wireStructureSelection(
|
||||
|
||||
const origRevet = stationControls.revetOffset.select;
|
||||
stationControls.revetOffset.select = (chainageM, key) => {
|
||||
markWallSelecting();
|
||||
// 다른 카드 부재를 고르는 중에 옛 카드가 제 선택을 푸는 것 — 폼 · 선택 칸은 새 것 몫(53-1)
|
||||
const nested = wallSelecting;
|
||||
// 푸는 것은 고름이 아님 — 표시를 켜면 뒤이은 카드 고름이 선택 칸에 안 적힘(53-1)
|
||||
if (key) markWallSelecting();
|
||||
origRevet(chainageM, key);
|
||||
if (key) remember?.(ownerChainageOf(chainageM), key);
|
||||
if (key) panel.showAtChainage(ownerChainageOf(chainageM));
|
||||
else panel.releasePipe();
|
||||
else if (!nested) {
|
||||
remember?.(ownerChainageOf(chainageM), ""); // 푼 부재가 B05 ↔ B06 오갈 때 다시 안 열림
|
||||
panel.releasePipe();
|
||||
}
|
||||
if (key) syncInletStructure();
|
||||
// 폼이 이 시설로 갈아 끼워진 뒤에 이식을 다시 돌린다 — 목적지 칸이 그제야 선다.
|
||||
refreshAdjustSlots();
|
||||
@@ -564,22 +589,30 @@ export function wireStructureSelection(
|
||||
|
||||
const origFord = stationControls.ford.select;
|
||||
stationControls.ford.select = (chainageM, role) => {
|
||||
markWallSelecting();
|
||||
const nested = wallSelecting;
|
||||
if (role) markWallSelecting();
|
||||
origFord(chainageM, role);
|
||||
if (role) remember?.(chainageM, role);
|
||||
if (role) panel.showAtChainage(chainageM);
|
||||
else panel.releasePipe();
|
||||
else if (!nested) {
|
||||
remember?.(chainageM, "");
|
||||
panel.releasePipe();
|
||||
}
|
||||
// 폼이 이 시설로 갈아 끼워진 뒤에 이식을 다시 돌린다 — 날개벽 칸이 그제야 선다
|
||||
// (배관 기슭막이와 같은 순서, 2026-08-30 사용자 지시 1).
|
||||
refreshAdjustSlots();
|
||||
};
|
||||
const origBox = stationControls.box.select;
|
||||
stationControls.box.select = (chainageM, role) => {
|
||||
markWallSelecting();
|
||||
const nested = wallSelecting;
|
||||
if (role) markWallSelecting();
|
||||
origBox(chainageM, role);
|
||||
if (role) remember?.(chainageM, role);
|
||||
if (role) panel.showAtChainage(chainageM);
|
||||
else panel.releasePipe();
|
||||
else if (!nested) {
|
||||
remember?.(chainageM, "");
|
||||
panel.releasePipe();
|
||||
}
|
||||
// 폼이 이 시설로 갈아 끼워진 뒤에 이식을 다시 돌린다 — 날개벽 칸이 그제야 선다.
|
||||
refreshAdjustSlots();
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
type DesignPageShared,
|
||||
} from "../A00_Common/design/design_shared";
|
||||
import { readState } from "../A00_Common/b_page_state";
|
||||
import { coversChainage } from "../A00_Common/design/design_selection";
|
||||
import { fetchLatestRoute, type RouteLatestResponse } from "../B05_Profile/B05_Profile_Api_Fetch";
|
||||
import type {
|
||||
CrossSection,
|
||||
@@ -133,6 +134,8 @@ export function createSectionProfile(
|
||||
let profile: DesignProfile | null = null;
|
||||
let detail: SectionDetailResponse | null = null;
|
||||
let marks: ReadonlyArray<StructureInstance> = [];
|
||||
/** 마지막으로 세운 알약 — 카드 고름이 측점선을 맞출 때 지킬지 가름(53-1). */
|
||||
let markedId: string | null = null;
|
||||
let types: ReadonlyArray<StructureType> = [];
|
||||
let stations: IrregularStation[] = [];
|
||||
let intervalM = 20;
|
||||
@@ -253,6 +256,7 @@ export function createSectionProfile(
|
||||
profile?.redraw();
|
||||
},
|
||||
selectMark(structureId) {
|
||||
markedId = structureId;
|
||||
profile?.setSelectedStructure(structureId);
|
||||
},
|
||||
showCard(section) {
|
||||
@@ -265,6 +269,10 @@ export function createSectionProfile(
|
||||
(entry) => Math.abs(entry.chainage_m - section.chainage_m) < MATCH_M,
|
||||
);
|
||||
profile.setSelectedStation(pipe ? irregularStationId(pipe.id) : section.station_id);
|
||||
// 일반 측점은 측점선 짝 알약이 없어 꺼짐 — 고른 구조물이 이 측점을 덮으면 그 알약을 지킴(53-1)
|
||||
const marked = markedId ? marks.find((entry) => entry.structure_id === markedId) : undefined;
|
||||
if (!pipe && marked && coversChainage(marked, section.chainage_m))
|
||||
profile.setSelectedStructure(markedId);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -80,8 +80,10 @@ sel.selectDesign(P, { chainageM: 205, structureId: "wall" }, { partKey: "own", f
|
||||
out.store1 = sel.readSelection(P);
|
||||
sel.selectDesign(P, { chainageM: 205, structureId: "wall" }, { from: "b06" });
|
||||
out.sameQuiet = heard.length; // 같은 값 · 부재키 생략 → 알리지 않음
|
||||
sel.selectDesign(P, { chainageM: 205.2, structureId: "wall" }, { from: "b06" });
|
||||
out.keyKept = sel.readSelection(P)?.partKey; // 같은 것을 다시 적으면 부재키를 지킨다
|
||||
sel.selectDesign(P, { chainageM: 173.09, structureId: null }, { from: "b05" });
|
||||
out.keyKept = sel.readSelection(P)?.partKey; // 부재키를 안 주면 지킨다(옛 규칙)
|
||||
out.keyDropped = sel.readSelection(P)?.partKey ?? null; // 다른 구조물로 옮기면 버린다(53-1)
|
||||
sel.selectDesign(P, sel.NO_SELECTION, { from: "b05" });
|
||||
out.cleared = sel.readSelection(P);
|
||||
out.heardFrom = heard.map((h) => [h.chainageM, h.structureId, h.from]);
|
||||
@@ -150,7 +152,7 @@ out.apply = {
|
||||
};
|
||||
|
||||
// ── B05 진입 때 되살리기 — 구조물 id 가 있을 때만 구조물로 연다 ─────────────
|
||||
const restoreRun = (pickValue) => {
|
||||
const restoreRun = (pickValue, stationOpens = false) => {
|
||||
sel.selectDesign(P, sel.NO_SELECTION);
|
||||
if (pickValue) sel.selectDesign(P, pickValue.selection, { partKey: pickValue.key });
|
||||
const calls = [];
|
||||
@@ -163,15 +165,17 @@ const restoreRun = (pickValue) => {
|
||||
},
|
||||
};
|
||||
const controls = { select: (p) => calls.push(["3d", p.chainageM, p.key ?? null]) };
|
||||
const done = pickSession.restoreStructurePick(controls, P, section, (c) =>
|
||||
calls.push(["station", c]),
|
||||
);
|
||||
const done = pickSession.restoreStructurePick(controls, P, section, (c) => {
|
||||
calls.push(["station", c]);
|
||||
opened = stationOpens; // 같은 측점 관이 먼저 열림
|
||||
});
|
||||
return { calls, done };
|
||||
};
|
||||
out.restore = {
|
||||
pipeOnly: restoreRun({ selection: { chainageM: 85.05, structureId: null } }),
|
||||
structure: restoreRun({ selection: { chainageM: 205, structureId: "wall" }, key: "own" }),
|
||||
nothing: restoreRun(null),
|
||||
structureOverPipe: restoreRun({ selection: { chainageM: 440.24, structureId: "mirror" } }, true),
|
||||
};
|
||||
sel.selectDesign(P, sel.NO_SELECTION);
|
||||
|
||||
|
||||
@@ -105,13 +105,19 @@ def test_세션에_적고_읽는다(out):
|
||||
|
||||
def test_부재키를_안_주면_지키고_해제하면_비운다(out):
|
||||
assert out["keyKept"] == "own"
|
||||
assert out["keyDropped"] is None # 다른 구조물로 옮기면 옛 부재가 안 따라옴(53-1)
|
||||
assert out["cleared"] is None
|
||||
|
||||
|
||||
def test_같은_값은_알리지_않고_출처를_함께_알린다(out):
|
||||
assert out["sameQuiet"] == 1
|
||||
assert out["heardFrom"] == [[205, "wall", "b06"], [173.09, None, "b05"], [None, None, "b05"]]
|
||||
assert out["afterOff"] == 3 # 구독을 끊으면 더는 안 들림
|
||||
assert out["heardFrom"] == [
|
||||
[205, "wall", "b06"],
|
||||
[205.2, "wall", "b06"],
|
||||
[173.09, None, "b05"],
|
||||
[None, None, "b05"],
|
||||
]
|
||||
assert out["afterOff"] == 4 # 구독을 끊으면 더는 안 들림
|
||||
|
||||
|
||||
def test_알림_도중_다시_낸_값은_적기만_한다(out):
|
||||
@@ -172,6 +178,13 @@ def test_B05_되살리기_적힌_것이_없으면_끝난_것(out):
|
||||
assert out["restore"]["nothing"] == {"calls": [], "done": True}
|
||||
|
||||
|
||||
def test_B05_되살리기_같은_측점_관보다_구조물_id가_먼저(out):
|
||||
# B06 에서 고른 반사경(22+0) — 측점이 관을 먼저 열어도 id 의 구조물로 연다(53-1)
|
||||
over = out["restore"]["structureOverPipe"]
|
||||
assert over["calls"][-1] == ["selectAt", 440.24, "mirror"]
|
||||
assert over["done"] is True
|
||||
|
||||
|
||||
# ── 배선 ───────────────────────────────────────────────────────────────
|
||||
def test_횡단도_벽_구체_눌러도_폼은_같은_길_하나():
|
||||
panel = _read("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts")
|
||||
|
||||
@@ -0,0 +1,607 @@
|
||||
/* 53-1 — B06 횡단도에서 구조물을 누르면 좌측 패널이 그 구조물로 켜지는가(전수)
|
||||
* 시작 화면: B06 횡단설계(#/b06-section) · 창마다 자기 검증 프로젝트 · 로그인 상태
|
||||
* 부르는 법: orca eval 은 30초 안팎에 끊김 — 단계는 시작만 하고 결과는 따로 읽음
|
||||
* orca eval --expression "window.__m531='setup'; $(cat 이 파일)" → 초안 사진 + 표 20종 중 없는 종류를 세션에만 넣음(저장 안 함)
|
||||
* orca eval --expression "window.__m531='check'; $(cat 이 파일)" → 전수 목록(벽 · 구체 · 카드 · 같은 측점 · 값 · 옮기기)
|
||||
* orca eval --expression "window.__m531='handoff'; $(cat 이 파일)" → 저장 안 하고 B06 → B05 → B06
|
||||
* orca eval --expression "window.__m531='restore'; $(cat 이 파일)" → 초안을 사진대로 · 뒤에 orca reload
|
||||
* 결과: orca eval --expression "JSON.stringify(window.__r531)" (끝나기 전엔 null)
|
||||
* 쓰는 것: sessionStorage 만(서버 저장 없음 · 공유 storage 안 씀)
|
||||
* 기대 수치: check · handoff 의 bad = [] · rows 마다 ok = true
|
||||
* ① 종류 · 기준 측점 = 누른 것 ② 컨테이너 몫(유입 · 유출 · N단 · 좌우)이면 그 상자 강조 + 보임
|
||||
* ③ 목록 강조 1 · 종단 알약 1 ④ 좌측 값 → 횡단 다시 그림 ⑤ 다시 누름 = 풂 · 빈 곳 · 같은 측점 · 옮기기 · B05 ↔ B06
|
||||
*/
|
||||
(() => {
|
||||
const MODE = window.__m531 || "check";
|
||||
window.__r531 = null;
|
||||
const PROJECT = localStorage.getItem("frd_current_project_id");
|
||||
const SNAP = "ui531-snap";
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const until = async (fn, ms = 20000) => {
|
||||
for (let t = 0; t < ms; t += 250) {
|
||||
if (fn()) return true;
|
||||
await w(250);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const sec = () => document.querySelector(".b05-structure-section");
|
||||
const selects = () => [...sec().querySelectorAll(".b05-structure__grid select")];
|
||||
const fire = (el, v) => {
|
||||
el.value = v;
|
||||
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
};
|
||||
const flat = (el) => el.innerText.replace(/\s+/g, " ").trim();
|
||||
const listRows = () => [...document.querySelectorAll(".b05-route__irregular-list li")];
|
||||
const cards = () => [...document.querySelectorAll("article.b06-cross-card")];
|
||||
const chainOf = (card) => {
|
||||
const m = card.id.match(/(\d{9})(\d{3})$/);
|
||||
return m ? Number(m[1]) + Number(m[2]) / 1000 : NaN;
|
||||
};
|
||||
const selectedCard = () => document.querySelector(".b06-cross-card--selected");
|
||||
const click = (el) => el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
/** 지금 좌측 패널 · 목록 · 알약 · 선택 칸. */
|
||||
const state = () => {
|
||||
const pickKey = Object.keys(sessionStorage).find(
|
||||
(k) => k.includes("structure-pick") && k.includes(PROJECT),
|
||||
);
|
||||
let pick = null;
|
||||
try {
|
||||
pick = JSON.parse(sessionStorage.getItem(pickKey));
|
||||
} catch {
|
||||
/* 없음 */
|
||||
}
|
||||
const box = [...document.querySelectorAll("fieldset.is-active")].map((f) => {
|
||||
const r = f.getBoundingClientRect();
|
||||
let sc = f.parentElement;
|
||||
while (sc && !/(auto|scroll)/.test(getComputedStyle(sc).overflowY)) sc = sc.parentElement;
|
||||
const b = sc ? sc.getBoundingClientRect() : { top: 0, bottom: innerHeight };
|
||||
const seen = r.height > 0 && r.top < Math.min(b.bottom, innerHeight) && r.bottom > b.top;
|
||||
return { legend: flat(f.querySelector("legend")), seen };
|
||||
});
|
||||
const anchor = sec().querySelectorAll(".b05-structure__position-row > label")[2];
|
||||
return {
|
||||
type: selects()[1]?.value ?? "",
|
||||
anchor: anchor ? [...anchor.querySelectorAll("input")].map((i) => i.value).join("+") : "",
|
||||
pick,
|
||||
box,
|
||||
list: listRows()
|
||||
.filter((li) => li.classList.contains("is-selected"))
|
||||
.map(flat),
|
||||
pill: [...document.querySelectorAll(".b05-structure__mark.is-selected")].map((m) => m.title),
|
||||
card: selectedCard()?.id ?? null,
|
||||
};
|
||||
};
|
||||
const bad = [];
|
||||
const known = []; // 고치지 않은 흠(횡단 그리기 몫) — 보고에 올림
|
||||
const rows = [];
|
||||
const judge = (name, ok, s, note = "") => {
|
||||
rows.push({
|
||||
name,
|
||||
ok,
|
||||
note,
|
||||
type: s.type,
|
||||
key: s.pick?.key ?? null,
|
||||
box: s.box,
|
||||
list: s.list,
|
||||
pill: s.pill.length,
|
||||
card: s.card?.slice(-9) ?? null,
|
||||
});
|
||||
if (!ok) bad.push(name);
|
||||
};
|
||||
/** 부재키 → 강조돼야 할 상자 이름. own(사면 구조물)은 컨테이너가 없음. */
|
||||
const boxFor = (key) =>
|
||||
key === "inlet"
|
||||
? /유입|좌|우/
|
||||
: key === "outlet"
|
||||
? /유출|좌|우/
|
||||
: /^b?extra/.test(key ?? "")
|
||||
? /추가/
|
||||
: key === "left" || key === "right"
|
||||
? /유입|유출/
|
||||
: null;
|
||||
const boxOk = (s) => {
|
||||
const want = boxFor(s.pick?.key);
|
||||
if (!want) return s.box.length === 0;
|
||||
return s.box.length === 1 && want.test(s.box[0].legend) && s.box[0].seen;
|
||||
};
|
||||
const commonOk = (s) =>
|
||||
!!s.type && s.list.length === 1 && s.pill.length === 1 && !!s.pick && boxOk(s);
|
||||
const hitsOf = (card) => [
|
||||
...card.querySelectorAll(".b06-chart__culvert-revet-hit, .b06-chart__box-hit"),
|
||||
];
|
||||
const uniqueHits = (card) => [...new Set(hitsOf(card))];
|
||||
/** 카드 고름을 풂(같은 카드 다시 누름 = 풂). */
|
||||
const clearCard = async () => {
|
||||
const c = selectedCard();
|
||||
if (c) {
|
||||
click(c.querySelector("svg"));
|
||||
await w(900);
|
||||
}
|
||||
};
|
||||
const openProfile = async () => {
|
||||
const prof = document.querySelector(".b05-route-profile");
|
||||
if (prof?.classList.contains("is-collapsed")) {
|
||||
document.querySelector(".ui-workflow-overlay__handle--bottom")?.click();
|
||||
await w(2500);
|
||||
}
|
||||
// 뒤에 깔린 탭은 그리기가 밀림 — 알약이 설 때까지 크기 알림을 보냄
|
||||
for (let i = 0; i < 20 && !document.querySelector(".b05-structure__mark"); i++) {
|
||||
dispatchEvent(new Event("resize"));
|
||||
await w(500);
|
||||
}
|
||||
};
|
||||
/** 세션에만 넣는 종류(표 01~08) — 이미 있으면 건너뜀. [군, 종류, 측점, +m, 길이(구간형)] */
|
||||
const ADD = [
|
||||
["G", "pavement_concrete", 22, 0, 10],
|
||||
["A", "cross_underdrain", 22, 0],
|
||||
["E", "mirror", 22, 0],
|
||||
["A", "ford_pavement", 3, 0],
|
||||
["A", "open_ditch", 5, 0],
|
||||
["A", "cross_drain_exposed", 6, 0],
|
||||
["E", "evac_road", 9, 0],
|
||||
["E", "guardrail", 11, 0, 10],
|
||||
["E", "curb", 14, 0],
|
||||
["E", "hazard_sign", 15, 0],
|
||||
["E", "position_sign", 18, 0],
|
||||
["E", "water_intake", 19, 0],
|
||||
["E", "completion_sign", 20, 0],
|
||||
["E", "barrier_gate", 21, 0],
|
||||
["E", "spoil_bank", 24, 0],
|
||||
["E", "borrow_pit", 25, 0],
|
||||
["호환", "etc", 27, 0],
|
||||
];
|
||||
const addOne = async ([g, t, no, plus, len]) => {
|
||||
const btn = (label) => [...sec().querySelectorAll("button")].find((b) => flat(b) === label);
|
||||
btn("리셋")?.click();
|
||||
await w(400);
|
||||
const n = listRows().length; // A군은 측점을 넣는 순간 임시로 목록에 섬
|
||||
const [gs, ts] = selects();
|
||||
fire(gs, g);
|
||||
await w(300);
|
||||
if (![...ts.options].some((o) => o.value === t)) return `${t}: 종류 없음`;
|
||||
fire(ts, t);
|
||||
await w(1200);
|
||||
const labels = [...sec().querySelectorAll(".b05-structure__position-row > label")];
|
||||
const anchor = labels[2].querySelectorAll("input");
|
||||
fire(anchor[0], String(no));
|
||||
await w(200);
|
||||
fire(anchor[1], String(plus));
|
||||
await w(300);
|
||||
const field = (re) =>
|
||||
[...sec().querySelectorAll("label")]
|
||||
.filter((l) => l.offsetParent)
|
||||
.find((l) => re.test(flat(l)))
|
||||
?.querySelector("input");
|
||||
if (len) {
|
||||
// 구간형 — 길이 · 전(시작 = 기준 − 전), 사토장은 시작 · 종료 칸
|
||||
if (field(/^길이/)) {
|
||||
fire(field(/^길이/), String(len));
|
||||
fire(field(/^기준측점 전/), String(len / 2));
|
||||
}
|
||||
if (!labels[1].hidden) {
|
||||
const [s0, s1] = labels[1].querySelectorAll("input");
|
||||
const [e0, e1] = labels[3].querySelectorAll("input");
|
||||
fire(s0, String(no - 1));
|
||||
fire(s1, "10");
|
||||
fire(e0, String(no));
|
||||
fire(e1, "10");
|
||||
}
|
||||
await w(300);
|
||||
} else if (!labels[1].hidden) {
|
||||
const [s0, s1] = labels[1].querySelectorAll("input");
|
||||
const [e0, e1] = labels[3].querySelectorAll("input");
|
||||
fire(s0, String(no - 1));
|
||||
fire(s1, "10");
|
||||
fire(e0, String(no));
|
||||
fire(e1, "10");
|
||||
await w(300);
|
||||
}
|
||||
btn("추가")?.click();
|
||||
await w(1500);
|
||||
return listRows().length > n ? null : `${t}: 안 들어감`;
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
// 뒤에 깔린 탭(ORCA)은 화면 프레임이 안 돎 — rAF 를 기다리는 넘김 · 스크롤이 멈춤. 시험 동안만 시간으로 대신
|
||||
let frame = false;
|
||||
requestAnimationFrame(() => (frame = true));
|
||||
await w(300);
|
||||
if (!frame && !window.__raf531) {
|
||||
window.__raf531 = window.requestAnimationFrame;
|
||||
window.requestAnimationFrame = (cb) => setTimeout(() => cb(performance.now()), 16);
|
||||
}
|
||||
await until(() => listRows().length > 0 && cards().length > 0);
|
||||
await w(1000);
|
||||
const out = { mode: MODE, project: PROJECT };
|
||||
|
||||
if (MODE === "setup") {
|
||||
if (!sessionStorage.getItem(SNAP))
|
||||
sessionStorage.setItem(
|
||||
SNAP,
|
||||
JSON.stringify(
|
||||
Object.entries(sessionStorage).filter(([k]) => k.startsWith("aislo:draft:")),
|
||||
),
|
||||
);
|
||||
const have = new Set(listRows().map((li) => flat(li).replace(/^[\d+~ ]+/, "")));
|
||||
out.added = [];
|
||||
for (const item of ADD) {
|
||||
const [g, t] = item;
|
||||
selects()[0] && fire(selects()[0], g);
|
||||
await w(200);
|
||||
const name = [...selects()[1].options].find((o) => o.value === t)?.text ?? t;
|
||||
if ([...have].some((h) => h.startsWith(name))) continue;
|
||||
const err = await addOne(item);
|
||||
out.added.push(err ?? name);
|
||||
if (err) bad.push(err);
|
||||
}
|
||||
sec()
|
||||
.querySelectorAll("button")
|
||||
.forEach((b) => flat(b) === "리셋" && b.click());
|
||||
// 다단 벽(추가 기슭막이 N단) — 관 유출 벽에 한 단 더함(세션만 · 단이 설 자리가 있는 관까지 차례로)
|
||||
out.tier = null;
|
||||
for (const card of cards().filter((c) => /유출 기슭막이 선택/.test(c.innerHTML))) {
|
||||
if (/추가\(성토부\)/.test(card.innerHTML)) {
|
||||
out.tier = card.id;
|
||||
break;
|
||||
}
|
||||
const outlet = () =>
|
||||
[
|
||||
...document.getElementById(card.id).querySelectorAll(".b06-chart__culvert-revet-hit"),
|
||||
].find((h) => /유출/.test(h.querySelector("title")?.textContent ?? ""));
|
||||
click(outlet());
|
||||
await w(1400);
|
||||
[...sec().querySelectorAll(".b06-structure-panel__struct--tier button")]
|
||||
.find((b) => b.offsetParent && /추가/.test(b.title))
|
||||
?.click();
|
||||
await w(2500);
|
||||
const added = /추가\(성토부\)/.test(document.getElementById(card.id).innerHTML);
|
||||
click(outlet()); // 풂
|
||||
await w(1000);
|
||||
if (added) {
|
||||
out.tier = card.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!out.tier) bad.push("다단 벽 못 세움");
|
||||
out.rows = listRows().map(flat);
|
||||
}
|
||||
|
||||
if (MODE === "check") {
|
||||
await openProfile();
|
||||
// ── 가. 횡단도에 그려진 고를 거리 전부 — 다른 카드에서 처음 누름 · 같은 카드 다른 부재 ──
|
||||
await clearCard();
|
||||
const targets = cards().filter((c) => hitsOf(c).length);
|
||||
for (const card of targets) {
|
||||
const n = uniqueHits(card).length;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const hit = uniqueHits(document.getElementById(card.id))[i];
|
||||
const before = state();
|
||||
click(hit);
|
||||
await w(1400);
|
||||
let s = state();
|
||||
// 이미 고른 부재였으면 풀렸을 것 — 한 번 더 눌러 고름
|
||||
if (!s.pick?.key && before.pick?.key) {
|
||||
click(uniqueHits(document.getElementById(card.id))[i]);
|
||||
await w(1400);
|
||||
s = state();
|
||||
}
|
||||
judge(`벽 ${card.id.slice(-9)}#${i}`, commonOk(s) && s.card === card.id, s);
|
||||
}
|
||||
}
|
||||
// ── 나. 같은 부재 다시 누름 = 풂(카드 고름은 남음) ──
|
||||
const t0 = targets[0];
|
||||
if (t0) {
|
||||
click(uniqueHits(t0)[0]);
|
||||
await w(1400);
|
||||
if (!state().pick?.key) {
|
||||
click(uniqueHits(document.getElementById(t0.id))[0]);
|
||||
await w(1400);
|
||||
}
|
||||
click(uniqueHits(document.getElementById(t0.id))[0]);
|
||||
await w(1400);
|
||||
const s = state();
|
||||
judge("다시 누름 = 풂", !s.pick?.key && s.box.length === 0 && s.card === t0.id, s);
|
||||
}
|
||||
// ── 다. 빈 곳 — 구조물 카드 · 빈 카드 · 다시 누름 = 카드 풂 ──
|
||||
await clearCard();
|
||||
const pipeCard = targets.find((c) => /유입 집수정|유출 기슭막이/.test(c.innerHTML));
|
||||
if (pipeCard) {
|
||||
click(pipeCard.querySelector("svg"));
|
||||
await w(1400);
|
||||
let s = state();
|
||||
judge("빈 곳(구조물 카드)", !!s.type && !s.pick?.key && s.box.length === 0, s);
|
||||
click(document.getElementById(pipeCard.id).querySelector("svg"));
|
||||
await w(1400);
|
||||
s = state();
|
||||
judge("빈 곳 다시 = 카드 풂", !s.card && !s.type && !s.pick, s);
|
||||
}
|
||||
const used = new Set(listRows().map((li) => flat(li).match(/^(\d+)\+/)?.[1]));
|
||||
const empty = cards().find((c) => !used.has(String(Math.floor(chainOf(c) / 20))));
|
||||
if (empty) {
|
||||
click(empty.querySelector("svg"));
|
||||
await w(1400);
|
||||
const s = state();
|
||||
judge("빈 카드", !s.type && s.box.length === 0 && s.card === empty.id, s);
|
||||
await clearCard();
|
||||
}
|
||||
// ── 라. 표 20종 — 목록으로 골라 종류 확인 → 그 카드 빈 곳 눌러 같은 것이 켜지나 ──
|
||||
const rowsNow = listRows().map(flat);
|
||||
for (const text of rowsNow) {
|
||||
const li = listRows().find((l) => flat(l) === text);
|
||||
if (!li) continue;
|
||||
await clearCard();
|
||||
(li.querySelector("button") ?? li).click();
|
||||
await w(1400);
|
||||
const viaList = state();
|
||||
const card = viaList.card && document.getElementById(viaList.card);
|
||||
judge(
|
||||
`목록 ${text}`,
|
||||
!!viaList.type && viaList.list.length === 1 && !!card && viaList.pill.length === 1,
|
||||
viaList,
|
||||
);
|
||||
if (!card) continue;
|
||||
await clearCard();
|
||||
click(card.querySelector("svg"));
|
||||
await w(1400);
|
||||
const viaCard = state();
|
||||
const same = viaCard.type === viaList.type;
|
||||
if (same)
|
||||
judge(`카드 ${text}`, viaCard.list.length === 1 && viaCard.pill.length === 1, viaCard);
|
||||
// 같은 카드에 관(그려진 것)이 있으면 관이 먼저 — 그림 없는 구조물은 목록으로만(흠 · 그리기 몫)
|
||||
else if (uniqueHits(card).length && viaCard.type)
|
||||
known.push(`카드 ${text} → ${viaCard.list[0] ?? viaCard.type}`);
|
||||
else judge(`카드 ${text}`, false, viaCard, `카드는 ${viaCard.type || "빈 것"}`);
|
||||
}
|
||||
await clearCard();
|
||||
// ── 마. 같은 측점 여럿 — 목록 구조물 → 그 카드 관 벽 → 다시 목록 ──
|
||||
const shared = rowsNow.find(
|
||||
(t) =>
|
||||
!/배관/.test(t) &&
|
||||
rowsNow.some((u) => /배관/.test(u) && u.split(" ")[0] === t.split(" ")[0]),
|
||||
);
|
||||
if (shared) {
|
||||
const pipeRow = rowsNow.find(
|
||||
(u) => /배관/.test(u) && u.split(" ")[0] === shared.split(" ")[0],
|
||||
);
|
||||
const pick = (t) => {
|
||||
const li = listRows().find((l) => flat(l) === t);
|
||||
(li.querySelector("button") ?? li).click();
|
||||
};
|
||||
pick(pipeRow);
|
||||
await w(1400);
|
||||
const pc = selectedCard();
|
||||
pick(shared);
|
||||
await w(1400);
|
||||
let s = state();
|
||||
judge(`같은 측점: 목록 ${shared}`, s.type !== "pipe" && !s.pick?.key && !!s.pick?.sid, s);
|
||||
const wall = pc && uniqueHits(document.getElementById(pc.id))[0];
|
||||
if (wall) {
|
||||
click(wall);
|
||||
await w(1400);
|
||||
s = state();
|
||||
judge("같은 측점: 그 카드 관 벽", s.type === "pipe" && commonOk(s), s);
|
||||
}
|
||||
pick(shared);
|
||||
await w(1400);
|
||||
s = state();
|
||||
judge("같은 측점: 다시 목록", s.type !== "pipe" && !s.pick?.key && !!s.pick?.sid, s);
|
||||
}
|
||||
// ── 바. 좌측 값 → 횡단 다시 그림 → 되돌림(관 유출 형태 · 세월교 련 수 · 박스 유입 구조) ──
|
||||
const titles = (card) =>
|
||||
[...document.getElementById(card.id).querySelectorAll("title")].map((t) => t.textContent);
|
||||
const ctlOf = (key) =>
|
||||
[...sec().querySelectorAll(`[data-spec-item="${key}"]`)]
|
||||
.find((e) => e.offsetParent)
|
||||
?.querySelector("input, select");
|
||||
const bodyCase = async (card, key, value, re) => {
|
||||
await clearCard();
|
||||
click(uniqueHits(document.getElementById(card.id))[0]);
|
||||
await w(1400);
|
||||
const ctl = ctlOf(key);
|
||||
if (!ctl) return judge(`값 → 횡단: ${key}`, false, state(), "칸 없음");
|
||||
const was = ctl.value;
|
||||
const before = titles(card).join("|");
|
||||
fire(ctl, value);
|
||||
await w(3000);
|
||||
const after = titles(card).join("|");
|
||||
judge(
|
||||
`값 → 횡단: ${key}=${value}`,
|
||||
re(after) && after !== before && boxOk(state()),
|
||||
state(),
|
||||
);
|
||||
fire(ctlOf(key), was);
|
||||
await w(3000);
|
||||
const back = titles(card).join("|");
|
||||
// 빈 칸 = 자동(24-4a) — 서버가 채운 기본 연장을 앞단이 지워 「연장」 표기만 빠짐(흠 · 저장하면 다시 섬)
|
||||
const noSpan = (t) => t.replace(/ · 연장 [\d.]+m/g, "");
|
||||
if (back !== before && noSpan(back) === noSpan(before))
|
||||
known.push(
|
||||
`값 되돌림 ${key}: 벽 설명의 「연장」 표기 빠짐(서버 기본값 · 저장 뒤 다시 섬)`,
|
||||
);
|
||||
judge(`값 되돌림: ${key}`, noSpan(back) === noSpan(before), state());
|
||||
};
|
||||
// 형태 칸은 구조를 고른 관에서만 섬 — 그 칸이 서는 관을 찾음
|
||||
for (const c of targets.filter((c) => /유출 기슭막이 선택/.test(c.innerHTML))) {
|
||||
await clearCard();
|
||||
click(uniqueHits(document.getElementById(c.id))[0]);
|
||||
await w(1400);
|
||||
if (!ctlOf("outlet.form")) continue;
|
||||
await bodyCase(c, "outlet.form", "콘크리트", (t) => /유출 기슭막이 콘크리트/.test(t));
|
||||
break;
|
||||
}
|
||||
const fordCard = targets.find((c) => /련/.test(titles(c).join("|")));
|
||||
if (fordCard) await bodyCase(fordCard, "ford.count", "3", (t) => /3련/.test(t));
|
||||
const boxCard = targets.find((c) => c.querySelector(".b06-chart__box-hit"));
|
||||
if (boxCard)
|
||||
await bodyCase(
|
||||
boxCard,
|
||||
"inlet.structure",
|
||||
"없음",
|
||||
(t) => (t.match(/날개벽 구간/g) ?? []).length === 1,
|
||||
);
|
||||
// ── 사. 옮기기 — 사면 구조물 기준 측점 +2 → 목록 · 카드 · 알약 · 선택 칸이 따라감 → 되돌림 ──
|
||||
await clearCard();
|
||||
// 사면 구조물 차례로 — 필수 칸이 빈 옛 구조물은 측점을 바꿔도 반영이 막히고 그 칸으로 포커스만 감(흠)
|
||||
const input = () =>
|
||||
sec()
|
||||
.querySelectorAll(".b05-structure__position-row > label")[2]
|
||||
.querySelectorAll("input")[0];
|
||||
const move = async (v) => {
|
||||
const el = input();
|
||||
el.focus();
|
||||
fire(el, String(v));
|
||||
el.blur();
|
||||
el.dispatchEvent(new FocusEvent("focusout", { bubbles: true }));
|
||||
await w(8000);
|
||||
return state();
|
||||
};
|
||||
for (const c of targets.filter((c) =>
|
||||
/독립 기슭막이/.test(c.querySelector(".b06-chart__culvert-revet title")?.textContent ?? ""),
|
||||
)) {
|
||||
await clearCard();
|
||||
click(uniqueHits(document.getElementById(c.id))[0]);
|
||||
await w(1400);
|
||||
const s0 = state();
|
||||
const no = Number(input().value);
|
||||
let s = await move(no + 2);
|
||||
if (s.list[0] === s0.list[0]) {
|
||||
const stuck = flat(document.activeElement?.closest("label") ?? document.body).slice(
|
||||
0,
|
||||
12,
|
||||
);
|
||||
known.push(`옮기기 ${s0.list[0]}: 반영 안 됨 — 빈 필수 칸 「${stuck}」 으로 포커스만`);
|
||||
await move(no);
|
||||
continue;
|
||||
}
|
||||
judge(
|
||||
"옮기기: 목록 · 카드 · 알약 · 선택 칸",
|
||||
s.list[0]?.startsWith(`${no + 2}+`) &&
|
||||
Math.abs(chainOf(document.getElementById(s.card)) - (no + 2) * 20) < 1 &&
|
||||
s.pill.length === 1 &&
|
||||
s.pick?.sid === s0.pick?.sid &&
|
||||
Math.abs(s.pick.at - (no + 2) * 20) < 1,
|
||||
s,
|
||||
);
|
||||
s = await move(no);
|
||||
judge(
|
||||
"옮기기 되돌림",
|
||||
s.list[0] === s0.list[0] &&
|
||||
Math.abs(chainOf(document.getElementById(s.card)) - no * 20) < 1 &&
|
||||
s.pill.length === 1,
|
||||
s,
|
||||
);
|
||||
break;
|
||||
}
|
||||
await clearCard();
|
||||
}
|
||||
|
||||
if (MODE === "handoff") {
|
||||
await openProfile();
|
||||
const go = async (hash) => {
|
||||
location.hash = hash;
|
||||
await until(() => listRows().length > 0 && selects()[1]?.value);
|
||||
await w(5000);
|
||||
return state();
|
||||
};
|
||||
if (!location.hash.includes("b06")) await go("#/b06-section");
|
||||
// 같은 측점에 다른 구조물이 있는 관을 고름(없으면 아무 관)
|
||||
const texts = listRows().map(flat);
|
||||
const pipeRow =
|
||||
texts.find(
|
||||
(t) =>
|
||||
/배관/.test(t) &&
|
||||
texts.some((u) => !/배관/.test(u) && u.split(" ")[0] === t.split(" ")[0]),
|
||||
) ?? texts.find((t) => /배관/.test(t));
|
||||
await clearCard();
|
||||
for (let i = 0; i < 2 && !selectedCard(); i++) {
|
||||
// 목록 다시 누름 = 풂 — 이미 골라져 있었으면 한 번 더
|
||||
const li = listRows().find((l) => flat(l) === pipeRow);
|
||||
(li.querySelector("button") ?? li).click();
|
||||
await w(1400);
|
||||
}
|
||||
const pipeCard = selectedCard();
|
||||
const outletHit = () =>
|
||||
uniqueHits(document.getElementById(pipeCard.id)).find((h) =>
|
||||
/유출/.test(h.querySelector("title")?.textContent ?? ""),
|
||||
);
|
||||
await clearCard();
|
||||
click(outletHit());
|
||||
await w(1400);
|
||||
const a = state();
|
||||
const b05 = await go("#/b05-profile");
|
||||
const b06 = await go("#/b06-section");
|
||||
judge(
|
||||
"관 유출 → B05",
|
||||
b05.type === a.type && b05.anchor === a.anchor && b05.list[0] === a.list[0],
|
||||
b05,
|
||||
);
|
||||
judge(
|
||||
"B05 → B06 유출 그대로",
|
||||
b06.pick?.key === "outlet" && boxOk(b06) && b06.list[0] === a.list[0],
|
||||
b06,
|
||||
);
|
||||
// 같은 측점 다른 구조물을 목록으로 — 부재키가 따라오지 않고 그 구조물이 열림
|
||||
const station = a.list[0]?.split(" ")[0];
|
||||
const other = listRows().find(
|
||||
(li) => flat(li).startsWith(station + " ") && !/배관/.test(flat(li)),
|
||||
);
|
||||
if (other) {
|
||||
const otherText = flat(other);
|
||||
(other.querySelector("button") ?? other).click();
|
||||
await w(1400);
|
||||
const c = state();
|
||||
const b05b = await go("#/b05-profile");
|
||||
const b06b = await go("#/b06-section");
|
||||
judge("같은 측점 구조물 → B05", b05b.type === c.type && !b05b.pick?.key, b05b);
|
||||
judge("B05 → B06 그 구조물", b06b.type === c.type && b06b.box.length === 0, b06b);
|
||||
// B06 에서 관 카드를 고른 채 B05 에서 같은 측점 다른 구조물을 고르고 돌아옴 — 카드가 이미 골라져 있어도 그 구조물
|
||||
const pick = (t) => {
|
||||
const row = listRows().find((l) => flat(l) === t);
|
||||
(row.querySelector("button") ?? row).click();
|
||||
};
|
||||
pick(pipeRow);
|
||||
await w(1400);
|
||||
await go("#/b05-profile");
|
||||
pick(otherText);
|
||||
await w(1400);
|
||||
const d = state();
|
||||
const b06c = await go("#/b06-section");
|
||||
judge(
|
||||
"B06 카드 골라진 채 B05 에서 다른 것 → B06",
|
||||
b06c.type === d.type && b06c.type !== "pipe",
|
||||
b06c,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (MODE === "restore") {
|
||||
const snap = sessionStorage.getItem(SNAP);
|
||||
if (!snap) out.error = "사진 없음";
|
||||
else {
|
||||
for (const k of Object.keys(sessionStorage))
|
||||
if (k.startsWith("aislo:draft:") || k.includes("section-detail"))
|
||||
sessionStorage.removeItem(k);
|
||||
for (const [k, v] of JSON.parse(snap)) sessionStorage.setItem(k, v);
|
||||
sessionStorage.removeItem(SNAP);
|
||||
const sorted = (list) => JSON.stringify([...list].sort(([a], [b]) => a.localeCompare(b)));
|
||||
out.same =
|
||||
sorted(Object.entries(sessionStorage).filter(([k]) => k.startsWith("aislo:draft:"))) ===
|
||||
sorted(JSON.parse(snap));
|
||||
}
|
||||
}
|
||||
|
||||
if (window.__raf531) {
|
||||
window.requestAnimationFrame = window.__raf531;
|
||||
delete window.__raf531;
|
||||
}
|
||||
out.count = rows.length;
|
||||
out.bad = bad;
|
||||
out.known = known;
|
||||
out.rows = out.rows ?? rows;
|
||||
return out;
|
||||
};
|
||||
run()
|
||||
.then((out) => (window.__r531 = out))
|
||||
.catch((e) => (window.__r531 = { error: String(e?.stack ?? e) }));
|
||||
return "started";
|
||||
})();
|
||||
Reference in New Issue
Block a user