restapi도 가능하게 구현

This commit is contained in:
leeheejin 2025-12-02 13:20:49 +09:00
parent 0789eb2e20
commit 2c447fd325
10 changed files with 749 additions and 260 deletions

View File

@ -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
); );

View File

@ -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: "화면 정보가 수정되었습니다." });

View File

@ -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,

View File

@ -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은 트랜잭션 종료 시 자동으로 해제됨

View File

@ -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 사용)
} }

View File

@ -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">

View File

@ -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 데이터 소스 로드 완료:", {

View File

@ -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>

View File

@ -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);

View File

@ -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 {