390 lines
12 KiB
TypeScript
390 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useMemo, useEffect } from "react";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { cn } from "@/lib/utils";
|
|
import {
|
|
Table,
|
|
FormInput,
|
|
Columns,
|
|
LayoutGrid,
|
|
X,
|
|
} from "lucide-react";
|
|
|
|
// 자동생성 설정
|
|
export interface AutoGenerateConfig {
|
|
templateType: "list" | "form" | "master-detail" | "card";
|
|
selectedColumns: string[];
|
|
includeSearch: boolean;
|
|
includeCrud: boolean; // CRUD 버튼 포함 여부
|
|
includeModal: boolean; // 등록/수정 모달 포함 여부
|
|
}
|
|
|
|
// 컬럼 정보
|
|
export interface AutoGenerateColumn {
|
|
columnName: string;
|
|
label: string;
|
|
webType: string;
|
|
required: boolean;
|
|
}
|
|
|
|
// 모달 props
|
|
export interface AutoGenerateModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
tableName: string;
|
|
tableLabel: string;
|
|
columns: AutoGenerateColumn[];
|
|
onGenerate: (config: AutoGenerateConfig) => void;
|
|
}
|
|
|
|
// 시스템 컬럼 목록
|
|
const SYSTEM_COLUMNS = [
|
|
"created_by",
|
|
"updated_by",
|
|
"created_date",
|
|
"updated_date",
|
|
"created_at",
|
|
"updated_at",
|
|
"writer",
|
|
"company_code",
|
|
];
|
|
|
|
// 시스템 컬럼 판별
|
|
function isSystemColumn(columnName: string): boolean {
|
|
return SYSTEM_COLUMNS.includes(columnName.toLowerCase());
|
|
}
|
|
|
|
// 템플릿 타입 정의
|
|
interface TemplateOption {
|
|
id: AutoGenerateConfig["templateType"];
|
|
label: string;
|
|
description: string;
|
|
icon: React.ReactNode;
|
|
}
|
|
|
|
const TEMPLATES: TemplateOption[] = [
|
|
{
|
|
id: "list",
|
|
label: "목록형",
|
|
description: "검색 + 테이블 + CRUD 버튼",
|
|
icon: (
|
|
<div className="flex flex-col items-center justify-center gap-0.5 text-[10px] text-muted-foreground">
|
|
<div className="flex gap-0.5">
|
|
<div className="h-1 w-6 bg-current" />
|
|
</div>
|
|
<div className="flex gap-0.5">
|
|
<div className="h-1 w-6 bg-current" />
|
|
</div>
|
|
<div className="flex gap-0.5">
|
|
<div className="h-1 w-6 bg-current" />
|
|
</div>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: "form",
|
|
label: "폼형",
|
|
description: "입력 필드 + 저장 버튼",
|
|
icon: (
|
|
<div className="flex flex-col items-center justify-center gap-0.5 text-[10px] text-muted-foreground">
|
|
<div className="flex gap-0.5">
|
|
<div className="h-1 w-3 bg-current" />
|
|
<div className="h-1 w-3 bg-current" />
|
|
</div>
|
|
<div className="flex gap-0.5">
|
|
<div className="h-1 w-3 bg-current" />
|
|
<div className="h-1 w-3 bg-current" />
|
|
</div>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: "master-detail",
|
|
label: "마스터-디테일",
|
|
description: "좌측 목록 + 우측 상세",
|
|
icon: (
|
|
<div className="flex items-center justify-center gap-0.5 text-[10px] text-muted-foreground">
|
|
<div className="flex flex-col gap-0.5">
|
|
<div className="h-1 w-2 bg-current" />
|
|
<div className="h-1 w-2 bg-current" />
|
|
<div className="h-1 w-2 bg-current" />
|
|
</div>
|
|
<div className="h-4 w-[1px] bg-current" />
|
|
<div className="flex flex-col gap-0.5">
|
|
<div className="h-1 w-2 bg-current" />
|
|
<div className="h-1 w-2 bg-current" />
|
|
</div>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: "card",
|
|
label: "카드형",
|
|
description: "카드 그리드 뷰",
|
|
icon: (
|
|
<div className="flex flex-wrap items-center justify-center gap-0.5 text-[10px] text-muted-foreground">
|
|
<div className="h-2 w-2 border border-current" />
|
|
<div className="h-2 w-2 border border-current" />
|
|
<div className="h-2 w-2 border border-current" />
|
|
</div>
|
|
),
|
|
},
|
|
];
|
|
|
|
export function AutoGenerateModal({
|
|
isOpen,
|
|
onClose,
|
|
tableName,
|
|
tableLabel,
|
|
columns,
|
|
onGenerate,
|
|
}: AutoGenerateModalProps) {
|
|
// 템플릿 타입 선택
|
|
const [templateType, setTemplateType] = useState<AutoGenerateConfig["templateType"]>("list");
|
|
|
|
// 컬럼 선택 (기본: 시스템 컬럼 제외한 모든 컬럼 선택)
|
|
const [selectedColumns, setSelectedColumns] = useState<Set<string>>(() => {
|
|
const initial = new Set<string>();
|
|
columns.forEach((col) => {
|
|
if (!isSystemColumn(col.columnName)) {
|
|
initial.add(col.columnName);
|
|
}
|
|
});
|
|
return initial;
|
|
});
|
|
|
|
// 모달이 열릴 때마다 선택 상태 리셋 (columns가 변경되면)
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
const initial = new Set<string>();
|
|
columns.forEach((col) => {
|
|
if (!isSystemColumn(col.columnName)) {
|
|
initial.add(col.columnName);
|
|
}
|
|
});
|
|
setSelectedColumns(initial);
|
|
setTemplateType("list");
|
|
}
|
|
}, [isOpen, columns]);
|
|
|
|
// 옵션
|
|
const [includeSearch, setIncludeSearch] = useState(true);
|
|
const [includeCrud, setIncludeCrud] = useState(true);
|
|
const [includeModal, setIncludeModal] = useState(true);
|
|
|
|
// 컬럼 체크박스 토글
|
|
const toggleColumn = (columnName: string) => {
|
|
setSelectedColumns((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(columnName)) {
|
|
next.delete(columnName);
|
|
} else {
|
|
next.add(columnName);
|
|
}
|
|
return next;
|
|
});
|
|
};
|
|
|
|
// 전체 선택/해제
|
|
const toggleAll = () => {
|
|
if (selectedColumns.size === columns.length) {
|
|
setSelectedColumns(new Set());
|
|
} else {
|
|
setSelectedColumns(new Set(columns.map((c) => c.columnName)));
|
|
}
|
|
};
|
|
|
|
// 생성 핸들러
|
|
const handleGenerate = () => {
|
|
const config: AutoGenerateConfig = {
|
|
templateType,
|
|
selectedColumns: Array.from(selectedColumns),
|
|
includeSearch,
|
|
includeCrud,
|
|
includeModal,
|
|
};
|
|
onGenerate(config);
|
|
};
|
|
|
|
// 선택된 컬럼 개수
|
|
const selectedCount = selectedColumns.size;
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
<DialogContent className="max-w-[95vw] sm:max-w-[600px]">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-base sm:text-lg">화면 자동 생성</DialogTitle>
|
|
<DialogDescription className="text-xs sm:text-sm">
|
|
테이블: <span className="font-medium text-foreground">{tableLabel || tableName}</span> ({tableName})
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4 sm:space-y-5">
|
|
{/* 템플릿 선택 */}
|
|
<div>
|
|
<Label className="text-xs font-medium sm:text-sm">어떤 형태로 만들까요?</Label>
|
|
<div className="mt-2 grid grid-cols-2 gap-2 sm:grid-cols-4">
|
|
{TEMPLATES.map((template) => (
|
|
<button
|
|
key={template.id}
|
|
type="button"
|
|
onClick={() => setTemplateType(template.id)}
|
|
className={cn(
|
|
"flex flex-col items-center justify-center gap-2 rounded-md border-2 p-3 transition-all hover:bg-accent",
|
|
templateType === template.id
|
|
? "border-primary bg-primary/5"
|
|
: "border-border",
|
|
)}
|
|
>
|
|
<div className="flex h-10 w-10 items-center justify-center rounded border border-border bg-background">
|
|
{template.icon}
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-xs font-medium sm:text-sm">{template.label}</div>
|
|
<div className="text-[10px] text-muted-foreground sm:text-xs">
|
|
{template.description}
|
|
</div>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
{/* 컬럼 선택 */}
|
|
<div>
|
|
<div className="mb-2 flex items-center justify-between">
|
|
<Label className="text-xs font-medium sm:text-sm">
|
|
포함할 컬럼 ({selectedCount}/{columns.length})
|
|
</Label>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={toggleAll}
|
|
className="h-7 text-xs"
|
|
>
|
|
{selectedColumns.size === columns.length ? "전체 해제" : "전체 선택"}
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="max-h-[200px] space-y-2 overflow-y-auto rounded-md border border-border p-3">
|
|
{columns.map((col) => {
|
|
const isSystem = isSystemColumn(col.columnName);
|
|
return (
|
|
<div key={col.columnName} className="flex items-center gap-2">
|
|
<Checkbox
|
|
id={`col-${col.columnName}`}
|
|
checked={selectedColumns.has(col.columnName)}
|
|
onCheckedChange={() => toggleColumn(col.columnName)}
|
|
/>
|
|
<Label
|
|
htmlFor={`col-${col.columnName}`}
|
|
className="flex flex-1 cursor-pointer items-center gap-2 text-xs sm:text-sm"
|
|
>
|
|
<span className={cn(isSystem && "text-muted-foreground")}>
|
|
{col.label || col.columnName}
|
|
</span>
|
|
<span className="text-[10px] text-muted-foreground">
|
|
({col.columnName})
|
|
</span>
|
|
{col.required && (
|
|
<Badge variant="destructive" className="h-4 text-[9px]">
|
|
필수
|
|
</Badge>
|
|
)}
|
|
{isSystem && (
|
|
<Badge variant="secondary" className="h-4 text-[9px]">
|
|
숨김 권장
|
|
</Badge>
|
|
)}
|
|
</Label>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
{/* 옵션 */}
|
|
<div>
|
|
<Label className="text-xs font-medium sm:text-sm">옵션</Label>
|
|
<div className="mt-2 space-y-2">
|
|
{templateType === "list" || templateType === "master-detail" ? (
|
|
<div className="flex items-center gap-2">
|
|
<Checkbox
|
|
id="includeSearch"
|
|
checked={includeSearch}
|
|
onCheckedChange={(checked) => setIncludeSearch(!!checked)}
|
|
/>
|
|
<Label htmlFor="includeSearch" className="cursor-pointer text-xs sm:text-sm">
|
|
검색 바 포함
|
|
</Label>
|
|
</div>
|
|
) : null}
|
|
|
|
{templateType === "list" || templateType === "form" ? (
|
|
<div className="flex items-center gap-2">
|
|
<Checkbox
|
|
id="includeCrud"
|
|
checked={includeCrud}
|
|
onCheckedChange={(checked) => setIncludeCrud(!!checked)}
|
|
/>
|
|
<Label htmlFor="includeCrud" className="cursor-pointer text-xs sm:text-sm">
|
|
CRUD 버튼 포함 (등록/수정/삭제)
|
|
</Label>
|
|
</div>
|
|
) : null}
|
|
|
|
{templateType === "list" ? (
|
|
<div className="flex items-center gap-2">
|
|
<Checkbox
|
|
id="includeModal"
|
|
checked={includeModal}
|
|
onCheckedChange={(checked) => setIncludeModal(!!checked)}
|
|
/>
|
|
<Label htmlFor="includeModal" className="cursor-pointer text-xs sm:text-sm">
|
|
등록/수정 모달 포함
|
|
</Label>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter className="gap-2 sm:gap-0">
|
|
<Button
|
|
variant="outline"
|
|
onClick={onClose}
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
>
|
|
취소
|
|
</Button>
|
|
<Button
|
|
onClick={handleGenerate}
|
|
disabled={selectedCount === 0}
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
>
|
|
생성하기
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|