From cdf4814719d85c34d117b91b6509fbb9c141ff45 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 18 Aug 2026 18:39:39 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05):=20=EC=9E=84=EC=8B=9C=20=EB=B0=B0?= =?UTF-8?q?=EC=B9=98=EB=A5=BC=20=EC=B8=A1=EC=A0=90+=EC=9E=94=EC=97=AC?= =?UTF-8?q?=EA=B1=B0=EB=A6=AC=20=EC=9E=85=EB=A0=A5=EC=9D=B4=20=EB=81=9D?= =?UTF-8?q?=EB=82=9C=20=EB=92=A4=EB=A1=9C=20=EC=A7=80=EC=97=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 측점번호만 넣고 잔여거리 칸으로 넘어가는 순간(blur) 임시 배치가 나가 재계산 동기화가 입력을 끊었다(2026-08-18 사용자 보고 2차). - 임시 배치 시점 변경: 측점 입력군(positionRow)에서 포커스가 완전히 빠져나간 뒤(focusout + rAF 판정) 또는 Enter. 칸 사이 이동 중에는 나가지 않는다. - 관이 계획선에 스냅되며 입력 누가거리와 어긋나면 임시 추적이 풀리던 것 — 허용오차를 0.05m에서 0.51m(관 매칭 공통 기준)로 넓히고, 재계산 후 실제 좌표로 임시·선택 누가거리를 갱신해 계속 따라간다. - 우클릭 추가는 측점이 확정돼 들어오므로 즉시 임시 배치(기존과 동일). Co-Authored-By: Claude Opus 5 (1M context) --- .../B05_Profile_UI_Structures_Panel.ts | 85 ++++++++++++------- 1 file changed, 56 insertions(+), 29 deletions(-) diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel.ts b/B05_Profile/B05_Profile_UI_Structures_Panel.ts index f83facf2..20863684 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel.ts @@ -264,26 +264,30 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu facilityOptions.root.classList.toggle("is-locked", locked); } - /** 측점 입력 변경 — A군이면 임시 배치를 만들거나 옮기고(세부유역 미리보기, - * 2026-08-18), 어느 군이든 옵션 잠금을 다시 판정한다. */ - function handleStationInput(): void { + /** A군이면 기준 측점(측점번호+잔여거리)으로 임시 배치를 만들거나 옮긴다 + * (세부유역 미리보기, 2026-08-18). 측점 두 칸을 **다 쓰고 빠져나온 뒤**에만 + * 부른다 — 측점번호만 넣고 거리 칸으로 넘어가는 순간 나가면 재계산 동기화가 + * 입력을 끊는다(2026-08-18 사용자 보고). */ + function commitTempPipe(): void { const type = currentType(); - if (type?.managed_by) { - const anchor = anchorFields.read(interval(), false); - // 기존 관(임시 아님)을 수정 중일 때 기준점 이동은 [수정]으로만 반영한다. - if (anchor !== null && (selectedPipeChainage === null || tempPipeChainage !== null)) { - const attributes: FacilityAttributes = { facility: type.type_id as PipeFacility }; - if (tempPipeChainage === null) { - tempPipeChainage = anchor; - callbacks.onPipeAdd(anchor, attributes); - } else if (Math.abs(tempPipeChainage - anchor) > 0.005) { - const from = tempPipeChainage; - tempPipeChainage = anchor; - callbacks.onPipeUpdate(from, anchor, attributes); - } - syncButtons(); - } + if (!type?.managed_by) return; + const anchor = anchorFields.read(interval(), false); + // 기존 관(임시 아님)을 수정 중일 때 기준점 이동은 [수정]으로만 반영한다. + if (anchor === null || (selectedPipeChainage !== null && tempPipeChainage === null)) return; + const attributes: FacilityAttributes = { facility: type.type_id as PipeFacility }; + if (tempPipeChainage === null) { + tempPipeChainage = anchor; + callbacks.onPipeAdd(anchor, attributes); + } else if (Math.abs(tempPipeChainage - anchor) > 0.005) { + const from = tempPipeChainage; + tempPipeChainage = anchor; + callbacks.onPipeUpdate(from, anchor, attributes); } + syncButtons(); + } + + /** 측점 입력 변경 — 옵션 잠금만 다시 판정한다(임시 배치는 commitTempPipe 몫). */ + function handleStationInput(): void { syncOptionLock(); } @@ -354,7 +358,7 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu if (tempPipeChainage === null) return; const chainage = tempPipeChainage; tempPipeChainage = null; - if (selectedPipeChainage !== null && Math.abs(selectedPipeChainage - chainage) < 0.05) { + if (selectedPipeChainage !== null && Math.abs(selectedPipeChainage - chainage) < 0.51) { selectedPipeChainage = null; } callbacks.onPipeRemove(chainage); @@ -449,7 +453,9 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu // 임시 배치 중인 그 관이 재계산을 거쳐 되돌아온 경우 — 사용자가 폼에 입력하는 // 중이므로 폼을 다시 그리지 않는다(입력이 지워진다, 2026-08-18 사용자 보고). // 재계산으로 갱신된 설계유량만 살짝 반영하고 선택 표시를 맞춘다. - const isTemp = tempPipeChainage !== null && Math.abs(pipe.chainage_m - tempPipeChainage) < 0.05; + // 관은 계획선에 스냅되며 입력 누가거리와 살짝 어긋날 수 있다 — 0.51m까지 같은 + // 관으로 본다(다른 곳의 관 매칭 기준과 동일). + const isTemp = tempPipeChainage !== null && Math.abs(pipe.chainage_m - tempPipeChainage) < 0.51; if (isTemp) { tempPipeChainage = pipe.chainage_m; selectedPipeChainage = pipe.chainage_m; @@ -637,12 +643,26 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu // 리셋은 폼만 아니라 구조물군·종류도 빈칸으로 되돌린다(2026-08-17 사용자 지시). // 임시 배치 취소도 겸한다(2026-08-18). resetButton.addEventListener("click", resetForm); - // 측점 입력 감시 — 옵션 잠금 해제 판정 + A군 임시 배치 생성·이동(2026-08-18). + // 측점 입력 감시 — 옵션 잠금 해제 판정(2026-08-18). [anchorFields, startFields, endFields].forEach((fields) => [fields.station, fields.remainder].forEach((input) => input.addEventListener("change", handleStationInput), ), ); + // A군 임시 배치는 측점 입력군에서 포커스가 완전히 빠져나간 뒤에만 — 측점번호를 + // 넣고 잔여거리 칸으로 옮기는 중간에는 나가지 않는다(2026-08-18 사용자 지시: + // 측점+거리를 다 받은 후 임시 배치). Enter로도 바로 나갈 수 있다. + positionRow.addEventListener("focusout", () => { + // 다음 포커스 대상이 확정된 다음 프레임에 판단한다(relatedTarget은 브라우저에 + // 따라 비어 온다). + window.requestAnimationFrame(() => { + if (positionRow.contains(document.activeElement)) return; + commitTempPipe(); + }); + }); + positionRow.addEventListener("keydown", (event) => { + if (event.key === "Enter") commitTempPipe(); + }); body.insertBefore(field("메모", memoField), actions); syncButtons(); @@ -672,13 +692,19 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu getStructures: () => [...structures], setPipeFacilities(pipes) { pipeFacilities = pipes.map((pipe) => ({ ...pipe })); - if ( - tempPipeChainage !== null && - !pipeFacilities.some((pipe) => Math.abs(pipe.chainage_m - tempPipeChainage!) < 0.05) - ) { - // 임시 배치 관이 재계산에서 사라졌다 — 임시 상태만 정리(관은 이미 없다). - tempPipeChainage = null; - syncButtons(); + if (tempPipeChainage !== null) { + // 스냅·재계산으로 좌표가 조금 옮겨진 임시 관을 계속 따라간다(0.51m 기준). + const hit = pipeFacilities.find( + (pipe) => Math.abs(pipe.chainage_m - tempPipeChainage!) < 0.51, + ); + if (hit) { + tempPipeChainage = hit.chainage_m; + if (selectedPipeChainage !== null) selectedPipeChainage = hit.chainage_m; + } else { + // 임시 배치 관이 재계산에서 사라졌다 — 임시 상태만 정리(관은 이미 없다). + tempPipeChainage = null; + syncButtons(); + } } if ( selectedPipeChainage !== null && @@ -743,7 +769,8 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu // BOX암거는 레지스트리 기본값(본체 2.0×2.0·날개벽 있음 1m/2m/45°)을 실어 폼을 // 연다 — [추가]만 눌러도 하류가 제원을 받는다(2026-08-17 사용자 확정 유지). syncFacilityForm(typeId === "box_culvert" ? defaultOptions(type) : {}); - if (type.managed_by) handleStationInput(); + // 우클릭은 측점이 이미 확정돼 들어온다 — 임시 배치 즉시 진행. + if (type.managed_by) commitTempPipe(); syncButtons(); renderList(); root.scrollIntoView({ block: "nearest" });