Files
homeclaw/tests/aws-minute.test.ts
T
kimandClaude Opus 5 6836b12210 feat: 오늘 누적 강수량을 AWS 매분자료 실측으로 전환 (23회 → 1회 호출)
AWS 매분자료에 RN-DAY(일 누적 강수량)가 그대로 들어 있다. 초단기실황을
시간별로 23번 불러 합산하던 걸 1회 조회로 대체한다. 격자 보간이 아니라
관측소 실측이고, 분 단위로 갱신된다.

지점은 ASOS(97개)가 아니라 AWS(745개) 중 최근접으로 따로 고른다.
ASOS에 묶어두면 시흥이 19.1km 떨어진 인천을 읽는데, AWS면 2.5km 시흥
관측소가 잡힌다(용인 17.3km → 처인역삼 1.2km). 실패하면 종전 초단기실황
합산으로 물러나므로 어디서든 값은 나온다.

디버깅에 시간을 쓴 함정 셋을 테스트로 고정했다:

  1) 컬럼이 위치로만 구분된다. RN-12H(12)를 RN-DAY(13)로 착각해서
     "AWS 9.8 vs 초단기실황 10.2"라는 가짜 불일치까지 만들어냈다.
     바로잡으니 서울·강릉은 소수점까지 일치한다.
  2) 마지막 행은 "지금 이 분"이라 아직 안 채워진 경우가 많다 — 전 컬럼
     -99.9. 맨 끝 행을 그대로 쓰면 관측이 멀쩡한데 전부 null이 되어
     폴백으로 샌다. 뒤에서부터 RN-DAY가 유효한 첫 행을 쓴다.
  3) 정시 RN-60m만 더하면 마지막 정시 이후 자투리가 빠진다. 부산에서
     27분 사이 4mm가 누락돼 "오늘 23.6 > 사상 19.6" 모순이 화면에 떴다.
     차액을 현재 시각 한 칸으로 넣어 시간 합이 RN-DAY와 맞게 만들었다.

RE(강수감지)는 전 지점 -99.9(null)라 사상 판정에 못 쓰고, 매분 RN-60m의
정시 값을 그 시간의 강수량으로 쓴다.

알려진 미해결: geocodeCity('독도')가 전남을 돌려줘서 엉뚱한 관측소가
잡힌다(AWS 목록엔 96번 독도가 실재한다). 이번 변경과 무관한 기존
지오코더 문제로, 구미 때 KR_CITY_COORDS를 만든 것과 같은 종류다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 13:32:52 +09:00

99 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* aws-minute.test.ts
*
* 방재기상관측(AWS) 매분자료 파싱. 오늘 누적 강수량을 초단기실황 23회 합산에서
* AWS `RN-DAY` 1회 조회로 바꾸면서 들어왔다(2026-08-16).
*
* 실제로 밟은 지뢰 세 개를 그대로 테스트로 남긴다:
*
* 1. 컬럼이 위치로만 구분된다. 헤더에 이름은 있지만 CSV엔 안 붙어서 한 칸 밀리면
* 기온을 강수량으로 읽는다 — RN-12H(12)를 RN-DAY(13)로 착각해 한 번 틀렸고,
* 그때 "AWS 9.8 vs 우리 10.2"라는 가짜 불일치까지 만들어냈다.
* 2. 마지막 행은 "지금 이 분"이라 아직 안 채워진 경우가 많다 — 전 컬럼 -99.9.
* 맨 끝 행을 그대로 쓰면 관측이 멀쩡한데 전부 null이 되어 폴백으로 샌다.
* 3. 정시 RN-60m만 더하면 마지막 정시 이후 자투리가 빠져서, "오늘 누적"이
* "이번 강수 누적"보다 커지는 모순이 화면에 뜬다(부산에서 27분에 4mm).
*/
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import { awsNum, parseAwsDay } from '../src/tools/weather';
// TM,STN,WD1,WS1,WDS,WSS,WD10,WS10,TA,RE,RN-15m,RN-60m,RN-12H,RN-DAY,HM,PA,PS,TD
const row = (tm: string, ta: string, rn60: string, rnDay: string) =>
`${tm},108,48.4,1.5,24.6,2.0,46.0,1.5,${ta},-99.9,0.0,${rn60},9.9,${rnDay},84.7,996.0,1005.7,22.6,=`;
const DEAD = (tm: string) => `${tm},108,` + Array(16).fill('-99.9').join(',') + ',=';
describe('awsNum — 결측 센티널', () => {
test('-50 이하는 관측 없음이라 null', () => {
// 0으로 읽으면 비가 안 온 것처럼 보이고, 더하면 합계가 음수로 망가진다.
assert.equal(awsNum('-99.9'), null);
assert.equal(awsNum('-50.1'), null);
});
test('정상값은 숫자로', () => {
assert.equal(awsNum('10.2'), 10.2);
assert.equal(awsNum('0.0'), 0);
assert.equal(awsNum(' 24.1 '), 24.1);
});
test('영하 기온은 살아남는다 (-50 초과)', () => {
assert.equal(awsNum('-15.3'), -15.3);
});
test('빈 값/undefined는 null', () => {
assert.equal(awsNum(undefined), null);
assert.equal(awsNum(''), null);
});
});
describe('parseAwsDay', () => {
test('RN-DAY를 13번 컬럼에서 읽는다 (12번은 RN-12H)', async () => {
const p = await parseAwsDay([row('202608161300', '24.3', '0.4', '10.2')].join('\n'));
assert.equal(p.rnDay, 10.2);
assert.equal(p.ta, 24.3);
});
test('마지막 행이 전부 결측이면 그 앞의 유효 행을 쓴다', async () => {
const csv = [
row('202608161325', '24.1', '0.2', '10.2'),
DEAD('202608161326'),
DEAD('202608161327'),
].join('\n');
const p = await parseAwsDay(csv);
assert.equal(p.rnDay, 10.2);
assert.equal(p.tm, '202608161325');
});
test('정시 행만 시간별 값으로 뽑는다', async () => {
const csv = [
row('202608160100', '24', '1.0', '1.0'),
row('202608160130', '24', '0.5', '1.5'), // 정시 아님 — 버려야 한다
row('202608160200', '24', '2.0', '3.0'),
].join('\n');
const p = await parseAwsDay(csv);
assert.deepEqual(p.hours.map(h => h.time), ['2026-08-16T01:00', '2026-08-16T02:00']);
});
test('자투리 구간을 채워 시간 합이 RN-DAY와 맞는다', async () => {
// 정시 합 3.0인데 RN-DAY 7.0 → 마지막 정시 이후 4.0이 더 왔다는 뜻.
const csv = [
row('202608161300', '22', '1.0', '3.0'),
row('202608161327', '22', '0.0', '7.0'),
].join('\n');
const p = await parseAwsDay(csv);
const summed = Math.round(p.hours.reduce((s, h) => s + h.mm, 0) * 10) / 10;
assert.equal(summed, p.rnDay);
assert.equal(p.hours[p.hours.length - 1].mm, 6); // 7.0 − 1.0(13시 정시)
assert.equal(p.hours[p.hours.length - 1].wet, true);
});
test('자투리가 없으면 덧붙이지 않는다', async () => {
const csv = [row('202608161300', '22', '2.5', '2.5')].join('\n');
const p = await parseAwsDay(csv);
assert.equal(p.hours.length, 1);
});
test('빈 응답은 전부 null', async () => {
const p = await parseAwsDay('#START7777\n#7777END\n');
assert.deepEqual({ rnDay: p.rnDay, tm: p.tm, hours: p.hours }, { rnDay: null, tm: null, hours: [] });
});
});