메뉴생성시 화면할당기능 구현

This commit is contained in:
kjs
2025-09-19 15:22:25 +09:00
parent d1e1c7964b
commit 84d4d49bd5
3 changed files with 962 additions and 10 deletions

View File

@@ -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<MenuFormModalProps> = ({
langKey: "",
});
// 화면 할당 관련 상태
const [urlType, setUrlType] = useState<"direct" | "screen">("screen"); // URL 직접 입력 or 화면 할당 (기본값: 화면 할당)
const [selectedScreen, setSelectedScreen] = useState<ScreenDefinition | null>(null);
const [screens, setScreens] = useState<ScreenDefinition[]>([]);
const [screenSearchText, setScreenSearchText] = useState("");
const [isScreenDropdownOpen, setIsScreenDropdownOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [isEdit, setIsEdit] = useState(false);
const [companies, setCompanies] = useState<Company[]>([]);
@@ -77,6 +88,132 @@ export const MenuFormModal: React.FC<MenuFormModalProps> = ({
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<MenuFormModalProps> = ({
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<MenuFormModalProps> = ({
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<MenuFormModalProps> = ({
}
}, [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<MenuFormModalProps> = ({
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<MenuFormModalProps> = ({
<div className="space-y-2">
<Label htmlFor="menuUrl">{getText(MENU_MANAGEMENT_KEYS.FORM_MENU_URL)}</Label>
<Input
id="menuUrl"
value={formData.menuUrl}
onChange={(e) => handleInputChange("menuUrl", e.target.value)}
placeholder={getText(MENU_MANAGEMENT_KEYS.FORM_MENU_URL_PLACEHOLDER)}
/>
{/* URL 타입 선택 */}
<RadioGroup value={urlType} onValueChange={handleUrlTypeChange} className="mb-3 flex space-x-6">
<div className="flex items-center space-x-2">
<RadioGroupItem value="screen" id="screen" />
<Label htmlFor="screen" className="cursor-pointer">
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="direct" id="direct" />
<Label htmlFor="direct" className="cursor-pointer">
URL
</Label>
</div>
</RadioGroup>
{/* 화면 할당 */}
{urlType === "screen" && (
<div className="space-y-2">
{/* 화면 선택 드롭다운 */}
<div className="relative">
<Button
type="button"
variant="outline"
onClick={() => setIsScreenDropdownOpen(!isScreenDropdownOpen)}
className="w-full justify-between"
>
<span className="text-left">
{selectedScreen ? selectedScreen.screenName : "화면을 선택하세요"}
</span>
<ChevronDown className="h-4 w-4" />
</Button>
{isScreenDropdownOpen && (
<div className="screen-dropdown absolute top-full right-0 left-0 z-50 mt-1 max-h-60 overflow-y-auto rounded-md border bg-white shadow-lg">
{/* 검색 입력 */}
<div className="sticky top-0 border-b bg-white p-2">
<div className="relative">
<Search className="absolute top-1/2 left-2 h-4 w-4 -translate-y-1/2 text-gray-400" />
<Input
placeholder="화면 검색..."
value={screenSearchText}
onChange={(e) => setScreenSearchText(e.target.value)}
className="pl-8"
/>
</div>
</div>
{/* 화면 목록 */}
<div className="max-h-48 overflow-y-auto">
{screens
.filter(
(screen) =>
screen.screenName.toLowerCase().includes(screenSearchText.toLowerCase()) ||
screen.screenCode.toLowerCase().includes(screenSearchText.toLowerCase()),
)
.map((screen, index) => (
<div
key={`screen-${screen.screenId || screen.id || index}-${screen.screenCode || index}`}
onClick={() => handleScreenSelect(screen)}
className="cursor-pointer border-b px-3 py-2 last:border-b-0 hover:bg-gray-100"
>
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-medium">{screen.screenName}</div>
<div className="text-xs text-gray-500">{screen.screenCode}</div>
</div>
<div className="text-xs text-gray-400">ID: {screen.screenId || screen.id || "N/A"}</div>
</div>
</div>
))}
{screens.filter(
(screen) =>
screen.screenName.toLowerCase().includes(screenSearchText.toLowerCase()) ||
screen.screenCode.toLowerCase().includes(screenSearchText.toLowerCase()),
).length === 0 && <div className="px-3 py-2 text-sm text-gray-500"> .</div>}
</div>
</div>
)}
</div>
{/* 선택된 화면 정보 표시 */}
{selectedScreen && (
<div className="rounded-md border bg-blue-50 p-3">
<div className="text-sm font-medium text-blue-900">{selectedScreen.screenName}</div>
<div className="text-xs text-blue-600">: {selectedScreen.screenCode}</div>
<div className="text-xs text-blue-600"> URL: {formData.menuUrl}</div>
</div>
)}
</div>
)}
{/* URL 직접 입력 */}
{urlType === "direct" && (
<Input
id="menuUrl"
value={formData.menuUrl}
onChange={(e) => handleInputChange("menuUrl", e.target.value)}
placeholder={getText(MENU_MANAGEMENT_KEYS.FORM_MENU_URL_PLACEHOLDER)}
/>
)}
</div>
<div className="space-y-2">

View File

@@ -406,7 +406,32 @@ const SelectBasicComponent: React.FC<SelectBasicComponentProps> = ({
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);

View File

@@ -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<string, any>; // default_config Json과 타입 불일치
validationRules?: Record<string, any>; // 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<string, any>; // any 타입 사용
config?: Record<string, any>; // any 타입 사용
onEvent?: (event: string, data: any) => void; // any 타입
}
// 실제 사용 시
<DynamicWebTypeRenderer
webType={component.webType || "text"} // WebType | undefined 전달
config={component.webTypeConfig} // WebTypeConfig 타입 전달
props={{
component: component, // ComponentData 타입
value: formData[component.columnName || component.id] || "",
onChange: (value: 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<string, any>; // Json 타입 매핑
validationRules?: Record<string, any>; // 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<string, any>; // 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<string, unknown>;
```
## 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<string, unknown>;
selectedRows?: unknown[];
selectedRowsData?: Record<string, unknown>[];
controlDataSource: ControlDataSource;
buttonId: string;
componentData?: ComponentData;
timestamp: string;
clickCount?: number;
}
export interface ActionResult {
success: boolean;
message: string;
data?: Record<string, unknown>;
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. **테스트 강화**: 타입 변경 시마다 충분한 테스트 수행
이 계획에 대한 의견이나 수정사항이 있으시면 말씀해 주세요.