반복입력 컴포넌트 통합

This commit is contained in:
kjs 2025-12-23 14:45:19 +09:00
parent 9af7fe5b98
commit 2513b89ca2
6 changed files with 2140 additions and 0 deletions

View File

@ -0,0 +1,628 @@
"use client";
/**
* UnifiedRepeater
*
* :
* - simple-repeater-table: 인라인
* - modal-repeater-table: 모달
* - repeat-screen-modal: 화면
* - related-data-buttons: 버튼
*
* .
*/
import React, { useState, useEffect, useCallback, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import { Plus, Trash2, Edit, Eye, GripVertical } from "lucide-react";
import { cn } from "@/lib/utils";
import {
UnifiedRepeaterConfig,
UnifiedRepeaterProps,
RepeaterButtonConfig,
ButtonActionType,
DEFAULT_REPEATER_CONFIG,
} from "@/types/unified-repeater";
import { apiClient } from "@/lib/api/client";
import { commonCodeApi } from "@/lib/api/commonCode";
// 모달 크기 매핑
const MODAL_SIZE_MAP = {
sm: "max-w-md",
md: "max-w-lg",
lg: "max-w-2xl",
xl: "max-w-4xl",
full: "max-w-[95vw]",
};
export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
config: propConfig,
parentId,
data: initialData,
onDataChange,
onRowClick,
onButtonClick,
className,
}) => {
// 설정 병합
const config: UnifiedRepeaterConfig = useMemo(
() => ({
...DEFAULT_REPEATER_CONFIG,
...propConfig,
dataSource: { ...DEFAULT_REPEATER_CONFIG.dataSource, ...propConfig.dataSource },
features: { ...DEFAULT_REPEATER_CONFIG.features, ...propConfig.features },
modal: { ...DEFAULT_REPEATER_CONFIG.modal, ...propConfig.modal },
button: { ...DEFAULT_REPEATER_CONFIG.button, ...propConfig.button },
}),
[propConfig],
);
// 상태
const [data, setData] = useState<any[]>(initialData || []);
const [loading, setLoading] = useState(false);
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
const [editingRow, setEditingRow] = useState<number | null>(null);
const [editedData, setEditedData] = useState<Record<string, any>>({});
const [modalOpen, setModalOpen] = useState(false);
const [modalRow, setModalRow] = useState<any>(null);
const [codeButtons, setCodeButtons] = useState<{ label: string; value: string; variant?: string }[]>([]);
// 데이터 로드
const loadData = useCallback(async () => {
if (!config.dataSource?.tableName || !parentId) return;
setLoading(true);
try {
const response = await apiClient.get(`/dynamic-form/${config.dataSource.tableName}`, {
params: {
[config.dataSource.foreignKey]: parentId,
},
});
if (response.data?.success && response.data?.data) {
const items = Array.isArray(response.data.data) ? response.data.data : [response.data.data];
setData(items);
onDataChange?.(items);
}
} catch (error) {
console.error("UnifiedRepeater 데이터 로드 실패:", error);
} finally {
setLoading(false);
}
}, [config.dataSource?.tableName, config.dataSource?.foreignKey, parentId, onDataChange]);
// 공통코드 버튼 로드
const loadCodeButtons = useCallback(async () => {
if (config.button?.sourceType !== "commonCode" || !config.button?.commonCode?.categoryCode) return;
try {
const response = await commonCodeApi.codes.getList(config.button.commonCode.categoryCode);
if (response.success && response.data) {
const labelField = config.button.commonCode.labelField || "codeName";
setCodeButtons(
response.data.map((code) => ({
label: labelField === "codeName" ? code.codeName : code.codeValue,
value: code.codeValue,
variant: config.button?.commonCode?.variantMapping?.[code.codeValue],
})),
);
}
} catch (error) {
console.error("공통코드 버튼 로드 실패:", error);
}
}, [config.button?.sourceType, config.button?.commonCode]);
// 초기 로드
useEffect(() => {
if (!initialData) {
loadData();
}
}, [loadData, initialData]);
useEffect(() => {
loadCodeButtons();
}, [loadCodeButtons]);
// 행 선택 토글
const toggleRowSelection = (index: number) => {
if (!config.features?.selectable) return;
setSelectedRows((prev) => {
const newSet = new Set(prev);
if (newSet.has(index)) {
newSet.delete(index);
} else {
if (!config.features?.multiSelect) {
newSet.clear();
}
newSet.add(index);
}
return newSet;
});
};
// 행 추가
const handleAddRow = async () => {
if (config.renderMode === "modal" || config.renderMode === "mixed") {
setModalRow(null);
setModalOpen(true);
} else {
// 인라인 추가
const newRow: any = {};
config.columns.forEach((col) => {
newRow[col.key] = "";
});
if (config.dataSource?.foreignKey && parentId) {
newRow[config.dataSource.foreignKey] = parentId;
}
const newData = [...data, newRow];
setData(newData);
onDataChange?.(newData);
setEditingRow(newData.length - 1);
}
};
// 행 삭제
const handleDeleteRow = async (index: number) => {
const row = data[index];
const rowId = row?.id || row?.objid;
if (rowId && config.dataSource?.tableName) {
try {
await apiClient.delete(`/dynamic-form/${config.dataSource.tableName}/${rowId}`);
} catch (error) {
console.error("행 삭제 실패:", error);
return;
}
}
const newData = data.filter((_, i) => i !== index);
setData(newData);
onDataChange?.(newData);
setSelectedRows((prev) => {
const newSet = new Set(prev);
newSet.delete(index);
return newSet;
});
};
// 선택된 행 일괄 삭제
const handleDeleteSelected = async () => {
if (selectedRows.size === 0) return;
const indices = Array.from(selectedRows).sort((a, b) => b - a); // 역순 정렬
for (const index of indices) {
await handleDeleteRow(index);
}
setSelectedRows(new Set());
};
// 인라인 편집 시작
const handleEditRow = (index: number) => {
if (!config.features?.inlineEdit) return;
setEditingRow(index);
setEditedData({ ...data[index] });
};
// 인라인 편집 저장
const handleSaveEdit = async () => {
if (editingRow === null) return;
const rowId = editedData?.id || editedData?.objid;
try {
if (rowId && config.dataSource?.tableName) {
await apiClient.put(`/dynamic-form/${config.dataSource.tableName}/${rowId}`, editedData);
} else if (config.dataSource?.tableName) {
const response = await apiClient.post(`/dynamic-form/${config.dataSource.tableName}`, editedData);
if (response.data?.data?.id) {
editedData.id = response.data.data.id;
}
}
const newData = [...data];
newData[editingRow] = editedData;
setData(newData);
onDataChange?.(newData);
} catch (error) {
console.error("저장 실패:", error);
}
setEditingRow(null);
setEditedData({});
};
// 인라인 편집 취소
const handleCancelEdit = () => {
setEditingRow(null);
setEditedData({});
};
// 행 클릭
const handleRowClick = (row: any, index: number) => {
if (config.features?.selectable) {
toggleRowSelection(index);
}
onRowClick?.(row);
if (config.renderMode === "modal" || config.renderMode === "mixed") {
setModalRow(row);
setModalOpen(true);
}
};
// 버튼 클릭 핸들러
const handleButtonAction = (action: ButtonActionType, row?: any, buttonConfig?: RepeaterButtonConfig) => {
onButtonClick?.(action, row, buttonConfig);
if (action === "view" && row) {
setModalRow(row);
setModalOpen(true);
}
};
// 공통코드 버튼 클릭
const handleCodeButtonClick = async (codeValue: string, row: any, index: number) => {
const valueField = config.button?.commonCode?.valueField;
if (!valueField) return;
const updatedRow = { ...row, [valueField]: codeValue };
const rowId = row?.id || row?.objid;
try {
if (rowId && config.dataSource?.tableName) {
await apiClient.put(`/dynamic-form/${config.dataSource.tableName}/${rowId}`, { [valueField]: codeValue });
}
const newData = [...data];
newData[index] = updatedRow;
setData(newData);
onDataChange?.(newData);
} catch (error) {
console.error("상태 변경 실패:", error);
}
};
// 모달 제목 생성
const getModalTitle = (row?: any) => {
const template = config.modal?.titleTemplate;
if (!template) return row ? "상세 보기" : "새 항목";
let title = template.prefix || "";
if (template.columnKey && row?.[template.columnKey]) {
title += row[template.columnKey];
}
title += template.suffix || "";
return title || (row ? "상세 보기" : "새 항목");
};
// 버튼 렌더링
const renderButtons = (row: any, index: number) => {
const isVertical = config.button?.layout === "vertical";
const buttonStyle = config.button?.style || "outline";
if (config.button?.sourceType === "commonCode") {
return (
<div className={cn("flex gap-1", isVertical && "flex-col")}>
{codeButtons.map((btn) => (
<Button
key={btn.value}
variant={(btn.variant as any) || buttonStyle}
size="sm"
className="h-7 text-xs"
onClick={(e) => {
e.stopPropagation();
handleCodeButtonClick(btn.value, row, index);
}}
>
{btn.label}
</Button>
))}
</div>
);
}
// 수동 버튼
return (
<div className={cn("flex gap-1", isVertical && "flex-col")}>
{(config.button?.manualButtons || []).map((btn) => (
<Button
key={btn.id}
variant={btn.variant as any}
size="sm"
className="h-7 text-xs"
onClick={(e) => {
e.stopPropagation();
handleButtonAction(btn.action, row, btn);
}}
>
{btn.label}
</Button>
))}
</div>
);
};
// 테이블 렌더링 (inline, mixed 모드)
const renderTable = () => {
if (config.renderMode === "button") return null;
return (
<div className="overflow-auto rounded-md border">
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
{config.features?.dragSort && <TableHead className="w-8" />}
{config.features?.selectable && <TableHead className="w-8" />}
{config.features?.showRowNumber && <TableHead className="w-12 text-center">#</TableHead>}
{config.columns
.filter((col) => col.visible !== false)
.map((col) => (
<TableHead key={col.key} style={{ width: col.width !== "auto" ? col.width : undefined }}>
{col.title}
</TableHead>
))}
{(config.features?.inlineEdit || config.features?.showDeleteButton || config.renderMode === "mixed") && (
<TableHead className="w-24 text-center"></TableHead>
)}
</TableRow>
</TableHeader>
<TableBody>
{data.length === 0 ? (
<TableRow>
<TableCell
colSpan={
config.columns.length +
(config.features?.dragSort ? 1 : 0) +
(config.features?.selectable ? 1 : 0) +
(config.features?.showRowNumber ? 1 : 0) +
1
}
className="text-muted-foreground py-8 text-center"
>
</TableCell>
</TableRow>
) : (
data.map((row, index) => (
<TableRow
key={row.id || row.objid || index}
className={cn(
"cursor-pointer hover:bg-muted/50",
selectedRows.has(index) && "bg-primary/10",
editingRow === index && "bg-blue-50",
)}
onClick={() => handleRowClick(row, index)}
>
{config.features?.dragSort && (
<TableCell className="w-8 p-1">
<GripVertical className="text-muted-foreground h-4 w-4 cursor-grab" />
</TableCell>
)}
{config.features?.selectable && (
<TableCell className="w-8 p-1">
<Checkbox
checked={selectedRows.has(index)}
onCheckedChange={() => toggleRowSelection(index)}
onClick={(e) => e.stopPropagation()}
/>
</TableCell>
)}
{config.features?.showRowNumber && (
<TableCell className="text-muted-foreground w-12 text-center text-xs">{index + 1}</TableCell>
)}
{config.columns
.filter((col) => col.visible !== false)
.map((col) => (
<TableCell key={col.key} className="py-2">
{editingRow === index ? (
<Input
value={editedData[col.key] || ""}
onChange={(e) =>
setEditedData((prev) => ({
...prev,
[col.key]: e.target.value,
}))
}
className="h-7 text-xs"
onClick={(e) => e.stopPropagation()}
/>
) : (
<span className="text-sm">{row[col.key]}</span>
)}
</TableCell>
))}
<TableCell className="w-24 p-1 text-center">
<div className="flex items-center justify-center gap-1">
{editingRow === index ? (
<>
<Button
size="sm"
variant="default"
className="h-6 px-2 text-xs"
onClick={(e) => {
e.stopPropagation();
handleSaveEdit();
}}
>
</Button>
<Button
size="sm"
variant="outline"
className="h-6 px-2 text-xs"
onClick={(e) => {
e.stopPropagation();
handleCancelEdit();
}}
>
</Button>
</>
) : (
<>
{config.features?.inlineEdit && (
<Button
size="icon"
variant="ghost"
className="h-6 w-6"
onClick={(e) => {
e.stopPropagation();
handleEditRow(index);
}}
>
<Edit className="h-3 w-3" />
</Button>
)}
{config.renderMode === "mixed" && (
<Button
size="icon"
variant="ghost"
className="h-6 w-6"
onClick={(e) => {
e.stopPropagation();
setModalRow(row);
setModalOpen(true);
}}
>
<Eye className="h-3 w-3" />
</Button>
)}
{config.features?.showDeleteButton && (
<Button
size="icon"
variant="ghost"
className="text-destructive h-6 w-6"
onClick={(e) => {
e.stopPropagation();
handleDeleteRow(index);
}}
>
<Trash2 className="h-3 w-3" />
</Button>
)}
</>
)}
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
);
};
// 버튼 모드 렌더링
const renderButtonMode = () => {
if (config.renderMode !== "button") return null;
const isVertical = config.button?.layout === "vertical";
return (
<div className={cn("flex flex-wrap gap-2", isVertical && "flex-col")}>
{data.map((row, index) => (
<div key={row.id || row.objid || index} className="flex items-center gap-2">
{renderButtons(row, index)}
</div>
))}
</div>
);
};
return (
<div className={cn("space-y-2", className)}>
{/* 헤더 (추가/삭제 버튼) */}
{(config.features?.showAddButton || (config.features?.selectable && selectedRows.size > 0)) && (
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{config.features?.showAddButton && (
<Button size="sm" variant="outline" onClick={handleAddRow} className="h-7 text-xs">
<Plus className="mr-1 h-3 w-3" />
</Button>
)}
{config.features?.selectable && selectedRows.size > 0 && config.features?.showDeleteButton && (
<Button size="sm" variant="destructive" onClick={handleDeleteSelected} className="h-7 text-xs">
<Trash2 className="mr-1 h-3 w-3" />
({selectedRows.size})
</Button>
)}
</div>
<span className="text-muted-foreground text-xs"> {data.length}</span>
</div>
)}
{/* 로딩 */}
{loading && <div className="text-muted-foreground py-4 text-center text-sm"> ...</div>}
{/* 메인 컨텐츠 */}
{!loading && (
<>
{renderTable()}
{renderButtonMode()}
{/* mixed 모드에서 버튼도 표시 */}
{config.renderMode === "mixed" && data.length > 0 && (
<div className="border-t pt-2">
{data.map((row, index) => (
<div key={row.id || row.objid || index} className="mb-1">
{renderButtons(row, index)}
</div>
))}
</div>
)}
</>
)}
{/* 모달 */}
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
<DialogContent className={cn(MODAL_SIZE_MAP[config.modal?.size || "md"])}>
<DialogHeader>
<DialogTitle>{getModalTitle(modalRow)}</DialogTitle>
</DialogHeader>
<div className="py-4">
{config.modal?.screenId ? (
// 화면 기반 모달 - 동적 화면 로드
<div className="text-muted-foreground text-center text-sm">
ID: {config.modal.screenId}
{/* TODO: DynamicScreen 컴포넌트로 교체 */}
</div>
) : (
// 기본 폼 표시
<div className="space-y-3">
{config.columns.map((col) => (
<div key={col.key} className="space-y-1">
<label className="text-sm font-medium">{col.title}</label>
<Input
value={modalRow?.[col.key] || ""}
onChange={(e) =>
setModalRow((prev: any) => ({
...prev,
[col.key]: e.target.value,
}))
}
className="h-9"
/>
</div>
))}
</div>
)}
</div>
</DialogContent>
</Dialog>
</div>
);
};
UnifiedRepeater.displayName = "UnifiedRepeater";
export default UnifiedRepeater;

File diff suppressed because it is too large Load Diff

View File

@ -88,6 +88,9 @@ import "./mail-recipient-selector/MailRecipientSelectorRenderer"; // 내부 인
// 🆕 연관 데이터 버튼 컴포넌트
import "./related-data-buttons/RelatedDataButtonsRenderer"; // 좌측 선택 데이터 기반 연관 테이블 버튼 표시
// 🆕 통합 반복 데이터 컴포넌트 (Unified)
import "./unified-repeater/UnifiedRepeaterRenderer"; // 인라인/모달/버튼 모드 통합
/**
*
*/

View File

@ -0,0 +1,105 @@
"use client";
/**
* UnifiedRepeater
*
*/
import React from "react";
import { ComponentRegistry } from "../../ComponentRegistry";
import { UnifiedRepeater } from "@/components/unified/UnifiedRepeater";
import { UnifiedRepeaterDefinition } from "./index";
import { UnifiedRepeaterConfig, DEFAULT_REPEATER_CONFIG } from "@/types/unified-repeater";
interface UnifiedRepeaterRendererProps {
component: any;
data?: any;
mode?: "view" | "edit";
isPreview?: boolean;
onDataChange?: (data: any[]) => void;
onRowClick?: (row: any) => void;
onButtonClick?: (action: string, row?: any, buttonConfig?: any) => void;
parentId?: string | number;
}
const UnifiedRepeaterRenderer: React.FC<UnifiedRepeaterRendererProps> = ({
component,
data,
mode,
isPreview,
onDataChange,
onRowClick,
onButtonClick,
parentId,
}) => {
// component.config에서 UnifiedRepeaterConfig 추출
const config: UnifiedRepeaterConfig = React.useMemo(() => {
const componentConfig = component?.config || component?.props?.config || {};
return {
...DEFAULT_REPEATER_CONFIG,
...componentConfig,
dataSource: {
...DEFAULT_REPEATER_CONFIG.dataSource,
...componentConfig.dataSource,
},
columns: componentConfig.columns || [],
features: {
...DEFAULT_REPEATER_CONFIG.features,
...componentConfig.features,
},
modal: {
...DEFAULT_REPEATER_CONFIG.modal,
...componentConfig.modal,
},
button: {
...DEFAULT_REPEATER_CONFIG.button,
...componentConfig.button,
},
};
}, [component]);
// parentId 결정: props에서 전달받거나 data에서 추출
const resolvedParentId = React.useMemo(() => {
if (parentId) return parentId;
if (data && config.dataSource?.referenceKey) {
return data[config.dataSource.referenceKey];
}
return undefined;
}, [parentId, data, config.dataSource?.referenceKey]);
// 미리보기 모드에서는 샘플 데이터 표시
if (isPreview) {
return (
<div className="rounded-md border border-dashed p-4 text-center">
<div className="text-muted-foreground text-sm">
<br />
<span className="text-xs">
: {config.renderMode} | : {config.dataSource?.tableName || "미설정"}
</span>
</div>
</div>
);
}
return (
<UnifiedRepeater
config={config}
parentId={resolvedParentId}
data={Array.isArray(data) ? data : undefined}
onDataChange={onDataChange}
onRowClick={onRowClick}
onButtonClick={onButtonClick}
className={component?.className}
/>
);
};
// 컴포넌트 레지스트리에 등록
ComponentRegistry.registerComponent({
...UnifiedRepeaterDefinition,
render: (props: any) => <UnifiedRepeaterRenderer {...props} />,
});
export default UnifiedRepeaterRenderer;

View File

@ -0,0 +1,98 @@
/**
* UnifiedRepeater
*
*
* simple-repeater-table, modal-repeater-table, repeat-screen-modal, related-data-buttons
*/
import { ComponentCategory } from "@/types/component";
import { createComponentDefinition } from "../../utils/createComponentDefinition";
import { UnifiedRepeaterConfigPanel } from "@/components/unified/config-panels/UnifiedRepeaterConfigPanel";
import { UnifiedRepeater } from "@/components/unified/UnifiedRepeater";
export const UnifiedRepeaterDefinition = createComponentDefinition({
id: "unified-repeater",
name: "통합 반복 데이터",
description: "반복 데이터 관리 (인라인/모달/버튼 모드)",
category: ComponentCategory.UNIFIED,
webType: "entity", // 반복 데이터는 엔티티 참조 타입
version: "1.0.0",
component: UnifiedRepeater, // React 컴포넌트 (필수)
// 기본 속성
defaultProps: {
config: {
renderMode: "inline",
dataSource: {
tableName: "",
foreignKey: "",
referenceKey: "",
},
columns: [],
modal: {
size: "md",
},
button: {
sourceType: "manual",
manualButtons: [],
layout: "horizontal",
style: "outline",
},
features: {
showAddButton: true,
showDeleteButton: true,
inlineEdit: false,
dragSort: false,
showRowNumber: false,
selectable: false,
multiSelect: false,
},
},
},
// 설정 스키마
configSchema: {
renderMode: {
type: "select",
label: "렌더링 모드",
options: [
{ value: "inline", label: "인라인 (테이블)" },
{ value: "modal", label: "모달" },
{ value: "button", label: "버튼" },
{ value: "mixed", label: "혼합 (테이블 + 버튼)" },
],
},
"dataSource.tableName": {
type: "tableSelect",
label: "데이터 테이블",
description: "반복 데이터가 저장된 테이블",
},
"dataSource.foreignKey": {
type: "columnSelect",
label: "연결 키 (FK)",
description: "부모 레코드를 참조하는 컬럼",
dependsOn: "dataSource.tableName",
},
"dataSource.referenceKey": {
type: "columnSelect",
label: "상위 키",
description: "현재 화면 테이블의 PK 컬럼",
useCurrentTable: true,
},
},
// 이벤트
events: ["onDataChange", "onRowClick", "onButtonClick"],
// 아이콘
icon: "Repeat",
// 태그
tags: ["data", "repeater", "table", "modal", "button", "unified"],
// 설정 패널
configPanel: UnifiedRepeaterConfigPanel,
});
export default UnifiedRepeaterDefinition;

View File

@ -0,0 +1,230 @@
/**
* UnifiedRepeater
*
* :
* - simple-repeater-table: 인라인
* - modal-repeater-table: 모달
* - repeat-screen-modal: 화면
* - related-data-buttons: 버튼
*/
// 렌더링 모드
export type RepeaterRenderMode = "inline" | "modal" | "button" | "mixed";
// 버튼 소스 타입
export type ButtonSourceType = "commonCode" | "manual";
// 버튼 액션 타입
export type ButtonActionType = "create" | "update" | "delete" | "view" | "navigate" | "custom";
// 버튼 색상/스타일
export type ButtonVariant = "default" | "primary" | "secondary" | "destructive" | "outline" | "ghost";
// 버튼 레이아웃
export type ButtonLayout = "horizontal" | "vertical";
// 모달 크기
export type ModalSize = "sm" | "md" | "lg" | "xl" | "full";
// 컬럼 너비 옵션
export type ColumnWidthOption = "auto" | "60px" | "80px" | "100px" | "120px" | "150px" | "200px" | "250px" | "300px";
// 컬럼 설정
export interface RepeaterColumnConfig {
key: string;
title: string;
width: ColumnWidthOption;
visible: boolean;
isJoinColumn?: boolean;
sourceTable?: string;
}
// 버튼 설정 (수동 모드)
export interface RepeaterButtonConfig {
id: string;
label: string;
action: ButtonActionType;
variant: ButtonVariant;
icon?: string;
confirmMessage?: string;
// 네비게이트 액션용
navigateScreen?: number;
navigateParams?: Record<string, string>;
// 커스텀 액션용
customHandler?: string;
}
// 공통코드 버튼 설정
export interface CommonCodeButtonConfig {
categoryCode: string;
labelField: "codeValue" | "codeName";
valueField: string; // 버튼 클릭 시 전달할 값의 컬럼
variantMapping?: Record<string, ButtonVariant>; // 코드값별 색상 매핑
}
// 모달 설정
export interface RepeaterModalConfig {
screenId?: number;
size: ModalSize;
titleTemplate?: {
prefix?: string;
columnKey?: string;
suffix?: string;
};
}
// 기능 옵션
export interface RepeaterFeatureOptions {
showAddButton: boolean;
showDeleteButton: boolean;
inlineEdit: boolean;
dragSort: boolean;
showRowNumber: boolean;
selectable: boolean;
multiSelect: boolean;
}
// 메인 설정 타입
export interface UnifiedRepeaterConfig {
// 렌더링 모드
renderMode: RepeaterRenderMode;
// 데이터 소스 설정
dataSource: {
tableName: string; // 데이터 테이블
foreignKey: string; // 연결 키 (FK) - 데이터 테이블의 컬럼
referenceKey: string; // 상위 키 - 현재 화면 테이블의 컬럼 (부모 ID)
filter?: { // 추가 필터 조건
column: string;
value: string;
};
};
// 컬럼 설정
columns: RepeaterColumnConfig[];
// 모달 설정 (modal, mixed 모드)
modal?: RepeaterModalConfig;
// 버튼 설정 (button, mixed 모드)
button?: {
sourceType: ButtonSourceType;
commonCode?: CommonCodeButtonConfig;
manualButtons?: RepeaterButtonConfig[];
layout: ButtonLayout;
style: ButtonVariant;
};
// 기능 옵션
features: RepeaterFeatureOptions;
// 스타일
style?: {
maxHeight?: string;
minHeight?: string;
borderless?: boolean;
compact?: boolean;
};
}
// 컴포넌트 Props
export interface UnifiedRepeaterProps {
config: UnifiedRepeaterConfig;
parentId?: string | number; // 부모 레코드 ID
data?: any[]; // 초기 데이터 (없으면 API로 로드)
onDataChange?: (data: any[]) => void;
onRowClick?: (row: any) => void;
onButtonClick?: (action: ButtonActionType, row?: any, buttonConfig?: RepeaterButtonConfig) => void;
className?: string;
}
// 기본 설정값
export const DEFAULT_REPEATER_CONFIG: UnifiedRepeaterConfig = {
renderMode: "inline",
dataSource: {
tableName: "",
foreignKey: "",
referenceKey: "",
},
columns: [],
modal: {
size: "md",
},
button: {
sourceType: "manual",
manualButtons: [],
layout: "horizontal",
style: "outline",
},
features: {
showAddButton: true,
showDeleteButton: true,
inlineEdit: false,
dragSort: false,
showRowNumber: false,
selectable: false,
multiSelect: false,
},
};
// 고정 옵션들 (콤보박스용)
export const RENDER_MODE_OPTIONS = [
{ value: "inline", label: "인라인 (테이블)" },
{ value: "modal", label: "모달" },
{ value: "button", label: "버튼" },
{ value: "mixed", label: "혼합 (테이블 + 버튼)" },
] as const;
export const MODAL_SIZE_OPTIONS = [
{ value: "sm", label: "작게 (sm)" },
{ value: "md", label: "중간 (md)" },
{ value: "lg", label: "크게 (lg)" },
{ value: "xl", label: "매우 크게 (xl)" },
{ value: "full", label: "전체 화면" },
] as const;
export const COLUMN_WIDTH_OPTIONS = [
{ value: "auto", label: "자동" },
{ value: "60px", label: "60px" },
{ value: "80px", label: "80px" },
{ value: "100px", label: "100px" },
{ value: "120px", label: "120px" },
{ value: "150px", label: "150px" },
{ value: "200px", label: "200px" },
{ value: "250px", label: "250px" },
{ value: "300px", label: "300px" },
] as const;
export const BUTTON_ACTION_OPTIONS = [
{ value: "create", label: "생성" },
{ value: "update", label: "수정" },
{ value: "delete", label: "삭제" },
{ value: "view", label: "보기" },
{ value: "navigate", label: "화면 이동" },
{ value: "custom", label: "커스텀" },
] as const;
export const BUTTON_VARIANT_OPTIONS = [
{ value: "default", label: "기본" },
{ value: "primary", label: "Primary" },
{ value: "secondary", label: "Secondary" },
{ value: "destructive", label: "삭제 (빨강)" },
{ value: "outline", label: "Outline" },
{ value: "ghost", label: "Ghost" },
] as const;
export const BUTTON_LAYOUT_OPTIONS = [
{ value: "horizontal", label: "가로 배치" },
{ value: "vertical", label: "세로 배치" },
] as const;
export const BUTTON_SOURCE_OPTIONS = [
{ value: "commonCode", label: "공통코드 사용" },
{ value: "manual", label: "수동 설정" },
] as const;
export const LABEL_FIELD_OPTIONS = [
{ value: "codeName", label: "코드명" },
{ value: "codeValue", label: "코드값" },
] as const;