feat: 인증 미들웨어 적용 및 화면 그룹 삭제 로직 개선

- 모든 라우트에 인증 미들웨어를 적용하여 보안을 강화하였습니다.
- 화면 그룹 삭제 시 회사 코드 확인 및 권한 체크 로직을 추가하여, 다른 회사의 그룹 삭제를 방지하였습니다.
- 채번 규칙, 카테고리 값, 테이블 타입 컬럼 복제 시 같은 회사로 복제하는 경우 경고 메시지를 추가하였습니다.
- 메뉴 URL 업데이트 기능을 추가하여 복제된 화면 ID에 맞게 URL을 재매핑하도록 하였습니다.
This commit is contained in:
DDD1542
2026-02-02 09:22:34 +09:00
parent 4daa77f9a1
commit 51492a8911
9 changed files with 784 additions and 508 deletions

View File

@@ -597,7 +597,7 @@ export default function CopyScreenModal({
screen_id: result.mainScreen.screenId,
screen_role: "MAIN",
display_order: 1,
target_company_code: finalCompanyCode, // 대상 회사 코드 전달
target_company_code: targetCompanyCode || sourceScreen.companyCode, // 대상 회사 코드 전달
});
console.log(`✅ 복제된 화면을 그룹(${selectedTargetGroupId})에 추가 완료`);
} catch (groupError) {
@@ -606,8 +606,68 @@ export default function CopyScreenModal({
}
}
// 추가 복사 옵션 처리 (단일 화면 복제용)
const sourceCompanyCode = sourceScreen.companyCode;
const copyTargetCompanyCode = targetCompanyCode || sourceCompanyCode;
let additionalCopyMessages: string[] = [];
// 채번규칙 복제
if (copyNumberingRules && sourceCompanyCode !== copyTargetCompanyCode) {
try {
console.log("📋 단일 화면: 채번규칙 복제 시작...");
const numberingResult = await apiClient.post("/api/screen-management/copy-numbering-rules", {
sourceCompanyCode,
targetCompanyCode: copyTargetCompanyCode
});
if (numberingResult.data.success) {
additionalCopyMessages.push(`채번규칙 ${numberingResult.data.copiedCount || 0}`);
console.log("✅ 채번규칙 복제 완료:", numberingResult.data);
}
} catch (err: any) {
console.error("채번규칙 복제 실패:", err);
}
}
// 카테고리 값 복제
if (copyCategoryValues && sourceCompanyCode !== copyTargetCompanyCode) {
try {
console.log("📋 단일 화면: 카테고리 값 복제 시작...");
const categoryResult = await apiClient.post("/api/screen-management/copy-category-mapping", {
sourceCompanyCode,
targetCompanyCode: copyTargetCompanyCode
});
if (categoryResult.data.success) {
additionalCopyMessages.push(`카테고리 값 ${categoryResult.data.copiedValues || 0}`);
console.log("✅ 카테고리 값 복제 완료:", categoryResult.data);
}
} catch (err: any) {
console.error("카테고리 값 복제 실패:", err);
}
}
// 테이블 타입 컬럼 복제
if (copyTableTypeColumns && sourceCompanyCode !== copyTargetCompanyCode) {
try {
console.log("📋 단일 화면: 테이블 타입 컬럼 복제 시작...");
const tableTypeResult = await apiClient.post("/api/screen-management/copy-table-type-columns", {
sourceCompanyCode,
targetCompanyCode: copyTargetCompanyCode
});
if (tableTypeResult.data.success) {
additionalCopyMessages.push(`테이블 타입 컬럼 ${tableTypeResult.data.copiedCount || 0}`);
console.log("✅ 테이블 타입 컬럼 복제 완료:", tableTypeResult.data);
}
} catch (err: any) {
console.error("테이블 타입 컬럼 복제 실패:", err);
}
}
const additionalInfo = additionalCopyMessages.length > 0
? ` + 추가: ${additionalCopyMessages.join(", ")}`
: "";
toast.success(
`화면 복사가 완료되었습니다! (메인 1개 + 모달 ${result.modalScreens.length}개)`
`화면 복사가 완료되었습니다! (메인 1개 + 모달 ${result.modalScreens.length}${additionalInfo})`
);
// 새로고침 완료 후 모달 닫기
@@ -1678,6 +1738,50 @@ export default function CopyScreenModal({
</div>
)}
{/* 추가 복사 옵션 (단일 화면 복제용) */}
<div className="space-y-2">
<Label className="text-xs sm:text-sm font-medium"> ():</Label>
{/* 채번규칙 복제 */}
<div className="flex items-center space-x-2 p-2 bg-muted/30 rounded-md ml-2">
<Checkbox
id="copyNumberingRulesScreen"
checked={copyNumberingRules}
onCheckedChange={(checked) => setCopyNumberingRules(checked === true)}
/>
<Label htmlFor="copyNumberingRulesScreen" className="text-xs sm:text-sm cursor-pointer flex items-center gap-2">
<Hash className="h-4 w-4 text-muted-foreground" />
</Label>
</div>
{/* 카테고리 값 복사 */}
<div className="flex items-center space-x-2 p-2 bg-muted/30 rounded-md ml-2">
<Checkbox
id="copyCategoryValuesScreen"
checked={copyCategoryValues}
onCheckedChange={(checked) => setCopyCategoryValues(checked === true)}
/>
<Label htmlFor="copyCategoryValuesScreen" className="text-xs sm:text-sm cursor-pointer flex items-center gap-2">
<Table className="h-4 w-4 text-muted-foreground" />
</Label>
</div>
{/* 테이블 타입관리 입력타입 설정 복사 */}
<div className="flex items-center space-x-2 p-2 bg-muted/30 rounded-md ml-2">
<Checkbox
id="copyTableTypeColumnsScreen"
checked={copyTableTypeColumns}
onCheckedChange={(checked) => setCopyTableTypeColumns(checked === true)}
/>
<Label htmlFor="copyTableTypeColumnsScreen" className="text-xs sm:text-sm cursor-pointer flex items-center gap-2">
<Settings className="h-4 w-4 text-muted-foreground" />
</Label>
</div>
</div>
{/* 화면명 일괄 수정 (접히는 옵션) */}
<details className="text-sm">
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">

View File

@@ -175,7 +175,7 @@ export function ScreenGroupTreeView({
const [syncProgress, setSyncProgress] = useState<{ message: string; detail?: string } | null>(null);
// 회사 선택 (최고 관리자용)
const { user, switchCompany } = useAuth();
const { user } = useAuth();
const [companies, setCompanies] = useState<Company[]>([]);
const [selectedCompanyCode, setSelectedCompanyCode] = useState<string>("");
const [isSyncCompanySelectOpen, setIsSyncCompanySelectOpen] = useState(false);
@@ -301,23 +301,18 @@ export function ScreenGroupTreeView({
}
};
// 회사 선택 시 회사 전환 + 상태 조회
// 회사 선택 시 상태만 변경 (페이지 새로고침 없이)
const handleCompanySelect = async (companyCode: string) => {
setSelectedCompanyCode(companyCode);
setIsSyncCompanySelectOpen(false);
setSyncStatus(null);
if (companyCode) {
// 🔧 회사 전환 (JWT 토큰 변경) - 모든 API가 선택한 회사로 동작하도록
const switchResult = await switchCompany(companyCode);
if (!switchResult.success) {
toast.error(switchResult.message || "회사 전환 실패");
return;
// 동기화 상태 조회 (선택한 회사 코드로)
const response = await getMenuScreenSyncStatus(companyCode);
if (response.success && response.data) {
setSyncStatus(response.data);
}
toast.success(`${companyCode} 회사로 전환되었습니다. 페이지를 새로고침합니다.`);
// 🔧 페이지 새로고침으로 새 JWT 확실하게 적용
window.location.reload();
}
};
@@ -447,17 +442,24 @@ export function ScreenGroupTreeView({
};
// 그룹과 모든 하위 그룹의 화면을 재귀적으로 수집
const getAllScreensInGroupRecursively = (groupId: number): ScreenDefinition[] => {
// 같은 회사의 그룹만 필터링하여 다른 회사 화면이 잘못 수집되는 것을 방지
const getAllScreensInGroupRecursively = (groupId: number, targetCompanyCode?: string): ScreenDefinition[] => {
const result: ScreenDefinition[] = [];
// 부모 그룹의 company_code 확인
const parentGroup = groups.find(g => g.id === groupId);
const companyCode = targetCompanyCode || parentGroup?.company_code;
// 현재 그룹의 화면들
const currentGroupScreens = getScreensInGroup(groupId);
result.push(...currentGroupScreens);
// 하위 그룹들 찾기
const childGroups = groups.filter((g) => (g as any).parent_group_id === groupId);
// 같은 회사 + 같은 부모를 가진 하위 그룹들 찾기
const childGroups = groups.filter((g) =>
(g as any).parent_group_id === groupId &&
(!companyCode || g.company_code === companyCode)
);
for (const childGroup of childGroups) {
const childScreens = getAllScreensInGroupRecursively(childGroup.id);
const childScreens = getAllScreensInGroupRecursively(childGroup.id, companyCode);
result.push(...childScreens);
}
@@ -465,13 +467,22 @@ export function ScreenGroupTreeView({
};
// 모든 하위 그룹 ID를 재귀적으로 수집 (삭제 순서: 자식 → 부모)
const getAllChildGroupIds = (groupId: number): number[] => {
// 같은 회사의 그룹만 필터링하여 다른 회사 그룹이 잘못 삭제되는 것을 방지
const getAllChildGroupIds = (groupId: number, targetCompanyCode?: string): number[] => {
const result: number[] = [];
const childGroups = groups.filter((g) => (g as any).parent_group_id === groupId);
// 부모 그룹의 company_code 확인
const parentGroup = groups.find(g => g.id === groupId);
const companyCode = targetCompanyCode || parentGroup?.company_code;
// 같은 회사 + 같은 부모를 가진 그룹만 필터링
const childGroups = groups.filter((g) =>
(g as any).parent_group_id === groupId &&
(!companyCode || g.company_code === companyCode)
);
for (const childGroup of childGroups) {
// 자식의 자식들을 먼저 수집 (깊은 곳부터)
const grandChildIds = getAllChildGroupIds(childGroup.id);
const grandChildIds = getAllChildGroupIds(childGroup.id, companyCode);
result.push(...grandChildIds);
result.push(childGroup.id);
}
@@ -483,10 +494,35 @@ export function ScreenGroupTreeView({
const confirmDeleteGroup = async () => {
if (!deletingGroup) return;
// 🔍 디버깅: 삭제 대상 그룹 정보
console.log("========== 그룹 삭제 디버깅 ==========");
console.log("삭제 대상 그룹:", {
id: deletingGroup.id,
name: deletingGroup.group_name,
company_code: deletingGroup.company_code,
parent_group_id: (deletingGroup as any).parent_group_id
});
// 🔍 디버깅: 전체 groups 배열에서 같은 회사 그룹 출력
const sameCompanyGroups = groups.filter(g => g.company_code === deletingGroup.company_code);
console.log("같은 회사 그룹들:", sameCompanyGroups.map(g => ({
id: g.id,
name: g.group_name,
parent_group_id: (g as any).parent_group_id
})));
// 삭제 전 통계 수집 (화면 수는 삭제 전에 계산)
const totalScreensToDelete = getAllScreensInGroupRecursively(deletingGroup.id).length;
const childGroupIds = getAllChildGroupIds(deletingGroup.id);
// 🔍 디버깅: 수집된 하위 그룹 ID들
console.log("수집된 하위 그룹 ID들:", childGroupIds);
console.log("하위 그룹 상세:", childGroupIds.map(id => {
const g = groups.find(grp => grp.id === id);
return g ? { id: g.id, name: g.group_name, parent_group_id: (g as any).parent_group_id } : { id, name: "NOT_FOUND" };
}));
console.log("==========================================");
// 총 작업 수 계산 (화면 + 하위 그룹 + 현재 그룹)
const totalSteps = totalScreensToDelete + childGroupIds.length + 1;
let currentStep = 0;
@@ -511,7 +547,7 @@ export function ScreenGroupTreeView({
total: totalSteps,
message: `화면 삭제 중: ${screen.screenName}`
});
await screenApi.deleteScreen(screen.screenId, "그룹 삭제와 함께 삭제");
await screenApi.deleteScreen(screen.screenId, "그룹 삭제와 함께 삭제", true); // force: true로 의존성 무시
}
console.log(`✅ 그룹 및 하위 그룹 내 화면 ${allScreens.length}개 삭제 완료`);
}

View File

@@ -41,6 +41,7 @@ export interface CreateCategoryValueInput {
icon?: string;
isActive?: boolean;
isDefault?: boolean;
targetCompanyCode?: string; // 저장할 회사 코드 (최고 관리자가 회사 선택 시)
}
// 카테고리 값 수정 입력