Compare commits
2 Commits
aca00b8704
...
a12f2273b3
| Author | SHA1 | Date |
|---|---|---|
|
|
a12f2273b3 | |
|
|
fb16e224f0 |
|
|
@ -17,6 +17,7 @@ import { toast } from "sonner";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
|
import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
|
||||||
import { TableSearchWidgetHeightProvider } from "@/contexts/TableSearchWidgetHeightContext";
|
import { TableSearchWidgetHeightProvider } from "@/contexts/TableSearchWidgetHeightContext";
|
||||||
|
import { useSplitPanelContext } from "@/contexts/SplitPanelContext";
|
||||||
|
|
||||||
interface ScreenModalState {
|
interface ScreenModalState {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|
@ -32,6 +33,7 @@ interface ScreenModalProps {
|
||||||
|
|
||||||
export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
const { userId, userName, user } = useAuth();
|
const { userId, userName, user } = useAuth();
|
||||||
|
const splitPanelContext = useSplitPanelContext();
|
||||||
|
|
||||||
const [modalState, setModalState] = useState<ScreenModalState>({
|
const [modalState, setModalState] = useState<ScreenModalState>({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
|
|
@ -152,6 +154,14 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
setFormData(editData);
|
setFormData(editData);
|
||||||
setOriginalData(editData); // 🆕 원본 데이터 저장 (UPDATE 판단용)
|
setOriginalData(editData); // 🆕 원본 데이터 저장 (UPDATE 판단용)
|
||||||
} else {
|
} else {
|
||||||
|
// 🆕 신규 등록 모드: 분할 패널 부모 데이터가 있으면 미리 설정
|
||||||
|
const parentData = splitPanelContext?.getMappedParentData() || {};
|
||||||
|
if (Object.keys(parentData).length > 0) {
|
||||||
|
console.log("🔗 [ScreenModal] 분할 패널 부모 데이터 초기값 설정:", parentData);
|
||||||
|
setFormData(parentData);
|
||||||
|
} else {
|
||||||
|
setFormData({});
|
||||||
|
}
|
||||||
setOriginalData(null); // 신규 등록 모드
|
setOriginalData(null); // 신규 등록 모드
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ export function ScreenSplitPanel({ screenId, config, initialFormData }: ScreenSp
|
||||||
leftScreenId: config?.leftScreenId,
|
leftScreenId: config?.leftScreenId,
|
||||||
rightScreenId: config?.rightScreenId,
|
rightScreenId: config?.rightScreenId,
|
||||||
configSplitRatio,
|
configSplitRatio,
|
||||||
|
parentDataMapping: config?.parentDataMapping,
|
||||||
configKeys: config ? Object.keys(config) : [],
|
configKeys: config ? Object.keys(config) : [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -125,6 +126,7 @@ export function ScreenSplitPanel({ screenId, config, initialFormData }: ScreenSp
|
||||||
splitPanelId={splitPanelId}
|
splitPanelId={splitPanelId}
|
||||||
leftScreenId={config?.leftScreenId || null}
|
leftScreenId={config?.leftScreenId || null}
|
||||||
rightScreenId={config?.rightScreenId || null}
|
rightScreenId={config?.rightScreenId || null}
|
||||||
|
parentDataMapping={config?.parentDataMapping || []}
|
||||||
>
|
>
|
||||||
<div className="flex h-full">
|
<div className="flex h-full">
|
||||||
{/* 좌측 패널 */}
|
{/* 좌측 패널 */}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,15 @@ export interface SplitPanelDataReceiver {
|
||||||
receiveData: (data: any[], mode: "append" | "replace" | "merge") => Promise<void>;
|
receiveData: (data: any[], mode: "append" | "replace" | "merge") => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 부모 데이터 매핑 설정
|
||||||
|
* 좌측 화면에서 선택한 데이터를 우측 화면 저장 시 자동으로 포함
|
||||||
|
*/
|
||||||
|
export interface ParentDataMapping {
|
||||||
|
sourceColumn: string; // 좌측 화면의 컬럼명 (예: equipment_code)
|
||||||
|
targetColumn: string; // 우측 화면 저장 시 사용할 컬럼명 (예: equipment_code)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 분할 패널 컨텍스트 값
|
* 분할 패널 컨텍스트 값
|
||||||
*/
|
*/
|
||||||
|
|
@ -54,6 +63,16 @@ interface SplitPanelContextValue {
|
||||||
addItemIds: (ids: string[]) => void;
|
addItemIds: (ids: string[]) => void;
|
||||||
removeItemIds: (ids: string[]) => void;
|
removeItemIds: (ids: string[]) => void;
|
||||||
clearItemIds: () => void;
|
clearItemIds: () => void;
|
||||||
|
|
||||||
|
// 🆕 좌측 선택 데이터 관리 (우측 화면 저장 시 부모 키 전달용)
|
||||||
|
selectedLeftData: Record<string, any> | null;
|
||||||
|
setSelectedLeftData: (data: Record<string, any> | null) => void;
|
||||||
|
|
||||||
|
// 🆕 부모 데이터 매핑 설정
|
||||||
|
parentDataMapping: ParentDataMapping[];
|
||||||
|
|
||||||
|
// 🆕 매핑된 부모 데이터 가져오기 (우측 화면 저장 시 사용)
|
||||||
|
getMappedParentData: () => Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SplitPanelContext = createContext<SplitPanelContextValue | null>(null);
|
const SplitPanelContext = createContext<SplitPanelContextValue | null>(null);
|
||||||
|
|
@ -62,6 +81,7 @@ interface SplitPanelProviderProps {
|
||||||
splitPanelId: string;
|
splitPanelId: string;
|
||||||
leftScreenId: number | null;
|
leftScreenId: number | null;
|
||||||
rightScreenId: number | null;
|
rightScreenId: number | null;
|
||||||
|
parentDataMapping?: ParentDataMapping[]; // 🆕 부모 데이터 매핑 설정
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,6 +92,7 @@ export function SplitPanelProvider({
|
||||||
splitPanelId,
|
splitPanelId,
|
||||||
leftScreenId,
|
leftScreenId,
|
||||||
rightScreenId,
|
rightScreenId,
|
||||||
|
parentDataMapping = [],
|
||||||
children,
|
children,
|
||||||
}: SplitPanelProviderProps) {
|
}: SplitPanelProviderProps) {
|
||||||
// 좌측/우측 화면의 데이터 수신자 맵
|
// 좌측/우측 화면의 데이터 수신자 맵
|
||||||
|
|
@ -84,6 +105,9 @@ export function SplitPanelProvider({
|
||||||
// 🆕 우측에 추가된 항목 ID 상태
|
// 🆕 우측에 추가된 항목 ID 상태
|
||||||
const [addedItemIds, setAddedItemIds] = useState<Set<string>>(new Set());
|
const [addedItemIds, setAddedItemIds] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
// 🆕 좌측에서 선택된 데이터 상태
|
||||||
|
const [selectedLeftData, setSelectedLeftData] = useState<Record<string, any> | null>(null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 데이터 수신자 등록
|
* 데이터 수신자 등록
|
||||||
*/
|
*/
|
||||||
|
|
@ -232,6 +256,40 @@ export function SplitPanelProvider({
|
||||||
logger.debug(`[SplitPanelContext] 항목 ID 초기화`);
|
logger.debug(`[SplitPanelContext] 항목 ID 초기화`);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🆕 좌측 선택 데이터 설정
|
||||||
|
*/
|
||||||
|
const handleSetSelectedLeftData = useCallback((data: Record<string, any> | null) => {
|
||||||
|
logger.info(`[SplitPanelContext] 좌측 선택 데이터 설정:`, {
|
||||||
|
hasData: !!data,
|
||||||
|
dataKeys: data ? Object.keys(data) : [],
|
||||||
|
});
|
||||||
|
setSelectedLeftData(data);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🆕 매핑된 부모 데이터 가져오기
|
||||||
|
* 우측 화면에서 저장 시 이 함수를 호출하여 부모 키 값을 가져옴
|
||||||
|
*/
|
||||||
|
const getMappedParentData = useCallback((): Record<string, any> => {
|
||||||
|
if (!selectedLeftData || parentDataMapping.length === 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappedData: Record<string, any> = {};
|
||||||
|
|
||||||
|
for (const mapping of parentDataMapping) {
|
||||||
|
const value = selectedLeftData[mapping.sourceColumn];
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
mappedData[mapping.targetColumn] = value;
|
||||||
|
logger.debug(`[SplitPanelContext] 부모 데이터 매핑: ${mapping.sourceColumn} → ${mapping.targetColumn} = ${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`[SplitPanelContext] 매핑된 부모 데이터:`, mappedData);
|
||||||
|
return mappedData;
|
||||||
|
}, [selectedLeftData, parentDataMapping]);
|
||||||
|
|
||||||
// 🆕 useMemo로 value 객체 메모이제이션 (무한 루프 방지)
|
// 🆕 useMemo로 value 객체 메모이제이션 (무한 루프 방지)
|
||||||
const value = React.useMemo<SplitPanelContextValue>(() => ({
|
const value = React.useMemo<SplitPanelContextValue>(() => ({
|
||||||
splitPanelId,
|
splitPanelId,
|
||||||
|
|
@ -247,6 +305,11 @@ export function SplitPanelProvider({
|
||||||
addItemIds,
|
addItemIds,
|
||||||
removeItemIds,
|
removeItemIds,
|
||||||
clearItemIds,
|
clearItemIds,
|
||||||
|
// 🆕 좌측 선택 데이터 관련
|
||||||
|
selectedLeftData,
|
||||||
|
setSelectedLeftData: handleSetSelectedLeftData,
|
||||||
|
parentDataMapping,
|
||||||
|
getMappedParentData,
|
||||||
}), [
|
}), [
|
||||||
splitPanelId,
|
splitPanelId,
|
||||||
leftScreenId,
|
leftScreenId,
|
||||||
|
|
@ -260,6 +323,10 @@ export function SplitPanelProvider({
|
||||||
addItemIds,
|
addItemIds,
|
||||||
removeItemIds,
|
removeItemIds,
|
||||||
clearItemIds,
|
clearItemIds,
|
||||||
|
selectedLeftData,
|
||||||
|
handleSetSelectedLeftData,
|
||||||
|
parentDataMapping,
|
||||||
|
getMappedParentData,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -692,6 +692,15 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
effectiveScreenId,
|
effectiveScreenId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 🆕 분할 패널 부모 데이터 가져오기 (우측 화면에서 저장 시 좌측 선택 데이터 포함)
|
||||||
|
let splitPanelParentData: Record<string, any> | undefined;
|
||||||
|
if (splitPanelContext && splitPanelPosition === "right") {
|
||||||
|
splitPanelParentData = splitPanelContext.getMappedParentData();
|
||||||
|
if (Object.keys(splitPanelParentData).length > 0) {
|
||||||
|
console.log("🔗 [ButtonPrimaryComponent] 분할 패널 부모 데이터 포함:", splitPanelParentData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const context: ButtonActionContext = {
|
const context: ButtonActionContext = {
|
||||||
formData: formData || {},
|
formData: formData || {},
|
||||||
originalData: originalData, // 🔧 빈 객체 대신 undefined 유지 (UPDATE 판단에 사용)
|
originalData: originalData, // 🔧 빈 객체 대신 undefined 유지 (UPDATE 판단에 사용)
|
||||||
|
|
@ -720,6 +729,8 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
flowSelectedStepId,
|
flowSelectedStepId,
|
||||||
// 🆕 컴포넌트별 설정 (parentDataMapping 등)
|
// 🆕 컴포넌트별 설정 (parentDataMapping 등)
|
||||||
componentConfigs,
|
componentConfigs,
|
||||||
|
// 🆕 분할 패널 부모 데이터 (좌측 화면에서 선택된 데이터)
|
||||||
|
splitPanelParentData,
|
||||||
} as ButtonActionContext;
|
} as ButtonActionContext;
|
||||||
|
|
||||||
// 확인이 필요한 액션인지 확인
|
// 확인이 필요한 액션인지 확인
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,9 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
||||||
const [loadedTableColumns, setLoadedTableColumns] = useState<any[]>([]);
|
const [loadedTableColumns, setLoadedTableColumns] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// 선택된 카드 상태
|
||||||
|
const [selectedCardId, setSelectedCardId] = useState<string | number | null>(null);
|
||||||
|
|
||||||
// 상세보기 모달 상태
|
// 상세보기 모달 상태
|
||||||
const [viewModalOpen, setViewModalOpen] = useState(false);
|
const [viewModalOpen, setViewModalOpen] = useState(false);
|
||||||
const [selectedData, setSelectedData] = useState<any>(null);
|
const [selectedData, setSelectedData] = useState<any>(null);
|
||||||
|
|
@ -261,26 +264,19 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
||||||
borderRadius: "12px", // 컨테이너 자체도 라운드 처리
|
borderRadius: "12px", // 컨테이너 자체도 라운드 처리
|
||||||
};
|
};
|
||||||
|
|
||||||
// 카드 스타일 - 통일된 디자인 시스템 적용
|
// 카드 스타일 - 컴팩트한 디자인
|
||||||
const cardStyle: React.CSSProperties = {
|
const cardStyle: React.CSSProperties = {
|
||||||
backgroundColor: "white",
|
backgroundColor: "white",
|
||||||
border: "2px solid #e5e7eb", // 더 명확한 테두리
|
border: "1px solid #e5e7eb",
|
||||||
borderRadius: "12px", // 통일된 라운드 처리
|
borderRadius: "8px",
|
||||||
padding: "24px", // 더 여유로운 패딩
|
padding: "16px",
|
||||||
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)", // 더 깊은 그림자
|
boxShadow: "0 1px 3px rgba(0, 0, 0, 0.08)",
|
||||||
transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)", // 부드러운 트랜지션
|
transition: "all 0.2s ease",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
minHeight: "240px", // 최소 높이 더 증가
|
|
||||||
cursor: isDesignMode ? "pointer" : "default",
|
cursor: isDesignMode ? "pointer" : "default",
|
||||||
// 호버 효과를 위한 추가 스타일
|
|
||||||
"&:hover": {
|
|
||||||
transform: "translateY(-2px)",
|
|
||||||
boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)",
|
|
||||||
borderColor: "#f59e0b", // 호버 시 오렌지 테두리
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 텍스트 자르기 함수
|
// 텍스트 자르기 함수
|
||||||
|
|
@ -328,6 +324,14 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCardClick = (data: any) => {
|
const handleCardClick = (data: any) => {
|
||||||
|
const cardId = data.id || data.objid || data.ID;
|
||||||
|
// 이미 선택된 카드를 다시 클릭하면 선택 해제
|
||||||
|
if (selectedCardId === cardId) {
|
||||||
|
setSelectedCardId(null);
|
||||||
|
} else {
|
||||||
|
setSelectedCardId(cardId);
|
||||||
|
}
|
||||||
|
|
||||||
if (componentConfig.onCardClick) {
|
if (componentConfig.onCardClick) {
|
||||||
componentConfig.onCardClick(data);
|
componentConfig.onCardClick(data);
|
||||||
}
|
}
|
||||||
|
|
@ -421,67 +425,75 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
||||||
? getColumnValue(data, componentConfig.columnMapping.imageColumn)
|
? getColumnValue(data, componentConfig.columnMapping.imageColumn)
|
||||||
: data.avatar || data.image || "";
|
: data.avatar || data.image || "";
|
||||||
|
|
||||||
|
const cardId = data.id || data.objid || data.ID || index;
|
||||||
|
const isCardSelected = selectedCardId === cardId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={data.id || index}
|
key={cardId}
|
||||||
style={cardStyle}
|
style={{
|
||||||
className="card-hover group cursor-pointer"
|
...cardStyle,
|
||||||
|
borderColor: isCardSelected ? "#000" : "#e5e7eb",
|
||||||
|
borderWidth: isCardSelected ? "2px" : "1px",
|
||||||
|
boxShadow: isCardSelected
|
||||||
|
? "0 4px 6px -1px rgba(0, 0, 0, 0.15)"
|
||||||
|
: "0 1px 3px rgba(0, 0, 0, 0.08)",
|
||||||
|
}}
|
||||||
|
className="card-hover group cursor-pointer transition-all duration-150"
|
||||||
onClick={() => handleCardClick(data)}
|
onClick={() => handleCardClick(data)}
|
||||||
>
|
>
|
||||||
{/* 카드 이미지 - 통일된 디자인 */}
|
{/* 카드 이미지 */}
|
||||||
{componentConfig.cardStyle?.showImage && componentConfig.columnMapping?.imageColumn && (
|
{componentConfig.cardStyle?.showImage && componentConfig.columnMapping?.imageColumn && (
|
||||||
<div className="mb-4 flex justify-center">
|
<div className="mb-2 flex justify-center">
|
||||||
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-gradient-to-br from-primary/10 to-primary/20 shadow-sm border-2 border-background">
|
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
|
||||||
<span className="text-2xl text-primary">👤</span>
|
<span className="text-lg text-primary">👤</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 카드 타이틀 - 통일된 디자인 */}
|
{/* 카드 타이틀 + 서브타이틀 (가로 배치) */}
|
||||||
{componentConfig.cardStyle?.showTitle && (
|
{(componentConfig.cardStyle?.showTitle || componentConfig.cardStyle?.showSubtitle) && (
|
||||||
<div className="mb-3">
|
<div className="mb-2 flex items-center gap-2 flex-wrap">
|
||||||
<h3 className="text-xl font-bold text-foreground leading-tight">{titleValue}</h3>
|
{componentConfig.cardStyle?.showTitle && (
|
||||||
|
<h3 className="text-base font-semibold text-foreground leading-tight">{titleValue}</h3>
|
||||||
|
)}
|
||||||
|
{componentConfig.cardStyle?.showSubtitle && subtitleValue && (
|
||||||
|
<span className="text-xs font-medium text-primary bg-primary/10 px-2 py-0.5 rounded-full">{subtitleValue}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 카드 서브타이틀 - 통일된 디자인 */}
|
{/* 카드 설명 */}
|
||||||
{componentConfig.cardStyle?.showSubtitle && (
|
|
||||||
<div className="mb-3">
|
|
||||||
<p className="text-sm font-semibold text-primary bg-primary/10 px-3 py-1 rounded-full inline-block">{subtitleValue}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 카드 설명 - 통일된 디자인 */}
|
|
||||||
{componentConfig.cardStyle?.showDescription && (
|
{componentConfig.cardStyle?.showDescription && (
|
||||||
<div className="mb-4 flex-1">
|
<div className="mb-2 flex-1">
|
||||||
<p className="text-sm leading-relaxed text-foreground bg-muted p-3 rounded-lg">
|
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||||
{truncateText(descriptionValue, componentConfig.cardStyle?.maxDescriptionLength || 100)}
|
{truncateText(descriptionValue, componentConfig.cardStyle?.maxDescriptionLength || 100)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 추가 표시 컬럼들 - 통일된 디자인 */}
|
{/* 추가 표시 컬럼들 - 가로 배치 */}
|
||||||
{componentConfig.columnMapping?.displayColumns &&
|
{componentConfig.columnMapping?.displayColumns &&
|
||||||
componentConfig.columnMapping.displayColumns.length > 0 && (
|
componentConfig.columnMapping.displayColumns.length > 0 && (
|
||||||
<div className="space-y-2 border-t border-border pt-4">
|
<div className="flex flex-wrap items-center gap-x-3 gap-y-0.5 border-t border-border pt-2 text-xs">
|
||||||
{componentConfig.columnMapping.displayColumns.map((columnName, idx) => {
|
{componentConfig.columnMapping.displayColumns.map((columnName, idx) => {
|
||||||
const value = getColumnValue(data, columnName);
|
const value = getColumnValue(data, columnName);
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={idx} className="flex justify-between items-center text-sm bg-background/50 px-3 py-2 rounded-lg border border-border">
|
<div key={idx} className="flex items-center gap-1">
|
||||||
<span className="text-muted-foreground font-medium capitalize">{getColumnLabel(columnName)}:</span>
|
<span className="text-muted-foreground">{getColumnLabel(columnName)}:</span>
|
||||||
<span className="font-semibold text-foreground bg-muted px-2 py-1 rounded-md text-xs">{value}</span>
|
<span className="font-medium text-foreground">{value}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 카드 액션 (선택사항) */}
|
{/* 카드 액션 */}
|
||||||
<div className="mt-3 flex justify-end space-x-2">
|
<div className="mt-2 flex justify-end space-x-2">
|
||||||
<button
|
<button
|
||||||
className="text-xs font-medium text-blue-600 hover:text-blue-800 transition-colors"
|
className="text-xs text-blue-600 hover:text-blue-800 transition-colors"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleCardView(data);
|
handleCardView(data);
|
||||||
|
|
@ -490,7 +502,7 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
||||||
상세보기
|
상세보기
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="text-xs font-medium text-muted-foreground hover:text-foreground transition-colors"
|
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleCardEdit(data);
|
handleCardEdit(data);
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,13 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Settings, Layout, ArrowRight, Database, Loader2, Check, ChevronsUpDown } from "lucide-react";
|
import { Settings, Layout, ArrowRight, Database, Loader2, Check, ChevronsUpDown, Plus, Trash2, Link2 } from "lucide-react";
|
||||||
import { screenApi } from "@/lib/api/screen";
|
import { screenApi } from "@/lib/api/screen";
|
||||||
|
import { getTableColumns } from "@/lib/api/tableManagement";
|
||||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { ParentDataMapping } from "@/contexts/SplitPanelContext";
|
||||||
|
|
||||||
interface ScreenSplitPanelConfigPanelProps {
|
interface ScreenSplitPanelConfigPanelProps {
|
||||||
config: any;
|
config: any;
|
||||||
|
|
@ -29,6 +31,10 @@ export function ScreenSplitPanelConfigPanel({ config = {}, onChange }: ScreenSpl
|
||||||
const [leftOpen, setLeftOpen] = useState(false);
|
const [leftOpen, setLeftOpen] = useState(false);
|
||||||
const [rightOpen, setRightOpen] = useState(false);
|
const [rightOpen, setRightOpen] = useState(false);
|
||||||
|
|
||||||
|
// 좌측 화면의 테이블 컬럼 목록
|
||||||
|
const [leftScreenColumns, setLeftScreenColumns] = useState<Array<{ columnName: string; columnLabel: string }>>([]);
|
||||||
|
const [isLoadingColumns, setIsLoadingColumns] = useState(false);
|
||||||
|
|
||||||
const [localConfig, setLocalConfig] = useState({
|
const [localConfig, setLocalConfig] = useState({
|
||||||
screenId: config.screenId || 0,
|
screenId: config.screenId || 0,
|
||||||
leftScreenId: config.leftScreenId || 0,
|
leftScreenId: config.leftScreenId || 0,
|
||||||
|
|
@ -37,6 +43,7 @@ export function ScreenSplitPanelConfigPanel({ config = {}, onChange }: ScreenSpl
|
||||||
resizable: config.resizable ?? true,
|
resizable: config.resizable ?? true,
|
||||||
buttonLabel: config.buttonLabel || "데이터 전달",
|
buttonLabel: config.buttonLabel || "데이터 전달",
|
||||||
buttonPosition: config.buttonPosition || "center",
|
buttonPosition: config.buttonPosition || "center",
|
||||||
|
parentDataMapping: config.parentDataMapping || [] as ParentDataMapping[],
|
||||||
...config,
|
...config,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -51,10 +58,51 @@ export function ScreenSplitPanelConfigPanel({ config = {}, onChange }: ScreenSpl
|
||||||
resizable: config.resizable ?? true,
|
resizable: config.resizable ?? true,
|
||||||
buttonLabel: config.buttonLabel || "데이터 전달",
|
buttonLabel: config.buttonLabel || "데이터 전달",
|
||||||
buttonPosition: config.buttonPosition || "center",
|
buttonPosition: config.buttonPosition || "center",
|
||||||
|
parentDataMapping: config.parentDataMapping || [],
|
||||||
...config,
|
...config,
|
||||||
});
|
});
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
|
||||||
|
// 좌측 화면이 변경되면 해당 화면의 테이블 컬럼 로드
|
||||||
|
useEffect(() => {
|
||||||
|
const loadLeftScreenColumns = async () => {
|
||||||
|
if (!localConfig.leftScreenId) {
|
||||||
|
setLeftScreenColumns([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoadingColumns(true);
|
||||||
|
|
||||||
|
// 좌측 화면 정보 조회
|
||||||
|
const screenData = await screenApi.getScreen(localConfig.leftScreenId);
|
||||||
|
if (!screenData?.tableName) {
|
||||||
|
console.warn("좌측 화면에 테이블이 설정되지 않았습니다.");
|
||||||
|
setLeftScreenColumns([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 테이블 컬럼 조회
|
||||||
|
const columnsResponse = await getTableColumns(screenData.tableName);
|
||||||
|
if (columnsResponse.success && columnsResponse.data?.columns) {
|
||||||
|
const columns = columnsResponse.data.columns.map((col: any) => ({
|
||||||
|
columnName: col.column_name || col.columnName,
|
||||||
|
columnLabel: col.column_label || col.columnLabel || col.column_name || col.columnName,
|
||||||
|
}));
|
||||||
|
setLeftScreenColumns(columns);
|
||||||
|
console.log("📋 좌측 화면 컬럼 로드 완료:", columns.length);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("좌측 화면 컬럼 로드 실패:", error);
|
||||||
|
setLeftScreenColumns([]);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingColumns(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadLeftScreenColumns();
|
||||||
|
}, [localConfig.leftScreenId]);
|
||||||
|
|
||||||
// 화면 목록 로드
|
// 화면 목록 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadScreens = async () => {
|
const loadScreens = async () => {
|
||||||
|
|
@ -94,10 +142,36 @@ export function ScreenSplitPanelConfigPanel({ config = {}, onChange }: ScreenSpl
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 부모 데이터 매핑 추가
|
||||||
|
const addParentDataMapping = () => {
|
||||||
|
const newMapping: ParentDataMapping = {
|
||||||
|
sourceColumn: "",
|
||||||
|
targetColumn: "",
|
||||||
|
};
|
||||||
|
const newMappings = [...(localConfig.parentDataMapping || []), newMapping];
|
||||||
|
updateConfig("parentDataMapping", newMappings);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 부모 데이터 매핑 수정
|
||||||
|
const updateParentDataMapping = (index: number, field: keyof ParentDataMapping, value: string) => {
|
||||||
|
const newMappings = [...(localConfig.parentDataMapping || [])];
|
||||||
|
newMappings[index] = {
|
||||||
|
...newMappings[index],
|
||||||
|
[field]: value,
|
||||||
|
};
|
||||||
|
updateConfig("parentDataMapping", newMappings);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 부모 데이터 매핑 삭제
|
||||||
|
const removeParentDataMapping = (index: number) => {
|
||||||
|
const newMappings = (localConfig.parentDataMapping || []).filter((_: any, i: number) => i !== index);
|
||||||
|
updateConfig("parentDataMapping", newMappings);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Tabs defaultValue="layout" className="w-full">
|
<Tabs defaultValue="layout" className="w-full">
|
||||||
<TabsList className="grid w-full grid-cols-2">
|
<TabsList className="grid w-full grid-cols-3">
|
||||||
<TabsTrigger value="layout" className="gap-2">
|
<TabsTrigger value="layout" className="gap-2">
|
||||||
<Layout className="h-4 w-4" />
|
<Layout className="h-4 w-4" />
|
||||||
레이아웃
|
레이아웃
|
||||||
|
|
@ -106,6 +180,10 @@ export function ScreenSplitPanelConfigPanel({ config = {}, onChange }: ScreenSpl
|
||||||
<Database className="h-4 w-4" />
|
<Database className="h-4 w-4" />
|
||||||
화면 설정
|
화면 설정
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="dataMapping" className="gap-2">
|
||||||
|
<Link2 className="h-4 w-4" />
|
||||||
|
데이터 전달
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
{/* 레이아웃 탭 */}
|
{/* 레이아웃 탭 */}
|
||||||
|
|
@ -295,7 +373,7 @@ export function ScreenSplitPanelConfigPanel({ config = {}, onChange }: ScreenSpl
|
||||||
|
|
||||||
<div className="rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-900 dark:bg-amber-950">
|
<div className="rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-900 dark:bg-amber-950">
|
||||||
<p className="text-xs text-amber-800 dark:text-amber-200">
|
<p className="text-xs text-amber-800 dark:text-amber-200">
|
||||||
💡 <strong>데이터 전달 방법:</strong> 좌측 화면에 테이블과 버튼을 배치하고, 버튼의 액션을
|
데이터 전달 방법: 좌측 화면에 테이블과 버튼을 배치하고, 버튼의 액션을
|
||||||
"transferData"로 설정하세요.
|
"transferData"로 설정하세요.
|
||||||
<br />
|
<br />
|
||||||
버튼 설정에서 소스 컴포넌트(테이블), 타겟 화면, 필드 매핑을 지정할 수 있습니다.
|
버튼 설정에서 소스 컴포넌트(테이블), 타겟 화면, 필드 매핑을 지정할 수 있습니다.
|
||||||
|
|
@ -306,6 +384,119 @@ export function ScreenSplitPanelConfigPanel({ config = {}, onChange }: ScreenSpl
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* 데이터 전달 탭 */}
|
||||||
|
<TabsContent value="dataMapping" className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">부모 데이터 자동 전달</CardTitle>
|
||||||
|
<CardDescription className="text-xs">
|
||||||
|
좌측 화면에서 행을 선택하면, 우측 화면의 추가/저장 시 지정된 컬럼 값이 자동으로 포함됩니다.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{!localConfig.leftScreenId ? (
|
||||||
|
<div className="rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-900 dark:bg-amber-950">
|
||||||
|
<p className="text-xs text-amber-800 dark:text-amber-200">
|
||||||
|
먼저 "화면 설정" 탭에서 좌측 화면을 선택하세요.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : isLoadingColumns ? (
|
||||||
|
<div className="flex items-center justify-center py-4">
|
||||||
|
<Loader2 className="text-muted-foreground h-5 w-5 animate-spin" />
|
||||||
|
<span className="text-muted-foreground ml-2 text-xs">컬럼 정보 로딩 중...</span>
|
||||||
|
</div>
|
||||||
|
) : leftScreenColumns.length === 0 ? (
|
||||||
|
<div className="rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-900 dark:bg-amber-950">
|
||||||
|
<p className="text-xs text-amber-800 dark:text-amber-200">
|
||||||
|
좌측 화면에 테이블이 설정되지 않았거나 컬럼 정보를 불러올 수 없습니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* 매핑 목록 */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
{(localConfig.parentDataMapping || []).map((mapping: ParentDataMapping, index: number) => (
|
||||||
|
<div key={index} className="flex items-center gap-2 rounded-lg border bg-gray-50 p-3 dark:bg-gray-900">
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Label className="text-xs text-gray-600">소스 컬럼 (좌측)</Label>
|
||||||
|
<Select
|
||||||
|
value={mapping.sourceColumn}
|
||||||
|
onValueChange={(value) => updateParentDataMapping(index, "sourceColumn", value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 text-xs">
|
||||||
|
<SelectValue placeholder="컬럼 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{leftScreenColumns.map((col) => (
|
||||||
|
<SelectItem key={col.columnName} value={col.columnName} className="text-xs">
|
||||||
|
{col.columnLabel} ({col.columnName})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<ArrowRight className="mt-5 h-4 w-4 text-gray-400" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<Label className="text-xs text-gray-600">타겟 컬럼 (우측 저장 시)</Label>
|
||||||
|
<Input
|
||||||
|
value={mapping.targetColumn}
|
||||||
|
onChange={(e) => updateParentDataMapping(index, "targetColumn", e.target.value)}
|
||||||
|
placeholder="저장할 컬럼명"
|
||||||
|
className="h-8 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => removeParentDataMapping(index)}
|
||||||
|
className="h-8 w-8 p-0 text-red-500 hover:bg-red-50 hover:text-red-600"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 매핑 추가 버튼 */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={addParentDataMapping}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
매핑 추가
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 안내 메시지 */}
|
||||||
|
<div className="rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-900 dark:bg-blue-950">
|
||||||
|
<p className="text-xs text-blue-800 dark:text-blue-200">
|
||||||
|
<strong>사용 예시:</strong>
|
||||||
|
<br />
|
||||||
|
좌측: 설비 목록 (equipment_mng)
|
||||||
|
<br />
|
||||||
|
우측: 점검항목 추가 화면
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
매핑 설정:
|
||||||
|
<br />
|
||||||
|
- 소스: equipment_code → 타겟: equipment_code
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
좌측에서 설비를 선택하고 우측에서 점검항목을 추가하면,
|
||||||
|
선택한 설비의 equipment_code가 자동으로 저장됩니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
{/* 설정 요약 */}
|
{/* 설정 요약 */}
|
||||||
|
|
@ -343,6 +534,14 @@ export function ScreenSplitPanelConfigPanel({ config = {}, onChange }: ScreenSpl
|
||||||
<span className="text-muted-foreground">크기 조절:</span>
|
<span className="text-muted-foreground">크기 조절:</span>
|
||||||
<span className="font-medium">{localConfig.resizable ? "가능" : "불가능"}</span>
|
<span className="font-medium">{localConfig.resizable ? "가능" : "불가능"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">데이터 매핑:</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{(localConfig.parentDataMapping || []).length > 0
|
||||||
|
? `${localConfig.parentDataMapping.length}개 설정`
|
||||||
|
: "미설정"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -1466,6 +1466,22 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
|
|
||||||
handleRowSelection(rowKey, !isCurrentlySelected);
|
handleRowSelection(rowKey, !isCurrentlySelected);
|
||||||
|
|
||||||
|
// 🆕 분할 패널 컨텍스트에 선택된 데이터 저장 (좌측 화면인 경우)
|
||||||
|
if (splitPanelContext && splitPanelPosition === "left") {
|
||||||
|
if (!isCurrentlySelected) {
|
||||||
|
// 선택된 경우: 데이터 저장
|
||||||
|
splitPanelContext.setSelectedLeftData(row);
|
||||||
|
console.log("🔗 [TableList] 분할 패널 좌측 데이터 저장:", {
|
||||||
|
row,
|
||||||
|
parentDataMapping: splitPanelContext.parentDataMapping,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 선택 해제된 경우: 데이터 초기화
|
||||||
|
splitPanelContext.setSelectedLeftData(null);
|
||||||
|
console.log("🔗 [TableList] 분할 패널 좌측 데이터 초기화");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log("행 클릭:", { row, index, isSelected: !isCurrentlySelected });
|
console.log("행 클릭:", { row, index, isSelected: !isCurrentlySelected });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -215,6 +215,9 @@ export interface ButtonActionContext {
|
||||||
|
|
||||||
// 🆕 컴포넌트별 설정 (parentDataMapping 등)
|
// 🆕 컴포넌트별 설정 (parentDataMapping 등)
|
||||||
componentConfigs?: Record<string, any>; // 컴포넌트 ID → 컴포넌트 설정
|
componentConfigs?: Record<string, any>; // 컴포넌트 ID → 컴포넌트 설정
|
||||||
|
|
||||||
|
// 🆕 분할 패널 부모 데이터 (좌측 화면에서 선택된 데이터)
|
||||||
|
splitPanelParentData?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -502,8 +505,15 @@ export class ButtonActionExecutor {
|
||||||
// console.log("✅ 채번 규칙 할당 완료");
|
// console.log("✅ 채번 규칙 할당 완료");
|
||||||
// console.log("📦 최종 formData:", JSON.stringify(formData, null, 2));
|
// console.log("📦 최종 formData:", JSON.stringify(formData, null, 2));
|
||||||
|
|
||||||
|
// 🆕 분할 패널 부모 데이터 병합 (좌측 화면에서 선택된 데이터)
|
||||||
|
const splitPanelData = context.splitPanelParentData || {};
|
||||||
|
if (Object.keys(splitPanelData).length > 0) {
|
||||||
|
console.log("🔗 [handleSave] 분할 패널 부모 데이터 병합:", splitPanelData);
|
||||||
|
}
|
||||||
|
|
||||||
const dataWithUserInfo = {
|
const dataWithUserInfo = {
|
||||||
...formData,
|
...splitPanelData, // 분할 패널 부모 데이터 먼저 적용
|
||||||
|
...formData, // 폼 데이터가 우선 (덮어쓰기 가능)
|
||||||
writer: formData.writer || writerValue, // ✅ 입력값 우선, 없으면 userId
|
writer: formData.writer || writerValue, // ✅ 입력값 우선, 없으면 userId
|
||||||
created_by: writerValue, // created_by는 항상 로그인한 사람
|
created_by: writerValue, // created_by는 항상 로그인한 사람
|
||||||
updated_by: writerValue, // updated_by는 항상 로그인한 사람
|
updated_by: writerValue, // updated_by는 항상 로그인한 사람
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue