- 전체 모듈 actions.ts redirect 에러 핸들링 추가 - CEODashboard DetailModal 추가 - MonthlyExpenseSection 개선 - fetch-wrapper redirect 에러 처리 - redirect-error 유틸 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
440 lines
14 KiB
TypeScript
440 lines
14 KiB
TypeScript
/**
|
|
* 게시판 관리 서버 액션
|
|
*
|
|
* API Endpoints:
|
|
* - GET /api/v1/boards - 접근 가능한 게시판 목록
|
|
* - GET /api/v1/boards/tenant - 테넌트 게시판만
|
|
* - GET /api/v1/boards/{code} - 게시판 상세 (코드 기반)
|
|
* - POST /api/v1/boards - 테넌트 게시판 생성
|
|
* - PUT /api/v1/boards/{id} - 테넌트 게시판 수정
|
|
* - DELETE /api/v1/boards/{id} - 테넌트 게시판 삭제
|
|
*/
|
|
|
|
'use server';
|
|
|
|
|
|
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
|
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
|
import type { Board, BoardApiData, BoardFormData } from './types';
|
|
|
|
// API 응답 타입
|
|
interface ApiResponse<T> {
|
|
success: boolean;
|
|
data: T;
|
|
message: string;
|
|
}
|
|
|
|
/**
|
|
* API 데이터 → 프론트엔드 타입 변환
|
|
*/
|
|
function transformApiToFrontend(apiData: BoardApiData): Board {
|
|
const extraSettings = apiData.extra_settings || {};
|
|
|
|
// permissions 추출 (read 권한 기준으로 사용)
|
|
const permissions = extraSettings.permissions?.read || [];
|
|
|
|
return {
|
|
id: String(apiData.id),
|
|
boardCode: apiData.board_code,
|
|
boardType: apiData.board_type || undefined,
|
|
target: extraSettings.target || 'all',
|
|
targetId: extraSettings.target_id,
|
|
targetName: extraSettings.target_name,
|
|
permissions: permissions.length > 0 ? permissions : undefined,
|
|
boardName: apiData.name,
|
|
description: apiData.description || undefined,
|
|
status: apiData.is_active ? 'active' : 'inactive',
|
|
isSystem: apiData.is_system,
|
|
authorId: apiData.created_by ? String(apiData.created_by) : '',
|
|
authorName: apiData.creator?.name || '시스템',
|
|
createdAt: apiData.created_at,
|
|
updatedAt: apiData.updated_at,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 프론트엔드 데이터 → API 요청 형식 변환
|
|
*/
|
|
function transformFrontendToApi(data: BoardFormData & { boardCode?: string; description?: string }, isUpdate = false): Record<string, unknown> {
|
|
// extra_settings 구성
|
|
const extraSettings: Record<string, unknown> = {
|
|
target: data.target,
|
|
target_name: data.target === 'department' ? data.targetName : null,
|
|
};
|
|
|
|
// 권한 대상인 경우 permissions 추가
|
|
if (data.target === 'permission' && data.permissions && data.permissions.length > 0) {
|
|
extraSettings.permissions = {
|
|
read: data.permissions,
|
|
write: data.permissions,
|
|
manage: data.permissions,
|
|
};
|
|
}
|
|
|
|
const result: Record<string, unknown> = {
|
|
name: data.boardName,
|
|
description: data.description || null,
|
|
is_active: data.status === 'active',
|
|
extra_settings: extraSettings,
|
|
};
|
|
|
|
// 생성 시에만 board_code 전송 (수정 시에는 코드 변경 불가)
|
|
if (!isUpdate && data.boardCode) {
|
|
result.board_code = data.boardCode;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* 게시판 목록 조회 (테넌트 게시판만 - 시스템 게시판 제외)
|
|
*/
|
|
export async function getBoards(filters?: {
|
|
board_type?: string;
|
|
search?: string;
|
|
}): Promise<{ success: boolean; data?: Board[]; error?: string; __authError?: boolean }> {
|
|
try {
|
|
const params = new URLSearchParams();
|
|
if (filters?.board_type) params.append('board_type', filters.board_type);
|
|
if (filters?.search) params.append('search', filters.search);
|
|
|
|
const queryString = params.toString();
|
|
// 테넌트 게시판만 조회 (시스템 게시판은 mng에서 관리)
|
|
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/tenant${queryString ? `?${queryString}` : ''}`;
|
|
|
|
const { response, error } = await serverFetch(url, {
|
|
method: 'GET',
|
|
cache: 'no-store',
|
|
});
|
|
|
|
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 { success: false, error: result.message || '게시판 목록 조회에 실패했습니다.' };
|
|
}
|
|
|
|
// data가 없거나 배열이 아닌 경우 빈 배열 반환
|
|
if (!result.data || !Array.isArray(result.data)) {
|
|
return { success: true, data: [] };
|
|
}
|
|
|
|
const boards = result.data.map(transformApiToFrontend);
|
|
return { success: true, data: boards };
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[BoardActions] getBoards error:', error);
|
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 테넌트 게시판만 조회
|
|
*/
|
|
export async function getTenantBoards(filters?: {
|
|
board_type?: string;
|
|
search?: string;
|
|
}): Promise<{ success: boolean; data?: Board[]; error?: string; __authError?: boolean }> {
|
|
try {
|
|
const params = new URLSearchParams();
|
|
if (filters?.board_type) params.append('board_type', filters.board_type);
|
|
if (filters?.search) params.append('search', filters.search);
|
|
|
|
const queryString = params.toString();
|
|
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/tenant${queryString ? `?${queryString}` : ''}`;
|
|
|
|
const { response, error } = await serverFetch(url, {
|
|
method: 'GET',
|
|
cache: 'no-store',
|
|
});
|
|
|
|
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 { success: false, error: result.message || '테넌트 게시판 목록 조회에 실패했습니다.' };
|
|
}
|
|
|
|
const boards = result.data.map(transformApiToFrontend);
|
|
return { success: true, data: boards };
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[BoardActions] getTenantBoards error:', error);
|
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 게시판 상세 조회 (코드 기반)
|
|
*/
|
|
export async function getBoardByCode(code: string): Promise<{ success: boolean; data?: Board; error?: string; __authError?: boolean }> {
|
|
try {
|
|
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${code}`;
|
|
|
|
const { response, error } = await serverFetch(url, {
|
|
method: 'GET',
|
|
cache: 'no-store',
|
|
});
|
|
|
|
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 || !result.data) {
|
|
return { success: false, error: result.message || '게시판을 찾을 수 없습니다.' };
|
|
}
|
|
|
|
return { success: true, data: transformApiToFrontend(result.data) };
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[BoardActions] getBoardByCode error:', error);
|
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 게시판 상세 조회 (ID 기반)
|
|
*/
|
|
export async function getBoardById(id: string): Promise<{ success: boolean; data?: Board; error?: string; __authError?: boolean }> {
|
|
try {
|
|
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${id}`;
|
|
|
|
const { response, error } = await serverFetch(url, {
|
|
method: 'GET',
|
|
cache: 'no-store',
|
|
});
|
|
|
|
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 || !result.data) {
|
|
return { success: false, error: result.message || '게시판을 찾을 수 없습니다.' };
|
|
}
|
|
|
|
return { success: true, data: transformApiToFrontend(result.data) };
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[BoardActions] getBoardById error:', error);
|
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 게시판 생성
|
|
*/
|
|
export async function createBoard(
|
|
data: BoardFormData & { boardCode: string; description?: string }
|
|
): Promise<{ success: boolean; data?: Board; error?: string; __authError?: boolean }> {
|
|
try {
|
|
const apiData = transformFrontendToApi(data);
|
|
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards`;
|
|
|
|
const { response, error } = await serverFetch(url, {
|
|
method: 'POST',
|
|
body: JSON.stringify(apiData),
|
|
});
|
|
|
|
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 {
|
|
success: false,
|
|
error: result.message || '게시판 생성에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: transformApiToFrontend(result.data),
|
|
};
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[BoardActions] createBoard error:', error);
|
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 게시판 수정
|
|
*/
|
|
export async function updateBoard(
|
|
id: string,
|
|
data: BoardFormData & { boardCode?: string; description?: string }
|
|
): Promise<{ success: boolean; data?: Board; error?: string; __authError?: boolean }> {
|
|
try {
|
|
const apiData = transformFrontendToApi(data, true); // isUpdate=true
|
|
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${id}`;
|
|
|
|
const { response, error } = await serverFetch(url, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(apiData),
|
|
});
|
|
|
|
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 {
|
|
success: false,
|
|
error: result.message || '게시판 수정에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: transformApiToFrontend(result.data),
|
|
};
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[BoardActions] updateBoard error:', error);
|
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 게시판 삭제
|
|
*/
|
|
export async function deleteBoard(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
|
try {
|
|
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${id}`;
|
|
|
|
const { response, error } = await serverFetch(url, {
|
|
method: 'DELETE',
|
|
});
|
|
|
|
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 {
|
|
success: false,
|
|
error: result.message || '게시판 삭제에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[BoardActions] deleteBoard error:', error);
|
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 게시판 일괄 삭제
|
|
*/
|
|
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 {
|
|
success: false,
|
|
error: `${failed.length}개의 게시판 삭제에 실패했습니다.`,
|
|
};
|
|
}
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[BoardActions] deleteBoardsBulk error:', error);
|
|
return { success: false, error: '서버 오류가 발생했습니다.' };
|
|
}
|
|
} |