release: 2026-03-31 (40건 커밋) #118

병합
HYOJIN develop 에서 main 로 40 commits 를 머지했습니다 2026-03-31 11:09:31 +09:00
24개의 변경된 파일36개의 추가작업 그리고 1488개의 파일을 삭제
Showing only changes of commit 82e7074b1c - Show all commits

파일 보기

@ -17,25 +17,14 @@ export interface BypassParamDto {
sortOrder: number;
}
export interface BypassFieldDto {
id?: number;
fieldName: string;
jsonProperty: string | null;
fieldType: string; // String, Integer, Long, Double, Boolean, LocalDateTime, List<Object>, Object
description: string;
sortOrder: number;
}
export interface BypassConfigRequest {
domainName: string;
displayName: string;
webclientBean: string;
externalPath: string;
httpMethod: string;
responseType: string;
description: string;
params: BypassParamDto[];
fields: BypassFieldDto[];
}
export interface BypassConfigResponse {
@ -46,20 +35,17 @@ export interface BypassConfigResponse {
webclientBean: string;
externalPath: string;
httpMethod: string;
responseType: string;
description: string;
generated: boolean;
generatedAt: string | null;
createdAt: string;
updatedAt: string;
params: BypassParamDto[];
fields: BypassFieldDto[];
}
export interface CodeGenerationResult {
controllerPath: string;
servicePaths: string[];
dtoPaths: string[];
message: string;
}
@ -117,16 +103,6 @@ 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, 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 하지 않음
});
if (!res.ok) throw new Error(`API Error: ${res.status} ${res.statusText}`);
return res.json();
},
getWebclientBeans: () =>
fetchJson<ApiResponse<WebClientBeanInfo[]>>(`${BASE}/webclient-beans`),
};

파일 보기

@ -3,12 +3,10 @@ import type {
BypassConfigRequest,
BypassConfigResponse,
BypassParamDto,
BypassFieldDto,
WebClientBeanInfo,
} from '../../api/bypassApi';
import BypassStepBasic from './BypassStepBasic';
import BypassStepParams from './BypassStepParams';
import BypassStepFields from './BypassStepFields';
interface BypassConfigModalProps {
open: boolean;
@ -18,21 +16,19 @@ interface BypassConfigModalProps {
onClose: () => void;
}
type StepNumber = 1 | 2 | 3;
type StepNumber = 1 | 2;
const STEP_LABELS: Record<StepNumber, string> = {
1: '기본 정보',
2: '파라미터',
3: 'DTO 필드',
};
const DEFAULT_FORM: Omit<BypassConfigRequest, 'params' | 'fields'> = {
const DEFAULT_FORM: Omit<BypassConfigRequest, 'params'> = {
domainName: '',
displayName: '',
webclientBean: '',
externalPath: '',
httpMethod: 'GET',
responseType: 'LIST',
description: '',
};
@ -49,10 +45,8 @@ export default function BypassConfigModal({
const [webclientBean, setWebclientBean] = useState('');
const [externalPath, setExternalPath] = useState('');
const [httpMethod, setHttpMethod] = useState('GET');
const [responseType, setResponseType] = useState('LIST');
const [description, setDescription] = useState('');
const [params, setParams] = useState<BypassParamDto[]>([]);
const [fields, setFields] = useState<BypassFieldDto[]>([]);
const [saving, setSaving] = useState(false);
useEffect(() => {
@ -64,20 +58,16 @@ export default function BypassConfigModal({
setWebclientBean(editConfig.webclientBean);
setExternalPath(editConfig.externalPath);
setHttpMethod(editConfig.httpMethod);
setResponseType(editConfig.responseType);
setDescription(editConfig.description);
setParams(editConfig.params);
setFields(editConfig.fields);
} else {
setDomainName(DEFAULT_FORM.domainName);
setDisplayName(DEFAULT_FORM.displayName);
setWebclientBean(DEFAULT_FORM.webclientBean);
setExternalPath(DEFAULT_FORM.externalPath);
setHttpMethod(DEFAULT_FORM.httpMethod);
setResponseType(DEFAULT_FORM.responseType);
setDescription(DEFAULT_FORM.description);
setParams([]);
setFields([]);
}
}, [open, editConfig]);
@ -90,7 +80,6 @@ export default function BypassConfigModal({
case 'webclientBean': setWebclientBean(value); break;
case 'externalPath': setExternalPath(value); break;
case 'httpMethod': setHttpMethod(value); break;
case 'responseType': setResponseType(value); break;
case 'description': setDescription(value); break;
}
};
@ -104,10 +93,8 @@ export default function BypassConfigModal({
webclientBean,
externalPath,
httpMethod,
responseType,
description,
params,
fields,
});
onClose();
} finally {
@ -115,7 +102,7 @@ export default function BypassConfigModal({
}
};
const steps: StepNumber[] = [1, 2, 3];
const steps: StepNumber[] = [1, 2];
return (
<div
@ -175,7 +162,6 @@ export default function BypassConfigModal({
webclientBean={webclientBean}
externalPath={externalPath}
httpMethod={httpMethod}
responseType={responseType}
description={description}
webclientBeans={webclientBeans}
isEdit={editConfig !== null}
@ -185,9 +171,6 @@ export default function BypassConfigModal({
{step === 2 && (
<BypassStepParams params={params} onChange={setParams} />
)}
{step === 3 && (
<BypassStepFields fields={fields} onChange={setFields} />
)}
</div>
{/* 하단 버튼 */}
@ -213,7 +196,7 @@ export default function BypassConfigModal({
</button>
)}
{step < 3 ? (
{step < 2 ? (
<button
type="button"
onClick={() => setStep((s) => (s + 1) as StepNumber)}

파일 보기

@ -6,7 +6,6 @@ interface BypassStepBasicProps {
webclientBean: string;
externalPath: string;
httpMethod: string;
responseType: string;
description: string;
webclientBeans: WebClientBeanInfo[];
isEdit: boolean;
@ -19,7 +18,6 @@ export default function BypassStepBasic({
webclientBean,
externalPath,
httpMethod,
responseType,
description,
webclientBeans,
isEdit,
@ -125,30 +123,6 @@ export default function BypassStepBasic({
</div>
</div>
{/* 응답 타입 */}
<div>
<label className="block text-sm font-medium text-wing-text mb-1">
<span className="text-red-500">*</span>
</label>
<div className="flex gap-2">
{['LIST', 'OBJECT'].map((type) => (
<button
key={type}
type="button"
onClick={() => onChange('responseType', type)}
className={[
'flex-1 py-2 text-sm font-medium rounded-lg border transition-colors',
responseType === type
? 'bg-wing-accent text-white border-wing-accent'
: 'bg-wing-card text-wing-muted border-wing-border hover:bg-wing-hover',
].join(' ')}
>
{type}
</button>
))}
</div>
</div>
{/* 설명 */}
<div className="md:col-span-2">
<label className="block text-sm font-medium text-wing-text mb-1">

파일 보기

@ -1,289 +0,0 @@
import { useState } from 'react';
import type { BypassFieldDto } from '../../api/bypassApi';
import { bypassApi } from '../../api/bypassApi';
interface BypassStepFieldsProps {
fields: BypassFieldDto[];
onChange: (fields: BypassFieldDto[]) => void;
}
type ActiveTab = 'manual' | 'json';
const FIELD_TYPES = ['String', 'Integer', 'Long', 'Double', 'Boolean', 'LocalDateTime', 'List<Object>', 'Object'];
function createEmptyField(sortOrder: number): BypassFieldDto {
return {
fieldName: '',
jsonProperty: null,
fieldType: 'String',
description: '',
sortOrder,
};
}
export default function BypassStepFields({ fields, onChange }: BypassStepFieldsProps) {
const [activeTab, setActiveTab] = useState<ActiveTab>('manual');
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)]);
};
const handleDelete = (index: number) => {
const updated = fields
.filter((_, i) => i !== index)
.map((f, i) => ({ ...f, sortOrder: i }));
onChange(updated);
};
const handleChange = (index: number, field: keyof BypassFieldDto, value: string | null) => {
const updated = fields.map((f, i) =>
i === index ? { ...f, [field]: value } : f,
);
onChange(updated);
};
const handleParseJson = async () => {
if (!jsonSample.trim()) {
setParseError('JSON 샘플을 입력하세요.');
return;
}
setParseError(null);
setParsing(true);
try {
const tf = parseMode === 'field' ? targetField : undefined;
const result = await bypassApi.parseJson(jsonSample.trim(), tf);
if (result.success && result.data) {
onChange(result.data);
setActiveTab('manual');
} else {
setParseError(result.message ?? 'JSON 파싱에 실패했습니다.');
}
} catch (err) {
setParseError(err instanceof Error ? err.message : 'JSON 파싱 중 오류가 발생했습니다.');
} finally {
setParsing(false);
}
};
return (
<div className="space-y-4">
<p className="text-sm text-wing-muted">
DTO에 . JSON .
</p>
{/* 탭 */}
<div className="flex gap-1 border-b border-wing-border">
<button
type="button"
onClick={() => setActiveTab('manual')}
className={[
'px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors',
activeTab === 'manual'
? 'border-wing-accent text-wing-accent'
: 'border-transparent text-wing-muted hover:text-wing-text',
].join(' ')}
>
</button>
<button
type="button"
onClick={() => setActiveTab('json')}
className={[
'px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors',
activeTab === 'json'
? 'border-wing-accent text-wing-accent'
: 'border-transparent text-wing-muted hover:text-wing-text',
].join(' ')}
>
JSON
</button>
</div>
{/* 직접 입력 탭 */}
{activeTab === 'manual' && (
<div className="space-y-4">
{fields.length === 0 ? (
<div className="py-10 text-center text-sm text-wing-muted border border-dashed border-wing-border rounded-lg bg-wing-card">
DTO . JSON .
</div>
) : (
<div className="overflow-x-auto">
<div className="mb-2 flex items-center justify-between">
<span className="text-xs text-wing-muted">
<span className="font-semibold text-wing-text">{fields.length}</span>
</span>
</div>
<table className="w-full text-sm">
<thead>
<tr className="border-b border-wing-border">
<th className="pb-2 text-left font-medium text-wing-muted pr-3 w-10">#</th>
<th className="pb-2 text-left font-medium text-wing-muted pr-3 min-w-[130px]"></th>
<th className="pb-2 text-left font-medium text-wing-muted pr-3 min-w-[130px]">JSON Property</th>
<th className="pb-2 text-left font-medium text-wing-muted pr-3 min-w-[150px]"></th>
<th className="pb-2 text-left font-medium text-wing-muted pr-3"></th>
<th className="pb-2 w-10"></th>
</tr>
</thead>
<tbody className="divide-y divide-wing-border">
{fields.map((field, index) => (
<tr key={index} className="group">
<td className="py-2 pr-3">
<span className="text-xs font-medium text-wing-muted tabular-nums">
#{index + 1}
</span>
</td>
<td className="py-2 pr-3">
<input
type="text"
value={field.fieldName}
onChange={(e) => handleChange(index, 'fieldName', e.target.value)}
placeholder="fieldName"
className="w-full px-2 py-1.5 text-sm rounded 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"
/>
</td>
<td className="py-2 pr-3">
<input
type="text"
value={field.jsonProperty ?? ''}
onChange={(e) => handleChange(index, 'jsonProperty', e.target.value || null)}
placeholder="원본과 같으면 비워두세요"
className="w-full px-2 py-1.5 text-sm rounded 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"
/>
</td>
<td className="py-2 pr-3">
<select
value={field.fieldType}
onChange={(e) => handleChange(index, 'fieldType', e.target.value)}
className="w-full px-2 py-1.5 text-sm rounded border border-wing-border bg-wing-surface text-wing-text focus:outline-none focus:ring-1 focus:ring-wing-accent/50"
>
{FIELD_TYPES.map((t) => (
<option key={t} value={t}>{t}</option>
))}
</select>
</td>
<td className="py-2 pr-3">
<input
type="text"
value={field.description}
onChange={(e) => handleChange(index, 'description', e.target.value)}
placeholder="필드 설명"
className="w-full px-2 py-1.5 text-sm rounded 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"
/>
</td>
<td className="py-2">
<button
type="button"
onClick={() => handleDelete(index)}
className="p-1.5 text-wing-muted hover:text-red-500 hover:bg-red-50 rounded transition-colors"
title="삭제"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
<button
type="button"
onClick={handleAdd}
className="flex items-center gap-1.5 px-3 py-2 text-sm font-medium text-wing-accent border border-wing-accent/30 rounded-lg hover:bg-wing-accent/10 transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
</button>
</div>
)}
{/* JSON 샘플 탭 */}
{activeTab === 'json' && (
<div className="space-y-3">
<p className="text-xs text-wing-muted">
JSON . .
</p>
<textarea
value={jsonSample}
onChange={(e) => {
setJsonSample(e.target.value);
setParseError(null);
}}
rows={10}
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>
)}
<button
type="button"
onClick={handleParseJson}
disabled={parsing}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-wing-accent hover:bg-wing-accent/80 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg transition-colors"
>
{parsing && (
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
</svg>
)}
{parsing ? '파싱 중...' : '파싱'}
</button>
</div>
)}
</div>
);
}

파일 보기

@ -475,14 +475,6 @@ export default function BypassConfig() {
<span className="font-mono text-wing-muted break-all">{path}</span>
</div>
))}
{generationResult.dtoPaths.map((path, idx) => (
<div key={`dto-${idx}`} className="flex gap-2 text-xs">
<span className="w-20 font-medium text-wing-accent shrink-0">
DTO {generationResult.dtoPaths.length > 1 ? idx + 1 : ''}
</span>
<span className="font-mono text-wing-muted break-all">{path}</span>
</div>
))}
</div>
<div className="flex items-start gap-2 bg-amber-50 text-amber-700 rounded-lg p-3 text-xs">
<span className="shrink-0">&#9888;</span>

파일 보기

@ -3,13 +3,11 @@ package com.snp.batch.global.controller;
import com.snp.batch.common.web.ApiResponse;
import com.snp.batch.global.dto.BypassConfigRequest;
import com.snp.batch.global.dto.BypassConfigResponse;
import com.snp.batch.global.dto.BypassFieldDto;
import com.snp.batch.global.dto.CodeGenerationResult;
import com.snp.batch.global.model.BypassApiConfig;
import com.snp.batch.global.repository.BypassApiConfigRepository;
import com.snp.batch.service.BypassCodeGenerator;
import com.snp.batch.service.BypassConfigService;
import com.snp.batch.service.JsonSchemaParser;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
@ -40,7 +38,6 @@ public class BypassConfigController {
private final BypassConfigService bypassConfigService;
private final BypassCodeGenerator bypassCodeGenerator;
private final JsonSchemaParser jsonSchemaParser;
private final BypassApiConfigRepository configRepository;
@Operation(summary = "설정 목록 조회")
@ -79,7 +76,7 @@ public class BypassConfigController {
@Operation(
summary = "코드 생성",
description = "등록된 설정의 도메인 전체를 기반으로 Controller, Service, DTO Java 소스 코드를 생성합니다. 같은 도메인의 모든 설정을 하나의 Controller로 합칩니다."
description = "등록된 설정의 도메인 전체를 기반으로 Controller, Service 소스 코드를 생성합니다. 같은 도메인의 모든 설정을 하나의 Controller로 합칩니다."
)
@PostMapping("/{id}/generate")
public ResponseEntity<ApiResponse<CodeGenerationResult>> generateCode(
@ -89,12 +86,10 @@ public class BypassConfigController {
BypassApiConfig config = configRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("설정을 찾을 수 없습니다: " + id));
// 같은 도메인의 모든 설정을 조회하여 함께 생성
List<BypassApiConfig> domainConfigs = configRepository.findByDomainNameOrderById(config.getDomainName());
CodeGenerationResult result = bypassCodeGenerator.generate(domainConfigs, force);
// 같은 도메인의 모든 설정을 generated로 마킹
domainConfigs.forEach(c -> bypassConfigService.markAsGenerated(c.getId()));
return ResponseEntity.ok(ApiResponse.success(result));
@ -106,26 +101,7 @@ public class BypassConfigController {
}
}
@Operation(
summary = "JSON 샘플 파싱",
description = "JSON 샘플에서 DTO 필드 목록을 추출합니다. targetField를 지정하면 해당 필드 내부를 파싱합니다. 예: targetField=data → root.data[0] 내부 필드 추출."
)
@PostMapping("/parse-json")
public ResponseEntity<ApiResponse<List<BypassFieldDto>>> parseJson(
@RequestBody String jsonSample,
@RequestParam(required = false) String targetField) {
try {
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()));
}
}
@Operation(
summary = "WebClient 빈 목록",
description = "사용 가능한 WebClient 빈 이름 목록을 반환합니다."
)
@Operation(summary = "WebClient 빈 목록", description = "사용 가능한 WebClient 빈 이름 목록을 반환합니다.")
@GetMapping("/webclient-beans")
public ResponseEntity<ApiResponse<List<Map<String, String>>>> getWebclientBeans() {
List<Map<String, String>> beans = List.of(

파일 보기

@ -31,15 +31,9 @@ public class BypassConfigRequest {
/** HTTP 메서드 */
private String httpMethod;
/** 응답 타입 */
private String responseType;
/** 설명 */
private String description;
/** 파라미터 목록 */
private List<BypassParamDto> params;
/** 응답 필드 목록 */
private List<BypassFieldDto> fields;
}

파일 보기

@ -37,9 +37,6 @@ public class BypassConfigResponse {
/** HTTP 메서드 */
private String httpMethod;
/** 응답 타입 */
private String responseType;
/** 설명 */
private String description;
@ -57,7 +54,4 @@ public class BypassConfigResponse {
/** 파라미터 목록 */
private List<BypassParamDto> params;
/** 응답 필드 목록 */
private List<BypassFieldDto> fields;
}

파일 보기

@ -1,33 +0,0 @@
package com.snp.batch.global.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* BYPASS API 응답 필드 정보 DTO
*/
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BypassFieldDto {
private Long id;
/** 필드 이름 (Java 클래스 필드명) */
private String fieldName;
/** JSON 프로퍼티명 (null이면 fieldName 사용) */
private String jsonProperty;
/** 필드 타입 (String, Integer, Long, Double, Boolean, LocalDateTime) */
private String fieldType;
/** 필드 설명 */
private String description;
/** 정렬 순서 */
private Integer sortOrder;
}

파일 보기

@ -76,14 +76,6 @@ public class BypassApiConfig {
@Builder.Default
private String httpMethod = "GET";
/**
* 응답 타입
* : "LIST", "SINGLE"
*/
@Column(name = "response_type", nullable = false, length = 20)
@Builder.Default
private String responseType = "LIST";
/**
* 설명
*/
@ -122,13 +114,6 @@ public class BypassApiConfig {
@Builder.Default
private List<BypassApiParam> params = new ArrayList<>();
/**
* API 응답 필드 목록
*/
@OneToMany(mappedBy = "config", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<BypassApiField> fields = new ArrayList<>();
/**
* 엔티티 저장 자동 호출 (INSERT )
* endpointName이 null이면 externalPath에서 자동 추출 (마이그레이션 대응)

파일 보기

@ -1,69 +0,0 @@
package com.snp.batch.global.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import lombok.*;
/**
* BYPASS API 응답 필드 정보를 저장하는 엔티티
* BypassApiConfig에 종속되며, 외부 API 응답에서 추출할 필드 메타데이터를 정의
*/
@Entity
@Table(
name = "bypass_api_field",
uniqueConstraints = @UniqueConstraint(columnNames = {"config_id", "field_name"})
)
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BypassApiField {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 연관된 BYPASS API 설정
*/
@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "config_id", nullable = false)
private BypassApiConfig config;
/**
* 필드 이름 (Java 클래스 필드명)
* : "imoNumber", "shipName", "vesselType"
*/
@Column(name = "field_name", nullable = false, length = 100)
private String fieldName;
/**
* JSON 프로퍼티명 (null이면 fieldName 사용)
* : "imo_number", "ship_name"
*/
@Column(name = "json_property", length = 100)
private String jsonProperty;
/**
* 필드 타입
* : "String", "Integer", "Long", "Double", "Boolean", "LocalDateTime"
*/
@Column(name = "field_type", nullable = false, length = 50)
@Builder.Default
private String fieldType = "String";
/**
* 필드 설명
*/
@Column(name = "description", length = 500)
private String description;
/**
* 정렬 순서
*/
@Column(name = "sort_order", nullable = false)
@Builder.Default
private Integer sortOrder = 0;
}

파일 보기

@ -1,25 +0,0 @@
package com.snp.batch.global.repository;
import com.snp.batch.global.model.BypassApiField;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* BypassApiField Repository
* JPA Repository 방식으로 자동 구현
*/
@Repository
public interface BypassApiFieldRepository extends JpaRepository<BypassApiField, Long> {
/**
* 설정 ID로 필드 목록 조회 (정렬 순서 기준 오름차순)
*/
List<BypassApiField> findByConfigIdOrderBySortOrder(Long configId);
/**
* 설정 ID로 필드 전체 삭제
*/
void deleteByConfigId(Long configId);
}

파일 보기

@ -1,85 +0,0 @@
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));
}
}

파일 보기

@ -1,23 +0,0 @@
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;
}

파일 보기

@ -1,122 +0,0 @@
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;
}

파일 보기

@ -1,45 +0,0 @@
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;
}

파일 보기

@ -1,122 +0,0 @@
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;
}

파일 보기

@ -1,34 +0,0 @@
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());
}
}

파일 보기

@ -1,35 +0,0 @@
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());
}
}

파일 보기

@ -1,38 +0,0 @@
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());
}
}

파일 보기

@ -1,36 +0,0 @@
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());
}
}

파일 보기

@ -2,7 +2,6 @@ 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.BypassApiField;
import com.snp.batch.global.model.BypassApiParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -19,7 +18,8 @@ import java.util.stream.Collectors;
/**
* BYPASS API Java 소스 코드를 자동 생성하는 서비스.
* 같은 도메인의 N개 설정을 받아 N개의 DTO + Service와 1개의 Controller를 생성합니다.
* 모든 API는 RAW 모드(JsonNode 패스스루) 생성됩니다.
* 같은 도메인의 N개 설정을 받아 N개의 Service와 1개의 Controller를 생성합니다.
*/
@Slf4j
@Service
@ -29,11 +29,8 @@ public class BypassCodeGenerator {
/**
* 같은 도메인의 BYPASS API 코드를 생성합니다.
* 엔드포인트별 DTO/Service를 각각 생성하고, Controller 1개에 모든 엔드포인트 메서드를 합칩니다.
*
* @param configs 같은 domainName을 가진 설정 목록
* @param force 기존 파일 덮어쓰기 여부
* @return 생성 결과
* 엔드포인트별 Service를 각각 생성하고, Controller 1개에 모든 엔드포인트 메서드를 합칩니다.
* 모든 응답은 JsonNode로 패스스루됩니다 (DTO 없음).
*/
public CodeGenerationResult generate(List<BypassApiConfig> configs, boolean force) {
if (configs == null || configs.isEmpty()) {
@ -44,25 +41,12 @@ public class BypassCodeGenerator {
String domain = configs.get(0).getDomainName();
String basePath = projectRoot + "/src/main/java/com/snp/batch/jobs/web/" + domain;
List<String> dtoPaths = new ArrayList<>();
List<String> servicePaths = new ArrayList<>();
for (BypassApiConfig config : configs) {
String endpointName = config.getEndpointName();
String dtoPath = basePath + "/dto/" + endpointName + "Dto.java";
String servicePath = basePath + "/service/" + endpointName + "Service.java";
// Service/DTO: 이미 존재하면 스킵 (force=true면 재생성)
if (!force && Files.exists(Path.of(dtoPath))) {
log.info("DTO 파일 이미 존재, 스킵: {}", dtoPath);
dtoPaths.add(dtoPath);
} else {
String dtoCode = generateDtoCode(domain, endpointName, config.getFields());
Path dtoFilePath = writeFile(dtoPath, dtoCode, true);
dtoPaths.add(dtoFilePath.toString());
}
if (!force && Files.exists(Path.of(servicePath))) {
log.info("Service 파일 이미 존재, 스킵: {}", servicePath);
servicePaths.add(servicePath);
@ -85,105 +69,40 @@ public class BypassCodeGenerator {
return CodeGenerationResult.builder()
.controllerPath(controllerFilePath.toString())
.servicePaths(servicePaths)
.dtoPaths(dtoPaths)
.message("코드 생성 완료. 서버를 재시작하면 새 API가 활성화됩니다.")
.build();
}
/**
* DTO 코드 생성.
* endpointName 기반 클래스명: {EndpointName}Dto
*/
private String generateDtoCode(String domain, String endpointName, List<BypassApiField> fields) {
String packageName = BASE_PACKAGE + "." + domain + ".dto";
boolean needsLocalDateTime = fields.stream()
.anyMatch(f -> "LocalDateTime".equals(f.getFieldType()));
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");
imports.append("import lombok.Builder;\n");
imports.append("import lombok.Getter;\n");
imports.append("import lombok.NoArgsConstructor;\n");
imports.append("import lombok.Setter;\n");
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) {
String jsonProp = field.getJsonProperty() != null ? field.getJsonProperty() : field.getFieldName();
fieldLines.append(" @JsonProperty(\"").append(jsonProp).append("\")\n");
fieldLines.append(" private ").append(field.getFieldType()).append(" ").append(field.getFieldName()).append(";\n\n");
}
return """
package {{PACKAGE}};
{{IMPORTS}}
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class {{CLASS_NAME}} {
{{FIELDS}}}
"""
.replace("{{PACKAGE}}", packageName)
.replace("{{IMPORTS}}", imports.toString())
.replace("{{CLASS_NAME}}", endpointName + "Dto")
.replace("{{FIELDS}}", fieldLines.toString());
}
/**
* Service 코드 생성.
* endpointName 기반 클래스명: {EndpointName}Service
* BaseBypassService<{EndpointName}Dto> 상속합니다.
* Service 코드 생성 (RAW 모드).
* BaseBypassService<JsonNode> 상속하여 fetchRawGet/fetchRawPost로 JsonNode를 반환합니다.
*/
private String generateServiceCode(String domain, String endpointName,
BypassApiConfig config, List<BypassApiParam> params) {
String packageName = BASE_PACKAGE + "." + domain + ".service";
String dtoPackage = BASE_PACKAGE + "." + domain + ".dto";
String dtoClass = endpointName + "Dto";
String serviceClass = endpointName + "Service";
boolean isList = "LIST".equalsIgnoreCase(config.getResponseType());
boolean isPost = "POST".equalsIgnoreCase(config.getHttpMethod());
String returnType = isList ? "List<" + dtoClass + ">" : dtoClass;
String methodName = "get" + endpointName + "Data";
String fetchMethod = buildFetchMethodCall(config, params, isList, isPost);
String fetchMethod = buildFetchMethodCall(config, params, isPost);
String methodParams = buildMethodParams(params);
String listImport = isList ? "import java.util.List;\n" : "";
return """
package {{PACKAGE}};
import {{DTO_IMPORT}};
import com.fasterxml.jackson.databind.JsonNode;
import com.snp.batch.common.web.service.BaseBypassService;
{{LIST_IMPORT}}import org.springframework.beans.factory.annotation.Qualifier;
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;
/**
* {{DISPLAY_NAME}} bypass 서비스
* 외부 Maritime API에서 데이터를 실시간 조회하여 그대로 반환
* 외부 Maritime API에서 데이터를 실시간 조회하여 JSON을 그대로 반환
*/
@Service
public class {{SERVICE_CLASS}} extends BaseBypassService<{{DTO_CLASS}}> {
public class {{SERVICE_CLASS}} extends BaseBypassService<JsonNode> {
public {{SERVICE_CLASS}}(
@Qualifier("{{WEBCLIENT_BEAN}}") WebClient webClient) {
@ -194,41 +113,35 @@ public class BypassCodeGenerator {
/**
* {{DISPLAY_NAME}} 데이터를 조회합니다.
*
* @return {{DISPLAY_NAME}}
*/
public {{RETURN_TYPE}} {{METHOD_NAME}}({{METHOD_PARAMS}}) {
public JsonNode {{METHOD_NAME}}({{METHOD_PARAMS}}) {
{{FETCH_METHOD}}
}
}
"""
.replace("{{PACKAGE}}", packageName)
.replace("{{DTO_IMPORT}}", dtoPackage + "." + dtoClass)
.replace("{{LIST_IMPORT}}", listImport)
.replace("{{DISPLAY_NAME}}", config.getDisplayName())
.replace("{{SERVICE_CLASS}}", serviceClass)
.replace("{{DTO_CLASS}}", dtoClass)
.replace("{{WEBCLIENT_BEAN}}", config.getWebclientBean())
.replace("{{EXTERNAL_PATH}}", config.getExternalPath())
.replace("{{RETURN_TYPE}}", returnType)
.replace("{{METHOD_NAME}}", methodName)
.replace("{{METHOD_PARAMS}}", methodParams)
.replace("{{FETCH_METHOD}}", fetchMethod);
}
/**
* Controller 코드 생성.
* 같은 도메인의 모든 설정을 합쳐 하나의 Controller에 N개의 엔드포인트 메서드를 생성합니다.
* Controller 코드 생성 (RAW 모드).
* 모든 엔드포인트가 ResponseEntity<ApiResponse<JsonNode>> 반환합니다.
*/
private String generateControllerCode(String domain, List<BypassApiConfig> configs) {
String packageName = BASE_PACKAGE + "." + domain + ".controller";
String servicePackage = BASE_PACKAGE + "." + domain + ".service";
String dtoPackage = BASE_PACKAGE + "." + domain + ".dto";
String domainCap = capitalize(domain);
String requestMappingPath = "/api/" + domain;
// imports 합산 (중복 제거)
// imports (중복 제거)
Set<String> importSet = new LinkedHashSet<>();
importSet.add("import com.fasterxml.jackson.databind.JsonNode;");
importSet.add("import com.snp.batch.common.web.ApiResponse;");
importSet.add("import com.snp.batch.common.web.controller.BaseBypassController;");
importSet.add("import io.swagger.v3.oas.annotations.Operation;");
@ -239,7 +152,6 @@ public class BypassCodeGenerator {
importSet.add("import org.springframework.web.bind.annotation.RequestMapping;");
importSet.add("import org.springframework.web.bind.annotation.RestController;");
boolean anyList = configs.stream().anyMatch(c -> "LIST".equalsIgnoreCase(c.getResponseType()));
boolean anyPost = configs.stream().anyMatch(c -> "POST".equalsIgnoreCase(c.getHttpMethod()));
boolean anyGet = configs.stream().anyMatch(c -> !"POST".equalsIgnoreCase(c.getHttpMethod()));
boolean anyPath = configs.stream()
@ -249,29 +161,14 @@ public class BypassCodeGenerator {
boolean anyBody = configs.stream()
.anyMatch(c -> c.getParams().stream().anyMatch(p -> "BODY".equalsIgnoreCase(p.getParamIn())));
if (anyList) {
importSet.add("import java.util.List;");
}
if (anyPost) {
importSet.add("import org.springframework.web.bind.annotation.PostMapping;");
}
if (anyGet) {
importSet.add("import org.springframework.web.bind.annotation.GetMapping;");
}
if (anyPath) {
importSet.add("import org.springframework.web.bind.annotation.PathVariable;");
}
if (anyQuery) {
importSet.add("import org.springframework.web.bind.annotation.RequestParam;");
}
if (anyBody) {
importSet.add("import org.springframework.web.bind.annotation.RequestBody;");
}
if (anyPost) importSet.add("import org.springframework.web.bind.annotation.PostMapping;");
if (anyGet) importSet.add("import org.springframework.web.bind.annotation.GetMapping;");
if (anyPath) importSet.add("import org.springframework.web.bind.annotation.PathVariable;");
if (anyQuery) importSet.add("import org.springframework.web.bind.annotation.RequestParam;");
if (anyBody) importSet.add("import org.springframework.web.bind.annotation.RequestBody;");
for (BypassApiConfig config : configs) {
String endpointName = config.getEndpointName();
importSet.add("import " + dtoPackage + "." + endpointName + "Dto;");
importSet.add("import " + servicePackage + "." + endpointName + "Service;");
importSet.add("import " + servicePackage + "." + config.getEndpointName() + "Service;");
}
String importsStr = importSet.stream().collect(Collectors.joining("\n"));
@ -279,13 +176,11 @@ public class BypassCodeGenerator {
// 필드 선언부
StringBuilder fields = new StringBuilder();
for (BypassApiConfig config : configs) {
String endpointName = config.getEndpointName();
String serviceClass = endpointName + "Service";
String serviceClass = config.getEndpointName() + "Service";
String serviceField = Character.toLowerCase(serviceClass.charAt(0)) + serviceClass.substring(1);
fields.append(" private final ").append(serviceClass).append(" ").append(serviceField).append(";\n");
}
// 태그 description은 번째 config의 webclientBean 기준
String tagPrefix = getTagPrefix(configs.get(0).getWebclientBean());
String tagDescription = tagPrefix + " " + domainCap + " bypass API";
@ -293,13 +188,10 @@ public class BypassCodeGenerator {
StringBuilder methods = new StringBuilder();
for (BypassApiConfig config : configs) {
String endpointName = config.getEndpointName();
String dtoClass = endpointName + "Dto";
String serviceClass = endpointName + "Service";
String serviceField = Character.toLowerCase(serviceClass.charAt(0)) + serviceClass.substring(1);
boolean isList = "LIST".equalsIgnoreCase(config.getResponseType());
boolean isPost = "POST".equalsIgnoreCase(config.getHttpMethod());
String responseGeneric = isList ? "List<" + dtoClass + ">" : dtoClass;
String mappingAnnotation = isPost ? "@PostMapping" : "@GetMapping";
String mappingPath = buildMappingPath(config.getParams(), config.getExternalPath());
String paramAnnotations = buildControllerParamAnnotations(config.getParams());
@ -313,8 +205,7 @@ public class BypassCodeGenerator {
.append(" 데이터를 요청하고 응답을 그대로 반환합니다.\"\n");
methods.append(" )\n");
methods.append(" ").append(mappingAnnotation).append(mappingPath).append("\n");
methods.append(" public ResponseEntity<ApiResponse<").append(responseGeneric).append(">> ")
.append(methodName).append("(");
methods.append(" public ResponseEntity<ApiResponse<JsonNode>> ").append(methodName).append("(");
if (!paramAnnotations.isEmpty()) {
methods.append(paramAnnotations);
}
@ -328,7 +219,7 @@ public class BypassCodeGenerator {
+ importsStr + "\n\n"
+ "/**\n"
+ " * " + domainCap + " bypass API\n"
+ " * S&P Maritime API에서 데이터를 실시간 조회하여 그대로 반환\n"
+ " * S&P Maritime API에서 데이터를 실시간 조회하여 JSON을 그대로 반환\n"
+ " */\n"
+ "@RestController\n"
+ "@RequestMapping(\"" + requestMappingPath + "\")\n"
@ -341,10 +232,9 @@ public class BypassCodeGenerator {
}
/**
* fetch 메서드 호출 코드 생성
* RAW fetch 메서드 호출 코드 생성
*/
private String buildFetchMethodCall(BypassApiConfig config, List<BypassApiParam> params,
boolean isList, boolean isPost) {
private String buildFetchMethodCall(BypassApiConfig config, List<BypassApiParam> params, boolean isPost) {
List<BypassApiParam> queryParams = params.stream()
.filter(p -> "QUERY".equalsIgnoreCase(p.getParamIn()))
.sorted((a, b) -> Integer.compare(a.getSortOrder(), b.getSortOrder()))
@ -358,23 +248,17 @@ public class BypassCodeGenerator {
}
uriBuilder.append("\n .build()");
String fetchName;
if (isPost) {
fetchName = isList ? "fetchPostList" : "fetchPostOne";
BypassApiParam bodyParam = params.stream()
.filter(p -> "BODY".equalsIgnoreCase(p.getParamIn()))
.findFirst().orElse(null);
String bodyArg = bodyParam != null ? bodyParam.getParamName() : "null";
return "return " + fetchName + "(" + bodyArg + ", " + uriBuilder + ");";
return "return fetchRawPost(" + bodyArg + ", " + uriBuilder + ");";
} else {
fetchName = isList ? "fetchGetList" : "fetchGetOne";
return "return " + fetchName + "(" + uriBuilder + ");";
return "return fetchRawGet(" + uriBuilder + ");";
}
}
/**
* 메서드 파라미터 목록 생성 (Java 타입 + 이름)
*/
private String buildMethodParams(List<BypassApiParam> params) {
return params.stream()
.sorted((a, b) -> Integer.compare(a.getSortOrder(), b.getSortOrder()))
@ -382,9 +266,6 @@ public class BypassCodeGenerator {
.collect(Collectors.joining(", "));
}
/**
* 서비스 호출 인자 목록 생성
*/
private String buildServiceCallArgs(List<BypassApiParam> params) {
return params.stream()
.sorted((a, b) -> Integer.compare(a.getSortOrder(), b.getSortOrder()))
@ -392,9 +273,6 @@ public class BypassCodeGenerator {
.collect(Collectors.joining(", "));
}
/**
* Controller 메서드 파라미터 어노테이션 포함 목록 생성
*/
private String buildControllerParamAnnotations(List<BypassApiParam> params) {
if (params.isEmpty()) {
return "";
@ -421,12 +299,6 @@ public class BypassCodeGenerator {
.collect(Collectors.joining(",\n "));
}
/**
* @GetMapping / @PostMapping에 붙는 경로 생성
* externalPath의 마지막 세그먼트를 내부 경로로 사용 + PATH 파라미터 추가
* : /RiskAndCompliance/CompliancesByImos ("/CompliancesByImos")
* PATH 파라미터 imo 추가 ("/CompliancesByImos/{imo}")
*/
private String buildMappingPath(List<BypassApiParam> params, String externalPath) {
String endpointSegment = "";
if (externalPath != null && !externalPath.isEmpty()) {
@ -451,14 +323,6 @@ public class BypassCodeGenerator {
return "(\"" + fullPath + "\")";
}
/**
* 파일을 지정 경로에 씁니다.
*
* @param path 파일 경로
* @param content 파일 내용
* @param force 덮어쓰기 허용 여부
* @return 생성된 파일 경로
*/
private Path writeFile(String path, String content, boolean force) {
Path filePath = Path.of(path);
if (Files.exists(filePath) && !force) {
@ -474,9 +338,6 @@ public class BypassCodeGenerator {
return filePath;
}
/**
* webclientBean 이름 Swagger @Tag description 접두사 변환
*/
private String getTagPrefix(String webclientBean) {
if (webclientBean == null) {
return "[Ship API]";
@ -488,9 +349,6 @@ public class BypassCodeGenerator {
};
}
/**
* paramType Swagger @Parameter example 기본값 결정
*/
private String getDefaultExample(String paramType) {
if (paramType == null) {
return "9876543";
@ -510,9 +368,6 @@ public class BypassCodeGenerator {
return Character.toUpperCase(s.charAt(0)) + s.substring(1);
}
/**
* param_type Java 타입 변환
*/
private String toJavaType(String paramType) {
if (paramType == null) {
return "String";

파일 보기

@ -2,10 +2,8 @@ package com.snp.batch.service;
import com.snp.batch.global.dto.BypassConfigRequest;
import com.snp.batch.global.dto.BypassConfigResponse;
import com.snp.batch.global.dto.BypassFieldDto;
import com.snp.batch.global.dto.BypassParamDto;
import com.snp.batch.global.model.BypassApiConfig;
import com.snp.batch.global.model.BypassApiField;
import com.snp.batch.global.model.BypassApiParam;
import com.snp.batch.global.repository.BypassApiConfigRepository;
import jakarta.persistence.EntityManager;
@ -30,8 +28,6 @@ public class BypassConfigService {
/**
* 설정 목록 조회
*
* @return 전체 BYPASS API 설정 목록
*/
public List<BypassConfigResponse> getConfigs() {
return configRepository.findAll().stream()
@ -41,9 +37,6 @@ public class BypassConfigService {
/**
* 설정 상세 조회
*
* @param id 설정 ID
* @return 설정 상세 정보
*/
public BypassConfigResponse getConfig(Long id) {
BypassApiConfig config = configRepository.findById(id)
@ -53,9 +46,6 @@ public class BypassConfigService {
/**
* 설정 등록
*
* @param request 등록 요청 DTO
* @return 등록된 설정 정보
*/
@Transactional
public BypassConfigResponse createConfig(BypassConfigRequest request) {
@ -73,22 +63,11 @@ public class BypassConfigService {
config.getParams().add(param);
});
}
if (request.getFields() != null) {
request.getFields().forEach(f -> {
BypassApiField field = toFieldEntity(f);
field.setConfig(config);
config.getFields().add(field);
});
}
return toResponse(configRepository.save(config));
}
/**
* 설정 수정
*
* @param id 설정 ID
* @param request 수정 요청 DTO
* @return 수정된 설정 정보
*/
@Transactional
public BypassConfigResponse updateConfig(Long id, BypassConfigRequest request) {
@ -100,12 +79,10 @@ public class BypassConfigService {
config.setWebclientBean(request.getWebclientBean());
config.setExternalPath(request.getExternalPath());
config.setHttpMethod(request.getHttpMethod());
config.setResponseType(request.getResponseType());
config.setDescription(request.getDescription());
// params/fields 교체: clear flush(DELETE 실행) 새로 추가
// params 교체: clear flush(DELETE 실행) 새로 추가
config.getParams().clear();
config.getFields().clear();
entityManager.flush();
if (request.getParams() != null) {
@ -116,21 +93,11 @@ public class BypassConfigService {
});
}
if (request.getFields() != null) {
request.getFields().forEach(f -> {
BypassApiField field = toFieldEntity(f);
field.setConfig(config);
config.getFields().add(field);
});
}
return toResponse(configRepository.save(config));
}
/**
* 설정 삭제
*
* @param id 설정 ID
*/
@Transactional
public void deleteConfig(Long id) {
@ -142,8 +109,6 @@ public class BypassConfigService {
/**
* 코드 생성 완료 마킹
*
* @param id 설정 ID
*/
@Transactional
public void markAsGenerated(Long id) {
@ -165,14 +130,12 @@ public class BypassConfigService {
.webclientBean(config.getWebclientBean())
.externalPath(config.getExternalPath())
.httpMethod(config.getHttpMethod())
.responseType(config.getResponseType())
.description(config.getDescription())
.generated(config.getGenerated())
.generatedAt(config.getGeneratedAt())
.createdAt(config.getCreatedAt())
.updatedAt(config.getUpdatedAt())
.params(config.getParams().stream().map(this::toParamDto).toList())
.fields(config.getFields().stream().map(this::toFieldDto).toList())
.build();
}
@ -184,14 +147,12 @@ public class BypassConfigService {
.webclientBean(request.getWebclientBean())
.externalPath(request.getExternalPath())
.httpMethod(request.getHttpMethod() != null ? request.getHttpMethod() : "GET")
.responseType(request.getResponseType() != null ? request.getResponseType() : "LIST")
.description(request.getDescription())
.build();
}
/**
* externalPath의 마지막 세그먼트를 endpointName으로 추출
* : "/RiskAndCompliance/CompliancesByImos" "CompliancesByImos"
*/
private String extractEndpointName(String externalPath) {
if (externalPath == null || externalPath.isEmpty()) {
@ -212,16 +173,6 @@ public class BypassConfigService {
.build();
}
private BypassApiField toFieldEntity(BypassFieldDto dto) {
return BypassApiField.builder()
.fieldName(dto.getFieldName())
.jsonProperty(dto.getJsonProperty())
.fieldType(dto.getFieldType() != null ? dto.getFieldType() : "String")
.description(dto.getDescription())
.sortOrder(dto.getSortOrder() != null ? dto.getSortOrder() : 0)
.build();
}
private BypassParamDto toParamDto(BypassApiParam param) {
return BypassParamDto.builder()
.id(param.getId())
@ -233,15 +184,4 @@ public class BypassConfigService {
.sortOrder(param.getSortOrder())
.build();
}
private BypassFieldDto toFieldDto(BypassApiField field) {
return BypassFieldDto.builder()
.id(field.getId())
.fieldName(field.getFieldName())
.jsonProperty(field.getJsonProperty())
.fieldType(field.getFieldType())
.description(field.getDescription())
.sortOrder(field.getSortOrder())
.build();
}
}

파일 보기

@ -1,165 +0,0 @@
package com.snp.batch.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.snp.batch.global.dto.BypassFieldDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* JSON 샘플 문자열에서 BypassFieldDto 목록을 추출하는 유틸리티 서비스.
* 1단계 깊이만 파싱하며, 중첩 객체는 Object 타입으로 처리합니다.
*/
@Slf4j
@Service
public class JsonSchemaParser {
private final ObjectMapper objectMapper;
public JsonSchemaParser(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
/**
* JSON 샘플 문자열에서 필드 목록을 추출합니다.
* 배열이면 번째 요소를 파싱합니다.
* 1단계 깊이만 파싱하며, 중첩 객체는 Object 타입으로 처리합니다.
*
* @param jsonSample JSON 샘플 문자열
* @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);
// 배열이면 번째 요소 추출
JsonNode target = root.isArray() ? root.get(0) : root;
if (target == null || !target.isObject()) {
log.warn("JSON 파싱: 유효한 객체를 찾을 수 없습니다.");
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(), fieldName);
// jsonProperty: fieldName과 원본 키가 다를 때만 설정
String jsonProperty = fieldName.equals(originalKey) ? null : originalKey;
result.add(BypassFieldDto.builder()
.fieldName(fieldName)
.jsonProperty(jsonProperty)
.fieldType(fieldType)
.sortOrder(sortOrder++)
.build());
}
} catch (Exception e) {
log.error("JSON 샘플 파싱 실패: {}", e.getMessage(), e);
return new ArrayList<>();
}
return result;
}
/**
* PascalCase camelCase 변환 헬퍼
*/
private String toCamelCase(String name) {
if (name == null || name.isEmpty()) {
return name;
}
if (Character.isUpperCase(name.charAt(0))) {
return Character.toLowerCase(name.charAt(0)) + name.substring(1);
}
return name;
}
/**
* JsonNode에서 Java 타입 추론.
* 배열 번째 요소가 ObjectNode인 경우 "{FieldName}Item" 타입으로 처리합니다.
*
* @param node 파싱할 노드
* @param fieldName 필드명 (배열 객체 타입명 생성에 사용)
*/
private String inferType(JsonNode node, String fieldName) {
if (node.isTextual()) {
return "String";
}
if (node.isInt()) {
return "Integer";
}
if (node.isLong() || (node.isNumber() && node.longValue() > Integer.MAX_VALUE)) {
return "Long";
}
if (node.isDouble() || node.isFloat()) {
return "Double";
}
if (node.isBoolean()) {
return "Boolean";
}
if (node.isNull()) {
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()) {
return "Object";
}
return "String";
}
private String capitalize(String s) {
if (s == null || s.isEmpty()) {
return s;
}
return Character.toUpperCase(s.charAt(0)) + s.substring(1);
}
}