restapi도 가능하게 구현
This commit is contained in:
parent
0789eb2e20
commit
2c447fd325
|
|
@ -32,8 +32,17 @@ export class FlowController {
|
||||||
*/
|
*/
|
||||||
createFlowDefinition = async (req: Request, res: Response): Promise<void> => {
|
createFlowDefinition = async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { name, description, tableName, dbSourceType, dbConnectionId } =
|
const {
|
||||||
req.body;
|
name,
|
||||||
|
description,
|
||||||
|
tableName,
|
||||||
|
dbSourceType,
|
||||||
|
dbConnectionId,
|
||||||
|
// REST API 관련 필드
|
||||||
|
restApiConnectionId,
|
||||||
|
restApiEndpoint,
|
||||||
|
restApiJsonPath,
|
||||||
|
} = req.body;
|
||||||
const userId = (req as any).user?.userId || "system";
|
const userId = (req as any).user?.userId || "system";
|
||||||
const userCompanyCode = (req as any).user?.companyCode;
|
const userCompanyCode = (req as any).user?.companyCode;
|
||||||
|
|
||||||
|
|
@ -43,6 +52,9 @@ export class FlowController {
|
||||||
tableName,
|
tableName,
|
||||||
dbSourceType,
|
dbSourceType,
|
||||||
dbConnectionId,
|
dbConnectionId,
|
||||||
|
restApiConnectionId,
|
||||||
|
restApiEndpoint,
|
||||||
|
restApiJsonPath,
|
||||||
userCompanyCode,
|
userCompanyCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -54,8 +66,11 @@ export class FlowController {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 테이블 이름이 제공된 경우에만 존재 확인
|
// REST API인 경우 테이블 존재 확인 스킵
|
||||||
if (tableName) {
|
const isRestApi = dbSourceType === "restapi";
|
||||||
|
|
||||||
|
// 테이블 이름이 제공된 경우에만 존재 확인 (REST API 제외)
|
||||||
|
if (tableName && !isRestApi && !tableName.startsWith("_restapi_")) {
|
||||||
const tableExists =
|
const tableExists =
|
||||||
await this.flowDefinitionService.checkTableExists(tableName);
|
await this.flowDefinitionService.checkTableExists(tableName);
|
||||||
if (!tableExists) {
|
if (!tableExists) {
|
||||||
|
|
@ -68,7 +83,16 @@ export class FlowController {
|
||||||
}
|
}
|
||||||
|
|
||||||
const flowDef = await this.flowDefinitionService.create(
|
const flowDef = await this.flowDefinitionService.create(
|
||||||
{ name, description, tableName, dbSourceType, dbConnectionId },
|
{
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
tableName,
|
||||||
|
dbSourceType,
|
||||||
|
dbConnectionId,
|
||||||
|
restApiConnectionId,
|
||||||
|
restApiEndpoint,
|
||||||
|
restApiJsonPath,
|
||||||
|
},
|
||||||
userId,
|
userId,
|
||||||
userCompanyCode
|
userCompanyCode
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -148,11 +148,42 @@ export const updateScreenInfo = async (
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { companyCode } = req.user as any;
|
const { companyCode } = req.user as any;
|
||||||
const { screenName, tableName, description, isActive } = req.body;
|
const {
|
||||||
|
screenName,
|
||||||
|
tableName,
|
||||||
|
description,
|
||||||
|
isActive,
|
||||||
|
// REST API 관련 필드 추가
|
||||||
|
dataSourceType,
|
||||||
|
dbSourceType,
|
||||||
|
dbConnectionId,
|
||||||
|
restApiConnectionId,
|
||||||
|
restApiEndpoint,
|
||||||
|
restApiJsonPath,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
console.log("화면 정보 수정 요청:", {
|
||||||
|
screenId: id,
|
||||||
|
dataSourceType,
|
||||||
|
restApiConnectionId,
|
||||||
|
restApiEndpoint,
|
||||||
|
restApiJsonPath,
|
||||||
|
});
|
||||||
|
|
||||||
await screenManagementService.updateScreenInfo(
|
await screenManagementService.updateScreenInfo(
|
||||||
parseInt(id),
|
parseInt(id),
|
||||||
{ screenName, tableName, description, isActive },
|
{
|
||||||
|
screenName,
|
||||||
|
tableName,
|
||||||
|
description,
|
||||||
|
isActive,
|
||||||
|
dataSourceType,
|
||||||
|
dbSourceType,
|
||||||
|
dbConnectionId,
|
||||||
|
restApiConnectionId,
|
||||||
|
restApiEndpoint,
|
||||||
|
restApiJsonPath,
|
||||||
|
},
|
||||||
companyCode
|
companyCode
|
||||||
);
|
);
|
||||||
res.json({ success: true, message: "화면 정보가 수정되었습니다." });
|
res.json({ success: true, message: "화면 정보가 수정되었습니다." });
|
||||||
|
|
|
||||||
|
|
@ -27,13 +27,20 @@ export class FlowDefinitionService {
|
||||||
tableName: request.tableName,
|
tableName: request.tableName,
|
||||||
dbSourceType: request.dbSourceType,
|
dbSourceType: request.dbSourceType,
|
||||||
dbConnectionId: request.dbConnectionId,
|
dbConnectionId: request.dbConnectionId,
|
||||||
|
restApiConnectionId: request.restApiConnectionId,
|
||||||
|
restApiEndpoint: request.restApiEndpoint,
|
||||||
|
restApiJsonPath: request.restApiJsonPath,
|
||||||
companyCode,
|
companyCode,
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
INSERT INTO flow_definition (name, description, table_name, db_source_type, db_connection_id, company_code, created_by)
|
INSERT INTO flow_definition (
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
name, description, table_name, db_source_type, db_connection_id,
|
||||||
|
rest_api_connection_id, rest_api_endpoint, rest_api_json_path,
|
||||||
|
company_code, created_by
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||||
RETURNING *
|
RETURNING *
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -43,6 +50,9 @@ export class FlowDefinitionService {
|
||||||
request.tableName || null,
|
request.tableName || null,
|
||||||
request.dbSourceType || "internal",
|
request.dbSourceType || "internal",
|
||||||
request.dbConnectionId || null,
|
request.dbConnectionId || null,
|
||||||
|
request.restApiConnectionId || null,
|
||||||
|
request.restApiEndpoint || null,
|
||||||
|
request.restApiJsonPath || "data",
|
||||||
companyCode,
|
companyCode,
|
||||||
userId,
|
userId,
|
||||||
];
|
];
|
||||||
|
|
@ -206,6 +216,10 @@ export class FlowDefinitionService {
|
||||||
tableName: row.table_name,
|
tableName: row.table_name,
|
||||||
dbSourceType: row.db_source_type || "internal",
|
dbSourceType: row.db_source_type || "internal",
|
||||||
dbConnectionId: row.db_connection_id,
|
dbConnectionId: row.db_connection_id,
|
||||||
|
// REST API 관련 필드
|
||||||
|
restApiConnectionId: row.rest_api_connection_id,
|
||||||
|
restApiEndpoint: row.rest_api_endpoint,
|
||||||
|
restApiJsonPath: row.rest_api_json_path,
|
||||||
companyCode: row.company_code || "*",
|
companyCode: row.company_code || "*",
|
||||||
isActive: row.is_active,
|
isActive: row.is_active,
|
||||||
createdBy: row.created_by,
|
createdBy: row.created_by,
|
||||||
|
|
|
||||||
|
|
@ -326,7 +326,19 @@ export class ScreenManagementService {
|
||||||
*/
|
*/
|
||||||
async updateScreenInfo(
|
async updateScreenInfo(
|
||||||
screenId: number,
|
screenId: number,
|
||||||
updateData: { screenName: string; tableName?: string; description?: string; isActive: string },
|
updateData: {
|
||||||
|
screenName: string;
|
||||||
|
tableName?: string;
|
||||||
|
description?: string;
|
||||||
|
isActive: string;
|
||||||
|
// REST API 관련 필드 추가
|
||||||
|
dataSourceType?: string;
|
||||||
|
dbSourceType?: string;
|
||||||
|
dbConnectionId?: number;
|
||||||
|
restApiConnectionId?: number;
|
||||||
|
restApiEndpoint?: string;
|
||||||
|
restApiJsonPath?: string;
|
||||||
|
},
|
||||||
userCompanyCode: string
|
userCompanyCode: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// 권한 확인
|
// 권한 확인
|
||||||
|
|
@ -348,24 +360,43 @@ export class ScreenManagementService {
|
||||||
throw new Error("이 화면을 수정할 권한이 없습니다.");
|
throw new Error("이 화면을 수정할 권한이 없습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 화면 정보 업데이트 (tableName 포함)
|
// 화면 정보 업데이트 (REST API 필드 포함)
|
||||||
await query(
|
await query(
|
||||||
`UPDATE screen_definitions
|
`UPDATE screen_definitions
|
||||||
SET screen_name = $1,
|
SET screen_name = $1,
|
||||||
table_name = $2,
|
table_name = $2,
|
||||||
description = $3,
|
description = $3,
|
||||||
is_active = $4,
|
is_active = $4,
|
||||||
updated_date = $5
|
updated_date = $5,
|
||||||
WHERE screen_id = $6`,
|
data_source_type = $6,
|
||||||
|
db_source_type = $7,
|
||||||
|
db_connection_id = $8,
|
||||||
|
rest_api_connection_id = $9,
|
||||||
|
rest_api_endpoint = $10,
|
||||||
|
rest_api_json_path = $11
|
||||||
|
WHERE screen_id = $12`,
|
||||||
[
|
[
|
||||||
updateData.screenName,
|
updateData.screenName,
|
||||||
updateData.tableName || null,
|
updateData.tableName || null,
|
||||||
updateData.description || null,
|
updateData.description || null,
|
||||||
updateData.isActive,
|
updateData.isActive,
|
||||||
new Date(),
|
new Date(),
|
||||||
|
updateData.dataSourceType || "database",
|
||||||
|
updateData.dbSourceType || "internal",
|
||||||
|
updateData.dbConnectionId || null,
|
||||||
|
updateData.restApiConnectionId || null,
|
||||||
|
updateData.restApiEndpoint || null,
|
||||||
|
updateData.restApiJsonPath || null,
|
||||||
screenId,
|
screenId,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log(`화면 정보 업데이트 완료: screenId=${screenId}`, {
|
||||||
|
dataSourceType: updateData.dataSourceType,
|
||||||
|
restApiConnectionId: updateData.restApiConnectionId,
|
||||||
|
restApiEndpoint: updateData.restApiEndpoint,
|
||||||
|
restApiJsonPath: updateData.restApiJsonPath,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -2016,37 +2047,40 @@ export class ScreenManagementService {
|
||||||
// Advisory lock 획득 (다른 트랜잭션이 같은 회사 코드를 생성하는 동안 대기)
|
// Advisory lock 획득 (다른 트랜잭션이 같은 회사 코드를 생성하는 동안 대기)
|
||||||
await client.query('SELECT pg_advisory_xact_lock($1)', [lockId]);
|
await client.query('SELECT pg_advisory_xact_lock($1)', [lockId]);
|
||||||
|
|
||||||
// 해당 회사의 기존 화면 코드들 조회
|
// 해당 회사의 기존 화면 코드들 조회 (모든 화면 - 삭제된 코드도 재사용 방지)
|
||||||
|
// LIMIT 제거하고 숫자 추출하여 최대값 찾기
|
||||||
const existingScreens = await client.query<{ screen_code: string }>(
|
const existingScreens = await client.query<{ screen_code: string }>(
|
||||||
`SELECT screen_code FROM screen_definitions
|
`SELECT screen_code FROM screen_definitions
|
||||||
WHERE company_code = $1 AND screen_code LIKE $2
|
WHERE screen_code LIKE $1
|
||||||
ORDER BY screen_code DESC
|
ORDER BY screen_code DESC`,
|
||||||
LIMIT 10`,
|
[`${companyCode}_%`]
|
||||||
[companyCode, `${companyCode}%`]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 회사 코드 뒤의 숫자 부분 추출하여 최대값 찾기
|
// 회사 코드 뒤의 숫자 부분 추출하여 최대값 찾기
|
||||||
let maxNumber = 0;
|
let maxNumber = 0;
|
||||||
const pattern = new RegExp(
|
const pattern = new RegExp(
|
||||||
`^${companyCode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:_)?(\\d+)$`
|
`^${companyCode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}_(\\d+)$`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log(`🔍 화면 코드 생성 - 조회된 화면 수: ${existingScreens.rows.length}`);
|
||||||
|
console.log(`🔍 패턴: ${pattern}`);
|
||||||
|
|
||||||
for (const screen of existingScreens.rows) {
|
for (const screen of existingScreens.rows) {
|
||||||
const match = screen.screen_code.match(pattern);
|
const match = screen.screen_code.match(pattern);
|
||||||
if (match) {
|
if (match) {
|
||||||
const number = parseInt(match[1], 10);
|
const number = parseInt(match[1], 10);
|
||||||
|
console.log(`🔍 매칭: ${screen.screen_code} → 숫자: ${number}`);
|
||||||
if (number > maxNumber) {
|
if (number > maxNumber) {
|
||||||
maxNumber = number;
|
maxNumber = number;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 다음 순번으로 화면 코드 생성 (3자리 패딩)
|
// 다음 순번으로 화면 코드 생성
|
||||||
const nextNumber = maxNumber + 1;
|
const nextNumber = maxNumber + 1;
|
||||||
const paddedNumber = nextNumber.toString().padStart(3, "0");
|
// 숫자가 3자리 이상이면 패딩 없이, 아니면 3자리 패딩
|
||||||
|
const newCode = `${companyCode}_${nextNumber}`;
|
||||||
const newCode = `${companyCode}_${paddedNumber}`;
|
console.log(`🔢 화면 코드 생성: ${companyCode} → ${newCode} (maxNumber: ${maxNumber}, nextNumber: ${nextNumber})`);
|
||||||
console.log(`🔢 화면 코드 생성: ${companyCode} → ${newCode} (maxNumber: ${maxNumber})`);
|
|
||||||
|
|
||||||
return newCode;
|
return newCode;
|
||||||
// Advisory lock은 트랜잭션 종료 시 자동으로 해제됨
|
// Advisory lock은 트랜잭션 종료 시 자동으로 해제됨
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,12 @@ export interface FlowDefinition {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
tableName: string;
|
tableName: string;
|
||||||
dbSourceType?: "internal" | "external"; // 데이터베이스 소스 타입
|
dbSourceType?: "internal" | "external" | "restapi"; // 데이터 소스 타입
|
||||||
dbConnectionId?: number; // 외부 DB 연결 ID (external인 경우)
|
dbConnectionId?: number; // 외부 DB 연결 ID (external인 경우)
|
||||||
|
// REST API 관련 필드
|
||||||
|
restApiConnectionId?: number; // REST API 연결 ID (restapi인 경우)
|
||||||
|
restApiEndpoint?: string; // REST API 엔드포인트
|
||||||
|
restApiJsonPath?: string; // JSON 응답에서 데이터 경로 (기본: data)
|
||||||
companyCode: string; // 회사 코드 (* = 공통)
|
companyCode: string; // 회사 코드 (* = 공통)
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
createdBy?: string;
|
createdBy?: string;
|
||||||
|
|
@ -22,8 +26,12 @@ export interface CreateFlowDefinitionRequest {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
tableName: string;
|
tableName: string;
|
||||||
dbSourceType?: "internal" | "external"; // 데이터베이스 소스 타입
|
dbSourceType?: "internal" | "external" | "restapi"; // 데이터 소스 타입
|
||||||
dbConnectionId?: number; // 외부 DB 연결 ID
|
dbConnectionId?: number; // 외부 DB 연결 ID
|
||||||
|
// REST API 관련 필드
|
||||||
|
restApiConnectionId?: number; // REST API 연결 ID
|
||||||
|
restApiEndpoint?: string; // REST API 엔드포인트
|
||||||
|
restApiJsonPath?: string; // JSON 응답에서 데이터 경로
|
||||||
companyCode?: string; // 회사 코드 (미제공 시 사용자의 company_code 사용)
|
companyCode?: string; // 회사 코드 (미제공 시 사용자의 company_code 사용)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import { formatErrorMessage } from "@/lib/utils/errorUtils";
|
||||||
import { tableManagementApi } from "@/lib/api/tableManagement";
|
import { tableManagementApi } from "@/lib/api/tableManagement";
|
||||||
import { ScrollToTop } from "@/components/common/ScrollToTop";
|
import { ScrollToTop } from "@/components/common/ScrollToTop";
|
||||||
import { ExternalDbConnectionAPI } from "@/lib/api/externalDbConnection";
|
import { ExternalDbConnectionAPI } from "@/lib/api/externalDbConnection";
|
||||||
|
import { ExternalRestApiConnectionAPI, ExternalRestApiConnection } from "@/lib/api/externalRestApiConnection";
|
||||||
|
|
||||||
export default function FlowManagementPage() {
|
export default function FlowManagementPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
@ -52,13 +53,19 @@ export default function FlowManagementPage() {
|
||||||
);
|
);
|
||||||
const [loadingTables, setLoadingTables] = useState(false);
|
const [loadingTables, setLoadingTables] = useState(false);
|
||||||
const [openTableCombobox, setOpenTableCombobox] = useState(false);
|
const [openTableCombobox, setOpenTableCombobox] = useState(false);
|
||||||
const [selectedDbSource, setSelectedDbSource] = useState<"internal" | number>("internal"); // "internal" 또는 외부 DB connection ID
|
// 데이터 소스 타입: "internal" (내부 DB), "external_db_숫자" (외부 DB), "restapi_숫자" (REST API)
|
||||||
|
const [selectedDbSource, setSelectedDbSource] = useState<string>("internal");
|
||||||
const [externalConnections, setExternalConnections] = useState<
|
const [externalConnections, setExternalConnections] = useState<
|
||||||
Array<{ id: number; connection_name: string; db_type: string }>
|
Array<{ id: number; connection_name: string; db_type: string }>
|
||||||
>([]);
|
>([]);
|
||||||
const [externalTableList, setExternalTableList] = useState<string[]>([]);
|
const [externalTableList, setExternalTableList] = useState<string[]>([]);
|
||||||
const [loadingExternalTables, setLoadingExternalTables] = useState(false);
|
const [loadingExternalTables, setLoadingExternalTables] = useState(false);
|
||||||
|
|
||||||
|
// REST API 연결 관련 상태
|
||||||
|
const [restApiConnections, setRestApiConnections] = useState<ExternalRestApiConnection[]>([]);
|
||||||
|
const [restApiEndpoint, setRestApiEndpoint] = useState("");
|
||||||
|
const [restApiJsonPath, setRestApiJsonPath] = useState("data");
|
||||||
|
|
||||||
// 생성 폼 상태
|
// 생성 폼 상태
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
|
|
@ -135,75 +142,132 @@ export default function FlowManagementPage() {
|
||||||
loadConnections();
|
loadConnections();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// REST API 연결 목록 로드
|
||||||
|
useEffect(() => {
|
||||||
|
const loadRestApiConnections = async () => {
|
||||||
|
try {
|
||||||
|
const connections = await ExternalRestApiConnectionAPI.getConnections({ is_active: "Y" });
|
||||||
|
setRestApiConnections(connections);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load REST API connections:", error);
|
||||||
|
setRestApiConnections([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadRestApiConnections();
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 외부 DB 테이블 목록 로드
|
// 외부 DB 테이블 목록 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedDbSource === "internal" || !selectedDbSource) {
|
// REST API인 경우 테이블 목록 로드 불필요
|
||||||
|
if (selectedDbSource === "internal" || !selectedDbSource || selectedDbSource.startsWith("restapi_")) {
|
||||||
setExternalTableList([]);
|
setExternalTableList([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadExternalTables = async () => {
|
// 외부 DB인 경우
|
||||||
try {
|
if (selectedDbSource.startsWith("external_db_")) {
|
||||||
setLoadingExternalTables(true);
|
const connectionId = selectedDbSource.replace("external_db_", "");
|
||||||
const token = localStorage.getItem("authToken");
|
|
||||||
|
const loadExternalTables = async () => {
|
||||||
|
try {
|
||||||
|
setLoadingExternalTables(true);
|
||||||
|
const token = localStorage.getItem("authToken");
|
||||||
|
|
||||||
const response = await fetch(`/api/multi-connection/connections/${selectedDbSource}/tables`, {
|
const response = await fetch(`/api/multi-connection/connections/${connectionId}/tables`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response && response.ok) {
|
if (response && response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.success && data.data) {
|
if (data.success && data.data) {
|
||||||
const tables = Array.isArray(data.data) ? data.data : [];
|
const tables = Array.isArray(data.data) ? data.data : [];
|
||||||
const tableNames = tables
|
const tableNames = tables
|
||||||
.map((t: string | { tableName?: string; table_name?: string; tablename?: string; name?: string }) =>
|
.map((t: string | { tableName?: string; table_name?: string; tablename?: string; name?: string }) =>
|
||||||
typeof t === "string" ? t : t.tableName || t.table_name || t.tablename || t.name,
|
typeof t === "string" ? t : t.tableName || t.table_name || t.tablename || t.name,
|
||||||
)
|
)
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
setExternalTableList(tableNames);
|
setExternalTableList(tableNames);
|
||||||
|
} else {
|
||||||
|
setExternalTableList([]);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setExternalTableList([]);
|
setExternalTableList([]);
|
||||||
}
|
}
|
||||||
} else {
|
} catch (error) {
|
||||||
|
console.error("외부 DB 테이블 목록 조회 오류:", error);
|
||||||
setExternalTableList([]);
|
setExternalTableList([]);
|
||||||
|
} finally {
|
||||||
|
setLoadingExternalTables(false);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
};
|
||||||
console.error("외부 DB 테이블 목록 조회 오류:", error);
|
|
||||||
setExternalTableList([]);
|
|
||||||
} finally {
|
|
||||||
setLoadingExternalTables(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadExternalTables();
|
loadExternalTables();
|
||||||
|
}
|
||||||
}, [selectedDbSource]);
|
}, [selectedDbSource]);
|
||||||
|
|
||||||
// 플로우 생성
|
// 플로우 생성
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
console.log("🚀 handleCreate called with formData:", formData);
|
console.log("🚀 handleCreate called with formData:", formData);
|
||||||
|
|
||||||
if (!formData.name || !formData.tableName) {
|
// REST API인 경우 테이블 이름 검증 스킵
|
||||||
console.log("❌ Validation failed:", { name: formData.name, tableName: formData.tableName });
|
const isRestApi = selectedDbSource.startsWith("restapi_");
|
||||||
|
|
||||||
|
if (!formData.name || (!isRestApi && !formData.tableName)) {
|
||||||
|
console.log("❌ Validation failed:", { name: formData.name, tableName: formData.tableName, isRestApi });
|
||||||
toast({
|
toast({
|
||||||
title: "입력 오류",
|
title: "입력 오류",
|
||||||
description: "플로우 이름과 테이블 이름은 필수입니다.",
|
description: isRestApi ? "플로우 이름은 필수입니다." : "플로우 이름과 테이블 이름은 필수입니다.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// REST API인 경우 엔드포인트 검증
|
||||||
|
if (isRestApi && !restApiEndpoint) {
|
||||||
|
toast({
|
||||||
|
title: "입력 오류",
|
||||||
|
description: "REST API 엔드포인트는 필수입니다.",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// DB 소스 정보 추가
|
// 데이터 소스 타입 및 ID 파싱
|
||||||
const requestData = {
|
let dbSourceType: "internal" | "external" | "restapi" = "internal";
|
||||||
|
let dbConnectionId: number | undefined = undefined;
|
||||||
|
let restApiConnectionId: number | undefined = undefined;
|
||||||
|
|
||||||
|
if (selectedDbSource === "internal") {
|
||||||
|
dbSourceType = "internal";
|
||||||
|
} else if (selectedDbSource.startsWith("external_db_")) {
|
||||||
|
dbSourceType = "external";
|
||||||
|
dbConnectionId = parseInt(selectedDbSource.replace("external_db_", ""));
|
||||||
|
} else if (selectedDbSource.startsWith("restapi_")) {
|
||||||
|
dbSourceType = "restapi";
|
||||||
|
restApiConnectionId = parseInt(selectedDbSource.replace("restapi_", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 요청 데이터 구성
|
||||||
|
const requestData: Record<string, unknown> = {
|
||||||
...formData,
|
...formData,
|
||||||
dbSourceType: selectedDbSource === "internal" ? "internal" : "external",
|
dbSourceType,
|
||||||
dbConnectionId: selectedDbSource === "internal" ? undefined : Number(selectedDbSource),
|
dbConnectionId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// REST API인 경우 추가 정보
|
||||||
|
if (dbSourceType === "restapi") {
|
||||||
|
requestData.restApiConnectionId = restApiConnectionId;
|
||||||
|
requestData.restApiEndpoint = restApiEndpoint;
|
||||||
|
requestData.restApiJsonPath = restApiJsonPath || "data";
|
||||||
|
// REST API는 가상 테이블명 사용
|
||||||
|
requestData.tableName = `_restapi_${restApiConnectionId}`;
|
||||||
|
}
|
||||||
|
|
||||||
console.log("✅ Calling createFlowDefinition with:", requestData);
|
console.log("✅ Calling createFlowDefinition with:", requestData);
|
||||||
const response = await createFlowDefinition(requestData);
|
const response = await createFlowDefinition(requestData as Parameters<typeof createFlowDefinition>[0]);
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
toast({
|
toast({
|
||||||
title: "생성 완료",
|
title: "생성 완료",
|
||||||
|
|
@ -212,6 +276,8 @@ export default function FlowManagementPage() {
|
||||||
setIsCreateDialogOpen(false);
|
setIsCreateDialogOpen(false);
|
||||||
setFormData({ name: "", description: "", tableName: "" });
|
setFormData({ name: "", description: "", tableName: "" });
|
||||||
setSelectedDbSource("internal");
|
setSelectedDbSource("internal");
|
||||||
|
setRestApiEndpoint("");
|
||||||
|
setRestApiJsonPath("data");
|
||||||
loadFlows();
|
loadFlows();
|
||||||
} else {
|
} else {
|
||||||
toast({
|
toast({
|
||||||
|
|
@ -415,125 +481,186 @@ export default function FlowManagementPage() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* DB 소스 선택 */}
|
{/* 데이터 소스 선택 */}
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs sm:text-sm">데이터베이스 소스</Label>
|
<Label className="text-xs sm:text-sm">데이터 소스</Label>
|
||||||
<Select
|
<Select
|
||||||
value={selectedDbSource.toString()}
|
value={selectedDbSource}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
const dbSource = value === "internal" ? "internal" : parseInt(value);
|
setSelectedDbSource(value);
|
||||||
setSelectedDbSource(dbSource);
|
// 소스 변경 시 테이블 선택 및 REST API 설정 초기화
|
||||||
// DB 소스 변경 시 테이블 선택 초기화
|
|
||||||
setFormData({ ...formData, tableName: "" });
|
setFormData({ ...formData, tableName: "" });
|
||||||
|
setRestApiEndpoint("");
|
||||||
|
setRestApiJsonPath("data");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
||||||
<SelectValue placeholder="데이터베이스 선택" />
|
<SelectValue placeholder="데이터 소스 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
{/* 내부 DB */}
|
||||||
<SelectItem value="internal">내부 데이터베이스</SelectItem>
|
<SelectItem value="internal">내부 데이터베이스</SelectItem>
|
||||||
{externalConnections.map((conn) => (
|
|
||||||
<SelectItem key={conn.id} value={conn.id.toString()}>
|
{/* 외부 DB 연결 */}
|
||||||
{conn.connection_name} ({conn.db_type?.toUpperCase()})
|
{externalConnections.length > 0 && (
|
||||||
</SelectItem>
|
<>
|
||||||
))}
|
<SelectItem value="__divider_db__" disabled className="text-xs text-muted-foreground">
|
||||||
|
-- 외부 데이터베이스 --
|
||||||
|
</SelectItem>
|
||||||
|
{externalConnections.map((conn) => (
|
||||||
|
<SelectItem key={`db_${conn.id}`} value={`external_db_${conn.id}`}>
|
||||||
|
{conn.connection_name} ({conn.db_type?.toUpperCase()})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* REST API 연결 */}
|
||||||
|
{restApiConnections.length > 0 && (
|
||||||
|
<>
|
||||||
|
<SelectItem value="__divider_api__" disabled className="text-xs text-muted-foreground">
|
||||||
|
-- REST API --
|
||||||
|
</SelectItem>
|
||||||
|
{restApiConnections.map((conn) => (
|
||||||
|
<SelectItem key={`api_${conn.id}`} value={`restapi_${conn.id}`}>
|
||||||
|
{conn.connection_name} (REST API)
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
||||||
플로우에서 사용할 데이터베이스를 선택합니다
|
플로우에서 사용할 데이터 소스를 선택합니다
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 테이블 선택 */}
|
{/* REST API인 경우 엔드포인트 설정 */}
|
||||||
<div>
|
{selectedDbSource.startsWith("restapi_") ? (
|
||||||
<Label htmlFor="tableName" className="text-xs sm:text-sm">
|
<>
|
||||||
연결 테이블 *
|
<div>
|
||||||
</Label>
|
<Label htmlFor="restApiEndpoint" className="text-xs sm:text-sm">
|
||||||
<Popover open={openTableCombobox} onOpenChange={setOpenTableCombobox}>
|
API 엔드포인트 *
|
||||||
<PopoverTrigger asChild>
|
</Label>
|
||||||
<Button
|
<Input
|
||||||
variant="outline"
|
id="restApiEndpoint"
|
||||||
role="combobox"
|
value={restApiEndpoint}
|
||||||
aria-expanded={openTableCombobox}
|
onChange={(e) => setRestApiEndpoint(e.target.value)}
|
||||||
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
|
placeholder="예: /api/data/list"
|
||||||
disabled={loadingTables || (selectedDbSource !== "internal" && loadingExternalTables)}
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||||
>
|
/>
|
||||||
{formData.tableName
|
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
||||||
? selectedDbSource === "internal"
|
데이터를 조회할 API 엔드포인트 경로입니다
|
||||||
? tableList.find((table) => table.tableName === formData.tableName)?.displayName ||
|
</p>
|
||||||
formData.tableName
|
</div>
|
||||||
: formData.tableName
|
<div>
|
||||||
: loadingTables || loadingExternalTables
|
<Label htmlFor="restApiJsonPath" className="text-xs sm:text-sm">
|
||||||
? "로딩 중..."
|
JSON 경로
|
||||||
: "테이블 선택"}
|
</Label>
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<Input
|
||||||
</Button>
|
id="restApiJsonPath"
|
||||||
</PopoverTrigger>
|
value={restApiJsonPath}
|
||||||
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
|
onChange={(e) => setRestApiJsonPath(e.target.value)}
|
||||||
<Command>
|
placeholder="예: data 또는 result.items"
|
||||||
<CommandInput placeholder="테이블 검색..." className="text-xs sm:text-sm" />
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||||
<CommandList>
|
/>
|
||||||
<CommandEmpty className="text-xs sm:text-sm">테이블을 찾을 수 없습니다.</CommandEmpty>
|
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
||||||
<CommandGroup>
|
응답 JSON에서 데이터 배열의 경로입니다 (기본: data)
|
||||||
{selectedDbSource === "internal"
|
</p>
|
||||||
? // 내부 DB 테이블 목록
|
</div>
|
||||||
tableList.map((table) => (
|
</>
|
||||||
<CommandItem
|
) : (
|
||||||
key={table.tableName}
|
/* 테이블 선택 (내부 DB 또는 외부 DB) */
|
||||||
value={table.tableName}
|
<div>
|
||||||
onSelect={(currentValue) => {
|
<Label htmlFor="tableName" className="text-xs sm:text-sm">
|
||||||
console.log("📝 Internal table selected:", {
|
연결 테이블 *
|
||||||
tableName: table.tableName,
|
</Label>
|
||||||
currentValue,
|
<Popover open={openTableCombobox} onOpenChange={setOpenTableCombobox}>
|
||||||
});
|
<PopoverTrigger asChild>
|
||||||
setFormData({ ...formData, tableName: currentValue });
|
<Button
|
||||||
setOpenTableCombobox(false);
|
variant="outline"
|
||||||
}}
|
role="combobox"
|
||||||
className="text-xs sm:text-sm"
|
aria-expanded={openTableCombobox}
|
||||||
>
|
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
|
||||||
<Check
|
disabled={loadingTables || (selectedDbSource !== "internal" && loadingExternalTables)}
|
||||||
className={cn(
|
>
|
||||||
"mr-2 h-4 w-4",
|
{formData.tableName
|
||||||
formData.tableName === table.tableName ? "opacity-100" : "opacity-0",
|
? selectedDbSource === "internal"
|
||||||
)}
|
? tableList.find((table) => table.tableName === formData.tableName)?.displayName ||
|
||||||
/>
|
formData.tableName
|
||||||
<div className="flex flex-col">
|
: formData.tableName
|
||||||
<span className="font-medium">{table.displayName || table.tableName}</span>
|
: loadingTables || loadingExternalTables
|
||||||
{table.description && (
|
? "로딩 중..."
|
||||||
<span className="text-[10px] text-gray-500">{table.description}</span>
|
: "테이블 선택"}
|
||||||
)}
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</div>
|
</Button>
|
||||||
</CommandItem>
|
</PopoverTrigger>
|
||||||
))
|
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
|
||||||
: // 외부 DB 테이블 목록
|
<Command>
|
||||||
externalTableList.map((tableName, index) => (
|
<CommandInput placeholder="테이블 검색..." className="text-xs sm:text-sm" />
|
||||||
<CommandItem
|
<CommandList>
|
||||||
key={`external-${selectedDbSource}-${tableName}-${index}`}
|
<CommandEmpty className="text-xs sm:text-sm">테이블을 찾을 수 없습니다.</CommandEmpty>
|
||||||
value={tableName}
|
<CommandGroup>
|
||||||
onSelect={(currentValue) => {
|
{selectedDbSource === "internal"
|
||||||
setFormData({ ...formData, tableName: currentValue });
|
? // 내부 DB 테이블 목록
|
||||||
setOpenTableCombobox(false);
|
tableList.map((table) => (
|
||||||
}}
|
<CommandItem
|
||||||
className="text-xs sm:text-sm"
|
key={table.tableName}
|
||||||
>
|
value={table.tableName}
|
||||||
<Check
|
onSelect={(currentValue) => {
|
||||||
className={cn(
|
console.log("📝 Internal table selected:", {
|
||||||
"mr-2 h-4 w-4",
|
tableName: table.tableName,
|
||||||
formData.tableName === tableName ? "opacity-100" : "opacity-0",
|
currentValue,
|
||||||
)}
|
});
|
||||||
/>
|
setFormData({ ...formData, tableName: currentValue });
|
||||||
<div>{tableName}</div>
|
setOpenTableCombobox(false);
|
||||||
</CommandItem>
|
}}
|
||||||
))}
|
className="text-xs sm:text-sm"
|
||||||
</CommandGroup>
|
>
|
||||||
</CommandList>
|
<Check
|
||||||
</Command>
|
className={cn(
|
||||||
</PopoverContent>
|
"mr-2 h-4 w-4",
|
||||||
</Popover>
|
formData.tableName === table.tableName ? "opacity-100" : "opacity-0",
|
||||||
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
)}
|
||||||
플로우의 모든 단계에서 사용할 기본 테이블입니다 (단계마다 상태 컬럼만 지정합니다)
|
/>
|
||||||
</p>
|
<div className="flex flex-col">
|
||||||
</div>
|
<span className="font-medium">{table.displayName || table.tableName}</span>
|
||||||
|
{table.description && (
|
||||||
|
<span className="text-[10px] text-gray-500">{table.description}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))
|
||||||
|
: // 외부 DB 테이블 목록
|
||||||
|
externalTableList.map((tableName, index) => (
|
||||||
|
<CommandItem
|
||||||
|
key={`external-${selectedDbSource}-${tableName}-${index}`}
|
||||||
|
value={tableName}
|
||||||
|
onSelect={(currentValue) => {
|
||||||
|
setFormData({ ...formData, tableName: currentValue });
|
||||||
|
setOpenTableCombobox(false);
|
||||||
|
}}
|
||||||
|
className="text-xs sm:text-sm"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
formData.tableName === tableName ? "opacity-100" : "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div>{tableName}</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
|
||||||
|
플로우의 모든 단계에서 사용할 기본 테이블입니다 (단계마다 상태 컬럼만 지정합니다)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="description" className="text-xs sm:text-sm">
|
<Label htmlFor="description" className="text-xs sm:text-sm">
|
||||||
|
|
|
||||||
|
|
@ -838,18 +838,46 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
// 화면의 기본 테이블/REST API 정보 로드
|
// 화면의 기본 테이블/REST API 정보 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadScreenDataSource = async () => {
|
const loadScreenDataSource = async () => {
|
||||||
|
console.log("🔍 [ScreenDesigner] 데이터 소스 로드 시작:", {
|
||||||
|
screenId: selectedScreen?.screenId,
|
||||||
|
screenName: selectedScreen?.screenName,
|
||||||
|
dataSourceType: selectedScreen?.dataSourceType,
|
||||||
|
tableName: selectedScreen?.tableName,
|
||||||
|
restApiConnectionId: selectedScreen?.restApiConnectionId,
|
||||||
|
restApiEndpoint: selectedScreen?.restApiEndpoint,
|
||||||
|
restApiJsonPath: selectedScreen?.restApiJsonPath,
|
||||||
|
});
|
||||||
|
|
||||||
// REST API 데이터 소스인 경우
|
// REST API 데이터 소스인 경우
|
||||||
if (selectedScreen?.dataSourceType === "restapi" && selectedScreen?.restApiConnectionId) {
|
// tableName이 restapi_로 시작하면 REST API로 간주
|
||||||
|
const isRestApi = selectedScreen?.dataSourceType === "restapi" ||
|
||||||
|
selectedScreen?.tableName?.startsWith("restapi_") ||
|
||||||
|
selectedScreen?.tableName?.startsWith("_restapi_");
|
||||||
|
|
||||||
|
if (isRestApi && (selectedScreen?.restApiConnectionId || selectedScreen?.tableName)) {
|
||||||
try {
|
try {
|
||||||
|
// 연결 ID 추출 (restApiConnectionId가 없으면 tableName에서 추출)
|
||||||
|
let connectionId = selectedScreen?.restApiConnectionId;
|
||||||
|
if (!connectionId && selectedScreen?.tableName) {
|
||||||
|
const match = selectedScreen.tableName.match(/restapi_(\d+)/);
|
||||||
|
connectionId = match ? parseInt(match[1]) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!connectionId) {
|
||||||
|
throw new Error("REST API 연결 ID를 찾을 수 없습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🌐 [ScreenDesigner] REST API 데이터 로드:", { connectionId });
|
||||||
|
|
||||||
const restApiData = await ExternalRestApiConnectionAPI.fetchData(
|
const restApiData = await ExternalRestApiConnectionAPI.fetchData(
|
||||||
selectedScreen.restApiConnectionId,
|
connectionId,
|
||||||
selectedScreen.restApiEndpoint,
|
selectedScreen?.restApiEndpoint,
|
||||||
selectedScreen.restApiJsonPath || "data",
|
selectedScreen?.restApiJsonPath || "response", // 기본값을 response로 변경
|
||||||
);
|
);
|
||||||
|
|
||||||
// REST API 응답에서 컬럼 정보 생성
|
// REST API 응답에서 컬럼 정보 생성
|
||||||
const columns: ColumnInfo[] = restApiData.columns.map((col) => ({
|
const columns: ColumnInfo[] = restApiData.columns.map((col) => ({
|
||||||
tableName: `restapi_${selectedScreen.restApiConnectionId}`,
|
tableName: `restapi_${connectionId}`,
|
||||||
columnName: col.columnName,
|
columnName: col.columnName,
|
||||||
columnLabel: col.columnLabel,
|
columnLabel: col.columnLabel,
|
||||||
dataType: col.dataType === "string" ? "varchar" : col.dataType === "number" ? "numeric" : col.dataType,
|
dataType: col.dataType === "string" ? "varchar" : col.dataType === "number" ? "numeric" : col.dataType,
|
||||||
|
|
@ -861,10 +889,17 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const tableInfo: TableInfo = {
|
const tableInfo: TableInfo = {
|
||||||
tableName: `restapi_${selectedScreen.restApiConnectionId}`,
|
tableName: `restapi_${connectionId}`,
|
||||||
tableLabel: restApiData.connectionInfo.connectionName || "REST API 데이터",
|
tableLabel: restApiData.connectionInfo.connectionName || "REST API 데이터",
|
||||||
columns,
|
columns,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log("✅ [ScreenDesigner] REST API 컬럼 로드 완료:", {
|
||||||
|
tableName: tableInfo.tableName,
|
||||||
|
tableLabel: tableInfo.tableLabel,
|
||||||
|
columnsCount: columns.length,
|
||||||
|
columns: columns.map(c => c.columnName),
|
||||||
|
});
|
||||||
|
|
||||||
setTables([tableInfo]);
|
setTables([tableInfo]);
|
||||||
console.log("REST API 데이터 소스 로드 완료:", {
|
console.log("REST API 데이터 소스 로드 완료:", {
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ import { cn } from "@/lib/utils";
|
||||||
import { MoreHorizontal, Edit, Trash2, Copy, Eye, Plus, Search, Palette, RotateCcw, Trash, Check, ChevronsUpDown } from "lucide-react";
|
import { MoreHorizontal, Edit, Trash2, Copy, Eye, Plus, Search, Palette, RotateCcw, Trash, Check, ChevronsUpDown } from "lucide-react";
|
||||||
import { ScreenDefinition } from "@/types/screen";
|
import { ScreenDefinition } from "@/types/screen";
|
||||||
import { screenApi } from "@/lib/api/screen";
|
import { screenApi } from "@/lib/api/screen";
|
||||||
|
import { ExternalRestApiConnectionAPI, ExternalRestApiConnection } from "@/lib/api/externalRestApiConnection";
|
||||||
import CreateScreenModal from "./CreateScreenModal";
|
import CreateScreenModal from "./CreateScreenModal";
|
||||||
import CopyScreenModal from "./CopyScreenModal";
|
import CopyScreenModal from "./CopyScreenModal";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
|
@ -132,10 +133,18 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||||
description: "",
|
description: "",
|
||||||
isActive: "Y",
|
isActive: "Y",
|
||||||
tableName: "",
|
tableName: "",
|
||||||
|
dataSourceType: "database" as "database" | "restapi",
|
||||||
|
restApiConnectionId: null as number | null,
|
||||||
|
restApiEndpoint: "",
|
||||||
|
restApiJsonPath: "data",
|
||||||
});
|
});
|
||||||
const [tables, setTables] = useState<Array<{ tableName: string; tableLabel: string }>>([]);
|
const [tables, setTables] = useState<Array<{ tableName: string; tableLabel: string }>>([]);
|
||||||
const [loadingTables, setLoadingTables] = useState(false);
|
const [loadingTables, setLoadingTables] = useState(false);
|
||||||
const [tableComboboxOpen, setTableComboboxOpen] = useState(false);
|
const [tableComboboxOpen, setTableComboboxOpen] = useState(false);
|
||||||
|
|
||||||
|
// REST API 연결 관련 상태 (편집용)
|
||||||
|
const [editRestApiConnections, setEditRestApiConnections] = useState<ExternalRestApiConnection[]>([]);
|
||||||
|
const [editRestApiComboboxOpen, setEditRestApiComboboxOpen] = useState(false);
|
||||||
|
|
||||||
// 미리보기 관련 상태
|
// 미리보기 관련 상태
|
||||||
const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
|
const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
|
||||||
|
|
@ -272,11 +281,19 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||||
|
|
||||||
const handleEdit = async (screen: ScreenDefinition) => {
|
const handleEdit = async (screen: ScreenDefinition) => {
|
||||||
setScreenToEdit(screen);
|
setScreenToEdit(screen);
|
||||||
|
|
||||||
|
// 데이터 소스 타입 결정
|
||||||
|
const isRestApi = screen.dataSourceType === "restapi" || screen.tableName?.startsWith("_restapi_");
|
||||||
|
|
||||||
setEditFormData({
|
setEditFormData({
|
||||||
screenName: screen.screenName,
|
screenName: screen.screenName,
|
||||||
description: screen.description || "",
|
description: screen.description || "",
|
||||||
isActive: screen.isActive,
|
isActive: screen.isActive,
|
||||||
tableName: screen.tableName || "",
|
tableName: screen.tableName || "",
|
||||||
|
dataSourceType: isRestApi ? "restapi" : "database",
|
||||||
|
restApiConnectionId: (screen as any).restApiConnectionId || null,
|
||||||
|
restApiEndpoint: (screen as any).restApiEndpoint || "",
|
||||||
|
restApiJsonPath: (screen as any).restApiJsonPath || "data",
|
||||||
});
|
});
|
||||||
setEditDialogOpen(true);
|
setEditDialogOpen(true);
|
||||||
|
|
||||||
|
|
@ -298,14 +315,50 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingTables(false);
|
setLoadingTables(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// REST API 연결 목록 로드
|
||||||
|
try {
|
||||||
|
const connections = await ExternalRestApiConnectionAPI.getConnections({ is_active: "Y" });
|
||||||
|
setEditRestApiConnections(connections);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("REST API 연결 목록 조회 실패:", error);
|
||||||
|
setEditRestApiConnections([]);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditSave = async () => {
|
const handleEditSave = async () => {
|
||||||
if (!screenToEdit) return;
|
if (!screenToEdit) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 데이터 소스 타입에 따라 업데이트 데이터 구성
|
||||||
|
const updateData: any = {
|
||||||
|
screenName: editFormData.screenName,
|
||||||
|
description: editFormData.description,
|
||||||
|
isActive: editFormData.isActive,
|
||||||
|
dataSourceType: editFormData.dataSourceType,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (editFormData.dataSourceType === "database") {
|
||||||
|
updateData.tableName = editFormData.tableName;
|
||||||
|
updateData.restApiConnectionId = null;
|
||||||
|
updateData.restApiEndpoint = null;
|
||||||
|
updateData.restApiJsonPath = null;
|
||||||
|
} else {
|
||||||
|
// REST API
|
||||||
|
updateData.tableName = `_restapi_${editFormData.restApiConnectionId}`;
|
||||||
|
updateData.restApiConnectionId = editFormData.restApiConnectionId;
|
||||||
|
updateData.restApiEndpoint = editFormData.restApiEndpoint;
|
||||||
|
updateData.restApiJsonPath = editFormData.restApiJsonPath || "data";
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("📤 화면 편집 저장 요청:", {
|
||||||
|
screenId: screenToEdit.screenId,
|
||||||
|
editFormData,
|
||||||
|
updateData,
|
||||||
|
});
|
||||||
|
|
||||||
// 화면 정보 업데이트 API 호출
|
// 화면 정보 업데이트 API 호출
|
||||||
await screenApi.updateScreenInfo(screenToEdit.screenId, editFormData);
|
await screenApi.updateScreenInfo(screenToEdit.screenId, updateData);
|
||||||
|
|
||||||
// 선택된 테이블의 라벨 찾기
|
// 선택된 테이블의 라벨 찾기
|
||||||
const selectedTable = tables.find((t) => t.tableName === editFormData.tableName);
|
const selectedTable = tables.find((t) => t.tableName === editFormData.tableName);
|
||||||
|
|
@ -318,10 +371,11 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||||
? {
|
? {
|
||||||
...s,
|
...s,
|
||||||
screenName: editFormData.screenName,
|
screenName: editFormData.screenName,
|
||||||
tableName: editFormData.tableName,
|
tableName: updateData.tableName,
|
||||||
tableLabel: tableLabel,
|
tableLabel: tableLabel,
|
||||||
description: editFormData.description,
|
description: editFormData.description,
|
||||||
isActive: editFormData.isActive,
|
isActive: editFormData.isActive,
|
||||||
|
dataSourceType: editFormData.dataSourceType,
|
||||||
}
|
}
|
||||||
: s,
|
: s,
|
||||||
),
|
),
|
||||||
|
|
@ -1216,65 +1270,184 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||||
placeholder="화면명을 입력하세요"
|
placeholder="화면명을 입력하세요"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 데이터 소스 타입 선택 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="edit-tableName">테이블 *</Label>
|
<Label>데이터 소스 타입</Label>
|
||||||
<Popover open={tableComboboxOpen} onOpenChange={setTableComboboxOpen}>
|
<Select
|
||||||
<PopoverTrigger asChild>
|
value={editFormData.dataSourceType}
|
||||||
<Button
|
onValueChange={(value: "database" | "restapi") => {
|
||||||
variant="outline"
|
setEditFormData({
|
||||||
role="combobox"
|
...editFormData,
|
||||||
aria-expanded={tableComboboxOpen}
|
dataSourceType: value,
|
||||||
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
|
tableName: "",
|
||||||
disabled={loadingTables}
|
restApiConnectionId: null,
|
||||||
>
|
restApiEndpoint: "",
|
||||||
{loadingTables
|
restApiJsonPath: "data",
|
||||||
? "로딩 중..."
|
});
|
||||||
: editFormData.tableName
|
}}
|
||||||
? tables.find((table) => table.tableName === editFormData.tableName)?.tableLabel || editFormData.tableName
|
>
|
||||||
: "테이블을 선택하세요"}
|
<SelectTrigger>
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<SelectValue />
|
||||||
</Button>
|
</SelectTrigger>
|
||||||
</PopoverTrigger>
|
<SelectContent>
|
||||||
<PopoverContent
|
<SelectItem value="database">데이터베이스</SelectItem>
|
||||||
className="p-0"
|
<SelectItem value="restapi">REST API</SelectItem>
|
||||||
style={{ width: "var(--radix-popover-trigger-width)" }}
|
</SelectContent>
|
||||||
align="start"
|
</Select>
|
||||||
>
|
|
||||||
<Command>
|
|
||||||
<CommandInput placeholder="테이블 검색..." className="text-xs sm:text-sm" />
|
|
||||||
<CommandList>
|
|
||||||
<CommandEmpty className="text-xs sm:text-sm">
|
|
||||||
테이블을 찾을 수 없습니다.
|
|
||||||
</CommandEmpty>
|
|
||||||
<CommandGroup>
|
|
||||||
{tables.map((table) => (
|
|
||||||
<CommandItem
|
|
||||||
key={table.tableName}
|
|
||||||
value={`${table.tableName} ${table.tableLabel}`}
|
|
||||||
onSelect={() => {
|
|
||||||
setEditFormData({ ...editFormData, tableName: table.tableName });
|
|
||||||
setTableComboboxOpen(false);
|
|
||||||
}}
|
|
||||||
className="text-xs sm:text-sm"
|
|
||||||
>
|
|
||||||
<Check
|
|
||||||
className={cn(
|
|
||||||
"mr-2 h-4 w-4",
|
|
||||||
editFormData.tableName === table.tableName ? "opacity-100" : "opacity-0"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="font-medium">{table.tableLabel}</span>
|
|
||||||
<span className="text-[10px] text-gray-500">{table.tableName}</span>
|
|
||||||
</div>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 데이터베이스 선택 (database 타입인 경우) */}
|
||||||
|
{editFormData.dataSourceType === "database" && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-tableName">테이블 *</Label>
|
||||||
|
<Popover open={tableComboboxOpen} onOpenChange={setTableComboboxOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={tableComboboxOpen}
|
||||||
|
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
|
||||||
|
disabled={loadingTables}
|
||||||
|
>
|
||||||
|
{loadingTables
|
||||||
|
? "로딩 중..."
|
||||||
|
: editFormData.tableName
|
||||||
|
? tables.find((table) => table.tableName === editFormData.tableName)?.tableLabel || editFormData.tableName
|
||||||
|
: "테이블을 선택하세요"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
className="p-0"
|
||||||
|
style={{ width: "var(--radix-popover-trigger-width)" }}
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="테이블 검색..." className="text-xs sm:text-sm" />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty className="text-xs sm:text-sm">
|
||||||
|
테이블을 찾을 수 없습니다.
|
||||||
|
</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{tables.map((table) => (
|
||||||
|
<CommandItem
|
||||||
|
key={table.tableName}
|
||||||
|
value={`${table.tableName} ${table.tableLabel}`}
|
||||||
|
onSelect={() => {
|
||||||
|
setEditFormData({ ...editFormData, tableName: table.tableName });
|
||||||
|
setTableComboboxOpen(false);
|
||||||
|
}}
|
||||||
|
className="text-xs sm:text-sm"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
editFormData.tableName === table.tableName ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{table.tableLabel}</span>
|
||||||
|
<span className="text-[10px] text-gray-500">{table.tableName}</span>
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* REST API 선택 (restapi 타입인 경우) */}
|
||||||
|
{editFormData.dataSourceType === "restapi" && (
|
||||||
|
<>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>REST API 연결 *</Label>
|
||||||
|
<Popover open={editRestApiComboboxOpen} onOpenChange={setEditRestApiComboboxOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={editRestApiComboboxOpen}
|
||||||
|
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
|
||||||
|
>
|
||||||
|
{editFormData.restApiConnectionId
|
||||||
|
? editRestApiConnections.find((c) => c.id === editFormData.restApiConnectionId)?.connection_name || "선택된 연결"
|
||||||
|
: "REST API 연결 선택"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
className="p-0"
|
||||||
|
style={{ width: "var(--radix-popover-trigger-width)" }}
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="연결 검색..." className="text-xs sm:text-sm" />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty className="text-xs sm:text-sm">
|
||||||
|
연결을 찾을 수 없습니다.
|
||||||
|
</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{editRestApiConnections.map((conn) => (
|
||||||
|
<CommandItem
|
||||||
|
key={conn.id}
|
||||||
|
value={conn.connection_name}
|
||||||
|
onSelect={() => {
|
||||||
|
setEditFormData({ ...editFormData, restApiConnectionId: conn.id || null });
|
||||||
|
setEditRestApiComboboxOpen(false);
|
||||||
|
}}
|
||||||
|
className="text-xs sm:text-sm"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
editFormData.restApiConnectionId === conn.id ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{conn.connection_name}</span>
|
||||||
|
<span className="text-[10px] text-gray-500">{conn.base_url}</span>
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-restApiEndpoint">API 엔드포인트</Label>
|
||||||
|
<Input
|
||||||
|
id="edit-restApiEndpoint"
|
||||||
|
value={editFormData.restApiEndpoint}
|
||||||
|
onChange={(e) => setEditFormData({ ...editFormData, restApiEndpoint: e.target.value })}
|
||||||
|
placeholder="예: /api/data/list"
|
||||||
|
/>
|
||||||
|
<p className="text-muted-foreground text-[10px]">
|
||||||
|
데이터를 조회할 API 엔드포인트 경로입니다
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-restApiJsonPath">JSON 경로</Label>
|
||||||
|
<Input
|
||||||
|
id="edit-restApiJsonPath"
|
||||||
|
value={editFormData.restApiJsonPath}
|
||||||
|
onChange={(e) => setEditFormData({ ...editFormData, restApiJsonPath: e.target.value })}
|
||||||
|
placeholder="예: data 또는 result.items"
|
||||||
|
/>
|
||||||
|
<p className="text-muted-foreground text-[10px]">
|
||||||
|
응답 JSON에서 데이터 배열의 경로입니다 (기본: data)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="edit-description">설명</Label>
|
<Label htmlFor="edit-description">설명</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
|
|
@ -1305,7 +1478,14 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||||
<Button variant="outline" onClick={() => setEditDialogOpen(false)}>
|
<Button variant="outline" onClick={() => setEditDialogOpen(false)}>
|
||||||
취소
|
취소
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleEditSave} disabled={!editFormData.screenName.trim() || !editFormData.tableName.trim()}>
|
<Button
|
||||||
|
onClick={handleEditSave}
|
||||||
|
disabled={
|
||||||
|
!editFormData.screenName.trim() ||
|
||||||
|
(editFormData.dataSourceType === "database" && !editFormData.tableName.trim()) ||
|
||||||
|
(editFormData.dataSourceType === "restapi" && !editFormData.restApiConnectionId)
|
||||||
|
}
|
||||||
|
>
|
||||||
저장
|
저장
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|
|
||||||
|
|
@ -1077,34 +1077,63 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
const search = searchTerm || undefined;
|
const search = searchTerm || undefined;
|
||||||
const filters = Object.keys(searchValues).length > 0 ? searchValues : undefined;
|
const filters = Object.keys(searchValues).length > 0 ? searchValues : undefined;
|
||||||
|
|
||||||
const entityJoinColumns = (tableConfig.columns || [])
|
// 🆕 REST API 데이터 소스 처리
|
||||||
.filter((col) => col.additionalJoinInfo)
|
const isRestApiTable = tableConfig.selectedTable.startsWith("restapi_") || tableConfig.selectedTable.startsWith("_restapi_");
|
||||||
.map((col) => ({
|
|
||||||
sourceTable: col.additionalJoinInfo!.sourceTable,
|
let response: any;
|
||||||
sourceColumn: col.additionalJoinInfo!.sourceColumn,
|
|
||||||
joinAlias: col.additionalJoinInfo!.joinAlias,
|
if (isRestApiTable) {
|
||||||
referenceTable: col.additionalJoinInfo!.referenceTable,
|
// REST API 데이터 소스인 경우
|
||||||
}));
|
const connectionIdMatch = tableConfig.selectedTable.match(/restapi_(\d+)/);
|
||||||
|
const connectionId = connectionIdMatch ? parseInt(connectionIdMatch[1]) : null;
|
||||||
|
|
||||||
|
if (connectionId) {
|
||||||
|
console.log("🌐 [TableList] REST API 데이터 소스 호출", { connectionId });
|
||||||
|
|
||||||
|
// REST API 연결 정보 가져오기 및 데이터 조회
|
||||||
|
const { ExternalRestApiConnectionAPI } = await import("@/lib/api/externalRestApiConnection");
|
||||||
|
const restApiData = await ExternalRestApiConnectionAPI.fetchData(
|
||||||
|
connectionId,
|
||||||
|
undefined, // endpoint - 연결 정보에서 가져옴
|
||||||
|
"response", // jsonPath - 기본값 response
|
||||||
|
);
|
||||||
|
|
||||||
|
response = {
|
||||||
|
data: restApiData.rows || [],
|
||||||
|
total: restApiData.total || restApiData.rows?.length || 0,
|
||||||
|
totalPages: Math.ceil((restApiData.total || restApiData.rows?.length || 0) / pageSize),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("✅ [TableList] REST API 응답:", {
|
||||||
|
dataLength: response.data.length,
|
||||||
|
total: response.total
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error("REST API 연결 ID를 찾을 수 없습니다.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 일반 DB 테이블인 경우 (기존 로직)
|
||||||
|
const entityJoinColumns = (tableConfig.columns || [])
|
||||||
|
.filter((col) => col.additionalJoinInfo)
|
||||||
|
.map((col) => ({
|
||||||
|
sourceTable: col.additionalJoinInfo!.sourceTable,
|
||||||
|
sourceColumn: col.additionalJoinInfo!.sourceColumn,
|
||||||
|
joinAlias: col.additionalJoinInfo!.joinAlias,
|
||||||
|
referenceTable: col.additionalJoinInfo!.referenceTable,
|
||||||
|
}));
|
||||||
|
|
||||||
// console.log("🔍 [TableList] API 호출 시작", {
|
// 🎯 항상 entityJoinApi 사용 (writer 컬럼 자동 조인 지원)
|
||||||
// tableName: tableConfig.selectedTable,
|
response = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, {
|
||||||
// page,
|
page,
|
||||||
// pageSize,
|
size: pageSize,
|
||||||
// sortBy,
|
sortBy,
|
||||||
// sortOrder,
|
sortOrder,
|
||||||
// });
|
search: filters,
|
||||||
|
enableEntityJoin: true,
|
||||||
// 🎯 항상 entityJoinApi 사용 (writer 컬럼 자동 조인 지원)
|
additionalJoinColumns: entityJoinColumns.length > 0 ? entityJoinColumns : undefined,
|
||||||
const response = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, {
|
dataFilter: tableConfig.dataFilter, // 🆕 데이터 필터 전달
|
||||||
page,
|
});
|
||||||
size: pageSize,
|
}
|
||||||
sortBy,
|
|
||||||
sortOrder,
|
|
||||||
search: filters,
|
|
||||||
enableEntityJoin: true,
|
|
||||||
additionalJoinColumns: entityJoinColumns.length > 0 ? entityJoinColumns : undefined,
|
|
||||||
dataFilter: tableConfig.dataFilter, // 🆕 데이터 필터 전달
|
|
||||||
});
|
|
||||||
|
|
||||||
// 실제 데이터의 item_number만 추출하여 중복 확인
|
// 실제 데이터의 item_number만 추출하여 중복 확인
|
||||||
const itemNumbers = (response.data || []).map((item: any) => item.item_number);
|
const itemNumbers = (response.data || []).map((item: any) => item.item_number);
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,13 @@ export interface CreateFlowDefinitionRequest {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
tableName: string;
|
tableName: string;
|
||||||
|
// 데이터 소스 관련
|
||||||
|
dbSourceType?: "internal" | "external" | "restapi";
|
||||||
|
dbConnectionId?: number;
|
||||||
|
// REST API 관련
|
||||||
|
restApiConnectionId?: number;
|
||||||
|
restApiEndpoint?: string;
|
||||||
|
restApiJsonPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateFlowDefinitionRequest {
|
export interface UpdateFlowDefinitionRequest {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue