refactor(529-2): 카카오 지도 부품 B08 → ui_template 공용 이전 · 키 길 공용(/api/map/kakao-key)

- ui_template_kakao_map.ts(editMap · loadKakaoMaps) · 지도 CSS 공용 이름(ui-kakao-map*)
- B08 거리이정표 네 곳은 공용을 부름 · projectId 인자 걷음(키가 프로젝트 무관)
- B08 /quantity/milestone/map-key 걷음 → common_util_map_router 로 옮김
- B08 이정표 시험 파일 경로 · fetch 가짜 · 공용 CSS 로 고침(동작 판정 그대로)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 19:26:34 +09:00
co-authored by Claude Opus 5.5
parent bc39953fbd
commit c2876aef60
16 changed files with 119 additions and 102 deletions
@@ -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}
@@ -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);
@@ -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();
@@ -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;
@@ -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),
@@ -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),
+21
View File
@@ -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}
+2
View File
@@ -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)
# ─────────────────────────────────────────────────────────────────────────
@@ -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;
@@ -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
@@ -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
@@ -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 키 · 도메인 등록 필요 — 지도 대신 [그림 변경]으로 넣으세요"
@@ -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
@@ -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 # 다른 모달과 같은 창 · 지도 높이
+34
View File
@@ -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;
}
@@ -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<KakaoMaps> | null = null;
/** SDK 한 번 싣기 — 키 없음 · 못 받음 = NO_MAP 으로 거절(다음 부름 때 다시) */
export function loadKakaoMaps(projectId: string): Promise<KakaoMaps> {
export function loadKakaoMaps(): Promise<KakaoMaps> {
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<Array<[number, number]>>): 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,
};
}