Darwin commited on
Commit
b5a9d38
Β·
0 Parent(s):

Initial commit: HanSoo Office Editor with FastAPI backend

Browse files
Files changed (4) hide show
  1. README.md +37 -0
  2. app.py +132 -0
  3. index.html +203 -0
  4. requirements.txt +4 -0
README.md ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HanSoo Office Editor
2
+
3
+ HWP Β· DOCX Β· XLSX Β· PPTX Β· PDF νŒŒμΌμ„ λΈŒλΌμš°μ €μ—μ„œ μ—…λ‘œλ“œν•˜κ³  μ²˜λ¦¬ν•  수 μžˆλŠ” μ˜€ν”ΌμŠ€ 에디터.
4
+
5
+ ## κΈ°λŠ₯
6
+
7
+ - **λ‹€μ–‘ν•œ 파일 ν˜•μ‹ 지원**: HWP, HWPX, DOCX, XLSX, PPTX, PDF, TXT, CSV, JSON
8
+ - **λ“œλž˜κ·Έ μ•€ λ“œλ‘­ μ—…λ‘œλ“œ**: 직관적인 파일 μ—…λ‘œλ“œ μΈν„°νŽ˜μ΄μŠ€
9
+ - **ν…μŠ€νŠΈ μΆ”μΆœ**: νŒŒμΌμ—μ„œ ν…μŠ€νŠΈ λ‚΄μš© μΆ”μΆœ
10
+ - **FAST API λ°±μ—”λ“œ**: λΉ λ₯Έ 파일 처리
11
+
12
+ ## 기술 μŠ€νƒ
13
+
14
+ - **λ°±μ—”λ“œ**: FastAPI, Uvicorn
15
+ - **ν”„λ‘ νŠΈμ—”λ“œ**: HTML5, CSS3, Vanilla JavaScript
16
+ - **배포**: HuggingFace Spaces
17
+
18
+ ## 둜컬 μ‹€ν–‰
19
+
20
+ ```bash
21
+ pip install -r requirements.txt
22
+ python app.py
23
+ ```
24
+
25
+ λΈŒλΌμš°μ €μ—μ„œ `http://localhost:7860` 접속
26
+
27
+ ## API μ—”λ“œν¬μΈνŠΈ
28
+
29
+ - `GET /` - 메인 νŽ˜μ΄μ§€
30
+ - `GET /health` - ν—¬μŠ€ 체크
31
+ - `POST /api/upload` - 파일 μ—…λ‘œλ“œ
32
+ - `GET /api/files/{file_id}` - 파일 정보 쑰회
33
+ - `POST /api/process` - 파일 처리 (μΆ”μΆœ, λ³€ν™˜)
34
+
35
+ ## λΌμ΄μ„ μŠ€
36
+
37
+ MIT License
app.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from fastapi import FastAPI, UploadFile, File, HTTPException, Form
4
+ from fastapi.staticfiles import StaticFiles
5
+ from fastapi.responses import FileResponse, JSONResponse
6
+ from fastapi.middleware.cors import CORSMiddleware
7
+ from pathlib import Path
8
+ import shutil
9
+ import uuid
10
+
11
+ app = FastAPI(title="HanSoo Office Editor", version="1.0.0")
12
+
13
+ # CORS μ„€μ •
14
+ app.add_middleware(
15
+ CORSMiddleware,
16
+ allow_origins=["*"],
17
+ allow_credentials=True,
18
+ allow_methods=["*"],
19
+ allow_headers=["*"],
20
+ )
21
+
22
+ # μ—…λ‘œλ“œ 디렉토리
23
+ UPLOAD_DIR = Path("/tmp/hansoo_uploads")
24
+ UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
25
+
26
+ # 정적 파일 제곡
27
+ app.mount("/static", StaticFiles(directory="static"), name="static")
28
+
29
+
30
+ @app.get("/")
31
+ def read_root():
32
+ """인덱슀 νŽ˜μ΄μ§€ 제곡"""
33
+ return FileResponse("index.html")
34
+
35
+
36
+ @app.get("/health")
37
+ def health_check():
38
+ """ν—¬μŠ€ 체크 μ—”λ“œν¬μΈνŠΈ"""
39
+ return {"status": "healthy", "service": "HanSoo Office Editor"}
40
+
41
+
42
+ @app.post("/api/upload")
43
+ async def upload_file(file: UploadFile = File(...)):
44
+ """파일 μ—…λ‘œλ“œ μ—”λ“œν¬μΈνŠΈ"""
45
+ try:
46
+ file_id = str(uuid.uuid4())
47
+ file_path = UPLOAD_DIR / f"{file_id}_{file.filename}"
48
+
49
+ with open(file_path, "wb") as buffer:
50
+ shutil.copyfileobj(file.file, buffer)
51
+
52
+ return {
53
+ "success": True,
54
+ "file_id": file_id,
55
+ "filename": file.filename,
56
+ "size": file_path.stat().st_size,
57
+ "content_type": file.content_type
58
+ }
59
+ except Exception as e:
60
+ raise HTTPException(status_code=500, detail=str(e))
61
+
62
+
63
+ @app.get("/api/files/{file_id}")
64
+ async def get_file(file_id: str):
65
+ """μ—…λ‘œλ“œλœ 파일 정보 쑰회"""
66
+ files = list(UPLOAD_DIR.glob(f"{file_id}_*"))
67
+ if not files:
68
+ raise HTTPException(status_code=404, detail="File not found")
69
+
70
+ file_path = files[0]
71
+ return {
72
+ "file_id": file_id,
73
+ "filename": file_path.name.replace(f"{file_id}_", ""),
74
+ "size": file_path.stat().st_size
75
+ }
76
+
77
+
78
+ @app.post("/api/process")
79
+ async def process_file(
80
+ file_id: str = Form(...),
81
+ operation: str = Form("extract")
82
+ ):
83
+ """파일 처리 μ—”λ“œν¬μΈνŠΈ (μΆ”μΆœ, λ³€ν™˜ λ“±)"""
84
+ files = list(UPLOAD_DIR.glob(f"{file_id}_*"))
85
+ if not files:
86
+ raise HTTPException(status_code=404, detail="File not found")
87
+
88
+ file_path = files[0]
89
+
90
+ try:
91
+ if operation == "extract":
92
+ # ν…μŠ€νŠΈ μΆ”μΆœ 둜직 (ν™•μž₯μžμ— 따라 처리)
93
+ content = extract_text(file_path)
94
+ return {"success": True, "content": content}
95
+ elif operation == "convert":
96
+ # λ³€ν™˜ 둜직
97
+ return {"success": True, "message": "Conversion completed"}
98
+ else:
99
+ raise HTTPException(status_code=400, detail="Unknown operation")
100
+ except Exception as e:
101
+ raise HTTPException(status_code=500, detail=str(e))
102
+
103
+
104
+ def extract_text(file_path: Path) -> str:
105
+ """νŒŒμΌμ—μ„œ ν…μŠ€νŠΈ μΆ”μΆœ"""
106
+ suffix = file_path.suffix.lower()
107
+
108
+ if suffix in [".txt", ".md", ".json", ".csv"]:
109
+ return file_path.read_text(encoding="utf-8")
110
+ elif suffix == ".pdf":
111
+ # PDF ν…μŠ€νŠΈ μΆ”μΆœ (PyPDF2 λ˜λŠ” pdfplumber ν•„μš”)
112
+ return "PDF text extraction requires additional dependencies"
113
+ elif suffix in [".docx", ".xlsx", ".pptx"]:
114
+ return "Office file extraction requires python-docx/openpyxl/python-pptx"
115
+ elif suffix in [".hwp", ".hwpx"]:
116
+ return "HWP extraction requires rhwp or pyhwp"
117
+ else:
118
+ return "Unsupported file format"
119
+
120
+
121
+ @app.on_event("shutdown")
122
+ def cleanup():
123
+ """μ„œλ²„ μ’…λ£Œ μ‹œ μ—…λ‘œλ“œ 디렉토리 정리"""
124
+ try:
125
+ shutil.rmtree(UPLOAD_DIR)
126
+ except:
127
+ pass
128
+
129
+
130
+ if __name__ == "__main__":
131
+ import uvicorn
132
+ uvicorn.run(app, host="0.0.0.0", port=int(os.getenv("PORT", 7860)))
index.html ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ko">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>HanSoo Office Editor</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; min-height: 100vh; }
10
+ .container { max-width: 1200px; margin: 0 auto; padding: 20px; }
11
+ header { background: #fff; padding: 20px; border-radius: 12px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
12
+ h1 { color: #333; font-size: 24px; margin-bottom: 8px; }
13
+ .subtitle { color: #666; font-size: 14px; }
14
+ .upload-area { background: #fff; padding: 40px; border-radius: 12px; border: 2px dashed #ddd; text-align: center; cursor: pointer; transition: all 0.3s; }
15
+ .upload-area:hover { border-color: #4CAF50; background: #f9fff9; }
16
+ .upload-area.dragover { border-color: #4CAF50; background: #e8f5e9; }
17
+ .upload-icon { font-size: 48px; margin-bottom: 16px; }
18
+ .upload-text { color: #666; font-size: 16px; margin-bottom: 8px; }
19
+ .upload-hint { color: #999; font-size: 12px; }
20
+ .file-list { background: #fff; padding: 20px; border-radius: 12px; margin-top: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
21
+ .file-item { display: flex; align-items: center; padding: 12px; border-bottom: 1px solid #eee; }
22
+ .file-item:last-child { border-bottom: none; }
23
+ .file-name { flex: 1; color: #333; }
24
+ .file-size { color: #999; font-size: 12px; margin-right: 16px; }
25
+ .file-actions { display: flex; gap: 8px; }
26
+ .btn { padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; transition: all 0.2s; }
27
+ .btn-primary { background: #4CAF50; color: #fff; }
28
+ .btn-primary:hover { background: #45a049; }
29
+ .btn-secondary { background: #e0e0e0; color: #333; }
30
+ .btn-secondary:hover { background: #d0d0d0; }
31
+ .btn-danger { background: #f44336; color: #fff; }
32
+ .btn-danger:hover { background: #da190b; }
33
+ .status { padding: 12px; border-radius: 6px; margin-top: 16px; display: none; }
34
+ .status.success { background: #e8f5e9; color: #2e7d32; display: block; }
35
+ .status.error { background: #ffebee; color: #c62828; display: block; }
36
+ .supported-formats { margin-top: 20px; padding: 16px; background: #fff3e0; border-radius: 8px; }
37
+ .supported-formats h3 { font-size: 14px; color: #e65100; margin-bottom: 8px; }
38
+ .supported-formats p { font-size: 12px; color: #666; }
39
+ #fileInput { display: none; }
40
+ </style>
41
+ </head>
42
+ <body>
43
+ <div class="container">
44
+ <header>
45
+ <h1>HanSoo Office Editor</h1>
46
+ <p class="subtitle">HWP Β· DOCX Β· XLSX Β· PPTX Β· PDF 파일 μ—…λ‘œλ“œ 및 처리</p>
47
+ </header>
48
+
49
+ <div class="upload-area" id="uploadArea">
50
+ <div class="upload-icon">πŸ“</div>
51
+ <div class="upload-text">νŒŒμΌμ„ λ“œλž˜κ·Έν•˜κ±°λ‚˜ ν΄λ¦­ν•˜μ—¬ μ—…λ‘œλ“œ</div>
52
+ <div class="upload-hint">μ΅œλŒ€ 50MB 지원</div>
53
+ <input type="file" id="fileInput" multiple>
54
+ </div>
55
+
56
+ <div class="supported-formats">
57
+ <h3>μ§€μ›ν•˜λŠ” 파일 ν˜•μ‹</h3>
58
+ <p>HWP, HWPX, DOCX, XLSX, PPTX, PDF, TXT, CSV, JSON</p>
59
+ </div>
60
+
61
+ <div class="file-list" id="fileList" style="display: none;">
62
+ <h2 style="margin-bottom: 16px; font-size: 18px;">μ—…λ‘œλ“œλœ 파일</h2>
63
+ <div id="fileItems"></div>
64
+ </div>
65
+
66
+ <div class="status" id="status"></div>
67
+ </div>
68
+
69
+ <script>
70
+ const uploadArea = document.getElementById('uploadArea');
71
+ const fileInput = document.getElementById('fileInput');
72
+ const fileList = document.getElementById('fileList');
73
+ const fileItems = document.getElementById('fileItems');
74
+ const status = document.getElementById('status');
75
+
76
+ let uploadedFiles = [];
77
+
78
+ // λ“œλž˜κ·Έ μ•€ λ“œλ‘­
79
+ uploadArea.addEventListener('dragover', (e) => {
80
+ e.preventDefault();
81
+ uploadArea.classList.add('dragover');
82
+ });
83
+
84
+ uploadArea.addEventListener('dragleave', () => {
85
+ uploadArea.classList.remove('dragover');
86
+ });
87
+
88
+ uploadArea.addEventListener('drop', (e) => {
89
+ e.preventDefault();
90
+ uploadArea.classList.remove('dragover');
91
+ handleFiles(e.dataTransfer.files);
92
+ });
93
+
94
+ // 클릭 μ—…λ‘œλ“œ
95
+ uploadArea.addEventListener('click', () => fileInput.click());
96
+ fileInput.addEventListener('change', (e) => handleFiles(e.target.files));
97
+
98
+ async function handleFiles(files) {
99
+ for (const file of files) {
100
+ await uploadFile(file);
101
+ }
102
+ }
103
+
104
+ async function uploadFile(file) {
105
+ showStatus('μ—…λ‘œλ“œ 쀑...', 'info');
106
+
107
+ const formData = new FormData();
108
+ formData.append('file', file);
109
+
110
+ try {
111
+ const response = await fetch('/api/upload', {
112
+ method: 'POST',
113
+ body: formData
114
+ });
115
+
116
+ const data = await response.json();
117
+
118
+ if (data.success) {
119
+ uploadedFiles.push({
120
+ file_id: data.file_id,
121
+ filename: data.filename,
122
+ size: data.size
123
+ });
124
+ renderFileList();
125
+ showStatus(`"${data.filename}" μ—…λ‘œλ“œ μ™„λ£Œ`, 'success');
126
+ } else {
127
+ showStatus('μ—…λ‘œλ“œ μ‹€νŒ¨', 'error');
128
+ }
129
+ } catch (error) {
130
+ showStatus(`였λ₯˜: ${error.message}`, 'error');
131
+ }
132
+ }
133
+
134
+ function renderFileList() {
135
+ if (uploadedFiles.length > 0) {
136
+ fileList.style.display = 'block';
137
+ fileItems.innerHTML = uploadedFiles.map(f => `
138
+ <div class="file-item">
139
+ <span class="file-name">${escapeHtml(f.filename)}</span>
140
+ <span class="file-size">${formatSize(f.size)}</span>
141
+ <div class="file-actions">
142
+ <button class="btn btn-primary" onclick="processFile('${f.file_id}', 'extract')">μΆ”μΆœ</button>
143
+ <button class="btn btn-danger" onclick="deleteFile('${f.file_id}')">μ‚­μ œ</button>
144
+ </div>
145
+ </div>
146
+ `).join('');
147
+ }
148
+ }
149
+
150
+ async function processFile(fileId, operation) {
151
+ showStatus('처리 쀑...', 'info');
152
+
153
+ const formData = new FormData();
154
+ formData.append('file_id', fileId);
155
+ formData.append('operation', operation);
156
+
157
+ try {
158
+ const response = await fetch('/api/process', {
159
+ method: 'POST',
160
+ body: formData
161
+ });
162
+
163
+ const data = await response.json();
164
+
165
+ if (data.success) {
166
+ showStatus('처리 μ™„λ£Œ', 'success');
167
+ console.log(data);
168
+ } else {
169
+ showStatus('처리 μ‹€νŒ¨', 'error');
170
+ }
171
+ } catch (error) {
172
+ showStatus(`였λ₯˜: ${error.message}`, 'error');
173
+ }
174
+ }
175
+
176
+ async function deleteFile(fileId) {
177
+ uploadedFiles = uploadedFiles.filter(f => f.file_id !== fileId);
178
+ renderFileList();
179
+ if (uploadedFiles.length > 0) {
180
+ showStatus('파일 μ‚­μ œ μ™„λ£Œ', 'success');
181
+ }
182
+ }
183
+
184
+ function showStatus(message, type) {
185
+ status.textContent = message;
186
+ status.className = `status ${type}`;
187
+ setTimeout(() => { status.className = 'status'; }, 3000);
188
+ }
189
+
190
+ function formatSize(bytes) {
191
+ if (bytes < 1024) return bytes + ' B';
192
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
193
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
194
+ }
195
+
196
+ function escapeHtml(text) {
197
+ const div = document.createElement('div');
198
+ div.textContent = text;
199
+ return div.innerHTML;
200
+ }
201
+ </script>
202
+ </body>
203
+ </html>
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ fastapi>=0.115.0
2
+ uvicorn[standard]>=0.34.0
3
+ python-multipart>=0.0.19
4
+ aiofiles>=24.1.0