876 lines
24 KiB
Markdown
876 lines
24 KiB
Markdown
|
|
# GCP Storage 파일 저장 로직 개발 문서
|
||
|
|
|
||
|
|
## 개요
|
||
|
|
|
||
|
|
이 문서는 `voice_ai_cnslt` 모듈에서 Google Cloud Storage (GCS)를 사용하여 오디오 파일을 저장하고 관리하는 로직에 대한 개발 가이드입니다.
|
||
|
|
|
||
|
|
## 목차
|
||
|
|
|
||
|
|
1. [시스템 아키텍처](#시스템-아키텍처)
|
||
|
|
2. [필수 설정](#필수-설정)
|
||
|
|
3. [GCS 업로드 로직](#gcs-업로드-로직)
|
||
|
|
4. [GCS 삭제 로직](#gcs-삭제-로직)
|
||
|
|
5. [인증 메커니즘](#인증-메커니즘)
|
||
|
|
6. [사용 시나리오](#사용-시나리오)
|
||
|
|
7. [에러 처리](#에러-처리)
|
||
|
|
8. [코드 예제](#코드-예제)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 시스템 아키텍처
|
||
|
|
|
||
|
|
### 파일 저장 흐름
|
||
|
|
|
||
|
|
```
|
||
|
|
[클라이언트]
|
||
|
|
↓ (오디오 파일 업로드)
|
||
|
|
[process_consult.php]
|
||
|
|
↓ (로컬 서버에 임시 저장)
|
||
|
|
[/uploads/consults/{tenant_id}/]
|
||
|
|
↓ (Google STT API 요청 시)
|
||
|
|
[GCS 업로드 필요 여부 확인]
|
||
|
|
↓ (필요한 경우)
|
||
|
|
[uploadToGCS() 함수 호출]
|
||
|
|
↓
|
||
|
|
[Google Cloud Storage]
|
||
|
|
↓ (GCS URI 반환)
|
||
|
|
[Google Speech-to-Text API]
|
||
|
|
```
|
||
|
|
|
||
|
|
### 주요 파일 구조
|
||
|
|
|
||
|
|
- **`voice_ai_cnslt/index.php`**: 프론트엔드 UI 및 녹음 기능
|
||
|
|
- **`voice_ai_cnslt/process_consult.php`**: 오디오 처리 및 GCS 업로드 로직
|
||
|
|
- **`voice_ai_cnslt/delete_consult.php`**: 파일 삭제 및 GCS 삭제 로직
|
||
|
|
- **`apikey/gcs_config.txt`**: GCS 버킷 설정 파일
|
||
|
|
- **`apikey/google_service_account.json`**: Google 서비스 계정 인증 정보
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 필수 설정
|
||
|
|
|
||
|
|
### 1. GCS 버킷 설정 파일
|
||
|
|
|
||
|
|
**파일 경로**: `/apikey/gcs_config.txt`
|
||
|
|
|
||
|
|
**형식**:
|
||
|
|
```ini
|
||
|
|
bucket_name=your-bucket-name
|
||
|
|
```
|
||
|
|
|
||
|
|
**예시**:
|
||
|
|
```ini
|
||
|
|
bucket_name=codebridge-speech-audio-files
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Google 서비스 계정 JSON 파일
|
||
|
|
|
||
|
|
**파일 경로**: `/apikey/google_service_account.json`
|
||
|
|
|
||
|
|
**필수 필드**:
|
||
|
|
- `client_email`: 서비스 계정 이메일
|
||
|
|
- `private_key`: RSA 개인 키 (PEM 형식)
|
||
|
|
- `project_id`: GCP 프로젝트 ID
|
||
|
|
|
||
|
|
**예시 구조**:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"type": "service_account",
|
||
|
|
"project_id": "your-project-id",
|
||
|
|
"private_key_id": "...",
|
||
|
|
"private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
|
||
|
|
"client_email": "your-service-account@your-project.iam.gserviceaccount.com",
|
||
|
|
...
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. 서비스 계정 권한
|
||
|
|
|
||
|
|
GCS 버킷에 대한 다음 권한이 필요합니다:
|
||
|
|
- `Storage Object Admin` 또는
|
||
|
|
- `Storage Admin`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## GCS 업로드 로직
|
||
|
|
|
||
|
|
### 함수 시그니처
|
||
|
|
|
||
|
|
```php
|
||
|
|
function uploadToGCS($file_path, $bucket_name, $object_name, $service_account_path)
|
||
|
|
```
|
||
|
|
|
||
|
|
### 매개변수
|
||
|
|
|
||
|
|
| 매개변수 | 타입 | 설명 |
|
||
|
|
|---------|------|------|
|
||
|
|
| `$file_path` | string | 업로드할 로컬 파일 경로 |
|
||
|
|
| `$bucket_name` | string | GCS 버킷 이름 |
|
||
|
|
| `$object_name` | string | GCS에 저장될 객체 이름 (경로 포함) |
|
||
|
|
| `$service_account_path` | string | 서비스 계정 JSON 파일 경로 |
|
||
|
|
|
||
|
|
### 반환값
|
||
|
|
|
||
|
|
- **성공**: `gs://{bucket_name}/{object_name}` 형식의 GCS URI 문자열
|
||
|
|
- **실패**: `false`
|
||
|
|
|
||
|
|
### 업로드 프로세스
|
||
|
|
|
||
|
|
1. **서비스 계정 파일 검증**
|
||
|
|
```php
|
||
|
|
if (!file_exists($service_account_path)) {
|
||
|
|
error_log('GCS 업로드 실패: 서비스 계정 파일 없음');
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
2. **OAuth 2.0 토큰 생성**
|
||
|
|
- JWT (JSON Web Token) 생성
|
||
|
|
- 서비스 계정 개인 키로 서명
|
||
|
|
- Google OAuth 2.0 엔드포인트에 토큰 요청
|
||
|
|
|
||
|
|
3. **파일 업로드**
|
||
|
|
- 파일 내용 읽기
|
||
|
|
- MIME 타입 자동 감지
|
||
|
|
- GCS REST API를 통한 업로드
|
||
|
|
|
||
|
|
### 사용 예시
|
||
|
|
|
||
|
|
```php
|
||
|
|
// process_consult.php에서 사용
|
||
|
|
$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를 Google STT API에 전달
|
||
|
|
$requestBody['audio'] = ['uri' => $gcs_uri];
|
||
|
|
} else {
|
||
|
|
// 업로드 실패 처리
|
||
|
|
echo json_encode(['ok' => false, 'error' => 'GCS 업로드 실패']);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 업로드 트리거 조건
|
||
|
|
|
||
|
|
GCS 업로드는 다음 조건에서 자동으로 트리거됩니다:
|
||
|
|
|
||
|
|
1. **Google STT API 오류 발생 시**
|
||
|
|
- 오류 메시지에 "GCS URI" 또는 "duration limit" 키워드 포함
|
||
|
|
- 오류 메시지에 "Inline audio exceeds" 포함
|
||
|
|
|
||
|
|
2. **파일 크기 제한**
|
||
|
|
- 인라인 오디오 제한 (약 10MB) 초과 시
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## GCS 삭제 로직
|
||
|
|
|
||
|
|
### 함수 시그니처
|
||
|
|
|
||
|
|
```php
|
||
|
|
function deleteFromGCS($bucket_name, $object_name, $service_account_path = null)
|
||
|
|
```
|
||
|
|
|
||
|
|
### 매개변수
|
||
|
|
|
||
|
|
| 매개변수 | 타입 | 설명 |
|
||
|
|
|---------|------|------|
|
||
|
|
| `$bucket_name` | string | GCS 버킷 이름 |
|
||
|
|
| `$object_name` | string | 삭제할 객체 이름 (경로 포함) |
|
||
|
|
| `$service_account_path` | string | 서비스 계정 JSON 파일 경로 (기본값: `/apikey/google_service_account.json`) |
|
||
|
|
|
||
|
|
### 반환값
|
||
|
|
|
||
|
|
- **성공**: `true`
|
||
|
|
- **실패**: `false`
|
||
|
|
|
||
|
|
### 삭제 프로세스
|
||
|
|
|
||
|
|
1. **GCS URI 파싱**
|
||
|
|
```php
|
||
|
|
// DB에 저장된 경로가 gs:// 형식인지 확인
|
||
|
|
if (strpos($consult['audio_file_path'], 'gs://') === 0) {
|
||
|
|
if (preg_match('#gs://([^/]+)/(.+)#', $gcs_uri, $matches)) {
|
||
|
|
$bucket_name = $matches[1];
|
||
|
|
$object_name = $matches[2];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
2. **OAuth 2.0 토큰 생성** (업로드와 동일)
|
||
|
|
|
||
|
|
3. **GCS REST API를 통한 삭제**
|
||
|
|
- DELETE 메서드 사용
|
||
|
|
- HTTP 204 (No Content) 또는 404 (Not Found) 응답 시 성공으로 간주
|
||
|
|
|
||
|
|
### 사용 예시
|
||
|
|
|
||
|
|
```php
|
||
|
|
// delete_consult.php에서 사용
|
||
|
|
if (strpos($consult['audio_file_path'], 'gs://') === 0) {
|
||
|
|
$gcs_uri = $consult['audio_file_path'];
|
||
|
|
if (preg_match('#gs://([^/]+)/(.+)#', $gcs_uri, $matches)) {
|
||
|
|
$bucket_name = $matches[1];
|
||
|
|
$object_name = $matches[2];
|
||
|
|
|
||
|
|
$gcs_deleted = deleteFromGCS($bucket_name, $object_name);
|
||
|
|
if (!$gcs_deleted) {
|
||
|
|
error_log('GCS 파일 삭제 실패: ' . $gcs_uri);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 인증 메커니즘
|
||
|
|
|
||
|
|
### OAuth 2.0 JWT Bearer 토큰
|
||
|
|
|
||
|
|
GCS API 접근을 위해 OAuth 2.0 JWT Bearer 토큰 방식을 사용합니다.
|
||
|
|
|
||
|
|
### JWT 생성 단계
|
||
|
|
|
||
|
|
1. **JWT 헤더 생성**
|
||
|
|
```php
|
||
|
|
$jwtHeader = base64_encode(json_encode([
|
||
|
|
'alg' => 'RS256',
|
||
|
|
'typ' => 'JWT'
|
||
|
|
]));
|
||
|
|
```
|
||
|
|
|
||
|
|
2. **JWT 클레임 생성**
|
||
|
|
```php
|
||
|
|
$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, // 1시간 유효
|
||
|
|
'iat' => $now
|
||
|
|
]));
|
||
|
|
```
|
||
|
|
|
||
|
|
3. **서명 생성**
|
||
|
|
```php
|
||
|
|
$privateKey = openssl_pkey_get_private($serviceAccount['private_key']);
|
||
|
|
openssl_sign($jwtHeader . '.' . $jwtClaim, $signature, $privateKey, OPENSSL_ALGO_SHA256);
|
||
|
|
$jwt = $jwtHeader . '.' . $jwtClaim . '.' . base64_encode($signature);
|
||
|
|
```
|
||
|
|
|
||
|
|
4. **액세스 토큰 요청**
|
||
|
|
```php
|
||
|
|
curl_setopt($tokenCh, CURLOPT_POSTFIELDS, http_build_query([
|
||
|
|
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
||
|
|
'assertion' => $jwt
|
||
|
|
]));
|
||
|
|
```
|
||
|
|
|
||
|
|
### 필요한 스코프
|
||
|
|
|
||
|
|
- **업로드/삭제**: `https://www.googleapis.com/auth/devstorage.full_control`
|
||
|
|
- **읽기 전용**: `https://www.googleapis.com/auth/devstorage.read_only`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 사용 시나리오
|
||
|
|
|
||
|
|
### 시나리오 1: 일반적인 오디오 업로드
|
||
|
|
|
||
|
|
1. 사용자가 오디오 파일을 녹음
|
||
|
|
2. `process_consult.php`로 파일 업로드
|
||
|
|
3. 로컬 서버에 임시 저장
|
||
|
|
4. Google STT API에 인라인 오디오로 요청
|
||
|
|
5. **성공**: 텍스트 변환 완료
|
||
|
|
6. **실패 (GCS 필요)**: GCS에 업로드 후 재시도
|
||
|
|
|
||
|
|
### 시나리오 2: 대용량 오디오 파일
|
||
|
|
|
||
|
|
1. 오디오 파일이 10MB 이상이거나 긴 녹음
|
||
|
|
2. Google STT API가 "GCS URI required" 오류 반환
|
||
|
|
3. `uploadToGCS()` 함수 자동 호출
|
||
|
|
4. GCS에 업로드 후 URI 획득
|
||
|
|
5. GCS URI로 STT API 재요청
|
||
|
|
|
||
|
|
### 시나리오 3: 파일 삭제
|
||
|
|
|
||
|
|
1. 사용자가 업무협의록 삭제 요청
|
||
|
|
2. `delete_consult.php` 실행
|
||
|
|
3. DB에서 `audio_file_path` 조회
|
||
|
|
4. 경로가 `gs://`로 시작하면 GCS 파일 삭제
|
||
|
|
5. 로컬 서버 파일 삭제
|
||
|
|
6. DB 레코드 삭제
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 에러 처리
|
||
|
|
|
||
|
|
### 일반적인 오류 및 해결 방법
|
||
|
|
|
||
|
|
#### 1. 서비스 계정 파일 없음
|
||
|
|
```
|
||
|
|
오류: GCS 업로드 실패: 서비스 계정 파일 없음
|
||
|
|
해결: /apikey/google_service_account.json 파일이 존재하는지 확인
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2. OAuth 토큰 요청 실패
|
||
|
|
```
|
||
|
|
오류: GCS 업로드 실패: OAuth 토큰 요청 실패 (HTTP 401)
|
||
|
|
해결:
|
||
|
|
- 서비스 계정 JSON 파일 형식 확인
|
||
|
|
- 개인 키가 올바른지 확인
|
||
|
|
- 서비스 계정이 활성화되어 있는지 확인
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 3. 버킷 접근 권한 없음
|
||
|
|
```
|
||
|
|
오류: GCS 업로드 실패 (HTTP 403)
|
||
|
|
해결:
|
||
|
|
- 서비스 계정에 Storage Object Admin 권한 부여
|
||
|
|
- 버킷 이름이 올바른지 확인
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 4. 버킷이 존재하지 않음
|
||
|
|
```
|
||
|
|
오류: GCS 업로드 실패 (HTTP 404)
|
||
|
|
해결:
|
||
|
|
- GCS 버킷이 생성되어 있는지 확인
|
||
|
|
- 버킷 이름이 올바른지 확인
|
||
|
|
```
|
||
|
|
|
||
|
|
### 로깅
|
||
|
|
|
||
|
|
모든 GCS 관련 오류는 `error_log()` 함수를 통해 기록됩니다:
|
||
|
|
|
||
|
|
```php
|
||
|
|
error_log('GCS 업로드 실패: 서비스 계정 파일 없음');
|
||
|
|
error_log('GCS 업로드 실패 (HTTP ' . $code . '): ' . $response);
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 코드 예제
|
||
|
|
|
||
|
|
### 전체 업로드 플로우 예제
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
// 1. 설정 파일 읽기
|
||
|
|
$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;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2. GCS 업로드 필요 여부 확인
|
||
|
|
if ($bucket_name && $needs_gcs) {
|
||
|
|
// 3. GCS 객체 이름 생성
|
||
|
|
$gcs_object_name = 'consults/' . $tenant_id . '/' . basename($file_path);
|
||
|
|
|
||
|
|
// 4. GCS 업로드 실행
|
||
|
|
$gcs_uri = uploadToGCS(
|
||
|
|
$file_path,
|
||
|
|
$bucket_name,
|
||
|
|
$gcs_object_name,
|
||
|
|
$googleServiceAccountFile
|
||
|
|
);
|
||
|
|
|
||
|
|
// 5. 업로드 결과 확인
|
||
|
|
if ($gcs_uri) {
|
||
|
|
// 성공: GCS URI 사용
|
||
|
|
$requestBody['audio'] = ['uri' => $gcs_uri];
|
||
|
|
} else {
|
||
|
|
// 실패: 오류 응답
|
||
|
|
echo json_encode([
|
||
|
|
'ok' => false,
|
||
|
|
'error' => 'GCS 업로드 실패',
|
||
|
|
'details' => 'GCS 설정을 확인해주세요.'
|
||
|
|
]);
|
||
|
|
exit;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
?>
|
||
|
|
```
|
||
|
|
|
||
|
|
### 전체 삭제 플로우 예제
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
// 1. DB에서 파일 경로 조회
|
||
|
|
$sql = "SELECT audio_file_path FROM consult_logs WHERE id = ?";
|
||
|
|
$stmt = $pdo->prepare($sql);
|
||
|
|
$stmt->execute([$consult_id]);
|
||
|
|
$consult = $stmt->fetch(PDO::FETCH_ASSOC);
|
||
|
|
|
||
|
|
// 2. 로컬 파일 삭제
|
||
|
|
if (!empty($consult['audio_file_path'])) {
|
||
|
|
$file_path = $_SERVER['DOCUMENT_ROOT'] . $consult['audio_file_path'];
|
||
|
|
if (file_exists($file_path)) {
|
||
|
|
@unlink($file_path);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. GCS 파일 삭제 (GCS URI인 경우)
|
||
|
|
if (strpos($consult['audio_file_path'], 'gs://') === 0) {
|
||
|
|
$gcs_uri = $consult['audio_file_path'];
|
||
|
|
if (preg_match('#gs://([^/]+)/(.+)#', $gcs_uri, $matches)) {
|
||
|
|
$bucket_name = $matches[1];
|
||
|
|
$object_name = $matches[2];
|
||
|
|
|
||
|
|
$gcs_deleted = deleteFromGCS($bucket_name, $object_name);
|
||
|
|
if (!$gcs_deleted) {
|
||
|
|
error_log('GCS 파일 삭제 실패: ' . $gcs_uri);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 4. DB 레코드 삭제
|
||
|
|
$delete_sql = "DELETE FROM consult_logs WHERE id = ?";
|
||
|
|
$delete_stmt = $pdo->prepare($delete_sql);
|
||
|
|
$delete_stmt->execute([$consult_id]);
|
||
|
|
?>
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## GCS 객체 이름 규칙
|
||
|
|
|
||
|
|
### 경로 구조
|
||
|
|
|
||
|
|
```
|
||
|
|
consults/{tenant_id}/{filename}
|
||
|
|
```
|
||
|
|
|
||
|
|
**예시**:
|
||
|
|
```
|
||
|
|
consults/user123/20241201_143022_abc123def456.webm
|
||
|
|
```
|
||
|
|
|
||
|
|
### 파일명 형식
|
||
|
|
|
||
|
|
```php
|
||
|
|
$file_name = date('Ymd_His') . "_" . uniqid() . ".webm";
|
||
|
|
```
|
||
|
|
|
||
|
|
- `Ymd_His`: 년월일_시분초 (예: 20241201_143022)
|
||
|
|
- `uniqid()`: 고유 ID
|
||
|
|
- 확장자: `.webm` (기본값)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 실전 적용 예제: 매니저 상담 음성 저장 로직
|
||
|
|
|
||
|
|
이 섹션에서는 문서의 내용을 기반으로 **매니저들의 상담 음성을 저장하는 로직**을 개발하는 방법을 설명합니다.
|
||
|
|
|
||
|
|
### 개발 전략
|
||
|
|
|
||
|
|
기존 `voice_ai_cnslt` 모듈의 로직을 재사용하되, 다음 항목만 변경하면 됩니다:
|
||
|
|
|
||
|
|
1. **업로드 디렉토리 경로**
|
||
|
|
2. **GCS 객체 경로 구조**
|
||
|
|
3. **DB 테이블명 및 필드**
|
||
|
|
4. **파일명 접두사 (선택사항)**
|
||
|
|
|
||
|
|
### 1. 파일 구조 설계
|
||
|
|
|
||
|
|
#### 업로드 디렉토리
|
||
|
|
```php
|
||
|
|
// 기존: /uploads/consults/{tenant_id}/
|
||
|
|
// 매니저 상담: /uploads/manager_consultations/{manager_id}/
|
||
|
|
$upload_dir = $_SERVER['DOCUMENT_ROOT'] . "/uploads/manager_consultations/" . $manager_id . "/";
|
||
|
|
```
|
||
|
|
|
||
|
|
#### GCS 객체 경로
|
||
|
|
```php
|
||
|
|
// 기존: consults/{tenant_id}/{filename}
|
||
|
|
// 매니저 상담: manager_consultations/{manager_id}/{filename}
|
||
|
|
$gcs_object_name = 'manager_consultations/' . $manager_id . '/' . basename($file_path);
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. 완전한 구현 예제
|
||
|
|
|
||
|
|
#### `process_manager_consultation.php` 파일
|
||
|
|
|
||
|
|
```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) {
|
||
|
|
// ... (기존 uploadToGCS 함수 코드 그대로 사용)
|
||
|
|
// 문서의 "GCS 업로드 로직" 섹션 참고
|
||
|
|
}
|
||
|
|
|
||
|
|
// 출력 버퍼 비우기
|
||
|
|
ob_clean();
|
||
|
|
header('Content-Type: application/json; charset=utf-8');
|
||
|
|
|
||
|
|
// 1. 권한 및 세션 체크
|
||
|
|
if (!isset($user_id) || $level > 5) {
|
||
|
|
echo json_encode(['ok' => false, 'error' => '접근 권한이 없습니다.']);
|
||
|
|
exit;
|
||
|
|
}
|
||
|
|
|
||
|
|
$manager_id = $user_id; // 또는 별도의 매니저 ID
|
||
|
|
$upload_dir = $_SERVER['DOCUMENT_ROOT'] . "/uploads/manager_consultations/" . $manager_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. Google STT 변환 (기존 로직과 동일)
|
||
|
|
$googleServiceAccountFile = $_SERVER['DOCUMENT_ROOT'] . '/apikey/google_service_account.json';
|
||
|
|
$accessToken = null;
|
||
|
|
|
||
|
|
// ... (OAuth 토큰 생성 로직 - 기존과 동일)
|
||
|
|
// ... (STT API 호출 로직 - 기존과 동일)
|
||
|
|
|
||
|
|
// 4. GCS 업로드 (필요한 경우)
|
||
|
|
$file_size = filesize($file_path);
|
||
|
|
$max_inline_size = 10 * 1024 * 1024; // 10MB
|
||
|
|
|
||
|
|
// STT API 오류 발생 시 GCS 업로드
|
||
|
|
if ($needs_gcs && $operation_code !== 200) {
|
||
|
|
$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 = 'manager_consultations/' . $manager_id . '/' . basename($file_path);
|
||
|
|
$gcs_uri = uploadToGCS($file_path, $bucket_name, $gcs_object_name, $googleServiceAccountFile);
|
||
|
|
|
||
|
|
if ($gcs_uri) {
|
||
|
|
$requestBody['audio'] = ['uri' => $gcs_uri];
|
||
|
|
// STT API 재시도...
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 5. STT 결과 처리 (기존과 동일)
|
||
|
|
// ... (폴링 로직, 텍스트 추출 등)
|
||
|
|
|
||
|
|
// 6. DB 저장 (매니저 상담 테이블)
|
||
|
|
try {
|
||
|
|
$pdo = db_connect();
|
||
|
|
if (!$pdo) {
|
||
|
|
throw new Exception('데이터베이스 연결 실패');
|
||
|
|
}
|
||
|
|
|
||
|
|
$expiry_date = date('Y-m-d H:i:s', strtotime('+7 days'));
|
||
|
|
|
||
|
|
// ⭐ 로컬 경로 또는 GCS URI 저장
|
||
|
|
$web_path = "/uploads/manager_consultations/" . $manager_id . "/" . $file_name;
|
||
|
|
// 또는 GCS URI가 있으면: $web_path = $gcs_uri;
|
||
|
|
|
||
|
|
// ⭐ 매니저 상담 테이블에 저장
|
||
|
|
$sql = "INSERT INTO manager_consultations
|
||
|
|
(manager_id, title, audio_file_path, transcript_text, summary_text, file_expiry_date)
|
||
|
|
VALUES (?, ?, ?, ?, ?, ?)";
|
||
|
|
$stmt = $pdo->prepare($sql);
|
||
|
|
|
||
|
|
$executeResult = $stmt->execute([
|
||
|
|
$manager_id,
|
||
|
|
$title,
|
||
|
|
$web_path, // 또는 $gcs_uri
|
||
|
|
$transcript,
|
||
|
|
$summary,
|
||
|
|
$expiry_date
|
||
|
|
]);
|
||
|
|
|
||
|
|
$insertId = $pdo->lastInsertId();
|
||
|
|
|
||
|
|
echo json_encode([
|
||
|
|
'ok' => true,
|
||
|
|
'id' => $insertId,
|
||
|
|
'title' => $title,
|
||
|
|
'message' => '매니저 상담 음성이 성공적으로 저장되었습니다.'
|
||
|
|
]);
|
||
|
|
} catch (Exception $e) {
|
||
|
|
error_log('DB 저장 오류: ' . $e->getMessage());
|
||
|
|
echo json_encode(['ok' => false, 'error' => '데이터베이스 저장 실패']);
|
||
|
|
}
|
||
|
|
?>
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `delete_manager_consultation.php` 파일
|
||
|
|
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
ob_start();
|
||
|
|
error_reporting(0);
|
||
|
|
|
||
|
|
require_once($_SERVER['DOCUMENT_ROOT'] . "/session.php");
|
||
|
|
require_once($_SERVER['DOCUMENT_ROOT'] . "/lib/mydb.php");
|
||
|
|
|
||
|
|
// ===== GCS 삭제 함수 (기존과 동일) =====
|
||
|
|
function deleteFromGCS($bucket_name, $object_name, $service_account_path = null) {
|
||
|
|
// ... (기존 deleteFromGCS 함수 코드 그대로 사용)
|
||
|
|
// 문서의 "GCS 삭제 로직" 섹션 참고
|
||
|
|
}
|
||
|
|
|
||
|
|
ob_clean();
|
||
|
|
header('Content-Type: application/json; charset=utf-8');
|
||
|
|
|
||
|
|
// 권한 체크
|
||
|
|
if (!isset($user_id) || $level > 5) {
|
||
|
|
echo json_encode(['ok' => false, 'error' => '접근 권한이 없습니다.']);
|
||
|
|
exit;
|
||
|
|
}
|
||
|
|
|
||
|
|
$consultation_id = isset($_POST['id']) ? intval($_POST['id']) : 0;
|
||
|
|
$manager_id = $user_id;
|
||
|
|
|
||
|
|
if ($consultation_id <= 0) {
|
||
|
|
echo json_encode(['ok' => false, 'error' => '잘못된 요청입니다.']);
|
||
|
|
exit;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
$pdo = db_connect();
|
||
|
|
|
||
|
|
// ⭐ 매니저 상담 테이블에서 조회
|
||
|
|
$sql = "SELECT audio_file_path FROM manager_consultations WHERE id = ?";
|
||
|
|
$stmt = $pdo->prepare($sql);
|
||
|
|
$stmt->execute([$consultation_id]);
|
||
|
|
$consultation = $stmt->fetch(PDO::FETCH_ASSOC);
|
||
|
|
|
||
|
|
if (!$consultation) {
|
||
|
|
echo json_encode(['ok' => false, 'error' => '상담 기록을 찾을 수 없습니다.']);
|
||
|
|
exit;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 1. 로컬 파일 삭제
|
||
|
|
if (!empty($consultation['audio_file_path'])) {
|
||
|
|
$file_path = $_SERVER['DOCUMENT_ROOT'] . $consultation['audio_file_path'];
|
||
|
|
if (file_exists($file_path)) {
|
||
|
|
@unlink($file_path);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2. GCS 파일 삭제 (GCS URI인 경우)
|
||
|
|
if (strpos($consultation['audio_file_path'], 'gs://') === 0) {
|
||
|
|
$gcs_uri = $consultation['audio_file_path'];
|
||
|
|
if (preg_match('#gs://([^/]+)/(.+)#', $gcs_uri, $matches)) {
|
||
|
|
$bucket_name = $matches[1];
|
||
|
|
$object_name = $matches[2];
|
||
|
|
|
||
|
|
$gcs_deleted = deleteFromGCS($bucket_name, $object_name);
|
||
|
|
if (!$gcs_deleted) {
|
||
|
|
error_log('GCS 파일 삭제 실패: ' . $gcs_uri);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. DB에서 삭제
|
||
|
|
$delete_sql = "DELETE FROM manager_consultations WHERE id = ?";
|
||
|
|
$delete_stmt = $pdo->prepare($delete_sql);
|
||
|
|
$delete_stmt->execute([$consultation_id]);
|
||
|
|
|
||
|
|
echo json_encode(['ok' => true, 'message' => '상담 기록이 삭제되었습니다.']);
|
||
|
|
|
||
|
|
} catch (Exception $e) {
|
||
|
|
error_log('삭제 오류: ' . $e->getMessage());
|
||
|
|
echo json_encode(['ok' => false, 'error' => '삭제 중 오류가 발생했습니다.']);
|
||
|
|
}
|
||
|
|
?>
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. DB 스키마 예제
|
||
|
|
|
||
|
|
```sql
|
||
|
|
-- 매니저 상담 테이블 생성
|
||
|
|
CREATE TABLE IF NOT EXISTS manager_consultations (
|
||
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
|
|
manager_id VARCHAR(50) NOT NULL,
|
||
|
|
title VARCHAR(100) NOT NULL,
|
||
|
|
audio_file_path TEXT,
|
||
|
|
transcript_text TEXT,
|
||
|
|
summary_text TEXT,
|
||
|
|
file_expiry_date DATETIME,
|
||
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
|
|
INDEX idx_manager_id (manager_id),
|
||
|
|
INDEX idx_created_at (created_at),
|
||
|
|
INDEX idx_expiry_date (file_expiry_date)
|
||
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4. 주요 변경 사항 체크리스트
|
||
|
|
|
||
|
|
매니저 상담 음성 저장 로직을 개발할 때 다음 항목을 변경/확인하세요:
|
||
|
|
|
||
|
|
- [ ] **업로드 디렉토리**: `/uploads/manager_consultations/{manager_id}/`
|
||
|
|
- [ ] **GCS 객체 경로**: `manager_consultations/{manager_id}/{filename}`
|
||
|
|
- [ ] **DB 테이블명**: `manager_consultations`
|
||
|
|
- [ ] **DB 필드명**: `manager_id` (기존 `tenant_id` 대신)
|
||
|
|
- [ ] **파일명 접두사**: 필요시 `manager_` 접두사 추가
|
||
|
|
- [ ] **권한 체크**: 매니저 권한 레벨 확인
|
||
|
|
- [ ] **만료 정책**: 보관 기간 설정 (예: 7일, 30일 등)
|
||
|
|
|
||
|
|
### 5. 재사용 가능한 함수
|
||
|
|
|
||
|
|
다음 함수들은 **그대로 재사용** 가능합니다:
|
||
|
|
|
||
|
|
1. ✅ `uploadToGCS()` - GCS 업로드 함수
|
||
|
|
2. ✅ `deleteFromGCS()` - GCS 삭제 함수
|
||
|
|
3. ✅ OAuth 2.0 토큰 생성 로직
|
||
|
|
4. ✅ Google STT API 호출 로직
|
||
|
|
5. ✅ 에러 처리 로직
|
||
|
|
|
||
|
|
**변경이 필요한 부분만**:
|
||
|
|
- 디렉토리 경로
|
||
|
|
- GCS 객체 경로
|
||
|
|
- DB 테이블/필드명
|
||
|
|
- 비즈니스 로직 (제목 생성, 요약 등)
|
||
|
|
|
||
|
|
### 6. 개발 순서
|
||
|
|
|
||
|
|
1. **DB 테이블 생성** (`manager_consultations`)
|
||
|
|
2. **업로드 디렉토리 생성** (`/uploads/manager_consultations/`)
|
||
|
|
3. **`uploadToGCS()` 함수 복사** (기존 코드 그대로)
|
||
|
|
4. **`deleteFromGCS()` 함수 복사** (기존 코드 그대로)
|
||
|
|
5. **`process_manager_consultation.php` 작성** (경로만 변경)
|
||
|
|
6. **`delete_manager_consultation.php` 작성** (경로만 변경)
|
||
|
|
7. **프론트엔드 연동** (기존 `index.php` 참고)
|
||
|
|
|
||
|
|
### 7. 테스트 체크리스트
|
||
|
|
|
||
|
|
- [ ] 작은 파일 (< 10MB) 업로드 테스트
|
||
|
|
- [ ] 큰 파일 (> 10MB) GCS 업로드 테스트
|
||
|
|
- [ ] STT 변환 성공 확인
|
||
|
|
- [ ] DB 저장 확인
|
||
|
|
- [ ] GCS URI 저장 확인
|
||
|
|
- [ ] 파일 삭제 (로컬 + GCS) 확인
|
||
|
|
- [ ] 만료 파일 정리 확인
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 보안 고려사항
|
||
|
|
|
||
|
|
### 1. 서비스 계정 파일 보안
|
||
|
|
|
||
|
|
- 서비스 계정 JSON 파일은 웹에서 직접 접근 불가능한 위치에 저장
|
||
|
|
- 파일 권한 설정: `chmod 600 google_service_account.json`
|
||
|
|
- `.htaccess` 또는 웹 서버 설정으로 `/apikey/` 디렉토리 접근 차단
|
||
|
|
|
||
|
|
### 2. 버킷 접근 제어
|
||
|
|
|
||
|
|
- 최소 권한 원칙 적용
|
||
|
|
- 필요한 경우 버킷 레벨 IAM 정책 설정
|
||
|
|
- 객체 레벨 ACL 설정 고려
|
||
|
|
|
||
|
|
### 3. 파일 경로 검증
|
||
|
|
|
||
|
|
- 사용자 입력값 검증
|
||
|
|
- 경로 조작 공격 방지
|
||
|
|
- SQL 인젝션 방지 (PDO Prepared Statement 사용)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 성능 최적화
|
||
|
|
|
||
|
|
### 1. 토큰 캐싱
|
||
|
|
|
||
|
|
현재 구현은 매번 새 토큰을 생성합니다. 성능 최적화를 위해 토큰 캐싱을 고려할 수 있습니다:
|
||
|
|
|
||
|
|
```php
|
||
|
|
// 토큰 캐싱 예시 (구현 필요)
|
||
|
|
$token_cache_file = sys_get_temp_dir() . '/gcs_token_' . md5($service_account_path) . '.json';
|
||
|
|
if (file_exists($token_cache_file)) {
|
||
|
|
$cached_token = json_decode(file_get_contents($token_cache_file), true);
|
||
|
|
if ($cached_token['expires_at'] > time()) {
|
||
|
|
$accessToken = $cached_token['access_token'];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. 비동기 업로드
|
||
|
|
|
||
|
|
대용량 파일의 경우 비동기 업로드를 고려할 수 있습니다.
|
||
|
|
|
||
|
|
### 3. 청크 업로드
|
||
|
|
|
||
|
|
매우 큰 파일의 경우 멀티파트 업로드를 사용할 수 있습니다.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 테스트 방법
|
||
|
|
|
||
|
|
### 1. 로컬 테스트
|
||
|
|
|
||
|
|
```php
|
||
|
|
// 테스트 스크립트
|
||
|
|
$test_file = '/path/to/test.webm';
|
||
|
|
$bucket_name = 'test-bucket';
|
||
|
|
$object_name = 'test/upload.webm';
|
||
|
|
$service_account = '/path/to/google_service_account.json';
|
||
|
|
|
||
|
|
$result = uploadToGCS($test_file, $bucket_name, $object_name, $service_account);
|
||
|
|
if ($result) {
|
||
|
|
echo "업로드 성공: " . $result . "\n";
|
||
|
|
} else {
|
||
|
|
echo "업로드 실패\n";
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. GCS 콘솔 확인
|
||
|
|
|
||
|
|
- [Google Cloud Console](https://console.cloud.google.com/storage)에서 업로드된 파일 확인
|
||
|
|
- 객체 메타데이터 및 접근 권한 확인
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 참고 자료
|
||
|
|
|
||
|
|
- [Google Cloud Storage REST API 문서](https://cloud.google.com/storage/docs/json_api)
|
||
|
|
- [OAuth 2.0 for Service Accounts](https://developers.google.com/identity/protocols/oauth2/service-account)
|
||
|
|
- [JWT (JSON Web Token) 표준](https://tools.ietf.org/html/rfc7519)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 변경 이력
|
||
|
|
|
||
|
|
| 날짜 | 버전 | 변경 내용 | 작성자 |
|
||
|
|
|------|------|----------|--------|
|
||
|
|
| 2024-12-01 | 1.0 | 초기 문서 작성 | - |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 문의 및 지원
|
||
|
|
|
||
|
|
GCS 저장 로직 관련 문의사항이나 개선 제안은 개발팀에 문의해주세요.
|
||
|
|
|