Public Source Viewer
비나래아카이브 개발자 포털
실제 서비스 구조를 살펴볼 수 있는 공개용 코드 뷰어입니다. 인증, 세션, 외부 연동, 토큰, 관리자 식별 등 보안상 민감한 구현은 파일 단위 또는 줄 단위로 검열됩니다.
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