Merge branch 'main' into feature/screen-management

This commit is contained in:
kjs
2026-01-07 11:55:45 +09:00
7 changed files with 311 additions and 22 deletions

View File

@@ -27,13 +27,14 @@ interface EmbeddedScreenProps {
onSelectionChanged?: (selectedRows: any[]) => void;
position?: SplitPanelPosition; // 분할 패널 내 위치 (left/right)
initialFormData?: Record<string, any>; // 🆕 수정 모드에서 전달되는 초기 데이터
groupedData?: Record<string, any>[]; // 🆕 그룹 데이터 (수정 모드에서 원본 데이터 추적용)
}
/**
* 임베드된 화면 컴포넌트
*/
export const EmbeddedScreen = forwardRef<EmbeddedScreenHandle, EmbeddedScreenProps>(
({ embedding, onSelectionChanged, position, initialFormData }, ref) => {
({ embedding, onSelectionChanged, position, initialFormData, groupedData }, ref) => {
const [layout, setLayout] = useState<ComponentData[]>([]);
const [selectedRows, setSelectedRows] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
@@ -430,6 +431,8 @@ export const EmbeddedScreen = forwardRef<EmbeddedScreenHandle, EmbeddedScreenPro
userId={userId}
userName={userName}
companyCode={companyCode}
groupedData={groupedData}
initialData={initialFormData}
/>
</div>
);

View File

@@ -17,13 +17,14 @@ interface ScreenSplitPanelProps {
screenId?: number;
config?: any; // 설정 패널에서 오는 config (leftScreenId, rightScreenId, splitRatio, resizable)
initialFormData?: Record<string, any>; // 🆕 수정 모드에서 전달되는 초기 데이터
groupedData?: Record<string, any>[]; // 🆕 그룹 데이터 (수정 모드에서 원본 데이터 추적용)
}
/**
* 분할 패널 컴포넌트
* 순수하게 화면 분할 기능만 제공합니다.
*/
export function ScreenSplitPanel({ screenId, config, initialFormData }: ScreenSplitPanelProps) {
export function ScreenSplitPanel({ screenId, config, initialFormData, groupedData }: ScreenSplitPanelProps) {
// config에서 splitRatio 추출 (기본값 50)
const configSplitRatio = config?.splitRatio ?? 50;
@@ -117,7 +118,7 @@ export function ScreenSplitPanel({ screenId, config, initialFormData }: ScreenSp
{/* 좌측 패널 */}
<div style={{ width: `${splitRatio}%` }} className="h-full flex-shrink-0 overflow-hidden border-r">
{hasLeftScreen ? (
<EmbeddedScreen embedding={leftEmbedding!} position="left" initialFormData={initialFormData} />
<EmbeddedScreen embedding={leftEmbedding!} position="left" initialFormData={initialFormData} groupedData={groupedData} />
) : (
<div className="flex h-full items-center justify-center bg-muted/30">
<p className="text-muted-foreground text-sm"> </p>
@@ -157,7 +158,7 @@ export function ScreenSplitPanel({ screenId, config, initialFormData }: ScreenSp
{/* 우측 패널 */}
<div style={{ width: `${100 - splitRatio}%` }} className="h-full flex-shrink-0 overflow-hidden">
{hasRightScreen ? (
<EmbeddedScreen embedding={rightEmbedding!} position="right" initialFormData={initialFormData} />
<EmbeddedScreen embedding={rightEmbedding!} position="right" initialFormData={initialFormData} groupedData={groupedData} />
) : (
<div className="flex h-full items-center justify-center bg-muted/30">
<p className="text-muted-foreground text-sm"> </p>

View File

@@ -60,6 +60,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
editModalTitle: String(config.action?.editModalTitle || ""),
editModalDescription: String(config.action?.editModalDescription || ""),
targetUrl: String(config.action?.targetUrl || ""),
groupByColumn: String(config.action?.groupByColumns?.[0] || ""),
});
const [screens, setScreens] = useState<ScreenOption[]>([]);
@@ -97,6 +98,11 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
const [modalTargetColumns, setModalTargetColumns] = useState<Array<{ name: string; label: string }>>([]);
const [modalSourcePopoverOpen, setModalSourcePopoverOpen] = useState<Record<number, boolean>>({});
const [modalTargetPopoverOpen, setModalTargetPopoverOpen] = useState<Record<number, boolean>>({});
// 🆕 그룹화 컬럼 선택용 상태
const [currentTableColumns, setCurrentTableColumns] = useState<Array<{ name: string; label: string }>>([]);
const [groupByColumnOpen, setGroupByColumnOpen] = useState(false);
const [groupByColumnSearch, setGroupByColumnSearch] = useState("");
const [modalSourceSearch, setModalSourceSearch] = useState<Record<number, string>>({});
const [modalTargetSearch, setModalTargetSearch] = useState<Record<number, string>>({});
@@ -130,6 +136,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
editModalTitle: String(latestAction.editModalTitle || ""),
editModalDescription: String(latestAction.editModalDescription || ""),
targetUrl: String(latestAction.targetUrl || ""),
groupByColumn: String(latestAction.groupByColumns?.[0] || ""),
});
// 🆕 제목 블록 초기화
@@ -327,6 +334,35 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
loadColumns();
}, [config.action?.dataTransfer?.sourceTable, config.action?.dataTransfer?.targetTable]);
// 🆕 현재 테이블 컬럼 로드 (그룹화 컬럼 선택용)
useEffect(() => {
if (!currentTableName) return;
const loadCurrentTableColumns = async () => {
try {
const response = await apiClient.get(`/table-management/tables/${currentTableName}/columns`);
if (response.data.success) {
let columnData = response.data.data;
if (!Array.isArray(columnData) && columnData?.columns) columnData = columnData.columns;
if (!Array.isArray(columnData) && columnData?.data) columnData = columnData.data;
if (Array.isArray(columnData)) {
const columns = columnData.map((col: any) => ({
name: col.name || col.columnName,
label: col.displayName || col.label || col.columnLabel || col.name || col.columnName,
}));
setCurrentTableColumns(columns);
console.log(`✅ 현재 테이블 ${currentTableName} 컬럼 로드 성공:`, columns.length, "개");
}
}
} catch (error) {
console.error("현재 테이블 컬럼 로드 실패:", error);
}
};
loadCurrentTableColumns();
}, [currentTableName]);
// 🆕 openModalWithData 소스/타겟 테이블 컬럼 로드
useEffect(() => {
const actionType = config.action?.type;
@@ -1529,6 +1565,106 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
</div>
</>
)}
<div>
<Label htmlFor="edit-group-by-column"> </Label>
<Popover open={groupByColumnOpen} onOpenChange={setGroupByColumnOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={groupByColumnOpen}
className="h-8 w-full justify-between text-xs"
>
{localInputs.groupByColumn ? (
<span>
{localInputs.groupByColumn}
{currentTableColumns.find((col) => col.name === localInputs.groupByColumn)?.label &&
currentTableColumns.find((col) => col.name === localInputs.groupByColumn)?.label !== localInputs.groupByColumn && (
<span className="ml-1 text-muted-foreground">
({currentTableColumns.find((col) => col.name === localInputs.groupByColumn)?.label})
</span>
)}
</span>
) : (
<span className="text-muted-foreground"> </span>
)}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
<div className="flex flex-col">
<div className="flex items-center border-b px-3 py-2">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<Input
placeholder="컬럼명 또는 라벨 검색..."
value={groupByColumnSearch}
onChange={(e) => setGroupByColumnSearch(e.target.value)}
className="border-0 p-0 focus-visible:ring-0"
/>
</div>
<div className="max-h-[200px] overflow-auto">
{currentTableColumns.length === 0 ? (
<div className="p-3 text-sm text-muted-foreground">
{currentTableName ? "컬럼을 불러오는 중..." : "테이블이 설정되지 않았습니다"}
</div>
) : (
<>
{/* 선택 해제 옵션 */}
<div
className="flex cursor-pointer items-center px-3 py-2 hover:bg-muted"
onClick={() => {
setLocalInputs((prev) => ({ ...prev, groupByColumn: "" }));
onUpdateProperty("componentConfig.action.groupByColumns", undefined);
setGroupByColumnOpen(false);
setGroupByColumnSearch("");
}}
>
<Check className={cn("mr-2 h-4 w-4", !localInputs.groupByColumn ? "opacity-100" : "opacity-0")} />
<span className="text-muted-foreground"> </span>
</div>
{/* 컬럼 목록 */}
{currentTableColumns
.filter((col) => {
if (!groupByColumnSearch) return true;
const search = groupByColumnSearch.toLowerCase();
return (
col.name.toLowerCase().includes(search) ||
col.label.toLowerCase().includes(search)
);
})
.map((col) => (
<div
key={col.name}
className="flex cursor-pointer items-center px-3 py-2 hover:bg-muted"
onClick={() => {
setLocalInputs((prev) => ({ ...prev, groupByColumn: col.name }));
onUpdateProperty("componentConfig.action.groupByColumns", [col.name]);
setGroupByColumnOpen(false);
setGroupByColumnSearch("");
}}
>
<Check
className={cn("mr-2 h-4 w-4", localInputs.groupByColumn === col.name ? "opacity-100" : "opacity-0")}
/>
<div className="flex flex-col">
<span className="font-medium">{col.name}</span>
{col.label !== col.name && (
<span className="text-xs text-muted-foreground">{col.label}</span>
)}
</div>
</div>
))}
</>
)}
</div>
</div>
</PopoverContent>
</Popover>
<p className="mt-1 text-xs text-muted-foreground">
</p>
</div>
</div>
)}