feat(B05/B06): 입력 흐름·알약 번호·초기화 모달·전후 분할·표시 반폭 정비
- 위치 마지막 칸 Tab → 상세 옵션 첫 칸(잠금 해제보다 탭 계산이 빨라 메모로
건너뛰던 문제). 화면 순서(위→아래) 그대로 이동.
- [초기선 복원] → "저장선 복원"으로 문구 교정 — 복원 목표는 마지막 저장선이다
(임시저장 시 편집이 정본에 반영). 최초 자동 계산 복귀는 [초기화] 안내.
- 종단 알약에 좌측 목록과 같은 중복 번호("옹벽 1/2") — 라벨·툴팁·벌룬 동일 표기.
- [초기화] 무동작 수정: 원인은 playwright 연결 브라우저의 네이티브 confirm 자동
취소. 공용 showConfirmDialog 모달(ui_template_elements) 신설로 교체.
- C군 옵션: 전길이 → "기준측점 전" + "기준측점 후" 신설(기본 10/5/5). 나중에
입력한 쪽이 살아남고 반대쪽 = 길이 − 그 값. 저장은 before_m 기준(후는 파생).
- B06 횡단 반폭 표시 기본 20 → 12 (계산·보유 샘플 폭은 백엔드 20m 유지 — 표시
범위만 좁힘, 저장·세션값 있으면 그 값 우선).
- 측점 범위 표기 폰트 = 기준 측점 입력 칸과 동일(13.33px), 볼드 유지.
- 검증: pytest 112 통과, tsc, 헤드 브라우저 — Tab 포커스 '높이', 전7→후3·후2→전8
연동, 범위 2+12.0~3+2.0, 알약 '옹벽 1/2', 초기화 모달 표시, 저장 왕복·원복.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -532,7 +532,16 @@
|
||||
},
|
||||
{
|
||||
"key": "before_m",
|
||||
"label": "전길이",
|
||||
"label": "기준측점 전",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": 5,
|
||||
"required": false,
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "after_m",
|
||||
"label": "기준측점 후",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": 5,
|
||||
@@ -581,7 +590,16 @@
|
||||
},
|
||||
{
|
||||
"key": "before_m",
|
||||
"label": "전길이",
|
||||
"label": "기준측점 전",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": 5,
|
||||
"required": false,
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "after_m",
|
||||
"label": "기준측점 후",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": 5,
|
||||
@@ -630,7 +648,16 @@
|
||||
},
|
||||
{
|
||||
"key": "before_m",
|
||||
"label": "전길이",
|
||||
"label": "기준측점 전",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": 5,
|
||||
"required": false,
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "after_m",
|
||||
"label": "기준측점 후",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": 5,
|
||||
@@ -679,7 +706,16 @@
|
||||
},
|
||||
{
|
||||
"key": "before_m",
|
||||
"label": "전길이",
|
||||
"label": "기준측점 전",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": 5,
|
||||
"required": false,
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "after_m",
|
||||
"label": "기준측점 후",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": 5,
|
||||
@@ -728,7 +764,16 @@
|
||||
},
|
||||
{
|
||||
"key": "before_m",
|
||||
"label": "전길이",
|
||||
"label": "기준측점 전",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": 5,
|
||||
"required": false,
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "after_m",
|
||||
"label": "기준측점 후",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": 5,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { CURRENT_PROJECT_ID_KEY, ROUTES } from "@config/config_frontend";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { hideLoadingOverlay, showLoadingOverlay, showToast } from "@ui/ui_template_elements";
|
||||
import {
|
||||
hideLoadingOverlay,
|
||||
showConfirmDialog,
|
||||
showLoadingOverlay,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { purgeOtherProjects } from "../A00_Common/b_asset_cache";
|
||||
import { createProgressCircle } from "@ui/ui_template_progress";
|
||||
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
||||
@@ -572,9 +577,11 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/** [초기화] — 사용자 편집 전부 폐기, 계획노선 CSV 기본값으로 B05·B06 재계산 후 재진입. */
|
||||
/** [초기화] — 사용자 편집 전부 폐기, 계획노선 CSV 기본값으로 B05·B06 재계산 후 재진입.
|
||||
* 네이티브 confirm은 공용 헤드 브라우저가 자동 취소해 버튼이 죽은 듯 보였다
|
||||
* (2026-08-19 진단) — 화면 안 모달로 확인받는다. */
|
||||
async function resetDesign(): Promise<void> {
|
||||
if (!window.confirm(L("B05_Route_Reset_Confirm"))) return;
|
||||
if (!(await showConfirmDialog(L("B05_Route_Reset_Confirm"), "초기화"))) return;
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
await resetRouteDesign(activeProjectId);
|
||||
|
||||
@@ -78,8 +78,13 @@ export function renderBalanceBar(params: BalanceBarParams): void {
|
||||
const reset = document.createElement("button");
|
||||
reset.type = "button";
|
||||
reset.className = "b05-route-profile__balance-reset";
|
||||
reset.textContent = "초기선 복원";
|
||||
reset.title = "모든 편집과 추가한 비정규 측점을 지우고 자동 산출된 계획선으로 되돌립니다.";
|
||||
// 복원 목표는 **마지막 저장된 계획선**이다 — 임시저장을 거치면 편집이 정본에
|
||||
// 반영되므로 최초 자동 계산선과 다를 수 있다(2026-08-19 사용자 확인). 최초
|
||||
// 파이프라인 결과로의 완전 복귀는 좌측 하단 [초기화](B05·B06 재계산)가 맡는다.
|
||||
reset.textContent = "저장선 복원";
|
||||
reset.title =
|
||||
"저장 후의 편집과 추가한 비정규 측점을 지우고 마지막 저장된 계획선으로 되돌립니다.\n" +
|
||||
"최초 자동 계산 상태로 되돌리려면 좌측 하단 [초기화]를 쓰세요.";
|
||||
reset.addEventListener("click", () => {
|
||||
params.onResetAll();
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
type StructureInstance,
|
||||
type StructureType,
|
||||
} from "./B05_Profile_Api_Structures";
|
||||
import { numberDuplicateNames } from "./B05_Profile_UI_Structures_List";
|
||||
import { formatStation } from "./B05_Profile_Util_Station";
|
||||
|
||||
/** 마크를 잡았다고 볼 여유(px) — 시작점에서 이만큼 **벗어나기 전에는** 이동으로
|
||||
@@ -125,10 +126,26 @@ export function buildStructureLane(options: StructureMarksOptions): HTMLElement
|
||||
const typeMap = new Map(options.types.map((type) => [type.type_id, type]));
|
||||
const rows = rowAssignments(options.structures, options.x);
|
||||
|
||||
// 좌측 목록과 같은 중복 이름 번호(2026-08-19 사용자 지시) — 시점에 가까운 순
|
||||
// "이름 1/2…". 알약 라벨(약호)·툴팁·벌룬 제목이 모두 이 번호를 따른다.
|
||||
const anchorSorted = [...options.structures].sort(
|
||||
(left, right) => structureAnchorM(left) - structureAnchorM(right),
|
||||
);
|
||||
const numberedNames = numberDuplicateNames(
|
||||
anchorSorted.map((entry) => typeMap.get(entry.type_id)?.name ?? entry.type_id),
|
||||
);
|
||||
const displayNames = new Map(
|
||||
anchorSorted.map((entry, index) => [entry.structure_id ?? "", numberedNames[index]]),
|
||||
);
|
||||
|
||||
options.structures.forEach((structure) => {
|
||||
const id = structure.structure_id;
|
||||
if (!id) return;
|
||||
const type = typeMap.get(structure.type_id);
|
||||
const rawName = type?.name ?? structure.type_id;
|
||||
const displayName = displayNames.get(id) ?? rawName;
|
||||
// "옹벽 1"에서 붙은 번호만 떼어 약호 뒤에 잇는다(알약은 약호 표기 유지).
|
||||
const numberSuffix = displayName.startsWith(rawName) ? displayName.slice(rawName.length) : "";
|
||||
const anchorPx = options.x(structureAnchorM(structure));
|
||||
const selected = options.selectedId === id;
|
||||
// 이웃과 겹치는 알약만 위·아래 두 줄로 가르고, 혼자면 레인 세로 가운데에 크게 둔다.
|
||||
@@ -161,9 +178,9 @@ export function buildStructureLane(options: StructureMarksOptions): HTMLElement
|
||||
mark.style.top = `${top}px`;
|
||||
mark.style.borderColor = type?.style?.color ?? "#888";
|
||||
mark.style.color = type?.style?.color ?? "#888";
|
||||
// 라벨은 3자 이하 짧은 이름(레지스트리 style.abbr, 2026-08-17 지시 4).
|
||||
mark.textContent = type?.style?.abbr ?? "?";
|
||||
mark.title = `${type?.name ?? structure.type_id} ${positionText(structure, options.stationIntervalM)}`;
|
||||
// 라벨은 3자 이하 짧은 이름(레지스트리 style.abbr, 2026-08-17 지시 4) + 중복 번호.
|
||||
mark.textContent = `${type?.style?.abbr ?? "?"}${numberSuffix}`;
|
||||
mark.title = `${displayName} ${positionText(structure, options.stationIntervalM)}`;
|
||||
mark.dataset.structureId = id;
|
||||
|
||||
mark.addEventListener("click", (event) => {
|
||||
@@ -174,7 +191,9 @@ export function buildStructureLane(options: StructureMarksOptions): HTMLElement
|
||||
lane.append(mark);
|
||||
|
||||
if (selected)
|
||||
lane.append(buildBalloon(structure, type, anchorPx, row, options.stationIntervalM));
|
||||
lane.append(
|
||||
buildBalloon(structure, type, anchorPx, row, options.stationIntervalM, displayName),
|
||||
);
|
||||
});
|
||||
|
||||
// Y축 띠 — 그래프 축을 레인까지 이어 붙인다. sticky라 가로 스크롤에도 왼쪽에
|
||||
@@ -203,6 +222,8 @@ function buildBalloon(
|
||||
anchorPx: number,
|
||||
row: number | null,
|
||||
stationIntervalM: number,
|
||||
/** 중복 번호가 붙은 표시 이름("옹벽 1") — 좌측 목록과 같은 표기(2026-08-19). */
|
||||
displayName?: string,
|
||||
): HTMLElement {
|
||||
const balloon = document.createElement("div");
|
||||
balloon.className = "b05-structure__balloon";
|
||||
@@ -212,7 +233,7 @@ function buildBalloon(
|
||||
balloon.style.borderColor = type?.style?.color ?? "#888";
|
||||
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = type?.name ?? structure.type_id;
|
||||
title.textContent = displayName ?? type?.name ?? structure.type_id;
|
||||
const position = document.createElement("span");
|
||||
position.textContent = positionText(structure, stationIntervalM);
|
||||
balloon.append(title, position);
|
||||
|
||||
@@ -186,8 +186,8 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
return Number.isFinite(length) && length > 0 ? length : null;
|
||||
}
|
||||
|
||||
/** 전길이 옵션(before_m, m) — 기준 측점에서 시점 쪽으로 물러나는 몫. 비우면 0
|
||||
* (= 기준이 곧 시작). 총길이를 넘길 수 없다 — 넘치면 총길이로 자른다(후길이 0). */
|
||||
/** 기준측점 전(before_m, m) — 기준 측점에서 시점 쪽으로 물러나는 몫. 비우면 0
|
||||
* (= 기준이 곧 시작). 총길이를 넘길 수 없다 — 넘치면 총길이로 자른다(후 0). */
|
||||
function readBeforeM(): number {
|
||||
const entry = optionInputs.find((input) => input.key === "before_m");
|
||||
if (!entry || entry.isEmpty()) return 0;
|
||||
@@ -197,6 +197,31 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
return length !== null && before > length ? length : before;
|
||||
}
|
||||
|
||||
/** 기준측점 전·후 중 나중에 고친 쪽 — 길이가 바뀔 때 이쪽 값을 지키며 재배분한다. */
|
||||
let lastSplitEdit: "before_m" | "after_m" = "before_m";
|
||||
|
||||
/** 길이·전·후 연동(2026-08-19 사용자 지시): 전 + 후 = 길이. 나중에 입력된 쪽이
|
||||
* 살아남고 반대쪽이 길이 − 그 값으로 맞춰진다. `normalizeSource`는 입력 확정
|
||||
* (change) 때만 true — 타이핑(input) 중에 그 칸을 다시 쓰면 입력이 끊긴다. */
|
||||
function syncSplitInputs(
|
||||
source: "length" | "before_m" | "after_m",
|
||||
normalizeSource: boolean,
|
||||
): void {
|
||||
const before = optionInputs.find((entry) => entry.key === "before_m");
|
||||
const after = optionInputs.find((entry) => entry.key === "after_m");
|
||||
if (before && after) {
|
||||
if (source !== "length") lastSplitEdit = source;
|
||||
const length = readLengthM() ?? 0;
|
||||
const keepEntry = source === "after_m" ? after : source === "before_m" ? before : null;
|
||||
const keep = keepEntry ?? (lastSplitEdit === "after_m" ? after : before);
|
||||
const raw = keep.isEmpty() ? 0 : Number(keep.read());
|
||||
const kept = Math.min(Math.max(Number.isFinite(raw) ? raw : 0, 0), length);
|
||||
if (normalizeSource) keep.input.value = kept.toFixed(1);
|
||||
(keep === before ? after : before).input.value = Math.max(length - kept, 0).toFixed(1);
|
||||
}
|
||||
syncRangeDisplay();
|
||||
}
|
||||
|
||||
/** 측점 범위 표시 갱신 — 시작 = 기준 − 전길이, 종료 = 시작 + 길이(2026-08-19
|
||||
* 사용자 지시: 전/후 분할, 후길이 = 길이 − 전길이 자동). 계산 불가면 "—". */
|
||||
function syncRangeDisplay(): void {
|
||||
@@ -411,14 +436,18 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
read: () => (option.input === "number" ? Number(input.value) || 0 : input.value),
|
||||
isEmpty: () => input.value.trim() === "",
|
||||
});
|
||||
// 길이·전길이가 바뀌면 측점 범위 표시가 즉시 따라온다(범위 계산 타입, 2026-08-19).
|
||||
if (option.key === "length_m" || option.key === "before_m") {
|
||||
const refresh = (): void => {
|
||||
// 길이·기준측점 전/후가 바뀌면 서로 연동하고 측점 범위 표시가 즉시 따라온다
|
||||
// (범위 계산 타입, 2026-08-19). 전·후는 **나중에 고친 쪽**이 살아남고 반대쪽이
|
||||
// 길이 − 그 값으로 맞춰진다. 길이를 고치면 나중에 고쳤던 쪽을 지키며 재배분.
|
||||
if (option.key === "length_m" || option.key === "before_m" || option.key === "after_m") {
|
||||
const source = (option.key === "length_m" ? "length" : option.key) as
|
||||
"length" | "before_m" | "after_m";
|
||||
const refresh = (normalize: boolean): void => {
|
||||
input.classList.remove("is-invalid");
|
||||
syncRangeDisplay();
|
||||
syncSplitInputs(source, normalize);
|
||||
};
|
||||
input.addEventListener("input", refresh);
|
||||
input.addEventListener("change", refresh);
|
||||
input.addEventListener("input", () => refresh(false));
|
||||
input.addEventListener("change", () => refresh(true));
|
||||
}
|
||||
});
|
||||
syncOptionLock();
|
||||
@@ -536,6 +565,11 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
const before = Math.max(structureAnchorM(target) - target.start_m, 0);
|
||||
beforeEntry.input.value = before.toFixed(1);
|
||||
}
|
||||
const afterEntry = optionInputs.find((entry) => entry.key === "after_m");
|
||||
if (afterEntry && target.options.after_m === undefined) {
|
||||
const after = Math.max(target.end_m - structureAnchorM(target), 0);
|
||||
afterEntry.input.value = after.toFixed(1);
|
||||
}
|
||||
}
|
||||
syncFacilityForm(target.options);
|
||||
} else {
|
||||
@@ -796,12 +830,41 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
commitPosition();
|
||||
});
|
||||
});
|
||||
/** 지금 보이는 위치 입력 칸들(화면 순서). Tab 흐름 판정에 쓴다. */
|
||||
function visiblePositionInputs(): HTMLInputElement[] {
|
||||
return [startFields, anchorFields, endFields]
|
||||
.filter((fields) => !fields.wrap.hidden)
|
||||
.flatMap((fields) => [fields.station, fields.remainder]);
|
||||
}
|
||||
|
||||
/** 상세 옵션의 첫 입력 칸으로 포커스 — 없으면 메모로. */
|
||||
function focusFirstDetailInput(): void {
|
||||
const target =
|
||||
optionInputs.find((entry) => !entry.input.disabled && !optionRow.hidden)?.input ??
|
||||
facilityOptions.root.querySelector<HTMLElement>(
|
||||
"input:not(:disabled), select:not(:disabled)",
|
||||
) ??
|
||||
memoField;
|
||||
target.focus();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
body.insertBefore(field("메모", memoField), actions);
|
||||
|
||||
@@ -223,10 +223,13 @@
|
||||
}
|
||||
|
||||
/* 측점 범위(계산 표시) — C군 등 길이 옵션 기반 구간 타입. 입력 칸이 아니라 기준
|
||||
* 측점 + 길이로 계산된 "시작 ~ 종료"를 보여만 준다(2026-08-19 사용자 지시). */
|
||||
* 측점 + 길이로 계산된 "시작 ~ 종료"를 보여만 준다(2026-08-19 사용자 지시).
|
||||
* 글자 크기는 기준 측점 입력 칸과 동일(크롬 input 기본 13.333px — 패널 입력에
|
||||
* 별도 font-size가 없어 이 값으로 그려진다), 굵기만 유지(2026-08-19 지시 7). */
|
||||
.b05-structure__position-row > .b05-route__field > .b05-structure__range-value {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 13.3333px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +89,10 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
});
|
||||
crossHalfWidthField.input.min = "0.1";
|
||||
crossHalfWidthField.input.step = "0.1";
|
||||
/** 횡단면도 **표시** 반폭 기본값(m) — 2026-08-19 사용자 지시로 20 → 12. 계산·보유
|
||||
* 샘플 폭은 백엔드 기본(SECTION_CROSS_HALF_WIDTH_M = 20m) 그대로라 12는 표시
|
||||
* 범위만 좁힌다(12 ≤ 20이므로 재계산 없음). 계산 범위와 혼동 금지. */
|
||||
const DISPLAY_HALF_WIDTH_DEFAULT_M = 12;
|
||||
|
||||
// 하단 액션 [종단 이동][임시저장][확정] — 종단 이동은 저장 없이 B05로 페이지 이동만,
|
||||
// 확정은 임시저장과 같은 내용 저장 후 stage 2·3을 닫고 B07 수량으로 넘어간다
|
||||
@@ -664,7 +668,9 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
crossHalfWidthField.input.value = String(context.defaults.cross_half_width_m);
|
||||
// 표시 기본 12m — 계산 기본(context.defaults.cross_half_width_m = 20m)과 다르다
|
||||
// (2026-08-19 사용자 지시). 사용자 확정 이력·세션값이 있으면 아래에서 덮는다.
|
||||
crossHalfWidthField.input.value = String(DISPLAY_HALF_WIDTH_DEFAULT_M);
|
||||
stationInterval = context.defaults.station_interval_m;
|
||||
rockBoundaryDefault = context.rock_boundary_default_offset_m;
|
||||
rockBoundaryStep = context.rock_boundary_step_m;
|
||||
@@ -708,10 +714,11 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
// 확정 이력의 표준 횡단면 설정값 복원(진행 중 세션 편집값이 있으면 패널이 무시).
|
||||
if (storedOptions?.standard_cross_section)
|
||||
standardPanel?.applyStored(storedOptions.standard_cross_section);
|
||||
// 입력칸의 기준값은 **백엔드 기본 반폭**(config SECTION_CROSS_HALF_WIDTH_M = 20m, 위에서
|
||||
// 이미 넣었다)이고, 사용자가 값을 지정해 확정한 이력(data.options)이 있을 때만 그 값으로
|
||||
// 바꾼다(2026-08-08 사용자 지시). 예전에는 options가 없으면 보유 샘플의 최대 offset을 계산해
|
||||
// 덮었는데, 옛 기본값(15m)으로 만들어진 노선이 15.0으로 보여 기준값 구실을 못 했다.
|
||||
// 입력칸의 기준값은 **표시 기본 12m**(2026-08-19 사용자 지시, 위에서 이미 넣었다 —
|
||||
// 계산·보유 샘플 폭은 백엔드 기본 20m 유지)이고, 사용자가 값을 지정해 확정한 이력
|
||||
// (data.options)이 있을 때만 그 값으로 바꾼다(2026-08-08 사용자 지시). 예전에는
|
||||
// options가 없으면 보유 샘플의 최대 offset을 계산해 덮었는데, 옛 기본값(15m)으로
|
||||
// 만들어진 노선이 15.0으로 보여 기준값 구실을 못 했다.
|
||||
// 표시값이 보유 샘플 폭보다 크면 [전체 측점 반영]이 알아서 재생성한다(applyPanelToAll).
|
||||
const storedHalfWidth = storedOptions?.cross_half_width_m ?? 0;
|
||||
if (storedHalfWidth > 0) crossHalfWidthField.input.value = storedHalfWidth.toFixed(1);
|
||||
|
||||
@@ -290,6 +290,35 @@ export function showToast(message: string, kind: ToastKind = "info", durationMs
|
||||
}, durationMs);
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 6-1. 확인 대화상자 (Confirm) — window.confirm 대체
|
||||
*
|
||||
* 공용 헤드 브라우저(playwright 연결)는 네이티브 confirm/alert를 자동 취소해
|
||||
* 버려 [초기화] 같은 버튼이 죽은 것처럼 보였다(2026-08-19 진단). 화면 안 모달로
|
||||
* 확인을 받아 어느 실행 환경에서도 같은 동작을 보장한다.
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
/** 확인/취소 모달 — 확인이면 true. 네이티브 confirm 대신 반드시 이것을 쓴다. */
|
||||
export function showConfirmDialog(message: string, confirmLabel = "확인"): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const overlay = el("div", { className: "ui-confirm" });
|
||||
const panel = el("div", { className: "ui-confirm__panel" });
|
||||
const text = el("p", { className: "ui-confirm__message", text: message });
|
||||
const actions = el("div", { className: "ui-confirm__actions" });
|
||||
const done = (result: boolean): void => {
|
||||
overlay.remove();
|
||||
resolve(result);
|
||||
};
|
||||
actions.append(
|
||||
createButton({ label: "취소", variant: "ghost", onClick: () => done(false) }),
|
||||
createButton({ label: confirmLabel, variant: "danger", onClick: () => done(true) }),
|
||||
);
|
||||
panel.append(text, actions);
|
||||
overlay.append(panel);
|
||||
document.body.append(overlay);
|
||||
});
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 7. 워크플로우 3단 레이아웃 셸 (frontend.md §2)
|
||||
* 상단 헤더 + 좌측 고정 패널 + 우측 뷰어 영역.
|
||||
@@ -706,6 +735,36 @@ const BASE_CSS = `
|
||||
.ui-toast--warning { background-color: var(--color-warning); }
|
||||
.ui-toast--info { background-color: var(--color-royal-amethyst); }
|
||||
|
||||
/* --- Confirm 모달 (window.confirm 대체 — 공용 브라우저 호환) --- */
|
||||
.ui-confirm {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(38, 17, 74, 0.35);
|
||||
z-index: var(--z-overlay);
|
||||
}
|
||||
.ui-confirm__panel {
|
||||
min-width: 300px;
|
||||
max-width: 420px;
|
||||
padding: var(--spacing-24);
|
||||
border-radius: var(--radius-cards);
|
||||
background-color: var(--color-surface-raised);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
.ui-confirm__message {
|
||||
margin: 0 0 var(--spacing-16);
|
||||
color: var(--color-text-body);
|
||||
font-size: var(--text-body-sm);
|
||||
white-space: pre-line;
|
||||
}
|
||||
.ui-confirm__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
/* --- Workflow Shell (3단 레이아웃) --- */
|
||||
.ui-wf { display: flex; flex-direction: column; height: 100%; }
|
||||
.ui-wf__header {
|
||||
|
||||
Reference in New Issue
Block a user