Public Source Viewer
비나래아카이브 개발자 포털
실제 서비스 구조를 살펴볼 수 있는 공개용 코드 뷰어입니다. 인증, 세션, 외부 연동, 토큰, 관리자 식별 등 보안상 민감한 구현은 파일 단위 또는 줄 단위로 검열됩니다.
view/hinana/plaza.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, user-scalable=no, maximum-scale=1.0">
7
<meta name="theme-color" content="<%= (typeof theme !== 'undefined' && theme === 'dark') ? '#0f141e' : '#f8f7f5' %>">
8
<meta name="apple-mobile-web-app-title" content="비나래 라운지">
9
<meta property="og:image" content="/image/train_hinana.png" />
10
<meta property="og:description" content="비나래 라운지 | 광장"/>
11
<meta property="og:url" content="hinana.moe/hinana/lounge/plaza"/>
12
<meta property="og:title" content="비나래 라운지"/>
13
<title>비나래 라운지 | 광장</title>
14
15
<link rel='stylesheet' href='/vendors/bootstrap/css/bootstrap.min.css' />
16
<script src="/vendors/bootstrap/js/bootstrap.min.js"></script>
17
<link href="https://fonts.googleapis.com/css?family=Montserrat:400,700" rel="stylesheet" type="text/css">
18
<link href="https://fonts.googleapis.com/css?family=Lato:300,400,700" rel="stylesheet" type="text/css">
19
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css">
20
<script src="/js/popup.js"></script>
21
22
<style>
23
:root {
24
--font-family: 'Noto Sans KR', sans-serif;
25
--bg-main: #f8f7f5; --bg-secondary: #ffffff; --bg-tertiary: #1a2238;
26
--text-primary: #1a2238; --text-secondary: #5e6676;
27
--accent-color: #c5a059; --border-color: #e5e1da;
28
--shadow-md: 0 10px 40px -10px rgba(26, 34, 56, 0.12);
29
--shadow-sm: 0 2px 8px rgba(0,0,0,0.05);
30
}
31
32
body.dark-mode {
33
--bg-main: #0f141e; --bg-secondary: #1a2238; --bg-tertiary: #0a0e17;
34
--text-primary: #e7e5e4; --text-secondary: #a8a29e;
35
--accent-color: #d4b47a; --border-color: #2e3a59;
36
}
37
38
/* 기본 설정 */
39
html, body {
40
height: auto !important; min-height: 100%; margin: 0; padding: 0;
41
font-family: var(--font-family); background-color: var(--bg-main); color: var(--text-primary);
42
overflow-x: hidden; overflow-y: auto; width: 100%;
43
}
44
a { text-decoration: none; color: inherit; }
45
* { box-sizing: border-box; } /* [중요] 패딩이 너비에 포함되도록 강제 */
46
47
/* [Header] */
48
.global-header {
49
height: 60px; background-color: var(--bg-tertiary); border-bottom: 1px solid var(--border-color);
50
display: flex; align-items: center; justify-content: space-between; padding: 0 40px;
51
position: sticky; top: 0; z-index: 1000; color: white; flex-wrap: wrap;
52
}
53
.header-logo { height: 32px; filter: none !important; -webkit-filter: none !important; mix-blend-mode: normal !important; }
54
.header-brand { display: flex; align-items: center; flex: 0 0 auto; }
55
.header-nav { display: flex; gap: 20px; align-items: center; transition: all 0.3s ease; }
56
57
/* [Layout] */
58
.layout-container {
59
display: flex;
60
min-height: calc(100vh - 60px);
61
background-color: var(--bg-main);
62
width: 100%; max-width: 100vw; overflow-x: hidden;
63
}
64
65
/* [Left Content Column] */
66
.content-column {
67
flex: 1; padding: 60px 40px;
68
display: flex; flex-direction: column; align-items: center;
69
width: 100%;
70
/* [중요] 화면이 좁아질 때 컨텐츠 영역이 줄어들 수 있게 허용 */
71
min-width: 0;
72
}
73
74
.plaza-hero { text-align: center; margin-bottom: 40px; }
75
.info-title { font-size: 2.5rem; font-weight: 700; color: var(--text-primary); margin: 10px 0; letter-spacing: -1px; }
76
77
.input-card {
78
width: 100%; max-width: 800px; background-color: var(--bg-secondary);
79
padding: 25px; border-radius: 12px; border: 1px solid var(--border-color);
80
border-top: 4px solid var(--accent-color) !important; box-shadow: var(--shadow-md);
81
margin-bottom: 50px;
82
}
83
.plaza-textarea {
84
width: 100%; height: 100px; border: 1px solid var(--border-color);
85
background: var(--bg-main); color: var(--text-primary);
86
padding: 15px; border-radius: 8px; resize: none; outline: none; transition: border-color 0.2s;
87
}
88
.plaza-textarea:focus { border-color: var(--accent-color); }
89
90
.plaza-grid {
91
width: 100%; max-width: 1000px;
92
display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
93
gap: 20px;
94
}
95
.message-card {
96
background: var(--bg-secondary); border: 1px solid var(--border-color);
97
padding: 20px; border-radius: 12px; position: relative;
98
transition: transform 0.2s, box-shadow 0.2s; box-shadow: var(--shadow-sm);
99
}
100
.message-card:hover { transform: translateY(-5px); border-color: var(--accent-color); box-shadow: var(--shadow-md); }
101
.msg-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
102
.msg-user { font-weight: 700; color: var(--accent-color); font-size: 0.95rem; }
103
.msg-timer { font-size: 0.65rem; color: #dc3545; font-weight: bold; border: 1px solid #dc3545; padding: 3px 8px; border-radius: 20px; display: flex; align-items: center; gap: 4px; }
104
.msg-content { font-size: 0.95rem; line-height: 1.6; white-space: pre-wrap; color: var(--text-primary); }
105
106
/* [Right Sidebar - 완전 고정] */
107
.info-column {
108
/* [해결 1] flex-shrink: 0 을 추가하여 절대 찌그러지거나 밀리지 않게 함 */
109
flex: 0 0 300px;
110
width: 300px;
111
background-color: var(--bg-secondary);
112
border-left: 1px solid var(--border-color);
113
padding: 30px;
114
display: flex; flex-direction: column;
115
}
116
.info-card { background-color: var(--bg-main); border: 1px solid var(--border-color); border-radius: 8px; margin-bottom: 20px; }
117
118
/* [Responsive - 해결 2] 분기점을 1200px로 상향 조정 */
119
@media (max-width: 1200px) {
120
.global-header { padding: 10px 20px; }
121
122
/* 레이아웃을 세로 모드로 전환 */
123
.layout-container { flex-direction: column; align-items: center; }
124
125
.content-column { width: 100%; padding: 40px 20px; }
126
127
/* 우측 패널을 하단으로 이동하고 너비 100%로 확장 */
128
.info-column {
129
width: 100%;
130
flex: auto; /* 고정 해제 */
131
border-left: none; border-top: 1px solid var(--border-color);
132
padding: 40px 20px;
133
}
134
.info-column > div:last-child { margin-bottom: 60px; }
135
}
136
137
@media (max-width: 991px) {
138
.global-header { height: auto; min-height: 70px; }
139
.header-nav { order: 2; gap: 15px !important; }
140
.header-controls {
141
width: 100%; order: 3; display: flex; justify-content: flex-end;
142
margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.1);
143
}
144
.header-brand { order: 1; }
145
}
146
147
/* 기타 스타일 (삭제 버튼, 페이지네이션) */
148
.btn-delete { background: none; border: none; color: var(--text-secondary); font-size: 0.8rem; padding: 2px 5px; cursor: pointer; transition: color 0.2s; display: flex; align-items: center; }
149
.btn-delete:hover { color: #dc3545; }
150
.pagination-container { margin-top: 40px; display: flex; justify-content: center; gap: 8px; }
151
.page-link-custom { display: flex; align-items: center; justify-content: center; min-width: 36px; height: 36px; padding: 0 10px; border: 1px solid var(--border-color); background-color: var(--bg-secondary); color: var(--text-primary); border-radius: 8px; font-size: 0.9rem; font-weight: 600; transition: all 0.2s; text-decoration: none; }
152
.page-link-custom:hover { border-color: var(--accent-color); color: var(--accent-color); transform: translateY(-2px); box-shadow: var(--shadow-sm); }
153
.page-link-custom.active { background-color: var(--accent-color); color: var(--ui-on-accent, #fff); border-color: var(--accent-color); }
154
.page-link-custom.disabled { opacity: 0.5; pointer-events: none; background-color: var(--bg-main); }
155
.verified-badge { color: #1d9bf0; font-size: 0.85em; margin-left: 2px; }
156
.verified-badge-admin { color: var(--accent-color); font-size: 0.85em; margin-left: 2px; }
157
</style>
158
<link rel="stylesheet" href="/css/lounge-modern.css">
159
<link rel="stylesheet" href="/css/site-ui.css?v=3">
160
<link rel="stylesheet" href="/css/site-app.css?v=1">
161
</head>
162
163
<body data-ui-page="plaza" class="ui-shell lounge-fixed-nav <%= (typeof theme !== 'undefined' && theme === 'dark') ? 'dark-mode' : '' %>">
164
<%- include('partials/site-header', { uiPage: 'plaza' }) %>
165
166
<div id="ui-content" tabindex="-1" role="main" class="layout-container">
167
<div class="content-column">
168
<div class="plaza-hero">
169
<span style="color: var(--accent-color); letter-spacing: 5px; font-weight: bold; font-size: 0.8rem;">TEMPORARY SQUARE</span>
170
<h2 class="info-title">비나래 광장</h2>
171
<div style="width: 60px; height: 1px; background: var(--accent-color); margin: 0 auto;"></div>
172
<p class="text-secondary small mt-3">이곳의 이야기는 24시간 후 바람처럼 사라집니다.</p><p class="text-secondary small">그래도 히나나가 들렀다 가기엔 충분한 시간이죠.</p>
173
</div>
174
175
<div class="input-card">
176
<form action="/hinana/plaza/post" method="POST">
177
<textarea
178
name="content"
179
class="plaza-textarea"
180
placeholder="광장에 남길 메시지를 적어주세요... (최대 300자)"
181
maxlength="300"
182
oninput="updateCharCount(this)"
183
></textarea>
184
185
<div class="text-end mt-1 mb-2" style="font-size: 0.75rem; color: var(--text-secondary);">
186
<span id="current-count">0</span> / 300
187
</div>
188
189
<div class="d-flex justify-content-between align-items-center mt-2">
190
<div class="d-flex gap-2 align-items-center">
191
<% if(!username) { %>
192
<input type="text" name="anonymousUsername" class="form-control form-control-sm" placeholder="닉네임" style="width: 120px;">
193
<input type="hidden" name="isAnonymous" value="true">
194
<% } else { %>
195
<span class="badge bg-secondary"><%= username %></span>
196
<% } %>
197
</div>
198
<button type="submit" class="btn btn-dark btn-sm px-4" style="background: var(--accent-color); border: none;">광장 한 편으로</button>
199
</div>
200
</form>
201
</div>
202
203
<div class="plaza-grid">
204
<% messages.forEach(msg => {
205
/* [수정] 남은 시간 실시간 계산 로직 추가 */
206
const now = new Date();
207
const posted = new Date(msg.timestamp);
208
const elapsed = now - posted; // 흐른 시간 (ms)
209
const lifeTime = 24 * 60 * 60 * 1000; // 수명 24시간 (ms)
210
const remaining = lifeTime - elapsed; // 남은 시간 (ms)
211
212
// 음수 방지 (삭제 직전 0으로 표시)
213
const safeRemaining = remaining > 0 ? remaining : 0;
214
215
const rHours = Math.floor(safeRemaining / (1000 * 60 * 60));
216
const rMinutes = Math.floor((safeRemaining % (1000 * 60 * 60)) / (1000 * 60));
217
%>
218
<div class="message-card">
219
<div class="msg-header">
220
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
221
222
<div class="d-flex align-items-center gap-2">
223
<span class="msg-timer" title="남은 시간">
224
<i class="bi bi-hourglass-split"></i>
225
<%= rHours %>H <%= rMinutes %>M
226
</span>
227
228
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
229
<form action="/hinana/plaza/delete" method="POST" data-confirm="정말 이 기록을 지우시겠습니까?" style="margin:0;">
230
<input type="hidden" name="id" value="<%= msg.id %>">
231
<button type="submit" class="btn-delete" title="삭제">
232
<i class="bi bi-trash3-fill"></i>
233
</button>
234
</form>
235
<% } %>
236
</div>
237
</div>
238
239
<div class="msg-content"><%= msg.content %></div>
240
241
<div class="mt-3 pt-2 border-top" style="font-size: 0.7rem; color: var(--text-secondary); border-color: var(--border-color) !important;">
242
<%= fmtDate(msg.timestamp) %>
243
</div>
244
</div>
245
<% }) %>
246
</div>
247
<% if (totalPages > 1) { %>
248
<div class="pagination-container">
249
<% if (currentPage > 1) { %>
250
<a href="?page=<%= currentPage - 1 %>" class="page-link-custom">
251
<i class="bi bi-chevron-left"></i>
252
</a>
253
<% } else { %>
254
<span class="page-link-custom disabled"><i class="bi bi-chevron-left"></i></span>
255
<% } %>
256
257
<%
258
let startPage = Math.max(1, currentPage - 2);
259
let endPage = Math.min(totalPages, startPage + 4);
260
if (endPage - startPage < 4) {
261
startPage = Math.max(1, endPage - 4);
262
}
263
%>
264
265
<% for(let i = startPage; i <= endPage; i++) { %>
266
<a href="?page=<%= i %>" class="page-link-custom <%= currentPage === i ? 'active' : '' %>">
267
<%= i %>
268
</a>
269
<% } %>
270
271
<% if (currentPage < totalPages) { %>
272
<a href="?page=<%= currentPage + 1 %>" class="page-link-custom">
273
<i class="bi bi-chevron-right"></i>
274
</a>
275
<% } else { %>
276
<span class="page-link-custom disabled"><i class="bi bi-chevron-right"></i></span>
277
<% } %>
278
</div>
279
<% } %>
280
</div>
281
282
<div class="info-column">
283
<div class="info-card p-4 mb-4 text-center">
284
<div style="font-size: 0.65rem; color: var(--accent-color); letter-spacing: 3px; font-weight: 700; margin-bottom: 20px;">PASSENGER INFO</div>
285
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
286
<div>
287
<% if(username) { %>
288
<a href="/logout?redirect=/hinana/plaza" class="btn btn-outline-dark btn-sm w-100 py-2">SIGN OUT</a>
289
<% } else { %>
290
<a href="/login?redirect=/hinana/plaza" class="btn btn-dark btn-sm w-100 py-2">SIGN IN</a>
291
<% } %>
292
</div>
293
</div>
294
295
<div class="info-card p-4 mb-4">
296
<div style="font-size: 0.65rem; color: var(--accent-color); letter-spacing: 3px; font-weight: 700; margin-bottom: 15px;">SYSTEM INFO</div>
297
<ul class="small text-secondary list-unstyled mb-0">
298
<li class="mb-1 d-flex justify-content-between">
299
<span>Version</span>
300
<span class="text-end">Ver. 9.2.1.0 - Iochi Mari</span>
301
</li>
302
</ul>
303
</div>
304
305
</div>
306
</div>
307
308
<footer class="site-footer">
309
<div class="footer-brand">
310
<img src="/image/sign.png" alt="비나래" class="footer-logo">
311
<div>
312
<div class="footer-title">비나래 라운지</div>
313
<div>Archive My Lounge</div>
314
</div>
315
</div>
316
<div>
317
<div class="footer-links">
318
<a href="/hinana/index">Archive</a>
319
<a href="/hinana/lounge">Lounge</a>
320
<a href="/hinana/gallery">Gallery</a>
321
<a href="/hinana/plaza">Plaza</a>
322
<a href="https://x.com/NoctchillHinana" target="_blank" rel="noreferrer">X</a>
323
</div>
324
<div class="footer-copy"><span class="footer-version">Ver. 9.2.1.0 - Iochi Mari</span>ⓒ 2024~2026. 비나래 | hinana.moe</div>
325
</div>
326
</footer>
327
328
<script>
329
function updateCharCount(textarea) {
330
const currentLength = textarea.value.length;
331
const counter = document.getElementById('current-count');
332
333
// 글자 수 업데이트
334
counter.innerText = currentLength;
335
336
// (선택사항) 300자 꽉 차면 빨간색으로 경고
337
if (currentLength >= 300) {
338
counter.style.color = '#dc3545'; // 빨간색
339
counter.style.fontWeight = 'bold';
340
} else {
341
counter.style.color = ''; // 기본색 복귀
342
counter.style.fontWeight = 'normal';
343
}
344
}
345
</script>
346
</body>
347
</html>
348