- BOMItem Omit 타입 시그니처 통일 (useTemplateManagement, SectionsTab, ItemMasterContext) - HeadersInit → Record<string, string> 타입 변경 - Zustand useShallow 마이그레이션 (zustand/react/shallow) - DataTable, ListPageTemplate 제네릭 타입 제약 추가 - 설정 관리 페이지 추가 (직급, 직책, 휴가정책, 근무일정, 권한) - HR 관리 페이지 추가 (급여, 휴가) - 단가관리 페이지 리팩토링 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
444 lines
10 KiB
TypeScript
444 lines
10 KiB
TypeScript
/**
|
|
* 품목기준관리 API 클라이언트
|
|
*
|
|
* Laravel 백엔드와 통신하는 API 함수들
|
|
* 동적 페이지 구성, 버전 관리, 멀티테넌시 지원
|
|
*/
|
|
|
|
import type {
|
|
PageConfig,
|
|
PageConfigRevision,
|
|
PageType,
|
|
DynamicFormData,
|
|
FetchPageConfigParams,
|
|
MasterDataApiResponse,
|
|
VersionComparisonResult,
|
|
} from '@/types/master-data';
|
|
|
|
// ===== 환경 변수 =====
|
|
|
|
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
|
|
|
|
// ===== 유틸리티 함수 =====
|
|
|
|
/**
|
|
* 인증 토큰 가져오기
|
|
*/
|
|
function getAuthToken(): string | null {
|
|
if (typeof window !== 'undefined') {
|
|
return localStorage.getItem('auth_token');
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Fetch 옵션 생성
|
|
*/
|
|
function createFetchOptions(options: RequestInit = {}): RequestInit {
|
|
const token = getAuthToken();
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
};
|
|
|
|
// Merge existing headers if they are a plain object
|
|
if (options.headers && typeof options.headers === 'object' && !Array.isArray(options.headers)) {
|
|
Object.assign(headers, options.headers);
|
|
}
|
|
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
|
|
return {
|
|
...options,
|
|
headers,
|
|
credentials: 'include',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* API 에러 처리
|
|
*/
|
|
async function handleApiResponse<T>(response: Response): Promise<T> {
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({
|
|
message: 'API 요청 실패',
|
|
}));
|
|
|
|
throw new Error(error.message || `HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data;
|
|
}
|
|
|
|
// ===== 페이지 구성 CRUD =====
|
|
|
|
/**
|
|
* 페이지 구성 목록 조회
|
|
*
|
|
* @example
|
|
* const configs = await fetchPageConfigs({ pageType: 'item-master' });
|
|
*/
|
|
export async function fetchPageConfigs(
|
|
params?: FetchPageConfigParams
|
|
): Promise<PageConfig[]> {
|
|
const queryParams = new URLSearchParams();
|
|
|
|
if (params) {
|
|
Object.entries(params).forEach(([key, value]) => {
|
|
if (value !== undefined && value !== null) {
|
|
queryParams.append(key, String(value));
|
|
}
|
|
});
|
|
}
|
|
|
|
const url = `${API_URL}/api/master-data/pages${queryParams.toString() ? `?${queryParams}` : ''}`;
|
|
|
|
const response = await fetch(url, createFetchOptions());
|
|
const data = await handleApiResponse<MasterDataApiResponse<PageConfig[]>>(response);
|
|
|
|
return data.data;
|
|
}
|
|
|
|
/**
|
|
* 특정 페이지 타입의 최신 구성 조회
|
|
*
|
|
* @param pageType - 페이지 타입
|
|
*
|
|
* @example
|
|
* const config = await fetchPageConfigByType('item-master');
|
|
*/
|
|
export async function fetchPageConfigByType(
|
|
pageType: PageType
|
|
): Promise<PageConfig | null> {
|
|
const response = await fetch(
|
|
`${API_URL}/api/master-data/pages/${pageType}`,
|
|
createFetchOptions()
|
|
);
|
|
|
|
if (response.status === 404) {
|
|
return null;
|
|
}
|
|
|
|
const data = await handleApiResponse<MasterDataApiResponse<PageConfig>>(response);
|
|
return data.data;
|
|
}
|
|
|
|
/**
|
|
* 페이지 구성 상세 조회 (특정 버전)
|
|
*
|
|
* @param id - 페이지 구성 ID
|
|
* @param version - 버전 번호 (선택사항, 기본: 최신)
|
|
*
|
|
* @example
|
|
* const config = await fetchPageConfigById('uuid', 2);
|
|
*/
|
|
export async function fetchPageConfigById(
|
|
id: string,
|
|
version?: number
|
|
): Promise<PageConfig> {
|
|
const url = version
|
|
? `${API_URL}/api/master-data/pages/${id}?version=${version}`
|
|
: `${API_URL}/api/master-data/pages/${id}`;
|
|
|
|
const response = await fetch(url, createFetchOptions());
|
|
const data = await handleApiResponse<MasterDataApiResponse<PageConfig>>(response);
|
|
|
|
return data.data;
|
|
}
|
|
|
|
/**
|
|
* 페이지 구성 생성
|
|
*
|
|
* @param configData - 페이지 구성 데이터
|
|
*
|
|
* @example
|
|
* const newConfig = await createPageConfig({
|
|
* pageName: '품목기준정보',
|
|
* pageType: 'item-master',
|
|
* sections: [...],
|
|
* isActive: true,
|
|
* });
|
|
*/
|
|
export async function createPageConfig(
|
|
configData: Omit<PageConfig, 'id' | 'version' | 'createdAt' | 'updatedAt' | 'tenantId'>
|
|
): Promise<PageConfig> {
|
|
const response = await fetch(
|
|
`${API_URL}/api/master-data/pages`,
|
|
createFetchOptions({
|
|
method: 'POST',
|
|
body: JSON.stringify(configData),
|
|
})
|
|
);
|
|
|
|
const data = await handleApiResponse<MasterDataApiResponse<PageConfig>>(response);
|
|
return data.data;
|
|
}
|
|
|
|
/**
|
|
* 페이지 구성 수정
|
|
*
|
|
* @param id - 페이지 구성 ID
|
|
* @param updates - 수정할 필드들
|
|
*
|
|
* @example
|
|
* const updatedConfig = await updatePageConfig('uuid', {
|
|
* sections: [...],
|
|
* });
|
|
*/
|
|
export async function updatePageConfig(
|
|
id: string,
|
|
updates: Partial<PageConfig>
|
|
): Promise<PageConfig> {
|
|
const response = await fetch(
|
|
`${API_URL}/api/master-data/pages/${id}`,
|
|
createFetchOptions({
|
|
method: 'PUT',
|
|
body: JSON.stringify(updates),
|
|
})
|
|
);
|
|
|
|
const data = await handleApiResponse<MasterDataApiResponse<PageConfig>>(response);
|
|
return data.data;
|
|
}
|
|
|
|
/**
|
|
* 페이지 구성 삭제
|
|
*
|
|
* @param id - 페이지 구성 ID
|
|
*/
|
|
export async function deletePageConfig(id: string): Promise<void> {
|
|
const response = await fetch(
|
|
`${API_URL}/api/master-data/pages/${id}`,
|
|
createFetchOptions({
|
|
method: 'DELETE',
|
|
})
|
|
);
|
|
|
|
await handleApiResponse<MasterDataApiResponse<null>>(response);
|
|
}
|
|
|
|
// ===== 버전 관리 =====
|
|
|
|
/**
|
|
* 페이지 구성 버전 이력 조회
|
|
*
|
|
* @param pageConfigId - 페이지 구성 ID
|
|
*
|
|
* @example
|
|
* const revisions = await fetchPageConfigRevisions('uuid');
|
|
*/
|
|
export async function fetchPageConfigRevisions(
|
|
pageConfigId: string
|
|
): Promise<PageConfigRevision[]> {
|
|
const response = await fetch(
|
|
`${API_URL}/api/master-data/pages/${pageConfigId}/revisions`,
|
|
createFetchOptions()
|
|
);
|
|
|
|
const data = await handleApiResponse<MasterDataApiResponse<PageConfigRevision[]>>(response);
|
|
return data.data;
|
|
}
|
|
|
|
/**
|
|
* 특정 버전 상세 조회
|
|
*
|
|
* @param pageConfigId - 페이지 구성 ID
|
|
* @param version - 버전 번호
|
|
*/
|
|
export async function fetchPageConfigRevisionByVersion(
|
|
pageConfigId: string,
|
|
version: number
|
|
): Promise<PageConfigRevision> {
|
|
const response = await fetch(
|
|
`${API_URL}/api/master-data/pages/${pageConfigId}/revisions/${version}`,
|
|
createFetchOptions()
|
|
);
|
|
|
|
const data = await handleApiResponse<MasterDataApiResponse<PageConfigRevision>>(response);
|
|
return data.data;
|
|
}
|
|
|
|
/**
|
|
* 버전 비교
|
|
*
|
|
* @param pageConfigId - 페이지 구성 ID
|
|
* @param version1 - 비교할 버전 1
|
|
* @param version2 - 비교할 버전 2
|
|
*
|
|
* @example
|
|
* const comparison = await comparePageConfigVersions('uuid', 1, 2);
|
|
*/
|
|
export async function comparePageConfigVersions(
|
|
pageConfigId: string,
|
|
version1: number,
|
|
version2: number
|
|
): Promise<VersionComparisonResult> {
|
|
const response = await fetch(
|
|
`${API_URL}/api/master-data/pages/${pageConfigId}/compare?v1=${version1}&v2=${version2}`,
|
|
createFetchOptions()
|
|
);
|
|
|
|
const data = await handleApiResponse<MasterDataApiResponse<VersionComparisonResult>>(response);
|
|
return data.data;
|
|
}
|
|
|
|
/**
|
|
* 버전 승인
|
|
*
|
|
* @param revisionId - 버전 ID
|
|
*
|
|
* @example
|
|
* await approvePageConfigRevision('revision-uuid');
|
|
*/
|
|
export async function approvePageConfigRevision(
|
|
revisionId: string
|
|
): Promise<PageConfigRevision> {
|
|
const response = await fetch(
|
|
`${API_URL}/api/master-data/revisions/${revisionId}/approve`,
|
|
createFetchOptions({
|
|
method: 'POST',
|
|
})
|
|
);
|
|
|
|
const data = await handleApiResponse<MasterDataApiResponse<PageConfigRevision>>(response);
|
|
return data.data;
|
|
}
|
|
|
|
/**
|
|
* 특정 버전으로 롤백
|
|
*
|
|
* @param pageConfigId - 페이지 구성 ID
|
|
* @param version - 롤백할 버전
|
|
*/
|
|
export async function rollbackPageConfig(
|
|
pageConfigId: string,
|
|
version: number
|
|
): Promise<PageConfig> {
|
|
const response = await fetch(
|
|
`${API_URL}/api/master-data/pages/${pageConfigId}/rollback`,
|
|
createFetchOptions({
|
|
method: 'POST',
|
|
body: JSON.stringify({ version }),
|
|
})
|
|
);
|
|
|
|
const data = await handleApiResponse<MasterDataApiResponse<PageConfig>>(response);
|
|
return data.data;
|
|
}
|
|
|
|
// ===== 동적 폼 데이터 =====
|
|
|
|
/**
|
|
* 동적 폼 데이터 조회
|
|
*
|
|
* @param pageType - 페이지 타입
|
|
* @param id - 데이터 ID (선택사항)
|
|
*/
|
|
export async function fetchFormData(
|
|
pageType: PageType,
|
|
id?: string
|
|
): Promise<DynamicFormData | DynamicFormData[]> {
|
|
const url = id
|
|
? `${API_URL}/api/master-data/form-data/${pageType}/${id}`
|
|
: `${API_URL}/api/master-data/form-data/${pageType}`;
|
|
|
|
const response = await fetch(url, createFetchOptions());
|
|
const data = await handleApiResponse<MasterDataApiResponse<DynamicFormData | DynamicFormData[]>>(response);
|
|
|
|
return data.data;
|
|
}
|
|
|
|
/**
|
|
* 동적 폼 데이터 저장
|
|
*
|
|
* @param formData - 폼 데이터
|
|
*/
|
|
export async function saveFormData(
|
|
formData: DynamicFormData
|
|
): Promise<DynamicFormData> {
|
|
const response = await fetch(
|
|
`${API_URL}/api/master-data/form-data`,
|
|
createFetchOptions({
|
|
method: 'POST',
|
|
body: JSON.stringify(formData),
|
|
})
|
|
);
|
|
|
|
const data = await handleApiResponse<MasterDataApiResponse<DynamicFormData>>(response);
|
|
return data.data;
|
|
}
|
|
|
|
/**
|
|
* 동적 폼 데이터 수정
|
|
*
|
|
* @param id - 데이터 ID
|
|
* @param updates - 수정할 필드들
|
|
*/
|
|
export async function updateFormData(
|
|
id: string,
|
|
updates: Partial<DynamicFormData>
|
|
): Promise<DynamicFormData> {
|
|
const response = await fetch(
|
|
`${API_URL}/api/master-data/form-data/${id}`,
|
|
createFetchOptions({
|
|
method: 'PUT',
|
|
body: JSON.stringify(updates),
|
|
})
|
|
);
|
|
|
|
const data = await handleApiResponse<MasterDataApiResponse<DynamicFormData>>(response);
|
|
return data.data;
|
|
}
|
|
|
|
/**
|
|
* 동적 폼 데이터 삭제
|
|
*
|
|
* @param id - 데이터 ID
|
|
*/
|
|
export async function deleteFormData(id: string): Promise<void> {
|
|
const response = await fetch(
|
|
`${API_URL}/api/master-data/form-data/${id}`,
|
|
createFetchOptions({
|
|
method: 'DELETE',
|
|
})
|
|
);
|
|
|
|
await handleApiResponse<MasterDataApiResponse<null>>(response);
|
|
}
|
|
|
|
// ===== 캐싱 =====
|
|
|
|
/**
|
|
* 페이지 구성 캐시 무효화
|
|
*
|
|
* @param pageType - 페이지 타입 (선택사항, 전체 무효화 가능)
|
|
*/
|
|
export async function invalidatePageConfigCache(
|
|
pageType?: PageType
|
|
): Promise<void> {
|
|
const url = pageType
|
|
? `${API_URL}/api/master-data/cache/invalidate/${pageType}`
|
|
: `${API_URL}/api/master-data/cache/invalidate`;
|
|
|
|
const response = await fetch(
|
|
url,
|
|
createFetchOptions({
|
|
method: 'POST',
|
|
})
|
|
);
|
|
|
|
await handleApiResponse<MasterDataApiResponse<null>>(response);
|
|
}
|
|
|
|
/**
|
|
* Next.js revalidation 트리거
|
|
*/
|
|
export async function revalidateMasterData(): Promise<void> {
|
|
if (typeof window === 'undefined') {
|
|
const { revalidatePath } = await import('next/cache');
|
|
revalidatePath('/master-data');
|
|
}
|
|
}
|