fix: 컴포넌트 등록 및 entity-search 검증 개선

- 컴포넌트 등록을 useEffect → 즉시 실행으로 변경 (3개 컴포넌트)
- entity-search-input tableName 검증 추가 (FE/BE)
- DynamicComponentRenderer에서 componentConfig props 전달 수정
- EntitySearchModal key prop 경고 해결
- 불필요한 ScreenDesigner 렌더링 코드 제거

Fixes: 컴포넌트 미등록 에러, tableName undefined 500 에러, React key 경고
This commit is contained in:
SeongHyun Kim 2025-11-17 16:48:42 +09:00
parent 6839deac97
commit 23cd677413
10 changed files with 50 additions and 68 deletions

View File

@ -17,6 +17,15 @@ export async function searchEntity(req: Request, res: Response) {
limit = "20",
} = req.query;
// tableName 유효성 검증
if (!tableName || tableName === "undefined" || tableName === "null") {
logger.warn("엔티티 검색 실패: 테이블명이 없음", { tableName });
return res.status(400).json({
success: false,
message: "테이블명이 지정되지 않았습니다. 컴포넌트 설정에서 sourceTable을 확인해주세요.",
});
}
// 멀티테넌시
const companyCode = req.user!.companyCode;

View File

@ -70,9 +70,6 @@ import { toast } from "sonner";
import { MenuAssignmentModal } from "./MenuAssignmentModal";
import { FileAttachmentDetailModal } from "./FileAttachmentDetailModal";
import { initializeComponents } from "@/lib/registry/components";
import { AutocompleteSearchInputRenderer } from "@/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputRenderer";
import { EntitySearchInputRenderer } from "@/lib/registry/components/entity-search-input/EntitySearchInputRenderer";
import { ModalRepeaterTableRenderer } from "@/lib/registry/components/modal-repeater-table/ModalRepeaterTableRenderer";
import { ScreenFileAPI } from "@/lib/api/screenFile";
import { safeMigrateLayout, needsMigration } from "@/lib/utils/widthToColumnSpan";
@ -4967,12 +4964,6 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
)}
</div>
</TableOptionsProvider>
{/* 숨겨진 컴포넌트 렌더러들 (레지스트리 등록용) */}
<div style={{ display: "none" }}>
<AutocompleteSearchInputRenderer />
<EntitySearchInputRenderer />
<ModalRepeaterTableRenderer />
</div>
</ScreenPreviewProvider>
);
}

View File

@ -79,14 +79,14 @@ export const CategoryValueAddDialog: React.FC<
const valueCode = generateCode(valueLabel);
onAdd({
tableName: "",
columnName: "",
tableName: "", // CategoryValueManager에서 오버라이드됨
columnName: "", // CategoryValueManager에서 오버라이드됨
valueCode,
valueLabel: valueLabel.trim(),
description: description.trim(),
color: color,
description: description.trim() || undefined, // 빈 문자열 대신 undefined
color: color === "none" ? undefined : color, // "none"은 undefined로
isDefault: false,
});
} as TableCategoryValue);
// 초기화
setValueLabel("");

View File

@ -308,6 +308,8 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
style: finalStyle, // size를 포함한 최종 style
config: component.componentConfig,
componentConfig: component.componentConfig,
// componentConfig의 모든 속성을 props로 spread (tableName, displayField 등)
...(component.componentConfig || {}),
value: currentValue, // formData에서 추출한 현재 값 전달
// 새로운 기능들 전달
autoGeneration: component.autoGeneration || component.componentConfig?.autoGeneration,

View File

@ -1,19 +1,9 @@
"use client";
import React, { useEffect } from "react";
import { ComponentRegistry } from "../../ComponentRegistry";
import { AutocompleteSearchInputDefinition } from "./index";
export function AutocompleteSearchInputRenderer() {
useEffect(() => {
ComponentRegistry.registerComponent(AutocompleteSearchInputDefinition);
console.log("✅ AutocompleteSearchInput 컴포넌트 등록 완료");
return () => {
// 컴포넌트 언마운트 시 해제하지 않음 (싱글톤 패턴)
};
}, []);
return null;
}
// 파일 로드 시 즉시 등록
ComponentRegistry.registerComponent(AutocompleteSearchInputDefinition);
console.log("✅ AutocompleteSearchInput 컴포넌트 등록 완료");

View File

@ -1,19 +1,9 @@
"use client";
import React, { useEffect } from "react";
import { ComponentRegistry } from "../../ComponentRegistry";
import { EntitySearchInputDefinition } from "./index";
export function EntitySearchInputRenderer() {
useEffect(() => {
ComponentRegistry.registerComponent(EntitySearchInputDefinition);
console.log("✅ EntitySearchInput 컴포넌트 등록 완료");
return () => {
// 컴포넌트 언마운트 시 해제하지 않음 (싱글톤 패턴)
};
}, []);
return null;
}
// 파일 로드 시 즉시 등록
ComponentRegistry.registerComponent(EntitySearchInputDefinition);
console.log("✅ EntitySearchInput 컴포넌트 등록 완료");

View File

@ -155,17 +155,19 @@ export function EntitySearchModal({
</td>
</tr>
) : (
results.map((item, index) => (
<tr
key={item[valueField] || index}
className="border-t hover:bg-accent cursor-pointer transition-colors"
onClick={() => handleSelect(item)}
>
{displayColumns.map((col, colIndex) => (
<td key={`${item[valueField] || index}-${col}-${colIndex}`} className="px-4 py-2">
{item[col] || "-"}
</td>
))}
results.map((item, index) => {
const uniqueKey = item[valueField] !== undefined ? `${item[valueField]}` : `row-${index}`;
return (
<tr
key={uniqueKey}
className="border-t hover:bg-accent cursor-pointer transition-colors"
onClick={() => handleSelect(item)}
>
{displayColumns.map((col) => (
<td key={`${uniqueKey}-${col}`} className="px-4 py-2">
{item[col] || "-"}
</td>
))}
<td className="px-4 py-2">
<Button
size="sm"
@ -180,7 +182,8 @@ export function EntitySearchModal({
</Button>
</td>
</tr>
))
);
})
)}
</tbody>
</table>

View File

@ -34,6 +34,13 @@ export function useEntitySearch({
const search = useCallback(
async (text: string, page: number = 1) => {
// tableName 유효성 검증
if (!tableName || tableName === "undefined" || tableName === "null") {
console.warn("엔티티 검색 건너뜀: tableName이 없음", { tableName });
setError("테이블명이 설정되지 않았습니다. 컴포넌트 설정을 확인해주세요.");
return;
}
try {
setLoading(true);
setError(null);

View File

@ -46,9 +46,9 @@ import "./table-search-widget"; // 🆕 테이블 검색 필터 위젯
import "./customer-item-mapping/CustomerItemMappingRenderer"; // 🆕 거래처별 품목정보
// 🆕 수주 등록 관련 컴포넌트들
import { AutocompleteSearchInputRenderer } from "./autocomplete-search-input/AutocompleteSearchInputRenderer";
import { EntitySearchInputRenderer } from "./entity-search-input/EntitySearchInputRenderer";
import { ModalRepeaterTableRenderer } from "./modal-repeater-table/ModalRepeaterTableRenderer";
import "./autocomplete-search-input/AutocompleteSearchInputRenderer";
import "./entity-search-input/EntitySearchInputRenderer";
import "./modal-repeater-table/ModalRepeaterTableRenderer";
import "./order-registration-modal/OrderRegistrationModalRenderer";
// 🆕 조건부 컨테이너 컴포넌트

View File

@ -1,19 +1,9 @@
"use client";
import React, { useEffect } from "react";
import { ComponentRegistry } from "../../ComponentRegistry";
import { ModalRepeaterTableDefinition } from "./index";
export function ModalRepeaterTableRenderer() {
useEffect(() => {
ComponentRegistry.registerComponent(ModalRepeaterTableDefinition);
console.log("✅ ModalRepeaterTable 컴포넌트 등록 완료");
return () => {
// 컴포넌트 언마운트 시 해제하지 않음 (싱글톤 패턴)
};
}, []);
return null;
}
// 파일 로드 시 즉시 등록
ComponentRegistry.registerComponent(ModalRepeaterTableDefinition);
console.log("✅ ModalRepeaterTable 컴포넌트 등록 완료");