diff --git a/.gitignore b/.gitignore index a5ac825..f014060 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,5 @@ yarn-error.log* .env.development.local .env.test.local .env.production.local -.env* \ No newline at end of file +.env* +.now \ No newline at end of file diff --git a/README.md b/README.md index 461c3ec..259ea53 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,11 @@ ## 실행 방법 -``` -npm install -npm run dev -``` +## [3주차] 유현우 미션 제출합니다. -- npm install : 필요한 모든 패키지를 설치합니다. 처음 1번만 실행하면 됩니다. -- npm run dev : react(next) 웹서버를 localhost:3000에서 실행합니다. +이번 미션은 사실 스터디 때 모르는 것들을 다 여쭤보고, 구현을 어느정도 했었기 때문에 그렇게 어렵지는 않았던 것 같습니다! 너무 설명을 잘해주셔가지구 ㅎㅎ 근데 중간 고사 기간이 점점 다가오니 다른 디테일에 신경을 썼어야하는데 신경을 많이 못 쓴 것 같아 아쉽습니다 ㅠㅠ
+비동기함수나 Request, Response, axios, POST, GET 같은 개념이나 함수들을 사용하면서 이런 개념들을 더 잘 이해하게 된 것 같습니다. 개념 자체는 알고 있고 한두번씩 써보긴 했었지만 개념이 조금 모호했었는데 이번에 사용하면서 완벽하진 않지만 좀 더 잘 이해하게 된 것 같습니다.
+컴포넌트나 state 같은 것들을 사용하면 사용할수록 점점 익숙해지는 것 같습니다.
+그런데 아직 memo는 정확히 어떨 때 사용해야하는지 애매하네요..로그인 창에서 쓰는 건 알겠지만 이번에 후보자들 보여주는 창에서는 어디다 넣어야할지 고민하다가 그냥 상위 컴포넌트에 넣었는데 잘 넣었는지 모르겠네요...ㅠㅠ
+그리고 예전에 혼자서 과제나 코딩을 할 때는 컨벤션을 엄격하게 지키지 않았는데 계속 보면서 익숙해져야겠습니다...그리고 css 틈 날때마다 공부는 하는데 아직은 생각한대로 척척 짜지지는 않네요..ㅠㅠ
+혼자 개발하면 기능 구현은 해도 막개발이라는 생각이 지워지질 않았는데 이렇게 코드 리뷰해주시니까 그런 불안이 많이 없어져서 좋은 것 같습니다. 꼼꼼하게 코드 리뷰해주시느라 항상 감사합니다 ㅎㅎ diff --git a/now.json b/now.json new file mode 100644 index 0000000..11ec8b2 --- /dev/null +++ b/now.json @@ -0,0 +1,12 @@ +{ + "version": 2, + "public": false, + "builds": [{ "src": "next.config.js", "use": "@now/next" }], + "build": { + "env": { + "NODE_ENV": "@react-vote-11th-node-env", + "PORT": "@react-vote-11th-port", + "API_HOST": "@react-vote-11th-api-host" + } + } +} diff --git a/package-lock.json b/package-lock.json index a439258..136f0db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1920,6 +1920,37 @@ } } }, + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "requires": { + "follow-redirects": "1.5.10" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", diff --git a/package.json b/package.json index 0e22c09..817c4b9 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "start": "node server" }, "dependencies": { + "axios": "^0.19.2", "compression": "^1.7.4", "dotenv": "^8.2.0", "express": "^4.17.1", diff --git a/pages/index.js b/pages/index.js index 6474ebb..5d751e2 100644 --- a/pages/index.js +++ b/pages/index.js @@ -1,19 +1,29 @@ -import React from "react"; +import React, { useState } from "react"; import styled from "styled-components"; import LoginForm from "../src/components/login-form"; +import VoteForm from "../src/components/vote-form"; export default function Home() { + const [isLoggedIn, setIsLoggedIn] = useState(false); + return ( - 리액트 투-표 - + 리액트 투-표 + {!isLoggedIn && } + {isLoggedIn && } ); } const Wrapper = styled.div` + font-size: 3rem; min-height: 100vh; padding: 10rem 40rem; background-color: Azure; `; + +const Title = styled.p` + font-weight: bold; + margin-bottom: 2rem; +`; diff --git a/pages/login.js b/pages/login.js new file mode 100644 index 0000000..7e359ef --- /dev/null +++ b/pages/login.js @@ -0,0 +1,32 @@ +import React, { useState } from "react"; +import styled from "styled-components"; +import { useRouter } from "next/router"; +import LoginForm from "../src/components/login-form"; + +import Link from "next/link"; + +export default function Home() { + const router = useRouter(); + const { userName } = router.query; + return ( + + 리액트 투-표 + + + 투표하러가기 + + + ); +} + +const Wrapper = styled.div` + font-size: 3rem; + min-height: 100vh; + padding: 10rem 40rem; + background-color: Azure; +`; + +const Title = styled.p` + font-weight: bold; + margin-bottom: 2rem; +`; diff --git a/pages/vote.js b/pages/vote.js new file mode 100644 index 0000000..eb1f1de --- /dev/null +++ b/pages/vote.js @@ -0,0 +1,33 @@ +import React, { useState } from "react"; +import styled from "styled-components"; +import { useRouter } from "next/router"; + +import VoteForm from "../src/components/vote-form"; + +export default function Home() { + const router = useRouter(); + const { userName } = router.query; + return ( + + router.back()}>리액트 투-표 + {userName}님 안녕하세요! + + + ); +} + +const Name = styled.span` + color: blue; +`; + +const Wrapper = styled.div` + font-size: 3rem; + min-height: 100vh; + padding: 10rem 40rem; + background-color: Azure; +`; + +const Title = styled.p` + font-weight: bold; + margin-bottom: 2rem; +`; diff --git a/src/components/candidate-form.js b/src/components/candidate-form.js new file mode 100644 index 0000000..2d805cb --- /dev/null +++ b/src/components/candidate-form.js @@ -0,0 +1,68 @@ +import React from "react"; +import styled from "styled-components"; +import axios from "axios"; + +export default function CandidateForm(props) { + const { name, voteCount, rank, id, refetch } = props; + + const voteCandidate = async () => { + await axios + .put(process.env.API_HOST + `/candidates/${id}/vote/`) + .then(function (response) { + console.log(response); + alert(name + "님에게 투표 완료!"); + refetch(); + // return response.data; + }) + .catch(function (error) { + console.log(error); + alert("투표 실패!"); + }); + }; + return ( + + {rank}위: + + {name}[{voteCount}표] + + + { + voteCandidate(); + }} + > + 투표 + + + ); +} + +const Wrapper = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + flex-direction: row; +`; + +const CandidateRank = styled.p` + font-weight: bolder; + font-size: 2.5rem; + border: none; + margin: none; +`; +const CandidateDesc = styled.p` + font-size: 2.5rem; + width: 40%; + border: none; + margin: none; +`; +const VoteBtn = styled.button` + background: blue; + color: white; + border: none; + border-radius: 0.7rem; + font-size: 2rem; + height: 3.5rem; + width: 5.5rem; + margin: none; +`; diff --git a/src/components/login-form.js b/src/components/login-form.js index 418d945..c36be59 100644 --- a/src/components/login-form.js +++ b/src/components/login-form.js @@ -1,10 +1,97 @@ -import React from "react"; +import React, { useState } from "react"; +import axios from "axios"; import styled from "styled-components"; -export default function LoginForm() { - return 안녕 나는 로그인 폼!; +function LoginForm(props) { + //State에 로그인에 필요한 데이터 저장 + + const [userData, setUserData] = useState({ + email: "example@ceos.or.kr", + password: "example1!", + }); + + const { loginSuccess } = props; + // 변수 이름 쉽게하기 위해 + const { email, password } = userData; + + const checkBlank = () => { + // 둘 중 하나라도 안 채워져 있을시 알림 + if (email === "" || password === "") { + alert("빈칸 채워주세요!!"); + return false; + } else { + return true; + } + }; + + const initFormData = (status) => { + const initializedUserData = { + email: "", + password: "", + }; + + switch (status) { + case 404: + alert("이메일이 존재하지 않습니다!!!"); + break; + case 422: + alert("비밀번호가 틀렸습니다!!!"); + initializedUserData.email = email; + break; + default: + alert("로그인을 다시 시도해주세요!"); + } + + setUserData(initializedUserData); + }; + // 로그인 시도 + const tryLogin = () => { + if (checkBlank() === false) { + console.log("실패"); + return; + } + axios + .post(process.env.API_HOST + "/auth/signin/", userData) + .then(function (response) { + alert("로그인에 성공하셨습니다!!!"); + loginSuccess(true); + console.log(response); + }) + .catch(function (error) { + initFormData(error.response.status); + }); + }; + + // 값이 변경될 때 + const handleFormChange = (e) => { + const { name, value } = e.target; + setUserData({ + ...userData, + [name]: value, + }); + }; + + return ( + + 로그인 + + EMAIL + + + + PASSWORD + + + + 로그인 + + + ); } +export default React.memo(LoginForm); +export const MemoizedLoginForm = React.memo(LoginForm); + const Wrapper = styled.div` width: 100%; min-height: 30rem; @@ -12,3 +99,39 @@ const Wrapper = styled.div` font-size: 18px; padding: 3rem 4rem; `; + +const LoginTitle = styled.p` + font-weight: bold; + font-size: 2rem; + margin-bottom: 3rem; +`; + +const InputWrapper = styled.div` + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 3rem; +`; + +const InputLabel = styled.p` + font-size: 1.5rem; + padding: 0px; + margin: 0px; +`; + +const DataInput = styled.input` + border: 1px solid rgb(97, 97, 97); + padding: 0.5rem 0.8rem; + width: 70%; +`; + +const LoginBtn = styled.button` + float: right; + background: rgb(222, 222, 222); + font-size: 1.5rem; + padding: 0.5rem 1rem; + border: none; + border-radius: 0.3rem; +`; diff --git a/src/components/myHooks/candidates.js b/src/components/myHooks/candidates.js new file mode 100644 index 0000000..8508f90 --- /dev/null +++ b/src/components/myHooks/candidates.js @@ -0,0 +1,38 @@ +import { useState, useEffect } from "react"; +import axios from "axios"; + +export const useCandidates = () => { + // 로그인 데이터 + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(false); + + const fetchData = async () => { + // 로딩 시작, 에러 없음 + setIsLoading(true); + setError(false); + + try { + const data = await axios + .get(process.env.API_HOST + "/candidates/", { + params: {}, + }) + .then(function (response) { + setIsLoading(false); + return response.data; + }); + setData(data); + } catch (error) { + console.log(error); + setError(error); + } + // 로딩 끝 + setIsLoading(false); + }; + + useEffect(() => { + fetchData(); + }, []); + + return { candidateList: data, isLoading, error, refetch: fetchData }; +}; diff --git a/src/components/vote-form.js b/src/components/vote-form.js index 65bc549..c9ab502 100644 --- a/src/components/vote-form.js +++ b/src/components/vote-form.js @@ -1,10 +1,39 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import styled from "styled-components"; +import CandidateForm from "./candidate-form"; +import { useCandidates } from "./myHooks/candidates"; -export default function VoteForm() { - return 안녕 나는 투표 폼!; +function VoteForm() { + const { candidateList, isLoading, error, refetch } = useCandidates(); + if (error) { + console.log(error); +
{error}
; + } + if (candidateList) + return ( + + + 프론트앤드 인기쟁이는 누구? + + CEOS 프론트엔드 개발자 인기 순위 및 투표 창입니다. + + {candidateList && + candidateList + .sort((a, b) => b.voteCount - a.voteCount) + .map((candidate, index) => { + const { _id: id, name, voteCount } = candidate; + return ( + + ); + })} + + + ); + return <>; } +export default React.memo(VoteForm); + const Wrapper = styled.div` width: 100%; min-height: 30rem; @@ -12,3 +41,22 @@ const Wrapper = styled.div` font-size: 18px; padding: 3rem 4rem; `; +const Title1 = styled.p` + font-size: 30px; + font-weight: bolder; +`; + +const Title2 = styled.p` + font-size: 26px; + font-weight: bolder; + color: grey; +`; + +const RedText = styled.span` + color: red; +`; + +const CandidateListWrapper = styled.div` + padding: 5rem 10rem; + border: 1px solid black; +`;