PageSourceSearch

https://www.exporum.com/theme/exporum/js/ajax-board.js?ver=2026.10.06-14:41:45

js exporum.com collected 2026-10-06 05:42:12 UTC 6,670 bytes, 143 lines download raw bytes

1/* ============================================================
2 * EX_AJAXBOARD — 공통 인플레이스 게시판 로더
3 * 대상: pr/news, pr/notice, pr/press (목록 인플레이스 + 상세 끼워넣기)
4 *       portfolio (목록 인플레이스 + view=wr_id 모달; 모달은 페이지 자체 JS가 처리)
5 * 참고 패턴: contact/faq.php 의 faqFetch (응답 전체 HTML → find(셀렉터) 추출 → 교체)
6 *
7 * 핵심 원칙
8 *  - 검색어/page/view(뷰토글) 전부 querystring 으로 URL 에 반영 → 그 URL 직접 접속 시 서버가 동일 상태 렌더
9 *  - 상세는 pretty url(/pf_news/79) 그대로 pushState. 해당 URL 직접 접속 시 G5 가 풀 상세페이지 렌더(progressive enhancement)
10 *  - 목록↔상세 전환만 인플레이스. 응답 HTML 에서 필요한 조각만 추출해 컨테이너 교체
11 *  - popstate(뒤/앞)에서 현재 URL 을 다시 fetch 해 동일 상태 복원
12 *
13 * exAjaxBoard({
14 *   container   : '#board-ajax',                 // 교체 대상 컨테이너(필수)
15 *   listSel     : '#board-ajax',                 // 목록 응답에서 추출할 셀렉터(기본=container)
16 *   navLinks    : '#board-ajax .paginate a, #board-ajax .view-toggle a', // 클릭→목록 fetch (위임)
17 *   searchForm  : '#board-ajax .search-form',    // submit→목록 fetch
18 *   detailSel   : '#section-pr-view',            // 상세 응답에서 추출할 셀렉터(선택)
19 *   detailLinks : '#board-ajax .subject a, #board-ajax .board-list a, #board-ajax .board-cards a', // 상세 fetch (위임)
20 *   detailTest  : /\/pf_[a-z0-9_]+\/\d+/,        // 상세 URL 판별 정규식(선택)
21 *   listPath    : location.pathname,             // 목록 기준 경로
22 *   onReplace   : function(mode, html){}         // 교체 후 콜백(선택)
23 * });
24 * ========================================================== */
25(function ($) {
26	'use strict';
27	if (!$) return;
28
29	/* EX_BOARD_SCROLLTOP: 페이징/검색/탭 전환 후 게시물 영역 상단으로 부드럽게 스크롤.
30	 * 고정 헤더(#header position:fixed, 데스크탑 ~100px / 768이하 ~90px)에 가려지지 않게
31	 * 헤더 실측 높이 + 여유 16px 만큼 오프셋. 전역 노출 → FAQ(faqFetch) 등에서도 재사용(중복 제거).
32	 * target: 스크롤 기준 엘리먼트(jQuery객체 또는 셀렉터). 이미 화면 상단 위쪽이면 스크롤 생략. */
33	/* EX_AJAX_TITLE: 인플레이스 교체 시 <title> 동기화용 — 응답 풀HTML(서버렌더)에서
34	 * <title> 텍스트를 추출(서버가 글제목으로 동적 세팅). 봇은 AJAX 미실행이라 SEO 영향 없고,
35	 * 사람 탐색 시 브라우저 탭/히스토리 제목을 글에 맞게 갱신하는 UX 목적. */
36	window.exExtractTitle = function (html) {
37		if (!html || typeof html !== 'string') return '';
38		var m = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
39		if (!m) return '';
40		var t = document.createElement('textarea');
41		t.innerHTML = m[1];
42		return t.value.replace(/\s+/g, ' ').trim();
43	};
44
45	window.exScrollToBoardTop = function (target) {
46		var $t = (target && target.jquery) ? target : $(target);
47		var el = $t && $t.length ? $t.get(0) : null;
48		if (!el) return;
49		var $hd = $('#header');
50		var headerH = ($hd.length && $hd.css('position') === 'fixed') ? $hd.outerHeight() : 0;
51		var offset = headerH + 16;
52		var top = el.getBoundingClientRect().top + window.pageYOffset - offset;
53		if (top < 0) top = 0;
54		// 이미 타깃 상단이 헤더 아래로 충분히 보이면(위로 스크롤 불필요) 스킵
55		if (Math.abs(window.pageYOffset - top) < 4) return;
56		if ('scrollBehavior' in document.documentElement.style) {
57			window.scrollTo({ top: top, behavior: 'smooth' });
58		} else {
59			$('html, body').stop().animate({ scrollTop: top }, 300);
60		}
61	};
62
63	window.exAjaxBoard = function (opt) {
64		opt = opt || {};
65		var container = opt.container;
66		if (!container || !$(container).length) return;
67		var listSel = opt.listSel || container;
68		var detailSel = opt.detailSel || null;
69		var detailTest = opt.detailTest || null;
70		var listPath = opt.listPath || location.pathname;
71		var onReplace = opt.onReplace || function () {};
72
73		function isDetailUrl(url) { return detailSel && detailTest && detailTest.test(url); }
74
75		function fetchList(url, push) {
76			$.get(url, function (html) {
77				var $piece = $('<div>').html(html).find(listSel).first();
78				if ($piece.length) $(container).replaceWith($piece);
79				if (push) history.pushState({ ex: 'list' }, '', url);
80				/* EX_AJAX_TITLE: 목록 복귀 시 목록 페이지 title 로 복원 */
81				var exListTitle = window.exExtractTitle(html);
82				if (exListTitle) document.title = exListTitle;
83				/* EX_BOARD_SCROLLTOP: 페이징/검색/탭 후 교체된 목록 컨테이너 상단으로 스크롤 */
84				if ($piece.length) window.exScrollToBoardTop($piece);
85				onReplace('list', html);
86			});
87		}
88
89		function fetchDetail(url, push) {
90			$.get(url, function (html) {
91				var $piece = $('<div>').html(html).find(detailSel).first();
92				if ($piece.length) $(container).empty().append($piece);
93				if (push) history.pushState({ ex: 'detail' }, '', url);
94				/* EX_AJAX_TITLE: 상세 글제목으로 document.title 갱신 */
95				var exDetailTitle = window.exExtractTitle(html);
96				if (exDetailTitle) document.title = exDetailTitle;
97				$(window).scrollTop(0);
98				onReplace('detail', html);
99			});
100		}
101
102		function navigate(url, push) {
103			if (isDetailUrl(url)) fetchDetail(url, push);
104			else fetchList(url, push);
105		}
106
107		// 목록 네비게이션(페이징·뷰토글·탭)
108		if (opt.navLinks) {
109			$(document).on('click.exab', opt.navLinks, function (e) {
110				var href = this.getAttribute('href');
111				if (!href || href === '#') { e.preventDefault(); return; }
112				e.preventDefault();
113				var url = (href.charAt(0) === '?') ? (listPath + href) : href;
114				fetchList(url, true);
115			});
116		}
117
118		// 검색폼
119		if (opt.searchForm) {
120			$(document).on('submit.exab', opt.searchForm, function (e) {
121				e.preventDefault();
122				var qs = $(this).serialize();
123				var url = listPath + (qs ? '?' + qs : '');
124				fetchList(url, true);
125			});
126		}
127
128		// 상세 링크(목록 → 상세 끼워넣기)
129		if (detailSel && opt.detailLinks) {
130			$(document).on('click.exab', opt.detailLinks, function (e) {
131				var href = this.getAttribute('href');
132				if (!href || !isDetailUrl(href)) return; // 비상세 링크는 기본 동작 유지
133				e.preventDefault();
134				fetchDetail(href, true);
135			});
136		}
137
138		// 뒤로/앞으로
139		window.addEventListener('popstate', function () { navigate(location.href, false); });
140
141		return { navigate: navigate, fetchList: fetchList, fetchDetail: fetchDetail };
142	};
143})(window.jQuery);

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.