Public Source Viewer

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

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

Redacted View
view/hinana/old_Gemini.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="#00D6FA">
8 <meta name="apple-mobile-web-app-title" content="비나래 아카이브">
9 <meta property="og:image" content="/image/2.png" />
10 <meta property="og:description" content="morikubo"/>
11 <meta property="og:url" content="hinana.moe"/>
12 <meta property="og:title" content="비나래 아카이브"/>
13 <title>히나나와 대화하기</title>
14 <script src="/js/popup.js"></script>
15
16 <link rel='stylesheet' href='/vendors/bootstrap/css/bootstrap.min.css' />
17 <link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet" type="text/css">
18 <link href="https://fonts.googleapis.com/css?family=Lato" rel="stylesheet" type="text/css">
19 <link rel="stylesheet" href="/css/hinana.css" type="text/css">
20
21 <!-- 마크다운 파서 & XSS 방지용 sanitize -->
22 <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
23 <script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.5/dist/purify.min.js"></script>
24
25 <script src="https://code.jquery.com/jquery-3.3.1.min.js"
26 integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
27 crossorigin="anonymous"></script>
28 <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js"
29 integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh"
30 crossorigin="anonymous"></script>
31 <script src="/vendors/bootstrap/js/bootstrap.min.js"></script>
32 </head>
33
34 <style>
35 .chat-container {
36 width: 100%;
37 max-width: 800px;
38 height: 85vh;
39 margin-left: auto;
40 margin-right: auto;
41 margin-top: 0;
42 margin-bottom: 0;
43 background-color: #fff;
44 border-radius: 8px;
45 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
46 display: flex;
47 flex-direction: column;
48 border: 1px solid #e0e0e0;
49 }
50 body.dark-mode .chat-container {
51 background-color: #2c2c2c;
52 border: 1px solid #444;
53 }
54
55 .chat-header {
56 background-color: #ffc0cb;
57 color: white;
58 padding: 15px;
59 text-align: center;
60 border-top-left-radius: 8px;
61 border-top-right-radius: 8px;
62 font-family: 'SeoulNamsanM', sans-serif;
63 }
64 .chat-header h2 {
65 margin: 0;
66 font-size: 1.2em;
67 font-weight: bold;
68 }
69
70 .chat-window {
71 flex-grow: 1;
72 padding: 20px;
73 overflow-y: auto;
74 display: flex;
75 flex-direction: column;
76 gap: 12px;
77 }
78
79 .message {
80 max-width: 80%;
81 padding: 10px 15px;
82 border-radius: 18px;
83 line-height: 1.6;
84 font-family: 'SeoulNamsanM', sans-serif;
85
86 /* 텍스트/레이아웃 설정 */
87 word-break: break-word;
88 text-align: left;
89 align-self: flex-start;
90 }
91
92 .message p {
93 margin: 0;
94 }
95
96 .hinana-message {
97 background-color: #f1f1f1;
98 color: #333;
99 border-bottom-left-radius: 4px;
100 }
101 body.dark-mode .hinana-message {
102 background-color: #3e3e3e;
103 color: #f1f1f1;
104 }
105
106 .user-message {
107 background-color: #ffc0cb;
108 color: white;
109 border-bottom-right-radius: 4px;
110 align-self: flex-end; /* 사용자 말풍선은 오른쪽 */
111 }
112
113 .chat-input-form {
114 display: flex;
115 padding: 15px;
116 border-top: 1px solid #ddd;
117 }
118 body.dark-mode .chat-input-form {
119 border-top: 1px solid #444;
120 }
121
122 #message-input {
123 flex-grow: 1;
124 border: 1px solid #ccc;
125 border-radius: 20px;
126 padding: 10px 15px;
127 font-size: 1em;
128 font-family: 'SeoulNamsanM', sans-serif;
129 }
130 body.dark-mode #message-input {
131 background-color: #3e3e3e;
132 color: #f1f1f1;
133 border-color: #555;
134 }
135 #message-input:focus {
136 outline: none;
137 border-color: #ffc0cb;
138 box-shadow: 0 0 5px rgba(255, 192, 203, 0.5);
139 }
140
141 .chat-input-form button {
142 background-color: #ffc0cb;
143 color: white;
144 border: none;
145 padding: 10px 20px;
146 margin-left: 10px;
147 border-radius: 20px;
148 cursor: pointer;
149 font-size: 1em;
150 font-family: 'SeoulNamsanM', sans-serif;
151 transition: background-color 0.2s;
152 }
153 .chat-input-form button:hover {
154 background-color: #f7a8b8;
155 }
156
157 div#services {
158 padding-top: 0;
159 padding-bottom: 0;
160 }
161
162 body.dark-mode .bg-grey,
163 body.dark-mode .navbar,
164 body.dark-mode .navbar .container-fluid {
165 background-color: #181818 !important;
166 color: #f1f1f1 !important;
167 }
168 body.dark-mode .navbar .nav-link,
169 body.dark-mode .navbar .navbar-brand {
170 color: #f1f1f1 !important;
171 }
172 </style>
173
174 <body id="myPage" data-spy="scroll" data-bs-target=".navbar" data-offset="60"
175 class="<%= theme === 'dark' ? 'dark-mode' : '' %>">
176
177 <nav class="navbar navbar-expand-md navbar-light bg-light" style="padding: 0.5rem 1rem;">
178 <div class="container-fluid">
179 <a class="navbar-brand" href="/">
180 <img src="/image/archive1.png" alt="비나래 아카이브"
181 class="theme-logo" style="max-height: 28px; width: auto;">
182 </a>
183 <button class="navbar-toggler" type="button" data-bs-toggle="collapse"
184 data-bs-target="#navbarNavAltMarkup"
185 aria-controls="navbarNavAltMarkup" aria-expanded="false"
186 aria-label="Toggle navigation">
187 <span class="navbar-toggler-icon"></span>
188 </button>
189 <div class="collapse navbar-collapse" id="navbarNavAltMarkup">
190 <div class="navbar-nav">
191 <a class="nav-link" href="/hinana/info">info</a>
192 <a class="nav-link" href="/hinana/blog">blog</a>
193 <a class="nav-link" href="/">메인 페이지로</a>
194 <form action="/toggle-theme" method="POST" class="d-inline" id="themeToggleForm">
195 <button type="submit" class="btn btn-sm btn-dark">
196 <span id="themeToggleLabel">
197 <%= theme === 'dark' ? '☀️ 라이트모드' : '🌙 다크모드' %>
198 </span>
199 </button>
200 </form>
201 </div>
202 <form id="reset-session-form"
203 action="/hinana/reset-session"
204 method="POST"
205 class="ms-auto"
206 style="margin-left:auto;">
207 <button type="submit" class="btn btn-danger btn-sm ms-2">
208 세션 초기화
209 </button>
210 </form>
211 </div>
212 </div>
213 </nav>
214
215 <div id="services" class="container-fluid text-center bg-grey">
216 <div class="chat-container">
217 <div class="chat-header">
218 <h2>HINANA CHAT</h2>
219 </div>
220 <div class="chat-window" id="chat-window">
221 <div class="message hinana-message" id="initial-hinana">
222 <%= initialMessage %>
223 </div>
224 </div>
225 <form class="chat-input-form" id="chat-form">
226 <input type="text" id="message-input"
227 placeholder="히나나에게 메시지 보내기..." autocomplete="off">
228 <button type="submit">전송</button>
229 </form>
230 </div>
231 </div>
232
233 <div style="text-align:left;" class="footer">
234 <img src="/image/sign.png" id="fumika_sign"
235 style="max-width:250px; max-height:initial; width:100%; height:100%;" />
236 </div>
237 <footer class="container-fluid text-center footer">
238 <a href="#myPage" title="To Top">
239 <span class="glyphicon glyphicon-chevron-up"></span>
240 </a>
241 <p style="margin-bottom: 0rem;" class="copyright">
242 Twitter - @NoctchillHinana
243 </p>
244 <p style="margin-bottom: 0rem;" class="copyright">
245 ⓒ 2024~2026. 비나래 | hinana.moe All rights reserved.
246 </p>
247 </footer>
248
249 <script>
250 // 세션 초기화 confirm
251 document.getElementById('reset-session-form').addEventListener('submit', function (e) {
252 e.preventDefault();
253 var form = this;
254 showConfirm('정말 초기화 할까요? 지금까지의 대화와 세션 정보가 모두 삭제됩니다.').then(function(ok) {
255 if (ok) form.submit();
256 });
257 });
258
259 const chatForm = document.getElementById('chat-form');
260 const messageInput = document.getElementById('message-input');
261 const chatWindow = document.getElementById('chat-window');
262
263 // 마크다운 + sanitize 렌더링 함수
264 function renderMarkdown(text) {
265 const html = marked.parse(text || '');
266 return DOMPurify.sanitize(html);
267 }
268
269 function appendMessage(text, type) {
270 const div = document.createElement('div');
271 div.classList.add('message');
272 if (type === 'user') {
273 div.classList.add('user-message');
274 } else {
275 div.classList.add('hinana-message');
276 }
277 div.innerHTML = renderMarkdown(text);
278 chatWindow.appendChild(div);
279 chatWindow.scrollTop = chatWindow.scrollHeight;
280 }
281
282 // 초기 히나나 메시지도 마크다운 적용
283 (function initFirstMessage() {
284 const first = document.getElementById('initial-hinana');
285 if (first) {
286 const raw = first.textContent.trim();
287 first.innerHTML = renderMarkdown(raw);
288 }
289 })();
290
291 chatForm.addEventListener('submit', async function (e) {
292 e.preventDefault();
293 const text = messageInput.value.trim();
294 if (!text) return;
295
296 appendMessage(text, 'user');
297 messageInput.value = '';
298
299 try {
300 const res = await fetch('/chat', {
301 method: 'POST',
302 headers: { 'Content-Type': 'application/json' },
303 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
304 });
305
306 const data = await res.json();
307 if (data.response) {
308 appendMessage(data.response, 'hinana');
309 } else if (data.error) {
310 appendMessage('흐으응~ 오류가 난 것 같아요... ' + data.error, 'hinana');
311 }
312 } catch (err) {
313 console.error(err);
314 appendMessage('히나나랑 연결이 잠깐 끊어진 것 같아요...', 'hinana');
315 }
316 });
317 </script>
318 </body>
319 </html>
320