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:
유병철
2026-02-04 20:26:27 +09:00
parent c1b63b850a
commit bb7e7a75e9
30 changed files with 737 additions and 665 deletions

View File

@@ -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={
<>
&apos;{formData.vendorName}&apos; ?
<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()}
/>
);
}