server.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. 'use server';
  2. import { cookies } from 'next/headers'
  3. import { ResultDto, TokenData, ApiError } from '@/dtos/response/common';
  4. import BoardManager from '@/types/forum/boardManager';
  5. import { CLAIM_NAME_IDENTIFIER, CLAIM_EMAIL, CLAIM_NAME } from '@/constants/common';
  6. import { fetchMemberInfo } from '@/lib/api/account';
  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().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. return await res.json() as ResultDto<T>;
  57. } catch (err) {
  58. let message = '서버와 통신이 불가합니다.';
  59. const status = 500;
  60. if (err instanceof Error) {
  61. message = err.message;
  62. }
  63. console.warn(`[${status}] ${message}`);
  64. return {
  65. success: false,
  66. status: status,
  67. message: message,
  68. data: null,
  69. errors: null
  70. } satisfies ResultDto<T>;
  71. }
  72. }
  73. export async function getAccessToken(): Promise<string|null> {
  74. return (await cookies()).get('accessToken')?.value ?? null;
  75. }
  76. export async function getRefreshToken(): Promise<string|null> {
  77. return (await cookies()).get('refreshToken')?.value ?? null;
  78. }
  79. // API 서버 URL 조회
  80. export async function getAPIUrl(): Promise<string> {
  81. return process.env.API_URL as string;
  82. }
  83. // SignalR Crypto 서버 URL 조회
  84. export async function getSignalRCryptoUrl(): Promise<string> {
  85. return process.env.SIGNALR_CRYPTO_URL as string;
  86. }
  87. // SignalR Chat 서버 URL 조회
  88. export async function getSignalRChatUrl(): Promise<string> {
  89. return process.env.SIGNALR_CHAT_URL as string;
  90. }
  91. // JWT 토큰에서 사용자 정보 추출
  92. export async function getTokenData(): Promise<TokenData|null> {
  93. try {
  94. const token = await getAccessToken();
  95. if (!token) {
  96. throw new Error('Access token not found');
  97. }
  98. const base64URL = token.split('.')[1]; // JWT의 Payload 부분
  99. const base64 = base64URL.replace(/-/g, '+').replace(/_/g, '/'); // Base64 형식 변환
  100. const jsonPayload = decodeURIComponent(
  101. atob(base64).split('').map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('')
  102. );
  103. const payload = JSON.parse(jsonPayload);
  104. return {
  105. id: payload[CLAIM_NAME_IDENTIFIER] || null,
  106. email: payload[CLAIM_EMAIL] || null,
  107. name: payload[CLAIM_NAME] || null
  108. };
  109. } catch {
  110. return null;
  111. }
  112. }
  113. // 첫번째 오류 조회
  114. export async function getFirstError(errors: ApiError[] | null): Promise<string|null> {
  115. if (!errors || errors.length === 0) {
  116. return null;
  117. }
  118. return errors[0].description ?? null;
  119. }
  120. // 서버 응답 메시지 분석
  121. export async function throwError<T>(res: ResultDto<T>): Promise<void> {
  122. if (res.success) {
  123. return;
  124. }
  125. let message:string|null = await getFirstError(res.errors);
  126. if (!message && res.message) {
  127. message = res.message;
  128. }
  129. switch (res.status) {
  130. case 400:
  131. throw Error(message || '잘못된 요청입니다.');
  132. case 401:
  133. throw Error('로그인 후 이용해 주세요.');
  134. case 403:
  135. throw Error('권한이 없습니다.');
  136. case 404:
  137. throw Error('요청하신 페이지를 찾을 수 없습니다.');
  138. }
  139. if (message) {
  140. throw Error(message);
  141. }
  142. }
  143. // 게시판, 게시글, 댓글 등 권한 확인
  144. export async function checkPermission(permission: number, boardManager: BoardManager[]): Promise<boolean> {
  145. if (permission > -1) {
  146. const member = await fetchMemberInfo();
  147. if (member.success && member.data) {
  148. if (
  149. // 게시판 접근 권한이 회원 등급보다 높거나
  150. permission < member.data.memberGrade.order ||
  151. // 게시판 관리자에 포함되어 있으면 권한이 있음
  152. boardManager.some(manager => member.data!.id && manager.user.email == member.data!.email)
  153. ) {
  154. return true;
  155. }
  156. }
  157. return false;
  158. }
  159. return true;
  160. }