Files
sam-kd/voice_ai_cnslt/process_consult.php
hskwon aca1767eb9 초기 커밋: 5130 레거시 시스템
- URL 하드코딩 → .env APP_URL 기반 동적 URL로 변경
- DB 연결 하드코딩 → .env 기반으로 변경
- MySQL strict mode DATE 오류 수정
2025-12-10 20:14:31 +09:00

808 lines
31 KiB
PHP

<?php
// 출력 버퍼링 시작 및 에러 리포팅 비활성화
error_reporting(0);
ob_start();
require_once($_SERVER['DOCUMENT_ROOT'] . "/session.php");
require_once($_SERVER['DOCUMENT_ROOT'] . "/lib/mydb.php");
// GCS 업로드 함수
function uploadToGCS($file_path, $bucket_name, $object_name, $service_account_path) {
if (!file_exists($service_account_path)) {
error_log('GCS 업로드 실패: 서비스 계정 파일 없음');
return false;
}
$serviceAccount = json_decode(file_get_contents($service_account_path), true);
if (!$serviceAccount) {
error_log('GCS 업로드 실패: 서비스 계정 JSON 파싱 오류');
return false;
}
// OAuth 2.0 토큰 생성
$now = time();
$jwtHeader = base64_encode(json_encode(['alg' => 'RS256', 'typ' => 'JWT']));
$jwtClaim = base64_encode(json_encode([
'iss' => $serviceAccount['client_email'],
'scope' => 'https://www.googleapis.com/auth/devstorage.full_control',
'aud' => 'https://oauth2.googleapis.com/token',
'exp' => $now + 3600,
'iat' => $now
]));
$privateKey = openssl_pkey_get_private($serviceAccount['private_key']);
if (!$privateKey) {
error_log('GCS 업로드 실패: 개인 키 읽기 오류');
return false;
}
openssl_sign($jwtHeader . '.' . $jwtClaim, $signature, $privateKey, OPENSSL_ALGO_SHA256);
openssl_free_key($privateKey);
$jwt = $jwtHeader . '.' . $jwtClaim . '.' . base64_encode($signature);
// OAuth 토큰 요청
$tokenCh = curl_init('https://oauth2.googleapis.com/token');
curl_setopt($tokenCh, CURLOPT_POST, true);
curl_setopt($tokenCh, CURLOPT_POSTFIELDS, http_build_query([
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion' => $jwt
]));
curl_setopt($tokenCh, CURLOPT_RETURNTRANSFER, true);
curl_setopt($tokenCh, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);
$tokenResponse = curl_exec($tokenCh);
$tokenCode = curl_getinfo($tokenCh, CURLINFO_HTTP_CODE);
curl_close($tokenCh);
if ($tokenCode !== 200) {
error_log('GCS 업로드 실패: OAuth 토큰 요청 실패 (HTTP ' . $tokenCode . ')');
return false;
}
$tokenData = json_decode($tokenResponse, true);
if (!isset($tokenData['access_token'])) {
error_log('GCS 업로드 실패: OAuth 토큰 없음');
return false;
}
$accessToken = $tokenData['access_token'];
// GCS에 파일 업로드
$file_content = file_get_contents($file_path);
$mime_type = mime_content_type($file_path) ?: 'audio/webm';
$upload_url = 'https://storage.googleapis.com/upload/storage/v1/b/' .
urlencode($bucket_name) . '/o?uploadType=media&name=' .
urlencode($object_name);
$ch = curl_init($upload_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $accessToken,
'Content-Type: ' . $mime_type,
'Content-Length: ' . strlen($file_content)
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $file_content);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code === 200) {
return 'gs://' . $bucket_name . '/' . $object_name;
} else {
error_log('GCS 업로드 실패 (HTTP ' . $code . '): ' . $response);
return false;
}
}
// 출력 버퍼 비우기
ob_clean();
header('Content-Type: application/json; charset=utf-8');
// 1. 권한 및 세션 체크
if (!isset($user_id) || $level > 5) {
echo json_encode(['ok' => false, 'error' => '접근 권한이 없습니다.']);
exit;
}
$tenant_id = $user_id; // session.php에서 user_id를 tenant_id로 사용
$upload_dir = $_SERVER['DOCUMENT_ROOT'] . "/uploads/consults/" . $tenant_id . "/";
// 2. 파일 업로드 처리
if (!file_exists($upload_dir)) mkdir($upload_dir, 0777, true);
if (!isset($_FILES['audio_file'])) {
echo json_encode(['ok' => false, 'error' => '오디오 파일이 없습니다.']);
exit;
}
// 파일 크기 확인
if ($_FILES['audio_file']['size'] == 0) {
echo json_encode(['ok' => false, 'error' => '오디오 파일이 비어있습니다. 녹음이 제대로 되지 않았을 수 있습니다.']);
exit;
}
$file_name = date('Ymd_His') . "_" . uniqid() . ".webm";
$file_path = $upload_dir . $file_name;
if (!move_uploaded_file($_FILES['audio_file']['tmp_name'], $file_path)) {
echo json_encode(['ok' => false, 'error' => '파일 저장 실패']);
exit;
}
// 3. STT 변환 (Google Cloud Speech-to-Text API)
// 서비스 계정 JSON 파일 또는 API 키 사용
$googleServiceAccountFile = $_SERVER['DOCUMENT_ROOT'] . '/apikey/google_service_account.json';
$googleApiKeyFile = $_SERVER['DOCUMENT_ROOT'] . '/apikey/google_api.txt';
$accessToken = null;
// 서비스 계정 JSON 파일이 있으면 OAuth 2.0 토큰 생성
if (file_exists($googleServiceAccountFile)) {
$serviceAccount = json_decode(file_get_contents($googleServiceAccountFile), true);
if (!$serviceAccount) {
echo json_encode(['ok' => false, 'error' => '서비스 계정 JSON 파일 형식이 올바르지 않습니다.']);
exit;
}
// JWT 생성 및 OAuth 2.0 토큰 요청
$now = time();
// JWT 헤더
$jwtHeader = [
'alg' => 'RS256',
'typ' => 'JWT'
];
// JWT 클레임
$jwtClaim = [
'iss' => $serviceAccount['client_email'],
'scope' => 'https://www.googleapis.com/auth/cloud-platform',
'aud' => 'https://oauth2.googleapis.com/token',
'exp' => $now + 3600,
'iat' => $now
];
// Base64 URL 인코딩 (표준 JWT 형식)
$base64UrlEncode = function($data) {
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
};
$encodedHeader = $base64UrlEncode(json_encode($jwtHeader));
$encodedClaim = $base64UrlEncode(json_encode($jwtClaim));
// 서명 생성
$privateKey = openssl_pkey_get_private($serviceAccount['private_key']);
if (!$privateKey) {
echo json_encode(['ok' => false, 'error' => '서비스 계정 개인 키를 읽을 수 없습니다.']);
exit;
}
$signature = '';
$signData = $encodedHeader . '.' . $encodedClaim;
if (!openssl_sign($signData, $signature, $privateKey, OPENSSL_ALGO_SHA256)) {
openssl_free_key($privateKey);
echo json_encode(['ok' => false, 'error' => 'JWT 서명 생성 실패']);
exit;
}
openssl_free_key($privateKey);
$encodedSignature = $base64UrlEncode($signature);
$jwt = $encodedHeader . '.' . $encodedClaim . '.' . $encodedSignature;
// OAuth 2.0 토큰 요청
$tokenCh = curl_init('https://oauth2.googleapis.com/token');
curl_setopt($tokenCh, CURLOPT_POST, true);
curl_setopt($tokenCh, CURLOPT_POSTFIELDS, http_build_query([
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion' => $jwt
]));
curl_setopt($tokenCh, CURLOPT_RETURNTRANSFER, true);
curl_setopt($tokenCh, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);
$tokenResponse = curl_exec($tokenCh);
$tokenCode = curl_getinfo($tokenCh, CURLINFO_HTTP_CODE);
$tokenError = curl_error($tokenCh);
curl_close($tokenCh);
if ($tokenCode === 200) {
$tokenData = json_decode($tokenResponse, true);
if (isset($tokenData['access_token'])) {
$accessToken = $tokenData['access_token'];
error_log('OAuth 토큰 생성 성공');
} else {
error_log('OAuth 토큰 응답 오류: ' . $tokenResponse);
echo json_encode(['ok' => false, 'error' => 'OAuth 토큰을 받을 수 없습니다.', 'details' => substr($tokenResponse, 0, 500)]);
exit;
}
} else {
error_log('OAuth 토큰 요청 실패 (HTTP ' . $tokenCode . '): ' . $tokenResponse);
$errorDetails = json_decode($tokenResponse, true);
$errorMessage = isset($errorDetails['error_description']) ? $errorDetails['error_description'] : (isset($errorDetails['error']) ? $errorDetails['error'] : substr($tokenResponse, 0, 500));
echo json_encode(['ok' => false, 'error' => 'OAuth 토큰 요청 실패 (HTTP ' . $tokenCode . ')', 'details' => $errorMessage]);
exit;
}
}
// OAuth 토큰이 없고 API 키 파일이 있으면 API 키 사용
if (!$accessToken && file_exists($googleApiKeyFile)) {
$googleApiKey = trim(file_get_contents($googleApiKeyFile));
if (!empty($googleApiKey)) {
// API 키 방식 사용 (기존 코드)
} else {
echo json_encode(['ok' => false, 'error' => 'Google API 키가 비어있습니다.']);
exit;
}
} elseif (!$accessToken) {
echo json_encode(['ok' => false, 'error' => 'Google 서비스 계정 JSON 파일 또는 API 키 파일이 필요합니다.']);
exit;
}
// 오디오 파일 크기 확인
$file_size = filesize($file_path);
$max_inline_size = 10 * 1024 * 1024; // 10MB (인라인 오디오 제한)
// 파일 확장자 확인
$file_extension = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
// 인코딩 자동 감지 (파일 확장자 기반)
$encoding = null;
$sample_rate = null;
switch ($file_extension) {
case 'webm':
$encoding = 'WEBM_OPUS';
$sample_rate = 48000;
break;
case 'wav':
$encoding = 'LINEAR16'; // WAV 파일은 LINEAR16
$sample_rate = 16000; // WAV는 보통 16kHz
break;
case 'mp3':
$encoding = 'MP3';
$sample_rate = null; // MP3는 자동 감지
break;
case 'ogg':
$encoding = 'OGG_OPUS';
$sample_rate = 48000;
break;
case 'm4a':
$encoding = 'MP4';
$sample_rate = null;
break;
default:
// 기본값: webm으로 가정
$encoding = 'WEBM_OPUS';
$sample_rate = 48000;
}
// API 요청 본문 구성
$config = [
'languageCode' => 'ko-KR', // 한국어
'enableAutomaticPunctuation' => true,
'model' => 'latest_long', // 긴 오디오용 모델
'audioChannelCount' => 1, // 모노 채널
'enableWordTimeOffsets' => false // 단어별 타임스탬프는 필요시에만
];
// 인코딩 설정 (일부 형식은 생략 가능)
if ($encoding) {
$config['encoding'] = $encoding;
}
// 샘플레이트 설정 (필요한 경우만)
if ($sample_rate) {
$config['sampleRateHertz'] = $sample_rate;
}
$requestBody = ['config' => $config];
// 오디오 소스 설정: 작은 파일은 인라인, 큰 파일은 GCS URI 사용
// 먼저 인라인으로 시도하고, 오류 발생 시 재시도 로직으로 처리
$audioContent = base64_encode(file_get_contents($file_path));
$requestBody['audio'] = ['content' => $audioContent];
// 모든 오디오 길이를 지원하기 위해 longrunningrecognize API 사용
// (짧은 오디오도 처리 가능하며, 1분 제한이 없음)
// longrunningrecognize는 비동기 작업이므로 작업을 시작하고 폴링해야 함
if ($accessToken) {
$apiUrl = 'https://speech.googleapis.com/v1/speech:longrunningrecognize';
$headers = [
'Content-Type: application/json',
'Authorization: Bearer ' . $accessToken
];
} else {
$apiUrl = 'https://speech.googleapis.com/v1/speech:longrunningrecognize?key=' . urlencode($googleApiKey);
$headers = ['Content-Type: application/json'];
}
$ch = curl_init($apiUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestBody));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$operation_response = curl_exec($ch);
$operation_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$operation_error = curl_error($ch);
curl_close($ch);
if ($operation_code !== 200) {
error_log('Google STT longrunningrecognize 시작 실패: ' . $operation_response);
// 오류 응답 파싱
$error_data = json_decode($operation_response, true);
$error_message = '';
$needs_gcs = false;
$needs_encoding_fix = false;
if (isset($error_data['error']['message'])) {
$error_message = $error_data['error']['message'];
// 인코딩 오류 감지
if (strpos($error_message, 'Encoding') !== false ||
strpos($error_message, 'LINEAR16') !== false ||
strpos($error_message, 'must either be omitted') !== false) {
$needs_encoding_fix = true;
}
// GCS URI 필요 오류인 경우 감지
if (strpos($error_message, 'GCS URI') !== false ||
strpos($error_message, 'duration limit') !== false ||
strpos($error_message, 'exceeds duration limit') !== false ||
strpos($error_message, 'Inline audio exceeds') !== false) {
$needs_gcs = true;
}
}
// 인코딩 오류인 경우, 인코딩을 생략하고 재시도
if ($needs_encoding_fix) {
// 인코딩과 샘플레이트를 제거하고 자동 감지하도록 설정
unset($requestBody['config']['encoding']);
unset($requestBody['config']['sampleRateHertz']);
// 재시도
$ch = curl_init($apiUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestBody));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$operation_response = curl_exec($ch);
$operation_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$operation_error = curl_error($ch);
curl_close($ch);
// 인코딩 수정 후 성공한 경우, 이후 로직으로 진행
if ($operation_code === 200) {
// 성공했으므로 이후 폴링 로직으로 진행
$needs_gcs = false;
} else {
error_log('Google STT 재시도 실패 (인코딩 수정): ' . $operation_response);
// 인코딩 수정 후에도 실패한 경우, 오류 데이터 다시 파싱
$error_data = json_decode($operation_response, true);
if (isset($error_data['error']['message'])) {
$error_message = $error_data['error']['message'];
// GCS URI 필요 오류인지 다시 확인
if (strpos($error_message, 'GCS URI') !== false ||
strpos($error_message, 'duration limit') !== false ||
strpos($error_message, 'exceeds duration limit') !== false ||
strpos($error_message, 'Inline audio exceeds') !== false) {
$needs_gcs = true;
}
}
}
}
// GCS URI가 필요한 경우, GCS에 업로드 후 URI 사용
if ($needs_gcs && $operation_code !== 200) {
// GCS 설정 확인
$gcs_config_file = $_SERVER['DOCUMENT_ROOT'] . '/apikey/gcs_config.txt';
$bucket_name = null;
if (file_exists($gcs_config_file)) {
$gcs_config = parse_ini_file($gcs_config_file);
$bucket_name = isset($gcs_config['bucket_name']) ? $gcs_config['bucket_name'] : null;
}
if ($bucket_name) {
// GCS에 파일 업로드
$gcs_object_name = 'consults/' . $tenant_id . '/' . basename($file_path);
$gcs_uri = uploadToGCS($file_path, $bucket_name, $gcs_object_name, $googleServiceAccountFile);
if ($gcs_uri) {
// GCS URI 사용
$requestBody['audio'] = ['uri' => $gcs_uri];
} else {
// GCS 업로드 실패
echo json_encode([
'ok' => false,
'error' => 'Google STT 변환 실패',
'details' => 'GCS 업로드에 실패했습니다. GCS 설정을 확인해주세요.',
'file_size_mb' => round($file_size / 1024 / 1024, 2)
]);
exit;
}
} else {
// GCS 설정이 없는 경우
echo json_encode([
'ok' => false,
'error' => 'Google STT 변환 실패',
'details' => '오디오 파일이 너무 깁니다. Google Cloud Storage 설정이 필요합니다. /apikey/gcs_config.txt 파일을 생성하고 bucket_name을 설정해주세요.',
'file_size_mb' => round($file_size / 1024 / 1024, 2),
'help' => '자세한 설정 방법은 voice_ai/구글클라우드스토리지설정방법.md 파일을 참고하세요.'
]);
exit;
}
// 재시도
$ch = curl_init($apiUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestBody));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$operation_response = curl_exec($ch);
$operation_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$operation_error = curl_error($ch);
curl_close($ch);
// 재시도도 실패한 경우
if ($operation_code !== 200) {
error_log('Google STT 재시도 실패 (GCS URI): ' . $operation_response);
echo json_encode([
'ok' => false,
'error' => 'Google STT 변환 실패 (HTTP ' . $operation_code . ')',
'details' => '오디오 파일이 너무 깁니다. Google Cloud Storage 설정이 필요할 수 있습니다.',
'curl_error' => $operation_error,
'file_size_mb' => round($file_size / 1024 / 1024, 2)
]);
exit;
}
// 재시도 성공 시 계속 진행
} else {
// 다른 오류인 경우
echo json_encode([
'ok' => false,
'error' => 'Google STT 변환 실패 (HTTP ' . $operation_code . ')',
'details' => $error_message ?: ($operation_response ? substr($operation_response, 0, 500) : '응답 없음'),
'curl_error' => $operation_error
]);
exit;
}
}
$operation_data = json_decode($operation_response, true);
if (!isset($operation_data['name'])) {
error_log('Google STT longrunningrecognize 작업 ID 없음: ' . $operation_response);
echo json_encode([
'ok' => false,
'error' => 'Google STT 작업 시작 실패',
'details' => substr($operation_response, 0, 500)
]);
exit;
}
$operation_name = $operation_data['name'];
// 작업 완료까지 폴링 (오디오 길이에 따라 동적으로 타임아웃 설정)
// 4시간 오디오는 처리에 약 1~2시간 소요될 수 있음
// 최대 4시간 대기 (2880회 * 5초 = 4시간)
// PHP 실행 시간 제한도 충분히 설정 필요 (ini_set('max_execution_time', 14400))
ini_set('max_execution_time', 14400); // 4시간
set_time_limit(14400); // 4시간
$max_polls = 2880; // 2880회 * 5초 = 4시간
$poll_count = 0;
$operation_done = false;
while ($poll_count < $max_polls && !$operation_done) {
sleep(5); // 5초 대기
$poll_count++;
// 작업 상태 확인
if ($accessToken) {
$poll_url = 'https://speech.googleapis.com/v1/operations/' . urlencode($operation_name);
$poll_headers = ['Authorization: Bearer ' . $accessToken];
} else {
$poll_url = 'https://speech.googleapis.com/v1/operations/' . urlencode($operation_name) . '?key=' . urlencode($googleApiKey);
$poll_headers = [];
}
$poll_ch = curl_init($poll_url);
curl_setopt($poll_ch, CURLOPT_RETURNTRANSFER, true);
if (!empty($poll_headers)) {
curl_setopt($poll_ch, CURLOPT_HTTPHEADER, $poll_headers);
}
$poll_response = curl_exec($poll_ch);
$poll_code = curl_getinfo($poll_ch, CURLINFO_HTTP_CODE);
curl_close($poll_ch);
if ($poll_code === 200) {
$poll_data = json_decode($poll_response, true);
if (isset($poll_data['done']) && $poll_data['done'] === true) {
if (isset($poll_data['error'])) {
error_log('Google STT longrunningrecognize 오류: ' . json_encode($poll_data['error']));
echo json_encode([
'ok' => false,
'error' => 'Google STT 변환 실패',
'details' => isset($poll_data['error']['message']) ? $poll_data['error']['message'] : '알 수 없는 오류'
]);
exit;
}
if (isset($poll_data['response']['results'])) {
$stt_data = ['results' => $poll_data['response']['results']];
$operation_done = true;
} else {
error_log('Google STT longrunningrecognize 결과 없음: ' . $poll_response);
echo json_encode([
'ok' => false,
'error' => 'Google STT 응답에 결과가 없습니다.',
'details' => substr($poll_response, 0, 500)
]);
exit;
}
}
} else {
error_log('Google STT longrunningrecognize 폴링 실패 (HTTP ' . $poll_code . '): ' . $poll_response);
}
}
if (!$operation_done) {
echo json_encode([
'ok' => false,
'error' => 'Google STT 변환이 시간 초과되었습니다. (4시간 이상 소요)',
'details' => '작업이 아직 완료되지 않았습니다. 매우 긴 오디오(4시간 이상)는 처리 시간이 오래 걸릴 수 있습니다. 작업 ID: ' . $operation_name,
'operation_name' => $operation_name,
'poll_count' => $poll_count,
'elapsed_time_minutes' => round($poll_count * 5 / 60, 1)
]);
exit;
}
// Google Speech-to-Text 응답 형식 처리
if (!isset($stt_data['results']) || empty($stt_data['results'])) {
// 오류 응답인 경우 처리
if (isset($stt_data['error'])) {
$errorMsg = isset($stt_data['error']['message']) ? $stt_data['error']['message'] : '알 수 없는 오류';
echo json_encode([
'ok' => false,
'error' => 'Google STT API 오류: ' . $errorMsg,
'response' => substr($stt_response, 0, 500)
]);
} else {
echo json_encode([
'ok' => false,
'error' => 'Google STT 응답에 결과가 없습니다. (오디오가 너무 짧거나 인식할 수 없는 형식일 수 있습니다)',
'response' => substr($stt_response, 0, 500)
]);
}
exit;
}
// 모든 결과를 합쳐서 텍스트 생성
$transcript = '';
foreach ($stt_data['results'] as $result) {
if (isset($result['alternatives'][0]['transcript'])) {
$transcript .= $result['alternatives'][0]['transcript'] . ' ';
}
}
$transcript = trim($transcript);
if (empty($transcript)) {
echo json_encode([
'ok' => false,
'error' => '인식된 텍스트가 없습니다. (오디오에 음성이 없거나 너무 작을 수 있습니다)',
'response' => substr($stt_response, 0, 500)
]);
exit;
}
// 4. 업무협의록 요약 및 제목 생성 (Claude API)
$claudeKeyFile = $_SERVER['DOCUMENT_ROOT'] . '/apikey/claude_api.txt';
if (!file_exists($claudeKeyFile)) {
// Claude API 키가 없어도 요약은 실패하지만 계속 진행
$claudeKey = '';
} else {
$claudeKey = trim(file_get_contents($claudeKeyFile));
}
// Claude API에 JSON 형식으로 응답 요청
$prompt = "다음 업무협의 녹취록을 분석하여 JSON 형식으로 응답해주세요.
요구사항:
1. \"title\": 업무협의 내용을 요약한 제목 (최대 20자, 한글 기준)
2. \"summary\": [업무협의 개요 / 주요 안건 / 결정 사항 / 향후 계획] 형식의 상세 요약
반드시 다음 JSON 형식으로만 응답해주세요:
{
\"title\": \"제목 (최대 20자)\",
\"summary\": \"상세 요약 내용\"
}
업무협의 녹취록:
" . $transcript;
$ch2 = curl_init('https://api.anthropic.com/v1/messages');
$requestBody = [
'model' => 'claude-3-5-haiku-20241022',
'max_tokens' => 2048,
'messages' => [['role' => 'user', 'content' => $prompt]]
];
curl_setopt($ch2, CURLOPT_POST, true);
curl_setopt($ch2, CURLOPT_HTTPHEADER, [
'x-api-key: ' . $claudeKey,
'anthropic-version: 2023-06-01',
'Content-Type: application/json'
]);
curl_setopt($ch2, CURLOPT_POSTFIELDS, json_encode($requestBody));
curl_setopt($ch2, CURLOPT_RETURNTRANSFER, true);
$ai_response = curl_exec($ch2);
$ai_code = curl_getinfo($ch2, CURLINFO_HTTP_CODE);
curl_close($ch2);
// 기본값 설정
$title = '무제 업무협의록';
$summary = '';
if ($ai_code !== 200 || empty($claudeKey)) {
if (empty($claudeKey)) {
$summary = "Claude API 키가 설정되지 않아 요약을 생성할 수 없습니다. (원문 저장됨)";
} else {
$summary = "요약 실패 (원문 저장됨)";
}
} else {
$ai_data = json_decode($ai_response, true);
if (isset($ai_data['content'][0]['text'])) {
$ai_text = $ai_data['content'][0]['text'];
// JSON 추출 시도 (코드 블록이나 마크다운 제거)
$ai_text = trim($ai_text);
// ```json 또는 ``` 제거
$ai_text = preg_replace('/^```(?:json)?\s*/m', '', $ai_text);
$ai_text = preg_replace('/\s*```$/m', '', $ai_text);
$ai_text = trim($ai_text);
// JSON 파싱 시도
$parsed_data = json_decode($ai_text, true);
if (is_array($parsed_data)) {
// JSON 파싱 성공
if (isset($parsed_data['title']) && !empty($parsed_data['title'])) {
$title = mb_substr(trim($parsed_data['title']), 0, 20, 'UTF-8');
}
if (isset($parsed_data['summary']) && !empty($parsed_data['summary'])) {
$summary = $parsed_data['summary'];
} else {
$summary = $ai_text; // JSON 형식이 아니면 전체 텍스트 사용
}
} else {
// JSON 파싱 실패 - 텍스트에서 제목 추출 시도
$summary = $ai_text;
// 텍스트에서 "title" 또는 "제목" 키워드로 제목 추출 시도
if (preg_match('/["\']?title["\']?\s*[:=]\s*["\']([^"\']{1,20})["\']/', $ai_text, $matches)) {
$title = mb_substr(trim($matches[1]), 0, 20, 'UTF-8');
} elseif (preg_match('/제목[:\s]+([^\n]{1,20})/', $ai_text, $matches)) {
$title = mb_substr(trim($matches[1]), 0, 20, 'UTF-8');
} else {
// 요약 텍스트의 첫 줄이나 첫 문장을 제목으로 사용
$lines = explode("\n", $ai_text);
$first_line = trim($lines[0]);
if (!empty($first_line) && mb_strlen($first_line, 'UTF-8') <= 20) {
$title = $first_line;
} elseif (!empty($first_line)) {
$title = mb_substr($first_line, 0, 20, 'UTF-8');
}
}
}
// 제목이 비어있으면 기본값 사용
if (empty($title) || $title === '무제 업무협의록') {
// 요약에서 의미있는 키워드 추출 시도
if (!empty($summary)) {
// 요약의 첫 문장이나 주요 키워드를 제목으로 사용
$summary_lines = explode("\n", $summary);
$first_summary_line = trim($summary_lines[0]);
if (!empty($first_summary_line)) {
// 괄호나 특수문자 제거
$first_summary_line = preg_replace('/[\[\(].*?[\]\)]/', '', $first_summary_line);
$first_summary_line = trim($first_summary_line);
if (mb_strlen($first_summary_line, 'UTF-8') <= 20) {
$title = $first_summary_line;
} else {
$title = mb_substr($first_summary_line, 0, 20, 'UTF-8');
}
}
}
}
} else {
$summary = "요약 응답 형식 오류 (원문 저장됨)";
}
}
// 제목 최종 검증 (20자 제한, 빈 값 방지)
$title = mb_substr(trim($title), 0, 20, 'UTF-8');
if (empty($title)) {
$title = '무제 업무협의록';
}
// 5. DB 저장 및 1주일 보관 설정
try {
$pdo = db_connect();
if (!$pdo) {
throw new Exception('데이터베이스 연결 실패');
}
$expiry_date = date('Y-m-d H:i:s', strtotime('+7 days'));
$web_path = "/uploads/consults/" . $tenant_id . "/" . $file_name;
// SQL 인젝션 방지는 PDO Prepared Statement 사용 권장
$sql = "INSERT INTO consult_logs (tenant_id, user_id, title, audio_file_path, transcript_text, summary_text, file_expiry_date)
VALUES (?, ?, ?, ?, ?, ?, ?)";
$stmt = $pdo->prepare($sql);
if (!$stmt) {
$errorInfo = $pdo->errorInfo();
throw new Exception('SQL 준비 실패: ' . ($errorInfo[2] ?? '알 수 없는 오류'));
}
$executeResult = $stmt->execute([$tenant_id, $tenant_id, $title, $web_path, $transcript, $summary, $expiry_date]);
if (!$executeResult) {
$errorInfo = $stmt->errorInfo();
throw new Exception('SQL 실행 실패: ' . ($errorInfo[2] ?? '알 수 없는 오류'));
}
$insertId = $pdo->lastInsertId();
if (!$insertId) {
throw new Exception('INSERT ID를 가져올 수 없습니다. 테이블이 존재하지 않거나 AUTO_INCREMENT가 설정되지 않았을 수 있습니다.');
}
// 성공 로그
error_log('업무협의록 저장 성공 - ID: ' . $insertId . ', 제목: ' . $title);
echo json_encode([
'ok' => true,
'id' => $insertId,
'title' => $title,
'transcript' => $transcript,
'summary' => $summary,
'message' => '업무협의록이 성공적으로 저장되었습니다.',
'db_info' => [
'insert_id' => $insertId,
'file_path' => $web_path,
'expiry_date' => $expiry_date
]
]);
} catch (PDOException $e) {
error_log('DB 저장 오류 (PDOException): ' . $e->getMessage());
echo json_encode([
'ok' => false,
'error' => '데이터베이스 저장 실패',
'details' => $e->getMessage(),
'error_code' => $e->getCode(),
'help' => 'consult_logs 테이블이 존재하는지 확인하세요. db_schema.sql 파일을 실행하여 테이블을 생성하세요.'
]);
} catch (Exception $e) {
error_log('DB 저장 오류 (Exception): ' . $e->getMessage());
echo json_encode([
'ok' => false,
'error' => '데이터베이스 저장 실패',
'details' => $e->getMessage(),
'help' => 'consult_logs 테이블이 존재하는지 확인하세요. db_schema.sql 파일을 실행하여 테이블을 생성하세요.'
]);
}
?>