-
📚 내 서재
+
내 서재
@@ -322,13 +322,13 @@
-
🏷️ 태그 및 키워드 관리
+ ️ 태그 및 키워드 관리
×
-
-
+
+
@@ -345,7 +345,7 @@
-
+
@@ -562,10 +562,10 @@
if (!readerId) {
const sortSelect = document.getElementById('library-sort-select');
- const tagFilterSelect = document.getElementById('library-tag-filter'); // 🆕 태그 필터
+ const tagFilterSelect = document.getElementById('library-tag-filter'); // 태그 필터
let currentSort = localStorage.getItem('webReader_sortOption') || 'lastRead';
- let currentTagFilter = 'all'; // 🆕 현재 선택된 태그 상태
+ let currentTagFilter = 'all'; // 현재 선택된 태그 상태
sortSelect.value = currentSort;
@@ -575,7 +575,7 @@
renderLibraryList();
});
- // 🆕 태그 변경 이벤트
+ // 태그 변경 이벤트
tagFilterSelect.addEventListener('change', (e) => {
currentTagFilter = e.target.value;
renderLibraryList();
@@ -592,7 +592,7 @@
return { ...book, savedData, pKey };
});
- // 2. 🆕 존재하는 모든 태그 수집 (중복 제거)
+ // 2. 존재하는 모든 태그 수집 (중복 제거)
const uniqueTags = new Set();
booksWithMeta.forEach(book => {
if (book.savedData.tags && Array.isArray(book.savedData.tags)) {
@@ -603,8 +603,8 @@
}
});
- // 3. 🆕 태그 필터 UI 업데이트 (현재 선택된 값 유지)
- tagFilterSelect.innerHTML = '
';
+ // 3. 태그 필터 UI 업데이트 (현재 선택된 값 유지)
+ tagFilterSelect.innerHTML = '
';
Array.from(uniqueTags).sort().forEach(tag => {
const option = document.createElement('option');
option.value = tag;
@@ -613,7 +613,7 @@
tagFilterSelect.appendChild(option);
});
- // 4. 🆕 태그 필터링 적용
+ // 4. 태그 필터링 적용
let filteredBooks = booksWithMeta;
if (currentTagFilter !== 'all') {
filteredBooks = booksWithMeta.filter(book => {
@@ -685,7 +685,7 @@
const totalChars = book.chapters ? book.chapters.reduce((sum, chap) => sum + (chap.content ? chap.content.length : 0), 0) : 0;
let progressText = savedData.chapter !== undefined ? `
(Ch.${savedData.chapter + 1})` : '';
- let bookStatsText = `
📚 총 ${totalChapters}화 · ${totalChars.toLocaleString()}자
`;
+ let bookStatsText = `
총 ${totalChapters}화 · ${totalChars.toLocaleString()}자
`;
let tagsText = '';
if (savedData.tags && savedData.tags.length > 0) {
@@ -699,8 +699,8 @@
tagsText = `
${badges}
`;
}
- let lastReadText = savedData.lastRead ? `
🕒 마지막 열람: ${savedData.lastRead}
` : '';
- let memoText = savedData.memo ? `
📝 ${savedData.memo}
` : '';
+ let lastReadText = savedData.lastRead ? `
마지막 열람: ${savedData.lastRead}
` : '';
+ let memoText = savedData.memo ? `
${savedData.memo}
` : '';
const titleArea = document.createElement('div');
titleArea.style.cssText = "flex: 1; display: flex; flex-direction: column; margin-bottom: 10px;";
@@ -732,7 +732,7 @@
const isCompleted = savedData.isCompleted || false;
const compBtn = document.createElement('button');
- compBtn.innerText = isCompleted ? "✔️ 완독" : "📖 읽는 중";
+ compBtn.innerText = isCompleted ? "✔️ 완독" : " 읽는 중";
compBtn.style.cssText = `grid-column: span 2; padding: 8px 0; background-color: ${isCompleted ? '#27ae60' : '#95a5a6'}; ${commonBtnStyle}`;
compBtn.onclick = (e) => {
e.stopPropagation();
@@ -1402,11 +1402,11 @@
// [8] 태그 및 키워드 자동 추출 로직 (+ 태그 룰 백업/복원)
// ============================================================================
const DEFAULT_KEYWORD_RULES = {
- "🐉 판타지": ["마법", "오크", "엘프", "드래곤", "제국", "마왕"],
+ " 판타지": ["마법", "오크", "엘프", "드래곤", "제국", "마왕"],
"⚔️ 무협": ["무림", "천마", "검기", "내공", "혈교", "무당파"],
- "🏢 현판": ["헌터", "게이트", "각성", "시스템", "상태창", "던전"],
- "💕 로맨스": ["황제", "공작", "백작", "영애", "황태자", "정략결혼"],
- "🚀 SF": ["우주선", "안드로이드", "인공지능", "사이보그", "은하제국"]
+ " 현판": ["헌터", "게이트", "각성", "시스템", "상태창", "던전"],
+ " 로맨스": ["황제", "공작", "백작", "영애", "황태자", "정략결혼"],
+ " SF": ["우주선", "안드로이드", "인공지능", "사이보그", "은하제국"]
};
function getKeywordRules() {
@@ -1466,7 +1466,7 @@
const btnSaveKeywordRules = document.getElementById('btn-save-keyword-rules');
const btnReapplyTags = document.getElementById('btn-reapply-tags');
- // 💡 태그 룰 전용 백업 및 복원 로직
+ // 태그 룰 전용 백업 및 복원 로직
document.getElementById('btn-backup-tags').addEventListener('click', () => {
const currentRules = getKeywordRules();
downloadJSON(currentRules, `tag_rules_backup_${Date.now()}.json`);
@@ -1520,7 +1520,7 @@
row.className = 'keyword-rule-row';
row.style.cssText = "display: flex; gap: 6px; margin-bottom: 8px; align-items: center;";
row.innerHTML = `
-
+
`;
diff --git a/app/src/main/assets/extensions/my_extension/sdfdf.js b/app/src/main/assets/extensions/my_extension/sdfdf.js
new file mode 100644
index 00000000..7f1b515b
--- /dev/null
+++ b/app/src/main/assets/extensions/my_extension/sdfdf.js
@@ -0,0 +1,2965 @@
+'use stict';
+
+// babel start
+function _regeneratorRuntime() {
+ "use strict"; /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
+ _regeneratorRuntime = function _regeneratorRuntime() {
+ return e;
+ };
+ var t, e = {},
+ r = Object.prototype,
+ n = r.hasOwnProperty,
+ o = Object.defineProperty || function(t, e, r) {
+ t[e] = r.value;
+ },
+ i = "function" == typeof Symbol ? Symbol : {},
+ a = i.iterator || "@@iterator",
+ c = i.asyncIterator || "@@asyncIterator",
+ u = i.toStringTag || "@@toStringTag";
+
+ function define(t, e, r) {
+ return Object.defineProperty(t, e, {
+ value: r,
+ enumerable: !0,
+ configurable: !0,
+ writable: !0
+ }), t[e];
+ }
+ try {
+ define({}, "");
+ } catch (t) {
+ define = function define(t, e, r) {
+ return t[e] = r;
+ };
+ }
+
+ function wrap(t, e, r, n) {
+ var i = e && e.prototype instanceof Generator ? e : Generator,
+ a = Object.create(i.prototype),
+ c = new Context(n || []);
+ return o(a, "_invoke", {
+ value: makeInvokeMethod(t, r, c)
+ }), a;
+ }
+
+ function tryCatch(t, e, r) {
+ try {
+ return {
+ type: "normal",
+ arg: t.call(e, r)
+ };
+ } catch (t) {
+ return {
+ type: "throw",
+ arg: t
+ };
+ }
+ }
+ e.wrap = wrap;
+ var h = "suspendedStart",
+ l = "suspendedYield",
+ f = "executing",
+ s = "completed",
+ y = {};
+
+ function Generator() {}
+
+ function GeneratorFunction() {}
+
+ function GeneratorFunctionPrototype() {}
+ var p = {};
+ define(p, a, function() {
+ return this;
+ });
+ var d = Object.getPrototypeOf,
+ v = d && d(d(values([])));
+ v && v !== r && n.call(v, a) && (p = v);
+ var g = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(p);
+
+ function defineIteratorMethods(t) {
+ _.forEach(["next", "throw", "return"], function(e) {
+ define(t, e, function(t) {
+ return this._invoke(e, t);
+ });
+ });
+ }
+
+ function AsyncIterator(t, e) {
+ function invoke(r, o, i, a) {
+ var c = tryCatch(t[r], t, o);
+ if ("throw" !== c.type) {
+ var u = c.arg,
+ h = u.value;
+ return h && "object" == _typeof(h) && n.call(h, "__await") ? e.resolve(h.__await).then(function(t) {
+ invoke("next", t, i, a);
+ }, function(t) {
+ invoke("throw", t, i, a);
+ }) : e.resolve(h).then(function(t) {
+ u.value = t, i(u);
+ }, function(t) {
+ return invoke("throw", t, i, a);
+ });
+ }
+ a(c.arg);
+ }
+ var r;
+ o(this, "_invoke", {
+ value: function value(t, n) {
+ function callInvokeWithMethodAndArg() {
+ return new e(function(e, r) {
+ invoke(t, n, e, r);
+ });
+ }
+ return r = r ? r.then(callInvokeWithMethodAndArg, callInvokeWithMethodAndArg) : callInvokeWithMethodAndArg();
+ }
+ });
+ }
+
+ function makeInvokeMethod(e, r, n) {
+ var o = h;
+ return function(i, a) {
+ if (o === f) throw Error("Generator is already running");
+ if (o === s) {
+ if ("throw" === i) throw a;
+ return {
+ value: t,
+ done: !0
+ };
+ }
+ for (n.method = i, n.arg = a;;) {
+ var c = n.delegate;
+ if (c) {
+ var u = maybeInvokeDelegate(c, n);
+ if (u) {
+ if (u === y) continue;
+ return u;
+ }
+ }
+ if ("next" === n.method) n.sent = n._sent = n.arg;
+ else if ("throw" === n.method) {
+ if (o === h) throw o = s, n.arg;
+ n.dispatchException(n.arg);
+ } else "return" === n.method && n.abrupt("return", n.arg);
+ o = f;
+ var p = tryCatch(e, r, n);
+ if ("normal" === p.type) {
+ if (o = n.done ? s : l, p.arg === y) continue;
+ return {
+ value: p.arg,
+ done: n.done
+ };
+ }
+ "throw" === p.type && (o = s, n.method = "throw", n.arg = p.arg);
+ }
+ };
+ }
+
+ function maybeInvokeDelegate(e, r) {
+ var n = r.method,
+ o = e.iterator[n];
+ if (o === t) return r.delegate = null, "throw" === n && e.iterator["return"] && (r.method = "return", r.arg = t, maybeInvokeDelegate(e, r), "throw" === r.method) || "return" !== n && (r.method = "throw", r.arg = new TypeError("The iterator does not provide a '" + n + "' method")), y;
+ var i = tryCatch(o, e.iterator, r.arg);
+ if ("throw" === i.type) return r.method = "throw", r.arg = i.arg, r.delegate = null, y;
+ var a = i.arg;
+ return a ? a.done ? (r[e.resultName] = a.value, r.next = e.nextLoc, "return" !== r.method && (r.method = "next", r.arg = t), r.delegate = null, y) : a : (r.method = "throw", r.arg = new TypeError("iterator result is not an object"), r.delegate = null, y);
+ }
+
+ function pushTryEntry(t) {
+ var e = {
+ tryLoc: t[0]
+ };
+ 1 in t && (e.catchLoc = t[1]), 2 in t && (e.finallyLoc = t[2], e.afterLoc = t[3]), this.tryEntries.push(e);
+ }
+
+ function resetTryEntry(t) {
+ var e = t.completion || {};
+ e.type = "normal", delete e.arg, t.completion = e;
+ }
+
+ function Context(t) {
+ this.tryEntries = [{
+ tryLoc: "root"
+ }], _.forEach(t, pushTryEntry, this), this.reset(!0);
+ }
+
+ function values(e) {
+ if (e || "" === e) {
+ var r = e[a];
+ if (r) return r.call(e);
+ if ("function" == typeof e.next) return e;
+ if (!isNaN(e.length)) {
+ var o = -1,
+ i = function next() {
+ for (; ++o < e.length;)
+ if (n.call(e, o)) return next.value = e[o], next.done = !1, next;
+ return next.value = t, next.done = !0, next;
+ };
+ return i.next = i;
+ }
+ }
+ throw new TypeError(_typeof(e) + " is not iterable");
+ }
+ return GeneratorFunction.prototype = GeneratorFunctionPrototype, o(g, "constructor", {
+ value: GeneratorFunctionPrototype,
+ configurable: !0
+ }), o(GeneratorFunctionPrototype, "constructor", {
+ value: GeneratorFunction,
+ configurable: !0
+ }), GeneratorFunction.displayName = define(GeneratorFunctionPrototype, u, "GeneratorFunction"), e.isGeneratorFunction = function(t) {
+ var e = "function" == typeof t && t.constructor;
+ return !!e && (e === GeneratorFunction || "GeneratorFunction" === (e.displayName || e.name));
+ }, e.mark = function(t) {
+ return Object.setPrototypeOf ? Object.setPrototypeOf(t, GeneratorFunctionPrototype) : (t.__proto__ = GeneratorFunctionPrototype, define(t, u, "GeneratorFunction")), t.prototype = Object.create(g), t;
+ }, e.awrap = function(t) {
+ return {
+ __await: t
+ };
+ }, defineIteratorMethods(AsyncIterator.prototype), define(AsyncIterator.prototype, c, function() {
+ return this;
+ }), e.AsyncIterator = AsyncIterator, e.async = function(t, r, n, o, i) {
+ void 0 === i && (i = Promise);
+ var a = new AsyncIterator(wrap(t, r, n, o), i);
+ return e.isGeneratorFunction(r) ? a : a.next().then(function(t) {
+ return t.done ? t.value : a.next();
+ });
+ }, defineIteratorMethods(g), define(g, u, "Generator"), define(g, a, function() {
+ return this;
+ }), define(g, "toString", function() {
+ return "[object Generator]";
+ }), e.keys = function(t) {
+ var e = Object(t),
+ r = [];
+ for (var n in e) r.push(n);
+ return r.reverse(),
+ function next() {
+ for (; r.length;) {
+ var t = r.pop();
+ if (t in e) return next.value = t, next.done = !1, next;
+ }
+ return next.done = !0, next;
+ };
+ }, e.values = values, Context.prototype = {
+ constructor: Context,
+ reset: function reset(e) {
+ if (this.prev = 0, this.next = 0, this.sent = this._sent = t, this.done = !1, this.delegate = null, this.method = "next", this.arg = t, _.forEach(this.tryEntries, resetTryEntry), !e)
+ for (var r in this) "t" === r.charAt(0) && n.call(this, r) && !isNaN(+r.slice(1)) && (this[r] = t);
+ },
+ stop: function stop() {
+ this.done = !0;
+ var t = this.tryEntries[0].completion;
+ if ("throw" === t.type) throw t.arg;
+ return this.rval;
+ },
+ dispatchException: function dispatchException(e) {
+ if (this.done) throw e;
+ var r = this;
+
+ function handle(n, o) {
+ return a.type = "throw", a.arg = e, r.next = n, o && (r.method = "next", r.arg = t), !!o;
+ }
+ for (var o = this.tryEntries.length - 1; o >= 0; --o) {
+ var i = this.tryEntries[o],
+ a = i.completion;
+ if ("root" === i.tryLoc) return handle("end");
+ if (i.tryLoc <= this.prev) {
+ var c = n.call(i, "catchLoc"),
+ u = n.call(i, "finallyLoc");
+ if (c && u) {
+ if (this.prev < i.catchLoc) return handle(i.catchLoc, !0);
+ if (this.prev < i.finallyLoc) return handle(i.finallyLoc);
+ } else if (c) {
+ if (this.prev < i.catchLoc) return handle(i.catchLoc, !0);
+ } else {
+ if (!u) throw Error("try statement without catch or finally");
+ if (this.prev < i.finallyLoc) return handle(i.finallyLoc);
+ }
+ }
+ }
+ },
+ abrupt: function abrupt(t, e) {
+ for (var r = this.tryEntries.length - 1; r >= 0; --r) {
+ var o = this.tryEntries[r];
+ if (o.tryLoc <= this.prev && n.call(o, "finallyLoc") && this.prev < o.finallyLoc) {
+ var i = o;
+ break;
+ }
+ }
+ i && ("break" === t || "continue" === t) && i.tryLoc <= e && e <= i.finallyLoc && (i = null);
+ var a = i ? i.completion : {};
+ return a.type = t, a.arg = e, i ? (this.method = "next", this.next = i.finallyLoc, y) : this.complete(a);
+ },
+ complete: function complete(t, e) {
+ if ("throw" === t.type) throw t.arg;
+ return "break" === t.type || "continue" === t.type ? this.next = t.arg : "return" === t.type ? (this.rval = this.arg = t.arg, this.method = "return", this.next = "end") : "normal" === t.type && e && (this.next = e), y;
+ },
+ finish: function finish(t) {
+ for (var e = this.tryEntries.length - 1; e >= 0; --e) {
+ var r = this.tryEntries[e];
+ if (r.finallyLoc === t) return this.complete(r.completion, r.afterLoc), resetTryEntry(r), y;
+ }
+ },
+ "catch": function _catch(t) {
+ for (var e = this.tryEntries.length - 1; e >= 0; --e) {
+ var r = this.tryEntries[e];
+ if (r.tryLoc === t) {
+ var n = r.completion;
+ if ("throw" === n.type) {
+ var o = n.arg;
+ resetTryEntry(r);
+ }
+ return o;
+ }
+ }
+ throw Error("illegal catch attempt");
+ },
+ delegateYield: function delegateYield(e, r, n) {
+ return this.delegate = {
+ iterator: values(e),
+ resultName: r,
+ nextLoc: n
+ }, "next" === this.method && (this.arg = t), y;
+ }
+ }, e;
+}
+
+function asyncGeneratorStep(n, t, e, r, o, a, c) {
+ try {
+ var i = n[a](c),
+ u = i.value;
+ } catch (n) {
+ return void e(n);
+ }
+ i.done ? t(u) : Promise.resolve(u).then(r, o);
+}
+
+function _asyncToGenerator(n) {
+ return function() {
+ var t = this,
+ e = arguments;
+ return new Promise(function(r, o) {
+ var a = n.apply(t, e);
+
+ function _next(n) {
+ asyncGeneratorStep(a, r, o, _next, _throw, "next", n);
+ }
+
+ function _throw(n) {
+ asyncGeneratorStep(a, r, o, _next, _throw, "throw", n);
+ }
+ _next(void 0);
+ });
+ };
+}
+
+function _classCallCheck(a, n) {
+ if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function");
+}
+
+function _defineProperties(e, r) {
+ for (var t = 0; t < r.length; t++) {
+ var o = r[t];
+ o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o);
+ }
+}
+
+function _createClass(e, r, t) {
+ return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", {
+ writable: !1
+ }), e;
+}
+
+function ownKeys(e, r) {
+ var t = Object.keys(e);
+ if (Object.getOwnPropertySymbols) {
+ var o = Object.getOwnPropertySymbols(e);
+ r && (o = o.filter(function(r) {
+ return Object.getOwnPropertyDescriptor(e, r).enumerable;
+ })), t.push.apply(t, o);
+ }
+ return t;
+}
+
+function _objectSpread(e) {
+ for (var r = 1; r < arguments.length; r++) {
+ var t = null != arguments[r] ? arguments[r] : {};
+ r % 2 ? _.forEach(ownKeys(Object(t), !0), function(r) {
+ _defineProperty(e, r, t[r]);
+ }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : _.forEach(ownKeys(Object(t)), function(r) {
+ Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
+ });
+ }
+ return e;
+}
+
+function _defineProperty(e, r, t) {
+ return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
+ value: t,
+ enumerable: !0,
+ configurable: !0,
+ writable: !0
+ }) : e[r] = t, e;
+}
+
+function _toPropertyKey(t) {
+ var i = _toPrimitive(t, "string");
+ return "symbol" == _typeof(i) ? i : i + "";
+}
+
+function _toPrimitive(t, r) {
+ if ("object" != _typeof(t) || !t) return t;
+ var e = t[Symbol.toPrimitive];
+ if (void 0 !== e) {
+ var i = e.call(t, r || "default");
+ if ("object" != _typeof(i)) return i;
+ throw new TypeError("@@toPrimitive must return a primitive value.");
+ }
+ return ("string" === r ? String : Number)(t);
+}
+
+function _typeof(o) {
+ "@babel/helpers - typeof";
+ return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
+ return typeof o;
+ } : function(o) {
+ return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
+ }, _typeof(o);
+}
+// babel end
+
+// polyfill start
+if (!Element.prototype.matches) {
+ Element.prototype.matches =
+ Element.prototype.msMatchesSelector ||
+ Element.prototype.webkitMatchesSelector;
+}
+
+if (!Element.prototype.closest) {
+ Element.prototype.closest = function (s) {
+ var el = this;
+
+ do {
+ if (Element.prototype.matches.call(el, s)) return el;
+ el = el.parentElement || el.parentNode;
+ } while (el !== null && el.nodeType === 1);
+ return null;
+ };
+}
+
+// Polyfill for Element.scrollIntoView
+if (!Element.prototype.scrollIntoView) {
+ Element.prototype.scrollIntoView = function (options) {
+ if (options && options.behavior === 'smooth') {
+ var scrollContainer = this;
+ while (scrollContainer && scrollContainer.scrollTop === 0 && scrollContainer.scrollLeft === 0) {
+ scrollContainer = scrollContainer.parentNode;
+ }
+ if (!scrollContainer) return;
+
+ var targetX = this.getBoundingClientRect().left + scrollContainer.scrollLeft;
+ var targetY = this.getBoundingClientRect().top + scrollContainer.scrollTop;
+ smoothScroll(scrollContainer, targetX, targetY, 300);
+ } else {
+ this.scrollIntoView(options);
+ }
+ };
+}
+
+// Polyfill for window.scroll and window.scrollTo
+if (!window.scroll || !window.scrollTo) {
+ window.scroll = window.scrollTo = function (options) {
+ if (options && options.behavior === 'smooth') {
+ smoothScroll(document.documentElement, options.left || 0, options.top || 0, 300);
+ } else {
+ window.scrollTo(options.left || 0, options.top || 0);
+ }
+ };
+}
+// polyfill end
+
+/**
+ * ES6 문법으로 작성
+ * NH.isElementInView(element, false) // 대상 엘리먼트가 현재 화면에 보여지고 있는 상태인지(스크롤)
+ * NH.UID('plugin', cnt, num) // 고유 ID 생성(플러그인 / 접근성)
+ * NH.attrs(selector, {aria-label: 'blue'}) // 여러 attribute의 값을 한번에
+ * NH.styles(selector, {background: 'blue'}) // 여러 style 을 한번에 입력
+ * NH.addClass(element, 'className') // class 추가
+ * NH.eventMaker(element, 'eventName', { ...option }) // 커스텀 이벤트 생성
+ * NH.removeClass(element, 'className') // class 제거
+ * NH.toggle(element, 'className') // 클래스를 토글
+ * NH.slideUp(target, duration); // slide 기능
+ * NH.slideDown(target, duration); // slide 기능
+ * NH.toggleSlide(target, duration); // slide 를 토글
+ */
+var NH = {
+ // 슬라이드 동작여부
+ slideAnimating: false,
+ /**
+ * 엘리먼트가 보여지고 있는 상태인지 확인
+ * @example
+ * // NH.isElementInView(element, false)
+ * @param {Element} element
+ * @param {Boolean} fullyInView
+ * @returns {Boolean}
+ */
+ isElementInView: function isElementInView(element, fullyInView) {
+ var pageTop = window.scrollY;
+ var pageBottom = pageTop + window.innerHeight;
+ var elementTop = element.offsetTop;
+ var elementBottom = elementTop + element.offsetHeight;
+ if (fullyInView === true) {
+ return pageTop < elementTop && pageBottom > elementBottom;
+ } else {
+ return elementTop <= pageBottom && elementBottom >= pageTop;
+ }
+ },
+ /**
+ * 플러그인의 고유한 id 를 생성할때 사용
+ * @example
+ * // NH.UID('plugin', cnt, num)
+ * @param {pluginName} string - plugin name
+ * @param {Number} cnt - current count
+ * @param {Number} [num] - optional param
+ * @returns {String} pluginName_cnt_num;
+ */
+ UID: function UID(pluginName, cnt) {
+ var num = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
+ var val = num !== '' ? "".concat(pluginName, "_").concat(cnt, "_").concat(num) : "".concat(pluginName, "_").concat(cnt);
+ return val;
+ },
+ /**
+ * 여러 attribute 를 object 형태로 입력
+ * @example
+ * NH.attrs(selector, {aria-label: 'blue'})
+ * @param {Element} element - selector 형태로 입력
+ * @param {Object} arr
+ */
+ attrs: function attrs(element, obj) {
+ var elementType = _typeof(element) === 'object';
+ var elem;
+ if (elementType) {
+ elem = element;
+ } else {
+ elem = document.querySelector(element);
+ }
+ try {
+ for (key in obj) {
+ var attr = key;
+ var val = obj[key];
+ elem.setAttribute(attr, val);
+ }
+ } catch (err) {
+ console.log(err);
+ }
+ },
+ /**
+ * 여러 style 를 object 형태로 입력
+ * @example
+ * NH.styles(selector, {background: 'blue'})
+ * @param {Element} element - selector 형태로 입력
+ * @param {Object} arr
+ */
+ styles: function styles(element, obj) {
+ var elementType = _typeof(element) === 'object';
+ var elem;
+ if (elementType) {
+ elem = element;
+ } else {
+ elem = document.querySelector(element);
+ }
+ try {
+ for (key in obj) {
+ var attr = key;
+ var val = obj[key];
+ elem.style[attr] = val;
+ }
+ } catch (err) {
+ console.log(err);
+ }
+ },
+ /**
+ * class 추가
+ * @example
+ * NH.addClass(element, 'className')
+ * @param {Element} element
+ * @param {String} className
+ */
+ addClass: function addClass(element, className) {
+ element.classList.add(className);
+ },
+ /**
+ * callback 을 위한 eventlistener 생성 기능
+ * NH.eventMaker(element, 'eventName', { ...option })
+ * @param {Element} element
+ * @param {String} eventName
+ * @param {Object} options
+ */
+ eventMaker: function eventMaker(element, eventName, options) {
+ var detailList = options;
+ var eventDetail = new CustomEvent(eventName, {
+ bubbles: true,
+ detail: _objectSpread({}, detailList)
+ });
+ try {
+ element.dispatchEvent(eventDetail);
+ } catch (err) {
+ console.log(err);
+ }
+ },
+ /**
+ * class 삭제
+ * @example
+ * NH.removeClass(element, 'className')
+ * @param {Element} element
+ * @param {String} className
+ */
+ removeClass: function removeClass(element, className) {
+ if (element.classList.contains(className)) {
+ element.classList.remove(className);
+ }
+ },
+ /**
+ * class 토글
+ * @example
+ * NH.toggle(element, 'className')
+ * @param {Element} element
+ * @param {String} className
+ */
+ toggle: function toggle(element, className) {
+ var _ = this;
+ var elementType = _typeof(element) === 'object';
+ var elem = elementType ? element : document.querySelectorAll(element);
+ if (elementType) {
+ elem.addEventListener('click', function (event) {
+ event.preventDefault();
+ if (elem.classList.contains(className)) {
+ _.removeClass(elem, className);
+ } else {
+ _.addClass(elem, className);
+ }
+ }, false);
+ } else {
+ _.forEach(elem, function (el) {
+ el.addEventListener('click', function (event) {
+ event.preventDefault();
+ if (el.classList.contains(className)) {
+ _.removeClass(el, className);
+ } else {
+ _.addClass(el, className);
+ }
+ });
+ });
+ }
+ },
+ /**
+ * slideUp
+ * @example
+ * NH.slideUp(target, duration);
+ * @param {Element} element
+ * @param {Number} duration
+ */
+ slideUp: function slideUp(target) {
+ var duration = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 500;
+ var _ = this;
+ if (_.slideAnimating) return;
+ _.slideAnimating = true;
+ _.styles(target, {
+ transitionProperty: 'height, margin, padding',
+ transitionDuration: duration + 'ms',
+ boxSizing: 'border-box',
+ height: "".concat(target.offsetHeight, "px"),
+ overflow: 'hidden',
+ paddingTop: 0,
+ paddingBottom: 0,
+ marginTop: 0,
+ marginBottom: 0
+ });
+ window.setTimeout(function () {
+ _.styles(target, {
+ height: 0
+ });
+ }, 10);
+ window.setTimeout(function () {
+ target.style.display = 'none';
+ target.style.removeProperty('height');
+ target.style.removeProperty('padding-top');
+ target.style.removeProperty('padding-bottom');
+ target.style.removeProperty('margin-top');
+ target.style.removeProperty('margin-bottom');
+ target.style.removeProperty('overflow');
+ target.style.removeProperty('transition-duration');
+ target.style.removeProperty('transition-property');
+ target.style.paddingTop = 0;
+ target.style.paddingBottom = 0;
+ _.slideAnimating = false;
+ }, duration);
+ },
+ /**
+ * slideDown
+ * @example
+ * NH.slideDown(target, duration);
+ * @param {Element} element
+ * @param {Number} duration
+ */
+ slideDown: function slideDown(target) {
+ var duration = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 500;
+ var _ = this;
+ if (_.slideAnimating) return;
+ _.slideAnimating = true;
+ target.style.removeProperty('display');
+ var display = window.getComputedStyle(target).display;
+ if (display === 'none') display = 'block';
+ target.style.display = display;
+ var height = target.offsetHeight;
+ _.styles(target, {
+ overflow: 'hidden',
+ height: 0,
+ marginTop: 0,
+ marginBottom: 0,
+ boxSizing: 'border-box',
+ transitionProperty: "height, margin, padding",
+ transitionDuration: "".concat(duration, "ms")
+ });
+ target.style.removeProperty('padding-top');
+ target.style.removeProperty('padding-bottom');
+ window.setTimeout(function () {
+ var computed = getComputedStyle(target);
+ var pt = parseFloat(computed.paddingTop);
+ var pb = parseFloat(computed.paddingBottom);
+ var bt = parseFloat(computed.borderTopWidth);
+ var bb = parseFloat(computed.borderBottomWidth);
+ _.styles(target, {
+ height: "".concat(height, "px")
+ });
+ }, 10);
+ window.setTimeout(function () {
+ target.style.removeProperty('margin-top');
+ target.style.removeProperty('margin-bottom');
+ target.style.removeProperty('height');
+ target.style.removeProperty('overflow');
+ target.style.removeProperty('transition-duration');
+ target.style.removeProperty('transition-property');
+ _.slideAnimating = false;
+ }, duration + 10);
+ },
+ /**
+ * toggleSlide
+ * @example
+ * NH.toggleSlide(element)
+ * @param {Element} target
+ * @param {Number} duration
+ * @returns
+ */
+ toggleSlide: function toggleSlide(target) {
+ var duration = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 500;
+ var _ = this;
+ if (window.getComputedStyle(target).display === 'none') {
+ return _.slideDown(target, duration);
+ } else {
+ return _.slideUp(target, duration);
+ }
+ },
+ /**
+ * elementPosition
+ * @param {Element} element
+ * @returns {Object} element 의 위치 / 정보
+ */
+ elementPosition: function elementPosition(element) {
+ var el = element;
+ var absVal = el.getBoundingClientRect();
+ var offset = {
+ windowWidth: window.innerWidth,
+ width: absVal.width,
+ height: absVal.height,
+ top: el.offsetTop,
+ left: el.offsetLeft,
+ absTop: Math.round(absVal.top),
+ absLeft: Math.round(absVal.left),
+ absRight: Math.round(window.innerWidth - (absVal.left + absVal.width))
+ };
+ return offset;
+ },
+ /**
+ * siblings 엘리먼트와 형제찾기
+ * @example
+ * NH.siblings(element)
+ * @param {element} element
+ * @returns elementArray
+ */
+ siblings: function siblings(element) {
+ if (element.tagName === undefined) return;
+ try {
+ var childEl = !!element.parentNode.children ? element.parentNode.children : element.parentNode;
+ var tempArr = [];
+ var i = 0;
+ for (i; i < childEl.length; i++) {
+ if (childEl[i].tagName === 'HTML' || childEl[i].tagName === 'HEAD' || childEl[i].tagName === 'BODY') return;
+ tempArr.push(childEl[i]);
+ }
+ return tempArr.filter(function (el) {
+ return el != element;
+ });
+ } catch (err) {
+ console.log(err);
+ }
+ }
+};
+
+// util end
+
+// IIFE start
+/**
+ * scrollDirection (IIFE: 즉시실행함수)
+ * @description - 스크롤이 끝났을때 방향을 확인하여 customEvent(changeDirection)에서 스크롤 방향 확인
+ * @description - 이동 거리가 5 이상일때 event 발생
+ */
+var scrollDirection = function () {
+ var lastScroll = 0;
+ var detail = {};
+ var scrollEvent;
+ // let lastX = 0;
+
+ window.addEventListener('touchmove', function (e) {
+ var currentScroll = window.scrollY || document.documentElement.scrollTop;
+ // console.log(
+ // 'target: ', e.target,
+ // 'clientX: ', pointer.clientX,
+ // 'screenX: ', pointer.screenX,
+ // )
+
+ if (e.changedTouches.length < 2) {
+ if (currentScroll > lastScroll) {
+ detail = {
+ startScroll: lastScroll,
+ endScroll: currentScroll,
+ distence: currentScroll - lastScroll,
+ direction: 'down'
+ };
+ } else {
+ detail = {
+ startScroll: lastScroll,
+ endScroll: currentScroll,
+ distence: lastScroll - currentScroll,
+ direction: 'up'
+ };
+ }
+ if (detail.distence > 5) {
+ scrollEvent = new CustomEvent('changeDirection', {
+ bubbles: true,
+ detail: _objectSpread({}, detail)
+ });
+ window.dispatchEvent(scrollEvent);
+ }
+ lastScroll = currentScroll <= 0 ? 0 : currentScroll;
+ }
+ });
+}();
+
+// 툴팁 기능
+var tooltipUI = function () {
+ var selector = {
+ item: '[data-tooltip="item"]',
+ button: '[data-tooltip="button"]',
+ content: '[data-tooltip="content"]',
+ label: '[data-tooltip="label"]',
+ close: '[data-tooltip="close"]'
+ };
+ function initTooltip(elem, idx) {
+ var element = {};
+ function open(button) {
+ var info = NH.elementPosition(button);
+ var position = {
+ left: info.width - info.absLeft,
+ right: (info.absRight - info.width + 5) * -1
+ };
+ if (!!element.content.closest('.bottom-popup')) {
+ position.left = info.width - info.absLeft + 10;
+ position.right = (info.absRight - info.width - 10) * -1;
+ }
+ if (!!element.content.closest('.menu-all')) {
+ var elemInfo = NH.elementPosition(element.item);
+
+ position.left = elemInfo.absLeft + info.absLeft * -1;
+ position.right = 'auto';
+ }
+ NH.addClass(element.content, 'is-open');
+ NH.attrs(element.content, {
+ 'tabindex': 0
+ });
+ // console.log(position.left, info)
+ NH.styles(element.content, {
+ 'left': "".concat(position.left, "px"),
+ 'right': "".concat(position.right, "px"),
+ 'z-index': 100
+ });
+
+ if (!!element.content.closest('.menu-all')) {
+ element.content.style.setProperty('--pin', "".concat(info.absLeft - elemInfo.left, "px"));
+ } else {
+ element.content.style.setProperty('--pin', "".concat(info.absLeft - info.width, "px"));
+ }
+ element.content.focus();
+ }
+ function close() {
+ NH.removeClass(element.content, 'is-open');
+ NH.attrs(element.content, {
+ 'tabindex': -1
+ });
+ element.content.removeAttribute('style');
+ element.button.focus();
+ }
+ function set() {
+ var _element$close;
+ element.item = elem;
+ element.button = elem.querySelector(selector.button);
+ element.content = elem.querySelector(selector.content);
+ element.label = elem.querySelector(selector.label);
+ element.close = elem.querySelector(selector.close);
+ element.counter = idx;
+ element.state = element.content.classList.contains('is-open');
+ NH.attrs(element.label, {
+ 'aria-hidden': true
+ });
+ NH.attrs(element.button, {
+ id: NH.UID('tooltipLabel', idx)
+ });
+ element.button.querySelector('.ir').innerHTML = element.label.textContent;
+ NH.attrs(element.content, {
+ 'role': 'tooltip',
+ 'aria-labelledby': NH.UID('tooltipLabel', idx),
+ 'tabindex': 0
+ });
+ element.button.addEventListener('click', function (event) {
+ var button = event.target;
+ element.state = element.content.classList.contains('is-open');
+ _.forEach(document.querySelectorAll(selector.content), function (elem) {
+ NH.removeClass(elem, 'is-open');
+ elem.removeAttribute('style');
+ });
+ if (element.state) {
+ close();
+ } else {
+ open(button);
+ }
+ });
+ (_element$close = element.close) === null || _element$close === void 0 || _element$close.addEventListener('click', function () {
+ close();
+ });
+ }
+ set(elem);
+ }
+ _.forEach(document.querySelectorAll(selector.item), function (elem, idx) {
+ new initTooltip(elem, idx);
+ });
+ document.addEventListener('click', function (e) {
+ var target = e.target;
+ var tooltipElem = document.querySelectorAll('.tooltip .is-open').length;
+ // console.log(!!target.closest('.tooltip'), tooltipElem)
+ if (!target.closest('.tooltip') && tooltipElem) {
+ NH.removeClass(document.querySelector('.tooltip .is-open'), 'is-open');
+ }
+ });
+}();
+
+// input 관련 기능
+var input = function () {
+ var selector = {
+ text: '[data-element="inputText"]',
+ clear: '[data-element="clear"]'
+ };
+ function initInput(el) {
+ var element = {};
+ function set() {
+ var _element$clear;
+
+ if (el.getAttribute('is-init')) return;
+ element.wrap = el;
+ element.input = el.querySelector('input');
+ element.clear = el.querySelector(selector.clear);
+
+ NH.attrs(element.wrap, {'is-init': true})
+ element.input.addEventListener('focus', function () {
+ NH.addClass(element.wrap, 'input-focus');
+ });
+ element.input.addEventListener('blur', function () {
+ NH.removeClass(element.wrap, 'input-focus');
+ });
+ element.input.addEventListener('input', function (evt) {
+ var target = evt.target;
+ if (!!target.value.length) {
+ NH.addClass(element.clear, 'is-active');
+ NH.addClass(element.input, 'input-active');
+ } else {
+ NH.removeClass(element.clear, 'is-active');
+ NH.removeClass(element.input, 'input-active');
+ }
+ });
+ (_element$clear = element.clear) === null || _element$clear === void 0 || _element$clear.addEventListener('click', function () {
+ element.input.value = '';
+ element.input.focus();
+ NH.removeClass(element.clear, 'is-active');
+ NH.removeClass(element.input, 'input-active');
+ });
+ }
+ set();
+ }
+ _.forEach(document.querySelectorAll(selector.text), function (elem, idx) {
+ new initInput(elem);
+ });
+
+ return {
+ init() {
+ _.forEach(document.querySelectorAll(selector.text), function (elem) {
+ new initInput(elem);
+ });
+ },
+ active(elId) {
+ var targetEl = document.querySelector(elId);
+ var removeButton = targetEl.closest('div').querySelector(selector.clear);
+
+ NH.addClass(removeButton, 'is-active')
+ },
+
+ inActive(elId) {
+ var targetEl = document.querySelector(elId);
+ var removeButton = targetEl.closest('div').querySelector(selector.clear);
+
+ NH.removeClass(removeButton, 'is-active')
+ },
+ clear(elId) {
+ var targetEl = document.querySelector(elId);
+ var removeButton = targetEl.closest('div').querySelector(selector.clear);
+
+ targetEl.value = '';
+ NH.removeClass(removeButton, 'is-active');
+ targetEl.focus();
+ }
+ }
+
+
+}();
+
+// animate label
+// component/input.html - animateLabel 영역
+var animateLabel = function () {
+ var selector = {
+ label: '[data-element="animateLabel"]',
+ activeClass: 'is-active'
+ };
+ function initInput(el) {
+ var element = {
+ wrap: el,
+ label: el.querySelector('label'),
+ input: el.querySelector('input')
+ };
+ function focusInput(event) {
+ var eventType = event;
+ if (eventType === 'blur' && !element.input.value) {
+ NH.removeClass(element.wrap, selector.activeClass);
+ } else {
+ NH.addClass(element.wrap, selector.activeClass);
+ }
+ }
+ element.input.addEventListener('focus', function (e) {
+ focusInput(e.type);
+ });
+ element.input.addEventListener('blur', function (e) {
+ focusInput(e.type);
+ });
+ }
+ _.forEach(document.querySelectorAll(selector.label), function (elem, idx) {
+ if (elem.querySelectorAll('input').length > 1) {
+ var _input = elem.querySelectorAll('input')[1];
+ _input.addEventListener('input', function () {
+ if (!!_input.value) {
+ _input.classList.add('is-active');
+ } else {
+ _input.classList.remove('is-active');
+ }
+ });
+ }
+ new initInput(elem);
+ });
+}();
+
+// search input 관련 기능
+var search = function () {
+ var selector = {
+ search: '[data-element="inputSearch"]',
+ clear: '[data-element="clear"]'
+ };
+ function initSearch(el) {
+ var element = {};
+ function set() {
+ element.wrap = el;
+ element.input = el.querySelector('input');
+ element.clear = el.querySelector(selector.clear);
+ element.input.addEventListener('focus', function () {
+ NH.addClass(element.wrap, 'input-focus');
+ });
+ element.input.addEventListener('blur', function () {
+ NH.removeClass(element.wrap, 'input-focus');
+ });
+ element.input.addEventListener('input', function (evt) {
+ var target = evt.target;
+ if (target.value.length) {
+ NH.addClass(element.clear, 'is-active');
+ }
+ });
+ element.clear.addEventListener('click', function () {
+ element.input.value = '';
+ element.input.focus();
+ NH.removeClass(element.clear, 'is-active');
+ });
+ }
+ set();
+ }
+ _.forEach(document.querySelectorAll(selector.search), function (elem, idx) {
+ new initSearch(elem);
+ });
+}();
+
+// input 포커스시 버튼 위치 변경 - fixed 버튼이 있을 경우 공통
+var getFocusStatus = function () {
+ var bodyEl = document.querySelector('body'); // body 요소를 선택
+ var inputEL = null; // 현재 포커스된 입력 요소를 저장할 변수
+
+ // 클릭 이벤트 리스너 추가
+ document.addEventListener('click', function (e) {
+ var target = e.target; // 클릭된 요소
+ var statusButton = target.classList.contains('input-text__clear'); // 클릭된 요소가 input-text__clear 클래스를 가지고 있는지 확인
+
+ // 클릭된 요소가 입력 요소이거나 statusButton인 경우
+ if (target.tagName === 'INPUT' || statusButton) {
+ // 입력 요소가 읽기 전용, 비활성화, 버튼, 체크박스, 라디오, 파일 타입인 경우 처리하지 않음
+ if (target.readOnly || target.disabled || target.getAttribute('type') === 'button' || target.getAttribute('type') === 'checkbox' || target.getAttribute('type') === 'radio' || target.getAttribute('type') === 'file') {
+ if (!statusButton) return;
+ }
+
+ inputEL = target; // 현재 포커스된 입력 요소로 설정
+ bodyEl.classList.add('status-focus'); // body에 status-focus 클래스 추가
+
+ // 입력 요소에서 포커스가 벗어날 때 이벤트 리스너 추가
+ inputEL.addEventListener('blur', function () {
+ bodyEl.classList.remove('status-focus'); // body에서 status-focus 클래스 제거
+ inputEL = null; // 현재 포커스된 입력 요소 초기화
+ });
+
+ // 200ms 후에 스크롤 이벤트 리스너 추가
+ setTimeout(function () {
+ document.addEventListener('scroll', function () {
+ bodyEl.classList.remove('status-focus'); // 스크롤 시 body에서 status-focus 클래스 제거
+ inputEL = null; // 현재 포커스된 입력 요소 초기화
+ }, {
+ once: true // 이벤트 리스너는 한 번만 실행
+ });
+ }, 200);
+
+ var initialViewportHeight = window.innerHeight; // 초기 뷰포트 높이 저장
+
+ // 윈도우 리사이즈 이벤트 리스너 추가
+ window.addEventListener('resize', function () {
+ var currentViewportHeight = window.innerHeight; // 현재 뷰포트 높이 저장
+ if (currentViewportHeight > initialViewportHeight) { // 뷰포트 높이가 커지면
+ bodyEl.classList.remove('status-focus'); // body에서 status-focus 클래스 제거
+ }
+ });
+ }
+
+ // 클릭된 요소가 bottom-popup__dim 클래스를 가진 경우
+ if (target.classList.contains('bottom-popup__dim')) {
+ var wrapper = target.closest('.bottom-popup'); // 부모 bottom-popup 요소 선택
+ var targetID = !!wrapper.getAttribute('id') ? wrapper.getAttribute('id') : wrapper.querySelector('.bottom-popup__wrap').getAttribute('id'); // bottom-popup ID 가져오기
+ bottomPopup.close(targetID); // bottomPopup 닫기
+ hideAll.reset(); // 모든 숨기기 초기화
+ }
+ });
+}();
+
+// 인덱스로 강제 탭 전환 - 개발에서 사용할 수 있으므로 삭제 X
+Element.prototype.tabChange = function (idx) {
+ var elem = this;
+ var buttonWrap = elem.querySelector(':scope > [data-js="tab__button-wrap"]');
+ var buttons = buttonWrap.querySelectorAll('[data-js="tab__button"]');
+ buttons[idx].click();
+ buttons[idx].focus();
+};
+
+// 탭의 위치로 스크롤 이동 - 개발에서 사용할 수 있으므로 삭제 X
+Element.prototype.tabPosition = function () {
+ var elem = this;
+ var elemHeader = elem.querySelector(':scope .tab__buttons');
+ var info = NH.elementPosition(elem);
+ var headerInfo = NH.elementPosition(elemHeader);
+ window.scroll({
+ top: info.top - headerInfo.height,
+ behavior: "smooth"
+ });
+};
+// IIFE end
+
+// TAB UI - component\tab.html
+var tabUI = function () {
+ // setAttrAria: 탭 요소에 ARIA 속성을 설정하는 함수
+ function setAttrAria(el, idx) {
+ var tabList = el.querySelector('[data-js="tab__button-wrap"]'); // 탭 버튼 랩퍼 요소를 선택
+ var tabButtons = tabList.querySelectorAll('[data-js="tab__button"]'); // 모든 탭 버튼 요소를 선택
+ var tabContentWrap = el.querySelector('[data-js="tab__content-wrap"]'); // 탭 콘텐츠 랩퍼 요소를 선택
+ var tabPanels = tabContentWrap.querySelectorAll(':scope > [data-js="tab__content"]'); // 모든 탭 콘텐츠 요소를 선택
+
+ // tabList에 'tablist' 역할 속성을 추가
+ NH.attrs(tabList, {
+ 'role': 'tablist'
+ });
+
+ // 각 탭 버튼에 대해 ARIA 속성과 ID를 설정
+ _.forEach(tabButtons, function (el, index) {
+ var tabButton = el;
+ NH.attrs(tabButton, {
+ 'role': 'tab',
+ 'id': "tabList_".concat(idx, "_").concat(index),
+ 'aria-controls': "tabPanel_".concat(idx, "_").concat(index),
+ 'data-index': index
+ });
+ });
+
+ // 각 탭 콘텐츠에 대해 ARIA 속성과 ID를 설정
+ _.forEach(tabPanels, function (el, index) {
+ var tabPanel = el;
+ NH.attrs(tabPanel, {
+ 'role': 'tabpanel',
+ 'id': "tabPanel_".concat(idx, "_").concat(index),
+ 'aria-labelledby': "tabList_".concat(idx, "_").concat(index)
+ });
+ });
+ }
+
+ // tabsAutomatic 클래스: 자동으로 탭을 관리하는 클래스
+ var tabsAutomatic = function () {
+
+ // 생성자 함수: 탭 그룹 요소를 인수로 받아 초기화
+ function tabsAutomatic(groupNode) {
+ _classCallCheck(this, tabsAutomatic);
+
+ this.tabListNode = groupNode; // 탭 리스트 요소
+ this.tabs = this.tabListNode.querySelectorAll('[role=tab]'); // 모든 탭 버튼 요소
+ this.tabPanels = []; // 탭 패널을 저장할 배열
+
+ // 각 탭 버튼에 대해 초기 설정
+ for (var i = 0; i < this.tabs.length; i += 1) {
+ var tab = this.tabs[i];
+ var tabPanels = document.getElementById(tab.getAttribute('aria-controls')); // 해당 탭 버튼이 제어하는 탭 패널 요소를 선택
+ tab.setAttribute('aria-selected', 'false'); // 모든 탭 버튼을 선택되지 않은 상태로 설정
+ this.tabPanels.push(tabPanels); // 탭 패널 배열에 추가
+
+ // 접근성 - is-active 가 들어가 있으면 selected 를 true 로 변경
+ if (tab.classList.contains('is-active')) {
+ tab.setAttribute('aria-selected', 'true');
+ }
+
+ tab.addEventListener('click', this.onClick.bind(this)); // 탭 버튼 클릭 시 이벤트 리스너 추가
+ }
+ }
+
+ // tabsAutomatic 클래스 메서드
+ return _createClass(tabsAutomatic, [
+ {
+ key: "setSelectedTab",
+ value: function setSelectedTab(currentTab, setFocus) {
+ var buttonWrap;
+ var activeIndex = 0;
+ var currentPanel;
+ var isSticky = !!currentTab.closest('[sticky-tab]'); // 'sticky-tab' 속성을 가진 부모 요소가 있는지 확인
+ var header = document.querySelector('#header') || document.querySelector('.full-popup__header'); // 헤더 요소 선택
+
+ if (typeof setFocus !== 'boolean') {
+ setFocus = true;
+ }
+
+ // 각 탭 버튼에 대해 선택된 상태를 설정
+ for (var i = 0; i < this.tabs.length; i += 1) {
+ var tab = this.tabs[i];
+ buttonWrap = tab.parentElement;
+
+ if (currentTab === tab) {
+ tab.setAttribute('aria-selected', 'true');
+ this.tabPanels[i].classList.add('is-active');
+ this.tabs[i].classList.add('is-active');
+ currentPanel = this.tabPanels[i];
+ activeIndex = i;
+ if (setFocus) {
+ tab.focus();
+ }
+ } else {
+ tab.setAttribute('aria-selected', 'false');
+ this.tabPanels[i].classList.remove('is-active');
+ this.tabs[i].classList.remove('is-active');
+ }
+ }
+
+ var tabEl = currentTab.closest('[data-js="tab"]');
+ var buttonHeight = NH.elementPosition(currentTab).height;
+ var isSubTab = !!tabEl.parentNode.closest('[data-js="tab"]');
+ var elTop = isSubTab ? NH.elementPosition(tabEl.parentNode.closest('[data-js="tab"]')).top - buttonHeight : NH.elementPosition(tabEl).top - buttonHeight;
+ var info = NH.elementPosition(currentTab);
+ var tabData = {
+ activeIndex: activeIndex
+ };
+
+ // 'tabChange' 이벤트를 발생시킴
+ NH.eventMaker(tabEl, 'tabChange', tabData);
+
+ // 페이지 스크롤 위치를 조정
+ window.scroll({
+ top: elTop < NH.elementPosition(header).height + 10 ? 0 : elTop,
+ behavior: "smooth"
+ });
+
+ // 버튼 랩퍼의 스크롤 위치를 조정
+ buttonWrap.scroll({
+ left: currentTab.offsetLeft + info.width / 2 - info.windowWidth / 2,
+ behavior: "smooth"
+ });
+
+ // 'sticky' 탭인 경우 스크롤 위치를 조정
+ if (isSticky) {
+ var scrollWrapper = currentTab.closest('.bottom-popup__inner');
+ var gotoPosition = activeIndex === 0 ? NH.elementPosition(currentPanel).top - 20 : NH.elementPosition(currentPanel).top;
+ scrollWrapper.scroll({
+ top: gotoPosition - 100,
+ behavior: "smooth"
+ });
+ }
+ }
+ },
+ {
+ key: "onClick",
+ value: function onClick(e) {
+ this.setSelectedTab(e.currentTarget);
+ }
+ }
+ ]);
+ }();
+
+ // init 함수: 초기화 함수
+ function init() {
+ var tabs = document.querySelectorAll('[data-js="tab"]');
+
+ // 각 탭 요소에 대해 ARIA 속성을 설정하고 tabsAutomatic 인스턴스를 생성
+ _.forEach(tabs, function (el, idx) {
+ setAttrAria(el, idx);
+ var tabLists = el.querySelectorAll('[role=tablist]');
+ for (var i = 0; i < tabLists.length; i++) {
+ new tabsAutomatic(tabLists[i]);
+ }
+ NH.eventMaker(el, 'initTab', {});
+ });
+ }
+
+ // 반환 객체: 공개 메서드 정의
+ return {
+ set: function set() {
+ init();
+ },
+ change: function change(element, idx) {}
+ };
+}();
+
+// accordion
+var accordionUI = function () {
+ // init 함수: 아코디언을 초기화하고 설정 옵션을 처리
+ function init() {
+ var accordion = document.querySelectorAll('[data-js="accordion"]'); // 모든 아코디언 요소를 선택
+ var options = {
+ activeClass: "is-open", // 아코디언이 열릴 때 추가될 클래스
+ open: "multiple" // 여러 아코디언 패널이 동시에 열릴 수 있는지 여부를 설정
+ };
+
+ // 각 아코디언 요소에 대해 초기 설정
+ _.forEach(accordion, function (item, i) {
+ var accordionItem = item.querySelectorAll('.accordion__item'); // 각 아코디언 아이템을 선택
+ var option = item.getAttribute('data-options'); // 아코디언의 옵션을 가져옴
+ var optObj = _objectSpread({}, JSON.parse(option)); // 옵션을 객체로 변환
+
+ // 각 아코디언 아이템에 대해 설정
+ _.forEach(accordionItem, function (item, idx) {
+ var accoButton = item.querySelector('.accordion__button'); // 아코디언 버튼 요소를 선택
+ var accoPanel = item.querySelector('.accordion__content'); // 아코디언 콘텐츠 요소를 선택
+
+ if (!accoButton.classList.contains('is-init')) { // 초기화되지 않은 버튼만 처리
+ // 아코디언 버튼에 속성을 설정
+ NH.attrs(accoButton, {
+ 'id': "accordion_button_".concat(i + 1, "_").concat(idx + 1),
+ 'aria-controls': "accordion_panel_".concat(i + 1, "_").concat(idx + 1)
+ });
+
+ // 아코디언 패널에 속성을 설정
+ NH.attrs(accoPanel, {
+ 'id': "accordion_panel_".concat(i + 1, "_").concat(idx + 1),
+ 'aria-labelledby': "accordion_button_".concat(i + 1, "_").concat(idx + 1)
+ });
+
+ // 아코디언 버튼 클릭 이벤트 리스너 설정
+ accoButton.addEventListener("click", function (e) {
+ var targetItem = e.target.closest('.accordion__item'); // 클릭한 아코디언 아이템
+ var targetPanel = targetItem.querySelector('.accordion__content'); // 클릭한 아코디언 패널
+ var targetButton = targetItem.querySelector('.accordion__button'); // 클릭한 아코디언 버튼
+ var itemIsOpen = document.querySelectorAll(".accordion__item.is-open"); // 열려 있는 아코디언 아이템들
+ var parentItem = targetItem.closest('[data-js="accordion"]'); // 부모 아코디언 요소
+ var isSingle = parentItem.classList.contains('single-guide'); // 단일 열림 모드인지 확인
+ var panelHeight = targetPanel.scrollHeight; // 패널의 전체 높이
+
+ // 아코디언 패널 열기/닫기
+ if (targetPanel.style.maxHeight) {
+ targetPanel.style.maxHeight = null; // 패널을 닫음
+ } else {
+ targetPanel.style.maxHeight = "".concat(panelHeight, "px"); // 패널을 염
+ }
+
+ // 아코디언 아이템 상태 토글
+ if (targetItem.classList.contains(options.activeClass)) {
+ NH.removeClass(targetItem, options.activeClass); // 아이템에서 활성 클래스 제거
+ NH.attrs(targetButton, {
+ 'aria-expanded': false // aria-expanded 속성 false로 설정
+ });
+ } else {
+ NH.addClass(targetItem, options.activeClass); // 아이템에 활성 클래스 추가
+ NH.attrs(targetButton, {
+ 'aria-expanded': true // aria-expanded 속성 true로 설정
+ });
+
+ // 단일 열림 모드인 경우 스크롤 위치 조정
+ if (isSingle) {
+ setTimeout(function () {
+ window.scrollTo({
+ top: NH.elementPosition(parentItem).top,
+ behavior: 'smooth'
+ });
+ }, 300);
+ }
+ }
+
+ // 다른 열려 있는 아코디언 아이템을 닫는 함수
+ function closeFunc() {
+ _.forEach(itemIsOpen, function (item) {
+ var buttonIsOpen = item.querySelector('.accordion__button');
+ item.querySelector('.accordion__content').style.maxHeight = null; // 패널을 닫음
+ NH.removeClass(item, options.activeClass); // 활성 클래스 제거
+ NH.attrs(buttonIsOpen, {
+ 'aria-expanded': false // aria-expanded 속성 false로 설정
+ });
+ });
+ }
+
+ // 옵션이 "multiple"이 아닌 경우, 다른 열려 있는 아코디언 아이템을 닫음
+ if (optObj.open !== options.open) {
+ closeFunc();
+ }
+ });
+
+ // 초기화 완료 표시
+ NH.addClass(accoButton, 'is-init');
+
+ // 2026-04-06 바로쓰기 사용방법 열려있는 상태
+ if(item.parentNode.classList.contains('accordion-direct') && idx === 0) {
+ accoPanel.style.maxHeight = "".concat(accoPanel.scrollHeight, "px");
+ NH.addClass(item, 'is-open');
+ NH.attrs(accoButton, {'aria-expanded' : true});
+ }
+ }
+ });
+ });
+ }
+
+ // 공개 메서드 정의
+ return {
+ set: function set() {
+ init(); // 초기화를 실행
+ },
+ change: function change() {}
+ };
+}();
+
+// 선택된 요소를 제외한 모든 요소에 aria-hidden 추가하는 함수
+var hideAll = function () {
+ var parentElements = [];
+ var hideEl = [];
+ function resetHidden() {
+ _.forEach(hideEl, function (elem) {
+ elem.removeAttribute('aria-hidden');
+ });
+ hideEl = [];
+ parentElements = [];
+ }
+ function setTarget(_x) {
+ return _setTarget.apply(this, arguments);
+ }
+ function _setTarget() {
+ _setTarget = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee(element) {
+ var el, i, siblings;
+ return _regeneratorRuntime().wrap(function _callee$(_context) {
+ while (1) switch (_context.prev = _context.next) {
+ case 0:
+ el = element;
+ i = 1;
+ case 2:
+ if (!el) {
+ _context.next = 8;
+ break;
+ }
+ _context.next = 5;
+ return parentElements.unshift(el);
+ case 5:
+ el = el.parentNode;
+ _context.next = 2;
+ break;
+ case 8:
+ ;
+ for (i; i <= parentElements.length - 1; i++) {
+ siblings = NH.siblings(parentElements[i]);
+ if (!!siblings) {
+ _.forEach(siblings, function (elem) {
+ if (elem.tagName === 'SCRIPT') return;
+ NH.attrs(elem, {
+ 'aria-hidden': true
+ });
+ hideEl.push(elem);
+ });
+ }
+ }
+ ;
+ case 11:
+ case "end":
+ return _context.stop();
+ }
+ }, _callee);
+ }));
+ return _setTarget.apply(this, arguments);
+ }
+ return {
+ // aria-hidden 활성화
+ enable: function enable(element) {
+ setTarget(element);
+ },
+ // aria-hidden 비활성화 - hideAll.enable(element) 이후에 사용
+ reset: function reset() {
+ resetHidden();
+ }
+ };
+}();
+
+// gnb 메뉴
+var gnbMenu = function () {
+ var timing = 500; // 애니메이션 타이밍 설정 (500ms)
+ var selector = { // 요소 선택자
+ wrap: '[data-gnb="wrap"]',
+ buttonOpen: '[data-gnb="open"]',
+ buttonClose: '[data-gnb="close"]',
+ content: '[data-gnb="content"]'
+ };
+ var element = {}; // 요소 저장 객체
+ var positionY = 0; // 현재 Y 위치 저장 (사용되지 않음)
+
+ // GNB 메뉴 열기 함수
+ function open() {
+ NH.attrs(element.buttonOpen, {
+ 'aria-expanded': true // 메뉴 열림 상태로 설정
+ });
+ element.content.style.removeProperty('display'); // 콘텐츠 요소의 display 속성 제거
+ setTimeout(function () {
+ NH.addClass(element.header, 'active-menu'); // 헤더에 active-menu 클래스 추가
+ NH.attrs(element.content, {
+ 'aria-hidden': false // 콘텐츠 요소의 aria-hidden 속성을 false로 설정
+ });
+ hideAll.enable(element.content); // 콘텐츠 요소 숨김 해제
+ }, 100);
+ setTimeout(function () {
+ element.content.querySelector('.menu-all__home').focus(); // 메뉴 내 첫 번째 요소에 포커스
+ }, timing + 100);
+ NH.styles(element.body, {
+ 'overflow': 'hidden', // body 요소의 오버플로우를 숨김
+ 'height': '100%' // body 요소의 높이를 100%로 설정
+ });
+ }
+
+ // GNB 메뉴 닫기 함수
+ function close() {
+ NH.attrs(element.buttonOpen, {
+ 'aria-expanded': false // 메뉴 닫힘 상태로 설정
+ });
+ NH.removeClass(element.header, 'active-menu'); // 헤더에서 active-menu 클래스 제거
+ hideAll.reset(); // 모든 숨김 설정 초기화
+ setTimeout(function () {
+ NH.styles(element.content, {
+ 'display': 'none' // 콘텐츠 요소를 display: none으로 설정
+ });
+ NH.attrs(element.content, {
+ 'aria-hidden': true // 콘텐츠 요소의 aria-hidden 속성을 true로 설정
+ });
+ element.buttonOpen.focus(); // 오픈 버튼에 포커스
+ }, timing);
+
+ element.body.style.removeProperty('overflow'); // body 요소의 overflow 속성 제거
+ }
+
+ // GNB 메뉴 설정 함수
+ function set() {
+ element.body = document.querySelector('body'); // body 요소 선택
+ element.header = document.querySelector('#header'); // 헤더 요소 선택
+ element.wrap = element.header.querySelector(selector.wrap); // GNB 랩 요소 선택
+ element.buttonOpen = element.header.querySelector(selector.buttonOpen); // 오픈 버튼 요소 선택
+ element.buttonClose = element.header.querySelector(selector.buttonClose); // 클로즈 버튼 요소 선택
+ element.content = element.header.querySelector(selector.content); // 콘텐츠 요소 선택
+
+ if (element.buttonOpen) { // 오픈 버튼이 있는 경우
+ NH.attrs(element.buttonOpen, {
+ 'aria-controls': 'menuContent', // aria-controls 속성 설정
+ 'aria-expanded': false // 초기 닫힘 상태로 설정
+ });
+ NH.attrs(element.content, {
+ 'aria-labelledby': 'menuButton', // aria-labelledby 속성 설정
+ 'role': 'menu', // role 속성 설정
+ 'aria-hidden': true // 초기 숨김 상태로 설정
+ });
+ NH.attrs(element.buttonClose, {
+ 'aria-labelled': '닫기', // aria-labelled 속성 설정
+ 'aria-controls': 'menuContent' // aria-controls 속성 설정
+ });
+ NH.styles(element.content, {
+ 'display': 'none' // 초기 display 속성 설정
+ });
+
+ // 오픈 버튼 클릭 이벤트 리스너 추가
+ element.buttonOpen.addEventListener('click', function (event) {
+ var status = element.buttonOpen.getAttribute('aria-expanded'); // 현재 상태 가져오기
+ status === true || status === 'true' ? close() : open(); // 상태에 따라 열기/닫기
+ });
+
+ // 클로즈 버튼 클릭 이벤트 리스너 추가
+ element.buttonClose.addEventListener('click', function (event) {
+ var status = element.buttonOpen.getAttribute('aria-expanded'); // 현재 상태 가져오기
+ status === true || status === 'true' ? close() : open(); // 상태에 따라 열기/닫기
+ });
+ }
+ }
+
+ return {
+ init: function init() {
+ document.querySelector('#header') && set(); // 헤더 요소가 존재하면 set 함수 실행
+ }
+ };
+}();
+
+// alert 팝업
+var alertPopup = function () {
+ // 요소 선택자 설정
+ var selector = {
+ alertOpenButton: '[data-element="alert__open"]', // 경고 팝업 열기 버튼
+ alertPopup: '[data-element="alertPopup"]', // 경고 팝업
+ dim: '[data-element="alert__dim"]', // 팝업 배경
+ closeButton: '[data-element="alert__close"]', // 닫기 버튼
+ confirmButton: '[data-element="alert__confirm"]' // 확인 버튼
+ };
+
+ // body 요소 선택
+ var body = document.querySelector('body');
+
+ // 경고 팝업 초기화 함수
+ function initAlert(element) {
+ var self = {};
+ self.item = element; // 현재 팝업 요소
+ var buttonID = '#' + self.item.querySelector('[id]').getAttribute('id');
+ var buttons = document.querySelectorAll(selector.alertOpenButton);
+ self.closeButton = self.item.querySelector(selector.closeButton);
+ self.confirmButton = self.item.querySelector(selector.confirmButton);
+ self.dim = self.item.querySelector(selector.dim);
+ self.type = self.item.getAttribute('data-type'); // 팝업 타입 (확인 팝업 여부)
+
+ // 열기 버튼 설정
+ _.forEach(buttons, function(button) {
+ if (button.getAttribute('href') === buttonID) {
+ self.OpenButton = button;
+ }
+ });
+
+ // 접근성 속성 설정
+ NH.attrs(self.item, {
+ 'role': 'alertdialog',
+ 'aria-modal': 'true'
+ });
+ NH.attrs(self.dim, {
+ 'aria-hidden': true
+ });
+
+ // 팝업 열기 함수
+ function open(target) {
+ self.dialog = self.item.querySelector(target);
+ var dialogBody = self.dialog.querySelector('.alert__body');
+ if (self.dialog) {
+ var targetAlert = self.dialog.closest('.alert');
+ NH.addClass(targetAlert, 'is-active');
+ NH.attrs(dialogBody, {
+ 'tabindex': 0
+ });
+ NH.addClass(body, 'non-scroll');
+ setTimeout(function () {
+ dialogBody.focus();
+ hideAll.enable(dialogBody);
+ }, 100);
+ }
+ }
+
+ // 팝업 닫기 함수
+ function close(target) {
+ NH.removeClass(target.closest('.alert'), 'is-active');
+ NH.removeClass(body, 'non-scroll');
+ hideAll.reset();
+ try {
+ if (self.OpenButton) {
+ self.OpenButton.focus(); // 원래 열기 버튼으로 포커스 이동
+ }
+ } catch (err) {
+ console.log(err);
+ }
+ }
+
+ // 확인 팝업 열기 함수
+ function confirmAlert(target) {
+ var targetAttr = target.getAttribute('data-id');
+ var targetId = "#".concat(targetAttr);
+ self.dialog = document.querySelector(targetId);
+ if (self.dialog) {
+ var targetAlert = self.dialog.closest('.alert');
+ NH.addClass(targetAlert, 'is-active');
+ NH.addClass(body, 'non-scroll');
+ }
+ }
+
+ // 열기 버튼 이벤트 리스너 추가
+ if (self.OpenButton) {
+ self.OpenButton.addEventListener('click', function (e) {
+ var target = e.target.getAttribute('href');
+ open(target);
+ });
+ }
+
+ // 닫기 버튼 이벤트 리스너 추가
+ if (!!self.closeButton) {
+ self.closeButton.addEventListener('click', function (e) {
+ var target = e.target;
+ close(target);
+ });
+ }
+
+ // 확인 팝업 이벤트 리스너 추가
+ if (self.type === 'confirm' && self.confirmButton) {
+ self.confirmButton.addEventListener('click', function (e) {
+ var target = e.target;
+ confirmAlert(target);
+ close(target);
+ });
+ }
+ }
+
+ // 모든 경고 팝업 초기화
+ function init() {
+ _.forEach(document.querySelectorAll(selector.alertPopup), function (element) {
+ initAlert(element);
+ });
+ }
+
+ // 특정 레이어 닫기 함수
+ function closeLayer(layer) {
+ var target = "#".concat(layer);
+ var targetEl = document.querySelector(target);
+ NH.removeClass(targetEl.closest('.alert'), 'is-active');
+ NH.removeClass(body, 'non-scroll');
+ hideAll.reset();
+ }
+
+ // 특정 레이어 열기 함수
+ function openLayer(targetId) {
+ var target = "#".concat(targetId);
+ var targetEl = document.querySelector(target);
+ if (targetEl) {
+ var targetAlert = targetEl.closest('.alert');
+ var alertBody = targetAlert.querySelector('.alert__body');
+ NH.addClass(targetAlert, 'is-active');
+ hideAll.enable(targetAlert);
+ NH.addClass(body, 'non-scroll');
+ NH.attrs(alertBody, {
+ 'tabindex': 0
+ });
+
+ setTimeout(function () {
+ alertBody.focus();
+ }, 100);
+ }
+ }
+
+ return {
+ set: function set() {
+ init(); // 초기화 함수 호출
+ },
+ close: function close(el) {
+ closeLayer(el); // 레이어 닫기 함수 호출
+ },
+ open: function open(target) {
+ openLayer(target); // 레이어 열기 함수 호출
+ }
+ };
+}();
+
+// 바텀팝업
+var bottomPopup = function () {
+ // 요소 선택자 설정
+ var selector = {
+ OpenButton: '[data-element="bottomPopup-open"]', // 열기 버튼
+ bottomPopup: '[data-element="bottomPopup"]', // 바텀 팝업
+ dim: '[data-element="bottomPopup__dim"]', // 팝업 배경
+ closeButton: '[data-element="bottomPopup__close"]' // 닫기 버튼
+ };
+
+ // body 요소 선택
+ var body = document.querySelector('body');
+
+ // 바텀 팝업 초기화 함수
+ function initBottomPop(element) {
+ var _self$closeButton;
+ var self = {};
+ var openButton = null;
+ self.item = element; // 현재 팝업 요소
+ self.Popup = self.item.querySelector('.bottom-popup__wrap'); // 팝업 래퍼
+ self.OpenButton = document.querySelectorAll(selector.OpenButton); // 모든 열기 버튼
+ self.closeButton = self.item.querySelector(selector.closeButton); // 닫기 버튼
+ self.dim = self.item.querySelector(selector.dim); // 팝업 배경
+
+ // 접근성 속성 설정
+ NH.attrs(self.item, {
+ 'role': 'dialog',
+ 'aria-modal': 'true'
+ });
+ NH.attrs(self.dim, {
+ 'aria-hidden': 'true'
+ });
+
+ // 열기 버튼 접근성 속성 설정
+ _.forEach(self.OpenButton, function (button) {
+ NH.attrs(button, {
+ 'aria-haspopup': 'true',
+ 'aria-expanded': 'false'
+ });
+ });
+
+ // 팝업 열기 함수
+ function open(target, targetId) {
+ self.dialog = self.item.querySelector(targetId); // 열고자 하는 팝업 요소
+ openButton = target; // 현재 열기 버튼 저장
+ if (self.dialog) {
+ var targetPopup = self.dialog.closest('.bottom-popup');
+ NH.attrs(target, {
+ 'aria-expanded': true
+ });
+ NH.addClass(targetPopup, 'is-active'); // 팝업 활성화 클래스 추가
+ NH.addClass(body, 'non-scroll'); // body 스크롤 비활성화
+ setTimeout(function () {
+ NH.addClass(self.dialog, 'slide-up'); // 팝업 슬라이드업 애니메이션 클래스 추가
+ NH.attrs(self.dialog, {
+ 'tabindex': 0
+ });
+ hideAll.enable(self.dialog); // 다른 모든 팝업 숨기기
+ self.dialog.focus();
+ NH.eventMaker(self.dialog, 'open', {}); // 팝업 열기 이벤트 발생
+ }, 150);
+ }
+ }
+
+ // 팝업 닫기 함수
+ function close(target) {
+ var botPop = target.closest('.bottom-popup');
+ NH.removeClass(botPop, 'is-active'); // 팝업 비활성화 클래스 제거
+ NH.removeClass(body, 'non-scroll'); // body 스크롤 활성화
+ if (!!self.dialog) {
+ setTimeout(function () {
+ NH.removeClass(self.dialog, 'slide-up'); // 팝업 슬라이드업 애니메이션 클래스 제거
+ self.dialog.removeAttribute('style');
+ self.dialog.removeAttribute('tabindex');
+ NH.eventMaker(self.dialog, 'close', {}); // 팝업 닫기 이벤트 발생
+ }, 150);
+ } else {
+ setTimeout(function () {
+ var _botPop$querySelector;
+ botPop.querySelector('.slide-up').classList.remove('slide-up');
+ (_botPop$querySelector = botPop.querySelector('.slide-up')) === null || _botPop$querySelector === void 0 || _botPop$querySelector.removeAttribute('tabindex');
+ NH.eventMaker(self.dialog, 'close', {}); // 팝업 닫기 이벤트 발생
+ }, 150);
+ }
+ if (!!openButton) {
+ NH.attrs(openButton, {
+ 'aria-expanded': false,
+ 'tabindex': 0
+ });
+ NH.eventMaker(botPop, 'close', {}); // 팝업 닫기 이벤트 발생
+ openButton.focus();
+ }
+ openButton = null;
+ hideAll.reset(); // 모든 팝업 숨기기 초기화
+ }
+
+ // 열기 버튼 클릭 이벤트 리스너 추가
+ _.forEach(self.OpenButton, function (button) {
+ button.addEventListener('click', function (e) {
+ e.preventDefault();
+ var target = e.target;
+ var targetId = e.target.tagName === 'BUTTON' ? e.target.getAttribute('data-href') : e.target.getAttribute('href');
+ open(target, targetId);
+ });
+ });
+
+ // 닫기 버튼 클릭 이벤트 리스너 추가
+ (_self$closeButton = self.closeButton) === null || _self$closeButton === void 0 || _self$closeButton.addEventListener('click', function (e) {
+ var target = e.target;
+ close(target);
+ });
+ }
+
+ // 특정 레이어 닫기 함수
+ function closeLayer(layer) {
+ var target = "#".concat(layer);
+ var targetEl = document.querySelector(target);
+ var targetPop = targetEl.getAttribute('id') === layer ? targetEl.closest('.bottom-popup') : targetEl;
+
+ NH.removeClass(targetPop, 'is-active'); // 팝업 비활성화 클래스 제거
+ NH.removeClass(body, 'non-scroll'); // body 스크롤 활성화
+ setTimeout(function () {
+ NH.removeClass(targetEl, 'slide-up'); // 팝업 슬라이드업 애니메이션 클래스 제거
+ hideAll.reset(); // 모든 팝업 숨기기 초기화
+ NH.eventMaker(targetEl, 'close', {}); // 팝업 닫기 이벤트 발생
+ }, 150);
+ }
+
+ // 모든 바텀 팝업 초기화
+ function init() {
+ _.forEach(document.querySelectorAll(selector.bottomPopup), function (element) {
+ new initBottomPop(element);
+ });
+ }
+
+ // 특정 레이어 열기 함수
+ function openLayer(targetId) {
+ var target = "#".concat(targetId);
+ var targetEl = document.querySelector(target);
+ if (targetEl) {
+ var targetPopup = targetEl.closest('.bottom-popup');
+ NH.addClass(targetPopup, 'is-active'); // 팝업 활성화 클래스 추가
+ NH.addClass(body, 'non-scroll'); // body 스크롤 비활성화
+ hideAll.enable(targetEl); // 다른 모든 팝업 숨기기
+ setTimeout(function () {
+ NH.addClass(targetEl, 'slide-up'); // 팝업 슬라이드업 애니메이션 클래스 추가
+ NH.attrs(targetEl, {
+ 'tabindex': 0
+ });
+ targetEl.focus();
+ NH.eventMaker(targetEl, 'open', {}); // 팝업 열기 이벤트 발생
+ }, 150);
+ }
+ }
+
+ return {
+ set: function set() {
+ init(); // 초기화 함수 호출
+ },
+ close: function close(el) {
+ closeLayer(el); // 레이어 닫기 함수 호출
+ },
+ open: function open(target) {
+ openLayer(target); // 레이어 열기 함수 호출
+ },
+ change: function change() {}
+ };
+}();
+
+// 바텀시트 - select 형태
+var bottomSheet = function () {
+ var selector = {
+ container: '[data-element="bottomSheet"]',
+ button: '[data-element="bottomSheet__button"]',
+ select: '[data-element="bottomSheet__select"]',
+ content: '[data-element="bottomSheet__content"]',
+ close: '[data-element="bottomSheet__close"]',
+ dim: '[data-element="bottomSheet__dim"]',
+ timer: 500
+ };
+ function set(el) {
+ var element = {};
+ var body = document.querySelector('body');
+ var currentText = '';
+ function initSelectEvent(id) {
+ var wrapId = document.querySelector("#".concat(id));
+ var optionList = wrapId.querySelectorAll('[data-element="bottomSheet__select"]');
+ _.forEach(optionList, function (sel) {
+ sel.addEventListener('click', function (ev) {
+ var target = ev.target;
+ select(target);
+ });
+ });
+ }
+ function open(elelemtId) {
+ var selectWrap = element.button.closest('.select');
+ var selectEl = selectWrap.querySelector('[data-element="bottomSheet__select"]');
+ var optTemplate = '';
+ if (element.hasSelect) {
+ var selectOpts = selectEl.querySelectorAll('option');
+ _.forEach(selectOpts, function (v) {
+ var activeClass = v.selected ? 'is-selected' : '';
+ if (v.value) {
+ optTemplate += "
").concat(v.innerHTML, "");
+ }
+ });
+ element.content.querySelector('.bottom-sheet__content').innerHTML = optTemplate;
+ initSelectEvent(elelemtId);
+ }
+ NH.addClass(element.container, 'is-active');
+ NH.addClass(body, 'non-scroll');
+ NH.attrs(element.content, {
+ 'tabindex': 0
+ });
+ element.content.focus();
+ setTimeout(function () {
+ NH.addClass(element.content, 'is-active');
+ hideAll.enable(element.content);
+ }, 100);
+ }
+ function close() {
+ NH.removeClass(element.content, 'is-active');
+ NH.removeClass(element.container, 'is-active');
+ NH.removeClass(body, 'non-scroll');
+ NH.eventMaker(element.button, 'change', {
+ selected: element.button.textContent
+ });
+ element.button.focus();
+ element.content.removeAttribute('tabindex');
+ hideAll.reset();
+ }
+ function select(target) {
+ var _element$container$qu;
+ currentText = target.textContent;
+ (_element$container$qu = element.container.querySelector('.is-selected')) === null || _element$container$qu === void 0 || _element$container$qu.classList.remove('is-selected');
+ NH.addClass(target, 'is-selected');
+ setTimeout(function () {
+ close();
+ element.button.textContent = currentText;
+ NH.addClass(element.button, 'default-selected');
+ }, 100);
+ if (element.button.closest('.animate-label--fixed')) {
+ element.button.closest('.animate-label--fixed').classList.add('is-active');
+ }
+ if (element.hasSelect) {
+ var dataValue = target.getAttribute('data-value');
+ element.selectEl.value = dataValue;
+ NH.eventMaker(element.selectEl, 'change', {
+ 'selectedValue': dataValue,
+ 'selectedText': target.textContent
+ });
+ }
+ }
+ function init() {
+ var elelemtId = el.getAttribute('id');
+ element.container = el;
+ element.button = document.querySelector("[data-href=\"".concat(elelemtId, "\"]"));
+ element.content = el.querySelector(selector.content);
+ element.select = el.querySelectorAll(selector.select);
+ element.closeButton = el.querySelector(selector.close);
+ element.dim = el.querySelector(selector.dim);
+ var selectWrap = element.button.closest('.select');
+ element.selectEl = selectWrap.querySelector('[data-element="bottomSheet__select"]');
+ element.hasSelect = !!element.selectEl;
+ element.container.style.setProperty('--timer', "".concat(selector.timer, "ms"));
+ NH.attrs(element.container, {
+ 'aria-modal': true
+ });
+ element.button.addEventListener('click', function (ev) {
+ open(elelemtId);
+ });
+ element.closeButton.addEventListener('click', function (ev) {
+ close();
+ });
+ element.dim.addEventListener('click', function (ev) {
+ close();
+ });
+ if (!element.hasSelect) {
+ _.forEach(element.select, function (sel) {
+ sel.addEventListener('click', function (ev) {
+ var target = ev.target;
+ select(target);
+ });
+ });
+ }
+ }
+ init();
+ }
+ function initSheet() {
+ var setEl = document.querySelectorAll(selector.container);
+ _.forEach(setEl, function (el) {
+ set(el);
+ });
+ }
+ return {
+ init: function init() {
+ initSheet();
+ },
+ close: function close(el) {
+ var body = document.querySelector('body');
+ var targetEl = document.querySelector("#".concat(el));
+ var targetInner = targetEl.querySelector('.is-active');
+ NH.removeClass(body, 'non-scroll');
+ NH.removeClass(targetEl, 'is-active');
+ if (targetInner !== null) {
+ NH.removeClass(targetInner, 'is-active');
+ }
+ hideAll.reset();
+ }
+ };
+}();
+
+// 알림함 항목에서 스와이프 하여 삭제하는 기능 - NHM_DW_02_01_01_01
+var touchSwipe = function () {
+ function init() {
+ var swipe = document.querySelectorAll('[data-js="swipe"]');
+ var options = {
+ activeClass: "is-open"
+ };
+ _.forEach(swipe, function (item, i) {
+ var swipeItem = item.querySelectorAll('[data-js="swipe__item"]');
+ _.forEach(swipeItem, function (item, i) {
+ var startPos;
+ var deleteButton = item.nextElementSibling;
+ // console.log(deleteButton);
+
+ //open
+ function open(target, siblings) {
+ // console.log('열림');
+ _.forEach(siblings, function (item, i) {
+ NH.removeClass(item, options.activeClass);
+ });
+ NH.addClass(target, options.activeClass);
+ }
+
+ //close
+ function close(target) {
+ // console.log('닫힘');
+ NH.removeClass(target, options.activeClass);
+ }
+
+ //touchstart Position 값
+ item.addEventListener('touchstart', function (e) {
+ startPos = e.touches[0].clientX;
+ });
+
+ // touchmove 이벤트
+ item.addEventListener("touchmove", function (e) {
+ var target = e.target.closest('[data-js="swipe__item"]');
+ var siblings = document.querySelectorAll('[data-js="swipe__item"].is-open');
+ var endPos = e.changedTouches[0].clientX;
+
+ // console.log(startPos, endPos);
+
+ if (startPos > endPos) {
+ //왼쪽으로 swipe했을 때
+ open(target, siblings);
+ } else {
+ //오른쪽으로 swipe했을 때
+ close(target);
+ }
+ });
+
+ // 삭제 버튼 focus 이벤트
+ deleteButton.addEventListener("focus", function (e) {
+ var targetSwipeItem = e.target.previousElementSibling;
+ var siblings = document.querySelectorAll('[data-js="swipe__item"].is-open');
+ open(targetSwipeItem, siblings);
+ });
+ });
+ });
+ } //init
+
+ return {
+ set: function set() {
+ init();
+ },
+ change: function change() {}
+ };
+}();
+var fullPopup = function () {
+ function openPopup(selector) {
+ var element = document.querySelector(selector);
+ NH.styles(element, {
+ 'display': 'block'
+ });
+ }
+ function closePopup(selector) {
+ var element = document.querySelector(selector);
+ NH.styles(element, {
+ 'display': 'none'
+ });
+ }
+ return {
+ open: function open(selector) {
+ openPopup(selector);
+ },
+ close: function close(selector) {
+ closePopup(selector);
+ }
+ };
+}();
+
+// string 을 태그형태로 변경
+function optimizeHTML(html) {
+ return html.replace(/>\s+/g, ">").replace(/\s+, "<");
+}
+
+/* datepicker(https://mymth.github.io/vanillajs-datepicker/#/) - lib.js 에 포함 */
+// component/input.html 하단에 추가
+var picker = function () {
+ var selector = {
+ element: 'input[data-element="datepicker"]',
+ button: '[data-element="button"]'
+ };
+ var defaultOpt = {
+ language: "ko",
+ format: "yyyy-mm-dd",
+ todayButton: true,
+ showOnFocus: false,
+ daysOfWeekHighlighted: [0],
+ todayButtonMode: 1,
+ maxView: 2,
+ datesDisabled: [],
+ nextArrow: "
\uB2E4\uC74C",
+ prevArrow: "
\uC774\uC804"
+ };
+
+ // 월만 바꿀 경우 사용 - NHM_MY_07_01_01
+ function changeMonthOnly(el, picker) {
+ var currentDate = picker.element.value;
+ var dateArray = currentDate.split('-');
+ el.value = "".concat(dateArray[0], "-").concat(dateArray[1]);
+ }
+
+ // prev 일 경우 aria-hidden 자동 삽입 기능 - datepicker_2.html
+ function hideCells(pickerElement) {
+ var pickerCells = pickerElement.querySelectorAll('.datepicker-cell');
+ _.forEach(pickerCells, function(cell) {
+ if (cell.classList.contains('prev')) {
+ NH.attrs(cell, {'aria-hidden': true})
+ } else {
+ cell.removeAttribute('aria-hidden');
+ }
+ });
+ }
+
+ // 접근성 포커스 갔을때 선택됨 추가
+ function focusCells(pickerElement) {
+ var pickerCells = pickerElement.querySelectorAll('.datepicker-cell');
+
+ _.forEach(pickerCells, function(cell) {
+ if(cell.classList.contains('focused')){
+ updateCellSpanIr(cell)
+ }
+ cell.addEventListener('click', function () {
+ updateCellSpanIr(cell)
+ });
+ });
+ }
+
+ function updateCellSpanIr(cell){
+ var span = '
선택됨';
+ var nodeChilde = cell.parentNode.querySelector('.ir');
+
+ if(nodeChilde){
+ nodeChilde.remove();
+ }
+ cell.insertAdjacentHTML('beforeend',span);
+
+ }
+
+ return {
+ init: function init() {
+ var elements = document.querySelectorAll(selector.element);
+
+ _.forEach(elements, function (element) {
+ if (element.getAttribute('is-init')) return; // 달력 재 적용 방지
+ element.setAttribute('is-init', true); // 달력 재 적용 방지
+
+ var opt = JSON.parse(element.getAttribute('data-options'));
+ var closestEl = element.closest('div');
+ var pickerValEl = closestEl.querySelector('[data-picker-value]');
+
+ var label = element.getAttribute('id');
+ var disabledDates = [];
+ var pickerBtn;
+ if (opt) {
+ disabledDates = !!opt.dateDisable ? opt.dateDisable : [];
+ }
+ if (!element.closest('.date-button')) {
+ pickerBtn = element.closest('div').querySelector('.input-date__picker');
+ NH.attrs(pickerBtn, {
+ 'tabindex': 0,
+ 'role': 'button',
+ 'aria-label': '날짜 선택'
+ });
+ pickerBtn.addEventListener('click', function () {
+ datepicker.picker.show();
+ });
+ }
+ if (!!disabledDates) {
+ _.forEach(disabledDates, function (dd) {
+ defaultOpt.datesDisabled.push(new Date(dd));
+ });
+ }
+
+ var elementOption = _objectSpread(_objectSpread({}, defaultOpt), opt);
+
+ var datepicker = new Datepicker(element, elementOption);
+
+ if (!!pickerValEl) {
+ changeMonthOnly(pickerValEl, datepicker);
+ }
+
+ element.addEventListener('show', function (e) {
+ datepicker.picker.element.querySelector('.datepicker-picker').focus();
+ datepicker.setDate([element.value]);
+ var activePicker = e.target.closest('div').querySelector('.datepicker.active');
+ var selectedDate = null;
+ selectedDate = datepicker.dates;
+ hideAll.enable(activePicker);
+ if (e.target.closest('.input-date')) {
+ NH.styles(e.target.closest('.input-date'), {
+ 'z-index': 9999
+ });
+ }
+ if (e.target.closest('.slide-up')) {
+ e.target.closest('.slide-up').classList.add('is-picker');
+ }
+ hideCells(datepicker.picker.element);
+ activePicker.querySelector('.cancel-btn').addEventListener('click', function (event) {
+ datepicker.setDate(selectedDate);
+ datepicker.hide();
+ NH.eventMaker(element, 'change', {});
+ }, {
+ once: true
+ });
+ //오늘 버튼 클릭시 접근성 선택됨 추가
+ activePicker.querySelector('.today-btn').addEventListener('click', function (event) {
+ focusCells(datepicker.picker.element);
+ });
+ //접근성 선택됨 추가
+ focusCells(datepicker.picker.element);
+ });
+
+ element.addEventListener('changeMonth', function(e) {
+ var pickerElement = datepicker.picker.element;
+
+ hideCells(pickerElement);
+ focusCells(datepicker.picker.element);
+ });
+
+ element.addEventListener('hide', function (e) {
+ hideAll.reset();
+ if (e.target.closest('.input-date')) {
+ var _e$target$closest;
+
+ (_e$target$closest = e.target.closest('.input-date')) === null || _e$target$closest === void 0 || _e$target$closest.style.removeProperty('z-index');
+ }
+ if (e.target.closest('.slide-up')) {
+ e.target.closest('.slide-up').classList.remove('is-picker');
+ }
+ // console.log(datepicker)
+ NH.eventMaker(element, 'change', {
+ date: datepicker.element.value,
+ valueElement: pickerValEl
+ });
+
+ // hideAll.enable(element);
+ // hideAll.reset();
+ setTimeout(function () {
+ if (!!pickerBtn) {
+ pickerBtn.focus();
+ } else {
+ element.focus();
+ }
+ }, 150);
+ });
+ if (pickerValEl) {
+ element.addEventListener('change', function () {
+ changeMonthOnly(pickerValEl, datepicker);
+ });
+ }
+ });
+ }
+ };
+}();
+
+/* toast popup - NHM_MN_P03_toast */
+var toastPopup = function () {
+ var body = document.body;
+ // body 에 마크업 append 하여 노출 후 body 에서 remove
+ return {
+ open: function open(message) {
+ var template = "
");
+ var content = optimizeHTML(template);
+ var element = document.createElement('div');
+ NH.addClass(element, 'toast');
+ element.innerHTML = "
".concat(message, "
");
+ body.appendChild(element);
+ function appendToast() {
+ NH.styles(element, {
+ 'transform': 'none'
+ });
+ setTimeout(function () {
+ NH.styles(element, {
+ 'transform': 'translateY(350%)'
+ });
+ }, 3000);
+ setTimeout(function () {
+ element.remove();
+ }, 3500);
+ }
+ var timer = setTimeout(appendToast, 500);
+ }
+ };
+}();
+
+/* floating scroll - NHM_CS_02_01 스크롤 방향에 따라 1:1 문의 버튼을 show / hide - 현재 사용안함 */
+var floatingScroll = function () {
+ return {
+ init: function init() {
+ var floatingBtn = document.querySelectorAll('[data-js="scroll-floating"]');
+ _.forEach(floatingBtn, function (button) {
+ var lastScroll = 0;
+ window.addEventListener('scroll', function (e) {
+ var currentScroll = window.scrollY;
+ lastScroll < currentScroll ? NH.addClass(button, 'is-show') : NH.removeClass(button, 'is-show');
+ lastScroll = currentScroll;
+ });
+ });
+ }
+ };
+}();
+
+// input value값 있을 때 (가계부 - 카테고리 편집)
+// inputValue 모듈을 정의하고 즉시 실행
+
+var inputValue = function () {
+ // 선택자를 저장하는 객체
+ var selector = {
+ text: '[data-element="inputValue"]', // 입력 필드 요소
+ "delete": '[data-element="delete"]' // 삭제 버튼 요소
+ };
+
+ // 각 입력 필드를 초기화하는 함수
+ function initInput(el) {
+ var element = {}; // 요소들을 저장할 객체
+
+ // 입력 필드를 설정하는 함수
+ function set() {
+ element.wrap = el; // 입력 필드를 감싸는 요소
+ element.input = el.querySelector('input'); // 입력 필드
+ element["delete"] = el.querySelector(selector["delete"]); // 삭제 버튼
+
+ // 입력 필드에 포커스가 생길 때 이벤트 설정
+ element.input.addEventListener('focus', function () {
+ NH.removeClass(element.wrap, 'has-value'); // 입력 필드의 값 클래스 제거
+ NH.addClass(element.wrap, 'focus'); // 포커스 클래스 추가
+ });
+
+ // 입력 필드에서 포커스가 사라질 때 이벤트 설정
+ element.input.addEventListener('blur', function () {
+ NH.removeClass(element.wrap, 'focus'); // 포커스 클래스 제거
+ if (element.input.value.length) { // 입력 필드에 값이 있으면
+ NH.addClass(element.wrap, 'has-value'); // 값 클래스 추가
+ }
+ });
+ }
+
+ // 입력 필드 설정 함수 호출
+ set();
+ }
+
+ // 선택된 모든 입력 필드에 대해 initInput 함수 호출
+ _.forEach(document.querySelectorAll(selector.text), function (elem, idx) {
+ new initInput(elem);
+ });
+}();
+
+// 가계부 floating
+var floatingPopup = function () {
+ // '[data-js="floatingPopup"]' 속성을 가진 요소 선택
+ var floatingWrap = document.querySelector('[data-js="floatingPopup"]');
+
+ // 플로팅 팝업의 초기화 및 이벤트 설정 함수
+ function floating() {
+ var floatingControl = floatingWrap.querySelector('.floating-popup__control'); // 팝업 열기/닫기 컨트롤 버튼
+ var floatingContent = floatingWrap.querySelector('.floating-popup__content'); // 팝업 내용 영역
+ var floatgingButtons = floatingWrap.querySelector('.floating-popup__buttons'); // 팝업 버튼 영역
+ var floatgingDim = floatingWrap.querySelector('.floating-popup__dim'); // 팝업 외부 영역 (딤 처리)
+ var body = document.querySelector('body'); // body 요소
+
+ // 컨트롤 버튼의 타이틀 속성 설정
+ NH.attrs(floatingControl, {
+ 'title': '내역 작성 폼 선택 열기'
+ });
+
+ // 버튼 열기 인터랙션 함수
+ function buttonOpen() {
+ setTimeout(function () {
+ NH.addClass(floatgingButtons, 'is-active'); // 버튼에 활성 클래스 추가
+ }, 100);
+ NH.eventMaker(floatgingButtons, 'open', {}); // 'open' 이벤트 발생
+ hideAll.enable(floatgingButtons); // hideAll 모듈을 사용하여 버튼 활성화
+ }
+
+ // 버튼 닫기 인터랙션 함수
+ function buttonClose() {
+ setTimeout(function () {
+ NH.removeClass(floatgingButtons, 'is-active'); // 버튼의 활성 클래스 제거
+ }, 100);
+ NH.eventMaker(floatgingButtons, 'close', {}); // 'close' 이벤트 발생
+ hideAll.reset(); // hideAll 모듈을 사용하여 초기화
+ }
+
+ // 컨트롤 버튼 클릭 이벤트 설정
+ floatingControl.addEventListener('click', function () {
+ var isOpen = floatingWrap.classList.contains('is-active'); // 팝업이 열려 있는지 확인
+ if (!isOpen) {
+ NH.addClass(floatingWrap, 'is-active'); // 팝업에 활성 클래스 추가
+ NH.attrs(floatingControl, {
+ 'title': '내역 작성 폼 선택 닫기'
+ });
+ NH.styles(body, {
+ overflow: 'hidden' // body의 스크롤 숨김
+ });
+ buttonOpen(); // 버튼 열기 인터랙션 호출
+ floatingContent.setAttribute('tabindex', 0); // 팝업 내용에 tabindex 설정
+ floatingContent.focus(); // 팝업 내용에 포커스 설정
+ } else {
+ NH.removeClass(floatingWrap, 'is-active'); // 팝업의 활성 클래스 제거
+ NH.attrs(floatingControl, {
+ 'title': '내역 작성 폼 선택 열기'
+ });
+ body.style.removeProperty('overflow'); // body의 스크롤 속성 제거
+ buttonClose(); // 버튼 닫기 인터랙션 호출
+ }
+ });
+
+ // 딤 영역 클릭 이벤트 설정
+ floatgingDim.addEventListener('click', function () {
+ NH.removeClass(floatingWrap, 'is-active'); // 팝업의 활성 클래스 제거
+ NH.attrs(floatingControl, {
+ 'title': '내역 작성 폼 선택 열기'
+ });
+ body.style.removeProperty('overflow'); // body의 스크롤 속성 제거
+ buttonClose(); // 버튼 닫기 인터랙션 호출
+ });
+ }
+
+ // 모듈의 공개 메서드
+ return {
+ init: function init() {
+ // 플로팅 팝업 초기화
+ floatingWrap && floating();
+ },
+ close: function close() {
+ var floatingControl = floatingWrap.querySelector('.floating-popup__control');
+ var floatgingButtons = floatingWrap.querySelector('.floating-popup__buttons');
+ var body = document.querySelector('body');
+ setTimeout(function () {
+ NH.removeClass(floatgingButtons, 'is-active'); // 버튼의 활성 클래스 제거
+ }, 100);
+ NH.removeClass(floatingWrap, 'is-active'); // 팝업의 활성 클래스 제거
+ NH.attrs(floatingControl, {
+ 'title': '내역 작성 폼 선택 열기'
+ });
+ body.style.removeProperty('overflow'); // body의 스크롤 속성 제거
+ NH.eventMaker(floatgingButtons, 'close', {}); // 'close' 이벤트 발생
+ hideAll.reset(); // hideAll 모듈을 사용하여 초기화
+ }
+ };
+}();
+
+// 보안키패드가 active 됫을 경우에 호출하는 함수(사용하지 않을 수 있음)
+var activeSecureKeypad = function () {
+ var currentElement = null;
+ return {
+ open: function open(el) {
+ var selector = el;
+ currentElement = document.querySelector(selector);
+ currentElement.querySelector('.bottom-popup__content').style.paddingBottom = '200px';
+ },
+ close: function close() {
+ currentElement.querySelector('.bottom-popup__content').removeAttribute('style');
+ currentElement = null;
+ }
+ };
+}();
+
+// 로티 인터렉션
+var lottieAnimation =function () {
+ var lottieContainer = document.querySelectorAll('.lottie-container');
+ function animation() {
+ _.forEach(lottieContainer, function (element) {
+ var lottieId = element.getAttribute('id');
+ var defaultOpt = {
+ container: document.getElementById(lottieId),
+ renderer: 'svg',
+ loop: false,
+ //반복재생
+ autoplay: true //자동재생
+ };
+ if (lottieId === 'animateMokkoji') {
+ mokkoji(defaultOpt);
+ } else if (lottieId === 'animateClap') {
+ clap(defaultOpt);
+ } else if (lottieId === 'animateLogo') {
+ logo(defaultOpt);
+ } else if (lottieId === 'animateShield') {
+ shield(defaultOpt);
+ } else if (lottieId === 'animatePoint') {
+ point(defaultOpt);
+ } else if (lottieId === 'animateLock') {
+ lock(defaultOpt);
+ } else if (lottieId === 'animateMembership') {
+ membership(defaultOpt);
+ } else if (lottieId === 'animateWelcome') {
+ welcome(defaultOpt);
+ } else if (lottieId === 'animateEvent01') {
+ event01(defaultOpt);
+ } else if (lottieId === 'animateEvent02') {
+ event02(defaultOpt);
+ } else if (lottieId === 'animateCheck') {
+ check(defaultOpt);
+ } else if (lottieId === 'animateExmark') {
+ exmark(defaultOpt);
+ } else if (lottieId === 'pointSuccess') {
+ pointSuccess(defaultOpt);
+ }
+ });
+ }
+ function logo(defaultOpt) {
+ var logo = bodymovin.loadAnimation(_objectSpread(_objectSpread({}, defaultOpt), {}, {
+ path: '../../../nhpot/js/json/logo_nh.json' // 파일 경로
+ }));
+ }
+ function mokkoji(defaultOpt) {
+ var mokkoji = bodymovin.loadAnimation(_objectSpread(_objectSpread({}, defaultOpt), {}, {
+ path: '../../../nhpot/js/json/mokkoji.json'
+ }));
+ }
+ function clap(defaultOpt) {
+ var clap = bodymovin.loadAnimation(_objectSpread(_objectSpread({}, defaultOpt), {}, {
+ path: '../../../nhpot/js/json/clap.json'
+ }));
+ }
+ function shield(defaultOpt) {
+ var shield = bodymovin.loadAnimation(_objectSpread(_objectSpread({}, defaultOpt), {}, {
+ path: '../../../nhpot/js/json/shield.json'
+ }));
+ }
+ function point(defaultOpt) {
+ var point = bodymovin.loadAnimation(_objectSpread(_objectSpread({}, defaultOpt), {}, {
+ path: '../../../nhpot/js/json/point.json'
+ }));
+ }
+ function lock(defaultOpt) {
+ var lock = bodymovin.loadAnimation(_objectSpread(_objectSpread({}, defaultOpt), {}, {
+ path: '../../../nhpot/js/json/lock.json'
+ }));
+ }
+ function membership(defaultOpt) {
+ var membership = bodymovin.loadAnimation(_objectSpread(_objectSpread({}, defaultOpt), {}, {
+ path: '../../../nhpot/js/json/membership.json'
+ }));
+ }
+ function welcome(defaultOpt) {
+ var welcome = bodymovin.loadAnimation(_objectSpread(_objectSpread({}, defaultOpt), {}, {
+ path: '../../../nhpot/js/json/welcome.json'
+ }));
+ }
+ function event01(defaultOpt) {
+ var event01 = bodymovin.loadAnimation(_objectSpread(_objectSpread({}, defaultOpt), {}, {
+ path: '../../../nhpot/js/json/event01.json'
+ }));
+ }
+ function event02(defaultOpt) {
+ var event02 = bodymovin.loadAnimation(_objectSpread(_objectSpread({}, defaultOpt), {}, {
+ path: '../../../nhpot/js/json/event02.json'
+ }));
+ }
+ function check(defaultOpt) {
+ var check = bodymovin.loadAnimation(_objectSpread(_objectSpread({}, defaultOpt), {}, {
+ path: '../../../nhpot/js/json/check.json'
+ }));
+ }
+ function exmark(defaultOpt) {
+ var exmark = bodymovin.loadAnimation(_objectSpread(_objectSpread({}, defaultOpt), {}, {
+ path: '../../../nhpot/js/json/exmark.json'
+ }));
+ }
+ function pointSuccess(defaultOpt) {
+ var pointSuccess = bodymovin.loadAnimation(_objectSpread(_objectSpread({}, defaultOpt), {}, {
+ path: '../../../nhpot/js/json/point_icon.json'
+ }));
+ }
+ return {
+ init: function init() {
+ animation();
+ }
+ };
+}();
+
+// checkboxText 모듈을 정의하고 즉시 실행(약관)
+var checkboxText = function () {
+ var inputCheck = document.querySelectorAll('.checked-link__item .input-check__input');
+ var borderCheck = document.querySelectorAll('.input-check-item--border');
+ function initInput(el) {
+ if (!!el.closest('.NHeventHead')) return;
+ var checkParent = el.parentElement;
+ var checkLink = checkParent.nextElementSibling; //a 링크
+ var label = NH.siblings(el)[0].querySelector('.ir');
+ var irText = checkLink.innerHTML;
+ label.innerHTML = irText;
+ checkLink.innerHTML = '';
+ }
+ _.forEach(inputCheck, function (elem, idx) {
+ new initInput(elem);
+ });
+ _.forEach(borderCheck, function (check) {
+ var checkLink = check.querySelector('a');
+ var label = check.querySelector('.ir');
+ // console.log(checkLink, label);
+ if (checkLink) {
+ var irText = checkLink.innerHTML;
+ label.innerHTML = irText;
+ checkLink.innerHTML = '';
+ }
+ });
+}();
+
+
+/* sticky 되며 탭 활성화 - NHM_MY_02_10_02_01 */
+var stickyTab = (function () {
+ var selector = {
+ wrapper: '[data-js="sticky-tab"]', // 탭을 감싸는 요소
+ buttons: '[data-js="tab__button"]', // 탭 버튼 요소들
+ contents: '[data-js="tab__content"]' // 탭 콘텐츠 요소들
+ };
+ var element = {};
+ var current = 0;
+ var startPos = 0;
+ var startCont = 0;
+
+ // 활성화된 탭을 변경하는 함수
+ function changeActive() {
+ var el = element.buttons[current];
+ var els = NH.siblings(el);
+ var scrolllLeft = NH.elementPosition(el).left;
+
+ els.forEach(function(el) {
+ el.classList.remove('is-active');
+ });
+
+ el.closest('.tab__buttons').scroll({
+ left: scrolllLeft - 20,
+ behavior: "smooth"
+ });
+ el.classList.add('is-active');
+ }
+
+ // 스크롤 이벤트 핸들러
+ function scrollEvent() {
+ var currentScroll = window.scrollY || document.documentElement.scrollTop; // 현재 스크롤 위치
+
+ // 스크롤 위치가 변화하지 않았으면 함수 종료
+ if (currentScroll === startPos) return;
+
+ // 스크롤 방향에 따라 고정 클래스 추가/제거
+ if (currentScroll > startPos) {
+ element.wrapper.classList.add('is-sticky');
+ } else {
+ element.wrapper.classList.remove('is-sticky');
+ }
+
+ // 각 콘텐츠 요소의 위치를 확인하여 현재 탭 인덱스 업데이트
+ _.forEach(element.contents, function (el, i) {
+ if (startCont > currentScroll) {
+ current = 0;
+ } else if (NH.isElementInView(el, false)) {
+ current = i;
+ }
+ });
+
+ startPos = currentScroll; // 스크롤 시작 위치 업데이트
+ changeActive(); // 활성화된 탭 변경
+ }
+
+ // 초기화 함수
+ function _init() {
+ var wrapper = document.querySelector(selector.wrapper); // 래퍼 요소 선택
+
+ // 래퍼 요소가 없으면 함수 종료
+ if (!wrapper) return;
+
+ startPos = wrapper.offsetTop; // 래퍼 요소의 시작 위치
+ element.wrapper = wrapper;
+ element.buttons = wrapper.querySelectorAll(selector.buttons); // 모든 탭 버튼 선택
+ element.contents = wrapper.querySelectorAll(selector.contents); // 모든 콘텐츠 요소 선택
+ if(element.contents.length == 0){
+ return;
+ }
+ startCont = NH.elementPosition(element.contents[0]).top; // 첫 번째 콘텐츠 요소의 시작 위치
+
+ // 스크롤 이벤트 리스너 추가
+ window.addEventListener('scroll', scrollEvent, true);
+
+ // 각 탭 버튼에 클릭 이벤트 리스너 추가
+ _.forEach(element.buttons, function (btn, i) {
+ btn.addEventListener('click', function () {
+ var top = NH.elementPosition(element.contents[i]).top; // 클릭된 콘텐츠 요소의 위치
+
+ // 스크롤 이벤트 리스너 일시 제거
+ window.removeEventListener('scroll', scrollEvent, true);
+
+ current = i; // 현재 탭 인덱스 업데이트
+ changeActive(); // 활성화된 탭 변경
+
+ // 클릭된 콘텐츠로 부드럽게 스크롤
+ window.scrollTo({
+ top: top - 130,
+ left: 0,
+ behavior: 'smooth'
+ });
+
+ // 스크롤 이벤트 리스너를 다시 추가
+ setTimeout(function () {
+ window.addEventListener('scroll', scrollEvent, true);
+ }, 600);
+ });
+ });
+ }
+
+ // 새로 고침 함수
+ function _refresh() {
+ window.removeEventListener('scroll', scrollEvent, true); // 스크롤 이벤트 리스너 제거
+ element = {}; // 요소 객체 초기화
+ _init(); // 다시 초기화
+ }
+
+ // 모듈 공개 메서드
+ return {
+ init: function init() {
+ _init(); // 초기화 메서드
+ },
+ refresh: function refresh() {
+ _refresh(); // 새로 고침 메서드
+ }
+ };
+})();
+
+var isMissionPot = (function () {
+ var isMission = document.querySelector('.mission-pot');
+ if (!!isMission) {
+ stickyTab.init();
+ var isTab = document.querySelector('[data-js="tab__content"]');
+ if (!!isTab) {
+ isTab.addEventListener('tabChange', function () {
+ stickyTab.refresh();
+ });
+ }
+ }
+})();
+
+// 올원뱅크 확인용 임시 js
+var isAllOne = (function (){
+ var url = window.location.href;
+ var param = url.split('?');
+
+ if (param[1] === 'allone') {
+ document.querySelector('body').classList.add('all-one')
+ }
+})();
+
+document.addEventListener('DOMContentLoaded', function () {
+ tabUI.set();
+ accordionUI.set();
+ alertPopup.set();
+ touchSwipe.set();
+ bottomPopup.set();
+ gnbMenu.init();
+ bottomSheet.init();
+ picker.init();
+ floatingScroll.init();
+ floatingPopup.init();
+ lottieAnimation.init();
+});
+
+// 2025-02-05 웹접근성 관련 추가(탭영역)
+const tabButtons = document.querySelectorAll('.tab__button');
+
+tabButtons.forEach(button => {
+
+ button.addEventListener('click', () => {
+ tabButtons.forEach(btn => {
+ btn.classList.remove('is-active');
+ updateIrSpan(btn);
+ });
+ button.classList.add('is-active');
+ updateIrSpan(button);
+ });
+});
+
+function updateIrSpan(button) {
+ const existingSpan = button.querySelector('.ir');
+
+ if (button.classList.contains('is-active')) {
+ if(!existingSpan) {
+ const span = document.createElement('span');
+ span.className = 'ir';
+ span.textContent = '선택됨';
+ button.appendChild(span);
+ }
+ } else {
+ if (existingSpan) {
+ existingSpan.remove();
+ }
+ }
+}
+
+const menuButtons = document.querySelectorAll('.button-sorting--line');
+
+menuButtons.forEach(button => {
+
+ button.addEventListener('click', () => {
+ menuButtons.forEach(btn => {
+ btn.classList.remove('is-active');
+ updateIrSpan(btn);
+ });
+ button.classList.add('is-active');
+ updateIrSpan(button);
+ });
+});
+
+function updateIrSpan(button) {
+ const existingSpan = button.querySelector('.ir');
+
+ if (button.classList.contains('is-active')) {
+ if(!existingSpan) {
+ const span = document.createElement('span');
+ span.className = 'ir';
+ span.textContent = '선택됨';
+ button.appendChild(span);
+ }
+ } else {
+ if (existingSpan) {
+ existingSpan.remove();
+ }
+ }
+}
+
diff --git a/app/src/main/assets/extensions/my_extension/sw.js b/app/src/main/assets/extensions/my_extension/sw.js
new file mode 100644
index 00000000..c7cc9946
--- /dev/null
+++ b/app/src/main/assets/extensions/my_extension/sw.js
@@ -0,0 +1,59 @@
+const CACHE_NAME = 'web-reader-cache-v1';
+const ASSETS_TO_CACHE = [
+ './reader.html',
+ 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js'
+];
+
+// 1. 설치 시 핵심 파일 및 외부 CDN 라이브러리 캐싱
+self.addEventListener('install', (event) => {
+ event.waitUntil(
+ caches.open(CACHE_NAME).then((cache) => {
+ return cache.addAll(ASSETS_TO_CACHE);
+ }).then(() => self.skipWaiting())
+ );
+});
+
+// 2. 구버전 캐시 정리
+self.addEventListener('activate', (event) => {
+ event.waitUntil(
+ caches.keys().then((keys) => {
+ return Promise.all(
+ keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))
+ );
+ }).then(() => self.clients.claim())
+ );
+});
+
+// 3. 네트워크 요청 가로채기: 오프라인일 때 캐시된 파일 반환
+self.addEventListener('fetch', (event) => {
+ const url = new URL(event.request.url);
+
+ // reader.html에 쿼리스트링(?id=book_xxx)이 붙은 경우에도 캐시된 reader.html을 반환하도록 처리
+ if (url.pathname.endsWith('reader.html') || url.pathname.endsWith('/')) {
+ event.respondWith(
+ fetch(event.request).catch(() => {
+ return caches.match('./reader.html');
+ })
+ );
+ return;
+ }
+
+ // 그 외 정적 리소스(JSZip 등) 캐시 우선 반환
+ event.respondWith(
+ caches.match(event.request).then((cachedResponse) => {
+ if (cachedResponse) {
+ return cachedResponse;
+ }
+ return fetch(event.request).then((networkResponse) => {
+ // 성공적으로 받아온 CDN 파일 등은 캐시에 복사해둠
+ if (networkResponse && networkResponse.status === 200 && networkResponse.type === 'cors') {
+ const responseToCache = networkResponse.clone();
+ caches.open(CACHE_NAME).then((cache) => {
+ cache.put(event.request, responseToCache);
+ });
+ }
+ return networkResponse;
+ });
+ })
+ );
+});
\ No newline at end of file
diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/CompletedFilesActivity.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/CompletedFilesActivity.kt
new file mode 100644
index 00000000..52f5a7b0
--- /dev/null
+++ b/app/src/main/kotlin/bums/lunatic/launcher/home/CompletedFilesActivity.kt
@@ -0,0 +1,1276 @@
+package bums.lunatic.launcher.home
+
+import android.content.ActivityNotFoundException
+import android.content.Context
+import android.content.Intent
+import android.graphics.Color
+import android.net.Uri
+import android.os.Bundle
+import android.os.Environment
+import android.text.Editable
+import android.text.TextWatcher
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.inputmethod.InputMethodManager
+import android.webkit.MimeTypeMap
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
+import android.widget.EditText
+import android.widget.ImageView
+import android.widget.Spinner
+import android.widget.TextView
+import android.widget.Toast
+import androidx.activity.OnBackPressedCallback
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.FileProvider
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import bums.lunatic.launcher.R
+import bums.lunatic.launcher.player.DocumentViewerActivity
+import bums.lunatic.launcher.player.ImageViewerActivity
+import bums.lunatic.launcher.utils.FileUtils.readTextWithEncoding
+import com.bumptech.glide.Glide
+import com.google.mlkit.nl.languageid.LanguageIdentification
+import com.google.mlkit.nl.translate.TranslateLanguage
+import com.google.mlkit.nl.translate.Translation
+import com.google.mlkit.nl.translate.TranslatorOptions
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.io.File
+import java.net.URLEncoder
+
+enum class FileFilterType(val label: String) { ALL("전체"), IMAGE("이미지"), VIDEO("영상"), DOCUMENT("문서"), OTHER("기타") }
+enum class FileSortType(val label: String) { LAST_USED("최근 사용"), FREQUENTLY_USED("자주 사용"), NAME("파일명"), DOWNLOAD_DATE("다운로드"), SIZE("용량") }
+enum class FileViewMode { LIST_TEXT, LIST_THUMB, GRID_LARGE, GRID_SMALL }
+
+// 💡 자막 번역 데이터를 담을 데이터 클래스 추가
+data class SubtitleBlock(
+ val startSec: Double,
+ val endSec: Double,
+ val text: String,
+ var translatedText: String? = null
+)
+
+class NaturalOrderComparator : Comparator
{
+ override fun compare(f1: File, f2: File): Int {
+ val splitRegex = Regex("(?<=\\D)(?=\\d)|(?<=\\d)(?=\\D)")
+ val parts1 = f1.name.split(splitRegex)
+ val parts2 = f2.name.split(splitRegex)
+
+ val limit = minOf(parts1.size, parts2.size)
+ for (i in 0 until limit) {
+ val p1 = parts1[i]
+ val p2 = parts2[i]
+ if (p1 == p2) continue
+
+ val isNum1 = p1.all { it.isDigit() }
+ val isNum2 = p2.all { it.isDigit() }
+
+ if (isNum1 && isNum2) {
+ val num1 = p1.trimStart('0')
+ val num2 = p2.trimStart('0')
+ val cmp = if (num1.length != num2.length) num1.length.compareTo(num2.length) else num1.compareTo(num2)
+ if (cmp != 0) return cmp
+ } else {
+ val strCmp = p1.compareTo(p2, ignoreCase = true)
+ if (strCmp != 0) return strCmp
+ }
+ }
+ return parts1.size.compareTo(parts2.size)
+ }
+}
+
+enum class RenameMode(val label: String) {
+ REPLACE("문자열 제거/치환"),
+ TRUNCATE_AFTER("기준 문자열 이후 제거 (앞 남기기)"),
+ TRUNCATE_BEFORE("기준 문자열 이전 제거 (뒤 남기기)"),
+ BATCH_NUMBERING("전체 변경 + 넘버링"),
+ SEQUENTIAL("순차적 직접 변경")
+}
+
+private val extImages = setOf("jpg", "jpeg", "png", "gif", "bmp", "webp")
+private val extVideos = setOf("mp4", "mkv", "avi", "mov", "wmv", "flv", "webm", "ts")
+private val extDocs = setOf("pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "hwp", "srt", "smi", "ass", "vtt")
+
+class CompletedFilesActivity : AppCompatActivity() {
+
+ private lateinit var recyclerView: RecyclerView
+ private lateinit var adapter: CompletedFilesAdapter
+ private lateinit var rootDir: File
+ private lateinit var currentDir: File
+ private var allFiles = listOf()
+
+ private var currentFilter = FileFilterType.ALL
+ private var currentSort = FileSortType.LAST_USED
+ private var currentViewMode = FileViewMode.LIST_THUMB
+ private var isDescending = true
+ private var searchQuery = ""
+ private var isSelectionMode = false
+ private val selectedFiles = mutableSetOf()
+ private lateinit var backPressedCallback: OnBackPressedCallback
+
+ // 외부 재생기 복귀 시 리스트 갱신을 위한 Launcher
+ private val videoPlayerLauncher = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) {
+ Toast.makeText(this, "시청 기록이 갱신되었습니다.", Toast.LENGTH_SHORT).show()
+ loadFiles()
+ }
+
+ private lateinit var geckoWeb: GeckoWeb
+ private var targetVideoForSubtitle: File? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.fragment_completed_files)
+
+ rootDir = File(getExternalFilesDir(null), "completed_torrents")
+ if (!rootDir.exists()) rootDir.mkdirs()
+ currentDir = rootDir
+
+ geckoWeb = findViewById(R.id.geckoWebFragment)
+ geckoWeb.visibility = View.GONE
+
+ backPressedCallback = object : OnBackPressedCallback(false) {
+ override fun handleOnBackPressed() {
+ if (isSelectionMode) toggleSelectionMode(false) else navigateUp()
+ }
+ }
+ onBackPressedDispatcher.addCallback(this, backPressedCallback)
+
+ setupRecyclerView()
+ setupControls()
+ loadFiles()
+ }
+
+ private fun toggleSelectionMode(enable: Boolean) {
+ isSelectionMode = enable
+ if (!enable) selectedFiles.clear()
+ adapter.updateSelection(selectedFiles)
+ findViewById(R.id.layoutSelectionActions)?.visibility = if (enable) View.VISIBLE else View.GONE
+ backPressedCallback.isEnabled = isSelectionMode || currentDir.absolutePath != rootDir.absolutePath
+ }
+
+ private fun setupRecyclerView() {
+ recyclerView = findViewById(R.id.recyclerViewCompletedFiles)
+ updateRecyclerViewLayoutManager()
+
+ adapter = CompletedFilesAdapter(
+ onItemClick = { file ->
+ if (file.name == "..") {
+ if (!isSelectionMode) navigateUp()
+ return@CompletedFilesAdapter
+ }
+
+ if (isSelectionMode) {
+ if (selectedFiles.contains(file)) selectedFiles.remove(file) else selectedFiles.add(file)
+ if (selectedFiles.isEmpty()) toggleSelectionMode(false)
+ else adapter.updateSelection(selectedFiles)
+ } else {
+ if (file.isDirectory) {
+ currentDir = file
+ loadFiles()
+ } else {
+ val intentFlags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
+
+ if (extVideos.contains(file.extension.lowercase())) {
+ val subtitlePath = findSubtitleFile(file.absolutePath)
+ launchExternalPlayer(file, subtitlePath)
+ } else if (extImages.contains(file.extension.lowercase())) {
+ trackFileAccess(file.name)
+ loadFiles()
+ val intent = Intent(this, ImageViewerActivity::class.java).apply {
+ putExtra("IMAGE_PATH", file.absolutePath)
+ flags = intentFlags
+ }
+ startActivity(intent)
+ } else if (extDocs.contains(file.extension.lowercase())) {
+ trackFileAccess(file.name)
+ loadFiles()
+ val intent = Intent(this, DocumentViewerActivity::class.java).apply {
+ putExtra("FILE_PATH", file.absolutePath)
+ flags = intentFlags
+ }
+ startActivity(intent)
+ } else {
+ openPrivateFile(this, file)
+ }
+ }
+ }
+ },
+ onItemLongClick = { file ->
+ if (file.name == "..") return@CompletedFilesAdapter
+
+ if (file.isDirectory && !isSelectionMode && !isProtectedFile(file)) {
+ showFolderDeleteDialog(file)
+ } else if (!isSelectionMode) {
+ showFileContextMenu(file)
+ }
+ },
+ getAccessCount = { fileName -> getAccessCount(fileName) }
+ )
+ adapter.setViewMode(currentViewMode)
+ recyclerView.adapter = adapter
+ }
+
+ private fun findSubtitleFile(videoPath: String): String {
+ val file = File(videoPath)
+ val name = file.nameWithoutExtension
+ val extensions = listOf("translated.srt", "srt", "ass", "smi", "vtt")
+ for (ext in extensions) {
+ val subName = if (ext.contains(".")) "${name}_$ext" else "$name.$ext"
+ val sub = File(file.parent, subName)
+ if (sub.exists()) return sub.absolutePath
+ }
+ return ""
+ }
+
+ private fun showFileContextMenu(file: File) {
+ val isVideo = extVideos.contains(file.extension.lowercase())
+ val options = mutableListOf("선택 모드 시작")
+
+ if (isVideo) options.add("자막 검색 및 다운로드")
+
+ android.app.AlertDialog.Builder(this)
+ .setTitle(file.name)
+ .setItems(options.toTypedArray()) { _, which ->
+ when (options[which]) {
+ "선택 모드 시작" -> {
+ toggleSelectionMode(true)
+ selectedFiles.add(file)
+ adapter.updateSelection(selectedFiles)
+ }
+ "자막 검색 및 다운로드" -> {
+ startSubtitleSearch(file)
+ }
+ }
+ }
+ .show()
+ }
+
+ private fun startSubtitleSearch(videoFile: File) {
+ targetVideoForSubtitle = videoFile
+ val query = videoFile.nameWithoutExtension
+ val searchUrl = "https://www.subtitlecat.com/index.php?search=${URLEncoder.encode(query, "UTF-8")}"
+
+ geckoWeb.loadUrl(searchUrl)
+ Toast.makeText(this, "자막을 검색 중입니다...", Toast.LENGTH_SHORT).show()
+ }
+
+ fun showSubtitleSelectionDialog(subList: List