카테고리 구현
This commit is contained in:
@@ -4,11 +4,11 @@ import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
||||
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
|
||||
} from "@/components/ui/resizable-dialog";
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -104,13 +104,13 @@ export default function CopyScreenModal({ isOpen, onClose, sourceScreen, onCopyS
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<ResizableDialogTitle className="flex items-center gap-2">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Copy className="h-5 w-5" />
|
||||
화면 복사
|
||||
</ResizableDialogTitle>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{sourceScreen?.screenName} 화면을 복사합니다. 화면 구성도 함께 복사됩니다.
|
||||
</ResizableDialogDescription>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
@@ -168,7 +168,7 @@ export default function CopyScreenModal({ isOpen, onClose, sourceScreen, onCopyS
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ResizableDialogFooter>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleClose} disabled={isCopying}>
|
||||
취소
|
||||
</Button>
|
||||
@@ -185,7 +185,7 @@ export default function CopyScreenModal({ isOpen, onClose, sourceScreen, onCopyS
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</ResizableDialogFooter>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -368,18 +368,30 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
// 검색 가능한 컬럼만 필터링
|
||||
const visibleColumns = component.columns?.filter((col: DataTableColumn) => col.visible) || [];
|
||||
|
||||
// 컬럼의 실제 웹 타입 정보 찾기
|
||||
// 컬럼의 실제 웹 타입 정보 찾기 (webType 또는 input_type)
|
||||
const getColumnWebType = useCallback(
|
||||
(columnName: string) => {
|
||||
// 먼저 컴포넌트에 설정된 컬럼에서 찾기 (화면 관리에서 설정한 값 우선)
|
||||
const componentColumn = component.columns?.find((col) => col.columnName === columnName);
|
||||
if (componentColumn?.widgetType && componentColumn.widgetType !== "text") {
|
||||
console.log(`🔍 [${columnName}] componentColumn.widgetType 사용:`, componentColumn.widgetType);
|
||||
return componentColumn.widgetType;
|
||||
}
|
||||
|
||||
// 없으면 테이블 타입 관리에서 설정된 값 찾기
|
||||
const tableColumn = tableColumns.find((col) => col.columnName === columnName);
|
||||
return tableColumn?.webType || "text";
|
||||
|
||||
// input_type 우선 사용 (category 등)
|
||||
const inputType = (tableColumn as any)?.input_type || (tableColumn as any)?.inputType;
|
||||
if (inputType) {
|
||||
console.log(`✅ [${columnName}] input_type 사용:`, inputType);
|
||||
return inputType;
|
||||
}
|
||||
|
||||
// 없으면 webType 사용
|
||||
const result = tableColumn?.webType || "text";
|
||||
console.log(`✅ [${columnName}] webType 사용:`, result);
|
||||
return result;
|
||||
},
|
||||
[component.columns, tableColumns],
|
||||
);
|
||||
@@ -1414,6 +1426,31 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
</div>
|
||||
);
|
||||
|
||||
case "category": {
|
||||
// 카테고리 셀렉트 (동적 import)
|
||||
const { CategorySelectComponent } = require("@/lib/registry/components/category-select/CategorySelectComponent");
|
||||
console.log("🎯 카테고리 렌더링 (편집 폼):", {
|
||||
tableName: component.tableName,
|
||||
columnName: column.columnName,
|
||||
columnLabel: column.label,
|
||||
value,
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<CategorySelectComponent
|
||||
tableName={component.tableName}
|
||||
columnName={column.columnName}
|
||||
value={value}
|
||||
onChange={(newValue) => handleEditFormChange(column.columnName, newValue)}
|
||||
placeholder={advancedConfig?.placeholder || `${column.label} 선택...`}
|
||||
required={isRequired}
|
||||
className={commonProps.className}
|
||||
/>
|
||||
{advancedConfig?.helpText && <p className="mt-1 text-xs text-gray-500">{advancedConfig.helpText}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
return (
|
||||
<div>
|
||||
@@ -1676,6 +1713,31 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
</div>
|
||||
);
|
||||
|
||||
case "category": {
|
||||
// 카테고리 셀렉트 (동적 import)
|
||||
const { CategorySelectComponent } = require("@/lib/registry/components/category-select/CategorySelectComponent");
|
||||
console.log("🎯 카테고리 렌더링 (추가 폼):", {
|
||||
tableName: component.tableName,
|
||||
columnName: column.columnName,
|
||||
columnLabel: column.label,
|
||||
value,
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<CategorySelectComponent
|
||||
tableName={component.tableName}
|
||||
columnName={column.columnName}
|
||||
value={value}
|
||||
onChange={(newValue) => handleAddFormChange(column.columnName, newValue)}
|
||||
placeholder={advancedConfig?.placeholder || `${column.label} 선택...`}
|
||||
required={isRequired}
|
||||
className={commonProps.className}
|
||||
/>
|
||||
{advancedConfig?.helpText && <p className="mt-1 text-xs text-gray-500">{advancedConfig.helpText}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -4,11 +4,11 @@ import React, { useState, useEffect, useRef } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
||||
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
|
||||
} from "@/components/ui/resizable-dialog";
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
@@ -345,26 +345,26 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResizableDialog open={isOpen} onOpenChange={onClose}>
|
||||
<ResizableDialogContent className="max-w-2xl">
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
{assignmentSuccess ? (
|
||||
// 성공 화면
|
||||
<>
|
||||
<ResizableDialogHeader>
|
||||
<ResizableDialogTitle className="flex items-center gap-2">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-green-100">
|
||||
<svg className="h-5 w-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
{assignmentMessage.includes("나중에") ? "화면 저장 완료" : "화면 할당 완료"}
|
||||
</ResizableDialogTitle>
|
||||
<ResizableDialogDescription>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{assignmentMessage.includes("나중에")
|
||||
? "화면이 성공적으로 저장되었습니다. 나중에 메뉴에 할당할 수 있습니다."
|
||||
: "화면이 성공적으로 메뉴에 할당되었습니다."}
|
||||
</ResizableDialogDescription>
|
||||
</ResizableDialogHeader>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border bg-green-50 p-4">
|
||||
@@ -386,7 +386,7 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ResizableDialogFooter>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={() => {
|
||||
// 타이머 정리
|
||||
@@ -407,19 +407,19 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
|
||||
<Monitor className="mr-2 h-4 w-4" />
|
||||
화면 목록으로 이동
|
||||
</Button>
|
||||
</ResizableDialogFooter>
|
||||
</DialogFooter>
|
||||
</>
|
||||
) : (
|
||||
// 기본 할당 화면
|
||||
<>
|
||||
<ResizableDialogHeader>
|
||||
<ResizableDialogTitle className="flex items-center gap-2">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Settings className="h-5 w-5" />
|
||||
메뉴에 화면 할당
|
||||
</ResizableDialogTitle>
|
||||
<ResizableDialogDescription>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
저장된 화면을 메뉴에 할당하여 사용자가 접근할 수 있도록 설정합니다.
|
||||
</ResizableDialogDescription>
|
||||
</DialogDescription>
|
||||
{screenInfo && (
|
||||
<div className="bg-accent mt-2 rounded-lg border p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -432,7 +432,7 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
|
||||
{screenInfo.description && <p className="mt-1 text-sm text-blue-700">{screenInfo.description}</p>}
|
||||
</div>
|
||||
)}
|
||||
</ResizableDialogHeader>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* 메뉴 선택 (검색 기능 포함) */}
|
||||
@@ -572,22 +572,22 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</ResizableDialogFooter>
|
||||
</DialogFooter>
|
||||
</>
|
||||
)}
|
||||
</ResizableDialogContent>
|
||||
</ResizableDialog>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 화면 교체 확인 대화상자 */}
|
||||
<ResizableDialog open={showReplaceDialog} onOpenChange={setShowReplaceDialog}>
|
||||
<ResizableDialogContent className="max-w-md">
|
||||
<ResizableDialogHeader>
|
||||
<ResizableDialogTitle className="flex items-center gap-2">
|
||||
<Dialog open={showReplaceDialog} onOpenChange={setShowReplaceDialog}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Monitor className="h-5 w-5 text-orange-600" />
|
||||
화면 교체 확인
|
||||
</ResizableDialogTitle>
|
||||
<ResizableDialogDescription>선택한 메뉴에 이미 할당된 화면이 있습니다.</ResizableDialogDescription>
|
||||
</ResizableDialogHeader>
|
||||
</DialogTitle>
|
||||
<DialogDescription>선택한 메뉴에 이미 할당된 화면이 있습니다.</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* 기존 화면 목록 */}
|
||||
@@ -652,9 +652,9 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</ResizableDialogFooter>
|
||||
</ResizableDialogContent>
|
||||
</ResizableDialog>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2555,6 +2555,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
componentConfig: {
|
||||
type: componentId, // text-input, number-input 등
|
||||
webType: column.widgetType, // 원본 웹타입 보존
|
||||
inputType: column.inputType, // ✅ input_type 추가 (category 등)
|
||||
...getDefaultWebTypeConfig(column.widgetType),
|
||||
// 코드 타입인 경우 코드 카테고리 정보 추가
|
||||
...(column.widgetType === "code" &&
|
||||
@@ -2618,6 +2619,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
componentConfig: {
|
||||
type: componentId, // text-input, number-input 등
|
||||
webType: column.widgetType, // 원본 웹타입 보존
|
||||
inputType: column.inputType, // ✅ input_type 추가 (category 등)
|
||||
...getDefaultWebTypeConfig(column.widgetType),
|
||||
// 코드 타입인 경우 코드 카테고리 정보 추가
|
||||
...(column.widgetType === "code" &&
|
||||
|
||||
@@ -17,15 +17,24 @@ import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertResizableDialogContent,
|
||||
AlertResizableDialogDescription,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertResizableDialogHeader,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { ResizableDialog, ResizableDialogContent, ResizableDialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
ResizableDialog,
|
||||
ResizableDialogContent,
|
||||
ResizableDialogHeader
|
||||
} from "@/components/ui/dialog";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { MoreHorizontal, Edit, Trash2, Copy, Eye, Plus, Search, Palette, RotateCcw, Trash } from "lucide-react";
|
||||
import { ScreenDefinition } from "@/types/screen";
|
||||
|
||||
@@ -6,49 +6,26 @@ import { CategoryValueManager } from "@/components/table-category/CategoryValueM
|
||||
|
||||
interface CategoryWidgetProps {
|
||||
widgetId: string;
|
||||
menuId?: number; // 현재 화면의 menuId (선택사항)
|
||||
tableName: string; // 현재 화면의 테이블
|
||||
selectedScreen?: any; // 화면 정보 전체 (menuId 추출용)
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리 관리 위젯 (좌우 분할)
|
||||
* - 좌측: 현재 테이블의 카테고리 타입 컬럼 목록
|
||||
* - 우측: 선택된 컬럼의 카테고리 값 관리
|
||||
* - 우측: 선택된 컬럼의 카테고리 값 관리 (테이블 스코프)
|
||||
*/
|
||||
export function CategoryWidget({
|
||||
widgetId,
|
||||
menuId: propMenuId,
|
||||
tableName,
|
||||
selectedScreen,
|
||||
}: CategoryWidgetProps) {
|
||||
export function CategoryWidget({ widgetId, tableName }: CategoryWidgetProps) {
|
||||
const [selectedColumn, setSelectedColumn] = useState<{
|
||||
columnName: string;
|
||||
columnLabel: string;
|
||||
} | null>(null);
|
||||
|
||||
// menuId 추출: props > selectedScreen > 기본값(1)
|
||||
const menuId =
|
||||
propMenuId ||
|
||||
selectedScreen?.menuId ||
|
||||
selectedScreen?.menu_id ||
|
||||
1; // 기본값
|
||||
|
||||
// menuId가 없으면 경고 메시지 표시
|
||||
if (!menuId || menuId === 1) {
|
||||
console.warn("⚠️ CategoryWidget: menuId가 제공되지 않아 기본값(1)을 사용합니다", {
|
||||
propMenuId,
|
||||
selectedScreen,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-[10px] gap-6">
|
||||
{/* 좌측: 카테고리 컬럼 리스트 (30%) */}
|
||||
<div className="w-[30%] border-r pr-6">
|
||||
<CategoryColumnList
|
||||
tableName={tableName}
|
||||
menuId={menuId}
|
||||
selectedColumn={selectedColumn?.columnName || null}
|
||||
onColumnSelect={(columnName, columnLabel) =>
|
||||
setSelectedColumn({ columnName, columnLabel })
|
||||
@@ -63,7 +40,6 @@ export function CategoryWidget({
|
||||
tableName={tableName}
|
||||
columnName={selectedColumn.columnName}
|
||||
columnLabel={selectedColumn.columnLabel}
|
||||
menuId={menuId}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-64 flex-col items-center justify-center rounded-lg border bg-card shadow-sm">
|
||||
|
||||
Reference in New Issue
Block a user