Public Source Viewer
비나래아카이브 개발자 포털
실제 서비스 구조를 살펴볼 수 있는 공개용 코드 뷰어입니다. 인증, 세션, 외부 연동, 토큰, 관리자 식별 등 보안상 민감한 구현은 파일 단위 또는 줄 단위로 검열됩니다.
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