feat: 중첩 JSON 구조 파싱 대응 및 DTO import 버그 수정 (#63)

백엔드:
- generateDtoCode에서 List/Map 타입 import 자동 추가
- JsonSchemaParser에 targetField 파라미터 추가 (특정 필드 내부만 파싱)
- 배열 내 객체 감지 시 List<{FieldName}Item> 타입으로 추론

프론트엔드:
- JSON 샘플 파싱 시 "전체 JSON / 특정 필드 내부" 선택 옵션 추가
- targetField 기본값 "data" (페이징 래퍼 대응)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
HYOJIN 2026-03-27 09:39:23 +09:00
부모 6391683959
커밋 9667d3a0ac
14개의 변경된 파일659개의 추가작업 그리고 9개의 파일을 삭제

파일 보기

@ -117,8 +117,9 @@ export const bypassApi = {
deleteJson<ApiResponse<void>>(`${BASE}/${id}`),
generateCode: (id: number, force = false) =>
postJson<ApiResponse<CodeGenerationResult>>(`${BASE}/${id}/generate?force=${force}`),
parseJson: async (jsonSample: string): Promise<ApiResponse<BypassFieldDto[]>> => {
const res = await fetch(`${BASE}/parse-json`, {
parseJson: async (jsonSample: string, targetField?: string): Promise<ApiResponse<BypassFieldDto[]>> => {
const params = targetField ? `?targetField=${encodeURIComponent(targetField)}` : '';
const res = await fetch(`${BASE}/parse-json${params}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: jsonSample, // 이미 JSON 문자열이므로 JSON.stringify 하지 않음

파일 보기

@ -26,6 +26,8 @@ export default function BypassStepFields({ fields, onChange }: BypassStepFieldsP
const [jsonSample, setJsonSample] = useState('');
const [parsing, setParsing] = useState(false);
const [parseError, setParseError] = useState<string | null>(null);
const [parseMode, setParseMode] = useState<'root' | 'field'>('root');
const [targetField, setTargetField] = useState('data');
const handleAdd = () => {
onChange([...fields, createEmptyField(fields.length)]);
@ -53,7 +55,8 @@ export default function BypassStepFields({ fields, onChange }: BypassStepFieldsP
setParseError(null);
setParsing(true);
try {
const result = await bypassApi.parseJson(jsonSample);
const tf = parseMode === 'field' ? targetField : undefined;
const result = await bypassApi.parseJson(jsonSample.trim(), tf);
if (result.success && result.data) {
onChange(result.data);
setActiveTab('manual');
@ -220,6 +223,48 @@ export default function BypassStepFields({ fields, onChange }: BypassStepFieldsP
placeholder={'{\n "riskScore": 85,\n "imoNumber": "1234567",\n "vesselName": "EXAMPLE SHIP"\n}'}
className="w-full px-3 py-2 text-sm font-mono rounded-lg border border-wing-border bg-wing-card text-wing-text placeholder:text-wing-muted focus:outline-none focus:ring-2 focus:ring-wing-accent/50 resize-none"
/>
{/* 파싱 옵션 */}
<div className="mt-3 space-y-2">
<label className="text-sm font-medium text-wing-text"> </label>
<div className="flex items-center gap-4">
<label className="flex items-center gap-2 text-sm text-wing-text cursor-pointer">
<input
type="radio"
name="parseMode"
checked={parseMode === 'root'}
onChange={() => setParseMode('root')}
className="accent-wing-accent"
/>
JSON
</label>
<label className="flex items-center gap-2 text-sm text-wing-text cursor-pointer">
<input
type="radio"
name="parseMode"
checked={parseMode === 'field'}
onChange={() => setParseMode('field')}
className="accent-wing-accent"
/>
</label>
</div>
{parseMode === 'field' && (
<div className="flex items-center gap-2">
<input
type="text"
value={targetField}
onChange={(e) => setTargetField(e.target.value)}
placeholder="필드명 (예: data, results, items)"
className="px-3 py-1.5 text-sm rounded-lg border border-wing-border bg-wing-surface text-wing-text placeholder:text-wing-muted focus:outline-none focus:ring-1 focus:ring-wing-accent/50"
/>
<span className="text-xs text-wing-muted">
, .
</span>
</div>
)}
</div>
{parseError && (
<p className="text-sm text-red-500">{parseError}</p>
)}

파일 보기

@ -108,12 +108,14 @@ public class BypassConfigController {
@Operation(
summary = "JSON 샘플 파싱",
description = "JSON 샘플에서 DTO 필드 목록을 추출합니다."
description = "JSON 샘플에서 DTO 필드 목록을 추출합니다. targetField를 지정하면 해당 필드 내부를 파싱합니다. 예: targetField=data → root.data[0] 내부 필드 추출."
)
@PostMapping("/parse-json")
public ResponseEntity<ApiResponse<List<BypassFieldDto>>> parseJson(@RequestBody String jsonSample) {
public ResponseEntity<ApiResponse<List<BypassFieldDto>>> parseJson(
@RequestBody String jsonSample,
@RequestParam(required = false) String targetField) {
try {
List<BypassFieldDto> fields = jsonSchemaParser.parse(jsonSample);
List<BypassFieldDto> fields = jsonSchemaParser.parse(jsonSample, targetField);
return ResponseEntity.ok(ApiResponse.success(fields));
} catch (Exception e) {
return ResponseEntity.badRequest().body(ApiResponse.error("JSON 파싱 실패: " + e.getMessage()));

파일 보기

@ -0,0 +1,85 @@
package com.snp.batch.jobs.web.compliance.controller;
import com.snp.batch.common.web.ApiResponse;
import com.snp.batch.common.web.controller.BaseBypassController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.snp.batch.jobs.web.compliance.dto.CompliancesByImosDto;
import com.snp.batch.jobs.web.compliance.service.CompliancesByImosService;
import com.snp.batch.jobs.web.compliance.dto.UpdatedComplianceListDto;
import com.snp.batch.jobs.web.compliance.service.UpdatedComplianceListService;
import com.snp.batch.jobs.web.compliance.dto.ComplianceValuesMeaningDto;
import com.snp.batch.jobs.web.compliance.service.ComplianceValuesMeaningService;
import com.snp.batch.jobs.web.compliance.dto.PagedUpdatedComplianceListDto;
import com.snp.batch.jobs.web.compliance.service.PagedUpdatedComplianceListService;
/**
* Compliance bypass API
* S&P Maritime API에서 데이터를 실시간 조회하여 그대로 반환
*/
@RestController
@RequestMapping("/api/compliance")
@RequiredArgsConstructor
@Tag(name = "Compliance", description = "[Service API] Compliance bypass API")
public class ComplianceController extends BaseBypassController {
private final CompliancesByImosService compliancesByImosService;
private final UpdatedComplianceListService updatedComplianceListService;
private final ComplianceValuesMeaningService complianceValuesMeaningService;
private final PagedUpdatedComplianceListService pagedUpdatedComplianceListService;
@Operation(
summary = "IMO 기반 Compliance 조회 조회",
description = "S&P API에서 IMO 기반 Compliance 조회 데이터를 요청하고 응답을 그대로 반환합니다."
)
@GetMapping("/CompliancesByImos")
public ResponseEntity<ApiResponse<List<CompliancesByImosDto>>> getCompliancesByImosData(@Parameter(description = "Comma separated IMOs up to a total of 100", example = "9876543")
@RequestParam(required = true) String imos) {
return execute(() -> compliancesByImosService.getCompliancesByImosData(imos));
}
@Operation(
summary = "기간 내 변경 Compliance 조회 조회",
description = "S&P API에서 기간 내 변경 Compliance 조회 데이터를 요청하고 응답을 그대로 반환합니다."
)
@GetMapping("/UpdatedComplianceList")
public ResponseEntity<ApiResponse<List<UpdatedComplianceListDto>>> getUpdatedComplianceListData(@Parameter(description = "Time/seconds are optional", example = "9876543")
@RequestParam(required = true) String fromDate,
@Parameter(description = "Time/seconds are optional. If unspecified, the current UTC date and time is used", example = "9876543")
@RequestParam(required = true) String toDate) {
return execute(() -> updatedComplianceListService.getUpdatedComplianceListData(fromDate, toDate));
}
@Operation(
summary = "모든 Compliance 지표 조회 조회",
description = "S&P API에서 모든 Compliance 지표 조회 데이터를 요청하고 응답을 그대로 반환합니다."
)
@GetMapping("/ComplianceValuesMeaning")
public ResponseEntity<ApiResponse<List<ComplianceValuesMeaningDto>>> getComplianceValuesMeaningData() {
return execute(() -> complianceValuesMeaningService.getComplianceValuesMeaningData());
}
@Operation(
summary = "PagedUpdatedComplianceList 조회",
description = "S&P API에서 PagedUpdatedComplianceList 데이터를 요청하고 응답을 그대로 반환합니다."
)
@GetMapping("/PagedUpdatedComplianceList")
public ResponseEntity<ApiResponse<List<PagedUpdatedComplianceListDto>>> getPagedUpdatedComplianceListData(@Parameter(description = "Time/seconds are optional", example = "9876543")
@RequestParam(required = true) String fromDate,
@Parameter(description = "Time/seconds are optional. If unspecified, the current UTC date and time is used", example = "9876543")
@RequestParam(required = true) String toDate,
@Parameter(description = "Page number to display.", example = "9876543")
@RequestParam(required = true) String pageNumber,
@Parameter(description = "How many elements will be on the single page. Maximum allowed is 1000.", example = "9876543")
@RequestParam(required = true) String pageSize) {
return execute(() -> pagedUpdatedComplianceListService.getPagedUpdatedComplianceListData(fromDate, toDate, pageNumber, pageSize));
}
}

파일 보기

@ -0,0 +1,23 @@
package com.snp.batch.jobs.web.compliance.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ComplianceValuesMeaningDto {
@JsonProperty("complianceValue")
private Integer complianceValue;
@JsonProperty("meaning")
private String meaning;
}

파일 보기

@ -0,0 +1,122 @@
package com.snp.batch.jobs.web.compliance.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CompliancesByImosDto {
@JsonProperty("lrimoShipNo")
private String lrimoShipNo;
@JsonProperty("dateAmended")
private String dateAmended;
@JsonProperty("legalOverall")
private Integer legalOverall;
@JsonProperty("shipBESSanctionList")
private Integer shipBESSanctionList;
@JsonProperty("shipDarkActivityIndicator")
private Integer shipDarkActivityIndicator;
@JsonProperty("shipDetailsNoLongerMaintained")
private Integer shipDetailsNoLongerMaintained;
@JsonProperty("shipEUSanctionList")
private Integer shipEUSanctionList;
@JsonProperty("shipFlagDisputed")
private Integer shipFlagDisputed;
@JsonProperty("shipFlagSanctionedCountry")
private Integer shipFlagSanctionedCountry;
@JsonProperty("shipHistoricalFlagSanctionedCountry")
private Integer shipHistoricalFlagSanctionedCountry;
@JsonProperty("shipOFACNonSDNSanctionList")
private Integer shipOFACNonSDNSanctionList;
@JsonProperty("shipOFACSanctionList")
private Integer shipOFACSanctionList;
@JsonProperty("shipOFACAdvisoryList")
private Integer shipOFACAdvisoryList;
@JsonProperty("shipOwnerOFACSSIList")
private Integer shipOwnerOFACSSIList;
@JsonProperty("shipOwnerAustralianSanctionList")
private Integer shipOwnerAustralianSanctionList;
@JsonProperty("shipOwnerBESSanctionList")
private Integer shipOwnerBESSanctionList;
@JsonProperty("shipOwnerCanadianSanctionList")
private Integer shipOwnerCanadianSanctionList;
@JsonProperty("shipOwnerEUSanctionList")
private Integer shipOwnerEUSanctionList;
@JsonProperty("shipOwnerFATFJurisdiction")
private Integer shipOwnerFATFJurisdiction;
@JsonProperty("shipOwnerHistoricalOFACSanctionedCountry")
private Integer shipOwnerHistoricalOFACSanctionedCountry;
@JsonProperty("shipOwnerOFACSanctionList")
private Integer shipOwnerOFACSanctionList;
@JsonProperty("shipOwnerOFACSanctionedCountry")
private Integer shipOwnerOFACSanctionedCountry;
@JsonProperty("shipOwnerParentCompanyNonCompliance")
private Integer shipOwnerParentCompanyNonCompliance;
@JsonProperty("shipOwnerParentFATFJurisdiction")
private String shipOwnerParentFATFJurisdiction;
@JsonProperty("shipOwnerParentOFACSanctionedCountry")
private String shipOwnerParentOFACSanctionedCountry;
@JsonProperty("shipOwnerSwissSanctionList")
private Integer shipOwnerSwissSanctionList;
@JsonProperty("shipOwnerUAESanctionList")
private Integer shipOwnerUAESanctionList;
@JsonProperty("shipOwnerUNSanctionList")
private Integer shipOwnerUNSanctionList;
@JsonProperty("shipSanctionedCountryPortCallLast12m")
private Integer shipSanctionedCountryPortCallLast12m;
@JsonProperty("shipSanctionedCountryPortCallLast3m")
private Integer shipSanctionedCountryPortCallLast3m;
@JsonProperty("shipSanctionedCountryPortCallLast6m")
private Integer shipSanctionedCountryPortCallLast6m;
@JsonProperty("shipSecurityLegalDisputeEvent")
private Integer shipSecurityLegalDisputeEvent;
@JsonProperty("shipSTSPartnerNonComplianceLast12m")
private Integer shipSTSPartnerNonComplianceLast12m;
@JsonProperty("shipSwissSanctionList")
private Integer shipSwissSanctionList;
@JsonProperty("shipUNSanctionList")
private Integer shipUNSanctionList;
}

파일 보기

@ -0,0 +1,45 @@
package com.snp.batch.jobs.web.compliance.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PagedUpdatedComplianceListDto {
@JsonProperty("pageSize")
private Integer pageSize;
@JsonProperty("pageNumber")
private Integer pageNumber;
@JsonProperty("firstPage")
private String firstPage;
@JsonProperty("lastPage")
private String lastPage;
@JsonProperty("totalPages")
private Integer totalPages;
@JsonProperty("totalRecords")
private Integer totalRecords;
@JsonProperty("nextPage")
private String nextPage;
@JsonProperty("previousPage")
private String previousPage;
@JsonProperty("data")
private List<Object> data;
}

파일 보기

@ -0,0 +1,122 @@
package com.snp.batch.jobs.web.compliance.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UpdatedComplianceListDto {
@JsonProperty("lrimoShipNo")
private String lrimoShipNo;
@JsonProperty("dateAmended")
private String dateAmended;
@JsonProperty("legalOverall")
private Integer legalOverall;
@JsonProperty("shipBESSanctionList")
private Integer shipBESSanctionList;
@JsonProperty("shipDarkActivityIndicator")
private Integer shipDarkActivityIndicator;
@JsonProperty("shipDetailsNoLongerMaintained")
private Integer shipDetailsNoLongerMaintained;
@JsonProperty("shipEUSanctionList")
private Integer shipEUSanctionList;
@JsonProperty("shipFlagDisputed")
private Integer shipFlagDisputed;
@JsonProperty("shipFlagSanctionedCountry")
private Integer shipFlagSanctionedCountry;
@JsonProperty("shipHistoricalFlagSanctionedCountry")
private Integer shipHistoricalFlagSanctionedCountry;
@JsonProperty("shipOFACNonSDNSanctionList")
private Integer shipOFACNonSDNSanctionList;
@JsonProperty("shipOFACSanctionList")
private Integer shipOFACSanctionList;
@JsonProperty("shipOFACAdvisoryList")
private Integer shipOFACAdvisoryList;
@JsonProperty("shipOwnerOFACSSIList")
private Integer shipOwnerOFACSSIList;
@JsonProperty("shipOwnerAustralianSanctionList")
private Integer shipOwnerAustralianSanctionList;
@JsonProperty("shipOwnerBESSanctionList")
private Integer shipOwnerBESSanctionList;
@JsonProperty("shipOwnerCanadianSanctionList")
private Integer shipOwnerCanadianSanctionList;
@JsonProperty("shipOwnerEUSanctionList")
private Integer shipOwnerEUSanctionList;
@JsonProperty("shipOwnerFATFJurisdiction")
private Integer shipOwnerFATFJurisdiction;
@JsonProperty("shipOwnerHistoricalOFACSanctionedCountry")
private Integer shipOwnerHistoricalOFACSanctionedCountry;
@JsonProperty("shipOwnerOFACSanctionList")
private Integer shipOwnerOFACSanctionList;
@JsonProperty("shipOwnerOFACSanctionedCountry")
private Integer shipOwnerOFACSanctionedCountry;
@JsonProperty("shipOwnerParentCompanyNonCompliance")
private Integer shipOwnerParentCompanyNonCompliance;
@JsonProperty("shipOwnerParentFATFJurisdiction")
private Integer shipOwnerParentFATFJurisdiction;
@JsonProperty("shipOwnerParentOFACSanctionedCountry")
private Integer shipOwnerParentOFACSanctionedCountry;
@JsonProperty("shipOwnerSwissSanctionList")
private Integer shipOwnerSwissSanctionList;
@JsonProperty("shipOwnerUAESanctionList")
private Integer shipOwnerUAESanctionList;
@JsonProperty("shipOwnerUNSanctionList")
private Integer shipOwnerUNSanctionList;
@JsonProperty("shipSanctionedCountryPortCallLast12m")
private Integer shipSanctionedCountryPortCallLast12m;
@JsonProperty("shipSanctionedCountryPortCallLast3m")
private Integer shipSanctionedCountryPortCallLast3m;
@JsonProperty("shipSanctionedCountryPortCallLast6m")
private Integer shipSanctionedCountryPortCallLast6m;
@JsonProperty("shipSecurityLegalDisputeEvent")
private Integer shipSecurityLegalDisputeEvent;
@JsonProperty("shipSTSPartnerNonComplianceLast12m")
private Integer shipSTSPartnerNonComplianceLast12m;
@JsonProperty("shipSwissSanctionList")
private Integer shipSwissSanctionList;
@JsonProperty("shipUNSanctionList")
private Integer shipUNSanctionList;
}

파일 보기

@ -0,0 +1,34 @@
package com.snp.batch.jobs.web.compliance.service;
import com.snp.batch.jobs.web.compliance.dto.ComplianceValuesMeaningDto;
import com.snp.batch.common.web.service.BaseBypassService;
import java.util.List;
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;
/**
* 모든 Compliance 지표 조회 bypass 서비스
* 외부 Maritime API에서 데이터를 실시간 조회하여 그대로 반환
*/
@Service
public class ComplianceValuesMeaningService extends BaseBypassService<ComplianceValuesMeaningDto> {
public ComplianceValuesMeaningService(
@Qualifier("maritimeServiceApiWebClient") WebClient webClient) {
super(webClient, "/RiskAndCompliance/ComplianceValuesMeaning", "모든 Compliance 지표 조회",
new ParameterizedTypeReference<>() {},
new ParameterizedTypeReference<>() {});
}
/**
* 모든 Compliance 지표 조회 데이터를 조회합니다.
*
* @return 모든 Compliance 지표 조회
*/
public List<ComplianceValuesMeaningDto> getComplianceValuesMeaningData() {
return fetchGetList(uri -> uri.path(getApiPath())
.build());
}
}

파일 보기

@ -0,0 +1,35 @@
package com.snp.batch.jobs.web.compliance.service;
import com.snp.batch.jobs.web.compliance.dto.CompliancesByImosDto;
import com.snp.batch.common.web.service.BaseBypassService;
import java.util.List;
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;
/**
* IMO 기반 Compliance 조회 bypass 서비스
* 외부 Maritime API에서 데이터를 실시간 조회하여 그대로 반환
*/
@Service
public class CompliancesByImosService extends BaseBypassService<CompliancesByImosDto> {
public CompliancesByImosService(
@Qualifier("maritimeServiceApiWebClient") WebClient webClient) {
super(webClient, "/RiskAndCompliance/CompliancesByImos", "IMO 기반 Compliance 조회",
new ParameterizedTypeReference<>() {},
new ParameterizedTypeReference<>() {});
}
/**
* IMO 기반 Compliance 조회 데이터를 조회합니다.
*
* @return IMO 기반 Compliance 조회
*/
public List<CompliancesByImosDto> getCompliancesByImosData(String imos) {
return fetchGetList(uri -> uri.path(getApiPath())
.queryParam("imos", imos)
.build());
}
}

파일 보기

@ -0,0 +1,38 @@
package com.snp.batch.jobs.web.compliance.service;
import com.snp.batch.jobs.web.compliance.dto.PagedUpdatedComplianceListDto;
import com.snp.batch.common.web.service.BaseBypassService;
import java.util.List;
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;
/**
* PagedUpdatedComplianceList bypass 서비스
* 외부 Maritime API에서 데이터를 실시간 조회하여 그대로 반환
*/
@Service
public class PagedUpdatedComplianceListService extends BaseBypassService<PagedUpdatedComplianceListDto> {
public PagedUpdatedComplianceListService(
@Qualifier("maritimeServiceApiWebClient") WebClient webClient) {
super(webClient, "/RiskAndCompliance/PagedUpdatedComplianceList", "PagedUpdatedComplianceList",
new ParameterizedTypeReference<>() {},
new ParameterizedTypeReference<>() {});
}
/**
* PagedUpdatedComplianceList 데이터를 조회합니다.
*
* @return PagedUpdatedComplianceList
*/
public List<PagedUpdatedComplianceListDto> getPagedUpdatedComplianceListData(String fromDate, String toDate, String pageNumber, String pageSize) {
return fetchGetList(uri -> uri.path(getApiPath())
.queryParam("fromDate", fromDate)
.queryParam("toDate", toDate)
.queryParam("pageNumber", pageNumber)
.queryParam("pageSize", pageSize)
.build());
}
}

파일 보기

@ -0,0 +1,36 @@
package com.snp.batch.jobs.web.compliance.service;
import com.snp.batch.jobs.web.compliance.dto.UpdatedComplianceListDto;
import com.snp.batch.common.web.service.BaseBypassService;
import java.util.List;
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;
/**
* 기간 변경 Compliance 조회 bypass 서비스
* 외부 Maritime API에서 데이터를 실시간 조회하여 그대로 반환
*/
@Service
public class UpdatedComplianceListService extends BaseBypassService<UpdatedComplianceListDto> {
public UpdatedComplianceListService(
@Qualifier("maritimeServiceApiWebClient") WebClient webClient) {
super(webClient, "/RiskAndCompliance/UpdatedComplianceList", "기간 내 변경 Compliance 조회",
new ParameterizedTypeReference<>() {},
new ParameterizedTypeReference<>() {});
}
/**
* 기간 변경 Compliance 조회 데이터를 조회합니다.
*
* @return 기간 변경 Compliance 조회
*/
public List<UpdatedComplianceListDto> getUpdatedComplianceListData(String fromDate, String toDate) {
return fetchGetList(uri -> uri.path(getApiPath())
.queryParam("fromDate", fromDate)
.queryParam("toDate", toDate)
.build());
}
}

파일 보기

@ -99,6 +99,11 @@ public class BypassCodeGenerator {
boolean needsLocalDateTime = fields.stream()
.anyMatch(f -> "LocalDateTime".equals(f.getFieldType()));
boolean needsList = fields.stream()
.anyMatch(f -> f.getFieldType() != null && f.getFieldType().startsWith("List"));
boolean needsMap = fields.stream()
.anyMatch(f -> f.getFieldType() != null && f.getFieldType().startsWith("Map"));
StringBuilder imports = new StringBuilder();
imports.append("import com.fasterxml.jackson.annotation.JsonProperty;\n");
imports.append("import lombok.AllArgsConstructor;\n");
@ -109,6 +114,12 @@ public class BypassCodeGenerator {
if (needsLocalDateTime) {
imports.append("import java.time.LocalDateTime;\n");
}
if (needsList) {
imports.append("import java.util.List;\n");
}
if (needsMap) {
imports.append("import java.util.Map;\n");
}
StringBuilder fieldLines = new StringBuilder();
for (BypassApiField field : fields) {

파일 보기

@ -32,6 +32,20 @@ public class JsonSchemaParser {
* @return 필드 목록 (파싱 실패 목록 반환)
*/
public List<BypassFieldDto> parse(String jsonSample) {
return parse(jsonSample, null);
}
/**
* JSON 샘플 문자열에서 필드 목록을 추출합니다.
* targetField가 지정된 경우 해당 필드 내부를 파싱 대상으로 사용합니다.
* 배열이면 번째 요소를 파싱합니다.
*
* @param jsonSample JSON 샘플 문자열
* @param targetField 파싱할 대상 필드명 (null이면 루트 파싱)
* : "data" root.data[0] 내부를 파싱
* @return 필드 목록 (파싱 실패 목록 반환)
*/
public List<BypassFieldDto> parse(String jsonSample, String targetField) {
List<BypassFieldDto> result = new ArrayList<>();
try {
JsonNode root = objectMapper.readTree(jsonSample);
@ -43,13 +57,35 @@ public class JsonSchemaParser {
return result;
}
// targetField가 지정된 경우 해당 필드 내부를 파싱 대상으로 사용
if (targetField != null && !targetField.isEmpty()) {
JsonNode nested = target.get(targetField);
if (nested != null) {
if (nested.isArray() && nested.size() > 0) {
target = nested.get(0);
} else if (nested.isObject()) {
target = nested;
} else {
log.warn("JSON 파싱: targetField='{}' 가 객체나 배열이 아닙니다.", targetField);
return result;
}
if (!target.isObject()) {
log.warn("JSON 파싱: targetField='{}' 내부에서 유효한 객체를 찾을 수 없습니다.", targetField);
return result;
}
} else {
log.warn("JSON 파싱: targetField='{}' 를 찾을 수 없습니다.", targetField);
return result;
}
}
int sortOrder = 0;
var fields = target.fields();
while (fields.hasNext()) {
var entry = fields.next();
String originalKey = entry.getKey();
String fieldName = toCamelCase(originalKey);
String fieldType = inferType(entry.getValue());
String fieldType = inferType(entry.getValue(), fieldName);
// jsonProperty: fieldName과 원본 키가 다를 때만 설정
String jsonProperty = fieldName.equals(originalKey) ? null : originalKey;
@ -82,9 +118,13 @@ public class JsonSchemaParser {
}
/**
* JsonNode에서 Java 타입 추론
* JsonNode에서 Java 타입 추론.
* 배열 번째 요소가 ObjectNode인 경우 "{FieldName}Item" 타입으로 처리합니다.
*
* @param node 파싱할 노드
* @param fieldName 필드명 (배열 객체 타입명 생성에 사용)
*/
private String inferType(JsonNode node) {
private String inferType(JsonNode node, String fieldName) {
if (node.isTextual()) {
return "String";
}
@ -104,6 +144,10 @@ public class JsonSchemaParser {
return "String";
}
if (node.isArray()) {
if (node.size() > 0 && node.get(0).isObject()) {
String itemTypeName = capitalize(fieldName) + "Item";
return "List<" + itemTypeName + ">";
}
return "List<Object>";
}
if (node.isObject()) {
@ -111,4 +155,11 @@ public class JsonSchemaParser {
}
return "String";
}
private String capitalize(String s) {
if (s == null || s.isEmpty()) {
return s;
}
return Character.toUpperCase(s.charAt(0)) + s.substring(1);
}
}