diff --git a/app/Services/WeatherService.php b/app/Services/WeatherService.php index e1433196..81779e5a 100644 --- a/app/Services/WeatherService.php +++ b/app/Services/WeatherService.php @@ -55,32 +55,66 @@ public function getWeeklyForecast(): array 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일) + // 2) 중기기온 + 중기날씨 (4일 ~ 10일) $midTa = $this->fetchMidTemperature(); $midLand = $this->fetchMidLandForecast(); - for ($i = 3; $i <= 7; $i++) { + // 오늘부터 7일간 데이터 조합 + $forecasts = []; + for ($i = 0; $i < 7; $i++) { $date = $today->copy()->addDays($i)->format('Ymd'); - $forecasts[$date] = [ + + // 단기예보 데이터 우선 + if (isset($short[$date])) { + $forecasts[] = $short[$date]; + + continue; + } + + // 중기예보 데이터 + $tmn = $midTa["taMin{$i}"] ?? null; + $tmx = $midTa["taMax{$i}"] ?? null; + $wf = $midLand["wf{$i}Am"] ?? ($midLand["wf{$i}"] ?? ''); + + $forecasts[] = [ 'date' => $date, - 'tmn' => $midTa["taMin{$i}"] ?? null, - 'tmx' => $midTa["taMax{$i}"] ?? null, - 'icon' => $this->midWeatherToIcon($midLand["wf{$i}Am"] ?? ($midLand["wf{$i}"] ?? '')), + 'tmn' => $tmn, + 'tmx' => $tmx, + 'icon' => ! empty($wf) ? $this->midWeatherToIcon($wf) : null, + 'weather_text' => $wf, ]; } - // 날짜순 정렬 후 7일만 - ksort($forecasts); + return $forecasts; + } - return array_slice(array_values($forecasts), 0, 7); + /** + * 기상청 API 호출 헬퍼 — serviceKey 이중 인코딩 방지 + */ + private function callApi(string $baseUrl, array $params): ?array + { + $params['serviceKey'] = $this->serviceKey; + $params['dataType'] = 'JSON'; + $queryString = http_build_query($params, '', '&', PHP_QUERY_RFC3986); + + $response = Http::timeout(10) + ->withOptions(['query' => $queryString]) + ->get($baseUrl); + + if (! $response->successful()) { + Log::warning('WeatherService API 실패', [ + 'url' => $baseUrl, + 'status' => $response->status(), + ]); + + return null; + } + + return $response->json(); } /** @@ -89,30 +123,22 @@ private function buildForecast(): array 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, + $body = $this->callApi(self::BASE_SHORT, [ 'pageNo' => 1, 'numOfRows' => 1000, - 'dataType' => 'JSON', 'base_date' => $baseDate, - 'base_time' => $baseTime, + 'base_time' => '0200', 'nx' => self::NX, 'ny' => self::NY, ]); - if (! $response->successful()) { - Log::warning('WeatherService: 단기예보 API 실패', ['status' => $response->status()]); - + if (! $body) { return []; } - $body = $response->json(); $items = data_get($body, 'response.body.items.item', []); - if (empty($items)) { return []; } @@ -137,14 +163,15 @@ private function fetchShortForecast(): array }; } - // 아이콘 매핑 $result = []; foreach ($grouped as $date => $data) { + $icon = $this->shortWeatherToIcon($data['sky'] ?? 1, $data['pty'] ?? 0); $result[$date] = [ 'date' => $date, 'tmn' => $data['tmn'], 'tmx' => $data['tmx'], - 'icon' => $this->shortWeatherToIcon($data['sky'] ?? 1, $data['pty'] ?? 0), + 'icon' => $icon, + 'weather_text' => $this->iconToText($icon), ]; } @@ -156,22 +183,18 @@ private function fetchShortForecast(): array */ private function fetchMidTemperature(): array { - $tmFc = $this->getMidBaseTmFc(); - - $response = Http::timeout(10)->get(self::BASE_MID_TA, [ - 'serviceKey' => $this->serviceKey, + $body = $this->callApi(self::BASE_MID_TA, [ 'pageNo' => 1, 'numOfRows' => 10, - 'dataType' => 'JSON', 'regId' => self::MID_TA_REG_ID, - 'tmFc' => $tmFc, + 'tmFc' => $this->getMidBaseTmFc(), ]); - if (! $response->successful()) { + if (! $body) { return []; } - $items = data_get($response->json(), 'response.body.items.item', []); + $items = data_get($body, 'response.body.items.item', []); return $items[0] ?? []; } @@ -181,22 +204,18 @@ private function fetchMidTemperature(): array */ private function fetchMidLandForecast(): array { - $tmFc = $this->getMidBaseTmFc(); - - $response = Http::timeout(10)->get(self::BASE_MID_LAND, [ - 'serviceKey' => $this->serviceKey, + $body = $this->callApi(self::BASE_MID_LAND, [ 'pageNo' => 1, 'numOfRows' => 10, - 'dataType' => 'JSON', 'regId' => self::MID_LAND_REG_ID, - 'tmFc' => $tmFc, + 'tmFc' => $this->getMidBaseTmFc(), ]); - if (! $response->successful()) { + if (! $body) { return []; } - $items = data_get($response->json(), 'response.body.items.item', []); + $items = data_get($body, 'response.body.items.item', []); return $items[0] ?? []; } @@ -219,28 +238,25 @@ private function getMidBaseTmFc(): string /** * 단기예보 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', // 눈날림 + 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', // 흐림 + 1 => 'sun', + 3 => 'cloud-sun', + 4 => 'cloud', default => 'sun', }; } @@ -254,8 +270,6 @@ private function midWeatherToIcon(string $wf): string return 'cloud'; } - $wf = mb_strtolower($wf); - if (str_contains($wf, '눈') && str_contains($wf, '비')) { return 'sleet'; } @@ -268,7 +282,7 @@ private function midWeatherToIcon(string $wf): string if (str_contains($wf, '흐림')) { return 'cloud'; } - if (str_contains($wf, '구름')) { + if (str_contains($wf, '구름많음') || str_contains($wf, '구름')) { return 'cloud-sun'; } if (str_contains($wf, '맑음')) { @@ -277,4 +291,20 @@ private function midWeatherToIcon(string $wf): string return 'cloud'; } + + /** + * 아이콘 → 한글 텍스트 + */ + private function iconToText(string $icon): string + { + return match ($icon) { + 'sun' => '맑음', + 'cloud-sun' => '구름많음', + 'cloud' => '흐림', + 'rain' => '비', + 'snow' => '눈', + 'sleet' => '비/눈', + default => '', + }; + } } diff --git a/resources/views/dashboard/partials/weather.blade.php b/resources/views/dashboard/partials/weather.blade.php index 533c93ef..2e5e986c 100644 --- a/resources/views/dashboard/partials/weather.blade.php +++ b/resources/views/dashboard/partials/weather.blade.php @@ -1,6 +1,6 @@ {{-- 주간 날씨 위젯 (HTMX 파티셜) --}} @if(empty($forecasts)) -