feat: 메뉴 복사 시 화면명 일괄 변환 기능 추가

새로운 기능:
- 화면명에서 특정 텍스트 제거 (예: '탑씰' 제거)
- 화면명에 접두사 추가 (예: '한신' 추가)
- 변환 로직: 제거 → 접두사 추가 순서로 적용

백엔드:
- menuCopyService.copyMenu()에 screenNameConfig 파라미터 추가
- copyScreens()에서 화면명 변환 로직 적용
- 정규식으로 전역 치환 (new RegExp(text, 'g'))

프론트엔드:
- MenuCopyDialog에 화면명 일괄 변경 UI 추가
- Checkbox로 기능 활성화/비활성화
- 2개 Input: removeText, addPrefix
- API 호출 시 screenNameConfig 전달

사용 예시:
1. '탑씰 회사정보' → '회사정보' (제거만)
2. '회사정보' → '한신 회사정보' (접두사만)
3. '탑씰 회사정보' → '한신 회사정보' (제거 + 접두사)

관련 파일:
- backend-node/src/services/menuCopyService.ts
- backend-node/src/controllers/adminController.ts
- frontend/lib/api/menu.ts
- frontend/components/admin/MenuCopyDialog.tsx
This commit is contained in:
kjs 2025-11-21 15:38:59 +09:00
parent 14802f507f
commit 8b3593c8fb
4 changed files with 140 additions and 9 deletions

View File

@ -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 성공");

View File

@ -726,7 +726,11 @@ export class MenuCopyService {
async copyMenu(
sourceMenuObjid: number,
targetCompanyCode: string,
userId: string
userId: string,
screenNameConfig?: {
removeText?: string;
addPrefix?: string;
}
): Promise<MenuCopyResult> {
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<number, number>,
userId: string,
client: PoolClient
client: PoolClient,
screenNameConfig?: {
removeText?: string;
addPrefix?: string;
}
): Promise<Map<number, number>> {
const screenIdMap = new Map<number, number>();
@ -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) {

View File

@ -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<MenuCopyResult | null>(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({
</div>
)}
{/* 화면명 일괄 변경 설정 */}
{!result && (
<div className="space-y-3">
<div className="flex items-center gap-2">
<Checkbox
id="useBulkRename"
checked={useBulkRename}
onCheckedChange={(checked) => setUseBulkRename(checked as boolean)}
disabled={copying}
/>
<Label
htmlFor="useBulkRename"
className="text-xs sm:text-sm font-medium cursor-pointer"
>
</Label>
</div>
{useBulkRename && (
<div className="space-y-3 pl-6 border-l-2">
<div>
<Label htmlFor="removeText" className="text-xs sm:text-sm">
</Label>
<Input
id="removeText"
value={removeText}
onChange={(e) => setRemoveText(e.target.value)}
placeholder="예: 탑씰"
disabled={copying}
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
(: "탑씰 회사정보" "회사정보")
</p>
</div>
<div>
<Label htmlFor="addPrefix" className="text-xs sm:text-sm">
</Label>
<Input
id="addPrefix"
value={addPrefix}
onChange={(e) => setAddPrefix(e.target.value)}
placeholder="예: 한신"
disabled={copying}
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
<p className="text-muted-foreground mt-1 text-[10px] sm:text-xs">
(: "회사정보" "한신 회사정보")
</p>
</div>
</div>
)}
</div>
)}
{/* 복사 항목 안내 */}
{!result && (
<div className="rounded-md border p-3 text-xs">
@ -192,6 +273,7 @@ export function MenuCopyDialog({
<li> + (, )</li>
<li> (, )</li>
<li> + </li>
<li> + </li>
</ul>
<p className="mt-2 text-warning">
.

View File

@ -166,12 +166,19 @@ export const menuApi = {
// 메뉴 복사
copyMenu: async (
menuObjid: number,
targetCompanyCode: string
targetCompanyCode: string,
screenNameConfig?: {
removeText?: string;
addPrefix?: string;
}
): Promise<ApiResponse<MenuCopyResult>> => {
try {
const response = await apiClient.post(
`/admin/menus/${menuObjid}/copy`,
{ targetCompanyCode }
{
targetCompanyCode,
screenNameConfig
}
);
return response.data;
} catch (error: any) {