Phase 2 완료 (4개): - 노무관리, 단가관리(건설), 입금, 출금 Phase 3 라우팅 구조 변경 완료 (22개): - 거래처(영업), 팝업관리, 공정관리, 게시판관리, 대손추심, Q&A - 현장관리, 실행내역, 견적관리, 견적(테스트) - 입찰관리, 이슈관리, 현장설명회, 견적서(건설) - 협력업체, 시공관리, 기성관리, 품목관리(건설) - 회계 도메인: 거래처, 매출, 세금계산서, 매입 신규 컴포넌트: - ErrorCard: 에러 페이지 UI 통일 - ServerErrorPage: V2 페이지 에러 처리 필수 - V2 Client 컴포넌트 및 Config 파일들 총 47개 상세 페이지 중 28개 완료, 19개 제외/불필요 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
553 lines
16 KiB
TypeScript
553 lines
16 KiB
TypeScript
"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,
|
|
};
|
|
} |