feat(backend): 그룹 폴리곤 API — 목록/상세/히스토리 엔드포인트
- GroupPolygonController: GET /api/vessel-analysis/groups (목록, 상세, 히스토리) - GroupPolygonService: JdbcTemplate + ST_AsGeoJSON + Caffeine 5분 캐시 - GroupPolygonDto: GeoJSON polygon + members JSONB 응답 구조 - CacheConfig: GROUP_POLYGONS 캐시 키 추가
This commit is contained in:
부모
2441e3068a
커밋
b0fafca8c9
@ -21,6 +21,7 @@ public class CacheConfig {
|
||||
public static final String SEISMIC = "seismic";
|
||||
public static final String PRESSURE = "pressure";
|
||||
public static final String VESSEL_ANALYSIS = "vessel-analysis";
|
||||
public static final String GROUP_POLYGONS = "group-polygons";
|
||||
|
||||
@Bean
|
||||
public CacheManager cacheManager() {
|
||||
@ -29,7 +30,8 @@ public class CacheConfig {
|
||||
OSINT_IRAN, OSINT_KOREA,
|
||||
SATELLITES,
|
||||
SEISMIC, PRESSURE,
|
||||
VESSEL_ANALYSIS
|
||||
VESSEL_ANALYSIS,
|
||||
GROUP_POLYGONS
|
||||
);
|
||||
manager.setCaffeine(Caffeine.newBuilder()
|
||||
.expireAfterWrite(2, TimeUnit.DAYS)
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
package gc.mda.kcg.domain.fleet;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/vessel-analysis/groups")
|
||||
@RequiredArgsConstructor
|
||||
public class GroupPolygonController {
|
||||
|
||||
private final GroupPolygonService groupPolygonService;
|
||||
|
||||
/**
|
||||
* 전체 그룹 폴리곤 목록 (최신 스냅샷, 5분 캐시)
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<Map<String, Object>> getGroups() {
|
||||
List<GroupPolygonDto> groups = groupPolygonService.getLatestGroups();
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"count", groups.size(),
|
||||
"items", groups
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 그룹 상세 (멤버, 면적, 폴리곤 생성 근거)
|
||||
*/
|
||||
@GetMapping("/{groupKey}/detail")
|
||||
public ResponseEntity<GroupPolygonDto> getGroupDetail(@PathVariable String groupKey) {
|
||||
GroupPolygonDto detail = groupPolygonService.getGroupDetail(groupKey);
|
||||
if (detail == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(detail);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 그룹 히스토리 (시간별 폴리곤 변화)
|
||||
*/
|
||||
@GetMapping("/{groupKey}/history")
|
||||
public ResponseEntity<List<GroupPolygonDto>> getGroupHistory(
|
||||
@PathVariable String groupKey,
|
||||
@RequestParam(defaultValue = "24") int hours) {
|
||||
List<GroupPolygonDto> history = groupPolygonService.getGroupHistory(groupKey, hours);
|
||||
return ResponseEntity.ok(history);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package gc.mda.kcg.domain.fleet;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class GroupPolygonDto {
|
||||
private String groupType;
|
||||
private String groupKey;
|
||||
private String groupLabel;
|
||||
private String snapshotTime;
|
||||
private Object polygon; // GeoJSON Polygon (parsed from ST_AsGeoJSON)
|
||||
private double centerLat;
|
||||
private double centerLon;
|
||||
private double areaSqNm;
|
||||
private int memberCount;
|
||||
private String zoneId;
|
||||
private String zoneName;
|
||||
private List<Map<String, Object>> members;
|
||||
private String color;
|
||||
}
|
||||
@ -0,0 +1,131 @@
|
||||
package gc.mda.kcg.domain.fleet;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import gc.mda.kcg.config.CacheConfig;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GroupPolygonService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final CacheManager cacheManager;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private static final String LATEST_GROUPS_SQL = """
|
||||
SELECT group_type, group_key, group_label, snapshot_time,
|
||||
ST_AsGeoJSON(polygon) AS polygon_geojson,
|
||||
ST_Y(center_point) AS center_lat, ST_X(center_point) AS center_lon,
|
||||
area_sq_nm, member_count, zone_id, zone_name, members, color
|
||||
FROM kcg.group_polygon_snapshots
|
||||
WHERE snapshot_time = (SELECT MAX(snapshot_time) FROM kcg.group_polygon_snapshots)
|
||||
ORDER BY group_type, member_count DESC
|
||||
""";
|
||||
|
||||
private static final String GROUP_DETAIL_SQL = """
|
||||
SELECT group_type, group_key, group_label, snapshot_time,
|
||||
ST_AsGeoJSON(polygon) AS polygon_geojson,
|
||||
ST_Y(center_point) AS center_lat, ST_X(center_point) AS center_lon,
|
||||
area_sq_nm, member_count, zone_id, zone_name, members, color
|
||||
FROM kcg.group_polygon_snapshots
|
||||
WHERE group_key = ?
|
||||
ORDER BY snapshot_time DESC
|
||||
LIMIT 1
|
||||
""";
|
||||
|
||||
private static final String GROUP_HISTORY_SQL = """
|
||||
SELECT group_type, group_key, group_label, snapshot_time,
|
||||
ST_AsGeoJSON(polygon) AS polygon_geojson,
|
||||
ST_Y(center_point) AS center_lat, ST_X(center_point) AS center_lon,
|
||||
area_sq_nm, member_count, zone_id, zone_name, members, color
|
||||
FROM kcg.group_polygon_snapshots
|
||||
WHERE group_key = ? AND snapshot_time > NOW() - CAST(? || ' hours' AS INTERVAL)
|
||||
ORDER BY snapshot_time DESC
|
||||
""";
|
||||
|
||||
/**
|
||||
* 최신 스냅샷의 전체 그룹 폴리곤 목록 (5분 캐시).
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<GroupPolygonDto> getLatestGroups() {
|
||||
Cache cache = cacheManager.getCache(CacheConfig.GROUP_POLYGONS);
|
||||
if (cache != null) {
|
||||
Cache.ValueWrapper wrapper = cache.get("data");
|
||||
if (wrapper != null) {
|
||||
return (List<GroupPolygonDto>) wrapper.get();
|
||||
}
|
||||
}
|
||||
|
||||
List<GroupPolygonDto> results = jdbcTemplate.query(LATEST_GROUPS_SQL, this::mapRow);
|
||||
|
||||
if (cache != null) {
|
||||
cache.put("data", results);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 그룹의 최신 상세 정보.
|
||||
*/
|
||||
public GroupPolygonDto getGroupDetail(String groupKey) {
|
||||
List<GroupPolygonDto> results = jdbcTemplate.query(GROUP_DETAIL_SQL, this::mapRow, groupKey);
|
||||
return results.isEmpty() ? null : results.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 그룹의 시간별 히스토리.
|
||||
*/
|
||||
public List<GroupPolygonDto> getGroupHistory(String groupKey, int hours) {
|
||||
return jdbcTemplate.query(GROUP_HISTORY_SQL, this::mapRow, groupKey, String.valueOf(hours));
|
||||
}
|
||||
|
||||
private GroupPolygonDto mapRow(ResultSet rs, int rowNum) throws SQLException {
|
||||
Object polygonObj = null;
|
||||
String polygonJson = rs.getString("polygon_geojson");
|
||||
if (polygonJson != null) {
|
||||
try {
|
||||
polygonObj = objectMapper.readValue(polygonJson, new TypeReference<Map<String, Object>>() {});
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to parse polygon GeoJSON: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<String, Object>> members = List.of();
|
||||
String membersJson = rs.getString("members");
|
||||
if (membersJson != null) {
|
||||
try {
|
||||
members = objectMapper.readValue(membersJson, new TypeReference<>() {});
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to parse members JSON: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return GroupPolygonDto.builder()
|
||||
.groupType(rs.getString("group_type"))
|
||||
.groupKey(rs.getString("group_key"))
|
||||
.groupLabel(rs.getString("group_label"))
|
||||
.snapshotTime(rs.getString("snapshot_time"))
|
||||
.polygon(polygonObj)
|
||||
.centerLat(rs.getDouble("center_lat"))
|
||||
.centerLon(rs.getDouble("center_lon"))
|
||||
.areaSqNm(rs.getDouble("area_sq_nm"))
|
||||
.memberCount(rs.getInt("member_count"))
|
||||
.zoneId(rs.getString("zone_id"))
|
||||
.zoneName(rs.getString("zone_name"))
|
||||
.members(members)
|
||||
.color(rs.getString("color"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
불러오는 중...
Reference in New Issue
Block a user