- 색상 코드 변수화
- 라이트 모드 추가
This commit is contained in:
2026-01-20 16:39:14 +09:00
parent 5db14159d0
commit 55e7c64ece
3 changed files with 987 additions and 191 deletions

169
script.js
View File

@@ -1,60 +1,43 @@
// Focus dot animation
const dot = document.querySelector('.dot-point');
function updateDotState() {
const isActive =
document.visibilityState === 'visible' &&
document.hasFocus();
function updateDotState() {
const isActive =
document.visibilityState === 'visible' &&
document.hasFocus();
dot.classList.toggle('is-active', isActive);
}
dot.classList.toggle('is-active', isActive);
}
document.addEventListener('visibilitychange', updateDotState);
window.addEventListener('focus', updateDotState);
window.addEventListener('blur', updateDotState);
document.addEventListener('visibilitychange', updateDotState);
window.addEventListener('focus', updateDotState);
window.addEventListener('blur', updateDotState);
// 최초 실행
updateDotState();
updateDotState();
// ping
async function checkStatus() {
const cards = document.querySelectorAll(".card");
const results = [];
for (const card of cards) {
const checkTasks = Array.from(cards).map(async (card) => {
const url = card.getAttribute("data-url");
const dot = card.querySelector(".card__decor.card__decor--tr");
// 결과 저장을 위한 약속(Promise) 생성
const checkTask = new Promise((resolve) => {
const img = new Image();
// 5초 타임아웃 설정
const timeoutId = setTimeout(() => {
img.src = "";
resolve({ dot, status: "offline" });
}, 5000);
try {
// mode: 'no-cors'를 사용하면 타 도메인의 리소스 존재 여부만 빠르게 확인 가능
const response = await fetch(`${url}/favicon.ico?t=${Date.now()}`, {
mode: 'no-cors',
signal: AbortSignal.timeout(5000)
});
img.onload = () => {
clearTimeout(timeoutId);
resolve({ dot, status: "online" });
};
return { dot, status: "online" };
} catch (error) {
return { dot, status: "offline" };
}
});
img.onerror = () => {
clearTimeout(timeoutId);
resolve({ dot, status: "offline" }); // 502 에러 시 이쪽으로 빠집니다.
};
const finalStatuses = await Promise.all(checkTasks);
// 캐시 방지를 위해 타임스탬프를 붙여서 파비콘 호출
img.src = `${url}/favicon.ico?t=${new Date().getTime()}`;
});
results.push(checkTask);
await new Promise((resolve) => setTimeout(resolve, 100)); // 0.1초 간격 요청
}
// 모든 결과가 모이면 한꺼번에 반영
const finalStatuses = await Promise.all(results);
requestAnimationFrame(() => {
finalStatuses.forEach(({ dot, status }) => {
dot.classList.remove("status-online", "status-offline");
@@ -66,9 +49,8 @@ async function checkStatus() {
checkStatus();
setInterval(checkStatus, 300000);
// 🔴 테스트 ON
// bolt animation - Test Mode
// const TEST_ONLY_SECOND_CARD = true;
// 🟢 테스트 OFF
const TEST_ONLY_SECOND_CARD = false;
// bolt animation
@@ -101,10 +83,9 @@ function startBoltAction() {
randomBolt.classList.add("is-acting");
// setTimeout 대신 이벤트 리스너 사용
randomBolt.addEventListener('animationend', () => {
randomBolt.classList.remove("is-acting");
}, { once: true }); // 메모리 관리를 위해 한 번만 실행 후 제거
}, { once: true });
}
const nextInterval = Math.random() * 4000 + 3000;
@@ -116,10 +97,8 @@ window.addEventListener("load", () => {
});
window.addEventListener('DOMContentLoaded', () => {
// 1. 상태 체크는 1초 뒤에 여유롭게 시작
setTimeout(checkStatus, 1000);
// 2. 나사 랜덤 각도 설정 (DOM이 준비된 직후 실행)
document.querySelectorAll('.card').forEach((card) => {
card.querySelectorAll('.card__decor').forEach((decor) => {
const angle = Math.floor(Math.random() * 180) - 30;
@@ -128,48 +107,39 @@ window.addEventListener('DOMContentLoaded', () => {
});
});
// 1. 사용할 테마 리스트
// Text Changer
const themes = ['semi-nova', 'nova', 'semi-solaris', 'solaris'];
const STORAGE_KEY = 'selected-theme';
// 2. 초기 로드 시 테마 적용 (기본값: semi-nova)
const savedTheme = localStorage.getItem(STORAGE_KEY) || themes[0];
document.body.classList.add(savedTheme);
window.addEventListener('keydown', (e) => {
// OS별 수정 키 판별 (Mac: Command, Win: Control)
const isMac = navigator.platform.toUpperCase().includes('MAC');
const isModifierPressed = isMac ? e.metaKey : e.ctrlKey;
// 방향키 좌/우 확인
const isLeft = e.code === 'ArrowLeft';
const isRight = e.code === 'ArrowRight';
if (
isModifierPressed &&
isModifierPressed &&
(isLeft || isRight) &&
// 입력창 안에서 커서 이동을 방해하지 않도록 체크
!['INPUT', 'TEXTAREA'].includes(document.activeElement?.tagName) &&
!document.activeElement?.isContentEditable // 에디터 영역 대응
!document.activeElement?.isContentEditable
) {
e.preventDefault();
// 현재 인덱스 찾기
let currentIndex = themes.findIndex(t => document.body.classList.contains(t));
if (currentIndex === -1) currentIndex = 0;
// 3. 좌/우 방향에 따른 인덱스 순환
if (isRight) {
// 오른쪽 화살표: 다음 테마
currentIndex = (currentIndex + 1) % themes.length;
} else if (isLeft) {
// 왼쪽 화살표: 이전 테마
currentIndex = (currentIndex - 1 + themes.length) % themes.length;
}
const nextTheme = themes[currentIndex];
// 4. 클래스 교체 및 저장
themes.forEach(t => document.body.classList.remove(t));
document.body.classList.add(nextTheme);
localStorage.setItem(STORAGE_KEY, nextTheme);
@@ -178,6 +148,7 @@ window.addEventListener('keydown', (e) => {
}
});
// Text Changer - Swipe Gesture
const logo = document.querySelector('header h1');
let startX = 0;
@@ -196,8 +167,6 @@ function applyTheme(index) {
const dot = document.querySelector('.dot-point');
if (dot) {
dot.classList.remove('blink-alert');
// 리플로우 유발 코드 제거 (void dot.offsetWidth;)
// 대신 브라우저의 다음 렌더링 사이클을 이용
requestAnimationFrame(() => {
dot.classList.add('blink-alert');
});
@@ -237,4 +206,82 @@ logo.addEventListener('pointercancel', reset);
function reset() {
isDragging = false;
logo.classList.remove('is-sliding');
}
}
// Theme Toggle Button
const THEME_STORAGE_KEY = 'color-theme';
const themeWrapper = document.querySelector('.theme-toggle-wrapper');
const themeToggleBtn = document.querySelector('.theme-toggle-btn');
const themeOptions = document.querySelectorAll('.theme-option');
function initTheme() {
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY) || 'dark';
applyColorTheme(savedTheme);
updateActiveState(savedTheme);
}
function applyColorTheme(theme) {
if (theme === 'light') {
document.documentElement.classList.add('light-mode');
} else {
document.documentElement.classList.remove('light-mode');
}
localStorage.setItem(THEME_STORAGE_KEY, theme);
}
function updateActiveState(theme) {
themeOptions.forEach(option => {
if (option.dataset.theme === theme) {
option.classList.add('is-active');
} else {
option.classList.remove('is-active');
}
});
}
themeToggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
themeWrapper.classList.toggle('is-open');
});
themeOptions.forEach(option => {
option.addEventListener('click', (e) => {
e.stopPropagation();
const selectedTheme = option.dataset.theme;
applyColorTheme(selectedTheme);
updateActiveState(selectedTheme);
themeWrapper.classList.remove('is-open');
});
});
document.addEventListener('click', () => {
themeWrapper.classList.remove('is-open');
});
initTheme();
// Theme toggle - Keyboard Shortcut
window.addEventListener('keydown', (e) => {
const isMac = navigator.platform.toUpperCase().includes('MAC');
const isModifierPressed = isMac ? e.metaKey : e.ctrlKey;
const isUp = e.code === 'ArrowUp';
const isDown = e.code === 'ArrowDown';
if (
isModifierPressed &&
(isUp || isDown) &&
!['INPUT', 'TEXTAREA'].includes(document.activeElement?.tagName) &&
!document.activeElement?.isContentEditable
) {
e.preventDefault();
const currentTheme = localStorage.getItem(THEME_STORAGE_KEY) || 'dark';
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
applyColorTheme(newTheme);
updateActiveState(newTheme);
console.log(`Color Theme: ${newTheme}`);
}
});