Files
vexplor/frontend/lib/api/screen.ts
kjs 658211b9d1 feat: 화면 복사 기능 개선 및 버튼 모달 설정 수정
## 주요 변경사항

### 1. 화면 복사 기능 강화
- 최고 관리자가 다른 회사로 화면 복사 가능하도록 개선
- 메인 화면과 연결된 모달 화면 자동 감지 및 일괄 복사
- 복사 시 버튼의 targetScreenId 자동 업데이트
- 일괄 이름 변경 기능 추가 (복사본 텍스트 제거)
- 중복 화면명 체크 기능 추가

#### 백엔드 (screenManagementService.ts)
- generateMultipleScreenCodes: 여러 화면 코드 일괄 생성 (Advisory Lock 사용)
- detectLinkedModalScreens: edit 액션도 모달로 감지하도록 개선
- checkDuplicateScreenName: 중복 화면명 체크 API 추가
- copyScreenWithModals: 메인+모달 일괄 복사 및 버튼 업데이트
- updateButtonTargetScreenIds: 복사된 모달로 버튼 targetScreenId 업데이트
- updated_date 컬럼 제거 (screen_layouts 테이블에 존재하지 않음)

#### 프론트엔드 (CopyScreenModal.tsx)
- 회사 선택 UI 추가 (최고 관리자 전용)
- 연결된 모달 화면 자동 감지 및 표시
- 일괄 이름 변경 기능 (텍스트 제거/추가)
- 실시간 미리보기
- 중복 화면명 체크

### 2. 버튼 설정 모달 화면 선택 개선
- 편집 중인 화면의 company_code 기준으로 화면 목록 조회
- 최고 관리자가 다른 회사 화면 편집 시 해당 회사의 모달 화면만 표시
- targetScreenId 문자열/숫자 타입 불일치 수정

#### 백엔드 (screenManagementController.ts)
- getScreens API에 companyCode 쿼리 파라미터 추가
- 최고 관리자는 다른 회사의 화면 목록 조회 가능

#### 프론트엔드
- ButtonConfigPanel: currentScreenCompanyCode props 추가
- DetailSettingsPanel: currentScreenCompanyCode 전달
- UnifiedPropertiesPanel: currentScreenCompanyCode 전달
- ScreenDesigner: selectedScreen.companyCode 전달
- targetScreenId 비교 시 parseInt 처리 (문자열→숫자)

### 3. 카테고리 메뉴별 컬럼 분리 기능
- 메뉴별로 카테고리 컬럼을 독립적으로 관리
- 카테고리 컬럼 추가/삭제 시 메뉴 스코프 적용

## 수정된 파일
- backend-node/src/services/screenManagementService.ts
- backend-node/src/controllers/screenManagementController.ts
- backend-node/src/routes/screenManagementRoutes.ts
- frontend/components/screen/CopyScreenModal.tsx
- frontend/components/screen/config-panels/ButtonConfigPanel.tsx
- frontend/components/screen/panels/DetailSettingsPanel.tsx
- frontend/components/screen/panels/UnifiedPropertiesPanel.tsx
- frontend/components/screen/ScreenDesigner.tsx
- frontend/lib/api/screen.ts
2025-11-13 12:17:10 +09:00

427 lines
14 KiB
TypeScript

import { apiClient } from "./client";
import {
ScreenDefinition,
CreateScreenRequest,
UpdateScreenRequest,
PaginatedResponse,
ScreenTemplate,
LayoutData,
} from "@/types/screen";
// 화면 정의 관련 API
export const screenApi = {
// 화면 목록 조회
getScreens: async (params: {
page?: number;
size?: number;
companyCode?: string;
searchTerm?: string;
}): Promise<PaginatedResponse<ScreenDefinition>> => {
const response = await apiClient.get("/screen-management/screens", { params });
const raw = response.data || {};
const items: any[] = (raw.data ?? raw.items ?? []) as any[];
const mapped: ScreenDefinition[] = items.map((it) => ({
...it,
// 문자열로 온 날짜를 Date 객체로 변환 (이미 Date이면 그대로 유지)
createdDate: it.createdDate ? new Date(it.createdDate) : undefined,
updatedDate: it.updatedDate ? new Date(it.updatedDate) : undefined,
}));
const page = raw.page ?? params.page ?? 1;
const size = raw.size ?? params.size ?? mapped.length;
const total = raw.total ?? mapped.length;
const totalPages = raw.totalPages ?? Math.max(1, Math.ceil(total / (size || 1)));
return { data: mapped, total, page, size, totalPages };
},
// 화면 상세 조회
getScreen: async (screenId: number): Promise<ScreenDefinition> => {
const response = await apiClient.get(`/screen-management/screens/${screenId}`);
const raw = response.data?.data || response.data;
return {
...raw,
createdDate: raw?.createdDate ? new Date(raw.createdDate) : undefined,
updatedDate: raw?.updatedDate ? new Date(raw.updatedDate) : undefined,
} as ScreenDefinition;
},
// 화면에 할당된 메뉴 조회
getScreenMenu: async (screenId: number): Promise<{ menuObjid: number; menuName?: string } | null> => {
const response = await apiClient.get(`/screen-management/screens/${screenId}/menu`);
return response.data?.data || null;
},
// 화면 생성
createScreen: async (screenData: CreateScreenRequest): Promise<ScreenDefinition> => {
const response = await apiClient.post("/screen-management/screens", screenData);
return response.data.data;
},
// 화면 코드 자동 생성 (회사코드 + 순번)
generateScreenCode: async (companyCode: string): Promise<string> => {
const response = await apiClient.get(`/screen-management/generate-screen-code/${companyCode}`);
return response.data.data.screenCode;
},
// 여러 개의 화면 코드 일괄 생성
generateMultipleScreenCodes: async (companyCode: string, count: number): Promise<string[]> => {
const response = await apiClient.post("/screen-management/generate-screen-codes", {
companyCode,
count,
});
return response.data.data.screenCodes;
},
// 화면 수정
updateScreen: async (screenId: number, screenData: UpdateScreenRequest): Promise<ScreenDefinition> => {
const response = await apiClient.put(`/screen-management/screens/${screenId}`, screenData);
return response.data.data;
},
// 화면 정보 수정 (메타데이터만)
updateScreenInfo: async (
screenId: number,
data: { screenName: string; description?: string; isActive: string },
): Promise<void> => {
await apiClient.put(`/screen-management/screens/${screenId}/info`, data);
},
// 화면 의존성 체크
checkScreenDependencies: async (
screenId: number,
): Promise<{
hasDependencies: boolean;
dependencies: Array<{
screenId: number;
screenName: string;
screenCode: string;
componentId: string;
componentType: string;
referenceType: string;
}>;
}> => {
const response = await apiClient.get(`/screen-management/screens/${screenId}/dependencies`);
return response.data;
},
// 화면 삭제 (휴지통으로 이동)
deleteScreen: async (screenId: number, deleteReason?: string, force?: boolean): Promise<void> => {
await apiClient.delete(`/screen-management/screens/${screenId}`, {
data: { deleteReason, force },
});
},
// 휴지통 화면 목록 조회
getDeletedScreens: async (params: {
page?: number;
size?: number;
}): Promise<
PaginatedResponse<ScreenDefinition & { deletedDate?: Date; deletedBy?: string; deleteReason?: string }>
> => {
const response = await apiClient.get("/screen-management/screens/trash/list", { params });
const raw = response.data || {};
const items: any[] = (raw.data ?? raw.items ?? []) as any[];
const mapped = items.map((it) => ({
...it,
createdDate: it.createdDate ? new Date(it.createdDate) : undefined,
updatedDate: it.updatedDate ? new Date(it.updatedDate) : undefined,
deletedDate: it.deletedDate ? new Date(it.deletedDate) : undefined,
}));
const page = raw.page ?? params.page ?? 1;
const size = raw.size ?? params.size ?? mapped.length;
const total = raw.total ?? mapped.length;
const totalPages = raw.totalPages ?? Math.max(1, Math.ceil(total / (size || 1)));
return { data: mapped, total, page, size, totalPages };
},
// 휴지통 화면 일괄 영구 삭제
bulkPermanentDeleteScreens: async (
screenIds: number[],
): Promise<{
deletedCount: number;
skippedCount: number;
errors: Array<{ screenId: number; error: string }>;
}> => {
const response = await apiClient.delete("/screen-management/screens/trash/bulk", {
data: { screenIds },
});
return response.data.result;
},
// 화면 복원 (휴지통에서 복원)
restoreScreen: async (screenId: number): Promise<void> => {
await apiClient.post(`/screen-management/screens/${screenId}/restore`);
},
// 화면 영구 삭제
permanentDeleteScreen: async (screenId: number): Promise<void> => {
await apiClient.delete(`/screen-management/screens/${screenId}/permanent`);
},
// 화면 레이아웃 저장
saveLayout: async (screenId: number, layoutData: LayoutData): Promise<void> => {
await apiClient.post(`/screen-management/screens/${screenId}/layout`, layoutData);
},
// 화면 레이아웃 저장 (ScreenDesigner_new.tsx용)
saveScreenLayout: async (screenId: number, layoutData: LayoutData): Promise<ApiResponse<void>> => {
const response = await apiClient.post(`/screen-management/screens/${screenId}/layout`, layoutData);
return response.data;
},
// 화면 레이아웃 조회
getLayout: async (screenId: number): Promise<LayoutData> => {
const response = await apiClient.get(`/screen-management/screens/${screenId}/layout`);
return response.data.data;
},
// 연결된 모달 화면 감지
detectLinkedModals: async (
screenId: number
): Promise<Array<{ screenId: number; screenName: string; screenCode: string }>> => {
const response = await apiClient.get(`/screen-management/screens/${screenId}/linked-modals`);
return response.data.data || [];
},
// 화면명 중복 체크
checkDuplicateScreenName: async (companyCode: string, screenName: string): Promise<boolean> => {
const response = await apiClient.post("/screen-management/screens/check-duplicate-name", {
companyCode,
screenName,
});
return response.data.data.isDuplicate || false;
},
// 화면 복사 (화면정보 + 레이아웃 모두 복사)
copyScreen: async (
sourceScreenId: number,
copyData: {
screenName: string;
screenCode: string;
description?: string;
},
): Promise<ScreenDefinition> => {
const response = await apiClient.post(`/screen-management/screens/${sourceScreenId}/copy`, copyData);
return response.data.data;
},
// 메인 화면 + 모달 화면들 일괄 복사
copyScreenWithModals: async (
sourceScreenId: number,
copyData: {
targetCompanyCode?: string; // 최고 관리자 전용: 다른 회사로 복사
mainScreen: {
screenName: string;
screenCode: string;
description?: string;
};
modalScreens: Array<{
sourceScreenId: number;
screenName: string;
screenCode: string;
}>;
}
): Promise<{
mainScreen: ScreenDefinition;
modalScreens: ScreenDefinition[];
}> => {
const response = await apiClient.post(
`/screen-management/screens/${sourceScreenId}/copy-with-modals`,
copyData
);
return response.data.data;
},
};
// 템플릿 관련 API
export const templateApi = {
// 템플릿 목록 조회
getTemplates: async (params: {
companyCode?: string;
templateType?: string;
isPublic?: boolean;
}): Promise<ScreenTemplate[]> => {
const response = await apiClient.get("/screen-management/templates", { params });
return response.data.data;
},
// 템플릿 상세 조회
getTemplate: async (templateId: number): Promise<ScreenTemplate> => {
const response = await apiClient.get(`/screen-management/templates/${templateId}`);
return response.data.data;
},
// 템플릿 생성
createTemplate: async (templateData: Partial<ScreenTemplate>): Promise<ScreenTemplate> => {
const response = await apiClient.post("/screen-management/templates", templateData);
return response.data.data;
},
// 템플릿 수정
updateTemplate: async (templateId: number, templateData: Partial<ScreenTemplate>): Promise<ScreenTemplate> => {
const response = await apiClient.put(`/screen-management/templates/${templateId}`, templateData);
return response.data.data;
},
// 템플릿 삭제
deleteTemplate: async (templateId: number): Promise<void> => {
await apiClient.delete(`/screen-management/templates/${templateId}`);
},
};
// 테이블 타입 관련 API
export const tableTypeApi = {
// 테이블 목록 조회
getTables: async (): Promise<
Array<{ tableName: string; displayName: string; description: string; columnCount: string }>
> => {
const response = await apiClient.get("/table-management/tables");
return response.data.data;
},
// 테이블 라벨 조회
getTableLabel: async (tableName: string): Promise<{ tableLabel?: string; description?: string }> => {
try {
const response = await apiClient.get(`/table-management/tables/${tableName}/labels`);
return response.data.data || {};
} catch (error) {
// 라벨이 없으면 빈 객체 반환
return {};
}
},
// 테이블 컬럼 정보 조회 (모든 컬럼)
getColumns: async (tableName: string): Promise<any[]> => {
const response = await apiClient.get(`/table-management/tables/${tableName}/columns?size=1000`);
// 새로운 API 응답 구조에 맞게 수정: { columns, total, page, size, totalPages }
const data = response.data.data || response.data;
return data.columns || data || [];
},
// 컬럼 입력 타입 정보 조회
getColumnInputTypes: async (tableName: string): Promise<any[]> => {
const response = await apiClient.get(`/table-management/tables/${tableName}/web-types`);
return response.data.data || [];
},
// 컬럼 웹 타입 설정
setColumnWebType: async (
tableName: string,
columnName: string,
webType: string,
detailSettings?: Record<string, any>,
inputType?: "direct" | "auto",
): Promise<void> => {
await apiClient.put(`/table-management/tables/${tableName}/columns/${columnName}/web-type`, {
webType,
detailSettings,
inputType,
});
},
// 테이블 데이터 조회 (페이지네이션 + 검색)
getTableData: async (
tableName: string,
params: {
page?: number;
size?: number;
search?: Record<string, any>; // 검색 조건
sortBy?: string;
sortOrder?: "asc" | "desc";
} = {},
): Promise<{
data: Record<string, any>[];
total: number;
page: number;
size: number;
totalPages: number;
}> => {
// 🔒 멀티테넌시: company_code 자동 필터링 활성화
const requestBody = {
...params,
autoFilter: {
enabled: true,
filterColumn: "company_code",
userField: "companyCode",
},
};
const response = await apiClient.post(`/table-management/tables/${tableName}/data`, requestBody);
const raw = response.data?.data || response.data;
return {
data: raw.data || [],
total: raw.total || 0,
page: raw.page || params.page || 1,
size: raw.size || params.size || 10,
totalPages: raw.totalPages || Math.ceil((raw.total || 0) / (params.size || 10)),
};
},
// 데이터 추가
addTableData: async (tableName: string, data: Record<string, any>): Promise<void> => {
await apiClient.post(`/table-management/tables/${tableName}/add`, data);
},
// 데이터 수정
editTableData: async (
tableName: string,
originalData: Record<string, any>,
updatedData: Record<string, any>,
): Promise<void> => {
await apiClient.put(`/table-management/tables/${tableName}/edit`, {
originalData,
updatedData,
});
},
// 데이터 삭제 (단일 또는 다중)
deleteTableData: async (tableName: string, data: Record<string, any>[] | { ids: string[] }): Promise<void> => {
await apiClient.delete(`/table-management/tables/${tableName}/delete`, { data });
},
// 🆕 단일 레코드 조회 (자동 입력용)
getTableRecord: async (
tableName: string,
filterColumn: string,
filterValue: any,
displayColumn: string,
): Promise<{ value: any; record: Record<string, any> }> => {
const response = await apiClient.post(`/table-management/tables/${tableName}/record`, {
filterColumn,
filterValue,
displayColumn,
});
return response.data.data;
},
};
// 메뉴-화면 할당 관련 API
export const menuScreenApi = {
// 화면을 메뉴에 할당
assignScreenToMenu: async (screenId: number, menuObjid: number, displayOrder?: number): Promise<void> => {
await apiClient.post(`/screen-management/screens/${screenId}/assign-menu`, {
menuObjid,
displayOrder,
});
},
// 메뉴별 할당된 화면 목록 조회
getScreensByMenu: async (menuObjid: number): Promise<ScreenDefinition[]> => {
const response = await apiClient.get(`/screen-management/menus/${menuObjid}/screens`);
return response.data.data.map((screen: any) => ({
...screen,
createdDate: screen.createdDate ? new Date(screen.createdDate) : new Date(),
updatedDate: screen.updatedDate ? new Date(screen.updatedDate) : new Date(),
}));
},
// 화면-메뉴 할당 해제
unassignScreenFromMenu: async (screenId: number, menuObjid: number): Promise<void> => {
await apiClient.delete(`/screen-management/screens/${screenId}/menus/${menuObjid}`);
},
};