- 진행단계 오버레이 버튼과 B그룹 페이지 제목을 파일입력/전처리/종단설계/횡단설계/
상세설계/수량산출/설계도서로 통일(ko/en). i18n 키는 유지하고 값만 교체.
B05 페이지 제목의 하드코딩 문구를 locale 참조로 교체.
- ui_template_resizer 공통 스플리터 신설. B05 하단 종단 패널은 위쪽 경계를 끌어
높이 조절(늘어난 만큼 그래프만 신축, 도면 테이블 높이는 고정), 안쪽 우측
배수유역 패널은 왼쪽 경계를 끌어 폭 조절(상한 = 하단 패널 폭의 70%).
크기는 sessionStorage에만 저장하고 CSS 변수로 적용해 접기 규칙과 충돌하지 않게 함.
- B04 2D 지도에 계획선 레이어 추가. 계획노선 좌표를 주는
GET /api/projects/{id}/planned-route 신설(B03 업로드 CSV → 사업지 좌표계 m).
선 색·굵기는 MapRender에 상수로 두고 B05 배수유역도와 공유.
- B04 지도 오버레이 재배치: 객체 표시 라벨은 우측 최하단, 배수유역 정보는 좌측 최상단.
- B04 지도·B05 배수유역도에서 좌버튼 드래그 팬 제거(가운데 버튼 전용).
유역선 핸들·배관 마커 편집은 좌버튼 그대로 유지, 팬 중에만 grabbing 커서.
- B05 배수유역도에 위성사진 표시 토글 추가(등고선 앞, 기본 on).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
122 lines
4.8 KiB
TypeScript
122 lines
4.8 KiB
TypeScript
import "./ui_template_resizer.css";
|
|
|
|
/* =============================================================================
|
|
* ui_template_resizer.ts
|
|
* 공통 스플리터 — 패널 경계를 끌어 크기를 조절한다.
|
|
*
|
|
* 조절값은 `sessionStorage`에만 남긴다. 같은 브라우저 세션에서는 페이지를 오가도
|
|
* 유지되고, 브라우저를 다시 열면 기본값으로 돌아간다(2026-08-01 사용자 지시).
|
|
* 사용자별 영구 저장은 나중에 따로 검토한다.
|
|
* ========================================================================== */
|
|
|
|
export interface PanelResizerOptions {
|
|
/** vertical=위아래로 끌어 높이 조절, horizontal=좌우로 끌어 폭 조절 */
|
|
axis: "vertical" | "horizontal";
|
|
/** 크기를 적용할 패널 */
|
|
target: HTMLElement;
|
|
/**
|
|
* 크기를 담을 CSS 변수 이름(예: `--b05-profile-height`).
|
|
* 인라인 width/height로 박으면 `.is-collapsed { height: 0 }` 같은 접기 규칙을 이겨 버려
|
|
* 패널이 접히지 않는다. 변수로 넘기면 접기 규칙이 그대로 이긴다.
|
|
*/
|
|
cssVar: string;
|
|
/** 손잡이가 패널의 위/왼쪽에 있으면 -1 — 끌어올리거나 왼쪽으로 끌면 커진다. */
|
|
direction: 1 | -1;
|
|
/** 최소 크기(px) */
|
|
min: number;
|
|
/** 최대 크기(px). 함수면 끌 때마다 다시 계산한다(부모의 70% 같은 상대 상한). */
|
|
max: number | (() => number);
|
|
/** 세션 저장 키. 생략하면 저장하지 않는다. */
|
|
storageKey?: string;
|
|
/** 크기가 바뀔 때마다 호출한다(차트 재계산 등). */
|
|
onResize?: (size: number) => void;
|
|
}
|
|
|
|
export interface PanelResizer {
|
|
/** 패널 경계에 붙일 손잡이. 패널이 `position: relative`여야 제자리에 앉는다. */
|
|
root: HTMLElement;
|
|
/** 저장된 크기를 다시 적용한다(재렌더 후 복원용). */
|
|
restore: () => void;
|
|
/** 기본 크기로 되돌린다(저장값도 지운다). */
|
|
reset: () => void;
|
|
dispose: () => void;
|
|
}
|
|
|
|
export function createPanelResizer(options: PanelResizerOptions): PanelResizer {
|
|
const { axis, target, cssVar, direction, min, storageKey, onResize } = options;
|
|
const root = document.createElement("div");
|
|
root.className = `ui-resizer ui-resizer--${axis}`;
|
|
root.setAttribute("role", "separator");
|
|
root.setAttribute("aria-orientation", axis === "vertical" ? "horizontal" : "vertical");
|
|
|
|
const maxOf = (): number => (typeof options.max === "function" ? options.max() : options.max);
|
|
|
|
const clamp = (size: number): number => {
|
|
const max = Math.max(min, maxOf());
|
|
return Math.min(max, Math.max(min, size));
|
|
};
|
|
|
|
function apply(size: number, persist: boolean): void {
|
|
const next = clamp(size);
|
|
target.style.setProperty(cssVar, `${Math.round(next)}px`);
|
|
if (persist && storageKey) sessionStorage.setItem(storageKey, String(Math.round(next)));
|
|
onResize?.(next);
|
|
}
|
|
|
|
function restore(): void {
|
|
if (!storageKey) return;
|
|
const saved = Number(sessionStorage.getItem(storageKey));
|
|
// 저장한 뒤 창 크기가 바뀌었을 수 있으니 복원할 때도 상·하한을 다시 씌운다.
|
|
if (Number.isFinite(saved) && saved > 0) apply(saved, false);
|
|
}
|
|
|
|
function reset(): void {
|
|
target.style.removeProperty(cssVar);
|
|
if (storageKey) sessionStorage.removeItem(storageKey);
|
|
onResize?.(axis === "vertical" ? target.clientHeight : target.clientWidth);
|
|
}
|
|
|
|
let startPosition = 0;
|
|
let startSize = 0;
|
|
|
|
root.addEventListener("pointerdown", (event) => {
|
|
// 왼쪽 버튼만 — 가운데·오른쪽 버튼은 브라우저 기본 동작에 맡긴다.
|
|
if (event.button !== 0) return;
|
|
event.preventDefault();
|
|
startPosition = axis === "vertical" ? event.clientY : event.clientX;
|
|
startSize = axis === "vertical" ? target.clientHeight : target.clientWidth;
|
|
root.classList.add("is-dragging");
|
|
// 끄는 동안 크기 transition이 걸려 있으면 손이 끄는 만큼 늦게 따라온다.
|
|
target.classList.add("is-resizing");
|
|
root.setPointerCapture(event.pointerId);
|
|
});
|
|
|
|
root.addEventListener("pointermove", (event) => {
|
|
if (!root.hasPointerCapture(event.pointerId)) return;
|
|
const current = axis === "vertical" ? event.clientY : event.clientX;
|
|
apply(startSize + (current - startPosition) * direction, true);
|
|
});
|
|
|
|
const stop = (event: PointerEvent): void => {
|
|
if (!root.hasPointerCapture(event.pointerId)) return;
|
|
root.releasePointerCapture(event.pointerId);
|
|
root.classList.remove("is-dragging");
|
|
target.classList.remove("is-resizing");
|
|
};
|
|
root.addEventListener("pointerup", stop);
|
|
root.addEventListener("pointercancel", stop);
|
|
// 더블클릭하면 기본 크기로 되돌린다(잘못 끌었을 때 되돌리는 가장 빠른 길).
|
|
root.addEventListener("dblclick", reset);
|
|
|
|
restore();
|
|
|
|
return {
|
|
root,
|
|
restore,
|
|
reset,
|
|
dispose() {
|
|
root.remove();
|
|
},
|
|
};
|
|
}
|