server.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. 'use server';
  2. import { cookies } from 'next/headers'
  3. import { ResultDto, TokenData, ApiError } from '@/types/response/common';
  4. import BoardManager from '@/types/forum/boardManager';
  5. import { MemberResponse } from '@/types/response/account/member';
  6. import { CLAIM_NAME_IDENTIFIER, CLAIM_EMAIL, CLAIM_NAME } from '@/constants/common';
  7. const API_URL = process.env.API_URL;
  8. export async function fetchJson<T>(url: string, options: RequestInit = {}): Promise<ResultDto<T>> {
  9. try {
  10. const actionURL = `${API_URL}${url.startsWith('/') ? url : `/${url}`}`;
  11. const cookie = await cookies();
  12. const cookieData = cookie.getAll().filter(c => c.name !== 'member').map(c => `${c.name}=${c.value}`).join("; ");
  13. const accessToken = cookie.get('accessToken')?.value;
  14. const res = await fetch(actionURL, {
  15. ...options,
  16. headers: {
  17. ...(options.body && !(options.body instanceof FormData) ? { 'Content-Type': 'application/json' } : {}),
  18. ...(options.headers || {}),
  19. ...(accessToken ? { 'Authorization': `Bearer ${accessToken}` } : {}),
  20. Cookie: cookieData,
  21. 'Accept': 'application/json'
  22. },
  23. cache: 'no-store'
  24. });
  25. // Set-Cookie 헤더 처리 — 백엔드 응답 쿠키를 Next.js 쿠키 저장소에 저장
  26. const setCookieHeader = res.headers.getSetCookie();
  27. if (setCookieHeader && setCookieHeader.length > 0)
  28. {
  29. const cookieStore = await cookies();
  30. for (const cookieString of setCookieHeader) {
  31. const parts = cookieString.split(';').map(p => p.trim());
  32. const [nameValue, ...attributes] = parts;
  33. const eqIndex = nameValue.indexOf('=');
  34. if (eqIndex === -1) {
  35. continue;
  36. }
  37. const name = nameValue.substring(0, eqIndex);
  38. const value = nameValue.substring(eqIndex + 1);
  39. const options: Record<string, unknown> = {};
  40. for (const attr of attributes) {
  41. const [key, val] = attr.split('=');
  42. const lowerKey = key.toLowerCase().trim();
  43. if (lowerKey === 'httponly') options.httpOnly = true;
  44. else if (lowerKey === 'secure') options.secure = true;
  45. else if (lowerKey === 'path') options.path = val;
  46. else if (lowerKey === 'samesite') options.sameSite = val?.toLowerCase() as 'lax' | 'strict' | 'none';
  47. else if (lowerKey === 'max-age') options.maxAge = parseInt(val);
  48. else if (lowerKey === 'expires') options.expires = new Date(val);
  49. }
  50. cookieStore.set(name, value, options);
  51. }
  52. }
  53. if (res.status === 204) {
  54. return { success: true, status: 204, message: null, data: null, errors: null } satisfies ResultDto<T>;
  55. }
  56. const contentType = res.headers.get('content-type');
  57. if (!contentType || !contentType.includes('application/json')) {
  58. return {
  59. success: false,
  60. status: res.status,
  61. message: null,
  62. data: null,
  63. errors: null
  64. } satisfies ResultDto<T>;
  65. }
  66. return await res.json() as ResultDto<T>;
  67. } catch (err) {
  68. let message = '서버와 통신이 불가합니다.';
  69. const status = 500;
  70. if (err instanceof Error) {
  71. message = err.message;
  72. }
  73. console.warn(`[${status}] ${message}`);
  74. return {
  75. success: false,
  76. status: status,
  77. message: message,
  78. data: null,
  79. errors: null
  80. } satisfies ResultDto<T>;
  81. }
  82. }
  83. export async function getAccessToken(): Promise<string|null> {
  84. return (await cookies()).get('accessToken')?.value ?? null;
  85. }
  86. export async function getRefreshToken(): Promise<string|null> {
  87. return (await cookies()).get('refreshToken')?.value ?? null;
  88. }
  89. // API 서버 URL 조회
  90. export async function getAPIUrl(): Promise<string> {
  91. return process.env.API_URL as string;
  92. }
  93. // SignalR Crypto 서버 URL 조회
  94. export async function getSignalRCryptoUrl(): Promise<string> {
  95. return process.env.SIGNALR_CRYPTO_URL as string;
  96. }
  97. // SignalR Chat 서버 URL 조회
  98. export async function getSignalRChatUrl(): Promise<string> {
  99. return process.env.SIGNALR_CHAT_URL as string;
  100. }
  101. // JWT 토큰에서 사용자 정보 추출
  102. export async function getTokenData(): Promise<TokenData|null> {
  103. try {
  104. const token = await getAccessToken();
  105. if (!token) {
  106. throw new Error('Access token not found');
  107. }
  108. const base64URL = token.split('.')[1]; // JWT의 Payload 부분
  109. const base64 = base64URL.replace(/-/g, '+').replace(/_/g, '/'); // Base64 형식 변환
  110. const jsonPayload = decodeURIComponent(
  111. atob(base64).split('').map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('')
  112. );
  113. const payload = JSON.parse(jsonPayload);
  114. return {
  115. id: payload[CLAIM_NAME_IDENTIFIER] || null,
  116. email: payload[CLAIM_EMAIL] || null,
  117. name: payload[CLAIM_NAME] || null
  118. };
  119. } catch {
  120. return null;
  121. }
  122. }
  123. // 첫번째 오류 조회
  124. export async function getFirstError(errors: ApiError[] | null): Promise<string|null> {
  125. if (!errors || errors.length === 0) {
  126. return null;
  127. }
  128. return errors[0].description ?? null;
  129. }
  130. // 서버 응답 메시지 분석
  131. export async function throwError<T>(res: ResultDto<T>): Promise<void> {
  132. if (res.success) {
  133. return;
  134. }
  135. let message:string|null = await getFirstError(res.errors);
  136. if (!message && res.message) {
  137. message = res.message;
  138. }
  139. switch (res.status) {
  140. case 400:
  141. throw Error(message || '잘못된 요청입니다.');
  142. case 401:
  143. throw Error('로그인 후 이용해 주세요.');
  144. case 403:
  145. throw Error('권한이 없습니다.');
  146. case 404:
  147. throw Error('요청하신 페이지를 찾을 수 없습니다.');
  148. }
  149. if (message) {
  150. throw Error(message);
  151. }
  152. }
  153. // 게시판, 게시글, 댓글 등 권한 확인
  154. export async function checkPermission(permission: number, boardManager: BoardManager[]): Promise<boolean> {
  155. if (permission <= -1) {
  156. return true;
  157. }
  158. const raw = (await cookies()).get('member')?.value;
  159. const member = raw ? JSON.parse(decodeURIComponent(raw)) as MemberResponse : null;
  160. if (!member) {
  161. return false;
  162. }
  163. // 최고관리자는 항상 허용
  164. if (member.isAdmin) {
  165. return true;
  166. }
  167. // 유효 권한 레벨 계산 (매니저=99, 일반=memberGrade.order)
  168. const isBoardManager = boardManager.some(manager => member.id && manager.user.email === member.email);
  169. const level = isBoardManager ? 99 : (member.memberGrade?.order ?? 0);
  170. return permission <= level;
  171. }