191 lines
7.4 KiB
PHP
191 lines
7.4 KiB
PHP
|
|
<?php
|
||
|
|
// 출력 버퍼링 시작
|
||
|
|
ob_start();
|
||
|
|
error_reporting(0);
|
||
|
|
|
||
|
|
require_once($_SERVER['DOCUMENT_ROOT'] . "/session.php");
|
||
|
|
require_once($_SERVER['DOCUMENT_ROOT'] . "/lib/mydb.php");
|
||
|
|
|
||
|
|
// 권한 체크
|
||
|
|
if (!isset($user_id) || $level > 5) {
|
||
|
|
header('HTTP/1.0 403 Forbidden');
|
||
|
|
die('접근 권한이 없습니다.');
|
||
|
|
}
|
||
|
|
|
||
|
|
// 회의 ID 확인
|
||
|
|
$meeting_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
|
||
|
|
$tenant_id = $user_id; // session.php에서 user_id를 tenant_id로 사용
|
||
|
|
|
||
|
|
if ($meeting_id <= 0) {
|
||
|
|
header('HTTP/1.0 400 Bad Request');
|
||
|
|
die('잘못된 요청입니다.');
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
$pdo = db_connect();
|
||
|
|
|
||
|
|
// MVP 단계: 모든 사용자가 모든 데이터를 볼 수 있도록 tenant_id 조건 제거
|
||
|
|
$sql = "SELECT audio_file_path, title, created_at
|
||
|
|
FROM meeting_logs
|
||
|
|
WHERE id = ?";
|
||
|
|
$stmt = $pdo->prepare($sql);
|
||
|
|
$stmt->execute([$meeting_id]);
|
||
|
|
$meeting = $stmt->fetch(PDO::FETCH_ASSOC);
|
||
|
|
|
||
|
|
if (!$meeting || empty($meeting['audio_file_path'])) {
|
||
|
|
header('HTTP/1.0 404 Not Found');
|
||
|
|
die('오디오 파일을 찾을 수 없습니다.');
|
||
|
|
}
|
||
|
|
|
||
|
|
// 파일 경로 구성
|
||
|
|
$file_path = $_SERVER['DOCUMENT_ROOT'] . $meeting['audio_file_path'];
|
||
|
|
|
||
|
|
// 경로 정규화 (슬래시 통일)
|
||
|
|
$file_path = str_replace('\\', '/', $file_path);
|
||
|
|
$file_path = preg_replace('#/+#', '/', $file_path);
|
||
|
|
|
||
|
|
// 파일 존재 확인
|
||
|
|
if (!file_exists($file_path)) {
|
||
|
|
// 디버깅 정보
|
||
|
|
error_log('다운로드 실패 - 파일 경로: ' . $file_path);
|
||
|
|
error_log('다운로드 실패 - DB 경로: ' . $meeting['audio_file_path']);
|
||
|
|
error_log('다운로드 실패 - DOCUMENT_ROOT: ' . $_SERVER['DOCUMENT_ROOT']);
|
||
|
|
error_log('다운로드 실패 - 파일 존재 여부: ' . (file_exists($file_path) ? 'YES' : 'NO'));
|
||
|
|
|
||
|
|
// 대체 경로 시도 (경로가 잘못된 경우)
|
||
|
|
$alt_path = $_SERVER['DOCUMENT_ROOT'] . str_replace('//', '/', $meeting['audio_file_path']);
|
||
|
|
if (file_exists($alt_path)) {
|
||
|
|
$file_path = $alt_path;
|
||
|
|
} else {
|
||
|
|
header('HTTP/1.0 404 Not Found');
|
||
|
|
header('Content-Type: text/plain; charset=utf-8');
|
||
|
|
die('오디오 파일이 서버에 존재하지 않습니다.\n경로: ' . $file_path);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 파일 확장자 확인 및 MIME 타입 설정
|
||
|
|
// 파일 경로에서 직접 확장자 추출
|
||
|
|
$file_extension = '';
|
||
|
|
if (preg_match('/\.([a-z0-9]+)$/i', $file_path, $matches)) {
|
||
|
|
$file_extension = strtolower($matches[1]);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 확장자가 없으면 경로에서 추출 시도
|
||
|
|
if (empty($file_extension)) {
|
||
|
|
$path_info = pathinfo($file_path);
|
||
|
|
$file_extension = isset($path_info['extension']) ? strtolower($path_info['extension']) : '';
|
||
|
|
}
|
||
|
|
|
||
|
|
// 확장자가 여전히 없으면 webm으로 기본 설정
|
||
|
|
if (empty($file_extension)) {
|
||
|
|
$file_extension = 'webm';
|
||
|
|
}
|
||
|
|
|
||
|
|
$mime_types = [
|
||
|
|
'webm' => 'audio/webm',
|
||
|
|
'wav' => 'audio/wav',
|
||
|
|
'mp3' => 'audio/mpeg',
|
||
|
|
'ogg' => 'audio/ogg',
|
||
|
|
'm4a' => 'audio/mp4'
|
||
|
|
];
|
||
|
|
|
||
|
|
$content_type = isset($mime_types[$file_extension])
|
||
|
|
? $mime_types[$file_extension]
|
||
|
|
: 'audio/webm'; // 기본값을 audio/webm으로 설정
|
||
|
|
|
||
|
|
// 다운로드 파일명 생성 (회의록 + 제목 + 날짜 + 확장자)
|
||
|
|
$title = $meeting['title'] ?: '회의녹음';
|
||
|
|
$date = date('Ymd_His', strtotime($meeting['created_at']));
|
||
|
|
|
||
|
|
// 파일명 안전하게 처리 (특수문자 제거, 공백을 언더스코어로)
|
||
|
|
$safe_title = preg_replace('/[^a-zA-Z0-9가-힣_\-]/u', '_', $title);
|
||
|
|
$safe_title = preg_replace('/\s+/', '_', $safe_title);
|
||
|
|
$safe_title = trim($safe_title, '_');
|
||
|
|
|
||
|
|
// 제목이 비어있거나 언더스코어만 있는 경우 기본값 사용
|
||
|
|
if (empty($safe_title) || preg_match('/^_+$/', $safe_title)) {
|
||
|
|
$safe_title = '회의녹음';
|
||
|
|
}
|
||
|
|
|
||
|
|
// 제목에 "회의록"이 이미 포함되어 있으면 제거 (중복 방지)
|
||
|
|
$safe_title = preg_replace('/^회의록[_\s]*/u', '', $safe_title);
|
||
|
|
$safe_title = preg_replace('/[_\s]*회의록$/u', '', $safe_title);
|
||
|
|
$safe_title = trim($safe_title, '_');
|
||
|
|
|
||
|
|
// 제목이 비어있으면 기본값 사용
|
||
|
|
if (empty($safe_title)) {
|
||
|
|
$safe_title = '회의녹음';
|
||
|
|
}
|
||
|
|
|
||
|
|
// 파일명 앞에 "회의록" 추가 (한 번만)
|
||
|
|
$download_filename = '회의록_' . $safe_title . '_' . $date . '.' . $file_extension;
|
||
|
|
|
||
|
|
// 출력 버퍼 비우기
|
||
|
|
ob_clean();
|
||
|
|
|
||
|
|
// 파일명이 확장자를 포함하는지 최종 확인
|
||
|
|
if (!preg_match('/\.' . preg_quote($file_extension, '/') . '$/i', $download_filename)) {
|
||
|
|
$download_filename .= '.' . $file_extension;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 헤더 설정 (브라우저 호환성을 위해 파일명 처리)
|
||
|
|
header('Content-Type: ' . $content_type);
|
||
|
|
// 파일명 처리: "회의록"을 "Meeting"으로 변환하여 브라우저 호환성 확보
|
||
|
|
$ascii_filename = str_replace('회의록', 'Meeting', $download_filename);
|
||
|
|
// "Meeting"이 중복되지 않도록 처리 (제목에 이미 "Meeting"이 포함된 경우)
|
||
|
|
$ascii_filename = preg_replace('/Meeting[_\s]*Meeting/i', 'Meeting', $ascii_filename);
|
||
|
|
// 나머지 한글과 특수문자를 언더스코어로 변환
|
||
|
|
$ascii_filename = preg_replace('/[^a-zA-Z0-9._-]/', '_', $ascii_filename);
|
||
|
|
// 연속된 언더스코어를 하나로 통합
|
||
|
|
$ascii_filename = preg_replace('/_+/', '_', $ascii_filename);
|
||
|
|
// 확장자가 확실히 포함되도록 재확인
|
||
|
|
if (!preg_match('/\.' . preg_quote($file_extension, '/') . '$/i', $ascii_filename)) {
|
||
|
|
$ascii_filename .= '.' . $file_extension;
|
||
|
|
}
|
||
|
|
// 파일 크기 재확인
|
||
|
|
$file_size = filesize($file_path);
|
||
|
|
if ($file_size === false || $file_size == 0) {
|
||
|
|
header('HTTP/1.0 500 Internal Server Error');
|
||
|
|
header('Content-Type: text/plain; charset=utf-8');
|
||
|
|
die('파일을 읽을 수 없습니다. (크기: ' . $file_size . ')');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Content-Disposition 헤더를 더 명확하게 설정 (RFC 5987 형식)
|
||
|
|
// 브라우저 호환성을 위해 두 가지 형식 모두 사용
|
||
|
|
$encoded_filename = rawurlencode($ascii_filename);
|
||
|
|
header('Content-Disposition: attachment; filename="' . $ascii_filename . '"; filename*=UTF-8\'\'' . $encoded_filename);
|
||
|
|
header('Content-Length: ' . $file_size);
|
||
|
|
header('Content-Transfer-Encoding: binary');
|
||
|
|
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
|
||
|
|
header('Pragma: public');
|
||
|
|
header('Expires: 0');
|
||
|
|
header('X-Content-Type-Options: nosniff');
|
||
|
|
|
||
|
|
// 디버깅용 (필요시 주석 해제)
|
||
|
|
// error_log('Download: ' . $download_filename . ' (extension: ' . $file_extension . ', path: ' . $file_path . ', size: ' . $file_size . ')');
|
||
|
|
|
||
|
|
// 파일 출력 (청크 단위로 읽어서 메모리 효율성 향상)
|
||
|
|
$handle = @fopen($file_path, 'rb');
|
||
|
|
if ($handle === false) {
|
||
|
|
header('HTTP/1.0 500 Internal Server Error');
|
||
|
|
header('Content-Type: text/plain; charset=utf-8');
|
||
|
|
die('파일을 열 수 없습니다.');
|
||
|
|
}
|
||
|
|
|
||
|
|
// 청크 단위로 출력 (8KB씩)
|
||
|
|
while (!feof($handle)) {
|
||
|
|
$chunk = fread($handle, 8192);
|
||
|
|
if ($chunk === false) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
echo $chunk;
|
||
|
|
flush();
|
||
|
|
}
|
||
|
|
fclose($handle);
|
||
|
|
exit;
|
||
|
|
|
||
|
|
} catch (Exception $e) {
|
||
|
|
header('HTTP/1.0 500 Internal Server Error');
|
||
|
|
die('파일 다운로드 중 오류가 발생했습니다.');
|
||
|
|
}
|