refactor: Update ComponentsPanel and SelectedItemsDetailInputComponent for improved functionality
- Updated ComponentsPanel to clarify the usage of the "selected-items-detail-input" component, indicating its application in the context of adding items for clients. - Enhanced SelectedItemsDetailInputComponent by introducing independent editing states for group entries, allowing for better management of item edits within groups. - Adjusted input field heights and styles for consistency and improved user experience. - Added a new property `maxEntries` to the FieldGroup interface to support 1:1 relationships and automatic entry generation. - Implemented overflow support for the component to handle cases with many items, ensuring a smoother user interface.
This commit is contained in:
@@ -94,8 +94,11 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
// 🆕 입력 모드 상태 (modal 모드일 때 사용)
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editingItemId, setEditingItemId] = useState<string | null>(null); // 현재 편집 중인 품목 ID
|
||||
const [editingGroupId, setEditingGroupId] = useState<string | null>(null); // 현재 편집 중인 그룹 ID
|
||||
const [editingDetailId, setEditingDetailId] = useState<string | null>(null); // 현재 편집 중인 항목 ID
|
||||
const [editingGroupId, setEditingGroupId] = useState<string | null>(null); // 현재 편집 중인 그룹 ID (레거시 호환)
|
||||
const [editingDetailId, setEditingDetailId] = useState<string | null>(null); // 현재 편집 중인 항목 ID (레거시 호환)
|
||||
|
||||
// 🆕 그룹별 독립 편집 상태: { [groupId]: entryId }
|
||||
const [editingEntries, setEditingEntries] = useState<Record<string, string | null>>({});
|
||||
|
||||
// 🆕 코드 카테고리별 옵션 캐싱
|
||||
const [codeOptions, setCodeOptions] = useState<Record<string, Array<{ label: string; value: string }>>>({});
|
||||
@@ -404,9 +407,14 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
const newItems: ItemData[] = modalData.map((item) => {
|
||||
const fieldGroups: Record<string, GroupEntry[]> = {};
|
||||
|
||||
// 각 그룹에 대해 빈 배열 초기화
|
||||
// 각 그룹에 대해 초기화 (maxEntries === 1이면 자동 1개 생성)
|
||||
groups.forEach((group) => {
|
||||
fieldGroups[group.id] = [];
|
||||
if (group.maxEntries === 1) {
|
||||
// 1:1 관계: 빈 entry 1개 자동 생성
|
||||
fieldGroups[group.id] = [{ id: `${group.id}_auto_1` }];
|
||||
} else {
|
||||
fieldGroups[group.id] = [];
|
||||
}
|
||||
});
|
||||
|
||||
// 그룹이 없으면 기본 그룹 생성
|
||||
@@ -757,6 +765,7 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
const componentStyle: React.CSSProperties = {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
overflowY: "auto", // 항목이 많을 때 스크롤 지원
|
||||
...component.style,
|
||||
...style,
|
||||
};
|
||||
@@ -976,6 +985,8 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
setEditingItemId(itemId);
|
||||
setEditingDetailId(newEntryId);
|
||||
setEditingGroupId(groupId);
|
||||
// 그룹별 독립 편집: 해당 그룹만 열기 (다른 그룹은 유지)
|
||||
setEditingEntries((prev) => ({ ...prev, [groupId]: newEntryId }));
|
||||
};
|
||||
|
||||
// 🆕 그룹 항목 제거 핸들러
|
||||
@@ -992,14 +1003,34 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
};
|
||||
}),
|
||||
);
|
||||
// 제거된 항목이 편집 중이었으면 해당 그룹 편집 닫기
|
||||
setEditingEntries((prev) => {
|
||||
if (prev[groupId] === entryId) {
|
||||
const next = { ...prev };
|
||||
delete next[groupId];
|
||||
return next;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
};
|
||||
|
||||
// 🆕 그룹 항목 편집 핸들러 (클릭하면 수정 가능)
|
||||
// 🆕 그룹 항목 편집 핸들러 (클릭하면 수정 가능) - 독립 편집
|
||||
const handleEditGroupEntry = (itemId: string, groupId: string, entryId: string) => {
|
||||
setIsEditing(true);
|
||||
setEditingItemId(itemId);
|
||||
setEditingGroupId(groupId);
|
||||
setEditingDetailId(entryId);
|
||||
// 그룹별 독립 편집: 해당 그룹만 토글 (다른 그룹은 유지)
|
||||
setEditingEntries((prev) => ({ ...prev, [groupId]: entryId }));
|
||||
};
|
||||
|
||||
// 🆕 특정 그룹의 편집 닫기 (다른 그룹은 유지)
|
||||
const closeGroupEditing = (groupId: string) => {
|
||||
setEditingEntries((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[groupId];
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// 🆕 다음 품목으로 이동
|
||||
@@ -1059,7 +1090,7 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
type="text"
|
||||
onChange={(e) => handleFieldChange(itemId, groupId, entryId, field.name, e.target.value)}
|
||||
maxLength={field.validation?.maxLength}
|
||||
className="h-10 text-sm"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1081,12 +1112,12 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
readOnly
|
||||
disabled
|
||||
className={cn(
|
||||
"h-10 text-sm",
|
||||
"h-7 text-xs",
|
||||
"bg-primary/10 border-primary/30 text-primary font-semibold",
|
||||
"cursor-not-allowed",
|
||||
)}
|
||||
/>
|
||||
<div className="text-primary/70 absolute top-1/2 right-2 -translate-y-1/2 text-[10px]">자동 계산</div>
|
||||
<div className="text-primary/70 absolute top-1/2 right-2 -translate-y-1/2 text-[9px]">자동 계산</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1098,7 +1129,7 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
onChange={(e) => handleFieldChange(itemId, groupId, entryId, field.name, e.target.value)}
|
||||
min={field.validation?.min}
|
||||
max={field.validation?.max}
|
||||
className="h-10 text-sm"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1117,7 +1148,7 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
target.showPicker();
|
||||
}
|
||||
}}
|
||||
className="h-10 cursor-pointer text-sm"
|
||||
className="h-7 cursor-pointer text-xs"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1137,8 +1168,8 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
<Textarea
|
||||
{...commonProps}
|
||||
onChange={(e) => handleFieldChange(itemId, groupId, entryId, field.name, e.target.value)}
|
||||
rows={2}
|
||||
className="resize-none text-xs sm:text-sm"
|
||||
rows={1}
|
||||
className="min-h-[28px] resize-none text-xs"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1163,7 +1194,7 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
onValueChange={(val) => handleFieldChange(itemId, groupId, entryId, field.name, val)}
|
||||
disabled={componentConfig.disabled || componentConfig.readonly}
|
||||
>
|
||||
<SelectTrigger size="default" className="w-full">
|
||||
<SelectTrigger size="default" className="h-7 w-full text-xs">
|
||||
<SelectValue placeholder={field.placeholder || "선택하세요"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -1264,28 +1295,42 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
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
|
||||
// displayItems가 없으면 기본 방식 (해당 그룹의 visible 필드만 나열)
|
||||
const fields = (componentConfig.additionalFields || []).filter((f) => {
|
||||
// 그룹 필터
|
||||
const matchGroup = componentConfig.fieldGroups && componentConfig.fieldGroups.length > 0
|
||||
? f.groupId === groupId
|
||||
: true;
|
||||
// hidden 필드 제외 (width: "0px"인 필드)
|
||||
const isVisible = f.width !== "0px";
|
||||
return matchGroup && isVisible;
|
||||
});
|
||||
|
||||
// 값이 있는 필드만 "라벨: 값" 형식으로 표시
|
||||
const displayParts = fields
|
||||
.map((f) => {
|
||||
const value = entry[f.name];
|
||||
if (!value) return "-";
|
||||
if (!value && value !== 0) return null;
|
||||
|
||||
const strValue = String(value);
|
||||
|
||||
// 🔧 ISO 날짜 형식 자동 감지 및 포맷팅 (필드 타입 무관)
|
||||
// ISO 8601 형식: YYYY-MM-DDTHH:mm:ss.sssZ 또는 YYYY-MM-DD
|
||||
// ISO 날짜 형식 자동 포맷팅
|
||||
const isoDateMatch = strValue.match(/^(\d{4})-(\d{2})-(\d{2})(T|\s|$)/);
|
||||
if (isoDateMatch) {
|
||||
const [, year, month, day] = isoDateMatch;
|
||||
return `${year}.${month}.${day}`;
|
||||
return `${f.label}: ${year}.${month}.${day}`;
|
||||
}
|
||||
|
||||
return strValue;
|
||||
return `${f.label}: ${strValue}`;
|
||||
})
|
||||
.join(" / ");
|
||||
.filter(Boolean);
|
||||
|
||||
// 빈 항목 표시: 그룹 필드명으로 안내 메시지 생성
|
||||
if (displayParts.length === 0) {
|
||||
const fieldLabels = fields.slice(0, 2).map(f => f.label).join("/");
|
||||
return `신규 ${fieldLabels} 입력`;
|
||||
}
|
||||
return displayParts.join(" ");
|
||||
}
|
||||
|
||||
// displayItems 설정대로 렌더링
|
||||
@@ -1459,15 +1504,117 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
|
||||
// 빈 상태 렌더링
|
||||
if (items.length === 0) {
|
||||
// 디자인 모드: 샘플 데이터로 미리보기 표시
|
||||
if (isDesignMode) {
|
||||
const sampleDisplayCols = componentConfig.displayColumns || [];
|
||||
const sampleFields = (componentConfig.additionalFields || []).filter(f => f.name !== "item_id" && f.width !== "0px");
|
||||
const sampleGroups = componentConfig.fieldGroups || [{ id: "default", title: "입력 정보", order: 0 }];
|
||||
const gridCols = sampleGroups.length === 1 ? "grid-cols-1" : "grid-cols-2";
|
||||
|
||||
return (
|
||||
<div style={componentStyle} className={className} onClick={handleClick}>
|
||||
<div className="bg-card space-y-3 p-3">
|
||||
{/* 미리보기 안내 배너 */}
|
||||
<div className="bg-muted/50 flex items-center gap-2 rounded-md px-3 py-2 text-xs">
|
||||
<span className="text-primary font-medium">[미리보기]</span>
|
||||
<span className="text-muted-foreground">실제 데이터가 전달되면 아래와 같은 형태로 표시됩니다</span>
|
||||
</div>
|
||||
|
||||
{/* 샘플 품목 카드 2개 */}
|
||||
{[1, 2].map((idx) => (
|
||||
<Card key={idx} className="border shadow-sm">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center justify-between text-sm font-semibold">
|
||||
<span>
|
||||
{idx}. {sampleDisplayCols.length > 0 ? `샘플 ${sampleDisplayCols[0]?.label || "품목"} ${idx}` : `샘플 항목 ${idx}`}
|
||||
</span>
|
||||
<Button type="button" variant="ghost" size="sm" className="h-6 w-6 p-0 text-red-400" disabled>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</CardTitle>
|
||||
{sampleDisplayCols.length > 0 && (
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{sampleDisplayCols.map((col, i) => (
|
||||
<span key={col.name}>
|
||||
{i > 0 && " | "}
|
||||
<span className="text-muted-foreground/60">{col.label}: 샘플값</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<div className={`grid ${gridCols} gap-2`}>
|
||||
{sampleGroups.map((group) => {
|
||||
const groupFields = sampleFields.filter(f =>
|
||||
sampleGroups.length <= 1 || f.groupId === group.id
|
||||
);
|
||||
if (groupFields.length === 0) return null;
|
||||
const isSingle = group.maxEntries === 1;
|
||||
|
||||
return (
|
||||
<Card key={group.id} className="border shadow-sm">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center justify-between text-xs font-semibold">
|
||||
<span>{group.title}</span>
|
||||
{!isSingle && (
|
||||
<Button type="button" size="sm" variant="outline" className="h-6 text-[10px]" disabled>
|
||||
+ 추가
|
||||
</Button>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1 pb-3">
|
||||
{isSingle ? (
|
||||
/* 1:1 그룹: 인라인 폼 미리보기 */
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{groupFields.slice(0, 4).map(f => (
|
||||
<div key={f.name} className="space-y-0.5">
|
||||
<span className="text-muted-foreground text-[9px]">{f.label}</span>
|
||||
<div className="bg-muted/40 h-5 rounded border text-[10px] leading-5 px-1">샘플</div>
|
||||
</div>
|
||||
))}
|
||||
{groupFields.length > 4 && (
|
||||
<div className="text-muted-foreground col-span-2 text-[9px]">외 {groupFields.length - 4}개 필드</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
/* 1:N 그룹: 다중 항목 미리보기 */
|
||||
<>
|
||||
<div className="bg-muted/30 flex items-center justify-between rounded border p-1.5 text-[10px]">
|
||||
<span className="truncate">
|
||||
1. {groupFields.slice(0, 2).map(f => `${f.label}: 샘플`).join(" / ")}
|
||||
</span>
|
||||
<X className="h-2.5 w-2.5 shrink-0 text-gray-400" />
|
||||
</div>
|
||||
{idx === 1 && (
|
||||
<div className="bg-muted/30 flex items-center justify-between rounded border p-1.5 text-[10px]">
|
||||
<span className="truncate">
|
||||
2. {groupFields.slice(0, 2).map(f => `${f.label}: 샘플`).join(" / ")}
|
||||
</span>
|
||||
<X className="h-2.5 w-2.5 shrink-0 text-gray-400" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 런타임 빈 상태
|
||||
return (
|
||||
<div style={componentStyle} className={className} onClick={handleClick}>
|
||||
<div className="border-border bg-muted/30 flex flex-col items-center justify-center rounded-lg border-2 border-dashed p-8 text-center">
|
||||
<p className="text-muted-foreground text-sm">{componentConfig.emptyMessage}</p>
|
||||
{isDesignMode && (
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
💡 이전 모달에서 "다음" 버튼으로 데이터를 전달하면 여기에 표시됩니다.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1483,141 +1630,147 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
|
||||
const sortedGroups = [...effectiveGroups].sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||
|
||||
// 그룹 수에 따라 grid 열 수 결정
|
||||
const gridCols = sortedGroups.length === 1 ? "grid-cols-1" : "grid-cols-2";
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className={`grid ${gridCols} gap-3`}>
|
||||
{sortedGroups.map((group) => {
|
||||
const groupFields = fields.filter((f) => (groups.length === 0 ? true : f.groupId === group.id));
|
||||
|
||||
if (groupFields.length === 0) return null;
|
||||
|
||||
const groupEntries = item.fieldGroups[group.id] || [];
|
||||
const isEditingThisGroup = isEditing && editingItemId === item.id && editingGroupId === group.id;
|
||||
// 그룹별 독립 편집 상태 사용
|
||||
const editingEntryIdForGroup = editingEntries[group.id] || null;
|
||||
|
||||
// 1:1 관계 그룹 (maxEntries === 1): 인라인 폼으로 바로 표시
|
||||
const isSingleEntry = group.maxEntries === 1;
|
||||
const singleEntry = isSingleEntry ? (groupEntries[0] || { id: `${group.id}_auto_1` }) : null;
|
||||
// hidden 필드 제외 (width: "0px"인 필드)
|
||||
const visibleFields = groupFields.filter((f) => f.width !== "0px");
|
||||
|
||||
return (
|
||||
<Card key={group.id} className="border-2 shadow-sm">
|
||||
<CardHeader className="pb-3">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center justify-between text-sm font-semibold">
|
||||
<span>{group.title}</span>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => handleAddGroupEntry(item.id, group.id)}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
+ 추가
|
||||
</Button>
|
||||
{/* 1:N 그룹만 + 추가 버튼 표시 */}
|
||||
{!isSingleEntry && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => handleAddGroupEntry(item.id, group.id)}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-6 text-[11px]"
|
||||
>
|
||||
+ 추가
|
||||
</Button>
|
||||
)}
|
||||
</CardTitle>
|
||||
{group.description && <p className="text-muted-foreground text-xs">{group.description}</p>}
|
||||
{group.description && <p className="text-muted-foreground text-[11px]">{group.description}</p>}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{/* 이미 입력된 항목들 */}
|
||||
{groupEntries.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{groupEntries.map((entry, idx) => {
|
||||
const isEditingThisEntry = isEditingThisGroup && editingDetailId === entry.id;
|
||||
|
||||
if (isEditingThisEntry) {
|
||||
// 편집 모드: 입력 필드 표시 (가로 배치)
|
||||
return (
|
||||
<Card key={entry.id} className="border-primary border-dashed">
|
||||
<CardContent className="p-3">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<span className="text-xs font-medium">수정 중</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setIsEditing(false);
|
||||
setEditingItemId(null);
|
||||
setEditingGroupId(null);
|
||||
setEditingDetailId(null);
|
||||
}}
|
||||
className="h-6 text-xs"
|
||||
>
|
||||
완료
|
||||
</Button>
|
||||
</div>
|
||||
{/* 🆕 가로 Grid 배치 (2~3열) */}
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{groupFields.map((field) => (
|
||||
<div key={field.name} className="space-y-1">
|
||||
<label className="text-xs font-medium">
|
||||
{field.label}
|
||||
{field.required && <span className="text-destructive ml-1">*</span>}
|
||||
</label>
|
||||
{renderField(field, item.id, group.id, entry.id, entry)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
} else {
|
||||
// 읽기 모드: 텍스트 표시 (클릭하면 수정)
|
||||
return (
|
||||
<div
|
||||
key={entry.id}
|
||||
className="bg-muted/30 hover:bg-muted/50 flex cursor-pointer items-center justify-between rounded border p-2 text-xs"
|
||||
onClick={() => handleEditGroupEntry(item.id, group.id, entry.id)}
|
||||
>
|
||||
<span className="flex items-center gap-1">
|
||||
{idx + 1}. {renderDisplayItems(entry, item, group.id)}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRemoveGroupEntry(item.id, group.id, entry.id);
|
||||
}}
|
||||
className="h-6 w-6 p-0"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
<CardContent className="space-y-1.5 pt-0">
|
||||
{/* === 1:1 그룹: 인라인 폼 (항상 편집 모드) - 컴팩트 === */}
|
||||
{isSingleEntry && singleEntry && (
|
||||
<div className="grid grid-cols-2 gap-x-2 gap-y-1.5">
|
||||
{visibleFields.map((field) => (
|
||||
<div key={field.name} className={cn(
|
||||
"space-y-0.5",
|
||||
field.type === "textarea" && "col-span-2"
|
||||
)}>
|
||||
<label className="text-[11px] font-medium leading-none">
|
||||
{field.label}
|
||||
{field.required && <span className="text-destructive ml-0.5">*</span>}
|
||||
</label>
|
||||
{renderField(field, item.id, group.id, singleEntry.id, singleEntry)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-xs italic">아직 입력된 항목이 없습니다.</p>
|
||||
)}
|
||||
|
||||
{/* 새 항목 입력 중 */}
|
||||
{isEditingThisGroup && editingDetailId && !groupEntries.find((e) => e.id === editingDetailId) && (
|
||||
<Card className="border-primary border-dashed">
|
||||
<CardContent className="space-y-2 p-3">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="text-xs font-medium">새 항목</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setIsEditing(false);
|
||||
setEditingItemId(null);
|
||||
setEditingGroupId(null);
|
||||
setEditingDetailId(null);
|
||||
}}
|
||||
className="h-6 text-xs"
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
{/* === 1:N 그룹: 다중 입력 (독립 편집) === */}
|
||||
{!isSingleEntry && (
|
||||
<>
|
||||
{groupEntries.length > 0 ? (
|
||||
<div className="space-y-1.5">
|
||||
{groupEntries.map((entry, idx) => {
|
||||
// 그룹별 독립 편집 상태 확인
|
||||
const isEditingThisEntry = editingEntryIdForGroup === entry.id;
|
||||
|
||||
return (
|
||||
<div key={entry.id} className="bg-muted/30 rounded border">
|
||||
{/* 헤더 (항상 표시) */}
|
||||
<div
|
||||
className="hover:bg-muted/50 flex cursor-pointer items-center justify-between px-2 py-1.5 text-xs"
|
||||
onClick={() => {
|
||||
if (isEditingThisEntry) {
|
||||
// 이 그룹만 닫기 (다른 그룹은 유지)
|
||||
closeGroupEditing(group.id);
|
||||
} else {
|
||||
handleEditGroupEntry(item.id, group.id, entry.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center gap-1">
|
||||
{idx + 1}. {renderDisplayItems(entry, item, group.id)}
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRemoveGroupEntry(item.id, group.id, entry.id);
|
||||
}}
|
||||
className="h-5 w-5 p-0"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 폼 영역 (편집 시에만 아래로 펼침) - 컴팩트 */}
|
||||
{isEditingThisEntry && (
|
||||
<div className="border-t px-2 pb-2 pt-1.5">
|
||||
<div className="grid grid-cols-2 gap-x-2 gap-y-1.5">
|
||||
{visibleFields.map((field) => (
|
||||
<div key={field.name} className={cn(
|
||||
"space-y-0.5",
|
||||
field.type === "textarea" && "col-span-2"
|
||||
)}>
|
||||
<label className="text-[11px] font-medium leading-none">
|
||||
{field.label}
|
||||
{field.required && <span className="text-destructive ml-0.5">*</span>}
|
||||
</label>
|
||||
{renderField(field, item.id, group.id, entry.id, entry)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-1.5 flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => closeGroupEditing(group.id)}
|
||||
className="h-6 px-3 text-[11px]"
|
||||
>
|
||||
완료
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{groupFields.map((field) => (
|
||||
<div key={field.name} className="space-y-1">
|
||||
<label className="text-xs font-medium">
|
||||
{field.label}
|
||||
{field.required && <span className="text-destructive ml-1">*</span>}
|
||||
</label>
|
||||
{renderField(field, item.id, group.id, editingDetailId, {})}
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-xs italic">아직 입력된 항목이 없습니다.</p>
|
||||
)}
|
||||
|
||||
{/* 새 항목은 handleAddGroupEntry에서 아코디언 항목으로 직접 추가됨 */}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user