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:
parent
14802f507f
commit
8b3593c8fb
|
|
@ -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 성공");
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
⚠️ 실제 데이터는 복사되지 않습니다.
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue