Public Source Viewer

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

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

Redacted View
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