PageSourceSearch

https://dwvil.com/js/post.js?1790781634

js dwvil.com collected 2026-10-02 00:30:28 UTC 36,920 bytes, 985 lines download raw bytes

1
2var POST = function(){
3	var code, post_body, body_input, is_notice,is_notice_input,is_secret,is_secret_input, post_form,post_subject,total_file_size;
4
5	var $board_container,$floara_obj;
6	var $post_img_library;
7	var $represent_img;
8	var $upload_cover_image_btn_obj, $delete_cover_image_btn_obj, $cover_image, $cover_image_tmp_no;
9	var $post_secret_password;
10	var $widget_wrap,board_code,listing_type,more_list_page;
11	var $listing_obj,type_data;
12	var option = {};
13	var category_list;
14	var board_data;
15	var sent = false;
16	var isIOS, isSafari, $fr_m_custom, $write_header, m_sticky_container_trigger_top, $toolbarContainer;
17
18	var reaction_token, reaction_token_key;
19
20	// macOS Chrome에서 게시물 본문의 '큰 이미지'를 드래그하면 즉시 취소되는(잡자마자 놓아지는) 문제 우회.
21	// 원인: macOS 크롬은 드래그 시 마우스를 따라다니는 미리보기(드래그 고스트)를 '화면에 펼친 실제 픽셀 크기'
22	//       기준으로 만드는데, 펼친 그림이 너무 크면 미리보기 생성에 실패하고 드래그 자체를 취소한다.
23	//       (Windows/Safari는 정상. 파일 용량이 아니라 가로x세로 픽셀 크기가 기준)
24	// 해결: 드래그 시작(dragstart) 시 가로 최대 120px짜리 작은 캔버스를 만들어 setDragImage로 지정 →
25	//       크롬이 무거운 원본으로 미리보기를 만들지 않게 하여 드래그가 취소되지 않는다.
26	//       (본문 원본 이미지 화질은 그대로, 드래그 중 따라다니는 미리보기만 축소)
27	var __macDragFixAttached = false;
28	var setupMacChromeImageDragFix = function(){
29		var ua = navigator.userAgent;
30		var isMacChrome = ua.indexOf('Macintosh') !== -1 && ua.indexOf('Chrome') !== -1 && ua.indexOf('Edg/') === -1 && ua.indexOf('OPR/') === -1;
31		if(!isMacChrome || __macDragFixAttached) return;
32		__macDragFixAttached = true;
33
34		document.addEventListener('dragstart', function(ev){
35			var target = ev.target;
36			if(!target || target.tagName !== 'IMG') return;
37			var dt = ev.dataTransfer;
38			if(!dt) return;
39			var maxW = 120;
40			var ratio = target.naturalWidth > 0 ? Math.min(1, maxW / target.naturalWidth) : 1;
41			var w = Math.max(1, Math.round((target.naturalWidth || target.width) * ratio));
42			var h = Math.max(1, Math.round((target.naturalHeight || target.height) * ratio));
43			var canvas = document.createElement('canvas');
44			canvas.width = w;
45			canvas.height = h;
46			var ctx = canvas.getContext('2d');
47			if(!ctx) return;
48			try {
49				ctx.drawImage(target, 0, 0, w, h);
50			} catch(e){
51				// CORS 등으로 그리기 실패 시 회색 박스로 대체 (드래그 자체는 성립시킴)
52				ctx.fillStyle = 'rgba(0,0,0,0.3)';
53				ctx.fillRect(0, 0, w, h);
54			}
55			canvas.style.position = 'fixed';
56			canvas.style.top = '-1000px';
57			canvas.style.left = '-1000px';
58			canvas.style.pointerEvents = 'none';
59			document.body.appendChild(canvas);
60			try {
61				dt.setDragImage(canvas, Math.round(w / 2), Math.round(h / 2));
62			} catch(e){ /* setDragImage 미지원 시 무시 */ }
63			setTimeout(function(){
64				if(canvas.parentNode) canvas.parentNode.removeChild(canvas);
65			}, 0);
66		}, true);
67	};
68
69	var postDeletePost = function (board_code,code,return_url,secret_pass){
70		if(confirm(LOCALIZE.설명_삭제하시겠습니까())){
71			$.ajax({
72				type		: 'post',
73				data:{'pcode':code,board_code:board_code,secret_pass:secret_pass},
74				url			: '/ajax/deletePost.cm',
75				dataType 	: 'json',
76				success		: function(result){
77					if(result.msg == 'SUCCESS'){
78						window.location.href = return_url;
79					}else{
80						alert(result.msg);
81					}
82				}
83			});
84		}
85	};
86
87
88	/***
89	 * 안드로이드앱에서 post글쓰기시 이미지 업로드 완료시 처리
90	 * @param image
91	 */
92
93	var $image_list_obj = {};
94	//이미지 임시저장 리스트 추가
95	if(!($image_list_obj.length > 0)) {
96		var image_list_html = $("<ul id='image_list' style='display: none'></ul>");
97		$("body form").append(image_list_html);
98		$image_list_obj = image_list_html;
99	}
100
101	var android_que = [];
102	var androidAppPostImageUploadComplete = function(image){
103		if(image.tmp_idx > 0){
104			android_que.push(image);
105			if(image.is_last == "Y") androidAppPostImageUploadAllComplete();
106		}
107	};
108
109	var androidAppPostImageUploadAllComplete = function(){
110		androidAppPostImageInsert(android_que[0]);
111	};
112
113	var androidAppPostImageInsert = function(image){
114			FroalaEditor('#post_body').image.insert(CDN_UPLOAD_URL + image.url, true);
115	};
116
117	var postAddImage = function(tmp_idx,size){
118		var uniq_id = makeUniq('image_');
119		var hidden_input = $('<input name="temp_images[]" value="' + tmp_idx + '" type="hidden" />');
120		var li 	= $('<li>').attr('id',uniq_id).data({'item':uniq_id,size:size});
121		li.append(hidden_input);
122		$image_list_obj.append(li);
123	};
124
125
126
127
128
129	var postInitWrite = function(key,data, str_category){
130		$("body").addClass("write_mode");
131		$board_container = $('#board_container');
132		post_body = $('#post_body');
133		post_subject = $('#post_subject');
134		body_input = $('#body_input');
135		post_form = $('#post_form');
136		$upload_cover_image_btn_obj = $('._upload_cover_image');
137		$delete_cover_image_btn_obj = $('._delete_cover_image');
138		$cover_image = $('#cover_image');
139		$cover_image_tmp_no = $('#cover_image_tmp_no');
140		code = key;
141		board_data = data;
142		total_file_size = 0;
143		category_list = categoryList(str_category);
144		if(IE_VERSION < 10){
145			CKEDITOR.replace( 'post_body',{
146				filebrowserImageUploadUrl: '/ajax/post_image_upload.cm?board_code='+key
147			});
148		}else{
149			if(android_version() == 4){
150				post_body.addClass('legacy_webview');
151			}
152			var placeholder = board_data.placeholder_edit;
153			if(placeholder == '') placeholder = getLocalizeString('설명_내용을_입력해주세요', '', '내용을 입력해주세요');
154      setFroala('#post_body', {
155        'code' : code,
156        'image_upload_url' : "/ajax/post_image_upload.cm",
157        'file_upload_url' : "/ajax/post_file_upload.cm",
158        'file_list_obj' : $("#file_list"),
159        'placeholderText' : placeholder,
160        'image_display' : 'inline',
161        'mobile_custom' : true
162      }, {
163        'image.inserted' : function($img, response){
164          if(IS_ANDROID_APP == 'Y'){
165            var img = post_body.find('img[src="' + CDN_UPLOAD_URL + image.url + '"]');
166            img.data(image);
167            postAddImage(image.tmp_idx, image.size);
168            android_que.splice(0, 1);
169            if(android_que.length > 0) androidAppPostImageUploadAllComplete();
170          }
171        }
172      });
173      setupMacChromeImageDragFix();
174		}
175
176		function dataURLtoBlob(dataurl) {
177			var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
178				bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
179			while(n--){
180				u8arr[n] = bstr.charCodeAt(n);
181			}
182			return new Blob([u8arr], {type:mime});
183		}
184
185		$delete_cover_image_btn_obj.on('click',function(){
186			POST.deleteCoverImage();
187		});
188
189		$upload_cover_image_btn_obj.setUploadImage({
190			url : '/ajax/upload_image.cm',
191			formData : {target : 'post', 'temp' : 'Y', 'param_name' : 'cover_image'}
192		}, function (msg, data,res) {
193			$.each(res.cover_image,function(i,file){
194				if(file.tmp_idx > 0){
195					$cover_image.val(CDN_UPLOAD_URL+file.url);
196					$cover_image_tmp_no.val(file.tmp_idx);
197					$delete_cover_image_btn_obj.show();
198					$board_container.toggleClass('bg_on',true);
199					$board_container.find('._cover_image').css('background-image',"url("+CDN_UPLOAD_URL+file.url+")");
200					$board_container.find('._cover_image_src').attr('src',CDN_UPLOAD_URL+file.url);
201				}
202			});
203		});
204
205		isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent);
206		isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
207		$fr_m_custom = $board_container.find('._fr-m-custom');
208		$write_header = $board_container.find('._write_header');
209		m_sticky_container_trigger_top = $fr_m_custom.offset().top;
210		$toolbarContainer = $fr_m_custom.find('#toolbarContainer');
211		if(isIOS && isSafari){
212			$write_header.css('position', 'absolute');
213		}
214		var timeoutTime = isIOS && isSafari ? 100 : 10;
215		var resize_time;
216		resizeStickyContainer();
217		$(window).off('scroll.mobile_write resize.mobile_write').on('scroll.mobile_write resize.mobile_write',function(){
218			var s_top = $(this).scrollTop();
219			if(isIOS && isSafari){
220				$write_header.css({'-webkit-transition': 'none', 'transition': 'none', 'top': 0});
221				if(s_top > m_sticky_container_trigger_top){
222					$toolbarContainer.css({'-webkit-transition': 'none', 'transition': 'none', 'top': 0});
223				}
224			}
225			if(resize_time) {
226				clearTimeout(resize_time);
227			}
228			resize_time = setTimeout(function() {
229				resizeStickyContainer();
230			}, timeoutTime);
231		});
232
233		// $(window).bind('beforeunload', function(){
234		// 	return LOCALIZE.설명_페이지를벗어나시겠습니까();
235		// });
236		var $_mobile_tool_bar = $('._mobile_tool_bar');
237		if($_mobile_tool_bar.find('._tool_btn:visible').length === 0){
238			$_mobile_tool_bar.hide();
239		}
240	};
241
242	function resizeStickyContainer(){
243		var s_top = $(this).scrollTop();
244		if(isIOS && isSafari){
245			$write_header.css({'-webkit-transition': 'top 100ms', 'transition': 'top 100ms', 'top': s_top + 'px'});
246			$fr_m_custom.toggleClass('m_sticky_container', s_top > m_sticky_container_trigger_top);
247			$fr_m_custom.toggleClass('m_sticky_container_ios', s_top > m_sticky_container_trigger_top);
248			if(s_top > m_sticky_container_trigger_top){
249				$toolbarContainer.css({'-webkit-transition': 'top 100ms', 'transition': 'top 100ms', 'top': s_top + 'px'});
250				post_body.css('padding-top', '50px');
251			}else{
252				$toolbarContainer.css({'-webkit-transition': 'none', 'transition': 'none', 'top': 'auto'});
253				post_body.css('padding-top', '50px');
254			}
255		}else{
256			$fr_m_custom.toggleClass('m_sticky_container', s_top > m_sticky_container_trigger_top);
257		}
258		if($(window).width() >= 768){
259			if($board_container.hasClass('bg_on'))
260				$board_container.find('#toolbarContainer').toggleClass('pc_sticky_toolbar', s_top > 487);
261			else
262				$board_container.find('#toolbarContainer').toggleClass('pc_sticky_toolbar', s_top > 180);
263		}
264	}
265
266	var deleteCoverImage = function(){
267		$cover_image.val('');
268		$cover_image_tmp_no.val('');
269		$board_container.toggleClass('bg_on',false);
270		$board_container.find('._cover_image').css('background-image','none');
271		$board_container.find('._cover_image_src').attr('src','');
272		$delete_cover_image_btn_obj.hide();
273	};
274
275
276	const setPostToken = async () => {
277		await $.ajax({
278			type: 'post',
279			url: '/set_post_client_token.cm',
280			dataType: 'json',
281			async : false
282		});
283	};
284
285	var postSubmit = async function(){
286		if(sent) return false;
287		await setPostToken(); // 토큰주입API호출
288
289		if(IE_VERSION < 10){
290			var body = CKEDITOR.instances.post_body.getData();
291			body_input.val(body);
292			post_form.submit();
293		}else{
294      if(post_body.hasClass('fr-code-view'))
295        FroalaEditor('#post_body').codeView.toggle();
296      var body = FroalaEditor('#post_body').html.get(true);
297
298			body_input.val(body);
299			post_form.submit();
300		}
301		sent = true;
302	};
303
304	var endSubmit = function(msg){
305		sent = false;
306		alert(msg);
307	};
308
309	// ── 요구형 문자 캡차 — 서버가 애매한 스팸 점수(0.50 이상·임계 미만)에서 요구한다 ──────
310	// 댓글 경로(`post_comment.js`)와 **같은 규칙·같은 모양**이다. 다른 점은 하나: 글쓰기 폼은
311	// hidden iframe 으로 제출되어(:185 `target="hidden_frame"`) 부모 창이 서버 응답을 직접 받지
312	// 못한다. 그래서 서버가 iframe 안에서 `parent.POST.showBandCaptcha(...)` 를 부르고, 이 창이
313	// 그 함수를 제공한다 — **구버전 JS(이 함수가 없는 부모 문서)와 섞여도** 서버가 typeof 로
314	// 갈라 기존 submit_error 경로로 떨어진다(`post_add.cm` 의 catch 뒤 분기).
315	//
316	// 입력칸 이름(captcha_key·captcha_answer)은 서버가 그리는 사이트 캡차와 **같다**. 블록은
317	// 반드시 폼 **안**에 있어야 한다 — 네이티브 form.submit() 이 폼 필드를 그대로 실어 보내므로,
318	// 폼 밖에 그리면 캡차 답이 서버에 도착하지 않는다.
319	var CAPTCHA_CHALLENGE_ATTR = 'data-fo-captcha-challenge';
320
321	// 사이트 캡차 화면(site_ui.cls)이 쓰는 새로고침 아이콘과 같은 모양이다.
322	var CAPTCHA_CHALLENGE_REFRESH_ICON = '<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">'
323		+ '<path d="M1 0.5H27C27.2761 0.5 27.5 0.723857 27.5 1V27C27.5 27.2761 27.2761 27.5 27 27.5H1C0.723858 27.5 0.5 27.2761 0.5 27V1C0.5 0.723858 0.723857 0.5 1 0.5Z" fill="white"/>'
324		+ '<path d="M1 0.5H27C27.2761 0.5 27.5 0.723857 27.5 1V27C27.5 27.2761 27.2761 27.5 27 27.5H1C0.723858 27.5 0.5 27.2761 0.5 27V1C0.5 0.723858 0.723857 0.5 1 0.5Z" stroke="#BCC0C6"/>'
325		+ '<path d="M15.3333 7.3335C15.3333 7.3335 15.8995 7.41438 18.2426 9.75752C20.5858 12.1007 20.5858 15.8997 18.2426 18.2428C17.4125 19.073 16.3995 19.609 15.3333 19.8509M15.3333 7.3335L19.3333 7.3335M15.3333 7.3335L15.3333 11.3335M12.6667 20.6667C12.6667 20.6667 12.1005 20.5858 9.75736 18.2427C7.41421 15.8995 7.41421 12.1005 9.75736 9.75739C10.5875 8.92721 11.6005 8.39116 12.6667 8.14925M12.6667 20.6667L8.66667 20.6668M12.6667 20.6667L12.6667 16.6668" stroke="#4B515B" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>'
326		+ '</svg>';
327
328	// 이 창의 글쓰기 폼. `POST.init` 이전(다른 화면)에는 없으므로 그때는 빈 집합이다.
329	var captchaChallengeForm = function(){
330		return (post_form && post_form.length > 0) ? post_form : $('#post_form');
331	};
332
333	// 그 폼의 요구 블록(없으면 빈 jQuery). 폼마다 하나만 만든다.
334	var captchaChallengeBlock = function(form){
335		return form.find('[' + CAPTCHA_CHALLENGE_ATTR + ']');
336	};
337
338	var createCaptchaChallengeBlock = function(){
339		var block = $('<div class="captcha_block" ' + CAPTCHA_CHALLENGE_ATTR + '="1" style="display: none;"></div>');
340		var row = $('<div style="display: flex; align-items: center;"></div>');
341		var holder = $('<div style="display: inline-flex; align-items: center; gap: 8px; margin-right: 16px;"></div>');
342		var refresh = $('<div class="_captcha_challenge_refresh" style="cursor: pointer; width: 28px; height: 28px;"></div>');
343
344		refresh.html(CAPTCHA_CHALLENGE_REFRESH_ICON);
345		holder.append($('<img width="113" height="30" alt="" />')).append(refresh);
346		row.append(holder)
347			.append('<input type="hidden" name="captcha_key" />')
348			.append('<input type="text" name="captcha_answer" class="capcha_input" />');
349		block.append(row).append('<span class="captcha_description"></span>');
350
351		// 문구는 기존 LOCALIZE 키를 그대로 쓴다(새 키를 만들지 않는다). 값은 **속성·텍스트로**
352		// 넣는다 — 문자열 조합으로 HTML 에 끼우면 현지화 문구가 마크업을 흔들 수 있다.
353		block.find('input[name="captcha_answer"]').attr('placeholder', getLocalizeString('설명_보안문자입력', '', '보안 문자를 입력해 주세요.'));
354		block.find('.captcha_description').text(getLocalizeString('설명_공백없이입력대소문자구분', '', '공백 없이 입력해 주세요. 대소문자를 구분합니다.'));
355
356		return block;
357	};
358
359	// 캡차 블록을 폼에 붙인다.
360	//
361	// 자리 — `.editor_box`(작성자 요약 아래·제목·에디터를 묶은 상자)의 **첫 자식**. 그 상자가
362	// 없는 스킨이면 폼 맨 앞으로 폴백한다(둘 다 폼 **안**이다: 답이 제출에 실리는 조건은 같다).
363	//
364	// **바깥이 아니라 안**인 이유(실측): 이 스킨은 fixed 헤더 높이만큼의 상단 여백을
365	// `.editor_box` 에 준다. ê·¸ 앞에 형제로 넣으면 ê·¸ 여백을 받지 못해 블록이 문ì
365„œ 최상단에
366	// 깔리고 헤더가 그 위를 덮는다 — 모바일에서는 폼이 뷰포트보다 짧아 스크롤도 없어
367	// 사용자가 캡차를 **볼 수 없었다**(390폭 실측). 안에 넣으면 같은 여백을 물려받는다.
368	var attachCaptchaChallengeBlock = function(form, block){
369		var editor_box = form.find('.editor_box').first();
370
371		if (editor_box.length > 0) {
372			editor_box.prepend(block);
373		} else {
374			form.prepend(block);
375		}
376	};
377
378	// 블록이 화면 밖이면 그쪽으로 스크롤한다 — 캡차를 요구하면서 보이지 않으면 풀 수 없다.
379	// 이미 보이면 아무것도 하지 않는다(새로고침마다 화면이 튀지 않게).
380	// 스크롤 API 가 없는 환경(구형 웹뷰·하네스 대역)에서는 조용히 지나간다 — 관측이 아니라
381	// 편의 기능이라, 없다고 등록이 막히면 안 된다.
382	var captchaChallengeScrollIntoViewIfNeeded = function(block){
383		var node = block.get(0);
384
385		if (!node || typeof node.getBoundingClientRect !== 'function') return;
386
387		var rect = node.getBoundingClientRect();
388
389		if (!rect || typeof rect.top !== 'number' || typeof rect.bottom !== 'number') return;
390
391		var viewport_height = window.innerHeight || 0;
392
393		if (rect.top < 0 || rect.bottom > viewport_height) {
394			if (typeof node.scrollIntoView === 'function') {
395				node.scrollIntoView({block: 'center'});
396			}
397		}
398	};
399
400	var applyCaptchaChallenge = function(result){
401		var form = captchaChallengeForm();
402
403		// 그릴 수 없는 응답이면 문구만 남는다(등록을 막지 않는다 — 서버가 저장 여부를 정한다).
404		// 이미지 키 이름은 요구 응답(iframe 스크립트)과 새로고침 응답이 **같다**(`captcha_img`).
405		if (form.length === 0 || !result || !result.captcha_img || !result.key) return;
406
407		var block = captchaChallengeBlock(form);
408
409		if (block.length === 0) {
410			block = createCaptchaChallengeBlock();
411			block.find('._captcha_challenge_refresh').on('click', function(){
412				refreshCaptchaChallenge();
413			});
414			// 헤더 아래 첫 영역 앞에 넣는다 — 그 상자가 없는 스킨이면 폼 맨 앞으로 폴백한다
415			// (둘 다 폼 **안**이다: 캡차 답이 폼 제출에 실리는 조건은 그대로다).
416			attachCaptchaChallengeBlock(form, block);
417		}
418
419		block.find('img').attr('src', result.captcha_img);
420		block.find('input[name="captcha_key"]').val(result.key);
421		block.find('input[name="captcha_answer"]').val('');
422		block.css('display', 'flex');
423
424		captchaChallengeScrollIntoViewIfNeeded(block);
425	};
426
427	// 새로고침 — **발급 전용 엔드포인트**로 새 이미지·키만 받는다.
428	//
429	// 글 등록 경로(post_add.cm)를 다시 태우면 안 된다: 그 요청은 판정·게이트를 다시 태우고,
430	// **본문·제목이 그대로 저장되어 실제로 글이 올라간다** — 사용자는 캡차만 새로 받으려 했다.
431	// 그래서 폼을 직렬화해 보내지 않는다(그 자체가 저장 요청의 재료다). 서버는 이 키의 결속
432	// 표식을 선점(DEL)한 요청에만 새 캡차를 발급한다.
433	var refreshCaptchaChallenge = function(){
434		var form = captchaChallengeForm();
435		var block = captchaChallengeBlock(form);
436		if (form.length === 0 || block.length === 0) return;
437
438		$.ajax({
439			type:'post',
440			data:{
441				'captcha_key': block.find('input[name="captcha_key"]').val(),
442				'target': 'post',
443				'band_captcha': '1'
444			},
445			url:'/ajax/refresh_captcha.cm',
446			dataType:'json',
447			success:function(result){
448				if (result.msg == 'SUCCESS') {
449					// 새 이미지·새 키를 그대로 반영한다(입력칸은 비운다).
450					applyCaptchaChallenge(result);
451				} else {
452					alert(result.msg);
453				}
454			}
455		});
456	};
457
458	// 부모 창에 캡차를 띄우는 함수 — **iframe 안의 서버 스크립트가 부른다**(post_add.cm).
459	// 문구는 기존 경로(submit_error)와 **같은 alert** 로 띄운다: 캡차를 그리지 못하는 응답이면
460	// 지금까지와 같이 문구만 보이고, 사용자는 다시 눌러 새 요구를 받는다.
461	var showBandCaptcha = function(data){
462		applyCaptchaChallenge(data);
463
464		// 문구는 서버가 늘 실어 보낸다(기존 키 '설명_자동입력문자불일치'). 없는 응답이면
465		// **빈 alert 를 띄우지 않고** 제출 잠금만 푼다 — 사용자는 다시 눌러 새 요구를 받는다.
466		if (data && data.msg) {
467			endSubmit(data.msg);
468		} else {
469			sent = false;
470		}
471	};
472
473	var postCancel = function(back_url){
474		if(isIOS && isSafari){
475			var s_top = $(this).scrollTop();
476			$write_header.css({'-webkit-transition': 'none', 'transition': 'none', 'position': 'fixed', 'top': 0});
477			$fr_m_custom.toggleClass('m_sticky_container', s_top > m_sticky_container_trigger_top);
478			$fr_m_custom.toggleClass('m_sticky_container_ios', s_top > m_sticky_container_trigger_top);
479			if(s_top > m_sticky_container_trigger_top){
480				$toolbarContainer.css({'-webkit-transition': 'none', 'transition': 'none', 'position': 'fixed', 'top': $write_header.height() + 'px'});
481			}else{
482				$toolbarContainer.css({'-webkit-transition': 'none', 'transition': 'none', 'top': 'auto'});
483			}
484		}
485		document.location.href = back_url;
486	};
487
488	var is_more = true;
489	var listing_obj = {};
490
491	var postInitMoreList = function(wcode,bcode,type){
492		listing_type = type;
493		$widget_wrap = $("#"+wcode);
494		board_code = bcode;
495		more_list_page = 1;
496		is_more = true;
497		return true;
498	};
499
500	var toggleAlarmPopup = function(){
501		var $alarm_popup = $('#alarm_popup');
502		var $dLabel = $('#dLabel');
503		$alarm_popup.toggleClass('open');
504		if($alarm_popup.hasClass('open')){
505			$(window).on('click.alarm_popup',function(event){
506				var $top_closest = $(event.target).closest('a');
507				if($top_closest.attr('id')!='dLabel'){
508					var $closest = $(event.target).closest('ul');
509					if($closest != null && !$closest.hasClass('dropdown-menu')){
510						$alarm_popup.removeClass('open');
511						$(window).off('click.alarm_popup');
512						var alarm_group_list = $alarm_popup.find("input[type='checkbox']").is(":checked");
513						if(alarm_group_list) $dLabel.addClass('active');
514						else $dLabel.removeClass('active');
515					}
516				}
517			});
518		}
519	};
520	var postDeleteFile = function(id){
521		var obj = $('#'+id);
522		var size = obj.data('size');
523		total_file_size -= size;
524		obj.remove();
525	};
526	var postAddFile = function(filename,file_code,tmp_idx,size){
527		var uniq_id = makeUniq('upfile_');
528		total_file_size += size;
529		var clear_ico = $('<i class="zmdi zmdi-close"></i>').data({'item':uniq_id,size:size}).click(function(e){
530			postDeleteFile(uniq_id);
531		});
532		var hidden_input = '';
533		if(file_code.length>0) {
534			hidden_input = $('<input name="upload_files[]" value="' + file_code + '" type="hidden" />');
535		}else if(Math.round(tmp_idx) > 0){
536			hidden_input = $('<input name="temp_files[]" value="' + tmp_idx + '" type="hidden" />');
537		}
538		var li 	= $('<li>').attr('id',uniq_id).data({'item':uniq_id,size:size});
539		// XSS 방지: 파일명은 HTML로 해석되지 않도록 text로 삽입 (KVE-2025-2806)
540		var $file_name_dom = $('<span></span>');
541		$file_name_dom.text(filename);
542		$file_name_dom.append($('<em></em>').text(' ' + GetFileSize(size)));
543		li.append($file_name_dom);
544		li.append(clear_ico);
545		li.append(hidden_input);
546		$("#file_list").append(li);
547	};
548
549	var MovePostPopup = function(post_code,menu_url){
550		$.ajax({
551			type		: 'post',
552			data:{'post_code':post_code,'menu_url':menu_url},
553			url			: '/ajax/move_post_popup.cm',
554			dataType 	: 'json',
555			success		: function(result){
556				if(result.msg == 'SUCCESS'){
557					var html = $(result.html);
558					$.cocoaDialog.open({type : 'site_alert', custom_popup : html});
559				}else{
560					alert(result.msg);
561				}
562			}
563		});
564	};
565
566	/**
567	 * 설정된 카테고리 생성
568	 * @returns {Array}
569	 */
570	var categoryList = function(str_category){
571		var b_data = board_data;
572		var category_type_list_temp = [];
573		var category_list_default = {};
574		category_list_default.key = 0;
575		category_list_default.value = str_category;
576		category_type_list_temp.push(category_list_default);
577
578		if(Array.isArray(b_data.category_list)){
579			$.each(b_data.category_list, function(key, val){
580				var category_list = {};
581				var $name = val.name;
582				var $color = val.color;
583				category_list.key = key + 1;
584				category_list.value = '<span style="color:' + $color + '">' + RemoveTag($name) + '</span>';
585				category_type_list_temp.push(category_list);
586			});
587		}
588		return category_type_list_temp;
589	};
590
591	/**
592	 * 분류 리스트 출력
593	 * @param $obj
594	 * @param default_code
595	 */
596	var categoryTypeSelect = function($obj,default_code){
597		$obj.find('._category_type_list').setSelectBox({
598			option: category_list,
599			'set' : {
600				select_custom_cls:'category_select',
601				custom_cls:'category_dropdown',
602				width:180
603			},
604			'default' : default_code,
605			change: function (o) {
606				$('#category_type').val(o.key);
607			}
608		});
609	};
610
611	var viewReviewPostDetail = function(idx, board_code){
612		$(function(){
613		$.ajax({
614			type : 'POST',
615			data : {idx : idx, board_code : board_code},
616			url : ('/ajax/review_post_detail_view.cm'),
617			dataType : 'json',
618			async : false,
619			cache : false,
620			success : function(res){
621				if(res.msg === 'SUCCESS'){
622					$.cocoaDialog.open({
623						type : 'prod_detail review', custom_popup : res.html, width : 800});
624					if(history.replaceState && history.pushState){
625						// 모달 히스토리 커스텀(IE 10 이상)
626						var current_url = location.href.indexOf('#') === -1 ? location.href : location.href.substr(0, location.href.indexOf('#'));
627						var back_url = document.referrer.indexOf('#') === -1 ? document.referrer : document.referrer.substr(0, document.referrer.indexOf('#'));
628						history.pushState(null, null, current_url);
629						history.replaceState(null, null, current_url + "#prod_detail_review!/" + res.idx);
630					}else{
631						location.hash = "prod_detail_review!/" + res.idx;
632					}
633					$(window).off('hashchange').on('hashchange',function(){
634						var hash_qna_spilt = location.hash.split('!/')[1];
635						if(!hash_qna_spilt){
636							$.cocoaDialog.close();
637						}else{
638							var hash_spilt_tab = location.hash.split('!/')[0];
639							if(hash_spilt_tab === '#prod_detail_review'){
640								viewReviewPostDetail(hash_qna_spilt,board_code);
641							}else if(hash_spilt_tab === '#prod_detail_qna'){
642								viewQnaPostDetail(hash_qna_spilt,board_code);
643							}
644						}
645					});
646					$('.modal_prod_detail').off('hidden.bs.modal').on('hidden.bs.modal', function (e) {
647						removeReviewHash();
648					});
649				}else{
650					alert(res.msg);
651				}
652			}
653		});
654		});
655	};
656
657	var removeReviewHash = function(){
658		$(window).off('hashchange');
659    $('html').toggleClass('modal-scroll-control', false);
660		var hash_review_spilt = location.hash.split('!/')[1];
661		if(hash_review_spilt){
662			if(history.replaceState && history.pushState){
663				history.back();
664			}else{
665				location.href = '#prod_detail_review';
666			}
667		}
668	};
669
670	var viewQnaPostDetail = function(idx, board_code){
671		$(function(){
672			$.ajax({
673				type : 'POST',
674				data : {idx : idx, board_code : board_code},
675				url : ('/ajax/qna_post_detail_view.cm'),
676				dataType : 'json',
677				async : false,
678				cache : false,
679				success : function(res){
680					if(res.msg === 'SUCCESS'){
681						$.cocoaDialog.open({
682							type : 'prod_detail review', custom_popup : res.html, width : 800});
683						if(history.replaceState && history.pushState){
684							// 모달 히스토리 커스텀(IE 10 이상)
685							var current_url = location.href.indexOf('#') === -1 ? location.href : location.href.substr(0, location.href.indexOf('#'));
686							var back_url = document.referrer.indexOf('#') === -1 ? document.referrer : document.referrer.substr(0, document.referrer.indexOf('#'));
687							history.pushState(null, null, current_url);
688							history.replaceState(null, null, current_url + "#prod_detail_qna!/" + res.idx);
689						}else{
690							location.hash = "prod_detail_qna!!/" + res.idx;
691						}
692						$(window).off('hashchange').on('hashchange',function(){
693							var hash_qna_spilt = location.hash.split('!/')[1];
694							if(!hash_qna_spilt){
695								$.cocoaDialog.close();
696							}else{
697								var hash_spilt_tab = location.hash.split('!/')[0];
698								if(hash_spilt_tab === '#prod_detail_review'){
699									viewReviewPostDetail(hash_qna_spilt, board_code);
700								}else if(hash_spilt_tab === '#prod_detail_qna'){
701									viewQnaPostDetail(hash_qna_spilt, board_code);
702								}
703							}
704						});
705						$('.modal_prod_detail').off('hidden.bs.modal').on('hidden.bs.modal', function (e) {
706							removeQnaHash();
707						});
708					}else{
709						alert(res.msg);
710					}
711				}
712			});
713		});
714	};
715
716	var removeQnaHash = function(){
717		$(window).off('hashchange');
718    $('html').toggleClass('modal-scroll-control', false);
719		var hash_qna_spilt = location.hash.split('!/')[1];
720		if(hash_qna_spilt){
721			if(history.replaceState && history.pushState){
722				history.back();
723			}else{
724				location.href = '#prod_detail_qna';
725			}
726		}
727	};
728
729  var alertNoSale = function(){
730    $.cocoaDialog.open({
731      type: 'site_alert',
732      custom_popup: `
733        <div class="layer_pop">
734            <div class="container-fluid">
735                <p class="tw-text-center tw-mb-0">
736                    ${getLocalizeString('설명_상품페이지이동불가안내', '', '해당 상품 페이지로 이동할 수 없습니다.')}
737                </p>
738            </div>
739            <div class="btn-group-justified">
740                <a href="javascript:" class="btn" onclick="$('.modal_site_alert').modal('hide');">${getLocalizeString('버튼_확인', '', '확인')}</a>
741            </div>
742        </div>`
743    }, function(){
744      $('.modal_site_alert').css('z-index', 100002);
745    });
746  }
747
748	return {
749		'init' : function(code, data, str_category) {
750			postInitWrite(code, data, str_category);
751		},
752		'initMoreList' : function(widget_code,board_code,listing_type) {
753			return postInitMoreList(widget_code,board_code,listing_type);
754		},
755		'submit' : function(){
756			postSubmit();
757		},
758		'submit_error': function(msg){
759			endSubmit(msg);
760		},
761		// 서버가 요구한 문자 캡차(애매한 스팸 점수)를 등록 버튼 아래에 띄운다 — iframe 안의
762		// 응답 스크립트(post_add.cm)가 부른다. 함수 이름·인자 모양이 서버와의 계약이다.
763		'showBandCaptcha': function(data){
764			showBandCaptcha(data);
765		},
766		'postCancel': function(back_url){
767			postCancel(back_url);
768		},
769		'addFile' : function(filename,file_code,tmp_idx,size){
770			postAddFile(filename,file_code,tmp_idx,size);
771		},
772		'androidAppPostImageUploadComplete' :function(image){
773			androidAppPostImageUploadComplete(image);
774		},
775		'androidAppPostImageUploadAllComplete' :function(){
776			androidAppPostImageUploadAllComplete();
777		},
778		'deletePost' : function (board_code,code,return_url,secret_pass) {
779			postDeletePost(board_code,code,return_url,secret_pass);
780		},
781		'moreList' : function(keyword,keyword_type,q){
782			moreList(keyword,keyword_type,q);
783		},
784		'toggleAlarmPopup' : function(){
785			toggleAlarmPopup();
786		},
787		'MovePostPopup' : function(post_code,menu_url){
788			MovePostPopup(post_code,menu_url);
789		},
790		'deleteCoverImage' : function(){
791			deleteCoverImage();
792		},
793		'deleteLibraryImage' : function(url){
794			deleteLibraryImage(url);
795		},
796		'categoryTypeSelect' : function($obj,defult_code){
797			categoryTypeSelect($obj,defult_code);
798		},
799		'categoryList' : function(str_category){
800			categoryList(str_category);
801		},
802		'viewReviewPostDetail' : function(idx, board_code){
803			viewReviewPostDetail(idx, board_code);
804		},
805		'removeReviewHash' : function(){
806			removeReviewHash();
807		},
808		'viewQnaPostDetail' : function(idx, board_code){
809			viewQnaPostDetail(idx, board_code);
810		},
811		'removeQnaHash' : function(){
812			removeQnaHash();
813		},
814    'alertNoSale' : function(){
815      alertNoSale();
816    }
817	};
818}();
819
820function POST_INIT_LIST(code,data){
821	var that 	= this;
822	that.type_data = data;
823	that.code = code;
824	that.windowWidth = $(window).width();
825	that.change_timer = setTimeout(function(){},1);
826	that.listing_obj = $('#post_card_'+that.code);
827
828	that.listing_obj.imagesLoaded().always(function(ins) {
829		that.listResize();
830	});
831
832	$('body').off('gridChange.'+that.code).on('gridChange.'+that.code,function(){
833		that.listing_obj.imagesLoaded().always(function(ins) {
834			that.listResize();
835		});
836	});
837
838	$(window).off('resize.'+that.code).on('resize.'+that.code,function(){
839		if ($(window).width() != that.windowWidth) {
840			that.windowWidth = $(window).width();
841		}else{
842			return;
843		}
844		clearTimeout(that.change_timer);
845		that.change_timer = setTimeout(function(){
846			that.listResize();
847		},1000);
848	});
849
850	this.listResize = function(){
851		that.listing_obj.imagesLoaded()
852			.always(function(){
853				var window_width = $(window).width();
854				if(!that.type_data.grid_gutter){
855					that.type_data.grid_gutter = 15;
856				}
857				if($('body').hasClass('device_type_m'))
858					window_width = 370;
859				if(window_width <= 991)
860					that.type_data.grid_gutter = that.type_data.grid_gutter/2;
861				that.listing_obj.css({'margin':'0 -'+that.type_data.grid_gutter+'px'});
862				if(that.type_data['design_type'] == 'grid' || that.type_data['design_type'] == 'masonry'){
863					that.listing_obj.css({'margin-top':'-'+that.type_data.grid_gutter+'px'});
864				}
865
866				var cnt = parseInt(that.type_data.grid_col_count);
867				var inner_width = that.listing_obj.width();
868
869				if(window_width <= 991){
870					if(typeof that.type_data.grid_mobile_col_count == "undefined")
871						cnt = 2;
872					else
873						cnt = parseInt(that.type_data.grid_mobile_col_count);
874				}else{
875					if(that.type_data.design_type == 'slide'){
876						inner_width = that.listing_obj.parent().width();
877					}else{
878						inner_width = that.listing_obj.width();
879					}
880
881					if(that.type_data.grid_extend_fix != 'Y'){
882						var s_width = (that.type_data.max_width - (that.type_data.document_margin * 2)) + that.type_data.grid_gutter * 2;
883						var item_max = s_width - ((cnt - 1) * parseInt(that.type_data.grid_gutter));
884						var grid_width = Math.floor(item_max / cnt);
885						var current_width = Math.floor((inner_width - ((cnt - 1) * parseInt(that.type_data.grid_gutter))) / cnt);
886						if(current_width > grid_width){
887							cnt = Math.floor(inner_width / grid_width);
888						}
889					}
890				}
891
892				var width = Math.floor(inner_width / cnt);
893				if(that.type_data['design_type'] == 'grid'){
894					that.listing_obj.find('._post_item_wrap').css({'padding' : that.type_data.grid_gutter + 'px'});
895					that.runColSize(cnt);
896				}else if(that.type_data['design_type'] == 'masonry'){
897					var $masonry_item = that.listing_obj.find('.ma-item');
898					var masonry_item_count = $masonry_item.length;
899					that.listing_obj.find('.ma-item').css({'width' : width,'padding' : that.type_data.grid_gutter + 'px'});
900					that.runMasonry(masonry_item_count);
901				}
902			});
903	};
904
905	this.runColSize = function(col_cnt){
906		that.listing_obj.find('._card_wrap').show();
907		var i = 1;
908		var $_row = $('<div />').addClass('_post_row post_row');
909		var is_append = false;
910		that.listing_obj.find('._dummy_item').remove();
911		var $item = that.listing_obj.find('._card_wrap');
912		var item_count = $item.length;
913
914		$item.each(function(e,$_obj){
915			$_row.append($_obj);
916			is_append = false;
917			if(i % col_cnt == 0){
918				that.listing_obj.append($_row);
919				is_append = true;
920				$_row = $('<div />').addClass('_post_row post_row');
921			}
922			i++;
923		});
924
925		if(!is_append){
926			var $tmp_item = $_row.find('._post_item_wrap');
927			if($tmp_item.length >0 ){
928				if($tmp_item.length < col_cnt){
929					var remain_cnt = col_cnt - $tmp_item.length;
930					for(var i = 0; i <remain_cnt; i++){
931						var $dummy_col = $('<div/>').addClass('dummy_col item_post _item _dummy_item list-style-card');
932						$_row.append($dummy_col);
933					}
934				}
935				that.listing_obj.append($_row);
936			}
937		}
938
939		that.listing_obj.find('._post_row').each(function(){
940			var $tmp_item = $(this).find('._post_item_wrap');
941			if($tmp_item.length ==0 ){
942				$(this).remove();
943			}
944		});
945
946		that.imgHeight();
947	};
948
949	this.imgHeight = function(){
950		if(that.type_data['design_type'] == 'grid'){
951			var height = Math.ceil(that.listing_obj.find('._post_item_wrap').eq(0).width() / (that.type_data.img_ratio / 100));
952			that.listing_obj.find('._img_wrap').height(height);
953		}
954	};
955
956	this.runMasonry = function(count){
957		if(count > 0){
958      let is_masonry_timeout;
959			that.listing_obj.off('layoutComplete').on( 'layoutComplete', function() {
960        if(that.listing_obj.css('visibility') !== 'visible'){
961          that.listing_obj.css('visibility', 'visible');
962          that.listing_obj.find('img').lazyload({
963            'effect' : 'fadeIn',
964            'effect_speed' : 50, // fadeIn 시간 단축(기본 400ms→50ms)
965            'load' : function() {
966              if(typeof is_masonry_timeout === 'number'){
967                clearTimeout(is_masonry_timeout);
968              }
969              is_masonry_timeout = setTimeout(function(){
970                /* 레이지 로드 완료 후 100ms 후에 masonry layout 재실행 */ 
971                that.listing_obj.masonry('layout');
972              }, 100);
973            },
974            'threshold': (window.innerHeight * 2)
975          });
976        }
977			});
978      that.listing_obj.masonry({
979        itemSelector: '.ma-item'
980      });
981    }else{
982      that.listing_obj.css('visibility','visible');
983    }
984	};
985}

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.