'use server'; import { cookies } from 'next/headers' import { ResultDto, TokenData, ApiError } from '@/types/response/common'; import BoardManager from '@/types/forum/boardManager'; import { MemberResponse } from '@/types/response/account/member'; import { CLAIM_NAME_IDENTIFIER, CLAIM_EMAIL, CLAIM_NAME } from '@/constants/common'; const API_URL = process.env.API_URL; export async function fetchJson(url: string, options: RequestInit = {}): Promise> { try { const actionURL = `${API_URL}${url.startsWith('/') ? url : `/${url}`}`; const cookie = await cookies(); const cookieData = cookie.getAll().filter(c => c.name !== 'member').map(c => `${c.name}=${c.value}`).join("; "); const accessToken = cookie.get('accessToken')?.value; const res = await fetch(actionURL, { ...options, headers: { ...(options.body && !(options.body instanceof FormData) ? { 'Content-Type': 'application/json' } : {}), ...(options.headers || {}), ...(accessToken ? { 'Authorization': `Bearer ${accessToken}` } : {}), Cookie: cookieData, 'Accept': 'application/json' }, cache: 'no-store' }); // Set-Cookie 헤더 처리 — 백엔드 응답 쿠키를 Next.js 쿠키 저장소에 저장 const setCookieHeader = res.headers.getSetCookie(); if (setCookieHeader && setCookieHeader.length > 0) { const cookieStore = await cookies(); for (const cookieString of setCookieHeader) { const parts = cookieString.split(';').map(p => p.trim()); const [nameValue, ...attributes] = parts; const eqIndex = nameValue.indexOf('='); if (eqIndex === -1) { continue; } const name = nameValue.substring(0, eqIndex); const value = nameValue.substring(eqIndex + 1); const options: Record = {}; for (const attr of attributes) { const [key, val] = attr.split('='); const lowerKey = key.toLowerCase().trim(); if (lowerKey === 'httponly') options.httpOnly = true; else if (lowerKey === 'secure') options.secure = true; else if (lowerKey === 'path') options.path = val; else if (lowerKey === 'samesite') options.sameSite = val?.toLowerCase() as 'lax' | 'strict' | 'none'; else if (lowerKey === 'max-age') options.maxAge = parseInt(val); else if (lowerKey === 'expires') options.expires = new Date(val); } cookieStore.set(name, value, options); } } if (res.status === 204) { return { success: true, status: 204, message: null, data: null, errors: null } satisfies ResultDto; } const contentType = res.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { return { success: false, status: res.status, message: null, data: null, errors: null } satisfies ResultDto; } return await res.json() as ResultDto; } catch (err) { let message = '서버와 통신이 불가합니다.'; const status = 500; if (err instanceof Error) { message = err.message; } console.warn(`[${status}] ${message}`); return { success: false, status: status, message: message, data: null, errors: null } satisfies ResultDto; } } export async function getAccessToken(): Promise { return (await cookies()).get('accessToken')?.value ?? null; } export async function getRefreshToken(): Promise { return (await cookies()).get('refreshToken')?.value ?? null; } // API 서버 URL 조회 export async function getAPIUrl(): Promise { return process.env.API_URL as string; } // SignalR Chat 서버 URL 조회 export async function getSignalRChatUrl(): Promise { return process.env.SIGNALR_CHAT_URL as string; } // JWT 토큰에서 사용자 정보 추출 export async function getTokenData(): Promise { try { const token = await getAccessToken(); if (!token) { throw new Error('Access token not found'); } const base64URL = token.split('.')[1]; // JWT의 Payload 부분 const base64 = base64URL.replace(/-/g, '+').replace(/_/g, '/'); // Base64 형식 변환 const jsonPayload = decodeURIComponent( atob(base64).split('').map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('') ); const payload = JSON.parse(jsonPayload); return { id: payload[CLAIM_NAME_IDENTIFIER] || null, email: payload[CLAIM_EMAIL] || null, name: payload[CLAIM_NAME] || null }; } catch { return null; } } // 첫번째 오류 조회 export async function getFirstError(errors: ApiError[] | null): Promise { if (!errors || errors.length === 0) { return null; } return errors[0].description ?? null; } // 서버 응답 메시지 분석 export async function throwError(res: ResultDto): Promise { if (res.success) { return; } let message:string|null = await getFirstError(res.errors); if (!message && res.message) { message = res.message; } switch (res.status) { case 400: throw Error(message || '잘못된 요청입니다.'); case 401: throw Error('로그인 후 이용해 주세요.'); case 403: throw Error('권한이 없습니다.'); case 404: throw Error('요청하신 페이지를 찾을 수 없습니다.'); } if (message) { throw Error(message); } } // 게시판, 게시글, 댓글 등 권한 확인 export async function checkPermission(permission: number, boardManager: BoardManager[]): Promise { if (permission <= -1) { return true; } const raw = (await cookies()).get('member')?.value; const member = raw ? JSON.parse(decodeURIComponent(raw)) as MemberResponse : null; if (!member) { return false; } // 최고관리자는 항상 허용 if (member.isAdmin) { return true; } // 유효 권한 레벨 계산 (매니저=99, 일반=memberGrade.order) const isBoardManager = boardManager.some(manager => member.id && manager.user.email === member.email); const level = isBoardManager ? 99 : (member.memberGrade?.order ?? 0); return permission <= level; }