Compare commits

...
11 Commits
Author SHA1 Message Date
headporter 61a5955378 fix : main banner modify
SpecialSource Frontend CD / deploy (push) Successful in 7s
2026-08-12 10:56:29 +09:00
headporter 365a3088ec fix : main banner modify
SpecialSource Frontend CD / deploy (push) Successful in 6s
2026-08-11 18:03:54 +09:00
headporter 816a1d7711 fix : main banner modify
SpecialSource Frontend CD / deploy (push) Failing after 4s
2026-08-11 17:59:52 +09:00
headporter 203d36b89b fix : main banner modify
SpecialSource Frontend CD / deploy (push) Successful in 8s
2026-08-11 11:08:25 +09:00
headporter 41e624b5b4 fix : main banner modify
SpecialSource Frontend CD / deploy (push) Successful in 7s
2026-08-11 10:27:53 +09:00
headporter 47120557a0 fix : main banner modify
SpecialSource Frontend CD / deploy (push) Successful in 9s
2026-08-11 10:09:26 +09:00
headporterandClaude Sonnet 5 319b3248ec fix : add missing TuiAlert component
SpecialSource Frontend CD / deploy (push) Successful in 6s
TuiAlert.vue was referenced by Main.vue but never committed, breaking the Docker/CI build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 22:47:33 +09:00
headporter ded7e6e6f9 fix : main banner modify
SpecialSource Frontend CD / deploy (push) Failing after 6s
2026-08-10 22:44:39 +09:00
headporter 2f9091ed70 Merge remote-tracking branch 'origin/master'
SpecialSource Frontend CD / deploy (push) Failing after 4s
2026-08-10 17:54:39 +09:00
headporter 1aa7086f51 fix : main banner modify 2026-08-10 17:54:29 +09:00
headporter ef847061eb fix : main banner modify 2026-08-10 17:52:27 +09:00
9 changed files with 1105 additions and 684 deletions
+3
View File
@@ -1,5 +1,8 @@
{
"hosting": {
"predeploy": [
"npm run build"
],
"public": "dist",
"ignore": [
"firebase.json",
-6
View File
@@ -1,9 +1,3 @@
<template>
<router-view />
</template>
<style>
/* 전역 초기화 스타일 */
body, html { margin: 0; padding: 0; width: 100%; height: 100%; background-color: #f1f5f9; }
#app { width: 100%; height: 100%; }
</style>
+20
View File
@@ -0,0 +1,20 @@
<script setup>
defineProps({
message: {
type: String,
required: true
}
});
defineEmits(['close']);
</script>
<template>
<div class="tui-alert-window">
<div class="tui-alert-header">
<span>[SYSTEM ALERT]</span>
</div>
<div class="tui-alert-body">{{ message }}</div>
<button class="tui-alert-btn" @click="$emit('close')">확인</button>
</div>
</template>
+35
View File
@@ -0,0 +1,35 @@
<script setup>
defineProps({
post: {
type: Object,
required: true
}
});
defineEmits(['close']);
</script>
<template>
<div class="tui-post-detail">
<div class="post-detail-head">
<div>
<div class="post-detail-eyebrow">{{ post.sourceLabel }} · POST_DETAIL.DAT</div>
<h3 class="post-detail-title">{{ post.title }}</h3>
</div>
<button class="post-detail-close" @click="$emit('close')" aria-label="닫기">✕</button>
</div>
<div class="post-detail-meta">
<span v-if="post.author">{{ post.author }}</span>
<span v-if="post.createdAt">{{ post.createdAt }}</span>
<span v-if="post.views !== null">조회 {{ post.views }}</span>
<span v-if="post.likes !== null">추천 {{ post.likes }}</span>
</div>
<div v-if="post.content" class="post-detail-text" v-html="post.content"></div>
<div v-if="post.url" class="post-detail-foot">
<a :href="post.url" target="_blank" rel="noopener noreferrer">원문에서 보기 ↗</a>
</div>
</div>
</template>
-81
View File
@@ -64,84 +64,3 @@ const barString = computed(() => {
</div>
</div>
</template>
<style scoped>
/* 🌟 [인프라 대통합최종 패치]
순정 레이아웃의 단단한 결합력(!important 락)은 그대로 유지하면서,
하드코딩된 색상값들을 마스터 테마 변수(var(--tui-...))로 싹 교체해 실시간 색상 연동 성공!
*/
.tui-window {
background-color: #000000 !important;
color: var(--tui-color) !important; /* 🟢 테마 주축 색상 연동 */
font-family: 'JetBrains Mono', monospace !important;
padding: 20px !important;
border-radius: 4px !important;
border: 1px solid var(--tui-color) !important; /* 🟢 테마 테두리 연동 */
box-shadow: 0 0 15px var(--tui-border) !important; /* 🟢 테마 글로우 연동 */
display: inline-block;
min-width: 520px;
box-sizing: border-box;
}
.tui-header {
color: var(--tui-dim) !important; /* 🟢 테마 감쇄(어두운) 색상 연동 */
margin-bottom: 15px !important;
font-size: 14px !important;
}
.tui-cmd {
color: #ffffff !important;
margin-left: 5px !important;
}
.tui-bar-row {
font-size: 20px !important;
letter-spacing: 1px !important;
white-space: pre !important;
display: flex !important;
align-items: center !important;
}
.tui-bracket {
color: var(--tui-dim) !important; /* 🟢 브래킷 색상 연동 */
}
.tui-bar {
color: var(--tui-color) !important; /* 🟢 차오르는 게이지 블록 색상 연동 */
text-shadow: 0 0 5px var(--tui-glow) !important; /* 🟢 게이지 네온 번짐 연동 */
}
.tui-percent {
color: #ffffff !important;
margin-left: 12px !important;
font-weight: bold !important;
}
.tui-status-row {
margin-top: 15px !important;
font-size: 14px !important;
border-top: 1px dotted var(--tui-border) !important; /* 🟢 구분 점선 테마 연동 */
padding-top: 10px !important;
}
.tui-label {
color: var(--tui-dim) !important; /* 🟢 라벨 색상 연동 */
margin-right: 8px !important;
}
.tui-message {
color: #ffffff !important;
word-break: break-all !important;
}
/* 📱 모바일 환경 최적화 다운사이징 */
@media screen and (max-width: 650px) {
.tui-window {
min-width: 90vw !important;
padding: 15px !important;
}
.tui-bar-row {
font-size: 14px !important;
}
}
</style>
+1
View File
@@ -1,6 +1,7 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router' // 라우터 가져오기
import './style.css' // 전역 스타일 (모든 페이지 공통 관리)
const app = createApp(App)
app.use(router) // 라우터 연결
+613 -259
View File
@@ -1,296 +1,650 @@
:root {
--text: #6b6375;
--text-h: #08060d;
--bg: #fff;
--border: #e5e4e7;
--code-bg: #f4f3ec;
--accent: #aa3bff;
--accent-bg: rgba(170, 59, 255, 0.1);
--accent-border: rgba(170, 59, 255, 0.5);
--social-bg: rgba(244, 243, 236, 0.5);
--shadow:
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
/* ==============================================================
전역 스타일 (Global Styles)
모든 페이지/컴포넌트의 스타일을 이 파일 한 곳에서 관리합니다.
============================================================== */
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, Consolas, monospace;
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;700&display=swap');
font: 18px/145% var(--sans);
letter-spacing: 0.18px;
color-scheme: light dark;
color: var(--text);
background: var(--bg);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* --- 전역 리셋 --- */
body, html { margin: 0; padding: 0; width: 100%; height: 100%; background-color: #f1f5f9; }
#app { width: 100%; height: 100%; }
@media (max-width: 1024px) {
font-size: 16px;
}
/* --- 폰트 강제 적용 대상 --- */
.homepage-wrapper,
.menu-btn,
.nav-item,
.terminal-box,
.tui-global-overlay,
.login-terminal-frame,
.project-item,
.about-text,
.contact-form,
.theme-selector,
.nav-stats,
.server-badge,
.board-card,
.rail-item,
.board-login-btn,
.board-load-more,
.mode-toggle-btn,
.post-detail-close,
.terminal,
.terminal span {
font-family: 'JetBrains Mono', monospace !important;
}
@media (prefers-color-scheme: dark) {
:root {
--text: #9ca3af;
--text-h: #f3f4f6;
--bg: #16171d;
--border: #2e303a;
--code-bg: #1f2028;
--accent: #c084fc;
--accent-bg: rgba(192, 132, 252, 0.15);
--accent-border: rgba(192, 132, 252, 0.5);
--social-bg: rgba(47, 48, 58, 0.5);
--shadow:
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
}
#social .button-icon {
filter: invert(1) brightness(2);
}
/* --- 테마 변수 모음 --- */
.theme-green {
--tui-color: #00ff00;
--tui-dim: #008800;
--tui-glow: rgba(0, 255, 0, 0.6);
--tui-border: rgba(0, 255, 0, 0.2);
}
.theme-amber {
--tui-color: #ffb000;
--tui-dim: #aa6600;
--tui-glow: rgba(255, 176, 0, 0.6);
--tui-border: rgba(255, 176, 0, 0.2);
}
.theme-vga {
--tui-color: #e2e8f0;
--tui-dim: #718096;
--tui-glow: rgba(226, 232, 240, 0.4);
--tui-border: rgba(226, 232, 240, 0.2);
}
.theme-ega {
--tui-color: #00ffff;
--tui-dim: #008b8b;
--tui-glow: rgba(0, 255, 255, 0.6);
--tui-border: rgba(0, 255, 255, 0.2);
}
.theme-white {
--tui-color: #f4f4f0;
--tui-dim: #9a9a94;
--tui-glow: rgba(244, 244, 240, 0.5);
--tui-border: rgba(244, 244, 240, 0.2);
}
.theme-plasma {
--tui-color: #ff7300;
--tui-dim: #b35100;
--tui-glow: rgba(255, 115, 0, 0.6);
--tui-border: rgba(255, 115, 0, 0.2);
}
body {
margin: 0;
/* ==============================================================
메인 페이지 (Main.vue)
============================================================== */
.terminal, .terminal span {
--color: var(--tui-color) !important;
text-shadow: 0 0 5px var(--tui-glow) !important;
}
h1,
h2 {
font-family: var(--heading);
font-weight: 500;
color: var(--text-h);
}
h1 {
font-size: 56px;
letter-spacing: -1.68px;
margin: 32px 0;
@media (max-width: 1024px) {
font-size: 36px;
margin: 20px 0;
}
}
h2 {
font-size: 24px;
line-height: 118%;
letter-spacing: -0.24px;
margin: 0 0 8px;
@media (max-width: 1024px) {
font-size: 20px;
}
}
p {
margin: 0;
}
code,
.counter {
font-family: var(--mono);
display: inline-flex;
border-radius: 4px;
color: var(--text-h);
}
code {
font-size: 15px;
line-height: 135%;
padding: 4px 8px;
background: var(--code-bg);
}
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}
#app {
width: 1126px;
max-width: 100%;
margin: 0 auto;
text-align: center;
border-inline: 1px solid var(--border);
min-height: 100svh;
.homepage-wrapper {
background-color: #050505;
color: var(--tui-color);
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
}
.nav-stats {
font-size: 13px;
color: var(--tui-color);
font-weight: bold;
border: 1px dashed var(--tui-border);
padding: 3px 10px;
background: rgba(0, 0, 0, 0.5);
letter-spacing: 1px;
}
.pulse-text {
color: var(--tui-color);
font-weight: bold;
animation: textPulse 2s infinite ease-in-out;
margin: 0 3px;
}
.theme-selector {
background: #111;
border: 1px solid var(--tui-border);
color: var(--tui-color);
font-size: 11px;
padding: 3px 6px;
outline: none;
cursor: pointer;
transition: 0.3s;
box-shadow: 0 0 4px var(--tui-border);
}
.theme-selector:hover {
border-color: var(--tui-color);
box-shadow: 0 0 8px var(--tui-glow);
}
.server-badge {
font-size: 11px;
font-weight: bold;
padding: 3px 8px;
border: 1px solid currentColor;
background: rgba(0, 0, 0, 0.5);
letter-spacing: 1px;
border-radius: 2px;
transition: 0.3s;
}
.server-badge.nas, .dash-chip.nas { color: var(--tui-color); text-shadow: 0 0 5px var(--tui-glow); border-color: var(--tui-border); }
.server-badge.firebase_night, .dash-chip.firebase_night { color: #ffb000; text-shadow: 0 0 5px rgba(255, 176, 0, 0.6); border-color: rgba(255, 176, 0, 0.3); }
.server-badge.firebase_direct, .dash-chip.firebase_direct { color: #ffb000; text-shadow: 0 0 8px rgba(255, 176, 0, 0.6); border-color: rgba(255, 176, 0, 0.4); }
.server-badge.firebase_failover, .dash-chip.firebase_failover { color: #ff3b3b; text-shadow: 0 0 8px #ff3b3b; border-color: rgba(255, 59, 59, 0.4); animation: badgeBlink 1.5s infinite ease-in-out; }
/* --- GNB --- */
.auth-nav {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 12px 20px;
padding: 15px clamp(12px, 4vw, 40px);
background: #000;
border-bottom: 1px solid var(--tui-border);
}
.nav-left { display: flex; align-items: center; flex-wrap: wrap; gap: 15px; }
.nav-menu { display: flex; flex-wrap: wrap; gap: 20px; }
.menu-btn { background: none; border: none; color: var(--tui-dim); font-size: 16px; cursor: pointer; transition: 0.3s; }
.menu-btn:hover, .menu-btn.active { color: var(--tui-color); text-shadow: 0 0 10px var(--tui-color); }
.auth-group { display: flex; align-items: center; flex-wrap: wrap; }
.nav-item {
color: var(--tui-color) !important; text-decoration: none; background: none;
border: 1px solid var(--tui-color) !important; padding: 6px 18px; margin-left: 10px;
cursor: pointer; font-size: 13px; box-shadow: 0 0 5px var(--tui-border); transition: 0.3s;
}
.nav-item:hover, .nav-item.active-auth {
background: var(--tui-border) !important; color: var(--tui-color) !important; box-shadow: 0 0 10px var(--tui-color);
}
.user-info { color: var(--tui-dim) !important; font-size: 12px; margin-right: 15px; letter-spacing: 1px;}
.status-dot { width: 8px; height: 8px; background: #ff3b3b; border-radius: 50%; box-shadow: 0 0 8px #ff3b3b; transition: 0.5s; }
.status-dot.secure-mode { background: var(--tui-color); box-shadow: 0 0 8px var(--tui-color); }
/* --- 컨텐츠 영역 --- */
.content-area { flex: 1 1 auto; padding: 8px 20px; min-height: 220px; overflow-y: auto; display: flex; flex-direction: column; transition: flex-grow 0.6s ease; }
.homepage-wrapper.intro-done .content-area { flex-grow: 0; }
.sub-page { max-width: 1000px; margin: 0 auto; padding-top: 15px; width: 100%; }
.page-title { border-bottom: 1px solid var(--tui-border); padding-bottom: 10px; margin-bottom: 20px; color: var(--tui-color);}
.main-page { display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 28px; flex: 1; position: relative; }
.tui-global-overlay {
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
background-color: rgba(0, 0, 0, 0.88); z-index: 9999; display: flex;
justify-content: center; align-items: center; pointer-events: all;
}
.ascii-container {
width: 100%; overflow-x: auto; overflow-y: hidden; padding: 6px 20px; box-sizing: border-box; position: relative;
max-height: 600px; opacity: 1; transition: max-height 0.6s ease, opacity 0.5s ease, padding 0.6s ease;
}
.ascii-container.is-collapsed { max-height: 0; opacity: 0; padding-top: 0; padding-bottom: 0; }
.ascii-clip { width: max-content; margin: 0 auto; overflow: hidden; }
.ascii-art {
font-weight: bold; white-space: pre; display: block; margin: 0 auto; width: max-content;
color: var(--tui-color) !important; font-size: 14px; line-height: 1.2;
border: none; background: transparent; transform-origin: center; opacity: 1;
animation: once-cyber-glitch-wobble 2.5s linear 1 forwards !important;
}
.bg-block { color: #050505 !important; text-shadow: none !important; }
/* 대시보드 패널 — ASCII 글로우 인트로(2.5s)가 끝난 뒤 뒤이어 등장 */
.dash-grid {
display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 14px;
width: 100%; max-width: 720px; padding: 0 20px; box-sizing: border-box;
opacity: 0; animation: dashReveal 0.6s ease-out 2.5s forwards;
}
.dash-panel {
background: rgba(0, 0, 0, 0.5); border: 1px solid var(--tui-border); border-radius: 3px;
padding: 12px 14px;
}
.dash-panel h4 {
margin: 0 0 10px; font-size: 10.5px; letter-spacing: 0.1em; color: var(--tui-dim); font-weight: 500;
}
.dash-stat { display: flex; justify-content: space-between; font-variant-numeric: tabular-nums; font-size: 12.5px; margin-bottom: 6px; }
.dash-stat:last-child { margin-bottom: 0; }
.dash-stat span { color: var(--tui-dim); }
.dash-stat b { color: var(--tui-color); font-weight: 600; }
.dash-chip {
font-size: 11px; font-weight: bold; padding: 3px 8px; border: 1px solid currentColor;
background: rgba(0, 0, 0, 0.5); letter-spacing: 1px; border-radius: 2px; display: inline-block;
}
.weather-panel { text-align: center; }
.weather-ascii {
font-family: inherit; white-space: pre; text-align: left; display: inline-block;
font-size: 11px; line-height: 1.15; color: var(--tui-color); text-shadow: 0 0 4px var(--tui-glow);
margin: 0 0 8px; border: none; background: transparent;
}
.weather-loading { font-size: 11px; color: var(--tui-dim); letter-spacing: 0.05em; padding: 8px 0; }
/* 📺 유튜브 그리드 레이아웃 */
#videos-container {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px;
padding: 10px 20px; max-width: 1300px; margin: 0 auto; width: 100%; box-sizing: border-box;
}
.video-card {
width: 100%; background: #000; padding: 10px; border: 1px solid var(--tui-border); box-sizing: border-box;
position: relative; cursor: pointer; transition: 0.3s;
}
.video-card:hover { border-color: var(--tui-color); box-shadow: 0 0 10px var(--tui-border); }
.video-title { color: var(--tui-dim); margin-top: 10px; font-size: 14px; height: 40px; overflow: hidden; font-family: sans-serif; }
iframe { width: 100%; height: 180px; border-radius: 4px; }
/* 보안 로그인 프레임 */
.login-terminal-frame {
max-width: 550px; margin: 40px auto; border: 1px dashed var(--tui-color); padding: 35px;
background: #020a02; box-shadow: 0 0 15px var(--tui-border); box-sizing: border-box;
}
.login-gate-title { color: var(--tui-color); font-size: 20px; margin-bottom: 10px; font-weight: bold;}
.login-gate-desc { color: var(--tui-dim); font-size: 12px; margin-bottom: 30px; line-height: 1.4; }
.login-form-form { display: flex; flex-direction: column; gap: 20px; }
.login-input-group { display: flex; align-items: center; border-bottom: 1px solid var(--tui-dim); padding-bottom: 5px; }
.login-label { width: 120px; color: var(--tui-color); font-weight: bold; font-size: 14px; flex-shrink: 0; }
.login-input { flex: 1; background: none; border: none; color: var(--tui-color); font-size: 15px; outline: none; min-width: 0; }
.login-submit-btn {
margin-top: 15px; background: none; border: 1px solid var(--tui-color);
color: var(--tui-color); padding: 12px; font-size: 14px; font-weight: bold; cursor: pointer; transition: 0.3s;
}
.login-submit-btn:hover { background: var(--tui-color); color: #000; box-shadow: 0 0 15px var(--tui-color); }
/* 프로젝트, 어바웃, 연락처 등 */
.project-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
.project-item { border: 1px solid var(--tui-border); padding: 15px; background: #080808; }
.item-header { font-weight: bold; margin-bottom: 10px; color: var(--tui-color); }
.about-text { line-height: 2; color: var(--tui-dim);}
.contact-form { line-height: 2; color: var(--tui-dim);}
/* --- 게시판 (BOARD) --- */
.board-page-head {
display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;
border-bottom: 1px solid var(--tui-border); padding-bottom: 10px; margin-bottom: 20px;
}
.board-page-head .page-title { border-bottom: none; padding-bottom: 0; margin-bottom: 0; }
.board-mode-toggle { display: flex; border: 1px solid var(--tui-border); }
.mode-toggle-btn {
background: none; border: none; color: var(--tui-dim); font-size: 12px;
padding: 6px 14px; cursor: pointer;
}
.mode-toggle-btn.active { color: #000; background: var(--tui-color); font-weight: bold; }
.mode-toggle-btn:not(.active):hover { color: var(--tui-color); }
.board-layout { display: grid; grid-template-columns: 168px 1fr; gap: 18px; align-items: start; }
.board-rail { display: flex; flex-direction: column; gap: 6px; }
.rail-item {
display: flex; justify-content: space-between; gap: 8px; font-size: 12px; padding: 7px 10px;
border: 1px solid var(--tui-border); color: var(--tui-dim); background: rgba(0, 0, 0, 0.4);
cursor: pointer;
}
.rail-item.active { color: var(--tui-color); border-color: var(--tui-color); box-shadow: 0 0 8px var(--tui-border) inset; }
.rail-item b { font-weight: 600; }
.board-feed { display: flex; flex-direction: column; gap: 10px; }
.board-status {
color: var(--tui-dim); font-size: 13px; padding: 30px 0; text-align: center;
display: flex; flex-direction: column; align-items: center; gap: 12px;
}
.board-login-btn {
background: none; border: 1px solid var(--tui-color); color: var(--tui-color);
padding: 7px 18px; font-size: 12px; font-weight: bold; cursor: pointer; transition: 0.3s;
}
.board-login-btn:hover { background: var(--tui-color); color: #000; box-shadow: 0 0 10px var(--tui-color); }
.board-load-more {
padding: 10px; border: 1px dashed var(--tui-border); background: none; color: var(--tui-dim);
cursor: pointer; font-size: 12px;
}
.board-load-more:hover { color: var(--tui-color); border-color: var(--tui-color); }
.board-load-more:disabled { opacity: 0.5; cursor: default; }
.board-card {
display: flex; gap: 12px; padding: 10px; border: 1px solid var(--tui-border); background: #080808;
cursor: pointer; text-align: left; width: 100%; color: inherit;
}
.board-card:hover { border-color: var(--tui-color); box-shadow: 0 0 10px var(--tui-border); }
.board-thumb { width: 64px; height: 48px; flex-shrink: 0; object-fit: cover; border: 1px solid #151515; background: #050505; }
.board-thumb-empty { background: repeating-linear-gradient(45deg, #0c0c0c, #0c0c0c 4px, #050505 4px, #050505 8px); }
.board-card-body { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 5px; }
.board-tag { font-size: 9.5px; font-weight: bold; padding: 2px 6px; letter-spacing: 0.04em; border: 1px solid var(--tui-color); color: var(--tui-color); align-self: flex-start; }
.board-card-title { font-size: 13px; color: var(--tui-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.board-card:hover .board-card-title { color: var(--tui-color); }
.board-card-meta { font-size: 10.5px; color: var(--tui-dim); display: flex; gap: 10px; }
/* --- 게시글 상세 (TuiPostDetail.vue) --- */
.tui-post-detail {
background-color: #000000 !important;
color: var(--tui-color) !important;
font-family: 'JetBrains Mono', monospace !important;
padding: 26px 26px 0 !important;
border-radius: 4px !important;
border: 1px solid var(--tui-color) !important;
box-shadow: 0 0 15px var(--tui-border) !important;
width: 100%;
max-width: 820px;
max-height: 90vh;
overflow-y: auto;
box-sizing: border-box;
}
.post-detail-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 14px; }
.post-detail-eyebrow { font-size: 10px; letter-spacing: 0.12em; color: var(--tui-dim); margin-bottom: 8px; }
.post-detail-title { margin: 0; font-size: 16px; line-height: 1.5; color: var(--tui-color); font-weight: 700; text-shadow: 0 0 8px var(--tui-glow); }
.post-detail-close {
background: none; border: 1px solid var(--tui-border); color: var(--tui-dim);
width: 28px; height: 28px; flex-shrink: 0; cursor: pointer; font-size: 14px;
}
.post-detail-close:hover { border-color: var(--tui-color); color: var(--tui-color); }
.post-detail-meta {
font-size: 11px; color: var(--tui-dim); display: flex; gap: 12px; flex-wrap: wrap;
margin: 12px 0 16px; padding-bottom: 14px; border-bottom: 1px dashed var(--tui-border);
}
.post-detail-text { font-size: 13px; line-height: 1.85; color: #ffffff; margin: 0 0 18px; }
.post-detail-text p { margin: 0 0 12px; }
.post-detail-text p:last-child { margin-bottom: 0; }
.post-detail-text a { color: var(--tui-color); text-decoration: underline; }
.post-detail-text b, .post-detail-text strong { color: var(--tui-color); font-weight: 700; }
.post-detail-text img { max-width: 100%; height: auto; display: block; margin: 12px 0; border: 1px solid #151515; }
.post-detail-text blockquote {
margin: 12px 0; padding: 8px 14px; border-left: 2px solid var(--tui-border); color: var(--tui-dim);
}
.post-detail-text ul, .post-detail-text ol { margin: 0 0 12px; padding-left: 22px; }
.post-detail-text li { margin-bottom: 4px; }
.post-detail-text h1, .post-detail-text h2, .post-detail-text h3,
.post-detail-text h4, .post-detail-text h5, .post-detail-text h6 {
color: var(--tui-color); margin: 16px 0 8px; font-size: 14px;
}
.post-detail-text table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 12px; }
.post-detail-text td, .post-detail-text th { border: 1px solid var(--tui-border); padding: 6px 8px; }
.post-detail-foot {
position: sticky; bottom: 0; z-index: 1;
margin: 18px -26px 0; padding: 14px 26px 20px;
background: #000; border-top: 1px solid var(--tui-border);
display: flex; justify-content: flex-end;
}
.post-detail-foot a { font-size: 11.5px; color: var(--tui-color); text-decoration: none; border: 1px solid var(--tui-color); padding: 7px 14px; }
.post-detail-foot a:hover { background: var(--tui-color); color: #000; }
/* 애니메이션 트랜지션 */
.fade-enter-active, .fade-leave-active { transition: opacity 0.25s ease; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
/* 하단 CRT 터미널 */
.crt-monitor-frame { height: 460px; margin: 6px 40px 14px; border: 2px solid #1a1a1a; background: #0a0a0a; overflow: hidden; }
.monitor-header { background: #151515; padding: 5px 15px; color: #555; display: flex; justify-content: space-between; font-size: 12px;}
.monitor-status { color: var(--tui-dim); animation: blink 2s infinite; }
.crt-screen-overlay { position: relative; height: calc(100% - 30px); background: #020802; }
.crt-screen-overlay::before {
content: " "; display: block; position: absolute; top: 0; left: 0; bottom: 0; right: 0;
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06));
z-index: 2; background-size: 100% 4px, 6px 100%; pointer-events: none;
}
.terminal-box { width: 100%; height: 100%; padding: 10px; box-sizing: border-box; }
/* ==============================================================
부팅 로그 스크린 (초기 진입 시 로그 출력 후 사라짐)
============================================================== */
.boot-log-screen {
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
background-color: #000; z-index: 10000; display: flex;
justify-content: center; align-items: center; pointer-events: all;
}
.boot-log-box {
width: 100%; max-width: 620px; padding: 0 20px; box-sizing: border-box;
font-family: 'JetBrains Mono', monospace;
}
.boot-log-line {
margin: 0 0 6px; font-size: 13px; color: var(--tui-color);
text-shadow: 0 0 5px var(--tui-glow); letter-spacing: 0.5px;
animation: bootLineIn 0.2s ease-out;
}
.boot-cursor {
color: var(--tui-color); font-size: 14px; animation: blink 1s infinite;
}
/* ==============================================================
진행률 바 (TuiProgressBar.vue)
============================================================== */
.tui-window {
background-color: #000000 !important;
color: var(--tui-color) !important;
font-family: 'JetBrains Mono', monospace !important;
padding: 20px !important;
border-radius: 4px !important;
border: 1px solid var(--tui-color) !important;
box-shadow: 0 0 15px var(--tui-border) !important;
display: inline-block;
min-width: 520px;
box-sizing: border-box;
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
.tui-header {
color: var(--tui-dim) !important;
margin-bottom: 15px !important;
font-size: 14px !important;
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
.tui-cmd {
color: #ffffff !important;
margin-left: 5px !important;
}
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.tui-bar-row {
font-size: 20px !important;
letter-spacing: 1px !important;
white-space: pre !important;
display: flex !important;
align-items: center !important;
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
.tui-bracket {
color: var(--tui-dim) !important;
}
@media (max-width: 1024px) {
flex-direction: column;
.tui-bar {
color: var(--tui-color) !important;
text-shadow: 0 0 5px var(--tui-glow) !important;
}
.tui-percent {
color: #ffffff !important;
margin-left: 12px !important;
font-weight: bold !important;
}
.tui-status-row {
margin-top: 15px !important;
font-size: 14px !important;
border-top: 1px dotted var(--tui-border) !important;
padding-top: 10px !important;
}
.tui-label {
color: var(--tui-dim) !important;
margin-right: 8px !important;
}
.tui-message {
color: #ffffff !important;
word-break: break-all !important;
}
/* ==============================================================
시스템 알림 (TuiAlert.vue) — alert() 공통 대체 컴포넌트
============================================================== */
.tui-alert-window {
background-color: #000000 !important;
color: var(--tui-color) !important;
font-family: 'JetBrains Mono', monospace !important;
padding: 25px !important;
border-radius: 4px !important;
border: 1px solid var(--tui-color) !important;
box-shadow: 0 0 15px var(--tui-border) !important;
min-width: 320px;
max-width: 90vw;
box-sizing: border-box;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
.tui-alert-header {
color: var(--tui-dim) !important;
margin-bottom: 15px !important;
font-size: 13px !important;
letter-spacing: 1px;
}
#next-steps ul {
list-style: none;
padding: 0;
.tui-alert-body {
color: #ffffff !important;
font-size: 14px;
line-height: 1.6;
margin-bottom: 20px;
word-break: break-word;
white-space: pre-line;
}
.tui-alert-btn {
background: none !important;
border: 1px solid var(--tui-color) !important;
color: var(--tui-color) !important;
padding: 8px 24px !important;
font-size: 13px !important;
font-weight: bold !important;
cursor: pointer;
transition: 0.3s;
font-family: 'JetBrains Mono', monospace !important;
}
.tui-alert-btn:hover {
background: var(--tui-color) !important;
color: #000 !important;
box-shadow: 0 0 15px var(--tui-color);
}
/* ==============================================================
로그인 페이지 (Login.vue)
============================================================== */
.login-container { display: flex; justify-content: center; align-items: center; height: 100vh; background: #0f172a; }
.login-box { background: white; padding: 40px; border-radius: 8px; width: 350px; text-align: center; }
.login-box input { width: 100%; padding: 12px; margin: 10px 0; box-sizing: border-box; border: 1px solid #cbd5e1; border-radius: 4px; }
.login-box button { width: 100%; padding: 12px; background: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; }
.login-box button:hover { background: #2563eb; }
.login-box .error { color: #ef4444; margin-top: 15px; font-size: 14px; }
/* ==============================================================
📱 반응형 분기점 (Responsive Design)
============================================================== */
@media screen and (max-width: 1100px) {
#videos-container { grid-template-columns: repeat(2, 1fr); }
.project-grid { grid-template-columns: repeat(2, 1fr); }
.board-layout { grid-template-columns: 130px 1fr; }
}
@media screen and (max-width: 650px) {
/* 1. 레이아웃 & 패딩 초기화 */
.content-area { padding: 10px; }
#videos-container { grid-template-columns: 1fr; padding: 10px; }
.project-grid { grid-template-columns: 1fr; }
.board-layout { grid-template-columns: 1fr; }
.board-rail { flex-direction: row; flex-wrap: wrap; }
.tui-post-detail { max-width: 92vw !important; padding: 16px 16px 0 !important; }
.post-detail-foot { margin: 18px -16px 0; padding: 12px 16px 16px; }
/* 2. 네비게이션바 줄바꿈 허용 (화면 밖으로 넘어가지 않게 방어) */
.auth-nav {
flex-direction: column;
gap: 15px;
padding: 15px 10px; /* 좌우 패딩 줄임 */
align-items: stretch; /* 전체 너비 채우기 */
}
.nav-left {
flex-wrap: wrap; /* 배지, 통계 등이 좁으면 아래로 줄바꿈 */
justify-content: center;
text-align: center;
gap: 10px;
width: 100%;
}
.nav-menu {
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 18px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
gap: 12px 16px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
.menu-btn { font-size: 14px; }
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.auth-group { width: 100%; justify-content: center; margin-top: 5px; flex-wrap: wrap; gap: 10px; }
.nav-item { margin: 0; } /* 간격 조정 */
.ticks {
position: relative;
/* 3. 로그인 폼 세로 배치 (글씨가 겹치는 것 방지) */
.login-terminal-frame {
margin: 15px 5px;
padding: 20px 15px; /* 모바일에서는 여백을 좁게 */
}
.login-input-group {
flex-direction: column; /* 라벨과 인풋을 위아래로 배치 */
align-items: flex-start;
border-bottom: none; /* 하단 선 제거 */
gap: 8px;
}
.login-label { width: 100%; font-size: 13px; }
.login-input {
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
border-bottom: 1px solid var(--tui-dim);
padding-bottom: 5px;
border-radius: 0;
}
&::before {
left: 0;
border-left-color: var(--border);
/* 4. ASCII 아트 가독성 및 가로 스크롤 허용 */
.ascii-container {
padding: 10px 5px;
}
&::after {
right: 0;
border-right-color: var(--border);
.ascii-art {
font-size: 9px !important; /* 모바일 사이즈에 맞게 축소 */
line-height: 1.2 !important;
}
.dash-grid {
grid-template-columns: 1fr;
padding: 0 15px;
}
/* 5. 터미널 창 모바일 최적화 */
.crt-monitor-frame {
height: 280px !important;
margin: 6px 10px !important;
}
.monitor-header { padding: 4px 10px; font-size: 10px; }
/* 6. 진행률 바 모바일 최적화 */
.tui-window {
min-width: 90vw !important;
padding: 15px !important;
}
.tui-bar-row {
font-size: 14px !important;
}
}
/* ==============================================================
키프레임 애니메이션
============================================================== */
@keyframes blink { 0%, 100% { opacity: 0.4; } 50% { opacity: 1; } }
@keyframes textPulse { 0%, 100% { opacity: 0.3; } 50% { opacity: 1; text-shadow: 0 0 8px var(--tui-glow); } }
@keyframes badgeBlink { 0%, 100% { opacity: 0.6; box-shadow: 0 0 5px rgba(255, 59, 59, 0.2); } 50% { opacity: 1; box-shadow: 0 0 15px rgba(255, 59, 59, 0.6); text-shadow: 0 0 12px #ff3b3b; } }
@keyframes dashReveal { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
@keyframes bootLineIn { from { opacity: 0; transform: translateX(-4px); } to { opacity: 1; transform: translateX(0); } }
@keyframes once-cyber-glitch-wobble {
0%, 25% { transform: translate(0) scale(1) skewX(0deg); color: var(--tui-color) !important; text-shadow: 0 0 8px var(--tui-glow) !important; opacity: 1; }
26% { transform: translate(-3px, 1.5px) scale(1.02); color: #ffffff !important; text-shadow: 2px 0 #ff0000, -2px 0 #0000ff !important; }
27% { transform: translate(3px, -1.5px) skewX(-2deg); color: var(--tui-color) !important; }
28% { transform: translate(-5px, 0) skewX(10deg); color: #ff00ff !important; text-shadow: -3px 0 magenta, 3px 0 cyan;}
29% { transform: translate(5px, 0) skewX(-10deg); color: #00ffff !important; text-shadow: 3px 0 magenta, -3px 0 cyan;}
30% { transform: translate(0); color: var(--tui-color) !important; text-shadow: 0 0 15px var(--tui-color) !important;}
31%, 50% { transform: translate(0); color: var(--tui-color) !important; text-shadow: 0 0 8px var(--tui-glow) !important;}
51% { transform: scale(1.05) skewY(1deg); color: #ffffff !important; opacity: 0.05; }
53% { opacity: 1; transform: translate(0); color: var(--tui-color) !important; text-shadow: 0 0 20px var(--tui-color), 0 0 40px var(--tui-glow) !important; }
90% { transform: translate(-2px, 1px); text-shadow: 2px 0 red, -2px 0 blue;}
92% { transform: translate(2px, -1px); text-shadow: -2px 0 red, 2px 0 blue;}
95% { transform: translate(0); color: var(--tui-color) !important; text-shadow: 0 0 20px var(--tui-color) !important;}
100% { transform: translate(0) scale(1) skewX(0deg); color: var(--tui-color) !important; text-shadow: 0 0 2px var(--tui-color) !important; opacity: 1; }
}
-9
View File
@@ -38,12 +38,3 @@ const handleLogin = async () => {
}
}
</script>
<style scoped>
.login-container { display: flex; justify-content: center; align-items: center; height: 100vh; background: #0f172a; }
.login-box { background: white; padding: 40px; border-radius: 8px; width: 350px; text-align: center; }
input { width: 100%; padding: 12px; margin: 10px 0; box-sizing: border-box; border: 1px solid #cbd5e1; border-radius: 4px; }
button { width: 100%; padding: 12px; background: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; }
button:hover { background: #2563eb; }
.error { color: #ef4444; margin-top: 15px; font-size: 14px; }
</style>
+427 -323
View File
@@ -1,5 +1,5 @@
<template>
<div :class="['homepage-wrapper', `theme-${activeTheme}`]">
<div :class="['homepage-wrapper', `theme-${activeTheme}`, { 'intro-done': logoDone }]">
<nav class="auth-nav">
<div class="nav-left">
<span class="status-dot" :class="{ 'secure-mode': isLoggedIn }"></span>
@@ -14,10 +14,12 @@
</div>
<select v-model="activeTheme" class="theme-selector">
<option value="green">P3_GREEN (기본)</option>
<option value="amber">HERCULES (호박)</option>
<option value="vga">VGA_GRAY (회색)</option>
<option value="ega">EGA_CYAN (청록)</option>
<option value="green">IBM 5151 (그린)</option>
<option value="amber">ZVM-1240 (호박)</option>
<option value="vga">IBM 8503 (회색)</option>
<option value="ega">IBM 5154 (청록)</option>
<option value="white">VT220 (백색)</option>
<option value="plasma">GRiD COMPASS (플라즈마)</option>
</select>
</div>
@@ -48,8 +50,47 @@
<transition name="fade" mode="out-in">
<div v-if="activeMenu === 'main'" class="sub-page main-page" key="main">
<div class="ascii-container">
<pre class="ascii-art" v-html="formattedAscii"></pre>
<div class="ascii-container" :class="{ 'is-collapsed': logoDone }">
<div class="ascii-clip">
<transition name="fade">
<pre
v-if="!isBooting && !logoDone"
class="ascii-art"
v-html="formattedAscii"
@animationend="logoDone = true"
></pre>
</transition>
</div>
</div>
<div v-if="!isBooting" class="dash-grid">
<div v-if="isLoggedIn" class="dash-panel">
<h4>LIVE SESSION</h4>
<div class="dash-stat"><span>LIVE</span><b class="pulse-text">{{ activeNow }}</b></div>
<div class="dash-stat"><span>TODAY</span><b>{{ todayUnique }}</b></div>
</div>
<div class="dash-panel">
<h4>SERVER SOURCE</h4>
<span :class="['dash-chip', currentServerSource]">[{{ currentServerSourceDisplay }}]</span>
</div>
<div class="dash-panel weather-panel">
<h4>SEOUL WEATHER</h4>
<div v-if="weather.loading" class="weather-loading">SYNCING...</div>
<div v-else-if="weather.error" class="weather-loading">SYNC FAILED</div>
<template v-else>
<pre class="weather-ascii">{{ weatherIcon }}</pre>
<div class="dash-stat"><span>TEMP</span><b>{{ weather.temp }}°C</b></div>
<div class="dash-stat"><span>COND</span><b>{{ weatherLabel }}</b></div>
</template>
</div>
<div class="dash-panel">
<h4>SESSION</h4>
<div class="dash-stat"><span>USER</span><b>{{ isLoggedIn ? currentUsername : 'anonymous' }}</b></div>
<div class="dash-stat"><span>DISPLAY</span><b>{{ activeThemeLabel }}</b></div>
</div>
</div>
</div>
@@ -70,6 +111,99 @@
</div>
</div>
<div v-else-if="activeMenu === 'board'" class="sub-page board-page" key="board">
<div class="board-page-head">
<h2 class="page-title">> EXECUTING COMMUNITY_FEED.EXE</h2>
<div class="board-mode-toggle">
<button
class="mode-toggle-btn"
:class="{ active: boardViewMode === 'unread' }"
@click="setBoardViewMode('unread')"
>
안 읽음
</button>
<button
class="mode-toggle-btn"
:class="{ active: boardViewMode === 'viewed' }"
@click="setBoardViewMode('viewed')"
>
읽음
</button>
</div>
</div>
<div class="board-layout">
<div class="board-rail">
<button
class="rail-item"
:class="{ active: activeBoardKey === '' }"
@click="selectBoardSource('')"
>
<span>ALL</span>
</button>
<button
v-for="src in boardSources"
:key="src.boardKey"
class="rail-item"
:class="{ active: activeBoardKey === src.boardKey }"
@click="selectBoardSource(src.boardKey)"
>
<span>{{ src.siteName }} · {{ src.boardName }}</span>
</button>
</div>
<div class="board-feed">
<div v-if="board.error === 'auth'" class="board-status">
로그인이 필요한 기능입니다.
<button class="board-login-btn" @click="changeMenu('login')">LOGIN</button>
</div>
<div v-else-if="board.loading && board.posts.length === 0" class="board-status">SYNCING BOARD DATA...</div>
<div v-else-if="board.error" class="board-status">FAILED TO LOAD BOARD DATA.</div>
<div v-else-if="board.posts.length === 0" class="board-status">
{{ boardViewMode === 'viewed' ? '아직 읽은 게시물이 없습니다.' : 'NO POSTS FOUND.' }}
</div>
<template v-else>
<button
v-for="post in board.posts"
:key="post.id"
class="board-card"
@click="openPostDetail(post)"
>
<img
v-if="post.images[0]"
:src="post.images[0]"
referrerpolicy="no-referrer"
loading="lazy"
class="board-thumb"
@error="$event.target.style.visibility = 'hidden'"
/>
<div v-else class="board-thumb board-thumb-empty"></div>
<div class="board-card-body">
<div class="board-card-top">
<span class="board-tag">{{ post.sourceLabel }}</span>
</div>
<div class="board-card-title">{{ post.title }}</div>
<div class="board-card-meta">
<span v-if="post.createdAt">{{ post.createdAt }}</span>
<span v-if="post.views !== null">조회 {{ post.views }}</span>
<span v-if="post.likes !== null">추천 {{ post.likes }}</span>
<span v-if="boardViewMode === 'viewed' && post.viewedAt">읽음 {{ post.viewedAt }}</span>
</div>
</div>
</button>
<button
v-if="board.hasMore"
class="board-load-more"
:disabled="board.loading"
@click="loadMoreBoardPosts"
>
{{ board.loading ? 'LOADING...' : '더 보기' }}
</button>
</template>
</div>
</div>
</div>
<div v-else-if="activeMenu === 'login'" class="sub-page login-page" key="login">
<div class="login-terminal-frame">
<h2 class="login-gate-title">> SECURE_AUTHENTICATION_GATEWAY.EXE</h2>
@@ -148,6 +282,27 @@
<TuiProgressBar :progress="overlayProgress" :status="overlayStatus" />
</div>
</transition>
<transition name="fade">
<div v-if="systemAlert.show" class="tui-global-overlay" @click.self="systemAlert.show = false">
<TuiAlert :message="systemAlert.message" @close="systemAlert.show = false" />
</div>
</transition>
<transition name="fade">
<div v-if="postDetail.show" class="tui-global-overlay" @click.self="closePostDetail">
<TuiPostDetail :post="postDetail.post" @close="closePostDetail" />
</div>
</transition>
<transition name="fade">
<div v-if="isBooting" class="boot-log-screen">
<div class="boot-log-box">
<p v-for="(line, idx) in bootLines" :key="idx" class="boot-log-line">{{ line }}</p>
<span class="boot-cursor">_</span>
</div>
</div>
</transition>
</div>
</template>
@@ -155,6 +310,8 @@
import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
import axios from 'axios';
import TuiProgressBar from '@/components/TuiProgressBar.vue';
import TuiAlert from '@/components/TuiAlert.vue';
import TuiPostDetail from '@/components/TuiPostDetail.vue';
const videos = ref([]);
const terminalRef = ref(null);
@@ -166,9 +323,18 @@ const loginPw = ref('');
const activeMenu = ref('main');
const authToken = ref(localStorage.getItem('token'));
const currentUsername = ref(localStorage.getItem('username') || '');
const VALID_THEMES = ['green', 'amber', 'vga', 'ega'];
const THEME_LABELS = {
green: 'IBM 5151 (그린)',
amber: 'ZVM-1240 (호박)',
vga: 'IBM 8503 (회색)',
ega: 'IBM 5154 (청록)',
white: 'VT220 (백색)',
plasma: 'GRiD COMPASS (플라즈마)'
};
const VALID_THEMES = Object.keys(THEME_LABELS);
const storedTheme = localStorage.getItem('theme');
const activeTheme = ref(VALID_THEMES.includes(storedTheme) ? storedTheme : 'green');
const activeThemeLabel = computed(() => THEME_LABELS[activeTheme.value]);
watch(activeTheme, (theme) => {
localStorage.setItem('theme', theme);
@@ -178,15 +344,254 @@ const isProcessing = ref(false);
const overlayProgress = ref(0);
const overlayStatus = ref('');
const isBooting = ref(true);
const logoDone = ref(false);
const bootLines = ref([]);
const BOOT_SEQUENCE = [
'[SYS] SPECIALSOURCE.SYSTEM BIOS V3.2',
'[SYS] PERFORMING MEMORY CHECK... OK',
'[SYS] INITIALIZING KERNEL MODULES...',
'[SYS] MOUNTING VIRTUAL FILESYSTEM... OK',
'[SYS] ESTABLISHING SECURE NETWORK LINK...',
'[SYS] LOADING DASHBOARD INTERFACE...',
'[SYS] SPAWNING TERMINAL CONSOLE...',
'[SUCCESS] BOOT SEQUENCE COMPLETE.'
];
const BOOT_LOG_STORAGE_KEY = 'lastBootLogTimestamp';
const BOOT_LOG_INTERVAL_MS = 60 * 60 * 1000;
const runBootSequence = () => {
let i = 0;
const pushLine = () => {
if (i < BOOT_SEQUENCE.length) {
bootLines.value.push(BOOT_SEQUENCE[i]);
i++;
setTimeout(pushLine, 220);
} else {
setTimeout(() => {
isBooting.value = false;
}, 500);
}
};
pushLine();
};
const shouldShowBootLog = () => {
const lastShown = Number(localStorage.getItem(BOOT_LOG_STORAGE_KEY));
return !lastShown || Date.now() - lastShown >= BOOT_LOG_INTERVAL_MS;
};
const markBootLogShown = () => {
localStorage.setItem(BOOT_LOG_STORAGE_KEY, String(Date.now()));
};
const systemAlert = ref({ show: false, message: '' });
const showSystemAlert = (message) => {
systemAlert.value = { show: true, message };
};
const todayUnique = ref(0);
const activeNow = ref(0);
let analyticsTimer = null;
let weatherTimer = null;
const currentServerSource = ref('nas');
const WEATHER_ICONS = {
clear: " \\ /\n .-.\n ― ( ) ―\n `-'\n / \\",
cloudy: " .--.\n .-( ).\n (___.__)__)",
overcast: " .--.\n .-( ).\n (___.__)__)",
fog: " _ - _ - _\n_ - _ - _ -\n _ - _ - _",
drizzle: " .-.\n ( ).\n (___(__)\n ' ' ' '",
rain: " .-.\n ( ).\n (___(__)\n ' ' ' ' '",
snow: " .-.\n ( ).\n (___(__)\n * * *",
thunder: " .-.\n ( ).\n (___(__)\n ⚡ ' ⚡"
};
const WEATHER_LABELS = {
clear: '맑음',
cloudy: '구름 조금',
overcast: '흐림',
fog: '안개',
drizzle: '이슬비',
rain: '비',
snow: '눈',
thunder: '뇌우'
};
const getWeatherIconKey = (code) => {
if (code === 0) return 'clear';
if (code === 1 || code === 2) return 'cloudy';
if (code === 3) return 'overcast';
if (code === 45 || code === 48) return 'fog';
if ([51, 53, 55, 56, 57].includes(code)) return 'drizzle';
if ([61, 63, 65, 66, 67, 80, 81, 82].includes(code)) return 'rain';
if ([71, 73, 75, 77, 85, 86].includes(code)) return 'snow';
if ([95, 96, 99].includes(code)) return 'thunder';
return 'cloudy';
};
const weather = ref({ loading: true, error: false, temp: null, iconKey: 'clear' });
const weatherIcon = computed(() => WEATHER_ICONS[weather.value.iconKey] || WEATHER_ICONS.cloudy);
const weatherLabel = computed(() => WEATHER_LABELS[weather.value.iconKey] || '');
const fetchWeatherData = async () => {
try {
const res = await axios.get('https://api.open-meteo.com/v1/forecast', {
params: {
latitude: 37.5665,
longitude: 126.9780,
current_weather: true,
timezone: 'Asia/Seoul'
}
});
const current = res.data && res.data.current_weather;
if (current) {
weather.value = {
loading: false,
error: false,
temp: Math.round(current.temperature),
iconKey: getWeatherIconKey(current.weathercode)
};
} else {
weather.value.loading = false;
weather.value.error = true;
}
} catch (err) {
console.error("날씨 데이터 로드 실패:", err);
weather.value.loading = false;
weather.value.error = true;
}
};
const BOARD_PAGE_SIZE = 20;
const board = ref({ loading: false, error: null, loaded: false, posts: [], page: 0, hasMore: false });
const boardSources = ref([]);
const activeBoardKey = ref('');
const boardViewMode = ref('unread'); // 'unread' | 'viewed'
const postDetail = ref({ show: false, post: null });
const boardAuthHeaders = () => ({ Authorization: `Bearer ${authToken.value}` });
const mapScrapedPost = (row) => ({
id: row.id,
source: row.boardKey,
sourceLabel: `${row.siteName} · ${row.boardName}`,
title: row.title || '(제목 없음)',
url: row.link || null,
author: row.author || '',
createdAt: row.postedAt || '',
views: row.viewCount ?? null,
likes: row.likeCount ?? null,
content: row.content || '',
images: Array.isArray(row.imageUrls) ? row.imageUrls : [],
viewedAt: row.viewedAt || null
});
const fetchBoardMeta = async () => {
try {
const apiBaseUrl = import.meta.env.VITE_API_URL || '';
const res = await axios.get(`${apiBaseUrl}/api/scraped-posts/boards`, {
withCredentials: true,
headers: boardAuthHeaders()
});
boardSources.value = res.data || [];
} catch (err) {
console.error("게시판 목록 로드 실패:", err);
}
};
const fetchBoardPosts = async (reset) => {
if (reset) {
board.value.page = 0;
board.value.posts = [];
}
board.value.loading = true;
board.value.error = null;
try {
const apiBaseUrl = import.meta.env.VITE_API_URL || '';
const endpoint = boardViewMode.value === 'viewed' ? '/api/scraped-posts/viewed' : '/api/scraped-posts';
const res = await axios.get(`${apiBaseUrl}${endpoint}`, {
params: {
board: activeBoardKey.value || undefined,
page: board.value.page,
size: BOARD_PAGE_SIZE
},
withCredentials: true,
headers: boardAuthHeaders()
});
const rows = (res.data && res.data.content) || [];
const mapped = rows.map(mapScrapedPost);
board.value.posts = reset ? mapped : board.value.posts.concat(mapped);
board.value.hasMore = res.data.last === false;
board.value.loaded = true;
} catch (err) {
console.error("게시판 데이터 로드 실패:", err);
board.value.error = err?.response?.status === 401 || err?.response?.status === 403 ? 'auth' : 'network';
} finally {
board.value.loading = false;
}
};
const selectBoardSource = (boardKey) => {
if (activeBoardKey.value === boardKey) return;
activeBoardKey.value = boardKey;
fetchBoardPosts(true);
};
const setBoardViewMode = (mode) => {
if (boardViewMode.value === mode) return;
boardViewMode.value = mode;
fetchBoardPosts(true);
};
const loadMoreBoardPosts = () => {
board.value.page += 1;
fetchBoardPosts(false);
};
const openPostDetail = async (post) => {
postDetail.value = { show: true, post };
// '읽음' 목록에서는 조회 처리를 다시 할 필요도, 목록에서 뺄 필요도 없다.
if (boardViewMode.value === 'viewed') return;
try {
const apiBaseUrl = import.meta.env.VITE_API_URL || '';
await axios.post(`${apiBaseUrl}/api/scraped-posts/${post.id}/view`, null, {
withCredentials: true,
headers: boardAuthHeaders()
});
board.value.posts = board.value.posts.filter((p) => p.id !== post.id);
} catch (err) {
console.error("게시물 조회 처리 실패:", err);
}
};
const closePostDetail = () => {
postDetail.value.show = false;
};
watch(activeMenu, (id) => {
if (id !== 'board') return;
if (!authToken.value) {
board.value.error = 'auth';
return;
}
if (!board.value.loaded && !board.value.loading) {
fetchBoardMeta();
fetchBoardPosts(true);
}
});
const menuItems = [
{ id: 'main', name: 'HOME' },
{ id: 'youtube', name: 'YOUTUBE' },
{ id: 'board', name: 'BOARD' },
// { id: 'project', name: 'PROJECT' },
{ id: 'about', name: 'ABOUT' },
{ id: 'contact', name: 'CONTACT' }
@@ -327,7 +732,7 @@ const fetchVideos = async () => {
};
const openVideo = (id) => window.open(`https://www.youtube.com/watch?v=${id}`);
const handleRegister = () => alert("회원가입은 현재 관리자 승인제로 운영됩니다.");
const handleRegister = () => showSystemAlert("회원가입은 현재 관리자 승인제로 운영됩니다.");
const handleLogout = () => {
localStorage.removeItem('token');
@@ -343,7 +748,7 @@ const handleLogout = () => {
const handleLoginSubmit = async () => {
if (!loginId.value || !loginPw.value) {
alert("아이디와 비밀번호를 모두 입력해 주세요.");
showSystemAlert("아이디와 비밀번호를 모두 입력해 주세요.");
return;
}
@@ -400,13 +805,21 @@ const handleLoginSubmit = async () => {
if (termInstance.value) {
termInstance.value.echo(`[[b;red;][CRITICAL ERROR]] ACCESS DENIED! INVALID ACCESS KEY.`);
}
alert("인증에 실패했습니다. 액세스 키를 확인하세요.");
showSystemAlert("인증에 실패했습니다. 액세스 키를 확인하세요.");
}
};
onMounted(() => {
if (shouldShowBootLog()) {
runBootSequence();
markBootLogShown();
} else {
isBooting.value = false;
}
fetchVideos();
fetchAnalyticsData();
fetchWeatherData();
detectRealtimeServerSource();
@@ -415,6 +828,8 @@ onMounted(() => {
detectRealtimeServerSource();
}, 30000);
weatherTimer = setInterval(fetchWeatherData, 600000);
const globaljQuery = window.$;
if (globaljQuery && terminalRef.value) {
termInstance.value = globaljQuery(terminalRef.value).terminal({
@@ -503,317 +918,6 @@ onMounted(() => {
onUnmounted(() => {
if (analyticsTimer) clearInterval(analyticsTimer);
if (weatherTimer) clearInterval(weatherTimer);
});
</script>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;700&display=swap');
.homepage-wrapper,
.menu-btn,
.nav-item,
.terminal-box,
.tui-global-overlay,
.login-terminal-frame,
.project-item,
.about-text,
.contact-form,
.theme-selector,
.nav-stats,
.server-badge,
:deep(.terminal),
:deep(.terminal span) {
font-family: 'JetBrains Mono', monospace !important;
}
/* 변수 모음 */
.theme-green {
--tui-color: #00ff00;
--tui-dim: #008800;
--tui-glow: rgba(0, 255, 0, 0.6);
--tui-border: rgba(0, 255, 0, 0.2);
}
.theme-amber {
--tui-color: #ffb000;
--tui-dim: #aa6600;
--tui-glow: rgba(255, 176, 0, 0.6);
--tui-border: rgba(255, 176, 0, 0.2);
}
.theme-vga {
--tui-color: #e2e8f0;
--tui-dim: #718096;
--tui-glow: rgba(226, 232, 240, 0.4);
--tui-border: rgba(226, 232, 240, 0.2);
}
.theme-ega {
--tui-color: #00ffff;
--tui-dim: #008b8b;
--tui-glow: rgba(0, 255, 255, 0.6);
--tui-border: rgba(0, 255, 255, 0.2);
}
</style>
<style scoped>
:deep(.terminal), :deep(.terminal span) {
--color: var(--tui-color) !important;
text-shadow: 0 0 5px var(--tui-glow) !important;
}
.homepage-wrapper {
background-color: #050505;
color: var(--tui-color);
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
}
.nav-stats {
font-size: 13px;
color: var(--tui-color);
font-weight: bold;
border: 1px dashed var(--tui-border);
padding: 3px 10px;
background: rgba(0, 0, 0, 0.5);
letter-spacing: 1px;
}
.pulse-text {
color: var(--tui-color);
font-weight: bold;
animation: textPulse 2s infinite ease-in-out;
margin: 0 3px;
}
.theme-selector {
background: #111;
border: 1px solid var(--tui-border);
color: var(--tui-color);
font-size: 11px;
padding: 3px 6px;
outline: none;
cursor: pointer;
transition: 0.3s;
box-shadow: 0 0 4px var(--tui-border);
}
.theme-selector:hover {
border-color: var(--tui-color);
box-shadow: 0 0 8px var(--tui-glow);
}
.server-badge {
font-size: 11px;
font-weight: bold;
padding: 3px 8px;
border: 1px solid currentColor;
background: rgba(0, 0, 0, 0.5);
letter-spacing: 1px;
border-radius: 2px;
transition: 0.3s;
}
.server-badge.nas { color: var(--tui-color); text-shadow: 0 0 5px var(--tui-glow); border-color: var(--tui-border); }
.server-badge.firebase_night { color: #ffb000; text-shadow: 0 0 5px rgba(255, 176, 0, 0.6); border-color: rgba(255, 176, 0, 0.3); }
.server-badge.firebase_direct { color: #ffb000; text-shadow: 0 0 8px rgba(255, 176, 0, 0.6); border-color: rgba(255, 176, 0, 0.4); }
.server-badge.firebase_failover { color: #ff3b3b; text-shadow: 0 0 8px #ff3b3b; border-color: rgba(255, 59, 59, 0.4); animation: badgeBlink 1.5s infinite ease-in-out; }
/* --- GNB --- */
.auth-nav {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 12px 20px;
padding: 15px clamp(12px, 4vw, 40px);
background: #000;
border-bottom: 1px solid var(--tui-border);
}
.nav-left { display: flex; align-items: center; flex-wrap: wrap; gap: 15px; }
.nav-menu { display: flex; flex-wrap: wrap; gap: 20px; }
.menu-btn { background: none; border: none; color: var(--tui-dim); font-size: 16px; cursor: pointer; transition: 0.3s; }
.menu-btn:hover, .menu-btn.active { color: var(--tui-color); text-shadow: 0 0 10px var(--tui-color); }
.auth-group { display: flex; align-items: center; flex-wrap: wrap; }
.nav-item {
color: var(--tui-color) !important; text-decoration: none; background: none;
border: 1px solid var(--tui-color) !important; padding: 6px 18px; margin-left: 10px;
cursor: pointer; font-size: 13px; box-shadow: 0 0 5px var(--tui-border); transition: 0.3s;
}
.nav-item:hover, .nav-item.active-auth {
background: var(--tui-border) !important; color: var(--tui-color) !important; box-shadow: 0 0 10px var(--tui-color);
}
.user-info { color: var(--tui-dim) !important; font-size: 12px; margin-right: 15px; letter-spacing: 1px;}
.status-dot { width: 8px; height: 8px; background: #ff3b3b; border-radius: 50%; box-shadow: 0 0 8px #ff3b3b; transition: 0.5s; }
.status-dot.secure-mode { background: var(--tui-color); box-shadow: 0 0 8px var(--tui-color); }
/* --- 컨텐츠 영역 --- */
.content-area { flex: 1; padding: 8px 20px; min-height: 220px; overflow-y: auto; display: flex; flex-direction: column; }
.sub-page { max-width: 1000px; margin: 0 auto; padding-top: 15px; width: 100%; }
.page-title { border-bottom: 1px solid var(--tui-border); padding-bottom: 10px; margin-bottom: 20px; color: var(--tui-color);}
.main-page { display: flex; justify-content: center; align-items: center; flex: 1; position: relative; }
.tui-global-overlay {
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
background-color: rgba(0, 0, 0, 0.88); z-index: 9999; display: flex;
justify-content: center; align-items: center; pointer-events: all;
}
.ascii-container { width: 100%; overflow: auto; padding: 6px 20px; box-sizing: border-box; position: relative; }
.ascii-art {
font-weight: bold; white-space: pre; display: block; margin: 0 auto; width: max-content;
color: var(--tui-color) !important; font-size: 14px; line-height: 1.2;
border: none; background: transparent; transform-origin: center; opacity: 1;
animation: once-cyber-glitch-wobble 2.5s linear 1 forwards !important;
}
:deep(.bg-block) { color: #050505 !important; text-shadow: none !important; }
/* 📺 유튜브 그리드 레이아웃 */
#videos-container {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px;
padding: 10px 20px; max-width: 1300px; margin: 0 auto; width: 100%; box-sizing: border-box;
}
.video-card {
width: 100%; background: #000; padding: 10px; border: 1px solid var(--tui-border); box-sizing: border-box;
position: relative; cursor: pointer; transition: 0.3s;
}
.video-card:hover { border-color: var(--tui-color); box-shadow: 0 0 10px var(--tui-border); }
.video-title { color: var(--tui-dim); margin-top: 10px; font-size: 14px; height: 40px; overflow: hidden; font-family: sans-serif; }
iframe { width: 100%; height: 180px; border-radius: 4px; }
/* 보안 로그인 프레임 */
.login-terminal-frame {
max-width: 550px; margin: 40px auto; border: 1px dashed var(--tui-color); padding: 35px;
background: #020a02; box-shadow: 0 0 15px var(--tui-border); box-sizing: border-box;
}
.login-gate-title { color: var(--tui-color); font-size: 20px; margin-bottom: 10px; font-weight: bold;}
.login-gate-desc { color: var(--tui-dim); font-size: 12px; margin-bottom: 30px; line-height: 1.4; }
.login-form-form { display: flex; flex-direction: column; gap: 20px; }
.login-input-group { display: flex; align-items: center; border-bottom: 1px solid var(--tui-dim); padding-bottom: 5px; }
.login-label { width: 120px; color: var(--tui-color); font-weight: bold; font-size: 14px; flex-shrink: 0; }
.login-input { flex: 1; background: none; border: none; color: var(--tui-color); font-size: 15px; outline: none; min-width: 0; }
.login-submit-btn {
margin-top: 15px; background: none; border: 1px solid var(--tui-color);
color: var(--tui-color); padding: 12px; font-size: 14px; font-weight: bold; cursor: pointer; transition: 0.3s;
}
.login-submit-btn:hover { background: var(--tui-color); color: #000; box-shadow: 0 0 15px var(--tui-color); }
/* 프로젝트, 어바웃, 연락처 등 */
.project-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
.project-item { border: 1px solid var(--tui-border); padding: 15px; background: #080808; }
.item-header { font-weight: bold; margin-bottom: 10px; color: var(--tui-color); }
.about-text { line-height: 2; color: var(--tui-dim);}
.contact-form { line-height: 2; color: var(--tui-dim);}
/* 애니메이션 트랜지션 */
.fade-enter-active, .fade-leave-active { transition: opacity 0.25s ease; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
/* 하단 CRT 터미널 */
.crt-monitor-frame { height: 300px; margin: 6px 40px 14px; border: 2px solid #1a1a1a; background: #0a0a0a; overflow: hidden; }
.monitor-header { background: #151515; padding: 5px 15px; color: #555; display: flex; justify-content: space-between; font-size: 12px;}
.monitor-status { color: var(--tui-dim); animation: blink 2s infinite; }
.crt-screen-overlay { position: relative; height: calc(100% - 30px); background: #020802; }
.crt-screen-overlay::before {
content: " "; display: block; position: absolute; top: 0; left: 0; bottom: 0; right: 0;
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06));
z-index: 2; background-size: 100% 4px, 6px 100%; pointer-events: none;
}
.terminal-box { width: 100%; height: 100%; padding: 10px; box-sizing: border-box; }
/* ==============================================================
📱 반응형 분기점 (Responsive Design)
============================================================== */
@media screen and (max-width: 1100px) {
#videos-container { grid-template-columns: repeat(2, 1fr); }
.project-grid { grid-template-columns: repeat(2, 1fr); }
}
@media screen and (max-width: 650px) {
/* 1. 레이아웃 & 패딩 초기화 */
.content-area { padding: 10px; }
#videos-container { grid-template-columns: 1fr; padding: 10px; }
.project-grid { grid-template-columns: 1fr; }
/* 2. 네비게이션바 줄바꿈 허용 (화면 밖으로 넘어가지 않게 방어) */
.auth-nav {
flex-direction: column;
gap: 15px;
padding: 15px 10px; /* 좌우 패딩 줄임 */
align-items: stretch; /* 전체 너비 채우기 */
}
.nav-left {
flex-wrap: wrap; /* 배지, 통계 등이 좁으면 아래로 줄바꿈 */
justify-content: center;
text-align: center;
gap: 10px;
width: 100%;
}
.nav-menu {
display: flex;
gap: 12px 16px;
flex-wrap: wrap;
justify-content: center;
width: 100%;
}
.menu-btn { font-size: 14px; }
.auth-group { width: 100%; justify-content: center; margin-top: 5px; flex-wrap: wrap; gap: 10px; }
.nav-item { margin: 0; } /* 간격 조정 */
/* 3. 로그인 폼 세로 배치 (글씨가 겹치는 것 방지) */
.login-terminal-frame {
margin: 15px 5px;
padding: 20px 15px; /* 모바일에서는 여백을 좁게 */
}
.login-input-group {
flex-direction: column; /* 라벨과 인풋을 위아래로 배치 */
align-items: flex-start;
border-bottom: none; /* 하단 선 제거 */
gap: 8px;
}
.login-label { width: 100%; font-size: 13px; }
.login-input {
width: 100%;
border-bottom: 1px solid var(--tui-dim);
padding-bottom: 5px;
border-radius: 0;
}
/* 4. ASCII 아트 가독성 및 가로 스크롤 허용 */
.ascii-container {
padding: 10px 5px;
}
.ascii-art {
font-size: 9px !important; /* 모바일 사이즈에 맞게 축소 */
line-height: 1.2 !important;
}
/* 5. 터미널 창 모바일 최적화 */
.crt-monitor-frame {
height: 180px !important;
margin: 6px 10px !important;
}
.monitor-header { padding: 4px 10px; font-size: 10px; }
}
@keyframes blink { 0%, 100% { opacity: 0.4; } 50% { opacity: 1; } }
@keyframes textPulse { 0%, 100% { opacity: 0.3; } 50% { opacity: 1; text-shadow: 0 0 8px var(--tui-glow); } }
@keyframes badgeBlink { 0%, 100% { opacity: 0.6; box-shadow: 0 0 5px rgba(255, 59, 59, 0.2); } 50% { opacity: 1; box-shadow: 0 0 15px rgba(255, 59, 59, 0.6); text-shadow: 0 0 12px #ff3b3b; } }
@keyframes once-cyber-glitch-wobble {
0%, 25% { transform: translate(0) scale(1) skewX(0deg); color: var(--tui-color) !important; text-shadow: 0 0 8px var(--tui-glow) !important; opacity: 1; }
26% { transform: translate(-3px, 1.5px) scale(1.02); color: #ffffff !important; text-shadow: 2px 0 #ff0000, -2px 0 #0000ff !important; }
27% { transform: translate(3px, -1.5px) skewX(-2deg); color: var(--tui-color) !important; }
28% { transform: translate(-5px, 0) skewX(10deg); color: #ff00ff !important; text-shadow: -3px 0 magenta, 3px 0 cyan;}
29% { transform: translate(5px, 0) skewX(-10deg); color: #00ffff !important; text-shadow: 3px 0 magenta, -3px 0 cyan;}
30% { transform: translate(0); color: var(--tui-color) !important; text-shadow: 0 0 15px var(--tui-color) !important;}
31%, 50% { transform: translate(0); color: var(--tui-color) !important; text-shadow: 0 0 8px var(--tui-glow) !important;}
51% { transform: scale(1.05) skewY(1deg); color: #ffffff !important; opacity: 0.05; }
53% { opacity: 1; transform: translate(0); color: var(--tui-color) !important; text-shadow: 0 0 20px var(--tui-color), 0 0 40px var(--tui-glow) !important; }
90% { transform: translate(-2px, 1px); text-shadow: 2px 0 red, -2px 0 blue;}
92% { transform: translate(2px, -1px); text-shadow: -2px 0 red, 2px 0 blue;}
95% { transform: translate(0); color: var(--tui-color) !important; text-shadow: 0 0 20px var(--tui-color) !important;}
100% { transform: translate(0) scale(1) skewX(0deg); color: var(--tui-color) !important; text-shadow: 0 0 15px var(--tui-color), 0 0 30px var(--tui-glow) !important; opacity: 1; }
}
</style>