feat: 같은 도메인에 여러 BYPASS API 엔드포인트 등록 지원 (#63)
- BypassApiConfig에 endpointName 필드 추가 (externalPath에서 자동 추출) - domainName unique 제약 → domainName + endpointName 복합 unique로 변경 - 코드 생성 시 같은 도메인의 모든 설정을 합쳐 Controller 1개 생성 - Service/DTO는 엔드포인트별로 별도 생성 - CodeGenerationResult에 servicePaths/dtoPaths 목록 반환 - 프론트엔드 타입/UI 업데이트 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
부모
0132408ae3
커밋
ce27c60985
@ -41,6 +41,7 @@ export interface BypassConfigRequest {
|
||||
export interface BypassConfigResponse {
|
||||
id: number;
|
||||
domainName: string;
|
||||
endpointName: string;
|
||||
displayName: string;
|
||||
webclientBean: string;
|
||||
externalPath: string;
|
||||
@ -57,8 +58,8 @@ export interface BypassConfigResponse {
|
||||
|
||||
export interface CodeGenerationResult {
|
||||
controllerPath: string;
|
||||
servicePath: string;
|
||||
dtoPath: string;
|
||||
servicePaths: string[];
|
||||
dtoPaths: string[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
|
||||
@ -463,19 +463,29 @@ export default function BypassConfig() {
|
||||
<p className="text-sm text-wing-text">{generationResult.message}</p>
|
||||
<div className="bg-wing-card rounded-lg p-3 space-y-2">
|
||||
<p className="text-xs font-semibold text-wing-text mb-1">생성된 파일</p>
|
||||
{[
|
||||
{ label: 'Controller', path: generationResult.controllerPath },
|
||||
{ label: 'Service', path: generationResult.servicePath },
|
||||
{ label: 'DTO', path: generationResult.dtoPath },
|
||||
].map(({ label, path }) => (
|
||||
<div key={label} className="flex gap-2 text-xs">
|
||||
<span className="w-20 font-medium text-wing-accent shrink-0">{label}</span>
|
||||
<div className="flex gap-2 text-xs">
|
||||
<span className="w-20 font-medium text-wing-accent shrink-0">Controller</span>
|
||||
<span className="font-mono text-wing-muted break-all">{generationResult.controllerPath}</span>
|
||||
</div>
|
||||
{generationResult.servicePaths.map((path, idx) => (
|
||||
<div key={`service-${idx}`} className="flex gap-2 text-xs">
|
||||
<span className="w-20 font-medium text-wing-accent shrink-0">
|
||||
Service {generationResult.servicePaths.length > 1 ? idx + 1 : ''}
|
||||
</span>
|
||||
<span className="font-mono text-wing-muted break-all">{path}</span>
|
||||
</div>
|
||||
))}
|
||||
{generationResult.dtoPaths.map((path, idx) => (
|
||||
<div key={`dto-${idx}`} className="flex gap-2 text-xs">
|
||||
<span className="w-20 font-medium text-wing-accent shrink-0">
|
||||
DTO {generationResult.dtoPaths.length > 1 ? idx + 1 : ''}
|
||||
</span>
|
||||
<span className="font-mono text-wing-muted break-all">{path}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-start gap-2 bg-amber-50 text-amber-700 rounded-lg p-3 text-xs">
|
||||
<span className="shrink-0">⚠</span>
|
||||
<span className="shrink-0">⚠</span>
|
||||
<span>서버를 재시작하면 새 API가 활성화됩니다.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -79,7 +79,7 @@ public class BypassConfigController {
|
||||
|
||||
@Operation(
|
||||
summary = "코드 생성",
|
||||
description = "등록된 설정을 기반으로 Controller, Service, DTO Java 소스 코드를 생성합니다."
|
||||
description = "등록된 설정의 도메인 전체를 기반으로 Controller, Service, DTO Java 소스 코드를 생성합니다. 같은 도메인의 모든 설정을 하나의 Controller로 합칩니다."
|
||||
)
|
||||
@PostMapping("/{id}/generate")
|
||||
public ResponseEntity<ApiResponse<CodeGenerationResult>> generateCode(
|
||||
@ -89,10 +89,13 @@ public class BypassConfigController {
|
||||
BypassApiConfig config = configRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("설정을 찾을 수 없습니다: " + id));
|
||||
|
||||
CodeGenerationResult result = bypassCodeGenerator.generate(
|
||||
config, config.getParams(), config.getFields(), force);
|
||||
// 같은 도메인의 모든 설정을 조회하여 함께 생성
|
||||
List<BypassApiConfig> domainConfigs = configRepository.findByDomainNameOrderById(config.getDomainName());
|
||||
|
||||
bypassConfigService.markAsGenerated(id);
|
||||
CodeGenerationResult result = bypassCodeGenerator.generate(domainConfigs, force);
|
||||
|
||||
// 같은 도메인의 모든 설정을 generated로 마킹
|
||||
domainConfigs.forEach(c -> bypassConfigService.markAsGenerated(c.getId()));
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(result));
|
||||
} catch (IllegalStateException e) {
|
||||
|
||||
@ -22,6 +22,9 @@ public class BypassConfigResponse {
|
||||
/** 도메인명 (패키지명/URL 경로) */
|
||||
private String domainName;
|
||||
|
||||
/** 엔드포인트명 (externalPath 마지막 세그먼트) */
|
||||
private String endpointName;
|
||||
|
||||
/** 표시명 */
|
||||
private String displayName;
|
||||
|
||||
|
||||
@ -5,8 +5,11 @@ import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 코드 자동 생성 결과 DTO
|
||||
* 같은 도메인에 N개의 엔드포인트를 지원하므로 Service/DTO는 목록으로 반환
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@ -17,11 +20,11 @@ public class CodeGenerationResult {
|
||||
/** 생성된 Controller 파일 경로 */
|
||||
private String controllerPath;
|
||||
|
||||
/** 생성된 Service 파일 경로 */
|
||||
private String servicePath;
|
||||
/** 생성된 Service 파일 경로 목록 (엔드포인트별) */
|
||||
private List<String> servicePaths;
|
||||
|
||||
/** 생성된 DTO 파일 경로 */
|
||||
private String dtoPath;
|
||||
/** 생성된 DTO 파일 경로 목록 (엔드포인트별) */
|
||||
private List<String> dtoPaths;
|
||||
|
||||
/** 결과 메시지 */
|
||||
private String message;
|
||||
|
||||
@ -14,7 +14,9 @@ import java.util.List;
|
||||
* JPA를 사용하므로 @PrePersist, @PreUpdate로 감사 필드 자동 설정
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "bypass_api_config")
|
||||
@Table(name = "bypass_api_config", uniqueConstraints = {
|
||||
@UniqueConstraint(columnNames = {"domain_name", "endpoint_name"})
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@ -30,9 +32,16 @@ public class BypassApiConfig {
|
||||
* 도메인명 (패키지명/URL 경로)
|
||||
* 예: "ship-info", "port-data"
|
||||
*/
|
||||
@Column(name = "domain_name", unique = true, nullable = false, length = 50)
|
||||
@Column(name = "domain_name", nullable = false, length = 50)
|
||||
private String domainName;
|
||||
|
||||
/**
|
||||
* 엔드포인트명 (externalPath의 마지막 세그먼트에서 자동 추출)
|
||||
* 예: "CompliancesByImos", "CompanyCompliancesByImos"
|
||||
*/
|
||||
@Column(name = "endpoint_name", nullable = false, length = 100)
|
||||
private String endpointName;
|
||||
|
||||
/**
|
||||
* 표시명
|
||||
* 예: "선박 정보 API", "항만 데이터 API"
|
||||
@ -117,12 +126,28 @@ public class BypassApiConfig {
|
||||
|
||||
/**
|
||||
* 엔티티 저장 전 자동 호출 (INSERT 시)
|
||||
* endpointName이 null이면 externalPath에서 자동 추출 (마이그레이션 대응)
|
||||
*/
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
this.createdAt = now;
|
||||
this.updatedAt = now;
|
||||
if (this.endpointName == null || this.endpointName.isEmpty()) {
|
||||
this.endpointName = extractEndpointName(this.externalPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 엔티티 업데이트 전 자동 호출 (UPDATE 시)
|
||||
* endpointName이 null이면 externalPath에서 자동 추출 (마이그레이션 대응)
|
||||
*/
|
||||
private static String extractEndpointName(String externalPath) {
|
||||
if (externalPath == null || externalPath.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
String[] segments = externalPath.split("/");
|
||||
return segments[segments.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -131,5 +156,8 @@ public class BypassApiConfig {
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
if (this.endpointName == null || this.endpointName.isEmpty()) {
|
||||
this.endpointName = extractEndpointName(this.externalPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import com.snp.batch.global.model.BypassApiConfig;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
@ -14,12 +15,17 @@ import java.util.Optional;
|
||||
public interface BypassApiConfigRepository extends JpaRepository<BypassApiConfig, Long> {
|
||||
|
||||
/**
|
||||
* 도메인명으로 BYPASS API 설정 조회
|
||||
* 도메인명으로 BYPASS API 설정 단건 조회 (하위 호환)
|
||||
*/
|
||||
Optional<BypassApiConfig> findByDomainName(String domainName);
|
||||
|
||||
/**
|
||||
* 도메인명 존재 여부 확인
|
||||
* 도메인명으로 BYPASS API 설정 목록 조회 (ID 순)
|
||||
*/
|
||||
boolean existsByDomainName(String domainName);
|
||||
List<BypassApiConfig> findByDomainNameOrderById(String domainName);
|
||||
|
||||
/**
|
||||
* 도메인명 + 엔드포인트명 복합 유니크 존재 여부 확인
|
||||
*/
|
||||
boolean existsByDomainNameAndEndpointName(String domainName, String endpointName);
|
||||
}
|
||||
|
||||
@ -0,0 +1,122 @@
|
||||
package com.snp.batch.jobs.web.compliance.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ComplianceBypassDto {
|
||||
|
||||
@JsonProperty("dateAmended")
|
||||
private String dateAmended;
|
||||
|
||||
@JsonProperty("legalOverall")
|
||||
private Integer legalOverall;
|
||||
|
||||
@JsonProperty("lrimoShipNo")
|
||||
private String lrimoShipNo;
|
||||
|
||||
@JsonProperty("shipBESSanctionList")
|
||||
private Integer shipBESSanctionList;
|
||||
|
||||
@JsonProperty("shipDarkActivityIndicator")
|
||||
private Integer shipDarkActivityIndicator;
|
||||
|
||||
@JsonProperty("shipDetailsNoLongerMaintained")
|
||||
private Integer shipDetailsNoLongerMaintained;
|
||||
|
||||
@JsonProperty("shipEUSanctionList")
|
||||
private Integer shipEUSanctionList;
|
||||
|
||||
@JsonProperty("shipFlagDisputed")
|
||||
private Integer shipFlagDisputed;
|
||||
|
||||
@JsonProperty("shipFlagSanctionedCountry")
|
||||
private Integer shipFlagSanctionedCountry;
|
||||
|
||||
@JsonProperty("shipHistoricalFlagSanctionedCountry")
|
||||
private Integer shipHistoricalFlagSanctionedCountry;
|
||||
|
||||
@JsonProperty("shipOFACAdvisoryList")
|
||||
private Integer shipOFACAdvisoryList;
|
||||
|
||||
@JsonProperty("shipOFACNonSDNSanctionList")
|
||||
private Integer shipOFACNonSDNSanctionList;
|
||||
|
||||
@JsonProperty("shipOFACSanctionList")
|
||||
private Integer shipOFACSanctionList;
|
||||
|
||||
@JsonProperty("shipOwnerAustralianSanctionList")
|
||||
private Integer shipOwnerAustralianSanctionList;
|
||||
|
||||
@JsonProperty("shipOwnerBESSanctionList")
|
||||
private Integer shipOwnerBESSanctionList;
|
||||
|
||||
@JsonProperty("shipOwnerCanadianSanctionList")
|
||||
private Integer shipOwnerCanadianSanctionList;
|
||||
|
||||
@JsonProperty("shipOwnerEUSanctionList")
|
||||
private Integer shipOwnerEUSanctionList;
|
||||
|
||||
@JsonProperty("shipOwnerFATFJurisdiction")
|
||||
private Integer shipOwnerFATFJurisdiction;
|
||||
|
||||
@JsonProperty("shipOwnerHistoricalOFACSanctionedCountry")
|
||||
private Integer shipOwnerHistoricalOFACSanctionedCountry;
|
||||
|
||||
@JsonProperty("shipOwnerOFACSSIList")
|
||||
private Integer shipOwnerOFACSSIList;
|
||||
|
||||
@JsonProperty("shipOwnerOFACSanctionList")
|
||||
private Integer shipOwnerOFACSanctionList;
|
||||
|
||||
@JsonProperty("shipOwnerOFACSanctionedCountry")
|
||||
private Integer shipOwnerOFACSanctionedCountry;
|
||||
|
||||
@JsonProperty("shipOwnerParentCompanyNonCompliance")
|
||||
private Integer shipOwnerParentCompanyNonCompliance;
|
||||
|
||||
@JsonProperty("shipOwnerParentFATFJurisdiction")
|
||||
private String shipOwnerParentFATFJurisdiction;
|
||||
|
||||
@JsonProperty("shipOwnerParentOFACSanctionedCountry")
|
||||
private String shipOwnerParentOFACSanctionedCountry;
|
||||
|
||||
@JsonProperty("shipOwnerSwissSanctionList")
|
||||
private Integer shipOwnerSwissSanctionList;
|
||||
|
||||
@JsonProperty("shipOwnerUAESanctionList")
|
||||
private Integer shipOwnerUAESanctionList;
|
||||
|
||||
@JsonProperty("shipOwnerUNSanctionList")
|
||||
private Integer shipOwnerUNSanctionList;
|
||||
|
||||
@JsonProperty("shipSTSPartnerNonComplianceLast12m")
|
||||
private Integer shipSTSPartnerNonComplianceLast12m;
|
||||
|
||||
@JsonProperty("shipSanctionedCountryPortCallLast12m")
|
||||
private Integer shipSanctionedCountryPortCallLast12m;
|
||||
|
||||
@JsonProperty("shipSanctionedCountryPortCallLast3m")
|
||||
private Integer shipSanctionedCountryPortCallLast3m;
|
||||
|
||||
@JsonProperty("shipSanctionedCountryPortCallLast6m")
|
||||
private Integer shipSanctionedCountryPortCallLast6m;
|
||||
|
||||
@JsonProperty("shipSecurityLegalDisputeEvent")
|
||||
private Integer shipSecurityLegalDisputeEvent;
|
||||
|
||||
@JsonProperty("shipSwissSanctionList")
|
||||
private Integer shipSwissSanctionList;
|
||||
|
||||
@JsonProperty("shipUNSanctionList")
|
||||
private Integer shipUNSanctionList;
|
||||
|
||||
}
|
||||
@ -11,12 +11,15 @@ import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* BYPASS API Java 소스 코드를 자동 생성하는 서비스.
|
||||
* RiskBypassService / RiskController 패턴을 기반으로 DTO, Service, Controller를 생성합니다.
|
||||
* 같은 도메인의 N개 설정을 받아 N개의 DTO + Service와 1개의 Controller를 생성합니다.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@ -25,47 +28,59 @@ public class BypassCodeGenerator {
|
||||
private static final String BASE_PACKAGE = "com.snp.batch.jobs.web";
|
||||
|
||||
/**
|
||||
* BYPASS API 코드를 생성합니다.
|
||||
* 같은 도메인의 BYPASS API 코드를 생성합니다.
|
||||
* 엔드포인트별 DTO/Service를 각각 생성하고, Controller 1개에 모든 엔드포인트 메서드를 합칩니다.
|
||||
*
|
||||
* @param config 설정 정보
|
||||
* @param params 파라미터 목록
|
||||
* @param fields DTO 필드 목록
|
||||
* @param configs 같은 domainName을 가진 설정 목록
|
||||
* @param force 기존 파일 덮어쓰기 여부
|
||||
* @return 생성 결과
|
||||
*/
|
||||
public CodeGenerationResult generate(BypassApiConfig config,
|
||||
List<BypassApiParam> params,
|
||||
List<BypassApiField> fields,
|
||||
boolean force) {
|
||||
public CodeGenerationResult generate(List<BypassApiConfig> configs, boolean force) {
|
||||
if (configs == null || configs.isEmpty()) {
|
||||
throw new IllegalArgumentException("생성할 설정이 없습니다.");
|
||||
}
|
||||
|
||||
String projectRoot = System.getProperty("user.dir");
|
||||
String domain = config.getDomainName();
|
||||
String domainCapitalized = capitalize(domain);
|
||||
|
||||
String dtoCode = generateDtoCode(domain, domainCapitalized, fields);
|
||||
String serviceCode = generateServiceCode(domain, domainCapitalized, config, params);
|
||||
String controllerCode = generateControllerCode(domain, domainCapitalized, config, params);
|
||||
|
||||
String domain = configs.get(0).getDomainName();
|
||||
String basePath = projectRoot + "/src/main/java/com/snp/batch/jobs/web/" + domain;
|
||||
Path dtoPath = writeFile(basePath + "/dto/" + domainCapitalized + "BypassDto.java", dtoCode, force);
|
||||
Path servicePath = writeFile(basePath + "/service/" + domainCapitalized + "BypassService.java", serviceCode, force);
|
||||
Path controllerPath = writeFile(basePath + "/controller/" + domainCapitalized + "Controller.java", controllerCode, force);
|
||||
|
||||
log.info("코드 생성 완료 - domain: {}, dto: {}, service: {}, controller: {}",
|
||||
domain, dtoPath, servicePath, controllerPath);
|
||||
List<String> dtoPaths = new ArrayList<>();
|
||||
List<String> servicePaths = new ArrayList<>();
|
||||
|
||||
for (BypassApiConfig config : configs) {
|
||||
String endpointName = config.getEndpointName();
|
||||
|
||||
String dtoCode = generateDtoCode(domain, endpointName, config.getFields());
|
||||
String serviceCode = generateServiceCode(domain, endpointName, config, config.getParams());
|
||||
|
||||
Path dtoFilePath = writeFile(basePath + "/dto/" + endpointName + "Dto.java", dtoCode, force);
|
||||
Path serviceFilePath = writeFile(basePath + "/service/" + endpointName + "Service.java", serviceCode, force);
|
||||
|
||||
dtoPaths.add(dtoFilePath.toString());
|
||||
servicePaths.add(serviceFilePath.toString());
|
||||
}
|
||||
|
||||
String controllerCode = generateControllerCode(domain, configs);
|
||||
String domainCapitalized = capitalize(domain);
|
||||
Path controllerFilePath = writeFile(
|
||||
basePath + "/controller/" + domainCapitalized + "Controller.java", controllerCode, force);
|
||||
|
||||
log.info("코드 생성 완료 - domain: {}, endpoints: {}, controller: {}",
|
||||
domain, configs.stream().map(BypassApiConfig::getEndpointName).toList(), controllerFilePath);
|
||||
|
||||
return CodeGenerationResult.builder()
|
||||
.dtoPath(dtoPath.toString())
|
||||
.servicePath(servicePath.toString())
|
||||
.controllerPath(controllerPath.toString())
|
||||
.controllerPath(controllerFilePath.toString())
|
||||
.servicePaths(servicePaths)
|
||||
.dtoPaths(dtoPaths)
|
||||
.message("코드 생성 완료. 서버를 재시작하면 새 API가 활성화됩니다.")
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO 코드 생성.
|
||||
* 각 field에 대해 @JsonProperty + private 필드를 생성합니다.
|
||||
* endpointName 기반 클래스명: {EndpointName}Dto
|
||||
*/
|
||||
private String generateDtoCode(String domain, String domainCap, List<BypassApiField> fields) {
|
||||
private String generateDtoCode(String domain, String endpointName, List<BypassApiField> fields) {
|
||||
String packageName = BASE_PACKAGE + "." + domain + ".dto";
|
||||
boolean needsLocalDateTime = fields.stream()
|
||||
.anyMatch(f -> "LocalDateTime".equals(f.getFieldType()));
|
||||
@ -103,24 +118,26 @@ public class BypassCodeGenerator {
|
||||
"""
|
||||
.replace("{{PACKAGE}}", packageName)
|
||||
.replace("{{IMPORTS}}", imports.toString())
|
||||
.replace("{{CLASS_NAME}}", domainCap + "BypassDto")
|
||||
.replace("{{CLASS_NAME}}", endpointName + "Dto")
|
||||
.replace("{{FIELDS}}", fieldLines.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Service 코드 생성.
|
||||
* BaseBypassService를 상속하여 GET/POST, LIST/SINGLE 조합에 맞는 fetch 메서드를 생성합니다.
|
||||
* endpointName 기반 클래스명: {EndpointName}Service
|
||||
* BaseBypassService<{EndpointName}Dto>를 상속합니다.
|
||||
*/
|
||||
private String generateServiceCode(String domain, String domainCap,
|
||||
private String generateServiceCode(String domain, String endpointName,
|
||||
BypassApiConfig config, List<BypassApiParam> params) {
|
||||
String packageName = BASE_PACKAGE + "." + domain + ".service";
|
||||
String dtoPackage = BASE_PACKAGE + "." + domain + ".dto";
|
||||
String dtoClass = domainCap + "BypassDto";
|
||||
String dtoClass = endpointName + "Dto";
|
||||
String serviceClass = endpointName + "Service";
|
||||
boolean isList = "LIST".equalsIgnoreCase(config.getResponseType());
|
||||
boolean isPost = "POST".equalsIgnoreCase(config.getHttpMethod());
|
||||
|
||||
String returnType = isList ? "List<" + dtoClass + ">" : dtoClass;
|
||||
String methodName = "get" + domainCap + "Data";
|
||||
String methodName = "get" + endpointName + "Data";
|
||||
String fetchMethod = buildFetchMethodCall(config, params, isList, isPost);
|
||||
String methodParams = buildMethodParams(params);
|
||||
|
||||
@ -141,9 +158,9 @@ public class BypassCodeGenerator {
|
||||
* 외부 Maritime API에서 데이터를 실시간 조회하여 그대로 반환
|
||||
*/
|
||||
@Service
|
||||
public class {{DOMAIN_CAP}}BypassService extends BaseBypassService<{{DTO_CLASS}}> {
|
||||
public class {{SERVICE_CLASS}} extends BaseBypassService<{{DTO_CLASS}}> {
|
||||
|
||||
public {{DOMAIN_CAP}}BypassService(
|
||||
public {{SERVICE_CLASS}}(
|
||||
@Qualifier("{{WEBCLIENT_BEAN}}") WebClient webClient) {
|
||||
super(webClient, "{{EXTERNAL_PATH}}", "{{DISPLAY_NAME}}",
|
||||
new ParameterizedTypeReference<>() {},
|
||||
@ -164,7 +181,7 @@ public class BypassCodeGenerator {
|
||||
.replace("{{DTO_IMPORT}}", dtoPackage + "." + dtoClass)
|
||||
.replace("{{LIST_IMPORT}}", listImport)
|
||||
.replace("{{DISPLAY_NAME}}", config.getDisplayName())
|
||||
.replace("{{DOMAIN_CAP}}", domainCap)
|
||||
.replace("{{SERVICE_CLASS}}", serviceClass)
|
||||
.replace("{{DTO_CLASS}}", dtoClass)
|
||||
.replace("{{WEBCLIENT_BEAN}}", config.getWebclientBean())
|
||||
.replace("{{EXTERNAL_PATH}}", config.getExternalPath())
|
||||
@ -176,95 +193,126 @@ public class BypassCodeGenerator {
|
||||
|
||||
/**
|
||||
* Controller 코드 생성.
|
||||
* BaseBypassController를 상속하여 GET/POST 엔드포인트를 생성합니다.
|
||||
* 같은 도메인의 모든 설정을 합쳐 하나의 Controller에 N개의 엔드포인트 메서드를 생성합니다.
|
||||
*/
|
||||
private String generateControllerCode(String domain, String domainCap,
|
||||
BypassApiConfig config, List<BypassApiParam> params) {
|
||||
private String generateControllerCode(String domain, List<BypassApiConfig> configs) {
|
||||
String packageName = BASE_PACKAGE + "." + domain + ".controller";
|
||||
String dtoPackage = BASE_PACKAGE + "." + domain + ".dto";
|
||||
String servicePackage = BASE_PACKAGE + "." + domain + ".service";
|
||||
String dtoClass = domainCap + "BypassDto";
|
||||
String serviceClass = domainCap + "BypassService";
|
||||
String serviceField = Character.toLowerCase(serviceClass.charAt(0)) + serviceClass.substring(1);
|
||||
String dtoPackage = BASE_PACKAGE + "." + domain + ".dto";
|
||||
String domainCap = capitalize(domain);
|
||||
String requestMappingPath = "/api/" + domain;
|
||||
|
||||
// imports 합산 (중복 제거)
|
||||
Set<String> importSet = new LinkedHashSet<>();
|
||||
importSet.add("import com.snp.batch.common.web.ApiResponse;");
|
||||
importSet.add("import com.snp.batch.common.web.controller.BaseBypassController;");
|
||||
importSet.add("import io.swagger.v3.oas.annotations.Operation;");
|
||||
importSet.add("import io.swagger.v3.oas.annotations.Parameter;");
|
||||
importSet.add("import io.swagger.v3.oas.annotations.tags.Tag;");
|
||||
importSet.add("import lombok.RequiredArgsConstructor;");
|
||||
importSet.add("import org.springframework.http.ResponseEntity;");
|
||||
importSet.add("import org.springframework.web.bind.annotation.RequestMapping;");
|
||||
importSet.add("import org.springframework.web.bind.annotation.RestController;");
|
||||
|
||||
boolean anyList = configs.stream().anyMatch(c -> "LIST".equalsIgnoreCase(c.getResponseType()));
|
||||
boolean anyPost = configs.stream().anyMatch(c -> "POST".equalsIgnoreCase(c.getHttpMethod()));
|
||||
boolean anyGet = configs.stream().anyMatch(c -> !"POST".equalsIgnoreCase(c.getHttpMethod()));
|
||||
boolean anyPath = configs.stream()
|
||||
.anyMatch(c -> c.getParams().stream().anyMatch(p -> "PATH".equalsIgnoreCase(p.getParamIn())));
|
||||
boolean anyQuery = configs.stream()
|
||||
.anyMatch(c -> c.getParams().stream().anyMatch(p -> "QUERY".equalsIgnoreCase(p.getParamIn())));
|
||||
boolean anyBody = configs.stream()
|
||||
.anyMatch(c -> c.getParams().stream().anyMatch(p -> "BODY".equalsIgnoreCase(p.getParamIn())));
|
||||
|
||||
if (anyList) {
|
||||
importSet.add("import java.util.List;");
|
||||
}
|
||||
if (anyPost) {
|
||||
importSet.add("import org.springframework.web.bind.annotation.PostMapping;");
|
||||
}
|
||||
if (anyGet) {
|
||||
importSet.add("import org.springframework.web.bind.annotation.GetMapping;");
|
||||
}
|
||||
if (anyPath) {
|
||||
importSet.add("import org.springframework.web.bind.annotation.PathVariable;");
|
||||
}
|
||||
if (anyQuery) {
|
||||
importSet.add("import org.springframework.web.bind.annotation.RequestParam;");
|
||||
}
|
||||
if (anyBody) {
|
||||
importSet.add("import org.springframework.web.bind.annotation.RequestBody;");
|
||||
}
|
||||
|
||||
for (BypassApiConfig config : configs) {
|
||||
String endpointName = config.getEndpointName();
|
||||
importSet.add("import " + dtoPackage + "." + endpointName + "Dto;");
|
||||
importSet.add("import " + servicePackage + "." + endpointName + "Service;");
|
||||
}
|
||||
|
||||
String importsStr = importSet.stream().collect(Collectors.joining("\n"));
|
||||
|
||||
// 필드 선언부
|
||||
StringBuilder fields = new StringBuilder();
|
||||
for (BypassApiConfig config : configs) {
|
||||
String endpointName = config.getEndpointName();
|
||||
String serviceClass = endpointName + "Service";
|
||||
String serviceField = Character.toLowerCase(serviceClass.charAt(0)) + serviceClass.substring(1);
|
||||
fields.append(" private final ").append(serviceClass).append(" ").append(serviceField).append(";\n");
|
||||
}
|
||||
|
||||
// 태그 description은 첫 번째 config의 webclientBean 기준
|
||||
String tagPrefix = getTagPrefix(configs.get(0).getWebclientBean());
|
||||
String tagDescription = tagPrefix + " " + domainCap + " bypass API";
|
||||
|
||||
// 엔드포인트 메서드 목록
|
||||
StringBuilder methods = new StringBuilder();
|
||||
for (BypassApiConfig config : configs) {
|
||||
String endpointName = config.getEndpointName();
|
||||
String dtoClass = endpointName + "Dto";
|
||||
String serviceClass = endpointName + "Service";
|
||||
String serviceField = Character.toLowerCase(serviceClass.charAt(0)) + serviceClass.substring(1);
|
||||
boolean isList = "LIST".equalsIgnoreCase(config.getResponseType());
|
||||
boolean isPost = "POST".equalsIgnoreCase(config.getHttpMethod());
|
||||
|
||||
String responseGeneric = isList ? "List<" + dtoClass + ">" : dtoClass;
|
||||
String mappingAnnotation = isPost ? "@PostMapping" : "@GetMapping";
|
||||
String listImport = isList ? "import java.util.List;\n" : "";
|
||||
String mappingPath = buildMappingPath(config.getParams(), config.getExternalPath());
|
||||
String paramAnnotations = buildControllerParamAnnotations(config.getParams());
|
||||
String serviceCallArgs = buildServiceCallArgs(config.getParams());
|
||||
String methodName = "get" + endpointName + "Data";
|
||||
|
||||
String paramAnnotations = buildControllerParamAnnotations(params);
|
||||
String methodParams = buildMethodParams(params);
|
||||
String serviceCallArgs = buildServiceCallArgs(params);
|
||||
String pathVariableImport = params.stream().anyMatch(p -> "PATH".equalsIgnoreCase(p.getParamIn()))
|
||||
? "import org.springframework.web.bind.annotation.PathVariable;\n" : "";
|
||||
String requestParamImport = params.stream().anyMatch(p -> "QUERY".equalsIgnoreCase(p.getParamIn()))
|
||||
? "import org.springframework.web.bind.annotation.RequestParam;\n" : "";
|
||||
String requestBodyImport = isPost
|
||||
? "import org.springframework.web.bind.annotation.RequestBody;\n" : "";
|
||||
String mappingImport = isPost
|
||||
? "import org.springframework.web.bind.annotation.PostMapping;\n"
|
||||
: "import org.springframework.web.bind.annotation.GetMapping;\n";
|
||||
|
||||
String mappingPath = buildMappingPath(params, config.getExternalPath());
|
||||
String requestMappingPath = "/api/" + domain;
|
||||
|
||||
return """
|
||||
package {{PACKAGE}};
|
||||
|
||||
import {{DTO_IMPORT}};
|
||||
import {{SERVICE_IMPORT}};
|
||||
import com.snp.batch.common.web.ApiResponse;
|
||||
import com.snp.batch.common.web.controller.BaseBypassController;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
{{LIST_IMPORT}}import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
{{MAPPING_IMPORT}}{{PATH_VARIABLE_IMPORT}}{{REQUEST_PARAM_IMPORT}}{{REQUEST_BODY_IMPORT}}
|
||||
/**
|
||||
* {{DISPLAY_NAME}} bypass API
|
||||
* S&P Maritime API에서 데이터를 실시간 조회하여 그대로 반환
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("{{REQUEST_MAPPING_PATH}}")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "{{DOMAIN_CAP}}", description = "{{TAG_PREFIX}} {{DISPLAY_NAME}} bypass API")
|
||||
public class {{DOMAIN_CAP}}Controller extends BaseBypassController {
|
||||
|
||||
private final {{SERVICE_CLASS}} {{SERVICE_FIELD}};
|
||||
|
||||
@Operation(
|
||||
summary = "{{DISPLAY_NAME}} 조회",
|
||||
description = "S&P API에서 {{DISPLAY_NAME}} 데이터를 요청하고 응답을 그대로 반환합니다."
|
||||
)
|
||||
{{MAPPING_ANNOTATION}}
|
||||
public ResponseEntity<ApiResponse<{{RESPONSE_GENERIC}}>> get{{DOMAIN_CAP}}Data({{PARAM_ANNOTATIONS}}) {
|
||||
return execute(() -> {{SERVICE_FIELD}}.get{{DOMAIN_CAP}}Data({{SERVICE_CALL_ARGS}}));
|
||||
methods.append("\n");
|
||||
methods.append(" @Operation(\n");
|
||||
methods.append(" summary = \"").append(config.getDisplayName()).append(" 조회\",\n");
|
||||
methods.append(" description = \"S&P API에서 ").append(config.getDisplayName())
|
||||
.append(" 데이터를 요청하고 응답을 그대로 반환합니다.\"\n");
|
||||
methods.append(" )\n");
|
||||
methods.append(" ").append(mappingAnnotation).append(mappingPath).append("\n");
|
||||
methods.append(" public ResponseEntity<ApiResponse<").append(responseGeneric).append(">> ")
|
||||
.append(methodName).append("(");
|
||||
if (!paramAnnotations.isEmpty()) {
|
||||
methods.append(paramAnnotations);
|
||||
}
|
||||
methods.append(") {\n");
|
||||
methods.append(" return execute(() -> ").append(serviceField)
|
||||
.append(".").append(methodName).append("(").append(serviceCallArgs).append("));\n");
|
||||
methods.append(" }\n");
|
||||
}
|
||||
"""
|
||||
.replace("{{PACKAGE}}", packageName)
|
||||
.replace("{{DTO_IMPORT}}", dtoPackage + "." + dtoClass)
|
||||
.replace("{{SERVICE_IMPORT}}", servicePackage + "." + serviceClass)
|
||||
.replace("{{LIST_IMPORT}}", listImport)
|
||||
.replace("{{PATH_VARIABLE_IMPORT}}", pathVariableImport)
|
||||
.replace("{{REQUEST_PARAM_IMPORT}}", requestParamImport)
|
||||
.replace("{{REQUEST_BODY_IMPORT}}", requestBodyImport)
|
||||
.replace("{{MAPPING_IMPORT}}", mappingImport)
|
||||
.replace("{{TAG_PREFIX}}", getTagPrefix(config.getWebclientBean()))
|
||||
.replace("{{DISPLAY_NAME}}", config.getDisplayName())
|
||||
.replace("{{DOMAIN_CAP}}", domainCap)
|
||||
.replace("{{REQUEST_MAPPING_PATH}}", requestMappingPath)
|
||||
.replace("{{SERVICE_CLASS}}", serviceClass)
|
||||
.replace("{{SERVICE_FIELD}}", serviceField)
|
||||
.replace("{{MAPPING_ANNOTATION}}", mappingAnnotation + mappingPath)
|
||||
.replace("{{RESPONSE_GENERIC}}", responseGeneric)
|
||||
.replace("{{PARAM_ANNOTATIONS}}", paramAnnotations.isEmpty() ? "" : paramAnnotations)
|
||||
.replace("{{SERVICE_CALL_ARGS}}", serviceCallArgs);
|
||||
|
||||
return "package " + packageName + ";\n\n"
|
||||
+ importsStr + "\n\n"
|
||||
+ "/**\n"
|
||||
+ " * " + domainCap + " bypass API\n"
|
||||
+ " * S&P Maritime API에서 데이터를 실시간 조회하여 그대로 반환\n"
|
||||
+ " */\n"
|
||||
+ "@RestController\n"
|
||||
+ "@RequestMapping(\"" + requestMappingPath + "\")\n"
|
||||
+ "@RequiredArgsConstructor\n"
|
||||
+ "@Tag(name = \"" + domainCap + "\", description = \"" + tagDescription + "\")\n"
|
||||
+ "public class " + domainCap + "Controller extends BaseBypassController {\n\n"
|
||||
+ fields
|
||||
+ methods
|
||||
+ "}\n";
|
||||
}
|
||||
|
||||
/**
|
||||
@ -355,7 +403,6 @@ public class BypassCodeGenerator {
|
||||
* PATH 파라미터 imo 추가 시 → ("/CompliancesByImos/{imo}")
|
||||
*/
|
||||
private String buildMappingPath(List<BypassApiParam> params, String externalPath) {
|
||||
// externalPath에서 마지막 세그먼트 추출
|
||||
String endpointSegment = "";
|
||||
if (externalPath != null && !externalPath.isEmpty()) {
|
||||
String[] segments = externalPath.split("/");
|
||||
@ -364,7 +411,6 @@ public class BypassCodeGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
// PATH 파라미터 추가
|
||||
List<BypassApiParam> pathParams = params.stream()
|
||||
.filter(p -> "PATH".equalsIgnoreCase(p.getParamIn()))
|
||||
.sorted((a, b) -> Integer.compare(a.getSortOrder(), b.getSortOrder()))
|
||||
|
||||
@ -57,8 +57,10 @@ public class BypassConfigService {
|
||||
*/
|
||||
@Transactional
|
||||
public BypassConfigResponse createConfig(BypassConfigRequest request) {
|
||||
if (configRepository.existsByDomainName(request.getDomainName())) {
|
||||
throw new IllegalArgumentException("이미 존재하는 도메인입니다: " + request.getDomainName());
|
||||
String endpointName = extractEndpointName(request.getExternalPath());
|
||||
if (configRepository.existsByDomainNameAndEndpointName(request.getDomainName(), endpointName)) {
|
||||
throw new IllegalArgumentException(
|
||||
"이미 존재하는 도메인+엔드포인트 조합입니다: " + request.getDomainName() + "/" + endpointName);
|
||||
}
|
||||
BypassApiConfig config = toEntity(request);
|
||||
|
||||
@ -155,6 +157,7 @@ public class BypassConfigService {
|
||||
return BypassConfigResponse.builder()
|
||||
.id(config.getId())
|
||||
.domainName(config.getDomainName())
|
||||
.endpointName(config.getEndpointName())
|
||||
.displayName(config.getDisplayName())
|
||||
.webclientBean(config.getWebclientBean())
|
||||
.externalPath(config.getExternalPath())
|
||||
@ -173,6 +176,7 @@ public class BypassConfigService {
|
||||
private BypassApiConfig toEntity(BypassConfigRequest request) {
|
||||
return BypassApiConfig.builder()
|
||||
.domainName(request.getDomainName())
|
||||
.endpointName(extractEndpointName(request.getExternalPath()))
|
||||
.displayName(request.getDisplayName())
|
||||
.webclientBean(request.getWebclientBean())
|
||||
.externalPath(request.getExternalPath())
|
||||
@ -182,6 +186,18 @@ public class BypassConfigService {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* externalPath의 마지막 세그먼트를 endpointName으로 추출
|
||||
* 예: "/RiskAndCompliance/CompliancesByImos" → "CompliancesByImos"
|
||||
*/
|
||||
private String extractEndpointName(String externalPath) {
|
||||
if (externalPath == null || externalPath.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
String[] segments = externalPath.split("/");
|
||||
return segments[segments.length - 1];
|
||||
}
|
||||
|
||||
private BypassApiParam toParamEntity(BypassParamDto dto) {
|
||||
return BypassApiParam.builder()
|
||||
.paramName(dto.getParamName())
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user