feat(WEB): 상세 페이지 권한 체계 통합 및 레이아웃/문서 기능 개선
권한 시스템 통합: - BadDebtDetail, LaborDetail, PricingDetail 권한 로직 정리 - BoardDetail, ClientDetail, ItemDetail 권한 적용 개선 - ProcessDetail, StepDetail, PermissionDetail 권한 리팩토링 - ContractDetail, HandoverReport, ProgressBilling 권한 연동 - ReceivingDetail, ShipmentDetail, WorkOrderDetail 권한 적용 - InspectionDetail, OrderSalesDetail, QuoteFooterBar 권한 개선 기능 개선: - AuthenticatedLayout 구조 리팩토링 - JointbarInspectionDocument 문서 레이아웃 개선 - PricingTableForm 폼 기능 보강 - DynamicItemForm, SectionsTab 개선 - 주문관리 상세/생산지시 페이지 개선 - VendorLedgerDetail 수정 설정: - Claude hooks 추가 (빌드 차단, 파일 크기 체크, 미사용 import 체크) - 품질감사 문서관리 계획 문서 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,7 +27,6 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { DeleteConfirmDialog, SaveConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import { badDebtConfig } from './badDebtConfig';
|
||||
import { toast } from 'sonner';
|
||||
@@ -43,7 +42,6 @@ import {
|
||||
} from './types';
|
||||
import { createBadDebt, updateBadDebt, deleteBadDebt, addBadDebtMemo, deleteBadDebtMemo } from './actions';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
|
||||
interface BadDebtDetailProps {
|
||||
mode: 'view' | 'edit' | 'new';
|
||||
@@ -96,7 +94,6 @@ const getEmptyRecord = (): Omit<BadDebtRecord, 'id' | 'createdAt' | 'updatedAt'>
|
||||
|
||||
export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProps) {
|
||||
const router = useRouter();
|
||||
const { canUpdate, canDelete } = usePermission();
|
||||
const isViewMode = mode === 'view';
|
||||
const isNewMode = mode === 'new';
|
||||
|
||||
@@ -114,9 +111,7 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp
|
||||
},
|
||||
});
|
||||
|
||||
// 다이얼로그 상태
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [showSaveDialog, setShowSaveDialog] = useState(false);
|
||||
// 상태
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// 새 메모 입력
|
||||
@@ -132,82 +127,43 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
}, []);
|
||||
|
||||
// 네비게이션 핸들러
|
||||
const handleEdit = useCallback(() => {
|
||||
router.push(`/ko/accounting/bad-debt-collection/${recordId}?mode=edit`);
|
||||
}, [router, recordId]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
if (isNewMode) {
|
||||
router.push('/ko/accounting/bad-debt-collection');
|
||||
} else {
|
||||
router.push(`/ko/accounting/bad-debt-collection/${recordId}?mode=view`);
|
||||
}
|
||||
}, [router, recordId, isNewMode]);
|
||||
|
||||
// 저장 핸들러
|
||||
const handleSave = useCallback(() => {
|
||||
setShowSaveDialog(true);
|
||||
}, []);
|
||||
|
||||
const handleConfirmSave = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setShowSaveDialog(false);
|
||||
|
||||
// 저장/등록 핸들러 (IntegratedDetailTemplate onSubmit용)
|
||||
const handleTemplateSubmit = useCallback(async (): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
if (isNewMode) {
|
||||
const result = await createBadDebt(formData);
|
||||
if (result.success) {
|
||||
toast.success('악성채권이 등록되었습니다.');
|
||||
router.push('/ko/accounting/bad-debt-collection');
|
||||
} else {
|
||||
toast.error(result.error || '등록에 실패했습니다.');
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, error: result.error || '등록에 실패했습니다.' };
|
||||
} else {
|
||||
const result = await updateBadDebt(recordId!, formData);
|
||||
if (result.success) {
|
||||
toast.success('악성채권이 수정되었습니다.');
|
||||
router.push(`/ko/accounting/bad-debt-collection/${recordId}?mode=view`);
|
||||
} else {
|
||||
toast.error(result.error || '수정에 실패했습니다.');
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, error: result.error || '수정에 실패했습니다.' };
|
||||
}
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('저장 오류:', error);
|
||||
toast.error('서버 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}, [formData, router, recordId, isNewMode]);
|
||||
|
||||
// 삭제 핸들러
|
||||
const handleDelete = useCallback(() => {
|
||||
setShowDeleteDialog(true);
|
||||
}, []);
|
||||
|
||||
const handleConfirmDelete = useCallback(async () => {
|
||||
if (!recordId) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setShowDeleteDialog(false);
|
||||
}, [formData, recordId, isNewMode]);
|
||||
|
||||
// 삭제 핸들러 (IntegratedDetailTemplate onDelete용)
|
||||
const handleTemplateDelete = useCallback(async (id: string | number): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
const result = await deleteBadDebt(recordId);
|
||||
const result = await deleteBadDebt(String(id));
|
||||
if (result.success) {
|
||||
toast.success('악성채권이 삭제되었습니다.');
|
||||
router.push('/ko/accounting/bad-debt-collection');
|
||||
} else {
|
||||
toast.error(result.error || '삭제에 실패했습니다.');
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, error: result.error || '삭제에 실패했습니다.' };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('삭제 오류:', error);
|
||||
toast.error('서버 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}, [router, recordId]);
|
||||
}, []);
|
||||
|
||||
// 메모 추가 핸들러
|
||||
const handleAddMemo = useCallback(async () => {
|
||||
@@ -340,39 +296,16 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp
|
||||
return {
|
||||
...badDebtConfig,
|
||||
title: titleMap[mode] || badDebtConfig.title,
|
||||
actions: {
|
||||
...badDebtConfig.actions,
|
||||
deleteConfirmMessage: {
|
||||
title: '악성채권 삭제',
|
||||
description: '이 악성채권 기록을 삭제하시겠습니까? 확인 클릭 시 목록으로 이동합니다.',
|
||||
},
|
||||
},
|
||||
};
|
||||
}, [mode]);
|
||||
|
||||
// 커스텀 헤더 액션 (저장 확인 다이얼로그 패턴 유지)
|
||||
const customHeaderActions = useMemo(() => {
|
||||
if (isViewMode) {
|
||||
return (
|
||||
<>
|
||||
{canDelete && (
|
||||
<Button variant="outline" className="text-red-500 border-red-200 hover:bg-red-50" onClick={handleDelete} disabled={isLoading}>
|
||||
{isLoading ? '처리중...' : '삭제'}
|
||||
</Button>
|
||||
)}
|
||||
{canUpdate && (
|
||||
<Button onClick={handleEdit} className="bg-blue-500 hover:bg-blue-600" disabled={isLoading}>
|
||||
수정
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Button variant="outline" onClick={handleCancel} disabled={isLoading}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSave} className="bg-blue-500 hover:bg-blue-600" disabled={isLoading}>
|
||||
{isLoading ? '처리중...' : (isNewMode ? '등록' : '저장')}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}, [isViewMode, isNewMode, isLoading, handleDelete, handleEdit, handleCancel, handleSave, mode, canUpdate, canDelete]);
|
||||
|
||||
// 입력 필드 렌더링 헬퍼
|
||||
const renderField = (
|
||||
label: string,
|
||||
@@ -1019,40 +952,16 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IntegratedDetailTemplate
|
||||
config={dynamicConfig}
|
||||
mode={isNewMode ? 'create' : (isViewMode ? 'view' : 'edit')}
|
||||
initialData={formData as unknown as Record<string, unknown>}
|
||||
itemId={recordId}
|
||||
isLoading={isLoading}
|
||||
headerActions={customHeaderActions}
|
||||
renderView={() => renderFormContent()}
|
||||
renderForm={() => renderFormContent()}
|
||||
/>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<DeleteConfirmDialog
|
||||
open={showDeleteDialog}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
onConfirm={handleConfirmDelete}
|
||||
title="악성채권 삭제"
|
||||
description={
|
||||
<>
|
||||
'{formData.vendorName}'의 악성채권 기록을 삭제하시겠습니까?
|
||||
<br />
|
||||
확인 클릭 시 목록으로 이동합니다.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 저장 확인 다이얼로그 */}
|
||||
<SaveConfirmDialog
|
||||
open={showSaveDialog}
|
||||
onOpenChange={setShowSaveDialog}
|
||||
onConfirm={handleConfirmSave}
|
||||
description="입력한 내용을 저장하시겠습니까?"
|
||||
/>
|
||||
</>
|
||||
<IntegratedDetailTemplate
|
||||
config={dynamicConfig}
|
||||
mode={isNewMode ? 'create' : (isViewMode ? 'view' : 'edit')}
|
||||
initialData={formData as unknown as Record<string, unknown>}
|
||||
itemId={recordId}
|
||||
isLoading={isLoading}
|
||||
onSubmit={handleTemplateSubmit}
|
||||
onDelete={handleTemplateDelete}
|
||||
renderView={() => renderFormContent()}
|
||||
renderForm={() => renderFormContent()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user