카드 컴포넌트 중간커밋
This commit is contained in:
parent
617655a42a
commit
fb16e224f0
|
|
@ -17,6 +17,7 @@ import { toast } from "sonner";
|
|||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
|
||||
import { TableSearchWidgetHeightProvider } from "@/contexts/TableSearchWidgetHeightContext";
|
||||
import { useSplitPanelContext } from "@/contexts/SplitPanelContext";
|
||||
|
||||
interface ScreenModalState {
|
||||
isOpen: boolean;
|
||||
|
|
@ -32,6 +33,7 @@ interface ScreenModalProps {
|
|||
|
||||
export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||
const { userId, userName, user } = useAuth();
|
||||
const splitPanelContext = useSplitPanelContext();
|
||||
|
||||
const [modalState, setModalState] = useState<ScreenModalState>({
|
||||
isOpen: false,
|
||||
|
|
@ -152,6 +154,14 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
setFormData(editData);
|
||||
setOriginalData(editData); // 🆕 원본 데이터 저장 (UPDATE 판단용)
|
||||
} else {
|
||||
// 🆕 신규 등록 모드: 분할 패널 부모 데이터가 있으면 미리 설정
|
||||
const parentData = splitPanelContext?.getMappedParentData() || {};
|
||||
if (Object.keys(parentData).length > 0) {
|
||||
console.log("🔗 [ScreenModal] 분할 패널 부모 데이터 초기값 설정:", parentData);
|
||||
setFormData(parentData);
|
||||
} else {
|
||||
setFormData({});
|
||||
}
|
||||
setOriginalData(null); // 신규 등록 모드
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export function ScreenSplitPanel({ screenId, config, initialFormData }: ScreenSp
|
|||
leftScreenId: config?.leftScreenId,
|
||||
rightScreenId: config?.rightScreenId,
|
||||
configSplitRatio,
|
||||
parentDataMapping: config?.parentDataMapping,
|
||||
configKeys: config ? Object.keys(config) : [],
|
||||
});
|
||||
|
||||
|
|
@ -125,6 +126,7 @@ export function ScreenSplitPanel({ screenId, config, initialFormData }: ScreenSp
|
|||
splitPanelId={splitPanelId}
|
||||
leftScreenId={config?.leftScreenId || null}
|
||||
rightScreenId={config?.rightScreenId || null}
|
||||
parentDataMapping={config?.parentDataMapping || []}
|
||||
>
|
||||
<div className="flex h-full">
|
||||
{/* 좌측 패널 */}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,15 @@ export interface SplitPanelDataReceiver {
|
|||
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;
|
||||
removeItemIds: (ids: string[]) => 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);
|
||||
|
|
@ -62,6 +81,7 @@ interface SplitPanelProviderProps {
|
|||
splitPanelId: string;
|
||||
leftScreenId: number | null;
|
||||
rightScreenId: number | null;
|
||||
parentDataMapping?: ParentDataMapping[]; // 🆕 부모 데이터 매핑 설정
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
|
|
@ -72,6 +92,7 @@ export function SplitPanelProvider({
|
|||
splitPanelId,
|
||||
leftScreenId,
|
||||
rightScreenId,
|
||||
parentDataMapping = [],
|
||||
children,
|
||||
}: SplitPanelProviderProps) {
|
||||
// 좌측/우측 화면의 데이터 수신자 맵
|
||||
|
|
@ -83,6 +104,9 @@ export function SplitPanelProvider({
|
|||
|
||||
// 🆕 우측에 추가된 항목 ID 상태
|
||||
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 초기화`);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 🆕 좌측 선택 데이터 설정
|
||||
*/
|
||||
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 객체 메모이제이션 (무한 루프 방지)
|
||||
const value = React.useMemo<SplitPanelContextValue>(() => ({
|
||||
splitPanelId,
|
||||
|
|
@ -247,6 +305,11 @@ export function SplitPanelProvider({
|
|||
addItemIds,
|
||||
removeItemIds,
|
||||
clearItemIds,
|
||||
// 🆕 좌측 선택 데이터 관련
|
||||
selectedLeftData,
|
||||
setSelectedLeftData: handleSetSelectedLeftData,
|
||||
parentDataMapping,
|
||||
getMappedParentData,
|
||||
}), [
|
||||
splitPanelId,
|
||||
leftScreenId,
|
||||
|
|
@ -260,6 +323,10 @@ export function SplitPanelProvider({
|
|||
addItemIds,
|
||||
removeItemIds,
|
||||
clearItemIds,
|
||||
selectedLeftData,
|
||||
handleSetSelectedLeftData,
|
||||
parentDataMapping,
|
||||
getMappedParentData,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -692,6 +692,15 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
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 = {
|
||||
formData: formData || {},
|
||||
originalData: originalData, // 🔧 빈 객체 대신 undefined 유지 (UPDATE 판단에 사용)
|
||||
|
|
@ -720,6 +729,8 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
flowSelectedStepId,
|
||||
// 🆕 컴포넌트별 설정 (parentDataMapping 등)
|
||||
componentConfigs,
|
||||
// 🆕 분할 패널 부모 데이터 (좌측 화면에서 선택된 데이터)
|
||||
splitPanelParentData,
|
||||
} as ButtonActionContext;
|
||||
|
||||
// 확인이 필요한 액션인지 확인
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
|||
const [loadedTableColumns, setLoadedTableColumns] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 선택된 카드 상태
|
||||
const [selectedCardId, setSelectedCardId] = useState<string | number | null>(null);
|
||||
|
||||
// 상세보기 모달 상태
|
||||
const [viewModalOpen, setViewModalOpen] = useState(false);
|
||||
const [selectedData, setSelectedData] = useState<any>(null);
|
||||
|
|
@ -261,26 +264,19 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
|||
borderRadius: "12px", // 컨테이너 자체도 라운드 처리
|
||||
};
|
||||
|
||||
// 카드 스타일 - 통일된 디자인 시스템 적용
|
||||
// 카드 스타일 - 컴팩트한 디자인
|
||||
const cardStyle: React.CSSProperties = {
|
||||
backgroundColor: "white",
|
||||
border: "2px solid #e5e7eb", // 더 명확한 테두리
|
||||
borderRadius: "12px", // 통일된 라운드 처리
|
||||
padding: "24px", // 더 여유로운 패딩
|
||||
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)", // 더 깊은 그림자
|
||||
transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)", // 부드러운 트랜지션
|
||||
border: "1px solid #e5e7eb",
|
||||
borderRadius: "8px",
|
||||
padding: "16px",
|
||||
boxShadow: "0 1px 3px rgba(0, 0, 0, 0.08)",
|
||||
transition: "all 0.2s ease",
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
position: "relative",
|
||||
minHeight: "240px", // 최소 높이 더 증가
|
||||
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 cardId = data.id || data.objid || data.ID;
|
||||
// 이미 선택된 카드를 다시 클릭하면 선택 해제
|
||||
if (selectedCardId === cardId) {
|
||||
setSelectedCardId(null);
|
||||
} else {
|
||||
setSelectedCardId(cardId);
|
||||
}
|
||||
|
||||
if (componentConfig.onCardClick) {
|
||||
componentConfig.onCardClick(data);
|
||||
}
|
||||
|
|
@ -421,67 +425,75 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
|||
? getColumnValue(data, componentConfig.columnMapping.imageColumn)
|
||||
: data.avatar || data.image || "";
|
||||
|
||||
const cardId = data.id || data.objid || data.ID || index;
|
||||
const isCardSelected = selectedCardId === cardId;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={data.id || index}
|
||||
style={cardStyle}
|
||||
className="card-hover group cursor-pointer"
|
||||
key={cardId}
|
||||
style={{
|
||||
...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)}
|
||||
>
|
||||
{/* 카드 이미지 - 통일된 디자인 */}
|
||||
{/* 카드 이미지 */}
|
||||
{componentConfig.cardStyle?.showImage && componentConfig.columnMapping?.imageColumn && (
|
||||
<div className="mb-4 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">
|
||||
<span className="text-2xl text-primary">👤</span>
|
||||
<div className="mb-2 flex justify-center">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
|
||||
<span className="text-lg text-primary">👤</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 카드 타이틀 - 통일된 디자인 */}
|
||||
{componentConfig.cardStyle?.showTitle && (
|
||||
<div className="mb-3">
|
||||
<h3 className="text-xl font-bold text-foreground leading-tight">{titleValue}</h3>
|
||||
{/* 카드 타이틀 + 서브타이틀 (가로 배치) */}
|
||||
{(componentConfig.cardStyle?.showTitle || componentConfig.cardStyle?.showSubtitle) && (
|
||||
<div className="mb-2 flex items-center gap-2 flex-wrap">
|
||||
{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>
|
||||
)}
|
||||
|
||||
{/* 카드 서브타이틀 - 통일된 디자인 */}
|
||||
{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 && (
|
||||
<div className="mb-4 flex-1">
|
||||
<p className="text-sm leading-relaxed text-foreground bg-muted p-3 rounded-lg">
|
||||
<div className="mb-2 flex-1">
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||
{truncateText(descriptionValue, componentConfig.cardStyle?.maxDescriptionLength || 100)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 추가 표시 컬럼들 - 통일된 디자인 */}
|
||||
{/* 추가 표시 컬럼들 - 가로 배치 */}
|
||||
{componentConfig.columnMapping?.displayColumns &&
|
||||
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) => {
|
||||
const value = getColumnValue(data, columnName);
|
||||
if (!value) return null;
|
||||
|
||||
return (
|
||||
<div key={idx} className="flex justify-between items-center text-sm bg-background/50 px-3 py-2 rounded-lg border border-border">
|
||||
<span className="text-muted-foreground font-medium capitalize">{getColumnLabel(columnName)}:</span>
|
||||
<span className="font-semibold text-foreground bg-muted px-2 py-1 rounded-md text-xs">{value}</span>
|
||||
<div key={idx} className="flex items-center gap-1">
|
||||
<span className="text-muted-foreground">{getColumnLabel(columnName)}:</span>
|
||||
<span className="font-medium text-foreground">{value}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 카드 액션 (선택사항) */}
|
||||
<div className="mt-3 flex justify-end space-x-2">
|
||||
{/* 카드 액션 */}
|
||||
<div className="mt-2 flex justify-end space-x-2">
|
||||
<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) => {
|
||||
e.stopPropagation();
|
||||
handleCardView(data);
|
||||
|
|
@ -490,7 +502,7 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
|||
상세보기
|
||||
</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) => {
|
||||
e.stopPropagation();
|
||||
handleCardEdit(data);
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
|||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
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 { getTableColumns } from "@/lib/api/tableManagement";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ParentDataMapping } from "@/contexts/SplitPanelContext";
|
||||
|
||||
interface ScreenSplitPanelConfigPanelProps {
|
||||
config: any;
|
||||
|
|
@ -29,6 +31,10 @@ export function ScreenSplitPanelConfigPanel({ config = {}, onChange }: ScreenSpl
|
|||
const [leftOpen, setLeftOpen] = 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({
|
||||
screenId: config.screenId || 0,
|
||||
leftScreenId: config.leftScreenId || 0,
|
||||
|
|
@ -37,6 +43,7 @@ export function ScreenSplitPanelConfigPanel({ config = {}, onChange }: ScreenSpl
|
|||
resizable: config.resizable ?? true,
|
||||
buttonLabel: config.buttonLabel || "데이터 전달",
|
||||
buttonPosition: config.buttonPosition || "center",
|
||||
parentDataMapping: config.parentDataMapping || [] as ParentDataMapping[],
|
||||
...config,
|
||||
});
|
||||
|
||||
|
|
@ -51,10 +58,51 @@ export function ScreenSplitPanelConfigPanel({ config = {}, onChange }: ScreenSpl
|
|||
resizable: config.resizable ?? true,
|
||||
buttonLabel: config.buttonLabel || "데이터 전달",
|
||||
buttonPosition: config.buttonPosition || "center",
|
||||
parentDataMapping: config.parentDataMapping || [],
|
||||
...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(() => {
|
||||
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 (
|
||||
<div className="space-y-4">
|
||||
<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">
|
||||
<Layout className="h-4 w-4" />
|
||||
레이아웃
|
||||
|
|
@ -106,6 +180,10 @@ export function ScreenSplitPanelConfigPanel({ config = {}, onChange }: ScreenSpl
|
|||
<Database className="h-4 w-4" />
|
||||
화면 설정
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="dataMapping" className="gap-2">
|
||||
<Link2 className="h-4 w-4" />
|
||||
데이터 전달
|
||||
</TabsTrigger>
|
||||
</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">
|
||||
<p className="text-xs text-amber-800 dark:text-amber-200">
|
||||
💡 <strong>데이터 전달 방법:</strong> 좌측 화면에 테이블과 버튼을 배치하고, 버튼의 액션을
|
||||
데이터 전달 방법: 좌측 화면에 테이블과 버튼을 배치하고, 버튼의 액션을
|
||||
"transferData"로 설정하세요.
|
||||
<br />
|
||||
버튼 설정에서 소스 컴포넌트(테이블), 타겟 화면, 필드 매핑을 지정할 수 있습니다.
|
||||
|
|
@ -306,6 +384,119 @@ export function ScreenSplitPanelConfigPanel({ config = {}, onChange }: ScreenSpl
|
|||
</CardContent>
|
||||
</Card>
|
||||
</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>
|
||||
|
||||
{/* 설정 요약 */}
|
||||
|
|
@ -343,6 +534,14 @@ export function ScreenSplitPanelConfigPanel({ config = {}, onChange }: ScreenSpl
|
|||
<span className="text-muted-foreground">크기 조절:</span>
|
||||
<span className="font-medium">{localConfig.resizable ? "가능" : "불가능"}</span>
|
||||
</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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -1466,6 +1466,22 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
|
||||
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 });
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -215,6 +215,9 @@ export interface ButtonActionContext {
|
|||
|
||||
// 🆕 컴포넌트별 설정 (parentDataMapping 등)
|
||||
componentConfigs?: Record<string, any>; // 컴포넌트 ID → 컴포넌트 설정
|
||||
|
||||
// 🆕 분할 패널 부모 데이터 (좌측 화면에서 선택된 데이터)
|
||||
splitPanelParentData?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -502,8 +505,15 @@ export class ButtonActionExecutor {
|
|||
// console.log("✅ 채번 규칙 할당 완료");
|
||||
// console.log("📦 최종 formData:", JSON.stringify(formData, null, 2));
|
||||
|
||||
// 🆕 분할 패널 부모 데이터 병합 (좌측 화면에서 선택된 데이터)
|
||||
const splitPanelData = context.splitPanelParentData || {};
|
||||
if (Object.keys(splitPanelData).length > 0) {
|
||||
console.log("🔗 [handleSave] 분할 패널 부모 데이터 병합:", splitPanelData);
|
||||
}
|
||||
|
||||
const dataWithUserInfo = {
|
||||
...formData,
|
||||
...splitPanelData, // 분할 패널 부모 데이터 먼저 적용
|
||||
...formData, // 폼 데이터가 우선 (덮어쓰기 가능)
|
||||
writer: formData.writer || writerValue, // ✅ 입력값 우선, 없으면 userId
|
||||
created_by: writerValue, // created_by는 항상 로그인한 사람
|
||||
updated_by: writerValue, // updated_by는 항상 로그인한 사람
|
||||
|
|
|
|||
Loading…
Reference in New Issue