diff --git a/backend/game/matchingConsumers.py b/backend/game/matchingConsumers.py index 1286de7..f9ef6e8 100644 --- a/backend/game/matchingConsumers.py +++ b/backend/game/matchingConsumers.py @@ -54,9 +54,9 @@ async def initialize(self, user1, user2): self.left_ball = Ball(0, SCREEN_HEIGHT, SCREEN_WIDTH, self.left_bar) self.right_ball = Ball(1, SCREEN_HEIGHT, SCREEN_WIDTH, self.right_bar) self.score = [0, 0] - print("user1:", user1, "user2:", user2) + # print("user1:", user1, "user2:", user2) user1_nickname, user2_nickname = await get_user_nicknames(user1, user2) - print("user1_nickname:", user1_nickname, "user2_nickname:", user2_nickname) + # print("user1_nickname:", user1_nickname, "user2_nickname:", user2_nickname) self.player = [user1_nickname, user2_nickname] self.penalty_time = [0, 0] print("initializing game state finished successfully!") @@ -75,8 +75,12 @@ async def connect(self): self.authenticated = False self.start_time = timezone.now() + if self.room_group_name not in MatchingGameConsumer.game_states: + return + await self.accept() + async def receive(self, text_data): text_data_json = json.loads(text_data) action = text_data_json["action"] @@ -85,10 +89,9 @@ async def receive(self, text_data): token = text_data_json.get("token") if not token or not self.authenticate(token): print("authentication failed") - await self.close(code=4001) + # await self.close(code=4001) return self.authenticated = True - self.my_uid = 0 # Join room group after successful authentication await self.channel_layer.group_add(self.room_group_name, self.channel_name) @@ -108,8 +111,6 @@ async def receive(self, text_data): MatchingGameConsumer.game_tasks[self.room_group_name] = ( asyncio.create_task(self.game_loop()) ) - elif not self.authenticated: - await self.close(code=4001) else: bar = text_data_json.get("bar") state = MatchingGameConsumer.game_states[self.room_group_name] @@ -158,22 +159,40 @@ def authenticate(self, token): return False async def disconnect(self, close_code): - # Leave room group + # 인증된 소켓만 처리 if self.authenticated: - await self.channel_layer.group_discard( - self.room_group_name, self.channel_name - ) - # Decrease the client count for this room + print("disconnected: ", self.user_index) + await self.channel_layer.group_discard(self.room_group_name, self.channel_name) + + # 현재 방의 접속자 수를 1 줄임 if self.room_group_name in MatchingGameConsumer.client_counts: MatchingGameConsumer.client_counts[self.room_group_name] -= 1 - if MatchingGameConsumer.client_counts[self.room_group_name] <= 0: - MatchingGameConsumer.game_tasks[self.room_group_name].cancel() - del MatchingGameConsumer.game_tasks[self.room_group_name] - del MatchingGameConsumer.game_states[self.room_group_name] - del MatchingGameConsumer.client_counts[self.room_group_name] + + # 만약 이제 방에 1명만 남았다면 -> 남은 사람이 승자 + if MatchingGameConsumer.client_counts[self.room_group_name] == 1: + # 누가 남았는지 판별 (내가 0이라면 1이 승자, 내가 1이라면 0이 승자) + winner_index = 1 - self.user_index + state = MatchingGameConsumer.game_states[self.room_group_name] + + # send_game_result를 통해 DB에 게임결과 저장 및 + # 나머지(승자)에게 게임 종료 메시지 전송 + await self.send_game_result(winner_index) + await asyncio.sleep(0.1) + await self.close() + + # 아무도 안 남았다면 방을 완전히 정리 + elif MatchingGameConsumer.client_counts[self.room_group_name] <= 0: + if self.room_group_name in MatchingGameConsumer.game_tasks: + MatchingGameConsumer.game_tasks[self.room_group_name].cancel() + del MatchingGameConsumer.game_tasks[self.room_group_name] + if self.room_group_name in MatchingGameConsumer.game_states: + del MatchingGameConsumer.game_states[self.room_group_name] + if self.room_group_name in MatchingGameConsumer.client_counts: + del MatchingGameConsumer.client_counts[self.room_group_name] else: MatchingGameConsumer.client_counts[self.room_group_name] = 0 + async def send_initialize_game(self): state = MatchingGameConsumer.game_states[self.room_group_name] await self.send( @@ -248,6 +267,7 @@ async def game_loop(self): async def send_game_result(self, winner): state = MatchingGameConsumer.game_states[self.room_group_name] + print("Game result for", self.room_group_name, ":", state.score, "Winner:", winner) await self.save_game_result(state, winner) await self.channel_layer.group_send( self.room_group_name, @@ -270,6 +290,10 @@ def save_game_result(self, state, winner): user2_obj = None score1, score2 = state.score + if winner == 0: + score1 = MAX_SCORE + else: + score2 = MAX_SCORE game = Game.objects.create( game_type="PvP", @@ -300,16 +324,21 @@ async def game_result_message(self, event): score = event["score"] winner = event["winner"] - # Send the game result to the WebSocket - await self.send( - text_data=json.dumps( - { - "type": "game_result", - "score": score, - "winner": winner, - } + if not self.scope["client"]: + return + try: + await self.send( + text_data=json.dumps( + { + "type": "game_result", + "score": score, + "winner": winner, + } + ) ) - ) + except Exception as e: + print("Failed to send game_result_message:", e, "for", self.user_index) + async def send_game_state(self): state = MatchingGameConsumer.game_states[self.room_group_name] diff --git a/backend/game/onlineConsumers.py b/backend/game/onlineConsumers.py index a7db3ed..c7e2319 100644 --- a/backend/game/onlineConsumers.py +++ b/backend/game/onlineConsumers.py @@ -5,6 +5,8 @@ from django.conf import settings from channels.exceptions import DenyConnection from .matchingConsumers import MatchingGameConsumer, MatchingGameState +import random +import uuid class OnlineConsumer(AsyncWebsocketConsumer): @@ -26,7 +28,7 @@ async def start_game(cls): user1 = cls.matching_queue.pop(0) user2 = cls.matching_queue.pop(0) - room_name = f"{user1.uid}_{user2.uid}" + room_name = f"{user1.uid}_{user2.uid}_{str(uuid.uuid4())[:8]}" game_state = MatchingGameState() await game_state.initialize(user1.uid, user2.uid) # 비동기 초기화 @@ -94,23 +96,28 @@ def authenticate(self, token): return False async def enter_matching(self): - # Add user to matching queue - if self not in OnlineConsumer.matching_queue: - OnlineConsumer.matching_queue.append(self) - print( - "Matching queue: ", [user.uid for user in OnlineConsumer.matching_queue] - ) + for user in list(OnlineConsumer.matching_queue): + if user.uid == self.uid: + OnlineConsumer.matching_queue.remove(user) + print(f"Replaced existing user {self.uid} in the matching queue.") + break + OnlineConsumer.matching_queue.append(self) + print( + "Matching queue: ", [user.uid for user in OnlineConsumer.matching_queue] + ) async def leave_matching(self): - # Remove user from matching queue - if self in OnlineConsumer.matching_queue: - OnlineConsumer.matching_queue.remove(self) - print(f"User {self.uid} removed from matching queue") - print( - "Matching queue: ", [user.uid for user in OnlineConsumer.matching_queue] - ) + for user in list(OnlineConsumer.matching_queue): + if user.uid == self.uid: + OnlineConsumer.matching_queue.remove(user) + print(f"User {self.uid} removed from matching queue") + break + print( + "Matching queue: ", [user.uid for user in OnlineConsumer.matching_queue] + ) async def disconnect(self, close_code): + await self.leave_matching() try: if self.uid in OnlineConsumer.online_user_list: OnlineConsumer.online_user_list.remove(self.uid) diff --git a/backend/login/views.py b/backend/login/views.py index de45531..31fd5fe 100644 --- a/backend/login/views.py +++ b/backend/login/views.py @@ -24,14 +24,17 @@ def login(request): def callback(request): code = request.data.get("code") if not code: + print("No code in request") return Response(status=401) access_token = get_acccess_token(code) if not access_token: + print("Failed to get access token!!!") return Response(status=401) user_data = get_user_info(access_token) if not user_data: + print("Failed to get user info") return Response(status=401) user, created = get_or_save_user(user_data) diff --git a/frontend/src/app.js b/frontend/src/app.js index 6be822d..9cecadd 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -1,5 +1,5 @@ -import { initializeRouter, createRoutes, changeUrl } from "./core/router.js"; -import { getCookie } from "./core/jwt.js"; +import { createRoutes } from "./core/router.js"; +import { showLoading } from "./core/showLoading.js"; class App { app; @@ -12,37 +12,6 @@ class App { export const root = new App(); export const routes = createRoutes(root); - export let socketList = []; -const closeAllSockets = () => { - socketList.forEach(socket => { - if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) { - socket.close(); - } - }); - socketList = []; -}; - -const online = () => { - const onlineSocket = new WebSocket( - 'wss://' - + "localhost:443" - + '/ws/online/' - ); - socketList.push(onlineSocket); - - console.log(onlineSocket); - onlineSocket.onopen = () => { - const token = getCookie("jwt"); - onlineSocket.send(JSON.stringify({ 'action': 'authenticate', 'token': token })); - }; - onlineSocket.onclose = () => { - console.log("online socket closed"); - closeAllSockets(); - changeUrl("/error", false); - }; -} - -initializeRouter(routes); -online(); \ No newline at end of file +showLoading(routes, socketList); \ No newline at end of file diff --git a/frontend/src/components/Main-Menu.js b/frontend/src/components/Main-Menu.js index 383d092..bfcae4a 100644 --- a/frontend/src/components/Main-Menu.js +++ b/frontend/src/components/Main-Menu.js @@ -2,6 +2,8 @@ import { Component } from "../core/Component.js"; import { List } from "./List.js"; import { changeUrl } from "../core/router.js"; import { parseJWT } from "../core/jwt.js"; +import { closeAllSockets } from "../core/showLoading.js"; +import { socketList } from "../app.js"; export class Menu extends Component { translate() { @@ -19,12 +21,12 @@ export class Menu extends Component { userMenuTexts: ["友達", "プロフィール", "ログアウト"], } }; - + this.translations = languages[this.props.lan.value]; - + } - template () { + template() { const payload = parseJWT(); if (!payload) this.uid = null; else this.uid = payload.id; @@ -42,29 +44,29 @@ export class Menu extends Component { `; } - mounted(){ - new List(document.querySelector("ul#gameMenu"), { className: "gameMode", ids: ["LocalGame", "MultiGame", "Tournament"], contents: this.translations.gameMenuTexts}); - new List(document.querySelector("ul#userMenu"), { className: "showInfo", ids: ["Friends", "Profile", "Logout"], contents: this.translations.userMenuTexts}); + mounted() { + new List(document.querySelector("ul#gameMenu"), { className: "gameMode", ids: ["LocalGame", "MultiGame", "Tournament"], contents: this.translations.gameMenuTexts }); + new List(document.querySelector("ul#userMenu"), { className: "showInfo", ids: ["Friends", "Profile", "Logout"], contents: this.translations.userMenuTexts }); } - - setEvent () { - + + setEvent() { + this.addEvent('click', '#Friends', () => { changeUrl("/main/friends"); }); - + this.addEvent('click', '#LocalGame', () => { changeUrl(`/game/local/${this.uid}`); }); - + this.addEvent('click', "#MultiGame", () => { changeUrl("/main/matching"); }); - + this.addEvent('click', "#Tournament", () => { changeUrl("/main/tournament"); }); - + function storeLang(value) { fetch("https://localhost:443/api/language/", { method: 'PUT', @@ -76,28 +78,28 @@ export class Menu extends Component { language: value }) }) - .then(response => { - if (!response.ok) { - throw new Error('Network response was not ok'); - } - }) - .catch(error => { - console.error('Fetch error:', error); - changeUrl("/"); - }); + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + }) + .catch(error => { + console.error('Fetch error:', error); + changeUrl("/"); + }); changeUrl("/main"); } - + this.addEvent('click', '#enButton', () => { this.props.lan.value = 0; storeLang(this.props.lan.value); }); - + this.addEvent('click', '#koButton', () => { this.props.lan.value = 1; storeLang(this.props.lan.value); }); - + this.addEvent('click', '#jpButton', () => { this.props.lan.value = 2; storeLang(this.props.lan.value); @@ -114,14 +116,18 @@ export class Menu extends Component { method: 'POST', credentials: 'include', // 쿠키를 포함하여 요청 (사용자 인증 필요 시) }) - .then(response => { - if (response.ok) changeUrl(`/`); - else throw new Error('Network response was not ok'); - }) - .catch(error => { - console.error('Fetch error:', error); - changeUrl("/"); - }); + .then(response => { + if (response.ok) { + closeAllSockets(socketList); + changeUrl("/"); + location.reload(true); + } + else throw new Error('Network response was not ok'); + }) + .catch(error => { + console.error('Fetch error:', error); + location.reload(true); + }); }); } } diff --git a/frontend/src/components/Match-Wait.js b/frontend/src/components/Match-Wait.js index b910e95..5c41f47 100644 --- a/frontend/src/components/Match-Wait.js +++ b/frontend/src/components/Match-Wait.js @@ -6,18 +6,19 @@ import { socketList } from "../app.js" export class WaitForMatch extends Component { initState() { - if (socketList[0] !== undefined) - { - console.log("send enter-matching"); - socketList[0].send(JSON.stringify({ 'action': 'enter-matching' })); - socketList[0].onmessage = (e) => { - const data = JSON.parse(e.data); - console.log(data); - if (data.action === 'start_game') { - console.log("start game on " + data.room_name); - changeUrl('/game/vs/' + data.room_name); - } - }; + if (socketList[0] !== undefined) { + setTimeout(() => { + console.log("send enter-matching"); + socketList[0].send(JSON.stringify({ 'action': 'enter-matching' })); + socketList[0].onmessage = (e) => { + const data = JSON.parse(e.data); + console.log(data); + if (data.action === 'start_game') { + console.log("start game on " + data.room_name); + changeUrl('/game/vs/' + data.room_name); + } + }; + }, 2000); // 1초 지연 } return {}; } @@ -34,12 +35,12 @@ export class WaitForMatch extends Component { mathcingText: ["ピッタリの相手を探してるよ..."], } }; - + this.translations = languages[this.props.lan.value]; - + } - template () { + template() { const translations = this.translations; return ` @@ -70,11 +71,17 @@ export class WaitForMatch extends Component { setEvent() { this.addEvent('click', '#goBack', (event) => { + console.log("send leave-matching"); + if (socketList[0] !== undefined) + socketList[0].send(JSON.stringify({ 'action': 'leave-matching' })); + window.removeEventListener('popstate', handleSocketClose); changeUrl("/main", false); }); const handleSocketClose = (e) => { - socketList[0].send(JSON.stringify({ 'action': 'leave-matching' })); + console.log("send leave-matching"); + if (socketList[0] !== undefined) + socketList[0].send(JSON.stringify({ 'action': 'leave-matching' })); window.removeEventListener('popstate', handleSocketClose); } diff --git a/frontend/src/core/loadingComponents.js b/frontend/src/core/loadingComponents.js new file mode 100644 index 0000000..bd41d30 --- /dev/null +++ b/frontend/src/core/loadingComponents.js @@ -0,0 +1,57 @@ +export function createLoadingElement() { + const loadingElement = document.createElement("div"); + loadingElement.id = "loading"; + loadingElement.style = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + `; + + const spinner = document.createElement("div"); + spinner.style = ` + width: 80px; + height: 80px; + border: 8px solid rgba(255, 255, 255, 0.2); + border-top: 8px solid #fff; + border-radius: 50%; + animation: spin 1s linear infinite; + `; + + const loadingText = document.createElement("div"); + loadingText.style = ` + position: absolute; + top: 60%; + color: white; + font-size: 24px; + font-family: Arial, sans-serif; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7); + animation: fadeIn 1.5s ease-in-out infinite; + `; + loadingText.innerText = "Loading, please wait..."; + + loadingElement.appendChild(spinner); + loadingElement.appendChild(loadingText); + return loadingElement; +} + +export function addLoadingStyles() { + const style = document.createElement("style"); + style.innerHTML = ` + @keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + @keyframes fadeIn { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } + } + `; + document.head.appendChild(style); +} \ No newline at end of file diff --git a/frontend/src/core/router.js b/frontend/src/core/router.js index 2d7f39a..342b7a1 100644 --- a/frontend/src/core/router.js +++ b/frontend/src/core/router.js @@ -13,6 +13,7 @@ import { GameTournament } from "../components/Game-Tournament.js"; import { GameMatching } from "../components/Game-matching.js"; import { Error } from "../components/Error.js"; import { GameResult } from "../components/Game-Result.js"; +import { getRequest, postRequest } from '../utils.js'; export const createRoutes = (root) => { return { @@ -50,12 +51,16 @@ export const createRoutes = (root) => { component: (props) => new GameTournament(root.app, props), }, "/game/tournament/:uid/result/:winner": { - component: (props) => { props["isTournament"] = true; - return new GameResult(root.app, props);} + component: (props) => { + props["isTournament"] = true; + return new GameResult(root.app, props); + } }, "/game/:uid/result/:winner": { - component: (props) => { props["isTournament"] = false; - return new GameResult(root.app, props);} + component: (props) => { + props["isTournament"] = false; + return new GameResult(root.app, props); + } }, "/game/vs/:room": { component: (props) => new GameMatching(root.app, props), @@ -82,74 +87,56 @@ export async function parsePath(path) { const urlParams = new URLSearchParams(window.location.search); if (urlParams.has('code')) { const code = urlParams.get('code'); - - // code 보내고 2FA 여부 확인!! (추가 부분!!) - fetch('https://localhost:443/api/callback/', { - method: 'POST', - credentials: 'include', // 쿠키를 포함하여 요청 - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ code }) - }) - .then(response => { - if (response.status == 200) - return response.json(); - else - return null; - }) - .then(data => { - if (data){ + + console.log("code:" + code); + try { + const response = await postRequest('/callback/', { code }); + + if (response && response.status === 200) { + const data = await response.json(); if (data.is_2FA) { - // email 전송 요청 - fetch('https://localhost:443/api/send-mail/',{ - method: 'GET', - credentials: 'include', // 쿠키를 포함하여 요청 - headers: { - 'Content-Type': 'application/json' - } - }) - .then(response => { - if (response.status == 200) - return changeUrl("/2FA", false); - else - return changeUrl("/", false); - }); + const mailResponse = await getRequest('/send-mail/'); + + if (mailResponse && mailResponse.status === 200) { + return changeUrl("/2FA", false); + } else { + return changeUrl("/", false); + } } else { - // API!!! jwt가 있으면 해당 유저의 데이터베이스에서 언어 번호 (0 or 1 or 2) 얻어오기 - fetch("https://localhost:443/api/language/", { - method: 'GET', - credentials: 'include', // 쿠키를 포함하여 요청 (사용자 인증 필요 시) - }) - .then(response => { - if (!response.ok){ - changeUrl("/"); - return null; - } - return response.json(); - }) - .then(data => { - if (data){ - console.log(data.language); - root.lan.value = data.language; - changeUrl('/main'); // 메인 페이지로 이동 - } - }); + const langResponse = await getRequest('/language/'); + + if (!langResponse || !langResponse.ok) { + changeUrl("/"); + return null; + } + + const langData = await langResponse.json(); + if (langData) { + console.log(langData.language); + root.lan.value = langData.language; + changeUrl('/main'); + return null; + } } + } else { + return changeUrl("/", false); } - else return changeUrl("/", false); - }) - .catch(error => { + } catch (error) { console.error('Error:', error); - }); - return ; + return changeUrl("/error"); + } } - const isAuthenticated = await checkAuth(); - if ((path === "/" || path === "/2FA") && isAuthenticated) { - return changeUrl("/main"); // /로 이동할 때 인증되어 있으면 /main으로 이동, replaceState 사용 - } else if ((path !== "/" && path !== "/2FA") && !isAuthenticated) { - return changeUrl("/"); // /를 제외한 다른 경로로 이동할 때 인증되지 않은 경우 /로 이동, replaceState 사용 + try { + const isAuthenticated = await checkAuth(); + if ((path === "/" || path === "/2FA") && isAuthenticated) { + return changeUrl("/main"); // /로 이동할 때 인증되어 있으면 /main으로 이동, replaceState 사용 + } else if ((path !== "/" && path !== "/2FA") && !isAuthenticated) { + return changeUrl("/", false); // /를 제외한 다른 경로로 이동할 때 인증되지 않은 경우 /로 이동, replaceState 사용 + } + } catch (error) { + console.error('Error:', error); + return changeUrl("/error"); } const routeKeys = Object.keys(routes); @@ -172,39 +159,13 @@ export async function parsePath(path) { changeUrl("/404", false); } -export const initializeRouter = () => { +export const initializeRouter = async () => { window.addEventListener("popstate", async () => { await parsePath(window.location.pathname); }); - fetch("https://localhost:443/api/language/", { - method: 'GET', - credentials: 'include', // 쿠키를 포함하여 요청 (사용자 인증 필요 시) - }) - .then(response => { - if (!response.ok){ - console.log("so bad"); - return null; - } - return response.json(); - }) - .then(data => { - if (data){ - root.lan.value = data.language; - } - parsePath(window.location.pathname); - }); }; async function checkAuth() { - try { - const response = await fetch('https://localhost:443/api/validate/', { - method: 'GET', - credentials: 'include', // 쿠키를 포함하여 요청 - }); - - return response.ok; // 상태가 200~299 범위에 있으면 true, 그렇지 않으면 false 반환 - } catch (error) { - console.error('Error:', error); - return false; - } + const response = await getRequest('/validate/'); + return response ? response.ok : false; } diff --git a/frontend/src/core/showLoading.js b/frontend/src/core/showLoading.js new file mode 100644 index 0000000..f7f850c --- /dev/null +++ b/frontend/src/core/showLoading.js @@ -0,0 +1,50 @@ +import { initializeRouter, changeUrl, parsePath } from "./router.js"; +import { getCookie } from "./jwt.js"; +import { createLoadingElement, addLoadingStyles } from "./loadingComponents.js"; + +const WEBSOCKET_URL = 'wss://localhost:443/ws/online/'; + +export const closeAllSockets = (socketList) => { + socketList.forEach(socket => { + if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) { + socket.close(); + } + }); + socketList.length = 0; // 배열 비우기 +}; + +const online = async (socketList) => { + const token = getCookie("jwt"); + if (!token) { + changeUrl("/", false); + return; + } + const onlineSocket = new WebSocket(WEBSOCKET_URL); + socketList.push(onlineSocket); + + onlineSocket.onopen = () => { + onlineSocket.send(JSON.stringify({ action: 'authenticate', token })); + console.log("online socket opened"); + }; + onlineSocket.onclose = () => { + console.log("online socket closed"); + closeAllSockets(socketList); + changeUrl("/error", false); + }; +}; + +export const showLoading = async (routes, socketList) => { + const loadingElement = createLoadingElement(); + document.body.appendChild(loadingElement); + addLoadingStyles(); + + await Promise.all([ + initializeRouter(routes), + parsePath(window.location.pathname), + changeUrl("/main", false) + ]); + setTimeout(async () => { + await online(socketList); + document.body.removeChild(loadingElement); + }, 1000); +}; \ No newline at end of file diff --git a/frontend/src/utils.js b/frontend/src/utils.js new file mode 100644 index 0000000..0f04d32 --- /dev/null +++ b/frontend/src/utils.js @@ -0,0 +1,31 @@ +export const API_BASE_URL = 'https://localhost:443/api'; + +export async function getRequest(endpoint) { + try { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + method: 'GET', + credentials: 'include', + }); + return response; + } catch (error) { + console.error('Error:', error); + return null; + } +} + +export async function postRequest(endpoint, body) { + try { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(body) + }); + return response; + } catch (error) { + console.error('Error:', error); + return null; + } +} \ No newline at end of file