Compare commits
No commits in common. "0ec6d082d6ebf52789dab41ca10809a0a99fdee1" and "31746e8a0b7920ea165f061e4597cc002a165e54" have entirely different histories.
0ec6d082d6
...
31746e8a0b
|
|
@ -3245,7 +3245,6 @@ export const resetUserPassword = async (
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 테이블 스키마 조회 (엑셀 업로드 컬럼 매핑용)
|
* 테이블 스키마 조회 (엑셀 업로드 컬럼 매핑용)
|
||||||
* column_labels 테이블에서 라벨 정보도 함께 가져옴
|
|
||||||
*/
|
*/
|
||||||
export async function getTableSchema(
|
export async function getTableSchema(
|
||||||
req: AuthenticatedRequest,
|
req: AuthenticatedRequest,
|
||||||
|
|
@ -3265,25 +3264,20 @@ export async function getTableSchema(
|
||||||
|
|
||||||
logger.info("테이블 스키마 조회", { tableName, companyCode });
|
logger.info("테이블 스키마 조회", { tableName, companyCode });
|
||||||
|
|
||||||
// information_schema와 column_labels를 JOIN하여 컬럼 정보와 라벨 정보 함께 가져오기
|
// information_schema에서 컬럼 정보 가져오기
|
||||||
const schemaQuery = `
|
const schemaQuery = `
|
||||||
SELECT
|
SELECT
|
||||||
ic.column_name,
|
column_name,
|
||||||
ic.data_type,
|
data_type,
|
||||||
ic.is_nullable,
|
is_nullable,
|
||||||
ic.column_default,
|
column_default,
|
||||||
ic.character_maximum_length,
|
character_maximum_length,
|
||||||
ic.numeric_precision,
|
numeric_precision,
|
||||||
ic.numeric_scale,
|
numeric_scale
|
||||||
cl.column_label,
|
FROM information_schema.columns
|
||||||
cl.display_order
|
WHERE table_schema = 'public'
|
||||||
FROM information_schema.columns ic
|
AND table_name = $1
|
||||||
LEFT JOIN column_labels cl
|
ORDER BY ordinal_position
|
||||||
ON cl.table_name = ic.table_name
|
|
||||||
AND cl.column_name = ic.column_name
|
|
||||||
WHERE ic.table_schema = 'public'
|
|
||||||
AND ic.table_name = $1
|
|
||||||
ORDER BY COALESCE(cl.display_order, ic.ordinal_position), ic.ordinal_position
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const columns = await query<any>(schemaQuery, [tableName]);
|
const columns = await query<any>(schemaQuery, [tableName]);
|
||||||
|
|
@ -3296,10 +3290,9 @@ export async function getTableSchema(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 컬럼 정보를 간단한 형태로 변환 (라벨 정보 포함)
|
// 컬럼 정보를 간단한 형태로 변환
|
||||||
const columnList = columns.map((col: any) => ({
|
const columnList = columns.map((col: any) => ({
|
||||||
name: col.column_name,
|
name: col.column_name,
|
||||||
label: col.column_label || col.column_name, // 라벨이 없으면 컬럼명 사용
|
|
||||||
type: col.data_type,
|
type: col.data_type,
|
||||||
nullable: col.is_nullable === "YES",
|
nullable: col.is_nullable === "YES",
|
||||||
default: col.column_default,
|
default: col.column_default,
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,10 @@ import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRendere
|
||||||
import { ScreenPreviewProvider } from "@/contexts/ScreenPreviewContext";
|
import { ScreenPreviewProvider } from "@/contexts/ScreenPreviewContext";
|
||||||
import { useAuth } from "@/hooks/useAuth"; // 🆕 사용자 정보
|
import { useAuth } from "@/hooks/useAuth"; // 🆕 사용자 정보
|
||||||
import { useResponsive } from "@/lib/hooks/useResponsive"; // 🆕 반응형 감지
|
import { useResponsive } from "@/lib/hooks/useResponsive"; // 🆕 반응형 감지
|
||||||
import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; // 테이블 옵션
|
import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; // 🆕 테이블 옵션
|
||||||
import { TableSearchWidgetHeightProvider, useTableSearchWidgetHeight } from "@/contexts/TableSearchWidgetHeightContext"; // 높이 관리
|
import { TableSearchWidgetHeightProvider, useTableSearchWidgetHeight } from "@/contexts/TableSearchWidgetHeightContext"; // 🆕 높이 관리
|
||||||
import { ScreenContextProvider } from "@/contexts/ScreenContext"; // 컴포넌트 간 통신
|
import { ScreenContextProvider } from "@/contexts/ScreenContext"; // 🆕 컴포넌트 간 통신
|
||||||
import { SplitPanelProvider } from "@/lib/registry/components/split-panel-layout/SplitPanelContext"; // 분할 패널 리사이즈
|
import { SplitPanelProvider } from "@/lib/registry/components/split-panel-layout/SplitPanelContext"; // 🆕 분할 패널 리사이즈
|
||||||
import { ActiveTabProvider } from "@/contexts/ActiveTabContext"; // 활성 탭 관리
|
|
||||||
|
|
||||||
function ScreenViewPage() {
|
function ScreenViewPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
@ -308,8 +307,7 @@ function ScreenViewPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScreenPreviewProvider isPreviewMode={false}>
|
<ScreenPreviewProvider isPreviewMode={false}>
|
||||||
<ActiveTabProvider>
|
<TableOptionsProvider>
|
||||||
<TableOptionsProvider>
|
|
||||||
<div ref={containerRef} className="bg-background h-full w-full overflow-auto p-3">
|
<div ref={containerRef} className="bg-background h-full w-full overflow-auto p-3">
|
||||||
{/* 레이아웃 준비 중 로딩 표시 */}
|
{/* 레이아웃 준비 중 로딩 표시 */}
|
||||||
{!layoutReady && (
|
{!layoutReady && (
|
||||||
|
|
@ -788,8 +786,7 @@ function ScreenViewPage() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TableOptionsProvider>
|
</TableOptionsProvider>
|
||||||
</ActiveTabProvider>
|
|
||||||
</ScreenPreviewProvider>
|
</ScreenPreviewProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import {
|
||||||
Plus,
|
Plus,
|
||||||
Minus,
|
Minus,
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
|
Save,
|
||||||
Zap,
|
Zap,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { importFromExcel, getExcelSheetNames } from "@/lib/utils/excelExport";
|
import { importFromExcel, getExcelSheetNames } from "@/lib/utils/excelExport";
|
||||||
|
|
@ -51,6 +52,12 @@ interface ColumnMapping {
|
||||||
systemColumn: string | null;
|
systemColumn: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UploadConfig {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
mappings: ColumnMapping[];
|
||||||
|
}
|
||||||
|
|
||||||
export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
|
|
@ -81,6 +88,8 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
const [excelColumns, setExcelColumns] = useState<string[]>([]);
|
const [excelColumns, setExcelColumns] = useState<string[]>([]);
|
||||||
const [systemColumns, setSystemColumns] = useState<TableColumn[]>([]);
|
const [systemColumns, setSystemColumns] = useState<TableColumn[]>([]);
|
||||||
const [columnMappings, setColumnMappings] = useState<ColumnMapping[]>([]);
|
const [columnMappings, setColumnMappings] = useState<ColumnMapping[]>([]);
|
||||||
|
const [configName, setConfigName] = useState<string>("");
|
||||||
|
const [configType, setConfigType] = useState<string>("");
|
||||||
|
|
||||||
// 4단계: 확인
|
// 4단계: 확인
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
|
@ -105,7 +114,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
|
|
||||||
const data = await importFromExcel(selectedFile, sheets[0]);
|
const data = await importFromExcel(selectedFile, sheets[0]);
|
||||||
setAllData(data);
|
setAllData(data);
|
||||||
setDisplayData(data); // 전체 데이터를 미리보기에 표시 (스크롤 가능)
|
setDisplayData(data.slice(0, 10));
|
||||||
|
|
||||||
if (data.length > 0) {
|
if (data.length > 0) {
|
||||||
const columns = Object.keys(data[0]);
|
const columns = Object.keys(data[0]);
|
||||||
|
|
@ -130,7 +139,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
try {
|
try {
|
||||||
const data = await importFromExcel(file, sheetName);
|
const data = await importFromExcel(file, sheetName);
|
||||||
setAllData(data);
|
setAllData(data);
|
||||||
setDisplayData(data); // 전체 데이터를 미리보기에 표시 (스크롤 가능)
|
setDisplayData(data.slice(0, 10));
|
||||||
|
|
||||||
if (data.length > 0) {
|
if (data.length > 0) {
|
||||||
const columns = Object.keys(data[0]);
|
const columns = Object.keys(data[0]);
|
||||||
|
|
@ -227,23 +236,13 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 자동 매핑 - 컬럼명과 라벨 모두 비교
|
// 자동 매핑
|
||||||
const handleAutoMapping = () => {
|
const handleAutoMapping = () => {
|
||||||
const newMappings = excelColumns.map((excelCol) => {
|
const newMappings = excelColumns.map((excelCol) => {
|
||||||
const normalizedExcelCol = excelCol.toLowerCase().trim();
|
const matchedSystemCol = systemColumns.find(
|
||||||
|
(sysCol) => sysCol.name.toLowerCase() === excelCol.toLowerCase()
|
||||||
// 1. 먼저 라벨로 매칭 시도
|
|
||||||
let matchedSystemCol = systemColumns.find(
|
|
||||||
(sysCol) => sysCol.label && sysCol.label.toLowerCase().trim() === normalizedExcelCol
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2. 라벨로 매칭되지 않으면 컬럼명으로 매칭 시도
|
|
||||||
if (!matchedSystemCol) {
|
|
||||||
matchedSystemCol = systemColumns.find(
|
|
||||||
(sysCol) => sysCol.name.toLowerCase().trim() === normalizedExcelCol
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
excelColumn: excelCol,
|
excelColumn: excelCol,
|
||||||
systemColumn: matchedSystemCol ? matchedSystemCol.name : null,
|
systemColumn: matchedSystemCol ? matchedSystemCol.name : null,
|
||||||
|
|
@ -266,6 +265,28 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 설정 저장
|
||||||
|
const handleSaveConfig = () => {
|
||||||
|
if (!configName.trim()) {
|
||||||
|
toast.error("거래처명을 입력해주세요.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: UploadConfig = {
|
||||||
|
name: configName,
|
||||||
|
type: configType,
|
||||||
|
mappings: columnMappings,
|
||||||
|
};
|
||||||
|
|
||||||
|
const savedConfigs = JSON.parse(
|
||||||
|
localStorage.getItem("excelUploadConfigs") || "[]"
|
||||||
|
);
|
||||||
|
savedConfigs.push(config);
|
||||||
|
localStorage.setItem("excelUploadConfigs", JSON.stringify(savedConfigs));
|
||||||
|
|
||||||
|
toast.success("설정이 저장되었습니다.");
|
||||||
|
};
|
||||||
|
|
||||||
// 다음 단계
|
// 다음 단계
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
if (currentStep === 1 && !file) {
|
if (currentStep === 1 && !file) {
|
||||||
|
|
@ -296,8 +317,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// allData를 사용하여 전체 데이터 업로드 (displayData는 미리보기용 10개만)
|
const mappedData = displayData.map((row) => {
|
||||||
const mappedData = allData.map((row) => {
|
|
||||||
const mappedRow: Record<string, any> = {};
|
const mappedRow: Record<string, any> = {};
|
||||||
columnMappings.forEach((mapping) => {
|
columnMappings.forEach((mapping) => {
|
||||||
if (mapping.systemColumn) {
|
if (mapping.systemColumn) {
|
||||||
|
|
@ -359,6 +379,8 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
setExcelColumns([]);
|
setExcelColumns([]);
|
||||||
setSystemColumns([]);
|
setSystemColumns([]);
|
||||||
setColumnMappings([]);
|
setColumnMappings([]);
|
||||||
|
setConfigName("");
|
||||||
|
setConfigType("");
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
|
|
@ -667,25 +689,27 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 3단계: 컬럼 매핑 */}
|
{/* 3단계: 컬럼 매핑 - 3단 레이아웃 */}
|
||||||
{currentStep === 3 && (
|
{currentStep === 3 && (
|
||||||
<div className="space-y-4">
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-[2fr_3fr_2fr]">
|
||||||
{/* 상단: 제목 + 자동 매핑 버튼 */}
|
{/* 왼쪽: 컬럼 매핑 설정 제목 + 자동 매핑 버튼 */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="space-y-4">
|
||||||
<h3 className="text-sm font-semibold sm:text-base">컬럼 매핑 설정</h3>
|
<div>
|
||||||
<Button
|
<h3 className="mb-3 text-sm font-semibold sm:text-base">컬럼 매핑 설정</h3>
|
||||||
type="button"
|
<Button
|
||||||
variant="default"
|
type="button"
|
||||||
size="sm"
|
variant="default"
|
||||||
onClick={handleAutoMapping}
|
size="sm"
|
||||||
className="h-8 text-xs sm:h-9 sm:text-sm"
|
onClick={handleAutoMapping}
|
||||||
>
|
className="h-8 w-full text-xs sm:h-9 sm:text-sm"
|
||||||
<Zap className="mr-2 h-4 w-4" />
|
>
|
||||||
자동 매핑
|
<Zap className="mr-2 h-4 w-4" />
|
||||||
</Button>
|
자동 매핑
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 매핑 리스트 */}
|
{/* 중앙: 매핑 리스트 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="grid grid-cols-[1fr_auto_1fr] gap-2 text-[10px] font-medium text-muted-foreground sm:text-xs">
|
<div className="grid grid-cols-[1fr_auto_1fr] gap-2 text-[10px] font-medium text-muted-foreground sm:text-xs">
|
||||||
<div>엑셀 컬럼</div>
|
<div>엑셀 컬럼</div>
|
||||||
|
|
@ -710,14 +734,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
||||||
<SelectValue placeholder="매핑 안함">
|
<SelectValue placeholder="매핑 안함" />
|
||||||
{mapping.systemColumn
|
|
||||||
? (() => {
|
|
||||||
const col = systemColumns.find(c => c.name === mapping.systemColumn);
|
|
||||||
return col?.label || mapping.systemColumn;
|
|
||||||
})()
|
|
||||||
: "매핑 안함"}
|
|
||||||
</SelectValue>
|
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="none" className="text-xs sm:text-sm">
|
<SelectItem value="none" className="text-xs sm:text-sm">
|
||||||
|
|
@ -729,7 +746,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
value={col.name}
|
value={col.name}
|
||||||
className="text-xs sm:text-sm"
|
className="text-xs sm:text-sm"
|
||||||
>
|
>
|
||||||
{col.label || col.name} ({col.type})
|
{col.name} ({col.type})
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
@ -738,6 +755,50 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 오른쪽: 현재 설정 저장 */}
|
||||||
|
<div className="rounded-md border border-border bg-muted/30 p-4">
|
||||||
|
<div className="mb-4 flex items-center gap-2">
|
||||||
|
<Save className="h-4 w-4" />
|
||||||
|
<h3 className="text-sm font-semibold sm:text-base">현재 설정 저장</h3>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="config-name" className="text-[10px] sm:text-xs">
|
||||||
|
거래처명 *
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="config-name"
|
||||||
|
value={configName}
|
||||||
|
onChange={(e) => setConfigName(e.target.value)}
|
||||||
|
placeholder="거래처 선택"
|
||||||
|
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="config-type" className="text-[10px] sm:text-xs">
|
||||||
|
유형
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="config-type"
|
||||||
|
value={configType}
|
||||||
|
onChange={(e) => setConfigType(e.target.value)}
|
||||||
|
placeholder="유형을 입력하세요 (예: 원자재)"
|
||||||
|
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSaveConfig}
|
||||||
|
className="h-8 w-full text-xs sm:h-9 sm:text-sm"
|
||||||
|
>
|
||||||
|
<Save className="mr-2 h-3 w-3" />
|
||||||
|
설정 저장
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -754,7 +815,7 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
<span className="font-medium">시트:</span> {selectedSheet}
|
<span className="font-medium">시트:</span> {selectedSheet}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="font-medium">데이터 행:</span> {allData.length}개
|
<span className="font-medium">데이터 행:</span> {displayData.length}개
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="font-medium">테이블:</span> {tableName}
|
<span className="font-medium">테이블:</span> {tableName}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import { useAuth } from "@/hooks/useAuth";
|
||||||
import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
|
import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
|
||||||
import { TableSearchWidgetHeightProvider } from "@/contexts/TableSearchWidgetHeightContext";
|
import { TableSearchWidgetHeightProvider } from "@/contexts/TableSearchWidgetHeightContext";
|
||||||
import { useSplitPanelContext } from "@/contexts/SplitPanelContext";
|
import { useSplitPanelContext } from "@/contexts/SplitPanelContext";
|
||||||
import { ActiveTabProvider } from "@/contexts/ActiveTabContext";
|
|
||||||
|
|
||||||
interface ScreenModalState {
|
interface ScreenModalState {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|
@ -667,7 +666,6 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : screenData ? (
|
) : screenData ? (
|
||||||
<ActiveTabProvider>
|
|
||||||
<TableOptionsProvider>
|
<TableOptionsProvider>
|
||||||
<div
|
<div
|
||||||
className="relative mx-auto bg-white"
|
className="relative mx-auto bg-white"
|
||||||
|
|
@ -740,7 +738,6 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</TableOptionsProvider>
|
</TableOptionsProvider>
|
||||||
</ActiveTabProvider>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
<p className="text-muted-foreground">화면 데이터가 없습니다.</p>
|
<p className="text-muted-foreground">화면 데이터가 없습니다.</p>
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,6 @@ import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
|
||||||
import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
|
import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
|
||||||
import { TableOptionsToolbar } from "./table-options/TableOptionsToolbar";
|
import { TableOptionsToolbar } from "./table-options/TableOptionsToolbar";
|
||||||
import { SplitPanelProvider } from "@/lib/registry/components/split-panel-layout/SplitPanelContext";
|
import { SplitPanelProvider } from "@/lib/registry/components/split-panel-layout/SplitPanelContext";
|
||||||
import { ActiveTabProvider } from "@/contexts/ActiveTabContext";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 🔗 연쇄 드롭다운 래퍼 컴포넌트
|
* 🔗 연쇄 드롭다운 래퍼 컴포넌트
|
||||||
|
|
@ -2104,8 +2103,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SplitPanelProvider>
|
<SplitPanelProvider>
|
||||||
<ActiveTabProvider>
|
<TableOptionsProvider>
|
||||||
<TableOptionsProvider>
|
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
{/* 테이블 옵션 툴바 */}
|
{/* 테이블 옵션 툴바 */}
|
||||||
<TableOptionsToolbar />
|
<TableOptionsToolbar />
|
||||||
|
|
@ -2212,8 +2210,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</TableOptionsProvider>
|
</TableOptionsProvider>
|
||||||
</ActiveTabProvider>
|
|
||||||
</SplitPanelProvider>
|
</SplitPanelProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -39,25 +39,22 @@ interface InteractiveScreenViewerProps {
|
||||||
id: number;
|
id: number;
|
||||||
tableName?: string;
|
tableName?: string;
|
||||||
};
|
};
|
||||||
menuObjid?: number; // 메뉴 OBJID (코드 스코프용)
|
menuObjid?: number; // 🆕 메뉴 OBJID (코드 스코프용)
|
||||||
onSave?: () => Promise<void>;
|
onSave?: () => Promise<void>;
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
onFlowRefresh?: () => void;
|
onFlowRefresh?: () => void;
|
||||||
// 외부에서 전달받는 사용자 정보 (ScreenModal 등에서 사용)
|
// 🆕 외부에서 전달받는 사용자 정보 (ScreenModal 등에서 사용)
|
||||||
userId?: string;
|
userId?: string;
|
||||||
userName?: string;
|
userName?: string;
|
||||||
companyCode?: string;
|
companyCode?: string;
|
||||||
// 그룹 데이터 (EditModal에서 전달)
|
// 🆕 그룹 데이터 (EditModal에서 전달)
|
||||||
groupedData?: Record<string, any>[];
|
groupedData?: Record<string, any>[];
|
||||||
// 비활성화할 필드 목록 (EditModal에서 전달)
|
// 🆕 비활성화할 필드 목록 (EditModal에서 전달)
|
||||||
disabledFields?: string[];
|
disabledFields?: string[];
|
||||||
// EditModal 내부인지 여부 (button-primary가 EditModal의 handleSave 사용하도록)
|
// 🆕 EditModal 내부인지 여부 (button-primary가 EditModal의 handleSave 사용하도록)
|
||||||
isInModal?: boolean;
|
isInModal?: boolean;
|
||||||
// 원본 데이터 (수정 모드에서 UPDATE 판단용)
|
// 🆕 원본 데이터 (수정 모드에서 UPDATE 판단용)
|
||||||
originalData?: Record<string, any> | null;
|
originalData?: Record<string, any> | null;
|
||||||
// 탭 관련 정보 (탭 내부의 컴포넌트에서 사용)
|
|
||||||
parentTabId?: string; // 부모 탭 ID
|
|
||||||
parentTabsComponentId?: string; // 부모 탭 컴포넌트 ID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerProps> = ({
|
export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerProps> = ({
|
||||||
|
|
@ -77,9 +74,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
groupedData,
|
groupedData,
|
||||||
disabledFields = [],
|
disabledFields = [],
|
||||||
isInModal = false,
|
isInModal = false,
|
||||||
originalData,
|
originalData, // 🆕 원본 데이터 (수정 모드에서 UPDATE 판단용)
|
||||||
parentTabId,
|
|
||||||
parentTabsComponentId,
|
|
||||||
}) => {
|
}) => {
|
||||||
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||||
const { userName: authUserName, user: authUser } = useAuth();
|
const { userName: authUserName, user: authUser } = useAuth();
|
||||||
|
|
@ -364,43 +359,43 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
component={comp}
|
component={comp}
|
||||||
isInteractive={true}
|
isInteractive={true}
|
||||||
formData={formData}
|
formData={formData}
|
||||||
originalData={originalData || undefined}
|
originalData={originalData || undefined} // 🆕 원본 데이터 전달 (UPDATE 판단용)
|
||||||
onFormDataChange={handleFormDataChange}
|
onFormDataChange={handleFormDataChange}
|
||||||
screenId={screenInfo?.id}
|
screenId={screenInfo?.id}
|
||||||
tableName={screenInfo?.tableName}
|
tableName={screenInfo?.tableName}
|
||||||
menuObjid={menuObjid}
|
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
|
||||||
userId={user?.userId}
|
userId={user?.userId} // ✅ 사용자 ID 전달
|
||||||
userName={user?.userName}
|
userName={user?.userName} // ✅ 사용자 이름 전달
|
||||||
companyCode={user?.companyCode}
|
companyCode={user?.companyCode} // ✅ 회사 코드 전달
|
||||||
onSave={onSave}
|
onSave={onSave} // 🆕 EditModal의 handleSave 콜백 전달
|
||||||
allComponents={allComponents}
|
allComponents={allComponents} // 🆕 같은 화면의 모든 컴포넌트 전달 (TableList 자동 감지용)
|
||||||
selectedRowsData={selectedRowsData}
|
selectedRowsData={selectedRowsData}
|
||||||
onSelectedRowsChange={(selectedRows, selectedData) => {
|
onSelectedRowsChange={(selectedRows, selectedData) => {
|
||||||
console.log("테이블에서 선택된 행 데이터:", selectedData);
|
console.log("🔍 테이블에서 선택된 행 데이터:", selectedData);
|
||||||
setSelectedRowsData(selectedData);
|
setSelectedRowsData(selectedData);
|
||||||
}}
|
}}
|
||||||
|
// 🆕 그룹 데이터 전달 (EditModal → ModalRepeaterTable)
|
||||||
groupedData={groupedData}
|
groupedData={groupedData}
|
||||||
|
// 🆕 비활성화 필드 전달 (EditModal → 각 컴포넌트)
|
||||||
disabledFields={disabledFields}
|
disabledFields={disabledFields}
|
||||||
flowSelectedData={flowSelectedData}
|
flowSelectedData={flowSelectedData}
|
||||||
flowSelectedStepId={flowSelectedStepId}
|
flowSelectedStepId={flowSelectedStepId}
|
||||||
onFlowSelectedDataChange={(selectedData, stepId) => {
|
onFlowSelectedDataChange={(selectedData, stepId) => {
|
||||||
console.log("플로우에서 선택된 데이터:", { selectedData, stepId });
|
console.log("🔍 플로우에서 선택된 데이터:", { selectedData, stepId });
|
||||||
setFlowSelectedData(selectedData);
|
setFlowSelectedData(selectedData);
|
||||||
setFlowSelectedStepId(stepId);
|
setFlowSelectedStepId(stepId);
|
||||||
}}
|
}}
|
||||||
onRefresh={
|
onRefresh={
|
||||||
onRefresh ||
|
onRefresh ||
|
||||||
(() => {
|
(() => {
|
||||||
console.log("InteractiveScreenViewerDynamic onRefresh 호출");
|
// 부모로부터 전달받은 onRefresh 또는 기본 동작
|
||||||
|
console.log("🔄 InteractiveScreenViewerDynamic onRefresh 호출");
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
onFlowRefresh={onFlowRefresh}
|
onFlowRefresh={onFlowRefresh}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
// buttonActions.ts가 이미 처리함
|
// buttonActions.ts가 이미 처리함
|
||||||
}}
|
}}
|
||||||
// 탭 관련 정보 전달
|
|
||||||
parentTabId={parentTabId}
|
|
||||||
parentTabsComponentId={parentTabsComponentId}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -958,10 +958,6 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
characterMaximumLength: col.characterMaximumLength || col.character_maximum_length,
|
characterMaximumLength: col.characterMaximumLength || col.character_maximum_length,
|
||||||
codeCategory: col.codeCategory || col.code_category,
|
codeCategory: col.codeCategory || col.code_category,
|
||||||
codeValue: col.codeValue || col.code_value,
|
codeValue: col.codeValue || col.code_value,
|
||||||
// 엔티티 타입용 참조 테이블 정보
|
|
||||||
referenceTable: col.referenceTable || col.reference_table,
|
|
||||||
referenceColumn: col.referenceColumn || col.reference_column,
|
|
||||||
displayColumn: col.displayColumn || col.display_column,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,18 @@ import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Database, Search, Info } from "lucide-react";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Database, Search, Plus, Trash2 } from "lucide-react";
|
||||||
import { WebTypeConfigPanelProps } from "@/lib/registry/types";
|
import { WebTypeConfigPanelProps } from "@/lib/registry/types";
|
||||||
import { WidgetComponent, EntityTypeConfig } from "@/types/screen";
|
import { WidgetComponent, EntityTypeConfig } from "@/types/screen";
|
||||||
import { tableTypeApi } from "@/lib/api/screen";
|
|
||||||
|
interface EntityField {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
type: string;
|
||||||
|
visible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
component,
|
component,
|
||||||
|
|
@ -19,31 +27,16 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
const widget = component as WidgetComponent;
|
const widget = component as WidgetComponent;
|
||||||
const config = (widget.webTypeConfig as EntityTypeConfig) || {};
|
const config = (widget.webTypeConfig as EntityTypeConfig) || {};
|
||||||
|
|
||||||
// 테이블 타입 관리에서 설정된 참조 테이블 정보
|
// 로컬 상태
|
||||||
const [referenceInfo, setReferenceInfo] = useState<{
|
|
||||||
referenceTable: string;
|
|
||||||
referenceColumn: string;
|
|
||||||
displayColumn: string;
|
|
||||||
isLoading: boolean;
|
|
||||||
error: string | null;
|
|
||||||
}>({
|
|
||||||
referenceTable: "",
|
|
||||||
referenceColumn: "",
|
|
||||||
displayColumn: "",
|
|
||||||
isLoading: true,
|
|
||||||
error: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 로컬 상태 (UI 관련 설정만)
|
|
||||||
const [localConfig, setLocalConfig] = useState<EntityTypeConfig>({
|
const [localConfig, setLocalConfig] = useState<EntityTypeConfig>({
|
||||||
entityType: config.entityType || "",
|
entityType: config.entityType || "",
|
||||||
displayFields: config.displayFields || [],
|
displayFields: config.displayFields || [],
|
||||||
searchFields: config.searchFields || [],
|
searchFields: config.searchFields || [],
|
||||||
valueField: config.valueField || "",
|
valueField: config.valueField || "id",
|
||||||
labelField: config.labelField || "",
|
labelField: config.labelField || "name",
|
||||||
multiple: config.multiple || false,
|
multiple: config.multiple || false,
|
||||||
searchable: config.searchable !== false,
|
searchable: config.searchable !== false, // 기본값 true
|
||||||
placeholder: config.placeholder || "항목을 선택하세요",
|
placeholder: config.placeholder || "엔티티를 선택하세요",
|
||||||
emptyMessage: config.emptyMessage || "검색 결과가 없습니다",
|
emptyMessage: config.emptyMessage || "검색 결과가 없습니다",
|
||||||
pageSize: config.pageSize || 20,
|
pageSize: config.pageSize || 20,
|
||||||
minSearchLength: config.minSearchLength || 1,
|
minSearchLength: config.minSearchLength || 1,
|
||||||
|
|
@ -54,95 +47,10 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
filters: config.filters || {},
|
filters: config.filters || {},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 테이블 타입 관리에서 설정된 참조 테이블 정보 로드
|
// 새 필드 추가용 상태
|
||||||
useEffect(() => {
|
const [newFieldName, setNewFieldName] = useState("");
|
||||||
const loadReferenceInfo = async () => {
|
const [newFieldLabel, setNewFieldLabel] = useState("");
|
||||||
// 컴포넌트의 테이블명과 컬럼명이 있는 경우에만 조회
|
const [newFieldType, setNewFieldType] = useState("string");
|
||||||
const tableName = widget.tableName;
|
|
||||||
const columnName = widget.columnName;
|
|
||||||
|
|
||||||
if (!tableName || !columnName) {
|
|
||||||
setReferenceInfo({
|
|
||||||
referenceTable: "",
|
|
||||||
referenceColumn: "",
|
|
||||||
displayColumn: "",
|
|
||||||
isLoading: false,
|
|
||||||
error: "테이블 또는 컬럼 정보가 없습니다.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 테이블 타입 관리에서 컬럼 정보 조회
|
|
||||||
const columns = await tableTypeApi.getColumns(tableName);
|
|
||||||
const columnInfo = columns.find((col: any) =>
|
|
||||||
(col.columnName || col.column_name) === columnName
|
|
||||||
);
|
|
||||||
|
|
||||||
if (columnInfo) {
|
|
||||||
const refTable = columnInfo.referenceTable || columnInfo.reference_table || "";
|
|
||||||
const refColumn = columnInfo.referenceColumn || columnInfo.reference_column || "";
|
|
||||||
const dispColumn = columnInfo.displayColumn || columnInfo.display_column || "";
|
|
||||||
|
|
||||||
// detailSettings에서도 정보 확인 (JSON 파싱)
|
|
||||||
let detailSettings: any = {};
|
|
||||||
if (columnInfo.detailSettings) {
|
|
||||||
try {
|
|
||||||
if (typeof columnInfo.detailSettings === 'string') {
|
|
||||||
detailSettings = JSON.parse(columnInfo.detailSettings);
|
|
||||||
} else {
|
|
||||||
detailSettings = columnInfo.detailSettings;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// JSON 파싱 실패 시 무시
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalRefTable = refTable || detailSettings.referenceTable || "";
|
|
||||||
const finalRefColumn = refColumn || detailSettings.referenceColumn || "";
|
|
||||||
const finalDispColumn = dispColumn || detailSettings.displayColumn || "";
|
|
||||||
|
|
||||||
setReferenceInfo({
|
|
||||||
referenceTable: finalRefTable,
|
|
||||||
referenceColumn: finalRefColumn,
|
|
||||||
displayColumn: finalDispColumn,
|
|
||||||
isLoading: false,
|
|
||||||
error: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
// webTypeConfig에 참조 테이블 정보 자동 설정
|
|
||||||
if (finalRefTable) {
|
|
||||||
const newConfig = {
|
|
||||||
...localConfig,
|
|
||||||
valueField: finalRefColumn || "id",
|
|
||||||
labelField: finalDispColumn || "name",
|
|
||||||
};
|
|
||||||
setLocalConfig(newConfig);
|
|
||||||
onUpdateProperty("webTypeConfig", newConfig);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setReferenceInfo({
|
|
||||||
referenceTable: "",
|
|
||||||
referenceColumn: "",
|
|
||||||
displayColumn: "",
|
|
||||||
isLoading: false,
|
|
||||||
error: "컬럼 정보를 찾을 수 없습니다.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("참조 테이블 정보 로드 실패:", error);
|
|
||||||
setReferenceInfo({
|
|
||||||
referenceTable: "",
|
|
||||||
referenceColumn: "",
|
|
||||||
displayColumn: "",
|
|
||||||
isLoading: false,
|
|
||||||
error: "참조 테이블 정보 로드 실패",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadReferenceInfo();
|
|
||||||
}, [widget.tableName, widget.columnName]);
|
|
||||||
|
|
||||||
// 컴포넌트 변경 시 로컬 상태 동기화
|
// 컴포넌트 변경 시 로컬 상태 동기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -151,11 +59,11 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
entityType: currentConfig.entityType || "",
|
entityType: currentConfig.entityType || "",
|
||||||
displayFields: currentConfig.displayFields || [],
|
displayFields: currentConfig.displayFields || [],
|
||||||
searchFields: currentConfig.searchFields || [],
|
searchFields: currentConfig.searchFields || [],
|
||||||
valueField: currentConfig.valueField || referenceInfo.referenceColumn || "",
|
valueField: currentConfig.valueField || "id",
|
||||||
labelField: currentConfig.labelField || referenceInfo.displayColumn || "",
|
labelField: currentConfig.labelField || "name",
|
||||||
multiple: currentConfig.multiple || false,
|
multiple: currentConfig.multiple || false,
|
||||||
searchable: currentConfig.searchable !== false,
|
searchable: currentConfig.searchable !== false,
|
||||||
placeholder: currentConfig.placeholder || "항목을 선택하세요",
|
placeholder: currentConfig.placeholder || "엔티티를 선택하세요",
|
||||||
emptyMessage: currentConfig.emptyMessage || "검색 결과가 없습니다",
|
emptyMessage: currentConfig.emptyMessage || "검색 결과가 없습니다",
|
||||||
pageSize: currentConfig.pageSize || 20,
|
pageSize: currentConfig.pageSize || 20,
|
||||||
minSearchLength: currentConfig.minSearchLength || 1,
|
minSearchLength: currentConfig.minSearchLength || 1,
|
||||||
|
|
@ -165,7 +73,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
apiEndpoint: currentConfig.apiEndpoint || "",
|
apiEndpoint: currentConfig.apiEndpoint || "",
|
||||||
filters: currentConfig.filters || {},
|
filters: currentConfig.filters || {},
|
||||||
});
|
});
|
||||||
}, [widget.webTypeConfig, referenceInfo.referenceColumn, referenceInfo.displayColumn]);
|
}, [widget.webTypeConfig]);
|
||||||
|
|
||||||
// 설정 업데이트 핸들러 (즉시 부모에게 전달 - 드롭다운, 체크박스 등)
|
// 설정 업데이트 핸들러 (즉시 부모에게 전달 - 드롭다운, 체크박스 등)
|
||||||
const updateConfig = (field: keyof EntityTypeConfig, value: any) => {
|
const updateConfig = (field: keyof EntityTypeConfig, value: any) => {
|
||||||
|
|
@ -184,6 +92,89 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
onUpdateProperty("webTypeConfig", localConfig);
|
onUpdateProperty("webTypeConfig", localConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 필드 추가
|
||||||
|
const addDisplayField = () => {
|
||||||
|
if (!newFieldName.trim() || !newFieldLabel.trim()) return;
|
||||||
|
|
||||||
|
const newField: EntityField = {
|
||||||
|
name: newFieldName.trim(),
|
||||||
|
label: newFieldLabel.trim(),
|
||||||
|
type: newFieldType,
|
||||||
|
visible: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newFields = [...localConfig.displayFields, newField];
|
||||||
|
updateConfig("displayFields", newFields);
|
||||||
|
setNewFieldName("");
|
||||||
|
setNewFieldLabel("");
|
||||||
|
setNewFieldType("string");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 필드 제거
|
||||||
|
const removeDisplayField = (index: number) => {
|
||||||
|
const newFields = localConfig.displayFields.filter((_, i) => i !== index);
|
||||||
|
updateConfig("displayFields", newFields);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 필드 업데이트 (입력 중) - 로컬 상태만 업데이트
|
||||||
|
const updateDisplayField = (index: number, field: keyof EntityField, value: any) => {
|
||||||
|
const newFields = [...localConfig.displayFields];
|
||||||
|
newFields[index] = { ...newFields[index], [field]: value };
|
||||||
|
setLocalConfig({ ...localConfig, displayFields: newFields });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 필드 업데이트 완료 (onBlur) - 부모에게 전달
|
||||||
|
const handleFieldBlur = () => {
|
||||||
|
onUpdateProperty("webTypeConfig", localConfig);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 검색 필드 토글
|
||||||
|
const toggleSearchField = (fieldName: string) => {
|
||||||
|
const currentSearchFields = localConfig.searchFields || [];
|
||||||
|
const newSearchFields = currentSearchFields.includes(fieldName)
|
||||||
|
? currentSearchFields.filter((f) => f !== fieldName)
|
||||||
|
: [...currentSearchFields, fieldName];
|
||||||
|
updateConfig("searchFields", newSearchFields);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 기본 엔티티 타입들
|
||||||
|
const commonEntityTypes = [
|
||||||
|
{ value: "user", label: "사용자", fields: ["id", "name", "email", "department"] },
|
||||||
|
{ value: "department", label: "부서", fields: ["id", "name", "code", "parentId"] },
|
||||||
|
{ value: "product", label: "제품", fields: ["id", "name", "code", "category", "price"] },
|
||||||
|
{ value: "customer", label: "고객", fields: ["id", "name", "company", "contact"] },
|
||||||
|
{ value: "project", label: "프로젝트", fields: ["id", "name", "status", "manager", "startDate"] },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 기본 엔티티 타입 적용
|
||||||
|
const applyEntityType = (entityType: string) => {
|
||||||
|
const entityConfig = commonEntityTypes.find((e) => e.value === entityType);
|
||||||
|
if (!entityConfig) return;
|
||||||
|
|
||||||
|
updateConfig("entityType", entityType);
|
||||||
|
updateConfig("apiEndpoint", `/api/entities/${entityType}`);
|
||||||
|
|
||||||
|
const defaultFields: EntityField[] = entityConfig.fields.map((field) => ({
|
||||||
|
name: field,
|
||||||
|
label: field.charAt(0).toUpperCase() + field.slice(1),
|
||||||
|
type: field.includes("Date") ? "date" : field.includes("price") || field.includes("Id") ? "number" : "string",
|
||||||
|
visible: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
updateConfig("displayFields", defaultFields);
|
||||||
|
updateConfig("searchFields", [entityConfig.fields[1] || "name"]); // 두 번째 필드를 기본 검색 필드로
|
||||||
|
};
|
||||||
|
|
||||||
|
// 필드 타입 옵션
|
||||||
|
const fieldTypes = [
|
||||||
|
{ value: "string", label: "문자열" },
|
||||||
|
{ value: "number", label: "숫자" },
|
||||||
|
{ value: "date", label: "날짜" },
|
||||||
|
{ value: "boolean", label: "불린" },
|
||||||
|
{ value: "email", label: "이메일" },
|
||||||
|
{ value: "url", label: "URL" },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|
@ -191,70 +182,12 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
<Database className="h-4 w-4" />
|
<Database className="h-4 w-4" />
|
||||||
엔티티 설정
|
엔티티 설정
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs">
|
<CardDescription className="text-xs">데이터베이스 엔티티 선택 필드의 설정을 관리합니다.</CardDescription>
|
||||||
데이터베이스 엔티티 선택 필드의 설정을 관리합니다.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{/* 참조 테이블 정보 (테이블 타입 관리에서 설정된 값 - 읽기 전용) */}
|
{/* 기본 설정 */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h4 className="text-sm font-medium flex items-center gap-2">
|
<h4 className="text-sm font-medium">기본 설정</h4>
|
||||||
참조 테이블 정보
|
|
||||||
<span className="bg-muted text-muted-foreground px-1.5 py-0.5 rounded text-[10px]">
|
|
||||||
테이블 타입 관리에서 설정
|
|
||||||
</span>
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
{referenceInfo.isLoading ? (
|
|
||||||
<div className="bg-muted/50 rounded-md border p-3">
|
|
||||||
<p className="text-xs text-muted-foreground">참조 테이블 정보 로딩 중...</p>
|
|
||||||
</div>
|
|
||||||
) : referenceInfo.error ? (
|
|
||||||
<div className="bg-destructive/10 rounded-md border border-destructive/20 p-3">
|
|
||||||
<p className="text-xs text-destructive flex items-center gap-1">
|
|
||||||
<Info className="h-3 w-3" />
|
|
||||||
{referenceInfo.error}
|
|
||||||
</p>
|
|
||||||
<p className="text-[10px] text-muted-foreground mt-1">
|
|
||||||
테이블 타입 관리에서 이 컬럼의 참조 테이블을 먼저 설정해주세요.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : !referenceInfo.referenceTable ? (
|
|
||||||
<div className="bg-amber-500/10 rounded-md border border-amber-500/20 p-3">
|
|
||||||
<p className="text-xs text-amber-700 flex items-center gap-1">
|
|
||||||
<Info className="h-3 w-3" />
|
|
||||||
참조 테이블이 설정되지 않았습니다.
|
|
||||||
</p>
|
|
||||||
<p className="text-[10px] text-muted-foreground mt-1">
|
|
||||||
테이블 타입 관리에서 이 컬럼의 참조 테이블을 먼저 설정해주세요.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="bg-muted/50 rounded-md border p-3 space-y-2">
|
|
||||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">참조 테이블:</span>
|
|
||||||
<div className="font-medium">{referenceInfo.referenceTable}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">참조 컬럼:</span>
|
|
||||||
<div className="font-medium">{referenceInfo.referenceColumn || "-"}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">표시 컬럼:</span>
|
|
||||||
<div className="font-medium">{referenceInfo.displayColumn || "-"}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-[10px] text-muted-foreground">
|
|
||||||
이 정보는 테이블 타입 관리에서 변경할 수 있습니다.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* UI 모드 설정 */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
<h4 className="text-sm font-medium">UI 설정</h4>
|
|
||||||
|
|
||||||
{/* UI 모드 선택 */}
|
{/* UI 모드 선택 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|
@ -283,6 +216,208 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="entityType" className="text-xs">
|
||||||
|
엔티티 타입
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="entityType"
|
||||||
|
value={localConfig.entityType || ""}
|
||||||
|
onChange={(e) => updateConfigLocal("entityType", e.target.value)}
|
||||||
|
onBlur={handleInputBlur}
|
||||||
|
placeholder="user, product, department..."
|
||||||
|
className="text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs">기본 엔티티 타입</Label>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{commonEntityTypes.map((entity) => (
|
||||||
|
<Button
|
||||||
|
key={entity.value}
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => applyEntityType(entity.value)}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
{entity.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="apiEndpoint" className="text-xs">
|
||||||
|
API 엔드포인트
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="apiEndpoint"
|
||||||
|
value={localConfig.apiEndpoint || ""}
|
||||||
|
onChange={(e) => updateConfigLocal("apiEndpoint", e.target.value)}
|
||||||
|
onBlur={handleInputBlur}
|
||||||
|
placeholder="/api/entities/user"
|
||||||
|
className="text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 필드 매핑 */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h4 className="text-sm font-medium">필드 매핑</h4>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="valueField" className="text-xs">
|
||||||
|
값 필드
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="valueField"
|
||||||
|
value={localConfig.valueField || ""}
|
||||||
|
onChange={(e) => updateConfigLocal("valueField", e.target.value)}
|
||||||
|
onBlur={handleInputBlur}
|
||||||
|
placeholder="id"
|
||||||
|
className="text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="labelField" className="text-xs">
|
||||||
|
라벨 필드
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="labelField"
|
||||||
|
value={localConfig.labelField || ""}
|
||||||
|
onChange={(e) => updateConfigLocal("labelField", e.target.value)}
|
||||||
|
onBlur={handleInputBlur}
|
||||||
|
placeholder="name"
|
||||||
|
className="text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 표시 필드 관리 */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h4 className="text-sm font-medium">표시 필드</h4>
|
||||||
|
|
||||||
|
{/* 새 필드 추가 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs">필드 추가</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
value={newFieldName}
|
||||||
|
onChange={(e) => setNewFieldName(e.target.value)}
|
||||||
|
placeholder="필드명"
|
||||||
|
className="flex-1 text-xs"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={newFieldLabel}
|
||||||
|
onChange={(e) => setNewFieldLabel(e.target.value)}
|
||||||
|
placeholder="라벨"
|
||||||
|
className="flex-1 text-xs"
|
||||||
|
/>
|
||||||
|
<Select value={newFieldType} onValueChange={setNewFieldType}>
|
||||||
|
<SelectTrigger className="w-24 text-xs">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{fieldTypes.map((type) => (
|
||||||
|
<SelectItem key={type.value} value={type.value}>
|
||||||
|
{type.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={addDisplayField}
|
||||||
|
disabled={!newFieldName.trim() || !newFieldLabel.trim()}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Plus className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 현재 필드 목록 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs">표시 필드 ({localConfig.displayFields.length}개)</Label>
|
||||||
|
<div className="max-h-40 space-y-2 overflow-y-auto">
|
||||||
|
{localConfig.displayFields.map((field, index) => (
|
||||||
|
<div key={`${field.name}-${index}`} className="flex items-center gap-2 rounded border p-2">
|
||||||
|
<Switch
|
||||||
|
checked={field.visible}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
const newFields = [...localConfig.displayFields];
|
||||||
|
newFields[index] = { ...newFields[index], visible: checked };
|
||||||
|
const newConfig = { ...localConfig, displayFields: newFields };
|
||||||
|
setLocalConfig(newConfig);
|
||||||
|
onUpdateProperty("webTypeConfig", newConfig);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={field.name}
|
||||||
|
onChange={(e) => updateDisplayField(index, "name", e.target.value)}
|
||||||
|
onBlur={handleFieldBlur}
|
||||||
|
placeholder="필드명"
|
||||||
|
className="flex-1 text-xs"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={field.label}
|
||||||
|
onChange={(e) => updateDisplayField(index, "label", e.target.value)}
|
||||||
|
onBlur={handleFieldBlur}
|
||||||
|
placeholder="라벨"
|
||||||
|
className="flex-1 text-xs"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={field.type}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const newFields = [...localConfig.displayFields];
|
||||||
|
newFields[index] = { ...newFields[index], type: value };
|
||||||
|
const newConfig = { ...localConfig, displayFields: newFields };
|
||||||
|
setLocalConfig(newConfig);
|
||||||
|
onUpdateProperty("webTypeConfig", newConfig);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-24 text-xs">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{fieldTypes.map((type) => (
|
||||||
|
<SelectItem key={type.value} value={type.value}>
|
||||||
|
{type.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={localConfig.searchFields.includes(field.name) ? "default" : "outline"}
|
||||||
|
onClick={() => toggleSearchField(field.name)}
|
||||||
|
className="p-1 text-xs"
|
||||||
|
title={localConfig.searchFields.includes(field.name) ? "검색 필드에서 제거" : "검색 필드로 추가"}
|
||||||
|
>
|
||||||
|
<Search className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => removeDisplayField(index)}
|
||||||
|
className="p-1 text-xs"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 검색 설정 */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h4 className="text-sm font-medium">검색 설정</h4>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="placeholder" className="text-xs">
|
<Label htmlFor="placeholder" className="text-xs">
|
||||||
플레이스홀더
|
플레이스홀더
|
||||||
|
|
@ -292,7 +427,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
value={localConfig.placeholder || ""}
|
value={localConfig.placeholder || ""}
|
||||||
onChange={(e) => updateConfigLocal("placeholder", e.target.value)}
|
onChange={(e) => updateConfigLocal("placeholder", e.target.value)}
|
||||||
onBlur={handleInputBlur}
|
onBlur={handleInputBlur}
|
||||||
placeholder="항목을 선택하세요"
|
placeholder="엔티티를 선택하세요"
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -310,11 +445,6 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 검색 설정 */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
<h4 className="text-sm font-medium">검색 설정</h4>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|
@ -353,7 +483,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
<Label htmlFor="searchable" className="text-xs">
|
<Label htmlFor="searchable" className="text-xs">
|
||||||
검색 가능
|
검색 가능
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-muted-foreground text-xs">항목을 검색할 수 있습니다.</p>
|
<p className="text-muted-foreground text-xs">엔티티를 검색할 수 있습니다.</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
id="searchable"
|
id="searchable"
|
||||||
|
|
@ -367,7 +497,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
<Label htmlFor="multiple" className="text-xs">
|
<Label htmlFor="multiple" className="text-xs">
|
||||||
다중 선택
|
다중 선택
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-muted-foreground text-xs">여러 항목을 선택할 수 있습니다.</p>
|
<p className="text-muted-foreground text-xs">여러 엔티티를 선택할 수 있습니다.</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
id="multiple"
|
id="multiple"
|
||||||
|
|
@ -377,6 +507,33 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 필터 설정 */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h4 className="text-sm font-medium">추가 필터</h4>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="filters" className="text-xs">
|
||||||
|
JSON 필터
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="filters"
|
||||||
|
value={JSON.stringify(localConfig.filters || {}, null, 2)}
|
||||||
|
onChange={(e) => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(e.target.value);
|
||||||
|
updateConfig("filters", parsed);
|
||||||
|
} catch {
|
||||||
|
// 유효하지 않은 JSON은 무시
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder='{"status": "active", "department": "IT"}'
|
||||||
|
className="font-mono text-xs"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
<p className="text-muted-foreground text-xs">API 요청에 추가될 필터 조건을 JSON 형태로 입력하세요.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 상태 설정 */}
|
{/* 상태 설정 */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h4 className="text-sm font-medium">상태 설정</h4>
|
<h4 className="text-sm font-medium">상태 설정</h4>
|
||||||
|
|
@ -386,7 +543,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
<Label htmlFor="required" className="text-xs">
|
<Label htmlFor="required" className="text-xs">
|
||||||
필수 선택
|
필수 선택
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-muted-foreground text-xs">반드시 항목을 선택해야 합니다.</p>
|
<p className="text-muted-foreground text-xs">엔티티가 반드시 선택되어야 합니다.</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
id="required"
|
id="required"
|
||||||
|
|
@ -400,7 +557,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
<Label htmlFor="readonly" className="text-xs">
|
<Label htmlFor="readonly" className="text-xs">
|
||||||
읽기 전용
|
읽기 전용
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-muted-foreground text-xs">값을 변경할 수 없습니다.</p>
|
<p className="text-muted-foreground text-xs">엔티티를 변경할 수 없습니다.</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
id="readonly"
|
id="readonly"
|
||||||
|
|
@ -417,18 +574,31 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2 rounded border bg-white p-2">
|
<div className="flex items-center gap-2 rounded border bg-white p-2">
|
||||||
<Database className="h-4 w-4 text-gray-400" />
|
<Database className="h-4 w-4 text-gray-400" />
|
||||||
<span className="flex-1 text-xs text-muted-foreground">
|
<span className="flex-1 text-xs text-muted-foreground">{localConfig.placeholder || "엔티티를 선택하세요"}</span>
|
||||||
{localConfig.placeholder || "항목을 선택하세요"}
|
|
||||||
</span>
|
|
||||||
{localConfig.searchable && <Search className="h-4 w-4 text-gray-400" />}
|
{localConfig.searchable && <Search className="h-4 w-4 text-gray-400" />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{localConfig.displayFields.length > 0 && (
|
||||||
|
<div className="text-muted-foreground text-xs">
|
||||||
|
<div className="font-medium">표시 필드:</div>
|
||||||
|
<div className="mt-1 flex flex-wrap gap-1">
|
||||||
|
{localConfig.displayFields
|
||||||
|
.filter((f) => f.visible)
|
||||||
|
.map((field, index) => (
|
||||||
|
<span key={index} className="rounded bg-gray-100 px-2 py-1">
|
||||||
|
{field.label}
|
||||||
|
{localConfig.searchFields.includes(field.name) && " 🔍"}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="text-muted-foreground text-xs">
|
<div className="text-muted-foreground text-xs">
|
||||||
<div>테이블: {referenceInfo.referenceTable || "미설정"}</div>
|
타입: {localConfig.entityType || "미정"}• 값 필드: {localConfig.valueField}• 라벨 필드:{" "}
|
||||||
<div>값 필드: {localConfig.valueField || referenceInfo.referenceColumn || "-"}</div>
|
{localConfig.labelField}
|
||||||
<div>표시 필드: {localConfig.labelField || referenceInfo.displayColumn || "-"}</div>
|
{localConfig.multiple && " • 다중선택"}
|
||||||
{localConfig.multiple && <span> / 다중선택</span>}
|
{localConfig.required && " • 필수"}
|
||||||
{localConfig.required && <span> / 필수</span>}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -439,3 +609,5 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
EntityConfigPanel.displayName = "EntityConfigPanel";
|
EntityConfigPanel.displayName = "EntityConfigPanel";
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,6 @@ interface DetailSettingsPanelProps {
|
||||||
currentTableName?: string; // 현재 화면의 테이블명
|
currentTableName?: string; // 현재 화면의 테이블명
|
||||||
tables?: TableInfo[]; // 전체 테이블 목록
|
tables?: TableInfo[]; // 전체 테이블 목록
|
||||||
currentScreenCompanyCode?: string; // 현재 편집 중인 화면의 회사 코드
|
currentScreenCompanyCode?: string; // 현재 편집 중인 화면의 회사 코드
|
||||||
components?: ComponentData[]; // 현재 화면의 모든 컴포넌트 (연쇄관계 부모 필드 선택용)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
|
|
@ -56,7 +55,6 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
currentTableName,
|
currentTableName,
|
||||||
tables = [], // 기본값 빈 배열
|
tables = [], // 기본값 빈 배열
|
||||||
currentScreenCompanyCode,
|
currentScreenCompanyCode,
|
||||||
components = [], // 기본값 빈 배열
|
|
||||||
}) => {
|
}) => {
|
||||||
// 데이터베이스에서 입력 가능한 웹타입들을 동적으로 가져오기
|
// 데이터베이스에서 입력 가능한 웹타입들을 동적으로 가져오기
|
||||||
const { webTypes } = useWebTypes({ active: "Y" });
|
const { webTypes } = useWebTypes({ active: "Y" });
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,15 @@ import { X, Loader2 } from "lucide-react";
|
||||||
import type { TabsComponent, TabItem } from "@/types/screen-management";
|
import type { TabsComponent, TabItem } from "@/types/screen-management";
|
||||||
import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic";
|
import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useActiveTab } from "@/contexts/ActiveTabContext";
|
|
||||||
|
|
||||||
interface TabsWidgetProps {
|
interface TabsWidgetProps {
|
||||||
component: TabsComponent;
|
component: TabsComponent;
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
menuObjid?: number; // 부모 화면의 메뉴 OBJID
|
menuObjid?: number; // 🆕 부모 화면의 메뉴 OBJID
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TabsWidget({ component, className, style, menuObjid }: TabsWidgetProps) {
|
export function TabsWidget({ component, className, style, menuObjid }: TabsWidgetProps) {
|
||||||
// ActiveTab context 사용
|
|
||||||
const { setActiveTab, removeTabsComponent } = useActiveTab();
|
|
||||||
const {
|
const {
|
||||||
tabs = [],
|
tabs = [],
|
||||||
defaultTab,
|
defaultTab,
|
||||||
|
|
@ -54,30 +51,12 @@ export function TabsWidget({ component, className, style, menuObjid }: TabsWidge
|
||||||
setVisibleTabs(tabs.filter((tab) => !tab.disabled));
|
setVisibleTabs(tabs.filter((tab) => !tab.disabled));
|
||||||
}, [tabs]);
|
}, [tabs]);
|
||||||
|
|
||||||
// 선택된 탭 변경 시 localStorage에 저장 + ActiveTab Context 업데이트
|
// 선택된 탭 변경 시 localStorage에 저장
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (persistSelection && typeof window !== "undefined") {
|
if (persistSelection && typeof window !== "undefined") {
|
||||||
localStorage.setItem(storageKey, selectedTab);
|
localStorage.setItem(storageKey, selectedTab);
|
||||||
}
|
}
|
||||||
|
}, [selectedTab, persistSelection, storageKey]);
|
||||||
// ActiveTab Context에 현재 활성 탭 정보 등록
|
|
||||||
const currentTabInfo = visibleTabs.find(t => t.id === selectedTab);
|
|
||||||
if (currentTabInfo) {
|
|
||||||
setActiveTab(component.id, {
|
|
||||||
tabId: selectedTab,
|
|
||||||
tabsComponentId: component.id,
|
|
||||||
screenId: currentTabInfo.screenId,
|
|
||||||
label: currentTabInfo.label,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [selectedTab, persistSelection, storageKey, component.id, visibleTabs, setActiveTab]);
|
|
||||||
|
|
||||||
// 컴포넌트 언마운트 시 ActiveTab Context에서 제거
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
removeTabsComponent(component.id);
|
|
||||||
};
|
|
||||||
}, [component.id, removeTabsComponent]);
|
|
||||||
|
|
||||||
// 초기 로드 시 선택된 탭의 화면 불러오기
|
// 초기 로드 시 선택된 탭의 화면 불러오기
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -241,18 +220,16 @@ export function TabsWidget({ component, className, style, menuObjid }: TabsWidge
|
||||||
margin: "0 auto",
|
margin: "0 auto",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{components.map((comp: any) => (
|
{components.map((component: any) => (
|
||||||
<InteractiveScreenViewerDynamic
|
<InteractiveScreenViewerDynamic
|
||||||
key={comp.id}
|
key={component.id}
|
||||||
component={comp}
|
component={component}
|
||||||
allComponents={components}
|
allComponents={components}
|
||||||
screenInfo={{
|
screenInfo={{
|
||||||
id: tab.screenId,
|
id: tab.screenId,
|
||||||
tableName: layoutData.tableName,
|
tableName: layoutData.tableName,
|
||||||
}}
|
}}
|
||||||
menuObjid={menuObjid}
|
menuObjid={menuObjid}
|
||||||
parentTabId={tab.id}
|
|
||||||
parentTabsComponentId={component.id}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,139 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React, {
|
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
useState,
|
|
||||||
useCallback,
|
|
||||||
ReactNode,
|
|
||||||
} from "react";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 활성 탭 정보
|
|
||||||
*/
|
|
||||||
export interface ActiveTabInfo {
|
|
||||||
tabId: string; // 탭 고유 ID
|
|
||||||
tabsComponentId: string; // 부모 탭 컴포넌트 ID
|
|
||||||
screenId?: number; // 탭에 연결된 화면 ID
|
|
||||||
label?: string; // 탭 라벨
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Context 값 타입
|
|
||||||
*/
|
|
||||||
interface ActiveTabContextValue {
|
|
||||||
// 현재 활성 탭 정보 (탭 컴포넌트 ID -> 활성 탭 정보)
|
|
||||||
activeTabs: Map<string, ActiveTabInfo>;
|
|
||||||
|
|
||||||
// 활성 탭 설정
|
|
||||||
setActiveTab: (tabsComponentId: string, tabInfo: ActiveTabInfo) => void;
|
|
||||||
|
|
||||||
// 활성 탭 조회
|
|
||||||
getActiveTab: (tabsComponentId: string) => ActiveTabInfo | undefined;
|
|
||||||
|
|
||||||
// 특정 탭 컴포넌트의 활성 탭 ID 조회
|
|
||||||
getActiveTabId: (tabsComponentId: string) => string | undefined;
|
|
||||||
|
|
||||||
// 전체 활성 탭 ID 목록 (모든 탭 컴포넌트에서)
|
|
||||||
getAllActiveTabIds: () => string[];
|
|
||||||
|
|
||||||
// 탭 컴포넌트 제거 시 정리
|
|
||||||
removeTabsComponent: (tabsComponentId: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ActiveTabContext = createContext<ActiveTabContextValue | undefined>(undefined);
|
|
||||||
|
|
||||||
export const ActiveTabProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
|
||||||
const [activeTabs, setActiveTabs] = useState<Map<string, ActiveTabInfo>>(new Map());
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 활성 탭 설정
|
|
||||||
*/
|
|
||||||
const setActiveTab = useCallback((tabsComponentId: string, tabInfo: ActiveTabInfo) => {
|
|
||||||
setActiveTabs((prev) => {
|
|
||||||
const newMap = new Map(prev);
|
|
||||||
newMap.set(tabsComponentId, tabInfo);
|
|
||||||
return newMap;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 활성 탭 조회
|
|
||||||
*/
|
|
||||||
const getActiveTab = useCallback(
|
|
||||||
(tabsComponentId: string) => {
|
|
||||||
return activeTabs.get(tabsComponentId);
|
|
||||||
},
|
|
||||||
[activeTabs]
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 특정 탭 컴포넌트의 활성 탭 ID 조회
|
|
||||||
*/
|
|
||||||
const getActiveTabId = useCallback(
|
|
||||||
(tabsComponentId: string) => {
|
|
||||||
return activeTabs.get(tabsComponentId)?.tabId;
|
|
||||||
},
|
|
||||||
[activeTabs]
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 전체 활성 탭 ID 목록
|
|
||||||
*/
|
|
||||||
const getAllActiveTabIds = useCallback(() => {
|
|
||||||
return Array.from(activeTabs.values()).map((info) => info.tabId);
|
|
||||||
}, [activeTabs]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 탭 컴포넌트 제거 시 정리
|
|
||||||
*/
|
|
||||||
const removeTabsComponent = useCallback((tabsComponentId: string) => {
|
|
||||||
setActiveTabs((prev) => {
|
|
||||||
const newMap = new Map(prev);
|
|
||||||
newMap.delete(tabsComponentId);
|
|
||||||
return newMap;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ActiveTabContext.Provider
|
|
||||||
value={{
|
|
||||||
activeTabs,
|
|
||||||
setActiveTab,
|
|
||||||
getActiveTab,
|
|
||||||
getActiveTabId,
|
|
||||||
getAllActiveTabIds,
|
|
||||||
removeTabsComponent,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ActiveTabContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Context Hook
|
|
||||||
*/
|
|
||||||
export const useActiveTab = () => {
|
|
||||||
const context = useContext(ActiveTabContext);
|
|
||||||
if (!context) {
|
|
||||||
// Context가 없으면 기본값 반환 (탭이 없는 화면에서 사용 시)
|
|
||||||
return {
|
|
||||||
activeTabs: new Map(),
|
|
||||||
setActiveTab: () => {},
|
|
||||||
getActiveTab: () => undefined,
|
|
||||||
getActiveTabId: () => undefined,
|
|
||||||
getAllActiveTabIds: () => [],
|
|
||||||
removeTabsComponent: () => {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional Context Hook (에러 없이 undefined 반환)
|
|
||||||
*/
|
|
||||||
export const useActiveTabOptional = () => {
|
|
||||||
return useContext(ActiveTabContext);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
@ -3,14 +3,12 @@ import React, {
|
||||||
useContext,
|
useContext,
|
||||||
useState,
|
useState,
|
||||||
useCallback,
|
useCallback,
|
||||||
useMemo,
|
|
||||||
ReactNode,
|
ReactNode,
|
||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import {
|
||||||
TableRegistration,
|
TableRegistration,
|
||||||
TableOptionsContextValue,
|
TableOptionsContextValue,
|
||||||
} from "@/types/table-options";
|
} from "@/types/table-options";
|
||||||
import { useActiveTab } from "./ActiveTabContext";
|
|
||||||
|
|
||||||
const TableOptionsContext = createContext<TableOptionsContextValue | undefined>(
|
const TableOptionsContext = createContext<TableOptionsContextValue | undefined>(
|
||||||
undefined
|
undefined
|
||||||
|
|
@ -91,35 +89,6 @@ export const TableOptionsProvider: React.FC<{ children: ReactNode }> = ({
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ActiveTab context 사용 (optional - 에러 방지)
|
|
||||||
const activeTabContext = useActiveTab();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 현재 활성 탭의 테이블만 반환
|
|
||||||
*/
|
|
||||||
const getActiveTabTables = useCallback(() => {
|
|
||||||
const allTables = Array.from(registeredTables.values());
|
|
||||||
const activeTabIds = activeTabContext.getAllActiveTabIds();
|
|
||||||
|
|
||||||
// 활성 탭이 없으면 탭에 속하지 않은 테이블만 반환
|
|
||||||
if (activeTabIds.length === 0) {
|
|
||||||
return allTables.filter(table => !table.parentTabId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 활성 탭에 속한 테이블 + 탭에 속하지 않은 테이블
|
|
||||||
return allTables.filter(table =>
|
|
||||||
!table.parentTabId || activeTabIds.includes(table.parentTabId)
|
|
||||||
);
|
|
||||||
}, [registeredTables, activeTabContext]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 특정 탭의 테이블만 반환
|
|
||||||
*/
|
|
||||||
const getTablesForTab = useCallback((tabId: string) => {
|
|
||||||
const allTables = Array.from(registeredTables.values());
|
|
||||||
return allTables.filter(table => table.parentTabId === tabId);
|
|
||||||
}, [registeredTables]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableOptionsContext.Provider
|
<TableOptionsContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
|
@ -130,8 +99,6 @@ export const TableOptionsProvider: React.FC<{ children: ReactNode }> = ({
|
||||||
updateTableDataCount,
|
updateTableDataCount,
|
||||||
selectedTableId,
|
selectedTableId,
|
||||||
setSelectedTableId,
|
setSelectedTableId,
|
||||||
getActiveTabTables,
|
|
||||||
getTablesForTab,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { apiClient } from "./client";
|
||||||
|
|
||||||
export interface TableColumn {
|
export interface TableColumn {
|
||||||
name: string;
|
name: string;
|
||||||
label: string; // 컬럼 라벨 (column_labels 테이블에서 가져옴)
|
|
||||||
type: string;
|
type: string;
|
||||||
nullable: boolean;
|
nullable: boolean;
|
||||||
default: string | null;
|
default: string | null;
|
||||||
|
|
|
||||||
|
|
@ -132,9 +132,6 @@ export interface DynamicComponentRendererProps {
|
||||||
mode?: "view" | "edit";
|
mode?: "view" | "edit";
|
||||||
// 모달 내에서 렌더링 여부
|
// 모달 내에서 렌더링 여부
|
||||||
isInModal?: boolean;
|
isInModal?: boolean;
|
||||||
// 탭 관련 정보 (탭 내부의 컴포넌트에서 사용)
|
|
||||||
parentTabId?: string; // 부모 탭 ID
|
|
||||||
parentTabsComponentId?: string; // 부모 탭 컴포넌트 ID
|
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -823,29 +823,28 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: `repeat(${componentConfig.cardsPerRow || 3}, 1fr)`, // 기본값 3 (한 행당 카드 수)
|
gridTemplateColumns: `repeat(${componentConfig.cardsPerRow || 3}, 1fr)`, // 기본값 3 (한 행당 카드 수)
|
||||||
gridAutoRows: "min-content", // 자동 행 생성으로 모든 데이터 표시
|
gridAutoRows: "min-content", // 자동 행 생성으로 모든 데이터 표시
|
||||||
gap: `${componentConfig.cardSpacing || 16}px`, // 카드 간격
|
gap: `${componentConfig.cardSpacing || 32}px`, // 간격 대폭 증가로 여유로운 느낌
|
||||||
padding: "16px", // 패딩
|
padding: "32px", // 패딩 대폭 증가
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
background: "transparent", // 배경색 제거
|
background: "#f8fafc", // 연한 하늘색 배경 (채도 낮춤)
|
||||||
overflow: "auto",
|
overflow: "auto",
|
||||||
borderRadius: "0", // 라운드 제거
|
borderRadius: "12px", // 컨테이너 자체도 라운드 처리
|
||||||
};
|
};
|
||||||
|
|
||||||
// 카드 스타일 - 컴팩트한 디자인
|
// 카드 스타일 - 컴팩트한 디자인
|
||||||
const cardStyle: React.CSSProperties = {
|
const cardStyle: React.CSSProperties = {
|
||||||
backgroundColor: "hsl(var(--card))",
|
backgroundColor: "white",
|
||||||
border: "1px solid hsl(var(--border))",
|
border: "1px solid #e5e7eb",
|
||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
padding: "16px",
|
padding: "16px",
|
||||||
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.05)",
|
boxShadow: "0 1px 3px rgba(0, 0, 0, 0.08)",
|
||||||
transition: "all 0.2s ease",
|
transition: "all 0.2s ease",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
cursor: isDesignMode ? "pointer" : "default",
|
cursor: isDesignMode ? "pointer" : "default",
|
||||||
width: "100%", // 전체 너비 차지
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 텍스트 자르기 함수
|
// 텍스트 자르기 함수
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Search, X, Check, ChevronsUpDown } from "lucide-react";
|
import { Search, X, Check, ChevronsUpDown } from "lucide-react";
|
||||||
|
|
@ -10,7 +10,6 @@ import { cn } from "@/lib/utils";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||||
import { dynamicFormApi } from "@/lib/api/dynamicForm";
|
import { dynamicFormApi } from "@/lib/api/dynamicForm";
|
||||||
import { cascadingRelationApi } from "@/lib/api/cascadingRelation";
|
|
||||||
|
|
||||||
export function EntitySearchInputComponent({
|
export function EntitySearchInputComponent({
|
||||||
tableName,
|
tableName,
|
||||||
|
|
@ -30,11 +29,6 @@ export function EntitySearchInputComponent({
|
||||||
additionalFields = [],
|
additionalFields = [],
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
// 연쇄관계 props
|
|
||||||
cascadingRelationCode,
|
|
||||||
parentValue: parentValueProp,
|
|
||||||
parentFieldId,
|
|
||||||
formData,
|
|
||||||
// 🆕 추가 props
|
// 🆕 추가 props
|
||||||
component,
|
component,
|
||||||
isInteractive,
|
isInteractive,
|
||||||
|
|
@ -44,21 +38,10 @@ export function EntitySearchInputComponent({
|
||||||
component?: any;
|
component?: any;
|
||||||
isInteractive?: boolean;
|
isInteractive?: boolean;
|
||||||
onFormDataChange?: (fieldName: string, value: any) => void;
|
onFormDataChange?: (fieldName: string, value: any) => void;
|
||||||
webTypeConfig?: any; // 웹타입 설정 (연쇄관계 등)
|
|
||||||
}) {
|
}) {
|
||||||
// uiMode가 있으면 우선 사용, 없으면 modeProp 사용, 기본값 "combo"
|
// uiMode가 있으면 우선 사용, 없으면 modeProp 사용, 기본값 "combo"
|
||||||
const mode = (uiMode || modeProp || "combo") as "select" | "modal" | "combo" | "autocomplete";
|
const mode = (uiMode || modeProp || "combo") as "select" | "modal" | "combo" | "autocomplete";
|
||||||
|
|
||||||
// 연쇄관계 설정 추출 (webTypeConfig 또는 component.componentConfig에서)
|
|
||||||
const config = component?.componentConfig || {};
|
|
||||||
const effectiveCascadingRelationCode = cascadingRelationCode || config.cascadingRelationCode;
|
|
||||||
const effectiveParentFieldId = parentFieldId || config.parentFieldId;
|
|
||||||
const effectiveCascadingRole = config.cascadingRole; // "parent" | "child" | undefined
|
|
||||||
|
|
||||||
// 부모 역할이면 연쇄관계 로직 적용 안함 (자식만 부모 값에 따라 필터링됨)
|
|
||||||
const isChildRole = effectiveCascadingRole === "child";
|
|
||||||
const shouldApplyCascading = effectiveCascadingRelationCode && isChildRole;
|
|
||||||
|
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [selectOpen, setSelectOpen] = useState(false);
|
const [selectOpen, setSelectOpen] = useState(false);
|
||||||
const [displayValue, setDisplayValue] = useState("");
|
const [displayValue, setDisplayValue] = useState("");
|
||||||
|
|
@ -67,82 +50,16 @@ export function EntitySearchInputComponent({
|
||||||
const [isLoadingOptions, setIsLoadingOptions] = useState(false);
|
const [isLoadingOptions, setIsLoadingOptions] = useState(false);
|
||||||
const [optionsLoaded, setOptionsLoaded] = useState(false);
|
const [optionsLoaded, setOptionsLoaded] = useState(false);
|
||||||
|
|
||||||
// 연쇄관계 상태
|
|
||||||
const [cascadingOptions, setCascadingOptions] = useState<EntitySearchResult[]>([]);
|
|
||||||
const [isCascadingLoading, setIsCascadingLoading] = useState(false);
|
|
||||||
const previousParentValue = useRef<any>(null);
|
|
||||||
|
|
||||||
// 부모 필드 값 결정 (직접 전달 또는 formData에서 추출) - 자식 역할일 때만 필요
|
|
||||||
const parentValue = isChildRole
|
|
||||||
? (parentValueProp ?? (effectiveParentFieldId && formData ? formData[effectiveParentFieldId] : undefined))
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
// filterCondition을 문자열로 변환하여 비교 (객체 참조 문제 해결)
|
// filterCondition을 문자열로 변환하여 비교 (객체 참조 문제 해결)
|
||||||
const filterConditionKey = JSON.stringify(filterCondition || {});
|
const filterConditionKey = JSON.stringify(filterCondition || {});
|
||||||
|
|
||||||
// 연쇄관계가 설정된 경우: 부모 값이 변경되면 자식 옵션 로드 (자식 역할일 때만)
|
// select 모드일 때 옵션 로드 (한 번만)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadCascadingOptions = async () => {
|
if (mode === "select" && tableName && !optionsLoaded) {
|
||||||
if (!shouldApplyCascading) return;
|
|
||||||
|
|
||||||
// 부모 값이 없으면 옵션 초기화
|
|
||||||
if (!parentValue) {
|
|
||||||
setCascadingOptions([]);
|
|
||||||
// 부모 값이 변경되면 현재 값도 초기화
|
|
||||||
if (previousParentValue.current !== null && previousParentValue.current !== parentValue) {
|
|
||||||
handleClear();
|
|
||||||
}
|
|
||||||
previousParentValue.current = parentValue;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 부모 값이 동일하면 스킵
|
|
||||||
if (previousParentValue.current === parentValue) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
previousParentValue.current = parentValue;
|
|
||||||
setIsCascadingLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log("🔗 연쇄관계 옵션 로드:", { effectiveCascadingRelationCode, parentValue });
|
|
||||||
const response = await cascadingRelationApi.getOptions(effectiveCascadingRelationCode, String(parentValue));
|
|
||||||
|
|
||||||
if (response.success && response.data) {
|
|
||||||
// 옵션을 EntitySearchResult 형태로 변환
|
|
||||||
const formattedOptions = response.data.map((opt: any) => ({
|
|
||||||
[valueField]: opt.value,
|
|
||||||
[displayField]: opt.label,
|
|
||||||
...opt, // 추가 필드도 포함
|
|
||||||
}));
|
|
||||||
setCascadingOptions(formattedOptions);
|
|
||||||
console.log("✅ 연쇄관계 옵션 로드 완료:", formattedOptions.length, "개");
|
|
||||||
|
|
||||||
// 현재 선택된 값이 새 옵션에 없으면 초기화
|
|
||||||
if (value && !formattedOptions.find((opt: any) => opt[valueField] === value)) {
|
|
||||||
handleClear();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setCascadingOptions([]);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ 연쇄관계 옵션 로드 실패:", error);
|
|
||||||
setCascadingOptions([]);
|
|
||||||
} finally {
|
|
||||||
setIsCascadingLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadCascadingOptions();
|
|
||||||
}, [shouldApplyCascading, effectiveCascadingRelationCode, parentValue, valueField, displayField]);
|
|
||||||
|
|
||||||
// select 모드일 때 옵션 로드 (연쇄관계가 없거나 부모 역할인 경우)
|
|
||||||
useEffect(() => {
|
|
||||||
if (mode === "select" && tableName && !optionsLoaded && !shouldApplyCascading) {
|
|
||||||
loadOptions();
|
loadOptions();
|
||||||
setOptionsLoaded(true);
|
setOptionsLoaded(true);
|
||||||
}
|
}
|
||||||
}, [mode, tableName, filterConditionKey, optionsLoaded, shouldApplyCascading]);
|
}, [mode, tableName, filterConditionKey, optionsLoaded]);
|
||||||
|
|
||||||
const loadOptions = async () => {
|
const loadOptions = async () => {
|
||||||
if (!tableName) return;
|
if (!tableName) return;
|
||||||
|
|
@ -165,19 +82,15 @@ export function EntitySearchInputComponent({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 실제 사용할 옵션 목록 (자식 역할이고 연쇄관계가 있으면 연쇄 옵션 사용)
|
|
||||||
const effectiveOptions = shouldApplyCascading ? cascadingOptions : options;
|
|
||||||
const isLoading = shouldApplyCascading ? isCascadingLoading : isLoadingOptions;
|
|
||||||
|
|
||||||
// value가 변경되면 표시값 업데이트 (외래키 값으로 데이터 조회)
|
// value가 변경되면 표시값 업데이트 (외래키 값으로 데이터 조회)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadDisplayValue = async () => {
|
const loadDisplayValue = async () => {
|
||||||
if (value && selectedData) {
|
if (value && selectedData) {
|
||||||
// 이미 selectedData가 있으면 표시값만 업데이트
|
// 이미 selectedData가 있으면 표시값만 업데이트
|
||||||
setDisplayValue(selectedData[displayField] || "");
|
setDisplayValue(selectedData[displayField] || "");
|
||||||
} else if (value && mode === "select" && effectiveOptions.length > 0) {
|
} else if (value && mode === "select" && options.length > 0) {
|
||||||
// select 모드에서 value가 있고 options가 로드된 경우
|
// select 모드에서 value가 있고 options가 로드된 경우
|
||||||
const found = effectiveOptions.find((opt) => opt[valueField] === value);
|
const found = options.find((opt) => opt[valueField] === value);
|
||||||
if (found) {
|
if (found) {
|
||||||
setSelectedData(found);
|
setSelectedData(found);
|
||||||
setDisplayValue(found[displayField] || "");
|
setDisplayValue(found[displayField] || "");
|
||||||
|
|
@ -229,7 +142,7 @@ export function EntitySearchInputComponent({
|
||||||
};
|
};
|
||||||
|
|
||||||
loadDisplayValue();
|
loadDisplayValue();
|
||||||
}, [value, displayField, effectiveOptions, mode, valueField, tableName, selectedData]);
|
}, [value, displayField, options, mode, valueField, tableName, selectedData]);
|
||||||
|
|
||||||
const handleSelect = (newValue: any, fullData: EntitySearchResult) => {
|
const handleSelect = (newValue: any, fullData: EntitySearchResult) => {
|
||||||
setSelectedData(fullData);
|
setSelectedData(fullData);
|
||||||
|
|
@ -287,7 +200,7 @@ export function EntitySearchInputComponent({
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={selectOpen}
|
aria-expanded={selectOpen}
|
||||||
disabled={disabled || isLoading || Boolean(shouldApplyCascading && !parentValue)}
|
disabled={disabled || isLoadingOptions}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full justify-between font-normal",
|
"w-full justify-between font-normal",
|
||||||
!componentHeight && "h-8 text-xs sm:h-10 sm:text-sm",
|
!componentHeight && "h-8 text-xs sm:h-10 sm:text-sm",
|
||||||
|
|
@ -295,11 +208,7 @@ export function EntitySearchInputComponent({
|
||||||
)}
|
)}
|
||||||
style={inputStyle}
|
style={inputStyle}
|
||||||
>
|
>
|
||||||
{isLoading
|
{isLoadingOptions ? "로딩 중..." : displayValue || placeholder}
|
||||||
? "로딩 중..."
|
|
||||||
: shouldApplyCascading && !parentValue
|
|
||||||
? "상위 항목을 먼저 선택하세요"
|
|
||||||
: displayValue || placeholder}
|
|
||||||
<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>
|
||||||
|
|
@ -309,7 +218,7 @@ export function EntitySearchInputComponent({
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty className="py-4 text-center text-xs sm:text-sm">항목을 찾을 수 없습니다.</CommandEmpty>
|
<CommandEmpty className="py-4 text-center text-xs sm:text-sm">항목을 찾을 수 없습니다.</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{effectiveOptions.map((option, index) => (
|
{options.map((option, index) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={option[valueField] || index}
|
key={option[valueField] || index}
|
||||||
value={`${option[displayField] || ""}-${option[valueField] || ""}`}
|
value={`${option[displayField] || ""}-${option[valueField] || ""}`}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
|
@ -8,27 +8,19 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, Command
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Plus, X, Check, ChevronsUpDown, Database, Info, Link2, ExternalLink } from "lucide-react";
|
import { Plus, X, Check, ChevronsUpDown } from "lucide-react";
|
||||||
// allComponents는 현재 사용되지 않지만 향후 확장을 위해 props에 유지
|
|
||||||
import { EntitySearchInputConfig } from "./config";
|
import { EntitySearchInputConfig } from "./config";
|
||||||
import { tableManagementApi } from "@/lib/api/tableManagement";
|
import { tableManagementApi } from "@/lib/api/tableManagement";
|
||||||
import { tableTypeApi } from "@/lib/api/screen";
|
|
||||||
import { cascadingRelationApi, CascadingRelation } from "@/lib/api/cascadingRelation";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
interface EntitySearchInputConfigPanelProps {
|
interface EntitySearchInputConfigPanelProps {
|
||||||
config: EntitySearchInputConfig;
|
config: EntitySearchInputConfig;
|
||||||
onConfigChange: (config: EntitySearchInputConfig) => void;
|
onConfigChange: (config: EntitySearchInputConfig) => void;
|
||||||
currentComponent?: any; // 테이블 패널에서 드래그한 컴포넌트 정보
|
|
||||||
allComponents?: any[]; // 현재 화면의 모든 컴포넌트 (연쇄 드롭다운 부모 감지용)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EntitySearchInputConfigPanel({
|
export function EntitySearchInputConfigPanel({
|
||||||
config,
|
config,
|
||||||
onConfigChange,
|
onConfigChange,
|
||||||
currentComponent,
|
|
||||||
allComponents = [],
|
|
||||||
}: EntitySearchInputConfigPanelProps) {
|
}: EntitySearchInputConfigPanelProps) {
|
||||||
const [localConfig, setLocalConfig] = useState(config);
|
const [localConfig, setLocalConfig] = useState(config);
|
||||||
const [allTables, setAllTables] = useState<any[]>([]);
|
const [allTables, setAllTables] = useState<any[]>([]);
|
||||||
|
|
@ -38,152 +30,8 @@ export function EntitySearchInputConfigPanel({
|
||||||
const [openTableCombo, setOpenTableCombo] = useState(false);
|
const [openTableCombo, setOpenTableCombo] = useState(false);
|
||||||
const [openDisplayFieldCombo, setOpenDisplayFieldCombo] = useState(false);
|
const [openDisplayFieldCombo, setOpenDisplayFieldCombo] = useState(false);
|
||||||
const [openValueFieldCombo, setOpenValueFieldCombo] = useState(false);
|
const [openValueFieldCombo, setOpenValueFieldCombo] = useState(false);
|
||||||
|
|
||||||
// 연쇄 드롭다운 설정 상태 (SelectBasicConfigPanel과 동일)
|
|
||||||
const [cascadingEnabled, setCascadingEnabled] = useState(!!config.cascadingRelationCode);
|
|
||||||
|
|
||||||
// 연쇄관계 목록
|
|
||||||
const [relationList, setRelationList] = useState<CascadingRelation[]>([]);
|
|
||||||
const [loadingRelations, setLoadingRelations] = useState(false);
|
|
||||||
|
|
||||||
// 테이블 타입 관리에서 설정된 참조 테이블 정보
|
|
||||||
const [referenceInfo, setReferenceInfo] = useState<{
|
|
||||||
referenceTable: string;
|
|
||||||
referenceColumn: string;
|
|
||||||
displayColumn: string;
|
|
||||||
isLoading: boolean;
|
|
||||||
isAutoLoaded: boolean; // 자동 로드되었는지 여부
|
|
||||||
error: string | null;
|
|
||||||
}>({
|
|
||||||
referenceTable: "",
|
|
||||||
referenceColumn: "",
|
|
||||||
displayColumn: "",
|
|
||||||
isLoading: false,
|
|
||||||
isAutoLoaded: false,
|
|
||||||
error: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 자동 설정 완료 여부 (중복 방지)
|
|
||||||
const autoConfigApplied = useRef(false);
|
|
||||||
|
|
||||||
// 테이블 패널에서 드래그한 컴포넌트인 경우, 참조 테이블 정보 자동 로드
|
// 전체 테이블 목록 로드
|
||||||
useEffect(() => {
|
|
||||||
const loadReferenceInfo = async () => {
|
|
||||||
// currentComponent에서 소스 테이블/컬럼 정보 추출
|
|
||||||
const sourceTableName = currentComponent?.tableName || currentComponent?.sourceTableName;
|
|
||||||
const sourceColumnName = currentComponent?.columnName || currentComponent?.sourceColumnName;
|
|
||||||
|
|
||||||
if (!sourceTableName || !sourceColumnName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 이미 config에 테이블명이 설정되어 있고, 자동 로드가 완료되었다면 스킵
|
|
||||||
if (config.tableName && autoConfigApplied.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setReferenceInfo(prev => ({ ...prev, isLoading: true, error: null }));
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 테이블 타입 관리에서 컬럼 정보 조회
|
|
||||||
const columns = await tableTypeApi.getColumns(sourceTableName);
|
|
||||||
const columnInfo = columns.find((col: any) =>
|
|
||||||
(col.columnName || col.column_name) === sourceColumnName
|
|
||||||
);
|
|
||||||
|
|
||||||
if (columnInfo) {
|
|
||||||
const refTable = columnInfo.referenceTable || columnInfo.reference_table || "";
|
|
||||||
const refColumn = columnInfo.referenceColumn || columnInfo.reference_column || "";
|
|
||||||
const dispColumn = columnInfo.displayColumn || columnInfo.display_column || "";
|
|
||||||
|
|
||||||
// detailSettings에서도 정보 확인 (JSON 파싱)
|
|
||||||
let detailSettings: any = {};
|
|
||||||
if (columnInfo.detailSettings) {
|
|
||||||
try {
|
|
||||||
if (typeof columnInfo.detailSettings === 'string') {
|
|
||||||
detailSettings = JSON.parse(columnInfo.detailSettings);
|
|
||||||
} else {
|
|
||||||
detailSettings = columnInfo.detailSettings;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// JSON 파싱 실패 시 무시
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalRefTable = refTable || detailSettings.referenceTable || "";
|
|
||||||
const finalRefColumn = refColumn || detailSettings.referenceColumn || "id";
|
|
||||||
const finalDispColumn = dispColumn || detailSettings.displayColumn || "name";
|
|
||||||
|
|
||||||
setReferenceInfo({
|
|
||||||
referenceTable: finalRefTable,
|
|
||||||
referenceColumn: finalRefColumn,
|
|
||||||
displayColumn: finalDispColumn,
|
|
||||||
isLoading: false,
|
|
||||||
isAutoLoaded: true,
|
|
||||||
error: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 참조 테이블 정보로 config 자동 설정 (config에 아직 설정이 없는 경우만)
|
|
||||||
if (finalRefTable && !config.tableName) {
|
|
||||||
autoConfigApplied.current = true;
|
|
||||||
const newConfig: EntitySearchInputConfig = {
|
|
||||||
...localConfig,
|
|
||||||
tableName: finalRefTable,
|
|
||||||
valueField: finalRefColumn,
|
|
||||||
displayField: finalDispColumn,
|
|
||||||
};
|
|
||||||
setLocalConfig(newConfig);
|
|
||||||
onConfigChange(newConfig);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setReferenceInfo({
|
|
||||||
referenceTable: "",
|
|
||||||
referenceColumn: "",
|
|
||||||
displayColumn: "",
|
|
||||||
isLoading: false,
|
|
||||||
isAutoLoaded: false,
|
|
||||||
error: "컬럼 정보를 찾을 수 없습니다.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("참조 테이블 정보 로드 실패:", error);
|
|
||||||
setReferenceInfo({
|
|
||||||
referenceTable: "",
|
|
||||||
referenceColumn: "",
|
|
||||||
displayColumn: "",
|
|
||||||
isLoading: false,
|
|
||||||
isAutoLoaded: false,
|
|
||||||
error: "참조 테이블 정보 로드 실패",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadReferenceInfo();
|
|
||||||
}, [currentComponent?.tableName, currentComponent?.columnName, currentComponent?.sourceTableName, currentComponent?.sourceColumnName]);
|
|
||||||
|
|
||||||
// 연쇄 관계 목록 로드
|
|
||||||
useEffect(() => {
|
|
||||||
if (cascadingEnabled && relationList.length === 0) {
|
|
||||||
loadRelationList();
|
|
||||||
}
|
|
||||||
}, [cascadingEnabled]);
|
|
||||||
|
|
||||||
// 연쇄 관계 목록 로드 함수
|
|
||||||
const loadRelationList = async () => {
|
|
||||||
setLoadingRelations(true);
|
|
||||||
try {
|
|
||||||
const response = await cascadingRelationApi.getList("Y");
|
|
||||||
if (response.success && response.data) {
|
|
||||||
setRelationList(response.data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("연쇄 관계 목록 로드 실패:", error);
|
|
||||||
} finally {
|
|
||||||
setLoadingRelations(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 전체 테이블 목록 로드 (수동 선택을 위해)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadTables = async () => {
|
const loadTables = async () => {
|
||||||
setIsLoadingTables(true);
|
setIsLoadingTables(true);
|
||||||
|
|
@ -225,11 +73,8 @@ export function EntitySearchInputConfigPanel({
|
||||||
loadColumns();
|
loadColumns();
|
||||||
}, [localConfig.tableName]);
|
}, [localConfig.tableName]);
|
||||||
|
|
||||||
// 컴포넌트 변경 시 로컬 상태 동기화
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLocalConfig(config);
|
setLocalConfig(config);
|
||||||
// 연쇄 드롭다운 설정 동기화
|
|
||||||
setCascadingEnabled(!!config.cascadingRelationCode);
|
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
|
||||||
const updateConfig = (updates: Partial<EntitySearchInputConfig>) => {
|
const updateConfig = (updates: Partial<EntitySearchInputConfig>) => {
|
||||||
|
|
@ -237,71 +82,6 @@ export function EntitySearchInputConfigPanel({
|
||||||
setLocalConfig(newConfig);
|
setLocalConfig(newConfig);
|
||||||
onConfigChange(newConfig);
|
onConfigChange(newConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 연쇄 드롭다운 활성화/비활성화
|
|
||||||
const handleCascadingToggle = (enabled: boolean) => {
|
|
||||||
setCascadingEnabled(enabled);
|
|
||||||
|
|
||||||
if (!enabled) {
|
|
||||||
// 비활성화 시 관계 설정 제거
|
|
||||||
const newConfig = {
|
|
||||||
...localConfig,
|
|
||||||
cascadingRelationCode: undefined,
|
|
||||||
cascadingRole: undefined,
|
|
||||||
cascadingParentField: undefined,
|
|
||||||
};
|
|
||||||
setLocalConfig(newConfig);
|
|
||||||
onConfigChange(newConfig);
|
|
||||||
} else {
|
|
||||||
// 활성화 시 관계 목록 로드
|
|
||||||
loadRelationList();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 연쇄 관계 선택 (역할은 별도 선택)
|
|
||||||
const handleRelationSelect = (code: string) => {
|
|
||||||
const newConfig = {
|
|
||||||
...localConfig,
|
|
||||||
cascadingRelationCode: code || undefined,
|
|
||||||
cascadingRole: undefined, // 역할은 별도로 선택
|
|
||||||
cascadingParentField: undefined,
|
|
||||||
};
|
|
||||||
setLocalConfig(newConfig);
|
|
||||||
onConfigChange(newConfig);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 역할 변경 핸들러
|
|
||||||
const handleRoleChange = (role: "parent" | "child") => {
|
|
||||||
const selectedRel = relationList.find(r => r.relation_code === localConfig.cascadingRelationCode);
|
|
||||||
|
|
||||||
if (role === "parent" && selectedRel) {
|
|
||||||
// 부모 역할: 부모 테이블 정보로 설정
|
|
||||||
const newConfig = {
|
|
||||||
...localConfig,
|
|
||||||
cascadingRole: role,
|
|
||||||
tableName: selectedRel.parent_table,
|
|
||||||
valueField: selectedRel.parent_value_column,
|
|
||||||
displayField: selectedRel.parent_label_column || selectedRel.parent_value_column,
|
|
||||||
cascadingParentField: undefined, // 부모 역할이면 부모 필드 필요 없음
|
|
||||||
};
|
|
||||||
setLocalConfig(newConfig);
|
|
||||||
onConfigChange(newConfig);
|
|
||||||
} else if (role === "child" && selectedRel) {
|
|
||||||
// 자식 역할: 자식 테이블 정보로 설정
|
|
||||||
const newConfig = {
|
|
||||||
...localConfig,
|
|
||||||
cascadingRole: role,
|
|
||||||
tableName: selectedRel.child_table,
|
|
||||||
valueField: selectedRel.child_value_column,
|
|
||||||
displayField: selectedRel.child_label_column || selectedRel.child_value_column,
|
|
||||||
};
|
|
||||||
setLocalConfig(newConfig);
|
|
||||||
onConfigChange(newConfig);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 선택된 관계 정보
|
|
||||||
const selectedRelation = relationList.find(r => r.relation_code === localConfig.cascadingRelationCode);
|
|
||||||
|
|
||||||
const addSearchField = () => {
|
const addSearchField = () => {
|
||||||
const fields = localConfig.searchFields || [];
|
const fields = localConfig.searchFields || [];
|
||||||
|
|
@ -354,213 +134,10 @@ export function EntitySearchInputConfigPanel({
|
||||||
updateConfig({ additionalFields: fields });
|
updateConfig({ additionalFields: fields });
|
||||||
};
|
};
|
||||||
|
|
||||||
// 자동 로드된 참조 테이블 정보가 있는지 확인
|
|
||||||
const hasAutoReference = referenceInfo.isAutoLoaded && referenceInfo.referenceTable;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 p-4">
|
<div className="space-y-4 p-4">
|
||||||
{/* 연쇄 드롭다운 설정 - SelectConfigPanel과 동일한 패턴 */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Link2 className="h-4 w-4" />
|
|
||||||
<h4 className="text-sm font-medium">연쇄 드롭다운</h4>
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
checked={cascadingEnabled}
|
|
||||||
onCheckedChange={handleCascadingToggle}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
다른 필드의 값에 따라 옵션이 동적으로 변경됩니다. (예: 창고 선택 → 해당 창고의 위치만 표시)
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{cascadingEnabled && (
|
|
||||||
<div className="space-y-3 rounded-md border p-3 bg-muted/30">
|
|
||||||
{/* 관계 선택 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">연쇄 관계 선택</Label>
|
|
||||||
<Select
|
|
||||||
value={localConfig.cascadingRelationCode || ""}
|
|
||||||
onValueChange={handleRelationSelect}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="text-xs">
|
|
||||||
<SelectValue placeholder={loadingRelations ? "로딩 중..." : "관계 선택"} />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{relationList.map((relation) => (
|
|
||||||
<SelectItem key={relation.relation_code} value={relation.relation_code}>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span>{relation.relation_name}</span>
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
{relation.parent_table} → {relation.child_table}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 역할 선택 */}
|
|
||||||
{localConfig.cascadingRelationCode && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">역할 선택</Label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
variant={localConfig.cascadingRole === "parent" ? "default" : "outline"}
|
|
||||||
className="flex-1 text-xs"
|
|
||||||
onClick={() => handleRoleChange("parent")}
|
|
||||||
>
|
|
||||||
부모 (상위 선택)
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
variant={localConfig.cascadingRole === "child" ? "default" : "outline"}
|
|
||||||
className="flex-1 text-xs"
|
|
||||||
onClick={() => handleRoleChange("child")}
|
|
||||||
>
|
|
||||||
자식 (하위 선택)
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
{localConfig.cascadingRole === "parent"
|
|
||||||
? "이 필드가 상위 선택 역할을 합니다. (예: 창고 선택)"
|
|
||||||
: localConfig.cascadingRole === "child"
|
|
||||||
? "이 필드는 상위 필드 값에 따라 옵션이 변경됩니다. (예: 위치 선택)"
|
|
||||||
: "이 필드의 역할을 선택하세요."}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 부모 필드 설정 (자식 역할일 때만) */}
|
|
||||||
{localConfig.cascadingRelationCode && localConfig.cascadingRole === "child" && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">부모 필드명</Label>
|
|
||||||
<Input
|
|
||||||
value={localConfig.cascadingParentField || ""}
|
|
||||||
onChange={(e) => updateConfig({ cascadingParentField: e.target.value || undefined })}
|
|
||||||
placeholder="예: warehouse_code"
|
|
||||||
className="text-xs"
|
|
||||||
/>
|
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
이 드롭다운의 옵션을 결정할 부모 필드의 컬럼명을 입력하세요.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 선택된 관계 정보 표시 */}
|
|
||||||
{selectedRelation && localConfig.cascadingRole && (
|
|
||||||
<div className="bg-background space-y-1 rounded-md p-2 text-xs">
|
|
||||||
{localConfig.cascadingRole === "parent" ? (
|
|
||||||
<>
|
|
||||||
<div className="font-medium text-blue-600">부모 역할 (상위 선택)</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">데이터 소스:</span>{" "}
|
|
||||||
<span className="font-medium">{selectedRelation.parent_table}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">저장 값:</span>{" "}
|
|
||||||
<span className="font-medium">{selectedRelation.parent_value_column}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="font-medium text-green-600">자식 역할 (하위 선택)</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">데이터 소스:</span>{" "}
|
|
||||||
<span className="font-medium">{selectedRelation.child_table}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">저장 값:</span>{" "}
|
|
||||||
<span className="font-medium">{selectedRelation.child_value_column}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">필터 컬럼:</span>{" "}
|
|
||||||
<span className="font-medium">{selectedRelation.child_filter_column}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 관계 관리 페이지 링크 */}
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<Link href="/admin/cascading-relations" target="_blank">
|
|
||||||
<Button variant="link" size="sm" className="h-auto p-0 text-xs">
|
|
||||||
<ExternalLink className="mr-1 h-3 w-3" />
|
|
||||||
관계 관리
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 구분선 - 연쇄 드롭다운 비활성화 시에만 표시 */}
|
|
||||||
{!cascadingEnabled && (
|
|
||||||
<div className="border-t pt-4">
|
|
||||||
<p className="text-[10px] text-muted-foreground mb-4">
|
|
||||||
아래에서 직접 테이블/필드를 설정하세요.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 참조 테이블 자동 로드 정보 표시 */}
|
|
||||||
{referenceInfo.isLoading && (
|
|
||||||
<div className="bg-muted/50 rounded-md border p-3">
|
|
||||||
<p className="text-xs text-muted-foreground">참조 테이블 정보 로딩 중...</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasAutoReference && !cascadingEnabled && (
|
|
||||||
<div className="bg-primary/5 rounded-md border border-primary/20 p-3 space-y-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Database className="h-4 w-4 text-primary" />
|
|
||||||
<span className="text-xs font-medium text-primary">테이블 타입에서 자동 설정됨</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">참조 테이블:</span>
|
|
||||||
<div className="font-medium">{referenceInfo.referenceTable}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">값 필드:</span>
|
|
||||||
<div className="font-medium">{referenceInfo.referenceColumn || "id"}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">표시 필드:</span>
|
|
||||||
<div className="font-medium">{referenceInfo.displayColumn || "name"}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-[10px] text-muted-foreground">
|
|
||||||
소스: {currentComponent?.tableName}.{currentComponent?.columnName}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{referenceInfo.error && !hasAutoReference && !cascadingEnabled && (
|
|
||||||
<div className="bg-amber-500/10 rounded-md border border-amber-500/20 p-3">
|
|
||||||
<p className="text-xs text-amber-700 flex items-center gap-1">
|
|
||||||
<Info className="h-3 w-3" />
|
|
||||||
{referenceInfo.error}
|
|
||||||
</p>
|
|
||||||
<p className="text-[10px] text-muted-foreground mt-1">
|
|
||||||
테이블을 수동으로 선택하세요.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs sm:text-sm">
|
<Label className="text-xs sm:text-sm">테이블명 *</Label>
|
||||||
테이블명 *
|
|
||||||
{hasAutoReference && (
|
|
||||||
<span className="text-[10px] text-muted-foreground ml-2">(자동 설정됨)</span>
|
|
||||||
)}
|
|
||||||
</Label>
|
|
||||||
<Popover open={openTableCombo} onOpenChange={setOpenTableCombo}>
|
<Popover open={openTableCombo} onOpenChange={setOpenTableCombo}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,5 @@ export interface EntitySearchInputConfig {
|
||||||
modalColumns?: string[];
|
modalColumns?: string[];
|
||||||
showAdditionalInfo?: boolean;
|
showAdditionalInfo?: boolean;
|
||||||
additionalFields?: string[];
|
additionalFields?: string[];
|
||||||
|
|
||||||
// 연쇄관계 설정 (cascading_relation 테이블과 연동)
|
|
||||||
cascadingRelationCode?: string; // 연쇄관계 코드 (WAREHOUSE_LOCATION 등)
|
|
||||||
cascadingRole?: "parent" | "child"; // 역할 (부모/자식)
|
|
||||||
cascadingParentField?: string; // 부모 필드의 컬럼명 (자식 역할일 때만 사용)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,13 +23,6 @@ export interface EntitySearchInputProps {
|
||||||
filterCondition?: Record<string, any>; // 추가 WHERE 조건
|
filterCondition?: Record<string, any>; // 추가 WHERE 조건
|
||||||
companyCode?: string; // 멀티테넌시
|
companyCode?: string; // 멀티테넌시
|
||||||
|
|
||||||
// 연쇄관계 설정
|
|
||||||
cascadingRelationCode?: string; // 연쇄관계 코드 (cascading_relation 테이블)
|
|
||||||
cascadingRole?: "parent" | "child"; // 역할 (부모/자식)
|
|
||||||
parentFieldId?: string; // 부모 필드의 컬럼명 (자식 역할일 때, formData에서 값 추출용)
|
|
||||||
parentValue?: any; // 부모 필드의 현재 값 (직접 전달)
|
|
||||||
formData?: Record<string, any>; // 전체 폼 데이터 (부모 값 추출용)
|
|
||||||
|
|
||||||
// 선택된 값
|
// 선택된 값
|
||||||
value?: any;
|
value?: any;
|
||||||
onChange?: (value: any, fullData?: any) => void;
|
onChange?: (value: any, fullData?: any) => void;
|
||||||
|
|
|
||||||
|
|
@ -16,16 +16,9 @@ declare global {
|
||||||
masterData: Record<string, any> | null;
|
masterData: Record<string, any> | null;
|
||||||
config: RelatedDataButtonsConfig | null;
|
config: RelatedDataButtonsConfig | null;
|
||||||
};
|
};
|
||||||
// 🆕 RelatedDataButtons가 대상으로 하는 테이블 목록 (전역 레지스트리)
|
|
||||||
__relatedButtonsTargetTables?: Set<string>;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 전역 레지스트리 초기화
|
|
||||||
if (typeof window !== "undefined" && !window.__relatedButtonsTargetTables) {
|
|
||||||
window.__relatedButtonsTargetTables = new Set();
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RelatedDataButtonsComponentProps {
|
interface RelatedDataButtonsComponentProps {
|
||||||
config: RelatedDataButtonsConfig;
|
config: RelatedDataButtonsConfig;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
@ -66,54 +59,11 @@ export const RelatedDataButtonsComponent: React.FC<RelatedDataButtonsComponentPr
|
||||||
setMasterData(null);
|
setMasterData(null);
|
||||||
setButtons([]);
|
setButtons([]);
|
||||||
setSelectedId(null);
|
setSelectedId(null);
|
||||||
setSelectedItem(null);
|
|
||||||
|
|
||||||
// 🆕 좌측 데이터가 없을 때 대상 테이블에 빈 상태 알림
|
|
||||||
if (config.events?.targetTable) {
|
|
||||||
window.dispatchEvent(new CustomEvent("related-button-select", {
|
|
||||||
detail: {
|
|
||||||
targetTable: config.events.targetTable,
|
|
||||||
filterColumn: config.events.targetFilterColumn,
|
|
||||||
filterValue: null, // null로 설정하여 빈 상태 표시
|
|
||||||
selectedData: null,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setMasterData(splitPanelContext.selectedLeftData);
|
setMasterData(splitPanelContext.selectedLeftData);
|
||||||
}, [splitPanelContext?.selectedLeftData, config.events]);
|
}, [splitPanelContext?.selectedLeftData]);
|
||||||
|
|
||||||
// 🆕 컴포넌트 마운트 시 대상 테이블에 필터 필요 알림
|
|
||||||
useEffect(() => {
|
|
||||||
if (config.events?.targetTable) {
|
|
||||||
// 전역 레지스트리에 등록
|
|
||||||
window.__relatedButtonsTargetTables?.add(config.events.targetTable);
|
|
||||||
|
|
||||||
// 이벤트도 발생 (이미 마운트된 테이블 컴포넌트를 위해)
|
|
||||||
window.dispatchEvent(new CustomEvent("related-button-register", {
|
|
||||||
detail: {
|
|
||||||
targetTable: config.events.targetTable,
|
|
||||||
filterColumn: config.events.targetFilterColumn,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
console.log("📝 [RelatedDataButtons] 대상 테이블에 필터 등록:", config.events.targetTable);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
// 컴포넌트 언마운트 시 등록 해제
|
|
||||||
if (config.events?.targetTable) {
|
|
||||||
window.__relatedButtonsTargetTables?.delete(config.events.targetTable);
|
|
||||||
|
|
||||||
window.dispatchEvent(new CustomEvent("related-button-unregister", {
|
|
||||||
detail: {
|
|
||||||
targetTable: config.events.targetTable,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [config.events?.targetTable, config.events?.targetFilterColumn]);
|
|
||||||
|
|
||||||
// 버튼 데이터 로드
|
// 버튼 데이터 로드
|
||||||
const loadButtons = useCallback(async () => {
|
const loadButtons = useCallback(async () => {
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,6 @@ import { codeCache } from "@/lib/caching/codeCache";
|
||||||
import { useEntityJoinOptimization } from "@/lib/hooks/useEntityJoinOptimization";
|
import { useEntityJoinOptimization } from "@/lib/hooks/useEntityJoinOptimization";
|
||||||
import { getFullImageUrl } from "@/lib/api/client";
|
import { getFullImageUrl } from "@/lib/api/client";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
// 🆕 RelatedDataButtons 전역 레지스트리 타입 선언
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
__relatedButtonsTargetTables?: Set<string>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
import {
|
import {
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
|
@ -208,9 +201,6 @@ export interface TableListComponentProps {
|
||||||
) => void;
|
) => void;
|
||||||
onConfigChange?: (config: any) => void;
|
onConfigChange?: (config: any) => void;
|
||||||
refreshKey?: number;
|
refreshKey?: number;
|
||||||
// 탭 관련 정보 (탭 내부의 테이블에서 사용)
|
|
||||||
parentTabId?: string; // 부모 탭 ID
|
|
||||||
parentTabsComponentId?: string; // 부모 탭 컴포넌트 ID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
@ -227,7 +217,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
config,
|
config,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
formData: propFormData,
|
formData: propFormData, // 🆕 부모에서 전달받은 formData
|
||||||
onFormDataChange,
|
onFormDataChange,
|
||||||
componentConfig,
|
componentConfig,
|
||||||
onSelectedRowsChange,
|
onSelectedRowsChange,
|
||||||
|
|
@ -235,9 +225,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
refreshKey,
|
refreshKey,
|
||||||
tableName,
|
tableName,
|
||||||
userId,
|
userId,
|
||||||
screenId,
|
screenId, // 화면 ID 추출
|
||||||
parentTabId,
|
|
||||||
parentTabsComponentId,
|
|
||||||
}) => {
|
}) => {
|
||||||
// ========================================
|
// ========================================
|
||||||
// 설정 및 스타일
|
// 설정 및 스타일
|
||||||
|
|
@ -322,15 +310,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
filterValue: any;
|
filterValue: any;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
// 🆕 RelatedDataButtons가 이 테이블을 대상으로 등록되어 있는지 여부
|
|
||||||
const [isRelatedButtonTarget, setIsRelatedButtonTarget] = useState(() => {
|
|
||||||
// 초기값: 전역 레지스트리에서 확인
|
|
||||||
if (typeof window !== "undefined" && window.__relatedButtonsTargetTables) {
|
|
||||||
return window.__relatedButtonsTargetTables.has(tableConfig.selectedTable || "");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// TableOptions Context
|
// TableOptions Context
|
||||||
const { registerTable, unregisterTable, updateTableDataCount } = useTableOptions();
|
const { registerTable, unregisterTable, updateTableDataCount } = useTableOptions();
|
||||||
const [filters, setFilters] = useState<TableFilter[]>([]);
|
const [filters, setFilters] = useState<TableFilter[]>([]);
|
||||||
|
|
@ -1021,11 +1000,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
onGroupChange: setGrouping,
|
onGroupChange: setGrouping,
|
||||||
onColumnVisibilityChange: setColumnVisibility,
|
onColumnVisibilityChange: setColumnVisibility,
|
||||||
getColumnUniqueValues, // 고유 값 조회 함수 등록
|
getColumnUniqueValues, // 고유 값 조회 함수 등록
|
||||||
onGroupSumChange: setGroupSumConfig, // 그룹별 합산 설정
|
onGroupSumChange: setGroupSumConfig, // 🆕 그룹별 합산 설정
|
||||||
// 탭 관련 정보 (탭 내부의 테이블인 경우)
|
|
||||||
parentTabId,
|
|
||||||
parentTabsComponentId,
|
|
||||||
screenId: screenId ? Number(screenId) : undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerTable(registration);
|
registerTable(registration);
|
||||||
|
|
@ -1579,16 +1554,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 RelatedDataButtons 대상이지만 아직 버튼이 선택되지 않은 경우
|
|
||||||
// → 빈 데이터 표시 (모든 데이터를 보여주지 않음)
|
|
||||||
if (isRelatedButtonTarget && !relatedButtonFilter) {
|
|
||||||
console.log("⚠️ [TableList] RelatedDataButtons 대상이지만 버튼 미선택 → 빈 데이터 표시");
|
|
||||||
setData([]);
|
|
||||||
setTotalItems(0);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🆕 RelatedDataButtons 필터 값 준비
|
// 🆕 RelatedDataButtons 필터 값 준비
|
||||||
let relatedButtonFilterValues: Record<string, any> = {};
|
let relatedButtonFilterValues: Record<string, any> = {};
|
||||||
if (relatedButtonFilter) {
|
if (relatedButtonFilter) {
|
||||||
|
|
@ -1802,7 +1767,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
splitPanelContext?.selectedLeftData,
|
splitPanelContext?.selectedLeftData,
|
||||||
// 🆕 RelatedDataButtons 필터 추가
|
// 🆕 RelatedDataButtons 필터 추가
|
||||||
relatedButtonFilter,
|
relatedButtonFilter,
|
||||||
isRelatedButtonTarget,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fetchTableDataDebounced = useCallback(
|
const fetchTableDataDebounced = useCallback(
|
||||||
|
|
@ -4819,45 +4783,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
};
|
};
|
||||||
}, [tableConfig.selectedTable, isDesignMode]);
|
}, [tableConfig.selectedTable, isDesignMode]);
|
||||||
|
|
||||||
// 🆕 테이블명 변경 시 전역 레지스트리에서 확인
|
|
||||||
useEffect(() => {
|
|
||||||
if (typeof window !== "undefined" && window.__relatedButtonsTargetTables && tableConfig.selectedTable) {
|
|
||||||
const isTarget = window.__relatedButtonsTargetTables.has(tableConfig.selectedTable);
|
|
||||||
if (isTarget) {
|
|
||||||
console.log("📝 [TableList] 전역 레지스트리에서 RelatedDataButtons 대상 확인:", tableConfig.selectedTable);
|
|
||||||
setIsRelatedButtonTarget(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [tableConfig.selectedTable]);
|
|
||||||
|
|
||||||
// 🆕 RelatedDataButtons 등록/해제 이벤트 리스너
|
|
||||||
useEffect(() => {
|
|
||||||
const handleRelatedButtonRegister = (event: CustomEvent) => {
|
|
||||||
const { targetTable } = event.detail || {};
|
|
||||||
if (targetTable === tableConfig.selectedTable) {
|
|
||||||
console.log("📝 [TableList] RelatedDataButtons 대상으로 등록됨:", tableConfig.selectedTable);
|
|
||||||
setIsRelatedButtonTarget(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRelatedButtonUnregister = (event: CustomEvent) => {
|
|
||||||
const { targetTable } = event.detail || {};
|
|
||||||
if (targetTable === tableConfig.selectedTable) {
|
|
||||||
console.log("📝 [TableList] RelatedDataButtons 대상에서 해제됨:", tableConfig.selectedTable);
|
|
||||||
setIsRelatedButtonTarget(false);
|
|
||||||
setRelatedButtonFilter(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("related-button-register" as any, handleRelatedButtonRegister);
|
|
||||||
window.addEventListener("related-button-unregister" as any, handleRelatedButtonUnregister);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("related-button-register" as any, handleRelatedButtonRegister);
|
|
||||||
window.removeEventListener("related-button-unregister" as any, handleRelatedButtonUnregister);
|
|
||||||
};
|
|
||||||
}, [tableConfig.selectedTable]);
|
|
||||||
|
|
||||||
// 🆕 RelatedDataButtons 선택 이벤트 리스너 (버튼 선택 시 테이블 필터링)
|
// 🆕 RelatedDataButtons 선택 이벤트 리스너 (버튼 선택 시 테이블 필터링)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleRelatedButtonSelect = (event: CustomEvent) => {
|
const handleRelatedButtonSelect = (event: CustomEvent) => {
|
||||||
|
|
@ -4865,20 +4790,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
|
|
||||||
// 이 테이블이 대상 테이블인지 확인
|
// 이 테이블이 대상 테이블인지 확인
|
||||||
if (targetTable === tableConfig.selectedTable) {
|
if (targetTable === tableConfig.selectedTable) {
|
||||||
// filterValue가 null이면 선택 해제 (빈 상태)
|
console.log("📌 [TableList] RelatedDataButtons 필터 적용:", {
|
||||||
if (filterValue === null || filterValue === undefined) {
|
tableName: tableConfig.selectedTable,
|
||||||
console.log("📌 [TableList] RelatedDataButtons 선택 해제 (빈 상태):", tableConfig.selectedTable);
|
filterColumn,
|
||||||
setRelatedButtonFilter(null);
|
filterValue,
|
||||||
setIsRelatedButtonTarget(true); // 대상으로 등록은 유지
|
});
|
||||||
} else {
|
setRelatedButtonFilter({ filterColumn, filterValue });
|
||||||
console.log("📌 [TableList] RelatedDataButtons 필터 적용:", {
|
|
||||||
tableName: tableConfig.selectedTable,
|
|
||||||
filterColumn,
|
|
||||||
filterValue,
|
|
||||||
});
|
|
||||||
setRelatedButtonFilter({ filterColumn, filterValue });
|
|
||||||
setIsRelatedButtonTarget(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -4891,12 +4808,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
|
|
||||||
// 🆕 relatedButtonFilter 변경 시 데이터 다시 로드
|
// 🆕 relatedButtonFilter 변경 시 데이터 다시 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDesignMode) {
|
if (relatedButtonFilter && !isDesignMode) {
|
||||||
// relatedButtonFilter가 있으면 데이터 로드, null이면 빈 상태 (setRefreshTrigger로 트리거)
|
console.log("🔄 [TableList] RelatedDataButtons 필터 변경으로 데이터 새로고침:", relatedButtonFilter);
|
||||||
console.log("🔄 [TableList] RelatedDataButtons 상태 변경:", {
|
|
||||||
relatedButtonFilter,
|
|
||||||
isRelatedButtonTarget
|
|
||||||
});
|
|
||||||
setRefreshTrigger((prev) => prev + 1);
|
setRefreshTrigger((prev) => prev + 1);
|
||||||
}
|
}
|
||||||
}, [relatedButtonFilter, isDesignMode]);
|
}, [relatedButtonFilter, isDesignMode]);
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import { Input } from "@/components/ui/input";
|
||||||
import { Settings, Filter, Layers, X, Check, ChevronsUpDown } from "lucide-react";
|
import { Settings, Filter, Layers, X, Check, ChevronsUpDown } from "lucide-react";
|
||||||
import { useTableOptions } from "@/contexts/TableOptionsContext";
|
import { useTableOptions } from "@/contexts/TableOptionsContext";
|
||||||
import { useTableSearchWidgetHeight } from "@/contexts/TableSearchWidgetHeightContext";
|
import { useTableSearchWidgetHeight } from "@/contexts/TableSearchWidgetHeightContext";
|
||||||
import { useActiveTab } from "@/contexts/ActiveTabContext";
|
|
||||||
import { ColumnVisibilityPanel } from "@/components/screen/table-options/ColumnVisibilityPanel";
|
import { ColumnVisibilityPanel } from "@/components/screen/table-options/ColumnVisibilityPanel";
|
||||||
import { FilterPanel } from "@/components/screen/table-options/FilterPanel";
|
import { FilterPanel } from "@/components/screen/table-options/FilterPanel";
|
||||||
import { GroupingPanel } from "@/components/screen/table-options/GroupingPanel";
|
import { GroupingPanel } from "@/components/screen/table-options/GroupingPanel";
|
||||||
|
|
@ -50,9 +49,8 @@ interface TableSearchWidgetProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableSearchWidget({ component, screenId, onHeightChange }: TableSearchWidgetProps) {
|
export function TableSearchWidget({ component, screenId, onHeightChange }: TableSearchWidgetProps) {
|
||||||
const { registeredTables, selectedTableId, setSelectedTableId, getTable, getActiveTabTables } = useTableOptions();
|
const { registeredTables, selectedTableId, setSelectedTableId, getTable } = useTableOptions();
|
||||||
const { isPreviewMode } = useScreenPreview(); // 미리보기 모드 확인
|
const { isPreviewMode } = useScreenPreview(); // 미리보기 모드 확인
|
||||||
const { getAllActiveTabIds, activeTabs } = useActiveTab(); // 활성 탭 정보
|
|
||||||
|
|
||||||
// 높이 관리 context (실제 화면에서만 사용)
|
// 높이 관리 context (실제 화면에서만 사용)
|
||||||
let setWidgetHeight:
|
let setWidgetHeight:
|
||||||
|
|
@ -65,9 +63,6 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
// Context가 없으면 (디자이너 모드) 무시
|
// Context가 없으면 (디자이너 모드) 무시
|
||||||
setWidgetHeight = undefined;
|
setWidgetHeight = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 탭별 필터 값 저장 (탭 ID -> 필터 값)
|
|
||||||
const [tabFilterValues, setTabFilterValues] = useState<Record<string, Record<string, any>>>({});
|
|
||||||
|
|
||||||
const [columnVisibilityOpen, setColumnVisibilityOpen] = useState(false);
|
const [columnVisibilityOpen, setColumnVisibilityOpen] = useState(false);
|
||||||
const [filterOpen, setFilterOpen] = useState(false);
|
const [filterOpen, setFilterOpen] = useState(false);
|
||||||
|
|
@ -93,48 +88,38 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
// Map을 배열로 변환
|
// Map을 배열로 변환
|
||||||
const allTableList = Array.from(registeredTables.values());
|
const allTableList = Array.from(registeredTables.values());
|
||||||
|
|
||||||
// 현재 활성 탭 ID 목록
|
// 대상 패널 위치에 따라 테이블 필터링 (tableId 패턴 기반)
|
||||||
const activeTabIds = useMemo(() => getAllActiveTabIds(), [activeTabs]);
|
|
||||||
|
|
||||||
// 대상 패널 위치 + 활성 탭에 따라 테이블 필터링
|
|
||||||
const tableList = useMemo(() => {
|
const tableList = useMemo(() => {
|
||||||
// 1단계: 활성 탭 기반 필터링
|
// "auto"면 모든 테이블 반환
|
||||||
// - 활성 탭에 속한 테이블만 표시
|
if (targetPanelPosition === "auto") {
|
||||||
// - 탭에 속하지 않은 테이블(parentTabId가 없는)도 포함
|
return allTableList;
|
||||||
let filteredByTab = allTableList.filter(table => {
|
}
|
||||||
// 탭에 속하지 않는 테이블은 항상 표시
|
|
||||||
if (!table.parentTabId) return true;
|
// 테이블 ID 패턴으로 필터링
|
||||||
// 활성 탭에 속한 테이블만 표시
|
// card-display-XXX: 좌측 패널 (카드 디스플레이)
|
||||||
return activeTabIds.includes(table.parentTabId);
|
// datatable-XXX, table-list-XXX: 우측 패널 (테이블 리스트)
|
||||||
|
const filteredTables = allTableList.filter(table => {
|
||||||
|
const tableId = table.tableId.toLowerCase();
|
||||||
|
|
||||||
|
if (targetPanelPosition === "left") {
|
||||||
|
// 좌측 패널 대상: card-display만
|
||||||
|
return tableId.includes("card-display") || tableId.includes("card");
|
||||||
|
} else if (targetPanelPosition === "right") {
|
||||||
|
// 우측 패널 대상: datatable, table-list 등 (card-display 제외)
|
||||||
|
const isCardDisplay = tableId.includes("card-display") || tableId.includes("card");
|
||||||
|
return !isCardDisplay;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2단계: 대상 패널 위치에 따라 추가 필터링
|
// 필터링된 결과가 없으면 모든 테이블 반환 (폴백)
|
||||||
if (targetPanelPosition !== "auto") {
|
if (filteredTables.length === 0) {
|
||||||
filteredByTab = filteredByTab.filter(table => {
|
return allTableList;
|
||||||
const tableId = table.tableId.toLowerCase();
|
|
||||||
|
|
||||||
if (targetPanelPosition === "left") {
|
|
||||||
// 좌측 패널 대상: card-display만
|
|
||||||
return tableId.includes("card-display") || tableId.includes("card");
|
|
||||||
} else if (targetPanelPosition === "right") {
|
|
||||||
// 우측 패널 대상: datatable, table-list 등 (card-display 제외)
|
|
||||||
const isCardDisplay = tableId.includes("card-display") || tableId.includes("card");
|
|
||||||
return !isCardDisplay;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 필터링된 결과가 없으면 탭 기반 필터링 결과만 반환
|
return filteredTables;
|
||||||
if (filteredByTab.length === 0) {
|
}, [allTableList, targetPanelPosition]);
|
||||||
return allTableList.filter(table =>
|
|
||||||
!table.parentTabId || activeTabIds.includes(table.parentTabId)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return filteredByTab;
|
|
||||||
}, [allTableList, targetPanelPosition, activeTabIds]);
|
|
||||||
|
|
||||||
// currentTable은 tableList(필터링된 목록)에서 가져와야 함
|
// currentTable은 tableList(필터링된 목록)에서 가져와야 함
|
||||||
const currentTable = useMemo(() => {
|
const currentTable = useMemo(() => {
|
||||||
|
|
@ -166,34 +151,6 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
}
|
}
|
||||||
}, [tableList, selectedTableId, autoSelectFirstTable, setSelectedTableId, targetPanelPosition]);
|
}, [tableList, selectedTableId, autoSelectFirstTable, setSelectedTableId, targetPanelPosition]);
|
||||||
|
|
||||||
// 현재 선택된 테이블의 탭 ID (탭별 필터 저장용)
|
|
||||||
const currentTableTabId = currentTable?.parentTabId;
|
|
||||||
|
|
||||||
// 탭별 필터 값 저장 키 생성
|
|
||||||
const getTabFilterStorageKey = (tableName: string, tabId?: string) => {
|
|
||||||
const baseKey = screenId
|
|
||||||
? `table_filter_values_${tableName}_screen_${screenId}`
|
|
||||||
: `table_filter_values_${tableName}`;
|
|
||||||
return tabId ? `${baseKey}_tab_${tabId}` : baseKey;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 탭 변경 시 이전 탭의 필터 값 저장 + 새 탭의 필터 값 복원
|
|
||||||
useEffect(() => {
|
|
||||||
if (!currentTable?.tableName) return;
|
|
||||||
|
|
||||||
// 현재 필터 값이 있으면 탭별로 저장
|
|
||||||
if (Object.keys(filterValues).length > 0 && currentTableTabId) {
|
|
||||||
const storageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId);
|
|
||||||
localStorage.setItem(storageKey, JSON.stringify(filterValues));
|
|
||||||
|
|
||||||
// 메모리 캐시에도 저장
|
|
||||||
setTabFilterValues(prev => ({
|
|
||||||
...prev,
|
|
||||||
[currentTableTabId]: filterValues
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}, [currentTableTabId, currentTable?.tableName]);
|
|
||||||
|
|
||||||
// 현재 테이블의 저장된 필터 불러오기 (동적 모드) 또는 고정 필터 적용 (고정 모드)
|
// 현재 테이블의 저장된 필터 불러오기 (동적 모드) 또는 고정 필터 적용 (고정 모드)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentTable?.tableName) return;
|
if (!currentTable?.tableName) return;
|
||||||
|
|
@ -208,32 +165,14 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
width: f.width || 200,
|
width: f.width || 200,
|
||||||
}));
|
}));
|
||||||
setActiveFilters(activeFiltersList);
|
setActiveFilters(activeFiltersList);
|
||||||
|
|
||||||
// 탭별 저장된 필터 값 복원
|
|
||||||
if (currentTableTabId) {
|
|
||||||
const storageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId);
|
|
||||||
const savedValues = localStorage.getItem(storageKey);
|
|
||||||
if (savedValues) {
|
|
||||||
try {
|
|
||||||
const parsedValues = JSON.parse(savedValues);
|
|
||||||
setFilterValues(parsedValues);
|
|
||||||
// 즉시 필터 적용
|
|
||||||
setTimeout(() => applyFilters(parsedValues), 100);
|
|
||||||
} catch {
|
|
||||||
setFilterValues({});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setFilterValues({});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 동적 모드: 화면별 + 탭별로 독립적인 필터 설정 불러오기
|
// 동적 모드: 화면별로 독립적인 필터 설정 불러오기
|
||||||
const filterConfigKey = screenId
|
const storageKey = screenId
|
||||||
? `table_filters_${currentTable.tableName}_screen_${screenId}${currentTableTabId ? `_tab_${currentTableTabId}` : ''}`
|
? `table_filters_${currentTable.tableName}_screen_${screenId}`
|
||||||
: `table_filters_${currentTable.tableName}`;
|
: `table_filters_${currentTable.tableName}`;
|
||||||
const savedFilters = localStorage.getItem(filterConfigKey);
|
const savedFilters = localStorage.getItem(storageKey);
|
||||||
|
|
||||||
if (savedFilters) {
|
if (savedFilters) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -254,39 +193,16 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
operator: "contains",
|
operator: "contains",
|
||||||
value: "",
|
value: "",
|
||||||
filterType: f.filterType,
|
filterType: f.filterType,
|
||||||
width: f.width || 200,
|
width: f.width || 200, // 저장된 너비 포함
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setActiveFilters(activeFiltersList);
|
setActiveFilters(activeFiltersList);
|
||||||
|
|
||||||
// 탭별 저장된 필터 값 복원
|
|
||||||
if (currentTableTabId) {
|
|
||||||
const valuesStorageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId);
|
|
||||||
const savedValues = localStorage.getItem(valuesStorageKey);
|
|
||||||
if (savedValues) {
|
|
||||||
try {
|
|
||||||
const parsedValues = JSON.parse(savedValues);
|
|
||||||
setFilterValues(parsedValues);
|
|
||||||
// 즉시 필터 적용
|
|
||||||
setTimeout(() => applyFilters(parsedValues), 100);
|
|
||||||
} catch {
|
|
||||||
setFilterValues({});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setFilterValues({});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setFilterValues({});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("저장된 필터 불러오기 실패:", error);
|
console.error("저장된 필터 불러오기 실패:", error);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// 필터 설정이 없으면 초기화
|
|
||||||
setFilterValues({});
|
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [currentTable?.tableName, filterMode, screenId, currentTableTabId, JSON.stringify(presetFilters)]);
|
}, [currentTable?.tableName, filterMode, screenId, JSON.stringify(presetFilters)]);
|
||||||
|
|
||||||
// select 옵션 초기 로드 (한 번만 실행, 이후 유지)
|
// select 옵션 초기 로드 (한 번만 실행, 이후 유지)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -384,12 +300,6 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
|
|
||||||
setFilterValues(newValues);
|
setFilterValues(newValues);
|
||||||
|
|
||||||
// 탭별 필터 값 저장
|
|
||||||
if (currentTable?.tableName && currentTableTabId) {
|
|
||||||
const storageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId);
|
|
||||||
localStorage.setItem(storageKey, JSON.stringify(newValues));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 실시간 검색: 값 변경 시 즉시 필터 적용
|
// 실시간 검색: 값 변경 시 즉시 필터 적용
|
||||||
applyFilters(newValues);
|
applyFilters(newValues);
|
||||||
};
|
};
|
||||||
|
|
@ -455,12 +365,6 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
setFilterValues({});
|
setFilterValues({});
|
||||||
setSelectedLabels({});
|
setSelectedLabels({});
|
||||||
currentTable?.onFilterChange([]);
|
currentTable?.onFilterChange([]);
|
||||||
|
|
||||||
// 탭별 저장된 필터 값도 초기화
|
|
||||||
if (currentTable?.tableName && currentTableTabId) {
|
|
||||||
const storageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId);
|
|
||||||
localStorage.removeItem(storageKey);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 필터 입력 필드 렌더링
|
// 필터 입력 필드 렌더링
|
||||||
|
|
|
||||||
|
|
@ -397,6 +397,7 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
|
||||||
// 🆕 수주 등록 관련 컴포넌트들은 간단한 인터페이스 사용
|
// 🆕 수주 등록 관련 컴포넌트들은 간단한 인터페이스 사용
|
||||||
const isSimpleConfigPanel = [
|
const isSimpleConfigPanel = [
|
||||||
"autocomplete-search-input",
|
"autocomplete-search-input",
|
||||||
|
"entity-search-input",
|
||||||
"modal-repeater-table",
|
"modal-repeater-table",
|
||||||
"conditional-container",
|
"conditional-container",
|
||||||
].includes(componentId);
|
].includes(componentId);
|
||||||
|
|
@ -405,19 +406,6 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
|
||||||
return <ConfigPanelComponent config={config} onConfigChange={onChange} />;
|
return <ConfigPanelComponent config={config} onConfigChange={onChange} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// entity-search-input은 currentComponent 정보 필요 (참조 테이블 자동 로드용)
|
|
||||||
// 그리고 allComponents 필요 (연쇄관계 부모 필드 선택용)
|
|
||||||
if (componentId === "entity-search-input") {
|
|
||||||
return (
|
|
||||||
<ConfigPanelComponent
|
|
||||||
config={config}
|
|
||||||
onConfigChange={onChange}
|
|
||||||
currentComponent={currentComponent}
|
|
||||||
allComponents={allComponents}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🆕 selected-items-detail-input은 특별한 props 사용
|
// 🆕 selected-items-detail-input은 특별한 props 사용
|
||||||
if (componentId === "selected-items-detail-input") {
|
if (componentId === "selected-items-detail-input") {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -55,17 +55,12 @@ export interface TableRegistration {
|
||||||
tableName: string; // 실제 DB 테이블명 (예: "item_info")
|
tableName: string; // 실제 DB 테이블명 (예: "item_info")
|
||||||
columns: TableColumn[];
|
columns: TableColumn[];
|
||||||
dataCount?: number; // 현재 표시된 데이터 건수
|
dataCount?: number; // 현재 표시된 데이터 건수
|
||||||
|
|
||||||
// 탭 관련 정보 (탭 내부에 있는 테이블의 경우)
|
|
||||||
parentTabId?: string; // 부모 탭 ID
|
|
||||||
parentTabsComponentId?: string; // 부모 탭 컴포넌트 ID
|
|
||||||
screenId?: number; // 소속 화면 ID
|
|
||||||
|
|
||||||
// 콜백 함수들
|
// 콜백 함수들
|
||||||
onFilterChange: (filters: TableFilter[]) => void;
|
onFilterChange: (filters: TableFilter[]) => void;
|
||||||
onGroupChange: (groups: string[]) => void;
|
onGroupChange: (groups: string[]) => void;
|
||||||
onColumnVisibilityChange: (columns: ColumnVisibility[]) => void;
|
onColumnVisibilityChange: (columns: ColumnVisibility[]) => void;
|
||||||
onGroupSumChange?: (config: GroupSumConfig | null) => void; // 그룹별 합산 설정 변경
|
onGroupSumChange?: (config: GroupSumConfig | null) => void; // 🆕 그룹별 합산 설정 변경
|
||||||
|
|
||||||
// 데이터 조회 함수 (선택 타입 필터용)
|
// 데이터 조회 함수 (선택 타입 필터용)
|
||||||
getColumnUniqueValues?: (columnName: string) => Promise<Array<{ label: string; value: string }>>;
|
getColumnUniqueValues?: (columnName: string) => Promise<Array<{ label: string; value: string }>>;
|
||||||
|
|
@ -82,8 +77,4 @@ export interface TableOptionsContextValue {
|
||||||
updateTableDataCount: (tableId: string, count: number) => void; // 데이터 건수 업데이트
|
updateTableDataCount: (tableId: string, count: number) => void; // 데이터 건수 업데이트
|
||||||
selectedTableId: string | null;
|
selectedTableId: string | null;
|
||||||
setSelectedTableId: (tableId: string | null) => void;
|
setSelectedTableId: (tableId: string | null) => void;
|
||||||
|
|
||||||
// 활성 탭 기반 필터링
|
|
||||||
getActiveTabTables: () => TableRegistration[]; // 현재 활성 탭의 테이블만 반환
|
|
||||||
getTablesForTab: (tabId: string) => TableRegistration[]; // 특정 탭의 테이블만 반환
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue