feat(B03): 계획노선 shapefile 입력과 PRJ 2개 분리를 지원한다
원청 정식 계획노선이 shapefile(UTM-K)로, 지형이 별도 PRJ(동부원점 Bessel)로 들어오는데 입력 경로가 shapefile 확장자를 막고 PRJ를 프로젝트당 1개로 전제했다. - 업로드 허용에 .shp/.shx/.dbf/.cpg 추가, 한 번에 보낼 파일 수 5 -> 10 - B03_FileInput_Engine_Shapefile: ESRI 규격 직접 파싱(GDAL 미사용). 형제 파일이 아직 안 왔어도 .shp 하나로 기하를 읽는다. .cpg 내용이 949뿐인 실물을 CP949로 정규화해 한글 속성을 살린다. - 노선 판독을 read_planned_route로 일원화(CSV/shapefile), PlannedRoute에 crs_input 추가 - 변환 입력은 EPSG 코드가 아니라 crs_input_from_prj가 주는 값(EPSG:n 또는 원문 WKT)이다. 실물 PRJ 2종 모두 to_epsg가 None이다. - shapefile 세트를 input/shp/ 한 폴더에 모은다(GDAL 요건). 노선 PRJ가 그 안에 남으므로 지형 PRJ(input/prj/)와 파일명 정렬 운에 기대지 않고 갈린다. find_project_prj가 지형 PRJ를 프로젝트 좌표계로 고른다. - 필수 세트를 노선 1종(csv 또는 shp) + prj + tfw로 완화, shp면 shx/dbf 동반 필수. - UI: 확장자 단독 슬롯 매칭을 basename 그룹핑으로 바꿔 노선 PRJ와 지형 PRJ가 같은 슬롯을 다투지 않게 하고, 노선 슬롯이 파일 한 벌을 담아 함께 전송한다. 자체검증: tmp/tests/test_route_shapefile_input.py 9개 통과, tsc --noEmit 통과, ruff check/format 통과. 전체 스위트 잔여 실패 11건은 HEAD 사본(git archive)에서 동일하게 재현되는 기존 실패다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,7 @@ import {
|
||||
getExtension,
|
||||
initializeSlots,
|
||||
makeSessionKey,
|
||||
splitShapefileSelection,
|
||||
type FileSlot,
|
||||
type FileSlotState,
|
||||
type StoredUploadSession,
|
||||
@@ -289,6 +290,7 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
async function assignFileToSlot(
|
||||
file: File,
|
||||
targetSlot?: FileSlot,
|
||||
companions: File[] = [],
|
||||
): Promise<void> {
|
||||
const extension = getExtension(file.name);
|
||||
const state = targetSlot
|
||||
@@ -323,6 +325,7 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
}
|
||||
|
||||
state.file = file;
|
||||
state.companions = companions.length ? companions : undefined;
|
||||
state.uploadSessionId = undefined;
|
||||
state.uploadStatus = "pending";
|
||||
state.progressBytes = 0;
|
||||
@@ -353,8 +356,12 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
// 개수는 "고른 파일 수"가 아니라 **최종적으로 차는 슬롯 수**로 센다.
|
||||
// 같은 슬롯을 다시 고르는 것은 교체라 개수가 늘지 않는다 — 더하기로 세면 5개를 고른
|
||||
// 뒤 파일 선택 영역으로 하나만 바꾸려 해도 초과로 막힌다(2026-08-08).
|
||||
// 계획노선 shapefile은 파일 한 벌이 슬롯 하나로 간다 — 확장자만 보면 노선 PRJ가
|
||||
// 지형 PRJ 슬롯을 덮어쓴다(2026-08-31).
|
||||
const { shapefile, rest } = splitShapefileSelection(files);
|
||||
const occupied = new Set(selectedStates().map((state) => state.slot));
|
||||
for (const file of files) {
|
||||
if (shapefile) occupied.add(targetSlot ?? "csv");
|
||||
for (const file of rest) {
|
||||
const extension = getExtension(file.name);
|
||||
const slot =
|
||||
targetSlot ??
|
||||
@@ -369,7 +376,14 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
}
|
||||
pageError.textContent = blocked ? L("B03_File_Error_LasFreeBlocked") : "";
|
||||
void (async () => {
|
||||
for (const file of files) await assignFileToSlot(file, targetSlot);
|
||||
if (shapefile) {
|
||||
await assignFileToSlot(
|
||||
shapefile.primary,
|
||||
targetSlot,
|
||||
shapefile.companions,
|
||||
);
|
||||
}
|
||||
for (const file of rest) await assignFileToSlot(file, targetSlot);
|
||||
await detectPausedUploads();
|
||||
})();
|
||||
}
|
||||
@@ -381,6 +395,7 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
localStorage.removeItem(makeSessionKey(activeProjectId, state.file));
|
||||
}
|
||||
state.file = undefined;
|
||||
state.companions = undefined;
|
||||
state.uploadSessionId = undefined;
|
||||
state.uploadStatus = "pending";
|
||||
state.progressBytes = 0;
|
||||
@@ -665,13 +680,32 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
setUploading(true);
|
||||
const uploaded: UploadedFileResult[] = [];
|
||||
try {
|
||||
for (let index = 0; index < targetStates.length; index += 1) {
|
||||
const state = targetStates[index];
|
||||
// 슬롯 하나가 파일 여럿일 수 있다(계획노선 shapefile 세트) — 완료 신호는 **마지막
|
||||
// 파일 한 건**에만 붙어야 하므로 슬롯이 아니라 파일 단위로 펼쳐서 센다.
|
||||
const jobs: { state: FileSlotState; file: File }[] = [];
|
||||
for (const state of targetStates) {
|
||||
for (const companion of state.companions ?? [])
|
||||
jobs.push({ state, file: companion });
|
||||
if (state.file) jobs.push({ state, file: state.file });
|
||||
}
|
||||
for (let index = 0; index < jobs.length; index += 1) {
|
||||
const { state, file } = jobs[index];
|
||||
// 동반 파일은 진행률·세션을 대표 파일과 섞지 않도록 임시 상태로 올린다.
|
||||
const uploadState =
|
||||
file === state.file
|
||||
? state
|
||||
: {
|
||||
...state,
|
||||
file,
|
||||
companions: undefined,
|
||||
uploadSessionId: undefined,
|
||||
progressBytes: 0,
|
||||
};
|
||||
uploaded.push(
|
||||
...(await uploadOneFile(
|
||||
activeProjectId,
|
||||
state,
|
||||
index === targetStates.length - 1,
|
||||
uploadState,
|
||||
index === jobs.length - 1,
|
||||
() => renderSlot(state.slot),
|
||||
lasFreeDesign,
|
||||
)),
|
||||
|
||||
Reference in New Issue
Block a user