Public Source Viewer

비나래아카이브 개발자 포털

실제 서비스 구조를 살펴볼 수 있는 공개용 코드 뷰어입니다. 인증, 세션, 외부 연동, 토큰, 관리자 식별 등 보안상 민감한 구현은 파일 단위 또는 줄 단위로 검열됩니다.

Redacted View
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>