Files
sam-react-prod/src/hooks/useClientList.ts

553 lines
16 KiB
TypeScript
Raw Normal View History

"use client";
import { useState, useCallback } from "react";
// ============================================
// 타입 정의
// ============================================
// 거래처 유형
export type ClientType = "매입" | "매출" | "매입매출";
// 악성채권 진행상태
export type BadDebtProgress = "협의중" | "소송중" | "회수완료" | "대손처리" | "";
// 백엔드 API 응답 타입 (확장)
export interface ClientApiResponse {
id: number;
tenant_id: number;
client_group_id: number | null;
client_code: string;
name: string;
contact_person: string | null;
phone: string | null;
email: string | null;
address: string | null;
business_no: string | null;
business_type: string | null;
business_item: string | null;
is_active: boolean;
created_at: string;
updated_at: string;
// 2차 추가 필드 (백엔드 완료 시 활성화)
client_type?: ClientType;
mobile?: string | null;
fax?: string | null;
manager_name?: string | null;
manager_tel?: string | null;
system_manager?: string | null;
account_id?: string | null;
account_password?: string | null;
purchase_payment_day?: string | null;
sales_payment_day?: string | null;
tax_agreement?: boolean;
tax_amount?: number | null;
tax_start_date?: string | null;
tax_end_date?: string | null;
bad_debt?: boolean;
bad_debt_amount?: number | null;
bad_debt_receive_date?: string | null;
bad_debt_end_date?: string | null;
bad_debt_progress?: BadDebtProgress;
memo?: string | null;
}
// 프론트엔드 타입 (확장)
export interface Client {
id: string;
code: string;
name: string;
businessNo: string;
representative: string; // contact_person
phone: string;
address: string;
email: string;
businessType: string;
businessItem: string;
registeredDate: string;
status: "활성" | "비활성";
groupId: string | null;
groupName?: string;
// 2차 추가 필드
clientType: ClientType;
mobile: string;
fax: string;
managerName: string;
managerTel: string;
systemManager: string;
accountId: string;
accountPassword: string;
purchasePaymentDay: string;
salesPaymentDay: string;
taxAgreement: boolean;
taxAmount: string;
taxStartDate: string;
taxEndDate: string;
badDebt: boolean;
badDebtAmount: string;
badDebtReceiveDate: string;
badDebtEndDate: string;
badDebtProgress: BadDebtProgress;
memo: string;
}
// 페이지네이션 정보
export interface PaginationInfo {
currentPage: number;
lastPage: number;
total: number;
perPage: number;
}
// 검색 파라미터
export interface ClientSearchParams {
page?: number;
size?: number;
q?: string;
onlyActive?: boolean;
}
// 생성/수정 요청 타입 (확장)
export interface ClientFormData {
clientCode?: string;
name: string;
businessNo: string;
representative: string;
phone: string;
address: string;
email: string;
businessType: string;
businessItem: string;
groupId?: string | null;
isActive: boolean;
// 2차 추가 필드
clientType: ClientType;
mobile: string;
fax: string;
managerName: string;
managerTel: string;
systemManager: string;
accountId: string;
accountPassword: string;
purchasePaymentDay: string;
salesPaymentDay: string;
taxAgreement: boolean;
taxAmount: string;
taxStartDate: string;
taxEndDate: string;
badDebt: boolean;
badDebtAmount: string;
badDebtReceiveDate: string;
badDebtEndDate: string;
badDebtProgress: BadDebtProgress;
memo: string;
}
// 폼 초기값
export const INITIAL_CLIENT_FORM: ClientFormData = {
name: "",
businessNo: "",
representative: "",
phone: "",
address: "",
email: "",
businessType: "",
businessItem: "",
groupId: null,
isActive: true,
clientType: "매입",
mobile: "",
fax: "",
managerName: "",
managerTel: "",
systemManager: "",
accountId: "",
accountPassword: "",
purchasePaymentDay: "말일",
salesPaymentDay: "말일",
taxAgreement: false,
taxAmount: "",
taxStartDate: "",
taxEndDate: "",
badDebt: false,
badDebtAmount: "",
badDebtReceiveDate: "",
badDebtEndDate: "",
badDebtProgress: "",
memo: "",
};
// ============================================
// 데이터 변환 유틸리티
// ============================================
// 거래처 유형 매핑 (API → Frontend)
const CLIENT_TYPE_MAP: Record<string, ClientType> = {
'PURCHASE': '매입',
'SALES': '매출',
'BOTH': '매입매출',
'매입': '매입',
'매출': '매출',
'매입매출': '매입매출',
};
// 거래처 유형 역매핑 (Frontend → API)
const CLIENT_TYPE_REVERSE_MAP: Record<ClientType, string> = {
'매입': 'PURCHASE',
'매출': 'SALES',
'매입매출': 'BOTH',
};
// 거래처 유형 변환 (API → Frontend)
function mapClientType(apiValue: string | undefined): ClientType {
if (!apiValue) return '매입';
return CLIENT_TYPE_MAP[apiValue] || '매입';
}
// API 응답 → 프론트엔드 타입 변환
export function transformClientFromApi(api: ClientApiResponse): Client {
return {
id: String(api.id),
code: api.client_code,
name: api.name,
representative: api.contact_person || "",
phone: api.phone || "",
email: api.email || "",
address: api.address || "",
businessNo: api.business_no || "",
businessType: api.business_type || "",
businessItem: api.business_item || "",
registeredDate: api.created_at ? api.created_at.split(" ")[0] : "",
status: api.is_active ? "활성" : "비활성",
groupId: api.client_group_id ? String(api.client_group_id) : null,
// 2차 추가 필드
clientType: mapClientType(api.client_type),
mobile: api.mobile || "",
fax: api.fax || "",
managerName: api.manager_name || "",
managerTel: api.manager_tel || "",
systemManager: api.system_manager || "",
accountId: api.account_id || "",
accountPassword: "", // 비밀번호는 조회 시 비움
purchasePaymentDay: api.purchase_payment_day || "말일",
salesPaymentDay: api.sales_payment_day || "말일",
taxAgreement: api.tax_agreement || false,
taxAmount: api.tax_amount ? String(api.tax_amount) : "",
taxStartDate: api.tax_start_date || "",
taxEndDate: api.tax_end_date || "",
badDebt: api.bad_debt || false,
badDebtAmount: api.bad_debt_amount ? String(api.bad_debt_amount) : "",
badDebtReceiveDate: api.bad_debt_receive_date || "",
badDebtEndDate: api.bad_debt_end_date || "",
badDebtProgress: api.bad_debt_progress || "",
memo: api.memo || "",
};
}
// 프론트엔드 → API 요청 변환 (생성)
export function transformClientToApiCreate(form: ClientFormData): Record<string, unknown> {
return {
client_code: form.clientCode,
name: form.name,
contact_person: form.representative || null,
phone: form.phone || null,
email: form.email || null,
address: form.address || null,
business_no: form.businessNo || null,
business_type: form.businessType || null,
business_item: form.businessItem || null,
client_group_id: form.groupId ? Number(form.groupId) : null,
is_active: form.isActive,
// 2차 추가 필드
client_type: CLIENT_TYPE_REVERSE_MAP[form.clientType] || form.clientType,
mobile: form.mobile || null,
fax: form.fax || null,
manager_name: form.managerName || null,
manager_tel: form.managerTel || null,
system_manager: form.systemManager || null,
account_id: form.accountId || null,
account_password: form.accountPassword || null,
purchase_payment_day: form.purchasePaymentDay || null,
sales_payment_day: form.salesPaymentDay || null,
tax_agreement: form.taxAgreement,
tax_amount: form.taxAmount ? Number(form.taxAmount) : null,
tax_start_date: form.taxStartDate || null,
tax_end_date: form.taxEndDate || null,
bad_debt: form.badDebt,
bad_debt_amount: form.badDebtAmount ? Number(form.badDebtAmount) : null,
bad_debt_receive_date: form.badDebtReceiveDate || null,
bad_debt_end_date: form.badDebtEndDate || null,
bad_debt_progress: form.badDebtProgress || null,
memo: form.memo || null,
};
}
// 프론트엔드 → API 요청 변환 (수정)
export function transformClientToApiUpdate(form: ClientFormData): Record<string, unknown> {
const data: Record<string, unknown> = {
name: form.name,
contact_person: form.representative || null,
phone: form.phone || null,
email: form.email || null,
address: form.address || null,
business_no: form.businessNo || null,
business_type: form.businessType || null,
business_item: form.businessItem || null,
client_group_id: form.groupId ? Number(form.groupId) : null,
is_active: form.isActive,
// 2차 추가 필드
client_type: CLIENT_TYPE_REVERSE_MAP[form.clientType] || form.clientType,
mobile: form.mobile || null,
fax: form.fax || null,
manager_name: form.managerName || null,
manager_tel: form.managerTel || null,
system_manager: form.systemManager || null,
account_id: form.accountId || null,
purchase_payment_day: form.purchasePaymentDay || null,
sales_payment_day: form.salesPaymentDay || null,
tax_agreement: form.taxAgreement,
tax_amount: form.taxAmount ? Number(form.taxAmount) : null,
tax_start_date: form.taxStartDate || null,
tax_end_date: form.taxEndDate || null,
bad_debt: form.badDebt,
bad_debt_amount: form.badDebtAmount ? Number(form.badDebtAmount) : null,
bad_debt_receive_date: form.badDebtReceiveDate || null,
bad_debt_end_date: form.badDebtEndDate || null,
bad_debt_progress: form.badDebtProgress || null,
memo: form.memo || null,
};
// 비밀번호는 입력한 경우에만 전송
if (form.accountPassword) {
data.account_password = form.accountPassword;
}
return data;
}
// Client → ClientFormData 변환 (수정 시 폼 초기화용)
export function clientToFormData(client: Client): ClientFormData {
return {
clientCode: client.code,
name: client.name,
businessNo: client.businessNo,
representative: client.representative,
phone: client.phone,
address: client.address,
email: client.email,
businessType: client.businessType,
businessItem: client.businessItem,
groupId: client.groupId,
isActive: client.status === "활성",
clientType: client.clientType,
mobile: client.mobile,
fax: client.fax,
managerName: client.managerName,
managerTel: client.managerTel,
systemManager: client.systemManager,
accountId: client.accountId,
accountPassword: "", // 비밀번호는 비움
purchasePaymentDay: client.purchasePaymentDay,
salesPaymentDay: client.salesPaymentDay,
taxAgreement: client.taxAgreement,
taxAmount: client.taxAmount,
taxStartDate: client.taxStartDate,
taxEndDate: client.taxEndDate,
badDebt: client.badDebt,
badDebtAmount: client.badDebtAmount,
badDebtReceiveDate: client.badDebtReceiveDate,
badDebtEndDate: client.badDebtEndDate,
badDebtProgress: client.badDebtProgress,
memo: client.memo,
};
}
// ============================================
// useClientList 훅
// ============================================
export function useClientList() {
const [clients, setClients] = useState<Client[]>([]);
const [pagination, setPagination] = useState<PaginationInfo | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// 목록 조회
const fetchClients = useCallback(async (params: ClientSearchParams = {}) => {
setIsLoading(true);
setError(null);
try {
const searchParams = new URLSearchParams();
if (params.page) searchParams.set("page", String(params.page));
if (params.size) searchParams.set("size", String(params.size));
if (params.q) searchParams.set("q", params.q);
if (params.onlyActive !== undefined) {
searchParams.set("only_active", params.onlyActive ? "1" : "0");
}
const response = await fetch(`/api/proxy/clients?${searchParams.toString()}`);
if (!response.ok) {
throw new Error(`API 오류: ${response.status}`);
}
const result = await response.json();
if (result.success && result.data) {
const apiClients: ClientApiResponse[] = result.data.data || [];
const transformedClients = apiClients.map(transformClientFromApi);
setClients(transformedClients);
setPagination({
currentPage: result.data.current_page || 1,
lastPage: result.data.last_page || 1,
total: result.data.total || 0,
perPage: result.data.per_page || 20,
});
} else {
throw new Error(result.message || "데이터 조회 실패");
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "알 수 없는 오류";
setError(errorMessage);
setClients([]);
setPagination(null);
} finally {
setIsLoading(false);
}
}, []);
// 단건 조회
const fetchClient = useCallback(async (id: string): Promise<Client | null> => {
try {
const response = await fetch(`/api/proxy/clients/${id}`);
if (!response.ok) {
throw new Error(`API 오류: ${response.status}`);
}
const result = await response.json();
if (result.success && result.data) {
return transformClientFromApi(result.data);
}
return null;
} catch (err) {
console.error("거래처 조회 실패:", err);
return null;
}
}, []);
// 생성
const createClient = useCallback(async (formData: ClientFormData): Promise<Client | null> => {
try {
const response = await fetch("/api/proxy/clients", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(transformClientToApiCreate(formData)),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || `API 오류: ${response.status}`);
}
const result = await response.json();
if (result.success && result.data) {
return transformClientFromApi(result.data);
}
return null;
} catch (err) {
console.error("거래처 생성 실패:", err);
throw err;
}
}, []);
// 수정
const updateClient = useCallback(async (id: string, formData: ClientFormData): Promise<Client | null> => {
try {
const response = await fetch(`/api/proxy/clients/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(transformClientToApiUpdate(formData)),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || `API 오류: ${response.status}`);
}
const result = await response.json();
if (result.success && result.data) {
return transformClientFromApi(result.data);
}
return null;
} catch (err) {
console.error("거래처 수정 실패:", err);
throw err;
}
}, []);
// 삭제
const deleteClient = useCallback(async (id: string): Promise<boolean> => {
try {
const response = await fetch(`/api/proxy/clients/${id}`, {
method: "DELETE",
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || `API 오류: ${response.status}`);
}
return true;
} catch (err) {
console.error("거래처 삭제 실패:", err);
throw err;
}
}, []);
// 활성/비활성 토글
const toggleClientStatus = useCallback(async (id: string): Promise<Client | null> => {
try {
const response = await fetch(`/api/proxy/clients/${id}/toggle`, {
method: "PATCH",
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || `API 오류: ${response.status}`);
}
const result = await response.json();
if (result.success && result.data) {
return transformClientFromApi(result.data);
}
return null;
} catch (err) {
console.error("거래처 상태 변경 실패:", err);
throw err;
}
}, []);
return {
// 상태
clients,
pagination,
isLoading,
error,
// 액션
fetchClients,
fetchClient,
createClient,
updateClient,
deleteClient,
toggleClientStatus,
// 유틸리티
setClients,
};
}