[260115]
- 네트워크 종속 항목 트리 - 효율적인 캐쉬 수명 사용 - 브라우저 오류가 콘솔에 로그되는 현상 제거 - 메타 설명 추가 - robots.txt 추가
This commit is contained in:
23
index.html
23
index.html
@@ -4,9 +4,18 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<title>SORI STUDIO</title>
|
||||
<style></style>
|
||||
<link rel="stylesheet" href="style.css?v=1.0.1" />
|
||||
<!-- Preconnect and DNS Prefetch -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.css">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Anton&family=Orbitron:wght@800&family=Staatliches&family=Tenor+Sans&display=swap">
|
||||
<link rel="preload" href="./font/NovaBySoristudio-Regular.otf" as="font" type="font/otf" crossorigin>
|
||||
<link rel="preload" href="./font/Solaris-3-Script.otf" as="font" type="font/otf" crossorigin>
|
||||
<link rel="preconnect" href="https://cdn.jsdelivr.net">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="dns-prefetch" href="https://npm.sori.studio">
|
||||
<link rel="dns-prefetch" href="https://git.sori.studio">
|
||||
<title>sori.studio</title>
|
||||
<meta name="description" content="소리스튜디오: 인프라 상태 모니터링 및 개인용 웹 서비스 대시보드입니다.">
|
||||
</head>
|
||||
|
||||
<body class="semi-nova m-0 p-0 flex flex-column align-center keep-all">
|
||||
@@ -14,6 +23,7 @@
|
||||
<h1 class="header-title m-0 p-0">sori<span class="dot-point">.</span>studio</h1>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="category-section mx-auto pt-5 px-0 pb-10">
|
||||
|
||||
<h2 class="section-title flex align-center h-6 pl-1 mb-6">Core Infrastructure</h2>
|
||||
@@ -135,8 +145,11 @@
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</main>
|
||||
<footer class="text-center p-4">
|
||||
<p>© 2025 sori.studio</p>
|
||||
</footer>
|
||||
<script src="script.js?v=1.0.1" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
2
robots.txt
Normal file
2
robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
101
script.js
101
script.js
@@ -19,28 +19,47 @@ const dot = document.querySelector('.dot-point');
|
||||
// ping
|
||||
async function checkStatus() {
|
||||
const cards = document.querySelectorAll(".card");
|
||||
const results = [];
|
||||
|
||||
cards.forEach(async (card) => {
|
||||
for (const card of cards) {
|
||||
const url = card.getAttribute("data-url");
|
||||
const dot = card.querySelector(".card__decor.card__decor--tr");
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
||||
// 결과 저장을 위한 약속(Promise) 생성
|
||||
const checkTask = new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
// 5초 타임아웃 설정
|
||||
const timeoutId = setTimeout(() => {
|
||||
img.src = "";
|
||||
resolve({ dot, status: "offline" });
|
||||
}, 5000);
|
||||
|
||||
await fetch(url, {
|
||||
mode: "no-cors",
|
||||
cache: "no-cache",
|
||||
signal: controller.signal,
|
||||
img.onload = () => {
|
||||
clearTimeout(timeoutId);
|
||||
resolve({ dot, status: "online" });
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
clearTimeout(timeoutId);
|
||||
resolve({ dot, status: "offline" }); // 502 에러 시 이쪽으로 빠집니다.
|
||||
};
|
||||
|
||||
// 캐시 방지를 위해 타임스탬프를 붙여서 파비콘 호출
|
||||
img.src = `${url}/favicon.ico?t=${new Date().getTime()}`;
|
||||
});
|
||||
|
||||
dot.classList.remove("status-offline");
|
||||
dot.classList.add("status-online");
|
||||
clearTimeout(timeoutId);
|
||||
} catch (e) {
|
||||
dot.classList.remove("status-online");
|
||||
dot.classList.add("status-offline");
|
||||
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");
|
||||
dot.classList.add(status === "online" ? "status-online" : "status-offline");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,20 +76,18 @@ function startBoltAction() {
|
||||
let allBolts = [];
|
||||
|
||||
if (TEST_ONLY_SECOND_CARD) {
|
||||
// 🔧 2번째 카드만 선택 (0-based index)
|
||||
const secondCard = document.querySelectorAll(".card")[1];
|
||||
if (!secondCard) return;
|
||||
|
||||
allBolts = secondCard.querySelectorAll(".card__decor");
|
||||
} else {
|
||||
// 🔹 전체 카드 대상
|
||||
allBolts = document.querySelectorAll(".card__decor");
|
||||
}
|
||||
|
||||
if (allBolts.length === 0) return;
|
||||
|
||||
const availableBolts = Array.from(allBolts).filter(
|
||||
(bolt) => !bolt.classList.contains("card__decor-acting")
|
||||
(bolt) => !bolt.classList.contains("is-acting")
|
||||
);
|
||||
|
||||
if (availableBolts.length > 0) {
|
||||
@@ -82,13 +99,12 @@ function startBoltAction() {
|
||||
|
||||
randomBolt.style.setProperty("--r", baseRot);
|
||||
|
||||
const BOLT_ANIMATION_DURATION = 16000; // CSS와 반드시 일치
|
||||
randomBolt.classList.add("is-acting");
|
||||
|
||||
randomBolt.classList.add("card__decor-acting");
|
||||
|
||||
setTimeout(() => {
|
||||
randomBolt.classList.remove("card__decor-acting");
|
||||
}, BOLT_ANIMATION_DURATION);
|
||||
// setTimeout 대신 이벤트 리스너 사용
|
||||
randomBolt.addEventListener('animationend', () => {
|
||||
randomBolt.classList.remove("is-acting");
|
||||
}, { once: true }); // 메모리 관리를 위해 한 번만 실행 후 제거
|
||||
}
|
||||
|
||||
const nextInterval = Math.random() * 4000 + 3000;
|
||||
@@ -99,16 +115,18 @@ window.addEventListener("load", () => {
|
||||
setTimeout(startBoltAction, 2000);
|
||||
});
|
||||
|
||||
// 나사 랜덤 각도 부연`
|
||||
document.querySelectorAll('.card').forEach((card, cardIndex) => {
|
||||
card.querySelectorAll('.card__decor').forEach((decor, decorIndex) => {
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
// 1. 상태 체크는 1초 뒤에 여유롭게 시작
|
||||
setTimeout(checkStatus, 1000);
|
||||
|
||||
// -30 ~ 150 정도가 나사 느낌 제일 안정적
|
||||
// 2. 나사 랜덤 각도 설정 (DOM이 준비된 직후 실행)
|
||||
document.querySelectorAll('.card').forEach((card) => {
|
||||
card.querySelectorAll('.card__decor').forEach((decor) => {
|
||||
const angle = Math.floor(Math.random() * 180) - 30;
|
||||
|
||||
decor.style.setProperty('--r', `${angle}deg`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 1. 사용할 테마 리스트
|
||||
const themes = ['semi-nova', 'nova', 'semi-solaris', 'solaris'];
|
||||
@@ -161,12 +179,10 @@ window.addEventListener('keydown', (e) => {
|
||||
});
|
||||
|
||||
const logo = document.querySelector('header h1');
|
||||
// const themes = ['semi-nova', 'nova', 'semi-solaris', 'solaris'];
|
||||
// const STORAGE_KEY = 'selected-theme';
|
||||
|
||||
let startX = 0;
|
||||
let isDragging = false;
|
||||
const SWIPE_THRESHOLD = 40; // px
|
||||
const SWIPE_THRESHOLD = 40;
|
||||
|
||||
function getCurrentThemeIndex() {
|
||||
return themes.findIndex(t => document.body.classList.contains(t));
|
||||
@@ -178,11 +194,13 @@ function applyTheme(index) {
|
||||
localStorage.setItem(STORAGE_KEY, themes[index]);
|
||||
|
||||
const dot = document.querySelector('.dot-point');
|
||||
|
||||
if (dot) {
|
||||
dot.classList.remove('blink-alert');
|
||||
void dot.offsetWidth; // 애니메이션 리셋
|
||||
// 리플로우 유발 코드 제거 (void dot.offsetWidth;)
|
||||
// 대신 브라우저의 다음 렌더링 사이클을 이용
|
||||
requestAnimationFrame(() => {
|
||||
dot.classList.add('blink-alert');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,10 +220,8 @@ logo.addEventListener('pointerup', (e) => {
|
||||
if (index === -1) index = 0;
|
||||
|
||||
if (deltaX > 0) {
|
||||
// 👉 오른쪽
|
||||
index = (index + 1) % themes.length;
|
||||
} else {
|
||||
// 👈 왼쪽
|
||||
index = (index - 1 + themes.length) % themes.length;
|
||||
}
|
||||
|
||||
@@ -222,18 +238,3 @@ function reset() {
|
||||
isDragging = false;
|
||||
logo.classList.remove('is-sliding');
|
||||
}
|
||||
// Focus dot animation
|
||||
function updateDotState() {
|
||||
const isActive =
|
||||
document.visibilityState === 'visible' &&
|
||||
document.hasFocus();
|
||||
|
||||
dot.classList.toggle('is-active', isActive);
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', updateDotState);
|
||||
window.addEventListener('focus', updateDotState);
|
||||
window.addEventListener('blur', updateDotState);
|
||||
|
||||
// 최초 실행
|
||||
updateDotState();
|
||||
@@ -1,5 +1,3 @@
|
||||
@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.css");
|
||||
@import url("https://fonts.googleapis.com/css2?family=Anton&family=Orbitron:wght@800&family=Staatliches&family=Tenor+Sans&display=swap");
|
||||
@font-face { font-family: "nova"; src: url("./font/NovaBySoristudio-Regular.otf") format("opentype"); font-weight: normal; font-style: normal; }
|
||||
@font-face { font-family: "solaris"; src: url("./font/Solaris-3-Script.otf") format("opentype"); font-weight: normal; font-style: normal; }
|
||||
|
||||
@@ -152,7 +150,7 @@ body {
|
||||
|
||||
|
||||
|
||||
.card__decor-acting::before { animation: bolt-spin-return 16s cubic-bezier(0.65, 0, 0.35, 1) forwards; }
|
||||
|
||||
|
||||
.solaris .section-title { font-size: 1.25rem; }
|
||||
.nova .section-title { font-size: 1.75rem; }
|
||||
@@ -174,7 +172,8 @@ body {
|
||||
.card__decor--br { bottom: 1rem; right: 1rem; }
|
||||
|
||||
.card__decor::before { content: ""; position: absolute; top: 50%; left: 50%; width: 64%; height: 1.5px; border-radius: 4px; background: #222; transform: translate(-50%, -50%) rotate(var(--r, 0deg)); transition: all 0.3s ease; }
|
||||
.card__decor.card__decor-acting::before { transform: translate(-50%, -50%) rotate(calc(var(--r, 0deg) + 45deg)); background: #000; }
|
||||
.card__decor.is-acting::before { transform: translate(-50%, -50%) rotate(calc(var(--r, 0deg) + 45deg)); background: #000; }
|
||||
.is-acting::before { animation: bolt-spin-return 16s cubic-bezier(0.65, 0, 0.35, 1) forwards; }
|
||||
|
||||
|
||||
/* keyframes */
|
||||
|
||||
Reference in New Issue
Block a user