ERP-node/frontend/lib/registry/components/related-data-buttons/RelatedDataButtonsComponent...

463 lines
16 KiB
TypeScript

"use client";
import React, { useState, useEffect, useCallback, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Plus, Star, Loader2, ExternalLink } from "lucide-react";
import { cn } from "@/lib/utils";
import { useSplitPanelContext } from "@/contexts/SplitPanelContext";
import { dataApi } from "@/lib/api/data";
import type { RelatedDataButtonsConfig, ButtonItem } from "./types";
// 전역 상태: 현재 선택된 버튼 데이터를 외부에서 접근 가능하게
declare global {
interface Window {
__relatedButtonsSelectedData?: {
selectedItem: ButtonItem | null;
masterData: Record<string, any> | null;
config: RelatedDataButtonsConfig | null;
};
// 🆕 RelatedDataButtons가 대상으로 하는 테이블 목록 (전역 레지스트리)
__relatedButtonsTargetTables?: Set<string>;
}
}
// 전역 레지스트리 초기화
if (typeof window !== "undefined" && !window.__relatedButtonsTargetTables) {
window.__relatedButtonsTargetTables = new Set();
}
interface RelatedDataButtonsComponentProps {
config: RelatedDataButtonsConfig;
className?: string;
style?: React.CSSProperties;
}
export const RelatedDataButtonsComponent: React.FC<RelatedDataButtonsComponentProps> = ({
config,
className,
style,
}) => {
const [buttons, setButtons] = useState<ButtonItem[]>([]);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [selectedItem, setSelectedItem] = useState<ButtonItem | null>(null);
const [loading, setLoading] = useState(false);
const [masterData, setMasterData] = useState<Record<string, any> | null>(null);
// SplitPanel Context 연결
const splitPanelContext = useSplitPanelContext();
// 선택된 데이터를 전역 상태에 저장 (외부 버튼에서 접근용)
useEffect(() => {
window.__relatedButtonsSelectedData = {
selectedItem,
masterData,
config,
};
console.log("🔄 [RelatedDataButtons] 전역 상태 업데이트:", {
selectedItem,
hasConfig: !!config,
modalLink: config?.modalLink,
});
}, [selectedItem, masterData, config]);
// 좌측 패널에서 선택된 데이터 감지
useEffect(() => {
if (!splitPanelContext?.selectedLeftData) {
setMasterData(null);
setButtons([]);
setSelectedId(null);
setSelectedItem(null);
// 🆕 좌측 데이터가 없을 때 대상 테이블에 빈 상태 알림
if (config.events?.targetTable) {
window.dispatchEvent(new CustomEvent("related-button-select", {
detail: {
targetTable: config.events.targetTable,
filterColumn: config.events.targetFilterColumn,
filterValue: null, // null로 설정하여 빈 상태 표시
selectedData: null,
},
}));
}
return;
}
setMasterData(splitPanelContext.selectedLeftData);
}, [splitPanelContext?.selectedLeftData, config.events]);
// 🆕 컴포넌트 마운트 시 대상 테이블에 필터 필요 알림
useEffect(() => {
if (config.events?.targetTable) {
// 전역 레지스트리에 등록
window.__relatedButtonsTargetTables?.add(config.events.targetTable);
// 이벤트도 발생 (이미 마운트된 테이블 컴포넌트를 위해)
window.dispatchEvent(new CustomEvent("related-button-register", {
detail: {
targetTable: config.events.targetTable,
filterColumn: config.events.targetFilterColumn,
},
}));
console.log("📝 [RelatedDataButtons] 대상 테이블에 필터 등록:", config.events.targetTable);
}
return () => {
// 컴포넌트 언마운트 시 등록 해제
if (config.events?.targetTable) {
window.__relatedButtonsTargetTables?.delete(config.events.targetTable);
window.dispatchEvent(new CustomEvent("related-button-unregister", {
detail: {
targetTable: config.events.targetTable,
},
}));
}
};
}, [config.events?.targetTable, config.events?.targetFilterColumn]);
// 버튼 데이터 로드
const loadButtons = useCallback(async () => {
if (!masterData || !config.buttonDataSource?.tableName) {
return;
}
const filterValue = masterData[config.sourceMapping.sourceColumn];
if (!filterValue) {
setButtons([]);
return;
}
setLoading(true);
try {
const { tableName, filterColumn, displayColumn, valueColumn, orderColumn, orderDirection } = config.buttonDataSource;
const response = await dataApi.getTableData(tableName, {
filters: { [filterColumn]: filterValue },
sortBy: orderColumn || "created_date",
sortOrder: (orderDirection?.toLowerCase() || "asc") as "asc" | "desc",
size: 50,
});
if (response.data && response.data.length > 0) {
const defaultConfig = config.buttonStyle?.defaultIndicator;
const items: ButtonItem[] = response.data.map((row: Record<string, any>) => {
let isDefault = false;
if (defaultConfig?.column) {
const val = row[defaultConfig.column];
const checkValue = defaultConfig.value || "Y";
isDefault = val === checkValue || val === true || val === "true";
}
return {
id: row.id || row[valueColumn || "id"],
displayText: row[displayColumn] || row.id,
value: row[valueColumn || "id"],
isDefault,
rawData: row,
};
});
setButtons(items);
// 자동 선택: 기본 항목 또는 첫 번째 항목
if (config.autoSelectFirst && items.length > 0) {
const defaultItem = items.find(item => item.isDefault);
const targetItem = defaultItem || items[0];
setSelectedId(targetItem.id);
setSelectedItem(targetItem);
emitSelection(targetItem);
}
}
} catch (error) {
console.error("RelatedDataButtons 데이터 로드 실패:", error);
setButtons([]);
} finally {
setLoading(false);
}
}, [masterData, config.buttonDataSource, config.sourceMapping, config.buttonStyle, config.autoSelectFirst]);
// masterData 변경 시 버튼 로드
useEffect(() => {
if (masterData) {
setSelectedId(null); // 마스터 변경 시 선택 초기화
setSelectedItem(null);
loadButtons();
}
}, [masterData, loadButtons]);
// 선택 이벤트 발생
const emitSelection = useCallback((item: ButtonItem) => {
if (!config.events?.targetTable || !config.events?.targetFilterColumn) {
return;
}
// 커스텀 이벤트 발생 (하위 테이블 필터링용)
window.dispatchEvent(new CustomEvent("related-button-select", {
detail: {
targetTable: config.events.targetTable,
filterColumn: config.events.targetFilterColumn,
filterValue: item.value,
selectedData: item.rawData,
},
}));
console.log("📌 RelatedDataButtons 선택 이벤트:", {
targetTable: config.events.targetTable,
filterColumn: config.events.targetFilterColumn,
filterValue: item.value,
});
}, [config.events]);
// 버튼 클릭 핸들러
const handleButtonClick = useCallback((item: ButtonItem) => {
setSelectedId(item.id);
setSelectedItem(item);
emitSelection(item);
}, [emitSelection]);
// 모달 열기 (선택된 버튼 데이터 전달)
const openModalWithSelectedData = useCallback((targetScreenId: number) => {
if (!selectedItem) {
console.warn("선택된 버튼이 없습니다.");
return;
}
// 데이터 매핑 적용
const initialData: Record<string, any> = {};
if (config.modalLink?.dataMapping) {
config.modalLink.dataMapping.forEach(mapping => {
if (mapping.sourceField === "value") {
initialData[mapping.targetField] = selectedItem.value;
} else if (mapping.sourceField === "id") {
initialData[mapping.targetField] = selectedItem.id;
} else if (selectedItem.rawData[mapping.sourceField] !== undefined) {
initialData[mapping.targetField] = selectedItem.rawData[mapping.sourceField];
}
});
} else {
// 기본 매핑: id를 routing_version_id로 전달
initialData["routing_version_id"] = selectedItem.value || selectedItem.id;
}
console.log("📤 RelatedDataButtons 모달 열기:", {
targetScreenId,
selectedItem,
initialData,
});
window.dispatchEvent(new CustomEvent("open-screen-modal", {
detail: {
screenId: targetScreenId,
initialData,
onSuccess: () => {
loadButtons(); // 모달 성공 후 새로고침
},
},
}));
}, [selectedItem, config.modalLink, loadButtons]);
// 외부 버튼에서 모달 열기 요청 수신
useEffect(() => {
const handleExternalModalOpen = (event: CustomEvent) => {
const { targetScreenId, componentId } = event.detail || {};
// componentId가 지정되어 있고 현재 컴포넌트가 아니면 무시
if (componentId && componentId !== config.sourceMapping?.sourceTable) {
return;
}
if (targetScreenId && selectedItem) {
openModalWithSelectedData(targetScreenId);
}
};
window.addEventListener("related-buttons-open-modal" as any, handleExternalModalOpen);
return () => {
window.removeEventListener("related-buttons-open-modal" as any, handleExternalModalOpen);
};
}, [selectedItem, config.sourceMapping, openModalWithSelectedData]);
// 내부 모달 링크 버튼 클릭
const handleModalLinkClick = useCallback(() => {
if (!config.modalLink?.targetScreenId) {
console.warn("모달 링크 설정이 없습니다.");
return;
}
openModalWithSelectedData(config.modalLink.targetScreenId);
}, [config.modalLink, openModalWithSelectedData]);
// 추가 버튼 클릭
const handleAddClick = useCallback(() => {
if (!config.addButton?.modalScreenId) return;
const filterValue = masterData?.[config.sourceMapping.sourceColumn];
window.dispatchEvent(new CustomEvent("open-screen-modal", {
detail: {
screenId: config.addButton.modalScreenId,
initialData: {
[config.buttonDataSource.filterColumn]: filterValue,
},
onSuccess: () => {
loadButtons(); // 모달 성공 후 새로고침
},
},
}));
}, [config.addButton, config.buttonDataSource.filterColumn, config.sourceMapping.sourceColumn, masterData, loadButtons]);
// 버튼 variant 계산
const getButtonVariant = useCallback((item: ButtonItem): "default" | "outline" | "secondary" | "ghost" => {
if (selectedId === item.id) {
return config.buttonStyle?.activeVariant || "default";
}
return config.buttonStyle?.variant || "outline";
}, [selectedId, config.buttonStyle]);
// 마스터 데이터 없음
if (!masterData) {
return (
<div className={cn("rounded-lg border bg-card p-4", className)} style={style}>
<p className="text-sm text-muted-foreground text-center">
</p>
</div>
);
}
const headerConfig = config.headerDisplay;
const addButtonConfig = config.addButton;
const modalLinkConfig = config.modalLink;
return (
<div className={cn("rounded-lg border bg-card", className)} style={style}>
{/* 헤더 영역 */}
{headerConfig?.show !== false && (
<div className="flex items-start justify-between p-4 pb-3">
<div>
{/* 제목 (품목명 등) */}
{headerConfig?.titleColumn && masterData[headerConfig.titleColumn] && (
<h3 className="text-lg font-semibold">
{masterData[headerConfig.titleColumn]}
</h3>
)}
{/* 부제목 (품목코드 등) */}
{headerConfig?.subtitleColumn && masterData[headerConfig.subtitleColumn] && (
<p className="text-sm text-muted-foreground">
{masterData[headerConfig.subtitleColumn]}
</p>
)}
</div>
<div className="flex items-center gap-2">
{/* 모달 링크 버튼 (헤더 위치) */}
{modalLinkConfig?.enabled && modalLinkConfig?.triggerType === "button" && modalLinkConfig?.buttonPosition === "header" && (
<Button
variant="outline"
size="sm"
onClick={handleModalLinkClick}
disabled={!selectedItem}
title={!selectedItem ? "버튼을 먼저 선택하세요" : ""}
>
<ExternalLink className="mr-1 h-4 w-4" />
{modalLinkConfig.buttonLabel || "상세 추가"}
</Button>
)}
{/* 헤더 위치 추가 버튼 */}
{addButtonConfig?.show && addButtonConfig?.position === "header" && (
<Button
variant="default"
size="sm"
onClick={handleAddClick}
className="bg-blue-600 hover:bg-blue-700"
>
<Plus className="mr-1 h-4 w-4" />
{addButtonConfig.label || "버전 추가"}
</Button>
)}
</div>
</div>
)}
{/* 버튼 영역 */}
<div className="px-4 pb-4">
{loading ? (
<div className="flex items-center justify-center py-4">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
) : buttons.length === 0 ? (
<div className="flex items-center gap-2">
<p className="text-sm text-muted-foreground">
{config.emptyMessage || "데이터가 없습니다"}
</p>
{/* 인라인 추가 버튼 (데이터 없을 때) */}
{addButtonConfig?.show && addButtonConfig?.position !== "header" && (
<Button
variant="outline"
size="sm"
onClick={handleAddClick}
className="border-dashed"
>
<Plus className="mr-1 h-4 w-4" />
{addButtonConfig.label || "추가"}
</Button>
)}
</div>
) : (
<div className="flex flex-wrap items-center gap-2">
{buttons.map((item) => (
<Button
key={item.id}
variant={getButtonVariant(item)}
size={config.buttonStyle?.size || "default"}
onClick={() => handleButtonClick(item)}
className={cn(
"relative",
selectedId === item.id && "ring-2 ring-primary ring-offset-1"
)}
>
{/* 기본 버전 별표 */}
{item.isDefault && config.buttonStyle?.defaultIndicator?.showStar && (
<Star className="mr-1.5 h-3.5 w-3.5 fill-yellow-400 text-yellow-400" />
)}
{item.displayText}
</Button>
))}
{/* 모달 링크 버튼 (인라인 위치) */}
{modalLinkConfig?.enabled && modalLinkConfig?.triggerType === "button" && modalLinkConfig?.buttonPosition !== "header" && (
<Button
variant="outline"
size={config.buttonStyle?.size || "default"}
onClick={handleModalLinkClick}
disabled={!selectedItem}
title={!selectedItem ? "버튼을 먼저 선택하세요" : ""}
>
<ExternalLink className="mr-1 h-4 w-4" />
{modalLinkConfig.buttonLabel || "상세 추가"}
</Button>
)}
{/* 인라인 추가 버튼 */}
{addButtonConfig?.show && addButtonConfig?.position !== "header" && (
<Button
variant="outline"
size={config.buttonStyle?.size || "default"}
onClick={handleAddClick}
className="border-dashed"
>
<Plus className="mr-1 h-4 w-4" />
{addButtonConfig.label || "추가"}
</Button>
)}
</div>
)}
</div>
</div>
);
};
export default RelatedDataButtonsComponent;