diff --git a/data/migration_v2_postgis_geography.sql b/data/migration_v2_postgis_geography.sql index 806090c..be6d2ee 100644 --- a/data/migration_v2_postgis_geography.sql +++ b/data/migration_v2_postgis_geography.sql @@ -18,13 +18,14 @@ WHERE location IS NULL AND longitude IS NOT NULL; -- 4. GIST 공간 인덱스 생성 +-- findNearbyWithGist()의 ST_DWithin + KNN 정렬용. ddl-auto는 geometry/GIST를 못 만들므로 +-- 이 마이그레이션이 유일한 생성 경로. CREATE INDEX IF NOT EXISTS idx_stores_location_gist ON stores USING GIST (location); --- 5. nearby 바운딩박스 선필터용 복합 B-tree 인덱스 -CREATE INDEX IF NOT EXISTS idx_stores_lat_lng - ON stores (latitude, longitude) - WHERE is_deleted = false; +-- NOTE: (latitude, longitude) B-tree 인덱스는 Store 엔티티의 @Index(idx_store_lat_lng) 로 이미 +-- ddl-auto 가 생성/유지하므로 여기서 별도 추가하지 않는다. 과거 버전에서 idx_stores_lat_lng +-- 라는 다른 이름으로 중복 생성되던 문제를 방지. -- 검증 -- SELECT pg_typeof(location) FROM stores LIMIT 1; -- geography 이어야 정상 diff --git a/grafana/provisioning/dashboards/catcheat-loadtest-dashboard.json b/grafana/provisioning/dashboards/catcheat-loadtest-dashboard.json index b6a4926..e77c6fd 100644 --- a/grafana/provisioning/dashboards/catcheat-loadtest-dashboard.json +++ b/grafana/provisioning/dashboards/catcheat-loadtest-dashboard.json @@ -2164,6 +2164,40 @@ "legendFormat": "SMEMBERS p95 (k6)" } ] + }, + { + "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "drawStyle": "line", + "fillOpacity": 10, + "lineWidth": 2, + "showPoints": "never", + "lineInterpolation": "smooth" + }, + "unit": "ops", + "min": 0 + } + }, + "gridPos": { "h": 8, "w": 24, "x": 0, "y": 200 }, + "id": 999, + "options": { + "legend": { "calcs": ["last", "max", "mean"], "displayMode": "table", "placement": "bottom" }, + "tooltip": { "mode": "multi", "sort": "desc" } + }, + "title": "D.5 Kafka 메시지 처리량 (Topic별)", + "description": "Consumer가 처리한 메시지 속도. 빈자리 시연 시 notification.vacancy.opened 라인 잠깐 솟구침.", + "type": "timeseries", + "targets": [ + { + "expr": "sum by (topic) (rate(kafka_consumer_fetch_manager_records_consumed_total[1m]))", + "legendFormat": "{{topic}}", + "refId": "A", + "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" } + } + ] } ], "refresh": "5s", diff --git a/src/main/java/com/catchtable/store/service/StoreService.java b/src/main/java/com/catchtable/store/service/StoreService.java index e761b2b..30a82ae 100644 --- a/src/main/java/com/catchtable/store/service/StoreService.java +++ b/src/main/java/com/catchtable/store/service/StoreService.java @@ -111,17 +111,12 @@ public String getNearbyPopularStoresForAi(ToolContext toolContext) { .collect(Collectors.joining("\n")); } - // 내 주변 매장 (바운딩박스 선필터 + 거리 정렬) + // 내 주변 매장 (PostGIS GIST 인덱스 + ST_DWithin + KNN 거리 정렬) @Transactional(readOnly = true) public List getNearbyStores(double latitude, double longitude, int page, int size) { int limitedSize = Math.min(size, 100); - // 반경 5km를 위경도 델타로 변환 (1도 ≈ 111km, 한국 위도 기준 경도 1도 ≈ 88km) - double deltaLat = 5000.0 / 111_000.0; - double deltaLng = 5000.0 / 88_000.0; - return storeRepository.findNearby( - latitude, longitude, - latitude - deltaLat, latitude + deltaLat, - longitude - deltaLng, longitude + deltaLng, + return storeRepository.findNearbyWithGist( + latitude, longitude, 5000.0, PageRequest.of(page, limitedSize) ).stream().map(StoreListResponse::from).toList(); } @@ -135,10 +130,11 @@ public List getNearbyStores(double latitude, double longitude public List getStoresInBounds( double minLat, double maxLat, double minLng, double maxLng, Double centerLat, Double centerLng, int limit) { + int limitedLimit = Math.min(limit, 100); double cLat = centerLat != null ? centerLat : (minLat + maxLat) / 2.0; double cLng = centerLng != null ? centerLng : (minLng + maxLng) / 2.0; return storeRepository - .findInBounds(minLat, maxLat, minLng, maxLng, cLat, cLng, PageRequest.of(0, limit)) + .findInBounds(minLat, maxLat, minLng, maxLng, cLat, cLng, PageRequest.of(0, limitedLimit)) .stream() .map(StoreListResponse::from) .toList();