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

@@ -3,6 +3,7 @@
/**
* 검사 관리 Server Actions
* API 연동 완료 (2025-12-26)
* fetch-wrapper 마이그레이션 완료 (2025-12-30)
*
* API Endpoints:
* - GET /api/v1/inspections - 목록 조회
@@ -14,7 +15,7 @@
* - PATCH /api/v1/inspections/{id}/complete - 검사 완료 처리
*/
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type {
Inspection,
InspectionStats,
@@ -23,19 +24,6 @@ import type {
InspectionItem,
} from './types';
// ===== 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 타입 =====
interface InspectionApiItem {
id: number;
@@ -190,9 +178,9 @@ export async function getInspections(params?: {
data: Inspection[];
pagination: PaginationMeta;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
@@ -212,12 +200,29 @@ export async function getInspections(params?: {
console.log('[InspectionActions] GET inspections:', url);
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (error) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: '검사 목록 조회에 실패했습니다.',
};
}
if (!response.ok) {
console.warn('[InspectionActions] GET inspections error:', response.status);
return {
@@ -277,9 +282,9 @@ export async function getInspectionStats(params?: {
success: boolean;
data?: InspectionStats;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.dateFrom) searchParams.set('date_from', params.dateFrom);
@@ -293,12 +298,25 @@ export async function getInspectionStats(params?: {
console.log('[InspectionActions] GET stats:', url);
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '통계 조회에 실패했습니다.',
};
}
if (!response.ok) {
console.warn('[InspectionActions] GET stats error:', response.status);
return {
@@ -341,19 +359,32 @@ export async function getInspectionById(id: string): Promise<{
success: boolean;
data?: Inspection;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/inspections/${id}`;
console.log('[InspectionActions] GET inspection:', url);
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '검사 조회에 실패했습니다.',
};
}
if (!response.ok) {
console.error('[InspectionActions] GET inspection error:', response.status);
return {
@@ -399,10 +430,9 @@ export async function createInspection(data: {
success: boolean;
data?: Inspection;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const apiData: Record<string, unknown> = {
inspection_type: data.inspectionType,
lot_no: data.lotNo,
@@ -425,15 +455,29 @@ export async function createInspection(data: {
console.log('[InspectionActions] POST inspection request:', apiData);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/inspections`,
{
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 = await response.json();
console.log('[InspectionActions] POST inspection response:', result);
@@ -470,10 +514,9 @@ export async function updateInspection(
success: boolean;
data?: Inspection;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const apiData: Record<string, unknown> = {};
if (data.items) {
@@ -503,15 +546,29 @@ export async function updateInspection(
console.log('[InspectionActions] PUT inspection request:', apiData);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/inspections/${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 = await response.json();
console.log('[InspectionActions] PUT inspection response:', result);
@@ -539,18 +596,31 @@ export async function updateInspection(
export async function deleteInspection(id: string): Promise<{
success: boolean;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/inspections/${id}`,
{
method: 'DELETE',
headers,
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '검사 삭제에 실패했습니다.',
};
}
const result = await response.json();
console.log('[InspectionActions] DELETE inspection response:', result);
@@ -582,10 +652,9 @@ export async function completeInspection(
success: boolean;
data?: Inspection;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const apiData = {
result: data.result === '합격' ? 'pass' : 'fail',
opinion: data.opinion,
@@ -593,15 +662,29 @@ export async function completeInspection(
console.log('[InspectionActions] PATCH complete request:', apiData);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/inspections/${id}/complete`,
{
method: 'PATCH',
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 = await response.json();
console.log('[InspectionActions] PATCH complete response:', result);
@@ -623,4 +706,4 @@ export async function completeInspection(
error: '서버 오류가 발생했습니다.',
};
}
}
}