From fab03a31bb943ebf0bca6e4265751c9125a671a4 Mon Sep 17 00:00:00 2001
From: HYOJIN
Date: Thu, 9 Apr 2026 15:40:49 +0900
Subject: [PATCH 1/3] =?UTF-8?q?feat(swagger):=20Swagger=20=EC=9D=91?=
=?UTF-8?q?=EB=8B=B5=20=EC=8A=A4=ED=82=A4=EB=A7=88=20=EC=9E=90=EB=8F=99=20?=
=?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=B0=8F=20API=20=EB=AC=B8=EC=84=9C=20?=
=?UTF-8?q?=EA=B0=9C=EC=84=A0=20(#14)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-Authored-By: Claude Opus 4.6 (1M context)
---
frontend/src/api/bypassApi.ts | 1 +
frontend/src/pages/BypassCatalog.tsx | 16 +-
.../batch/global/config/SwaggerConfig.java | 73 +-
.../batch/global/dto/BypassConfigRequest.java | 3 +
.../global/dto/BypassConfigResponse.java | 3 +
.../global/dto/bypass/APSShipResult.java | 19 +
.../global/dto/bypass/ComplianceDetails.java | 83 +
.../batch/global/dto/bypass/PortFacility.java | 99 +
.../batch/global/dto/bypass/RiskDetails.java | 97 +
.../dto/bypass/RiskWithNarrativesDetails.java | 181 +
.../global/dto/bypass/TargetsParameters.java | 15 +
.../batch/global/dto/bypass/sTargetCount.java | 17 +
.../batch/global/model/BypassApiConfig.java | 6 +
.../web/ais/controller/AisController.java | 50 +
.../ais/service/GetTargetCountService.java | 31 +
.../controller/ComplianceController.java | 14 +-
.../service/CompliancesByImosService.java | 2 +-
.../controller/FacilityController.java | 45 +
.../web/facility/service/PortsService.java | 31 +
.../web/risk/controller/RiskController.java | 24 +-
.../web/risk/service/RisksByImosService.java | 2 +-
.../service/UpdatedComplianceListService.java | 2 +-
.../web/ship/controller/ShipController.java | 14 +-
.../GetShipDataByIHSLRorIMOService.java | 2 +-
.../batch/service/BypassCodeGenerator.java | 111 +-
.../batch/service/BypassConfigService.java | 3 +
.../batch/service/SwaggerSchemaGenerator.java | 252 +
.../resources/swagger/swagger_aisapi.json | 3853 +++++
.../resources/swagger/swagger_shipsapi.json | 11075 +++++++++++++
.../swagger/swagger_webservices.json | 13363 ++++++++++++++++
30 files changed, 29442 insertions(+), 45 deletions(-)
create mode 100644 src/main/java/com/snp/batch/global/dto/bypass/APSShipResult.java
create mode 100644 src/main/java/com/snp/batch/global/dto/bypass/ComplianceDetails.java
create mode 100644 src/main/java/com/snp/batch/global/dto/bypass/PortFacility.java
create mode 100644 src/main/java/com/snp/batch/global/dto/bypass/RiskDetails.java
create mode 100644 src/main/java/com/snp/batch/global/dto/bypass/RiskWithNarrativesDetails.java
create mode 100644 src/main/java/com/snp/batch/global/dto/bypass/TargetsParameters.java
create mode 100644 src/main/java/com/snp/batch/global/dto/bypass/sTargetCount.java
create mode 100644 src/main/java/com/snp/batch/jobs/web/ais/controller/AisController.java
create mode 100644 src/main/java/com/snp/batch/jobs/web/ais/service/GetTargetCountService.java
create mode 100644 src/main/java/com/snp/batch/jobs/web/facility/controller/FacilityController.java
create mode 100644 src/main/java/com/snp/batch/jobs/web/facility/service/PortsService.java
create mode 100644 src/main/java/com/snp/batch/service/SwaggerSchemaGenerator.java
create mode 100644 src/main/resources/swagger/swagger_aisapi.json
create mode 100644 src/main/resources/swagger/swagger_shipsapi.json
create mode 100644 src/main/resources/swagger/swagger_webservices.json
diff --git a/frontend/src/api/bypassApi.ts b/frontend/src/api/bypassApi.ts
index 1fc943d..d034062 100644
--- a/frontend/src/api/bypassApi.ts
+++ b/frontend/src/api/bypassApi.ts
@@ -39,6 +39,7 @@ export interface BypassConfigResponse {
description: string;
generated: boolean;
generatedAt: string | null;
+ responseSchemaClass: string | null;
createdAt: string;
updatedAt: string;
params: BypassParamDto[];
diff --git a/frontend/src/pages/BypassCatalog.tsx b/frontend/src/pages/BypassCatalog.tsx
index 47c49b9..f52b0f5 100644
--- a/frontend/src/pages/BypassCatalog.tsx
+++ b/frontend/src/pages/BypassCatalog.tsx
@@ -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 = {
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 = {
+ 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() {
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 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
diff --git a/src/main/java/com/snp/batch/global/dto/BypassConfigRequest.java b/src/main/java/com/snp/batch/global/dto/BypassConfigRequest.java
index d008ecb..ed4b224 100644
--- a/src/main/java/com/snp/batch/global/dto/BypassConfigRequest.java
+++ b/src/main/java/com/snp/batch/global/dto/BypassConfigRequest.java
@@ -34,6 +34,9 @@ public class BypassConfigRequest {
/** 설명 */
private String description;
+ /** Swagger 응답 스키마 DTO 클래스 (FQCN) */
+ private String responseSchemaClass;
+
/** 파라미터 목록 */
private List params;
}
diff --git a/src/main/java/com/snp/batch/global/dto/BypassConfigResponse.java b/src/main/java/com/snp/batch/global/dto/BypassConfigResponse.java
index a587260..fab93cf 100644
--- a/src/main/java/com/snp/batch/global/dto/BypassConfigResponse.java
+++ b/src/main/java/com/snp/batch/global/dto/BypassConfigResponse.java
@@ -40,6 +40,9 @@ public class BypassConfigResponse {
/** 설명 */
private String description;
+ /** Swagger 응답 스키마 DTO 클래스 (FQCN) */
+ private String responseSchemaClass;
+
/** 코드 생성 완료 여부 */
private Boolean generated;
diff --git a/src/main/java/com/snp/batch/global/dto/bypass/APSShipResult.java b/src/main/java/com/snp/batch/global/dto/bypass/APSShipResult.java
new file mode 100644
index 0000000..640b9e3
--- /dev/null
+++ b/src/main/java/com/snp/batch/global/dto/bypass/APSShipResult.java
@@ -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;
+}
diff --git a/src/main/java/com/snp/batch/global/dto/bypass/ComplianceDetails.java b/src/main/java/com/snp/batch/global/dto/bypass/ComplianceDetails.java
new file mode 100644
index 0000000..717721f
--- /dev/null
+++ b/src/main/java/com/snp/batch/global/dto/bypass/ComplianceDetails.java
@@ -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;
+}
diff --git a/src/main/java/com/snp/batch/global/dto/bypass/PortFacility.java b/src/main/java/com/snp/batch/global/dto/bypass/PortFacility.java
new file mode 100644
index 0000000..419c7d0
--- /dev/null
+++ b/src/main/java/com/snp/batch/global/dto/bypass/PortFacility.java
@@ -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;
+}
diff --git a/src/main/java/com/snp/batch/global/dto/bypass/RiskDetails.java b/src/main/java/com/snp/batch/global/dto/bypass/RiskDetails.java
new file mode 100644
index 0000000..372baca
--- /dev/null
+++ b/src/main/java/com/snp/batch/global/dto/bypass/RiskDetails.java
@@ -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;
+}
diff --git a/src/main/java/com/snp/batch/global/dto/bypass/RiskWithNarrativesDetails.java b/src/main/java/com/snp/batch/global/dto/bypass/RiskWithNarrativesDetails.java
new file mode 100644
index 0000000..2bdbcff
--- /dev/null
+++ b/src/main/java/com/snp/batch/global/dto/bypass/RiskWithNarrativesDetails.java
@@ -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;
+}
diff --git a/src/main/java/com/snp/batch/global/dto/bypass/TargetsParameters.java b/src/main/java/com/snp/batch/global/dto/bypass/TargetsParameters.java
new file mode 100644
index 0000000..5daa8b6
--- /dev/null
+++ b/src/main/java/com/snp/batch/global/dto/bypass/TargetsParameters.java
@@ -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;
+}
diff --git a/src/main/java/com/snp/batch/global/dto/bypass/sTargetCount.java b/src/main/java/com/snp/batch/global/dto/bypass/sTargetCount.java
new file mode 100644
index 0000000..aac7f21
--- /dev/null
+++ b/src/main/java/com/snp/batch/global/dto/bypass/sTargetCount.java
@@ -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;
+}
diff --git a/src/main/java/com/snp/batch/global/model/BypassApiConfig.java b/src/main/java/com/snp/batch/global/model/BypassApiConfig.java
index 1c5b64d..73bdf01 100644
--- a/src/main/java/com/snp/batch/global/model/BypassApiConfig.java
+++ b/src/main/java/com/snp/batch/global/model/BypassApiConfig.java
@@ -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;
+
/**
* 생성 일시 (감사 필드)
*/
diff --git a/src/main/java/com/snp/batch/jobs/web/ais/controller/AisController.java b/src/main/java/com/snp/batch/jobs/web/ais/controller/AisController.java
new file mode 100644
index 0000000..aa993e2
--- /dev/null
+++ b/src/main/java/com/snp/batch/jobs/web/ais/controller/AisController.java
@@ -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 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));
+ }
+}
diff --git a/src/main/java/com/snp/batch/jobs/web/ais/service/GetTargetCountService.java b/src/main/java/com/snp/batch/jobs/web/ais/service/GetTargetCountService.java
new file mode 100644
index 0000000..0d8ad95
--- /dev/null
+++ b/src/main/java/com/snp/batch/jobs/web/ais/service/GetTargetCountService.java
@@ -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 {
+
+ 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());
+ }
+}
diff --git a/src/main/java/com/snp/batch/jobs/web/compliance/controller/ComplianceController.java b/src/main/java/com/snp/batch/jobs/web/compliance/controller/ComplianceController.java
index d4836fe..2f15671 100644
--- a/src/main/java/com/snp/batch/jobs/web/compliance/controller/ComplianceController.java
+++ b/src/main/java/com/snp/batch/jobs/web/compliance/controller/ComplianceController.java
@@ -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 getCompliancesByImosData(@Parameter(description = "Comma separated IMOs up to a total of 100", example = "9876543")
@RequestParam(required = true) String imos) {
diff --git a/src/main/java/com/snp/batch/jobs/web/compliance/service/CompliancesByImosService.java b/src/main/java/com/snp/batch/jobs/web/compliance/service/CompliancesByImosService.java
index 3f96e32..32f91ce 100644
--- a/src/main/java/com/snp/batch/jobs/web/compliance/service/CompliancesByImosService.java
+++ b/src/main/java/com/snp/batch/jobs/web/compliance/service/CompliancesByImosService.java
@@ -8,7 +8,7 @@ import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
/**
- * IMO 기반 선박 규정준수 조회 bypass 서비스
+ * IMO 기반 선박 규정준수 조회 서비스
* 외부 Maritime API에서 데이터를 실시간 조회하여 JSON을 그대로 반환
*/
@Service
diff --git a/src/main/java/com/snp/batch/jobs/web/facility/controller/FacilityController.java b/src/main/java/com/snp/batch/jobs/web/facility/controller/FacilityController.java
new file mode 100644
index 0000000..b3f786d
--- /dev/null
+++ b/src/main/java/com/snp/batch/jobs/web/facility/controller/FacilityController.java
@@ -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 getPortsData() {
+ return executeRaw(() -> portsService.getPortsData());
+ }
+}
diff --git a/src/main/java/com/snp/batch/jobs/web/facility/service/PortsService.java b/src/main/java/com/snp/batch/jobs/web/facility/service/PortsService.java
new file mode 100644
index 0000000..3765a45
--- /dev/null
+++ b/src/main/java/com/snp/batch/jobs/web/facility/service/PortsService.java
@@ -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 {
+
+ public PortsService(
+ @Qualifier("maritimeServiceApiWebClient") WebClient webClient) {
+ super(webClient, "/Facilities/Ports", "항구 시설 조회",
+ new ParameterizedTypeReference<>() {},
+ new ParameterizedTypeReference<>() {});
+ }
+
+ /**
+ * 항구 시설 조회 데이터를 조회합니다.
+ */
+ public JsonNode getPortsData() {
+ return fetchRawGet(uri -> uri.path(getApiPath())
+ .build());
+ }
+}
diff --git a/src/main/java/com/snp/batch/jobs/web/risk/controller/RiskController.java b/src/main/java/com/snp/batch/jobs/web/risk/controller/RiskController.java
index 9bbd908..e1ddb5c 100644
--- a/src/main/java/com/snp/batch/jobs/web/risk/controller/RiskController.java
+++ b/src/main/java/com/snp/batch/jobs/web/risk/controller/RiskController.java
@@ -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 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 getUpdatedComplianceListData(@Parameter(description = "Time/seconds are optional", example = "2026-03-30T07:01:27.000Z")
@RequestParam(required = true) String fromDate,
diff --git a/src/main/java/com/snp/batch/jobs/web/risk/service/RisksByImosService.java b/src/main/java/com/snp/batch/jobs/web/risk/service/RisksByImosService.java
index 0496927..3ecc01b 100644
--- a/src/main/java/com/snp/batch/jobs/web/risk/service/RisksByImosService.java
+++ b/src/main/java/com/snp/batch/jobs/web/risk/service/RisksByImosService.java
@@ -8,7 +8,7 @@ import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
/**
- * IMO 기반 선박 위험지표 조회 bypass 서비스
+ * IMO 기반 선박 위험지표 조회 서비스
* 외부 Maritime API에서 데이터를 실시간 조회하여 JSON을 그대로 반환
*/
@Service
diff --git a/src/main/java/com/snp/batch/jobs/web/risk/service/UpdatedComplianceListService.java b/src/main/java/com/snp/batch/jobs/web/risk/service/UpdatedComplianceListService.java
index dfb661b..bc0ae4c 100644
--- a/src/main/java/com/snp/batch/jobs/web/risk/service/UpdatedComplianceListService.java
+++ b/src/main/java/com/snp/batch/jobs/web/risk/service/UpdatedComplianceListService.java
@@ -8,7 +8,7 @@ import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
/**
- * 기간 내 변경된 위험지표 조회 bypass 서비스
+ * 기간 내 변경된 위험지표 조회 서비스
* 외부 Maritime API에서 데이터를 실시간 조회하여 JSON을 그대로 반환
*/
@Service
diff --git a/src/main/java/com/snp/batch/jobs/web/ship/controller/ShipController.java b/src/main/java/com/snp/batch/jobs/web/ship/controller/ShipController.java
index dc79cde..a6e087a 100644
--- a/src/main/java/com/snp/batch/jobs/web/ship/controller/ShipController.java
+++ b/src/main/java/com/snp/batch/jobs/web/ship/controller/ShipController.java
@@ -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 getGetShipDataByIHSLRorIMOData(@Parameter(description = "", example = "9876543")
@RequestParam(required = true) String ihslrOrImo) {
diff --git a/src/main/java/com/snp/batch/jobs/web/ship/service/GetShipDataByIHSLRorIMOService.java b/src/main/java/com/snp/batch/jobs/web/ship/service/GetShipDataByIHSLRorIMOService.java
index 04e6e57..9e8d95c 100644
--- a/src/main/java/com/snp/batch/jobs/web/ship/service/GetShipDataByIHSLRorIMOService.java
+++ b/src/main/java/com/snp/batch/jobs/web/ship/service/GetShipDataByIHSLRorIMOService.java
@@ -8,7 +8,7 @@ import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
/**
- * IMO 기반 선박제원정보 조회 bypass 서비스
+ * IMO 기반 선박제원정보 조회 서비스
* 외부 Maritime API에서 데이터를 실시간 조회하여 JSON을 그대로 반환
*/
@Service
diff --git a/src/main/java/com/snp/batch/service/BypassCodeGenerator.java b/src/main/java/com/snp/batch/service/BypassCodeGenerator.java
index 9fdbc7f..c763124 100644
--- a/src/main/java/com/snp/batch/service/BypassCodeGenerator.java
+++ b/src/main/java/com/snp/batch/service/BypassCodeGenerator.java
@@ -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 servicePaths = new ArrayList<>();
+ // Generate response/request DTOs from swagger.json
+ Map responseSchemaMap = new HashMap<>();
+ Map 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 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를 반환합니다 (외부 API 원본 JSON 그대로).
*/
- private String generateControllerCode(String domain, List configs) {
+ private String generateControllerCode(String domain, List configs, Map responseSchemaMap, Map 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 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 ").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 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 params) {
+ private String buildControllerParamAnnotations(List 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"
diff --git a/src/main/java/com/snp/batch/service/BypassConfigService.java b/src/main/java/com/snp/batch/service/BypassConfigService.java
index 5e4c0d7..98116ad 100644
--- a/src/main/java/com/snp/batch/service/BypassConfigService.java
+++ b/src/main/java/com/snp/batch/service/BypassConfigService.java
@@ -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();
}
diff --git a/src/main/java/com/snp/batch/service/SwaggerSchemaGenerator.java b/src/main/java/com/snp/batch/service/SwaggerSchemaGenerator.java
new file mode 100644
index 0000000..5a99c78
--- /dev/null
+++ b/src/main/java/com/snp/batch/service/SwaggerSchemaGenerator.java
@@ -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 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> fields = properties.fields();
+ while (fields.hasNext()) {
+ Map.Entry 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