feat: BY PASS API 등록 프로세스 설계 및 개발 (#63) #108
@ -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">⚠</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);
|
||||
}
|
||||
}
|
||||
불러오는 중...
Reference in New Issue
Block a user