feat(D 예정노선): 지형 칸 → 위치 칸 · 주소 · 장소 찾기 → 이동 → 받기 · 등록 지도 찾기 (PLAN 596-2 · 596-5 · 요구 463)
- 새 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>
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
* · 사업연도 끝 · 임도 종류 · 측점은 안 받음(끝 연도 = D03 [저장] · 나머지 = 서버 기본) · 설계속도 없음(591-1).
|
* · 사업연도 끝 · 임도 종류 · 측점은 안 받음(끝 연도 = D03 [저장] · 나머지 = 서버 기본) · 설계속도 없음(591-1).
|
||||||
* 기준점(591-1 · 요구 452 · 453) = 카카오 지도(공용 editMap · 마우스 설정 따름) 누름 · 끌기로 한 점 →
|
* 기준점(591-1 · 요구 452 · 453) = 카카오 지도(공용 editMap · 마우스 설정 따름) 누름 · 끌기로 한 점 →
|
||||||
* GET /api/map/address(VWorld · 출처 표기) → 주소 글 · 사업 지역 「시군구 읍면」 채움(손으로 고친 뒤엔 안 덮음)
|
* GET /api/map/address(VWorld · 출처 표기) → 주소 글 · 사업 지역 「시군구 읍면」 채움(손으로 고친 뒤엔 안 덮음)
|
||||||
|
* 지도 머리 [위성] 오른쪽 = 주소 · 장소 찾기(596-5 · 예정노선 위치 칸과 같은 `createPlaceFind` · 서버 VWorld) → 고름 = 그 자리로 기준점.
|
||||||
* [등록] = POST /api/d01/project(site = 경위도 · 주소는 서버가 다시 찾음) → 현재 프로젝트 · D03 으로.
|
* [등록] = POST /api/d01/project(site = 경위도 · 주소는 서버가 다시 찾음) → 현재 프로젝트 · D03 으로.
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ import { readByKey, writeByKey } from "../A00_Common/b_page_state";
|
|||||||
import { createGroupProject } from "../A00_Common/group_workflow";
|
import { createGroupProject } from "../A00_Common/group_workflow";
|
||||||
import { navigateTo } from "../A00_Common/router";
|
import { navigateTo } from "../A00_Common/router";
|
||||||
import { createNameComposer } from "../B02_ProjRegister/B02_ProjRegister_UI_Name";
|
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 KOREA_CENTER: [number, number] = [127.8, 36.3];
|
||||||
const DIGITS = 6; // 경위도 소수 자리(약 0.1 m)
|
const DIGITS = 6; // 경위도 소수 자리(약 0.1 m)
|
||||||
@@ -85,12 +87,17 @@ function sitePicker(onRegion: (region: string) => void): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
draw();
|
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");
|
const head = document.createElement("div");
|
||||||
head.className = "ui-general-block__actions";
|
head.className = "ui-general-block__actions";
|
||||||
head.append(createText(t("DE_Reg_Site"), "muted", 14), typePick.root);
|
head.style.flexWrap = "wrap";
|
||||||
|
head.append(createText(t("DE_Reg_Site"), "muted", 14), typePick.root, field);
|
||||||
const root = document.createElement("div");
|
const root = document.createElement("div");
|
||||||
root.dataset.d01Site = "";
|
root.dataset.d01Site = "";
|
||||||
root.append(head, map.root, where);
|
root.append(head, find.root, map.root, where);
|
||||||
return { root, value: () => site };
|
return { root, value: () => site };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,10 +19,8 @@
|
|||||||
import "./D03_PlanAnalysis_UI_Style.css";
|
import "./D03_PlanAnalysis_UI_Style.css";
|
||||||
import { ROUTES } from "@config/config_frontend";
|
import { ROUTES } from "@config/config_frontend";
|
||||||
import { attachCollapsible, createPanelCard } from "@ui/ui_template_collapsible";
|
import { attachCollapsible, createPanelCard } from "@ui/ui_template_collapsible";
|
||||||
import { pickFiles } from "@ui/ui_template_drop";
|
|
||||||
import { askConfirm } from "@ui/ui_template_modal";
|
import { askConfirm } from "@ui/ui_template_modal";
|
||||||
import { createProgressCircle } from "@ui/ui_template_progress";
|
import { createProgressCircle } from "@ui/ui_template_progress";
|
||||||
import { createText } from "@ui/ui_template_text";
|
|
||||||
import {
|
import {
|
||||||
createButton,
|
createButton,
|
||||||
hideLoadingOverlay,
|
hideLoadingOverlay,
|
||||||
@@ -42,9 +40,12 @@ import {
|
|||||||
fetchTerrainExtent,
|
fetchTerrainExtent,
|
||||||
savePlanNetwork,
|
savePlanNetwork,
|
||||||
startTerrain,
|
startTerrain,
|
||||||
|
type PlaceHit,
|
||||||
type PlanJob,
|
type PlanJob,
|
||||||
type PlanNetworkView,
|
type PlanNetworkView,
|
||||||
type PlanSurface,
|
type PlanSurface,
|
||||||
|
type TerrainCenter,
|
||||||
|
type TerrainStart,
|
||||||
} from "./D03_PlanAnalysis_Api";
|
} from "./D03_PlanAnalysis_Api";
|
||||||
import { openPlan3d } from "./D03_PlanAnalysis_UI_3D";
|
import { openPlan3d } from "./D03_PlanAnalysis_UI_3D";
|
||||||
import { createPlanDraw } from "./D03_PlanAnalysis_UI_Draw";
|
import { createPlanDraw } from "./D03_PlanAnalysis_UI_Draw";
|
||||||
@@ -59,6 +60,7 @@ import { createPlanMeasure } from "./D03_PlanAnalysis_UI_Measure";
|
|||||||
import { createPlanPoints } from "./D03_PlanAnalysis_UI_Points";
|
import { createPlanPoints } from "./D03_PlanAnalysis_UI_Points";
|
||||||
import { createPlanPieces } from "./D03_PlanAnalysis_UI_Pieces";
|
import { createPlanPieces } from "./D03_PlanAnalysis_UI_Pieces";
|
||||||
import { createPlanProps } from "./D03_PlanAnalysis_UI_Props";
|
import { createPlanProps } from "./D03_PlanAnalysis_UI_Props";
|
||||||
|
import { createPlanSite } from "./D03_PlanAnalysis_UI_Site";
|
||||||
import {
|
import {
|
||||||
emptyNetwork,
|
emptyNetwork,
|
||||||
fromServer,
|
fromServer,
|
||||||
@@ -75,16 +77,26 @@ const POLL_MS = 1000;
|
|||||||
const INTERRUPTED = "끊김(서버 다시 켜짐 등)";
|
const INTERRUPTED = "끊김(서버 다시 켜짐 등)";
|
||||||
/** 처음 맞춤 = 기준점 둘레(서버 `SITE_RADIUS_M`). */
|
/** 처음 맞춤 = 기준점 둘레(서버 `SITE_RADIUS_M`). */
|
||||||
const SITE_RADIUS_M = 1000;
|
const SITE_RADIUS_M = 1000;
|
||||||
|
/** 조용히 받기가 덮는 거리 — 기준점에서 이보다 먼 지점 · 선은 안 받음(위치를 멀리 옮긴 뒤 옛 선 = 거대 받기 막음). */
|
||||||
|
const COVER_MAX_M = 10000;
|
||||||
/** 되돌리기 쌓는 수. */
|
/** 되돌리기 쌓는 수. */
|
||||||
const UNDO_MAX = 100;
|
const UNDO_MAX = 100;
|
||||||
/** 캐시 꼴 판 — 옛 꼴(지점 없음 · 갈래 고름)은 안 읽음. */
|
/** 캐시 꼴 판 — 옛 꼴(지점 없음 · 갈래 고름 · 위치 없음)은 안 읽음. */
|
||||||
const DRAFT_VERSION = 4;
|
const DRAFT_VERSION = 5;
|
||||||
|
|
||||||
/** 한 걸음 = 지점(+ 이름 · 차례 같음 · 빈 값 = 「지점 N」) + 노선(되돌리기 · 캐시 · 고침 판정). */
|
/** 기준점(위치 칸 · 등록과 한 칸 `projects.site_*`). */
|
||||||
|
interface Site {
|
||||||
|
lon: number;
|
||||||
|
lat: number;
|
||||||
|
address: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 한 걸음 = 지점(+ 이름 · 차례 같음 · 빈 값 = 「지점 N」) + 노선 + 위치(되돌리기 · 캐시 · 고침 판정). */
|
||||||
interface Snap {
|
interface Snap {
|
||||||
network: PlanNetwork;
|
network: PlanNetwork;
|
||||||
points: Vertex[];
|
points: Vertex[];
|
||||||
pointNames: string[];
|
pointNames: string[];
|
||||||
|
site: Site | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Draft extends Snap {
|
interface Draft extends Snap {
|
||||||
@@ -100,10 +112,12 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
|||||||
let editable = false;
|
let editable = false;
|
||||||
let points: Vertex[] = []; // 지점(긋기에 붙는 점 · 591-2)
|
let points: Vertex[] = []; // 지점(긋기에 붙는 점 · 591-2)
|
||||||
let pointNames: string[] = []; // 지점 이름(596 · 597-2)
|
let pointNames: string[] = []; // 지점 이름(596 · 597-2)
|
||||||
|
let site: Site | null = null; // 위치(캐시 · [저장] 때 정본 596-2)
|
||||||
|
let savedSite: Site | null = null;
|
||||||
let epsg = 0;
|
let epsg = 0;
|
||||||
let yearStart = new Date().getFullYear(); // 새 선 처음 해
|
let yearStart = new Date().getFullYear(); // 새 선 처음 해
|
||||||
let network: PlanNetwork = emptyNetwork();
|
let network: PlanNetwork = emptyNetwork();
|
||||||
let savedJson = JSON.stringify({ network, points, pointNames }); // 못 읽은 채 떠남 = 고침 없음
|
let savedJson = JSON.stringify({ network, points, pointNames, site }); // 못 읽은 채 떠남 = 고침 없음
|
||||||
let undoStack: Snap[] = [];
|
let undoStack: Snap[] = [];
|
||||||
let redoStack: Snap[] = [];
|
let redoStack: Snap[] = [];
|
||||||
let surface: PlanSurface | null = null;
|
let surface: PlanSurface | null = null;
|
||||||
@@ -113,6 +127,7 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
|||||||
let ready = false; // 들어올 때 받기 판단이 끝남(그 앞 보기 바뀜은 안 받음)
|
let ready = false; // 들어올 때 받기 판단이 끝남(그 앞 보기 바뀜은 안 받음)
|
||||||
let busy = false;
|
let busy = false;
|
||||||
let failed: string | null = null;
|
let failed: string | null = null;
|
||||||
|
let note = ""; // 위치 칸 알림(기준점 없음 · 못 불러옴)
|
||||||
let measuring = false;
|
let measuring = false;
|
||||||
let lastSaveMs: number | null = null;
|
let lastSaveMs: number | null = null;
|
||||||
|
|
||||||
@@ -158,7 +173,7 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
|||||||
epsg: () => epsg,
|
epsg: () => epsg,
|
||||||
draw,
|
draw,
|
||||||
commit: (nextPoints, nextNames, next) =>
|
commit: (nextPoints, nextNames, next) =>
|
||||||
step({ network: next, points: nextPoints, pointNames: nextNames }),
|
step({ network: next, points: nextPoints, pointNames: nextNames, site }),
|
||||||
center: () => viewMid,
|
center: () => viewMid,
|
||||||
show: (nextPoints, next) => {
|
show: (nextPoints, next) => {
|
||||||
[points, network] = [nextPoints, next];
|
[points, network] = [nextPoints, next];
|
||||||
@@ -194,30 +209,16 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
|||||||
main.className = "d03-plan";
|
main.className = "d03-plan";
|
||||||
main.append(map.root);
|
main.append(map.root);
|
||||||
|
|
||||||
const terrainText = createText("", "muted");
|
const siteBox = createPlanSite({
|
||||||
terrainText.classList.add("d03-plan__hint");
|
onPick: (hit) => void pickSite(hit),
|
||||||
const retryButton = createButton({
|
onRetry: () => {
|
||||||
label: "다시 받기",
|
|
||||||
variant: "filled",
|
|
||||||
size: 28,
|
|
||||||
onClick: () => {
|
|
||||||
[failed, lastTried, ready] = [null, "", true];
|
[failed, lastTried, ready] = [null, "", true];
|
||||||
void ensureCovered();
|
void ensureCovered(true);
|
||||||
},
|
},
|
||||||
|
onUpload: (files) => void uploadRoads(files),
|
||||||
});
|
});
|
||||||
const terrainCard = createPanelCard("지형", false);
|
const siteCard = createPanelCard("위치", false);
|
||||||
const roadButton = createButton({
|
siteCard.append(...siteBox.parts);
|
||||||
label: "임도망 올리기",
|
|
||||||
variant: "ghost",
|
|
||||||
size: 28,
|
|
||||||
onClick: () =>
|
|
||||||
pickFiles({ accept: ".zip,.shp,.shx,.dbf,.prj,.cpg", multiple: true }, (files) => {
|
|
||||||
void backdrop.uploadRoads(files);
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
roadButton.title =
|
|
||||||
"임도망도 SHP(시군구 zip 하나 · 또는 shp · shx · dbf · prj 함께 · 100 MB 까지)";
|
|
||||||
terrainCard.append(terrainText, retryButton, roadButton);
|
|
||||||
const pointCard = createPanelCard("지점", false);
|
const pointCard = createPanelCard("지점", false);
|
||||||
pointCard.append(marks.panel);
|
pointCard.append(marks.panel);
|
||||||
const drawCard = createPanelCard("그리기", false);
|
const drawCard = createPanelCard("그리기", false);
|
||||||
@@ -235,7 +236,7 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
|||||||
actions.append(saveButton, resetButton);
|
actions.append(saveButton, resetButton);
|
||||||
const panel = document.createElement("div");
|
const panel = document.createElement("div");
|
||||||
panel.className = "ui-sidebar-panel d03-plan__panel";
|
panel.className = "ui-sidebar-panel d03-plan__panel";
|
||||||
panel.append(terrainCard, pointCard, drawCard, pieceCard, actions);
|
panel.append(siteCard, pointCard, drawCard, pieceCard, actions);
|
||||||
attachCollapsible(panel);
|
attachCollapsible(panel);
|
||||||
// 떠날 때(단계 아이콘 · [<] [>] · 머리 메뉴 · 뒤로) 고친 선이 있으면 공용 물음 [취소][폐기][저장](572-3)
|
// 떠날 때(단계 아이콘 · [<] [>] · 머리 메뉴 · 뒤로) 고친 선이 있으면 공용 물음 [취소][폐기][저장](572-3)
|
||||||
mountGroupStep(root, ROUTES.D03_PLAN_ANALYSIS, {
|
mountGroupStep(root, ROUTES.D03_PLAN_ANALYSIS, {
|
||||||
@@ -248,7 +249,27 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
|||||||
root.querySelector(".ui-workflow-layout")?.classList.add("d03-plan-layout");
|
root.querySelector(".ui-workflow-layout")?.classList.add("d03-plan-layout");
|
||||||
|
|
||||||
const allVertices = (): Vertex[] => network.lines.flatMap((line) => line.vertices);
|
const allVertices = (): Vertex[] => network.lines.flatMap((line) => line.vertices);
|
||||||
const snap = (): Snap => ({ network, points, pointNames });
|
const snap = (): Snap => ({ network, points, pointNames, site });
|
||||||
|
/** 위치를 옮기고 [저장] 전 — 받기 · 범위 물음에 새 기준점을 실음. */
|
||||||
|
const moved = (): TerrainCenter["center"] =>
|
||||||
|
site && JSON.stringify(site) !== JSON.stringify(savedSite)
|
||||||
|
? { lon: site.lon, lat: site.lat }
|
||||||
|
: null;
|
||||||
|
/** 기준점 둘레 ±1 km(작업 좌표) — 처음 맞춤 · 위치 옮김 · 임도망 맞춤. */
|
||||||
|
const around = (at: Site | null): Vertex[] => {
|
||||||
|
const tm = tmParams(epsg);
|
||||||
|
if (!at || !tm) return [];
|
||||||
|
const [x, y] = lonLatToTm(tm, at.lon, at.lat);
|
||||||
|
return [
|
||||||
|
[x - SITE_RADIUS_M, y - SITE_RADIUS_M],
|
||||||
|
[x + SITE_RADIUS_M, y + SITE_RADIUS_M],
|
||||||
|
];
|
||||||
|
};
|
||||||
|
const fitTo = (box: Vertex[]): void => {
|
||||||
|
if (!box.length) return;
|
||||||
|
map.update({ fit: box });
|
||||||
|
map.fit();
|
||||||
|
};
|
||||||
const dirty = (): boolean => JSON.stringify(snap()) !== savedJson;
|
const dirty = (): boolean => JSON.stringify(snap()) !== savedJson;
|
||||||
const keepDraft = (): void =>
|
const keepDraft = (): void =>
|
||||||
dirty()
|
dirty()
|
||||||
@@ -259,10 +280,11 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
|||||||
function step(next: Snap): void {
|
function step(next: Snap): void {
|
||||||
undoStack = [...undoStack.slice(1 - UNDO_MAX), snap()];
|
undoStack = [...undoStack.slice(1 - UNDO_MAX), snap()];
|
||||||
redoStack = [];
|
redoStack = [];
|
||||||
[network, points, pointNames] = [
|
[network, points, pointNames, site] = [
|
||||||
normalizePieces(next.network, yearStart),
|
normalizePieces(next.network, yearStart),
|
||||||
next.points,
|
next.points,
|
||||||
next.pointNames,
|
next.pointNames,
|
||||||
|
next.site,
|
||||||
];
|
];
|
||||||
keepDraft();
|
keepDraft();
|
||||||
refresh();
|
refresh();
|
||||||
@@ -270,14 +292,14 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function commit(next: PlanNetwork): void {
|
function commit(next: PlanNetwork): void {
|
||||||
step({ network: next, points, pointNames });
|
step({ network: next, points, pointNames, site });
|
||||||
}
|
}
|
||||||
|
|
||||||
function undo(): void {
|
function undo(): void {
|
||||||
const last = undoStack.pop();
|
const last = undoStack.pop();
|
||||||
if (!last) return;
|
if (!last) return;
|
||||||
redoStack.push(snap());
|
redoStack.push(snap());
|
||||||
({ network, points, pointNames } = last);
|
({ network, points, pointNames, site } = last);
|
||||||
keepDraft();
|
keepDraft();
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
@@ -286,7 +308,7 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
|||||||
const next = redoStack.pop();
|
const next = redoStack.pop();
|
||||||
if (!next) return;
|
if (!next) return;
|
||||||
undoStack.push(snap());
|
undoStack.push(snap());
|
||||||
({ network, points, pointNames } = next);
|
({ network, points, pointNames, site } = next);
|
||||||
keepDraft();
|
keepDraft();
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
@@ -315,14 +337,16 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
|||||||
const inside = (list: Vertex[]): boolean =>
|
const inside = (list: Vertex[]): boolean =>
|
||||||
!!have &&
|
!!have &&
|
||||||
list.every(([x, y]) => x >= have![0] && x <= have![2] && y >= have![1] && y <= have![3]);
|
list.every(([x, y]) => x >= have![0] && x <= have![2] && y >= have![1] && y <= have![3]);
|
||||||
const outside = (): boolean => !!have && !inside([...points, ...allVertices()]);
|
|
||||||
|
|
||||||
/** 조용히 받기(요구 453) — 지점 · 선이 받은 범위 밖이면 이어 받음(지도 보기만 옮김 = 안 받음 596-4) ·
|
/** 조용히 받기(요구 453) — 지점 · 선이 받은 범위 밖이면 이어 받음(지도 보기만 옮김 = 안 받음 596-4) ·
|
||||||
* 단추 · 막는 창 없음 · 실패 · 끊김 = [다시 받기] · 같은 요청은 한 번만. */
|
* 단추 · 막는 창 없음 · 실패 · 끊김 = [다시 받기] · 같은 요청은 한 번만. */
|
||||||
async function ensureCovered(): Promise<void> {
|
async function ensureCovered(again = false): Promise<void> {
|
||||||
if (!ready || !editable || busy || failed || !epsg || !root.isConnected) return;
|
if (!ready || !editable || busy || failed || !epsg || !root.isConnected) return;
|
||||||
const extra = [...points, ...allVertices()];
|
const [lo, hi] = around(site);
|
||||||
if (inside(extra)) return; // 받을 때 서버가 여유를 더함
|
const near = ([x, y]: Vertex): boolean =>
|
||||||
|
!lo || Math.hypot(x - (lo[0] + hi[0]) / 2, y - (lo[1] + hi[1]) / 2) <= COVER_MAX_M;
|
||||||
|
const extra = [...points, ...allVertices()].filter(near);
|
||||||
|
if (!again && inside(extra)) return; // 받을 때 서버가 여유를 더함
|
||||||
const key = JSON.stringify([have, extra]);
|
const key = JSON.stringify([have, extra]);
|
||||||
if (key === lastTried) return;
|
if (key === lastTried) return;
|
||||||
lastTried = key;
|
lastTried = key;
|
||||||
@@ -343,34 +367,89 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
|||||||
props.render();
|
props.render();
|
||||||
pieces.render();
|
pieces.render();
|
||||||
marks.render();
|
marks.render();
|
||||||
retryButton.hidden = !editable || busy || !failed;
|
siteBox.render({
|
||||||
roadButton.hidden = !editable;
|
address: site?.address ?? null,
|
||||||
if (!busy)
|
note: failed ? `지형 받기 실패 — ${failed}` : note,
|
||||||
terrainText.textContent = failed
|
retry: !busy && !!failed,
|
||||||
? `지형 받기 실패 — ${failed}`
|
editable,
|
||||||
: !surface
|
});
|
||||||
? editable
|
|
||||||
? "받은 지형 없음 — 기준점 둘레를 받습니다(길이 · 기울기는 지형을 받은 뒤)."
|
|
||||||
: "받은 지형 없음"
|
|
||||||
: outside()
|
|
||||||
? "지점 또는 그은 선이 받은 지형 밖 — 이어 받습니다."
|
|
||||||
: "받은 지형 안";
|
|
||||||
saveButton.hidden = resetButton.hidden = !editable;
|
saveButton.hidden = resetButton.hidden = !editable;
|
||||||
saveButton.disabled = busy || !dirty();
|
saveButton.disabled = busy || !dirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function download(extra: Vertex[]): Promise<void> {
|
/** 받기 — 멂(받은 범위에서 5 km 넘음) · 다른 TM 띠 = 물음 한 번 뒤 옛 범위 버리고 새로(`reset`) ·
|
||||||
|
* 아니오 = 안 받음(거짓). 이미 도는 일(다른 창)이면 그것을 지켜봄. */
|
||||||
|
async function download(extra: Vertex[]): Promise<boolean> {
|
||||||
busy = true; // 요청이 가는 동안 다른 받기를 막음(지켜보기가 이어 받음)
|
busy = true; // 요청이 가는 동안 다른 받기를 막음(지켜보기가 이어 받음)
|
||||||
|
const where: TerrainCenter = { center: moved() };
|
||||||
try {
|
try {
|
||||||
await startTerrain(projectId, null, extra);
|
let answer: TerrainStart = await startTerrain(projectId, null, extra, where);
|
||||||
|
if (answer.status === "far" || answer.status === "band") {
|
||||||
|
const band = answer.status === "band";
|
||||||
|
const ok = await askConfirm({
|
||||||
|
title: band ? "다른 좌표 띠로 옮김" : "멀리 옮김",
|
||||||
|
message: band
|
||||||
|
? `${answer.message} 그은 선 · 지점을 지우고 새 자리 둘레를 받습니다.`
|
||||||
|
: `${answer.message} 받은 지형을 버리고 새 자리 둘레를 받습니다.`,
|
||||||
|
okLabel: "새로 받기",
|
||||||
|
danger: band,
|
||||||
|
});
|
||||||
|
if (!ok) {
|
||||||
|
busy = false;
|
||||||
|
refresh();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (answer.status === "band") clearForBand(answer.center_epsg);
|
||||||
|
answer = await startTerrain(projectId, null, [], { ...where, reset: true });
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 이미 도는 일(다른 창)이면 그것을 지켜봄
|
|
||||||
if (!/이미 돌고/.test(errorText(error))) {
|
if (!/이미 돌고/.test(errorText(error))) {
|
||||||
busy = false;
|
busy = false;
|
||||||
return void showToast(errorText(error), "error");
|
refresh();
|
||||||
|
showToast(errorText(error), "error");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await follow();
|
await follow();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 다른 TM 띠 — 작업 좌표가 바뀜 · 선 · 지점을 걷고 되돌리기를 비움(옛 좌표라 못 되돌림). */
|
||||||
|
function clearForBand(next: number): void {
|
||||||
|
epsg = next;
|
||||||
|
[network, points, pointNames] = [emptyNetwork(), [], []];
|
||||||
|
[undoStack, redoStack, have] = [[], [], null];
|
||||||
|
map.update({ epsg });
|
||||||
|
keepDraft();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 위치 칸 고름 — 기준점 캐시(한 걸음 · [저장] 때 정본) → 지도 이동 → 둘레 받기(멂 · 띠 = 물음 · 아니오 = 되돌림). */
|
||||||
|
async function pickSite(hit: PlaceHit): Promise<void> {
|
||||||
|
if (busy) return void showToast("지형을 받는 중 — 끝난 뒤 다시 고르세요.", "info");
|
||||||
|
epsg ||= hit.lon < 126 ? 5185 : hit.lon < 128 ? 5186 : 5187; // 기준점 없던 프로젝트(서버 띠 규칙)
|
||||||
|
[note, failed, lastTried, ready] = ["", null, "", false]; // 걸음의 조용히 받기는 막고 아래에서 한 번
|
||||||
|
step({ ...snap(), site: { lon: hit.lon, lat: hit.lat, address: hit.address || hit.title } });
|
||||||
|
ready = true;
|
||||||
|
fitTo(around(site));
|
||||||
|
if (await download([])) return; // 새 자리 둘레(center) — 지점 · 선은 끝난 뒤 조용히 받기가
|
||||||
|
undo(); // 물음에 아니오 · 받기 못 함 = 위치 그대로
|
||||||
|
redoStack.pop();
|
||||||
|
fitTo(around(site));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 임도망 올리기 — 지도 맞춤 = 주소지 둘레 ∪ 잘린 임도 상자(없으면 주소지). */
|
||||||
|
async function uploadRoads(files: File[]): Promise<void> {
|
||||||
|
const info = await backdrop.uploadRoads(files);
|
||||||
|
const tm = tmParams(epsg);
|
||||||
|
if (!info) return;
|
||||||
|
const roads: Vertex[] =
|
||||||
|
tm && info.bounds
|
||||||
|
? [
|
||||||
|
lonLatToTm(tm, info.bounds[0], info.bounds[1]),
|
||||||
|
lonLatToTm(tm, info.bounds[2], info.bounds[3]),
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
fitTo([...around(site), ...roads]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 도는 받기를 지켜보고 끝나면 지형 · 바탕을 다시 읽음. */
|
/** 도는 받기를 지켜보고 끝나면 지형 · 바탕을 다시 읽음. */
|
||||||
@@ -432,14 +511,16 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
|||||||
? fromServer(server.lines, server.marks, server.pieces, yearStart)
|
? fromServer(server.lines, server.marks, server.pieces, yearStart)
|
||||||
: emptyNetwork();
|
: emptyNetwork();
|
||||||
points = view.base.points.map((p): Vertex => [p.x, p.y]);
|
points = view.base.points.map((p): Vertex => [p.x, p.y]);
|
||||||
pointNames = view.base.points.map(() => ""); // 594-1 이름 칸이 들어오면 `p.name ?? ""`
|
pointNames = view.base.points.map((p) => p.name ?? "");
|
||||||
|
site = savedSite = view.base.site;
|
||||||
if (view.base.epsg) epsg = view.base.epsg;
|
if (view.base.epsg) epsg = view.base.epsg;
|
||||||
savedJson = JSON.stringify(snap());
|
savedJson = JSON.stringify(snap());
|
||||||
}
|
}
|
||||||
|
|
||||||
function serverBody(): Parameters<typeof savePlanNetwork>[1] {
|
function serverBody(): Parameters<typeof savePlanNetwork>[1] {
|
||||||
const { lines, pieces } = network;
|
const { lines, pieces } = network;
|
||||||
return { epsg, points, lines, marks: marksOf(network), pieces };
|
const body = { epsg, points, point_names: pointNames, lines, marks: marksOf(network), pieces };
|
||||||
|
return moved() && site ? { ...body, site } : body;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** [저장] — 서버가 다시 셈 · 실패면 그대로(캐시 남음). 참 = 저장됨. */
|
/** [저장] — 서버가 다시 셈 · 실패면 그대로(캐시 남음). 참 = 저장됨. */
|
||||||
@@ -476,7 +557,7 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
|||||||
draw.setMode("idle");
|
draw.setMode("idle");
|
||||||
showLoadingOverlay();
|
showLoadingOverlay();
|
||||||
try {
|
try {
|
||||||
const body = { epsg, points, lines: [], marks: [], pieces: [] };
|
const body = { epsg, points, point_names: pointNames, lines: [], marks: [], pieces: [] };
|
||||||
takeServer(await savePlanNetwork(projectId, body));
|
takeServer(await savePlanNetwork(projectId, body));
|
||||||
clearState("d03-plan", projectId);
|
clearState("d03-plan", projectId);
|
||||||
undoStack = [];
|
undoStack = [];
|
||||||
@@ -509,6 +590,8 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
|||||||
base: () => points,
|
base: () => points,
|
||||||
have: () => have,
|
have: () => have,
|
||||||
busy: () => busy,
|
busy: () => busy,
|
||||||
|
site: () => site,
|
||||||
|
pickSite,
|
||||||
lineNames: () => lineNames(network.lines),
|
lineNames: () => lineNames(network.lines),
|
||||||
screenOf: map.screenOf,
|
screenOf: map.screenOf,
|
||||||
screenOfLonLat: map.screenOfLonLat,
|
screenOfLonLat: map.screenOfLonLat,
|
||||||
@@ -555,26 +638,18 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
|||||||
editable = !!(await fetchSessionUser())?.is_forest_service;
|
editable = !!(await fetchSessionUser())?.is_forest_service;
|
||||||
draw.setEditable(editable);
|
draw.setEditable(editable);
|
||||||
const view = await fetchPlanNetwork(projectId);
|
const view = await fetchPlanNetwork(projectId);
|
||||||
const { epsg: workEpsg, site } = view.base;
|
if (!view.base.epsg) {
|
||||||
if (!workEpsg) {
|
note = "기준점이 없습니다 — 주소를 찾아 정하세요.";
|
||||||
terrainText.textContent = "기준점이 없습니다 — 예정노선 등록에서 정하세요.";
|
ready = true;
|
||||||
return;
|
return refresh();
|
||||||
}
|
}
|
||||||
epsg = workEpsg;
|
epsg = view.base.epsg;
|
||||||
takeServer(view);
|
takeServer(view);
|
||||||
const draft = readState<Draft>("d03-plan", projectId);
|
const draft = readState<Draft>("d03-plan", projectId);
|
||||||
if (draft?.version === DRAFT_VERSION && draft.network && Array.isArray(draft.points))
|
if (draft?.version === DRAFT_VERSION && draft.network && Array.isArray(draft.points))
|
||||||
({ network, points, pointNames } = draft);
|
({ network, points, pointNames, site } = draft);
|
||||||
const tm = tmParams(epsg);
|
|
||||||
const siteXy = site && tm ? lonLatToTm(tm, site.lon, site.lat) : null;
|
|
||||||
const around: Vertex[] = siteXy
|
|
||||||
? [
|
|
||||||
[siteXy[0] - SITE_RADIUS_M, siteXy[1] - SITE_RADIUS_M],
|
|
||||||
[siteXy[0] + SITE_RADIUS_M, siteXy[1] + SITE_RADIUS_M],
|
|
||||||
]
|
|
||||||
: [];
|
|
||||||
const drawn = [...points, ...allVertices()];
|
const drawn = [...points, ...allVertices()];
|
||||||
map.update({ fit: drawn.length ? drawn : around, epsg });
|
map.update({ fit: drawn.length && !moved() ? drawn : around(site), epsg });
|
||||||
refresh();
|
refresh();
|
||||||
await loadSurface();
|
await loadSurface();
|
||||||
refresh();
|
refresh();
|
||||||
@@ -585,12 +660,13 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
|||||||
await follow();
|
await follow();
|
||||||
} else if (editable && !job.error) {
|
} else if (editable && !job.error) {
|
||||||
// 들어올 때 받은 범위가 기준점 둘레 · 지점을 못 덮으면 바로 받기(서버가 셈 · 실패 뒤엔 [다시 받기])
|
// 들어올 때 받은 범위가 기준점 둘레 · 지점을 못 덮으면 바로 받기(서버가 셈 · 실패 뒤엔 [다시 받기])
|
||||||
const extent = await fetchTerrainExtent(projectId, null, []);
|
const extent = await fetchTerrainExtent(projectId, null, [], moved());
|
||||||
ready = true;
|
ready = true;
|
||||||
if (!extent.covered) await download([]);
|
if (!extent.covered) await download([]);
|
||||||
else await ensureCovered();
|
else await ensureCovered();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
terrainText.textContent = `불러오지 못했습니다 — ${errorText(error)}`;
|
note = `불러오지 못했습니다 — ${errorText(error)}`;
|
||||||
|
refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* D03_PlanAnalysis_UI_Site.ts
|
||||||
|
* D03 「위치」 칸(PLAN 596-2 · 요구 463-3~6 · 19) — 지금 기준점 주소 한 줄 · 주소 · 장소 찾기
|
||||||
|
* (서버 VWorld `searchPlaces` · 부품 `createFindList` · Enter = 찾기 · ↑ ↓ Enter = 고름) ·
|
||||||
|
* 알림 한 줄(받기 실패 · 끊김 · 기준점 없음 · 못 불러옴만) + [다시 받기] · [임도망 올리기].
|
||||||
|
* 고름 = 페이지가 받기 물음(멂 · 다른 TM 띠) → 기준점 캐시 → 지도 이동 → 둘레 받기 · [저장] 때 정본(등록과 한 칸).
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
import { pickFiles } from "@ui/ui_template_drop";
|
||||||
|
import { createButton } from "@ui/ui_template_elements";
|
||||||
|
import { createFindList, type FindList } from "@ui/ui_template_find_list";
|
||||||
|
import { t } from "@ui/ui_template_locale";
|
||||||
|
import { createText } from "@ui/ui_template_text";
|
||||||
|
import { searchPlaces, type PlaceHit } from "./D03_PlanAnalysis_Api";
|
||||||
|
|
||||||
|
/** 찾기 글 최소 길이(서버 VWorld 한 글자 = 쓸모없는 후보). */
|
||||||
|
const MIN_QUERY = 2;
|
||||||
|
|
||||||
|
export interface PlanSiteParams {
|
||||||
|
/** 후보 고름 — 받기 · 캐시 · 이동은 페이지. */
|
||||||
|
onPick: (hit: PlaceHit) => void;
|
||||||
|
onRetry: () => void;
|
||||||
|
onUpload: (files: File[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlanSiteState {
|
||||||
|
address: string | null;
|
||||||
|
/** 알림(실패 · 끊김 · 기준점 없음 · 오류) — 빈 값 = 숨김. */
|
||||||
|
note: string;
|
||||||
|
retry: boolean;
|
||||||
|
editable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlanSite {
|
||||||
|
/** 칸 몸(카드에 바로 붙임 · 카드 간격을 그대로 씀). */
|
||||||
|
parts: HTMLElement[];
|
||||||
|
render: (state: PlanSiteState) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 주소 · 장소 찾기 칸(부품 `createFindList`) — Enter = 찾기 · ↑ ↓ Enter · 누름 = 고름 ·
|
||||||
|
* 등록(D01) 지도 머리 · 이 「위치」 칸이 같이 씀. */
|
||||||
|
export function createPlaceFind(onPick: (hit: PlaceHit) => void): FindList {
|
||||||
|
const find = createFindList({ placeholder: t("DE_Reg_Find") });
|
||||||
|
let asked = 0; // 옛 찾기 답 버림
|
||||||
|
|
||||||
|
async function run(): Promise<void> {
|
||||||
|
const query = find.search.value.trim();
|
||||||
|
find.setRows([]);
|
||||||
|
if (query.length < MIN_QUERY) return void (find.hint.textContent = t("DE_Reg_FindShort"));
|
||||||
|
const at = ++asked;
|
||||||
|
find.hint.textContent = t("DE_Reg_FindBusy");
|
||||||
|
try {
|
||||||
|
const hits = await searchPlaces(query);
|
||||||
|
if (at !== asked) return;
|
||||||
|
find.hint.textContent = hits.length ? "" : t("DE_Reg_FindNone");
|
||||||
|
find.setRows(
|
||||||
|
hits.map((hit, i) => ({
|
||||||
|
key: String(i),
|
||||||
|
tag: t(hit.kind === "place" ? "DE_Reg_FindPlace" : "DE_Reg_FindAddress"),
|
||||||
|
name: hit.title,
|
||||||
|
sub: hit.address !== hit.title ? hit.address : undefined,
|
||||||
|
onPick: () => {
|
||||||
|
find.setRows([]);
|
||||||
|
find.search.value = hit.title;
|
||||||
|
onPick(hit);
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (at === asked)
|
||||||
|
find.hint.textContent = `${t("DE_Reg_FindFail")} — ${error instanceof Error ? error.message : error}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 고를 줄이 있으면 부품이 Enter 를 먼저 씀(고름) · 없으면 찾기 · 글을 고치면 옛 줄 걷음
|
||||||
|
find.search.addEventListener("input", () => find.setRows([]));
|
||||||
|
find.search.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key !== "Enter" || event.defaultPrevented || event.isComposing) return;
|
||||||
|
event.preventDefault();
|
||||||
|
void run();
|
||||||
|
});
|
||||||
|
return find;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPlanSite(params: PlanSiteParams): PlanSite {
|
||||||
|
const address = createText("", "muted", 14);
|
||||||
|
const note = createText("", "error");
|
||||||
|
const find = createPlaceFind(params.onPick);
|
||||||
|
|
||||||
|
const retry = createButton({
|
||||||
|
label: "다시 받기",
|
||||||
|
variant: "filled",
|
||||||
|
size: 28,
|
||||||
|
onClick: params.onRetry,
|
||||||
|
});
|
||||||
|
const upload = createButton({
|
||||||
|
label: "임도망 올리기",
|
||||||
|
variant: "ghost",
|
||||||
|
size: 28,
|
||||||
|
onClick: () =>
|
||||||
|
pickFiles({ accept: ".zip,.shp,.shx,.dbf,.prj,.cpg", multiple: true }, params.onUpload),
|
||||||
|
});
|
||||||
|
upload.title = "임도망도 SHP(시군구 zip 하나 · 또는 shp · shx · dbf · prj 함께 · 100 MB 까지)";
|
||||||
|
return {
|
||||||
|
parts: [address, find.root, note, retry, upload],
|
||||||
|
render(state) {
|
||||||
|
address.textContent = state.address ?? "";
|
||||||
|
address.hidden = !state.address;
|
||||||
|
note.textContent = state.note;
|
||||||
|
note.hidden = !state.note;
|
||||||
|
find.root.hidden = upload.hidden = !state.editable;
|
||||||
|
retry.hidden = !state.editable || !state.retry;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -35,6 +35,13 @@ def test_d03_parts():
|
|||||||
layers = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_Layers.ts")
|
layers = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_Layers.ts")
|
||||||
assert 'createElement("progress")' not in ts and "createProgressBar(" not in ts
|
assert 'createElement("progress")' not in ts and "createProgressBar(" not in ts
|
||||||
assert "createProgressCircle({ size: 40, corner: true })" in ts # 596-3 받기 = 지도 귀퉁이 서클
|
assert "createProgressCircle({ size: 40, corner: true })" in ts # 596-3 받기 = 지도 귀퉁이 서클
|
||||||
|
site = _src(
|
||||||
|
"D03_PlanAnalysis/D03_PlanAnalysis_UI_Site.ts"
|
||||||
|
) # 596-2 위치 칸 · 596-5 등록 찾기 = 같은 부품
|
||||||
|
assert "createFindList(" in site and "searchPlaces(" in site and "createPlanSite(" in ts
|
||||||
|
assert "createPlaceFind(" in _src("D01_PlanRegister/D01_PlanRegister_UI_Page.ts")
|
||||||
|
for raw in ('createElement("button")', 'createElement("input")'):
|
||||||
|
assert raw not in site, raw
|
||||||
# 562-3 — 날 단추 · 입력 · 고르기 없음 · 도구줄 · 제약 칸 · 연도 고름은 부품
|
# 562-3 — 날 단추 · 입력 · 고르기 없음 · 도구줄 · 제약 칸 · 연도 고름은 부품
|
||||||
for src in (ts, draw, draw_panel, tools, layers):
|
for src in (ts, draw, draw_panel, tools, layers):
|
||||||
for raw in ('createElement("button")', 'createElement("input")', 'createElement("select")'):
|
for raw in ('createElement("button")', 'createElement("input")', 'createElement("select")'):
|
||||||
@@ -89,8 +96,12 @@ def test_d03_parts():
|
|||||||
props = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_Props.ts") # 585-2 조각 연도 = 속성 판
|
props = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_Props.ts") # 585-2 조각 연도 = 속성 판
|
||||||
assert "createSelectField(" in props and "createFloatPanel(" in props
|
assert "createSelectField(" in props and "createFloatPanel(" in props
|
||||||
assert 'createElement("button")' not in props and 'createElement("select")' not in props
|
assert 'createElement("button")' not in props and 'createElement("select")' not in props
|
||||||
assert 'createElement("select")' not in three and "createSelectField" not in three # 601 선 고름 걷음
|
assert (
|
||||||
assert "createCheckbox" not in three and "setCorridors(" in three # 601 노선망 예상 코리도 한 번에
|
'createElement("select")' not in three and "createSelectField" not in three
|
||||||
|
) # 601 선 고름 걷음
|
||||||
|
assert (
|
||||||
|
"createCheckbox" not in three and "setCorridors(" in three
|
||||||
|
) # 601 노선망 예상 코리도 한 번에
|
||||||
for dead in (".d03-plan__bar", ".d03-plan__mode", "is-picked"):
|
for dead in (".d03-plan__bar", ".d03-plan__mode", "is-picked"):
|
||||||
assert dead not in css, dead
|
assert dead not in css, dead
|
||||||
assert "b05-route__3d-modal" not in three and "b05-route__3d-body" not in three # 빌림 끊음
|
assert "b05-route__3d-modal" not in three and "b05-route__3d-body" not in three # 빌림 끊음
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ def test_de_pages_guard_leave():
|
|||||||
if name:
|
if name:
|
||||||
assert f'discard: () => clearState("{name}", projectId)' in call, path
|
assert f'discard: () => clearState("{name}", projectId)' in call, path
|
||||||
# D03 — 못 읽은 채(프로젝트 없음 · 불러오기 실패) 떠나면 고침 없음
|
# D03 — 못 읽은 채(프로젝트 없음 · 불러오기 실패) 떠나면 고침 없음
|
||||||
assert "let savedJson = JSON.stringify({ network, points });" in _src(
|
assert "let savedJson = JSON.stringify({ network, points, pointNames, site });" in _src(
|
||||||
"D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts"
|
"D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts"
|
||||||
)
|
)
|
||||||
e03 = _src("E03_StabSection/E03_StabSection_UI_Page.ts")
|
e03 = _src("E03_StabSection/E03_StabSection_UI_Page.ts")
|
||||||
|
|||||||
@@ -255,7 +255,9 @@ def test_screen_defs() -> None:
|
|||||||
assert entry and entry.group(2) == "false", key # 처음 값 끔
|
assert entry and entry.group(2) == "false", key # 처음 값 끔
|
||||||
assert re.search(rf"^\s+{entry.group(1)}:", icons, re.M), key
|
assert re.search(rf"^\s+{entry.group(1)}:", icons, re.M), key
|
||||||
assert f"--draw-map-{color}:" in theme and f'"--draw-map-{color}"' in page
|
assert f"--draw-map-{color}:" in theme and f'"--draw-map-{color}"' in page
|
||||||
assert re.search(r"^\s+upload:", icons, re.M) and "pickFiles(" in page # 부품 상자 고르기 창
|
site = (folder / "D03_PlanAnalysis_UI_Site.ts").read_text("utf-8") # 596-2 올리기 = 위치 칸
|
||||||
|
assert "pickFiles(" in site and "pickFiles(" not in page # 부품 상자 고르기 창
|
||||||
|
assert "uploadRoads" in page and "uploadPlanForestRoads(" in page
|
||||||
assert "<input" not in page and 'createElement("input")' not in page
|
assert "<input" not in page and 'createElement("input")' not in page
|
||||||
assert paint.count("base: true,") == paint.count("draw: (context, map) =>") # 겹 모두 바탕
|
assert paint.count("base: true,") == paint.count("draw: (context, map) =>") # 겹 모두 바탕
|
||||||
assert "임도 짐작" in legend and "올린 임도망도" in legend # 범례 구분
|
assert "임도 짐작" in legend and "올린 임도망도" in legend # 범례 구분
|
||||||
|
|||||||
@@ -102,6 +102,13 @@ export const ui_locales_de = {
|
|||||||
DE_Reg_SiteNoAddress: ["주소를 찾지 못했습니다", "Address not found"],
|
DE_Reg_SiteNoAddress: ["주소를 찾지 못했습니다", "Address not found"],
|
||||||
DE_Reg_SiteSource: ["출처", "Source"],
|
DE_Reg_SiteSource: ["출처", "Source"],
|
||||||
DE_Reg_SiteRequired: ["지도에서 기준점을 찍으세요.", "Set the reference point on the map."],
|
DE_Reg_SiteRequired: ["지도에서 기준점을 찍으세요.", "Set the reference point on the map."],
|
||||||
|
DE_Reg_Find: ["주소 · 장소 찾기 (Enter)", "Find address or place (Enter)"],
|
||||||
|
DE_Reg_FindShort: ["두 글자 이상", "Two or more characters"],
|
||||||
|
DE_Reg_FindBusy: ["찾는 중…", "Searching…"],
|
||||||
|
DE_Reg_FindNone: ["찾은 곳 없음", "Nothing found"],
|
||||||
|
DE_Reg_FindFail: ["찾지 못함", "Search failed"],
|
||||||
|
DE_Reg_FindAddress: ["주소", "Address"],
|
||||||
|
DE_Reg_FindPlace: ["장소", "Place"],
|
||||||
DE_Reg_MapType: ["바탕 지도", "Base map"],
|
DE_Reg_MapType: ["바탕 지도", "Base map"],
|
||||||
DE_Reg_MapNormal: ["일반", "Map"],
|
DE_Reg_MapNormal: ["일반", "Map"],
|
||||||
DE_Reg_MapSatellite: ["위성", "Satellite"],
|
DE_Reg_MapSatellite: ["위성", "Satellite"],
|
||||||
|
|||||||
Reference in New Issue
Block a user