Files
sam-react-prod/src/hooks/useClientGroupList.ts
byeongcheolryu 751e65f59b 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>
2025-12-04 20:52:42 +09:00

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,
};
}