925줄 한 파일을 셋으로 나눔 (동작 불변). - `B03_FileInput_UI_Page.ts` 692줄 — 화면 조립·카드 렌더·현황 표시 - `B03_FileInput_UI_Page_Flow.ts` 262줄 — 중단 세션 이어올리기·보관함 이관·청크 업로드· 초기 계산 중 재잠금·서비스워커. 화면 상태는 `UploadFlowContext` 창구로만 받음 - `B03_FileInput_UI_Page_Rules.ts` 107줄 — 필수 카드 판정·파일 적합성·업로드 가능 판정· 서버 파일의 카드 매핑 (상태를 가두지 않는 순수 함수) 검증: 공용 브라우저에서 새 프로젝트 만들어 노선 5종 실제 선택·업로드 — 카드 5장 선택 후 [파일 업로드] 비활성, [LAS 없이 설계] 켜면 활성(판정 규칙 정상), 업로드 후 4장 완료 표시(20초). 남은 `route_prj` 오류·400 은 지형 자료가 없어 분리 전에도 같던 것. `tsc --noEmit` 통과, tmp/tests 378 passed Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
108 lines
4.5 KiB
TypeScript
108 lines
4.5 KiB
TypeScript
/* =============================================================================
|
|
* B03_FileInput_UI_Page_Rules.ts
|
|
* 파일 입력 화면의 판정 규칙 — 어떤 카드가 지금 필수인가, 고른 파일이 그 자리에 맞는가,
|
|
* 업로드를 시작해도 되는가, 서버 파일이 어느 카드로 가는가.
|
|
*
|
|
* 화면 조립(`B03_FileInput_UI_Page.ts`)이 700줄을 넘어 떼어냈다(2026-09-04).
|
|
* 화면 상태를 가두지 않고 **인자로 받는 순수 함수**만 둔다 — 판정 결과는 종전과 같다.
|
|
* ========================================================================== */
|
|
|
|
import { UPLOAD_MAX_FILES, UPLOAD_MAX_MB } from "@config/config_frontend";
|
|
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
|
import type { UploadOverviewFile } from "./B03_FileInput_Api_Fetch";
|
|
import {
|
|
getExtension,
|
|
SHAPEFILE_DEPENDENT_SLOTS,
|
|
TERRAIN_SLOTS,
|
|
type FileSlot,
|
|
type FileSlotState,
|
|
} from "./B03_FileInput_UI_Support";
|
|
|
|
function L(key: keyof typeof ui_locales): string {
|
|
return ui_locales[key][currentLanguageIndex];
|
|
}
|
|
|
|
export type SlotMap = Map<FileSlot, FileSlotState>;
|
|
|
|
/** 노선 도형 카드에 shapefile이 들어와 있는가. */
|
|
export function routeIsShapefile(slots: SlotMap): boolean {
|
|
const state = slots.get("csv");
|
|
const name = state?.file?.name ?? state?.serverUploaded?.name;
|
|
return getExtension(name ?? "") === ".shp";
|
|
}
|
|
|
|
/**
|
|
* 이 카드가 지금 필수인가.
|
|
*
|
|
* shapefile 형제 카드(.shx/.dbf/노선 .prj)는 노선 도형이 shapefile일 때만 필수다 —
|
|
* CSV 한 장으로 넣는 흐름을 막으면 안 된다(2026-08-31).
|
|
*/
|
|
export function isSlotRequired(
|
|
state: FileSlotState,
|
|
slots: SlotMap,
|
|
lasFreeDesign: boolean,
|
|
): boolean {
|
|
if (SHAPEFILE_DEPENDENT_SLOTS.includes(state.slot)) return routeIsShapefile(slots);
|
|
// LAS 없이 설계면 지형 자료(포인트클라우드·좌표계·래스터)는 통째로 받지 않는다.
|
|
if (TERRAIN_SLOTS.includes(state.slot)) return lasFreeDesign ? false : state.isRequired;
|
|
return state.isRequired;
|
|
}
|
|
|
|
/** 고른 파일이 그 카드의 확장자·크기 규칙에 맞는가. 어긋나면 안내 문구를 돌려준다. */
|
|
export function validateFileForSlot(file: File, state: FileSlotState): string | null {
|
|
const extension = getExtension(file.name);
|
|
const maxBytes = UPLOAD_MAX_MB * 1024 * 1024;
|
|
if (!state.extensions.includes(extension)) return L("B03_File_Error_SlotType");
|
|
if (file.size === 0 || file.size > maxBytes) return L("B03_File_Error_Size");
|
|
return null;
|
|
}
|
|
|
|
/** 업로드를 시작해도 되는가. 안 되면 첫 번째 사유를 돌려준다. */
|
|
export function validateSlots(
|
|
slots: SlotMap,
|
|
selected: FileSlotState[],
|
|
activeProjectId: string,
|
|
lasFreeDesign: boolean,
|
|
): string | null {
|
|
if (!activeProjectId) return L("B03_File_Error_Project");
|
|
if (selected.length === 0) return L("B03_File_Error_Required");
|
|
if (selected.length > UPLOAD_MAX_FILES) return L("B03_File_Error_Count");
|
|
// 필수 슬롯은 로컬 선택이 없어도 **서버에 이미 올라간 파일**이 있으면 충족이다 —
|
|
// 재접속 후 파일 하나만 교체 업로드하는 흐름을 막지 않는다(2026-08-04 사용자 지시).
|
|
const missingRequired = Array.from(slots.values()).some(
|
|
(state) => isSlotRequired(state, slots, lasFreeDesign) && !state.file && !state.serverUploaded,
|
|
);
|
|
if (missingRequired) return L("B03_File_Error_RequiredSlots");
|
|
if (!lasFreeDesign) {
|
|
const lasState = slots.get("las_laz");
|
|
if (!lasState?.file && !lasState?.serverUploaded) return L("B03_File_Error_Las");
|
|
}
|
|
for (const state of selected) {
|
|
if (state.error) return state.error;
|
|
const validation = validateFileForSlot(state.file!, state);
|
|
if (validation) return validation;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 서버 현황의 파일 한 건이 어느 카드로 가는가.
|
|
*
|
|
* PRJ 두 장은 확장자가 같다 — 노선 세트는 `input/shp/`에 모여 있으므로 저장 경로로
|
|
* 가린다(2026-08-31). 옛 프로젝트의 노선은 `.csv`로 올라가 있어 계획노선 도형 카드로
|
|
* 보낸다(이제 새로 받지는 않는다, 2026-09-03).
|
|
*/
|
|
export function slotForOverviewFile(
|
|
file: UploadOverviewFile,
|
|
slots: SlotMap,
|
|
): FileSlot | undefined {
|
|
const extension = `.${file.file_type.toLowerCase()}`;
|
|
if (extension === ".prj") {
|
|
return (file.relative_path ?? "").includes("/input/shp/") ? "route_prj" : "prj";
|
|
}
|
|
if (extension === ".csv") return "csv";
|
|
return Array.from(slots.values()).find(
|
|
(candidate) => candidate.slot !== "route_prj" && candidate.extensions.includes(extension),
|
|
)?.slot;
|
|
}
|