Files
custom-launcher/js/main.js
T

391 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const shortcut = document.getElementById('findMyShortcut');
let failIndex = 0;
const apiUrl = '/custom/launcher/api.php';
const iconItems = [
{
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')
}
];
function renderIcons() {
shortcut.className = 'find-container';
shortcut.innerHTML = '';
iconItems.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);
});
const manage = document.createElement('button');
manage.type = 'button';
manage.className = 'launcher-manage';
manage.textContent = '관리';
manage.addEventListener('click', () => showPasswordChangeForm());
shortcut.appendChild(manage);
}
async function requireAccess(nextAction) {
try {
const session = await fetch(`${apiUrl}?session=1`, { credentials: 'same-origin' }).then(res => res.json());
if (session.authenticated) {
nextAction && nextAction();
return;
}
} catch (_) {
}
showPasswordForm(nextAction);
}
function showPasswordForm(onSuccess) {
shortcut.className = 'password-container';
shortcut.innerHTML = '';
const title = createFormHeader('비밀번호', renderIcons);
const input = createPasswordInput('blog_secret', '비밀번호');
const button = createButton('확인');
const changeButton = createTextButton('암호 변경', () => showPasswordChangeForm());
const msg = document.createElement('div');
msg.id = 'failMsg';
const inputRow = document.createElement('div');
inputRow.className = 'launcher-input-row';
inputRow.appendChild(input);
inputRow.appendChild(button);
const column = document.createElement('div');
column.className = 'launcher-form-column';
column.appendChild(title);
column.appendChild(inputRow);
column.appendChild(changeButton);
shortcut.appendChild(column);
const submit = async () => {
const password = input.value;
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 = '요청을 처리하지 못했습니다.';
}
};
button.addEventListener('click', submit);
input.addEventListener('keydown', e => {
if (e.key === 'Enter') submit();
});
input.focus();
}
function showPasswordChangeForm() {
shortcut.className = 'password-container';
shortcut.innerHTML = '';
const title = createFormHeader('암호 변경', renderIcons);
const currentInput = createPasswordInput('current_password', '현재 암호');
const newInput = createPasswordInput('new_password', '새 암호');
const confirmInput = createPasswordInput('confirm_password', '새 암호 확인');
const button = createButton('변경');
const msg = document.createElement('div');
msg.id = 'failMsg';
const column = document.createElement('div');
column.className = 'launcher-form-column';
column.appendChild(title);
column.appendChild(currentInput);
column.appendChild(newInput);
column.appendChild(confirmInput);
column.appendChild(button);
column.appendChild(msg);
shortcut.appendChild(column);
const submit = async () => {
const currentPassword = currentInput.value;
const newPassword = newInput.value;
const confirmPassword = confirmInput.value;
if (newPassword !== confirmPassword) {
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_too_short' ? '새 암호는 4자 이상이어야 합니다.' : '현재 암호가 틀립니다.';
} catch (_) {
msg.style.color = 'red';
msg.textContent = '요청을 처리하지 못했습니다.';
}
};
button.addEventListener('click', submit);
[currentInput, newInput, confirmInput].forEach(input => {
input.addEventListener('keydown', e => {
if (e.key === 'Enter') submit();
});
});
currentInput.focus();
}
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 = `
<div id="poke_container" style="width:100%; display:flex; flex-direction:column; gap:6px;">
<p id="poke_title" style="text-align:center; font-weight:bold; margin-bottom:4px; color:#007AFF; font-size:1.05em;">${countText}</p>
<input type="text" id="poke_sender" placeholder="보내는 이 (미입력 시 익명)" style="padding:8px 12px; border:1px solid #eee; border-radius:10px; outline:none; font-size:14px; background:#f9f9f9;">
<input type="text" id="poke_message" placeholder="할 말 (선택 사항)" style="padding:8px 12px; border:1px solid #eee; border-radius:10px; outline:none; font-size:14px; background:#f9f9f9;">
<div class="button-custom-container" style="margin-top:6px; gap:6px;">
<button class="button-custom" id="btn_poke_cancel" style="background-color:#eee; color:#666; flex:1; min-width:auto;">닫기</button>
<button class="button-custom" id="btn_poke_confirm" style="flex:2; min-width:auto;">콕 찌르기</button>
</div>
<p id="poke_status" style="text-align:center; font-size:10px; color:#bbb; margin-top:2px; height:1px;"></p>
</div>
`;
document.getElementById('btn_poke_cancel').onclick = () => closeLauncherAlert('poke_completed');
document.getElementById('btn_poke_confirm').onclick = handlePoke;
};
const refreshCount = () => fetch(`${apiUrl}?pokeCount=1`)
.then(res => res.json())
.then(data => {
const n = Number(data.count) || 0;
document.getElementById('poke_title').innerHTML = `현재까지 <span style="color:#ff3b30;">${n}회</span> 찔렸습니다!`;
})
.catch(() => {
document.getElementById('poke_title').innerText = '현재 횟수를 불러올 수 없음';
});
const handlePoke = () => {
const sender = document.getElementById('poke_sender').value.trim() || '익명';
const message = document.getElementById('poke_message').value.trim() || '';
const statusField = document.getElementById('poke_status');
const confirmBtn = document.getElementById('btn_poke_confirm');
confirmBtn.disabled = true;
statusField.innerText = '전송 중...';
fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ action: 'poke', sender, message })
})
.then(res => res.ok ? res.json() : Promise.reject())
.then(() => refreshCount())
.then(() => {
statusField.innerText = '성공!';
setTimeout(() => {
confirmBtn.disabled = false;
statusField.innerText = '';
}, 300);
})
.catch(() => {
statusField.innerText = '오류 발생';
confirmBtn.disabled = false;
});
};
renderMain();
refreshCount();
}
function createFormHeader(text, onBack) {
const header = document.createElement('div');
header.className = 'launcher-form-header';
const backLink = document.createElement('a');
backLink.innerHTML = '<span class="backArrow"></span> 목록';
backLink.className = 'launcher-back';
backLink.addEventListener('click', onBack);
const title = document.createElement('span');
title.className = 'headerTitle';
title.textContent = text;
header.appendChild(backLink);
header.appendChild(title);
return header;
}
function createPasswordInput(id, placeholder) {
const input = document.createElement('input');
input.type = 'password';
input.placeholder = placeholder;
input.id = id;
input.autocomplete = 'current-password';
return input;
}
function createButton(text) {
const button = document.createElement('button');
button.type = 'button';
button.textContent = text;
return button;
}
function createTextButton(text, onClick) {
const button = document.createElement('button');
button.type = 'button';
button.className = 'launcher-text-button';
button.textContent = text;
button.addEventListener('click', onClick);
return button;
}
function launcherAlert(name) {
if (typeof showAlert === 'function') {
showAlert(name);
}
}
function closeLauncherAlert(name) {
if (typeof closeAlert === 'function') {
closeAlert(name);
}
}
document.addEventListener('DOMContentLoaded', () => {
renderIcons();
const findShortcut = document.getElementById('findMyShortcut');
if (findShortcut) {
findShortcut.style.userSelect = 'none';
findShortcut.style.webkitUserSelect = 'none';
findShortcut.style.msUserSelect = 'none';
findShortcut.style.MozUserSelect = 'none';
findShortcut.style.webkitTouchCallout = 'none';
findShortcut.addEventListener('dragstart', e => e.preventDefault());
}
});
if (location.pathname.split('/').filter(Boolean).length === 1) {
document.title = '블로그 - 채건닷컴';
}