์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿค– AI ์—์ด์ „ํŠธ์— ํŒŒ์ผ์—…๋กœ๋“œ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ํ•˜๊ธฐ: ์‹ค์ „ ๊ฐ€์ด๋“œ์™€ ๊ตฌํ˜„ ์ „๋žต

๐Ÿค– AI ์—์ด์ „ํŠธ์— ํŒŒ์ผ์—…๋กœ๋“œ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ํ•˜๊ธฐ: ์‹ค์ „ ๊ฐ€์ด๋“œ์™€ ๊ตฌํ˜„ ์ „๋žต

๋‹น์‹ ์˜ AI ์—์ด์ „ํŠธ๋ฅผ ํ•œ ๋‹จ๊ณ„ ์—…๊ทธ๋ ˆ์ด๋“œํ•˜๋Š” ํ•„์ˆ˜ ๊ธฐ๋Šฅ ์™„๋ฒฝ ์ •๋ณต! ๐Ÿ“โœจ

AI Agent + File Upload System

๐ŸŽฏ ์™œ AI ์—์ด์ „ํŠธ์— ํŒŒ์ผ์—…๋กœ๋“œ ๊ธฐ๋Šฅ์ด ํ•„์š”ํ• ๊นŒ?

์•ˆ๋…•, ์นœ๊ตฌ! ์˜ค๋Š˜์€ AI ์—์ด์ „ํŠธ ๊ฐœ๋ฐœ์—์„œ ์ •๋ง ์ค‘์š”ํ•œ ์ฃผ์ œ๋ฅผ ๋‹ค๋ค„๋ณผ ๊ฑฐ์•ผ. ๋ฐ”๋กœ ํŒŒ์ผ์—…๋กœ๋“œ ๊ธฐ๋Šฅ์ด์ง€! ๐Ÿš€

์š”์ฆ˜ AI ์—์ด์ „ํŠธ๋“ค์ด ์ •๋ง ๋˜‘๋˜‘ํ•ด์ง€๊ณ  ์žˆ์ž–์•„? ๊ทธ๋Ÿฐ๋ฐ ๋ง์ด์•ผ, ์•„๋ฌด๋ฆฌ ๋˜‘๋˜‘ํ•œ AI๋ผ๋„ ์‚ฌ์šฉ์ž๊ฐ€ ๊ฐ€์ง„ ๋ฌธ์„œ, ์ด๋ฏธ์ง€, ๋ฐ์ดํ„ฐ๋ฅผ ์ง์ ‘ ๋ฐ›์•„์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์—†๋‹ค๋ฉด ๊ทธ ๋Šฅ๋ ฅ์ด ๋ฐ˜์ชฝ์งœ๋ฆฌ๊ฐ€ ๋˜๋Š” ๊ฑฐ์•ผ. ๋งˆ์น˜ ์š”๋ฆฌ ์‹ค๋ ฅ์€ ์ตœ๊ณ ์ธ๋ฐ ์žฌ๋ฃŒ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์—†๋Š” ์…ฐํ”„ ๊ฐ™์€ ๊ฑฐ์ง€. ๐Ÿ˜…

์‹ค์ œ๋กœ ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ AI ๊ด€๋ จ ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•˜๋Š” ๊ฐœ๋ฐœ์ž๋“ค์„ ๋ณด๋ฉด, ํŒŒ์ผ์—…๋กœ๋“œ ๊ธฐ๋Šฅ์„ ์ž˜ ๊ตฌํ˜„ํ•œ ์—์ด์ „ํŠธ๊ฐ€ ์‚ฌ์šฉ์ž ๋งŒ์กฑ๋„๊ฐ€ ํ›จ์”ฌ ๋†’๋‹ค๋Š” ๊ฑธ ์•Œ ์ˆ˜ ์žˆ์–ด. ์™œ๋ƒํ•˜๋ฉด ์‚ฌ์šฉ์ž๋“ค์€ ์ž์‹ ์˜ ๋ฐ์ดํ„ฐ๋ฅผ AI์—๊ฒŒ ์ง์ ‘ ์ „๋‹ฌํ•˜๊ณ  ์‹ถ์–ด ํ•˜๊ฑฐ๋“ !

๐Ÿ“Š ํŒŒ์ผ์—…๋กœ๋“œ ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•œ ์‹ค์ œ ์‚ฌ๋ก€๋“ค: โ€ข ๋ฌธ์„œ ๋ถ„์„ ์—์ด์ „ํŠธ: PDF, Word ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•ด์„œ ์š”์•ฝ์ด๋‚˜ ๋ฒˆ์—ญ ์š”์ฒญ
โ€ข ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ์—์ด์ „ํŠธ: ์‚ฌ์ง„์„ ์—…๋กœ๋“œํ•ด์„œ ๊ฐ์ฒด ์ธ์‹, ์Šคํƒ€์ผ ๋ณ€ํ™˜
โ€ข ๋ฐ์ดํ„ฐ ๋ถ„์„ ์—์ด์ „ํŠธ: 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>
๐Ÿ’ก ํ”„๋กœ ํŒ: ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ๊ตฌํ˜„ํ•  ๋•Œ๋Š” preventDefault()๋ฅผ ๊ผญ ํ˜ธ์ถœํ•ด์•ผ ํ•ด! ์•ˆ ๊ทธ๋Ÿฌ๋ฉด ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํŒŒ์ผ์„ ์ƒˆ ํƒญ์—์„œ ์—ด์–ด๋ฒ„๋ฆฌ๊ฑฐ๋“ . ๐Ÿ˜ฑ

โš™๏ธ ๋ฐฑ์—”๋“œ ํŒŒ์ผ ์ฒ˜๋ฆฌ ๊ตฌํ˜„

ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ํŒŒ์ผ์„ ๋ณด๋ƒˆ์œผ๋ฉด, ์ด์ œ ๋ฐฑ์—”๋“œ์—์„œ ๋ฐ›์•„์„œ ์ฒ˜๋ฆฌํ•ด์•ผ๊ฒ ์ง€? ์—ฌ๊ธฐ์„œ๋Š” ์—ฌ๋Ÿฌ ํ”„๋ ˆ์ž„์›Œํฌ๋ณ„๋กœ ๊ตฌํ˜„ ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณผ๊ฒŒ! ๐Ÿ› ๏ธ

๐Ÿ”น 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: '์—…๋กœ๋“œ ์‹คํŒจ' });
    }
});
๐Ÿ’ก ํด๋ผ์šฐ๋“œ ์Šคํ† ๋ฆฌ์ง€ ์„ ํƒ ํŒ: โ€ข AWS S3: ๊ฐ€์žฅ ์„ฑ์ˆ™ํ•œ ์ƒํƒœ๊ณ„, ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ, ์ „ ์„ธ๊ณ„ ๋ฆฌ์ „
โ€ข 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;
    }
}
๐Ÿ“š ๋ฌธ์„œ ํŒŒ์‹ฑ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ถ”์ฒœ: โ€ข PDF: pdf-parse, pdfjs-dist
โ€ข 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 ์ฒ˜๋ฆฌ ๋กœ์ง
}
๐Ÿ’ก ๋Œ€์šฉ๋Ÿ‰ ํŒŒ์ผ ์ฒ˜๋ฆฌ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค: โ€ข ํŒŒ์ผ ํฌ๊ธฐ์— ๋”ฐ๋ผ ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ๋ถ„๊ธฐ (์˜ˆ: 10MB ์ด์ƒ์€ ์ฒญํฌ ์—…๋กœ๋“œ)
โ€ข ์—…๋กœ๋“œ ์ง„ํ–‰๋ฅ  ํ‘œ์‹œ๋กœ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„ 
โ€ข ํƒ€์ž„์•„์›ƒ ์„ค์ •์„ ๋„‰๋„‰ํ•˜๊ฒŒ (๋Œ€์šฉ๋Ÿ‰ ํŒŒ์ผ์€ ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆผ)
โ€ข ์ž„์‹œ ํŒŒ์ผ์€ ์ฒ˜๋ฆฌ ํ›„ ๋ฐ˜๋“œ์‹œ ์‚ญ์ œ
โ€ข ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… ํ ํ™œ์šฉ (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);
    }
}
๐Ÿ’ก UX ๊ฐœ์„  ํŒ: โ€ข ์—…๋กœ๋“œ ์ „ ํŒŒ์ผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ œ๊ณต (ํŠนํžˆ ์ด๋ฏธ์ง€)
โ€ข ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ์˜์—ญ์„ ์‹œ๊ฐ์ ์œผ๋กœ ๋ช…ํ™•ํ•˜๊ฒŒ
โ€ข ์—…๋กœ๋“œ ์ค‘ ์ทจ์†Œ ๋ฒ„ํŠผ ์ œ๊ณต
โ€ข ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ๋ช…ํ™•ํ•œ ๋ฉ”์‹œ์ง€์™€ ์žฌ์‹œ๋„ ์˜ต์…˜
โ€ข ์—…๋กœ๋“œ ์™„๋ฃŒ ํ›„ ๋‹ค์Œ ์•ก์…˜ ์•ˆ๋‚ด
โ€ข ๋ชจ๋ฐ”์ผ์—์„œ๋„ ์ž˜ ์ž‘๋™ํ•˜๋„๋ก ๋ฐ˜์‘ํ˜• ๋””์ž์ธ

๐ŸŽฏ ์‹ค์ „ ์˜ˆ์ œ: ์™„์ „ํ•œ 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 ํ†ตํ•ฉ, ๋ณด์•ˆ, ์ตœ์ ํ™”, ๋ชจ๋‹ˆํ„ฐ๋ง๊นŒ์ง€ ์ •๋ง ๋งŽ์€ ๋‚ด์šฉ์„ ๋‹ค๋ค˜์ง€!

์ด์ œ ๋„ˆ๋„ ์‹ค์ „์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์™„์ „ํ•œ ํŒŒ์ผ์—…๋กœ๋“œ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์„ ๊ฑฐ์•ผ. ๋ฌผ๋ก  ์ฒ˜์Œ๋ถ€ํ„ฐ ์™„๋ฒฝํ•  ์ˆœ ์—†์–ด. ์ž‘์€ ๊ฒƒ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ ์ ์ง„์ ์œผ๋กœ ๊ฐœ์„ ํ•ด ๋‚˜๊ฐ€๋ฉด ๋ผ! ๐Ÿ’ช

๐ŸŽ“ ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๋‚˜์•„๊ฐ€๊ธฐ: โ€ข ์‹ค์‹œ๊ฐ„ ํ˜‘์—… ๊ธฐ๋Šฅ ์ถ”๊ฐ€ (WebSocket)
โ€ข ํŒŒ์ผ ๋ฒ„์ „ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ
โ€ข OCR ๊ธฐ๋Šฅ์œผ๋กœ ์ด๋ฏธ์ง€ ์† ํ…์ŠคํŠธ ์ถ”์ถœ
โ€ข ์Œ์„ฑ ํŒŒ์ผ ์ฒ˜๋ฆฌ ๋ฐ STT ํ†ตํ•ฉ
โ€ข ๋‹ค๊ตญ์–ด ์ง€์›
โ€ข ๋ชจ๋ฐ”์ผ ์•ฑ ์—ฐ๋™
โ€ข ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ๊ธฐ๋Šฅ (๊ถŒํ•œ ๊ด€๋ฆฌ, ๊ฐ์‚ฌ ๋กœ๊ทธ ๋“ฑ)

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์ด๋Ÿฐ ๊ธฐ์ˆ ์„ ํ™œ์šฉํ•œ ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•˜๋ฉด, ์ •๋ง ๋งŽ์€ ์‚ฌ๋žŒ๋“ค์—๊ฒŒ ๋„์›€์„ ์ค„ ์ˆ˜ ์žˆ์„ ๊ฑฐ์•ผ! ๋ฌธ์„œ ๋ถ„์„, ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ, ๋ฐ์ดํ„ฐ ๋ถ„์„ ๋“ฑ AI ์—์ด์ „ํŠธ์˜ ๊ฐ€๋Šฅ์„ฑ์€ ๋ฌด๊ถ๋ฌด์ง„ํ•˜๊ฑฐ๋“ . ๐Ÿš€

์ด ๊ธ€์ด ๋„ˆ์˜ AI ์—์ด์ „ํŠธ ๊ฐœ๋ฐœ ์—ฌ์ •์— ๋„์›€์ด ๋˜์—ˆ์œผ๋ฉด ์ข‹๊ฒ ์–ด. ๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๊ฑฐ๋‚˜ ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด, ๊ฐœ๋ฐœ์ž ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์งˆ๋ฌธํ•˜๊ฑฐ๋‚˜ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•ด๋ด. ์šฐ๋ฆฌ ๋ชจ๋‘ ํ•จ๊ป˜ ์„ฑ์žฅํ•˜๋Š” ๊ฑฐ๋‹ˆ๊นŒ! ๐Ÿ˜Š

ํ™”์ดํŒ…! ๋ฉ‹์ง„ AI ์—์ด์ „ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! ๐ŸŽฏโœจ

๐Ÿ“ ๐Ÿค– โœจ Happy Coding! Build Amazing AI Agents with File Upload
๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.