File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
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,
};
}