feat: 견적 V2 품목 검색 API 연동 및 수동 품목 관리 개선
- ItemSearchModal: API 프록시 라우트 연동, 검색 유효성 검사 (영문/한글 1자 이상) - items.ts: HttpOnly 쿠키 인증을 위한 프록시 라우트 사용 - LocationDetailPanel: 수동 품목 추가 시 subtotals/grouped_items 동시 업데이트 - QuoteRegistrationV2: 견적 산출 시 수동 추가 품목(is_manual) 보존 - 상세별 합계에서 수동 추가 품목이 카테고리별로 표시되도록 개선
This commit is contained in:
@@ -86,6 +86,40 @@ async function handleApiResponse<T>(response: Response): Promise<T> {
|
||||
* fetchItems().then(setItems);
|
||||
* }, []);
|
||||
*/
|
||||
// API 응답 → 프론트엔드(camelCase) 변환
|
||||
interface ApiItemResponse {
|
||||
id?: number | string;
|
||||
// DB 필드명 (code, name)
|
||||
code?: string;
|
||||
name?: string;
|
||||
item_type?: string;
|
||||
unit?: string;
|
||||
specification?: string;
|
||||
is_active?: boolean | number;
|
||||
// snake_case 대안 (item_code, item_name)
|
||||
item_code?: string;
|
||||
item_name?: string;
|
||||
// camelCase도 지원 (이미 변환된 경우)
|
||||
itemCode?: string;
|
||||
itemName?: string;
|
||||
itemType?: string;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
function transformItemFromApi(apiItem: ApiItemResponse): ItemMaster {
|
||||
return {
|
||||
id: String(apiItem.id || ''),
|
||||
// 우선순위: code > item_code > itemCode
|
||||
itemCode: apiItem.code || apiItem.item_code || apiItem.itemCode || '',
|
||||
// 우선순위: name > item_name > itemName
|
||||
itemName: apiItem.name || apiItem.item_name || apiItem.itemName || '',
|
||||
itemType: (apiItem.item_type || apiItem.itemType || 'FG') as ItemMaster['itemType'],
|
||||
unit: apiItem.unit || '',
|
||||
specification: apiItem.specification || '',
|
||||
isActive: apiItem.is_active === true || apiItem.is_active === 1 || apiItem.isActive === true,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchItems(
|
||||
params?: FetchItemsParams
|
||||
): Promise<ItemMaster[]> {
|
||||
@@ -99,12 +133,60 @@ export async function fetchItems(
|
||||
});
|
||||
}
|
||||
|
||||
const url = `${API_URL}/api/items${queryParams.toString() ? `?${queryParams}` : ''}`;
|
||||
// 클라이언트에서 호출 시 프록시 라우트 사용 (HttpOnly 쿠키 인증 지원)
|
||||
const isClient = typeof window !== 'undefined';
|
||||
const baseUrl = isClient ? '/api/proxy' : `${API_URL}/api/v1`;
|
||||
const url = `${baseUrl}/items${queryParams.toString() ? `?${queryParams}` : ''}`;
|
||||
|
||||
const response = await fetch(url, createFetchOptions());
|
||||
const data = await handleApiResponse<ApiResponse<ItemMaster[]>>(response);
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
return data.data;
|
||||
if (!response.ok) {
|
||||
throw new Error(`품목 조회 실패: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// 디버깅: API 응답 구조 확인
|
||||
console.log('[fetchItems] API 응답:', JSON.stringify(result, null, 2).slice(0, 1000));
|
||||
|
||||
// 데이터 추출
|
||||
let rawItems: ApiItemResponse[] = [];
|
||||
|
||||
if (result.success && result.data) {
|
||||
// 페이지네이션 응답: { success: true, data: { data: [...], ... } }
|
||||
if (result.data.data && Array.isArray(result.data.data)) {
|
||||
rawItems = result.data.data;
|
||||
}
|
||||
// 단순 배열 응답: { success: true, data: [...] }
|
||||
else if (Array.isArray(result.data)) {
|
||||
rawItems = result.data;
|
||||
}
|
||||
}
|
||||
// 직접 배열 응답 (fallback)
|
||||
else if (Array.isArray(result)) {
|
||||
rawItems = result;
|
||||
}
|
||||
|
||||
// 디버깅: rawItems 첫 번째 항목 확인
|
||||
if (rawItems.length > 0) {
|
||||
console.log('[fetchItems] rawItems[0]:', rawItems[0]);
|
||||
}
|
||||
|
||||
// snake_case → camelCase 변환
|
||||
const transformed = rawItems.map(transformItemFromApi);
|
||||
|
||||
// 디버깅: 변환된 첫 번째 항목 확인
|
||||
if (transformed.length > 0) {
|
||||
console.log('[fetchItems] transformed[0]:', transformed[0]);
|
||||
}
|
||||
|
||||
return transformed;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user