Files
sam-manage/docs/kma-weather-api.md
김보곤 b717a1cc62 docs: [weather] 기상청 날씨 API 연동 가이드 추가
- 발표시각별 데이터 커버리지 차이 문서화 (06시 vs 18시)
- D+3~D+4 사각지대 문제 및 다중 발표 병합 해결법
- 디버깅 명령어, 트러블슈팅 포함
2026-02-22 19:14:24 +09:00

11 KiB

기상청 날씨 API 연동 가이드

작성일: 2026-02-22 상태: 운영 중 관련 코드: mng/app/Services/WeatherService.php


1. 개요

1.1 목적

SAM 대시보드의 주간 날씨 위젯에서 사용하는 기상청 공공데이터 API의 동작 특성과 구현 시 주의사항을 기록한다.

1.2 사용 API

API 엔드포인트 용도
단기예보 VilageFcstInfoService_2.0/getVilageFcst 오늘 ~ D+3 날씨
중기기온 MidFcstInfoService/getMidTa D+4 ~ D+10 최저/최고기온
중기육상예보 MidFcstInfoService/getMidLandFcst D+4 ~ D+10 날씨/강수확률

1.3 서비스 키 설정

// config/services.php
'kma' => [
    'service_key' => env('KMA_SERVICE_KEY'),
],

2. 핵심 주의사항 (발표시각별 데이터 범위)

경고: 기상청 중기예보 API는 발표시각에 따라 제공하는 날짜 범위가 다르다!

2.1 발표시각별 데이터 커버리지

시간대        │ 단기예보(0200)  │ 중기예보(0600)  │ 중기예보(1800)
─────────────┼────────────────┼────────────────┼────────────────
D+0 (오늘)   │ ✅ TMN/TMX/SKY │                │
D+1 (내일)   │ ✅ TMN/TMX/SKY │                │
D+2 (모레)   │ ✅ TMN/TMX/SKY │                │
D+3          │ ⚠️ SKY만       │ ❌ 없음         │ ❌ 없음
D+4          │ ❌ 없음         │ ✅ 있음         │ ❌ 없음
D+5          │ ❌ 없음         │ ✅ 있음         │ ✅ 있음
D+6          │ ❌ 없음         │ ✅ 있음         │ ✅ 있음
D+7 ~ D+10  │ ❌ 없음         │ ✅ 있음         │ ✅ 있음

2.2 핵심 문제: D+3 ~ D+4 사각지대

┌─────────────────────────────────────────────────────────┐
│                    데이터 커버리지 갭                      │
│                                                         │
│  단기(0200): ████████████████░░░░░░░░░░░░░░░░░░░░░░░░  │
│              D+0  D+1  D+2  D+3                         │
│                              ↑ SKY만, TMN/TMX 없음       │
│                                                         │
│  중기(0600): ░░░░░░░░░░░░░░░░░░░░████████████████████  │
│                                   D+4  D+5  D+6 ...    │
│                              ↑                          │
│                          D+3 = 사각지대!                  │
│                                                         │
│  중기(1800): ░░░░░░░░░░░░░░░░░░░░░░░░░████████████████ │
│                                        D+5  D+6 ...    │
│                              ↑    ↑                     │
│                          D+3, D+4 = 사각지대!             │
└─────────────────────────────────────────────────────────┘

2.3 해결 방법: 다중 발표 데이터 병합

18시 이후에는 18시 발표에 D+4 데이터가 없으므로, 여러 발표시각의 데이터를 병합해야 한다.

병합 전략 (최신 우선):
1. 18시 발표 데이터 적용 (D+5 ~ D+10)
2. 06시 발표 데이터로 빈 필드 보충 (D+4)
3. 전일 18시 발표로 추가 보충 (필요 시)

3. 단기예보 API 특성

3.1 발표시각

base_time 설명
0200 새벽 2시 발표 (현재 사용 중)
0500, 0800, 1100, 1400, 1700, 2000, 2300 3시간 간격 발표

3.2 카테고리

카테고리 설명 비고
TMN 일 최저기온 D+0 ~ D+2만 제공
TMX 일 최고기온 D+0 ~ D+2만 제공
SKY 하늘상태 1=맑음, 3=구름많음, 4=흐림
PTY 강수형태 0=없음, 1=비, 2=비/눈, 3=눈, 4=소나기
POP 강수확률 0~100%

3.3 주의: D+3 부분 데이터

단기예보 base_time=0200은 D+3 날짜의 SKY/PTY/POP 데이터를 포함할 수 있지만, TMN/TMX는 없다. 코드에서 D+3 단기예보 데이터를 사용할 때 기온이 없을 수 있음에 주의한다.

// ⚠️ D+3 단기예보: tmn=null, tmx=null, sky=있음, pty=있음
$grouped[$fcstDate] = [
    'tmn' => null,  // TMN 카테고리가 D+3에 없음
    'tmx' => null,  // TMX 카테고리가 D+3에 없음
    'sky' => 1,     // SKY는 D+3에도 있음
    'pty' => 0,     // PTY는 D+3에도 있음
];

4. 중기예보 API 특성

4.1 발표시각

  • 06시: 매일 06:00 발표 → D+4 ~ D+10 제공
  • 18시: 매일 18:00 발표 → D+5 ~ D+10 제공

경고: 18시 발표는 D+3, D+4 데이터를 제공하지 않는다!

4.2 응답 필드 구조

중기기온 (getMidTa)

taMin3, taMax3   ← D+3 (06시 발표에서도 없을 수 있음)
taMin4, taMax4   ← D+4 (06시 발표에 있음, 18시 발표에 없음)
taMin5, taMax5   ← D+5
...
taMin10, taMax10 ← D+10

중기육상예보 (getMidLandFcst)

wf3Am, wf3Pm, rnSt3Am, rnSt3Pm   ← D+3 (없을 수 있음)
wf4Am, wf4Pm, rnSt4Am, rnSt4Pm   ← D+4 (06시에만)
wf5Am, wf5Pm, rnSt5Am, rnSt5Pm   ← D+5
wf6Am, wf6Pm, rnSt6Am, rnSt6Pm   ← D+6
wf7, rnSt7                        ← D+7 (AM/PM 구분 없음)
wf8, rnSt8                        ← D+8
wf9, rnSt9                        ← D+9
wf10, rnSt10                      ← D+10

4.3 실측 데이터 (2026-02-22 기준)

                  06시 발표              18시 발표
Day+3 (수):       taMin=없음             taMin=없음
Day+4 (목):       taMin=2, taMax=14      taMin=없음, taMax=없음
Day+5 (금):       taMin=4, taMax=13      taMin=4, taMax=13
Day+6 (토):       taMin=4, taMax=13      taMin=3, taMax=14

5. 데이터 흐름 아키텍처

5.1 전체 흐름

HTMX 요청
  │
  └→ GET /dashboard/weather
     │
     └→ DashboardWeatherController::weather()
        │
        └→ WeatherService::getWeeklyForecast()
           │
           ├→ 캐시 확인 (weather_forecast_7days)
           │   ├ 완전한 데이터: 3시간 TTL
           │   └ 불완전 데이터: 10분 TTL
           │
           └→ buildForecast()
              │
              ├→ fetchShortForecast()    ← D+0 ~ D+3(부분)
              ├→ fetchMidTemperature()   ← D+4 ~ D+10 (병합)
              └→ fetchMidLandForecast()  ← D+4 ~ D+10 (병합)
              │
              └→ 7일 데이터 조합
                 ├ 단기 TMN/TMX 있으면 → 단기예보 우선
                 └ 단기 TMN/TMX 없으면 → 중기예보 우선

5.2 관련 파일

파일 역할
app/Services/WeatherService.php API 호출, 데이터 병합, 캐싱
app/Http/Controllers/DashboardWeatherController.php HTMX 요청 처리
resources/views/dashboard/partials/weather.blade.php 7일 날씨 카드 렌더링
resources/views/dashboard/partials/weather-icon.blade.php 날씨 SVG 아이콘

6. 구현 규칙

R1. 중기예보는 반드시 다중 발표 병합

단일 발표시각만 사용하면 D+4 데이터가 빠질 수 있다. 반드시 여러 발표시각의 응답을 병합한다.

// ✅ 올바른 방식: 최신 발표 우선, 이전 발표로 빈 필드 보충
private function fetchMidMerged(string $baseUrl, string $regId): array
{
    $merged = [];
    foreach ($this->getMidBaseTmFcCandidates() as $tmFc) {
        $items = $this->callApi($baseUrl, [...]);
        foreach ($items[0] as $key => $value) {
            if ($value !== null && $value !== '' && !isset($merged[$key])) {
                $merged[$key] = $value;  // 최신 데이터 우선
            }
        }
    }
    return $merged;
}
// ❌ 잘못된 방식: 첫 번째 비어있지 않은 응답만 사용
foreach ($tmFcList as $tmFc) {
    $items = $this->callApi(...);
    if (!empty($items)) return $items[0];  // D+4 데이터 누락!
}

R2. 단기예보 데이터 완전성 검증

단기예보가 D+3/D+4에 SKY/PTY만 반환하고 TMN/TMX가 없는 경우, 해당 날의 icon/pop은 신뢰할 수 없다. TMN/TMX 유무로 "완전한 단기예보"인지 판단한다.

// ✅ 단기예보 TMN/TMX가 있으면 신뢰 가능한 데이터
$shortComplete = $shortData && ($shortData['tmn'] !== null || $shortData['tmx'] !== null);

if ($shortComplete) {
    // 단기예보 icon, pop 사용
} else {
    // 중기예보 icon, pop 우선 사용
}

R3. 불완전 데이터는 짧은 캐시

7일 중 기온 데이터가 없는 날이 있으면 10분만 캐시하여 빠른 재시도를 유도한다.

$hasIncomplete = collect($forecasts)->contains(
    fn ($fc) => $fc['tmn'] === null && $fc['tmx'] === null && $fc['icon'] === null
);
Cache::put('weather_forecast_7days', $forecasts, $hasIncomplete ? 600 : 10800);

R4. 날씨 문자열 → 아이콘 매핑

중기예보 문자열 아이콘 비고
맑음 sun
구름많음, 구름 cloud-sun
흐림 cloud
, 소나기 rain
snow
비/눈 (눈+비 포함) sleet str_contains 순서 주의

7. 트러블슈팅

7.1 특정 요일만 "준비 중" 표시

원인: 18시 이후 중기예보가 해당 요일 데이터를 미제공 해결: 06시 발표 데이터 병합 확인, 캐시 클리어 (php artisan cache:clear)

7.2 전체 날씨 미표시 ("날씨 정보를 불러올 수 없습니다")

원인: KMA_SERVICE_KEY 미설정 또는 API 호출 실패 해결: .envKMA_SERVICE_KEY 확인, 기상청 API 상태 점검

7.3 기온 데이터 갱신 안 됨

원인: 3시간 캐시 (weather_forecast_7days) 해결: docker exec sam-mng-1 php artisan cache:clear

7.4 디버깅 명령어

# 현재 날씨 데이터 확인
docker exec sam-mng-1 php artisan tinker --execute="
\$ws = app(App\Services\WeatherService::class);
\$forecasts = \$ws->getWeeklyForecast();
foreach (\$forecasts as \$i => \$fc) {
    \$dt = \Carbon\Carbon::createFromFormat('Ymd', \$fc['date']);
    \$day = ['일','월','화','수','목','금','토'][\$dt->dayOfWeek];
    echo \"[\$i] \$day (\$fc[date]) tmn=\" . (\$fc['tmn'] ?? 'NULL')
       . \" tmx=\" . (\$fc['tmx'] ?? 'NULL')
       . \" icon=\" . (\$fc['icon'] ?? 'NULL')
       . \" pop=\" . (\$fc['pop'] ?? 0) . \"\n\";
}
"

# 캐시 강제 클리어 후 재조회
docker exec sam-mng-1 php artisan cache:clear

8. 변경 이력

날짜 내용
2026-02-22 최초 작성. 중기예보 발표시각별 데이터 범위 차이 발견 및 병합 로직 구현

관련 문서

  • 기상청 공공데이터 포털: https://www.data.go.kr
  • 단기예보 API: VilageFcstInfoService_2.0
  • 중기예보 API: MidFcstInfoService

최종 업데이트: 2026-02-22