diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..f848ff3dd --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules/ +/.pnp +.pnp.js +.env +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +node_modules +yarn-error.log +*.json + +*.env diff --git a/package.json b/package.json new file mode 100644 index 000000000..01136a91a --- /dev/null +++ b/package.json @@ -0,0 +1,66 @@ +{ + "name": "gnims-pwa", + "version": "0.1.0", + "private": true, + "dependencies": { + "@reduxjs/toolkit": "^1.9.2", + "@testing-library/jest-dom": "^5.16.1", + "@testing-library/react": "^13.0.0", + "@testing-library/user-event": "^13.5.0", + "axios": "^1.3.2", + "event-source-polyfill": "^1.0.31", + "html-webpack-plugin": "^5.5.0", + "json-server": "^0.17.1", + "picker": "^0.1.4", + "react": "^18.2.0", + "react-date": "^2.0.0", + "react-datepicker": "^4.10.0", + "react-dom": "^18.2.0", + "react-modal": "^3.16.1", + "react-redux": "^8.0.5", + "react-router-dom": "^6.8.1", + "react-scripts": "5.0.1", + "react-spinners": "^0.13.8", + "redux": "^4.2.1", + "styled-components": "^5.3.6", + "styled-reset": "^4.4.5", + "tailwindcss": "^3.2.6", + "web-vitals": "^2.1.4", + "workbox-background-sync": "^6.4.2", + "workbox-broadcast-update": "^6.4.2", + "workbox-cacheable-response": "^6.4.2", + "workbox-core": "^6.4.2", + "workbox-expiration": "^6.4.2", + "workbox-google-analytics": "^6.4.2", + "workbox-navigation-preload": "^6.4.2", + "workbox-precaching": "^6.4.2", + "workbox-range-requests": "^6.4.2", + "workbox-routing": "^6.4.2", + "workbox-strategies": "^6.4.2", + "workbox-streams": "^6.4.2" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 000000000..65d6cc856 --- /dev/null +++ b/public/index.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + React App + + + +
+ + + diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 000000000..f9051fe71 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,8 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/src/App.js b/src/App.js new file mode 100644 index 000000000..a1874532a --- /dev/null +++ b/src/App.js @@ -0,0 +1,14 @@ +import React from "react"; +import Router from "./shared/Router"; +import GlobalStyles from "./styles/GlobalStyle"; + +function App() { + return ( + <> + + + + ); +} + +export default App; diff --git a/src/App.test.js b/src/App.test.js new file mode 100644 index 000000000..2a68616d9 --- /dev/null +++ b/src/App.test.js @@ -0,0 +1,9 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import App from './App'; + +test('renders learn react link', () => { + render(); + const linkElement = screen.getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/src/api/LoginApi.jsx b/src/api/LoginApi.jsx new file mode 100644 index 000000000..63b7b99ae --- /dev/null +++ b/src/api/LoginApi.jsx @@ -0,0 +1,21 @@ +import { instance } from "../shared/AxiosInstance"; + +export const LoginApi = { + EmailLogin: (payload) => { + const data = instance.post("/auth/login", payload); + return data; + }, + + KakaoLogin: async (payload) => { + console.log("카카오 페이로드", payload); + return await instance.post("kakao/login", payload); + }, + + SendEmailAuthenticationNumber: async (payload) => { + return await instance.post("/auth/password", payload); + }, + + SendAuthenticationNumber: async (payload) => { + return await instance.patch("/auth/code", payload); + }, +}; diff --git a/src/api/ScheduleApi.jsx b/src/api/ScheduleApi.jsx new file mode 100644 index 000000000..358f074f6 --- /dev/null +++ b/src/api/ScheduleApi.jsx @@ -0,0 +1,41 @@ +import { instance } from "../shared/AxiosInstance"; + +export const ScheduleApi = { + //스케줄 전체 조회 + getSccheduleApi: (payload) => { + //const data = instance.get(`/v2-dto/users/${payload}/events`); + const data = instance.get(`/users/${payload}/events`); + return data; + }, + + getInfiniteScrollPage: (payload) => { + const data = instance.get( + `/v2-page/users/${payload.userId}/events?page=${payload.page}&size=${3}` + ); + return data; + }, + + //스케줄 등록 + postScheduleApi: (payload) => { + const data = instance.post("/events", payload); + return data; + }, + //스케줄 수정 + editScheduleApi: (payload) => { + console.log("수정데이터?", payload); + const data = instance.put(`/events/${payload.eventId}`, payload.Schedule); + return data; + }, + + //지난일정 조회 + getPastScheduleApi: () => { + const data = instance.get("/events/past"); + return data; + }, + + //스케줄 삭제 + deleteScheduleApi: (payload) => { + const data = instance.delete(`/events/${payload}`); + return data; + }, +}; diff --git a/src/api/Signup.jsx b/src/api/Signup.jsx new file mode 100644 index 000000000..89da009c5 --- /dev/null +++ b/src/api/Signup.jsx @@ -0,0 +1,17 @@ +import { instance } from "../shared/AxiosInstance"; + +export const SignupApi = { + emailDoubleCheck: async (payload) => { + const { data } = await instance.post("/auth/email", payload); + return data; + }, + nickNameDoubleCheck: async (payload) => { + const { data } = await instance.post("/auth/nickname", payload); + return data; + }, + + Signup: async (payload) => { + const { data } = await instance.post("/auth/signup", payload); + return data; + }, +}; diff --git a/src/api/UserApi.jsx b/src/api/UserApi.jsx new file mode 100644 index 000000000..01857fdb0 --- /dev/null +++ b/src/api/UserApi.jsx @@ -0,0 +1,20 @@ +import { async } from "q"; +import { instance } from "../shared/AxiosInstance"; + +export const UserApi = { + userSearch: async (payload) => { + const { data } = await instance.get( + `/users/search?username=${payload}&page=${0}&size=${10}`, + payload + ); + return data; + }, + editProfile: async (payload) => { + const data = await instance.patch("users/profile", payload); + return data; + }, + passwordChange: async (payload) => { + const { data } = await instance.patch("/auth/password", payload); + return data; + }, +}; diff --git a/src/components/Schedule/ScheduleDetail.jsx b/src/components/Schedule/ScheduleDetail.jsx new file mode 100644 index 000000000..c96bcdfb7 --- /dev/null +++ b/src/components/Schedule/ScheduleDetail.jsx @@ -0,0 +1,136 @@ +import axios from "axios"; +import React, { memo, useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import kebab from "../../img/kebab.png"; +import BottomNavi from "../layout/BottomNavi"; +import KebabModal from "../modal/KebabButtonModal"; +import gnimsIcon from "../../img/gnimslogo1.png"; +import { useDispatch, useSelector } from "react-redux"; +import { __getScheduleDetail } from "../../redux/modules/ScheduleSlice"; +import schedulealoneIcon from "../../img/schedulealone.png"; + +const ScheduleDetail = () => { + const dispatch = useDispatch(); + const schedule = useSelector((state) => state.ScheduleSlice.oldschedules); + console.log("올드?", schedule); + // 모달 노출시키는 여부 + const [modalOpen, setModalOpen] = useState(false); + const showModalHandler = () => { + setModalOpen(true); + }; + //id구하기 + const { id } = useParams(); + console.log("이벤트아이디?", id); + + //데이터베이스를 담을 schedule + // const [schedule, setSchedule] = useState([]); + + // const fetchTodos = async () => { + // await instance.get(`/events/${id}`).then((appData) => { + // setSchedule(appData.data.data); + // }, []); + // }; + + const time = schedule.time?.split(":", 2).join(":"); + useEffect(() => { + dispatch(__getScheduleDetail(id)); + }, []); + console.log("디테일스케줄?", schedule); + // useEffect(() => { + // fetchTodos(); + // }, []); + + const joiner = schedule.invitees; + console.log(joiner); + const numberOfJoiner = joiner && joiner.length; + const hostId = schedule.hostId; + //isHidden은 해당 스케쥴이 본인의 스케쥴이 아닐 땐 케밥버튼이 보이지 않게하기 위해 쓰인다. 기본값은 flex이고, + let isHidden = ""; + //스케쥴의 참여자에 로그인한 본인의 닉네임이 포함되지 않으면 hidden값이 입혀진다. + if (hostId !== Number(sessionStorage.getItem("userId"))) { + isHidden = "hidden"; + } + console.log(hostId === Number(sessionStorage.getItem("userId"))); + console.log(hostId); + console.log(Number(sessionStorage.getItem("userId"))); + + return ( +
+
+
+ {/* 케밥모달이 열리면 bottomNavi는 사라집니다 */} + {modalOpen ? false : } +
+ {modalOpen && } +
+
+ 케밥메뉴 +
+
+
{schedule.date}
{time}
+
+
+ {schedule.subject} +
{" "} +
+ D- + {schedule.dday === 0 ?
DAY
:
{schedule.dday}
} +
+
+
+
+ {/* 참여자는 2명이상일 때부터 화면에 보입니다. */} + {numberOfJoiner !== 1 ? ( +
+ 참여자 +
+ {joiner && + joiner.map((a) => { + return ( + {a.username} + ); + })} +
+
+ ) : ( + false + )} +
+ {/* 일정의 내용이 없을 땐 화면에 보이지 않습니다. */} + {schedule.content ? ( +
+ 일정내용{" "} +
+ {schedule.content} +
+
+ ) : ( + false + )} + {!schedule.content && numberOfJoiner === 1 ? ( +
+ gnimslogo +
+ 혼자만의 일정이군요!
+ 때로는 개인시간도 중요한 법이죠. +
+
+ ) : null} +
+
+
+ ); +}; + +export default memo(ScheduleDetail); diff --git a/src/components/button/LoginButton.jsx b/src/components/button/LoginButton.jsx new file mode 100644 index 000000000..d532f50dd --- /dev/null +++ b/src/components/button/LoginButton.jsx @@ -0,0 +1,18 @@ +import React from "react"; + +const LoginButton = ({ onEvent, img }) => { + return ( +
+ 로그인버튼들 +
+ ); +}; + +export default LoginButton; diff --git a/src/components/follow/FollowList.jsx b/src/components/follow/FollowList.jsx new file mode 100644 index 000000000..8cf23ea55 --- /dev/null +++ b/src/components/follow/FollowList.jsx @@ -0,0 +1,81 @@ +import React, { useState, useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { __getFollower, __getFollowing } from "../../redux/modules/FollowSlice"; +import FollowerCard from "./FollowerCard"; +import FollowingCard from "./FollowingCard"; + +const FollowList = () => { + const dispatch = useDispatch(); + //탭 상태 변화 + const [activeTab, setActiveTab] = useState("follower"); + + //조건부 렌더링 설정하기 위한 스테이트 + const [bdColor, setBdColor] = useState({ + followerBD: "border-b-[1px] border-black", + followingBD: "", + }); + const followerList = useSelector((state) => state.FollowSlice.followerList); + const followingList = useSelector((state) => state.FollowSlice.followingList); + + useEffect(() => { + dispatch(__getFollower()); + }, [FollowList]); + + const handleTabChange = (tab) => { + setActiveTab(tab); + if (tab === "follower") { + // + setBdColor({ + followerBD: "border-b-[1px] border-black", + followingBD: "", + }); + dispatch(__getFollower()); + } else { + setBdColor({ + followerBD: "", + followingBD: "border-b-[1px] border-black", + }); + dispatch(__getFollowing()); + } + }; + + return ( +
+
+ + +
+
+ {activeTab === "follower" ? ( +
+ {followerList.map((follower) => { + return ( + + ); + })} +
+ ) : ( +
+ {followingList.map((following) => { + return ( + + ); + })} +
+ )} +
+
+ ); +}; + +export default FollowList; diff --git a/src/components/follow/FollowerCard.jsx b/src/components/follow/FollowerCard.jsx new file mode 100644 index 000000000..eba856ec5 --- /dev/null +++ b/src/components/follow/FollowerCard.jsx @@ -0,0 +1,56 @@ +import React, { useState } from "react"; +import { useDispatch } from "react-redux"; +import { useNavigate } from "react-router-dom"; +import { __postFollowState } from "../../redux/modules/FollowSlice"; + +const FollowerCard = ({ follower }) => { + const navigate = useNavigate(); + const dispatch = useDispatch(); + const [isFollowed, setIsFollowed] = useState( + follower.followStatus === "ACTIVE" || "INIT" + ); + + const [btnColor, setBtnColor] = useState( + follower.followStatus === "ACTIVE" || "INIT" ? "#A31414" : "#002C51" + ); + + const handleClick = (e) => { + dispatch(__postFollowState(follower.followId)); + setIsFollowed(!isFollowed); + if (isFollowed) setBtnColor("#002C51"); + else setBtnColor("#A31414"); + }; + + return ( +
+
{ + navigate(`/friends/${follower.followId}`); + sessionStorage.setItem("clickedUserName", follower.username); + sessionStorage.setItem("clickedUserImg", follower.profile); + }} + > +
+ 프로필 +
+
{follower.username}
+
+ {isFollowed ? ( +
+ {isFollowed ? "취소" : "팔로우"} +
+ ) : ( +
+ {isFollowed ? "취소" : "팔로우"} +
+ )} +
+ ); +}; + +export default FollowerCard; diff --git a/src/components/follow/FollowingCard.jsx b/src/components/follow/FollowingCard.jsx new file mode 100644 index 000000000..7612a9473 --- /dev/null +++ b/src/components/follow/FollowingCard.jsx @@ -0,0 +1,55 @@ +import React, { useState } from "react"; +import { useDispatch } from "react-redux"; +import { useNavigate } from "react-router-dom"; +import { __postFollowState } from "../../redux/modules/FollowSlice"; + +const FollowingCard = ({ following }) => { + const dispatch = useDispatch(); + const [isFollowed, setIsFollowed] = useState( + following.followStatus === "ACTIVE" || "INIT" + ); + const navigate = useNavigate(); + const [btnColor, setBtnColor] = useState( + following.followStatus === "ACTIVE" || "INIT" ? "#A31414" : "#002C51" + ); + + const handleClick = (e) => { + dispatch(__postFollowState(following.followId)); + setIsFollowed(!isFollowed); + if (isFollowed) setBtnColor("#002C51"); + else setBtnColor("#A31414"); + }; + + return ( +
+
{ + navigate(`/friends/${following.followId}`); + sessionStorage.setItem("clickedUserName", following.username); + sessionStorage.setItem("clickedUserImg", following.profile); + }} + > +
+ 프로필 +
+
{following.username}
+
+ {isFollowed ? ( +
+ {isFollowed ? "취소" : "팔로우"} +
+ ) : ( +
+ {isFollowed ? "취소" : "팔로우"} +
+ )} +
+ ); +}; + +export default FollowingCard; diff --git a/src/components/follow/SearchCards.jsx b/src/components/follow/SearchCards.jsx new file mode 100644 index 000000000..7f624600a --- /dev/null +++ b/src/components/follow/SearchCards.jsx @@ -0,0 +1,51 @@ +import React, { useState } from "react"; +import { useDispatch } from "react-redux"; +import { useNavigate } from "react-router-dom"; +import { __postFollowState } from "../../redux/modules/FollowSlice"; + +const SearchCards = ({ userInfo }) => { + const navigate = useNavigate(); + const dispatch = useDispatch(); + const [isFollowed, setIsFollowed] = useState(userInfo.isFollowed === true); + console.log(isFollowed); + + const [btnColor, setBtnColor] = useState( + userInfo.isFollowed === true ? null : "#002C51" + ); + + const handleClick = (e) => { + dispatch(__postFollowState(userInfo.userId)); + setIsFollowed(!isFollowed); + if (isFollowed) setBtnColor("#002C51"); + else setBtnColor(null); + }; + + return ( +
+
{ + navigate(`/friends/${userInfo.userId}`); + sessionStorage.setItem("clickedUserName", userInfo.username); + sessionStorage.setItem("clickedUserImg", userInfo.profileImage); + }} + > +
+ 프로필 +
+
{userInfo.username}
+
+ {isFollowed || ( +
+ {isFollowed || "팔로우"} +
+ )} +
+ ); +}; + +export default SearchCards; diff --git a/src/components/follow/UserSearch.jsx b/src/components/follow/UserSearch.jsx new file mode 100644 index 000000000..def864fa6 --- /dev/null +++ b/src/components/follow/UserSearch.jsx @@ -0,0 +1,74 @@ +import React, { useCallback, useEffect, useState } from "react"; +import searchIcon from "../../img/searchIcon.png"; +import { debounce } from "lodash"; +import { UserApi } from "../../api/UserApi"; +import SearchCards from "./SearchCards"; +import { useRef } from "react"; +import LoadingPage from "../../page/LoadingPage"; + +const UserSearch = () => { + const [user, setUser] = useState([]); + const userSearchName = useRef(); + const [time, Settime] = useState(0); + const [loading, setLoading] = useState(false); + const onUserSearch = (event) => { + setLoading(true); + const { value } = event.target; + console.log("test"); + Settime(() => 1000); + handlePrice(value); + }; + + const handlePrice = debounce(async (payload) => { + try { + console.log(payload); + const response = await UserApi.userSearch(payload); + const userInfo = response.data; + console.log(userInfo); + setUser(() => userInfo); + setLoading(false); + } catch (e) { + console.log("api 호출 실패"); + } + }, time); + + useEffect(() => { + handlePrice(); + }, []); + + return ( +
+ {loading && } +
+
+ +
{ + Settime(() => 0); + handlePrice(userSearchName.current.value); + }} + > + 돋보기 아이콘 +
+
+
+ {user.map((list) => { + return ; + })} +
+
+
+ ); +}; + +export default UserSearch; diff --git a/src/components/layout/BottomNavi.jsx b/src/components/layout/BottomNavi.jsx new file mode 100644 index 000000000..8faeffa83 --- /dev/null +++ b/src/components/layout/BottomNavi.jsx @@ -0,0 +1,62 @@ +import React, { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { offFollow, onFollow, offMy, onMy, offHome, onHome } from "../../img"; + +const BottomNavi = () => { + //클릭했을때 아이콘이 바뀌게 하기 위한 스테이트 + const [onIcon, setOnicon] = useState(); + // const [onIcon, setOnicon] = useState(true,false,false]); 이런 방법도 있음 + const navigate = useNavigate(); + + //클릭했을때 아이콘을 바꾸기 위한 함수 + const handleIconClick = (iconName) => { + setOnicon(iconName); + }; + + return ( +
+
+
+
{ + // setOnicon([true,false,false]) + handleIconClick("follow"); + navigate("/follow"); + }} + className="w-[24px] cursor-pointer h-[24px]" + > + 팔로우 + {/* 팔로우 */} +
+
{ + // setOnicon([false,ture,false]) + handleIconClick("home"); + navigate("/main"); + }} + className="w-[24px] h-[24px] cursor-pointer" + > + 홈 + {/* 홈 */} +
+
{ + // setOnicon([false,false,true]) + handleIconClick("My"); + navigate("/profile"); + }} + className="cursor-pointer px-[3.5px] c py-[5.5px] w-[24px] h-[24px]" + > + 마이 + {/* 마이 */} +
+
+
+
+ ); +}; + +export default BottomNavi; diff --git a/src/components/layout/Label.jsx b/src/components/layout/Label.jsx new file mode 100644 index 000000000..7aba2e88c --- /dev/null +++ b/src/components/layout/Label.jsx @@ -0,0 +1,20 @@ +import React from "react"; + +const Label = ({ htmlFor, children }) => { + return ( + <> +
+
+ +
+
+ + ); +}; + +export default Label; diff --git a/src/components/layout/Layout.jsx b/src/components/layout/Layout.jsx new file mode 100644 index 000000000..9beefdf03 --- /dev/null +++ b/src/components/layout/Layout.jsx @@ -0,0 +1,143 @@ +import styled from "styled-components"; +import React from "react"; +import TopNavBar from "./TopNavBar"; +import BottomNavi from "./BottomNavi"; +import { useLocation } from "react-router-dom"; +import { useEffect } from "react"; +import { useState } from "react"; +import TopNavTitleBar from "./TopNavTitleBar"; + +const Layout = ({ children }) => { + const pagePathName = useLocation(); + const [header, setHeader] = useState(null); + const id = pagePathName.pathname.split("/")[2]; + + useEffect(() => { + const userId = sessionStorage.getItem("userId"); + const pageName = pagePathName.pathname; + + switch (pageName) { + case "/": + if (userId !== null) { + setHeader(() => ); + } + break; + case "/main": + setHeader(() => ); + break; + case `/detail/${id}`: + setHeader(() => ); + break; + case `/friendsdetail/${id}`: + setHeader(() => () => ( + + {sessionStorage.getItem("clickedUserName")}님의 일정 + + )); + break; + case `/friends/${id}`: + setHeader(() => ( + + {sessionStorage.getItem("clickedUserName")}님의 일정 + + )); + break; + case "/schedule": + setHeader(() => 일정 추가); + break; + case "/schedule/edit": + setHeader(() => 일정 수정); + break; + case "/signup": + setHeader(() => 회원가입 ); + break; + case "/signup/setProfileName": + setHeader(() => 프로필 설정); + break; + case "/signup/setProfileImg": + setHeader(() => 프로필 설정); + break; + case "/profile": + setHeader(() => 마이페이지); + break; + case "/scheduleinvitation": + setHeader(() => 내게 온 초대목록); + break; + case "/follow": + setHeader(() => 팔로우 목록); + break; + case "/pastEvents": + setHeader(() => 나의 지난 일정); + break; + case "/notification": + setHeader(() => 알림); + break; + case "/userSearch": + setHeader(() => 유저 검색); + break; + case "/editProfile": + setHeader(() => 프로필 변경); + break; + case "/login/auth/InputEmail": + setHeader(() => 이메일 입력); + break; + case "/ChangePassword": + setHeader(() => 비밀번호 수정); + break; + case "/developing": + setHeader(() => 개발중...); + break; + default: + setHeader(null); + break; + } + }, [pagePathName.pathname, id]); + + return ( + + + {header} + {children} + {pagePathName.pathname === "/login" || + pagePathName.pathname === "/signup" || + pagePathName.pathname === "/signup/setProfileName" || + pagePathName.pathname === "/signup/setProfileImg" || + pagePathName.pathname === "/main" || + pagePathName.pathname === "/" || + pagePathName.pathname === `/detail/${id}` ? null : ( + + )} + + + ); +}; + +export default Layout; + +const OutWrap = styled.div` + display: flex; + flex: 1; + justify-content: center; + + /* box-sizing: border-box; */ +`; + +const Container = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100vh; + min-width: 375px; + background-color: #f8fcff; + font-family: Pretendard-Regular; +`; + +const Slider = styled.div` + flex: 1; + overflow: scroll; + -ms-overflow-style: none; + scrollbar-width: none; + ::-webkit-scrollbar { + display: none; + } +`; diff --git a/src/components/layout/TopNavBar.jsx b/src/components/layout/TopNavBar.jsx new file mode 100644 index 000000000..91afe5c2e --- /dev/null +++ b/src/components/layout/TopNavBar.jsx @@ -0,0 +1,57 @@ +import React from "react"; +import searchIcon from "../../img/searchIcon.png"; +import plusIcon from "../../img/plusIcon.png"; +import notifyIcon from "../../img/notifyIcon.png"; +import { useNavigate } from "react-router-dom"; +import gnimsLogo from "../../img/gnimsLogo.png"; + +const TopNavBar = () => { + const navigate = useNavigate(); + return ( +
+
+ gnimsLogo { + navigate("/main"); + }} + /> +
+
+ 검색버튼 { + navigate("/userSearch"); + console.log("검색페이지로이동"); + }} + /> + 추가버튼 { + console.log("스케쥴추가페이지로이동!"); + //스케줄 추가를 하기 위한 파라미터 값을 넘긴다. + navigate("/schedule", { state: { type: "add", id: "" } }); + }} + /> + 알림버튼 { + console.log("알림페이지로 이동"); + navigate("/notification"); + }} + /> +
+
+ ); +}; + +export default TopNavBar; diff --git a/src/components/layout/TopNavTitleBar.jsx b/src/components/layout/TopNavTitleBar.jsx new file mode 100644 index 000000000..a856f5c46 --- /dev/null +++ b/src/components/layout/TopNavTitleBar.jsx @@ -0,0 +1,28 @@ +import React from "react"; +import { useNavigate } from "react-router-dom"; +import backIncom from "../../img/arrowback.png"; +const TopNavTitleBar = ({ children }) => { + const navigate = useNavigate(); + return ( +
+
+
+ 검색버튼 { + navigate(-1); + }} + /> +
+
+ {children} +
+
+
+ ); +}; + +export default TopNavTitleBar; diff --git a/src/components/layout/index.jsx b/src/components/layout/index.jsx new file mode 100644 index 000000000..d30accd0b --- /dev/null +++ b/src/components/layout/index.jsx @@ -0,0 +1,8 @@ +import BottomNavi from "./BottomNavi"; +import Label from "./Label"; +import Layout from "./Layout"; +import TopNavBar from "./TopNavBar"; +import TopNavTitleBar from "./TopNavTitleBar"; +import LoginSignupInputBox from "./input/LoginSignupInputBox"; + +export {BottomNavi,Label,Layout,TopNavBar,TopNavTitleBar,LoginSignupInputBox} \ No newline at end of file diff --git a/src/components/layout/input/LoginSignupInputBox.jsx b/src/components/layout/input/LoginSignupInputBox.jsx new file mode 100644 index 000000000..cbb18b3a5 --- /dev/null +++ b/src/components/layout/input/LoginSignupInputBox.jsx @@ -0,0 +1,23 @@ +import React, { forwardRef } from "react"; + +const LoginSignupInputBox = forwardRef((props, ref) => { + const { type, placeholder, onChange, id, bgColor, shadow } = props; + return ( + <> +
+ +
+ + ); +}); + +export default LoginSignupInputBox; diff --git a/src/components/login/EmailLogin.jsx b/src/components/login/EmailLogin.jsx new file mode 100644 index 000000000..04b086e55 --- /dev/null +++ b/src/components/login/EmailLogin.jsx @@ -0,0 +1,272 @@ +import React, { useRef, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import IsModal from "../modal/Modal"; +import kakaologo from "../../img/kakao_login_medium_narrow.png"; +import { KAKAO_AUTH_URL } from "../../shared/OAuth"; +import LoginButton from "../button/LoginButton"; +import "../style/login.css"; +import { __emailLogin } from "../../redux/modules/LoginSlice"; +import { useDispatch, useSelector } from "react-redux"; +import NaverLogin from "../../page/NaverLoginPage"; +import Label from "../layout/Label"; +import LoginSignupInputBox from "../layout/input/LoginSignupInputBox"; +import gnimsLogo from "../../img/gnimslogo1.png"; +import { EventSourcePolyfill } from "event-source-polyfill"; + +const EmailLogin = () => { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const [isOpen, setOpen] = useState(false); + const [ModalStr, setModalStr] = useState({ + modalTitle: "", + modalMessage: "", + }); + const [style, setStyle] = useState({ + bgColorEmail: "bg-inputBox", + bgColorPassword: "bg-inputBox", + shadowEmail: "", + shadowPassword: "", + }); + + //서버에 전달하기 위한 input Ref 생성 + const userEmailRef = useRef(); + const userPasswordRef = useRef(); + + //아이디 비밀번호가 틀렸을시 문구표시여부 + const [regulation, SetRegulation] = useState({ + regulationEmail: true, + regulationPassword: true, + }); + + //이메일, 비밀번호 정규 표현식 + const emailRegulationExp = + /^[a-zA-Z0-9+-\_.]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/; + const passwordRegulationExp = /^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z\d]{8,16}$/; + + //유효성검사 + const onValidity = (event) => { + const { id, value } = event.target; + if (id === "userEmail") { + setStyle(() => ({ + ...style, + bgColorEmail: "bg-inputBoxFocus", + shadowEmail: "drop-shadow-inputBoxShadow", + })); + if (value.trim() === "") { + setStyle(() => ({ + ...style, + bgColorEmail: "bg-inputBox", + shadowEmail: "", + })); + } + if (!emailRegulationExp.test(value)) { + SetRegulation(() => ({ ...regulation, regulationEmail: false })); + } else { + SetRegulation(() => ({ ...regulation, regulationEmail: true })); + } + } else { + setStyle(() => ({ + ...style, + bgColorPassword: "bg-inputBoxFocus", + shadowPassword: "drop-shadow-inputBoxShadow", + })); + if (value.trim() === "") { + setStyle(() => ({ + ...style, + bgColorPassword: "bg-inputBox", + shadowPassword: "", + })); + } + if (!passwordRegulationExp.test(value)) { + SetRegulation(() => ({ ...regulation, regulationPassword: false })); + } else SetRegulation(() => ({ ...regulation, regulationPassword: true })); + } + }; + + //모달창 + const onModalOpen = () => { + setOpen({ isOpen: true }); + }; + const onMoalClose = () => { + setOpen({ isOpen: false }); + }; + let eventSource; + const fetchSse = async () => { + try { + //EventSource생성. + eventSource = new EventSourcePolyfill("https://eb.jxxhxxx.shop/connect", { + //headers에 토큰을 꼭 담아줘야 500이 안뜬다. + headers: { + Authorization: sessionStorage.getItem("accessToken"), + }, + withCredentials: true, + }); + // SSE 연결 성공 시 호출되는 이벤트 핸들러 + eventSource.onopen = () => { + console.log("SSE 연결완료"); + }; + } catch (error) { + console.log("에러발생:", error); + } + }; + //서버에 전달 + const onSubmit = async (event) => { + event.preventDefault(); + const userEmailCurrent = userEmailRef.current; + const userPasswordCurrent = userPasswordRef.current; + const emailValue = userEmailCurrent.value; + const passwordValue = userPasswordCurrent.value; + + //이메일 빈칸 및 유효성검사 + if (emailValue.trim() === "") { + SetRegulation(() => ({ ...regulation, regulationEmail: false })); + userEmailCurrent.focus(); + return; + } else if (!emailRegulationExp.test(emailValue)) { + SetRegulation(() => ({ ...regulation, regulationEmail: false })); + return; + } + + //비밀번호빈칸 및 유효성검사 + if (passwordValue.trim() === "") { + SetRegulation(() => ({ ...regulation, regulationPassword: false })); + userPasswordCurrent.focus(); + return; + } else if (!passwordRegulationExp.test(passwordValue)) { + SetRegulation(() => ({ ...regulation, regulationPassword: false })); + userPasswordCurrent.focus(); + return; + } + + await dispatch( + __emailLogin({ + email: emailValue, + password: passwordValue, + navigate, + onModalOpen, + setModalStr, + }) + ); + onSubmit().then(fetchSse()); + }; + + //카카오 로그인 + const onClickKakaoLongin = () => { + window.location.href = KAKAO_AUTH_URL; + }; + + return ( +
+
+
+
+
+ 곰캐릭터가 우쭐거리며 왠지 잘될 것 같은 기분포즈 중 +
+
+
+
+
+
+ + +
+
+ +
+
+
+
+ + +
+
+ +
+
+ + +
+
+
+
+ +
+
+ +
+
+
+
간편 로그인
+
+
+
+ +

+ 네이버 +

+
+
+ +

+ 카카오 +

+
+
+ +
+
+
+ ); +}; + +export default EmailLogin; diff --git a/src/components/login/InputEmail.jsx b/src/components/login/InputEmail.jsx new file mode 100644 index 000000000..cfd2c317c --- /dev/null +++ b/src/components/login/InputEmail.jsx @@ -0,0 +1,275 @@ +import React, { useState } from "react"; +import Label from "../layout/Label"; +import LoginSignupInputBox from "../layout/input/LoginSignupInputBox"; +import IsModal from "../modal/Modal"; +import { useRef } from "react"; +import { LoginApi } from "../../api/LoginApi"; +import { useNavigate } from "react-router"; + +const InputEmail = () => { + const navigator = useNavigate(); + const emailRef = useRef(); + const authenticationNumberRef = useRef(); + const emailRegulationExp = + /^[a-zA-Z0-9+-\_.]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/; + + const [style, setStyle] = useState({ + bgColorEmail: "bg-inputBox", + shadowEmail: "", + bgColorAuthenticationNumber: "bg-inputBox", + shadowAuthenticationNumber: "", + }); + const [regulation, SetRegulation] = useState({ + emailError: "", + authenticationNumberError: "", + }); + const [isOpen, setOpen] = useState(false); + const [ModalStr, setModalStr] = useState({ + modalTitle: "", + modalMessage: "", + }); + const [isLoding, setIsLoding] = useState(false); + const [InputCheck, setInputCheck] = useState({ input: false, modal: false }); + + const onModalOpen = () => { + setOpen({ isOpen: true }); + }; + const onMoalClose = () => { + setOpen({ isOpen: false }); + if (InputCheck.modal) { + navigator("/ChangePassword"); + } + }; + + const onInputColor = (event) => { + const { id } = event.target; + const emailRefCurrent = emailRef.current; + const authenticationNumberRefCurrent = authenticationNumberRef.current; + if (id === "email") { + console.log("email입니다"); + setStyle(() => ({ + ...style, + bgColorEmail: "bg-inputBoxFocus", + shadowEmail: "drop-shadow-inputBoxShadow", + })); + if (emailRefCurrent.value.trim() === "") { + setStyle(() => ({ + ...style, + bgColorEmail: "bg-inputBox", + shadowEmail: "", + })); + } + if (emailRegulationExp.test(emailRefCurrent.value)) { + SetRegulation(() => ({ + ...regulation, + emailError: "", + })); + } else { + SetRegulation(() => ({ + ...regulation, + emailError: "올바른 이메일 형식이 아닙니다.", + })); + + emailRefCurrent.focus(); + return; + } + } else { + setStyle(() => ({ + ...style, + bgColorAuthenticationNumber: "bg-inputBoxFocus", + shadowAuthenticationNumber: "drop-shadow-inputBoxShadow", + })); + if (authenticationNumberRefCurrent.value.trim() === "") { + setStyle(() => ({ + ...style, + bgColorAuthenticationNumber: "bg-inputBox", + shadowAuthenticationNumber: "", + })); + } + } + }; + + const onSendEmail = () => { + const email = emailRef.current; + if (email.value.trim() === "") { + SetRegulation(() => ({ + ...regulation, + emailError: "이메일을 입력해주세요", + })); + email.focus(); + return; + } else { + SetRegulation(() => ({ + ...regulation, + emailError: "", + })); + } + + sendEmail({ email: email.value }); + }; + + const sendEmail = async (payload) => { + try { + setIsLoding(() => true); + onModalOpen(); + const data = await LoginApi.SendEmailAuthenticationNumber(payload); + setIsLoding(() => false); + console.log(data); + if (data.status === 200) { + sessionStorage.setItem("changePasswordEmail", payload.email); + setModalStr({ + modalTitle: "이메일함을 확인해주세요", + modalMessage: "인증번호를 입력해주세요", + }); + setInputCheck(() => ({ ...InputCheck, input: true })); + } + } catch (error) { + console.log(error.response); + const { data } = error.response; + if (data.status === 400) { + setIsLoding(() => false); + setModalStr({ + modalTitle: data.message, + modalMessage: "이메일을 확인해주세요.", + }); + } + } + }; + + const onSubmitNextPage = () => { + const email = emailRef.current; + const authenticationNumber = authenticationNumberRef.current; + + if (email.value.trim() === "") { + SetRegulation(() => ({ + ...regulation, + emailError: "이메일을 입력해주세요", + })); + email.focus(); + return; + } else { + SetRegulation(() => ({ + ...regulation, + emailError: "", + })); + } + + if (authenticationNumber.value.trim() === "") { + SetRegulation(() => ({ + ...regulation, + authenticationNumberError: "인증번호를 입력해주세요", + })); + authenticationNumber.focus(); + return; + } else { + SetRegulation(() => ({ + ...regulation, + authenticationNumberError: "", + })); + } + + if (InputCheck.input) { + onSubmitNextPageAxios({ + email: email.value, + code: authenticationNumber.value, + }); + } + }; + + const onSubmitNextPageAxios = async (payload) => { + try { + setIsLoding(() => true); + onModalOpen(); + const { data } = await LoginApi.SendAuthenticationNumber(payload); + setIsLoding(() => false); + console.log(data); + if (data.status === 200) { + setModalStr({ + ...ModalStr, + modalTitle: data.message, + modalMessage: "", + }); + setInputCheck(() => ({ ...InputCheck, modal: true })); + } + } catch (error) { + console.log(error.response); + const { data } = error.response; + if (data.status === 400) { + setIsLoding(() => false); + setModalStr(() => ({ + ...ModalStr, + modalTitle: "인증번호 실패", + modalMessage: `인증번호가 잘못 입력되었습니다. \n 인증요청을 재시도 해주세요.`, + })); + } + } + }; + + return ( +
+
+
+ +
+
+
+
+ +
+ 인증 요청 +
+
+
+

+ {regulation.emailError} +

+
+
+ +
+
+

+ {regulation.authenticationNumberError} +

+
+
+
+ +
+
+ +
+
+ ); +}; + +export default InputEmail; diff --git a/src/components/login/KakaoLogin.jsx b/src/components/login/KakaoLogin.jsx new file mode 100644 index 000000000..9ef2de6ec --- /dev/null +++ b/src/components/login/KakaoLogin.jsx @@ -0,0 +1,17 @@ +import React from "react"; +import kakaologo from "../../img/kakao_login_medium_narrow.png"; +import { KAKAO_AUTH_URL } from "../../shared/OAuth"; + +const KakaoLogin = () => { + //버튼을 눌렀을때 인가 코드를 받아기 위한 주소로 넘어감 + const onClickKakaoLongin = () => { + window.location.href = KAKAO_AUTH_URL; + }; + return ( +
+ 카카오로그인 +
+ ); +}; + +export default KakaoLogin; diff --git a/src/components/login/KakaoLoginLoding.jsx b/src/components/login/KakaoLoginLoding.jsx new file mode 100644 index 000000000..db515f5a6 --- /dev/null +++ b/src/components/login/KakaoLoginLoding.jsx @@ -0,0 +1,21 @@ +import React, { useEffect, useRef, useState } from "react"; +import { useDispatch } from "react-redux"; +import { useNavigate } from "react-router-dom"; +import { __kakaologin } from "../../redux/modules/LoginSlice"; + +//인가코드를 백으로 전달하기 위한 페이지 +const KakaoLoginLoding = () => { + const dispatch = useDispatch(); + + // new URL 객체에서 searchParams객체의 get메소드를 사용하여 'code'키의 값을 추출 + const code = new URL(window.location.href).searchParams.get("code"); + console.log("카카오 인가코드", code); + + // 페이지가 로딩됨과 동시에 디스패치로 code 전달 + useEffect(() => { + dispatch(__kakaologin(code)); + }); + return
카카오 로딩 페이지
; +}; + +export default KakaoLoginLoding; diff --git a/src/components/login/NaverLogin.jsx b/src/components/login/NaverLogin.jsx new file mode 100644 index 000000000..a7caba914 --- /dev/null +++ b/src/components/login/NaverLogin.jsx @@ -0,0 +1,45 @@ +import { useEffect, useRef } from "react"; +import NaverUnion from "../../img/NaverUnion.png"; + +const NaverLogin = () => { + const naverRef = useRef(); + const { naver } = window; + + const NAVER_CLIENT_ID = "T9R5hFNUTuTa1UqoVBcO"; + // process.env.REACT_APP_NAVER_CLIENT_ID; + const NAVER_CALLBACK_URL = + "https://gnims-chaejung-work-git-dev-angelachaejung.vercel.app/social/naver-login"; + // process.env.REACT_APP_NAVER_CALLBACK_URL; + const initializeNaverLogin = () => { + const naverLogin = new naver.LoginWithNaverId({ + clientId: NAVER_CLIENT_ID, + callbackUrl: NAVER_CALLBACK_URL, + isPopup: false, + loginButton: { color: "green", type: 1, height: 58 }, + callbackHandle: true, + }); + naverLogin.init(); + }; + //원형아이콘클릭해도 네이버로그인이 가능. + const handleClick = () => { + naverRef.current.children[0].click(); + }; + // 화면 첫 렌더링이후 바로 실행 + useEffect(() => { + initializeNaverLogin(); + }, []); + + return ( + <> + {/*네이버 로그인아이콘표시 */} +