Public Source Viewer
비나래아카이브 개발자 포털
실제 서비스 구조를 살펴볼 수 있는 공개용 코드 뷰어입니다. 인증, 세션, 외부 연동, 토큰, 관리자 식별 등 보안상 민감한 구현은 파일 단위 또는 줄 단위로 검열됩니다.
view/hinana/pedia.ejs
공개 가능
1
<!DOCTYPE html>
2
<html lang="ko">
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
<meta name="theme-color" content="<%= theme === 'dark' ? '#0e1218' : '#f7f5ef' %>">
8
<meta property="og:title" content="히나나피디아 — 비나래 라운지">
9
<meta property="og:description" content="Discord 사용자들이 남긴 작품 별점과 감상을 함께 봅니다.">
10
<title>히나나피디아 — 비나래 라운지</title>
11
<link rel="stylesheet" href="/vendors/bootstrap/css/bootstrap.min.css">
12
<script src="/vendors/bootstrap/js/bootstrap.min.js"></script>
13
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;700&family=Montserrat:wght@300;400;700&display=swap" rel="stylesheet">
14
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css">
15
<script src="/js/popup.js"></script>
16
<style>
17
*,*::before,*::after{box-sizing:border-box}
18
:root{--bg-main:#f8f7f5;--bg-secondary:#fff;--bg-tertiary:#1a2238;--bg-card:#fff;--bg-input:#f1f0ed;--text-primary:#1a2238;--text-secondary:#5e6676;--accent:#c5a059;--accent-dim:rgba(197,160,89,.12);--border:#e5e1da;--font:'Noto Sans KR',sans-serif}
19
body.dark-mode{--bg-main:#0f141e;--bg-secondary:#1a2238;--bg-tertiary:#111827;--bg-card:#1e2a42;--bg-input:#243050;--text-primary:#e7e5e4;--text-secondary:#94a3b8;--border:#2e3a59}
20
html,body{margin:0;padding:0;background:var(--bg-main);color:var(--text-primary);font-family:var(--font);min-height:100vh;display:flex;flex-direction:column;transition:background .2s,color .2s}a{text-decoration:none;color:inherit}button,input,select{font:inherit}
21
.site-header{background:var(--bg-tertiary);height:60px;padding:0 24px;display:flex;align-items:center;justify-content:space-between;gap:14px;border-bottom:1px solid var(--border);position:sticky;top:0;z-index:100}.site-header .brand{display:flex;align-items:center;gap:12px;min-width:0;flex:1;font-size:.9rem;font-weight:700;color:#fff}.site-header .brand>span:last-child{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.header-logo{height:26px;max-width:170px;object-fit:contain}.site-header nav{display:flex;align-items:center;gap:6px;flex:0 0 auto}.site-header nav a{font-size:.82rem;color:var(--text-secondary);margin-left:20px;transition:color .2s}.site-header nav a:hover{color:var(--accent)}
22
.page-body{flex:1;max-width:920px;width:100%;margin:0 auto;padding:40px 24px 0}.page-title{margin-bottom:24px}.page-title h1{font-size:1.5rem;font-weight:700;margin:0 0 6px}.page-title p{font-size:.82rem;color:var(--text-secondary);margin:0;line-height:1.6}.command-info{display:inline-flex;align-items:center;gap:8px;margin-top:12px;padding:7px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-card);font-size:.75rem;color:var(--text-secondary)}.command-info code{color:var(--accent);font-weight:700}
23
.stats-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-bottom:20px}.stat-card{background:var(--bg-card);border:1px solid var(--border);border-radius:12px;padding:18px 16px;text-align:center}.stat-card.highlight{border-color:var(--accent);background:var(--accent-dim)}.stat-label{font-size:.68rem;text-transform:uppercase;letter-spacing:.1em;color:var(--text-secondary);margin-bottom:8px}.stat-value{font-size:1.8rem;font-weight:700;color:var(--text-primary);line-height:1;font-feature-settings:'tnum'}.stat-card.highlight .stat-value{color:var(--accent)}
24
.filter-card{display:flex;gap:10px;background:var(--bg-card);border:1px solid var(--border);border-radius:12px;padding:14px;margin-bottom:20px}.filter-card input,.filter-card select{min-height:40px;border:1px solid var(--border);border-radius:8px;background:var(--bg-input);color:var(--text-primary);padding:0 12px;outline:none}.filter-card input{flex:1;min-width:0}.filter-card select{width:150px}.filter-card input:focus,.filter-card select:focus{border-color:var(--accent)}.filter-btn{min-height:40px;padding:0 16px;border:0;border-radius:8px;background:var(--accent);color:#fff;font-weight:700}.reset-btn{min-height:40px;padding:0 12px;display:inline-flex;align-items:center;border:1px solid var(--border);border-radius:8px;color:var(--text-secondary);font-size:.76rem}.reset-btn:hover{border-color:var(--accent);color:var(--accent)}
25
.work-list{display:grid;gap:14px;margin-bottom:40px}.work-card{background:var(--bg-card);border:1px solid var(--border);border-radius:12px;overflow:hidden}.work-head{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:18px;align-items:center;padding:18px 20px;background:var(--accent-dim)}.work-title{margin:0;font-size:1.08rem;font-weight:700;word-break:break-word}.work-meta{margin-top:6px;color:var(--text-secondary);font-size:.72rem}.score{text-align:right}.score-number{font-size:1.55rem;font-weight:700;color:var(--accent)}.score-number small{font-size:.7rem;color:var(--text-secondary)}
26
.stars{position:relative;display:inline-block;font-size:.95rem;line-height:1;letter-spacing:1px;white-space:nowrap}.stars-back{color:var(--border)}.stars-fill{position:absolute;inset:0 auto 0 0;overflow:hidden;color:var(--accent);white-space:nowrap}
27
.reviews{border-top:1px solid var(--border)}.reviews summary{padding:10px 20px;color:var(--text-secondary);font-size:.74rem;font-weight:700;cursor:pointer;list-style:none}.reviews summary::-webkit-details-marker{display:none}.reviews summary i{transition:transform .2s}.reviews[open] summary i{transform:rotate(180deg)}.review-list{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(360px,100%),1fr));gap:1px;background:var(--border);border-top:1px solid var(--border)}.review{padding:16px 18px;background:var(--bg-card);min-width:0}.review-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.review-user{font-size:.8rem;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.review-rating{color:var(--accent);font-size:.78rem;font-weight:700;white-space:nowrap}.review-content{margin:10px 0 0;white-space:pre-wrap;word-break:break-word;font-size:.84rem;line-height:1.65}.review-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:10px}.review-date{color:var(--text-secondary);font-size:.66rem}.delete-review{border:1px solid var(--border);border-radius:6px;background:transparent;color:#dc6262;padding:4px 8px;font-size:.66rem}.delete-review:hover{border-color:#dc6262}
28
.empty{padding:64px 20px;text-align:center;border:1px dashed var(--border);border-radius:12px;background:var(--bg-card);color:var(--text-secondary);margin-bottom:40px}.empty i{display:block;margin-bottom:10px;color:var(--accent);font-size:1.8rem}.pagination{display:flex;justify-content:center;gap:6px;margin:22px 0 40px}.page-btn{min-width:36px;height:36px;display:grid;place-items:center;padding:0 9px;border:1px solid var(--border);border-radius:6px;background:var(--bg-card);color:var(--text-secondary);font-size:.78rem}.page-btn.current{background:var(--accent);border-color:var(--accent);color:#fff}.page-btn.disabled{pointer-events:none;opacity:.35}
29
@media(max-width:600px){.page-body{padding:24px 16px 0}.site-header{padding:0 12px;gap:8px}.site-header .brand{gap:8px;font-size:.82rem}.header-logo{max-width:132px;height:24px}.site-header nav a{margin-left:0;width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;font-size:0}.site-header nav a i{font-size:1rem;margin:0!important}.stats-grid{gap:8px}.stat-card{padding:14px 8px}.stat-value{font-size:1.35rem}.filter-card{flex-wrap:wrap}.filter-card input{flex-basis:100%}.filter-card select{flex:1}.work-head{grid-template-columns:1fr}.score{text-align:left}.review-list{grid-template-columns:1fr}}
30
</style>
31
<link rel="stylesheet" href="/css/lounge-modern.css">
32
</head>
33
<body class="<%= theme === 'dark' ? 'dark-mode' : '' %>">
34
<header class="site-header">
35
<div class="brand"><a href="/hinana/lounge"><img src="/image/<%= theme === 'dark' ? 'lounge1.png' : 'lounge.png' %>" alt="비나래 라운지" class="header-logo"></a><span style="opacity:.4;font-weight:300;color:#fff">|</span><span style="color:#fff">히나나피디아</span></div>
36
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
37
</header>
38
39
<%
40
function pageUrl(targetPage) {
41
const params = new URLSearchParams();
42
if (search) params.set('q', search);
43
if (sort !== 'recent') params.set('sort', sort);
44
if (targetPage > 1) params.set('page', String(targetPage));
45
const query = params.toString();
46
return '/hinana/pedia' + (query ? '?' + query : '');
47
}
48
%>
49
50
<main class="page-body">
51
<section class="page-title"><h1><i class="bi bi-journal-star me-2" style="color:var(--accent)"></i>히나나피디아</h1><p>Discord에서 모인 작품 별점과 감상을 한곳에서 함께 봅니다.</p><div class="command-info"><i class="bi bi-discord"></i><code>/피디아</code><span>작품명 · 별점 · 내용을 입력해 주세요.</span></div></section>
52
53
<section class="stats-grid">
54
<div class="stat-card"><div class="stat-label">등록 작품</div><div class="stat-value"><%= stats.workCount.toLocaleString() %></div></div>
55
<div class="stat-card"><div class="stat-label">참여 평가</div><div class="stat-value"><%= stats.reviewCount.toLocaleString() %></div></div>
56
<div class="stat-card highlight"><div class="stat-label">전체 평균</div><div class="stat-value"><%= stats.averageRating.toFixed(1) %></div></div>
57
</section>
58
59
<form class="filter-card" method="GET" action="/hinana/pedia">
60
<input type="search" name="q" value="<%= search %>" maxlength="100" placeholder="작품명 검색" aria-label="작품명 검색">
61
<select name="sort" aria-label="정렬">
62
<option value="recent" <%= sort === 'recent' ? 'selected' : '' %>>최근 평가순</option>
63
<option value="rating" <%= sort === 'rating' ? 'selected' : '' %>>별점 높은순</option>
64
<option value="reviews" <%= sort === 'reviews' ? 'selected' : '' %>>평가 많은순</option>
65
<option value="title" <%= sort === 'title' ? 'selected' : '' %>>작품명순</option>
66
</select>
67
<button type="submit" class="filter-btn"><i class="bi bi-search me-1"></i>찾기</button>
68
<% if(search || sort !== 'recent'){ %><a class="reset-btn" href="/hinana/pedia">초기화</a><% } %>
69
</form>
70
71
<% if(works.length === 0){ %>
72
<div class="empty"><i class="bi bi-journal-star"></i><% if(search){ %>검색한 작품의 평가가 없습니다.<% } else { %>아직 등록된 작품이 없습니다.<br>Discord에서 <strong>/피디아</strong>로 첫 평가를 남겨보세요.<% } %></div>
73
<% } else { %>
74
<section class="work-list">
75
<% works.forEach(function(work){ %>
76
<article class="work-card">
77
<div class="work-head">
78
<div><h2 class="work-title"><%= work.title %></h2><div class="work-meta">평가 <strong><%= work.reviewCount %>명</strong> · 최근 갱신 <%= new Date(work.latestAt).toLocaleDateString('ko-KR') %></div></div>
79
<div class="score"><div class="score-number"><%= work.averageRating.toFixed(1) %> <small>/ 5.0</small></div><div class="stars" aria-label="평균 별점 <%= work.averageRating.toFixed(1) %>점"><span class="stars-back">★★★★★</span><span class="stars-fill" style="width:<%= work.averageRating / 5 * 100 %>%">★★★★★</span></div></div>
80
</div>
81
<details class="reviews" open>
82
<summary>모든 평가 <%= work.reviewCount %>개 <i class="bi bi-chevron-up ms-1"></i></summary>
83
<div class="review-list">
84
<% work.reviews.forEach(function(review){ %>
85
<article class="review">
86
<div class="review-head"><span class="review-user"><i class="bi bi-discord me-1"></i><%= review.discordDisplayName || review.discordUsername %></span><span class="review-rating">★ <%= review.rating.toFixed(1) %></span></div>
87
<p class="review-content"><%= review.content %></p>
88
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
89
</article>
90
<% }); %>
91
</div>
92
</details>
93
</article>
94
<% }); %>
95
</section>
96
97
<% if(totalPages > 1){ %><nav class="pagination" aria-label="페이지 이동"><a class="page-btn <%= page <= 1 ? 'disabled' : '' %>" href="<%= pageUrl(page - 1) %>"><i class="bi bi-chevron-left"></i></a><% for(let p=Math.max(1,page-2);p<=Math.min(totalPages,page+2);p++){ %><a class="page-btn <%= p === page ? 'current' : '' %>" href="<%= pageUrl(p) %>"><%= p %></a><% } %><a class="page-btn <%= page >= totalPages ? 'disabled' : '' %>" href="<%= pageUrl(page + 1) %>"><i class="bi bi-chevron-right"></i></a></nav><% } %>
98
<% } %>
99
</main>
100
101
<footer class="site-footer">
102
<div class="footer-brand"><img src="/image/sign.png" alt="비나래" class="footer-logo"><div><div class="footer-title">비나래 라운지</div><div>Archive My Lounge</div></div></div>
103
<div><div class="footer-links"><a href="/hinana/index">Archive</a><a href="/hinana/lounge">Lounge</a><a href="/hinana/gallery">Gallery</a><a href="/hinana/plaza">Plaza</a><a href="https://x.com/NoctchillHinana" target="_blank" rel="noreferrer">X</a></div><div class="footer-copy"><span class="footer-version">Ver. 8.3.1.0-Utsumi Aoba</span>ⓒ 2024~2026. 비나래 | hinana.moe</div></div>
104
</footer>
105
</body>
106
</html>
107