3000, 'H0' => 3000, 'QTY' => 1] * @param int $tenantId 테넌트 ID * @return array 성공 시 API 응답, 실패 시 ['success' => false, 'error' => '...'] */ public function calculateBom(string $finishedGoodsCode, array $variables, int $tenantId): array { try { $apiKey = config('api-explorer.default_environments.0.api_key') ?: env('FLOW_TESTER_API_KEY', ''); // Bearer token: ApiTokenService로 세션 토큰 확인, 만료 시 재발급 $bearerToken = $this->resolveApiToken($tenantId); [$connectUrl, $headers] = self::resolveApiConnection('/api/v1/quotes/calculate/bom'); $headers = array_merge($headers, [ 'X-API-KEY' => $apiKey, 'X-TENANT-ID' => (string) $tenantId, ]); $http = Http::timeout(30)->withoutVerifying()->withHeaders($headers); if ($bearerToken) { $http = $http->withToken($bearerToken); } // API의 QuoteBomCalculateRequest는 W0, H0, QTY 등을 최상위 레벨에서 기대 $payload = array_merge( ['finished_goods_code' => $finishedGoodsCode], $variables // W0, H0, QTY 등을 풀어서 전송 ); $response = $http->post($connectUrl, $payload); if ($response->successful()) { $json = $response->json(); // ApiResponse::handle()는 {success, message, data} 구조로 래핑 return $json['data'] ?? $json; } Log::warning('FormulaApiService: API 호출 실패', [ 'status' => $response->status(), 'body' => $response->body(), 'code' => $finishedGoodsCode, ]); return [ 'success' => false, 'error' => 'API 응답 오류: HTTP '.$response->status(), ]; } catch (\Exception $e) { Log::error('FormulaApiService: 예외 발생', [ 'message' => $e->getMessage(), 'code' => $finishedGoodsCode, ]); return [ 'success' => false, 'error' => '수식 계산 서버 연결 실패: '.$e->getMessage(), ]; } } /** * API 연결 URL과 헤더를 환경에 맞게 결정 * * Docker (API_INTERNAL_URL 설정 시): * URL: https://nginx{$path} | Host: api.sam.kr * 서버 (API_INTERNAL_URL 미설정): * URL: https://api.dev.codebridge-x.com{$path} | Host 헤더 불필요 * * @return array{0: string, 1: array} [연결 URL, 추가 헤더] */ public static function resolveApiConnection(string $path): array { $baseUrl = config('services.api.base_url'); $internalUrl = config('services.api.internal_url'); $headers = [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', ]; if ($internalUrl) { // Docker: nginx 컨테이너 경유, Host 헤더로 서버 블록 라우팅 $host = parse_url($baseUrl, PHP_URL_HOST) ?: 'api.sam.kr'; $headers['Host'] = $host; return [rtrim($internalUrl, '/').$path, $headers]; } // 서버: base_url로 직접 연결 return [rtrim($baseUrl, '/').$path, $headers]; } /** * API Bearer token 확보 (세션 토큰 → 만료/미존재 시 재발급) */ private function resolveApiToken(int $tenantId): ?string { $tokenService = new ApiTokenService; // 세션에 유효한 토큰이 있으면 사용 if (! $tokenService->isTokenExpired()) { return $tokenService->getSessionToken(); } // 토큰 만료 또는 미존재 → 교환 시도 $userId = auth()->id(); if (! $userId) { Log::warning('[FormulaApiService] 인증된 사용자 없음 - API 토큰 교환 불가'); return null; } $result = $tokenService->exchangeToken($userId, $tenantId); if ($result['success']) { $tokenService->storeTokenInSession( $result['data']['access_token'], $result['data']['expires_in'] ); return $result['data']['access_token']; } Log::warning('[FormulaApiService] API 토큰 교환 실패', [ 'error' => $result['error'] ?? '', ]); return null; } }