ginipick commited on
Commit
5da4d89
ยท
verified ยท
1 Parent(s): a59558a

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1307 -0
app.py ADDED
@@ -0,0 +1,1307 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template_string
2
+
3
+ app = Flask(__name__)
4
+
5
+ HTML_TEMPLATE = '''
6
+ <!DOCTYPE html>
7
+ <html lang="ko">
8
+ <head>
9
+ <meta charset="UTF-8">
10
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
11
+ <title>์›นํˆฐ ํŽธ์ง‘ ์ŠคํŠœ๋””์˜ค Pro</title>
12
+ <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@100;300;400;500;700;900&family=Black+Han+Sans&family=Jua&family=Do+Hyeon&family=Sunflower:wght@300;500;700&family=Cute+Font&family=Gamja+Flower&family=Hi+Melody&family=Nanum+Pen+Script&family=Stylish&family=Single+Day&family=Gaegu:wght@300;400;700&display=swap" rel="stylesheet">
13
+ <style>
14
+ * {
15
+ margin: 0;
16
+ padding: 0;
17
+ box-sizing: border-box;
18
+ }
19
+
20
+ body {
21
+ font-family: 'Noto Sans KR', sans-serif;
22
+ background: #1a1a1a;
23
+ color: #fff;
24
+ overflow: hidden;
25
+ }
26
+
27
+ .container {
28
+ display: flex;
29
+ height: 100vh;
30
+ }
31
+
32
+ /* ์ขŒ์ธก ํˆด๋ฐ” */
33
+ .toolbar {
34
+ width: 320px;
35
+ background: #2a2a2a;
36
+ padding: 20px;
37
+ overflow-y: auto;
38
+ border-right: 1px solid #444;
39
+ }
40
+
41
+ .toolbar::-webkit-scrollbar {
42
+ width: 8px;
43
+ }
44
+
45
+ .toolbar::-webkit-scrollbar-track {
46
+ background: #1a1a1a;
47
+ }
48
+
49
+ .toolbar::-webkit-scrollbar-thumb {
50
+ background: #4a9eff;
51
+ border-radius: 4px;
52
+ }
53
+
54
+ .toolbar h2 {
55
+ font-size: 18px;
56
+ margin-bottom: 20px;
57
+ color: #fff;
58
+ border-bottom: 2px solid #4a9eff;
59
+ padding-bottom: 10px;
60
+ }
61
+
62
+ .tool-section {
63
+ margin-bottom: 30px;
64
+ }
65
+
66
+ .tool-section h3 {
67
+ font-size: 14px;
68
+ margin-bottom: 15px;
69
+ color: #aaa;
70
+ text-transform: uppercase;
71
+ }
72
+
73
+ /* ํŒŒ์ผ ์—…๋กœ๋“œ ์˜์—ญ */
74
+ .file-upload {
75
+ border: 2px dashed #4a9eff;
76
+ border-radius: 8px;
77
+ padding: 20px;
78
+ text-align: center;
79
+ cursor: pointer;
80
+ transition: all 0.3s;
81
+ margin-bottom: 20px;
82
+ }
83
+
84
+ .file-upload:hover {
85
+ background: rgba(74, 158, 255, 0.1);
86
+ border-color: #6ab7ff;
87
+ }
88
+
89
+ .file-upload input {
90
+ display: none;
91
+ }
92
+
93
+ /* ํŒŒ์ผ ๋ฆฌ์ŠคํŠธ */
94
+ .file-list {
95
+ background: #333;
96
+ border-radius: 8px;
97
+ padding: 10px;
98
+ margin-top: 10px;
99
+ max-height: 150px;
100
+ overflow-y: auto;
101
+ }
102
+
103
+ .file-item {
104
+ font-size: 12px;
105
+ color: #aaa;
106
+ padding: 3px 0;
107
+ border-bottom: 1px solid #444;
108
+ }
109
+
110
+ .file-item:last-child {
111
+ border-bottom: none;
112
+ }
113
+
114
+ /* ์ปดํฌ๋„ŒํŠธ ๋ฒ„ํŠผ๋“ค */
115
+ .component-grid {
116
+ display: grid;
117
+ grid-template-columns: repeat(3, 1fr);
118
+ gap: 8px;
119
+ }
120
+
121
+ .component-btn {
122
+ background: #3a3a3a;
123
+ border: 1px solid #555;
124
+ border-radius: 8px;
125
+ padding: 10px;
126
+ cursor: pointer;
127
+ transition: all 0.3s;
128
+ text-align: center;
129
+ color: #fff;
130
+ }
131
+
132
+ .component-btn:hover {
133
+ background: #4a4a4a;
134
+ border-color: #4a9eff;
135
+ transform: translateY(-2px);
136
+ }
137
+
138
+ .component-btn svg {
139
+ width: 35px;
140
+ height: 35px;
141
+ margin-bottom: 3px;
142
+ }
143
+
144
+ .component-btn span {
145
+ display: block;
146
+ font-size: 11px;
147
+ }
148
+
149
+ /* ์†์„ฑ ํŽธ์ง‘ ํŒจ๋„ */
150
+ .properties-panel {
151
+ background: #333;
152
+ border-radius: 8px;
153
+ padding: 15px;
154
+ margin-top: 20px;
155
+ }
156
+
157
+ .property-group {
158
+ margin-bottom: 15px;
159
+ }
160
+
161
+ .property-group label {
162
+ display: block;
163
+ font-size: 12px;
164
+ color: #aaa;
165
+ margin-bottom: 5px;
166
+ }
167
+
168
+ .property-group input,
169
+ .property-group select,
170
+ .property-group textarea {
171
+ width: 100%;
172
+ padding: 8px;
173
+ background: #2a2a2a;
174
+ border: 1px solid #444;
175
+ border-radius: 4px;
176
+ color: #fff;
177
+ }
178
+
179
+ .color-picker {
180
+ display: grid;
181
+ grid-template-columns: repeat(6, 1fr);
182
+ gap: 5px;
183
+ }
184
+
185
+ .color-option {
186
+ width: 35px;
187
+ height: 35px;
188
+ border-radius: 4px;
189
+ cursor: pointer;
190
+ border: 2px solid transparent;
191
+ }
192
+
193
+ .color-option.selected {
194
+ border-color: #4a9eff;
195
+ box-shadow: 0 0 10px rgba(74, 158, 255, 0.5);
196
+ }
197
+
198
+ /* ๋ฉ”์ธ ์ž‘์—… ์˜์—ญ */
199
+ .workspace {
200
+ flex: 1;
201
+ display: flex;
202
+ flex-direction: column;
203
+ background: #1a1a1a;
204
+ }
205
+
206
+ .workspace-header {
207
+ background: #2a2a2a;
208
+ padding: 15px 20px;
209
+ border-bottom: 1px solid #444;
210
+ display: flex;
211
+ justify-content: space-between;
212
+ align-items: center;
213
+ }
214
+
215
+ .workspace-tools {
216
+ display: flex;
217
+ gap: 10px;
218
+ }
219
+
220
+ .tool-btn {
221
+ background: #3a3a3a;
222
+ border: 1px solid #555;
223
+ border-radius: 6px;
224
+ padding: 8px 16px;
225
+ color: #fff;
226
+ cursor: pointer;
227
+ transition: all 0.3s;
228
+ }
229
+
230
+ .tool-btn:hover {
231
+ background: #4a4a4a;
232
+ border-color: #4a9eff;
233
+ }
234
+
235
+ .tool-btn.primary {
236
+ background: #4a9eff;
237
+ border-color: #4a9eff;
238
+ }
239
+
240
+ .tool-btn.primary:hover {
241
+ background: #6ab7ff;
242
+ }
243
+
244
+ /* ์บ”๋ฒ„์Šค ์˜์—ญ */
245
+ .canvas-container {
246
+ flex: 1;
247
+ overflow: auto;
248
+ padding: 20px;
249
+ display: flex;
250
+ justify-content: center;
251
+ background: #222;
252
+ position: relative;
253
+ }
254
+
255
+ .canvas-wrapper {
256
+ position: relative;
257
+ background: white;
258
+ box-shadow: 0 10px 40px rgba(0,0,0,0.5);
259
+ min-height: 800px;
260
+ width: 700px;
261
+ }
262
+
263
+ /* ์ด๋ฏธ์ง€ ์ปจํ…Œ์ด๋„ˆ */
264
+ .image-container {
265
+ position: relative;
266
+ width: 100%;
267
+ }
268
+
269
+ .image-number {
270
+ position: absolute;
271
+ left: -40px;
272
+ top: 10px;
273
+ background: #4a9eff;
274
+ color: white;
275
+ width: 30px;
276
+ height: 30px;
277
+ border-radius: 50%;
278
+ display: flex;
279
+ align-items: center;
280
+ justify-content: center;
281
+ font-weight: bold;
282
+ font-size: 14px;
283
+ z-index: 10;
284
+ box-shadow: 0 2px 5px rgba(0,0,0,0.3);
285
+ }
286
+
287
+ .webtoon-image {
288
+ width: 100%;
289
+ display: block;
290
+ margin-bottom: 20px;
291
+ }
292
+
293
+ /* ๋“œ๋ž˜๊ทธ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ */
294
+ .draggable-component {
295
+ position: absolute;
296
+ cursor: move;
297
+ user-select: none;
298
+ z-index: 50;
299
+ }
300
+
301
+ .draggable-component.dragging {
302
+ z-index: 1000;
303
+ opacity: 0.8;
304
+ }
305
+
306
+ .draggable-component.selected {
307
+ outline: 2px solid #4a9eff;
308
+ z-index: 100;
309
+ }
310
+
311
+ .draggable-component .resize-handle {
312
+ position: absolute;
313
+ width: 10px;
314
+ height: 10px;
315
+ background: #4a9eff;
316
+ border: 1px solid #fff;
317
+ display: none;
318
+ }
319
+
320
+ .draggable-component.selected .resize-handle {
321
+ display: block;
322
+ }
323
+
324
+ .resize-handle.nw { top: -5px; left: -5px; cursor: nw-resize; }
325
+ .resize-handle.ne { top: -5px; right: -5px; cursor: ne-resize; }
326
+ .resize-handle.sw { bottom: -5px; left: -5px; cursor: sw-resize; }
327
+ .resize-handle.se { bottom: -5px; right: -5px; cursor: se-resize; }
328
+
329
+ /* ๋งํ’์„  ์Šคํƒ€์ผ๋“ค */
330
+ .speech-bubble {
331
+ background: white;
332
+ border: 2px solid black;
333
+ border-radius: 20px;
334
+ padding: 30px 15px 15px 15px;
335
+ min-width: 100px;
336
+ min-height: 50px;
337
+ position: relative;
338
+ width: 100%;
339
+ height: 100%;
340
+ }
341
+
342
+ .speech-bubble-tail {
343
+ position: absolute;
344
+ bottom: -10px;
345
+ left: 20px;
346
+ width: 0;
347
+ height: 0;
348
+ border-left: 10px solid transparent;
349
+ border-right: 10px solid transparent;
350
+ border-top: 10px solid black;
351
+ }
352
+
353
+ .speech-bubble-tail::after {
354
+ content: '';
355
+ position: absolute;
356
+ bottom: 3px;
357
+ left: -10px;
358
+ width: 0;
359
+ height: 0;
360
+ border-left: 10px solid transparent;
361
+ border-right: 10px solid transparent;
362
+ border-top: 10px solid white;
363
+ }
364
+
365
+ .thought-bubble {
366
+ background: white;
367
+ border: 2px solid black;
368
+ border-radius: 50%;
369
+ padding: 40px 20px 20px 20px;
370
+ min-width: 120px;
371
+ min-height: 80px;
372
+ position: relative;
373
+ width: 100%;
374
+ height: 100%;
375
+ }
376
+
377
+ .thought-bubble-dots {
378
+ position: absolute;
379
+ bottom: -25px;
380
+ left: 20px;
381
+ }
382
+
383
+ .thought-dot {
384
+ position: absolute;
385
+ background: white;
386
+ border: 2px solid black;
387
+ border-radius: 50%;
388
+ }
389
+
390
+ .thought-dot.large {
391
+ width: 20px;
392
+ height: 20px;
393
+ }
394
+
395
+ .thought-dot.small {
396
+ width: 12px;
397
+ height: 12px;
398
+ left: -5px;
399
+ top: 10px;
400
+ }
401
+
402
+ .shout-bubble {
403
+ background: white;
404
+ border: 3px solid black;
405
+ padding: 30px 15px 15px 15px;
406
+ min-width: 100px;
407
+ min-height: 50px;
408
+ position: relative;
409
+ width: 100%;
410
+ height: 100%;
411
+ clip-path: polygon(10% 0%, 90% 0%, 100% 10%, 100% 90%, 90% 100%, 10% 100%, 0% 90%, 0% 10%);
412
+ }
413
+
414
+ .narration-box {
415
+ background: rgba(0, 0, 0, 0.85);
416
+ border: 2px solid white;
417
+ padding: 30px 15px 15px 15px;
418
+ min-width: 150px;
419
+ min-height: 40px;
420
+ width: 100%;
421
+ height: 100%;
422
+ }
423
+
424
+ .narration-box .component-text {
425
+ color: white !important;
426
+ }
427
+
428
+ .text-box {
429
+ background: white;
430
+ border: 2px solid black;
431
+ padding: 20px 10px 10px 10px;
432
+ min-width: 100px;
433
+ min-height: 40px;
434
+ width: 100%;
435
+ height: 100%;
436
+ }
437
+
438
+ .transparent-box {
439
+ background: rgba(255, 255, 255, 0.8);
440
+ border: 1px dashed #999;
441
+ padding: 20px 10px 10px 10px;
442
+ min-width: 100px;
443
+ min-height: 40px;
444
+ width: 100%;
445
+ height: 100%;
446
+ }
447
+
448
+ .action-effect {
449
+ background: transparent;
450
+ border: none;
451
+ padding: 20px 10px 10px 10px;
452
+ min-width: 80px;
453
+ min-height: 40px;
454
+ width: 100%;
455
+ height: 100%;
456
+ font-weight: bold;
457
+ text-shadow: 2px 2px 0 white, -2px -2px 0 white, 2px -2px 0 white, -2px 2px 0 white;
458
+ }
459
+
460
+ .emotion-bubble {
461
+ background: #ffeb3b;
462
+ border: 2px solid black;
463
+ border-radius: 15px;
464
+ padding: 20px 10px 10px 10px;
465
+ min-width: 80px;
466
+ min-height: 40px;
467
+ width: 100%;
468
+ height: 100%;
469
+ }
470
+
471
+ .whisper-bubble {
472
+ background: #f0f0f0;
473
+ border: 1px dashed #666;
474
+ border-radius: 15px;
475
+ padding: 20px 10px 10px 10px;
476
+ min-width: 100px;
477
+ min-height: 40px;
478
+ width: 100%;
479
+ height: 100%;
480
+ opacity: 0.9;
481
+ }
482
+
483
+ .gradient-box {
484
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
485
+ border: 2px solid white;
486
+ border-radius: 10px;
487
+ padding: 30px 15px 15px 15px;
488
+ min-width: 100px;
489
+ min-height: 40px;
490
+ width: 100%;
491
+ height: 100%;
492
+ }
493
+
494
+ .gradient-box .component-text {
495
+ color: white !important;
496
+ }
497
+
498
+ .component-text {
499
+ width: 100%;
500
+ height: 100%;
501
+ border: none;
502
+ background: transparent;
503
+ resize: none;
504
+ outline: none;
505
+ font-family: inherit;
506
+ font-size: 14px;
507
+ color: black;
508
+ text-align: center;
509
+ padding: 5px;
510
+ overflow: visible;
511
+ line-height: 1.4;
512
+ }
513
+
514
+ /* ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ */
515
+ .loading {
516
+ display: none;
517
+ position: fixed;
518
+ top: 50%;
519
+ left: 50%;
520
+ transform: translate(-50%, -50%);
521
+ background: rgba(0,0,0,0.9);
522
+ padding: 30px;
523
+ border-radius: 10px;
524
+ z-index: 10000;
525
+ text-align: center;
526
+ }
527
+
528
+ .loading.active {
529
+ display: block;
530
+ }
531
+
532
+ .loading-spinner {
533
+ border: 3px solid #f3f3f3;
534
+ border-top: 3px solid #4a9eff;
535
+ border-radius: 50%;
536
+ width: 40px;
537
+ height: 40px;
538
+ animation: spin 1s linear infinite;
539
+ margin: 0 auto 10px;
540
+ }
541
+
542
+ @keyframes spin {
543
+ 0% { transform: rotate(0deg); }
544
+ 100% { transform: rotate(360deg); }
545
+ }
546
+
547
+ /* ์ด๋ฏธ์ง€ ๊ฐ„๊ฒฉ ์กฐ์ ˆ */
548
+ .spacing-control {
549
+ display: flex;
550
+ align-items: center;
551
+ gap: 10px;
552
+ }
553
+
554
+ .spacing-control input[type="range"] {
555
+ flex: 1;
556
+ }
557
+
558
+ .spacing-value {
559
+ width: 50px;
560
+ text-align: center;
561
+ }
562
+
563
+ /* ๊ฐœ๋ณ„ ์ €์žฅ ์ž…๋ ฅ */
564
+ .save-individual {
565
+ display: flex;
566
+ gap: 5px;
567
+ margin-top: 10px;
568
+ }
569
+
570
+ .save-individual input {
571
+ width: 60px;
572
+ }
573
+
574
+ .save-individual button {
575
+ flex: 1;
576
+ padding: 5px;
577
+ background: #28a745;
578
+ border: none;
579
+ border-radius: 4px;
580
+ color: white;
581
+ cursor: pointer;
582
+ }
583
+
584
+ .save-individual button:hover {
585
+ background: #5cbf5c;
586
+ }
587
+ </style>
588
+ </head>
589
+ <body>
590
+ <div class="container">
591
+ <!-- ์ขŒ์ธก ํˆด๋ฐ” -->
592
+ <div class="toolbar">
593
+ <h2>๐ŸŽจ ์›นํˆฐ ํŽธ์ง‘ ๋„๊ตฌ</h2>
594
+
595
+ <!-- ํŒŒ์ผ ์—…๋กœ๋“œ -->
596
+ <div class="tool-section">
597
+ <h3>๐Ÿ“ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ</h3>
598
+ <div class="file-upload" onclick="document.getElementById('fileInput').click()">
599
+ <svg width="40" height="40" fill="#4a9eff">
600
+ <rect x="5" y="10" width="30" height="25" fill="none" stroke="currentColor" stroke-width="2"/>
601
+ <polyline points="15,25 20,20 25,25" fill="none" stroke="currentColor" stroke-width="2"/>
602
+ <line x1="20" y1="20" x2="20" y2="30" stroke="currentColor" stroke-width="2"/>
603
+ </svg>
604
+ <p>ํด๋ฆญํ•˜์—ฌ ์ด๋ฏธ์ง€ ์„ ํƒ</p>
605
+ <p style="font-size: 11px; color: #888; margin-top: 5px;">ํŒŒ์ผ๋ช… ๊ธฐ์ค€ ์ž๋™ ์ •๋ ฌ (1.jpg, 2.jpg...)</p>
606
+ <input type="file" id="fileInput" multiple accept="image/*">
607
+ </div>
608
+ <div id="fileList"></div>
609
+
610
+ <!-- ์ด๋ฏธ์ง€ ๊ฐ„๊ฒฉ ์กฐ์ ˆ -->
611
+ <div class="property-group">
612
+ <label>์ด๋ฏธ์ง€ ๊ฐ„๊ฒฉ</label>
613
+ <div class="spacing-control">
614
+ <input type="range" id="imageSpacing" min="0" max="100" value="20" onchange="updateImageSpacing()">
615
+ <span class="spacing-value" id="spacingValue">20px</span>
616
+ </div>
617
+ </div>
618
+
619
+ <!-- ๊ฐœ๋ณ„ ์ด๋ฏธ์ง€ ์ €์žฅ -->
620
+ <div class="property-group">
621
+ <label>๊ฐœ๋ณ„ ์ด๋ฏธ์ง€ ์ €์žฅ</label>
622
+ <div class="save-individual">
623
+ <input type="number" id="imageNumber" placeholder="๋ฒˆํ˜ธ" min="1">
624
+ <button onclick="exportSingleImage()">์ €์žฅ</button>
625
+ </div>
626
+ </div>
627
+ </div>
628
+
629
+ <!-- ์ปดํฌ๋„ŒํŠธ -->
630
+ <div class="tool-section">
631
+ <h3>๐Ÿ’ฌ ๋งํ’์„  & ํšจ๊ณผ</h3>
632
+ <div class="component-grid">
633
+ <div class="component-btn" onclick="addComponent('speech')">
634
+ <svg viewBox="0 0 100 100" fill="none" stroke="white" stroke-width="2">
635
+ <ellipse cx="50" cy="40" rx="40" ry="30"/>
636
+ <path d="M 30 55 L 25 75 L 40 55"/>
637
+ </svg>
638
+ <span>๋งํ’์„ </span>
639
+ </div>
640
+ <div class="component-btn" onclick="addComponent('thought')">
641
+ <svg viewBox="0 0 100 100" fill="none" stroke="white" stroke-width="2">
642
+ <ellipse cx="50" cy="40" rx="35" ry="25"/>
643
+ <circle cx="30" cy="65" r="8"/>
644
+ <circle cx="20" cy="75" r="5"/>
645
+ </svg>
646
+ <span>์ƒ๊ฐํ’์„ </span>
647
+ </div>
648
+ <div class="component-btn" onclick="addComponent('shout')">
649
+ <svg viewBox="0 0 100 100" fill="none" stroke="white" stroke-width="3">
650
+ <polygon points="50,10 90,30 85,70 50,90 15,70 10,30"/>
651
+ </svg>
652
+ <span>์™ธ์นจํ’์„ </span>
653
+ </div>
654
+ <div class="component-btn" onclick="addComponent('whisper')">
655
+ <svg viewBox="0 0 100 100" fill="none" stroke="white" stroke-width="1" stroke-dasharray="3,3">
656
+ <ellipse cx="50" cy="45" rx="35" ry="25"/>
657
+ </svg>
658
+ <span>์†์‚ญ์ž„</span>
659
+ </div>
660
+ <div class="component-btn" onclick="addComponent('emotion')">
661
+ <svg viewBox="0 0 100 100" fill="#ffeb3b" stroke="white" stroke-width="2">
662
+ <ellipse cx="50" cy="45" rx="35" ry="25"/>
663
+ </svg>
664
+ <span>๊ฐ์ •ํ‘œํ˜„</span>
665
+ </div>
666
+ <div class="component-btn" onclick="addComponent('action')">
667
+ <svg viewBox="0 0 100 100" fill="none" stroke="white" stroke-width="2">
668
+ <text x="50" y="50" text-anchor="middle" font-size="30" fill="white">!</text>
669
+ </svg>
670
+ <span>ํšจ๊ณผ์Œ</span>
671
+ </div>
672
+ <div class="component-btn" onclick="addComponent('narration')">
673
+ <svg viewBox="0 0 100 100" fill="#333" stroke="white" stroke-width="2">
674
+ <rect x="15" y="30" width="70" height="40"/>
675
+ </svg>
676
+ <span>๋‚˜๋ ˆ์ด์…˜</span>
677
+ </div>
678
+ <div class="component-btn" onclick="addComponent('box')">
679
+ <svg viewBox="0 0 100 100" fill="none" stroke="white" stroke-width="2">
680
+ <rect x="20" y="30" width="60" height="40"/>
681
+ </svg>
682
+ <span>ํ…์ŠคํŠธ๋ฐ•์Šค</span>
683
+ </div>
684
+ <div class="component-btn" onclick="addComponent('transparent')">
685
+ <svg viewBox="0 0 100 100" fill="none" stroke="white" stroke-width="2" stroke-dasharray="5,5">
686
+ <rect x="20" y="30" width="60" height="40"/>
687
+ </svg>
688
+ <span>ํˆฌ๋ช…๋ฐ•์Šค</span>
689
+ </div>
690
+ <div class="component-btn" onclick="addComponent('gradient')">
691
+ <svg viewBox="0 0 100 100">
692
+ <defs>
693
+ <linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
694
+ <stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
695
+ <stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
696
+ </linearGradient>
697
+ </defs>
698
+ <rect x="20" y="30" width="60" height="40" fill="url(#grad1)" stroke="white" stroke-width="2"/>
699
+ </svg>
700
+ <span>๊ทธ๋ผ๋””์–ธํŠธ</span>
701
+ </div>
702
+ </div>
703
+ </div>
704
+
705
+ <!-- ์†์„ฑ ํŽธ์ง‘ -->
706
+ <div class="tool-section">
707
+ <h3>โš™๏ธ ์†์„ฑ ํŽธ์ง‘</h3>
708
+ <div class="properties-panel">
709
+ <div class="property-group">
710
+ <label>ํฐํŠธ</label>
711
+ <select id="fontSelect" onchange="updateSelectedComponent()">
712
+ <optgroup label="๊ธฐ๋ณธ ํฐํŠธ">
713
+ <option value="Arial">Arial</option>
714
+ <option value="Verdana">Verdana</option>
715
+ <option value="Georgia">Georgia</option>
716
+ <option value="'Times New Roman'">Times New Roman</option>
717
+ <option value="'Courier New'">Courier New</option>
718
+ <option value="'Comic Sans MS'">Comic Sans MS</option>
719
+ </optgroup>
720
+ <optgroup label="ํ•œ๊ธ€ ํฐํŠธ">
721
+ <option value="'Noto Sans KR'" selected>๋…ธํ†  ์‚ฐ์Šค</option>
722
+ <option value="'Black Han Sans'">๊ฒ€์€๊ณ ๋”•</option>
723
+ <option value="'Jua'">์ฃผ์•„</option>
724
+ <option value="'Do Hyeon'">๋„ํ˜„</option>
725
+ <option value="'Sunflower'">ํ•ด๋ฐ”๋ผ๊ธฐ</option>
726
+ <option value="'Cute Font'">๊ท€์—ฌ์šด ๊ธ€๊ผด</option>
727
+ <option value="'Gamja Flower'">๊ฐ์ž๊ฝƒ</option>
728
+ <option value="'Hi Melody'">ํ•˜์ด ๋ฉœ๋กœ๋””</option>
729
+ <option value="'Nanum Pen Script'">๋‚˜๋ˆ”ํŽœ</option>
730
+ <option value="'Stylish'">์Šคํƒ€์ผ๋ฆฌ์‹œ</option>
731
+ <option value="'Single Day'">์‹ฑ๊ธ€๋ฐ์ด</option>
732
+ <option value="'Gaegu'">๊ฐœ๊ตฌ์Ÿ์ด</option>
733
+ </optgroup>
734
+ </select>
735
+ </div>
736
+ <div class="property-group">
737
+ <label>๊ธ€์ž ํฌ๊ธฐ</label>
738
+ <input type="number" id="fontSizeInput" value="14" min="8" max="72" onchange="updateSelectedComponent()">
739
+ </div>
740
+ <div class="property-group">
741
+ <label>๊ธ€์ž ๊ตต๊ธฐ</label>
742
+ <select id="fontWeightSelect" onchange="updateSelectedComponent()">
743
+ <option value="100">์•„์ฃผ ์–‡๊ฒŒ</option>
744
+ <option value="300">์–‡๊ฒŒ</option>
745
+ <option value="400" selected>๋ณดํ†ต</option>
746
+ <option value="500">์•ฝ๊ฐ„ ๊ตต๊ฒŒ</option>
747
+ <option value="700">๊ตต๊ฒŒ</option>
748
+ <option value="900">์•„์ฃผ ๊ตต๊ฒŒ</option>
749
+ </select>
750
+ </div>
751
+ <div class="property-group">
752
+ <label>๊ธ€์ž ์ƒ‰์ƒ</label>
753
+ <div class="color-picker">
754
+ <div class="color-option selected" style="background: black" onclick="selectColor(this, 'black')"></div>
755
+ <div class="color-option" style="background: white; border: 1px solid #ccc;" onclick="selectColor(this, 'white')"></div>
756
+ <div class="color-option" style="background: #ff0000" onclick="selectColor(this, '#ff0000')"></div>
757
+ <div class="color-option" style="background: #ff6b6b" onclick="selectColor(this, '#ff6b6b')"></div>
758
+ <div class="color-option" style="background: #0066ff" onclick="selectColor(this, '#0066ff')"></div>
759
+ <div class="color-option" style="background: #4ecdc4" onclick="selectColor(this, '#4ecdc4')"></div>
760
+ <div class="color-option" style="background: #00b300" onclick="selectColor(this, '#00b300')"></div>
761
+ <div class="color-option" style="background: #95e1d3" onclick="selectColor(this, '#95e1d3')"></div>
762
+ <div class="color-option" style="background: #ffe100" onclick="selectColor(this, '#ffe100')"></div>
763
+ <div class="color-option" style="background: #ff9800" onclick="selectColor(this, '#ff9800')"></div>
764
+ <div class="color-option" style="background: #9c27b0" onclick="selectColor(this, '#9c27b0')"></div>
765
+ <div class="color-option" style="background: #ff00ff" onclick="selectColor(this, '#ff00ff')"></div>
766
+ </div>
767
+ </div>
768
+ <div class="property-group">
769
+ <label>ํ…์ŠคํŠธ</label>
770
+ <textarea id="textInput" rows="3" placeholder="ํ…์ŠคํŠธ ์ž…๋ ฅ..." onkeyup="updateSelectedComponent()"></textarea>
771
+ </div>
772
+ </div>
773
+ </div>
774
+ </div>
775
+
776
+ <!-- ๋ฉ”์ธ ์ž‘์—… ์˜์—ญ -->
777
+ <div class="workspace">
778
+ <div class="workspace-header">
779
+ <h1 style="font-size: 20px;">๐ŸŽจ ์›นํˆฐ ํŽธ์ง‘ ์ŠคํŠœ๋””์˜ค Pro</h1>
780
+ <div class="workspace-tools">
781
+ <button class="tool-btn" onclick="deleteSelected()">๐Ÿ—‘๏ธ ์„ ํƒ ์‚ญ์ œ</button>
782
+ <button class="tool-btn" onclick="clearAll()">๐Ÿ”„ ์ „์ฒด ์ดˆ๊ธฐํ™”</button>
783
+ <button class="tool-btn primary" onclick="exportSingleImage()">๐Ÿ’พ ์ด๋ฏธ์ง€ ์ €์žฅ</button>
784
+ </div>
785
+ </div>
786
+
787
+ <div class="canvas-container">
788
+ <div class="canvas-wrapper" id="canvas">
789
+ <!-- ์—…๋กœ๋“œ๋œ ์ด๋ฏธ์ง€์™€ ์ปดํฌ๋„ŒํŠธ๋“ค์ด ์—ฌ๊ธฐ์— ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค -->
790
+ </div>
791
+ </div>
792
+ </div>
793
+ </div>
794
+
795
+ <div class="loading" id="loading">
796
+ <div class="loading-spinner"></div>
797
+ <p>์ฒ˜๋ฆฌ ์ค‘...</p>
798
+ </div>
799
+
800
+ <!-- html2canvas ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ -->
801
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
802
+
803
+ <script>
804
+ let selectedComponent = null;
805
+ let isDragging = false;
806
+ let isResizing = false;
807
+ let dragOffset = { x: 0, y: 0 };
808
+ let componentCounter = 0;
809
+ let selectedColor = 'black';
810
+ let uploadedImages = [];
811
+ let currentDragElement = null;
812
+
813
+ // ์ž์—ฐ์Šค๋Ÿฌ์šด ์ •๋ ฌ ํ•จ์ˆ˜
814
+ function naturalSort(a, b) {
815
+ const extractNumber = (filename) => {
816
+ const match = filename.match(/(\d+)/);
817
+ return match ? parseInt(match[1]) : 0;
818
+ };
819
+
820
+ const aNum = extractNumber(a.name);
821
+ const bNum = extractNumber(b.name);
822
+
823
+ if (aNum !== bNum) {
824
+ return aNum - bNum;
825
+ }
826
+
827
+ return a.name.localeCompare(b.name);
828
+ }
829
+
830
+ // ํŒŒ์ผ ์—…๋กœ๋“œ ์ฒ˜๋ฆฌ
831
+ document.getElementById('fileInput').addEventListener('change', async function(e) {
832
+ const files = Array.from(e.target.files);
833
+ files.sort(naturalSort);
834
+
835
+ const canvas = document.getElementById('canvas');
836
+ canvas.querySelectorAll('.image-container').forEach(container => container.remove());
837
+ uploadedImages = [];
838
+
839
+ let fileListHTML = '<div class="file-list">';
840
+
841
+ const loadPromises = files.map((file, index) => {
842
+ return new Promise((resolve) => {
843
+ if (file.type.startsWith('image/')) {
844
+ const reader = new FileReader();
845
+
846
+ reader.onload = function(event) {
847
+ resolve({
848
+ index: index,
849
+ src: event.target.result,
850
+ name: file.name
851
+ });
852
+ };
853
+
854
+ reader.readAsDataURL(file);
855
+ } else {
856
+ resolve(null);
857
+ }
858
+ });
859
+ });
860
+
861
+ const loadedImages = await Promise.all(loadPromises);
862
+
863
+ loadedImages.forEach((imageData, index) => {
864
+ if (imageData) {
865
+ const container = document.createElement('div');
866
+ container.className = 'image-container';
867
+ container.id = 'image-container-' + (index + 1);
868
+
869
+ const numberLabel = document.createElement('div');
870
+ numberLabel.className = 'image-number';
871
+ numberLabel.textContent = index + 1;
872
+
873
+ const img = document.createElement('img');
874
+ img.src = imageData.src;
875
+ img.className = 'webtoon-image';
876
+ img.alt = imageData.name;
877
+ img.id = 'webtoon-image-' + (index + 1);
878
+
879
+ container.appendChild(numberLabel);
880
+ container.appendChild(img);
881
+ canvas.appendChild(container);
882
+
883
+ uploadedImages.push({
884
+ id: index + 1,
885
+ name: imageData.name,
886
+ element: img
887
+ });
888
+
889
+ fileListHTML += `<div class="file-item">${index + 1}. ${imageData.name}</div>`;
890
+ }
891
+ });
892
+
893
+ fileListHTML += '</div>';
894
+
895
+ const fileList = document.getElementById('fileList');
896
+ fileList.innerHTML = '<p style="font-size: 12px; color: #4a9eff; margin-bottom: 10px;">โœ… ์—…๋กœ๋“œ๋œ ํŒŒ์ผ: ' + files.length + '๊ฐœ</p>' + fileListHTML;
897
+
898
+ updateImageSpacing();
899
+ });
900
+
901
+ // ์ด๋ฏธ์ง€ ๊ฐ„๊ฒฉ ์—…๋ฐ์ดํŠธ
902
+ function updateImageSpacing() {
903
+ const spacing = document.getElementById('imageSpacing').value;
904
+ document.getElementById('spacingValue').textContent = spacing + 'px';
905
+
906
+ document.querySelectorAll('.webtoon-image').forEach(img => {
907
+ img.style.marginBottom = spacing + 'px';
908
+ });
909
+ }
910
+
911
+ // ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€
912
+ function addComponent(type) {
913
+ const canvas = document.getElementById('canvas');
914
+ const component = document.createElement('div');
915
+ component.className = 'draggable-component';
916
+ component.id = 'component-' + componentCounter++;
917
+
918
+ const canvasContainer = document.querySelector('.canvas-container');
919
+ const scrollTop = canvasContainer.scrollTop;
920
+
921
+ component.style.left = '50px';
922
+ component.style.top = (scrollTop + 50) + 'px';
923
+ component.style.width = '150px';
924
+ component.style.height = '80px';
925
+
926
+ let innerContent = '';
927
+
928
+ switch(type) {
929
+ case 'speech':
930
+ innerContent = `
931
+ <div class="speech-bubble">
932
+ <textarea class="component-text" placeholder="๋Œ€์‚ฌ ์ž…๋ ฅ..."></textarea>
933
+ <div class="speech-bubble-tail"></div>
934
+ </div>`;
935
+ break;
936
+ case 'thought':
937
+ innerContent = `
938
+ <div class="thought-bubble">
939
+ <textarea class="component-text" placeholder="์ƒ๊ฐ ์ž…๋ ฅ..."></textarea>
940
+ <div class="thought-bubble-dots">
941
+ <div class="thought-dot large"></div>
942
+ <div class="thought-dot small"></div>
943
+ </div>
944
+ </div>`;
945
+ break;
946
+ case 'shout':
947
+ innerContent = `
948
+ <div class="shout-bubble">
949
+ <textarea class="component-text" placeholder="์™ธ์นจ!" style="font-weight: bold;"></textarea>
950
+ </div>`;
951
+ break;
952
+ case 'whisper':
953
+ innerContent = `
954
+ <div class="whisper-bubble">
955
+ <textarea class="component-text" placeholder="์†์‚ญ์ž„..." style="font-style: italic;"></textarea>
956
+ </div>`;
957
+ break;
958
+ case 'emotion':
959
+ innerContent = `
960
+ <div class="emotion-bubble">
961
+ <textarea class="component-text" placeholder="โ™ฅ โ™ช โ˜…"></textarea>
962
+ </div>`;
963
+ break;
964
+ case 'action':
965
+ innerContent = `
966
+ <div class="action-effect">
967
+ <textarea class="component-text" placeholder="์ฟต!" style="font-size: 24px; font-weight: bold;"></textarea>
968
+ </div>`;
969
+ break;
970
+ case 'narration':
971
+ innerContent = `
972
+ <div class="narration-box">
973
+ <textarea class="component-text" placeholder="๋‚˜๋ ˆ์ด์…˜..."></textarea>
974
+ </div>`;
975
+ break;
976
+ case 'box':
977
+ innerContent = `
978
+ <div class="text-box">
979
+ <textarea class="component-text" placeholder="ํ…์ŠคํŠธ ์ž…๋ ฅ..."></textarea>
980
+ </div>`;
981
+ break;
982
+ case 'transparent':
983
+ innerContent = `
984
+ <div class="transparent-box">
985
+ <textarea class="component-text" placeholder="ํ…์ŠคํŠธ ์ž…๋ ฅ..."></textarea>
986
+ </div>`;
987
+ break;
988
+ case 'gradient':
989
+ innerContent = `
990
+ <div class="gradient-box">
991
+ <textarea class="component-text" placeholder="ํŠน์ˆ˜ ํšจ๊ณผ!"></textarea>
992
+ </div>`;
993
+ break;
994
+ }
995
+
996
+ component.innerHTML = innerContent + `
997
+ <div class="resize-handle nw"></div>
998
+ <div class="resize-handle ne"></div>
999
+ <div class="resize-handle sw"></div>
1000
+ <div class="resize-handle se"></div>
1001
+ `;
1002
+
1003
+ canvas.appendChild(component);
1004
+
1005
+ // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€
1006
+ component.addEventListener('mousedown', startDrag);
1007
+
1008
+ // ํ…์ŠคํŠธ ์˜์—ญ ํด๋ฆญ ์‹œ ๋“œ๋ž˜๊ทธ ๋ฐฉ์ง€
1009
+ const textArea = component.querySelector('.component-text');
1010
+ if (textArea) {
1011
+ textArea.addEventListener('mousedown', function(e) {
1012
+ e.stopPropagation();
1013
+ });
1014
+ textArea.addEventListener('click', function(e) {
1015
+ e.stopPropagation();
1016
+ selectComponent(component);
1017
+ });
1018
+ }
1019
+
1020
+ // ๋ฆฌ์‚ฌ์ด์ฆˆ ํ•ธ๋“ค ์ด๋ฒคํŠธ
1021
+ component.querySelectorAll('.resize-handle').forEach(handle => {
1022
+ handle.addEventListener('mousedown', startResize);
1023
+ });
1024
+
1025
+ selectComponent(component);
1026
+ }
1027
+
1028
+ // ์ปดํฌ๋„ŒํŠธ ์„ ํƒ
1029
+ function selectComponent(component) {
1030
+ document.querySelectorAll('.draggable-component').forEach(c => {
1031
+ c.classList.remove('selected');
1032
+ });
1033
+
1034
+ component.classList.add('selected');
1035
+ selectedComponent = component;
1036
+
1037
+ const textArea = component.querySelector('.component-text');
1038
+ if (textArea) {
1039
+ document.getElementById('textInput').value = textArea.value;
1040
+ document.getElementById('fontSizeInput').value = parseInt(textArea.style.fontSize) || 14;
1041
+ document.getElementById('fontSelect').value = textArea.style.fontFamily || "'Noto Sans KR'";
1042
+ document.getElementById('fontWeightSelect').value = textArea.style.fontWeight || '400';
1043
+ }
1044
+ }
1045
+
1046
+ // ๋“œ๋ž˜๊ทธ ์‹œ์ž‘ (์ˆ˜์ •๋จ)
1047
+ function startDrag(e) {
1048
+ if (e.target.classList.contains('resize-handle')) return;
1049
+ if (e.target.classList.contains('component-text')) return;
1050
+
1051
+ const component = e.currentTarget;
1052
+ selectComponent(component);
1053
+
1054
+ isDragging = true;
1055
+ currentDragElement = component;
1056
+
1057
+ // ๋“œ๋ž˜๊ทธ ์ค‘ ํด๋ž˜์Šค ์ถ”๊ฐ€
1058
+ component.classList.add('dragging');
1059
+
1060
+ const canvasRect = document.getElementById('canvas').getBoundingClientRect();
1061
+ const componentRect = component.getBoundingClientRect();
1062
+
1063
+ dragOffset.x = e.clientX - componentRect.left;
1064
+ dragOffset.y = e.clientY - componentRect.top;
1065
+
1066
+ document.addEventListener('mousemove', drag);
1067
+ document.addEventListener('mouseup', stopDrag);
1068
+
1069
+ e.preventDefault();
1070
+ }
1071
+
1072
+ // ๋“œ๋ž˜๊ทธ ์ค‘ (์ˆ˜์ •๋จ)
1073
+ function drag(e) {
1074
+ if (!isDragging || !currentDragElement) return;
1075
+
1076
+ const canvas = document.getElementById('canvas');
1077
+ const canvasRect = canvas.getBoundingClientRect();
1078
+ const containerScrollTop = document.querySelector('.canvas-container').scrollTop;
1079
+
1080
+ let newX = e.clientX - canvasRect.left - dragOffset.x;
1081
+ let newY = e.clientY - canvasRect.top - dragOffset.y + containerScrollTop;
1082
+
1083
+ // ์บ”๋ฒ„์Šค ๋ฒ”์œ„ ๋‚ด๋กœ ์ œํ•œ
1084
+ newX = Math.max(0, Math.min(newX, canvas.offsetWidth - currentDragElement.offsetWidth));
1085
+ newY = Math.max(0, newY);
1086
+
1087
+ currentDragElement.style.left = newX + 'px';
1088
+ currentDragElement.style.top = newY + 'px';
1089
+
1090
+ e.preventDefault();
1091
+ }
1092
+
1093
+ // ๋“œ๋ž˜๊ทธ ์ข…๋ฃŒ (์ˆ˜์ •๋จ)
1094
+ function stopDrag(e) {
1095
+ if (currentDragElement) {
1096
+ currentDragElement.classList.remove('dragging');
1097
+ }
1098
+ isDragging = false;
1099
+ currentDragElement = null;
1100
+ document.removeEventListener('mousemove', drag);
1101
+ document.removeEventListener('mouseup', stopDrag);
1102
+
1103
+ e.preventDefault();
1104
+ }
1105
+
1106
+ // ๋ฆฌ์‚ฌ์ด์ฆˆ ์‹œ์ž‘
1107
+ function startResize(e) {
1108
+ e.stopPropagation();
1109
+ isResizing = true;
1110
+
1111
+ const handle = e.target;
1112
+ const component = handle.parentElement;
1113
+ selectComponent(component);
1114
+
1115
+ const startX = e.clientX;
1116
+ const startY = e.clientY;
1117
+ const startWidth = component.offsetWidth;
1118
+ const startHeight = component.offsetHeight;
1119
+ const startLeft = component.offsetLeft;
1120
+ const startTop = component.offsetTop;
1121
+
1122
+ function resize(e) {
1123
+ if (!isResizing) return;
1124
+
1125
+ const dx = e.clientX - startX;
1126
+ const dy = e.clientY - startY;
1127
+
1128
+ if (handle.classList.contains('se')) {
1129
+ component.style.width = Math.max(80, startWidth + dx) + 'px';
1130
+ component.style.height = Math.max(40, startHeight + dy) + 'px';
1131
+ } else if (handle.classList.contains('sw')) {
1132
+ component.style.width = Math.max(80, startWidth - dx) + 'px';
1133
+ component.style.height = Math.max(40, startHeight + dy) + 'px';
1134
+ component.style.left = Math.min(startLeft + dx, startLeft + startWidth - 80) + 'px';
1135
+ } else if (handle.classList.contains('ne')) {
1136
+ component.style.width = Math.max(80, startWidth + dx) + 'px';
1137
+ component.style.height = Math.max(40, startHeight - dy) + 'px';
1138
+ component.style.top = Math.min(startTop + dy, startTop + startHeight - 40) + 'px';
1139
+ } else if (handle.classList.contains('nw')) {
1140
+ component.style.width = Math.max(80, startWidth - dx) + 'px';
1141
+ component.style.height = Math.max(40, startHeight - dy) + 'px';
1142
+ component.style.left = Math.min(startLeft + dx, startLeft + startWidth - 80) + 'px';
1143
+ component.style.top = Math.min(startTop + dy, startTop + startHeight - 40) + 'px';
1144
+ }
1145
+ }
1146
+
1147
+ function stopResize() {
1148
+ isResizing = false;
1149
+ document.removeEventListener('mousemove', resize);
1150
+ document.removeEventListener('mouseup', stopResize);
1151
+ }
1152
+
1153
+ document.addEventListener('mousemove', resize);
1154
+ document.addEventListener('mouseup', stopResize);
1155
+ }
1156
+
1157
+ // ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ
1158
+ function updateSelectedComponent() {
1159
+ if (!selectedComponent) return;
1160
+
1161
+ const textArea = selectedComponent.querySelector('.component-text');
1162
+ if (textArea) {
1163
+ textArea.value = document.getElementById('textInput').value;
1164
+ textArea.style.fontSize = document.getElementById('fontSizeInput').value + 'px';
1165
+ textArea.style.fontFamily = document.getElementById('fontSelect').value;
1166
+ textArea.style.fontWeight = document.getElementById('fontWeightSelect').value;
1167
+ textArea.style.color = selectedColor;
1168
+ }
1169
+ }
1170
+
1171
+ // ์ƒ‰์ƒ ์„ ํƒ
1172
+ function selectColor(element, color) {
1173
+ document.querySelectorAll('.color-option').forEach(opt => {
1174
+ opt.classList.remove('selected');
1175
+ });
1176
+ element.classList.add('selected');
1177
+ selectedColor = color;
1178
+ updateSelectedComponent();
1179
+ }
1180
+
1181
+ // ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ ์‚ญ์ œ
1182
+ function deleteSelected() {
1183
+ if (selectedComponent) {
1184
+ selectedComponent.remove();
1185
+ selectedComponent = null;
1186
+ document.getElementById('textInput').value = '';
1187
+ }
1188
+ }
1189
+
1190
+ // ์ „์ฒด ์ดˆ๊ธฐํ™”
1191
+ function clearAll() {
1192
+ if (confirm('๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?')) {
1193
+ document.querySelectorAll('.draggable-component').forEach(c => c.remove());
1194
+ selectedComponent = null;
1195
+ document.getElementById('textInput').value = '';
1196
+ }
1197
+ }
1198
+
1199
+ // ๊ฐœ๋ณ„ ์ด๋ฏธ์ง€ ์ €์žฅ
1200
+ function exportSingleImage() {
1201
+ const imageNumber = parseInt(document.getElementById('imageNumber').value);
1202
+
1203
+ if (!imageNumber || imageNumber < 1) {
1204
+ alert('โš ๏ธ ์ €์žฅํ•  ์ด๋ฏธ์ง€ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.');
1205
+ return;
1206
+ }
1207
+
1208
+ const imageContainer = document.getElementById('image-container-' + imageNumber);
1209
+
1210
+ if (!imageContainer) {
1211
+ alert('โš ๏ธ ' + imageNumber + '๋ฒˆ ์ด๋ฏธ์ง€๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.');
1212
+ return;
1213
+ }
1214
+
1215
+ const loading = document.getElementById('loading');
1216
+ loading.classList.add('active');
1217
+
1218
+ document.querySelectorAll('.draggable-component').forEach(c => {
1219
+ c.classList.remove('selected');
1220
+ });
1221
+
1222
+ const numberLabel = imageContainer.querySelector('.image-number');
1223
+ if (numberLabel) numberLabel.style.display = 'none';
1224
+
1225
+ const tempContainer = document.createElement('div');
1226
+ tempContainer.style.position = 'relative';
1227
+ tempContainer.style.width = imageContainer.offsetWidth + 'px';
1228
+ tempContainer.style.background = 'white';
1229
+
1230
+ const imgClone = imageContainer.querySelector('.webtoon-image').cloneNode(true);
1231
+ imgClone.style.marginBottom = '0';
1232
+ tempContainer.appendChild(imgClone);
1233
+
1234
+ const imageTop = imageContainer.offsetTop;
1235
+ const imageBottom = imageTop + imageContainer.offsetHeight;
1236
+
1237
+ document.querySelectorAll('.draggable-component').forEach(component => {
1238
+ const compTop = parseInt(component.style.top);
1239
+ const compBottom = compTop + component.offsetHeight;
1240
+
1241
+ if (compTop < imageBottom && compBottom > imageTop) {
1242
+ const compClone = component.cloneNode(true);
1243
+ compClone.classList.remove('selected');
1244
+ compClone.style.top = (compTop - imageTop) + 'px';
1245
+ tempContainer.appendChild(compClone);
1246
+ }
1247
+ });
1248
+
1249
+ tempContainer.style.position = 'absolute';
1250
+ tempContainer.style.left = '-9999px';
1251
+ document.body.appendChild(tempContainer);
1252
+
1253
+ html2canvas(tempContainer, {
1254
+ backgroundColor: '#ffffff',
1255
+ scale: 2,
1256
+ useCORS: true,
1257
+ logging: false
1258
+ }).then(function(canvas) {
1259
+ const link = document.createElement('a');
1260
+ link.download = 'webtoon_' + imageNumber + '_' + new Date().getTime() + '.png';
1261
+ link.href = canvas.toDataURL('image/png');
1262
+ link.click();
1263
+
1264
+ document.body.removeChild(tempContainer);
1265
+
1266
+ if (numberLabel) numberLabel.style.display = 'flex';
1267
+
1268
+ loading.classList.remove('active');
1269
+
1270
+ alert('โœ… ' + imageNumber + '๋ฒˆ ์ด๋ฏธ์ง€๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!');
1271
+ }).catch(function(error) {
1272
+ document.body.removeChild(tempContainer);
1273
+ if (numberLabel) numberLabel.style.display = 'flex';
1274
+ loading.classList.remove('active');
1275
+ console.error('Error:', error);
1276
+ alert('โš ๏ธ ์ด๋ฏธ์ง€ ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.');
1277
+ });
1278
+ }
1279
+
1280
+ // ์บ”๋ฒ„์Šค ํด๋ฆญ ์ด๋ฒคํŠธ
1281
+ document.getElementById('canvas').addEventListener('click', function(e) {
1282
+ if (e.target === this || e.target.classList.contains('webtoon-image')) {
1283
+ document.querySelectorAll('.draggable-component').forEach(c => {
1284
+ c.classList.remove('selected');
1285
+ });
1286
+ selectedComponent = null;
1287
+ document.getElementById('textInput').value = '';
1288
+ }
1289
+ });
1290
+
1291
+ // ํ‚ค๋ณด๋“œ ๋‹จ์ถ•ํ‚ค
1292
+ document.addEventListener('keydown', function(e) {
1293
+ if (e.key === 'Delete' && selectedComponent) {
1294
+ deleteSelected();
1295
+ }
1296
+ });
1297
+ </script>
1298
+ </body>
1299
+ </html>
1300
+ '''
1301
+
1302
+ @app.route('/')
1303
+ def index():
1304
+ return render_template_string(HTML_TEMPLATE)
1305
+
1306
+ if __name__ == '__main__':
1307
+ app.run(debug=True, port=5000)