/** * 게시판 관리 서버 액션 * * 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 { 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 { // extra_settings 구성 const extraSettings: Record = { 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 = { 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; 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; 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; 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; 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; 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; 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: '서버 오류가 발생했습니다.' }; } }