- 네트워크 종속 항목 트리
- 효율적인 캐쉬 수명 사용
- 브라우저 오류가 콘솔에 로그되는 현상 제거
- 메타 설명 추가
- robots.txt 추가
This commit is contained in:
2026-01-14 23:58:23 +09:00
parent 0e3368901a
commit 5db14159d0
4 changed files with 181 additions and 166 deletions

View File

@@ -4,9 +4,18 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="style.css" /> <link rel="stylesheet" href="style.css?v=1.0.1" />
<title>SORI STUDIO</title> <!-- Preconnect and DNS Prefetch -->
<style></style> <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> </head>
<body class="semi-nova m-0 p-0 flex flex-column align-center keep-all"> <body class="semi-nova m-0 p-0 flex flex-column align-center keep-all">
@@ -14,129 +23,133 @@
<h1 class="header-title m-0 p-0">sori<span class="dot-point">.</span>studio</h1> <h1 class="header-title m-0 p-0">sori<span class="dot-point">.</span>studio</h1>
</header> </header>
<section class="category-section mx-auto pt-5 px-0 pb-10"> <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> <h2 class="section-title flex align-center h-6 pl-1 mb-6">Core Infrastructure</h2>
<div class="container w-full grid grid-col-3 gap-6"> <div class="container w-full grid grid-col-3 gap-6">
<!-- Nginx Proxy Manager --> <!-- Nginx Proxy Manager -->
<a href="https://npm.sori.studio" class="card" data-url="https://npm.sori.studio"> <a href="https://npm.sori.studio" class="card" data-url="https://npm.sori.studio">
<!-- Decorative Element --> <!-- Decorative Element -->
<div class="card__decor-layer"><span class="card__decor card__decor--tl"></span><span class="card__decor card__decor--tr"></span><span class="card__decor card__decor--bl"></span><span class="card__decor card__decor--br"></span></div> <div class="card__decor-layer"><span class="card__decor card__decor--tl"></span><span class="card__decor card__decor--tr"></span><span class="card__decor card__decor--bl"></span><span class="card__decor card__decor--br"></span></div>
<!-- Card Title --> <!-- Card Title -->
<div class="card__header w-full min-w-0 flex flex-column justify-center align-center overflow-hidden"> <div class="card__header w-full min-w-0 flex flex-column justify-center align-center overflow-hidden">
<div class="card__title max-w-full min-w-0 mt-2 text-center text-ellipsis glyph">Proxy Manager</div> <div class="card__title max-w-full min-w-0 mt-2 text-center text-ellipsis glyph">Proxy Manager</div>
<div class="card__subtitle">Nginx Proxy Manager</div> <div class="card__subtitle">Nginx Proxy Manager</div>
</div> </div>
<!-- Card Body --> <!-- Card Body -->
<div class="card__body"> <div class="card__body">
<div class="card__meta--url">npm.sori.studio</div> <div class="card__meta--url">npm.sori.studio</div>
<p class="card__desc" data-lang="ko">리버스 프록시 관리자</p> <p class="card__desc" data-lang="ko">리버스 프록시 관리자</p>
<p class="card__desc" data-lang="en">Reverse proxy manager</p> <p class="card__desc" data-lang="en">Reverse proxy manager</p>
</div> </div>
</a> </a>
<!-- gitea --> <!-- gitea -->
<a href="https://git.sori.studio" class="card" data-url="https://git.sori.studio"> <a href="https://git.sori.studio" class="card" data-url="https://git.sori.studio">
<!-- Decorative Element --> <!-- Decorative Element -->
<div class="card__decor-layer"><span class="card__decor card__decor--tl"></span><span class="card__decor card__decor--tr"></span><span class="card__decor card__decor--bl"></span><span class="card__decor card__decor--br"></span></div> <div class="card__decor-layer"><span class="card__decor card__decor--tl"></span><span class="card__decor card__decor--tr"></span><span class="card__decor card__decor--bl"></span><span class="card__decor card__decor--br"></span></div>
<!-- Card Title --> <!-- Card Title -->
<div class="card__header w-full min-w-0 flex flex-column justify-center align-center overflow-hidden"> <div class="card__header w-full min-w-0 flex flex-column justify-center align-center overflow-hidden">
<div class="card__title max-w-full min-w-0 mt-2 text-center text-ellipsis glyph">Gitea</div> <div class="card__title max-w-full min-w-0 mt-2 text-center text-ellipsis glyph">Gitea</div>
<div class="card__subtitle">Gitea</div> <div class="card__subtitle">Gitea</div>
</div> </div>
<!-- Card Body --> <!-- Card Body -->
<div class="card__body"> <div class="card__body">
<div class="card__meta--url">git.sori.studio</div> <div class="card__meta--url">git.sori.studio</div>
<p class="card__desc" data-lang="ko">개발 리소스 저장소</p> <p class="card__desc" data-lang="ko">개발 리소스 저장소</p>
<p class="card__desc" data-lang="en">Development resource repository</p> <p class="card__desc" data-lang="en">Development resource repository</p>
</div> </div>
</a> </a>
<!-- Pocket Base --> <!-- Pocket Base -->
<a href="https://api.sori.studio" class="card" data-url="https://api.sori.studio"> <a href="https://api.sori.studio" class="card" data-url="https://api.sori.studio">
<!-- Decorative Element --> <!-- Decorative Element -->
<div class="card__decor-layer"><span class="card__decor card__decor--tl"></span><span class="card__decor card__decor--tr"></span><span class="card__decor card__decor--bl"></span><span class="card__decor card__decor--br"></span></div> <div class="card__decor-layer"><span class="card__decor card__decor--tl"></span><span class="card__decor card__decor--tr"></span><span class="card__decor card__decor--bl"></span><span class="card__decor card__decor--br"></span></div>
<!-- Card Title --> <!-- Card Title -->
<div class="card__header w-full min-w-0 flex flex-column justify-center align-center overflow-hidden"> <div class="card__header w-full min-w-0 flex flex-column justify-center align-center overflow-hidden">
<div class="card__title max-w-full min-w-0 mt-2 text-center text-ellipsis glyph">Pocket Base</div> <div class="card__title max-w-full min-w-0 mt-2 text-center text-ellipsis glyph">Pocket Base</div>
<div class="card__subtitle">Pocket Base</div> <div class="card__subtitle">Pocket Base</div>
</div> </div>
<!-- Card Body --> <!-- Card Body -->
<div class="card__body"> <div class="card__body">
<div class="card__meta--url">api.sori.studio</div> <div class="card__meta--url">api.sori.studio</div>
<p class="card__desc" data-lang="ko">개인화 API 서버</p> <p class="card__desc" data-lang="ko">개인화 API 서버</p>
<p class="card__desc" data-lang="en">Custom API Server</p> <p class="card__desc" data-lang="en">Custom API Server</p>
</div> </div>
</a> </a>
</div> </div>
</section> </section>
<section class="category-section mx-auto pt-5 px-0 pb-10"> <section class="category-section mx-auto pt-5 px-0 pb-10">
<h2 class="section-title flex align-center pl-1 mb-6">Public Services</h2> <h2 class="section-title flex align-center pl-1 mb-6">Public Services</h2>
<div class="container w-full grid grid-col-3 gap-6"> <div class="container w-full grid grid-col-3 gap-6">
<!-- union arena deck builder --> <!-- union arena deck builder -->
<a href="https://uniare.sori.studio" class="card" data-url="https://uniare.sori.studio"> <a href="https://uniare.sori.studio" class="card" data-url="https://uniare.sori.studio">
<!-- Decorative Element --> <!-- Decorative Element -->
<div class="card__decor-layer"><span class="card__decor card__decor--tl"></span><span class="card__decor card__decor--tr"></span><span class="card__decor card__decor--bl"></span><span class="card__decor card__decor--br"></span></div> <div class="card__decor-layer"><span class="card__decor card__decor--tl"></span><span class="card__decor card__decor--tr"></span><span class="card__decor card__decor--bl"></span><span class="card__decor card__decor--br"></span></div>
<!-- Card Title --> <!-- Card Title -->
<div class="card__header w-full min-w-0 flex flex-column justify-center align-center overflow-hidden"> <div class="card__header w-full min-w-0 flex flex-column justify-center align-center overflow-hidden">
<div class="card__title max-w-full min-w-0 mt-2 text-center text-ellipsis glyph">UNIARE</div> <div class="card__title max-w-full min-w-0 mt-2 text-center text-ellipsis glyph">UNIARE</div>
<div class="card__subtitle">Union Arena Deck Builder</div> <div class="card__subtitle">Union Arena Deck Builder</div>
</div> </div>
<!-- Card Body --> <!-- Card Body -->
<div class="card__body"> <div class="card__body">
<div class="card__meta--url">uniare.sori.studio</div> <div class="card__meta--url">uniare.sori.studio</div>
<p class="card__desc" data-lang="ko">반다이 남코의 TCG 'Union Arena'의 덱 빌더</p> <p class="card__desc" data-lang="ko">반다이 남코의 TCG 'Union Arena'의 덱 빌더</p>
<p class="card__desc" data-lang="en">Deck builder for the TCG 'Union Arena' by Bandai Namco</p> <p class="card__desc" data-lang="en">Deck builder for the TCG 'Union Arena' by Bandai Namco</p>
</div> </div>
</a> </a>
</div> </div>
</section> </section>
<section class="category-section mx-auto pt-5 px-0 pb-10"> <section class="category-section mx-auto pt-5 px-0 pb-10">
<h2 class="section-title flex align-center pl-1 mb-6">Personal Archive</h2> <h2 class="section-title flex align-center pl-1 mb-6">Personal Archive</h2>
<div class="container w-full grid grid-col-3 gap-6"> <div class="container w-full grid grid-col-3 gap-6">
<!-- mastodon --> <!-- mastodon -->
<a href="https://sns.sori.studio" class="card" data-url="https://sns.sori.studio"> <a href="https://sns.sori.studio" class="card" data-url="https://sns.sori.studio">
<!-- Decorative Element --> <!-- Decorative Element -->
<div class="card__decor-layer"><span class="card__decor card__decor--tl"></span><span class="card__decor card__decor--tr"></span><span class="card__decor card__decor--bl"></span><span class="card__decor card__decor--br"></span></div> <div class="card__decor-layer"><span class="card__decor card__decor--tl"></span><span class="card__decor card__decor--tr"></span><span class="card__decor card__decor--bl"></span><span class="card__decor card__decor--br"></span></div>
<!-- Card Title --> <!-- Card Title -->
<div class="card__header w-full min-w-0 flex flex-column justify-center align-center overflow-hidden"> <div class="card__header w-full min-w-0 flex flex-column justify-center align-center overflow-hidden">
<div class="card__title max-w-full min-w-0 mt-2 text-center text-ellipsis glyph">Mastodon</div> <div class="card__title max-w-full min-w-0 mt-2 text-center text-ellipsis glyph">Mastodon</div>
<div class="card__subtitle">Sori.Space Mastodon</div> <div class="card__subtitle">Sori.Space Mastodon</div>
</div> </div>
<!-- Card Body --> <!-- Card Body -->
<div class="card__body"> <div class="card__body">
<div class="card__meta--url">sns.sori.studio</div> <div class="card__meta--url">sns.sori.studio</div>
<p class="card__desc" data-lang="ko">소리 스튜디오(Sori Studio)의 공식 마스토돈 인스턴스입니다.<br />운영자 zenn의 개인적인 기록과 소소한 이야기를 <p class="card__desc" data-lang="ko">소리 스튜디오(Sori Studio)의 공식 마스토돈 인스턴스입니다.<br />운영자 zenn의 개인적인 기록과 소소한 이야기를
공유하는 독립적인 공간입니다.</p> 공유하는 독립적인 공간입니다.</p>
<p class="card__desc" data-lang="en">This is the official Mastodon instance of Sori Studio.<br />It is an <p class="card__desc" data-lang="en">This is the official Mastodon instance of Sori Studio.<br />It is an
independent space where operator zenn shares his personal records and small stories.</p> independent space where operator zenn shares his personal records and small stories.</p>
</div> </div>
</a> </a>
</div> </div>
</section> </section>
</main>
<script src="script.js"></script> <footer class="text-center p-4">
<p>&copy; 2025 sori.studio</p>
</footer>
<script src="script.js?v=1.0.1" defer></script>
</body> </body>
</html> </html>

2
robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-agent: *
Allow: /

121
script.js
View File

@@ -19,28 +19,47 @@ const dot = document.querySelector('.dot-point');
// ping // ping
async function checkStatus() { async function checkStatus() {
const cards = document.querySelectorAll(".card"); const cards = document.querySelectorAll(".card");
const results = [];
cards.forEach(async (card) => { for (const card of cards) {
const url = card.getAttribute("data-url"); const url = card.getAttribute("data-url");
const dot = card.querySelector(".card__decor.card__decor--tr"); const dot = card.querySelector(".card__decor.card__decor--tr");
try { // 결과 저장을 위한 약속(Promise) 생성
const controller = new AbortController(); const checkTask = new Promise((resolve) => {
const timeoutId = setTimeout(() => controller.abort(), 5000); const img = new Image();
// 5초 타임아웃 설정
const timeoutId = setTimeout(() => {
img.src = "";
resolve({ dot, status: "offline" });
}, 5000);
await fetch(url, { img.onload = () => {
mode: "no-cors", clearTimeout(timeoutId);
cache: "no-cache", resolve({ dot, status: "online" });
signal: controller.signal, };
});
dot.classList.remove("status-offline"); img.onerror = () => {
dot.classList.add("status-online"); clearTimeout(timeoutId);
clearTimeout(timeoutId); resolve({ dot, status: "offline" }); // 502 에러 시 이쪽으로 빠집니다.
} catch (e) { };
dot.classList.remove("status-online");
dot.classList.add("status-offline"); // 캐시 방지를 위해 타임스탬프를 붙여서 파비콘 호출
} 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");
dot.classList.add(status === "online" ? "status-online" : "status-offline");
});
}); });
} }
@@ -57,20 +76,18 @@ function startBoltAction() {
let allBolts = []; let allBolts = [];
if (TEST_ONLY_SECOND_CARD) { if (TEST_ONLY_SECOND_CARD) {
// 🔧 2번째 카드만 선택 (0-based index)
const secondCard = document.querySelectorAll(".card")[1]; const secondCard = document.querySelectorAll(".card")[1];
if (!secondCard) return; if (!secondCard) return;
allBolts = secondCard.querySelectorAll(".card__decor"); allBolts = secondCard.querySelectorAll(".card__decor");
} else { } else {
// 🔹 전체 카드 대상
allBolts = document.querySelectorAll(".card__decor"); allBolts = document.querySelectorAll(".card__decor");
} }
if (allBolts.length === 0) return; if (allBolts.length === 0) return;
const availableBolts = Array.from(allBolts).filter( const availableBolts = Array.from(allBolts).filter(
(bolt) => !bolt.classList.contains("card__decor-acting") (bolt) => !bolt.classList.contains("is-acting")
); );
if (availableBolts.length > 0) { if (availableBolts.length > 0) {
@@ -82,13 +99,12 @@ function startBoltAction() {
randomBolt.style.setProperty("--r", baseRot); randomBolt.style.setProperty("--r", baseRot);
const BOLT_ANIMATION_DURATION = 16000; // CSS와 반드시 일치 randomBolt.classList.add("is-acting");
randomBolt.classList.add("card__decor-acting"); // setTimeout 대신 이벤트 리스너 사용
randomBolt.addEventListener('animationend', () => {
setTimeout(() => { randomBolt.classList.remove("is-acting");
randomBolt.classList.remove("card__decor-acting"); }, { once: true }); // 메모리 관리를 위해 한 번만 실행 후 제거
}, BOLT_ANIMATION_DURATION);
} }
const nextInterval = Math.random() * 4000 + 3000; const nextInterval = Math.random() * 4000 + 3000;
@@ -99,14 +115,16 @@ window.addEventListener("load", () => {
setTimeout(startBoltAction, 2000); setTimeout(startBoltAction, 2000);
}); });
// 나사 랜덤 각도 부연` window.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.card').forEach((card, cardIndex) => { // 1. 상태 체크는 1초 뒤에 여유롭게 시작
card.querySelectorAll('.card__decor').forEach((decor, decorIndex) => { setTimeout(checkStatus, 1000);
// -30 ~ 150 정도가 나사 느낌 제일 안정적 // 2. 나사 랜덤 각도 설정 (DOM이 준비된 직후 실행)
const angle = Math.floor(Math.random() * 180) - 30; document.querySelectorAll('.card').forEach((card) => {
card.querySelectorAll('.card__decor').forEach((decor) => {
decor.style.setProperty('--r', `${angle}deg`); const angle = Math.floor(Math.random() * 180) - 30;
decor.style.setProperty('--r', `${angle}deg`);
});
}); });
}); });
@@ -161,12 +179,10 @@ window.addEventListener('keydown', (e) => {
}); });
const logo = document.querySelector('header h1'); const logo = document.querySelector('header h1');
// const themes = ['semi-nova', 'nova', 'semi-solaris', 'solaris'];
// const STORAGE_KEY = 'selected-theme';
let startX = 0; let startX = 0;
let isDragging = false; let isDragging = false;
const SWIPE_THRESHOLD = 40; // px const SWIPE_THRESHOLD = 40;
function getCurrentThemeIndex() { function getCurrentThemeIndex() {
return themes.findIndex(t => document.body.classList.contains(t)); return themes.findIndex(t => document.body.classList.contains(t));
@@ -177,13 +193,15 @@ function applyTheme(index) {
document.body.classList.add(themes[index]); document.body.classList.add(themes[index]);
localStorage.setItem(STORAGE_KEY, themes[index]); localStorage.setItem(STORAGE_KEY, themes[index]);
const dot = document.querySelector('.dot-point'); const dot = document.querySelector('.dot-point');
if (dot) {
if (dot) { dot.classList.remove('blink-alert');
dot.classList.remove('blink-alert'); // 리플로우 유발 코드 제거 (void dot.offsetWidth;)
void dot.offsetWidth; // 애니메이션 리셋 // 대신 브라우저의 다음 렌더링 사이클을 이용
dot.classList.add('blink-alert'); requestAnimationFrame(() => {
} dot.classList.add('blink-alert');
});
}
} }
logo.addEventListener('pointerdown', (e) => { logo.addEventListener('pointerdown', (e) => {
@@ -202,10 +220,8 @@ logo.addEventListener('pointerup', (e) => {
if (index === -1) index = 0; if (index === -1) index = 0;
if (deltaX > 0) { if (deltaX > 0) {
// 👉 오른쪽
index = (index + 1) % themes.length; index = (index + 1) % themes.length;
} else { } else {
// 👈 왼쪽
index = (index - 1 + themes.length) % themes.length; index = (index - 1 + themes.length) % themes.length;
} }
@@ -221,19 +237,4 @@ logo.addEventListener('pointercancel', reset);
function reset() { function reset() {
isDragging = false; isDragging = false;
logo.classList.remove('is-sliding'); 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();

View File

@@ -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: "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; } @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; } .solaris .section-title { font-size: 1.25rem; }
.nova .section-title { font-size: 1.75rem; } .nova .section-title { font-size: 1.75rem; }
@@ -174,7 +172,8 @@ body {
.card__decor--br { bottom: 1rem; right: 1rem; } .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::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 */ /* keyframes */