refactor(WEB): Server Action 공통화 및 보안 강화
- executeServerAction 공통 유틸 도입으로 actions.ts 대폭 간소화 (50+개 파일) - sanitize 유틸 추가 (XSS 방지) - middleware CSP 헤더 추가 및 Open Redirect 방지 - 프록시 라우트 로깅 개발환경 한정으로 변경 - 프로덕션 불필요 console.log 제거 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ import { deletePost } from '../actions';
|
||||
import type { Post, Comment } from '../types';
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { useMenuStore } from '@/store/menuStore';
|
||||
import { sanitizeHTML } from '@/lib/sanitize';
|
||||
|
||||
interface BoardDetailProps {
|
||||
post: Post;
|
||||
@@ -159,7 +160,7 @@ export function BoardDetail({ post, comments: initialComments, currentUserId }:
|
||||
{/* 내용 (HTML 렌더링) */}
|
||||
<div
|
||||
className="prose prose-sm max-w-none"
|
||||
dangerouslySetInnerHTML={{ __html: post.content }}
|
||||
dangerouslySetInnerHTML={{ __html: sanitizeHTML(post.content) }}
|
||||
/>
|
||||
|
||||
{/* 첨부파일 */}
|
||||
|
||||
@@ -1,38 +1,16 @@
|
||||
/**
|
||||
* 게시판 관리 서버 액션
|
||||
*
|
||||
* 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 { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
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;
|
||||
}
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
@@ -52,389 +30,117 @@ function transformApiToFrontend(apiData: BoardApiData): Board {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 프론트엔드 데이터 → 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,
|
||||
};
|
||||
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,
|
||||
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;
|
||||
}
|
||||
|
||||
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: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
board_type?: string; search?: string;
|
||||
}): Promise<ActionResult<Board[]>> {
|
||||
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();
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards/tenant${queryString ? `?${queryString}` : ''}`,
|
||||
transform: (data: BoardApiData[]) => (Array.isArray(data) ? data : []).map(transformApiToFrontend),
|
||||
errorMessage: '게시판 목록 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 게시판만 조회
|
||||
*/
|
||||
// ===== 테넌트 게시판만 조회 =====
|
||||
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: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
board_type?: string; search?: string;
|
||||
}): Promise<ActionResult<Board[]>> {
|
||||
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();
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards/tenant${queryString ? `?${queryString}` : ''}`,
|
||||
transform: (data: BoardApiData[]) => data.map(transformApiToFrontend),
|
||||
errorMessage: '테넌트 게시판 목록 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시판 상세 조회 (코드 기반)
|
||||
*/
|
||||
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: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
// ===== 게시판 상세 조회 (코드 기반) =====
|
||||
export async function getBoardByCode(code: string): Promise<ActionResult<Board>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards/${code}`,
|
||||
transform: (data: BoardApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '게시판을 찾을 수 없습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시판 상세 조회 (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: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
// ===== 게시판 상세 조회 (ID 기반) =====
|
||||
export async function getBoardById(id: string): Promise<ActionResult<Board>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards/${id}`,
|
||||
transform: (data: BoardApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '게시판을 찾을 수 없습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시판 생성
|
||||
*/
|
||||
// ===== 게시판 생성 =====
|
||||
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: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
): Promise<ActionResult<Board>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards`,
|
||||
method: 'POST',
|
||||
body: transformFrontendToApi(data),
|
||||
transform: (d: BoardApiData) => transformApiToFrontend(d),
|
||||
errorMessage: '게시판 생성에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시판 수정
|
||||
*/
|
||||
// ===== 게시판 수정 =====
|
||||
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: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
): Promise<ActionResult<Board>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards/${id}`,
|
||||
method: 'PUT',
|
||||
body: transformFrontendToApi(data, true),
|
||||
transform: (d: BoardApiData) => transformApiToFrontend(d),
|
||||
errorMessage: '게시판 수정에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시판 삭제
|
||||
*/
|
||||
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 deleteBoard(id: string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards/${id}`,
|
||||
method: 'DELETE',
|
||||
errorMessage: '게시판 삭제에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시판 일괄 삭제
|
||||
*/
|
||||
export async function deleteBoardsBulk(ids: string[]): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
// ===== 게시판 일괄 삭제 =====
|
||||
export async function deleteBoardsBulk(ids: string[]): Promise<ActionResult> {
|
||||
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}개의 게시판 삭제에 실패했습니다.`,
|
||||
};
|
||||
}
|
||||
|
||||
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: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
}
|
||||
@@ -1,382 +1,117 @@
|
||||
/**
|
||||
* 동적 게시판 Server Actions
|
||||
* 일반 게시판 게시글 API 호출 (/api/v1/boards/{code}/posts)
|
||||
*/
|
||||
|
||||
'use server';
|
||||
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import type {
|
||||
PostApiData,
|
||||
PostPaginationResponse,
|
||||
ApiResponse,
|
||||
PostFilters,
|
||||
CommentApiData,
|
||||
CommentsApiResponse,
|
||||
} from '@/components/customer-center/shared/types';
|
||||
|
||||
/**
|
||||
* 게시글 목록 조회
|
||||
*/
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== 게시글 API =====
|
||||
|
||||
export async function getDynamicBoardPosts(
|
||||
boardCode: string,
|
||||
filters?: PostFilters
|
||||
): Promise<{ success: boolean; data?: PostPaginationResponse; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
if (filters?.is_notice !== undefined) params.append('is_notice', String(filters.is_notice));
|
||||
if (filters?.status) params.append('status', filters.status);
|
||||
if (filters?.per_page) params.append('per_page', String(filters.per_page));
|
||||
if (filters?.page) params.append('page', String(filters.page));
|
||||
|
||||
const queryString = params.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts${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<PostPaginationResponse>;
|
||||
try {
|
||||
result = await response.json();
|
||||
} catch {
|
||||
console.error('[DynamicBoardActions] JSON parse error');
|
||||
return { success: false, error: '서버 응답 형식 오류입니다.' };
|
||||
}
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '게시글 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[DynamicBoardActions] getDynamicBoardPosts error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
boardCode: string, filters?: PostFilters
|
||||
): Promise<ActionResult<PostPaginationResponse>> {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
if (filters?.is_notice !== undefined) params.append('is_notice', String(filters.is_notice));
|
||||
if (filters?.status) params.append('status', filters.status);
|
||||
if (filters?.per_page) params.append('per_page', String(filters.per_page));
|
||||
if (filters?.page) params.append('page', String(filters.page));
|
||||
const queryString = params.toString();
|
||||
return executeServerAction<PostPaginationResponse>({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts${queryString ? `?${queryString}` : ''}`,
|
||||
errorMessage: '게시글 목록 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 상세 조회
|
||||
*/
|
||||
export async function getDynamicBoardPost(
|
||||
boardCode: string,
|
||||
postId: number | string
|
||||
): Promise<{ success: boolean; data?: PostApiData; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}`;
|
||||
|
||||
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: '게시글 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PostApiData> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success || !result.data) {
|
||||
return { success: false, error: result.message || '게시글을 찾을 수 없습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[DynamicBoardActions] getDynamicBoardPost error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
boardCode: string, postId: number | string
|
||||
): Promise<ActionResult<PostApiData>> {
|
||||
return executeServerAction<PostApiData>({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}`,
|
||||
errorMessage: '게시글을 찾을 수 없습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 등록
|
||||
*/
|
||||
export async function createDynamicBoardPost(
|
||||
boardCode: string,
|
||||
data: {
|
||||
title: string;
|
||||
content: string;
|
||||
is_secret?: boolean;
|
||||
is_notice?: boolean;
|
||||
custom_fields?: Record<string, string>;
|
||||
}
|
||||
): Promise<{ success: boolean; data?: PostApiData; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts`;
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시글 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '게시글 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[DynamicBoardActions] createDynamicBoardPost error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
data: { title: string; content: string; is_secret?: boolean; is_notice?: boolean; custom_fields?: Record<string, string> }
|
||||
): Promise<ActionResult<PostApiData>> {
|
||||
return executeServerAction<PostApiData>({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts`,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
errorMessage: '게시글 등록에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 수정
|
||||
*/
|
||||
export async function updateDynamicBoardPost(
|
||||
boardCode: string,
|
||||
postId: number | string,
|
||||
data: {
|
||||
title?: string;
|
||||
content?: string;
|
||||
is_secret?: boolean;
|
||||
is_notice?: boolean;
|
||||
custom_fields?: Record<string, string>;
|
||||
}
|
||||
): Promise<{ success: boolean; data?: PostApiData; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}`;
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시글 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '게시글 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[DynamicBoardActions] updateDynamicBoardPost error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
boardCode: string, postId: number | string,
|
||||
data: { title?: string; content?: string; is_secret?: boolean; is_notice?: boolean; custom_fields?: Record<string, string> }
|
||||
): Promise<ActionResult<PostApiData>> {
|
||||
return executeServerAction<PostApiData>({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}`,
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
errorMessage: '게시글 수정에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 삭제
|
||||
*/
|
||||
export async function deleteDynamicBoardPost(
|
||||
boardCode: string,
|
||||
postId: number | string
|
||||
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}`;
|
||||
|
||||
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: '게시글 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '게시글 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[DynamicBoardActions] deleteDynamicBoardPost error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
boardCode: string, postId: number | string
|
||||
): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}`,
|
||||
method: 'DELETE',
|
||||
errorMessage: '게시글 삭제에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 댓글 API =====
|
||||
|
||||
/**
|
||||
* 댓글 목록 조회
|
||||
*/
|
||||
export async function getDynamicBoardComments(
|
||||
boardCode: string,
|
||||
postId: number | string
|
||||
): Promise<{ success: boolean; data?: CommentsApiResponse; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}/comments`;
|
||||
|
||||
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: '댓글 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<CommentsApiResponse> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '댓글 목록 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[DynamicBoardActions] getDynamicBoardComments error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
boardCode: string, postId: number | string
|
||||
): Promise<ActionResult<CommentsApiResponse>> {
|
||||
return executeServerAction<CommentsApiResponse>({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}/comments`,
|
||||
errorMessage: '댓글 목록 조회에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 댓글 작성
|
||||
*/
|
||||
export async function createDynamicBoardComment(
|
||||
boardCode: string,
|
||||
postId: number | string,
|
||||
content: string
|
||||
): Promise<{ success: boolean; data?: CommentApiData; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}/comments`;
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ content }),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '댓글 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '댓글 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[DynamicBoardActions] createDynamicBoardComment error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
boardCode: string, postId: number | string, content: string
|
||||
): Promise<ActionResult<CommentApiData>> {
|
||||
return executeServerAction<CommentApiData>({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}/comments`,
|
||||
method: 'POST',
|
||||
body: { content },
|
||||
errorMessage: '댓글 등록에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 댓글 수정
|
||||
*/
|
||||
export async function updateDynamicBoardComment(
|
||||
boardCode: string,
|
||||
postId: number | string,
|
||||
commentId: number | string,
|
||||
content: string
|
||||
): Promise<{ success: boolean; data?: CommentApiData; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}/comments/${commentId}`;
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ content }),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '댓글 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '댓글 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: result.data };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[DynamicBoardActions] updateDynamicBoardComment error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
boardCode: string, postId: number | string, commentId: number | string, content: string
|
||||
): Promise<ActionResult<CommentApiData>> {
|
||||
return executeServerAction<CommentApiData>({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}/comments/${commentId}`,
|
||||
method: 'PUT',
|
||||
body: { content },
|
||||
errorMessage: '댓글 수정에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 댓글 삭제
|
||||
*/
|
||||
export async function deleteDynamicBoardComment(
|
||||
boardCode: string,
|
||||
postId: number | string,
|
||||
commentId: number | string
|
||||
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}/comments/${commentId}`;
|
||||
|
||||
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: '댓글 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '댓글 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[DynamicBoardActions] deleteDynamicBoardComment error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
boardCode: string, postId: number | string, commentId: number | string
|
||||
): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}/comments/${commentId}`,
|
||||
method: 'DELETE',
|
||||
errorMessage: '댓글 삭제에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
@@ -1,31 +1,17 @@
|
||||
/**
|
||||
* 게시판 게시글 Server Actions
|
||||
*
|
||||
* API Endpoints:
|
||||
* - GET /api/v1/boards/{code}/posts - 게시글 목록
|
||||
* - GET /api/v1/boards/{code}/posts/{id} - 게시글 상세
|
||||
* - GET /api/v1/my-posts - 나의 게시글
|
||||
* - POST /api/v1/boards/{code}/posts - 게시글 작성
|
||||
* - PUT /api/v1/boards/{code}/posts/{id} - 게시글 수정
|
||||
* - DELETE /api/v1/boards/{code}/posts/{id} - 게시글 삭제
|
||||
*/
|
||||
|
||||
'use server';
|
||||
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import { executeServerAction, type ActionResult } from '@/lib/api/execute-server-action';
|
||||
import type {
|
||||
PostApiData,
|
||||
PostPaginationResponse,
|
||||
ApiResponse,
|
||||
PostFilters,
|
||||
Post,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* API 데이터 → 프론트엔드 타입 변환
|
||||
*/
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// ===== 변환 =====
|
||||
function transformApiToPost(apiData: PostApiData, boardName?: string): Post {
|
||||
return {
|
||||
id: String(apiData.id),
|
||||
@@ -40,7 +26,7 @@ function transformApiToPost(apiData: PostApiData, boardName?: string): Post {
|
||||
authorPosition: apiData.author?.position,
|
||||
isPinned: apiData.is_notice,
|
||||
isSecret: apiData.is_secret,
|
||||
allowComments: true, // API에서 board 설정 참조 필요
|
||||
allowComments: true,
|
||||
viewCount: apiData.views,
|
||||
attachments: (apiData.files || []).map(file => ({
|
||||
id: String(file.id),
|
||||
@@ -55,269 +41,93 @@ function transformApiToPost(apiData: PostApiData, boardName?: string): Post {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 목록 조회
|
||||
*/
|
||||
function buildPostFilterParams(filters?: PostFilters): string {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
if (filters?.is_notice !== undefined) params.append('is_notice', String(filters.is_notice));
|
||||
if (filters?.status) params.append('status', filters.status);
|
||||
if (filters?.per_page) params.append('per_page', String(filters.per_page));
|
||||
if (filters?.page) params.append('page', String(filters.page));
|
||||
if (filters?.board_code) params.append('board_code', filters.board_code);
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
// ===== 게시글 목록 조회 =====
|
||||
export async function getPosts(
|
||||
boardCode: string,
|
||||
filters?: PostFilters
|
||||
): Promise<{ success: boolean; data?: PostPaginationResponse; posts?: Post[]; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
if (filters?.is_notice !== undefined) params.append('is_notice', String(filters.is_notice));
|
||||
if (filters?.status) params.append('status', filters.status);
|
||||
if (filters?.per_page) params.append('per_page', String(filters.per_page));
|
||||
if (filters?.page) params.append('page', String(filters.page));
|
||||
|
||||
const queryString = params.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts${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<PostPaginationResponse>;
|
||||
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 posts = result.data.data.map(post => transformApiToPost(post));
|
||||
|
||||
return { success: true, data: result.data, posts };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[BoardActions] getPosts error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
const queryString = buildPostFilterParams(filters);
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts${queryString ? `?${queryString}` : ''}`,
|
||||
transform: (data: PostPaginationResponse) => ({
|
||||
raw: data,
|
||||
posts: data.data.map(post => transformApiToPost(post)),
|
||||
}),
|
||||
errorMessage: '게시글 목록 조회에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data?.raw, posts: result.data?.posts, error: result.error, __authError: result.__authError };
|
||||
}
|
||||
|
||||
/**
|
||||
* 나의 게시글 목록 조회
|
||||
*/
|
||||
// ===== 나의 게시글 목록 조회 =====
|
||||
export async function getMyPosts(
|
||||
filters?: PostFilters
|
||||
): Promise<{ success: boolean; data?: PostPaginationResponse; posts?: Post[]; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.search) params.append('search', filters.search);
|
||||
if (filters?.board_code) params.append('board_code', filters.board_code);
|
||||
if (filters?.status) params.append('status', filters.status);
|
||||
if (filters?.per_page) params.append('per_page', String(filters.per_page));
|
||||
if (filters?.page) params.append('page', String(filters.page));
|
||||
|
||||
const queryString = params.toString();
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/my-posts${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<PostPaginationResponse>;
|
||||
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 posts = result.data.data.map(post => transformApiToPost(post));
|
||||
|
||||
return { success: true, data: result.data, posts };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[BoardActions] getMyPosts error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
const queryString = buildPostFilterParams(filters);
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/my-posts${queryString ? `?${queryString}` : ''}`,
|
||||
transform: (data: PostPaginationResponse) => ({
|
||||
raw: data,
|
||||
posts: data.data.map(post => transformApiToPost(post)),
|
||||
}),
|
||||
errorMessage: '나의 게시글 조회에 실패했습니다.',
|
||||
});
|
||||
return { success: result.success, data: result.data?.raw, posts: result.data?.posts, error: result.error, __authError: result.__authError };
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 상세 조회
|
||||
*/
|
||||
export async function getPost(
|
||||
boardCode: string,
|
||||
postId: number | string
|
||||
): Promise<{ success: boolean; data?: Post; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}`;
|
||||
|
||||
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: '게시글 조회에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result: ApiResponse<PostApiData> = await response.json();
|
||||
|
||||
if (!response.ok || !result.success || !result.data) {
|
||||
return { success: false, error: result.message || '게시글을 찾을 수 없습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToPost(result.data) };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[BoardActions] getPost error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
// ===== 게시글 상세 조회 =====
|
||||
export async function getPost(boardCode: string, postId: number | string): Promise<ActionResult<Post>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}`,
|
||||
transform: (data: PostApiData) => transformApiToPost(data),
|
||||
errorMessage: '게시글을 찾을 수 없습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 등록
|
||||
*/
|
||||
// ===== 게시글 등록 =====
|
||||
export async function createPost(
|
||||
boardCode: string,
|
||||
data: {
|
||||
title: string;
|
||||
content: string;
|
||||
is_notice?: boolean;
|
||||
is_secret?: boolean;
|
||||
custom_fields?: Record<string, string>;
|
||||
}
|
||||
): Promise<{ success: boolean; data?: Post; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts`;
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시글 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '게시글 등록에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToPost(result.data) };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[BoardActions] createPost error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
data: { title: string; content: string; is_notice?: boolean; is_secret?: boolean; custom_fields?: Record<string, string> }
|
||||
): Promise<ActionResult<Post>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts`,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
transform: (d: PostApiData) => transformApiToPost(d),
|
||||
errorMessage: '게시글 등록에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 수정
|
||||
*/
|
||||
// ===== 게시글 수정 =====
|
||||
export async function updatePost(
|
||||
boardCode: string,
|
||||
postId: number | string,
|
||||
data: {
|
||||
title?: string;
|
||||
content?: string;
|
||||
is_notice?: boolean;
|
||||
is_secret?: boolean;
|
||||
custom_fields?: Record<string, string>;
|
||||
}
|
||||
): Promise<{ success: boolean; data?: Post; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}`;
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: '게시글 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '게시글 수정에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true, data: transformApiToPost(result.data) };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[BoardActions] updatePost error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
data: { title?: string; content?: string; is_notice?: boolean; is_secret?: boolean; custom_fields?: Record<string, string> }
|
||||
): Promise<ActionResult<Post>> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}`,
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
transform: (d: PostApiData) => transformApiToPost(d),
|
||||
errorMessage: '게시글 수정에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 삭제
|
||||
*/
|
||||
export async function deletePost(
|
||||
boardCode: string,
|
||||
postId: number | string
|
||||
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}`;
|
||||
|
||||
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: '게시글 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return { success: false, error: result.message || '게시글 삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[BoardActions] deletePost error:', error);
|
||||
return { success: false, error: '서버 오류가 발생했습니다.' };
|
||||
}
|
||||
// ===== 게시글 삭제 =====
|
||||
export async function deletePost(boardCode: string, postId: number | string): Promise<ActionResult> {
|
||||
return executeServerAction({
|
||||
url: `${API_URL}/api/v1/boards/${boardCode}/posts/${postId}`,
|
||||
method: 'DELETE',
|
||||
errorMessage: '게시글 삭제에 실패했습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user