ํŒŒ์ผ ๊ด€๋ฆฌ

ํŒŒ์ผ ๊ด€๋ฆฌ

๋ผ์ด๋ฏน์Šค์—์„œ ํŒŒ์ผ๊ณผ ์ฒจ๋ถ€ํŒŒ์ผ์„ ํšจ๊ณผ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ํ•™์Šตํ•ฉ๋‹ˆ๋‹ค.

ํŒŒ์ผ ์—…๋กœ๋“œ ๊ธฐ๋ณธ

์—…๋กœ๋“œ ์„ค์ •

// config/config.inc.php
$config->file = new stdClass();
$config->file->allowed_extensions = array('jpg', 'jpeg', 'gif', 'png', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'zip');
$config->file->max_file_size = 2 * 1024 * 1024; // 2MB
$config->file->max_upload_size = 10 * 1024 * 1024; // 10MB ์ดํ•ฉ
$config->file->image_quality = 85; // JPEG ํ’ˆ์งˆ
$config->file->watermark = false;

๊ฒŒ์‹œํŒ๋ณ„ ํŒŒ์ผ ์„ค์ •

// ๊ฒŒ์‹œํŒ ์„ค์ •์—์„œ ํŒŒ์ผ ์—…๋กœ๋“œ ์ œํ•œ
$module_config = new stdClass();
$module_config->use_file_upload = 'Y';
$module_config->allowed_filesize = 5 * 1024 * 1024; // 5MB
$module_config->allowed_attach_count = 3; // ์ตœ๋Œ€ 3๊ฐœ ํŒŒ์ผ
$module_config->allowed_extensions = 'jpg,png,gif,pdf,doc,hwp';

ํŒŒ์ผ ์—…๋กœ๋“œ ์ธํ„ฐํŽ˜์ด์Šค

๊ธฐ๋ณธ ํŒŒ์ผ ์—…๋กœ๋“œ ํผ

<!-- ๊ธฐ๋ณธ ํŒŒ์ผ ์—…๋กœ๋“œ -->
<div class="file-upload-area">
    <label for="file_upload">ํŒŒ์ผ ์ฒจ๋ถ€</label>
    <input type="file" id="file_upload" name="files[]" multiple 
           accept=".jpg,.jpeg,.png,.gif,.pdf,.doc,.docx,.hwp">

    <div class="upload-info">
        <p>์ตœ๋Œ€ ํŒŒ์ผ ํฌ๊ธฐ: 5MB</p>
        <p>ํ—ˆ์šฉ ํ™•์žฅ์ž: jpg, png, gif, pdf, doc, docx, hwp</p>
        <p>์ตœ๋Œ€ {$module_info->allowed_attach_count}๊ฐœ ํŒŒ์ผ ์—…๋กœ๋“œ ๊ฐ€๋Šฅ</p>
    </div>
</div>

<!-- ์—…๋กœ๋“œ๋œ ํŒŒ์ผ ๋ชฉ๋ก -->
<div id="uploaded-files" class="uploaded-files">
    <!-- JavaScript๋กœ ๋™์  ์ƒ์„ฑ -->
</div>

๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ์—…๋กœ๋“œ

<div class="drag-drop-area" id="dropArea">
    <div class="drop-message">
        <i class="fa fa-cloud-upload"></i>
        <p>ํŒŒ์ผ์„ ์—ฌ๊ธฐ์— ๋“œ๋ž˜๊ทธํ•˜๊ฑฐ๋‚˜ ํด๋ฆญํ•˜์—ฌ ์—…๋กœ๋“œํ•˜์„ธ์š”</p>
        <input type="file" id="fileInput" multiple style="display: none;">
        <button type="button" class="btn-select-files">ํŒŒ์ผ ์„ ํƒ</button>
    </div>

    <div class="progress-area" id="progressArea" style="display: none;">
        <div class="progress-bar">
            <div class="progress-fill" id="progressFill"></div>
        </div>
        <div class="progress-text" id="progressText">0%</div>
    </div>
</div>

<script>
class DragDropUpload {
    constructor(dropAreaId, fileInputId) {
        this.dropArea = document.getElementById(dropAreaId);
        this.fileInput = document.getElementById(fileInputId);
        this.progressArea = document.getElementById('progressArea');
        this.progressFill = document.getElementById('progressFill');
        this.progressText = document.getElementById('progressText');

        this.maxFileSize = 5 * 1024 * 1024; // 5MB
        this.allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'application/pdf'];

        this.initEvents();
    }

    initEvents() {
        // ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ์ด๋ฒคํŠธ
        this.dropArea.addEventListener('dragover', this.handleDragOver.bind(this));
        this.dropArea.addEventListener('dragleave', this.handleDragLeave.bind(this));
        this.dropArea.addEventListener('drop', this.handleDrop.bind(this));

        // ํด๋ฆญ ์—…๋กœ๋“œ
        this.dropArea.addEventListener('click', () => {
            this.fileInput.click();
        });

        // ํŒŒ์ผ ์„ ํƒ ์ด๋ฒคํŠธ
        this.fileInput.addEventListener('change', (e) => {
            this.handleFiles(e.target.files);
        });
    }

    handleDragOver(e) {
        e.preventDefault();
        this.dropArea.classList.add('drag-over');
    }

    handleDragLeave(e) {
        e.preventDefault();
        this.dropArea.classList.remove('drag-over');
    }

    handleDrop(e) {
        e.preventDefault();
        this.dropArea.classList.remove('drag-over');

        const files = e.dataTransfer.files;
        this.handleFiles(files);
    }

    handleFiles(files) {
        for (let file of files) {
            if (this.validateFile(file)) {
                this.uploadFile(file);
            }
        }
    }

    validateFile(file) {
        // ํŒŒ์ผ ํฌ๊ธฐ ๊ฒ€์‚ฌ
        if (file.size > this.maxFileSize) {
            alert(`ํŒŒ์ผ ํฌ๊ธฐ๊ฐ€ ๋„ˆ๋ฌด ํฝ๋‹ˆ๋‹ค. ์ตœ๋Œ€ ${this.maxFileSize / 1024 / 1024}MB๊นŒ์ง€ ์—…๋กœ๋“œ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.`);
            return false;
        }

        // ํŒŒ์ผ ํƒ€์ž… ๊ฒ€์‚ฌ
        if (!this.allowedTypes.includes(file.type)) {
            alert('ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค.');
            return false;
        }

        return true;
    }

    async uploadFile(file) {
        const formData = new FormData();
        formData.append('file', file);
        formData.append('module', 'file');
        formData.append('act', 'procFileUpload');

        this.showProgress();

        try {
            const response = await fetch('/index.php', {
                method: 'POST',
                body: formData,
                onUploadProgress: (progressEvent) => {
                    const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
                    this.updateProgress(percentCompleted);
                }
            });

            const result = await response.json();

            if (result.error === '0') {
                this.onUploadSuccess(result);
            } else {
                this.onUploadError(result.message);
            }
        } catch (error) {
            this.onUploadError('์—…๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.');
        }

        this.hideProgress();
    }

    showProgress() {
        this.progressArea.style.display = 'block';
    }

    hideProgress() {
        setTimeout(() => {
            this.progressArea.style.display = 'none';
            this.updateProgress(0);
        }, 1000);
    }

    updateProgress(percent) {
        this.progressFill.style.width = percent + '%';
        this.progressText.textContent = percent + '%';
    }

    onUploadSuccess(result) {
        // ์—…๋กœ๋“œ๋œ ํŒŒ์ผ์„ ๋ชฉ๋ก์— ์ถ”๊ฐ€
        this.addFileToList(result.file_info);
        alert('ํŒŒ์ผ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
    }

    onUploadError(message) {
        alert('์—…๋กœ๋“œ ์‹คํŒจ: ' + message);
    }

    addFileToList(fileInfo) {
        const fileList = document.getElementById('uploaded-files');
        const fileItem = document.createElement('div');
        fileItem.className = 'file-item';
        fileItem.innerHTML = `
            <div class="file-info">
                <span class="file-name">${fileInfo.source_filename}</span>
                <span class="file-size">${this.formatFileSize(fileInfo.file_size)}</span>
            </div>
            <div class="file-actions">
                <button type="button" class="btn-download" data-file-srl="${fileInfo.file_srl}">
                    <i class="fa fa-download"></i>
                </button>
                <button type="button" class="btn-delete" data-file-srl="${fileInfo.file_srl}">
                    <i class="fa fa-trash"></i>
                </button>
            </div>
        `;

        fileList.appendChild(fileItem);
    }

    formatFileSize(bytes) {
        if (bytes === 0) return '0 Bytes';

        const k = 1024;
        const sizes = ['Bytes', 'KB', 'MB', 'GB'];
        const i = Math.floor(Math.log(bytes) / Math.log(k));

        return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
    }
}

// ์ดˆ๊ธฐํ™”
const dragDropUpload = new DragDropUpload('dropArea', 'fileInput');
</script>

CSS ์Šคํƒ€์ผ

.drag-drop-area {
    border: 2px dashed #ccc;
    border-radius: 8px;
    padding: 40px;
    text-align: center;
    cursor: pointer;
    transition: all 0.3s ease;
    margin: 20px 0;
}

.drag-drop-area:hover,
.drag-drop-area.drag-over {
    border-color: #007bff;
    background-color: #f8f9ff;
}

.drop-message {
    color: #666;
}

.drop-message i {
    font-size: 3em;
    color: #ccc;
    margin-bottom: 10px;
}

.drop-message p {
    margin: 10px 0;
    font-size: 16px;
}

.btn-select-files {
    background: #007bff;
    color: white;
    border: none;
    padding: 10px 20px;
    border-radius: 4px;
    cursor: pointer;
    margin-top: 10px;
}

.progress-area {
    margin-top: 20px;
}

.progress-bar {
    width: 100%;
    height: 20px;
    background: #f0f0f0;
    border-radius: 10px;
    overflow: hidden;
}

.progress-fill {
    height: 100%;
    background: #007bff;
    transition: width 0.3s ease;
}

.progress-text {
    margin-top: 10px;
    font-weight: bold;
    color: #007bff;
}

.uploaded-files {
    margin-top: 20px;
}

.file-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px;
    border: 1px solid #eee;
    border-radius: 4px;
    margin-bottom: 5px;
}

.file-info {
    flex: 1;
}

.file-name {
    font-weight: bold;
    margin-right: 10px;
}

.file-size {
    color: #666;
    font-size: 0.9em;
}

.file-actions {
    display: flex;
    gap: 5px;
}

.file-actions button {
    background: none;
    border: 1px solid #ddd;
    padding: 5px 8px;
    border-radius: 3px;
    cursor: pointer;
}

.btn-download:hover {
    background: #28a745;
    color: white;
    border-color: #28a745;
}

.btn-delete:hover {
    background: #dc3545;
    color: white;
    border-color: #dc3545;
}

์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ

์ธ๋„ค์ผ ์ƒ์„ฑ

class ImageProcessor
{
    /**
     * ์ธ๋„ค์ผ ์ƒ์„ฑ
     */
    public static function createThumbnail($source_path, $dest_path, $width, $height, $quality = 85)
    {
        $image_info = getimagesize($source_path);
        if (!$image_info) {
            return false;
        }

        $original_width = $image_info[0];
        $original_height = $image_info[1];
        $image_type = $image_info[2];

        // ์†Œ์Šค ์ด๋ฏธ์ง€ ๋กœ๋“œ
        switch ($image_type) {
            case IMAGETYPE_JPEG:
                $source_image = imagecreatefromjpeg($source_path);
                break;
            case IMAGETYPE_PNG:
                $source_image = imagecreatefrompng($source_path);
                break;
            case IMAGETYPE_GIF:
                $source_image = imagecreatefromgif($source_path);
                break;
            default:
                return false;
        }

        // ์ธ๋„ค์ผ ํฌ๊ธฐ ๊ณ„์‚ฐ
        $thumb_size = self::calculateThumbnailSize($original_width, $original_height, $width, $height);

        // ์ธ๋„ค์ผ ์ด๋ฏธ์ง€ ์ƒ์„ฑ
        $thumb_image = imagecreatetruecolor($thumb_size['width'], $thumb_size['height']);

        // PNG ํˆฌ๋ช…๋„ ์œ ์ง€
        if ($image_type == IMAGETYPE_PNG) {
            imagealphablending($thumb_image, false);
            imagesavealpha($thumb_image, true);
            $transparent = imagecolorallocatealpha($thumb_image, 255, 255, 255, 127);
            imagefill($thumb_image, 0, 0, $transparent);
        }

        // ์ด๋ฏธ์ง€ ๋ฆฌ์ƒ˜ํ”Œ๋ง
        imagecopyresampled(
            $thumb_image, $source_image,
            0, 0, 0, 0,
            $thumb_size['width'], $thumb_size['height'],
            $original_width, $original_height
        );

        // ์ธ๋„ค์ผ ์ €์žฅ
        $result = false;
        switch ($image_type) {
            case IMAGETYPE_JPEG:
                $result = imagejpeg($thumb_image, $dest_path, $quality);
                break;
            case IMAGETYPE_PNG:
                $result = imagepng($thumb_image, $dest_path);
                break;
            case IMAGETYPE_GIF:
                $result = imagegif($thumb_image, $dest_path);
                break;
        }

        // ๋ฉ”๋ชจ๋ฆฌ ํ•ด์ œ
        imagedestroy($source_image);
        imagedestroy($thumb_image);

        return $result;
    }

    /**
     * ์ธ๋„ค์ผ ํฌ๊ธฐ ๊ณ„์‚ฐ
     */
    private static function calculateThumbnailSize($original_width, $original_height, $max_width, $max_height)
    {
        $ratio = min($max_width / $original_width, $max_height / $original_height);

        return array(
            'width' => round($original_width * $ratio),
            'height' => round($original_height * $ratio)
        );
    }

    /**
     * ์›Œํ„ฐ๋งˆํฌ ์ถ”๊ฐ€
     */
    public static function addWatermark($image_path, $watermark_path, $position = 'bottom-right')
    {
        $image = imagecreatefromjpeg($image_path);
        $watermark = imagecreatefrompng($watermark_path);

        $image_width = imagesx($image);
        $image_height = imagesy($image);
        $watermark_width = imagesx($watermark);
        $watermark_height = imagesy($watermark);

        // ์›Œํ„ฐ๋งˆํฌ ์œ„์น˜ ๊ณ„์‚ฐ
        switch ($position) {
            case 'top-left':
                $dest_x = 10;
                $dest_y = 10;
                break;
            case 'top-right':
                $dest_x = $image_width - $watermark_width - 10;
                $dest_y = 10;
                break;
            case 'bottom-left':
                $dest_x = 10;
                $dest_y = $image_height - $watermark_height - 10;
                break;
            case 'bottom-right':
            default:
                $dest_x = $image_width - $watermark_width - 10;
                $dest_y = $image_height - $watermark_height - 10;
                break;
            case 'center':
                $dest_x = ($image_width - $watermark_width) / 2;
                $dest_y = ($image_height - $watermark_height) / 2;
                break;
        }

        // ์›Œํ„ฐ๋งˆํฌ ํ•ฉ์„ฑ
        imagecopy($image, $watermark, $dest_x, $dest_y, 0, 0, $watermark_width, $watermark_height);

        // ์ด๋ฏธ์ง€ ์ €์žฅ
        imagejpeg($image, $image_path, 85);

        // ๋ฉ”๋ชจ๋ฆฌ ํ•ด์ œ
        imagedestroy($image);
        imagedestroy($watermark);

        return true;
    }
}

์ด๋ฏธ์ง€ ๋ทฐ์–ด

<!-- ์ด๋ฏธ์ง€ ๊ฐค๋Ÿฌ๋ฆฌ -->
<div class="image-gallery">
    <!--@foreach($uploaded_files as $file)-->
    <!--@if($file->isImage())-->
    <div class="image-item">
        <img src="{$file->getThumbnail(200, 200)}" 
             alt="{$file->source_filename}"
             data-full-image="{$file->uploaded_filename}"
             class="gallery-thumbnail">

        <div class="image-overlay">
            <div class="image-actions">
                <button type="button" class="btn-view" data-image="{$file->uploaded_filename}">
                    <i class="fa fa-eye"></i>
                </button>
                <button type="button" class="btn-download" data-file-srl="{$file->file_srl}">
                    <i class="fa fa-download"></i>
                </button>
                <button type="button" class="btn-delete" data-file-srl="{$file->file_srl}">
                    <i class="fa fa-trash"></i>
                </button>
            </div>
        </div>
    </div>
    <!--@end-->
    <!--@end-->
</div>

<!-- ์ด๋ฏธ์ง€ ๋ชจ๋‹ฌ -->
<div id="imageModal" class="image-modal" style="display: none;">
    <div class="modal-overlay" onclick="closeImageModal()"></div>
    <div class="modal-content">
        <img id="modalImage" src="" alt="">
        <button type="button" class="modal-close" onclick="closeImageModal()">
            <i class="fa fa-times"></i>
        </button>
        <div class="modal-navigation">
            <button type="button" class="btn-prev" onclick="navigateImage(-1)">
                <i class="fa fa-chevron-left"></i>
            </button>
            <button type="button" class="btn-next" onclick="navigateImage(1)">
                <i class="fa fa-chevron-right"></i>
            </button>
        </div>
    </div>
</div>

<script>
class ImageViewer {
    constructor() {
        this.images = [];
        this.currentIndex = 0;
        this.modal = document.getElementById('imageModal');
        this.modalImage = document.getElementById('modalImage');

        this.initEvents();
        this.loadImages();
    }

    initEvents() {
        // ์ธ๋„ค์ผ ํด๋ฆญ ์ด๋ฒคํŠธ
        document.addEventListener('click', (e) => {
            if (e.target.classList.contains('gallery-thumbnail')) {
                const imageSrc = e.target.dataset.fullImage;
                this.openModal(imageSrc);
            }

            if (e.target.closest('.btn-view')) {
                const imageSrc = e.target.closest('.btn-view').dataset.image;
                this.openModal(imageSrc);
            }
        });

        // ํ‚ค๋ณด๋“œ ๋‚ด๋น„๊ฒŒ์ด์…˜
        document.addEventListener('keydown', (e) => {
            if (this.modal.style.display === 'block') {
                switch (e.key) {
                    case 'Escape':
                        this.closeModal();
                        break;
                    case 'ArrowLeft':
                        this.navigateImage(-1);
                        break;
                    case 'ArrowRight':
                        this.navigateImage(1);
                        break;
                }
            }
        });
    }

    loadImages() {
        const thumbnails = document.querySelectorAll('.gallery-thumbnail');
        this.images = Array.from(thumbnails).map(thumb => thumb.dataset.fullImage);
    }

    openModal(imageSrc) {
        this.currentIndex = this.images.indexOf(imageSrc);
        this.modalImage.src = imageSrc;
        this.modal.style.display = 'block';
        document.body.style.overflow = 'hidden';
    }

    closeModal() {
        this.modal.style.display = 'none';
        document.body.style.overflow = '';
    }

    navigateImage(direction) {
        if (this.images.length === 0) return;

        this.currentIndex += direction;

        if (this.currentIndex >= this.images.length) {
            this.currentIndex = 0;
        } else if (this.currentIndex < 0) {
            this.currentIndex = this.images.length - 1;
        }

        this.modalImage.src = this.images[this.currentIndex];
    }
}

// ์ „์—ญ ํ•จ์ˆ˜๋“ค
function closeImageModal() {
    imageViewer.closeModal();
}

function navigateImage(direction) {
    imageViewer.navigateImage(direction);
}

// ์ดˆ๊ธฐํ™”
const imageViewer = new ImageViewer();
</script>

ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ

์•ˆ์ „ํ•œ ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ

class FileDownloader
{
    /**
     * ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ ์ฒ˜๋ฆฌ
     */
    public static function downloadFile($file_srl, $check_permission = true)
    {
        // ํŒŒ์ผ ์ •๋ณด ์กฐํšŒ
        $oFileModel = getModel('file');
        $file_info = $oFileModel->getFile($file_srl);

        if (!$file_info || !$file_info->file_srl) {
            return new BaseObject(-1, 'ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.');
        }

        // ๊ถŒํ•œ ํ™•์ธ
        if ($check_permission) {
            $permission_result = self::checkDownloadPermission($file_info);
            if (!$permission_result->toBool()) {
                return $permission_result;
            }
        }

        $file_path = $file_info->uploaded_filename;

        // ํŒŒ์ผ ์กด์žฌ ํ™•์ธ
        if (!file_exists($file_path)) {
            return new BaseObject(-1, 'ํŒŒ์ผ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.');
        }

        // ๋‹ค์šด๋กœ๋“œ ์นด์šดํŠธ ์ฆ๊ฐ€
        self::increaseDownloadCount($file_srl);

        // ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ ํ—ค๋” ์„ค์ •
        self::setDownloadHeaders($file_info);

        // ํŒŒ์ผ ์ถœ๋ ฅ
        self::outputFile($file_path);

        exit;
    }

    /**
     * ๋‹ค์šด๋กœ๋“œ ๊ถŒํ•œ ํ™•์ธ
     */
    private static function checkDownloadPermission($file_info)
    {
        $logged_info = Context::get('logged_info');

        // ๊ด€๋ฆฌ์ž๋Š” ๋ชจ๋“  ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ ๊ฐ€๋Šฅ
        if ($logged_info && $logged_info->is_admin == 'Y') {
            return new BaseObject();
        }

        // ํŒŒ์ผ ์†Œ์œ ์ž ํ™•์ธ
        if ($logged_info && $file_info->member_srl == $logged_info->member_srl) {
            return new BaseObject();
        }

        // ๊ฒŒ์‹œ๋ฌผ ๊ถŒํ•œ ํ™•์ธ
        if ($file_info->document_srl) {
            $oDocumentModel = getModel('document');
            $oDocument = $oDocumentModel->getDocument($file_info->document_srl);

            if ($oDocument && $oDocument->isAccessible()) {
                return new BaseObject();
            }
        }

        return new BaseObject(-1, 'ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.');
    }

    /**
     * ๋‹ค์šด๋กœ๋“œ ํ—ค๋” ์„ค์ •
     */
    private static function setDownloadHeaders($file_info)
    {
        $filename = $file_info->source_filename;
        $file_size = filesize($file_info->uploaded_filename);

        // ๋ธŒ๋ผ์šฐ์ €๋ณ„ ํŒŒ์ผ๋ช… ์ธ์ฝ”๋”ฉ
        $user_agent = $_SERVER['HTTP_USER_AGENT'];
        if (strpos($user_agent, 'MSIE') !== false || strpos($user_agent, 'Trident') !== false) {
            $filename = rawurlencode($filename);
        } else {
            $filename = '=?UTF-8?B?' . base64_encode($filename) . '?=';
        }

        // ํ—ค๋” ์„ค์ •
        header('Content-Type: application/octet-stream');
        header('Content-Disposition: attachment; filename="' . $filename . '"');
        header('Content-Transfer-Encoding: binary');
        header('Content-Length: ' . $file_size);
        header('Cache-Control: private, no-transform, no-store, must-revalidate');
        header('Pragma: no-cache');
        header('Expires: 0');

        // ์ด๋ฏธ์ง€ ํŒŒ์ผ์ธ ๊ฒฝ์šฐ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ—ˆ์šฉ
        if ($file_info->isImage() && isset($_GET['preview'])) {
            header('Content-Type: ' . $file_info->mime_type);
            header('Content-Disposition: inline; filename="' . $filename . '"');
        }
    }

    /**
     * ํŒŒ์ผ ์ถœ๋ ฅ
     */
    private static function outputFile($file_path)
    {
        $chunk_size = 8192; // 8KB์”ฉ ์ฝ๊ธฐ

        if ($file_handle = fopen($file_path, 'rb')) {
            while (!feof($file_handle)) {
                echo fread($file_handle, $chunk_size);
                flush();
            }
            fclose($file_handle);
        }
    }

    /**
     * ๋‹ค์šด๋กœ๋“œ ํšŸ์ˆ˜ ์ฆ๊ฐ€
     */
    private static function increaseDownloadCount($file_srl)
    {
        $args = new stdClass();
        $args->file_srl = $file_srl;
        executeQuery('file.updateDownloadCount', $args);
    }
}

ํŒŒ์ผ ๊ด€๋ฆฌ ๋„๊ตฌ

ํŒŒ์ผ ์ •๋ฆฌ ์Šคํฌ๋ฆฝํŠธ

class FileManager
{
    /**
     * ๊ณ ์•„ ํŒŒ์ผ ์ •๋ฆฌ
     */
    public static function cleanOrphanFiles()
    {
        // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์—†๋Š” ํŒŒ์ผ๋“ค ์ฐพ๊ธฐ
        $upload_dir = _XE_PATH_ . 'files/attach/';
        $files = FileHandler::readDir($upload_dir, '/\.(jpg|jpeg|png|gif|pdf|doc|docx|xls|xlsx|ppt|pptx|zip|hwp)$/i');

        $orphan_files = array();
        $total_size = 0;

        foreach ($files as $file) {
            $file_path = $upload_dir . $file;

            // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ํŒŒ์ผ ํ™•์ธ
            $args = new stdClass();
            $args->uploaded_filename = $file_path;
            $output = executeQuery('file.getFileByPath', $args);

            if (!$output->data) {
                $orphan_files[] = $file;
                $total_size += filesize($file_path);
            }
        }

        return array(
            'count' => count($orphan_files),
            'files' => $orphan_files,
            'total_size' => $total_size
        );
    }

    /**
     * ์ž„์‹œ ํŒŒ์ผ ์ •๋ฆฌ
     */
    public static function cleanTempFiles($older_than_days = 7)
    {
        $temp_dir = _XE_PATH_ . 'files/cache/tmp/';
        $cutoff_time = time() - ($older_than_days * 24 * 60 * 60);

        $temp_files = FileHandler::readDir($temp_dir);
        $cleaned_files = array();
        $total_size = 0;

        foreach ($temp_files as $file) {
            $file_path = $temp_dir . $file;

            if (filemtime($file_path) < $cutoff_time) {
                $file_size = filesize($file_path);
                if (unlink($file_path)) {
                    $cleaned_files[] = $file;
                    $total_size += $file_size;
                }
            }
        }

        return array(
            'count' => count($cleaned_files),
            'files' => $cleaned_files,
            'total_size' => $total_size
        );
    }

    /**
     * ์ค‘๋ณต ํŒŒ์ผ ์ฐพ๊ธฐ
     */
    public static function findDuplicateFiles()
    {
        $args = new stdClass();
        $output = executeQuery('file.getAllFiles', $args);

        if (!$output->toBool() || !$output->data) {
            return array();
        }

        $file_hashes = array();
        $duplicates = array();

        foreach ($output->data as $file_info) {
            if (!file_exists($file_info->uploaded_filename)) {
                continue;
            }

            $file_hash = md5_file($file_info->uploaded_filename);

            if (isset($file_hashes[$file_hash])) {
                if (!isset($duplicates[$file_hash])) {
                    $duplicates[$file_hash] = array($file_hashes[$file_hash]);
                }
                $duplicates[$file_hash][] = $file_info;
            } else {
                $file_hashes[$file_hash] = $file_info;
            }
        }

        return $duplicates;
    }

    /**
     * ์Šคํ† ๋ฆฌ์ง€ ์‚ฌ์šฉ๋Ÿ‰ ๋ถ„์„
     */
    public static function analyzeStorageUsage()
    {
        $stats = array(
            'total_files' => 0,
            'total_size' => 0,
            'by_extension' => array(),
            'by_month' => array(),
            'largest_files' => array()
        );

        $args = new stdClass();
        $output = executeQuery('file.getFileStatistics', $args);

        if ($output->toBool() && $output->data) {
            foreach ($output->data as $file_info) {
                $stats['total_files']++;
                $stats['total_size'] += $file_info->file_size;

                // ํ™•์žฅ์ž๋ณ„ ํ†ต๊ณ„
                $ext = strtolower(pathinfo($file_info->source_filename, PATHINFO_EXTENSION));
                if (!isset($stats['by_extension'][$ext])) {
                    $stats['by_extension'][$ext] = array('count' => 0, 'size' => 0);
                }
                $stats['by_extension'][$ext]['count']++;
                $stats['by_extension'][$ext]['size'] += $file_info->file_size;

                // ์›”๋ณ„ ํ†ต๊ณ„
                $month = date('Y-m', strtotime($file_info->regdate));
                if (!isset($stats['by_month'][$month])) {
                    $stats['by_month'][$month] = array('count' => 0, 'size' => 0);
                }
                $stats['by_month'][$month]['count']++;
                $stats['by_month'][$month]['size'] += $file_info->file_size;

                // ํฐ ํŒŒ์ผ๋“ค
                $stats['largest_files'][] = array(
                    'filename' => $file_info->source_filename,
                    'size' => $file_info->file_size,
                    'regdate' => $file_info->regdate
                );
            }
        }

        // ํฐ ํŒŒ์ผ ์ •๋ ฌ (์ƒ์œ„ 10๊ฐœ)
        usort($stats['largest_files'], function($a, $b) {
            return $b['size'] - $a['size'];
        });
        $stats['largest_files'] = array_slice($stats['largest_files'], 0, 10);

        return $stats;
    }
}

ํŒŒ์ผ ๋ณด์•ˆ

์—…๋กœ๋“œ ํŒŒ์ผ ๊ฒ€์ฆ

class FileValidator
{
    private static $dangerous_extensions = array(
        'php', 'php3', 'php4', 'php5', 'phtml', 'asp', 'aspx', 'jsp', 'js', 'vbs', 'exe', 'com', 'bat', 'cmd', 'scr'
    );

    /**
     * ํŒŒ์ผ ์•ˆ์ „์„ฑ ๊ฒ€์‚ฌ
     */
    public static function validateFile($uploaded_file)
    {
        $errors = array();

        // ํ™•์žฅ์ž ๊ฒ€์‚ฌ
        $extension_check = self::checkExtension($uploaded_file['name']);
        if (!$extension_check['valid']) {
            $errors[] = $extension_check['message'];
        }

        // MIME ํƒ€์ž… ๊ฒ€์‚ฌ
        $mime_check = self::checkMimeType($uploaded_file['tmp_name'], $uploaded_file['type']);
        if (!$mime_check['valid']) {
            $errors[] = $mime_check['message'];
        }

        // ํŒŒ์ผ ๋‚ด์šฉ ๊ฒ€์‚ฌ
        $content_check = self::checkFileContent($uploaded_file['tmp_name']);
        if (!$content_check['valid']) {
            $errors[] = $content_check['message'];
        }

        // ํŒŒ์ผ ํฌ๊ธฐ ๊ฒ€์‚ฌ
        $size_check = self::checkFileSize($uploaded_file['size']);
        if (!$size_check['valid']) {
            $errors[] = $size_check['message'];
        }

        return array(
            'valid' => empty($errors),
            'errors' => $errors
        );
    }

    /**
     * ํ™•์žฅ์ž ๊ฒ€์‚ฌ
     */
    private static function checkExtension($filename)
    {
        $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));

        // ์œ„ํ—˜ํ•œ ํ™•์žฅ์ž ์ฐจ๋‹จ
        if (in_array($extension, self::$dangerous_extensions)) {
            return array(
                'valid' => false,
                'message' => '์—…๋กœ๋“œ๊ฐ€ ๊ธˆ์ง€๋œ ํŒŒ์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค: ' . $extension
            );
        }

        // ํ—ˆ์šฉ๋œ ํ™•์žฅ์ž ๋ชฉ๋ก ํ™•์ธ
        $allowed_extensions = Context::get('allowed_extensions');
        if ($allowed_extensions && !in_array($extension, $allowed_extensions)) {
            return array(
                'valid' => false,
                'message' => 'ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค: ' . $extension
            );
        }

        return array('valid' => true);
    }

    /**
     * MIME ํƒ€์ž… ๊ฒ€์‚ฌ
     */
    private static function checkMimeType($tmp_name, $declared_type)
    {
        if (!function_exists('finfo_open')) {
            return array('valid' => true); // finfo ํ™•์žฅ์ด ์—†์œผ๋ฉด ํ†ต๊ณผ
        }

        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $actual_type = finfo_file($finfo, $tmp_name);
        finfo_close($finfo);

        // ํ—ˆ์šฉ๋œ MIME ํƒ€์ž… ๋ชฉ๋ก
        $allowed_types = array(
            'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/bmp',
            'application/pdf', 'application/msword', 'application/vnd.ms-excel',
            'application/vnd.ms-powerpoint', 'text/plain', 'application/zip'
        );

        if (!in_array($actual_type, $allowed_types)) {
            return array(
                'valid' => false,
                'message' => 'ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํƒ€์ž…์ž…๋‹ˆ๋‹ค: ' . $actual_type
            );
        }

        return array('valid' => true);
    }

    /**
     * ํŒŒ์ผ ๋‚ด์šฉ ๊ฒ€์‚ฌ
     */
    private static function checkFileContent($tmp_name)
    {
        $file_content = file_get_contents($tmp_name, false, null, 0, 1024); // ์ฒซ 1KB๋งŒ ์ฝ๊ธฐ

        // PHP ํƒœ๊ทธ ๊ฒ€์‚ฌ
        if (strpos($file_content, '<?php') !== false || strpos($file_content, '<?=') !== false) {
            return array(
                'valid' => false,
                'message' => '์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ์ฝ”๋“œ๊ฐ€ ํฌํ•จ๋œ ํŒŒ์ผ์ž…๋‹ˆ๋‹ค.'
            );
        }

        // ์Šคํฌ๋ฆฝํŠธ ํƒœ๊ทธ ๊ฒ€์‚ฌ
        if (stripos($file_content, '<script') !== false) {
            return array(
                'valid' => false,
                'message' => '์Šคํฌ๋ฆฝํŠธ๊ฐ€ ํฌํ•จ๋œ ํŒŒ์ผ์ž…๋‹ˆ๋‹ค.'
            );
        }

        return array('valid' => true);
    }

    /**
     * ํŒŒ์ผ ํฌ๊ธฐ ๊ฒ€์‚ฌ
     */
    private static function checkFileSize($file_size)
    {
        $max_size = Context::get('max_file_size') ?: (2 * 1024 * 1024); // ๊ธฐ๋ณธ 2MB

        if ($file_size > $max_size) {
            return array(
                'valid' => false,
                'message' => 'ํŒŒ์ผ ํฌ๊ธฐ๊ฐ€ ๋„ˆ๋ฌด ํฝ๋‹ˆ๋‹ค. ์ตœ๋Œ€ ' . self::formatBytes($max_size) . '๊นŒ์ง€ ์—…๋กœ๋“œ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.'
            );
        }

        return array('valid' => true);
    }

    /**
     * ๋ฐ”์ดํŠธ๋ฅผ ์ฝ๊ธฐ ์‰ฌ์šด ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜
     */
    private static function formatBytes($bytes, $precision = 2)
    {
        $units = array('B', 'KB', 'MB', 'GB', 'TB');

        for ($i = 0; $bytes > 1024; $i++) {
            $bytes /= 1024;
        }

        return round($bytes, $precision) . ' ' . $units[$i];
    }
}

๋ชจ๋ฒ” ์‚ฌ๋ก€

  1. ๋ณด์•ˆ: ์—…๋กœ๋“œ ํŒŒ์ผ์— ๋Œ€ํ•œ ์ฒ ์ €ํ•œ ๊ฒ€์ฆ ์ˆ˜ํ–‰
  2. ์„ฑ๋Šฅ: ๋Œ€์šฉ๋Ÿ‰ ํŒŒ์ผ ์ฒ˜๋ฆฌ ์‹œ ์ฒญํฌ ๋‹จ์œ„๋กœ ์ฒ˜๋ฆฌ
  3. ์‚ฌ์šฉ์ž ๊ฒฝํ—˜: ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ๊ณผ ์ง„ํ–‰๋ฅ  ํ‘œ์‹œ ์ œ๊ณต
  4. ์ €์žฅ์†Œ ๊ด€๋ฆฌ: ์ •๊ธฐ์ ์ธ ํŒŒ์ผ ์ •๋ฆฌ์™€ ์ค‘๋ณต ์ œ๊ฑฐ
  5. ์ ‘๊ทผ ์ œ์–ด: ํŒŒ์ผ๋ณ„ ๊ถŒํ•œ ๊ด€๋ฆฌ๋กœ ๋ณด์•ˆ ๊ฐ•ํ™”

๋‹ค์Œ ๋‹จ๊ณ„

ํŒŒ์ผ ๊ด€๋ฆฌ๋ฅผ ๋งˆ์Šคํ„ฐํ–ˆ๋‹ค๋ฉด, ์œ„์ ฏ ์‚ฌ์šฉ๋ฒ•์—์„œ ๋””์ž์ธ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•์„ ํ•™์Šตํ•˜์„ธ์š”.