feat(D03): 연도 구역 마커 끌어 놓기 · 선 밖으로 끌어내 지움 · 긋는 해 걷음 · 갈래 시점 지정 (PLAN 585-3 · 요구 445 A6 · 446)

- 도구줄 「연도 마커」(공용 지도 도구줄 year-mark · 깃발 아이콘)를 선 위로 끌어 놓음 = 그 자리 구간을 둘로 — 구역만 나눔 · 해 그대로
- 끄는 동안 마름모 미리보기 · 선 끝 · 다른 마커 1 m 안 · 선 밖 = 경고 색 · 못 놓음
- 놓인 마커 끌기 = 경계 옮김 · 선 밖으로 끌어냄 · 오른쪽 누름 · Delete = 지움(앞 구간과 합침) · 같은 해 양쪽 = 속 빈 마름모
- 좌측 [연도 구분] · 「긋는 해」 걷음 · 새 선 = 사업연도 시작 해 · 새 갈래 = 붙은 자리 구간 해
- 속성 판 [갈래 시점 지정] = 다른 선에 닿은 끝을 시점(No.0)으로 갈래 · 꼬리에 닿은 머리 = 한 선 · 본선 = [방향 바꾸기]
- 시험: 585-2 시험에 나눔 · 합침 · 갈래 해 · 갈래 시점 덧 · 530-3 갈래 해 · 553-4 속성 판 · 568-1 도구줄 목록 맞춤

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-29 18:51:04 +09:00
co-authored by Claude Opus 5.5
parent e6951b5c73
commit cc909d107e
13 changed files with 259 additions and 103 deletions
+8 -60
View File
@@ -9,9 +9,9 @@
* 미리보기 = 마지막 점 → 마우스 길이(m) · 가상 종단 기울기(원지반 % · ° · + 오르막) — 제약 칸 넷:
* 자유 · 길이 고정 · 기울기 고정(기울기 고정 · 길이 자유 · 마우스 방향 첫 자리) · 둘 다(양각기 원 위 후보 중
* 마우스에 가까운 것) · 제약 기울기 = 오르내림 같은 크기(562-1 짝이 |기울기|로 셈).
* 긋는 해 = 새 조각 처음 해 · 긋는 중 해를 바꾸면 다음 점부터 그 해(지금 끝 자리에 연도 구분 마크).
* 연도 구분 = 선 위 누름(마크 · 뒤 조각 고름 → 페이지가 연도 고름을 엶) · 오른쪽 = 지움.
* 고르기 · 고치기(쉴 때) = `D03_PlanAnalysis_UI_Edit`.
* 긋기는 해를 안 물음(585-3) — 새 선 = 사업연도 시작 해 · 갈래 = 붙은 자리 구간 해 · 이어 긋기 = 끝 구간이 늘어남 ·
* 여러 해 = 다 그은 뒤 도구줄 「연도 마커」를 선 위로 끌어 구역을 나누고 속성 판에서 해를 고름.
* 고르기 · 고치기 · 마커(쉴 때) = `D03_PlanAnalysis_UI_Edit`.
* 키 = Enter 끝 · Esc 이번 긋기 취소 · Backspace 마지막 점 빼기(설정 창 단축키 표 D03 줄).
* 셈 = 562-1 짝(`logic/D03_PlanAnalysis_Draw`) · 꼴 다루기 = `logic/D03_PlanAnalysis_Plan` ·
* 값을 바꾸면 `commit(다음 꼴)` 한 번 = 되돌리기 한 걸음(스택 · 캐시 = 페이지).
@@ -27,7 +27,6 @@ import {
fixedLengthPoint,
fixedSlopePoint,
groundSlopePct,
lineLength,
segmentLength,
} from "./logic/D03_PlanAnalysis_Draw";
import {
@@ -37,13 +36,9 @@ import {
dropLastVertex,
insertVertex,
newLine,
normalizePieces,
removeLine,
removeMark,
reverseLine,
samePoint,
setPieceYear,
splitPiece,
type PlanNetwork,
type Vertex,
} from "./logic/D03_PlanAnalysis_Plan";
@@ -63,7 +58,7 @@ export const DRAW_DEFAULT_LENGTH_M = 20;
export const DRAW_DEFAULT_SLOPE_PCT = 9;
export type DrawConstraint = "free" | "length" | "slope" | "both";
export type DrawMode = "idle" | "draw" | "mark";
export type DrawMode = "idle" | "draw";
export interface DrawPreview {
point: Vertex;
@@ -83,12 +78,8 @@ export interface PlanDrawParams {
network: () => PlanNetwork;
heightAt: () => (x: number, y: number) => number;
commit: (next: PlanNetwork) => void;
/** 긋는 해 — 새 조각 처음 해 · 긋는 중 바꾸면 다음 점부터. */
year: () => number;
/** 모드 · 그리는 선이 바뀜 — 페이지가 단추 · 안내를 다시. */
onMode: () => void;
/** 연도 구분 마크를 놓음 — 뒤 조각(id) 고름 · 연도 고름. */
onMark: (piece: string) => void;
}
export interface PlanDraw {
@@ -128,15 +119,9 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
size: 28,
onClick: () => setMode(mode === "draw" ? "idle" : "draw"),
});
const markButton = createButton({
label: "연도 구분",
variant: "ghost",
size: 28,
onClick: () => setMode(mode === "mark" ? "idle" : "mark"),
});
const modeRow = document.createElement("div");
modeRow.className = "d03-plan__row";
modeRow.append(drawButton, markButton);
modeRow.append(drawButton);
const kindSegment = createSegment({
options: [
{ value: "free", label: "자유" },
@@ -203,12 +188,9 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
}
function panelState(): void {
const network = params.network();
drawButton.querySelector(".ui-btn__label")!.textContent = mode === "draw" ? "긋기 끝" : "긋기";
drawButton.disabled = !editable;
markButton.disabled = !editable || !network.lines.some((line) => lineLength(line.vertices) > 0);
drawButton.setAttribute("aria-pressed", String(mode === "draw"));
markButton.setAttribute("aria-pressed", String(mode === "mark"));
panel.dataset.mode = mode;
hint.textContent = !editable
? "보기만 — 산림청 계정에서 긋습니다."
@@ -216,9 +198,7 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
? active === null
? "지점 · 선 끝 · 선 위를 눌러 시작합니다."
: "누름 = 점 · Enter 끝 · Esc 취소 · Backspace 마지막 점 빼기"
: mode === "mark"
? "선 위를 눌러 연도를 나눕니다 · 오른쪽 누름 = 마크 지움"
: "[긋기]로 지점 · 선에서 긋습니다 · 선을 누르면 고릅니다.";
: "[긋기]로 지점 · 선에서 긋습니다 · 선 누름 = 고름 · 「연도 마커」를 선 위로 끌면 구역을 나눕니다.";
}
fieldsState();
@@ -395,21 +375,6 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
return false;
}
/** 긋는 해가 끝 조각 해와 다르면 `fromM`(이번 점 앞 길이)에서 나눠 새 조각 = 긋는 해. */
function yearSplit(network: PlanNetwork, line: number, fromM: number): PlanNetwork {
const year = params.year();
if (!(fromM > 0)) return network;
let next = normalizePieces(network, year);
const id = next.lines[line].id;
const tail = next.pieces.filter((piece) => piece.line === id).at(-1);
if (!tail || tail.year === year) return next;
next = splitPiece(next, id, fromM);
const index = next.pieces.findIndex(
(piece) => piece.line === id && Math.abs(piece.from_m - fromM) < 1e-3,
);
return index >= 0 ? setPieceYear(next, index, year) : next;
}
function addPoint(): boolean {
if (active === null || !preview) return false;
if (preview.blocked) {
@@ -419,11 +384,9 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
const line = active;
const ended = preview.snapped;
let next = params.network();
const before = Math.round(lineLength(next.lines[line].vertices) * 1000) / 1000;
if (preview.onLine)
next = insertVertex(next, preview.onLine.line, preview.onLine.segment, preview.point);
next = yearSplit(appendVertex(next, line, preview.point), line, before);
params.commit(next);
params.commit(appendVertex(next, line, preview.point));
if (ended) {
showToast(`${preview.note ?? "붙음"} — 긋기 끝`, "success");
setMode("idle");
@@ -458,21 +421,6 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
preview = previewAt(px, py, map);
return addPoint();
}
if (mode === "mark") {
const network = params.network();
const hit = lineAt(network, px, py, map);
if (!hit) return false;
const id = network.lines[hit.line].id;
const next =
button === 2
? removeMark(network, id, hit.atM, SNAP_PX / map.pxPerMeter)
: splitPiece(network, id, hit.atM);
if (next === network) return true;
const added = next.pieces.find((piece) => !network.pieces.some((p) => p.id === piece.id));
params.commit(next);
if (added) params.onMark(added.id);
return true;
}
return false;
},
hover(px, py, map) {
@@ -500,7 +448,7 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
setMode("idle");
return true;
}
if (mode !== "idle" && event.key === "Escape") {
if (mode === "draw" && event.key === "Escape") {
if (mode === "draw" && strokeStart && strokeStart !== params.network())
params.commit(strokeStart);
active = null; // 되돌린 꼴이라 stop 의 버림 · 뒤집기는 안 함