fix: 품목관리 수정 기능 버그 수정 및 Sales 페이지 추가
## 품목관리 수정 버그 수정 - FG(제품) 수정 시 품목명 반영 안되는 문제 해결 - productName → name 필드 매핑 추가 - FG 품목코드 = 품목명 동기화 로직 추가 - Materials(SM, RM, CS) 수정페이지 진입 오류 해결 - UNIQUE 제약조건 위반 오류 해결 ## Sales 페이지 - 거래처관리 (client-management-sales-admin) 페이지 구현 - 견적관리 (quote-management) 페이지 구현 - 관련 컴포넌트 및 훅 추가 ## 기타 - 회원가입 페이지 차단 처리 - 디버깅용 콘솔 로그 추가 (PUT 요청/응답 확인용) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
530
src/hooks/useClientList.ts
Normal file
530
src/hooks/useClientList.ts
Normal file
@@ -0,0 +1,530 @@
|
||||
"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: "Y" | "N";
|
||||
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 응답 → 프론트엔드 타입 변환
|
||||
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 === "Y" ? "활성" : "비활성",
|
||||
groupId: api.client_group_id ? String(api.client_group_id) : null,
|
||||
// 2차 추가 필드
|
||||
clientType: 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 ? "Y" : "N",
|
||||
// 2차 추가 필드
|
||||
client_type: 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 ? "Y" : "N",
|
||||
// 2차 추가 필드
|
||||
client_type: 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user