Public Source Viewer

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

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

Redacted View
view/hinana/developer.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 <meta name="theme-color" content="<%= (typeof theme !== 'undefined' && theme === 'dark') ? '#000000' : '#ffffff' %>">
8 <meta name="apple-mobile-web-app-title" content="비나래 개발자 포털">
9 <meta name="description" content="비나래 아카이브의 공개 가능한 소스 구조와 구현 일부를 확인할 수 있는 개발자 포털입니다.">
10 <meta property="og:type" content="website">
11 <meta property="og:site_name" content="비나래 아카이브">
12 <meta property="og:title" content="비나래아카이브 개발자 포털">
13 <meta property="og:description" content="비나래 아카이브의 공개 가능한 소스 구조와 구현 일부를 확인할 수 있는 개발자 포털입니다.">
14 <meta property="og:url" content="https://hinana.moe/hinana/developer">
15 <meta property="og:image" content="https://hinana.moe/image/title.png">
16 <meta name="twitter:card" content="summary_large_image">
17 <meta name="twitter:title" content="비나래아카이브 개발자 포털">
18 <meta name="twitter:description" content="비나래 아카이브의 공개 가능한 소스 구조와 구현 일부를 확인할 수 있는 개발자 포털입니다.">
19 <meta name="twitter:image" content="https://hinana.moe/image/title.png">
20 <title>비나래아카이브 개발자 포털</title>
21 <link rel="stylesheet" href="/vendors/bootstrap/css/bootstrap.min.css" />
22 <script src="/vendors/bootstrap/js/bootstrap.min.js"></script>
23 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css">
24 <style>
25 :root {
26 --font-family: 'Noto Sans KR', sans-serif;
27 --bg-main: #ffffff; --bg-secondary: #f7f9f9; --bg-tertiary: #eff3f4;
28 --text-primary: #0f1419; --text-secondary: #536471;
29 --accent-color: #1d9bf0; --danger-color: #f4212e; --border-color: #eff3f4;
30 --success-color: #00ba7c; --warning-color: #b7791f;
31 --code-bg: #0f172a; --code-text: #dbeafe; --code-muted: #64748b;
32 --shadow-sm: 0 1px 2px 0 rgba(15, 20, 25, 0.06);
33 }
34 body.dark-mode {
35 --bg-main: #000000; --bg-secondary: #16181c; --bg-tertiary: #202327;
36 --text-primary: #e7e9ea; --text-secondary: #71767b;
37 --border-color: #2f3336; --code-bg: #06080d; --code-text: #e7e9ea;
38 --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.5);
39 }
40 * { box-sizing: border-box; }
41 html, body { margin: 0; min-height: 100vh; font-family: var(--font-family); background: var(--bg-main); color: var(--text-primary); }
42 a { color: inherit; text-decoration: none; }
43 .global-header {
44 height: 60px; background-color: rgba(255, 255, 255, 0.9); border-bottom: 1px solid var(--border-color);
45 display: flex; align-items: center; justify-content: space-between; padding: 0 20px;
46 position: sticky; top: 0; z-index: 1000; backdrop-filter: blur(12px);
47 }
48 body.dark-mode .global-header { background-color: rgba(0, 0, 0, 0.86); }
49 .header-logo { height: 28px; width: auto; mix-blend-mode: multiply; }
50 body.dark-mode .header-logo { mix-blend-mode: screen; }
51 .header-nav { position: absolute; left: 50%; transform: translateX(-50%); display: flex; gap: 20px; align-items: center; }
52 .nav-link { font-weight: 600; font-size: 0.95rem; color: var(--text-secondary); cursor: pointer; transition: color 0.2s; }
53 .nav-link:hover, .nav-link.active { color: var(--text-primary); }
54 .icon-btn { border: none; background: transparent; color: var(--text-secondary); font-size: 1.1rem; cursor: pointer; padding: 4px; }
55
56 .portal-shell { width: min(1320px, calc(100% - 32px)); margin: 34px auto 48px; }
57 .portal-head {
58 display: flex; align-items: flex-end; justify-content: space-between; gap: 18px;
59 padding-bottom: 16px; border-bottom: 1px solid var(--border-color); margin-bottom: 18px;
60 }
61 .eyebrow { color: var(--accent-color); font-size: 0.72rem; font-weight: 800; letter-spacing: 0.16em; text-transform: uppercase; }
62 h1 { margin: 6px 0 0; font-size: clamp(1.55rem, 3vw, 2.2rem); font-weight: 850; letter-spacing: 0; }
63 .portal-desc { color: var(--text-secondary); font-size: 0.88rem; margin: 8px 0 0; max-width: 760px; line-height: 1.7; }
64 .portal-grid { display: grid; grid-template-columns: 320px 1fr; gap: 16px; align-items: start; }
65 .panel {
66 background: var(--bg-secondary); border: 1px solid var(--border-color);
67 border-radius: 8px; box-shadow: var(--shadow-sm); overflow: hidden;
68 }
69 .panel-title {
70 display: flex; align-items: center; justify-content: space-between; gap: 12px;
71 padding: 14px 16px; border-bottom: 1px solid var(--border-color);
72 font-size: 0.82rem; font-weight: 800; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.06em;
73 }
74 .file-list { max-height: calc(100vh - 220px); overflow: auto; padding: 8px; }
75 .file-link {
76 display: block; padding: 9px 10px; border-radius: 6px;
77 color: var(--text-secondary); font-size: 0.82rem; line-height: 1.35;
78 overflow: hidden; text-overflow: ellipsis;
79 }
80 .file-link:hover { background: var(--bg-tertiary); color: var(--text-primary); }
81 .file-link.active { background: rgba(29,155,240,0.12); color: var(--accent-color); font-weight: 800; }
82 .file-dir { display: block; font-size: 0.68rem; color: var(--text-secondary); opacity: 0.72; margin-top: 2px; }
83 .badge-safe, .badge-redacted {
84 display: inline-flex; align-items: center; border-radius: 999px; padding: 4px 8px;
85 font-size: 0.68rem; font-weight: 850; white-space: nowrap;
86 }
87 .badge-safe { background: rgba(0,186,124,0.14); color: var(--success-color); }
88 .badge-redacted { background: rgba(255,212,0,0.18); color: var(--warning-color); }
89 .code-head {
90 display: flex; align-items: center; justify-content: space-between; gap: 12px;
91 padding: 14px 16px; border-bottom: 1px solid var(--border-color);
92 }
93 .code-path { font-weight: 850; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
94 .notice {
95 margin: 14px 16px 0; padding: 12px 14px; border-radius: 8px;
96 background: rgba(255,212,0,0.14); color: var(--warning-color);
97 border: 1px solid rgba(255,212,0,0.22);
98 font-size: 0.82rem; font-weight: 700;
99 }
100 .code-view {
101 margin: 14px 16px 16px; border-radius: 8px; overflow: auto;
102 background: var(--code-bg); color: var(--code-text);
103 max-height: calc(100vh - 270px);
104 font-family: Consolas, "SFMono-Regular", Menlo, Monaco, monospace;
105 font-size: 0.78rem; line-height: 1.55;
106 }
107 .code-line { display: grid; grid-template-columns: 56px minmax(0, 1fr); min-width: 760px; }
108 .line-no { color: var(--code-muted); text-align: right; padding: 0 12px; user-select: none; border-right: 1px solid rgba(148,163,184,0.16); }
109 .line-text { white-space: pre; padding: 0 14px; }
110 .code-line.redacted .line-text { color: #facc15; font-weight: 800; }
111 .footer-area {
112 width: min(1320px, calc(100% - 32px));
113 margin: 0 auto 38px;
114 padding: 24px 0 0;
115 border-top: 1px solid var(--border-color);
116 color: var(--text-secondary);
117 display: flex;
118 align-items: center;
119 justify-content: space-between;
120 gap: 20px;
121 font-size: 0.78rem;
122 }
123 .footer-brand { display: flex; align-items: center; gap: 12px; min-width: 0; }
124 .footer-logo {
125 width: 40px; height: 40px; object-fit: contain;
126 opacity: 0.72; mix-blend-mode: multiply; flex: 0 0 auto;
127 }
128 body.dark-mode .footer-logo { mix-blend-mode: screen; opacity: 0.86; }
129 .footer-title { font-weight: 850; color: var(--text-primary); font-size: 0.9rem; }
130 .footer-sub { margin-top: 2px; }
131 .footer-links { display: flex; align-items: center; justify-content: flex-end; gap: 12px; flex-wrap: wrap; }
132 .footer-links a { color: var(--text-secondary); font-weight: 750; }
133 .footer-links a:hover { color: var(--accent-color); }
134 .footer-copy { margin-top: 4px; text-align: right; font-size: 0.72rem; }
135 @media (max-width: 900px) {
136 .global-header { flex-wrap: wrap; height: auto; padding: 10px 15px; gap: 8px; }
137 .header-nav { position: static; transform: none; width: 100%; order: 3; justify-content: center; border-top: 1px solid var(--border-color); padding-top: 8px; }
138 .portal-grid { grid-template-columns: 1fr; }
139 .file-list { max-height: 240px; }
140 .code-view { max-height: none; }
141 .footer-area { flex-direction: column; align-items: flex-start; margin-bottom: 30px; }
142 .footer-links { justify-content: flex-start; }
143 .footer-copy { text-align: left; }
144 }
145 </style>
146 </head>
147 <body class="<%= (typeof theme !== 'undefined' && theme === 'dark') ? 'dark-mode' : '' %>">
148 <header class="global-header">
149 <a href="/hinana/index">
150 <img src="/image/<%= (typeof theme !== 'undefined' && theme === 'dark') ? 'archive1.png' : 'archive.png' %>" alt="비나래 아카이브" class="header-logo">
151 </a>
152 <nav class="header-nav">
153 <a href="/hinana/index" class="nav-link">Archive</a>
154 <a href="/hinana/blog" class="nav-link">Blog</a>
155 <a href="/hinana/lounge" class="nav-link">Lounge</a>
156 <a href="/hinana/developer" class="nav-link active">Developer</a>
157 </nav>
158 <form action="/toggle-theme" method="POST" style="margin:0;">
159 <button type="submit" class="icon-btn" title="테마 변경">
160 <i class="bi <%= (typeof theme !== 'undefined' && theme==='dark') ? 'bi-moon-stars-fill':'bi-sun-fill' %>"></i>
161 </button>
162 </form>
163 </header>
164
165 <main class="portal-shell">
166 <div class="portal-head">
167 <div>
168 <div class="eyebrow">Public Source Viewer</div>
169 <h1>비나래아카이브 개발자 포털</h1>
170 <p class="portal-desc">실제 서비스 구조를 살펴볼 수 있는 공개용 코드 뷰어입니다. 인증, 세션, 외부 연동, 토큰, 관리자 식별 등 보안상 민감한 구현은 파일 단위 또는 줄 단위로 검열됩니다.</p>
171 </div>
172 <span class="badge-redacted"><i class="bi bi-shield-lock-fill me-1"></i> Redacted View</span>
173 </div>
174
175 <div class="portal-grid">
176 <aside class="panel">
177 <div class="panel-title">
178 <span><i class="bi bi-folder2-open me-1"></i> Files</span>
179 <span><%= files.length %></span>
180 </div>
181 <div class="file-list" id="developer-file-list">
182 <% files.forEach(function(file) { %>
183 <a class="file-link <%= selected.entry.path === file.path ? 'active' : '' %>" href="/hinana/developer?file=<%= encodeURIComponent(file.path) %>">
184 <%= file.name %>
185 <% if (file.sensitive) { %><span class="badge-redacted ms-1">locked</span><% } %>
186 <span class="file-dir"><%= file.directory %></span>
187 </a>
188 <% }) %>
189 </div>
190 </aside>
191
192 <section class="panel">
193 <div class="code-head">
194 <div class="code-path"><i class="bi bi-file-earmark-code me-1"></i><%= selected.entry.path || 'No file' %></div>
195 <% if (selected.redactedFile) { %>
196 <span class="badge-redacted">보안 비공개</span>
197 <% } else { %>
198 <span class="badge-safe">공개 가능</span>
199 <% } %>
200 </div>
201 <% if (selected.notice) { %>
202 <div class="notice"><i class="bi bi-exclamation-triangle-fill me-1"></i><%= selected.notice %></div>
203 <% } %>
204 <div class="code-view">
205 <% selected.lines.forEach(function(line) { %>
206 <div class="code-line <%= line.redacted ? 'redacted' : '' %>">
207 <span class="line-no"><%= line.number %></span>
208 <span class="line-text"><%= line.text %></span>
209 </div>
210 <% }) %>
211 </div>
212 </section>
213 </div>
214 </main>
215 <footer class="footer-area">
216 <div class="footer-brand">
217 <img src="/image/sign.png" alt="비나래" class="footer-logo">
218 <div>
219 <div class="footer-title">비나래아카이브 개발자 포털</div>
220 <div class="footer-sub">Public Source Viewer</div>
221 </div>
222 </div>
223 <div>
224 <div class="footer-links">
225 <a href="/hinana/index">Archive</a>
226 <a href="/hinana/blog">Blog</a>
227 <a href="/hinana/lounge">Lounge</a>
228 <a href="/hinana/developer">Developer</a>
229 <a href="https://x.com/NoctchillHinana">X</a>
230 </div>
231 <div class="footer-copy">ⓒ 2024~2026. 비나래 | hinana.moe</div>
232 </div>
233 </footer>
234 <script>
235 (function() {
236 var list = document.getElementById('developer-file-list');
237 var active = list ? list.querySelector('.file-link.active') : null;
238 if (!list || !active) return;
239
240 var targetTop = active.offsetTop - (list.clientHeight / 2) + (active.clientHeight / 2);
241 list.scrollTop = Math.max(targetTop, 0);
242 })();
243 </script>
244 </body>
245 </html>
246