- 거래처 · 기준점 창 제목 = 이름만 · 시점 창 = 「시점 · 중점」 - 소재지 지도에 시점 표지(고정 · 끌기 없음) · 소재지 없으면 시점 중심 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018jonfdxn1Xit9bdkNJpE4G
153 lines
5.9 KiB
TypeScript
153 lines
5.9 KiB
TypeScript
/* =============================================================================
|
|
* B08_Quantity_UI_Milestone_Place.ts
|
|
* 거리이정표 등록 모달 「소재지」 칸(PLAN 244-13 · 사용자 147번 5) — 주소 찾기 · 지도에서 고르기만(직접 입력 없음).
|
|
*
|
|
* 주소 찾기 = B01 회사 주소와 같은 서버 후보 검색(VWorld 대리 · `searchAddress`) → 후보 고름 = 소재지 · 표지
|
|
* 지도 = 다음(카카오) 지도 JS(`…_KakaoMap` editMap) — 지도 누름 = 그 자리 소재지 · 표지 끌어 옮김 = 다시 고름 ·
|
|
* 고른 좌표 → 서버 GET …/milestone/reverse(VWorld 주소 한 줄 · 못 찾음 = 위도, 경도 글)
|
|
* 값 = 주소 글 · 경도 · 위도(캐시 · [저장] 때 서버)
|
|
* ========================================================================== */
|
|
|
|
import { createButton, createInputField, el, showToast } from "@ui/ui_template_elements";
|
|
import { searchAddress, type AddressCandidate } from "../B01_Dashboard/B01_Dashboard_Api_Fetch";
|
|
import { request } from "./B08_Quantity_UI_Hydro";
|
|
import { editMap } from "./B08_Quantity_UI_Milestone_KakaoMap";
|
|
import { liveMilestone, startPoint } from "./B08_Quantity_UI_Milestone_State";
|
|
|
|
export interface Place {
|
|
address: string;
|
|
lon: number | null;
|
|
lat: number | null;
|
|
}
|
|
|
|
export interface PlaceHandle {
|
|
root: HTMLElement;
|
|
value: () => Place;
|
|
}
|
|
|
|
/** 지번 후보 = 지번 줄이 시 · 도까지 갖춤(B01 주소 칸과 같은 고름) */
|
|
const lineOf = (c: AddressCandidate): string =>
|
|
(c.category === "parcel" ? c.parcel : c.road) || c.road || c.parcel;
|
|
|
|
/** 좌표 → 주소 한 줄(서버 VWorld) · 못 찾음 = 「위도, 경도」 글 */
|
|
export async function reverseAddress(projectId: string, lon: number, lat: number): Promise<string> {
|
|
const got = await request<{ address: string | null }>(
|
|
`/projects/${projectId}/quantity/milestone/reverse?lon=${lon}&lat=${lat}`,
|
|
).catch(() => null);
|
|
return got?.address || `${lat.toFixed(5)}, ${lon.toFixed(5)}`;
|
|
}
|
|
|
|
/** 소재지 칸 — name() = 지도 표지 이름(거래처 · 기준점 이름) */
|
|
export function placePicker(projectId: string, initial: Place, name: () => string): PlaceHandle {
|
|
let place: Place = { ...initial };
|
|
let seq = 0; // 늦게 온 주소가 새 자리를 덮지 않게
|
|
const query = createInputField({
|
|
label: "주소 찾기",
|
|
compact: true,
|
|
placeholder: "도로명 · 지번 · 건물명 일부 (예 병곡리 968-31)",
|
|
});
|
|
const find = createButton({ label: "주소 찾기", variant: "ghost", onClick: () => void search() });
|
|
find.dataset.b08MilestonePlace = "find";
|
|
const list = el("div", { className: "b01-dashboard__address-list" });
|
|
list.hidden = true;
|
|
const where = el("p", { className: "b08-milestone__place" });
|
|
where.dataset.b08MilestonePlace = "value";
|
|
// 262-2 시점 = 거리이정표가 쓰는 시점(끌어 옮긴 덮개 · 없으면 노선 시점)
|
|
const start = (): [number, number] | null => startPoint(liveMilestone(projectId));
|
|
const map = editMap(projectId, {
|
|
hint: "지도 누름 = 그 자리를 소재지로 · 표지 끌어 옮김",
|
|
onClick: (lon, lat) => void moveTo(lon, lat),
|
|
onMove: (_key, lon, lat) => void moveTo(lon, lat),
|
|
center: start,
|
|
});
|
|
|
|
const draw = (): void => {
|
|
where.textContent = `소재지 — ${place.address || "주소 찾기 · 지도에서 고르세요"}`;
|
|
const s = start();
|
|
map.set(
|
|
[
|
|
...(s
|
|
? [{ key: "start", name: "시점", lon: s[0], lat: s[1], movable: false, fixed: true }]
|
|
: []),
|
|
...(place.lon != null && place.lat != null
|
|
? [
|
|
{
|
|
key: "place",
|
|
name: name() || "소재지",
|
|
lon: place.lon,
|
|
lat: place.lat,
|
|
movable: true,
|
|
},
|
|
]
|
|
: []),
|
|
],
|
|
[],
|
|
);
|
|
};
|
|
|
|
async function moveTo(lon: number, lat: number): Promise<void> {
|
|
const mine = ++seq;
|
|
place = { ...place, lon, lat };
|
|
draw();
|
|
const address = await reverseAddress(projectId, lon, lat);
|
|
if (mine !== seq) return;
|
|
place.address = address;
|
|
draw();
|
|
}
|
|
|
|
async function search(): Promise<void> {
|
|
const text = query.input.value.trim();
|
|
if (text.length < 2) return showToast("주소를 두 글자 이상 넣으세요.", "error");
|
|
find.disabled = true;
|
|
try {
|
|
const items = await searchAddress(text);
|
|
list.hidden = false;
|
|
list.replaceChildren(
|
|
...(items.length
|
|
? items.map((item) => {
|
|
const option = el("button", {
|
|
className: "b01-dashboard__address-item",
|
|
attrs: { type: "button" },
|
|
children: [
|
|
el("strong", { text: lineOf(item) }),
|
|
el("small", { text: [item.building, item.zipcode].filter(Boolean).join(" · ") }),
|
|
],
|
|
});
|
|
option.addEventListener("click", () => {
|
|
seq++;
|
|
place = { address: lineOf(item), lon: item.lon, lat: item.lat };
|
|
list.hidden = true;
|
|
draw();
|
|
});
|
|
return option;
|
|
})
|
|
: [el("p", { text: "찾은 주소가 없습니다 — 다른 낱말로 찾거나 지도에서 고르세요." })]),
|
|
);
|
|
} catch {
|
|
showToast("주소를 찾지 못했습니다. 잠시 뒤 다시 하세요.", "error");
|
|
} finally {
|
|
find.disabled = false;
|
|
}
|
|
}
|
|
// 검색 칸 Enter = 찾기만(모달 안 닫힘)
|
|
query.input.addEventListener("keydown", (e) => {
|
|
if (e.key !== "Enter") return;
|
|
e.preventDefault();
|
|
void search();
|
|
});
|
|
|
|
draw();
|
|
return {
|
|
root: el("div", {
|
|
className: "b08-milestone__place-field",
|
|
children: [
|
|
el("div", { className: "b08-milestone__line-edit", children: [query.root, find] }),
|
|
list,
|
|
where,
|
|
map.root,
|
|
],
|
|
}),
|
|
value: () => ({ ...place }),
|
|
};
|
|
}
|