배치 UPSERT 기능 및 고정값 매핑 버그 수정
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -58,6 +58,10 @@ export default function BatchEditPage() {
|
||||
const [cronSchedule, setCronSchedule] = useState("0 12 * * *");
|
||||
const [description, setDescription] = useState("");
|
||||
const [isActive, setIsActive] = useState("Y");
|
||||
const [saveMode, setSaveMode] = useState<"INSERT" | "UPSERT">("INSERT");
|
||||
const [conflictKey, setConflictKey] = useState("");
|
||||
const [authServiceName, setAuthServiceName] = useState("");
|
||||
const [authServiceNames, setAuthServiceNames] = useState<string[]>([]);
|
||||
|
||||
// 연결 정보
|
||||
const [connections, setConnections] = useState<ConnectionInfo[]>([]);
|
||||
@@ -87,9 +91,20 @@ export default function BatchEditPage() {
|
||||
if (batchId) {
|
||||
loadBatchConfig();
|
||||
loadConnections();
|
||||
loadAuthServiceNames();
|
||||
}
|
||||
}, [batchId]);
|
||||
|
||||
// 인증 서비스명 목록 로드
|
||||
const loadAuthServiceNames = async () => {
|
||||
try {
|
||||
const names = await BatchAPI.getAuthServiceNames();
|
||||
setAuthServiceNames(names);
|
||||
} catch (error) {
|
||||
console.error("인증 서비스 목록 로드 실패:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 연결 정보가 로드된 후 배치 설정의 연결 정보 설정
|
||||
useEffect(() => {
|
||||
if (batchConfig && connections.length > 0 && batchConfig.batch_mappings && batchConfig.batch_mappings.length > 0) {
|
||||
@@ -184,6 +199,9 @@ export default function BatchEditPage() {
|
||||
setCronSchedule(config.cron_schedule);
|
||||
setDescription(config.description || "");
|
||||
setIsActive(config.is_active || "Y");
|
||||
setSaveMode((config as any).save_mode || "INSERT");
|
||||
setConflictKey((config as any).conflict_key || "");
|
||||
setAuthServiceName((config as any).auth_service_name || "");
|
||||
|
||||
if (config.batch_mappings && config.batch_mappings.length > 0) {
|
||||
console.log("📊 매핑 정보:", config.batch_mappings);
|
||||
@@ -460,7 +478,10 @@ export default function BatchEditPage() {
|
||||
description,
|
||||
cronSchedule,
|
||||
isActive,
|
||||
mappings
|
||||
mappings,
|
||||
saveMode,
|
||||
conflictKey: saveMode === "UPSERT" ? conflictKey : undefined,
|
||||
authServiceName: authServiceName || undefined
|
||||
});
|
||||
|
||||
toast.success("배치 설정이 성공적으로 수정되었습니다.");
|
||||
@@ -558,6 +579,68 @@ export default function BatchEditPage() {
|
||||
/>
|
||||
<Label htmlFor="isActive">활성화</Label>
|
||||
</div>
|
||||
|
||||
{/* 저장 모드 설정 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-4 border-t">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="saveMode">저장 모드</Label>
|
||||
<Select
|
||||
value={saveMode}
|
||||
onValueChange={(value: "INSERT" | "UPSERT") => setSaveMode(value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="저장 모드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="INSERT">INSERT (항상 새로 추가)</SelectItem>
|
||||
<SelectItem value="UPSERT">UPSERT (있으면 업데이트, 없으면 추가)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
UPSERT: 동일한 키가 있으면 업데이트, 없으면 새로 추가합니다.
|
||||
</p>
|
||||
</div>
|
||||
{saveMode === "UPSERT" && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="conflictKey">충돌 기준 컬럼 *</Label>
|
||||
<Input
|
||||
id="conflictKey"
|
||||
value={conflictKey}
|
||||
onChange={(e) => setConflictKey(e.target.value)}
|
||||
placeholder="예: device_serial_number"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
UPSERT 시 중복 여부를 판단할 컬럼명을 입력하세요.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 인증 토큰 서비스 설정 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-4 border-t">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="authServiceName">인증 토큰 서비스</Label>
|
||||
<Select
|
||||
value={authServiceName || "none"}
|
||||
onValueChange={(value) => setAuthServiceName(value === "none" ? "" : value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="인증 토큰 서비스 선택 (선택사항)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">사용 안 함</SelectItem>
|
||||
{authServiceNames.map((name) => (
|
||||
<SelectItem key={name} value={name}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
REST API 호출 시 auth_tokens 테이블에서 토큰을 가져와 Authorization 헤더에 설정합니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@ export interface BatchConfig {
|
||||
cron_schedule: string;
|
||||
is_active?: string;
|
||||
company_code?: string;
|
||||
save_mode?: 'INSERT' | 'UPSERT'; // 저장 모드 (기본: INSERT)
|
||||
conflict_key?: string; // UPSERT 시 충돌 기준 컬럼명
|
||||
auth_service_name?: string; // REST API 인증에 사용할 토큰 서비스명
|
||||
created_date?: Date;
|
||||
created_by?: string;
|
||||
updated_date?: Date;
|
||||
@@ -386,6 +389,26 @@ export class BatchAPI {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* auth_tokens 테이블의 서비스명 목록 조회
|
||||
*/
|
||||
static async getAuthServiceNames(): Promise<string[]> {
|
||||
try {
|
||||
const response = await apiClient.get<{
|
||||
success: boolean;
|
||||
data: string[];
|
||||
}>(`/batch-management/auth-services`);
|
||||
|
||||
if (response.data.success) {
|
||||
return response.data.data || [];
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error("인증 서비스 목록 조회 오류:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BatchJob export 추가 (이미 위에서 interface로 정의됨)
|
||||
|
||||
@@ -5,7 +5,7 @@ import { apiClient } from "./client";
|
||||
|
||||
// 배치관리 전용 타입 정의
|
||||
export interface BatchConnectionInfo {
|
||||
type: 'internal' | 'external';
|
||||
type: "internal" | "external";
|
||||
id?: number;
|
||||
name: string;
|
||||
db_type?: string;
|
||||
@@ -39,9 +39,7 @@ class BatchManagementAPIClass {
|
||||
*/
|
||||
static async getAvailableConnections(): Promise<BatchConnectionInfo[]> {
|
||||
try {
|
||||
const response = await apiClient.get<BatchApiResponse<BatchConnectionInfo[]>>(
|
||||
`${this.BASE_PATH}/connections`
|
||||
);
|
||||
const response = await apiClient.get<BatchApiResponse<BatchConnectionInfo[]>>(`${this.BASE_PATH}/connections`);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || "커넥션 목록 조회에 실패했습니다.");
|
||||
@@ -58,15 +56,15 @@ class BatchManagementAPIClass {
|
||||
* 특정 커넥션의 테이블 목록 조회
|
||||
*/
|
||||
static async getTablesFromConnection(
|
||||
connectionType: 'internal' | 'external',
|
||||
connectionId?: number
|
||||
connectionType: "internal" | "external",
|
||||
connectionId?: number,
|
||||
): Promise<string[]> {
|
||||
try {
|
||||
let url = `${this.BASE_PATH}/connections/${connectionType}`;
|
||||
if (connectionType === 'external' && connectionId) {
|
||||
if (connectionType === "external" && connectionId) {
|
||||
url += `/${connectionId}`;
|
||||
}
|
||||
url += '/tables';
|
||||
url += "/tables";
|
||||
|
||||
const response = await apiClient.get<BatchApiResponse<string[]>>(url);
|
||||
|
||||
@@ -85,13 +83,13 @@ class BatchManagementAPIClass {
|
||||
* 특정 테이블의 컬럼 정보 조회
|
||||
*/
|
||||
static async getTableColumns(
|
||||
connectionType: 'internal' | 'external',
|
||||
connectionType: "internal" | "external",
|
||||
tableName: string,
|
||||
connectionId?: number
|
||||
connectionId?: number,
|
||||
): Promise<BatchColumnInfo[]> {
|
||||
try {
|
||||
let url = `${this.BASE_PATH}/connections/${connectionType}`;
|
||||
if (connectionType === 'external' && connectionId) {
|
||||
if (connectionType === "external" && connectionId) {
|
||||
url += `/${connectionId}`;
|
||||
}
|
||||
url += `/tables/${encodeURIComponent(tableName)}/columns`;
|
||||
@@ -120,14 +118,16 @@ class BatchManagementAPIClass {
|
||||
apiUrl: string,
|
||||
apiKey: string,
|
||||
endpoint: string,
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
|
||||
method: "GET" | "POST" | "PUT" | "DELETE" = "GET",
|
||||
paramInfo?: {
|
||||
paramType: 'url' | 'query';
|
||||
paramType: "url" | "query";
|
||||
paramName: string;
|
||||
paramValue: string;
|
||||
paramSource: 'static' | 'dynamic';
|
||||
paramSource: "static" | "dynamic";
|
||||
},
|
||||
requestBody?: string
|
||||
requestBody?: string,
|
||||
authServiceName?: string, // DB에서 토큰 가져올 서비스명
|
||||
dataArrayPath?: string, // 데이터 배열 경로 (예: response, data.items)
|
||||
): Promise<{
|
||||
fields: string[];
|
||||
samples: any[];
|
||||
@@ -139,7 +139,7 @@ class BatchManagementAPIClass {
|
||||
apiKey,
|
||||
endpoint,
|
||||
method,
|
||||
requestBody
|
||||
requestBody,
|
||||
};
|
||||
|
||||
// 파라미터 정보가 있으면 추가
|
||||
@@ -150,11 +150,23 @@ class BatchManagementAPIClass {
|
||||
requestData.paramSource = paramInfo.paramSource;
|
||||
}
|
||||
|
||||
const response = await apiClient.post<BatchApiResponse<{
|
||||
fields: string[];
|
||||
samples: any[];
|
||||
totalCount: number;
|
||||
}>>(`${this.BASE_PATH}/rest-api/preview`, requestData);
|
||||
// DB에서 토큰 가져올 서비스명 추가
|
||||
if (authServiceName) {
|
||||
requestData.authServiceName = authServiceName;
|
||||
}
|
||||
|
||||
// 데이터 배열 경로 추가
|
||||
if (dataArrayPath) {
|
||||
requestData.dataArrayPath = dataArrayPath;
|
||||
}
|
||||
|
||||
const response = await apiClient.post<
|
||||
BatchApiResponse<{
|
||||
fields: string[];
|
||||
samples: any[];
|
||||
totalCount: number;
|
||||
}>
|
||||
>(`${this.BASE_PATH}/rest-api/preview`, requestData);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || "REST API 미리보기에 실패했습니다.");
|
||||
@@ -167,6 +179,24 @@ class BatchManagementAPIClass {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증 서비스명 목록 조회
|
||||
*/
|
||||
static async getAuthServiceNames(): Promise<string[]> {
|
||||
try {
|
||||
const response = await apiClient.get<BatchApiResponse<string[]>>(`${this.BASE_PATH}/auth-services`);
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.message || "인증 서비스 목록 조회에 실패했습니다.");
|
||||
}
|
||||
|
||||
return response.data.data || [];
|
||||
} catch (error) {
|
||||
console.error("인증 서비스 목록 조회 오류:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* REST API 배치 저장
|
||||
*/
|
||||
@@ -176,15 +206,17 @@ class BatchManagementAPIClass {
|
||||
cronSchedule: string;
|
||||
description?: string;
|
||||
apiMappings: any[];
|
||||
}): Promise<{ success: boolean; message: string; data?: any; }> {
|
||||
authServiceName?: string;
|
||||
dataArrayPath?: string; // REST API 응답에서 데이터 배열 경로
|
||||
saveMode?: "INSERT" | "UPSERT";
|
||||
conflictKey?: string;
|
||||
}): Promise<{ success: boolean; message: string; data?: any }> {
|
||||
try {
|
||||
const response = await apiClient.post<BatchApiResponse<any>>(
|
||||
`${this.BASE_PATH}/rest-api/save`, batchData
|
||||
);
|
||||
const response = await apiClient.post<BatchApiResponse<any>>(`${this.BASE_PATH}/rest-api/save`, batchData);
|
||||
return {
|
||||
success: response.data.success,
|
||||
message: response.data.message || "",
|
||||
data: response.data.data
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("REST API 배치 저장 오류:", error);
|
||||
@@ -193,4 +225,4 @@ class BatchManagementAPIClass {
|
||||
}
|
||||
}
|
||||
|
||||
export const BatchManagementAPI = BatchManagementAPIClass;
|
||||
export const BatchManagementAPI = BatchManagementAPIClass;
|
||||
|
||||
Reference in New Issue
Block a user