feat: 화면 편집기에서 메뉴 기반 데이터 스코프 적용

- 백엔드: screenManagementService에 getMenuByScreen 함수 추가
- 백엔드: GET /api/screen-management/screens/:id/menu 엔드포인트 추가
- 프론트엔드: screenApi.getScreenMenu() 함수 추가
- ScreenDesigner: 화면 로드 시 menu_objid 자동 조회
- ScreenDesigner: menuObjid를 RealtimePreview와 UnifiedPropertiesPanel에 전달
- UnifiedPropertiesPanel: menuObjid를 DynamicComponentConfigPanel에 전달

이로써 화면 편집기에서 코드/카테고리/채번규칙이 해당 화면이 할당된 메뉴 기준으로 필터링됨
This commit is contained in:
kjs
2025-11-11 16:28:17 +09:00
parent 32d4575fb5
commit 6534d03ecd
14 changed files with 226 additions and 125 deletions

View File

@@ -50,17 +50,6 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
menuObjid, // 🆕 메뉴 OBJID
...props
}) => {
// 🚨 최우선 디버깅: 컴포넌트가 실행되는지 확인
console.log("🚨🚨🚨 SelectBasicComponent 실행됨!!!", {
componentId: component?.id,
componentType: component?.type,
webType: component?.webType,
tableName: component?.tableName,
columnName: component?.columnName,
screenId,
timestamp: new Date().toISOString(),
});
const [isOpen, setIsOpen] = useState(false);
// webTypeConfig 또는 componentConfig 사용 (DynamicWebTypeRenderer 호환성)
@@ -79,30 +68,6 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
// autocomplete의 경우 검색어 관리
const [searchQuery, setSearchQuery] = useState("");
console.log("🔍 SelectBasicComponent 초기화 (React Query):", {
componentId: component.id,
externalValue,
componentConfigValue: componentConfig?.value,
webTypeConfigValue: (props as any).webTypeConfig?.value,
configValue: config?.value,
finalSelectedValue: externalValue || config?.value || "",
tableName: component.tableName,
columnName: component.columnName,
staticCodeCategory: config?.codeCategory,
// React Query 디버깅 정보
timestamp: new Date().toISOString(),
mountCount: ++(window as any).selectMountCount || ((window as any).selectMountCount = 1),
});
// 언마운트 시 로깅
useEffect(() => {
const componentId = component.id;
console.log(`🔍 [${componentId}] SelectBasicComponent 마운트됨`);
return () => {
console.log(`🔍 [${componentId}] SelectBasicComponent 언마운트됨`);
};
}, [component.id]);
const selectRef = useRef<HTMLDivElement>(null);
@@ -117,11 +82,6 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
// 코드 카테고리 결정: 동적 카테고리 > 설정 카테고리 (메모이제이션)
const codeCategory = useMemo(() => {
const category = dynamicCodeCategory || staticCodeCategory;
console.log(`🔑 [${component.id}] 코드 카테고리 결정:`, {
dynamicCodeCategory,
staticCodeCategory,
finalCategory: category,
});
return category;
}, [dynamicCodeCategory, staticCodeCategory, component.id]);
@@ -136,32 +96,25 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
isFetching,
} = useCodeOptions(codeCategory, isCodeCategoryValid, menuObjid);
// React Query 상태 디버깅
// 디버깅: menuObjid가 제대로 전달되는지 확인
useEffect(() => {
console.log(`🎯 [${component.id}] React Query 상태:`, {
codeCategory,
isCodeCategoryValid,
codeOptionsLength: codeOptions.length,
isLoadingCodes,
isFetching,
cacheStatus: isFetching ? "FETCHING" : "FROM_CACHE",
});
}, [component.id, codeCategory, isCodeCategoryValid, codeOptions.length, isLoadingCodes, isFetching]);
if (codeCategory && codeCategory !== "none") {
console.log(`🎯 [SelectBasicComponent ${component.id}] 코드 옵션 로드:`, {
codeCategory,
menuObjid,
hasMenuObjid: !!menuObjid,
isCodeCategoryValid,
codeOptionsCount: codeOptions.length,
isLoading: isLoadingCodes,
});
}
}, [component.id, codeCategory, menuObjid, codeOptions.length, isLoadingCodes, isCodeCategoryValid]);
// 외부 value prop 변경 시 selectedValue 업데이트
useEffect(() => {
const newValue = externalValue || config?.value || "";
// 값이 실제로 다른 경우에만 업데이트 (빈 문자열도 유효한 값으로 처리)
if (newValue !== selectedValue) {
console.log(`🔄 SelectBasicComponent value 업데이트: "${selectedValue}" → "${newValue}"`);
console.log("🔍 업데이트 조건 분석:", {
externalValue,
componentConfigValue: componentConfig?.value,
configValue: config?.value,
newValue,
selectedValue,
shouldUpdate: newValue !== selectedValue,
});
setSelectedValue(newValue);
}
}, [externalValue, config?.value]);
@@ -190,23 +143,12 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
const labelMatch = options.find((option) => option.label === selectedValue);
if (labelMatch) {
newLabel = labelMatch.label;
console.log(`🔍 [${component.id}] 코드명으로 매치 발견: "${selectedValue}" → "${newLabel}"`);
} else {
// 2) selectedValue가 코드값인 경우라면 원래 로직대로 라벨을 찾되, 없으면 원값 표시
newLabel = selectedValue; // 코드값 그대로 표시 (예: "555")
console.log(`🔍 [${component.id}] 코드값 원본 유지: "${selectedValue}"`);
}
}
console.log(`🏷️ [${component.id}] 라벨 업데이트:`, {
selectedValue,
selectedOption: selectedOption ? { value: selectedOption.value, label: selectedOption.label } : null,
newLabel,
optionsCount: options.length,
allOptionsValues: options.map((o) => o.value),
allOptionsLabels: options.map((o) => o.label),
});
if (newLabel !== selectedLabel) {
setSelectedLabel(newLabel);
}
@@ -216,15 +158,6 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
const handleToggle = () => {
if (isDesignMode) return;
console.log(`🖱️ [${component.id}] 드롭다운 토글 (React Query): ${isOpen}${!isOpen}`);
console.log(`📊 [${component.id}] 현재 상태:`, {
codeCategory,
isLoadingCodes,
codeOptionsLength: codeOptions.length,
tableName: component.tableName,
columnName: component.columnName,
});
// React Query가 자동으로 캐시 관리하므로 수동 새로고침 불필요
setIsOpen(!isOpen);
};
@@ -242,17 +175,8 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
// 인터랙티브 모드에서 폼 데이터 업데이트 (TextInputComponent와 동일한 로직)
if (isInteractive && onFormDataChange && component.columnName) {
console.log(`📤 SelectBasicComponent -> onFormDataChange 호출: ${component.columnName} = "${value}"`);
onFormDataChange(component.columnName, value);
} else {
console.log("❌ SelectBasicComponent onFormDataChange 조건 미충족:", {
isInteractive,
hasOnFormDataChange: !!onFormDataChange,
hasColumnName: !!component.columnName,
});
}
console.log(`✅ [${component.id}] 옵션 선택:`, { value, label });
};
// 외부 클릭 시 드롭다운 닫기
@@ -280,12 +204,6 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
// 모든 옵션 가져오기
const getAllOptions = () => {
const configOptions = config.options || [];
console.log(`🔧 [${component.id}] 옵션 병합:`, {
codeOptionsLength: codeOptions.length,
codeOptions: codeOptions.map((o: Option) => ({ value: o.value, label: o.label })),
configOptionsLength: configOptions.length,
configOptions: configOptions.map((o: Option) => ({ value: o.value, label: o.label })),
});
return [...codeOptions, ...configOptions];
};