From 5e035f0362053d53961904f37aa169ca20f0001f Mon Sep 17 00:00:00 2001 From: htlee Date: Mon, 2 Mar 2026 13:42:36 +0900 Subject: [PATCH 1/2] =?UTF-8?q?refactor:=20SignalKindCode=20=EB=A7=A4?= =?UTF-8?q?=ED=95=91=20=EA=B7=9C=EC=B9=99=20=EA=B0=9C=EC=84=A0=20=E2=80=94?= =?UTF-8?q?=20shipName=20BUOY=20=EA=B2=80=EC=B6=9C=20+=20=EC=B9=98?= =?UTF-8?q?=ED=99=98=201=ED=9A=8C=ED=99=94=20+=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SignalKindCode 매핑 변경: aton→DEFAULT, tug/tender→DEFAULT, Vessel+towing/dredging/diving→DEFAULT, Vessel+leisure→DEFAULT - shipName 기반 BUOY 검출: '.' '_' 문자 2개 이상 → BUOY - 캐시 저장 시 1회 치환, API 응답 시 DB/캐시 값 직접 사용 - 응답 경로 6곳 resolve() 재계산 제거 Co-Authored-By: Claude Opus 4.6 --- .../batch/reader/ChnPrmShipCacheWarmer.java | 2 +- .../batch/writer/AisTargetCacheWriter.java | 5 +- .../domain/gis/service/GisService.java | 7 +- .../vessel/service/VesselPositionService.java | 7 +- .../global/util/SignalKindCode.java | 69 ++++++++++----- .../util/VesselTrackToCompactConverter.java | 8 +- .../service/ChunkedTrackStreamingService.java | 49 ++++++----- .../service/DailyTrackCacheManager.java | 9 +- .../global/util/SignalKindCodeTest.java | 39 ++++++--- .../signal_batch/util/SignalKindCodeTest.java | 84 +++++++++++++++++-- 10 files changed, 204 insertions(+), 75 deletions(-) diff --git a/src/main/java/gc/mda/signal_batch/batch/reader/ChnPrmShipCacheWarmer.java b/src/main/java/gc/mda/signal_batch/batch/reader/ChnPrmShipCacheWarmer.java index 50acbb7..a0dd218 100644 --- a/src/main/java/gc/mda/signal_batch/batch/reader/ChnPrmShipCacheWarmer.java +++ b/src/main/java/gc/mda/signal_batch/batch/reader/ChnPrmShipCacheWarmer.java @@ -107,7 +107,7 @@ public class ChnPrmShipCacheWarmer implements ApplicationRunner { entities.forEach(entity -> { if (entity.getSignalKindCode() == null) { SignalKindCode kindCode = SignalKindCode.resolve( - entity.getVesselType(), entity.getExtraInfo()); + entity.getVesselType(), entity.getExtraInfo(), entity.getName()); entity.setSignalKindCode(kindCode.getCode()); } }); diff --git a/src/main/java/gc/mda/signal_batch/batch/writer/AisTargetCacheWriter.java b/src/main/java/gc/mda/signal_batch/batch/writer/AisTargetCacheWriter.java index 78fe92e..4fac46d 100644 --- a/src/main/java/gc/mda/signal_batch/batch/writer/AisTargetCacheWriter.java +++ b/src/main/java/gc/mda/signal_batch/batch/writer/AisTargetCacheWriter.java @@ -35,9 +35,10 @@ public class AisTargetCacheWriter implements ItemWriter { List items = chunk.getItems(); log.debug("AIS Target 캐시 업데이트 시작: {} 건", items.size()); - // 1. SignalKindCode 치환 + // 1. SignalKindCode 치환 (vesselType + extraInfo + shipName 기반, 캐시 저장 시 1회만) items.forEach(item -> { - SignalKindCode kindCode = SignalKindCode.resolve(item.getVesselType(), item.getExtraInfo()); + SignalKindCode kindCode = SignalKindCode.resolve( + item.getVesselType(), item.getExtraInfo(), item.getName()); item.setSignalKindCode(kindCode.getCode()); }); diff --git a/src/main/java/gc/mda/signal_batch/domain/gis/service/GisService.java b/src/main/java/gc/mda/signal_batch/domain/gis/service/GisService.java index ed7c180..e9b1545 100644 --- a/src/main/java/gc/mda/signal_batch/domain/gis/service/GisService.java +++ b/src/main/java/gc/mda/signal_batch/domain/gis/service/GisService.java @@ -5,7 +5,6 @@ import gc.mda.signal_batch.domain.vessel.dto.TrackResponse; import gc.mda.signal_batch.domain.vessel.dto.VesselStatsResponse; import gc.mda.signal_batch.domain.vessel.dto.VesselTracksRequest; import gc.mda.signal_batch.domain.vessel.dto.CompactVesselTrack; -import gc.mda.signal_batch.global.util.SignalKindCode; import gc.mda.signal_batch.global.util.TrackSimplificationUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; @@ -604,9 +603,11 @@ public class GisService { Map vesselInfo = getVesselInfo(mmsi); String shipName = vesselInfo.get("ship_name"); String shipType = vesselInfo.get("ship_type"); + String signalKindCode = vesselInfo.get("signal_kind_code"); String nationalCode = (mmsi != null && mmsi.length() >= 3) ? mmsi.substring(0, 3) : null; - String shipKindCode = SignalKindCode.resolve(shipType, null).getCode(); + String shipKindCode = (signalKindCode != null && !signalKindCode.isEmpty()) + ? signalKindCode : "000027"; return CompactVesselTrack.builder() .vesselId(mmsi) @@ -628,7 +629,7 @@ public class GisService { JdbcTemplate jdbcTemplate = new JdbcTemplate(queryDataSource); try { String sql = """ - SELECT ship_nm as ship_name, vessel_type as ship_type + SELECT ship_nm as ship_name, vessel_type as ship_type, signal_kind_code FROM signal.t_ais_position WHERE mmsi = ? LIMIT 1 diff --git a/src/main/java/gc/mda/signal_batch/domain/vessel/service/VesselPositionService.java b/src/main/java/gc/mda/signal_batch/domain/vessel/service/VesselPositionService.java index f378374..80d1e3f 100644 --- a/src/main/java/gc/mda/signal_batch/domain/vessel/service/VesselPositionService.java +++ b/src/main/java/gc/mda/signal_batch/domain/vessel/service/VesselPositionService.java @@ -3,7 +3,6 @@ package gc.mda.signal_batch.domain.vessel.service; import gc.mda.signal_batch.domain.ship.service.ShipImageService; import gc.mda.signal_batch.domain.ship.service.ShipImageService.ShipImageSummary; import gc.mda.signal_batch.domain.vessel.dto.RecentVesselPositionDto; -import gc.mda.signal_batch.global.util.SignalKindCode; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -124,6 +123,7 @@ public class VesselPositionService { cog, name as ship_nm, vessel_type as ship_ty, + signal_kind_code, last_update FROM signal.t_ais_position WHERE last_update >= NOW() - INTERVAL '%d minutes' @@ -145,8 +145,9 @@ public class VesselPositionService { String mmsi = rs.getString("mmsi"); String shipTy = rs.getString("ship_ty"); - // shipKindCode 계산 (vesselType 기반, extraInfo 없음) - String shipKindCode = SignalKindCode.resolve(shipTy, null).getCode(); + // shipKindCode: DB에 저장된 치환값 사용 + String signalKindCode = rs.getString("signal_kind_code"); + String shipKindCode = signalKindCode != null ? signalKindCode : "000027"; // nationalCode 계산 (MMSI 앞 3자리 = MID) String nationalCode = mmsi != null && mmsi.length() >= 3 diff --git a/src/main/java/gc/mda/signal_batch/global/util/SignalKindCode.java b/src/main/java/gc/mda/signal_batch/global/util/SignalKindCode.java index cb4b6d6..fbc7998 100644 --- a/src/main/java/gc/mda/signal_batch/global/util/SignalKindCode.java +++ b/src/main/java/gc/mda/signal_batch/global/util/SignalKindCode.java @@ -6,10 +6,11 @@ import lombok.RequiredArgsConstructor; /** * MDA 선종 범례코드 * - * S&P Global AIS API의 vesselType + extraInfo를 기반으로 + * S&P Global AIS API의 vesselType + extraInfo + shipName을 기반으로 * MDA 범례코드(signalKindCode)로 치환한다. * - * ShipKindCodeConverter를 대체하며, SNP-Batch-1의 치환 로직을 이식. + * 치환은 캐시 저장 시(AisTargetCacheWriter) 1회만 수행하며, + * API 응답 시에는 캐시 또는 DB의 signal_kind_code를 직접 사용한다. */ @Getter @RequiredArgsConstructor @@ -28,18 +29,32 @@ public enum SignalKindCode { private final String koreanName; /** - * vesselType + extraInfo → MDA 범례코드 치환 - * - * 치환 우선순위: - * 1. vesselType 단독 매칭 (Cargo, Tanker, Passenger, AtoN 등) - * 2. vesselType + extraInfo 조합 매칭 (Vessel + Fishing 등) - * 3. fallback → DEFAULT (000027) + * vesselType + extraInfo → MDA 범례코드 치환 (하위 호환용) + * shipName 기반 BUOY 검출 불가 — 캐시 저장 시에는 3-파라미터 버전 사용 권장. */ public static SignalKindCode resolve(String vesselType, String extraInfo) { + return resolve(vesselType, extraInfo, null); + } + + /** + * vesselType + extraInfo + shipName → MDA 범례코드 치환 + * + * 치환 우선순위: + * 1. shipName 기반 BUOY 검출 ('.' '_' 문자가 2개 이상 → 부이/항로표지) + * 2. vesselType 단독 매칭 (Cargo, Tanker, Passenger 등) + * 3. vesselType + extraInfo 조합 매칭 (Vessel + Fishing 등) + * 4. fallback → DEFAULT (000027) + */ + public static SignalKindCode resolve(String vesselType, String extraInfo, String shipName) { + // 1. shipName 기반 BUOY 검출: '.' 또는 '_' 문자가 2개 이상 + if (hasBuoyNamePattern(shipName)) { + return BUOY; + } + String vt = normalizeOrEmpty(vesselType); String ei = normalizeOrEmpty(extraInfo); - // 1. vesselType 단독 매칭 + // 2. vesselType 단독 매칭 switch (vt) { case "cargo": return CARGO; @@ -48,7 +63,7 @@ public enum SignalKindCode { case "passenger": return FERRY; case "aton": - return BUOY; + return DEFAULT; case "law enforcement": return GOV; case "search and rescue": @@ -60,19 +75,19 @@ public enum SignalKindCode { } // vesselType 그룹 매칭 - if (matchesAny(vt, "tug", "pilot boat", "tender", "anti pollution", "medical transport")) { + if (matchesAny(vt, "pilot boat", "anti pollution", "medical transport")) { return GOV; } if (matchesAny(vt, "high speed craft", "wing in ground-effect")) { return FERRY; } - // 2. "Vessel" + extraInfo 조합 + // 3. "Vessel" + extraInfo 조합 if ("vessel".equals(vt)) { return resolveVesselExtraInfo(ei); } - // 3. "N/A" + extraInfo 조합 + // 4. "N/A" + extraInfo 조합 if ("n/a".equals(vt)) { if (ei.startsWith("hazardous cat")) { return CARGO; @@ -80,7 +95,7 @@ public enum SignalKindCode { return DEFAULT; } - // 4. fallback + // 5. fallback return DEFAULT; } @@ -91,18 +106,32 @@ public enum SignalKindCode { if ("military operations".equals(extraInfo)) { return GOV; } - if (matchesAny(extraInfo, "towing", "towing (large)", "dredging/underwater ops", "diving operations")) { - return GOV; - } - if (matchesAny(extraInfo, "pleasure craft", "sailing", "n/a")) { - return FISHING; - } if (extraInfo.startsWith("hazardous cat")) { return CARGO; } return DEFAULT; } + /** + * shipName에 '.' 또는 '_' 문자가 2개 이상 포함되면 부이/항로표지로 판정 + */ + static boolean hasBuoyNamePattern(String shipName) { + if (shipName == null || shipName.isBlank()) { + return false; + } + int count = 0; + for (int i = 0; i < shipName.length(); i++) { + char c = shipName.charAt(i); + if (c == '.' || c == '_') { + count++; + if (count >= 2) { + return true; + } + } + } + return false; + } + private static boolean matchesAny(String value, String... candidates) { for (String candidate : candidates) { if (candidate.equals(value)) { diff --git a/src/main/java/gc/mda/signal_batch/global/util/VesselTrackToCompactConverter.java b/src/main/java/gc/mda/signal_batch/global/util/VesselTrackToCompactConverter.java index 79a1abd..b1b75dd 100644 --- a/src/main/java/gc/mda/signal_batch/global/util/VesselTrackToCompactConverter.java +++ b/src/main/java/gc/mda/signal_batch/global/util/VesselTrackToCompactConverter.java @@ -122,16 +122,18 @@ public class VesselTrackToCompactConverter { int pointCount = geometry.size(); double avgSpeed = pointCount > 0 ? totalDistance / Math.max(1, pointCount) * 60 : 0; - // 선박 정보 설정 + // 선박 정보 설정 (캐시에 이미 치환된 signalKindCode 사용) String shipName = null; String shipType = null; String shipKindCode = null; if (vesselInfo != null) { shipName = vesselInfo.getName(); shipType = vesselInfo.getVesselType(); - shipKindCode = SignalKindCode.resolve(vesselInfo.getVesselType(), vesselInfo.getExtraInfo()).getCode(); + shipKindCode = vesselInfo.getSignalKindCode() != null + ? vesselInfo.getSignalKindCode() + : SignalKindCode.DEFAULT.getCode(); } else { - shipKindCode = SignalKindCode.resolve(null, null).getCode(); + shipKindCode = SignalKindCode.DEFAULT.getCode(); } String nationalCode = mmsi.length() >= 3 ? mmsi.substring(0, 3) : mmsi; diff --git a/src/main/java/gc/mda/signal_batch/global/websocket/service/ChunkedTrackStreamingService.java b/src/main/java/gc/mda/signal_batch/global/websocket/service/ChunkedTrackStreamingService.java index b2f8873..1f1660f 100644 --- a/src/main/java/gc/mda/signal_batch/global/websocket/service/ChunkedTrackStreamingService.java +++ b/src/main/java/gc/mda/signal_batch/global/websocket/service/ChunkedTrackStreamingService.java @@ -1,6 +1,5 @@ package gc.mda.signal_batch.global.websocket.service; -import gc.mda.signal_batch.global.util.SignalKindCode; import gc.mda.signal_batch.global.exception.MemoryBudgetExceededException; import gc.mda.signal_batch.global.util.TrackMemoryEstimator; import gc.mda.signal_batch.global.websocket.dto.TrackChunkResponse; @@ -117,11 +116,13 @@ public class ChunkedTrackStreamingService { private static class VesselInfo { String shipName; String shipType; + String signalKindCode; long cacheTime; - VesselInfo(String shipName, String shipType) { + VesselInfo(String shipName, String shipType, String signalKindCode) { this.shipName = shipName != null ? shipName : "-"; this.shipType = shipType != null ? shipType : "-"; + this.signalKindCode = signalKindCode != null ? signalKindCode : "000027"; this.cacheTime = System.currentTimeMillis(); } @@ -229,15 +230,16 @@ public class ChunkedTrackStreamingService { return cached; } - // DB에서 조회 + // DB에서 조회 (signal_kind_code는 캐시 저장 시 치환된 값) try { - String sql = "SELECT ship_nm, vessel_type FROM signal.t_ais_position " + + String sql = "SELECT ship_nm, vessel_type, signal_kind_code FROM signal.t_ais_position " + "WHERE mmsi = ? LIMIT 1"; VesselInfo info = queryJdbcTemplate.queryForObject(sql, (rs, rowNum) -> new VesselInfo( rs.getString("ship_nm"), - rs.getString("vessel_type") + rs.getString("vessel_type"), + rs.getString("signal_kind_code") ), mmsi ); @@ -249,7 +251,7 @@ public class ChunkedTrackStreamingService { } catch (Exception e) { log.debug("No vessel info found for {}, using defaults", mmsi); - VesselInfo defaultInfo = new VesselInfo(null, null); + VesselInfo defaultInfo = new VesselInfo(null, null, null); vesselInfoCache.put(mmsi, defaultInfo); return defaultInfo; } @@ -287,7 +289,7 @@ public class ChunkedTrackStreamingService { // 캐시에 없는 것들은 DB에서 배치 조회 if (!uncachedIds.isEmpty()) { try { - String sql = "SELECT mmsi, ship_nm, vessel_type " + + String sql = "SELECT mmsi, ship_nm, vessel_type, signal_kind_code " + "FROM signal.t_ais_position " + "WHERE mmsi IN (" + String.join(",", Collections.nCopies(uncachedIds.size(), "?")) + ")"; @@ -296,7 +298,8 @@ public class ChunkedTrackStreamingService { String vesselId = rs.getString("mmsi"); VesselInfo info = new VesselInfo( rs.getString("ship_nm"), - rs.getString("vessel_type") + rs.getString("vessel_type"), + rs.getString("signal_kind_code") ); result.put(vesselId, info); vesselInfoCache.put(vesselId, info); @@ -308,7 +311,7 @@ public class ChunkedTrackStreamingService { // 기본값 설정 for (String vesselId : uncachedIds) { if (!result.containsKey(vesselId)) { - VesselInfo defaultInfo = new VesselInfo(null, null); + VesselInfo defaultInfo = new VesselInfo(null, null, null); result.put(vesselId, defaultInfo); vesselInfoCache.put(vesselId, defaultInfo); } @@ -365,7 +368,7 @@ public class ChunkedTrackStreamingService { .map(id -> "?") .collect(Collectors.joining(",")); - String sql = "SELECT mmsi, ship_nm, vessel_type " + + String sql = "SELECT mmsi, ship_nm, vessel_type, signal_kind_code " + "FROM signal.t_ais_position " + "WHERE mmsi IN (" + placeholders + ")"; @@ -375,7 +378,8 @@ public class ChunkedTrackStreamingService { String visselId = rs.getString("mmsi"); VesselInfo info = new VesselInfo( rs.getString("ship_nm"), - rs.getString("vessel_type") + rs.getString("vessel_type"), + rs.getString("signal_kind_code") ); // 세션 캐시와 전역 캐시 모두에 저장 sessionCache.put(visselId, info); @@ -387,7 +391,7 @@ public class ChunkedTrackStreamingService { // DB에 없는 선박은 기본값으로 세션 캐시에 저장 (전역 캐시에는 저장 안함) for (String visselId : batch) { if (!foundIds.contains(visselId)) { - sessionCache.put(visselId, new VesselInfo(null, null)); + sessionCache.put(visselId, new VesselInfo(null, null, null)); } } @@ -395,7 +399,7 @@ public class ChunkedTrackStreamingService { log.warn("Failed to batch load vessel info: {}", e.getMessage()); for (String visselId : batch) { if (!sessionCache.containsKey(visselId)) { - sessionCache.put(visselId, new VesselInfo(null, null)); + sessionCache.put(visselId, new VesselInfo(null, null, null)); } } } @@ -433,7 +437,7 @@ public class ChunkedTrackStreamingService { .map(id -> "?") .collect(Collectors.joining(",")); - String sql = "SELECT mmsi, ship_nm, vessel_type " + + String sql = "SELECT mmsi, ship_nm, vessel_type, signal_kind_code " + "FROM signal.t_ais_position " + "WHERE mmsi IN (" + placeholders + ")"; @@ -443,7 +447,8 @@ public class ChunkedTrackStreamingService { String vesselId = rs.getString("mmsi"); VesselInfo info = new VesselInfo( rs.getString("ship_nm"), - rs.getString("vessel_type") + rs.getString("vessel_type"), + rs.getString("signal_kind_code") ); vesselInfoCache.put(vesselId, info); foundIds.add(vesselId); @@ -452,7 +457,7 @@ public class ChunkedTrackStreamingService { // DB에 없는 선박은 기본값으로 캐시 for (String vesselId : batch) { if (!foundIds.contains(vesselId)) { - vesselInfoCache.put(vesselId, new VesselInfo(null, null)); + vesselInfoCache.put(vesselId, new VesselInfo(null, null, null)); } } @@ -461,7 +466,7 @@ public class ChunkedTrackStreamingService { // 실패 시 기본값으로 캐시 for (String vesselId : batch) { if (!vesselInfoCache.containsKey(vesselId)) { - vesselInfoCache.put(vesselId, new VesselInfo(null, null)); + vesselInfoCache.put(vesselId, new VesselInfo(null, null, null)); } } } @@ -635,7 +640,7 @@ public class ChunkedTrackStreamingService { VesselInfo vesselInfo = getVesselInfo(mmsi); accumulator.shipName = vesselInfo.shipName; accumulator.shipType = vesselInfo.shipType; - accumulator.shipKindCode = SignalKindCode.resolve(vesselInfo.shipType, null).getCode(); + accumulator.shipKindCode = vesselInfo.signalKindCode; vesselMap.put(vesselId, accumulator); } @@ -1101,7 +1106,7 @@ public class ChunkedTrackStreamingService { VesselInfo vesselInfo = getVesselInfo(track.getVesselId()); accumulator.shipName = vesselInfo.shipName; accumulator.shipType = vesselInfo.shipType; - accumulator.shipKindCode = SignalKindCode.resolve(vesselInfo.shipType, null).getCode(); + accumulator.shipKindCode = vesselInfo.signalKindCode; mergedMap.put(vesselId, accumulator); } @@ -1985,7 +1990,7 @@ public class ChunkedTrackStreamingService { acc.mmsi = finalMmsi; acc.shipName = info.shipName; acc.shipType = info.shipType; - acc.shipKindCode = SignalKindCode.resolve(info.shipType, null).getCode(); + acc.shipKindCode = info.signalKindCode; return acc; }); @@ -2742,13 +2747,13 @@ public class ChunkedTrackStreamingService { // 세션 캐시에서 조회 (이미 preload됨) VesselInfo info = sessionVesselCache.get(finalMmsi3); if (info == null) { - info = new VesselInfo(null, null); + info = new VesselInfo(null, null, null); } VesselAccumulator acc = new VesselAccumulator(); acc.mmsi = finalMmsi3; acc.shipName = info.shipName; acc.shipType = info.shipType; - acc.shipKindCode = SignalKindCode.resolve(info.shipType, null).getCode(); + acc.shipKindCode = info.signalKindCode; return acc; }); diff --git a/src/main/java/gc/mda/signal_batch/global/websocket/service/DailyTrackCacheManager.java b/src/main/java/gc/mda/signal_batch/global/websocket/service/DailyTrackCacheManager.java index e8de418..db948a9 100644 --- a/src/main/java/gc/mda/signal_batch/global/websocket/service/DailyTrackCacheManager.java +++ b/src/main/java/gc/mda/signal_batch/global/websocket/service/DailyTrackCacheManager.java @@ -310,8 +310,9 @@ public class DailyTrackCacheManager { double avgSpeed = acc.pointCount > 0 ? acc.totalDistance / Math.max(1, acc.pointCount) * 60 : 0; - // shipKindCode 계산 - String shipKindCode = SignalKindCode.resolve(acc.shipType, null).getCode(); + // shipKindCode: 캐시 저장 시 치환된 값 사용 (DB fallback 포함) + String shipKindCode = acc.signalKindCode != null + ? acc.signalKindCode : SignalKindCode.DEFAULT.getCode(); // nationalCode 계산 (MMSI 앞 3자리 = MID) String nationalCode = acc.mmsi.length() >= 3 ? acc.mmsi.substring(0, 3) : acc.mmsi; @@ -723,7 +724,7 @@ public class DailyTrackCacheManager { try (Connection conn = queryDataSource.getConnection()) { String placeholders = batch.stream().map(id -> "?").collect(Collectors.joining(",")); - String sql = "SELECT mmsi, name as ship_nm, vessel_type as ship_ty " + + String sql = "SELECT mmsi, name as ship_nm, vessel_type as ship_ty, signal_kind_code " + "FROM signal.t_ais_position " + "WHERE mmsi IN (" + placeholders + ")"; @@ -739,6 +740,7 @@ public class DailyTrackCacheManager { if (acc != null) { acc.shipName = rs.getString("ship_nm"); acc.shipType = rs.getString("ship_ty"); + acc.signalKindCode = rs.getString("signal_kind_code"); enriched++; } } @@ -782,6 +784,7 @@ public class DailyTrackCacheManager { String mmsi; String shipName; String shipType; + String signalKindCode; List geometry = new ArrayList<>(500); List timestamps = new ArrayList<>(500); List speeds = new ArrayList<>(500); diff --git a/src/test/java/gc/mda/signal_batch/global/util/SignalKindCodeTest.java b/src/test/java/gc/mda/signal_batch/global/util/SignalKindCodeTest.java index 5fde184..1009e03 100644 --- a/src/test/java/gc/mda/signal_batch/global/util/SignalKindCodeTest.java +++ b/src/test/java/gc/mda/signal_batch/global/util/SignalKindCodeTest.java @@ -12,6 +12,25 @@ import static org.junit.jupiter.api.Assertions.*; class SignalKindCodeTest { + @Nested + @DisplayName("shipName 기반 BUOY 검출") + class ShipNameBuoy { + + @Test + @DisplayName("'.' 또는 '_' 2개 이상 → BUOY (vesselType 무시)") + void resolve_buoyByName() { + assertEquals("000028", SignalKindCode.resolve("Cargo", null, "BUOY_01_23").getCode()); + assertEquals("000028", SignalKindCode.resolve("Tanker", null, "AIS.BUOY.01").getCode()); + } + + @Test + @DisplayName("'.' 또는 '_' 1개 이하 → vesselType 기준") + void resolve_notBuoyByName() { + assertEquals("000023", SignalKindCode.resolve("Cargo", null, "M.V CARGO").getCode()); + assertEquals("000024", SignalKindCode.resolve("Tanker", null, "OIL_TANKER").getCode()); + } + } + @Nested @DisplayName("vesselType 단독 매칭") class VesselTypeDirect { @@ -21,7 +40,7 @@ class SignalKindCodeTest { "Cargo, 000023", "Tanker, 000024", "Passenger, 000022", - "AtoN, 000028", + "AtoN, 000027", "Law Enforcement, 000025", "Search and Rescue, 000021", "Local Vessel, 000020" @@ -38,11 +57,11 @@ class SignalKindCodeTest { @ParameterizedTest @CsvSource({ - "Tug, 000025", "Pilot Boat, 000025", - "Tender, 000025", "Anti Pollution, 000025", "Medical Transport, 000025", + "Tug, 000027", + "Tender, 000027", "High Speed Craft, 000022", "Wing in Ground-effect, 000022" }) @@ -60,13 +79,13 @@ class SignalKindCodeTest { @CsvSource({ "Vessel, Fishing, 000020", "Vessel, Military Operations, 000025", - "Vessel, Towing, 000025", - "Vessel, Towing (Large), 000025", - "Vessel, Dredging/Underwater Ops, 000025", - "Vessel, Diving Operations, 000025", - "Vessel, Pleasure Craft, 000020", - "Vessel, Sailing, 000020", - "Vessel, N/A, 000020", + "Vessel, Towing, 000027", + "Vessel, Towing (Large), 000027", + "Vessel, Dredging/Underwater Ops, 000027", + "Vessel, Diving Operations, 000027", + "Vessel, Pleasure Craft, 000027", + "Vessel, Sailing, 000027", + "Vessel, N/A, 000027", "Vessel, Hazardous Cat A, 000023", "Vessel, Hazardous Cat B, 000023", "Vessel, Unknown, 000027" diff --git a/src/test/java/gc/mda/signal_batch/util/SignalKindCodeTest.java b/src/test/java/gc/mda/signal_batch/util/SignalKindCodeTest.java index a65b1ea..43731bc 100644 --- a/src/test/java/gc/mda/signal_batch/util/SignalKindCodeTest.java +++ b/src/test/java/gc/mda/signal_batch/util/SignalKindCodeTest.java @@ -14,6 +14,34 @@ import static org.assertj.core.api.Assertions.assertThat; @DisplayName("SignalKindCode - MDA 선종 범례코드 치환") class SignalKindCodeTest { + @Nested + @DisplayName("shipName 기반 BUOY 검출 (최우선)") + class ShipNameBuoy { + + @ParameterizedTest(name = "shipName={0} → BUOY") + @ValueSource(strings = {"BUOY_01_23", "AIS.BUOY.01", "LIGHT__HOUSE", "A.B.C"}) + @DisplayName("'.' 또는 '_' 2개 이상 → BUOY") + void resolve_buoyByName(String shipName) { + assertThat(SignalKindCode.resolve("Cargo", null, shipName)) + .isEqualTo(SignalKindCode.BUOY); + } + + @ParameterizedTest(name = "shipName={0} → vesselType 기준") + @ValueSource(strings = {"M.V CARGO", "SHIP_ONE", "NORMAL SHIP", "ABC"}) + @DisplayName("'.' 또는 '_' 1개 이하 → shipName 무시, vesselType 기준") + void resolve_notBuoyByName(String shipName) { + assertThat(SignalKindCode.resolve("Cargo", null, shipName)) + .isEqualTo(SignalKindCode.CARGO); + } + + @Test + @DisplayName("shipName null → vesselType 기준") + void resolve_nullShipName() { + assertThat(SignalKindCode.resolve("Cargo", null, null)) + .isEqualTo(SignalKindCode.CARGO); + } + } + @Nested @DisplayName("vesselType 단독 매칭") class VesselTypeDirect { @@ -23,7 +51,6 @@ class SignalKindCodeTest { "cargo, CARGO", "tanker, TANKER", "passenger, FERRY", - "aton, BUOY", "law enforcement, GOV", "search and rescue, KCGV", "local vessel, FISHING" @@ -33,6 +60,12 @@ class SignalKindCodeTest { SignalKindCode result = SignalKindCode.resolve(vesselType, null); assertThat(result.name()).isEqualTo(expectedName); } + + @Test + @DisplayName("aton → DEFAULT (부이가 아닌 일반 장비)") + void resolve_aton() { + assertThat(SignalKindCode.resolve("aton", null)).isEqualTo(SignalKindCode.DEFAULT); + } } @Nested @@ -40,12 +73,19 @@ class SignalKindCodeTest { class VesselTypeGroup { @ParameterizedTest(name = "vesselType={0} → GOV") - @ValueSource(strings = {"tug", "pilot boat", "tender", "anti pollution", "medical transport"}) + @ValueSource(strings = {"pilot boat", "anti pollution", "medical transport"}) @DisplayName("GOV 그룹 매칭") void resolve_govGroup(String vesselType) { assertThat(SignalKindCode.resolve(vesselType, null)).isEqualTo(SignalKindCode.GOV); } + @ParameterizedTest(name = "vesselType={0} → DEFAULT") + @ValueSource(strings = {"tug", "tender"}) + @DisplayName("tug, tender → DEFAULT") + void resolve_tugTenderDefault(String vesselType) { + assertThat(SignalKindCode.resolve(vesselType, null)).isEqualTo(SignalKindCode.DEFAULT); + } + @ParameterizedTest(name = "vesselType={0} → FERRY") @ValueSource(strings = {"high speed craft", "wing in ground-effect"}) @DisplayName("FERRY 그룹 매칭") @@ -70,18 +110,18 @@ class SignalKindCodeTest { assertThat(SignalKindCode.resolve("Vessel", "Military Operations")).isEqualTo(SignalKindCode.GOV); } - @ParameterizedTest(name = "Vessel + {0} → GOV") + @ParameterizedTest(name = "Vessel + {0} → DEFAULT") @ValueSource(strings = {"towing", "towing (large)", "dredging/underwater ops", "diving operations"}) - @DisplayName("Vessel + 해양작업 → GOV") + @DisplayName("Vessel + 해양작업 → DEFAULT") void resolve_vesselMarineOps(String extraInfo) { - assertThat(SignalKindCode.resolve("Vessel", extraInfo)).isEqualTo(SignalKindCode.GOV); + assertThat(SignalKindCode.resolve("Vessel", extraInfo)).isEqualTo(SignalKindCode.DEFAULT); } - @ParameterizedTest(name = "Vessel + {0} → FISHING") + @ParameterizedTest(name = "Vessel + {0} → DEFAULT") @ValueSource(strings = {"pleasure craft", "sailing", "n/a"}) - @DisplayName("Vessel + 레저/기타 → FISHING") + @DisplayName("Vessel + 레저/기타 → DEFAULT") void resolve_vesselLeisure(String extraInfo) { - assertThat(SignalKindCode.resolve("Vessel", extraInfo)).isEqualTo(SignalKindCode.FISHING); + assertThat(SignalKindCode.resolve("Vessel", extraInfo)).isEqualTo(SignalKindCode.DEFAULT); } @Test @@ -164,4 +204,32 @@ class SignalKindCodeTest { assertThat(SignalKindCode.BUOY.getCode()).isEqualTo("000028"); } } + + @Nested + @DisplayName("shipName BUOY 판정 (resolve 3-param 통합 검증)") + class BuoyNamePattern { + + @ParameterizedTest(name = "{0} → BUOY") + @ValueSource(strings = {"A.B.C", "BUOY_01_02", "._", "A.B_C"}) + @DisplayName("2개 이상 특수문자 → BUOY") + void resolve_buoyPattern(String name) { + // vesselType과 무관하게 BUOY로 치환 + assertThat(SignalKindCode.resolve(null, null, name)).isEqualTo(SignalKindCode.BUOY); + } + + @ParameterizedTest(name = "{0} → not BUOY") + @ValueSource(strings = {"ABC", "A.B", "A_B", "NORMAL"}) + @DisplayName("1개 이하 특수문자 → shipName 무시") + void resolve_notBuoyPattern(String name) { + assertThat(SignalKindCode.resolve(null, null, name)).isEqualTo(SignalKindCode.DEFAULT); + } + + @Test + @DisplayName("null/blank shipName → vesselType 기준") + void resolve_nullBlankName() { + assertThat(SignalKindCode.resolve("Cargo", null, null)).isEqualTo(SignalKindCode.CARGO); + assertThat(SignalKindCode.resolve("Cargo", null, "")).isEqualTo(SignalKindCode.CARGO); + assertThat(SignalKindCode.resolve("Cargo", null, " ")).isEqualTo(SignalKindCode.CARGO); + } + } } From e0fc7607546fce995dd7d8496c38120de1ca61c4 Mon Sep 17 00:00:00 2001 From: htlee Date: Mon, 2 Mar 2026 13:44:23 +0900 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20=EB=A6=B4=EB=A6=AC=EC=A6=88=20?= =?UTF-8?q?=EB=85=B8=ED=8A=B8=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/RELEASE-NOTES.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/RELEASE-NOTES.md b/docs/RELEASE-NOTES.md index d122b02..fb0795e 100644 --- a/docs/RELEASE-NOTES.md +++ b/docs/RELEASE-NOTES.md @@ -4,6 +4,10 @@ ## [Unreleased] +### 변경 +- SignalKindCode 매핑 규칙 개선 — aton/tug/tender→DEFAULT, shipName BUOY 검출 추가 +- 응답 경로 signal_kind_code 치환 1회화 — 캐시 저장 시 치환, 응답 시 DB/캐시 값 직접 사용 + ## [2026-03-02] ### 추가