commit c0fc076d3487becf72f803427a5398fc8e182318 Author: seo Date: Mon Jun 8 02:33:14 2026 +0900 Initial private import diff --git a/api.php b/api.php new file mode 100644 index 0000000..d33a971 --- /dev/null +++ b/api.php @@ -0,0 +1,88 @@ + [ + 'Authorization: Bearer ' . HA_LONG_LIVED_TOKEN, + 'Content-Type: application/json' + ], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 5, + ]); + $res = curl_exec($ch); + $errno = curl_errno($ch); + $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($errno !== 0 || $status < 200 || $status >= 300) { + return null; + } + + $json = json_decode($res, true); + // HA는 상태값을 문자열로 줍니다. 숫자면 (string)"12" 형태. + if (!is_array($json) || !isset($json['state'])) return null; + + // 숫자로 캐스팅 시도 (실패해도 문자열 반환 가능) + $stateRaw = $json['state']; + if (is_numeric($stateRaw)) { + return (int)$stateRaw; + } + return $stateRaw; // 숫자가 아니면 원본 문자열 +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $password = $_POST['password'] ?? ''; + + if ($password === '0010') { + echo json_encode(['result' => 'ok']); + } else { + echo json_encode(['result' => 'fail']); + } + exit; +} + +if ($_SERVER['REQUEST_METHOD'] === 'GET') { + if (isset($_GET['failMessages'])) { + echo json_encode([ + 'failMessages' => [ + '비밀번호가 틀립니다.', + ] + ], JSON_UNESCAPED_UNICODE); + exit; + } + + if (isset($_GET['pokeCount'])) { + // HA 엔티티 상태 읽기 + $count = ha_get_entity_state(POKE_ENTITY_ID); + + if ($count === null) { + http_response_code(502); + echo json_encode([ + 'ok' => false, + 'error' => 'failed_to_fetch_from_ha' + ], JSON_UNESCAPED_UNICODE); + exit; + } + + echo json_encode([ + 'ok' => true, + 'count' => $count + ], JSON_UNESCAPED_UNICODE); + exit; + } + + http_response_code(404); + echo json_encode(['error' => 'not_found']); + exit; +} + +http_response_code(405); +echo json_encode(['error' => 'method_not_allowed']); +?> \ No newline at end of file diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..1aa5904 --- /dev/null +++ b/css/style.css @@ -0,0 +1,106 @@ +.find-container { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100px; + background: #f2f2f7; + border-radius: 16px; + padding: 12px; + user-select: none; + margin-bottom: 10px; +} + +.password-container { + display: block; + margin: 0 auto; + width: fit-content; + height: auto; + background: #f2f2f7; + border-radius: 16px; + padding: 12px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + margin-bottom: 10px; +} + +#findMyShortcut input[type="password"] { + padding: 8px 12px; + font-size: 16px; + border-radius: 16px; + border: 1px solid #ccc; + width: 200px; + text-align: center; + font-weight: 500; + background-color: #fff; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + appearance: none; + outline: none; + -webkit-appearance: none; +} + +#findMyShortcut input[type="password"]:focus { + border-color: #999; + box-shadow: none; +} + +#findMyShortcut button { + padding: 10px 12px; + font-size: 15px; + border-radius: 16px; + background-color: #007aff; + color: white; + border: none; + margin-left: 8px; + font-weight: 500; + transition: background 0.2s ease; +} + +#findMyShortcut button:active { + background-color: #0051c7; +} + +.iconContainer { + display: flex; + flex-direction: column; + align-items: center; + cursor: pointer; + width: 90px; + margin: 0 12px; + border-radius: 16px; + transition: background 0.2s ease; +} + +.iconContainer img { + width: 60px; + height: 60px; + border-radius: 16px; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15); + background: #fff; + transition: opacity 0.2s ease; +} + +.iconContainer:active img { + opacity: 0.5; +} + +.iconLabel { + margin-top: 8px; + font-size: 13px; + color: #000; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + text-align: center; + + display: -webkit-box; + -webkit-line-clamp: 1; /* 1줄만 표시하고 자름 (2줄 자르려면 2로 변경) */ + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +} + +#failMsg { + font-size: 13px; + color: red; + min-height: 18px; + margin-top: 8px; + text-align: center; +} \ No newline at end of file diff --git a/img/R1280x0.jpg b/img/R1280x0.jpg new file mode 100644 index 0000000..ecc206f Binary files /dev/null and b/img/R1280x0.jpg differ diff --git a/img/car.png b/img/car.png new file mode 100644 index 0000000..effef08 Binary files /dev/null and b/img/car.png differ diff --git a/img/find-my.svg b/img/find-my.svg new file mode 100644 index 0000000..5e3b857 --- /dev/null +++ b/img/find-my.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/img/wakeonlan-svgrepo-com.svg b/img/wakeonlan-svgrepo-com.svg new file mode 100644 index 0000000..0897486 --- /dev/null +++ b/img/wakeonlan-svgrepo-com.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..9de94de --- /dev/null +++ b/js/main.js @@ -0,0 +1,317 @@ +const shortcut = document.getElementById('findMyShortcut'); +let failIndex = 0; + +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: () => { + fetch('/custom/wakeonlan/api.php?status=1') + .then(res => res.ok ? res.json() : Promise.reject()) + .then(data => { + + if (!data || data.result !== 'ok') { + showAlert('error'); + return; + } + + if (data.state === 'on') { + showAlert('wakeonlan_already'); + return; + } + + fetch('/custom/wakeonlan/api.php', { method: 'POST' }) + .then(res => res.ok ? res.json() : Promise.reject()) + .then(() => { + showAlert('wakeonlan_request_sent'); + + let attempts = 0; + const interval = setInterval(() => { + attempts++; + fetch('/custom/wakeonlan/api.php?status=1') + .then(res => res.ok ? res.json() : Promise.reject()) + .then(status => { + if (status.state === 'on') { + clearInterval(interval); + showAlert('wakeonlan_success'); + } + else if (attempts >= 12) { + clearInterval(interval); + } + }) + .catch(() => clearInterval(interval)); + }, 5000); + }) + .catch(() => showAlert('error')); + }) + .catch(() => showAlert('error')); + } + }, + { + id: 'poke', + img: '/custom/launcher/img/R1280x0.jpg', + label: '콕 찌르기', + onclick: () => { + showAlert('poke_completed'); + const alertBox = document.querySelector('#poke_completed'); + const content = alertBox.querySelector('.alert-content'); + + content.style.minHeight = 'auto'; + content.style.padding = '15px 20px 10px 20px'; + content.style.justifyContent = 'flex-start'; + + const renderMain = (countText = "조회 중...") => { + content.innerHTML = ` +
+

${countText}

+ + + + + +
+ + +
+

+
+ `; + document.getElementById('btn_poke_cancel').onclick = () => { closeAlert('poke_completed'); }; + document.getElementById('btn_poke_confirm').onclick = handlePoke; + }; + + fetch('/custom/launcher/api.php?pokeCount=0') + .then(res => res.json()) + .then(data => { + const n = Number(data.count) || 0; + document.getElementById('poke_title').innerHTML = `현재까지 ${n}회 찔렸습니다!`; + }) + .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 titleField = document.getElementById('poke_title'); + const statusField = document.getElementById('poke_status'); + const confirmBtn = document.getElementById('btn_poke_confirm'); + + confirmBtn.disabled = true; + statusField.innerText = "전송 중..."; + + const params = new URLSearchParams({ sender, message }); + fetch(`https://ha.chaegeon.com/api/webhook/jczSPJyIX4iG4JTvy9HztFq91MsrIfsUErFh4DLstDcsm7RKL?${params.toString()}`, { + method: 'POST', + mode: 'no-cors' + }); + + fetch(`/custom/launcher/api.php?pokeCount=1&sender=${encodeURIComponent(sender)}`) + .then(res => res.json()) + .then(data => { + const n = Number(data.count); + titleField.innerHTML = `현재까지 ${n}회 찔렸습니다!`; + statusField.innerText = "성공!"; + setTimeout(() => { + confirmBtn.disabled = false; + statusField.innerText = ""; + }, 300); + }) + .catch(() => { + statusField.innerText = "오류 발생"; + confirmBtn.disabled = false; + }); + }; + + renderMain(); + } + }, + { + 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); + }); +} + +function requireAccess(nextAction) { + const access = getCookie('access'); + if (access === '9KxH6236') { + nextAction && nextAction(); + } else { + showPasswordForm(nextAction); + } +} + +function showPasswordForm(onSuccess) { + shortcut.className = 'password-container'; + shortcut.innerHTML = ''; + + const header = document.createElement('div'); + header.id = 'settingsHeader'; + header.style.display = 'flex'; + header.style.alignItems = 'center'; + header.style.justifyContent = 'center'; + header.style.position = 'relative'; + header.style.marginBottom = '15px'; + header.style.padding = '12px'; + header.style.fontSize = '16px'; + header.style.color = '#000'; + header.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; + + const backLink = document.createElement('a'); + backLink.id = 'blogBackLink'; + backLink.innerHTML = ' 목록'; + backLink.style.position = 'absolute'; + backLink.style.left = '0px'; + backLink.style.fontSize = '16px'; + backLink.style.top = '3px'; + backLink.style.color = '#007AFF'; + backLink.style.textDecoration = 'none'; + backLink.style.fontWeight = 'normal'; + backLink.style.cursor = 'pointer'; + backLink.addEventListener('click', () => renderIcons()); + + backLink.querySelector('.backArrow').style.cssText = ` + font-size: 26px; + position: relative; + top: 1px; + margin-right: 1px; +`; + + const title = document.createElement('span'); + title.className = 'headerTitle'; + title.textContent = '비밀번호'; + title.style.fontWeight = 'bold'; + + header.appendChild(backLink); + header.appendChild(title); + shortcut.appendChild(header); + + const input = document.createElement('input'); + input.type = 'password'; + input.placeholder = '비밀번호'; + input.id = 'blog_secret'; + input.inputMode = 'numeric'; + input.pattern = '[0-9]*'; + + const button = document.createElement('button'); + button.id = 'blog_secret_check'; + button.textContent = '확인'; + + const msg = document.createElement('div'); + msg.id = 'failMsg'; + + const inputRow = document.createElement('div'); + inputRow.style.display = 'flex'; + inputRow.style.alignItems = 'center'; + inputRow.appendChild(input); + inputRow.appendChild(button); + + const column = document.createElement('div'); + column.style.display = 'flex'; + column.style.flexDirection = 'column'; + column.style.alignItems = 'center'; + column.appendChild(inputRow); + + shortcut.appendChild(column); + + const submit = async () => { + const password = input.value; + + try { + const messageRes = await fetch('/custom/launcher/api.php?failMessages=1'); + const messagesJson = await messageRes.json(); + const failMessages = messagesJson.failMessages || []; + + const res = await fetch('/custom/launcher/api.php', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ password }) + }); + + const data = await res.json(); + + if (data.result === 'ok') { + document.cookie = 'access=9KxH6236; path=/; max-age=21600; SameSite=Strict' + (location.protocol === 'https:' ? '; Secure' : ''); + if (typeof onSuccess === 'function') { + onSuccess(); + } else { + location.reload(); + } + } else { + column.appendChild(msg); + msg.textContent = failMessages[failIndex] || '비밀번호가 틀렸습니다.'; + failIndex = (failIndex + 1) % Math.max(1, failMessages.length); + input.value = ''; + input.focus(); + } + } catch (_) { + } + }; + + button.addEventListener('click', submit); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') submit(); + }); +} + +function getCookie(name) { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop().split(';').shift(); + return null; +} + +document.addEventListener('DOMContentLoaded', async () => { + 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 = "블로그 - 채건닷컴"; \ No newline at end of file