Merge branch 'feature/dead-code-cleanup' into develop
This commit is contained in:
커밋
50badbe2bb
@ -1,233 +0,0 @@
|
|||||||
package com.snp.batch.common.util;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
public class JsonChangeDetector {
|
|
||||||
|
|
||||||
// Map으로 변환 시 사용할 ObjectMapper (표준 Mapper 사용)
|
|
||||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
|
||||||
|
|
||||||
// 해시 비교에서 제외할 필드 목록 (DataSetVersion 등)
|
|
||||||
// 이 목록은 모든 JSON 계층에 걸쳐 적용됩니다.
|
|
||||||
private static final java.util.Set<String> EXCLUDE_KEYS =
|
|
||||||
java.util.Set.of("DataSetVersion", "APSStatus", "LastUpdateDateTime");
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// ✅ LIST_SORT_KEYS: 정적 초기화 블록을 사용한 Map 정의
|
|
||||||
// =========================================================================
|
|
||||||
private static final Map<String, String> LIST_SORT_KEYS;
|
|
||||||
static {
|
|
||||||
// TreeMap을 사용하여 키를 알파벳 순으로 정렬할 수도 있지만, 여기서는 HashMap을 사용하고 final로 만듭니다.
|
|
||||||
Map<String, String> map = new HashMap<>();
|
|
||||||
// List 필드명 // 정렬 기준 복합 키 (JSON 필드명, 쉼표로 구분)
|
|
||||||
map.put("OwnerHistory", "OwnerCode,EffectiveDate,Sequence");
|
|
||||||
map.put("CrewList", "LRNO,Shipname,Nationality");
|
|
||||||
map.put("StowageCommodity", "Sequence,CommodityCode,StowageCode");
|
|
||||||
map.put("GroupBeneficialOwnerHistory", "EffectiveDate,GroupBeneficialOwnerCode,Sequence");
|
|
||||||
map.put("ShipManagerHistory", "EffectiveDate,ShipManagerCode,Sequence");
|
|
||||||
map.put("OperatorHistory", "EffectiveDate,OperatorCode,Sequence");
|
|
||||||
map.put("TechnicalManagerHistory", "EffectiveDate,Sequence,TechnicalManagerCode");
|
|
||||||
map.put("BareBoatCharterHistory", "Sequence,EffectiveDate,BBChartererCode");
|
|
||||||
map.put("NameHistory", "Sequence,EffectiveDate");
|
|
||||||
map.put("FlagHistory", "FlagCode,EffectiveDate,Sequence");
|
|
||||||
map.put("PandIHistory", "PandIClubCode,EffectiveDate");
|
|
||||||
map.put("CallSignAndMmsiHistory", "EffectiveDate,SeqNo");
|
|
||||||
map.put("IceClass", "IceClassCode");
|
|
||||||
map.put("SafetyManagementCertificateHistory", "Sequence");
|
|
||||||
map.put("ClassHistory", "ClassCode,EffectiveDate,Sequence");
|
|
||||||
map.put("SurveyDatesHistory", "ClassSocietyCode");
|
|
||||||
map.put("SurveyDatesHistoryUnique", "ClassSocietyCode,SurveyDate,SurveyType");
|
|
||||||
map.put("SisterShipLinks", "LinkedLRNO");
|
|
||||||
map.put("StatusHistory", "Sequence,StatusCode,StatusDate");
|
|
||||||
map.put("SpecialFeature", "Sequence,SpecialFeatureCode");
|
|
||||||
map.put("Thrusters", "Sequence");
|
|
||||||
map.put("DarkActivityConfirmed", "Lrno,Mmsi,Dark_Time,Dark_Status");
|
|
||||||
map.put("CompanyComplianceDetails", "OwCode");
|
|
||||||
map.put("CompanyVesselRelationships", "LRNO");
|
|
||||||
map.put("CompanyDetailsComplexWithCodesAndParent", "OWCODE,LastChangeDate");
|
|
||||||
|
|
||||||
LIST_SORT_KEYS = Collections.unmodifiableMap(map);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// 1. JSON 문자열을 정렬 및 필터링된 Map으로 변환하는 핵심 로직
|
|
||||||
// =========================================================================
|
|
||||||
/**
|
|
||||||
* JSON 문자열을 Map으로 변환하고, 특정 키를 제거하며, 키 순서가 정렬된 상태로 만듭니다.
|
|
||||||
* @param jsonString API 응답 또는 DB에서 읽은 JSON 문자열
|
|
||||||
* @return 필터링되고 정렬된 Map 객체
|
|
||||||
*/
|
|
||||||
public static Map<String, Object> jsonToSortedFilteredMap(String jsonString) {
|
|
||||||
if (jsonString == null || jsonString.trim().isEmpty()) {
|
|
||||||
return Collections.emptyMap();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. Map<String, Object>으로 1차 변환합니다. (순서 보장 안됨)
|
|
||||||
Map<String, Object> rawMap = MAPPER.readValue(jsonString,
|
|
||||||
new com.fasterxml.jackson.core.type.TypeReference<Map<String, Object>>() {});
|
|
||||||
|
|
||||||
// 2. 재귀 함수를 호출하여 키를 제거하고 TreeMap(키 순서 정렬)으로 깊은 복사합니다.
|
|
||||||
return deepFilterAndSort(rawMap);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("Error converting JSON to filtered Map: " + e.getMessage());
|
|
||||||
// 예외 발생 시 빈 Map 반환
|
|
||||||
return Collections.emptyMap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map을 재귀적으로 탐색하며 제외 키를 제거하고 TreeMap(알파벳 순서)으로 변환합니다.
|
|
||||||
*/
|
|
||||||
private static Map<String, Object> deepFilterAndSort(Map<String, Object> rawMap) {
|
|
||||||
// Map을 TreeMap으로 생성하여 키 순서를 알파벳 순으로 강제 정렬합니다.
|
|
||||||
Map<String, Object> sortedMap = new TreeMap<>();
|
|
||||||
|
|
||||||
for (Map.Entry<String, Object> entry : rawMap.entrySet()) {
|
|
||||||
String key = entry.getKey();
|
|
||||||
Object value = entry.getValue();
|
|
||||||
|
|
||||||
// 🔑 1. 제외할 키 값인지 확인
|
|
||||||
if (EXCLUDE_KEYS.contains(key)) {
|
|
||||||
continue; // 제외
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 값의 타입에 따라 재귀 처리
|
|
||||||
if (value instanceof Map) {
|
|
||||||
// 재귀 호출: 하위 Map을 필터링하고 정렬
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Map<String, Object> subMap = (Map<String, Object>) value;
|
|
||||||
sortedMap.put(key, deepFilterAndSort(subMap));
|
|
||||||
} else if (value instanceof List) {
|
|
||||||
// List 처리: List 내부의 Map 요소만 재귀 호출
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
List<Object> rawList = (List<Object>) value;
|
|
||||||
List<Object> filteredList = new ArrayList<>();
|
|
||||||
|
|
||||||
// 1. List 내부의 Map 요소들을 재귀적으로 필터링/정렬하여 filteredList에 추가
|
|
||||||
for (Object item : rawList) {
|
|
||||||
if (item instanceof Map) {
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Map<String, Object> itemMap = (Map<String, Object>) item;
|
|
||||||
// List의 요소인 Map도 필터링하고 정렬 (Map의 필드 순서 정렬)
|
|
||||||
filteredList.add(deepFilterAndSort(itemMap));
|
|
||||||
} else {
|
|
||||||
filteredList.add(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 🔑 List 필드명에 따른 복합 순서 정렬 로직 (수정된 핵심 로직)
|
|
||||||
String listFieldName = entry.getKey();
|
|
||||||
String sortKeysString = LIST_SORT_KEYS.get(listFieldName); // 쉼표로 구분된 복합 키 문자열
|
|
||||||
|
|
||||||
if (sortKeysString != null && !filteredList.isEmpty() && filteredList.get(0) instanceof Map) {
|
|
||||||
// 복합 키 문자열을 개별 키 배열로 분리
|
|
||||||
final String[] sortKeys = sortKeysString.split(",");
|
|
||||||
|
|
||||||
// Map 요소를 가진 리스트인 경우에만 정렬 실행
|
|
||||||
try {
|
|
||||||
Collections.sort(filteredList, new Comparator<Object>() {
|
|
||||||
@Override
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public int compare(Object o1, Object o2) {
|
|
||||||
Map<String, Object> map1 = (Map<String, Object>) o1;
|
|
||||||
Map<String, Object> map2 = (Map<String, Object>) o2;
|
|
||||||
|
|
||||||
// 복합 키(sortKeys)를 순서대로 순회하며 비교
|
|
||||||
for (String rawSortKey : sortKeys) {
|
|
||||||
// 키의 공백 제거
|
|
||||||
String sortKey = rawSortKey.trim();
|
|
||||||
|
|
||||||
Object key1 = map1.get(sortKey);
|
|
||||||
Object key2 = map2.get(sortKey);
|
|
||||||
|
|
||||||
// null 값 처리 로직
|
|
||||||
if (key1 == null && key2 == null) {
|
|
||||||
continue; // 두 값이 동일하므로 다음 키로 이동
|
|
||||||
}
|
|
||||||
if (key1 == null) {
|
|
||||||
// key1이 null이고 key2는 null이 아니면, key2가 더 크다고 (뒤 순서) 간주하고 1 반환
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (key2 == null) {
|
|
||||||
// key2가 null이고 key1은 null이 아니면, key1이 더 크다고 (뒤 순서) 간주하고 -1 반환
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 값을 문자열로 변환하여 비교 (String, Number, Date 타입 모두 처리 가능)
|
|
||||||
int comparisonResult = key1.toString().compareTo(key2.toString());
|
|
||||||
|
|
||||||
// 현재 키에서 순서가 결정되면 즉시 반환
|
|
||||||
if (comparisonResult != 0) {
|
|
||||||
return comparisonResult;
|
|
||||||
}
|
|
||||||
// comparisonResult == 0 이면 다음 키로 이동하여 비교를 계속함
|
|
||||||
}
|
|
||||||
|
|
||||||
// 모든 키를 비교해도 동일한 경우
|
|
||||||
// 이 경우 두 Map은 해시값 측면에서 동일한 것으로 간주되어야 합니다.
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("List sort failed for key " + listFieldName + ": " + e.getMessage());
|
|
||||||
// 정렬 실패 시 원래 순서 유지 (filteredList 상태 유지)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sortedMap.put(key, filteredList);
|
|
||||||
} else {
|
|
||||||
// String, Number 등 기본 타입은 그대로 추가
|
|
||||||
sortedMap.put(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sortedMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// 2. 해시 생성 로직
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 필터링되고 정렬된 Map의 문자열 표현을 기반으로 SHA-256 해시를 생성합니다.
|
|
||||||
*/
|
|
||||||
public static String getSha256HashFromMap(Map<String, Object> sortedMap) {
|
|
||||||
// 1. Map을 String으로 변환: TreeMap 덕분에 toString() 결과가 항상 동일한 순서를 가집니다.
|
|
||||||
String mapString = sortedMap.toString();
|
|
||||||
|
|
||||||
try {
|
|
||||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
||||||
byte[] hash = digest.digest(mapString.getBytes("UTF-8"));
|
|
||||||
|
|
||||||
// 바이트 배열을 16진수 문자열로 변환
|
|
||||||
StringBuilder hexString = new StringBuilder();
|
|
||||||
for (byte b : hash) {
|
|
||||||
String hex = Integer.toHexString(0xff & b);
|
|
||||||
if (hex.length() == 1) hexString.append('0');
|
|
||||||
hexString.append(hex);
|
|
||||||
}
|
|
||||||
return hexString.toString();
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("Error generating hash: " + e.getMessage());
|
|
||||||
return "HASH_ERROR";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// 3. 해시값 비교 로직
|
|
||||||
// =========================================================================
|
|
||||||
public static boolean isChanged(String previousHash, String currentHash) {
|
|
||||||
// DB 해시가 null인 경우 (첫 Insert)는 변경된 것으로 간주
|
|
||||||
if (previousHash == null || previousHash.isEmpty()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 해시값이 다르면 변경된 것으로 간주
|
|
||||||
return !Objects.equals(previousHash, currentHash);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
package com.snp.batch.common.util;
|
|
||||||
|
|
||||||
public class SafeGetDataUtil {
|
|
||||||
private String safeGetString(String value) {
|
|
||||||
if (value == null || value.trim().isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return value.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Double safeGetDouble(String value) {
|
|
||||||
if (value == null || value.trim().isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return Double.parseDouble(value);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Long safeGetLong(String value) {
|
|
||||||
if (value == null || value.trim().isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return Long.parseLong(value.trim());
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,300 +0,0 @@
|
|||||||
package com.snp.batch.common.web.controller;
|
|
||||||
|
|
||||||
import com.snp.batch.common.web.ApiResponse;
|
|
||||||
import com.snp.batch.common.web.service.BaseService;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 모든 REST Controller의 공통 베이스 클래스
|
|
||||||
* CRUD API의 일관된 구조 제공
|
|
||||||
*
|
|
||||||
* 이 클래스는 추상 클래스이므로 @Tag를 붙이지 않습니다.
|
|
||||||
* 하위 클래스에서 @Tag를 정의하면 모든 엔드포인트가 해당 태그로 그룹화됩니다.
|
|
||||||
*
|
|
||||||
* @param <D> DTO 타입
|
|
||||||
* @param <ID> ID 타입
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public abstract class BaseController<D, ID> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service 반환 (하위 클래스에서 구현)
|
|
||||||
*/
|
|
||||||
protected abstract BaseService<?, D, ID> getService();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 리소스 이름 반환 (로깅용)
|
|
||||||
*/
|
|
||||||
protected abstract String getResourceName();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 단건 생성
|
|
||||||
*/
|
|
||||||
@Operation(
|
|
||||||
summary = "리소스 생성",
|
|
||||||
description = "새로운 리소스를 생성합니다",
|
|
||||||
responses = {
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "200",
|
|
||||||
description = "생성 성공"
|
|
||||||
),
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "500",
|
|
||||||
description = "서버 오류"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@PostMapping
|
|
||||||
public ResponseEntity<ApiResponse<D>> create(
|
|
||||||
@Parameter(description = "생성할 리소스 데이터", required = true)
|
|
||||||
@RequestBody D dto) {
|
|
||||||
log.info("{} 생성 요청", getResourceName());
|
|
||||||
try {
|
|
||||||
D created = getService().create(dto);
|
|
||||||
return ResponseEntity.ok(
|
|
||||||
ApiResponse.success(getResourceName() + " created successfully", created)
|
|
||||||
);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("{} 생성 실패", getResourceName(), e);
|
|
||||||
return ResponseEntity.internalServerError().body(
|
|
||||||
ApiResponse.error("Failed to create " + getResourceName() + ": " + e.getMessage())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 단건 조회
|
|
||||||
*/
|
|
||||||
@Operation(
|
|
||||||
summary = "리소스 조회",
|
|
||||||
description = "ID로 특정 리소스를 조회합니다",
|
|
||||||
responses = {
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "200",
|
|
||||||
description = "조회 성공"
|
|
||||||
),
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "404",
|
|
||||||
description = "리소스 없음"
|
|
||||||
),
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "500",
|
|
||||||
description = "서버 오류"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@GetMapping("/{id}")
|
|
||||||
public ResponseEntity<ApiResponse<D>> getById(
|
|
||||||
@Parameter(description = "리소스 ID", required = true)
|
|
||||||
@PathVariable ID id) {
|
|
||||||
log.info("{} 조회 요청: ID={}", getResourceName(), id);
|
|
||||||
try {
|
|
||||||
return getService().findById(id)
|
|
||||||
.map(dto -> ResponseEntity.ok(ApiResponse.success(dto)))
|
|
||||||
.orElse(ResponseEntity.notFound().build());
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("{} 조회 실패: ID={}", getResourceName(), id, e);
|
|
||||||
return ResponseEntity.internalServerError().body(
|
|
||||||
ApiResponse.error("Failed to get " + getResourceName() + ": " + e.getMessage())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 전체 조회
|
|
||||||
*/
|
|
||||||
@Operation(
|
|
||||||
summary = "전체 리소스 조회",
|
|
||||||
description = "모든 리소스를 조회합니다",
|
|
||||||
responses = {
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "200",
|
|
||||||
description = "조회 성공"
|
|
||||||
),
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "500",
|
|
||||||
description = "서버 오류"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@GetMapping
|
|
||||||
public ResponseEntity<ApiResponse<List<D>>> getAll() {
|
|
||||||
log.info("{} 전체 조회 요청", getResourceName());
|
|
||||||
try {
|
|
||||||
List<D> list = getService().findAll();
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(list));
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("{} 전체 조회 실패", getResourceName(), e);
|
|
||||||
return ResponseEntity.internalServerError().body(
|
|
||||||
ApiResponse.error("Failed to get all " + getResourceName() + ": " + e.getMessage())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 페이징 조회 (JDBC 기반)
|
|
||||||
*
|
|
||||||
* @param offset 시작 위치 (기본값: 0)
|
|
||||||
* @param limit 조회 개수 (기본값: 20)
|
|
||||||
*/
|
|
||||||
@Operation(
|
|
||||||
summary = "페이징 조회",
|
|
||||||
description = "페이지 단위로 리소스를 조회합니다",
|
|
||||||
responses = {
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "200",
|
|
||||||
description = "조회 성공"
|
|
||||||
),
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "500",
|
|
||||||
description = "서버 오류"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@GetMapping("/page")
|
|
||||||
public ResponseEntity<ApiResponse<List<D>>> getPage(
|
|
||||||
@Parameter(description = "시작 위치 (0부터 시작)", example = "0")
|
|
||||||
@RequestParam(defaultValue = "0") int offset,
|
|
||||||
@Parameter(description = "조회 개수", example = "20")
|
|
||||||
@RequestParam(defaultValue = "20") int limit) {
|
|
||||||
log.info("{} 페이징 조회 요청: offset={}, limit={}",
|
|
||||||
getResourceName(), offset, limit);
|
|
||||||
try {
|
|
||||||
List<D> list = getService().findAll(offset, limit);
|
|
||||||
long total = getService().count();
|
|
||||||
|
|
||||||
// 페이징 정보를 포함한 응답
|
|
||||||
return ResponseEntity.ok(
|
|
||||||
ApiResponse.success("Retrieved " + list.size() + " items (total: " + total + ")", list)
|
|
||||||
);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("{} 페이징 조회 실패", getResourceName(), e);
|
|
||||||
return ResponseEntity.internalServerError().body(
|
|
||||||
ApiResponse.error("Failed to get page of " + getResourceName() + ": " + e.getMessage())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 단건 수정
|
|
||||||
*/
|
|
||||||
@Operation(
|
|
||||||
summary = "리소스 수정",
|
|
||||||
description = "기존 리소스를 수정합니다",
|
|
||||||
responses = {
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "200",
|
|
||||||
description = "수정 성공"
|
|
||||||
),
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "404",
|
|
||||||
description = "리소스 없음"
|
|
||||||
),
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "500",
|
|
||||||
description = "서버 오류"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@PutMapping("/{id}")
|
|
||||||
public ResponseEntity<ApiResponse<D>> update(
|
|
||||||
@Parameter(description = "리소스 ID", required = true)
|
|
||||||
@PathVariable ID id,
|
|
||||||
@Parameter(description = "수정할 리소스 데이터", required = true)
|
|
||||||
@RequestBody D dto) {
|
|
||||||
log.info("{} 수정 요청: ID={}", getResourceName(), id);
|
|
||||||
try {
|
|
||||||
D updated = getService().update(id, dto);
|
|
||||||
return ResponseEntity.ok(
|
|
||||||
ApiResponse.success(getResourceName() + " updated successfully", updated)
|
|
||||||
);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
return ResponseEntity.notFound().build();
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("{} 수정 실패: ID={}", getResourceName(), id, e);
|
|
||||||
return ResponseEntity.internalServerError().body(
|
|
||||||
ApiResponse.error("Failed to update " + getResourceName() + ": " + e.getMessage())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 단건 삭제
|
|
||||||
*/
|
|
||||||
@Operation(
|
|
||||||
summary = "리소스 삭제",
|
|
||||||
description = "기존 리소스를 삭제합니다",
|
|
||||||
responses = {
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "200",
|
|
||||||
description = "삭제 성공"
|
|
||||||
),
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "404",
|
|
||||||
description = "리소스 없음"
|
|
||||||
),
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "500",
|
|
||||||
description = "서버 오류"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@DeleteMapping("/{id}")
|
|
||||||
public ResponseEntity<ApiResponse<Void>> delete(
|
|
||||||
@Parameter(description = "리소스 ID", required = true)
|
|
||||||
@PathVariable ID id) {
|
|
||||||
log.info("{} 삭제 요청: ID={}", getResourceName(), id);
|
|
||||||
try {
|
|
||||||
getService().deleteById(id);
|
|
||||||
return ResponseEntity.ok(
|
|
||||||
ApiResponse.success(getResourceName() + " deleted successfully", null)
|
|
||||||
);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
return ResponseEntity.notFound().build();
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("{} 삭제 실패: ID={}", getResourceName(), id, e);
|
|
||||||
return ResponseEntity.internalServerError().body(
|
|
||||||
ApiResponse.error("Failed to delete " + getResourceName() + ": " + e.getMessage())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 존재 여부 확인
|
|
||||||
*/
|
|
||||||
@Operation(
|
|
||||||
summary = "리소스 존재 확인",
|
|
||||||
description = "특정 ID의 리소스가 존재하는지 확인합니다",
|
|
||||||
responses = {
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "200",
|
|
||||||
description = "확인 성공"
|
|
||||||
),
|
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
|
||||||
responseCode = "500",
|
|
||||||
description = "서버 오류"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@GetMapping("/{id}/exists")
|
|
||||||
public ResponseEntity<ApiResponse<Boolean>> exists(
|
|
||||||
@Parameter(description = "리소스 ID", required = true)
|
|
||||||
@PathVariable ID id) {
|
|
||||||
log.debug("{} 존재 여부 확인: ID={}", getResourceName(), id);
|
|
||||||
try {
|
|
||||||
boolean exists = getService().existsById(id);
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(exists));
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("{} 존재 여부 확인 실패: ID={}", getResourceName(), id, e);
|
|
||||||
return ResponseEntity.internalServerError().body(
|
|
||||||
ApiResponse.error("Failed to check existence: " + e.getMessage())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
package com.snp.batch.common.web.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 모든 DTO의 공통 베이스 클래스
|
|
||||||
* 생성/수정 정보 등 공통 필드
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public abstract class BaseDto {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 생성 일시
|
|
||||||
*/
|
|
||||||
private LocalDateTime createdAt;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 수정 일시
|
|
||||||
*/
|
|
||||||
private LocalDateTime updatedAt;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 생성자
|
|
||||||
*/
|
|
||||||
private String createdBy;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 수정자
|
|
||||||
*/
|
|
||||||
private String updatedBy;
|
|
||||||
}
|
|
||||||
@ -1,202 +0,0 @@
|
|||||||
package com.snp.batch.common.web.service;
|
|
||||||
|
|
||||||
import com.snp.batch.common.batch.repository.BaseJdbcRepository;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 하이브리드 서비스 Base 클래스 (DB 캐시 + 외부 API 프록시)
|
|
||||||
*
|
|
||||||
* 사용 시나리오:
|
|
||||||
* 1. 클라이언트 요청 → DB 조회 (캐시 Hit)
|
|
||||||
* - 캐시 데이터 유효 시 즉시 반환
|
|
||||||
* 2. 캐시 Miss 또는 만료 시
|
|
||||||
* - 외부 서비스 API 호출
|
|
||||||
* - DB에 저장 (캐시 갱신)
|
|
||||||
* - 클라이언트에게 반환
|
|
||||||
*
|
|
||||||
* 장점:
|
|
||||||
* - 빠른 응답 (DB 캐시)
|
|
||||||
* - 외부 서비스 장애 시에도 캐시 데이터 제공 가능
|
|
||||||
* - 외부 API 호출 횟수 감소 (비용 절감)
|
|
||||||
*
|
|
||||||
* @param <T> Entity 타입
|
|
||||||
* @param <D> DTO 타입
|
|
||||||
* @param <ID> ID 타입
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public abstract class BaseHybridService<T, D, ID> extends BaseServiceImpl<T, D, ID> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* WebClient 반환 (하위 클래스에서 구현)
|
|
||||||
*/
|
|
||||||
protected abstract WebClient getWebClient();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 외부 서비스 이름 반환
|
|
||||||
*/
|
|
||||||
protected abstract String getExternalServiceName();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 캐시 유효 시간 (초)
|
|
||||||
* 기본값: 300초 (5분)
|
|
||||||
*/
|
|
||||||
protected long getCacheTtlSeconds() {
|
|
||||||
return 300;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 요청 타임아웃
|
|
||||||
*/
|
|
||||||
protected Duration getTimeout() {
|
|
||||||
return Duration.ofSeconds(30);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 하이브리드 조회: DB 캐시 우선, 없으면 외부 API 호출
|
|
||||||
*
|
|
||||||
* @param id 조회 키
|
|
||||||
* @return DTO
|
|
||||||
*/
|
|
||||||
@Transactional
|
|
||||||
public D findByIdHybrid(ID id) {
|
|
||||||
log.info("[하이브리드] ID로 조회: {}", id);
|
|
||||||
|
|
||||||
// 1. DB 캐시 조회
|
|
||||||
Optional<D> cached = findById(id);
|
|
||||||
|
|
||||||
if (cached.isPresent()) {
|
|
||||||
// 캐시 유효성 검증
|
|
||||||
if (isCacheValid(cached.get())) {
|
|
||||||
log.info("[하이브리드] 캐시 Hit - DB에서 반환");
|
|
||||||
return cached.get();
|
|
||||||
} else {
|
|
||||||
log.info("[하이브리드] 캐시 만료 - 외부 API 호출");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.info("[하이브리드] 캐시 Miss - 외부 API 호출");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 외부 API 호출
|
|
||||||
try {
|
|
||||||
D externalData = fetchFromExternalApi(id);
|
|
||||||
|
|
||||||
// 3. DB 저장 (캐시 갱신)
|
|
||||||
T entity = toEntity(externalData);
|
|
||||||
T saved = getRepository().save(entity);
|
|
||||||
|
|
||||||
log.info("[하이브리드] 외부 데이터 DB 저장 완료");
|
|
||||||
return toDto(saved);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("[하이브리드] 외부 API 호출 실패: {}", e.getMessage());
|
|
||||||
|
|
||||||
// 4. 외부 API 실패 시 만료된 캐시라도 반환 (Fallback)
|
|
||||||
if (cached.isPresent()) {
|
|
||||||
log.warn("[하이브리드] Fallback - 만료된 캐시 반환");
|
|
||||||
return cached.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new RuntimeException("데이터 조회 실패: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 외부 API에서 데이터 조회 (하위 클래스에서 구현)
|
|
||||||
*
|
|
||||||
* @param id 조회 키
|
|
||||||
* @return DTO
|
|
||||||
*/
|
|
||||||
protected abstract D fetchFromExternalApi(ID id) throws Exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 캐시 유효성 검증
|
|
||||||
* 기본 구현: updated_at 기준으로 TTL 체크
|
|
||||||
*
|
|
||||||
* @param dto 캐시 데이터
|
|
||||||
* @return 유효 여부
|
|
||||||
*/
|
|
||||||
protected boolean isCacheValid(D dto) {
|
|
||||||
// BaseDto를 상속한 경우 updatedAt 체크
|
|
||||||
try {
|
|
||||||
LocalDateTime updatedAt = extractUpdatedAt(dto);
|
|
||||||
if (updatedAt == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalDateTime now = LocalDateTime.now();
|
|
||||||
long elapsedSeconds = Duration.between(updatedAt, now).getSeconds();
|
|
||||||
|
|
||||||
return elapsedSeconds < getCacheTtlSeconds();
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("캐시 유효성 검증 실패 - 항상 최신 데이터 조회: {}", e.getMessage());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DTO에서 updatedAt 추출 (하위 클래스에서 오버라이드 가능)
|
|
||||||
*/
|
|
||||||
protected LocalDateTime extractUpdatedAt(D dto) {
|
|
||||||
// 기본 구현: 항상 캐시 무효 (외부 API 호출)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 강제 캐시 갱신 (외부 API 호출 강제)
|
|
||||||
*/
|
|
||||||
@Transactional
|
|
||||||
public D refreshCache(ID id) throws Exception {
|
|
||||||
log.info("[하이브리드] 캐시 강제 갱신: {}", id);
|
|
||||||
|
|
||||||
D externalData = fetchFromExternalApi(id);
|
|
||||||
T entity = toEntity(externalData);
|
|
||||||
T saved = getRepository().save(entity);
|
|
||||||
|
|
||||||
return toDto(saved);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 외부 API GET 요청
|
|
||||||
*/
|
|
||||||
protected <RES> RES callExternalGet(String endpoint, Map<String, String> params, Class<RES> responseType) {
|
|
||||||
log.info("[{}] GET 요청: endpoint={}", getExternalServiceName(), endpoint);
|
|
||||||
|
|
||||||
return getWebClient()
|
|
||||||
.get()
|
|
||||||
.uri(uriBuilder -> {
|
|
||||||
uriBuilder.path(endpoint);
|
|
||||||
if (params != null) {
|
|
||||||
params.forEach(uriBuilder::queryParam);
|
|
||||||
}
|
|
||||||
return uriBuilder.build();
|
|
||||||
})
|
|
||||||
.retrieve()
|
|
||||||
.bodyToMono(responseType)
|
|
||||||
.timeout(getTimeout())
|
|
||||||
.block();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 외부 API POST 요청
|
|
||||||
*/
|
|
||||||
protected <REQ, RES> RES callExternalPost(String endpoint, REQ requestBody, Class<RES> responseType) {
|
|
||||||
log.info("[{}] POST 요청: endpoint={}", getExternalServiceName(), endpoint);
|
|
||||||
|
|
||||||
return getWebClient()
|
|
||||||
.post()
|
|
||||||
.uri(endpoint)
|
|
||||||
.bodyValue(requestBody)
|
|
||||||
.retrieve()
|
|
||||||
.bodyToMono(responseType)
|
|
||||||
.timeout(getTimeout())
|
|
||||||
.block();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,176 +0,0 @@
|
|||||||
package com.snp.batch.common.web.service;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 외부 API 프록시 서비스 Base 클래스
|
|
||||||
*
|
|
||||||
* 목적: 해외 외부 서비스를 국내에서 우회 접근할 수 있도록 프록시 역할 수행
|
|
||||||
*
|
|
||||||
* 사용 시나리오:
|
|
||||||
* - 외부 서비스가 해외에 있고 국내 IP에서만 접근 가능
|
|
||||||
* - 클라이언트 A → 우리 서버 (국내) → 외부 서비스 (해외) → 응답 전달
|
|
||||||
*
|
|
||||||
* 장점:
|
|
||||||
* - 실시간 데이터 제공 (DB 캐시 없이)
|
|
||||||
* - 외부 서비스의 최신 데이터 보장
|
|
||||||
* - DB 저장 부담 없음
|
|
||||||
*
|
|
||||||
* @param <REQ> 요청 DTO 타입
|
|
||||||
* @param <RES> 응답 DTO 타입
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public abstract class BaseProxyService<REQ, RES> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* WebClient 반환 (하위 클래스에서 구현)
|
|
||||||
* 외부 서비스별로 인증, Base URL 등 설정
|
|
||||||
*/
|
|
||||||
protected abstract WebClient getWebClient();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 외부 서비스 이름 반환 (로깅용)
|
|
||||||
*/
|
|
||||||
protected abstract String getServiceName();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 요청 타임아웃 (밀리초)
|
|
||||||
* 기본값: 30초
|
|
||||||
*/
|
|
||||||
protected Duration getTimeout() {
|
|
||||||
return Duration.ofSeconds(30);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET 요청 프록시
|
|
||||||
*
|
|
||||||
* @param endpoint 엔드포인트 경로 (예: "/api/ships")
|
|
||||||
* @param params 쿼리 파라미터
|
|
||||||
* @param responseType 응답 클래스 타입
|
|
||||||
* @return 외부 서비스 응답
|
|
||||||
*/
|
|
||||||
public RES proxyGet(String endpoint, Map<String, String> params, Class<RES> responseType) {
|
|
||||||
log.info("[{}] GET 요청 프록시: endpoint={}, params={}", getServiceName(), endpoint, params);
|
|
||||||
|
|
||||||
try {
|
|
||||||
WebClient.RequestHeadersSpec<?> spec = getWebClient()
|
|
||||||
.get()
|
|
||||||
.uri(uriBuilder -> {
|
|
||||||
uriBuilder.path(endpoint);
|
|
||||||
if (params != null) {
|
|
||||||
params.forEach(uriBuilder::queryParam);
|
|
||||||
}
|
|
||||||
return uriBuilder.build();
|
|
||||||
});
|
|
||||||
|
|
||||||
RES response = spec.retrieve()
|
|
||||||
.bodyToMono(responseType)
|
|
||||||
.timeout(getTimeout())
|
|
||||||
.block();
|
|
||||||
|
|
||||||
log.info("[{}] 응답 성공", getServiceName());
|
|
||||||
return response;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("[{}] 프록시 요청 실패: {}", getServiceName(), e.getMessage(), e);
|
|
||||||
throw new RuntimeException("외부 서비스 호출 실패: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST 요청 프록시
|
|
||||||
*
|
|
||||||
* @param endpoint 엔드포인트 경로
|
|
||||||
* @param requestBody 요청 본문
|
|
||||||
* @param responseType 응답 클래스 타입
|
|
||||||
* @return 외부 서비스 응답
|
|
||||||
*/
|
|
||||||
public RES proxyPost(String endpoint, REQ requestBody, Class<RES> responseType) {
|
|
||||||
log.info("[{}] POST 요청 프록시: endpoint={}", getServiceName(), endpoint);
|
|
||||||
|
|
||||||
try {
|
|
||||||
RES response = getWebClient()
|
|
||||||
.post()
|
|
||||||
.uri(endpoint)
|
|
||||||
.bodyValue(requestBody)
|
|
||||||
.retrieve()
|
|
||||||
.bodyToMono(responseType)
|
|
||||||
.timeout(getTimeout())
|
|
||||||
.block();
|
|
||||||
|
|
||||||
log.info("[{}] 응답 성공", getServiceName());
|
|
||||||
return response;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("[{}] 프록시 요청 실패: {}", getServiceName(), e.getMessage(), e);
|
|
||||||
throw new RuntimeException("외부 서비스 호출 실패: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PUT 요청 프록시
|
|
||||||
*/
|
|
||||||
public RES proxyPut(String endpoint, REQ requestBody, Class<RES> responseType) {
|
|
||||||
log.info("[{}] PUT 요청 프록시: endpoint={}", getServiceName(), endpoint);
|
|
||||||
|
|
||||||
try {
|
|
||||||
RES response = getWebClient()
|
|
||||||
.put()
|
|
||||||
.uri(endpoint)
|
|
||||||
.bodyValue(requestBody)
|
|
||||||
.retrieve()
|
|
||||||
.bodyToMono(responseType)
|
|
||||||
.timeout(getTimeout())
|
|
||||||
.block();
|
|
||||||
|
|
||||||
log.info("[{}] 응답 성공", getServiceName());
|
|
||||||
return response;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("[{}] 프록시 요청 실패: {}", getServiceName(), e.getMessage(), e);
|
|
||||||
throw new RuntimeException("외부 서비스 호출 실패: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DELETE 요청 프록시
|
|
||||||
*/
|
|
||||||
public void proxyDelete(String endpoint, Map<String, String> params) {
|
|
||||||
log.info("[{}] DELETE 요청 프록시: endpoint={}, params={}", getServiceName(), endpoint, params);
|
|
||||||
|
|
||||||
try {
|
|
||||||
getWebClient()
|
|
||||||
.delete()
|
|
||||||
.uri(uriBuilder -> {
|
|
||||||
uriBuilder.path(endpoint);
|
|
||||||
if (params != null) {
|
|
||||||
params.forEach(uriBuilder::queryParam);
|
|
||||||
}
|
|
||||||
return uriBuilder.build();
|
|
||||||
})
|
|
||||||
.retrieve()
|
|
||||||
.bodyToMono(Void.class)
|
|
||||||
.timeout(getTimeout())
|
|
||||||
.block();
|
|
||||||
|
|
||||||
log.info("[{}] DELETE 성공", getServiceName());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("[{}] 프록시 DELETE 실패: {}", getServiceName(), e.getMessage(), e);
|
|
||||||
throw new RuntimeException("외부 서비스 호출 실패: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 커스텀 요청 처리 (하위 클래스에서 오버라이드)
|
|
||||||
* 복잡한 로직이 필요한 경우 사용
|
|
||||||
*/
|
|
||||||
protected RES customRequest(REQ request) {
|
|
||||||
throw new UnsupportedOperationException("커스텀 요청이 구현되지 않았습니다");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
package com.snp.batch.common.web.service;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 모든 서비스의 공통 인터페이스 (JDBC 기반)
|
|
||||||
* CRUD 기본 메서드 정의
|
|
||||||
*
|
|
||||||
* @param <T> Entity 타입
|
|
||||||
* @param <D> DTO 타입
|
|
||||||
* @param <ID> ID 타입
|
|
||||||
*/
|
|
||||||
public interface BaseService<T, D, ID> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 단건 생성
|
|
||||||
*
|
|
||||||
* @param dto 생성할 데이터 DTO
|
|
||||||
* @return 생성된 데이터 DTO
|
|
||||||
*/
|
|
||||||
D create(D dto);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 단건 조회
|
|
||||||
*
|
|
||||||
* @param id 조회할 ID
|
|
||||||
* @return 조회된 데이터 DTO (Optional)
|
|
||||||
*/
|
|
||||||
Optional<D> findById(ID id);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 전체 조회
|
|
||||||
*
|
|
||||||
* @return 전체 데이터 DTO 리스트
|
|
||||||
*/
|
|
||||||
List<D> findAll();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 페이징 조회
|
|
||||||
*
|
|
||||||
* @param offset 시작 위치 (0부터 시작)
|
|
||||||
* @param limit 조회 개수
|
|
||||||
* @return 페이징된 데이터 리스트
|
|
||||||
*/
|
|
||||||
List<D> findAll(int offset, int limit);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 전체 개수 조회
|
|
||||||
*
|
|
||||||
* @return 전체 데이터 개수
|
|
||||||
*/
|
|
||||||
long count();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 단건 수정
|
|
||||||
*
|
|
||||||
* @param id 수정할 ID
|
|
||||||
* @param dto 수정할 데이터 DTO
|
|
||||||
* @return 수정된 데이터 DTO
|
|
||||||
*/
|
|
||||||
D update(ID id, D dto);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 단건 삭제
|
|
||||||
*
|
|
||||||
* @param id 삭제할 ID
|
|
||||||
*/
|
|
||||||
void deleteById(ID id);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 존재 여부 확인
|
|
||||||
*
|
|
||||||
* @param id 확인할 ID
|
|
||||||
* @return 존재 여부
|
|
||||||
*/
|
|
||||||
boolean existsById(ID id);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Entity를 DTO로 변환
|
|
||||||
*
|
|
||||||
* @param entity 엔티티
|
|
||||||
* @return DTO
|
|
||||||
*/
|
|
||||||
D toDto(T entity);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DTO를 Entity로 변환
|
|
||||||
*
|
|
||||||
* @param dto DTO
|
|
||||||
* @return 엔티티
|
|
||||||
*/
|
|
||||||
T toEntity(D dto);
|
|
||||||
}
|
|
||||||
@ -1,131 +0,0 @@
|
|||||||
package com.snp.batch.common.web.service;
|
|
||||||
|
|
||||||
import com.snp.batch.common.batch.repository.BaseJdbcRepository;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* BaseService의 기본 구현 (JDBC 기반)
|
|
||||||
* 공통 CRUD 로직 구현
|
|
||||||
*
|
|
||||||
* @param <T> Entity 타입
|
|
||||||
* @param <D> DTO 타입
|
|
||||||
* @param <ID> ID 타입
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
public abstract class BaseServiceImpl<T, D, ID> implements BaseService<T, D, ID> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Repository 반환 (하위 클래스에서 구현)
|
|
||||||
*/
|
|
||||||
protected abstract BaseJdbcRepository<T, ID> getRepository();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 엔티티 이름 반환 (로깅용)
|
|
||||||
*/
|
|
||||||
protected abstract String getEntityName();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public D create(D dto) {
|
|
||||||
log.info("{} 생성 시작", getEntityName());
|
|
||||||
T entity = toEntity(dto);
|
|
||||||
T saved = getRepository().save(entity);
|
|
||||||
log.info("{} 생성 완료: ID={}", getEntityName(), extractId(saved));
|
|
||||||
return toDto(saved);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<D> findById(ID id) {
|
|
||||||
log.debug("{} 조회: ID={}", getEntityName(), id);
|
|
||||||
return getRepository().findById(id).map(this::toDto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<D> findAll() {
|
|
||||||
log.debug("{} 전체 조회", getEntityName());
|
|
||||||
return getRepository().findAll().stream()
|
|
||||||
.map(this::toDto)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<D> findAll(int offset, int limit) {
|
|
||||||
log.debug("{} 페이징 조회: offset={}, limit={}", getEntityName(), offset, limit);
|
|
||||||
|
|
||||||
// 하위 클래스에서 제공하는 페이징 쿼리 실행
|
|
||||||
List<T> entities = executePagingQuery(offset, limit);
|
|
||||||
|
|
||||||
return entities.stream()
|
|
||||||
.map(this::toDto)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 페이징 쿼리 실행 (하위 클래스에서 구현)
|
|
||||||
*
|
|
||||||
* @param offset 시작 위치
|
|
||||||
* @param limit 조회 개수
|
|
||||||
* @return Entity 리스트
|
|
||||||
*/
|
|
||||||
protected abstract List<T> executePagingQuery(int offset, int limit);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long count() {
|
|
||||||
log.debug("{} 개수 조회", getEntityName());
|
|
||||||
return getRepository().count();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public D update(ID id, D dto) {
|
|
||||||
log.info("{} 수정 시작: ID={}", getEntityName(), id);
|
|
||||||
|
|
||||||
T entity = getRepository().findById(id)
|
|
||||||
.orElseThrow(() -> new IllegalArgumentException(
|
|
||||||
getEntityName() + " not found with id: " + id));
|
|
||||||
|
|
||||||
updateEntity(entity, dto);
|
|
||||||
T updated = getRepository().save(entity);
|
|
||||||
|
|
||||||
log.info("{} 수정 완료: ID={}", getEntityName(), id);
|
|
||||||
return toDto(updated);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public void deleteById(ID id) {
|
|
||||||
log.info("{} 삭제: ID={}", getEntityName(), id);
|
|
||||||
|
|
||||||
if (!getRepository().existsById(id)) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
getEntityName() + " not found with id: " + id);
|
|
||||||
}
|
|
||||||
|
|
||||||
getRepository().deleteById(id);
|
|
||||||
log.info("{} 삭제 완료: ID={}", getEntityName(), id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean existsById(ID id) {
|
|
||||||
return getRepository().existsById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Entity 업데이트 (하위 클래스에서 구현)
|
|
||||||
*
|
|
||||||
* @param entity 업데이트할 엔티티
|
|
||||||
* @param dto 업데이트 데이터
|
|
||||||
*/
|
|
||||||
protected abstract void updateEntity(T entity, D dto);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Entity에서 ID 추출 (로깅용, 하위 클래스에서 구현)
|
|
||||||
*/
|
|
||||||
protected abstract ID extractId(T entity);
|
|
||||||
}
|
|
||||||
불러오는 중...
Reference in New Issue
Block a user