From b2ad4f7e406b10d08fb3fae5a7c53f4034ad8fd2 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 11 Oct 2026 16:33:24 +0900 Subject: [PATCH] =?UTF-8?q?feat(B08):=20262=20=EC=83=81=EC=9E=90=20?= =?UTF-8?q?=EC=B0=BD=20=EC=A0=9C=EB=AA=A9=20=EC=9D=B4=EB=A6=84=EB=A7=8C=20?= =?UTF-8?q?=C2=B7=20=EC=86=8C=EC=9E=AC=EC=A7=80=20=EC=A7=80=EB=8F=84=20?= =?UTF-8?q?=EC=8B=9C=EC=A0=90=20=ED=91=9C=EC=A7=80=20=EA=B3=A0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 거래처 · 기준점 창 제목 = 이름만 · 시점 창 = 「시점 · 중점」 - 소재지 지도에 시점 표지(고정 · 끌기 없음) · 소재지 없으면 시점 중심 Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_018jonfdxn1Xit9bdkNJpE4G --- B08_Quantity/B08_Quantity_UI_Milestone.css | 13 ++++++ .../B08_Quantity_UI_Milestone_KakaoMap.ts | 18 ++++++++- .../B08_Quantity_UI_Milestone_Modal.ts | 6 +-- .../B08_Quantity_UI_Milestone_Place.ts | 34 ++++++++++------ .../test_254_3_b08_milestone_box_modal_pic.py | 4 +- ...est_262_1_b08_milestone_title_start_pin.py | 40 +++++++++++++++++++ 6 files changed, 95 insertions(+), 20 deletions(-) create mode 100644 resources/tester/test_262_1_b08_milestone_title_start_pin.py diff --git a/B08_Quantity/B08_Quantity_UI_Milestone.css b/B08_Quantity/B08_Quantity_UI_Milestone.css index 4d0df3b34..09b24921d 100644 --- a/B08_Quantity/B08_Quantity_UI_Milestone.css +++ b/B08_Quantity/B08_Quantity_UI_Milestone.css @@ -204,6 +204,19 @@ white-space: nowrap; } +/* 262-2 고정 시점 표지 — 둥근 붉은 알약(고르는 표지 = 기본 핀 + 흰 이름표와 구별) */ +.b08-milestone__map-start { + padding: 3px 10px; + border: 2px solid #fff; + border-radius: 999px; + background: #e60046; + color: #fff; + font-size: 12px; + font-weight: 700; + white-space: nowrap; + pointer-events: none; +} + /* 등록 · 연결선 모달(244-13) — 더 큰 창 · 2열 칸 */ .ui-modal.b08-milestone__modal { width: min(960px, 94vw); diff --git a/B08_Quantity/B08_Quantity_UI_Milestone_KakaoMap.ts b/B08_Quantity/B08_Quantity_UI_Milestone_KakaoMap.ts index 956d534ff..a6c2af12f 100644 --- a/B08_Quantity/B08_Quantity_UI_Milestone_KakaoMap.ts +++ b/B08_Quantity/B08_Quantity_UI_Milestone_KakaoMap.ts @@ -98,6 +98,8 @@ export interface MapPin { lon: number; lat: number; movable: boolean; + /** 262-2 고정 시점 표지 — 끌기 · 맞춤 · 보기 이동에 안 쓰임 · 고르는 표지와 모양이 다름 */ + fixed?: boolean; } export interface EditMap { @@ -127,8 +129,9 @@ export function editMap( loadKakaoMaps(projectId).then( (maps) => requestAnimationFrame(() => { + const moving = pins.filter((p) => !p.fixed); const points = [ - ...pins.map((p) => new maps.LatLng(p.lat, p.lon)), + ...moving.map((p) => new maps.LatLng(p.lat, p.lon)), ...lines.flat().map(([lat, lon]) => new maps.LatLng(lat, lon)), ]; const c = opts.center?.(); @@ -156,6 +159,17 @@ export function editMap( ), ...pins.flatMap((p) => { const at = new maps.LatLng(p.lat, p.lon); + if (p.fixed) + return [ + new maps.CustomOverlay({ + position: at, + content: el("span", { + className: "b08-milestone__map-start", + text: p.name, + }), + yAnchor: 0.5, + }), + ]; const marker = new maps.Marker({ position: at, title: p.name, draggable: p.movable }); if (p.movable) maps.event.addListener(marker, "dragend", () => { @@ -176,7 +190,7 @@ export function editMap( if (fitted) { // 보기 밖 표지만 그쪽으로 옮김(줌 그대로) const view = map.getBounds(); - const out = pins.find((p) => !view.contain(new maps.LatLng(p.lat, p.lon))); + const out = moving.find((p) => !view.contain(new maps.LatLng(p.lat, p.lon))); if (out) map.panTo(new maps.LatLng(out.lat, out.lon)); } else if (points.length > 1) { const bounds = new maps.LatLngBounds(); diff --git a/B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts b/B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts index 0e3394a59..97c93a15f 100644 --- a/B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts +++ b/B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts @@ -106,8 +106,8 @@ export function openPlaceModal( const title = target.add ? "요소 추가 — 기준점 · 거래처" : base - ? `기준점 — ${base.name || "기준점"}` - : `거래처 — ${route!.name || "거래처"}`; // 254-3 표 머리 글(항목 정보) 없음 + ? base.name || "기준점" + : route!.name || "거래처"; // 262-1 「—」 앞 글자 없음 openModal({ title, closeLabel: "닫기", @@ -259,7 +259,7 @@ export interface SiteAddress { /** 시점(공사지) 상자 — 중점 측점(km · 저장은 m) · 시점 → 중점 도로 · 아래 카카오 지도(시점 · 중점 표지) */ export function openSiteInfo(projectId: string): void { openModal({ - title: "공사지 — 시점 · 중점", + title: "시점 · 중점", closeLabel: "닫기", dialogClass: "b08-milestone__modal", mount: (body, close, actions) => { diff --git a/B08_Quantity/B08_Quantity_UI_Milestone_Place.ts b/B08_Quantity/B08_Quantity_UI_Milestone_Place.ts index aa65196e9..f4f2af5be 100644 --- a/B08_Quantity/B08_Quantity_UI_Milestone_Place.ts +++ b/B08_Quantity/B08_Quantity_UI_Milestone_Place.ts @@ -12,7 +12,7 @@ import { createButton, createInputField, el, showToast } from "@ui/ui_template_e 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 { siteInfo } from "./B08_Quantity_UI_Milestone_State"; +import { liveMilestone, startPoint } from "./B08_Quantity_UI_Milestone_State"; export interface Place { address: string; @@ -52,27 +52,35 @@ export function placePicker(projectId: string, initial: Place, name: () => strin 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: () => siteInfo()?.start_lonlat ?? null, + center: start, }); const draw = (): void => { where.textContent = `소재지 — ${place.address || "주소 찾기 · 지도에서 고르세요"}`; + const s = start(); map.set( - place.lon != null && place.lat != null - ? [ - { - key: "place", - name: name() || "소재지", - lon: place.lon, - lat: place.lat, - movable: true, - }, - ] - : [], + [ + ...(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, + }, + ] + : []), + ], [], ); }; diff --git a/resources/tester/test_254_3_b08_milestone_box_modal_pic.py b/resources/tester/test_254_3_b08_milestone_box_modal_pic.py index aeec089ba..0a78098f0 100644 --- a/resources/tester/test_254_3_b08_milestone_box_modal_pic.py +++ b/resources/tester/test_254_3_b08_milestone_box_modal_pic.py @@ -1,6 +1,6 @@ """254-3 B08 거리이정표 상자 모달 제목 · 장소 그림(사용자 162-6 · 8). -① 상자 누름 모달 제목 = 「거래처 — 이름」 · 「기준점 — 이름」(표 머리 글 「항목 정보」 없음) +① 상자 누름 모달 제목 = 이름만(262-1 · 「—」 앞 글자 없음 · 표 머리 글 「항목 정보」 없음) ② 장소 그림(256-1 `place_images`) = 상자 안 글 아래 작게 · 그림 없으면 자리 없음(상자 높이 그대로) """ @@ -67,7 +67,7 @@ def test_장소_그림_상자_안_글_아래(): def test_모달_제목_화면_그림(): modal = _read(B08 + "UI_Milestone_Modal.ts") - assert "`거래처 — ${route!.name" in modal and "`기준점 — ${base.name" in modal + assert 'route!.name || "거래처"' in modal and 'base.name || "기준점"' in modal # 262-1 assert "항목 정보 —" not in modal ui = _read(B08 + "UI_Milestone_Diagram.ts") assert "placeImageBlobUrl(projectId, id)" in ui and "placeImageKey(id)" in ui diff --git a/resources/tester/test_262_1_b08_milestone_title_start_pin.py b/resources/tester/test_262_1_b08_milestone_title_start_pin.py new file mode 100644 index 000000000..c002f4175 --- /dev/null +++ b/resources/tester/test_262_1_b08_milestone_title_start_pin.py @@ -0,0 +1,40 @@ +"""262 B08 거리이정표 상자 창 제목 · 소재지 지도 시점 표지(사용자 164-1). + +262-1 거래처 · 기준점 창 제목 = 이름만(빈 이름 = 「거래처」 · 「기준점」) · 시점 창 = 「시점 · 중점」 · 요소 추가 창 그대로 +262-2 소재지 지도 = 시점 표지 고정(끌기 없음 · 맞춤에서 빠짐) · 시점 없으면 표지 없음 · 소재지 없으면 시점 중심 +""" + +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +B08 = "B08_Quantity/B08_Quantity_UI_Milestone" + + +def _read(name): + return (ROOT / f"{B08}{name}").read_text(encoding="utf-8") + + +def test_제목_이름만(): + modal = _read("_Modal.ts") + title = modal[modal.index("const title = target.add") : modal.index("openModal({")] + assert '? base.name || "기준점"' in title and ': route!.name || "거래처"' in title + assert '"요소 추가 — 기준점 · 거래처"' in title + assert 'title: "시점 · 중점",' in modal and "공사지 —" not in modal + assert re.search(r"title: `?[^\n]*(거래처|기준점|공사지) —", modal) is None + + +def test_시점_표지_고정(): + place = _read("_Place.ts") + assert "startPoint(liveMilestone(projectId))" in place # 거리이정표가 쓰는 시점 + assert "center: start" in place # 소재지 없음 = 시점 중심 + assert "movable: false, fixed: true" in place and 'name: "시점"' in place + assert "...(s" in place and " : [])," in place # 시점 없음 = 표지 없음 + kakao = _read("_KakaoMap.ts") + assert "fixed?: boolean;" in kakao + assert "const moving = pins.filter((p) => !p.fixed);" in kakao + assert "...moving.map(" in kakao # 처음 보기 = 소재지 중심(시점은 맞춤에서 빠짐) + assert "moving.find(" in kakao # 보기 이동에서도 빠짐 + assert "if (p.fixed)" in kakao and "b08-milestone__map-start" in kakao + css = _read(".css") + assert ".b08-milestone__map-start {" in css