Files
Aislo/B03_FileInput/B03_FileInput_UI_Support.ts
T
eomsangdonandClaude Opus 5 e2f6a101b6 feat(B03): 재접속 업로드 현황(10) + B05·B06 자동 설계 체인(9)
10. 재접속 현황·완료 표시·재업로드 경고:
- GET /projects/{id}/upload-overview 신설 — 완료 파일 목록(input_files 정본),
  중단 청크 세션(진행률), 필수 파일·WF1 분석 완료 여부
- 진입 시 서버 정본으로 슬롯 카드 표시(serverUploaded), localStorage는 보조로 강등
- 파일 재선택 전에도 중단 세션 이어올리기 안내 배너
- 전체 완료 배지 + 완료 슬롯 재업로드 시 교체 확인 모달(승인 시에만 진행)
- 필수 슬롯 검증: 서버 업로드분 있으면 충족 — 단일 파일 교체 업로드 허용

9. 자동 설계 체인 연장 (B03_FileInput_Service_Chain.py):
- WF1 자동 확정 후 같은 백그라운드 태스크에서 ① 계획노선 CSV 기반 B05 기본 경로
  계산(solve) ② 경로 확정(stage 2) ③ B06 기본 횡단 설계 확정(stage 3)까지 진행
- 수동 이력 보호: 프로젝트에 경로가 이미 있으면 건너뜀
- 단계별 실패 격리: 실패 단계에서 멈추고 로그·workflow 상태로만 기록
- AUTO_DESIGN_CHAIN_ENABLED config 플래그(기본 True)

typecheck·ruff·B03 unittest(7건) 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 18:09:15 +09:00

153 lines
4.4 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;
/**
* 서버(DB `input_files`)에 이미 업로드 완료된 파일 정보 — 재접속 현황의 정본.
* 로컬 파일을 새로 고르지 않아도 카드에 완료 상태로 표시하고, 이 슬롯에 새 파일을
* 올리면 교체 확인 모달을 띄우는 근거가 된다(2026-08-04 사용자 지시).
*/
serverUploaded?: { name: string; sizeMb: number };
}
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;
}