Merge remote-tracking branch 'origin/sub_laptop_5' into main_laptop_1

design_page.ts import 충돌 = 424 의 좌우 이동 import 지움 + 428 pipelineDetail 살림

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6BNBFXFndbqYxu6sZuknK
This commit is contained in:
2026-10-18 16:13:03 +09:00
co-authored by Claude Opus 5.5
10 changed files with 552 additions and 12 deletions
+20 -1
View File
@@ -57,6 +57,8 @@ import { useStationKeys } from "./B06_Section_Station_Keys";
import { hasStaleDesigns, staleKey } from "./B06_Section_UI_Section_Common";
import { applyStructureAreaRows, structureAreaRows } from "./B06_Section_Structure_Layouts";
import { withPipeOptions } from "./B06_Section_Section_Adjusts";
import { extendUnclosed } from "../B05_Profile/logic/B05_Profile_Sections_Ground";
import { designTerrainGridNow } from "../B05_Profile/B05_Profile_Api_TerrainGrid";
/** 계획선 편집 델타 — B05 `AlignmentEdits`와 저장분 `profile_alignment.edits`가 같은 모양이다. */
export interface CrossRefreshEdits {
@@ -367,6 +369,8 @@ function refreshLocally(input: CrossRefreshInput): number[] | null {
return Math.max(session ?? (typeof stored === "number" ? stored : 0), 0);
};
// 미교차 넓힘 격자 — 받기 전이면 null(받은 뒤 다시 셈이 넓힘 · 지문에 실음)
const grid = designTerrainGridNow(projectId)?.grid ?? null;
const updated: number[] = [];
for (const section of detail.cross_sections) {
const previous = section.design;
@@ -432,7 +436,7 @@ function refreshLocally(input: CrossRefreshInput): number[] | null {
// 국부 연산(346-2) — 입력 지문(지반 표본 · 계획고 · 횡단 입력 · 구조물 맥락 · 표준값)이 같고
// 설계가 그때 낸 그 객체면 앞 결과 그대로. 계획고 ▲ 한 번에 다시 세는 측점은 곡선 둘레 몇 곳뿐.
const samples = section.samples ?? [];
const fingerprint = `${standardKey}|${elevation}|${samples.length}|${JSON.stringify({ ...options, standard: null })}`;
const fingerprint = `${standardKey}|${elevation}|${samples.length}|${grid ? "grid" : ""}|${JSON.stringify({ ...options, standard: null })}`;
const memo = memos.get(section);
if (
memo?.samples === section.samples &&
@@ -444,6 +448,21 @@ function refreshLocally(input: CrossRefreshInput): number[] | null {
let next;
try {
next = computeCrossDesign(samples, elevation, options);
// 미교차 넓힘 — 서버 [저장] `extend_unclosed_sections` 짝 · 넓힌 표본을 측점에 남김(428 · 묶음 cross 와 같음)
const wide =
grid && next.slope_unclosed
? extendUnclosed(
samples,
[section.center_x, section.center_y],
section.frame.left_xy,
(more) => computeCrossDesign(more as typeof samples, elevation, options),
grid,
)
: null;
if (wide && (wide.info.left_m || wide.info.right_m)) {
section.samples = wide.samples as typeof samples;
next = computeCrossDesign(section.samples, elevation, options);
}
} catch {
continue; // 샘플 부족·값 손상 측점은 건너뛴다(서버 엔진과 같은 태도).
}
@@ -10,11 +10,15 @@
* 대신 **왜 값이 없는지**가 화면에 남는다.
* ⚠ 지표 샘플링 조건이 저장에 없으면 **단추를 잠그고 사유를 보인다** — 조건을 지어내면
* 그 측점만 다른 지표에서 뽑혀 옆 측점과 지반고가 어긋난다.
* [저장] 뒤 다시 봄(428) — 관 · 구조물 초안이 정본으로 나가 비면 저장이 끝난 뒤 한 번 더 묻고 줄을 갈음
* (서버가 관 PUT 끝에 측점을 다시 세움 · 옛 알림이 남지 않게).
* ========================================================================== */
import { API_BASE_URL } from "@config/config_frontend";
import { createButton, showToast } from "@ui/ui_template_elements";
import { attachHoverInfo } from "@ui/ui_template_hover_info";
import { useDesignStore } from "../A00_Common/design/design_store";
import { savingDesign } from "../A00_Common/design/design_save";
interface MissingStation {
chainage_m: number;
@@ -49,12 +53,14 @@ export async function mountMissingStationNotice(
projectId: string,
onCreated: () => void | Promise<void>,
): Promise<void> {
watchSaves(host, projectId, onCreated);
let data: MissingResponse;
try {
data = await call(projectId, "GET");
} catch {
return; // 점검이 안 되는 것으로 화면을 막지 않는다 — 이 줄은 덤이다.
}
host.querySelector(":scope > .b06-missing-stations")?.remove(); // 다시 봄 = 줄 하나
const missing = data.missing ?? [];
if (!missing.length) return;
@@ -99,3 +105,27 @@ export async function mountMissingStationNotice(
}
host.prepend(box);
}
const watched = new WeakSet<HTMLElement>();
/** 관 · 구조물 초안이 비면(저장 · 되돌림) 저장이 끝난 뒤 다시 물음 — 자리(`host`)가 떠나면 끊음. */
function watchSaves(
host: HTMLElement,
projectId: string,
onCreated: () => void | Promise<void>,
): void {
if (watched.has(host)) return;
watched.add(host);
const store = useDesignStore(projectId, null);
let timer = 0;
// ponytail: 저장 끝 신호가 없어 0.3 s 마다 봄 — 저장 끝 알림 창구가 생기면 그리로
const recheck = (): void => {
window.clearTimeout(timer);
if (!host.isConnected) return stops.forEach((stop) => stop());
if (savingDesign(projectId)) timer = window.setTimeout(recheck, 300);
else void mountMissingStationNotice(host, projectId, onCreated);
};
const stops = (["pipes", "structures"] as const).map((key) =>
store.subscribe(key, (value) => value === null && recheck()),
);
}
+40 -4
View File
@@ -5,6 +5,9 @@
* · 같은 측점(열쇠 · 없으면 같은 자리 같은 종류)은 앞 횡단을 이어 받음 — 설계 · 구조물 제원 · 이름 그대로 ·
* 자리(누가거리 · 좌표 · 방위 · 확폭)와 지반 표본(채워 왔으면)만 새것.
* · 새 측점 = 지반 표본 + 기본 설계(계획고 0 → 낡음 → 브라우저 다시 셈이 곧 채움).
* · 상단측 = 새 지반 표본으로 다시(단면 유형 기본값 재료 · 서버 횡단 파일 `uphill_side` 짝 · 428).
* · 0.5 m 넘게 옮긴 관 · 구조물 측점 = 기본 설계 + 사용자가 만진 칸만 — 서버가 그 행을 지우고 기본 설계에
* [저장] 수정분을 얹는 것과 같음(`sync_cross_section_rows` · `compute_default_designs` · 428).
* · 빠진 측점 = 목록에서 뺌(세션 고침은 열쇠로 남음 · 노선이 바뀐 때만 「노선 밖 고침」 · 부르는 쪽).
* · 옛 상세는 그대로 둠 — 새 객체를 돌려줌(쥔 쪽이 갈아끼움).
*
@@ -15,6 +18,7 @@
import type { PipelineStation } from "../../A00_Common/design/logic/design_pipeline_types";
import type { CrossSection, SectionDetailResponse, SectionStation } from "../B06_Section_Api_Fetch";
import { uphillSide } from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail";
interface Sample {
offset_m: number;
@@ -51,15 +55,36 @@ export function formatStation(chainageM: number): string {
export const stationIdOf = (chainageM: number): string =>
`station_${String(Math.round(chainageM * 1000)).padStart(12, "0")}`;
/** 새 측점의 기본 설계 — 계획고 0 이라 늘 낡음 → 다시 셈이 세션 선택 · 기본값으로 채움. */
const freshDesign = () =>
/** 새 측점의 기본 설계 — 계획고 0 이라 늘 낡음 → 다시 셈이 세션 선택 · 기본값으로 채움 · 단면 유형 = 상단측 깎기. */
const freshDesign = (uphill: CrossSection["uphill_side"]) =>
({
design_elevation_m: 0,
ground_type: "ripping_rock",
section_mode: "left_cut",
section_mode: `${uphill ?? "left"}_cut`,
ditch_type: "standard",
}) as unknown as NonNullable<CrossSection["design"]>;
/** 서버가 행을 지우고 새로 세우는 옮김 폭(m) — `sync_cross_section_rows` 의 0.5 m 짝. */
const FACILITY_RESEED_M = 0.5;
/** 다시 셈 입력 중 기본 설계가 정하는 칸 — 옮긴 시설 측점은 버림(세션 고침은 열쇠째 옮겨 다시 얹힘). */
const AUTO_INPUT_KEYS = [
"ditch_side",
"rock_boundary_offset_m",
"two_stage_slope",
"ditch_choice",
"ditch_enabled",
];
/** 옮긴 시설 측점 설계 — 기본 설계 위에 앞 설계의 나머지(사용자가 만진 칸 · 결과 칸은 다시 셈이 갈음). */
function reseeded(
prior: CrossSection["design"],
uphill: CrossSection["uphill_side"],
): NonNullable<CrossSection["design"]> {
const design = { ...prior, ...freshDesign(uphill) } as Record<string, unknown>;
for (const key of AUTO_INPUT_KEYS) delete design[key];
return design as unknown as NonNullable<CrossSection["design"]>;
}
function kindOf(station: PipelineStation): SectionStation["kind"] {
if (station.kind === "end") return station.key.endsWith("EP") ? "ep" : "bp";
if (station.kind === "curve") return "curve";
@@ -169,7 +194,18 @@ export function swapCrossSections(
};
longStations.push({ ...place, ...(prior?.structure ? { structure: prior.structure } : {}) });
const samples = station.ground ? groundSamples(station) : (prior?.samples ?? []);
return prior ? { ...prior, ...place, samples } : { ...place, samples, design: freshDesign() };
const uphill = station.ground ? uphillSide(station) : (prior?.uphill_side ?? null);
if (!prior) return { ...place, uphill_side: uphill, samples, design: freshDesign(uphill) };
const reseed =
(station.kind === "pipe" || station.kind === "structure") &&
Math.abs(prior.chainage_m - station.chainageM) > FACILITY_RESEED_M;
return {
...prior,
...place,
uphill_side: uphill,
samples,
...(reseed ? { design: reseeded(prior.design, uphill) } : {}),
};
});
const removed = detail.cross_sections
.map((section, i) => (used.has(section) ? null : oldKeys[i]))