@@ -11,15 +11,16 @@ public class AnomalyDetector {
1111 // 지구 반지름 (km)
1212 private static final double EARTH_RADIUS_KM = 6371.0 ;
1313
14+ // GPS 오차 범위 상수화
15+ private static final double GPS_ERROR_MARGIN_KM = 0.1 ; // 100m: 일반 GPS 오차
16+ private static final double GPS_RECONNECTION_ERROR_KM = 0.3 ; // 300m: GPS 재연결 오차
17+ private static final double SHORT_MOVEMENT_MARGIN_KM = 0.3 ; // 300m: 짧은 시간 이동 허용
18+
1419 // 3단계 속도 임계값
1520 private static final double EXTREME_SPEED_KMH = 1000.0 ; // 즉시 차단
1621 private static final double HIGH_SPEED_KMH = 300.0 ; // 강한 의심
1722 private static final double SUSPICIOUS_SPEED_KMH = 150.0 ; // 누적 의심
1823
19- // 시간 간격 기준 (초)
20- private static final long SHORT_INTERVAL_SEC = 300 ; // 5분
21- private static final long MEDIUM_INTERVAL_SEC = 3600 ; // 1시간
22-
2324 // 시간 동기화 허용 오차 (분)
2425 private static final int TIME_SYNC_TOLERANCE_MINUTES = 10 ;
2526
@@ -49,11 +50,7 @@ public static double calculateDistance(double lat1, double lng1, double lat2, do
4950 */
5051 public static double calculateSpeed (double distance , long timeDiffSeconds ) {
5152 if (timeDiffSeconds <= 0 ) {
52- if (distance >= 0.001 ) { // 1m 이상 이동
53- return Double .MAX_VALUE ;
54- }
55-
56- return 0.0 ;
53+ return -1.0 ;
5754 }
5855
5956 double hours = timeDiffSeconds / 3600.0 ;
@@ -68,14 +65,23 @@ public static double calculateSpeed(double distance, long timeDiffSeconds) {
6865 public static int classifyMovementAnomaly (
6966 double previousLat , double previousLng ,
7067 double currentLat , double currentLng ,
71- LocalDateTime previousTime , LocalDateTime currentTime ) {
68+ LocalDateTime previousTime , LocalDateTime currentTime ,
69+ int duplicateRequestThresholdSeconds ) {
7270 long timeDiffSeconds = Duration .between (previousTime , currentTime ).getSeconds ();
7371 double distance = calculateDistance (previousLat , previousLng , currentLat , currentLng );
7472
75- // 시간 역행 (클라이언트 조작)
73+ log .debug ("이동 분석 - 시간차: {}초, 거리: {}km, 이전시각: {}, 현재시각: {}" ,
74+ timeDiffSeconds , distance , previousTime , currentTime );
75+
76+ // === 1단계: 중복 요청 필터링 ===
77+ if (timeDiffSeconds >= 0 && timeDiffSeconds < duplicateRequestThresholdSeconds ) {
78+ log .debug ("중복 요청 감지 ({}초 이내): GPS 오차 무시" , timeDiffSeconds );
79+ return 0 ;
80+ }
81+
82+ // === 2단계: 시간 역행 검증 ===
7683 if (timeDiffSeconds < 0 ) {
77- // GPS 오차 범위(100m) 내는 허용
78- if (distance < 0.1 ) {
84+ if (distance < GPS_ERROR_MARGIN_KM ) {
7985 log .debug ("시간 역행이지만 GPS 오차 범위 내: {}초, {}km" , timeDiffSeconds , distance );
8086 return 0 ;
8187 }
@@ -84,10 +90,10 @@ public static int classifyMovementAnomaly(
8490 return 3 ;
8591 }
8692
87- // 정확히 동일 시간 (0초)
93+ // === 3단계: 동일 시간 검증 ===
8894 if (timeDiffSeconds == 0 ) {
89- // 200m 미만은 GPS 재연결 오차로 간주
90- if (distance < 0.2 ) {
95+ // 300m 미만은 GPS 재연결 오차로 간주
96+ if (distance < GPS_RECONNECTION_ERROR_KM ) {
9197 log .debug ("동일 시간이지만 GPS 재연결 오차 범위 내: {}km" , distance );
9298 return 0 ;
9399 }
@@ -96,41 +102,34 @@ public static int classifyMovementAnomaly(
96102 return 3 ;
97103 }
98104
99- // 일반 GPS 오차(5-10m) + 실내 오차(~50m) 고려
100- if (distance < 0.1 ) { // 100m
105+ // === 4단계: GPS 오차 범위 검증 ===
106+ if (distance < GPS_ERROR_MARGIN_KM ) { // 100m
101107 log .debug ("이동 거리 {}km는 GPS 오차 범위 내로 판단" , distance );
102108 return 0 ;
103109 }
104110
105- // 짧은 시간 + 짧은 거리는 추가로 관대하게
106- if (timeDiffSeconds < 60 && distance < 0.2 ) { // 1분 & 200m
111+ // === 5단계: 짧은 시간 + 짧은 거리 추가 관대 처리 ===
112+ if (timeDiffSeconds < 60 && distance < SHORT_MOVEMENT_MARGIN_KM ) { // 1분 & 300m
107113 log .debug ("짧은 시간 이동({}초, {}km)은 정상으로 판단" , timeDiffSeconds , distance );
108114 return 0 ;
109115 }
110116
117+ // === 6단계: 속도 계산 및 분류 ===
111118 double speed = calculateSpeed (distance , timeDiffSeconds );
112119
113- // 시간 간격에 따라 임계값 조정
114- double adjustedExtremeThreshold = EXTREME_SPEED_KMH ;
115- double adjustedHighThreshold = HIGH_SPEED_KMH ;
116- double adjustedSuspiciousThreshold = SUSPICIOUS_SPEED_KMH ;
117-
118- if (timeDiffSeconds < 300 ) { // 5분 미만
119- adjustedSuspiciousThreshold = 80.0 ;
120- } else if (timeDiffSeconds < 600 ) { // 10분 미만
121- adjustedSuspiciousThreshold = 100.0 ;
122- } else if (timeDiffSeconds < 1800 ) { // 30분 미만
123- adjustedSuspiciousThreshold = 120.0 ;
124- } else if (timeDiffSeconds < 3600 ) { // 1시간 미만
125- adjustedSuspiciousThreshold = 140.0 ;
126- } else { // 1시간 이상
127- adjustedSuspiciousThreshold = 150.0 ;
120+ if (speed < 0 ) {
121+ log .warn ("속도 계산 불가: 시간차={}초, 거리={}km" , timeDiffSeconds , distance );
122+ return 0 ;
128123 }
129124
130- if (speed >= adjustedExtremeThreshold ) {
125+ double adjustedSuspiciousThreshold = calculateAdjustedThreshold (timeDiffSeconds );
126+
127+ log .debug ("계산된 속도: {}km/h (조정된 의심 임계값: {}km/h)" , speed , adjustedSuspiciousThreshold );
128+
129+ if (speed >= EXTREME_SPEED_KMH ) {
131130 log .warn ("즉시 차단급 이동 감지: {}km/h" , speed );
132131 return 3 ;
133- } else if (speed >= adjustedHighThreshold ) {
132+ } else if (speed >= HIGH_SPEED_KMH ) {
134133 log .warn ("강한 의심 이동 감지: {}km/h" , speed );
135134 return 2 ;
136135 } else if (speed >= adjustedSuspiciousThreshold ) {
@@ -141,6 +140,21 @@ public static int classifyMovementAnomaly(
141140 return 0 ;
142141 }
143142
143+ // 시간 간격에 따른 임계값 계산
144+ private static double calculateAdjustedThreshold (long timeDiffSeconds ) {
145+ if (timeDiffSeconds < 300 ) { // 5분 미만
146+ return 80.0 ;
147+ } else if (timeDiffSeconds < 600 ) { // 10분 미만
148+ return 100.0 ;
149+ } else if (timeDiffSeconds < 1800 ) { // 30분 미만
150+ return 120.0 ;
151+ } else if (timeDiffSeconds < 3600 ) { // 1시간 미만
152+ return 140.0 ;
153+ } else { // 1시간 이상
154+ return 150.0 ;
155+ }
156+ }
157+
144158 /**
145159 * 서버 시간과 클라이언트 시간의 차이가 허용 범위 내인지 검증
146160 * 클라이언트가 시간을 조작했을 가능성을 감지
@@ -153,7 +167,12 @@ public static boolean isTimeManipulation(LocalDateTime serverTime, LocalDateTime
153167
154168 long diffMinutes = Math .abs (Duration .between (serverTime , clientTime ).toMinutes ());
155169
156- return diffMinutes > TIME_SYNC_TOLERANCE_MINUTES ;
170+ if (diffMinutes > TIME_SYNC_TOLERANCE_MINUTES ) {
171+ log .warn ("시간 조작 의심: 서버-클라이언트 시간차 = {}분" , diffMinutes );
172+ return true ;
173+ }
174+
175+ return false ;
157176 }
158177
159178 /**
@@ -165,8 +184,13 @@ public static boolean isValidCoordinate(Double latitude, Double longitude) {
165184 return false ;
166185 }
167186
168- // 위도: -90 ~ 90, 경도: -180 ~ 180
169- return latitude >= -90.0 && latitude <= 90.0
187+ boolean isValid = latitude >= -90.0 && latitude <= 90.0
170188 && longitude >= -180.0 && longitude <= 180.0 ;
189+
190+ if (!isValid ) {
191+ log .warn ("유효하지 않은 좌표: lat={}, lng={}" , latitude , longitude );
192+ }
193+
194+ return isValid ;
171195 }
172196}
0 commit comments