diff --git a/app/Http/Controllers/RdController.php b/app/Http/Controllers/RdController.php index a5f9588a..f5e7180e 100644 --- a/app/Http/Controllers/RdController.php +++ b/app/Http/Controllers/RdController.php @@ -9,6 +9,8 @@ use App\Services\Rd\AiQuotationService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Log; use Illuminate\View\View; class RdController extends Controller @@ -337,4 +339,117 @@ public function soundLogo(Request $request): View|\Illuminate\Http\Response return view('rd.sound-logo.index'); } + + /** + * 사운드 로고 AI 생성 (Gemini API) + */ + public function soundLogoGenerate(Request $request): JsonResponse + { + $request->validate([ + 'prompt' => 'required|string|max:500', + 'category' => 'nullable|string', + 'duration' => 'nullable|numeric|min:0.3|max:5', + ]); + + $apiKey = config('services.gemini.api_key'); + $baseUrl = config('services.gemini.base_url', 'https://generativelanguage.googleapis.com/v1beta'); + $model = config('services.gemini.model', 'gemini-2.5-flash'); + + if (! $apiKey) { + return response()->json(['success' => false, 'error' => 'Gemini API 키가 설정되지 않았습니다.'], 500); + } + + $category = $request->category ?? '기업 시그널'; + $duration = $request->duration ?? 1.5; + + $prompt = <<prompt} +- 카테고리: {$category} +- 목표 길이: {$duration}초 + +## 사용 가능한 음표 +C3, C#3, D3, D#3, E3, F3, F#3, G3, G#3, A3, A#3, B3, +C4, C#4, D4, D#4, E4, F4, F#4, G4, G#4, A4, A#4, B4, +C5, C#5, D5, D#5, E5, F5, F#5, G5, G#5, A5, A#5, B5, C6 + +## 음표 타입 +- note: 단일 음 (note 필드 필수) +- chord: 화음 (chord 배열 필수, 2~4개 음) +- rest: 쉼표 (duration만 필요) + +## 신스 타입 +- sine: 부드러움 (기업 로고, 알림에 적합) +- triangle: 따뜻함 (성공, 게임에 적합) +- square: 8bit/디지털 (게임, UI에 적합) +- sawtooth: 날카로움 (록, 긴급 알림에 적합) + +## 반드시 아래 JSON 형식으로만 응답하세요 +{ + "name": "사운드 이름", + "desc": "사운드 설명 (한줄)", + "synth": "sine", + "adsr": { "attack": 10, "decay": 80, "sustain": 0.6, "release": 400 }, + "volume": 0.8, + "reverb": 0.3, + "notes": [ + { "type": "note", "note": "C5", "duration": 0.20, "velocity": 0.8 }, + { "type": "rest", "duration": 0.10 }, + { "type": "chord", "chord": ["C4", "E4", "G4"], "duration": 0.50, "velocity": 1.0 } + ] +} + +## 설계 원칙 +- 음표의 duration 합계가 목표 길이({$duration}초)에 근접하도록 설계 +- velocity: 0.3~1.0 (음의 강약으로 표현력 추가) +- ADSR: attack(1~500ms), decay(10~1000ms), sustain(0~1.0), release(10~3000ms) +- 카테고리 특성에 맞는 synth와 ADSR 선택 +- 음악적으로 조화롭고 기억에 남는 멜로디 설계 +- 최소 2개, 최대 12개 음표 사용 +PROMPT; + + try { + $response = Http::timeout(30)->post( + "{$baseUrl}/models/{$model}:generateContent?key={$apiKey}", + [ + 'contents' => [ + ['parts' => [['text' => $prompt]]], + ], + 'generationConfig' => [ + 'temperature' => 0.9, + 'maxOutputTokens' => 2048, + 'responseMimeType' => 'application/json', + ], + ] + ); + } catch (\Exception $e) { + Log::error('SoundLogo AI 생성 실패', ['error' => $e->getMessage()]); + + return response()->json(['success' => false, 'error' => 'AI 서버 연결 실패'], 500); + } + + if (! $response->successful()) { + Log::error('SoundLogo AI API 오류', ['status' => $response->status(), 'body' => $response->body()]); + + return response()->json(['success' => false, 'error' => 'AI 생성 실패: '.$response->status()], 500); + } + + $data = $response->json(); + $text = $data['candidates'][0]['content']['parts'][0]['text'] ?? ''; + + // JSON 파싱 (코드블록 제거) + $text = preg_replace('/^```(?:json)?\s*/m', '', $text); + $text = preg_replace('/```\s*$/m', '', $text); + $result = json_decode(trim($text), true); + + if (! $result || ! isset($result['notes'])) { + Log::warning('SoundLogo AI 응답 파싱 실패', ['text' => substr($text, 0, 500)]); + + return response()->json(['success' => false, 'error' => 'AI 응답을 파싱할 수 없습니다.'], 500); + } + + return response()->json(['success' => true, 'data' => $result]); + } } diff --git a/resources/views/rd/sound-logo/index.blade.php b/resources/views/rd/sound-logo/index.blade.php index 638a63bf..cd3c76e8 100644 --- a/resources/views/rd/sound-logo/index.blade.php +++ b/resources/views/rd/sound-logo/index.blade.php @@ -200,6 +200,8 @@ display: flex; align-items: center; gap: 10px; padding: 12px 16px; background: var(--sl-card); border-top: 1px solid var(--sl-border); flex-shrink: 0; } + +@keyframes spin { to { transform: rotate(360deg); } }
@@ -215,6 +217,9 @@ +
@@ -325,6 +330,119 @@ + + +