weather_kma가 RN1(직전 1시간)만 내보내서 "오늘 비 얼마나 왔어"에
답할 데이터가 없었다. 모델이 "1시간 값만 있어 일 누적은 못 낸다"고
한 건 정확한 진술이었고, 공백은 도구 쪽이었다.
초단기실황이 같은 날 과거 base_time을 받아준다는 걸 라이브 API로
확인해, 시간별 실측을 합쳐 일 누적을 만든다. 새 API 승인 불필요.
RN1(HH00)은 HH00에 "끝나는" 1시간이라 0000은 어제 23~24시 —
0100부터 합산한다.
여러 날에 걸친 비는 관측으로 안 된다. 초단기실황은 resultCode 10
("최근 1일 간의 자료만 제공합니다")으로 잘리고, ASOS 시간자료는
이 키로 403 미승인이다. 그래서 사상 누적만 Open-Meteo로 내고
오늘 누적(기상청 실측)과 한 숫자로 합치지 않는다 — 이음매에서
서울 기준 9.8 vs 10.8이 충돌하는데 섞은 값은 검증이 안 된다.
사상 판정: 6시간 이상 그치면 다른 비(기상청 강수 계속시간 관례),
시간당 0.1mm 미만은 강수시간 제외, 조회창 7일(꽉 차면 하한 표시).
이미 6시간 넘게 그친 비와 오늘 안에서 시작한 비는 표시하지 않는다.
forecast_days=1로 받은 뒤 현재 시각까지만 잘라 써서 미도래 예보가
누적에 섞이지 않게 한다.
교차검증(2026-08-16 서울, 같은 구간): KMA 9.8mm vs Open-Meteo 10.3mm.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
79 lines
3.2 KiB
TypeScript
79 lines
3.2 KiB
TypeScript
/**
|
|
* weather-kma-daily-rain.test.ts
|
|
*
|
|
* weather_kma의 초단기실황은 오랫동안 RN1(직전 1시간 강수량)만 내보냈다. 그래서 "오늘 비
|
|
* 얼마나 왔어?"를 물으면 모델이 — 틀린 말은 아니지만 쓸모없게 — "1시간 강수량만 있고 일
|
|
* 누적은 없어서 답할 수 없다"고 답했다(2026-08-16 실제 사례).
|
|
*
|
|
* 초단기실황이 같은 날 과거 base_time을 받아준다는 걸 라이브 API로 확인해서, 시간별 실측을
|
|
* 합쳐 일 누적을 만든다. 이 파일은 그 합산 로직 — 네트워크를 안 타는 순수 함수다.
|
|
*
|
|
* 핵심 규칙: RN1(HH00)은 HH00에 "끝나는" 1시간의 강수량이다. 따라서 오늘 00시~현재는
|
|
* base_time 0100..HH00의 합이고, 0000은 어제 23~24시라 빠져야 한다. 이 경계는 호출부
|
|
* (fetchKmaTodayRain)가 지키고, 여기서는 넘어온 값들의 합산·결측 처리만 검증한다.
|
|
*/
|
|
|
|
import { test, describe } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { sumKmaHourlyRain } from '../src/tools/weather';
|
|
|
|
describe('sumKmaHourlyRain — 시간별 RN1 실측 합산', () => {
|
|
test('숫자 문자열을 더해 일 누적을 낸다', () => {
|
|
const { mm, missing } = sumKmaHourlyRain([
|
|
{ hour: 1, rn1: '0' },
|
|
{ hour: 2, rn1: '3.1' },
|
|
{ hour: 3, rn1: '0' },
|
|
{ hour: 4, rn1: '1.5' },
|
|
]);
|
|
assert.equal(mm, 4.6);
|
|
assert.deepEqual(missing, []);
|
|
});
|
|
|
|
test('부동소수 드리프트를 소수 1자리로 정리한다', () => {
|
|
// 0.1 + 0.2 = 0.30000000000000004 — 관측값이 소수 1자리라 그냥 더하면 그대로 새어나온다.
|
|
const { mm } = sumKmaHourlyRain([
|
|
{ hour: 1, rn1: '0.1' },
|
|
{ hour: 2, rn1: '0.2' },
|
|
]);
|
|
assert.equal(mm, 0.3);
|
|
});
|
|
|
|
test('"강수없음"은 결측이 아니라 0으로 센다', () => {
|
|
// 초단기실황은 숫자를 주지만 형제 엔드포인트들은 0을 "강수없음"으로 보낸다.
|
|
// 이걸 결측으로 세면 멀쩡한 합계에 "결측" 딱지가 붙는다.
|
|
const { mm, missing } = sumKmaHourlyRain([
|
|
{ hour: 1, rn1: '강수없음' },
|
|
{ hour: 2, rn1: '2.0' },
|
|
]);
|
|
assert.equal(mm, 2);
|
|
assert.deepEqual(missing, []);
|
|
});
|
|
|
|
test('null/빈문자는 결측으로 기록하되 합산은 계속한다', () => {
|
|
// 한 시각이 NO_DATA로 빠져도 나머지 합계는 살아야 한다 — 대신 부분합임을 알려야 한다.
|
|
const { mm, missing } = sumKmaHourlyRain([
|
|
{ hour: 1, rn1: '1.0' },
|
|
{ hour: 2, rn1: null },
|
|
{ hour: 3, rn1: '' },
|
|
{ hour: 4, rn1: undefined },
|
|
{ hour: 5, rn1: '2.0' },
|
|
]);
|
|
assert.equal(mm, 3);
|
|
assert.deepEqual(missing, [2, 3, 4]);
|
|
});
|
|
|
|
test('전부 결측이면 0mm에 결측 목록만 남는다', () => {
|
|
// 호출부는 이 경우를 "조회 불가"로 바꿔서 0mm를 사실처럼 말하지 않게 한다.
|
|
const { mm, missing } = sumKmaHourlyRain([
|
|
{ hour: 1, rn1: null },
|
|
{ hour: 2, rn1: null },
|
|
]);
|
|
assert.equal(mm, 0);
|
|
assert.deepEqual(missing, [1, 2]);
|
|
});
|
|
|
|
test('빈 입력은 0mm', () => {
|
|
assert.deepEqual(sumKmaHourlyRain([]), { mm: 0, missing: [] });
|
|
});
|
|
});
|