Compare commits
6 Commits
cddce40f35
...
967b76591b
| Author | SHA1 | Date |
|---|---|---|
|
|
967b76591b | |
|
|
4cd27639e6 | |
|
|
7bb26e0e30 | |
|
|
def94c41f4 | |
|
|
eef1451c5a | |
|
|
e234f88577 |
|
|
@ -3,16 +3,18 @@
|
||||||
import React, { useState, useEffect, useMemo, useCallback } from "react";
|
import React, { useState, useEffect, useMemo, useCallback } from "react";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { ComponentRendererProps } from "@/types/component";
|
import { ComponentRendererProps } from "@/types/component";
|
||||||
import { SelectedItemsDetailInputConfig, AdditionalFieldDefinition, ItemData, GroupEntry } from "./types";
|
import { SelectedItemsDetailInputConfig, AdditionalFieldDefinition, ItemData, GroupEntry, DisplayItem } from "./types";
|
||||||
import { useModalDataStore, ModalDataItem } from "@/stores/modalDataStore";
|
import { useModalDataStore, ModalDataItem } from "@/stores/modalDataStore";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
|
import * as LucideIcons from "lucide-react";
|
||||||
import { commonCodeApi } from "@/lib/api/commonCode";
|
import { commonCodeApi } from "@/lib/api/commonCode";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
|
@ -542,6 +544,164 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 🆕 displayItems를 렌더링하는 헬퍼 함수 (그룹별)
|
||||||
|
const renderDisplayItems = useCallback((entry: GroupEntry, item: ItemData, groupId: string) => {
|
||||||
|
// 🆕 해당 그룹의 displayItems 가져오기
|
||||||
|
const group = (componentConfig.fieldGroups || []).find(g => g.id === groupId);
|
||||||
|
const displayItems = group?.displayItems || [];
|
||||||
|
|
||||||
|
if (displayItems.length === 0) {
|
||||||
|
// displayItems가 없으면 기본 방식 (해당 그룹의 필드만 나열)
|
||||||
|
const fields = (componentConfig.additionalFields || []).filter(f =>
|
||||||
|
componentConfig.fieldGroups && componentConfig.fieldGroups.length > 0
|
||||||
|
? f.groupId === groupId
|
||||||
|
: true
|
||||||
|
);
|
||||||
|
return fields.map((f) => entry[f.name] || "-").join(" / ");
|
||||||
|
}
|
||||||
|
|
||||||
|
// displayItems 설정대로 렌더링
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{displayItems.map((displayItem) => {
|
||||||
|
const styleClasses = cn(
|
||||||
|
displayItem.bold && "font-bold",
|
||||||
|
displayItem.underline && "underline",
|
||||||
|
displayItem.italic && "italic"
|
||||||
|
);
|
||||||
|
|
||||||
|
const inlineStyle: React.CSSProperties = {
|
||||||
|
color: displayItem.color,
|
||||||
|
backgroundColor: displayItem.backgroundColor,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (displayItem.type) {
|
||||||
|
case "icon": {
|
||||||
|
if (!displayItem.icon) return null;
|
||||||
|
const IconComponent = (LucideIcons as any)[displayItem.icon];
|
||||||
|
if (!IconComponent) return null;
|
||||||
|
return (
|
||||||
|
<IconComponent
|
||||||
|
key={displayItem.id}
|
||||||
|
className="h-3 w-3 inline-block mr-1"
|
||||||
|
style={inlineStyle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "text":
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={displayItem.id}
|
||||||
|
className={styleClasses}
|
||||||
|
style={inlineStyle}
|
||||||
|
>
|
||||||
|
{displayItem.value}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "field": {
|
||||||
|
const fieldValue = entry[displayItem.fieldName || ""];
|
||||||
|
const isEmpty = fieldValue === null || fieldValue === undefined || fieldValue === "";
|
||||||
|
|
||||||
|
// 🆕 빈 값 처리
|
||||||
|
if (isEmpty) {
|
||||||
|
switch (displayItem.emptyBehavior) {
|
||||||
|
case "hide":
|
||||||
|
return null; // 항목 숨김
|
||||||
|
case "default":
|
||||||
|
// 기본값 표시
|
||||||
|
const defaultValue = displayItem.defaultValue || "-";
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={displayItem.id}
|
||||||
|
className={cn(styleClasses, "text-muted-foreground")}
|
||||||
|
style={inlineStyle}
|
||||||
|
>
|
||||||
|
{displayItem.label}{defaultValue}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
case "blank":
|
||||||
|
default:
|
||||||
|
// 빈 칸으로 표시
|
||||||
|
return (
|
||||||
|
<span key={displayItem.id} className={styleClasses} style={inlineStyle}>
|
||||||
|
{displayItem.label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 값이 있는 경우, 형식에 맞게 표시
|
||||||
|
let formattedValue = fieldValue;
|
||||||
|
switch (displayItem.format) {
|
||||||
|
case "currency":
|
||||||
|
// 천 단위 구분
|
||||||
|
formattedValue = new Intl.NumberFormat("ko-KR").format(Number(fieldValue) || 0);
|
||||||
|
break;
|
||||||
|
case "number":
|
||||||
|
formattedValue = new Intl.NumberFormat("ko-KR").format(Number(fieldValue) || 0);
|
||||||
|
break;
|
||||||
|
case "date":
|
||||||
|
// YYYY.MM.DD 형식
|
||||||
|
if (fieldValue) {
|
||||||
|
const date = new Date(fieldValue);
|
||||||
|
if (!isNaN(date.getTime())) {
|
||||||
|
formattedValue = date.toLocaleDateString("ko-KR", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
}).replace(/\. /g, ".").replace(/\.$/, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "badge":
|
||||||
|
// 배지로 표시
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
key={displayItem.id}
|
||||||
|
variant={displayItem.badgeVariant || "default"}
|
||||||
|
className={styleClasses}
|
||||||
|
style={inlineStyle}
|
||||||
|
>
|
||||||
|
{displayItem.label}{formattedValue}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
case "text":
|
||||||
|
default:
|
||||||
|
// 일반 텍스트
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span key={displayItem.id} className={styleClasses} style={inlineStyle}>
|
||||||
|
{displayItem.label}{formattedValue}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "badge": {
|
||||||
|
const fieldValue = displayItem.fieldName ? entry[displayItem.fieldName] : displayItem.value;
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
key={displayItem.id}
|
||||||
|
variant={displayItem.badgeVariant || "default"}
|
||||||
|
className={styleClasses}
|
||||||
|
style={inlineStyle}
|
||||||
|
>
|
||||||
|
{displayItem.label}{fieldValue}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}, [componentConfig.fieldGroups, componentConfig.additionalFields]);
|
||||||
|
|
||||||
// 빈 상태 렌더링
|
// 빈 상태 렌더링
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -650,8 +810,8 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||||
className="flex items-center justify-between border rounded p-2 text-xs bg-muted/30 cursor-pointer hover:bg-muted/50"
|
className="flex items-center justify-between border rounded p-2 text-xs bg-muted/30 cursor-pointer hover:bg-muted/50"
|
||||||
onClick={() => handleEditGroupEntry(item.id, group.id, entry.id)}
|
onClick={() => handleEditGroupEntry(item.id, group.id, entry.id)}
|
||||||
>
|
>
|
||||||
<span>
|
<span className="flex items-center gap-1">
|
||||||
{idx + 1}. {groupFields.map((f) => entry[f.name] || "-").join(" / ")}
|
{idx + 1}. {renderDisplayItems(entry, item, group.id)}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@ import { Button } from "@/components/ui/button";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Plus, X } from "lucide-react";
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||||
import { SelectedItemsDetailInputConfig, AdditionalFieldDefinition, FieldGroup } from "./types";
|
import { Plus, X, ChevronDown, ChevronRight } from "lucide-react";
|
||||||
|
import { SelectedItemsDetailInputConfig, AdditionalFieldDefinition, FieldGroup, DisplayItem, DisplayItemType, EmptyBehavior, DisplayFieldFormat } from "./types";
|
||||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@/components/ui/command";
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@/components/ui/command";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
import { Check, ChevronsUpDown } from "lucide-react";
|
import { Check, ChevronsUpDown } from "lucide-react";
|
||||||
|
|
@ -46,6 +47,12 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
// 🆕 필드 그룹 상태
|
// 🆕 필드 그룹 상태
|
||||||
const [localFieldGroups, setLocalFieldGroups] = useState<FieldGroup[]>(config.fieldGroups || []);
|
const [localFieldGroups, setLocalFieldGroups] = useState<FieldGroup[]>(config.fieldGroups || []);
|
||||||
|
|
||||||
|
// 🆕 그룹별 펼침/접힘 상태
|
||||||
|
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
// 🆕 그룹별 표시 항목 설정 펼침/접힘 상태
|
||||||
|
const [expandedDisplayItems, setExpandedDisplayItems] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
// 🆕 원본 테이블 선택 상태
|
// 🆕 원본 테이블 선택 상태
|
||||||
const [sourceTableSelectOpen, setSourceTableSelectOpen] = useState(false);
|
const [sourceTableSelectOpen, setSourceTableSelectOpen] = useState(false);
|
||||||
const [sourceTableSearchValue, setSourceTableSearchValue] = useState("");
|
const [sourceTableSearchValue, setSourceTableSearchValue] = useState("");
|
||||||
|
|
@ -169,6 +176,71 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
);
|
);
|
||||||
}, [allTables, sourceTableSearchValue]);
|
}, [allTables, sourceTableSearchValue]);
|
||||||
|
|
||||||
|
// 🆕 그룹별 항목 표시 설정 핸들러
|
||||||
|
const addDisplayItemToGroup = (groupId: string, type: DisplayItemType) => {
|
||||||
|
const newItem: DisplayItem = {
|
||||||
|
type,
|
||||||
|
id: `display-${Date.now()}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (type === "field") {
|
||||||
|
// 해당 그룹의 필드만 선택 가능하도록
|
||||||
|
const groupFields = localFields.filter(f => f.groupId === groupId);
|
||||||
|
newItem.fieldName = groupFields[0]?.name || "";
|
||||||
|
newItem.format = "text";
|
||||||
|
newItem.emptyBehavior = "default";
|
||||||
|
} else if (type === "icon") {
|
||||||
|
newItem.icon = "Circle";
|
||||||
|
} else if (type === "text") {
|
||||||
|
newItem.value = "텍스트";
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedGroups = localFieldGroups.map(g => {
|
||||||
|
if (g.id === groupId) {
|
||||||
|
return {
|
||||||
|
...g,
|
||||||
|
displayItems: [...(g.displayItems || []), newItem]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return g;
|
||||||
|
});
|
||||||
|
|
||||||
|
setLocalFieldGroups(updatedGroups);
|
||||||
|
handleChange("fieldGroups", updatedGroups);
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
// 🆕 선택된 원본 테이블 표시명
|
// 🆕 선택된 원본 테이블 표시명
|
||||||
const selectedSourceTableLabel = useMemo(() => {
|
const selectedSourceTableLabel = useMemo(() => {
|
||||||
if (!config.sourceTable) return "원본 테이블을 선택하세요";
|
if (!config.sourceTable) return "원본 테이블을 선택하세요";
|
||||||
|
|
@ -555,23 +627,45 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
추가 입력 필드를 여러 카드로 나눠서 표시 (예: 거래처 정보, 단가 정보)
|
추가 입력 필드를 여러 카드로 나눠서 표시 (예: 거래처 정보, 단가 정보)
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{localFieldGroups.map((group, index) => (
|
{localFieldGroups.map((group, index) => {
|
||||||
<Card key={group.id} className="border-2">
|
const isGroupExpanded = expandedGroups[group.id] ?? true;
|
||||||
<CardContent className="space-y-2 pt-3 sm:space-y-3 sm:pt-4">
|
|
||||||
<div className="flex items-center justify-between">
|
return (
|
||||||
<span className="text-xs font-semibold text-gray-700 sm:text-sm">그룹 {index + 1}</span>
|
<Collapsible
|
||||||
<Button
|
key={group.id}
|
||||||
type="button"
|
open={isGroupExpanded}
|
||||||
variant="ghost"
|
onOpenChange={(open) => setExpandedGroups(prev => ({ ...prev, [group.id]: open }))}
|
||||||
size="icon"
|
>
|
||||||
onClick={() => removeFieldGroup(group.id)}
|
<Card className="border-2">
|
||||||
className="h-6 w-6 text-red-500 hover:text-red-700 sm:h-7 sm:w-7"
|
<CardContent className="p-3 sm:p-4">
|
||||||
>
|
<div className="flex items-center justify-between mb-2">
|
||||||
<X className="h-3 w-3 sm:h-4 sm:w-4" />
|
<CollapsibleTrigger asChild>
|
||||||
</Button>
|
<Button variant="ghost" size="sm" className="h-auto p-0 hover:bg-transparent">
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
|
{isGroupExpanded ? (
|
||||||
|
<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>
|
||||||
|
|
||||||
{/* 그룹 ID */}
|
<CollapsibleContent className="space-y-2 sm:space-y-3 mt-2">
|
||||||
|
{/* 그룹 ID */}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-[10px] sm:text-xs">그룹 ID</Label>
|
<Label className="text-[10px] sm:text-xs">그룹 ID</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -615,9 +709,198 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
min="0"
|
min="0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
{/* 🆕 이 그룹의 항목 표시 설정 */}
|
||||||
))}
|
<Collapsible
|
||||||
|
open={expandedDisplayItems[group.id] ?? false}
|
||||||
|
onOpenChange={(open) => setExpandedDisplayItems(prev => ({ ...prev, [group.id]: open }))}
|
||||||
|
>
|
||||||
|
<div className="rounded-lg border-2 border-dashed border-primary/30 bg-primary/5 p-2">
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<Button variant="ghost" size="sm" className="h-auto w-full justify-start p-1 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>
|
||||||
|
|
||||||
|
<CollapsibleContent className="space-y-2 mt-2">
|
||||||
|
{/* 추가 버튼들 */}
|
||||||
|
<div className="flex gap-1 flex-wrap">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => addDisplayItemToGroup(group.id, "icon")}
|
||||||
|
className="h-5 px-1.5 text-[8px] sm:h-6 sm:px-2 sm:text-[10px]"
|
||||||
|
>
|
||||||
|
<Plus className="mr-0.5 h-2.5 w-2.5 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.5 text-[8px] sm:h-6 sm:px-2 sm:text-[10px]"
|
||||||
|
>
|
||||||
|
<Plus className="mr-0.5 h-2.5 w-2.5 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.5 text-[8px] sm:h-6 sm:px-2 sm:text-[10px]"
|
||||||
|
>
|
||||||
|
<Plus className="mr-0.5 h-2.5 w-2.5 sm:mr-1 sm:h-3 sm:w-3" />
|
||||||
|
텍스트
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[9px] text-muted-foreground sm:text-[10px]">
|
||||||
|
이 그룹의 입력 항목이 추가되면 어떻게 표시될지 설정
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{(!group.displayItems || group.displayItems.length === 0) ? (
|
||||||
|
<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 w-full 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 w-full text-[9px] sm:text-[10px]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 표시 형식 */}
|
||||||
|
<Select
|
||||||
|
value={item.format || "text"}
|
||||||
|
onValueChange={(value) => updateDisplayItemInGroup(group.id, itemIndex, { format: value as DisplayFieldFormat })}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-6 w-full 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>
|
||||||
|
|
||||||
|
{/* 빈 값 처리 */}
|
||||||
|
<Select
|
||||||
|
value={item.emptyBehavior || "default"}
|
||||||
|
onValueChange={(value) => updateDisplayItemInGroup(group.id, itemIndex, { emptyBehavior: value as EmptyBehavior })}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-6 w-full 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 w-full text-[9px] sm:text-[10px]"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CollapsibleContent>
|
||||||
|
</div>
|
||||||
|
</Collapsible>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Collapsible>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@ export interface FieldGroup {
|
||||||
description?: string;
|
description?: string;
|
||||||
/** 그룹 표시 순서 */
|
/** 그룹 표시 순서 */
|
||||||
order?: number;
|
order?: number;
|
||||||
|
/** 🆕 이 그룹의 항목 표시 설정 (그룹별로 다른 표시 형식 가능) */
|
||||||
|
displayItems?: DisplayItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -135,6 +137,68 @@ export interface GroupEntry {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🆕 표시 항목 타입
|
||||||
|
*/
|
||||||
|
export type DisplayItemType = "icon" | "field" | "text" | "badge";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🆕 빈 값 처리 방식
|
||||||
|
*/
|
||||||
|
export type EmptyBehavior = "hide" | "default" | "blank";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🆕 필드 표시 형식
|
||||||
|
*/
|
||||||
|
export type DisplayFieldFormat = "text" | "date" | "currency" | "number" | "badge";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🆕 표시 항목 정의 (아이콘, 필드, 텍스트, 배지)
|
||||||
|
*/
|
||||||
|
export interface DisplayItem {
|
||||||
|
/** 항목 타입 */
|
||||||
|
type: DisplayItemType;
|
||||||
|
|
||||||
|
/** 고유 ID */
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
// === type: "field" 인 경우 ===
|
||||||
|
/** 필드명 (컬럼명) */
|
||||||
|
fieldName?: string;
|
||||||
|
/** 라벨 (예: "거래처:", "단가:") */
|
||||||
|
label?: string;
|
||||||
|
/** 표시 형식 */
|
||||||
|
format?: DisplayFieldFormat;
|
||||||
|
/** 빈 값일 때 동작 */
|
||||||
|
emptyBehavior?: EmptyBehavior;
|
||||||
|
/** 기본값 (빈 값일 때 표시) */
|
||||||
|
defaultValue?: string;
|
||||||
|
|
||||||
|
// === type: "icon" 인 경우 ===
|
||||||
|
/** 아이콘 이름 (lucide-react 아이콘명) */
|
||||||
|
icon?: string;
|
||||||
|
|
||||||
|
// === type: "text" 인 경우 ===
|
||||||
|
/** 텍스트 내용 */
|
||||||
|
value?: string;
|
||||||
|
|
||||||
|
// === type: "badge" 인 경우 ===
|
||||||
|
/** 배지 스타일 */
|
||||||
|
badgeVariant?: "default" | "secondary" | "destructive" | "outline";
|
||||||
|
|
||||||
|
// === 공통 스타일 ===
|
||||||
|
/** 굵게 표시 */
|
||||||
|
bold?: boolean;
|
||||||
|
/** 밑줄 표시 */
|
||||||
|
underline?: boolean;
|
||||||
|
/** 기울임 표시 */
|
||||||
|
italic?: boolean;
|
||||||
|
/** 텍스트 색상 */
|
||||||
|
color?: string;
|
||||||
|
/** 배경 색상 */
|
||||||
|
backgroundColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 🆕 품목 + 그룹별 여러 입력 항목
|
* 🆕 품목 + 그룹별 여러 입력 항목
|
||||||
* 각 필드 그룹마다 독립적으로 여러 개의 입력을 추가할 수 있음
|
* 각 필드 그룹마다 독립적으로 여러 개의 입력을 추가할 수 있음
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue