feat(TableSection): 테이블 컬럼 부모값 받기 기능 추가
TableColumnConfig에 receiveFromParent, parentFieldName 속성 추가 allComponents에서 부모 화면 필드 자동 추출 컬럼 설정에 "부모값" 스위치 및 부모 필드 선택 UI 추가 handleAddItems()에서 부모값 자동 적용 로직 구현
This commit is contained in:
parent
6a4ebf362c
commit
533eaf5c9f
|
|
@ -621,6 +621,14 @@ export function TableSectionRenderer({
|
|||
if (col.defaultValue !== undefined && newItem[col.field] === undefined) {
|
||||
newItem[col.field] = col.defaultValue;
|
||||
}
|
||||
|
||||
// 부모에서 값 받기 (receiveFromParent)
|
||||
if (col.receiveFromParent) {
|
||||
const parentField = col.parentFieldName || col.field;
|
||||
if (formData[parentField] !== undefined) {
|
||||
newItem[col.field] = formData[parentField];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newItem;
|
||||
|
|
|
|||
|
|
@ -48,13 +48,24 @@ const HelpText = ({ children }: { children: React.ReactNode }) => (
|
|||
<p className="text-[10px] text-muted-foreground mt-0.5">{children}</p>
|
||||
);
|
||||
|
||||
export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFormModalConfigPanelProps) {
|
||||
// 부모 화면에서 전달 가능한 필드 타입
|
||||
interface AvailableParentField {
|
||||
name: string; // 필드명 (columnName)
|
||||
label: string; // 표시 라벨
|
||||
sourceComponent?: string; // 출처 컴포넌트 (예: "TableList", "SplitPanelLayout2")
|
||||
sourceTable?: string; // 출처 테이블명
|
||||
}
|
||||
|
||||
export function UniversalFormModalConfigPanel({ config, onChange, allComponents = [] }: UniversalFormModalConfigPanelProps) {
|
||||
// 테이블 목록
|
||||
const [tables, setTables] = useState<{ name: string; label: string }[]>([]);
|
||||
const [tableColumns, setTableColumns] = useState<{
|
||||
[tableName: string]: { name: string; type: string; label: string }[];
|
||||
}>({});
|
||||
|
||||
// 부모 화면에서 전달 가능한 필드 목록
|
||||
const [availableParentFields, setAvailableParentFields] = useState<AvailableParentField[]>([]);
|
||||
|
||||
// 채번규칙 목록
|
||||
const [numberingRules, setNumberingRules] = useState<{ id: string; name: string }[]>([]);
|
||||
|
||||
|
|
@ -72,6 +83,186 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor
|
|||
loadNumberingRules();
|
||||
}, []);
|
||||
|
||||
// allComponents에서 부모 화면에서 전달 가능한 필드 추출
|
||||
useEffect(() => {
|
||||
const extractParentFields = async () => {
|
||||
if (!allComponents || allComponents.length === 0) {
|
||||
setAvailableParentFields([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const fields: AvailableParentField[] = [];
|
||||
|
||||
for (const comp of allComponents) {
|
||||
// 컴포넌트 타입 추출 (여러 위치에서 확인)
|
||||
const compType = comp.componentId || comp.componentConfig?.type || comp.componentConfig?.id || comp.type;
|
||||
const compConfig = comp.componentConfig || {};
|
||||
|
||||
// 1. TableList / InteractiveDataTable - 테이블 컬럼 추출
|
||||
if (compType === "table-list" || compType === "interactive-data-table") {
|
||||
const tableName = compConfig.selectedTable || compConfig.tableName;
|
||||
if (tableName) {
|
||||
// 테이블 컬럼 로드
|
||||
try {
|
||||
const response = await apiClient.get(`/table-management/tables/${tableName}/columns`);
|
||||
const columns = response.data?.data?.columns;
|
||||
if (response.data?.success && Array.isArray(columns)) {
|
||||
columns.forEach((col: any) => {
|
||||
const colName = col.columnName || col.column_name;
|
||||
const colLabel = col.displayName || col.columnComment || col.column_comment || colName;
|
||||
fields.push({
|
||||
name: colName,
|
||||
label: colLabel,
|
||||
sourceComponent: "TableList",
|
||||
sourceTable: tableName,
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`테이블 컬럼 로드 실패 (${tableName}):`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. SplitPanelLayout2 - 데이터 전달 필드 및 소스 테이블 컬럼 추출
|
||||
if (compType === "split-panel-layout2") {
|
||||
// dataTransferFields 추출
|
||||
const transferFields = compConfig.dataTransferFields;
|
||||
if (transferFields && Array.isArray(transferFields)) {
|
||||
transferFields.forEach((field: any) => {
|
||||
if (field.targetColumn) {
|
||||
fields.push({
|
||||
name: field.targetColumn,
|
||||
label: field.targetColumn,
|
||||
sourceComponent: "SplitPanelLayout2",
|
||||
sourceTable: compConfig.leftPanel?.tableName,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 좌측 패널 테이블 컬럼도 추출
|
||||
const leftTableName = compConfig.leftPanel?.tableName;
|
||||
if (leftTableName) {
|
||||
try {
|
||||
const response = await apiClient.get(`/table-management/tables/${leftTableName}/columns`);
|
||||
const columns = response.data?.data?.columns;
|
||||
if (response.data?.success && Array.isArray(columns)) {
|
||||
columns.forEach((col: any) => {
|
||||
const colName = col.columnName || col.column_name;
|
||||
const colLabel = col.displayName || col.columnComment || col.column_comment || colName;
|
||||
// 중복 방지
|
||||
if (!fields.some(f => f.name === colName && f.sourceTable === leftTableName)) {
|
||||
fields.push({
|
||||
name: colName,
|
||||
label: colLabel,
|
||||
sourceComponent: "SplitPanelLayout2 (좌측)",
|
||||
sourceTable: leftTableName,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`테이블 컬럼 로드 실패 (${leftTableName}):`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 기타 테이블 관련 컴포넌트
|
||||
if (compType === "card-display" || compType === "simple-repeater-table") {
|
||||
const tableName = compConfig.tableName || compConfig.initialDataConfig?.sourceTable;
|
||||
if (tableName) {
|
||||
try {
|
||||
const response = await apiClient.get(`/table-management/tables/${tableName}/columns`);
|
||||
const columns = response.data?.data?.columns;
|
||||
if (response.data?.success && Array.isArray(columns)) {
|
||||
columns.forEach((col: any) => {
|
||||
const colName = col.columnName || col.column_name;
|
||||
const colLabel = col.displayName || col.columnComment || col.column_comment || colName;
|
||||
if (!fields.some(f => f.name === colName && f.sourceTable === tableName)) {
|
||||
fields.push({
|
||||
name: colName,
|
||||
label: colLabel,
|
||||
sourceComponent: compType,
|
||||
sourceTable: tableName,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`테이블 컬럼 로드 실패 (${tableName}):`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 버튼 컴포넌트 - openModalWithData의 fieldMappings/dataMapping에서 소스 컬럼 추출
|
||||
if (compType === "button-primary" || compType === "button" || compType === "button-secondary") {
|
||||
const action = compConfig.action || {};
|
||||
|
||||
// fieldMappings에서 소스 컬럼 추출
|
||||
const fieldMappings = action.fieldMappings || [];
|
||||
fieldMappings.forEach((mapping: any) => {
|
||||
if (mapping.sourceColumn && !fields.some(f => f.name === mapping.sourceColumn)) {
|
||||
fields.push({
|
||||
name: mapping.sourceColumn,
|
||||
label: mapping.sourceColumn,
|
||||
sourceComponent: "Button (fieldMappings)",
|
||||
sourceTable: action.sourceTableName,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// dataMapping에서 소스 컬럼 추출
|
||||
const dataMapping = action.dataMapping || [];
|
||||
dataMapping.forEach((mapping: any) => {
|
||||
if (mapping.sourceColumn && !fields.some(f => f.name === mapping.sourceColumn)) {
|
||||
fields.push({
|
||||
name: mapping.sourceColumn,
|
||||
label: mapping.sourceColumn,
|
||||
sourceComponent: "Button (dataMapping)",
|
||||
sourceTable: action.sourceTableName,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 현재 모달의 저장 테이블 컬럼도 추가 (부모에서 전달받을 수 있는 값들)
|
||||
const currentTableName = config.saveConfig?.tableName;
|
||||
if (currentTableName) {
|
||||
try {
|
||||
const response = await apiClient.get(`/table-management/tables/${currentTableName}/columns`);
|
||||
const columns = response.data?.data?.columns;
|
||||
if (response.data?.success && Array.isArray(columns)) {
|
||||
columns.forEach((col: any) => {
|
||||
const colName = col.columnName || col.column_name;
|
||||
const colLabel = col.displayName || col.columnComment || col.column_comment || colName;
|
||||
if (!fields.some(f => f.name === colName)) {
|
||||
fields.push({
|
||||
name: colName,
|
||||
label: colLabel,
|
||||
sourceComponent: "현재 폼 테이블",
|
||||
sourceTable: currentTableName,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`현재 테이블 컬럼 로드 실패 (${currentTableName}):`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// 중복 제거 (같은 name이면 첫 번째만 유지)
|
||||
const uniqueFields = fields.filter((field, index, self) =>
|
||||
index === self.findIndex(f => f.name === field.name)
|
||||
);
|
||||
|
||||
setAvailableParentFields(uniqueFields);
|
||||
};
|
||||
|
||||
extractParentFields();
|
||||
}, [allComponents, config.saveConfig?.tableName]);
|
||||
|
||||
// 저장 테이블 변경 시 컬럼 로드
|
||||
useEffect(() => {
|
||||
if (config.saveConfig.tableName) {
|
||||
|
|
@ -85,9 +276,10 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor
|
|||
const data = response.data?.data;
|
||||
if (response.data?.success && Array.isArray(data)) {
|
||||
setTables(
|
||||
data.map((t: { tableName?: string; table_name?: string; tableLabel?: string; table_label?: string }) => ({
|
||||
data.map((t: { tableName?: string; table_name?: string; displayName?: string; tableLabel?: string; table_label?: string }) => ({
|
||||
name: t.tableName || t.table_name || "",
|
||||
label: t.tableLabel || t.table_label || t.tableName || t.table_name || "",
|
||||
// displayName 우선, 없으면 tableLabel, 그것도 없으면 테이블명
|
||||
label: t.displayName || t.tableLabel || t.table_label || "",
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
|
@ -620,6 +812,12 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor
|
|||
setSelectedField(field);
|
||||
setFieldDetailModalOpen(true);
|
||||
}}
|
||||
tableName={config.saveConfig.tableName}
|
||||
tableColumns={tableColumns[config.saveConfig.tableName || ""]?.map(col => ({
|
||||
name: col.name,
|
||||
type: col.type,
|
||||
label: col.label || col.name
|
||||
})) || []}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -666,6 +864,7 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor
|
|||
tableColumns={tableColumns}
|
||||
numberingRules={numberingRules}
|
||||
onLoadTableColumns={loadTableColumns}
|
||||
availableParentFields={availableParentFields}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -706,6 +905,7 @@ export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFor
|
|||
)}
|
||||
onLoadTableColumns={loadTableColumns}
|
||||
allSections={config.sections as FormSectionConfig[]}
|
||||
availableParentFields={availableParentFields}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -36,6 +36,17 @@ const HelpText = ({ children }: { children: React.ReactNode }) => (
|
|||
<p className="text-[10px] text-muted-foreground mt-0.5">{children}</p>
|
||||
);
|
||||
|
||||
/**
|
||||
* 부모 화면에서 전달 가능한 필드 타입
|
||||
* 유니버셜 폼 모달에서 "부모에서 값 받기" 설정 시 선택 가능한 필드 목록
|
||||
*/
|
||||
export interface AvailableParentField {
|
||||
name: string; // 필드명 (columnName)
|
||||
label: string; // 표시 라벨
|
||||
sourceComponent?: string; // 출처 컴포넌트 (예: "TableList", "SplitPanelLayout2")
|
||||
sourceTable?: string; // 출처 테이블명
|
||||
}
|
||||
|
||||
interface FieldDetailSettingsModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
|
|
@ -45,6 +56,8 @@ interface FieldDetailSettingsModalProps {
|
|||
tableColumns: { [tableName: string]: { name: string; type: string; label: string }[] };
|
||||
numberingRules: { id: string; name: string }[];
|
||||
onLoadTableColumns: (tableName: string) => void;
|
||||
// 부모 화면에서 전달 가능한 필드 목록 (선택사항)
|
||||
availableParentFields?: AvailableParentField[];
|
||||
}
|
||||
|
||||
export function FieldDetailSettingsModal({
|
||||
|
|
@ -56,6 +69,7 @@ export function FieldDetailSettingsModal({
|
|||
tableColumns,
|
||||
numberingRules,
|
||||
onLoadTableColumns,
|
||||
availableParentFields = [],
|
||||
}: FieldDetailSettingsModalProps) {
|
||||
// 로컬 상태로 필드 설정 관리
|
||||
const [localField, setLocalField] = useState<FormFieldConfig>(field);
|
||||
|
|
@ -293,6 +307,49 @@ export function FieldDetailSettingsModal({
|
|||
/>
|
||||
</div>
|
||||
<HelpText>부모 화면에서 전달받은 값으로 자동 채워집니다</HelpText>
|
||||
|
||||
{/* 부모에서 값 받기 활성화 시 필드 선택 */}
|
||||
{localField.receiveFromParent && (
|
||||
<div className="mt-3 space-y-2 p-3 rounded-md bg-blue-50 border border-blue-200">
|
||||
<Label className="text-xs font-medium text-blue-700">부모 필드명 선택</Label>
|
||||
{availableParentFields.length > 0 ? (
|
||||
<Select
|
||||
value={localField.parentFieldName || localField.columnName}
|
||||
onValueChange={(value) => updateField({ parentFieldName: value })}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue placeholder="필드 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableParentFields.map((pf) => (
|
||||
<SelectItem key={pf.name} value={pf.name}>
|
||||
<div className="flex flex-col">
|
||||
<span>{pf.label || pf.name}</span>
|
||||
{pf.sourceComponent && (
|
||||
<span className="text-[9px] text-muted-foreground">
|
||||
{pf.sourceComponent}{pf.sourceTable && ` (${pf.sourceTable})`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
value={localField.parentFieldName || ""}
|
||||
onChange={(e) => updateField({ parentFieldName: e.target.value })}
|
||||
placeholder={`예: ${localField.columnName || "parent_field_name"}`}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
부모 화면에서 전달받을 필드명을 입력하세요. 비워두면 "{localField.columnName}"을 사용합니다.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Accordion으로 고급 설정 */}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Di
|
|||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Plus, Trash2, Database, Layers, Info } from "lucide-react";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||
import { Plus, Trash2, Database, Layers, Info, Check, ChevronsUpDown } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SaveConfig, SubTableSaveConfig, SubTableFieldMapping, FormSectionConfig, FormFieldConfig, SectionSaveMode } from "../types";
|
||||
|
|
@ -50,6 +52,11 @@ export function SaveSettingsModal({
|
|||
saveConfig.customApiSave?.enabled && saveConfig.customApiSave?.multiTable?.enabled ? "multi" : "single"
|
||||
);
|
||||
|
||||
// 테이블 검색 Popover 상태
|
||||
const [singleTableSearchOpen, setSingleTableSearchOpen] = useState(false);
|
||||
const [mainTableSearchOpen, setMainTableSearchOpen] = useState(false);
|
||||
const [subTableSearchOpen, setSubTableSearchOpen] = useState<Record<number, boolean>>({});
|
||||
|
||||
// open이 변경될 때마다 데이터 동기화
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
|
|
@ -376,24 +383,68 @@ export function SaveSettingsModal({
|
|||
|
||||
<div>
|
||||
<Label className="text-[10px]">저장 테이블</Label>
|
||||
<Select
|
||||
value={localSaveConfig.tableName || ""}
|
||||
onValueChange={(value) => {
|
||||
updateSaveConfig({ tableName: value });
|
||||
onLoadTableColumns(value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs mt-1">
|
||||
<SelectValue placeholder="테이블 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{tables.map((t) => (
|
||||
<SelectItem key={t.name} value={t.name}>
|
||||
{t.label || t.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Popover open={singleTableSearchOpen} onOpenChange={setSingleTableSearchOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={singleTableSearchOpen}
|
||||
className="h-8 w-full justify-between text-xs mt-1 font-normal"
|
||||
>
|
||||
{localSaveConfig.tableName ? (
|
||||
<div className="flex flex-col items-start text-left">
|
||||
<span className="font-medium">{localSaveConfig.tableName}</span>
|
||||
{(() => {
|
||||
const tableLabel = tables.find(t => t.name === localSaveConfig.tableName)?.label;
|
||||
return tableLabel && tableLabel !== localSaveConfig.tableName ? (
|
||||
<span className="text-[10px] text-muted-foreground">{tableLabel}</span>
|
||||
) : null;
|
||||
})()}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-muted-foreground">테이블 선택...</span>
|
||||
)}
|
||||
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 w-[300px]" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="테이블 검색..." className="text-xs" />
|
||||
<CommandList className="max-h-[250px]">
|
||||
<CommandEmpty className="text-xs py-4 text-center">
|
||||
테이블을 찾을 수 없습니다.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{tables.map((t) => (
|
||||
<CommandItem
|
||||
key={t.name}
|
||||
value={`${t.name} ${t.label}`}
|
||||
onSelect={() => {
|
||||
updateSaveConfig({ tableName: t.name });
|
||||
onLoadTableColumns(t.name);
|
||||
setSingleTableSearchOpen(false);
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
localSaveConfig.tableName === t.name ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{t.name}</span>
|
||||
{t.label && (
|
||||
<span className="text-[10px] text-muted-foreground">{t.label}</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<HelpText>폼 데이터를 저장할 테이블을 선택하세요</HelpText>
|
||||
</div>
|
||||
|
||||
|
|
@ -426,37 +477,81 @@ export function SaveSettingsModal({
|
|||
|
||||
<div>
|
||||
<Label className="text-[10px]">메인 테이블명</Label>
|
||||
<Select
|
||||
value={localSaveConfig.customApiSave?.multiTable?.mainTable?.tableName || ""}
|
||||
onValueChange={(value) => {
|
||||
updateSaveConfig({
|
||||
customApiSave: {
|
||||
...localSaveConfig.customApiSave,
|
||||
apiType: "multi-table",
|
||||
multiTable: {
|
||||
...localSaveConfig.customApiSave?.multiTable,
|
||||
enabled: true,
|
||||
mainTable: {
|
||||
...localSaveConfig.customApiSave?.multiTable?.mainTable,
|
||||
tableName: value,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
onLoadTableColumns(value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs mt-1">
|
||||
<SelectValue placeholder="테이블 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{tables.map((t) => (
|
||||
<SelectItem key={t.name} value={t.name}>
|
||||
{t.label || t.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Popover open={mainTableSearchOpen} onOpenChange={setMainTableSearchOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={mainTableSearchOpen}
|
||||
className="h-8 w-full justify-between text-xs mt-1 font-normal"
|
||||
>
|
||||
{localSaveConfig.customApiSave?.multiTable?.mainTable?.tableName ? (
|
||||
<div className="flex flex-col items-start text-left">
|
||||
<span className="font-medium">{localSaveConfig.customApiSave.multiTable.mainTable.tableName}</span>
|
||||
{(() => {
|
||||
const tableLabel = tables.find(t => t.name === localSaveConfig.customApiSave?.multiTable?.mainTable?.tableName)?.label;
|
||||
return tableLabel && tableLabel !== localSaveConfig.customApiSave?.multiTable?.mainTable?.tableName ? (
|
||||
<span className="text-[10px] text-muted-foreground">{tableLabel}</span>
|
||||
) : null;
|
||||
})()}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-muted-foreground">테이블 선택...</span>
|
||||
)}
|
||||
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 w-[300px]" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="테이블 검색..." className="text-xs" />
|
||||
<CommandList className="max-h-[250px]">
|
||||
<CommandEmpty className="text-xs py-4 text-center">
|
||||
테이블을 찾을 수 없습니다.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{tables.map((t) => (
|
||||
<CommandItem
|
||||
key={t.name}
|
||||
value={`${t.name} ${t.label}`}
|
||||
onSelect={() => {
|
||||
updateSaveConfig({
|
||||
customApiSave: {
|
||||
...localSaveConfig.customApiSave,
|
||||
apiType: "multi-table",
|
||||
multiTable: {
|
||||
...localSaveConfig.customApiSave?.multiTable,
|
||||
enabled: true,
|
||||
mainTable: {
|
||||
...localSaveConfig.customApiSave?.multiTable?.mainTable,
|
||||
tableName: t.name,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
onLoadTableColumns(t.name);
|
||||
setMainTableSearchOpen(false);
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
localSaveConfig.customApiSave?.multiTable?.mainTable?.tableName === t.name ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{t.name}</span>
|
||||
{t.label && (
|
||||
<span className="text-[10px] text-muted-foreground">{t.label}</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<HelpText>주요 데이터를 저장할 메인 테이블 (예: orders, user_info)</HelpText>
|
||||
</div>
|
||||
|
||||
|
|
@ -576,24 +671,71 @@ export function SaveSettingsModal({
|
|||
<AccordionContent className="px-3 pb-3 space-y-3">
|
||||
<div>
|
||||
<Label className="text-[10px]">서브 테이블명</Label>
|
||||
<Select
|
||||
value={subTable.tableName || ""}
|
||||
onValueChange={(value) => {
|
||||
updateSubTable(subIndex, { tableName: value });
|
||||
onLoadTableColumns(value);
|
||||
}}
|
||||
<Popover
|
||||
open={subTableSearchOpen[subIndex] || false}
|
||||
onOpenChange={(open) => setSubTableSearchOpen(prev => ({ ...prev, [subIndex]: open }))}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs mt-1">
|
||||
<SelectValue placeholder="테이블 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{tables.map((t) => (
|
||||
<SelectItem key={t.name} value={t.name}>
|
||||
{t.label || t.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={subTableSearchOpen[subIndex] || false}
|
||||
className="h-8 w-full justify-between text-xs mt-1 font-normal"
|
||||
>
|
||||
{subTable.tableName ? (
|
||||
<div className="flex flex-col items-start text-left">
|
||||
<span className="font-medium">{subTable.tableName}</span>
|
||||
{(() => {
|
||||
const tableLabel = tables.find(t => t.name === subTable.tableName)?.label;
|
||||
return tableLabel && tableLabel !== subTable.tableName ? (
|
||||
<span className="text-[10px] text-muted-foreground">{tableLabel}</span>
|
||||
) : null;
|
||||
})()}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-muted-foreground">테이블 선택...</span>
|
||||
)}
|
||||
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 w-[280px]" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="테이블 검색..." className="text-xs" />
|
||||
<CommandList className="max-h-[200px]">
|
||||
<CommandEmpty className="text-xs py-4 text-center">
|
||||
테이블을 찾을 수 없습니다.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{tables.map((t) => (
|
||||
<CommandItem
|
||||
key={t.name}
|
||||
value={`${t.name} ${t.label}`}
|
||||
onSelect={() => {
|
||||
updateSubTable(subIndex, { tableName: t.name });
|
||||
onLoadTableColumns(t.name);
|
||||
setSubTableSearchOpen(prev => ({ ...prev, [subIndex]: false }));
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
subTable.tableName === t.name ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{t.name}</span>
|
||||
{t.label && (
|
||||
<span className="text-[10px] text-muted-foreground">{t.label}</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<HelpText>반복 데이터를 저장할 서브 테이블</HelpText>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Di
|
|||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Plus, Trash2, GripVertical, ChevronUp, ChevronDown, Settings as SettingsIcon } from "lucide-react";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||
import { Plus, Trash2, GripVertical, ChevronUp, ChevronDown, Settings as SettingsIcon, Check, ChevronsUpDown } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { FormSectionConfig, FormFieldConfig, OptionalFieldGroupConfig, FIELD_TYPE_OPTIONS } from "../types";
|
||||
import { defaultFieldConfig, generateFieldId, generateUniqueId } from "../config";
|
||||
|
|
@ -21,12 +23,22 @@ const HelpText = ({ children }: { children: React.ReactNode }) => (
|
|||
<p className="text-[10px] text-muted-foreground mt-0.5">{children}</p>
|
||||
);
|
||||
|
||||
// 테이블 컬럼 정보 타입
|
||||
interface TableColumnInfo {
|
||||
name: string;
|
||||
type: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface SectionLayoutModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
section: FormSectionConfig;
|
||||
onSave: (updates: Partial<FormSectionConfig>) => void;
|
||||
onOpenFieldDetail: (field: FormFieldConfig) => void;
|
||||
// 저장 테이블의 컬럼 정보
|
||||
tableName?: string;
|
||||
tableColumns?: TableColumnInfo[];
|
||||
}
|
||||
|
||||
export function SectionLayoutModal({
|
||||
|
|
@ -35,8 +47,13 @@ export function SectionLayoutModal({
|
|||
section,
|
||||
onSave,
|
||||
onOpenFieldDetail,
|
||||
tableName = "",
|
||||
tableColumns = [],
|
||||
}: SectionLayoutModalProps) {
|
||||
|
||||
// 컬럼 선택 Popover 상태 (필드별)
|
||||
const [columnSearchOpen, setColumnSearchOpen] = useState<Record<string, boolean>>({});
|
||||
|
||||
// 로컬 상태로 섹션 관리 (fields가 없으면 빈 배열로 초기화)
|
||||
const [localSection, setLocalSection] = useState<FormSectionConfig>(() => ({
|
||||
...section,
|
||||
|
|
@ -443,11 +460,90 @@ export function SectionLayoutModal({
|
|||
</div>
|
||||
<div>
|
||||
<Label className="text-[9px]">컬럼명</Label>
|
||||
<Input
|
||||
value={field.columnName}
|
||||
onChange={(e) => updateField(field.id, { columnName: e.target.value })}
|
||||
className="h-6 text-[9px] mt-0.5"
|
||||
/>
|
||||
{tableColumns.length > 0 ? (
|
||||
<Popover
|
||||
open={columnSearchOpen[field.id] || false}
|
||||
onOpenChange={(open) => setColumnSearchOpen(prev => ({ ...prev, [field.id]: open }))}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={columnSearchOpen[field.id] || false}
|
||||
className="h-6 w-full justify-between text-[9px] mt-0.5 font-normal"
|
||||
>
|
||||
{field.columnName ? (
|
||||
<div className="flex flex-col items-start text-left truncate">
|
||||
<span className="font-medium truncate">{field.columnName}</span>
|
||||
{(() => {
|
||||
const col = tableColumns.find(c => c.name === field.columnName);
|
||||
return col?.label && col.label !== field.columnName ? (
|
||||
<span className="text-[8px] text-muted-foreground truncate">({col.label})</span>
|
||||
) : null;
|
||||
})()}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-muted-foreground">컬럼 선택...</span>
|
||||
)}
|
||||
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 w-[280px]" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
|
||||
<CommandList className="max-h-[200px]">
|
||||
<CommandEmpty className="text-xs py-3 text-center">
|
||||
컬럼을 찾을 수 없습니다.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{tableColumns.map((col) => (
|
||||
<CommandItem
|
||||
key={col.name}
|
||||
value={`${col.name} ${col.label}`}
|
||||
onSelect={() => {
|
||||
updateField(field.id, {
|
||||
columnName: col.name,
|
||||
// 라벨이 기본값이면 컬럼 라벨로 자동 설정
|
||||
...(field.label.startsWith("새 필드") || field.label.startsWith("field_")
|
||||
? { label: col.label || col.name }
|
||||
: {})
|
||||
});
|
||||
setColumnSearchOpen(prev => ({ ...prev, [field.id]: false }));
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
field.columnName === col.name ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-medium">{col.name}</span>
|
||||
{col.label && col.label !== col.name && (
|
||||
<span className="text-muted-foreground">({col.label})</span>
|
||||
)}
|
||||
</div>
|
||||
{tableName && (
|
||||
<span className="text-[9px] text-muted-foreground">{tableName}</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<Input
|
||||
value={field.columnName}
|
||||
onChange={(e) => updateField(field.id, { columnName: e.target.value })}
|
||||
className="h-6 text-[9px] mt-0.5"
|
||||
placeholder="저장 테이블을 먼저 설정하세요"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -821,24 +917,106 @@ export function SectionLayoutModal({
|
|||
className="h-5 text-[8px]"
|
||||
placeholder="라벨"
|
||||
/>
|
||||
<Input
|
||||
value={field.columnName}
|
||||
onChange={(e) => {
|
||||
const newGroups = localSection.optionalFieldGroups?.map((g) =>
|
||||
g.id === group.id
|
||||
? {
|
||||
...g,
|
||||
fields: g.fields.map((f) =>
|
||||
f.id === field.id ? { ...f, columnName: e.target.value } : f
|
||||
),
|
||||
}
|
||||
: g
|
||||
);
|
||||
updateSection({ optionalFieldGroups: newGroups });
|
||||
}}
|
||||
className="h-5 text-[8px]"
|
||||
placeholder="컬럼명"
|
||||
/>
|
||||
{tableColumns.length > 0 ? (
|
||||
<Popover
|
||||
open={columnSearchOpen[`opt-${field.id}`] || false}
|
||||
onOpenChange={(open) => setColumnSearchOpen(prev => ({ ...prev, [`opt-${field.id}`]: open }))}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className="h-6 w-full justify-between text-[8px] font-normal px-1"
|
||||
>
|
||||
{field.columnName ? (
|
||||
<div className="flex flex-col items-start text-left truncate">
|
||||
<span className="font-medium truncate">{field.columnName}</span>
|
||||
{(() => {
|
||||
const col = tableColumns.find(c => c.name === field.columnName);
|
||||
return col?.label && col.label !== field.columnName ? (
|
||||
<span className="text-[7px] text-muted-foreground truncate">({col.label})</span>
|
||||
) : null;
|
||||
})()}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-muted-foreground">컬럼...</span>
|
||||
)}
|
||||
<ChevronsUpDown className="h-2.5 w-2.5 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 w-[250px]" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
|
||||
<CommandList className="max-h-[180px]">
|
||||
<CommandEmpty className="text-xs py-2 text-center">
|
||||
컬럼을 찾을 수 없습니다.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{tableColumns.map((col) => (
|
||||
<CommandItem
|
||||
key={col.name}
|
||||
value={`${col.name} ${col.label}`}
|
||||
onSelect={() => {
|
||||
const newGroups = localSection.optionalFieldGroups?.map((g) =>
|
||||
g.id === group.id
|
||||
? {
|
||||
...g,
|
||||
fields: g.fields.map((f) =>
|
||||
f.id === field.id
|
||||
? {
|
||||
...f,
|
||||
columnName: col.name,
|
||||
...(f.label.startsWith("필드 ") ? { label: col.label || col.name } : {})
|
||||
}
|
||||
: f
|
||||
),
|
||||
}
|
||||
: g
|
||||
);
|
||||
updateSection({ optionalFieldGroups: newGroups });
|
||||
setColumnSearchOpen(prev => ({ ...prev, [`opt-${field.id}`]: false }));
|
||||
}}
|
||||
className="text-[9px]"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-1 h-2.5 w-2.5",
|
||||
field.columnName === col.name ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{col.name}</span>
|
||||
{col.label && col.label !== col.name && (
|
||||
<span className="text-[8px] text-muted-foreground">({col.label})</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<Input
|
||||
value={field.columnName}
|
||||
onChange={(e) => {
|
||||
const newGroups = localSection.optionalFieldGroups?.map((g) =>
|
||||
g.id === group.id
|
||||
? {
|
||||
...g,
|
||||
fields: g.fields.map((f) =>
|
||||
f.id === field.id ? { ...f, columnName: e.target.value } : f
|
||||
),
|
||||
}
|
||||
: g
|
||||
);
|
||||
updateSection({ optionalFieldGroups: newGroups });
|
||||
}}
|
||||
className="h-5 text-[8px]"
|
||||
placeholder="컬럼명"
|
||||
/>
|
||||
)}
|
||||
<Select
|
||||
value={field.fieldType}
|
||||
onValueChange={(value) => {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,14 @@ const HelpText = ({ children }: { children: React.ReactNode }) => (
|
|||
<p className="text-[10px] text-muted-foreground mt-0.5">{children}</p>
|
||||
);
|
||||
|
||||
// 부모 화면에서 전달 가능한 필드 타입
|
||||
interface AvailableParentField {
|
||||
name: string; // 필드명 (columnName)
|
||||
label: string; // 표시 라벨
|
||||
sourceComponent?: string; // 출처 컴포넌트
|
||||
sourceTable?: string; // 출처 테이블명
|
||||
}
|
||||
|
||||
// 컬럼 설정 아이템 컴포넌트
|
||||
interface ColumnSettingItemProps {
|
||||
col: TableColumnConfig;
|
||||
|
|
@ -62,6 +70,7 @@ interface ColumnSettingItemProps {
|
|||
sections: { id: string; title: string }[]; // 섹션 목록
|
||||
formFields: { columnName: string; label: string; sectionId?: string }[]; // 폼 필드 목록
|
||||
tableConfig: TableSectionConfig; // 현재 행 필드 목록 표시용
|
||||
availableParentFields?: AvailableParentField[]; // 부모 화면에서 전달 가능한 필드 목록
|
||||
onLoadTableColumns: (tableName: string) => void;
|
||||
onUpdate: (updates: Partial<TableColumnConfig>) => void;
|
||||
onMoveUp: () => void;
|
||||
|
|
@ -82,6 +91,7 @@ function ColumnSettingItem({
|
|||
sections,
|
||||
formFields,
|
||||
tableConfig,
|
||||
availableParentFields = [],
|
||||
onLoadTableColumns,
|
||||
onUpdate,
|
||||
onMoveUp,
|
||||
|
|
@ -90,6 +100,7 @@ function ColumnSettingItem({
|
|||
}: ColumnSettingItemProps) {
|
||||
const [fieldSearchOpen, setFieldSearchOpen] = useState(false);
|
||||
const [sourceFieldSearchOpen, setSourceFieldSearchOpen] = useState(false);
|
||||
const [parentFieldSearchOpen, setParentFieldSearchOpen] = useState(false);
|
||||
const [lookupTableOpenMap, setLookupTableOpenMap] = useState<Record<string, boolean>>({});
|
||||
|
||||
// 조회 옵션 추가
|
||||
|
|
@ -402,6 +413,14 @@ function ColumnSettingItem({
|
|||
/>
|
||||
<span>필수</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-xs cursor-pointer" title="부모 화면에서 전달받은 값을 모든 행에 적용">
|
||||
<Switch
|
||||
checked={col.receiveFromParent ?? false}
|
||||
onCheckedChange={(checked) => onUpdate({ receiveFromParent: checked })}
|
||||
className="scale-75"
|
||||
/>
|
||||
<span className="text-blue-600">부모값</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-xs cursor-pointer">
|
||||
<Switch
|
||||
checked={col.lookup?.enabled ?? false}
|
||||
|
|
@ -432,6 +451,103 @@ function ColumnSettingItem({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* 부모에서 값 받기 설정 (부모값 ON일 때만 표시) */}
|
||||
{col.receiveFromParent && (
|
||||
<div className="border-t pt-3 mt-3 space-y-2">
|
||||
<Label className="text-xs font-medium text-blue-600">부모 필드 선택</Label>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
부모 화면에서 전달받을 필드를 선택하세요. 모든 행에 동일한 값이 적용됩니다.
|
||||
</p>
|
||||
{availableParentFields.length > 0 ? (
|
||||
<Popover open={parentFieldSearchOpen} onOpenChange={setParentFieldSearchOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={parentFieldSearchOpen}
|
||||
className="h-8 w-full justify-between text-xs"
|
||||
>
|
||||
<span className="truncate">
|
||||
{col.parentFieldName
|
||||
? availableParentFields.find(f => f.name === col.parentFieldName)?.label || col.parentFieldName
|
||||
: `(기본: ${col.field})`}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 w-[300px]" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="부모 필드 검색..." className="text-xs" />
|
||||
<CommandList className="max-h-[250px]">
|
||||
<CommandEmpty className="text-xs py-4 text-center">
|
||||
사용 가능한 부모 필드가 없습니다.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{/* 기본값 (필드명과 동일) */}
|
||||
<CommandItem
|
||||
value="__same_as_field__"
|
||||
onSelect={() => {
|
||||
onUpdate({ parentFieldName: undefined });
|
||||
setParentFieldSearchOpen(false);
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
!col.parentFieldName ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<span className="text-muted-foreground">(기본: {col.field})</span>
|
||||
</CommandItem>
|
||||
{/* 부모 필드 목록 */}
|
||||
{availableParentFields.map((pf) => (
|
||||
<CommandItem
|
||||
key={pf.name}
|
||||
value={`${pf.name} ${pf.label}`}
|
||||
onSelect={() => {
|
||||
onUpdate({ parentFieldName: pf.name });
|
||||
setParentFieldSearchOpen(false);
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
col.parentFieldName === pf.name ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col flex-1 min-w-0">
|
||||
<span className="font-medium truncate">{pf.label || pf.name}</span>
|
||||
{pf.sourceComponent && (
|
||||
<span className="text-[10px] text-muted-foreground truncate">
|
||||
{pf.sourceComponent}{pf.sourceTable && ` (${pf.sourceTable})`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
value={col.parentFieldName || ""}
|
||||
onChange={(e) => onUpdate({ parentFieldName: e.target.value })}
|
||||
placeholder={col.field}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
비워두면 "{col.field}"를 사용합니다.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 조회 설정 (조회 ON일 때만 표시) */}
|
||||
{col.lookup?.enabled && (
|
||||
<div className="border-t pt-3 mt-3 space-y-3">
|
||||
|
|
@ -1119,6 +1235,8 @@ interface TableSectionSettingsModalProps {
|
|||
onLoadCategoryList?: () => void;
|
||||
// 전체 섹션 목록 (다른 섹션 필드 참조용)
|
||||
allSections?: FormSectionConfig[];
|
||||
// 부모 화면에서 전달 가능한 필드 목록
|
||||
availableParentFields?: AvailableParentField[];
|
||||
}
|
||||
|
||||
export function TableSectionSettingsModal({
|
||||
|
|
@ -1132,6 +1250,7 @@ export function TableSectionSettingsModal({
|
|||
categoryList = [],
|
||||
onLoadCategoryList,
|
||||
allSections = [],
|
||||
availableParentFields = [],
|
||||
}: TableSectionSettingsModalProps) {
|
||||
// 로컬 상태
|
||||
const [title, setTitle] = useState(section.title);
|
||||
|
|
@ -1693,6 +1812,7 @@ export function TableSectionSettingsModal({
|
|||
sections={otherSections}
|
||||
formFields={otherSectionFields}
|
||||
tableConfig={tableConfig}
|
||||
availableParentFields={availableParentFields}
|
||||
onLoadTableColumns={onLoadTableColumns}
|
||||
onUpdate={(updates) => updateColumn(index, updates)}
|
||||
onMoveUp={() => moveColumn(index, "up")}
|
||||
|
|
|
|||
|
|
@ -335,6 +335,10 @@ export interface TableColumnConfig {
|
|||
// 날짜 일괄 적용 (type이 "date"일 때만 사용)
|
||||
// 활성화 시 첫 번째 날짜 입력 시 모든 행에 동일한 날짜가 자동 적용됨
|
||||
batchApply?: boolean;
|
||||
|
||||
// 부모에서 값 받기 (모든 행에 동일한 값 적용)
|
||||
receiveFromParent?: boolean; // 부모에서 값 받기 활성화
|
||||
parentFieldName?: string; // 부모 필드명 (미지정 시 field와 동일)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
|
|
@ -705,6 +709,8 @@ export interface UniversalFormModalComponentProps {
|
|||
export interface UniversalFormModalConfigPanelProps {
|
||||
config: UniversalFormModalConfig;
|
||||
onChange: (config: UniversalFormModalConfig) => void;
|
||||
// 화면 설계 시 같은 화면의 다른 컴포넌트들 (부모 데이터 필드 추출용)
|
||||
allComponents?: any[];
|
||||
}
|
||||
|
||||
// 필드 타입 옵션
|
||||
|
|
|
|||
Loading…
Reference in New Issue