diff --git a/salesmanagement/api/sales_tenants.php b/salesmanagement/api/sales_tenants.php index 362f872..35095a4 100644 --- a/salesmanagement/api/sales_tenants.php +++ b/salesmanagement/api/sales_tenants.php @@ -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("권한이 없습니다."); diff --git a/salesmanagement/index.php b/salesmanagement/index.php index 6e79d58..7c4f07a 100644 --- a/salesmanagement/index.php +++ b/salesmanagement/index.php @@ -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 = '';