diff --git a/B08_Quantity/B08_Quantity_Router_Milestone.py b/B08_Quantity/B08_Quantity_Router_Milestone.py index da687e23b..4e5c7fe82 100644 --- a/B08_Quantity/B08_Quantity_Router_Milestone.py +++ b/B08_Quantity/B08_Quantity_Router_Milestone.py @@ -22,8 +22,6 @@ 373 화면이 구간 `legs[].path` 로 캐시 → [저장]) GET /api/projects/{id}/quantity/milestone/reverse?lon=&lat= 지도에서 고른 소재지(244-13) → VWorld 주소 한 줄(서버 단독 · 저장 안 함 · 못 찾음 = null) - GET /api/projects/{id}/quantity/milestone/map-key 카카오 지도 JavaScript 키(244-9 · - 공개용 · .env KAKAO_JS_KEY · 없으면 null) 정본 = 프로젝트 storage `B08_Quantity/milestone/`(`milestone.json` + `<경로 id>.<확장자>` 사용자 그림 · `auto/<경로 id>.png` 자동 그림 · 폴더가 목록 · B09 견적서 파일 길 꼴) · 보이는 그림 = 사용자 @@ -676,12 +674,3 @@ async def milestone_directions(project_id: UUID, body: Directions) -> dict[str, """[길찾기] 한 구간 — 단추 누를 때만(조작 중 자동 호출 없음) · 길 선은 점 줄여 돌려줌(373).""" got = await asyncio.to_thread(directions_km, body.origin, body.destination) return {**got, "path": thin_path(got["path"])} if got.get("path") else got - - -KAKAO_JS = "KAKAO_JS_KEY" - - -@router.get(PATH + "/map-key") -async def milestone_map_key(project_id: UUID) -> dict[str, Any]: - """카카오 지도 JavaScript 키(공개용 · 도메인 등록으로 막힘) — 없으면 null = 화면 안내 한 줄.""" - return {"key": os.environ.get(KAKAO_JS, "").strip() or None} diff --git a/B08_Quantity/B08_Quantity_UI_Milestone.css b/B08_Quantity/B08_Quantity_UI_Milestone.css index 7ba848e6b..747844737 100644 --- a/B08_Quantity/B08_Quantity_UI_Milestone.css +++ b/B08_Quantity/B08_Quantity_UI_Milestone.css @@ -188,35 +188,6 @@ pointer-events: none; } -/* 경로 지도(244-9) — 카카오 지도 칸 · 표지 이름 · 높이 어디서나 한 값(150) */ -.b08-milestone__map { - width: 100%; - height: 480px; - border: 1px solid var(--color-border); -} - -.b08-milestone__map-label { - padding: 2px 6px; - border: 1px solid #3c3c3c; - background: #fff; - color: #141414; - font-size: 12px; - 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_LegModal.ts b/B08_Quantity/B08_Quantity_UI_Milestone_LegModal.ts index 2d873d60a..b306bf393 100644 --- a/B08_Quantity/B08_Quantity_UI_Milestone_LegModal.ts +++ b/B08_Quantity/B08_Quantity_UI_Milestone_LegModal.ts @@ -23,7 +23,7 @@ import { type SegKind, } from "./B08_Quantity_Milestone_Calc"; import { ownerOf, segmentEnds, setLeg } from "./B08_Quantity_Milestone_Layout"; -import { editMap, type MapPin } from "./B08_Quantity_UI_Milestone_KakaoMap"; +import { editMap, type MapPin } from "@ui/ui_template_kakao_map"; import { fmtKm, toastKm } from "./B08_Quantity_UI_Milestone_Modal"; import { reverseAddress } from "./B08_Quantity_UI_Milestone_Place"; import { @@ -224,7 +224,7 @@ export function openLegInfo(projectId: string, from: string): void { drawMap(); }; // 끝점 끌어 옮김 = 그 소재지 고침(주소 다시) + 다시 길찾기 - const map = editMap(projectId, { + const map = editMap({ hint: "경로 = 카카오 길찾기 · 거래처 · 기준점 표지를 끌어 옮기면 다시 길찾기", onMove: (key, lon, lat) => { apply(); diff --git a/B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts b/B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts index e665d5969..bba005a19 100644 --- a/B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts +++ b/B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts @@ -35,7 +35,7 @@ import { } from "./B08_Quantity_Milestone_Calc"; import { setLeg } from "./B08_Quantity_Milestone_Layout"; import { request } from "./B08_Quantity_UI_Hydro"; -import { editMap, type MapPin } from "./B08_Quantity_UI_Milestone_KakaoMap"; +import { editMap, type MapPin } from "@ui/ui_template_kakao_map"; import { placePicker } from "./B08_Quantity_UI_Milestone_Place"; import { capturePlace, @@ -305,7 +305,7 @@ export function openSiteInfo(projectId: string): void { }); save.dataset.b08MilestoneSite = "save"; actions.append(save); - const map = editMap(projectId, { hint: "", onMove: () => undefined }); + const map = editMap({ hint: "", onMove: () => undefined }); const pins = (s: { start_lonlat: [number, number] | null; mid_lonlat: [number, number] | null; diff --git a/B08_Quantity/B08_Quantity_UI_Milestone_Place.ts b/B08_Quantity/B08_Quantity_UI_Milestone_Place.ts index f4f2af5be..42abd90a1 100644 --- a/B08_Quantity/B08_Quantity_UI_Milestone_Place.ts +++ b/B08_Quantity/B08_Quantity_UI_Milestone_Place.ts @@ -3,7 +3,7 @@ * 거리이정표 등록 모달 「소재지」 칸(PLAN 244-13 · 사용자 147번 5) — 주소 찾기 · 지도에서 고르기만(직접 입력 없음). * * 주소 찾기 = B01 회사 주소와 같은 서버 후보 검색(VWorld 대리 · `searchAddress`) → 후보 고름 = 소재지 · 표지 - * 지도 = 다음(카카오) 지도 JS(`…_KakaoMap` editMap) — 지도 누름 = 그 자리 소재지 · 표지 끌어 옮김 = 다시 고름 · + * 지도 = 다음(카카오) 지도 JS(`ui_template_kakao_map` editMap) — 지도 누름 = 그 자리 소재지 · 표지 끌어 옮김 = 다시 고름 · * 고른 좌표 → 서버 GET …/milestone/reverse(VWorld 주소 한 줄 · 못 찾음 = 위도, 경도 글) * 값 = 주소 글 · 경도 · 위도(캐시 · [저장] 때 서버) * ========================================================================== */ @@ -11,7 +11,7 @@ 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 { editMap } from "@ui/ui_template_kakao_map"; import { liveMilestone, startPoint } from "./B08_Quantity_UI_Milestone_State"; export interface Place { @@ -54,7 +54,7 @@ export function placePicker(projectId: string, initial: Place, name: () => strin where.dataset.b08MilestonePlace = "value"; // 262-2 시점 = 거리이정표가 쓰는 시점(끌어 옮긴 덮개 · 없으면 노선 시점) const start = (): [number, number] | null => startPoint(liveMilestone(projectId)); - const map = editMap(projectId, { + const map = editMap({ hint: "지도 누름 = 그 자리를 소재지로 · 표지 끌어 옮김", onClick: (lon, lat) => void moveTo(lon, lat), onMove: (_key, lon, lat) => void moveTo(lon, lat), diff --git a/B08_Quantity/B08_Quantity_UI_Milestone_Route.ts b/B08_Quantity/B08_Quantity_UI_Milestone_Route.ts index 40526113d..8e628dee3 100644 --- a/B08_Quantity/B08_Quantity_UI_Milestone_Route.ts +++ b/B08_Quantity/B08_Quantity_UI_Milestone_Route.ts @@ -20,7 +20,7 @@ import { createButton, el, showToast } from "@ui/ui_template_elements"; import type { MilestoneData, MilestoneRoute } from "./B08_Quantity_Milestone_Calc"; import { chainOf, ownerOf, reachesSite } from "./B08_Quantity_Milestone_Layout"; import { DEST_TEXT } from "./B08_Quantity_Milestone_Table"; -import { editMap, type MapPin } from "./B08_Quantity_UI_Milestone_KakaoMap"; +import { editMap, type MapPin } from "@ui/ui_template_kakao_map"; import { openPlaceModal } from "./B08_Quantity_UI_Milestone_Modal"; import { reverseAddress } from "./B08_Quantity_UI_Milestone_Place"; import { @@ -185,7 +185,7 @@ export function routeView(projectId: string, routeId: string): MilestoneView { putMilestone(projectId, fresh); await refind(touchingSegments(fresh, key)); }; - const map = editMap(projectId, { + const map = editMap({ hint: "지도 누름 = 그 자리를 소재지로 · 표지 끌어 옮김", onClick: (lon, lat) => void moveTo(routeId, lon, lat), onMove: (key, lon, lat) => void moveTo(key, lon, lat), diff --git a/common_util/common_util_map_router.py b/common_util/common_util_map_router.py new file mode 100644 index 000000000..96c51ea66 --- /dev/null +++ b/common_util/common_util_map_router.py @@ -0,0 +1,21 @@ +"""카카오 지도 JavaScript 키 공용 길(529-2) — B08 거리이정표 · D 예정노선 지도가 씀. + +GET /api/map/kakao-key {key} — 공개용(도메인 등록으로 막힘) · .env KAKAO_JS_KEY · 없으면 null +""" + +from __future__ import annotations + +import os +from typing import Any + +from fastapi import APIRouter + +router = APIRouter(prefix="/api/map", tags=["map"]) + +KAKAO_JS = "KAKAO_JS_KEY" + + +@router.get("/kakao-key") +async def kakao_map_key() -> dict[str, Any]: + """없으면 null = 화면 안내 한 줄.""" + return {"key": os.environ.get(KAKAO_JS, "").strip() or None} diff --git a/main.py b/main.py index d9042e54b..9e294d58a 100644 --- a/main.py +++ b/main.py @@ -94,6 +94,7 @@ from common_util.common_util_auth import ( # 개발이 아니면 세 입구 모두 403 으로 거절한다(화면 단추 숨김은 보조). from common_util.common_util_dev_unlock_router import router as dev_unlock_router from common_util.common_util_help_router import router as help_router +from common_util.common_util_map_router import router as map_router from common_util.common_util_resource_monitor import sample_resources_loop from common_util.common_util_temp_cleanup import cleanup_expired_temp_uploads_loop from config.config_db import close_db_pool, get_db_pool, init_db_pool @@ -517,6 +518,7 @@ app.include_router(m02_project_items_router, dependencies=protected) app.include_router(dev_unlock_router, dependencies=protected_with_company) # [?] 설명서 글(54-2) — 로그인만 · 파일을 부를 때마다 읽음. app.include_router(help_router, dependencies=protected) +app.include_router(map_router, dependencies=protected) # ───────────────────────────────────────────────────────────────────────── diff --git a/resources/tester/test_244_10_b08_milestone_tailscale.py b/resources/tester/test_244_10_b08_milestone_tailscale.py index c4bbfbc45..2488e14ab 100644 --- a/resources/tester/test_244_10_b08_milestone_tailscale.py +++ b/resources/tester/test_244_10_b08_milestone_tailscale.py @@ -13,7 +13,7 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[2] IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE) LAYOUT = "B08_Quantity/B08_Quantity_Milestone_Layout.ts" -KMAP = "B08_Quantity/B08_Quantity_UI_Milestone_KakaoMap.ts" +KMAP = "ui_template/ui_template_kakao_map.ts" ORIGIN = "http://100.72.32.27:5173" @@ -25,7 +25,8 @@ STUBS = f""" const el = (tag: string, o: any = {{}}) => ({{ tag, hidden: false, textContent: o.text ?? "", className: o.className, dataset: {{}}, children: o.children ?? [] }}); const requestAnimationFrame = (f: () => void) => setTimeout(f, 0); -const request = async () => ({{ key: "fake-js-key" }}); +const API_BASE_URL = ""; +(globalThis as any).fetch = async () => ({{ ok: true, json: async () => ({{ key: "fake-js-key" }}) }}); const injected: string[] = []; (globalThis as any).window = globalThis; (globalThis as any).location = {{ origin: {json.dumps(ORIGIN)} }}; @@ -50,7 +51,7 @@ def _view(setup): + IMPORTS.sub("", _read(KMAP)) + """ // 156 경로 탭 지도 칸 없앰 — 같은 SDK 싣기 · 안내 길 = 연결선 모달 지도 칸(editMap) -const view = editMap("p1", { onMove: () => {}, hint: "길 선 = 카카오 길찾기 · 경로 그림은 서버가 자동으로 만듦" }); +const view = editMap({ onMove: () => {}, hint: "길 선 = 카카오 길찾기 · 경로 그림은 서버가 자동으로 만듦" }); view.set([{ key: "r1", name: "거창군청", lon: 127.9, lat: 35.68, movable: true }], routePaths()); await new Promise((r) => setTimeout(r, 30)); const [hint, box] = view.root.children; diff --git a/resources/tester/test_244_13_b08_milestone_place_leg.py b/resources/tester/test_244_13_b08_milestone_place_leg.py index b6a019bc9..f54a8f1d1 100644 --- a/resources/tester/test_244_13_b08_milestone_place_leg.py +++ b/resources/tester/test_244_13_b08_milestone_place_leg.py @@ -30,7 +30,7 @@ IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE) B08 = "B08_Quantity/B08_Quantity_" CALC = B08 + "Milestone_Calc.ts" LAYOUT = B08 + "Milestone_Layout.ts" -KMAP = B08 + "UI_Milestone_KakaoMap.ts" +KMAP = "ui_template/ui_template_kakao_map.ts" STATE = B08 + "UI_Milestone_State.ts" BASE = "/api/projects/00000000-0000-0000-0000-000000000001/quantity/milestone" @@ -158,7 +158,8 @@ const el = (tag: string, o: any = {}) => ({ tag, hidden: false, textContent: o.t const requestAnimationFrame = (f: () => void) => setTimeout(f, 0); (globalThis as any).window = globalThis; (globalThis as any).document = { createElement: () => ({}), head: { append: () => undefined } }; -const request = async () => ({ key: "fake-js-key" }); +const API_BASE_URL = ""; +(globalThis as any).fetch = async () => ({ ok: true, json: async () => ({ key: "fake-js-key" }) }); const LOG: any = { markers: [], handlers: [], lines: 0 }; class LatLng { lat: number; lng: number; constructor(lat: number, lng: number) { this.lat = lat; this.lng = lng; } @@ -183,7 +184,7 @@ def test_고르는_지도_누름_끌기(): [KMAP], """ const moved: any[] = []; const clicked: any[] = []; -const m = editMap("p1", { hint: "h", onMove: (k, lon, lat) => moved.push([k, lon, lat]), +const m = editMap({ hint: "h", onMove: (k, lon, lat) => moved.push([k, lon, lat]), onClick: (lon, lat) => clicked.push([lon, lat]), center: () => [127.83, 35.64] }); m.set([], []); await new Promise((r) => setTimeout(r, 20)); @@ -292,7 +293,7 @@ def test_화면_등록_모달_연결선_모달(): assert "소재지" in modal and "거래처 위치" not in modal for text in (modal, place): assert 'label: "직접 입력"' not in text and "buildAddressField" not in text - assert "editMap(projectId" in place and "onClick: (lon, lat) => void moveTo(lon, lat)" in place + assert "editMap({" in place and "onClick: (lon, lat) => void moveTo(lon, lat)" in place assert "/quantity/milestone/reverse?lon=" in place # ② 상자 누름 = 같은 등록 모달 · [기준점 +] 없음 assert "base: (id) => openPlaceModal(projectId, { add: false, id })," in diagram diff --git a/resources/tester/test_244_4_b08_milestone_modal.py b/resources/tester/test_244_4_b08_milestone_modal.py index 31cdf6dd5..77ed4ccc2 100644 --- a/resources/tester/test_244_4_b08_milestone_modal.py +++ b/resources/tester/test_244_4_b08_milestone_modal.py @@ -151,7 +151,7 @@ def test_화면_모달_팬줌_그림탭(): assert 'from "../B01_Dashboard/B01_Dashboard_Api_Fetch"' in place assert "searchAddress(text)" in place # ② 시점 모달 — 지도 = 공용 카카오 지도(255-1 · 주소 줄 지움) - assert "/quantity/milestone/site-address" in modal and "editMap(projectId" in modal + assert "/quantity/milestone/site-address" in modal and "editMap({" in modal # ⑤ 거래처 상자 = 같은 등록 모달(고침 · 삭제 · 244-13) · 구간 km · 도로 구분 = 연결선 모달 for text in ('"수정"', 'label: "삭제"', "rowValues()", "putMilestone(projectId, fresh)"): assert text in add diff --git a/resources/tester/test_244_9_b08_milestone_route_map.py b/resources/tester/test_244_9_b08_milestone_route_map.py index fe54091ec..7a8cefdc8 100644 --- a/resources/tester/test_244_9_b08_milestone_route_map.py +++ b/resources/tester/test_244_9_b08_milestone_route_map.py @@ -22,11 +22,12 @@ if str(ROOT) not in sys.path: from B08_Quantity import B08_Quantity_Router_Milestone as ms # noqa: E402 from B08_Quantity.B08_Quantity_Milestone_Calc import migrate_milestone # noqa: E402 +from common_util import common_util_map_router as map_router # noqa: E402 IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE) BASE = "/api/projects/00000000-0000-0000-0000-000000000001/quantity/milestone" LAYOUT = "B08_Quantity/B08_Quantity_Milestone_Layout.ts" -KMAP = "B08_Quantity/B08_Quantity_UI_Milestone_KakaoMap.ts" +KMAP = "ui_template/ui_template_kakao_map.ts" GEOCHANG = {"lon": 127.9095, "lat": 35.6866} BYEONGGOK = {"lon": 127.85, "lat": 35.66} BODY = { @@ -108,14 +109,14 @@ def test_좌표_저장_안_함(tmp_path): def test_JS_키_길(monkeypatch): app = FastAPI() - app.include_router(ms.router) + app.include_router(map_router.router) # 529-2 공용 길로 옮김 client = TestClient(app) - monkeypatch.delenv(ms.KAKAO_JS, raising=False) - assert client.get(BASE + "/map-key").json() == {"key": None} - monkeypatch.setenv(ms.KAKAO_JS, " ") - assert client.get(BASE + "/map-key").json() == {"key": None} - monkeypatch.setenv(ms.KAKAO_JS, "fake-js-key") - assert client.get(BASE + "/map-key").json() == {"key": "fake-js-key"} + monkeypatch.delenv(map_router.KAKAO_JS, raising=False) + assert client.get("/api/map/kakao-key").json() == {"key": None} + monkeypatch.setenv(map_router.KAKAO_JS, " ") + assert client.get("/api/map/kakao-key").json() == {"key": None} + monkeypatch.setenv(map_router.KAKAO_JS, "fake-js-key") + assert client.get("/api/map/kakao-key").json() == {"key": "fake-js-key"} def test_표지_셋(): @@ -164,10 +165,11 @@ def _map(key, paths, extra=""): const KEY = {json.dumps(key)}; const PATHS = {json.dumps(paths)}; const asked: string[] = []; -const request = async (url: string) => (asked.push(url), {{ key: KEY }}); +const API_BASE_URL = ""; +(globalThis as any).fetch = async (url: string) => (asked.push(url), {{ ok: true, json: async () => ({{ key: KEY }}) }}); {extra} // 156 경로 탭 지도 칸 없앰 — 같은 SDK 싣기 · 안내 길 = 연결선 모달 지도 칸(editMap) -const view = editMap("p1", {{ onMove: () => {{}}, hint: "길 선 = 카카오 길찾기 · 경로 그림은 서버가 자동으로 만듦" }}); +const view = editMap({{ onMove: () => {{}}, hint: "길 선 = 카카오 길찾기 · 경로 그림은 서버가 자동으로 만듦" }}); view.set([{{ key: "r1", name: "거창군청", lon: 127.9, lat: 35.68, movable: true }}, {{ key: "base", name: "병곡리", lon: 127.85, lat: 35.66, movable: true }}, {{ key: "start", name: "시점", lon: 127.83, lat: 35.64, movable: false }}], PATHS); @@ -181,7 +183,7 @@ console.log(JSON.stringify({{ asked, injected, hint: hint.textContent, boxHidden def test_키_없음_안내_한_줄(): got = _map(None, []) - assert got["asked"] == ["/projects/p1/quantity/milestone/map-key"] + assert got["asked"] == ["/map/kakao-key"] assert got["injected"] == [] # SDK 안 부름 assert got["hint"] == ( "카카오 JavaScript 키 · 도메인 등록 필요 — 지도 대신 [그림 변경]으로 넣으세요" diff --git a/resources/tester/test_250_1_b08_milestone_roads_width.py b/resources/tester/test_250_1_b08_milestone_roads_width.py index eca1c4cc6..6eb744146 100644 --- a/resources/tester/test_250_1_b08_milestone_roads_width.py +++ b/resources/tester/test_250_1_b08_milestone_roads_width.py @@ -73,11 +73,12 @@ def test_폭_전체_지도_높이_한_값(): block = css[css.index(".b08-milestone {") :] assert "width: 100%;" in block[: block.index("}")] assert ".b08-milestone > .ui-data-table .ui-data-table__table {\n width: 100%;" in css - assert css.count("height: 480px;") == 1 and ".b08-milestone__map {" in css + kcss = _read("ui_template/ui_template_kakao_map.css") # 529-2 공용 지도 칸 + assert kcss.count("height: 480px;") == 1 and ".ui-kakao-map__box {" in kcss def test_153_지도_보기_유지_상자_색_끌어_옮김(): - kmap = _read("B08_Quantity/B08_Quantity_UI_Milestone_KakaoMap.ts") + kmap = _read("ui_template/ui_template_kakao_map.ts") assert ( "let fitted = false;" in kmap and "map.panTo(" in kmap ) # 처음 한 번만 맞춤 · 그 뒤 줌 그대로 @@ -170,9 +171,7 @@ def test_157_도구_왼쪽_아래_경로_탭_지도_그림_변경_초기화(): route = _read("B08_Quantity/B08_Quantity_UI_Milestone_Route.ts") assert 'label: "그림 변경"' in route and 'label: "초기화"' in route assert "그림 지우기" not in route and "자동 그림으로 되돌리기" not in route.split("*/", 1)[1] - assert ( - "map.root.hidden = user;" in route and "editMap(projectId" in route - ) # 그림 없으면 길찾기 지도 + assert "map.root.hidden = user;" in route and "editMap({" in route # 그림 없으면 길찾기 지도 assert ".b08-milestone__route-head {" in css and "justify-content: space-between;" in css state = _read("B08_Quantity/B08_Quantity_UI_Milestone_State.ts") assert "export const hasUserImage" in state and "canRevertImage" not in state diff --git a/resources/tester/test_255_1_b08_milestone_site_modal.py b/resources/tester/test_255_1_b08_milestone_site_modal.py index 51ce9d5b0..29efd249e 100644 --- a/resources/tester/test_255_1_b08_milestone_site_modal.py +++ b/resources/tester/test_255_1_b08_milestone_site_modal.py @@ -46,6 +46,6 @@ def test_시점_모달_km_카카오_지도(): # 설명 줄 · 등록 주소 · 브이월드 주소 줄 · B01 타일 지도 없음 for gone in ("시점 = 노선 첫 점", "프로젝트 등록 주소", "브이월드 주소", "buildAddressMap"): assert gone not in modal - assert "editMap(projectId" in site + assert "editMap({" in site assert '["start", "시점", s.start_lonlat]' in site and '["mid", "중점", s.mid_lonlat]' in site assert 'dialogClass: "b08-milestone__modal"' in site # 다른 모달과 같은 창 · 지도 높이 diff --git a/ui_template/ui_template_kakao_map.css b/ui_template/ui_template_kakao_map.css new file mode 100644 index 000000000..5a6ccad0e --- /dev/null +++ b/ui_template/ui_template_kakao_map.css @@ -0,0 +1,34 @@ +/* 카카오 지도 칸 공용(529-2) — 지도 · 안내 · 표지 이름 · 높이 어디서나 한 값(150) */ +.ui-kakao-map__box { + width: 100%; + height: 480px; + border: 1px solid var(--color-border); +} + +.ui-kakao-map__hint { + margin: 0; + color: var(--color-text-secondary); + font-size: var(--text-caption); +} + +.ui-kakao-map__label { + padding: 2px 6px; + border: 1px solid #3c3c3c; + background: #fff; + color: #141414; + font-size: 12px; + white-space: nowrap; +} + +/* 262-2 고정 시점 표지 — 둥근 붉은 알약(고르는 표지 = 기본 핀 + 흰 이름표와 구별) */ +.ui-kakao-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; +} diff --git a/B08_Quantity/B08_Quantity_UI_Milestone_KakaoMap.ts b/ui_template/ui_template_kakao_map.ts similarity index 81% rename from B08_Quantity/B08_Quantity_UI_Milestone_KakaoMap.ts rename to ui_template/ui_template_kakao_map.ts index a6c2af12f..3c10ed1a6 100644 --- a/B08_Quantity/B08_Quantity_UI_Milestone_KakaoMap.ts +++ b/ui_template/ui_template_kakao_map.ts @@ -1,17 +1,16 @@ /* ============================================================================= - * B08_Quantity_UI_Milestone_KakaoMap.ts - * 거리이정표 카카오 지도 칸(PLAN 244-9 · 사용자 138번) — 등록 모달(소재지 고르기) · 연결선 모달(길 선 · 표지 끌기) - * 한 벌(`editMap`) · 경로 하위 탭 지도 칸은 없앰(156 · 탭 = 그림만). + * ui_template_kakao_map.ts + * 카카오 지도 칸 공용(PLAN 244-9 · 529-2) — 고르는 지도 한 벌(`editMap`) · B08 거리이정표 · D 예정노선이 씀. * - * SDK = dapi.kakao.com/v2/maps/sdk.js(autoload=false) · 키 = 서버 GET …/milestone/map-key + * SDK = dapi.kakao.com/v2/maps/sdk.js(autoload=false) · 키 = 서버 GET /api/map/kakao-key * (.env JS 키 · 공개용 · 도메인 등록으로 막힘) · 키 없음 · 못 뜸 = 안내 한 줄 + 사진 올리기 - * 길 선 = 마지막 [길찾기] 응답 경로 좌표(메모리만 · 저장 안 함) · 처음 한 번만 맞춤(153 · 그 뒤 보기 그대로) - * 경로 그림 = 서버 자동 캡처(244-11 · `…_State` autoRouteImage) — 브라우저 캡처 없음(접속 주소 무관) + * 길 선 = 부르는 쪽이 준 [위도, 경도] 목록 · 처음 한 번만 맞춤(153 · 그 뒤 보기 그대로) * (244-10) SDK 못 받음(도메인 미등록 401) = 지금 주소(location.origin) 등록 안내 한 줄 * ========================================================================== */ -import { el } from "@ui/ui_template_elements"; -import { request } from "./B08_Quantity_UI_Hydro"; +import "./ui_template_kakao_map.css"; +import { API_BASE_URL } from "@config/config_frontend"; +import { el } from "./ui_template_elements"; export const NO_MAP = "카카오 JavaScript 키 · 도메인 등록 필요 — 지도 대신 [그림 변경]으로 넣으세요"; @@ -58,11 +57,12 @@ interface KakaoMaps { let sdk: Promise | null = null; /** SDK 한 번 싣기 — 키 없음 · 못 받음 = NO_MAP 으로 거절(다음 부름 때 다시) */ -export function loadKakaoMaps(projectId: string): Promise { +export function loadKakaoMaps(): Promise { sdk ??= (async () => { - const { key } = await request<{ key: string | null }>( - `/projects/${projectId}/quantity/milestone/map-key`, - ); + const response = await fetch(`${API_BASE_URL}/map/kakao-key`, { credentials: "include" }); + const { key } = response.ok + ? ((await response.json()) as { key: string | null }) + : { key: null }; if (!key) throw new Error(NO_MAP); const w = window as unknown as { kakao?: { maps: KakaoMaps } }; if (!w.kakao?.maps) { @@ -109,24 +109,21 @@ export interface EditMap { } /** 고르는 지도 — 움직일 표지 끌기 = onMove · 지도 누름 = onClick(있을 때만) · 점 없으면 center(경도, 위도) */ -export function editMap( - projectId: string, - opts: { - onMove: (key: string, lon: number, lat: number) => void; - onClick?: (lon: number, lat: number) => void; - center?: () => [number, number] | null; - hint: string; - }, -): EditMap { - const box = el("div", { className: "b08-milestone__map" }); - const hint = el("p", { className: "b08-options__hint", text: opts.hint }); - hint.dataset.b08MilestoneMapHint = ""; +export function editMap(opts: { + onMove: (key: string, lon: number, lat: number) => void; + onClick?: (lon: number, lat: number) => void; + center?: () => [number, number] | null; + hint: string; +}): EditMap { + const box = el("div", { className: "ui-kakao-map__box" }); + const hint = el("p", { className: "ui-kakao-map__hint", text: opts.hint }); + hint.dataset.kakaoMapHint = ""; let map: KMap | null = null; let drawn: KOverlay[] = []; let fitted = false; // 153 — 처음 한 번만 맞춤 · 그 뒤 고름 · 끌기는 보기(중심 · 줌) 그대로 const set = (pins: MapPin[], lines: Array>): void => { - loadKakaoMaps(projectId).then( + loadKakaoMaps().then( (maps) => requestAnimationFrame(() => { const moving = pins.filter((p) => !p.fixed); @@ -164,7 +161,7 @@ export function editMap( new maps.CustomOverlay({ position: at, content: el("span", { - className: "b08-milestone__map-start", + className: "ui-kakao-map__start", text: p.name, }), yAnchor: 0.5, @@ -180,7 +177,7 @@ export function editMap( marker, new maps.CustomOverlay({ position: at, - content: el("span", { className: "b08-milestone__map-label", text: p.name }), + content: el("span", { className: "ui-kakao-map__label", text: p.name }), yAnchor: 2.6, }), ]; @@ -211,7 +208,7 @@ export function editMap( }; return { - root: el("div", { className: "b08-milestone__map-wrap", children: [hint, box] }), + root: el("div", { className: "ui-kakao-map", children: [hint, box] }), set, }; }