Public Source Viewer
비나래아카이브 개발자 포털
실제 서비스 구조를 살펴볼 수 있는 공개용 코드 뷰어입니다. 인증, 세션, 외부 연동, 토큰, 관리자 식별 등 보안상 민감한 구현은 파일 단위 또는 줄 단위로 검열됩니다.
view/hinana/userInfo.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="비나래 아카이브"/>
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:400,700" rel="stylesheet" type="text/css">
17
<link href="https://fonts.googleapis.com/css?family=Lato:300,400,700" rel="stylesheet" type="text/css">
18
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css">
19
<script src="/js/popup.js"></script>
20
<title>내 정보 - 비나래 아카이브</title>
21
22
<style>
23
:root {
24
--font-family: 'Noto Sans KR', sans-serif;
25
--bg-main: #ffffff; --bg-secondary: #f7f9f9; --bg-tertiary: #eff3f4;
26
--text-primary: #0f1419; --text-secondary: #536471;
27
--accent-color: #1d9bf0; --danger-color: #f4212e; --border-color: #eff3f4;
28
--success-color: #00ba7c; --warning-color: #ffd400;
29
--shadow-sm: 0 1px 2px 0 rgba(15, 20, 25, 0.06);
30
--shadow-md: 0 8px 24px rgba(15, 20, 25, 0.08);
31
}
32
body.dark-mode {
33
--bg-main: #000000; --bg-secondary: #16181c; --bg-tertiary: #202327;
34
--text-primary: #e7e9ea; --text-secondary: #71767b;
35
--accent-color: #1d9bf0; --border-color: #2f3336;
36
--success-color: #00ba7c; --warning-color: #ffd400;
37
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.5);
38
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.7);
39
}
40
41
html, body { margin: 0; min-height: 100vh; font-family: var(--font-family); background-color: var(--bg-main); color: var(--text-primary); }
42
a { text-decoration: none; color: inherit; }
43
44
.global-header {
45
height: 60px; background-color: rgba(255, 255, 255, 0.9); border-bottom: 1px solid var(--border-color);
46
display: flex; align-items: center; justify-content: space-between; padding: 0 20px;
47
position: sticky; top: 0; z-index: 1000; backdrop-filter: blur(12px);
48
}
49
body.dark-mode .global-header { background-color: rgba(0, 0, 0, 0.86); }
50
.header-brand { display: flex; align-items: center; }
51
.header-logo { height: 28px; width: auto; mix-blend-mode: multiply; }
52
body.dark-mode .header-logo { mix-blend-mode: screen; }
53
.header-nav {
54
position: absolute; left: 50%; transform: translateX(-50%);
55
display: flex; gap: 20px; align-items: center;
56
}
57
.nav-link { font-weight: 600; font-size: 0.95rem; color: var(--text-secondary); cursor: pointer; transition: color 0.2s; }
58
.nav-link:hover { color: var(--accent-color); }
59
.nav-link.active { color: var(--text-primary); }
60
.nav-divider { opacity: 0.3; color: var(--text-secondary); }
61
.header-controls { display: flex; align-items: center; gap: 10px; z-index: 10; position: relative; }
62
.icon-btn { border: none; background: transparent; color: var(--text-secondary); font-size: 1.1rem; cursor: pointer; padding: 4px; transition: color 0.2s; }
63
.icon-btn:hover { color: var(--text-primary); }
64
65
.content-area { max-width: 920px; margin: 34px auto; padding: 0 20px; }
66
67
.info-card {
68
background-color: var(--bg-secondary); border: 1px solid var(--border-color);
69
border-radius: 8px; box-shadow: var(--shadow-sm); padding: 28px; margin-bottom: 18px;
70
}
71
.info-card h5 { font-size: 0.8rem; font-weight: 700; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 10px; }
72
.info-card .value { font-size: 1.4rem; font-weight: 700; color: var(--text-primary); }
73
74
.bookmark-card {
75
background: var(--bg-secondary);
76
border: 1px solid var(--border-color); border-left: 4px solid var(--accent-color);
77
border-radius: 8px; box-shadow: var(--shadow-sm); padding: 24px 28px; margin-bottom: 18px;
78
display: flex; align-items: center; gap: 20px;
79
}
80
.bookmark-icon { font-size: 2rem; color: var(--accent-color); }
81
.bookmark-label { font-size: 0.8rem; font-weight: 700; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; }
82
.bookmark-value { font-size: 1.8rem; font-weight: 800; color: var(--accent-color); }
83
.bookmark-sub { font-size: 0.75rem; color: var(--text-secondary); margin-top: 2px; }
84
85
.section-title {
86
font-size: 1.2rem; font-weight: 700; color: var(--text-primary);
87
margin: 40px 0 15px; padding-bottom: 10px; border-bottom: 1px solid var(--border-color);
88
}
89
.section-title i { color: var(--accent-color); margin-right: 8px; }
90
91
.admin-card {
92
background-color: var(--bg-secondary); border: 1px solid var(--border-color);
93
border-radius: 8px; box-shadow: var(--shadow-sm); padding: 24px 28px; margin-bottom: 15px;
94
}
95
.admin-card h5 { font-size: 0.95rem; font-weight: 700; color: var(--text-primary); margin-bottom: 8px; }
96
97
.table { color: var(--text-primary); background-color: var(--bg-secondary); }
98
.table thead { background-color: var(--bg-tertiary); }
99
.table thead th { color: var(--text-primary); border-color: var(--border-color); font-size: 0.85rem; }
100
.table td { border-color: var(--border-color); vertical-align: middle; color: var(--text-primary); }
101
.table-striped > tbody > tr:nth-of-type(odd) { background-color: var(--bg-main); }
102
.table-striped > tbody > tr:nth-of-type(even) { background-color: var(--bg-secondary); }
103
104
.btn-accent {
105
background-color: var(--accent-color); color: white; border: none;
106
padding: 8px 20px; border-radius: 8px; font-weight: 600; font-size: 0.85rem; cursor: pointer; transition: opacity 0.2s;
107
}
108
.btn-accent:hover { opacity: 0.85; color: white; }
109
110
.btn-back {
111
border: 1px solid var(--border-color); background: transparent;
112
color: var(--text-secondary); padding: 10px 25px; border-radius: 8px; font-weight: 600; cursor: pointer; transition: all 0.2s;
113
}
114
.btn-back:hover { border-color: var(--accent-color); color: var(--accent-color); }
115
116
.pagination-bar {
117
display: flex; align-items: center; justify-content: center; gap: 6px;
118
margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--border-color);
119
}
120
.page-btn {
121
min-width: 34px; height: 34px; border: 1px solid var(--border-color); border-radius: 6px;
122
background: var(--bg-main); color: var(--text-primary); font-size: 0.85rem; font-weight: 600;
123
cursor: pointer; display: flex; align-items: center; justify-content: center;
124
transition: all 0.15s; font-family: inherit; padding: 0 8px;
125
}
126
.page-btn:hover { border-color: var(--accent-color); color: var(--accent-color); }
127
.page-btn.active { background: var(--accent-color); color: white; border-color: var(--accent-color); }
128
.page-btn:disabled { opacity: 0.4; cursor: default; }
129
130
.badge-on { background-color: var(--success-color); color: #fff; }
131
.badge-off { background-color: var(--danger-color); color: #fff; }
132
133
.security-log-list { display: grid; gap: 10px; }
134
.security-log-row {
135
display: grid; grid-template-columns: 160px 1fr; gap: 14px;
136
padding: 14px; border: 1px solid var(--border-color); border-radius: 8px;
137
background: var(--bg-main);
138
}
139
.log-time { font-size: 0.78rem; color: var(--text-secondary); line-height: 1.5; }
140
.log-main { min-width: 0; }
141
.log-head { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-bottom: 5px; }
142
.log-badge {
143
display: inline-flex; align-items: center; border-radius: 999px; padding: 3px 8px;
144
font-size: 0.7rem; font-weight: 800; letter-spacing: 0.02em;
145
background: var(--bg-tertiary); color: var(--text-secondary);
146
}
147
.log-login_success, .log-signup_success { background: rgba(0, 186, 124, 0.14); color: var(--success-color); }
148
.log-login_failure, .log-login_blocked, .log-access_denied { background: rgba(244, 33, 46, 0.12); color: var(--danger-color); }
149
.log-admin_action { background: rgba(29, 155, 240, 0.14); color: var(--accent-color); }
150
.log-feature_use { background: rgba(255, 212, 0, 0.18); color: #997000; }
151
body.dark-mode .log-feature_use { color: var(--warning-color); }
152
.log-action { font-weight: 800; color: var(--text-primary); }
153
.log-meta, .log-detail { font-size: 0.78rem; color: var(--text-secondary); overflow-wrap: anywhere; }
154
.log-empty { color: var(--text-secondary); font-size: 0.9rem; margin: 0; }
155
156
body.dark-mode .btn.btn-sm { border-color: currentColor; }
157
body.dark-mode .admin-card h5 { color: var(--text-primary); }
158
body.dark-mode .table { --bs-table-bg: var(--bg-secondary); --bs-table-striped-bg: var(--bg-main); --bs-table-color: var(--text-primary); }
159
160
.footer-area {
161
text-align: center; padding: 40px 20px; color: var(--text-secondary); font-size: 0.75rem; line-height: 1.8;
162
}
163
.footer-area img { width: 160px; opacity: 0.7; mix-blend-mode: multiply; }
164
body.dark-mode .footer-area img { mix-blend-mode: screen; }
165
166
@media (max-width: 768px) {
167
.header-nav { position: static; transform: none; gap: 12px; }
168
.global-header { flex-wrap: wrap; height: auto; padding: 10px 15px; gap: 8px; }
169
.bookmark-card { flex-direction: column; text-align: center; gap: 10px; }
170
.content-area { margin: 22px auto; padding: 0 14px; }
171
.info-card, .admin-card, .bookmark-card { padding: 20px; }
172
.security-log-row { grid-template-columns: 1fr; gap: 8px; }
173
}
174
</style>
175
</head>
176
177
<body class="<%= (typeof theme !== 'undefined' && theme === 'dark') ? 'dark-mode' : '' %>">
178
<header class="global-header">
179
<div class="header-brand">
180
<a href="/hinana/index">
181
<img src="/image/<%= (typeof theme !== 'undefined' && theme === 'dark') ? 'archive1.png' : 'archive.png' %>" alt="Hinana Archive" class="header-logo">
182
</a>
183
</div>
184
<nav class="header-nav">
185
<a href="/hinana/index" class="nav-link">Archive</a>
186
<a href="/hinana/info" class="nav-link">Info</a>
187
<a href="/hinana/blog" class="nav-link">Blog</a>
188
<a href="/hinana/lounge" class="nav-link">Lounge</a>
189
<span class="nav-divider">|</span>
190
<% if (username) { %>
191
<a href="/logout?redirect=/hinana/index" class="nav-link text-danger fw-bold">Logout</a>
192
<% } %>
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="content-area">
205
<div class="info-card">
206
<h5><i class="bi bi-person-circle"></i> 닉네임</h5>
207
<div style="display: flex; align-items: center; gap: 15px;">
208
<% if (typeof currentUserProfileImage !== 'undefined' && currentUserProfileImage) { %>
209
<div style="position:relative; flex-shrink:0;">
210
<img src="<%= currentUserProfileImage %>" style="width:56px; height:56px; border-radius:50%; object-fit:cover; border:2px solid var(--border-color);">
211
<button onclick="deleteMyProfilePic()" title="프로필 사진 삭제" style="position:absolute; top:-4px; right:-4px; width:20px; height:20px; border-radius:50%; border:none; background:var(--danger-color); color:white; font-size:0.6rem; cursor:pointer; display:flex; align-items:center; justify-content:center; padding:0;"><i class="bi bi-x"></i></button>
212
</div>
213
<% } else { %>
214
<div style="width:56px; height:56px; border-radius:50%; background:var(--bg-tertiary); display:flex; align-items:center; justify-content:center; font-size:1.5rem; font-weight:bold; color:var(--text-secondary); flex-shrink:0; border:2px solid var(--border-color);"><%= username.substring(0,1).toUpperCase() %></div>
215
<% } %>
216
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
217
</div>
218
<div style="margin-top: 12px;">
219
<a href="/hinana/shop" class="btn btn-sm" style="border: 1px solid var(--accent-color); color: var(--accent-color); font-weight: 600; font-size: 0.8rem;">
220
<i class="bi bi-camera-fill me-1"></i> 프로필 변경
221
</a>
222
</div>
223
</div>
224
225
<div class="bookmark-card" style="flex-direction: column; gap: 12px;">
226
<div style="display: flex; align-items: center; gap: 15px;">
227
<div class="bookmark-icon"><i class="bi bi-bookmark-fill"></i></div>
228
<div>
229
<div class="bookmark-label">책갈피</div>
230
<div class="bookmark-value">총 <%= typeof bookmarks !== 'undefined' ? bookmarks : 0 %>개</div>
231
</div>
232
</div>
233
<div style="border-top: 1px solid var(--border-color); padding-top: 10px; display: flex; flex-direction: column; gap: 6px;">
234
<div class="bookmark-sub" style="display: flex; justify-content: space-between;">
235
<span>SNS 활동으로 획득</span>
236
<!-- <span class="fw-bold" style="color: var(--text-primary);"><%= (typeof bookmarks !== 'undefined' ? bookmarks : 0) - (typeof tetrisBookmarks !== 'undefined' ? tetrisBookmarks : 0) %>개 <span style="font-weight: normal; color: var(--text-secondary);">(하루 최대 10개)</span></span> -->
237
</div>
238
<div class="bookmark-sub" style="display: flex; justify-content: space-between;">
239
<span>테트리스로 획득</span>
240
<!-- <span class="fw-bold" style="color: var(--text-primary);"><%= typeof tetrisBookmarks !== 'undefined' ? tetrisBookmarks : 0 %>개 <span style="font-weight: normal; color: var(--text-secondary);">(하루 최대 10개)</span></span> -->
241
</div>
242
</div>
243
</div>
244
245
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
246
<div class="section-title"><i class="bi bi-gear-fill"></i> 관리자 패널: 계정 관리</div>
247
<div class="admin-card">
248
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 15px;">
249
<span style="font-size: 0.75rem; color: var(--accent-color); letter-spacing: 3px; font-weight: 700;">USER LIST</span>
250
<span style="font-size: 0.8rem; color: var(--text-secondary);"><%= users.length %>명</span>
251
</div>
252
<div style="margin-bottom: 15px;">
253
<div style="position: relative;">
254
<i class="bi bi-search" style="position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--text-secondary); font-size: 0.85rem;"></i>
255
<input type="text" id="account-search" placeholder="유저 검색..." oninput="searchAccounts()" style="width: 100%; padding: 10px 12px 10px 36px; border: 1px solid var(--border-color); border-radius: 8px; background: var(--bg-main); color: var(--text-primary); font-size: 0.85rem; font-family: inherit; box-sizing: border-box;">
256
</div>
257
</div>
258
<div class="table-responsive">
259
<table class="table table-striped table-borderless mb-0">
260
<thead>
261
<tr>
262
<th>아이디 (닉네임)</th>
263
<th style="white-space: nowrap; text-align: right;">액션</th>
264
</tr>
265
</thead>
266
<tbody id="account-list">
267
<% users.forEach(user => { %>
268
<tr class="account-row" data-username="<%= user.username %>">
269
<td style="display:flex; align-items:center; gap:8px;">
270
<% if (user.profileImage) { %>
271
<img src="<%= user.profileImage %>" style="width:24px; height:24px; border-radius:50%; object-fit:cover; border:1px solid var(--border-color);">
272
<% } %>
273
<%= user.username %>
274
</td>
275
<td style="text-align: right;">
276
<div style="display:flex; gap:6px; align-items:center; flex-wrap:nowrap; justify-content:flex-end;">
277
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
278
<span style="color: var(--text-secondary);">(관리자)</span>
279
<% } else { %>
280
<form action="/hinana/delete-user" method="POST"
281
data-confirm="<%= user.username %> 계정을 정말로 삭제하시겠습니까?">
282
<input type="hidden" name="usernameToDelete" value="<%= user.username %>">
283
<button type="submit" class="btn btn-sm" style="color: var(--danger-color); border: 1px solid var(--danger-color); white-space: nowrap;">
284
<i class="bi bi-trash-fill"></i> 삭제
285
</button>
286
</form>
287
<% } %>
288
<% if (user.profileImage) { %>
289
<button class="btn btn-sm" style="color: var(--danger-color); border: 1px solid var(--danger-color); white-space: nowrap;" onclick="deleteProfilePic('<%= user.username %>')">
290
<i class="bi bi-person-x-fill"></i> 프로필 삭제
291
</button>
292
<% } %>
293
</div>
294
</td>
295
</tr>
296
<% }); %>
297
</tbody>
298
</table>
299
</div>
300
<div class="pagination-bar" id="account-pagination"></div>
301
</div>
302
<% } %>
303
304
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
305
<div class="admin-card" style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:15px;">
306
<div>
307
<h5>오늘 올라온 글</h5>
308
<p style="font-size:0.8rem; color:var(--text-secondary); margin:0 0 6px;">오늘(자정 기준) 공개 게시글 수</p>
309
<span id="today-count-val" style="font-size:1.6rem; font-weight:800; color:var(--accent-color);">—</span>
310
<span style="font-size:0.85rem; color:var(--text-secondary); margin-left:4px;">개</span>
311
</div>
312
<button class="btn btn-sm" style="border:1px solid var(--border-color); color:var(--text-secondary);" onclick="refreshTodayCount()">
313
<i class="bi bi-arrow-clockwise"></i> 새로고침
314
</button>
315
</div>
316
<% } %>
317
318
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
319
<div class="section-title"><i class="bi bi-speedometer2"></i> 관리자 패널: 운영 도구</div>
320
<div class="admin-card" style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:15px;">
321
<div>
322
<h5>통합 모니터링</h5>
323
<p style="font-size:0.8rem; color:var(--text-secondary); margin:0;">기능 토글, API 연동 상태, 데이터 파일 상태를 한 곳에서 확인합니다.</p>
324
</div>
325
<a href="/hinana/monitor" class="btn btn-sm" style="color: var(--accent-color); border: 1px solid var(--accent-color); font-weight: 600;">
326
<i class="bi bi-speedometer2"></i> 열기
327
</a>
328
</div>
329
<% } %>
330
331
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
332
<div class="section-title"><i class="bi bi-shield-lock-fill"></i> 보안 로그</div>
333
<div class="admin-card">
334
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:16px;">
335
<div>
336
<h5 style="margin-bottom:4px;">최근 로그인 및 기능 사용 기록</h5>
337
<p style="font-size:0.8rem; color:var(--text-secondary); margin:0;">비밀번호와 가입 코드는 기록하지 않습니다.</p>
338
</div>
339
<span style="font-size:0.8rem; color:var(--text-secondary); white-space:nowrap;"><%= (typeof securityLogs !== 'undefined' && securityLogs) ? securityLogs.length : 0 %>개 표시</span>
340
</div>
341
<% if (typeof securityLogs !== 'undefined' && securityLogs && securityLogs.length) { %>
342
<div class="security-log-list" id="security-log-list">
343
<% securityLogs.forEach(function(log) { %>
344
<div class="security-log-row security-log-item">
345
<div class="log-time">
346
<i class="bi bi-clock"></i>
347
<%= typeof fmtDate === 'function' ? fmtDate(log.createdAt) : log.createdAt %>
348
</div>
349
<div class="log-main">
350
<div class="log-head">
351
<span class="log-badge log-<%= log.type %>"><%= log.type %></span>
352
<span class="log-action"><%= log.action %></span>
353
</div>
354
<div class="log-meta">
355
사용자: <strong><%= log.actor || '알 수 없음' %></strong>
356
<% if (log.target) { %> · 대상: <strong><%= log.target %></strong><% } %>
357
<% if (log.ip) { %> · IP: <%= log.ip %><% } %>
358
<% if (log.path) { %> · 경로: <%= log.path %><% } %>
359
</div>
360
<% if (log.detail) { %>
361
<div class="log-detail"><%= log.detail %></div>
362
<% } %>
363
</div>
364
</div>
365
<% }); %>
366
</div>
367
<div class="pagination-bar" id="security-log-pagination"></div>
368
<% } else { %>
369
<p class="log-empty">아직 기록된 보안 로그가 없습니다.</p>
370
<% } %>
371
</div>
372
<% } %>
373
374
<div class="section-title"><i class="bi bi-sliders"></i> 설정</div>
375
376
<div class="admin-card" style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:15px;">
377
<div>
378
<h5>오늘의 글 알림</h5>
379
<p style="font-size:0.8rem; color:var(--text-secondary); margin:0;">매일 오후 6시 기준, 새로 올라온 글이 있으면 앱 알림을 보내드립니다.</p>
380
<% if (typeof dailyPostNotif !== 'undefined' && dailyPostNotif) { %>
381
<span class="badge badge-on mt-2 d-inline-block"><i class="bi bi-check-circle-fill"></i> 활성화</span>
382
<% } else { %>
383
<span class="badge badge-off mt-2 d-inline-block"><i class="bi bi-x-circle-fill"></i> 비활성화</span>
384
<% } %>
385
</div>
386
<form action="/hinana/toggle-daily-post-notif" method="POST">
387
<% if (typeof dailyPostNotif !== 'undefined' && dailyPostNotif) { %>
388
<button type="submit" class="btn btn-sm" style="color: var(--danger-color); border: 1px solid var(--danger-color);">
389
<i class="bi bi-bell-slash-fill"></i> 끄기
390
</button>
391
<% } else { %>
392
<button type="submit" class="btn btn-sm" style="color: #16a34a; border: 1px solid #16a34a;">
393
<i class="bi bi-bell-fill"></i> 켜기
394
</button>
395
<% } %>
396
</form>
397
</div>
398
399
<div style="margin-top: 30px;">
400
<a href="/hinana/index" class="btn-back">
401
<i class="bi bi-arrow-left"></i> 게시판으로 돌아가기
402
</a>
403
</div>
404
</div>
405
406
<div class="footer-area">
407
<img src="/image/sign.png" alt="sign"><br>
408
<strong>비나래 ARCHIVE</strong><br>
409
X - @NoctchillHinana<br>
410
© 2024~2026. 비나래 | hinana.moe
411
</div>
412
<script>
413
function refreshTodayCount() {
414
fetch('/api/today-count')
415
.then(r => r.json())
416
.then(d => {
417
var el = document.getElementById('today-count-val');
418
if (el) el.textContent = d.count ?? '?';
419
})
420
.catch(function() {});
421
}
422
refreshTodayCount();
423
</script>
424
<script>
425
// 계정 관리 검색 + 페이지네이션
426
(function() {
427
var PER_PAGE = 15;
428
var allRows = Array.from(document.querySelectorAll('#account-list .account-row'));
429
if (allRows.length === 0) return;
430
var filteredRows = allRows.slice();
431
432
function searchAccounts() {
433
var query = document.getElementById('account-search').value.trim().toLowerCase();
434
filteredRows = allRows.filter(function(row) {
435
var username = (row.getAttribute('data-username') || '').toLowerCase();
436
return username.indexOf(query) !== -1;
437
});
438
renderPage(1);
439
}
440
441
function renderPage(page) {
442
var totalPages = Math.ceil(filteredRows.length / PER_PAGE) || 1;
443
var start = (page - 1) * PER_PAGE;
444
var end = start + PER_PAGE;
445
446
allRows.forEach(function(row) { row.style.display = 'none'; });
447
filteredRows.forEach(function(row, i) {
448
row.style.display = (i >= start && i < end) ? '' : 'none';
449
});
450
451
var pag = document.getElementById('account-pagination');
452
if (totalPages <= 1) { pag.innerHTML = ''; return; }
453
454
var html = '';
455
html += '<button class="page-btn" onclick="accountPage(' + Math.max(1, page - 1) + ')" ' + (page === 1 ? 'disabled' : '') + '><i class="bi bi-chevron-left"></i></button>';
456
for (var p = 1; p <= totalPages; p++) {
457
html += '<button class="page-btn' + (p === page ? ' active' : '') + '" onclick="accountPage(' + p + ')">' + p + '</button>';
458
}
459
html += '<button class="page-btn" onclick="accountPage(' + Math.min(totalPages, page + 1) + ')" ' + (page === totalPages ? 'disabled' : '') + '><i class="bi bi-chevron-right"></i></button>';
460
pag.innerHTML = html;
461
}
462
463
window.accountPage = renderPage;
464
window.searchAccounts = searchAccounts;
465
renderPage(1);
466
})();
467
468
// 보안 로그 30개 단위 페이지네이션
469
(function() {
470
var PER_PAGE = 30;
471
var rows = Array.from(document.querySelectorAll('#security-log-list .security-log-item'));
472
if (rows.length === 0) return;
473
474
function renderSecurityLogPage(page) {
475
var totalPages = Math.ceil(rows.length / PER_PAGE) || 1;
476
var safePage = Math.min(Math.max(page, 1), totalPages);
477
var start = (safePage - 1) * PER_PAGE;
478
var end = start + PER_PAGE;
479
480
rows.forEach(function(row, i) {
481
row.style.display = (i >= start && i < end) ? '' : 'none';
482
});
483
484
var pag = document.getElementById('security-log-pagination');
485
if (!pag) return;
486
if (totalPages <= 1) {
487
pag.innerHTML = '';
488
return;
489
}
490
491
var html = '';
492
html += '<button class="page-btn" onclick="securityLogPage(' + Math.max(1, safePage - 1) + ')" ' + (safePage === 1 ? 'disabled' : '') + '><i class="bi bi-chevron-left"></i></button>';
493
for (var p = 1; p <= totalPages; p++) {
494
html += '<button class="page-btn' + (p === safePage ? ' active' : '') + '" onclick="securityLogPage(' + p + ')">' + p + '</button>';
495
}
496
html += '<button class="page-btn" onclick="securityLogPage(' + Math.min(totalPages, safePage + 1) + ')" ' + (safePage === totalPages ? 'disabled' : '') + '><i class="bi bi-chevron-right"></i></button>';
497
pag.innerHTML = html;
498
}
499
500
window.securityLogPage = renderSecurityLogPage;
501
renderSecurityLogPage(1);
502
})();
503
504
async function deleteProfilePic(username) {
505
if (!await showConfirm(username + '의 프로필 사진을 삭제하시겠습니까?')) return;
506
try {
507
const res = await fetch('/hinana/admin/delete-profile-pic', {
508
method: 'POST',
509
headers: { 'Content-Type': 'application/json' },
510
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
511
});
512
const data = await res.json();
513
await showAlert(data.message);
514
if (data.success) location.reload();
515
} catch (e) {
516
await showAlert('오류가 발생했습니다.');
517
}
518
}
519
520
async function deleteMyProfilePic() {
521
if (!await showConfirm('프로필 사진을 삭제하시겠습니까?')) return;
522
try {
523
const res = await fetch('/hinana/shop/delete-profile-pic', {
524
method: 'POST',
525
headers: { 'Content-Type': 'application/json' }
526
});
527
const data = await res.json();
528
await showAlert(data.message);
529
if (data.success) location.reload();
530
} catch (e) {
531
await showAlert('오류가 발생했습니다.');
532
}
533
}
534
</script>
535
<script>if ("serviceWorker" in navigator) { navigator.serviceWorker.register("/sw.js"); }</script>
536
</body>
537
</html>
538