Add environment variable example and update .gitignore

- Created a new .env.example file to provide a template for environment variables, including database connection details, JWT settings, encryption keys, and external API keys.
- Updated .gitignore to include additional test output directories and archive files, ensuring that unnecessary files are not tracked by Git.
- Removed outdated approval test reports and scripts that are no longer needed, streamlining the project structure.

These changes improve the clarity of environment configuration and maintain a cleaner repository.
This commit is contained in:
kjs
2026-04-01 12:12:15 +09:00
parent 250a83b581
commit ccb0c8df4c
112 changed files with 1165 additions and 11644 deletions

View File

@@ -1,52 +0,0 @@
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();
async function addButtonWebType() {
try {
console.log("🔍 버튼 웹타입 확인 중...");
// 기존 button 웹타입 확인
const existingButton = await prisma.web_type_standards.findUnique({
where: { web_type: "button" },
});
if (existingButton) {
console.log("✅ 버튼 웹타입이 이미 존재합니다.");
console.log("📄 기존 설정:", JSON.stringify(existingButton, null, 2));
return;
}
console.log(" 버튼 웹타입 추가 중...");
// 버튼 웹타입 추가
const buttonWebType = await prisma.web_type_standards.create({
data: {
web_type: "button",
type_name: "버튼",
type_name_eng: "Button",
description: "클릭 가능한 버튼 컴포넌트",
category: "action",
component_name: "ButtonWidget",
config_panel: "ButtonConfigPanel",
default_config: {
actionType: "custom",
variant: "default",
},
sort_order: 100,
is_active: "Y",
created_by: "system",
updated_by: "system",
},
});
console.log("✅ 버튼 웹타입이 성공적으로 추가되었습니다!");
console.log("📄 추가된 설정:", JSON.stringify(buttonWebType, null, 2));
} catch (error) {
console.error("❌ 버튼 웹타입 추가 실패:", error);
} finally {
await prisma.$disconnect();
}
}
addButtonWebType();

View File

@@ -1,34 +0,0 @@
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();
async function addDataMappingColumn() {
try {
console.log(
"🔄 external_call_configs 테이블에 data_mapping_config 컬럼 추가 중..."
);
// data_mapping_config JSONB 컬럼 추가
await prisma.$executeRaw`
ALTER TABLE external_call_configs
ADD COLUMN IF NOT EXISTS data_mapping_config JSONB
`;
console.log("✅ data_mapping_config 컬럼이 성공적으로 추가되었습니다.");
// 기존 레코드에 기본값 설정
await prisma.$executeRaw`
UPDATE external_call_configs
SET data_mapping_config = '{"direction": "none"}'::jsonb
WHERE data_mapping_config IS NULL
`;
console.log("✅ 기존 레코드에 기본값이 설정되었습니다.");
} catch (error) {
console.error("❌ 컬럼 추가 실패:", error);
} finally {
await prisma.$disconnect();
}
}
addDataMappingColumn();

View File

@@ -27,11 +27,11 @@ async function addExternalDbConnection() {
name: "운영_외부_PostgreSQL",
description: "운영용 외부 PostgreSQL 데이터베이스",
dbType: "postgresql",
host: "39.117.244.52",
port: 11132,
databaseName: "plm",
username: "postgres",
password: "ph0909!!", // 이 값은 암호화되어 저장됩니다
host: process.env.EXT_DB_HOST || "localhost",
port: parseInt(process.env.EXT_DB_PORT || "5432"),
databaseName: process.env.EXT_DB_NAME || "vexplor_dev",
username: process.env.EXT_DB_USER || "postgres",
password: process.env.EXT_DB_PASSWORD || "", // 환경변수로 전달
sslEnabled: false,
isActive: true,
},

View File

@@ -1,105 +0,0 @@
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();
async function addMissingColumns() {
try {
console.log("🔄 누락된 컬럼들을 screen_layouts 테이블에 추가 중...");
// layout_type 컬럼 추가
try {
await prisma.$executeRaw`
ALTER TABLE screen_layouts
ADD COLUMN IF NOT EXISTS layout_type VARCHAR(50);
`;
console.log("✅ layout_type 컬럼 추가 완료");
} catch (error) {
console.log(
" layout_type 컬럼이 이미 존재하거나 추가 중 오류:",
error.message
);
}
// layout_config 컬럼 추가
try {
await prisma.$executeRaw`
ALTER TABLE screen_layouts
ADD COLUMN IF NOT EXISTS layout_config JSONB;
`;
console.log("✅ layout_config 컬럼 추가 완료");
} catch (error) {
console.log(
" layout_config 컬럼이 이미 존재하거나 추가 중 오류:",
error.message
);
}
// zones_config 컬럼 추가
try {
await prisma.$executeRaw`
ALTER TABLE screen_layouts
ADD COLUMN IF NOT EXISTS zones_config JSONB;
`;
console.log("✅ zones_config 컬럼 추가 완료");
} catch (error) {
console.log(
" zones_config 컬럼이 이미 존재하거나 추가 중 오류:",
error.message
);
}
// zone_id 컬럼 추가
try {
await prisma.$executeRaw`
ALTER TABLE screen_layouts
ADD COLUMN IF NOT EXISTS zone_id VARCHAR(100);
`;
console.log("✅ zone_id 컬럼 추가 완료");
} catch (error) {
console.log(
" zone_id 컬럼이 이미 존재하거나 추가 중 오류:",
error.message
);
}
// 인덱스 생성 (성능 향상)
try {
await prisma.$executeRaw`
CREATE INDEX IF NOT EXISTS idx_screen_layouts_layout_type
ON screen_layouts(layout_type);
`;
console.log("✅ layout_type 인덱스 생성 완료");
} catch (error) {
console.log(" layout_type 인덱스 생성 중 오류:", error.message);
}
try {
await prisma.$executeRaw`
CREATE INDEX IF NOT EXISTS idx_screen_layouts_zone_id
ON screen_layouts(zone_id);
`;
console.log("✅ zone_id 인덱스 생성 완료");
} catch (error) {
console.log(" zone_id 인덱스 생성 중 오류:", error.message);
}
// 최종 테이블 구조 확인
const columns = await prisma.$queryRaw`
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_name = 'screen_layouts'
ORDER BY ordinal_position
`;
console.log("\n📋 screen_layouts 테이블 최종 구조:");
console.table(columns);
console.log("\n🎉 모든 누락된 컬럼 추가 작업이 완료되었습니다!");
} catch (error) {
console.error("❌ 컬럼 추가 중 오류 발생:", error);
} finally {
await prisma.$disconnect();
}
}
addMissingColumns();

View File

@@ -1,318 +0,0 @@
/**
* 탑씰(company_7) 버튼 스타일 일괄 변경 스크립트
*
* 사용법:
* npx ts-node scripts/btn-bulk-update-company7.ts --test # 1건만 테스트 (ROLLBACK)
* npx ts-node scripts/btn-bulk-update-company7.ts --run # 전체 실행 (COMMIT)
* npx ts-node scripts/btn-bulk-update-company7.ts --backup # 백업 테이블만 생성
* npx ts-node scripts/btn-bulk-update-company7.ts --restore # 백업에서 원복
*/
import { Pool } from "pg";
// ── 배포 DB 연결 ──
const pool = new Pool({
connectionString:
"postgresql://postgres:vexplor0909!!@211.115.91.141:11134/vexplor",
});
const COMPANY_CODE = "COMPANY_7";
const BACKUP_TABLE = "screen_layouts_v2_backup_20260313";
// ── 액션별 기본 아이콘 매핑 (frontend/lib/button-icon-map.tsx 기준) ──
const actionIconMap: Record<string, string> = {
save: "Check",
delete: "Trash2",
edit: "Pencil",
navigate: "ArrowRight",
modal: "Maximize2",
transferData: "SendHorizontal",
excel_download: "Download",
excel_upload: "Upload",
quickInsert: "Zap",
control: "Settings",
barcode_scan: "ScanLine",
operation_control: "Truck",
event: "Send",
copy: "Copy",
};
const FALLBACK_ICON = "SquareMousePointer";
function getIconForAction(actionType?: string): string {
if (actionType && actionIconMap[actionType]) {
return actionIconMap[actionType];
}
return FALLBACK_ICON;
}
// ── 버튼 컴포넌트인지 판별 (최상위 + 탭 내부 둘 다 지원) ──
function isTopLevelButton(comp: any): boolean {
return (
comp.url?.includes("v2-button-primary") ||
comp.overrides?.type === "v2-button-primary"
);
}
function isTabChildButton(comp: any): boolean {
return comp.componentType === "v2-button-primary";
}
function isButtonComponent(comp: any): boolean {
return isTopLevelButton(comp) || isTabChildButton(comp);
}
// ── 탭 위젯인지 판별 ──
function isTabsWidget(comp: any): boolean {
return (
comp.url?.includes("v2-tabs-widget") ||
comp.overrides?.type === "v2-tabs-widget"
);
}
// ── 버튼 스타일 변경 (최상위 버튼용: overrides 사용) ──
function applyButtonStyle(config: any, actionType: string | undefined) {
const iconName = getIconForAction(actionType);
config.displayMode = "icon-text";
config.icon = {
name: iconName,
type: "lucide",
size: "보통",
...(config.icon?.color ? { color: config.icon.color } : {}),
};
config.iconTextPosition = "right";
config.iconGap = 6;
if (!config.style) config.style = {};
delete config.style.width; // 레거시 하드코딩 너비 제거 (size.width만 사용)
config.style.borderRadius = "8px";
config.style.labelColor = "#FFFFFF";
config.style.fontSize = "12px";
config.style.fontWeight = "normal";
config.style.labelTextAlign = "left";
if (actionType === "delete") {
config.style.backgroundColor = "#F04544";
} else if (actionType === "excel_upload" || actionType === "excel_download") {
config.style.backgroundColor = "#212121";
} else {
config.style.backgroundColor = "#3B83F6";
}
}
function updateButtonStyle(comp: any): boolean {
if (isTopLevelButton(comp)) {
const overrides = comp.overrides || {};
const actionType = overrides.action?.type;
if (!comp.size) comp.size = {};
comp.size.height = 40;
applyButtonStyle(overrides, actionType);
comp.overrides = overrides;
return true;
}
if (isTabChildButton(comp)) {
const config = comp.componentConfig || {};
const actionType = config.action?.type;
if (!comp.size) comp.size = {};
comp.size.height = 40;
applyButtonStyle(config, actionType);
comp.componentConfig = config;
// 탭 내부 버튼은 렌더러가 comp.style (최상위)에서 스타일을 읽음
if (!comp.style) comp.style = {};
comp.style.borderRadius = "8px";
comp.style.labelColor = "#FFFFFF";
comp.style.fontSize = "12px";
comp.style.fontWeight = "normal";
comp.style.labelTextAlign = "left";
comp.style.backgroundColor = config.style.backgroundColor;
return true;
}
return false;
}
// ── 백업 테이블 생성 ──
async function createBackup() {
console.log(`\n=== 백업 테이블 생성: ${BACKUP_TABLE} ===`);
const exists = await pool.query(
`SELECT to_regclass($1) AS tbl`,
[BACKUP_TABLE],
);
if (exists.rows[0].tbl) {
console.log(`백업 테이블이 이미 존재합니다: ${BACKUP_TABLE}`);
const count = await pool.query(`SELECT COUNT(*) FROM ${BACKUP_TABLE}`);
console.log(`기존 백업 레코드 수: ${count.rows[0].count}`);
return;
}
await pool.query(
`CREATE TABLE ${BACKUP_TABLE} AS
SELECT * FROM screen_layouts_v2
WHERE company_code = $1`,
[COMPANY_CODE],
);
const count = await pool.query(`SELECT COUNT(*) FROM ${BACKUP_TABLE}`);
console.log(`백업 완료. 레코드 수: ${count.rows[0].count}`);
}
// ── 백업에서 원복 ──
async function restoreFromBackup() {
console.log(`\n=== 백업에서 원복: ${BACKUP_TABLE} ===`);
const result = await pool.query(
`UPDATE screen_layouts_v2 AS target
SET layout_data = backup.layout_data,
updated_at = backup.updated_at
FROM ${BACKUP_TABLE} AS backup
WHERE target.screen_id = backup.screen_id
AND target.company_code = backup.company_code
AND target.layer_id = backup.layer_id`,
);
console.log(`원복 완료. 변경된 레코드 수: ${result.rowCount}`);
}
// ── 메인: 버튼 일괄 변경 ──
async function updateButtons(testMode: boolean) {
const modeLabel = testMode ? "테스트 (1건, ROLLBACK)" : "전체 실행 (COMMIT)";
console.log(`\n=== 버튼 일괄 변경 시작 [${modeLabel}] ===`);
// company_7 레코드 조회
const rows = await pool.query(
`SELECT screen_id, layer_id, company_code, layout_data
FROM screen_layouts_v2
WHERE company_code = $1
ORDER BY screen_id, layer_id`,
[COMPANY_CODE],
);
console.log(`대상 레코드 수: ${rows.rowCount}`);
if (!rows.rowCount) {
console.log("변경할 레코드가 없습니다.");
return;
}
const client = await pool.connect();
try {
await client.query("BEGIN");
let totalUpdated = 0;
let totalButtons = 0;
const targetRows = testMode ? [rows.rows[0]] : rows.rows;
for (const row of targetRows) {
const layoutData = row.layout_data;
if (!layoutData?.components || !Array.isArray(layoutData.components)) {
continue;
}
let buttonsInRow = 0;
for (const comp of layoutData.components) {
// 최상위 버튼 처리
if (updateButtonStyle(comp)) {
buttonsInRow++;
}
// 탭 위젯 내부 버튼 처리
if (isTabsWidget(comp)) {
const tabs = comp.overrides?.tabs || [];
for (const tab of tabs) {
const tabComps = tab.components || [];
for (const tabComp of tabComps) {
if (updateButtonStyle(tabComp)) {
buttonsInRow++;
}
}
}
}
}
if (buttonsInRow > 0) {
await client.query(
`UPDATE screen_layouts_v2
SET layout_data = $1, updated_at = NOW()
WHERE screen_id = $2 AND company_code = $3 AND layer_id = $4`,
[JSON.stringify(layoutData), row.screen_id, row.company_code, row.layer_id],
);
totalUpdated++;
totalButtons += buttonsInRow;
console.log(
` screen_id=${row.screen_id}, layer_id=${row.layer_id} → 버튼 ${buttonsInRow}개 변경`,
);
// 테스트 모드: 변경 전후 비교를 위해 첫 번째 버튼 출력
if (testMode) {
const sampleBtn = layoutData.components.find(isButtonComponent);
if (sampleBtn) {
console.log("\n--- 변경 후 샘플 버튼 ---");
console.log(JSON.stringify(sampleBtn, null, 2));
}
}
}
}
console.log(`\n--- 결과 ---`);
console.log(`변경된 레코드: ${totalUpdated}`);
console.log(`변경된 버튼: ${totalButtons}`);
if (testMode) {
await client.query("ROLLBACK");
console.log("\n[테스트 모드] ROLLBACK 완료. 실제 DB 변경 없음.");
} else {
await client.query("COMMIT");
console.log("\nCOMMIT 완료.");
}
} catch (err) {
await client.query("ROLLBACK");
console.error("\n에러 발생. ROLLBACK 완료.", err);
throw err;
} finally {
client.release();
}
}
// ── CLI 진입점 ──
async function main() {
const arg = process.argv[2];
if (!arg || !["--test", "--run", "--backup", "--restore"].includes(arg)) {
console.log("사용법:");
console.log(" --test : 1건 테스트 (ROLLBACK, DB 변경 없음)");
console.log(" --run : 전체 실행 (COMMIT)");
console.log(" --backup : 백업 테이블 생성");
console.log(" --restore : 백업에서 원복");
process.exit(1);
}
try {
if (arg === "--backup") {
await createBackup();
} else if (arg === "--restore") {
await restoreFromBackup();
} else if (arg === "--test") {
await createBackup();
await updateButtons(true);
} else if (arg === "--run") {
await createBackup();
await updateButtons(false);
}
} catch (err) {
console.error("스크립트 실행 실패:", err);
process.exit(1);
} finally {
await pool.end();
}
}
main();

View File

@@ -1,75 +0,0 @@
/**
* dashboards 테이블 구조 확인 스크립트
*/
const { Pool } = require('pg');
const databaseUrl = process.env.DATABASE_URL || 'postgresql://postgres:ph0909!!@39.117.244.52:11132/plm';
const pool = new Pool({
connectionString: databaseUrl,
});
async function checkDashboardStructure() {
const client = await pool.connect();
try {
console.log('🔍 dashboards 테이블 구조 확인 중...\n');
// 컬럼 정보 조회
const columns = await client.query(`
SELECT
column_name,
data_type,
is_nullable,
column_default
FROM information_schema.columns
WHERE table_name = 'dashboards'
ORDER BY ordinal_position
`);
console.log('📋 dashboards 테이블 컬럼:\n');
columns.rows.forEach((col, index) => {
console.log(`${index + 1}. ${col.column_name} (${col.data_type}) - Nullable: ${col.is_nullable}`);
});
// 샘플 데이터 조회
console.log('\n📊 샘플 데이터 (첫 1개):');
const sample = await client.query(`
SELECT * FROM dashboards LIMIT 1
`);
if (sample.rows.length > 0) {
console.log(JSON.stringify(sample.rows[0], null, 2));
} else {
console.log('❌ 데이터가 없습니다.');
}
// dashboard_elements 테이블도 확인
console.log('\n🔍 dashboard_elements 테이블 구조 확인 중...\n');
const elemColumns = await client.query(`
SELECT
column_name,
data_type,
is_nullable
FROM information_schema.columns
WHERE table_name = 'dashboard_elements'
ORDER BY ordinal_position
`);
console.log('📋 dashboard_elements 테이블 컬럼:\n');
elemColumns.rows.forEach((col, index) => {
console.log(`${index + 1}. ${col.column_name} (${col.data_type}) - Nullable: ${col.is_nullable}`);
});
} catch (error) {
console.error('❌ 오류 발생:', error.message);
} finally {
client.release();
await pool.end();
}
}
checkDashboardStructure();

View File

@@ -1,55 +0,0 @@
/**
* 데이터베이스 테이블 확인 스크립트
*/
const { Pool } = require('pg');
const databaseUrl = process.env.DATABASE_URL || 'postgresql://postgres:ph0909!!@39.117.244.52:11132/plm';
const pool = new Pool({
connectionString: databaseUrl,
});
async function checkTables() {
const client = await pool.connect();
try {
console.log('🔍 데이터베이스 테이블 확인 중...\n');
// 테이블 목록 조회
const result = await client.query(`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name
`);
console.log(`📊 총 ${result.rows.length}개의 테이블 발견:\n`);
result.rows.forEach((row, index) => {
console.log(`${index + 1}. ${row.table_name}`);
});
// dashboard 관련 테이블 검색
console.log('\n🔎 dashboard 관련 테이블:');
const dashboardTables = result.rows.filter(row =>
row.table_name.toLowerCase().includes('dashboard')
);
if (dashboardTables.length === 0) {
console.log('❌ dashboard 관련 테이블을 찾을 수 없습니다.');
} else {
dashboardTables.forEach(row => {
console.log(`${row.table_name}`);
});
}
} catch (error) {
console.error('❌ 오류 발생:', error.message);
} finally {
client.release();
await pool.end();
}
}
checkTables();

View File

@@ -1,74 +0,0 @@
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();
async function createComponentTable() {
try {
console.log("🔧 component_standards 테이블 생성 중...");
// 테이블 생성 SQL
await prisma.$executeRaw`
CREATE TABLE IF NOT EXISTS component_standards (
component_code VARCHAR(50) PRIMARY KEY,
component_name VARCHAR(100) NOT NULL,
component_name_eng VARCHAR(100),
description TEXT,
category VARCHAR(50) NOT NULL,
icon_name VARCHAR(50),
default_size JSON,
component_config JSON NOT NULL,
preview_image VARCHAR(255),
sort_order INTEGER DEFAULT 0,
is_active CHAR(1) DEFAULT 'Y',
is_public CHAR(1) DEFAULT 'Y',
company_code VARCHAR(50) NOT NULL,
created_date TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(50),
updated_date TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP,
updated_by VARCHAR(50)
)
`;
console.log("✅ component_standards 테이블 생성 완료");
// 인덱스 생성
await prisma.$executeRaw`
CREATE INDEX IF NOT EXISTS idx_component_standards_category
ON component_standards (category)
`;
await prisma.$executeRaw`
CREATE INDEX IF NOT EXISTS idx_component_standards_company
ON component_standards (company_code)
`;
console.log("✅ 인덱스 생성 완료");
// 테이블 코멘트 추가
await prisma.$executeRaw`
COMMENT ON TABLE component_standards IS 'UI 컴포넌트 표준 정보를 저장하는 테이블'
`;
console.log("✅ 테이블 코멘트 추가 완료");
} catch (error) {
console.error("❌ 테이블 생성 실패:", error);
throw error;
} finally {
await prisma.$disconnect();
}
}
// 실행
if (require.main === module) {
createComponentTable()
.then(() => {
console.log("🎉 테이블 생성 완료!");
process.exit(0);
})
.catch((error) => {
console.error("💥 테이블 생성 실패:", error);
process.exit(1);
});
}
module.exports = { createComponentTable };

View File

@@ -1,309 +0,0 @@
/**
* 레이아웃 표준 데이터 초기화 스크립트
* 기본 레이아웃들을 layout_standards 테이블에 삽입합니다.
*/
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();
// 기본 레이아웃 데이터
const PREDEFINED_LAYOUTS = [
{
layout_code: "GRID_2X2_001",
layout_name: "2x2 그리드",
layout_name_eng: "2x2 Grid",
description: "2행 2열의 균등한 그리드 레이아웃입니다.",
layout_type: "grid",
category: "basic",
icon_name: "grid",
default_size: { width: 800, height: 600 },
layout_config: {
grid: { rows: 2, columns: 2, gap: 16 },
},
zones_config: [
{
id: "zone1",
name: "상단 좌측",
position: { row: 0, column: 0 },
size: { width: "50%", height: "50%" },
},
{
id: "zone2",
name: "상단 우측",
position: { row: 0, column: 1 },
size: { width: "50%", height: "50%" },
},
{
id: "zone3",
name: "하단 좌측",
position: { row: 1, column: 0 },
size: { width: "50%", height: "50%" },
},
{
id: "zone4",
name: "하단 우측",
position: { row: 1, column: 1 },
size: { width: "50%", height: "50%" },
},
],
sort_order: 1,
is_active: "Y",
is_public: "Y",
company_code: "DEFAULT",
},
{
layout_code: "FORM_TWO_COLUMN_001",
layout_name: "2단 폼 레이아웃",
layout_name_eng: "Two Column Form",
description: "좌우 2단으로 구성된 폼 레이아웃입니다.",
layout_type: "grid",
category: "form",
icon_name: "columns",
default_size: { width: 800, height: 400 },
layout_config: {
grid: { rows: 1, columns: 2, gap: 24 },
},
zones_config: [
{
id: "left",
name: "좌측 입력 영역",
position: { row: 0, column: 0 },
size: { width: "50%", height: "100%" },
},
{
id: "right",
name: "우측 입력 영역",
position: { row: 0, column: 1 },
size: { width: "50%", height: "100%" },
},
],
sort_order: 2,
is_active: "Y",
is_public: "Y",
company_code: "DEFAULT",
},
{
layout_code: "FLEXBOX_ROW_001",
layout_name: "가로 플렉스박스",
layout_name_eng: "Horizontal Flexbox",
description: "가로 방향으로 배치되는 플렉스박스 레이아웃입니다.",
layout_type: "flexbox",
category: "basic",
icon_name: "flex",
default_size: { width: 800, height: 300 },
layout_config: {
flexbox: {
direction: "row",
justify: "flex-start",
align: "stretch",
wrap: "nowrap",
gap: 16,
},
},
zones_config: [
{
id: "left",
name: "좌측 영역",
position: {},
size: { width: "50%", height: "100%" },
},
{
id: "right",
name: "우측 영역",
position: {},
size: { width: "50%", height: "100%" },
},
],
sort_order: 3,
is_active: "Y",
is_public: "Y",
company_code: "DEFAULT",
},
{
layout_code: "SPLIT_HORIZONTAL_001",
layout_name: "수평 분할",
layout_name_eng: "Horizontal Split",
description: "크기 조절이 가능한 수평 분할 레이아웃입니다.",
layout_type: "split",
category: "basic",
icon_name: "separator-horizontal",
default_size: { width: 800, height: 400 },
layout_config: {
split: {
direction: "horizontal",
ratio: [50, 50],
minSize: [200, 200],
resizable: true,
splitterSize: 4,
},
},
zones_config: [
{
id: "left",
name: "좌측 패널",
position: {},
size: { width: "50%", height: "100%" },
isResizable: true,
},
{
id: "right",
name: "우측 패널",
position: {},
size: { width: "50%", height: "100%" },
isResizable: true,
},
],
sort_order: 4,
is_active: "Y",
is_public: "Y",
company_code: "DEFAULT",
},
{
layout_code: "TABS_HORIZONTAL_001",
layout_name: "수평 탭",
layout_name_eng: "Horizontal Tabs",
description: "상단에 탭이 있는 탭 레이아웃입니다.",
layout_type: "tabs",
category: "navigation",
icon_name: "tabs",
default_size: { width: 800, height: 500 },
layout_config: {
tabs: {
position: "top",
variant: "default",
size: "md",
defaultTab: "tab1",
closable: false,
},
},
zones_config: [
{
id: "tab1",
name: "첫 번째 탭",
position: {},
size: { width: "100%", height: "100%" },
},
{
id: "tab2",
name: "두 번째 탭",
position: {},
size: { width: "100%", height: "100%" },
},
{
id: "tab3",
name: "세 번째 탭",
position: {},
size: { width: "100%", height: "100%" },
},
],
sort_order: 5,
is_active: "Y",
is_public: "Y",
company_code: "DEFAULT",
},
{
layout_code: "TABLE_WITH_FILTERS_001",
layout_name: "필터가 있는 테이블",
layout_name_eng: "Table with Filters",
description: "상단에 필터가 있고 하단에 테이블이 있는 레이아웃입니다.",
layout_type: "flexbox",
category: "table",
icon_name: "table",
default_size: { width: 1000, height: 600 },
layout_config: {
flexbox: {
direction: "column",
justify: "flex-start",
align: "stretch",
wrap: "nowrap",
gap: 16,
},
},
zones_config: [
{
id: "filters",
name: "검색 필터",
position: {},
size: { width: "100%", height: "auto" },
},
{
id: "table",
name: "데이터 테이블",
position: {},
size: { width: "100%", height: "1fr" },
},
],
sort_order: 6,
is_active: "Y",
is_public: "Y",
company_code: "DEFAULT",
},
];
async function initializeLayoutStandards() {
try {
console.log("🏗️ 레이아웃 표준 데이터 초기화 시작...");
// 기존 데이터 확인
const existingLayouts = await prisma.layout_standards.count();
if (existingLayouts > 0) {
console.log(`⚠️ 이미 ${existingLayouts}개의 레이아웃이 존재합니다.`);
console.log(
"기존 데이터를 삭제하고 새로 생성하시겠습니까? (기본값: 건너뛰기)"
);
// 기존 데이터가 있으면 건너뛰기 (안전을 위해)
console.log("💡 기존 데이터를 유지하고 건너뜁니다.");
return;
}
// 데이터 삽입
let insertedCount = 0;
for (const layoutData of PREDEFINED_LAYOUTS) {
try {
await prisma.layout_standards.create({
data: {
...layoutData,
created_date: new Date(),
updated_date: new Date(),
created_by: "SYSTEM",
updated_by: "SYSTEM",
},
});
console.log(`${layoutData.layout_name} 생성 완료`);
insertedCount++;
} catch (error) {
console.error(`${layoutData.layout_name} 생성 실패:`, error.message);
}
}
console.log(
`🎉 레이아웃 표준 데이터 초기화 완료! (${insertedCount}/${PREDEFINED_LAYOUTS.length})`
);
} catch (error) {
console.error("❌ 레이아웃 표준 데이터 초기화 실패:", error);
throw error;
}
}
// 스크립트 실행
if (require.main === module) {
initializeLayoutStandards()
.then(() => {
console.log("✨ 스크립트 실행 완료");
process.exit(0);
})
.catch((error) => {
console.error("💥 스크립트 실행 실패:", error);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
}
module.exports = { initializeLayoutStandards };

View File

@@ -1,200 +0,0 @@
/**
* 🔥 버튼 제어관리 성능 최적화 인덱스 설치 스크립트
*
* 사용법:
* node scripts/install-dataflow-indexes.js
*/
const { PrismaClient } = require("@prisma/client");
const fs = require("fs");
const path = require("path");
const prisma = new PrismaClient();
async function installDataflowIndexes() {
try {
console.log("🔥 Starting Button Dataflow Performance Optimization...\n");
// SQL 파일 읽기
const sqlFilePath = path.join(
__dirname,
"../database/migrations/add_button_dataflow_indexes.sql"
);
const sqlContent = fs.readFileSync(sqlFilePath, "utf8");
console.log("📖 Reading SQL migration file...");
console.log(`📁 File: ${sqlFilePath}\n`);
// 데이터베이스 연결 확인
console.log("🔍 Checking database connection...");
await prisma.$queryRaw`SELECT 1`;
console.log("✅ Database connection OK\n");
// 기존 인덱스 상태 확인
console.log("🔍 Checking existing indexes...");
const existingIndexes = await prisma.$queryRaw`
SELECT indexname, tablename
FROM pg_indexes
WHERE tablename = 'dataflow_diagrams'
AND indexname LIKE 'idx_dataflow%'
ORDER BY indexname;
`;
if (existingIndexes.length > 0) {
console.log("📋 Existing dataflow indexes:");
existingIndexes.forEach((idx) => {
console.log(` - ${idx.indexname}`);
});
} else {
console.log("📋 No existing dataflow indexes found");
}
console.log("");
// 테이블 상태 확인
console.log("🔍 Checking dataflow_diagrams table stats...");
const tableStats = await prisma.$queryRaw`
SELECT
COUNT(*) as total_rows,
COUNT(*) FILTER (WHERE control IS NOT NULL) as with_control,
COUNT(*) FILTER (WHERE plan IS NOT NULL) as with_plan,
COUNT(*) FILTER (WHERE category IS NOT NULL) as with_category,
COUNT(DISTINCT company_code) as companies
FROM dataflow_diagrams;
`;
if (tableStats.length > 0) {
const stats = tableStats[0];
console.log(`📊 Table Statistics:`);
console.log(` - Total rows: ${stats.total_rows}`);
console.log(` - With control: ${stats.with_control}`);
console.log(` - With plan: ${stats.with_plan}`);
console.log(` - With category: ${stats.with_category}`);
console.log(` - Companies: ${stats.companies}`);
}
console.log("");
// SQL 실행
console.log("🚀 Installing performance indexes...");
console.log("⏳ This may take a few minutes for large datasets...\n");
const startTime = Date.now();
// SQL을 문장별로 나누어 실행 (PostgreSQL 함수 때문에)
const sqlStatements = sqlContent
.split(/;\s*(?=\n|$)/)
.filter(
(stmt) =>
stmt.trim().length > 0 &&
!stmt.trim().startsWith("--") &&
!stmt.trim().startsWith("/*")
);
for (let i = 0; i < sqlStatements.length; i++) {
const statement = sqlStatements[i].trim();
if (statement.length === 0) continue;
try {
// DO 블록이나 복합 문장 처리
if (
statement.includes("DO $$") ||
statement.includes("CREATE OR REPLACE VIEW")
) {
console.log(
`⚡ Executing statement ${i + 1}/${sqlStatements.length}...`
);
await prisma.$executeRawUnsafe(statement + ";");
} else if (statement.startsWith("CREATE INDEX")) {
const indexName =
statement.match(/CREATE INDEX[^"]*"?([^"\s]+)"?/)?.[1] || "unknown";
console.log(`🔧 Creating index: ${indexName}...`);
await prisma.$executeRawUnsafe(statement + ";");
} else if (statement.startsWith("ANALYZE")) {
console.log(`📊 Analyzing table statistics...`);
await prisma.$executeRawUnsafe(statement + ";");
} else {
await prisma.$executeRawUnsafe(statement + ";");
}
} catch (error) {
// 이미 존재하는 인덱스 에러는 무시
if (error.message.includes("already exists")) {
console.log(`⚠️ Index already exists, skipping...`);
} else {
console.error(`❌ Error executing statement: ${error.message}`);
console.error(`📝 Statement: ${statement.substring(0, 100)}...`);
}
}
}
const endTime = Date.now();
const executionTime = (endTime - startTime) / 1000;
console.log(
`\n✅ Index installation completed in ${executionTime.toFixed(2)} seconds!`
);
// 설치된 인덱스 확인
console.log("\n🔍 Verifying installed indexes...");
const newIndexes = await prisma.$queryRaw`
SELECT
indexname,
pg_size_pretty(pg_relation_size(indexrelid)) as size
FROM pg_stat_user_indexes
WHERE tablename = 'dataflow_diagrams'
AND indexname LIKE 'idx_dataflow%'
ORDER BY indexname;
`;
if (newIndexes.length > 0) {
console.log("📋 Installed indexes:");
newIndexes.forEach((idx) => {
console.log(`${idx.indexname} (${idx.size})`);
});
}
// 성능 통계 조회
console.log("\n📊 Performance statistics:");
try {
const perfStats =
await prisma.$queryRaw`SELECT * FROM dataflow_performance_stats;`;
if (perfStats.length > 0) {
const stats = perfStats[0];
console.log(` - Table size: ${stats.table_size}`);
console.log(` - Total diagrams: ${stats.total_rows}`);
console.log(` - With control: ${stats.with_control}`);
console.log(` - Companies: ${stats.companies}`);
}
} catch (error) {
console.log(" ⚠️ Performance view not available yet");
}
console.log("\n🎯 Performance Optimization Complete!");
console.log("Expected improvements:");
console.log(" - Button dataflow lookup: 500ms+ → 10-50ms ⚡");
console.log(" - Category filtering: 200ms+ → 5-20ms ⚡");
console.log(" - Company queries: 100ms+ → 5-15ms ⚡");
console.log("\n💡 Monitor performance with:");
console.log(" SELECT * FROM dataflow_performance_stats;");
console.log(" SELECT * FROM dataflow_index_efficiency;");
} catch (error) {
console.error("\n❌ Error installing dataflow indexes:", error);
process.exit(1);
} finally {
await prisma.$disconnect();
}
}
// 실행
if (require.main === module) {
installDataflowIndexes()
.then(() => {
console.log("\n🎉 Installation completed successfully!");
process.exit(0);
})
.catch((error) => {
console.error("\n💥 Installation failed:", error);
process.exit(1);
});
}
module.exports = { installDataflowIndexes };

View File

@@ -1,46 +0,0 @@
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();
async function getComponents() {
try {
const components = await prisma.component_standards.findMany({
where: { is_active: "Y" },
select: {
component_code: true,
component_name: true,
category: true,
component_config: true,
},
orderBy: [{ category: "asc" }, { sort_order: "asc" }],
});
console.log("📋 데이터베이스 컴포넌트 목록:");
console.log("=".repeat(60));
const grouped = components.reduce((acc, comp) => {
if (!acc[comp.category]) {
acc[comp.category] = [];
}
acc[comp.category].push(comp);
return acc;
}, {});
Object.entries(grouped).forEach(([category, comps]) => {
console.log(`\n🏷️ ${category.toUpperCase()} 카테고리:`);
comps.forEach((comp) => {
const type = comp.component_config?.type || "unknown";
console.log(
` - ${comp.component_code}: ${comp.component_name} (type: ${type})`
);
});
});
console.log(`\n${components.length}개 컴포넌트 발견`);
} catch (error) {
console.error("Error:", error);
} finally {
await prisma.$disconnect();
}
}
getComponents();

View File

@@ -1,168 +0,0 @@
import { query } from "../src/database/db";
import { logger } from "../src/utils/logger";
/**
* input_type을 web_type으로 마이그레이션하는 스크립트
*
* 목적:
* - column_labels 테이블의 input_type 값을 읽어서
* - 해당하는 기본 web_type 값으로 변환
* - web_type이 null인 경우에만 업데이트
*/
// input_type → 기본 web_type 매핑
const INPUT_TYPE_TO_WEB_TYPE: Record<string, string> = {
text: "text", // 일반 텍스트
number: "number", // 정수
date: "date", // 날짜
code: "code", // 코드 선택박스
entity: "entity", // 엔티티 참조
select: "select", // 선택박스
checkbox: "checkbox", // 체크박스
radio: "radio", // 라디오버튼
direct: "text", // direct는 text로 매핑
};
async function migrateInputTypeToWebType() {
try {
logger.info("=".repeat(60));
logger.info("input_type → web_type 마이그레이션 시작");
logger.info("=".repeat(60));
// 1. 현재 상태 확인
const stats = await query<{
total: string;
has_input_type: string;
has_web_type: string;
needs_migration: string;
}>(
`SELECT
COUNT(*) as total,
COUNT(input_type) FILTER (WHERE input_type IS NOT NULL) as has_input_type,
COUNT(web_type) FILTER (WHERE web_type IS NOT NULL) as has_web_type,
COUNT(*) FILTER (WHERE input_type IS NOT NULL AND web_type IS NULL) as needs_migration
FROM column_labels`
);
const stat = stats[0];
logger.info("\n📊 현재 상태:");
logger.info(` - 전체 컬럼: ${stat.total}`);
logger.info(` - input_type 있음: ${stat.has_input_type}`);
logger.info(` - web_type 있음: ${stat.has_web_type}`);
logger.info(` - 마이그레이션 필요: ${stat.needs_migration}`);
if (parseInt(stat.needs_migration) === 0) {
logger.info("\n✅ 마이그레이션이 필요한 데이터가 없습니다.");
return;
}
// 2. input_type별 분포 확인
const distribution = await query<{
input_type: string;
count: string;
}>(
`SELECT
input_type,
COUNT(*) as count
FROM column_labels
WHERE input_type IS NOT NULL AND web_type IS NULL
GROUP BY input_type
ORDER BY input_type`
);
logger.info("\n📋 input_type별 분포:");
distribution.forEach((item) => {
const webType =
INPUT_TYPE_TO_WEB_TYPE[item.input_type] || item.input_type;
logger.info(` - ${item.input_type}${webType}: ${item.count}`);
});
// 3. 마이그레이션 실행
logger.info("\n🔄 마이그레이션 실행 중...");
let totalUpdated = 0;
for (const [inputType, webType] of Object.entries(INPUT_TYPE_TO_WEB_TYPE)) {
const result = await query(
`UPDATE column_labels
SET
web_type = $1,
updated_date = NOW()
WHERE input_type = $2
AND web_type IS NULL
RETURNING id, table_name, column_name`,
[webType, inputType]
);
if (result.length > 0) {
logger.info(
`${inputType}${webType}: ${result.length}개 업데이트`
);
totalUpdated += result.length;
// 처음 5개만 출력
result.slice(0, 5).forEach((row: any) => {
logger.info(` - ${row.table_name}.${row.column_name}`);
});
if (result.length > 5) {
logger.info(` ... 외 ${result.length - 5}`);
}
}
}
// 4. 결과 확인
const afterStats = await query<{
total: string;
has_web_type: string;
}>(
`SELECT
COUNT(*) as total,
COUNT(web_type) FILTER (WHERE web_type IS NOT NULL) as has_web_type
FROM column_labels`
);
const afterStat = afterStats[0];
logger.info("\n" + "=".repeat(60));
logger.info("✅ 마이그레이션 완료!");
logger.info("=".repeat(60));
logger.info(`📊 최종 통계:`);
logger.info(` - 전체 컬럼: ${afterStat.total}`);
logger.info(` - web_type 설정됨: ${afterStat.has_web_type}`);
logger.info(` - 업데이트된 컬럼: ${totalUpdated}`);
logger.info("=".repeat(60));
// 5. 샘플 데이터 출력
logger.info("\n📝 샘플 데이터 (check_report_mng 테이블):");
const samples = await query<{
column_name: string;
input_type: string;
web_type: string;
detail_settings: string;
}>(
`SELECT
column_name,
input_type,
web_type,
detail_settings
FROM column_labels
WHERE table_name = 'check_report_mng'
ORDER BY column_name
LIMIT 10`
);
samples.forEach((sample) => {
logger.info(
` ${sample.column_name}: ${sample.input_type}${sample.web_type}`
);
});
process.exit(0);
} catch (error) {
logger.error("❌ 마이그레이션 실패:", error);
process.exit(1);
}
}
// 스크립트 실행
migrateInputTypeToWebType();

View File

@@ -1,35 +0,0 @@
/**
* system_notice 테이블 생성 마이그레이션 실행
*/
const { Pool } = require('pg');
const fs = require('fs');
const path = require('path');
const pool = new Pool({
connectionString: process.env.DATABASE_URL || 'postgresql://postgres:ph0909!!@39.117.244.52:11132/plm',
ssl: false,
});
async function run() {
const client = await pool.connect();
try {
const sqlPath = path.join(__dirname, '../../db/migrations/1050_create_system_notice.sql');
const sql = fs.readFileSync(sqlPath, 'utf8');
await client.query(sql);
console.log('OK: system_notice 테이블 생성 완료');
// 검증
const result = await client.query(
"SELECT column_name FROM information_schema.columns WHERE table_name='system_notice' ORDER BY ordinal_position"
);
console.log('컬럼:', result.rows.map(r => r.column_name).join(', '));
} catch (e) {
console.error('ERROR:', e.message);
process.exit(1);
} finally {
client.release();
await pool.end();
}
}
run();

View File

@@ -1,53 +0,0 @@
/**
* SQL 마이그레이션 실행 스크립트
* 사용법: node scripts/run-migration.js
*/
const fs = require('fs');
const path = require('path');
const { Pool } = require('pg');
// DATABASE_URL에서 연결 정보 파싱
const databaseUrl = process.env.DATABASE_URL || 'postgresql://postgres:ph0909!!@39.117.244.52:11132/plm';
// 데이터베이스 연결 설정
const pool = new Pool({
connectionString: databaseUrl,
});
async function runMigration() {
const client = await pool.connect();
try {
console.log('🔄 마이그레이션 시작...\n');
// SQL 파일 읽기 (Docker 컨테이너 내부 경로)
const sqlPath = '/tmp/migration.sql';
const sql = fs.readFileSync(sqlPath, 'utf8');
console.log('📄 SQL 파일 로드 완료');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
// SQL 실행
await client.query(sql);
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('✅ 마이그레이션 성공적으로 완료되었습니다!');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
} catch (error) {
console.error('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.error('❌ 마이그레이션 실패:');
console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.error(error);
console.error('\n💡 롤백이 필요한 경우 롤백 스크립트를 실행하세요.');
process.exit(1);
} finally {
client.release();
await pool.end();
}
}
// 실행
runMigration();

View File

@@ -1,38 +0,0 @@
/**
* system_notice 마이그레이션 실행 스크립트
* 사용법: node scripts/run-notice-migration.js
*/
const fs = require('fs');
const path = require('path');
const { Pool } = require('pg');
const pool = new Pool({
connectionString: process.env.DATABASE_URL || 'postgresql://postgres:ph0909!!@39.117.244.52:11132/plm',
ssl: false,
});
async function run() {
const client = await pool.connect();
try {
const sqlPath = path.join(__dirname, '../../db/migrations/1050_create_system_notice.sql');
const sql = fs.readFileSync(sqlPath, 'utf8');
console.log('마이그레이션 실행 중...');
await client.query(sql);
console.log('마이그레이션 완료');
// 컬럼 확인
const check = await client.query(
"SELECT column_name FROM information_schema.columns WHERE table_name='system_notice' ORDER BY ordinal_position"
);
console.log('테이블 컬럼:', check.rows.map(r => r.column_name).join(', '));
} catch (e) {
console.error('오류:', e.message);
process.exit(1);
} finally {
client.release();
await pool.end();
}
}
run();

View File

@@ -1,294 +0,0 @@
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();
// 기본 템플릿 데이터 정의
const defaultTemplates = [
{
template_code: "advanced-data-table-v2",
template_name: "고급 데이터 테이블 v2",
template_name_eng: "Advanced Data Table v2",
description:
"검색, 필터링, 페이징, CRUD 기능이 포함된 완전한 데이터 테이블 컴포넌트",
category: "table",
icon_name: "table",
default_size: {
width: 1000,
height: 680,
},
layout_config: {
components: [
{
type: "datatable",
label: "고급 데이터 테이블",
position: { x: 0, y: 0 },
size: { width: 1000, height: 680 },
style: {
border: "1px solid #e5e7eb",
borderRadius: "8px",
backgroundColor: "#ffffff",
padding: "0",
},
columns: [
{
id: "id",
label: "ID",
type: "number",
visible: true,
sortable: true,
filterable: false,
width: 80,
},
{
id: "name",
label: "이름",
type: "text",
visible: true,
sortable: true,
filterable: true,
width: 150,
},
{
id: "email",
label: "이메일",
type: "email",
visible: true,
sortable: true,
filterable: true,
width: 200,
},
{
id: "status",
label: "상태",
type: "select",
visible: true,
sortable: true,
filterable: true,
width: 100,
},
{
id: "created_date",
label: "생성일",
type: "date",
visible: true,
sortable: true,
filterable: true,
width: 120,
},
],
filters: [
{
id: "status",
label: "상태",
type: "select",
options: [
{ label: "전체", value: "" },
{ label: "활성", value: "active" },
{ label: "비활성", value: "inactive" },
],
},
{ id: "name", label: "이름", type: "text" },
{ id: "email", label: "이메일", type: "text" },
],
pagination: {
enabled: true,
pageSize: 10,
pageSizeOptions: [5, 10, 20, 50, 100],
showPageSizeSelector: true,
showPageInfo: true,
showFirstLast: true,
},
actions: {
showSearchButton: true,
searchButtonText: "검색",
enableExport: true,
enableRefresh: true,
enableAdd: true,
enableEdit: true,
enableDelete: true,
addButtonText: "추가",
editButtonText: "수정",
deleteButtonText: "삭제",
},
addModalConfig: {
title: "새 데이터 추가",
description: "테이블에 새로운 데이터를 추가합니다.",
width: "lg",
layout: "two-column",
gridColumns: 2,
fieldOrder: ["name", "email", "status"],
requiredFields: ["name", "email"],
hiddenFields: ["id", "created_date"],
advancedFieldConfigs: {
status: {
type: "select",
options: [
{ label: "활성", value: "active" },
{ label: "비활성", value: "inactive" },
],
},
},
submitButtonText: "추가",
cancelButtonText: "취소",
},
},
],
},
sort_order: 1,
is_active: "Y",
is_public: "Y",
company_code: "*",
created_by: "system",
updated_by: "system",
},
{
template_code: "universal-button",
template_name: "범용 버튼",
template_name_eng: "Universal Button",
description:
"다양한 기능을 설정할 수 있는 범용 버튼. 상세설정에서 기능을 선택하세요.",
category: "button",
icon_name: "mouse-pointer",
default_size: {
width: 80,
height: 36,
},
layout_config: {
components: [
{
type: "widget",
widgetType: "button",
label: "버튼",
position: { x: 0, y: 0 },
size: { width: 80, height: 36 },
style: {
backgroundColor: "#3b82f6",
color: "#ffffff",
border: "none",
borderRadius: "6px",
fontSize: "14px",
fontWeight: "500",
},
},
],
},
sort_order: 2,
is_active: "Y",
is_public: "Y",
company_code: "*",
created_by: "system",
updated_by: "system",
},
{
template_code: "file-upload",
template_name: "파일 첨부",
template_name_eng: "File Upload",
description: "드래그앤드롭 파일 업로드 영역",
category: "file",
icon_name: "upload",
default_size: {
width: 300,
height: 120,
},
layout_config: {
components: [
{
type: "widget",
widgetType: "file",
label: "파일 첨부",
position: { x: 0, y: 0 },
size: { width: 300, height: 120 },
style: {
border: "2px dashed #d1d5db",
borderRadius: "8px",
backgroundColor: "#f9fafb",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "14px",
color: "#6b7280",
},
},
],
},
sort_order: 3,
is_active: "Y",
is_public: "Y",
company_code: "*",
created_by: "system",
updated_by: "system",
},
{
template_code: "form-container",
template_name: "폼 컨테이너",
template_name_eng: "Form Container",
description: "입력 폼을 위한 기본 컨테이너 레이아웃",
category: "form",
icon_name: "form",
default_size: {
width: 400,
height: 300,
},
layout_config: {
components: [
{
type: "container",
label: "폼 컨테이너",
position: { x: 0, y: 0 },
size: { width: 400, height: 300 },
style: {
border: "1px solid #e5e7eb",
borderRadius: "8px",
backgroundColor: "#ffffff",
padding: "16px",
},
},
],
},
sort_order: 4,
is_active: "Y",
is_public: "Y",
company_code: "*",
created_by: "system",
updated_by: "system",
},
];
async function seedTemplates() {
console.log("🌱 템플릿 시드 데이터 삽입 시작...");
try {
// 기존 템플릿이 있는지 확인하고 없는 경우에만 삽입
for (const template of defaultTemplates) {
const existing = await prisma.template_standards.findUnique({
where: { template_code: template.template_code },
});
if (!existing) {
await prisma.template_standards.create({
data: template,
});
console.log(`✅ 템플릿 '${template.template_name}' 생성됨`);
} else {
console.log(`⏭️ 템플릿 '${template.template_name}' 이미 존재함`);
}
}
console.log("🎉 템플릿 시드 데이터 삽입 완료!");
} catch (error) {
console.error("❌ 템플릿 시드 데이터 삽입 실패:", error);
throw error;
} finally {
await prisma.$disconnect();
}
}
// 스크립트가 직접 실행될 때만 시드 함수 실행
if (require.main === module) {
seedTemplates().catch((error) => {
console.error(error);
process.exit(1);
});
}
module.exports = { seedTemplates };

View File

@@ -1,411 +0,0 @@
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();
// 실제 UI 구성에 필요한 컴포넌트들
const uiComponents = [
// === 액션 컴포넌트 ===
{
component_code: "button-primary",
component_name: "기본 버튼",
component_name_eng: "Primary Button",
description: "일반적인 액션을 위한 기본 버튼 컴포넌트",
category: "action",
icon_name: "MousePointer",
default_size: { width: 100, height: 36 },
component_config: {
type: "button",
variant: "primary",
text: "버튼",
action: "custom",
style: {
backgroundColor: "#3b82f6",
color: "#ffffff",
borderRadius: "6px",
fontSize: "14px",
fontWeight: "500",
},
},
sort_order: 10,
},
{
component_code: "button-secondary",
component_name: "보조 버튼",
component_name_eng: "Secondary Button",
description: "보조 액션을 위한 버튼 컴포넌트",
category: "action",
icon_name: "MousePointer",
default_size: { width: 100, height: 36 },
component_config: {
type: "button",
variant: "secondary",
text: "취소",
action: "cancel",
style: {
backgroundColor: "#f1f5f9",
color: "#475569",
borderRadius: "6px",
fontSize: "14px",
},
},
sort_order: 11,
},
// === 레이아웃 컴포넌트 ===
{
component_code: "card-basic",
component_name: "기본 카드",
component_name_eng: "Basic Card",
description: "정보를 그룹화하는 기본 카드 컴포넌트",
category: "layout",
icon_name: "Square",
default_size: { width: 400, height: 300 },
component_config: {
type: "card",
title: "카드 제목",
showHeader: true,
showFooter: false,
style: {
backgroundColor: "#ffffff",
border: "1px solid #e5e7eb",
borderRadius: "8px",
padding: "16px",
boxShadow: "0 1px 3px rgba(0, 0, 0, 0.1)",
},
},
sort_order: 20,
},
{
component_code: "dashboard-grid",
component_name: "대시보드 그리드",
component_name_eng: "Dashboard Grid",
description: "대시보드를 위한 그리드 레이아웃 컴포넌트",
category: "layout",
icon_name: "LayoutGrid",
default_size: { width: 800, height: 600 },
component_config: {
type: "dashboard",
columns: 3,
gap: 16,
items: [],
style: {
backgroundColor: "#f8fafc",
padding: "20px",
borderRadius: "8px",
},
},
sort_order: 21,
},
{
component_code: "panel-collapsible",
component_name: "접을 수 있는 패널",
component_name_eng: "Collapsible Panel",
description: "접고 펼칠 수 있는 패널 컴포넌트",
category: "layout",
icon_name: "ChevronDown",
default_size: { width: 500, height: 200 },
component_config: {
type: "panel",
title: "패널 제목",
collapsible: true,
defaultExpanded: true,
style: {
backgroundColor: "#ffffff",
border: "1px solid #e5e7eb",
borderRadius: "8px",
},
},
sort_order: 22,
},
// === 데이터 표시 컴포넌트 ===
{
component_code: "stats-card",
component_name: "통계 카드",
component_name_eng: "Statistics Card",
description: "수치와 통계를 표시하는 카드 컴포넌트",
category: "data",
icon_name: "BarChart3",
default_size: { width: 250, height: 120 },
component_config: {
type: "stats",
title: "총 판매량",
value: "1,234",
unit: "개",
trend: "up",
percentage: "+12.5%",
style: {
backgroundColor: "#ffffff",
border: "1px solid #e5e7eb",
borderRadius: "8px",
padding: "20px",
},
},
sort_order: 30,
},
{
component_code: "progress-bar",
component_name: "진행률 표시",
component_name_eng: "Progress Bar",
description: "작업 진행률을 표시하는 컴포넌트",
category: "data",
icon_name: "BarChart2",
default_size: { width: 300, height: 60 },
component_config: {
type: "progress",
label: "진행률",
value: 65,
max: 100,
showPercentage: true,
style: {
backgroundColor: "#f1f5f9",
borderRadius: "4px",
height: "8px",
},
},
sort_order: 31,
},
{
component_code: "chart-basic",
component_name: "기본 차트",
component_name_eng: "Basic Chart",
description: "데이터를 시각화하는 기본 차트 컴포넌트",
category: "data",
icon_name: "TrendingUp",
default_size: { width: 500, height: 300 },
component_config: {
type: "chart",
chartType: "line",
title: "차트 제목",
data: [],
options: {
responsive: true,
plugins: {
legend: { position: "top" },
},
},
},
sort_order: 32,
},
// === 네비게이션 컴포넌트 ===
{
component_code: "breadcrumb",
component_name: "브레드크럼",
component_name_eng: "Breadcrumb",
description: "현재 위치를 표시하는 네비게이션 컴포넌트",
category: "navigation",
icon_name: "ChevronRight",
default_size: { width: 400, height: 32 },
component_config: {
type: "breadcrumb",
items: [
{ label: "홈", href: "/" },
{ label: "관리자", href: "/admin" },
{ label: "현재 페이지" },
],
separator: ">",
},
sort_order: 40,
},
{
component_code: "tabs-horizontal",
component_name: "가로 탭",
component_name_eng: "Horizontal Tabs",
description: "컨텐츠를 탭으로 구분하는 네비게이션 컴포넌트",
category: "navigation",
icon_name: "Tabs",
default_size: { width: 500, height: 300 },
component_config: {
type: "tabs",
orientation: "horizontal",
tabs: [
{ id: "tab1", label: "탭 1", content: "첫 번째 탭 내용" },
{ id: "tab2", label: "탭 2", content: "두 번째 탭 내용" },
],
defaultTab: "tab1",
},
sort_order: 41,
},
{
component_code: "pagination",
component_name: "페이지네이션",
component_name_eng: "Pagination",
description: "페이지를 나눠서 표시하는 네비게이션 컴포넌트",
category: "navigation",
icon_name: "ChevronLeft",
default_size: { width: 300, height: 40 },
component_config: {
type: "pagination",
currentPage: 1,
totalPages: 10,
showFirst: true,
showLast: true,
showPrevNext: true,
},
sort_order: 42,
},
// === 피드백 컴포넌트 ===
{
component_code: "alert-info",
component_name: "정보 알림",
component_name_eng: "Info Alert",
description: "정보를 사용자에게 알리는 컴포넌트",
category: "feedback",
icon_name: "Info",
default_size: { width: 400, height: 60 },
component_config: {
type: "alert",
variant: "info",
title: "알림",
message: "중요한 정보를 확인해주세요.",
dismissible: true,
icon: true,
},
sort_order: 50,
},
{
component_code: "badge-status",
component_name: "상태 뱃지",
component_name_eng: "Status Badge",
description: "상태나 카테고리를 표시하는 뱃지 컴포넌트",
category: "feedback",
icon_name: "Tag",
default_size: { width: 80, height: 24 },
component_config: {
type: "badge",
text: "활성",
variant: "success",
size: "sm",
style: {
backgroundColor: "#10b981",
color: "#ffffff",
borderRadius: "12px",
fontSize: "12px",
},
},
sort_order: 51,
},
{
component_code: "loading-spinner",
component_name: "로딩 스피너",
component_name_eng: "Loading Spinner",
description: "로딩 상태를 표시하는 스피너 컴포넌트",
category: "feedback",
icon_name: "RefreshCw",
default_size: { width: 100, height: 100 },
component_config: {
type: "loading",
variant: "spinner",
size: "md",
message: "로딩 중...",
overlay: false,
},
sort_order: 52,
},
// === 입력 컴포넌트 ===
{
component_code: "search-box",
component_name: "검색 박스",
component_name_eng: "Search Box",
description: "검색 기능이 있는 입력 컴포넌트",
category: "input",
icon_name: "Search",
default_size: { width: 300, height: 40 },
component_config: {
type: "search",
placeholder: "검색어를 입력하세요...",
showButton: true,
debounce: 500,
style: {
borderRadius: "20px",
border: "1px solid #d1d5db",
},
},
sort_order: 60,
},
{
component_code: "filter-dropdown",
component_name: "필터 드롭다운",
component_name_eng: "Filter Dropdown",
description: "데이터 필터링을 위한 드롭다운 컴포넌트",
category: "input",
icon_name: "Filter",
default_size: { width: 200, height: 40 },
component_config: {
type: "filter",
label: "필터",
options: [
{ value: "all", label: "전체" },
{ value: "active", label: "활성" },
{ value: "inactive", label: "비활성" },
],
defaultValue: "all",
multiple: false,
},
sort_order: 61,
},
];
async function seedUIComponents() {
try {
console.log("🚀 UI 컴포넌트 시딩 시작...");
// 기존 데이터 삭제
console.log("📝 기존 컴포넌트 데이터 삭제 중...");
await prisma.$executeRaw`DELETE FROM component_standards`;
// 새 컴포넌트 데이터 삽입
console.log("📦 새로운 UI 컴포넌트 삽입 중...");
for (const component of uiComponents) {
await prisma.component_standards.create({
data: {
...component,
company_code: "DEFAULT",
created_by: "system",
updated_by: "system",
},
});
console.log(`${component.component_name} 컴포넌트 생성됨`);
}
console.log(
`\n🎉 총 ${uiComponents.length}개의 UI 컴포넌트가 성공적으로 생성되었습니다!`
);
// 카테고리별 통계
const categoryCounts = {};
uiComponents.forEach((component) => {
categoryCounts[component.category] =
(categoryCounts[component.category] || 0) + 1;
});
console.log("\n📊 카테고리별 컴포넌트 수:");
Object.entries(categoryCounts).forEach(([category, count]) => {
console.log(` ${category}: ${count}`);
});
} catch (error) {
console.error("❌ UI 컴포넌트 시딩 실패:", error);
throw error;
} finally {
await prisma.$disconnect();
}
}
// 실행
if (require.main === module) {
seedUIComponents()
.then(() => {
console.log("✨ UI 컴포넌트 시딩 완료!");
process.exit(0);
})
.catch((error) => {
console.error("💥 시딩 실패:", error);
process.exit(1);
});
}
module.exports = { seedUIComponents, uiComponents };

View File

@@ -1,209 +0,0 @@
/**
* 디지털 트윈 외부 DB (DO_DY) 연결 및 쿼리 테스트 스크립트
* READ-ONLY: SELECT 쿼리만 실행
*/
import { Pool } from "pg";
import mysql from "mysql2/promise";
import { CredentialEncryption } from "../src/utils/credentialEncryption";
async function testDigitalTwinDb() {
// 내부 DB 연결 (연결 정보 저장용)
const internalPool = new Pool({
host: process.env.DB_HOST || "localhost",
port: parseInt(process.env.DB_PORT || "5432"),
database: process.env.DB_NAME || "plm",
user: process.env.DB_USER || "postgres",
password: process.env.DB_PASSWORD || "ph0909!!",
});
const encryptionKey =
process.env.ENCRYPTION_SECRET_KEY || "default-secret-key-for-development";
const encryption = new CredentialEncryption(encryptionKey);
try {
console.log("🚀 디지털 트윈 외부 DB 연결 테스트 시작\n");
// 디지털 트윈 외부 DB 연결 정보
const digitalTwinConnection = {
name: "디지털트윈_DO_DY",
description: "디지털 트윈 후판(자재) 재고 정보 데이터베이스 (MariaDB)",
dbType: "mysql", // MariaDB는 MySQL 프로토콜 사용
host: "1.240.13.83",
port: 4307,
databaseName: "DO_DY",
username: "root",
password: "pohangms619!#",
sslEnabled: false,
isActive: true,
};
console.log("📝 연결 정보:");
console.log(` - 이름: ${digitalTwinConnection.name}`);
console.log(` - DB 타입: ${digitalTwinConnection.dbType}`);
console.log(` - 호스트: ${digitalTwinConnection.host}:${digitalTwinConnection.port}`);
console.log(` - 데이터베이스: ${digitalTwinConnection.databaseName}\n`);
// 1. 외부 DB 직접 연결 테스트
console.log("🔍 외부 DB 직접 연결 테스트 중...");
const externalConnection = await mysql.createConnection({
host: digitalTwinConnection.host,
port: digitalTwinConnection.port,
database: digitalTwinConnection.databaseName,
user: digitalTwinConnection.username,
password: digitalTwinConnection.password,
connectTimeout: 10000,
});
console.log("✅ 외부 DB 연결 성공!\n");
// 2. SELECT 쿼리 실행
console.log("📊 WSTKKY 테이블 쿼리 실행 중...\n");
const query = `
SELECT
SKUMKEY -- 제품번호
, SKUDESC -- 자재명
, SKUTHIC -- 두께
, SKUWIDT -- 폭
, SKULENG -- 길이
, SKUWEIG -- 중량
, STOTQTY -- 수량
, SUOMKEY -- 단위
FROM DO_DY.WSTKKY
LIMIT 10
`;
const [rows] = await externalConnection.execute(query);
console.log("✅ 쿼리 실행 성공!\n");
console.log(`📦 조회된 데이터: ${Array.isArray(rows) ? rows.length : 0}\n`);
if (Array.isArray(rows) && rows.length > 0) {
console.log("🔍 샘플 데이터 (첫 3건):\n");
rows.slice(0, 3).forEach((row: any, index: number) => {
console.log(`[${index + 1}]`);
console.log(` 제품번호(SKUMKEY): ${row.SKUMKEY}`);
console.log(` 자재명(SKUDESC): ${row.SKUDESC}`);
console.log(` 두께(SKUTHIC): ${row.SKUTHIC}`);
console.log(` 폭(SKUWIDT): ${row.SKUWIDT}`);
console.log(` 길이(SKULENG): ${row.SKULENG}`);
console.log(` 중량(SKUWEIG): ${row.SKUWEIG}`);
console.log(` 수량(STOTQTY): ${row.STOTQTY}`);
console.log(` 단위(SUOMKEY): ${row.SUOMKEY}\n`);
});
// 전체 데이터 JSON 출력
console.log("📄 전체 데이터 (JSON):");
console.log(JSON.stringify(rows, null, 2));
console.log("\n");
}
await externalConnection.end();
// 3. 내부 DB에 연결 정보 저장
console.log("💾 내부 DB에 연결 정보 저장 중...");
const encryptedPassword = encryption.encrypt(digitalTwinConnection.password);
// 중복 체크
const existingResult = await internalPool.query(
"SELECT id FROM flow_external_db_connection WHERE name = $1",
[digitalTwinConnection.name]
);
let connectionId: number;
if (existingResult.rows.length > 0) {
connectionId = existingResult.rows[0].id;
console.log(`⚠️ 이미 존재하는 연결 (ID: ${connectionId})`);
// 기존 연결 업데이트
await internalPool.query(
`UPDATE flow_external_db_connection
SET description = $1,
db_type = $2,
host = $3,
port = $4,
database_name = $5,
username = $6,
password_encrypted = $7,
ssl_enabled = $8,
is_active = $9,
updated_at = NOW(),
updated_by = 'system'
WHERE name = $10`,
[
digitalTwinConnection.description,
digitalTwinConnection.dbType,
digitalTwinConnection.host,
digitalTwinConnection.port,
digitalTwinConnection.databaseName,
digitalTwinConnection.username,
encryptedPassword,
digitalTwinConnection.sslEnabled,
digitalTwinConnection.isActive,
digitalTwinConnection.name,
]
);
console.log(`✅ 연결 정보 업데이트 완료`);
} else {
// 새 연결 추가
const result = await internalPool.query(
`INSERT INTO flow_external_db_connection (
name,
description,
db_type,
host,
port,
database_name,
username,
password_encrypted,
ssl_enabled,
is_active,
created_by
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'system')
RETURNING id`,
[
digitalTwinConnection.name,
digitalTwinConnection.description,
digitalTwinConnection.dbType,
digitalTwinConnection.host,
digitalTwinConnection.port,
digitalTwinConnection.databaseName,
digitalTwinConnection.username,
encryptedPassword,
digitalTwinConnection.sslEnabled,
digitalTwinConnection.isActive,
]
);
connectionId = result.rows[0].id;
console.log(`✅ 새 연결 추가 완료 (ID: ${connectionId})`);
}
console.log("\n✅ 모든 테스트 완료!");
console.log(`\n📌 연결 ID: ${connectionId}`);
console.log(" 이 ID를 사용하여 플로우 관리나 제어 관리에서 외부 DB를 연동할 수 있습니다.");
} catch (error: any) {
console.error("\n❌ 오류 발생:", error.message);
console.error("상세 정보:", error);
throw error;
} finally {
await internalPool.end();
}
}
// 스크립트 실행
testDigitalTwinDb()
.then(() => {
console.log("\n🎉 스크립트 완료");
process.exit(0);
})
.catch((error) => {
console.error("\n💥 스크립트 실패:", error);
process.exit(1);
});

View File

@@ -1,121 +0,0 @@
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();
async function testTemplateCreation() {
console.log("🧪 템플릿 생성 테스트 시작...");
try {
// 1. 테이블 존재 여부 확인
console.log("1. 템플릿 테이블 존재 여부 확인 중...");
try {
const count = await prisma.template_standards.count();
console.log(`✅ template_standards 테이블 발견 (현재 ${count}개 레코드)`);
} catch (error) {
if (error.code === "P2021") {
console.log("❌ template_standards 테이블이 존재하지 않습니다.");
console.log("👉 데이터베이스 마이그레이션이 필요합니다.");
return;
}
throw error;
}
// 2. 샘플 템플릿 생성 테스트
console.log("2. 샘플 템플릿 생성 중...");
const sampleTemplate = {
template_code: "test-button-" + Date.now(),
template_name: "테스트 버튼",
template_name_eng: "Test Button",
description: "테스트용 버튼 템플릿",
category: "button",
icon_name: "mouse-pointer",
default_size: {
width: 80,
height: 36,
},
layout_config: {
components: [
{
type: "widget",
widgetType: "button",
label: "테스트 버튼",
position: { x: 0, y: 0 },
size: { width: 80, height: 36 },
style: {
backgroundColor: "#3b82f6",
color: "#ffffff",
border: "none",
borderRadius: "6px",
},
},
],
},
sort_order: 999,
is_active: "Y",
is_public: "Y",
company_code: "*",
created_by: "test",
updated_by: "test",
};
const created = await prisma.template_standards.create({
data: sampleTemplate,
});
console.log("✅ 샘플 템플릿 생성 성공:", created.template_code);
// 3. 생성된 템플릿 조회 테스트
console.log("3. 템플릿 조회 테스트 중...");
const retrieved = await prisma.template_standards.findUnique({
where: { template_code: created.template_code },
});
if (retrieved) {
console.log("✅ 템플릿 조회 성공:", retrieved.template_name);
console.log(
"📄 Layout Config:",
JSON.stringify(retrieved.layout_config, null, 2)
);
}
// 4. 카테고리 목록 조회 테스트
console.log("4. 카테고리 목록 조회 테스트 중...");
const categories = await prisma.template_standards.findMany({
where: { is_active: "Y" },
select: { category: true },
distinct: ["category"],
});
console.log(
"✅ 발견된 카테고리:",
categories.map((c) => c.category)
);
// 5. 테스트 데이터 정리
console.log("5. 테스트 데이터 정리 중...");
await prisma.template_standards.delete({
where: { template_code: created.template_code },
});
console.log("✅ 테스트 데이터 정리 완료");
console.log("🎉 모든 테스트 통과!");
} catch (error) {
console.error("❌ 테스트 실패:", error);
console.error("📋 상세 정보:", {
message: error.message,
code: error.code,
stack: error.stack?.split("\n").slice(0, 5),
});
} finally {
await prisma.$disconnect();
}
}
// 스크립트 실행
testTemplateCreation();

View File

@@ -1,86 +0,0 @@
/**
* 마이그레이션 검증 스크립트
*/
const { Pool } = require('pg');
const databaseUrl = process.env.DATABASE_URL || 'postgresql://postgres:ph0909!!@39.117.244.52:11132/plm';
const pool = new Pool({
connectionString: databaseUrl,
});
async function verifyMigration() {
const client = await pool.connect();
try {
console.log('🔍 마이그레이션 결과 검증 중...\n');
// 전체 요소 수
const total = await client.query(`
SELECT COUNT(*) as count FROM dashboard_elements
`);
// 새로운 subtype별 개수
const mapV2 = await client.query(`
SELECT COUNT(*) as count FROM dashboard_elements WHERE element_subtype = 'map-summary-v2'
`);
const chart = await client.query(`
SELECT COUNT(*) as count FROM dashboard_elements WHERE element_subtype = 'chart'
`);
const listV2 = await client.query(`
SELECT COUNT(*) as count FROM dashboard_elements WHERE element_subtype = 'list-v2'
`);
const metricV2 = await client.query(`
SELECT COUNT(*) as count FROM dashboard_elements WHERE element_subtype = 'custom-metric-v2'
`);
const alertV2 = await client.query(`
SELECT COUNT(*) as count FROM dashboard_elements WHERE element_subtype = 'risk-alert-v2'
`);
// 테스트 subtype 남아있는지 확인
const remaining = await client.query(`
SELECT COUNT(*) as count FROM dashboard_elements WHERE element_subtype LIKE '%-test%'
`);
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('📊 마이그레이션 결과 요약');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log(`전체 요소 수: ${total.rows[0].count}`);
console.log(`map-summary-v2: ${mapV2.rows[0].count}`);
console.log(`chart: ${chart.rows[0].count}`);
console.log(`list-v2: ${listV2.rows[0].count}`);
console.log(`custom-metric-v2: ${metricV2.rows[0].count}`);
console.log(`risk-alert-v2: ${alertV2.rows[0].count}`);
console.log('');
if (parseInt(remaining.rows[0].count) > 0) {
console.log(`⚠️ 테스트 subtype이 ${remaining.rows[0].count}개 남아있습니다!`);
} else {
console.log('✅ 모든 테스트 subtype이 정상적으로 변경되었습니다!');
}
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('');
console.log('🎉 마이그레이션이 성공적으로 완료되었습니다!');
console.log('');
console.log('다음 단계:');
console.log('1. 프론트엔드 애플리케이션을 새로고침하세요');
console.log('2. 대시보드를 열어 위젯이 정상적으로 작동하는지 확인하세요');
console.log('3. 문제가 발생하면 백업에서 복원하세요');
console.log('');
} catch (error) {
console.error('❌ 오류 발생:', error.message);
} finally {
client.release();
await pool.end();
}
}
verifyMigration();