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

This commit is contained in:
2026-09-09 21:58:29 +09:00
5 changed files with 96 additions and 2 deletions
+3
View File
@@ -621,6 +621,9 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
context: () => context,
standardValues: () => standardPanel?.getValues(),
flushCulvertOptions: () => stationControls.flushCulvertOptions(),
// 저장 직전 전 측점 재계산 — 표준단면·계획선을 고친 뒤 안 만진 측점이 옛 면적으로
// 실려 나가던 자리(2026-09-09 실측).
reconcileDesigns: () => reconcileStaleDesigns({ force: true }),
patchSources: () => ({
rockOffsets,
cutSlopeRatios,
@@ -351,6 +351,11 @@ export interface SectionPersistContext {
flushCulvertOptions: () => Promise<void>;
/** 측점별 편집분의 출처 묶음 — 세션·제어기에 흩어진 값을 페이지가 모아 준다. */
patchSources: () => CrossPatchSources;
/** 저장 직전 **전 측점을 다시 계산**한다(표준단면·계획선 최신값으로).
* ⚠ 없으면 옛 면적이 그대로 실려 나간다 — 2026-09-09 실측: 표준단면 암 측구 상폭을
* 0.69 → 0.9 로 고친 뒤 저장했더니 정본 한 줄에 **새 측구 기하(0.9) + 옛 절토 면적
* (0.69 기준 3.33㎡)** 이 섞여 남았다. 만진 측점만 새 값으로 서던 자리다. */
reconcileDesigns: () => Promise<void>;
}
/** 확정과 임시 저장이 함께 보내는 편집분(암 경계선 오프셋 + 유토곡선 + balloon 위치). */
@@ -444,6 +449,9 @@ export async function saveCurrentSections(ctx: SectionPersistContext): Promise<v
if (!ctx.projectId || routeId === null) return;
showLoadingOverlay();
try {
// ⚠ 면적을 모으기 **전에** 전 측점을 다시 계산한다 — 안 그러면 표준단면을 고친 뒤
// 안 만진 측점이 옛 면적으로 실려 나간다(2026-09-09 실측).
await ctx.reconcileDesigns();
await flushPendingEdits(ctx, ctx.projectId);
const edits = collectSectionEdits(ctx);
await saveSections(
@@ -472,6 +480,7 @@ export async function confirmCurrentSections(ctx: SectionPersistContext): Promis
if (!ctx.projectId || routeId === null) return;
showLoadingOverlay();
try {
await ctx.reconcileDesigns(); // 저장과 같은 이유 — 옛 면적이 정본으로 굳는 것을 막는다
await flushPendingEdits(ctx, ctx.projectId);
const edits = collectSectionEdits(ctx);
await confirmSections(
+30 -2
View File
@@ -284,7 +284,29 @@ export function createStandardPanel(
const body = document.createElement("div");
body.className = "b06-std__body";
const persist = (): void => writeSession(projectId, state);
// ⚠ **패널 값이 측점에 실렸는지**를 보인다(2026-09-09 실측). 패널을 고쳐도 측점은 그대로라
// [전체 반영]을 눌러야 따라오는데, **카드를 하나 만지면 그 측점만** 새 값으로 다시 선다
// (`handleDesignChange` 가 패널 값을 실어 보냄). 알려 주지 않으면 한 화면에 옛 값과 새 값이
// 섞여 서고, 사용자는 왜 그 측점만 값이 달라졌는지 알 길이 없다.
// ⓘ 실측 예 — 측점 20.0m 절토 계 3.33㎡(저장분) ↔ 3.90㎡(패널 초안: 암 측구 상폭 0.69→0.9).
let applied = JSON.stringify(defaults);
const pending = document.createElement("p");
pending.className = "b06-std__pending";
pending.textContent =
"고친 값이 측점에 아직 안 실렸습니다 — [전체 측점 반영]을 누르면 바로 따라오고, " +
"[저장]·[확정] 때도 전 측점이 이 값으로 다시 섭니다. 그전에 카드를 만지면 그 측점만 먼저 바뀝니다.";
const showPending = (): void => {
const now = JSON.stringify(state);
// 값이 실렸는지 눈으로도 캘 수 있게 남긴다 — 화면 실측에서 판정 근거가 된다.
pending.dataset.now = String(now.length);
pending.dataset.applied = String(applied.length);
pending.hidden = now === applied;
};
const persist = (): void => {
writeSession(projectId, state);
showPending();
};
/** 필드 한 칸. 공통은 토사 값을 보이고, 고치면 세 그룹에 함께 펼친다. */
const buildField = (spec: NumberFieldSpec, grid: HTMLElement): void => {
@@ -411,17 +433,23 @@ export function createStandardPanel(
actions.append(resetButton);
// 횡단 반폭은 사이드의 **별도 컨테이너**로 뺐다(2026-08-23) — 여기는 표준단면 설정만.
root.append(body, loader, actions);
showPending();
root.append(body, loader, pending, actions);
return {
root,
getValues: () => state,
applyStored: (stored) => {
// 측점에 실려 있는 값이 곧 정본이다 — 「안 실린 값」 판정 기준을 여기서 잡는다.
applied = JSON.stringify(stored);
showPending();
if (hadSession) return; // 진행 중 세션 편집값이 우선.
(Object.keys(stored) as StandardCrossKey[]).forEach((key) => {
if (stored[key]) state[key] = JSON.parse(JSON.stringify(stored[key])) as StandardCrossGroup;
});
unifyCommon(state);
applied = JSON.stringify(state);
showPending();
renderBody();
},
};
+10
View File
@@ -318,6 +318,16 @@
gap: var(--spacing-8);
}
/* 고친 값이 아직 측점에 안 실렸다는 알림 — [전체 측점 반영] 바로 위. */
.b06-std__pending {
margin: var(--spacing-8) 0 0;
padding: var(--spacing-8);
border-left: 3px solid var(--color-warning, #d98324);
background: var(--color-surface-muted, rgb(217 131 36 / 8%));
font-size: 0.85em;
line-height: 1.45;
}
/* --- 우측 결과 --- */
.b06-profile__result {
display: flex;
@@ -0,0 +1,44 @@
"""[저장]·[확정]은 **면적을 모으기 전에** 전 측점을 다시 계산한다 (2026-09-09 실측).
⚠ 잡은 자리 — 표준단면 「암 측구 상폭」을 0.69 → 0.9 로 고쳐 저장했더니 정본 한 줄에
**새 측구 기하(상폭 0.9 · 측구 0.18㎡)** 와 **옛 절토 면적(0.69 기준 3.33㎡)** 이 섞여
남았다. 브라우저는 만진 측점만 다시 계산하는데 저장은 **전 측점 면적**을 실어 보내므로,
안 만진 측점이 옛 값 그대로 정본이 된다.
(같은 입력을 주면 파이썬·TS 는 같은 값을 낸다 — 거울 문제가 아니라 **입력이 갈린 것**이다.)
⇒ `saveCurrentSections`·`confirmCurrentSections` 가 `collectSectionEdits` **앞에서**
`ctx.reconcileDesigns()` 를 부른다. 순서가 뒤집히면 옛 면적이 그대로 실린다.
"""
from __future__ import annotations
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
PERSIST = ROOT / "B06_Section" / "B06_Section_UI_Page_Persist.ts"
PAGE = ROOT / "B06_Section" / "B06_Section_UI_Page.ts"
def _body(source: str, marker: str) -> str:
start = source.index(marker)
return source[start : start + 1200]
def test_저장이_면적을_모으기_전에_다시_계산한다() -> None:
text = PERSIST.read_text(encoding="utf-8")
for marker in (
"export async function saveCurrentSections",
"export async function confirmCurrentSections",
):
body = _body(text, marker)
assert "ctx.reconcileDesigns()" in body, f"{marker} 가 재계산을 안 부른다"
assert body.index("ctx.reconcileDesigns()") < body.index("collectSectionEdits(ctx)"), (
f"{marker}: 재계산이 면적 수집보다 뒤에 있다 — 옛 면적이 실린다"
)
def test_페이지가_재계산_통로를_이어_준다() -> None:
text = PAGE.read_text(encoding="utf-8")
assert "reconcileDesigns: () => reconcileStaleDesigns({ force: true })" in text, (
"페이지가 통로를 안 이어 주면 저장이 옛 값 그대로 나간다"
)