180 lines
6.0 KiB
TypeScript
180 lines
6.0 KiB
TypeScript
/* =============================================================================
|
|
* 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<void> {
|
|
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);
|
|
}
|