- 새 D03_PlanAnalysis_UI_Site — 지금 주소 · 찾기(createFindList · searchPlaces) · 실패 · 끊김 한 줄 + [다시 받기] · [임도망 올리기] - 위치 = 캐시 한 걸음(Snap site · 판 5 · 되돌리기 · 떠남 물음) → [저장] 몸 site · 지점 이름 point_names - 고름 → 지도 이동 → 둘레 받기(center) · 멂 · 다른 TM 띠 = 물음 한 번 뒤 reset · 아니오 = 위치 되돌림 - 임도망 올린 뒤 지도 맞춤 = 주소지 둘레 ∪ 잘린 임도 상자(없으면 주소지) - 조용히 받기 = 기준점 10 km 안 지점 · 선만 · 칸 글 걷음(실패 · 끊김 · 기준점 없음 · 못 불러옴만) - 등록(D01) 지도 머리 [위성] 오른쪽 찾기 = 같은 createPlaceFind · 사전 DE_Reg_Find* - 시험 553_4 위치 칸 · 등록 찾기 부품 · 560_4 savedJson 꼴 · 584_3 올리기 자리(위치 칸) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
189 lines
8.0 KiB
TypeScript
189 lines
8.0 KiB
TypeScript
/* =============================================================================
|
|
* D01_PlanRegister_UI_Page.ts
|
|
* D01 예정노선 등록(요구서 345 3-1 · 389 3장 1~3 · PLAN 560-1) — B02 등록 화면 꼴 · 도면 칸 없음.
|
|
* 칸 = 사업연도(시작) · 사업지역 · 프로젝트명(B02 이름 조합기 · 「<시작 연도> <사업 지역> 노선 계획」)
|
|
* · 사업연도 끝 · 임도 종류 · 측점은 안 받음(끝 연도 = D03 [저장] · 나머지 = 서버 기본) · 설계속도 없음(591-1).
|
|
* 기준점(591-1 · 요구 452 · 453) = 카카오 지도(공용 editMap · 마우스 설정 따름) 누름 · 끌기로 한 점 →
|
|
* GET /api/map/address(VWorld · 출처 표기) → 주소 글 · 사업 지역 「시군구 읍면」 채움(손으로 고친 뒤엔 안 덮음)
|
|
* 지도 머리 [위성] 오른쪽 = 주소 · 장소 찾기(596-5 · 예정노선 위치 칸과 같은 `createPlaceFind` · 서버 VWorld) → 고름 = 그 자리로 기준점.
|
|
* [등록] = POST /api/d01/project(site = 경위도 · 주소는 서버가 다시 찾음) → 현재 프로젝트 · D03 으로.
|
|
* ========================================================================== */
|
|
|
|
import { API_BASE_URL, ROUTES } from "@config/config_frontend";
|
|
import { createButton, createCard, createInputField, showToast } from "@ui/ui_template_elements";
|
|
import { createFormGrid, createGeneralLayout } from "@ui/ui_template_general_layout";
|
|
import { createHelpButton } from "@ui/ui_template_help";
|
|
import { editMap, type MapType } from "@ui/ui_template_kakao_map";
|
|
import { t } from "@ui/ui_template_locale";
|
|
import { createSegment } from "@ui/ui_template_segment";
|
|
import { createText } from "@ui/ui_template_text";
|
|
import { readByKey, writeByKey } from "../A00_Common/b_page_state";
|
|
import { createGroupProject } from "../A00_Common/group_workflow";
|
|
import { navigateTo } from "../A00_Common/router";
|
|
import { createNameComposer } from "../B02_ProjRegister/B02_ProjRegister_UI_Name";
|
|
import { createPlaceFind } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Site";
|
|
|
|
const KOREA_CENTER: [number, number] = [127.8, 36.3];
|
|
const DIGITS = 6; // 경위도 소수 자리(약 0.1 m)
|
|
const PREF_MAP_TYPE = "aislo:pref:d01.map.type";
|
|
|
|
interface Place {
|
|
address: string | null;
|
|
region: string | null;
|
|
source: string;
|
|
}
|
|
|
|
/** 기준점 고르기 — 지도 누름 · 끌기 = 한 점 · 주소(VWorld) · onRegion = 「시군구 읍면」 */
|
|
function sitePicker(onRegion: (region: string) => void): {
|
|
root: HTMLElement;
|
|
value: () => [number, number] | null;
|
|
} {
|
|
let site: [number, number] | null = null;
|
|
let seq = 0; // 늦게 온 주소가 새 자리를 덮지 않게
|
|
const where = createText(t("DE_Reg_SiteEmpty"), "muted", 14);
|
|
where.dataset.d01Site = "address";
|
|
const map = editMap({
|
|
hint: t("DE_Reg_SiteHint"),
|
|
center: () => KOREA_CENTER,
|
|
onClick: (lon, lat) => void moveTo(lon, lat),
|
|
onMove: (_key, lon, lat) => void moveTo(lon, lat),
|
|
});
|
|
const mapType = (readByKey(PREF_MAP_TYPE) === "HYBRID" ? "HYBRID" : "ROADMAP") as MapType;
|
|
map.setType(mapType);
|
|
const typePick = createSegment({
|
|
options: [
|
|
{ value: "ROADMAP", label: t("DE_Reg_MapNormal") },
|
|
{ value: "HYBRID", label: t("DE_Reg_MapSatellite") },
|
|
],
|
|
value: mapType,
|
|
ariaLabel: t("DE_Reg_MapType"),
|
|
onChange: (value) => {
|
|
map.setType(value as MapType);
|
|
writeByKey(PREF_MAP_TYPE, value);
|
|
},
|
|
});
|
|
const draw = (): void =>
|
|
map.set(
|
|
site
|
|
? [{ key: "site", name: t("DE_Reg_Site"), lon: site[0], lat: site[1], movable: true }]
|
|
: [],
|
|
[],
|
|
);
|
|
|
|
async function moveTo(lon: number, lat: number): Promise<void> {
|
|
const mine = ++seq;
|
|
site = [+lon.toFixed(DIGITS), +lat.toFixed(DIGITS)];
|
|
where.textContent = t("DE_Reg_SiteFinding");
|
|
draw();
|
|
const place = await fetch(`${API_BASE_URL}/map/address?lon=${site[0]}&lat=${site[1]}`, {
|
|
credentials: "include",
|
|
})
|
|
.then((r) => (r.ok ? (r.json() as Promise<Place>) : null))
|
|
.catch(() => null);
|
|
if (mine !== seq) return;
|
|
const text = place?.address || `${t("DE_Reg_SiteNoAddress")} (${site[1]}, ${site[0]})`;
|
|
where.textContent = `${t("DE_Reg_SiteAddress")}${text}${place ? ` · ${t("DE_Reg_SiteSource")} ${place.source}` : ""}`;
|
|
if (place?.region) onRegion(place.region);
|
|
}
|
|
|
|
draw();
|
|
// 찾기 = 칸은 머리 줄([위성] 오른쪽 · 좁으면 줄바꿈) · 알림 · 후보 줄은 머리 아래
|
|
const find = createPlaceFind((hit) => void moveTo(hit.lon, hit.lat));
|
|
const field = find.search.parentElement!;
|
|
field.style.flex = "1 1 12rem";
|
|
const head = document.createElement("div");
|
|
head.className = "ui-general-block__actions";
|
|
head.style.flexWrap = "wrap";
|
|
head.append(createText(t("DE_Reg_Site"), "muted", 14), typePick.root, field);
|
|
const root = document.createElement("div");
|
|
root.dataset.d01Site = "";
|
|
root.append(head, find.root, map.root, where);
|
|
return { root, value: () => site };
|
|
}
|
|
|
|
export function renderD01PlanRegister(root: HTMLElement): void {
|
|
const yearStart = createInputField({
|
|
label: t("DE_Reg_YearStart"),
|
|
type: "number",
|
|
value: String(new Date().getFullYear()),
|
|
min: 2000,
|
|
max: 2100,
|
|
});
|
|
const region = createInputField({
|
|
label: t("B02_Proj_Field_Region"),
|
|
placeholder: t("B02_Proj_Field_Region_Placeholder"),
|
|
required: true,
|
|
});
|
|
const name = createInputField({ label: t("DE_Reg_Name"), required: true });
|
|
|
|
// 이름 = B02 조합기 그대로(손대기 전 = 자동 · 고친 뒤 = 조각만 갈아 끼움)
|
|
const composer = createNameComposer();
|
|
const syncName = (): void => {
|
|
name.input.value = composer.update({
|
|
year: yearStart.input.value.trim(),
|
|
region: region.input.value,
|
|
type: t("DE_Reg_KindPlan"),
|
|
});
|
|
};
|
|
name.input.addEventListener("input", () => composer.edit(name.input.value));
|
|
for (const field of [yearStart, region]) field.input.addEventListener("input", syncName);
|
|
syncName();
|
|
|
|
// 사업 지역 = 주소의 「시군구 읍면」 — 비었거나 지난번 채운 그대로일 때만(손으로 고친 값은 둠)
|
|
let autoRegion = "";
|
|
const site = sitePicker((value) => {
|
|
const now = region.input.value.trim();
|
|
if (now && now !== autoRegion) return;
|
|
autoRegion = value;
|
|
region.input.value = value;
|
|
region.setError();
|
|
syncName();
|
|
});
|
|
|
|
const submit = (): void => {
|
|
const fields = [yearStart, region, name];
|
|
fields.forEach((field) => field.setError());
|
|
const y0 = Number.parseInt(yearStart.input.value, 10);
|
|
const errors: Array<[(typeof fields)[number], string | false]> = [
|
|
[yearStart, !(y0 >= 2000 && y0 <= 2100) && t("Common_Validation_NumberRange")],
|
|
[region, !region.input.value.trim() && t("B02_Proj_Error_Required")],
|
|
[name, !name.input.value.trim() && t("B02_Proj_Error_Required")],
|
|
];
|
|
const bad = errors.filter(([, message]) => message);
|
|
bad.forEach(([field, message]) => field.setError(message as string));
|
|
if (bad.length) return;
|
|
const at = site.value();
|
|
if (!at) return showToast(t("DE_Reg_SiteRequired"), "error");
|
|
void createGroupProject(
|
|
"/d01/project",
|
|
{
|
|
project_name: name.input.value.trim(),
|
|
project_year: y0,
|
|
region: region.input.value.trim(),
|
|
site: { lon: at[0], lat: at[1] },
|
|
},
|
|
ROUTES.D03_PLAN_ANALYSIS,
|
|
);
|
|
};
|
|
|
|
const grid = createFormGrid([yearStart, region, name].map((f) => f.root));
|
|
const actions = document.createElement("div");
|
|
actions.className = "ui-general-block__actions ui-general-block__actions--end";
|
|
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({
|
|
narrow: true,
|
|
title: t("DE_Reg_PlanTitle"),
|
|
subtitle: t("DE_Reg_PlanSubtitle"),
|
|
content: createCard({ body: [grid, site.root, actions], raised: true }),
|
|
});
|
|
root.append(layout.root);
|
|
}
|