feat(swagger): Swagger 응답 스키마 자동 생성 및 API 문서 개선 (#14) #15

병합
HYOJIN feature/ISSUE-14-swagger-response-example 에서 develop 로 2 commits 를 머지했습니다 2026-04-09 15:43:17 +09:00
30개의 변경된 파일29442개의 추가작업 그리고 45개의 파일을 삭제
Showing only changes of commit fab03a31bb - Show all commits

파일 보기

@ -39,6 +39,7 @@ export interface BypassConfigResponse {
description: string;
generated: boolean;
generatedAt: string | null;
responseSchemaClass: string | null;
createdAt: string;
updatedAt: string;
params: BypassParamDto[];

파일 보기

@ -14,6 +14,7 @@ interface BypassConfig {
domainName: string;
endpointName: string;
displayName: string;
webclientBean: string;
httpMethod: string;
externalPath: string;
description: string;
@ -36,15 +37,18 @@ const METHOD_COLORS: Record<string, string> = {
DELETE: 'bg-red-100 text-red-700',
};
const SWAGGER_BASE = '/snp-global/swagger-ui/index.html?urls.primaryName=3.%20Bypass%20API';
const SWAGGER_GROUP_MAP: Record<string, string> = {
maritimeApiWebClient: '3-1.%20Ships%20API',
maritimeAisApiWebClient: '3-2.%20AIS%20API',
maritimeServiceApiWebClient: '3-3.%20Web%20Services%20API',
};
function buildSwaggerDeepLink(config: BypassConfig): string {
// Swagger UI deep link: #/{Tag}/{operationId}
// Tag = domainName 첫글자 대문자 (예: compliance → Compliance)
// operationId = get{EndpointName}Data (SpringDoc 기본 패턴)
const group = SWAGGER_GROUP_MAP[config.webclientBean] ?? '3-1.%20Ships%20API';
const base = `/snp-global/swagger-ui/index.html?urls.primaryName=${group}`;
const tag = config.domainName.charAt(0).toUpperCase() + config.domainName.slice(1);
const operationId = `get${config.endpointName}Data`;
return `${SWAGGER_BASE}#/${tag}/${operationId}`;
return `${base}#/${tag}/${operationId}`;
}
export default function BypassCatalog() {
@ -98,7 +102,7 @@ export default function BypassCatalog() {
</p>
</div>
<a
href={SWAGGER_BASE}
href="/snp-global/swagger-ui/index.html"
target="_blank"
rel="noopener noreferrer"
className="px-4 py-2 text-sm font-medium text-white bg-wing-accent hover:bg-wing-accent/80 rounded-lg transition-colors no-underline"

파일 보기

@ -5,6 +5,7 @@ import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.servers.Server;
import io.swagger.v3.oas.models.tags.Tag;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@ -12,6 +13,8 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Swagger/OpenAPI 3.0 설정
@ -41,17 +44,16 @@ public class SwaggerConfig {
@ConditionalOnProperty(name = "app.environment", havingValue = "dev", matchIfMissing = true)
public GroupedOpenApi bypassConfigApi() {
return GroupedOpenApi.builder()
.group("2. Bypass Config")
.group("2. API Config")
.pathsToMatch("/api/bypass-config/**")
.addOpenApiCustomizer(openApi -> openApi.info(new Info()
.title("Bypass Config API")
.description("Bypass API 설정 및 코드 생성 관리 API")
.title("API Config")
.description("API 설정 및 코드 생성 관리")
.version("v1.0.0")))
.build();
}
@Bean
@ConditionalOnProperty(name = "app.environment", havingValue = "dev", matchIfMissing = true)
public GroupedOpenApi screeningGuideApi() {
return GroupedOpenApi.builder()
.group("4. Screening Guide")
@ -64,16 +66,59 @@ public class SwaggerConfig {
}
@Bean
public GroupedOpenApi bypassApi() {
public GroupedOpenApi shipsBypassApi() {
return createBypassApiGroup("3-1. Ships API", "[Ship API]",
"S&P Global Ships/Events/PSC 데이터를 제공합니다.");
}
@Bean
public GroupedOpenApi aisBypassApi() {
return createBypassApiGroup("3-2. AIS API", "[AIS API]",
"S&P Global AIS(선박위치추적) 데이터를 제공합니다.");
}
@Bean
public GroupedOpenApi servicesBypassApi() {
return createBypassApiGroup("3-3. Web Services API", "[Service API]",
"S&P Global Maritime Web Services 데이터를 제공합니다.");
}
private GroupedOpenApi createBypassApiGroup(String group, String tagPrefix, String description) {
return GroupedOpenApi.builder()
.group("3. Bypass API")
.group(group)
.pathsToMatch("/api/**")
.pathsToExclude("/api/bypass-config/**", "/api/screening-guide/**", "/api/bypass-account/**")
.addOpenApiCustomizer(openApi -> {
openApi.info(new Info()
.title("Bypass API")
.description("S&P Global 선박/해운 데이터를 제공합니다.")
.title(group)
.description(description)
.version("v1.0.0"));
// Tag description에 prefix가 포함된 것만 필터링
if (openApi.getTags() != null) {
Set<String> matchingTags = openApi.getTags().stream()
.filter(tag -> tag.getDescription() != null
&& tag.getDescription().startsWith(tagPrefix))
.map(Tag::getName)
.collect(Collectors.toSet());
if (openApi.getPaths() != null) {
if (matchingTags.isEmpty()) {
// 매칭 태그가 없으면 모든 경로 제거
openApi.getPaths().clear();
} else {
openApi.getPaths().entrySet().removeIf(entry -> {
var pathItem = entry.getValue();
return pathItem.readOperations().stream()
.noneMatch(op -> op.getTags() != null
&& op.getTags().stream().anyMatch(matchingTags::contains));
});
}
}
// 매칭되지 않는 태그 제거
openApi.getTags().removeIf(tag -> !matchingTags.contains(tag.getName()));
}
})
.build();
}
@ -82,11 +127,11 @@ public class SwaggerConfig {
@ConditionalOnProperty(name = "app.environment", havingValue = "dev", matchIfMissing = true)
public GroupedOpenApi bypassAccountApi() {
return GroupedOpenApi.builder()
.group("5. Bypass Account")
.group("5. Account Management")
.pathsToMatch("/api/bypass-account/**")
.addOpenApiCustomizer(openApi -> openApi.info(new Info()
.title("Bypass Account Management API")
.description("Bypass API 계정 및 신청 관리 API")
.title("Account Management API")
.description("API 계정 및 신청 관리")
.version("v1.0.0")))
.build();
}
@ -123,10 +168,10 @@ public class SwaggerConfig {
S&P Global Maritime 데이터 서비스 REST API 문서입니다.
### 제공 API
- **Bypass API**: S&P Global 선박/해운 데이터 조회
- **Bypass Config API**: Bypass API 설정 관리
- **Ships / AIS / Web Services API**: S&P Global 선박/해운 데이터 조회
- **API Config**: API 설정 관리
- **Screening Guide API**: Risk & Compliance 스크리닝 가이드
- **Bypass Account API**: API 계정 관리
- **Account Management API**: API 계정 관리
### 버전 정보
- API Version: v1.0.0

파일 보기

@ -34,6 +34,9 @@ public class BypassConfigRequest {
/** 설명 */
private String description;
/** Swagger 응답 스키마 DTO 클래스 (FQCN) */
private String responseSchemaClass;
/** 파라미터 목록 */
private List<BypassParamDto> params;
}

파일 보기

@ -40,6 +40,9 @@ public class BypassConfigResponse {
/** 설명 */
private String description;
/** Swagger 응답 스키마 DTO 클래스 (FQCN) */
private String responseSchemaClass;
/** 코드 생성 완료 여부 */
private Boolean generated;

파일 보기

@ -0,0 +1,19 @@
package com.snp.batch.global.dto.bypass;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
/**
* S&P Global API 응답 스키마 (Swagger 문서용)
* 클래스는 자동 생성되었습니다. 직접 수정하지 마세요.
*/
@Getter
@Schema(description = "APSShipResult")
public class APSShipResult {
@Schema(description = "apsStatus", example = "")
private String apsStatus;
@Schema(description = "shipCount", example = "0")
private Integer shipCount;
@Schema(description = "apsShipDetail", example = "")
private String apsShipDetail;
}

파일 보기

@ -0,0 +1,83 @@
package com.snp.batch.global.dto.bypass;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
/**
* S&P Global API 응답 스키마 (Swagger 문서용)
* 클래스는 자동 생성되었습니다. 직접 수정하지 마세요.
*/
@Getter
@Schema(description = "ComplianceDetails")
public class ComplianceDetails {
@Schema(description = "lrimoShipNo", example = "")
private String lrimoShipNo;
@Schema(description = "dateAmended", example = "")
private String dateAmended;
@Schema(description = "legalOverall", example = "0")
private Integer legalOverall;
@Schema(description = "shipBESSanctionList", example = "0")
private Integer shipBESSanctionList;
@Schema(description = "shipDarkActivityIndicator", example = "0")
private Integer shipDarkActivityIndicator;
@Schema(description = "shipDetailsNoLongerMaintained", example = "0")
private Integer shipDetailsNoLongerMaintained;
@Schema(description = "shipEUSanctionList", example = "0")
private Integer shipEUSanctionList;
@Schema(description = "shipFlagDisputed", example = "0")
private Integer shipFlagDisputed;
@Schema(description = "shipFlagSanctionedCountry", example = "0")
private Integer shipFlagSanctionedCountry;
@Schema(description = "shipHistoricalFlagSanctionedCountry", example = "0")
private Integer shipHistoricalFlagSanctionedCountry;
@Schema(description = "shipOFACNonSDNSanctionList", example = "0")
private Integer shipOFACNonSDNSanctionList;
@Schema(description = "shipOFACSanctionList", example = "0")
private Integer shipOFACSanctionList;
@Schema(description = "shipOFACAdvisoryList", example = "0")
private Integer shipOFACAdvisoryList;
@Schema(description = "shipOwnerOFACSSIList", example = "0")
private Integer shipOwnerOFACSSIList;
@Schema(description = "shipOwnerAustralianSanctionList", example = "0")
private Integer shipOwnerAustralianSanctionList;
@Schema(description = "shipOwnerBESSanctionList", example = "0")
private Integer shipOwnerBESSanctionList;
@Schema(description = "shipOwnerCanadianSanctionList", example = "0")
private Integer shipOwnerCanadianSanctionList;
@Schema(description = "shipOwnerEUSanctionList", example = "0")
private Integer shipOwnerEUSanctionList;
@Schema(description = "shipOwnerFATFJurisdiction", example = "0")
private Integer shipOwnerFATFJurisdiction;
@Schema(description = "shipOwnerHistoricalOFACSanctionedCountry", example = "0")
private Integer shipOwnerHistoricalOFACSanctionedCountry;
@Schema(description = "shipOwnerOFACSanctionList", example = "0")
private Integer shipOwnerOFACSanctionList;
@Schema(description = "shipOwnerOFACSanctionedCountry", example = "0")
private Integer shipOwnerOFACSanctionedCountry;
@Schema(description = "shipOwnerParentCompanyNonCompliance", example = "0")
private Integer shipOwnerParentCompanyNonCompliance;
@Schema(description = "shipOwnerParentFATFJurisdiction", example = "0")
private Integer shipOwnerParentFATFJurisdiction;
@Schema(description = "shipOwnerParentOFACSanctionedCountry", example = "0")
private Integer shipOwnerParentOFACSanctionedCountry;
@Schema(description = "shipOwnerSwissSanctionList", example = "0")
private Integer shipOwnerSwissSanctionList;
@Schema(description = "shipOwnerUAESanctionList", example = "0")
private Integer shipOwnerUAESanctionList;
@Schema(description = "shipOwnerUNSanctionList", example = "0")
private Integer shipOwnerUNSanctionList;
@Schema(description = "shipSanctionedCountryPortCallLast12m", example = "0")
private Integer shipSanctionedCountryPortCallLast12m;
@Schema(description = "shipSanctionedCountryPortCallLast3m", example = "0")
private Integer shipSanctionedCountryPortCallLast3m;
@Schema(description = "shipSanctionedCountryPortCallLast6m", example = "0")
private Integer shipSanctionedCountryPortCallLast6m;
@Schema(description = "shipSecurityLegalDisputeEvent", example = "0")
private Integer shipSecurityLegalDisputeEvent;
@Schema(description = "shipSTSPartnerNonComplianceLast12m", example = "0")
private Integer shipSTSPartnerNonComplianceLast12m;
@Schema(description = "shipSwissSanctionList", example = "0")
private Integer shipSwissSanctionList;
@Schema(description = "shipUNSanctionList", example = "0")
private Integer shipUNSanctionList;
}

파일 보기

@ -0,0 +1,99 @@
package com.snp.batch.global.dto.bypass;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
/**
* S&P Global API 응답 스키마 (Swagger 문서용)
* 클래스는 자동 생성되었습니다. 직접 수정하지 마세요.
*/
@Getter
@Schema(description = "PortFacility")
public class PortFacility {
@Schema(description = "port_ID", example = "0")
private Integer port_ID;
@Schema(description = "old_ID", example = "")
private String old_ID;
@Schema(description = "status", example = "")
private String status;
@Schema(description = "port_Name", example = "")
private String port_Name;
@Schema(description = "unlocode", example = "")
private String unlocode;
@Schema(description = "countryCode", example = "")
private String countryCode;
@Schema(description = "country_Name", example = "")
private String country_Name;
@Schema(description = "dec_Lat", example = "0.0")
private Double dec_Lat;
@Schema(description = "dec_Long", example = "0.0")
private Double dec_Long;
@Schema(description = "position", example = "")
private String position;
@Schema(description = "time_Zone", example = "")
private String time_Zone;
@Schema(description = "dayLight_Saving_Time", example = "false")
private Boolean dayLight_Saving_Time;
@Schema(description = "maximum_Draft", example = "0.0")
private Double maximum_Draft;
@Schema(description = "breakbulk_Facilities", example = "false")
private Boolean breakbulk_Facilities;
@Schema(description = "container_Facilities", example = "false")
private Boolean container_Facilities;
@Schema(description = "dry_Bulk_Facilities", example = "false")
private Boolean dry_Bulk_Facilities;
@Schema(description = "liquid_Facilities", example = "false")
private Boolean liquid_Facilities;
@Schema(description = "roRo_Facilities", example = "false")
private Boolean roRo_Facilities;
@Schema(description = "passenger_Facilities", example = "false")
private Boolean passenger_Facilities;
@Schema(description = "dry_Dock_Facilities", example = "false")
private Boolean dry_Dock_Facilities;
@Schema(description = "lpG_Facilities", example = "0")
private Integer lpG_Facilities;
@Schema(description = "lnG_Facilities", example = "0")
private Integer lnG_Facilities;
@Schema(description = "ispS_Compliant", example = "false")
private Boolean ispS_Compliant;
@Schema(description = "csI_Compliant", example = "false")
private Boolean csI_Compliant;
@Schema(description = "last_Update", example = "")
private String last_Update;
@Schema(description = "entry_Date", example = "")
private String entry_Date;
@Schema(description = "region_Name", example = "")
private String region_Name;
@Schema(description = "continent_Name", example = "")
private String continent_Name;
@Schema(description = "master_POID", example = "")
private String master_POID;
@Schema(description = "wS_Port", example = "0")
private Integer wS_Port;
@Schema(description = "max_LOA", example = "0.0")
private Double max_LOA;
@Schema(description = "max_Beam", example = "0.0")
private Double max_Beam;
@Schema(description = "max_DWT", example = "0")
private Integer max_DWT;
@Schema(description = "max_Offshore_Draught", example = "0.0")
private Double max_Offshore_Draught;
@Schema(description = "max_Offshore_LOA", example = "0.0")
private Double max_Offshore_LOA;
@Schema(description = "max_Offshore_BCM", example = "0.0")
private Double max_Offshore_BCM;
@Schema(description = "max_Offshore_DWT", example = "0.0")
private Double max_Offshore_DWT;
@Schema(description = "lnG_Bunker", example = "false")
private Boolean lnG_Bunker;
@Schema(description = "dO_Bunker", example = "false")
private Boolean dO_Bunker;
@Schema(description = "fO_Bunker", example = "false")
private Boolean fO_Bunker;
@Schema(description = "free_Trade_Zone", example = "false")
private Boolean free_Trade_Zone;
@Schema(description = "ecO_Port", example = "false")
private Boolean ecO_Port;
@Schema(description = "emission_Control_Area", example = "false")
private Boolean emission_Control_Area;
}

파일 보기

@ -0,0 +1,97 @@
package com.snp.batch.global.dto.bypass;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
/**
* S&P Global API 응답 스키마 (Swagger 문서용)
* 클래스는 자동 생성되었습니다. 직접 수정하지 마세요.
*/
@Getter
@Schema(description = "RiskDetails")
public class RiskDetails {
@Schema(description = "lrno", example = "")
private String lrno;
@Schema(description = "lastUpdated", example = "")
private String lastUpdated;
@Schema(description = "riskDataMaintained", example = "0")
private Integer riskDataMaintained;
@Schema(description = "daysSinceLastSeenOnAIS", example = "0")
private Integer daysSinceLastSeenOnAIS;
@Schema(description = "daysUnderAIS", example = "0")
private Integer daysUnderAIS;
@Schema(description = "imoCorrectOnAIS", example = "0")
private Integer imoCorrectOnAIS;
@Schema(description = "sailingUnderName", example = "0")
private Integer sailingUnderName;
@Schema(description = "anomalousMessagesFromMMSI", example = "0")
private Integer anomalousMessagesFromMMSI;
@Schema(description = "mostRecentDarkActivity", example = "0")
private Integer mostRecentDarkActivity;
@Schema(description = "portCalls", example = "0")
private Integer portCalls;
@Schema(description = "portRisk", example = "0")
private Integer portRisk;
@Schema(description = "stsOperations", example = "0")
private Integer stsOperations;
@Schema(description = "driftingHighSeas", example = "0")
private Integer driftingHighSeas;
@Schema(description = "riskEvents", example = "0")
private Integer riskEvents;
@Schema(description = "flagChanges", example = "0")
private Integer flagChanges;
@Schema(description = "flagParisMOUPerformance", example = "0")
private Integer flagParisMOUPerformance;
@Schema(description = "flagTokyoMOUPeformance", example = "0")
private Integer flagTokyoMOUPeformance;
@Schema(description = "flagUSCGMOUPerformance", example = "0")
private Integer flagUSCGMOUPerformance;
@Schema(description = "uscgQualship21", example = "0")
private Integer uscgQualship21;
@Schema(description = "timeSincePSCInspection", example = "0")
private Integer timeSincePSCInspection;
@Schema(description = "pscInspections", example = "0")
private Integer pscInspections;
@Schema(description = "pscDefects", example = "0")
private Integer pscDefects;
@Schema(description = "pscDetentions", example = "0")
private Integer pscDetentions;
@Schema(description = "currentSMCCertificate", example = "0")
private Integer currentSMCCertificate;
@Schema(description = "docChanges", example = "0")
private Integer docChanges;
@Schema(description = "currentClass", example = "0")
private Integer currentClass;
@Schema(description = "classStatusChanges", example = "0")
private Integer classStatusChanges;
@Schema(description = "pandICoverage", example = "0")
private Integer pandICoverage;
@Schema(description = "nameChanges", example = "0")
private Integer nameChanges;
@Schema(description = "gboChanges", example = "0")
private Integer gboChanges;
@Schema(description = "ageOfShip", example = "0")
private Integer ageOfShip;
@Schema(description = "iuuFishingViolation", example = "0")
private Integer iuuFishingViolation;
@Schema(description = "draughtChanges", example = "0")
private Integer draughtChanges;
@Schema(description = "mostRecentSanctionedPortCall", example = "0")
private Integer mostRecentSanctionedPortCall;
@Schema(description = "singleShipOperation", example = "0")
private Integer singleShipOperation;
@Schema(description = "fleetSafety", example = "0")
private Integer fleetSafety;
@Schema(description = "fleetPSC", example = "0")
private Integer fleetPSC;
@Schema(description = "specialSurveyOverdue", example = "0")
private Integer specialSurveyOverdue;
@Schema(description = "ownerUnknown", example = "0")
private Integer ownerUnknown;
@Schema(description = "russianPortCall", example = "0")
private Integer russianPortCall;
@Schema(description = "russianOwnerRegistration", example = "0")
private Integer russianOwnerRegistration;
@Schema(description = "russianSTS", example = "0")
private Integer russianSTS;
}

파일 보기

@ -0,0 +1,181 @@
package com.snp.batch.global.dto.bypass;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
/**
* S&P Global API 응답 스키마 (Swagger 문서용)
* 클래스는 자동 생성되었습니다. 직접 수정하지 마세요.
*/
@Getter
@Schema(description = "RiskWithNarrativesDetails")
public class RiskWithNarrativesDetails {
@Schema(description = "lrno", example = "")
private String lrno;
@Schema(description = "lastUpdated", example = "")
private String lastUpdated;
@Schema(description = "riskDataMaintained", example = "0")
private Integer riskDataMaintained;
@Schema(description = "daysSinceLastSeenOnAIS", example = "0")
private Integer daysSinceLastSeenOnAIS;
@Schema(description = "daysSinceLastSeenOnAISNarrative", example = "")
private String daysSinceLastSeenOnAISNarrative;
@Schema(description = "daysUnderAIS", example = "0")
private Integer daysUnderAIS;
@Schema(description = "daysUnderAISNarrative", example = "")
private String daysUnderAISNarrative;
@Schema(description = "imoCorrectOnAIS", example = "0")
private Integer imoCorrectOnAIS;
@Schema(description = "imoCorrectOnAISNarrative", example = "")
private String imoCorrectOnAISNarrative;
@Schema(description = "sailingUnderName", example = "0")
private Integer sailingUnderName;
@Schema(description = "sailingUnderNameNarrative", example = "")
private String sailingUnderNameNarrative;
@Schema(description = "anomalousMessagesFromMMSI", example = "0")
private Integer anomalousMessagesFromMMSI;
@Schema(description = "anomalousMessagesFromMMSINarrative", example = "")
private String anomalousMessagesFromMMSINarrative;
@Schema(description = "mostRecentDarkActivity", example = "0")
private Integer mostRecentDarkActivity;
@Schema(description = "mostRecentDarkActivityNarrative", example = "")
private String mostRecentDarkActivityNarrative;
@Schema(description = "portCalls", example = "0")
private Integer portCalls;
@Schema(description = "portCallsNarrative", example = "")
private String portCallsNarrative;
@Schema(description = "portRisk", example = "0")
private Integer portRisk;
@Schema(description = "portRiskNarrative", example = "")
private String portRiskNarrative;
@Schema(description = "stsOperations", example = "0")
private Integer stsOperations;
@Schema(description = "stsOperationsNarrative", example = "")
private String stsOperationsNarrative;
@Schema(description = "driftingHighSeas", example = "0")
private Integer driftingHighSeas;
@Schema(description = "driftingHighSeasNarrative", example = "")
private String driftingHighSeasNarrative;
@Schema(description = "riskEvents", example = "0")
private Integer riskEvents;
@Schema(description = "riskEventNarrative", example = "")
private String riskEventNarrative;
@Schema(description = "riskEventNarrativeExtended", example = "")
private String riskEventNarrativeExtended;
@Schema(description = "flagChanges", example = "0")
private Integer flagChanges;
@Schema(description = "flagChangeNarrative", example = "")
private String flagChangeNarrative;
@Schema(description = "flagParisMOUPerformance", example = "0")
private Integer flagParisMOUPerformance;
@Schema(description = "flagParisMOUPerformanceNarrative", example = "")
private String flagParisMOUPerformanceNarrative;
@Schema(description = "flagTokyoMOUPeformance", example = "0")
private Integer flagTokyoMOUPeformance;
@Schema(description = "flagTokyoMOUPeformanceNarrative", example = "")
private String flagTokyoMOUPeformanceNarrative;
@Schema(description = "flagUSCGMOUPerformance", example = "0")
private Integer flagUSCGMOUPerformance;
@Schema(description = "flagUSCGMOUPerformanceNarrative", example = "")
private String flagUSCGMOUPerformanceNarrative;
@Schema(description = "uscgQualship21", example = "0")
private Integer uscgQualship21;
@Schema(description = "uscgQualship21Narrative", example = "")
private String uscgQualship21Narrative;
@Schema(description = "timeSincePSCInspection", example = "0")
private Integer timeSincePSCInspection;
@Schema(description = "timeSincePSCInspectionNarrative", example = "")
private String timeSincePSCInspectionNarrative;
@Schema(description = "pscInspections", example = "0")
private Integer pscInspections;
@Schema(description = "pscInspectionNarrative", example = "")
private String pscInspectionNarrative;
@Schema(description = "pscDefects", example = "0")
private Integer pscDefects;
@Schema(description = "pscDefectsNarrative", example = "")
private String pscDefectsNarrative;
@Schema(description = "pscDetentions", example = "0")
private Integer pscDetentions;
@Schema(description = "pscDetentionsNarrative", example = "")
private String pscDetentionsNarrative;
@Schema(description = "currentSMCCertificate", example = "0")
private Integer currentSMCCertificate;
@Schema(description = "currentSMCCertificateNarrative", example = "")
private String currentSMCCertificateNarrative;
@Schema(description = "docChanges", example = "0")
private Integer docChanges;
@Schema(description = "docChangesNarrative", example = "")
private String docChangesNarrative;
@Schema(description = "currentClass", example = "0")
private Integer currentClass;
@Schema(description = "currentClassNarrative", example = "")
private String currentClassNarrative;
@Schema(description = "currentClassNarrativeExtended", example = "")
private String currentClassNarrativeExtended;
@Schema(description = "classStatusChanges", example = "0")
private Integer classStatusChanges;
@Schema(description = "classStatusChangesNarrative", example = "")
private String classStatusChangesNarrative;
@Schema(description = "pandICoverage", example = "0")
private Integer pandICoverage;
@Schema(description = "pandICoverageNarrative", example = "")
private String pandICoverageNarrative;
@Schema(description = "pandICoverageNarrativeExtended", example = "")
private String pandICoverageNarrativeExtended;
@Schema(description = "nameChanges", example = "0")
private Integer nameChanges;
@Schema(description = "nameChangesNarrative", example = "")
private String nameChangesNarrative;
@Schema(description = "gboChanges", example = "0")
private Integer gboChanges;
@Schema(description = "gboChangesNarrative", example = "")
private String gboChangesNarrative;
@Schema(description = "ageOfShip", example = "0")
private Integer ageOfShip;
@Schema(description = "ageofShipNarrative", example = "")
private String ageofShipNarrative;
@Schema(description = "iuuFishingViolation", example = "0")
private Integer iuuFishingViolation;
@Schema(description = "iuuFishingNarrative", example = "")
private String iuuFishingNarrative;
@Schema(description = "draughtChanges", example = "0")
private Integer draughtChanges;
@Schema(description = "draughtChangesNarrative", example = "")
private String draughtChangesNarrative;
@Schema(description = "mostRecentSanctionedPortCall", example = "0")
private Integer mostRecentSanctionedPortCall;
@Schema(description = "mostRecentSanctionedPortCallNarrative", example = "")
private String mostRecentSanctionedPortCallNarrative;
@Schema(description = "singleShipOperation", example = "0")
private Integer singleShipOperation;
@Schema(description = "singleShipOperationNarrative", example = "")
private String singleShipOperationNarrative;
@Schema(description = "fleetSafety", example = "0")
private Integer fleetSafety;
@Schema(description = "fleetSafetyNarrative", example = "")
private String fleetSafetyNarrative;
@Schema(description = "fleetPSC", example = "0")
private Integer fleetPSC;
@Schema(description = "fleetPSCNarrative", example = "")
private String fleetPSCNarrative;
@Schema(description = "specialSurveyOverdue", example = "0")
private Integer specialSurveyOverdue;
@Schema(description = "specialSurveyOverdueNarrative", example = "")
private String specialSurveyOverdueNarrative;
@Schema(description = "ownerUnknown", example = "0")
private Integer ownerUnknown;
@Schema(description = "ownerUnknownNarrative", example = "")
private String ownerUnknownNarrative;
@Schema(description = "russianPortCall", example = "0")
private Integer russianPortCall;
@Schema(description = "russianPortCallNarrative", example = "")
private String russianPortCallNarrative;
@Schema(description = "russianOwnerRegistration", example = "0")
private Integer russianOwnerRegistration;
@Schema(description = "russianOwnerRegistrationNarrative", example = "")
private String russianOwnerRegistrationNarrative;
@Schema(description = "russianSTS", example = "0")
private Integer russianSTS;
@Schema(description = "russianSTSNarrative", example = "")
private String russianSTSNarrative;
}

파일 보기

@ -0,0 +1,15 @@
package com.snp.batch.global.dto.bypass;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
/**
* S&P Global API 응답 스키마 (Swagger 문서용)
* 클래스는 자동 생성되었습니다. 직접 수정하지 마세요.
*/
@Getter
@Schema(description = "TargetsParameters")
public class TargetsParameters {
@Schema(description = "sinceSeconds", example = "0")
private Integer sinceSeconds;
}

파일 보기

@ -0,0 +1,17 @@
package com.snp.batch.global.dto.bypass;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
/**
* S&P Global API 응답 스키마 (Swagger 문서용)
* 클래스는 자동 생성되었습니다. 직접 수정하지 마세요.
*/
@Getter
@Schema(description = "sTargetCount")
public class sTargetCount {
@Schema(description = "apsStatus", example = "")
private String apsStatus;
@Schema(description = "targetCount", example = "0")
private Integer targetCount;
}

파일 보기

@ -95,6 +95,12 @@ public class BypassApiConfig {
@Column(name = "generated_at")
private LocalDateTime generatedAt;
/**
* Swagger 응답 스키마 DTO 클래스 (FQCN, 코드 생성 자동 설정)
*/
@Column(name = "response_schema_class", length = 300)
private String responseSchemaClass;
/**
* 생성 일시 (감사 필드)
*/

파일 보기

@ -0,0 +1,50 @@
package com.snp.batch.jobs.web.ais.controller;
import com.fasterxml.jackson.databind.JsonNode;
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;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import com.snp.batch.jobs.web.ais.service.GetTargetCountService;
/**
* Ais API
* S&P Maritime API에서 데이터를 실시간 조회하여 JSON을 그대로 반환
*/
@RestController
@RequestMapping("/api/ais")
@RequiredArgsConstructor
@Tag(name = "Ais", description = "[AIS API] Ais API")
public class AisController extends BaseBypassController {
private final GetTargetCountService getTargetCountService;
@Operation(
summary = "선박 AIS 대상 수 조회",
description = "선박 AIS 대상 수 조회"
)
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "선박 AIS 대상 수 조회",
content = @io.swagger.v3.oas.annotations.media.Content(
mediaType = "application/json",
array = @io.swagger.v3.oas.annotations.media.ArraySchema(
schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = com.snp.batch.global.dto.bypass.sTargetCount.class)
)
)
)
@PostMapping("/GetTargetCount")
public ResponseEntity<JsonNode> getGetTargetCountData(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "",
content = @io.swagger.v3.oas.annotations.media.Content(
mediaType = "application/json",
schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = com.snp.batch.global.dto.bypass.TargetsParameters.class)))
@RequestBody JsonNode sinceSeconds) {
return executeRaw(() -> getTargetCountService.getGetTargetCountData(sinceSeconds));
}
}

파일 보기

@ -0,0 +1,31 @@
package com.snp.batch.jobs.web.ais.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.snp.batch.common.web.service.BaseBypassService;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
/**
* 선박 AIS 대상 조회 서비스
* 외부 Maritime API에서 데이터를 실시간 조회하여 JSON을 그대로 반환
*/
@Service
public class GetTargetCountService extends BaseBypassService<JsonNode> {
public GetTargetCountService(
@Qualifier("maritimeAisApiWebClient") WebClient webClient) {
super(webClient, "/AisSvc.svc/AIS/GetTargetCount", "선박 AIS 대상 수 조회",
new ParameterizedTypeReference<>() {},
new ParameterizedTypeReference<>() {});
}
/**
* 선박 AIS 대상 조회 데이터를 조회합니다.
*/
public JsonNode getGetTargetCountData(JsonNode sinceSeconds) {
return fetchRawPost(sinceSeconds, uri -> uri.path(getApiPath())
.build());
}
}

파일 보기

@ -14,13 +14,13 @@ import org.springframework.web.bind.annotation.RequestParam;
import com.snp.batch.jobs.web.compliance.service.CompliancesByImosService;
/**
* Compliance bypass API
* Compliance API
* S&P Maritime API에서 데이터를 실시간 조회하여 JSON을 그대로 반환
*/
@RestController
@RequestMapping("/api/compliance")
@RequiredArgsConstructor
@Tag(name = "Compliance", description = "[Service API] Compliance bypass API")
@Tag(name = "Compliance", description = "[Service API] Compliance API")
public class ComplianceController extends BaseBypassController {
private final CompliancesByImosService compliancesByImosService;
@ -29,6 +29,16 @@ public class ComplianceController extends BaseBypassController {
summary = "IMO 기반 선박 규정준수 조회",
description = "Gets details of the IMOs of ships with full compliance details that match given IMOs"
)
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "IMO 기반 선박 규정준수 조회",
content = @io.swagger.v3.oas.annotations.media.Content(
mediaType = "application/json",
array = @io.swagger.v3.oas.annotations.media.ArraySchema(
schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = com.snp.batch.global.dto.bypass.ComplianceDetails.class)
)
)
)
@GetMapping("/CompliancesByImos")
public ResponseEntity<JsonNode> getCompliancesByImosData(@Parameter(description = "Comma separated IMOs up to a total of 100", example = "9876543")
@RequestParam(required = true) String imos) {

파일 보기

@ -8,7 +8,7 @@ import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
/**
* IMO 기반 선박 규정준수 조회 bypass 서비스
* IMO 기반 선박 규정준수 조회 서비스
* 외부 Maritime API에서 데이터를 실시간 조회하여 JSON을 그대로 반환
*/
@Service

파일 보기

@ -0,0 +1,45 @@
package com.snp.batch.jobs.web.facility.controller;
import com.fasterxml.jackson.databind.JsonNode;
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;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;
import com.snp.batch.jobs.web.facility.service.PortsService;
/**
* Facility API
* S&P Maritime API에서 데이터를 실시간 조회하여 JSON을 그대로 반환
*/
@RestController
@RequestMapping("/api/facility")
@RequiredArgsConstructor
@Tag(name = "Facility", description = "[Service API] Facility API")
public class FacilityController extends BaseBypassController {
private final PortsService portsService;
@Operation(
summary = "항구 시설 조회",
description = "항구 시설 조회"
)
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "항구 시설 조회",
content = @io.swagger.v3.oas.annotations.media.Content(
mediaType = "application/json",
array = @io.swagger.v3.oas.annotations.media.ArraySchema(
schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = com.snp.batch.global.dto.bypass.PortFacility.class)
)
)
)
@GetMapping("/Ports")
public ResponseEntity<JsonNode> getPortsData() {
return executeRaw(() -> portsService.getPortsData());
}
}

파일 보기

@ -0,0 +1,31 @@
package com.snp.batch.jobs.web.facility.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.snp.batch.common.web.service.BaseBypassService;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
/**
* 항구 시설 조회 서비스
* 외부 Maritime API에서 데이터를 실시간 조회하여 JSON을 그대로 반환
*/
@Service
public class PortsService extends BaseBypassService<JsonNode> {
public PortsService(
@Qualifier("maritimeServiceApiWebClient") WebClient webClient) {
super(webClient, "/Facilities/Ports", "항구 시설 조회",
new ParameterizedTypeReference<>() {},
new ParameterizedTypeReference<>() {});
}
/**
* 항구 시설 조회 데이터를 조회합니다.
*/
public JsonNode getPortsData() {
return fetchRawGet(uri -> uri.path(getApiPath())
.build());
}
}

파일 보기

@ -15,13 +15,13 @@ import com.snp.batch.jobs.web.risk.service.RisksByImosService;
import com.snp.batch.jobs.web.risk.service.UpdatedComplianceListService;
/**
* Risk bypass API
* Risk API
* S&P Maritime API에서 데이터를 실시간 조회하여 JSON을 그대로 반환
*/
@RestController
@RequestMapping("/api/risk")
@RequiredArgsConstructor
@Tag(name = "Risk", description = "[Service API] Risk bypass API")
@Tag(name = "Risk", description = "[Service API] Risk API")
public class RiskController extends BaseBypassController {
private final RisksByImosService risksByImosService;
@ -31,6 +31,16 @@ public class RiskController extends BaseBypassController {
summary = "IMO 기반 선박 위험지표 조회",
description = "Gets details of the IMOs of all ships with risk updates as a collection"
)
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "IMO 기반 선박 위험지표 조회",
content = @io.swagger.v3.oas.annotations.media.Content(
mediaType = "application/json",
array = @io.swagger.v3.oas.annotations.media.ArraySchema(
schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = com.snp.batch.global.dto.bypass.RiskWithNarrativesDetails.class)
)
)
)
@GetMapping("/RisksByImos")
public ResponseEntity<JsonNode> getRisksByImosData(@Parameter(description = "Comma separated IMOs up to a total of 100", example = "9876543")
@RequestParam(required = true) String imos) {
@ -41,6 +51,16 @@ public class RiskController extends BaseBypassController {
summary = "기간 내 변경된 위험지표 조회",
description = "Gets details of the IMOs of all ships with risk updates"
)
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "기간 내 변경된 위험지표 조회",
content = @io.swagger.v3.oas.annotations.media.Content(
mediaType = "application/json",
array = @io.swagger.v3.oas.annotations.media.ArraySchema(
schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = com.snp.batch.global.dto.bypass.RiskDetails.class)
)
)
)
@GetMapping("/UpdatedRiskList")
public ResponseEntity<JsonNode> getUpdatedComplianceListData(@Parameter(description = "Time/seconds are optional", example = "2026-03-30T07:01:27.000Z")
@RequestParam(required = true) String fromDate,

파일 보기

@ -8,7 +8,7 @@ import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
/**
* IMO 기반 선박 위험지표 조회 bypass 서비스
* IMO 기반 선박 위험지표 조회 서비스
* 외부 Maritime API에서 데이터를 실시간 조회하여 JSON을 그대로 반환
*/
@Service

파일 보기

@ -8,7 +8,7 @@ import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
/**
* 기간 변경된 위험지표 조회 bypass 서비스
* 기간 변경된 위험지표 조회 서비스
* 외부 Maritime API에서 데이터를 실시간 조회하여 JSON을 그대로 반환
*/
@Service

파일 보기

@ -14,13 +14,13 @@ import org.springframework.web.bind.annotation.RequestParam;
import com.snp.batch.jobs.web.ship.service.GetShipDataByIHSLRorIMOService;
/**
* Ship bypass API
* Ship API
* S&P Maritime API에서 데이터를 실시간 조회하여 JSON을 그대로 반환
*/
@RestController
@RequestMapping("/api/ship")
@RequiredArgsConstructor
@Tag(name = "Ship", description = "[Ship API] Ship bypass API")
@Tag(name = "Ship", description = "[Ship API] Ship API")
public class ShipController extends BaseBypassController {
private final GetShipDataByIHSLRorIMOService getShipDataByIHSLRorIMOService;
@ -29,6 +29,16 @@ public class ShipController extends BaseBypassController {
summary = "IMO 기반 선박제원정보 조회",
description = "IMO 기반 선박제원정보 조회"
)
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "IMO 기반 선박제원정보 조회",
content = @io.swagger.v3.oas.annotations.media.Content(
mediaType = "application/json",
array = @io.swagger.v3.oas.annotations.media.ArraySchema(
schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = com.snp.batch.global.dto.bypass.APSShipResult.class)
)
)
)
@GetMapping("/GetShipDataByIHSLRorIMO")
public ResponseEntity<JsonNode> getGetShipDataByIHSLRorIMOData(@Parameter(description = "", example = "9876543")
@RequestParam(required = true) String ihslrOrImo) {

파일 보기

@ -8,7 +8,7 @@ import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
/**
* IMO 기반 선박제원정보 조회 bypass 서비스
* IMO 기반 선박제원정보 조회 서비스
* 외부 Maritime API에서 데이터를 실시간 조회하여 JSON을 그대로 반환
*/
@Service

파일 보기

@ -3,6 +3,7 @@ package com.snp.batch.service;
import com.snp.batch.global.dto.CodeGenerationResult;
import com.snp.batch.global.model.BypassApiConfig;
import com.snp.batch.global.model.BypassApiParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -11,8 +12,10 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@ -23,8 +26,11 @@ import java.util.stream.Collectors;
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class BypassCodeGenerator {
private final SwaggerSchemaGenerator swaggerSchemaGenerator;
private static final String BASE_PACKAGE = "com.snp.batch.jobs.web";
/**
@ -43,6 +49,26 @@ public class BypassCodeGenerator {
List<String> servicePaths = new ArrayList<>();
// Generate response/request DTOs from swagger.json
Map<Long, String> responseSchemaMap = new HashMap<>();
Map<Long, String> requestBodySchemaMap = new HashMap<>();
for (BypassApiConfig config : configs) {
String fqcn = swaggerSchemaGenerator.generateResponseDto(
config.getWebclientBean(), config.getExternalPath(), force);
if (fqcn != null) {
responseSchemaMap.put(config.getId(), fqcn);
config.setResponseSchemaClass(fqcn);
}
// POST 엔드포인트의 requestBody DTO 생성
if ("POST".equalsIgnoreCase(config.getHttpMethod())) {
String reqFqcn = swaggerSchemaGenerator.generateRequestBodyDto(
config.getWebclientBean(), config.getExternalPath(), force);
if (reqFqcn != null) {
requestBodySchemaMap.put(config.getId(), reqFqcn);
}
}
}
for (BypassApiConfig config : configs) {
String endpointName = config.getEndpointName();
String servicePath = basePath + "/service/" + endpointName + "Service.java";
@ -58,7 +84,7 @@ public class BypassCodeGenerator {
}
// Controller: 모든 엔드포인트를 합치므로 항상 재생성
String controllerCode = generateControllerCode(domain, configs);
String controllerCode = generateControllerCode(domain, configs, responseSchemaMap, requestBodySchemaMap);
String domainCapitalized = capitalize(domain);
Path controllerFilePath = writeFile(
basePath + "/controller/" + domainCapitalized + "Controller.java", controllerCode, true);
@ -83,9 +109,23 @@ public class BypassCodeGenerator {
String serviceClass = endpointName + "Service";
boolean isPost = "POST".equalsIgnoreCase(config.getHttpMethod());
// POST인데 BODY 파라미터가 없으면 자동 추가
List<BypassApiParam> effectiveParams = new ArrayList<>(params);
if (isPost && effectiveParams.stream().noneMatch(p -> "BODY".equalsIgnoreCase(p.getParamIn()))) {
BypassApiParam autoBody = new BypassApiParam();
autoBody.setParamName("body");
autoBody.setParamType("STRING");
autoBody.setParamIn("BODY");
autoBody.setRequired(false);
autoBody.setDescription("요청 본문 (JSON)");
autoBody.setExample("{}");
autoBody.setSortOrder(999);
effectiveParams.add(autoBody);
}
String methodName = "get" + endpointName + "Data";
String fetchMethod = buildFetchMethodCall(config, params, isPost);
String methodParams = buildMethodParams(params);
String fetchMethod = buildFetchMethodCall(config, effectiveParams, isPost);
String methodParams = buildMethodParams(effectiveParams);
return """
package {{PACKAGE}};
@ -98,7 +138,7 @@ public class BypassCodeGenerator {
import org.springframework.web.reactive.function.client.WebClient;
/**
* {{DISPLAY_NAME}} bypass 서비스
* {{DISPLAY_NAME}} 서비스
* 외부 Maritime API에서 데이터를 실시간 조회하여 JSON을 그대로 반환
*/
@Service
@ -133,7 +173,7 @@ public class BypassCodeGenerator {
* Controller 코드 생성 (RAW 모드).
* 모든 엔드포인트가 ResponseEntity<JsonNode> 반환합니다 (외부 API 원본 JSON 그대로).
*/
private String generateControllerCode(String domain, List<BypassApiConfig> configs) {
private String generateControllerCode(String domain, List<BypassApiConfig> configs, Map<Long, String> responseSchemaMap, Map<Long, String> requestBodySchemaMap) {
String packageName = BASE_PACKAGE + "." + domain + ".controller";
String servicePackage = BASE_PACKAGE + "." + domain + ".service";
String domainCap = capitalize(domain);
@ -157,7 +197,7 @@ public class BypassCodeGenerator {
.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()
boolean anyBody = anyPost || configs.stream()
.anyMatch(c -> c.getParams().stream().anyMatch(p -> "BODY".equalsIgnoreCase(p.getParamIn())));
if (anyPost) importSet.add("import org.springframework.web.bind.annotation.PostMapping;");
@ -181,7 +221,7 @@ public class BypassCodeGenerator {
}
String tagPrefix = getTagPrefix(configs.get(0).getWebclientBean());
String tagDescription = tagPrefix + " " + domainCap + " bypass API";
String tagDescription = tagPrefix + " " + domainCap + " API";
// 엔드포인트 메서드 목록
StringBuilder methods = new StringBuilder();
@ -191,10 +231,25 @@ public class BypassCodeGenerator {
String serviceField = Character.toLowerCase(serviceClass.charAt(0)) + serviceClass.substring(1);
boolean isPost = "POST".equalsIgnoreCase(config.getHttpMethod());
// POST인데 BODY 파라미터가 없으면 자동 추가
List<BypassApiParam> ctrlParams = new ArrayList<>(config.getParams());
if (isPost && ctrlParams.stream().noneMatch(p -> "BODY".equalsIgnoreCase(p.getParamIn()))) {
BypassApiParam autoBody = new BypassApiParam();
autoBody.setParamName("body");
autoBody.setParamType("STRING");
autoBody.setParamIn("BODY");
autoBody.setRequired(false);
autoBody.setDescription("요청 본문 (JSON)");
autoBody.setExample("{}");
autoBody.setSortOrder(999);
ctrlParams.add(autoBody);
}
String mappingAnnotation = isPost ? "@PostMapping" : "@GetMapping";
String mappingPath = buildMappingPath(config.getParams(), config.getExternalPath());
String paramAnnotations = buildControllerParamAnnotations(config.getParams());
String serviceCallArgs = buildServiceCallArgs(config.getParams());
String mappingPath = buildMappingPath(ctrlParams, config.getExternalPath());
String reqBodySchema = requestBodySchemaMap.get(config.getId());
String paramAnnotations = buildControllerParamAnnotations(ctrlParams, reqBodySchema);
String serviceCallArgs = buildServiceCallArgs(ctrlParams);
String methodName = "get" + endpointName + "Data";
methods.append("\n");
@ -205,6 +260,20 @@ public class BypassCodeGenerator {
: config.getDisplayName() + " 데이터를 요청하고 응답을 그대로 반환합니다.";
methods.append(" description = \"").append(opDescription).append("\"\n");
methods.append(" )\n");
// @ApiResponse with schema (if available)
String schemaClass = responseSchemaMap.get(config.getId());
if (schemaClass != null && !schemaClass.isEmpty()) {
methods.append(" @io.swagger.v3.oas.annotations.responses.ApiResponse(\n");
methods.append(" responseCode = \"200\",\n");
methods.append(" description = \"").append(config.getDisplayName()).append("\",\n");
methods.append(" content = @io.swagger.v3.oas.annotations.media.Content(\n");
methods.append(" mediaType = \"application/json\",\n");
methods.append(" array = @io.swagger.v3.oas.annotations.media.ArraySchema(\n");
methods.append(" schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = ").append(schemaClass).append(".class)\n");
methods.append(" )\n");
methods.append(" )\n");
methods.append(" )\n");
}
methods.append(" ").append(mappingAnnotation).append(mappingPath).append("\n");
methods.append(" public ResponseEntity<JsonNode> ").append(methodName).append("(");
if (!paramAnnotations.isEmpty()) {
@ -219,7 +288,7 @@ public class BypassCodeGenerator {
return "package " + packageName + ";\n\n"
+ importsStr + "\n\n"
+ "/**\n"
+ " * " + domainCap + " bypass API\n"
+ " * " + domainCap + " API\n"
+ " * S&P Maritime API에서 데이터를 실시간 조회하여 JSON을 그대로 반환\n"
+ " */\n"
+ "@RestController\n"
@ -263,7 +332,10 @@ public class BypassCodeGenerator {
private String buildMethodParams(List<BypassApiParam> params) {
return params.stream()
.sorted((a, b) -> Integer.compare(a.getSortOrder(), b.getSortOrder()))
.map(p -> toJavaType(p.getParamType()) + " " + p.getParamName())
.map(p -> {
String type = "BODY".equalsIgnoreCase(p.getParamIn()) ? "JsonNode" : toJavaType(p.getParamType());
return type + " " + p.getParamName();
})
.collect(Collectors.joining(", "));
}
@ -274,7 +346,7 @@ public class BypassCodeGenerator {
.collect(Collectors.joining(", "));
}
private String buildControllerParamAnnotations(List<BypassApiParam> params) {
private String buildControllerParamAnnotations(List<BypassApiParam> params, String requestBodySchemaClass) {
if (params.isEmpty()) {
return "";
}
@ -290,8 +362,17 @@ public class BypassCodeGenerator {
return switch (p.getParamIn().toUpperCase()) {
case "PATH" -> "@Parameter(description = \"" + description + "\", example = \"" + example + "\")\n"
+ " @PathVariable " + javaType + " " + paramName;
case "BODY" -> "@Parameter(description = \"" + description + "\", example = \"" + example + "\")\n"
+ " @RequestBody " + javaType + " " + paramName;
case "BODY" -> {
StringBuilder bodyAnno = new StringBuilder();
bodyAnno.append("@io.swagger.v3.oas.annotations.parameters.RequestBody(description = \"").append(description).append("\"");
if (requestBodySchemaClass != null && !requestBodySchemaClass.isEmpty()) {
bodyAnno.append(",\n content = @io.swagger.v3.oas.annotations.media.Content(\n");
bodyAnno.append(" mediaType = \"application/json\",\n");
bodyAnno.append(" schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = ").append(requestBodySchemaClass).append(".class))");
}
bodyAnno.append(")\n @RequestBody JsonNode ").append(paramName);
yield bodyAnno.toString();
}
default -> {
String required = Boolean.TRUE.equals(p.getRequired()) ? "true" : "false";
yield "@Parameter(description = \"" + description + "\", example = \"" + example + "\")\n"

파일 보기

@ -80,6 +80,7 @@ public class BypassConfigService {
config.setExternalPath(request.getExternalPath());
config.setHttpMethod(request.getHttpMethod());
config.setDescription(request.getDescription());
config.setResponseSchemaClass(request.getResponseSchemaClass());
// params 교체: clear flush(DELETE 실행) 새로 추가
config.getParams().clear();
@ -131,6 +132,7 @@ public class BypassConfigService {
.externalPath(config.getExternalPath())
.httpMethod(config.getHttpMethod())
.description(config.getDescription())
.responseSchemaClass(config.getResponseSchemaClass())
.generated(config.getGenerated())
.generatedAt(config.getGeneratedAt())
.createdAt(config.getCreatedAt())
@ -148,6 +150,7 @@ public class BypassConfigService {
.externalPath(request.getExternalPath())
.httpMethod(request.getHttpMethod() != null ? request.getHttpMethod() : "GET")
.description(request.getDescription())
.responseSchemaClass(request.getResponseSchemaClass())
.build();
}

파일 보기

@ -0,0 +1,252 @@
package com.snp.batch.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Iterator;
import java.util.Map;
@Slf4j
@Service
public class SwaggerSchemaGenerator {
private static final String DTO_PACKAGE = "com.snp.batch.global.dto.bypass";
private static final String DTO_BASE_PATH = "src/main/java/com/snp/batch/global/dto/bypass";
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final Map<String, String> SWAGGER_FILE_MAP = Map.of(
"maritimeAisApiWebClient", "swagger/swagger_aisapi.json",
"maritimeApiWebClient", "swagger/swagger_shipsapi.json",
"maritimeServiceApiWebClient", "swagger/swagger_webservices.json"
);
/**
* swagger.json에서 응답 스키마를 추출하여 DTO Java 클래스를 자동 생성합니다.
* @return 생성된 DTO의 FQCN, 스키마를 찾을 없으면 null
*/
public String generateResponseDto(String webclientBean, String externalPath, boolean force) {
String swaggerFile = SWAGGER_FILE_MAP.get(webclientBean);
if (swaggerFile == null) {
log.warn("Unknown webclientBean: {}, skipping DTO generation", webclientBean);
return null;
}
try {
JsonNode swagger = loadSwaggerJson(swaggerFile);
if (swagger == null) return null;
// Find path in swagger
JsonNode paths = swagger.get("paths");
if (paths == null || !paths.has(externalPath)) {
log.info("Path {} not found in {}, skipping DTO generation", externalPath, swaggerFile);
return null;
}
// Get 200 response schema $ref
JsonNode pathItem = paths.get(externalPath);
// Try GET first, then POST
JsonNode operation = pathItem.has("get") ? pathItem.get("get") : pathItem.has("post") ? pathItem.get("post") : null;
if (operation == null) return null;
JsonNode responses = operation.get("responses");
if (responses == null || !responses.has("200")) return null;
JsonNode response200 = responses.get("200");
JsonNode content = response200.get("content");
if (content == null) return null;
JsonNode jsonContent = content.has("application/json") ? content.get("application/json") : content.has("text/json") ? content.get("text/json") : null;
if (jsonContent == null) return null;
JsonNode schema = jsonContent.get("schema");
if (schema == null) return null;
// Resolve $ref - could be direct or array items
String schemaRef = null;
boolean isArray = "array".equals(schema.has("type") ? schema.get("type").asText() : "");
if (isArray && schema.has("items") && schema.get("items").has("$ref")) {
schemaRef = schema.get("items").get("$ref").asText();
} else if (schema.has("$ref")) {
schemaRef = schema.get("$ref").asText();
}
if (schemaRef == null) return null;
// Extract schema name from $ref (e.g., "#/components/schemas/Foo.Bar.Baz" -> "Baz")
String fullSchemaName = schemaRef.substring(schemaRef.lastIndexOf("/") + 1);
String className = fullSchemaName.contains(".")
? fullSchemaName.substring(fullSchemaName.lastIndexOf(".") + 1)
: fullSchemaName;
// Get schema definition
JsonNode schemas = swagger.at("/components/schemas/" + fullSchemaName);
if (schemas.isMissingNode()) return null;
// Generate DTO file
String projectRoot = System.getProperty("user.dir");
String filePath = projectRoot + "/" + DTO_BASE_PATH + "/" + className + ".java";
if (!force && Files.exists(Path.of(filePath))) {
log.info("DTO already exists, skipping: {}", filePath);
return DTO_PACKAGE + "." + className;
}
String javaCode = generateDtoCode(className, schemas);
Files.createDirectories(Path.of(filePath).getParent());
Files.writeString(Path.of(filePath), javaCode, StandardCharsets.UTF_8);
log.info("DTO generated: {}", filePath);
return DTO_PACKAGE + "." + className;
} catch (Exception e) {
log.error("Failed to generate response DTO for {} {}: {}", webclientBean, externalPath, e.getMessage());
return null;
}
}
/**
* swagger.json에서 requestBody 스키마를 추출하여 DTO Java 클래스를 자동 생성합니다.
* @return 생성된 DTO의 FQCN, 스키마를 찾을 없으면 null
*/
public String generateRequestBodyDto(String webclientBean, String externalPath, boolean force) {
String swaggerFile = SWAGGER_FILE_MAP.get(webclientBean);
if (swaggerFile == null) return null;
try {
JsonNode swagger = loadSwaggerJson(swaggerFile);
if (swagger == null) return null;
JsonNode paths = swagger.get("paths");
if (paths == null || !paths.has(externalPath)) return null;
JsonNode pathItem = paths.get(externalPath);
JsonNode operation = pathItem.has("post") ? pathItem.get("post") : null;
if (operation == null) return null;
JsonNode requestBody = operation.get("requestBody");
if (requestBody == null) return null;
JsonNode content = requestBody.get("content");
if (content == null) return null;
JsonNode jsonContent = content.has("application/json") ? content.get("application/json")
: content.has("application/json-patch+json") ? content.get("application/json-patch+json")
: content.has("text/json") ? content.get("text/json") : null;
if (jsonContent == null) return null;
JsonNode schema = jsonContent.get("schema");
if (schema == null || !schema.has("$ref")) return null;
String schemaRef = schema.get("$ref").asText();
String fullSchemaName = schemaRef.substring(schemaRef.lastIndexOf("/") + 1);
String className = fullSchemaName.contains(".")
? fullSchemaName.substring(fullSchemaName.lastIndexOf(".") + 1)
: fullSchemaName;
JsonNode schemaDef = swagger.at("/components/schemas/" + fullSchemaName);
if (schemaDef.isMissingNode()) return null;
String projectRoot = System.getProperty("user.dir");
String filePath = projectRoot + "/" + DTO_BASE_PATH + "/" + className + ".java";
if (!force && Files.exists(Path.of(filePath))) {
log.info("Request DTO already exists, skipping: {}", filePath);
return DTO_PACKAGE + "." + className;
}
String javaCode = generateDtoCode(className, schemaDef);
Files.createDirectories(Path.of(filePath).getParent());
Files.writeString(Path.of(filePath), javaCode, StandardCharsets.UTF_8);
log.info("Request DTO generated: {}", filePath);
return DTO_PACKAGE + "." + className;
} catch (Exception e) {
log.error("Failed to generate request body DTO for {} {}: {}", webclientBean, externalPath, e.getMessage());
return null;
}
}
private JsonNode loadSwaggerJson(String resourcePath) {
try {
ClassPathResource resource = new ClassPathResource(resourcePath);
try (InputStream is = resource.getInputStream()) {
return objectMapper.readTree(is);
}
} catch (IOException e) {
log.error("Failed to load swagger file: {}", resourcePath, e);
return null;
}
}
private String generateDtoCode(String className, JsonNode schema) {
StringBuilder sb = new StringBuilder();
sb.append("package ").append(DTO_PACKAGE).append(";\n\n");
sb.append("import io.swagger.v3.oas.annotations.media.Schema;\n");
sb.append("import lombok.Getter;\n\n");
String title = schema.has("title") ? schema.get("title").asText() : className;
sb.append("/**\n");
sb.append(" * S&P Global API 응답 스키마 (Swagger 문서용)\n");
sb.append(" * 이 클래스는 자동 생성되었습니다. 직접 수정하지 마세요.\n");
sb.append(" */\n");
sb.append("@Getter\n");
sb.append("@Schema(description = \"").append(title).append("\")\n");
sb.append("public class ").append(className).append(" {\n");
JsonNode properties = schema.get("properties");
if (properties != null) {
Iterator<Map.Entry<String, JsonNode>> fields = properties.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> field = fields.next();
String fieldName = field.getKey();
JsonNode fieldSchema = field.getValue();
String javaType = resolveJavaType(fieldSchema);
String example = getExampleValue(fieldSchema);
sb.append(" @Schema(description = \"").append(fieldName).append("\"");
if (example != null) {
sb.append(", example = \"").append(example).append("\"");
}
sb.append(")\n");
sb.append(" private ").append(javaType).append(" ").append(fieldName).append(";\n");
}
}
sb.append("}\n");
return sb.toString();
}
private String resolveJavaType(JsonNode fieldSchema) {
String type = fieldSchema.has("type") ? fieldSchema.get("type").asText() : "string";
String format = fieldSchema.has("format") ? fieldSchema.get("format").asText() : "";
return switch (type) {
case "integer" -> "int64".equals(format) ? "Long" : "Integer";
case "number" -> "Double";
case "boolean" -> "Boolean";
case "array" -> "java.util.List<Object>";
default -> "String";
};
}
private String getExampleValue(JsonNode fieldSchema) {
String type = fieldSchema.has("type") ? fieldSchema.get("type").asText() : "string";
return switch (type) {
case "integer" -> "0";
case "number" -> "0.0";
case "boolean" -> "false";
case "array" -> null;
default -> "";
};
}
}

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Load Diff

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Load Diff

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Load Diff