저장후 시각적 효과 표시
This commit is contained in:
parent
f57a7babe6
commit
aa066a1ea9
|
|
@ -42,6 +42,7 @@
|
||||||
- **웹타입 지원**: text, number, decimal, date, datetime, select, dropdown, textarea, boolean, checkbox, radio, code, entity, file
|
- **웹타입 지원**: text, number, decimal, date, datetime, select, dropdown, textarea, boolean, checkbox, radio, code, entity, file
|
||||||
- **데이터 테이블 컴포넌트**: 완전한 실시간 설정 시스템, 컬럼 관리, 필터링, 페이징
|
- **데이터 테이블 컴포넌트**: 완전한 실시간 설정 시스템, 컬럼 관리, 필터링, 페이징
|
||||||
- **🆕 실시간 데이터 테이블**: 실제 PostgreSQL 데이터 조회, 웹타입별 검색 필터, 페이지네이션, 데이터 포맷팅
|
- **🆕 실시간 데이터 테이블**: 실제 PostgreSQL 데이터 조회, 웹타입별 검색 필터, 페이지네이션, 데이터 포맷팅
|
||||||
|
- **🆕 화면 저장 후 메뉴 할당**: 저장 완료 시 자동 메뉴 할당 모달, 기존 화면 교체 확인, 시각적 피드백 및 자동 목록 복귀
|
||||||
|
|
||||||
#### 🔧 해결된 기술적 문제들
|
#### 🔧 해결된 기술적 문제들
|
||||||
|
|
||||||
|
|
@ -410,6 +411,9 @@ const removeItem = useCallback(
|
||||||
- **회사별 메뉴 할당**: 각 회사의 메뉴에만 화면 할당
|
- **회사별 메뉴 할당**: 각 회사의 메뉴에만 화면 할당
|
||||||
- **메뉴-화면 연결**: 메뉴와 화면의 1:1 또는 1:N 연결
|
- **메뉴-화면 연결**: 메뉴와 화면의 1:1 또는 1:N 연결
|
||||||
- **권한 기반 메뉴 표시**: 사용자 권한에 따른 메뉴 표시 제어
|
- **권한 기반 메뉴 표시**: 사용자 권한에 따른 메뉴 표시 제어
|
||||||
|
- **🆕 저장 후 자동 할당**: 화면 저장 완료 시 메뉴 할당 모달 자동 팝업
|
||||||
|
- **🆕 기존 화면 교체**: 이미 할당된 화면이 있을 때 교체 확인 및 안전한 처리
|
||||||
|
- **🆕 완전한 워크플로우**: 저장 → 할당 → 목록 복귀의 자연스러운 흐름
|
||||||
|
|
||||||
## 🗄️ 데이터베이스 설계
|
## 🗄️ 데이터베이스 설계
|
||||||
|
|
||||||
|
|
@ -1172,9 +1176,264 @@ function generateValidationRules(column: ColumnInfo): ValidationRule[] {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 🎯 메뉴 할당 시스템 (신규 완성)
|
||||||
|
|
||||||
|
### 1. 화면 저장 후 메뉴 할당 워크플로우
|
||||||
|
|
||||||
|
#### 전체 프로세스
|
||||||
|
|
||||||
|
```
|
||||||
|
화면 설계 완료 → 저장 버튼 클릭 → 메뉴 할당 모달 자동 팝업
|
||||||
|
↓
|
||||||
|
메뉴 선택 및 할당 OR "나중에 할당" 클릭
|
||||||
|
↓
|
||||||
|
성공 화면 표시 (3초간 시각적 피드백)
|
||||||
|
↓
|
||||||
|
자동으로 화면 목록 페이지로 이동
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 메뉴 할당 모달 (MenuAssignmentModal)
|
||||||
|
|
||||||
|
**주요 기능:**
|
||||||
|
|
||||||
|
1. **관리자 메뉴만 표시**: 화면관리는 관리자 전용 기능이므로 관리자 메뉴(`menuType: "0"`)만 로드
|
||||||
|
2. **셀렉트박스 내부 검색**: 메뉴명, URL, 설명으로 실시간 검색 가능
|
||||||
|
3. **기존 화면 감지**: 선택한 메뉴에 이미 할당된 화면이 있는지 자동 확인
|
||||||
|
4. **화면 교체 확인**: 기존 화면이 있을 때 교체 확인 대화상자 표시
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface MenuAssignmentModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
screenInfo: ScreenDefinition | null;
|
||||||
|
onAssignmentComplete?: () => void;
|
||||||
|
onBackToList?: () => void; // 화면 목록으로 돌아가는 콜백
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 메뉴 검색 시스템
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 셀렉트박스 내부 검색 구현
|
||||||
|
<SelectContent className="max-h-64">
|
||||||
|
{/* 검색 입력 필드 */}
|
||||||
|
<div className="sticky top-0 z-10 border-b bg-white p-2">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400" />
|
||||||
|
<Input
|
||||||
|
placeholder="메뉴명, URL, 설명으로 검색..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.stopPropagation(); // 이벤트 전파 방지
|
||||||
|
setSearchTerm(e.target.value);
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => e.stopPropagation()}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="h-8 pr-8 pl-10 text-sm"
|
||||||
|
/>
|
||||||
|
{searchTerm && (
|
||||||
|
<button onClick={() => setSearchTerm("")}>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* 메뉴 옵션들 */}
|
||||||
|
<div className="max-h-48 overflow-y-auto">{getMenuOptions()}</div>
|
||||||
|
</SelectContent>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 기존 화면 감지 및 교체 시스템
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 메뉴 선택 시 기존 할당된 화면 확인
|
||||||
|
const handleMenuSelect = async (menuId: string) => {
|
||||||
|
const menu = menus.find((m) => m.objid?.toString() === menuId);
|
||||||
|
setSelectedMenu(menu || null);
|
||||||
|
|
||||||
|
if (menu) {
|
||||||
|
try {
|
||||||
|
const menuObjid = parseInt(menu.objid?.toString() || "0");
|
||||||
|
const screens = await menuScreenApi.getScreensByMenu(menuObjid);
|
||||||
|
setExistingScreens(screens);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("할당된 화면 조회 실패:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 할당 시 기존 화면 확인
|
||||||
|
const handleAssignScreen = async () => {
|
||||||
|
if (existingScreens.length > 0) {
|
||||||
|
// 이미 같은 화면이 할당되어 있는지 확인
|
||||||
|
const alreadyAssigned = existingScreens.some(
|
||||||
|
(screen) => screen.screenId === screenInfo.screenId
|
||||||
|
);
|
||||||
|
if (alreadyAssigned) {
|
||||||
|
toast.info("이미 해당 메뉴에 할당된 화면입니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 다른 화면이 할당되어 있으면 교체 확인
|
||||||
|
setShowReplaceDialog(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기존 화면이 없으면 바로 할당
|
||||||
|
await performAssignment();
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 화면 교체 확인 대화상자
|
||||||
|
|
||||||
|
**시각적 구분:**
|
||||||
|
|
||||||
|
- 🔴 **제거될 화면**: 빨간색 배경으로 표시
|
||||||
|
- 🟢 **새로 할당될 화면**: 초록색 배경으로 표시
|
||||||
|
- 🟠 **주의 메시지**: 작업이 되돌릴 수 없음을 명확히 안내
|
||||||
|
|
||||||
|
**안전한 교체 프로세스:**
|
||||||
|
|
||||||
|
1. 기존 화면들을 하나씩 제거
|
||||||
|
2. 새 화면 할당
|
||||||
|
3. 성공/실패 로그 출력
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 기존 화면 교체인 경우 기존 화면들 먼저 제거
|
||||||
|
if (replaceExisting && existingScreens.length > 0) {
|
||||||
|
for (const existingScreen of existingScreens) {
|
||||||
|
try {
|
||||||
|
await menuScreenApi.unassignScreenFromMenu(
|
||||||
|
existingScreen.screenId,
|
||||||
|
menuObjid
|
||||||
|
);
|
||||||
|
console.log(`기존 화면 "${existingScreen.screenName}" 제거 완료`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`기존 화면 "${existingScreen.screenName}" 제거 실패:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 새 화면 할당
|
||||||
|
await menuScreenApi.assignScreenToMenu(screenInfo.screenId, menuObjid);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 성공 피드백 및 자동 이동
|
||||||
|
|
||||||
|
**성공 화면 구성:**
|
||||||
|
|
||||||
|
- ✅ **체크마크 아이콘**: 성공을 나타내는 녹색 체크마크
|
||||||
|
- 🎯 **성공 메시지**: 구체적인 할당 완료 메시지
|
||||||
|
- ⏱️ **자동 이동 안내**: "3초 후 자동으로 화면 목록으로 이동합니다..."
|
||||||
|
- 🔵 **로딩 애니메이션**: 3개의 점이 순차적으로 바운스하는 애니메이션
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 성공 상태 설정
|
||||||
|
setAssignmentSuccess(true);
|
||||||
|
setAssignmentMessage(successMessage);
|
||||||
|
|
||||||
|
// 3초 후 자동으로 화면 목록으로 이동
|
||||||
|
setTimeout(() => {
|
||||||
|
if (onBackToList) {
|
||||||
|
onBackToList();
|
||||||
|
} else {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
```
|
||||||
|
|
||||||
|
**성공 화면 UI:**
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
{assignmentSuccess ? (
|
||||||
|
// 성공 화면
|
||||||
|
<>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-green-100">
|
||||||
|
<svg className="h-5 w-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
화면 할당 완료
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="rounded-lg border bg-green-50 p-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-green-100">
|
||||||
|
<Monitor className="h-5 w-5 text-green-600" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm font-medium text-green-900">{assignmentMessage}</p>
|
||||||
|
<p className="mt-1 text-xs text-green-700">
|
||||||
|
3초 후 자동으로 화면 목록으로 이동합니다...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 로딩 애니메이션 */}
|
||||||
|
<div className="flex items-center justify-center space-x-2">
|
||||||
|
<div className="h-2 w-2 animate-bounce rounded-full bg-green-500 [animation-delay:-0.3s]"></div>
|
||||||
|
<div className="h-2 w-2 animate-bounce rounded-full bg-green-500 [animation-delay:-0.15s]"></div>
|
||||||
|
<div className="h-2 w-2 animate-bounce rounded-full bg-green-500"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
// 기본 할당 화면
|
||||||
|
// ...
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 사용자 경험 개선사항
|
||||||
|
|
||||||
|
1. **선택적 할당**: 필수가 아닌 선택적 기능으로 "나중에 할당" 가능
|
||||||
|
2. **직관적 UI**: 저장된 화면 정보를 모달에서 바로 확인 가능
|
||||||
|
3. **검색 기능**: 많은 메뉴 중에서 쉽게 찾을 수 있음
|
||||||
|
4. **상태 표시**: 메뉴 활성/비활성 상태, 기존 할당된 화면 정보 표시
|
||||||
|
5. **완전한 워크플로우**: 저장 → 할당 → 목록 복귀의 자연스러운 흐름
|
||||||
|
|
||||||
## 🌐 API 설계
|
## 🌐 API 설계
|
||||||
|
|
||||||
### 1. 화면 정의 API
|
### 1. 메뉴-화면 할당 API
|
||||||
|
|
||||||
|
#### 화면을 메뉴에 할당
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
POST /screen-management/screens/:screenId/assign-menu
|
||||||
|
Request: {
|
||||||
|
menuObjid: number;
|
||||||
|
displayOrder?: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 메뉴별 할당된 화면 목록 조회
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
GET /screen-management/menus/:menuObjid/screens
|
||||||
|
Response: {
|
||||||
|
success: boolean;
|
||||||
|
data: ScreenDefinition[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 화면-메뉴 할당 해제
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
DELETE /screen-management/screens/:screenId/menus/:menuObjid
|
||||||
|
Response: {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 화면 정의 API
|
||||||
|
|
||||||
#### 화면 목록 조회 (회사별)
|
#### 화면 목록 조회 (회사별)
|
||||||
|
|
||||||
|
|
@ -2675,7 +2934,19 @@ export class TableTypeIntegrationService {
|
||||||
3. **커스터마이징**: 템플릿을 기반으로 필요한 부분 수정
|
3. **커스터마이징**: 템플릿을 기반으로 필요한 부분 수정
|
||||||
4. **저장**: 커스터마이징된 화면 저장
|
4. **저장**: 커스터마이징된 화면 저장
|
||||||
|
|
||||||
### 6. 메뉴 할당 및 관리
|
### 6. 메뉴 할당 및 관리 (신규 완성)
|
||||||
|
|
||||||
|
#### 🆕 저장 후 자동 메뉴 할당
|
||||||
|
|
||||||
|
1. **화면 저장 완료**: 화면 설계 완료 후 저장 버튼 클릭
|
||||||
|
2. **메뉴 할당 모달 자동 팝업**: 저장 성공 시 즉시 메뉴 할당 모달 표시
|
||||||
|
3. **관리자 메뉴 검색**: 메뉴명, URL, 설명으로 실시간 검색
|
||||||
|
4. **기존 화면 확인**: 선택한 메뉴에 이미 할당된 화면 자동 감지
|
||||||
|
5. **교체 확인**: 기존 화면이 있을 때 교체 여부 확인 대화상자
|
||||||
|
6. **안전한 교체**: 기존 화면 제거 후 새 화면 할당
|
||||||
|
7. **성공 피드백**: 3초간 성공 화면 표시 후 자동으로 화면 목록으로 이동
|
||||||
|
|
||||||
|
#### 기존 메뉴 할당 방식
|
||||||
|
|
||||||
1. **메뉴 선택**: 화면을 할당할 메뉴 선택 (회사별 메뉴만 표시)
|
1. **메뉴 선택**: 화면을 할당할 메뉴 선택 (회사별 메뉴만 표시)
|
||||||
2. **화면 할당**: 선택한 화면을 메뉴에 할당
|
2. **화면 할당**: 선택한 화면을 메뉴에 할당
|
||||||
|
|
@ -2778,6 +3049,7 @@ export class TableTypeIntegrationService {
|
||||||
- [x] 메뉴-화면 할당 기능 구현
|
- [x] 메뉴-화면 할당 기능 구현
|
||||||
- [x] 인터랙티브 화면 뷰어 구현
|
- [x] 인터랙티브 화면 뷰어 구현
|
||||||
- [x] 사용자 피드백 반영 완료
|
- [x] 사용자 피드백 반영 완료
|
||||||
|
- [x] 🆕 화면 저장 후 메뉴 할당 워크플로우 구현
|
||||||
|
|
||||||
**구현 완료 사항:**
|
**구현 완료 사항:**
|
||||||
|
|
||||||
|
|
@ -2788,6 +3060,7 @@ export class TableTypeIntegrationService {
|
||||||
- 메뉴 관리에서 화면 할당 기능 구현
|
- 메뉴 관리에서 화면 할당 기능 구현
|
||||||
- 할당된 화면을 실제 사용 가능한 인터랙티브 화면으로 렌더링
|
- 할당된 화면을 실제 사용 가능한 인터랙티브 화면으로 렌더링
|
||||||
- 실제 사용자 입력 및 상호작용 가능한 완전 기능 화면 구현
|
- 실제 사용자 입력 및 상호작용 가능한 완전 기능 화면 구현
|
||||||
|
- 🆕 **완전한 메뉴 할당 워크플로우**: 저장 → 메뉴 할당 모달 → 기존 화면 교체 확인 → 성공 피드백 → 목록 복귀
|
||||||
|
|
||||||
## 🎯 현재 구현된 핵심 기능
|
## 🎯 현재 구현된 핵심 기능
|
||||||
|
|
||||||
|
|
@ -3559,3 +3832,4 @@ ComponentData = ContainerComponent | WidgetComponent | GroupComponent 등으로
|
||||||
- ✅ **메뉴 연동**: 설계한 화면을 실제 메뉴에 할당하여 즉시 사용
|
- ✅ **메뉴 연동**: 설계한 화면을 실제 메뉴에 할당하여 즉시 사용
|
||||||
- ✅ **인터랙티브 화면**: 할당된 화면에서 실제 사용자 입력 및 상호작용 가능
|
- ✅ **인터랙티브 화면**: 할당된 화면에서 실제 사용자 입력 및 상호작용 가능
|
||||||
- ✅ **13가지 웹 타입 지원**: 모든 업무 요구사항에 대응 가능한 다양한 위젯
|
- ✅ **13가지 웹 타입 지원**: 모든 업무 요구사항에 대응 가능한 다양한 위젯
|
||||||
|
- ✅ **🆕 완전한 메뉴 할당 워크플로우**: 저장 → 자동 메뉴 할당 → 기존 화면 교체 확인 → 성공 피드백 → 목록 복귀의 완벽한 사용자 경험
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,29 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// "나중에 할당" 처리 - 시각적 효과 포함
|
||||||
|
const handleAssignLater = () => {
|
||||||
|
if (!screenInfo) return;
|
||||||
|
|
||||||
|
// 성공 상태 설정 (나중에 할당 메시지)
|
||||||
|
setAssignmentSuccess(true);
|
||||||
|
setAssignmentMessage(`"${screenInfo.screenName}" 화면이 저장되었습니다. 나중에 메뉴에 할당할 수 있습니다.`);
|
||||||
|
|
||||||
|
// 할당 완료 콜백 호출
|
||||||
|
if (onAssignmentComplete) {
|
||||||
|
onAssignmentComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3초 후 자동으로 화면 목록으로 이동
|
||||||
|
setTimeout(() => {
|
||||||
|
if (onBackToList) {
|
||||||
|
onBackToList();
|
||||||
|
} else {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
// 필터된 메뉴 목록
|
// 필터된 메뉴 목록
|
||||||
const filteredMenus = menus.filter((menu) => {
|
const filteredMenus = menus.filter((menu) => {
|
||||||
if (!searchTerm) return true;
|
if (!searchTerm) return true;
|
||||||
|
|
@ -268,9 +291,13 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
화면 할당 완료
|
{assignmentMessage.includes("나중에") ? "화면 저장 완료" : "화면 할당 완료"}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>화면이 성공적으로 메뉴에 할당되었습니다.</DialogDescription>
|
<DialogDescription>
|
||||||
|
{assignmentMessage.includes("나중에")
|
||||||
|
? "화면이 성공적으로 저장되었습니다. 나중에 메뉴에 할당할 수 있습니다."
|
||||||
|
: "화면이 성공적으로 메뉴에 할당되었습니다."}
|
||||||
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|
@ -427,17 +454,7 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="flex gap-2">
|
<DialogFooter className="flex gap-2">
|
||||||
<Button
|
<Button variant="outline" onClick={handleAssignLater} disabled={assigning}>
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
if (onBackToList) {
|
|
||||||
onBackToList();
|
|
||||||
} else {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={assigning}
|
|
||||||
>
|
|
||||||
<X className="mr-2 h-4 w-4" />
|
<X className="mr-2 h-4 w-4" />
|
||||||
나중에 할당
|
나중에 할당
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue