Public Source Viewer
비나래아카이브 개발자 포털
실제 서비스 구조를 살펴볼 수 있는 공개용 코드 뷰어입니다. 인증, 세션, 외부 연동, 토큰, 관리자 식별 등 보안상 민감한 구현은 파일 단위 또는 줄 단위로 검열됩니다.
view/hinana/main.ejs
공개 가능
1
<!DOCTYPE html>
2
<html lang="ko" xmlns="http://www.w3.org/1999/xhtml">
3
<head>
4
<meta charset="utf-8" />
5
<meta name="color-scheme" content="light dark">
6
<meta name="viewport" content="width=device-width, initial-scale=1">
7
<link rel="manifest" href="/manifest.json">
8
<meta name="theme-color" content="<%= (typeof theme !== 'undefined' && theme === 'dark') ? '#000000' : '#ffffff' %>">
9
<meta name="apple-mobile-web-app-title" content="비나래 아카이브">
10
<meta property="og:image" content="/image/2.png" />
11
<meta property="og:description" content="morikubo"/>
12
<meta property="og:url" content="hinana.moe"/>
13
<meta property="og:title" content="비나래 아카이브"/>
14
<link rel='stylesheet' href='/vendors/bootstrap/css/bootstrap.min.css' />
15
<script src="/vendors/bootstrap/js/bootstrap.min.js"></script>
16
<link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet" type="text/css">
17
<link href="https://fonts.googleapis.com/css?family=Lato" rel="stylesheet" type="text/css">
18
<link rel="stylesheet" href="/css/hinana.css" type="text/css">
19
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css">
20
<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
21
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js" integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh" crossorigin="anonymous"></script>
22
<title>비나래 아카이브</title>
23
<script type="text/javascript">
24
function applyTheme(theme) {
25
console.log(theme);
26
if (theme === 'dark') {
27
document.body.classList.add('dark-mode');
28
} else {
29
document.body.classList.remove('dark-mode');
30
}
31
}
32
33
document.addEventListener('DOMContentLoaded', () => {
34
const savedTheme = getCookie('theme') || '<%= theme || "light" %>';
35
applyTheme(savedTheme);
36
});
37
38
function getCookie(name) {
39
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
40
return match ? match[2] : null;
41
}
42
43
function setCookie(name, value, days = 365) {
44
const d = new Date();
45
d.setTime(d.getTime() + (days*24*60*60*1000));
46
document.cookie = `${name}=${value};path=/;expires=${d.toUTCString()}`;
47
}
48
</script>
49
<style>
50
.d-none {
51
display: none;
52
}
53
body {
54
margin: 0;
55
font-family: 'Noto Sans KR', sans-serif;
56
background-color: #f0f2f5;
57
}
58
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;700&display=swap');
59
60
.welcome-section {
61
display: flex;
62
flex-direction: column;
63
justify-content: center;
64
align-items: center;
65
min-height: 100vh;
66
padding: 2rem;
67
box-sizing: border-box;
68
position: relative;
69
overflow: hidden;
70
}
71
.welcome-title {
72
font-size: 3.5rem;
73
font-weight: 700;
74
color: #2c3e50;
75
margin-bottom: 4rem;
76
position: relative; /* z-index 적용을 위해 추가 */
77
z-index: 2;
78
}
79
.books-container {
80
display: flex;
81
flex-wrap: wrap;
82
justify-content: center;
83
gap: 3rem;
84
}
85
a {
86
text-decoration: none;
87
color: inherit;
88
}
89
.book {
90
position: relative;
91
width: 220px;
92
height: 300px;
93
border-radius: 2px 4px 4px 2px;
94
box-shadow: 5px 5px 15px rgba(0, 0, 0, 0.2);
95
display: flex;
96
justify-content: center;
97
align-items: center;
98
background-color: #444;
99
background-size: cover;
100
background-position: center;
101
background-repeat: no-repeat;
102
transition: transform 0.2s ease-in-out;
103
z-index: 1;
104
}
105
.book:hover {
106
transform: scale(1.03);
107
}
108
.book::before {
109
content: '';
110
position: absolute;
111
top: 0;
112
left: 0;
113
width: 25px;
114
height: 100%;
115
border-radius: 2px 0 0 2px;
116
background: linear-gradient( to bottom, #a93226 0%, #a93226 10%, #f1c40f 10%, #f1c40f 14%, #a93226 14%, #a93226 18%, #f1c40f 18%, #f1c40f 22%, #a93226 22%, #a93226 100% );
117
}
118
.book::after {
119
content: '';
120
position: absolute;
121
bottom: 0;
122
left: 0;
123
width: calc(100% - 5px);
124
right: 0;
125
margin: 0 auto;
126
height: 15px;
127
background: repeating-linear-gradient( to right, #e0e0e0, #e0e0e0 1px, #f7f7f7 1px, #f7f7f7 2px );
128
border-top: 1px solid #ccc;
129
border-radius: 0 0 4px 4px;
130
}
131
#book1 {
132
background-image: linear-gradient(rgba(0,0,0,0.3), rgba(0,0,0,0.3)), url('images/VRChat_2025-07-03_13-11-49.995_1920x1080.png');
133
}
134
#book2 {
135
background-image: linear-gradient(rgba(0,0,0,0.3), rgba(0,0,0,0.3)), url('images/VRChat_2025-07-03_13-16-00.144_1920x1080.png');
136
}
137
#book3 {
138
background-image: linear-gradient(rgba(0,0,0,0.3), rgba(0,0,0,0.3)), url('images/lounge_bg.png'); /* 이미지 경로를 변경하세요 */
139
background-color: #2c3e50; /* 이미지가 없을 때를 대비한 기본 배경색 */
140
}
141
.book-title {
142
color: white;
143
font-size: 1.5rem;
144
font-weight: 700;
145
text-align: center;
146
padding: 20px;
147
position: relative;
148
z-index: 1;
149
text-shadow: -1.5px -1.5px 0 #000, 1.5px -1.5px 0 #000, -1.5px 1.5px 0 #000, 1.5px 1.5px 0 #000;
150
}
151
@media (max-width: 768px) {
152
.book {
153
width: 180px;
154
height: 250px;
155
}
156
}
157
158
@keyframes fall {
159
0% {
160
transform: translateY(-20vh) rotateZ(-10deg);
161
opacity: 0;
162
}
163
10% {
164
opacity: 0.7;
165
}
166
90% {
167
opacity: 0.5;
168
}
169
100% {
170
transform: translateY(120vh) rotateZ(10deg);
171
opacity: 0;
172
}
173
}
174
.falling-leaf {
175
position: absolute;
176
top: -200px;
177
background-size: contain;
178
background-repeat: no-repeat;
179
animation: fall linear forwards;
180
z-index: 0;
181
filter: blur(5px);
182
opacity: 0;
183
pointer-events: none;
184
}
185
</style>
186
</head>
187
<body id="myPage" data-spy="scroll" data-bs-target=".navbar" data-offset="60" class="<%= theme === 'dark' ? 'dark-mode' : '' %>">
188
<nav class="navbar navbar-expand-md navbar-light bg-light" style="padding: 0.5rem 1rem;">
189
<div class="container-fluid">
190
<a class="navbar-brand" href="/hinana/index">
191
<img src="/image/archive1.png" alt="비나래 아카이브" class="theme-logo" style="max-height: 28px; width: auto;">
192
</a>
193
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
194
<span class="navbar-toggler-icon"></span>
195
</button>
196
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
197
<div class="navbar-nav">
198
<a class="nav-link" href="/hinana/info">info</a>
199
<a class="nav-link" href="/hinana/blog">blog</a>
200
<a class="nav-link" href="/hinana/lounge">lounge</a>
201
<form action="/toggle-theme" method="POST" class="d-inline" id="themeToggleForm">
202
<button type="submit" class="btn btn-sm btn-dark">
203
<span id="themeToggleLabel"><%= theme === 'dark' ? '☀️ 라이트모드' : '🌙 다크모드' %></span>
204
</button>
205
</form>
206
</div>
207
</div>
208
</div>
209
</nav>
210
<div id="services" class="container-fluid_sub text bg-grey fumikasan_web_post_left">
211
<div class="welcome-section">
212
<h1 class="welcome-title">환영합니다!</h1>
213
<div class="books-container">
214
<a href="./hinana/index" rel="noopener noreferrer">
215
<div class="book" id="book1">
216
<span class="book-title">비나래 아카이브</span>
217
</div>
218
</a>
219
<a href="./hinana/info" rel="noopener noreferrer">
220
<div class="book" id="book2">
221
<span class="book-title">비나래씨는요?</span>
222
</div>
223
</a>
224
<a href="./hinana/lounge" rel="noopener noreferrer">
225
<div class="book" id="book3">
226
<span class="book-title">비나래 라운지</span>
227
</div>
228
</a>
229
</div>
230
</div>
231
</div>
232
<div style="text-align:left;" class="footer">
233
<img src="/image/sign.png" id="fumika_sign" style="max-width:250px; max-height:initial; width:100%; height:100%;" />
234
</div>
235
<footer class="container-fluid text-center footer">
236
<a href="#myPage" title="To Top">
237
<span class="glyphicon glyphicon-chevron-up"></span>
238
</a>
239
<p style="margin-bottom: 0rem;" class="copyright">X - @NoctchillHinana</p>
240
<p style="margin-bottom: 0rem;" class="copyright">ⓒ 2024~2026. 비나래 | hinana.moe All rights reserved.</p>
241
</footer>
242
243
<script>
244
document.addEventListener('DOMContentLoaded', function() {
245
const container = document.querySelector('.welcome-section');
246
const imageSources = [
247
'image/1.png', 'image/2.png', 'image/3.png',
248
'image/4.png', 'image/5.png', 'image/6.png',
249
'image/7.png', 'image/8.png', 'image/9.png',
250
];
251
const leafCount = 15;
252
253
function randomBetween(a, b) {
254
return Math.random() * (b - a) + a;
255
}
256
257
function spawnLeaf() {
258
const leaf = document.createElement('div');
259
leaf.classList.add('falling-leaf');
260
// 이미지, 위치, 크기 랜덤 설정
261
const randomIndex = Math.floor(Math.random() * imageSources.length);
262
leaf.style.backgroundImage = `url('${imageSources[randomIndex]}')`;
263
264
leaf.style.left = `${randomBetween(-10, 110)}vw`;
265
const size = randomBetween(200, 350);
266
leaf.style.width = `${size}px`;
267
leaf.style.height = `${size}px`;
268
269
const duration = randomBetween(15, 25);
270
leaf.style.animationDuration = `${duration}s`;
271
272
// 자연스러운 시작을 위해 animationDelay 사용 (초기 딜레이만)
273
leaf.style.animationDelay = `${randomBetween(0, 1.2)}s`;
274
275
leaf.addEventListener('animationend', () => {
276
leaf.remove();
277
// 애니메이션이 끝난 뒤 새로 spawn (무한 루프)
278
spawnLeaf();
279
});
280
281
container.appendChild(leaf);
282
}
283
284
for (let i = 0; i < leafCount; i++) {
285
spawnLeaf();
286
}
287
});
288
</script>
289
<script>if ("serviceWorker" in navigator) { navigator.serviceWorker.register("/sw.js"); }</script>
290
</body>
291
</html>