refactor(b06): 관 목록을 담는 자리를 한 벌로 합침
B05 는 관 목록 전체를 세션 스냅샷으로, B06 은 바뀐 것만 따로 담고 있어 담는 꼴이 둘이었음. 저장 순서를 맞춰 결과는 같게 했으나 자리가 둘인 한 같은 사고가 다시 남. B06 의 추가·삭제·이동을 모두 B05 가 쓰는 스냅샷(pipes)에 쓰도록 바꾸고, B06 전용 예약 경로(culvertedit 세션·queueAdd·queueRemove·queueMove)를 걷어 냄. 옛 세션에 남아 있던 이동 예약만 저장 때 한 번 흘려보내게 읽기를 남김. 이제 관을 넣고 빼고 옮기는 길이 좌측 폼·목록·종단 우클릭·측점선 끌기 모두 한 함수이고, 담기는 자리도 한 곳임. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fe1QWPTfw11PaKh2LjwXdR
This commit is contained in:
@@ -107,8 +107,6 @@ export const STATE_REGISTRY = {
|
|||||||
culvertopt: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:culvertopt:${p}:${r}` },
|
culvertopt: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:culvertopt:${p}:${r}` },
|
||||||
/** 배수관 이동(측점 옮김) 예약. */
|
/** 배수관 이동(측점 옮김) 예약. */
|
||||||
culvertmove: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:culvertmove:${p}:${r}` },
|
culvertmove: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:culvertmove:${p}:${r}` },
|
||||||
/** 배수관 추가·삭제 예약(2026-09-12) — B06 종단·목록에서 넣고 뺀 관. */
|
|
||||||
culvertedit: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:culvertedit:${p}:${r}` },
|
|
||||||
/** 암 경계선 오프셋(측점별). */
|
/** 암 경계선 오프셋(측점별). */
|
||||||
rockb: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:rockb:${p}:${r}` },
|
rockb: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:rockb:${p}:${r}` },
|
||||||
/** 측점별 암 절토 경사비(1:n 의 n) — 카드에서 넣은 사용자 값(2026-09-07).
|
/** 측점별 암 절토 경사비(1:n 의 n) — 카드에서 넣은 사용자 값(2026-09-07).
|
||||||
|
|||||||
@@ -171,16 +171,15 @@ export async function tempSaveAction(ctx: PageActionContext): Promise<void> {
|
|||||||
);
|
);
|
||||||
// B06 조정창에서 만진 배수관 구간값도 세션에만 있다 — 함께 내보낸다
|
// B06 조정창에서 만진 배수관 구간값도 세션에만 있다 — 함께 내보낸다
|
||||||
// (CLAUDE.md 5장: 영구저장은 [저장]·[확정]에서만). 실패해도 저장은 진행한다.
|
// (CLAUDE.md 5장: 영구저장은 [저장]·[확정]에서만). 실패해도 저장은 진행한다.
|
||||||
// ⚠ 키 **셋을 다 넘긴다**(2026-09-12 사용자: 어느 페이지에서 저장해도 결과가 같아야
|
// ⚠ 이동 키까지 넘긴다(2026-09-12 사용자: 어느 페이지에서 저장해도 결과가 같아야
|
||||||
// 한다). 종전에는 구간값 키만 넘겨, B06 에서 예약한 관 이동·추가·삭제가 B05 [임시저장]
|
// 한다). 종전에는 구간값 키만 넘겨, 옛 세션에 남은 관 이동 예약이 B05 [임시저장]에서는
|
||||||
// 에서는 조용히 빠졌다.
|
// 조용히 빠졌다. 관 목록 자체는 이제 두 페이지가 같은 스냅샷(`pipes`)을 쓴다.
|
||||||
if (latest?.route?.id != null) {
|
if (latest?.route?.id != null) {
|
||||||
const routeId = latest.route.id;
|
const routeId = latest.route.id;
|
||||||
await flushCulvertOptions(
|
await flushCulvertOptions(
|
||||||
projectId,
|
projectId,
|
||||||
stateKey("culvertopt", projectId, routeId),
|
stateKey("culvertopt", projectId, routeId),
|
||||||
stateKey("culvertmove", projectId, routeId),
|
stateKey("culvertmove", projectId, routeId),
|
||||||
stateKey("culvertedit", projectId, routeId),
|
|
||||||
).catch(() => undefined);
|
).catch(() => undefined);
|
||||||
}
|
}
|
||||||
// B06에서 만져 **캐시에 얹힌** 횡단 수정분을 함께 남긴다 — 안 보내면 바로 아래
|
// B06에서 만져 **캐시에 얹힌** 횡단 수정분을 함께 남긴다 — 안 보내면 바로 아래
|
||||||
|
|||||||
@@ -28,14 +28,6 @@ type PendingMap = Record<string, CulvertOptionPatch>;
|
|||||||
export interface CulvertOptionWriter {
|
export interface CulvertOptionWriter {
|
||||||
/** 측점 옵션을 세션에 예약한다. 같은 측점의 앞선 예약과는 합쳐진다. */
|
/** 측점 옵션을 세션에 예약한다. 같은 측점의 앞선 예약과는 합쳐진다. */
|
||||||
queue: (chainageM: number, patch: CulvertOptionPatch) => void;
|
queue: (chainageM: number, patch: CulvertOptionPatch) => void;
|
||||||
/** 기준 측점 이동을 예약한다(2026-08-29 사용자: B06에서도 옮길 수 있어야 한다).
|
|
||||||
* 세부 배수유역 재분할은 저장 시 서버가 관 목록을 다시 받아 처리한다. */
|
|
||||||
queueMove: (fromChainageM: number, toChainageM: number) => void;
|
|
||||||
/** 관(계곡 통과 시설) 추가를 예약한다 — B05·B06 은 한 페이지라 넣는 자리도 같아야 한다
|
|
||||||
* (2026-09-12 사용자). 이동과 같은 규칙으로 세션에만 쌓고 [저장]·[확정]에서 나간다. */
|
|
||||||
queueAdd: (chainageM: number, facility?: string) => void;
|
|
||||||
/** 관 삭제를 예약한다. 추가해 둔 것을 다시 지우면 예약만 걷어 낸다. */
|
|
||||||
queueRemove: (chainageM: number) => void;
|
|
||||||
/** 예약분을 즉시 내보낸다([저장]·[확정]에서만 부른다). */
|
/** 예약분을 즉시 내보낸다([저장]·[확정]에서만 부른다). */
|
||||||
flush: () => Promise<void>;
|
flush: () => Promise<void>;
|
||||||
}
|
}
|
||||||
@@ -67,7 +59,9 @@ function writePending(sessionKey: string | null, pending: PendingMap): void {
|
|||||||
* 하지 않는다. 성공하면 세션을 비우고, 실패하면 **그대로 두어** 다음 [저장]에서
|
* 하지 않는다. 성공하면 세션을 비우고, 실패하면 **그대로 두어** 다음 [저장]에서
|
||||||
* 다시 시도한다 — 조용히 잃으면 화면과 저장분이 갈린다.
|
* 다시 시도한다 — 조용히 잃으면 화면과 저장분이 갈린다.
|
||||||
*/
|
*/
|
||||||
/** 예약된 이동 — 옛 측점 키 → 새 누가거리(m). */
|
/** 예약된 이동 — 옛 측점 키 → 새 누가거리(m).
|
||||||
|
* ⚠ **이제 쓰는 곳은 없다**(2026-09-12 일원화: 관 목록은 B05·B06 이 같은 스냅샷을 쓴다).
|
||||||
|
* 옛 세션에 남아 있던 예약을 저장 때 한 번 흘려보내려고 읽기만 남겨 둔다. */
|
||||||
type PendingMoves = Record<string, number>;
|
type PendingMoves = Record<string, number>;
|
||||||
|
|
||||||
function readMoves(sessionKey: string | null): PendingMoves {
|
function readMoves(sessionKey: string | null): PendingMoves {
|
||||||
@@ -80,59 +74,16 @@ function readMoves(sessionKey: string | null): PendingMoves {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 예약된 추가·삭제 — 이동과 같은 세션 칸에 함께 담는다(키 하나로 읽고 쓴다). */
|
|
||||||
interface PendingEdits {
|
|
||||||
/** 넣을 관 — 누가거리(m)와 시설 종류(기본 배관). */
|
|
||||||
adds: Array<{ chainage_m: number; facility?: string }>;
|
|
||||||
/** 지울 관 — 측점 키(누가거리 2자리). */
|
|
||||||
removes: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const EMPTY_EDITS: PendingEdits = { adds: [], removes: [] };
|
|
||||||
|
|
||||||
function readEdits(sessionKey: string | null): PendingEdits {
|
|
||||||
if (!sessionKey) return { ...EMPTY_EDITS, adds: [], removes: [] };
|
|
||||||
try {
|
|
||||||
const raw = window.sessionStorage.getItem(sessionKey);
|
|
||||||
const parsed = raw ? (JSON.parse(raw) as Partial<PendingEdits>) : null;
|
|
||||||
return {
|
|
||||||
adds: Array.isArray(parsed?.adds) ? parsed.adds : [],
|
|
||||||
removes: Array.isArray(parsed?.removes) ? parsed.removes : [],
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
return { adds: [], removes: [] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeEdits(sessionKey: string | null, edits: PendingEdits): void {
|
|
||||||
if (!sessionKey) return;
|
|
||||||
try {
|
|
||||||
if (!edits.adds.length && !edits.removes.length) window.sessionStorage.removeItem(sessionKey);
|
|
||||||
else window.sessionStorage.setItem(sessionKey, JSON.stringify(edits));
|
|
||||||
} catch {
|
|
||||||
/* 세션 저장 실패는 무시 — 값은 화면 캐시에 남아 있다. */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function flushCulvertOptions(
|
export async function flushCulvertOptions(
|
||||||
projectId: string | null,
|
projectId: string | null,
|
||||||
sessionKey: string | null,
|
sessionKey: string | null,
|
||||||
moveKey: string | null = null,
|
moveKey: string | null = null,
|
||||||
editKey: string | null = null,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const pending = readPending(sessionKey);
|
const pending = readPending(sessionKey);
|
||||||
const moves = readMoves(moveKey);
|
const moves = readMoves(moveKey);
|
||||||
const edits = readEdits(editKey);
|
if (!projectId || (Object.keys(pending).length === 0 && Object.keys(moves).length === 0)) return;
|
||||||
if (
|
|
||||||
!projectId ||
|
|
||||||
(Object.keys(pending).length === 0 &&
|
|
||||||
Object.keys(moves).length === 0 &&
|
|
||||||
!edits.adds.length &&
|
|
||||||
!edits.removes.length)
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
const current = await fetchDetailPipePoints(projectId);
|
const current = await fetchDetailPipePoints(projectId);
|
||||||
let touched = edits.adds.length > 0 || edits.removes.length > 0;
|
let touched = false;
|
||||||
const points: DetailPipeInput[] = current.pipe_points.map((point) => {
|
const points: DetailPipeInput[] = current.pipe_points.map((point) => {
|
||||||
// 좌표는 서버가 다시 계산하므로 싣지 않는다(DetailPipeInput 규약).
|
// 좌표는 서버가 다시 계산하므로 싣지 않는다(DetailPipeInput 규약).
|
||||||
const { lonlat: _lonlat, ...input } = point;
|
const { lonlat: _lonlat, ...input } = point;
|
||||||
@@ -148,23 +99,9 @@ export async function flushCulvertOptions(
|
|||||||
if (movedTo !== undefined) next.chainage_m = movedTo;
|
if (movedTo !== undefined) next.chainage_m = movedTo;
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
// 지울 것을 걷어 내고 넣을 것을 붙인다 — 서버는 이 목록을 그대로 정본으로 삼는다.
|
if (touched) await saveDetailPipePoints(projectId, points);
|
||||||
const removed = new Set(edits.removes);
|
|
||||||
const next = points.filter((point) => !removed.has(keyOf(point.chainage_m)));
|
|
||||||
for (const add of edits.adds) {
|
|
||||||
if (next.some((point) => Math.abs(point.chainage_m - add.chainage_m) < 0.005)) continue;
|
|
||||||
next.push({
|
|
||||||
chainage_m: add.chainage_m,
|
|
||||||
// 사람이 넣은 자리 — 세류 교차·간격 규칙이 만든 것과 구분한다.
|
|
||||||
source: "user",
|
|
||||||
...(add.facility ? { facility: add.facility as DetailPipeInput["facility"] } : {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
next.sort((left, right) => left.chainage_m - right.chainage_m);
|
|
||||||
if (touched) await saveDetailPipePoints(projectId, next);
|
|
||||||
writePending(sessionKey, {});
|
writePending(sessionKey, {});
|
||||||
writePending(moveKey, {});
|
writePending(moveKey, {});
|
||||||
writeEdits(editKey, { adds: [], removes: [] });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -177,8 +114,6 @@ export function createCulvertOptionWriter(
|
|||||||
onError?: (message: string) => void,
|
onError?: (message: string) => void,
|
||||||
/** 이동 예약 세션 키 — 없으면 이동은 메모리에도 남지 않는다. */
|
/** 이동 예약 세션 키 — 없으면 이동은 메모리에도 남지 않는다. */
|
||||||
moveKey: () => string | null = () => null,
|
moveKey: () => string | null = () => null,
|
||||||
/** 추가·삭제 예약 세션 키 — 없으면 추가·삭제를 받지 않는다. */
|
|
||||||
editKey: () => string | null = () => null,
|
|
||||||
): CulvertOptionWriter {
|
): CulvertOptionWriter {
|
||||||
return {
|
return {
|
||||||
queue(chainageM, patch) {
|
queue(chainageM, patch) {
|
||||||
@@ -187,43 +122,9 @@ export function createCulvertOptionWriter(
|
|||||||
pending[keyOf(chainageM)] = { ...(pending[keyOf(chainageM)] ?? {}), ...patch };
|
pending[keyOf(chainageM)] = { ...(pending[keyOf(chainageM)] ?? {}), ...patch };
|
||||||
writePending(key, pending);
|
writePending(key, pending);
|
||||||
},
|
},
|
||||||
queueMove(fromChainageM, toChainageM) {
|
|
||||||
const key = moveKey();
|
|
||||||
if (!key) return;
|
|
||||||
const moves = readMoves(key);
|
|
||||||
// 이미 옮긴 관을 또 옮기면 **원래 자리** 기준으로 최종 위치만 남긴다.
|
|
||||||
const origin =
|
|
||||||
Object.keys(moves).find((entry) => Math.abs(moves[entry] - fromChainageM) < 0.005) ??
|
|
||||||
keyOf(fromChainageM);
|
|
||||||
moves[origin] = toChainageM;
|
|
||||||
window.sessionStorage.setItem(key, JSON.stringify(moves));
|
|
||||||
},
|
|
||||||
queueAdd(chainageM, facility) {
|
|
||||||
const key = editKey();
|
|
||||||
if (!key) return;
|
|
||||||
const edits = readEdits(key);
|
|
||||||
const at = Number(chainageM.toFixed(2));
|
|
||||||
// 같은 자리를 지웠다가 다시 넣으면 삭제 예약만 걷어 낸다(정본은 그대로).
|
|
||||||
const removedAt = edits.removes.indexOf(keyOf(at));
|
|
||||||
if (removedAt >= 0) edits.removes.splice(removedAt, 1);
|
|
||||||
else if (!edits.adds.some((entry) => Math.abs(entry.chainage_m - at) < 0.005))
|
|
||||||
edits.adds.push({ chainage_m: at, ...(facility ? { facility } : {}) });
|
|
||||||
writeEdits(key, edits);
|
|
||||||
},
|
|
||||||
queueRemove(chainageM) {
|
|
||||||
const key = editKey();
|
|
||||||
if (!key) return;
|
|
||||||
const edits = readEdits(key);
|
|
||||||
const at = Number(chainageM.toFixed(2));
|
|
||||||
// 아직 정본에 없는(이번에 넣은) 관이면 추가 예약만 거둔다.
|
|
||||||
const addedAt = edits.adds.findIndex((entry) => Math.abs(entry.chainage_m - at) < 0.005);
|
|
||||||
if (addedAt >= 0) edits.adds.splice(addedAt, 1);
|
|
||||||
else if (!edits.removes.includes(keyOf(at))) edits.removes.push(keyOf(at));
|
|
||||||
writeEdits(key, edits);
|
|
||||||
},
|
|
||||||
async flush() {
|
async flush() {
|
||||||
try {
|
try {
|
||||||
await flushCulvertOptions(projectId(), sessionKey(), moveKey(), editKey());
|
await flushCulvertOptions(projectId(), sessionKey(), moveKey());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onError?.(error instanceof Error ? error.message : "배수관 구간값 저장 실패");
|
onError?.(error instanceof Error ? error.message : "배수관 구간값 저장 실패");
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -201,10 +201,6 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
|||||||
queuePipeOptions: (chainageM, patch) => stationControls.queueCulvertOptions(chainageM, patch),
|
queuePipeOptions: (chainageM, patch) => stationControls.queueCulvertOptions(chainageM, patch),
|
||||||
applyPipeOptions: (chainageM, patch) =>
|
applyPipeOptions: (chainageM, patch) =>
|
||||||
applyPipeOptionsToCache(pipeOptionsContext, chainageM, patch),
|
applyPipeOptionsToCache(pipeOptionsContext, chainageM, patch),
|
||||||
movePipe: (fromChainageM, toChainageM) =>
|
|
||||||
stationControls.queueCulvertMove(fromChainageM, toChainageM),
|
|
||||||
addPipe: (chainageM, facility) => stationControls.queueCulvertAdd(chainageM, facility),
|
|
||||||
removePipe: (chainageM) => stationControls.queueCulvertRemove(chainageM),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const dockDivider = document.createElement("hr");
|
const dockDivider = document.createElement("hr");
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ export interface StationControlDeps {
|
|||||||
| "extraspan"
|
| "extraspan"
|
||||||
| "culvertopt"
|
| "culvertopt"
|
||||||
| "culvertmove"
|
| "culvertmove"
|
||||||
| "culvertedit"
|
|
||||||
| "revetlink"
|
| "revetlink"
|
||||||
| "fordadjust"
|
| "fordadjust"
|
||||||
| "boxadjust",
|
| "boxadjust",
|
||||||
@@ -95,12 +94,6 @@ export interface StationControls {
|
|||||||
flushCulvertOptions: () => Promise<void>;
|
flushCulvertOptions: () => Promise<void>;
|
||||||
/** 관 옵션 조각을 예약한다 — 좌측 폼의 [수정]도 이 경로로 정본에 간다(2026-08-29). */
|
/** 관 옵션 조각을 예약한다 — 좌측 폼의 [수정]도 이 경로로 정본에 간다(2026-08-29). */
|
||||||
queueCulvertOptions: (chainageM: number, patch: Record<string, number | string>) => void;
|
queueCulvertOptions: (chainageM: number, patch: Record<string, number | string>) => void;
|
||||||
/** 기준 측점 이동을 예약한다 — B06 구조물 배치 폼의 측점 칸이 쓴다(2026-08-29). */
|
|
||||||
queueCulvertMove: (fromChainageM: number, toChainageM: number) => void;
|
|
||||||
/** 관 추가를 예약한다 — B06 종단 우클릭·목록이 쓴다(2026-09-12 B05·B06 일원화). */
|
|
||||||
queueCulvertAdd: (chainageM: number, facility?: string) => void;
|
|
||||||
/** 관 삭제를 예약한다. */
|
|
||||||
queueCulvertRemove: (chainageM: number) => void;
|
|
||||||
load: () => void;
|
load: () => void;
|
||||||
/** 전체 반영 — 개별 반폭을 전역값으로 덮는다(없으면 비운다). */
|
/** 전체 반영 — 개별 반폭을 전역값으로 덮는다(없으면 비운다). */
|
||||||
applyGlobalWidth: (requested: number | undefined, chainages: number[]) => void;
|
applyGlobalWidth: (requested: number | undefined, chainages: number[]) => void;
|
||||||
@@ -437,7 +430,6 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
|||||||
() => deps.sessionKey("culvertopt"),
|
() => deps.sessionKey("culvertopt"),
|
||||||
deps.onSaveError,
|
deps.onSaveError,
|
||||||
() => deps.sessionKey("culvertmove"),
|
() => deps.sessionKey("culvertmove"),
|
||||||
() => deps.sessionKey("culvertedit"),
|
|
||||||
);
|
);
|
||||||
const linkSession = createLinkFlagSession(() => deps.sessionKey("revetlink"));
|
const linkSession = createLinkFlagSession(() => deps.sessionKey("revetlink"));
|
||||||
const linkDetached = linkSession.detached;
|
const linkDetached = linkSession.detached;
|
||||||
@@ -623,10 +615,6 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
|||||||
},
|
},
|
||||||
flushCulvertOptions: () => culvertOptions.flush(),
|
flushCulvertOptions: () => culvertOptions.flush(),
|
||||||
queueCulvertOptions: (chainageM, patch) => culvertOptions.queue(chainageM, patch),
|
queueCulvertOptions: (chainageM, patch) => culvertOptions.queue(chainageM, patch),
|
||||||
queueCulvertMove: (fromChainageM, toChainageM) =>
|
|
||||||
culvertOptions.queueMove(fromChainageM, toChainageM),
|
|
||||||
queueCulvertAdd: (chainageM, facility) => culvertOptions.queueAdd(chainageM, facility),
|
|
||||||
queueCulvertRemove: (chainageM) => culvertOptions.queueRemove(chainageM),
|
|
||||||
load: () => {
|
load: () => {
|
||||||
loadStationWidths();
|
loadStationWidths();
|
||||||
loadRevetShifts();
|
loadRevetShifts();
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
type StructureInstance,
|
type StructureInstance,
|
||||||
type StructureType,
|
type StructureType,
|
||||||
} from "../B05_Profile/B05_Profile_Api_Structures";
|
} from "../B05_Profile/B05_Profile_Api_Structures";
|
||||||
import { readPendingPipes } from "../B05_Profile/B05_Profile_Api_Pipes_Draft";
|
import { readPendingPipes, writePendingPipes } from "../B05_Profile/B05_Profile_Api_Pipes_Draft";
|
||||||
import { fetchDetailPipePoints } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
import { fetchDetailPipePoints } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||||
import {
|
import {
|
||||||
readStructurePick,
|
readStructurePick,
|
||||||
@@ -78,13 +78,6 @@ export interface B06StructuresPanelDeps {
|
|||||||
/** 바뀐 값을 그 측점 횡단 캐시(culvert 스펙)에 얹고 카드를 다시 그린다 —
|
/** 바뀐 값을 그 측점 횡단 캐시(culvert 스펙)에 얹고 카드를 다시 그린다 —
|
||||||
* 조정창과 같은 흐름이라 폼 조작도 도면에 바로 보인다(2026-08-29 사용자 보고). */
|
* 조정창과 같은 흐름이라 폼 조작도 도면에 바로 보인다(2026-08-29 사용자 보고). */
|
||||||
applyPipeOptions?: (chainageM: number, patch: Record<string, number | string>) => void;
|
applyPipeOptions?: (chainageM: number, patch: Record<string, number | string>) => void;
|
||||||
/** 기준 측점 이동을 예약한다 — 세부 배수유역 재분할은 [저장]·[확정] 때 서버가
|
|
||||||
* 관 목록을 다시 받아 처리한다(2026-08-29 사용자: B06에서도 옮길 수 있어야 한다). */
|
|
||||||
movePipe?: (fromChainageM: number, toChainageM: number) => void;
|
|
||||||
/** 관(계곡 통과 시설)을 넣는다 — 이동과 같은 예약 경로(2026-09-12 B05·B06 일원화). */
|
|
||||||
addPipe?: (chainageM: number, facility?: string) => void;
|
|
||||||
/** 관을 뺀다 — 같은 예약 경로. */
|
|
||||||
removePipe?: (chainageM: number) => void;
|
|
||||||
/** 구조물 목록이 바뀌었다 — 벽(C군)은 횡단 제원으로 얹혀 **면적까지 달라지므로**
|
/** 구조물 목록이 바뀌었다 — 벽(C군)은 횡단 제원으로 얹혀 **면적까지 달라지므로**
|
||||||
* 화면이 횡단을 다시 받아 그려야 한다(2026-09-06 사용자 확정). */
|
* 화면이 횡단을 다시 받아 그려야 한다(2026-09-06 사용자 확정). */
|
||||||
onStructuresChanged?: () => void;
|
onStructuresChanged?: () => void;
|
||||||
@@ -267,7 +260,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
|||||||
onReveal: () => deps.reveal?.(),
|
onReveal: () => deps.reveal?.(),
|
||||||
// 관 추가 — 정본은 [저장]·[확정]에서 나간다. 화면 목록·알약은 바로 세운다.
|
// 관 추가 — 정본은 [저장]·[확정]에서 나간다. 화면 목록·알약은 바로 세운다.
|
||||||
onPipeAdd: (chainageM, attributes) => {
|
onPipeAdd: (chainageM, attributes) => {
|
||||||
if (!deps.addPipe) {
|
if (!deps.projectId) {
|
||||||
showToast(PIPE_ADD_GUIDE, "error");
|
showToast(PIPE_ADD_GUIDE, "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -276,38 +269,28 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
|||||||
showToast("그 자리에는 계곡 통과 시설이 이미 있습니다.", "error");
|
showToast("그 자리에는 계곡 통과 시설이 이미 있습니다.", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const facility = attributes.facility ?? "pipe";
|
pipeFacilities.push({
|
||||||
deps.addPipe(at, facility);
|
chainage_m: at,
|
||||||
pipeFacilities.push({ chainage_m: at, facility, options: attributes.options ?? {} });
|
facility: attributes.facility ?? "pipe",
|
||||||
|
source: "user",
|
||||||
|
options: attributes.options ?? {},
|
||||||
|
});
|
||||||
pipeFacilities.sort((left, right) => left.chainage_m - right.chainage_m);
|
pipeFacilities.sort((left, right) => left.chainage_m - right.chainage_m);
|
||||||
currentChainageM = at;
|
currentChainageM = at;
|
||||||
|
persistPipes();
|
||||||
section.setPipeFacilities(pipeFacilities);
|
section.setPipeFacilities(pipeFacilities);
|
||||||
pushMarks();
|
pushMarks();
|
||||||
deps.onStructuresChanged?.();
|
deps.onStructuresChanged?.();
|
||||||
showToast(PIPE_ADD_NOTICE, "success");
|
showToast(PIPE_ADD_NOTICE, "success");
|
||||||
},
|
},
|
||||||
// 값 수정은 B06에서도 받는다 — 조정창 구간값과 같은 저장 경로(캐시 예약 →
|
// 값 수정·기준점 이동 모두 B06 에서 받는다 — 이동은 종단 끌기와 **같은 함수**를 탄다
|
||||||
// [저장]·[확정])로 보낸다. 기준점 이동만 배수유역 재분할이 걸려 B05 몫이다.
|
// (2026-09-12 일원화). 재분할은 [저장]·[확정] 뒤 서버 몫이다.
|
||||||
onPipeUpdate: (fromChainageM, toChainageM, attributes) => {
|
onPipeUpdate: (fromChainageM, toChainageM, attributes) => {
|
||||||
// 위치가 "옮겨졌다"고 볼 기준은 관 매칭과 같은 0.51m다 — 폼의 측점 표기는
|
// 위치가 "옮겨졌다"고 볼 기준은 관 매칭과 같은 0.51m다 — 폼의 측점 표기는
|
||||||
// 0.1m로 반올림되므로 0.005m 기준으로 보면 값만 고쳐도 이동으로 잡힌다
|
// 0.1m로 반올림되므로 0.005m 기준으로 보면 값만 고쳐도 이동으로 잡힌다
|
||||||
// (2026-08-29 사용자 보고: 높이만 바꿨는데 이동 안내가 떴다).
|
// (2026-08-29 사용자 보고: 높이만 바꿨는데 이동 안내가 떴다).
|
||||||
const moved = Math.abs(fromChainageM - toChainageM) > PIPE_MATCH_M;
|
if (Math.abs(fromChainageM - toChainageM) > PIPE_MATCH_M)
|
||||||
if (moved) {
|
movePipeTo(fromChainageM, toChainageM);
|
||||||
if (deps.movePipe) {
|
|
||||||
deps.movePipe(fromChainageM, toChainageM);
|
|
||||||
const hit = pipeFacilities.find(
|
|
||||||
(entry) => Math.abs(entry.chainage_m - fromChainageM) < PIPE_MATCH_M,
|
|
||||||
);
|
|
||||||
if (hit) hit.chainage_m = toChainageM;
|
|
||||||
currentChainageM = toChainageM;
|
|
||||||
section.setPipeFacilities(pipeFacilities);
|
|
||||||
pushMarks();
|
|
||||||
showToast(PIPE_MOVE_NOTICE, "success");
|
|
||||||
} else {
|
|
||||||
showToast(PIPE_MOVE_GUIDE, "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const patch = attributes.options;
|
const patch = attributes.options;
|
||||||
if (!patch || !Object.keys(patch).length) return;
|
if (!patch || !Object.keys(patch).length) return;
|
||||||
if (!deps.queuePipeOptions) {
|
if (!deps.queuePipeOptions) {
|
||||||
@@ -324,6 +307,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
|||||||
(entry) => Math.abs(entry.chainage_m - fromChainageM) < PIPE_MATCH_M,
|
(entry) => Math.abs(entry.chainage_m - fromChainageM) < PIPE_MATCH_M,
|
||||||
);
|
);
|
||||||
if (hit) hit.options = { ...(hit.options ?? {}), ...patch };
|
if (hit) hit.options = { ...(hit.options ?? {}), ...patch };
|
||||||
|
persistPipes();
|
||||||
section.setPipeFacilities(pipeFacilities);
|
section.setPipeFacilities(pipeFacilities);
|
||||||
},
|
},
|
||||||
onPipeRemove: (chainageM) => removePipeAt(chainageM),
|
onPipeRemove: (chainageM) => removePipeAt(chainageM),
|
||||||
@@ -440,14 +424,33 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
|||||||
section.setPipeFacilities(pipeFacilities);
|
section.setPipeFacilities(pipeFacilities);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 관 목록을 **미저장 스냅샷**으로 남긴다 — B05 가 쓰는 바로 그 세션이다(2026-09-12
|
||||||
|
* 사용자: 담는 꼴도 한 벌이어야 한다). 종전에는 B06 만 「바뀐 것만」 따로 담아
|
||||||
|
* 두 페이지가 서로 다른 자리를 보고 저장 순서에 따라 결과가 갈렸다.
|
||||||
|
*/
|
||||||
|
function persistPipes(): void {
|
||||||
|
if (!deps.projectId) return;
|
||||||
|
writePendingPipes(
|
||||||
|
deps.projectId,
|
||||||
|
pipeFacilities.map((pipe) => ({
|
||||||
|
chainage_m: pipe.chainage_m,
|
||||||
|
source: pipe.source ?? "user",
|
||||||
|
facility: pipe.facility,
|
||||||
|
...(pipe.start_m !== undefined ? { start_m: pipe.start_m } : {}),
|
||||||
|
...(pipe.end_m !== undefined ? { end_m: pipe.end_m } : {}),
|
||||||
|
...(pipe.options ? { options: pipe.options } : {}),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** 관 옮기기 — 폼의 측점 칸과 종단 끌기가 같은 길을 탄다. 재분할은 [저장]·[확정] 뒤. */
|
/** 관 옮기기 — 폼의 측점 칸과 종단 끌기가 같은 길을 탄다. 재분할은 [저장]·[확정] 뒤. */
|
||||||
function movePipeTo(fromChainageM: number, toChainageM: number): void {
|
function movePipeTo(fromChainageM: number, toChainageM: number): void {
|
||||||
if (!deps.movePipe) {
|
if (!deps.projectId) {
|
||||||
showToast(PIPE_MOVE_GUIDE, "error");
|
showToast(PIPE_MOVE_GUIDE, "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const to = Number(toChainageM.toFixed(2));
|
const to = Number(toChainageM.toFixed(2));
|
||||||
deps.movePipe(fromChainageM, to);
|
|
||||||
const hit = pipeFacilities.find(
|
const hit = pipeFacilities.find(
|
||||||
(entry) => Math.abs(entry.chainage_m - fromChainageM) < PIPE_MATCH_M,
|
(entry) => Math.abs(entry.chainage_m - fromChainageM) < PIPE_MATCH_M,
|
||||||
);
|
);
|
||||||
@@ -455,6 +458,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
|||||||
if (currentChainageM !== null && Math.abs(currentChainageM - fromChainageM) < PIPE_MATCH_M)
|
if (currentChainageM !== null && Math.abs(currentChainageM - fromChainageM) < PIPE_MATCH_M)
|
||||||
currentChainageM = to;
|
currentChainageM = to;
|
||||||
pipeFacilities.sort((left, right) => left.chainage_m - right.chainage_m);
|
pipeFacilities.sort((left, right) => left.chainage_m - right.chainage_m);
|
||||||
|
persistPipes();
|
||||||
section.setPipeFacilities(pipeFacilities);
|
section.setPipeFacilities(pipeFacilities);
|
||||||
pushMarks();
|
pushMarks();
|
||||||
showToast(PIPE_MOVE_NOTICE, "success");
|
showToast(PIPE_MOVE_NOTICE, "success");
|
||||||
@@ -462,17 +466,17 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
|||||||
|
|
||||||
/** 관 빼기 — 목록·폼·종단 우클릭이 같은 길을 탄다. 정본은 [저장]·[확정]에서 나간다. */
|
/** 관 빼기 — 목록·폼·종단 우클릭이 같은 길을 탄다. 정본은 [저장]·[확정]에서 나간다. */
|
||||||
function removePipeAt(chainageM: number): void {
|
function removePipeAt(chainageM: number): void {
|
||||||
if (!deps.removePipe) {
|
if (!deps.projectId) {
|
||||||
showToast(PIPE_ADD_GUIDE, "error");
|
showToast(PIPE_ADD_GUIDE, "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const at = Number(chainageM.toFixed(2));
|
const at = Number(chainageM.toFixed(2));
|
||||||
deps.removePipe(at);
|
|
||||||
pipeFacilities = pipeFacilities.filter(
|
pipeFacilities = pipeFacilities.filter(
|
||||||
(entry) => Math.abs(entry.chainage_m - at) >= PIPE_MATCH_M,
|
(entry) => Math.abs(entry.chainage_m - at) >= PIPE_MATCH_M,
|
||||||
);
|
);
|
||||||
if (currentChainageM !== null && Math.abs(currentChainageM - at) < PIPE_MATCH_M)
|
if (currentChainageM !== null && Math.abs(currentChainageM - at) < PIPE_MATCH_M)
|
||||||
currentChainageM = null;
|
currentChainageM = null;
|
||||||
|
persistPipes();
|
||||||
section.setPipeFacilities(pipeFacilities);
|
section.setPipeFacilities(pipeFacilities);
|
||||||
pushMarks();
|
pushMarks();
|
||||||
deps.onStructuresChanged?.();
|
deps.onStructuresChanged?.();
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
"""B06 종단에서 넣고 뺀 **계곡 통과 시설**이 세션에 제대로 쌓이는지(2026-09-12 사용자 지시).
|
"""관 편집이 **한 자리에만** 쌓이는지(2026-09-12 사용자: 담는 꼴도 한 벌이어야 한다).
|
||||||
|
|
||||||
B05·B06 은 한 페이지이므로 관 추가·삭제도 양쪽에서 되어야 한다. 다만 정본에 바로 쓰지
|
B05 는 관 목록 전체를 세션 스냅샷(`pipes`)으로 담고, B06 은 바뀐 것만 따로 담고 있었다.
|
||||||
않고 **세션에 예약**했다가 [저장]·[확정]에서 한 번에 내보낸다(CLAUDE.md 5장). 넣었다 바로
|
저장 순서에 따라 한쪽이 다른 쪽을 덮어 **넣은 관이 사라지는** 일이 생겼다. 그래서 관
|
||||||
빼면 예약만 걷어 내야 하고, 뺐다 다시 넣어도 마찬가지다 — 그러지 않으면 저장 때 있지도
|
목록은 두 페이지가 같은 스냅샷을 쓰도록 합쳤다.
|
||||||
않은 관을 지우거나 같은 자리에 둘이 선다.
|
|
||||||
|
여기서 지키는 것은 둘이다.
|
||||||
|
① 저장기(`createCulvertOptionWriter`)에는 **관을 넣고 빼는 길이 없다** — 남아 있으면
|
||||||
|
관 목록을 담는 자리가 다시 둘이 된다.
|
||||||
|
② 옛 세션에 남아 있던 **이동 예약은 저장 때 흘려보낸다** — 합치기 전에 만들어 둔
|
||||||
|
예약이 조용히 사라지면 안 된다.
|
||||||
|
|
||||||
실제 화면 코드를 그대로 컴파일해 Node 로 돌린다(다단 하한 시험과 같은 방식).
|
실제 화면 코드를 그대로 컴파일해 Node 로 돌린다(다단 하한 시험과 같은 방식).
|
||||||
"""
|
"""
|
||||||
@@ -25,15 +30,28 @@ const { writeFileSync } = require("node:fs");
|
|||||||
const Module = require("node:module");
|
const Module = require("node:module");
|
||||||
|
|
||||||
// 화면 코드가 번들러 별칭(`@config/…`)을 쓴다 — Node 에는 없으니 빈 껍데기로 돌려준다.
|
// 화면 코드가 번들러 별칭(`@config/…`)을 쓴다 — Node 에는 없으니 빈 껍데기로 돌려준다.
|
||||||
// 이 시험이 보는 것은 세션 예약 로직뿐이라 값은 안 쓴다.
|
|
||||||
const load = Module._load;
|
const load = Module._load;
|
||||||
|
const saved = [];
|
||||||
Module._load = function (request, parent, isMain) {
|
Module._load = function (request, parent, isMain) {
|
||||||
if (request.startsWith("@config/") || request.startsWith("@util/") || request.startsWith("@ui/"))
|
if (request.startsWith("@config/") || request.startsWith("@util/") || request.startsWith("@ui/"))
|
||||||
return new Proxy({}, { get: () => () => undefined });
|
return new Proxy({}, { get: () => () => undefined });
|
||||||
return load.call(this, request, parent, isMain);
|
const mod = load.call(this, request, parent, isMain);
|
||||||
|
// 관 정본 읽기·쓰기는 서버로 나가므로 가로챈다.
|
||||||
|
if (request.includes("B04_PreProcess_Api_Fetch")) {
|
||||||
|
return {
|
||||||
|
...mod,
|
||||||
|
fetchDetailPipePoints: async () => ({
|
||||||
|
pipe_points: [
|
||||||
|
{ chainage_m: 120, lonlat: [0, 0], source: "stream" },
|
||||||
|
{ chainage_m: 200, lonlat: [0, 0], source: "stream" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
saveDetailPipePoints: async (_projectId, points) => saved.push(points),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return mod;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 세션 저장소 흉내 — 화면 코드는 window.sessionStorage 만 쓴다.
|
|
||||||
const store = new Map();
|
const store = new Map();
|
||||||
global.window = {
|
global.window = {
|
||||||
sessionStorage: {
|
sessionStorage: {
|
||||||
@@ -43,39 +61,36 @@ global.window = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const { createCulvertOptionWriter } = require(process.argv[3]);
|
const api = require(process.argv[3]);
|
||||||
const EDIT_KEY = "edit";
|
const writer = api.createCulvertOptionWriter(
|
||||||
const writer = createCulvertOptionWriter(
|
|
||||||
() => "p1",
|
() => "p1",
|
||||||
() => "opt",
|
() => "opt",
|
||||||
undefined,
|
undefined,
|
||||||
() => "move",
|
() => "move",
|
||||||
() => EDIT_KEY,
|
|
||||||
);
|
);
|
||||||
const read = () => JSON.parse(store.get(EDIT_KEY) ?? '{"adds":[],"removes":[]}');
|
|
||||||
const steps = {};
|
|
||||||
|
|
||||||
writer.queueAdd(120, "pipe");
|
// ① 관을 넣고 빼는 길이 저장기에 남아 있으면 안 된다.
|
||||||
writer.queueAdd(140);
|
const methods = Object.keys(writer).sort();
|
||||||
steps.afterTwoAdds = read();
|
|
||||||
|
|
||||||
// 방금 넣은 자리를 다시 빼면 **추가 예약만** 사라진다(정본에는 아직 없다).
|
// ② 합치기 전 세션에 남아 있던 이동 예약은 저장 때 흘려보낸다.
|
||||||
writer.queueRemove(140);
|
store.set("move", JSON.stringify({ "120.00": 135 }));
|
||||||
steps.afterUndoAdd = read();
|
|
||||||
|
|
||||||
// 정본에 있던 관을 빼면 삭제 예약으로 남는다.
|
api
|
||||||
writer.queueRemove(200);
|
.flushCulvertOptions("p1", "opt", "move")
|
||||||
steps.afterRemove = read();
|
.then(() => {
|
||||||
|
writeFileSync(
|
||||||
// 뺀 자리를 다시 넣으면 삭제 예약만 걷어 낸다(새로 만들지 않는다).
|
process.argv[2],
|
||||||
writer.queueAdd(200);
|
JSON.stringify({
|
||||||
steps.afterUndoRemove = read();
|
methods,
|
||||||
|
savedChainages: (saved[0] ?? []).map((point) => point.chainage_m),
|
||||||
// 같은 자리를 두 번 넣어도 하나만 남는다.
|
moveLeft: store.get("move") ?? null,
|
||||||
writer.queueAdd(120);
|
}),
|
||||||
steps.afterDuplicateAdd = read();
|
);
|
||||||
|
})
|
||||||
writeFileSync(process.argv[2], JSON.stringify(steps));
|
.catch((error) => {
|
||||||
|
writeFileSync(process.argv[2], JSON.stringify({ error: String(error) }));
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@@ -112,24 +127,13 @@ def _run(tmp_path: Path) -> dict:
|
|||||||
return json.loads(result.read_text(encoding="utf-8"))
|
return json.loads(result.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
def test_추가와_삭제_예약이_서로를_걷어_낸다(tmp_path: Path) -> None:
|
def test_관_목록을_담는_자리는_하나뿐이다(tmp_path: Path) -> None:
|
||||||
steps = _run(tmp_path)
|
result = _run(tmp_path)
|
||||||
|
assert "error" not in result, result.get("error")
|
||||||
|
|
||||||
# 넣은 둘이 그대로 쌓인다.
|
# ① 저장기에는 구간값 예약과 내보내기만 있다 — 관을 넣고 빼는 길은 스냅샷 한 곳뿐이다.
|
||||||
assert [entry["chainage_m"] for entry in steps["afterTwoAdds"]["adds"]] == [120.0, 140.0]
|
assert result["methods"] == ["flush", "queue"]
|
||||||
assert steps["afterTwoAdds"]["adds"][0]["facility"] == "pipe"
|
|
||||||
assert steps["afterTwoAdds"]["removes"] == []
|
|
||||||
|
|
||||||
# 방금 넣은 자리를 빼면 추가 예약만 사라지고 삭제 예약은 안 생긴다.
|
# ② 옛 세션의 이동 예약은 저장 때 반영되고 비워진다.
|
||||||
assert [entry["chainage_m"] for entry in steps["afterUndoAdd"]["adds"]] == [120.0]
|
assert result["savedChainages"] == [135, 200]
|
||||||
assert steps["afterUndoAdd"]["removes"] == []
|
assert result["moveLeft"] is None
|
||||||
|
|
||||||
# 정본에 있던 관은 삭제 예약으로 남는다.
|
|
||||||
assert steps["afterRemove"]["removes"] == ["200.00"]
|
|
||||||
|
|
||||||
# 뺀 자리를 다시 넣으면 삭제 예약만 걷힌다 — 새 관을 만들지 않는다.
|
|
||||||
assert steps["afterUndoRemove"]["removes"] == []
|
|
||||||
assert [entry["chainage_m"] for entry in steps["afterUndoRemove"]["adds"]] == [120.0]
|
|
||||||
|
|
||||||
# 같은 자리를 두 번 넣어도 하나만 남는다.
|
|
||||||
assert [entry["chainage_m"] for entry in steps["afterDuplicateAdd"]["adds"]] == [120.0]
|
|
||||||
|
|||||||
Reference in New Issue
Block a user