대용량 파일 업로드 지원을 위한 청크(Chunked) 업로드 방식 도입
This commit is contained in:
@@ -387,6 +387,64 @@ try {
|
||||
$stmt->execute([$tenant_id, $currentUser['id'], $scenario_type, $step_id, $log_text, $audio_file_path, $consultation_type]);
|
||||
|
||||
echo json_encode(['success' => true, 'message' => '기록이 저장되었습니다.']);
|
||||
} elseif ($action === 'upload_chunk') {
|
||||
$upload_id = $data['uploadId'] ?? '';
|
||||
$chunk_index = intval($data['chunkIndex'] ?? 0);
|
||||
$total_chunks = intval($data['totalChunks'] ?? 1);
|
||||
$filename = $data['fileName'] ?? 'uploaded_file';
|
||||
$tenant_id = $data['tenant_id'] ?? null;
|
||||
$scenario_type = $data['scenario_type'] ?? 'manager';
|
||||
$step_id = $data['step_id'] ?? null;
|
||||
|
||||
if (!$upload_id || !isset($_FILES['file'])) throw new Exception("청크 데이터가 유효하지 않습니다.");
|
||||
|
||||
$chunk_dir = __DIR__ . "/../uploads/chunks/" . $upload_id;
|
||||
if (!file_exists($chunk_dir)) mkdir($chunk_dir, 0777, true);
|
||||
|
||||
$chunk_file = $chunk_dir . "/" . $chunk_index;
|
||||
if (!move_uploaded_file($_FILES['file']['tmp_name'], $chunk_file)) {
|
||||
throw new Exception("청크 저장 실패");
|
||||
}
|
||||
|
||||
// 모든 청크가 도착했는지 확인
|
||||
$received_chunks = count(glob($chunk_dir . "/*"));
|
||||
if ($received_chunks === $total_chunks) {
|
||||
$upload_dir = __DIR__ . "/../uploads/attachments/" . $tenant_id . "/";
|
||||
if (!file_exists($upload_dir)) mkdir($upload_dir, 0777, true);
|
||||
|
||||
$file_ext = pathinfo($filename, PATHINFO_EXTENSION);
|
||||
$file_name_only = pathinfo($filename, PATHINFO_FILENAME);
|
||||
$new_filename = date('Ymd_His') . "_" . uniqid() . "_" . $filename;
|
||||
$final_path = $upload_dir . $new_filename;
|
||||
|
||||
$out = fopen($final_path, "wb");
|
||||
for ($i = 0; $i < $total_chunks; $i++) {
|
||||
$chunk_path = $chunk_dir . "/" . $i;
|
||||
if (!file_exists($chunk_path)) throw new Exception("청크 누락: " . $i);
|
||||
$in = fopen($chunk_path, "rb");
|
||||
while ($buff = fread($in, 4096)) {
|
||||
fwrite($out, $buff);
|
||||
}
|
||||
fclose($in);
|
||||
unlink($chunk_path);
|
||||
}
|
||||
fclose($out);
|
||||
rmdir($chunk_dir);
|
||||
|
||||
$save_path = "uploads/attachments/" . $tenant_id . "/" . $new_filename;
|
||||
$saved_paths = [[
|
||||
'name' => $filename,
|
||||
'path' => $save_path,
|
||||
'size' => filesize($final_path)
|
||||
]];
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO sales_tenant_consultations (tenant_id, manager_id, scenario_type, step_id, log_text, attachment_paths, consultation_type) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$tenant_id, $currentUser['id'], $scenario_type, $step_id, '첨부파일 업로드 (대용량)', json_encode($saved_paths, JSON_UNESCAPED_UNICODE), 'file']);
|
||||
|
||||
echo json_encode(['success' => true, 'completed' => true, 'message' => '업로드 완료']);
|
||||
} else {
|
||||
echo json_encode(['success' => true, 'completed' => false, 'chunk' => $chunk_index]);
|
||||
}
|
||||
} elseif ($action === 'upload_attachments') {
|
||||
$tenant_id = $data['tenant_id'] ?? null;
|
||||
if (!checkTenantPermission($pdo, $tenant_id, $currentUser)) throw new Exception("권한이 없습니다.");
|
||||
|
||||
@@ -2428,40 +2428,49 @@
|
||||
const selectedFiles = Array.from(e.target.files);
|
||||
if (selectedFiles.length === 0) return;
|
||||
|
||||
// 클라이언트 사이드 용량 체크 (기본 20MB)
|
||||
const MAX_SIZE = 20 * 1024 * 1024;
|
||||
for (const file of selectedFiles) {
|
||||
if (file.size > MAX_SIZE) {
|
||||
alert(`파일 '${file.name}'의 용량이 너무 큽니다. (최대 20MB)`);
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setUploading(true);
|
||||
const formData = new FormData();
|
||||
formData.append('tenant_id', tenantId);
|
||||
formData.append('scenario_type', scenarioType);
|
||||
formData.append('step_id', stepId);
|
||||
formData.append('consultation_type', 'file');
|
||||
selectedFiles.forEach(file => formData.append('files[]', file));
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetch('api/sales_tenants.php?action=upload_attachments', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.status === 413) {
|
||||
throw new Error('파일 용량이 서버 허용 범위를 초과했습니다. (413 Content Too Large)');
|
||||
const CHUNK_SIZE = 1 * 1024 * 1024; // 1MB chunks
|
||||
|
||||
for (const file of selectedFiles) {
|
||||
const uploadId = Date.now().toString(36) + Math.random().toString(36).substring(2);
|
||||
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
|
||||
|
||||
for (let i = 0; i < totalChunks; i++) {
|
||||
const chunk = file.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
|
||||
const formData = new FormData();
|
||||
formData.append('file', chunk);
|
||||
formData.append('uploadId', uploadId);
|
||||
formData.append('chunkIndex', i);
|
||||
formData.append('totalChunks', totalChunks);
|
||||
formData.append('fileName', file.name);
|
||||
formData.append('tenant_id', tenantId);
|
||||
formData.append('scenario_type', scenarioType);
|
||||
formData.append('step_id', stepId);
|
||||
formData.append('consultation_type', 'file');
|
||||
|
||||
const response = await fetch('api/sales_tenants.php?action=upload_chunk', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.status === 413) {
|
||||
throw new Error(`파일 '${file.name}'이 서버의 단일 요청 제한을 초과했습니다. 관리자에게 Nginx 수정을 요청하거나 더 작은 청크를 사용해야 합니다.`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || '청크 업로드 실패');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) loadFiles();
|
||||
else alert('업로드 실패: ' + result.error);
|
||||
loadFiles();
|
||||
alert('모든 파일이 성공적으로 업로드되었습니다.');
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
alert('업로드 중 오류가 발생했습니다.');
|
||||
alert('업로드 중 오류가 발생했습니다: ' + error.message);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||
|
||||
Reference in New Issue
Block a user