- 지형 파일 개수 제한 해제(정확히 1개 → 1장 이상), 한 카드에 여러 장 담기 (화면 표시 「용화_서편.las 외 1장」, 업로드는 한 장씩 차례로 전송) - 구조화 엔진이 여러 파일을 합친 범위로 한 벌 생성 — WF1 자동 전처리·B04 재분석 모두 프로젝트 지형 파일 전부를 대상으로 실행 - 점이 1억 개를 넘으면 씨닝 — 지면 분류점은 전부 남기고 나머지만 0.5m 칸 최저점으로 축소 (용화 실측: 4,900만점 → 249만점, 지면점 1,140,716개 그대로, 1m 지면격자 표고차 0.0000m) - 머리글만 읽어 5km 넘게 떨어진 파일은 업로드 거부 (임도는 길어도 2~3km) - 화면 조립부 700줄 준수를 위해 terrainCoverage 를 판정 모듈로 이동 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
264 lines
8.9 KiB
TypeScript
264 lines
8.9 KiB
TypeScript
import { ui_locales } from "@ui/ui_template_locale";
|
|
|
|
/**
|
|
* 카드 한 장 = 파일 한 개. 계획노선 shapefile은 파일이 다섯이므로 카드도 다섯이다
|
|
* (2026-08-31 사용자 지시) — 어느 파일이 왔고 어느 것이 비었는지 화면에서 바로 보인다.
|
|
* `route_prj`(노선 좌표계)와 `prj`(지형 좌표계)는 확장자가 같아 basename으로 가른다.
|
|
*/
|
|
export type FileSlot =
|
|
"csv" | "shx" | "dbf" | "cpg" | "route_prj" | "las_laz" | "prj" | "tfw" | "tif" | "dxf";
|
|
|
|
/** 왼쪽(계획노선) 컨테이너에 놓이는 슬롯. */
|
|
export const ROUTE_SLOTS: readonly FileSlot[] = ["csv", "shx", "dbf", "cpg", "route_prj"];
|
|
|
|
/** 오른쪽(지형·LAS) 컨테이너에 놓이는 슬롯. */
|
|
export const TERRAIN_SLOTS: readonly FileSlot[] = ["las_laz", "prj", "tfw", "tif"];
|
|
|
|
/** 노선 도형이 shapefile일 때 함께 있어야 하는 슬롯(.cpg는 없으면 CP949). */
|
|
export const SHAPEFILE_DEPENDENT_SLOTS: readonly FileSlot[] = ["shx", "dbf", "route_prj"];
|
|
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;
|
|
/**
|
|
* 같은 카드에 더 담은 파일 — 지형 자료(포인트클라우드)만 여러 장을 받는다.
|
|
* 드론 라이다는 사업지가 넓으면 도엽별로 나뉘어 오고, 전처리가 합쳐서 쓴다
|
|
* (2026-09-06 사용자 확정). 업로드는 이 목록을 한 장씩 차례로 올린다.
|
|
*/
|
|
extraFiles?: 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; metadata?: Record<string, unknown> };
|
|
}
|
|
|
|
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: "⌁",
|
|
// `.csv`는 받지 않는다 — 내부 계산이 만드는 파일이라 사용자가 넣는 자료가 아니다
|
|
// (2026-09-03 사용자 지시). 슬롯 키 `csv`는 저장·서버 규약이라 그대로 둔다.
|
|
extensions: [".shp"],
|
|
isRequired: true,
|
|
},
|
|
{
|
|
slot: "shx",
|
|
labelKey: "B03_File_Slot_RouteIndex",
|
|
icon: "⋮",
|
|
extensions: [".shx"],
|
|
isRequired: false,
|
|
},
|
|
{
|
|
slot: "dbf",
|
|
labelKey: "B03_File_Slot_RouteAttribute",
|
|
icon: "▤",
|
|
extensions: [".dbf"],
|
|
isRequired: false,
|
|
},
|
|
{
|
|
slot: "cpg",
|
|
labelKey: "B03_File_Slot_RouteEncoding",
|
|
icon: "⌨",
|
|
extensions: [".cpg"],
|
|
isRequired: false,
|
|
},
|
|
{
|
|
slot: "route_prj",
|
|
labelKey: "B03_File_Slot_RouteProjection",
|
|
icon: "◈",
|
|
extensions: [".prj"],
|
|
isRequired: false,
|
|
},
|
|
{
|
|
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,
|
|
},
|
|
];
|
|
|
|
/** 카드에 적을 파일 이름 — 여러 장이면 「첫 장 외 N장」. */
|
|
export function slotFileLabel(state: FileSlotState): string {
|
|
const name = state.file?.name ?? state.serverUploaded?.name ?? "";
|
|
const extras = state.extraFiles?.length ?? 0;
|
|
return extras > 0 ? `${name} 외 ${extras}장` : name;
|
|
}
|
|
|
|
/**
|
|
* 같은 카드에 파일을 더 담는다 — 담았으면 true, 이 카드가 한 장짜리면 false.
|
|
* 지형 자료(포인트클라우드)만 여러 장을 받는다. 같은 이름은 다시 담지 않는다.
|
|
*/
|
|
export function pushExtraFile(state: FileSlotState, file: File): boolean {
|
|
if (state.slot !== "las_laz") return false;
|
|
const extras = state.extraFiles ?? [];
|
|
if (!extras.some((item) => item.name === file.name)) state.extraFiles = [...extras, file];
|
|
state.error = undefined;
|
|
return true;
|
|
}
|
|
|
|
export function getExtension(fileName: string): string {
|
|
const index = fileName.lastIndexOf(".");
|
|
return index >= 0 ? fileName.slice(index).toLowerCase() : "";
|
|
}
|
|
|
|
export function getBaseName(fileName: string): string {
|
|
const index = fileName.lastIndexOf(".");
|
|
return index >= 0 ? fileName.slice(0, index) : fileName;
|
|
}
|
|
|
|
/**
|
|
* 고른 파일을 카드(슬롯)에 배정한다.
|
|
*
|
|
* `.prj`만 확장자로 갈리지 않는다 — 노선 좌표계와 지형 좌표계가 같은 확장자다.
|
|
* **노선 도형(.shp)과 basename이 같은 것**만 노선 좌표계 카드로 보내고, 나머지는
|
|
* 지형 좌표계 카드로 보낸다. `routeStem`은 이미 골라 둔 노선 도형의 basename으로,
|
|
* 노선 PRJ를 나중에 따로 추가하는 경우를 받아 준다.
|
|
*/
|
|
export function planSlotAssignments(
|
|
files: readonly File[],
|
|
slotConfigs: readonly SlotConfig[],
|
|
routeStem?: string,
|
|
): { file: File; slot?: FileSlot }[] {
|
|
const batchStem = files
|
|
.filter((file) => getExtension(file.name) === ".shp")
|
|
.map((file) => getBaseName(file.name))[0];
|
|
const stem = batchStem ?? routeStem;
|
|
return files.map((file) => {
|
|
const extension = getExtension(file.name);
|
|
if (extension === ".prj") {
|
|
const isRoute = stem !== undefined && getBaseName(file.name) === stem;
|
|
return { file, slot: (isRoute ? "route_prj" : "prj") as FileSlot };
|
|
}
|
|
const config = slotConfigs.find(
|
|
(candidate) => candidate.slot !== "route_prj" && candidate.extensions.includes(extension),
|
|
);
|
|
return { file, slot: config?.slot };
|
|
});
|
|
}
|
|
|
|
/** 슬롯 설정 목록 — 배정 규칙이 카드 정의와 같은 것을 쓰도록 밖으로 연다. */
|
|
export function slotConfigs(): readonly SlotConfig[] {
|
|
return SLOT_CONFIGS;
|
|
}
|
|
|
|
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-select" type="button"></button>
|
|
<button class="b03-file__card-remove" type="button"></button>
|
|
</div>
|
|
<div class="b03-file__card-content">
|
|
<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__preview"></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;
|
|
}
|