상세입력 컴포넌트 테이블 선택 기능 추가
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useMemo, useCallback } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { ComponentRendererProps } from "@/types/component";
|
||||
import { SelectedItemsDetailInputConfig, AdditionalFieldDefinition } from "./types";
|
||||
import { useModalDataStore, ModalDataItem } from "@/stores/modalDataStore";
|
||||
@@ -12,6 +13,7 @@ import { Button } from "@/components/ui/button";
|
||||
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 { commonCodeApi } from "@/lib/api/commonCode";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface SelectedItemsDetailInputComponentProps extends ComponentRendererProps {
|
||||
@@ -38,6 +40,10 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
screenId,
|
||||
...props
|
||||
}) => {
|
||||
// 🆕 URL 파라미터에서 dataSourceId 읽기
|
||||
const searchParams = useSearchParams();
|
||||
const urlDataSourceId = searchParams?.get("dataSourceId") || undefined;
|
||||
|
||||
// 컴포넌트 설정
|
||||
const componentConfig = useMemo(() => ({
|
||||
dataSourceId: component.id || "default",
|
||||
@@ -52,13 +58,22 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
...component.config,
|
||||
} as SelectedItemsDetailInputConfig), [config, component.config, component.id]);
|
||||
|
||||
// 모달 데이터 스토어에서 데이터 가져오기
|
||||
// dataSourceId를 안정적으로 유지
|
||||
// 🆕 dataSourceId 우선순위: URL 파라미터 > 컴포넌트 설정 > component.id
|
||||
const dataSourceId = useMemo(
|
||||
() => componentConfig.dataSourceId || component.id || "default",
|
||||
[componentConfig.dataSourceId, component.id]
|
||||
() => urlDataSourceId || componentConfig.dataSourceId || component.id || "default",
|
||||
[urlDataSourceId, componentConfig.dataSourceId, component.id]
|
||||
);
|
||||
|
||||
// 디버깅 로그
|
||||
useEffect(() => {
|
||||
console.log("📍 [SelectedItemsDetailInput] dataSourceId 결정:", {
|
||||
urlDataSourceId,
|
||||
configDataSourceId: componentConfig.dataSourceId,
|
||||
componentId: component.id,
|
||||
finalDataSourceId: dataSourceId,
|
||||
});
|
||||
}, [urlDataSourceId, componentConfig.dataSourceId, component.id, dataSourceId]);
|
||||
|
||||
// 전체 레지스트리를 가져와서 컴포넌트 내부에서 필터링 (캐싱 문제 회피)
|
||||
const dataRegistry = useModalDataStore((state) => state.dataRegistry);
|
||||
const modalData = useMemo(
|
||||
@@ -70,6 +85,79 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
|
||||
// 로컬 상태로 데이터 관리
|
||||
const [items, setItems] = useState<ModalDataItem[]>([]);
|
||||
|
||||
// 🆕 코드 카테고리별 옵션 캐싱
|
||||
const [codeOptions, setCodeOptions] = useState<Record<string, Array<{ label: string; value: string }>>>({});
|
||||
|
||||
// 🆕 필드에 codeCategory가 있으면 자동으로 옵션 로드
|
||||
useEffect(() => {
|
||||
const loadCodeOptions = async () => {
|
||||
// 🆕 code/category 타입 필드 + codeCategory가 있는 필드 모두 처리
|
||||
const codeFields = componentConfig.additionalFields?.filter(
|
||||
(field) => field.inputType === "code" || field.inputType === "category"
|
||||
);
|
||||
|
||||
if (!codeFields || codeFields.length === 0) return;
|
||||
|
||||
const newOptions: Record<string, Array<{ label: string; value: string }>> = { ...codeOptions };
|
||||
|
||||
// 🆕 대상 테이블의 컬럼 메타데이터에서 codeCategory 가져오기
|
||||
const targetTable = componentConfig.targetTable;
|
||||
let targetTableColumns: any[] = [];
|
||||
|
||||
if (targetTable) {
|
||||
try {
|
||||
const { tableTypeApi } = await import("@/lib/api/screen");
|
||||
const columnsResponse = await tableTypeApi.getColumns(targetTable);
|
||||
targetTableColumns = columnsResponse || [];
|
||||
} catch (error) {
|
||||
console.error("❌ 대상 테이블 컬럼 조회 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
for (const field of codeFields) {
|
||||
// 이미 codeCategory가 있으면 사용
|
||||
let codeCategory = field.codeCategory;
|
||||
|
||||
// 🆕 codeCategory가 없으면 대상 테이블 컬럼에서 찾기
|
||||
if (!codeCategory && targetTableColumns.length > 0) {
|
||||
const columnMeta = targetTableColumns.find(
|
||||
(col: any) => (col.columnName || col.column_name) === field.name
|
||||
);
|
||||
if (columnMeta) {
|
||||
codeCategory = columnMeta.codeCategory || columnMeta.code_category;
|
||||
console.log(`🔍 필드 "${field.name}"의 codeCategory를 메타데이터에서 찾음:`, codeCategory);
|
||||
}
|
||||
}
|
||||
|
||||
if (!codeCategory) {
|
||||
console.warn(`⚠️ 필드 "${field.name}"의 codeCategory를 찾을 수 없습니다`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 이미 로드된 옵션이면 스킵
|
||||
if (newOptions[codeCategory]) continue;
|
||||
|
||||
try {
|
||||
const response = await commonCodeApi.options.getOptions(codeCategory);
|
||||
if (response.success && response.data) {
|
||||
newOptions[codeCategory] = response.data.map((opt) => ({
|
||||
label: opt.label,
|
||||
value: opt.value,
|
||||
}));
|
||||
console.log(`✅ 코드 옵션 로드 완료: ${codeCategory}`, newOptions[codeCategory]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ 코드 옵션 로드 실패: ${codeCategory}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
setCodeOptions(newOptions);
|
||||
};
|
||||
|
||||
loadCodeOptions();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [componentConfig.additionalFields, componentConfig.targetTable]);
|
||||
|
||||
// 모달 데이터가 변경되면 로컬 상태 업데이트
|
||||
useEffect(() => {
|
||||
@@ -151,7 +239,130 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
required: field.required,
|
||||
};
|
||||
|
||||
switch (field.type) {
|
||||
// 🆕 inputType이 있으면 우선 사용, 없으면 field.type 사용
|
||||
const renderType = field.inputType || field.type;
|
||||
|
||||
// 🆕 inputType에 따라 적절한 컴포넌트 렌더링
|
||||
switch (renderType) {
|
||||
// 기본 타입들
|
||||
case "text":
|
||||
case "varchar":
|
||||
case "char":
|
||||
return (
|
||||
<Input
|
||||
{...commonProps}
|
||||
type="text"
|
||||
onChange={(e) => handleFieldChange(item.id, field.name, e.target.value)}
|
||||
maxLength={field.validation?.maxLength}
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
/>
|
||||
);
|
||||
|
||||
case "number":
|
||||
case "int":
|
||||
case "integer":
|
||||
case "bigint":
|
||||
case "decimal":
|
||||
case "numeric":
|
||||
return (
|
||||
<Input
|
||||
{...commonProps}
|
||||
type="number"
|
||||
onChange={(e) => handleFieldChange(item.id, field.name, e.target.value)}
|
||||
min={field.validation?.min}
|
||||
max={field.validation?.max}
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
/>
|
||||
);
|
||||
|
||||
case "date":
|
||||
case "timestamp":
|
||||
case "datetime":
|
||||
return (
|
||||
<Input
|
||||
{...commonProps}
|
||||
type="date"
|
||||
onChange={(e) => handleFieldChange(item.id, field.name, e.target.value)}
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
/>
|
||||
);
|
||||
|
||||
case "checkbox":
|
||||
case "boolean":
|
||||
case "bool":
|
||||
return (
|
||||
<Checkbox
|
||||
checked={value === true || value === "true"}
|
||||
onCheckedChange={(checked) => handleFieldChange(item.id, field.name, checked)}
|
||||
disabled={componentConfig.disabled || componentConfig.readonly}
|
||||
/>
|
||||
);
|
||||
|
||||
case "textarea":
|
||||
return (
|
||||
<Textarea
|
||||
{...commonProps}
|
||||
onChange={(e) => handleFieldChange(item.id, field.name, e.target.value)}
|
||||
rows={2}
|
||||
className="resize-none text-xs sm:text-sm"
|
||||
/>
|
||||
);
|
||||
|
||||
// 🆕 추가 inputType들
|
||||
case "code":
|
||||
case "category":
|
||||
// 🆕 codeCategory를 field.codeCategory 또는 codeOptions에서 찾기
|
||||
let categoryOptions = field.options; // 기본값
|
||||
|
||||
if (field.codeCategory && codeOptions[field.codeCategory]) {
|
||||
categoryOptions = codeOptions[field.codeCategory];
|
||||
} else {
|
||||
// codeCategory가 없으면 모든 codeOptions에서 이 필드에 맞는 옵션 찾기
|
||||
const matchedCategory = Object.keys(codeOptions).find((cat) => {
|
||||
// 필드명과 매칭되는 카테고리 찾기 (예: currency_code → CURRENCY)
|
||||
return field.name.toLowerCase().includes(cat.toLowerCase().replace('_', ''));
|
||||
});
|
||||
if (matchedCategory) {
|
||||
categoryOptions = codeOptions[matchedCategory];
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={value || ""}
|
||||
onValueChange={(val) => handleFieldChange(item.id, field.name, val)}
|
||||
disabled={componentConfig.disabled || componentConfig.readonly}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-full text-xs sm:h-10 sm:text-sm">
|
||||
<SelectValue placeholder={field.placeholder || "선택하세요"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categoryOptions && categoryOptions.length > 0 ? (
|
||||
categoryOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem value="" disabled>
|
||||
옵션 로딩 중...
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
|
||||
case "entity":
|
||||
// TODO: EntitySelect 컴포넌트 사용
|
||||
return (
|
||||
<Input
|
||||
{...commonProps}
|
||||
type="text"
|
||||
onChange={(e) => handleFieldChange(item.id, field.name, e.target.value)}
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
/>
|
||||
);
|
||||
|
||||
case "select":
|
||||
return (
|
||||
<Select
|
||||
@@ -172,48 +383,8 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
</Select>
|
||||
);
|
||||
|
||||
case "textarea":
|
||||
return (
|
||||
<Textarea
|
||||
{...commonProps}
|
||||
onChange={(e) => handleFieldChange(item.id, field.name, e.target.value)}
|
||||
rows={2}
|
||||
className="resize-none text-xs sm:text-sm"
|
||||
/>
|
||||
);
|
||||
|
||||
case "date":
|
||||
return (
|
||||
<Input
|
||||
{...commonProps}
|
||||
type="date"
|
||||
onChange={(e) => handleFieldChange(item.id, field.name, e.target.value)}
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
/>
|
||||
);
|
||||
|
||||
case "number":
|
||||
return (
|
||||
<Input
|
||||
{...commonProps}
|
||||
type="number"
|
||||
onChange={(e) => handleFieldChange(item.id, field.name, e.target.value)}
|
||||
min={field.validation?.min}
|
||||
max={field.validation?.max}
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
/>
|
||||
);
|
||||
|
||||
case "checkbox":
|
||||
return (
|
||||
<Checkbox
|
||||
checked={value === true || value === "true"}
|
||||
onCheckedChange={(checked) => handleFieldChange(item.id, field.name, checked)}
|
||||
disabled={componentConfig.disabled || componentConfig.readonly}
|
||||
/>
|
||||
);
|
||||
|
||||
default: // text
|
||||
// 기본값: 텍스트 입력
|
||||
default:
|
||||
return (
|
||||
<Input
|
||||
{...commonProps}
|
||||
@@ -254,9 +425,9 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
)}
|
||||
|
||||
{/* 원본 데이터 컬럼 */}
|
||||
{componentConfig.displayColumns?.map((colName) => (
|
||||
<TableHead key={colName} className="h-12 px-4 py-3 text-xs font-semibold sm:text-sm">
|
||||
{colName}
|
||||
{componentConfig.displayColumns?.map((col) => (
|
||||
<TableHead key={col.name} className="h-12 px-4 py-3 text-xs font-semibold sm:text-sm">
|
||||
{col.label || col.name}
|
||||
</TableHead>
|
||||
))}
|
||||
|
||||
@@ -284,9 +455,9 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
)}
|
||||
|
||||
{/* 원본 데이터 표시 */}
|
||||
{componentConfig.displayColumns?.map((colName) => (
|
||||
<TableCell key={colName} className="h-14 px-4 py-3 text-xs sm:text-sm">
|
||||
{item.originalData[colName] || "-"}
|
||||
{componentConfig.displayColumns?.map((col) => (
|
||||
<TableCell key={col.name} className="h-14 px-4 py-3 text-xs sm:text-sm">
|
||||
{item.originalData[col.name] || "-"}
|
||||
</TableCell>
|
||||
))}
|
||||
|
||||
@@ -349,10 +520,10 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
|
||||
<CardContent className="space-y-3">
|
||||
{/* 원본 데이터 표시 */}
|
||||
{componentConfig.displayColumns?.map((colName) => (
|
||||
<div key={colName} className="flex items-center justify-between text-xs sm:text-sm">
|
||||
<span className="font-medium text-muted-foreground">{colName}:</span>
|
||||
<span>{item.originalData[colName] || "-"}</span>
|
||||
{componentConfig.displayColumns?.map((col) => (
|
||||
<div key={col.name} className="flex items-center justify-between text-xs sm:text-sm">
|
||||
<span className="font-medium text-muted-foreground">{col.label || col.name}:</span>
|
||||
<span>{item.originalData[col.name] || "-"}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user