Merge remote-tracking branch 'origin/sub_laptop_6' into main_laptop_1
This commit is contained in:
@@ -194,15 +194,36 @@ export async function confirmSections(
|
||||
);
|
||||
}
|
||||
|
||||
/** 서버 정본 셈이 실패했으면 경고 한 줄(388-2) — 저장은 남음 · 면적 · 관 길이는 옛 서버 값. */
|
||||
/** 서버 정본 셈이 실패했으면 경고 한 줄(388-2) — 저장은 남음 · 면적 · 관 길이는 옛 서버 값.
|
||||
* 서버가 버린 고침(행 없는 측점 · 기본 설계 못 냄 · 측점 맞춤 실패)도 한 줄(445 M5). */
|
||||
async function warnUnrecomputed(
|
||||
request: Promise<SectionConfirmResponse>,
|
||||
): Promise<SectionConfirmResponse> {
|
||||
const result = await request;
|
||||
if (result.recomputed === false)
|
||||
void import("@ui/ui_template_elements").then(({ showToast }) =>
|
||||
showToast(L("B06_Recompute_Failed"), "warning", 8000),
|
||||
);
|
||||
const dropped = [
|
||||
...new Set([
|
||||
...(result.dropped_patch_chainages ?? []),
|
||||
...(result.default_failed_chainages ?? []),
|
||||
]),
|
||||
];
|
||||
const where = dropped
|
||||
.slice(0, 5)
|
||||
.map((at) => `${at.toFixed(2)}m`)
|
||||
.join(" · ");
|
||||
const lost = [
|
||||
...(dropped.length
|
||||
? [
|
||||
`횡단 고침 ${dropped.length}곳을 못 남겼습니다 — 측점 행이 없음: ${where}` +
|
||||
(dropped.length > 5 ? ` 외 ${dropped.length - 5}곳` : ""),
|
||||
]
|
||||
: []),
|
||||
...(result.stations_synced === false ? ["구조물 측점 맞춤 실패 — 옛 측점이 남았습니다."] : []),
|
||||
];
|
||||
if (result.recomputed === false || lost.length)
|
||||
void import("@ui/ui_template_elements").then(({ showToast }) => {
|
||||
if (result.recomputed === false) showToast(L("B06_Recompute_Failed"), "warning", 8000);
|
||||
if (lost.length) showToast(lost.join(" "), "warning", 8000);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -360,6 +360,10 @@ export interface SectionConfirmResponse {
|
||||
confirmed: boolean;
|
||||
/** 서버 정본 셈(면적 · 관 길이 · 유토곡선)이 됐나 — false 면 경고(388-2). */
|
||||
recomputed?: boolean;
|
||||
/** 버린 고침(445 M5) — 행 없는 측점 patch · 기본 설계를 못 낸 측점 · 구조물 측점 맞춤 실패. */
|
||||
dropped_patch_chainages?: number[];
|
||||
default_failed_chainages?: number[];
|
||||
stations_synced?: boolean;
|
||||
}
|
||||
|
||||
/** 측점 표준횡단 설계 지정값 (버튼 상태). */
|
||||
|
||||
@@ -33,6 +33,7 @@ async def merge_cross_section_designs(
|
||||
entries: list[tuple[float, dict[str, Any]]],
|
||||
replace: bool,
|
||||
project_id: UUID | None = None,
|
||||
missed: list[float] | None = None,
|
||||
) -> int:
|
||||
"""측점 여러 곳의 `data.design` 을 한 번에 쓴다. 실제로 바뀐 행 수를 돌려준다.
|
||||
|
||||
@@ -43,7 +44,7 @@ async def merge_cross_section_designs(
|
||||
파일만 쓰고 DB 행을 안 만들기 때문이다(한 행짜리 함수와 같은 규칙).
|
||||
"""
|
||||
return await merge_cross_section_design_batches(
|
||||
connection, route_id=route_id, batches=[(entries, replace, project_id)]
|
||||
connection, route_id=route_id, batches=[(entries, replace, project_id)], missed=missed
|
||||
)
|
||||
|
||||
|
||||
@@ -52,11 +53,13 @@ async def merge_cross_section_design_batches(
|
||||
*,
|
||||
route_id: int,
|
||||
batches: list[tuple[list[tuple[float, dict[str, Any]]], bool, UUID | None]],
|
||||
missed: list[float] | None = None,
|
||||
) -> int:
|
||||
"""여러 벌(`(entries, replace, project_id)`)을 차례로 얹되 읽기 한 번 · 쓰기 한 번(342-2).
|
||||
|
||||
앞 벌이 만든 새 행도 뒤 벌이 찾는다(따로 부르던 때와 같은 결과). 값이 그대로인 행은 쓰지
|
||||
않는다 — 같은 입력의 [저장]·[확정]이 행을 다시 쓰지 않게.
|
||||
않는다 — 같은 입력의 [저장]·[확정]이 행을 다시 쓰지 않게. `missed` 를 주면 행이 없어 못 얹은
|
||||
측점(값이 있는 줄만)을 담음 — 버린 수 알림(445 M5).
|
||||
"""
|
||||
if not any(entries for entries, _, _ in batches):
|
||||
return 0
|
||||
@@ -98,6 +101,8 @@ async def merge_cross_section_design_batches(
|
||||
row_id = find(chainage_m)
|
||||
if row_id is None:
|
||||
if project_id is None or not (payload or replace):
|
||||
if missed is not None and payload:
|
||||
missed.append(chainage_m)
|
||||
continue
|
||||
row_id = -(len(fresh) + 1)
|
||||
fresh[row_id] = (str(project_id), chainage_m)
|
||||
|
||||
@@ -38,7 +38,7 @@ from B06_Section.B06_Section_Repository_Bulk import merge_cross_section_designs
|
||||
|
||||
# 원본은 `B06_Section_Router_Design` 이다 — `B06_Section_Router` 를 거쳐 들여오던 것을
|
||||
# 곧바로 잇는다(2026-09-06). 그 재수출이 없어지면서 서버가 뜨지 못했다.
|
||||
from B06_Section.B06_Section_Router_Design import STRUCTURE_ADJUST_KEYS
|
||||
from B06_Section.B06_Section_Router_Design import CARRY_INPUT_KEYS, STRUCTURE_ADJUST_KEYS
|
||||
from B06_Section.B06_Section_Router_Design import (
|
||||
compute_default_designs as _compute_default_designs,
|
||||
)
|
||||
@@ -91,20 +91,47 @@ def _rowless_station_chainages(
|
||||
return result
|
||||
|
||||
|
||||
async def _stand_patch_stations(project_id: UUID) -> None:
|
||||
async def _stand_patch_stations(project_id: UUID) -> bool:
|
||||
"""저장 · 확정 앞에 구조물 측점을 지금 관 · 구조물 목록으로 맞춘다(B05·B06 한 페이지처럼).
|
||||
|
||||
옮긴 관 · 구조물 측점은 B05 확정 전이면 종단 측점 · 횡단 파일 · DB 행이 없고, 옛 자리 측점은
|
||||
남아 있다 — 행 없는 측점의 수정이 버려지고 옛 측점이 카드 · 수량에 남음. [측점 만들기]와 같은
|
||||
조건으로 세우고 낡은 것을 지우면(맞출 것이 없으면 그냥 돌아옴 · 272) 아래
|
||||
`_rowless_station_chainages` 가 행을 만들고 수정이 그 행에 얹힌다.
|
||||
`_rowless_station_chainages` 가 행을 만들고 수정이 그 행에 얹힌다. 맞춤이 실패하면 False
|
||||
(응답에 실어 알림 · 445 M5).
|
||||
"""
|
||||
from B06_Section.B06_Section_Router_Stations import ensure_structure_stations
|
||||
|
||||
try:
|
||||
await ensure_structure_stations(project_id)
|
||||
return True
|
||||
except Exception: # 맞춤이 안 돼도 저장은 진행 — 옛 측점이 남을 뿐(다음 저장에서 다시)
|
||||
logger.exception("B06 저장: 구조물 측점 맞춤 실패(저장은 진행): project_id=%s", project_id)
|
||||
return False
|
||||
|
||||
|
||||
def _carried_inputs(
|
||||
request: SectionConfirmRequest | None, rowless: list[float]
|
||||
) -> dict[float, dict[str, Any]]:
|
||||
"""행 없는 측점(옮긴 관 · 구조물 · 새 측점)에 온 patch 의 지반 등 입력 — 기본 설계를 그 입력으로
|
||||
세움(445 H2 · 노선 [확인] 이월과 같은 `carried` · 측구 방향 결과 칸 = 화면 다시 셈)."""
|
||||
carried: dict[float, dict[str, Any]] = {}
|
||||
for item in (request.cross_patches or []) if request else []:
|
||||
at = next((c for c in rowless if abs(c - item.chainage_m) < 0.01), None)
|
||||
if at is None:
|
||||
continue
|
||||
dumped = item.model_dump()
|
||||
picked = {key: dumped[key] for key in CARRY_INPUT_KEYS if dumped.get(key) is not None}
|
||||
if picked:
|
||||
carried.setdefault(round(at, 3), {}).update(picked)
|
||||
return carried
|
||||
|
||||
|
||||
def _default_failed(
|
||||
rowless: list[float], defaults: list[tuple[float, dict[str, Any]]]
|
||||
) -> list[float]:
|
||||
"""기본 설계를 못 낸 측점 — 계산 불가 · 횡단 파일 없음(445 M5)."""
|
||||
return [c for c in rowless if all(abs(c - at) >= 0.01 for at, _ in defaults)]
|
||||
|
||||
|
||||
async def _apply_section_edits(
|
||||
@@ -113,8 +140,12 @@ async def _apply_section_edits(
|
||||
request: SectionConfirmRequest | None,
|
||||
default_designs: list[tuple[float, dict[str, Any]]],
|
||||
project_id: UUID | None = None,
|
||||
) -> None:
|
||||
"""임시 저장과 확정이 **함께 쓰는** 저장 본체. 트랜잭션은 호출한 쪽이 연다."""
|
||||
) -> list[float]:
|
||||
"""임시 저장과 확정이 **함께 쓰는** 저장 본체. 트랜잭션은 호출한 쪽이 연다.
|
||||
|
||||
돌려주는 값 = 행이 없어 못 얹은 측점 patch 의 누가거리(445 M5 · 화면이 알림).
|
||||
"""
|
||||
missed: list[float] = []
|
||||
# 행마다 쓰면 원격 DB 왕복이 행 수만큼 난다(측정: 22행 670ms) — 한 문장으로 묶는다.
|
||||
await merge_cross_section_designs(
|
||||
connection,
|
||||
@@ -158,8 +189,11 @@ async def _apply_section_edits(
|
||||
patches.append((patch_item.chainage_m, patch))
|
||||
# 측점 patch 도 한 문장으로 — 전 측점을 보내는 저장에서 왕복이 측점 수만큼 났다.
|
||||
await merge_cross_section_designs(
|
||||
connection, route_id=route_id, entries=resets + patches, replace=False
|
||||
connection, route_id=route_id, entries=resets + patches, replace=False, missed=missed
|
||||
)
|
||||
if missed:
|
||||
logger.warning("B06 저장: 행 없는 측점 patch 버림 route_id=%s %s", route_id, missed)
|
||||
return missed
|
||||
|
||||
|
||||
async def _recompute_stored_designs(project_id: UUID, route_id: int) -> bool:
|
||||
@@ -267,7 +301,7 @@ async def save_sections(
|
||||
return stale
|
||||
stored_path = await get_project_storage_relative_path(connection, project_id)
|
||||
# 측점부터 맞춤 — 아래 행 조회가 맞춘(옮기고 지운) 뒤 행을 봄(272)
|
||||
await _stand_patch_stations(project_id)
|
||||
synced = await _stand_patch_stations(project_id)
|
||||
async with pool.acquire() as connection:
|
||||
known = await get_cross_section_chainages(connection, route_id)
|
||||
# 행은 있는데 **설계가 빈** 측점도 채운다(2026-09-09). 「빈 설계」란 `design` 이
|
||||
@@ -296,12 +330,13 @@ async def save_sections(
|
||||
str(existing["longitudinal_file_path"]),
|
||||
rowless,
|
||||
request.standard_cross_section if request else None,
|
||||
_carried_inputs(request, rowless),
|
||||
)
|
||||
|
||||
async with pool.acquire() as connection:
|
||||
await connection.begin()
|
||||
try:
|
||||
await _apply_section_edits(
|
||||
missed = await _apply_section_edits(
|
||||
connection, route_id, request, default_designs, project_id
|
||||
)
|
||||
await connection.commit()
|
||||
@@ -312,7 +347,13 @@ async def save_sections(
|
||||
recomputed = await _recompute_stored_designs(project_id, route_id)
|
||||
await _prebuild_corridor(project_id, route_id, project_root)
|
||||
return SectionConfirmResponse(
|
||||
project_id=str(project_id), route_id=route_id, confirmed=False, recomputed=recomputed
|
||||
project_id=str(project_id),
|
||||
route_id=route_id,
|
||||
confirmed=False,
|
||||
recomputed=recomputed,
|
||||
dropped_patch_chainages=missed or [],
|
||||
default_failed_chainages=_default_failed(rowless, default_designs),
|
||||
stations_synced=synced,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
@@ -354,7 +395,7 @@ async def confirm_sections(
|
||||
if stale:
|
||||
return stale
|
||||
stored_path = await get_project_storage_relative_path(connection, project_id)
|
||||
await _stand_patch_stations(
|
||||
synced = await _stand_patch_stations(
|
||||
project_id
|
||||
) # 측점부터 맞춤 — 아래 행 조회가 맞춘 뒤 행을 봄(272)
|
||||
async with pool.acquire() as connection:
|
||||
@@ -384,6 +425,7 @@ async def confirm_sections(
|
||||
str(existing["longitudinal_file_path"]),
|
||||
missing,
|
||||
request.standard_cross_section if request else None,
|
||||
_carried_inputs(request, missing),
|
||||
)
|
||||
|
||||
marks.append(("기본설계 계산(미지정 측점)", time.perf_counter()))
|
||||
@@ -391,7 +433,7 @@ async def confirm_sections(
|
||||
async with pool.acquire() as connection:
|
||||
await connection.begin()
|
||||
try:
|
||||
await _apply_section_edits(
|
||||
missed = await _apply_section_edits(
|
||||
connection, route_id, request, default_designs, project_id
|
||||
)
|
||||
await confirm_sections_for_route(connection, route_id)
|
||||
@@ -445,7 +487,12 @@ async def confirm_sections(
|
||||
marks.append(("3D 길 모형 서버 짓기", time.perf_counter()))
|
||||
_log_steps("B06 확정", marks)
|
||||
return SectionConfirmResponse(
|
||||
project_id=str(project_id), route_id=route_id, recomputed=recomputed
|
||||
project_id=str(project_id),
|
||||
route_id=route_id,
|
||||
recomputed=recomputed,
|
||||
dropped_patch_chainages=missed or [],
|
||||
default_failed_chainages=_default_failed(missing, default_designs),
|
||||
stations_synced=synced,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("B06 종횡단 확정 실패: project_id=%s", project_id)
|
||||
|
||||
@@ -195,7 +195,8 @@ class CrossSectionPatch(BaseModel):
|
||||
# 암 경계선 오프셋(m, 지면선 기준 하향 음수 — 계획선 아님). 암 지반 측점만 의미 있다.
|
||||
rock_boundary_offset_m: float | None = None
|
||||
# 측점별 표시 반폭(m) — 카드 개별 조절값. 전역 반폭과 다를 때만 실린다(2026-08-06).
|
||||
display_half_width_m: float | None = Field(default=None, gt=0)
|
||||
# 0 = 되돌림(빈 전체 폭 · 정본의 옛 개별 폭을 지움 · 445 M2) — 읽는 쪽은 0 이하를 「없음」
|
||||
display_half_width_m: float | None = Field(default=None, ge=0)
|
||||
# 측점별 암 절토 경사비(1:n 의 n) — 카드에서 넣은 값(2026-09-07 사용자 지시).
|
||||
# **0 은 「표준값을 씀」**(되돌리기)이라 gt=0 이 아니라 ge=0 이다 — 0 이 와야
|
||||
# 정본에 남은 옛 사용자 값이 지워진다.
|
||||
@@ -284,6 +285,10 @@ class SectionConfirmResponse(BaseModel):
|
||||
confirmed: bool = True
|
||||
# 서버 정본 셈(면적 · 관 길이 · 유토곡선)이 됐나 — False 면 화면이 경고 한 줄(388-2)
|
||||
recomputed: bool = True
|
||||
# 버린 고침(445 M5) — 행이 없어 못 얹은 측점 patch · 기본 설계를 못 낸 측점 · 측점 맞춤 실패
|
||||
dropped_patch_chainages: list[float] = Field(default_factory=list)
|
||||
default_failed_chainages: list[float] = Field(default_factory=list)
|
||||
stations_synced: bool = True
|
||||
|
||||
|
||||
class SectionOptionDefaults(BaseModel):
|
||||
|
||||
@@ -65,12 +65,22 @@ export function withDraftAdjusts(
|
||||
for (const [role, value] of own(revet)) {
|
||||
const adjust = wallAdjust(value);
|
||||
if (adjust) design.revet_adjust = { ...(design.revet_adjust ?? {}), [role]: adjust };
|
||||
else if (value === null && design.revet_adjust?.[role]) {
|
||||
// null = 되돌림 — 저장분 그 벽을 뺌(445 M2)
|
||||
const { [role]: _gone, ...rest } = design.revet_adjust;
|
||||
design.revet_adjust = rest;
|
||||
}
|
||||
}
|
||||
if (typeof inlet[at] === "string" && INLET.includes(inlet[at] as string))
|
||||
design.inlet_structure = inlet[at] as CrossDesign["inlet_structure"];
|
||||
if (isObj(basin[at])) design.basin_adjust = { ...DEFAULT_BASIN_ADJUST, ...basin[at] };
|
||||
// 세션에 있는 쪽만 — 없는 쪽은 저장분 그대로(445 H3)
|
||||
const saved = design.extra_wall_counts;
|
||||
if (at in extra || `b${at}` in extra)
|
||||
design.extra_wall_counts = { outlet: count(extra[at]), basin: count(extra[`b${at}`]) };
|
||||
design.extra_wall_counts = {
|
||||
outlet: at in extra ? count(extra[at]) : (saved?.outlet ?? 0),
|
||||
basin: `b${at}` in extra ? count(extra[`b${at}`]) : (saved?.basin ?? 0),
|
||||
};
|
||||
for (const [wall, value] of own(spans)) {
|
||||
const span = value as { lengthM: number; beforeM: number; afterM: number } | null;
|
||||
if (!span || !Number.isFinite(span.beforeM) || !Number.isFinite(span.afterM)) continue;
|
||||
|
||||
@@ -29,8 +29,12 @@ import {
|
||||
} from "./B06_Section_Section_Drafts";
|
||||
import { applyStructureAreaRows, structureAreaRows } from "./B06_Section_Structure_Layouts";
|
||||
import { withDraftAdjusts, withPipeOptions } from "./B06_Section_Section_Adjusts";
|
||||
import type { CrossSectionPatch, SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
import { forgetCanon, keepCanon, sessionDetail } from "./B06_Section_Section_Canon";
|
||||
import type {
|
||||
CrossSectionPatch,
|
||||
SectionDetailResponse,
|
||||
StandardCrossSection,
|
||||
} from "./B06_Section_Api_Fetch";
|
||||
import { forgetCanon, keepCanon, savedDesigns, sessionDetail } from "./B06_Section_Section_Canon";
|
||||
import { fetchSectionDetail, saveSections } from "./B06_Section_Api_Fetch";
|
||||
import { staleKeyAgainstScreen, USER_TOUCHED_KEYS } from "./B06_Section_Cross_Refresh";
|
||||
import { kindChangedChainages, STRUCTURE_ADJUST_KEYS } from "./B06_Section_Api_Culvert_Options";
|
||||
@@ -44,7 +48,16 @@ import {
|
||||
type Keyed,
|
||||
type KeyedState,
|
||||
} from "./B06_Section_Station_Keys";
|
||||
import { sameStationList, swapCrossSections } from "./logic/B06_Section_Station_Swap";
|
||||
import {
|
||||
ROW_CARRY_KEYS,
|
||||
sameStationList,
|
||||
swapCrossSections,
|
||||
} from "./logic/B06_Section_Station_Swap";
|
||||
import { mergeCrossPatches } from "./logic/B06_Section_UI_Page_Patches";
|
||||
import { crossDesignChoices } from "./B06_Section_Cross_Design_Session";
|
||||
import { readCutSlopeSession, readRockBoundarySession } from "./B06_Section_UI_Page_Persist";
|
||||
import { readStandardCrossSession } from "./B06_Section_UI_Standard_Panel";
|
||||
import { balloonOffsetsPayload } from "@util/common_util_mass_haul_balance_view";
|
||||
import { emitSwap, type DetailListener } from "./B06_Section_Swap_Bus";
|
||||
|
||||
export { onAnySwap } from "./B06_Section_Swap_Bus";
|
||||
@@ -319,16 +332,20 @@ export function peekSectionDetail(
|
||||
* 최종본이다. B05 [임시저장]도 이 세트를 함께 보내야 B06에서 만진 구조물 조정이
|
||||
* 영구저장소에 남는다 — 안 보내면 캐시만 비워져 편집이 사라진다.
|
||||
*
|
||||
* 초기 계산값에는 없는 키(사용자가 만져야 생기는 키)만 싣는다.
|
||||
* 초기 계산값에는 없는 키(사용자가 만져야 생기는 키)를 측점마다 **통째로** 싣는다 — [저장] patch 의 바탕
|
||||
* (445 H3 · 세션 고침은 그 위에 `mergeCrossPatches`). `savedAt`(서버 행 누가거리)을 주면 그 자리 행이 없는
|
||||
* 측점(옮긴 관 · 구조물 · 새 측점 = 서버가 기본 설계로 새로 세움)은 지반 등 입력(`ROW_CARRY_KEYS`)도 실음(445 H2).
|
||||
*/
|
||||
export function crossPatchesFromCache(
|
||||
detail: SectionDetailResponse,
|
||||
kindChanged: readonly number[] = [],
|
||||
savedAt?: readonly number[],
|
||||
): CrossSectionPatch[] {
|
||||
const patches: CrossSectionPatch[] = [];
|
||||
// 관 종류를 바꾼 측점(318-4) — 옛 종류 조정값은 서버가 비움 · 캐시에 남은 옛 값을 다시 안 실음
|
||||
const structureKeys: readonly string[] = STRUCTURE_ADJUST_KEYS;
|
||||
const reset = (at: number) => kindChanged.some((c) => Math.abs(c - at) < PIPE_MATCH_M);
|
||||
const rowless = (at: number) => !!savedAt && savedAt.every((c) => Math.abs(c - at) >= 0.01);
|
||||
for (const at of kindChanged) patches.push({ chainage_m: at, structure_reset: true });
|
||||
for (const section of detail.cross_sections) {
|
||||
const design = section.design;
|
||||
@@ -341,7 +358,10 @@ export function crossPatchesFromCache(
|
||||
// 조용히 사라졌다(`extra_spans` 실사고 `b6941bd2`).
|
||||
const source = design as unknown as Record<string, unknown>;
|
||||
const target = patch as unknown as Record<string, unknown>;
|
||||
for (const key of USER_TOUCHED_KEYS) {
|
||||
const keys = rowless(section.chainage_m)
|
||||
? [...USER_TOUCHED_KEYS, ...ROW_CARRY_KEYS]
|
||||
: USER_TOUCHED_KEYS;
|
||||
for (const key of keys) {
|
||||
const value = source[key];
|
||||
if (value === undefined || value === null || (skip && structureKeys.includes(key))) continue;
|
||||
target[key] = value;
|
||||
@@ -353,14 +373,62 @@ export function crossPatchesFromCache(
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐시에 얹힌 사용자 수정분을 영구저장소에 남긴다(노선 저장 뒤 · 임시저장용). 수정분이 없어도
|
||||
* 한 번 보냄 — 서버가 지금 종단으로 횡단을 다시 셈(429 ④ · 노선 뒤에 나간 계획고 초안).
|
||||
* 저장 실패는 호출자가 처리한다.
|
||||
* [저장] · [확정] 횡단 patch(445) — 바탕 = 캐시 설계의 사용자 칸(측점마다 통째 · 서버 행이 없는 측점은 지반 등
|
||||
* 입력까지) 위에 세션 고침(`overlay` · 벽 · 단 dict 는 열쇠마다 합침 · 안쪽 null = 되돌림). 캐시가 없으면 세션 고침만.
|
||||
*/
|
||||
export function cachedCrossPatches(
|
||||
projectId: string,
|
||||
routeId: number,
|
||||
overlay: readonly CrossSectionPatch[] = [],
|
||||
): CrossSectionPatch[] {
|
||||
const detail = cache.get(keyOf(projectId, routeId));
|
||||
const savedAt = savedDesigns(projectId, routeId)?.map((row) => row.chainage_m);
|
||||
const base = detail
|
||||
? crossPatchesFromCache(detail, kindChangedChainages(projectId), savedAt)
|
||||
: [];
|
||||
return mergeCrossPatches(base, overlay);
|
||||
}
|
||||
|
||||
/** 노선 저장 뒤 세션에만 있는 고침(카드 선택 · 암선 · 암 경사) — 횡단 보기 [저장] 본문과 같은 칸(445 H5). */
|
||||
function sessionOverlay(projectId: string, routeId: number): CrossSectionPatch[] {
|
||||
const out: CrossSectionPatch[] = [];
|
||||
crossDesignChoices(projectId, routeId).forEach((choice, chainage) => {
|
||||
const patch: CrossSectionPatch = {
|
||||
chainage_m: chainage,
|
||||
ground_type: choice.ground_type,
|
||||
section_mode: choice.section_mode,
|
||||
};
|
||||
if (choice.ditch_side !== undefined) patch.ditch_side = choice.ditch_side;
|
||||
if (choice.ditch_type !== undefined) patch.ditch_type = choice.ditch_type;
|
||||
if (choice.paved !== undefined) patch.paved = choice.paved;
|
||||
if (choice.two_stage_slope !== undefined) patch.two_stage_slope = choice.two_stage_slope;
|
||||
if (typeof choice.ditch_choice === "boolean") patch.ditch_choice = choice.ditch_choice;
|
||||
out.push(patch);
|
||||
});
|
||||
const rock = readRockBoundarySession(projectId, routeId) ?? {};
|
||||
for (const [chainage, offset] of Object.entries(rock))
|
||||
if (typeof offset === "number")
|
||||
out.push({ chainage_m: Number(chainage), rock_boundary_offset_m: offset });
|
||||
for (const [chainage, ratio] of Object.entries(readCutSlopeSession(projectId, routeId)))
|
||||
out.push({ chainage_m: Number(chainage), cut_slope_ratio_user: ratio });
|
||||
return out.filter((patch) => Number.isFinite(patch.chainage_m));
|
||||
}
|
||||
|
||||
/**
|
||||
* 노선 저장 뒤 고침을 영구저장소에 남긴다 — 횡단 보기 [저장]과 같은 본문(캐시 사용자 칸 · 카드 선택 · 암선 ·
|
||||
* 암 경사 · 표준 횡단면 · 풍선 자리 · 445 H5). 수정분이 없어도 한 번 보냄 — 서버가 지금 종단으로 횡단을 다시 셈
|
||||
* (429 ④ · 노선 뒤에 나간 계획고 초안). 저장 실패는 호출자가 처리한다.
|
||||
*/
|
||||
export async function saveCachedCrossPatches(projectId: string, routeId: number): Promise<number> {
|
||||
const detail = cache.get(keyOf(projectId, routeId));
|
||||
const patches = detail ? crossPatchesFromCache(detail, kindChangedChainages(projectId)) : [];
|
||||
await saveSections(projectId, routeId, undefined, patches.length ? patches : undefined);
|
||||
const patches = cachedCrossPatches(projectId, routeId, sessionOverlay(projectId, routeId));
|
||||
const standard = readStandardCrossSession(projectId) as StandardCrossSection | null;
|
||||
await saveSections(
|
||||
projectId,
|
||||
routeId,
|
||||
standard ?? undefined,
|
||||
patches.length ? patches : undefined,
|
||||
{ balloon_offsets: balloonOffsetsPayload() },
|
||||
);
|
||||
clearState("kindreset", projectId); // 비울 자리를 보냈음(318-4)
|
||||
return patches.length;
|
||||
}
|
||||
|
||||
@@ -479,6 +479,9 @@ export function carryChainageStates(
|
||||
: rekey(state, raw);
|
||||
writeState(name, next, projectId, toRouteId);
|
||||
}
|
||||
// 관 종류 바꿈 자리(318-4 · 프로젝트 칸) — 구조물 조절과 같은 규칙으로 옮김(445 M7)
|
||||
const resets = readState<Record<string, unknown>>("kindreset", projectId);
|
||||
if (resets) writeState("kindreset", rekey("revetx", resets), projectId);
|
||||
return outside.size;
|
||||
}
|
||||
|
||||
@@ -512,6 +515,8 @@ function moveIdleStates(moves: ReadonlyMap<string, string>, owned: ReadonlySet<s
|
||||
: move(raw);
|
||||
writeState(name, next, projectId, routeId);
|
||||
}
|
||||
const resets = readState<Record<string, unknown>>("kindreset", projectId); // 445 M7
|
||||
if (resets) writeState("kindreset", move(resets), projectId);
|
||||
}
|
||||
|
||||
afterAnySwap((_detail, moves, owned) => moveIdleStates(moves, owned));
|
||||
|
||||
@@ -311,7 +311,7 @@ export function buildStructurePanel(
|
||||
parts.row.classList.add("b06-structure-panel__struct--basin-size");
|
||||
const readout = document.createElement("span");
|
||||
readout.className = "b06-structure-panel__hval";
|
||||
// 한계 없음 — 그림이 안 서는 0 이하만 한 눈금에서 멈춤(54-51).
|
||||
// 범위 = 서버 1.0~2.0 — 조정창(`updateAdjust`)이 자름(445 M1 · 넘으면 422 로 저장이 멈춤).
|
||||
const step = (delta: number) =>
|
||||
basinPatch(() => ({
|
||||
[field]: Math.max(
|
||||
|
||||
@@ -505,11 +505,15 @@ export function crossSaveParts(ctx: SectionPersistContext): DesignViewSave {
|
||||
const routeId = ctx.routeId();
|
||||
if (routeId === null || !ctx.projectId) return;
|
||||
const edits = collectSectionEdits(ctx);
|
||||
// 바탕 = 캐시 설계의 사용자 칸(측점마다 통째 · 옮긴 측점은 지반 등 입력까지) + 세션 고침(445 H2 · H3 · M2)
|
||||
// 가게는 늦게 불러옴(가게 → 다시 셈 → 이 파일 순환 없음)
|
||||
const { cachedCrossPatches } = await import("./B06_Section_Section_Store");
|
||||
const patches = cachedCrossPatches(ctx.projectId, routeId, edits.crossPatches);
|
||||
await (confirm ? confirmSections : saveSections)(
|
||||
ctx.projectId,
|
||||
routeId,
|
||||
ctx.standardValues(),
|
||||
edits.crossPatches.length ? edits.crossPatches : undefined,
|
||||
patches.length ? patches : undefined,
|
||||
edits.massHaul,
|
||||
);
|
||||
clearState("kindreset", ctx.projectId); // 비울 자리를 보냈음(318-4)
|
||||
|
||||
@@ -89,10 +89,10 @@ export interface StationControls {
|
||||
widths: Map<string, number>;
|
||||
inletStructures: Map<string, InletStructureChoice>;
|
||||
basinAdjustments: Map<string, BasinAdjust>;
|
||||
/** 확정 payload용 — 측점별 기슭막이 4축 조작값(역할 → 값). */
|
||||
revetAdjustsByChainage: () => Map<number, Record<string, WallAdjust>>;
|
||||
/** 확정 payload용 — 측점별 다단 단 수. */
|
||||
extraCountsByChainage: () => Map<number, { outlet: number; basin: number }>;
|
||||
/** 확정 payload용 — 측점별 기슭막이 4축 조작값(역할 → 값 · null = 되돌림). */
|
||||
revetAdjustsByChainage: () => Map<number, Record<string, WallAdjust | null>>;
|
||||
/** 확정 payload용 — 측점별 다단 단 수(세션에 있는 쪽만). */
|
||||
extraCountsByChainage: () => Map<number, Partial<{ outlet: number; basin: number }>>;
|
||||
/** 확정 payload용 — 측점별 연동 해제·종단경사 반영(2026-08-24). */
|
||||
linkFlagsByChainage: () => Map<number, { detached?: boolean; followGrade?: boolean }>;
|
||||
/** 다단 단별 구간값(2026-08-29) — 확정·임시저장 payload용. */
|
||||
@@ -127,10 +127,11 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
* 카드 하단 ◀/▶/↺으로 1m씩 조절. 세션에 보관했다가 종/횡단 확정·임시저장 때
|
||||
* cross_patches(design.display_half_width_m)로 영구 저장돼 재접근 시 유지된다.
|
||||
* 값 우선순위: 세션 → 저장값(design) → 없음(전역 반폭). */
|
||||
// 0 = 되돌림(빈 전체 폭 · 저장분 개별 폭을 지움 · 445 M2)
|
||||
const widthSession = createSessionMap<number>(
|
||||
() => deps.sessionKey("crossw"),
|
||||
(value) =>
|
||||
typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined,
|
||||
typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined,
|
||||
);
|
||||
const stationWidths = widthSession.values;
|
||||
const loadStationWidths = widthSession.load;
|
||||
@@ -142,7 +143,7 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
const stationWidthControl: StationWidthControl = {
|
||||
widthFor: (section) => {
|
||||
const session = stationWidths.get(widthKey(section.chainage_m));
|
||||
if (session !== undefined) return session;
|
||||
if (session !== undefined) return session > 0 ? session : undefined;
|
||||
const stored = section.design?.display_half_width_m;
|
||||
return typeof stored === "number" && stored > 0 ? stored : undefined;
|
||||
},
|
||||
@@ -169,9 +170,11 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
* 값은 세션에만 담는다 — 자동 자리가 지형·계획고를 따라 다시 풀리므로, 손으로
|
||||
* 만진 값은 그 세션의 표시 조정으로 본다. 키는 `누가거리:역할`.
|
||||
* 구 형식(숫자 = x 이동량)도 읽어 준다. `select`는 다시 그리지 않는다(줌·팬 보존). */
|
||||
const revetSession = createSessionMap<WallAdjust>(
|
||||
// null = 되돌림(저장분 그 벽을 지움 · 다시 열어도 안 살아남 · 445 M2)
|
||||
const revetSession = createSessionMap<WallAdjust | null>(
|
||||
() => deps.sessionKey("revetx"),
|
||||
(value) => {
|
||||
if (value === null) return null;
|
||||
// 구 형식(숫자 = x 이동량)도 읽어 준다.
|
||||
if (typeof value === "number" && Number.isFinite(value)) return { ...ZERO_ADJUST, x: value };
|
||||
if (!value || typeof value !== "object") return undefined;
|
||||
@@ -196,11 +199,12 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
/** 손으로 미는 범위 한계(m) — 조정 단위가 관 길이 1m이라 ±10m(=관 10m분)까지 둔다. */
|
||||
const round1 = (value: number): number => Math.round(value * 10) / 10;
|
||||
const clampMove = (value: number): number => Math.min(Math.max(round1(value), -10), 10);
|
||||
/** 집수정 내공 = 서버 범위(`BasinAdjustPatch` 1.0~2.0 · 넘으면 422 로 저장이 멈춤 · 445 M1). */
|
||||
const clampBasinInner = (value: number): number => Math.min(Math.max(value, 1), 2);
|
||||
const isDefaultAdjust = (value: WallAdjust): boolean =>
|
||||
Math.abs(value.x) < 1e-9 && value.d == null && value.h == null && value.m == null;
|
||||
const storeAdjust = (key: string, value: WallAdjust): void => {
|
||||
if (isDefaultAdjust(value)) revetShifts.delete(key);
|
||||
else revetShifts.set(key, value);
|
||||
revetShifts.set(key, isDefaultAdjust(value) ? null : value);
|
||||
persistRevetShifts();
|
||||
// 같은 값을 캐시(design.revet_adjust)에도 실어 3D가 바로 따라오게 한다.
|
||||
const [chainage, role] = key.split(":");
|
||||
@@ -314,10 +318,14 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
* auto(규칙)/revet(기슭막이+배관)/I/L/U(집수정 형식). 세션에만 담는다. */
|
||||
const basinSession = createSessionMap<BasinAdjust>(
|
||||
() => deps.sessionKey("basinadjust"),
|
||||
(value) =>
|
||||
value && typeof value === "object"
|
||||
? { ...DEFAULT_BASIN_ADJUST, ...(value as Partial<BasinAdjust>) }
|
||||
: undefined,
|
||||
(value) => {
|
||||
if (!value || typeof value !== "object") return undefined;
|
||||
const adjust = { ...DEFAULT_BASIN_ADJUST, ...(value as Partial<BasinAdjust>) };
|
||||
// 옛 세션의 범위 밖 내공 — 서버가 422 로 막아 저장이 멈춤(445 M1)
|
||||
adjust.innerWidthM = clampBasinInner(adjust.innerWidthM);
|
||||
adjust.innerHeightM = clampBasinInner(adjust.innerHeightM);
|
||||
return adjust;
|
||||
},
|
||||
);
|
||||
const structSession = createSessionMap<InletStructureChoice>(
|
||||
() => deps.sessionKey("inletstruct"),
|
||||
@@ -335,9 +343,10 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
|
||||
/* ── 유출측 추가 기슭막이 개수(2026-08-22 사용자 — 성토부 5m 이상 계단식) ──
|
||||
* 측점별 개수만 세션에 담는다. 각 벽의 이동량은 revetShifts에 `extra{n}` 키로. */
|
||||
// 0 = 되돌림(저장분 단 수를 지움 · 445 M2)
|
||||
const extraSession = createSessionMap<number>(
|
||||
() => deps.sessionKey("extrawall"),
|
||||
(value) => (Number.isInteger(value) && (value as number) > 0 ? (value as number) : undefined),
|
||||
(value) => (Number.isInteger(value) && (value as number) >= 0 ? (value as number) : undefined),
|
||||
);
|
||||
const extraCounts = extraSession.values;
|
||||
const loadExtraCounts = extraSession.load;
|
||||
@@ -349,6 +358,13 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
const extraKey = (chainageM: number, side: "outlet" | "basin"): string =>
|
||||
side === "basin" ? `b${chainageM.toFixed(2)}` : chainageM.toFixed(2);
|
||||
const extraPrefix = (side: "outlet" | "basin"): string => (side === "basin" ? "bextra" : "extra");
|
||||
/** 두 쪽 단 수 — 세션 → 저장분 · `side` 는 `count`(캐시 patch · 다른 쪽을 0 으로 덮지 않음 · 445 H3). */
|
||||
const countsWith = (chainageM: number, side: "outlet" | "basin", count: number) => {
|
||||
const counts = { outlet: 0, basin: 0, ...sectionAt(chainageM)?.design?.extra_wall_counts };
|
||||
for (const other of ["outlet", "basin"] as const)
|
||||
counts[other] = extraCounts.get(extraKey(chainageM, other)) ?? counts[other];
|
||||
return { ...counts, [side]: count };
|
||||
};
|
||||
|
||||
const extraWallControl: ExtraWallControl = {
|
||||
countFor: (section, side = "outlet") =>
|
||||
@@ -363,11 +379,12 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
consumeEqualize: (section) => pendingEqualize.delete(section.chainage_m.toFixed(2)),
|
||||
setCount: (chainageM, requested, side = "outlet") => {
|
||||
const key = extraKey(chainageM, side);
|
||||
const previous = extraCounts.get(key) ?? 0;
|
||||
// 세션 → 저장분(새 탭에서 저장된 단을 줄여도 먹음 · 445 M2)
|
||||
const previous =
|
||||
extraCounts.get(key) ?? sectionAt(chainageM)?.design?.extra_wall_counts?.[side] ?? 0;
|
||||
const count = Math.max(0, Math.min(9, Math.round(requested)));
|
||||
if (count === previous) return;
|
||||
if (count === 0) extraCounts.delete(key);
|
||||
else extraCounts.set(key, count);
|
||||
extraCounts.set(key, count); // 0 도 남김 — 저장분 단 수를 지움(445 M2)
|
||||
// 줄어든 단의 이동량·구간값은 지운다 — 다시 늘리면 기본값에서 시작한다.
|
||||
for (let i = count; i < previous; i += 1) {
|
||||
revetShifts.delete(revetKey(chainageM, `${extraPrefix(side)}${i}` as RevetKey));
|
||||
@@ -376,12 +393,7 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
persistRevetShifts();
|
||||
persistExtraSpans();
|
||||
persistExtraCounts();
|
||||
patchCachedDesign(chainageM, {
|
||||
extra_wall_counts: {
|
||||
outlet: extraCounts.get(extraKey(chainageM, "outlet")) ?? 0,
|
||||
basin: extraCounts.get(extraKey(chainageM, "basin")) ?? 0,
|
||||
},
|
||||
});
|
||||
patchCachedDesign(chainageM, { extra_wall_counts: countsWith(chainageM, side, count) });
|
||||
refreshWithLinked(chainageM);
|
||||
deps.onExtraCount?.();
|
||||
},
|
||||
@@ -401,10 +413,7 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
persistExtraCounts();
|
||||
// 캐시 patch 도 세운 만큼으로 — 세션 키를 지우면 `countFor` 가 옛 patch 수로 돌아감(54-1)
|
||||
patchCachedDesign(chainageM, {
|
||||
extra_wall_counts: {
|
||||
outlet: extraCounts.get(extraKey(chainageM, "outlet")) ?? 0,
|
||||
basin: extraCounts.get(extraKey(chainageM, "basin")) ?? 0,
|
||||
},
|
||||
extra_wall_counts: countsWith(chainageM, side, Math.max(built, 0)),
|
||||
});
|
||||
deps.onExtraCount?.();
|
||||
},
|
||||
@@ -431,8 +440,8 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
const current = basinAdjustments.get(key) ?? { ...DEFAULT_BASIN_ADJUST };
|
||||
const next = { ...current, ...patch };
|
||||
basinAdjustments.set(key, {
|
||||
innerWidthM: Math.max(0.1, round1(next.innerWidthM)),
|
||||
innerHeightM: Math.max(0.1, round1(next.innerHeightM)),
|
||||
innerWidthM: clampBasinInner(round1(next.innerWidthM)),
|
||||
innerHeightM: clampBasinInner(round1(next.innerHeightM)),
|
||||
lateralM: Math.max(0, clampMove(next.lateralM)),
|
||||
slopeM: Math.max(0, clampMove(next.slopeM)),
|
||||
});
|
||||
@@ -515,12 +524,12 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
*/
|
||||
const dropLinkedAdjusts = (chainageM: number): void => {
|
||||
const prefix = `${chainageM.toFixed(2)}:`;
|
||||
for (const key of [...revetShifts.keys()]) {
|
||||
if (!key.startsWith(prefix) || key.slice(prefix.length) === "own") continue;
|
||||
revetShifts.delete(key);
|
||||
}
|
||||
persistRevetShifts();
|
||||
const design = sectionAt(chainageM)?.design;
|
||||
const roles = [...revetShifts.keys()].filter((key) => key.startsWith(prefix));
|
||||
for (const role of Object.keys(design?.revet_adjust ?? {})) roles.push(`${prefix}${role}`);
|
||||
// 저장분 굳힌 값도 되돌림 표시(null · 445 M2)
|
||||
for (const key of roles) if (key.slice(prefix.length) !== "own") revetShifts.set(key, null);
|
||||
persistRevetShifts();
|
||||
if (!design?.revet_adjust) return;
|
||||
const own = design.revet_adjust.own;
|
||||
design.revet_adjust = own ? { own } : {};
|
||||
@@ -607,7 +616,7 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
basinAdjustments,
|
||||
// 확정·임시저장 payload용 — 세션 키(누가거리:역할)를 측점별로 되접는다.
|
||||
revetAdjustsByChainage: () => {
|
||||
const result = new Map<number, Record<string, WallAdjust>>();
|
||||
const result = new Map<number, Record<string, WallAdjust | null>>();
|
||||
revetShifts.forEach((adjust, key) => {
|
||||
const [chainage, role] = key.split(":");
|
||||
const value = Number(chainage);
|
||||
@@ -631,12 +640,13 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
return result;
|
||||
},
|
||||
extraCountsByChainage: () => {
|
||||
const result = new Map<number, { outlet: number; basin: number }>();
|
||||
// 세션에 있는 쪽만 — 없는 쪽은 바탕(캐시 설계) 값 그대로(445 H3 · `mergeCrossPatches`)
|
||||
const result = new Map<number, Partial<{ outlet: number; basin: number }>>();
|
||||
extraCounts.forEach((count, key) => {
|
||||
const basin = key.startsWith("b");
|
||||
const value = Number(basin ? key.slice(1) : key);
|
||||
if (!Number.isFinite(value)) return;
|
||||
const bucket = result.get(value) ?? { outlet: 0, basin: 0 };
|
||||
const bucket = result.get(value) ?? {};
|
||||
bucket[basin ? "basin" : "outlet"] = count;
|
||||
result.set(value, bucket);
|
||||
});
|
||||
@@ -669,11 +679,9 @@ export function createStationControls(deps: StationControlDeps): StationControls
|
||||
},
|
||||
applyGlobalWidth: (requested, chainages) => {
|
||||
stationWidths.clear();
|
||||
if (requested !== undefined) {
|
||||
for (const chainageM of chainages) {
|
||||
stationWidths.set(widthKey(chainageM), clampStationWidth(requested));
|
||||
}
|
||||
}
|
||||
// 빈 전체 폭 = 0(되돌림 — 저장분 개별 폭도 지움 · 445 M2)
|
||||
const width = requested === undefined ? 0 : clampStationWidth(requested);
|
||||
for (const chainageM of chainages) stationWidths.set(widthKey(chainageM), width);
|
||||
persistStationWidths();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -408,6 +408,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
// 이래야 B05 에서 넣고 B06 으로 넘어와도 같은 목록이 보인다(2026-09-12 사용자).
|
||||
const pendingPipes = readPendingPipes(projectId);
|
||||
pipeFacilities = (pendingPipes ?? pipeResponse.pipe_points).map((pipe) => ({
|
||||
id: pipe.id, // 측점 열쇠 `pipe:<id>` — 초안까지 들고 감(445 H2)
|
||||
chainage_m: pipe.chainage_m,
|
||||
facility: pipe.facility ?? "pipe",
|
||||
start_m: pipe.start_m,
|
||||
@@ -496,6 +497,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
writePendingPipes(
|
||||
deps.projectId,
|
||||
pipeFacilities.map((pipe) => ({
|
||||
...(pipe.id ? { id: pipe.id } : {}), // B05 초안과 같은 꼴(445 H2)
|
||||
chainage_m: pipe.chainage_m,
|
||||
source: pipe.source ?? "user",
|
||||
facility: pipe.facility,
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
* 자리(누가거리 · 좌표 · 방위 · 확폭)와 지반 표본(채워 왔으면)만 새것.
|
||||
* · 새 측점 = 지반 표본 + 기본 설계(계획고 0 → 낡음 → 브라우저 다시 셈이 곧 채움).
|
||||
* · 상단측 = 새 지반 표본으로 다시(단면 유형 기본값 재료 · 서버 횡단 파일 `uphill_side` 짝 · 428).
|
||||
* · 0.5 m 넘게 옮긴 관 · 구조물 측점 = 기본 설계 + 사용자가 만진 칸만 — 서버가 그 행을 지우고 기본 설계에
|
||||
* [저장] 수정분을 얹는 것과 같음(`sync_cross_section_rows` · `compute_default_designs` · 428).
|
||||
* · 0.5 m 넘게 옮긴 관 · 구조물 측점 = 기본 설계 + 사용자 칸(만진 칸 · 지반 등 입력 `ROW_CARRY_KEYS`) — 서버가
|
||||
* 그 행을 지우고 기본 설계에 [저장] patch 를 얹음(`sync_cross_section_rows` · `compute_default_designs` · 428) ·
|
||||
* patch 가 그 칸을 모두 실음(`crossPatchesFromCache` · 445 H2).
|
||||
* · 빠진 측점 = 목록에서 뺌(세션 고침은 열쇠로 남음 · 노선이 바뀐 때만 「노선 밖 고침」 · 부르는 쪽).
|
||||
* · 옛 상세는 그대로 둠 — 새 객체를 돌려줌(쥔 쪽이 갈아끼움).
|
||||
*
|
||||
@@ -66,21 +67,30 @@ const freshDesign = (uphill: CrossSection["uphill_side"]) =>
|
||||
|
||||
/** 서버가 행을 지우고 새로 세우는 옮김 폭(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",
|
||||
/** 다시 셈 입력 중 새 지형이 정하는 칸 — 옮긴 시설 측점은 버림(단면 유형은 기본 설계 · 서버 `CARRY_INPUT_KEYS` 밖). */
|
||||
const AUTO_INPUT_KEYS = ["ditch_side", "ditch_enabled"];
|
||||
/**
|
||||
* 옮긴 측점이 들고 가는 사용자 입력(445 H2) — 서버 `CARRY_INPUT_KEYS` 짝(노선 [확인] 이월과 같은 칸).
|
||||
* 서버에 그 자리 행이 없으면(옮김 · 새 측점) [저장] patch 가 캐시 설계의 이 칸을 실음.
|
||||
*/
|
||||
export const ROW_CARRY_KEYS = [
|
||||
"ground_type",
|
||||
"ditch_type",
|
||||
"ditch_choice",
|
||||
"ditch_enabled",
|
||||
];
|
||||
"two_stage_slope",
|
||||
"rock_boundary_offset_m",
|
||||
"cut_slope_ratio_user",
|
||||
"berm",
|
||||
] as const;
|
||||
|
||||
/** 옮긴 시설 측점 설계 — 기본 설계 위에 앞 설계의 나머지(사용자가 만진 칸 · 결과 칸은 다시 셈이 갈음). */
|
||||
/** 옮긴 시설 측점 설계 — 기본 설계 위에 앞 설계의 나머지(사용자 칸 · 지반 등 입력) · 결과 칸은 다시 셈이 갈음. */
|
||||
function reseeded(
|
||||
prior: CrossSection["design"],
|
||||
uphill: CrossSection["uphill_side"],
|
||||
): NonNullable<CrossSection["design"]> {
|
||||
const design = { ...prior, ...freshDesign(uphill) } as Record<string, unknown>;
|
||||
const before = (prior ?? {}) as Record<string, unknown>;
|
||||
const design = { ...before, ...freshDesign(uphill) } as Record<string, unknown>;
|
||||
for (const key of ROW_CARRY_KEYS) if (before[key] !== undefined) design[key] = before[key];
|
||||
for (const key of AUTO_INPUT_KEYS) delete design[key];
|
||||
return design as unknown as NonNullable<CrossSection["design"]>;
|
||||
}
|
||||
|
||||
@@ -22,8 +22,10 @@ export interface CrossPatchSources {
|
||||
stationWidths: Map<string, number>;
|
||||
inletStructures: Map<string, InletStructureChoice>;
|
||||
basinAdjustments: Map<string, BasinAdjust>;
|
||||
revetAdjusts: Map<number, Record<string, StoredWallAdjust>>;
|
||||
extraCounts: Map<number, { outlet: number; basin: number }>;
|
||||
/** 벽 역할 → 값 · null = 되돌림(바탕 dict 에서 그 벽을 뺌 · 445 M2). */
|
||||
revetAdjusts: Map<number, Record<string, StoredWallAdjust | null>>;
|
||||
/** 세션에 있는 쪽만(445 H3 — 없는 쪽은 바탕 값 그대로). */
|
||||
extraCounts: Map<number, Partial<{ outlet: number; basin: number }>>;
|
||||
/** 다단 단별 구간값(2026-08-29) — 키는 벽 키("extra0"…/"bextra0"…). */
|
||||
extraSpans: Map<number, Record<string, StoredWallSpan>>;
|
||||
fordAdjusts: Map<number, FordAdjust>;
|
||||
@@ -59,11 +61,12 @@ export function buildCrossPatches(sources: CrossPatchSources): CrossSectionPatch
|
||||
patchFor(Number(chainage)).basin_adjust = adjust;
|
||||
});
|
||||
// 기슭막이 4축·다단 단 수 — 세션 전용이던 값을 정본에 실어 확정한다(2026-08-24).
|
||||
// 되돌림(null) · 한쪽 단 수는 바탕과 합칠 때 풀림(`mergeCrossPatches`) — 혼자 나가면 null 걷고 빈 쪽 0
|
||||
sources.revetAdjusts.forEach((adjusts, chainage) => {
|
||||
patchFor(chainage).revet_adjust = adjusts;
|
||||
patchFor(chainage).revet_adjust = adjusts as Record<string, StoredWallAdjust>;
|
||||
});
|
||||
sources.extraCounts.forEach((counts, chainage) => {
|
||||
patchFor(chainage).extra_wall_counts = counts;
|
||||
patchFor(chainage).extra_wall_counts = counts as { outlet: number; basin: number };
|
||||
});
|
||||
// 다단 단별 구간값 — 세션에서 만진 값을 정본에 싣는다(2026-08-29 사용자).
|
||||
sources.extraSpans.forEach((spans, chainage) => {
|
||||
@@ -83,3 +86,44 @@ export function buildCrossPatches(sources: CrossPatchSources): CrossSectionPatch
|
||||
});
|
||||
return [...patchByChainage.values()];
|
||||
}
|
||||
|
||||
/** 벽 · 단 · 쪽 열쇠로 담는 칸 — 세션은 만진 열쇠만 들고 있어 바탕(캐시 설계)과 **열쇠마다** 합침(445 H3). */
|
||||
const DICT_KEYS = new Set(["revet_adjust", "extra_spans", "extra_wall_counts"]);
|
||||
|
||||
type Loose = Record<string, unknown>;
|
||||
const isDict = (value: unknown): value is Loose =>
|
||||
!!value && typeof value === "object" && !Array.isArray(value);
|
||||
/** 안쪽 null = 되돌림 — 그 열쇠를 뺌(서버는 dict 를 통째 갈아 끼움). */
|
||||
const withoutNulls = (dict: Loose): Loose =>
|
||||
Object.fromEntries(Object.entries(dict).filter(([, value]) => value !== null));
|
||||
|
||||
/**
|
||||
* 저장 patch = 바탕(캐시 설계의 사용자 칸 · 측점마다 통째) 위에 세션 고침을 얹음(445 H2 · H3 · M2).
|
||||
* 같은 측점 = 누가거리 0.01 m 글. 벽 · 단 · 쪽 dict 는 열쇠마다 합치고 안쪽 null 은 뺌 · 나머지 칸은 세션이 이김.
|
||||
* 바탕에 없는 측점의 세션 고침도 그대로 나감(안쪽 null 걷고 · 단 수 빈 쪽 0).
|
||||
*/
|
||||
export function mergeCrossPatches(
|
||||
base: readonly CrossSectionPatch[],
|
||||
overlay: readonly CrossSectionPatch[],
|
||||
): CrossSectionPatch[] {
|
||||
const out = new Map<string, Loose>();
|
||||
const join = (patch: CrossSectionPatch): void => {
|
||||
const at = patch.chainage_m.toFixed(2);
|
||||
const into = out.get(at) ?? { chainage_m: patch.chainage_m };
|
||||
for (const [key, value] of Object.entries(patch)) {
|
||||
if (key === "chainage_m" || value === undefined) continue;
|
||||
into[key] =
|
||||
DICT_KEYS.has(key) && isDict(value)
|
||||
? withoutNulls({ ...(isDict(into[key]) ? into[key] : {}), ...value })
|
||||
: value;
|
||||
}
|
||||
out.set(at, into);
|
||||
};
|
||||
base.forEach(join);
|
||||
overlay.forEach(join);
|
||||
for (const patch of out.values()) {
|
||||
const counts = patch.extra_wall_counts;
|
||||
if (isDict(counts)) patch.extra_wall_counts = { outlet: 0, basin: 0, ...counts };
|
||||
}
|
||||
return [...out.values()] as unknown as CrossSectionPatch[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user