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:
@@ -12,7 +12,7 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import type { Board, BoardApiData, BoardFormData } from './types';
|
||||
|
||||
// API 응답 타입
|
||||
@@ -22,21 +22,6 @@ interface ApiResponse<T> {
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 데이터 → 프론트엔드 타입 변환
|
||||
*/
|
||||
@@ -89,10 +74,8 @@ function transformFrontendToApi(data: BoardFormData & { boardCode?: string; desc
|
||||
export async function getBoards(filters?: {
|
||||
board_type?: string;
|
||||
search?: string;
|
||||
}): Promise<{ success: boolean; data?: Board[]; error?: string }> {
|
||||
}): Promise<{ success: boolean; data?: Board[]; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.board_type) params.append('board_type', filters.board_type);
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
@@ -101,14 +84,16 @@ export async function getBoards(filters?: {
|
||||
// 테넌트 게시판만 조회 (시스템 게시판은 mng에서 관리)
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/tenant${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[BoardActions] GET boards error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시판 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
@@ -120,7 +105,7 @@ export async function getBoards(filters?: {
|
||||
return { success: false, error: '서버 응답 형식 오류입니다.' };
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '게시판 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
@@ -143,10 +128,8 @@ export async function getBoards(filters?: {
|
||||
export async function getTenantBoards(filters?: {
|
||||
board_type?: string;
|
||||
search?: string;
|
||||
}): Promise<{ success: boolean; data?: Board[]; error?: string }> {
|
||||
}): Promise<{ success: boolean; data?: Board[]; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.board_type) params.append('board_type', filters.board_type);
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
@@ -154,20 +137,28 @@ export async function getTenantBoards(filters?: {
|
||||
const queryString = params.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/tenant${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[BoardActions] GET tenant boards error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '테넌트 게시판 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<BoardApiData[]> = await response.json();
|
||||
let result: ApiResponse<BoardApiData[]>;
|
||||
try {
|
||||
result = await response.json();
|
||||
} catch {
|
||||
console.error('[BoardActions] JSON parse error');
|
||||
return { success: false, error: '서버 응답 형식 오류입니다.' };
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '테넌트 게시판 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
@@ -182,27 +173,32 @@ export async function getTenantBoards(filters?: {
|
||||
/**
|
||||
* 게시판 상세 조회 (코드 기반)
|
||||
*/
|
||||
export async function getBoardByCode(code: string): Promise<{ success: boolean; data?: Board; error?: string }> {
|
||||
export async function getBoardByCode(code: string): Promise<{ success: boolean; data?: Board; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${code}`;
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${code}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[BoardActions] GET board error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시판 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<BoardApiData> = await response.json();
|
||||
let result: ApiResponse<BoardApiData>;
|
||||
try {
|
||||
result = await response.json();
|
||||
} catch {
|
||||
console.error('[BoardActions] JSON parse error');
|
||||
return { success: false, error: '서버 응답 형식 오류입니다.' };
|
||||
}
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
if (!response.ok || !result.success || !result.data) {
|
||||
return { success: false, error: result.message || '게시판을 찾을 수 없습니다.' };
|
||||
}
|
||||
|
||||
@@ -216,27 +212,32 @@ export async function getBoardByCode(code: string): Promise<{ success: boolean;
|
||||
/**
|
||||
* 게시판 상세 조회 (ID 기반)
|
||||
*/
|
||||
export async function getBoardById(id: string): Promise<{ success: boolean; data?: Board; error?: string }> {
|
||||
export async function getBoardById(id: string): Promise<{ success: boolean; data?: Board; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${id}`;
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${id}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[BoardActions] GET board by id error:', response.status);
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시판 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<BoardApiData> = await response.json();
|
||||
let result: ApiResponse<BoardApiData>;
|
||||
try {
|
||||
result = await response.json();
|
||||
} catch {
|
||||
console.error('[BoardActions] JSON parse error');
|
||||
return { success: false, error: '서버 응답 형식 오류입니다.' };
|
||||
}
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
if (!response.ok || !result.success || !result.data) {
|
||||
return { success: false, error: result.message || '게시판을 찾을 수 없습니다.' };
|
||||
}
|
||||
|
||||
@@ -252,21 +253,31 @@ export async function getBoardById(id: string): Promise<{ success: boolean; data
|
||||
*/
|
||||
export async function createBoard(
|
||||
data: BoardFormData & { boardCode: string; description?: string }
|
||||
): Promise<{ success: boolean; data?: Board; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: Board; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards`;
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시판 생성에 실패했습니다.' };
|
||||
}
|
||||
|
||||
let result: ApiResponse<BoardApiData>;
|
||||
try {
|
||||
result = await response.json();
|
||||
} catch {
|
||||
console.error('[BoardActions] JSON parse error');
|
||||
return { success: false, error: '서버 응답 형식 오류입니다.' };
|
||||
}
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
@@ -291,21 +302,31 @@ export async function createBoard(
|
||||
export async function updateBoard(
|
||||
id: string,
|
||||
data: BoardFormData & { boardCode?: string; description?: string }
|
||||
): Promise<{ success: boolean; data?: Board; error?: string }> {
|
||||
): Promise<{ success: boolean; data?: Board; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const apiData = transformFrontendToApi(data, true); // isUpdate=true
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${id}`;
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(apiData),
|
||||
}
|
||||
);
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(apiData),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시판 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
let result: ApiResponse<BoardApiData>;
|
||||
try {
|
||||
result = await response.json();
|
||||
} catch {
|
||||
console.error('[BoardActions] JSON parse error');
|
||||
return { success: false, error: '서버 응답 형식 오류입니다.' };
|
||||
}
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
@@ -327,19 +348,29 @@ export async function updateBoard(
|
||||
/**
|
||||
* 게시판 삭제
|
||||
*/
|
||||
export async function deleteBoard(id: string): Promise<{ success: boolean; error?: string }> {
|
||||
export async function deleteBoard(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const headers = await getApiHeaders();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${id}`;
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${id}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
}
|
||||
);
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시판 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
let result: { success: boolean; message?: string };
|
||||
try {
|
||||
result = await response.json();
|
||||
} catch {
|
||||
console.error('[BoardActions] JSON parse error');
|
||||
return { success: false, error: '서버 응답 형식 오류입니다.' };
|
||||
}
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
@@ -358,10 +389,15 @@ export async function deleteBoard(id: string): Promise<{ success: boolean; error
|
||||
/**
|
||||
* 게시판 일괄 삭제
|
||||
*/
|
||||
export async function deleteBoardsBulk(ids: string[]): Promise<{ success: boolean; error?: string }> {
|
||||
export async function deleteBoardsBulk(ids: string[]): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const results = await Promise.all(ids.map(id => deleteBoard(id)));
|
||||
const failed = results.filter(r => !r.success);
|
||||
const hasAuthError = results.some(r => r.__authError);
|
||||
|
||||
if (hasAuthError) {
|
||||
return { success: false, error: '인증이 만료되었습니다.', __authError: true };
|
||||
}
|
||||
|
||||
if (failed.length > 0) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user