diff --git a/app/Services/WeatherService.php b/app/Services/WeatherService.php index ec26c4d3..b97856b7 100644 --- a/app/Services/WeatherService.php +++ b/app/Services/WeatherService.php @@ -86,25 +86,30 @@ private function buildForecast(): array $midTmx = $midTa["taMax{$i}"] ?? null; $midWf = $midLand["wf{$i}Am"] ?? ($midLand["wf{$i}"] ?? ''); $midPop = max($midLand["rnSt{$i}Am"] ?? 0, $midLand["rnSt{$i}Pm"] ?? 0, $midLand["rnSt{$i}"] ?? 0); + $midIcon = ! empty($midWf) ? $this->midWeatherToIcon($midWf) : null; - if ($shortData) { - // 단기예보 우선, 기온/강수확률 없으면 중기로 보충 + // 단기예보에 TMN/TMX가 있으면 완전한 데이터로 판단 + $shortComplete = $shortData && ($shortData['tmn'] !== null || $shortData['tmx'] !== null); + + if ($shortComplete) { + // 단기예보 우선, 기온만 없으면 중기로 보충 $forecasts[] = [ 'date' => $date, 'tmn' => $shortData['tmn'] ?? $midTmn, 'tmx' => $shortData['tmx'] ?? $midTmx, - 'icon' => $shortData['icon'] ?? (! empty($midWf) ? $this->midWeatherToIcon($midWf) : null), + 'icon' => $shortData['icon'] ?? $midIcon, 'weather_text' => $shortData['weather_text'] ?: $midWf, 'pop' => $shortData['pop'] ?? $midPop, ]; } else { + // 중기예보 우선 (단기예보가 없거나 TMN/TMX 미포함) $forecasts[] = [ 'date' => $date, - 'tmn' => $midTmn, - 'tmx' => $midTmx, - 'icon' => ! empty($midWf) ? $this->midWeatherToIcon($midWf) : null, - 'weather_text' => $midWf, - 'pop' => $midPop, + 'tmn' => $midTmn ?? ($shortData['tmn'] ?? null), + 'tmx' => $midTmx ?? ($shortData['tmx'] ?? null), + 'icon' => $midIcon ?? ($shortData['icon'] ?? null), + 'weather_text' => ! empty($midWf) ? $midWf : ($shortData['weather_text'] ?? ''), + 'pop' => $midPop > 0 ? $midPop : ($shortData['pop'] ?? 0), ]; } } @@ -201,51 +206,55 @@ private function fetchShortForecast(): array } /** - * 중기기온 API 호출 (18시 발표 실패 시 06시 fallback) + * 중기기온 API 호출 (최신 발표 우선, 이전 발표로 빈 필드 보충) + * + * 기상청 중기예보 API 특성: + * - 06시 발표: D+4 ~ D+10 제공 (D+3 없음) + * - 18시 발표: D+5 ~ D+10 제공 (D+3, D+4 없음) + * → 18시에는 최신 데이터가 D+5부터만 있으므로, 06시 데이터로 D+4를 보충 */ private function fetchMidTemperature(): array { - $tmFcList = $this->getMidBaseTmFcCandidates(); - - foreach ($tmFcList as $tmFc) { - $body = $this->callApi(self::BASE_MID_TA, [ - 'pageNo' => 1, - 'numOfRows' => 10, - 'regId' => self::MID_TA_REG_ID, - 'tmFc' => $tmFc, - ]); - - $items = $body ? data_get($body, 'response.body.items.item', []) : []; - if (! empty($items)) { - return $items[0]; - } - } - - return []; + return $this->fetchMidMerged(self::BASE_MID_TA, self::MID_TA_REG_ID); } /** - * 중기날씨 API 호출 (18시 발표 실패 시 06시 fallback) + * 중기날씨 API 호출 (최신 발표 우선, 이전 발표로 빈 필드 보충) */ private function fetchMidLandForecast(): array { - $tmFcList = $this->getMidBaseTmFcCandidates(); + return $this->fetchMidMerged(self::BASE_MID_LAND, self::MID_LAND_REG_ID); + } - foreach ($tmFcList as $tmFc) { - $body = $this->callApi(self::BASE_MID_LAND, [ + /** + * 중기예보 데이터를 여러 발표시각에서 병합 (최신 우선) + */ + private function fetchMidMerged(string $baseUrl, string $regId): array + { + $merged = []; + + foreach ($this->getMidBaseTmFcCandidates() as $tmFc) { + $body = $this->callApi($baseUrl, [ 'pageNo' => 1, 'numOfRows' => 10, - 'regId' => self::MID_LAND_REG_ID, + 'regId' => $regId, 'tmFc' => $tmFc, ]); $items = $body ? data_get($body, 'response.body.items.item', []) : []; - if (! empty($items)) { - return $items[0]; + if (empty($items)) { + continue; + } + + // 최신 발표 데이터 우선, 이전 발표로 빈 필드만 보충 + foreach ($items[0] as $key => $value) { + if ($value !== null && $value !== '' && ! isset($merged[$key])) { + $merged[$key] = $value; + } } } - return []; + return $merged; } /**