ginipick commited on
Commit
37c334b
ยท
verified ยท
1 Parent(s): bfd96c7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +27 -2592
app.py CHANGED
@@ -1311,2598 +1311,33 @@ async def root(request: Request, pdf_id: Optional[str] = Query(None)):
1311
  return get_html_content()
1312
 
1313
  # HTML ๋ฌธ์ž์—ด (AI ๋ฒ„ํŠผ ๋ฐ ์ฑ—๋ด‡ UI ์ถ”๊ฐ€)
1314
- HTML = """
1315
- <!doctype html>
1316
- <html lang="ko">
1317
- <head>
1318
- <meta charset="utf-8">
1319
- <title>FlipBook Space</title>
1320
- <link rel="stylesheet" href="/static/flipbook.css">
1321
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
1322
- <script src="/static/three.js"></script>
1323
- <script src="/static/iscroll.js"></script>
1324
- <script src="/static/mark.js"></script>
1325
- <script src="/static/mod3d.js"></script>
1326
- <script src="/static/pdf.js"></script>
1327
- <script src="/static/flipbook.js"></script>
1328
- <script src="/static/flipbook.book3.js"></script>
1329
- <script src="/static/flipbook.scroll.js"></script>
1330
- <script src="/static/flipbook.swipe.js"></script>
1331
- <script src="/static/flipbook.webgl.js"></script>
1332
- <style>
1333
- /* ์ „์ฒด ์‚ฌ์ดํŠธ ํŒŒ์Šคํ…”ํ†ค ํ…Œ๋งˆ */
1334
- :root {
1335
- --primary-color: #a5d8ff; /* ํŒŒ์Šคํ…” ๋ธ”๋ฃจ */
1336
- --secondary-color: #ffd6e0; /* ํŒŒ์Šคํ…” ํ•‘ํฌ */
1337
- --tertiary-color: #c3fae8; /* ํŒŒ์Šคํ…” ๋ฏผํŠธ */
1338
- --accent-color: #d0bfff; /* ํŒŒ์Šคํ…” ํผํ”Œ */
1339
- --ai-color: #86e8ab; /* AI ๋ฒ„ํŠผ ์ƒ‰์ƒ */
1340
- --ai-hover: #65d68a; /* AI ํ˜ธ๋ฒ„ ์ƒ‰์ƒ */
1341
- --bg-color: #f8f9fa; /* ๋ฐ์€ ๋ฐฐ๊ฒฝ */
1342
- --text-color: #495057; /* ๋ถ€๋“œ๋Ÿฌ์šด ์–ด๋‘์šด ์ƒ‰ */
1343
- --card-bg: #ffffff; /* ์นด๋“œ ๋ฐฐ๊ฒฝ์ƒ‰ */
1344
- --shadow-sm: 0 2px 8px rgba(0,0,0,0.05);
1345
- --shadow-md: 0 4px 12px rgba(0,0,0,0.08);
1346
- --shadow-lg: 0 8px 24px rgba(0,0,0,0.12);
1347
- --radius-sm: 8px;
1348
- --radius-md: 12px;
1349
- --radius-lg: 16px;
1350
- --transition: all 0.3s ease;
1351
- }
1352
-
1353
-
1354
- body {
1355
- margin: 0;
1356
- background: var(--bg-color);
1357
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1358
- color: var(--text-color);
1359
- /* ์ƒˆ๋กœ์šด ํผํ”Œ ๊ณ„ํ†ต ๊ณ ๊ธ‰์Šค๋Ÿฌ์šด ๊ทธ๋ผ๋””์—์ด์…˜ ๋ฐฐ๊ฒฝ */
1360
- background-image: linear-gradient(135deg, #2a0845 0%, #6441a5 50%, #c9a8ff 100%);
1361
- background-attachment: fixed;
1362
- }
1363
-
1364
- /* ๋ทฐ์–ด ๋ชจ๋“œ์ผ ๋•Œ ๋ฐฐ๊ฒฝ ๋ณ€๊ฒฝ */
1365
- .viewer-mode {
1366
- background-image: linear-gradient(135deg, #30154e 0%, #6b47ad 50%, #d5b8ff 100%) !important;
1367
- }
1368
-
1369
- /* ํ—ค๋” ์ œ๋ชฉ ์ œ๊ฑฐ ๋ฐ Home ๋ฒ„ํŠผ ๋ ˆ์ด์–ด ์ฒ˜๋ฆฌ */
1370
- .floating-home, .floating-ai {
1371
- position: fixed;
1372
- top: 20px;
1373
- left: 20px;
1374
- width: 60px;
1375
- height: 60px;
1376
- border-radius: 50%;
1377
- background: rgba(255, 255, 255, 0.9);
1378
- backdrop-filter: blur(10px);
1379
- box-shadow: var(--shadow-md);
1380
- z-index: 9999;
1381
- display: flex;
1382
- justify-content: center;
1383
- align-items: center;
1384
- cursor: pointer;
1385
- transition: var(--transition);
1386
- overflow: hidden;
1387
- }
1388
-
1389
- .floating-ai {
1390
- top: 90px; /* Home ๋ฒ„ํŠผ ์•„๋ž˜์— ์œ„์น˜ */
1391
- background: rgba(134, 232, 171, 0.9); /* AI ๋ฒ„ํŠผ ์ƒ‰์ƒ */
1392
- }
1393
-
1394
- .floating-home:hover, .floating-ai:hover {
1395
- transform: scale(1.05);
1396
- box-shadow: var(--shadow-lg);
1397
- }
1398
-
1399
- .floating-home .icon, .floating-ai .icon {
1400
- display: flex;
1401
- justify-content: center;
1402
- align-items: center;
1403
- width: 100%;
1404
- height: 100%;
1405
- font-size: 22px;
1406
- color: var(--primary-color);
1407
- transition: var(--transition);
1408
- }
1409
-
1410
- .floating-ai .icon {
1411
- color: white;
1412
- }
1413
-
1414
- .floating-home:hover .icon {
1415
- color: #8bc5f8;
1416
- }
1417
-
1418
- .floating-ai:hover .icon {
1419
- color: #ffffff;
1420
- }
1421
-
1422
- .floating-home .title, .floating-ai .title {
1423
- position: absolute;
1424
- left: 70px;
1425
- background: rgba(255, 255, 255, 0.95);
1426
- padding: 8px 20px;
1427
- border-radius: 20px;
1428
- box-shadow: var(--shadow-sm);
1429
- font-weight: 600;
1430
- font-size: 14px;
1431
- white-space: nowrap;
1432
- pointer-events: none;
1433
- opacity: 0;
1434
- transform: translateX(-10px);
1435
- transition: all 0.3s ease;
1436
- }
1437
-
1438
- .floating-home:hover .title, .floating-ai:hover .title {
1439
- opacity: 1;
1440
- transform: translateX(0);
1441
- }
1442
-
1443
- /* ๊ด€๋ฆฌ์ž ๋ฒ„ํŠผ ์Šคํƒ€์ผ */
1444
- #adminButton {
1445
- position: fixed;
1446
- top: 20px;
1447
- right: 20px;
1448
- background: rgba(255, 255, 255, 0.9);
1449
- backdrop-filter: blur(10px);
1450
- box-shadow: var(--shadow-md);
1451
- border-radius: 30px;
1452
- padding: 8px 20px;
1453
- display: flex;
1454
- align-items: center;
1455
- font-weight: 600;
1456
- font-size: 14px;
1457
- cursor: pointer;
1458
- transition: var(--transition);
1459
- z-index: 9999;
1460
- }
1461
-
1462
- #adminButton i {
1463
- margin-right: 8px;
1464
- color: var(--accent-color);
1465
- }
1466
-
1467
- #adminButton:hover {
1468
- transform: translateY(-3px);
1469
- box-shadow: var(--shadow-lg);
1470
- }
1471
-
1472
- /* ๊ด€๋ฆฌ์ž ๋กœ๊ทธ์ธ ๋ชจ๋‹ฌ */
1473
- .modal {
1474
- display: none;
1475
- position: fixed;
1476
- top: 0;
1477
- left: 0;
1478
- width: 100%;
1479
- height: 100%;
1480
- background: rgba(0, 0, 0, 0.5);
1481
- backdrop-filter: blur(5px);
1482
- z-index: 10000;
1483
- align-items: center;
1484
- justify-content: center;
1485
- }
1486
-
1487
- .modal-content {
1488
- background: white;
1489
- border-radius: var(--radius-md);
1490
- padding: 30px;
1491
- width: 90%;
1492
- max-width: 400px;
1493
- box-shadow: var(--shadow-lg);
1494
- text-align: center;
1495
- }
1496
-
1497
- .modal-content h2 {
1498
- margin-top: 0;
1499
- color: var(--accent-color);
1500
- margin-bottom: 20px;
1501
- }
1502
-
1503
- .modal-content input {
1504
- width: 100%;
1505
- padding: 12px;
1506
- margin-bottom: 20px;
1507
- border: 1px solid #ddd;
1508
- border-radius: var(--radius-sm);
1509
- font-size: 16px;
1510
- box-sizing: border-box;
1511
- }
1512
-
1513
- .modal-content button {
1514
- padding: 10px 20px;
1515
- border: none;
1516
- border-radius: var(--radius-sm);
1517
- background: var(--accent-color);
1518
- color: white;
1519
- font-weight: 600;
1520
- cursor: pointer;
1521
- margin: 0 5px;
1522
- transition: var(--transition);
1523
- }
1524
-
1525
- .modal-content button:hover {
1526
- opacity: 0.9;
1527
- transform: translateY(-2px);
1528
- }
1529
-
1530
- .modal-content #adminLoginClose {
1531
- background: #f1f3f5;
1532
- color: var(--text-color);
1533
- }
1534
-
1535
- #home, #viewerPage, #adminPage {
1536
- padding-top: 100px;
1537
- max-width: 1200px;
1538
- margin: 0 auto;
1539
- padding-bottom: 60px;
1540
- padding-left: 30px;
1541
- padding-right: 30px;
1542
- position: relative;
1543
- }
1544
-
1545
- /* ์—…๋กœ๋“œ ๋ฒ„ํŠผ ์Šคํƒ€์ผ */
1546
- .upload-container {
1547
- display: flex;
1548
- margin-bottom: 30px;
1549
- justify-content: center;
1550
- }
1551
-
1552
- button.upload {
1553
- all: unset;
1554
- cursor: pointer;
1555
- padding: 12px 20px;
1556
- border-radius: var(--radius-md);
1557
- background: white;
1558
- margin: 0 10px;
1559
- font-weight: 500;
1560
- display: flex;
1561
- align-items: center;
1562
-
1563
-
1564
- box-shadow: var(--shadow-sm);
1565
- transition: var(--transition);
1566
- position: relative;
1567
- overflow: hidden;
1568
- }
1569
-
1570
- button.upload::before {
1571
- content: '';
1572
- position: absolute;
1573
- top: 0;
1574
- left: 0;
1575
- width: 100%;
1576
- height: 100%;
1577
- background: linear-gradient(120deg, var(--primary-color) 0%, var(--secondary-color) 100%);
1578
- opacity: 0.08;
1579
- z-index: -1;
1580
- }
1581
-
1582
- button.upload:hover {
1583
- transform: translateY(-3px);
1584
- box-shadow: var(--shadow-md);
1585
- }
1586
-
1587
- button.upload:hover::before {
1588
- opacity: 0.15;
1589
- }
1590
-
1591
- button.upload i {
1592
- margin-right: 8px;
1593
- font-size: 20px;
1594
- }
1595
-
1596
- /* ๊ทธ๋ฆฌ๋“œ ๋ฐ ์นด๋“œ ์Šคํƒ€์ผ */
1597
- .grid {
1598
- display: grid;
1599
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
1600
- gap: 24px;
1601
- margin-top: 36px;
1602
- }
1603
-
1604
- .card {
1605
- background: var(--card-bg);
1606
- border-radius: var(--radius-md);
1607
- cursor: pointer;
1608
- box-shadow: var(--shadow-sm);
1609
- width: 100%;
1610
- height: 280px;
1611
- position: relative;
1612
- display: flex;
1613
- flex-direction: column;
1614
- align-items: center;
1615
- justify-content: center;
1616
- transition: var(--transition);
1617
- overflow: hidden;
1618
- }
1619
-
1620
- .card::before {
1621
- content: '';
1622
- position: absolute;
1623
- top: 0;
1624
- left: 0;
1625
- width: 100%;
1626
- height: 100%;
1627
- background: linear-gradient(135deg, var(--secondary-color) 0%, var(--primary-color) 100%);
1628
- opacity: 0.06;
1629
- z-index: 1;
1630
- }
1631
-
1632
- .card::after {
1633
- content: '';
1634
- position: absolute;
1635
- top: 0;
1636
- left: 0;
1637
- width: 100%;
1638
- height: 30%;
1639
- background: linear-gradient(to bottom, rgba(255,255,255,0.8) 0%, rgba(255,255,255,0) 100%);
1640
- z-index: 2;
1641
- }
1642
-
1643
- .card img {
1644
- width: 65%;
1645
- height: auto;
1646
- object-fit: contain;
1647
- position: absolute;
1648
- top: 50%;
1649
- left: 50%;
1650
- transform: translate(-50%, -65%);
1651
- border: 1px solid rgba(0,0,0,0.05);
1652
- box-shadow: 0 4px 15px rgba(0,0,0,0.08);
1653
- z-index: 3;
1654
- transition: var(--transition);
1655
- }
1656
-
1657
- .card:hover {
1658
- transform: translateY(-5px);
1659
- box-shadow: var(--shadow-md);
1660
- }
1661
-
1662
- .card:hover img {
1663
- transform: translate(-50%, -65%) scale(1.03);
1664
- box-shadow: 0 8px 20px rgba(0,0,0,0.12);
1665
- }
1666
-
1667
- .card p {
1668
- position: absolute;
1669
- bottom: 20px;
1670
- left: 50%;
1671
- transform: translateX(-50%);
1672
- background: rgba(255, 255, 255, 0.9);
1673
- padding: 8px 16px;
1674
- border-radius: 30px;
1675
- box-shadow: 0 2px 10px rgba(0,0,0,0.05);
1676
- width: 80%;
1677
- text-align: center;
1678
- white-space: nowrap;
1679
- overflow: hidden;
1680
- text-overflow: ellipsis;
1681
- font-size: 14px;
1682
- font-weight: 500;
1683
- color: var(--text-color);
1684
- z-index: 4;
1685
- transition: var(--transition);
1686
- }
1687
-
1688
- .card:hover p {
1689
- background: rgba(255, 255, 255, 0.95);
1690
- box-shadow: 0 4px 12px rgba(0,0,0,0.08);
1691
- }
1692
-
1693
- /* ์บ์‹œ ์ƒํƒœ ๋ฑƒ์ง€ */
1694
- .cached-status {
1695
- position: absolute;
1696
- top: 10px;
1697
- right: 10px;
1698
- background: var(--accent-color);
1699
- color: white;
1700
- font-size: 11px;
1701
- padding: 3px 8px;
1702
- border-radius: 12px;
1703
- z-index: 5;
1704
- box-shadow: var(--shadow-sm);
1705
- }
1706
-
1707
- /* ๊ด€๋ฆฌ์ž ์นด๋“œ ์ถ”๊ฐ€ ์Šคํƒ€์ผ */
1708
- .admin-card {
1709
- height: 300px;
1710
- }
1711
-
1712
- .admin-card .card-inner {
1713
- width: 100%;
1714
- height: 100%;
1715
- position: relative;
1716
- display: flex;
1717
- flex-direction: column;
1718
- align-items: center;
1719
- }
1720
-
1721
- .delete-btn, .feature-btn, .unfeature-btn {
1722
- position: absolute;
1723
- bottom: 60px;
1724
- left: 50%;
1725
- transform: translateX(-50%);
1726
- background: #ff7675;
1727
- color: white;
1728
- border: none;
1729
- border-radius: 20px;
1730
- padding: 5px 15px;
1731
- font-size: 12px;
1732
- cursor: pointer;
1733
- z-index: 10;
1734
- transition: var(--transition);
1735
- }
1736
-
1737
- .feature-btn {
1738
- bottom: 95px;
1739
- background: #74b9ff;
1740
- }
1741
-
1742
- .unfeature-btn {
1743
- bottom: 95px;
1744
- background: #a29bfe;
1745
- }
1746
-
1747
- .delete-btn:hover, .feature-btn:hover, .unfeature-btn:hover {
1748
- opacity: 0.9;
1749
- transform: translateX(-50%) scale(1.05);
1750
- }
1751
-
1752
- /* ๋ทฐ์–ด ์Šคํƒ€์ผ */
1753
- #viewer {
1754
- width: 90%;
1755
- height: 90vh;
1756
- max-width: 90%;
1757
- margin: 0;
1758
- background: var(--card-bg);
1759
- border: none;
1760
- border-radius: var(--radius-lg);
1761
- position: fixed;
1762
- top: 50%;
1763
- left: 50%;
1764
- transform: translate(-50%, -50%);
1765
- z-index: 1000;
1766
- box-shadow: var(--shadow-lg);
1767
- max-height: calc(90vh - 40px);
1768
- aspect-ratio: auto;
1769
- object-fit: contain;
1770
- overflow: hidden;
1771
- }
1772
-
1773
- /* FlipBook ์ปจํŠธ๋กค๋ฐ” ์Šคํƒ€์ผ */
1774
- .flipbook-container .fb3d-menu-bar {
1775
- z-index: 2000 !important;
1776
- opacity: 1 !important;
1777
- bottom: 0 !important;
1778
- background-color: rgba(255,255,255,0.9) !important;
1779
- backdrop-filter: blur(10px) !important;
1780
- border-radius: 0 0 var(--radius-lg) var(--radius-lg) !important;
1781
- padding: 12px 0 !important;
1782
- box-shadow: 0 -4px 20px rgba(0,0,0,0.1) !important;
1783
- }
1784
-
1785
- .flipbook-container .fb3d-menu-bar > ul > li > img,
1786
- .flipbook-container .fb3d-menu-bar > ul > li > div {
1787
- opacity: 1 !important;
1788
- transform: scale(1.2) !important;
1789
- filter: drop-shadow(0 2px 3px rgba(0,0,0,0.1)) !important;
1790
- }
1791
-
1792
- .flipbook-container .fb3d-menu-bar > ul > li {
1793
- margin: 0 12px !important;
1794
- }
1795
-
1796
- /* ๋ฉ”๋‰ด ํˆดํŒ ์Šคํƒ€์ผ */
1797
- .flipbook-container .fb3d-menu-bar > ul > li > span {
1798
- background-color: rgba(0,0,0,0.7) !important;
1799
- color: white !important;
1800
- border-radius: var(--radius-sm) !important;
1801
- padding: 8px 12px !important;
1802
- font-size: 13px !important;
1803
- bottom: 55px !important;
1804
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important;
1805
- letter-spacing: 0.3px !important;
1806
- }
1807
-
1808
- /* ๋ทฐ์–ด ๋ชจ๋“œ์ผ ๋•Œ ๋ฐฐ๊ฒฝ ์˜ค๋ฒ„๋ ˆ์ด */
1809
- .viewer-mode {
1810
- background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%) !important;
1811
- }
1812
-
1813
- /* ๋ทฐ์–ด ํŽ˜์ด์ง€ ๋ฐฐ๊ฒฝ */
1814
- #viewerPage {
1815
- background: transparent;
1816
- }
1817
-
1818
- /* ๋กœ๋”ฉ ์• ๋‹ˆ๋ฉ”์ด์…˜ */
1819
- @keyframes spin {
1820
- 0% { transform: rotate(0deg); }
1821
- 100% { transform: rotate(360deg); }
1822
- }
1823
-
1824
- .loading-spinner {
1825
- border: 4px solid rgba(255,255,255,0.3);
1826
- border-top: 4px solid var(--primary-color);
1827
- border-radius: 50%;
1828
- width: 50px;
1829
- height: 50px;
1830
- margin: 0 auto;
1831
- animation: spin 1.5s ease-in-out infinite;
1832
- }
1833
-
1834
- .loading-container {
1835
- position: absolute;
1836
- top: 50%;
1837
- left: 50%;
1838
- transform: translate(-50%, -50%);
1839
- text-align: center;
1840
- background: rgba(255, 255, 255, 0.85);
1841
- backdrop-filter: blur(10px);
1842
- padding: 30px;
1843
- border-radius: var(--radius-md);
1844
- box-shadow: var(--shadow-md);
1845
- z-index: 9999;
1846
- }
1847
-
1848
- .loading-text {
1849
- margin-top: 20px;
1850
- font-size: 16px;
1851
- color: var(--text-color);
1852
- font-weight: 500;
1853
- }
1854
-
1855
- /* ํŽ˜์ด์ง€ ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ */
1856
- @keyframes fadeIn {
1857
- from { opacity: 0; }
1858
- to { opacity: 1; }
1859
- }
1860
-
1861
- .fade-in {
1862
- animation: fadeIn 0.5s ease-out;
1863
- }
1864
-
1865
- /* ์ถ”๊ฐ€ ์Šคํƒ€์ผ */
1866
- .section-title {
1867
- font-size: 1.3rem;
1868
- font-weight: 600;
1869
- margin: 30px 0 15px;
1870
- color: var(--text-color);
1871
- }
1872
-
1873
- .no-projects {
1874
- text-align: center;
1875
- margin: 40px 0;
1876
- color: var(--text-color);
1877
- font-size: 16px;
1878
- }
1879
-
1880
- /* ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ” */
1881
- .progress-bar-container {
1882
- width: 100%;
1883
- height: 6px;
1884
- background-color: rgba(0,0,0,0.1);
1885
- border-radius: 3px;
1886
- margin-top: 15px;
1887
- overflow: hidden;
1888
- }
1889
-
1890
- .progress-bar {
1891
- height: 100%;
1892
- background: linear-gradient(to right, var(--primary-color), var(--accent-color));
1893
- border-radius: 3px;
1894
- transition: width 0.3s ease;
1895
- }
1896
-
1897
- /* ํ—ค๋” ๋กœ๊ณ  ๋ฐ ํƒ€์ดํ‹€ */
1898
- .library-header {
1899
- position: fixed;
1900
- top: 12px;
1901
- left: 0;
1902
- right: 0;
1903
- text-align: center;
1904
- z-index: 100;
1905
- pointer-events: none;
1906
- }
1907
-
1908
- .library-header .title {
1909
- display: inline-block;
1910
- padding: 8px 24px; /* ํŒจ๋”ฉ ์ถ•์†Œ */
1911
- background: rgba(255, 255, 255, 0.85);
1912
- backdrop-filter: blur(10px);
1913
- border-radius: 25px; /* ํ…Œ๋‘๋ฆฌ ๋ชจ์„œ๋ฆฌ ์ถ•์†Œ */
1914
- box-shadow: var(--shadow-md);
1915
- font-size: 1.25rem; /* ๊ธ€์ž ํฌ๊ธฐ ์ถ•์†Œ (1.5rem์—์„œ 1.25rem์œผ๋กœ) */
1916
- font-weight: 600;
1917
- background-image: linear-gradient(120deg, #8e74eb 0%, #9d66ff 100%); /* ์ œ๋ชฉ ์ƒ‰์ƒ๋„ ๋ฐ”ํƒ•ํ™”๋ฉด๊ณผ ์–ด์šธ๋ฆฌ๊ฒŒ ๋ณ€๊ฒฝ */
1918
- -webkit-background-clip: text;
1919
- background-clip: text;
1920
- color: transparent;
1921
- pointer-events: all;
1922
- }
1923
-
1924
- /* ์ ์ง„์  ๋กœ๋”ฉ ํ‘œ์‹œ */
1925
- .loading-pages {
1926
- position: absolute;
1927
- bottom: 20px;
1928
- left: 50%;
1929
- transform: translateX(-50%);
1930
- background: rgba(255, 255, 255, 0.9);
1931
- padding: 10px 20px;
1932
- border-radius: 20px;
1933
- box-shadow: var(--shadow-md);
1934
- font-size: 14px;
1935
- color: var(--text-color);
1936
- z-index: 9998;
1937
- text-align: center;
1938
- }
1939
-
1940
- /* ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ์Šคํƒ€์ผ */
1941
- #adminPage {
1942
- color: white;
1943
- max-width: 1400px;
1944
- }
1945
-
1946
- #adminPage h1 {
1947
- font-size: 2rem;
1948
- margin-bottom: 30px;
1949
- text-align: center;
1950
- background-image: linear-gradient(120deg, #e0c3fc 0%, #8ec5fc 100%);
1951
- -webkit-background-clip: text;
1952
- background-clip: text;
1953
- color: transparent;
1954
- }
1955
-
1956
- #adminBackButton {
1957
- position: absolute;
1958
- top: 20px;
1959
- left: 20px;
1960
- background: rgba(255, 255, 255, 0.9);
1961
- backdrop-filter: blur(10px);
1962
- box-shadow: var(--shadow-md);
1963
- border: none;
1964
- border-radius: 30px;
1965
- padding: 8px 20px;
1966
- display: flex;
1967
- align-items: center;
1968
- font-weight: 600;
1969
- font-size: 14px;
1970
- cursor: pointer;
1971
- transition: var(--transition);
1972
- }
1973
-
1974
- #adminBackButton:hover {
1975
- transform: translateY(-3px);
1976
- box-shadow: var(--shadow-lg);
1977
- }
1978
-
1979
- /* ๊ด€๋ฆฌ์ž ๊ทธ๋ฆฌ๋“œ */
1980
- #adminGrid {
1981
- grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
1982
- }
1983
-
1984
- /* AI ์ฑ—๋ด‡ UI ์Šคํƒ€์ผ */
1985
- #aiChatContainer {
1986
- display: none;
1987
- position: fixed;
1988
- top: 0;
1989
- right: 0;
1990
- width: 400px;
1991
- height: 100%;
1992
- background: rgba(255, 255, 255, 0.95);
1993
- backdrop-filter: blur(10px);
1994
- box-shadow: -5px 0 20px rgba(0, 0, 0, 0.1);
1995
- z-index: 9999;
1996
- transition: all 0.3s ease;
1997
- transform: translateX(100%);
1998
- padding: 20px;
1999
- box-sizing: border-box;
2000
- display: flex;
2001
- flex-direction: column;
2002
- }
2003
-
2004
- #aiChatContainer.active {
2005
- transform: translateX(0);
2006
- }
2007
-
2008
- #aiChatHeader {
2009
- display: flex;
2010
- justify-content: space-between;
2011
- align-items: center;
2012
- margin-bottom: 15px;
2013
- padding-bottom: 15px;
2014
- border-bottom: 1px solid rgba(0, 0, 0, 0.1);
2015
- }
2016
-
2017
- #aiChatHeader h3 {
2018
- margin: 0;
2019
- color: #333;
2020
- font-size: 18px;
2021
- display: flex;
2022
- align-items: center;
2023
- }
2024
-
2025
- #aiChatHeader h3 i {
2026
- margin-right: 10px;
2027
- color: var(--ai-color);
2028
- }
2029
-
2030
- #aiChatClose {
2031
- background: none;
2032
- border: none;
2033
- cursor: pointer;
2034
- font-size: 18px;
2035
- color: #666;
2036
- transition: var(--transition);
2037
- }
2038
-
2039
- #aiChatClose:hover {
2040
- color: #333;
2041
- transform: scale(1.1);
2042
- }
2043
-
2044
- #aiChatMessages {
2045
- flex: 1;
2046
- overflow-y: auto;
2047
- padding: 10px 0;
2048
- margin-bottom: 15px;
2049
- }
2050
-
2051
- .chat-message {
2052
- margin-bottom: 15px;
2053
- display: flex;
2054
- align-items: flex-start;
2055
- }
2056
-
2057
- .chat-message.user {
2058
- flex-direction: row-reverse;
2059
- }
2060
-
2061
- .chat-avatar {
2062
- width: 35px;
2063
- height: 35px;
2064
- border-radius: 50%;
2065
- display: flex;
2066
- justify-content: center;
2067
- align-items: center;
2068
- margin-right: 10px;
2069
- flex-shrink: 0;
2070
- }
2071
-
2072
- .chat-message.user .chat-avatar {
2073
- margin-right: 0;
2074
- margin-left: 10px;
2075
- background: var(--primary-color);
2076
- color: white;
2077
- }
2078
-
2079
- .chat-message.ai .chat-avatar {
2080
- background: var(--ai-color);
2081
- color: white;
2082
- }
2083
-
2084
- .chat-content {
2085
- background: #f1f1f1;
2086
- padding: 12px 15px;
2087
- border-radius: 18px;
2088
- max-width: 75%;
2089
- word-break: break-word;
2090
- position: relative;
2091
- font-size: 14px;
2092
- line-height: 1.4;
2093
- }
2094
-
2095
- .chat-message.user .chat-content {
2096
- background: var(--primary-color);
2097
- color: white;
2098
- border-bottom-right-radius: 4px;
2099
- }
2100
-
2101
- .chat-message.ai .chat-content {
2102
- background: #f1f1f1;
2103
- color: #333;
2104
- border-bottom-left-radius: 4px;
2105
- }
2106
-
2107
- #aiChatForm {
2108
- display: flex;
2109
- border-top: 1px solid rgba(0, 0, 0, 0.1);
2110
- padding-top: 15px;
2111
- }
2112
-
2113
- #aiChatInput {
2114
- flex: 1;
2115
- padding: 12px 15px;
2116
- border: 1px solid #ddd;
2117
- border-radius: 25px;
2118
- font-size: 14px;
2119
- outline: none;
2120
- transition: var(--transition);
2121
- }
2122
-
2123
- #aiChatInput:focus {
2124
- border-color: var(--ai-color);
2125
- box-shadow: 0 0 0 2px rgba(134, 232, 171, 0.2);
2126
- }
2127
-
2128
- #aiChatSubmit {
2129
- background: var(--ai-color);
2130
- border: none;
2131
- color: white;
2132
- width: 45px;
2133
- height: 45px;
2134
- border-radius: 50%;
2135
- margin-left: 10px;
2136
- display: flex;
2137
- justify-content: center;
2138
- align-items: center;
2139
- cursor: pointer;
2140
- transition: var(--transition);
2141
- }
2142
-
2143
- #aiChatSubmit:hover {
2144
- background: var(--ai-hover);
2145
- transform: scale(1.05);
2146
- }
2147
-
2148
- #aiChatSubmit:disabled {
2149
- background: #ccc;
2150
- cursor: not-allowed;
2151
- }
2152
-
2153
- .typing-indicator {
2154
- display: flex;
2155
- align-items: center;
2156
- margin-top: 5px;
2157
- font-size: 12px;
2158
- color: #666;
2159
- }
2160
-
2161
- .typing-indicator span {
2162
- height: 8px;
2163
- width: 8px;
2164
- background: var(--ai-color);
2165
- border-radius: 50%;
2166
- display: inline-block;
2167
- margin-right: 3px;
2168
- animation: typing 1s infinite;
2169
- }
2170
-
2171
- .typing-indicator span:nth-child(2) {
2172
- animation-delay: 0.2s;
2173
- }
2174
-
2175
- .typing-indicator span:nth-child(3) {
2176
- animation-delay: 0.4s;
2177
- }
2178
-
2179
- @keyframes typing {
2180
- 0% { transform: translateY(0); }
2181
- 50% { transform: translateY(-5px); }
2182
- 100% { transform: translateY(0); }
2183
- }
2184
-
2185
- .chat-time {
2186
- font-size: 10px;
2187
- color: #999;
2188
- margin-top: 5px;
2189
- text-align: right;
2190
- }
2191
-
2192
- /* ์ฝ”๋“œ ๋ธ”๋ก ์Šคํƒ€์ผ */
2193
- .chat-content pre {
2194
- background: rgba(0, 0, 0, 0.05);
2195
- padding: 10px;
2196
- border-radius: 5px;
2197
- overflow-x: auto;
2198
- font-family: monospace;
2199
- font-size: 12px;
2200
- margin: 10px 0;
2201
- }
2202
-
2203
- /* ๋งˆํฌ๋‹ค์šด ์Šคํƒ€์ผ */
2204
- .chat-content strong {
2205
- font-weight: bold;
2206
- }
2207
-
2208
- .chat-content em {
2209
- font-style: italic;
2210
- }
2211
-
2212
- .chat-content ul, .chat-content ol {
2213
- margin-left: 20px;
2214
- margin-top: 5px;
2215
- margin-bottom: 5px;
2216
- }
2217
-
2218
- /* ๊ณต์œ  ๋ฒ„ํŠผ */
2219
- #shareChat {
2220
- padding: 8px 15px;
2221
- background: #f1f1f1;
2222
- border: none;
2223
- border-radius: 20px;
2224
- font-size: 12px;
2225
- color: #666;
2226
- cursor: pointer;
2227
- margin-top: 5px;
2228
- transition: var(--transition);
2229
- }
2230
-
2231
- #shareChat:hover {
2232
- background: #ddd;
2233
- }
2234
-
2235
- /* ๋ฐ˜์‘ํ˜• ๋””์ž์ธ */
2236
- @media (max-width: 768px) {
2237
- .grid {
2238
- grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
2239
- gap: 16px;
2240
- }
2241
-
2242
- .card {
2243
- height: 240px;
2244
- }
2245
-
2246
- .library-header .title {
2247
- font-size: 1.25rem;
2248
- padding: 10px 20px;
2249
- }
2250
-
2251
- .floating-home, .floating-ai {
2252
- width: 50px;
2253
- height: 50px;
2254
- }
2255
-
2256
- .floating-home .icon, .floating-ai .icon {
2257
- font-size: 18px;
2258
- }
2259
-
2260
- #adminButton {
2261
- padding: 6px 15px;
2262
- font-size: 12px;
2263
- }
2264
-
2265
- #aiChatContainer {
2266
- width: 100%;
2267
- }
2268
- }
2269
- </style>
2270
- </head>
2271
- <body>
2272
- <!-- ์ œ๋ชฉ์„ Home ๋ฒ„ํŠผ๊ณผ ํ•จ๊ป˜ ๋ ˆ์ด์–ด๋กœ ์ฒ˜๋ฆฌ -->
2273
- <div id="homeButton" class="floating-home" style="display:none;">
2274
- <div class="icon"><i class="fas fa-home"></i></div>
2275
- <div class="title">ํ™ˆ์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ</div>
2276
- </div>
2277
-
2278
- <!-- AI ๋ฒ„ํŠผ ์ถ”๊ฐ€ -->
2279
- <div id="aiButton" class="floating-ai" style="display:none;">
2280
- <div class="icon"><i class="fas fa-robot"></i></div>
2281
- <div class="title">AI ์–ด์‹œ์Šคํ„ดํŠธ</div>
2282
- </div>
2283
-
2284
- <!-- AI ์ฑ—๋ด‡ ์ปจํ…Œ์ด๋„ˆ -->
2285
- <div id="aiChatContainer">
2286
- <div id="aiChatHeader">
2287
- <h3><i class="fas fa-robot"></i> AI ์–ด์‹œ์Šคํ„ดํŠธ</h3>
2288
- <button id="aiChatClose"><i class="fas fa-times"></i></button>
2289
- </div>
2290
- <div id="aiChatMessages"></div>
2291
- <form id="aiChatForm">
2292
- <input type="text" id="aiChatInput" placeholder="PDF์— ๋Œ€ํ•ด ์งˆ๋ฌธํ•˜์„ธ์š”..." autocomplete="off">
2293
- <button type="submit" id="aiChatSubmit"><i class="fas fa-paper-plane"></i></button>
2294
- </form>
2295
- </div>
2296
-
2297
- <!-- ๊ด€๋ฆฌ์ž ๋ฒ„ํŠผ -->
2298
- <div id="adminButton">
2299
- <i class="fas fa-cog"></i> Admin
2300
- </div>
2301
-
2302
- <!-- ๊ด€๋ฆฌ์ž ๋กœ๊ทธ์ธ ๋ชจ๋‹ฌ -->
2303
- <div id="adminLoginModal" class="modal">
2304
- <div class="modal-content">
2305
- <h2>๊ด€๋ฆฌ์ž ๋กœ๊ทธ์ธ</h2>
2306
- <input type="password" id="adminPasswordInput" placeholder="๊ด€๋ฆฌ์ž ๋น„๋ฐ€๋ฒˆํ˜ธ">
2307
- <div>
2308
- <button id="adminLoginButton">๋กœ๊ทธ์ธ</button>
2309
- <button id="adminLoginClose">์ทจ์†Œ</button>
2310
- </div>
2311
- </div>
2312
- </div>
2313
-
2314
- <!-- ์„ผํ„ฐ ์ •๋ ฌ๋œ ํƒ€์ดํ‹€ -->
2315
- <div class="library-header">
2316
- <div class="title">AI FlipBook Maker</div>
2317
- </div>
2318
-
2319
- <section id="home" class="fade-in">
2320
- <div class="upload-container">
2321
- <button class="upload" id="pdfUploadBtn">
2322
- <i class="fas fa-file-pdf"></i> PDF Upload
2323
- </button>
2324
- <button class="upload" id="textToAIBookBtn">
2325
- <i class="fas fa-file-alt"></i> Text to AI-Book
2326
- </button>
2327
- <input id="pdfInput" type="file" accept="application/pdf" style="display:none">
2328
- <input id="textInput" type="file" accept=".txt,.docx,.doc" style="display:none">
2329
- </div>
2330
-
2331
- <div class="section-title">Projects</div>
2332
- <div class="grid" id="grid">
2333
- <!-- ์นด๋“œ๊ฐ€ ์—ฌ๊ธฐ์— ๋™์ ์œผ๋กœ ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค -->
2334
- </div>
2335
- <div id="noProjects" class="no-projects" style="display: none;">
2336
- ํ”„๋กœ์ ํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. PDF๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์‹œ์ž‘ํ•˜์„ธ์š”.
2337
- </div>
2338
- </section>
2339
-
2340
- <section id="viewerPage" style="display:none">
2341
- <div id="viewer"></div>
2342
- <div id="loadingPages" class="loading-pages" style="display:none;">ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ค‘... <span id="loadingPagesCount">0/0</span></div>
2343
- </section>
2344
-
2345
- <!-- ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ -->
2346
- <section id="adminPage" style="display:none" class="fade-in">
2347
- <h1>๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€</h1>
2348
- <button id="adminBackButton"><i class="fas fa-arrow-left"></i> ๋’ค๋กœ ๊ฐ€๊ธฐ</button>
2349
-
2350
- <div class="section-title">์ €์žฅ๋œ PDF ๋ชฉ๋ก</div>
2351
- <div class="grid" id="adminGrid">
2352
- <!-- ๊ด€๋ฆฌ์ž PDF ์นด๋“œ๊ฐ€ ์—ฌ๊ธฐ์— ๋™์ ์œผ๋กœ ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค -->
2353
- </div>
2354
- <div id="noAdminProjects" class="no-projects" style="display: none;">
2355
- ์ €์žฅ๋œ PDF๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. PDF๋ฅผ ์—…๋กœ๋“œํ•˜์—ฌ ์‹œ์ž‘ํ•˜์„ธ์š”.
2356
- </div>
2357
- </section>
2358
-
2359
- <script>
2360
- let projects=[], fb=null;
2361
- const grid=document.getElementById('grid'), viewer=document.getElementById('viewer');
2362
- pdfjsLib.GlobalWorkerOptions.workerSrc='/static/pdf.worker.js';
2363
-
2364
- // ์„œ๋ฒ„์—์„œ ๋ฏธ๋ฆฌ ๋กœ๋“œ๋œ PDF ํ”„๋กœ์ ํŠธ
2365
- let serverProjects = [];
2366
-
2367
- // ํ˜„์žฌ ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ƒํƒœ
2368
- let currentLoadingPdfPath = null;
2369
- let pageLoadingInterval = null;
2370
-
2371
- // ํ˜„์žฌ ์—ด๋ฆฐ PDF์˜ ID
2372
- let currentPdfId = null;
2373
-
2374
- // ์˜ค๋””์˜ค ์ปจํ…์ŠคํŠธ์™€ ์ดˆ๊ธฐํ™” ์ƒํƒœ ๊ด€๋ฆฌ
2375
- let audioInitialized = false;
2376
- let audioContext = null;
2377
-
2378
- // AI ์ฑ—๋ด‡ ๊ด€๋ จ ๋ณ€์ˆ˜
2379
- let isAiChatActive = false;
2380
- let isAiProcessing = false;
2381
- let hasLoadedSummary = false;
2382
-
2383
- // ์˜ค๋””์˜ค ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜
2384
- function initializeAudio() {
2385
- if (audioInitialized) return Promise.resolve();
2386
-
2387
- return new Promise((resolve) => {
2388
- // ์˜ค๋””์˜ค ์ปจํ…์ŠคํŠธ ์ƒ์„ฑ (์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ์ด ํ•„์š” ์—†๋Š” ์ดˆ๊ธฐํ™”)
2389
- audioContext = new (window.AudioContext || window.webkitAudioContext)();
2390
-
2391
- // MP3 ๋กœ๋“œ ๋ฐ ์ดˆ๊ธฐํ™”
2392
- const audio = new Audio('/static/turnPage2.mp3');
2393
- audio.volume = 0.01; // ์ตœ์†Œ ๋ณผ๋ฅจ์œผ๋กœ ์„ค์ •
2394
-
2395
- // ๋กœ๋“œ๋œ ์˜ค๋””์˜ค ์žฌ์ƒ ์‹œ๋„ (์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ ์š”๊ตฌ๋  ์ˆ˜ ์žˆ์Œ)
2396
- const playPromise = audio.play();
2397
-
2398
- if (playPromise !== undefined) {
2399
- playPromise
2400
- .then(() => {
2401
- // ์„ฑ๊ณต์ ์œผ๋กœ ์žฌ์ƒ๋จ - ์ฆ‰์‹œ ์ผ์‹œ์ •์ง€
2402
- audio.pause();
2403
- audioInitialized = true;
2404
- console.log('์˜ค๋””์˜ค ์ดˆ๊ธฐํ™” ์„ฑ๊ณต');
2405
- resolve();
2406
- })
2407
- .catch((error) => {
2408
- console.log('์ž๋™ ์˜ค๋””์˜ค ์ดˆ๊ธฐํ™” ์‹คํŒจ, ์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ ํ•„์š”:', error);
2409
-
2410
- // ์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ, ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€
2411
- const initOnUserAction = function() {
2412
- const tempAudio = new Audio('/static/turnPage2.mp3');
2413
- tempAudio.volume = 0.01;
2414
- tempAudio.play()
2415
- .then(() => {
2416
- tempAudio.pause();
2417
- audioInitialized = true;
2418
- console.log('์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ์œผ๋กœ ์˜ค๋””์˜ค ์ดˆ๊ธฐํ™” ์„ฑ๊ณต');
2419
- resolve();
2420
-
2421
- // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ œ๊ฑฐ
2422
- ['click', 'touchstart', 'keydown'].forEach(event => {
2423
- document.removeEventListener(event, initOnUserAction, { capture: true });
2424
- });
2425
- })
2426
- .catch(e => console.error('์˜ค๋””์˜ค ์ดˆ๊ธฐํ™” ์‹คํŒจ:', e));
2427
- };
2428
-
2429
- // ์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ ์ด๋ฒคํŠธ์— ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€
2430
- ['click', 'touchstart', 'keydown'].forEach(event => {
2431
- document.addEventListener(event, initOnUserAction, { once: true, capture: true });
2432
- });
2433
-
2434
- // ํŽ˜์ด์ง€ ๋กœ๋“œ ์งํ›„ ์‚ฌ์šฉ์ž์—๊ฒŒ ์˜ค๋””์˜ค ํ™œ์„ฑํ™” ์š”์ฒญ
2435
- if (window.location.pathname.startsWith('/view/')) {
2436
- // ์˜ค๋””์˜ค ํ™œ์„ฑํ™” ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ (๋ฐ”๋กœ๊ฐ€๊ธฐ ๋งํฌ๋กœ ์ ‘์†ํ•œ ๊ฒฝ์šฐ)
2437
- setTimeout(() => {
2438
- const audioPrompt = document.createElement('div');
2439
- audioPrompt.style.position = 'fixed';
2440
- audioPrompt.style.bottom = '80px';
2441
- audioPrompt.style.left = '50%';
2442
- audioPrompt.style.transform = 'translateX(-50%)';
2443
- audioPrompt.style.backgroundColor = 'rgba(0,0,0,0.7)';
2444
- audioPrompt.style.color = 'white';
2445
- audioPrompt.style.padding = '10px 20px';
2446
- audioPrompt.style.borderRadius = '20px';
2447
- audioPrompt.style.zIndex = '10000';
2448
- audioPrompt.style.cursor = 'pointer';
2449
- audioPrompt.innerHTML = 'ํŽ˜์ด์ง€ ์–ด๋””๋“  ํด๋ฆญํ•˜์—ฌ ์†Œ๋ฆฌ ํšจ๊ณผ ํ™œ์„ฑํ™” <i class="fas fa-volume-up"></i>';
2450
- audioPrompt.id = 'audioPrompt';
2451
-
2452
- // ํด๋ฆญ ์‹œ ์†Œ๋ฆฌ ํ™œ์„ฑํ™” ๋ฐ ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ
2453
- audioPrompt.addEventListener('click', function() {
2454
- initOnUserAction();
2455
- audioPrompt.remove();
2456
- });
2457
-
2458
- document.body.appendChild(audioPrompt);
2459
-
2460
- // 10์ดˆ ํ›„ ์ž๋™์œผ๋กœ ์ˆจ๊น€
2461
- setTimeout(() => {
2462
- if (document.getElementById('audioPrompt')) {
2463
- document.getElementById('audioPrompt').remove();
2464
- }
2465
- }, 10000);
2466
- }, 2000);
2467
- }
2468
- });
2469
- } else {
2470
- // ๋ธŒ๋ผ์šฐ์ €๊ฐ€ Promise ๊ธฐ๋ฐ˜ ์žฌ์ƒ์„ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ
2471
- audioInitialized = true;
2472
- resolve();
2473
- }
2474
- });
2475
- }
2476
-
2477
- /* โ”€โ”€ ์œ ํ‹ธ โ”€โ”€ */
2478
- function $id(id){return document.getElementById(id)}
2479
-
2480
- // ํ˜„์žฌ ์‹œ๊ฐ„์„ ํฌ๋งทํŒ…ํ•˜๋Š” ํ•จ์ˆ˜
2481
- function formatTime() {
2482
- const now = new Date();
2483
- const hours = now.getHours().toString().padStart(2, '0');
2484
- const minutes = now.getMinutes().toString().padStart(2, '0');
2485
- return `${hours}:${minutes}`;
2486
- }
2487
-
2488
- // AI ์ฑ—๋ด‡ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ ํ•จ์ˆ˜
2489
- function addChatMessage(content, isUser = false) {
2490
- const messagesContainer = $id('aiChatMessages');
2491
- const messageElement = document.createElement('div');
2492
- messageElement.className = `chat-message ${isUser ? 'user' : 'ai'}`;
2493
-
2494
- const currentTime = formatTime();
2495
-
2496
- messageElement.innerHTML = `
2497
- <div class="chat-avatar">
2498
- <i class="fas ${isUser ? 'fa-user' : 'fa-robot'}"></i>
2499
- </div>
2500
- <div class="chat-bubble">
2501
- <div class="chat-content">${content}</div>
2502
- <div class="chat-time">${currentTime}</div>
2503
- </div>
2504
- `;
2505
-
2506
- messagesContainer.appendChild(messageElement);
2507
- messagesContainer.scrollTop = messagesContainer.scrollHeight;
2508
- return messageElement;
2509
- }
2510
-
2511
- // ๋กœ๋”ฉ ํ‘œ์‹œ๊ธฐ ์ถ”๊ฐ€ ํ•จ์ˆ˜
2512
- function addTypingIndicator() {
2513
- const messagesContainer = $id('aiChatMessages');
2514
- const indicatorElement = document.createElement('div');
2515
- indicatorElement.className = 'typing-indicator';
2516
- indicatorElement.innerHTML = `
2517
- <div class="chat-avatar">
2518
- <i class="fas fa-robot"></i>
2519
- </div>
2520
- <div>
2521
- <span></span>
2522
- <span></span>
2523
- <span></span>
2524
- </div>
2525
- `;
2526
- messagesContainer.appendChild(indicatorElement);
2527
- messagesContainer.scrollTop = messagesContainer.scrollHeight;
2528
- return indicatorElement;
2529
- }
2530
-
2531
- // AI ์ฑ—๋ด‡ ํ† ๊ธ€ ํ•จ์ˆ˜
2532
- function toggleAiChat(show = true) {
2533
- const aiChatContainer = $id('aiChatContainer');
2534
-
2535
- if (show) {
2536
- // ์ฑ—๋ด‡ ํ‘œ์‹œ
2537
- aiChatContainer.style.display = 'flex';
2538
- setTimeout(() => {
2539
- aiChatContainer.classList.add('active');
2540
- }, 10);
2541
- isAiChatActive = true;
2542
-
2543
- // ์ฒ˜์Œ ์—ด ๋•Œ ์ž๋™ ์š”์•ฝ ๋กœ๋“œ
2544
- if (!hasLoadedSummary && currentPdfId) {
2545
- loadPdfSummary();
2546
- }
2547
- } else {
2548
- // ์ฑ—๋ด‡ ์ˆจ๊ธฐ๊ธฐ
2549
- aiChatContainer.classList.remove('active');
2550
- setTimeout(() => {
2551
- aiChatContainer.style.display = 'none';
2552
- }, 300);
2553
- isAiChatActive = false;
2554
- }
2555
- }
2556
-
2557
- // PDF ์š”์•ฝ ๋กœ๋“œ ํ•จ์ˆ˜
2558
- // PDF ์š”์•ฝ ๋กœ๋“œ ํ•จ์ˆ˜
2559
- async function loadPdfSummary() {
2560
- if (!currentPdfId || isAiProcessing || hasLoadedSummary) return;
2561
-
2562
- try {
2563
- isAiProcessing = true;
2564
- const typingIndicator = addTypingIndicator();
2565
-
2566
- // ์„œ๋ฒ„์— ์š”์•ฝ ์š”์ฒญ
2567
- const response = await fetch(`/api/ai/summarize-pdf/${currentPdfId}`);
2568
- const data = await response.json();
2569
-
2570
- // ๋กœ๋”ฉ ํ‘œ์‹œ๊ธฐ ์ œ๊ฑฐ
2571
- typingIndicator.remove();
2572
-
2573
- if (data.error) {
2574
- // ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ
2575
- addChatMessage(`PDF ์š”์•ฝ์„ ์ƒ์„ฑํ•˜๋Š” ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: ${data.error}<br><br>๊ณ„์† ์งˆ๋ฌธ์„ ์ž…๋ ฅํ•˜์‹œ๋ฉด PDF ๋‚ด์šฉ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๋‹ต๋ณ€์„ ์‹œ๋„ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.`);
2576
-
2577
- // ์š”์•ฝ์ด ์‹คํŒจํ•ด๋„ ํŠน์ • ๊ฒฝ์šฐ์—๋Š” ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆฌ๊ณ  ๊ณ„์† ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋„๋ก ์„ค์ •
2578
- if (data.summary) {
2579
- addChatMessage(`<strong>PDF์—์„œ ์ถ”์ถœํ•œ ์ •๋ณด:</strong><br>${data.summary}`);
2580
- hasLoadedSummary = true;
2581
- }
2582
- } else {
2583
- // ํ™˜์˜ ๋ฉ”์‹œ์ง€์™€ ์š”์•ฝ ์ถ”๊ฐ€
2584
- addChatMessage(`์•ˆ๋…•ํ•˜์„ธ์š”! ์ด PDF์— ๋Œ€ํ•ด ์–ด๋–ค ๊ฒƒ์ด๋“  ์งˆ๋ฌธํ•ด์ฃผ์„ธ์š”. ์ œ๊ฐ€ ๋„์™€๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.<br><br><strong>PDF ์š”์•ฝ:</strong><br>${data.summary}`);
2585
- hasLoadedSummary = true;
2586
- }
2587
- } catch (error) {
2588
- console.error("PDF ์š”์•ฝ ๋กœ๋“œ ์˜ค๋ฅ˜:", error);
2589
- addChatMessage(`PDF ์š”์•ฝ์„ ๋กœ๋“œํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„ ์—ฐ๊ฒฐ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.<br><br>์–ด๋–ค ์งˆ๋ฌธ์ด๋“  ์ž…๋ ฅํ•˜์‹œ๋ฉด ์ตœ์„ ์„ ๋‹คํ•ด ๋‹ต๋ณ€ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.`);
2590
- } finally {
2591
- isAiProcessing = false;
2592
- }
2593
- }
2594
-
2595
- // ์งˆ๋ฌธ ์ œ์ถœ ํ•จ์ˆ˜
2596
- async function submitQuestion(question) {
2597
- if (!currentPdfId || isAiProcessing || !question.trim()) return;
2598
-
2599
- try {
2600
- isAiProcessing = true;
2601
- $id('aiChatSubmit').disabled = true;
2602
-
2603
- // ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
2604
- addChatMessage(question, true);
2605
-
2606
- // ๋กœ๋”ฉ ํ‘œ์‹œ๊ธฐ ์ถ”๊ฐ€
2607
- const typingIndicator = addTypingIndicator();
2608
-
2609
- // ์„œ๋ฒ„์— ์งˆ์˜ ์š”์ฒญ
2610
- const response = await fetch(`/api/ai/query-pdf/${currentPdfId}`, {
2611
- method: 'POST',
2612
- headers: {
2613
- 'Content-Type': 'application/json'
2614
- },
2615
- body: JSON.stringify({ query: question }),
2616
- // ํƒ€์ž„์•„์›ƒ ์„ค์ • ์ถ”๊ฐ€
2617
- signal: AbortSignal.timeout(60000) // 60์ดˆ ํƒ€์ž„์•„์›ƒ
2618
- });
2619
-
2620
- const data = await response.json();
2621
-
2622
- // ๋กœ๋”ฉ ํ‘œ์‹œ๊ธฐ ์ œ๊ฑฐ
2623
- typingIndicator.remove();
2624
-
2625
- if (data.error) {
2626
- // ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ์นœ์ ˆํ•œ ์•ˆ๋‚ด ์ œ๊ณต
2627
- if (data.error.includes("API ํ‚ค")) {
2628
- addChatMessage("์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ AI ์„œ๋น„์Šค์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์‹œ์Šคํ…œ ๊ด€๋ฆฌ์ž์—๊ฒŒ API ํ‚ค ์„ค์ •์„ ํ™•์ธํ•ด๋‹ฌ๋ผ๊ณ  ์š”์ฒญํ•ด์ฃผ์„ธ์š”.");
2629
- } else if (data.error.includes("์—ฐ๊ฒฐ")) {
2630
- addChatMessage("์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. AI ์„œ๋น„์Šค์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ์„ ํ™•์ธํ•˜๊ฑฐ๋‚˜ ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.");
2631
- } else {
2632
- addChatMessage(`์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์งˆ๋ฌธ์— ๋‹ต๋ณ€ํ•˜๋Š” ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: ${data.error}`);
2633
- }
2634
- } else {
2635
- // AI ์‘๋‹ต ์ถ”๊ฐ€
2636
- addChatMessage(data.answer);
2637
- }
2638
- } catch (error) {
2639
- console.error("์งˆ๋ฌธ ์ œ์ถœ ์˜ค๋ฅ˜:", error);
2640
- if (error.name === 'AbortError') {
2641
- addChatMessage("์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์‘๋‹ต ์‹œ๊ฐ„์ด ๋„ˆ๋ฌด ์˜ค๋ž˜ ๊ฑธ๋ ค ์š”์ฒญ์ด ์ทจ์†Œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ์„ ํ™•์ธํ•˜๊ฑฐ๋‚˜ ๋” ์งง์€ ์งˆ๋ฌธ์œผ๋กœ ๋‹ค์‹œ ์‹œ๋„ํ•ด๋ณด์„ธ์š”.");
2642
- } else {
2643
- addChatMessage("์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์„œ๋ฒ„์™€ ํ†ต์‹  ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.");
2644
- }
2645
- } finally {
2646
- isAiProcessing = false;
2647
- $id('aiChatSubmit').disabled = false;
2648
- $id('aiChatInput').value = '';
2649
- $id('aiChatInput').focus();
2650
- }
2651
- }
2652
-
2653
-
2654
- // DOM์ด ๋กœ๋“œ๋˜๋ฉด ์‹คํ–‰
2655
- document.addEventListener('DOMContentLoaded', function() {
2656
- console.log("DOM ๋กœ๋“œ ์™„๋ฃŒ, ์ด๋ฒคํŠธ ์„ค์ • ์‹œ์ž‘");
2657
-
2658
- // ์˜ค๋””์˜ค ์ดˆ๊ธฐํ™” ์‹œ๋„
2659
- initializeAudio().catch(e => console.warn('์˜ค๋””์˜ค ์ดˆ๊ธฐํ™” ์‹คํŒจ:', e));
2660
-
2661
- // PDF ์—…๋กœ๋“œ ๋ฒ„ํŠผ
2662
- const pdfBtn = document.getElementById('pdfUploadBtn');
2663
- const pdfInput = document.getElementById('pdfInput');
2664
-
2665
- if (pdfBtn && pdfInput) {
2666
- console.log("PDF ์—…๋กœ๋“œ ๋ฒ„ํŠผ ์ฐพ์Œ");
2667
-
2668
- // ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ํŒŒ์ผ ์ž…๋ ฅ ํŠธ๋ฆฌ๊ฑฐ
2669
- pdfBtn.addEventListener('click', function() {
2670
- console.log("PDF ๋ฒ„ํŠผ ํด๋ฆญ๋จ");
2671
- pdfInput.click();
2672
- });
2673
-
2674
- // ํŒŒ์ผ ์„ ํƒ ์‹œ ์ฒ˜๋ฆฌ
2675
- pdfInput.addEventListener('change', function(e) {
2676
- console.log("PDF ํŒŒ์ผ ์„ ํƒ๋จ:", e.target.files.length);
2677
- const file = e.target.files[0];
2678
- if (!file) return;
2679
-
2680
- // ์„œ๋ฒ„์— PDF ์—…๋กœ๋“œ (์˜๊ตฌ ์ €์žฅ์†Œ์— ์ €์žฅ)
2681
- uploadPdfToServer(file);
2682
- });
2683
- } else {
2684
- console.error("PDF ์—…๋กœ๋“œ ์š”์†Œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ");
2685
- }
2686
-
2687
- // ํ…์ŠคํŠธ ์—…๋กœ๋“œ ๋ฒ„ํŠผ
2688
- const textBtn = document.getElementById('textToAIBookBtn');
2689
- const textInput = document.getElementById('textInput');
2690
-
2691
- if (textBtn && textInput) {
2692
- // ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ํŒŒ์ผ ์ž…๋ ฅ ํŠธ๋ฆฌ๊ฑฐ
2693
- textBtn.addEventListener('click', function() {
2694
- textInput.click();
2695
- });
2696
-
2697
- // ํŒŒ์ผ ์„ ํƒ ์‹œ ์ฒ˜๋ฆฌ
2698
- textInput.addEventListener('change', function(e) {
2699
- const file = e.target.files[0];
2700
- if (!file) return;
2701
-
2702
- // ์„œ๋ฒ„์— ํ…์ŠคํŠธ ํŒŒ์ผ ์—…๋กœ๋“œ (์˜๊ตฌ ์ €์žฅ์†Œ์— PDF๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ €์žฅ)
2703
- uploadTextToServer(file);
2704
- });
2705
- }
2706
-
2707
- // ์„œ๋ฒ„ PDF ๋กœ๋“œ ๋ฐ ์บ์‹œ ์ƒํƒœ ํ™•์ธ
2708
- loadServerPDFs();
2709
-
2710
- // ์บ์‹œ ์ƒํƒœ๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ํ™•์ธ
2711
- setInterval(checkCacheStatus, 3000);
2712
-
2713
- // ๊ด€๋ฆฌ์ž ๋ฒ„ํŠผ ์ด๋ฒคํŠธ ์„ค์ •
2714
- setupAdminFunctions();
2715
-
2716
- // ํ™ˆ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ ์„ค์ •
2717
- const homeButton = document.getElementById('homeButton');
2718
- if (homeButton) {
2719
- homeButton.addEventListener('click', function() {
2720
- if(fb) {
2721
- fb.destroy();
2722
- viewer.innerHTML = '';
2723
- fb = null;
2724
- }
2725
- toggle(true);
2726
-
2727
- // ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ ์ •๋ฆฌ
2728
- if (pageLoadingInterval) {
2729
- clearInterval(pageLoadingInterval);
2730
- pageLoadingInterval = null;
2731
- }
2732
- $id('loadingPages').style.display = 'none';
2733
- currentLoadingPdfPath = null;
2734
- currentPdfId = null;
2735
-
2736
- // AI ์ฑ—๋ด‡ ๋‹ซ๊ธฐ
2737
- toggleAiChat(false);
2738
- hasLoadedSummary = false; // ์š”์•ฝ ๋กœ๋“œ ์ƒํƒœ ์ดˆ๊ธฐํ™”
2739
- });
2740
- }
2741
-
2742
- // AI ๋ฒ„ํŠผ ์ด๋ฒคํŠธ ์„ค์ •
2743
- const aiButton = document.getElementById('aiButton');
2744
- if (aiButton) {
2745
- aiButton.addEventListener('click', function() {
2746
- toggleAiChat(!isAiChatActive);
2747
- });
2748
- }
2749
-
2750
- // AI ์ฑ—๋ด‡ ๋‹ซ๊ธฐ ๋ฒ„ํŠผ
2751
- const aiChatClose = document.getElementById('aiChatClose');
2752
- if (aiChatClose) {
2753
- aiChatClose.addEventListener('click', function() {
2754
- toggleAiChat(false);
2755
- });
2756
- }
2757
-
2758
- // AI ์ฑ—๋ด‡ ํผ ์ œ์ถœ
2759
- const aiChatForm = document.getElementById('aiChatForm');
2760
- if (aiChatForm) {
2761
- aiChatForm.addEventListener('submit', function(e) {
2762
- e.preventDefault();
2763
- const inputField = document.getElementById('aiChatInput');
2764
- const question = inputField.value.trim();
2765
-
2766
- if (question && !isAiProcessing) {
2767
- submitQuestion(question);
2768
- }
2769
- });
2770
- }
2771
- });
2772
-
2773
- // ์„œ๋ฒ„์— PDF ์—…๋กœ๋“œ ํ•จ์ˆ˜
2774
- async function uploadPdfToServer(file) {
2775
- try {
2776
- showLoading("PDF ์—…๋กœ๋“œ ์ค‘...");
2777
-
2778
- const formData = new FormData();
2779
- formData.append('file', file);
2780
-
2781
- const response = await fetch('/api/upload-pdf', {
2782
- method: 'POST',
2783
- body: formData
2784
- });
2785
-
2786
- const result = await response.json();
2787
-
2788
- if (result.success) {
2789
- hideLoading();
2790
-
2791
- // ์—…๋กœ๋“œ ์„ฑ๊ณต ์‹œ ์„œ๋ฒ„ PDF ๋ฆฌ์ŠคํŠธ ๋ฆฌ๋กœ๋“œ
2792
- await loadServerPDFs();
2793
-
2794
- // ์„ฑ๊ณต ๋ฉ”์‹œ์ง€
2795
- showMessage("PDF๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ๊ณต์œ  URL: " + result.viewUrl);
2796
- } else {
2797
- hideLoading();
2798
- showError("์—…๋กœ๋“œ ์‹คํŒจ: " + (result.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"));
2799
- }
2800
- } catch (error) {
2801
- console.error("PDF ์—…๋กœ๋“œ ์˜ค๋ฅ˜:", error);
2802
- hideLoading();
2803
- showError("PDF ์—…๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
2804
- }
2805
- }
2806
-
2807
- // ์„œ๋ฒ„์— ํ…์ŠคํŠธ ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜์—ฌ PDF๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜
2808
- async function uploadTextToServer(file) {
2809
- try {
2810
- showLoading("ํ…์ŠคํŠธ ๋ถ„์„ ๋ฐ PDF ๋ณ€ํ™˜ ์ค‘...");
2811
-
2812
- const formData = new FormData();
2813
- formData.append('file', file);
2814
-
2815
- const response = await fetch('/api/text-to-pdf', {
2816
- method: 'POST',
2817
- body: formData
2818
- });
2819
-
2820
- const result = await response.json();
2821
-
2822
- if (result.success) {
2823
- hideLoading();
2824
-
2825
- // ์—…๋กœ๋“œ ์„ฑ๊ณต ์‹œ ์„œ๋ฒ„ PDF ๋ฆฌ์ŠคํŠธ ๋ฆฌ๋กœ๋“œ
2826
- await loadServerPDFs();
2827
-
2828
- // ์„ฑ๊ณต ๋ฉ”์‹œ์ง€
2829
- showMessage("ํ…์ŠคํŠธ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ PDF๋กœ ๋ณ€ํ™˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ๊ณต์œ  URL: " + result.viewUrl);
2830
- } else {
2831
- hideLoading();
2832
- showError("๋ณ€ํ™˜ ์‹คํŒจ: " + (result.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"));
2833
- }
2834
- } catch (error) {
2835
- console.error("ํ…์ŠคํŠธ ๋ณ€ํ™˜ ์˜ค๋ฅ˜:", error);
2836
- hideLoading();
2837
- showError("ํ…์ŠคํŠธ๋ฅผ PDF๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
2838
- }
2839
- }
2840
-
2841
- function addCard(i, thumb, title, isCached = false, pdfId = null) {
2842
- const d = document.createElement('div');
2843
- d.className = 'card fade-in';
2844
- d.onclick = () => open(i);
2845
-
2846
- // PDF ID๊ฐ€ ์žˆ์œผ๋ฉด ๋ฐ์ดํ„ฐ ์†์„ฑ์œผ๋กœ ์ €์žฅ
2847
- if (pdfId) {
2848
- d.dataset.pdfId = pdfId;
2849
- }
2850
-
2851
- // ์ œ๋ชฉ ์ฒ˜๋ฆฌ
2852
- const displayTitle = title ?
2853
- (title.length > 15 ? title.substring(0, 15) + '...' : title) :
2854
- 'ํ”„๋กœ์ ํŠธ ' + (i+1);
2855
-
2856
- // ์บ์‹œ ์ƒํƒœ ๋ฑƒ์ง€ ์ถ”๊ฐ€
2857
- const cachedBadge = isCached ?
2858
- '<div class="cached-status">์บ์‹œ๋จ</div>' : '';
2859
-
2860
- // ๋ฐ”๋กœ๊ฐ€๊ธฐ ๋งํฌ ์ถ”๊ฐ€ (PDF ID๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ์—๋งŒ)
2861
- const linkHtml = pdfId ?
2862
- `<div style="position: absolute; bottom: 55px; left: 50%; transform: translateX(-50%); z-index:5;">
2863
- <a href="/view/${pdfId}" target="_blank" style="color:#4a6ee0; font-size:11px;">Share Link</a>
2864
- </div>` : '';
2865
-
2866
- d.innerHTML = `
2867
- <div class="card-inner">
2868
- ${cachedBadge}
2869
- <img src="${thumb}" alt="${displayTitle}" loading="lazy">
2870
- ${linkHtml}
2871
- <p title="${title || 'ํ”„๋กœ์ ํŠธ ' + (i+1)}">${displayTitle}</p>
2872
- </div>
2873
- `;
2874
- grid.appendChild(d);
2875
-
2876
- // ํ”„๋กœ์ ํŠธ๊ฐ€ ์žˆ์œผ๋ฉด 'ํ”„๋กœ์ ํŠธ ์—†์Œ' ๋ฉ”์‹œ์ง€ ์ˆจ๊ธฐ๊ธฐ
2877
- $id('noProjects').style.display = 'none';
2878
- }
2879
-
2880
- /* โ”€โ”€ ํ”„๋กœ์ ํŠธ ์ €์žฅ โ”€โ”€ */
2881
- function save(pages, title, isCached = false, pdfId = null) {
2882
- const id = projects.push(pages) - 1;
2883
- addCard(id, pages[0].thumb, title, isCached, pdfId);
2884
- }
2885
-
2886
- /* โ”€โ”€ ์„œ๋ฒ„ PDF ๋กœ๋“œ ๋ฐ ์บ์‹œ ์ƒํƒœ ํ™•์ธ โ”€โ”€ */
2887
- async function loadServerPDFs() {
2888
- try {
2889
- // ๋กœ๋”ฉ ํ‘œ์‹œ ์ถ”๊ฐ€
2890
- if (document.querySelectorAll('.card').length === 0) {
2891
- showLoading("๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋กœ๋”ฉ ์ค‘...");
2892
- }
2893
-
2894
- // ๋จผ์ € ์บ์‹œ ์ƒํƒœ ํ™•์ธ
2895
- const cacheStatusRes = await fetch('/api/cache-status');
2896
- const cacheStatus = await cacheStatusRes.json();
2897
-
2898
- // PDF ํ”„๋กœ์ ํŠธ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
2899
- const response = await fetch('/api/pdf-projects');
2900
- serverProjects = await response.json();
2901
-
2902
- // ๊ธฐ์กด ๊ทธ๋ฆฌ๋“œ ์ดˆ๊ธฐํ™”
2903
- grid.innerHTML = '';
2904
- projects = [];
2905
-
2906
- if (serverProjects.length === 0) {
2907
- hideLoading();
2908
- $id('noProjects').style.display = 'block';
2909
- return;
2910
- }
2911
-
2912
- // ์„œ๋ฒ„ PDF ๋กœ๋“œ ๋ฐ ์ธ๋„ค์ผ ์ƒ์„ฑ (๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๋กœ ์ตœ์ ํ™”)
2913
- const thumbnailPromises = serverProjects.map(async (project, index) => {
2914
- updateLoading(`PDF ํ”„๋กœ์ ํŠธ ๋กœ๋”ฉ ์ค‘... (${index+1}/${serverProjects.length})`);
2915
-
2916
- const pdfName = project.name;
2917
- const isCached = cacheStatus[pdfName] && cacheStatus[pdfName].status === "completed";
2918
-
2919
- try {
2920
- // ์ธ๋„ค์ผ ๊ฐ€์ ธ์˜ค๊ธฐ
2921
- const response = await fetch(`/api/pdf-thumbnail?path=${encodeURIComponent(project.path)}`);
2922
- const data = await response.json();
2923
-
2924
- if(data.thumbnail) {
2925
- const pages = [{
2926
- src: data.thumbnail,
2927
- thumb: data.thumbnail,
2928
- path: project.path,
2929
- cached: isCached
2930
- }];
2931
-
2932
- return {
2933
- pages,
2934
- name: project.name,
2935
- isCached,
2936
- id: project.id
2937
- };
2938
- }
2939
- } catch (err) {
2940
- console.error(`์ธ๋„ค์ผ ๋กœ๋“œ ์˜ค๋ฅ˜ (${project.name}):`, err);
2941
- }
2942
-
2943
- return null;
2944
- });
2945
-
2946
- // ๋ชจ๋“  ์ธ๋„ค์ผ ์š”์ฒญ ๊ธฐ๋‹ค๋ฆฌ๊ธฐ
2947
- const results = await Promise.all(thumbnailPromises);
2948
-
2949
- // ์„ฑ๊ณต์ ์œผ๋กœ ๊ฐ€์ ธ์˜จ ๊ฒฐ๊ณผ๋งŒ ํ‘œ์‹œ
2950
- results.filter(result => result !== null).forEach(result => {
2951
- save(result.pages, result.name, result.isCached, result.id);
2952
- });
2953
-
2954
- hideLoading();
2955
-
2956
- // ํ”„๋กœ์ ํŠธ๊ฐ€ ์—†์„ ๊ฒฝ์šฐ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ
2957
- if (document.querySelectorAll('.card').length === 0) {
2958
- $id('noProjects').style.display = 'block';
2959
- }
2960
- } catch(error) {
2961
- console.error('์„œ๋ฒ„ PDF ๋กœ๋“œ ์‹คํŒจ:', error);
2962
- hideLoading();
2963
- showError("๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋กœ๋”ฉ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
2964
- }
2965
- }
2966
-
2967
- /* โ”€โ”€ ์บ์‹œ ์ƒํƒœ ์ •๊ธฐ์ ์œผ๋กœ ํ™•์ธ โ”€โ”€ */
2968
- async function checkCacheStatus() {
2969
- try {
2970
- const response = await fetch('/api/cache-status');
2971
- const cacheStatus = await response.json();
2972
-
2973
- // ํ˜„์žฌ ์นด๋“œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
2974
- const cards = document.querySelectorAll('.card');
2975
-
2976
- for(let i = 0; i < cards.length; i++) {
2977
- if(projects[i] && projects[i][0] && projects[i][0].path) {
2978
- const pdfPath = projects[i][0].path;
2979
- const pdfName = pdfPath.split('/').pop().replace('.pdf', '');
2980
-
2981
- // ์บ์‹œ ์ƒํƒœ ๋ฑƒ์ง€ ์—…๋ฐ์ดํŠธ
2982
- let badgeEl = cards[i].querySelector('.cached-status');
2983
-
2984
- if(cacheStatus[pdfName] && cacheStatus[pdfName].status === "completed") {
2985
- if(!badgeEl) {
2986
- badgeEl = document.createElement('div');
2987
- badgeEl.className = 'cached-status';
2988
- badgeEl.textContent = '์บ์‹œ๋จ';
2989
- cards[i].querySelector('.card-inner')?.appendChild(badgeEl);
2990
- } else if (badgeEl.textContent !== '์บ์‹œ๋จ') {
2991
- badgeEl.textContent = '์บ์‹œ๋จ';
2992
- badgeEl.style.background = 'var(--accent-color)';
2993
- }
2994
- projects[i][0].cached = true;
2995
- } else if(cacheStatus[pdfName] && cacheStatus[pdfName].status === "processing") {
2996
- if(!badgeEl) {
2997
- badgeEl = document.createElement('div');
2998
- badgeEl.className = 'cached-status';
2999
- cards[i].querySelector('.card-inner')?.appendChild(badgeEl);
3000
- }
3001
- badgeEl.textContent = `${cacheStatus[pdfName].progress}%`;
3002
- badgeEl.style.background = 'var(--secondary-color)';
3003
- }
3004
- }
3005
- }
3006
-
3007
- // ํ˜„์žฌ ๋กœ๋”ฉ ์ค‘์ธ PDF๊ฐ€ ์žˆ์œผ๋ฉด ์ƒํƒœ ํ™•์ธ
3008
- if (currentLoadingPdfPath && pageLoadingInterval) {
3009
- const pdfName = currentLoadingPdfPath.split('/').pop().replace('.pdf', '');
3010
-
3011
- if (cacheStatus[pdfName]) {
3012
- const status = cacheStatus[pdfName].status;
3013
- const progress = cacheStatus[pdfName].progress || 0;
3014
-
3015
- if (status === "completed") {
3016
- // ์บ์‹ฑ ์™„๋ฃŒ ์‹œ
3017
- clearInterval(pageLoadingInterval);
3018
- $id('loadingPages').style.display = 'none';
3019
- currentLoadingPdfPath = null;
3020
-
3021
- // ์™„๋ฃŒ๋œ ์บ์‹œ๋กœ ํ”Œ๋ฆฝ๋ถ ๋‹ค์‹œ ๋กœ๋“œ
3022
- refreshFlipBook();
3023
- } else if (status === "processing") {
3024
- // ์ง„ํ–‰ ์ค‘์ผ ๋•Œ ํ‘œ์‹œ ์—…๋ฐ์ดํŠธ
3025
- $id('loadingPages').style.display = 'block';
3026
- $id('loadingPagesCount').textContent = `${progress}%`;
3027
- }
3028
- }
3029
- }
3030
-
3031
- } catch(error) {
3032
- console.error('์บ์‹œ ์ƒํƒœ ํ™•์ธ ์˜ค๋ฅ˜:', error);
3033
- }
3034
- }
3035
-
3036
- /* โ”€โ”€ PDF ID๋กœ PDF ์—ด๏ฟฝ๏ฟฝ โ”€โ”€ */
3037
- async function openPdfById(pdfId, pdfPath, isCached = false) {
3038
- try {
3039
- // ์˜ค๋””์˜ค ์ดˆ๊ธฐํ™” ์‹œ๋„
3040
- await initializeAudio().catch(e => console.warn('PDF ์—ด๊ธฐ ์ „ ์˜ค๋””์˜ค ์ดˆ๊ธฐํ™” ์‹คํŒจ:', e));
3041
-
3042
- // ๋จผ์ € ํ™ˆ ํ™”๋ฉด์—์„œ ์นด๋“œ๋ฅผ ์ฐพ์•„์„œ ํด๋ฆญํ•˜๋Š” ๋ฐฉ๋ฒ• ์‹œ๋„
3043
- let foundCard = false;
3044
- const cards = document.querySelectorAll('.card');
3045
-
3046
- for (let i = 0; i < cards.length; i++) {
3047
- if (cards[i].dataset.pdfId === pdfId) {
3048
- cards[i].click();
3049
- foundCard = true;
3050
- break;
3051
- }
3052
- }
3053
-
3054
- // ์นด๋“œ๋ฅผ ์ฐพ์ง€ ๋ชปํ•œ ๊ฒฝ์šฐ ์ง์ ‘ ์˜คํ”ˆ
3055
- if (!foundCard) {
3056
- toggle(false);
3057
- showLoading("PDF ์ค€๋น„ ์ค‘...");
3058
-
3059
- let pages = [];
3060
-
3061
- // ์ด๋ฏธ ์บ์‹œ๋œ ๊ฒฝ์šฐ ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ
3062
- if (isCached) {
3063
- try {
3064
- const response = await fetch(`/api/cached-pdf?path=${encodeURIComponent(pdfPath)}`);
3065
- const cachedData = await response.json();
3066
-
3067
- if (cachedData.status === "completed" && cachedData.pages) {
3068
- hideLoading();
3069
- createFlipBook(cachedData.pages);
3070
- // ํ˜„์žฌ ์—ด๋ฆฐ PDF์˜ ID ์ €์žฅ
3071
- currentPdfId = pdfId;
3072
- // AI ๋ฒ„ํŠผ ํ‘œ์‹œ
3073
- $id('aiButton').style.display = 'block';
3074
- return;
3075
- }
3076
- } catch (error) {
3077
- console.error("์บ์‹œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ:", error);
3078
- }
3079
- }
3080
-
3081
- // ์ธ๋„ค์ผ ๊ฐ€์ ธ์˜ค๊ธฐ
3082
- try {
3083
- const thumbResponse = await fetch(`/api/pdf-thumbnail?path=${encodeURIComponent(pdfPath)}`);
3084
- const thumbData = await thumbResponse.json();
3085
-
3086
- if (thumbData.thumbnail) {
3087
- pages = [{
3088
- src: thumbData.thumbnail,
3089
- thumb: thumbData.thumbnail,
3090
- path: pdfPath,
3091
- cached: isCached
3092
- }];
3093
- }
3094
- } catch (error) {
3095
- console.error("์ธ๋„ค์ผ ๋กœ๋“œ ์‹คํŒจ:", error);
3096
- }
3097
-
3098
- // ์ผ๋‹จ ๊ธฐ๋ณธ ํŽ˜์ด์ง€ ์ถ”๊ฐ€
3099
- if (pages.length === 0) {
3100
- pages = [{
3101
- path: pdfPath,
3102
- cached: isCached
3103
- }];
3104
- }
3105
-
3106
- // ํ”„๋กœ์ ํŠธ์— ์ถ”๊ฐ€ํ•˜๊ณ  ๋ทฐ์–ด ์‹คํ–‰
3107
- const projectId = projects.push(pages) - 1;
3108
- hideLoading();
3109
- open(projectId);
3110
-
3111
- // ํ˜„์žฌ ์—ด๋ฆฐ PDF์˜ ID ์ €์žฅ
3112
- currentPdfId = pdfId;
3113
- // AI ๋ฒ„ํŠผ ํ‘œ์‹œ
3114
- $id('aiButton').style.display = 'block';
3115
- }
3116
- } catch (error) {
3117
- console.error("PDF ID๋กœ ์—ด๊ธฐ ์‹คํŒจ:", error);
3118
- hideLoading();
3119
- showError("PDF๋ฅผ ์—ด ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.");
3120
- }
3121
- }
3122
-
3123
- /* โ”€โ”€ ํ˜„์žฌ PDF์˜ ๊ณ ์œ  URL ์ƒ์„ฑ ๋ฐ ๋ณต์‚ฌ โ”€โ”€ */
3124
- function copyPdfShareUrl() {
3125
- if (!currentPdfId) {
3126
- showError("๊ณต์œ ํ•  PDF๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.");
3127
- return;
3128
- }
3129
-
3130
- // ํ˜„์žฌ ๋„๋ฉ”์ธ ๊ธฐ๋ฐ˜ ์ „์ฒด URL ์ƒ์„ฑ
3131
- const shareUrl = `${window.location.origin}/view/${currentPdfId}`;
3132
-
3133
- // ํด๋ฆฝ๋ณด๋“œ์— ๋ณต์‚ฌ
3134
- navigator.clipboard.writeText(shareUrl)
3135
- .then(() => {
3136
- showMessage("PDF ๋งํฌ๊ฐ€ ๋ณต์‚ฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!");
3137
- })
3138
- .catch(err => {
3139
- console.error("ํด๋ฆฝ๋ณด๋“œ ๋ณต์‚ฌ ์‹คํŒจ:", err);
3140
- showError("๋งํฌ ๋ณต์‚ฌ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.");
3141
- });
3142
- }
3143
-
3144
- /* โ”€โ”€ ์นด๋“œ โ†’ FlipBook โ”€โ”€ */
3145
- async function open(i) {
3146
- toggle(false);
3147
- const pages = projects[i];
3148
-
3149
- // PDF ID ์ฐพ๊ธฐ ๋ฐ ์ €์žฅ
3150
- const card = document.querySelectorAll('.card')[i];
3151
- if (card && card.dataset.pdfId) {
3152
- currentPdfId = card.dataset.pdfId;
3153
- // AI ๋ฒ„ํŠผ ํ‘œ์‹œ
3154
- $id('aiButton').style.display = 'block';
3155
- } else {
3156
- currentPdfId = null;
3157
- // AI ๋ฒ„ํŠผ ์ˆจ๊น€
3158
- $id('aiButton').style.display = 'none';
3159
- }
3160
-
3161
- // AI ์ฑ—๋ด‡ ์ดˆ๊ธฐํ™”
3162
- toggleAiChat(false);
3163
- hasLoadedSummary = false;
3164
- $id('aiChatMessages').innerHTML = '';
3165
-
3166
- // ๊ธฐ์กด FlipBook ์ •๋ฆฌ
3167
- if(fb) {
3168
- fb.destroy();
3169
- viewer.innerHTML = '';
3170
- }
3171
-
3172
- // ์„œ๋ฒ„ PDF ๋˜๋Š” ๋กœ์ปฌ ํ”„๋กœ์ ํŠธ ์ฒ˜๋ฆฌ
3173
- if(pages[0].path) {
3174
- const pdfPath = pages[0].path;
3175
-
3176
- // ์ ์ง„์  ๋กœ๋”ฉ ํ”Œ๋ž˜๊ทธ ์ดˆ๊ธฐํ™”
3177
- let progressiveLoading = false;
3178
- currentLoadingPdfPath = pdfPath;
3179
-
3180
- // ์บ์‹œ ์—ฌ๋ถ€ ํ™•์ธ
3181
- if(pages[0].cached) {
3182
- // ์บ์‹œ๋œ PDF ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
3183
- showLoading("์บ์‹œ๋œ PDF ๋กœ๋”ฉ ์ค‘...");
3184
-
3185
- try {
3186
- const response = await fetch(`/api/cached-pdf?path=${encodeURIComponent(pdfPath)}`);
3187
- const cachedData = await response.json();
3188
-
3189
- if(cachedData.status === "completed" && cachedData.pages) {
3190
- hideLoading();
3191
- createFlipBook(cachedData.pages);
3192
- currentLoadingPdfPath = null;
3193
- return;
3194
- } else if(cachedData.status === "processing" && cachedData.pages && cachedData.pages.length > 0) {
3195
- // ์ผ๋ถ€ ํŽ˜์ด์ง€๊ฐ€ ์ด๋ฏธ ์ฒ˜๋ฆฌ๋œ ๊ฒฝ์šฐ ์ ์ง„์  ๋กœ๋”ฉ ์‚ฌ์šฉ
3196
- hideLoading();
3197
- createFlipBook(cachedData.pages);
3198
- progressiveLoading = true;
3199
-
3200
- // ์ ์ง„์  ๋กœ๋”ฉ ์ค‘์ž„์„ ํ‘œ์‹œ
3201
- startProgressiveLoadingIndicator(cachedData.progress, cachedData.total_pages);
3202
- }
3203
- } catch(error) {
3204
- console.error("์บ์‹œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์˜ค๋ฅ˜:", error);
3205
- // ์บ์‹œ ๋กœ๋”ฉ ์‹คํŒจ ์‹œ ์›๋ณธ PDF๋กœ ๋Œ€์ฒด
3206
- }
3207
- }
3208
-
3209
- if (!progressiveLoading) {
3210
- // ์บ์‹œ๊ฐ€ ์—†๊ฑฐ๋‚˜ ๋กœ๋”ฉ ์‹คํŒจ ์‹œ ์„œ๋ฒ„ PDF ๋กœ๋“œ
3211
- showLoading("PDF ์ค€๋น„ ์ค‘...");
3212
-
3213
- try {
3214
- const response = await fetch(`/api/pdf-content?path=${encodeURIComponent(pdfPath)}`);
3215
- const data = await response.json();
3216
-
3217
- // ์บ์‹œ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋œ ๊ฒฝ์šฐ
3218
- if(data.redirect) {
3219
- const redirectRes = await fetch(data.redirect);
3220
- const cachedData = await redirectRes.json();
3221
-
3222
- if(cachedData.status === "completed" && cachedData.pages) {
3223
- hideLoading();
3224
- createFlipBook(cachedData.pages);
3225
- currentLoadingPdfPath = null;
3226
- return;
3227
- } else if(cachedData.status === "processing" && cachedData.pages && cachedData.pages.length > 0) {
3228
- // ์ผ๋ถ€ ํŽ˜์ด์ง€๊ฐ€ ์ด๋ฏธ ์ฒ˜๋ฆฌ๋œ ๊ฒฝ์šฐ ์ ์ง„์  ๋กœ๋”ฉ ์‚ฌ์šฉ
3229
- hideLoading();
3230
- createFlipBook(cachedData.pages);
3231
-
3232
- // ์ ์ง„์  ๋กœ๋”ฉ ์ค‘์ž„์„ ํ‘œ์‹œ
3233
- startProgressiveLoadingIndicator(cachedData.progress, cachedData.total_pages);
3234
- return;
3235
- }
3236
- }
3237
-
3238
- // ์›๋ณธ PDF ๋กœ๋“œ (ArrayBuffer ํ˜•ํƒœ)
3239
- const pdfResponse = await fetch(`/api/pdf-content?path=${encodeURIComponent(pdfPath)}`);
3240
-
3241
- // JSON ์‘๋‹ต์ธ ๊ฒฝ์šฐ (๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ๋“ฑ)
3242
- try {
3243
- const jsonData = await pdfResponse.clone().json();
3244
- if (jsonData.redirect) {
3245
- const redirectRes = await fetch(jsonData.redirect);
3246
- const cachedData = await redirectRes.json();
3247
-
3248
- if(cachedData.pages && cachedData.pages.length > 0) {
3249
- hideLoading();
3250
- createFlipBook(cachedData.pages);
3251
-
3252
- if(cachedData.status === "processing") {
3253
- startProgressiveLoadingIndicator(cachedData.progress, cachedData.total_pages);
3254
- } else {
3255
- currentLoadingPdfPath = null;
3256
- }
3257
- return;
3258
- }
3259
- }
3260
- } catch (e) {
3261
- // JSON ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ์›๋ณธ PDF ๋ฐ์ดํ„ฐ๋กœ ์ฒ˜๋ฆฌ
3262
- }
3263
-
3264
- // ArrayBuffer ํ˜•ํƒœ์˜ PDF ๋ฐ์ดํ„ฐ
3265
- const pdfData = await pdfResponse.arrayBuffer();
3266
-
3267
- // PDF ๋กœ๋“œ ๋ฐ ํŽ˜์ด์ง€ ๋ Œ๋”๋ง
3268
- const pdf = await pdfjsLib.getDocument({data: pdfData}).promise;
3269
- const pdfPages = [];
3270
-
3271
- for(let p = 1; p <= pdf.numPages; p++) {
3272
- updateLoading(`ํŽ˜์ด์ง€ ์ค€๋น„ ์ค‘... (${p}/${pdf.numPages})`);
3273
-
3274
- const pg = await pdf.getPage(p);
3275
- const vp = pg.getViewport({scale: 1});
3276
- const c = document.createElement('canvas');
3277
- c.width = vp.width;
3278
- c.height = vp.height;
3279
-
3280
- await pg.render({canvasContext: c.getContext('2d'), viewport: vp}).promise;
3281
- pdfPages.push({src: c.toDataURL(), thumb: c.toDataURL()});
3282
- }
3283
-
3284
- hideLoading();
3285
- createFlipBook(pdfPages);
3286
- currentLoadingPdfPath = null;
3287
-
3288
- } catch(error) {
3289
- console.error('PDF ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
3290
- hideLoading();
3291
- showError("PDF๋ฅผ ๋กœ๋“œํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
3292
- currentLoadingPdfPath = null;
3293
- }
3294
- }
3295
- } else {
3296
- // ๋กœ์ปฌ ์—…๋กœ๋“œ๋œ ํ”„๋กœ์ ํŠธ ์‹คํ–‰
3297
- createFlipBook(pages);
3298
- currentLoadingPdfPath = null;
3299
- }
3300
- }
3301
-
3302
- /* โ”€โ”€ ์ ์ง„์  ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ ์‹œ์ž‘ โ”€โ”€ */
3303
- function startProgressiveLoadingIndicator(progress, totalPages) {
3304
- // ์ง„ํ–‰ ์ƒํƒœ ํ‘œ์‹œ ํ™œ์„ฑํ™”
3305
- $id('loadingPages').style.display = 'block';
3306
- $id('loadingPagesCount').textContent = `${progress}%`;
3307
-
3308
- // ๊ธฐ์กด ์ธํ„ฐ๋ฒŒ ์ œ๊ฑฐ
3309
- if (pageLoadingInterval) {
3310
- clearInterval(pageLoadingInterval);
3311
- }
3312
-
3313
- // ์ฃผ๊ธฐ์ ์œผ๋กœ ์บ์‹œ ์ƒํƒœ ํ™•์ธ (2์ดˆ๋งˆ๋‹ค)
3314
- pageLoadingInterval = setInterval(async () => {
3315
- if (!currentLoadingPdfPath) {
3316
- clearInterval(pageLoadingInterval);
3317
- $id('loadingPages').style.display = 'none';
3318
- return;
3319
- }
3320
-
3321
- try {
3322
- const response = await fetch(`/api/cache-status?path=${encodeURIComponent(currentLoadingPdfPath)}`);
3323
- const status = await response.json();
3324
-
3325
- // ์ƒํƒœ ์—…๋ฐ์ดํŠธ
3326
- if (status.status === "completed") {
3327
- clearInterval(pageLoadingInterval);
3328
- $id('loadingPages').style.display = 'none';
3329
- refreshFlipBook(); // ์™„๋ฃŒ๋œ ๋ฐ์ดํ„ฐ๋กœ ์ƒˆ๋กœ๊ณ ์นจ
3330
- currentLoadingPdfPath = null;
3331
- } else if (status.status === "processing") {
3332
- $id('loadingPagesCount').textContent = `${status.progress}%`;
3333
- }
3334
- } catch (e) {
3335
- console.error("์บ์‹œ ์ƒํƒœ ํ™•์ธ ์˜ค๋ฅ˜:", e);
3336
- }
3337
- }, 1000);
3338
- }
3339
-
3340
- /* โ”€โ”€ ํ”Œ๋ฆฝ๋ถ ์ƒˆ๋กœ๊ณ ์นจ โ”€โ”€ */
3341
- async function refreshFlipBook() {
3342
- if (!currentLoadingPdfPath || !fb) return;
3343
-
3344
- try {
3345
- const response = await fetch(`/api/cached-pdf?path=${encodeURIComponent(currentLoadingPdfPath)}`);
3346
- const cachedData = await response.json();
3347
-
3348
- if(cachedData.status === "completed" && cachedData.pages) {
3349
- // ๊ธฐ์กด ํ”Œ๋ฆฝ๋ถ ์ •๋ฆฌ
3350
- fb.destroy();
3351
- viewer.innerHTML = '';
3352
-
3353
- // ์ƒˆ ๋ฐ์ดํ„ฐ๋กœ ์žฌ์ƒ์„ฑ
3354
- createFlipBook(cachedData.pages);
3355
- currentLoadingPdfPath = null;
3356
- }
3357
- } catch (e) {
3358
- console.error("ํ”Œ๋ฆฝ๋ถ ์ƒˆ๋กœ๊ณ ์นจ ์˜ค๋ฅ˜:", e);
3359
- }
3360
- }
3361
-
3362
- function createFlipBook(pages) {
3363
- console.log('FlipBook ์ƒ์„ฑ ์‹œ์ž‘. ํŽ˜์ด์ง€ ์ˆ˜:', pages.length);
3364
-
3365
- try {
3366
- // ํ™”๋ฉด ๋น„์œจ ๊ณ„์‚ฐ
3367
- const calculateAspectRatio = () => {
3368
- const windowWidth = window.innerWidth;
3369
- const windowHeight = window.innerHeight;
3370
- const aspectRatio = windowWidth / windowHeight;
3371
-
3372
- // ๋„ˆ๋น„ ๋˜๋Š” ๋†’์ด ๊ธฐ์ค€์œผ๋กœ ์ตœ๋Œ€ 90% ์ œํ•œ
3373
- let width, height;
3374
- if (aspectRatio > 1) { // ๊ฐ€๋กœ ํ™”๋ฉด
3375
- height = Math.min(windowHeight * 0.9, windowHeight - 40);
3376
- width = height * aspectRatio * 0.8; // ๊ฐ€๋กœ ํ™”๋ฉด์—์„œ๋Š” ์•ฝ๊ฐ„ ์ค„์ž„
3377
- if (width > windowWidth * 0.9) {
3378
- width = windowWidth * 0.9;
3379
- height = width / (aspectRatio * 0.8);
3380
- }
3381
- } else { // ์„ธ๋กœ ํ™”๋ฉด
3382
- width = Math.min(windowWidth * 0.9, windowWidth - 40);
3383
- height = width / aspectRatio * 0.9; // ์„ธ๋กœ ํ™”๋ฉด์—์„œ๋Š” ์•ฝ๊ฐ„ ๋Š˜๋ฆผ
3384
- if (height > windowHeight * 0.9) {
3385
- height = windowHeight * 0.9;
3386
- width = height * aspectRatio * 0.9;
3387
- }
3388
- }
3389
-
3390
- // ์ตœ์  ์‚ฌ์ด์ฆˆ ๋ฐ˜ํ™˜
3391
- return {
3392
- width: Math.round(width),
3393
- height: Math.round(height)
3394
- };
3395
- };
3396
-
3397
- // ์ดˆ๊ธฐ ํ™”๋ฉด ๋น„์œจ ๊ณ„์‚ฐ
3398
- const size = calculateAspectRatio();
3399
- viewer.style.width = size.width + 'px';
3400
- viewer.style.height = size.height + 'px';
3401
-
3402
- // ์‚ฌ์šด๋“œ ์ดˆ๊ธฐํ™” ์—ฌ๋ถ€ ํ™•์ธ
3403
- if (!audioInitialized) {
3404
- initializeAudio()
3405
- .then(() => console.log('FlipBook ์ƒ์„ฑ ์ „ ์˜ค๋””์˜ค ์ดˆ๊ธฐํ™” ์™„๋ฃŒ'))
3406
- .catch(e => console.warn('FlipBook ์ƒ์„ฑ ์‹œ ์˜ค๋””์˜ค ์ดˆ๊ธฐํ™” ์‹คํŒจ:', e));
3407
- }
3408
-
3409
- // ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ ์ •์ œ (๋นˆ ํŽ˜์ด์ง€ ์ฒ˜๋ฆฌ)
3410
- const validPages = pages.map(page => {
3411
- // src๊ฐ€ ์—†๋Š” ํŽ˜์ด์ง€๋Š” ๋กœ๋”ฉ ์ค‘ ์ด๋ฏธ์ง€๋กœ ๋Œ€์ฒด
3412
- if (!page || !page.src) {
3413
- return {
3414
- src: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjVmNWY1Ii8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iIGZpbGw9IiM1NTUiPkxvYWRpbmcuLi48L3RleHQ+PC9zdmc+',
3415
- thumb: page && page.thumb ? page.thumb : ''
3416
- };
3417
- }
3418
- return page;
3419
- });
3420
-
3421
- fb = new FlipBook(viewer, {
3422
- pages: validPages,
3423
- viewMode: 'webgl',
3424
- autoSize: true,
3425
- flipDuration: 800,
3426
- backgroundColor: '#fff',
3427
- /* ๐Ÿ”Š ๋‚ด์žฅ ์‚ฌ์šด๋“œ */
3428
- sound: true,
3429
- assets: {flipMp3: '/static/turnPage2.mp3', hardFlipMp3: '/static/turnPage2.mp3'}, // ์ ˆ๋Œ€ ๊ฒฝ๋กœ๋กœ ์ˆ˜์ •
3430
- controlsProps: {
3431
- enableFullscreen: true,
3432
- enableToc: true,
3433
- enableDownload: false,
3434
- enablePrint: false,
3435
- enableZoom: true,
3436
- enableShare: true, // ๊ณต์œ  ๋ฒ„ํŠผ ํ™œ์„ฑํ™”
3437
- enableSearch: true,
3438
- enableAutoPlay: true,
3439
- enableAnnotation: false,
3440
- enableSound: true,
3441
- enableLightbox: false,
3442
- layout: 10, // ๋ ˆ์ด์•„์›ƒ ์˜ต์…˜
3443
- skin: 'light', // ์Šคํ‚จ ์Šคํƒ€์ผ
3444
- autoNavigationTime: 3600, // ์ž๋™ ๋„˜๊น€ ์‹œ๊ฐ„(์ดˆ)
3445
- hideControls: false, // ์ปจํŠธ๋กค ์ˆจ๊น€ ๋น„ํ™œ์„ฑํ™”
3446
- paddingTop: 10, // ์ƒ๋‹จ ํŒจ๋”ฉ
3447
- paddingLeft: 10, // ์ขŒ์ธก ํŒจ๋”ฉ
3448
- paddingRight: 10, // ์šฐ์ธก ํŒจ๋”ฉ
3449
- paddingBottom: 10, // ํ•˜๋‹จ ํŒจ๋”ฉ
3450
- pageTextureSize: 1024, // ํŽ˜์ด์ง€ ํ…์Šค์ฒ˜ ํฌ๊ธฐ
3451
- thumbnails: true, // ์„ฌ๋„ค์ผ ํ™œ์„ฑํ™”
3452
- autoHideControls: false, // ์ž๋™ ์ˆจ๊น€ ๋น„ํ™œ์„ฑํ™”
3453
- controlsTimeout: 8000, // ์ปจํŠธ๋กค ํ‘œ์‹œ ์‹œ๊ฐ„ ์—ฐ์žฅ
3454
- shareHandler: copyPdfShareUrl // ๊ณต์œ  ํ•ธ๋“ค๋Ÿฌ ์„ค์ •
3455
- }
3456
- });
3457
-
3458
- // ํ™”๋ฉด ํฌ๊ธฐ ๋ณ€๊ฒฝ ์‹œ FlipBook ํฌ๊ธฐ ์กฐ์ •
3459
- window.addEventListener('resize', () => {
3460
- if (fb) {
3461
- const newSize = calculateAspectRatio();
3462
- viewer.style.width = newSize.width + 'px';
3463
- viewer.style.height = newSize.height + 'px';
3464
- fb.resize();
3465
- }
3466
- });
3467
-
3468
- // FlipBook ์ƒ์„ฑ ํ›„ ์ปจํŠธ๋กค๋ฐ” ๊ฐ•์ œ ํ‘œ์‹œ
3469
- setTimeout(() => {
3470
- try {
3471
- // ์ปจํŠธ๋กค๋ฐ” ๊ด€๋ จ ์š”์†Œ ์ฐพ๊ธฐ ๋ฐ ์Šคํƒ€์ผ ์ ์šฉ
3472
- const menuBars = document.querySelectorAll('.flipbook-container .fb3d-menu-bar');
3473
- if (menuBars && menuBars.length > 0) {
3474
- menuBars.forEach(menuBar => {
3475
- menuBar.style.display = 'block';
3476
- menuBar.style.opacity = '1';
3477
- menuBar.style.visibility = 'visible';
3478
- menuBar.style.zIndex = '9999';
3479
- });
3480
- }
3481
- } catch (e) {
3482
- console.warn('์ปจํŠธ๋กค๋ฐ” ์Šคํƒ€์ผ ์ ์šฉ ์ค‘ ์˜ค๋ฅ˜:', e);
3483
- }
3484
- }, 1000);
3485
-
3486
- console.log('FlipBook ์ƒ์„ฑ ์™„๋ฃŒ');
3487
- } catch (error) {
3488
- console.error('FlipBook ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error);
3489
- showError("FlipBook์„ ์ƒ์„ฑํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
3490
- }
3491
- }
3492
-
3493
- /* โ”€โ”€ ๋„ค๋น„๊ฒŒ์ด์…˜ โ”€โ”€ */
3494
- function toggle(showHome){
3495
- $id('home').style.display=showHome?'block':'none';
3496
- $id('viewerPage').style.display=showHome?'none':'block';
3497
- $id('homeButton').style.display=showHome?'none':'block';
3498
- $id('adminPage').style.display='none';
3499
-
3500
- // AI ๋ฒ„ํŠผ ๊ด€๋ฆฌ
3501
- $id('aiButton').style.display = (!showHome && currentPdfId) ? 'block' : 'none';
3502
-
3503
- // AI ์ฑ—๋ด‡์ด ์—ด๋ ค์žˆ์œผ๋ฉด ๋‹ซ๊ธฐ
3504
- if (isAiChatActive) {
3505
- toggleAiChat(false);
3506
- }
3507
-
3508
- // ๋ทฐ์–ด ๋ชจ๋“œ์ผ ๋•Œ ์Šคํƒ€์ผ ๋ณ€๊ฒฝ
3509
- if(!showHome) {
3510
- document.body.classList.add('viewer-mode');
3511
- } else {
3512
- document.body.classList.remove('viewer-mode');
3513
- }
3514
- }
3515
-
3516
- /* -- ๊ด€๋ฆฌ์ž ๊ธฐ๋Šฅ -- */
3517
- function setupAdminFunctions() {
3518
- // ๊ด€๋ฆฌ์ž ๋ฒ„ํŠผ ํด๋ฆญ - ๋ชจ๋‹ฌ ํ‘œ์‹œ
3519
- const adminButton = document.getElementById('adminButton');
3520
- const adminLoginModal = document.getElementById('adminLoginModal');
3521
- const adminLoginClose = document.getElementById('adminLoginClose');
3522
- const adminLoginButton = document.getElementById('adminLoginButton');
3523
- const adminPasswordInput = document.getElementById('adminPasswordInput');
3524
- const adminBackButton = document.getElementById('adminBackButton');
3525
-
3526
- if (adminButton) {
3527
- adminButton.addEventListener('click', function() {
3528
- if (adminLoginModal) {
3529
- adminLoginModal.style.display = 'flex';
3530
- if (adminPasswordInput) {
3531
- adminPasswordInput.value = '';
3532
- adminPasswordInput.focus();
3533
- }
3534
- }
3535
- });
3536
- }
3537
-
3538
- // ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ ๋ฒ„ํŠผ
3539
- if (adminLoginClose) {
3540
- adminLoginClose.addEventListener('click', function() {
3541
- if (adminLoginModal) {
3542
- adminLoginModal.style.display = 'none';
3543
- }
3544
- });
3545
- }
3546
-
3547
- // ์—”ํ„ฐ ํ‚ค๋กœ ๋กœ๊ทธ์ธ
3548
- if (adminPasswordInput) {
3549
- adminPasswordInput.addEventListener('keyup', function(e) {
3550
- if (e.key === 'Enter' && adminLoginButton) {
3551
- adminLoginButton.click();
3552
- }
3553
- });
3554
- }
3555
-
3556
- // ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ
3557
- if (adminLoginButton) {
3558
- adminLoginButton.addEventListener('click', async function() {
3559
- if (!adminPasswordInput) return;
3560
-
3561
- const password = adminPasswordInput.value;
3562
-
3563
- try {
3564
- showLoading("๋กœ๊ทธ์ธ ์ค‘...");
3565
-
3566
- const formData = new FormData();
3567
- formData.append('password', password);
3568
-
3569
- const response = await fetch('/api/admin-login', {
3570
- method: 'POST',
3571
- body: formData
3572
- });
3573
-
3574
- const data = await response.json();
3575
-
3576
- hideLoading();
3577
-
3578
- if (data.success) {
3579
- // ๋กœ๊ทธ์ธ ์„ฑ๊ณต - ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ํ‘œ์‹œ
3580
- if (adminLoginModal) {
3581
- adminLoginModal.style.display = 'none';
3582
- }
3583
- showAdminPage();
3584
- } else {
3585
- // ๋กœ๊ทธ์ธ ์‹คํŒจ
3586
- showError("๊ด€๋ฆฌ์ž ์ธ์ฆ ์‹คํŒจ: ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.");
3587
- }
3588
- } catch (error) {
3589
- console.error("๊ด€๋ฆฌ์ž ๋กœ๊ทธ์ธ ์˜ค๋ฅ˜:", error);
3590
- hideLoading();
3591
- showError("๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
3592
- }
3593
- });
3594
- }
3595
-
3596
- // ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ๋’ค๋กœ๊ฐ€๊ธฐ
3597
- if (adminBackButton) {
3598
- adminBackButton.addEventListener('click', function() {
3599
- document.getElementById('adminPage').style.display = 'none';
3600
- document.getElementById('home').style.display = 'block';
3601
- });
3602
- }
3603
- }
3604
-
3605
- // ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ํ‘œ์‹œ
3606
- async function showAdminPage() {
3607
- showLoading("๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์ค‘...");
3608
-
3609
- // ๋‹ค๋ฅธ ํŽ˜์ด์ง€ ์ˆจ๊ธฐ๊ธฐ
3610
- $id('home').style.display = 'none';
3611
- $id('viewerPage').style.display = 'none';
3612
-
3613
- // ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€์˜ PDF ๋ชฉ๋ก ๋กœ๋“œ
3614
- try {
3615
- const response = await fetch('/api/permanent-pdf-projects');
3616
- const data = await response.json();
3617
-
3618
- const adminGrid = $id('adminGrid');
3619
- adminGrid.innerHTML = ''; // ๊ธฐ์กด ๋‚ด์šฉ ์ง€์šฐ๊ธฐ
3620
-
3621
- if (data.length === 0) {
3622
- $id('noAdminProjects').style.display = 'block';
3623
- } else {
3624
- $id('noAdminProjects').style.display = 'none';
3625
-
3626
- // ๊ฐ PDF ํŒŒ์ผ์— ๋Œ€ํ•œ ์นด๋“œ ์ƒ์„ฑ
3627
- const thumbnailPromises = data.map(async (pdf) => {
3628
- try {
3629
- // ์ธ๋„ค์ผ ๊ฐ€์ ธ์˜ค๊ธฐ
3630
- const thumbResponse = await fetch(`/api/pdf-thumbnail?path=${encodeURIComponent(pdf.path)}`);
3631
- const thumbData = await thumbResponse.json();
3632
-
3633
- // ํ‘œ์‹œ ์—ฌ๋ถ€ ํ™•์ธ (๋ฉ”์ธ ํŽ˜์ด์ง€์— ํ‘œ์‹œ๋˜๋Š”์ง€)
3634
- const mainPdfPath = pdf.path.split('/').pop();
3635
- const isMainDisplayed = serverProjects.some(p => p.path.includes(mainPdfPath));
3636
-
3637
- // ๊ด€๋ฆฌ์ž ์นด๋“œ ์ƒ์„ฑ
3638
- const card = document.createElement('div');
3639
- card.className = 'admin-card card fade-in';
3640
-
3641
- // ๊ณ ์œ  URL ์ƒ์„ฑ
3642
- const viewUrl = `${window.location.origin}/view/${pdf.id}`;
3643
-
3644
- // ์ธ๋„ค์ผ ๋ฐ ์ •๋ณด
3645
- card.innerHTML = `
3646
- <div class="card-inner">
3647
- ${pdf.cached ? '<div class="cached-status">์บ์‹œ๋จ</div>' : ''}
3648
- <img src="${thumbData.thumbnail || ''}" alt="${pdf.name}" loading="lazy">
3649
- <p title="${pdf.name}">${pdf.name.length > 15 ? pdf.name.substring(0, 15) + '...' : pdf.name}</p>
3650
- <div style="position: absolute; bottom: 130px; left: 50%; transform: translateX(-50%); z-index:10;">
3651
- <a href="${viewUrl}" target="_blank" style="color:#4a6ee0; font-size:12px;">๋ฐ”๋กœ๊ฐ€๊ธฐ ๋งํฌ</a>
3652
- </div>
3653
- ${isMainDisplayed ?
3654
- `<button class="unfeature-btn" data-path="${pdf.path}">๋ฉ”์ธ์—์„œ ์ œ๊ฑฐ</button>` :
3655
- `<button class="feature-btn" data-path="${pdf.path}">๋ฉ”์ธ์— ํ‘œ์‹œ</button>`}
3656
- <button class="delete-btn" data-path="${pdf.path}">์‚ญ์ œ</button>
3657
- </div>
3658
- `;
3659
-
3660
- adminGrid.appendChild(card);
3661
-
3662
- // ์‚ญ์ œ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ
3663
- const deleteBtn = card.querySelector('.delete-btn');
3664
- if (deleteBtn) {
3665
- deleteBtn.addEventListener('click', async function(e) {
3666
- e.stopPropagation(); // ์นด๋“œ ํด๋ฆญ ์ด๋ฒคํŠธ ์ „ํŒŒ ๋ฐฉ์ง€
3667
-
3668
- if (confirm(`์ •๋ง "${pdf.name}" PDF๋ฅผ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?`)) {
3669
- try {
3670
- showLoading("PDF ์‚ญ์ œ ์ค‘...");
3671
-
3672
- const response = await fetch(`/api/admin/delete-pdf?path=${encodeURIComponent(pdf.path)}`, {
3673
- method: 'DELETE'
3674
- });
3675
-
3676
- const result = await response.json();
3677
-
3678
- hideLoading();
3679
-
3680
- if (result.success) {
3681
- card.remove();
3682
- showMessage("PDF๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.");
3683
-
3684
- // ๋ฉ”์ธ PDF ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ
3685
- loadServerPDFs();
3686
- } else {
3687
- showError("์‚ญ์ œ ์‹คํŒจ: " + (result.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"));
3688
- }
3689
- } catch (error) {
3690
- console.error("PDF ์‚ญ์ œ ์˜ค๋ฅ˜:", error);
3691
- hideLoading();
3692
- showError("PDF ์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
3693
- }
3694
- }
3695
- });
3696
- }
3697
-
3698
- // ๋ฉ”์ธ์— ํ‘œ์‹œ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ
3699
- const featureBtn = card.querySelector('.feature-btn');
3700
- if (featureBtn) {
3701
- featureBtn.addEventListener('click', async function(e) {
3702
- e.stopPropagation(); // ์นด๋“œ ํด๋ฆญ ์ด๋ฒคํŠธ ์ „ํŒŒ ๋ฐฉ์ง€
3703
-
3704
- try {
3705
- showLoading("์ฒ˜๋ฆฌ ์ค‘...");
3706
-
3707
- const response = await fetch(`/api/admin/feature-pdf?path=${encodeURIComponent(pdf.path)}`, {
3708
- method: 'POST'
3709
- });
3710
-
3711
- const result = await response.json();
3712
-
3713
- hideLoading();
3714
-
3715
- if (result.success) {
3716
- showMessage("PDF๊ฐ€ ๋ฉ”์ธ ํŽ˜์ด์ง€์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.");
3717
- // ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ
3718
- showAdminPage();
3719
- // ๋ฉ”์ธ PDF ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ
3720
- loadServerPDFs();
3721
- } else {
3722
- showError("์ฒ˜๋ฆฌ ์‹คํŒจ: " + (result.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"));
3723
- }
3724
- } catch (error) {
3725
- console.error("PDF ํ‘œ์‹œ ์„ค์ • ์˜ค๋ฅ˜:", error);
3726
- hideLoading();
3727
- showError("์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
3728
- }
3729
- });
3730
- }
3731
-
3732
- // ๋ฉ”์ธ์—์„œ ์ œ๊ฑฐ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ
3733
- const unfeatureBtn = card.querySelector('.unfeature-btn');
3734
- if (unfeatureBtn) {
3735
- unfeatureBtn.addEventListener('click', async function(e) {
3736
- e.stopPropagation(); // ์นด๋“œ ํด๋ฆญ ์ด๋ฒคํŠธ ์ „ํŒŒ ๋ฐฉ์ง€
3737
-
3738
- try {
3739
- showLoading("์ฒ˜๋ฆฌ ์ค‘...");
3740
-
3741
- const response = await fetch(`/api/admin/unfeature-pdf?path=${encodeURIComponent(pdf.path)}`, {
3742
- method: 'DELETE'
3743
- });
3744
-
3745
- const result = await response.json();
3746
-
3747
- hideLoading();
3748
-
3749
- if (result.success) {
3750
- showMessage("PDF๊ฐ€ ๋ฉ”์ธ ํŽ˜์ด์ง€์—์„œ ์ œ๊ฑฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.");
3751
- // ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ
3752
- showAdminPage();
3753
- // ๋ฉ”์ธ PDF ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ
3754
- loadServerPDFs();
3755
- } else {
3756
- showError("์ฒ˜๋ฆฌ ์‹คํŒจ: " + (result.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"));
3757
- }
3758
- } catch (error) {
3759
- console.error("PDF ํ‘œ์‹œ ํ•ด์ œ ์˜ค๋ฅ˜:", error);
3760
- hideLoading();
3761
- showError("์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
3762
- }
3763
- });
3764
- }
3765
- } catch (error) {
3766
- console.error(`PDF ${pdf.name} ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜:`, error);
3767
- }
3768
- });
3769
-
3770
- await Promise.all(thumbnailPromises);
3771
- }
3772
-
3773
- // ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ํ‘œ์‹œ
3774
- hideLoading();
3775
- $id('adminPage').style.display = 'block';
3776
-
3777
- } catch (error) {
3778
- console.error("๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ๋กœ๋“œ ์˜ค๋ฅ˜:", error);
3779
- hideLoading();
3780
- showError("๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
3781
- $id('home').style.display = 'block'; // ์˜ค๋ฅ˜ ์‹œ ํ™ˆ์œผ๋กœ ๋ณต๊ท€
3782
- }
3783
- }
3784
-
3785
- /* -- ๋กœ๋”ฉ ๋ฐ ์˜ค๋ฅ˜ ํ‘œ์‹œ -- */
3786
- function showLoading(message, progress = -1) {
3787
- // ๊ธฐ์กด ๋กœ๋”ฉ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์žˆ๋‹ค๋ฉด ์ œ๊ฑฐ
3788
- hideLoading();
3789
-
3790
- const loadingContainer = document.createElement('div');
3791
- loadingContainer.className = 'loading-container fade-in';
3792
- loadingContainer.id = 'loadingContainer';
3793
-
3794
- let progressBarHtml = '';
3795
- if (progress >= 0) {
3796
- progressBarHtml = `
3797
- <div class="progress-bar-container">
3798
- <div id="progressBar" class="progress-bar" style="width: ${progress}%;"></div>
3799
- </div>
3800
- `;
3801
- }
3802
-
3803
- loadingContainer.innerHTML = `
3804
- <div class="loading-spinner"></div>
3805
- <p class="loading-text" id="loadingText">${message || '๋กœ๋”ฉ ์ค‘...'}</p>
3806
- ${progressBarHtml}
3807
- `;
3808
-
3809
- document.body.appendChild(loadingContainer);
3810
- }
3811
-
3812
- function updateLoading(message, progress = -1) {
3813
- const loadingText = $id('loadingText');
3814
- if (loadingText) {
3815
- loadingText.textContent = message;
3816
- }
3817
-
3818
- if (progress >= 0) {
3819
- let progressBar = $id('progressBar');
3820
-
3821
- if (!progressBar) {
3822
- const loadingContainer = $id('loadingContainer');
3823
- if (loadingContainer) {
3824
- const progressContainer = document.createElement('div');
3825
- progressContainer.className = 'progress-bar-container';
3826
- progressContainer.innerHTML = `<div id="progressBar" class="progress-bar" style="width: ${progress}%;"></div>`;
3827
- loadingContainer.appendChild(progressContainer);
3828
- progressBar = $id('progressBar');
3829
- }
3830
- } else {
3831
- progressBar.style.width = `${progress}%`;
3832
- }
3833
- }
3834
- }
3835
-
3836
- function hideLoading() {
3837
- const loadingContainer = $id('loadingContainer');
3838
- if (loadingContainer) {
3839
- loadingContainer.remove();
3840
- }
3841
- }
3842
-
3843
- function showError(message) {
3844
- // ๊ธฐ์กด ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๊ฐ€ ์žˆ๋‹ค๋ฉด ์ œ๊ฑฐ
3845
- const existingError = $id('errorContainer');
3846
- if (existingError) {
3847
- existingError.remove();
3848
- }
3849
-
3850
- const errorContainer = document.createElement('div');
3851
- errorContainer.className = 'loading-container fade-in';
3852
- errorContainer.id = 'errorContainer';
3853
- errorContainer.innerHTML = `
3854
- <p class="loading-text" style="color: #e74c3c;">${message}</p>
3855
- <button id="errorCloseBtn" style="margin-top: 15px; padding: 8px 16px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer;">ํ™•์ธ</button>
3856
- `;
3857
-
3858
- document.body.appendChild(errorContainer);
3859
-
3860
- // ํ™•์ธ ๋ฒ„ํŠผ ํด๋ฆญ ์ด๋ฒคํŠธ
3861
- $id('errorCloseBtn').onclick = () => {
3862
- errorContainer.remove();
3863
- };
3864
-
3865
- // 5์ดˆ ํ›„ ์ž๋™์œผ๋กœ ๋‹ซ๊ธฐ
3866
- setTimeout(() => {
3867
- if ($id('errorContainer')) {
3868
- $id('errorContainer').remove();
3869
- }
3870
- }, 5000);
3871
- }
3872
-
3873
- function showMessage(message) {
3874
- // ๊ธฐ์กด ๋ฉ”์‹œ์ง€๊ฐ€ ์žˆ๋‹ค๋ฉด ์ œ๊ฑฐ
3875
- const existingMessage = $id('messageContainer');
3876
- if (existingMessage) {
3877
- existingMessage.remove();
3878
- }
3879
-
3880
- const messageContainer = document.createElement('div');
3881
- messageContainer.className = 'loading-container fade-in';
3882
- messageContainer.id = 'messageContainer';
3883
- messageContainer.innerHTML = `
3884
- <p class="loading-text" style="color: #2ecc71;">${message}</p>
3885
- <button id="messageCloseBtn" style="margin-top: 15px; padding: 8px 16px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer;">ํ™•์ธ</button>
3886
- `;
3887
-
3888
- document.body.appendChild(messageContainer);
3889
-
3890
- // ํ™•์ธ ๋ฒ„ํŠผ ํด๋ฆญ ์ด๋ฒคํŠธ
3891
- $id('messageCloseBtn').onclick = () => {
3892
- messageContainer.remove();
3893
- };
3894
-
3895
- // 3์ดˆ ํ›„ ์ž๋™์œผ๋กœ ๋‹ซ๊ธฐ
3896
- setTimeout(() => {
3897
- if ($id('messageContainer')) {
3898
- $id('messageContainer').remove();
3899
- }
3900
- }, 3000);
3901
- }
3902
- </script>
3903
- </body>
3904
- </html>
3905
- """
3906
 
3907
  if __name__ == "__main__":
3908
  uvicorn.run("app:app", host="0.0.0.0", port=int(os.getenv("PORT", 7860)))
 
1311
  return get_html_content()
1312
 
1313
  # HTML ๋ฌธ์ž์—ด (AI ๋ฒ„ํŠผ ๋ฐ ์ฑ—๋ด‡ UI ์ถ”๊ฐ€)
1314
+ # HTML ๋ฌธ์ž์—ด (AI ๋ฒ„ํŠผ ๋ฐ ์ฑ—๋ด‡ UI ์ถ”๊ฐ€)
1315
+ import os
1316
+
1317
+ # Hugging Face Space์˜ secret์—์„œ HTML ํ…œํ”Œ๋ฆฟ ๋กœ๋“œ
1318
+ HTML = os.getenv("HTML_TEMPLATE", "")
1319
+
1320
+ # HTML์ด ๋น„์–ด์žˆ์„ ๊ฒฝ์šฐ ๊ธฐ๋ณธ HTML ์‚ฌ์šฉ (fallback)
1321
+ if not HTML:
1322
+ logger.warning("HTML_TEMPLATE secret์ด ์„ค์ •๋˜์–ด ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ HTML์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.")
1323
+ HTML = """
1324
+ <!doctype html>
1325
+ <html lang="ko">
1326
+ <head>
1327
+ <meta charset="utf-8">
1328
+ <title>FlipBook Space</title>
1329
+ <style>
1330
+ body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
1331
+ .error { color: red; }
1332
+ </style>
1333
+ </head>
1334
+ <body>
1335
+ <h1>HTML ํ…œํ”Œ๋ฆฟ์„ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค</h1>
1336
+ <p class="error">HTML_TEMPLATE secret์ด ์„ค์ •๋˜์–ด ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.</p>
1337
+ <p>Hugging Face Space์˜ secret ์˜์—ญ์— HTML_TEMPLATE์„ ์„ค์ •ํ•ด์ฃผ์„ธ์š”.</p>
1338
+ </body>
1339
+ </html>
1340
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1341
 
1342
  if __name__ == "__main__":
1343
  uvicorn.run("app:app", host="0.0.0.0", port=int(os.getenv("PORT", 7860)))