/** * 다음(카카오) 우편번호 서비스 연동 — 기획 MCH_002_p ③. * * 스크립트를 문서에 미리 넣지 않고 버튼을 누른 순간 한 번만 받는다. 가맹점 등록은 자주 열리는 * 화면이 아니라 모든 페이지가 이 30KB를 지고 다닐 이유가 없다. * * 좌표는 이 서비스가 주지 않는다 — 주소만 준다. 위도·경도는 카카오 지도 SDK의 지오코더가 * 필요하고 앱 키를 요구하는데 아직 발급되지 않았다(`NEXT_PUBLIC_KAKAO_MAP_KEY`). 키가 설정되면 * `geocode()`가 자동으로 동작하고, 없으면 좌표를 비운 채로 진행한다. */ const POSTCODE_SRC = 'https://t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js'; const KAKAO_SDK_SRC = 'https://dapi.kakao.com/v2/maps/sdk.js'; export type PostcodeResult = { /** 우편번호 5자리. */ zonecode: string; /** 사용자가 고른 표기의 주소(도로명 또는 지번). */ address: string; latitude: string; longitude: string; }; type DaumPostcodeData = { zonecode?: string; address?: string; roadAddress?: string; jibunAddress?: string; userSelectedType?: string; }; declare global { interface Window { daum?: { Postcode: new (options: { oncomplete: (data: DaumPostcodeData) => void; onclose?: () => void; }) => { open: () => void }; }; kakao?: { maps: { load: (callback: () => void) => void; services?: { Geocoder: new () => { addressSearch: ( address: string, callback: ( result: Array<{ x: string; y: string }>, status: string ) => void ) => void; }; Status: { OK: string }; }; }; }; } } const loaded = new Map>(); /** 같은 주소의 스크립트는 한 번만 받는다 — 두 번 넣으면 전역이 다시 초기화된다. */ function loadScript(src: string): Promise { const cached = loaded.get(src); if (cached) { return cached; } const pending = new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = src; script.async = true; script.onload = () => resolve(); script.onerror = () => reject(new Error(`스크립트를 불러오지 못했습니다: ${src}`)); document.head.appendChild(script); }); loaded.set(src, pending); return pending; } /** 주소 → 좌표. 앱 키가 없거나 찾지 못하면 빈 값을 돌려준다(주소 입력은 계속 진행된다). */ async function geocode(address: string): Promise<{ lat: string; lng: string }> { const key = process.env.NEXT_PUBLIC_KAKAO_MAP_KEY; if (!key) { return { lat: '', lng: '' }; } try { await loadScript( `${KAKAO_SDK_SRC}?appkey=${encodeURIComponent(key)}&autoload=false&libraries=services` ); } catch { return { lat: '', lng: '' }; } return new Promise((resolve) => { const maps = window.kakao?.maps; if (!maps) { resolve({ lat: '', lng: '' }); return; } maps.load(() => { const services = window.kakao?.maps.services; if (!services) { resolve({ lat: '', lng: '' }); return; } new services.Geocoder().addressSearch(address, (result, status) => { const first = status === services.Status.OK ? result[0] : undefined; // 카카오는 x가 경도, y가 위도다. resolve({ lat: first?.y ?? '', lng: first?.x ?? '' }); }); }); }); } /** * 우편번호 창을 열고 고른 주소를 돌려준다. 사용자가 닫으면 `null`이다. */ export async function openPostcodeSearch(): Promise { await loadScript(POSTCODE_SRC); const picked = await new Promise((resolve) => { const Postcode = window.daum?.Postcode; if (!Postcode) { resolve(null); return; } let completed = false; new Postcode({ oncomplete: (data) => { completed = true; resolve(data); }, // 고르지 않고 닫으면 oncomplete가 오지 않는다 — 그때 null로 끝낸다. onclose: () => { if (!completed) { resolve(null); } }, }).open(); }); if (!picked) { return null; } // 사용자가 고른 표기를 그대로 쓴다(도로명 R / 지번 J). const address = (picked.userSelectedType === 'J' ? picked.jibunAddress : picked.roadAddress) || picked.address || ''; const { lat, lng } = await geocode(address); return { zonecode: picked.zonecode ?? '', address, latitude: lat, longitude: lng, }; }