PageSourceSearch

https://seoulchurch.or.kr/js/recent_product.js?1772502784

js seoulchurch.or.kr collected 2026-09-25 09:48:55 UTC 9,672 bytes, 458 lines download raw bytes

1(function(modules) {
2	'use strict';
3
4	const { root, document, $ } = modules;
5
6	const utils = (function() {
7		const http = (url, params = undefined) => {
8			return new Promise((resolve, reject) => {
9				$.ajax({
10					url,
11					data: params,
12					cache: false,
13					async: true,
14					type: 'POST',
15					dataType: 'json',
16					success: function(res) {
17						const isSuccess = res.msg === 'SUCCESS';
18
19						if (!isSuccess) {
20							return reject(res);
21						}
22
23						resolve(res);
24					},
25				});
26			});
27		}
28
29		const bind = (instance, fn) => {
30			return function(...args) {
31				fn.call(instance, ...args);
32			};
33		}
34
35		const isLogin = () => {
36			/**
37			 * 로그인 여부 체크
38			 * @ref FO, global_vars.sub
39			 */
40			return !window.IS_GUEST;
41		}
42
43		const getMemberHash = () => {
44			/**
45			 * 해싱된 멤버 키
46			 * @ref FO, global_vars.sub
47			 */
48			return window.MEMBER_HASH;
49		}
50
51		return { http, bind, isLogin, getMemberHash };
52	})();
53
54	const localize = (function() {
55		const 타이틀_최근본상품 = () => {
56			return getLocalizeString('타이틀_최근본상품', undefined, '최근 본 상품');
57		}
58
59		return { 타이틀_최근본상품 };
60	})();
61
62	const cookie = (function() {
63		const get = (key) => {
64			return `; ${document.cookie}`
65				.split(`; ${key}=`)[1]
66				?.split(';')[0] ?? null;
67		}
68
69		const has = (key) => {
70			return get(key) !== null;
71		}
72
73		const set = (key, value = '', maxAge = undefined) => {
74			document.cookie = `${key}=${value}; path=/;${maxAge ? ` max-age=${maxAge}` : ''}`;
75		}
76
77		const remove = (key) => {
78			document.cookie = `${key}=''' path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
79		}
80
81		return { get, has, set, remove };
82	})();
83
84	const keyStore = (function() {
85		const PREFIX = '_rp';
86		const SEPARATOR = '_';
87
88		const generateCacheExpiresKey = () => {
89			return [PREFIX, 'c', utils.getMemberHash()].join(SEPARATOR);
90		}
91
92		const generateMemberKeyByMemberHash = () => {
93			return [PREFIX, 'm', utils.getMemberHash()].join(SEPARATOR);
94		}
95
96		return {
97			CACHE_KEY: generateCacheExpiresKey(),
98			MEMBER_KEY: generateMemberKeyByMemberHash(),
99		};
100	})();
101
102	const apis = (function() {
103		const CONSTANTS = {
104			HTTP_ERROR_CODE: {
105				NOT_ENABLED_SITE: 'not enabled site',
106				NEED_TO_LOGIN: 'need to login',
107				FAIL_TO_SEARCH: 'Fail to Search',
108			},
109		};
110
111		const getRecentProducts = async () => {
112			try {
113				const res = await utils.http('/ajax/shop/get_recent_view_prod_list.cm');
114
115				return res['recent_view_prod_list'];
116			} catch (e) {
117				const errorCode = e.message;
118
119				/** 조회 실패인 경우, 빈 배열 처리 */
120				if (errorCode === CONSTANTS.HTTP_ERROR_CODE.FAIL_TO_SEARCH) {
121					return [];
122				}
123
124				throw e;
125			}
126		}
127
128		return { getRecentProducts };
129	})();
130
131	const cache = (function() {
132		const MAX_READ_COUNT = 5;
133		const MAX_CACHE_COUNT = 100;
134
135		const storage = window.localStorage;
136
137		const encode = (source) => {
138			return btoa(encodeURIComponent(source));
139		}
140
141		const decode = (source) => {
142			return decodeURIComponent(atob(source));
143		}
144
145		const isExpires = () => {
146			return !cookie.has(keyStore.CACHE_KEY);
147		}
148
149		const isEmpty = () => {
150			return get().length === 0;
151		}
152
153		const get = () => {
154			try {
155				const currentRecentProducts = storage.getItem(keyStore.MEMBER_KEY);
156
157				if (currentRecentProducts === null) {
158					return [];
159				}
160
161				return JSON.parse(decode(currentRecentProducts)) ?? [];
162			} catch (e) {
163				return [];
164			}
165		}
166
167		const set = (recentProducts) => {
168			storage.setItem(keyStore.MEMBER_KEY, encode(JSON.stringify(recentProducts)));
169		}
170
171		const clear = () => {
172			storage.removeItem(keyStore.MEMBER_KEY);
173
174			cookie.remove(keyStore.CACHE_KEY);
175		}
176
177		const has = (recentProdeucts, targetRecentProduct) => {
178			return recentProdeucts.some(recentProduct => recentProduct.code === targetRecentProduct.code);
179		}
180
181		const read = () => {
182			if (isExpires()) {
183				return [];
184			}
185
186			return get().slice(0, MAX_READ_COUNT);
187		}
188
189		const write = (recentProducts) => {
190			set(recentProducts.slice(0, MAX_CACHE_COUNT));
191
192			/** 데이터가 수정된 경우 cache 시간 증가(1시간) */
193			cookie.set(keyStore.CACHE_KEY, '1', 60 * 60);
194		}
195
196		const add = (recentProducts) => {
197			const currentRecentProducts = get();
198
199			const targetRecentProducts = recentProducts.reduce((acc, cur) => {
200				if (has(currentRecentProducts, cur)) {
201					return acc;
202				}
203
204				acc.push(cur);
205
206				return acc;
207			}, []);
208
209			write([
210				...targetRecentProducts,
211				...currentRecentProducts,
212			]);
213		}
214
215		return {
216			isExpires,
217			isEmpty,
218			clear,
219			read,
220			write,
221			add,
222		};
223	})();
224
225		const renderer = (function () {
226			const CONSTANTS = {
227				ROOT: 'recent-product-root',
228			};
229
230			const escapeHtml = (source) => {
231				const text = String(source ?? '');
232				return text
233					.replace(/&/g, '&')
234					.replace(/</g, '&lt;')
235					.replace(/>/g, '&gt;')
236					.replace(/"/g, '&quot;')
237					.replace(/'/g, '&#39;');
238			}
239
240			const safeUrl = (source) => {
241				const url = String(source ?? '').trim();
242				if (url === '' || /^\s*javascript:/i.test(url)) {
243					return '#';
244				}
245				return url;
246			}
247
248		const refine = (strings) => {
249			return strings.map(string => string.replace(/[\r\n\t]+/g, '').trim());
250		}
251
252		const combine = (chunks, args) => {
253			return chunks.reduce((acc, cur, index) => `${acc}${cur}${args[index] ?? ''}`, '');
254		}
255
256		const html = (tags, ...args) => {
257			return combine(refine(tags), refine(args));
258		}
259
260		const createContentHtml = (children) => {
261			return html`
262				<div class="recent-product">
263					<div class="recent-product__inner">
264						<p class="recent-product__title">${localize.타이틀_최근본상품()}</p>
265						${children}
266					</div>
267				</div>
268			`;
269		}
270
271		const createProductItemContainerHtml = (children) => {
272			return html`
273				<div class="recent-product__item-container">
274					${children}
275				</div>
276			`;
277		}
278
279			const createProductItemHtml = (product) => {
280				const safeLinkUrl = escapeHtml(safeUrl(product.link_url));
281				const safeCode = escapeHtml(product.code);
282				const safeThumbnail = escapeHtml(safeUrl(product.thumbnail));
283				const safeName = escapeHtml(product.name);
284
285				return html`
286					<a class="recent-product__item-root" href="${safeLinkUrl}" data-code="${safeCode}">
287						<div class="recent-product__item-img-wrapper">
288							<img src="${safeThumbnail}" alt="${safeName}" class="recent-product__item-img" />
289						</div>
290						<p class="recent-product__item-name">${safeName}</p>
291					</a>
292				`;
293			}
294
295		const createRootElement = () => {
296			const $root = document.createElement('div');
297
298			$root.setAttribute('id', CONSTANTS.ROOT);
299
300			$root.style.setProperty('display', 'contents');
301
302			return $root;
303		}
304
305		const render = (products) => {
306			const $root = createRootElement();
307			const recentProductsHtml = createContentHtml(
308				createProductItemContainerHtml(
309					products.map(product => createProductItemHtml(product)).join(''),
310				),
311			);
312
313			$root.innerHTML = recentProductsHtml;
314
315			document.body.appendChild($root);
316		}
317
318		const destroy = () => {
319			const $root = document.getElementById(CONSTANTS.ROOT);
320
321			if ($root) {
322				$root.remove();
323			}
324		}
325
326		return { render, destroy };
327	})();
328
329	function RecentProduct() {
330		this.recentProductQueue = [];
331		this.mount = false;
332	}
333
334	RecentProduct.prototype.isNonMember = function() {
335		return !utils.isLogin();
336	}
337
338	RecentProduct.prototype.enqueueRecentProduct = function(recentProduct) {
339		this.recentProductQueue.push(recentProduct);
340	}
341
342	RecentProduct.prototype.clearRecentProductQueue = function() {
343		this.recentProductQueue = [];
344	}
345
346	RecentProduct.prototype.hasRecentProductQueue = function() {
347		return this.recentProductQueue.length !== 0;
348	}
349
350	RecentProduct.prototype.isEmpty = function(recentProducts) {
351		return recentProducts.length === 0;
352	}
353
354	RecentProduct.prototype.prepare = async function() {
355		/** 만료 되었을 경우 clear */
356		if (cache.isExpires()) {
357			cache.clear();
358
359			const recentProducts = await apis.getRecentProducts();
360
361			cache.write(recentProducts);
362		}
363
364		/** 상품 상세에서 데이터를 추가한 경우, 캐시에 저장 */
365		if (this.hasRecentProductQueue()) {
366			cache.add(this.recentProductQueue);
367
368			this.clearRecentProductQueue();
369		}
370	}
371
372	RecentProduct.prototype.getRecentProducts = async function() {
373		return cache.read();
374	}
375
376	RecentProduct.prototype.show = async function() {
377		if (this.mount) {
378			return;
379		}
380
381		this.mount = true;
382
383		try {
384			await this.prepare();
385
386			const recentProducts = await this.getRecentProducts();
387
388			if (this.isEmpty(recentProducts)) {
389				return;
390			}
391
392			renderer.destroy();
393			renderer.render(recentProducts);
394		} catch (e) {
395			console.error(e.message);
396		}
397	}
398
399	RecentProduct.prototype.refresh = async function() {
400		if (!this.mount) {
401			return;
402		}
403
404		try {
405			renderer.destroy();
406
407			await this.prepare();
408
409			const recentProducts = await this.getRecentProducts();
410
411			if (this.isEmpty(recentProducts)) {
412				return;
413			}
414
415			renderer.render(recentProducts);
416		} catch (e) {
417			console.error(e.message);
418		}
419	}
420
421	const recentProduct = new RecentProduct();
422
423	const plugin = function() {}
424
425	plugin.show = utils.bind(recentProduct, function() {
426		if (this.isNonMember()) {
427			return;
428		}
429
430		this.show();
431	});
432
433	plugin.refresh = utils.bind(recentProduct, function() {
434		if (this.isNonMember()) {
435			return;
436		}
437
438		this.refresh();
439	});
440
441	plugin.enqueueRecentProduct = utils.bind(recentProduct, function(recentProduct) {
442		if (this.isNonMember()) {
443			return;
444		}
445
446		this.enqueueRecentProduct(recentProduct);
447	});
448
449	root.RECENT_PRODUCT = plugin;
450})(function(root) {
451	const modules = {
452		root,
453		document: root.document,
454		$ : root.$,
455	};
456
457	return modules;
458}(window));

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.