From 6ce4a28c335521dd2069a88bf9ac5257f50e6ea1 Mon Sep 17 00:00:00 2001 From: kiyeong Date: Sun, 1 Dec 2024 00:49:17 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=EC=B9=9C=EA=B5=AC=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 +- pnpm-lock.yaml | 92 +++++++++++++---- src/Friends/Friends.tsx | 219 +++++++++++++++++++++++++++++++++++++++- src/routes/Routes.tsx | 54 ++++++---- 4 files changed, 319 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index 92f6519..c36e3ef 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,13 @@ "framer-motion": "11.0.3", "react": "18.2.0", "react-dom": "18.2.0", - "react-router-dom": "^6.28.0" + "react-router-dom": "^7.0.1" }, "devDependencies": { "@eslint/js": "^9.13.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", + "@types/react-router-dom": "^5.3.3", "@vitejs/plugin-react": "^4.3.3", "eslint": "^9.13.0", "eslint-plugin-react-hooks": "^5.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e5c79e6..5ef678d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,8 +27,8 @@ importers: specifier: 18.2.0 version: 18.2.0(react@18.2.0) react-router-dom: - specifier: ^6.28.0 - version: 6.28.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + specifier: ^7.0.1 + version: 7.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) devDependencies: '@eslint/js': specifier: ^9.13.0 @@ -39,6 +39,9 @@ importers: '@types/react-dom': specifier: ^18.3.1 version: 18.3.1 + '@types/react-router-dom': + specifier: ^5.3.3 + version: 5.3.3 '@vitejs/plugin-react': specifier: ^4.3.3 version: 4.3.3(vite@5.4.11) @@ -917,10 +920,6 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - '@remix-run/router@1.21.0': - resolution: {integrity: sha512-xfSkCAchbdG5PnbrKqFWwia4Bi61nH+wm8wLEqfHDyp7Y3dZzgqS2itV8i4gAq9pC2HsTpwyBC6Ds8VHZ96JlA==} - engines: {node: '>=14.0.0'} - '@rollup/rollup-android-arm-eabi@4.27.2': resolution: {integrity: sha512-Tj+j7Pyzd15wAdSJswvs5CJzJNV+qqSUcr/aCD+jpQSBtXvGnV0pnrjoc8zFTe9fcKCatkpFpOO7yAzpO998HA==} cpu: [arm] @@ -1023,9 +1022,15 @@ packages: '@types/babel__traverse@7.20.6': resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/history@4.7.11': + resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1044,6 +1049,12 @@ packages: '@types/react-dom@18.3.1': resolution: {integrity: sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==} + '@types/react-router-dom@5.3.3': + resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==} + + '@types/react-router@5.1.20': + resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} + '@types/react@18.3.12': resolution: {integrity: sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==} @@ -1208,6 +1219,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + copy-to-clipboard@3.3.3: resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} @@ -1681,18 +1696,22 @@ packages: '@types/react': optional: true - react-router-dom@6.28.0: - resolution: {integrity: sha512-kQ7Unsl5YdyOltsPGl31zOjLrDv+m2VcIEcIHqYYD3Lp0UppLjrzcfJqDJwXxFw3TH/yvapbnUvPlAj7Kx5nbg==} - engines: {node: '>=14.0.0'} + react-router-dom@7.0.1: + resolution: {integrity: sha512-duBzwAAiIabhFPZfDjcYpJ+f08TMbPMETgq254GWne2NW1ZwRHhZLj7tpSp8KGb7JvZzlLcjGUnqLxpZQVEPng==} + engines: {node: '>=20.0.0'} peerDependencies: - react: '>=16.8' - react-dom: '>=16.8' + react: '>=18' + react-dom: '>=18' - react-router@6.28.0: - resolution: {integrity: sha512-HrYdIFqdrnhDw0PqG/AKjAqEqM7AvxCz0DQ4h2W8k6nqmc5uRBYDag0SBxx9iYz5G8gnuNVLzUe13wl9eAsXXg==} - engines: {node: '>=14.0.0'} + react-router@7.0.1: + resolution: {integrity: sha512-WVAhv9oWCNsja5AkK6KLpXJDSJCQizOIyOd4vvB/+eHGbYx5vkhcmcmwWjQ9yqkRClogi+xjEg9fNEOd5EX/tw==} + engines: {node: '>=20.0.0'} peerDependencies: - react: '>=16.8' + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true react-style-singleton@2.2.1: resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} @@ -1743,6 +1762,9 @@ packages: engines: {node: '>=10'} hasBin: true + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1796,6 +1818,9 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + turbo-stream@2.4.0: + resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -2964,8 +2989,6 @@ snapshots: '@popperjs/core@2.11.8': {} - '@remix-run/router@1.21.0': {} - '@rollup/rollup-android-arm-eabi@4.27.2': optional: true @@ -3041,8 +3064,12 @@ snapshots: dependencies: '@babel/types': 7.26.0 + '@types/cookie@0.6.0': {} + '@types/estree@1.0.6': {} + '@types/history@4.7.11': {} + '@types/json-schema@7.0.15': {} '@types/lodash.mergewith@4.6.7': @@ -3059,6 +3086,17 @@ snapshots: dependencies: '@types/react': 18.3.12 + '@types/react-router-dom@5.3.3': + dependencies: + '@types/history': 4.7.11 + '@types/react': 18.3.12 + '@types/react-router': 5.1.20 + + '@types/react-router@5.1.20': + dependencies: + '@types/history': 4.7.11 + '@types/react': 18.3.12 + '@types/react@18.3.12': dependencies: '@types/prop-types': 15.7.13 @@ -3254,6 +3292,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie@1.0.2: {} + copy-to-clipboard@3.3.3: dependencies: toggle-selection: 1.0.6 @@ -3710,17 +3750,21 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 - react-router-dom@6.28.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + react-router-dom@7.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: - '@remix-run/router': 1.21.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-router: 6.28.0(react@18.2.0) + react-router: 7.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - react-router@6.28.0(react@18.2.0): + react-router@7.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: - '@remix-run/router': 1.21.0 + '@types/cookie': 0.6.0 + cookie: 1.0.2 react: 18.2.0 + set-cookie-parser: 2.7.1 + turbo-stream: 2.4.0 + optionalDependencies: + react-dom: 18.2.0(react@18.2.0) react-style-singleton@2.2.1(@types/react@18.3.12)(react@18.2.0): dependencies: @@ -3783,6 +3827,8 @@ snapshots: semver@7.6.3: {} + set-cookie-parser@2.7.1: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -3819,6 +3865,8 @@ snapshots: tslib@2.8.1: {} + turbo-stream@2.4.0: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 diff --git a/src/Friends/Friends.tsx b/src/Friends/Friends.tsx index d71964d..b399533 100644 --- a/src/Friends/Friends.tsx +++ b/src/Friends/Friends.tsx @@ -1,7 +1,133 @@ -import React from "react"; -import { Box, Heading, ChakraProvider } from "@chakra-ui/react"; +import React, { useState, useEffect } from "react"; +import { + Box, + Heading, + ChakraProvider, + Button, + Input, + VStack, + Text, + useToast, + Stack, + Divider, + InputGroup, + InputRightElement, + IconButton, +} from "@chakra-ui/react"; +import { ArrowForwardIcon } from "@chakra-ui/icons"; +import api from "../api/interceptor"; const Friends: React.FC = () => { + const [view, setView] = useState<"list" | "pending">("list"); + const [friends, setFriends] = useState([]); + const [pendingRequests, setPendingRequests] = useState([]); + const [email, setEmail] = useState(""); + const toast = useToast(); + + useEffect(() => { + const fetchFriends = async () => { + try { + const response = await api.get("/api/friend"); + setFriends(response.data); + } catch (error) { + console.error("친구 목록을 가져오는 중 오류 발생:", error); + } + }; + fetchFriends(); + }, []); + + useEffect(() => { + const fetchPendingRequests = async () => { + try { + const response = await api.get("/api/friend/pending"); + setPendingRequests(response.data); + } catch (error) { + console.error("대기 중인 친구 요청을 가져오는 중 오류 발생:", error); + } + }; + fetchPendingRequests(); + }, []); + + const sendFriendRequest = async () => { + if (!email) return; // 이메일이 없으면 전송 안 함 + try { + await api.post("/api/friend", { email }); + toast({ + title: "친구 요청 성공!", + description: `${email}님에게 친구 요청을 보냈습니다.`, + status: "success", + duration: 1500, + isClosable: true, + position: "top", + }); + setEmail(""); // 입력창 초기화 + } catch (error: any) { + if (error.response?.status === 404) { + toast({ + title: "에러", + description: "해당 이메일을 가진 사용자를 찾을 수 없습니다.", + status: "error", + duration: 1500, + isClosable: true, + position: "top", + }); + } else if (error.response?.status === 400) { + toast({ + title: "에러", + description: "본인에게는 친구 요청을 할 수 없습니다.", + status: "error", + duration: 1500, + isClosable: true, + position: "top", + }); + } else { + console.error("친구 요청 중 오류 발생:", error); + } + } + }; + + const acceptRequest = async (id: number) => { + try { + await api.put(`/api/friend/accept/${id}`); + toast({ + title: "친구 요청 수락", + description: "친구 요청을 수락했습니다.", + status: "success", + duration: 1500, + isClosable: true, + }); + setPendingRequests((prev) => + prev.filter((request) => request.friendRequestId !== id) + ); + } catch (error) { + console.error("친구 요청 수락 중 오류 발생:", error); + } + }; + + const rejectRequest = async (id: number) => { + try { + await api.delete(`/api/friend/${id}`); + toast({ + title: "친구 요청 거절", + description: "친구 요청을 거절했습니다.", + status: "info", + duration: 1500, + isClosable: true, + }); + setPendingRequests((prev) => + prev.filter((request) => request.friendRequestId !== id) + ); + } catch (error) { + console.error("친구 요청 거절 중 오류 발생:", error); + } + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + sendFriendRequest(); + } + }; + return ( { > - - 친구 관리 페이지입니다. + + 친구 관리 페이지 + + + + + + {view === "list" ? ( + + + + setEmail(e.target.value)} + onKeyPress={handleKeyPress} // 엔터키 핸들링 + /> + + } + onClick={sendFriendRequest} // 버튼 클릭 시 요청 전송 + colorScheme="blue" + /> + + + + + {friends.map((friend) => ( + + {friend.friendName} + {friend.friendEmail} + + ))} + + ) : ( + + {pendingRequests.map((request) => ( + + {request.requesterName} + {request.requesterEmail} + + + + + + ))} + + )} diff --git a/src/routes/Routes.tsx b/src/routes/Routes.tsx index 9be8715..aa3a3a8 100644 --- a/src/routes/Routes.tsx +++ b/src/routes/Routes.tsx @@ -7,28 +7,40 @@ import Friends from "../Friends/Friends"; import { RouterPath } from "./path"; // 경로 상수 가져오기 // 라우터 정의 -const router = createBrowserRouter([ +const router = createBrowserRouter( + [ + { + path: RouterPath.root, // 루트 경로 + element: , // 시작 페이지를 직접 렌더링 + }, + { + path: RouterPath.rediretcion, // 리다이렉션 페이지 경로 + element: , // 리다이렉션 페이지를 직접 렌더링 + }, + { + path: RouterPath.main, // 메인 페이지 경로 + element: , // 메인 페이지를 직접 렌더링 + }, + { + path: RouterPath.mypage, // 마이페이지 경로 + element: , // 마이페이지를 직접 렌더링 + }, + { + path: RouterPath.friends, // 친구 페이지 경로 + element: , // 친구 페이지를 직접 렌더링 + }, + ], { - path: RouterPath.root, // 루트 경로 - element: , // 시작 페이지를 직접 렌더링 - }, - { - path: RouterPath.rediretcion, // 리다이렉션 페이지 경로 - element: , // 리다이렉션 페이지를 직접 렌더링 - }, - { - path: RouterPath.main, // 메인 페이지 경로 - element: , // 메인 페이지를 직접 렌더링 - }, - { - path: RouterPath.mypage, // 마이페이지 경로 - element: , // 마이페이지를 직접 렌더링 - }, - { - path: RouterPath.friends, // 친구 페이지 경로 - element: , // 친구 페이지를 직접 렌더링 - }, -]); + future: { + v7_startTransition: true, // v7에서 React.startTransition 사용 + v7_relativeSplatPath: true, // Splat 경로 처리 방식 업데이트 + v7_fetcherPersist: true, // Fetcher 지속성 변경 + v7_normalizeFormMethod: true, // Form Method 대소문자 통일 + v7_partialHydration: true, // Hydration 동작 변경 + v7_skipActionErrorRevalidation: true, // Action 오류 처리 변경 + }, + } +); // 라우터를 렌더링하는 컴포넌트 export const Routes = () => {