Merge branch 'sam-kkk' into develop
This commit is contained in:
@@ -17,11 +17,9 @@
|
||||
|
||||
import { useState, useRef, useEffect, useCallback, useTransition, useMemo } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { ClientDetailClientV2 } from '@/components/clients/ClientDetailClientV2';
|
||||
import { useClientList, Client } from "@/hooks/useClientList";
|
||||
import {
|
||||
Building2,
|
||||
Plus,
|
||||
Users,
|
||||
CheckCircle,
|
||||
Loader2,
|
||||
@@ -43,7 +41,7 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { ListMobileCard, InfoField } from "@/components/organisms/MobileCard";
|
||||
import { DeleteConfirmDialog } from "@/components/ui/confirm-dialog";
|
||||
// DeleteConfirmDialog 제거 — 판매관리 거래처는 읽기 전용
|
||||
import { sendNewClientNotification } from "@/lib/actions/fcm";
|
||||
import { isNextRedirectError } from "@/lib/utils/redirect-error";
|
||||
|
||||
@@ -359,9 +357,11 @@ export default function CustomerAccountManagementPage() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
// mode=new 처리 (모든 훅 호출 후에 조건부 return - React 훅 규칙 준수)
|
||||
// mode=new 차단 — 등록은 회계관리>거래처관리에서만 가능
|
||||
if (mode === 'new') {
|
||||
return <ClientDetailClientV2 />;
|
||||
toast.error('거래처 등록은 회계관리 > 거래처관리에서 가능합니다.');
|
||||
router.push('/sales/client-management-sales-admin');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 상태 뱃지
|
||||
@@ -588,53 +588,13 @@ export default function CustomerAccountManagementPage() {
|
||||
<Bell className="w-4 h-4 mr-2" />
|
||||
신규업체
|
||||
</Button>
|
||||
<Button onClick={handleAddNew}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
거래처 등록
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
|
||||
renderTableRow,
|
||||
renderMobileCard,
|
||||
|
||||
renderDialogs: () => (
|
||||
<>
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<DeleteConfirmDialog
|
||||
open={isDeleteDialogOpen}
|
||||
onOpenChange={setIsDeleteDialogOpen}
|
||||
onConfirm={handleConfirmDelete}
|
||||
title="거래처 삭제 확인"
|
||||
description={
|
||||
<>
|
||||
{deleteTargetId
|
||||
? `거래처: ${clients.find((c) => c.id === deleteTargetId)?.name || deleteTargetId}`
|
||||
: ""}
|
||||
<br />
|
||||
이 거래처를 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 일괄 삭제 확인 다이얼로그 */}
|
||||
<DeleteConfirmDialog
|
||||
open={isBulkDeleteDialogOpen}
|
||||
onOpenChange={setIsBulkDeleteDialogOpen}
|
||||
onConfirm={handleConfirmBulkDelete}
|
||||
title="일괄 삭제 확인"
|
||||
description={
|
||||
<>
|
||||
선택한 <strong>{selectedItems.size}개</strong>의 거래처를 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
삭제된 데이터는 복구할 수 없습니다.
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
// 판매관리 거래처는 읽기 전용 — 삭제 다이얼로그 불필요
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,62 +7,39 @@
|
||||
* 클라이언트 사이드 데이터 페칭 (useClientList 훅 활용)
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
|
||||
import type { DetailMode, IntegratedDetailTemplateRef } from '@/components/templates/IntegratedDetailTemplate/types';
|
||||
import type { Client, ClientFormData } from '@/hooks/useClientList';
|
||||
import type { DetailMode } from '@/components/templates/IntegratedDetailTemplate/types';
|
||||
import type { Client } from '@/hooks/useClientList';
|
||||
import { useClientList } from '@/hooks/useClientList';
|
||||
import { clientDetailConfig } from './clientDetailConfig';
|
||||
import { toast } from 'sonner';
|
||||
import { useDevFillContext } from '@/components/dev/DevFillContext';
|
||||
import { generateClientData } from '@/components/dev/generators/clientData';
|
||||
|
||||
interface ClientDetailClientV2Props {
|
||||
clientId?: string;
|
||||
initialMode?: DetailMode;
|
||||
}
|
||||
|
||||
// 8자리 영문+숫자 코드 생성
|
||||
function generateCode(): string {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < 8; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function ClientDetailClientV2({ clientId, initialMode }: ClientDetailClientV2Props) {
|
||||
export function ClientDetailClientV2({ clientId }: ClientDetailClientV2Props) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { fetchClient, createClient, updateClient, deleteClient } = useClientList();
|
||||
const templateRef = useRef<IntegratedDetailTemplateRef>(null);
|
||||
const { isEnabled, registerFillForm, unregisterFillForm } = useDevFillContext();
|
||||
const { fetchClient } = useClientList();
|
||||
|
||||
// URL 쿼리에서 모드 결정
|
||||
const modeFromQuery = searchParams.get('mode') as DetailMode | null;
|
||||
const isNewMode = !clientId || clientId === 'new';
|
||||
|
||||
const [mode, setMode] = useState<DetailMode>(() => {
|
||||
if (isNewMode) return 'create';
|
||||
if (initialMode) return initialMode;
|
||||
if (modeFromQuery === 'edit') return 'edit';
|
||||
return 'view';
|
||||
});
|
||||
// 판매관리 거래처는 읽기 전용 — 수정/등록은 회계관리>거래처관리에서만 가능
|
||||
const [mode] = useState<DetailMode>('view');
|
||||
|
||||
const [clientData, setClientData] = useState<Client | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(!isNewMode);
|
||||
const [generatedCode, setGeneratedCode] = useState<string>('');
|
||||
|
||||
// 데이터 로드
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
// 신규 등록 차단 — 회계관리>거래처관리에서만 가능
|
||||
if (isNewMode) {
|
||||
// 신규 등록 시 코드 생성
|
||||
const code = generateCode();
|
||||
setGeneratedCode(code);
|
||||
setIsLoading(false);
|
||||
toast.error('거래처 등록은 회계관리 > 거래처관리에서 가능합니다.');
|
||||
router.push(clientDetailConfig.basePath);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -87,164 +64,32 @@ export function ClientDetailClientV2({ clientId, initialMode }: ClientDetailClie
|
||||
loadData();
|
||||
}, [clientId, isNewMode, router, fetchClient]);
|
||||
|
||||
// URL 쿼리 변경 감지
|
||||
useEffect(() => {
|
||||
if (!isNewMode && modeFromQuery === 'edit') {
|
||||
setMode('edit');
|
||||
} else if (!isNewMode && !modeFromQuery) {
|
||||
setMode('view');
|
||||
}
|
||||
}, [modeFromQuery, isNewMode]);
|
||||
|
||||
// DevFill 등록 (신규 등록 모드일 때만)
|
||||
useEffect(() => {
|
||||
if (!isEnabled || !isNewMode) return;
|
||||
|
||||
const handleDevFill = () => {
|
||||
const data = generateClientData();
|
||||
if (templateRef.current) {
|
||||
templateRef.current.setFormData(data as unknown as Record<string, unknown>);
|
||||
}
|
||||
};
|
||||
|
||||
registerFillForm('client', handleDevFill);
|
||||
|
||||
return () => {
|
||||
unregisterFillForm('client');
|
||||
};
|
||||
}, [isEnabled, isNewMode, registerFillForm, unregisterFillForm]);
|
||||
|
||||
// 모드 변경 핸들러
|
||||
// 모드 변경 차단 — 읽기 전용
|
||||
const handleModeChange = useCallback(
|
||||
(newMode: DetailMode) => {
|
||||
setMode(newMode);
|
||||
if (newMode === 'edit' && clientId) {
|
||||
router.push(`${clientDetailConfig.basePath}/${clientId}?mode=edit`);
|
||||
} else if (newMode === 'view' && clientId) {
|
||||
router.push(`${clientDetailConfig.basePath}/${clientId}?mode=view`);
|
||||
}
|
||||
(_newMode: DetailMode) => {
|
||||
toast.info('거래처 수정은 회계관리 > 거래처관리에서 가능합니다.');
|
||||
},
|
||||
[router, clientId]
|
||||
[]
|
||||
);
|
||||
|
||||
// 저장 핸들러
|
||||
const handleSubmit = useCallback(
|
||||
async (formData: Record<string, unknown>) => {
|
||||
try {
|
||||
// formData를 ClientFormData로 변환
|
||||
const clientFormData: ClientFormData = {
|
||||
clientCode: (formData.clientCode as string) || generatedCode,
|
||||
name: formData.name as string,
|
||||
businessNo: formData.businessNo as string,
|
||||
representative: formData.representative as string,
|
||||
phone: formData.phone as string,
|
||||
address: formData.address as string,
|
||||
email: formData.email as string,
|
||||
businessType: formData.businessType as string,
|
||||
businessItem: formData.businessItem as string,
|
||||
isActive: formData.isActive === 'true',
|
||||
clientType: (formData.clientType as ClientFormData['clientType']) || '매입',
|
||||
mobile: formData.mobile as string,
|
||||
fax: formData.fax as string,
|
||||
managerName: formData.managerName as string,
|
||||
managerTel: formData.managerTel as string,
|
||||
systemManager: formData.systemManager as string,
|
||||
accountId: formData.accountId as string || '',
|
||||
accountPassword: formData.accountPassword as string || '',
|
||||
purchasePaymentDay: '말일',
|
||||
salesPaymentDay: '말일',
|
||||
taxAgreement: false,
|
||||
taxAmount: '',
|
||||
taxStartDate: '',
|
||||
taxEndDate: '',
|
||||
badDebt: false,
|
||||
badDebtAmount: '',
|
||||
badDebtReceiveDate: '',
|
||||
badDebtEndDate: '',
|
||||
badDebtProgress: '',
|
||||
memo: formData.memo as string || '',
|
||||
};
|
||||
|
||||
if (isNewMode) {
|
||||
const result = await createClient(clientFormData);
|
||||
if (result) {
|
||||
toast.success('거래처가 등록되었습니다.');
|
||||
router.push(clientDetailConfig.basePath);
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, error: '거래처 등록에 실패했습니다.' };
|
||||
} else {
|
||||
const result = await updateClient(clientId!, clientFormData);
|
||||
if (result) {
|
||||
toast.success('거래처가 수정되었습니다.');
|
||||
router.push(`${clientDetailConfig.basePath}/${clientId}?mode=view`);
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, error: '거래처 수정에 실패했습니다.' };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('저장 실패:', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : '저장 중 오류가 발생했습니다.' };
|
||||
}
|
||||
},
|
||||
[isNewMode, clientId, generatedCode, router, createClient, updateClient]
|
||||
);
|
||||
|
||||
// 삭제 핸들러
|
||||
const handleDelete = useCallback(
|
||||
async (id: string | number) => {
|
||||
try {
|
||||
const result = await deleteClient(String(id));
|
||||
if (result) {
|
||||
toast.success('거래처가 삭제되었습니다.');
|
||||
router.push(clientDetailConfig.basePath);
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, error: '거래처 삭제에 실패했습니다.' };
|
||||
} catch (error) {
|
||||
console.error('삭제 실패:', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : '삭제 중 오류가 발생했습니다.' };
|
||||
}
|
||||
},
|
||||
[router, deleteClient]
|
||||
);
|
||||
|
||||
// 취소 핸들러
|
||||
// 취소(목록으로) 핸들러
|
||||
const handleCancel = useCallback(() => {
|
||||
if (isNewMode) {
|
||||
router.push(clientDetailConfig.basePath);
|
||||
} else {
|
||||
setMode('view');
|
||||
router.push(`${clientDetailConfig.basePath}/${clientId}?mode=view`);
|
||||
}
|
||||
}, [router, clientId, isNewMode]);
|
||||
|
||||
// 초기 데이터 (신규 등록 시 코드 포함)
|
||||
const initialData = isNewMode
|
||||
? ({ code: generatedCode } as Client)
|
||||
: clientData || undefined;
|
||||
router.push(clientDetailConfig.basePath);
|
||||
}, [router]);
|
||||
|
||||
// 타이틀 동적 설정
|
||||
const dynamicConfig = {
|
||||
...clientDetailConfig,
|
||||
title:
|
||||
mode === 'create'
|
||||
? '거래처'
|
||||
: mode === 'edit'
|
||||
? clientData?.name || '거래처'
|
||||
: clientData?.name || '거래처 상세',
|
||||
title: clientData?.name || '거래처 상세',
|
||||
};
|
||||
|
||||
return (
|
||||
<IntegratedDetailTemplate
|
||||
ref={templateRef}
|
||||
config={dynamicConfig}
|
||||
mode={mode}
|
||||
initialData={initialData as Record<string, unknown> | undefined}
|
||||
initialData={clientData as unknown as Record<string, unknown> | undefined}
|
||||
itemId={clientId}
|
||||
isLoading={isLoading}
|
||||
onSubmit={handleSubmit}
|
||||
onDelete={handleDelete}
|
||||
onCancel={handleCancel}
|
||||
onModeChange={handleModeChange}
|
||||
/>
|
||||
|
||||
@@ -202,16 +202,12 @@ export const clientDetailConfig: DetailConfig<Client> = {
|
||||
actions: {
|
||||
submitLabel: '저장',
|
||||
cancelLabel: '취소',
|
||||
showDelete: true,
|
||||
showDelete: false,
|
||||
deleteLabel: '삭제',
|
||||
showEdit: true,
|
||||
showEdit: false,
|
||||
editLabel: '수정',
|
||||
showBack: true,
|
||||
backLabel: '목록',
|
||||
deleteConfirmMessage: {
|
||||
title: '거래처 삭제',
|
||||
description: '이 거래처를 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다.',
|
||||
},
|
||||
},
|
||||
transformInitialData: (data: Client) => ({
|
||||
businessNo: data.businessNo || '',
|
||||
|
||||
Reference in New Issue
Block a user