Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/dashboard
This commit is contained in:
commit
963e57d7e7
|
|
@ -233,6 +233,14 @@ app.listen(PORT, HOST, async () => {
|
||||||
logger.info(`🔗 Health check: http://${HOST}:${PORT}/health`);
|
logger.info(`🔗 Health check: http://${HOST}:${PORT}/health`);
|
||||||
logger.info(`🌐 External access: http://39.117.244.52:${PORT}/health`);
|
logger.info(`🌐 External access: http://39.117.244.52:${PORT}/health`);
|
||||||
|
|
||||||
|
// 대시보드 마이그레이션 실행
|
||||||
|
try {
|
||||||
|
const { runDashboardMigration } = await import('./database/runMigration');
|
||||||
|
await runDashboardMigration();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`❌ 대시보드 마이그레이션 실패:`, error);
|
||||||
|
}
|
||||||
|
|
||||||
// 배치 스케줄러 초기화
|
// 배치 스케줄러 초기화
|
||||||
try {
|
try {
|
||||||
await BatchSchedulerService.initialize();
|
await BatchSchedulerService.initialize();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { PostgreSQLService } from './PostgreSQLService';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 데이터베이스 마이그레이션 실행
|
||||||
|
* dashboard_elements 테이블에 custom_title, show_header 컬럼 추가
|
||||||
|
*/
|
||||||
|
export async function runDashboardMigration() {
|
||||||
|
try {
|
||||||
|
console.log('🔄 대시보드 마이그레이션 시작...');
|
||||||
|
|
||||||
|
// custom_title 컬럼 추가
|
||||||
|
await PostgreSQLService.query(`
|
||||||
|
ALTER TABLE dashboard_elements
|
||||||
|
ADD COLUMN IF NOT EXISTS custom_title VARCHAR(255)
|
||||||
|
`);
|
||||||
|
console.log('✅ custom_title 컬럼 추가 완료');
|
||||||
|
|
||||||
|
// show_header 컬럼 추가
|
||||||
|
await PostgreSQLService.query(`
|
||||||
|
ALTER TABLE dashboard_elements
|
||||||
|
ADD COLUMN IF NOT EXISTS show_header BOOLEAN DEFAULT true
|
||||||
|
`);
|
||||||
|
console.log('✅ show_header 컬럼 추가 완료');
|
||||||
|
|
||||||
|
// 기존 데이터 업데이트
|
||||||
|
await PostgreSQLService.query(`
|
||||||
|
UPDATE dashboard_elements
|
||||||
|
SET show_header = true
|
||||||
|
WHERE show_header IS NULL
|
||||||
|
`);
|
||||||
|
console.log('✅ 기존 데이터 업데이트 완료');
|
||||||
|
|
||||||
|
console.log('✅ 대시보드 마이그레이션 완료!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 대시보드 마이그레이션 실패:', error);
|
||||||
|
// 이미 컬럼이 있는 경우는 무시
|
||||||
|
if (error instanceof Error && error.message.includes('already exists')) {
|
||||||
|
console.log('ℹ️ 컬럼이 이미 존재합니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -60,9 +60,9 @@ export class DashboardService {
|
||||||
INSERT INTO dashboard_elements (
|
INSERT INTO dashboard_elements (
|
||||||
id, dashboard_id, element_type, element_subtype,
|
id, dashboard_id, element_type, element_subtype,
|
||||||
position_x, position_y, width, height,
|
position_x, position_y, width, height,
|
||||||
title, content, data_source_config, chart_config,
|
title, custom_title, show_header, content, data_source_config, chart_config,
|
||||||
display_order, created_at, updated_at
|
display_order, created_at, updated_at
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
elementId,
|
elementId,
|
||||||
|
|
@ -74,6 +74,8 @@ export class DashboardService {
|
||||||
element.size.width,
|
element.size.width,
|
||||||
element.size.height,
|
element.size.height,
|
||||||
element.title,
|
element.title,
|
||||||
|
element.customTitle || null,
|
||||||
|
element.showHeader !== false, // 기본값 true
|
||||||
element.content || null,
|
element.content || null,
|
||||||
JSON.stringify(element.dataSource || {}),
|
JSON.stringify(element.dataSource || {}),
|
||||||
JSON.stringify(element.chartConfig || {}),
|
JSON.stringify(element.chartConfig || {}),
|
||||||
|
|
@ -335,6 +337,8 @@ export class DashboardService {
|
||||||
height: row.height,
|
height: row.height,
|
||||||
},
|
},
|
||||||
title: row.title,
|
title: row.title,
|
||||||
|
customTitle: row.custom_title || undefined,
|
||||||
|
showHeader: row.show_header !== false, // 기본값 true
|
||||||
content: row.content,
|
content: row.content,
|
||||||
dataSource: JSON.parse(row.data_source_config || "{}"),
|
dataSource: JSON.parse(row.data_source_config || "{}"),
|
||||||
chartConfig: JSON.parse(row.chart_config || "{}"),
|
chartConfig: JSON.parse(row.chart_config || "{}"),
|
||||||
|
|
@ -460,9 +464,9 @@ export class DashboardService {
|
||||||
INSERT INTO dashboard_elements (
|
INSERT INTO dashboard_elements (
|
||||||
id, dashboard_id, element_type, element_subtype,
|
id, dashboard_id, element_type, element_subtype,
|
||||||
position_x, position_y, width, height,
|
position_x, position_y, width, height,
|
||||||
title, content, data_source_config, chart_config,
|
title, custom_title, show_header, content, data_source_config, chart_config,
|
||||||
display_order, created_at, updated_at
|
display_order, created_at, updated_at
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
elementId,
|
elementId,
|
||||||
|
|
@ -474,6 +478,8 @@ export class DashboardService {
|
||||||
element.size.width,
|
element.size.width,
|
||||||
element.size.height,
|
element.size.height,
|
||||||
element.title,
|
element.title,
|
||||||
|
element.customTitle || null,
|
||||||
|
element.showHeader !== false, // 기본값 true
|
||||||
element.content || null,
|
element.content || null,
|
||||||
JSON.stringify(element.dataSource || {}),
|
JSON.stringify(element.dataSource || {}),
|
||||||
JSON.stringify(element.chartConfig || {}),
|
JSON.stringify(element.chartConfig || {}),
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ export interface DashboardElement {
|
||||||
height: number;
|
height: number;
|
||||||
};
|
};
|
||||||
title: string;
|
title: string;
|
||||||
|
customTitle?: string; // 사용자 정의 제목 (옵션)
|
||||||
|
showHeader?: boolean; // 헤더 표시 여부 (기본값: true)
|
||||||
content?: string;
|
content?: string;
|
||||||
dataSource?: {
|
dataSource?: {
|
||||||
type: "api" | "database" | "static";
|
type: "api" | "database" | "static";
|
||||||
|
|
|
||||||
|
|
@ -455,7 +455,7 @@ export function CanvasElement({
|
||||||
>
|
>
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="flex cursor-move items-center justify-between border-b border-gray-200 bg-gray-50 p-3">
|
<div className="flex cursor-move items-center justify-between border-b border-gray-200 bg-gray-50 p-3">
|
||||||
<span className="text-sm font-bold text-gray-800">{element.title}</span>
|
<span className="text-sm font-bold text-gray-800">{element.customTitle || element.title}</span>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
{/* 설정 버튼 (시계, 달력, 기사관리 위젯은 자체 설정 UI 사용) */}
|
{/* 설정 버튼 (시계, 달력, 기사관리 위젯은 자체 설정 UI 사용) */}
|
||||||
{onConfigure &&
|
{onConfigure &&
|
||||||
|
|
|
||||||
|
|
@ -338,6 +338,8 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
||||||
position: el.position,
|
position: el.position,
|
||||||
size: el.size,
|
size: el.size,
|
||||||
title: el.title,
|
title: el.title,
|
||||||
|
customTitle: el.customTitle,
|
||||||
|
showHeader: el.showHeader,
|
||||||
content: el.content,
|
content: el.content,
|
||||||
dataSource: el.dataSource,
|
dataSource: el.dataSource,
|
||||||
chartConfig: el.chartConfig,
|
chartConfig: el.chartConfig,
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
|
||||||
const [queryResult, setQueryResult] = useState<QueryResult | null>(null);
|
const [queryResult, setQueryResult] = useState<QueryResult | null>(null);
|
||||||
const [currentStep, setCurrentStep] = useState<1 | 2>(1);
|
const [currentStep, setCurrentStep] = useState<1 | 2>(1);
|
||||||
const [customTitle, setCustomTitle] = useState<string>(element.customTitle || "");
|
const [customTitle, setCustomTitle] = useState<string>(element.customTitle || "");
|
||||||
|
const [showHeader, setShowHeader] = useState<boolean>(element.showHeader !== false);
|
||||||
|
|
||||||
// 차트 설정이 필요 없는 위젯 (쿼리/API만 필요)
|
// 차트 설정이 필요 없는 위젯 (쿼리/API만 필요)
|
||||||
const isSimpleWidget =
|
const isSimpleWidget =
|
||||||
|
|
@ -122,13 +123,14 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
|
||||||
dataSource,
|
dataSource,
|
||||||
chartConfig,
|
chartConfig,
|
||||||
customTitle: customTitle.trim() || undefined, // 빈 문자열이면 undefined
|
customTitle: customTitle.trim() || undefined, // 빈 문자열이면 undefined
|
||||||
|
showHeader, // 헤더 표시 여부
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(" 저장할 element:", updatedElement);
|
console.log(" 저장할 element:", updatedElement);
|
||||||
|
|
||||||
onSave(updatedElement);
|
onSave(updatedElement);
|
||||||
onClose();
|
onClose();
|
||||||
}, [element, dataSource, chartConfig, customTitle, onSave, onClose]);
|
}, [element, dataSource, chartConfig, customTitle, showHeader, onSave, onClose]);
|
||||||
|
|
||||||
// 모달이 열려있지 않으면 렌더링하지 않음
|
// 모달이 열려있지 않으면 렌더링하지 않음
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
@ -217,6 +219,20 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
|
||||||
💡 비워두면 테이블명으로 자동 생성됩니다 (예: "maintenance_schedules 목록")
|
💡 비워두면 테이블명으로 자동 생성됩니다 (예: "maintenance_schedules 목록")
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 헤더 표시 여부 */}
|
||||||
|
<div className="mt-4 flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="showHeader"
|
||||||
|
checked={showHeader}
|
||||||
|
onChange={(e) => setShowHeader(e.target.checked)}
|
||||||
|
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
|
||||||
|
/>
|
||||||
|
<label htmlFor="showHeader" className="ml-2 block text-sm text-gray-700">
|
||||||
|
위젯 헤더 표시
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 진행 상황 표시 - 간단한 위젯은 표시 안 함 */}
|
{/* 진행 상황 표시 - 간단한 위젯은 표시 안 함 */}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ export interface DashboardElement {
|
||||||
size: Size;
|
size: Size;
|
||||||
title: string;
|
title: string;
|
||||||
customTitle?: string; // 사용자 정의 제목 (옵션)
|
customTitle?: string; // 사용자 정의 제목 (옵션)
|
||||||
|
showHeader?: boolean; // 헤더 표시 여부 (기본값: true)
|
||||||
content: string;
|
content: string;
|
||||||
dataSource?: ChartDataSource; // 데이터 소스 설정
|
dataSource?: ChartDataSource; // 데이터 소스 설정
|
||||||
chartConfig?: ChartConfig; // 차트 설정
|
chartConfig?: ChartConfig; // 차트 설정
|
||||||
|
|
|
||||||
|
|
@ -300,29 +300,31 @@ function ViewerElement({ element, data, isLoading, onRefresh }: ViewerElementPro
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
>
|
>
|
||||||
{/* 헤더 */}
|
{/* 헤더 (showHeader가 false가 아닐 때만 표시) */}
|
||||||
<div className="flex items-center justify-between border-b border-gray-200 bg-gray-50 px-4 py-3">
|
{element.showHeader !== false && (
|
||||||
<h3 className="text-sm font-semibold text-gray-800">{element.title}</h3>
|
<div className="flex items-center justify-between border-b border-gray-200 bg-gray-50 px-4 py-3">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-800">{element.customTitle || element.title}</h3>
|
||||||
|
|
||||||
{/* 새로고침 버튼 (호버 시에만 표시) */}
|
{/* 새로고침 버튼 (호버 시에만 표시) */}
|
||||||
{isHovered && (
|
{isHovered && (
|
||||||
<button
|
<button
|
||||||
onClick={onRefresh}
|
onClick={onRefresh}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="hover:text-muted-foreground text-gray-400 disabled:opacity-50"
|
className="hover:text-muted-foreground text-gray-400 disabled:opacity-50"
|
||||||
title="새로고침"
|
title="새로고침"
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="h-4 w-4 animate-spin rounded-full border border-gray-400 border-t-transparent" />
|
<div className="h-4 w-4 animate-spin rounded-full border border-gray-400 border-t-transparent" />
|
||||||
) : (
|
) : (
|
||||||
"🔄"
|
"🔄"
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 내용 */}
|
{/* 내용 */}
|
||||||
<div className="h-[calc(100%-57px)]">
|
<div className={element.showHeader !== false ? "h-[calc(100%-57px)]" : "h-full"}>
|
||||||
{element.type === "chart" ? (
|
{element.type === "chart" ? (
|
||||||
<ChartRenderer element={element} data={data} width={element.size.width} height={element.size.height - 57} />
|
<ChartRenderer element={element} data={data} width={element.size.width} height={element.size.height - 57} />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue