diff --git a/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx b/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx index a70d8e74..da16a349 100644 --- a/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx +++ b/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx @@ -3,16 +3,18 @@ import React, { useState, useEffect, useMemo, useCallback } from "react"; import { useSearchParams } from "next/navigation"; 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 { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Checkbox } from "@/components/ui/checkbox"; import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { X } from "lucide-react"; +import * as LucideIcons from "lucide-react"; import { commonCodeApi } from "@/lib/api/commonCode"; import { cn } from "@/lib/utils"; @@ -542,6 +544,158 @@ export const SelectedItemsDetailInputComponent: React.FC { + const displayItems = componentConfig.displayItems || []; + + if (displayItems.length === 0) { + // displayItemsκ°€ μ—†μœΌλ©΄ κΈ°λ³Έ 방식 (λͺ¨λ“  ν•„λ“œ λ‚˜μ—΄) + const fields = componentConfig.additionalFields || []; + 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 ( + + ); + } + + case "text": + return ( + + {displayItem.value} + + ); + + 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 ( + + {displayItem.label}{defaultValue} + + ); + case "blank": + default: + // 빈 칸으둜 ν‘œμ‹œ + return ( + + {displayItem.label} + + ); + } + } + + // 값이 μžˆλŠ” 경우, ν˜•μ‹μ— 맞게 ν‘œμ‹œ + 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 ( + + {displayItem.label}{formattedValue} + + ); + case "text": + default: + // 일반 ν…μŠ€νŠΈ + break; + } + + return ( + + {displayItem.label}{formattedValue} + + ); + } + + case "badge": { + const fieldValue = displayItem.fieldName ? entry[displayItem.fieldName] : displayItem.value; + return ( + + {displayItem.label}{fieldValue} + + ); + } + + default: + return null; + } + })} + + ); + }, [componentConfig.displayItems, componentConfig.additionalFields]); + // 빈 μƒνƒœ λ Œλ”λ§ if (items.length === 0) { return ( @@ -650,8 +804,8 @@ export const SelectedItemsDetailInputComponent: React.FC handleEditGroupEntry(item.id, group.id, entry.id)} > - - {idx + 1}. {groupFields.map((f) => entry[f.name] || "-").join(" / ")} + + {idx + 1}. {renderDisplayItems(entry, item)} + + + + + +

+ 각 μž…λ ₯ ν•­λͺ©μ΄ μΆ”κ°€λ˜λ©΄ μ–΄λ–»κ²Œ ν‘œμ‹œλ μ§€ μ„€μ •ν•©λ‹ˆλ‹€ +

+ + {displayItems.length === 0 ? ( +
+ ν‘œμ‹œ ν•­λͺ©μ΄ μ—†μŠ΅λ‹ˆλ‹€. μœ„μ˜ λ²„νŠΌμœΌλ‘œ μΆ”κ°€ν•˜μ„Έμš”. +
+ (λ―Έμ„€μ • μ‹œ λͺ¨λ“  ν•„λ“œλ₯Ό " / "둜 κ΅¬λΆ„ν•˜μ—¬ ν‘œμ‹œ) +
+ ) : ( +
+ {displayItems.map((item, index) => ( + + + {/* 헀더 */} +
+ + {item.type === "icon" && "🎨 μ•„μ΄μ½˜"} + {item.type === "field" && "πŸ“ ν•„λ“œ"} + {item.type === "text" && "πŸ’¬ ν…μŠ€νŠΈ"} + {item.type === "badge" && "🏷️ λ°°μ§€"} + + +
+ + {/* μ•„μ΄μ½˜ μ„€μ • */} + {item.type === "icon" && ( +
+
+ + updateDisplayItem(index, { icon: e.target.value })} + placeholder="예: Building, User, Package" + className="h-7 text-xs" + /> +

+ lucide-react μ•„μ΄μ½˜ 이름을 μž…λ ₯ν•˜μ„Έμš” +

+
+
+ )} + + {/* ν…μŠ€νŠΈ μ„€μ • */} + {item.type === "text" && ( +
+ + updateDisplayItem(index, { value: e.target.value })} + placeholder="예: | , / , - " + className="h-7 text-xs" + /> +
+ )} + + {/* ν•„λ“œ μ„€μ • */} + {item.type === "field" && ( +
+ {/* ν•„λ“œ 선택 */} +
+ + +
+ + {/* 라벨 */} +
+ + updateDisplayItem(index, { label: e.target.value })} + placeholder="예: 거래처:, 단가:" + className="h-7 text-xs" + /> +
+ + {/* ν‘œμ‹œ ν˜•μ‹ */} +
+ + +
+ + {/* 빈 κ°’ 처리 */} +
+ + +
+ + {/* κΈ°λ³Έκ°’ (emptyBehaviorκ°€ "default"일 λ•Œλ§Œ) */} + {item.emptyBehavior === "default" && ( +
+ + updateDisplayItem(index, { defaultValue: e.target.value })} + placeholder="예: λ―Έμž…λ ₯, 0, -" + className="h-7 text-xs" + /> +
+ )} +
+ )} + + {/* μŠ€νƒ€μΌ μ„€μ • */} +
+ +
+ + + +
+ + {/* 색상 */} +
+
+ + updateDisplayItem(index, { color: e.target.value })} + className="h-7 w-full" + /> +
+
+ + updateDisplayItem(index, { backgroundColor: e.target.value })} + className="h-7 w-full" + /> +
+
+
+
+
+ ))} +
+ )} + + {/* μ‚¬μš© μ˜ˆμ‹œ */}

πŸ’‘ μ‚¬μš© μ˜ˆμ‹œ

diff --git a/frontend/lib/registry/components/selected-items-detail-input/types.ts b/frontend/lib/registry/components/selected-items-detail-input/types.ts index c8a67a62..6685fc50 100644 --- a/frontend/lib/registry/components/selected-items-detail-input/types.ts +++ b/frontend/lib/registry/components/selected-items-detail-input/types.ts @@ -115,6 +115,12 @@ export interface SelectedItemsDetailInputConfig extends ComponentConfig { */ inputMode?: "inline" | "modal"; + /** + * πŸ†• ν•­λͺ© ν‘œμ‹œ μ„€μ • (각 그룹의 μž…λ ₯ ν•­λͺ©μ„ μ–΄λ–»κ²Œ ν‘œμ‹œν• μ§€) + * 예: [{ type: "icon", icon: "Building" }, { type: "field", fieldName: "customer_name", label: "거래처:" }] + */ + displayItems?: DisplayItem[]; + /** * 빈 μƒνƒœ λ©”μ‹œμ§€ */ @@ -135,6 +141,68 @@ export interface GroupEntry { [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; +} + /** * πŸ†• ν’ˆλͺ© + 그룹별 μ—¬λŸ¬ μž…λ ₯ ν•­λͺ© * 각 ν•„λ“œ κ·Έλ£Ήλ§ˆλ‹€ λ…λ¦½μ μœΌλ‘œ μ—¬λŸ¬ 개의 μž…λ ₯을 μΆ”κ°€ν•  수 있음