Initial profile site commit

This commit is contained in:
2026-05-31 21:05:59 +09:00
commit db81f0e4a4
49 changed files with 18829 additions and 0 deletions
+229
View File
@@ -0,0 +1,229 @@
<?php
require_once 'config.php';
require_once __DIR__ . '/error_config.php';
session_start();
set_json_headers();
require_auth();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
json_response(['error' => '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();
json_response(['error' => '파일 저장 실패', 'detail' => $err, 'target' => $target_path, 'tmp' => $file['tmp_name'], 'tmp_exists' => file_exists($file['tmp_name'])], 500);
}