ginipick commited on
Commit
88cca71
·
1 Parent(s): ee971b5

Create 공개버전-시크릿에 담을 html 코드 코드.html

Browse files
공개버전-시크릿에 담을 html 코드 코드.html ADDED
@@ -0,0 +1,2590 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="ko">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>FlipBook Space</title>
6
+ <link rel="stylesheet" href="/static/flipbook.css">
7
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
8
+ <script src="/static/three.js"></script>
9
+ <script src="/static/iscroll.js"></script>
10
+ <script src="/static/mark.js"></script>
11
+ <script src="/static/mod3d.js"></script>
12
+ <script src="/static/pdf.js"></script>
13
+ <script src="/static/flipbook.js"></script>
14
+ <script src="/static/flipbook.book3.js"></script>
15
+ <script src="/static/flipbook.scroll.js"></script>
16
+ <script src="/static/flipbook.swipe.js"></script>
17
+ <script src="/static/flipbook.webgl.js"></script>
18
+ <style>
19
+ /* 전체 사이트 파스텔톤 테마 */
20
+ :root {
21
+ --primary-color: #a5d8ff; /* 파스텔 블루 */
22
+ --secondary-color: #ffd6e0; /* 파스텔 핑크 */
23
+ --tertiary-color: #c3fae8; /* 파스텔 민트 */
24
+ --accent-color: #d0bfff; /* 파스텔 퍼플 */
25
+ --ai-color: #86e8ab; /* AI 버튼 색상 */
26
+ --ai-hover: #65d68a; /* AI 호버 색상 */
27
+ --bg-color: #f8f9fa; /* 밝은 배경 */
28
+ --text-color: #495057; /* 부드러운 어두운 색 */
29
+ --card-bg: #ffffff; /* 카드 배경색 */
30
+ --shadow-sm: 0 2px 8px rgba(0,0,0,0.05);
31
+ --shadow-md: 0 4px 12px rgba(0,0,0,0.08);
32
+ --shadow-lg: 0 8px 24px rgba(0,0,0,0.12);
33
+ --radius-sm: 8px;
34
+ --radius-md: 12px;
35
+ --radius-lg: 16px;
36
+ --transition: all 0.3s ease;
37
+ }
38
+
39
+
40
+ body {
41
+ margin: 0;
42
+ background: var(--bg-color);
43
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
44
+ color: var(--text-color);
45
+ /* 새로운 퍼플 계통 고급스러운 그라디에이션 배경 */
46
+ background-image: linear-gradient(135deg, #2a0845 0%, #6441a5 50%, #c9a8ff 100%);
47
+ background-attachment: fixed;
48
+ }
49
+
50
+ /* 뷰어 모드일 때 배경 변경 */
51
+ .viewer-mode {
52
+ background-image: linear-gradient(135deg, #30154e 0%, #6b47ad 50%, #d5b8ff 100%) !important;
53
+ }
54
+
55
+ /* 헤더 제목 제거 및 Home 버튼 레이어 처리 */
56
+ .floating-home, .floating-ai {
57
+ position: fixed;
58
+ top: 20px;
59
+ left: 20px;
60
+ width: 60px;
61
+ height: 60px;
62
+ border-radius: 50%;
63
+ background: rgba(255, 255, 255, 0.9);
64
+ backdrop-filter: blur(10px);
65
+ box-shadow: var(--shadow-md);
66
+ z-index: 9999;
67
+ display: flex;
68
+ justify-content: center;
69
+ align-items: center;
70
+ cursor: pointer;
71
+ transition: var(--transition);
72
+ overflow: hidden;
73
+ }
74
+
75
+ .floating-ai {
76
+ top: 90px; /* Home 버튼 아래에 위치 */
77
+ background: rgba(134, 232, 171, 0.9); /* AI 버튼 색상 */
78
+ }
79
+
80
+ .floating-home:hover, .floating-ai:hover {
81
+ transform: scale(1.05);
82
+ box-shadow: var(--shadow-lg);
83
+ }
84
+
85
+ .floating-home .icon, .floating-ai .icon {
86
+ display: flex;
87
+ justify-content: center;
88
+ align-items: center;
89
+ width: 100%;
90
+ height: 100%;
91
+ font-size: 22px;
92
+ color: var(--primary-color);
93
+ transition: var(--transition);
94
+ }
95
+
96
+ .floating-ai .icon {
97
+ color: white;
98
+ }
99
+
100
+ .floating-home:hover .icon {
101
+ color: #8bc5f8;
102
+ }
103
+
104
+ .floating-ai:hover .icon {
105
+ color: #ffffff;
106
+ }
107
+
108
+ .floating-home .title, .floating-ai .title {
109
+ position: absolute;
110
+ left: 70px;
111
+ background: rgba(255, 255, 255, 0.95);
112
+ padding: 8px 20px;
113
+ border-radius: 20px;
114
+ box-shadow: var(--shadow-sm);
115
+ font-weight: 600;
116
+ font-size: 14px;
117
+ white-space: nowrap;
118
+ pointer-events: none;
119
+ opacity: 0;
120
+ transform: translateX(-10px);
121
+ transition: all 0.3s ease;
122
+ }
123
+
124
+ .floating-home:hover .title, .floating-ai:hover .title {
125
+ opacity: 1;
126
+ transform: translateX(0);
127
+ }
128
+
129
+ /* 관리자 버튼 스타일 */
130
+ #adminButton {
131
+ position: fixed;
132
+ top: 20px;
133
+ right: 20px;
134
+ background: rgba(255, 255, 255, 0.9);
135
+ backdrop-filter: blur(10px);
136
+ box-shadow: var(--shadow-md);
137
+ border-radius: 30px;
138
+ padding: 8px 20px;
139
+ display: flex;
140
+ align-items: center;
141
+ font-weight: 600;
142
+ font-size: 14px;
143
+ cursor: pointer;
144
+ transition: var(--transition);
145
+ z-index: 9999;
146
+ }
147
+
148
+ #adminButton i {
149
+ margin-right: 8px;
150
+ color: var(--accent-color);
151
+ }
152
+
153
+ #adminButton:hover {
154
+ transform: translateY(-3px);
155
+ box-shadow: var(--shadow-lg);
156
+ }
157
+
158
+ /* 관리자 로그인 모달 */
159
+ .modal {
160
+ display: none;
161
+ position: fixed;
162
+ top: 0;
163
+ left: 0;
164
+ width: 100%;
165
+ height: 100%;
166
+ background: rgba(0, 0, 0, 0.5);
167
+ backdrop-filter: blur(5px);
168
+ z-index: 10000;
169
+ align-items: center;
170
+ justify-content: center;
171
+ }
172
+
173
+ .modal-content {
174
+ background: white;
175
+ border-radius: var(--radius-md);
176
+ padding: 30px;
177
+ width: 90%;
178
+ max-width: 400px;
179
+ box-shadow: var(--shadow-lg);
180
+ text-align: center;
181
+ }
182
+
183
+ .modal-content h2 {
184
+ margin-top: 0;
185
+ color: var(--accent-color);
186
+ margin-bottom: 20px;
187
+ }
188
+
189
+ .modal-content input {
190
+ width: 100%;
191
+ padding: 12px;
192
+ margin-bottom: 20px;
193
+ border: 1px solid #ddd;
194
+ border-radius: var(--radius-sm);
195
+ font-size: 16px;
196
+ box-sizing: border-box;
197
+ }
198
+
199
+ .modal-content button {
200
+ padding: 10px 20px;
201
+ border: none;
202
+ border-radius: var(--radius-sm);
203
+ background: var(--accent-color);
204
+ color: white;
205
+ font-weight: 600;
206
+ cursor: pointer;
207
+ margin: 0 5px;
208
+ transition: var(--transition);
209
+ }
210
+
211
+ .modal-content button:hover {
212
+ opacity: 0.9;
213
+ transform: translateY(-2px);
214
+ }
215
+
216
+ .modal-content #adminLoginClose {
217
+ background: #f1f3f5;
218
+ color: var(--text-color);
219
+ }
220
+
221
+ #home, #viewerPage, #adminPage {
222
+ padding-top: 100px;
223
+ max-width: 1200px;
224
+ margin: 0 auto;
225
+ padding-bottom: 60px;
226
+ padding-left: 30px;
227
+ padding-right: 30px;
228
+ position: relative;
229
+ }
230
+
231
+ /* 업로드 버튼 스타일 */
232
+ .upload-container {
233
+ display: flex;
234
+ margin-bottom: 30px;
235
+ justify-content: center;
236
+ }
237
+
238
+ button.upload {
239
+ all: unset;
240
+ cursor: pointer;
241
+ padding: 12px 20px;
242
+ border-radius: var(--radius-md);
243
+ background: white;
244
+ margin: 0 10px;
245
+ font-weight: 500;
246
+ display: flex;
247
+ align-items: center;
248
+
249
+
250
+ box-shadow: var(--shadow-sm);
251
+ transition: var(--transition);
252
+ position: relative;
253
+ overflow: hidden;
254
+ }
255
+
256
+ button.upload::before {
257
+ content: '';
258
+ position: absolute;
259
+ top: 0;
260
+ left: 0;
261
+ width: 100%;
262
+ height: 100%;
263
+ background: linear-gradient(120deg, var(--primary-color) 0%, var(--secondary-color) 100%);
264
+ opacity: 0.08;
265
+ z-index: -1;
266
+ }
267
+
268
+ button.upload:hover {
269
+ transform: translateY(-3px);
270
+ box-shadow: var(--shadow-md);
271
+ }
272
+
273
+ button.upload:hover::before {
274
+ opacity: 0.15;
275
+ }
276
+
277
+ button.upload i {
278
+ margin-right: 8px;
279
+ font-size: 20px;
280
+ }
281
+
282
+ /* 그리드 및 카드 스타일 */
283
+ .grid {
284
+ display: grid;
285
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
286
+ gap: 24px;
287
+ margin-top: 36px;
288
+ }
289
+
290
+ .card {
291
+ background: var(--card-bg);
292
+ border-radius: var(--radius-md);
293
+ cursor: pointer;
294
+ box-shadow: var(--shadow-sm);
295
+ width: 100%;
296
+ height: 280px;
297
+ position: relative;
298
+ display: flex;
299
+ flex-direction: column;
300
+ align-items: center;
301
+ justify-content: center;
302
+ transition: var(--transition);
303
+ overflow: hidden;
304
+ }
305
+
306
+ .card::before {
307
+ content: '';
308
+ position: absolute;
309
+ top: 0;
310
+ left: 0;
311
+ width: 100%;
312
+ height: 100%;
313
+ background: linear-gradient(135deg, var(--secondary-color) 0%, var(--primary-color) 100%);
314
+ opacity: 0.06;
315
+ z-index: 1;
316
+ }
317
+
318
+ .card::after {
319
+ content: '';
320
+ position: absolute;
321
+ top: 0;
322
+ left: 0;
323
+ width: 100%;
324
+ height: 30%;
325
+ background: linear-gradient(to bottom, rgba(255,255,255,0.8) 0%, rgba(255,255,255,0) 100%);
326
+ z-index: 2;
327
+ }
328
+
329
+ .card img {
330
+ width: 65%;
331
+ height: auto;
332
+ object-fit: contain;
333
+ position: absolute;
334
+ top: 50%;
335
+ left: 50%;
336
+ transform: translate(-50%, -65%);
337
+ border: 1px solid rgba(0,0,0,0.05);
338
+ box-shadow: 0 4px 15px rgba(0,0,0,0.08);
339
+ z-index: 3;
340
+ transition: var(--transition);
341
+ }
342
+
343
+ .card:hover {
344
+ transform: translateY(-5px);
345
+ box-shadow: var(--shadow-md);
346
+ }
347
+
348
+ .card:hover img {
349
+ transform: translate(-50%, -65%) scale(1.03);
350
+ box-shadow: 0 8px 20px rgba(0,0,0,0.12);
351
+ }
352
+
353
+ .card p {
354
+ position: absolute;
355
+ bottom: 20px;
356
+ left: 50%;
357
+ transform: translateX(-50%);
358
+ background: rgba(255, 255, 255, 0.9);
359
+ padding: 8px 16px;
360
+ border-radius: 30px;
361
+ box-shadow: 0 2px 10px rgba(0,0,0,0.05);
362
+ width: 80%;
363
+ text-align: center;
364
+ white-space: nowrap;
365
+ overflow: hidden;
366
+ text-overflow: ellipsis;
367
+ font-size: 14px;
368
+ font-weight: 500;
369
+ color: var(--text-color);
370
+ z-index: 4;
371
+ transition: var(--transition);
372
+ }
373
+
374
+ .card:hover p {
375
+ background: rgba(255, 255, 255, 0.95);
376
+ box-shadow: 0 4px 12px rgba(0,0,0,0.08);
377
+ }
378
+
379
+ /* 캐시 상태 뱃지 */
380
+ .cached-status {
381
+ position: absolute;
382
+ top: 10px;
383
+ right: 10px;
384
+ background: var(--accent-color);
385
+ color: white;
386
+ font-size: 11px;
387
+ padding: 3px 8px;
388
+ border-radius: 12px;
389
+ z-index: 5;
390
+ box-shadow: var(--shadow-sm);
391
+ }
392
+
393
+ /* 관리자 카드 추가 스타일 */
394
+ .admin-card {
395
+ height: 300px;
396
+ }
397
+
398
+ .admin-card .card-inner {
399
+ width: 100%;
400
+ height: 100%;
401
+ position: relative;
402
+ display: flex;
403
+ flex-direction: column;
404
+ align-items: center;
405
+ }
406
+
407
+ .delete-btn, .feature-btn, .unfeature-btn {
408
+ position: absolute;
409
+ bottom: 60px;
410
+ left: 50%;
411
+ transform: translateX(-50%);
412
+ background: #ff7675;
413
+ color: white;
414
+ border: none;
415
+ border-radius: 20px;
416
+ padding: 5px 15px;
417
+ font-size: 12px;
418
+ cursor: pointer;
419
+ z-index: 10;
420
+ transition: var(--transition);
421
+ }
422
+
423
+ .feature-btn {
424
+ bottom: 95px;
425
+ background: #74b9ff;
426
+ }
427
+
428
+ .unfeature-btn {
429
+ bottom: 95px;
430
+ background: #a29bfe;
431
+ }
432
+
433
+ .delete-btn:hover, .feature-btn:hover, .unfeature-btn:hover {
434
+ opacity: 0.9;
435
+ transform: translateX(-50%) scale(1.05);
436
+ }
437
+
438
+ /* 뷰어 스타일 */
439
+ #viewer {
440
+ width: 90%;
441
+ height: 90vh;
442
+ max-width: 90%;
443
+ margin: 0;
444
+ background: var(--card-bg);
445
+ border: none;
446
+ border-radius: var(--radius-lg);
447
+ position: fixed;
448
+ top: 50%;
449
+ left: 50%;
450
+ transform: translate(-50%, -50%);
451
+ z-index: 1000;
452
+ box-shadow: var(--shadow-lg);
453
+ max-height: calc(90vh - 40px);
454
+ aspect-ratio: auto;
455
+ object-fit: contain;
456
+ overflow: hidden;
457
+ }
458
+
459
+ /* FlipBook 컨트롤바 스타일 */
460
+ .flipbook-container .fb3d-menu-bar {
461
+ z-index: 2000 !important;
462
+ opacity: 1 !important;
463
+ bottom: 0 !important;
464
+ background-color: rgba(255,255,255,0.9) !important;
465
+ backdrop-filter: blur(10px) !important;
466
+ border-radius: 0 0 var(--radius-lg) var(--radius-lg) !important;
467
+ padding: 12px 0 !important;
468
+ box-shadow: 0 -4px 20px rgba(0,0,0,0.1) !important;
469
+ }
470
+
471
+ .flipbook-container .fb3d-menu-bar > ul > li > img,
472
+ .flipbook-container .fb3d-menu-bar > ul > li > div {
473
+ opacity: 1 !important;
474
+ transform: scale(1.2) !important;
475
+ filter: drop-shadow(0 2px 3px rgba(0,0,0,0.1)) !important;
476
+ }
477
+
478
+ .flipbook-container .fb3d-menu-bar > ul > li {
479
+ margin: 0 12px !important;
480
+ }
481
+
482
+ /* 메뉴 툴팁 스타일 */
483
+ .flipbook-container .fb3d-menu-bar > ul > li > span {
484
+ background-color: rgba(0,0,0,0.7) !important;
485
+ color: white !important;
486
+ border-radius: var(--radius-sm) !important;
487
+ padding: 8px 12px !important;
488
+ font-size: 13px !important;
489
+ bottom: 55px !important;
490
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important;
491
+ letter-spacing: 0.3px !important;
492
+ }
493
+
494
+ /* 뷰어 모드일 때 배경 오버레이 */
495
+ .viewer-mode {
496
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%) !important;
497
+ }
498
+
499
+ /* 뷰어 페이지 배경 */
500
+ #viewerPage {
501
+ background: transparent;
502
+ }
503
+
504
+ /* 로딩 애니메이션 */
505
+ @keyframes spin {
506
+ 0% { transform: rotate(0deg); }
507
+ 100% { transform: rotate(360deg); }
508
+ }
509
+
510
+ .loading-spinner {
511
+ border: 4px solid rgba(255,255,255,0.3);
512
+ border-top: 4px solid var(--primary-color);
513
+ border-radius: 50%;
514
+ width: 50px;
515
+ height: 50px;
516
+ margin: 0 auto;
517
+ animation: spin 1.5s ease-in-out infinite;
518
+ }
519
+
520
+ .loading-container {
521
+ position: absolute;
522
+ top: 50%;
523
+ left: 50%;
524
+ transform: translate(-50%, -50%);
525
+ text-align: center;
526
+ background: rgba(255, 255, 255, 0.85);
527
+ backdrop-filter: blur(10px);
528
+ padding: 30px;
529
+ border-radius: var(--radius-md);
530
+ box-shadow: var(--shadow-md);
531
+ z-index: 9999;
532
+ }
533
+
534
+ .loading-text {
535
+ margin-top: 20px;
536
+ font-size: 16px;
537
+ color: var(--text-color);
538
+ font-weight: 500;
539
+ }
540
+
541
+ /* 페이지 전환 애니메이션 */
542
+ @keyframes fadeIn {
543
+ from { opacity: 0; }
544
+ to { opacity: 1; }
545
+ }
546
+
547
+ .fade-in {
548
+ animation: fadeIn 0.5s ease-out;
549
+ }
550
+
551
+ /* 추가 스타일 */
552
+ .section-title {
553
+ font-size: 1.3rem;
554
+ font-weight: 600;
555
+ margin: 30px 0 15px;
556
+ color: var(--text-color);
557
+ }
558
+
559
+ .no-projects {
560
+ text-align: center;
561
+ margin: 40px 0;
562
+ color: var(--text-color);
563
+ font-size: 16px;
564
+ }
565
+
566
+ /* 프로그레스 바 */
567
+ .progress-bar-container {
568
+ width: 100%;
569
+ height: 6px;
570
+ background-color: rgba(0,0,0,0.1);
571
+ border-radius: 3px;
572
+ margin-top: 15px;
573
+ overflow: hidden;
574
+ }
575
+
576
+ .progress-bar {
577
+ height: 100%;
578
+ background: linear-gradient(to right, var(--primary-color), var(--accent-color));
579
+ border-radius: 3px;
580
+ transition: width 0.3s ease;
581
+ }
582
+
583
+ /* 헤더 로고 및 타이틀 */
584
+ .library-header {
585
+ position: fixed;
586
+ top: 12px;
587
+ left: 0;
588
+ right: 0;
589
+ text-align: center;
590
+ z-index: 100;
591
+ pointer-events: none;
592
+ }
593
+
594
+ .library-header .title {
595
+ display: inline-block;
596
+ padding: 8px 24px; /* 패딩 축소 */
597
+ background: rgba(255, 255, 255, 0.85);
598
+ backdrop-filter: blur(10px);
599
+ border-radius: 25px; /* 테두리 모서리 축소 */
600
+ box-shadow: var(--shadow-md);
601
+ font-size: 1.25rem; /* 글자 크기 축소 (1.5rem에서 1.25rem으로) */
602
+ font-weight: 600;
603
+ background-image: linear-gradient(120deg, #8e74eb 0%, #9d66ff 100%); /* 제목 색상도 바탕화면과 어울리게 변경 */
604
+ -webkit-background-clip: text;
605
+ background-clip: text;
606
+ color: transparent;
607
+ pointer-events: all;
608
+ }
609
+
610
+ /* 점진적 로딩 표시 */
611
+ .loading-pages {
612
+ position: absolute;
613
+ bottom: 20px;
614
+ left: 50%;
615
+ transform: translateX(-50%);
616
+ background: rgba(255, 255, 255, 0.9);
617
+ padding: 10px 20px;
618
+ border-radius: 20px;
619
+ box-shadow: var(--shadow-md);
620
+ font-size: 14px;
621
+ color: var(--text-color);
622
+ z-index: 9998;
623
+ text-align: center;
624
+ }
625
+
626
+ /* 관리자 페이지 스타일 */
627
+ #adminPage {
628
+ color: white;
629
+ max-width: 1400px;
630
+ }
631
+
632
+ #adminPage h1 {
633
+ font-size: 2rem;
634
+ margin-bottom: 30px;
635
+ text-align: center;
636
+ background-image: linear-gradient(120deg, #e0c3fc 0%, #8ec5fc 100%);
637
+ -webkit-background-clip: text;
638
+ background-clip: text;
639
+ color: transparent;
640
+ }
641
+
642
+ #adminBackButton {
643
+ position: absolute;
644
+ top: 20px;
645
+ left: 20px;
646
+ background: rgba(255, 255, 255, 0.9);
647
+ backdrop-filter: blur(10px);
648
+ box-shadow: var(--shadow-md);
649
+ border: none;
650
+ border-radius: 30px;
651
+ padding: 8px 20px;
652
+ display: flex;
653
+ align-items: center;
654
+ font-weight: 600;
655
+ font-size: 14px;
656
+ cursor: pointer;
657
+ transition: var(--transition);
658
+ }
659
+
660
+ #adminBackButton:hover {
661
+ transform: translateY(-3px);
662
+ box-shadow: var(--shadow-lg);
663
+ }
664
+
665
+ /* 관리자 그리드 */
666
+ #adminGrid {
667
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
668
+ }
669
+
670
+ /* AI 챗봇 UI 스타일 */
671
+ #aiChatContainer {
672
+ display: none;
673
+ position: fixed;
674
+ top: 0;
675
+ right: 0;
676
+ width: 400px;
677
+ height: 100%;
678
+ background: rgba(255, 255, 255, 0.95);
679
+ backdrop-filter: blur(10px);
680
+ box-shadow: -5px 0 20px rgba(0, 0, 0, 0.1);
681
+ z-index: 9999;
682
+ transition: all 0.3s ease;
683
+ transform: translateX(100%);
684
+ padding: 20px;
685
+ box-sizing: border-box;
686
+ display: flex;
687
+ flex-direction: column;
688
+ }
689
+
690
+ #aiChatContainer.active {
691
+ transform: translateX(0);
692
+ }
693
+
694
+ #aiChatHeader {
695
+ display: flex;
696
+ justify-content: space-between;
697
+ align-items: center;
698
+ margin-bottom: 15px;
699
+ padding-bottom: 15px;
700
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
701
+ }
702
+
703
+ #aiChatHeader h3 {
704
+ margin: 0;
705
+ color: #333;
706
+ font-size: 18px;
707
+ display: flex;
708
+ align-items: center;
709
+ }
710
+
711
+ #aiChatHeader h3 i {
712
+ margin-right: 10px;
713
+ color: var(--ai-color);
714
+ }
715
+
716
+ #aiChatClose {
717
+ background: none;
718
+ border: none;
719
+ cursor: pointer;
720
+ font-size: 18px;
721
+ color: #666;
722
+ transition: var(--transition);
723
+ }
724
+
725
+ #aiChatClose:hover {
726
+ color: #333;
727
+ transform: scale(1.1);
728
+ }
729
+
730
+ #aiChatMessages {
731
+ flex: 1;
732
+ overflow-y: auto;
733
+ padding: 10px 0;
734
+ margin-bottom: 15px;
735
+ }
736
+
737
+ .chat-message {
738
+ margin-bottom: 15px;
739
+ display: flex;
740
+ align-items: flex-start;
741
+ }
742
+
743
+ .chat-message.user {
744
+ flex-direction: row-reverse;
745
+ }
746
+
747
+ .chat-avatar {
748
+ width: 35px;
749
+ height: 35px;
750
+ border-radius: 50%;
751
+ display: flex;
752
+ justify-content: center;
753
+ align-items: center;
754
+ margin-right: 10px;
755
+ flex-shrink: 0;
756
+ }
757
+
758
+ .chat-message.user .chat-avatar {
759
+ margin-right: 0;
760
+ margin-left: 10px;
761
+ background: var(--primary-color);
762
+ color: white;
763
+ }
764
+
765
+ .chat-message.ai .chat-avatar {
766
+ background: var(--ai-color);
767
+ color: white;
768
+ }
769
+
770
+ .chat-content {
771
+ background: #f1f1f1;
772
+ padding: 12px 15px;
773
+ border-radius: 18px;
774
+ max-width: 75%;
775
+ word-break: break-word;
776
+ position: relative;
777
+ font-size: 14px;
778
+ line-height: 1.4;
779
+ }
780
+
781
+ .chat-message.user .chat-content {
782
+ background: var(--primary-color);
783
+ color: white;
784
+ border-bottom-right-radius: 4px;
785
+ }
786
+
787
+ .chat-message.ai .chat-content {
788
+ background: #f1f1f1;
789
+ color: #333;
790
+ border-bottom-left-radius: 4px;
791
+ }
792
+
793
+ #aiChatForm {
794
+ display: flex;
795
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
796
+ padding-top: 15px;
797
+ }
798
+
799
+ #aiChatInput {
800
+ flex: 1;
801
+ padding: 12px 15px;
802
+ border: 1px solid #ddd;
803
+ border-radius: 25px;
804
+ font-size: 14px;
805
+ outline: none;
806
+ transition: var(--transition);
807
+ }
808
+
809
+ #aiChatInput:focus {
810
+ border-color: var(--ai-color);
811
+ box-shadow: 0 0 0 2px rgba(134, 232, 171, 0.2);
812
+ }
813
+
814
+ #aiChatSubmit {
815
+ background: var(--ai-color);
816
+ border: none;
817
+ color: white;
818
+ width: 45px;
819
+ height: 45px;
820
+ border-radius: 50%;
821
+ margin-left: 10px;
822
+ display: flex;
823
+ justify-content: center;
824
+ align-items: center;
825
+ cursor: pointer;
826
+ transition: var(--transition);
827
+ }
828
+
829
+ #aiChatSubmit:hover {
830
+ background: var(--ai-hover);
831
+ transform: scale(1.05);
832
+ }
833
+
834
+ #aiChatSubmit:disabled {
835
+ background: #ccc;
836
+ cursor: not-allowed;
837
+ }
838
+
839
+ .typing-indicator {
840
+ display: flex;
841
+ align-items: center;
842
+ margin-top: 5px;
843
+ font-size: 12px;
844
+ color: #666;
845
+ }
846
+
847
+ .typing-indicator span {
848
+ height: 8px;
849
+ width: 8px;
850
+ background: var(--ai-color);
851
+ border-radius: 50%;
852
+ display: inline-block;
853
+ margin-right: 3px;
854
+ animation: typing 1s infinite;
855
+ }
856
+
857
+ .typing-indicator span:nth-child(2) {
858
+ animation-delay: 0.2s;
859
+ }
860
+
861
+ .typing-indicator span:nth-child(3) {
862
+ animation-delay: 0.4s;
863
+ }
864
+
865
+ @keyframes typing {
866
+ 0% { transform: translateY(0); }
867
+ 50% { transform: translateY(-5px); }
868
+ 100% { transform: translateY(0); }
869
+ }
870
+
871
+ .chat-time {
872
+ font-size: 10px;
873
+ color: #999;
874
+ margin-top: 5px;
875
+ text-align: right;
876
+ }
877
+
878
+ /* 코드 블록 스타일 */
879
+ .chat-content pre {
880
+ background: rgba(0, 0, 0, 0.05);
881
+ padding: 10px;
882
+ border-radius: 5px;
883
+ overflow-x: auto;
884
+ font-family: monospace;
885
+ font-size: 12px;
886
+ margin: 10px 0;
887
+ }
888
+
889
+ /* 마크다운 스타일 */
890
+ .chat-content strong {
891
+ font-weight: bold;
892
+ }
893
+
894
+ .chat-content em {
895
+ font-style: italic;
896
+ }
897
+
898
+ .chat-content ul, .chat-content ol {
899
+ margin-left: 20px;
900
+ margin-top: 5px;
901
+ margin-bottom: 5px;
902
+ }
903
+
904
+ /* 공유 버튼 */
905
+ #shareChat {
906
+ padding: 8px 15px;
907
+ background: #f1f1f1;
908
+ border: none;
909
+ border-radius: 20px;
910
+ font-size: 12px;
911
+ color: #666;
912
+ cursor: pointer;
913
+ margin-top: 5px;
914
+ transition: var(--transition);
915
+ }
916
+
917
+ #shareChat:hover {
918
+ background: #ddd;
919
+ }
920
+
921
+ /* 반응형 디자인 */
922
+ @media (max-width: 768px) {
923
+ .grid {
924
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
925
+ gap: 16px;
926
+ }
927
+
928
+ .card {
929
+ height: 240px;
930
+ }
931
+
932
+ .library-header .title {
933
+ font-size: 1.25rem;
934
+ padding: 10px 20px;
935
+ }
936
+
937
+ .floating-home, .floating-ai {
938
+ width: 50px;
939
+ height: 50px;
940
+ }
941
+
942
+ .floating-home .icon, .floating-ai .icon {
943
+ font-size: 18px;
944
+ }
945
+
946
+ #adminButton {
947
+ padding: 6px 15px;
948
+ font-size: 12px;
949
+ }
950
+
951
+ #aiChatContainer {
952
+ width: 100%;
953
+ }
954
+ }
955
+ </style>
956
+ </head>
957
+ <body>
958
+ <!-- 제목을 Home 버튼과 함께 레이어로 처리 -->
959
+ <div id="homeButton" class="floating-home" style="display:none;">
960
+ <div class="icon"><i class="fas fa-home"></i></div>
961
+ <div class="title">홈으로 돌아가기</div>
962
+ </div>
963
+
964
+ <!-- AI 버튼 추가 -->
965
+ <div id="aiButton" class="floating-ai" style="display:none;">
966
+ <div class="icon"><i class="fas fa-robot"></i></div>
967
+ <div class="title">AI 어시스턴트</div>
968
+ </div>
969
+
970
+ <!-- AI 챗봇 컨테이너 -->
971
+ <div id="aiChatContainer">
972
+ <div id="aiChatHeader">
973
+ <h3><i class="fas fa-robot"></i> AI 어시스턴트</h3>
974
+ <button id="aiChatClose"><i class="fas fa-times"></i></button>
975
+ </div>
976
+ <div id="aiChatMessages"></div>
977
+ <form id="aiChatForm">
978
+ <input type="text" id="aiChatInput" placeholder="PDF에 대해 질문하세요..." autocomplete="off">
979
+ <button type="submit" id="aiChatSubmit"><i class="fas fa-paper-plane"></i></button>
980
+ </form>
981
+ </div>
982
+
983
+ <!-- 관리자 버튼 -->
984
+ <div id="adminButton">
985
+ <i class="fas fa-cog"></i> Admin
986
+ </div>
987
+
988
+ <!-- 관리자 로그인 모달 -->
989
+ <div id="adminLoginModal" class="modal">
990
+ <div class="modal-content">
991
+ <h2>관리자 로그인</h2>
992
+ <input type="password" id="adminPasswordInput" placeholder="관리자 비밀번호">
993
+ <div>
994
+ <button id="adminLoginButton">로그인</button>
995
+ <button id="adminLoginClose">취소</button>
996
+ </div>
997
+ </div>
998
+ </div>
999
+
1000
+ <!-- 센터 정렬된 타이틀 -->
1001
+ <div class="library-header">
1002
+ <div class="title">AI FlipBook Maker</div>
1003
+ </div>
1004
+
1005
+ <section id="home" class="fade-in">
1006
+ <div class="upload-container">
1007
+ <button class="upload" id="pdfUploadBtn">
1008
+ <i class="fas fa-file-pdf"></i> PDF Upload
1009
+ </button>
1010
+ <button class="upload" id="textToAIBookBtn">
1011
+ <i class="fas fa-file-alt"></i> Text to AI-Book
1012
+ </button>
1013
+ <input id="pdfInput" type="file" accept="application/pdf" style="display:none">
1014
+ <input id="textInput" type="file" accept=".txt,.docx,.doc" style="display:none">
1015
+ </div>
1016
+
1017
+ <div class="section-title">Projects</div>
1018
+ <div class="grid" id="grid">
1019
+ <!-- 카드가 여기에 동적으로 추가됩니다 -->
1020
+ </div>
1021
+ <div id="noProjects" class="no-projects" style="display: none;">
1022
+ 프로젝트가 없습니다. PDF를 추가하여 시작하세요.
1023
+ </div>
1024
+ </section>
1025
+
1026
+ <section id="viewerPage" style="display:none">
1027
+ <div id="viewer"></div>
1028
+ <div id="loadingPages" class="loading-pages" style="display:none;">페이지 로딩 중... <span id="loadingPagesCount">0/0</span></div>
1029
+ </section>
1030
+
1031
+ <!-- 관리자 페이지 -->
1032
+ <section id="adminPage" style="display:none" class="fade-in">
1033
+ <h1>관리자 페이지</h1>
1034
+ <button id="adminBackButton"><i class="fas fa-arrow-left"></i> 뒤로 가기</button>
1035
+
1036
+ <div class="section-title">저장된 PDF 목록</div>
1037
+ <div class="grid" id="adminGrid">
1038
+ <!-- 관리자 PDF 카드가 여기에 동적으로 추가됩니다 -->
1039
+ </div>
1040
+ <div id="noAdminProjects" class="no-projects" style="display: none;">
1041
+ 저장된 PDF가 없습니다. PDF를 업로드하여 시작하세요.
1042
+ </div>
1043
+ </section>
1044
+
1045
+ <script>
1046
+ let projects=[], fb=null;
1047
+ const grid=document.getElementById('grid'), viewer=document.getElementById('viewer');
1048
+ pdfjsLib.GlobalWorkerOptions.workerSrc='/static/pdf.worker.js';
1049
+
1050
+ // 서버에서 미리 로드된 PDF 프로젝트
1051
+ let serverProjects = [];
1052
+
1053
+ // 현재 페이지 로딩 상태
1054
+ let currentLoadingPdfPath = null;
1055
+ let pageLoadingInterval = null;
1056
+
1057
+ // 현재 열린 PDF의 ID
1058
+ let currentPdfId = null;
1059
+
1060
+ // 오디오 컨텍스트와 초기화 상태 관리
1061
+ let audioInitialized = false;
1062
+ let audioContext = null;
1063
+
1064
+ // AI 챗봇 관련 변수
1065
+ let isAiChatActive = false;
1066
+ let isAiProcessing = false;
1067
+ let hasLoadedSummary = false;
1068
+
1069
+ // 오디오 초기화 함수
1070
+ function initializeAudio() {
1071
+ if (audioInitialized) return Promise.resolve();
1072
+
1073
+ return new Promise((resolve) => {
1074
+ // 오디오 컨텍스트 생성 (사용자 상호작용이 필요 없는 초기화)
1075
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
1076
+
1077
+ // MP3 로드 및 초기화
1078
+ const audio = new Audio('/static/turnPage2.mp3');
1079
+ audio.volume = 0.01; // 최소 볼륨으로 설정
1080
+
1081
+ // 로드된 오디오 재생 시도 (사용자 상호작용 요구될 수 있음)
1082
+ const playPromise = audio.play();
1083
+
1084
+ if (playPromise !== undefined) {
1085
+ playPromise
1086
+ .then(() => {
1087
+ // 성공적으로 재생됨 - 즉시 일시정지
1088
+ audio.pause();
1089
+ audioInitialized = true;
1090
+ console.log('오디오 초기화 성공');
1091
+ resolve();
1092
+ })
1093
+ .catch((error) => {
1094
+ console.log('자동 오디오 초기화 실패, 사용자 상호작용 필요:', error);
1095
+
1096
+ // 사용자 상호작용이 필요한 경우, 이벤트 리스너 추가
1097
+ const initOnUserAction = function() {
1098
+ const tempAudio = new Audio('/static/turnPage2.mp3');
1099
+ tempAudio.volume = 0.01;
1100
+ tempAudio.play()
1101
+ .then(() => {
1102
+ tempAudio.pause();
1103
+ audioInitialized = true;
1104
+ console.log('사용자 상호작용으로 오디오 초기화 성공');
1105
+ resolve();
1106
+
1107
+ // 이벤트 리스너 제거
1108
+ ['click', 'touchstart', 'keydown'].forEach(event => {
1109
+ document.removeEventListener(event, initOnUserAction, { capture: true });
1110
+ });
1111
+ })
1112
+ .catch(e => console.error('오디오 초기화 실패:', e));
1113
+ };
1114
+
1115
+ // 사용자 상호작용 이벤트에 리스너 추가
1116
+ ['click', 'touchstart', 'keydown'].forEach(event => {
1117
+ document.addEventListener(event, initOnUserAction, { once: true, capture: true });
1118
+ });
1119
+
1120
+ // 페이지 로드 직후 사용자에게 오디오 활성화 요청
1121
+ if (window.location.pathname.startsWith('/view/')) {
1122
+ // 오디오 활성화 안내 메시지 표시 (바로가기 링크로 접속한 경우)
1123
+ setTimeout(() => {
1124
+ const audioPrompt = document.createElement('div');
1125
+ audioPrompt.style.position = 'fixed';
1126
+ audioPrompt.style.bottom = '80px';
1127
+ audioPrompt.style.left = '50%';
1128
+ audioPrompt.style.transform = 'translateX(-50%)';
1129
+ audioPrompt.style.backgroundColor = 'rgba(0,0,0,0.7)';
1130
+ audioPrompt.style.color = 'white';
1131
+ audioPrompt.style.padding = '10px 20px';
1132
+ audioPrompt.style.borderRadius = '20px';
1133
+ audioPrompt.style.zIndex = '10000';
1134
+ audioPrompt.style.cursor = 'pointer';
1135
+ audioPrompt.innerHTML = '페이지 어디든 클릭하여 소리 효과 활성화 <i class="fas fa-volume-up"></i>';
1136
+ audioPrompt.id = 'audioPrompt';
1137
+
1138
+ // 클릭 시 소리 활성화 및 메시지 제거
1139
+ audioPrompt.addEventListener('click', function() {
1140
+ initOnUserAction();
1141
+ audioPrompt.remove();
1142
+ });
1143
+
1144
+ document.body.appendChild(audioPrompt);
1145
+
1146
+ // 10초 후 자동으로 숨김
1147
+ setTimeout(() => {
1148
+ if (document.getElementById('audioPrompt')) {
1149
+ document.getElementById('audioPrompt').remove();
1150
+ }
1151
+ }, 10000);
1152
+ }, 2000);
1153
+ }
1154
+ });
1155
+ } else {
1156
+ // 브라우저가 Promise 기반 재생을 지원하지 않는 경우
1157
+ audioInitialized = true;
1158
+ resolve();
1159
+ }
1160
+ });
1161
+ }
1162
+
1163
+ /* ── 유틸 ── */
1164
+ function $id(id){return document.getElementById(id)}
1165
+
1166
+ // 현재 시간을 포맷팅하는 함수
1167
+ function formatTime() {
1168
+ const now = new Date();
1169
+ const hours = now.getHours().toString().padStart(2, '0');
1170
+ const minutes = now.getMinutes().toString().padStart(2, '0');
1171
+ return `${hours}:${minutes}`;
1172
+ }
1173
+
1174
+ // AI 챗봇 메시지 추가 함수
1175
+ function addChatMessage(content, isUser = false) {
1176
+ const messagesContainer = $id('aiChatMessages');
1177
+ const messageElement = document.createElement('div');
1178
+ messageElement.className = `chat-message ${isUser ? 'user' : 'ai'}`;
1179
+
1180
+ const currentTime = formatTime();
1181
+
1182
+ messageElement.innerHTML = `
1183
+ <div class="chat-avatar">
1184
+ <i class="fas ${isUser ? 'fa-user' : 'fa-robot'}"></i>
1185
+ </div>
1186
+ <div class="chat-bubble">
1187
+ <div class="chat-content">${content}</div>
1188
+ <div class="chat-time">${currentTime}</div>
1189
+ </div>
1190
+ `;
1191
+
1192
+ messagesContainer.appendChild(messageElement);
1193
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
1194
+ return messageElement;
1195
+ }
1196
+
1197
+ // 로딩 표시기 추가 함수
1198
+ function addTypingIndicator() {
1199
+ const messagesContainer = $id('aiChatMessages');
1200
+ const indicatorElement = document.createElement('div');
1201
+ indicatorElement.className = 'typing-indicator';
1202
+ indicatorElement.innerHTML = `
1203
+ <div class="chat-avatar">
1204
+ <i class="fas fa-robot"></i>
1205
+ </div>
1206
+ <div>
1207
+ <span></span>
1208
+ <span></span>
1209
+ <span></span>
1210
+ </div>
1211
+ `;
1212
+ messagesContainer.appendChild(indicatorElement);
1213
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
1214
+ return indicatorElement;
1215
+ }
1216
+
1217
+ // AI 챗봇 토글 함수
1218
+ function toggleAiChat(show = true) {
1219
+ const aiChatContainer = $id('aiChatContainer');
1220
+
1221
+ if (show) {
1222
+ // 챗봇 표시
1223
+ aiChatContainer.style.display = 'flex';
1224
+ setTimeout(() => {
1225
+ aiChatContainer.classList.add('active');
1226
+ }, 10);
1227
+ isAiChatActive = true;
1228
+
1229
+ // 처음 열 때 자동 요약 로드
1230
+ if (!hasLoadedSummary && currentPdfId) {
1231
+ loadPdfSummary();
1232
+ }
1233
+ } else {
1234
+ // 챗봇 숨기기
1235
+ aiChatContainer.classList.remove('active');
1236
+ setTimeout(() => {
1237
+ aiChatContainer.style.display = 'none';
1238
+ }, 300);
1239
+ isAiChatActive = false;
1240
+ }
1241
+ }
1242
+
1243
+ // PDF 요약 로드 함수
1244
+ // PDF 요약 로드 함수
1245
+ async function loadPdfSummary() {
1246
+ if (!currentPdfId || isAiProcessing || hasLoadedSummary) return;
1247
+
1248
+ try {
1249
+ isAiProcessing = true;
1250
+ const typingIndicator = addTypingIndicator();
1251
+
1252
+ // 서버에 요약 요청
1253
+ const response = await fetch(`/api/ai/summarize-pdf/${currentPdfId}`);
1254
+ const data = await response.json();
1255
+
1256
+ // 로딩 표시기 제거
1257
+ typingIndicator.remove();
1258
+
1259
+ if (data.error) {
1260
+ // 오류 메시지 표시
1261
+ addChatMessage(`PDF 요약을 생성하는 중 문제가 발생했습니다: ${data.error}<br><br>계속 질문을 입력하시면 PDF 내용을 기반으로 답변을 시도하겠습니다.`);
1262
+
1263
+ // 요약이 실패해도 특정 경우에는 사용자에게 알리고 계속 사용 가능하도록 설정
1264
+ if (data.summary) {
1265
+ addChatMessage(`<strong>PDF에서 추출한 정보:</strong><br>${data.summary}`);
1266
+ hasLoadedSummary = true;
1267
+ }
1268
+ } else {
1269
+ // 환영 메시지와 요약 추가
1270
+ addChatMessage(`안녕하세요! 이 PDF에 대해 어떤 것이든 질문해주세요. 제가 도와드리겠습니다.<br><br><strong>PDF 요약:</strong><br>${data.summary}`);
1271
+ hasLoadedSummary = true;
1272
+ }
1273
+ } catch (error) {
1274
+ console.error("PDF 요약 로드 오류:", error);
1275
+ addChatMessage(`PDF 요약을 로드하는 중 오류가 발생했습니다. 서버 연결을 확인해주세요.<br><br>어떤 질문이든 입력하시면 최선을 다해 답변하겠습니다.`);
1276
+ } finally {
1277
+ isAiProcessing = false;
1278
+ }
1279
+ }
1280
+
1281
+ // 질문 제출 함수
1282
+ async function submitQuestion(question) {
1283
+ if (!currentPdfId || isAiProcessing || !question.trim()) return;
1284
+
1285
+ try {
1286
+ isAiProcessing = true;
1287
+ $id('aiChatSubmit').disabled = true;
1288
+
1289
+ // 사용자 메시지 추가
1290
+ addChatMessage(question, true);
1291
+
1292
+ // 로딩 표시기 추가
1293
+ const typingIndicator = addTypingIndicator();
1294
+
1295
+ // 서버에 질의 요청
1296
+ const response = await fetch(`/api/ai/query-pdf/${currentPdfId}`, {
1297
+ method: 'POST',
1298
+ headers: {
1299
+ 'Content-Type': 'application/json'
1300
+ },
1301
+ body: JSON.stringify({ query: question }),
1302
+ // 타임아웃 설정 추가
1303
+ signal: AbortSignal.timeout(60000) // 60초 타임아웃
1304
+ });
1305
+
1306
+ const data = await response.json();
1307
+
1308
+ // 로딩 표시기 제거
1309
+ typingIndicator.remove();
1310
+
1311
+ if (data.error) {
1312
+ // 오류 메시지에 따라 다른 친절한 안내 제공
1313
+ if (data.error.includes("API 키")) {
1314
+ addChatMessage("죄송합니다. 현재 AI 서비스에 연결할 수 없습니다. 시스템 관리자에게 API 키 설정을 확인해달라고 요청해주세요.");
1315
+ } else if (data.error.includes("연결")) {
1316
+ addChatMessage("죄송합니다. AI 서비스에 연결할 수 없습니다. 인터넷 연결을 확인하거나 잠시 후 다시 시도해주세요.");
1317
+ } else {
1318
+ addChatMessage(`죄송합니다. 질문에 답변하는 중 문제가 발생했습니다: ${data.error}`);
1319
+ }
1320
+ } else {
1321
+ // AI 응답 추가
1322
+ addChatMessage(data.answer);
1323
+ }
1324
+ } catch (error) {
1325
+ console.error("질문 제출 오류:", error);
1326
+ if (error.name === 'AbortError') {
1327
+ addChatMessage("죄송합니다. 응답 시간이 너무 오래 걸려 요청이 취소되었습니다. 인터넷 연결을 확인하거나 더 짧은 질문으로 다시 시도해보세요.");
1328
+ } else {
1329
+ addChatMessage("죄송합니다. 서버와 통신 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.");
1330
+ }
1331
+ } finally {
1332
+ isAiProcessing = false;
1333
+ $id('aiChatSubmit').disabled = false;
1334
+ $id('aiChatInput').value = '';
1335
+ $id('aiChatInput').focus();
1336
+ }
1337
+ }
1338
+
1339
+
1340
+ // DOM이 로드되면 실행
1341
+ document.addEventListener('DOMContentLoaded', function() {
1342
+ console.log("DOM 로드 완료, 이벤트 설정 시작");
1343
+
1344
+ // 오디오 초기화 시도
1345
+ initializeAudio().catch(e => console.warn('오디오 초기화 실패:', e));
1346
+
1347
+ // PDF 업로드 버튼
1348
+ const pdfBtn = document.getElementById('pdfUploadBtn');
1349
+ const pdfInput = document.getElementById('pdfInput');
1350
+
1351
+ if (pdfBtn && pdfInput) {
1352
+ console.log("PDF 업로드 버튼 찾음");
1353
+
1354
+ // 버튼 클릭 시 파일 입력 트리거
1355
+ pdfBtn.addEventListener('click', function() {
1356
+ console.log("PDF 버튼 클릭됨");
1357
+ pdfInput.click();
1358
+ });
1359
+
1360
+ // 파일 선택 시 처리
1361
+ pdfInput.addEventListener('change', function(e) {
1362
+ console.log("PDF 파일 선택됨:", e.target.files.length);
1363
+ const file = e.target.files[0];
1364
+ if (!file) return;
1365
+
1366
+ // 서버에 PDF 업로드 (영구 저장소에 저장)
1367
+ uploadPdfToServer(file);
1368
+ });
1369
+ } else {
1370
+ console.error("PDF 업로드 요소를 찾을 수 없음");
1371
+ }
1372
+
1373
+ // 텍스트 업로드 버튼
1374
+ const textBtn = document.getElementById('textToAIBookBtn');
1375
+ const textInput = document.getElementById('textInput');
1376
+
1377
+ if (textBtn && textInput) {
1378
+ // 버튼 클릭 시 파일 입력 트리거
1379
+ textBtn.addEventListener('click', function() {
1380
+ textInput.click();
1381
+ });
1382
+
1383
+ // 파일 선택 시 처리
1384
+ textInput.addEventListener('change', function(e) {
1385
+ const file = e.target.files[0];
1386
+ if (!file) return;
1387
+
1388
+ // 서버에 텍스트 파일 업로드 (영구 저장소에 PDF로 변환하여 저장)
1389
+ uploadTextToServer(file);
1390
+ });
1391
+ }
1392
+
1393
+ // 서버 PDF 로드 및 캐시 상태 확인
1394
+ loadServerPDFs();
1395
+
1396
+ // 캐시 상태를 주기적으로 확인
1397
+ setInterval(checkCacheStatus, 3000);
1398
+
1399
+ // 관리자 버튼 이벤트 설정
1400
+ setupAdminFunctions();
1401
+
1402
+ // 홈 버튼 이벤트 설정
1403
+ const homeButton = document.getElementById('homeButton');
1404
+ if (homeButton) {
1405
+ homeButton.addEventListener('click', function() {
1406
+ if(fb) {
1407
+ fb.destroy();
1408
+ viewer.innerHTML = '';
1409
+ fb = null;
1410
+ }
1411
+ toggle(true);
1412
+
1413
+ // 로딩 인디케이터 정리
1414
+ if (pageLoadingInterval) {
1415
+ clearInterval(pageLoadingInterval);
1416
+ pageLoadingInterval = null;
1417
+ }
1418
+ $id('loadingPages').style.display = 'none';
1419
+ currentLoadingPdfPath = null;
1420
+ currentPdfId = null;
1421
+
1422
+ // AI 챗봇 닫기
1423
+ toggleAiChat(false);
1424
+ hasLoadedSummary = false; // 요약 로드 상태 초기화
1425
+ });
1426
+ }
1427
+
1428
+ // AI 버튼 이벤트 설정
1429
+ const aiButton = document.getElementById('aiButton');
1430
+ if (aiButton) {
1431
+ aiButton.addEventListener('click', function() {
1432
+ toggleAiChat(!isAiChatActive);
1433
+ });
1434
+ }
1435
+
1436
+ // AI 챗봇 닫기 버튼
1437
+ const aiChatClose = document.getElementById('aiChatClose');
1438
+ if (aiChatClose) {
1439
+ aiChatClose.addEventListener('click', function() {
1440
+ toggleAiChat(false);
1441
+ });
1442
+ }
1443
+
1444
+ // AI 챗봇 폼 제출
1445
+ const aiChatForm = document.getElementById('aiChatForm');
1446
+ if (aiChatForm) {
1447
+ aiChatForm.addEventListener('submit', function(e) {
1448
+ e.preventDefault();
1449
+ const inputField = document.getElementById('aiChatInput');
1450
+ const question = inputField.value.trim();
1451
+
1452
+ if (question && !isAiProcessing) {
1453
+ submitQuestion(question);
1454
+ }
1455
+ });
1456
+ }
1457
+ });
1458
+
1459
+ // 서버에 PDF 업로드 함수
1460
+ async function uploadPdfToServer(file) {
1461
+ try {
1462
+ showLoading("PDF 업로드 중...");
1463
+
1464
+ const formData = new FormData();
1465
+ formData.append('file', file);
1466
+
1467
+ const response = await fetch('/api/upload-pdf', {
1468
+ method: 'POST',
1469
+ body: formData
1470
+ });
1471
+
1472
+ const result = await response.json();
1473
+
1474
+ if (result.success) {
1475
+ hideLoading();
1476
+
1477
+ // 업로드 성공 시 서버 PDF 리스트 리로드
1478
+ await loadServerPDFs();
1479
+
1480
+ // 성공 메시지
1481
+ showMessage("PDF가 성공적으로 업로드되었습니다! 공유 URL: " + result.viewUrl);
1482
+ } else {
1483
+ hideLoading();
1484
+ showError("업로드 실패: " + (result.message || "알 수 없는 오류"));
1485
+ }
1486
+ } catch (error) {
1487
+ console.error("PDF 업로드 오류:", error);
1488
+ hideLoading();
1489
+ showError("PDF 업로드 중 오류가 발생했습니다.");
1490
+ }
1491
+ }
1492
+
1493
+ // 서버에 텍스트 파일을 업로드하여 PDF로 변환하는 함수
1494
+ async function uploadTextToServer(file) {
1495
+ try {
1496
+ showLoading("텍스트 분석 및 PDF 변환 중...");
1497
+
1498
+ const formData = new FormData();
1499
+ formData.append('file', file);
1500
+
1501
+ const response = await fetch('/api/text-to-pdf', {
1502
+ method: 'POST',
1503
+ body: formData
1504
+ });
1505
+
1506
+ const result = await response.json();
1507
+
1508
+ if (result.success) {
1509
+ hideLoading();
1510
+
1511
+ // 업로드 성공 시 서버 PDF 리스트 리로드
1512
+ await loadServerPDFs();
1513
+
1514
+ // 성공 메시지
1515
+ showMessage("텍스트가 성공적으로 PDF로 변환되었습니다! 공유 URL: " + result.viewUrl);
1516
+ } else {
1517
+ hideLoading();
1518
+ showError("변환 실패: " + (result.message || "알 수 없는 오류"));
1519
+ }
1520
+ } catch (error) {
1521
+ console.error("텍스트 변환 오류:", error);
1522
+ hideLoading();
1523
+ showError("텍스트를 PDF로 변환하는 중 오류가 발생했습니다.");
1524
+ }
1525
+ }
1526
+
1527
+ function addCard(i, thumb, title, isCached = false, pdfId = null) {
1528
+ const d = document.createElement('div');
1529
+ d.className = 'card fade-in';
1530
+ d.onclick = () => open(i);
1531
+
1532
+ // PDF ID가 있으면 데이터 속성으로 저장
1533
+ if (pdfId) {
1534
+ d.dataset.pdfId = pdfId;
1535
+ }
1536
+
1537
+ // 제목 처리
1538
+ const displayTitle = title ?
1539
+ (title.length > 15 ? title.substring(0, 15) + '...' : title) :
1540
+ '프로젝트 ' + (i+1);
1541
+
1542
+ // 캐시 상태 뱃지 추가
1543
+ const cachedBadge = isCached ?
1544
+ '<div class="cached-status">캐시됨</div>' : '';
1545
+
1546
+ // 바로가기 링크 추가 (PDF ID가 있는 경우에만)
1547
+ const linkHtml = pdfId ?
1548
+ `<div style="position: absolute; bottom: 55px; left: 50%; transform: translateX(-50%); z-index:5;">
1549
+ <a href="/view/${pdfId}" target="_blank" style="color:#4a6ee0; font-size:11px;">Share Link</a>
1550
+ </div>` : '';
1551
+
1552
+ d.innerHTML = `
1553
+ <div class="card-inner">
1554
+ ${cachedBadge}
1555
+ <img src="${thumb}" alt="${displayTitle}" loading="lazy">
1556
+ ${linkHtml}
1557
+ <p title="${title || '프로젝트 ' + (i+1)}">${displayTitle}</p>
1558
+ </div>
1559
+ `;
1560
+ grid.appendChild(d);
1561
+
1562
+ // 프로젝트가 있으면 '프로젝트 없음' 메시지 숨기기
1563
+ $id('noProjects').style.display = 'none';
1564
+ }
1565
+
1566
+ /* ── 프로젝트 저장 ── */
1567
+ function save(pages, title, isCached = false, pdfId = null) {
1568
+ const id = projects.push(pages) - 1;
1569
+ addCard(id, pages[0].thumb, title, isCached, pdfId);
1570
+ }
1571
+
1572
+ /* ── 서버 PDF 로드 및 캐시 상태 확인 ── */
1573
+ async function loadServerPDFs() {
1574
+ try {
1575
+ // 로딩 표시 추가
1576
+ if (document.querySelectorAll('.card').length === 0) {
1577
+ showLoading("라이브러리 로딩 중...");
1578
+ }
1579
+
1580
+ // 먼저 캐시 상태 확인
1581
+ const cacheStatusRes = await fetch('/api/cache-status');
1582
+ const cacheStatus = await cacheStatusRes.json();
1583
+
1584
+ // PDF 프로젝트 목록 가져오기
1585
+ const response = await fetch('/api/pdf-projects');
1586
+ serverProjects = await response.json();
1587
+
1588
+ // 기존 그리드 초기화
1589
+ grid.innerHTML = '';
1590
+ projects = [];
1591
+
1592
+ if (serverProjects.length === 0) {
1593
+ hideLoading();
1594
+ $id('noProjects').style.display = 'block';
1595
+ return;
1596
+ }
1597
+
1598
+ // 서버 PDF 로드 및 썸네일 생성 (병렬 처리로 최적화)
1599
+ const thumbnailPromises = serverProjects.map(async (project, index) => {
1600
+ updateLoading(`PDF 프로젝트 로딩 중... (${index+1}/${serverProjects.length})`);
1601
+
1602
+ const pdfName = project.name;
1603
+ const isCached = cacheStatus[pdfName] && cacheStatus[pdfName].status === "completed";
1604
+
1605
+ try {
1606
+ // 썸네일 가져오기
1607
+ const response = await fetch(`/api/pdf-thumbnail?path=${encodeURIComponent(project.path)}`);
1608
+ const data = await response.json();
1609
+
1610
+ if(data.thumbnail) {
1611
+ const pages = [{
1612
+ src: data.thumbnail,
1613
+ thumb: data.thumbnail,
1614
+ path: project.path,
1615
+ cached: isCached
1616
+ }];
1617
+
1618
+ return {
1619
+ pages,
1620
+ name: project.name,
1621
+ isCached,
1622
+ id: project.id
1623
+ };
1624
+ }
1625
+ } catch (err) {
1626
+ console.error(`썸네일 로드 오류 (${project.name}):`, err);
1627
+ }
1628
+
1629
+ return null;
1630
+ });
1631
+
1632
+ // 모든 썸네일 요청 기다리기
1633
+ const results = await Promise.all(thumbnailPromises);
1634
+
1635
+ // 성공적으로 가져온 결과만 표시
1636
+ results.filter(result => result !== null).forEach(result => {
1637
+ save(result.pages, result.name, result.isCached, result.id);
1638
+ });
1639
+
1640
+ hideLoading();
1641
+
1642
+ // 프로젝트가 없을 경우 메시지 표시
1643
+ if (document.querySelectorAll('.card').length === 0) {
1644
+ $id('noProjects').style.display = 'block';
1645
+ }
1646
+ } catch(error) {
1647
+ console.error('서버 PDF 로드 실패:', error);
1648
+ hideLoading();
1649
+ showError("라이브러리 로딩 중 오류가 발생했습니다.");
1650
+ }
1651
+ }
1652
+
1653
+ /* ── 캐시 상태 정기적으로 확인 ── */
1654
+ async function checkCacheStatus() {
1655
+ try {
1656
+ const response = await fetch('/api/cache-status');
1657
+ const cacheStatus = await response.json();
1658
+
1659
+ // 현재 카드 상태 업데이트
1660
+ const cards = document.querySelectorAll('.card');
1661
+
1662
+ for(let i = 0; i < cards.length; i++) {
1663
+ if(projects[i] && projects[i][0] && projects[i][0].path) {
1664
+ const pdfPath = projects[i][0].path;
1665
+ const pdfName = pdfPath.split('/').pop().replace('.pdf', '');
1666
+
1667
+ // 캐시 상태 뱃지 업데이트
1668
+ let badgeEl = cards[i].querySelector('.cached-status');
1669
+
1670
+ if(cacheStatus[pdfName] && cacheStatus[pdfName].status === "completed") {
1671
+ if(!badgeEl) {
1672
+ badgeEl = document.createElement('div');
1673
+ badgeEl.className = 'cached-status';
1674
+ badgeEl.textContent = '캐시됨';
1675
+ cards[i].querySelector('.card-inner')?.appendChild(badgeEl);
1676
+ } else if (badgeEl.textContent !== '캐시됨') {
1677
+ badgeEl.textContent = '캐시됨';
1678
+ badgeEl.style.background = 'var(--accent-color)';
1679
+ }
1680
+ projects[i][0].cached = true;
1681
+ } else if(cacheStatus[pdfName] && cacheStatus[pdfName].status === "processing") {
1682
+ if(!badgeEl) {
1683
+ badgeEl = document.createElement('div');
1684
+ badgeEl.className = 'cached-status';
1685
+ cards[i].querySelector('.card-inner')?.appendChild(badgeEl);
1686
+ }
1687
+ badgeEl.textContent = `${cacheStatus[pdfName].progress}%`;
1688
+ badgeEl.style.background = 'var(--secondary-color)';
1689
+ }
1690
+ }
1691
+ }
1692
+
1693
+ // 현재 로딩 중인 PDF가 있으면 상태 확인
1694
+ if (currentLoadingPdfPath && pageLoadingInterval) {
1695
+ const pdfName = currentLoadingPdfPath.split('/').pop().replace('.pdf', '');
1696
+
1697
+ if (cacheStatus[pdfName]) {
1698
+ const status = cacheStatus[pdfName].status;
1699
+ const progress = cacheStatus[pdfName].progress || 0;
1700
+
1701
+ if (status === "completed") {
1702
+ // 캐싱 완료 시
1703
+ clearInterval(pageLoadingInterval);
1704
+ $id('loadingPages').style.display = 'none';
1705
+ currentLoadingPdfPath = null;
1706
+
1707
+ // 완료된 캐시로 플립북 다시 로드
1708
+ refreshFlipBook();
1709
+ } else if (status === "processing") {
1710
+ // 진행 중일 때 표시 업데이트
1711
+ $id('loadingPages').style.display = 'block';
1712
+ $id('loadingPagesCount').textContent = `${progress}%`;
1713
+ }
1714
+ }
1715
+ }
1716
+
1717
+ } catch(error) {
1718
+ console.error('캐시 상태 확인 오류:', error);
1719
+ }
1720
+ }
1721
+
1722
+ /* ── PDF ID로 PDF 열기 ── */
1723
+ async function openPdfById(pdfId, pdfPath, isCached = false) {
1724
+ try {
1725
+ // 오디오 초기화 시도
1726
+ await initializeAudio().catch(e => console.warn('PDF 열기 전 오디오 초기화 실패:', e));
1727
+
1728
+ // 먼저 홈 화면에서 카드를 찾아서 클릭하는 방법 시도
1729
+ let foundCard = false;
1730
+ const cards = document.querySelectorAll('.card');
1731
+
1732
+ for (let i = 0; i < cards.length; i++) {
1733
+ if (cards[i].dataset.pdfId === pdfId) {
1734
+ cards[i].click();
1735
+ foundCard = true;
1736
+ break;
1737
+ }
1738
+ }
1739
+
1740
+ // 카드를 찾지 못한 경우 직접 오픈
1741
+ if (!foundCard) {
1742
+ toggle(false);
1743
+ showLoading("PDF 준비 중...");
1744
+
1745
+ let pages = [];
1746
+
1747
+ // 이미 캐시된 경우 캐시된 데이터 사용
1748
+ if (isCached) {
1749
+ try {
1750
+ const response = await fetch(`/api/cached-pdf?path=${encodeURIComponent(pdfPath)}`);
1751
+ const cachedData = await response.json();
1752
+
1753
+ if (cachedData.status === "completed" && cachedData.pages) {
1754
+ hideLoading();
1755
+ createFlipBook(cachedData.pages);
1756
+ // 현재 열린 PDF의 ID 저장
1757
+ currentPdfId = pdfId;
1758
+ // AI 버튼 표시
1759
+ $id('aiButton').style.display = 'block';
1760
+ return;
1761
+ }
1762
+ } catch (error) {
1763
+ console.error("캐시 데이터 로드 실패:", error);
1764
+ }
1765
+ }
1766
+
1767
+ // 썸네일 가져오기
1768
+ try {
1769
+ const thumbResponse = await fetch(`/api/pdf-thumbnail?path=${encodeURIComponent(pdfPath)}`);
1770
+ const thumbData = await thumbResponse.json();
1771
+
1772
+ if (thumbData.thumbnail) {
1773
+ pages = [{
1774
+ src: thumbData.thumbnail,
1775
+ thumb: thumbData.thumbnail,
1776
+ path: pdfPath,
1777
+ cached: isCached
1778
+ }];
1779
+ }
1780
+ } catch (error) {
1781
+ console.error("썸네일 로드 실패:", error);
1782
+ }
1783
+
1784
+ // 일단 기본 페이지 추가
1785
+ if (pages.length === 0) {
1786
+ pages = [{
1787
+ path: pdfPath,
1788
+ cached: isCached
1789
+ }];
1790
+ }
1791
+
1792
+ // 프로젝트에 추가하고 뷰어 실행
1793
+ const projectId = projects.push(pages) - 1;
1794
+ hideLoading();
1795
+ open(projectId);
1796
+
1797
+ // 현재 열린 PDF의 ID 저장
1798
+ currentPdfId = pdfId;
1799
+ // AI 버튼 표시
1800
+ $id('aiButton').style.display = 'block';
1801
+ }
1802
+ } catch (error) {
1803
+ console.error("PDF ID로 열기 실패:", error);
1804
+ hideLoading();
1805
+ showError("PDF를 열 수 없습니다. 다시 시도해주세요.");
1806
+ }
1807
+ }
1808
+
1809
+ /* ── 현재 PDF의 고유 URL 생성 및 복사 ── */
1810
+ function copyPdfShareUrl() {
1811
+ if (!currentPdfId) {
1812
+ showError("공유할 PDF가 없습니다.");
1813
+ return;
1814
+ }
1815
+
1816
+ // 현재 도메인 기반 전체 URL 생성
1817
+ const shareUrl = `${window.location.origin}/view/${currentPdfId}`;
1818
+
1819
+ // 클립보드에 복사
1820
+ navigator.clipboard.writeText(shareUrl)
1821
+ .then(() => {
1822
+ showMessage("PDF 링크가 복사되었습니다!");
1823
+ })
1824
+ .catch(err => {
1825
+ console.error("클립보드 복사 실패:", err);
1826
+ showError("링크 복사에 실패했습니다.");
1827
+ });
1828
+ }
1829
+
1830
+ /* ── 카드 → FlipBook ── */
1831
+ async function open(i) {
1832
+ toggle(false);
1833
+ const pages = projects[i];
1834
+
1835
+ // PDF ID 찾기 및 저장
1836
+ const card = document.querySelectorAll('.card')[i];
1837
+ if (card && card.dataset.pdfId) {
1838
+ currentPdfId = card.dataset.pdfId;
1839
+ // AI 버튼 표시
1840
+ $id('aiButton').style.display = 'block';
1841
+ } else {
1842
+ currentPdfId = null;
1843
+ // AI 버튼 숨김
1844
+ $id('aiButton').style.display = 'none';
1845
+ }
1846
+
1847
+ // AI 챗봇 초기화
1848
+ toggleAiChat(false);
1849
+ hasLoadedSummary = false;
1850
+ $id('aiChatMessages').innerHTML = '';
1851
+
1852
+ // 기존 FlipBook 정리
1853
+ if(fb) {
1854
+ fb.destroy();
1855
+ viewer.innerHTML = '';
1856
+ }
1857
+
1858
+ // 서버 PDF 또는 로컬 프로젝트 처리
1859
+ if(pages[0].path) {
1860
+ const pdfPath = pages[0].path;
1861
+
1862
+ // 점진적 로딩 플래그 초기화
1863
+ let progressiveLoading = false;
1864
+ currentLoadingPdfPath = pdfPath;
1865
+
1866
+ // 캐시 여부 확인
1867
+ if(pages[0].cached) {
1868
+ // 캐시된 PDF 데이터 가져오기
1869
+ showLoading("캐시된 PDF 로딩 중...");
1870
+
1871
+ try {
1872
+ const response = await fetch(`/api/cached-pdf?path=${encodeURIComponent(pdfPath)}`);
1873
+ const cachedData = await response.json();
1874
+
1875
+ if(cachedData.status === "completed" && cachedData.pages) {
1876
+ hideLoading();
1877
+ createFlipBook(cachedData.pages);
1878
+ currentLoadingPdfPath = null;
1879
+ return;
1880
+ } else if(cachedData.status === "processing" && cachedData.pages && cachedData.pages.length > 0) {
1881
+ // 일부 페이지가 이미 처리된 경우 점진적 로딩 사용
1882
+ hideLoading();
1883
+ createFlipBook(cachedData.pages);
1884
+ progressiveLoading = true;
1885
+
1886
+ // 점진적 로딩 중임을 표시
1887
+ startProgressiveLoadingIndicator(cachedData.progress, cachedData.total_pages);
1888
+ }
1889
+ } catch(error) {
1890
+ console.error("캐시 데이터 로드 오류:", error);
1891
+ // 캐시 로딩 실패 시 원본 PDF로 대체
1892
+ }
1893
+ }
1894
+
1895
+ if (!progressiveLoading) {
1896
+ // 캐시가 없거나 로딩 실패 시 서버 PDF 로드
1897
+ showLoading("PDF 준비 중...");
1898
+
1899
+ try {
1900
+ const response = await fetch(`/api/pdf-content?path=${encodeURIComponent(pdfPath)}`);
1901
+ const data = await response.json();
1902
+
1903
+ // 캐시로 리다이렉트된 경우
1904
+ if(data.redirect) {
1905
+ const redirectRes = await fetch(data.redirect);
1906
+ const cachedData = await redirectRes.json();
1907
+
1908
+ if(cachedData.status === "completed" && cachedData.pages) {
1909
+ hideLoading();
1910
+ createFlipBook(cachedData.pages);
1911
+ currentLoadingPdfPath = null;
1912
+ return;
1913
+ } else if(cachedData.status === "processing" && cachedData.pages && cachedData.pages.length > 0) {
1914
+ // 일부 페이지가 이미 처리된 경우 점진적 로딩 사용
1915
+ hideLoading();
1916
+ createFlipBook(cachedData.pages);
1917
+
1918
+ // 점진적 로딩 중임을 표시
1919
+ startProgressiveLoadingIndicator(cachedData.progress, cachedData.total_pages);
1920
+ return;
1921
+ }
1922
+ }
1923
+
1924
+ // 원본 PDF 로드 (ArrayBuffer 형태)
1925
+ const pdfResponse = await fetch(`/api/pdf-content?path=${encodeURIComponent(pdfPath)}`);
1926
+
1927
+ // JSON 응답인 경우 (리다이렉트 등)
1928
+ try {
1929
+ const jsonData = await pdfResponse.clone().json();
1930
+ if (jsonData.redirect) {
1931
+ const redirectRes = await fetch(jsonData.redirect);
1932
+ const cachedData = await redirectRes.json();
1933
+
1934
+ if(cachedData.pages && cachedData.pages.length > 0) {
1935
+ hideLoading();
1936
+ createFlipBook(cachedData.pages);
1937
+
1938
+ if(cachedData.status === "processing") {
1939
+ startProgressiveLoadingIndicator(cachedData.progress, cachedData.total_pages);
1940
+ } else {
1941
+ currentLoadingPdfPath = null;
1942
+ }
1943
+ return;
1944
+ }
1945
+ }
1946
+ } catch (e) {
1947
+ // JSON 파싱 실패 시 원본 PDF 데이터로 처리
1948
+ }
1949
+
1950
+ // ArrayBuffer 형태의 PDF 데이터
1951
+ const pdfData = await pdfResponse.arrayBuffer();
1952
+
1953
+ // PDF 로드 및 페이지 렌더링
1954
+ const pdf = await pdfjsLib.getDocument({data: pdfData}).promise;
1955
+ const pdfPages = [];
1956
+
1957
+ for(let p = 1; p <= pdf.numPages; p++) {
1958
+ updateLoading(`페이지 준비 중... (${p}/${pdf.numPages})`);
1959
+
1960
+ const pg = await pdf.getPage(p);
1961
+ const vp = pg.getViewport({scale: 1});
1962
+ const c = document.createElement('canvas');
1963
+ c.width = vp.width;
1964
+ c.height = vp.height;
1965
+
1966
+ await pg.render({canvasContext: c.getContext('2d'), viewport: vp}).promise;
1967
+ pdfPages.push({src: c.toDataURL(), thumb: c.toDataURL()});
1968
+ }
1969
+
1970
+ hideLoading();
1971
+ createFlipBook(pdfPages);
1972
+ currentLoadingPdfPath = null;
1973
+
1974
+ } catch(error) {
1975
+ console.error('PDF 처리 중 오류 발생:', error);
1976
+ hideLoading();
1977
+ showError("PDF를 로드하는 중 오류가 발생했습니다.");
1978
+ currentLoadingPdfPath = null;
1979
+ }
1980
+ }
1981
+ } else {
1982
+ // 로컬 업로드된 프로젝트 실행
1983
+ createFlipBook(pages);
1984
+ currentLoadingPdfPath = null;
1985
+ }
1986
+ }
1987
+
1988
+ /* ── 점진적 로딩 인디케이터 시작 ── */
1989
+ function startProgressiveLoadingIndicator(progress, totalPages) {
1990
+ // 진행 상태 표시 활성화
1991
+ $id('loadingPages').style.display = 'block';
1992
+ $id('loadingPagesCount').textContent = `${progress}%`;
1993
+
1994
+ // 기존 인터벌 제거
1995
+ if (pageLoadingInterval) {
1996
+ clearInterval(pageLoadingInterval);
1997
+ }
1998
+
1999
+ // 주기적으로 캐시 상태 확인 (2초마다)
2000
+ pageLoadingInterval = setInterval(async () => {
2001
+ if (!currentLoadingPdfPath) {
2002
+ clearInterval(pageLoadingInterval);
2003
+ $id('loadingPages').style.display = 'none';
2004
+ return;
2005
+ }
2006
+
2007
+ try {
2008
+ const response = await fetch(`/api/cache-status?path=${encodeURIComponent(currentLoadingPdfPath)}`);
2009
+ const status = await response.json();
2010
+
2011
+ // 상태 업데이트
2012
+ if (status.status === "completed") {
2013
+ clearInterval(pageLoadingInterval);
2014
+ $id('loadingPages').style.display = 'none';
2015
+ refreshFlipBook(); // 완료된 데이터로 새로고침
2016
+ currentLoadingPdfPath = null;
2017
+ } else if (status.status === "processing") {
2018
+ $id('loadingPagesCount').textContent = `${status.progress}%`;
2019
+ }
2020
+ } catch (e) {
2021
+ console.error("캐시 상태 확인 오류:", e);
2022
+ }
2023
+ }, 1000);
2024
+ }
2025
+
2026
+ /* ── 플립북 새로고침 ── */
2027
+ async function refreshFlipBook() {
2028
+ if (!currentLoadingPdfPath || !fb) return;
2029
+
2030
+ try {
2031
+ const response = await fetch(`/api/cached-pdf?path=${encodeURIComponent(currentLoadingPdfPath)}`);
2032
+ const cachedData = await response.json();
2033
+
2034
+ if(cachedData.status === "completed" && cachedData.pages) {
2035
+ // 기존 플립북 정리
2036
+ fb.destroy();
2037
+ viewer.innerHTML = '';
2038
+
2039
+ // 새 데이터로 재생성
2040
+ createFlipBook(cachedData.pages);
2041
+ currentLoadingPdfPath = null;
2042
+ }
2043
+ } catch (e) {
2044
+ console.error("플립북 새로고침 오류:", e);
2045
+ }
2046
+ }
2047
+
2048
+ function createFlipBook(pages) {
2049
+ console.log('FlipBook 생성 시작. 페이지 수:', pages.length);
2050
+
2051
+ try {
2052
+ // 화면 비율 계산
2053
+ const calculateAspectRatio = () => {
2054
+ const windowWidth = window.innerWidth;
2055
+ const windowHeight = window.innerHeight;
2056
+ const aspectRatio = windowWidth / windowHeight;
2057
+
2058
+ // 너비 또는 높이 기준으로 최대 90% 제한
2059
+ let width, height;
2060
+ if (aspectRatio > 1) { // 가로 화면
2061
+ height = Math.min(windowHeight * 0.9, windowHeight - 40);
2062
+ width = height * aspectRatio * 0.8; // 가로 화면에서는 약간 줄임
2063
+ if (width > windowWidth * 0.9) {
2064
+ width = windowWidth * 0.9;
2065
+ height = width / (aspectRatio * 0.8);
2066
+ }
2067
+ } else { // 세로 화면
2068
+ width = Math.min(windowWidth * 0.9, windowWidth - 40);
2069
+ height = width / aspectRatio * 0.9; // 세로 화면에서는 약간 늘림
2070
+ if (height > windowHeight * 0.9) {
2071
+ height = windowHeight * 0.9;
2072
+ width = height * aspectRatio * 0.9;
2073
+ }
2074
+ }
2075
+
2076
+ // 최적 사이즈 반환
2077
+ return {
2078
+ width: Math.round(width),
2079
+ height: Math.round(height)
2080
+ };
2081
+ };
2082
+
2083
+ // 초기 화면 비율 계산
2084
+ const size = calculateAspectRatio();
2085
+ viewer.style.width = size.width + 'px';
2086
+ viewer.style.height = size.height + 'px';
2087
+
2088
+ // 사운드 초기화 여부 확인
2089
+ if (!audioInitialized) {
2090
+ initializeAudio()
2091
+ .then(() => console.log('FlipBook 생성 전 오디오 초기화 완료'))
2092
+ .catch(e => console.warn('FlipBook 생성 시 오디오 초기화 실패:', e));
2093
+ }
2094
+
2095
+ // 페이지 데이터 정제 (빈 페이지 처리)
2096
+ const validPages = pages.map(page => {
2097
+ // src가 없는 페이지는 로딩 중 이미지로 대체
2098
+ if (!page || !page.src) {
2099
+ return {
2100
+ src: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjVmNWY1Ii8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iIGZpbGw9IiM1NTUiPkxvYWRpbmcuLi48L3RleHQ+PC9zdmc+',
2101
+ thumb: page && page.thumb ? page.thumb : ''
2102
+ };
2103
+ }
2104
+ return page;
2105
+ });
2106
+
2107
+ fb = new FlipBook(viewer, {
2108
+ pages: validPages,
2109
+ viewMode: 'webgl',
2110
+ autoSize: true,
2111
+ flipDuration: 800,
2112
+ backgroundColor: '#fff',
2113
+ /* 🔊 내장 사운드 */
2114
+ sound: true,
2115
+ assets: {flipMp3: '/static/turnPage2.mp3', hardFlipMp3: '/static/turnPage2.mp3'}, // 절대 경로로 수정
2116
+ controlsProps: {
2117
+ enableFullscreen: true,
2118
+ enableToc: true,
2119
+ enableDownload: false,
2120
+ enablePrint: false,
2121
+ enableZoom: true,
2122
+ enableShare: true, // 공유 버튼 활성화
2123
+ enableSearch: true,
2124
+ enableAutoPlay: true,
2125
+ enableAnnotation: false,
2126
+ enableSound: true,
2127
+ enableLightbox: false,
2128
+ layout: 10, // 레이아웃 옵션
2129
+ skin: 'light', // 스킨 스타일
2130
+ autoNavigationTime: 3600, // 자동 넘김 시간(초)
2131
+ hideControls: false, // 컨트롤 숨김 비활성화
2132
+ paddingTop: 10, // 상단 패딩
2133
+ paddingLeft: 10, // 좌측 패딩
2134
+ paddingRight: 10, // 우측 패딩
2135
+ paddingBottom: 10, // 하단 패딩
2136
+ pageTextureSize: 1024, // 페이지 텍스처 크기
2137
+ thumbnails: true, // 섬네일 활성화
2138
+ autoHideControls: false, // 자동 숨김 비활성화
2139
+ controlsTimeout: 8000, // 컨트롤 표시 시간 연장
2140
+ shareHandler: copyPdfShareUrl // 공유 핸들러 설정
2141
+ }
2142
+ });
2143
+
2144
+ // 화면 크기 변경 시 FlipBook 크기 조정
2145
+ window.addEventListener('resize', () => {
2146
+ if (fb) {
2147
+ const newSize = calculateAspectRatio();
2148
+ viewer.style.width = newSize.width + 'px';
2149
+ viewer.style.height = newSize.height + 'px';
2150
+ fb.resize();
2151
+ }
2152
+ });
2153
+
2154
+ // FlipBook 생성 후 컨트롤바 강제 표시
2155
+ setTimeout(() => {
2156
+ try {
2157
+ // 컨트롤바 관련 요소 찾기 및 스타일 적용
2158
+ const menuBars = document.querySelectorAll('.flipbook-container .fb3d-menu-bar');
2159
+ if (menuBars && menuBars.length > 0) {
2160
+ menuBars.forEach(menuBar => {
2161
+ menuBar.style.display = 'block';
2162
+ menuBar.style.opacity = '1';
2163
+ menuBar.style.visibility = 'visible';
2164
+ menuBar.style.zIndex = '9999';
2165
+ });
2166
+ }
2167
+ } catch (e) {
2168
+ console.warn('컨트롤바 스타일 적용 중 오류:', e);
2169
+ }
2170
+ }, 1000);
2171
+
2172
+ console.log('FlipBook 생성 완료');
2173
+ } catch (error) {
2174
+ console.error('FlipBook 생성 중 오류 발생:', error);
2175
+ showError("FlipBook을 생성하는 중 오류가 발생했습니다.");
2176
+ }
2177
+ }
2178
+
2179
+ /* ── 네비게이션 ── */
2180
+ function toggle(showHome){
2181
+ $id('home').style.display=showHome?'block':'none';
2182
+ $id('viewerPage').style.display=showHome?'none':'block';
2183
+ $id('homeButton').style.display=showHome?'none':'block';
2184
+ $id('adminPage').style.display='none';
2185
+
2186
+ // AI 버튼 관리
2187
+ $id('aiButton').style.display = (!showHome && currentPdfId) ? 'block' : 'none';
2188
+
2189
+ // AI 챗봇이 열려있으면 닫기
2190
+ if (isAiChatActive) {
2191
+ toggleAiChat(false);
2192
+ }
2193
+
2194
+ // 뷰어 모드일 때 스타일 변경
2195
+ if(!showHome) {
2196
+ document.body.classList.add('viewer-mode');
2197
+ } else {
2198
+ document.body.classList.remove('viewer-mode');
2199
+ }
2200
+ }
2201
+
2202
+ /* -- 관리자 기능 -- */
2203
+ function setupAdminFunctions() {
2204
+ // 관리자 버튼 클릭 - 모달 표시
2205
+ const adminButton = document.getElementById('adminButton');
2206
+ const adminLoginModal = document.getElementById('adminLoginModal');
2207
+ const adminLoginClose = document.getElementById('adminLoginClose');
2208
+ const adminLoginButton = document.getElementById('adminLoginButton');
2209
+ const adminPasswordInput = document.getElementById('adminPasswordInput');
2210
+ const adminBackButton = document.getElementById('adminBackButton');
2211
+
2212
+ if (adminButton) {
2213
+ adminButton.addEventListener('click', function() {
2214
+ if (adminLoginModal) {
2215
+ adminLoginModal.style.display = 'flex';
2216
+ if (adminPasswordInput) {
2217
+ adminPasswordInput.value = '';
2218
+ adminPasswordInput.focus();
2219
+ }
2220
+ }
2221
+ });
2222
+ }
2223
+
2224
+ // 모달 닫기 버튼
2225
+ if (adminLoginClose) {
2226
+ adminLoginClose.addEventListener('click', function() {
2227
+ if (adminLoginModal) {
2228
+ adminLoginModal.style.display = 'none';
2229
+ }
2230
+ });
2231
+ }
2232
+
2233
+ // 엔터 키로 로그인
2234
+ if (adminPasswordInput) {
2235
+ adminPasswordInput.addEventListener('keyup', function(e) {
2236
+ if (e.key === 'Enter' && adminLoginButton) {
2237
+ adminLoginButton.click();
2238
+ }
2239
+ });
2240
+ }
2241
+
2242
+ // 로그인 버튼
2243
+ if (adminLoginButton) {
2244
+ adminLoginButton.addEventListener('click', async function() {
2245
+ if (!adminPasswordInput) return;
2246
+
2247
+ const password = adminPasswordInput.value;
2248
+
2249
+ try {
2250
+ showLoading("로그인 중...");
2251
+
2252
+ const formData = new FormData();
2253
+ formData.append('password', password);
2254
+
2255
+ const response = await fetch('/api/admin-login', {
2256
+ method: 'POST',
2257
+ body: formData
2258
+ });
2259
+
2260
+ const data = await response.json();
2261
+
2262
+ hideLoading();
2263
+
2264
+ if (data.success) {
2265
+ // 로그인 성공 - 관리자 페이지 표시
2266
+ if (adminLoginModal) {
2267
+ adminLoginModal.style.display = 'none';
2268
+ }
2269
+ showAdminPage();
2270
+ } else {
2271
+ // 로그인 실패
2272
+ showError("관리자 인증 실패: 비밀번호가 일치하지 않습니다.");
2273
+ }
2274
+ } catch (error) {
2275
+ console.error("관리자 로그인 오류:", error);
2276
+ hideLoading();
2277
+ showError("로그인 처리 중 오류가 발생했습니다.");
2278
+ }
2279
+ });
2280
+ }
2281
+
2282
+ // 관리자 페이지 뒤로가기
2283
+ if (adminBackButton) {
2284
+ adminBackButton.addEventListener('click', function() {
2285
+ document.getElementById('adminPage').style.display = 'none';
2286
+ document.getElementById('home').style.display = 'block';
2287
+ });
2288
+ }
2289
+ }
2290
+
2291
+ // 관리자 페이지 표시
2292
+ async function showAdminPage() {
2293
+ showLoading("관리자 페이지 로딩 중...");
2294
+
2295
+ // 다른 페이지 숨기기
2296
+ $id('home').style.display = 'none';
2297
+ $id('viewerPage').style.display = 'none';
2298
+
2299
+ // 관리자 페이지의 PDF 목록 로드
2300
+ try {
2301
+ const response = await fetch('/api/permanent-pdf-projects');
2302
+ const data = await response.json();
2303
+
2304
+ const adminGrid = $id('adminGrid');
2305
+ adminGrid.innerHTML = ''; // 기존 내용 지우기
2306
+
2307
+ if (data.length === 0) {
2308
+ $id('noAdminProjects').style.display = 'block';
2309
+ } else {
2310
+ $id('noAdminProjects').style.display = 'none';
2311
+
2312
+ // 각 PDF 파일에 대한 카드 생성
2313
+ const thumbnailPromises = data.map(async (pdf) => {
2314
+ try {
2315
+ // 썸네일 가져오기
2316
+ const thumbResponse = await fetch(`/api/pdf-thumbnail?path=${encodeURIComponent(pdf.path)}`);
2317
+ const thumbData = await thumbResponse.json();
2318
+
2319
+ // 표시 여부 확인 (메인 페이지에 표시되는지)
2320
+ const mainPdfPath = pdf.path.split('/').pop();
2321
+ const isMainDisplayed = serverProjects.some(p => p.path.includes(mainPdfPath));
2322
+
2323
+ // 관리자 카드 생성
2324
+ const card = document.createElement('div');
2325
+ card.className = 'admin-card card fade-in';
2326
+
2327
+ // 고유 URL 생성
2328
+ const viewUrl = `${window.location.origin}/view/${pdf.id}`;
2329
+
2330
+ // 썸네일 및 정보
2331
+ card.innerHTML = `
2332
+ <div class="card-inner">
2333
+ ${pdf.cached ? '<div class="cached-status">캐시됨</div>' : ''}
2334
+ <img src="${thumbData.thumbnail || ''}" alt="${pdf.name}" loading="lazy">
2335
+ <p title="${pdf.name}">${pdf.name.length > 15 ? pdf.name.substring(0, 15) + '...' : pdf.name}</p>
2336
+ <div style="position: absolute; bottom: 130px; left: 50%; transform: translateX(-50%); z-index:10;">
2337
+ <a href="${viewUrl}" target="_blank" style="color:#4a6ee0; font-size:12px;">바로가기 링크</a>
2338
+ </div>
2339
+ ${isMainDisplayed ?
2340
+ `<button class="unfeature-btn" data-path="${pdf.path}">메인에서 제거</button>` :
2341
+ `<button class="feature-btn" data-path="${pdf.path}">메인에 표시</button>`}
2342
+ <button class="delete-btn" data-path="${pdf.path}">삭제</button>
2343
+ </div>
2344
+ `;
2345
+
2346
+ adminGrid.appendChild(card);
2347
+
2348
+ // 삭제 버튼 이벤트
2349
+ const deleteBtn = card.querySelector('.delete-btn');
2350
+ if (deleteBtn) {
2351
+ deleteBtn.addEventListener('click', async function(e) {
2352
+ e.stopPropagation(); // 카드 클릭 이벤트 전파 방지
2353
+
2354
+ if (confirm(`정말 "${pdf.name}" PDF를 삭제하시겠습니까?`)) {
2355
+ try {
2356
+ showLoading("PDF 삭제 중...");
2357
+
2358
+ const response = await fetch(`/api/admin/delete-pdf?path=${encodeURIComponent(pdf.path)}`, {
2359
+ method: 'DELETE'
2360
+ });
2361
+
2362
+ const result = await response.json();
2363
+
2364
+ hideLoading();
2365
+
2366
+ if (result.success) {
2367
+ card.remove();
2368
+ showMessage("PDF가 성공적으로 삭제되었습니다.");
2369
+
2370
+ // 메인 PDF 목록 새로고침
2371
+ loadServerPDFs();
2372
+ } else {
2373
+ showError("삭제 실��: " + (result.message || "알 수 없는 오류"));
2374
+ }
2375
+ } catch (error) {
2376
+ console.error("PDF 삭제 오류:", error);
2377
+ hideLoading();
2378
+ showError("PDF 삭제 중 오류가 발생했습니다.");
2379
+ }
2380
+ }
2381
+ });
2382
+ }
2383
+
2384
+ // 메인에 표시 버튼 이벤트
2385
+ const featureBtn = card.querySelector('.feature-btn');
2386
+ if (featureBtn) {
2387
+ featureBtn.addEventListener('click', async function(e) {
2388
+ e.stopPropagation(); // 카드 클릭 이벤트 전파 방지
2389
+
2390
+ try {
2391
+ showLoading("처리 중...");
2392
+
2393
+ const response = await fetch(`/api/admin/feature-pdf?path=${encodeURIComponent(pdf.path)}`, {
2394
+ method: 'POST'
2395
+ });
2396
+
2397
+ const result = await response.json();
2398
+
2399
+ hideLoading();
2400
+
2401
+ if (result.success) {
2402
+ showMessage("PDF가 메인 페이지에 표시됩니다.");
2403
+ // 관리자 페이지 새로고침
2404
+ showAdminPage();
2405
+ // 메인 PDF 목록 새로고침
2406
+ loadServerPDFs();
2407
+ } else {
2408
+ showError("처리 실패: " + (result.message || "알 수 없는 오류"));
2409
+ }
2410
+ } catch (error) {
2411
+ console.error("PDF 표시 설정 오류:", error);
2412
+ hideLoading();
2413
+ showError("처리 중 오류가 발생했습니다.");
2414
+ }
2415
+ });
2416
+ }
2417
+
2418
+ // 메인에서 제거 버튼 이벤트
2419
+ const unfeatureBtn = card.querySelector('.unfeature-btn');
2420
+ if (unfeatureBtn) {
2421
+ unfeatureBtn.addEventListener('click', async function(e) {
2422
+ e.stopPropagation(); // 카드 클릭 이벤트 전파 방지
2423
+
2424
+ try {
2425
+ showLoading("처리 중...");
2426
+
2427
+ const response = await fetch(`/api/admin/unfeature-pdf?path=${encodeURIComponent(pdf.path)}`, {
2428
+ method: 'DELETE'
2429
+ });
2430
+
2431
+ const result = await response.json();
2432
+
2433
+ hideLoading();
2434
+
2435
+ if (result.success) {
2436
+ showMessage("PDF가 메인 페이지에서 제거되었습니다.");
2437
+ // 관리자 페이지 새로고침
2438
+ showAdminPage();
2439
+ // 메인 PDF 목록 새로고침
2440
+ loadServerPDFs();
2441
+ } else {
2442
+ showError("처리 실패: " + (result.message || "알 수 없는 오류"));
2443
+ }
2444
+ } catch (error) {
2445
+ console.error("PDF 표시 해제 오류:", error);
2446
+ hideLoading();
2447
+ showError("처리 중 오류가 발생했습니다.");
2448
+ }
2449
+ });
2450
+ }
2451
+ } catch (error) {
2452
+ console.error(`PDF ${pdf.name} 처리 오류:`, error);
2453
+ }
2454
+ });
2455
+
2456
+ await Promise.all(thumbnailPromises);
2457
+ }
2458
+
2459
+ // 관리자 페이지 표시
2460
+ hideLoading();
2461
+ $id('adminPage').style.display = 'block';
2462
+
2463
+ } catch (error) {
2464
+ console.error("관리자 페이지 로드 오류:", error);
2465
+ hideLoading();
2466
+ showError("관리자 페이지 로드 중 오류가 발생했습니다.");
2467
+ $id('home').style.display = 'block'; // 오류 시 홈으로 복귀
2468
+ }
2469
+ }
2470
+
2471
+ /* -- 로딩 및 오류 표시 -- */
2472
+ function showLoading(message, progress = -1) {
2473
+ // 기존 로딩 컨테이너가 있다면 제거
2474
+ hideLoading();
2475
+
2476
+ const loadingContainer = document.createElement('div');
2477
+ loadingContainer.className = 'loading-container fade-in';
2478
+ loadingContainer.id = 'loadingContainer';
2479
+
2480
+ let progressBarHtml = '';
2481
+ if (progress >= 0) {
2482
+ progressBarHtml = `
2483
+ <div class="progress-bar-container">
2484
+ <div id="progressBar" class="progress-bar" style="width: ${progress}%;"></div>
2485
+ </div>
2486
+ `;
2487
+ }
2488
+
2489
+ loadingContainer.innerHTML = `
2490
+ <div class="loading-spinner"></div>
2491
+ <p class="loading-text" id="loadingText">${message || '로딩 중...'}</p>
2492
+ ${progressBarHtml}
2493
+ `;
2494
+
2495
+ document.body.appendChild(loadingContainer);
2496
+ }
2497
+
2498
+ function updateLoading(message, progress = -1) {
2499
+ const loadingText = $id('loadingText');
2500
+ if (loadingText) {
2501
+ loadingText.textContent = message;
2502
+ }
2503
+
2504
+ if (progress >= 0) {
2505
+ let progressBar = $id('progressBar');
2506
+
2507
+ if (!progressBar) {
2508
+ const loadingContainer = $id('loadingContainer');
2509
+ if (loadingContainer) {
2510
+ const progressContainer = document.createElement('div');
2511
+ progressContainer.className = 'progress-bar-container';
2512
+ progressContainer.innerHTML = `<div id="progressBar" class="progress-bar" style="width: ${progress}%;"></div>`;
2513
+ loadingContainer.appendChild(progressContainer);
2514
+ progressBar = $id('progressBar');
2515
+ }
2516
+ } else {
2517
+ progressBar.style.width = `${progress}%`;
2518
+ }
2519
+ }
2520
+ }
2521
+
2522
+ function hideLoading() {
2523
+ const loadingContainer = $id('loadingContainer');
2524
+ if (loadingContainer) {
2525
+ loadingContainer.remove();
2526
+ }
2527
+ }
2528
+
2529
+ function showError(message) {
2530
+ // 기존 오류 메시지가 있다면 제거
2531
+ const existingError = $id('errorContainer');
2532
+ if (existingError) {
2533
+ existingError.remove();
2534
+ }
2535
+
2536
+ const errorContainer = document.createElement('div');
2537
+ errorContainer.className = 'loading-container fade-in';
2538
+ errorContainer.id = 'errorContainer';
2539
+ errorContainer.innerHTML = `
2540
+ <p class="loading-text" style="color: #e74c3c;">${message}</p>
2541
+ <button id="errorCloseBtn" style="margin-top: 15px; padding: 8px 16px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer;">확인</button>
2542
+ `;
2543
+
2544
+ document.body.appendChild(errorContainer);
2545
+
2546
+ // 확인 버튼 클릭 이벤트
2547
+ $id('errorCloseBtn').onclick = () => {
2548
+ errorContainer.remove();
2549
+ };
2550
+
2551
+ // 5초 후 자동으로 닫기
2552
+ setTimeout(() => {
2553
+ if ($id('errorContainer')) {
2554
+ $id('errorContainer').remove();
2555
+ }
2556
+ }, 5000);
2557
+ }
2558
+
2559
+ function showMessage(message) {
2560
+ // 기존 메시지가 있다면 제거
2561
+ const existingMessage = $id('messageContainer');
2562
+ if (existingMessage) {
2563
+ existingMessage.remove();
2564
+ }
2565
+
2566
+ const messageContainer = document.createElement('div');
2567
+ messageContainer.className = 'loading-container fade-in';
2568
+ messageContainer.id = 'messageContainer';
2569
+ messageContainer.innerHTML = `
2570
+ <p class="loading-text" style="color: #2ecc71;">${message}</p>
2571
+ <button id="messageCloseBtn" style="margin-top: 15px; padding: 8px 16px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer;">확인</button>
2572
+ `;
2573
+
2574
+ document.body.appendChild(messageContainer);
2575
+
2576
+ // 확인 버튼 클릭 이벤트
2577
+ $id('messageCloseBtn').onclick = () => {
2578
+ messageContainer.remove();
2579
+ };
2580
+
2581
+ // 3초 후 자동으로 닫기
2582
+ setTimeout(() => {
2583
+ if ($id('messageContainer')) {
2584
+ $id('messageContainer').remove();
2585
+ }
2586
+ }, 3000);
2587
+ }
2588
+ </script>
2589
+ </body>
2590
+ </html>