상세입력 컴포넌트 테이블 선택 기능 추가
This commit is contained in:
parent
a783317820
commit
bc557c4074
|
|
@ -1068,43 +1068,131 @@ export class ScreenManagementService {
|
|||
[tableName]
|
||||
);
|
||||
|
||||
// column_labels 테이블에서 입력타입 정보 조회 (있는 경우)
|
||||
const webTypeInfo = await query<{
|
||||
// 🆕 table_type_columns에서 입력타입 정보 조회 (회사별만, fallback 없음)
|
||||
// 멀티테넌시: 각 회사는 자신의 설정만 사용, 최고관리자 설정은 별도 관리
|
||||
console.log(`🔍 [getTableColumns] 시작: table=${tableName}, company=${companyCode}`);
|
||||
|
||||
const typeInfo = await query<{
|
||||
column_name: string;
|
||||
input_type: string | null;
|
||||
column_label: string | null;
|
||||
detail_settings: any;
|
||||
}>(
|
||||
`SELECT column_name, input_type, column_label, detail_settings
|
||||
`SELECT column_name, input_type, detail_settings
|
||||
FROM table_type_columns
|
||||
WHERE table_name = $1
|
||||
AND company_code = $2
|
||||
ORDER BY id DESC`, // 최신 레코드 우선 (중복 방지)
|
||||
[tableName, companyCode]
|
||||
);
|
||||
|
||||
console.log(`📊 [getTableColumns] typeInfo 조회 완료: ${typeInfo.length}개`);
|
||||
const currencyCodeType = typeInfo.find(t => t.column_name === 'currency_code');
|
||||
if (currencyCodeType) {
|
||||
console.log(`💰 [getTableColumns] currency_code 발견:`, currencyCodeType);
|
||||
} else {
|
||||
console.log(`⚠️ [getTableColumns] currency_code 없음`);
|
||||
}
|
||||
|
||||
// column_labels 테이블에서 라벨 정보 조회 (우선순위 2)
|
||||
const labelInfo = await query<{
|
||||
column_name: string;
|
||||
column_label: string | null;
|
||||
}>(
|
||||
`SELECT column_name, column_label
|
||||
FROM column_labels
|
||||
WHERE table_name = $1`,
|
||||
[tableName]
|
||||
);
|
||||
|
||||
// 컬럼 정보 매핑
|
||||
return columns.map((column: any) => {
|
||||
const webTypeData = webTypeInfo.find(
|
||||
(wt) => wt.column_name === column.column_name
|
||||
// 🆕 category_column_mapping에서 코드 카테고리 정보 조회
|
||||
const categoryInfo = await query<{
|
||||
physical_column_name: string;
|
||||
logical_column_name: string;
|
||||
}>(
|
||||
`SELECT physical_column_name, logical_column_name
|
||||
FROM category_column_mapping
|
||||
WHERE table_name = $1
|
||||
AND company_code = $2`,
|
||||
[tableName, companyCode]
|
||||
);
|
||||
|
||||
return {
|
||||
// 컬럼 정보 매핑
|
||||
const columnMap = new Map<string, any>();
|
||||
|
||||
// 먼저 information_schema에서 가져온 컬럼들로 기본 맵 생성
|
||||
columns.forEach((column: any) => {
|
||||
columnMap.set(column.column_name, {
|
||||
tableName: tableName,
|
||||
columnName: column.column_name,
|
||||
columnLabel:
|
||||
webTypeData?.column_label ||
|
||||
this.getColumnLabel(column.column_name),
|
||||
dataType: column.data_type,
|
||||
webType:
|
||||
(webTypeData?.input_type as WebType) ||
|
||||
this.inferWebType(column.data_type),
|
||||
isNullable: column.is_nullable,
|
||||
columnDefault: column.column_default || undefined,
|
||||
characterMaximumLength: column.character_maximum_length || undefined,
|
||||
numericPrecision: column.numeric_precision || undefined,
|
||||
numericScale: column.numeric_scale || undefined,
|
||||
detailSettings: webTypeData?.detail_settings || undefined,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`🗺️ [getTableColumns] 기본 columnMap 생성: ${columnMap.size}개`);
|
||||
|
||||
// table_type_columns에서 input_type 추가 (중복 시 최신 것만)
|
||||
const addedTypes = new Set<string>();
|
||||
typeInfo.forEach((type) => {
|
||||
const colName = type.column_name;
|
||||
if (!addedTypes.has(colName) && columnMap.has(colName)) {
|
||||
const col = columnMap.get(colName);
|
||||
col.inputType = type.input_type;
|
||||
col.webType = type.input_type; // webType도 동일하게 설정
|
||||
col.detailSettings = type.detail_settings;
|
||||
addedTypes.add(colName);
|
||||
|
||||
if (colName === 'currency_code') {
|
||||
console.log(`✅ [getTableColumns] currency_code inputType 설정됨: ${type.input_type}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`🏷️ [getTableColumns] inputType 추가 완료: ${addedTypes.size}개`);
|
||||
|
||||
// column_labels에서 라벨 추가
|
||||
labelInfo.forEach((label) => {
|
||||
const col = columnMap.get(label.column_name);
|
||||
if (col) {
|
||||
col.columnLabel = label.column_label || this.getColumnLabel(label.column_name);
|
||||
}
|
||||
});
|
||||
|
||||
// category_column_mapping에서 코드 카테고리 추가
|
||||
categoryInfo.forEach((cat) => {
|
||||
const col = columnMap.get(cat.physical_column_name);
|
||||
if (col) {
|
||||
col.codeCategory = cat.logical_column_name;
|
||||
}
|
||||
});
|
||||
|
||||
// 최종 결과 생성
|
||||
const result = Array.from(columnMap.values()).map((col) => ({
|
||||
...col,
|
||||
// 기본값 설정
|
||||
columnLabel: col.columnLabel || this.getColumnLabel(col.columnName),
|
||||
inputType: col.inputType || this.inferWebType(col.dataType),
|
||||
webType: col.webType || this.inferWebType(col.dataType),
|
||||
detailSettings: col.detailSettings || undefined,
|
||||
codeCategory: col.codeCategory || undefined,
|
||||
}));
|
||||
|
||||
// 디버깅: currency_code의 최종 inputType 확인
|
||||
const currencyCodeResult = result.find(r => r.columnName === 'currency_code');
|
||||
if (currencyCodeResult) {
|
||||
console.log(`🎯 [getTableColumns] 최종 currency_code:`, {
|
||||
inputType: currencyCodeResult.inputType,
|
||||
webType: currencyCodeResult.webType,
|
||||
dataType: currencyCodeResult.dataType
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✅ [getTableColumns] 반환: ${result.length}개 컬럼`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("테이블 컬럼 조회 실패:", error);
|
||||
throw new Error("테이블 컬럼 정보를 조회할 수 없습니다.");
|
||||
|
|
|
|||
|
|
@ -165,6 +165,10 @@ export class TableManagementService {
|
|||
const offset = (page - 1) * size;
|
||||
|
||||
// 🔥 company_code가 있으면 table_type_columns 조인하여 회사별 inputType 가져오기
|
||||
console.log(
|
||||
`🔍 [getColumnList] 시작: table=${tableName}, company=${companyCode}`
|
||||
);
|
||||
|
||||
const rawColumns = companyCode
|
||||
? await query<any>(
|
||||
`SELECT
|
||||
|
|
@ -174,6 +178,8 @@ export class TableManagementService {
|
|||
c.data_type as "dbType",
|
||||
COALESCE(cl.input_type, 'text') as "webType",
|
||||
COALESCE(ttc.input_type, cl.input_type, 'direct') as "inputType",
|
||||
ttc.input_type as "ttc_input_type",
|
||||
cl.input_type as "cl_input_type",
|
||||
COALESCE(ttc.detail_settings::text, cl.detail_settings, '') as "detailSettings",
|
||||
COALESCE(cl.description, '') as "description",
|
||||
c.is_nullable as "isNullable",
|
||||
|
|
@ -250,6 +256,22 @@ export class TableManagementService {
|
|||
[tableName, size, offset]
|
||||
);
|
||||
|
||||
// 디버깅: currency_code 확인
|
||||
const currencyCol = rawColumns.find(
|
||||
(col: any) => col.columnName === "currency_code"
|
||||
);
|
||||
if (currencyCol) {
|
||||
console.log(`🎯 [getColumnList] currency_code 원본 쿼리 결과:`, {
|
||||
columnName: currencyCol.columnName,
|
||||
inputType: currencyCol.inputType,
|
||||
ttc_input_type: currencyCol.ttc_input_type,
|
||||
cl_input_type: currencyCol.cl_input_type,
|
||||
webType: currencyCol.webType,
|
||||
});
|
||||
} else {
|
||||
console.log(`⚠️ [getColumnList] currency_code가 rawColumns에 없음`);
|
||||
}
|
||||
|
||||
// 🆕 category_column_mapping 조회
|
||||
const tableExistsResult = await query<any>(
|
||||
`SELECT EXISTS (
|
||||
|
|
|
|||
|
|
@ -119,7 +119,19 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
// 전역 모달 이벤트 리스너
|
||||
useEffect(() => {
|
||||
const handleOpenModal = (event: CustomEvent) => {
|
||||
const { screenId, title, description, size } = event.detail;
|
||||
const { screenId, title, description, size, urlParams } = event.detail;
|
||||
|
||||
// 🆕 URL 파라미터가 있으면 현재 URL에 추가
|
||||
if (urlParams && typeof window !== "undefined") {
|
||||
const currentUrl = new URL(window.location.href);
|
||||
Object.entries(urlParams).forEach(([key, value]) => {
|
||||
currentUrl.searchParams.set(key, String(value));
|
||||
});
|
||||
// pushState로 URL 변경 (페이지 새로고침 없이)
|
||||
window.history.pushState({}, "", currentUrl.toString());
|
||||
console.log("✅ URL 파라미터 추가:", urlParams);
|
||||
}
|
||||
|
||||
setModalState({
|
||||
isOpen: true,
|
||||
screenId,
|
||||
|
|
@ -130,6 +142,15 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
// 🆕 URL 파라미터 제거
|
||||
if (typeof window !== "undefined") {
|
||||
const currentUrl = new URL(window.location.href);
|
||||
// dataSourceId 파라미터 제거
|
||||
currentUrl.searchParams.delete("dataSourceId");
|
||||
window.history.pushState({}, "", currentUrl.toString());
|
||||
console.log("🧹 URL 파라미터 제거");
|
||||
}
|
||||
|
||||
setModalState({
|
||||
isOpen: false,
|
||||
screenId: null,
|
||||
|
|
|
|||
|
|
@ -316,6 +316,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
|||
userId={user?.userId} // ✅ 사용자 ID 전달
|
||||
userName={user?.userName} // ✅ 사용자 이름 전달
|
||||
companyCode={user?.companyCode} // ✅ 회사 코드 전달
|
||||
allComponents={allComponents} // 🆕 같은 화면의 모든 컴포넌트 전달 (TableList 자동 감지용)
|
||||
selectedRowsData={selectedRowsData}
|
||||
onSelectedRowsChange={(selectedRows, selectedData) => {
|
||||
console.log("🔍 테이블에서 선택된 행 데이터:", selectedData);
|
||||
|
|
|
|||
|
|
@ -419,17 +419,22 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
</p>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="data-source-id">데이터 소스 ID</Label>
|
||||
<Label htmlFor="data-source-id">
|
||||
데이터 소스 ID <span className="text-primary">(선택사항)</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="data-source-id"
|
||||
placeholder="예: item_info (테이블명과 동일하게 입력)"
|
||||
placeholder="비워두면 자동으로 감지됩니다"
|
||||
value={component.componentConfig?.action?.dataSourceId || ""}
|
||||
onChange={(e) => {
|
||||
onUpdateProperty("componentConfig.action.dataSourceId", e.target.value);
|
||||
}}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-primary font-medium">
|
||||
✨ 비워두면 같은 화면의 TableList를 자동으로 감지합니다
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
TableList에서 데이터를 저장한 ID와 동일해야 합니다 (보통 테이블명)
|
||||
직접 지정하려면 테이블명을 입력하세요 (예: item_info)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ export interface ButtonPrimaryComponentProps extends ComponentRendererProps {
|
|||
// 플로우 선택된 데이터 정보 (플로우 위젯 선택 액션용)
|
||||
flowSelectedData?: any[];
|
||||
flowSelectedStepId?: number | null;
|
||||
|
||||
// 🆕 같은 화면의 모든 컴포넌트 (TableList 자동 감지용)
|
||||
allComponents?: any[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -88,6 +91,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
selectedRowsData,
|
||||
flowSelectedData,
|
||||
flowSelectedStepId,
|
||||
allComponents, // 🆕 같은 화면의 모든 컴포넌트
|
||||
...props
|
||||
}) => {
|
||||
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||
|
|
@ -409,6 +413,8 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
sortOrder, // 🆕 정렬 방향
|
||||
columnOrder, // 🆕 컬럼 순서
|
||||
tableDisplayData, // 🆕 화면에 표시된 데이터
|
||||
// 🆕 같은 화면의 모든 컴포넌트 (TableList 자동 감지용)
|
||||
allComponents,
|
||||
// 플로우 선택된 데이터 정보 추가
|
||||
flowSelectedData,
|
||||
flowSelectedStepId,
|
||||
|
|
|
|||
|
|
@ -1,77 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { ButtonPrimaryConfig } from "./types";
|
||||
|
||||
export interface ButtonPrimaryConfigPanelProps {
|
||||
config: ButtonPrimaryConfig;
|
||||
onChange: (config: Partial<ButtonPrimaryConfig>) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* ButtonPrimary 설정 패널
|
||||
* 컴포넌트의 설정값들을 편집할 수 있는 UI 제공
|
||||
*/
|
||||
export const ButtonPrimaryConfigPanel: React.FC<ButtonPrimaryConfigPanelProps> = ({ config, onChange }) => {
|
||||
const handleChange = (key: keyof ButtonPrimaryConfig, value: any) => {
|
||||
onChange({ [key]: value });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm font-medium">button-primary 설정</div>
|
||||
|
||||
{/* 버튼 관련 설정 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="text">버튼 텍스트</Label>
|
||||
<Input id="text" value={config.text || ""} onChange={(e) => handleChange("text", e.target.value)} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="actionType">액션 타입</Label>
|
||||
<Select value={config.actionType || "button"} onValueChange={(value) => handleChange("actionType", value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="button">Button</SelectItem>
|
||||
<SelectItem value="submit">Submit</SelectItem>
|
||||
<SelectItem value="reset">Reset</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 공통 설정 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="disabled">비활성화</Label>
|
||||
<Checkbox
|
||||
id="disabled"
|
||||
checked={config.disabled || false}
|
||||
onCheckedChange={(checked) => handleChange("disabled", checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="required">필수 입력</Label>
|
||||
<Checkbox
|
||||
id="required"
|
||||
checked={config.required || false}
|
||||
onCheckedChange={(checked) => handleChange("required", checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="readonly">읽기 전용</Label>
|
||||
<Checkbox
|
||||
id="readonly"
|
||||
checked={config.readonly || false}
|
||||
onCheckedChange={(checked) => handleChange("readonly", checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -5,7 +5,6 @@ import { createComponentDefinition } from "../../utils/createComponentDefinition
|
|||
import { ComponentCategory } from "@/types/component";
|
||||
import type { WebType } from "@/types/screen";
|
||||
import { ButtonPrimaryWrapper } from "./ButtonPrimaryComponent";
|
||||
import { ButtonPrimaryConfigPanel } from "./ButtonPrimaryConfigPanel";
|
||||
import { ButtonPrimaryConfig } from "./types";
|
||||
|
||||
/**
|
||||
|
|
@ -31,7 +30,7 @@ export const ButtonPrimaryDefinition = createComponentDefinition({
|
|||
},
|
||||
},
|
||||
defaultSize: { width: 120, height: 40 },
|
||||
configPanel: ButtonPrimaryConfigPanel,
|
||||
configPanel: undefined, // 상세 설정 패널(ButtonConfigPanel)이 대신 사용됨
|
||||
icon: "MousePointer",
|
||||
tags: ["버튼", "액션", "클릭"],
|
||||
version: "1.0.0",
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
@ -71,6 +86,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(() => {
|
||||
if (modalData && modalData.length > 0) {
|
||||
|
|
@ -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>
|
||||
))}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,9 +17,12 @@ import { cn } from "@/lib/utils";
|
|||
export interface SelectedItemsDetailInputConfigPanelProps {
|
||||
config: SelectedItemsDetailInputConfig;
|
||||
onChange: (config: Partial<SelectedItemsDetailInputConfig>) => void;
|
||||
tableColumns?: Array<{ columnName: string; columnLabel?: string; dataType?: string }>;
|
||||
sourceTableColumns?: Array<{ columnName: string; columnLabel?: string; dataType?: string }>; // 🆕 원본 테이블 컬럼
|
||||
targetTableColumns?: Array<{ columnName: string; columnLabel?: string; dataType?: string }>; // 🆕 대상 테이블 컬럼
|
||||
allTables?: Array<{ tableName: string; displayName?: string }>;
|
||||
onTableChange?: (tableName: string) => void;
|
||||
screenTableName?: string; // 🆕 현재 화면의 테이블명 (자동 설정용)
|
||||
onSourceTableChange?: (tableName: string) => void; // 🆕 원본 테이블 변경 콜백
|
||||
onTargetTableChange?: (tableName: string) => void; // 🆕 대상 테이블 변경 콜백 (기존 onTableChange 대체)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -29,14 +32,38 @@ export interface SelectedItemsDetailInputConfigPanelProps {
|
|||
export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailInputConfigPanelProps> = ({
|
||||
config,
|
||||
onChange,
|
||||
tableColumns = [],
|
||||
sourceTableColumns = [], // 🆕 원본 테이블 컬럼
|
||||
targetTableColumns = [], // 🆕 대상 테이블 컬럼
|
||||
allTables = [],
|
||||
onTableChange,
|
||||
screenTableName, // 🆕 현재 화면의 테이블명
|
||||
onSourceTableChange, // 🆕 원본 테이블 변경 콜백
|
||||
onTargetTableChange, // 🆕 대상 테이블 변경 콜백
|
||||
}) => {
|
||||
const [localFields, setLocalFields] = useState<AdditionalFieldDefinition[]>(config.additionalFields || []);
|
||||
const [displayColumns, setDisplayColumns] = useState<string[]>(config.displayColumns || []);
|
||||
const [displayColumns, setDisplayColumns] = useState<Array<{ name: string; label: string; width?: string }>>(config.displayColumns || []);
|
||||
const [fieldPopoverOpen, setFieldPopoverOpen] = useState<Record<number, boolean>>({});
|
||||
|
||||
// 🆕 원본 테이블 선택 상태
|
||||
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은 의존성에서 제외 (한 번만 실행)
|
||||
|
||||
const handleChange = (key: keyof SelectedItemsDetailInputConfig, value: any) => {
|
||||
onChange({ [key]: value });
|
||||
};
|
||||
|
|
@ -46,7 +73,7 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
|||
handleChange("additionalFields", fields);
|
||||
};
|
||||
|
||||
const handleDisplayColumnsChange = (columns: string[]) => {
|
||||
const handleDisplayColumnsChange = (columns: Array<{ name: string; label: string; width?: string }>) => {
|
||||
setDisplayColumns(columns);
|
||||
handleChange("displayColumns", columns);
|
||||
};
|
||||
|
|
@ -74,28 +101,47 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
|||
};
|
||||
|
||||
// 표시 컬럼 추가
|
||||
const addDisplayColumn = (columnName: string) => {
|
||||
if (!displayColumns.includes(columnName)) {
|
||||
handleDisplayColumnsChange([...displayColumns, columnName]);
|
||||
const addDisplayColumn = (columnName: string, columnLabel: string) => {
|
||||
if (!displayColumns.some(col => col.name === columnName)) {
|
||||
handleDisplayColumnsChange([...displayColumns, { name: columnName, label: columnLabel }]);
|
||||
}
|
||||
};
|
||||
|
||||
// 표시 컬럼 제거
|
||||
const removeDisplayColumn = (columnName: string) => {
|
||||
handleDisplayColumnsChange(displayColumns.filter((col) => col !== columnName));
|
||||
handleDisplayColumnsChange(displayColumns.filter((col) => col.name !== columnName));
|
||||
};
|
||||
|
||||
// 사용되지 않은 컬럼 목록
|
||||
// 🆕 표시 컬럼용: 원본 테이블에서 사용되지 않은 컬럼 목록
|
||||
const availableColumns = useMemo(() => {
|
||||
const usedColumns = new Set([...displayColumns, ...localFields.map((f) => f.name)]);
|
||||
return tableColumns.filter((col) => !usedColumns.has(col.columnName));
|
||||
}, [tableColumns, displayColumns, localFields]);
|
||||
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]);
|
||||
|
||||
// 테이블 선택 Combobox 상태
|
||||
const [tableSelectOpen, setTableSelectOpen] = useState(false);
|
||||
const [tableSearchValue, setTableSearchValue] = useState("");
|
||||
// 🆕 추가 입력 필드용: 대상 테이블에서 사용되지 않은 컬럼 목록
|
||||
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]);
|
||||
|
||||
// 필터링된 테이블 목록
|
||||
// 🆕 원본 테이블 필터링
|
||||
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]);
|
||||
|
||||
// 🆕 선택된 원본 테이블 표시명
|
||||
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]);
|
||||
|
||||
// 대상 테이블 필터링
|
||||
const filteredTables = useMemo(() => {
|
||||
if (!tableSearchValue) return allTables;
|
||||
const searchLower = tableSearchValue.toLowerCase();
|
||||
|
|
@ -105,9 +151,9 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
|||
);
|
||||
}, [allTables, tableSearchValue]);
|
||||
|
||||
// 선택된 테이블 표시명
|
||||
// 선택된 대상 테이블 표시명
|
||||
const selectedTableLabel = useMemo(() => {
|
||||
if (!config.targetTable) return "테이블을 선택하세요";
|
||||
if (!config.targetTable) return "저장 대상 테이블을 선택하세요";
|
||||
const table = allTables.find((t) => t.tableName === config.targetTable);
|
||||
return table ? table.displayName || table.tableName : config.targetTable;
|
||||
}, [config.targetTable, allTables]);
|
||||
|
|
@ -116,21 +162,83 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
|||
<div className="space-y-4">
|
||||
{/* 데이터 소스 ID */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-semibold sm:text-sm">데이터 소스 ID</Label>
|
||||
<Label className="text-xs font-semibold sm:text-sm">
|
||||
데이터 소스 ID <span className="text-primary font-normal">(자동 설정됨)</span>
|
||||
</Label>
|
||||
<Input
|
||||
value={config.dataSourceId || ""}
|
||||
onChange={(e) => handleChange("dataSourceId", e.target.value)}
|
||||
placeholder="table-list-123"
|
||||
placeholder="비워두면 URL 파라미터에서 자동 설정"
|
||||
className="h-7 text-xs sm:h-8 sm:text-sm"
|
||||
/>
|
||||
<p className="text-[10px] text-primary font-medium sm:text-xs">
|
||||
✨ URL 파라미터에서 자동으로 가져옵니다 (Button이 전달)
|
||||
</p>
|
||||
<p className="text-[10px] text-gray-500 sm:text-xs">
|
||||
💡 이전 모달에서 데이터를 전달하는 컴포넌트 ID (보통 TableList의 ID)
|
||||
테스트용으로 직접 입력하려면 테이블명을 입력하세요
|
||||
</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)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 저장 대상 테이블 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-semibold sm:text-sm">저장 대상 테이블</Label>
|
||||
<Label className="text-xs font-semibold sm:text-sm">
|
||||
저장 대상 테이블 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Popover open={tableSelectOpen} onOpenChange={setTableSelectOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
|
|
@ -161,8 +269,8 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
|||
handleChange("targetTable", currentValue);
|
||||
setTableSelectOpen(false);
|
||||
setTableSearchValue("");
|
||||
if (onTableChange) {
|
||||
onTableChange(currentValue);
|
||||
if (onTargetTableChange) {
|
||||
onTargetTableChange(currentValue);
|
||||
}
|
||||
}}
|
||||
className="text-xs sm:text-sm"
|
||||
|
|
@ -187,14 +295,14 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
|||
<div className="space-y-2">
|
||||
<Label className="text-xs font-semibold sm:text-sm">표시할 원본 데이터 컬럼</Label>
|
||||
<div className="space-y-2">
|
||||
{displayColumns.map((colName, index) => (
|
||||
{displayColumns.map((col, index) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<Input value={colName} readOnly className="h-7 flex-1 text-xs sm:h-8 sm:text-sm" />
|
||||
<Input value={col.label || col.name} readOnly className="h-7 flex-1 text-xs sm:h-8 sm:text-sm" />
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => removeDisplayColumn(colName)}
|
||||
onClick={() => removeDisplayColumn(col.name)}
|
||||
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" />
|
||||
|
|
@ -223,7 +331,7 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
|||
<CommandItem
|
||||
key={column.columnName}
|
||||
value={column.columnName}
|
||||
onSelect={() => addDisplayColumn(column.columnName)}
|
||||
onSelect={() => addDisplayColumn(column.columnName, column.columnLabel || column.columnName)}
|
||||
className="text-xs sm:text-sm"
|
||||
>
|
||||
<div>
|
||||
|
|
@ -284,7 +392,7 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
|||
<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]">
|
||||
{availableColumns.map((column) => (
|
||||
{availableTargetColumns.map((column) => (
|
||||
<CommandItem
|
||||
key={column.columnName}
|
||||
value={column.columnName}
|
||||
|
|
@ -292,6 +400,8 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
|||
updateField(index, {
|
||||
name: column.columnName,
|
||||
label: column.columnLabel || column.columnName,
|
||||
inputType: column.inputType || "text", // 🆕 inputType 포함
|
||||
codeCategory: column.codeCategory, // 🆕 codeCategory 포함
|
||||
});
|
||||
setFieldPopoverOpen({ ...fieldPopoverOpen, [index]: false });
|
||||
}}
|
||||
|
|
@ -328,37 +438,16 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
|||
|
||||
<div className="grid grid-cols-2 gap-2 sm:gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] sm:text-xs">타입</Label>
|
||||
<Select
|
||||
value={field.type}
|
||||
onValueChange={(value) =>
|
||||
updateField(index, { type: value as AdditionalFieldDefinition["type"] })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-6 w-full text-[10px] sm:h-7 sm:text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="text" className="text-[10px] sm:text-xs">
|
||||
텍스트
|
||||
</SelectItem>
|
||||
<SelectItem value="number" className="text-[10px] sm:text-xs">
|
||||
숫자
|
||||
</SelectItem>
|
||||
<SelectItem value="date" className="text-[10px] sm:text-xs">
|
||||
날짜
|
||||
</SelectItem>
|
||||
<SelectItem value="select" className="text-[10px] sm:text-xs">
|
||||
선택박스
|
||||
</SelectItem>
|
||||
<SelectItem value="checkbox" className="text-[10px] sm:text-xs">
|
||||
체크박스
|
||||
</SelectItem>
|
||||
<SelectItem value="textarea" className="text-[10px] sm:text-xs">
|
||||
텍스트영역
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ export interface AdditionalFieldDefinition {
|
|||
label: string;
|
||||
/** 입력 타입 */
|
||||
type: "text" | "number" | "date" | "select" | "checkbox" | "textarea";
|
||||
/** 🆕 데이터베이스 inputType (실제 렌더링 시 사용) */
|
||||
inputType?: string;
|
||||
/** 🆕 코드 카테고리 (inputType이 code/category일 때) */
|
||||
codeCategory?: string;
|
||||
/** 필수 입력 여부 */
|
||||
required?: boolean;
|
||||
/** 플레이스홀더 */
|
||||
|
|
@ -43,13 +47,20 @@ export interface SelectedItemsDetailInputConfig extends ComponentConfig {
|
|||
dataSourceId?: string;
|
||||
|
||||
/**
|
||||
* 표시할 원본 데이터 컬럼들
|
||||
* 예: ["item_code", "item_name", "spec", "unit"]
|
||||
* 🆕 원본 데이터 테이블명 (표시 컬럼 선택 시 사용)
|
||||
* 예: "item_info"
|
||||
*/
|
||||
displayColumns?: string[];
|
||||
sourceTable?: string;
|
||||
|
||||
/**
|
||||
* 표시할 원본 데이터 컬럼들 (name, label, width)
|
||||
* 원본 데이터 테이블의 컬럼을 표시
|
||||
*/
|
||||
displayColumns?: Array<{ name: string; label: string; width?: string }>;
|
||||
|
||||
/**
|
||||
* 추가 입력 필드 정의
|
||||
* 저장 대상 테이블의 컬럼을 입력
|
||||
*/
|
||||
additionalFields?: AdditionalFieldDefinition[];
|
||||
|
||||
|
|
|
|||
|
|
@ -107,6 +107,9 @@ export interface ButtonActionContext {
|
|||
flowSelectedData?: any[];
|
||||
flowSelectedStepId?: number | null;
|
||||
|
||||
// 🆕 같은 화면의 모든 컴포넌트 (TableList 자동 감지용)
|
||||
allComponents?: any[];
|
||||
|
||||
// 제어 실행을 위한 추가 정보
|
||||
buttonId?: string;
|
||||
|
||||
|
|
@ -686,8 +689,28 @@ export class ButtonActionExecutor {
|
|||
dataSourceId: config.dataSourceId,
|
||||
});
|
||||
|
||||
// 1. dataSourceId 확인 (없으면 selectedRows에서 데이터 전달)
|
||||
const dataSourceId = config.dataSourceId || context.tableName || "default";
|
||||
// 🆕 1. dataSourceId 자동 결정
|
||||
let dataSourceId = config.dataSourceId;
|
||||
|
||||
// dataSourceId가 없으면 같은 화면의 TableList 자동 감지
|
||||
if (!dataSourceId && context.allComponents) {
|
||||
const tableListComponent = context.allComponents.find(
|
||||
(comp: any) => comp.componentType === "table-list" && comp.componentConfig?.tableName
|
||||
);
|
||||
|
||||
if (tableListComponent) {
|
||||
dataSourceId = tableListComponent.componentConfig.tableName;
|
||||
console.log("✨ TableList 자동 감지:", {
|
||||
componentId: tableListComponent.id,
|
||||
tableName: dataSourceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 여전히 없으면 context.tableName 또는 "default" 사용
|
||||
if (!dataSourceId) {
|
||||
dataSourceId = context.tableName || "default";
|
||||
}
|
||||
|
||||
// 2. modalDataStore에서 데이터 확인
|
||||
try {
|
||||
|
|
@ -711,7 +734,7 @@ export class ButtonActionExecutor {
|
|||
return false;
|
||||
}
|
||||
|
||||
// 3. 모달 열기
|
||||
// 3. 모달 열기 + URL 파라미터로 dataSourceId 전달
|
||||
if (config.targetScreenId) {
|
||||
// config에 modalDescription이 있으면 우선 사용
|
||||
let description = config.modalDescription || "";
|
||||
|
|
@ -726,13 +749,14 @@ export class ButtonActionExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
// 전역 모달 상태 업데이트를 위한 이벤트 발생
|
||||
// 🆕 전역 모달 상태 업데이트를 위한 이벤트 발생 (URL 파라미터 포함)
|
||||
const modalEvent = new CustomEvent("openScreenModal", {
|
||||
detail: {
|
||||
screenId: config.targetScreenId,
|
||||
title: config.modalTitle || "데이터 입력",
|
||||
description: description,
|
||||
size: config.modalSize || "lg", // 데이터 입력 화면은 기본 large
|
||||
urlParams: { dataSourceId }, // 🆕 URL 파라미터로 dataSourceId 전달
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const CONFIG_PANEL_MAP: Record<string, () => Promise<any>> = {
|
|||
"radio-basic": () => import("@/lib/registry/components/radio-basic/RadioBasicConfigPanel"),
|
||||
"toggle-switch": () => import("@/lib/registry/components/toggle-switch/ToggleSwitchConfigPanel"),
|
||||
"file-upload": () => import("@/lib/registry/components/file-upload/FileUploadConfigPanel"),
|
||||
"button-primary": () => import("@/lib/registry/components/button-primary/ButtonPrimaryConfigPanel"),
|
||||
"button-primary": () => import("@/components/screen/config-panels/ButtonConfigPanel"),
|
||||
"text-display": () => import("@/lib/registry/components/text-display/TextDisplayConfigPanel"),
|
||||
"slider-basic": () => import("@/lib/registry/components/slider-basic/SliderBasicConfigPanel"),
|
||||
"image-display": () => import("@/lib/registry/components/image-display/ImageDisplayConfigPanel"),
|
||||
|
|
@ -70,6 +70,7 @@ export async function getComponentConfigPanel(componentId: string): Promise<Reac
|
|||
module.FlowWidgetConfigPanel || // flow-widget의 export명
|
||||
module.CustomerItemMappingConfigPanel || // customer-item-mapping의 export명
|
||||
module.SelectedItemsDetailInputConfigPanel || // selected-items-detail-input의 export명
|
||||
module.ButtonConfigPanel || // button-primary의 export명
|
||||
module.default;
|
||||
|
||||
if (!ConfigPanelComponent) {
|
||||
|
|
@ -141,6 +142,10 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
|
|||
const [selectedTableColumns, setSelectedTableColumns] = React.useState(tableColumns);
|
||||
const [allTablesList, setAllTablesList] = React.useState<any[]>([]);
|
||||
|
||||
// 🆕 selected-items-detail-input 전용 상태
|
||||
const [sourceTableColumns, setSourceTableColumns] = React.useState<any[]>([]);
|
||||
const [targetTableColumns, setTargetTableColumns] = React.useState<any[]>([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
|
|
@ -176,14 +181,15 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
|
|||
setSelectedTableColumns(tableColumns);
|
||||
}, [tableColumns]);
|
||||
|
||||
// RepeaterConfigPanel인 경우에만 전체 테이블 목록 로드
|
||||
// RepeaterConfigPanel과 selected-items-detail-input에서 전체 테이블 목록 로드
|
||||
React.useEffect(() => {
|
||||
if (componentId === "repeater-field-group") {
|
||||
if (componentId === "repeater-field-group" || componentId === "selected-items-detail-input") {
|
||||
const loadAllTables = async () => {
|
||||
try {
|
||||
const { tableManagementApi } = await import("@/lib/api/tableManagement");
|
||||
const response = await tableManagementApi.getTableList();
|
||||
if (response.success && response.data) {
|
||||
console.log(`✅ 전체 테이블 목록 로드 완료 (${componentId}):`, response.data.length);
|
||||
setAllTablesList(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -194,6 +200,57 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
|
|||
}
|
||||
}, [componentId]);
|
||||
|
||||
// 🆕 selected-items-detail-input: 초기 sourceTable/targetTable 컬럼 로드
|
||||
React.useEffect(() => {
|
||||
if (componentId === "selected-items-detail-input") {
|
||||
console.log("🔍 selected-items-detail-input 초기 설정:", config);
|
||||
|
||||
// 원본 테이블 컬럼 로드
|
||||
if (config.sourceTable) {
|
||||
const loadSourceColumns = async () => {
|
||||
try {
|
||||
const { tableTypeApi } = await import("@/lib/api/screen");
|
||||
const columnsResponse = await tableTypeApi.getColumns(config.sourceTable);
|
||||
|
||||
const columns = (columnsResponse || []).map((col: any) => ({
|
||||
columnName: col.columnName || col.column_name,
|
||||
columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name,
|
||||
dataType: col.dataType || col.data_type || col.dbType,
|
||||
}));
|
||||
|
||||
console.log("✅ 원본 테이블 컬럼 초기 로드 완료:", columns.length);
|
||||
setSourceTableColumns(columns);
|
||||
} catch (error) {
|
||||
console.error("❌ 원본 테이블 컬럼 초기 로드 실패:", error);
|
||||
}
|
||||
};
|
||||
loadSourceColumns();
|
||||
}
|
||||
|
||||
// 대상 테이블 컬럼 로드
|
||||
if (config.targetTable) {
|
||||
const loadTargetColumns = async () => {
|
||||
try {
|
||||
const { tableTypeApi } = await import("@/lib/api/screen");
|
||||
const columnsResponse = await tableTypeApi.getColumns(config.targetTable);
|
||||
|
||||
const columns = (columnsResponse || []).map((col: any) => ({
|
||||
columnName: col.columnName || col.column_name,
|
||||
columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name,
|
||||
dataType: col.dataType || col.data_type || col.dbType,
|
||||
}));
|
||||
|
||||
console.log("✅ 대상 테이블 컬럼 초기 로드 완료:", columns.length);
|
||||
setTargetTableColumns(columns);
|
||||
} catch (error) {
|
||||
console.error("❌ 대상 테이블 컬럼 초기 로드 실패:", error);
|
||||
}
|
||||
};
|
||||
loadTargetColumns();
|
||||
}
|
||||
}
|
||||
}, [componentId, config.sourceTable, config.targetTable]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="rounded-md border border-dashed border-gray-300 bg-gray-50 p-4">
|
||||
|
|
@ -266,6 +323,63 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
|
|||
}
|
||||
};
|
||||
|
||||
// 🆕 원본 테이블 컬럼 로드 핸들러 (selected-items-detail-input용)
|
||||
const handleSourceTableChange = async (tableName: string) => {
|
||||
console.log("🔄 원본 테이블 변경:", tableName);
|
||||
try {
|
||||
const { tableTypeApi } = await import("@/lib/api/screen");
|
||||
const columnsResponse = await tableTypeApi.getColumns(tableName);
|
||||
|
||||
const columns = (columnsResponse || []).map((col: any) => ({
|
||||
columnName: col.columnName || col.column_name,
|
||||
columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name,
|
||||
dataType: col.dataType || col.data_type || col.dbType,
|
||||
inputType: col.inputType || col.input_type, // 🆕 inputType 추가
|
||||
}));
|
||||
|
||||
console.log("✅ 원본 테이블 컬럼 로드 완료:", columns.length);
|
||||
setSourceTableColumns(columns);
|
||||
} catch (error) {
|
||||
console.error("❌ 원본 테이블 컬럼 로드 실패:", error);
|
||||
setSourceTableColumns([]);
|
||||
}
|
||||
};
|
||||
|
||||
// 🆕 대상 테이블 컬럼 로드 핸들러 (selected-items-detail-input용)
|
||||
const handleTargetTableChange = async (tableName: string) => {
|
||||
console.log("🔄 대상 테이블 변경:", tableName);
|
||||
try {
|
||||
const { tableTypeApi } = await import("@/lib/api/screen");
|
||||
const columnsResponse = await tableTypeApi.getColumns(tableName);
|
||||
|
||||
console.log("📡 [handleTargetTableChange] API 응답 (원본):", {
|
||||
totalColumns: columnsResponse.length,
|
||||
sampleColumns: columnsResponse.slice(0, 3),
|
||||
currency_code_raw: columnsResponse.find((c: any) => (c.columnName || c.column_name) === 'currency_code')
|
||||
});
|
||||
|
||||
const columns = (columnsResponse || []).map((col: any) => ({
|
||||
columnName: col.columnName || col.column_name,
|
||||
columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name,
|
||||
dataType: col.dataType || col.data_type || col.dbType,
|
||||
inputType: col.inputType || col.input_type, // 🆕 inputType 추가
|
||||
codeCategory: col.codeCategory || col.code_category, // 🆕 codeCategory 추가
|
||||
}));
|
||||
|
||||
console.log("✅ 대상 테이블 컬럼 변환 완료:", {
|
||||
tableName,
|
||||
totalColumns: columns.length,
|
||||
currency_code: columns.find((c: any) => c.columnName === "currency_code"),
|
||||
discount_rate: columns.find((c: any) => c.columnName === "discount_rate")
|
||||
});
|
||||
|
||||
setTargetTableColumns(columns);
|
||||
} catch (error) {
|
||||
console.error("❌ 대상 테이블 컬럼 로드 실패:", error);
|
||||
setTargetTableColumns([]);
|
||||
}
|
||||
};
|
||||
|
||||
// 🆕 수주 등록 관련 컴포넌트들은 간단한 인터페이스 사용
|
||||
const isSimpleConfigPanel = [
|
||||
"autocomplete-search-input",
|
||||
|
|
@ -279,6 +393,22 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
|
|||
return <ConfigPanelComponent config={config} onConfigChange={onChange} />;
|
||||
}
|
||||
|
||||
// 🆕 selected-items-detail-input은 특별한 props 사용
|
||||
if (componentId === "selected-items-detail-input") {
|
||||
return (
|
||||
<ConfigPanelComponent
|
||||
config={config}
|
||||
onChange={onChange}
|
||||
sourceTableColumns={sourceTableColumns} // 🆕 원본 테이블 컬럼
|
||||
targetTableColumns={targetTableColumns} // 🆕 대상 테이블 컬럼
|
||||
allTables={allTablesList.length > 0 ? allTablesList : tables} // 전체 테이블 목록 (동적 로드 or 전달된 목록)
|
||||
screenTableName={screenTableName} // 🆕 현재 화면의 테이블명 (자동 설정용)
|
||||
onSourceTableChange={handleSourceTableChange} // 🆕 원본 테이블 변경 핸들러
|
||||
onTargetTableChange={handleTargetTableChange} // 🆕 대상 테이블 변경 핸들러
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfigPanelComponent
|
||||
config={config}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,375 @@
|
|||
# 선택 항목 상세입력 완전 자동화 가이드 🚀
|
||||
|
||||
## ✨ 완전 자동화 완료!
|
||||
|
||||
이제 **아무 설정도 하지 않아도** 데이터가 자동으로 전달됩니다!
|
||||
|
||||
---
|
||||
|
||||
## 🎯 자동화된 흐름
|
||||
|
||||
### 1단계: TableList (선택 화면)
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ TableList 컴포넌트 │
|
||||
│ - 테이블: item_info │
|
||||
│ - 체크박스로 품목 3개 선택 │
|
||||
└─────────────────────────────────┘
|
||||
↓ (자동 저장)
|
||||
┌─────────────────────────────────┐
|
||||
│ modalDataStore │
|
||||
│ { "item_info": [선택된 데이터] }│
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2단계: Button (다음 버튼)
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ Button 컴포넌트 │
|
||||
│ - 액션: "데이터 전달 + 모달열기"│
|
||||
│ - dataSourceId: (비워둠) │ ← 자동 감지!
|
||||
└─────────────────────────────────┘
|
||||
↓ (자동 감지)
|
||||
┌─────────────────────────────────┐
|
||||
│ 같은 화면에서 TableList 찾기 │
|
||||
│ → tableName = "item_info" │
|
||||
└─────────────────────────────────┘
|
||||
↓ (URL 전달)
|
||||
┌─────────────────────────────────┐
|
||||
│ 모달 열기 │
|
||||
│ URL: ?dataSourceId=item_info │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3단계: SelectedItemsDetailInput (상세 입력 화면)
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ SelectedItemsDetailInput │
|
||||
│ - dataSourceId: (비워둠) │ ← URL에서 자동 읽기!
|
||||
└─────────────────────────────────┘
|
||||
↓ (자동 읽기)
|
||||
┌─────────────────────────────────┐
|
||||
│ URL: ?dataSourceId=item_info │
|
||||
│ → modalDataStore에서 데이터 로드│
|
||||
└─────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────┐
|
||||
│ 화면에 데이터 표시 │
|
||||
│ 선택된 3개 품목 + 입력 필드 │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 설정 방법 (완전 자동)
|
||||
|
||||
### 1단계: 선택 화면 구성
|
||||
|
||||
#### TableList 컴포넌트
|
||||
```yaml
|
||||
테이블: item_info
|
||||
옵션:
|
||||
- 체크박스 표시: ✓
|
||||
- 다중 선택: ✓
|
||||
```
|
||||
|
||||
**설정 끝!** 선택된 데이터는 자동으로 `modalDataStore`에 저장됩니다.
|
||||
|
||||
#### Button 컴포넌트
|
||||
```yaml
|
||||
버튼 텍스트: "다음"
|
||||
버튼 액션: "데이터 전달 + 모달 열기" 🆕
|
||||
|
||||
설정:
|
||||
- 데이터 소스 ID: (비워둠) ← ✨ 자동 감지!
|
||||
- 모달 제목: "상세 정보 입력"
|
||||
- 모달 크기: Large
|
||||
- 대상 화면: (상세 입력 화면 선택)
|
||||
```
|
||||
|
||||
**중요**: `데이터 소스 ID`를 **비워두세요**! 자동으로 같은 화면의 TableList를 찾아서 테이블명을 사용합니다.
|
||||
|
||||
---
|
||||
|
||||
### 2단계: 상세 입력 화면 구성
|
||||
|
||||
#### SelectedItemsDetailInput 컴포넌트 추가
|
||||
```yaml
|
||||
컴포넌트: "선택 항목 상세입력"
|
||||
```
|
||||
|
||||
**설정 끝!** URL 파라미터에서 자동으로 `dataSourceId`를 읽어옵니다.
|
||||
|
||||
#### 상세 설정 (선택사항)
|
||||
```yaml
|
||||
데이터 소스 ID: (비워둠) ← ✨ URL에서 자동!
|
||||
|
||||
표시할 컬럼:
|
||||
- 품목코드 (item_code)
|
||||
- 품목명 (item_name)
|
||||
- 규격 (specification)
|
||||
|
||||
추가 입력 필드:
|
||||
- 수량 (quantity): 숫자
|
||||
- 단가 (unit_price): 숫자
|
||||
- 납기일 (delivery_date): 날짜
|
||||
- 비고 (remarks): 텍스트영역
|
||||
|
||||
옵션:
|
||||
- 레이아웃: 테이블 형식 (Grid)
|
||||
- 항목 번호 표시: ✓
|
||||
- 항목 제거 허용: ☐
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 실제 동작 시나리오
|
||||
|
||||
### 시나리오: 수주 등록
|
||||
|
||||
#### 1단계: 품목 선택
|
||||
```
|
||||
사용자가 품목 테이블에서 3개 선택:
|
||||
✓ [PD-001] 케이블 100m
|
||||
✓ [PD-002] 커넥터 50개
|
||||
✓ [PD-003] 단자대 20개
|
||||
```
|
||||
|
||||
#### 2단계: "다음" 버튼 클릭
|
||||
```javascript
|
||||
// 자동으로 일어나는 일:
|
||||
1. 같은 화면에서 table-list 컴포넌트 찾기
|
||||
→ componentType === "table-list"
|
||||
→ tableName === "item_info"
|
||||
|
||||
2. modalDataStore에서 데이터 확인
|
||||
→ modalData = [
|
||||
{ id: "PD-001", originalData: {...} },
|
||||
{ id: "PD-002", originalData: {...} },
|
||||
{ id: "PD-003", originalData: {...} }
|
||||
]
|
||||
|
||||
3. 모달 열기 + URL 파라미터 전달
|
||||
→ URL: /screen/detail-input?dataSourceId=item_info
|
||||
```
|
||||
|
||||
#### 3단계: 상세 정보 입력
|
||||
```
|
||||
자동으로 표시됨:
|
||||
|
||||
┌───────────────────────────────────────────────────────┐
|
||||
│ 상세 정보 입력 │
|
||||
├───────────────────────────────────────────────────────┤
|
||||
│ # │ 품목코드 │ 품목명 │ 수량 │ 단가 │ 납기일 │
|
||||
├───┼──────────┼────────────┼──────┼────────┼─────────┤
|
||||
│ 1 │ PD-001 │ 케이블100m │ [ ] │ [ ] │ [ ] │
|
||||
│ 2 │ PD-002 │ 커넥터50개 │ [ ] │ [ ] │ [ ] │
|
||||
│ 3 │ PD-003 │ 단자대20개 │ [ ] │ [ ] │ [ ] │
|
||||
└───────────────────────────────────────────────────────┘
|
||||
|
||||
사용자가 수량, 단가, 납기일만 입력하면 끝!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 UI 미리보기
|
||||
|
||||
### Button 설정 화면
|
||||
```
|
||||
버튼 액션
|
||||
├─ 데이터 전달 + 모달 열기 🆕
|
||||
│
|
||||
└─ 데이터 전달 + 모달 설정
|
||||
│
|
||||
├─ 데이터 소스 ID (선택사항)
|
||||
│ [ ]
|
||||
│ ✨ 비워두면 같은 화면의 TableList를 자동으로 감지합니다
|
||||
│ 직접 지정하려면 테이블명을 입력하세요 (예: item_info)
|
||||
│
|
||||
├─ 모달 제목
|
||||
│ [상세 정보 입력 ]
|
||||
│
|
||||
├─ 모달 크기
|
||||
│ [큰 (Large) - 권장 ▼]
|
||||
│
|
||||
└─ 대상 화면 선택
|
||||
[화면을 선택하세요... ▼]
|
||||
```
|
||||
|
||||
### SelectedItemsDetailInput 설정 화면
|
||||
```
|
||||
데이터 소스 ID (자동 설정됨)
|
||||
[ ]
|
||||
✨ URL 파라미터에서 자동으로 가져옵니다 (Button이 전달)
|
||||
테스트용으로 직접 입력하려면 테이블명을 입력하세요
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 자동화 원리
|
||||
|
||||
### 1. TableList → modalDataStore
|
||||
```typescript
|
||||
// TableListComponent.tsx
|
||||
const handleRowSelection = (rowKey: string, checked: boolean) => {
|
||||
// ... 선택 처리 ...
|
||||
|
||||
// 🆕 자동으로 스토어에 저장
|
||||
const { useModalDataStore } = await import("@/stores/modalDataStore");
|
||||
const dataSourceId = tableName; // item_info
|
||||
const modalDataItems = selectedRowsData.map(row => ({
|
||||
id: row.item_code,
|
||||
originalData: row,
|
||||
additionalData: {}
|
||||
}));
|
||||
useModalDataStore.getState().setData(dataSourceId, modalDataItems);
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Button → TableList 자동 감지
|
||||
```typescript
|
||||
// buttonActions.ts - handleOpenModalWithData()
|
||||
let dataSourceId = config.dataSourceId;
|
||||
|
||||
// 🆕 비워있으면 자동 감지
|
||||
if (!dataSourceId && context.allComponents) {
|
||||
const tableListComponent = context.allComponents.find(
|
||||
(comp) => comp.componentType === "table-list" && comp.componentConfig?.tableName
|
||||
);
|
||||
|
||||
if (tableListComponent) {
|
||||
dataSourceId = tableListComponent.componentConfig.tableName;
|
||||
console.log("✨ TableList 자동 감지:", dataSourceId);
|
||||
}
|
||||
}
|
||||
|
||||
// 🆕 URL 파라미터로 전달
|
||||
const modalEvent = new CustomEvent("openScreenModal", {
|
||||
detail: {
|
||||
screenId: config.targetScreenId,
|
||||
urlParams: { dataSourceId } // ← URL에 추가
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 3. SelectedItemsDetailInput → URL 읽기
|
||||
```typescript
|
||||
// SelectedItemsDetailInputComponent.tsx
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const urlDataSourceId = searchParams?.get("dataSourceId");
|
||||
|
||||
// 🆕 우선순위: URL > 설정 > component.id
|
||||
const dataSourceId = useMemo(
|
||||
() => urlDataSourceId || componentConfig.dataSourceId || component.id,
|
||||
[urlDataSourceId, componentConfig.dataSourceId, component.id]
|
||||
);
|
||||
|
||||
// 🆕 스토어에서 데이터 로드
|
||||
const dataRegistry = useModalDataStore((state) => state.dataRegistry);
|
||||
const modalData = dataRegistry[dataSourceId] || [];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 테스트 시나리오
|
||||
|
||||
### 기본 테스트
|
||||
1. **화면 편집기 열기**
|
||||
2. **첫 번째 화면 (선택) 만들기**:
|
||||
- TableList 추가 (item_info)
|
||||
- Button 추가 (버튼 액션: "데이터 전달 + 모달 열기")
|
||||
- **dataSourceId는 비워둠!**
|
||||
3. **두 번째 화면 (상세 입력) 만들기**:
|
||||
- SelectedItemsDetailInput 추가
|
||||
- **dataSourceId는 비워둠!**
|
||||
- 표시 컬럼 설정
|
||||
- 추가 입력 필드 설정
|
||||
4. **할당된 화면에서 테스트**:
|
||||
- 품목 3개 선택
|
||||
- "다음" 버튼 클릭
|
||||
- 상세 입력 화면에서 데이터 확인 ✅
|
||||
|
||||
### 고급 테스트 (직접 지정)
|
||||
```yaml
|
||||
시나리오: 여러 TableList가 있는 화면
|
||||
|
||||
화면 구성:
|
||||
- TableList (item_info) ← 품목
|
||||
- TableList (customer_info) ← 고객
|
||||
- Button (품목 상세입력) ← dataSourceId: "item_info"
|
||||
- Button (고객 상세입력) ← dataSourceId: "customer_info"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚨 주의사항
|
||||
|
||||
### ❌ 잘못된 사용법
|
||||
```yaml
|
||||
# 1. 같은 화면에 TableList가 여러 개 있는데 비워둠
|
||||
TableList 1: item_info
|
||||
TableList 2: customer_info
|
||||
Button: dataSourceId = (비워둠) ← 어느 것을 선택해야 할까?
|
||||
|
||||
해결: dataSourceId를 명시적으로 지정
|
||||
```
|
||||
|
||||
### ✅ 올바른 사용법
|
||||
```yaml
|
||||
# 1. TableList가 1개인 경우
|
||||
TableList: item_info
|
||||
Button: dataSourceId = (비워둠) ← 자동 감지 OK!
|
||||
|
||||
# 2. TableList가 여러 개인 경우
|
||||
TableList 1: item_info
|
||||
TableList 2: customer_info
|
||||
Button 1: dataSourceId = "item_info" ← 명시
|
||||
Button 2: dataSourceId = "customer_info" ← 명시
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 완료 체크리스트
|
||||
|
||||
### 구현 완료 ✅
|
||||
- [x] TableList → modalDataStore 자동 저장
|
||||
- [x] Button → TableList 자동 감지
|
||||
- [x] Button → URL 파라미터 전달
|
||||
- [x] SelectedItemsDetailInput → URL 자동 읽기
|
||||
- [x] 설정 패널 UI에 "자동" 힌트 추가
|
||||
|
||||
### 사용자 경험 ✅
|
||||
- [x] dataSourceId 입력 불필요 (자동)
|
||||
- [x] 일관된 데이터 흐름 (자동)
|
||||
- [x] 오류 메시지 명확 (자동)
|
||||
- [x] 직관적인 UI (자동 힌트)
|
||||
|
||||
---
|
||||
|
||||
## 📝 요약
|
||||
|
||||
**이제 사용자는 단 3단계만 하면 됩니다:**
|
||||
|
||||
1. **TableList 추가** → 테이블 선택
|
||||
2. **Button 추가** → 액션 "데이터 전달 + 모달 열기" 선택
|
||||
3. **SelectedItemsDetailInput 추가** → 필드 설정
|
||||
|
||||
**dataSourceId는 자동으로 처리됩니다!** ✨
|
||||
|
||||
---
|
||||
|
||||
## 🔗 관련 파일
|
||||
|
||||
- `frontend/stores/modalDataStore.ts` - 데이터 저장소
|
||||
- `frontend/lib/utils/buttonActions.ts` - 버튼 액션 (자동 감지)
|
||||
- `frontend/lib/registry/components/table-list/TableListComponent.tsx` - 자동 저장
|
||||
- `frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx` - URL 자동 읽기
|
||||
- `frontend/components/screen/config-panels/ButtonConfigPanel.tsx` - 버튼 설정 UI
|
||||
- `frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputConfigPanel.tsx` - 상세 입력 설정 UI
|
||||
|
||||
---
|
||||
|
||||
**🎉 완전 자동화 완료!**
|
||||
|
||||
Loading…
Reference in New Issue