2025-11-17 12:23:45 +09:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import React, { useState, useMemo } from "react";
|
|
|
|
|
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
|
|
|
import { Card, CardContent } from "@/components/ui/card";
|
2025-11-18 10:25:28 +09:00
|
|
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
|
|
|
|
import { Plus, X, ChevronDown, ChevronRight } from "lucide-react";
|
feat: 항목 표시 설정 기능 추가 (기본값, 빈 값 처리 포함)
- DisplayItem 타입 추가 (icon, field, text, badge)
- 필드별 표시 형식 지원 (text, currency, number, date, badge)
- 빈 값 처리 옵션 추가 (hide, default, blank)
- 기본값 설정 기능 추가
- 스타일 옵션 추가 (굵게, 밑줄, 기울임, 색상)
- renderDisplayItems 헬퍼 함수로 유연한 표시 렌더링
- SelectedItemsDetailInputConfigPanel에 항목 표시 설정 UI 추가
- displayItems가 없으면 기존 방식(모든 필드 나열)으로 폴백
2025-11-18 10:14:31 +09:00
|
|
|
import { SelectedItemsDetailInputConfig, AdditionalFieldDefinition, FieldGroup, DisplayItem, DisplayItemType, EmptyBehavior, DisplayFieldFormat } from "./types";
|
2025-11-17 12:23:45 +09:00
|
|
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@/components/ui/command";
|
|
|
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
|
|
|
import { Check, ChevronsUpDown } from "lucide-react";
|
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
|
|
|
|
|
|
export interface SelectedItemsDetailInputConfigPanelProps {
|
|
|
|
|
config: SelectedItemsDetailInputConfig;
|
|
|
|
|
onChange: (config: Partial<SelectedItemsDetailInputConfig>) => void;
|
2025-11-17 15:25:08 +09:00
|
|
|
sourceTableColumns?: Array<{ columnName: string; columnLabel?: string; dataType?: string }>; // 🆕 원본 테이블 컬럼
|
|
|
|
|
targetTableColumns?: Array<{ columnName: string; columnLabel?: string; dataType?: string }>; // 🆕 대상 테이블 컬럼
|
2025-11-17 12:23:45 +09:00
|
|
|
allTables?: Array<{ tableName: string; displayName?: string }>;
|
2025-11-17 15:25:08 +09:00
|
|
|
screenTableName?: string; // 🆕 현재 화면의 테이블명 (자동 설정용)
|
|
|
|
|
onSourceTableChange?: (tableName: string) => void; // 🆕 원본 테이블 변경 콜백
|
|
|
|
|
onTargetTableChange?: (tableName: string) => void; // 🆕 대상 테이블 변경 콜백 (기존 onTableChange 대체)
|
2025-11-17 12:23:45 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* SelectedItemsDetailInput 설정 패널
|
|
|
|
|
* 컴포넌트의 설정값들을 편집할 수 있는 UI 제공
|
|
|
|
|
*/
|
|
|
|
|
export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailInputConfigPanelProps> = ({
|
|
|
|
|
config,
|
|
|
|
|
onChange,
|
2025-11-17 15:25:08 +09:00
|
|
|
sourceTableColumns = [], // 🆕 원본 테이블 컬럼
|
|
|
|
|
targetTableColumns = [], // 🆕 대상 테이블 컬럼
|
2025-11-17 12:23:45 +09:00
|
|
|
allTables = [],
|
2025-11-17 15:25:08 +09:00
|
|
|
screenTableName, // 🆕 현재 화면의 테이블명
|
|
|
|
|
onSourceTableChange, // 🆕 원본 테이블 변경 콜백
|
|
|
|
|
onTargetTableChange, // 🆕 대상 테이블 변경 콜백
|
2025-11-17 12:23:45 +09:00
|
|
|
}) => {
|
|
|
|
|
const [localFields, setLocalFields] = useState<AdditionalFieldDefinition[]>(config.additionalFields || []);
|
2025-11-17 15:25:08 +09:00
|
|
|
const [displayColumns, setDisplayColumns] = useState<Array<{ name: string; label: string; width?: string }>>(config.displayColumns || []);
|
2025-11-17 12:23:45 +09:00
|
|
|
const [fieldPopoverOpen, setFieldPopoverOpen] = useState<Record<number, boolean>>({});
|
2025-11-17 15:25:08 +09:00
|
|
|
|
2025-11-18 09:56:49 +09:00
|
|
|
// 🆕 필드 그룹 상태
|
|
|
|
|
const [localFieldGroups, setLocalFieldGroups] = useState<FieldGroup[]>(config.fieldGroups || []);
|
|
|
|
|
|
2025-11-18 10:25:28 +09:00
|
|
|
// 🆕 그룹별 펼침/접힘 상태
|
|
|
|
|
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
|
|
|
|
|
|
|
|
|
|
// 🆕 그룹별 표시 항목 설정 펼침/접힘 상태
|
|
|
|
|
const [expandedDisplayItems, setExpandedDisplayItems] = useState<Record<string, boolean>>({});
|
|
|
|
|
|
2025-11-17 15:25:08 +09:00
|
|
|
// 🆕 원본 테이블 선택 상태
|
|
|
|
|
const [sourceTableSelectOpen, setSourceTableSelectOpen] = useState(false);
|
|
|
|
|
const [sourceTableSearchValue, setSourceTableSearchValue] = useState("");
|
|
|
|
|
|
|
|
|
|
// 🆕 대상 테이블 선택 상태 (기존 tableSelectOpen)
|
|
|
|
|
const [tableSelectOpen, setTableSelectOpen] = useState(false);
|
|
|
|
|
const [tableSearchValue, setTableSearchValue] = useState("");
|
|
|
|
|
|
|
|
|
|
// 🆕 초기 로드 시 screenTableName을 targetTable로 자동 설정
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (screenTableName && !config.targetTable) {
|
|
|
|
|
console.log("✨ 현재 화면 테이블을 저장 대상 테이블로 자동 설정:", screenTableName);
|
|
|
|
|
handleChange("targetTable", screenTableName);
|
|
|
|
|
|
|
|
|
|
// 컬럼도 자동 로드
|
|
|
|
|
if (onTargetTableChange) {
|
|
|
|
|
onTargetTableChange(screenTableName);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [screenTableName]); // config.targetTable은 의존성에서 제외 (한 번만 실행)
|
2025-11-17 12:23:45 +09:00
|
|
|
|
|
|
|
|
const handleChange = (key: keyof SelectedItemsDetailInputConfig, value: any) => {
|
|
|
|
|
onChange({ [key]: value });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleFieldsChange = (fields: AdditionalFieldDefinition[]) => {
|
|
|
|
|
setLocalFields(fields);
|
|
|
|
|
handleChange("additionalFields", fields);
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-17 15:25:08 +09:00
|
|
|
const handleDisplayColumnsChange = (columns: Array<{ name: string; label: string; width?: string }>) => {
|
2025-11-17 12:23:45 +09:00
|
|
|
setDisplayColumns(columns);
|
|
|
|
|
handleChange("displayColumns", columns);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 필드 추가
|
|
|
|
|
const addField = () => {
|
|
|
|
|
const newField: AdditionalFieldDefinition = {
|
|
|
|
|
name: `field_${localFields.length + 1}`,
|
|
|
|
|
label: `필드 ${localFields.length + 1}`,
|
|
|
|
|
type: "text",
|
|
|
|
|
};
|
|
|
|
|
handleFieldsChange([...localFields, newField]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 필드 제거
|
|
|
|
|
const removeField = (index: number) => {
|
|
|
|
|
handleFieldsChange(localFields.filter((_, i) => i !== index));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 필드 수정
|
|
|
|
|
const updateField = (index: number, updates: Partial<AdditionalFieldDefinition>) => {
|
|
|
|
|
const newFields = [...localFields];
|
|
|
|
|
newFields[index] = { ...newFields[index], ...updates };
|
|
|
|
|
handleFieldsChange(newFields);
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-18 09:56:49 +09:00
|
|
|
// 🆕 필드 그룹 관리
|
|
|
|
|
const handleFieldGroupsChange = (groups: FieldGroup[]) => {
|
|
|
|
|
setLocalFieldGroups(groups);
|
|
|
|
|
handleChange("fieldGroups", groups);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const addFieldGroup = () => {
|
|
|
|
|
const newGroup: FieldGroup = {
|
|
|
|
|
id: `group_${localFieldGroups.length + 1}`,
|
|
|
|
|
title: `그룹 ${localFieldGroups.length + 1}`,
|
|
|
|
|
order: localFieldGroups.length,
|
|
|
|
|
};
|
|
|
|
|
handleFieldGroupsChange([...localFieldGroups, newGroup]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const removeFieldGroup = (groupId: string) => {
|
|
|
|
|
// 그룹 삭제 시 해당 그룹에 속한 필드들의 groupId도 제거
|
|
|
|
|
const updatedFields = localFields.map(field =>
|
|
|
|
|
field.groupId === groupId ? { ...field, groupId: undefined } : field
|
|
|
|
|
);
|
|
|
|
|
setLocalFields(updatedFields);
|
|
|
|
|
handleChange("additionalFields", updatedFields);
|
|
|
|
|
handleFieldGroupsChange(localFieldGroups.filter(g => g.id !== groupId));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const updateFieldGroup = (groupId: string, updates: Partial<FieldGroup>) => {
|
|
|
|
|
const newGroups = localFieldGroups.map(g =>
|
|
|
|
|
g.id === groupId ? { ...g, ...updates } : g
|
|
|
|
|
);
|
|
|
|
|
handleFieldGroupsChange(newGroups);
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-17 12:23:45 +09:00
|
|
|
// 표시 컬럼 추가
|
2025-11-17 15:25:08 +09:00
|
|
|
const addDisplayColumn = (columnName: string, columnLabel: string) => {
|
|
|
|
|
if (!displayColumns.some(col => col.name === columnName)) {
|
|
|
|
|
handleDisplayColumnsChange([...displayColumns, { name: columnName, label: columnLabel }]);
|
2025-11-17 12:23:45 +09:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 표시 컬럼 제거
|
|
|
|
|
const removeDisplayColumn = (columnName: string) => {
|
2025-11-17 15:25:08 +09:00
|
|
|
handleDisplayColumnsChange(displayColumns.filter((col) => col.name !== columnName));
|
2025-11-17 12:23:45 +09:00
|
|
|
};
|
|
|
|
|
|
2025-11-17 15:25:08 +09:00
|
|
|
// 🆕 표시 컬럼용: 원본 테이블에서 사용되지 않은 컬럼 목록
|
2025-11-17 12:23:45 +09:00
|
|
|
const availableColumns = useMemo(() => {
|
2025-11-17 15:25:08 +09:00
|
|
|
const usedColumns = new Set([...displayColumns.map(c => c.name), ...localFields.map((f) => f.name)]);
|
|
|
|
|
return sourceTableColumns.filter((col) => !usedColumns.has(col.columnName));
|
|
|
|
|
}, [sourceTableColumns, displayColumns, localFields]);
|
2025-11-17 12:23:45 +09:00
|
|
|
|
2025-11-17 15:25:08 +09:00
|
|
|
// 🆕 추가 입력 필드용: 대상 테이블에서 사용되지 않은 컬럼 목록
|
|
|
|
|
const availableTargetColumns = useMemo(() => {
|
|
|
|
|
const usedColumns = new Set([...displayColumns.map(c => c.name), ...localFields.map((f) => f.name)]);
|
|
|
|
|
return targetTableColumns.filter((col) => !usedColumns.has(col.columnName));
|
|
|
|
|
}, [targetTableColumns, displayColumns, localFields]);
|
2025-11-17 12:23:45 +09:00
|
|
|
|
2025-11-17 15:25:08 +09:00
|
|
|
// 🆕 원본 테이블 필터링
|
|
|
|
|
const filteredSourceTables = useMemo(() => {
|
|
|
|
|
if (!sourceTableSearchValue) return allTables;
|
|
|
|
|
const searchLower = sourceTableSearchValue.toLowerCase();
|
|
|
|
|
return allTables.filter(
|
|
|
|
|
(table) =>
|
|
|
|
|
table.tableName.toLowerCase().includes(searchLower) || table.displayName?.toLowerCase().includes(searchLower),
|
|
|
|
|
);
|
|
|
|
|
}, [allTables, sourceTableSearchValue]);
|
|
|
|
|
|
2025-11-18 10:21:36 +09:00
|
|
|
// 🆕 그룹별 항목 표시 설정 핸들러
|
|
|
|
|
const addDisplayItemToGroup = (groupId: string, type: DisplayItemType) => {
|
feat: 항목 표시 설정 기능 추가 (기본값, 빈 값 처리 포함)
- DisplayItem 타입 추가 (icon, field, text, badge)
- 필드별 표시 형식 지원 (text, currency, number, date, badge)
- 빈 값 처리 옵션 추가 (hide, default, blank)
- 기본값 설정 기능 추가
- 스타일 옵션 추가 (굵게, 밑줄, 기울임, 색상)
- renderDisplayItems 헬퍼 함수로 유연한 표시 렌더링
- SelectedItemsDetailInputConfigPanel에 항목 표시 설정 UI 추가
- displayItems가 없으면 기존 방식(모든 필드 나열)으로 폴백
2025-11-18 10:14:31 +09:00
|
|
|
const newItem: DisplayItem = {
|
|
|
|
|
type,
|
|
|
|
|
id: `display-${Date.now()}`,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (type === "field") {
|
2025-11-18 10:21:36 +09:00
|
|
|
// 해당 그룹의 필드만 선택 가능하도록
|
|
|
|
|
const groupFields = localFields.filter(f => f.groupId === groupId);
|
|
|
|
|
newItem.fieldName = groupFields[0]?.name || "";
|
feat: 항목 표시 설정 기능 추가 (기본값, 빈 값 처리 포함)
- DisplayItem 타입 추가 (icon, field, text, badge)
- 필드별 표시 형식 지원 (text, currency, number, date, badge)
- 빈 값 처리 옵션 추가 (hide, default, blank)
- 기본값 설정 기능 추가
- 스타일 옵션 추가 (굵게, 밑줄, 기울임, 색상)
- renderDisplayItems 헬퍼 함수로 유연한 표시 렌더링
- SelectedItemsDetailInputConfigPanel에 항목 표시 설정 UI 추가
- displayItems가 없으면 기존 방식(모든 필드 나열)으로 폴백
2025-11-18 10:14:31 +09:00
|
|
|
newItem.format = "text";
|
|
|
|
|
newItem.emptyBehavior = "default";
|
|
|
|
|
} else if (type === "icon") {
|
|
|
|
|
newItem.icon = "Circle";
|
|
|
|
|
} else if (type === "text") {
|
|
|
|
|
newItem.value = "텍스트";
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-18 10:21:36 +09:00
|
|
|
const updatedGroups = localFieldGroups.map(g => {
|
|
|
|
|
if (g.id === groupId) {
|
|
|
|
|
return {
|
|
|
|
|
...g,
|
|
|
|
|
displayItems: [...(g.displayItems || []), newItem]
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return g;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setLocalFieldGroups(updatedGroups);
|
|
|
|
|
handleChange("fieldGroups", updatedGroups);
|
feat: 항목 표시 설정 기능 추가 (기본값, 빈 값 처리 포함)
- DisplayItem 타입 추가 (icon, field, text, badge)
- 필드별 표시 형식 지원 (text, currency, number, date, badge)
- 빈 값 처리 옵션 추가 (hide, default, blank)
- 기본값 설정 기능 추가
- 스타일 옵션 추가 (굵게, 밑줄, 기울임, 색상)
- renderDisplayItems 헬퍼 함수로 유연한 표시 렌더링
- SelectedItemsDetailInputConfigPanel에 항목 표시 설정 UI 추가
- displayItems가 없으면 기존 방식(모든 필드 나열)으로 폴백
2025-11-18 10:14:31 +09:00
|
|
|
};
|
|
|
|
|
|
2025-11-18 10:21:36 +09:00
|
|
|
const removeDisplayItemFromGroup = (groupId: string, itemIndex: number) => {
|
|
|
|
|
const updatedGroups = localFieldGroups.map(g => {
|
|
|
|
|
if (g.id === groupId) {
|
|
|
|
|
return {
|
|
|
|
|
...g,
|
|
|
|
|
displayItems: (g.displayItems || []).filter((_, i) => i !== itemIndex)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return g;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setLocalFieldGroups(updatedGroups);
|
|
|
|
|
handleChange("fieldGroups", updatedGroups);
|
feat: 항목 표시 설정 기능 추가 (기본값, 빈 값 처리 포함)
- DisplayItem 타입 추가 (icon, field, text, badge)
- 필드별 표시 형식 지원 (text, currency, number, date, badge)
- 빈 값 처리 옵션 추가 (hide, default, blank)
- 기본값 설정 기능 추가
- 스타일 옵션 추가 (굵게, 밑줄, 기울임, 색상)
- renderDisplayItems 헬퍼 함수로 유연한 표시 렌더링
- SelectedItemsDetailInputConfigPanel에 항목 표시 설정 UI 추가
- displayItems가 없으면 기존 방식(모든 필드 나열)으로 폴백
2025-11-18 10:14:31 +09:00
|
|
|
};
|
|
|
|
|
|
2025-11-18 10:21:36 +09:00
|
|
|
const updateDisplayItemInGroup = (groupId: string, itemIndex: number, updates: Partial<DisplayItem>) => {
|
|
|
|
|
const updatedGroups = localFieldGroups.map(g => {
|
|
|
|
|
if (g.id === groupId) {
|
|
|
|
|
const updatedItems = [...(g.displayItems || [])];
|
|
|
|
|
updatedItems[itemIndex] = { ...updatedItems[itemIndex], ...updates };
|
|
|
|
|
return {
|
|
|
|
|
...g,
|
|
|
|
|
displayItems: updatedItems
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return g;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setLocalFieldGroups(updatedGroups);
|
|
|
|
|
handleChange("fieldGroups", updatedGroups);
|
feat: 항목 표시 설정 기능 추가 (기본값, 빈 값 처리 포함)
- DisplayItem 타입 추가 (icon, field, text, badge)
- 필드별 표시 형식 지원 (text, currency, number, date, badge)
- 빈 값 처리 옵션 추가 (hide, default, blank)
- 기본값 설정 기능 추가
- 스타일 옵션 추가 (굵게, 밑줄, 기울임, 색상)
- renderDisplayItems 헬퍼 함수로 유연한 표시 렌더링
- SelectedItemsDetailInputConfigPanel에 항목 표시 설정 UI 추가
- displayItems가 없으면 기존 방식(모든 필드 나열)으로 폴백
2025-11-18 10:14:31 +09:00
|
|
|
};
|
|
|
|
|
|
2025-11-17 15:25:08 +09:00
|
|
|
// 🆕 선택된 원본 테이블 표시명
|
|
|
|
|
const selectedSourceTableLabel = useMemo(() => {
|
|
|
|
|
if (!config.sourceTable) return "원본 테이블을 선택하세요";
|
|
|
|
|
const table = allTables.find((t) => t.tableName === config.sourceTable);
|
|
|
|
|
return table ? table.displayName || table.tableName : config.sourceTable;
|
|
|
|
|
}, [config.sourceTable, allTables]);
|
|
|
|
|
|
|
|
|
|
// 대상 테이블 필터링
|
2025-11-17 12:23:45 +09:00
|
|
|
const filteredTables = useMemo(() => {
|
|
|
|
|
if (!tableSearchValue) return allTables;
|
|
|
|
|
const searchLower = tableSearchValue.toLowerCase();
|
|
|
|
|
return allTables.filter(
|
|
|
|
|
(table) =>
|
|
|
|
|
table.tableName.toLowerCase().includes(searchLower) || table.displayName?.toLowerCase().includes(searchLower),
|
|
|
|
|
);
|
|
|
|
|
}, [allTables, tableSearchValue]);
|
|
|
|
|
|
2025-11-17 15:25:08 +09:00
|
|
|
// 선택된 대상 테이블 표시명
|
2025-11-17 12:23:45 +09:00
|
|
|
const selectedTableLabel = useMemo(() => {
|
2025-11-17 15:25:08 +09:00
|
|
|
if (!config.targetTable) return "저장 대상 테이블을 선택하세요";
|
2025-11-17 12:23:45 +09:00
|
|
|
const table = allTables.find((t) => t.tableName === config.targetTable);
|
|
|
|
|
return table ? table.displayName || table.tableName : config.targetTable;
|
|
|
|
|
}, [config.targetTable, allTables]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
{/* 데이터 소스 ID */}
|
|
|
|
|
<div className="space-y-2">
|
2025-11-17 15:25:08 +09:00
|
|
|
<Label className="text-xs font-semibold sm:text-sm">
|
|
|
|
|
데이터 소스 ID <span className="text-primary font-normal">(자동 설정됨)</span>
|
|
|
|
|
</Label>
|
2025-11-17 12:23:45 +09:00
|
|
|
<Input
|
|
|
|
|
value={config.dataSourceId || ""}
|
|
|
|
|
onChange={(e) => handleChange("dataSourceId", e.target.value)}
|
2025-11-17 15:25:08 +09:00
|
|
|
placeholder="비워두면 URL 파라미터에서 자동 설정"
|
2025-11-17 12:23:45 +09:00
|
|
|
className="h-7 text-xs sm:h-8 sm:text-sm"
|
|
|
|
|
/>
|
2025-11-17 15:25:08 +09:00
|
|
|
<p className="text-[10px] text-primary font-medium sm:text-xs">
|
|
|
|
|
✨ URL 파라미터에서 자동으로 가져옵니다 (Button이 전달)
|
|
|
|
|
</p>
|
2025-11-17 12:23:45 +09:00
|
|
|
<p className="text-[10px] text-gray-500 sm:text-xs">
|
2025-11-17 15:25:08 +09:00
|
|
|
테스트용으로 직접 입력하려면 테이블명을 입력하세요
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 🆕 원본 데이터 테이블 */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label className="text-xs font-semibold sm:text-sm">
|
|
|
|
|
원본 데이터 테이블 <span className="text-destructive">*</span>
|
|
|
|
|
</Label>
|
|
|
|
|
<Popover open={sourceTableSelectOpen} onOpenChange={setSourceTableSelectOpen}>
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
role="combobox"
|
|
|
|
|
aria-expanded={sourceTableSelectOpen}
|
|
|
|
|
className="h-8 w-full justify-between text-xs sm:text-sm"
|
|
|
|
|
disabled={allTables.length === 0}
|
|
|
|
|
>
|
|
|
|
|
{selectedSourceTableLabel}
|
|
|
|
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50 sm:h-4 sm:w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
<PopoverContent className="w-full p-0" align="start">
|
|
|
|
|
<Command>
|
|
|
|
|
<CommandInput placeholder="테이블 검색..." value={sourceTableSearchValue} onValueChange={setSourceTableSearchValue} className="h-8 text-xs sm:text-sm" />
|
|
|
|
|
<CommandEmpty>테이블을 찾을 수 없습니다.</CommandEmpty>
|
|
|
|
|
<CommandGroup className="max-h-64 overflow-auto">
|
|
|
|
|
{filteredSourceTables.map((table) => (
|
|
|
|
|
<CommandItem
|
|
|
|
|
key={table.tableName}
|
|
|
|
|
value={table.tableName}
|
|
|
|
|
onSelect={(currentValue) => {
|
|
|
|
|
handleChange("sourceTable", currentValue);
|
|
|
|
|
setSourceTableSelectOpen(false);
|
|
|
|
|
setSourceTableSearchValue("");
|
|
|
|
|
if (onSourceTableChange) {
|
|
|
|
|
onSourceTableChange(currentValue);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
className="text-xs sm:text-sm"
|
|
|
|
|
>
|
|
|
|
|
<Check
|
|
|
|
|
className={cn(
|
|
|
|
|
"mr-2 h-3 w-3 sm:h-4 sm:w-4",
|
|
|
|
|
config.sourceTable === table.tableName ? "opacity-100" : "opacity-0",
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
{table.displayName || table.tableName}
|
|
|
|
|
</CommandItem>
|
|
|
|
|
))}
|
|
|
|
|
</CommandGroup>
|
|
|
|
|
</Command>
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
</Popover>
|
|
|
|
|
<p className="text-[10px] text-gray-500 sm:text-xs">
|
|
|
|
|
이전 화면에서 전달받은 데이터의 원본 테이블 (예: item_info)
|
2025-11-17 12:23:45 +09:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 저장 대상 테이블 */}
|
|
|
|
|
<div className="space-y-2">
|
2025-11-17 15:25:08 +09:00
|
|
|
<Label className="text-xs font-semibold sm:text-sm">
|
|
|
|
|
저장 대상 테이블 <span className="text-destructive">*</span>
|
|
|
|
|
</Label>
|
2025-11-17 12:23:45 +09:00
|
|
|
<Popover open={tableSelectOpen} onOpenChange={setTableSelectOpen}>
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
role="combobox"
|
|
|
|
|
aria-expanded={tableSelectOpen}
|
|
|
|
|
className="h-7 w-full justify-between text-xs sm:h-8 sm:text-sm"
|
|
|
|
|
>
|
|
|
|
|
{selectedTableLabel}
|
|
|
|
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50 sm:h-4 sm:w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
<PopoverContent className="w-full p-0" align="start">
|
|
|
|
|
<Command>
|
|
|
|
|
<CommandInput
|
|
|
|
|
placeholder="테이블 검색..."
|
|
|
|
|
value={tableSearchValue}
|
|
|
|
|
onValueChange={setTableSearchValue}
|
|
|
|
|
className="text-xs sm:text-sm"
|
|
|
|
|
/>
|
|
|
|
|
<CommandEmpty className="text-xs sm:text-sm">테이블을 찾을 수 없습니다.</CommandEmpty>
|
|
|
|
|
<CommandGroup className="max-h-48 overflow-auto sm:max-h-64">
|
|
|
|
|
{filteredTables.map((table) => (
|
|
|
|
|
<CommandItem
|
|
|
|
|
key={table.tableName}
|
|
|
|
|
value={table.tableName}
|
|
|
|
|
onSelect={(currentValue) => {
|
|
|
|
|
handleChange("targetTable", currentValue);
|
|
|
|
|
setTableSelectOpen(false);
|
|
|
|
|
setTableSearchValue("");
|
2025-11-17 15:25:08 +09:00
|
|
|
if (onTargetTableChange) {
|
|
|
|
|
onTargetTableChange(currentValue);
|
2025-11-17 12:23:45 +09:00
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
className="text-xs sm:text-sm"
|
|
|
|
|
>
|
|
|
|
|
<Check
|
|
|
|
|
className={cn(
|
|
|
|
|
"mr-2 h-3 w-3 sm:h-4 sm:w-4",
|
|
|
|
|
config.targetTable === table.tableName ? "opacity-100" : "opacity-0",
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
{table.displayName || table.tableName}
|
|
|
|
|
</CommandItem>
|
|
|
|
|
))}
|
|
|
|
|
</CommandGroup>
|
|
|
|
|
</Command>
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
</Popover>
|
|
|
|
|
<p className="text-[10px] text-gray-500 sm:text-xs">최종 데이터를 저장할 테이블</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 표시할 원본 데이터 컬럼 */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label className="text-xs font-semibold sm:text-sm">표시할 원본 데이터 컬럼</Label>
|
|
|
|
|
<div className="space-y-2">
|
2025-11-17 15:25:08 +09:00
|
|
|
{displayColumns.map((col, index) => (
|
2025-11-17 12:23:45 +09:00
|
|
|
<div key={index} className="flex items-center gap-2">
|
2025-11-17 15:25:08 +09:00
|
|
|
<Input value={col.label || col.name} readOnly className="h-7 flex-1 text-xs sm:h-8 sm:text-sm" />
|
2025-11-17 12:23:45 +09:00
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
2025-11-17 15:25:08 +09:00
|
|
|
onClick={() => removeDisplayColumn(col.name)}
|
2025-11-17 12:23:45 +09:00
|
|
|
className="h-6 w-6 text-red-500 hover:bg-red-50 sm:h-7 sm:w-7"
|
|
|
|
|
>
|
|
|
|
|
<X className="h-3 w-3 sm:h-4 sm:w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
|
|
<Popover>
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="h-7 w-full border-dashed text-xs sm:text-sm"
|
|
|
|
|
>
|
|
|
|
|
<Plus className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
|
|
|
|
|
컬럼 추가
|
|
|
|
|
</Button>
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
<PopoverContent className="w-full p-0" align="start">
|
|
|
|
|
<Command>
|
|
|
|
|
<CommandInput placeholder="컬럼 검색..." className="text-xs sm:text-sm" />
|
|
|
|
|
<CommandEmpty className="text-xs sm:text-sm">사용 가능한 컬럼이 없습니다.</CommandEmpty>
|
|
|
|
|
<CommandGroup className="max-h-48 overflow-auto sm:max-h-64">
|
|
|
|
|
{availableColumns.map((column) => (
|
|
|
|
|
<CommandItem
|
|
|
|
|
key={column.columnName}
|
|
|
|
|
value={column.columnName}
|
2025-11-17 15:25:08 +09:00
|
|
|
onSelect={() => addDisplayColumn(column.columnName, column.columnLabel || column.columnName)}
|
2025-11-17 12:23:45 +09:00
|
|
|
className="text-xs sm:text-sm"
|
|
|
|
|
>
|
|
|
|
|
<div>
|
|
|
|
|
<div className="font-medium">{column.columnLabel || column.columnName}</div>
|
|
|
|
|
{column.dataType && <div className="text-[10px] text-gray-500">{column.dataType}</div>}
|
|
|
|
|
</div>
|
|
|
|
|
</CommandItem>
|
|
|
|
|
))}
|
|
|
|
|
</CommandGroup>
|
|
|
|
|
</Command>
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
</Popover>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-[10px] text-gray-500 sm:text-xs">
|
|
|
|
|
전달받은 원본 데이터 중 화면에 표시할 컬럼 (예: 품목코드, 품목명)
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 추가 입력 필드 정의 */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label className="text-xs font-semibold sm:text-sm">추가 입력 필드 정의</Label>
|
|
|
|
|
|
|
|
|
|
{localFields.map((field, index) => (
|
|
|
|
|
<Card key={index} className="border-2">
|
|
|
|
|
<CardContent className="space-y-2 pt-3 sm:space-y-3 sm:pt-4">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<span className="text-xs font-semibold text-gray-700 sm:text-sm">필드 {index + 1}</span>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
onClick={() => removeField(index)}
|
|
|
|
|
className="h-5 w-5 text-red-500 hover:bg-red-50 sm:h-6 sm:w-6"
|
|
|
|
|
>
|
|
|
|
|
<X className="h-2 w-2 sm:h-3 sm:w-3" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-2 sm:gap-3">
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label className="text-[10px] sm:text-xs">필드명 (컬럼)</Label>
|
|
|
|
|
<Popover
|
|
|
|
|
open={fieldPopoverOpen[index] || false}
|
|
|
|
|
onOpenChange={(open) => setFieldPopoverOpen({ ...fieldPopoverOpen, [index]: open })}
|
|
|
|
|
>
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
role="combobox"
|
|
|
|
|
className="h-6 w-full justify-between text-[10px] sm:h-7 sm:text-xs"
|
|
|
|
|
>
|
|
|
|
|
{field.name || "컬럼 선택"}
|
|
|
|
|
<ChevronsUpDown className="ml-1 h-2 w-2 shrink-0 opacity-50 sm:h-3 sm:w-3" />
|
|
|
|
|
</Button>
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
<PopoverContent className="w-[180px] p-0 sm:w-[200px]">
|
|
|
|
|
<Command>
|
|
|
|
|
<CommandInput placeholder="컬럼 검색..." className="h-6 text-[10px] sm:h-7 sm:text-xs" />
|
|
|
|
|
<CommandEmpty className="text-[10px] sm:text-xs">사용 가능한 컬럼이 없습니다.</CommandEmpty>
|
|
|
|
|
<CommandGroup className="max-h-[150px] overflow-auto sm:max-h-[200px]">
|
2025-11-17 15:25:08 +09:00
|
|
|
{availableTargetColumns.map((column) => (
|
2025-11-17 12:23:45 +09:00
|
|
|
<CommandItem
|
|
|
|
|
key={column.columnName}
|
|
|
|
|
value={column.columnName}
|
|
|
|
|
onSelect={() => {
|
|
|
|
|
updateField(index, {
|
|
|
|
|
name: column.columnName,
|
|
|
|
|
label: column.columnLabel || column.columnName,
|
2025-11-17 15:25:08 +09:00
|
|
|
inputType: column.inputType || "text", // 🆕 inputType 포함
|
|
|
|
|
codeCategory: column.codeCategory, // 🆕 codeCategory 포함
|
2025-11-17 12:23:45 +09:00
|
|
|
});
|
|
|
|
|
setFieldPopoverOpen({ ...fieldPopoverOpen, [index]: false });
|
|
|
|
|
}}
|
|
|
|
|
className="text-[10px] sm:text-xs"
|
|
|
|
|
>
|
|
|
|
|
<Check
|
|
|
|
|
className={cn(
|
|
|
|
|
"mr-1 h-2 w-2 sm:mr-2 sm:h-3 sm:w-3",
|
|
|
|
|
field.name === column.columnName ? "opacity-100" : "opacity-0",
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
<div>
|
|
|
|
|
<div className="font-medium">{column.columnLabel}</div>
|
|
|
|
|
<div className="text-[9px] text-gray-500">{column.columnName}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</CommandItem>
|
|
|
|
|
))}
|
|
|
|
|
</CommandGroup>
|
|
|
|
|
</Command>
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
</Popover>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label className="text-[10px] sm:text-xs">라벨</Label>
|
|
|
|
|
<Input
|
|
|
|
|
value={field.label}
|
|
|
|
|
onChange={(e) => updateField(index, { label: e.target.value })}
|
|
|
|
|
placeholder="필드 라벨"
|
|
|
|
|
className="h-6 w-full text-[10px] sm:h-7 sm:text-xs"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-2 sm:gap-3">
|
|
|
|
|
<div className="space-y-1">
|
2025-11-17 15:25:08 +09:00
|
|
|
<Label className="text-[10px] sm:text-xs">타입 (자동)</Label>
|
|
|
|
|
<Input
|
|
|
|
|
value={field.inputType || field.type || "text"}
|
|
|
|
|
readOnly
|
|
|
|
|
disabled
|
|
|
|
|
className="h-6 w-full text-[10px] sm:h-7 sm:text-xs bg-muted"
|
|
|
|
|
/>
|
|
|
|
|
<p className="text-[9px] text-primary sm:text-[10px]">
|
|
|
|
|
테이블 타입관리에서 자동 설정됨
|
|
|
|
|
</p>
|
2025-11-17 12:23:45 +09:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label className="text-[10px] sm:text-xs">Placeholder</Label>
|
|
|
|
|
<Input
|
|
|
|
|
value={field.placeholder || ""}
|
|
|
|
|
onChange={(e) => updateField(index, { placeholder: e.target.value })}
|
|
|
|
|
placeholder="입력 안내"
|
|
|
|
|
className="h-6 w-full text-[10px] sm:h-7 sm:text-xs"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-11-18 09:56:49 +09:00
|
|
|
{/* 🆕 필드 그룹 선택 */}
|
|
|
|
|
{localFieldGroups.length > 0 && (
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label className="text-[10px] sm:text-xs">필드 그룹 (선택사항)</Label>
|
|
|
|
|
<Select
|
|
|
|
|
value={field.groupId || "none"}
|
|
|
|
|
onValueChange={(value) => updateField(index, { groupId: value === "none" ? undefined : value })}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="h-6 text-[10px] sm:h-7 sm:text-xs">
|
|
|
|
|
<SelectValue placeholder="그룹 없음" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="none" className="text-xs">그룹 없음</SelectItem>
|
|
|
|
|
{localFieldGroups.map((group) => (
|
|
|
|
|
<SelectItem key={group.id} value={group.id} className="text-xs">
|
|
|
|
|
{group.title} ({group.id})
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
<p className="text-[9px] text-gray-500 sm:text-[10px]">
|
|
|
|
|
같은 그룹 ID를 가진 필드들은 같은 카드에 표시됩니다
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-11-17 12:23:45 +09:00
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<Checkbox
|
|
|
|
|
id={`required-${index}`}
|
|
|
|
|
checked={field.required ?? false}
|
|
|
|
|
onCheckedChange={(checked) => updateField(index, { required: checked as boolean })}
|
|
|
|
|
/>
|
|
|
|
|
<Label htmlFor={`required-${index}`} className="cursor-pointer text-[10px] font-normal sm:text-xs">
|
|
|
|
|
필수 입력
|
|
|
|
|
</Label>
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={addField}
|
|
|
|
|
className="h-7 w-full border-dashed text-xs sm:text-sm"
|
|
|
|
|
>
|
|
|
|
|
<Plus className="mr-1 h-3 w-3 sm:mr-2 sm:h-4 sm:w-4" />
|
|
|
|
|
필드 추가
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-11-18 09:56:49 +09:00
|
|
|
{/* 🆕 필드 그룹 관리 */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label className="text-xs font-semibold sm:text-sm">필드 그룹 설정</Label>
|
|
|
|
|
<p className="text-[10px] text-gray-500 sm:text-xs">
|
|
|
|
|
추가 입력 필드를 여러 카드로 나눠서 표시 (예: 거래처 정보, 단가 정보)
|
|
|
|
|
</p>
|
|
|
|
|
|
2025-11-18 10:28:07 +09:00
|
|
|
{localFieldGroups.map((group, index) => {
|
|
|
|
|
const isGroupExpanded = expandedGroups[group.id] ?? true;
|
|
|
|
|
|
|
|
|
|
return (
|
2025-11-18 10:25:28 +09:00
|
|
|
<Collapsible
|
|
|
|
|
key={group.id}
|
2025-11-18 10:28:07 +09:00
|
|
|
open={isGroupExpanded}
|
2025-11-18 10:25:28 +09:00
|
|
|
onOpenChange={(open) => setExpandedGroups(prev => ({ ...prev, [group.id]: open }))}
|
|
|
|
|
>
|
|
|
|
|
<Card className="border-2">
|
|
|
|
|
<CardContent className="space-y-2 p-3 sm:space-y-3 sm:p-4">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<CollapsibleTrigger asChild>
|
|
|
|
|
<Button variant="ghost" size="sm" className="h-auto p-0 hover:bg-transparent">
|
|
|
|
|
<div className="flex items-center gap-2">
|
2025-11-18 10:28:07 +09:00
|
|
|
{isGroupExpanded ? (
|
2025-11-18 10:25:28 +09:00
|
|
|
<ChevronDown className="h-4 w-4" />
|
|
|
|
|
) : (
|
|
|
|
|
<ChevronRight className="h-4 w-4" />
|
|
|
|
|
)}
|
|
|
|
|
<span className="text-xs font-semibold sm:text-sm">
|
|
|
|
|
그룹 {index + 1}: {group.title || group.id}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</Button>
|
|
|
|
|
</CollapsibleTrigger>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
onClick={() => removeFieldGroup(group.id)}
|
|
|
|
|
className="h-6 w-6 text-red-500 hover:text-red-700 sm:h-7 sm:w-7"
|
|
|
|
|
>
|
|
|
|
|
<X className="h-3 w-3 sm:h-4 sm:w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-11-18 10:28:07 +09:00
|
|
|
<CollapsibleContent className="space-y-2 sm:space-y-3">
|
|
|
|
|
{/* 그룹 ID */}
|
2025-11-18 09:56:49 +09:00
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label className="text-[10px] sm:text-xs">그룹 ID</Label>
|
|
|
|
|
<Input
|
|
|
|
|
value={group.id}
|
|
|
|
|
onChange={(e) => updateFieldGroup(group.id, { id: e.target.value })}
|
|
|
|
|
className="h-7 text-xs sm:h-8 sm:text-sm"
|
|
|
|
|
placeholder="group_customer"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 그룹 제목 */}
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label className="text-[10px] sm:text-xs">그룹 제목</Label>
|
|
|
|
|
<Input
|
|
|
|
|
value={group.title}
|
|
|
|
|
onChange={(e) => updateFieldGroup(group.id, { title: e.target.value })}
|
|
|
|
|
className="h-7 text-xs sm:h-8 sm:text-sm"
|
|
|
|
|
placeholder="거래처 정보"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 그룹 설명 */}
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label className="text-[10px] sm:text-xs">그룹 설명 (선택사항)</Label>
|
|
|
|
|
<Input
|
|
|
|
|
value={group.description || ""}
|
|
|
|
|
onChange={(e) => updateFieldGroup(group.id, { description: e.target.value })}
|
|
|
|
|
className="h-7 text-xs sm:h-8 sm:text-sm"
|
|
|
|
|
placeholder="거래처 관련 정보를 입력합니다"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 표시 순서 */}
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label className="text-[10px] sm:text-xs">표시 순서</Label>
|
|
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
value={group.order || 0}
|
|
|
|
|
onChange={(e) => updateFieldGroup(group.id, { order: parseInt(e.target.value) || 0 })}
|
|
|
|
|
className="h-7 text-xs sm:h-8 sm:text-sm"
|
|
|
|
|
min="0"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2025-11-18 10:21:36 +09:00
|
|
|
|
|
|
|
|
{/* 🆕 이 그룹의 항목 표시 설정 */}
|
2025-11-18 10:25:28 +09:00
|
|
|
<Collapsible
|
|
|
|
|
open={expandedDisplayItems[group.id] ?? false}
|
|
|
|
|
onOpenChange={(open) => setExpandedDisplayItems(prev => ({ ...prev, [group.id]: open }))}
|
|
|
|
|
>
|
|
|
|
|
<div className="space-y-2 rounded-lg border-2 border-dashed border-primary/30 bg-primary/5 p-2">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<CollapsibleTrigger asChild>
|
|
|
|
|
<Button variant="ghost" size="sm" className="h-auto p-0 hover:bg-transparent">
|
|
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
{expandedDisplayItems[group.id] ? (
|
|
|
|
|
<ChevronDown className="h-3 w-3" />
|
|
|
|
|
) : (
|
|
|
|
|
<ChevronRight className="h-3 w-3" />
|
|
|
|
|
)}
|
|
|
|
|
<Label className="text-[10px] font-semibold sm:text-xs cursor-pointer">
|
|
|
|
|
항목 표시 설정 ({(group.displayItems || []).length}개)
|
|
|
|
|
</Label>
|
|
|
|
|
</div>
|
|
|
|
|
</Button>
|
|
|
|
|
</CollapsibleTrigger>
|
|
|
|
|
<div className="flex gap-1">
|
2025-11-18 10:21:36 +09:00
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => addDisplayItemToGroup(group.id, "icon")}
|
|
|
|
|
className="h-5 px-1 text-[9px] sm:h-6 sm:px-2 sm:text-[10px]"
|
|
|
|
|
>
|
|
|
|
|
<Plus className="mr-0.5 h-2 w-2 sm:mr-1 sm:h-3 sm:w-3" />
|
|
|
|
|
아이콘
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => addDisplayItemToGroup(group.id, "field")}
|
|
|
|
|
className="h-5 px-1 text-[9px] sm:h-6 sm:px-2 sm:text-[10px]"
|
|
|
|
|
>
|
|
|
|
|
<Plus className="mr-0.5 h-2 w-2 sm:mr-1 sm:h-3 sm:w-3" />
|
|
|
|
|
필드
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => addDisplayItemToGroup(group.id, "text")}
|
|
|
|
|
className="h-5 px-1 text-[9px] sm:h-6 sm:px-2 sm:text-[10px]"
|
|
|
|
|
>
|
|
|
|
|
<Plus className="mr-0.5 h-2 w-2 sm:mr-1 sm:h-3 sm:w-3" />
|
|
|
|
|
텍스트
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-11-18 10:25:28 +09:00
|
|
|
<CollapsibleContent className="space-y-2">
|
|
|
|
|
<p className="text-[9px] text-muted-foreground sm:text-[10px]">
|
|
|
|
|
이 그룹의 입력 항목이 추가되면 어떻게 표시될지 설정
|
|
|
|
|
</p>
|
2025-11-18 10:21:36 +09:00
|
|
|
|
2025-11-18 10:25:28 +09:00
|
|
|
{(!group.displayItems || group.displayItems.length === 0) ? (
|
2025-11-18 10:21:36 +09:00
|
|
|
<div className="rounded border border-dashed p-2 text-center text-[10px] text-muted-foreground">
|
|
|
|
|
미설정 (모든 필드를 " / "로 구분하여 표시)
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
{group.displayItems.map((item, itemIndex) => (
|
|
|
|
|
<div key={item.id} className="rounded border bg-card p-2 space-y-1">
|
|
|
|
|
{/* 헤더 */}
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<span className="text-[9px] font-medium sm:text-[10px]">
|
|
|
|
|
{item.type === "icon" && "🎨"}
|
|
|
|
|
{item.type === "field" && "📝"}
|
|
|
|
|
{item.type === "text" && "💬"}
|
|
|
|
|
{item.type === "badge" && "🏷️"}
|
|
|
|
|
</span>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => removeDisplayItemFromGroup(group.id, itemIndex)}
|
|
|
|
|
className="h-4 w-4 p-0"
|
|
|
|
|
>
|
|
|
|
|
<X className="h-2 w-2" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 아이콘 설정 */}
|
|
|
|
|
{item.type === "icon" && (
|
|
|
|
|
<Input
|
|
|
|
|
value={item.icon || ""}
|
|
|
|
|
onChange={(e) => updateDisplayItemInGroup(group.id, itemIndex, { icon: e.target.value })}
|
|
|
|
|
placeholder="Building"
|
|
|
|
|
className="h-6 text-[9px] sm:text-[10px]"
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 텍스트 설정 */}
|
|
|
|
|
{item.type === "text" && (
|
|
|
|
|
<Input
|
|
|
|
|
value={item.value || ""}
|
|
|
|
|
onChange={(e) => updateDisplayItemInGroup(group.id, itemIndex, { value: e.target.value })}
|
|
|
|
|
placeholder="| , / , -"
|
|
|
|
|
className="h-6 text-[9px] sm:text-[10px]"
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 필드 설정 */}
|
|
|
|
|
{item.type === "field" && (
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
{/* 필드 선택 */}
|
|
|
|
|
<Select
|
|
|
|
|
value={item.fieldName || ""}
|
|
|
|
|
onValueChange={(value) => updateDisplayItemInGroup(group.id, itemIndex, { fieldName: value })}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="h-6 text-[9px] sm:text-[10px]">
|
|
|
|
|
<SelectValue placeholder="필드 선택" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{localFields.filter(f => f.groupId === group.id).map((field) => (
|
|
|
|
|
<SelectItem key={field.name} value={field.name} className="text-[9px] sm:text-[10px]">
|
|
|
|
|
{field.label || field.name}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
|
|
|
|
|
{/* 라벨 */}
|
|
|
|
|
<Input
|
|
|
|
|
value={item.label || ""}
|
|
|
|
|
onChange={(e) => updateDisplayItemInGroup(group.id, itemIndex, { label: e.target.value })}
|
|
|
|
|
placeholder="거래처:"
|
|
|
|
|
className="h-6 text-[9px] sm:text-[10px]"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* 표시 형식 */}
|
|
|
|
|
<Select
|
|
|
|
|
value={item.format || "text"}
|
|
|
|
|
onValueChange={(value) => updateDisplayItemInGroup(group.id, itemIndex, { format: value as DisplayFieldFormat })}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="h-6 text-[9px] sm:text-[10px]">
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="text" className="text-[9px] sm:text-[10px]">텍스트</SelectItem>
|
|
|
|
|
<SelectItem value="currency" className="text-[9px] sm:text-[10px]">금액</SelectItem>
|
|
|
|
|
<SelectItem value="number" className="text-[9px] sm:text-[10px]">숫자</SelectItem>
|
|
|
|
|
<SelectItem value="date" className="text-[9px] sm:text-[10px]">날짜</SelectItem>
|
|
|
|
|
<SelectItem value="badge" className="text-[9px] sm:text-[10px]">배지</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
|
|
|
|
|
{/* 빈 값 처리 */}
|
|
|
|
|
<div className="grid grid-cols-2 gap-1">
|
|
|
|
|
<Select
|
|
|
|
|
value={item.emptyBehavior || "default"}
|
|
|
|
|
onValueChange={(value) => updateDisplayItemInGroup(group.id, itemIndex, { emptyBehavior: value as EmptyBehavior })}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="h-6 text-[9px] sm:text-[10px]">
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="hide" className="text-[9px] sm:text-[10px]">숨김</SelectItem>
|
|
|
|
|
<SelectItem value="default" className="text-[9px] sm:text-[10px]">기본값</SelectItem>
|
|
|
|
|
<SelectItem value="blank" className="text-[9px] sm:text-[10px]">빈칸</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
|
|
|
|
|
{/* 기본값 */}
|
|
|
|
|
{item.emptyBehavior === "default" && (
|
|
|
|
|
<Input
|
|
|
|
|
value={item.defaultValue || ""}
|
|
|
|
|
onChange={(e) => updateDisplayItemInGroup(group.id, itemIndex, { defaultValue: e.target.value })}
|
|
|
|
|
placeholder="미입력"
|
|
|
|
|
className="h-6 text-[9px] sm:text-[10px]"
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2025-11-18 10:25:28 +09:00
|
|
|
)}
|
|
|
|
|
</CollapsibleContent>
|
|
|
|
|
</div>
|
|
|
|
|
</Collapsible>
|
|
|
|
|
|
|
|
|
|
</CollapsibleContent>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</Collapsible>
|
2025-11-18 10:28:07 +09:00
|
|
|
);
|
|
|
|
|
})}
|
2025-11-18 09:56:49 +09:00
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={addFieldGroup}
|
|
|
|
|
className="h-7 w-full border-dashed text-xs sm:text-sm"
|
|
|
|
|
>
|
|
|
|
|
<Plus className="mr-1 h-3 w-3 sm:mr-2 sm:h-4 sm:w-4" />
|
|
|
|
|
그룹 추가
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
{localFieldGroups.length > 0 && (
|
|
|
|
|
<p className="text-[10px] text-amber-600 sm:text-xs">
|
|
|
|
|
💡 추가 입력 필드의 "필드 그룹 ID"에 위에서 정의한 그룹 ID를 입력하세요
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 입력 모드 설정 */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label className="text-xs font-semibold sm:text-sm">입력 모드</Label>
|
|
|
|
|
<Select
|
|
|
|
|
value={config.inputMode || "inline"}
|
|
|
|
|
onValueChange={(value) => handleChange("inputMode", value as "inline" | "modal")}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="h-7 text-xs sm:h-8 sm:text-sm">
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="inline" className="text-xs sm:text-sm">
|
|
|
|
|
항상 표시 (Inline)
|
|
|
|
|
</SelectItem>
|
|
|
|
|
<SelectItem value="modal" className="text-xs sm:text-sm">
|
|
|
|
|
추가 버튼으로 표시 (Modal)
|
|
|
|
|
</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
<p className="text-[10px] text-muted-foreground sm:text-xs">
|
|
|
|
|
{config.inputMode === "modal"
|
|
|
|
|
? "추가 버튼 클릭 시 입력창 표시, 완료 후 작은 카드로 표시"
|
|
|
|
|
: "모든 항목의 입력창을 항상 표시"}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-11-17 12:23:45 +09:00
|
|
|
{/* 레이아웃 설정 */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label className="text-xs font-semibold sm:text-sm">레이아웃</Label>
|
|
|
|
|
<Select
|
|
|
|
|
value={config.layout || "grid"}
|
|
|
|
|
onValueChange={(value) => handleChange("layout", value as "grid" | "card")}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="h-7 text-xs sm:h-8 sm:text-sm">
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="grid" className="text-xs sm:text-sm">
|
|
|
|
|
테이블 형식 (Grid)
|
|
|
|
|
</SelectItem>
|
|
|
|
|
<SelectItem value="card" className="text-xs sm:text-sm">
|
|
|
|
|
카드 형식 (Card)
|
|
|
|
|
</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
<p className="text-[10px] text-muted-foreground sm:text-xs">
|
|
|
|
|
{config.layout === "grid" ? "행 단위로 데이터를 표시합니다" : "각 항목을 카드로 표시합니다"}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 옵션 */}
|
|
|
|
|
<div className="space-y-2 rounded-lg border p-3 sm:p-4">
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<Checkbox
|
|
|
|
|
id="show-index"
|
|
|
|
|
checked={config.showIndex ?? true}
|
|
|
|
|
onCheckedChange={(checked) => handleChange("showIndex", checked as boolean)}
|
|
|
|
|
/>
|
|
|
|
|
<Label htmlFor="show-index" className="cursor-pointer text-[10px] font-normal sm:text-xs">
|
|
|
|
|
항목 번호 표시
|
|
|
|
|
</Label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<Checkbox
|
|
|
|
|
id="allow-remove"
|
|
|
|
|
checked={config.allowRemove ?? false}
|
|
|
|
|
onCheckedChange={(checked) => handleChange("allowRemove", checked as boolean)}
|
|
|
|
|
/>
|
|
|
|
|
<Label htmlFor="allow-remove" className="cursor-pointer text-[10px] font-normal sm:text-xs">
|
|
|
|
|
항목 삭제 허용
|
|
|
|
|
</Label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<Checkbox
|
|
|
|
|
id="disabled"
|
|
|
|
|
checked={config.disabled ?? false}
|
|
|
|
|
onCheckedChange={(checked) => handleChange("disabled", checked as boolean)}
|
|
|
|
|
/>
|
|
|
|
|
<Label htmlFor="disabled" className="cursor-pointer text-[10px] font-normal sm:text-xs">
|
|
|
|
|
비활성화 (읽기 전용)
|
|
|
|
|
</Label>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 사용 예시 */}
|
|
|
|
|
<div className="rounded-lg bg-blue-50 p-2 text-xs sm:p-3 sm:text-sm">
|
|
|
|
|
<p className="mb-1 font-medium text-blue-900">💡 사용 예시</p>
|
|
|
|
|
<ul className="space-y-1 text-[10px] text-blue-700 sm:text-xs">
|
|
|
|
|
<li>• 품목 선택 모달 → 다음 버튼 → 거래처별 가격 입력</li>
|
|
|
|
|
<li>• 사용자 선택 모달 → 다음 버튼 → 권한 및 부서 할당</li>
|
|
|
|
|
<li>• 제품 선택 모달 → 다음 버튼 → 수량 및 납기일 입력</li>
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
SelectedItemsDetailInputConfigPanel.displayName = "SelectedItemsDetailInputConfigPanel";
|