Merge pull request 'feature/screen-management' (#83) from feature/screen-management into main

Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/83
This commit is contained in:
kjs 2025-10-01 17:59:06 +09:00
commit 63ee0fbb5a
3 changed files with 156 additions and 125 deletions

View File

@ -1,7 +1,11 @@
// @ts-ignore // @ts-ignore
import * as oracledb from 'oracledb'; import * as oracledb from "oracledb";
import { DatabaseConnector, ConnectionConfig, QueryResult } from '../interfaces/DatabaseConnector'; import {
import { ConnectionTestResult, TableInfo } from '../types/externalDbTypes'; DatabaseConnector,
ConnectionConfig,
QueryResult,
} from "../interfaces/DatabaseConnector";
import { ConnectionTestResult, TableInfo } from "../types/externalDbTypes";
export class OracleConnector implements DatabaseConnector { export class OracleConnector implements DatabaseConnector {
private connection: oracledb.Connection | null = null; private connection: oracledb.Connection | null = null;
@ -9,7 +13,7 @@ export class OracleConnector implements DatabaseConnector {
constructor(config: ConnectionConfig) { constructor(config: ConnectionConfig) {
this.config = config; this.config = config;
// Oracle XE 21c 특화 설정 // Oracle XE 21c 특화 설정
// oracledb.outFormat = oracledb.OUT_FORMAT_OBJECT; // oracledb.outFormat = oracledb.OUT_FORMAT_OBJECT;
// oracledb.autoCommit = true; // oracledb.autoCommit = true;
@ -19,31 +23,31 @@ export class OracleConnector implements DatabaseConnector {
try { try {
// Oracle XE 21c 연결 문자열 구성 // Oracle XE 21c 연결 문자열 구성
const connectionString = this.buildConnectionString(); const connectionString = this.buildConnectionString();
const connectionConfig: any = { const connectionConfig: any = {
user: this.config.user, user: this.config.user,
password: this.config.password, password: this.config.password,
connectString: connectionString connectString: connectionString,
}; };
this.connection = await oracledb.getConnection(connectionConfig); this.connection = await oracledb.getConnection(connectionConfig);
console.log('Oracle XE 21c 연결 성공'); console.log("Oracle XE 21c 연결 성공");
} catch (error: any) { } catch (error: any) {
console.error('Oracle XE 21c 연결 실패:', error); console.error("Oracle XE 21c 연결 실패:", error);
throw new Error(`Oracle 연결 실패: ${error.message}`); throw new Error(`Oracle 연결 실패: ${error.message}`);
} }
} }
private buildConnectionString(): string { private buildConnectionString(): string {
const { host, port, database } = this.config; const { host, port, database } = this.config;
// Oracle XE 21c는 기본적으로 XE 서비스명을 사용 // Oracle XE 21c는 기본적으로 XE 서비스명을 사용
// 다양한 연결 문자열 형식 지원 // 다양한 연결 문자열 형식 지원
if (database.includes('/') || database.includes(':')) { if (database.includes("/") || database.includes(":")) {
// 이미 완전한 연결 문자열인 경우 // 이미 완전한 연결 문자열인 경우
return database; return database;
} }
// Oracle XE 21c 표준 형식 // Oracle XE 21c 표준 형식
return `${host}:${port}/${database}`; return `${host}:${port}/${database}`;
} }
@ -53,9 +57,9 @@ export class OracleConnector implements DatabaseConnector {
try { try {
await this.connection.close(); await this.connection.close();
this.connection = null; this.connection = null;
console.log('Oracle 연결 해제됨'); console.log("Oracle 연결 해제됨");
} catch (error: any) { } catch (error: any) {
console.error('Oracle 연결 해제 실패:', error); console.error("Oracle 연결 해제 실패:", error);
} }
} }
} }
@ -65,28 +69,28 @@ export class OracleConnector implements DatabaseConnector {
if (!this.connection) { if (!this.connection) {
await this.connect(); await this.connect();
} }
// Oracle XE 21c 버전 확인 쿼리 // Oracle XE 21c 버전 확인 쿼리
const result = await this.connection!.execute( const result = await this.connection!.execute(
'SELECT BANNER FROM V$VERSION WHERE BANNER LIKE \'Oracle%\'' "SELECT BANNER FROM V$VERSION WHERE BANNER LIKE 'Oracle%'"
); );
console.log('Oracle 버전:', result.rows); console.log("Oracle 버전:", result.rows);
return { return {
success: true, success: true,
message: '연결 성공', message: "연결 성공",
details: { details: {
server_version: (result.rows as any)?.[0]?.BANNER || 'Unknown' server_version: (result.rows as any)?.[0]?.BANNER || "Unknown",
} },
}; };
} catch (error: any) { } catch (error: any) {
console.error('Oracle 연결 테스트 실패:', error); console.error("Oracle 연결 테스트 실패:", error);
return { return {
success: false, success: false,
message: '연결 실패', message: "연결 실패",
details: { details: {
server_version: error.message server_version: error.message,
} },
}; };
} }
} }
@ -98,52 +102,64 @@ export class OracleConnector implements DatabaseConnector {
try { try {
const startTime = Date.now(); const startTime = Date.now();
// 쿼리 타입 확인 (DML인지 SELECT인지)
const isDML = /^\s*(INSERT|UPDATE|DELETE|MERGE)/i.test(query);
// Oracle XE 21c 쿼리 실행 옵션 // Oracle XE 21c 쿼리 실행 옵션
const options: any = { const options: any = {
outFormat: (oracledb as any).OUT_FORMAT_OBJECT, // OBJECT format outFormat: (oracledb as any).OUT_FORMAT_OBJECT, // OBJECT format
maxRows: 10000, // XE 제한 고려 maxRows: 10000, // XE 제한 고려
fetchArraySize: 100 fetchArraySize: 100,
autoCommit: isDML, // ✅ DML 쿼리는 자동 커밋
}; };
console.log("Oracle 쿼리 실행:", {
query: query.substring(0, 100) + "...",
isDML,
autoCommit: options.autoCommit,
});
const result = await this.connection!.execute(query, params, options); const result = await this.connection!.execute(query, params, options);
const executionTime = Date.now() - startTime; const executionTime = Date.now() - startTime;
console.log('Oracle 쿼리 실행 결과:', { console.log("Oracle 쿼리 실행 결과:", {
query, query,
rowCount: result.rows?.length || 0, rowCount: result.rows?.length || 0,
rowsAffected: result.rowsAffected,
metaData: result.metaData?.length || 0, metaData: result.metaData?.length || 0,
executionTime: `${executionTime}ms`, executionTime: `${executionTime}ms`,
actualRows: result.rows, actualRows: result.rows,
metaDataInfo: result.metaData metaDataInfo: result.metaData,
autoCommit: options.autoCommit,
}); });
return { return {
rows: result.rows || [], rows: result.rows || [],
rowCount: result.rowsAffected || (result.rows?.length || 0), rowCount: result.rowsAffected || result.rows?.length || 0,
fields: this.extractFieldInfo(result.metaData || []) fields: this.extractFieldInfo(result.metaData || []),
}; };
} catch (error: any) { } catch (error: any) {
console.error('Oracle 쿼리 실행 실패:', error); console.error("Oracle 쿼리 실행 실패:", error);
throw new Error(`쿼리 실행 실패: ${error.message}`); throw new Error(`쿼리 실행 실패: ${error.message}`);
} }
} }
private extractFieldInfo(metaData: any[]): any[] { private extractFieldInfo(metaData: any[]): any[] {
return metaData.map(field => ({ return metaData.map((field) => ({
name: field.name, name: field.name,
type: this.mapOracleType(field.dbType), type: this.mapOracleType(field.dbType),
length: field.precision || field.byteSize, length: field.precision || field.byteSize,
nullable: field.nullable nullable: field.nullable,
})); }));
} }
private mapOracleType(oracleType: any): string { private mapOracleType(oracleType: any): string {
// Oracle XE 21c 타입 매핑 (간단한 방식) // Oracle XE 21c 타입 매핑 (간단한 방식)
if (typeof oracleType === 'string') { if (typeof oracleType === "string") {
return oracleType; return oracleType;
} }
return 'UNKNOWN'; return "UNKNOWN";
} }
async getTables(): Promise<TableInfo[]> { async getTables(): Promise<TableInfo[]> {
@ -155,22 +171,21 @@ export class OracleConnector implements DatabaseConnector {
ORDER BY table_name ORDER BY table_name
`; `;
console.log('Oracle 테이블 조회 시작 - 사용자:', this.config.user); console.log("Oracle 테이블 조회 시작 - 사용자:", this.config.user);
const result = await this.executeQuery(query); const result = await this.executeQuery(query);
console.log('사용자 스키마 테이블 조회 결과:', result.rows); console.log("사용자 스키마 테이블 조회 결과:", result.rows);
const tables = result.rows.map((row: any) => ({ const tables = result.rows.map((row: any) => ({
table_name: row.TABLE_NAME, table_name: row.TABLE_NAME,
columns: [], columns: [],
description: null description: null,
})); }));
console.log(`${tables.length}개의 사용자 테이블을 찾았습니다.`); console.log(`${tables.length}개의 사용자 테이블을 찾았습니다.`);
return tables; return tables;
} catch (error: any) { } catch (error: any) {
console.error('Oracle 테이블 목록 조회 실패:', error); console.error("Oracle 테이블 목록 조회 실패:", error);
throw new Error(`테이블 목록 조회 실패: ${error.message}`); throw new Error(`테이블 목록 조회 실패: ${error.message}`);
} }
} }
@ -178,7 +193,7 @@ export class OracleConnector implements DatabaseConnector {
async getColumns(tableName: string): Promise<any[]> { async getColumns(tableName: string): Promise<any[]> {
try { try {
console.log(`[OracleConnector] getColumns 호출: tableName=${tableName}`); console.log(`[OracleConnector] getColumns 호출: tableName=${tableName}`);
const query = ` const query = `
SELECT SELECT
column_name, column_name,
@ -192,41 +207,44 @@ export class OracleConnector implements DatabaseConnector {
WHERE table_name = UPPER(:tableName) WHERE table_name = UPPER(:tableName)
ORDER BY column_id ORDER BY column_id
`; `;
console.log(`[OracleConnector] 쿼리 실행 시작: ${query}`); console.log(`[OracleConnector] 쿼리 실행 시작: ${query}`);
const result = await this.executeQuery(query, [tableName]); const result = await this.executeQuery(query, [tableName]);
console.log(`[OracleConnector] 쿼리 결과:`, result.rows); console.log(`[OracleConnector] 쿼리 결과:`, result.rows);
console.log(`[OracleConnector] 결과 개수:`, result.rows ? result.rows.length : 'null/undefined'); console.log(
`[OracleConnector] 결과 개수:`,
result.rows ? result.rows.length : "null/undefined"
);
const mappedResult = result.rows.map((row: any) => ({ const mappedResult = result.rows.map((row: any) => ({
column_name: row.COLUMN_NAME, column_name: row.COLUMN_NAME,
data_type: this.formatOracleDataType(row), data_type: this.formatOracleDataType(row),
is_nullable: row.NULLABLE === 'Y' ? 'YES' : 'NO', is_nullable: row.NULLABLE === "Y" ? "YES" : "NO",
column_default: row.DATA_DEFAULT column_default: row.DATA_DEFAULT,
})); }));
console.log(`[OracleConnector] 매핑된 결과:`, mappedResult); console.log(`[OracleConnector] 매핑된 결과:`, mappedResult);
return mappedResult; return mappedResult;
} catch (error: any) { } catch (error: any) {
console.error('[OracleConnector] getColumns 오류:', error); console.error("[OracleConnector] getColumns 오류:", error);
throw new Error(`테이블 컬럼 조회 실패: ${error.message}`); throw new Error(`테이블 컬럼 조회 실패: ${error.message}`);
} }
} }
private formatOracleDataType(row: any): string { private formatOracleDataType(row: any): string {
const { DATA_TYPE, DATA_LENGTH, DATA_PRECISION, DATA_SCALE } = row; const { DATA_TYPE, DATA_LENGTH, DATA_PRECISION, DATA_SCALE } = row;
switch (DATA_TYPE) { switch (DATA_TYPE) {
case 'NUMBER': case "NUMBER":
if (DATA_PRECISION && DATA_SCALE !== null) { if (DATA_PRECISION && DATA_SCALE !== null) {
return `NUMBER(${DATA_PRECISION},${DATA_SCALE})`; return `NUMBER(${DATA_PRECISION},${DATA_SCALE})`;
} else if (DATA_PRECISION) { } else if (DATA_PRECISION) {
return `NUMBER(${DATA_PRECISION})`; return `NUMBER(${DATA_PRECISION})`;
} }
return 'NUMBER'; return "NUMBER";
case 'VARCHAR2': case "VARCHAR2":
case 'CHAR': case "CHAR":
return `${DATA_TYPE}(${DATA_LENGTH})`; return `${DATA_TYPE}(${DATA_LENGTH})`;
default: default:
return DATA_TYPE; return DATA_TYPE;

View File

@ -268,17 +268,19 @@ const ActionConditionBuilder: React.FC<ActionConditionBuilderProps> = ({
{fromColumns.length > 0 && ( {fromColumns.length > 0 && (
<> <>
<div className="text-muted-foreground px-2 py-1 text-xs font-medium">FROM </div> <div className="text-muted-foreground px-2 py-1 text-xs font-medium">FROM </div>
{fromColumns.map((column) => ( {fromColumns
<SelectItem key={`from_${column.columnName}`} value={`from.${column.columnName}`}> .filter((column) => column.columnName) // 빈 문자열 제외
<div className="flex items-center gap-2"> .map((column) => (
<span className="text-blue-600">📤</span> <SelectItem key={`from_${column.columnName}`} value={`from.${column.columnName}`}>
<span>{column.displayName || column.columnName}</span> <div className="flex items-center gap-2">
<Badge variant="outline" className="text-xs"> <span className="text-blue-600">📤</span>
{column.webType || column.dataType} <span>{column.displayName || column.columnName}</span>
</Badge> <Badge variant="outline" className="text-xs">
</div> {column.webType || column.dataType}
</SelectItem> </Badge>
))} </div>
</SelectItem>
))}
</> </>
)} )}
@ -286,15 +288,17 @@ const ActionConditionBuilder: React.FC<ActionConditionBuilderProps> = ({
{toColumns.length > 0 && ( {toColumns.length > 0 && (
<> <>
<div className="text-muted-foreground px-2 py-1 text-xs font-medium">TO </div> <div className="text-muted-foreground px-2 py-1 text-xs font-medium">TO </div>
{toColumns.map((column) => ( {toColumns
<SelectItem key={`to_${column.columnName}`} value={`to.${column.columnName}`}> .filter((column) => column.columnName) // 빈 문자열 제외
<div className="flex items-center gap-2"> .map((column) => (
<span className="text-green-600">📥</span> <SelectItem key={`to_${column.columnName}`} value={`to.${column.columnName}`}>
<span>{column.displayName || column.columnName}</span> <div className="flex items-center gap-2">
<Badge variant="outline" className="text-xs"> <span className="text-green-600">📥</span>
{column.webType || column.dataType} <span>{column.displayName || column.columnName}</span>
</Badge> <Badge variant="outline" className="text-xs">
</div> {column.webType || column.dataType}
</Badge>
</div>
</SelectItem> </SelectItem>
))} ))}
</> </>
@ -488,14 +492,16 @@ const ActionConditionBuilder: React.FC<ActionConditionBuilderProps> = ({
{fromColumns.length > 0 && ( {fromColumns.length > 0 && (
<> <>
<div className="text-muted-foreground px-2 py-1 text-xs font-medium">FROM </div> <div className="text-muted-foreground px-2 py-1 text-xs font-medium">FROM </div>
{fromColumns.map((column) => ( {fromColumns
<SelectItem key={`from_${column.columnName}`} value={`from.${column.columnName}`}> .filter((column) => column.columnName) // 빈 문자열 제외
<div className="flex items-center gap-2"> .map((column) => (
<span className="text-blue-600">📤</span> <SelectItem key={`from_${column.columnName}`} value={`from.${column.columnName}`}>
<span>{column.displayName || column.columnName}</span> <div className="flex items-center gap-2">
</div> <span className="text-blue-600">📤</span>
</SelectItem> <span>{column.displayName || column.columnName}</span>
))} </div>
</SelectItem>
))}
</> </>
)} )}
@ -503,14 +509,16 @@ const ActionConditionBuilder: React.FC<ActionConditionBuilderProps> = ({
{toColumns.length > 0 && ( {toColumns.length > 0 && (
<> <>
<div className="text-muted-foreground px-2 py-1 text-xs font-medium">TO </div> <div className="text-muted-foreground px-2 py-1 text-xs font-medium">TO </div>
{toColumns.map((column) => ( {toColumns
<SelectItem key={`to_${column.columnName}`} value={`to.${column.columnName}`}> .filter((column) => column.columnName) // 빈 문자열 제외
<div className="flex items-center gap-2"> .map((column) => (
<span className="text-green-600">📥</span> <SelectItem key={`to_${column.columnName}`} value={`to.${column.columnName}`}>
<span>{column.displayName || column.columnName}</span> <div className="flex items-center gap-2">
</div> <span className="text-green-600">📥</span>
</SelectItem> <span>{column.displayName || column.columnName}</span>
))} </div>
</SelectItem>
))}
</> </>
)} )}
</SelectContent> </SelectContent>
@ -612,14 +620,16 @@ const ActionConditionBuilder: React.FC<ActionConditionBuilderProps> = ({
<div className="text-muted-foreground px-2 py-1 text-xs font-medium"> <div className="text-muted-foreground px-2 py-1 text-xs font-medium">
FROM FROM
</div> </div>
{fromColumns.map((column) => ( {fromColumns
<SelectItem key={`from_${column.columnName}`} value={`from.${column.columnName}`}> .filter((column) => column.columnName) // 빈 문자열 제외
<div className="flex items-center gap-2"> .map((column) => (
<span className="text-blue-600">📤</span> <SelectItem key={`from_${column.columnName}`} value={`from.${column.columnName}`}>
<span>{column.displayName || column.columnName}</span> <div className="flex items-center gap-2">
</div> <span className="text-blue-600">📤</span>
</SelectItem> <span>{column.displayName || column.columnName}</span>
))} </div>
</SelectItem>
))}
</> </>
)} )}
@ -627,13 +637,15 @@ const ActionConditionBuilder: React.FC<ActionConditionBuilderProps> = ({
{toColumns.length > 0 && ( {toColumns.length > 0 && (
<> <>
<div className="text-muted-foreground px-2 py-1 text-xs font-medium">TO </div> <div className="text-muted-foreground px-2 py-1 text-xs font-medium">TO </div>
{toColumns.map((column) => ( {toColumns
<SelectItem key={`to_${column.columnName}`} value={`to.${column.columnName}`}> .filter((column) => column.columnName) // 빈 문자열 제외
<div className="flex items-center gap-2"> .map((column) => (
<span className="text-green-600">📥</span> <SelectItem key={`to_${column.columnName}`} value={`to.${column.columnName}`}>
<span>{column.displayName || column.columnName}</span> <div className="flex items-center gap-2">
</div> <span className="text-green-600">📥</span>
</SelectItem> <span>{column.displayName || column.columnName}</span>
</div>
</SelectItem>
))} ))}
</> </>
)} )}
@ -729,16 +741,18 @@ const ActionConditionBuilder: React.FC<ActionConditionBuilderProps> = ({
<SelectValue placeholder="대상 필드" /> <SelectValue placeholder="대상 필드" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{getAvailableFieldsForMapping(index).map((column) => ( {getAvailableFieldsForMapping(index)
<SelectItem key={column.columnName} value={column.columnName}> .filter((column) => column.columnName) // 빈 문자열 제외
<div className="flex items-center gap-2"> .map((column) => (
<span>{column.displayName || column.columnName}</span> <SelectItem key={column.columnName} value={column.columnName}>
<Badge variant="outline" className="text-xs"> <div className="flex items-center gap-2">
{column.webType || column.dataType} <span>{column.displayName || column.columnName}</span>
</Badge> <Badge variant="outline" className="text-xs">
</div> {column.webType || column.dataType}
</SelectItem> </Badge>
))} </div>
</SelectItem>
))}
</SelectContent> </SelectContent>
</Select> </Select>

View File

@ -51,7 +51,7 @@ export const EditModal: React.FC<EditModalProps> = ({
console.log(`🎯 계산된 모달 크기: ${maxWidth}px x ${maxHeight}px`); console.log(`🎯 계산된 모달 크기: ${maxWidth}px x ${maxHeight}px`);
console.log( console.log(
`📍 컴포넌트 위치들:`, "📍 컴포넌트 위치들:",
components.map((c) => ({ x: c.position?.x, y: c.position?.y, w: c.size?.width, h: c.size?.height })), components.map((c) => ({ x: c.position?.x, y: c.position?.y, w: c.size?.width, h: c.size?.height })),
); );
return { width: maxWidth, height: maxHeight }; return { width: maxWidth, height: maxHeight };
@ -85,7 +85,7 @@ export const EditModal: React.FC<EditModalProps> = ({
// 스크롤 완전 제거 // 스크롤 완전 제거
if (modalContent) { if (modalContent) {
modalContent.style.overflow = "hidden"; modalContent.style.overflow = "hidden";
console.log(`🚫 스크롤 완전 비활성화`); console.log("🚫 스크롤 완전 비활성화");
} }
}, 100); // 100ms 지연으로 렌더링 완료 후 실행 }, 100); // 100ms 지연으로 렌더링 완료 후 실행
} }
@ -152,7 +152,7 @@ export const EditModal: React.FC<EditModalProps> = ({
// 코드 타입인 경우 특별히 로깅 // 코드 타입인 경우 특별히 로깅
if ((comp as any).widgetType === "code") { if ((comp as any).widgetType === "code") {
console.log(` 🔍 코드 타입 세부정보:`, { console.log(" 🔍 코드 타입 세부정보:", {
columnName: comp.columnName, columnName: comp.columnName,
componentId: comp.id, componentId: comp.id,
formValue, formValue,
@ -275,22 +275,21 @@ export const EditModal: React.FC<EditModalProps> = ({
{components.map((component, index) => ( {components.map((component, index) => (
<div <div
key={component.id} key={component.id}
className="rounded-xl border border-gray-200/60 bg-gradient-to-br from-white to-gray-50/30 p-4 shadow-sm transition-all duration-200 hover:shadow-md"
style={{ style={{
position: "absolute", position: "absolute",
top: component.position?.y || 0, top: component.position?.y || 0,
left: component.position?.x || 0, left: component.position?.x || 0,
width: component.size?.width || 200, width: component.size?.width || 200,
height: component.size?.height || 40, height: component.size?.height || 40,
zIndex: component.position?.z || (1000 + index), // 모달 내부에서 충분히 높은 z-index zIndex: component.position?.z || 1000 + index, // 모달 내부에서 충분히 높은 z-index
}} }}
> >
{/* 위젯 컴포넌트는 InteractiveScreenViewer 사용 (라벨 표시를 위해) */} {/* 위젯 컴포넌트는 InteractiveScreenViewer 사용 (라벨 표시) */}
{component.type === "widget" ? ( {component.type === "widget" ? (
<InteractiveScreenViewer <InteractiveScreenViewer
component={component} component={component}
allComponents={components} allComponents={components}
hideLabel={true} // 라벨 숨김 (원래 화면과 동일하게) hideLabel={false} // ✅ 라벨 표시
formData={formData} formData={formData}
onFormDataChange={(fieldName, value) => { onFormDataChange={(fieldName, value) => {
console.log("📝 폼 데이터 변경:", fieldName, value); console.log("📝 폼 데이터 변경:", fieldName, value);
@ -314,7 +313,7 @@ export const EditModal: React.FC<EditModalProps> = ({
...component, ...component,
style: { style: {
...component.style, ...component.style,
labelDisplay: false, // 라벨 숨김 (원래 화면과 동일하게) labelDisplay: true, // ✅ 라벨 표시
}, },
}} }}
screenId={screenId} screenId={screenId}