상세입력 컴포넌트 테이블 선택 기능 추가
This commit is contained in:
parent
a783317820
commit
bc557c4074
|
|
@ -1068,43 +1068,131 @@ export class ScreenManagementService {
|
||||||
[tableName]
|
[tableName]
|
||||||
);
|
);
|
||||||
|
|
||||||
// column_labels 테이블에서 입력타입 정보 조회 (있는 경우)
|
// 🆕 table_type_columns에서 입력타입 정보 조회 (회사별만, fallback 없음)
|
||||||
const webTypeInfo = await query<{
|
// 멀티테넌시: 각 회사는 자신의 설정만 사용, 최고관리자 설정은 별도 관리
|
||||||
|
console.log(`🔍 [getTableColumns] 시작: table=${tableName}, company=${companyCode}`);
|
||||||
|
|
||||||
|
const typeInfo = await query<{
|
||||||
column_name: string;
|
column_name: string;
|
||||||
input_type: string | null;
|
input_type: string | null;
|
||||||
column_label: string | null;
|
|
||||||
detail_settings: any;
|
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
|
FROM column_labels
|
||||||
WHERE table_name = $1`,
|
WHERE table_name = $1`,
|
||||||
[tableName]
|
[tableName]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 컬럼 정보 매핑
|
// 🆕 category_column_mapping에서 코드 카테고리 정보 조회
|
||||||
return columns.map((column: any) => {
|
const categoryInfo = await query<{
|
||||||
const webTypeData = webTypeInfo.find(
|
physical_column_name: string;
|
||||||
(wt) => wt.column_name === column.column_name
|
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,
|
tableName: tableName,
|
||||||
columnName: column.column_name,
|
columnName: column.column_name,
|
||||||
columnLabel:
|
|
||||||
webTypeData?.column_label ||
|
|
||||||
this.getColumnLabel(column.column_name),
|
|
||||||
dataType: column.data_type,
|
dataType: column.data_type,
|
||||||
webType:
|
|
||||||
(webTypeData?.input_type as WebType) ||
|
|
||||||
this.inferWebType(column.data_type),
|
|
||||||
isNullable: column.is_nullable,
|
isNullable: column.is_nullable,
|
||||||
columnDefault: column.column_default || undefined,
|
columnDefault: column.column_default || undefined,
|
||||||
characterMaximumLength: column.character_maximum_length || undefined,
|
characterMaximumLength: column.character_maximum_length || undefined,
|
||||||
numericPrecision: column.numeric_precision || undefined,
|
numericPrecision: column.numeric_precision || undefined,
|
||||||
numericScale: column.numeric_scale || 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) {
|
} catch (error) {
|
||||||
console.error("테이블 컬럼 조회 실패:", error);
|
console.error("테이블 컬럼 조회 실패:", error);
|
||||||
throw new Error("테이블 컬럼 정보를 조회할 수 없습니다.");
|
throw new Error("테이블 컬럼 정보를 조회할 수 없습니다.");
|
||||||
|
|
|
||||||
|
|
@ -165,6 +165,10 @@ export class TableManagementService {
|
||||||
const offset = (page - 1) * size;
|
const offset = (page - 1) * size;
|
||||||
|
|
||||||
// 🔥 company_code가 있으면 table_type_columns 조인하여 회사별 inputType 가져오기
|
// 🔥 company_code가 있으면 table_type_columns 조인하여 회사별 inputType 가져오기
|
||||||
|
console.log(
|
||||||
|
`🔍 [getColumnList] 시작: table=${tableName}, company=${companyCode}`
|
||||||
|
);
|
||||||
|
|
||||||
const rawColumns = companyCode
|
const rawColumns = companyCode
|
||||||
? await query<any>(
|
? await query<any>(
|
||||||
`SELECT
|
`SELECT
|
||||||
|
|
@ -174,6 +178,8 @@ export class TableManagementService {
|
||||||
c.data_type as "dbType",
|
c.data_type as "dbType",
|
||||||
COALESCE(cl.input_type, 'text') as "webType",
|
COALESCE(cl.input_type, 'text') as "webType",
|
||||||
COALESCE(ttc.input_type, cl.input_type, 'direct') as "inputType",
|
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(ttc.detail_settings::text, cl.detail_settings, '') as "detailSettings",
|
||||||
COALESCE(cl.description, '') as "description",
|
COALESCE(cl.description, '') as "description",
|
||||||
c.is_nullable as "isNullable",
|
c.is_nullable as "isNullable",
|
||||||
|
|
@ -250,6 +256,22 @@ export class TableManagementService {
|
||||||
[tableName, size, offset]
|
[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 조회
|
// 🆕 category_column_mapping 조회
|
||||||
const tableExistsResult = await query<any>(
|
const tableExistsResult = await query<any>(
|
||||||
`SELECT EXISTS (
|
`SELECT EXISTS (
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,19 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
// 전역 모달 이벤트 리스너
|
// 전역 모달 이벤트 리스너
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleOpenModal = (event: CustomEvent) => {
|
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({
|
setModalState({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
screenId,
|
screenId,
|
||||||
|
|
@ -130,6 +142,15 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseModal = () => {
|
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({
|
setModalState({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
screenId: null,
|
screenId: null,
|
||||||
|
|
|
||||||
|
|
@ -316,6 +316,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
userId={user?.userId} // ✅ 사용자 ID 전달
|
userId={user?.userId} // ✅ 사용자 ID 전달
|
||||||
userName={user?.userName} // ✅ 사용자 이름 전달
|
userName={user?.userName} // ✅ 사용자 이름 전달
|
||||||
companyCode={user?.companyCode} // ✅ 회사 코드 전달
|
companyCode={user?.companyCode} // ✅ 회사 코드 전달
|
||||||
|
allComponents={allComponents} // 🆕 같은 화면의 모든 컴포넌트 전달 (TableList 자동 감지용)
|
||||||
selectedRowsData={selectedRowsData}
|
selectedRowsData={selectedRowsData}
|
||||||
onSelectedRowsChange={(selectedRows, selectedData) => {
|
onSelectedRowsChange={(selectedRows, selectedData) => {
|
||||||
console.log("🔍 테이블에서 선택된 행 데이터:", selectedData);
|
console.log("🔍 테이블에서 선택된 행 데이터:", selectedData);
|
||||||
|
|
|
||||||
|
|
@ -419,17 +419,22 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="data-source-id">데이터 소스 ID</Label>
|
<Label htmlFor="data-source-id">
|
||||||
|
데이터 소스 ID <span className="text-primary">(선택사항)</span>
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="data-source-id"
|
id="data-source-id"
|
||||||
placeholder="예: item_info (테이블명과 동일하게 입력)"
|
placeholder="비워두면 자동으로 감지됩니다"
|
||||||
value={component.componentConfig?.action?.dataSourceId || ""}
|
value={component.componentConfig?.action?.dataSourceId || ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
onUpdateProperty("componentConfig.action.dataSourceId", e.target.value);
|
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">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
TableList에서 데이터를 저장한 ID와 동일해야 합니다 (보통 테이블명)
|
직접 지정하려면 테이블명을 입력하세요 (예: item_info)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,9 @@ export interface ButtonPrimaryComponentProps extends ComponentRendererProps {
|
||||||
// 플로우 선택된 데이터 정보 (플로우 위젯 선택 액션용)
|
// 플로우 선택된 데이터 정보 (플로우 위젯 선택 액션용)
|
||||||
flowSelectedData?: any[];
|
flowSelectedData?: any[];
|
||||||
flowSelectedStepId?: number | null;
|
flowSelectedStepId?: number | null;
|
||||||
|
|
||||||
|
// 🆕 같은 화면의 모든 컴포넌트 (TableList 자동 감지용)
|
||||||
|
allComponents?: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -88,6 +91,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
selectedRowsData,
|
selectedRowsData,
|
||||||
flowSelectedData,
|
flowSelectedData,
|
||||||
flowSelectedStepId,
|
flowSelectedStepId,
|
||||||
|
allComponents, // 🆕 같은 화면의 모든 컴포넌트
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||||
|
|
@ -409,6 +413,8 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
sortOrder, // 🆕 정렬 방향
|
sortOrder, // 🆕 정렬 방향
|
||||||
columnOrder, // 🆕 컬럼 순서
|
columnOrder, // 🆕 컬럼 순서
|
||||||
tableDisplayData, // 🆕 화면에 표시된 데이터
|
tableDisplayData, // 🆕 화면에 표시된 데이터
|
||||||
|
// 🆕 같은 화면의 모든 컴포넌트 (TableList 자동 감지용)
|
||||||
|
allComponents,
|
||||||
// 플로우 선택된 데이터 정보 추가
|
// 플로우 선택된 데이터 정보 추가
|
||||||
flowSelectedData,
|
flowSelectedData,
|
||||||
flowSelectedStepId,
|
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 { ComponentCategory } from "@/types/component";
|
||||||
import type { WebType } from "@/types/screen";
|
import type { WebType } from "@/types/screen";
|
||||||
import { ButtonPrimaryWrapper } from "./ButtonPrimaryComponent";
|
import { ButtonPrimaryWrapper } from "./ButtonPrimaryComponent";
|
||||||
import { ButtonPrimaryConfigPanel } from "./ButtonPrimaryConfigPanel";
|
|
||||||
import { ButtonPrimaryConfig } from "./types";
|
import { ButtonPrimaryConfig } from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -31,7 +30,7 @@ export const ButtonPrimaryDefinition = createComponentDefinition({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultSize: { width: 120, height: 40 },
|
defaultSize: { width: 120, height: 40 },
|
||||||
configPanel: ButtonPrimaryConfigPanel,
|
configPanel: undefined, // 상세 설정 패널(ButtonConfigPanel)이 대신 사용됨
|
||||||
icon: "MousePointer",
|
icon: "MousePointer",
|
||||||
tags: ["버튼", "액션", "클릭"],
|
tags: ["버튼", "액션", "클릭"],
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect, useMemo, useCallback } from "react";
|
import React, { useState, useEffect, useMemo, useCallback } from "react";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
import { ComponentRendererProps } from "@/types/component";
|
import { ComponentRendererProps } from "@/types/component";
|
||||||
import { SelectedItemsDetailInputConfig, AdditionalFieldDefinition } from "./types";
|
import { SelectedItemsDetailInputConfig, AdditionalFieldDefinition } from "./types";
|
||||||
import { useModalDataStore, ModalDataItem } from "@/stores/modalDataStore";
|
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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
|
import { commonCodeApi } from "@/lib/api/commonCode";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export interface SelectedItemsDetailInputComponentProps extends ComponentRendererProps {
|
export interface SelectedItemsDetailInputComponentProps extends ComponentRendererProps {
|
||||||
|
|
@ -38,6 +40,10 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||||
screenId,
|
screenId,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
|
// 🆕 URL 파라미터에서 dataSourceId 읽기
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const urlDataSourceId = searchParams?.get("dataSourceId") || undefined;
|
||||||
|
|
||||||
// 컴포넌트 설정
|
// 컴포넌트 설정
|
||||||
const componentConfig = useMemo(() => ({
|
const componentConfig = useMemo(() => ({
|
||||||
dataSourceId: component.id || "default",
|
dataSourceId: component.id || "default",
|
||||||
|
|
@ -52,13 +58,22 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||||
...component.config,
|
...component.config,
|
||||||
} as SelectedItemsDetailInputConfig), [config, component.config, component.id]);
|
} as SelectedItemsDetailInputConfig), [config, component.config, component.id]);
|
||||||
|
|
||||||
// 모달 데이터 스토어에서 데이터 가져오기
|
// 🆕 dataSourceId 우선순위: URL 파라미터 > 컴포넌트 설정 > component.id
|
||||||
// dataSourceId를 안정적으로 유지
|
|
||||||
const dataSourceId = useMemo(
|
const dataSourceId = useMemo(
|
||||||
() => componentConfig.dataSourceId || component.id || "default",
|
() => urlDataSourceId || componentConfig.dataSourceId || component.id || "default",
|
||||||
[componentConfig.dataSourceId, component.id]
|
[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 dataRegistry = useModalDataStore((state) => state.dataRegistry);
|
||||||
const modalData = useMemo(
|
const modalData = useMemo(
|
||||||
|
|
@ -71,6 +86,79 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||||
// 로컬 상태로 데이터 관리
|
// 로컬 상태로 데이터 관리
|
||||||
const [items, setItems] = useState<ModalDataItem[]>([]);
|
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(() => {
|
useEffect(() => {
|
||||||
if (modalData && modalData.length > 0) {
|
if (modalData && modalData.length > 0) {
|
||||||
|
|
@ -151,7 +239,130 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||||
required: field.required,
|
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":
|
case "select":
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
|
|
@ -172,48 +383,8 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||||
</Select>
|
</Select>
|
||||||
);
|
);
|
||||||
|
|
||||||
case "textarea":
|
// 기본값: 텍스트 입력
|
||||||
return (
|
default:
|
||||||
<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
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
|
|
@ -254,9 +425,9 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 원본 데이터 컬럼 */}
|
{/* 원본 데이터 컬럼 */}
|
||||||
{componentConfig.displayColumns?.map((colName) => (
|
{componentConfig.displayColumns?.map((col) => (
|
||||||
<TableHead key={colName} className="h-12 px-4 py-3 text-xs font-semibold sm:text-sm">
|
<TableHead key={col.name} className="h-12 px-4 py-3 text-xs font-semibold sm:text-sm">
|
||||||
{colName}
|
{col.label || col.name}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|
@ -284,9 +455,9 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 원본 데이터 표시 */}
|
{/* 원본 데이터 표시 */}
|
||||||
{componentConfig.displayColumns?.map((colName) => (
|
{componentConfig.displayColumns?.map((col) => (
|
||||||
<TableCell key={colName} className="h-14 px-4 py-3 text-xs sm:text-sm">
|
<TableCell key={col.name} className="h-14 px-4 py-3 text-xs sm:text-sm">
|
||||||
{item.originalData[colName] || "-"}
|
{item.originalData[col.name] || "-"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|
@ -349,10 +520,10 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||||
|
|
||||||
<CardContent className="space-y-3">
|
<CardContent className="space-y-3">
|
||||||
{/* 원본 데이터 표시 */}
|
{/* 원본 데이터 표시 */}
|
||||||
{componentConfig.displayColumns?.map((colName) => (
|
{componentConfig.displayColumns?.map((col) => (
|
||||||
<div key={colName} className="flex items-center justify-between text-xs sm:text-sm">
|
<div key={col.name} className="flex items-center justify-between text-xs sm:text-sm">
|
||||||
<span className="font-medium text-muted-foreground">{colName}:</span>
|
<span className="font-medium text-muted-foreground">{col.label || col.name}:</span>
|
||||||
<span>{item.originalData[colName] || "-"}</span>
|
<span>{item.originalData[col.name] || "-"}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,12 @@ import { cn } from "@/lib/utils";
|
||||||
export interface SelectedItemsDetailInputConfigPanelProps {
|
export interface SelectedItemsDetailInputConfigPanelProps {
|
||||||
config: SelectedItemsDetailInputConfig;
|
config: SelectedItemsDetailInputConfig;
|
||||||
onChange: (config: Partial<SelectedItemsDetailInputConfig>) => void;
|
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 }>;
|
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> = ({
|
export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailInputConfigPanelProps> = ({
|
||||||
config,
|
config,
|
||||||
onChange,
|
onChange,
|
||||||
tableColumns = [],
|
sourceTableColumns = [], // 🆕 원본 테이블 컬럼
|
||||||
|
targetTableColumns = [], // 🆕 대상 테이블 컬럼
|
||||||
allTables = [],
|
allTables = [],
|
||||||
onTableChange,
|
screenTableName, // 🆕 현재 화면의 테이블명
|
||||||
|
onSourceTableChange, // 🆕 원본 테이블 변경 콜백
|
||||||
|
onTargetTableChange, // 🆕 대상 테이블 변경 콜백
|
||||||
}) => {
|
}) => {
|
||||||
const [localFields, setLocalFields] = useState<AdditionalFieldDefinition[]>(config.additionalFields || []);
|
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 [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) => {
|
const handleChange = (key: keyof SelectedItemsDetailInputConfig, value: any) => {
|
||||||
onChange({ [key]: value });
|
onChange({ [key]: value });
|
||||||
};
|
};
|
||||||
|
|
@ -46,7 +73,7 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
handleChange("additionalFields", fields);
|
handleChange("additionalFields", fields);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDisplayColumnsChange = (columns: string[]) => {
|
const handleDisplayColumnsChange = (columns: Array<{ name: string; label: string; width?: string }>) => {
|
||||||
setDisplayColumns(columns);
|
setDisplayColumns(columns);
|
||||||
handleChange("displayColumns", columns);
|
handleChange("displayColumns", columns);
|
||||||
};
|
};
|
||||||
|
|
@ -74,28 +101,47 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
};
|
};
|
||||||
|
|
||||||
// 표시 컬럼 추가
|
// 표시 컬럼 추가
|
||||||
const addDisplayColumn = (columnName: string) => {
|
const addDisplayColumn = (columnName: string, columnLabel: string) => {
|
||||||
if (!displayColumns.includes(columnName)) {
|
if (!displayColumns.some(col => col.name === columnName)) {
|
||||||
handleDisplayColumnsChange([...displayColumns, columnName]);
|
handleDisplayColumnsChange([...displayColumns, { name: columnName, label: columnLabel }]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 표시 컬럼 제거
|
// 표시 컬럼 제거
|
||||||
const removeDisplayColumn = (columnName: string) => {
|
const removeDisplayColumn = (columnName: string) => {
|
||||||
handleDisplayColumnsChange(displayColumns.filter((col) => col !== columnName));
|
handleDisplayColumnsChange(displayColumns.filter((col) => col.name !== columnName));
|
||||||
};
|
};
|
||||||
|
|
||||||
// 사용되지 않은 컬럼 목록
|
// 🆕 표시 컬럼용: 원본 테이블에서 사용되지 않은 컬럼 목록
|
||||||
const availableColumns = useMemo(() => {
|
const availableColumns = useMemo(() => {
|
||||||
const usedColumns = new Set([...displayColumns, ...localFields.map((f) => f.name)]);
|
const usedColumns = new Set([...displayColumns.map(c => c.name), ...localFields.map((f) => f.name)]);
|
||||||
return tableColumns.filter((col) => !usedColumns.has(col.columnName));
|
return sourceTableColumns.filter((col) => !usedColumns.has(col.columnName));
|
||||||
}, [tableColumns, displayColumns, localFields]);
|
}, [sourceTableColumns, displayColumns, localFields]);
|
||||||
|
|
||||||
// 테이블 선택 Combobox 상태
|
// 🆕 추가 입력 필드용: 대상 테이블에서 사용되지 않은 컬럼 목록
|
||||||
const [tableSelectOpen, setTableSelectOpen] = useState(false);
|
const availableTargetColumns = useMemo(() => {
|
||||||
const [tableSearchValue, setTableSearchValue] = useState("");
|
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(() => {
|
const filteredTables = useMemo(() => {
|
||||||
if (!tableSearchValue) return allTables;
|
if (!tableSearchValue) return allTables;
|
||||||
const searchLower = tableSearchValue.toLowerCase();
|
const searchLower = tableSearchValue.toLowerCase();
|
||||||
|
|
@ -105,9 +151,9 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
);
|
);
|
||||||
}, [allTables, tableSearchValue]);
|
}, [allTables, tableSearchValue]);
|
||||||
|
|
||||||
// 선택된 테이블 표시명
|
// 선택된 대상 테이블 표시명
|
||||||
const selectedTableLabel = useMemo(() => {
|
const selectedTableLabel = useMemo(() => {
|
||||||
if (!config.targetTable) return "테이블을 선택하세요";
|
if (!config.targetTable) return "저장 대상 테이블을 선택하세요";
|
||||||
const table = allTables.find((t) => t.tableName === config.targetTable);
|
const table = allTables.find((t) => t.tableName === config.targetTable);
|
||||||
return table ? table.displayName || table.tableName : config.targetTable;
|
return table ? table.displayName || table.tableName : config.targetTable;
|
||||||
}, [config.targetTable, allTables]);
|
}, [config.targetTable, allTables]);
|
||||||
|
|
@ -116,21 +162,83 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* 데이터 소스 ID */}
|
{/* 데이터 소스 ID */}
|
||||||
<div className="space-y-2">
|
<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
|
<Input
|
||||||
value={config.dataSourceId || ""}
|
value={config.dataSourceId || ""}
|
||||||
onChange={(e) => handleChange("dataSourceId", e.target.value)}
|
onChange={(e) => handleChange("dataSourceId", e.target.value)}
|
||||||
placeholder="table-list-123"
|
placeholder="비워두면 URL 파라미터에서 자동 설정"
|
||||||
className="h-7 text-xs sm:h-8 sm:text-sm"
|
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">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 저장 대상 테이블 */}
|
{/* 저장 대상 테이블 */}
|
||||||
<div className="space-y-2">
|
<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}>
|
<Popover open={tableSelectOpen} onOpenChange={setTableSelectOpen}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -161,8 +269,8 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
handleChange("targetTable", currentValue);
|
handleChange("targetTable", currentValue);
|
||||||
setTableSelectOpen(false);
|
setTableSelectOpen(false);
|
||||||
setTableSearchValue("");
|
setTableSearchValue("");
|
||||||
if (onTableChange) {
|
if (onTargetTableChange) {
|
||||||
onTableChange(currentValue);
|
onTargetTableChange(currentValue);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="text-xs sm:text-sm"
|
className="text-xs sm:text-sm"
|
||||||
|
|
@ -187,14 +295,14 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs font-semibold sm:text-sm">표시할 원본 데이터 컬럼</Label>
|
<Label className="text-xs font-semibold sm:text-sm">표시할 원본 데이터 컬럼</Label>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{displayColumns.map((colName, index) => (
|
{displayColumns.map((col, index) => (
|
||||||
<div key={index} className="flex items-center gap-2">
|
<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
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
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"
|
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" />
|
<X className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||||
|
|
@ -223,7 +331,7 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={column.columnName}
|
key={column.columnName}
|
||||||
value={column.columnName}
|
value={column.columnName}
|
||||||
onSelect={() => addDisplayColumn(column.columnName)}
|
onSelect={() => addDisplayColumn(column.columnName, column.columnLabel || column.columnName)}
|
||||||
className="text-xs sm:text-sm"
|
className="text-xs sm:text-sm"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -284,7 +392,7 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
<CommandInput placeholder="컬럼 검색..." className="h-6 text-[10px] sm:h-7 sm:text-xs" />
|
<CommandInput placeholder="컬럼 검색..." className="h-6 text-[10px] sm:h-7 sm:text-xs" />
|
||||||
<CommandEmpty className="text-[10px] sm:text-xs">사용 가능한 컬럼이 없습니다.</CommandEmpty>
|
<CommandEmpty className="text-[10px] sm:text-xs">사용 가능한 컬럼이 없습니다.</CommandEmpty>
|
||||||
<CommandGroup className="max-h-[150px] overflow-auto sm:max-h-[200px]">
|
<CommandGroup className="max-h-[150px] overflow-auto sm:max-h-[200px]">
|
||||||
{availableColumns.map((column) => (
|
{availableTargetColumns.map((column) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={column.columnName}
|
key={column.columnName}
|
||||||
value={column.columnName}
|
value={column.columnName}
|
||||||
|
|
@ -292,6 +400,8 @@ export const SelectedItemsDetailInputConfigPanel: React.FC<SelectedItemsDetailIn
|
||||||
updateField(index, {
|
updateField(index, {
|
||||||
name: column.columnName,
|
name: column.columnName,
|
||||||
label: column.columnLabel || column.columnName,
|
label: column.columnLabel || column.columnName,
|
||||||
|
inputType: column.inputType || "text", // 🆕 inputType 포함
|
||||||
|
codeCategory: column.codeCategory, // 🆕 codeCategory 포함
|
||||||
});
|
});
|
||||||
setFieldPopoverOpen({ ...fieldPopoverOpen, [index]: false });
|
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="grid grid-cols-2 gap-2 sm:gap-3">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-[10px] sm:text-xs">타입</Label>
|
<Label className="text-[10px] sm:text-xs">타입 (자동)</Label>
|
||||||
<Select
|
<Input
|
||||||
value={field.type}
|
value={field.inputType || field.type || "text"}
|
||||||
onValueChange={(value) =>
|
readOnly
|
||||||
updateField(index, { type: value as AdditionalFieldDefinition["type"] })
|
disabled
|
||||||
}
|
className="h-6 w-full text-[10px] sm:h-7 sm:text-xs bg-muted"
|
||||||
>
|
/>
|
||||||
<SelectTrigger className="h-6 w-full text-[10px] sm:h-7 sm:text-xs">
|
<p className="text-[9px] text-primary sm:text-[10px]">
|
||||||
<SelectValue />
|
테이블 타입관리에서 자동 설정됨
|
||||||
</SelectTrigger>
|
</p>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,10 @@ export interface AdditionalFieldDefinition {
|
||||||
label: string;
|
label: string;
|
||||||
/** 입력 타입 */
|
/** 입력 타입 */
|
||||||
type: "text" | "number" | "date" | "select" | "checkbox" | "textarea";
|
type: "text" | "number" | "date" | "select" | "checkbox" | "textarea";
|
||||||
|
/** 🆕 데이터베이스 inputType (실제 렌더링 시 사용) */
|
||||||
|
inputType?: string;
|
||||||
|
/** 🆕 코드 카테고리 (inputType이 code/category일 때) */
|
||||||
|
codeCategory?: string;
|
||||||
/** 필수 입력 여부 */
|
/** 필수 입력 여부 */
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
/** 플레이스홀더 */
|
/** 플레이스홀더 */
|
||||||
|
|
@ -43,13 +47,20 @@ export interface SelectedItemsDetailInputConfig extends ComponentConfig {
|
||||||
dataSourceId?: string;
|
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[];
|
additionalFields?: AdditionalFieldDefinition[];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,9 @@ export interface ButtonActionContext {
|
||||||
flowSelectedData?: any[];
|
flowSelectedData?: any[];
|
||||||
flowSelectedStepId?: number | null;
|
flowSelectedStepId?: number | null;
|
||||||
|
|
||||||
|
// 🆕 같은 화면의 모든 컴포넌트 (TableList 자동 감지용)
|
||||||
|
allComponents?: any[];
|
||||||
|
|
||||||
// 제어 실행을 위한 추가 정보
|
// 제어 실행을 위한 추가 정보
|
||||||
buttonId?: string;
|
buttonId?: string;
|
||||||
|
|
||||||
|
|
@ -686,8 +689,28 @@ export class ButtonActionExecutor {
|
||||||
dataSourceId: config.dataSourceId,
|
dataSourceId: config.dataSourceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 1. dataSourceId 확인 (없으면 selectedRows에서 데이터 전달)
|
// 🆕 1. dataSourceId 자동 결정
|
||||||
const dataSourceId = config.dataSourceId || context.tableName || "default";
|
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에서 데이터 확인
|
// 2. modalDataStore에서 데이터 확인
|
||||||
try {
|
try {
|
||||||
|
|
@ -711,7 +734,7 @@ export class ButtonActionExecutor {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 모달 열기
|
// 3. 모달 열기 + URL 파라미터로 dataSourceId 전달
|
||||||
if (config.targetScreenId) {
|
if (config.targetScreenId) {
|
||||||
// config에 modalDescription이 있으면 우선 사용
|
// config에 modalDescription이 있으면 우선 사용
|
||||||
let description = config.modalDescription || "";
|
let description = config.modalDescription || "";
|
||||||
|
|
@ -726,13 +749,14 @@ export class ButtonActionExecutor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 전역 모달 상태 업데이트를 위한 이벤트 발생
|
// 🆕 전역 모달 상태 업데이트를 위한 이벤트 발생 (URL 파라미터 포함)
|
||||||
const modalEvent = new CustomEvent("openScreenModal", {
|
const modalEvent = new CustomEvent("openScreenModal", {
|
||||||
detail: {
|
detail: {
|
||||||
screenId: config.targetScreenId,
|
screenId: config.targetScreenId,
|
||||||
title: config.modalTitle || "데이터 입력",
|
title: config.modalTitle || "데이터 입력",
|
||||||
description: description,
|
description: description,
|
||||||
size: config.modalSize || "lg", // 데이터 입력 화면은 기본 large
|
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"),
|
"radio-basic": () => import("@/lib/registry/components/radio-basic/RadioBasicConfigPanel"),
|
||||||
"toggle-switch": () => import("@/lib/registry/components/toggle-switch/ToggleSwitchConfigPanel"),
|
"toggle-switch": () => import("@/lib/registry/components/toggle-switch/ToggleSwitchConfigPanel"),
|
||||||
"file-upload": () => import("@/lib/registry/components/file-upload/FileUploadConfigPanel"),
|
"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"),
|
"text-display": () => import("@/lib/registry/components/text-display/TextDisplayConfigPanel"),
|
||||||
"slider-basic": () => import("@/lib/registry/components/slider-basic/SliderBasicConfigPanel"),
|
"slider-basic": () => import("@/lib/registry/components/slider-basic/SliderBasicConfigPanel"),
|
||||||
"image-display": () => import("@/lib/registry/components/image-display/ImageDisplayConfigPanel"),
|
"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.FlowWidgetConfigPanel || // flow-widget의 export명
|
||||||
module.CustomerItemMappingConfigPanel || // customer-item-mapping의 export명
|
module.CustomerItemMappingConfigPanel || // customer-item-mapping의 export명
|
||||||
module.SelectedItemsDetailInputConfigPanel || // selected-items-detail-input의 export명
|
module.SelectedItemsDetailInputConfigPanel || // selected-items-detail-input의 export명
|
||||||
|
module.ButtonConfigPanel || // button-primary의 export명
|
||||||
module.default;
|
module.default;
|
||||||
|
|
||||||
if (!ConfigPanelComponent) {
|
if (!ConfigPanelComponent) {
|
||||||
|
|
@ -141,6 +142,10 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
|
||||||
const [selectedTableColumns, setSelectedTableColumns] = React.useState(tableColumns);
|
const [selectedTableColumns, setSelectedTableColumns] = React.useState(tableColumns);
|
||||||
const [allTablesList, setAllTablesList] = React.useState<any[]>([]);
|
const [allTablesList, setAllTablesList] = React.useState<any[]>([]);
|
||||||
|
|
||||||
|
// 🆕 selected-items-detail-input 전용 상태
|
||||||
|
const [sourceTableColumns, setSourceTableColumns] = React.useState<any[]>([]);
|
||||||
|
const [targetTableColumns, setTargetTableColumns] = React.useState<any[]>([]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
|
|
||||||
|
|
@ -176,14 +181,15 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
|
||||||
setSelectedTableColumns(tableColumns);
|
setSelectedTableColumns(tableColumns);
|
||||||
}, [tableColumns]);
|
}, [tableColumns]);
|
||||||
|
|
||||||
// RepeaterConfigPanel인 경우에만 전체 테이블 목록 로드
|
// RepeaterConfigPanel과 selected-items-detail-input에서 전체 테이블 목록 로드
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (componentId === "repeater-field-group") {
|
if (componentId === "repeater-field-group" || componentId === "selected-items-detail-input") {
|
||||||
const loadAllTables = async () => {
|
const loadAllTables = async () => {
|
||||||
try {
|
try {
|
||||||
const { tableManagementApi } = await import("@/lib/api/tableManagement");
|
const { tableManagementApi } = await import("@/lib/api/tableManagement");
|
||||||
const response = await tableManagementApi.getTableList();
|
const response = await tableManagementApi.getTableList();
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
|
console.log(`✅ 전체 테이블 목록 로드 완료 (${componentId}):`, response.data.length);
|
||||||
setAllTablesList(response.data);
|
setAllTablesList(response.data);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -194,6 +200,57 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
|
||||||
}
|
}
|
||||||
}, [componentId]);
|
}, [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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border border-dashed border-gray-300 bg-gray-50 p-4">
|
<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 = [
|
const isSimpleConfigPanel = [
|
||||||
"autocomplete-search-input",
|
"autocomplete-search-input",
|
||||||
|
|
@ -279,6 +393,22 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
|
||||||
return <ConfigPanelComponent config={config} onConfigChange={onChange} />;
|
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 (
|
return (
|
||||||
<ConfigPanelComponent
|
<ConfigPanelComponent
|
||||||
config={config}
|
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