feat(WEB): Phase 6 IntegratedDetailTemplate 마이그레이션 완료

Phase 6 마이그레이션 (41개 컴포넌트 완료):
- 건설/시공: 협력업체, 시공관리, 기성관리, 발주관리, 계약관리 등
- 영업: 견적관리(V2), 고객관리(V2), 수주관리
- 회계: 청구관리, 매입관리, 매출관리, 거래처관리, 악성채권 등
- 생산: 작업지시, 검수관리
- 출고: 출하관리
- 자재: 입고관리, 재고현황
- 고객센터: 문의관리, 이벤트관리, 공지관리
- 인사: 직원관리
- 설정: 권한관리

주요 변경사항:
- 34개 xxxConfig.ts 파일 생성 (설정 기반 페이지 구성)
- PageLayout/PageHeader → IntegratedDetailTemplate 통합
- 일관된 타이틀/버튼 영역 (목록, 상세, 수정, 삭제)
- 1112줄 코드 감소 (중복 제거)

프로젝트 공통화 현황 분석 문서 추가:
- 상세 페이지 62%, 목록 페이지 82% 공통화 달성
- 추가 공통화 기회 및 로드맵 정리

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-01-20 15:51:02 +09:00
parent 6f457b28f3
commit 61e3a0ed60
71 changed files with 4743 additions and 4402 deletions

View File

@@ -1,8 +1,7 @@
'use client';
import React, { useState, useEffect } from 'react';
import { ChevronDown, ChevronRight, Shield, ArrowLeft } from 'lucide-react';
import { Button } from '@/components/ui/button';
import React, { useState, useEffect, useCallback } from 'react';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent } from '@/components/ui/card';
@@ -32,8 +31,8 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { PageHeader } from '@/components/organisms/PageHeader';
import { PageLayout } from '@/components/organisms/PageLayout';
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
import { permissionConfig } from './permissionConfig';
import type { Permission, MenuPermission, PermissionType } from './types';
interface PermissionDetailProps {
@@ -213,12 +212,6 @@ export function PermissionDetail({ permission, onBack, onSave, onDelete }: Permi
});
};
// 메뉴가 부모 메뉴인지 확인
const isParentMenu = (menuId: string): boolean => {
const menu = menuStructure.find(m => m.id === menuId);
return !!(menu?.children && menu.children.length > 0);
};
// 권한 토글 (자동 저장)
const handlePermissionToggle = (menuId: string, permType: PermissionType) => {
const newMenuPermissions = menuPermissions.map(mp =>
@@ -289,15 +282,84 @@ export function PermissionDetail({ permission, onBack, onSave, onDelete }: Permi
});
};
// 삭제
const handleDelete = () => setDeleteDialogOpen(true);
// 삭제 확인
const confirmDelete = () => {
onDelete?.(permission);
setDeleteDialogOpen(false);
onBack();
};
// IntegratedDetailTemplate용 삭제 핸들러
const handleFormDelete = useCallback(async () => {
setDeleteDialogOpen(true);
return { success: true };
}, []);
// 폼 콘텐츠 렌더링
const renderFormContent = useCallback(() => (
<div className="space-y-6">
{/* 기본 정보 */}
<Card>
<CardContent className="p-6">
<h3 className="text-lg font-semibold mb-4"> </h3>
<div className="grid grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="perm-name"></Label>
<Input
id="perm-name"
value={name}
onChange={(e) => handleNameChange(e.target.value)}
onBlur={handleNameBlur}
/>
</div>
<div className="space-y-2">
<Label htmlFor="perm-status"></Label>
<Select value={status} onValueChange={handleStatusChange}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="active"></SelectItem>
<SelectItem value="hidden"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
{/* 메뉴별 권한 설정 테이블 */}
<Card>
<CardContent className="p-6">
<h3 className="text-lg font-semibold mb-4"></h3>
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="border-b">
<TableHead className="w-64 py-4"></TableHead>
{PERMISSION_TYPES.map(pt => (
<TableHead key={pt} className="text-center w-24 py-4">
<div className="flex flex-col items-center gap-2">
<span className="text-sm font-medium">{PERMISSION_LABELS_MAP[pt]}</span>
<Checkbox
checked={menuPermissions.length > 0 && menuPermissions.every(mp => mp.permissions[pt])}
onCheckedChange={(checked) => handleColumnSelectAll(pt, !!checked)}
/>
</div>
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{renderMenuRows()}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</div>
), [name, status, menuPermissions, handleNameChange, handleNameBlur, handleStatusChange, handleColumnSelectAll, renderMenuRows]);
// 메뉴 행 렌더링 (재귀적으로 부모-자식 처리)
const renderMenuRows = () => {
const rows: React.ReactElement[] = [];
@@ -369,90 +431,18 @@ export function PermissionDetail({ permission, onBack, onSave, onDelete }: Permi
};
return (
<PageLayout>
{/* 페이지 헤더 */}
<PageHeader
title="권한 상세"
description="권한 상세 정보를 관리합니다"
icon={Shield}
actions={
<Button variant="ghost" size="sm" onClick={onBack}>
<ArrowLeft className="h-4 w-4 mr-2" />
</Button>
}
<>
<IntegratedDetailTemplate
config={permissionConfig}
mode="view"
initialData={permission}
itemId={permission.id}
onBack={onBack}
onDelete={handleFormDelete}
renderView={() => renderFormContent()}
renderForm={() => renderFormContent()}
/>
{/* 삭제/수정 버튼 (타이틀 아래, 기본정보 위) */}
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={handleDelete}>
</Button>
<Button onClick={() => onBack()}>
</Button>
</div>
{/* 기본 정보 */}
<Card>
<CardContent className="p-6">
<h3 className="text-lg font-semibold mb-4"> </h3>
<div className="grid grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="perm-name"></Label>
<Input
id="perm-name"
value={name}
onChange={(e) => handleNameChange(e.target.value)}
onBlur={handleNameBlur}
/>
</div>
<div className="space-y-2">
<Label htmlFor="perm-status"></Label>
<Select value={status} onValueChange={handleStatusChange}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="active"></SelectItem>
<SelectItem value="hidden"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
{/* 메뉴별 권한 설정 테이블 */}
<Card>
<CardContent className="p-6">
<h3 className="text-lg font-semibold mb-4"></h3>
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="border-b">
<TableHead className="w-64 py-4"></TableHead>
{PERMISSION_TYPES.map(pt => (
<TableHead key={pt} className="text-center w-24 py-4">
<div className="flex flex-col items-center gap-2">
<span className="text-sm font-medium">{PERMISSION_LABELS_MAP[pt]}</span>
<Checkbox
checked={menuPermissions.length > 0 && menuPermissions.every(mp => mp.permissions[pt])}
onCheckedChange={(checked) => handleColumnSelectAll(pt, !!checked)}
/>
</div>
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{renderMenuRows()}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
{/* 삭제 확인 다이얼로그 */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
@@ -477,6 +467,6 @@ export function PermissionDetail({ permission, onBack, onSave, onDelete }: Permi
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</PageLayout>
</>
);
}

View File

@@ -0,0 +1,30 @@
import { Shield } from 'lucide-react';
import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types';
/**
* 권한 상세 페이지 Config
*
* 참고: 이 config는 타이틀/버튼 영역만 정의
* 폼 내용은 renderView/renderForm에서 처리
*
* 특이사항:
* - 인라인 수정 (권한명, 상태)
* - 메뉴별 권한 설정 테이블
* - 자동 저장
*/
export const permissionConfig: DetailConfig = {
title: '권한 상세',
description: '권한 상세 정보를 관리합니다',
icon: Shield,
basePath: '/settings/permissions',
fields: [], // renderView/renderForm 사용으로 필드 정의 불필요
gridColumns: 2,
actions: {
showBack: true,
showDelete: true,
showEdit: true,
backLabel: '목록으로',
editLabel: '수정',
deleteLabel: '삭제',
},
};