대시보드에서 "군용기가 안 보이는데?", "그냥 이모지고 방향이 없는데?"라는 지적을 받고 실제로 파고든 기록. 방향(heading)을 파이프라인 끝까지 관통시키고 우크라이나·이란까지 조회 지역을 넘혔는데, 그 과정에서 "왜 그 두 지역은 여전히 거의 안 보이는지"까지 실측으로 확인했다.
ADS-B 원본 응답에는 track(실제 진행방향, 도 단위, 0=북/시계방향)이 이미 들어있었는데, AdsbFiPollingService가 이걸 한 번도 안 읽고 있었다. 대시보드 마커는 그냥 고정된 이모지였고 회전도 없었다.
JsonNode trackNode = ac.path("track");
Double heading = trackNode.isNumber() ? trackNode.asDouble() : null;
이 값을 TargetEvent(→TargetDto→Target 엔티티까지 전부) 새 필드로 추가해서 Kafka부터 WebSocket, REST API까지 관통시켰다. 가짜 드론 시뮬레이터는 heading을 안 채우므로 null이 그대로 남는데, 프론트에서 이 null을 "북쪽을 향한다"고 거짓으로 그리지 않고 그냥 점(dot)으로 표시하게 했다 — 모르는 걸 안다고 하지 않는 게 중요했다.
CSS 삼각형(border-bottom 트릭)은 기본이 "위쪽 = 0도"라서, transform: rotate(${heading}deg)를 그대로 걸면 CSS의 시계방향 회전과 항공 방위각(북 기준 시계방향)이 정확히 일치한다 — 별도 오프셋 계산이 필요 없었다.
스크린샷에서 지적받은 또 다른 문제. 이 값은 실제 항공기 ID가 아니라 트랜스폰더의 콜사인 필드가 깨져서 온 값이었다 — 고도/속도가 매번 다른데도 같은 문자열로 잡히는 걸 보고 확인했다(서로 다른 여러 항공기가 같은 깨진 콜사인을 공유하고 있었다는 뜻).
String targetId = flight.matches("[A-Za-z0-9]+") ? flight : hex;
콜사인이 영숫자로만 이뤄진 유효한 형태가 아니면 ICAO hex 코드로 대체하도록 간단히 검증을 추가했다.
private static final List<Region> REGIONS = List.of(
new Region("KOREA", "대한민국(수도권)", 37.5665, 126.9780, 100),
new Region("UKRAINE", "우크라이나(키이우)", 50.4501, 30.5234, 250),
new Region("IRAN", "이란(테헤란)", 35.6892, 51.3890, 250)
);
한 폴링 주기(20초) 안에서 세 지역을 순차 조회하되, adsb.fi의 "초당 1회" 제한을 지키려고 지역 사이에 1.1초씩 텀을 둔다. 프론트는 세 지역 데이터가 전부 같은 WebSocket 스트림에 섞여 들어오는 걸 받아서, 탭을 누르면 그 지역의 위경도 경계 안에 있는 것만 클라이언트에서 걸러보여준다(백엔드에 지역별 API를 새로 만들 필요가 없었다).
지역을 넘혀 배포했는데도 로그에 계속 "우크라이나 0대", "이란 0대"가 찍혀서, 버그인지 실제 데이터가 없는 건지 API에 직접 물어보았다.
adsb.fi 우크라이나 250nm(최대 반경): 0대
adsb.fi 이란 250nm: 0대
adsb.fi 전세계 군용기 목록: 227대
adsb.lol 우크라이나 250nm: 1대(BTI420, 에어발틱 여객기 — 군용 아님)
adsb.lol 이란 250nm: 0대
adsb.lol 전세계 군용기 목록: 190대
두 서비스(adsb.fi, adsb.lol) 다 거의 또같이 안 잡힌다. aggregator를 잘못 골라서가 아니라 ADS-B라는 기술 자체의 근본적 한계다. 그런데 우크라이나와 이란은 "0대"라는 같은 결과에도 원인이 서로 다르다.
한국은 100nm 반경에서도 60여 대가 꾸준히 잡히는 것과 극명히 대비된다. 결국 ADS-B 기반 관측 가능성은 "그 지역에 얼마나 활발한 민간 항공 교통이 있는가"뿐 아니라 "그 신호를 주울 자원봉사자 커뮤니티가 그 지역에 존재하는가"라는, 기술과 무관한 사회적 변수에도 좌우된다는 뜻이다.