diff --git a/backend-node/src/controllers/adminController.ts b/backend-node/src/controllers/adminController.ts index bfc1f3b1..746bf931 100644 --- a/backend-node/src/controllers/adminController.ts +++ b/backend-node/src/controllers/adminController.ts @@ -3308,12 +3308,21 @@ export async function copyMenu( return; } + // 화면명 변환 설정 (선택사항) + const screenNameConfig = req.body.screenNameConfig + ? { + removeText: req.body.screenNameConfig.removeText, + addPrefix: req.body.screenNameConfig.addPrefix, + } + : undefined; + // 메뉴 복사 실행 const menuCopyService = new MenuCopyService(); const result = await menuCopyService.copyMenu( parseInt(menuObjid, 10), targetCompanyCode, - userId + userId, + screenNameConfig ); logger.info("✅ 메뉴 복사 API 성공"); diff --git a/backend-node/src/services/menuCopyService.ts b/backend-node/src/services/menuCopyService.ts index 5551fa32..7187dd2e 100644 --- a/backend-node/src/services/menuCopyService.ts +++ b/backend-node/src/services/menuCopyService.ts @@ -726,7 +726,11 @@ export class MenuCopyService { async copyMenu( sourceMenuObjid: number, targetCompanyCode: string, - userId: string + userId: string, + screenNameConfig?: { + removeText?: string; + addPrefix?: string; + } ): Promise { logger.info(` 🚀 ============================================ @@ -807,7 +811,8 @@ export class MenuCopyService { targetCompanyCode, flowIdMap, userId, - client + client, + screenNameConfig ); // === 4단계: 메뉴 복사 === @@ -1048,7 +1053,11 @@ export class MenuCopyService { targetCompanyCode: string, flowIdMap: Map, userId: string, - client: PoolClient + client: PoolClient, + screenNameConfig?: { + removeText?: string; + addPrefix?: string; + } ): Promise> { const screenIdMap = new Map(); @@ -1087,6 +1096,25 @@ export class MenuCopyService { client ); + // 2-1) 화면명 변환 적용 + let transformedScreenName = screenDef.screen_name; + if (screenNameConfig) { + // 1. 제거할 텍스트 제거 + if (screenNameConfig.removeText?.trim()) { + transformedScreenName = transformedScreenName.replace( + new RegExp(screenNameConfig.removeText.trim(), "g"), + "" + ); + transformedScreenName = transformedScreenName.trim(); // 앞뒤 공백 제거 + } + + // 2. 접두사 추가 + if (screenNameConfig.addPrefix?.trim()) { + transformedScreenName = + screenNameConfig.addPrefix.trim() + " " + transformedScreenName; + } + } + // 3) screen_definitions 복사 (deleted 필드는 NULL로 설정, 삭제된 화면도 활성화) const newScreenResult = await client.query<{ screen_id: number }>( `INSERT INTO screen_definitions ( @@ -1097,7 +1125,7 @@ export class MenuCopyService { ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING screen_id`, [ - screenDef.screen_name, + transformedScreenName, // 변환된 화면명 newScreenCode, // 새 화면 코드 screenDef.table_name, targetCompanyCode, // 새 회사 코드 @@ -1634,7 +1662,12 @@ export class MenuCopyService { const existsResult = await client.query( `SELECT value_id FROM table_column_category_values WHERE table_name = $1 AND column_name = $2 AND value_code = $3 AND company_code = $4`, - [value.table_name, value.column_name, value.value_code, targetCompanyCode] + [ + value.table_name, + value.column_name, + value.value_code, + targetCompanyCode, + ] ); if (existsResult.rows.length > 0) { diff --git a/frontend/components/admin/MenuCopyDialog.tsx b/frontend/components/admin/MenuCopyDialog.tsx index 138e835b..46de8f4b 100644 --- a/frontend/components/admin/MenuCopyDialog.tsx +++ b/frontend/components/admin/MenuCopyDialog.tsx @@ -13,6 +13,8 @@ import { } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, @@ -49,6 +51,11 @@ export function MenuCopyDialog({ const [result, setResult] = useState(null); const [loadingCompanies, setLoadingCompanies] = useState(false); + // 화면명 일괄 변경 설정 + const [useBulkRename, setUseBulkRename] = useState(false); + const [removeText, setRemoveText] = useState(""); + const [addPrefix, setAddPrefix] = useState(""); + // 회사 목록 로드 useEffect(() => { if (open) { @@ -56,6 +63,9 @@ export function MenuCopyDialog({ // 다이얼로그가 열릴 때마다 초기화 setTargetCompanyCode(""); setResult(null); + setUseBulkRename(false); + setRemoveText(""); + setAddPrefix(""); } }, [open]); @@ -93,7 +103,20 @@ export function MenuCopyDialog({ setResult(null); try { - const response = await menuApi.copyMenu(menuObjid, targetCompanyCode); + // 화면명 변환 설정 (사용 중일 때만 전달) + const screenNameConfig = + useBulkRename && (removeText.trim() || addPrefix.trim()) + ? { + removeText: removeText.trim() || undefined, + addPrefix: addPrefix.trim() || undefined, + } + : undefined; + + const response = await menuApi.copyMenu( + menuObjid, + targetCompanyCode, + screenNameConfig + ); if (response.success && response.data) { setResult(response.data); @@ -183,6 +206,64 @@ export function MenuCopyDialog({ )} + {/* 화면명 일괄 변경 설정 */} + {!result && ( +
+
+ setUseBulkRename(checked as boolean)} + disabled={copying} + /> + +
+ + {useBulkRename && ( +
+
+ + setRemoveText(e.target.value)} + placeholder="예: 탑씰" + disabled={copying} + className="h-8 text-xs sm:h-10 sm:text-sm" + /> +

+ 화면명에서 이 텍스트를 제거합니다 (예: "탑씰 회사정보" → "회사정보") +

+
+ +
+ + setAddPrefix(e.target.value)} + placeholder="예: 한신" + disabled={copying} + className="h-8 text-xs sm:h-10 sm:text-sm" + /> +

+ 화면명 앞에 이 텍스트를 추가합니다 (예: "회사정보" → "한신 회사정보") +

+
+
+ )} +
+ )} + {/* 복사 항목 안내 */} {!result && (
@@ -192,6 +273,7 @@ export function MenuCopyDialog({
  • 화면 + 레이아웃 (모달, 조건부 컨테이너)
  • 플로우 제어 (스텝, 연결)
  • 코드 카테고리 + 코드
  • +
  • 카테고리 설정 + 채번 규칙
  • ⚠️ 실제 데이터는 복사되지 않습니다. diff --git a/frontend/lib/api/menu.ts b/frontend/lib/api/menu.ts index d8964257..a39fc7c6 100644 --- a/frontend/lib/api/menu.ts +++ b/frontend/lib/api/menu.ts @@ -166,12 +166,19 @@ export const menuApi = { // 메뉴 복사 copyMenu: async ( menuObjid: number, - targetCompanyCode: string + targetCompanyCode: string, + screenNameConfig?: { + removeText?: string; + addPrefix?: string; + } ): Promise> => { try { const response = await apiClient.post( `/admin/menus/${menuObjid}/copy`, - { targetCompanyCode } + { + targetCompanyCode, + screenNameConfig + } ); return response.data; } catch (error: any) {