diff --git a/package-lock.json b/package-lock.json index d458cbc..30639fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/react-fontawesome": "^0.2.0", + "@hookform/resolvers": "^3.3.4", "axios": "^1.6.8", "date-fns": "^3.6.0", "next": "13.5.6", @@ -17,9 +18,11 @@ "react-color": "^2.19.3", "react-datepicker": "^6.9.0", "react-dom": "^18", + "react-hook-form": "^7.51.4", "react-intersection-observer": "^9.10.0", "sass": "^1.75.0", - "stylelint-config-standard": "^36.0.0" + "stylelint-config-standard": "^36.0.0", + "yup": "^1.4.0" }, "devDependencies": { "@types/node": "^20", @@ -397,6 +400,14 @@ "react": ">=16.3" } }, + "node_modules/@hookform/resolvers": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.4.tgz", + "integrity": "sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -3706,6 +3717,11 @@ "react-is": "^16.13.1" } }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -3794,6 +3810,21 @@ "react": "^18.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.51.4", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.4.tgz", + "integrity": "sha512-V14i8SEkh+V1gs6YtD0hdHYnoL4tp/HX/A45wWQN15CYr9bFRmmRdYStSO5L65lCCZRF+kYiSKhm9alqbcdiVA==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-intersection-observer": { "version": "9.10.0", "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.10.0.tgz", @@ -4717,6 +4748,11 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" + }, "node_modules/tinycolor2": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", @@ -4733,6 +4769,11 @@ "node": ">=8.0" } }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, "node_modules/ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -5036,6 +5077,28 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yup": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.4.0.tgz", + "integrity": "sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, + "node_modules/yup/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index cee095c..5c6f0a7 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/react-fontawesome": "^0.2.0", + "@hookform/resolvers": "^3.3.4", "axios": "^1.6.8", "date-fns": "^3.6.0", "next": "13.5.6", @@ -18,9 +19,11 @@ "react-color": "^2.19.3", "react-datepicker": "^6.9.0", "react-dom": "^18", + "react-hook-form": "^7.51.4", "react-intersection-observer": "^9.10.0", "sass": "^1.75.0", - "stylelint-config-standard": "^36.0.0" + "stylelint-config-standard": "^36.0.0", + "yup": "^1.4.0" }, "devDependencies": { "@types/node": "^20", diff --git a/src/apis/authService.ts b/src/apis/authService.ts index 7406436..96f6e3b 100644 --- a/src/apis/authService.ts +++ b/src/apis/authService.ts @@ -1,5 +1,6 @@ -import httpClient from './httpClient'; -import { AxiosError } from 'axios'; +import createHttpClient from './createHttpClient'; + +const httpClient = createHttpClient(); interface UserData { email: string; @@ -12,32 +13,7 @@ interface Credentials { password: string; } -// 회원가입 -export const registerUser = async (userData: UserData) => { - try { - const response = await httpClient.post('/users', userData); - return response.data; - } catch (error) { - const typedError = error as AxiosError; // 타입 단언 - if (typedError.response) { - throw typedError.response.data; // 오류 응답 바디 접근 - } else { - throw new Error('An unknown error occurred'); - } - } -}; - -// 로그인 -export const login = async (credentials: Credentials) => { - try { - const response = await httpClient.post('/auth/login', credentials); - return response.data; - } catch (error) { - const typedError = error as AxiosError; - if (typedError.response) { - throw typedError.response.data || 'Login failed'; - } else { - throw new Error('Network error'); - } - } +export const authService = { + registerUser: async (userData: UserData) => await httpClient.post('/users', userData), + login: async (credentials: Credentials) => await httpClient.post('/auth/login', credentials), }; diff --git a/src/apis/createHttpClient.ts b/src/apis/createHttpClient.ts index 2dd6993..8c67bdc 100644 --- a/src/apis/createHttpClient.ts +++ b/src/apis/createHttpClient.ts @@ -1,9 +1,9 @@ import httpClient from './httpClient'; export const createHttpClient = () => { - async function get(url: string) { + async function get(url: string, options?: { params?: any; headers?: any }) { try { - const response = await httpClient.get(url); + const response = await httpClient.get(url, options); return response.data; } catch (error) { // location.href = '../pages/NotFoundPage'; @@ -16,8 +16,8 @@ export const createHttpClient = () => { return response.data; } - async function put(url: string, data: P) { - const response = await httpClient.put(url, data); + async function put(url: string, data: P, options?: { headers?: any }) { + const response = await httpClient.put(url, data, options); return response.data; } diff --git a/src/apis/invitationService.ts b/src/apis/invitationService.ts index b2f365d..78cf28a 100644 --- a/src/apis/invitationService.ts +++ b/src/apis/invitationService.ts @@ -1,4 +1,7 @@ -import httpClient from './httpClient'; +import createHttpClient from './createHttpClient'; +import { InvitationResponse } from '@/src/apis/schema/dashboardResponse'; + +const httpClient = createHttpClient(); interface FetchInvitationsParams { teamId: string; @@ -13,38 +16,14 @@ interface UpdateInvitationParams { accept: boolean; } -// 초대된 대시보듬 목록 GET -export const fetchInvitations = async (params: FetchInvitationsParams) => { - const { teamId, size, cursorId, title } = params; - try { - const response = await httpClient.get(`/invitations`, { - params: { - teamId, - size, - cursorId, - title, - }, - }); - console.log(response.data); - return response.data; - } catch (error) { - console.error('Failed to load invitations', error); - throw error; - } -}; - -// 수락, 거절 버튼 클릭 시 PUT -export const updateInvitation = async ({ teamId, invitationId, accept }: UpdateInvitationParams) => { - const body = { inviteAccepted: accept }; - try { - const response = await httpClient.put(`/invitations/${invitationId}`, body, { - params: { - teamId, - }, - }); - return response.data; - } catch (error) { - console.error('Error updating invitation', error); - throw error; - } +export const invitationService = { + fetchInvitations: async (params: FetchInvitationsParams) => + await httpClient.get('/invitations', { params }), + updateInvitation: async ({ teamId, invitationId, accept }: UpdateInvitationParams) => + await httpClient.put( + `/invitations/${invitationId}?teamId=${teamId}`, + { + inviteAccepted: accept, + } + ), }; diff --git a/src/apis/myDashboardService.ts b/src/apis/myDashboardService.ts index 59d4dc8..79eb5f8 100644 --- a/src/apis/myDashboardService.ts +++ b/src/apis/myDashboardService.ts @@ -1,7 +1,8 @@ -import axios from 'axios'; -import httpClient from './httpClient'; +import createHttpClient from './createHttpClient'; +import { DashboardListResponse } from '@/src/apis/schema/dashboardResponse'; +const httpClient = createHttpClient(); -interface fetchDashboardsParams { +interface FetchDashboardsParams { teamId: string; navigationMethod: 'infiniteScroll' | 'pagination'; cursorId?: number; @@ -10,22 +11,7 @@ interface fetchDashboardsParams { } // 대시보드 목록 불러오기 -export const fetchDashboards = async (params: fetchDashboardsParams) => { - try { - const { teamId, navigationMethod, cursorId, page, size } = params; - const response = await httpClient.get('/dashboards', { - params: { - teamId, - navigationMethod, - cursorId, - page, - size, - }, - }); - console.log(response.data); - return response.data; - } catch (error) { - console.error('대시보드 데이터를 불러오는데 실패했습니다:', error); - throw error; - } +export const myDashboardService = { + fetchDashboards: async (params: FetchDashboardsParams) => + await httpClient.get('/dashboards', { params }), }; diff --git a/src/apis/schema/dashboardResponse.ts b/src/apis/schema/dashboardResponse.ts index c975cd6..f76187b 100644 --- a/src/apis/schema/dashboardResponse.ts +++ b/src/apis/schema/dashboardResponse.ts @@ -87,3 +87,31 @@ export interface UserInfoResponse { createdAt: string; updatedAt: string; } + +export interface Invitation { + id: number; + inviter: { + nickname: string; + email: string; + id: number; + }; + teamId: string; + dashboard: { + title: string; + id: number; + }; + invitee: { + nickname: string; + email: string; + id: number; + }; + inviteAccepted: boolean; + createdAt: string; + updatedAt: string; +} + +export interface InvitationResponse { + totalCount: number; + cursorId: any; + invitations: Invitation[]; +} diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx index 0e9535d..6bac87c 100644 --- a/src/pages/Login/index.tsx +++ b/src/pages/Login/index.tsx @@ -2,7 +2,7 @@ import styles from './Login.module.scss'; import Input from '@/src/components/common/Input'; import BaseButton from '@/src/components/common/Button/BaseButton'; import React, { useState, useEffect } from 'react'; -import { login } from '@/src/apis/authService'; +import { authService } from '@/src/apis/authService'; import { saveTokenToLocalStorage } from '@/src/utils/authUtils'; import Logo from '@/src/assets/images/Logo.png'; import Title from '@/src/assets/images/Title.png'; @@ -81,7 +81,7 @@ export default function Login() { e.preventDefault(); if (!isSubmitEnabled) return; try { - const data = await login(formData); + const data = (await authService.login(formData)) as any; console.log('로그인 성공', data); saveTokenToLocalStorage(data.accessToken); window.location.href = '/Mydashboard'; diff --git a/src/pages/Mydashboard/index.tsx b/src/pages/Mydashboard/index.tsx index e6f150e..69a7157 100644 --- a/src/pages/Mydashboard/index.tsx +++ b/src/pages/Mydashboard/index.tsx @@ -2,10 +2,10 @@ import DashboardButton from '@/src/components/common/Button/DashboardButton'; import styles from './Mydashboard.module.scss'; import React, { useState, useEffect, ReactElement, use } from 'react'; import DashboardLinkButton, { dashboardData } from '@/src/components/common/Button/DashboardLinkButton'; -import { fetchDashboards } from '@/src/apis/myDashboardService'; +import { myDashboardService } from '@/src/apis/myDashboardService'; import PagenationButton from '@/src/components/common/Button/PagenationButton'; import TaskButton from '@/src/components/common/Button/TaskButton'; -import { fetchInvitations, updateInvitation } from '@/src/apis/invitationService'; +import { invitationService } from '@/src/apis/invitationService'; import { NextPageWithLayout } from '../_app'; import HeaderSidebarLayout from '@/src/components/common/Layout/HeaderSidebarLayout'; import { useRouter } from 'next/router'; @@ -14,33 +14,7 @@ import DoubleButtonModal from '@/src/components/Modal/DoubleButtonModal'; import Image from 'next/image'; import search from '@/src/assets/icons/search.svg'; import unsubscribe from '@/src/assets/icons/unsubscribe.svg'; - -interface Invitation { - id: number; - inviter: { - nickname: string; - email: string; - id: number; - }; - teamId: string; - dashboard: { - title: string; - id: number; - }; - invitee: { - nickname: string; - email: string; - id: number; - }; - inviteAccepted: boolean; - createdAt: string; - updatedAt: string; -} - -interface InvitationResponse { - totalCount: number; - invitations: Invitation[]; -} +import { Invitation, InvitationResponse } from '@/src/apis/schema/dashboardResponse'; const Mydashboard: NextPageWithLayout = () => { const router = useRouter(); @@ -102,7 +76,7 @@ const Mydashboard: NextPageWithLayout = () => { page: page, size: 5, }; - const data = await fetchDashboards(params); + const data = await myDashboardService.fetchDashboards(params); setDashboards(data.dashboards); setTotalPages(Math.ceil(data.totalCount / params.size)); }; @@ -118,7 +92,7 @@ const Mydashboard: NextPageWithLayout = () => { cursorId: cursor, // 무한 스크롤 위해 다른 데이터 배치를 가져오는데 사용됨 }; try { - const data = await fetchInvitations(params); + const data = await invitationService.fetchInvitations(params); const newInvitations = data.invitations; // Set을 사용하여 id 기반으로 중복 제거 const uniqueInvitations = new Map(invitations.concat(newInvitations).map(inv => [inv.id, inv])); @@ -136,11 +110,24 @@ const Mydashboard: NextPageWithLayout = () => { // 초대 수락 const acceptInvitation = async (invitationId: number) => { try { - const response = await updateInvitation({ teamId: '4-16', invitationId, accept: true }); - setInvitations(current => current.filter(inv => inv.id !== invitationId)); + await invitationService.updateInvitation({ teamId: '4-16', invitationId, accept: true }); + const acceptedInvitation = invitations.find(inv => inv.id === invitationId); + if (!acceptedInvitation) { + throw new Error('Invitation not found'); + } + setInvitations(current => current.filter(inv => inv.id !== invitationId)); setDashboards(currentDashboards => { - const newDashboards = [response.dashboard, ...currentDashboards]; + const newDashboard: dashboardData = { + id: acceptedInvitation.dashboard.id, + title: acceptedInvitation.dashboard.title, + color: 'defaultColor', // 적절한 기본값 설정 + userId: acceptedInvitation.invitee.id, // 가정: 초대받은 사용자 ID 사용 + createdAt: acceptedInvitation.createdAt, // 초대 정보에서 가져온 시간 사용 + updatedAt: acceptedInvitation.updatedAt, // 초대 정보에서 가져온 시간 사용 + createdByMe: false, // 가정: 현재 사용자가 생성하지 않음 + }; + const newDashboards = [newDashboard, ...currentDashboards]; const newTotalPages = Math.ceil(newDashboards.length / MAX_DASHBOARD_PER_PAGE); // 총 페이지 수 업데이트 @@ -158,7 +145,7 @@ const Mydashboard: NextPageWithLayout = () => { // 초대 거절 const rejectInvitation = async (invitationId: number) => { try { - const response = await updateInvitation({ teamId: '4-16', invitationId, accept: false }); + const response = await invitationService.updateInvitation({ teamId: '4-16', invitationId, accept: false }); setInvitations(current => current.filter(inv => inv.id !== invitationId)); } catch (error) { console.error('Failed to reject invitation', error); diff --git a/src/pages/SignUp/index.tsx b/src/pages/SignUp/index.tsx index 54c4b53..55b3fa4 100644 --- a/src/pages/SignUp/index.tsx +++ b/src/pages/SignUp/index.tsx @@ -1,7 +1,7 @@ import Input from '@/src/components/common/Input'; import styles from './SignUp.module.scss'; import React, { useState, useEffect, FormEvent } from 'react'; -import { registerUser } from '@/src/apis/authService'; +import { authService } from '@/src/apis/authService'; import BaseButton from '@/src/components/common/Button/BaseButton'; import { faL } from '@fortawesome/free-solid-svg-icons'; import Logo from '@/src/assets/images/Logo.png'; @@ -125,7 +125,7 @@ export default function SignUp() { if (isSubmitEnabled) { // 서버에 데이터 전송 로직 try { - const result = await registerUser(formData); + const result = await authService.registerUser(formData); console.log('Registration successful:', result); alert('가입이 완료되었습니다'); window.location.href = '/Login';