PageSourceSearch

https://kyobodasomcare.com/js/ui.js

js kyobodasomcare.com collected 2026-10-08 18:50:40 UTC 31,526 bytes, 982 lines download raw bytes

1(() => {
2	const initUI = () => {
3		const ANI_DEFAUT = 'cubic-bezier(0.22, 1, 0.36, 1)';
4		const EVENT_BINDED_CLASS = "binded";
5		// Classes
6		class Dialog {
7			// Private members
8			#dialog = null;
9			#willShowCallbacks = [];
10			#didShowCallbacks = [];
11			#willCloseCallbacks = [];
12			#didCloseCallbacks = [];
13			#isModal = true;
14			// Public members
15
16			constructor(queryOrElement) {
17				let dialog = null;
18				if (queryOrElement instanceof Element && queryOrElement.tagName === 'DIALOG') {
19					dialog = queryOrElement;
20				} else {
21					dialog = document.querySelector(queryOrElement);
22				}
23
24				if (!(dialog instanceof Element) || dialog.tagName !== 'DIALOG') {
25					console.error('다음 Selector에 해당하는 Dialog가 존재하지 않습니다: ' + queryOrElement);
26					return;
27				}
28
29				this.#dialog = dialog;
30				this.#init();
31			}
32			
33			// Private Methods
34			#init() {
35				// Closeable Event
36				const closeables = this.#dialog.querySelectorAll("*[closeable]");
37				if (closeables !== null) {
38					closeables.forEach(closeableElement => closeableElement.addEventListener('click', () => this.close()));
39				}
40				// Dim Click
41				this.#dialog.addEventListener('click', (e) => {
42					e.target === this.#dialog && this.close();
43				});
44				// ESC Event
45				this.#dialog.addEventListener('cancel', (e) => {
46					e.preventDefault(); // 기존에 바로 닫히는 기능 끄기
47					if (this.#dialog.classList.contains('loading')) {
48						return;
49					}
50					this.close();
51				});
52			}
53			#isFunc(func) {
54				return func instanceof Function || console.error('해당 인스턴스는 함수가 아닙니다.');
55			}
56
57			// Public Methods (Add CallBack Method)
58			addWillShowCallback(func) {
59				if (!this.#isFunc(func)) { return; }
60				this.#willShowCallbacks.push(func);
61			}
62			addDidShowCallback(func) {
63				if (!this.#isFunc(func)) { return; }
64				this.#didShowCallbacks.push(func);
65			}
66			addWillCloseCallback(func) {
67				if (!this.#isFunc(func)) { return; }
68				this.#willCloseCallbacks.push(func);
69			}
70			addDidCloseCallback(func) {
71				if (!this.#isFunc(func)) { return; }
72				this.#didCloseCallbacks.push(func);
73			}
74			onConfirmCallback(func) {
75				const confirmBtns = this.#dialog.querySelectorAll('button[confirm]');
76				confirmBtns.forEach((confirmBtn) => {
77					confirmBtn.addEventListener('click', func);
78					confirmBtn.addEventListener('click', () => {
79						this.close();
80					});
81				});
82			}
83			onCancelCallback(func) {
84				const cancelBtns = this.#dialog.querySelectorAll('button[cancel]');
85				cancelBtns.forEach((cancelBtn) => {
86					cancelBtn.addEventListener('click', func);
87				});
88			}
89			setIsModal(isModal) {
90				this.#isModal = isModal;
91			}
92
93			// Public Methods (Util)
94			show() {
95				// Run Will Show Callback Methods
96				this.#willShowCallbacks.forEach(cb => cb());
97				document.body.classList.add('disable-scroll');
98				this.#isModal ? this.#dialog.showModal() : this.#dialog.show()
99
100				// Focus 초기화	
101				this.#dialog.focus();
102				this.#dialog.blur();
103				const dialogAnimation = this.#dialog.animate(
104					[
105						{ transform: 'translateY(20px)', opacity: 0.5 },
106						{ transform: 'translateY(0)', opacity: 1 }
107					],
108					{
109						duration: 500,
110						easing: ANI_DEFAUT,
111						fill: 'forwards'
112					}
113				);
114				dialogAnimation.addEventListener('finish', () => {
115					// Run Did Show Callback Methods
116					this.#didShowCallbacks.forEach(cb => cb());
117				});
118			}
119			close() {
120				if (this.#dialog.classList.contains('closing')) {
121					return;
122				}
123				// Run Will Close Callback Methods
124				this.#dialog.classList.add('closing');
125				this.#willCloseCallbacks.forEach(cb => cb());
126				const dialogAnimation = this.#dialog.animate(
127					[
128						{ transform: 'translateY(0)', opacity: 1 },
129						{ transform: 'translateY(20px)', opacity: 0 }
130					],
131					{
132						duration: 300,
133						easing: ANI_DEFAUT,
134						fill: 'forwards'
135					}
136				);
137				dialogAnimation.addEventListener('finish', () => {
138					this.#dialog.close();
139					this.#didCloseCallbacks.forEach(cb => cb());
140					this.#dialog.classList.remove('closing');
141
142					if (document.querySelector('dialog[open]') === null) {
143						document.body.classList.remove('disable-scroll');
144					}
145				});
146			}
147		}
148		// Init Methods
149		const initLayout = () => {
150			const header = document.querySelector(`.wrap .header:not(.${EVENT_BINDED_CLASS})`);
151			const footer = document.querySelector('.wrap .footer');
152			const main = document.querySelector('.wrap .main');
153
154			if (!(header instanceof Element) || !(footer instanceof Element) || !(main instanceof Element)) {
155				return;
156			}
157
158			header.classList.add(EVENT_BINDED_CLASS);
159			const setMainMinHeight = () => {
160				const minHeight = window.innerHeight - (header.clientHeight + footer.clientHeight);
161
162				main.style.minHeight = `${minHeight}px`;
163			}
164			setMainMinHeight();
165			window.addEventListener('resize', setMainMinHeight);
166		}
167		const initMenu = () => {
168			const menuContainer = document.querySelector(`.menu-container:not(.${EVENT_BINDED_CLASS})`);
169			if (!(menuContainer instanceof Element)) {
170				return;
171			}
172			menuContainer.classList.add(EVENT_BINDED_CLASS);
173			const menuBtn = document.querySelector('.header .menu-btn');
174			const closeBtn = document.querySelector('.menu-header .close-btn');
175			const menuDim = menuContainer.querySelector('.sub-menu-bg');
176			const depth1s = document.querySelectorAll('.menu .item .depth1:not(.binded)');
177			
178			function openMenu() {
179				document.body.classList.add('disable-scroll');
180				menuContainer.classList.add('show');
181			}
182			function closeMenu() {
183				document.body.classList.remove('disable-scroll');
184				menuContainer.classList.remove('show');
185			}
186
187			menuBtn.addEventListener('click', openMenu); // 모바일 햄버거 메뉴 열기
188			closeBtn.addEventListener('click', closeMenu); // 모바일 햄버거 메뉴 닫기
189			menuDim.addEventListener('click', closeMenu);
190
191			// 모바일 depth1 메뉴 여닫이 이벤트
192			depth1s.forEach((depth1) => {
193				depth1.classList.add('binded');
194				depth1.addEventListener('click', () => {
195					if (!UI.isMobile()) {
196						return;
197					}
198
199					depth1.classList.toggle('open');
200				});
201			});
202		}
203		const initInput = () => {
204			// Inputs
205			const inputs = [...document.querySelectorAll(`.input-wrap:not(.${EVENT_BINDED_CLASS})`)];
206
207			inputs.forEach(input => {
208				// 초기 셋팅
209				input.classList.add(EVENT_BINDED_CLASS);
210
211				const inputBtn = input.querySelector('.input-btn');
212				const inputEl = input.querySelector('.input');
213				const onFocus = () => {
214					input.classList.add('focus');
215				};
216				const onBlur = () => {
217					input.classList.remove('focus');
218				}
219
220				if (!(inputEl instanceof Element)) {
221					return;
222				}
223
224				// 공통 포커스 이벤트
225				inputEl.addEventListener('focus', onFocus);
226				inputEl.addEventListener('blur', onBlur);
227
228				if (!(inputBtn instanceof Element)) {
229					return;
230				}
231				inputBtn.addEventListener('focus', onFocus);
232				inputBtn.addEventListener('blur', onBlur);
233				// 인풋 지우기 버튼
234				if (inputBtn.classList.contains('type-reset')) {
235					const forInput = document.getElementById(inputBtn.dataset.for);
236					if (!(forInput instanceof Element)) {
237						console.error('data-for의 값을 확인해주세요').
238						return;
239					}
240
241					// Input 이벤트
242					forInput.addEventListener('focus', () => {
243						inputBtn.classList.remove('hide');
244					});
245					forInput.addEventListener('blur', (e) => {
246						if (e.relatedTarget && e.relatedTarget == inputBtn) {
247							return;
248						}
249						inputBtn.classList.add('hide');
250					});
251
252					// 버튼 이벤트
253					inputBtn.addEventListener('click', () => {
254						forInput.value = '';
255						forInput.focus();
256					});
257					inputBtn.addEventListener('blur', () => {
258						inputBtn.classList.add('hide');
259					});
260				}
261				// 비밀번호 나타내기/숨기기 토글
262				else if (inputBtn.classList.contains('type-eye')) {
263					const forInput = document.getElementById(inputBtn.dataset.for);
264					const eyeIcon = inputBtn.querySelector('.icon.eye');
265					const closedEyeIcon = inputBtn.querySelector('.icon.closed-eye');
266
267					if (!(forInput instanceof Element) ||
268						!(eyeIcon instanceof Element)  ||
269						!(closedEyeIcon instanceof Element)) {
270							console.error('나타내기/숨기기 요소들을 확인해주세요').
271						return;
272					}
273
274					inputBtn.addEventListener('click', () => {
275						if (forInput.getAttribute('type') === 'password') {
276							forInput.setAttribute('type', 'text');
277							eyeIcon.classList.add('hide');
278							closedEyeIcon.classList.remove('hide');
279						} else {
280							forInput.setAttribute('type', 'password');
281							eyeIcon.classList.remove('hide');
282							closedEyeIcon.classList.add('hide');
283						}
284					});
285				}
286				// 날짜 입력
287				// else if (inputBtn.classList.contains('type-calendar')) {
288				// 	const forInput = document.getElementById(inputBtn.dataset.for);
289				// 	if (!(forInput instanceof Element)) {
290				// 		console.error('data-for의 값을 확인해주세요').
291				// 		return;
292				// 	}
293					
294				// 	inputBtn.addEventListener('click', () => {
295				// 		calendar({
296				// 			targetQuery: inputBtn.dataset.for,
297				// 			disableWeekend: forInput.dataset.disableWeekend,
298				// 		});
299				// 	})
300				// }
301			});
302			
303			// Select
304			const selects = [...document.querySelectorAll(`.select:not(.${EVENT_BINDED_CLASS})`)];
305			selects.forEach(select => {
306				select.classList.add(EVENT_BINDED_CLASS);
307				const selectBtn = select.querySelector('.select-btn');
308				const selectInput = select.querySelector('select');
309
310				if (!(selectBtn instanceof Element)) {
311					return;
312				}
313
314				function openSelect() {
315					const fakeOptions = document.createElement('ul');
316					fakeOptions.classList.add('options');
317					fakeOptions.setAttribute('tabIndex', -1);
318					const options = select.querySelectorAll('select>option');
319					
320					options.forEach(option => {
321						const fakeOption = document.createElement('li');
322						const fakeOptionBtn = document.createElement('button');
323
324						if (option.selected) {
325							fakeOption.classList.add('active');
326						}
327						fakeOptionBtn.classList.add('option-btn');
328						fakeOptionBtn.textContent = option.textContent;
329						fakeOptionBtn.addEventListener('click', () => {
330							const activeFakeOption = fakeOptions.querySelector('.active');
331							if (activeFakeOption instanceof Element) {
332								activeFakeOption.classList.remove('active');
333							}
334							if (option.selected === false) {
335								fakeOption.classList.add('active');
336								selectBtn.textContent = option.textContent;
337								option.selected = true;
338								selectInput.value = option.value;
339								selectInput.dispatchEvent(new Event('change'));
340							}
341
342							closeSelect();
343						});
344						fakeOptionBtn.addEventListener('focus', () => {
345							select.classList.add('focus');
346						});
347						fakeOptionBtn.addEventListener('blur', (e) => {
348							if (!select.classList.contains('open')) {
349								return;
350							}
351							if (e.relatedTarget && e.relatedTarget.classList.contains('option-btn')) {
352								return;
353							}
354							if (e.relatedTarget && e.relatedTarget == selectBtn) {
355								return;
356							}
357							closeSelect();
358						});
359						if (options.item(options.length - 1) === option) {
360						}
361						fakeOption.appendChild(fakeOptionBtn);
362						fakeOptions.append(fakeOption);
363					});
364
365					select.appendChild(fakeOptions);
366					// 위치가 화면의 절반을 초과하면 위로 나타내기
367					if (fakeOptions.getBoundingClientRect().top > window.innerHeight / 2) {
368						fakeOptions.classList.add('up');
369					}
370
371					select.classList.add('open');
372					select.classList.add('focus')
373				}
374				function closeSelect() {
375					select.classList.remove('open');
376					select.classList.remove('focus');
377
378					if (select.querySelector('.options') != null) {
379						select.querySelector('.options').remove();
380					}
381				}
382
383				selectBtn.addEventListener('mousedown', () => {
384					if (select.classList.contains('open')) {
385						closeSelect();
386					} else {
387						openSelect();
388					}
389				});
390				selectBtn.addEventListener('focus', () => {
391					select.classList.add('focus');
392					if (!select.classList.contains('open')) {
393						openSelect();
394					}
395				});
396				selectBtn.addEventListener('blur', (e) => {
397					if (e.relatedTarget && e.relatedTarget.classList.contains('option-btn')) {
398						return;
399					}
400					if (e.relatedTarget && e.relatedTarget.classList.contains('options')) {
401						return;
402					}
403					closeSelect();
404				});
405			});
406		}
407		const initTab = () => {
408			const tabWraps = [...document.querySelectorAll(`.tab-wrap:not(.${EVENT_BINDED_CLASS})`)];
409
410			tabWraps.forEach(tabWrap => {
411				function getTabBtn() {
412					for (let i = 0; i < tabWrap.children.length; i++) {
413						const child = tabWrap.children[i];
414
415						if (child.classList.contains('tab-btns')) {
416							return [...child.children];
417						}
418					}
419
420					return undefined;
421				}
422				const tabBtns = getTabBtn();
423				const tabs = tabWrap.querySelector('.tabs');
424				let currenetTab = undefined;
425
426				window.addEventListener('resize', () => {
427					tabs.style.maxHeight = `${currenetTab.clientHeight}px`;
428				});
429
430				// init
431				tabs.style.transform = 'translateX(0)';
432
433				tabBtns.forEach((tabBtn, key) => {
434					if (key === 0) {
435						const tabId = tabBtn.dataset.for;
436						const tab = tabWrap.querySelector(`#${tabId}`);
437						tabs.style.maxHeight = `${tab.clientHeight}px`;
438						currenetTab = tab;
439					}
440
441					tabBtn.addEventListener('click', () => {
442						const tabId = tabBtn.dataset.for;
443						const tab = tabWrap.querySelector(`#${tabId}`);
444						const bar = tabWrap.querySelector('.bar');
445						const persent = key * 100;
446						
447						currenetTab = tab;
448						tabWrap.querySelector('.tab-btn.active').classList.remove('active');
449						tabBtn.classList.add('active');
450						// 탭 크기 변경
451						tabs.style.maxHeight = `${tab.clientHeight}px`;
452						window.dispatchEvent(new Event('resize'));
453						// 탭 이동
454						const tabAnimation = tabs.animate(
455							[
456								{ transform: tabWrap.style.transform },
457								{ transform: `translateX(-${persent}%)` }
458							],
459							{
460								duration: 500,
461								easing: ANI_DEFAUT,
462								fill: 'forwards'
463							}
464						);
465						// 탭 버튼 이동
466						const barAnimation = bar.animate(
467							[
468								{ transform: tabWrap.style.transform },
469								{ transform: `translateX(${persent}%)` }
470							],
471							{
472								duration: 500,
473								easing: ANI_DEFAUT,
474								fill: 'forwards'
475							}
476						);
477					});
478				});
479			});
480		}
481		const initSideIndex = () => {
482			const sideIdxs = [...document.querySelectorAll(`.side-idx:not(.${EVENT_BINDED_CLASS})`)];
483
484			sideIdxs.forEach(sideIdx => {
485				const sideBtn = sideIdx.querySelector('.side-btn');
486
487				sideBtn.addEventListener('click', () => {
488					sideIdx.classList.toggle('open');
489				});
490				sideIdx.classList.add(EVENT_BINDED_CLASS);
491			});
492		}
493		const initShowable = () => {
494			const observer = new IntersectionObserver((entries, observer) => {
495				entries.forEach(entry => {
496					if (entry.isIntersecting) {
497						const delay = entry.target.dataset.delay ? entry.target.dataset.delay : 0;
498						setTimeout(() => {
499							entry.target.classList.add("visible");
500							if (entry.target.showableCallBack instanceof Function) {
501								entry.target.showableCallBack();
502							}
503						}, delay);
504						observer.unobserve(entry.target);
505					}
506				});
507			}, { threshold: 0.2 }); // 요소가 20% 이상 보이면 실행
508
509			document.querySelectorAll(`.showable:not(.${EVENT_BINDED_CLASS})`).forEach(element => {
510				observer.observe(element);
511				element.classList.add(EVENT_BINDED_CLASS);
512			});
513		}
514		const initParentToggle = () => {
515			const parents = [...document.querySelectorAll(`.parent:not(.${EVENT_BINDED_CLASS})`)];
516
517			parents.forEach(parent => {
518				parent.addEventListener('click', (e) => {
519					parent.classList.toggle('active');
520				});
521				parent.classList.add(EVENT_BINDED_CLASS);
522			});
523		}
524		// Run Init Methods
525		document.addEventListener('DOMContentLoaded', () => {
526			initLayout();
527			initMenu();
528			initInput();
529			initTab();
530			initSideIndex();
531			initShowable();
532			initParentToggle();
533		});
534		// 외부 Methods
535		const alert = ({ 
536			title = '',
537			msg = '',
538			confirmBtnText = '확인',
539			onConfirm = () => {},
540		} = {}) => {
541			const alertElement = document.createElement('dialog');
542			const headerStr = title ? `
543				<header class="dialog-header">
544					<span class="dialog-tt">${title.replaceAll('\n', '<br>')}</span>
545				</header>` : '';
546			const alertStr = `
547				<div class="dialog">
548					${headerStr}
549					<div class="dialog-main">
550						<p class="msg">${msg.replaceAll('\n', '<br>')}</p>
551					</div>
552					<footer class="dialog-footer">
553						<button class="btn size-md-fix" type="button" confirm>${confirmBtnText}</button>
554					</footer>
555				</div>
556			`;
557			alertElement.classList.add('type-alert');
558			alertElement.innerHTML = alertStr;
559			const alert = new Dialog(alertElement);
560			alert.onConfirmCallback(onConfirm);
561			alert.addDidCloseCallback(() => {
562				alertElement.remove();
563			});
564			document.body.appendChild(alertElement);
565			alert.show();
566		}
567		const confirm = ({
568			title = '',
569			msg = '',
570			cancelBtnText = '취소',
571			confirmBtnText = '확인',
572			onCancel = () => {},
573			onConfirm = () => {},
574		} = {}) => {
575			const confirmElement = document.createElement('dialog');
576			const headerStr = title ? `
577				<header class="dialog-header">
578					<span class="dialog-tt">${title.replaceAll('\n', '<br>')}</span>
579				</header>` : '';
580			const confirmStr = `
581				<div class="dialog">
582					${headerStr}
583					<div class="dialog-main">
584						<p class="msg">${msg.replaceAll('\n', '<br>')}</p>
585					</div>
586					<footer class="dialog-footer">
587						<button class="btn size-md-fix theme-gray" type="button" closeable cancel>${cancelBtnText}</button>
588						<button class="btn size-md-fix" type="button" confirm>${confirmBtnText}</button>
589					</footer>
590				</div>
591			`;
592			confirmElement.classList.add('type-alert');
593			confirmElement.innerHTML = confirmStr;
594			const confirm = new Dialog(confirmElement);
595			
596			confirm.onConfirmCallback(onConfirm);
597			confirm.onCancelCallback(onCancel);
598			confirm.addDidCloseCallback(() => {
599				confirmElement.remove();
600			});
601
602			document.body.appendChild(confirmElement);
603			confirm.show();
604		}
605		const calendar = ({
606			targetQuery = '',
607			min = '1900-01-01',
608			max = '2099-12-31',
609			disableWeekend = false,
610			getHolyDays = () => {return []},
611		} = {}) => {
612			// Validation
613			const input = document.querySelector(targetQuery);
614			if (input === null || !(input instanceof Element)) {
615				console.error(`targetQuery: ${targetQuery}에 해당하는 요소를 찾을 수 없습니다.`);
616				return;
617			}
618			if (!(input instanceof HTMLInputElement)) {
619				console.error(`targetQuery: ${targetQuery}에 해당하는 요소는 Input 요소가 아닙니다.`);
620				return;
621			}
622
623			// Calendar Element Create & Append
624			const calendarDialogEl = document.createElement('dialog');
625			calendarDialogEl.classList.add('type-calendar');
626			calendarDialogEl.innerHTML = `
627				<div class="dialog">
628					<header class="dialog-header">
629						<span class="dialog-tt">
630							<button class="schedule-btn prev" type="button">
631								<i class="icon arrow-left-s"></i>
632							</button>
633							<span class="month-str">01월</span>
634							<button class="schedule-btn next" type="button">
635								<i class="icon arrow-right-s"></i>
636							</button>
637						</span>
638						<div class="select calendar-year">
639							<button class="select-btn" type="button">2024</button>
640							<select class="blind" tabindex="-1">
641							</select>
642						</div>
643						<button class="btn theme-transparent type-icon" type="button" closeable="">
644							<i class="icon cross"><i class="blind">Dialog 닫기</i></i>
645						</button>
646					</header>
647					<div class="dialog-main">
648						<div class="calendar-wrap">
649							<div class="weekend">
650								<span>일</span>
651								<span>월</span>
652								<span>화</span>
653								<span>수</span>
654								<span>목</span>
655								<span>금</span>
656								<span>토</span>
657							</div>
658							<div class="days">
659							</div>
660						</div>
661					</div>
662				</div>`;
663			const dialog = new Dialog(calendarDialogEl);
664			const prevBtn = calendarDialogEl.querySelector('.schedule-btn.prev');
665			const nextBtn = calendarDialogEl.querySelector('.schedule-btn.next');
666
667			let minDate = new Date(min);
668			let maxDate = new Date(max);
669			let inputDate = new Date(input.value);
670			let currentDate = new Date();
671
672			if (minDate > maxDate) {
673				minDate = new Date('1900-01-01');
674				maxDate = new Date('2099-12-31');
675				console.warn(`최소날짜(${minDate})보다 최대날짜(${maxDate})가 클 수 없습니다. 해당 값들이 기본값으로 변경됩니다.`);
676			}
677			if (isNaN(inputDate.getTime())) {
678				console.warn(`Input값: ${input.value} 는 유효하지 않은 값입니다. 현재 날짜로 값이 변경됩니다.`);
679				inputDate = new Date();
680			}
681			currentDate.setTime(inputDate.getTime());
682			minDate.setHours(0, 0, 0, 0);
683			maxDate.setHours(0, 0, 0, 0);
684			inputDate.setHours(0, 0, 0, 0);
685			currentDate.setHours(0, 0, 0, 0);
686			if (currentDate < minDate || currentDate > maxDate) {
687				console.warn(`${input.value} 는 유효하지 않은 값입니다. 현재 날짜로 값이 변경됩니다.`);
688				inputDate = new Date();
689				inputDate.setHours(0, 0, 0, 0);
690			}
691
692			function zf(n) {
693				return `0${n}`.slice(-2);
694			}
695			function generateCalendar(year, month) {
696				let firstDay = new Date(year, month - 1, 1); // 해당 달의 첫 번째 날
697				let lastDay = new Date(year, month, 0); // 해당 달의 마지막 날
698			
699				let prevLastDay = new Date(year, month - 1, 0).getDate(); // 이전 달의 마지막 날
700				let nextStartDay = 1; // 다음 달 시작 날짜
701			
702				let startWeekday = firstDay.getDay(); // 첫날의 요일 (0: 일요일, 6: 토요일)
703				let totalDays = lastDay.getDate(); // 이번 달 총 일수
704			
705				let calendar = [];
706			
707				// 이전 달의 여분 날짜 채우기
708				for (let i = startWeekday - 1; i >= 0; i--) {
709					calendar.push({ 
710						year: month - 1 === 0 ? year - 1 : year, 
711						month: month - 1 === 0 ? 12 : month - 1, 
712						day: prevLastDay - i, 
713						currentMonth: false 
714					});
715				}
716			
717				// 이번 달 날짜 채우기
718				for (let day = 1; day <= totalDays; day++) {
719					const dayDate = new Date();
720					dayDate.setHours(0, 0, 0, 0);
721					dayDate.setFullYear(year, month-1, day);
722
723					calendar.push({
724						year,
725						month,
726						day,
727						currentMonth: !(dayDate < minDate || dayDate > maxDate),
728					});
729				}
730			
731				// 다음 달의 여분 날짜 채우기
732				let remaining = (7 - (calendar.length % 7)) % 7; // 남은 칸 수 계산
733				for (let i = 0; i < remaining; i++) {
734					calendar.push({
735						year: month + 1 === 13 ? year + 1 : year,
736						month: month + 1 === 13 ? 1 : month + 1,
737						day: nextStartDay++,
738						currentMonth: false
739					});
740				}
741			
742				return calendar;
743			}
744			function renderCalendar(year, month) {
745				currentDate.setFullYear(year);
746				currentDate.setMonth(month-1);
747				// Days
748				const days = generateCalendar(year, month);
749				const daysWrap = calendarDialogEl.querySelector('.days');
750				daysWrap.innerHTML = '';
751
752				days.forEach((day) => {
753					const span = document.createElement('span');
754					span.textContent = day.day;
755					span.id = `D${day.year}${zf(day.month)}${zf(day.day)}`;
756					span.dataset.date = `${day.year}-${zf(day.month)}-${zf(day.day)}`;
757					if (!day.currentMonth) {
758						span.classList.add('disable');
759					} else {
760						// 주말 비활성화 && 주말인 경우
761						const nowDate = new Date(span.dataset.date);
762						if (disableWeekend && (nowDate.getDay() === 0 || nowDate.getDay() === 6)) {
763							span.onclick = function (e) {
764								alert({
765									msg: '해당 요일은 선택할 수 없습니다.'
766								});
767							};
768						} else {
769							span.tabIndex = 0;
770							span.onclick = function (e) {
771								input.value = span.dataset.date;
772								dialog.close();
773							};
774						}
775					}
776
777					daysWrap.appendChild(span);
778				});
779
780				// Month
781				const monthStr = calendarDialogEl.querySelector('.month-str');
782				monthStr.textContent = `${zf(month)}월`;
783				
784				// 이전달/다음달 활성화&비활성화
785				const tempDate = new Date();
786				tempDate.setHours(0, 0, 0, 0);
787				tempDate.setFullYear(year);
788				tempDate.setMonth(month-1);
789				tempDate.setDate(1);
790				
791				prevBtn.removeAttribute('disabled');
792				nextBtn.removeAttribute('disabled');
793				if (tempDate <= minDate) {
794					prevBtn.setAttribute('disabled',  true);
795				}
796				tempDate.setMonth(month);
797				if (tempDate > maxDate) {
798					nextBtn.setAttribute('disabled',  true);
799				}
800
801				// Select
802				const selectWrap = calendarDialogEl.querySelector('.calendar-year');
803				const select = selectWrap.querySelector('select');
804				const SELECTABLE_YEAR_COUNT = 10;
805
806				select.innerHTML = '';
807				for (let i = 0; i < SELECTABLE_YEAR_COUNT; i++) {
808					const option = document.createElement('option');
809					option.textContent = year - SELECTABLE_YEAR_COUNT/2 + i;
810					option.value = option.textContent;
811
812					if (Number(option.value) === Number(year)) {
813						selectWrap.querySelector('.select-btn').textContent = year;
814						option.selected = true;
815					}
816
817					if (Number(option.textContent) >= minDate.getFullYear() &&
818						Number(option.textContent) <= maxDate.getFullYear()) {
819						select.appendChild(option);
820					}
821				}
822				select.onchange = function(e) {
823					// Select 년도 선택시 해당 년도로 달력 재렌더링
824					renderCalendar(e.target.value, month);
825				}
826				initInput();
827
828				// 추가로 날짜별 속성 정의
829				// 1. 공휴일 추가
830				const holyDays = [...getHolyDays(year, month)];
831				holyDays.forEach((holyDay) => {
832					const holyDayEl = document.getElementById(`D${holyDay.replaceAll('.', '').replaceAll('-', '')}`);
833					if (holyDayEl instanceof Element) {
834						holyDayEl.classList.add('holyday');
835						holyDayEl.onclick = function (e) {
836							alert({
837								msg: '해당 요일은 선택할 수 없습니다.'
838							});
839						};
840					}
841				});
842				// 2. 당일 날짜 추가
843				const newDate = new Date();
844				const nowDayEl = document.getElementById(`D${newDate.getFullYear()}${zf(newDate.getMonth()+1)}${zf(newDate.getDate())}`);
845				if (nowDayEl !== null) {
846					nowDayEl.classList.add('now');
847				}
848				// 3. 활성화 날짜 Active
849				const currentDayEl = document.getElementById(`D${inputDate.getFullYear()}${zf(inputDate.getMonth()+1)}${zf(inputDate.getDate())}`);
850				if (currentDayEl !== null && 
851					!currentDayEl.classList.contains('disable') &&
852					!currentDayEl.classList.contains('holyday')) {
853					
854					if (disableWeekend && 
855						(inputDate.getDay() === 0 || inputDate.getDay() === 6)) {
856						return;
857					}
858					currentDayEl.classList.add('active');
859				}
860			}
861
862			// Init UI, Event
863			dialog.addWillShowCallback(() => {
864				// 최초 1회 렌더링
865				document.body.appendChild(calendarDialogEl);
866				renderCalendar(inputDate.getFullYear(), inputDate.getMonth()+1);
867			});
868			dialog.addDidCloseCallback(() => {
869				calendarDialogEl.remove();
870			});
871			prevBtn.addEventListener('click', function () {
872				currentDate.setMonth(currentDate.getMonth() - 1);
873				renderCalendar(currentDate.getFullYear(), currentDate.getMonth()+1);
874			});
875			nextBtn.addEventListener('click', function () {
876				currentDate.setMonth(currentDate.getMonth() + 1);
877				renderCalendar(currentDate.getFullYear(), currentDate.getMonth()+1);
878			});
879
880			return dialog;
881		}
882		const loading = (target) => {
883			if ((target instanceof Element)) {
884				const loadingElement = document.createElement('div');
885				const parent = target;
886				let originParentPosition = '';
887				
888				if (parent.style.position) {
889					originParentPosition = parent.style.position;
890				}
891				parent.style.position = 'relative';
892				loadingElement.classList.add('loading');
893				loadingElement.innerHTML = `
894					<p class="blind">로딩중...</p>
895					<i class="icon loading-gif"></i>
896				`;
897	
898				parent.appendChild(loadingElement);
899
900				return {
901					close: () => {
902						if (originParentPosition !== '') {
903							parent.style.position = originParentPosition;
904						} else {
905							parent.style.position = '';
906						}
907						loadingElement.remove();
908					}
909				}
910			} else {
911				const loadingElement = document.createElement('dialog');
912				const parent = document.body;
913				loadingElement.classList.add('loading');
914				loadingElement.innerHTML = `
915					<p class="blind">로딩중...</p>
916					<i class="icon loading-gif"></i>
917				`;
918	
919				const loading = new Dialog(loadingElement);
920				loading.addDidCloseCallback(() => {
921					loadingElement.remove();
922				});
923	
924				parent.appendChild(loadingElement);
925				loading.show();
926
927				return loading;
928			}
929		}
930
931		// UI.~ 로 접근하도록 설정
932		const global = {
933			Dialog: Dialog,
934			alert: alert,
935			confirm: confirm,
936			loading: loading,
937			calendar: calendar,
938			isMobile: () => {
939				const MOBILE_WIDTH = 768; // 768px 부터 모바일로 판단
940				return window.innerWidth <= MOBILE_WIDTH;
941			},
942			isPC: () => {
943				const userAgent = navigator.userAgent.toLowerCase();
944			
945				// 모바일 및 태블릿 기기 User-Agent 패턴 목록
946				const mobileRegex = /(iphone|ipod|ipad|android|blackberry|bb10|windows phone|mobile|tablet)/;
947				// 태블릿 종류인지 확인
948				const isIPad =  (userAgent.includes("ipad")) || 
949								(userAgent.includes("macintosh") && "ontouchend" in document && navigator.maxTouchPoints > 0);
950				// 안드로이드 테블릿 종류 확인								
951				const isAndroidTablet = userAgent.includes("android") && !userAgent.includes("mobile");
952			
953				// 기본 User-Agent 검사 (모바일, 태블릿이면 false)
954				if (mobileRegex.test(userAgent)) {
955					return false;
956				}
957				 // 태블릿은 PC가 아님
958				if (isIPad || isAndroidTablet) {
959					return false;
960				}
961				// User-Agent Client Hints 지원 여부 확인
962				if (navigator.userAgentData && navigator.userAgentData.platform) {
963					const platform = navigator.userAgentData.platform.toLowerCase();
964					
965					// 일반적으로 PC 플랫폼으로 분류되는 값 확인
966					const pcPlatforms = ["windows", "macos", "linux", "chrome os"];
967					
968					return pcPlatforms.includes(platform);
969				}
970				
971				 // 화면 크기를 추가적으로 검사
972				if (global.isMobile()) {
973					return false;
974				}
975			
976				return true; // 기본적으로 PC로 간주
977			}
978		}
979		window.UI = global;
980	}
981	initUI();
982})();

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.