serviceKey = config('services.kma.service_key', ''); } /** * 7일 예보 데이터 반환 (3시간 캐시) */ public function getWeeklyForecast(): array { if (empty($this->serviceKey)) { return []; } return Cache::remember('weather_forecast_7days', 10800, function () { try { return $this->buildForecast(); } catch (\Throwable $e) { Log::warning('WeatherService: 예보 조합 실패', ['error' => $e->getMessage()]); return []; } }); } private function buildForecast(): array { $today = Carbon::today(); $forecasts = []; // 1) 단기예보 (오늘 ~ +2일, 최대 3일) $short = $this->fetchShortForecast(); foreach ($short as $date => $data) { $forecasts[$date] = $data; } // 2) 중기기온 + 중기날씨 (3일 ~ 7일) $midTa = $this->fetchMidTemperature(); $midLand = $this->fetchMidLandForecast(); for ($i = 3; $i <= 7; $i++) { $date = $today->copy()->addDays($i)->format('Ymd'); $forecasts[$date] = [ 'date' => $date, 'tmn' => $midTa["taMin{$i}"] ?? null, 'tmx' => $midTa["taMax{$i}"] ?? null, 'icon' => $this->midWeatherToIcon($midLand["wf{$i}Am"] ?? ($midLand["wf{$i}"] ?? '')), ]; } // 날짜순 정렬 후 7일만 ksort($forecasts); return array_slice(array_values($forecasts), 0, 7); } /** * 단기예보 API 호출 — TMN/TMX/SKY/PTY 추출 */ private function fetchShortForecast(): array { $now = Carbon::now(); // base_date: 오늘, base_time: 0200 (02시 발표 → TMN/TMX 포함) $baseDate = $now->hour < 2 ? $now->copy()->subDay()->format('Ymd') : $now->format('Ymd'); $baseTime = '0200'; $response = Http::timeout(10)->get(self::BASE_SHORT, [ 'serviceKey' => $this->serviceKey, 'pageNo' => 1, 'numOfRows' => 1000, 'dataType' => 'JSON', 'base_date' => $baseDate, 'base_time' => $baseTime, 'nx' => self::NX, 'ny' => self::NY, ]); if (! $response->successful()) { Log::warning('WeatherService: 단기예보 API 실패', ['status' => $response->status()]); return []; } $body = $response->json(); $items = data_get($body, 'response.body.items.item', []); if (empty($items)) { return []; } // 날짜별 그룹 $grouped = []; foreach ($items as $item) { $fcstDate = $item['fcstDate']; $category = $item['category']; $value = $item['fcstValue']; if (! isset($grouped[$fcstDate])) { $grouped[$fcstDate] = ['date' => $fcstDate, 'tmn' => null, 'tmx' => null, 'sky' => null, 'pty' => null]; } match ($category) { 'TMN' => $grouped[$fcstDate]['tmn'] = (int) $value, 'TMX' => $grouped[$fcstDate]['tmx'] = (int) $value, 'SKY' => $grouped[$fcstDate]['sky'] ??= (int) $value, 'PTY' => $grouped[$fcstDate]['pty'] ??= (int) $value, default => null, }; } // 아이콘 매핑 $result = []; foreach ($grouped as $date => $data) { $result[$date] = [ 'date' => $date, 'tmn' => $data['tmn'], 'tmx' => $data['tmx'], 'icon' => $this->shortWeatherToIcon($data['sky'] ?? 1, $data['pty'] ?? 0), ]; } return $result; } /** * 중기기온 API 호출 */ private function fetchMidTemperature(): array { $tmFc = $this->getMidBaseTmFc(); $response = Http::timeout(10)->get(self::BASE_MID_TA, [ 'serviceKey' => $this->serviceKey, 'pageNo' => 1, 'numOfRows' => 10, 'dataType' => 'JSON', 'regId' => self::MID_TA_REG_ID, 'tmFc' => $tmFc, ]); if (! $response->successful()) { return []; } $items = data_get($response->json(), 'response.body.items.item', []); return $items[0] ?? []; } /** * 중기날씨 API 호출 */ private function fetchMidLandForecast(): array { $tmFc = $this->getMidBaseTmFc(); $response = Http::timeout(10)->get(self::BASE_MID_LAND, [ 'serviceKey' => $this->serviceKey, 'pageNo' => 1, 'numOfRows' => 10, 'dataType' => 'JSON', 'regId' => self::MID_LAND_REG_ID, 'tmFc' => $tmFc, ]); if (! $response->successful()) { return []; } $items = data_get($response->json(), 'response.body.items.item', []); return $items[0] ?? []; } /** * 중기예보 발표시각 (06시/18시 기준) */ private function getMidBaseTmFc(): string { $now = Carbon::now(); if ($now->hour >= 18) { return $now->format('Ymd').'1800'; } elseif ($now->hour >= 6) { return $now->format('Ymd').'0600'; } return $now->copy()->subDay()->format('Ymd').'1800'; } /** * 단기예보 SKY + PTY → 아이콘 이름 * PTY 우선: 비/눈/진눈깨비가 있으면 해당 아이콘 */ private function shortWeatherToIcon(int $sky, int $pty): string { // PTY (강수형태) 우선 if ($pty > 0) { return match ($pty) { 1, 4 => 'rain', // 비, 소나기 2 => 'sleet', // 비/눈 3 => 'snow', // 눈 5 => 'rain', // 빗방울 6 => 'sleet', // 빗방울/눈날림 7 => 'snow', // 눈날림 default => 'cloud', }; } // SKY (하늘상태) return match ($sky) { 1 => 'sun', // 맑음 3 => 'cloud-sun', // 구름많음 4 => 'cloud', // 흐림 default => 'sun', }; } /** * 중기예보 한글 날씨 문자열 → 아이콘 이름 */ private function midWeatherToIcon(string $wf): string { if (empty($wf)) { return 'cloud'; } $wf = mb_strtolower($wf); if (str_contains($wf, '눈') && str_contains($wf, '비')) { return 'sleet'; } if (str_contains($wf, '눈')) { return 'snow'; } if (str_contains($wf, '비') || str_contains($wf, '소나기')) { return 'rain'; } if (str_contains($wf, '흐림')) { return 'cloud'; } if (str_contains($wf, '구름')) { return 'cloud-sun'; } if (str_contains($wf, '맑음')) { return 'sun'; } return 'cloud'; } }