ํ์ผ ๊ด๋ฆฌ¶
๋ผ์ด๋ฏน์ค์์ ํ์ผ๊ณผ ์ฒจ๋ถํ์ผ์ ํจ๊ณผ์ ์ผ๋ก ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ํ์ตํฉ๋๋ค.
ํ์ผ ์ ๋ก๋ ๊ธฐ๋ณธ¶
์ ๋ก๋ ์ค์ ¶
// 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];
}
}
๋ชจ๋ฒ ์ฌ๋ก¶
- ๋ณด์: ์ ๋ก๋ ํ์ผ์ ๋ํ ์ฒ ์ ํ ๊ฒ์ฆ ์ํ
- ์ฑ๋ฅ: ๋์ฉ๋ ํ์ผ ์ฒ๋ฆฌ ์ ์ฒญํฌ ๋จ์๋ก ์ฒ๋ฆฌ
- ์ฌ์ฉ์ ๊ฒฝํ: ๋๋๊ทธ ์ค ๋๋กญ๊ณผ ์งํ๋ฅ ํ์ ์ ๊ณต
- ์ ์ฅ์ ๊ด๋ฆฌ: ์ ๊ธฐ์ ์ธ ํ์ผ ์ ๋ฆฌ์ ์ค๋ณต ์ ๊ฑฐ
- ์ ๊ทผ ์ ์ด: ํ์ผ๋ณ ๊ถํ ๊ด๋ฆฌ๋ก ๋ณด์ ๊ฐํ
๋ค์ ๋จ๊ณ¶
ํ์ผ ๊ด๋ฆฌ๋ฅผ ๋ง์คํฐํ๋ค๋ฉด, ์์ ฏ ์ฌ์ฉ๋ฒ์์ ๋์์ธ ์ปค์คํฐ๋ง์ด์ง์ ํ์ตํ์ธ์.