Ver3.0 ๐ค AI ์์ด์ ํธ์ ํ์ผ์ ๋ก๋ ๊ธฐ๋ฅ ์ถ๊ฐํ๊ธฐ: ์ค์ ๊ฐ์ด๋์ ๊ตฌํ ์ ๋ต

๐ค AI ์์ด์ ํธ์ ํ์ผ์ ๋ก๋ ๊ธฐ๋ฅ ์ถ๊ฐํ๊ธฐ: ์ค์ ๊ฐ์ด๋์ ๊ตฌํ ์ ๋ต
๋น์ ์ AI ์์ด์ ํธ๋ฅผ ํ ๋จ๊ณ ์ ๊ทธ๋ ์ด๋ํ๋ ํ์ ๊ธฐ๋ฅ ์๋ฒฝ ์ ๋ณต! ๐โจ
๐ฏ ์ AI ์์ด์ ํธ์ ํ์ผ์ ๋ก๋ ๊ธฐ๋ฅ์ด ํ์ํ ๊น?
์๋
, ์น๊ตฌ! ์ค๋์ AI ์์ด์ ํธ ๊ฐ๋ฐ์์ ์ ๋ง ์ค์ํ ์ฃผ์ ๋ฅผ ๋ค๋ค๋ณผ ๊ฑฐ์ผ. ๋ฐ๋ก ํ์ผ์
๋ก๋ ๊ธฐ๋ฅ์ด์ง! ๐
์์ฆ AI ์์ด์ ํธ๋ค์ด ์ ๋ง ๋๋ํด์ง๊ณ ์์์? ๊ทธ๋ฐ๋ฐ ๋ง์ด์ผ, ์๋ฌด๋ฆฌ ๋๋ํ AI๋ผ๋ ์ฌ์ฉ์๊ฐ ๊ฐ์ง ๋ฌธ์, ์ด๋ฏธ์ง, ๋ฐ์ดํฐ๋ฅผ ์ง์ ๋ฐ์์ ์ฒ๋ฆฌํ ์ ์๋ค๋ฉด ๊ทธ ๋ฅ๋ ฅ์ด ๋ฐ์ชฝ์ง๋ฆฌ๊ฐ ๋๋ ๊ฑฐ์ผ. ๋ง์น ์๋ฆฌ ์ค๋ ฅ์ ์ต๊ณ ์ธ๋ฐ ์ฌ๋ฃ๋ฅผ ๋ฐ์ ์ ์๋ ์
ฐํ ๊ฐ์ ๊ฑฐ์ง. ๐
์ค์ ๋ก ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ AI ๊ด๋ จ ์๋น์ค๋ฅผ ์ ๊ณตํ๋ ๊ฐ๋ฐ์๋ค์ ๋ณด๋ฉด, ํ์ผ์
๋ก๋ ๊ธฐ๋ฅ์ ์ ๊ตฌํํ ์์ด์ ํธ๊ฐ ์ฌ์ฉ์ ๋ง์กฑ๋๊ฐ ํจ์ฌ ๋๋ค๋ ๊ฑธ ์ ์ ์์ด. ์๋ํ๋ฉด ์ฌ์ฉ์๋ค์ ์์ ์ ๋ฐ์ดํฐ๋ฅผ AI์๊ฒ ์ง์ ์ ๋ฌํ๊ณ ์ถ์ด ํ๊ฑฐ๋ !
โข ์ด๋ฏธ์ง ์ฒ๋ฆฌ ์์ด์ ํธ: ์ฌ์ง์ ์ ๋ก๋ํด์ ๊ฐ์ฒด ์ธ์, ์คํ์ผ ๋ณํ
โข ๋ฐ์ดํฐ ๋ถ์ ์์ด์ ํธ: CSV, Excel ํ์ผ๋ก ํต๊ณ ๋ถ์ ๋ฐ ์๊ฐํ
โข ์ฝ๋ ๋ฆฌ๋ทฐ ์์ด์ ํธ: ์์ค์ฝ๋ ํ์ผ์ ์ ๋ก๋ํด์ ๋ฒ๊ทธ ์ฐพ๊ธฐ
โข ์์ฑ ์ฒ๋ฆฌ ์์ด์ ํธ: ์ค๋์ค ํ์ผ์ ํ ์คํธ๋ก ๋ณํ
๐๏ธ ํ์ผ์ ๋ก๋ ์์คํ ์ ๊ธฐ๋ณธ ์ํคํ ์ฒ
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ํ์ผ์
๋ก๋ ์์คํ
์ ์ด๋ป๊ฒ ๊ตฌ์ถํ๋์ง ์์๋ณด์! ๋จผ์ ์ ์ฒด์ ์ธ ๊ทธ๋ฆผ์ ์ดํดํ๋ ๊ฒ ์ค์ํด. ๐จ
ํ์ผ์
๋ก๋ ์์คํ
์ ํฌ๊ฒ ํ๋ก ํธ์๋, ๋ฐฑ์๋, ์คํ ๋ฆฌ์ง, ๊ทธ๋ฆฌ๊ณ AI ์ฒ๋ฆฌ ๋ ์ด์ด๋ก ๊ตฌ์ฑ๋ผ. ๊ฐ ๋ ์ด์ด๊ฐ ์ ๊ธฐ์ ์ผ๋ก ์ฐ๊ฒฐ๋์ด์ผ ์์ ์ ์ธ ์์คํ
์ด ๋ง๋ค์ด์ง์ง!
๐น 1๋จ๊ณ: ํ๋ก ํธ์๋ ํ์ผ ์ ํ ์ธํฐํ์ด์ค
์ฌ์ฉ์๊ฐ ํ์ผ์ ์ ํํ๋ UI๋ถํฐ ์์ํด๋ณด์. ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ HTML5 ํ์ผ ์
๋ ฅ๋ถํฐ ๋๋๊ทธ ์ค ๋๋กญ๊น์ง ๋ค์ํ ๋ฐฉ๋ฒ์ด ์์ด.
๊ธฐ๋ณธ HTML ํ์ผ ์
๋ ฅ:
<input type="file" id="fileInput" accept=".pdf,.docx,.txt,.jpg,.png">
<button onclick="uploadFile()">์
๋ก๋</button>
<script>
async function uploadFile() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
alert('ํ์ผ์ ์ ํํด์ฃผ์ธ์!');
return;
}
// ํ์ผ ํฌ๊ธฐ ์ฒดํฌ (์: 10MB ์ ํ)
if (file.size > 10 * 1024 * 1024) {
alert('ํ์ผ ํฌ๊ธฐ๋ 10MB๋ฅผ ์ด๊ณผํ ์ ์์ต๋๋ค.');
return;
}
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
console.log('์
๋ก๋ ์ฑ๊ณต:', result);
} catch (error) {
console.error('์
๋ก๋ ์คํจ:', error);
}
}
</script>
์ด ์ฝ๋๋ ์ ๋ง ๊ธฐ๋ณธ์ ์ธ ํํ์ผ. ์ค์ ํ๋ก๋์ ํ๊ฒฝ์์๋ ๋ ๋ง์ ๊ฒ์ฆ๊ณผ ์๋ฌ ์ฒ๋ฆฌ๊ฐ ํ์ํ์ง! ๐
๐น 2๋จ๊ณ: ๋๋๊ทธ ์ค ๋๋กญ ๊ธฐ๋ฅ ์ถ๊ฐ
์ฌ์ฉ์ ๊ฒฝํ์ ํ์ธต ์ ๊ทธ๋ ์ด๋ํ๋ ค๋ฉด ๋๋๊ทธ ์ค ๋๋กญ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ ๊ฒ ์ข์. ์์ฆ ์ฌ์ฉ์๋ค์ ์ด๋ฐ ์ง๊ด์ ์ธ ์ธํฐํ์ด์ค๋ฅผ ๊ธฐ๋ํ๊ฑฐ๋ !
<div id="dropZone" style="border: 2px dashed #ccc; padding: 50px; text-align: center;">
ํ์ผ์ ์ฌ๊ธฐ์ ๋๋๊ทธํ๊ฑฐ๋ ํด๋ฆญํ์ธ์
</div>
<script>
const dropZone = document.getElementById('dropZone');
// ๋๋๊ทธ ์ค๋ฒ ์ด๋ฒคํธ
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.style.borderColor = '#4CAF50';
dropZone.style.backgroundColor = '#f0f8ff';
});
// ๋๋๊ทธ ๋ ๋จ ์ด๋ฒคํธ
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.style.borderColor = '#ccc';
dropZone.style.backgroundColor = 'transparent';
});
// ๋๋กญ ์ด๋ฒคํธ
dropZone.addEventListener('drop', async (e) => {
e.preventDefault();
dropZone.style.borderColor = '#ccc';
dropZone.style.backgroundColor = 'transparent';
const files = e.dataTransfer.files;
if (files.length > 0) {
await handleFileUpload(files[0]);
}
});
// ํด๋ฆญ ์ด๋ฒคํธ
dropZone.addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.onchange = (e) => {
if (e.target.files.length > 0) {
handleFileUpload(e.target.files[0]);
}
};
input.click();
});
async function handleFileUpload(file) {
console.log('์
๋ก๋ํ ํ์ผ:', file.name);
// ์ฌ๊ธฐ์ ์ค์ ์
๋ก๋ ๋ก์ง ๊ตฌํ
}
</script>
โ๏ธ ๋ฐฑ์๋ ํ์ผ ์ฒ๋ฆฌ ๊ตฌํ
ํ๋ก ํธ์๋์์ ํ์ผ์ ๋ณด๋์ผ๋ฉด, ์ด์ ๋ฐฑ์๋์์ ๋ฐ์์ ์ฒ๋ฆฌํด์ผ๊ฒ ์ง? ์ฌ๊ธฐ์๋ ์ฌ๋ฌ ํ๋ ์์ํฌ๋ณ๋ก ๊ตฌํ ๋ฐฉ๋ฒ์ ์์๋ณผ๊ฒ! ๐ ๏ธ
๐น Node.js + Express ๊ตฌํ
Node.js๋ AI ์์ด์ ํธ ๋ฐฑ์๋๋ก ์ ๋ง ์ธ๊ธฐ๊ฐ ๋ง์. ๋น๋๊ธฐ ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ ฅํ๊ณ , JavaScript ์ํ๊ณ์ ํ๋ถํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ฉํ ์ ์๊ฑฐ๋ !
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs').promises;
const app = express();
// ํ์ผ ์ ์ฅ ์ค์
const storage = multer.diskStorage({
destination: async (req, file, cb) => {
const uploadDir = './uploads';
try {
await fs.mkdir(uploadDir, { recursive: true });
cb(null, uploadDir);
} catch (error) {
cb(error, null);
}
},
filename: (req, file, cb) => {
// ํ์ผ๋ช
์ค๋ณต ๋ฐฉ์ง๋ฅผ ์ํด ํ์์คํฌํ ์ถ๊ฐ
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
}
});
// ํ์ผ ํํฐ๋ง (ํ์ฉํ ํ์ผ ํ์
์ง์ )
const fileFilter = (req, file, cb) => {
const allowedTypes = /jpeg|jpg|png|gif|pdf|doc|docx|txt|csv/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype);
if (extname && mimetype) {
cb(null, true);
} else {
cb(new Error('์ง์ํ์ง ์๋ ํ์ผ ํ์์
๋๋ค!'), false);
}
};
const upload = multer({
storage: storage,
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB ์ ํ
fileFilter: fileFilter
});
// ํ์ผ ์
๋ก๋ ์๋ํฌ์ธํธ
app.post('/api/upload', upload.single('file'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'ํ์ผ์ด ์์ต๋๋ค.' });
}
const fileInfo = {
originalName: req.file.originalname,
filename: req.file.filename,
path: req.file.path,
size: req.file.size,
mimetype: req.file.mimetype,
uploadedAt: new Date().toISOString()
};
// ์ฌ๊ธฐ์ AI ์์ด์ ํธ์๊ฒ ํ์ผ ์ ๋ณด ์ ๋ฌ
const aiResult = await processWithAI(fileInfo);
res.json({
success: true,
file: fileInfo,
aiResult: aiResult
});
} catch (error) {
console.error('์
๋ก๋ ์๋ฌ:', error);
res.status(500).json({ error: 'ํ์ผ ์
๋ก๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.' });
}
});
// AI ์ฒ๋ฆฌ ํจ์ (์์)
async function processWithAI(fileInfo) {
// ์ค์ ๋ก๋ ์ฌ๊ธฐ์ OpenAI API, Claude API ๋ฑ์ ํธ์ถ
console.log('AI ์ฒ๋ฆฌ ์์:', fileInfo.originalName);
// ํ์ผ ํ์
์ ๋ฐ๋ฅธ ์ฒ๋ฆฌ
if (fileInfo.mimetype.startsWith('image/')) {
return { type: 'image', message: '์ด๋ฏธ์ง ๋ถ์ ์๋ฃ' };
} else if (fileInfo.mimetype === 'application/pdf') {
return { type: 'document', message: 'PDF ๋ฌธ์ ๋ถ์ ์๋ฃ' };
}
return { type: 'unknown', message: 'ํ์ผ ์ฒ๋ฆฌ ์๋ฃ' };
}
app.listen(3000, () => {
console.log('์๋ฒ๊ฐ 3000๋ฒ ํฌํธ์์ ์คํ ์ค์
๋๋ค!');
});
์ด ์ฝ๋๋ Multer ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํด์ ํ์ผ ์ ๋ก๋๋ฅผ ์ฒ๋ฆฌํด. Multer๋ Express์์ ํ์ผ ์ ๋ก๋๋ฅผ ๋ค๋ฃฐ ๋ ์ฌ์ค์ ํ์ค์ด์ผ! ๐ฆ
๐น Python + FastAPI ๊ตฌํ
Python์ AI/ML ์์ ์ ์ต์ ํ๋์ด ์์ด์ AI ์์ด์ ํธ ๋ฐฑ์๋๋ก ๋ง์ด ์ฌ์ฉ๋ผ. FastAPI๋ ๋น ๋ฅด๊ณ ํ๋์ ์ธ ํ๋ ์์ํฌ์ผ!
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.responses import JSONResponse
import shutil
import os
from pathlib import Path
from datetime import datetime
import aiofiles
app = FastAPI()
# ์
๋ก๋ ๋๋ ํ ๋ฆฌ ์ค์
UPLOAD_DIR = Path("./uploads")
UPLOAD_DIR.mkdir(exist_ok=True)
# ํ์ฉํ ํ์ผ ํ์ฅ์
ALLOWED_EXTENSIONS = {'.pdf', '.docx', '.txt', '.jpg', '.jpeg', '.png', '.csv'}
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
def validate_file(filename: str, file_size: int) -> bool:
"""ํ์ผ ์ ํจ์ฑ ๊ฒ์ฌ"""
ext = Path(filename).suffix.lower()
if ext not in ALLOWED_EXTENSIONS:
raise HTTPException(
status_code=400,
detail=f"์ง์ํ์ง ์๋ ํ์ผ ํ์์
๋๋ค. ํ์ฉ: {ALLOWED_EXTENSIONS}"
)
if file_size > MAX_FILE_SIZE:
raise HTTPException(
status_code=400,
detail=f"ํ์ผ ํฌ๊ธฐ๊ฐ ๋๋ฌด ํฝ๋๋ค. ์ต๋: {MAX_FILE_SIZE / 1024 / 1024}MB"
)
return True
@app.post("/api/upload")
async def upload_file(file: UploadFile = File(...)):
"""ํ์ผ ์
๋ก๋ ์๋ํฌ์ธํธ"""
try:
# ํ์ผ ๋ด์ฉ์ ๋ฉ๋ชจ๋ฆฌ์ ์ฝ์ด์ ํฌ๊ธฐ ํ์ธ
contents = await file.read()
file_size = len(contents)
# ์ ํจ์ฑ ๊ฒ์ฌ
validate_file(file.filename, file_size)
# ๊ณ ์ ํ ํ์ผ๋ช
์์ฑ
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
unique_filename = f"{timestamp}_{file.filename}"
file_path = UPLOAD_DIR / unique_filename
# ํ์ผ ์ ์ฅ (๋น๋๊ธฐ)
async with aiofiles.open(file_path, 'wb') as f:
await f.write(contents)
# ํ์ผ ์ ๋ณด ๊ตฌ์ฑ
file_info = {
"original_name": file.filename,
"saved_name": unique_filename,
"path": str(file_path),
"size": file_size,
"content_type": file.content_type,
"uploaded_at": datetime.now().isoformat()
}
# AI ์ฒ๋ฆฌ (์์)
ai_result = await process_with_ai(file_path, file.content_type)
return JSONResponse(content={
"success": True,
"file": file_info,
"ai_result": ai_result
})
except HTTPException as he:
raise he
except Exception as e:
raise HTTPException(status_code=500, detail=f"์
๋ก๋ ์ค ์ค๋ฅ ๋ฐ์: {str(e)}")
async def process_with_ai(file_path: Path, content_type: str):
"""AI ์ฒ๋ฆฌ ํจ์"""
# ์ค์ ๋ก๋ ์ฌ๊ธฐ์ OpenAI, Anthropic ๋ฑ์ API ํธ์ถ
if content_type.startswith('image/'):
# ์ด๋ฏธ์ง ์ฒ๋ฆฌ ๋ก์ง
return {
"type": "image",
"message": "์ด๋ฏธ์ง ๋ถ์์ด ์๋ฃ๋์์ต๋๋ค.",
"analysis": "๊ฐ์ฒด ์ธ์ ๊ฒฐ๊ณผ..."
}
elif content_type == 'application/pdf':
# PDF ์ฒ๋ฆฌ ๋ก์ง
return {
"type": "document",
"message": "๋ฌธ์ ๋ถ์์ด ์๋ฃ๋์์ต๋๋ค.",
"summary": "๋ฌธ์ ์์ฝ..."
}
return {
"type": "general",
"message": "ํ์ผ ์ฒ๋ฆฌ๊ฐ ์๋ฃ๋์์ต๋๋ค."
}
@app.get("/api/files")
async def list_files():
"""์
๋ก๋๋ ํ์ผ ๋ชฉ๋ก ์กฐํ"""
files = []
for file_path in UPLOAD_DIR.iterdir():
if file_path.is_file():
stat = file_path.stat()
files.append({
"name": file_path.name,
"size": stat.st_size,
"created": datetime.fromtimestamp(stat.st_ctime).isoformat()
})
return {"files": files}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
FastAPI๋ ์๋์ผ๋ก API ๋ฌธ์๋ฅผ ์์ฑํด์ฃผ๊ณ , ํ์ ํํ ์ ํตํ ๊ฒ์ฆ๋ ํด์ค์ ์ ๋ง ํธ๋ฆฌํด! ๐โจ
โข ํ์ผ ํ์ฅ์๋ง ๋ฏฟ์ง ๋ง๊ณ ์ค์ MIME ํ์ ๋ ํ์ธํ๊ธฐ
โข ํ์ผ ํฌ๊ธฐ ์ ํ ๋ฐ๋์ ์ค์ ํ๊ธฐ
โข ์ ๋ก๋๋ ํ์ผ์ ์น ๋ฃจํธ ๋ฐ์ ์ ์ฅํ๊ธฐ
โข ํ์ผ๋ช ์ ํน์๋ฌธ์๋ ๊ฒฝ๋ก ์กฐ์ ์๋ ์ฐจ๋จํ๊ธฐ
โข ๋ฐ์ด๋ฌ์ค ์ค์บ ๊ณ ๋ คํ๊ธฐ
โ๏ธ ํด๋ผ์ฐ๋ ์คํ ๋ฆฌ์ง ์ฐ๋ํ๊ธฐ
๋ก์ปฌ ๋์คํฌ์ ํ์ผ์ ์ ์ฅํ๋ ๊ฒ๋ ์ข์ง๋ง, ํ๋ก๋์
ํ๊ฒฝ์์๋ ํด๋ผ์ฐ๋ ์คํ ๋ฆฌ์ง๋ฅผ ์ฌ์ฉํ๋ ๊ฒ ํจ์ฌ ์์ ์ ์ด์ผ! ํ์ฅ์ฑ๋ ์ข๊ณ , ๋ฐฑ์
๋ ์๋์ผ๋ก ๋๊ฑฐ๋ . ๐ฅ๏ธ
๊ฐ์ฅ ๋ง์ด ์ฌ์ฉ๋๋ ํด๋ผ์ฐ๋ ์คํ ๋ฆฌ์ง ์๋น์ค๋ค์ ์์๋ณด์!
๐น AWS S3 ์ฐ๋
AWS S3๋ ๊ฐ์ฅ ๋์ค์ ์ธ ๊ฐ์ฒด ์คํ ๋ฆฌ์ง ์๋น์ค์ผ. ์์ ์ฑ๊ณผ ํ์ฅ์ฑ์ด ๋ฐ์ด๋์ง!
// Node.js + AWS SDK v3
const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3");
const fs = require('fs');
// S3 ํด๋ผ์ด์ธํธ ์ด๊ธฐํ
const s3Client = new S3Client({
region: "ap-northeast-2", // ์์ธ ๋ฆฌ์
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
}
});
async function uploadToS3(file, bucketName) {
try {
const fileStream = fs.createReadStream(file.path);
const uploadParams = {
Bucket: bucketName,
Key: `uploads/${Date.now()}-${file.originalname}`,
Body: fileStream,
ContentType: file.mimetype,
// ํ์ผ์ ๊ณต๊ฐ๋ก ์ค์ ํ๋ ค๋ฉด (์ ํ์ฌํญ)
// ACL: 'public-read'
};
const command = new PutObjectCommand(uploadParams);
const result = await s3Client.send(command);
// S3 URL ์์ฑ
const fileUrl = `https://${bucketName}.s3.ap-northeast-2.amazonaws.com/${uploadParams.Key}`;
return {
success: true,
url: fileUrl,
key: uploadParams.Key,
etag: result.ETag
};
} catch (error) {
console.error('S3 ์
๋ก๋ ์คํจ:', error);
throw error;
}
}
// Express ๋ผ์ฐํธ์์ ์ฌ์ฉ
app.post('/api/upload-to-s3', upload.single('file'), async (req, res) => {
try {
const s3Result = await uploadToS3(req.file, 'my-ai-agent-bucket');
// ๋ก์ปฌ ์์ ํ์ผ ์ญ์
await fs.promises.unlink(req.file.path);
res.json({
success: true,
file: s3Result
});
} catch (error) {
res.status(500).json({ error: '์
๋ก๋ ์คํจ' });
}
});
๐น Google Cloud Storage ์ฐ๋
Google Cloud Storage๋ ํ๋ฅญํ ์ ํ์ง์ผ. ํนํ Google AI ์๋น์ค๋ค๊ณผ ์ฐ๋ํ ๋ ํธ๋ฆฌํด!
// Node.js + Google Cloud Storage
const { Storage } = require('@google-cloud/storage');
const path = require('path');
// GCS ํด๋ผ์ด์ธํธ ์ด๊ธฐํ
const storage = new Storage({
projectId: 'your-project-id',
keyFilename: './service-account-key.json'
});
const bucketName = 'my-ai-agent-bucket';
const bucket = storage.bucket(bucketName);
async function uploadToGCS(file) {
try {
const blob = bucket.file(`uploads/${Date.now()}-${file.originalname}`);
const blobStream = blob.createWriteStream({
resumable: false,
metadata: {
contentType: file.mimetype
}
});
return new Promise((resolve, reject) => {
blobStream.on('error', (error) => {
reject(error);
});
blobStream.on('finish', async () => {
// ํ์ผ์ ๊ณต๊ฐ๋ก ์ค์ (์ ํ์ฌํญ)
// await blob.makePublic();
const publicUrl = `https://storage.googleapis.com/${bucketName}/${blob.name}`;
resolve({
success: true,
url: publicUrl,
filename: blob.name
});
});
blobStream.end(file.buffer);
});
} catch (error) {
console.error('GCS ์
๋ก๋ ์คํจ:', error);
throw error;
}
}
// Express ๋ผ์ฐํธ
app.post('/api/upload-to-gcs', upload.single('file'), async (req, res) => {
try {
const gcsResult = await uploadToGCS(req.file);
res.json({
success: true,
file: gcsResult
});
} catch (error) {
res.status(500).json({ error: '์
๋ก๋ ์คํจ' });
}
});
โข Google Cloud Storage: Google AI ์๋น์ค์ ํตํฉ ์ฉ์ด, ๊ฐ๋จํ API
โข Azure Blob Storage: Microsoft ์ํ๊ณ์ ํตํฉ, ์ํฐํ๋ผ์ด์ฆ ์นํ์
โข Cloudflare R2: S3 ํธํ API, ๋ฌด๋ฃ egress(๋ค์ด๋ก๋ ํธ๋ํฝ)
๊ฐ๊ฒฉ๊ณผ ๊ธฐ๋ฅ์ ๋น๊ตํด์ ํ๋ก์ ํธ์ ๋ง๋ ๊ฑธ ์ ํํ๋ฉด ๋ผ! ๐ฐ
๐ค AI ์์ด์ ํธ์ ํ์ผ ์ฐ๋ํ๊ธฐ
์, ์ด์ ๊ฐ์ฅ ์ค์ํ ๋ถ๋ถ์ด์ผ! ์ ๋ก๋๋ ํ์ผ์ ์ค์ ๋ก AI ์์ด์ ํธ๊ฐ ์ฒ๋ฆฌํ๋๋ก ๋ง๋๋ ๊ฑฐ์ง. ์ฌ๊ธฐ์๋ ์ฃผ์ AI API๋ค๊ณผ์ ์ฐ๋ ๋ฐฉ๋ฒ์ ์์๋ณผ๊ฒ! ๐ง โจ
๐น OpenAI API์ ํ์ผ ์ฐ๋
OpenAI์ GPT-4 Vision์ด๋ Assistants API๋ฅผ ์ฌ์ฉํ๋ฉด ํ์ผ์ ์ง์ ์ฒ๋ฆฌํ ์ ์์ด!
const OpenAI = require('openai');
const fs = require('fs');
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY
});
// ์ด๋ฏธ์ง ๋ถ์ (GPT-4 Vision)
async function analyzeImage(imagePath) {
try {
const imageBuffer = fs.readFileSync(imagePath);
const base64Image = imageBuffer.toString('base64');
const response = await openai.chat.completions.create({
model: "gpt-4-vision-preview",
messages: [
{
role: "user",
content: [
{
type: "text",
text: "์ด ์ด๋ฏธ์ง๋ฅผ ์์ธํ ๋ถ์ํด์ฃผ์ธ์."
},
{
type: "image_url",
image_url: {
url: `data:image/jpeg;base64,${base64Image}`
}
}
]
}
],
max_tokens: 1000
});
return {
analysis: response.choices[0].message.content,
usage: response.usage
};
} catch (error) {
console.error('์ด๋ฏธ์ง ๋ถ์ ์คํจ:', error);
throw error;
}
}
// ๋ฌธ์ ์ฒ๋ฆฌ (Assistants API)
async function processDocument(filePath, question) {
try {
// 1. ํ์ผ ์
๋ก๋
const file = await openai.files.create({
file: fs.createReadStream(filePath),
purpose: 'assistants'
});
// 2. Assistant ์์ฑ
const assistant = await openai.beta.assistants.create({
name: "๋ฌธ์ ๋ถ์ ์ด์์คํดํธ",
instructions: "๋น์ ์ ๋ฌธ์๋ฅผ ๋ถ์ํ๊ณ ์ง๋ฌธ์ ๋ตํ๋ ์ ๋ฌธ๊ฐ์
๋๋ค.",
model: "gpt-4-1106-preview",
tools: [{ type: "retrieval" }],
file_ids: [file.id]
});
// 3. Thread ์์ฑ
const thread = await openai.beta.threads.create();
// 4. ๋ฉ์์ง ์ถ๊ฐ
await openai.beta.threads.messages.create(thread.id, {
role: "user",
content: question
});
// 5. Run ์คํ
const run = await openai.beta.threads.runs.create(thread.id, {
assistant_id: assistant.id
});
// 6. ์๋ฃ ๋๊ธฐ
let runStatus = await openai.beta.threads.runs.retrieve(thread.id, run.id);
while (runStatus.status !== 'completed') {
await new Promise(resolve => setTimeout(resolve, 1000));
runStatus = await openai.beta.threads.runs.retrieve(thread.id, run.id);
if (runStatus.status === 'failed') {
throw new Error('Run failed');
}
}
// 7. ์๋ต ๊ฐ์ ธ์ค๊ธฐ
const messages = await openai.beta.threads.messages.list(thread.id);
const lastMessage = messages.data[0];
return {
answer: lastMessage.content[0].text.value,
fileId: file.id,
assistantId: assistant.id
};
} catch (error) {
console.error('๋ฌธ์ ์ฒ๋ฆฌ ์คํจ:', error);
throw error;
}
}
// Express ๋ผ์ฐํธ
app.post('/api/analyze-file', upload.single('file'), async (req, res) => {
try {
const file = req.file;
const question = req.body.question || "์ด ํ์ผ์ ๋ด์ฉ์ ์์ฝํด์ฃผ์ธ์.";
let result;
if (file.mimetype.startsWith('image/')) {
result = await analyzeImage(file.path);
} else {
result = await processDocument(file.path, question);
}
// ์์ ํ์ผ ์ญ์
await fs.promises.unlink(file.path);
res.json({
success: true,
result: result
});
} catch (error) {
res.status(500).json({ error: '๋ถ์ ์คํจ' });
}
});
๐น Claude API์ ํ์ผ ์ฐ๋
Anthropic์ Claude๋ ์ด๋ฏธ์ง์ ๋ฌธ์๋ฅผ ์ฒ๋ฆฌํ ์ ์์ด. ํนํ ๊ธด ๋ฌธ๋งฅ ์ฒ๋ฆฌ์ ๊ฐ์ ์ด ์์ง!
const Anthropic = require('@anthropic-ai/sdk');
const fs = require('fs');
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY
});
// ์ด๋ฏธ์ง ๋ถ์
async function analyzeImageWithClaude(imagePath) {
try {
const imageBuffer = fs.readFileSync(imagePath);
const base64Image = imageBuffer.toString('base64');
// ์ด๋ฏธ์ง ํ์
ํ์ธ
const imageType = imagePath.endsWith('.png') ? 'image/png' : 'image/jpeg';
const message = await anthropic.messages.create({
model: "claude-3-opus-20240229",
max_tokens: 1024,
messages: [
{
role: "user",
content: [
{
type: "image",
source: {
type: "base64",
media_type: imageType,
data: base64Image
}
},
{
type: "text",
text: "์ด ์ด๋ฏธ์ง์ ๋ํด ์์ธํ ์ค๋ช
ํด์ฃผ์ธ์."
}
]
}
]
});
return {
analysis: message.content[0].text,
usage: message.usage
};
} catch (error) {
console.error('Claude ์ด๋ฏธ์ง ๋ถ์ ์คํจ:', error);
throw error;
}
}
// ํ
์คํธ ๋ฌธ์ ์ฒ๋ฆฌ
async function processTextWithClaude(textContent, question) {
try {
const message = await anthropic.messages.create({
model: "claude-3-opus-20240229",
max_tokens: 2048,
messages: [
{
role: "user",
content: `๋ค์ ๋ฌธ์๋ฅผ ์ฝ๊ณ ์ง๋ฌธ์ ๋ตํด์ฃผ์ธ์.\n\n๋ฌธ์:\n${textContent}\n\n์ง๋ฌธ: ${question}`
}
]
});
return {
answer: message.content[0].text,
usage: message.usage
};
} catch (error) {
console.error('Claude ๋ฌธ์ ์ฒ๋ฆฌ ์คํจ:', error);
throw error;
}
}
// PDF๋ DOCX๋ฅผ ํ
์คํธ๋ก ๋ณํํ๋ ํฌํผ ํจ์๋ค
const pdfParse = require('pdf-parse');
const mammoth = require('mammoth');
async function extractTextFromPDF(pdfPath) {
const dataBuffer = fs.readFileSync(pdfPath);
const data = await pdfParse(dataBuffer);
return data.text;
}
async function extractTextFromDOCX(docxPath) {
const result = await mammoth.extractRawText({ path: docxPath });
return result.value;
}
// ํตํฉ ํ์ผ ์ฒ๋ฆฌ ํจ์
async function processFileWithClaude(file, question) {
try {
let textContent;
if (file.mimetype.startsWith('image/')) {
return await analyzeImageWithClaude(file.path);
} else if (file.mimetype === 'application/pdf') {
textContent = await extractTextFromPDF(file.path);
} else if (file.mimetype === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
textContent = await extractTextFromDOCX(file.path);
} else if (file.mimetype === 'text/plain') {
textContent = fs.readFileSync(file.path, 'utf-8');
} else {
throw new Error('์ง์ํ์ง ์๋ ํ์ผ ํ์์
๋๋ค.');
}
return await processTextWithClaude(textContent, question);
} catch (error) {
console.error('ํ์ผ ์ฒ๋ฆฌ ์คํจ:', error);
throw error;
}
}
โข Word (DOCX): mammoth, docx
โข Excel: xlsx, exceljs
โข CSV: csv-parser, papaparse
โข ์ด๋ฏธ์ง OCR: tesseract.js, Google Vision API
โข ์ค๋์ค: OpenAI Whisper API, Google Speech-to-Text
โก ๋์ฉ๋ ํ์ผ ์ฒ๋ฆฌ ์ต์ ํ
์์ ํ์ผ์ ์ฝ๊ฒ ์ฒ๋ฆฌํ ์ ์์ง๋ง, ๋์ฉ๋ ํ์ผ์ ํน๋ณํ ์ฒ๋ฆฌ๊ฐ ํ์ํด! ๋ฉ๋ชจ๋ฆฌ ์ค๋ฒํ๋ก์ฐ๋ ํ์์์ ๊ฐ์ ๋ฌธ์ ๋ฅผ ํผํ๋ ค๋ฉด ์คํธ๋ฆฌ๋ฐ๊ณผ ์ฒญํฌ ์ฒ๋ฆฌ๋ฅผ ํ์ฉํด์ผ ํด. ๐
๐น ์ฒญํฌ ์ ๋ก๋ ๊ตฌํ
ํฐ ํ์ผ์ ์์ ์กฐ๊ฐ์ผ๋ก ๋๋ ์ ์ ๋ก๋ํ๋ ๋ฐฉ์์ด์ผ. ๋คํธ์ํฌ ์ค๋ฅ๊ฐ ๋ฐ์ํด๋ ์ฒ์๋ถํฐ ๋ค์ ์์ํ ํ์๊ฐ ์์ด!
// ํ๋ก ํธ์๋: ์ฒญํฌ ์
๋ก๋
class ChunkedUploader {
constructor(file, chunkSize = 1024 * 1024) { // 1MB ์ฒญํฌ
this.file = file;
this.chunkSize = chunkSize;
this.totalChunks = Math.ceil(file.size / chunkSize);
this.uploadedChunks = 0;
}
async upload(onProgress) {
const uploadId = this.generateUploadId();
for (let chunkIndex = 0; chunkIndex < this.totalChunks; chunkIndex++) {
const start = chunkIndex * this.chunkSize;
const end = Math.min(start + this.chunkSize, this.file.size);
const chunk = this.file.slice(start, end);
await this.uploadChunk(chunk, chunkIndex, uploadId);
this.uploadedChunks++;
const progress = (this.uploadedChunks / this.totalChunks) * 100;
if (onProgress) {
onProgress(progress);
}
}
// ๋ชจ๋ ์ฒญํฌ ์
๋ก๋ ์๋ฃ ํ ๋ณํฉ ์์ฒญ
return await this.completeUpload(uploadId);
}
async uploadChunk(chunk, chunkIndex, uploadId) {
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('chunkIndex', chunkIndex);
formData.append('totalChunks', this.totalChunks);
formData.append('uploadId', uploadId);
formData.append('filename', this.file.name);
const response = await fetch('/api/upload-chunk', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`์ฒญํฌ ${chunkIndex} ์
๋ก๋ ์คํจ`);
}
return await response.json();
}
async completeUpload(uploadId) {
const response = await fetch('/api/complete-upload', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
uploadId: uploadId,
filename: this.file.name,
totalChunks: this.totalChunks
})
});
return await response.json();
}
generateUploadId() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
}
// ์ฌ์ฉ ์์
async function handleLargeFileUpload(file) {
const uploader = new ChunkedUploader(file);
try {
const result = await uploader.upload((progress) => {
console.log(`์
๋ก๋ ์งํ๋ฅ : ${progress.toFixed(2)}%`);
// ํ๋ก๊ทธ๋ ์ค ๋ฐ ์
๋ฐ์ดํธ
updateProgressBar(progress);
});
console.log('์
๋ก๋ ์๋ฃ:', result);
} catch (error) {
console.error('์
๋ก๋ ์คํจ:', error);
}
}
// ๋ฐฑ์๋: ์ฒญํฌ ์์ ๋ฐ ๋ณํฉ
const express = require('express');
const fs = require('fs').promises;
const path = require('path');
const app = express();
const CHUNK_DIR = './chunks';
const UPLOAD_DIR = './uploads';
// ์ฒญํฌ ์
๋ก๋ ์๋ํฌ์ธํธ
app.post('/api/upload-chunk', upload.single('chunk'), async (req, res) => {
try {
const { chunkIndex, totalChunks, uploadId, filename } = req.body;
// ์ฒญํฌ ๋๋ ํ ๋ฆฌ ์์ฑ
const uploadChunkDir = path.join(CHUNK_DIR, uploadId);
await fs.mkdir(uploadChunkDir, { recursive: true });
// ์ฒญํฌ ํ์ผ ์ ์ฅ
const chunkPath = path.join(uploadChunkDir, `chunk-${chunkIndex}`);
await fs.rename(req.file.path, chunkPath);
res.json({
success: true,
chunkIndex: parseInt(chunkIndex),
message: `์ฒญํฌ ${chunkIndex}/${totalChunks} ์
๋ก๋ ์๋ฃ`
});
} catch (error) {
console.error('์ฒญํฌ ์
๋ก๋ ์๋ฌ:', error);
res.status(500).json({ error: '์ฒญํฌ ์
๋ก๋ ์คํจ' });
}
});
// ์
๋ก๋ ์๋ฃ ๋ฐ ๋ณํฉ ์๋ํฌ์ธํธ
app.post('/api/complete-upload', express.json(), async (req, res) => {
try {
const { uploadId, filename, totalChunks } = req.body;
const uploadChunkDir = path.join(CHUNK_DIR, uploadId);
const finalPath = path.join(UPLOAD_DIR, `${Date.now()}-${filename}`);
// ์ต์ข
ํ์ผ ์์ฑ
const writeStream = require('fs').createWriteStream(finalPath);
// ๋ชจ๋ ์ฒญํฌ๋ฅผ ์์๋๋ก ๋ณํฉ
for (let i = 0; i < totalChunks; i++) {
const chunkPath = path.join(uploadChunkDir, `chunk-${i}`);
const chunkBuffer = await fs.readFile(chunkPath);
writeStream.write(chunkBuffer);
}
writeStream.end();
// ๋ณํฉ ์๋ฃ ๋๊ธฐ
await new Promise((resolve, reject) => {
writeStream.on('finish', resolve);
writeStream.on('error', reject);
});
// ์ฒญํฌ ๋๋ ํ ๋ฆฌ ์ญ์
await fs.rm(uploadChunkDir, { recursive: true });
// ํ์ผ ์ ๋ณด
const stats = await fs.stat(finalPath);
res.json({
success: true,
file: {
path: finalPath,
size: stats.size,
filename: filename
},
message: 'ํ์ผ ์
๋ก๋ ๋ฐ ๋ณํฉ ์๋ฃ'
});
} catch (error) {
console.error('๋ณํฉ ์๋ฌ:', error);
res.status(500).json({ error: 'ํ์ผ ๋ณํฉ ์คํจ' });
}
});
๐น ์คํธ๋ฆฌ๋ฐ ์ฒ๋ฆฌ
ํ์ผ์ ๋ฉ๋ชจ๋ฆฌ์ ์ ๋ถ ๋ก๋ํ์ง ์๊ณ ์คํธ๋ฆผ์ผ๋ก ์ฒ๋ฆฌํ๋ฉด ๋ฉ๋ชจ๋ฆฌ๋ฅผ ์ ์ฝํ ์ ์์ด!
const fs = require('fs');
const readline = require('readline');
// ๋์ฉ๋ ํ
์คํธ ํ์ผ์ ์ค ๋จ์๋ก ์ฒ๋ฆฌ
async function processLargeTextFile(filePath, processLine) {
const fileStream = fs.createReadStream(filePath);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
let lineNumber = 0;
for await (const line of rl) {
lineNumber++;
await processLine(line, lineNumber);
}
return lineNumber;
}
// ์ฌ์ฉ ์์: CSV ํ์ผ ์ฒ๋ฆฌ
async function processCsvFile(filePath) {
const results = [];
let header = null;
await processLargeTextFile(filePath, async (line, lineNumber) => {
if (lineNumber === 1) {
header = line.split(',');
} else {
const values = line.split(',');
const row = {};
header.forEach((key, index) => {
row[key] = values[index];
});
results.push(row);
// ๋ฐฐ์น ์ฒ๋ฆฌ (1000์ค๋ง๋ค)
if (results.length >= 1000) {
await processBatch(results);
results.length = 0; // ๋ฐฐ์ด ๋น์ฐ๊ธฐ
}
}
});
// ๋จ์ ๋ฐ์ดํฐ ์ฒ๋ฆฌ
if (results.length > 0) {
await processBatch(results);
}
}
async function processBatch(batch) {
// AI ์์ด์ ํธ๋ก ๋ฐฐ์น ์ ์ก
console.log(`${batch.length}๊ฐ ํ ์ฒ๋ฆฌ ์ค...`);
// ์ค์ AI ์ฒ๋ฆฌ ๋ก์ง
}
โข ์ ๋ก๋ ์งํ๋ฅ ํ์๋ก ์ฌ์ฉ์ ๊ฒฝํ ๊ฐ์
โข ํ์์์ ์ค์ ์ ๋๋ํ๊ฒ (๋์ฉ๋ ํ์ผ์ ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆผ)
โข ์์ ํ์ผ์ ์ฒ๋ฆฌ ํ ๋ฐ๋์ ์ญ์
โข ๋ฐฑ๊ทธ๋ผ์ด๋ ์์ ํ ํ์ฉ (Redis Queue, Bull ๋ฑ)
๐ ๋ณด์๊ณผ ๊ฒ์ฆ
ํ์ผ ์ ๋ก๋ ๊ธฐ๋ฅ์ ๋ณด์ ์ทจ์ฝ์ ์ ์ฃผ์ ์์ธ์ด ๋ ์ ์์ด. ์ฒ ์ ํ ๊ฒ์ฆ๊ณผ ๋ณด์ ์กฐ์น๊ฐ ํ์์ผ! ๐ก๏ธ
๐น ํ์ผ ํ์ ๊ฒ์ฆ
ํ์ฅ์๋ง ๋ฏฟ์ผ๋ฉด ์ ๋ผ! ์ค์ ํ์ผ ๋ด์ฉ์ ๊ฒ์ฌํด์ผ ํด.
const fileType = require('file-type');
const fs = require('fs').promises;
async function validateFileType(filePath, allowedTypes) {
try {
// ํ์ผ์ ์ค์ ํ์
ํ์ธ (๋งค์ง ๋๋ฒ ๊ธฐ๋ฐ)
const buffer = await fs.readFile(filePath);
const type = await fileType.fromBuffer(buffer);
if (!type) {
throw new Error('ํ์ผ ํ์
์ ํ์ธํ ์ ์์ต๋๋ค.');
}
// ํ์ฉ๋ ํ์
์ธ์ง ํ์ธ
if (!allowedTypes.includes(type.mime)) {
throw new Error(`ํ์ฉ๋์ง ์์ ํ์ผ ํ์
: ${type.mime}`);
}
return {
valid: true,
mime: type.mime,
ext: type.ext
};
} catch (error) {
return {
valid: false,
error: error.message
};
}
}
// ์ฌ์ฉ ์์
app.post('/api/secure-upload', upload.single('file'), async (req, res) => {
try {
const allowedTypes = [
'image/jpeg',
'image/png',
'application/pdf',
'text/plain'
];
const validation = await validateFileType(req.file.path, allowedTypes);
if (!validation.valid) {
await fs.unlink(req.file.path); // ํ์ผ ์ญ์
return res.status(400).json({ error: validation.error });
}
// ํ์ผ ์ฒ๋ฆฌ ๊ณ์...
res.json({ success: true, fileType: validation.mime });
} catch (error) {
res.status(500).json({ error: '์
๋ก๋ ์คํจ' });
}
});
๐น ํ์ผ๋ช ์๋ํ์ด์ง
์ ์์ ์ธ ํ์ผ๋ช ์ผ๋ก ์ธํ ๊ฒฝ๋ก ์กฐ์ ๊ณต๊ฒฉ์ ๋ฐฉ์งํด์ผ ํด!
const path = require('path');
const crypto = require('crypto');
function sanitizeFilename(filename) {
// 1. ๊ฒฝ๋ก ๊ตฌ๋ถ์ ์ ๊ฑฐ
let sanitized = filename.replace(/[\/\\]/g, '');
// 2. ํน์๋ฌธ์ ์ ๊ฑฐ (์ํ๋ฒณ, ์ซ์, ์ , ํ์ดํ, ์ธ๋์ค์ฝ์ด๋ง ํ์ฉ)
sanitized = sanitized.replace(/[^a-zA-Z0-9._-]/g, '_');
// 3. ์ฐ์๋ ์ ์ ๊ฑฐ (.. ๊ณต๊ฒฉ ๋ฐฉ์ง)
sanitized = sanitized.replace(/\.{2,}/g, '.');
// 4. ํ์ผ๋ช
์ด ์ ์ผ๋ก ์์ํ์ง ์๋๋ก
sanitized = sanitized.replace(/^\.+/, '');
// 5. ํ์ผ๋ช
๊ธธ์ด ์ ํ (255์)
if (sanitized.length > 255) {
const ext = path.extname(sanitized);
const name = path.basename(sanitized, ext);
sanitized = name.substring(0, 255 - ext.length) + ext;
}
return sanitized;
}
function generateSecureFilename(originalFilename) {
const ext = path.extname(originalFilename);
const hash = crypto.randomBytes(16).toString('hex');
const timestamp = Date.now();
return `${timestamp}-${hash}${ext}`;
}
// ์ฌ์ฉ ์์
app.post('/api/safe-upload', upload.single('file'), async (req, res) => {
try {
const originalName = req.file.originalname;
const sanitizedName = sanitizeFilename(originalName);
const secureName = generateSecureFilename(sanitizedName);
const newPath = path.join(UPLOAD_DIR, secureName);
await fs.rename(req.file.path, newPath);
res.json({
success: true,
originalName: originalName,
savedName: secureName
});
} catch (error) {
res.status(500).json({ error: '์
๋ก๋ ์คํจ' });
}
});
๐น ๋ฐ์ด๋ฌ์ค ์ค์บ
ํ๋ก๋์ ํ๊ฒฝ์์๋ ์ ๋ก๋๋ ํ์ผ์ ๋ํ ๋ฐ์ด๋ฌ์ค ์ค์บ์ ๊ณ ๋ คํด์ผ ํด!
const NodeClam = require('clamscan');
// ClamAV ์ด๊ธฐํ
const clamscan = new NodeClam().init({
removeInfected: true, // ๊ฐ์ผ๋ ํ์ผ ์๋ ์ญ์
quarantineInfected: './quarantine/', // ๊ฒฉ๋ฆฌ ๋๋ ํ ๋ฆฌ
scanLog: './scan.log',
debugMode: false,
clamdscan: {
host: 'localhost',
port: 3310
}
});
async function scanFile(filePath) {
try {
const scanner = await clamscan;
const { isInfected, viruses } = await scanner.isInfected(filePath);
return {
safe: !isInfected,
viruses: viruses
};
} catch (error) {
console.error('์ค์บ ์๋ฌ:', error);
throw error;
}
}
// ์
๋ก๋ ๋ฏธ๋ค์จ์ด์ ํตํฉ
app.post('/api/scan-upload', upload.single('file'), async (req, res) => {
try {
// ๋ฐ์ด๋ฌ์ค ์ค์บ
const scanResult = await scanFile(req.file.path);
if (!scanResult.safe) {
return res.status(400).json({
error: '์
์ฑ ํ์ผ์ด ๊ฐ์ง๋์์ต๋๋ค.',
viruses: scanResult.viruses
});
}
// ์์ ํ ํ์ผ, ์ฒ๋ฆฌ ๊ณ์...
res.json({ success: true, message: 'ํ์ผ์ด ์์ ํฉ๋๋ค.' });
} catch (error) {
res.status(500).json({ error: '์ค์บ ์คํจ' });
}
});
- โ ํ์ผ ํฌ๊ธฐ ์ ํ ์ค์
- โ ํ์ฉ๋ ํ์ผ ํ์ ๋ง ์ ๋ก๋ ๊ฐ๋ฅ
- โ ์ค์ ํ์ผ ๋ด์ฉ ๊ฒ์ฆ (๋งค์ง ๋๋ฒ)
- โ ํ์ผ๋ช ์๋ํ์ด์ง
- โ ์ ๋ก๋ ๋๋ ํ ๋ฆฌ๋ ์คํ ๊ถํ ์ ๊ฑฐ
- โ ์ ๋ก๋๋ ํ์ผ์ ์น ๋ฃจํธ ๋ฐ์ ์ ์ฅ
- โ ์ฌ์ฉ์๋ณ ์ ๋ก๋ ํ์ ์ ํ (Rate Limiting)
- โ ๋ฐ์ด๋ฌ์ค ์ค์บ (๊ฐ๋ฅํ๋ฉด)
- โ HTTPS ์ฌ์ฉ ํ์
๐ ์งํ๋ฅ ํ์์ ์ฌ์ฉ์ ๊ฒฝํ
ํ์ผ ์ ๋ก๋๋ ์๊ฐ์ด ๊ฑธ๋ฆด ์ ์์ด์, ์ฌ์ฉ์์๊ฒ ์งํ ์ํฉ์ ๋ณด์ฌ์ฃผ๋ ๊ฒ ์ ๋ง ์ค์ํด! ์ข์ UX๋ ์ฌ์ฉ์ ๋ง์กฑ๋๋ฅผ ํฌ๊ฒ ๋์ฌ์ค. ๐จโจ
๐น ์ค์๊ฐ ์งํ๋ฅ ํ์
XMLHttpRequest๋ Fetch API์ progress ์ด๋ฒคํธ๋ฅผ ํ์ฉํ๋ฉด ์ค์๊ฐ ์งํ๋ฅ ์ ํ์ํ ์ ์์ด!
// ํ๋ก ํธ์๋: ์งํ๋ฅ ํ์
class FileUploadWithProgress {
constructor(fileInputId, progressBarId) {
this.fileInput = document.getElementById(fileInputId);
this.progressBar = document.getElementById(progressBarId);
}
async upload(url) {
const file = this.fileInput.files[0];
if (!file) {
alert('ํ์ผ์ ์ ํํด์ฃผ์ธ์!');
return;
}
const formData = new FormData();
formData.append('file', file);
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
// ์
๋ก๋ ์งํ๋ฅ ์ด๋ฒคํธ
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
this.updateProgress(percentComplete);
}
});
// ์๋ฃ ์ด๋ฒคํธ
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
resolve(response);
} else {
reject(new Error('์
๋ก๋ ์คํจ'));
}
});
// ์๋ฌ ์ด๋ฒคํธ
xhr.addEventListener('error', () => {
reject(new Error('๋คํธ์ํฌ ์ค๋ฅ'));
});
xhr.open('POST', url);
xhr.send(formData);
});
}
updateProgress(percent) {
this.progressBar.style.width = `${percent}%`;
this.progressBar.textContent = `${Math.round(percent)}%`;
// ์๋ฃ ์ ์์ ๋ณ๊ฒฝ
if (percent >= 100) {
this.progressBar.style.backgroundColor = '#4CAF50';
}
}
}
// HTML
/*
<div style="width: 100%; background: #f0f0f0; border-radius: 10px; overflow: hidden;">
<div id="progressBar" style="width: 0%; height: 30px; background: #2196F3;
text-align: center; line-height: 30px; color: white; transition: width 0.3s;">
0%
</div>
</div>
<input type="file" id="fileInput">
<button onclick="startUpload()">์
๋ก๋</button>
*/
async function startUpload() {
const uploader = new FileUploadWithProgress('fileInput', 'progressBar');
try {
const result = await uploader.upload('/api/upload');
console.log('์
๋ก๋ ์ฑ๊ณต:', result);
alert('ํ์ผ ์
๋ก๋๊ฐ ์๋ฃ๋์์ต๋๋ค!');
} catch (error) {
console.error('์
๋ก๋ ์คํจ:', error);
alert('์
๋ก๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.');
}
}
๐น ๋ฉํฐ ํ์ผ ์ ๋ก๋
์ฌ๋ฌ ํ์ผ์ ๋์์ ์ ๋ก๋ํ๊ณ ๊ฐ๊ฐ์ ์งํ๋ฅ ์ ํ์ํ๋ ๊ธฐ๋ฅ๋ ๊ตฌํํด๋ณด์!
class MultiFileUploader {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.uploads = [];
}
async uploadFiles(files) {
const uploadPromises = Array.from(files).map((file, index) => {
return this.uploadSingleFile(file, index);
});
try {
const results = await Promise.all(uploadPromises);
return results;
} catch (error) {
console.error('์ผ๋ถ ํ์ผ ์
๋ก๋ ์คํจ:', error);
throw error;
}
}
async uploadSingleFile(file, index) {
// ์งํ๋ฅ UI ์์ฑ
const uploadItem = this.createUploadItem(file, index);
this.container.appendChild(uploadItem);
const formData = new FormData();
formData.append('file', file);
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percent = (e.loaded / e.total) * 100;
this.updateUploadItem(index, percent, 'uploading');
}
});
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
this.updateUploadItem(index, 100, 'complete');
resolve(JSON.parse(xhr.responseText));
} else {
this.updateUploadItem(index, 0, 'error');
reject(new Error('์
๋ก๋ ์คํจ'));
}
});
xhr.addEventListener('error', () => {
this.updateUploadItem(index, 0, 'error');
reject(new Error('๋คํธ์ํฌ ์ค๋ฅ'));
});
xhr.open('POST', '/api/upload');
xhr.send(formData);
});
}
createUploadItem(file, index) {
const item = document.createElement('div');
item.id = `upload-item-${index}`;
item.style.cssText = 'margin: 10px 0; padding: 15px; border: 1px solid #ddd; border-radius: 8px;';
item.innerHTML = `
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<span style="font-weight: bold;">${file.name}</span>
<span style="color: #666;">${this.formatFileSize(file.size)}</span>
</div>
<div style="width: 100%; background: #f0f0f0; border-radius: 5px; overflow: hidden;">
<div id="progress-${index}" style="width: 0%; height: 20px; background: #2196F3;
transition: width 0.3s;"></div>
</div>
<div id="status-${index}" style="margin-top: 5px; font-size: 0.9em; color: #666;">
๋๊ธฐ ์ค...
</div>
`;
return item;
}
updateUploadItem(index, percent, status) {
const progressBar = document.getElementById(`progress-${index}`);
const statusText = document.getElementById(`status-${index}`);
if (progressBar) {
progressBar.style.width = `${percent}%`;
}
if (statusText) {
switch (status) {
case 'uploading':
statusText.textContent = `์
๋ก๋ ์ค... ${Math.round(percent)}%`;
statusText.style.color = '#2196F3';
break;
case 'complete':
statusText.textContent = 'โ ์
๋ก๋ ์๋ฃ';
statusText.style.color = '#4CAF50';
if (progressBar) progressBar.style.background = '#4CAF50';
break;
case 'error':
statusText.textContent = 'โ ์
๋ก๋ ์คํจ';
statusText.style.color = '#f44336';
if (progressBar) progressBar.style.background = '#f44336';
break;
}
}
}
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 Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}
}
// ์ฌ์ฉ ์์
async function handleMultipleFiles() {
const input = document.getElementById('multiFileInput');
const files = input.files;
if (files.length === 0) {
alert('ํ์ผ์ ์ ํํด์ฃผ์ธ์!');
return;
}
const uploader = new MultiFileUploader('uploadContainer');
try {
const results = await uploader.uploadFiles(files);
console.log('๋ชจ๋ ํ์ผ ์
๋ก๋ ์๋ฃ:', results);
} catch (error) {
console.error('์
๋ก๋ ์ค ์ค๋ฅ:', error);
}
}
โข ๋๋๊ทธ ์ค ๋๋กญ ์์ญ์ ์๊ฐ์ ์ผ๋ก ๋ช ํํ๊ฒ
โข ์ ๋ก๋ ์ค ์ทจ์ ๋ฒํผ ์ ๊ณต
โข ์๋ฌ ๋ฐ์ ์ ๋ช ํํ ๋ฉ์์ง์ ์ฌ์๋ ์ต์
โข ์ ๋ก๋ ์๋ฃ ํ ๋ค์ ์ก์ ์๋ด
โข ๋ชจ๋ฐ์ผ์์๋ ์ ์๋ํ๋๋ก ๋ฐ์ํ ๋์์ธ
๐ฏ ์ค์ ์์ : ์์ ํ AI ๋ฌธ์ ๋ถ์ ์์ด์ ํธ
์, ์ด์ ์ง๊ธ๊น์ง ๋ฐฐ์ด ๋ชจ๋ ๊ฑธ ์ข ํฉํด์ ์ค์ ๋ก ๋์ํ๋ AI ๋ฌธ์ ๋ถ์ ์์ด์ ํธ๋ฅผ ๋ง๋ค์ด๋ณด์! ์ด ์์ ๋ ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ์๋น์ค๋ก ์ ๊ณตํ ์ ์๋ ์์ค์ด์ผ. ๐
๐น ์ ์ฒด ์์คํ ๊ตฌ์กฐ
// ํ๋ก์ ํธ ๊ตฌ์กฐ
/*
ai-document-analyzer/
โโโ frontend/
โ โโโ index.html
โ โโโ app.js
โ โโโ styles.css
โโโ backend/
โ โโโ server.js
โ โโโ routes/
โ โ โโโ upload.js
โ โ โโโ analyze.js
โ โโโ services/
โ โ โโโ fileService.js
โ โ โโโ aiService.js
โ โ โโโ storageService.js
โ โโโ middleware/
โ โโโ validation.js
โ โโโ security.js
โโโ uploads/
โโโ .env
โโโ package.json
*/
๐น ๋ฐฑ์๋ ๋ฉ์ธ ์๋ฒ
// backend/server.js
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const uploadRoutes = require('./routes/upload');
const analyzeRoutes = require('./routes/analyze');
const app = express();
// ๋ณด์ ๋ฏธ๋ค์จ์ด
app.use(helmet());
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
credentials: true
}));
// Rate Limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15๋ถ
max: 100, // ์ต๋ 100 ์์ฒญ
message: '๋๋ฌด ๋ง์ ์์ฒญ์ด ๋ฐ์ํ์ต๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.'
});
app.use('/api/', limiter);
// Body Parser
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// ๋ผ์ฐํธ
app.use('/api/upload', uploadRoutes);
app.use('/api/analyze', analyzeRoutes);
// ํฌ์ค ์ฒดํฌ
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// ์๋ฌ ํธ๋ค๋ฌ
app.use((error, req, res, next) => {
console.error('์๋ฒ ์๋ฌ:', error);
res.status(error.status || 500).json({
error: error.message || '์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.',
...(process.env.NODE_ENV === 'development' && { stack: error.stack })
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`๐ AI ๋ฌธ์ ๋ถ์ ์๋ฒ๊ฐ ${PORT}๋ฒ ํฌํธ์์ ์คํ ์ค์
๋๋ค!`);
console.log(`๐ ํ๊ฒฝ: ${process.env.NODE_ENV || 'development'}`);
});
๐น ํ์ผ ์ ๋ก๋ ๋ผ์ฐํธ
// backend/routes/upload.js
const express = require('express');
const multer = require('multer');
const path = require('path');
const { validateFile, sanitizeFilename } = require('../middleware/validation');
const fileService = require('../services/fileService');
const router = express.Router();
// Multer ์ค์
const storage = multer.diskStorage({
destination: './uploads/temp',
filename: (req, file, cb) => {
const uniqueName = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}${path.extname(file.originalname)}`;
cb(null, uniqueName);
}
});
const upload = multer({
storage: storage,
limits: {
fileSize: 50 * 1024 * 1024, // 50MB
files: 5 // ์ต๋ 5๊ฐ ํ์ผ
},
fileFilter: (req, file, cb) => {
const allowedTypes = /pdf|doc|docx|txt|csv|xlsx|jpg|jpeg|png/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
if (extname) {
cb(null, true);
} else {
cb(new Error('์ง์ํ์ง ์๋ ํ์ผ ํ์์
๋๋ค.'));
}
}
});
// ๋จ์ผ ํ์ผ ์
๋ก๋
router.post('/single', upload.single('file'), validateFile, async (req, res, next) => {
try {
const file = req.file;
// ํ์ผ ์ฒ๋ฆฌ ๋ฐ ์ ์ฅ
const processedFile = await fileService.processUploadedFile(file);
res.json({
success: true,
file: {
id: processedFile.id,
originalName: file.originalname,
size: file.size,
type: file.mimetype,
uploadedAt: new Date().toISOString()
},
message: 'ํ์ผ์ด ์ฑ๊ณต์ ์ผ๋ก ์
๋ก๋๋์์ต๋๋ค.'
});
} catch (error) {
next(error);
}
});
// ๋ค์ค ํ์ผ ์
๋ก๋
router.post('/multiple', upload.array('files', 5), async (req, res, next) => {
try {
const files = req.files;
const processedFiles = await Promise.all(
files.map(file => fileService.processUploadedFile(file))
);
res.json({
success: true,
files: processedFiles.map((pf, index) => ({
id: pf.id,
originalName: files[index].originalname,
size: files[index].size,
type: files[index].mimetype
})),
message: `${files.length}๊ฐ์ ํ์ผ์ด ์
๋ก๋๋์์ต๋๋ค.`
});
} catch (error) {
next(error);
}
});
module.exports = router;
๐น AI ๋ถ์ ์๋น์ค
// backend/services/aiService.js
const OpenAI = require('openai');
const Anthropic = require('@anthropic-ai/sdk');
const fs = require('fs').promises;
const pdfParse = require('pdf-parse');
const mammoth = require('mammoth');
class AIService {
constructor() {
this.openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY
});
this.anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY
});
}
async analyzeDocument(fileInfo, analysisType = 'summary') {
try {
// ํ์ผ ๋ด์ฉ ์ถ์ถ
const content = await this.extractContent(fileInfo);
// ๋ถ์ ํ์
์ ๋ฐ๋ผ ๋ค๋ฅธ ์ฒ๋ฆฌ
switch (analysisType) {
case 'summary':
return await this.summarizeDocument(content);
case 'keywords':
return await this.extractKeywords(content);
case 'sentiment':
return await this.analyzeSentiment(content);
case 'qa':
return await this.answerQuestions(content, fileInfo.questions);
default:
return await this.generalAnalysis(content);
}
} catch (error) {
console.error('AI ๋ถ์ ์๋ฌ:', error);
throw error;
}
}
async extractContent(fileInfo) {
const filePath = fileInfo.path;
const mimeType = fileInfo.mimetype;
if (mimeType === 'application/pdf') {
const dataBuffer = await fs.readFile(filePath);
const data = await pdfParse(dataBuffer);
return data.text;
} else if (mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
const result = await mammoth.extractRawText({ path: filePath });
return result.value;
} else if (mimeType === 'text/plain') {
return await fs.readFile(filePath, 'utf-8');
} else if (mimeType.startsWith('image/')) {
return await this.analyzeImage(filePath);
}
throw new Error('์ง์ํ์ง ์๋ ํ์ผ ํ์์
๋๋ค.');
}
async summarizeDocument(content) {
const response = await this.openai.chat.completions.create({
model: "gpt-4-turbo-preview",
messages: [
{
role: "system",
content: "๋น์ ์ ๋ฌธ์๋ฅผ ๋ถ์ํ๊ณ ์์ฝํ๋ ์ ๋ฌธ๊ฐ์
๋๋ค. ํต์ฌ ๋ด์ฉ์ ๊ฐ๊ฒฐํ๊ณ ๋ช
ํํ๊ฒ ์์ฝํด์ฃผ์ธ์."
},
{
role: "user",
content: `๋ค์ ๋ฌธ์๋ฅผ ์์ฝํด์ฃผ์ธ์:\n\n${content.substring(0, 12000)}`
}
],
temperature: 0.5,
max_tokens: 1000
});
return {
type: 'summary',
result: response.choices[0].message.content,
wordCount: content.split(/\s+/).length,
characterCount: content.length
};
}
async extractKeywords(content) {
const response = await this.openai.chat.completions.create({
model: "gpt-4-turbo-preview",
messages: [
{
role: "system",
content: "๋ฌธ์์์ ์ค์ํ ํค์๋์ ์ฃผ์ ๋ฅผ ์ถ์ถํด์ฃผ์ธ์. JSON ํ์์ผ๋ก ๋ฐํํ์ธ์."
},
{
role: "user",
content: `๋ค์ ๋ฌธ์์์ ํค์๋๋ฅผ ์ถ์ถํด์ฃผ์ธ์:\n\n${content.substring(0, 12000)}`
}
],
temperature: 0.3,
max_tokens: 500
});
return {
type: 'keywords',
result: response.choices[0].message.content
};
}
async analyzeSentiment(content) {
const response = await this.anthropic.messages.create({
model: "claude-3-opus-20240229",
max_tokens: 500,
messages: [
{
role: "user",
content: `๋ค์ ํ
์คํธ์ ๊ฐ์ ์ ๋ถ์ํด์ฃผ์ธ์. ๊ธ์ ์ , ๋ถ์ ์ , ์ค๋ฆฝ์ ๋น์จ๊ณผ ์ ๋ฐ์ ์ธ ํค์ ์ค๋ช
ํด์ฃผ์ธ์:\n\n${content.substring(0, 10000)}`
}
]
});
return {
type: 'sentiment',
result: response.content[0].text
};
}
async answerQuestions(content, questions) {
if (!questions || questions.length === 0) {
throw new Error('์ง๋ฌธ์ด ์ ๊ณต๋์ง ์์์ต๋๋ค.');
}
const answers = await Promise.all(
questions.map(async (question) => {
const response = await this.openai.chat.completions.create({
model: "gpt-4-turbo-preview",
messages: [
{
role: "system",
content: "๋น์ ์ ๋ฌธ์๋ฅผ ์ฝ๊ณ ์ง๋ฌธ์ ๋ตํ๋ ์ ๋ฌธ๊ฐ์
๋๋ค."
},
{
role: "user",
content: `๋ฌธ์:\n${content.substring(0, 12000)}\n\n์ง๋ฌธ: ${question}`
}
],
temperature: 0.7,
max_tokens: 500
});
return {
question: question,
answer: response.choices[0].message.content
};
})
);
return {
type: 'qa',
result: answers
};
}
async analyzeImage(imagePath) {
const imageBuffer = await fs.readFile(imagePath);
const base64Image = imageBuffer.toString('base64');
const response = await this.openai.chat.completions.create({
model: "gpt-4-vision-preview",
messages: [
{
role: "user",
content: [
{
type: "text",
text: "์ด ์ด๋ฏธ์ง์ ๋ด์ฉ์ ์์ธํ ์ค๋ช
ํด์ฃผ์ธ์."
},
{
type: "image_url",
image_url: {
url: `data:image/jpeg;base64,${base64Image}`
}
}
]
}
],
max_tokens: 1000
});
return response.choices[0].message.content;
}
async generalAnalysis(content) {
const response = await this.openai.chat.completions.create({
model: "gpt-4-turbo-preview",
messages: [
{
role: "system",
content: "๋ฌธ์๋ฅผ ์ข
ํฉ์ ์ผ๋ก ๋ถ์ํ์ฌ ์ฃผ์ ๋ด์ฉ, ๊ตฌ์กฐ, ํน์ง์ ์ค๋ช
ํด์ฃผ์ธ์."
},
{
role: "user",
content: `๋ค์ ๋ฌธ์๋ฅผ ๋ถ์ํด์ฃผ์ธ์:\n\n${content.substring(0, 12000)}`
}
],
temperature: 0.7,
max_tokens: 1500
});
return {
type: 'general',
result: response.choices[0].message.content
};
}
}
module.exports = new AIService();
๐น ๋ถ์ ๋ผ์ฐํธ
// backend/routes/analyze.js
const express = require('express');
const aiService = require('../services/aiService');
const fileService = require('../services/fileService');
const router = express.Router();
router.post('/', async (req, res, next) => {
try {
const { fileId, analysisType, questions } = req.body;
if (!fileId) {
return res.status(400).json({ error: 'ํ์ผ ID๊ฐ ํ์ํฉ๋๋ค.' });
}
// ํ์ผ ์ ๋ณด ์กฐํ
const fileInfo = await fileService.getFileInfo(fileId);
if (!fileInfo) {
return res.status(404).json({ error: 'ํ์ผ์ ์ฐพ์ ์ ์์ต๋๋ค.' });
}
// ์ง๋ฌธ์ด ์์ผ๋ฉด ์ถ๊ฐ
if (questions) {
fileInfo.questions = questions;
}
// AI ๋ถ์ ์คํ
const analysisResult = await aiService.analyzeDocument(fileInfo, analysisType);
// ๋ถ์ ๊ฒฐ๊ณผ ์ ์ฅ
await fileService.saveAnalysisResult(fileId, analysisResult);
res.json({
success: true,
fileId: fileId,
analysis: analysisResult,
analyzedAt: new Date().toISOString()
});
} catch (error) {
next(error);
}
});
// ๋ถ์ ๊ฒฐ๊ณผ ์กฐํ
router.get('/:fileId', async (req, res, next) => {
try {
const { fileId } = req.params;
const analysisResult = await fileService.getAnalysisResult(fileId);
if (!analysisResult) {
return res.status(404).json({ error: '๋ถ์ ๊ฒฐ๊ณผ๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.' });
}
res.json({
success: true,
result: analysisResult
});
} catch (error) {
next(error);
}
});
module.exports = router;
๐น ํ๋ก ํธ์๋ ํตํฉ
// frontend/app.js
class DocumentAnalyzer {
constructor() {
this.apiUrl = 'http://localhost:3000/api';
this.currentFileId = null;
}
async uploadAndAnalyze(file, analysisType = 'summary') {
try {
// 1. ํ์ผ ์
๋ก๋
this.showStatus('ํ์ผ ์
๋ก๋ ์ค...', 'info');
const uploadResult = await this.uploadFile(file);
this.currentFileId = uploadResult.file.id;
// 2. AI ๋ถ์ ์์ฒญ
this.showStatus('AI ๋ถ์ ์ค... ์ ์๋ง ๊ธฐ๋ค๋ ค์ฃผ์ธ์.', 'info');
const analysisResult = await this.analyzeFile(this.currentFileId, analysisType);
// 3. ๊ฒฐ๊ณผ ํ์
this.displayResult(analysisResult);
this.showStatus('๋ถ์ ์๋ฃ!', 'success');
return analysisResult;
} catch (error) {
this.showStatus(`์ค๋ฅ: ${error.message}`, 'error');
throw error;
}
}
async uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
const response = await fetch(`${this.apiUrl}/upload/single`, {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('ํ์ผ ์
๋ก๋ ์คํจ');
}
return await response.json();
}
async analyzeFile(fileId, analysisType) {
const response = await fetch(`${this.apiUrl}/analyze`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
fileId: fileId,
analysisType: analysisType
})
});
if (!response.ok) {
throw new Error('๋ถ์ ์คํจ');
}
return await response.json();
}
displayResult(result) {
const resultContainer = document.getElementById('resultContainer');
let html = '<div class="result-card">';
html += `<h3>๋ถ์ ๊ฒฐ๊ณผ (${result.analysis.type})</h3>`;
if (result.analysis.type === 'summary') {
html += `<div class="summary">${result.analysis.result}</div>`;
html += `<div class="stats">`;
html += `<span>๋จ์ด ์: ${result.analysis.wordCount}</span>`;
html += `<span>๋ฌธ์ ์: ${result.analysis.characterCount}</span>`;
html += `</div>`;
} else if (result.analysis.type === 'qa') {
html += '<div class="qa-results">';
result.analysis.result.forEach(qa => {
html += `<div class="qa-item">`;
html += `<strong>Q: ${qa.question}</strong>`;
html += `<p>A: ${qa.answer}</p>`;
html += `</div>`;
});
html += '</div>';
} else {
html += `<div class="analysis-result">${result.analysis.result}</div>`;
}
html += '</div>';
resultContainer.innerHTML = html;
}
showStatus(message, type) {
const statusElement = document.getElementById('status');
statusElement.textContent = message;
statusElement.className = `status ${type}`;
}
}
// ์ด๊ธฐํ
const analyzer = new DocumentAnalyzer();
// ์ด๋ฒคํธ ๋ฆฌ์ค๋
document.getElementById('fileInput').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (file) {
const analysisType = document.getElementById('analysisType').value;
await analyzer.uploadAndAnalyze(file, analysisType);
}
});
๐ง ํธ๋ฌ๋ธ์ํ ๊ณผ ์ต์ ํ
์ค์ ์ด์ํ๋ค ๋ณด๋ฉด ์ฌ๋ฌ ๋ฌธ์ ๋ค์ ๋ง์ฃผ์น๊ฒ ๋ผ. ์์ฃผ ๋ฐ์ํ๋ ๋ฌธ์ ๋ค๊ณผ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ์์๋ณด์! ๐ ๏ธ
| ๋ฌธ์ | ์์ธ | ํด๊ฒฐ ๋ฐฉ๋ฒ |
|---|---|---|
| ์ ๋ก๋ ํ์์์ | ๋์ฉ๋ ํ์ผ, ๋๋ฆฐ ๋คํธ์ํฌ | ํ์์์ ์ค์ ์ฆ๊ฐ, ์ฒญํฌ ์ ๋ก๋ ์ฌ์ฉ |
| ๋ฉ๋ชจ๋ฆฌ ๋ถ์กฑ | ํ์ผ์ ๋ฉ๋ชจ๋ฆฌ์ ์ ์ฒด ๋ก๋ | ์คํธ๋ฆผ ์ฒ๋ฆฌ, ์ฒญํฌ ๋จ์ ์ฒ๋ฆฌ |
| ๋์ ์ ๋ก๋ ๊ณผ๋ถํ | ๋๋ฌด ๋ง์ ๋์ ์์ฒญ | ํ ์์คํ ๋์ , Rate Limiting |
| AI API ๋น์ฉ ํญ์ฆ | ๋ถํ์ํ API ํธ์ถ | ์บ์ฑ, ๊ฒฐ๊ณผ ์ฌ์ฌ์ฉ, ํ ํฐ ์ต์ ํ |
| ํ์ผ ์์ | ์ ์ก ์ค ์ค๋ฅ | ์ฒดํฌ์ฌ ๊ฒ์ฆ, ์ฌ์๋ ๋ก์ง |
- โ ์บ์ฑ ์ ๋ต: ๋์ผํ ํ์ผ์ ๋ถ์ ๊ฒฐ๊ณผ๋ ์บ์์ ์ ์ฅ
- โ CDN ํ์ฉ: ์ ์ ํ์ผ์ CDN์ผ๋ก ์๋น
- โ ์์ถ: gzip/brotli ์์ถ ํ์ฑํ
- โ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ธ๋ฑ์ฑ: ํ์ผ ์กฐํ ์ฟผ๋ฆฌ ์ต์ ํ
- โ ๋น๋๊ธฐ ์ฒ๋ฆฌ: ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆฌ๋ ์์ ์ ๋ฐฑ๊ทธ๋ผ์ด๋์์
- โ ๋ก๋ ๋ฐธ๋ฐ์ฑ: ํธ๋ํฝ ๋ถ์ฐ
- โ ๋ชจ๋ํฐ๋ง: ์ฑ๋ฅ ์งํ ์ค์๊ฐ ์ถ์
๐น ์บ์ฑ ๊ตฌํ
const redis = require('redis');
const crypto = require('crypto');
class CacheService {
constructor() {
this.client = redis.createClient({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379
});
this.client.on('error', (err) => {
console.error('Redis ์๋ฌ:', err);
});
}
generateCacheKey(fileHash, analysisType) {
return `analysis:${fileHash}:${analysisType}`;
}
async getFileHash(filePath) {
const fileBuffer = await fs.readFile(filePath);
return crypto.createHash('sha256').update(fileBuffer).digest('hex');
}
async getCachedResult(fileHash, analysisType) {
const key = this.generateCacheKey(fileHash, analysisType);
return new Promise((resolve, reject) => {
this.client.get(key, (err, data) => {
if (err) reject(err);
resolve(data ? JSON.parse(data) : null);
});
});
}
async setCachedResult(fileHash, analysisType, result, ttl = 3600) {
const key = this.generateCacheKey(fileHash, analysisType);
return new Promise((resolve, reject) => {
this.client.setex(key, ttl, JSON.stringify(result), (err) => {
if (err) reject(err);
resolve(true);
});
});
}
}
// ์ฌ์ฉ ์์
const cacheService = new CacheService();
async function analyzeWithCache(fileInfo, analysisType) {
// ํ์ผ ํด์ ์์ฑ
const fileHash = await cacheService.getFileHash(fileInfo.path);
// ์บ์ ํ์ธ
const cachedResult = await cacheService.getCachedResult(fileHash, analysisType);
if (cachedResult) {
console.log('์บ์์์ ๊ฒฐ๊ณผ ๋ฐํ');
return { ...cachedResult, fromCache: true };
}
// ์บ์ ๋ฏธ์ค - AI ๋ถ์ ์คํ
const result = await aiService.analyzeDocument(fileInfo, analysisType);
// ๊ฒฐ๊ณผ ์บ์ฑ
await cacheService.setCachedResult(fileHash, analysisType, result);
return { ...result, fromCache: false };
}
๐ ๋ชจ๋ํฐ๋ง๊ณผ ๋ก๊น
ํ๋ก๋์ ํ๊ฒฝ์์๋ ์์คํ ์ ๋ชจ๋ํฐ๋งํ๊ณ ๋ก๊ทธ๋ฅผ ๊ด๋ฆฌํ๋ ๊ฒ ์ ๋ง ์ค์ํด! ๋ฌธ์ ๊ฐ ์๊ฒผ์ ๋ ๋น ๋ฅด๊ฒ ํ์ ํ๊ณ ๋์ํ ์ ์๊ฑฐ๋ . ๐
๐น ๊ตฌ์กฐํ๋ ๋ก๊น
const winston = require('winston');
// ๋ก๊ฑฐ ์ค์
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'ai-document-analyzer' },
transports: [
// ์๋ฌ ๋ก๊ทธ๋ ๋ณ๋ ํ์ผ์
new winston.transports.File({
filename: 'logs/error.log',
level: 'error'
}),
// ๋ชจ๋ ๋ก๊ทธ
new winston.transports.File({
filename: 'logs/combined.log'
})
]
});
// ๊ฐ๋ฐ ํ๊ฒฝ์์๋ ์ฝ์์๋ ์ถ๋ ฅ
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}));
}
// ๋ก๊น
๋ฏธ๋ค์จ์ด
function requestLogger(req, res, next) {
const startTime = Date.now();
res.on('finish', () => {
const duration = Date.now() - startTime;
logger.info('HTTP Request', {
method: req.method,
url: req.url,
status: res.statusCode,
duration: `${duration}ms`,
userAgent: req.get('user-agent'),
ip: req.ip
});
});
next();
}
// ํ์ผ ์
๋ก๋ ๋ก๊น
function logFileUpload(fileInfo, userId) {
logger.info('File Upload', {
userId: userId,
filename: fileInfo.originalname,
size: fileInfo.size,
mimetype: fileInfo.mimetype,
timestamp: new Date().toISOString()
});
}
// AI ๋ถ์ ๋ก๊น
function logAIAnalysis(fileId, analysisType, duration, success) {
logger.info('AI Analysis', {
fileId: fileId,
analysisType: analysisType,
duration: `${duration}ms`,
success: success,
timestamp: new Date().toISOString()
});
}
module.exports = { logger, requestLogger, logFileUpload, logAIAnalysis };
๐น ์ฑ๋ฅ ๋ฉํธ๋ฆญ ์์ง
const prometheus = require('prom-client');
// ๋ฉํธ๋ฆญ ๋ ์ง์คํธ๋ฆฌ
const register = new prometheus.Registry();
// ๊ธฐ๋ณธ ๋ฉํธ๋ฆญ ์์ง
prometheus.collectDefaultMetrics({ register });
// ์ปค์คํ
๋ฉํธ๋ฆญ
const uploadCounter = new prometheus.Counter({
name: 'file_uploads_total',
help: '์ด ํ์ผ ์
๋ก๋ ์',
labelNames: ['status', 'file_type']
});
const uploadSize = new prometheus.Histogram({
name: 'file_upload_size_bytes',
help: '์
๋ก๋๋ ํ์ผ ํฌ๊ธฐ',
buckets: [1024, 10240, 102400, 1024000, 10240000, 52428800]
});
const analysisTime = new prometheus.Histogram({
name: 'ai_analysis_duration_seconds',
help: 'AI ๋ถ์ ์์ ์๊ฐ',
labelNames: ['analysis_type'],
buckets: [1, 5, 10, 30, 60, 120]
});
const activeUploads = new prometheus.Gauge({
name: 'active_uploads',
help: 'ํ์ฌ ์งํ ์ค์ธ ์
๋ก๋ ์'
});
register.registerMetric(uploadCounter);
register.registerMetric(uploadSize);
register.registerMetric(analysisTime);
register.registerMetric(activeUploads);
// ๋ฉํธ๋ฆญ ์๋ํฌ์ธํธ
app.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
});
// ๋ฉํธ๋ฆญ ๊ธฐ๋ก ํจ์๋ค
function recordUpload(fileSize, fileType, success) {
uploadCounter.inc({
status: success ? 'success' : 'failure',
file_type: fileType
});
uploadSize.observe(fileSize);
}
function recordAnalysis(analysisType, durationSeconds) {
analysisTime.observe({ analysis_type: analysisType }, durationSeconds);
}
module.exports = {
recordUpload,
recordAnalysis,
activeUploads
};
๐ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์! ์ฌ๊ธฐ๊น์ง ์ ๋ง ๊ธด ์ฌ์ ์ด์์ด! ๐
์ฐ๋ฆฌ๋ AI ์์ด์ ํธ์ ํ์ผ์
๋ก๋ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ ๋ชจ๋ ๊ณผ์ ์ ์ดํด๋ดค์ด. ํ๋ก ํธ์๋์ ๋๋๊ทธ ์ค ๋๋กญ๋ถํฐ ๋ฐฑ์๋์ ํ์ผ ์ฒ๋ฆฌ, ํด๋ผ์ฐ๋ ์คํ ๋ฆฌ์ง ์ฐ๋, AI API ํตํฉ, ๋ณด์, ์ต์ ํ, ๋ชจ๋ํฐ๋ง๊น์ง ์ ๋ง ๋ง์ ๋ด์ฉ์ ๋ค๋ค์ง!
์ด์ ๋๋ ์ค์ ์์ ์ฌ์ฉํ ์ ์๋ ์์ ํ ํ์ผ์
๋ก๋ ์์คํ
์ ๊ตฌ์ถํ ์ ์์ ๊ฑฐ์ผ. ๋ฌผ๋ก ์ฒ์๋ถํฐ ์๋ฒฝํ ์ ์์ด. ์์ ๊ฒ๋ถํฐ ์์ํด์ ์ ์ง์ ์ผ๋ก ๊ฐ์ ํด ๋๊ฐ๋ฉด ๋ผ! ๐ช
โข ํ์ผ ๋ฒ์ ๊ด๋ฆฌ ์์คํ
โข OCR ๊ธฐ๋ฅ์ผ๋ก ์ด๋ฏธ์ง ์ ํ ์คํธ ์ถ์ถ
โข ์์ฑ ํ์ผ ์ฒ๋ฆฌ ๋ฐ STT ํตํฉ
โข ๋ค๊ตญ์ด ์ง์
โข ๋ชจ๋ฐ์ผ ์ฑ ์ฐ๋
โข ์ํฐํ๋ผ์ด์ฆ ๊ธฐ๋ฅ (๊ถํ ๊ด๋ฆฌ, ๊ฐ์ฌ ๋ก๊ทธ ๋ฑ)
์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ์ด๋ฐ ๊ธฐ์ ์ ํ์ฉํ ์๋น์ค๋ฅผ ์ ๊ณตํ๋ฉด, ์ ๋ง ๋ง์ ์ฌ๋๋ค์๊ฒ ๋์์ ์ค ์ ์์ ๊ฑฐ์ผ! ๋ฌธ์ ๋ถ์, ์ด๋ฏธ์ง ์ฒ๋ฆฌ, ๋ฐ์ดํฐ ๋ถ์ ๋ฑ AI ์์ด์ ํธ์ ๊ฐ๋ฅ์ฑ์ ๋ฌด๊ถ๋ฌด์งํ๊ฑฐ๋ . ๐
์ด ๊ธ์ด ๋์ AI ์์ด์ ํธ ๊ฐ๋ฐ ์ฌ์ ์ ๋์์ด ๋์์ผ๋ฉด ์ข๊ฒ ์ด. ๊ถ๊ธํ ์ ์ด ์๊ฑฐ๋ ๋งํ๋ ๋ถ๋ถ์ด ์๋ค๋ฉด, ๊ฐ๋ฐ์ ์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํ๊ฑฐ๋ ๊ณต์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํด๋ด. ์ฐ๋ฆฌ ๋ชจ๋ ํจ๊ป ์ฑ์ฅํ๋ ๊ฑฐ๋๊น! ๐
ํ์ดํ
! ๋ฉ์ง AI ์์ด์ ํธ๋ฅผ ๋ง๋ค์ด๋ณด์! ๐ฏโจ
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.