Public Source Viewer
비나래아카이브 개발자 포털
실제 서비스 구조를 살펴볼 수 있는 공개용 코드 뷰어입니다. 인증, 세션, 외부 연동, 토큰, 관리자 식별 등 보안상 민감한 구현은 파일 단위 또는 줄 단위로 검열됩니다.
view/hinana/editPost.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 property="og:image" content="/image/2.png" />
10
<meta property="og:description" content="비나래 아카이브 도서관"/>
11
<meta property="og:url" content="https://hinana.moe/hinana/blog"/>
12
<meta property="og:title" content="비나래 아카이브 도서관"/>
13
<link rel='stylesheet' href='/vendors/bootstrap/css/bootstrap.min.css' />
14
<script src="/vendors/bootstrap/js/bootstrap.min.js"></script>
15
<link href="https://fonts.googleapis.com/css?family=Montserrat:400,700" rel="stylesheet" type="text/css">
16
<link href="https://fonts.googleapis.com/css?family=Lato:300,400,700" rel="stylesheet" type="text/css">
17
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css">
18
<link href="https://cdn.quilljs.com/1.3.7/quill.snow.css" rel="stylesheet">
19
<script src="https://cdn.quilljs.com/1.3.7/quill.min.js"></script>
20
<title>비나래 아카이브 도서관</title>
21
22
<style>
23
:root {
24
--font-family: 'Noto Sans KR', sans-serif;
25
--bg-main: #ffffff; --bg-secondary: #f7f9f9; --bg-tertiary: #eff3f4;
26
--text-primary: #0f1419; --text-secondary: #536471;
27
--accent-color: #1d9bf0; --danger-color: #f4212e; --border-color: #eff3f4;
28
--shadow-sm: 0 1px 3px rgba(0,0,0,0.08); --shadow-md: 0 4px 12px rgba(0,0,0,0.10);
29
}
30
body.dark-mode {
31
--bg-main: #000000; --bg-secondary: #16181c; --bg-tertiary: #202327;
32
--text-primary: #e7e9ea; --text-secondary: #71767b;
33
--accent-color: #1d9bf0; --danger-color: #f4212e; --border-color: #2f3336;
34
--shadow-sm: 0 1px 3px rgba(255,255,255,0.04); --shadow-md: 0 4px 12px rgba(0,0,0,0.6);
35
}
36
37
html, body { margin: 0; min-height: 100vh; font-family: var(--font-family); background-color: var(--bg-main); color: var(--text-primary); }
38
a { text-decoration: none; color: inherit; }
39
40
.global-header {
41
height: 60px;
42
background-color: rgba(255,255,255,0.85); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
43
border-bottom: 1px solid var(--border-color);
44
display: flex; align-items: center; justify-content: space-between; padding: 0 20px;
45
position: sticky; top: 0; z-index: 1000;
46
}
47
body.dark-mode .global-header { background-color: rgba(0,0,0,0.85); }
48
.header-brand { display: flex; align-items: center; }
49
.header-logo { height: 28px; width: auto; }
50
body:not(.dark-mode) .logo-night { display: none; }
51
body.dark-mode .logo-day { display: none; }
52
53
.header-nav {
54
position: absolute; left: 50%; transform: translateX(-50%);
55
display: flex; gap: 20px; align-items: center;
56
}
57
.nav-link { font-weight: 600; font-size: 0.95rem; color: var(--text-secondary); cursor: pointer; transition: color 0.2s; }
58
.nav-link:hover { color: var(--accent-color); }
59
.nav-link.active { color: var(--text-primary); }
60
.nav-divider { opacity: 0.3; color: var(--text-secondary); }
61
62
.header-controls { display: flex; align-items: center; gap: 10px; z-index: 10; position: relative; }
63
64
/* Quill 에디터 테마 */
65
.ql-toolbar { border-color: var(--border-color) !important; background: var(--bg-tertiary); border-radius: 8px 8px 0 0; }
66
.ql-container { border-color: var(--border-color) !important; background: var(--bg-main); border-radius: 0 0 8px 8px; font-size: 1rem; font-family: var(--font-family); min-height: 400px; }
67
.ql-editor { min-height: 400px; color: var(--text-primary); }
68
.ql-editor.ql-blank::before { color: var(--text-secondary); }
69
body.dark-mode .ql-toolbar .ql-stroke { stroke: var(--text-secondary); }
70
body.dark-mode .ql-toolbar .ql-fill { fill: var(--text-secondary); }
71
body.dark-mode .ql-toolbar .ql-picker { color: var(--text-secondary); }
72
body.dark-mode .ql-toolbar button:hover .ql-stroke,
73
body.dark-mode .ql-toolbar .ql-active .ql-stroke { stroke: var(--accent-color); }
74
body.dark-mode .ql-toolbar button:hover .ql-fill,
75
body.dark-mode .ql-toolbar .ql-active .ql-fill { fill: var(--accent-color); }
76
body.dark-mode .ql-picker-options { background: var(--bg-secondary); border-color: var(--border-color); }
77
body.dark-mode .ql-picker-item { color: var(--text-primary); }
78
.icon-btn { border: none; background: transparent; color: var(--text-secondary); font-size: 1.1rem; cursor: pointer; padding: 4px; transition: color 0.2s; }
79
.icon-btn:hover { color: var(--text-primary); }
80
81
.content-area {
82
max-width: 900px; margin: 40px auto; padding: 0 20px;
83
}
84
85
.write-card {
86
background-color: var(--bg-secondary); border: 1px solid var(--border-color);
87
border-radius: 12px; box-shadow: var(--shadow-sm); padding: 40px;
88
}
89
.write-card h2 { font-size: 1.5rem; font-weight: 700; color: var(--accent-color); margin-bottom: 30px; }
90
91
.form-control {
92
background-color: var(--bg-main); border: 1px solid var(--border-color);
93
color: var(--text-primary); border-radius: 8px;
94
}
95
.form-control:focus {
96
border-color: var(--accent-color); box-shadow: 0 0 0 2px rgba(29, 155, 240, 0.15);
97
}
98
.form-label { color: var(--text-secondary); font-weight: 600; font-size: 0.9rem; }
99
.tag-options { display: flex; flex-wrap: wrap; gap: 8px; }
100
.tag-chip-check { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--border-color); border-radius: 999px; padding: 7px 12px; background: var(--bg-main); color: var(--text-primary); cursor: pointer; font-weight: 600; }
101
.tag-chip-check input { accent-color: var(--accent-color); }
102
.tag-delete-btn { border: none; background: transparent; color: var(--text-secondary); padding: 0 0 0 2px; line-height: 1; cursor: pointer; }
103
.tag-delete-btn:hover { color: #dc3545; }
104
.tag-create-row { display: flex; gap: 8px; margin-top: 10px; }
105
.tag-create-row .form-control { max-width: 220px; }
106
107
.btn-write {
108
background-color: var(--accent-color); color: var(--ui-on-accent, #fff); border: none;
109
padding: 10px 30px; border-radius: 8px; font-weight: 600; cursor: pointer; transition: opacity 0.2s;
110
}
111
.btn-write:hover { opacity: 0.85; color: white; }
112
113
.btn-home {
114
border: 1px solid var(--border-color); background: transparent;
115
color: var(--text-secondary); padding: 10px 30px; border-radius: 8px; cursor: pointer; transition: all 0.2s;
116
}
117
.btn-home:hover { border-color: var(--accent-color); color: var(--accent-color); }
118
119
.footer-area {
120
text-align: center; padding: 40px 20px; color: var(--text-secondary); font-size: 0.75rem; line-height: 1.8;
121
}
122
.footer-area img { width: 160px; opacity: 0.7; mix-blend-mode: multiply; }
123
body.dark-mode .footer-area img { mix-blend-mode: screen; }
124
125
@media (max-width: 768px) {
126
.header-nav { position: static; transform: none; gap: 12px; }
127
.global-header { flex-wrap: wrap; height: auto; padding: 10px 15px; gap: 8px; }
128
.write-card { padding: 25px; }
129
}
130
</style>
131
<link rel="stylesheet" href="/css/site-ui.css?v=3">
132
<link rel="stylesheet" href="/css/site-app.css?v=1">
133
</head>
134
135
<body data-ui-page="editPost" class="ui-shell <%= (typeof theme !== 'undefined' && theme === 'dark') ? 'dark-mode' : '' %>">
136
<%- include('partials/site-header', { uiPage: 'editPost' }) %>
137
<div id="ui-content" tabindex="-1" class="ui-content-anchor"></div>
138
139
<div class="content-area">
140
<div class="write-card">
141
<h2>게시물 수정</h2>
142
<form action="/hinana/edit-post" method="POST">
143
<input type="hidden" name="postId" value="<%= post.id %>">
144
<div class="mb-3">
145
<label for="title" class="form-label">제목</label>
146
<input type="text" name="title" class="form-control" value="<%= post.title %>" required>
147
</div>
148
<div class="mb-3">
149
<label class="form-label">태그</label>
150
<% const selectedPostTags = Array.isArray(post.tags) ? post.tags : []; %>
151
<div class="tag-options">
152
<% if (typeof availableTags !== 'undefined' && availableTags.length) { %>
153
<% availableTags.forEach(function(tag) { %>
154
<label class="tag-chip-check">
155
<input type="checkbox" name="tags" value="<%= tag %>" <%= selectedPostTags.includes(tag) ? 'checked' : '' %>>
156
<span>#<%= tag %></span>
157
<button type="button" data-tag="<%= tag %>" class="tag-delete-btn" title="태그 삭제" onclick="deleteBlogTagFromButton(event, this)">×</button>
158
</label>
159
<% }) %>
160
<% } else { %>
161
<span class="text-secondary small">생성된 태그가 없습니다.</span>
162
<% } %>
163
</div>
164
<div class="tag-create-row">
165
<input type="text" name="tagName" form="blog-tag-form" class="form-control" maxlength="20" placeholder="새 태그">
166
<button type="submit" form="blog-tag-form" class="btn-home">태그 추가</button>
167
</div>
168
</div>
169
<div class="mb-3">
170
<label class="form-label">내용</label>
171
<div id="quill-editor"></div>
172
<input type="hidden" id="content" name="content">
173
</div>
174
<div class="d-flex gap-2">
175
<button type="submit" class="btn-write">수정하기</button>
176
<a href="/hinana/blog" class="btn-home">취소</a>
177
</div>
178
</form>
179
<form id="blog-tag-form" action="/hinana/blog/tags" method="POST">
180
<input type="hidden" name="redirect" value="/hinana/edit-post/<%= post.id %>">
181
</form>
182
<form id="blog-tag-delete-form" action="/hinana/blog/tags/delete" method="POST">
183
<input type="hidden" name="redirect" value="/hinana/edit-post/<%= post.id %>">
184
<input type="hidden" name="tagName" value="">
185
</form>
186
</div>
187
</div>
188
189
<div class="footer-area">
190
<img src="/image/sign.png" alt="sign"><br>
191
<strong>비나래 ARCHIVE</strong><br>
192
X - @NoctchillHinana<br>
193
© 2024~2026. 비나래 | hinana.moe
194
</div>
195
196
<script>
197
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
198
199
const quill = new Quill('#quill-editor', {
200
theme: 'snow',
201
modules: {
202
toolbar: {
203
container: [
204
[{ header: [1, 2, 3, false] }],
205
['bold', 'italic', 'underline'],
206
[{ list: 'ordered' }, { list: 'bullet' }],
207
['link', 'image'],
208
['clean']
209
],
210
handlers: { image: quillImageHandler }
211
}
212
}
213
});
214
215
quill.clipboard.dangerouslyPasteHTML(initialContent);
216
217
function quillImageHandler() {
218
const input = document.createElement('input');
219
input.setAttribute('type', 'file');
220
input.setAttribute('accept', 'image/*');
221
input.click();
222
input.onchange = function () {
223
const file = input.files[0];
224
if (!file) return;
225
const fd = new FormData();
226
fd.append('image', file);
227
fetch('/upload/image', { method: 'POST', body: fd })
228
.then(r => r.json())
229
.then(data => {
230
const range = quill.getSelection();
231
quill.insertEmbed(range ? range.index : 0, 'image', data.location);
232
})
233
.catch(() => alert('이미지 업로드에 실패했어요.'));
234
};
235
}
236
237
document.querySelector('form[action="/hinana/edit-post"]').addEventListener('submit', function () {
238
document.getElementById('content').value = quill.root.innerHTML;
239
});
240
241
function deleteBlogTagFromButton(event, button) {
242
event.preventDefault();
243
event.stopPropagation();
244
245
if (!confirm('태그를 삭제할까요?')) {
246
return;
247
}
248
249
const form = document.getElementById('blog-tag-delete-form');
250
form.querySelector('input[name="tagName"]').value = button.dataset.tag || '';
251
form.submit();
252
}
253
</script>
254
</body>
255
</html>
256