카드 디스플레이

선택안함 옵션 추가
This commit is contained in:
kjs 2025-12-16 14:56:31 +09:00
parent f7e3c1924c
commit 963e0c2d24
3 changed files with 362 additions and 170 deletions

View File

@ -424,18 +424,16 @@ export class EntityJoinController {
config.referenceTable config.referenceTable
); );
// 현재 display_column으로 사용 중인 컬럼 제외 // 현재 display_column 정보 (참고용으로만 사용, 필터링하지 않음)
const currentDisplayColumn = const currentDisplayColumn =
config.displayColumn || config.displayColumns[0]; config.displayColumn || config.displayColumns[0];
const availableColumns = columns.filter(
(col) => col.columnName !== currentDisplayColumn // 모든 컬럼 표시 (기본 표시 컬럼도 포함)
);
return { return {
joinConfig: config, joinConfig: config,
tableName: config.referenceTable, tableName: config.referenceTable,
currentDisplayColumn: currentDisplayColumn, currentDisplayColumn: currentDisplayColumn,
availableColumns: availableColumns.map((col) => ({ availableColumns: columns.map((col) => ({
columnName: col.columnName, columnName: col.columnName,
columnLabel: col.displayName || col.columnName, columnLabel: col.displayName || col.columnName,
dataType: col.dataType, dataType: col.dataType,

View File

@ -4,6 +4,7 @@ import React, { useEffect, useState, useMemo, useCallback, useRef } from "react"
import { ComponentRendererProps } from "@/types/component"; import { ComponentRendererProps } from "@/types/component";
import { CardDisplayConfig } from "./types"; import { CardDisplayConfig } from "./types";
import { tableTypeApi } from "@/lib/api/screen"; import { tableTypeApi } from "@/lib/api/screen";
import { entityJoinApi } from "@/lib/api/entityJoin";
import { getFullImageUrl, apiClient } from "@/lib/api/client"; import { getFullImageUrl, apiClient } from "@/lib/api/client";
import { filterDOMProps } from "@/lib/utils/domPropsFilter"; import { filterDOMProps } from "@/lib/utils/domPropsFilter";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
@ -308,10 +309,35 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
search: Object.keys(linkedFilterValues).length > 0 ? linkedFilterValues : undefined, search: Object.keys(linkedFilterValues).length > 0 ? linkedFilterValues : undefined,
}; };
// 조인 컬럼 설정 가져오기 (componentConfig에서)
const joinColumnsConfig = component.componentConfig?.joinColumns || [];
const entityJoinColumns = joinColumnsConfig
.filter((col: any) => col.isJoinColumn)
.map((col: any) => ({
columnName: col.columnName,
sourceColumn: col.sourceColumn,
referenceTable: col.referenceTable,
referenceColumn: col.referenceColumn,
displayColumn: col.referenceColumn,
label: col.label,
joinAlias: col.columnName, // 백엔드에서 필요한 joinAlias 추가
sourceTable: tableNameToUse, // 기준 테이블
}));
// 테이블 데이터, 컬럼 정보, 입력 타입 정보를 병렬로 로드 // 테이블 데이터, 컬럼 정보, 입력 타입 정보를 병렬로 로드
const [dataResponse, columnsResponse, inputTypesResponse] = await Promise.all([ // 조인 컬럼이 있으면 entityJoinApi 사용
tableTypeApi.getTableData(tableNameToUse, apiParams), let dataResponse;
if (entityJoinColumns.length > 0) {
console.log("🔗 [CardDisplay] 엔티티 조인 API 사용:", entityJoinColumns);
dataResponse = await entityJoinApi.getTableDataWithJoins(tableNameToUse, {
...apiParams,
additionalJoinColumns: entityJoinColumns,
});
} else {
dataResponse = await tableTypeApi.getTableData(tableNameToUse, apiParams);
}
const [columnsResponse, inputTypesResponse] = await Promise.all([
tableTypeApi.getColumns(tableNameToUse), tableTypeApi.getColumns(tableNameToUse),
tableTypeApi.getColumnInputTypes(tableNameToUse), tableTypeApi.getColumnInputTypes(tableNameToUse),
]); ]);

View File

@ -1,6 +1,21 @@
"use client"; "use client";
import React from "react"; import React, { useState, useEffect } from "react";
import { entityJoinApi } from "@/lib/api/entityJoin";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Trash2 } from "lucide-react";
interface CardDisplayConfigPanelProps { interface CardDisplayConfigPanelProps {
config: any; config: any;
@ -9,9 +24,32 @@ interface CardDisplayConfigPanelProps {
tableColumns?: any[]; tableColumns?: any[];
} }
interface EntityJoinColumn {
tableName: string;
columnName: string;
columnLabel: string;
dataType: string;
joinAlias: string;
suggestedLabel: string;
}
interface JoinTable {
tableName: string;
currentDisplayColumn: string;
joinConfig?: {
sourceColumn: string;
};
availableColumns: Array<{
columnName: string;
columnLabel: string;
dataType: string;
description?: string;
}>;
}
/** /**
* CardDisplay * CardDisplay
* UI * UI +
*/ */
export const CardDisplayConfigPanel: React.FC<CardDisplayConfigPanelProps> = ({ export const CardDisplayConfigPanel: React.FC<CardDisplayConfigPanelProps> = ({
config, config,
@ -19,6 +57,40 @@ export const CardDisplayConfigPanel: React.FC<CardDisplayConfigPanelProps> = ({
screenTableName, screenTableName,
tableColumns = [], tableColumns = [],
}) => { }) => {
// 엔티티 조인 컬럼 상태
const [entityJoinColumns, setEntityJoinColumns] = useState<{
availableColumns: EntityJoinColumn[];
joinTables: JoinTable[];
}>({ availableColumns: [], joinTables: [] });
const [loadingEntityJoins, setLoadingEntityJoins] = useState(false);
// 엔티티 조인 컬럼 정보 가져오기
useEffect(() => {
const fetchEntityJoinColumns = async () => {
const tableName = config.tableName || screenTableName;
if (!tableName) {
setEntityJoinColumns({ availableColumns: [], joinTables: [] });
return;
}
setLoadingEntityJoins(true);
try {
const result = await entityJoinApi.getEntityJoinColumns(tableName);
setEntityJoinColumns({
availableColumns: result.availableColumns || [],
joinTables: result.joinTables || [],
});
} catch (error) {
console.error("Entity 조인 컬럼 조회 오류:", error);
setEntityJoinColumns({ availableColumns: [], joinTables: [] });
} finally {
setLoadingEntityJoins(false);
}
};
fetchEntityJoinColumns();
}, [config.tableName, screenTableName]);
const handleChange = (key: string, value: any) => { const handleChange = (key: string, value: any) => {
onChange({ ...config, [key]: value }); onChange({ ...config, [key]: value });
}; };
@ -28,7 +100,6 @@ export const CardDisplayConfigPanel: React.FC<CardDisplayConfigPanelProps> = ({
let newConfig = { ...config }; let newConfig = { ...config };
let current = newConfig; let current = newConfig;
// 중첩 객체 생성
for (let i = 0; i < keys.length - 1; i++) { for (let i = 0; i < keys.length - 1; i++) {
if (!current[keys[i]]) { if (!current[keys[i]]) {
current[keys[i]] = {}; current[keys[i]] = {};
@ -40,6 +111,47 @@ export const CardDisplayConfigPanel: React.FC<CardDisplayConfigPanelProps> = ({
onChange(newConfig); onChange(newConfig);
}; };
// 컬럼 선택 시 조인 컬럼이면 joinColumns 설정도 함께 업데이트
const handleColumnSelect = (path: string, columnName: string) => {
const joinColumn = entityJoinColumns.availableColumns.find(
(col) => col.joinAlias === columnName
);
if (joinColumn) {
const joinColumnsConfig = config.joinColumns || [];
const existingJoinColumn = joinColumnsConfig.find(
(jc: any) => jc.columnName === columnName
);
if (!existingJoinColumn) {
const joinTableInfo = entityJoinColumns.joinTables?.find(
(jt) => jt.tableName === joinColumn.tableName
);
const newJoinColumnConfig = {
columnName: joinColumn.joinAlias,
label: joinColumn.suggestedLabel || joinColumn.columnLabel,
sourceColumn: joinTableInfo?.joinConfig?.sourceColumn || "",
referenceTable: joinColumn.tableName,
referenceColumn: joinColumn.columnName,
isJoinColumn: true,
};
onChange({
...config,
columnMapping: {
...config.columnMapping,
[path.split(".")[1]]: columnName,
},
joinColumns: [...joinColumnsConfig, newJoinColumnConfig],
});
return;
}
}
handleNestedChange(path, columnName);
};
// 표시 컬럼 추가 // 표시 컬럼 추가
const addDisplayColumn = () => { const addDisplayColumn = () => {
const currentColumns = config.columnMapping?.displayColumns || []; const currentColumns = config.columnMapping?.displayColumns || [];
@ -58,122 +170,198 @@ export const CardDisplayConfigPanel: React.FC<CardDisplayConfigPanelProps> = ({
const updateDisplayColumn = (index: number, value: string) => { const updateDisplayColumn = (index: number, value: string) => {
const currentColumns = [...(config.columnMapping?.displayColumns || [])]; const currentColumns = [...(config.columnMapping?.displayColumns || [])];
currentColumns[index] = value; currentColumns[index] = value;
const joinColumn = entityJoinColumns.availableColumns.find(
(col) => col.joinAlias === value
);
if (joinColumn) {
const joinColumnsConfig = config.joinColumns || [];
const existingJoinColumn = joinColumnsConfig.find(
(jc: any) => jc.columnName === value
);
if (!existingJoinColumn) {
const joinTableInfo = entityJoinColumns.joinTables?.find(
(jt) => jt.tableName === joinColumn.tableName
);
const newJoinColumnConfig = {
columnName: joinColumn.joinAlias,
label: joinColumn.suggestedLabel || joinColumn.columnLabel,
sourceColumn: joinTableInfo?.joinConfig?.sourceColumn || "",
referenceTable: joinColumn.tableName,
referenceColumn: joinColumn.columnName,
isJoinColumn: true,
};
onChange({
...config,
columnMapping: {
...config.columnMapping,
displayColumns: currentColumns,
},
joinColumns: [...joinColumnsConfig, newJoinColumnConfig],
});
return;
}
}
handleNestedChange("columnMapping.displayColumns", currentColumns); handleNestedChange("columnMapping.displayColumns", currentColumns);
}; };
// 테이블별로 조인 컬럼 그룹화
const joinColumnsByTable: Record<string, EntityJoinColumn[]> = {};
entityJoinColumns.availableColumns.forEach((col) => {
if (!joinColumnsByTable[col.tableName]) {
joinColumnsByTable[col.tableName] = [];
}
joinColumnsByTable[col.tableName].push(col);
});
// 컬럼 선택 셀렉트 박스 렌더링 (Shadcn UI)
const renderColumnSelect = (
value: string,
onChangeHandler: (value: string) => void,
placeholder: string = "컬럼을 선택하세요"
) => {
return (
<Select
value={value || "__none__"}
onValueChange={(val) => onChangeHandler(val === "__none__" ? "" : val)}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
{/* 선택 안함 옵션 */}
<SelectItem value="__none__" className="text-xs text-muted-foreground">
</SelectItem>
{/* 기본 테이블 컬럼 */}
{tableColumns.length > 0 && (
<SelectGroup>
<SelectLabel className="text-xs font-semibold text-muted-foreground">
</SelectLabel>
{tableColumns.map((column) => (
<SelectItem
key={column.columnName}
value={column.columnName}
className="text-xs"
>
{column.columnLabel || column.columnName}
</SelectItem>
))}
</SelectGroup>
)}
{/* 조인 테이블별 컬럼 */}
{Object.entries(joinColumnsByTable).map(([tableName, columns]) => (
<SelectGroup key={tableName}>
<SelectLabel className="text-xs font-semibold text-blue-600">
{tableName} ()
</SelectLabel>
{columns.map((col) => (
<SelectItem
key={col.joinAlias}
value={col.joinAlias}
className="text-xs"
>
{col.suggestedLabel || col.columnLabel}
</SelectItem>
))}
</SelectGroup>
))}
</SelectContent>
</Select>
);
};
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="text-sm font-medium text-gray-700"> </div> <div className="text-sm font-medium"> </div>
{/* 테이블이 선택된 경우 컬럼 매핑 설정 */} {/* 테이블이 선택된 경우 컬럼 매핑 설정 */}
{tableColumns && tableColumns.length > 0 && ( {tableColumns && tableColumns.length > 0 && (
<div className="space-y-3"> <div className="space-y-3">
<h5 className="text-xs font-medium text-gray-700"> </h5> <h5 className="text-xs font-medium text-muted-foreground"> </h5>
<div> {loadingEntityJoins && (
<label className="mb-1 block text-xs font-medium text-gray-600"> </label> <div className="text-xs text-muted-foreground"> ...</div>
<select )}
value={config.columnMapping?.titleColumn || ""}
onChange={(e) => handleNestedChange("columnMapping.titleColumn", e.target.value)} <div className="space-y-1">
className="w-full rounded border border-gray-300 px-2 py-1 text-sm" <Label className="text-xs"> </Label>
> {renderColumnSelect(
<option value=""> </option> config.columnMapping?.titleColumn || "",
{tableColumns.map((column) => ( (value) => handleColumnSelect("columnMapping.titleColumn", value)
<option key={column.columnName} value={column.columnName}> )}
{column.columnLabel || column.columnName} ({column.dataType})
</option>
))}
</select>
</div> </div>
<div> <div className="space-y-1">
<label className="mb-1 block text-xs font-medium text-gray-600"> </label> <Label className="text-xs"> </Label>
<select {renderColumnSelect(
value={config.columnMapping?.subtitleColumn || ""} config.columnMapping?.subtitleColumn || "",
onChange={(e) => handleNestedChange("columnMapping.subtitleColumn", e.target.value)} (value) => handleColumnSelect("columnMapping.subtitleColumn", value)
className="w-full rounded border border-gray-300 px-2 py-1 text-sm" )}
>
<option value=""> </option>
{tableColumns.map((column) => (
<option key={column.columnName} value={column.columnName}>
{column.columnLabel || column.columnName} ({column.dataType})
</option>
))}
</select>
</div> </div>
<div> <div className="space-y-1">
<label className="mb-1 block text-xs font-medium text-gray-600"> </label> <Label className="text-xs"> </Label>
<select {renderColumnSelect(
value={config.columnMapping?.descriptionColumn || ""} config.columnMapping?.descriptionColumn || "",
onChange={(e) => handleNestedChange("columnMapping.descriptionColumn", e.target.value)} (value) => handleColumnSelect("columnMapping.descriptionColumn", value)
className="w-full rounded border border-gray-300 px-2 py-1 text-sm" )}
>
<option value=""> </option>
{tableColumns.map((column) => (
<option key={column.columnName} value={column.columnName}>
{column.columnLabel || column.columnName} ({column.dataType})
</option>
))}
</select>
</div> </div>
<div> <div className="space-y-1">
<label className="mb-1 block text-xs font-medium text-gray-600"> </label> <Label className="text-xs"> </Label>
<select {renderColumnSelect(
value={config.columnMapping?.imageColumn || ""} config.columnMapping?.imageColumn || "",
onChange={(e) => handleNestedChange("columnMapping.imageColumn", e.target.value)} (value) => handleColumnSelect("columnMapping.imageColumn", value)
className="w-full rounded border border-gray-300 px-2 py-1 text-sm" )}
>
<option value=""> </option>
{tableColumns.map((column) => (
<option key={column.columnName} value={column.columnName}>
{column.columnLabel || column.columnName} ({column.dataType})
</option>
))}
</select>
</div> </div>
{/* 동적 표시 컬럼 추가 */} {/* 동적 표시 컬럼 추가 */}
<div> <div className="space-y-2">
<div className="mb-2 flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="text-xs font-medium text-gray-600"> </label> <Label className="text-xs"> </Label>
<button <Button
type="button" type="button"
variant="outline"
size="sm"
onClick={addDisplayColumn} onClick={addDisplayColumn}
className="rounded bg-blue-500 px-2 py-1 text-xs text-white hover:bg-blue-600" className="h-6 px-2 text-xs"
> >
+ +
</button> </Button>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
{(config.columnMapping?.displayColumns || []).map((column: string, index: number) => ( {(config.columnMapping?.displayColumns || []).map((column: string, index: number) => (
<div key={index} className="flex items-center space-x-2"> <div key={index} className="flex items-center gap-2">
<select <div className="flex-1">
value={column} {renderColumnSelect(
onChange={(e) => updateDisplayColumn(index, e.target.value)} column,
className="flex-1 rounded border border-gray-300 px-2 py-1 text-sm" (value) => updateDisplayColumn(index, value)
> )}
<option value=""> </option> </div>
{tableColumns.map((col) => ( <Button
<option key={col.columnName} value={col.columnName}>
{col.columnLabel || col.columnName} ({col.dataType})
</option>
))}
</select>
<button
type="button" type="button"
variant="ghost"
size="sm"
onClick={() => removeDisplayColumn(index)} onClick={() => removeDisplayColumn(index)}
className="rounded bg-red-500 px-2 py-1 text-xs text-white hover:bg-red-600" className="h-8 w-8 p-0 text-destructive hover:bg-destructive/10 hover:text-destructive"
> >
<Trash2 className="h-4 w-4" />
</button> </Button>
</div> </div>
))} ))}
{(!config.columnMapping?.displayColumns || config.columnMapping.displayColumns.length === 0) && ( {(!config.columnMapping?.displayColumns || config.columnMapping.displayColumns.length === 0) && (
<div className="rounded border border-dashed border-gray-300 py-2 text-center text-xs text-gray-500"> <div className="rounded-md border border-dashed border-muted-foreground/30 py-3 text-center text-xs text-muted-foreground">
"컬럼 추가" "컬럼 추가"
</div> </div>
)} )}
@ -184,186 +372,166 @@ export const CardDisplayConfigPanel: React.FC<CardDisplayConfigPanelProps> = ({
{/* 카드 스타일 설정 */} {/* 카드 스타일 설정 */}
<div className="space-y-3"> <div className="space-y-3">
<h5 className="text-xs font-medium text-gray-700"> </h5> <h5 className="text-xs font-medium text-muted-foreground"> </h5>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-3">
<div> <div className="space-y-1">
<label className="mb-1 block text-xs font-medium text-gray-600"> </label> <Label className="text-xs"> </Label>
<input <Input
type="number" type="number"
min="1" min="1"
max="6" max="6"
value={config.cardsPerRow || 3} value={config.cardsPerRow || 3}
onChange={(e) => handleChange("cardsPerRow", parseInt(e.target.value))} onChange={(e) => handleChange("cardsPerRow", parseInt(e.target.value))}
className="w-full rounded border border-gray-300 px-2 py-1 text-sm" className="h-8 text-xs"
/> />
</div> </div>
<div> <div className="space-y-1">
<label className="mb-1 block text-xs font-medium text-gray-600"> (px)</label> <Label className="text-xs"> (px)</Label>
<input <Input
type="number" type="number"
min="0" min="0"
max="50" max="50"
value={config.cardSpacing || 16} value={config.cardSpacing || 16}
onChange={(e) => handleChange("cardSpacing", parseInt(e.target.value))} onChange={(e) => handleChange("cardSpacing", parseInt(e.target.value))}
className="w-full rounded border border-gray-300 px-2 py-1 text-sm" className="h-8 text-xs"
/> />
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<input <Checkbox
type="checkbox"
id="showTitle" id="showTitle"
checked={config.cardStyle?.showTitle ?? true} checked={config.cardStyle?.showTitle ?? true}
onChange={(e) => handleNestedChange("cardStyle.showTitle", e.target.checked)} onCheckedChange={(checked) => handleNestedChange("cardStyle.showTitle", checked)}
className="rounded border-gray-300"
/> />
<label htmlFor="showTitle" className="text-xs text-gray-600"> <Label htmlFor="showTitle" className="text-xs font-normal">
</label> </Label>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<input <Checkbox
type="checkbox"
id="showSubtitle" id="showSubtitle"
checked={config.cardStyle?.showSubtitle ?? true} checked={config.cardStyle?.showSubtitle ?? true}
onChange={(e) => handleNestedChange("cardStyle.showSubtitle", e.target.checked)} onCheckedChange={(checked) => handleNestedChange("cardStyle.showSubtitle", checked)}
className="rounded border-gray-300"
/> />
<label htmlFor="showSubtitle" className="text-xs text-gray-600"> <Label htmlFor="showSubtitle" className="text-xs font-normal">
</label> </Label>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<input <Checkbox
type="checkbox"
id="showDescription" id="showDescription"
checked={config.cardStyle?.showDescription ?? true} checked={config.cardStyle?.showDescription ?? true}
onChange={(e) => handleNestedChange("cardStyle.showDescription", e.target.checked)} onCheckedChange={(checked) => handleNestedChange("cardStyle.showDescription", checked)}
className="rounded border-gray-300"
/> />
<label htmlFor="showDescription" className="text-xs text-gray-600"> <Label htmlFor="showDescription" className="text-xs font-normal">
</label> </Label>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<input <Checkbox
type="checkbox"
id="showImage" id="showImage"
checked={config.cardStyle?.showImage ?? false} checked={config.cardStyle?.showImage ?? false}
onChange={(e) => handleNestedChange("cardStyle.showImage", e.target.checked)} onCheckedChange={(checked) => handleNestedChange("cardStyle.showImage", checked)}
className="rounded border-gray-300"
/> />
<label htmlFor="showImage" className="text-xs text-gray-600"> <Label htmlFor="showImage" className="text-xs font-normal">
</label> </Label>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<input <Checkbox
type="checkbox"
id="showActions" id="showActions"
checked={config.cardStyle?.showActions ?? true} checked={config.cardStyle?.showActions ?? true}
onChange={(e) => handleNestedChange("cardStyle.showActions", e.target.checked)} onCheckedChange={(checked) => handleNestedChange("cardStyle.showActions", checked)}
className="rounded border-gray-300"
/> />
<label htmlFor="showActions" className="text-xs text-gray-600"> <Label htmlFor="showActions" className="text-xs font-normal">
</label> </Label>
</div> </div>
{/* 개별 버튼 설정 (액션 버튼이 활성화된 경우에만 표시) */} {/* 개별 버튼 설정 */}
{(config.cardStyle?.showActions ?? true) && ( {(config.cardStyle?.showActions ?? true) && (
<div className="ml-4 space-y-2 border-l-2 border-gray-200 pl-3"> <div className="ml-5 space-y-2 border-l-2 border-muted pl-3">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<input <Checkbox
type="checkbox"
id="showViewButton" id="showViewButton"
checked={config.cardStyle?.showViewButton ?? true} checked={config.cardStyle?.showViewButton ?? true}
onChange={(e) => handleNestedChange("cardStyle.showViewButton", e.target.checked)} onCheckedChange={(checked) => handleNestedChange("cardStyle.showViewButton", checked)}
className="rounded border-gray-300"
/> />
<label htmlFor="showViewButton" className="text-xs text-gray-600"> <Label htmlFor="showViewButton" className="text-xs font-normal">
</label> </Label>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<input <Checkbox
type="checkbox"
id="showEditButton" id="showEditButton"
checked={config.cardStyle?.showEditButton ?? true} checked={config.cardStyle?.showEditButton ?? true}
onChange={(e) => handleNestedChange("cardStyle.showEditButton", e.target.checked)} onCheckedChange={(checked) => handleNestedChange("cardStyle.showEditButton", checked)}
className="rounded border-gray-300"
/> />
<label htmlFor="showEditButton" className="text-xs text-gray-600"> <Label htmlFor="showEditButton" className="text-xs font-normal">
</label> </Label>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<input <Checkbox
type="checkbox"
id="showDeleteButton" id="showDeleteButton"
checked={config.cardStyle?.showDeleteButton ?? false} checked={config.cardStyle?.showDeleteButton ?? false}
onChange={(e) => handleNestedChange("cardStyle.showDeleteButton", e.target.checked)} onCheckedChange={(checked) => handleNestedChange("cardStyle.showDeleteButton", checked)}
className="rounded border-gray-300"
/> />
<label htmlFor="showDeleteButton" className="text-xs text-gray-600"> <Label htmlFor="showDeleteButton" className="text-xs font-normal">
</label> </Label>
</div> </div>
</div> </div>
)} )}
</div> </div>
<div> <div className="space-y-1">
<label className="mb-1 block text-xs font-medium text-gray-600"> </label> <Label className="text-xs"> </Label>
<input <Input
type="number" type="number"
min="10" min="10"
max="500" max="500"
value={config.cardStyle?.maxDescriptionLength || 100} value={config.cardStyle?.maxDescriptionLength || 100}
onChange={(e) => handleNestedChange("cardStyle.maxDescriptionLength", parseInt(e.target.value))} onChange={(e) => handleNestedChange("cardStyle.maxDescriptionLength", parseInt(e.target.value))}
className="w-full rounded border border-gray-300 px-2 py-1 text-sm" className="h-8 text-xs"
/> />
</div> </div>
</div> </div>
{/* 공통 설정 */} {/* 공통 설정 */}
<div className="space-y-3"> <div className="space-y-3">
<h5 className="text-xs font-medium text-gray-700"> </h5> <h5 className="text-xs font-medium text-muted-foreground"> </h5>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<input <Checkbox
type="checkbox"
id="disabled" id="disabled"
checked={config.disabled || false} checked={config.disabled || false}
onChange={(e) => handleChange("disabled", e.target.checked)} onCheckedChange={(checked) => handleChange("disabled", checked)}
className="rounded border-gray-300"
/> />
<label htmlFor="disabled" className="text-xs text-gray-600"> <Label htmlFor="disabled" className="text-xs font-normal">
</label> </Label>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<input <Checkbox
type="checkbox"
id="readonly" id="readonly"
checked={config.readonly || false} checked={config.readonly || false}
onChange={(e) => handleChange("readonly", e.target.checked)} onCheckedChange={(checked) => handleChange("readonly", checked)}
className="rounded border-gray-300"
/> />
<label htmlFor="readonly" className="text-xs text-gray-600"> <Label htmlFor="readonly" className="text-xs font-normal">
</label> </Label>
</div> </div>
</div> </div>
</div> </div>