From 84d4d49bd5170a87f316d923a79873b86ad52558 Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 19 Sep 2025 15:22:25 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A9=94=EB=89=B4=EC=83=9D=EC=84=B1=EC=8B=9C?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4=ED=95=A0=EB=8B=B9=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/admin/MenuFormModal.tsx | 340 +++++++++- .../select-basic/SelectBasicComponent.tsx | 27 +- 화면관리_타입_문제_분석_및_해결방안.md | 605 ++++++++++++++++++ 3 files changed, 962 insertions(+), 10 deletions(-) create mode 100644 화면관리_타입_문제_분석_및_해결방안.md diff --git a/frontend/components/admin/MenuFormModal.tsx b/frontend/components/admin/MenuFormModal.tsx index be3c2132..f87b4905 100644 --- a/frontend/components/admin/MenuFormModal.tsx +++ b/frontend/components/admin/MenuFormModal.tsx @@ -3,14 +3,18 @@ import React, { useState, useEffect } from "react"; import { MenuItem, MenuFormData, menuApi, LangKey } from "@/lib/api/menu"; import { companyAPI } from "@/lib/api/company"; +import { screenApi } from "@/lib/api/screen"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { toast } from "sonner"; +import { ChevronDown, Search } from "lucide-react"; import { MENU_MANAGEMENT_KEYS } from "@/lib/utils/multilang"; +import { ScreenDefinition } from "@/types/screen"; interface Company { company_code: string; @@ -70,6 +74,13 @@ export const MenuFormModal: React.FC = ({ langKey: "", }); + // 화면 할당 관련 상태 + const [urlType, setUrlType] = useState<"direct" | "screen">("screen"); // URL 직접 입력 or 화면 할당 (기본값: 화면 할당) + const [selectedScreen, setSelectedScreen] = useState(null); + const [screens, setScreens] = useState([]); + const [screenSearchText, setScreenSearchText] = useState(""); + const [isScreenDropdownOpen, setIsScreenDropdownOpen] = useState(false); + const [loading, setLoading] = useState(false); const [isEdit, setIsEdit] = useState(false); const [companies, setCompanies] = useState([]); @@ -77,6 +88,132 @@ export const MenuFormModal: React.FC = ({ const [isLangKeyDropdownOpen, setIsLangKeyDropdownOpen] = useState(false); const [langKeySearchText, setLangKeySearchText] = useState(""); + // 화면 목록 로드 + const loadScreens = async () => { + try { + const response = await screenApi.getScreens({ size: 1000 }); // 모든 화면 가져오기 + + console.log("🔍 화면 목록 로드 디버깅:", { + totalScreens: response.data.length, + firstScreen: response.data[0], + firstScreenFields: response.data[0] ? Object.keys(response.data[0]) : [], + firstScreenValues: response.data[0] ? Object.values(response.data[0]) : [], + allScreenIds: response.data + .map((s) => ({ + screenId: s.screenId, + legacyId: s.id, + name: s.screenName, + code: s.screenCode, + })) + .slice(0, 5), // 처음 5개만 출력 + }); + + setScreens(response.data); + console.log("✅ 화면 목록 로드 완료:", response.data.length); + } catch (error) { + console.error("❌ 화면 목록 로드 실패:", error); + toast.error("화면 목록을 불러오는데 실패했습니다."); + } + }; + + // 화면 선택 시 URL 자동 설정 + const handleScreenSelect = (screen: ScreenDefinition) => { + console.log("🖥️ 화면 선택 디버깅:", { + screen, + screenId: screen.screenId, + screenIdType: typeof screen.screenId, + legacyId: screen.id, + allFields: Object.keys(screen), + screenValues: Object.values(screen), + }); + + // ScreenDefinition에서는 screenId 필드를 사용 + const actualScreenId = screen.screenId || screen.id; + + if (!actualScreenId) { + console.error("❌ 화면 ID를 찾을 수 없습니다:", screen); + toast.error("화면 ID를 찾을 수 없습니다. 다른 화면을 선택해주세요."); + return; + } + + setSelectedScreen(screen); + setIsScreenDropdownOpen(false); + + // 실제 라우팅 패턴에 맞게 URL 생성: /screens/[screenId] (복수형) + // 관리자 메뉴인 경우 mode=admin 파라미터 추가 + let screenUrl = `/screens/${actualScreenId}`; + + // 현재 메뉴 타입이 관리자인지 확인 (0 또는 "admin") + const isAdminMenu = menuType === "0" || menuType === "admin" || formData.menuType === "0"; + if (isAdminMenu) { + screenUrl += "?mode=admin"; + } + + setFormData((prev) => ({ + ...prev, + menuUrl: screenUrl, + })); + + console.log("🖥️ 화면 선택 완료:", { + screenId: screen.screenId, + legacyId: screen.id, + actualScreenId, + screenName: screen.screenName, + menuType: menuType, + formDataMenuType: formData.menuType, + isAdminMenu, + generatedUrl: screenUrl, + }); + }; + + // URL 타입 변경 시 처리 + const handleUrlTypeChange = (type: "direct" | "screen") => { + console.log("🔄 URL 타입 변경:", { + from: urlType, + to: type, + currentSelectedScreen: selectedScreen?.screenName, + currentUrl: formData.menuUrl, + }); + + setUrlType(type); + + if (type === "direct") { + // 직접 입력 모드로 변경 시 선택된 화면 초기화 + setSelectedScreen(null); + // URL 필드도 초기화 (사용자가 직접 입력할 수 있도록) + setFormData((prev) => ({ + ...prev, + menuUrl: "", + })); + } else { + // 화면 할당 모드로 변경 시 + // 기존에 선택된 화면이 있고, 해당 화면의 URL이 있다면 유지 + if (selectedScreen) { + console.log("📋 기존 선택된 화면 유지:", selectedScreen.screenName); + // 현재 선택된 화면으로 URL 재생성 + const actualScreenId = selectedScreen.screenId || selectedScreen.id; + let screenUrl = `/screens/${actualScreenId}`; + + // 관리자 메뉴인 경우 mode=admin 파라미터 추가 + const isAdminMenu = menuType === "0" || menuType === "admin" || formData.menuType === "0"; + if (isAdminMenu) { + screenUrl += "?mode=admin"; + } + + setFormData((prev) => ({ + ...prev, + menuUrl: screenUrl, + })); + } else { + // 선택된 화면이 없으면 URL만 초기화 + setFormData((prev) => ({ + ...prev, + menuUrl: "", + })); + } + } + }; + // loadMenuData 함수를 먼저 정의 const loadMenuData = async () => { console.log("loadMenuData 호출됨 - menuId:", menuId); @@ -124,11 +261,16 @@ export const MenuFormModal: React.FC = ({ convertedStatus = "INACTIVE"; } + const menuUrl = menu.menu_url || menu.MENU_URL || ""; + + // URL이 "/screens/"로 시작하면 화면 할당으로 판단 (실제 라우팅 패턴에 맞게 수정) + const isScreenUrl = menuUrl.startsWith("/screens/"); + setFormData({ objid: menu.objid || menu.OBJID, parentObjId: menu.parent_obj_id || menu.PARENT_OBJ_ID || "0", menuNameKor: menu.menu_name_kor || menu.MENU_NAME_KOR || "", - menuUrl: menu.menu_url || menu.MENU_URL || "", + menuUrl: menuUrl, menuDesc: menu.menu_desc || menu.MENU_DESC || "", seq: menu.seq || menu.SEQ || 1, menuType: convertedMenuType, @@ -137,6 +279,57 @@ export const MenuFormModal: React.FC = ({ langKey: langKey, // 다국어 키 설정 }); + // URL 타입 설정 + if (isScreenUrl) { + setUrlType("screen"); + // "/screens/123" 또는 "/screens/123?mode=admin" 형태에서 ID 추출 + const screenId = menuUrl.match(/\/screens\/(\d+)/)?.[1]; + if (screenId) { + console.log("🔍 기존 메뉴에서 화면 ID 추출:", { + menuUrl, + screenId, + hasAdminParam: menuUrl.includes("mode=admin"), + currentScreensCount: screens.length, + }); + + // 화면 설정 함수 + const setScreenFromId = () => { + const screen = screens.find((s) => s.screenId.toString() === screenId || s.id?.toString() === screenId); + if (screen) { + setSelectedScreen(screen); + console.log("🖥️ 기존 메뉴의 할당된 화면 설정:", { + screen, + originalUrl: menuUrl, + hasAdminParam: menuUrl.includes("mode=admin"), + }); + return true; + } else { + console.warn("⚠️ 해당 ID의 화면을 찾을 수 없음:", { + screenId, + availableScreens: screens.map((s) => ({ screenId: s.screenId, id: s.id, name: s.screenName })), + }); + return false; + } + }; + + // 화면 목록이 이미 있으면 즉시 설정, 없으면 로드 완료 대기 + if (screens.length > 0) { + console.log("📋 화면 목록이 이미 로드됨 - 즉시 설정"); + setScreenFromId(); + } else { + console.log("⏳ 화면 목록 로드 대기 중..."); + // 화면 ID를 저장해두고, 화면 목록 로드 완료 후 설정 + setTimeout(() => { + console.log("🔄 재시도: 화면 목록 로드 후 설정"); + setScreenFromId(); + }, 500); + } + } + } else { + setUrlType("direct"); + setSelectedScreen(null); + } + console.log("설정된 폼 데이터:", { objid: menu.objid || menu.OBJID, parentObjId: menu.parent_obj_id || menu.PARENT_OBJ_ID || "0", @@ -237,6 +430,35 @@ export const MenuFormModal: React.FC = ({ } }, [isOpen, formData.companyCode]); + // 화면 목록 로드 + useEffect(() => { + if (isOpen) { + loadScreens(); + } + }, [isOpen]); + + // 화면 목록 로드 완료 후 기존 메뉴의 할당된 화면 설정 + useEffect(() => { + if (screens.length > 0 && isEdit && formData.menuUrl && urlType === "screen") { + const menuUrl = formData.menuUrl; + if (menuUrl.startsWith("/screens/")) { + const screenId = menuUrl.match(/\/screens\/(\d+)/)?.[1]; + if (screenId && !selectedScreen) { + console.log("🔄 화면 목록 로드 완료 - 기존 할당 화면 자동 설정"); + const screen = screens.find((s) => s.screenId.toString() === screenId || s.id?.toString() === screenId); + if (screen) { + setSelectedScreen(screen); + console.log("✅ 기존 메뉴의 할당된 화면 자동 설정 완료:", { + screenId, + screenName: screen.screenName, + menuUrl, + }); + } + } + } + } + }, [screens, isEdit, formData.menuUrl, urlType, selectedScreen]); + // 드롭다운 외부 클릭 시 닫기 useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -245,16 +467,20 @@ export const MenuFormModal: React.FC = ({ setIsLangKeyDropdownOpen(false); setLangKeySearchText(""); } + if (!target.closest(".screen-dropdown")) { + setIsScreenDropdownOpen(false); + setScreenSearchText(""); + } }; - if (isLangKeyDropdownOpen) { + if (isLangKeyDropdownOpen || isScreenDropdownOpen) { document.addEventListener("mousedown", handleClickOutside); } return () => { document.removeEventListener("mousedown", handleClickOutside); }; - }, [isLangKeyDropdownOpen]); + }, [isLangKeyDropdownOpen, isScreenDropdownOpen]); const loadCompanies = async () => { try { @@ -516,12 +742,108 @@ export const MenuFormModal: React.FC = ({
- handleInputChange("menuUrl", e.target.value)} - placeholder={getText(MENU_MANAGEMENT_KEYS.FORM_MENU_URL_PLACEHOLDER)} - /> + + {/* URL 타입 선택 */} + +
+ + +
+
+ + +
+
+ + {/* 화면 할당 */} + {urlType === "screen" && ( +
+ {/* 화면 선택 드롭다운 */} +
+ + + {isScreenDropdownOpen && ( +
+ {/* 검색 입력 */} +
+
+ + setScreenSearchText(e.target.value)} + className="pl-8" + /> +
+
+ + {/* 화면 목록 */} +
+ {screens + .filter( + (screen) => + screen.screenName.toLowerCase().includes(screenSearchText.toLowerCase()) || + screen.screenCode.toLowerCase().includes(screenSearchText.toLowerCase()), + ) + .map((screen, index) => ( +
handleScreenSelect(screen)} + className="cursor-pointer border-b px-3 py-2 last:border-b-0 hover:bg-gray-100" + > +
+
+
{screen.screenName}
+
{screen.screenCode}
+
+
ID: {screen.screenId || screen.id || "N/A"}
+
+
+ ))} + {screens.filter( + (screen) => + screen.screenName.toLowerCase().includes(screenSearchText.toLowerCase()) || + screen.screenCode.toLowerCase().includes(screenSearchText.toLowerCase()), + ).length === 0 &&
검색 결과가 없습니다.
} +
+
+ )} +
+ + {/* 선택된 화면 정보 표시 */} + {selectedScreen && ( +
+
{selectedScreen.screenName}
+
코드: {selectedScreen.screenCode}
+
생성된 URL: {formData.menuUrl}
+
+ )} +
+ )} + + {/* URL 직접 입력 */} + {urlType === "direct" && ( + handleInputChange("menuUrl", e.target.value)} + placeholder={getText(MENU_MANAGEMENT_KEYS.FORM_MENU_URL_PLACEHOLDER)} + /> + )}
diff --git a/frontend/lib/registry/components/select-basic/SelectBasicComponent.tsx b/frontend/lib/registry/components/select-basic/SelectBasicComponent.tsx index 06798417..61490c8e 100644 --- a/frontend/lib/registry/components/select-basic/SelectBasicComponent.tsx +++ b/frontend/lib/registry/components/select-basic/SelectBasicComponent.tsx @@ -406,7 +406,32 @@ const SelectBasicComponent: React.FC = ({ const options = getAllOptions(); const selectedOption = options.find((option) => option.value === selectedValue); - const newLabel = selectedOption?.label || ""; + + // 🎯 코드 타입의 경우 코드값과 코드명을 모두 고려하여 라벨 찾기 + let newLabel = selectedOption?.label || ""; + + // selectedOption이 없고 selectedValue가 있다면, 코드명으로도 검색해보기 + if (!selectedOption && selectedValue && codeOptions.length > 0) { + // 1) selectedValue가 코드명인 경우 (예: "국내") + 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); diff --git a/화면관리_타입_문제_분석_및_해결방안.md b/화면관리_타입_문제_분석_및_해결방안.md new file mode 100644 index 00000000..e299872c --- /dev/null +++ b/화면관리_타입_문제_분석_및_해결방안.md @@ -0,0 +1,605 @@ +# 화면관리 시스템 타입 문제 분석 및 해결방안 + +## 📋 현재 상황 분석 + +### 주요 시스템들 + +1. **화면관리 시스템** (Screen Management) +2. **제어관리 시스템** (Button Dataflow Control) +3. **테이블 타입관리 시스템** (Table Type Management) + +### 발견된 문제점들 + +## 🚨 1. 타입 정의 분산 및 중복 문제 + +### 1.1 WebType 타입 정의 분산 + +**문제**: WebType이 여러 파일에서 서로 다르게 정의되어 불일치 발생 + +#### 현재 상황: + +- `frontend/types/screen.ts`: 화면관리용 WebType 정의 +- `backend-node/src/types/tableManagement.ts`: 테이블관리용 타입 정의 +- `backend-node/prisma/schema.prisma`: DB 스키마의 web_type_standards 모델 +- `frontend/lib/registry/types.ts`: 레지스트리용 WebType 정의 + +#### 구체적 충돌 사례: + +```typescript +// frontend/types/screen.ts +export type WebType = + | "text" + | "number" + | "date" + | "code" + | "entity" + | "textarea" + | "boolean" + | "decimal" + | "button" + | "datetime" + | "dropdown" + | "text_area" + | "checkbox" + | "radio" + | "file" + | "email" + | "tel" + | "url"; + +// 실제 DB에서는 다른 web_type 값들이 존재할 수 있음 +// 예: "varchar", "integer", "timestamp" 등 +``` + +### 1.2 ButtonActionType 중복 정의 + +**문제**: 버튼 액션 타입이 여러 곳에서 다르게 정의됨 + +#### 충돌 위치: + +- `frontend/types/screen.ts`: `"control"` 포함, `"modal"` 포함 +- `frontend/lib/utils/buttonActions.ts`: `"cancel"` 포함, `"modal"` 포함 +- `frontend/hooks/admin/useButtonActions.ts`: DB 스키마 기반 정의 + +#### 문제 코드: + +```typescript +// frontend/types/screen.ts +export type ButtonActionType = + | "save" + | "delete" + | "edit" + | "add" + | "search" + | "reset" + | "submit" + | "close" + | "popup" + | "modal" + | "newWindow" + | "navigate" + | "control"; + +// frontend/lib/utils/buttonActions.ts +export type ButtonActionType = + | "save" + | "cancel" + | "delete" + | "edit" + | "add" + | "search" + | "reset" + | "submit" + | "close" + | "popup" + | "navigate" + | "modal" + | "newWindow"; +``` + +## 🚨 2. 데이터베이스 스키마와 TypeScript 타입 불일치 + +### 2.1 web_type_standards 테이블 불일치 + +**문제**: Prisma 스키마와 TypeScript 인터페이스 간 필드명/타입 차이 + +#### DB 스키마: + +```sql +model web_type_standards { + web_type String @id @db.VarChar(50) + type_name String @db.VarChar(100) + type_name_eng String? @db.VarChar(100) + description String? + category String? @default("input") @db.VarChar(50) + default_config Json? -- JSON 타입 + validation_rules Json? -- JSON 타입 + component_name String? @default("TextWidget") @db.VarChar(100) + config_panel String? @db.VarChar(100) +} +``` + +#### TypeScript 인터페이스: + +```typescript +export interface WebTypeDefinition { + id: string; // web_type와 매핑되지 않음 + name: string; // type_name과 매핑? + category: string; + description: string; + defaultConfig: Record; // default_config Json과 타입 불일치 + validationRules?: Record; // validation_rules Json과 타입 불일치 + isActive: boolean; // DB에는 is_active String 필드 +} +``` + +### 2.2 ColumnInfo 타입 불일치 + +**문제**: 테이블 컬럼 정보 타입이 프론트엔드/백엔드에서 다름 + +#### 백엔드 타입: + +```typescript +// backend-node/src/types/tableManagement.ts +export interface ColumnTypeInfo { + columnName: string; + displayName: string; + dataType: string; + dbType: string; + webType: string; // string 타입 + inputType?: "direct" | "auto"; + detailSettings: string; // JSON 문자열 + isNullable: string; // "Y" | "N" 문자열 + isPrimaryKey: boolean; +} +``` + +#### 프론트엔드 타입: + +```typescript +// frontend/types/screen.ts +export interface ColumnInfo { + tableName: string; + columnName: string; + columnLabel?: string; + dataType: string; + webType?: WebType; // WebType union 타입 (불일치!) + inputType?: "direct" | "auto"; + isNullable: string; + detailSettings?: string; // optional vs required 차이 +} +``` + +## 🚨 3. 컴포넌트 인터페이스 타입 안전성 문제 + +### 3.1 ComponentData 타입 캐스팅 문제 + +**문제**: 런타임에 타입 안전성이 보장되지 않는 강제 캐스팅 + +#### 문제 코드: + +```typescript +// frontend/components/screen/RealtimePreview.tsx +const widget = component as WidgetComponent; // 위험한 강제 캐스팅 + +// frontend/components/screen/InteractiveScreenViewer.tsx +component: any; // any 타입으로 타입 안전성 상실 +``` + +### 3.2 DynamicWebTypeRenderer Props 불일치 + +**문제**: 동적 렌더링 시 props 타입이 일관되지 않음 + +#### 문제 위치: + +```typescript +// frontend/lib/registry/DynamicWebTypeRenderer.tsx +export interface DynamicComponentProps { + webType: string; // WebType이 아닌 string + props?: Record; // any 타입 사용 + config?: Record; // any 타입 사용 + onEvent?: (event: string, data: any) => void; // any 타입 +} + +// 실제 사용 시 + {...} // any 타입 콜백 + }} +/> +``` + +## 🚨 4. 제어관리 시스템 타입 문제 + +### 4.1 ButtonDataflowConfig 타입 복잡성 + +**문제**: 제어관리 설정이 복잡하고 타입 안전성 부족 + +#### 현재 타입: + +```typescript +export interface ButtonDataflowConfig { + controlMode: "simple" | "advanced"; + selectedDiagramId?: number; + selectedRelationshipId?: number; + directControl?: { + conditions: DataflowCondition[]; // 복잡한 중첩 타입 + actions: any[]; // any 타입 사용 + }; +} +``` + +### 4.2 OptimizedButtonDataflowService 타입 문제 + +**문제**: 서비스 클래스에서 any 타입 남용으로 타입 안전성 상실 + +#### Linter 오류 (57개): + +- `Unexpected any` 경고 26개 +- `unknown` 타입 오류 2개 +- 사용되지 않는 변수 경고 29개 + +## 🎯 해결방안 및 구현 계획 + +## Phase 1: 중앙집중식 타입 정의 통합 (우선순위: 높음) + +### 1.1 통합 타입 파일 생성 + +``` +frontend/types/ +├── unified-core.ts # 핵심 공통 타입들 +├── screen-management.ts # 화면관리 전용 타입 +├── control-management.ts # 제어관리 전용 타입 +├── table-management.ts # 테이블관리 전용 타입 +└── index.ts # 모든 타입 re-export +``` + +### 1.2 WebType 통합 정의 + +```typescript +// frontend/types/unified-core.ts +export type WebType = + | "text" + | "number" + | "decimal" + | "date" + | "datetime" + | "select" + | "dropdown" + | "radio" + | "checkbox" + | "boolean" + | "textarea" + | "code" + | "entity" + | "file" + | "email" + | "tel" + | "url" + | "button"; + +// DB에서 동적으로 로드되는 웹타입도 지원 +export type DynamicWebType = WebType | string; +``` + +### 1.3 ButtonActionType 통합 정의 + +```typescript +// frontend/types/unified-core.ts +export type ButtonActionType = + | "save" + | "cancel" + | "delete" + | "edit" + | "add" + | "search" + | "reset" + | "submit" + | "close" + | "popup" + | "modal" + | "navigate" + | "control"; +``` + +## Phase 2: 데이터베이스 타입 매핑 표준화 (우선순위: 높음) + +### 2.1 Prisma 스키마 기반 타입 생성 + +```typescript +// frontend/types/database-mappings.ts +import { web_type_standards, button_action_standards } from "@prisma/client"; + +// Prisma 타입을 프론트엔드 타입으로 변환하는 매퍼 +export type WebTypeStandard = web_type_standards; + +export interface WebTypeDefinition { + webType: string; // web_type 필드 + typeName: string; // type_name 필드 + typeNameEng?: string; // type_name_eng 필드 + description?: string; + category: string; + defaultConfig: Record; // Json 타입 매핑 + validationRules?: Record; // Json 타입 매핑 + componentName?: string; // component_name 필드 + configPanel?: string; // config_panel 필드 + isActive: boolean; // is_active "Y"/"N" → boolean 변환 +} + +// 변환 함수 +export const mapWebTypeStandardToDefinition = ( + standard: WebTypeStandard +): WebTypeDefinition => ({ + webType: standard.web_type, + typeName: standard.type_name, + typeNameEng: standard.type_name_eng || undefined, + description: standard.description || undefined, + category: standard.category || "input", + defaultConfig: (standard.default_config as any) || {}, + validationRules: (standard.validation_rules as any) || undefined, + componentName: standard.component_name || undefined, + configPanel: standard.config_panel || undefined, + isActive: standard.is_active === "Y", +}); +``` + +### 2.2 ColumnInfo 타입 통합 + +```typescript +// frontend/types/table-management.ts +export interface UnifiedColumnInfo { + // 공통 필드 + tableName: string; + columnName: string; + displayName: string; + dataType: string; // DB 데이터 타입 + webType: DynamicWebType; // 웹 타입 (동적 지원) + + // 상세 정보 + inputType: "direct" | "auto"; + detailSettings?: Record; // JSON 파싱된 객체 + description?: string; + isNullable: boolean; // "Y"/"N" → boolean 변환 + isPrimaryKey: boolean; + + // 표시 옵션 + isVisible?: boolean; + displayOrder?: number; + + // 메타데이터 + maxLength?: number; + numericPrecision?: number; + numericScale?: number; + defaultValue?: string; + + // 참조 관계 + codeCategory?: string; + referenceTable?: string; + referenceColumn?: string; + displayColumn?: string; +} +``` + +## Phase 3: 컴포넌트 타입 안전성 강화 (우선순위: 중간) + +### 3.1 ComponentData 타입 가드 구현 + +```typescript +// frontend/types/screen-management.ts +export type ComponentData = + | ContainerComponent + | WidgetComponent + | GroupComponent + | DataTableComponent; + +// 타입 가드 함수들 +export const isWidgetComponent = ( + component: ComponentData +): component is WidgetComponent => { + return component.type === "widget"; +}; + +export const isContainerComponent = ( + component: ComponentData +): component is ContainerComponent => { + return component.type === "container"; +}; + +// 안전한 타입 캐스팅 유틸리티 +export const asWidgetComponent = ( + component: ComponentData +): WidgetComponent => { + if (!isWidgetComponent(component)) { + throw new Error(`Expected WidgetComponent, got ${component.type}`); + } + return component; +}; +``` + +### 3.2 DynamicWebTypeRenderer Props 타입 강화 + +```typescript +// frontend/lib/registry/types.ts +export interface StrictDynamicComponentProps { + webType: DynamicWebType; + component: ComponentData; + config?: WebTypeConfig; + value?: unknown; + onChange?: (value: unknown) => void; + onEvent?: (event: WebTypeEvent) => void; + readonly?: boolean; + required?: boolean; + className?: string; +} + +export interface WebTypeEvent { + type: "change" | "blur" | "focus" | "click"; + value: unknown; + field?: string; +} + +export type WebTypeConfig = Record; +``` + +## Phase 4: 제어관리 시스템 타입 정리 (우선순위: 중간) + +### 4.1 ButtonDataflowConfig 타입 명확화 + +```typescript +// frontend/types/control-management.ts +export interface ButtonDataflowConfig { + // 기본 설정 + controlMode: "simple" | "advanced"; + + // 관계도 방식 + selectedDiagramId?: number; + selectedRelationshipId?: number; + + // 직접 설정 방식 + directControl?: DirectControlConfig; +} + +export interface DirectControlConfig { + conditions: DataflowCondition[]; + actions: DataflowAction[]; + logic?: "AND" | "OR"; +} + +export interface DataflowCondition { + id: string; + type: "condition" | "group"; + field?: string; + operator?: ConditionOperator; + value?: unknown; + dataSource?: "form" | "table-selection" | "both"; +} + +export interface DataflowAction { + id: string; + type: ActionType; + tableName?: string; + operation?: "INSERT" | "UPDATE" | "DELETE" | "SELECT"; + fields?: ActionField[]; + conditions?: DataflowCondition[]; +} + +export type ConditionOperator = + | "=" + | "!=" + | ">" + | "<" + | ">=" + | "<=" + | "LIKE" + | "IN" + | "NOT IN"; +export type ActionType = "database" | "api" | "notification" | "redirect"; +``` + +### 4.2 OptimizedButtonDataflowService 타입 정리 + +```typescript +// frontend/lib/services/optimizedButtonDataflowService.ts + +// any 타입 제거 및 구체적 타입 정의 +export interface ExecutionContext { + formData: Record; + selectedRows?: unknown[]; + selectedRowsData?: Record[]; + controlDataSource: ControlDataSource; + buttonId: string; + componentData?: ComponentData; + timestamp: string; + clickCount?: number; +} + +export interface ActionResult { + success: boolean; + message: string; + data?: Record; + error?: string; +} + +export interface ValidationResult { + success: boolean; + message?: string; + canExecuteImmediately: boolean; + actions?: DataflowAction[]; +} +``` + +## Phase 5: 마이그레이션 및 검증 (우선순위: 낮음) + +### 5.1 점진적 마이그레이션 계획 + +1. **Step 1**: 새로운 통합 타입 파일들 생성 +2. **Step 2**: 기존 파일들에서 새 타입 import로 변경 +3. **Step 3**: 타입 가드 및 유틸리티 함수 적용 +4. **Step 4**: any 타입 제거 및 구체적 타입 적용 +5. **Step 5**: 기존 타입 정의 파일들 제거 + +### 5.2 검증 도구 구축 + +```typescript +// scripts/type-validation.ts +// 타입 일관성 검증 스크립트 작성 +// DB 스키마와 TypeScript 타입 간 일치성 검증 +// 컴포넌트 Props 타입 검증 +``` + +## 📋 구현 우선순위 + +### 🔥 즉시 해결 필요 (Critical) + +1. **WebType 통합** - 가장 많이 사용되는 기본 타입 +2. **ButtonActionType 통합** - 제어관리 시스템 안정성 확보 +3. **ColumnInfo 타입 표준화** - 테이블 관리 기능 정상화 + +### ⚡ 단기간 해결 (High) + +4. **ComponentData 타입 가드** - 런타임 안전성 확보 +5. **DB 타입 매핑** - 프론트엔드/백엔드 연동 안정화 +6. **DynamicWebTypeRenderer Props 정리** - 동적 렌더링 안정성 + +### 📅 중장기 해결 (Medium) + +7. **OptimizedButtonDataflowService any 타입 제거** - 코드 품질 향상 +8. **ButtonDataflowConfig 구조 개선** - 제어관리 시스템 고도화 +9. **타입 검증 도구 구축** - 지속적인 품질 관리 + +## 💡 기대 효과 + +### 개발 경험 개선 + +- 타입 자동완성 정확도 향상 +- 컴파일 타임 오류 감소 +- IDE 지원 기능 활용도 증대 + +### 시스템 안정성 향상 + +- 런타임 타입 오류 방지 +- API 연동 안정성 확보 +- 데이터 일관성 보장 + +### 유지보수성 향상 + +- 코드 가독성 개선 +- 리팩토링 안정성 확보 +- 새 기능 추가 시 사이드 이펙트 최소화 + +--- + +## 🚀 다음 단계 + +이 분석을 바탕으로 다음과 같은 단계로 진행하는 것을 권장합니다: + +1. **우선순위 검토**: 위의 우선순위가 프로젝트 상황에 적합한지 검토 +2. **Phase 1 착수**: 통합 타입 파일 생성부터 시작 +3. **점진적 적용**: 한 번에 모든 것을 바꾸지 말고 단계적으로 적용 +4. **테스트 강화**: 타입 변경 시마다 충분한 테스트 수행 + +이 계획에 대한 의견이나 수정사항이 있으시면 말씀해 주세요.