## 품목관리 수정 버그 수정 - 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>
366 lines
10 KiB
TypeScript
366 lines
10 KiB
TypeScript
/**
|
|
* 거래처 그룹(ClientGroup) API 훅
|
|
*
|
|
* 백엔드 API: /api/v1/client-groups
|
|
* - GET /client-groups - 목록 조회
|
|
* - GET /client-groups/{id} - 단건 조회
|
|
* - POST /client-groups - 생성
|
|
* - PUT /client-groups/{id} - 수정
|
|
* - DELETE /client-groups/{id} - 삭제
|
|
* - PATCH /client-groups/{id}/toggle - 활성/비활성 토글
|
|
*/
|
|
|
|
import { useState, useCallback } from 'react';
|
|
|
|
// 백엔드 API 응답 타입
|
|
export interface ClientGroupApiResponse {
|
|
id: number;
|
|
tenant_id: number;
|
|
group_code: string;
|
|
group_name: string;
|
|
price_rate: string | number; // decimal(10,4)
|
|
is_active: boolean | number; // 0 or 1
|
|
created_at: string;
|
|
updated_at: string;
|
|
created_by: number | null;
|
|
updated_by: number | null;
|
|
}
|
|
|
|
// 프론트엔드 타입
|
|
export interface ClientGroup {
|
|
id: string;
|
|
code: string;
|
|
name: string;
|
|
priceRate: number;
|
|
status: '활성' | '비활성';
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
// 폼 데이터 타입
|
|
export interface ClientGroupFormData {
|
|
groupCode: string;
|
|
groupName: string;
|
|
priceRate: number;
|
|
isActive?: boolean;
|
|
}
|
|
|
|
// 페이지네이션 정보
|
|
export interface PaginationInfo {
|
|
currentPage: number;
|
|
lastPage: number;
|
|
perPage: number;
|
|
total: number;
|
|
from: number;
|
|
to: number;
|
|
}
|
|
|
|
// 훅 반환 타입
|
|
export interface UseClientGroupListReturn {
|
|
groups: ClientGroup[];
|
|
pagination: PaginationInfo | null;
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
fetchGroups: (params?: FetchGroupsParams) => Promise<void>;
|
|
fetchGroup: (id: string) => Promise<ClientGroup | null>;
|
|
createGroup: (data: ClientGroupFormData) => Promise<ClientGroup>;
|
|
updateGroup: (id: string, data: ClientGroupFormData) => Promise<ClientGroup>;
|
|
deleteGroup: (id: string) => Promise<void>;
|
|
toggleGroupStatus: (id: string) => Promise<void>;
|
|
}
|
|
|
|
// API 요청 파라미터
|
|
interface FetchGroupsParams {
|
|
page?: number;
|
|
size?: number;
|
|
q?: string;
|
|
onlyActive?: boolean;
|
|
}
|
|
|
|
// API 응답 → 프론트엔드 타입 변환
|
|
function transformGroupFromApi(apiGroup: ClientGroupApiResponse): ClientGroup {
|
|
// is_active가 boolean 또는 number(0/1)일 수 있음
|
|
const isActive = apiGroup.is_active === true || apiGroup.is_active === 1;
|
|
|
|
return {
|
|
id: String(apiGroup.id),
|
|
code: apiGroup.group_code || '',
|
|
name: apiGroup.group_name || '',
|
|
priceRate: Number(apiGroup.price_rate) || 0,
|
|
status: isActive ? '활성' : '비활성',
|
|
createdAt: apiGroup.created_at || '',
|
|
updatedAt: apiGroup.updated_at || '',
|
|
};
|
|
}
|
|
|
|
// 프론트엔드 타입 → API 요청 변환 (생성용)
|
|
function transformGroupToApiCreate(data: ClientGroupFormData): Record<string, unknown> {
|
|
return {
|
|
group_code: data.groupCode,
|
|
group_name: data.groupName,
|
|
price_rate: data.priceRate,
|
|
is_active: data.isActive !== false, // 기본값 true
|
|
};
|
|
}
|
|
|
|
// 프론트엔드 타입 → API 요청 변환 (수정용)
|
|
function transformGroupToApiUpdate(data: ClientGroupFormData): Record<string, unknown> {
|
|
return {
|
|
group_code: data.groupCode,
|
|
group_name: data.groupName,
|
|
price_rate: data.priceRate,
|
|
is_active: data.isActive,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 거래처 그룹 관리 훅
|
|
*/
|
|
export function useClientGroupList(): UseClientGroupListReturn {
|
|
const [groups, setGroups] = useState<ClientGroup[]>([]);
|
|
const [pagination, setPagination] = useState<PaginationInfo | null>(null);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
/**
|
|
* 거래처 그룹 목록 조회
|
|
*/
|
|
const fetchGroups = useCallback(async (params?: FetchGroupsParams) => {
|
|
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', String(params.onlyActive));
|
|
}
|
|
|
|
const queryString = searchParams.toString();
|
|
const url = `/api/proxy/client-groups${queryString ? `?${queryString}` : ''}`;
|
|
|
|
const response = await fetch(url, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
credentials: 'include',
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`거래처 그룹 목록 조회 실패: ${response.status}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
// Laravel paginate 응답 구조: { success, data: { current_page, data: [...], last_page, ... } }
|
|
if (result.success && result.data) {
|
|
const paginatedData = result.data;
|
|
const items: ClientGroupApiResponse[] = paginatedData.data || [];
|
|
const transformedGroups = items.map(transformGroupFromApi);
|
|
setGroups(transformedGroups);
|
|
|
|
setPagination({
|
|
currentPage: paginatedData.current_page || 1,
|
|
lastPage: paginatedData.last_page || 1,
|
|
perPage: paginatedData.per_page || 20,
|
|
total: paginatedData.total || 0,
|
|
from: paginatedData.from || 0,
|
|
to: paginatedData.to || 0,
|
|
});
|
|
} else if (Array.isArray(result)) {
|
|
// 단순 배열 응답 (페이지네이션 없음)
|
|
const transformedGroups = result.map(transformGroupFromApi);
|
|
setGroups(transformedGroups);
|
|
setPagination(null);
|
|
}
|
|
} catch (err) {
|
|
const errorMessage = err instanceof Error ? err.message : '목록 조회 중 오류가 발생했습니다';
|
|
setError(errorMessage);
|
|
console.error('fetchGroups error:', err);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* 거래처 그룹 단건 조회
|
|
*/
|
|
const fetchGroup = useCallback(async (id: string): Promise<ClientGroup | null> => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const response = await fetch(`/api/proxy/client-groups/${id}`, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
credentials: 'include',
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`거래처 그룹 조회 실패: ${response.status}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
const data = result.data || result;
|
|
|
|
return transformGroupFromApi(data);
|
|
} catch (err) {
|
|
const errorMessage = err instanceof Error ? err.message : '조회 중 오류가 발생했습니다';
|
|
setError(errorMessage);
|
|
console.error('fetchGroup error:', err);
|
|
return null;
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* 거래처 그룹 생성
|
|
*/
|
|
const createGroup = useCallback(async (data: ClientGroupFormData): Promise<ClientGroup> => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const apiData = transformGroupToApiCreate(data);
|
|
|
|
const response = await fetch('/api/proxy/client-groups', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify(apiData),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.message || `거래처 그룹 생성 실패: ${response.status}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
const resultData = result.data || result;
|
|
|
|
return transformGroupFromApi(resultData);
|
|
} catch (err) {
|
|
const errorMessage = err instanceof Error ? err.message : '생성 중 오류가 발생했습니다';
|
|
setError(errorMessage);
|
|
throw err;
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* 거래처 그룹 수정
|
|
*/
|
|
const updateGroup = useCallback(async (id: string, data: ClientGroupFormData): Promise<ClientGroup> => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const apiData = transformGroupToApiUpdate(data);
|
|
|
|
const response = await fetch(`/api/proxy/client-groups/${id}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify(apiData),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.message || `거래처 그룹 수정 실패: ${response.status}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
const resultData = result.data || result;
|
|
|
|
return transformGroupFromApi(resultData);
|
|
} catch (err) {
|
|
const errorMessage = err instanceof Error ? err.message : '수정 중 오류가 발생했습니다';
|
|
setError(errorMessage);
|
|
throw err;
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* 거래처 그룹 삭제
|
|
*/
|
|
const deleteGroup = useCallback(async (id: string): Promise<void> => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const response = await fetch(`/api/proxy/client-groups/${id}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
credentials: 'include',
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.message || `거래처 그룹 삭제 실패: ${response.status}`);
|
|
}
|
|
} catch (err) {
|
|
const errorMessage = err instanceof Error ? err.message : '삭제 중 오류가 발생했습니다';
|
|
setError(errorMessage);
|
|
throw err;
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* 거래처 그룹 활성/비활성 토글
|
|
*/
|
|
const toggleGroupStatus = useCallback(async (id: string): Promise<void> => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const response = await fetch(`/api/proxy/client-groups/${id}/toggle`, {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
credentials: 'include',
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.message || `상태 변경 실패: ${response.status}`);
|
|
}
|
|
} catch (err) {
|
|
const errorMessage = err instanceof Error ? err.message : '상태 변경 중 오류가 발생했습니다';
|
|
setError(errorMessage);
|
|
throw err;
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
return {
|
|
groups,
|
|
pagination,
|
|
isLoading,
|
|
error,
|
|
fetchGroups,
|
|
fetchGroup,
|
|
createGroup,
|
|
updateGroup,
|
|
deleteGroup,
|
|
toggleGroupStatus,
|
|
};
|
|
} |