chpark-sync #425
|
|
@ -40,32 +40,33 @@ export const EmbeddedScreen = forwardRef<EmbeddedScreenHandle, EmbeddedScreenPro
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [screenInfo, setScreenInfo] = useState<any>(null);
|
const [screenInfo, setScreenInfo] = useState<any>(null);
|
||||||
const [formData, setFormData] = useState<Record<string, any>>(initialFormData || {}); // 🆕 초기 데이터로 시작
|
const [formData, setFormData] = useState<Record<string, any>>(initialFormData || {}); // 🆕 초기 데이터로 시작
|
||||||
|
const [formDataVersion, setFormDataVersion] = useState(0); // 🆕 폼 데이터 버전 (강제 리렌더링용)
|
||||||
|
|
||||||
// 컴포넌트 참조 맵
|
// 컴포넌트 참조 맵
|
||||||
const componentRefs = useRef<Map<string, DataReceivable>>(new Map());
|
const componentRefs = useRef<Map<string, DataReceivable>>(new Map());
|
||||||
|
|
||||||
// 분할 패널 컨텍스트 (분할 패널 내부에 있을 때만 사용)
|
// 분할 패널 컨텍스트 (분할 패널 내부에 있을 때만 사용)
|
||||||
const splitPanelContext = useSplitPanelContext();
|
const splitPanelContext = useSplitPanelContext();
|
||||||
|
|
||||||
// 🆕 사용자 정보 가져오기 (저장 액션에 필요)
|
// 🆕 사용자 정보 가져오기 (저장 액션에 필요)
|
||||||
const { userId, userName, companyCode } = useAuth();
|
const { userId, userName, companyCode } = useAuth();
|
||||||
|
|
||||||
// 컴포넌트들의 실제 영역 계산 (가로폭 맞춤을 위해)
|
// 컴포넌트들의 실제 영역 계산 (가로폭 맞춤을 위해)
|
||||||
const contentBounds = React.useMemo(() => {
|
const contentBounds = React.useMemo(() => {
|
||||||
if (layout.length === 0) return { width: 0, height: 0 };
|
if (layout.length === 0) return { width: 0, height: 0 };
|
||||||
|
|
||||||
let maxRight = 0;
|
let maxRight = 0;
|
||||||
let maxBottom = 0;
|
let maxBottom = 0;
|
||||||
|
|
||||||
layout.forEach((component) => {
|
layout.forEach((component) => {
|
||||||
const { position: compPosition = { x: 0, y: 0 }, size = { width: 200, height: 40 } } = component;
|
const { position: compPosition = { x: 0, y: 0 }, size = { width: 200, height: 40 } } = component;
|
||||||
const right = (compPosition.x || 0) + (size.width || 200);
|
const right = (compPosition.x || 0) + (size.width || 200);
|
||||||
const bottom = (compPosition.y || 0) + (size.height || 40);
|
const bottom = (compPosition.y || 0) + (size.height || 40);
|
||||||
|
|
||||||
if (right > maxRight) maxRight = right;
|
if (right > maxRight) maxRight = right;
|
||||||
if (bottom > maxBottom) maxBottom = bottom;
|
if (bottom > maxBottom) maxBottom = bottom;
|
||||||
});
|
});
|
||||||
|
|
||||||
return { width: maxRight, height: maxBottom };
|
return { width: maxRight, height: maxBottom };
|
||||||
}, [layout]);
|
}, [layout]);
|
||||||
|
|
||||||
|
|
@ -92,26 +93,49 @@ export const EmbeddedScreen = forwardRef<EmbeddedScreenHandle, EmbeddedScreenPro
|
||||||
}, [initialFormData]);
|
}, [initialFormData]);
|
||||||
|
|
||||||
// 🆕 분할 패널에서 좌측 선택 데이터가 변경되면 우측 화면의 formData에 자동 반영
|
// 🆕 분할 패널에서 좌측 선택 데이터가 변경되면 우측 화면의 formData에 자동 반영
|
||||||
// disableAutoDataTransfer가 true이면 자동 전달 비활성화 (버튼 클릭으로만 전달)
|
// 🆕 좌측 선택 데이터 (분할 패널 컨텍스트에서 직접 참조)
|
||||||
|
const selectedLeftData = splitPanelContext?.selectedLeftData;
|
||||||
|
|
||||||
|
// 🆕 좌측 선택 데이터가 변경되면 우측 formData를 업데이트
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 우측 화면인 경우에만 적용
|
// 우측 화면인 경우에만 적용
|
||||||
if (position !== "right" || !splitPanelContext) return;
|
if (position !== "right" || !splitPanelContext) {
|
||||||
|
|
||||||
// 자동 데이터 전달이 비활성화된 경우 스킵
|
|
||||||
if (splitPanelContext.disableAutoDataTransfer) {
|
|
||||||
console.log("🔗 [EmbeddedScreen] 자동 데이터 전달 비활성화됨 - 버튼 클릭으로만 전달");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mappedData = splitPanelContext.getMappedParentData();
|
// 자동 데이터 전달이 비활성화된 경우 스킵
|
||||||
if (Object.keys(mappedData).length > 0) {
|
if (splitPanelContext.disableAutoDataTransfer) {
|
||||||
console.log("🔗 [EmbeddedScreen] 분할 패널 부모 데이터 자동 반영:", mappedData);
|
return;
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
...mappedData,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}, [position, splitPanelContext, splitPanelContext?.selectedLeftData]);
|
|
||||||
|
// 🆕 현재 화면의 모든 컴포넌트에서 columnName 수집
|
||||||
|
const allColumnNames = layout.filter((comp) => comp.columnName).map((comp) => comp.columnName as string);
|
||||||
|
|
||||||
|
// 🆕 모든 필드를 빈 값으로 초기화한 후, selectedLeftData로 덮어쓰기
|
||||||
|
const initializedFormData: Record<string, any> = {};
|
||||||
|
|
||||||
|
// 먼저 모든 컬럼을 빈 문자열로 초기화
|
||||||
|
allColumnNames.forEach((colName) => {
|
||||||
|
initializedFormData[colName] = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
// selectedLeftData가 있으면 해당 값으로 덮어쓰기
|
||||||
|
if (selectedLeftData && Object.keys(selectedLeftData).length > 0) {
|
||||||
|
Object.keys(selectedLeftData).forEach((key) => {
|
||||||
|
// null/undefined는 빈 문자열로, 나머지는 그대로
|
||||||
|
initializedFormData[key] = selectedLeftData[key] ?? "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🔗 [EmbeddedScreen] 우측 폼 데이터 교체:", {
|
||||||
|
allColumnNames,
|
||||||
|
selectedLeftDataKeys: selectedLeftData ? Object.keys(selectedLeftData) : [],
|
||||||
|
initializedFormDataKeys: Object.keys(initializedFormData),
|
||||||
|
});
|
||||||
|
|
||||||
|
setFormData(initializedFormData);
|
||||||
|
setFormDataVersion((v) => v + 1); // 🆕 버전 증가로 컴포넌트 강제 리렌더링
|
||||||
|
}, [position, splitPanelContext, selectedLeftData, layout]);
|
||||||
|
|
||||||
// 선택 변경 이벤트 전파
|
// 선택 변경 이벤트 전파
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -377,15 +401,15 @@ export const EmbeddedScreen = forwardRef<EmbeddedScreenHandle, EmbeddedScreenPro
|
||||||
<p className="text-muted-foreground text-sm">화면에 컴포넌트가 없습니다.</p>
|
<p className="text-muted-foreground text-sm">화면에 컴포넌트가 없습니다.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className="relative w-full"
|
className="relative w-full"
|
||||||
style={{
|
style={{
|
||||||
minHeight: contentBounds.height + 20, // 여유 공간 추가
|
minHeight: contentBounds.height + 20, // 여유 공간 추가
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{layout.map((component) => {
|
{layout.map((component) => {
|
||||||
const { position: compPosition = { x: 0, y: 0, z: 1 }, size = { width: 200, height: 40 } } = component;
|
const { position: compPosition = { x: 0, y: 0, z: 1 }, size = { width: 200, height: 40 } } = component;
|
||||||
|
|
||||||
// 컴포넌트가 컨테이너 너비를 초과하지 않도록 너비 조정
|
// 컴포넌트가 컨테이너 너비를 초과하지 않도록 너비 조정
|
||||||
// 부모 컨테이너의 100%를 기준으로 계산
|
// 부모 컨테이너의 100%를 기준으로 계산
|
||||||
const componentStyle: React.CSSProperties = {
|
const componentStyle: React.CSSProperties = {
|
||||||
|
|
@ -397,13 +421,9 @@ export const EmbeddedScreen = forwardRef<EmbeddedScreenHandle, EmbeddedScreenPro
|
||||||
// 컴포넌트가 오른쪽 경계를 넘어가면 너비 조정
|
// 컴포넌트가 오른쪽 경계를 넘어가면 너비 조정
|
||||||
maxWidth: `calc(100% - ${compPosition.x || 0}px)`,
|
maxWidth: `calc(100% - ${compPosition.x || 0}px)`,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div key={`${component.id}-${formDataVersion}`} className="absolute" style={componentStyle}>
|
||||||
key={component.id}
|
|
||||||
className="absolute"
|
|
||||||
style={componentStyle}
|
|
||||||
>
|
|
||||||
<DynamicComponentRenderer
|
<DynamicComponentRenderer
|
||||||
component={component}
|
component={component}
|
||||||
isInteractive={true}
|
isInteractive={true}
|
||||||
|
|
|
||||||
|
|
@ -333,22 +333,72 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
|
|
||||||
const loadModalMappingColumns = async () => {
|
const loadModalMappingColumns = async () => {
|
||||||
// 소스 테이블: 현재 화면의 분할 패널 또는 테이블에서 감지
|
// 소스 테이블: 현재 화면의 분할 패널 또는 테이블에서 감지
|
||||||
// allComponents에서 split-panel-layout 또는 table-list 찾기
|
|
||||||
let sourceTableName: string | null = null;
|
let sourceTableName: string | null = null;
|
||||||
|
|
||||||
|
console.log("[openModalWithData] 컬럼 로드 시작:", {
|
||||||
|
allComponentsCount: allComponents.length,
|
||||||
|
currentTableName,
|
||||||
|
targetScreenId: config.action?.targetScreenId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 모든 컴포넌트 타입 로그
|
||||||
|
allComponents.forEach((comp, idx) => {
|
||||||
|
const compType = comp.componentType || (comp as any).componentConfig?.type;
|
||||||
|
console.log(` [${idx}] componentType: ${compType}, tableName: ${(comp as any).componentConfig?.tableName || (comp as any).componentConfig?.leftPanel?.tableName || 'N/A'}`);
|
||||||
|
});
|
||||||
|
|
||||||
for (const comp of allComponents) {
|
for (const comp of allComponents) {
|
||||||
const compType = comp.componentType || (comp as any).componentConfig?.type;
|
const compType = comp.componentType || (comp as any).componentConfig?.type;
|
||||||
|
const compConfig = (comp as any).componentConfig || {};
|
||||||
|
|
||||||
|
// 분할 패널 타입들 (다양한 경로에서 테이블명 추출)
|
||||||
if (compType === "split-panel-layout" || compType === "screen-split-panel") {
|
if (compType === "split-panel-layout" || compType === "screen-split-panel") {
|
||||||
// 분할 패널의 좌측 테이블명
|
sourceTableName = compConfig?.leftPanel?.tableName ||
|
||||||
sourceTableName = (comp as any).componentConfig?.leftPanel?.tableName ||
|
compConfig?.leftTableName ||
|
||||||
(comp as any).componentConfig?.leftTableName;
|
compConfig?.tableName;
|
||||||
break;
|
if (sourceTableName) {
|
||||||
|
console.log(`✅ [openModalWithData] split-panel-layout에서 소스 테이블 감지: ${sourceTableName}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// split-panel-layout2 타입 (새로운 분할 패널)
|
||||||
|
if (compType === "split-panel-layout2") {
|
||||||
|
sourceTableName = compConfig?.leftPanel?.tableName ||
|
||||||
|
compConfig?.tableName ||
|
||||||
|
compConfig?.leftTableName;
|
||||||
|
if (sourceTableName) {
|
||||||
|
console.log(`✅ [openModalWithData] split-panel-layout2에서 소스 테이블 감지: ${sourceTableName}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 테이블 리스트 타입
|
||||||
if (compType === "table-list") {
|
if (compType === "table-list") {
|
||||||
sourceTableName = (comp as any).componentConfig?.tableName;
|
sourceTableName = compConfig?.tableName;
|
||||||
|
if (sourceTableName) {
|
||||||
|
console.log(`✅ [openModalWithData] table-list에서 소스 테이블 감지: ${sourceTableName}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🆕 모든 컴포넌트에서 tableName 찾기 (폴백)
|
||||||
|
if (!sourceTableName && compConfig?.tableName) {
|
||||||
|
sourceTableName = compConfig.tableName;
|
||||||
|
console.log(`✅ [openModalWithData] ${compType}에서 소스 테이블 감지 (폴백): ${sourceTableName}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 여전히 없으면 currentTableName 사용 (화면 레벨 테이블명)
|
||||||
|
if (!sourceTableName && currentTableName) {
|
||||||
|
sourceTableName = currentTableName;
|
||||||
|
console.log(`✅ [openModalWithData] currentTableName에서 소스 테이블 사용: ${sourceTableName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sourceTableName) {
|
||||||
|
console.warn("[openModalWithData] 소스 테이블을 찾을 수 없습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
// 소스 테이블 컬럼 로드
|
// 소스 테이블 컬럼 로드
|
||||||
if (sourceTableName) {
|
if (sourceTableName) {
|
||||||
|
|
@ -361,11 +411,11 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
|
|
||||||
if (Array.isArray(columnData)) {
|
if (Array.isArray(columnData)) {
|
||||||
const columns = columnData.map((col: any) => ({
|
const columns = columnData.map((col: any) => ({
|
||||||
name: col.name || col.columnName,
|
name: col.name || col.columnName || col.column_name,
|
||||||
label: col.displayName || col.label || col.columnLabel || col.name || col.columnName,
|
label: col.displayName || col.label || col.columnLabel || col.display_name || col.name || col.columnName || col.column_name,
|
||||||
}));
|
}));
|
||||||
setModalSourceColumns(columns);
|
setModalSourceColumns(columns);
|
||||||
console.log(`✅ [openModalWithData] 소스 테이블(${sourceTableName}) 컬럼 로드:`, columns.length);
|
console.log(`✅ [openModalWithData] 소스 테이블(${sourceTableName}) 컬럼 로드 완료:`, columns.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -379,8 +429,12 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
try {
|
try {
|
||||||
// 타겟 화면 정보 가져오기
|
// 타겟 화면 정보 가져오기
|
||||||
const screenResponse = await apiClient.get(`/screen-management/screens/${targetScreenId}`);
|
const screenResponse = await apiClient.get(`/screen-management/screens/${targetScreenId}`);
|
||||||
|
console.log("[openModalWithData] 타겟 화면 응답:", screenResponse.data);
|
||||||
|
|
||||||
if (screenResponse.data.success && screenResponse.data.data) {
|
if (screenResponse.data.success && screenResponse.data.data) {
|
||||||
const targetTableName = screenResponse.data.data.tableName;
|
const targetTableName = screenResponse.data.data.tableName;
|
||||||
|
console.log("[openModalWithData] 타겟 화면 테이블명:", targetTableName);
|
||||||
|
|
||||||
if (targetTableName) {
|
if (targetTableName) {
|
||||||
const columnResponse = await apiClient.get(`/table-management/tables/${targetTableName}/columns`);
|
const columnResponse = await apiClient.get(`/table-management/tables/${targetTableName}/columns`);
|
||||||
if (columnResponse.data.success) {
|
if (columnResponse.data.success) {
|
||||||
|
|
@ -390,23 +444,27 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
|
|
||||||
if (Array.isArray(columnData)) {
|
if (Array.isArray(columnData)) {
|
||||||
const columns = columnData.map((col: any) => ({
|
const columns = columnData.map((col: any) => ({
|
||||||
name: col.name || col.columnName,
|
name: col.name || col.columnName || col.column_name,
|
||||||
label: col.displayName || col.label || col.columnLabel || col.name || col.columnName,
|
label: col.displayName || col.label || col.columnLabel || col.display_name || col.name || col.columnName || col.column_name,
|
||||||
}));
|
}));
|
||||||
setModalTargetColumns(columns);
|
setModalTargetColumns(columns);
|
||||||
console.log(`✅ [openModalWithData] 타겟 테이블(${targetTableName}) 컬럼 로드:`, columns.length);
|
console.log(`✅ [openModalWithData] 타겟 테이블(${targetTableName}) 컬럼 로드 완료:`, columns.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("[openModalWithData] 타겟 화면에 테이블명이 없습니다.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("타겟 화면 테이블 컬럼 로드 실패:", error);
|
console.error("타겟 화면 테이블 컬럼 로드 실패:", error);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("[openModalWithData] 타겟 화면 ID가 없습니다.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadModalMappingColumns();
|
loadModalMappingColumns();
|
||||||
}, [config.action?.type, config.action?.targetScreenId, allComponents]);
|
}, [config.action?.type, config.action?.targetScreenId, allComponents, currentTableName]);
|
||||||
|
|
||||||
// 화면 목록 가져오기 (현재 편집 중인 화면의 회사 코드 기준)
|
// 화면 목록 가져오기 (현재 편집 중인 화면의 회사 코드 기준)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -1158,11 +1216,12 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-3">
|
||||||
{(config.action?.fieldMappings || []).map((mapping: any, index: number) => (
|
{(config.action?.fieldMappings || []).map((mapping: any, index: number) => (
|
||||||
<div key={index} className="flex items-center gap-2 rounded-md border bg-background p-2">
|
<div key={index} className="rounded-md border bg-background p-3 space-y-2">
|
||||||
{/* 소스 필드 선택 (Combobox) */}
|
{/* 소스 필드 선택 (Combobox) - 세로 배치 */}
|
||||||
<div className="flex-1">
|
<div className="space-y-1">
|
||||||
|
<Label className="text-[10px] text-muted-foreground">소스 컬럼</Label>
|
||||||
<Popover
|
<Popover
|
||||||
open={modalSourcePopoverOpen[index] || false}
|
open={modalSourcePopoverOpen[index] || false}
|
||||||
onOpenChange={(open) => setModalSourcePopoverOpen((prev) => ({ ...prev, [index]: open }))}
|
onOpenChange={(open) => setModalSourcePopoverOpen((prev) => ({ ...prev, [index]: open }))}
|
||||||
|
|
@ -1171,15 +1230,17 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
className="h-7 w-full justify-between text-xs"
|
className="h-8 w-full justify-between text-xs"
|
||||||
>
|
>
|
||||||
{mapping.sourceField
|
<span className="truncate">
|
||||||
? modalSourceColumns.find((c) => c.name === mapping.sourceField)?.label || mapping.sourceField
|
{mapping.sourceField
|
||||||
: "소스 컬럼 선택"}
|
? modalSourceColumns.find((c) => c.name === mapping.sourceField)?.label || mapping.sourceField
|
||||||
|
: "소스 컬럼 선택"}
|
||||||
|
</span>
|
||||||
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-[200px] p-0" align="start">
|
<PopoverContent className="w-[--radix-popover-trigger-width] max-w-[280px] p-0" align="start">
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="컬럼 검색..."
|
placeholder="컬럼 검색..."
|
||||||
|
|
@ -1187,7 +1248,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
value={modalSourceSearch[index] || ""}
|
value={modalSourceSearch[index] || ""}
|
||||||
onValueChange={(value) => setModalSourceSearch((prev) => ({ ...prev, [index]: value }))}
|
onValueChange={(value) => setModalSourceSearch((prev) => ({ ...prev, [index]: value }))}
|
||||||
/>
|
/>
|
||||||
<CommandList>
|
<CommandList className="max-h-[200px]">
|
||||||
<CommandEmpty className="py-2 text-center text-xs">컬럼을 찾을 수 없습니다</CommandEmpty>
|
<CommandEmpty className="py-2 text-center text-xs">컬럼을 찾을 수 없습니다</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{modalSourceColumns.map((col) => (
|
{modalSourceColumns.map((col) => (
|
||||||
|
|
@ -1208,9 +1269,9 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
mapping.sourceField === col.name ? "opacity-100" : "opacity-0"
|
mapping.sourceField === col.name ? "opacity-100" : "opacity-0"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<span>{col.label}</span>
|
<span className="truncate">{col.label}</span>
|
||||||
{col.label !== col.name && (
|
{col.label !== col.name && (
|
||||||
<span className="ml-1 text-muted-foreground">({col.name})</span>
|
<span className="ml-1 text-muted-foreground truncate">({col.name})</span>
|
||||||
)}
|
)}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
|
|
@ -1221,10 +1282,14 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="text-xs text-muted-foreground">→</span>
|
{/* 화살표 표시 */}
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<span className="text-xs text-muted-foreground">↓</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 타겟 필드 선택 (Combobox) */}
|
{/* 타겟 필드 선택 (Combobox) - 세로 배치 */}
|
||||||
<div className="flex-1">
|
<div className="space-y-1">
|
||||||
|
<Label className="text-[10px] text-muted-foreground">타겟 컬럼</Label>
|
||||||
<Popover
|
<Popover
|
||||||
open={modalTargetPopoverOpen[index] || false}
|
open={modalTargetPopoverOpen[index] || false}
|
||||||
onOpenChange={(open) => setModalTargetPopoverOpen((prev) => ({ ...prev, [index]: open }))}
|
onOpenChange={(open) => setModalTargetPopoverOpen((prev) => ({ ...prev, [index]: open }))}
|
||||||
|
|
@ -1233,15 +1298,17 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
className="h-7 w-full justify-between text-xs"
|
className="h-8 w-full justify-between text-xs"
|
||||||
>
|
>
|
||||||
{mapping.targetField
|
<span className="truncate">
|
||||||
? modalTargetColumns.find((c) => c.name === mapping.targetField)?.label || mapping.targetField
|
{mapping.targetField
|
||||||
: "타겟 컬럼 선택"}
|
? modalTargetColumns.find((c) => c.name === mapping.targetField)?.label || mapping.targetField
|
||||||
|
: "타겟 컬럼 선택"}
|
||||||
|
</span>
|
||||||
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-[200px] p-0" align="start">
|
<PopoverContent className="w-[--radix-popover-trigger-width] max-w-[280px] p-0" align="start">
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="컬럼 검색..."
|
placeholder="컬럼 검색..."
|
||||||
|
|
@ -1249,7 +1316,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
value={modalTargetSearch[index] || ""}
|
value={modalTargetSearch[index] || ""}
|
||||||
onValueChange={(value) => setModalTargetSearch((prev) => ({ ...prev, [index]: value }))}
|
onValueChange={(value) => setModalTargetSearch((prev) => ({ ...prev, [index]: value }))}
|
||||||
/>
|
/>
|
||||||
<CommandList>
|
<CommandList className="max-h-[200px]">
|
||||||
<CommandEmpty className="py-2 text-center text-xs">컬럼을 찾을 수 없습니다</CommandEmpty>
|
<CommandEmpty className="py-2 text-center text-xs">컬럼을 찾을 수 없습니다</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{modalTargetColumns.map((col) => (
|
{modalTargetColumns.map((col) => (
|
||||||
|
|
@ -1270,9 +1337,9 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
mapping.targetField === col.name ? "opacity-100" : "opacity-0"
|
mapping.targetField === col.name ? "opacity-100" : "opacity-0"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<span>{col.label}</span>
|
<span className="truncate">{col.label}</span>
|
||||||
{col.label !== col.name && (
|
{col.label !== col.name && (
|
||||||
<span className="ml-1 text-muted-foreground">({col.name})</span>
|
<span className="ml-1 text-muted-foreground truncate">({col.name})</span>
|
||||||
)}
|
)}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
|
|
@ -1284,19 +1351,22 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 삭제 버튼 */}
|
{/* 삭제 버튼 */}
|
||||||
<Button
|
<div className="flex justify-end pt-1">
|
||||||
type="button"
|
<Button
|
||||||
variant="ghost"
|
type="button"
|
||||||
size="icon"
|
variant="ghost"
|
||||||
className="h-7 w-7 text-destructive hover:bg-destructive/10"
|
size="sm"
|
||||||
onClick={() => {
|
className="h-6 text-[10px] text-destructive hover:bg-destructive/10"
|
||||||
const mappings = [...(config.action?.fieldMappings || [])];
|
onClick={() => {
|
||||||
mappings.splice(index, 1);
|
const mappings = [...(config.action?.fieldMappings || [])];
|
||||||
onUpdateProperty("componentConfig.action.fieldMappings", mappings);
|
mappings.splice(index, 1);
|
||||||
}}
|
onUpdateProperty("componentConfig.action.fieldMappings", mappings);
|
||||||
>
|
}}
|
||||||
<X className="h-4 w-4" />
|
>
|
||||||
</Button>
|
<X className="h-3 w-3 mr-1" />
|
||||||
|
삭제
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -880,6 +880,44 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 모달 액션인데 선택된 데이터가 있으면 경고 메시지 표시하고 중단
|
||||||
|
// (신규 등록 모달에서 선택된 데이터가 초기값으로 전달되는 것을 방지)
|
||||||
|
if (processedConfig.action.type === "modal" && effectiveSelectedRowsData && effectiveSelectedRowsData.length > 0) {
|
||||||
|
toast.warning("신규 등록 시에는 테이블에서 선택된 항목을 해제해주세요.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 수정(edit) 액션 검증
|
||||||
|
if (processedConfig.action.type === "edit") {
|
||||||
|
// 선택된 데이터가 없으면 경고
|
||||||
|
if (!effectiveSelectedRowsData || effectiveSelectedRowsData.length === 0) {
|
||||||
|
toast.warning("수정할 항목을 선택해주세요.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// groupByColumns 설정이 있으면 해당 컬럼 값이 유일한지 확인
|
||||||
|
const groupByColumns = processedConfig.action.groupByColumns;
|
||||||
|
if (groupByColumns && groupByColumns.length > 0 && effectiveSelectedRowsData.length > 1) {
|
||||||
|
// 첫 번째 그룹핑 컬럼 기준으로 중복 체크 (예: order_no)
|
||||||
|
const groupByColumn = groupByColumns[0];
|
||||||
|
const uniqueValues = new Set(
|
||||||
|
effectiveSelectedRowsData.map((row: any) => row[groupByColumn]).filter(Boolean)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (uniqueValues.size > 1) {
|
||||||
|
// 컬럼명을 한글로 변환 (order_no -> 수주번호)
|
||||||
|
const columnLabels: Record<string, string> = {
|
||||||
|
order_no: "수주번호",
|
||||||
|
shipment_no: "출하번호",
|
||||||
|
purchase_no: "구매번호",
|
||||||
|
};
|
||||||
|
const columnLabel = columnLabels[groupByColumn] || groupByColumn;
|
||||||
|
toast.warning(`${columnLabel} 하나만 선택해주세요. (현재 ${uniqueValues.size}개 선택됨)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 🆕 모든 컴포넌트의 설정 수집 (parentDataMapping 등)
|
// 🆕 모든 컴포넌트의 설정 수집 (parentDataMapping 등)
|
||||||
const componentConfigs: Record<string, any> = {};
|
const componentConfigs: Record<string, any> = {};
|
||||||
if (allComponents && Array.isArray(allComponents)) {
|
if (allComponents && Array.isArray(allComponents)) {
|
||||||
|
|
@ -919,21 +957,27 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 분할 패널 우측이면 screenContext.formData와 props.formData를 병합
|
// 🆕 분할 패널 우측이면 여러 소스에서 formData를 병합
|
||||||
// screenContext.formData: RepeaterFieldGroup 등 컴포넌트가 직접 업데이트한 데이터
|
// 우선순위: props.formData > screenContext.formData > splitPanelParentData
|
||||||
// props.formData: 부모에서 전달된 폼 데이터
|
|
||||||
const screenContextFormData = screenContext?.formData || {};
|
const screenContextFormData = screenContext?.formData || {};
|
||||||
const propsFormData = formData || {};
|
const propsFormData = formData || {};
|
||||||
|
|
||||||
// 병합: props.formData를 기본으로 하고, screenContext.formData로 오버라이드
|
// 병합: splitPanelParentData를 기본으로, props.formData, screenContext.formData 순으로 오버라이드
|
||||||
// (RepeaterFieldGroup 데이터는 screenContext에만 있음)
|
// (일반 폼 필드는 props.formData, RepeaterFieldGroup은 screenContext.formData에 있음)
|
||||||
const effectiveFormData = { ...propsFormData, ...screenContextFormData };
|
let effectiveFormData = { ...propsFormData, ...screenContextFormData };
|
||||||
|
|
||||||
|
// 🆕 분할 패널 우측이고 formData가 비어있으면 splitPanelParentData 사용
|
||||||
|
if (splitPanelPosition === "right" && Object.keys(effectiveFormData).length === 0 && splitPanelParentData) {
|
||||||
|
effectiveFormData = { ...splitPanelParentData };
|
||||||
|
console.log("🔍 [ButtonPrimary] 분할 패널 우측 - splitPanelParentData 사용:", Object.keys(effectiveFormData));
|
||||||
|
}
|
||||||
|
|
||||||
console.log("🔍 [ButtonPrimary] formData 선택:", {
|
console.log("🔍 [ButtonPrimary] formData 선택:", {
|
||||||
hasScreenContextFormData: Object.keys(screenContextFormData).length > 0,
|
hasScreenContextFormData: Object.keys(screenContextFormData).length > 0,
|
||||||
screenContextKeys: Object.keys(screenContextFormData),
|
screenContextKeys: Object.keys(screenContextFormData),
|
||||||
hasPropsFormData: Object.keys(propsFormData).length > 0,
|
hasPropsFormData: Object.keys(propsFormData).length > 0,
|
||||||
propsFormDataKeys: Object.keys(propsFormData),
|
propsFormDataKeys: Object.keys(propsFormData),
|
||||||
|
hasSplitPanelParentData: !!splitPanelParentData && Object.keys(splitPanelParentData).length > 0,
|
||||||
splitPanelPosition,
|
splitPanelPosition,
|
||||||
effectiveFormDataKeys: Object.keys(effectiveFormData),
|
effectiveFormDataKeys: Object.keys(effectiveFormData),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
|
|
||||||
// 자동생성된 값 상태
|
// 자동생성된 값 상태
|
||||||
const [autoGeneratedValue, setAutoGeneratedValue] = useState<string>("");
|
const [autoGeneratedValue, setAutoGeneratedValue] = useState<string>("");
|
||||||
|
|
||||||
// API 호출 중복 방지를 위한 ref
|
// API 호출 중복 방지를 위한 ref
|
||||||
const isGeneratingRef = React.useRef(false);
|
const isGeneratingRef = React.useRef(false);
|
||||||
const hasGeneratedRef = React.useRef(false);
|
const hasGeneratedRef = React.useRef(false);
|
||||||
|
|
@ -104,7 +104,6 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
const currentFormValue = formData?.[component.columnName];
|
const currentFormValue = formData?.[component.columnName];
|
||||||
const currentComponentValue = component.value;
|
const currentComponentValue = component.value;
|
||||||
|
|
||||||
|
|
||||||
// 자동생성된 값이 없고, 현재 값도 없을 때만 생성
|
// 자동생성된 값이 없고, 현재 값도 없을 때만 생성
|
||||||
if (!autoGeneratedValue && !currentFormValue && !currentComponentValue) {
|
if (!autoGeneratedValue && !currentFormValue && !currentComponentValue) {
|
||||||
isGeneratingRef.current = true; // 생성 시작 플래그
|
isGeneratingRef.current = true; // 생성 시작 플래그
|
||||||
|
|
@ -145,7 +144,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
if (isInteractive && onFormDataChange && component.columnName) {
|
if (isInteractive && onFormDataChange && component.columnName) {
|
||||||
console.log("📝 formData 업데이트:", component.columnName, generatedValue);
|
console.log("📝 formData 업데이트:", component.columnName, generatedValue);
|
||||||
onFormDataChange(component.columnName, generatedValue);
|
onFormDataChange(component.columnName, generatedValue);
|
||||||
|
|
||||||
// 채번 규칙 ID도 함께 저장 (저장 시점에 실제 할당하기 위함)
|
// 채번 규칙 ID도 함께 저장 (저장 시점에 실제 할당하기 위함)
|
||||||
if (testAutoGeneration.type === "numbering_rule" && testAutoGeneration.options?.numberingRuleId) {
|
if (testAutoGeneration.type === "numbering_rule" && testAutoGeneration.options?.numberingRuleId) {
|
||||||
const ruleIdKey = `${component.columnName}_numberingRuleId`;
|
const ruleIdKey = `${component.columnName}_numberingRuleId`;
|
||||||
|
|
@ -181,12 +180,12 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
// width는 항상 100%로 고정 (부모 컨테이너가 gridColumns로 크기 제어)
|
// width는 항상 100%로 고정 (부모 컨테이너가 gridColumns로 크기 제어)
|
||||||
width: "100%",
|
width: "100%",
|
||||||
// 숨김 기능: 편집 모드에서만 연하게 표시
|
// 숨김 기능: 편집 모드에서만 연하게 표시
|
||||||
...(isHidden &&
|
...(isHidden &&
|
||||||
isDesignMode && {
|
isDesignMode && {
|
||||||
opacity: 0.4,
|
opacity: 0.4,
|
||||||
backgroundColor: "hsl(var(--muted))",
|
backgroundColor: "hsl(var(--muted))",
|
||||||
pointerEvents: "auto",
|
pointerEvents: "auto",
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 디자인 모드 스타일
|
// 디자인 모드 스타일
|
||||||
|
|
@ -361,7 +360,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
<div className={`relative w-full ${className || ""}`} style={componentStyle} {...safeDomProps}>
|
<div className={`relative w-full ${className || ""}`} style={componentStyle} {...safeDomProps}>
|
||||||
{/* 라벨 렌더링 */}
|
{/* 라벨 렌더링 */}
|
||||||
{component.label && component.style?.labelDisplay !== false && (
|
{component.label && component.style?.labelDisplay !== false && (
|
||||||
<label className="absolute -top-6 left-0 text-sm font-medium text-muted-foreground">
|
<label className="text-muted-foreground absolute -top-6 left-0 text-sm font-medium">
|
||||||
{component.label}
|
{component.label}
|
||||||
{component.required && <span className="text-destructive">*</span>}
|
{component.required && <span className="text-destructive">*</span>}
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -386,15 +385,17 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full flex-1 rounded-md border px-3 py-2 text-sm transition-all duration-200 outline-none",
|
"h-full flex-1 rounded-md border px-3 py-2 text-sm transition-all duration-200 outline-none",
|
||||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
|
||||||
isSelected ? "border-ring ring-2 ring-ring/50" : "border-input",
|
isSelected ? "border-ring ring-ring/50 ring-2" : "border-input",
|
||||||
componentConfig.disabled ? "bg-muted text-muted-foreground cursor-not-allowed opacity-50" : "bg-background text-foreground",
|
componentConfig.disabled
|
||||||
"disabled:cursor-not-allowed"
|
? "bg-muted text-muted-foreground cursor-not-allowed opacity-50"
|
||||||
|
: "bg-background text-foreground",
|
||||||
|
"disabled:cursor-not-allowed",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* @ 구분자 */}
|
{/* @ 구분자 */}
|
||||||
<span className="text-base font-medium text-muted-foreground">@</span>
|
<span className="text-muted-foreground text-base font-medium">@</span>
|
||||||
|
|
||||||
{/* 도메인 선택/입력 (Combobox) */}
|
{/* 도메인 선택/입력 (Combobox) */}
|
||||||
<Popover open={emailDomainOpen} onOpenChange={setEmailDomainOpen}>
|
<Popover open={emailDomainOpen} onOpenChange={setEmailDomainOpen}>
|
||||||
|
|
@ -406,14 +407,18 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
disabled={componentConfig.disabled || false}
|
disabled={componentConfig.disabled || false}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-full flex-1 items-center justify-between rounded-md border px-3 py-2 text-sm transition-all duration-200",
|
"flex h-full flex-1 items-center justify-between rounded-md border px-3 py-2 text-sm transition-all duration-200",
|
||||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
|
||||||
isSelected ? "border-ring ring-2 ring-ring/50" : "border-input",
|
isSelected ? "border-ring ring-ring/50 ring-2" : "border-input",
|
||||||
componentConfig.disabled ? "cursor-not-allowed bg-muted text-muted-foreground opacity-50" : "bg-background text-foreground",
|
componentConfig.disabled
|
||||||
|
? "bg-muted text-muted-foreground cursor-not-allowed opacity-50"
|
||||||
|
: "bg-background text-foreground",
|
||||||
"hover:border-ring/80",
|
"hover:border-ring/80",
|
||||||
emailDomainOpen && "border-ring ring-2 ring-ring/50",
|
emailDomainOpen && "border-ring ring-ring/50 ring-2",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className={cn("truncate", !emailDomain && "text-muted-foreground")}>{emailDomain || "도메인 선택"}</span>
|
<span className={cn("truncate", !emailDomain && "text-muted-foreground")}>
|
||||||
|
{emailDomain || "도메인 선택"}
|
||||||
|
</span>
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</button>
|
</button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
|
|
@ -470,7 +475,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
<div className={`relative w-full ${className || ""}`} style={componentStyle} {...safeDomProps}>
|
<div className={`relative w-full ${className || ""}`} style={componentStyle} {...safeDomProps}>
|
||||||
{/* 라벨 렌더링 */}
|
{/* 라벨 렌더링 */}
|
||||||
{component.label && component.style?.labelDisplay !== false && (
|
{component.label && component.style?.labelDisplay !== false && (
|
||||||
<label className="absolute -top-6 left-0 text-sm font-medium text-muted-foreground">
|
<label className="text-muted-foreground absolute -top-6 left-0 text-sm font-medium">
|
||||||
{component.label}
|
{component.label}
|
||||||
{component.required && <span className="text-destructive">*</span>}
|
{component.required && <span className="text-destructive">*</span>}
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -496,14 +501,16 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full flex-1 rounded-md border px-3 py-2 text-center text-sm transition-all duration-200 outline-none",
|
"h-full flex-1 rounded-md border px-3 py-2 text-center text-sm transition-all duration-200 outline-none",
|
||||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
|
||||||
isSelected ? "border-ring ring-2 ring-ring/50" : "border-input",
|
isSelected ? "border-ring ring-ring/50 ring-2" : "border-input",
|
||||||
componentConfig.disabled ? "bg-muted text-muted-foreground cursor-not-allowed opacity-50" : "bg-background text-foreground",
|
componentConfig.disabled
|
||||||
"disabled:cursor-not-allowed"
|
? "bg-muted text-muted-foreground cursor-not-allowed opacity-50"
|
||||||
|
: "bg-background text-foreground",
|
||||||
|
"disabled:cursor-not-allowed",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className="text-base font-medium text-muted-foreground">-</span>
|
<span className="text-muted-foreground text-base font-medium">-</span>
|
||||||
|
|
||||||
{/* 두 번째 부분 */}
|
{/* 두 번째 부분 */}
|
||||||
<input
|
<input
|
||||||
|
|
@ -524,14 +531,16 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full flex-1 rounded-md border px-3 py-2 text-center text-sm transition-all duration-200 outline-none",
|
"h-full flex-1 rounded-md border px-3 py-2 text-center text-sm transition-all duration-200 outline-none",
|
||||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
|
||||||
isSelected ? "border-ring ring-2 ring-ring/50" : "border-input",
|
isSelected ? "border-ring ring-ring/50 ring-2" : "border-input",
|
||||||
componentConfig.disabled ? "bg-muted text-muted-foreground cursor-not-allowed opacity-50" : "bg-background text-foreground",
|
componentConfig.disabled
|
||||||
"disabled:cursor-not-allowed"
|
? "bg-muted text-muted-foreground cursor-not-allowed opacity-50"
|
||||||
|
: "bg-background text-foreground",
|
||||||
|
"disabled:cursor-not-allowed",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className="text-base font-medium text-muted-foreground">-</span>
|
<span className="text-muted-foreground text-base font-medium">-</span>
|
||||||
|
|
||||||
{/* 세 번째 부분 */}
|
{/* 세 번째 부분 */}
|
||||||
<input
|
<input
|
||||||
|
|
@ -552,10 +561,12 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full flex-1 rounded-md border px-3 py-2 text-center text-sm transition-all duration-200 outline-none",
|
"h-full flex-1 rounded-md border px-3 py-2 text-center text-sm transition-all duration-200 outline-none",
|
||||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
|
||||||
isSelected ? "border-ring ring-2 ring-ring/50" : "border-input",
|
isSelected ? "border-ring ring-ring/50 ring-2" : "border-input",
|
||||||
componentConfig.disabled ? "bg-muted text-muted-foreground cursor-not-allowed opacity-50" : "bg-background text-foreground",
|
componentConfig.disabled
|
||||||
"disabled:cursor-not-allowed"
|
? "bg-muted text-muted-foreground cursor-not-allowed opacity-50"
|
||||||
|
: "bg-background text-foreground",
|
||||||
|
"disabled:cursor-not-allowed",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -569,7 +580,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
<div className={`relative w-full ${className || ""}`} style={componentStyle} {...safeDomProps}>
|
<div className={`relative w-full ${className || ""}`} style={componentStyle} {...safeDomProps}>
|
||||||
{/* 라벨 렌더링 */}
|
{/* 라벨 렌더링 */}
|
||||||
{component.label && component.style?.labelDisplay !== false && (
|
{component.label && component.style?.labelDisplay !== false && (
|
||||||
<label className="absolute -top-6 left-0 text-sm font-medium text-muted-foreground">
|
<label className="text-muted-foreground absolute -top-6 left-0 text-sm font-medium">
|
||||||
{component.label}
|
{component.label}
|
||||||
{component.required && <span className="text-destructive">*</span>}
|
{component.required && <span className="text-destructive">*</span>}
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -591,10 +602,12 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full w-[100px] cursor-pointer rounded-md border px-2 py-2 text-sm transition-all duration-200 outline-none",
|
"h-full w-[100px] cursor-pointer rounded-md border px-2 py-2 text-sm transition-all duration-200 outline-none",
|
||||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
|
||||||
isSelected ? "border-ring ring-2 ring-ring/50" : "border-input",
|
isSelected ? "border-ring ring-ring/50 ring-2" : "border-input",
|
||||||
componentConfig.disabled ? "bg-muted text-muted-foreground cursor-not-allowed opacity-50" : "bg-background text-foreground",
|
componentConfig.disabled
|
||||||
"disabled:cursor-not-allowed"
|
? "bg-muted text-muted-foreground cursor-not-allowed opacity-50"
|
||||||
|
: "bg-background text-foreground",
|
||||||
|
"disabled:cursor-not-allowed",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<option value="https://">https://</option>
|
<option value="https://">https://</option>
|
||||||
|
|
@ -619,10 +632,12 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full flex-1 rounded-md border px-3 py-2 text-sm transition-all duration-200 outline-none",
|
"h-full flex-1 rounded-md border px-3 py-2 text-sm transition-all duration-200 outline-none",
|
||||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
|
||||||
isSelected ? "border-ring ring-2 ring-ring/50" : "border-input",
|
isSelected ? "border-ring ring-ring/50 ring-2" : "border-input",
|
||||||
componentConfig.disabled ? "bg-muted text-muted-foreground cursor-not-allowed opacity-50" : "bg-background text-foreground",
|
componentConfig.disabled
|
||||||
"disabled:cursor-not-allowed"
|
? "bg-muted text-muted-foreground cursor-not-allowed opacity-50"
|
||||||
|
: "bg-background text-foreground",
|
||||||
|
"disabled:cursor-not-allowed",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -636,7 +651,7 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
<div className={`relative w-full ${className || ""}`} style={componentStyle} {...safeDomProps}>
|
<div className={`relative w-full ${className || ""}`} style={componentStyle} {...safeDomProps}>
|
||||||
{/* 라벨 렌더링 */}
|
{/* 라벨 렌더링 */}
|
||||||
{component.label && component.style?.labelDisplay !== false && (
|
{component.label && component.style?.labelDisplay !== false && (
|
||||||
<label className="absolute -top-6 left-0 text-sm font-medium text-muted-foreground">
|
<label className="text-muted-foreground absolute -top-6 left-0 text-sm font-medium">
|
||||||
{component.label}
|
{component.label}
|
||||||
{component.required && <span className="text-destructive">*</span>}
|
{component.required && <span className="text-destructive">*</span>}
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -669,11 +684,13 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"box-border h-full w-full max-w-full resize-none rounded-md border px-3 py-2 text-sm transition-all duration-200 outline-none",
|
"box-border h-full w-full max-w-full resize-none rounded-md border px-3 py-2 text-sm transition-all duration-200 outline-none",
|
||||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
|
||||||
"placeholder:text-muted-foreground",
|
"placeholder:text-muted-foreground",
|
||||||
isSelected ? "border-ring ring-2 ring-ring/50" : "border-input",
|
isSelected ? "border-ring ring-ring/50 ring-2" : "border-input",
|
||||||
componentConfig.disabled ? "bg-muted text-muted-foreground cursor-not-allowed opacity-50" : "bg-background text-foreground",
|
componentConfig.disabled
|
||||||
"disabled:cursor-not-allowed"
|
? "bg-muted text-muted-foreground cursor-not-allowed opacity-50"
|
||||||
|
: "bg-background text-foreground",
|
||||||
|
"disabled:cursor-not-allowed",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -692,13 +709,15 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
|
|
||||||
{/* 수동/자동 모드 표시 배지 */}
|
{/* 수동/자동 모드 표시 배지 */}
|
||||||
{testAutoGeneration.enabled && testAutoGeneration.type === "numbering_rule" && isInteractive && (
|
{testAutoGeneration.enabled && testAutoGeneration.type === "numbering_rule" && isInteractive && (
|
||||||
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1">
|
<div className="absolute top-1/2 right-2 flex -translate-y-1/2 items-center gap-1">
|
||||||
<span className={cn(
|
<span
|
||||||
"text-[10px] px-2 py-0.5 rounded-full font-medium",
|
className={cn(
|
||||||
isManualMode
|
"rounded-full px-2 py-0.5 text-[10px] font-medium",
|
||||||
? "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400"
|
isManualMode
|
||||||
: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
|
? "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400"
|
||||||
)}>
|
: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400",
|
||||||
|
)}
|
||||||
|
>
|
||||||
{isManualMode ? "수동" : "자동"}
|
{isManualMode ? "수동" : "자동"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -706,12 +725,12 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type={inputType}
|
type={inputType}
|
||||||
defaultValue={(() => {
|
value={(() => {
|
||||||
let displayValue = "";
|
let displayValue = "";
|
||||||
|
|
||||||
if (isInteractive && formData && component.columnName) {
|
if (isInteractive && formData && component.columnName) {
|
||||||
// 인터랙티브 모드: formData 우선, 없으면 자동생성 값
|
// 인터랙티브 모드: formData 우선, 없으면 자동생성 값
|
||||||
const rawValue = formData[component.columnName] || autoGeneratedValue || "";
|
const rawValue = formData[component.columnName] ?? autoGeneratedValue ?? "";
|
||||||
// 객체인 경우 빈 문자열로 변환 (에러 방지)
|
// 객체인 경우 빈 문자열로 변환 (에러 방지)
|
||||||
displayValue = typeof rawValue === "object" ? "" : String(rawValue);
|
displayValue = typeof rawValue === "object" ? "" : String(rawValue);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -724,31 +743,33 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
})()}
|
})()}
|
||||||
placeholder={
|
placeholder={
|
||||||
testAutoGeneration.enabled && testAutoGeneration.type !== "none"
|
testAutoGeneration.enabled && testAutoGeneration.type !== "none"
|
||||||
? isManualMode
|
? isManualMode
|
||||||
? "수동 입력 모드"
|
? "수동 입력 모드"
|
||||||
: `자동생성: ${AutoGenerationUtils.getTypeDescription(testAutoGeneration.type)}`
|
: `자동생성: ${AutoGenerationUtils.getTypeDescription(testAutoGeneration.type)}`
|
||||||
: componentConfig.placeholder || defaultPlaceholder
|
: componentConfig.placeholder || defaultPlaceholder
|
||||||
}
|
}
|
||||||
pattern={validationPattern}
|
pattern={validationPattern}
|
||||||
title={
|
title={
|
||||||
webType === "tel"
|
webType === "tel"
|
||||||
? "전화번호 형식: 010-1234-5678"
|
? "전화번호 형식: 010-1234-5678"
|
||||||
: isManualMode
|
: isManualMode
|
||||||
? `${component.label} (수동 입력 모드 - 채번 규칙 미적용)`
|
? `${component.label} (수동 입력 모드 - 채번 규칙 미적용)`
|
||||||
: component.label
|
: component.label
|
||||||
? `${component.label}${component.columnName ? ` (${component.columnName})` : ""}`
|
? `${component.label}${component.columnName ? ` (${component.columnName})` : ""}`
|
||||||
: component.columnName || undefined
|
: component.columnName || undefined
|
||||||
}
|
}
|
||||||
disabled={componentConfig.disabled || false}
|
disabled={componentConfig.disabled || false}
|
||||||
required={componentConfig.required || false}
|
required={componentConfig.required || false}
|
||||||
readOnly={componentConfig.readonly || false}
|
readOnly={componentConfig.readonly || false}
|
||||||
className={cn(
|
className={cn(
|
||||||
"box-border h-full w-full max-w-full rounded-md border px-3 py-2 text-sm shadow-sm transition-all duration-200 outline-none",
|
"box-border h-full w-full max-w-full rounded-md border px-3 py-2 text-sm shadow-sm transition-all duration-200 outline-none",
|
||||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
|
||||||
"placeholder:text-muted-foreground",
|
"placeholder:text-muted-foreground",
|
||||||
isSelected ? "border-ring ring-2 ring-ring/50" : "border-input",
|
isSelected ? "border-ring ring-ring/50 ring-2" : "border-input",
|
||||||
componentConfig.disabled ? "bg-muted text-muted-foreground cursor-not-allowed opacity-50" : "bg-background text-foreground",
|
componentConfig.disabled
|
||||||
"disabled:cursor-not-allowed"
|
? "bg-muted text-muted-foreground cursor-not-allowed opacity-50"
|
||||||
|
: "bg-background text-foreground",
|
||||||
|
"disabled:cursor-not-allowed",
|
||||||
)}
|
)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
handleClick(e);
|
handleClick(e);
|
||||||
|
|
@ -774,9 +795,9 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
console.log("🔄 수동 모드로 전환:", {
|
console.log("🔄 수동 모드로 전환:", {
|
||||||
field: component.columnName,
|
field: component.columnName,
|
||||||
original: originalAutoGeneratedValue,
|
original: originalAutoGeneratedValue,
|
||||||
modified: newValue
|
modified: newValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🆕 채번 규칙 ID 제거 (수동 모드이므로 더 이상 채번 규칙 사용 안 함)
|
// 🆕 채번 규칙 ID 제거 (수동 모드이므로 더 이상 채번 규칙 사용 안 함)
|
||||||
if (isInteractive && onFormDataChange && component.columnName) {
|
if (isInteractive && onFormDataChange && component.columnName) {
|
||||||
const ruleIdKey = `${component.columnName}_numberingRuleId`;
|
const ruleIdKey = `${component.columnName}_numberingRuleId`;
|
||||||
|
|
@ -789,9 +810,9 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
|
||||||
setIsManualMode(false);
|
setIsManualMode(false);
|
||||||
console.log("🔄 자동 모드로 복구:", {
|
console.log("🔄 자동 모드로 복구:", {
|
||||||
field: component.columnName,
|
field: component.columnName,
|
||||||
value: newValue
|
value: newValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 채번 규칙 ID 복구
|
// 채번 규칙 ID 복구
|
||||||
if (isInteractive && onFormDataChange && component.columnName) {
|
if (isInteractive && onFormDataChange && component.columnName) {
|
||||||
const ruleId = testAutoGeneration.options?.numberingRuleId;
|
const ruleId = testAutoGeneration.options?.numberingRuleId;
|
||||||
|
|
|
||||||
|
|
@ -1780,16 +1780,15 @@ export class ButtonActionExecutor {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 모달 액션 처리
|
* 모달 액션 처리
|
||||||
* 🔧 modal 액션은 항상 신규 등록(INSERT) 모드로 동작
|
* 선택된 데이터가 있으면 함께 전달 (출하계획 등에서 사용)
|
||||||
* edit 액션만 수정(UPDATE) 모드로 동작해야 함
|
|
||||||
*/
|
*/
|
||||||
private static async handleModal(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
private static async handleModal(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
||||||
// 모달 열기 로직
|
// 모달 열기 로직
|
||||||
console.log("모달 열기 (신규 등록 모드):", {
|
console.log("모달 열기:", {
|
||||||
title: config.modalTitle,
|
title: config.modalTitle,
|
||||||
size: config.modalSize,
|
size: config.modalSize,
|
||||||
targetScreenId: config.targetScreenId,
|
targetScreenId: config.targetScreenId,
|
||||||
// 🔧 selectedRowsData는 modal 액션에서 사용하지 않음 (신규 등록이므로)
|
selectedRowsData: context.selectedRowsData,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (config.targetScreenId) {
|
if (config.targetScreenId) {
|
||||||
|
|
@ -1806,11 +1805,10 @@ export class ButtonActionExecutor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔧 modal 액션은 신규 등록이므로 selectedData를 전달하지 않음
|
// 선택된 행 데이터 수집
|
||||||
// selectedData가 있으면 ScreenModal에서 originalData로 인식하여 UPDATE 모드로 동작하게 됨
|
const selectedData = context.selectedRowsData || [];
|
||||||
// edit 액션만 selectedData/editData를 사용하여 UPDATE 모드로 동작
|
console.log("📦 [handleModal] 선택된 데이터:", selectedData);
|
||||||
console.log("📦 [handleModal] 신규 등록 모드 - selectedData 전달하지 않음");
|
console.log("📦 [handleModal] 분할 패널 부모 데이터:", context.splitPanelParentData);
|
||||||
console.log("📦 [handleModal] 분할 패널 부모 데이터 (초기값으로 사용):", context.splitPanelParentData);
|
|
||||||
|
|
||||||
// 전역 모달 상태 업데이트를 위한 이벤트 발생
|
// 전역 모달 상태 업데이트를 위한 이벤트 발생
|
||||||
const modalEvent = new CustomEvent("openScreenModal", {
|
const modalEvent = new CustomEvent("openScreenModal", {
|
||||||
|
|
@ -1819,11 +1817,10 @@ export class ButtonActionExecutor {
|
||||||
title: config.modalTitle || "화면",
|
title: config.modalTitle || "화면",
|
||||||
description: description,
|
description: description,
|
||||||
size: config.modalSize || "md",
|
size: config.modalSize || "md",
|
||||||
// 🔧 신규 등록이므로 selectedData/selectedIds를 전달하지 않음
|
// 선택된 행 데이터 전달
|
||||||
// edit 액션에서만 이 데이터를 사용
|
selectedData: selectedData,
|
||||||
selectedData: [],
|
selectedIds: selectedData.map((row: any) => row.id).filter(Boolean),
|
||||||
selectedIds: [],
|
// 분할 패널 부모 데이터 전달 (탭 안 모달에서 사용)
|
||||||
// 🆕 분할 패널 부모 데이터 전달 (탭 안 모달에서 초기값으로 사용)
|
|
||||||
splitPanelParentData: context.splitPanelParentData || {},
|
splitPanelParentData: context.splitPanelParentData || {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -2023,11 +2020,18 @@ export class ButtonActionExecutor {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🆕 modalDataStore에서 선택된 전체 데이터 가져오기 (RepeatScreenModal에서 사용)
|
||||||
|
const modalData = dataRegistry[dataSourceId] || [];
|
||||||
|
const selectedData = modalData.map((item: any) => item.originalData || item);
|
||||||
|
const selectedIds = selectedData.map((row: any) => row.id).filter(Boolean);
|
||||||
|
|
||||||
console.log("📦 [openModalWithData] 부모 데이터 전달:", {
|
console.log("📦 [openModalWithData] 부모 데이터 전달:", {
|
||||||
dataSourceId,
|
dataSourceId,
|
||||||
rawParentData,
|
rawParentData,
|
||||||
mappedParentData: parentData,
|
mappedParentData: parentData,
|
||||||
fieldMappings: config.fieldMappings,
|
fieldMappings: config.fieldMappings,
|
||||||
|
selectedDataCount: selectedData.length,
|
||||||
|
selectedIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🆕 전역 모달 상태 업데이트를 위한 이벤트 발생 (URL 파라미터 포함)
|
// 🆕 전역 모달 상태 업데이트를 위한 이벤트 발생 (URL 파라미터 포함)
|
||||||
|
|
@ -2039,6 +2043,9 @@ export class ButtonActionExecutor {
|
||||||
size: config.modalSize || "lg", // 데이터 입력 화면은 기본 large
|
size: config.modalSize || "lg", // 데이터 입력 화면은 기본 large
|
||||||
urlParams: { dataSourceId }, // 🆕 주 데이터 소스만 전달 (나머지는 modalDataStore에서 자동으로 찾음)
|
urlParams: { dataSourceId }, // 🆕 주 데이터 소스만 전달 (나머지는 modalDataStore에서 자동으로 찾음)
|
||||||
splitPanelParentData: parentData, // 🆕 부모 데이터 전달 (excludeFilter에서 사용)
|
splitPanelParentData: parentData, // 🆕 부모 데이터 전달 (excludeFilter에서 사용)
|
||||||
|
// 🆕 선택된 데이터 전달 (RepeatScreenModal에서 groupedData로 사용)
|
||||||
|
selectedData: selectedData,
|
||||||
|
selectedIds: selectedIds,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue