/* ============================================================================= * D01_PlanRegister_UI_Page.ts * D01 예정노선 등록(요구서 345 3-1 · PLAN 529-1) — B02 등록 화면 꼴 · 도면 칸 없음. * 칸 = 사업연도 시작 · 끝 · 사업지역 · 종류 「노선 계획」(고정) · 임도 종류(B02 값) · 설계속도 * (종류별 고를 수 있는 값 · 기본 = 가장 낮은 값 = B05 resolve_design_speed) · 프로젝트명(B02 이름 * 조합기 · 연도 자리 = 「시작~끝」) · 노선 시작 · 종료 누가거리. * [등록] = POST /api/d01/project → 현재 프로젝트 · D02 로. * ========================================================================== */ import "../B02_ProjRegister/B02_ProjRegister_UI_Style.css"; import { ROUTES } from "@config/config_frontend"; import { createButton, createCard, createInputField, createSelectField, } from "@ui/ui_template_elements"; import { createGeneralLayout } from "@ui/ui_template_general_layout"; import { createHelpButton } from "@ui/ui_template_help"; import { createText } from "@ui/ui_template_text"; import { t } from "@ui/ui_template_locale"; import { stationLabel } from "../common_util/common_util_station"; import { createGroupProject } from "../A00_Common/group_workflow"; import { navigateTo } from "../A00_Common/router"; import { createNameComposer } from "../B02_ProjRegister/B02_ProjRegister_UI_Name"; import { SPEED_CHOICES } from "../B05_Profile/logic/B05_Profile_Grade_Criteria"; /** B02 임도 종류 → 설계속도 기준 등급(B05 grade_class_of 와 같음) */ const GRADE_CLASS: Record = { main: "trunk", fire: "fire", work: "work" }; /** 측점 간격 — D03 `PLAN_STATION_INTERVAL_M` 와 같음 */ const STATION_INTERVAL_M = 20; const numberOrNull = (text: string): number | null => text.trim() === "" ? null : Number.parseFloat(text); export function renderD01PlanRegister(root: HTMLElement): void { const year = new Date().getFullYear(); const yearStart = createInputField({ label: t("DE_Reg_YearStart"), type: "number", value: String(year), min: 2000, max: 2100, }); const yearEnd = createInputField({ label: t("DE_Reg_YearEnd"), type: "number", value: String(year), min: 2000, max: 2100, }); const region = createInputField({ label: t("B02_Proj_Field_Region"), placeholder: t("B02_Proj_Field_Region_Placeholder"), required: true, }); const kind = createInputField({ label: t("DE_Reg_Kind"), value: t("DE_Reg_KindPlan") }); kind.input.readOnly = true; const roadType = createSelectField({ label: t("B02_Proj_Field_RoadType"), options: [ { value: "main", text: t("B02_Proj_RoadType_Main") }, { value: "fire", text: t("B02_Proj_RoadType_Fire") }, { value: "work", text: t("B02_Proj_RoadType_Work") }, ], }); const speed = createSelectField({ label: t("DE_Reg_DesignSpeed"), options: [] }); const name = createInputField({ label: t("DE_Reg_Name"), required: true }); const startM = createInputField({ label: t("DE_Reg_StartM"), type: "number", min: 0 }); const endM = createInputField({ label: t("DE_Reg_EndM"), type: "number", min: 0 }); // 누가거리 칸 옆 즉시 측점 표기(저장값 · 서버 검사는 m 그대로) for (const field of [startM, endM]) { const shown = createText("", "muted"); shown.dataset.station = ""; field.root.insertBefore(shown, field.root.lastElementChild); field.input.addEventListener("input", () => { const value = numberOrNull(field.input.value); shown.textContent = value !== null && Number.isFinite(value) && value >= 0 ? `= ${stationLabel(value, STATION_INTERVAL_M, 1)}` : ""; }); } // 설계속도 = 종류별 고를 수 있는 값 · 기본 = 가장 낮은 값(20) const syncSpeeds = (): void => { const choices = SPEED_CHOICES[GRADE_CLASS[roadType.select.value]] ?? [20]; speed.setOptions( choices.map((kph) => ({ value: String(kph), text: `${kph} km/h` })), String(Math.min(...choices)), ); }; // 이름 = B02 조합기 그대로(손대기 전 = 자동 · 고친 뒤 = 조각만 갈아 끼움) const composer = createNameComposer(); const years = (): string => { const [a, b] = [yearStart.input.value.trim(), yearEnd.input.value.trim()]; return b && b !== a ? `${a}~${b}` : a; }; const syncName = (): void => { name.input.value = composer.update({ year: years(), region: region.input.value, type: roadType.select.selectedOptions[0]?.textContent ?? "", }); }; name.input.addEventListener("input", () => composer.edit(name.input.value)); for (const field of [yearStart, yearEnd, region]) field.input.addEventListener("input", syncName); roadType.select.addEventListener("change", () => (syncSpeeds(), syncName())); syncSpeeds(); syncName(); const submit = (): void => { const fields = [yearStart, yearEnd, region, name, startM, endM]; fields.forEach((field) => field.setError()); const y0 = Number.parseInt(yearStart.input.value, 10); const y1 = Number.parseInt(yearEnd.input.value, 10); const [s, e] = [numberOrNull(startM.input.value), numberOrNull(endM.input.value)]; const errors: Array<[(typeof fields)[number], string | false]> = [ [yearStart, !(y0 >= 2000 && y0 <= 2100) && t("Common_Validation_NumberRange")], [yearEnd, !(y1 >= y0 && y1 <= 2100) && t("DE_Reg_YearOrder")], [region, !region.input.value.trim() && t("B02_Proj_Error_Required")], [name, !name.input.value.trim() && t("B02_Proj_Error_Required")], [startM, s !== null && !(s >= 0) && t("Common_Validation_NumberRange")], [endM, e !== null && s !== null && !(e > s) && t("DE_Reg_ChainOrder")], ]; const bad = errors.filter(([, message]) => message); bad.forEach(([field, message]) => field.setError(message as string)); if (bad.length) return; void createGroupProject( "/d01/project", { project_name: name.input.value.trim(), project_year: y0, project_year_end: y1, region: region.input.value.trim(), road_type: roadType.select.value, design_speed_kph: Number(speed.select.value), start_chainage_m: s, end_chainage_m: e, }, ROUTES.D02_PLAN_BASE, ); }; const grid = document.createElement("div"); grid.className = "b02-proj__grid"; grid.append( ...[yearStart, yearEnd, region, kind, roadType, speed, name, startM, endM].map((f) => f.root), ); const actions = document.createElement("div"); actions.className = "ui-general-block__actions b02-proj__actions"; actions.append( createHelpButton(), createButton({ label: t("Common_Btn_Cancel"), variant: "ghost", onClick: () => navigateTo(ROUTES.B01_ACCOUNT), }), createButton({ label: t("DE_Reg_Submit"), variant: "filled", onClick: submit }), ); const layout = createGeneralLayout({ pageClass: "b02-proj", title: t("DE_Reg_PlanTitle"), subtitle: t("DE_Reg_PlanSubtitle"), content: createCard({ body: [grid, actions], raised: true }), }); root.append(layout.root); }