Files
Aislo/B03_FileInput/B03_FileInput_UI_Support.ts
T
eomsangdonandClaude Fable 5 8a0d640e0e refactor(B04/B05): 배수유역 분석을 B04로 이관, B05는 저장분 소비만
분석이 30초 걸리는데 B05는 일반 사용자 화면이다. 관리자 확인용 B04에서
한 번 돌려 저장하고, B05는 그 결과를 읽어 관 보충과 세부유역만 처리한다
(2026-07-31 사용자 지시).

노선 원천 변경
- B05 확정 경로 -> B03 업로드 계획 노선 파일(CSV). 분석이 노선 설계보다
  먼저 끝나 있어야 하기 때문. 샘플 planned_route_sample_epsg5187.csv 로 검증.
- common_util_route_geometry.py 신설 — RouteVertex/StructureCandidate/누가거리
  보간/세류 교차점/계획 노선 CSV 리더. B04와 B05가 같은 표현을 쓰도록 공용화.
  열 이름은 대소문자·한글 표기를 함께 받는다(B03이 여러 형식 수용 예정).

B04 (관리자 확인용, 신규)
- Engine_Watershed_{Grid,Stream,Descent,Flow,Expand,Export} — B05에서 git mv
- Engine_Watershed_Analyze.py — 1~8단계 오케스트레이션
- Router_Watershed.py — GET /drainage/primary-region
- UI_Watershed.ts — 2D 지도 GIS 레이어 그룹에 "배수유역" 토글 추가.
  격자/화살표/세류망/1차영역/2차유역/기본관을 겹쳐 그린다.
- 저장 위치 B05_wf2_Route/drainage -> B04_wf1_Surface/drainage
- 03_road_routing 단계 추가: B05가 세부유역을 나눌 최소 배열(셀->도로셀 귀속,
  유하장, 강도, 도로셀 제원, 셀 표고) + 계획도로선/기본배관/2차유역 기하

B05 (일반 사용자용, 축소)
- Engine_Drainage_Basin.py — B04 산출물 로더 + 관 보충(9) + 측구 라우팅/세부유역(10,11)
- Engine_Drainage.py 는 관경 산정만 남기고 322 -> 27줄
- Router_Drainage.py 509 -> 142줄. POST /drainage/basins 만 남김
- 화살표·격자·강도 띠 렌더 제거. 계획도로선/기본배관/2차유역만 받는다

삭제
- _legacy_watershed/ 4파일 (능선 행진 방식 원본 보관본)
- Engine_Watershed_Basin.py (B04 Analyze + B05 Drainage_Basin 으로 분할)
- GET /drainage/candidates 와 propose_structure_stations (구방식 후보 제안)

E2E 검증 (실데이터)
  B04 분석 28.2s -> 저장(geojson 11KB + npz 2.6MB)
  B05 로드 + 세부 설계 0.11s   <-- 30초가 0.1초로
  면적 457,404m2 로 B04 2차 유역과 정확히 일치
  관 편집 재산정 0.12s, 관 3개 -> 세부유역 3개, 면적 보존

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 20:57:09 +09:00

147 lines
4.1 KiB
TypeScript

import { ui_locales } from "@ui/ui_template_locale";
export type FileSlot = "csv" | "las_laz" | "prj" | "tfw" | "tif" | "dxf";
export type UploadStatus = "pending" | "uploading" | "completed" | "failed";
export interface SlotConfig {
slot: FileSlot;
labelKey: keyof typeof ui_locales;
icon: string;
extensions: readonly string[];
isRequired: boolean;
}
export interface FileSlotState extends SlotConfig {
file?: File;
uploadSessionId?: string;
uploadStatus: UploadStatus;
progressBytes: number;
speedMbs: number;
etaSeconds: number | null;
error?: string;
}
export interface StoredUploadSession {
key: string;
projectId: string;
slot: FileSlot;
fileName: string;
fileSize: number;
uploadSessionId: string;
chunkSizeBytes: number;
totalChunks: number;
completedChunks: number;
updatedAt: number;
}
const SLOT_CONFIGS: readonly SlotConfig[] = [
{
slot: "csv",
labelKey: "B03_File_Slot_PlannedRoute",
icon: "⌁",
extensions: [".csv"],
isRequired: true,
},
{
slot: "las_laz",
labelKey: "B03_File_Slot_PointCloud",
icon: "●",
extensions: [".las", ".laz"],
isRequired: true,
},
{
slot: "prj",
labelKey: "B03_File_Slot_Projection",
icon: "◇",
extensions: [".prj"],
isRequired: true,
},
{
slot: "tfw",
labelKey: "B03_File_Slot_RasterCoord",
icon: "□",
extensions: [".tfw"],
isRequired: true,
},
{
slot: "tif",
labelKey: "B03_File_Slot_TerrainDem",
icon: "▧",
extensions: [".tif"],
isRequired: false,
},
];
export function getExtension(fileName: string): string {
const index = fileName.lastIndexOf(".");
return index >= 0 ? fileName.slice(index).toLowerCase() : "";
}
export function formatBytes(bytes: number): string {
const gb = bytes / 1024 / 1024 / 1024;
if (gb >= 1) return `${gb.toFixed(2)} GB`;
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
}
export function formatEta(seconds: number | null): string {
if (seconds === null || !Number.isFinite(seconds)) return "-";
if (seconds < 60) return `${Math.ceil(seconds)}s`;
return `${Math.ceil(seconds / 60)}m`;
}
export function makeSessionKey(projectId: string, file: File): string {
return `b03_upload_${projectId}_${file.name}_${file.size}`;
}
export function initializeSlots(): Map<FileSlot, FileSlotState> {
const map = new Map<FileSlot, FileSlotState>();
for (const config of SLOT_CONFIGS) {
map.set(config.slot, {
...config,
uploadStatus: "pending",
progressBytes: 0,
speedMbs: 0,
etaSeconds: null,
});
}
return map;
}
export function createFileCardTemplate(): HTMLTemplateElement {
const template = document.createElement("template");
template.id = "file-card-template";
template.innerHTML = `
<article class="b03-file__card b03-file__card--empty">
<div class="b03-file__card-header">
<span class="b03-file__card-icon" aria-hidden="true"></span>
<div class="b03-file__card-heading">
<strong class="b03-file__card-label"></strong>
<span class="b03-file__card-ext"></span>
</div>
<div class="b03-file__card-badge-container"></div>
<button class="b03-file__card-remove" type="button"></button>
</div>
<div class="b03-file__card-content">
<button class="b03-file__card-select" type="button"></button>
<input class="b03-file__slot-input" type="file" />
<div class="b03-file__file-info">
<span class="b03-file__file-name"></span>
<span class="b03-file__file-size"></span>
</div>
<div class="b03-file__progress-section">
<div class="b03-file__progress-bar-container">
<div class="b03-file__progress-bar"></div>
</div>
<div class="b03-file__progress-info">
<span class="b03-file__progress-bytes"></span>
<span class="b03-file__progress-speed"></span>
<span class="b03-file__progress-eta"></span>
</div>
</div>
<div class="b03-file__error-message" role="alert"></div>
</div>
</article>
`;
return template;
}