feat(B04/B05/공통): 7단계 명칭 통일, B05 패널 드래그 리사이즈, B04 계획선 레이어, 2D 지도 좌드래그 팬 제거
- 진행단계 오버레이 버튼과 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>
This commit is contained in:
@@ -99,12 +99,12 @@ export const ui_locales = {
|
||||
/* ---------------------------------------------------------------------------
|
||||
* 워크플로우 공통 (B04~B09 상단 진행 단계 라벨)
|
||||
* ------------------------------------------------------------------------ */
|
||||
WF_Step_Surface: ["지표면 분석", "Surface Analysis"],
|
||||
WF_Step_Route: ["경로 설계", "Route Design"],
|
||||
WF_Step_ProfileCross: ["종횡단 생성", "Profile & Cross-section"],
|
||||
WF_Step_DesignDetail: ["상세 설계", "Detailed Design"],
|
||||
WF_Step_Quantity: ["수량 산출", "Quantity Takeoff"],
|
||||
WF_Step_Estimation: ["견적 / 문서", "Estimation / Docs"],
|
||||
WF_Step_Surface: ["전처리", "Preprocess"],
|
||||
WF_Step_Route: ["종단설계", "Profile Design"],
|
||||
WF_Step_ProfileCross: ["횡단설계", "Cross Design"],
|
||||
WF_Step_DesignDetail: ["상세설계", "Detail Design"],
|
||||
WF_Step_Quantity: ["수량산출", "Quantity"],
|
||||
WF_Step_Estimation: ["설계도서", "Design Docs"],
|
||||
WF_State_Stale: ["무효화됨 (하위 단계 변경)", "Stale (invalidated by downstream changes)"],
|
||||
WF_State_Failed: ["실패", "Failed"],
|
||||
WF_State_Complete: ["완료", "Complete"],
|
||||
@@ -525,7 +525,7 @@ export const ui_locales = {
|
||||
B02_Proj_Error_Required: ["필수 항목을 입력하세요.", "Please fill in required fields."],
|
||||
|
||||
/* --- B03_FileInput 파일 입력 --- */
|
||||
B03_File_Title: ["파일 입력", "File Input"],
|
||||
B03_File_Title: ["파일입력", "File Input"],
|
||||
B03_File_Subtitle: [
|
||||
"필수 계획노선과 지형·포인트클라우드 파일을 업로드하세요.",
|
||||
"Upload the required planned route, terrain, and point cloud files.",
|
||||
@@ -609,7 +609,7 @@ export const ui_locales = {
|
||||
],
|
||||
|
||||
/* --- B04_wf1_Surface 지표면 모델 분석 --- */
|
||||
B04_Surface_Title: ["1차 · 지표면 모델 분석", "Step 1 · Surface Analysis"],
|
||||
B04_Surface_Title: ["전처리", "Preprocess"],
|
||||
B04_Surface_Field_InputId: ["입력 파일 ID", "Input File ID"],
|
||||
B04_Surface_Field_InputId_Placeholder: ["예: 1", "e.g. 1"],
|
||||
B04_Surface_Group_Filters: ["지면 필터 선택", "Ground Filters"],
|
||||
@@ -651,6 +651,11 @@ export const ui_locales = {
|
||||
B04_Surface_Map_Background: ["배경 지도", "Basemap"],
|
||||
B04_Surface_Map_GisLayer: ["국가 GIS 레이어", "National GIS Layer"],
|
||||
B04_Surface_Map_None: ["없음", "None"],
|
||||
B04_Surface_Map_PlannedRoute: ["계획선", "Planned route"],
|
||||
B04_Surface_Map_PlannedRouteEmpty: [
|
||||
"B03에서 계획노선 파일을 올리면 표시됩니다.",
|
||||
"Shown after the planned route file is uploaded in B03.",
|
||||
],
|
||||
B04_Surface_Map_Satellite: ["위성", "Satellite"],
|
||||
B04_Surface_Map_Hybrid: ["하이브리드", "Hybrid"],
|
||||
B04_Surface_Map_White: ["백지도", "White map"],
|
||||
@@ -687,7 +692,7 @@ export const ui_locales = {
|
||||
B04_Surface_Load_Failed: ["모델 목록을 불러오지 못했습니다.", "Failed to load models."],
|
||||
|
||||
/* --- B05_wf2_Route 경로 설계 --- */
|
||||
B05_Route_Title: ["2차 · 경로 설계", "Step 2 · Route Design"],
|
||||
B05_Route_Title: ["종단설계", "Profile Design"],
|
||||
B05_Route_Group_Points: ["경로 제어점", "Route Control Points"],
|
||||
B05_Route_Point_BP: ["시점 (BP)", "Begin (BP)"],
|
||||
B05_Route_Point_EP: ["종점 (EP)", "End (EP)"],
|
||||
@@ -744,7 +749,7 @@ export const ui_locales = {
|
||||
B05_Route_Field_StationLines: ["측점 가로선", "Station cross lines"],
|
||||
|
||||
/* --- B06_wf3_ProfileCross 종·횡단 생성 --- */
|
||||
B06_Profile_Title: ["3차 · 종·횡단 생성", "Step 3 · Profile & Cross-section"],
|
||||
B06_Profile_Title: ["횡단설계", "Cross Design"],
|
||||
B06_Profile_Group_Route: ["대상 경로", "Target Route"],
|
||||
B06_Profile_Field_RouteId: ["경로 ID (routes.id)", "Route ID"],
|
||||
B06_Profile_Field_Filter: ["지면 필터", "Ground filter"],
|
||||
@@ -930,7 +935,7 @@ export const ui_locales = {
|
||||
],
|
||||
|
||||
/* --- B07_wf4_DesignDetail 상세 설계 --- */
|
||||
B07_Design_Title: ["4차 · 상세 설계", "Step 4 · Detailed Design"],
|
||||
B07_Design_Title: ["상세설계", "Detail Design"],
|
||||
B07_Cad_Side_Pending: [
|
||||
"사이드 패널은 전달 데이터 확정 후 구성됩니다.",
|
||||
"Side panel will be configured after the upstream data spec is finalized.",
|
||||
@@ -955,10 +960,10 @@ export const ui_locales = {
|
||||
B07_Info_Station: ["측점", "Station"],
|
||||
|
||||
/* --- B08_wf5_Quantity 수량 산출 --- */
|
||||
B08_Quantity_Title: ["5차 · 수량 산출", "Step 5 · Quantity Takeoff"],
|
||||
B08_Quantity_Title: ["수량산출", "Quantity"],
|
||||
|
||||
/* --- B09_wf6_Estimation 견적·문서 --- */
|
||||
B09_Estimation_Title: ["6차 · 견적·문서", "Step 6 · Estimation & Documents"],
|
||||
B09_Estimation_Title: ["설계도서", "Design Docs"],
|
||||
|
||||
/* --- B10_Payment 결재 --- */
|
||||
B10_Payment_Title: ["결재", "Payment"],
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/* 공통 스플리터(드래그 리사이저) — 패널 경계에 얹는 얇은 손잡이.
|
||||
레이아웃을 밀지 않도록 경계 위에 절대배치하고, 평소에는 보이지 않다가
|
||||
마우스를 얹거나 끌 때만 색이 들어온다. */
|
||||
|
||||
.ui-resizer {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
background: transparent;
|
||||
transition: background var(--transition-fast);
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.ui-resizer:hover,
|
||||
.ui-resizer.is-dragging {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
/* 위아래로 끌어 높이를 조절 — 패널 위쪽 경계에 가로로 눕는다. */
|
||||
.ui-resizer--vertical {
|
||||
top: -3px;
|
||||
right: 0;
|
||||
left: 0;
|
||||
height: 6px;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
/* 좌우로 끌어 폭을 조절 — 패널 왼쪽 경계에 세로로 선다. */
|
||||
.ui-resizer--horizontal {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: -3px;
|
||||
width: 6px;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
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();
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user