const shortcut = document.getElementById('findMyShortcut'); let failIndex = 0; const apiUrl = '/custom/launcher/api.php'; const PIN_LENGTH = 4; const userIconItems = [ { id: 'findmydevice', img: '/custom/launcher/img/find-my.svg', label: '나의 찾기', onclick: () => requireAccess(() => location.href = '/findmydevice') }, /* { id: 'wakeonlan', img: '/custom/launcher/img/wakeonlan-svgrepo-com.svg', label: 'PC 켜기', onclick: () => requireAccess(wakeComputer) }, */ { id: 'poke', img: '/custom/launcher/img/R1280x0.jpg', label: '콕 찌르기', onclick: showPokeForm }, { id: 'car', img: '/custom/launcher/img/car.png', label: '니모', onclick: () => requireAccess(() => location.href = 'https://seo.chaegeon.com/car/monitor.php') }, { id: 'git', img: 'https://git.chaegeon.com/assets/img/favicon.svg', label: 'GIT', onclick: () => location.href = 'https://git.chaegeon.com/explore/repos' } ]; const adminIconItems = [ userIconItems[0], userIconItems[1], userIconItems[2], { id: 'project', img: 'https://git.chaegeon.com/assets/img/favicon.svg', label: '프로젝트', onclick: () => location.href = 'https://git.chaegeon.com/explore/repos' }, { id: 'block', img: '/admin/favicon/block/favicon.svg', label: 'Block', onclick: () => requireAccess(() => location.href = 'https://chaegeon.com/admin/index.php') }, { id: 'notice', img: '/admin/favicon/notice/favicon.svg', label: 'Notice', onclick: () => requireAccess(() => location.href = 'https://chaegeon.com/admin/notice.php') }, { id: 'weblog', img: '/weblog/favicon.svg', label: 'Weblog', onclick: () => requireAccess(() => location.href = 'https://chaegeon.com/weblog/index.php') }, { id: 'speedstats', img: '/custom/speedtest/favicon.ico', label: 'Speed Stats', onclick: () => requireAccess(() => location.href = 'https://chaegeon.com/custom/speedtest/results/stats.php') } ]; async function renderIcons() { const session = await getSession(); if (session.loginBlocked) { renderLoginBlocked(session); return; } const isAdmin = !!session.admin; const items = isAdmin ? adminIconItems : userIconItems; shortcut.className = `find-container ${isAdmin ? 'launcher-admin-grid' : ''}`; shortcut.innerHTML = ''; items.forEach(item => { const div = document.createElement('div'); div.className = 'iconContainer'; div.id = item.id; if (item.onclick) div.addEventListener('click', item.onclick); const img = document.createElement('img'); img.src = item.img; img.alt = item.label; const label = document.createElement('div'); label.className = 'iconLabel'; label.textContent = item.label; div.appendChild(img); div.appendChild(label); shortcut.appendChild(div); }); if (!isAdmin) { return; } const manage = document.createElement('button'); manage.type = 'button'; manage.className = 'launcher-manage'; manage.textContent = '관리'; manage.addEventListener('click', () => showPasswordChangeForm()); shortcut.appendChild(manage); } async function hasAdminSession() { const session = await getSession(); return !!session.admin; } async function getSession() { try { return await fetch(`${apiUrl}?session=1`, { credentials: 'same-origin' }).then(res => res.json()); } catch (_) { return { authenticated: false, admin: false }; } } async function requireAccess(nextAction) { try { const session = await getSession(); if (session.loginBlocked) { renderLoginBlocked(session); return; } if (session.admin || session.authenticated) { nextAction && nextAction(); return; } } catch (_) { } showPasswordForm(nextAction); } function renderLoginBlocked(session = {}) { shortcut.className = 'launcher-blocked'; shortcut.innerHTML = ''; const title = document.createElement('strong'); title.textContent = '현재 로그인할 수 없는 상태입니다.'; const message = document.createElement('div'); message.textContent = session.blockedMessage || '잠시 후 다시 시도하세요.'; shortcut.appendChild(title); shortcut.appendChild(message); } function showPasswordForm(onSuccess) { shortcut.className = 'password-container'; shortcut.innerHTML = ''; const title = createFormHeader('비밀번호', renderIcons); const input = createPinInput('blog_secret', '비밀번호'); const msg = document.createElement('div'); msg.id = 'failMsg'; const inputRow = document.createElement('div'); inputRow.className = 'launcher-input-row'; inputRow.appendChild(input); const column = document.createElement('div'); column.className = 'launcher-form-column'; column.appendChild(title); column.appendChild(inputRow); shortcut.appendChild(column); const submit = async () => { const password = onlyDigits(input.value); input.value = password; if (password.length !== PIN_LENGTH) return; try { const messageRes = await fetch(`${apiUrl}?failMessages=1`); const messagesJson = await messageRes.json(); const failMessages = messagesJson.failMessages || []; const res = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify({ action: 'login', password }) }); const data = await res.json(); if (res.ok && data.result === 'ok') { if (typeof onSuccess === 'function') { onSuccess(); } else { renderIcons(); } return; } column.appendChild(msg); msg.textContent = failMessages[failIndex] || '비밀번호가 틀렸습니다.'; failIndex = (failIndex + 1) % Math.max(1, failMessages.length); input.value = ''; input.focus(); } catch (_) { column.appendChild(msg); msg.textContent = '요청을 처리하지 못했습니다.'; } }; const keypad = createPinKeypad([input], submit); column.appendChild(keypad); attachPinInputEvents(input, submit); input.focus(); } function showPasswordChangeForm() { shortcut.className = 'password-container'; shortcut.innerHTML = ''; const title = createFormHeader('암호 변경', renderIcons); const currentInput = createPinInput('current_password', '현재 암호'); const newInput = createPinInput('new_password', '새 암호'); const confirmInput = createPinInput('confirm_password', '새 암호 확인'); const msg = document.createElement('div'); msg.id = 'failMsg'; const status = document.createElement('div'); status.className = 'launcher-pin-status'; status.textContent = '현재 암호 입력'; const column = document.createElement('div'); column.className = 'launcher-form-column'; column.appendChild(title); column.appendChild(status); column.appendChild(currentInput); column.appendChild(newInput); column.appendChild(confirmInput); column.appendChild(msg); shortcut.appendChild(column); const submit = async () => { const currentPassword = onlyDigits(currentInput.value); const newPassword = onlyDigits(newInput.value); const confirmPassword = onlyDigits(confirmInput.value); currentInput.value = currentPassword; newInput.value = newPassword; confirmInput.value = confirmPassword; if ([currentPassword, newPassword, confirmPassword].some(value => value.length !== PIN_LENGTH)) return; if (newPassword !== confirmPassword) { msg.textContent = '새 암호가 일치하지 않습니다.'; newInput.value = ''; confirmInput.value = ''; newInput.focus(); return; } if (!isDigitPin(currentPassword) || !isDigitPin(newPassword)) { msg.textContent = '암호는 숫자만 입력할 수 있습니다.'; return; } try { const res = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify({ action: 'changePassword', currentPassword, newPassword }) }); const data = await res.json(); if (res.ok && data.result === 'ok') { msg.style.color = '#34c759'; msg.textContent = '변경되었습니다.'; setTimeout(renderIcons, 700); return; } msg.style.color = 'red'; msg.textContent = data.message === 'password_digits_only' ? '암호는 숫자 4자리여야 합니다.' : '현재 암호가 틀립니다.'; if (data.message === 'current_password_invalid') { currentInput.value = ''; newInput.value = ''; confirmInput.value = ''; currentInput.focus(); } } catch (_) { msg.style.color = 'red'; msg.textContent = '요청을 처리하지 못했습니다.'; } }; const labels = new Map([ [currentInput, '현재 암호 입력'], [newInput, '새 암호 입력'], [confirmInput, '새 암호 확인 입력'], ]); const syncStatus = () => { const active = document.activeElement && labels.has(document.activeElement) ? document.activeElement : currentInput; status.textContent = labels.get(active); }; const keypad = createPinKeypad([currentInput, newInput, confirmInput], submit, syncStatus); column.insertBefore(keypad, msg); [currentInput, newInput, confirmInput].forEach(input => { attachPinInputEvents(input, submit); input.addEventListener('focus', syncStatus); }); currentInput.focus(); syncStatus(); } async function wakeComputer() { try { const data = await fetch('/custom/wakeonlan/api.php?status=1', { credentials: 'same-origin' }) .then(res => res.ok ? res.json() : Promise.reject()); if (!data || data.result !== 'ok') { launcherAlert('error'); return; } if (data.state === 'on') { launcherAlert('wakeonlan_already'); return; } await fetch('/custom/wakeonlan/api.php', { method: 'POST', credentials: 'same-origin' }) .then(res => res.ok ? res.json() : Promise.reject()); launcherAlert('wakeonlan_request_sent'); let attempts = 0; const interval = setInterval(() => { attempts++; fetch('/custom/wakeonlan/api.php?status=1', { credentials: 'same-origin' }) .then(res => res.ok ? res.json() : Promise.reject()) .then(status => { if (status.state === 'on') { clearInterval(interval); launcherAlert('wakeonlan_success'); } else if (attempts >= 12) { clearInterval(interval); } }) .catch(() => clearInterval(interval)); }, 5000); } catch (_) { launcherAlert('error'); } } function showPokeForm() { launcherAlert('poke_completed'); const alertBox = document.querySelector('#poke_completed'); const content = alertBox?.querySelector('.alert-content'); if (!content) return; content.style.minHeight = 'auto'; content.style.padding = '15px 20px 10px 20px'; content.style.justifyContent = 'flex-start'; const renderMain = (countText = '조회 중...') => { content.innerHTML = `
${countText}