PageSourceSearch

https://pythonmagazine.com/plugin/editor/jodit/modules/rara-naver-map-render.js?ver=1790248736

js pythonmagazine.com collected 2026-09-24 11:19:15 UTC 41,695 bytes, 747 lines download raw bytes

1/* RARA Naver map renderer shared by write, published-post and member-list pages. */
2(function(window, document){
3	'use strict';
4
5	/* 지역검색 API 키와 NAVER Cloud Maps 키는 서로 다른 자격증명입니다. */
6	var MAP_KEY = String(window.raraNaverMapClientId || '').replace(/^\s+|\s+$/g, '');
7	var SDK_URL = MAP_KEY ? ('https://oapi.map.naver.com/openapi/v3/maps.js?ncpKeyId=' + encodeURIComponent(MAP_KEY) + '&submodules=geocoder') : '';
8	var DEFAULT_PLACE = {
9		name: '네이버 1784',
10		query: '네이버 1784',
11		roadAddress: '경기도 성남시 분당구 정자일로 95',
12		address: '경기도 성남시 분당구 정자동 178-1',
13		x: '127.1054328',
14		y: '37.3595963',
15		mapUrl: 'https://map.naver.com/p/search/' + encodeURIComponent('네이버 1784')
16	};
17	var sdkPromise = null;
18	var observer = null;
19	var PUBLISHED_CONTENT_SELECTOR = '#bo_v_con, .member-view-content, .member-notice-content';
20
21	function toText(value){
22		return String(value == null ? '' : value).replace(/\s+/g, ' ').trim();
23	}
24
25	function escapeHtml(value){
26		return String(value == null ? '' : value)
27			.replace(/&/g, '&')
28			.replace(/</g, '&lt;')
29			.replace(/>/g, '&gt;')
30			.replace(/"/g, '&quot;')
31			.replace(/'/g, '&#39;');
32	}
33
34	function normalizeCoord(x, y){
35		var lng = parseFloat(String(x || '').replace(/,/g, ''));
36		var lat = parseFloat(String(y || '').replace(/,/g, ''));
37		if(!isFinite(lng) || !isFinite(lat)) return null;
38		if(Math.abs(lng) > 180) lng = lng / 10000000;
39		if(Math.abs(lat) > 90) lat = lat / 10000000;
40		if(lng < 120 || lng > 135 || lat < 30 || lat > 45) return null;
41		return {lat:lat, lng:lng};
42	}
43
44	function placeEndpoint(){
45		if(window.raraJoditPlaceSearchUrl) return window.raraJoditPlaceSearchUrl;
46		var imageProxy = window.raraJoditImageProxyUrl || '';
47		if(imageProxy && imageProxy.indexOf('image_proxy.php') !== -1){
48			return imageProxy.replace('image_proxy.php', 'naver_place_search.php');
49		}
50		return '/plugin/editor/jodit/naver_place_search.php';
51	}
52
53	function staticMapEndpoint(){
54		if(window.raraJoditStaticMapUrl) return window.raraJoditStaticMapUrl;
55		var imageProxy = window.raraJoditImageProxyUrl || '';
56		if(imageProxy && imageProxy.indexOf('image_proxy.php') !== -1){
57			return imageProxy.replace('image_proxy.php', 'naver_static_map.php');
58		}
59		return '/plugin/editor/jodit/naver_static_map.php';
60	}
61
62	function mapUrl(name){
63		return 'https://map.naver.com/p/search/' + encodeURIComponent(name || '');
64	}
65
66	function staticMapUrl(place){
67		place = place || {};
68		var x = place.x || place.mapx || '';
69		var y = place.y || place.mapy || '';
70		if(!normalizeCoord(x, y)) return '';
71		return staticMapEndpoint() + '?x=' + encodeURIComponent(x) + '&y=' + encodeURIComponent(y) + '&level=19&name=' + encodeURIComponent(place.name || place.query || '');
72	}
73
74	function cardPlace(card){
75		var body = card ? card.querySelector('.rara-naver-place-body') : null;
76		var titleNode = body ? (body.querySelector('.rara-naver-place-title, strong') || body.firstElementChild) : null;
77		var addressNode = body ? (body.querySelector('.rara-naver-place-address, em') || (titleNode && titleNode.nextElementSibling)) : null;
78		var name = toText(card && card.getAttribute('data-rara-place-name'));
79		if(!name || name === '장소') name = toText(titleNode ? titleNode.textContent : '');
80		var address = toText(card && card.getAttribute('data-rara-place-address'));
81		if(!address) address = toText(addressNode ? addressNode.textContent : '');
82		var savedMapUrl = card ? (card.getAttribute('data-rara-place-url') || card.getAttribute('href') || '') : '';
83		return {
84			name:name || '장소',
85			query:name || '장소',
86			roadAddress:address,
87			x:card ? (card.getAttribute('data-rara-place-mapx') || '') : '',
88			y:card ? (card.getAttribute('data-rara-place-mapy') || '') : '',
89			mapUrl:savedMapUrl || mapUrl(name)
90		};
91	}
92
93	function placeholderMarkup(place){
94		place = place || {};
95		var image = staticMapUrl(place);
96		if(image){
97			return '<img class="rara-naver-place-static-map" src="' + escapeHtml(image) + '" alt="' + escapeHtml(place.name || place.query || '장소') + ' 지도" loading="lazy" decoding="async">';
98		}
99		return '<span class="rara-naver-place-grid" aria-hidden="true"></span><span class="rara-naver-place-pin" aria-hidden="true"><i></i></span>';
100	}
101
102	function coordFromStaticImage(card){
103		var img = card ? card.querySelector('.rara-naver-place-map img[src*="naver_static_map.php"]') : null;
104		if(!img) return null;
105		try {
106			var url = new URL(img.getAttribute('src'), window.location.href);
107			return normalizeCoord(url.searchParams.get('x'), url.searchParams.get('y'));
108		} catch(e) {
109			return null;
110		}
111	}
112
113	function ensureSdk(){
114		if(window.naver && window.naver.maps) return Promise.resolve(window.naver.maps);
115		if(!SDK_URL) return Promise.reject(new Error('NAVER Maps Client ID is not configured'));
116		if(sdkPromise) return sdkPromise;
117		sdkPromise = new Promise(function(resolve, reject){
118			var existing = document.querySelector('script[data-rara-naver-map-sdk]');
119			if(existing){
120				if(window.naver && window.naver.maps){
121					resolve(window.naver.maps);
122					return;
123				}
124				existing.addEventListener('load', function(){
125					if(window.naver && window.naver.maps) resolve(window.naver.maps);
126					else reject(new Error('Naver Maps SDK unavailable'));
127				}, {once:true});
128				existing.addEventListener('error', reject, {once:true});
129				return;
130			}
131			var script = document.createElement('script');
132			script.src = SDK_URL;
133			script.async = true;
134			script.defer = true;
135			script.setAttribute('data-rara-naver-map-sdk', '1');
136			script.onload = function(){
137				if(window.naver && window.naver.maps) resolve(window.naver.maps);
138				else reject(new Error('Naver Maps SDK unavailable'));
139			};
140			script.onerror = reject;
141			(document.head || document.documentElement).appendChild(script);
142		});
143		return sdkPromise;
144	}
145
146	function ensureStyle(){
147		if(document.getElementById('rara-naver-map-render-style')) return;
148		var style = document.createElement('style');
149		style.id = 'rara-naver-map-render-style';
150		style.textContent = [
151			'.rara-naver-place-card{display:block!important;width:100%!important;max-width:802px!important;margin:28px auto!important;border:1px solid #e1e5e9!important;border-radius:8px!important;background:#fff!important;color:#172033!important;text-decoration:none!important;box-shadow:0 10px 30px rgba(15,23,42,.08)!important;box-sizing:border-box!important;overflow:hidden!important;text-align:left!important;}',
152			'.rara-naver-place-map{position:relative!important;display:block!important;width:100%!important;height:360px!important;min-height:360px!important;border-bottom:1px solid #eef0f2!important;background:#e8f1ea!important;overflow:hidden!important;}',
153			'.rara-naver-map-canvas,.rara-naver-map-fallback{position:absolute!important;inset:0!important;display:flex!important;width:100%!important;height:100%!important;min-height:100%!important;flex-direction:column!important;align-items:center!important;justify-content:center!important;gap:8px!important;background:linear-gradient(145deg,#edf7f1 0%,#e5f1e9 100%)!important;}',
154			'.rara-naver-map-fallback:before{content:"";position:absolute;inset:0;background-image:linear-gradient(rgba(255,255,255,.7) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.7) 1px,transparent 1px);background-size:64px 64px;pointer-events:none;}',
155			'.rara-naver-map-fallback-brand,.rara-naver-map-fallback-text{position:relative;z-index:1;display:block;text-align:center;}',
156			'.rara-naver-map-fallback-brand{color:#03c75a;font-size:24px;font-weight:700;letter-spacing:-.04em;}',
157			'.rara-naver-map-fallback-text{color:#64748b;font-size:14px;font-weight:500;}',
158			'.rara-naver-place-map.is-map-ready>.rara-naver-place-grid,.rara-naver-place-map.is-map-ready>.rara-naver-place-logo,.rara-naver-place-map.is-map-ready>.rara-naver-place-pin{display:none!important;}',
159			'.rara-naver-place-static-map{display:block!important;width:100%!important;height:100%!important;max-width:none!important;margin:0!important;object-fit:cover!important;}',
160			'.rara-naver-place-body{display:flex!important;min-height:103px!important;padding:22px 26px 24px!important;flex-direction:column!important;align-items:flex-start!important;justify-content:center!important;gap:8px!important;background:#fff!important;box-sizing:border-box!important;text-align:left!important;}',
161			'.rara-naver-place-body>.rara-naver-place-title,.rara-naver-place-body>strong,.rara-naver-place-body>:first-child{display:block!important;width:100%!important;margin:0!important;color:#172033!important;font-size:19px!important;font-weight:650!important;line-height:1.4!important;text-align:left!important;box-sizing:border-box!important;}',
162			'.rara-naver-place-body>.rara-naver-place-address,.rara-naver-place-body>em,.rara-naver-place-body>:nth-child(2){display:block!important;width:100%!important;margin:0!important;color:#697386!important;font-size:14px!important;font-style:normal!important;font-weight:400!important;line-height:1.55!important;text-align:left!important;box-sizing:border-box!important;}',
163			'.rara-map-loading,.rara-map-error{position:absolute;
163left:50%;top:50%;z-index:5;transform:translate(-50%,-50%);display:inline-flex;align-items:center;justify-content:center;min-width:148px;height:40px;padding:0 16px;border:1px solid rgba(226,232,240,.9);border-radius:999px;background:rgba(255,255,255,.95);box-shadow:0 8px 24px rgba(15,23,42,.12);color:#475569;font-size:14px;font-weight:600;white-space:nowrap;box-sizing:border-box;}',
164			'.rara-map-loading:before{content:"";width:14px;height:14px;margin-right:8px;border:2px solid #cbd5e1;border-top-color:#03c75a;border-radius:50%;animation:rara-map-spin .8s linear infinite;box-sizing:border-box;}',
165			'.rara-map-error{color:#0a7f3f;}',
166			'.rara-map-error:before{content:none;}',
167			'.jodit-wysiwyg .rara-naver-map-canvas,.jodit-wysiwyg .rara-naver-map-fallback{pointer-events:none!important;}',
168			'.rara-place-preview-map .rara-naver-map-canvas,.rara-place-preview-map .rara-naver-map-fallback{pointer-events:auto!important;}',
169			'@keyframes rara-map-spin{to{transform:rotate(360deg)}}',
170			'@media(max-width:768px){.rara-naver-place-card{max-width:100%!important;margin:22px auto!important;border-radius:6px!important}.rara-naver-place-map{height:260px!important;min-height:260px!important}.rara-naver-place-body{min-height:94px!important;padding:18px 20px 20px!important}.rara-naver-place-body>.rara-naver-place-title,.rara-naver-place-body>strong,.rara-naver-place-body>:first-child{font-size:17px!important}}',
171			'@media(max-width:768px){#bo_v_con.rara-has-place-card,.member-view-content.rara-has-place-card{overflow:visible!important}}',
172			'@media(max-width:768px){#bo_v_con .rara-naver-place-card,#bo_v_con .rara-naver-place-card[data-rara-block-align],.member-view-content .rara-naver-place-card,.member-view-content .rara-naver-place-card[data-rara-block-align]{display:block!important;width:100vw!important;width:100dvw!important;max-width:100vw!important;max-width:100dvw!important;margin-top:28px!important;margin-right:calc(50% - 50vw)!important;margin-right:calc(50% - 50dvw)!important;margin-bottom:28px!important;margin-left:calc(50% - 50vw)!important;margin-left:calc(50% - 50dvw)!important;border:0!important;border-radius:0!important;background:#fff!important;box-shadow:none!important;overflow:hidden!important}}',
173			'@media(max-width:768px){#bo_v_con .rara-naver-place-map,.member-view-content .rara-naver-place-map{width:100%!important;height:auto!important;min-height:0!important;aspect-ratio:20/9!important;border-bottom:1px solid #e6e6e6!important;background:#eef2ef!important;isolation:isolate!important;touch-action:pan-y!important;-webkit-user-select:none!important;user-select:none!important}}',
174			'@media(max-width:768px){#bo_v_con .rara-naver-place-map .rara-naver-map-canvas,#bo_v_con .rara-naver-place-map .rara-naver-map-canvas *,#bo_v_con .rara-naver-place-map .rara-naver-map-fallback,#bo_v_con .rara-naver-place-map .rara-naver-map-fallback *,#bo_v_con .rara-naver-place-map .rara-naver-place-static-map,.member-view-content .rara-naver-place-map .rara-naver-map-canvas,.member-view-content .rara-naver-place-map .rara-naver-map-canvas *,.member-view-content .rara-naver-place-map .rara-naver-map-fallback,.member-view-content .rara-naver-place-map .rara-naver-map-fallback *,.member-view-content .rara-naver-place-map .rara-naver-place-static-map{pointer-events:none!important;touch-action:pan-y!important;-webkit-user-select:none!important;user-select:none!important}}',
175			'@media(max-width:768px){#bo_v_con .rara-naver-place-map::after,.member-view-content .rara-naver-place-map::after{content:""!important;position:absolute!important;inset:0!important;z-index:2147483646!important;display:block!important;background:transparent!important;pointer-events:auto!important;touch-action:pan-y!important;-webkit-tap-highlight-color:transparent!important}}',
176			'@media(max-width:768px){#bo_v_con .rara-naver-place-body,.member-view-content .rara-naver-place-body{min-height:116px!important;padding:18px 20px 20px!important;gap:5px!important;border-bottom:1px solid #e0e0e0!important;background:#fff!important;justify-content:flex-start!important}}',
177			'@media(max-width:768px){#bo_v_con .rara-naver-place-body>.rara-naver-place-title,#bo_v_con .rara-naver-place-body>strong,#bo_v_con .rara-naver-place-body>:first-child,.member-view-content .rara-naver-place-body>.rara-naver-place-title,.member-view-content .rara-naver-place-body>strong,.member-view-content .rara-naver-place-body>:first-child{color:#333!important;font-size:18px!important;font-weight:600!important;line-height:1.35!important;letter-spacing:-.035em!important}}',
178			'@media(max-width:768px){#bo_v_con .rara-naver-place-body>.rara-naver-place-address,#bo_v_con .rara-naver-place-body>em,#bo_v_con .rara-naver-place-body>:nth-child(2),.member-view-content .rara-naver-place-body>.rara-naver-place-address,.member-view-content .rara-naver-place-body>em,.member-view-content .rara-naver-place-body>:nth-child(2){color:#767676!important;font-size:14px!important;line-height:1.5!important;letter-spacing:-.02em!important}}',
179			'@media(min-width:769px){#bo_v_con .rara-naver-place-card,.member-view-content .rara-naver-place-card{position:relative!important;left:auto!important;display:block!important;width:100%!important;max-width:100%!important;margin:28px auto!important;border:1px solid #e0e0e0!important;border-radius:0!important;background:#fff!important;box-shadow:0 1px 2px rgba(0,0,0,.12)!important;transform:none!important;overflow:hidden!important;box-sizing:border-box!important}}',
180			'@media(min-width:769px){#bo_v_con .rara-naver-place-map,.member-view-content .rara-naver-place-map{width:100%!important;height:auto!important;min-height:0!important;aspect-ratio:20/9!important;border-bottom:0!important;background:#f4f5f3!important}}',
181			'@media(min-width:769px){#bo_v_con .rara-naver-place-body,.member-view-content .rara-naver-place-body{height:75px!important;min-height:75px!important;padding:14px 22px 15px!important;gap:0!important;border:0!important;background:#fff!important;justify-content:flex-start!important}}',
182			'@media(min-width:769px){#bo_v_con .rara-naver-place-body>.rara-naver-place-title,#bo_v_con .rara-naver-place-body>strong,#bo_v_con .rara-naver-place-body>:first-child,.member-view-content .rara-naver-place-body>.rara-naver-place-title,.member-view-content .rara-naver-place-body>strong,.member-view-content .rara-naver-place-body>:first-child{color:#333!important;font-size:18px!important;font-weight:400!important;line-height:24px!important;letter-spacing:-.025em!important}}',
183			'@media(min-width:769px){#bo_v_con .rara-naver-place-body>.rara-naver-place-address,#bo_v_con .rara-naver-place-body>em,#bo_v_con .rara-naver-place-body>:nth-child(2),.member-view-content .rara-naver-place-body>.rara-naver-place-address,.member-view-content .rara-naver-place-body>em,.member-view-content .rara-naver-place-body>:nth-child(2){margin-top:4px!important;color:#999!important;font-size:13px!important;line-height:18px!important;letter-spacing:-.02em!important}}',
184			'@media(min-width:1920px){#bo_v_con .rara-naver-place-card.rara-public-place-card,.member-view-content .rara-naver-place-card.rara-public-place-card{width:100%!important;max-width:100%!important;margin-top:28px!important;margin-bottom:28px!important;border-width:.052083333vw!important;box-shadow:0 .052083333vw .104166667vw rgba(0,0,0,.12)!important}}',
185			'@media(min-width:1920px){#bo_v_con .rara-naver-place-card.rara-public-place-card .rara-naver-place-body,.member-view-content .rara-naver-place-card.rara-public-place-card .rara-naver-place-body{height:3.90625vw!important;min-height:3.90625vw!important;padding:.729166667vw 1.145833333vw .78125vw!important}}',
186			'@media(min-width:1920px){#bo_v_con .rara-naver-place-card.rara-public-place-card .rara-naver-place-body>.rara-naver-place-title,#bo_v_con .rara-naver-place-card.rara-public-place-card .rara-naver-place-body>strong,#bo_v_con .rara-naver-place-card.rara-public-place-card .rara-naver-place-body>:first-child,.member-view-content .rara-naver-place-card.rara-public-place-card .rara-naver-place-body>.rara-naver-place-title,.member-view-content .rara-naver-place-card.rara-public-place-card .rara-naver-place-body>strong,.member-view-content .rara-naver-place-card.rara-public-place-card .rara-naver-place-body>:first-child{font-size:.9375vw!important;line-height:1.25vw!important}}',
187			'@media(min-width:1920px){#bo_v_con .rara-naver-place-card.rara-public-place-card .rara-naver-place-body>.rara-naver-place-address,#bo_v_con .rara-naver-place-card.rara-public-place-card .rara-naver-place-body>em,#bo_v_con .rara-naver-place-card.rara-public-place-card .rara-naver-place-body>:nth-child(2),.member-view-content .rara-naver-place-card.rara-public-place-card .rara-naver-place-body>.rara-naver-place-address,.member-view-content .rara-naver-place-card.rara-public-place-card .rara-naver-place-body>em,.member-view-content .rara-naver-place-card.rara-public-place-card .rara-naver-place-body>:nth-child(2){margin-top:.208333333vw!important;font-size:.677083333vw!important;line-height:.9375vw!important}}'
188		].join('');
189		/* Public detail and company landing pages must share one place-card style. */
190		style.textContent = style.textContent.replace(
191			/\.member-view-content/g,
192			':is(.member-view-content,.member-notice-content)'
193		);
194		document.head.appendChild(style);
195	}
196
197	function setMessage(target, text, cls){
198		if(!target) return;
199		target.classList.remove('is-map-ready');
200		target.innerHTML = '<span class="' + (cls || 'rara-map-loading') + '">' + escapeHtml(text) + '</span>';
201	}
202
203	function renderOfficialStaticMap(target, coords, name, token){
204		if(!target || !coords || typeof window.fetch !== 'function') return Promise.resolve(false);
205		var url = staticMapUrl({x:coords.lng, y:coords.lat, name:name || '장소'});
206		if(!url) return Promise.resolve(false);
207		return window.fetch(url, {credentials:'same-origin', cache:'force-cache'}).then(function(response){
208			var type = String(response.headers && response.headers.get ? response.headers.get('content-type') 
208: '').toLowerCase();
209			if(!response.ok || !/^image\/(?:png|jpe?g|webp)/.test(type)) return false;
210			return response.blob().then(function(blob){
211				if(target.__raraMapToken !== token) return false;
212				var objectUrl = window.URL && window.URL.createObjectURL ? window.URL.createObjectURL(blob) : '';
213				if(!objectUrl) return false;
214				target.classList.add('is-map-ready');
215				target.innerHTML = '<img class="rara-naver-place-static-map" src="' + escapeHtml(objectUrl) + '" alt="' + escapeHtml(name || '장소') + ' 네이버 지도" loading="eager" decoding="async">';
216				var image = target.querySelector('.rara-naver-place-static-map');
217				if(image) image.addEventListener('load', function(){ window.URL.revokeObjectURL(objectUrl); }, {once:true});
218				return true;
219			});
220		}).catch(function(){ return false; });
221	}
222
223	function fallbackMap(target, coords, name, token){
224		if(!target || target.__raraMapToken !== token) return;
225		if(!coords){
226			setMessage(target, '위치 정보를 확인할 수 없습니다', 'rara-map-error');
227			return;
228		}
229		target.classList.add('is-map-ready');
230		target.innerHTML = '<span class="rara-naver-map-fallback" role="img" aria-label="' + escapeHtml(name || '장소') + ' 네이버 지도">' +
231			'<strong class="rara-naver-map-fallback-brand">NAVER 지도</strong>' +
232			'<span class="rara-naver-map-fallback-text">장소 정보 영역에서 네이버 지도를 확인할 수 있습니다.</span>' +
233		'</span>';
234	}
235
236	function lockMapCanvasFocus(canvas){
237		if(!canvas || !canvas.setAttribute) return;
238		canvas.setAttribute('aria-hidden', 'true');
239		canvas.setAttribute('tabindex', '-1');
240		canvas.querySelectorAll('[tabindex],a[href],button,input,select,textarea').forEach(function(node){
241			node.setAttribute('tabindex', '-1');
242		});
243	}
244
245	function renderMap(target, coords, name, owner){
246		if(!target || !coords) return Promise.reject(new Error('No coordinates'));
247		ensureStyle();
248		var editorStaticOnly = !!(owner && owner.nodeType === 1 && isEditorCard(owner));
249		var publicMobileStatic = !!(owner && owner.nodeType === 1 && isPublishedMobileCard(owner));
250		var publicDesktopMap = !!(owner && owner.nodeType === 1 && publishedBoundary(owner) && !publicMobileStatic);
251		var token = (target.__raraMapToken || 0) + 1;
252		target.__raraMapToken = token;
253		if(publicMobileStatic){
254			var accessiblePlace = cardPlace(owner);
255			target.classList.add('rara-map-touch-locked');
256			target.setAttribute('role', 'img');
257			target.setAttribute('aria-label', (name || accessiblePlace.name || '장소') +
258				(accessiblePlace.roadAddress ? ', ' + accessiblePlace.roadAddress : '') + ' 위치 지도');
259			target.removeAttribute('tabindex');
260		} else {
261			target.classList.remove('rara-map-touch-locked');
262			target.removeAttribute('role');
263			target.removeAttribute('aria-label');
264		}
265		setMessage(target, '지도 불러오는 중');
266		/*
267		 * Keep contenteditable static so Jodit never serializes NAVER's tile DOM.
268		 * Mobile published content intentionally uses the official static image so
269		 * a swipe always scrolls the article instead of panning or zooming the map.
270		 */
271		if(editorStaticOnly){
272			return renderOfficialStaticMap(target, coords, name, token).then(function(rendered){
273				if(rendered || target.__raraMapToken !== token) return null;
274				target.classList.add('is-map-ready');
275				target.innerHTML = placeholderMarkup({x:coords.lng, y:coords.lat, name:name || '장소'});
276				return null;
277			});
278		}
279
280		var staticFirst = publicMobileStatic
281			? renderOfficialStaticMap(target, coords, name, token)
282			: Promise.resolve(false);
283
284		return staticFirst.then(function(rendered){
285			if(rendered || target.__raraMapToken !== token) return null;
286			return ensureSdk();
287		}).then(function(maps){
288			if(!maps || target.__raraMapToken !== token) return null;
289			target.classList.add('is-map-ready');
290			target.innerHTML = '';
291			var canvas = document.createElement('span');
292			canvas.className = 'rara-naver-map-canvas';
293			canvas.setAttribute('data-rara-interactive-map', '1');
294			if(publicMobileStatic){
295				lockMapCanvasFocus(canvas);
296			}
297			target.appendChild(canvas);
298			var center = new maps.LatLng(coords.lat, coords.lng);
299			var publishedInteractionLimited = publicMobileStatic || publicDesktopMap;
300			var zoomControlOptions = {position:maps.Position.TOP_RIGHT};
301			if(publicDesktopMap && maps.ZoomControlStyle && maps.ZoomControlStyle.SMALL !== undefined){
302				zoomControlOptions.style = maps.ZoomControlStyle.SMALL;
303			}
304			var map = new maps.Map(canvas, {
305				center:center,
306				zoom:publicDesktopMap ? 17 : 19,
307				minZoom:7,
308				draggable:!publicMobileStatic,
309				scrollWheel:!publishedInteractionLimited,
310				pinchZoom:!publishedInteractionLimited,
311				keyboardShortcuts:!publishedInteractionLimited,
312				disableDoubleClickZoom:publishedInteractionLimited,
313				disableDoubleTapZoom:publishedInteractionLimited,
314				disableTwoFingerTapZoom:publishedInteractionLimited,
315				zoomControl:!publicMobileStatic,
316				zoomControlOptions:zoomControlOptions,
317				mapDataControl:false,
318				scaleControl:publicDesktopMap,
319				logoControl:true,
320				logoControlOptions:{position:maps.Position.BOTTOM_LEFT}
321			});
322			if(publicMobileStatic) lockMapCanvasFocus(canvas);
323			var markerOptions = {position:center, map:map, title:name || '장소'};
324			if(publicDesktopMap && maps.Point){
325				var markerScale = Math.max(1, Number(window.innerWidth || 0) / 1920);
326				var markerPx = function(value){ return (value * markerScale).toFixed(3) + 'px'; };
327				var markerIcon = {
328					content:'<span aria-hidden="true" style="position:relative;display:block;width:' + markerPx(34) + ';height:' + markerPx(42) + '"><span style="position:absolute;
328left:' + markerPx(2) + ';top:' + markerPx(1) + ';width:' + markerPx(30) + ';height:' + markerPx(30) + ';border:' + markerPx(1) + ' solid #00a84f;border-radius:50% 50% 50% 0;background:#03c75a;box-shadow:0 ' + markerPx(1) + ' ' + markerPx(2) + ' rgba(0,0,0,.22);box-sizing:border-box;transform:rotate(-45deg)"></span><span style="position:absolute;left:' + markerPx(12) + ';top:' + markerPx(12) + ';width:0;height:0;border-left:' + markerPx(5) + ' solid transparent;border-right:' + markerPx(5) + ' solid transparent;border-top:' + markerPx(7) + ' solid #fff"></span></span>',
329					anchor:new maps.Point(17 * markerScale, 38 * markerScale)
330				};
331				if(maps.Size) markerIcon.size = new maps.Size(34 * markerScale, 42 * markerScale);
332				markerOptions.icon = markerIcon;
333			}
334			var marker = new maps.Marker(markerOptions);
335			if(owner){
336				owner.__raraNaverMap = map;
337				owner.__raraNaverMarker = marker;
338			}
339			target.__raraNaverMap = map;
340			target.__raraNaverMarker = marker;
341			setTimeout(function(){
342				if(target.__raraMapToken !== token) return;
343				if(publicMobileStatic) lockMapCanvasFocus(canvas);
344				try { maps.Event.trigger(map, 'resize'); } catch(e){}
345				try { map.setCenter(center); } catch(e){}
346			}, 140);
347			setTimeout(function(){
348				if(target.__raraMapToken !== token || !canvas.isConnected) return;
349				if(publicMobileStatic) lockMapCanvasFocus(canvas);
350				/* 인증 실패 시 타사 지도로 바꾸지 않고 NAVER 전용 안내 화면을 유지합니다. */
351				if(!canvas.firstElementChild){
352					fallbackMap(target, coords, name, token);
353				}
354			}, 1200);
355			return map;
356		}).catch(function(){
357			return renderOfficialStaticMap(target, coords, name, token).then(function(rendered){
358				if(!rendered) fallbackMap(target, coords, name, token);
359				return null;
360			});
361		});
362	}
363
364	function fetchCoords(card){
365		if(card.__raraMapFetchPromise) return card.__raraMapFetchPromise;
366		var place = cardPlace(card);
367		var endpoint = placeEndpoint();
368		var separator = endpoint.indexOf('?') === -1 ? '?' : '&';
369		card.__raraMapFetchPromise = fetch(endpoint + separator + 'mode=resolve&query=' + encodeURIComponent(place.name) + '&address=' + encodeURIComponent(place.roadAddress || ''), {
370			credentials:'same-origin',
371			cache:'no-store'
372		}).then(function(res){
373			if(!res.ok) throw new Error('Place resolve failed: ' + res.status);
374			return res.json();
375		}).then(function(data){
376			if(!data || data.success === false) return null;
377			var first = data && data.list ? data.list[0] : null;
378			if(!first) return null;
379			card.setAttribute('data-rara-place-name', first.name || place.name);
380			card.setAttribute('data-rara-place-address', first.roadAddress || first.address || '');
381			card.setAttribute('data-rara-place-mapx', first.x || first.mapx || '');
382			card.setAttribute('data-rara-place-mapy', first.y || first.mapy || '');
383			card.setAttribute('data-rara-place-url', first.mapUrl || mapUrl(first.name || place.name));
384			var bodyName = card.querySelector('.rara-naver-place-body strong');
385			var bodyAddr = card.querySelector('.rara-naver-place-body em');
386			if(bodyName && first.name) bodyName.textContent = first.name;
387			if(bodyAddr && (first.roadAddress || first.address)) bodyAddr.textContent = first.roadAddress || first.address;
388			return normalizeCoord(first.x || first.mapx, first.y || first.mapy);
389		}).catch(function(){ return null; }).then(function(coords){
390			if(!coords) card.__raraMapFetchPromise = null;
391			return coords;
392		});
393		return card.__raraMapFetchPromise;
394	}
395
396	function geocodeCoords(card){
397		var place = cardPlace(card);
398		var query = place.roadAddress || place.name;
399		if(!query) return Promise.resolve(null);
400		return ensureSdk().then(function(maps){
401			return new Promise(function(resolve){
402				if(!maps || !maps.Service || typeof maps.Service.geocode !== 'function'){
403					resolve(null);
404					return;
405				}
406				maps.Service.geocode({query:query}, function(status, response){
407					if(status !== maps.Service.Status.OK || !response || !response.v2 || !response.v2.addresses || !response.v2.addresses[0]){
408						resolve(null);
409						return;
410					}
411					var address = response.v2.addresses[0];
412					card.setAttribute('data-rara-place-mapx', address.x || '');
413					card.setAttribute('data-rara-place-mapy', address.y || '');
414					resolve(normalizeCoord(address.x, address.y));
415				});
416			});
417		}).catch(function(){ return null; });
418	}
419
420	function isEditorCard(card){
421		return !!(card && card.closest && card.closest('.jodit-wysiwyg'));
422	}
423
424	function publishedBoundary(card){
425		if(!card || !card.closest || isEditorCard(card)) return null;
426		return card.closest(PUBLISHED_CONTENT_SELECTOR);
427	}
428
429	function isPublishedMobileCard(card){
430		if(!publishedBoundary(card)) return false;
431		return !!(window.matchMedia && window.matchMedia('(max-width: 768px)').matches);
432	}
433
434	function markPublishedCard(card){
435		var boundary = publishedBoundary(card);
436		if(!boundary) return;
437		boundary.classList.add('rara-has-place-card');
438		card.classList.add('rara-public-place-card');
439	}
440
441	function hasMapTarget(card){
442		return !!(card && card.querySelector && card.querySelector('.rara-naver-place-map'));
443	}
444
445	function isNaverSdkImage(image){
446		var src = image ? String(image.getAttribute('src') || image.src || '').toLowerCase() : '';
447		if(!src) return false;
448		return /(?:pstatic\.net|naver\.net|naver\.com)/.test(src) &&
449			/(?:\/styles\/basic\/|\/static\/maps\/|maptile|map\/tile|zoom-|sprite|mantle)/.test(src);
450	}
451
452	function isLeakedMapCard(card){
453		if(!card || hasMapTarget(card)) return false;
454		var href = String(card.getAttribute('href') || card.getAttribute('data-rara-place-url') || '').toLowerCase();
455		if(href.indexOf('map.naver.com') === -1) return false;
456		var images = card.querySelectorAll('img');
457		if(!images.length) return false;
458		return Array.prototype.every.call(images, isNaverSdkImage);
459	}
460
461	function contentBoundary(node, root){
462		var boundary = node && node.closest ? node.closest('.jodit-wysiwyg, ' + PUBLISHED_CONTENT_SELECTOR) : null;
463		if(boundary) return boundary;
464		if(root && root.nodeType === 1) return root;
465		return document.body || document.documentElement;
466	}
467
468	function directChildOf(node, boundary){
469		var current = node;
470		while(current && current.parentElement && current.parentElement !== boundary){
471			current = current.parentElement;
472		}
473		return current;
474	}
475
476	function isLeakedSdkTree(node){
477		if(!node || !node.querySelectorAll) return false;
478		var cards = node.querySelectorAll('.rara-naver-place-card');
479		if(!cards.length) return false;
480		var leaked = 0;
481		for(var i = 0; i < cards.length; i++){
482			if(hasMapTarget(cards[i])) return false;
483			if(isLeakedMapCard(cards[i])) leaked++;
484		}
485		if(!leaked) return false;
486		var images = node.querySelectorAll('img');
487		if(!images.length || !Array.prototype.every.call(images, isNaverSdkImage)) return false;
488		return !toText(node.textContent || '');
489	}
490
491	function isLeakedSdkFragment(node){
492		if(!node || !node.querySelectorAll) return false;
493		if(isLeakedSdkTree(node)) return true;
494		if(node.querySelector('.rara-naver-place-map')) return false;
495		var images = node.querySelectorAll('img');
496		if(!images.length || !Array.prototype.every.call(images, isNaverSdkImage)) return false;
497		var text = toText(node.textContent || '').replace(/지도 컨트롤러 범례|부동산|거리|읍,면,동|시,군,구|시,도|국가/g, '');
498		return !toText(text);
499	}
500
501	/* Repair content saved before the editor-only static preview was introduced. */
502	function repairLeakedMapArtifacts(root){
503		var scope = root && root.querySelectorAll ? root : document;
504		var validCards = Array.prototype.filter.call(scope.querySelectorAll('.rara-naver-place-card'), hasMapTarget);
505		validCards.forEach(function(card){
506			if(card.querySelector('.rara-naver-place-body')) return;
507			var boundary = contentBoundary(card, scope);
508			var top = directChildOf(card, boundary);
509			var cursor = top ? top.nextElementSibling : null;
510			var artifacts = [];
511			for(var i = 0; cursor && i < 8; i++){
512				var next = cursor.nextElementSibling;
513				if(isLeakedSdkFragment(cursor)){
514					artifacts.push(cursor);
515					cursor = next;
516					continue;
517				}
518				if(cursor.matches && cursor.matches('.rara-naver-place-body')){
519					card.appendChild(cursor);
520					break;
521				}
522				break;
523			}
524			artifacts.forEach(function(node){
525				if(node.parentNode) node.parentNode.removeChild(node);
526			});
527		});
528
529		var leakedCards = Array.prototype.filter.call(scope.querySelectorAll('.rara-naver-place-card'), isLeakedMapCard);
530		var removals = [];
531		leakedCards.forEach(function(card){
532			var boundary = contentBoundary(card, scope);
533			var top = directChildOf(card, boundary);
534			var target = isLeakedSdkTree(top) ? top : card;
535			if(removals.indexOf(target) === -1) removals.push(target);
536		});
537		removals.forEach(function(node){
538			if(node.parentNode) node.parentNode.removeChild(node);
539		});
540		return removals.length;
541	}
542
543	function normalizeCard(card){
544		if(!card) return;
545		markPublishedCard(card);
546		var place = cardPlace(card);
547		var body = card.querySelector('.rara-naver-place-body');
548		if(body){
549			var titleNode = body.querySelector('.rara-naver-place-title, strong') || body.firstElementChild;
550			var addressNode = body.querySelector('.rara-naver-place-address, em') || (titleNode && titleNode.nextElementSibling);
551			if(titleNode && titleNode.classList) titleNode.classList.add('rara-naver-place-title');
552			if(addressNode && addressNode.classList) addressNode.classList.add('rara-naver-place-address');
553		}
554		if(!card.getAttribute('data-rara-place-name')) card.setAttribute('data-rara-place-name', place.name);
555		if(!card.getAttribute('data-rara-place-address') && place.roadAddress) card.setAttribute('data-rara-place-address', place.roadAddress);
556		if(!card.getAttribute('data-rara-place-url')) card.setAttribute('data-rara-place-url', place.mapUrl || mapUrl(place.name));
557		card.setAttribute('data-rara-map-version', '2');
558		if(card.tagName && card.tagName.toLowerCase() === 'a'){
559			card.setAttribute('draggable', 'false');
560			if(isEditorCard(card)){
561				card.removeAttribute('href');
562				card.removeAttribute('target');
563				card.removeAttribute('rel');
564				card.setAttribute('role', 'group');
565			} else {
566				card.setAttribute('href', place.mapUrl || mapUrl(place.name));
567				card.setAttribute('target', '_blank');
568				card.setAttribute('rel', 'noopener noreferrer');
569				card.setAttribute('aria-label', place.name + ' 네이버 지도 열기');
570			}
571		}
572	}
573
574	function renderCard(card){
575		if(!card || card.__raraMapRendering) return;
576		var target = card.querySelector('.rara-naver-place-map');
577		if(!target) return;
578		normalizeCard(card);
579		card.__raraMapRendering = true;
580		var place = cardPlace(card);
581		var coords = normalizeCoord(place.x, place.y) || coordFromStaticImage(card);
582		var finish = function(found){
583			if(!found){
584				var retries = parseInt(card.__raraMapRetryCount || 0, 10) || 0;
585				if(retries < 1){
586					card.__raraMapRetryCount = retries + 1;
587					setMessage(target, '위치 정보를 다시 확인하고 있습니다', 'rara-map-loading');
588					setTimeout(function(){
589						card.__raraMapRendering = false;
590						renderCard(card);
591					}, 1200);
592					return;
593				}
594				setMessage(target, '위치 정보를 확인할 수 없습니다', 'rara-map-error');
595				return;
596			}
597			card.__raraMapRetryCount = 0;
598			renderMap(target, found, cardPlace(card).name, card);
599		};
600		if(coords){
601			finish(coords);
602			return;
603		}
604		fetchCoords(card).then(function(fetched){
605			if(fetched) return fetched;
606			return geocodeCoords(card);
607		}).then(finish);
608	}
609
610	function renderPreview(target, place){
611		place = place || DEFAULT_PLACE;
612		var coords = normalizeCoord(place.x || place.mapx, place.y || place.mapy);
613		if(coords) return renderMap(target, coords, place.name || place.query || '장소', target);
614		setMessage(target, '위치 정보를 찾는 중');
615		return fetch(placeEndpoint() + '?query=' + encodeURIComponent(place.name || place.query || ''), {
616			credentials:'same-origin', cache:'no-store'
617		}).then(function(res){ return res.json(); }).then(function(data){
618			var first = data && data.list ? data.list[0] : null;
619			var found = first ? normalizeCoord(first.x || first.mapx, first.y || first.mapy) : null;
620			if(!found) throw new Error('No coordinates');
621			return renderMap(target, found, first.name || place.name || '장소', target);
622		}).catch(function(){
623			setMessage(target, '위치 정보를 확인할 수 없습니다', 'rara-map-error');
624			return null;
625		});
626	}
627
628	function renderAll(root){
629		ensureStyle();
630		repairLeakedMapArtifacts(root || document);
631		(root || document).querySelectorAll('.rara-naver-place-card').forEach(renderCard);
632	}
633
634	function prepareEditorDomForSave(root){
635		var scope = root && root.editor ? root.editor : root;
636		if(!scope || !scope.querySelectorAll) return;
637		repairLeakedMapArtifacts(scope);
638		scope.querySelectorAll('.rara-naver-place-card').forEach(function(card){
639			var target = card.querySelector('.rara-naver-place-map');
640			if(!target) return;
641			normalizeCard(card);
642			target.__raraMapToken = (target.__raraMapToken || 0) + 1;
643			target.classList.remove('is-map-ready');
644			target.innerHTML = placeholderMarkup(cardPlace(card));
645		});
646	}
647
648	function serializeForSave(root, html){
649		var scope = root && root.editor ? root.editor : root;
650		var container = document.createElement('div');
651		if(typeof html === 'string') container.innerHTML = html;
652		else if(scope && typeof scope.innerHTML === 'string') container.innerHTML = scope.innerHTML;
653		else return '';
654		repairLeakedMapArtifacts(container);
655		container.querySelectorAll('.rara-naver-place-card').forEach(function(card){
656			var target = card.querySelector('.rara-naver-place-map');
657			if(!target) return;
658			normalizeCard(card);
659			target.classList.remove('is-map-ready');
660			target.innerHTML = placeholderMarkup(cardPlace(card));
661		});
662		container.querySelectorAll('.rara-v3-selected,.rara-v3-dragging').forEach(function(node){
663			node.classList.remove('rara-v3-selected', 'rara-v3-dragging');
664			node.removeAttribute('data-rara-v3-selected');
665		});
666		return container.innerHTML;
667	}
668
669	function prepareForSave(root, html){
670		return serializeForSave(root, html);
671	}
672
673	function bindClickGuard(){
674		if(window.__raraNaverMapClickGuard) return;
675		window.__raraNaverMapClickGuard = true;
676		document.addEventListener('click', function(e){
677			var card = e.target && e.target.closest ? e.target.closest('.rara-naver-place-card') : null;
678			if(!card) return;
679			if(isEditorCard(card) || (e.target.closest && e.target.closest('.rara-naver-place-map'))){
680				e.preventDefault();
681			}
682		}, true);
683		document.addEventListener('dragstart', function(e){
684			var card = e.target && e.target.closest ? e.target.closest('.rara-naver-place-card') : null;
685			if(card) e.preventDefault();
686		}, true);
687	}
688
689	function observe(){
690		if(observer || !window.MutationObserver) return;
691		observer = new MutationObserver(function(mutations){
692			mutations.forEach(function(mutation){
693				mutation.addedNodes && Array.prototype.forEach.call(mutation.addedNodes, function(node){
694					if(node.nodeType !== 1) return;
695					if(node.matches && node.matches('.rara-naver-place-card')) renderCard(node);
696					else if(node.querySelectorAll) renderAll(node);
697				});
698			});
699		});
700		observer.observe(document.body || document.documentElement, {childList:true, subtree:true});
701	}
702
703	function init(){
704		bindClickGuard();
705		renderAll(document);
706		observe();
707		if(!window.__raraNaverMapSubmitBound){
708			window.__raraNaverMapSubmitBound = true;
709			document.addEventListener('submit', function(e){
710				var form = e.target;
711				var area = form && form.querySelector ? form.querySelector('.jodit-wysiwyg') : null;
712				if(!area) return;
713				/* Sanitize the live map before the image-link serializer clones it. */
714				prepareEditorDomForSave(area);
715				var source = typeof window.raraSerializeImageLinks === 'function'
716					? window.raraSerializeImageLinks(area)
717					: area.innerHTML;
718				var content = prepareForSave(area, source);
719				var textarea = form.querySelector('textarea[name="wr_content"]');
720				if(textarea && content) textarea.value = content;
721			}, true);
722		}
723	}
724
725	if(document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
726	else init();
727
728	window.RaraNaverMapRender = {
729		renderAll:renderAll,
730		renderCard:renderCard,
731		renderPreview:renderPreview,
732		prepareForSave:prepareForSave,
733		serializeForSave:serializeForSave,
734		defaultPlace:function(){
735			return {
736				name:DEFAULT_PLACE.name,
737				query:DEFAULT_PLACE.query,
738				roadAddress:DEFAULT_PLACE.roadAddress,
739				address:DEFAULT_PLACE.address,
740				x:DEFAULT_PLACE.x,
741				y:DEFAULT_PLACE.y,
742				mapUrl:DEFAULT_PLACE.mapUrl
743			};
744		}
745	};
746	window.__raraNaverMapRenderVersion = 'v18-unified-public-boundaries-20260803';
747})(window, document);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.