B05_Profile_UI_Structures_Panel.ts 1024 -> 669줄. 조각 셋 모두 700줄 이하. - _Structures_Panel_Types.ts(94줄) — GROUP_LABELS · PipeFacilityItem · StructuresSection · StructuresCallbacks · 기본 구간 길이 상수. 패널이 다시 export 해 옛 임포트 경로 유지. - _Structures_Panel_Commit.ts(312줄) — 폼 불러오기(구조물·계곡 통과 시설)와 [추가]/[수정] 저장, 목록 알림. 검증 순서·분기·수치는 그대로이고 패널 클로저 값만 ctx 로 받음. - _Structures_Panel_Events.ts(146줄) — 드롭다운·버튼·측점 입력 배선. 리스너 본문 그대로. StructuresEventContext 가 StructuresCommitContext 를 상속해 패널은 컨텍스트 하나만 만듦. 가변 상태 4개(editingId · selectedPipeChainage · tempPipeChainage · positionConfirmed)의 주인은 그대로 패널이고, 읽기 함수 + set* 로만 넘김. 검증: 분리 전 export 4개가 진입 파일에서 그대로 보임, tsc --noEmit 통과, prettier 적용, tmp/tests 183 passed. 공용 브라우저(5174)에서 실제 조작으로 저장 경로까지 확인 — C 사면안정/옹벽을 기준측점 5+0 에 [추가] -> 목록 반영, [저장] -> 서버 정본 2건(revision 0->1), 새로고침 후 목록에서 골라 [삭제] -> [저장] -> 서버 0건(revision 4). 시험 프로젝트는 원래 상태(구조물 0건)로 되돌려 둠. 참고(분리와 무관한 기존 동작): 방금 [추가]해 아직 저장 전인 항목은 structure_id 가 없어 [삭제]가 듣지 않음. 저장 뒤에는 정상 삭제됨. 분리 전 코드와 같은 분기라 이번 변경과 무관. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
147 lines
7.1 KiB
TypeScript
147 lines
7.1 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_UI_Structures_Panel_Events.ts
|
|
* 구조물 배치 패널의 **입력 배선** — 구조물군·종류 드롭다운, [추가]/[삭제]/[리셋],
|
|
* 측점 입력 확정(change·focusout·Enter·Tab), 메모 변경.
|
|
*
|
|
* `B05_Profile_UI_Structures_Panel` 이 700줄을 넘겨 떼어낸 조각이다(2026-09-04).
|
|
* 리스너 본문·순서는 옮기기 전 그대로이고, 패널이 쥔 값만 `ctx` 로 받는다.
|
|
* 처리 함수(commit·loadForm 등)는 그대로 패널 소유다 — 여기서는 이어 붙이기만 한다.
|
|
* ========================================================================== */
|
|
|
|
import type { StructureInstance, StructureType } from "./B05_Profile_Api_Structures";
|
|
import type { StructuresCommitContext } from "./B05_Profile_UI_Structures_Panel_Commit";
|
|
|
|
/** 배선에 필요한 패널 내부 값 — 이름은 분리 전 지역변수와 같다. */
|
|
/** 배선에 필요한 값 — 저장 쪽(`StructuresCommitContext`)과 겹치는 것은 그대로 물려받는다. */
|
|
export interface StructuresEventContext extends StructuresCommitContext {
|
|
typeSelect: HTMLSelectElement;
|
|
primary: HTMLButtonElement;
|
|
removeButton: HTMLButtonElement;
|
|
resetButton: HTMLButtonElement;
|
|
positionRow: HTMLElement;
|
|
types: () => StructureType[];
|
|
commit: (live?: boolean) => Promise<void>;
|
|
resetForm: () => void;
|
|
handleStationInput: () => void;
|
|
commitPosition: () => void;
|
|
liveCommit: () => void;
|
|
loadForm: (target: StructureInstance | null) => void;
|
|
emit: () => void;
|
|
optionRow: HTMLElement;
|
|
}
|
|
|
|
export function bindStructuresEvents(ctx: StructuresEventContext): void {
|
|
ctx.groupSelect.addEventListener("change", () => {
|
|
ctx.cancelTempPipe(); // 종류가 달라진다 — 임시 배치는 취소(2026-08-18).
|
|
ctx.syncTypeOptions();
|
|
// 종류가 바뀌면 기존 항목 수정이 아니라 새 항목 추가로 넘어간다(옵션 스키마가 달라진다).
|
|
if (ctx.editingId()) {
|
|
ctx.setEditingId(null);
|
|
ctx.syncButtons();
|
|
ctx.renderList();
|
|
ctx.callbacks.onSelect(null);
|
|
}
|
|
});
|
|
ctx.typeSelect.addEventListener("change", () => {
|
|
ctx.cancelTempPipe(); // 종류가 달라진다 — 임시 배치는 취소(2026-08-18).
|
|
ctx.syncPlacementFields();
|
|
ctx.renderOptionFields();
|
|
// 배수관·BOX암거·세월교·독립 기슭막이는 부속 옵션 서브폼이 따라 열려야 한다 —
|
|
// 수동 추가든 자동 배치 지점 편집이든 같은 옵션을 받는다(2026-08-17 사용자 지시:
|
|
// 자동은 통수단면으로 자리를 잡아 준 것일 뿐 같은 구조물이다).
|
|
ctx.syncFacilityForm();
|
|
if (ctx.editingId()) {
|
|
ctx.setEditingId(null);
|
|
ctx.syncButtons();
|
|
ctx.renderList();
|
|
ctx.callbacks.onSelect(null);
|
|
}
|
|
});
|
|
ctx.primary.addEventListener("click", () => void ctx.commit());
|
|
ctx.removeButton.addEventListener("click", () => {
|
|
// 임시 배치 중이면 취소와 같다 — 관을 물리고 추가 직전 상태로(2026-08-18).
|
|
if (ctx.tempPipeChainage() !== null) {
|
|
ctx.resetForm();
|
|
return;
|
|
}
|
|
// 계곡 통과 시설이 골라져 있으면 관 지점 정본에서 지운다(배수유역 패널 경유).
|
|
if (ctx.selectedPipeChainage() !== null) {
|
|
ctx.callbacks.onPipeRemove(ctx.selectedPipeChainage() as number);
|
|
ctx.setSelectedPipeChainage(null);
|
|
ctx.syncButtons();
|
|
ctx.renderList();
|
|
return;
|
|
}
|
|
if (!ctx.editingId()) return;
|
|
const index = ctx.structures.findIndex((entry) => entry.structure_id === ctx.editingId());
|
|
if (index >= 0) ctx.structures.splice(index, 1);
|
|
ctx.loadForm(null);
|
|
ctx.emit();
|
|
});
|
|
// 리셋은 폼만 아니라 구조물군·종류도 빈칸으로 되돌린다(2026-08-17 사용자 지시).
|
|
// 임시 배치 취소도 겸한다(2026-08-18).
|
|
ctx.resetButton.addEventListener("click", ctx.resetForm);
|
|
// 측점 입력 감시 — 옵션 잠금 해제 판정(2026-08-18).
|
|
[ctx.anchorFields, ctx.startFields, ctx.endFields].forEach((fields) =>
|
|
[fields.station, fields.remainder].forEach((input) =>
|
|
input.addEventListener("change", ctx.handleStationInput),
|
|
),
|
|
);
|
|
// 측점번호+잔여거리가 **둘 다 확정된 순간** 임시 배치·옵션 활성화가 바로 나간다
|
|
// (2026-08-18 사용자 지시 — 입력군을 떠나기 전이라도). 잔여거리 값이 확정되면
|
|
// 즉시, 측점번호 수정은 잔여거리가 이미 있을 때만 즉시.
|
|
ctx.anchorFields.remainder.addEventListener("change", () => ctx.commitPosition());
|
|
ctx.anchorFields.station.addEventListener("change", () => {
|
|
if (ctx.anchorFields.remainder.value.trim() !== "") ctx.commitPosition();
|
|
});
|
|
// 그 외(잔여거리를 안 쓰는 정측점 입력 등)는 입력군에서 포커스가 완전히 빠져나간
|
|
// 뒤에 확정한다 — 측점번호만 넣고 잔여거리 칸으로 옮기는 중간에는 나가지 않는다.
|
|
// Enter로도 바로 나갈 수 있다.
|
|
ctx.positionRow.addEventListener("focusout", () => {
|
|
// 다음 포커스 대상이 확정된 다음 프레임에 판단한다(relatedTarget은 브라우저에
|
|
// 따라 비어 온다).
|
|
window.requestAnimationFrame(() => {
|
|
if (ctx.positionRow.contains(document.activeElement)) return;
|
|
ctx.commitPosition();
|
|
});
|
|
});
|
|
/** 지금 보이는 위치 입력 칸들(화면 순서). Tab 흐름 판정에 쓴다. */
|
|
function visiblePositionInputs(): HTMLInputElement[] {
|
|
return [ctx.startFields, ctx.anchorFields, ctx.endFields]
|
|
.filter((fields) => !fields.wrap.hidden)
|
|
.flatMap((fields) => [fields.station, fields.remainder]);
|
|
}
|
|
|
|
/** 상세 옵션의 첫 입력 칸으로 포커스 — 없으면 메모로. */
|
|
function focusFirstDetailInput(): void {
|
|
const target =
|
|
ctx.optionInputs.find((entry) => !entry.input.disabled && !ctx.optionRow.hidden)?.input ??
|
|
ctx.facilityOptions.root.querySelector<HTMLElement>(
|
|
"input:not(:disabled), select:not(:disabled)",
|
|
) ??
|
|
ctx.memoField;
|
|
target.focus();
|
|
}
|
|
|
|
ctx.positionRow.addEventListener("keydown", (event) => {
|
|
// Enter = 입력 확정 — 포커스를 빼서 focusout 경로 하나로 처리한다(측점번호+
|
|
// 잔여거리 합산값이 그대로 임시 배치 좌표가 된다).
|
|
if (event.key === "Enter" && document.activeElement instanceof HTMLElement) {
|
|
document.activeElement.blur();
|
|
}
|
|
// Tab = 화면 순서(위→아래)대로 다음 입력인 상세 옵션 첫 칸으로. 위치 확정으로
|
|
// 옵션 잠금이 풀리는 시점이 브라우저 기본 탭 계산보다 늦어, disabled 상태를 본
|
|
// 탭이 메모까지 건너뛰던 문제(2026-08-19 사용자 보고).
|
|
if (event.key === "Tab" && !event.shiftKey) {
|
|
const inputs = visiblePositionInputs();
|
|
if (document.activeElement === inputs[inputs.length - 1]) {
|
|
event.preventDefault();
|
|
(document.activeElement as HTMLElement).blur(); // change → 위치 확정 → 잠금 해제
|
|
window.requestAnimationFrame(focusFirstDetailInput);
|
|
}
|
|
}
|
|
});
|
|
|
|
ctx.memoField.addEventListener("change", () => ctx.liveCommit());
|
|
}
|