'POST만 허용됩니다'], 405); } if (!isset($_FILES['file']) && !isset($_FILES['image'])) { json_response(['error' => '파일 업로드 실패'], 400); } // 'file' 또는 'image' 둘 다 받기 (호환성) $file = $_FILES['file'] ?? $_FILES['image']; if ($file['error'] !== UPLOAD_ERR_OK) { json_response(['error' => '파일 업로드 실패 (코드: ' . $file['error'] . ')'], 400); } // ===================================================== // 파일 사이즈 조정 // 이미지 5MB / 동영상 100MB / 일반 파일 50MB // 단위: byte (1MB = 1024 * 1024) // 제한 없음으로 하려면 PHP_INT_MAX로 설정 // 단, Synology PHP의 upload_max_filesize / post_max_size 설정도 함께 조정 필요 // ===================================================== $MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 이미지: 5 MB $MAX_VIDEO_SIZE = 100 * 1024 * 1024; // 동영상: 100 MB $MAX_FILE_SIZE = 50 * 1024 * 1024; // 일반 파일: 50 MB // ===================================================== // 허용 MIME 타입과 확장자 $image_types = [ 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp' ]; $video_types = [ 'video/mp4' => 'mp4', 'video/webm' => 'webm', 'video/ogg' => 'ogv', 'video/quicktime' => 'mov' ]; // 일반 파일 허용 목록 (MIME → 확장자) $file_types = [ 'application/pdf' => 'pdf', 'application/zip' => 'zip', 'application/x-zip-compressed' => 'zip', 'application/x-7z-compressed' => '7z', 'application/x-rar-compressed' => 'rar', 'application/vnd.ms-excel' => 'xls', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx', 'application/msword' => 'doc', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx', 'application/vnd.ms-powerpoint' => 'ppt', 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx', 'text/plain' => 'txt', 'text/markdown' => 'md', 'text/csv' => 'csv', 'application/json' => 'json', ]; $finfo = new finfo(FILEINFO_MIME_TYPE); $mime = $finfo->file($file['tmp_name']); $is_image = isset($image_types[$mime]); $is_video = isset($video_types[$mime]); $is_file = !$is_image && !$is_video && isset($file_types[$mime]); if (!$is_image && !$is_video && !$is_file) { json_response(['error' => '허용되지 않는 파일 형식입니다. (이미지/동영상/PDF/ZIP/문서 등)'], 400); } // 파일 크기 검증 if ($is_image && $file['size'] > $MAX_IMAGE_SIZE) { $mb = round($MAX_IMAGE_SIZE / 1024 / 1024, 1); json_response(['error' => "이미지 파일은 {$mb}MB 이하여야 합니다"], 400); } if ($is_video && $file['size'] > $MAX_VIDEO_SIZE) { $mb = round($MAX_VIDEO_SIZE / 1024 / 1024, 1); json_response(['error' => "동영상 파일은 {$mb}MB 이하여야 합니다"], 400); } if ($is_file && $file['size'] > $MAX_FILE_SIZE) { $mb = round($MAX_FILE_SIZE / 1024 / 1024, 1); json_response(['error' => "파일은 {$mb}MB 이하여야 합니다"], 400); } if ($is_image) { $ext = $image_types[$mime]; $kind = 'image'; $prefix = 'img_'; } elseif ($is_video) { $ext = $video_types[$mime]; $kind = 'video'; $prefix = 'vid_'; } else { $ext = $file_types[$mime]; $kind = 'file'; $prefix = 'file_'; // 일반 파일은 원본 파일명 일부를 살림 (정리 후) $origName = pathinfo($file['name'], PATHINFO_FILENAME); $origName = preg_replace('/[^\p{L}\p{N}\-_]/u', '_', $origName); $origName = mb_substr($origName, 0, 30, 'UTF-8'); $prefix = $origName . '_'; } // ===================================================== // 폴더명 생성 // ===================================================== function sanitize_folder_name($title) { $title = trim($title); if ($title === '') return ''; // 위험한 문자 제거 $title = preg_replace('/[\/\\\\\:\*\?"<>\|]/u', '', $title); $title = preg_replace('/\.\.+/u', '', $title); // 공백을 하이픈으로 $title = preg_replace('/\s+/u', '-', $title); // 영문/숫자/한글/하이픈/언더스코어 외 모두 제거 $title = preg_replace('/[^\p{L}\p{N}\-_]/u', '', $title); $title = trim($title, '-_'); if (mb_strlen($title, 'UTF-8') > 50) { $title = mb_substr($title, 0, 50, 'UTF-8'); } return $title; } $projectTitle = isset($_POST['project_title']) ? trim($_POST['project_title']) : ''; $folderName = sanitize_folder_name($projectTitle); if ($folderName === '') { $folderName = '_untitled'; } $uploadDir = UPLOADS_DIR . '/' . $folderName; if (!is_dir(UPLOADS_DIR)) { @mkdir(UPLOADS_DIR, 0775, true); } if (!is_dir($uploadDir)) { if (!@mkdir($uploadDir, 0775, true)) { json_response(['error' => '폴더 생성 실패. uploads 폴더에 쓰기 권한을 확인해주세요.'], 500); } } // 안전한 파일명 생성 $filename = uniqid($prefix, true) . '.' . $ext; $target_path = $uploadDir . '/' . $filename; // ===================================================== // 이미지: WebP 변환 시도 (GD 지원 시) // 동영상/일반 파일은 원본 그대로 저장 // ===================================================== function try_save_as_webp(string $tmp, string $dest_dir, string $base_name, string $mime, int $quality = 82): string|false { if (!function_exists('imagewebp')) return false; $src = match ($mime) { 'image/jpeg' => @imagecreatefromjpeg($tmp), 'image/png' => @imagecreatefrompng($tmp), 'image/gif' => @imagecreatefromgif($tmp), 'image/webp' => @imagecreatefromwebp($tmp), default => false, }; if (!$src) return false; // PNG 투명도 보존 if ($mime === 'image/png') { imagepalettetotruecolor($src); imagealphablending($src, true); imagesavealpha($src, true); } // 가로 1200px 초과 시 리사이즈 $ow = imagesx($src); $oh = imagesy($src); if ($ow > 1200) { $nw = 1200; $nh = (int)round($oh * (1200 / $ow)); $dst = imagecreatetruecolor($nw, $nh); imagealphablending($dst, false); imagesavealpha($dst, true); $t = imagecolorallocatealpha($dst, 0, 0, 0, 127); imagefilledrectangle($dst, 0, 0, $nw, $nh, $t); imagecopyresampled($dst, $src, 0, 0, 0, 0, $nw, $nh, $ow, $oh); imagedestroy($src); $src = $dst; } $dest_path = $dest_dir . '/' . $base_name . '.webp'; $ok = imagewebp($src, $dest_path, $quality); imagedestroy($src); return $ok ? $dest_path : false; } if ($is_image) { $base_name = pathinfo($filename, PATHINFO_FILENAME); $webp_path = try_save_as_webp($file['tmp_name'], $uploadDir, $base_name, $mime); if ($webp_path !== false) { $filename = $base_name . '.webp'; $target_path = $webp_path; $saved = true; } else { $saved = move_uploaded_file($file['tmp_name'], $target_path); } } else { $saved = move_uploaded_file($file['tmp_name'], $target_path); } if ($saved) { json_response([ 'success' => true, 'url' => 'uploads/' . $folderName . '/' . $filename, 'filename' => $filename, 'original_name' => $file['name'], 'folder' => $folderName, 'kind' => $kind ]); } else { $err = error_get_last(); error_log('Upload save failed: ' . json_encode([ 'detail' => $err, 'target' => $target_path, 'tmp_exists' => file_exists($file['tmp_name']) ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); json_response(['error' => '파일 저장 실패'], 500); }