File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
/**
* 다음(카카오) 우편번호 서비스 연동 — 기획 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<string, Promise<void>>();
/** 같은 주소의 스크립트는 한 번만 받는다 — 두 번 넣으면 전역이 다시 초기화된다. */
function loadScript(src: string): Promise<void> {
const cached = loaded.get(src);
if (cached) {
return cached;
}
const pending = new Promise<void>((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<PostcodeResult | null> {
await loadScript(POSTCODE_SRC);
const picked = await new Promise<DaumPostcodeData | null>((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,
};
}