feat:상세모달에 브라우저 내장 오디오 플레이어 추가

- DetailModal에 <audio> 태그 기반 재생기 추가 (다운로드 없이 바로 재생)
- 다운로드 엔드포인트에 ?inline=1 파라미터 지원 (스트리밍 재생용)
- Content-Length, Accept-Ranges 헤더 추가
- 플레이어 옆 다운로드 버튼 배치

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-07 14:24:59 +09:00
parent 29a670522a
commit 6d02cc5e5e
2 changed files with 25 additions and 3 deletions

View File

@@ -229,7 +229,7 @@ public function status(int $id): JsonResponse
/**
* GCS 음성파일 다운로드
*/
public function download(int $id): Response|JsonResponse
public function download(Request $request, int $id): Response|JsonResponse
{
$recording = AiVoiceRecording::find($id);
@@ -252,6 +252,7 @@ public function download(int $id): Response|JsonResponse
// 파일 확장자 추출
$extension = pathinfo($recording->audio_file_path, PATHINFO_EXTENSION) ?: 'webm';
$mimeType = 'audio/'.$extension;
// 파일명 생성 (한글 제목 지원)
$title = $recording->title ?: 'recording';
@@ -259,8 +260,12 @@ public function download(int $id): Response|JsonResponse
$filename = $safeTitle.'.'.$extension;
$encodedFilename = rawurlencode($filename);
$disposition = $request->query('inline') ? 'inline' : 'attachment';
return response($content)
->header('Content-Type', 'audio/'.$extension)
->header('Content-Disposition', "attachment; filename=\"{$encodedFilename}\"; filename*=UTF-8''{$encodedFilename}");
->header('Content-Type', $mimeType)
->header('Content-Length', strlen($content))
->header('Accept-Ranges', 'bytes')
->header('Content-Disposition', "{$disposition}; filename=\"{$encodedFilename}\"; filename*=UTF-8''{$encodedFilename}");
}
}

View File

@@ -755,6 +755,23 @@ className={`flex-1 text-xs font-medium py-1.5 rounded-md transition-colors ${tab
)}
</div>
{/* 오디오 플레이어 */}
{data.audio_file_path && (
<div className="px-6 py-3 bg-gray-50 border-b border-gray-100">
<div className="flex items-center gap-3">
<Icon name="volume-2" className="w-4 h-4 text-purple-500 flex-shrink-0" />
<audio controls className="flex-1 h-9" style={{minWidth:0}}>
<source src={`${API}/${data.id}/download?inline=1`} type="audio/webm" />
</audio>
<a href={`${API}/${data.id}/download`} download
className="p-1.5 text-gray-400 hover:text-blue-600 hover:bg-blue-100 rounded-lg transition-colors flex-shrink-0"
title="파일 다운로드">
<Icon name="download" className="w-4 h-4" />
</a>
</div>
</div>
)}
{/* Body */}
<div className="flex-1 overflow-y-auto px-6 py-4">
{data.status === 'PROCESSING' && (