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;
+`;