Public Source Viewer

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

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

Redacted View
view/hinana/signup.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 <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
20 <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js" integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh" crossorigin="anonymous"></script>
21 <title>비나래 아카이브</title>
22 <style>
23 /* [Theme Variables] */
24 :root {
25 --font-family: 'Noto Sans KR', sans-serif;
26 /* Light Mode */
27 --bg-main: #e4dfd7; --bg-secondary: #f2f0eb; --bg-tertiary: #dcd6ce;
28 --text-primary: #3e3a36; --text-secondary: #69615c;
29 --accent-color: #b45309; --danger-color: #dc2626; --border-color: #ccc6bc;
30 --shadow-sm: 0 1px 2px 0 rgba(60, 50, 40, 0.08);
31 }
32 /* Dark Mode */
33 body.dark-mode {
34 --bg-main: #1c1917; --bg-secondary: #292524; --bg-tertiary: #44403c;
35 --text-primary: #e7e5e4; --text-secondary: #a8a29e;
36 --border-color: #44403c; --accent-color: #f59e0b;
37 --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.5);
38 }
39
40 /* 기본 설정 */
41 html, body {
42 height: 100%; margin: 0; padding: 0;
43 font-family: var(--font-family);
44 background-color: var(--bg-main);
45 color: var(--text-primary);
46 overflow: hidden;
47 }
48 a { text-decoration: none; color: inherit; }
49
50 /* [Header] */
51 .global-header {
52 height: 60px; background-color: var(--bg-tertiary); border-bottom: 1px solid var(--border-color);
53 display: flex; align-items: center; justify-content: space-between; padding: 0 20px;
54 position: sticky; top: 0; z-index: 1000;
55 }
56 .header-brand { display: flex; align-items: center; gap: 15px; }
57 .header-logo { height: 28px; width: auto; mix-blend-mode: multiply; }
58 body.dark-mode .header-logo { mix-blend-mode: screen; }
59
60 .header-nav {
61 position: absolute; left: 50%; transform: translateX(-50%);
62 display: flex; gap: 20px; align-items: center; z-index: 5;
63 }
64 .nav-link { font-weight: 600; font-size: 0.95rem; color: var(--text-secondary); cursor: pointer; }
65 .nav-link:hover { color: var(--accent-color); }
66 .nav-link.active { color: var(--text-primary); }
67 .nav-divider { opacity: 0.3; color: var(--text-secondary); }
68 .login-link { color: var(--accent-color); font-weight: bold; }
69
70 .header-controls { display: flex; align-items: center; gap: 10px; z-index: 10; position: relative; background-color: var(--bg-tertiary); }
71 .icon-btn { border: none; background: transparent; color: var(--text-secondary); font-size: 1.1rem; cursor: pointer; padding: 4px; }
72 .icon-btn:hover { color: var(--text-primary); }
73
74 /* [Layout] */
75 .layout-container { display: flex; height: calc(100vh - 60px); }
76 .shelf-column { width: 300px; background-color: var(--bg-secondary); border-right: 1px solid var(--border-color); display: flex; flex-direction: column; justify-content: center; align-items: center; }
77
78 /* [Center Content] */
79 .content-column {
80 flex: 1; display: flex; flex-direction: column;
81 background-color: var(--bg-main); position: relative; overflow: hidden;
82 align-items: center; justify-content: center;
83 }
84
85 .login-card {
86 width: 100%; max-width: 400px;
87 background-color: var(--bg-secondary);
88 border: 1px solid var(--border-color);
89 border-radius: 12px;
90 box-shadow: var(--shadow-sm);
91 padding: 40px;
92 text-align: center;
93 }
94
95 .login-title {
96 font-size: 1.5rem; font-weight: 700; color: var(--accent-color); margin-bottom: 30px;
97 letter-spacing: -0.5px;
98 }
99
100 .form-control {
101 background-color: var(--bg-main); border: 1px solid var(--border-color);
102 color: var(--text-primary); padding: 12px; border-radius: 8px; margin-bottom: 5px;
103 }
104 .form-control:focus {
105 border-color: var(--accent-color); outline: none; box-shadow: 0 0 0 2px rgba(180, 83, 9, 0.1);
106 }
107 .form-text { font-size: 0.75rem; color: var(--text-secondary); text-align: left; margin-bottom: 15px; margin-left: 4px; }
108
109 .btn-login {
110 width: 100%; padding: 12px; border-radius: 8px; font-weight: bold;
111 background-color: var(--accent-color); border: none; color: white;
112 transition: opacity 0.2s; margin-top: 10px;
113 }
114 .btn-login:hover { opacity: 0.9; }
115
116 .btn-back {
117 display: block; width: 100%; margin-top: 15px;
118 color: var(--text-secondary); text-decoration: none; font-size: 0.9rem;
119 }
120 .btn-back:hover { color: var(--text-primary); text-decoration: underline; }
121
122 /* [Right Sidebar] */
123 .info-column { width: 260px; background-color: var(--bg-secondary); border-left: 1px solid var(--border-color); padding: 30px 20px; display: flex; flex-direction: column; gap: 30px; }
124 .info-card { background-color: var(--bg-main); border-radius: 12px; padding: 20px; border: 1px solid var(--border-color); box-shadow: var(--shadow-sm); }
125 .info-card-title { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; color: var(--text-secondary); margin-bottom: 10px; }
126
127 .theme-toggle-wrapper { display: flex; align-items: center; justify-content: space-between; }
128 .switch { position: relative; display: inline-block; width: 40px; height: 22px; vertical-align: middle; }
129 .switch input { opacity: 0; width: 0; height: 0; }
130 .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 34px; }
131 .slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; }
132 input:checked + .slider { background-color: var(--accent-color); }
133 input:checked + .slider:before { transform: translateX(18px); }
134
135 .info-column .footer, .info-column footer.footer {
136 background-color: transparent !important; box-shadow: none !important;
137 border: none !important; margin-top: 20px; color: var(--text-secondary) !important; text-align: center;
138 }
139 .footer-logo { width: 80px; opacity: 0.5; mix-blend-mode: multiply; margin-bottom: 8px; }
140 body.dark-mode .footer-logo { mix-blend-mode: screen; }
141
142 /* [반응형 수정: 검은 바 제거 및 레이아웃 정리] */
143 @media (max-width: 960px) {
144 html, body { overflow-y: auto !important; height: auto !important; padding-top: 0 !important; }
145 .global-header { position: fixed !important; top: 0; left: 0; right: 0; z-index: 10000 !important; height: auto !important; min-height: 60px; padding: 10px 15px; flex-wrap: wrap; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
146
147 .header-nav { position: static !important; transform: none !important; gap: 10px; font-size: 0.9rem; order: 3; width: 100%; justify-content: center; margin-top: 5px; padding-top: 5px; border-top: 1px solid rgba(0,0,0,0.05); }
148 .header-left { flex: auto; order: 1; }
149 .header-controls { flex: auto; order: 2; margin-left: auto; justify-content: flex-end; }
150
151 .layout-container {
152 flex-direction: column;
153 height: auto !important;
154 margin-top: 0 !important;
155 /* [수정] 상단 여백을 70~80px로 줄여서 헤더와 본문 사이 검은 바 제거 */
156 padding-top: 75px !important;
157 min-height: calc(100vh - 60px);
158 }
159
160 .shelf-column { display: none; }
161 .content-column { width: 100%; height: auto !important; min-height: 500px; flex: none; order: 1; border: none; padding: 40px 20px; }
162
163 /* 우측 패널 하단 배치 */
164 .info-column {
165 display: flex !important; width: 100%; height: auto;
166 border-left: none; border-top: 1px solid var(--border-color);
167 order: 2; padding: 30px 20px;
168 flex-direction: column; gap: 20px;
169 }
170 .info-card { width: 100%; margin-bottom: 0; }
171 .info-column .footer { display: block !important; width: 100%; text-align: center; margin-top: 20px; }
172 }
173 .d-none { display: none !important; }
174 </style>
175 </head>
176
177 <body class="<%= (typeof theme !== 'undefined' && theme === 'dark') ? 'dark-mode' : '' %>">
178
179 <header class="global-header">
180 <div class="header-brand">
181 <a href="/hinana/index">
182 <img src="/image/<%= (typeof theme !== 'undefined' && theme === 'dark') ? 'archive1.png' : 'archive.png' %>"
183 alt="Hinana Archive" class="header-logo">
184 </a>
185 </div>
186 <nav class="header-nav">
187 <a href="/hinana/index" class="nav-link">Archive</a>
188 <a href="/hinana/info" class="nav-link">Info</a>
189 <a href="/hinana/blog" class="nav-link">Blog</a>
190 <a href="/hinana/lounge" class="nav-link">Lounge</a>
191 <span class="nav-divider">|</span>
192 <a href="/login" class="nav-link login-link fw-bold">Login</a>
193 </nav>
194 <div class="header-controls">
195 <a href="/hinana/gallery#brand-assets" class="nav-link" style="font-size:0.9rem;">사이트 맵</a>
196 <form action="/toggle-theme" method="POST" class="d-inline">
197 <button type="submit" class="icon-btn" title="테마 변경">
198 <i class="bi <%= (typeof theme !== 'undefined' && theme==='dark') ? 'bi-moon-stars-fill':'bi-sun-fill' %>"></i>
199 </button>
200 </form>
201 </div>
202 </header>
203
204 <div class="layout-container">
205
206 <div class="shelf-column">
207 <div class="text-center">
208 <a href="/hinana/index" class="btn btn-outline-secondary btn-sm"><i class="bi bi-arrow-left"></i> 홈으로</a>
209 </div>
210 </div>
211
212 <div class="content-column">
213 <div class="login-card">
214 <h2 class="login-title">SIGN UP</h2>
215 <form action="/signup" method="POST" onsubmit="return validateForm()">
216 <div class="mb-3 text-start">
217 <label for="username" class="form-label small text-secondary fw-bold">USERNAME</label>
218 <input type="text" class="form-control" id="username" name="username"
219 required maxlength="20" pattern="^[a-zA-Z0-9_]+$"
220 placeholder="20자 이내 (영문, 숫자)" autocomplete="off">
221 <div class="form-text">* 영문, 숫자, 밑줄(_)만 사용 가능 (최대 20자)</div>
222 </div>
223 <div class="mb-4 text-start">
224 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
225 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
226 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
227 <div class="form-text">* 보안을 위해 4자리 이상 입력해주세요.</div>
228 </div>
229 <div class="mb-4 text-start">
230 <label for="code" class="form-label small text-secondary fw-bold">SIGN UP CODE</label>
231 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
232 required placeholder="가입 코드" autocomplete="off">
233 </div>
234 <button type="submit" class="btn-login" id="btn-submit">가입하기</button>
235 <a href="/login" class="btn-back">이미 계정이 있으신가요? 로그인</a>
236 </form>
237 </div>
238 </div>
239
240 <div class="info-column">
241 <div class="info-card">
242 <div class="info-card-title">Settings</div>
243 <div class="theme-toggle-wrapper">
244 <span class="d-flex align-items-center gap-2 small text-secondary">
245 <i class="bi <%= (typeof theme!=='undefined'&&theme==='dark')?'bi-moon-stars-fill':'bi-sun-fill' %>"></i>
246 <%= (typeof theme !== 'undefined' && theme === 'dark') ? 'Dark Mode' : 'Light Mode' %>
247 </span>
248 <form action="/toggle-theme" method="POST" id="theme-form">
249 <label class="switch" style="transform:scale(0.8);">
250 <input type="checkbox" <%= (typeof theme!=='undefined'&&theme==='dark')?'checked':'' %> onchange="document.getElementById('theme-form').submit()">
251 <span class="slider"></span>
252 </label>
253 </form>
254 </div>
255 </div>
256 <div class="info-card">
257 <div class="info-card-title">System Info</div>
258 <ul class="small text-secondary list-unstyled mb-0">
259 <li class="mb-1 d-flex justify-content-between"><span>Version</span></li>
260 <li class="mb-1 d-flex justify-content-between"><span> Ver. 6.5.4.0-Kozeki Ui</span></li>
261 </ul>
262 </div>
263 <div style="text-align:center; margin-top: 40px;" class="footer">
264 <img src="/image/sign.png" id="fumika_sign" style="max-width:200px; width:80%; opacity:0.8; mix-blend-mode:multiply;" />
265 </div>
266 <footer class="text-center footer mt-2">
267 <p style="margin-bottom: 0rem; font-size: 0.8rem; color: var(--text-secondary);">X - @NoctchillHinana</p>
268 <p style="margin-bottom: 0rem; font-size: 0.8rem; color: var(--text-secondary);">ⓒ 2024~2026. 비나래 | hinana.moe All rights reserved.</p>
269 </footer>
270 </div>
271 </div>
272
273 <script>
274 // 서버에서 테마가 안 넘어왔을 때 쿠키 확인
275 function getCookie(name) {
276 const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
277 return match ? match[2] : null;
278 }
279
280 const savedTheme = getCookie('theme');
281 if (savedTheme === 'dark') {
282 document.body.classList.add('dark-mode');
283 }
284
285 // 서명 이미지 블렌드 모드
286 if (document.body.classList.contains('dark-mode')) {
287 const sign = document.getElementById('fumika_sign');
288 if(sign) sign.style.mixBlendMode = 'screen';
289 }
290
291 function validateForm() {
292 const btn = document.getElementById('btn-submit');
293 btn.disabled = true;
294 btn.innerText = '처리 중...';
295 return true;
296 }
297 </script>
298 <script>if ("serviceWorker" in navigator) { navigator.serviceWorker.register("/sw.js"); }</script>
299 </body>
300 </html>
301