From b717a1cc62fe2135b330b2f74c193f4994130b2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Sun, 22 Feb 2026 19:09:33 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20[weather]=20=EA=B8=B0=EC=83=81=EC=B2=AD?= =?UTF-8?q?=20=EB=82=A0=EC=94=A8=20API=20=EC=97=B0=EB=8F=99=20=EA=B0=80?= =?UTF-8?q?=EC=9D=B4=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 발표시각별 데이터 커버리지 차이 문서화 (06시 vs 18시) - D+3~D+4 사각지대 문제 및 다중 발표 병합 해결법 - 디버깅 명령어, 트러블슈팅 포함 --- docs/INDEX.md | 1 + docs/kma-weather-api.md | 334 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 335 insertions(+) create mode 100644 docs/kma-weather-api.md diff --git a/docs/INDEX.md b/docs/INDEX.md index c8599dd4..4dd5b7ba 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -86,6 +86,7 @@ ### 프로젝트 문서 - **[SETUP_GUIDE.md](./SETUP_GUIDE.md)** - 초기 설정 가이드 - **[HTMX_API_PATTERN.md](./HTMX_API_PATTERN.md)** - HTMX + API 패턴 가이드 - **[LAYOUT_PATTERN.md](./LAYOUT_PATTERN.md)** - 레이아웃 패턴 가이드 +- **[kma-weather-api.md](./kma-weather-api.md)** - 기상청 날씨 API 연동 가이드 (발표시각별 데이터 범위 주의) **SAM 공통 문서:** - **[📊 ../../docs/specs/database-schema.md](../../docs/specs/database-schema.md)** - 데이터베이스 스키마 (Phase 4: 8개 테이블 상세) diff --git a/docs/kma-weather-api.md b/docs/kma-weather-api.md new file mode 100644 index 00000000..c57c8d95 --- /dev/null +++ b/docs/kma-weather-api.md @@ -0,0 +1,334 @@ +# 기상청 날씨 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 서비스 키 설정 + +```php +// 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 단기예보 데이터를 사용할 때 기온이 없을 수 있음에 주의한다. + +```php +// ⚠️ 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 데이터가 빠질 수 있다. 반드시 여러 발표시각의 응답을 병합한다. + +```php +// ✅ 올바른 방식: 최신 발표 우선, 이전 발표로 빈 필드 보충 +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; +} +``` + +```php +// ❌ 잘못된 방식: 첫 번째 비어있지 않은 응답만 사용 +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 유무로 "완전한 단기예보"인지 판단한다. + +```php +// ✅ 단기예보 TMN/TMX가 있으면 신뢰 가능한 데이터 +$shortComplete = $shortData && ($shortData['tmn'] !== null || $shortData['tmx'] !== null); + +if ($shortComplete) { + // 단기예보 icon, pop 사용 +} else { + // 중기예보 icon, pop 우선 사용 +} +``` + +### R3. 불완전 데이터는 짧은 캐시 + +7일 중 기온 데이터가 없는 날이 있으면 10분만 캐시하여 빠른 재시도를 유도한다. + +```php +$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 호출 실패 +**해결**: `.env`의 `KMA_SERVICE_KEY` 확인, 기상청 API 상태 점검 + +### 7.3 기온 데이터 갱신 안 됨 + +**원인**: 3시간 캐시 (`weather_forecast_7days`) +**해결**: `docker exec sam-mng-1 php artisan cache:clear` + +### 7.4 디버깅 명령어 + +```bash +# 현재 날씨 데이터 확인 +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