/* ============================================================================= * B02_ProjRegister_UI_Page.ts * 로그인 후 02: 프로젝트 등록 (신규 임도 설계 프로젝트 기본 정보 입력) * * 제약 준수 (frontend.md): * - 모든 문구는 ui_template_locale.ts 참조 (하드코딩 금지, §3). * - 색상/간격은 theme.css 변수 + 공통 컴포넌트만 사용 (§1, §2). * - 이벤트 핸들러는 onB02_[기능]_[액션] 명명 (§4). * - 백엔드 전송 전 1차 유효성 검사 (§4). * ========================================================================== */ import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale"; import { createButton, createInputField, createSelectField, createCard, hideLoadingOverlay, showLoadingOverlay, showToast, } from "@ui/ui_template_elements"; import { isBlank } from "@util/common_util_validate"; import { navigateTo } from "../A00_Common/router"; import { API_BASE_URL, CURRENT_PROJECT_ID_KEY, ROUTES } from "@config/config_frontend"; import "./B02_ProjRegister_UI_Style.css"; /** locale 헬퍼 */ function L(key: keyof typeof ui_locales): string { return ui_locales[key][currentLanguageIndex]; } /* ----------------------------------------------------------------------------- * 페이지 진입점 * -------------------------------------------------------------------------- */ export function renderB02ProjRegister(root: HTMLElement): void { const page = document.createElement("div"); page.className = "b02-proj"; const header = document.createElement("div"); header.className = "b02-proj__header"; const title = document.createElement("h1"); title.className = "b02-proj__title"; title.textContent = L("B02_Proj_Title"); const subtitle = document.createElement("p"); subtitle.className = "b02-proj__subtitle"; subtitle.textContent = L("B02_Proj_Subtitle"); header.append(title, subtitle); // 입력 필드 const nameField = createInputField({ label: L("B02_Proj_Field_Name"), placeholder: L("B02_Proj_Field_Name_Placeholder"), required: true, }); const regionField = createInputField({ label: L("B02_Proj_Field_Region"), placeholder: L("B02_Proj_Field_Region_Placeholder"), required: true, }); const roadTypeField = createSelectField({ label: L("B02_Proj_Field_RoadType"), options: [ { value: "main", text: L("B02_Proj_RoadType_Main") }, { value: "branch", text: L("B02_Proj_RoadType_Branch") }, { value: "fire", text: L("B02_Proj_RoadType_Fire") }, { value: "stream", text: L("B02_Proj_RoadType_Stream") }, ], }); const currentYear = new Date().getFullYear(); const yearField = createInputField({ label: L("B02_Proj_Field_Year"), type: "number", value: String(currentYear), min: 2000, max: currentYear + 5, }); const lengthField = createInputField({ label: L("B02_Proj_Field_Length"), placeholder: L("B02_Proj_Field_Length_Placeholder"), type: "number", min: 0, }); const memoField = createInputField({ label: L("B02_Proj_Field_Memo"), placeholder: L("B02_Proj_Field_Memo_Placeholder"), type: "text", }); const submitBtn = createButton({ label: L("B02_Proj_Submit"), variant: "filled", onClick: onB02_Proj_Submit_Click, }); submitBtn.classList.add("b02-proj__submit"); async function onB02_Proj_Submit_Click(): Promise { nameField.setError(); regionField.setError(); yearField.setError(); lengthField.setError(); // 1차 유효성: 필수값 검사 let hasError = false; const projectYear = Number.parseInt(yearField.input.value, 10); const estimatedLength = isBlank(lengthField.input.value) ? null : Number.parseFloat(lengthField.input.value); if (isBlank(nameField.input.value)) { nameField.setError(L("B02_Proj_Error_Required")); hasError = true; } if (isBlank(regionField.input.value)) { regionField.setError(L("B02_Proj_Error_Required")); hasError = true; } if (!Number.isInteger(projectYear) || projectYear < 2000 || projectYear > 2100) { yearField.setError(L("Common_Validation_NumberRange")); hasError = true; } if (estimatedLength !== null && (!Number.isFinite(estimatedLength) || estimatedLength < 0)) { lengthField.setError(L("Common_Validation_NumberRange")); hasError = true; } if (hasError) return; showLoadingOverlay(); try { const response = await fetch(`${API_BASE_URL}/b02/project`, { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ name: nameField.input.value.trim(), region: regionField.input.value.trim(), road_type: roadTypeField.select.value, project_year: projectYear, estimated_length_m: estimatedLength, memo: memoField.input.value.trim() || null, }), }); if (!response.ok) { let detail = L("Common_Status_Error"); try { const errorBody = (await response.json()) as { detail?: string; message?: string }; detail = errorBody.detail ?? errorBody.message ?? detail; } catch { detail = `${detail} (${response.status})`; } throw new Error(detail); } const data = (await response.json()) as { project_id: string }; localStorage.setItem(CURRENT_PROJECT_ID_KEY, data.project_id); showToast(L("B02_Proj_Success"), "success"); navigateTo(ROUTES.B03_FILE_INPUT); } catch (error) { const message = error instanceof Error ? error.message : L("Common_Status_Error"); showToast(message, "error"); } finally { hideLoadingOverlay(); } } const grid = document.createElement("div"); grid.className = "b02-proj__grid"; grid.append( nameField.root, regionField.root, roadTypeField.root, yearField.root, lengthField.root, memoField.root, ); const card = createCard({ body: [grid, submitBtn], raised: true }); page.append(header, card); root.append(page); }