feat: 완전한 웹 UI 구현 및 문서 처리 파이프라인 완성
✨ 새로운 기능: - FastAPI 기반 완전한 웹 UI 구현 - 반응형 디자인 (모바일/태블릿 지원) - 드래그앤드롭 파일 업로드 인터페이스 - 실시간 AI 챗봇 인터페이스 - 문서 관리 및 검색 시스템 - 진행률 표시 및 상태 알림 🎨 UI 구성: - 메인 대시보드: 서버 상태, 통계, 빠른 접근 - 파일 업로드: 드래그앤드롭, 처리 옵션, 진행률 - 문서 관리: 검색, 정렬, 미리보기, 다운로드 - AI 챗봇: 실시간 대화, 문서 기반 RAG, 빠른 질문 🔧 기술 스택: - FastAPI + Jinja2 템플릿 - 모던 CSS (그라디언트, 애니메이션) - Font Awesome 아이콘 - JavaScript (ES6+) 🚀 완성된 기능: - 파일 업로드 → 텍스트 추출 → 임베딩 → 번역 → HTML 생성 - 벡터 검색 및 RAG 기반 질의응답 - 다중 모델 지원 (기본/부스팅/영어 전용) - API 키 인증 및 CORS 설정 - NAS 연동 및 파일 내보내기
This commit is contained in:
@@ -0,0 +1,493 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<title>문서 Industrial Safety and Health Management(7:ED)_0 Contents-2025-08-13 (원문)</title>
|
||||||
|
<style>
|
||||||
|
body{font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans KR', 'Apple SD Gothic Neo', Arial, sans-serif; line-height:1.6; margin:24px;}
|
||||||
|
article{max-width: 900px; margin: auto;}
|
||||||
|
h1{font-size: 1.6rem; margin-bottom: 1rem;}
|
||||||
|
.chunk{white-space: pre-wrap; margin: 1rem 0;}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<article>
|
||||||
|
<h1>문서 Industrial Safety and Health Management(7:ED)_0 Contents-2025-08-13 (원문)</h1>
|
||||||
|
<div class="chunk" id="c0">Industrial Safety and Health
|
||||||
|
Management
|
||||||
|
Seventh Edition
|
||||||
|
C. Ray Asfahl
|
||||||
|
David W. Rieske
|
||||||
|
University of Arkansas
|
||||||
|
Pearson
|
||||||
|
33o Hudson Street, NY NY 10013</div>
|
||||||
|
<div class="chunk" id="c1">
|
||||||
|
Senior Vice President Courseware Portfolio Management: Marcia Horton
|
||||||
|
Director, Portfolio Management: Engineering, Computer Science & Global Editions: Julian Partridge
|
||||||
|
Specialist, Higher Ed Portfolio Management: Holly Stark
|
||||||
|
Portfolio Management Assistant: Emily Egan
|
||||||
|
Managing Content Producer: Scott Disanno
|
||||||
|
Content Producer: Carole Snyder
|
||||||
|
Web Developer: Steve Wright
|
||||||
|
Rights and Permissions Manager: Ben Ferrini
|
||||||
|
Manufacturing Buyer, Higher Ed, Lake Side Communications Inc (LSC): Maura Zaldivar-Garcia
|
||||||
|
Inventory Manager: Ann Lam
|
||||||
|
Product Marketing Manager: Yvonne Vannatta
|
||||||
|
Field Marketing Manager: Demetrius Hall
|
||||||
|
Marketing Assistant: Jon Bryant
|
||||||
|
Cover Designer: Black Horse Designs
|
||||||
|
Full-Service Project Manager: Billu Suresh, SPi Global
|
||||||
|
Composition: SPi Global
|
||||||
|
Copyright © 2019 Pearson Education, Inc. All rights reserved. Manufactured in the United States of America. This
|
||||||
|
publication is protected by copyright, and permission should be obtained from the publisher prior to any prohibited
|
||||||
|
reproduction, storage in a retrieval system, or transmissio</div>
|
||||||
|
<div class="chunk" id="c2">tates of America. This
|
||||||
|
publication is protected by copyright, and permission should be obtained from the publisher prior to any prohibited
|
||||||
|
reproduction, storage in a retrieval system, or transmission in any form or by any means, electronic, mechanical,
|
||||||
|
photocopying, recording, or likewise. For information regarding permissions, request forms and the appropriate
|
||||||
|
contacts within the Pearson Education Global Rights & Permissions department, please visit http://www.pearsoned
|
||||||
|
.com∕permissions.
|
||||||
|
Many of the designations by manufacturers and sellers to distinguish their products are claimed as trademarks. Where
|
||||||
|
those designations appear in this book, and the publisher was aware of a trademark claim, the designations have been
|
||||||
|
printed in initial caps or all caps.
|
||||||
|
The author and publisher of this book have used their best efforts in preparing this book. These efforts include the
|
||||||
|
development, research, and testing of theories and programs to determine their effectiveness. The author and publisher
|
||||||
|
make no warranty of any kind, expressed or implied, with regard to these programs or the documentation contained
|
||||||
|
in this book. The author and publisher shall not be liable in any event for</div>
|
||||||
|
<div class="chunk" id="c3">ublisher
|
||||||
|
make no warranty of any kind, expressed or implied, with regard to these programs or the documentation contained
|
||||||
|
in this book. The author and publisher shall not be liable in any event for incidental or consequential damages with, or
|
||||||
|
arising out of, the furnishing, performance, or use of these programs.
|
||||||
|
Library of Congress Cataioging in-Publication Data
|
||||||
|
Names: Asfahl, C. Ray, 1938- author. ∣ Rieske, David W., author.
|
||||||
|
Title: Industrial safety and health management ∕ C. Ray Asfahl, David W.
|
||||||
|
Rieske, University of Arkansas.
|
||||||
|
Description: Seventh edition. ∣ NY, NY : Pearson, [2019] ∣
|
||||||
|
Includes bibliographical references and index.
|
||||||
|
Identifiers: LCCN 2017050947 ∣ ISBN 9780134630564 (alk. paper) ∣
|
||||||
|
ISBN 0134630564 (alk. paper)
|
||||||
|
Subjects: LCSH: Industrial safety. ∣ Industrial hygiene.
|
||||||
|
Classification: LCC T55 .A83 2019 ∣ DDC 658.4∕08--dc23 LC record available at https://lccn.loc.gov/2017050947
|
||||||
|
Pearson ISBN-13: 978-0-13-463056-4
|
||||||
|
ISBN-10: 0-13-463056-4
|
||||||
|
17 2024</div>
|
||||||
|
<div class="chunk" id="c4">
|
||||||
|
Contents
|
||||||
|
Preface ix
|
||||||
|
CHAPTER 1 The Safety and Health Manager 1
|
||||||
|
A Reasonable Objective 2
|
||||||
|
Safety versus Health 4
|
||||||
|
Role in the Corporate Structure 5
|
||||||
|
Resources at Hand 6
|
||||||
|
Summary 12
|
||||||
|
Exercises and Study Questions 12
|
||||||
|
Research Exercises</div>
|
||||||
|
<div class="chunk" id="c5">1 The Safety and Health Manager 1
|
||||||
|
A Reasonable Objective 2
|
||||||
|
Safety versus Health 4
|
||||||
|
Role in the Corporate Structure 5
|
||||||
|
Resources at Hand 6
|
||||||
|
Summary 12
|
||||||
|
Exercises and Study Questions 12
|
||||||
|
Research Exercises 13
|
||||||
|
CHAPTER 2 Development of the Safety and Health Function 15
|
||||||
|
Workers’ Compensation 16
|
||||||
|
Recordkeeping 21
|
||||||
|
Accident Cause Analysis 35
|
||||||
|
Organization of Committees 36
|
||||||
|
Safety and Health Economics 37
|
||||||
|
Training 41
|
||||||
|
Job Placement Testing 43
|
||||||
|
The Smoke-Free Workplace 44
|
||||||
|
Bloodborne Pathogens 45
|
||||||
|
Workplace Violence 47
|
||||||
|
Summary 48
|
||||||
|
Exercises and Study Questions 49
|
||||||
|
Research Exercises 53
|
||||||
|
CHAPTER Z Concepts of Hazard Avoidance 54
|
||||||
|
The Enforcement Approach 55
|
||||||
|
The Psychological Approach 57
|
||||||
|
The Engineering Approach 59
|
||||||
|
The Analytical Approach 67
|
||||||
|
Hazard-Classification Scale 76
|
||||||
|
Summary 82
|
||||||
|
Exercises and Study Questions 83
|
||||||
|
Research Exercises 86
|
||||||
|
Standards Research Questions 87
|
||||||
|
iii</div>
|
||||||
|
<div class="chunk" id="c6">
|
||||||
|
iv Contents
|
||||||
|
CHAPTER 4 Impact of Federal Regulation 88
|
||||||
|
Standards 88
|
||||||
|
NIOSH 93
|
||||||
|
Enforcement 94
|
||||||
|
Public Uproar IOO
|
||||||
|
Role of the States 102
|
||||||
|
Political Trends 104
|
||||||
|
Immigrant Workers 111
|
||||||
|
Summary 111
|
||||||
|
Exercises and Study Questions 112
|
||||||
|
Research Exercises 113
|
||||||
|
Standards Research Questions 114
|
||||||
|
CHAPTER 5 Information Systems 115
|
||||||
|
Hazard Communication 116
|
||||||
|
Inter</div>
|
||||||
|
<div class="chunk" id="c7">Trends 104
|
||||||
|
Immigrant Workers 111
|
||||||
|
Summary 111
|
||||||
|
Exercises and Study Questions 112
|
||||||
|
Research Exercises 113
|
||||||
|
Standards Research Questions 114
|
||||||
|
CHAPTER 5 Information Systems 115
|
||||||
|
Hazard Communication 116
|
||||||
|
International Standards 123
|
||||||
|
Environmental Protection Agency 123
|
||||||
|
Department of Homeland Security 128
|
||||||
|
Computer Information Systems 129
|
||||||
|
Summary 131
|
||||||
|
Exercises and Study Questions 131
|
||||||
|
Research Exercises 132
|
||||||
|
Standards Research Questions 133
|
||||||
|
CHAPTER 6 Process Safety and Disaster Preparedness 134
|
||||||
|
Process Information 135
|
||||||
|
Process Analysis 139
|
||||||
|
Operating Procedures 140
|
||||||
|
Training 141
|
||||||
|
Contractor Personnel 142
|
||||||
|
Acts of Terrorism 142
|
||||||
|
Workplace Security 145
|
||||||
|
Active Shooter Incidents 146
|
||||||
|
Summary 146
|
||||||
|
Exercises and Study Questions 147
|
||||||
|
Research Exercises 148
|
||||||
|
Standards Research Questions 148
|
||||||
|
CHAPTER 7 Buildings and Facilities 150
|
||||||
|
Walking and Working Surfaces 151
|
||||||
|
Exits 162
|
||||||
|
Illumination 164
|
||||||
|
Miscellaneous Facilities 165
|
||||||
|
Sanitation 169
|
||||||
|
Summary 169</div>
|
||||||
|
<div class="chunk" id="c8">
|
||||||
|
Contents v
|
||||||
|
Exercises and Study Questions 170
|
||||||
|
Research Exercises 171
|
||||||
|
Standards Research Questions 171
|
||||||
|
CHAPTER 8 Ergonomics 172
|
||||||
|
Facets of Ergonomics 172
|
||||||
|
Workplace Musculoskeletal Disorders 176
|
||||||
|
Affected Industries 179
|
||||||
|
Ergonomics Standards 179
|
||||||
|
WMSD Management Programs 182
|
||||||
|
Ergon</div>
|
||||||
|
<div class="chunk" id="c9">rds Research Questions 171
|
||||||
|
CHAPTER 8 Ergonomics 172
|
||||||
|
Facets of Ergonomics 172
|
||||||
|
Workplace Musculoskeletal Disorders 176
|
||||||
|
Affected Industries 179
|
||||||
|
Ergonomics Standards 179
|
||||||
|
WMSD Management Programs 182
|
||||||
|
Ergonomic Risk Analysis 184
|
||||||
|
NIOSH Lifting Equation 185
|
||||||
|
Sources of Ergonomic Hazards 193
|
||||||
|
Summary 202
|
||||||
|
Exercises and Study Questions 203
|
||||||
|
Research Exercises 204
|
||||||
|
Standards Research Question 205
|
||||||
|
CHAPTER 9 Health and Toxic Substances 206
|
||||||
|
Baseline Examinations 206
|
||||||
|
Toxic Substances 207
|
||||||
|
Measures of Exposure 216
|
||||||
|
Standards Completion Project 220
|
||||||
|
Detecting Contaminants 222
|
||||||
|
Summary 229
|
||||||
|
Exercises and Study Questions 230
|
||||||
|
Research Exercises 234
|
||||||
|
Standards Research Questions 235
|
||||||
|
CHAPTER 10 Environmental Control and Noise 236
|
||||||
|
Ventilation 236
|
||||||
|
ASHRAE Standards and Indoor Air Quality 242
|
||||||
|
Industrial Noise 243
|
||||||
|
Radiation 260
|
||||||
|
Summary 260
|
||||||
|
Exercises and Study Questions 261
|
||||||
|
Research Exercises 265
|
||||||
|
Standards Research Questions 265
|
||||||
|
CHAPTER 11 Flammable and Explosive Materials 267
|
||||||
|
Flammable Liquids 267
|
||||||
|
Sources of Ignition 272
|
||||||
|
Standards Compliance 274
|
||||||
|
Combustible Liquids 276
|
||||||
|
Spray Finishing 278</div>
|
||||||
|
<div class="chunk" id="c10">
|
||||||
|
vi Contents
|
||||||
|
Dip Tanks 281
|
||||||
|
Explosives 281
|
||||||
|
Liquefied Petroleum Gas 282
|
||||||
|
Combustible Dust 284
|
||||||
|
Conclusion 285
|
||||||
|
Exercises and Study Quest</div>
|
||||||
|
<div class="chunk" id="c11">tandards Compliance 274
|
||||||
|
Combustible Liquids 276
|
||||||
|
Spray Finishing 278</div>
|
||||||
|
<div class="chunk" id="c12">
|
||||||
|
vi Contents
|
||||||
|
Dip Tanks 281
|
||||||
|
Explosives 281
|
||||||
|
Liquefied Petroleum Gas 282
|
||||||
|
Combustible Dust 284
|
||||||
|
Conclusion 285
|
||||||
|
Exercises and Study Questions 285
|
||||||
|
Research Exercises 287
|
||||||
|
Standards Research Questions 288
|
||||||
|
CHAPTER 12 Personal Protection and First Aid 289
|
||||||
|
Protection Need Assessment 290
|
||||||
|
Personal Protective Equipment (PPE) Training 291
|
||||||
|
Hearing Protection 292
|
||||||
|
Determining the Noise Reduction Rating 293
|
||||||
|
Eye and Face Protection 294
|
||||||
|
Respiratory Protection 296
|
||||||
|
Confined Space Entry 309
|
||||||
|
Head Protection 312
|
||||||
|
Miscellaneous Personal Protective Equipment 313
|
||||||
|
First Aid 315
|
||||||
|
Summary 316
|
||||||
|
Exercises and Study Questions 317
|
||||||
|
Research Exercises 319
|
||||||
|
Standards Research Questions 320
|
||||||
|
CHAPTER 13 Fire Protection 321
|
||||||
|
Mechanics of Fire 322
|
||||||
|
Industrial Fires 322
|
||||||
|
Fire Prevention 323
|
||||||
|
Dust Explosions 323
|
||||||
|
Emergency Evacuation 324
|
||||||
|
Fire Brigades 326
|
||||||
|
Fire Extinguishers 327
|
||||||
|
Standpipe and Hose Systems 329
|
||||||
|
Automatic Sprinkler Systems 330
|
||||||
|
Fixed Extinguishing Systems 330
|
||||||
|
Summary 331
|
||||||
|
Exercises and Study Questions 332
|
||||||
|
Research Exercises 334
|
||||||
|
Standards Research Questions 334
|
||||||
|
CHAPTER 14 Materials Handling and Storage 335
|
||||||
|
Materials Storage 336
|
||||||
|
Industrial Trucks 337
|
||||||
|
Passenger</div>
|
||||||
|
<div class="chunk" id="c13">ummary 331
|
||||||
|
Exercises and Study Questions 332
|
||||||
|
Research Exercises 334
|
||||||
|
Standards Research Questions 334
|
||||||
|
CHAPTER 14 Materials Handling and Storage 335
|
||||||
|
Materials Storage 336
|
||||||
|
Industrial Trucks 337
|
||||||
|
Passengers 343
|
||||||
|
Cranes 344</div>
|
||||||
|
<div class="chunk" id="c14">
|
||||||
|
Contents vii
|
||||||
|
Slings 358
|
||||||
|
Conveyors 362
|
||||||
|
Lifting 363
|
||||||
|
Summary 365
|
||||||
|
Exercises and Study Questions 365
|
||||||
|
Research Exercise 368
|
||||||
|
CHAPTER 15 Machine Guarding 369
|
||||||
|
General Machine Guarding 369
|
||||||
|
Safeguarding the Point of Operation 379
|
||||||
|
Power Presses 386
|
||||||
|
Heat Processes 406
|
||||||
|
Grinding Machines 406
|
||||||
|
Saws 408
|
||||||
|
Miscellaneous Machine Guarding 413
|
||||||
|
Miscellaneous Machines and Processes 416
|
||||||
|
Industrial Robots 417
|
||||||
|
Evolution in Robotics and Intelligent Machines 420
|
||||||
|
Summary 421
|
||||||
|
Exercises and Study Questions 422
|
||||||
|
Standards Research Questions 425
|
||||||
|
CHAPTER 16 Welding 426
|
||||||
|
Process Terminology 426
|
||||||
|
Gas Welding Hazards 430
|
||||||
|
Arc Welding Hazards 437
|
||||||
|
Resistance Welding Hazards 438
|
||||||
|
Fires and Explosions 439
|
||||||
|
Eye Protection 441
|
||||||
|
Protective Clothing 442
|
||||||
|
Gases and Fumes 443
|
||||||
|
Summary 446
|
||||||
|
Exercises and Study Questions 447
|
||||||
|
Research Exercises 449
|
||||||
|
Standards Research Questions 450
|
||||||
|
CHAPTER 17 Electrical Hazards 451
|
||||||
|
Electrocution Hazards 451
|
||||||
|
Fire Hazards 464
|
||||||
|
Arc Flash 469
|
||||||
|
Test Equipment 471
|
||||||
|
Exposure to High-Voltage Power Lines 473</div>
|
||||||
|
<div class="chunk" id="c15">ch Exercises 449
|
||||||
|
Standards Research Questions 450
|
||||||
|
CHAPTER 17 Electrical Hazards 451
|
||||||
|
Electrocution Hazards 451
|
||||||
|
Fire Hazards 464
|
||||||
|
Arc Flash 469
|
||||||
|
Test Equipment 471
|
||||||
|
Exposure to High-Voltage Power Lines 473
|
||||||
|
Frequent Violations 473
|
||||||
|
Summary 474
|
||||||
|
Exercises and Study Questions 475</div>
|
||||||
|
<div class="chunk" id="c16">
|
||||||
|
viii Contents
|
||||||
|
Research Exercises 478
|
||||||
|
Standards Research Questions 478
|
||||||
|
CHAPTER 18 Construction 479
|
||||||
|
General Facilities 480
|
||||||
|
Personal Protective Equipment 482
|
||||||
|
Fire Protection 486
|
||||||
|
Tools 486
|
||||||
|
Electrical 488
|
||||||
|
Ladders and Scaffolds 490
|
||||||
|
Floors and Stairways 493
|
||||||
|
Cranes and Hoists 493
|
||||||
|
Heavy Vehicles and Equipment 498
|
||||||
|
ROPS 498
|
||||||
|
Trenching and Excavations 501
|
||||||
|
Concrete Work 505
|
||||||
|
Steel Erection 507
|
||||||
|
Demolition 508
|
||||||
|
Explosive Blasting 509
|
||||||
|
Electric Utilities 510
|
||||||
|
Summary 511
|
||||||
|
Exercises and Study Questions 512
|
||||||
|
Research Exercises 515
|
||||||
|
APPENDICES
|
||||||
|
A OSHA Permissible Exposure Limits 516
|
||||||
|
B Medical Treatment 535
|
||||||
|
C First-Aid Treatment 536
|
||||||
|
D Classification of Medical Treatment 538
|
||||||
|
E Highly Hazardous Chemicals, Toxics, and Reactives 540
|
||||||
|
F North American Industry Classification System (NAICS) Code 544
|
||||||
|
G States Having Federally Approved State Plans for
|
||||||
|
Occupational Safety and Health Standards and Enforcement 548
|
||||||
|
Bibliography 549
|
||||||
|
Glossary 560
|
||||||
|
Index 568</div>
|
||||||
|
<div class="chunk" id="c17">Industry Classification System (NAICS) Code 544
|
||||||
|
G States Having Federally Approved State Plans for
|
||||||
|
Occupational Safety and Health Standards and Enforcement 548
|
||||||
|
Bibliography 549
|
||||||
|
Glossary 560
|
||||||
|
Index 568</div>
|
||||||
|
<div class="chunk" id="c18">
|
||||||
|
Preface
|
||||||
|
The seventh edition of Industrial Safety and Health Management remains true to
|
||||||
|
the purpose of engaging the reader in the common sense approaches to safety and
|
||||||
|
health from a concept, process, and compliance perspective. The book retains its
|
||||||
|
easy-to-read format while increasing the retention of the reader through additional
|
||||||
|
case studies and statistics, relevant topics, and additional explanation of difficult-to-
|
||||||
|
Understand concepts.
|
||||||
|
Much of the safety change we see comes on the heels of major disasters or
|
||||||
|
social trends and changes. The past decade has seen many. The explosion of a major
|
||||||
|
sugar processing plant has driven a renewed focus on combustible dust, an outbreak
|
||||||
|
of Ebola brought focus on contagious diseases, the sinking of a major oil derrick
|
||||||
|
initiated a discussion on regulatory oversight and process health, and numerous
|
||||||
|
acts of violence bring our attention to security in the workplace. Social trends such
|
||||||
|
as the rise of "gig" or "on-demand" employment have brou</div>
|
||||||
|
<div class="chunk" id="c19">n regulatory oversight and process health, and numerous
|
||||||
|
acts of violence bring our attention to security in the workplace. Social trends such
|
||||||
|
as the rise of "gig" or "on-demand" employment have brought about questions
|
||||||
|
of the definition of an "employee" and coverage for safety nets such as workers’
|
||||||
|
compensation. Regulatory changes have even precipitated the complete removal of
|
||||||
|
workers’ compensation in some states. In other areas, the effectiveness of workers’
|
||||||
|
compensation led to a robust dialog on whether or not a permanently injured em
|
||||||
|
ployee truly receives compensation commensurate to his or her injury. Meanwhile
|
||||||
|
rises in the number of states legalizing marijuana have caused companies to ques
|
||||||
|
tion current drug screening programs and medical treatment programs.
|
||||||
|
Regulation has changed as well. The adoption of the Globally Harmonized System
|
||||||
|
for Hazard Communication or GHS has completely changed the way we think about
|
||||||
|
hazard communication. The new system crosses language barriers and helps workers
|
||||||
|
who may not be able to read or may not be fluent in a given language with a series of
|
||||||
|
pictograms depicting the dangers of certain chemicals. Hazards are now categorized
|
||||||
|
in a st</div>
|
||||||
|
<div class="chunk" id="c20">rs and helps workers
|
||||||
|
who may not be able to read or may not be fluent in a given language with a series of
|
||||||
|
pictograms depicting the dangers of certain chemicals. Hazards are now categorized
|
||||||
|
in a standard way which drives increased consistency of approach. For the first time in
|
||||||
|
nearly 20 years, fines associated with citations have gone up considerably. Meanwhile,
|
||||||
|
record fines have been levied against corporations associated with major disasters. The
|
||||||
|
classification of companies has also been changed to the modernized North America
|
||||||
|
Industry Classification System (NAICS).
|
||||||
|
As the authors have used the text in their classrooms, a critical focus has been
|
||||||
|
on addressing the most common areas that students will be expected to apply in
|
||||||
|
an industrial setting. Additional explanation around the concepts of PELs has been
|
||||||
|
given to help students to understand the differences among PEL’S, Ceilings, and
|
||||||
|
other measures. Calculations around the Noise Reduction Rating (NRR) and how it
|
||||||
|
is practically used will help students address the prevalent danger of industrial noise
|
||||||
|
in their work environments. In addition, explained in more detail is sometimes the
|
||||||
|
confusing concept of applying workers</div>
|
||||||
|
<div class="chunk" id="c21">ally used will help students address the prevalent danger of industrial noise
|
||||||
|
in their work environments. In addition, explained in more detail is sometimes the
|
||||||
|
confusing concept of applying workers’ compensation and practical aspects of pro
|
||||||
|
tecting employees.</div>
|
||||||
|
<div class="chunk" id="c22">
|
||||||
|
X Preface
|
||||||
|
WHAT'S NEW IN THIS EDITION?
|
||||||
|
For easy reference, the authors have summarized the new features of this edition as
|
||||||
|
follows:
|
||||||
|
• Overhaul of hazard communication standard and incorporation of the Globally
|
||||||
|
Harmonized System
|
||||||
|
• Increased discussion on workers’ compensation rates and calculations
|
||||||
|
• Trends in workers’ compensation privatization and states “opting-out”
|
||||||
|
• Layers of coverage for permanent injuries
|
||||||
|
• Coverage of the trends in the gig economy and the changing nature of
|
||||||
|
employees
|
||||||
|
• OSHA usage of reporting in “Big Data”
|
||||||
|
• Changes in SIC to North American Industry Classification System (NAICS)
|
||||||
|
• Discussion of bloodborne pathogens and protecting workers from diseases such
|
||||||
|
as HIV and Ebola
|
||||||
|
• Increased coverage of workplace security
|
||||||
|
• Discussion of preparation and response techniques for active shooter scenarios
|
||||||
|
• Impact of medical marijuana
|
||||||
|
• Changes in OSHA citation penalty levels
|
||||||
|
• Increased coverage of Targe</div>
|
||||||
|
<div class="chunk" id="c23">orkplace security
|
||||||
|
• Discussion of preparation and response techniques for active shooter scenarios
|
||||||
|
• Impact of medical marijuana
|
||||||
|
• Changes in OSHA citation penalty levels
|
||||||
|
• Increased coverage of Target Industry programs
|
||||||
|
• Coverage of fatigue and worker safety
|
||||||
|
• Practical discussion of PELs, STELs, Ceiling Limits and how they interact
|
||||||
|
• Changes to flammable liquid classification
|
||||||
|
• Coverage of calculations and usage of Noise Reduction Rating (NRR)
|
||||||
|
• Coverage of long-term health impact to World Trade Center first responders
|
||||||
|
• OSHA s work against the dangers of combustible dust
|
||||||
|
• Additional practical and pragmatic assessment of penalty levels
|
||||||
|
• Additional review of OSHA programs such as SHARP and VPP as OSHA is
|
||||||
|
increasing its collaborative approach in recent years
|
||||||
|
• Additional case studies to bring home to readers about the concepts of safety and
|
||||||
|
health</div>
|
||||||
|
<div class="chunk" id="c24">
|
||||||
|
Preface xi
|
||||||
|
ACKNOWLEDGMENTS
|
||||||
|
Both authors wish to express their appreciation to companies and individuals who have
|
||||||
|
contributed ideas and support for the seventh edition. Special thanks to Richard
|
||||||
|
Wallace, Jimmy Baker, and the entire team at Pratt & Whitney for ideas, pictures,
|
||||||
|
and best practices from their world-class facility. And</div>
|
||||||
|
<div class="chunk" id="c25">d support for the seventh edition. Special thanks to Richard
|
||||||
|
Wallace, Jimmy Baker, and the entire team at Pratt & Whitney for ideas, pictures,
|
||||||
|
and best practices from their world-class facility. Andrew Hilliard, President of Safety
|
||||||
|
Maker, Inc. and E.C. Daven, President of Safety Services, Inc. provided valuable insights
|
||||||
|
and visual examples. Erica Asfahl provided mechanical engineering advice. David Trigg
|
||||||
|
and David Bryan answered questions and provided data on OSHA developments.
|
||||||
|
We are grateful to Ken Kolosh and the team at the National Safety Council for their
|
||||||
|
statistics provided in many areas of the text. Tara Mercer and the National Council on
|
||||||
|
Compensation Insurance shared valuable insights into trends and developments such
|
||||||
|
as the gig economy and the impact of medical marijuana. We learned from Alejandra
|
||||||
|
Nolibos about developments in state workers’ compensation changes. Finally, we
|
||||||
|
dedicate this edition to our patient and supportive families who have endured the
|
||||||
|
process of bringing forth this seventh edition.
|
||||||
|
C. Ray Asfahl
|
||||||
|
David W. Rieske</div>
|
||||||
|
</article>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
20
outputs/html/test.html
Normal file
20
outputs/html/test.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<title>문서 test-doc-001 (원문)</title>
|
||||||
|
<style>
|
||||||
|
body{font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans KR', 'Apple SD Gothic Neo', Arial, sans-serif; line-height:1.6; margin:24px;}
|
||||||
|
article{max-width: 900px; margin: auto;}
|
||||||
|
h1{font-size: 1.6rem; margin-bottom: 1rem;}
|
||||||
|
.chunk{white-space: pre-wrap; margin: 1rem 0;}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<article>
|
||||||
|
<h1>문서 test-doc-001 (원문)</h1>
|
||||||
|
<div class="chunk" id="c0">테스트 문서입니다.</div>
|
||||||
|
</article>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
20
outputs/html/ui-test.html
Normal file
20
outputs/html/ui-test.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<title>문서 ui-test-001 (원문)</title>
|
||||||
|
<style>
|
||||||
|
body{font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans KR', 'Apple SD Gothic Neo', Arial, sans-serif; line-height:1.6; margin:24px;}
|
||||||
|
article{max-width: 900px; margin: auto;}
|
||||||
|
h1{font-size: 1.6rem; margin-bottom: 1rem;}
|
||||||
|
.chunk{white-space: pre-wrap; margin: 1rem 0;}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<article>
|
||||||
|
<h1>문서 ui-test-001 (원문)</h1>
|
||||||
|
<div class="chunk" id="c0">UI 테스트용 문서입니다. 웹 인터페이스를 통한 업로드 테스트.</div>
|
||||||
|
</article>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -4,3 +4,5 @@ requests==2.32.4
|
|||||||
pydantic==2.8.2
|
pydantic==2.8.2
|
||||||
pypdf==6.0.0
|
pypdf==6.0.0
|
||||||
tiktoken==0.11.0
|
tiktoken==0.11.0
|
||||||
|
python-multipart
|
||||||
|
jinja2
|
||||||
|
|||||||
117
server/main.py
117
server/main.py
@@ -1,11 +1,16 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException, Depends, UploadFile, File, Form
|
from fastapi import FastAPI, HTTPException, Depends, UploadFile, File, Form, Request
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import List, Dict, Any
|
from typing import List, Dict, Any
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from .config import settings
|
from .config import settings
|
||||||
from .ollama_client import OllamaClient
|
from .ollama_client import OllamaClient
|
||||||
@@ -18,6 +23,14 @@ from .pipeline import DocumentPipeline
|
|||||||
|
|
||||||
app = FastAPI(title="Local AI Server", version="0.2.1")
|
app = FastAPI(title="Local AI Server", version="0.2.1")
|
||||||
|
|
||||||
|
# 템플릿과 정적 파일 설정
|
||||||
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||||
|
|
||||||
|
# HTML 출력 디렉토리도 정적 파일로 서빙
|
||||||
|
if Path("outputs/html").exists():
|
||||||
|
app.mount("/html", StaticFiles(directory="outputs/html"), name="html")
|
||||||
|
|
||||||
# CORS
|
# CORS
|
||||||
import os
|
import os
|
||||||
cors_origins = os.getenv("CORS_ORIGINS", "*")
|
cors_origins = os.getenv("CORS_ORIGINS", "*")
|
||||||
@@ -68,6 +81,7 @@ class PipelineIngestRequest(BaseModel):
|
|||||||
summarize: bool = False
|
summarize: bool = False
|
||||||
summary_sentences: int = 5
|
summary_sentences: int = 5
|
||||||
summary_language: str | None = None
|
summary_language: str | None = None
|
||||||
|
html_basename: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
@@ -179,6 +193,7 @@ def pipeline_ingest(req: PipelineIngestRequest, _: None = Depends(require_api_ke
|
|||||||
summarize=req.summarize,
|
summarize=req.summarize,
|
||||||
summary_sentences=req.summary_sentences,
|
summary_sentences=req.summary_sentences,
|
||||||
summary_language=req.summary_language,
|
summary_language=req.summary_language,
|
||||||
|
html_basename=req.html_basename,
|
||||||
)
|
)
|
||||||
exported_html: str | None = None
|
exported_html: str | None = None
|
||||||
if result.html_path and settings.export_html_dir:
|
if result.html_path and settings.export_html_dir:
|
||||||
@@ -233,6 +248,7 @@ async def pipeline_ingest_file(
|
|||||||
generate_html=generate_html,
|
generate_html=generate_html,
|
||||||
translate=translate,
|
translate=translate,
|
||||||
target_language=target_language,
|
target_language=target_language,
|
||||||
|
html_basename=file.filename,
|
||||||
)
|
)
|
||||||
exported_html: str | None = None
|
exported_html: str | None = None
|
||||||
if result.html_path and settings.export_html_dir:
|
if result.html_path and settings.export_html_dir:
|
||||||
@@ -356,3 +372,102 @@ def chat_completions(req: ChatCompletionsRequest, _: None = Depends(require_api_
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# UI 라우트들
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@app.get("/", response_class=HTMLResponse)
|
||||||
|
async def dashboard(request: Request):
|
||||||
|
"""메인 대시보드 페이지"""
|
||||||
|
# 서버 상태 가져오기
|
||||||
|
status = {
|
||||||
|
"base_model": settings.base_model,
|
||||||
|
"boost_model": settings.boost_model,
|
||||||
|
"embedding_model": settings.embedding_model,
|
||||||
|
"index_loaded": len(index.rows) if index else 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 최근 문서 (임시 데이터 - 실제로는 DB나 파일에서 가져올 것)
|
||||||
|
recent_documents = []
|
||||||
|
|
||||||
|
# 통계 (임시 데이터)
|
||||||
|
stats = {
|
||||||
|
"total_documents": len(index.rows) if index else 0,
|
||||||
|
"total_chunks": len(index.rows) if index else 0,
|
||||||
|
"today_processed": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
return templates.TemplateResponse("index.html", {
|
||||||
|
"request": request,
|
||||||
|
"status": status,
|
||||||
|
"recent_documents": recent_documents,
|
||||||
|
"stats": stats,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/upload", response_class=HTMLResponse)
|
||||||
|
async def upload_page(request: Request):
|
||||||
|
"""파일 업로드 페이지"""
|
||||||
|
return templates.TemplateResponse("upload.html", {
|
||||||
|
"request": request,
|
||||||
|
"api_key": os.getenv("API_KEY", "")
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def format_file_size(bytes_size):
|
||||||
|
"""파일 크기 포맷팅 헬퍼 함수"""
|
||||||
|
if bytes_size == 0:
|
||||||
|
return "0 Bytes"
|
||||||
|
k = 1024
|
||||||
|
sizes = ["Bytes", "KB", "MB", "GB"]
|
||||||
|
i = int(bytes_size / k)
|
||||||
|
if i >= len(sizes):
|
||||||
|
i = len(sizes) - 1
|
||||||
|
return f"{bytes_size / (k ** i):.2f} {sizes[i]}"
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/documents", response_class=HTMLResponse)
|
||||||
|
async def documents_page(request: Request):
|
||||||
|
"""문서 관리 페이지"""
|
||||||
|
# HTML 파일 목록 가져오기
|
||||||
|
html_dir = Path("outputs/html")
|
||||||
|
html_files = []
|
||||||
|
if html_dir.exists():
|
||||||
|
for file in html_dir.glob("*.html"):
|
||||||
|
stat = file.stat()
|
||||||
|
html_files.append({
|
||||||
|
"name": file.name,
|
||||||
|
"size": stat.st_size,
|
||||||
|
"created": datetime.fromtimestamp(stat.st_ctime).strftime("%Y-%m-%d %H:%M"),
|
||||||
|
"url": f"/html/{file.name}"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 날짜순 정렬 (최신순)
|
||||||
|
html_files.sort(key=lambda x: x["created"], reverse=True)
|
||||||
|
|
||||||
|
return templates.TemplateResponse("documents.html", {
|
||||||
|
"request": request,
|
||||||
|
"documents": html_files,
|
||||||
|
"formatFileSize": format_file_size,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/chat", response_class=HTMLResponse)
|
||||||
|
async def chat_page(request: Request):
|
||||||
|
"""AI 챗봇 페이지"""
|
||||||
|
# 서버 상태 정보
|
||||||
|
status = {
|
||||||
|
"base_model": settings.base_model,
|
||||||
|
"boost_model": settings.boost_model,
|
||||||
|
"embedding_model": settings.embedding_model,
|
||||||
|
"index_loaded": len(index.rows) if index else 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
return templates.TemplateResponse("chat.html", {
|
||||||
|
"request": request,
|
||||||
|
"status": status,
|
||||||
|
"current_time": datetime.now().strftime("%H:%M"),
|
||||||
|
"api_key": os.getenv("API_KEY", "")
|
||||||
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -73,8 +73,10 @@ class DocumentPipeline:
|
|||||||
translated.append(content.strip())
|
translated.append(content.strip())
|
||||||
return translated
|
return translated
|
||||||
|
|
||||||
def build_html(self, doc_id: str, title: str, ko_text: str) -> str:
|
def build_html(self, basename: str, title: str, ko_text: str) -> str:
|
||||||
html_path = self.output_dir / "html" / f"{doc_id}.html"
|
# Ensure .html suffix and sanitize basename
|
||||||
|
safe_base = Path(basename).stem + ".html"
|
||||||
|
html_path = self.output_dir / "html" / safe_base
|
||||||
html = f"""
|
html = f"""
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang=\"ko\">\n<head>\n<meta charset=\"utf-8\"/>\n<title>{title}</title>\n<style>
|
<html lang=\"ko\">\n<head>\n<meta charset=\"utf-8\"/>\n<title>{title}</title>\n<style>
|
||||||
@@ -102,6 +104,7 @@ h1{{font-size: 1.6rem; margin-bottom: 1rem;}}
|
|||||||
summarize: bool = False,
|
summarize: bool = False,
|
||||||
summary_sentences: int = 5,
|
summary_sentences: int = 5,
|
||||||
summary_language: str | None = None,
|
summary_language: str | None = None,
|
||||||
|
html_basename: str | None = None,
|
||||||
) -> PipelineResult:
|
) -> PipelineResult:
|
||||||
parts = chunk_text(text, max_chars=1200, overlap=200)
|
parts = chunk_text(text, max_chars=1200, overlap=200)
|
||||||
|
|
||||||
@@ -124,7 +127,8 @@ h1{{font-size: 1.6rem; margin-bottom: 1rem;}}
|
|||||||
html_path: str | None = None
|
html_path: str | None = None
|
||||||
if generate_html:
|
if generate_html:
|
||||||
title_suffix = "요약+번역본" if (summarize and translate) else ("요약본" if summarize else ("번역본" if translate else "원문"))
|
title_suffix = "요약+번역본" if (summarize and translate) else ("요약본" if summarize else ("번역본" if translate else "원문"))
|
||||||
html_path = self.build_html(doc_id, title=f"문서 {doc_id} ({title_suffix})", ko_text="\n\n".join(translated))
|
basename = html_basename or f"{doc_id}.html"
|
||||||
|
html_path = self.build_html(basename, title=f"문서 {doc_id} ({title_suffix})", ko_text="\n\n".join(translated))
|
||||||
|
|
||||||
return PipelineResult(doc_id=doc_id, html_path=html_path, added_chunks=added, chunks=len(translated))
|
return PipelineResult(doc_id=doc_id, html_path=html_path, added_chunks=added, chunks=len(translated))
|
||||||
|
|
||||||
|
|||||||
394
static/css/style.css
Normal file
394
static/css/style.css
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
/* 기본 스타일 리셋 */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans KR', 'Apple SD Gothic Neo', Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 네비게이션 */
|
||||||
|
.navbar {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 1rem 0;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-logo {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-logo i {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
display: flex;
|
||||||
|
list-style: none;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
background-color: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 메인 콘텐츠 */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding: 0 2rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 카드 스타일 */
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 2px solid #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #495057;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-subtitle {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 그리드 레이아웃 */
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-2 {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-3 {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 버튼 */
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-decoration: none;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 15px rgba(86, 171, 47, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 폼 요소 */
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: border-color 0.3s, box-shadow 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 드래그 앤 드롭 영역 */
|
||||||
|
.drop-zone {
|
||||||
|
border: 2px dashed #ced4da;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 3rem;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.3s;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone:hover,
|
||||||
|
.drop-zone.dragover {
|
||||||
|
border-color: #667eea;
|
||||||
|
background-color: rgba(102, 126, 234, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone i {
|
||||||
|
font-size: 3rem;
|
||||||
|
color: #6c757d;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone.dragover i {
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 상태 표시 */
|
||||||
|
.status {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-success {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-processing {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-error {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 테이블 */
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th,
|
||||||
|
.table td {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 진행률 바 */
|
||||||
|
.progress {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||||
|
transition: width 0.3s;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 알림 */
|
||||||
|
.alert {
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border-left: 4px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border-left-color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border-left-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
background-color: #d1ecf1;
|
||||||
|
color: #0c5460;
|
||||||
|
border-left-color: #17a2b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 푸터 */
|
||||||
|
.footer {
|
||||||
|
background-color: #343a40;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem 0;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 반응형 디자인 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.nav-container {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-2,
|
||||||
|
.grid-3 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 로딩 스피너 */
|
||||||
|
.spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 3px solid rgba(255,255,255,.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top-color: #fff;
|
||||||
|
animation: spin 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 채팅 인터페이스 */
|
||||||
|
.chat-container {
|
||||||
|
height: 500px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-area {
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: white;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
max-width: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user {
|
||||||
|
background-color: #667eea;
|
||||||
|
color: white;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.assistant {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
87
static/js/main.js
Normal file
87
static/js/main.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// 공통 JavaScript 기능들
|
||||||
|
|
||||||
|
// API 호출 헬퍼
|
||||||
|
async function apiCall(url, options = {}) {
|
||||||
|
const defaultOptions = {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(url, { ...defaultOptions, ...options });
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 파일 크기 포맷팅
|
||||||
|
function formatFileSize(bytes) {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 날짜 포맷팅
|
||||||
|
function formatDate(dateString) {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleString('ko-KR');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 토스트 알림
|
||||||
|
function showToast(message, type = 'info') {
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = `alert alert-${type} toast`;
|
||||||
|
toast.textContent = message;
|
||||||
|
toast.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 1000;
|
||||||
|
min-width: 300px;
|
||||||
|
animation: slideIn 0.3s ease;
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.style.animation = 'slideOut 0.3s ease';
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(toast);
|
||||||
|
}, 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 로딩 상태 관리
|
||||||
|
function setLoading(element, loading = true) {
|
||||||
|
if (loading) {
|
||||||
|
element.disabled = true;
|
||||||
|
element.innerHTML = '<span class="spinner"></span> 처리 중...';
|
||||||
|
} else {
|
||||||
|
element.disabled = false;
|
||||||
|
element.innerHTML = element.getAttribute('data-original-text') || '완료';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 애니메이션 CSS 추가
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes slideIn {
|
||||||
|
from { transform: translateX(100%); opacity: 0; }
|
||||||
|
to { transform: translateX(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideOut {
|
||||||
|
from { transform: translateX(0); opacity: 1; }
|
||||||
|
to { transform: translateX(100%); opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
animation: slideIn 0.3s ease;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
39
templates/base.html
Normal file
39
templates/base.html
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}AI 문서 처리 서버{% endblock %}</title>
|
||||||
|
<link href="/static/css/style.css" rel="stylesheet">
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar">
|
||||||
|
<div class="nav-container">
|
||||||
|
<a href="/" class="nav-logo">
|
||||||
|
<i class="fas fa-robot"></i> AI 문서 서버
|
||||||
|
</a>
|
||||||
|
<ul class="nav-menu">
|
||||||
|
<li><a href="/" class="nav-link">대시보드</a></li>
|
||||||
|
<li><a href="/upload" class="nav-link">업로드</a></li>
|
||||||
|
<li><a href="/documents" class="nav-link">문서관리</a></li>
|
||||||
|
<li><a href="/chat" class="nav-link">AI 챗봇</a></li>
|
||||||
|
<li><a href="/docs" class="nav-link" target="_blank">API 문서</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="main-content">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="footer-content">
|
||||||
|
<p>© 2025 AI 문서 처리 서버 | Mac Mini M4 Pro 64GB</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="/static/js/main.js"></script>
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
631
templates/chat.html
Normal file
631
templates/chat.html
Normal file
@@ -0,0 +1,631 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}AI 챗봇 - AI 문서 처리 서버{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="grid grid-2">
|
||||||
|
<!-- 채팅 영역 -->
|
||||||
|
<div class="card chat-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="card-title">
|
||||||
|
<i class="fas fa-robot"></i> AI 문서 챗봇
|
||||||
|
</h2>
|
||||||
|
<div class="chat-status">
|
||||||
|
<span class="status status-success">
|
||||||
|
<i class="fas fa-circle"></i> 온라인
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-container">
|
||||||
|
<div class="chat-messages" id="chatMessages">
|
||||||
|
<div class="message assistant">
|
||||||
|
<div class="message-content">
|
||||||
|
<i class="fas fa-robot message-icon"></i>
|
||||||
|
<div class="message-text">
|
||||||
|
안녕하세요! 저는 문서 기반 AI 어시스턴트입니다.
|
||||||
|
업로드된 문서들에 대해 질문하시면 답변해드리겠습니다.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="message-time">{{ current_time }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-input-area">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control chat-input" id="messageInput"
|
||||||
|
placeholder="메시지를 입력하세요..." maxlength="1000">
|
||||||
|
<button type="button" class="btn btn-primary" id="sendBtn" onclick="sendMessage()">
|
||||||
|
<i class="fas fa-paper-plane"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="input-options">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="useRAG" checked>
|
||||||
|
<i class="fas fa-search"></i> 문서 검색 사용
|
||||||
|
</label>
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="forceBoost">
|
||||||
|
<i class="fas fa-rocket"></i> 고성능 모델 사용
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 설정 및 정보 패널 -->
|
||||||
|
<div class="settings-panel">
|
||||||
|
<!-- 빠른 질문 -->
|
||||||
|
<div class="card">
|
||||||
|
<h3><i class="fas fa-lightning-bolt"></i> 빠른 질문</h3>
|
||||||
|
<div class="quick-questions">
|
||||||
|
<button onclick="askQuickQuestion('전체 문서를 요약해주세요')" class="btn btn-sm btn-outline">
|
||||||
|
문서 요약
|
||||||
|
</button>
|
||||||
|
<button onclick="askQuickQuestion('주요 키워드를 추출해주세요')" class="btn btn-sm btn-outline">
|
||||||
|
키워드 추출
|
||||||
|
</button>
|
||||||
|
<button onclick="askQuickQuestion('이 문서의 핵심 내용은 무엇인가요?')" class="btn btn-sm btn-outline">
|
||||||
|
핵심 내용
|
||||||
|
</button>
|
||||||
|
<button onclick="askQuickQuestion('관련된 다른 문서가 있나요?')" class="btn btn-sm btn-outline">
|
||||||
|
관련 문서
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 검색 결과 -->
|
||||||
|
<div class="card">
|
||||||
|
<h3><i class="fas fa-search"></i> 관련 문서 검색</h3>
|
||||||
|
<input type="text" class="form-control mb-2" id="searchInput"
|
||||||
|
placeholder="문서 내용 검색..." onkeyup="searchDocuments(event)">
|
||||||
|
<div class="search-results" id="searchResults">
|
||||||
|
<p class="text-muted">검색어를 입력하세요</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 모델 정보 -->
|
||||||
|
<div class="card">
|
||||||
|
<h3><i class="fas fa-cog"></i> 모델 설정</h3>
|
||||||
|
<div class="model-info">
|
||||||
|
<div class="info-item">
|
||||||
|
<strong>기본 모델:</strong> {{ status.base_model }}
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<strong>부스트 모델:</strong> {{ status.boost_model }}
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<strong>임베딩 모델:</strong> {{ status.embedding_model }}
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<strong>인덱스 문서:</strong> {{ status.index_loaded }}개
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 채팅 기록 관리 -->
|
||||||
|
<div class="card">
|
||||||
|
<h3><i class="fas fa-history"></i> 대화 관리</h3>
|
||||||
|
<div class="chat-controls">
|
||||||
|
<button onclick="clearChat()" class="btn btn-sm btn-secondary w-100 mb-2">
|
||||||
|
<i class="fas fa-trash"></i> 대화 기록 삭제
|
||||||
|
</button>
|
||||||
|
<button onclick="exportChat()" class="btn btn-sm btn-outline w-100">
|
||||||
|
<i class="fas fa-download"></i> 대화 내용 내보내기
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.chat-card {
|
||||||
|
height: 600px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-card .card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.assistant {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user .message-content {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-icon {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
min-width: 40px;
|
||||||
|
min-height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user .message-icon {
|
||||||
|
background-color: #667eea;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.assistant .message-icon {
|
||||||
|
background-color: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-text {
|
||||||
|
background: white;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user .message-text {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-time {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #6c757d;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
margin-left: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user .message-time {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 3rem;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-area {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group .form-control {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-options {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-questions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #667eea;
|
||||||
|
color: #667eea;
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline:hover {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-item {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-item:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-title {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-snippet {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #6c757d;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 1px solid #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-controls .btn {
|
||||||
|
justify-content: center;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-100 { width: 100%; }
|
||||||
|
.mb-2 { margin-bottom: 0.5rem; }
|
||||||
|
.text-muted { color: #6c757d; }
|
||||||
|
|
||||||
|
/* 로딩 애니메이션 */
|
||||||
|
.typing-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background-color: #6c757d;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: typing 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
|
||||||
|
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
|
||||||
|
|
||||||
|
@keyframes typing {
|
||||||
|
0%, 60%, 100% { opacity: 0.3; }
|
||||||
|
30% { opacity: 1; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
let chatHistory = [];
|
||||||
|
let isWaitingForResponse = false;
|
||||||
|
|
||||||
|
// DOM 요소들
|
||||||
|
const chatMessages = document.getElementById('chatMessages');
|
||||||
|
const messageInput = document.getElementById('messageInput');
|
||||||
|
const sendBtn = document.getElementById('sendBtn');
|
||||||
|
const searchInput = document.getElementById('searchInput');
|
||||||
|
const searchResults = document.getElementById('searchResults');
|
||||||
|
const useRAGCheckbox = document.getElementById('useRAG');
|
||||||
|
const forceBoostCheckbox = document.getElementById('forceBoost');
|
||||||
|
|
||||||
|
// 페이지 로드 시 초기화
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
messageInput.addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
sendMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// URL 파라미터 확인 (특정 문서로 대화하기)
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const doc = urlParams.get('doc');
|
||||||
|
if (doc) {
|
||||||
|
addMessage('assistant', `"${doc}" 문서에 대해 질문해주세요. 어떤 내용이 궁금하신가요?`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 메시지 전송
|
||||||
|
async function sendMessage() {
|
||||||
|
const message = messageInput.value.trim();
|
||||||
|
if (!message || isWaitingForResponse) return;
|
||||||
|
|
||||||
|
// 사용자 메시지 추가
|
||||||
|
addMessage('user', message);
|
||||||
|
messageInput.value = '';
|
||||||
|
isWaitingForResponse = true;
|
||||||
|
sendBtn.disabled = true;
|
||||||
|
|
||||||
|
// 타이핑 인디케이터 표시
|
||||||
|
const typingId = addTypingIndicator();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/chat', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-API-Key': '{{ api_key }}'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
messages: chatHistory,
|
||||||
|
use_rag: useRAGCheckbox.checked,
|
||||||
|
force_boost: forceBoostCheckbox.checked,
|
||||||
|
top_k: 5
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// 타이핑 인디케이터 제거
|
||||||
|
removeTypingIndicator(typingId);
|
||||||
|
|
||||||
|
// AI 응답 추가
|
||||||
|
addMessage('assistant', data.response.message?.content || data.response.response || '응답을 받지 못했습니다.');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Chat error:', error);
|
||||||
|
removeTypingIndicator(typingId);
|
||||||
|
addMessage('assistant', '죄송합니다. 응답 중 오류가 발생했습니다. 다시 시도해주세요.');
|
||||||
|
} finally {
|
||||||
|
isWaitingForResponse = false;
|
||||||
|
sendBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 메시지 추가
|
||||||
|
function addMessage(sender, content) {
|
||||||
|
const messageDiv = document.createElement('div');
|
||||||
|
messageDiv.className = `message ${sender}`;
|
||||||
|
|
||||||
|
const now = new Date().toLocaleTimeString('ko-KR', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
|
||||||
|
const icon = sender === 'user' ? 'fas fa-user' : 'fas fa-robot';
|
||||||
|
|
||||||
|
messageDiv.innerHTML = `
|
||||||
|
<div class="message-content">
|
||||||
|
<i class="${icon} message-icon"></i>
|
||||||
|
<div class="message-text">${content}</div>
|
||||||
|
</div>
|
||||||
|
<div class="message-time">${now}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
chatMessages.appendChild(messageDiv);
|
||||||
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||||
|
|
||||||
|
// 채팅 히스토리에 추가
|
||||||
|
chatHistory.push({
|
||||||
|
role: sender === 'user' ? 'user' : 'assistant',
|
||||||
|
content: content
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 타이핑 인디케이터 추가
|
||||||
|
function addTypingIndicator() {
|
||||||
|
const typingDiv = document.createElement('div');
|
||||||
|
typingDiv.className = 'message assistant';
|
||||||
|
typingDiv.id = 'typing-' + Date.now();
|
||||||
|
|
||||||
|
typingDiv.innerHTML = `
|
||||||
|
<div class="message-content">
|
||||||
|
<i class="fas fa-robot message-icon"></i>
|
||||||
|
<div class="typing-indicator">
|
||||||
|
<div class="typing-dot"></div>
|
||||||
|
<div class="typing-dot"></div>
|
||||||
|
<div class="typing-dot"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
chatMessages.appendChild(typingDiv);
|
||||||
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||||
|
|
||||||
|
return typingDiv.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 타이핑 인디케이터 제거
|
||||||
|
function removeTypingIndicator(typingId) {
|
||||||
|
const typingDiv = document.getElementById(typingId);
|
||||||
|
if (typingDiv) {
|
||||||
|
typingDiv.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 빠른 질문
|
||||||
|
function askQuickQuestion(question) {
|
||||||
|
messageInput.value = question;
|
||||||
|
sendMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 문서 검색
|
||||||
|
async function searchDocuments(event) {
|
||||||
|
if (event.key !== 'Enter') return;
|
||||||
|
|
||||||
|
const query = searchInput.value.trim();
|
||||||
|
if (!query) {
|
||||||
|
searchResults.innerHTML = '<p class="text-muted">검색어를 입력하세요</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/search', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
query: query,
|
||||||
|
top_k: 5
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
displaySearchResults(data.results);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Search error:', error);
|
||||||
|
searchResults.innerHTML = '<p class="text-muted">검색 중 오류가 발생했습니다</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 검색 결과 표시
|
||||||
|
function displaySearchResults(results) {
|
||||||
|
if (!results || results.length === 0) {
|
||||||
|
searchResults.innerHTML = '<p class="text-muted">검색 결과가 없습니다</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultsHtml = results.map(result => `
|
||||||
|
<div class="search-result-item" onclick="useSearchResult('${result.text.replace(/'/g, "\\'")}')">
|
||||||
|
<div class="search-result-title">문서 ID: ${result.id}</div>
|
||||||
|
<div class="search-result-snippet">${result.text.substring(0, 100)}...</div>
|
||||||
|
<small class="text-muted">유사도: ${(result.score * 100).toFixed(1)}%</small>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
searchResults.innerHTML = resultsHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 검색 결과 활용
|
||||||
|
function useSearchResult(text) {
|
||||||
|
messageInput.value = `다음 내용에 대해 설명해주세요: "${text.substring(0, 50)}..."`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 대화 기록 삭제
|
||||||
|
function clearChat() {
|
||||||
|
if (!confirm('정말로 대화 기록을 모두 삭제하시겠습니까?')) return;
|
||||||
|
|
||||||
|
chatHistory = [];
|
||||||
|
chatMessages.innerHTML = `
|
||||||
|
<div class="message assistant">
|
||||||
|
<div class="message-content">
|
||||||
|
<i class="fas fa-robot message-icon"></i>
|
||||||
|
<div class="message-text">
|
||||||
|
안녕하세요! 저는 문서 기반 AI 어시스턴트입니다.
|
||||||
|
업로드된 문서들에 대해 질문하시면 답변해드리겠습니다.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="message-time">${new Date().toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' })}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 대화 내용 내보내기
|
||||||
|
function exportChat() {
|
||||||
|
if (chatHistory.length === 0) {
|
||||||
|
showToast('내보낼 대화 내용이 없습니다.', 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatText = chatHistory.map(msg =>
|
||||||
|
`${msg.role === 'user' ? '사용자' : 'AI'}: ${msg.content}`
|
||||||
|
).join('\n\n');
|
||||||
|
|
||||||
|
const blob = new Blob([chatText], { type: 'text/plain' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `chat-export-${new Date().toISOString().slice(0, 10)}.txt`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 토스트 알림
|
||||||
|
function showToast(message, type = 'info') {
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = `alert alert-${type}`;
|
||||||
|
toast.textContent = message;
|
||||||
|
toast.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 1001;
|
||||||
|
min-width: 300px;
|
||||||
|
animation: slideIn 0.3s ease;
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.style.animation = 'slideOut 0.3s ease';
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.body.contains(toast)) {
|
||||||
|
document.body.removeChild(toast);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
348
templates/documents.html
Normal file
348
templates/documents.html
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}문서 관리 - AI 문서 처리 서버{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h1 class="card-title">
|
||||||
|
<i class="fas fa-folder-open"></i> 문서 관리
|
||||||
|
</h1>
|
||||||
|
<p class="card-subtitle">처리된 문서들을 확인하고 관리하세요</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 통계 요약 -->
|
||||||
|
<div class="grid grid-3 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<h3><i class="fas fa-file-alt"></i> 총 문서 수</h3>
|
||||||
|
<div class="stat-number">{{ documents|length }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h3><i class="fas fa-database"></i> 총 용량</h3>
|
||||||
|
<div class="stat-number" id="totalSize">계산 중...</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h3><i class="fas fa-calendar"></i> 최근 업로드</h3>
|
||||||
|
<div class="stat-number">
|
||||||
|
{% if documents %}
|
||||||
|
{{ documents[0].created.split(' ')[0] }}
|
||||||
|
{% else %}
|
||||||
|
없음
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 검색 및 필터 -->
|
||||||
|
<div class="search-controls mb-4">
|
||||||
|
<div class="grid grid-2">
|
||||||
|
<div>
|
||||||
|
<input type="text" class="form-control" id="searchInput"
|
||||||
|
placeholder="파일명으로 검색..." onkeyup="filterDocuments()">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<select class="form-control" id="sortSelect" onchange="sortDocuments()">
|
||||||
|
<option value="name">이름순</option>
|
||||||
|
<option value="date" selected>날짜순 (최신)</option>
|
||||||
|
<option value="size">크기순</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 문서 목록 -->
|
||||||
|
{% if documents %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table" id="documentsTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><i class="fas fa-file"></i> 파일명</th>
|
||||||
|
<th><i class="fas fa-calendar"></i> 생성일시</th>
|
||||||
|
<th><i class="fas fa-weight"></i> 크기</th>
|
||||||
|
<th><i class="fas fa-cogs"></i> 액션</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for doc in documents %}
|
||||||
|
<tr class="document-row" data-name="{{ doc.name|lower }}" data-date="{{ doc.created }}" data-size="{{ doc.size }}">
|
||||||
|
<td>
|
||||||
|
<div class="file-info">
|
||||||
|
<i class="fas fa-file-alt text-primary"></i>
|
||||||
|
<strong>{{ doc.name }}</strong>
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">{{ formatFileSize(doc.size) }}</small>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{{ doc.created }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="file-size" data-bytes="{{ doc.size }}">{{ formatFileSize(doc.size) }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<a href="/html/{{ doc.name }}" class="btn btn-sm btn-primary" target="_blank" title="HTML 보기">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
<button onclick="openChatWithDoc('{{ doc.name }}')" class="btn btn-sm btn-success" title="이 문서로 대화하기">
|
||||||
|
<i class="fas fa-comments"></i>
|
||||||
|
</button>
|
||||||
|
<button onclick="downloadDoc('{{ doc.name }}')" class="btn btn-sm btn-secondary" title="다운로드">
|
||||||
|
<i class="fas fa-download"></i>
|
||||||
|
</button>
|
||||||
|
<button onclick="deleteDoc('{{ doc.name }}')" class="btn btn-sm btn-danger" title="삭제">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="fas fa-inbox fa-4x text-muted mb-3"></i>
|
||||||
|
<h3>처리된 문서가 없습니다</h3>
|
||||||
|
<p class="text-muted mb-4">파일을 업로드하여 AI 처리를 시작하세요.</p>
|
||||||
|
<a href="/upload" class="btn btn-primary btn-lg">
|
||||||
|
<i class="fas fa-cloud-upload-alt"></i> 첫 문서 업로드하기
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 파일 미리보기 모달 -->
|
||||||
|
<div class="modal" id="previewModal" style="display: none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 id="previewTitle">문서 미리보기</h3>
|
||||||
|
<button onclick="closePreview()" class="btn btn-sm btn-secondary">
|
||||||
|
<i class="fas fa-times"></i> 닫기
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<iframe id="previewFrame" style="width: 100%; height: 500px; border: none;"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.search-controls {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info i {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 900px;
|
||||||
|
max-height: 90%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #667eea;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-4 { margin-bottom: 2rem; }
|
||||||
|
.text-primary { color: #667eea; }
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
let allDocuments = [];
|
||||||
|
|
||||||
|
// 페이지 로드 시 초기화
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// 문서 데이터 수집
|
||||||
|
const rows = document.querySelectorAll('.document-row');
|
||||||
|
allDocuments = Array.from(rows).map(row => ({
|
||||||
|
element: row,
|
||||||
|
name: row.dataset.name,
|
||||||
|
date: new Date(row.dataset.date),
|
||||||
|
size: parseInt(row.dataset.size)
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 총 용량 계산
|
||||||
|
const totalBytes = allDocuments.reduce((sum, doc) => sum + doc.size, 0);
|
||||||
|
document.getElementById('totalSize').textContent = formatFileSize(totalBytes);
|
||||||
|
|
||||||
|
// 기본 정렬 (날짜순)
|
||||||
|
sortDocuments();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 파일 크기 포맷팅
|
||||||
|
function formatFileSize(bytes) {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 문서 검색 필터링
|
||||||
|
function filterDocuments() {
|
||||||
|
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
||||||
|
const rows = document.querySelectorAll('.document-row');
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
const fileName = row.dataset.name;
|
||||||
|
if (fileName.includes(searchTerm)) {
|
||||||
|
row.style.display = '';
|
||||||
|
} else {
|
||||||
|
row.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 문서 정렬
|
||||||
|
function sortDocuments() {
|
||||||
|
const sortBy = document.getElementById('sortSelect').value;
|
||||||
|
const tbody = document.querySelector('#documentsTable tbody');
|
||||||
|
|
||||||
|
allDocuments.sort((a, b) => {
|
||||||
|
switch (sortBy) {
|
||||||
|
case 'name':
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
case 'date':
|
||||||
|
return b.date - a.date; // 최신순
|
||||||
|
case 'size':
|
||||||
|
return b.size - a.size; // 큰 것부터
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// DOM 재정렬
|
||||||
|
allDocuments.forEach(doc => {
|
||||||
|
tbody.appendChild(doc.element);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 문서로 채팅 시작
|
||||||
|
function openChatWithDoc(fileName) {
|
||||||
|
const docName = fileName.replace('.html', '');
|
||||||
|
window.location.href = `/chat?doc=${encodeURIComponent(docName)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 문서 다운로드
|
||||||
|
function downloadDoc(fileName) {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = `/html/${fileName}`;
|
||||||
|
link.download = fileName;
|
||||||
|
link.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 문서 삭제
|
||||||
|
async function deleteDoc(fileName) {
|
||||||
|
if (!confirm(`정말로 "${fileName}" 문서를 삭제하시겠습니까?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 실제로는 DELETE API를 구현해야 함
|
||||||
|
showToast('삭제 기능은 아직 구현되지 않았습니다.', 'info');
|
||||||
|
// TODO: DELETE /api/documents/{fileName} 구현
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Delete error:', error);
|
||||||
|
showToast('삭제 중 오류가 발생했습니다.', 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 미리보기 열기
|
||||||
|
function openPreview(fileName) {
|
||||||
|
document.getElementById('previewTitle').textContent = `${fileName} 미리보기`;
|
||||||
|
document.getElementById('previewFrame').src = `/html/${fileName}`;
|
||||||
|
document.getElementById('previewModal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 미리보기 닫기
|
||||||
|
function closePreview() {
|
||||||
|
document.getElementById('previewModal').style.display = 'none';
|
||||||
|
document.getElementById('previewFrame').src = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 토스트 알림 (main.js에서 가져옴)
|
||||||
|
function showToast(message, type = 'info') {
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = `alert alert-${type}`;
|
||||||
|
toast.textContent = message;
|
||||||
|
toast.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 1001;
|
||||||
|
min-width: 300px;
|
||||||
|
animation: slideIn 0.3s ease;
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.style.animation = 'slideOut 0.3s ease';
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.body.contains(toast)) {
|
||||||
|
document.body.removeChild(toast);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
229
templates/index.html
Normal file
229
templates/index.html
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}AI 문서 처리 서버 - 대시보드{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h1 class="card-title">
|
||||||
|
<i class="fas fa-tachometer-alt"></i> 대시보드
|
||||||
|
</h1>
|
||||||
|
<p class="card-subtitle">AI 문서 처리 서버 현황을 확인하세요</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 시스템 상태 -->
|
||||||
|
<div class="grid grid-3">
|
||||||
|
<div class="card">
|
||||||
|
<h3><i class="fas fa-server"></i> 서버 상태</h3>
|
||||||
|
<div class="status status-success">
|
||||||
|
<i class="fas fa-check-circle"></i> 정상 운영
|
||||||
|
</div>
|
||||||
|
<p class="mt-2">
|
||||||
|
<strong>모델:</strong> {{ status.base_model }}<br>
|
||||||
|
<strong>임베딩:</strong> {{ status.embedding_model }}<br>
|
||||||
|
<strong>인덱스:</strong> {{ status.index_loaded }}개 문서
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3><i class="fas fa-upload"></i> 빠른 업로드</h3>
|
||||||
|
<div class="drop-zone-mini" onclick="window.location.href='/upload'">
|
||||||
|
<i class="fas fa-cloud-upload-alt"></i>
|
||||||
|
<p>파일을 업로드하려면 클릭하세요</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3><i class="fas fa-search"></i> 빠른 검색</h3>
|
||||||
|
<form onsubmit="quickSearch(event)">
|
||||||
|
<input type="text" class="form-control" placeholder="문서 내용 검색..." id="quickSearchInput">
|
||||||
|
<button type="submit" class="btn btn-primary mt-2 w-100">
|
||||||
|
<i class="fas fa-search"></i> 검색
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 최근 문서 -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="card-title">
|
||||||
|
<i class="fas fa-file-alt"></i> 최근 처리된 문서
|
||||||
|
</h2>
|
||||||
|
<a href="/documents" class="btn btn-secondary">모든 문서 보기</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if recent_documents %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>문서 ID</th>
|
||||||
|
<th>처리 시간</th>
|
||||||
|
<th>청크 수</th>
|
||||||
|
<th>상태</th>
|
||||||
|
<th>액션</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for doc in recent_documents %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ doc.id }}</td>
|
||||||
|
<td>{{ doc.created_at }}</td>
|
||||||
|
<td>{{ doc.chunks }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="status status-success">
|
||||||
|
<i class="fas fa-check"></i> 완료
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="/html/{{ doc.html_file }}" class="btn btn-sm btn-primary" target="_blank">
|
||||||
|
<i class="fas fa-eye"></i> 보기
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
|
||||||
|
<p class="text-muted">아직 처리된 문서가 없습니다.</p>
|
||||||
|
<a href="/upload" class="btn btn-primary">첫 문서 업로드하기</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 통계 -->
|
||||||
|
<div class="grid grid-2">
|
||||||
|
<div class="card">
|
||||||
|
<h3><i class="fas fa-chart-bar"></i> 처리 통계</h3>
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-number">{{ stats.total_documents }}</span>
|
||||||
|
<span class="stat-label">총 문서 수</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-number">{{ stats.total_chunks }}</span>
|
||||||
|
<span class="stat-label">총 청크 수</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-number">{{ stats.today_processed }}</span>
|
||||||
|
<span class="stat-label">오늘 처리</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3><i class="fas fa-robot"></i> AI 챗봇</h3>
|
||||||
|
<p>문서 기반 질의응답을 시작하세요.</p>
|
||||||
|
<div class="quick-chat">
|
||||||
|
<input type="text" class="form-control mb-2" placeholder="질문을 입력하세요..." id="quickChatInput">
|
||||||
|
<button onclick="quickChat()" class="btn btn-success w-100">
|
||||||
|
<i class="fas fa-paper-plane"></i> 질문하기
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<a href="/chat" class="btn btn-secondary w-100 mt-2">전체 채팅 화면으로</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.drop-zone-mini {
|
||||||
|
border: 2px dashed #ced4da;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone-mini:hover {
|
||||||
|
border-color: #667eea;
|
||||||
|
background-color: rgba(102, 126, 234, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone-mini i {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #6c757d;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
display: block;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-2 { margin-top: 0.5rem; }
|
||||||
|
.mb-2 { margin-bottom: 0.5rem; }
|
||||||
|
.mb-3 { margin-bottom: 1rem; }
|
||||||
|
.py-4 { padding: 2rem 0; }
|
||||||
|
.w-100 { width: 100%; }
|
||||||
|
.text-center { text-align: center; }
|
||||||
|
.text-muted { color: #6c757d; }
|
||||||
|
.table-responsive { overflow-x: auto; }
|
||||||
|
.btn-sm { padding: 0.25rem 0.5rem; font-size: 0.875rem; }
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
function quickSearch(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const query = document.getElementById('quickSearchInput').value;
|
||||||
|
if (query.trim()) {
|
||||||
|
window.location.href = `/search?q=${encodeURIComponent(query)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function quickChat() {
|
||||||
|
const question = document.getElementById('quickChatInput').value;
|
||||||
|
if (question.trim()) {
|
||||||
|
// 간단한 챗봇 미리보기 (실제 구현은 /chat 페이지에서)
|
||||||
|
alert('채팅 기능은 "AI 챗봇" 페이지에서 이용하실 수 있습니다.');
|
||||||
|
window.location.href = '/chat';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 페이지 로드 시 서버 상태 업데이트
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// 실시간 상태 업데이트 (선택사항)
|
||||||
|
updateStatus();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function updateStatus() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/health');
|
||||||
|
const data = await response.json();
|
||||||
|
// 상태 업데이트 로직
|
||||||
|
console.log('서버 상태:', data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('상태 확인 실패:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
395
templates/upload.html
Normal file
395
templates/upload.html
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}파일 업로드 - AI 문서 처리 서버{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h1 class="card-title">
|
||||||
|
<i class="fas fa-cloud-upload-alt"></i> 파일 업로드
|
||||||
|
</h1>
|
||||||
|
<p class="card-subtitle">PDF 또는 TXT 파일을 업로드하여 AI 처리를 시작하세요</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="uploadForm" enctype="multipart/form-data">
|
||||||
|
<div class="grid grid-2">
|
||||||
|
<!-- 파일 선택 영역 -->
|
||||||
|
<div>
|
||||||
|
<h3><i class="fas fa-file"></i> 파일 선택</h3>
|
||||||
|
|
||||||
|
<div class="drop-zone" id="dropZone">
|
||||||
|
<i class="fas fa-cloud-upload-alt"></i>
|
||||||
|
<h4>파일을 드래그하거나 클릭하여 선택</h4>
|
||||||
|
<p>PDF, TXT 파일 지원 (최대 200MB)</p>
|
||||||
|
<input type="file" id="fileInput" name="file" accept=".pdf,.txt" style="display: none;">
|
||||||
|
<button type="button" class="btn btn-primary mt-2" onclick="document.getElementById('fileInput').click()">
|
||||||
|
<i class="fas fa-folder-open"></i> 파일 선택
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="fileInfo" class="file-info" style="display: none;">
|
||||||
|
<h4><i class="fas fa-file-check"></i> 선택된 파일</h4>
|
||||||
|
<div id="fileDetails"></div>
|
||||||
|
<button type="button" class="btn btn-secondary btn-sm mt-2" onclick="clearFile()">
|
||||||
|
<i class="fas fa-times"></i> 다른 파일 선택
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 옵션 설정 -->
|
||||||
|
<div>
|
||||||
|
<h3><i class="fas fa-cogs"></i> 처리 옵션</h3>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="docId">문서 ID</label>
|
||||||
|
<input type="text" class="form-control" id="docId" name="doc_id"
|
||||||
|
placeholder="예: report-2025-001" required>
|
||||||
|
<small class="text-muted">고유한 문서 식별자를 입력하세요</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="generateHtml" name="generate_html" checked>
|
||||||
|
<i class="fas fa-code"></i> HTML 파일 생성
|
||||||
|
</label>
|
||||||
|
<small class="text-muted">읽기 쉬운 HTML 형태로 변환합니다</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="translate" name="translate">
|
||||||
|
<i class="fas fa-language"></i> 한국어로 번역
|
||||||
|
</label>
|
||||||
|
<small class="text-muted">영어 문서를 한국어로 번역합니다</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" id="targetLangGroup" style="display: none;">
|
||||||
|
<label class="form-label" for="targetLanguage">번역 언어</label>
|
||||||
|
<select class="form-control" id="targetLanguage" name="target_language">
|
||||||
|
<option value="ko">한국어</option>
|
||||||
|
<option value="en">영어</option>
|
||||||
|
<option value="ja">일본어</option>
|
||||||
|
<option value="zh">중국어</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="summarize" name="summarize">
|
||||||
|
<i class="fas fa-compress-alt"></i> 요약 생성
|
||||||
|
</label>
|
||||||
|
<small class="text-muted">긴 문서를 요약하여 처리합니다</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" id="summaryGroup" style="display: none;">
|
||||||
|
<label class="form-label" for="summarySentences">요약 문장 수</label>
|
||||||
|
<input type="number" class="form-control" id="summarySentences"
|
||||||
|
name="summary_sentences" value="5" min="3" max="10">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="upload-actions">
|
||||||
|
<button type="submit" class="btn btn-success btn-lg" id="uploadBtn" disabled>
|
||||||
|
<i class="fas fa-rocket"></i> 업로드 및 처리 시작
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="resetForm()">
|
||||||
|
<i class="fas fa-redo"></i> 초기화
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- 진행 상황 -->
|
||||||
|
<div id="progressSection" style="display: none;">
|
||||||
|
<h3><i class="fas fa-tasks"></i> 처리 진행 상황</h3>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" id="progressBar" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
<div id="progressText">준비 중...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 결과 -->
|
||||||
|
<div id="resultSection" style="display: none;">
|
||||||
|
<h3><i class="fas fa-check-circle"></i> 처리 완료</h3>
|
||||||
|
<div id="resultContent"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.checkbox-group {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label input[type="checkbox"] {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-details {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-actions {
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding-top: 2rem;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#progressSection {
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
#resultSection {
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background-color: #d4edda;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-lg {
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
let selectedFile = null;
|
||||||
|
|
||||||
|
// DOM 요소들
|
||||||
|
const dropZone = document.getElementById('dropZone');
|
||||||
|
const fileInput = document.getElementById('fileInput');
|
||||||
|
const fileInfo = document.getElementById('fileInfo');
|
||||||
|
const fileDetails = document.getElementById('fileDetails');
|
||||||
|
const uploadBtn = document.getElementById('uploadBtn');
|
||||||
|
const uploadForm = document.getElementById('uploadForm');
|
||||||
|
const progressSection = document.getElementById('progressSection');
|
||||||
|
const progressBar = document.getElementById('progressBar');
|
||||||
|
const progressText = document.getElementById('progressText');
|
||||||
|
const resultSection = document.getElementById('resultSection');
|
||||||
|
const resultContent = document.getElementById('resultContent');
|
||||||
|
|
||||||
|
// 체크박스 이벤트
|
||||||
|
document.getElementById('translate').addEventListener('change', function() {
|
||||||
|
const targetLangGroup = document.getElementById('targetLangGroup');
|
||||||
|
targetLangGroup.style.display = this.checked ? 'block' : 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('summarize').addEventListener('change', function() {
|
||||||
|
const summaryGroup = document.getElementById('summaryGroup');
|
||||||
|
summaryGroup.style.display = this.checked ? 'block' : 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 드래그 앤 드롭 이벤트
|
||||||
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.add('dragover');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragleave', () => {
|
||||||
|
dropZone.classList.remove('dragover');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('dragover');
|
||||||
|
|
||||||
|
const files = e.dataTransfer.files;
|
||||||
|
if (files.length > 0) {
|
||||||
|
handleFileSelect(files[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 파일 선택 이벤트
|
||||||
|
fileInput.addEventListener('change', (e) => {
|
||||||
|
if (e.target.files.length > 0) {
|
||||||
|
handleFileSelect(e.target.files[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 파일 처리 함수
|
||||||
|
function handleFileSelect(file) {
|
||||||
|
// 파일 타입 검증
|
||||||
|
const allowedTypes = ['application/pdf', 'text/plain'];
|
||||||
|
if (!allowedTypes.includes(file.type) &&
|
||||||
|
!file.name.toLowerCase().endsWith('.pdf') &&
|
||||||
|
!file.name.toLowerCase().endsWith('.txt')) {
|
||||||
|
showToast('PDF 또는 TXT 파일만 업로드할 수 있습니다.', 'danger');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 파일 크기 검증 (200MB)
|
||||||
|
if (file.size > 200 * 1024 * 1024) {
|
||||||
|
showToast('파일 크기는 200MB를 초과할 수 없습니다.', 'danger');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedFile = file;
|
||||||
|
|
||||||
|
// 파일 정보 표시
|
||||||
|
fileDetails.innerHTML = `
|
||||||
|
<div class="file-details">
|
||||||
|
<div><strong>파일명:</strong> ${file.name}</div>
|
||||||
|
<div><strong>크기:</strong> ${formatFileSize(file.size)}</div>
|
||||||
|
<div><strong>타입:</strong> ${file.type || '알 수 없음'}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
dropZone.style.display = 'none';
|
||||||
|
fileInfo.style.display = 'block';
|
||||||
|
uploadBtn.disabled = false;
|
||||||
|
|
||||||
|
// 문서 ID 자동 생성
|
||||||
|
const timestamp = new Date().toISOString().slice(0, 10);
|
||||||
|
const baseName = file.name.replace(/\.[^/.]+$/, "");
|
||||||
|
document.getElementById('docId').value = `${baseName}-${timestamp}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 파일 선택 취소
|
||||||
|
function clearFile() {
|
||||||
|
selectedFile = null;
|
||||||
|
fileInput.value = '';
|
||||||
|
dropZone.style.display = 'block';
|
||||||
|
fileInfo.style.display = 'none';
|
||||||
|
uploadBtn.disabled = true;
|
||||||
|
progressSection.style.display = 'none';
|
||||||
|
resultSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 폼 초기화
|
||||||
|
function resetForm() {
|
||||||
|
clearFile();
|
||||||
|
uploadForm.reset();
|
||||||
|
document.getElementById('generateHtml').checked = true;
|
||||||
|
document.getElementById('targetLangGroup').style.display = 'none';
|
||||||
|
document.getElementById('summaryGroup').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 폼 제출
|
||||||
|
uploadForm.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!selectedFile) {
|
||||||
|
showToast('파일을 선택해주세요.', 'danger');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', selectedFile);
|
||||||
|
formData.append('doc_id', document.getElementById('docId').value);
|
||||||
|
formData.append('generate_html', document.getElementById('generateHtml').checked);
|
||||||
|
formData.append('translate', document.getElementById('translate').checked);
|
||||||
|
formData.append('target_language', document.getElementById('targetLanguage').value);
|
||||||
|
|
||||||
|
// 진행 상황 표시
|
||||||
|
progressSection.style.display = 'block';
|
||||||
|
resultSection.style.display = 'none';
|
||||||
|
setLoading(uploadBtn);
|
||||||
|
|
||||||
|
try {
|
||||||
|
updateProgress(20, '파일 업로드 중...');
|
||||||
|
|
||||||
|
const response = await fetch('/pipeline/ingest_file', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-API-Key': '{{ api_key }}'
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgress(80, '처리 중...');
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
updateProgress(100, '완료!');
|
||||||
|
|
||||||
|
// 결과 표시
|
||||||
|
setTimeout(() => {
|
||||||
|
showResult(result);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
showToast('파일이 성공적으로 처리되었습니다!', 'success');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload error:', error);
|
||||||
|
showToast('업로드 중 오류가 발생했습니다: ' + error.message, 'danger');
|
||||||
|
progressSection.style.display = 'none';
|
||||||
|
} finally {
|
||||||
|
uploadBtn.disabled = false;
|
||||||
|
uploadBtn.innerHTML = '<i class="fas fa-rocket"></i> 업로드 및 처리 시작';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateProgress(percent, text) {
|
||||||
|
progressBar.style.width = percent + '%';
|
||||||
|
progressText.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showResult(result) {
|
||||||
|
resultContent.innerHTML = `
|
||||||
|
<div class="result-details">
|
||||||
|
<h4><i class="fas fa-info-circle"></i> 처리 결과</h4>
|
||||||
|
<div class="grid grid-2" style="margin-top: 1rem;">
|
||||||
|
<div>
|
||||||
|
<strong>문서 ID:</strong> ${result.doc_id}<br>
|
||||||
|
<strong>처리된 청크:</strong> ${result.added}개<br>
|
||||||
|
<strong>전체 청크:</strong> ${result.chunks}개
|
||||||
|
</div>
|
||||||
|
<div class="result-actions">
|
||||||
|
${result.html_path ? `
|
||||||
|
<a href="/html/${result.html_path.split('/').pop()}"
|
||||||
|
class="btn btn-primary" target="_blank">
|
||||||
|
<i class="fas fa-eye"></i> HTML 보기
|
||||||
|
</a>
|
||||||
|
` : ''}
|
||||||
|
<a href="/documents" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-list"></i> 문서 목록
|
||||||
|
</a>
|
||||||
|
<a href="/chat" class="btn btn-success">
|
||||||
|
<i class="fas fa-comments"></i> 문서로 대화하기
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
resultSection.style.display = 'block';
|
||||||
|
progressSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user