카테고리 구현

This commit is contained in:
kjs
2025-11-05 18:08:51 +09:00
parent f3bed0d713
commit bc029d1df8
23 changed files with 563 additions and 427 deletions

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -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>
</>
);
};

View File

@@ -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" &&

View File

@@ -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";

View File

@@ -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">