feat: fetchWrapper 마이그레이션 및 토큰 리프레시 캐싱 구현

- 40+ actions.ts 파일을 fetchWrapper 패턴으로 마이그레이션
- 토큰 리프레시 캐싱 로직 추가 (refresh-token.ts)
- ApiErrorContext 추가로 전역 에러 처리 개선
- HR EmployeeForm 컴포넌트 개선
- 참조함(ReferenceBox) 기능 수정
- juil 테스트 URL 페이지 추가
- claudedocs 문서 업데이트

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2025-12-30 17:00:18 +09:00
parent 0e5307f7a3
commit d38b1242d7
82 changed files with 7434 additions and 4775 deletions

View File

@@ -1,24 +1,8 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { Process, ProcessFormData, ClassificationRule } from '@/types/process';
// ============================================================================
// API 헤더 생성
// ============================================================================
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ============================================================================
// API 타입 정의
// ============================================================================
@@ -144,9 +128,8 @@ export async function getProcessList(params?: {
q?: string;
status?: string;
process_type?: string;
}): Promise<{ success: boolean; data?: { items: Process[]; total: number; page: number; totalPages: number }; error?: string }> {
}): Promise<{ success: boolean; data?: { items: Process[]; total: number; page: number; totalPages: number }; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
@@ -155,11 +138,19 @@ export async function getProcessList(params?: {
if (params?.status) searchParams.set('status', params.status);
if (params?.process_type) searchParams.set('process_type', params.process_type);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.API_URL}/v1/processes?${searchParams.toString()}`,
{ method: 'GET', headers, cache: 'no-store' }
{ method: 'GET', cache: 'no-store' }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '목록 조회에 실패했습니다.' };
}
const result: ApiResponse<PaginatedResponse<ApiProcess>> = await response.json();
if (!response.ok || !result.success) {
@@ -184,16 +175,21 @@ export async function getProcessList(params?: {
/**
* 공정 상세 조회
*/
export async function getProcessById(id: string): Promise<{ success: boolean; data?: Process; error?: string }> {
export async function getProcessById(id: string): Promise<{ success: boolean; data?: Process; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${process.env.API_URL}/v1/processes/${id}`, {
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/processes/${id}`, {
method: 'GET',
headers,
cache: 'no-store',
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '조회에 실패했습니다.' };
}
const result: ApiResponse<ApiProcess> = await response.json();
if (!response.ok || !result.success) {
@@ -210,17 +206,23 @@ export async function getProcessById(id: string): Promise<{ success: boolean; da
/**
* 공정 생성
*/
export async function createProcess(data: ProcessFormData): Promise<{ success: boolean; data?: Process; error?: string }> {
export async function createProcess(data: ProcessFormData): Promise<{ success: boolean; data?: Process; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
const response = await fetch(`${process.env.API_URL}/v1/processes`, {
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/processes`, {
method: 'POST',
headers,
body: JSON.stringify(apiData),
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '등록에 실패했습니다.' };
}
const result: ApiResponse<ApiProcess> = await response.json();
if (!response.ok || !result.success) {
@@ -237,17 +239,23 @@ export async function createProcess(data: ProcessFormData): Promise<{ success: b
/**
* 공정 수정
*/
export async function updateProcess(id: string, data: ProcessFormData): Promise<{ success: boolean; data?: Process; error?: string }> {
export async function updateProcess(id: string, data: ProcessFormData): Promise<{ success: boolean; data?: Process; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
const response = await fetch(`${process.env.API_URL}/v1/processes/${id}`, {
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/processes/${id}`, {
method: 'PUT',
headers,
body: JSON.stringify(apiData),
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '수정에 실패했습니다.' };
}
const result: ApiResponse<ApiProcess> = await response.json();
if (!response.ok || !result.success) {
@@ -264,15 +272,20 @@ export async function updateProcess(id: string, data: ProcessFormData): Promise<
/**
* 공정 삭제
*/
export async function deleteProcess(id: string): Promise<{ success: boolean; error?: string }> {
export async function deleteProcess(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${process.env.API_URL}/v1/processes/${id}`, {
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/processes/${id}`, {
method: 'DELETE',
headers,
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '삭제에 실패했습니다.' };
}
const result: ApiResponse<null> = await response.json();
if (!response.ok || !result.success) {
@@ -289,16 +302,21 @@ export async function deleteProcess(id: string): Promise<{ success: boolean; err
/**
* 공정 일괄 삭제
*/
export async function deleteProcesses(ids: string[]): Promise<{ success: boolean; deletedCount?: number; error?: string }> {
export async function deleteProcesses(ids: string[]): Promise<{ success: boolean; deletedCount?: number; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${process.env.API_URL}/v1/processes`, {
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/processes`, {
method: 'DELETE',
headers,
body: JSON.stringify({ ids: ids.map((id) => parseInt(id, 10)) }),
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '일괄 삭제에 실패했습니다.' };
}
const result: ApiResponse<{ deleted_count: number }> = await response.json();
if (!response.ok || !result.success) {
@@ -315,15 +333,20 @@ export async function deleteProcesses(ids: string[]): Promise<{ success: boolean
/**
* 공정 상태 토글
*/
export async function toggleProcessActive(id: string): Promise<{ success: boolean; data?: Process; error?: string }> {
export async function toggleProcessActive(id: string): Promise<{ success: boolean; data?: Process; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${process.env.API_URL}/v1/processes/${id}/toggle`, {
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/processes/${id}/toggle`, {
method: 'PATCH',
headers,
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '상태 변경에 실패했습니다.' };
}
const result: ApiResponse<ApiProcess> = await response.json();
if (!response.ok || !result.success) {
@@ -344,16 +367,22 @@ export async function getProcessOptions(): Promise<{
success: boolean;
data?: Array<{ id: string; processCode: string; processName: string; processType: string; department: string }>;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${process.env.API_URL}/v1/processes/options`, {
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/processes/options`, {
method: 'GET',
headers,
cache: 'no-store',
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '옵션 조회에 실패했습니다.' };
}
const result: ApiResponse<Array<{ id: number; process_code: string; process_name: string; process_type: string; department: string }>> =
await response.json();
@@ -384,16 +413,22 @@ export async function getProcessStats(): Promise<{
success: boolean;
data?: { total: number; active: number; inactive: number; byType: Record<string, number> };
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${process.env.API_URL}/v1/processes/stats`, {
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/processes/stats`, {
method: 'GET',
headers,
cache: 'no-store',
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '통계 조회에 실패했습니다.' };
}
const result: ApiResponse<{ total: number; active: number; inactive: number; by_type: Record<string, number> }> = await response.json();
if (!response.ok || !result.success) {