Files
sam-manage/app/Services/WeatherService.php

281 lines
7.9 KiB
PHP
Raw Normal View History

<?php
namespace App\Services;
use Carbon\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class WeatherService
{
private const BASE_SHORT = 'https://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getVilageFcst';
private const BASE_MID_TA = 'https://apis.data.go.kr/1360000/MidFcstInfoService/getMidTa';
private const BASE_MID_LAND = 'https://apis.data.go.kr/1360000/MidFcstInfoService/getMidLandFcst';
// 서울 격자 좌표
private const NX = 60;
private const NY = 127;
// 중기예보 지역 코드 (서울)
private const MID_TA_REG_ID = '11B10101'; // 서울 기온
private const MID_LAND_REG_ID = '11B00000'; // 서울·인천·경기
private string $serviceKey;
public function __construct()
{
$this->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';
}
}