mirror of
https://github.com/headporter81/specialsource-homepage-frontend.git
synced 2026-10-07 15:21:10 +09:00
Compare commits
11
Commits
22918fc366
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61a5955378 | ||
|
|
365a3088ec | ||
|
|
816a1d7711 | ||
|
|
203d36b89b | ||
|
|
41e624b5b4 | ||
|
|
47120557a0 | ||
|
|
319b3248ec | ||
|
|
ded7e6e6f9 | ||
|
|
2f9091ed70 | ||
|
|
1aa7086f51 | ||
|
|
ef847061eb |
@@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"hosting": {
|
"hosting": {
|
||||||
|
"predeploy": [
|
||||||
|
"npm run build"
|
||||||
|
],
|
||||||
"public": "dist",
|
"public": "dist",
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"firebase.json",
|
"firebase.json",
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-view />
|
<router-view />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* 전역 초기화 스타일 */
|
|
||||||
body, html { margin: 0; padding: 0; width: 100%; height: 100%; background-color: #f1f5f9; }
|
|
||||||
#app { width: 100%; height: 100%; }
|
|
||||||
</style>
|
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -64,84 +64,3 @@ const barString = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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,6 +1,7 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router' // 라우터 가져오기
|
import router from './router' // 라우터 가져오기
|
||||||
|
import './style.css' // 전역 스타일 (모든 페이지 공통 관리)
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
app.use(router) // 라우터 연결
|
app.use(router) // 라우터 연결
|
||||||
|
|||||||
+613
-259
@@ -1,296 +1,650 @@
|
|||||||
:root {
|
/* ==============================================================
|
||||||
--text: #6b6375;
|
전역 스타일 (Global Styles)
|
||||||
--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;
|
|
||||||
|
|
||||||
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
|
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;700&display=swap');
|
||||||
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
--mono: ui-monospace, Consolas, monospace;
|
|
||||||
|
|
||||||
font: 18px/145% var(--sans);
|
/* --- 전역 리셋 --- */
|
||||||
letter-spacing: 0.18px;
|
body, html { margin: 0; padding: 0; width: 100%; height: 100%; background-color: #f1f5f9; }
|
||||||
color-scheme: light dark;
|
#app { width: 100%; height: 100%; }
|
||||||
color: var(--text);
|
|
||||||
background: var(--bg);
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
|
|
||||||
@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 {
|
.theme-green {
|
||||||
--text: #9ca3af;
|
--tui-color: #00ff00;
|
||||||
--text-h: #f3f4f6;
|
--tui-dim: #008800;
|
||||||
--bg: #16171d;
|
--tui-glow: rgba(0, 255, 0, 0.6);
|
||||||
--border: #2e303a;
|
--tui-border: rgba(0, 255, 0, 0.2);
|
||||||
--code-bg: #1f2028;
|
}
|
||||||
--accent: #c084fc;
|
.theme-amber {
|
||||||
--accent-bg: rgba(192, 132, 252, 0.15);
|
--tui-color: #ffb000;
|
||||||
--accent-border: rgba(192, 132, 252, 0.5);
|
--tui-dim: #aa6600;
|
||||||
--social-bg: rgba(47, 48, 58, 0.5);
|
--tui-glow: rgba(255, 176, 0, 0.6);
|
||||||
--shadow:
|
--tui-border: rgba(255, 176, 0, 0.2);
|
||||||
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
|
}
|
||||||
}
|
.theme-vga {
|
||||||
|
--tui-color: #e2e8f0;
|
||||||
#social .button-icon {
|
--tui-dim: #718096;
|
||||||
filter: invert(1) brightness(2);
|
--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,
|
.homepage-wrapper {
|
||||||
h2 {
|
background-color: #050505;
|
||||||
font-family: var(--heading);
|
color: var(--tui-color);
|
||||||
font-weight: 500;
|
min-height: 100vh;
|
||||||
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;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
#center {
|
.tui-header {
|
||||||
display: flex;
|
color: var(--tui-dim) !important;
|
||||||
flex-direction: column;
|
margin-bottom: 15px !important;
|
||||||
gap: 25px;
|
font-size: 14px !important;
|
||||||
place-content: center;
|
|
||||||
place-items: center;
|
|
||||||
flex-grow: 1;
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
padding: 32px 20px 24px;
|
|
||||||
gap: 18px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#next-steps {
|
.tui-cmd {
|
||||||
display: flex;
|
color: #ffffff !important;
|
||||||
border-top: 1px solid var(--border);
|
margin-left: 5px !important;
|
||||||
text-align: left;
|
}
|
||||||
|
|
||||||
& > div {
|
.tui-bar-row {
|
||||||
flex: 1 1 0;
|
font-size: 20px !important;
|
||||||
padding: 32px;
|
letter-spacing: 1px !important;
|
||||||
@media (max-width: 1024px) {
|
white-space: pre !important;
|
||||||
padding: 24px 20px;
|
display: flex !important;
|
||||||
}
|
align-items: center !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.tui-bracket {
|
||||||
margin-bottom: 16px;
|
color: var(--tui-dim) !important;
|
||||||
width: 22px;
|
}
|
||||||
height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
.tui-bar {
|
||||||
flex-direction: column;
|
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;
|
text-align: center;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#docs {
|
.tui-alert-header {
|
||||||
border-right: 1px solid var(--border);
|
color: var(--tui-dim) !important;
|
||||||
|
margin-bottom: 15px !important;
|
||||||
@media (max-width: 1024px) {
|
font-size: 13px !important;
|
||||||
border-right: none;
|
letter-spacing: 1px;
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#next-steps ul {
|
.tui-alert-body {
|
||||||
list-style: none;
|
color: #ffffff !important;
|
||||||
padding: 0;
|
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;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 12px 16px;
|
||||||
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;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
li {
|
|
||||||
flex: 1 1 calc(50% - 8px);
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
}
|
.menu-btn { font-size: 14px; }
|
||||||
}
|
|
||||||
|
|
||||||
#spacer {
|
.auth-group { width: 100%; justify-content: center; margin-top: 5px; flex-wrap: wrap; gap: 10px; }
|
||||||
height: 88px;
|
.nav-item { margin: 0; } /* 간격 조정 */
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
height: 48px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ticks {
|
/* 3. 로그인 폼 세로 배치 (글씨가 겹치는 것 방지) */
|
||||||
position: relative;
|
.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%;
|
width: 100%;
|
||||||
|
border-bottom: 1px solid var(--tui-dim);
|
||||||
&::before,
|
padding-bottom: 5px;
|
||||||
&::after {
|
border-radius: 0;
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: -4.5px;
|
|
||||||
border: 5px solid transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before {
|
/* 4. ASCII 아트 가독성 및 가로 스크롤 허용 */
|
||||||
left: 0;
|
.ascii-container {
|
||||||
border-left-color: var(--border);
|
padding: 10px 5px;
|
||||||
}
|
}
|
||||||
&::after {
|
.ascii-art {
|
||||||
right: 0;
|
font-size: 9px !important; /* 모바일 사이즈에 맞게 축소 */
|
||||||
border-right-color: var(--border);
|
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; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,12 +38,3 @@ const handleLogin = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</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
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="['homepage-wrapper', `theme-${activeTheme}`]">
|
<div :class="['homepage-wrapper', `theme-${activeTheme}`, { 'intro-done': logoDone }]">
|
||||||
<nav class="auth-nav">
|
<nav class="auth-nav">
|
||||||
<div class="nav-left">
|
<div class="nav-left">
|
||||||
<span class="status-dot" :class="{ 'secure-mode': isLoggedIn }"></span>
|
<span class="status-dot" :class="{ 'secure-mode': isLoggedIn }"></span>
|
||||||
@@ -14,10 +14,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<select v-model="activeTheme" class="theme-selector">
|
<select v-model="activeTheme" class="theme-selector">
|
||||||
<option value="green">P3_GREEN (기본)</option>
|
<option value="green">IBM 5151 (그린)</option>
|
||||||
<option value="amber">HERCULES (호박)</option>
|
<option value="amber">ZVM-1240 (호박)</option>
|
||||||
<option value="vga">VGA_GRAY (회색)</option>
|
<option value="vga">IBM 8503 (회색)</option>
|
||||||
<option value="ega">EGA_CYAN (청록)</option>
|
<option value="ega">IBM 5154 (청록)</option>
|
||||||
|
<option value="white">VT220 (백색)</option>
|
||||||
|
<option value="plasma">GRiD COMPASS (플라즈마)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -48,8 +50,47 @@
|
|||||||
<transition name="fade" mode="out-in">
|
<transition name="fade" mode="out-in">
|
||||||
|
|
||||||
<div v-if="activeMenu === 'main'" class="sub-page main-page" key="main">
|
<div v-if="activeMenu === 'main'" class="sub-page main-page" key="main">
|
||||||
<div class="ascii-container">
|
<div class="ascii-container" :class="{ 'is-collapsed': logoDone }">
|
||||||
<pre class="ascii-art" v-html="formattedAscii"></pre>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -70,6 +111,99 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 v-else-if="activeMenu === 'login'" class="sub-page login-page" key="login">
|
||||||
<div class="login-terminal-frame">
|
<div class="login-terminal-frame">
|
||||||
<h2 class="login-gate-title">> SECURE_AUTHENTICATION_GATEWAY.EXE</h2>
|
<h2 class="login-gate-title">> SECURE_AUTHENTICATION_GATEWAY.EXE</h2>
|
||||||
@@ -148,6 +282,27 @@
|
|||||||
<TuiProgressBar :progress="overlayProgress" :status="overlayStatus" />
|
<TuiProgressBar :progress="overlayProgress" :status="overlayStatus" />
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -155,6 +310,8 @@
|
|||||||
import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
|
import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import TuiProgressBar from '@/components/TuiProgressBar.vue';
|
import TuiProgressBar from '@/components/TuiProgressBar.vue';
|
||||||
|
import TuiAlert from '@/components/TuiAlert.vue';
|
||||||
|
import TuiPostDetail from '@/components/TuiPostDetail.vue';
|
||||||
|
|
||||||
const videos = ref([]);
|
const videos = ref([]);
|
||||||
const terminalRef = ref(null);
|
const terminalRef = ref(null);
|
||||||
@@ -166,9 +323,18 @@ const loginPw = ref('');
|
|||||||
const activeMenu = ref('main');
|
const activeMenu = ref('main');
|
||||||
const authToken = ref(localStorage.getItem('token'));
|
const authToken = ref(localStorage.getItem('token'));
|
||||||
const currentUsername = ref(localStorage.getItem('username') || '');
|
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 storedTheme = localStorage.getItem('theme');
|
||||||
const activeTheme = ref(VALID_THEMES.includes(storedTheme) ? storedTheme : 'green');
|
const activeTheme = ref(VALID_THEMES.includes(storedTheme) ? storedTheme : 'green');
|
||||||
|
const activeThemeLabel = computed(() => THEME_LABELS[activeTheme.value]);
|
||||||
|
|
||||||
watch(activeTheme, (theme) => {
|
watch(activeTheme, (theme) => {
|
||||||
localStorage.setItem('theme', theme);
|
localStorage.setItem('theme', theme);
|
||||||
@@ -178,15 +344,254 @@ const isProcessing = ref(false);
|
|||||||
const overlayProgress = ref(0);
|
const overlayProgress = ref(0);
|
||||||
const overlayStatus = ref('');
|
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 todayUnique = ref(0);
|
||||||
const activeNow = ref(0);
|
const activeNow = ref(0);
|
||||||
let analyticsTimer = null;
|
let analyticsTimer = null;
|
||||||
|
let weatherTimer = null;
|
||||||
|
|
||||||
const currentServerSource = ref('nas');
|
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 = [
|
const menuItems = [
|
||||||
{ id: 'main', name: 'HOME' },
|
{ id: 'main', name: 'HOME' },
|
||||||
{ id: 'youtube', name: 'YOUTUBE' },
|
{ id: 'youtube', name: 'YOUTUBE' },
|
||||||
|
{ id: 'board', name: 'BOARD' },
|
||||||
// { id: 'project', name: 'PROJECT' },
|
// { id: 'project', name: 'PROJECT' },
|
||||||
{ id: 'about', name: 'ABOUT' },
|
{ id: 'about', name: 'ABOUT' },
|
||||||
{ id: 'contact', name: 'CONTACT' }
|
{ id: 'contact', name: 'CONTACT' }
|
||||||
@@ -327,7 +732,7 @@ const fetchVideos = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openVideo = (id) => window.open(`https://www.youtube.com/watch?v=${id}`);
|
const openVideo = (id) => window.open(`https://www.youtube.com/watch?v=${id}`);
|
||||||
const handleRegister = () => alert("회원가입은 현재 관리자 승인제로 운영됩니다.");
|
const handleRegister = () => showSystemAlert("회원가입은 현재 관리자 승인제로 운영됩니다.");
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
@@ -343,7 +748,7 @@ const handleLogout = () => {
|
|||||||
|
|
||||||
const handleLoginSubmit = async () => {
|
const handleLoginSubmit = async () => {
|
||||||
if (!loginId.value || !loginPw.value) {
|
if (!loginId.value || !loginPw.value) {
|
||||||
alert("아이디와 비밀번호를 모두 입력해 주세요.");
|
showSystemAlert("아이디와 비밀번호를 모두 입력해 주세요.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,13 +805,21 @@ const handleLoginSubmit = async () => {
|
|||||||
if (termInstance.value) {
|
if (termInstance.value) {
|
||||||
termInstance.value.echo(`[[b;red;][CRITICAL ERROR]] ACCESS DENIED! INVALID ACCESS KEY.`);
|
termInstance.value.echo(`[[b;red;][CRITICAL ERROR]] ACCESS DENIED! INVALID ACCESS KEY.`);
|
||||||
}
|
}
|
||||||
alert("인증에 실패했습니다. 액세스 키를 확인하세요.");
|
showSystemAlert("인증에 실패했습니다. 액세스 키를 확인하세요.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
if (shouldShowBootLog()) {
|
||||||
|
runBootSequence();
|
||||||
|
markBootLogShown();
|
||||||
|
} else {
|
||||||
|
isBooting.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
fetchVideos();
|
fetchVideos();
|
||||||
fetchAnalyticsData();
|
fetchAnalyticsData();
|
||||||
|
fetchWeatherData();
|
||||||
|
|
||||||
detectRealtimeServerSource();
|
detectRealtimeServerSource();
|
||||||
|
|
||||||
@@ -415,6 +828,8 @@ onMounted(() => {
|
|||||||
detectRealtimeServerSource();
|
detectRealtimeServerSource();
|
||||||
}, 30000);
|
}, 30000);
|
||||||
|
|
||||||
|
weatherTimer = setInterval(fetchWeatherData, 600000);
|
||||||
|
|
||||||
const globaljQuery = window.$;
|
const globaljQuery = window.$;
|
||||||
if (globaljQuery && terminalRef.value) {
|
if (globaljQuery && terminalRef.value) {
|
||||||
termInstance.value = globaljQuery(terminalRef.value).terminal({
|
termInstance.value = globaljQuery(terminalRef.value).terminal({
|
||||||
@@ -503,317 +918,6 @@ onMounted(() => {
|
|||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (analyticsTimer) clearInterval(analyticsTimer);
|
if (analyticsTimer) clearInterval(analyticsTimer);
|
||||||
|
if (weatherTimer) clearInterval(weatherTimer);
|
||||||
});
|
});
|
||||||
</script>
|
</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>
|
|
||||||
Reference in New Issue
Block a user