From ba5429cfcdcb6261e9c25d05f484bff1408f2d37 Mon Sep 17 00:00:00 2001 From: ksbin1025 Date: Mon, 9 Aug 2021 14:51:59 +0900 Subject: [PATCH] =?UTF-8?q?master=20=EB=B3=91=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- client/package-lock.json | 11 +- client/public/images/19.png | Bin 0 -> 9110 bytes client/public/images/icon-bus.png | Bin 0 -> 997 bytes client/public/images/icon-car.png | Bin 0 -> 1060 bytes client/public/images/movieTheater.jpg | Bin 0 -> 86654 bytes client/src/App.js | 5 +- client/src/apis/auth.api.js | 61 ++- client/src/apis/movie.api.js | 16 +- client/src/components/Admin/TicketFeeTable.js | 37 +- client/src/components/BoxOffice/BoxOffice.js | 309 +++++++----- .../BoxOffice/box-office.module.css | 34 -- client/src/components/BoxOffice/mystyle.css | 34 -- client/src/components/Collection.js | 44 +- client/src/components/CountButton.js | 17 +- client/src/components/Kakaopay.js | 37 -- client/src/components/Login/Login.js | 29 +- client/src/components/MovieCard/MovieCard.js | 5 +- client/src/components/MovieChart.js | 6 +- client/src/components/MovieComing.js | 15 +- client/src/components/MyInfo/MyInfo.js | 324 +++++++++++- .../src/components/MyInfo/my-info.module.scss | 179 ++++++- client/src/components/Navs/MainNav.js | 9 +- client/src/components/Navs/SubNav.js | 23 +- .../ReservationDetails/ReservationDetails.js | 28 ++ .../components/ReservationDetails/index.js | 1 + .../reservation-details.module.scss | 39 ++ client/src/components/SearchResult.js | 4 +- client/src/components/SeatTable/SeatTable.js | 48 +- .../SeatTable/seatTable.module.scss | 24 +- client/src/components/Signup/Signup.js | 219 ++++---- .../src/components/Signup/signup.module.scss | 14 +- client/src/components/TheaterInfo.js | 122 +++++ .../TicketingMovie/TicketingMovie.js | 21 +- .../TicketingTheater/TicketingTheater.js | 13 +- .../TicketingTimeTable/TicketingTimeTable.js | 10 +- client/src/components/Video.js | 11 +- client/src/context/auth_context.js | 31 +- client/src/pages/HomePage.js | 29 +- client/src/pages/MovieListPage.js | 23 +- client/src/pages/MoviePage.js | 53 +- client/src/pages/MyPage.js | 4 +- client/src/pages/PaymentCompletePage.js | 72 +++ client/src/pages/PaymentPage.js | 54 -- client/src/pages/PaymentPage/PaymentPage.js | 224 +++++++++ .../pages/PaymentPage/PaymentPage.module.scss | 10 + client/src/pages/PaymentPage/index.js | 1 + client/src/pages/TheaterPage.js | 15 +- client/src/pages/TicketingPage.js | 52 +- client/src/pages/TicketingSeatPage.js | 273 +++++++--- client/src/utils/auth.js | 6 +- package-lock.json | 212 +------- package.json | 4 +- server/app.js | 3 +- server/config/app.config.js | 1 + server/controllers/email.controller.js | 94 ++-- server/controllers/kakaopay.controller.js | 2 - server/controllers/movie.controller.js | 93 ++-- server/controllers/reservation.controller.js | 84 ++++ server/controllers/theater.controller.js | 18 +- server/controllers/user.controller.js | 469 +++++++++++++++--- server/db/index.js | 8 +- server/index.js | 12 +- server/models/confirmnum.model.js | 33 ++ server/models/guest.model.js | 40 ++ server/models/reservation.model.js | 8 +- server/models/theater.model.js | 2 +- server/models/user.model.js | 16 +- server/routes/index.js | 2 + server/routes/movie.route.js | 6 +- server/routes/reservation.route.js | 17 + server/routes/theater.route.js | 5 +- server/routes/user.route.js | 52 +- 73 files changed, 2610 insertions(+), 1170 deletions(-) create mode 100644 client/public/images/19.png create mode 100644 client/public/images/icon-bus.png create mode 100644 client/public/images/icon-car.png create mode 100644 client/public/images/movieTheater.jpg delete mode 100644 client/src/components/BoxOffice/box-office.module.css delete mode 100644 client/src/components/BoxOffice/mystyle.css delete mode 100644 client/src/components/Kakaopay.js create mode 100644 client/src/components/ReservationDetails/ReservationDetails.js create mode 100644 client/src/components/ReservationDetails/index.js create mode 100644 client/src/components/ReservationDetails/reservation-details.module.scss create mode 100644 client/src/components/TheaterInfo.js create mode 100644 client/src/pages/PaymentCompletePage.js delete mode 100644 client/src/pages/PaymentPage.js create mode 100644 client/src/pages/PaymentPage/PaymentPage.js create mode 100644 client/src/pages/PaymentPage/PaymentPage.module.scss create mode 100644 client/src/pages/PaymentPage/index.js create mode 100644 server/controllers/reservation.controller.js create mode 100644 server/models/confirmnum.model.js create mode 100644 server/models/guest.model.js create mode 100644 server/routes/reservation.route.js diff --git a/.gitignore b/.gitignore index 362797a..f596774 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /node_modules .env.development -.env \ No newline at end of file +.env +upload/ \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index 38d0bb5..046e20d 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -4419,9 +4419,9 @@ } }, "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" }, "cookie-signature": { "version": "1.0.6", @@ -6510,6 +6510,11 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", diff --git a/client/public/images/19.png b/client/public/images/19.png new file mode 100644 index 0000000000000000000000000000000000000000..55861dc24895bb5ed4272b4e05297a8be1e617d0 GIT binary patch literal 9110 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 literal 0 HcmV?d00001 diff --git a/client/public/images/icon-bus.png b/client/public/images/icon-bus.png new file mode 100644 index 0000000000000000000000000000000000000000..8bf2124942dbf240798f16210e91bf7005f49a7e GIT binary patch literal 997 zcmVYAX9X8WNB|8RBvx=!KdMT000AU zNklU4R?vNS9RdI^HesK`@@7)j8Dik_l}Ai@ePg47_p zT42ya_7Yv8s3^Kn8I)x`Mno@KWT!)R??6P~<|FQA3X3pOG%$(WD{9$02wf6kh zcdh?g>%Z22H?l0##unfzoQKnJWPBQC)_%uMe1hljYN}Er0dfe|VKoj_KYJaQV|*%* z2XPzz#*26vpW!?FjK5~M;fLW^TpS%d56|M7P9Ue_bNqo7c&E*uFT*CB(1V7L;s(5x zC|ih6F$ZVH_aw-}ScQ$axr5j@II)MLFpBwY$~NLA+=n~bfqaa!aRJ`%Ak=)Q7ZZP~ z|6YOD@Bz+m2eLE#@i^?NDaaWyXLsP_b|8B(J3jkr>RR*gEq;r+k_0K*)64L9k6=1Y zb{LAi<UEQ}HOy!ZWzGD#)eso5p^-)($Me*mRG&x5QER z=xO-)PZgf~9&g~DN#0Nic#nZUoq#Q*Ttf*FLkRfcw;)Li{ zjC2vEj84@$*Oq#7V2)Htiz3jcpM|baOtR_HSkqK*n%!N2S_tuk9zOP3hmGu7snWNG8 TjOX*T00000NkvXXu0mjfDJ0>x literal 0 HcmV?d00001 diff --git a/client/public/images/icon-car.png b/client/public/images/icon-car.png new file mode 100644 index 0000000000000000000000000000000000000000..4d3d18315b6a01fa7b48cc35a74a634e8938d1f2 GIT binary patch literal 1060 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 z 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

H`FfFuZRbVl#~a=! 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 OFi$+uMHNLYkN??uq@Nf7 literal 0 HcmV?d00001 diff --git a/client/src/App.js b/client/src/App.js index 1ce9d6f..b3f0c60 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -14,7 +14,8 @@ import AdminPage from "./pages/AdminPage/AdminPage"; import TicketingPage from "./pages/TicketingPage"; import TicketingSeatPage from './pages/TicketingSeatPage' import SearchPage from "./pages/SearchPage"; -import Payment from "./pages/PaymentPage"; +import Payment from "./pages/PaymentPage/PaymentPage"; +import PaymentCompletePage from "./pages/PaymentCompletePage"; function App() { @@ -46,4 +47,4 @@ function App() { ); } -export default App; +export default App; \ No newline at end of file diff --git a/client/src/apis/auth.api.js b/client/src/apis/auth.api.js index 4840415..27aa753 100644 --- a/client/src/apis/auth.api.js +++ b/client/src/apis/auth.api.js @@ -1,6 +1,11 @@ import axios from "axios"; import { baseUrl } from "../utils/baseUrl.js"; -import config from "../utils/clientConfig.js"; + +const getUser = async () => { + const url = `${baseUrl}/api/auth/user` + const { data } = await axios.get(url) + return data +} const login = async (login) => { const payload = login; @@ -9,41 +14,61 @@ const login = async (login) => { }; const logout = async () => { - alert("로그아웃되었습니다."); - localStorage.removeItem(config.loginUser); - await axios.get(`${baseUrl}/api/auth/logout`); + const { data } = await axios.get(`${baseUrl}/api/auth/logout`); + return data }; const signup = async (user) => { - const url = `${baseUrl}/api/auth/signup` - await axios.post(url, user) + const url = `${baseUrl}/api/auth/signup`; + const { data } = await axios.post(url, user); + return data +} + +const confirmMbnum = async (phone) => { + const url = `${baseUrl}/api/auth/phone/${phone}` + const { data } = await axios.post(url); + return data +} + +const confirmNum = async (confirmNum) => { + const url = `${baseUrl}/api/auth/num` + const { data } = await axios.post(url, confirmNum); + return data } -const compareId = async (userId) => { - const url = `${baseUrl}/api/auth/${userId}` +const profile = async (formData) => { + const url = `${baseUrl}/api/auth/profile` + const { data } = await axios.post(url, formData) + return data +} +const getMember = async () => { + const url = `${baseUrl}/api/auth/member` const { data } = await axios.get(url) return data -} +} -const confirmMbnum = async (id,token) => { - const url = `${baseUrl}/api/auth/${id}/${token}` +const comparePw = async (pw) => { + const url = `${baseUrl}/api/auth/pw/${pw}` const { data } = await axios.get(url) return data } -const getNickName = async(id) =>{ - const url = `${baseUrl}/api/auth/${id}` - console.log("url : ", url); - const { nickName } = await axios.get(url) - return nickName +const modifyUser = async (user) => { + const url = `${baseUrl}/api/auth/modify` + const { data } = await axios.post(url, user) + return data } const authApi = { + getUser, login, logout, signup, - compareId, confirmMbnum, - getNickName, + confirmNum, + profile, + getMember, + comparePw, + modifyUser, }; export default authApi \ No newline at end of file diff --git a/client/src/apis/movie.api.js b/client/src/apis/movie.api.js index abe991a..e6d218e 100644 --- a/client/src/apis/movie.api.js +++ b/client/src/apis/movie.api.js @@ -11,11 +11,6 @@ const getAllfromTM = async (pageNum) => { return data } -const getMoviesfromTM = async (category) => { - const response = await axios.get(`${baseUrl}/api/movie/showmovies/${category}`) - return response.data -} - const getMovieInfofromTM = async (id) => { const movieId = id const response = await axios.get(`${TMDBUrl}/${movieId}?api_key=${process.env.REACT_APP_TMDB_API_KEY}&language=ko-KR`) @@ -28,13 +23,13 @@ const getImagesfromTM = async (id) => { return response.data } -const getCreditsfromTM = async (id) =>{ +const getCreditsfromTM = async (id) => { const movieId = id const response = await axios.get(`${TMDBUrl}/${movieId}/credits?api_key=${process.env.REACT_APP_TMDB_API_KEY}`) return response.data } -const getVideosfromTM = async (id) =>{ +const getVideosfromTM = async (id) => { const movieId = id const response = await axios.get(`${TMDBUrl}/${movieId}/videos?api_key=${process.env.REACT_APP_TMDB_API_KEY}`) return response.data.results @@ -45,6 +40,11 @@ const getListfromDB = async () => { return data } +const getListByCategoryfromDB = async (category) => { + const { data } = await axios.get(`${baseUrl}/api/movie/movielist/${category}`) + return data +} + const submit = async (movieId) => { const { data } = await axios.post(`${baseUrl}/api/movie/${movieId}`) return data @@ -68,7 +68,7 @@ const search = async ({ type, keyword }, pageNum) => { const movieApi = { getAllfromTM, - getMoviesfromTM, + getListByCategoryfromDB, getMovieInfofromTM, getImagesfromTM, getCreditsfromTM, diff --git a/client/src/components/Admin/TicketFeeTable.js b/client/src/components/Admin/TicketFeeTable.js index 184cdbd..7f591a0 100644 --- a/client/src/components/Admin/TicketFeeTable.js +++ b/client/src/components/Admin/TicketFeeTable.js @@ -1,11 +1,14 @@ import { useState, useEffect } from "react"; import cinemaApi from "../../apis/cinema.api.js"; import catchErrors from "../../utils/catchErrors.js"; +import { useAuth } from '../../context/auth_context'; import styles from "./admin.module.scss"; const TicketFeeTable = ({ selectTheater, setEditFee, formRef }) => { const [ticketFee, setTicketFee] = useState([]) const [error, setError] = useState("") + const { user } = useAuth() + useEffect(() => { if (selectTheater !== 0) getOne(selectTheater) @@ -48,7 +51,7 @@ const TicketFeeTable = ({ selectTheater, setEditFee, formRef }) => { } return ( - +
@@ -57,7 +60,7 @@ const TicketFeeTable = ({ selectTheater, setEditFee, formRef }) => { - + {user.role === "admin" ? : <>} @@ -70,12 +73,15 @@ const TicketFeeTable = ({ selectTheater, setEditFee, formRef }) => { - + {user.role === "admin" + ? + + : <>} @@ -107,12 +113,15 @@ const TicketFeeTable = ({ selectTheater, setEditFee, formRef }) => { - + {user.role === "admin" + ? + + : <>} ) : diff --git a/client/src/components/BoxOffice/BoxOffice.js b/client/src/components/BoxOffice/BoxOffice.js index 1cabc69..a7c3f77 100644 --- a/client/src/components/BoxOffice/BoxOffice.js +++ b/client/src/components/BoxOffice/BoxOffice.js @@ -1,147 +1,186 @@ import { useState, useEffect } from "react" -import movieApi from '../../apis/movie.api.js' -import "./box-office.module.css" -const BoxOffice = () => { - const [TMDB_TopRated_Data, setTMDB_TopRated_Data] = useState() - useEffect(() => { - // getTMDB_TopRated() - let items = document.querySelectorAll('.carousel .carousel-item') - // console.log("item", items) - items.forEach((el) => { - const minPerSlide = 4 - let next = el.nextElementSibling - for (let i = 1; i < minPerSlide; i++) { - if (!next) { - // wrap carousel by using first child - next = items[0] - } - let cloneChild = next.cloneNode(true) - el.appendChild(cloneChild.children[0]) - next = next.nextElementSibling - } - }) - }, []) - - async function getTMDB_TopRated() { - const category = "popular" - try { - const data = await movieApi.getMoviesfromTM(category) - console.log(data) - setTMDB_TopRated_Data(data) - } catch (error) { - console.log(error) - } - } +const BoxOffice = ({TMDB_TopRated_Data}) => { return (
- {console.log(TMDB_TopRated_Data)} -
-

Bootstrap Multi Slide Carousel

-
-
-
-
-
-
-
- -
-
Slide 1
-
-
-
-
-
-
-
- -
-
Slide 2
-
-
-
-
-
-
-
- -
-
Slide 3
-
-
-
-
-
-
-
- -
-
Slide 4
-
-
-
-
-
-
-
- -
-
Slide 5
-
-
-
-
-
-
-
- -
-
Slide 6
-
-
+ {/*
+
+ {TMDB_TopRated_Data.length>0 + ? + TMDB_TopRated_Data.map((movie, index) => { +
+ {console.log(movie.poster_path)} + Movie Poster
+ }) + : +
+ {console.log("스틸컷 불러오기 오류")} + 등록된 스틸컷이 없습니다.
- - - - - - -
-
-
advances one slide at a time
-
-
-
-
- {TMDB_TopRated_Data ? - TMDB_TopRated_Data.map((moviePoster, index) => ( -
-
-
-
- -
-
-
-
- )) - : (
영화를 불러올 수 없습니다:(
)} -
- - - - - - + }
-
+ + +
*/} +
) } -export default BoxOffice \ No newline at end of file +export default BoxOffice + +// import { useState, useEffect } from "react" +// import movieApi from '../../apis/movie.api.js' +// import "./box-office.module.css" + +// const BoxOffice = () => { +// const [TMDB_TopRated_Data, setTMDB_TopRated_Data] = useState() +// useEffect(() => { +// // getTMDB_TopRated() +// let items = document.querySelectorAll('.carousel .carousel-item') +// // console.log("item", items) +// items.forEach((el) => { +// const minPerSlide = 4 +// let next = el.nextElementSibling +// for (let i = 1; i < minPerSlide; i++) { +// if (!next) { +// // wrap carousel by using first child +// next = items[0] +// } +// let cloneChild = next.cloneNode(true) +// el.appendChild(cloneChild.children[0]) +// next = next.nextElementSibling +// } +// }) +// }, []) + +// async function getTMDB_TopRated() { +// const category = "popular" +// try { +// const data = await movieApi.getMoviesfromTM(category) +// console.log(data) +// setTMDB_TopRated_Data(data) +// } catch (error) { +// console.log(error) +// } +// } + +// return ( +//
+// {console.log(TMDB_TopRated_Data)} +//
+//

Bootstrap Multi Slide Carousel

+//
+//
+//
+//
+//
+//
+//
+// +//
+//
Slide 1
+//
+//
+//
+//
+//
+//
+//
+// +//
+//
Slide 2
+//
+//
+//
+//
+//
+//
+//
+// +//
+//
Slide 3
+//
+//
+//
+//
+//
+//
+//
+// +//
+//
Slide 4
+//
+//
+//
+//
+//
+//
+//
+// +//
+//
Slide 5
+//
+//
+//
+//
+//
+//
+//
+// +//
+//
Slide 6
+//
+//
+//
+//
+// +// +// +// +// +// +//
+//
+//
advances one slide at a time
+//
+//
+//
+//
+// {TMDB_TopRated_Data ? +// TMDB_TopRated_Data.map((moviePoster, index) => ( +//
+//
+//
+//
+// +//
+//
+//
+//
+// )) +// : (
영화를 불러올 수 없습니다:(
)} +//
+// +// +// +// +// +// +//
+//
+//
+// ) +// } + +// export default BoxOffice \ No newline at end of file diff --git a/client/src/components/BoxOffice/box-office.module.css b/client/src/components/BoxOffice/box-office.module.css deleted file mode 100644 index 6e44ad3..0000000 --- a/client/src/components/BoxOffice/box-office.module.css +++ /dev/null @@ -1,34 +0,0 @@ -@media (max-width: 767px) { - .carousel-inner .carousel-item > div { - display: none; - } - .carousel-inner .carousel-item > div:first-child { - display: block; - } -} - -.carousel-inner .carousel-item.active, -.carousel-inner .carousel-item-next, -.carousel-inner .carousel-item-prev { - display: flex; -} - -/* medium and up screens */ -@media (min-width: 768px) { - - .carousel-inner .carousel-item-end.active, - .carousel-inner .carousel-item-next { - transform: translateX(25%); - } - - .carousel-inner .carousel-item-start.active, - .carousel-inner .carousel-item-prev { - transform: translateX(-25%); - } -} - -.carousel-inner .carousel-item-end, -.carousel-inner .carousel-item-start { -transform: translateX(0); -} - diff --git a/client/src/components/BoxOffice/mystyle.css b/client/src/components/BoxOffice/mystyle.css deleted file mode 100644 index b4c221b..0000000 --- a/client/src/components/BoxOffice/mystyle.css +++ /dev/null @@ -1,34 +0,0 @@ - -@media (max-width: 767px) { - .carousel-inner .carousel-item > div { - display: none; - } - .carousel-inner .carousel-item > div:first-child { - display: block; - } -} - -.carousel-inner .carousel-item.active, -.carousel-inner .carousel-item-next, -.carousel-inner .carousel-item-prev { - display: flex; -} - -/* medium and up screens */ -@media (min-width: 768px) { - - .carousel-inner .carousel-item-end.active, - .carousel-inner .carousel-item-next { - transform: translateX(25%); - } - - .carousel-inner .carousel-item-start.active, - .carousel-inner .carousel-item-prev { - transform: translateX(-25%); - } -} - -.carousel-inner .carousel-item-end, -.carousel-inner .carousel-item-start { - transform: translateX(0); -} diff --git a/client/src/components/Collection.js b/client/src/components/Collection.js index 56bba02..65a2135 100644 --- a/client/src/components/Collection.js +++ b/client/src/components/Collection.js @@ -1,10 +1,46 @@ -const Collection = () => { +import { useEffect, useState } from 'react' +import movieApi from '../apis/movie.api.js' +import catchErrors from '../utils/catchErrors.js' + +const Collection = ({ TMDB_TopRated_Data }) => { + const [videoUrls, setVideoUrls] = useState([]) + const [error, setError] = useState("") + + useEffect(() => { + if (TMDB_TopRated_Data.length > 0) { + getVideos() + } + }, [TMDB_TopRated_Data]) + + async function getVideos() { + try { + const data = await movieApi.getVideosfromTM(TMDB_TopRated_Data[0].id) + setVideoUrls(data) + } catch (error) { + catchErrors(error, setError) + } + } return ( <>

Movie Collection

-
-
- +
+
+ {videoUrls.length > 0 + ? +
+
+ +
+
+ + :
예고편 정보가 존재하지 않습니다.
} +
+ {TMDB_TopRated_Data.length > 0 + ? + + : +
+ }
) diff --git a/client/src/components/CountButton.js b/client/src/components/CountButton.js index 2a3e11b..ca00a3d 100644 --- a/client/src/components/CountButton.js +++ b/client/src/components/CountButton.js @@ -1,26 +1,25 @@ -import {useState} from 'react' -const CountButton = (props) => { - const name = props.name +const CountButton = ({name, count, setCount}) => { function handleCount(event) { if (event.target.name === "backbutton") { - props.setCount({...props.count, [name] :props.count[name] - 1}) + setCount({...count, [name] :count[name] - 1}) } else if (event.target.name === "forwardbutton") { - props.setCount({...props.count, [name] :props.count[name] + 1}) + setCount({...count, [name] :count[name] + 1}) } else { - props.setCount({...props.count, [name] :event.target.value}) + setCount({...count, [name] :event.target.value}) } } + return ( -
+ <> - + -
+ ) } diff --git a/client/src/components/Kakaopay.js b/client/src/components/Kakaopay.js deleted file mode 100644 index f0a3e1d..0000000 --- a/client/src/components/Kakaopay.js +++ /dev/null @@ -1,37 +0,0 @@ -import axios from 'axios' - -const Kakaopay = (props) => { - async function handleClick() { - try { - const response = await axios.post('/api/kakaopay/test/single', { - cid: 'TC0ONETIME', - partner_order_id: 'orderNum', - partner_user_id: 'userName', - item_name: props.ticketInfo.title, - quantity: props.ticketInfo.teenager+props.ticketInfo.adult+props.ticketInfo.elderly, - total_amount: props.ticketInfo.teenager * 7000 + props.ticketInfo.adult * 8000 + props.ticketInfo.elderly * 6000, - vat_amount: 0, - tax_free_amount: 0, - approval_url: 'http://localhost:3000/', - fail_url: 'http://localhost:3000/', - cancel_url: 'http://localhost:3000/', - }) - console.log(response.data) - if (response.data) { - window.location.href = response.data.redirect_url - } - } catch (error) { - console.log(error) - } - } - - return ( - <> - - - ) -} - -export default Kakaopay \ No newline at end of file diff --git a/client/src/components/Login/Login.js b/client/src/components/Login/Login.js index e152f6f..da31480 100644 --- a/client/src/components/Login/Login.js +++ b/client/src/components/Login/Login.js @@ -1,21 +1,18 @@ import { useState } from "react"; -import styles from "./login.module.scss"; import { Redirect } from "react-router-dom"; +import { useAuth } from "../../context/auth_context.js"; import catchErrors from "../../utils/catchErrors"; -import {useAuth} from "../../context/auth_context.js"; +import styles from "./login.module.scss"; const Login = () => { - const {login, loading} = useAuth(); - //useState를 이용해서 각 state 생성 및 초기값 저장 - const [state, setState] = useState(true); // 이 줄은 css에 해당하는 state - //state변수 지정 하지만 이 변수는 react에 의해 없어지지 않음, 그리고 그 다음 변수는 state변수를 갱신해주는 함수 + const { login, loading } = useAuth(); + const [state, setState] = useState(true); const [user, setUser] = useState({ id: '', password: '' }); const [error, setError] = useState(""); const [success, setSuccess] = useState(false); - const [guest, setGuset] = useState({ guestName: '', gusetBirthday: '', @@ -23,10 +20,7 @@ const Login = () => { guestPassword: '' }) - //input태그에 걸려있는 onchange에서 실행할 함수설정 const handleLoginOnChange = (e) => { - // ... 전개 연산자 - // 현재 state에 방금 변화한 값을 다시 저장함 setUser({ ...user, [e.target.name]: e.target.value @@ -41,9 +35,12 @@ const Login = () => { } const requestServer = async (data) => { - if(data === user){ - await login(data); - }else{ + if (data === user) { + const success = await login(data); + if (success) { + setSuccess(true); + alert('로그인이 완료되었습니다.') + } } } @@ -51,11 +48,8 @@ const Login = () => { e.preventDefault(); try { setError(""); - console.log(e.target.name); if (e.target.name === "login") { requestServer(user); - alert('로그인이 완료되었습니다.') - setSuccess(true); } else { requestServer(guest); @@ -86,7 +80,6 @@ const Login = () => {
- {/* 로그인 */}
@@ -95,7 +88,6 @@ const Login = () => { 회원이 아니십니까?
- {/* 비회원예매 학인 */}
@@ -103,7 +95,6 @@ const Login = () => { -

※ 비회원 정보 오 입력 시 예매 내역 확인/취소 및 티켓 발권이 어려울 수 있으니 다시 한번 확인해 주시기 바랍니다.

diff --git a/client/src/components/MovieCard/MovieCard.js b/client/src/components/MovieCard/MovieCard.js index 83d558c..05b50cc 100644 --- a/client/src/components/MovieCard/MovieCard.js +++ b/client/src/components/MovieCard/MovieCard.js @@ -23,8 +23,9 @@ const MovieCard = ({ list }) => {
{movie.overview}
- {movie.title} -

예매율: {movie.ticket_sales}0% | {movie.runtime}분

+ {movie.adult? :<>} +
{movie.title}
+

예매율: {Math.round((movie.ticket_sales/(movie.totalReservationRate.totalReservationRate||1))*100)}% | {movie.runtime}분

{movie.release_date} 개봉

{ const [TMDB_TopRated_Data, setTMDB_TopRated_Data] = useState([]) const [error, setError] = useState("") @@ -16,8 +15,7 @@ const MovieChart = () => { async function getTMDB_TopRated() { try { setError("") - const data = await movieApi.getMoviesfromTM(category) - console.log("sdad==", data) + const data = await movieApi.getListByCategoryfromDB(category) setTMDB_TopRated_Data([...data]) } catch (error) { catchErrors(error, setError) @@ -30,7 +28,7 @@ const MovieChart = () => {
- :

영화정보를 로딩할 수 없습니다.

+ :

} ) diff --git a/client/src/components/MovieComing.js b/client/src/components/MovieComing.js index 21bf716..dd4d353 100644 --- a/client/src/components/MovieComing.js +++ b/client/src/components/MovieComing.js @@ -1,9 +1,8 @@ import { useState, useEffect } from 'react' -import movieApi from "../apis/movie.api.js" -// import MovieCard from "./MovieCard/index.js" import { Link } from 'react-router-dom' -import styles from './MovieCard/MovieCard.js' +import movieApi from "../apis/movie.api.js" import catchErrors from '../utils/catchErrors.js' +import styles from './MovieCard/movie-card.module.scss' const MovieComing = () => { const [TMDB_UpComing_Data, setTMDB_UpComing_Data] = useState([]) @@ -17,7 +16,7 @@ const MovieComing = () => { async function getTMDB_UpComing() { try { setError("") - const response = await movieApi.getMoviesfromTM(category) + const response = await movieApi.getListByCategoryfromDB(category) setTMDB_UpComing_Data([...response]) } catch (error) { catchErrors(error, setError) @@ -28,7 +27,6 @@ const MovieComing = () => { <> {TMDB_UpComing_Data.length !== 0 ?
- {/* */} {TMDB_UpComing_Data.map(movie => (
{
{movie.overview}
- {movie.title} -

예매율: {movie.ticket_sales}0% | {movie.runtime}분

+ {movie.adult ? : <>} +
{movie.title}
+

{movie.runtime}분

{movie.release_date} 개봉

{
))}
- :

영화정보를 로딩할 수 없습니다.

+ :

} ) diff --git a/client/src/components/MyInfo/MyInfo.js b/client/src/components/MyInfo/MyInfo.js index 6d2df93..d521678 100644 --- a/client/src/components/MyInfo/MyInfo.js +++ b/client/src/components/MyInfo/MyInfo.js @@ -1,35 +1,317 @@ -import styles from "./my-info.module.scss"; -import { useAuth } from "../../context/auth_context"; import { useState, useEffect } from "react"; import authApi from "../../apis/auth.api"; +import catchErrors from "../../utils/catchErrors.js"; +import styles from "./my-info.module.scss"; const MyInfo = () => { - const { user } = useAuth(); - const [userNickName, setUserNickName] = useState(""); - - const getNickName = async (id) => { - console.log(id); - const nickname = await authApi.getNickName(id); - console.log(nickname); - return nickname - } + const [userNickName, setUserNickName] = useState("사용자"); + const [img, setImg] = useState(""); + const [profile, setProfile] = useState(""); + const [startTime, setStartTime] = useState(""); + const [page, setPage] = useState(true); + const [presentPw, setPresentPw] = useState(""); + const [loading, setLoading] = useState(false); + const [userRe, setUserRe] = useState({ + userName: "", + userEmail: "", + userNickName: "", + userMbnum: "", + userPassword: "", + userRePassword: "" + }) + const [confirmMb, setConfirmMb] = useState(false); + const [number, setNumber] = useState(null); + const [mbError, setMbError] = useState(false); + const [error, setError] = useState(""); + const [errorMsg, setErrorMsg] = useState({ + errorName: false, + errorEmail: false, + errorNickName: false, + errorMbnum: false, + errorPassword: false, + }) useEffect(() => { - let name = getNickName(user.id); - setUserNickName(name); + getMember(); }, []) + const getMember = async () => { + const member = await authApi.getMember(); + setUserNickName(member.nickname); + setProfile(member.img); + } + + const handlePwOnChange = (e) => { + setPresentPw(e.target.value) + } + + const handleUserOnChange = (e) => { + setUserRe({ + ...userRe, + [e.target.name]: e.target.value + }) + if (e.target.name === "userMbnum") { + setUserRe({ + ...userRe, + [e.target.name]: String(e.target.value) + }) + } + } + const enterKey = (e) => { + if (e.key === "Enter") { + handleOnSummitVerify(e); + } + } + + const handleOnChange = (e) => { + setImg(e.target.files[0]); + } + + const handleOnSummitForm = async (e) => { + e.preventDefault(); + try { + setError("") + const formData = new FormData(); + formData.append("image", img); + const image = await authApi.profile(formData); + setProfile(image.img); + } catch (error) { + catchErrors(error, setError); + } + } + + const handleOnSummitVerify = async (e) => { + e.preventDefault(); + try { + setError(""); + setLoading(true); + const pw = presentPw; + const confirmPw = await authApi.comparePw(pw); + if (confirmPw) { + setPage(false); + setPresentPw(""); + } else { + alert("비밀번호가 일치하지 않습니다."); + } + } catch (error) { + catchErrors(error, setError); + } finally { + setLoading(false); + } + } + + const handleOnClickMbnum = async (e) => { + e.preventDefault(); + try { + setStartTime(""); + setError(""); + setLoading(true); + const phone = userRe.userMbnum; + const message = await authApi.confirmMbnum(phone); + if (message.isSuccess) { + setMbError("보냄"); + setStartTime(message.startTime); + } + } catch (error) { + catchErrors(error, setError); + } finally { + setLoading(false); + } + } + + const handleOnChangeMb = (e) => { + setNumber(String(e.target.value)); + } + + const handleOnClickMbConfirm = async (e) => { + e.preventDefault(); + try { + setError(""); + setLoading(true); + const confirmNum = { userMbnum: userRe.userMbnum, number: number, startTime: startTime }; + const message = await authApi.confirmNum(confirmNum); + setMbError(message); + if (message === "성공") { + setConfirmMb(true); + console.log("인증완료"); + } + } catch (error) { + catchErrors(error, setError); + } finally { + setLoading(false); + } + } + + const validationPw = () => { + if (userRe.userPassword !== userRe.userRePassword) return false; + else return true; + } + + const handleOnSummit = async (e) => { + e.preventDefault(); + try { + setError(""); + setLoading(true); + let validPw = validationPw(); + if (confirmMb) { + if (validPw) { + const userData = userRe; + const error = await authApi.modifyUser(userData); + if (error === "성공") { + alert("회원정보수정에 성공하였습니다.") + } else { + setErrorMsg(error); + alert("형식에 맞게 다시 작성해주세요"); + } + } else { + throw new Error("비밀번호가 일치하지 않습니다."); + } + } else { + throw new Error("핸드폰 번호를 인증해주세요."); + } + } catch (error) { + catchErrors(error, setError); + } finally { + setLoading(false); + } + } + + const handleOnclickOut = (e) => { + setConfirmMb(false); + } + + const handleOnClick = (e) => { + e.preventDefault(); + handleOnclickOut(e); + setPage(true); + } + return ( -
- 마이페이지 -
- -
- {`${userNickName}`}님 반갑습니다! - + <> +
+ 마이페이지 +
+
+

프로필 변경

+ +
+
+ {`${userNickName}`}님 반갑습니다! + +
+
+
+ + -
+ ) } diff --git a/client/src/components/MyInfo/my-info.module.scss b/client/src/components/MyInfo/my-info.module.scss index 6cf65ac..f80cf6c 100644 --- a/client/src/components/MyInfo/my-info.module.scss +++ b/client/src/components/MyInfo/my-info.module.scss @@ -1,7 +1,180 @@ -.title{ +.main { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; +} + +.title { + display: flex; + justify-content: center; + color: #FEDC00; + font-size: 2.5rem; + margin: 2rem 0; +} + +.confirm { + color: black; + padding-right: 8px; + text-align: center; + display: flex; + align-items: center; +} + +.input2 { + width: 9.01rem; +} + +.userName { + color: white; + font-size: 1.5rem; +} + +.contents { + display: flex; + width: 100%; + justify-content: spa; + align-items: center; + padding-top: 5px; +} + +.signupLabel { + color: black; + padding-right: 8px; + text-align: left; +} + +.input_file_button { + padding: 6px 25px; + background-color: #FF6600; + border-radius: 4px; + color: white; + cursor: pointer; +} + +.inputContent { + display: flex; + justify-content: space-around; + align-items: center; + margin: 1rem 0; +} + +.box { + width: 12rem; + margin: 0px 3rem; + position: relative; + display: flex; + align-items: center; + + &:hover { + display: block; + } +} + +.hoverTxt { + display: none; + position: absolute; + top: 4rem; + left: 1.6rem; + color: #FEDC00; + font-size: 1.5rem; +} + +.box:hover .hoverTxt { + display: block; +} + +.profile { + width: 10rem; + height: 10rem; + + &:hover { + opacity: 0.5; + } +} + +.input { + margin: 0.5rem 0 0 0; + padding: 0.5rem 0 0.5rem 0; + border-radius: 3px; + text-align: center; +} + +input[type=password] { + font-family: 'Courier New', Courier, monospace; +} + +input::placeholder { + font-family: 'HangeulNuriB' +} + +.inputSize { + width: 15.3rem; +} + +input[type="number"]::-webkit-outer-spin-button, +input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; +} + +.butterYellowAndBtn { + color: black; + font-size: 1rem; + background-color: #FEDC00; + border: 1px solid black; + text-align: center; +} + +.btnHover:hover { + background-color: black; + color: #FEDC00; + transition: ease-out; + border: 1px solid #FEDC00; + text-align: center; +} + +.passwordConfirmError { + margin-bottom: 0; + margin-top: 0.5rem; + margin-right: 3rem; + text-align: end; + font-size: 13px; + font-weight: bold; + color: black; +} + +@media (max-width: 576px) { + .title { display: flex; justify-content: center; color: #FEDC00; - font-size: 25px; - margin: 1rem 0; + font-size: 2rem; + margin: 2rem 0; + } + + .box { + width: 8rem; + margin: 0px 1rem; + position: relative; + } + + .profile { + width: 8rem; + height: 8rem; + } + + .userName { + color: white; + font-size: 1.1rem; + } + + .hoverTxt { + display: none; + position: absolute; + top: 3.2rem; + left: 1.7rem; + color : #FEDC00; + font-size: 1rem; + } } \ No newline at end of file diff --git a/client/src/components/Navs/MainNav.js b/client/src/components/Navs/MainNav.js index 2378476..f398019 100644 --- a/client/src/components/Navs/MainNav.js +++ b/client/src/components/Navs/MainNav.js @@ -1,5 +1,6 @@ import { useState } from "react"; -import { useHistory } from "react-router"; +import { Link, useHistory } from "react-router-dom"; + import Search from "../Search"; const MainNav = () => { @@ -12,9 +13,9 @@ const MainNav = () => { return ( ) diff --git a/client/src/components/Navs/SubNav.js b/client/src/components/Navs/SubNav.js index e3bf480..e0d278e 100644 --- a/client/src/components/Navs/SubNav.js +++ b/client/src/components/Navs/SubNav.js @@ -1,22 +1,23 @@ import { Link } from "react-router-dom"; import { useAuth } from "../../context/auth_context.js" + const SubNav = () => { const { user, logout } = useAuth(); return ( - <> {(user) ? - : - ) } + export default SubNav \ No newline at end of file diff --git a/client/src/components/ReservationDetails/ReservationDetails.js b/client/src/components/ReservationDetails/ReservationDetails.js new file mode 100644 index 0000000..a061048 --- /dev/null +++ b/client/src/components/ReservationDetails/ReservationDetails.js @@ -0,0 +1,28 @@ +import styles from "./reservation-details.module.scss"; + +const ReservationDetails = () => { + return ( +
+
나의 예매 내역
+
+
+
+ 영화 포스터 +
+
+ 영화제목 + 예매확인번호 + 예매날짜 + 상영관 + 좌석정보 + 결제금액 + 결제수단 +
+
+
+
나의 리뷰
+
+ ) +} + +export default ReservationDetails \ No newline at end of file diff --git a/client/src/components/ReservationDetails/index.js b/client/src/components/ReservationDetails/index.js new file mode 100644 index 0000000..287aac2 --- /dev/null +++ b/client/src/components/ReservationDetails/index.js @@ -0,0 +1 @@ +export { default } from "./ReservationDetails" \ No newline at end of file diff --git a/client/src/components/ReservationDetails/reservation-details.module.scss b/client/src/components/ReservationDetails/reservation-details.module.scss new file mode 100644 index 0000000..4dafcf0 --- /dev/null +++ b/client/src/components/ReservationDetails/reservation-details.module.scss @@ -0,0 +1,39 @@ +.width{ + width : 100%; + display: flex; + justify-content: center; +} + +.header{ + display: flex; + justify-content: center; + background-color: #FEDC00; + width: 80%; + text-align: center; + font-size: 2.5rem; + margin: 5rem; +} +.body{ + width: 80%; + border-top: 1px solid #FEDC00; + border-bottom: 1px solid #FEDC00; +} + +.span .layout{ + color:white; + font-size: 1.5rem; +} + +@media (max-width: 403px) { + + .header{ + display: flex; + justify-content: center; + background-color: #FEDC00; + width: 80%; + text-align: center; + font-size: 1.5rem; + margin: 2rem; + } + +} \ No newline at end of file diff --git a/client/src/components/SearchResult.js b/client/src/components/SearchResult.js index 0868a11..7c815c4 100644 --- a/client/src/components/SearchResult.js +++ b/client/src/components/SearchResult.js @@ -26,7 +26,7 @@ const SearchResult = () => { } return ( - <> +
{result.length !== 0 ? ( <>

'{title}' 에 관한 검색 결과입니다.

@@ -36,7 +36,7 @@ const SearchResult = () => { ) :

'{title}' 에 관한 검색 결과가 존재하지 않습니다.

} - +
) } diff --git a/client/src/components/SeatTable/SeatTable.js b/client/src/components/SeatTable/SeatTable.js index d9ec81b..d803e29 100644 --- a/client/src/components/SeatTable/SeatTable.js +++ b/client/src/components/SeatTable/SeatTable.js @@ -1,40 +1,38 @@ -import { useState } from 'react' import styles from './seatTable.module.scss' -const SeatTable = (props) => { +const SeatTable = ({ theaterInfo, count, setSelectedSeats, selectedSeats, reservedSeats }) => { const table = [] - for (let rowIndex = 0; rowIndex < props.allSeat.row; rowIndex++) { - table.push({String.fromCharCode(rowIndex + 65)}) - // console.log(String.fromCharCode(rowIndex+65)) - for (let colIndex = 0; colIndex < props.allSeat.col; colIndex++) { - table.push( - - - - ) + + if (theaterInfo) { + for (let rowIndex = 0; rowIndex < theaterInfo.rows; rowIndex++) { + table.push({String.fromCharCode(rowIndex + 65)}) + for (let colIndex = 0; colIndex < theaterInfo.columns; colIndex++) { + table.push( + {reservedSeats.find(el => el === String(rowIndex + 1) + '-' + String(colIndex + 1)) + ? + + : + + } + ) + } + table.push(
) } - table.push(
) } function handleClick(event) { - const num = Object.values(props.count).reduce((a, b) => (a + b)) - if (props.selectedSeats.find(el => el === event.target.name)) { - //제거 - const deleted = props.selectedSeats.filter((element) => element !== event.target.name); - props.setSelectedSeats(deleted) + const num = Object.values(count).reduce((a, b) => (a + b)) + if (selectedSeats.find(el => el === event.target.name + '-' + event.target.id)) { + const deleted = selectedSeats.filter((element) => element !== event.target.name + '-' + event.target.id); + setSelectedSeats(deleted) } else { - if (props.selectedSeats.length > num - 1) { - alert("선택한 좌석이 예매인원보다 많습니다.") - } else { - //추가 - props.setSelectedSeats([...props.selectedSeats, event.target.name]) - } + if (selectedSeats.length > num - 1) alert("선택한 좌석이 예매인원보다 많습니다.") + else setSelectedSeats([...selectedSeats, event.target.name + '-' + event.target.id]) } - } + return (
- {console.log(props.selectedSeats)}
Screen
{table}
diff --git a/client/src/components/SeatTable/seatTable.module.scss b/client/src/components/SeatTable/seatTable.module.scss index 10907c7..9b49038 100644 --- a/client/src/components/SeatTable/seatTable.module.scss +++ b/client/src/components/SeatTable/seatTable.module.scss @@ -1,15 +1,23 @@ .btn { - border:0; + border: 0; background: black; color: white; - &:hover{ - border:0; - background: red ; - color:white + + &:hover { + border: 0; + background: red; + color: white; } } + .on { - border:0; - background: red ; - color:white + border: 0; + background: red; + color: white; +} + +.btnBlock { + background: gray; + border: 0; + color: white; } \ No newline at end of file diff --git a/client/src/components/Signup/Signup.js b/client/src/components/Signup/Signup.js index 381fb31..db82ccd 100644 --- a/client/src/components/Signup/Signup.js +++ b/client/src/components/Signup/Signup.js @@ -1,13 +1,13 @@ -import styles from "./signup.module.scss"; import { useState } from "react"; +import { Redirect } from "react-router-dom"; import authApi from "../../apis/auth.api.js"; -import { Redirect } from "react-router"; import catchErrors from "../../utils/catchErrors.js"; -import Twilio from "twilio"; +import styles from "./signup.module.scss"; const Signup = () => { const [user, setUser] = useState({ userId: "", + userName: "", userEmail: "", userNickName: "", userBirthday: "", @@ -15,25 +15,24 @@ const Signup = () => { userPassword: "", userRePassword: "" }) - + const [startTime, setStartTime] = useState(""); + const [number, setNumber] = useState(null); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); - //각 타입별 error 유무 state + + const [mbError, setMbError] = useState(false); const [error, setError] = useState(""); const [errorMsg, setErrorMsg] = useState({ - errorId: null, + errorId: false, + errorName: false, errorEmail: false, errorNickName: false, errorBirthday: false, errorMbnum: false, errorPassword: false, - errorRePassword: false }) - // id중복확인 여부 state와 가입하기 누르면 id 임시 저장 - const [overlapId, setOverlapId] = useState(false); - const [preId, setPreId] = useState(""); + const [confirmMb, setConfirmMb] = useState(false); - //입력할때마다 state에 저장 const handleUserOnChange = (e) => { setUser({ ...user, @@ -47,116 +46,73 @@ const Signup = () => { } } - //id(중복확인 체크, 형식 에러) - const handleOnClickId = async (e) => { + const handleOnClickMbnum = async (e) => { e.preventDefault(); try { + setStartTime(""); setError(""); - if (user.userId.length < 5) { - setErrorMsg(errorMsg => ({ - ...errorMsg, - [e.target.name]: true - })); - if (overlapId === true) { - setOverlapId(() => (false)); - }; - } else { - const userId = user.userId; - await authApi.compareId(userId); - if (!await authApi.compareId(userId)) { - alert("이 아이디는 사용가능합니다.") - setErrorMsg(errorMsg => ({ - ...errorMsg, - [e.target.name]: false - })); - setOverlapId(() => (true)); - } else { - alert("이미 사용중인 아이디입니다.") - setOverlapId(() => (false)); - } + setLoading(true) + const phone = user.userMbnum; + const message = await authApi.confirmMbnum(phone); + if (message.isSuccess) { + setMbError("보냄"); + setStartTime(message.startTime); } } catch (error) { - catchErrors(error, setError) + catchErrors(error, setError); } finally { setLoading(false); } } - const handleOnClickMbnum = async (e) => { - try { - const id = "AC01ecdbffb36dc0766cfea487a54a4c6e"; - const token = "1d86d5d43760b5dce5582badf7b0a775"; - await authApi.confirmMbnum(id,token); - } catch (error) { - console.log('twilio error'+ error) - } + const handleOnChangeMb = (e) => { + setNumber(String(e.target.value)); } - const handleOnSummit = async (e) => { + const handleOnClickMbConfirm = async (e) => { e.preventDefault(); try { - setError(() => ("")); - //처리가 될때까지 버튼(가입하기)이 안눌리게 지정 - setLoading(() => (true)); - //유효성 검사 - validation(); - const userData = user; - //서버로 전송 - await authApi.signup(userData) - alert("가입이 완료되었습니다. 로그인 해주세요."); - setSuccess(true); + setError(""); + setLoading(true); + const confirmNum = { userMbnum: user.userMbnum, number: number, startTime: startTime }; + const message = await authApi.confirmNum(confirmNum); + setMbError(message); + if (message === "성공") { + setConfirmMb(true); + } } catch (error) { - //에러전송 catchErrors(error, setError); } finally { setLoading(false); } } - //비교하여 error메세지 반환 - const vaildationData = (text, compareValue, error) => { - if (text !== compareValue) { - setErrorMsg(errorMsg => ({ ...errorMsg, [error]: true })); - } else { - setErrorMsg(errorMsg => ({ ...errorMsg, [error]: false })); - } - } - //아이디 비번 유효성 검사 - const vaildationIdPw = (text, minValue, error) => { - if ((text < minValue)) { - setErrorMsg(errorMsg => ({ ...errorMsg, [error]: true })); - } else if (text >= minValue) { - setErrorMsg(errorMsg => ({ ...errorMsg, [error]: false })); - if (overlapId === true) { - if (preId !== user.userId) { - setOverlapId(false); - } - } - } + const validationPw = () => { + if (user.userPassword !== user.userRePassword) return false; + else return true; } - //유효성 검사 - const validation = () => { - setPreId(user.userId); - //아이디 유효성 검사 - vaildationIdPw(user.userId.length, 5, "errorId"); - //별명 유효성 검사 - vaildationData((user.userNickName.length === 0), false, "errorNickName"); - // 생일 유효성 검사 - vaildationData(user.userBirthday.length, 6, "errorBirthday"); - // 휴대폰 유효성 검사 - vaildationData(user.userMbnum.length, 11, "errorMbnum"); - // 비밀번호 유효성 검사 - vaildationIdPw(user.userPassword.length, 8, "errorPassword"); - // 비밀번호 확인 유효성 검사 - vaildationData(user.userRePassword, user.userPassword, "errorRePassword"); - // 최종 유효성 검사 - if (overlapId && (Object.values(errorMsg).some((element) => (element)) === false)) { - } else if (!overlapId && (Object.values(errorMsg).some((element) => (element)) === false)) { - setErrorMsg(errorMsg => ({ ...errorMsg, errorId: false })); - throw new Error("먼저 아이디 중복확인을 해주세요"); - } else { - throw new Error("유효하지 않은 데이터입니다."); + const handleOnSummit = async (e) => { + e.preventDefault(); + try { + setError(""); + setLoading(true); + let validPw = validationPw(); + if (confirmMb) { + if (validPw) { + const userData = user; + const error = await authApi.signup(userData); + if (error === "성공") setSuccess(true); + else { + setErrorMsg(error); + alert("형식에 맞게 다시 작성해주세요"); + } + } else throw new Error("비밀번호가 일치하지 않습니다."); + } else throw new Error("핸드폰 번호를 인증해주세요."); + } catch (error) { + catchErrors(error, setError); + } finally { + setLoading(false); } } @@ -164,73 +120,80 @@ const Signup = () => { return ; } - return ( - // 데이터 입력과 유효성 검사 후 보이는 경고창
회원가입
-
- - -
+ +
+ {errorMsg.errorId &&

5~10자리 사이로 입력해주세요.

} +
+
+
+ +
- {(overlapId === false) && errorMsg.errorId &&

5~10자리 사이로 입력해주세요.

} - {overlapId && (errorMsg.errorId === false) &&

아이디 중복이 확인되었습니다.

} - {(errorMsg.errorId === false) && (overlapId === false) &&

아이디 중복확인을 해주세요.

} + {errorMsg.errorName &&

이름을 입력해주세요

}
- +
- {errorMsg.errorEmail &&

이메일을 입력해주세요

} + {errorMsg.errorEmail &&

이메일을 입력해주세요

}
- +
- {errorMsg.errorNickName &&

10자 이내로 입력해주세요.

} + {errorMsg.errorNickName &&

10자 이내로 입력해주세요.

}
-
- +
- {errorMsg.errorBirthday &&

숫자 6자리를 입력해주세요.

} + {errorMsg.errorBirthday &&

숫자 6자리를 입력해주세요.

}
-
- - + + +
+
+ {errorMsg.errorMbnum &&

-없이 숫자 11자리를 입력해주세요.

} +
+
+ +
+ + + +
+ {(mbError === "재전송") &&

유효시간이 만료되었습니다. 재전송해주세요.

} + {(mbError === "보냄") &&

5분이내에 입력해주세요.

} + {(mbError === "성공") &&

인증되었습니다.

} + {(mbError === "실패") &&

인증번호를 다시 입력해주세요.

}
- {errorMsg.errorMbnum &&

-없이 숫자 11자리를 입력해주세요.

}
- +
- {errorMsg.errorPassword &&

8~11자리 사이로 입력해주세요.

} + {errorMsg.errorPassword &&

8~11자리 사이로 입력해주세요.

}
- -
-
- - -
- {errorMsg.errorRePassword &&

비밀번호가 일치하지 않습니다.

} +
+ +
-
diff --git a/client/src/components/Signup/signup.module.scss b/client/src/components/Signup/signup.module.scss index cfd428e..c4715f5 100644 --- a/client/src/components/Signup/signup.module.scss +++ b/client/src/components/Signup/signup.module.scss @@ -5,7 +5,13 @@ font-size: 1.7rem; margin-top: 2rem; } - +.confirm{ + color: white; + padding-right: 8px; + text-align: center; + display: flex; + align-items: center; +} .contents{ display: flex; width: 100%; @@ -31,7 +37,9 @@ border-radius: 3px; text-align: center; } - +.input2 { + width: 9.01rem; +} input[type=password]{ font-family: 'Courier New', Courier, monospace; } @@ -65,7 +73,7 @@ border: 1px solid white ; text-align: center; } -.passwordConfirmError{ +.errorMsg{ margin-bottom: 0; margin-top: 0.5rem; text-align: end; diff --git a/client/src/components/TheaterInfo.js b/client/src/components/TheaterInfo.js new file mode 100644 index 0000000..c186787 --- /dev/null +++ b/client/src/components/TheaterInfo.js @@ -0,0 +1,122 @@ +import { useRef, useState, useEffect } from 'react' +import cinemaApi from "../apis/cinema.api,js" +import theaterApi from "../apis/theater.api.js" +import catchErrors from "../utils/catchErrors" + +const { kakao } = window; +const options = { + center: new kakao.maps.LatLng(37.365264512305174, 127.10676860117488), + level: 3 +}; + +const TheaterInfo = () => { + const container = useRef(null) + const [cinemaInfo, setCinemaInfo] = useState() + const [currentInfo, setCurrentInfo] = useState({ + name: "init", + title: "init", + information: "init" + }) + const [error, setError] = useState() + const [tabContent, setTabContent] = useState([]) + useEffect(() => { + getTheaterInfo() + }, []) + + useEffect(() => { + if (currentInfo.title === "address") { + const container = document.getElementById("map"); + const options = { + center: new kakao.maps.LatLng(33.450701, 126.570667), + level: 3, + }; + const map = new kakao.maps.Map(container, options); + const geocoder = new kakao.maps.services.Geocoder(); + geocoder.addressSearch(`${cinemaInfo.address}`, function (result, status) { + if (status === kakao.maps.services.Status.OK) { + const coords = new kakao.maps.LatLng(result[0].y, result[0].x); + const marker = new kakao.maps.Marker({ + map: map, + position: coords + }); + const infowindow = new kakao.maps.InfoWindow({ + content: '
Butter Studio
' + }); + infowindow.open(map, marker); + map.setCenter(coords); + } + }); + } + }, [currentInfo]); + + async function getTheaterInfo() { + try { + const response = await cinemaApi.getCinemaInfo() + const response2 = await theaterApi.getAll() + setCinemaInfo({ ...response.data, theaterNum: response2.data.length }) + setCurrentInfo({ + name: "대중교통 안내", + title: "transportation", + information: response.data.transportation + }) + } catch (error) { + catchErrors(error, setError) + } + } + + function handleClick(e) { + setCurrentInfo({ + name: e.target.name, + title: e.target.id, + information: e.target.value + }) + } + + return ( + <> + {cinemaInfo ? +
+ {console.log(cinemaInfo)} +

{cinemaInfo.cinemaName}

+
+ +
+
총 상영관 수: {cinemaInfo.theaterNum}개
+
{cinemaInfo.address}
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
{currentInfo.information}
+
+
{currentInfo.information}
+
+
+
+
+
+ : +
+ 극장정보를 불러올 수 없습니다. +
} + + ) +} + +export default TheaterInfo \ No newline at end of file diff --git a/client/src/components/TicketingMovie/TicketingMovie.js b/client/src/components/TicketingMovie/TicketingMovie.js index 612fe1e..96e0718 100644 --- a/client/src/components/TicketingMovie/TicketingMovie.js +++ b/client/src/components/TicketingMovie/TicketingMovie.js @@ -1,34 +1,35 @@ -import React, { useState, useEffect } from 'react' -import axios from 'axios' +import { useState, useEffect } from 'react' +import movieApi from '../../apis/movie.api' +import catchErrors from '../../utils/catchErrors' import styles from "./ticketingMovie.module.scss" -const TicketingMovie = (props) => { +const TicketingMovie = ({ ticketInfo, setTicketInfo }) => { const [movieList, setMovieList] = useState([]) + const [error, setError] = useState() + useEffect(() => { getMovieList() }, []) async function getMovieList() { try { - const response = await axios.get(`/api/movie/movielist`) - setMovieList(response.data) + const response = await movieApi.getListByCategoryfromDB() + setMovieList(response) } catch (error) { - + catchErrors(error, setError) } } function handleClick(event) { - console.log(event.target.name) - props.setTicketInfo({...props.ticketInfo, movieId: event.target.name }) + setTicketInfo({ ...ticketInfo, movieId: event.target.name }) } return (
- {console.log(props.ticketInfo.movieId)}
{movieList.length > 0 ? movieList.map(movie => ( - )) diff --git a/client/src/components/TicketingTheater/TicketingTheater.js b/client/src/components/TicketingTheater/TicketingTheater.js index fc1071a..4c76ee1 100644 --- a/client/src/components/TicketingTheater/TicketingTheater.js +++ b/client/src/components/TicketingTheater/TicketingTheater.js @@ -1,20 +1,15 @@ import styles from "./ticketingTheater.module.scss" -const TicketingTheater = (props) => { + +const TicketingTheater = ({ ticketInfo, cinemaInfo, setTicketInfo }) => { function handleClick(event) { - // event.preventDefault() - console.log(event.target.name) - props.setTicketInfo({ ...props.ticketInfo, theater:event.target.name}) + setTicketInfo({ ...ticketInfo, cinema: event.target.name }) } return (
- {props.theaterInfo.theater.length > 0 - ? props.theaterInfo.theater.map(name => ( - - )) - :
영화관 정보가 존재하지 않습니다.
} +
) diff --git a/client/src/components/TicketingTimeTable/TicketingTimeTable.js b/client/src/components/TicketingTimeTable/TicketingTimeTable.js index e17e0ad..8b26dc9 100644 --- a/client/src/components/TicketingTimeTable/TicketingTimeTable.js +++ b/client/src/components/TicketingTimeTable/TicketingTimeTable.js @@ -1,10 +1,10 @@ -const TicketingTimeTable = (props) => { +const TicketingTimeTable = ({ ticketInfo }) => { + return (
-
- {console.log(props.ticketInfo.movieId, props.ticketInfo.theater)} - {props.ticketInfo.movieId && props.ticketInfo.theater - ?
{props.ticketInfo.movieId} {props.ticketInfo.theater}
+
+ {ticketInfo.movieId && ticketInfo.cinema + ?
{ticketInfo.movieId} {ticketInfo.cinema}
:
영화와 극장을 모두 선택해주세요.
}
diff --git a/client/src/components/Video.js b/client/src/components/Video.js index ac36712..a29b645 100644 --- a/client/src/components/Video.js +++ b/client/src/components/Video.js @@ -1,19 +1,24 @@ import { useEffect, useState } from 'react' import movieApi from '../apis/movie.api.js' -const Video = (props) => { +import catchErrors from "../utils/catchErrors.js" + +const Video = ({ movieId }) => { const [videoUrls, setVideoUrls] = useState([]) + const [error, setError] = useState("") + useEffect(() => { getVideos() }, []) async function getVideos() { try { - const data = await movieApi.getVideosfromTM(props.movieId) + const data = await movieApi.getVideosfromTM(movieId) setVideoUrls(data) } catch (error) { - console.log(error) + catchErrors(error, setError) } } + return (
{videoUrls.length > 0 diff --git a/client/src/context/auth_context.js b/client/src/context/auth_context.js index 4132158..f0341b4 100644 --- a/client/src/context/auth_context.js +++ b/client/src/context/auth_context.js @@ -1,15 +1,11 @@ -import axios from "axios"; -import { createContext, useCallback, useContext, useState } from "react"; +import { createContext, useCallback, useContext, useEffect, useState } from "react"; import authApi from "../apis/auth.api"; -import { getLocalUser } from "../utils/auth"; -import {baseUrl} from "../utils/baseUrl"; import catchErrors from "../utils/catchErrors"; -import config from "../utils/clientConfig"; const AuthContext = createContext({ error: "", loading: false, - user: {id:0, role:"user"}, + user: { id: 0, role: "user" }, setUser: () => { }, login: () => Promise.resolve(false), logout: () => { }, @@ -19,16 +15,24 @@ const AuthContext = createContext({ const AuthProvider = ({ children }) => { const [error, setError] = useState(""); const [loading, setLoading] = useState(false); - const [user, setUser] = useState(getLocalUser()); + const [user, setUser] = useState({ id: 0, role: "user" }); + + const getUser = async () => { + const { id, role } = await authApi.getUser(); + const user = { "id": id, "role": role }; + setUser(user); + }; + + useEffect(() => { + getUser(); + }, []); const login = useCallback(async (id, password) => { try { setError(""); setLoading(true); const user = await authApi.login(id, password); - localStorage.setItem(config.loginUser, JSON.stringify(user)); setUser(user); - return true; } catch (error) { catchErrors(error, setError); @@ -41,11 +45,10 @@ const AuthProvider = ({ children }) => { const logout = useCallback(async () => { try { setError(""); - setUser(null); - alert("로그아웃되었습니다."); - localStorage.removeItem(config.loginUser); setLoading(true); - await axios.get(`${baseUrl}/api/auth/logout`); + const user = await authApi.logout(); + setUser(user); + alert("로그아웃되었습니다."); } catch (error) { catchErrors(error, setError); } finally { @@ -64,7 +67,6 @@ const AuthProvider = ({ children }) => { if (data.redirectUrl) { errorMsg = data.message; console.log("Error response with redirected message:", errorMsg); - console.log("redirect url", data.redirectUrl); return await logout(); } } @@ -75,7 +77,6 @@ const AuthProvider = ({ children }) => { errorMsg = error.message; console.log("Error message:", errorMsg); } - displayError(errorMsg); }, []); diff --git a/client/src/pages/HomePage.js b/client/src/pages/HomePage.js index 4fd44cc..4efc9b8 100644 --- a/client/src/pages/HomePage.js +++ b/client/src/pages/HomePage.js @@ -1,13 +1,36 @@ - +import { useState, useEffect } from "react" import BoxOffice from "../components/BoxOffice"; import Collection from "../components/Collection"; import Footer from "../components/Footer"; +import movieApi from '../apis/movie.api' +import catchErrors from '../utils/catchErrors.js' const HomePage = () => { + const [TMDB_TopRated_Data, setTMDB_TopRated_Data] = useState([]) + const [error, setError] = useState("") + const category = "popular" + + useEffect(() => { + getTMDB_TopRated() + }, []) + + async function getTMDB_TopRated() { + try { + setError("") + const data = await movieApi.getListByCategoryfromDB(category) + data.sort(function (a, b) { + return b.popularity - a.popularity + }) + setTMDB_TopRated_Data([...data]) + } catch (error) { + catchErrors(error, setError) + } + } + return ( <> - - + +
) diff --git a/client/src/pages/MovieListPage.js b/client/src/pages/MovieListPage.js index a87ab1d..945c62b 100644 --- a/client/src/pages/MovieListPage.js +++ b/client/src/pages/MovieListPage.js @@ -3,28 +3,15 @@ import MovieChart from '../components/MovieChart.js' import MovieComing from '../components/MovieComing.js' const MovieListPage = () => { - const [state, setState] = useState(true) + const [state, setState] = useState(0) return (
-
-
    -
  • - -
  • -
  • - -
  • -
-
-
-
- -
-
- -
+
+ +
+
{state === 0 ? : }
) } diff --git a/client/src/pages/MoviePage.js b/client/src/pages/MoviePage.js index 1e63d7a..a88c6d1 100644 --- a/client/src/pages/MoviePage.js +++ b/client/src/pages/MoviePage.js @@ -1,18 +1,18 @@ import { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; -import movieApi from '../apis/movie.api.js' -import Video from '../components/Video.js' +import Video from '../components/Video.js'; +import movieApi from '../apis/movie.api.js'; +import catchErrors from "../utils/catchErrors.js"; const MoviePage = ({ location }) => { const [movieInfo, setMovieInfo] = useState({ ...location.state, stillCuts: [], - cast: "", - director: "", - // genres: [], - attendance: "" + cast: [], + director: [] }) const [state, setState] = useState(0) + const [error, setError] = useState("") useEffect(() => { getImagesAndCredits() @@ -24,32 +24,20 @@ const MoviePage = ({ location }) => { const still = images.backdrops.map(el => el.file_path) const credits = await movieApi.getCreditsfromTM(movieInfo.id) const castsInfo = credits.cast.map(el => el.name) - const casts = castsInfo.reduce((acc, cur, idx) => { - if (idx !== 0) return acc + ', ' + cur - else return acc + cur - }, "") - console.log(castsInfo) - const directorsInfo = await credits.crew.filter(element => element.job === "Director") - const directors = directorsInfo.reduce((acc, cur, idx) => { - if (idx !== 0) return acc + ', ' + cur.name - else return acc + cur.name - }, "") - - console.log("directorInfo=", directorsInfo) + const directorsInfo = await credits.crew.filter(element => element.job === "Director").map(el => el.name) setMovieInfo({ ...movieInfo, stillCuts: still, - cast: casts, - director: directors + cast: castsInfo, + director: directorsInfo }) } catch (error) { - console.log(error) + catchErrors(error, setError) } } return (
- {console.log(movieInfo)}
{movieInfo.stillCuts.length > 0 @@ -76,11 +64,18 @@ const MoviePage = ({ location }) => {
영화포스터
-
+

{movieInfo.title}

-

예매율: 0% 누적관객수: {movieInfo.attendance}명

-

감독: {movieInfo.director}

-

출연: {movieInfo.cast}

+

예매율:{Math.round((movieInfo.ticket_sales / (movieInfo.totalReservationRate.totalReservationRate || 1)) * 100)}% 누적관객수: {movieInfo.ticket_sales}명

+ {movieInfo.director || movieInfo.cast + ? + <> +

감독: {movieInfo.director.map(el => el) + ' '}

+

출연: {movieInfo.cast.slice(0, 5).map(el => el) + ' '}

+ + : + <> + }

장르: {movieInfo.genres.reduce((acc, cur, idx) => { if (idx !== 0) return acc + ', ' + cur.name else return acc + cur.name @@ -98,7 +93,7 @@ const MoviePage = ({ location }) => {

-
+
  • @@ -113,13 +108,13 @@ const MoviePage = ({ location }) => {
-
{movieInfo.overview}
+
{movieInfo.overview}
-
관람평
+
관람평
diff --git a/client/src/pages/MyPage.js b/client/src/pages/MyPage.js index 7fa4a73..b64f128 100644 --- a/client/src/pages/MyPage.js +++ b/client/src/pages/MyPage.js @@ -1,9 +1,11 @@ import MyInfo from "../components/MyInfo/MyInfo" +import ReservationDetails from "../components/ReservationDetails"; const MyPage = () => { return ( -
+
+
) } diff --git a/client/src/pages/PaymentCompletePage.js b/client/src/pages/PaymentCompletePage.js new file mode 100644 index 0000000..c724c23 --- /dev/null +++ b/client/src/pages/PaymentCompletePage.js @@ -0,0 +1,72 @@ +import axios from 'axios' +import { useEffect, useState } from 'react' +import { useAuth } from '../context/auth_context' +import catchErrors from '../utils/catchErrors' + + +const PaymentCompletePage = () => { + const { user } = useAuth() + const [error, setError] = useState() + + useEffect(() => { + if (user.role === "member") { + getUserInfo() + } else { + getGuestInfo() + } + }, [user]) + + async function getGuestInfo() { + try { + if (user.id > 0) { + const response = await axios.get(`/api/auth/guestinfo/${user.id}`) + const response2 = await axios.post(`/api/reservation/findonereservation`, { + userType: "guest", + user: user.id + }) + if (response.data || response2.data) { + const responseEmail = await axios.post('/api/email/send', { + reservationData: [...response2.data], + userData: { ...response.data }, + cinema: "Butter Studio 조치원", + title: "더 수어사이드 스쿼드", + theater: "1", + time: "2021/07/21 10:00" + }) + } + } + } catch (error) { + catchErrors(error, setError) + } + } + + async function getUserInfo() { + try { + const response = await axios.post(`/api/auth/getuserinfo`, { + id: user.id + }) + const response2 = await axios.post(`/api/reservation/findonereservation`, { + userType: "member", + user: user.id + }) + if (response.data || response2.data) { + const responseEmail = await axios.post('/api/email/send', { + ...response2.data, + ...response.data, + }) + } + } catch (error) { + catchErrors(error, setError) + } + } + + return ( +
+

예매가 정상적으로 완료되었습니다.

+ + {user.role === "member" ? : <>} +
+ ) +} + +export default PaymentCompletePage \ No newline at end of file diff --git a/client/src/pages/PaymentPage.js b/client/src/pages/PaymentPage.js deleted file mode 100644 index 64ea0b0..0000000 --- a/client/src/pages/PaymentPage.js +++ /dev/null @@ -1,54 +0,0 @@ -import axios from 'axios' -import { useState } from 'react' -import Kakaopay from '../components/Kakaopay' - -const Payment = ({ location }) => { - const [ticketInfo, setTicketInfo] = useState({ ...location.state }) - - async function SendMail(e) { - try { - const response = await axios.post('/api/email/send',{ - email:e.target.name - }) - console.log(response.data) - } catch (error) { - console.log(error) - } - } - - return ( -
- {console.log(ticketInfo)} -
-
-

결제하기

-
-
-
-
-
결제방법
- - -
-
- 영화포스터 -
{ticketInfo.title}
-
{ticketInfo.theater}
-
{ticketInfo.time}
-
{ticketInfo.selectedCinemaNum}관 {ticketInfo.selectedSeats}
-
-
청소년: {ticketInfo.teenager}명
-
성인: {ticketInfo.adult}명
-
경로우대: {ticketInfo.elderly}명
-
총 결제금액: {ticketInfo.teenager*7000 +ticketInfo.adult*8000+ticketInfo.elderly*6000}
-
-
-
-
- -
-
- ) -} - -export default Payment \ No newline at end of file diff --git a/client/src/pages/PaymentPage/PaymentPage.js b/client/src/pages/PaymentPage/PaymentPage.js new file mode 100644 index 0000000..b18710a --- /dev/null +++ b/client/src/pages/PaymentPage/PaymentPage.js @@ -0,0 +1,224 @@ +import axios from 'axios' +import { useEffect, useState } from 'react' +import { useAuth } from '../../context/auth_context' +import catchErrors from '../../utils/catchErrors' +import styles from './PaymentPage.module.scss' + +const Payment = ({ location }) => { + const { user } = useAuth() + const [guestInfo, setGuestInfo] = useState({}) + const [guestID, setGuestID] = useState() + const [userInfo, setUserInfo] = useState({ + nickname: "", + email: "", + birth: "", + phoneNumber: "" + }) + const [ticketInfo, setTicketInfo] = useState({ ...location.state }) + const [element, setElement] = useState() + const [error, setError] = useState("") + + useEffect(() => { + if (user.role === "member") { + getUserInfo() + } + }, []) + + async function getUserInfo() { + try { + setError("") + const response = await axios.post(`/api/auth/getuserinfo`, { + id: user.id + }) + setUserInfo(response.data) + } catch (error) { + catchErrors(error, setError) + } + } + + function handleChangeGuest(e) { + setGuestInfo({ ...guestInfo, [e.target.name]: String(e.target.value) }) + } + + async function handleClickGuest() { + try { + setError("") + const response = await axios.post('/api/auth/guest/save', { + ...guestInfo + }) + setGuestID(response.data.id) + alert("비회원 정보가 저장되었습니다.") + } catch (error) { + catchErrors(error, setError) + } + } + + function kakaoBtnClick() { + setElement( +
+

'카카오페이'

를 선택하셨습니다.

+

결제하기를 눌러 결제를 이어가주세요.

+
+ ) + setTicketInfo({ ...ticketInfo, payment: "카카오페이" }) + } + + async function reservationComplete() { + try { + setError("") + if (user.role === "member") { + const response = await axios.post(`/api/reservation/save`, { + userType: "member", + user: userInfo.id, + ...ticketInfo, + timetable: 1 + }) + const responsekakao = await axios.post('/api/kakaopay/test/single', { + cid: 'TC0ONETIME', + partner_order_id: 'orderNum', + partner_user_id: userInfo.id || guestInfo.id, + item_name: ticketInfo.title, + quantity: ticketInfo.adult + ticketInfo.youth + ticketInfo.senior, + total_amount: ticketInfo.totalFee, + vat_amount: 0, + tax_free_amount: 0, + approval_url: 'http://localhost:3000/paymentcomplete', + fail_url: 'http://localhost:3000/ticket', + cancel_url: 'http://localhost:3000/ticket', + }) + if (response && responsekakao) { + window.location.href = responsekakao.data.redirect_url + } + } else { + if (guestID) { + const response = await axios.post(`/api/reservation/save`, { + userType: "guest", + user: guestID, + ...ticketInfo, + timetable: 1 + }) + const responsekakao = await axios.post('/api/kakaopay/test/single', { + cid: 'TC0ONETIME', + partner_order_id: 'orderNum', + partner_user_id: 'user', + item_name: ticketInfo.title, + quantity: ticketInfo.adult + ticketInfo.youth + ticketInfo.senior, + total_amount: ticketInfo.totalFee, + vat_amount: 0, + tax_free_amount: 0, + approval_url: 'http://localhost:3000/paymentcomplete', + fail_url: 'http://localhost:3000/ticket', + cancel_url: 'http://localhost:3000/ticket', + }) + if (response && responsekakao) { + window.location.href = responsekakao.data.redirect_url + } + } else { + alert("비회원 정보를 모두 입력 후 비회원 정보 저장 버튼을 눌러주세요.") + } + } + } catch (error) { + catchErrors(error, setError) + } + } + + + return ( +
+
+
+

결제하기

+
+
+
+
+ {user.role === "member" + ? +
+
회원정보
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+

+ ※ 회원정보 변경은 마이페이지에서 가능합니다. +

+
+
+ : +
+
비회원예매 정보입력
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+

+ ※ 비회원 정보 오기입 시 예매 내역 확인/취소 및 티켓 발권이 어려울 수 있으니 다시 한번 확인해 주시기 바랍니다. +

+
+ +
+ } +
결제방법
+ + + {element} +
+ +
+
+ +
+
+ 영화포스터 +
{ticketInfo.title}
+
{ticketInfo.cinema}
+
{ticketInfo.time}
+
{ticketInfo.selectedTheater}관 {ticketInfo.selectedSeats.map(el => String.fromCharCode(parseInt(el.split('-')[0]) + 65) + el.split('-')[1]) + ' '}
+
+
성인: {ticketInfo.adult}명
+
청소년: {ticketInfo.youth}명
+
경로우대: {ticketInfo.senior}명
+
총 결제금액: {ticketInfo.totalFee}원
+
+
+
+
+ +
+ ) +} + +export default Payment \ No newline at end of file diff --git a/client/src/pages/PaymentPage/PaymentPage.module.scss b/client/src/pages/PaymentPage/PaymentPage.module.scss new file mode 100644 index 0000000..0b66cbd --- /dev/null +++ b/client/src/pages/PaymentPage/PaymentPage.module.scss @@ -0,0 +1,10 @@ +.labelStyle { + display: inline-block; + width: 80px; + text-align: right; + margin-right: 1rem; +} + +.warningText { + font-size: small; +} \ No newline at end of file diff --git a/client/src/pages/PaymentPage/index.js b/client/src/pages/PaymentPage/index.js new file mode 100644 index 0000000..415835b --- /dev/null +++ b/client/src/pages/PaymentPage/index.js @@ -0,0 +1 @@ +export { default } from "./PaymentPage" \ No newline at end of file diff --git a/client/src/pages/TheaterPage.js b/client/src/pages/TheaterPage.js index 3a7558c..1d123d3 100644 --- a/client/src/pages/TheaterPage.js +++ b/client/src/pages/TheaterPage.js @@ -1,10 +1,13 @@ -import {useState} from 'react' +import { useState } from 'react' +import TicketFeeTable from '../components/Admin/TicketFeeTable' +import TheaterInfo from '../components/TheaterInfo' const TheaterPage = () => { const [state, setState] = useState(0) + return (
-
+
  • @@ -19,13 +22,17 @@ const TheaterPage = () => {
-
극장정보
+
상영시간표
-
관람료
+
+
+ +
+
diff --git a/client/src/pages/TicketingPage.js b/client/src/pages/TicketingPage.js index 41206c8..47fe2e3 100644 --- a/client/src/pages/TicketingPage.js +++ b/client/src/pages/TicketingPage.js @@ -1,22 +1,26 @@ import { useState, useEffect } from 'react' import { Link } from 'react-router-dom' import movieApi from '../apis/movie.api.js' +import cinemaApi from "../apis/cinema.api.js" import TicketingMovie from "../components/TicketingMovie/TicketingMovie.js" import TicketingTheater from "../components/TicketingTheater/TicketingTheater.js" import TicketingTimeTable from "../components/TicketingTimeTable/TicketingTimeTable.js" +import catchErrors from "../utils/catchErrors.js" const TicketingPage = ({ location }) => { const [ticketInfo, setTicketInfo] = useState({ ...location.state, - theater:"", - selectedCinemaNum: 3, + cinema: "", + selectedTheater: "1", time: "2021/07/21 10:00" }) - const [theaterInfo, setTheaterInfo] = useState({ - theater: ["Butter Studio 조치원"], - cinemaNum: [1, 2, 3, 4] - }) + const [cinemaInfo, setCinemaInfo] = useState({}) const [movieInfo, setMovieInfo] = useState() + const [error, setError] = useState("") + + useEffect(() => { + getCinemaInfo() + }, []) useEffect(() => { getMovieInfo() @@ -27,30 +31,36 @@ const TicketingPage = ({ location }) => { const data = await movieApi.getMovieInfofromTM(ticketInfo.movieId) setMovieInfo(data) } catch (error) { - console.log(error) + catchErrors(error, setError) + } + } + + async function getCinemaInfo() { + try { + const response = await cinemaApi.getCinemaInfo() + setCinemaInfo(response.data) + } catch (error) { + catchErrors(error, setError) } } return (
-
- {console.log(ticketInfo)} -

영화

-

극장

- +

극장

+

시간표

- +
-
+
{movieInfo ? 영화포스터 @@ -58,28 +68,26 @@ const TicketingPage = ({ location }) => {
극장선택
- {movieInfo && ticketInfo.theater + {movieInfo && ticketInfo.cinema ?
  • 영화: {movieInfo.title}
  • -
  • 극장: {ticketInfo.theater}
  • +
  • 극장: {ticketInfo.cinema}
  • 일시: {ticketInfo.time}
  • -
  • 상영관: {ticketInfo.selectedCinemaNum}
  • +
  • 상영관: {ticketInfo.selectedTheater}
:
}
좌석선택
- {movieInfo && ticketInfo.theater + {movieInfo && ticketInfo.cinema ? 예매하기 - : - 예매하기 - + : 예매하기 }
diff --git a/client/src/pages/TicketingSeatPage.js b/client/src/pages/TicketingSeatPage.js index a82ec12..86d1a28 100644 --- a/client/src/pages/TicketingSeatPage.js +++ b/client/src/pages/TicketingSeatPage.js @@ -1,100 +1,221 @@ -import { Link } from 'react-router-dom' -import { useState } from 'react' +import { useState, useEffect, useRef } from 'react' +import { Link, useHistory } from 'react-router-dom' import CountButton from '../components/CountButton' import SeatTable from '../components/SeatTable/SeatTable' +import axios from 'axios' +import { useAuth } from '../context/auth_context.js' +import { Modal } from 'bootstrap' +import catchErrors from '../utils/catchErrors' +import styles from '../components/SeatTable/seatTable.module.scss' const TicketingSeatPage = ({ location }) => { + const history = useHistory() + const modalRef = useRef(null) + const modal = useRef() + const { user } = useAuth() + const [error, setError] = useState() const [ticketInfo, setTicketInfo] = useState({ ...location.state }) + const [theaterInfo, setTheaterInfo] = useState({ theatertypeId: 0 }) const [selectedSeats, setSelectedSeats] = useState([]) + const [reservedSeats, setReservedSeats] = useState([]) + const [ticketFee, setTicketFee] = useState({ + youth: 0, + adult: 0, + senior: 0 + }) const [count, setCount] = useState({ + youth: 0, adult: 0, - teenager: 0, - elderly: 0 + senior: 0 }) - const allSeat = { row: 6, col: 10 } - return ( -
- {console.log(ticketInfo)} -
-
-

좌석선택

-
-
-
-
-
-
-
일반
-
청소년
-
경로우대
-
-
- - - -
-
- {/* 일반 - + useEffect(() => { + getInfo() + }, []) + + useEffect(() => { + getTicketFee() + }, [theaterInfo.theatertypeId]) - 청소년 - + async function getInfo() { + try { + const response = await axios.post('/api/theater/getInfo', { + theaterName: ticketInfo.selectedTheater + }) + setTheaterInfo(response.data) + const response2 = await axios.post('/api/reservation/findreservation', { + timetable: 1 + }) + const reserve = response2.data.map((el) => + el.row + '-' + el.col + ) + setReservedSeats(reserve) + } catch (error) { + catchErrors(error, setError) + } + } - 경로우대 - */} + async function getTicketFee() { + try { + const response3 = await axios.get(`/api/info/ticketfee`, { + params: { + theaterTypeId: theaterInfo.theatertypeId + } + }) + const basicFee = response3.data[0].day + response3.data[0].defaultPrice + response3.data[0].weekdays + setTicketFee({ + adult: basicFee + response3.data[0].adult, + youth: basicFee + response3.data[0].youth, + senior: basicFee + response3.data[0].senior + }) + } catch (error) { + catchErrors(error, setError) + } + } -
-
-
{ticketInfo.theater} | {ticketInfo.selectedCinemaNum}관
-
{ticketInfo.title}
-
{ticketInfo.time}
+ function loginModal() { + if (user.role === "member") { + history.push("/payment", { + ...ticketInfo, selectedSeats: selectedSeats, ...count, totalFee: count.adult * ticketFee.adult + count.youth * ticketFee.youth + count.senior * ticketFee.senior + }); + } else { + modal.current = new Modal(modalRef.current) + modal.current?.show() + } + } + + return ( + <> +
+
+
+
+
로그인이 필요한 서비스입니다.
+ +
+
+ 로그인을 하시겠습니까? 비회원예매로 진행하시겠습니까? +
+
+ + + + + + +
+
-
-
- -
-
-

선택됨

-

선택불가

+
+
+
+

좌석선택

+
-
-
-
- {ticketInfo - ? 영화포스터 - :
영화선택
} +
+
+
+
+
+ 일반 + + + +
+
+ 청소년 + + {ticketInfo.adult + ? + + : + + } + +
+
+ 경로우대 + + + +
+
+
+
+
+
{ticketInfo.cinema} | {ticketInfo.selectedTheater}관
+
{ticketInfo.title}
+
{ticketInfo.time}
+
-
-
극장선택
- {ticketInfo - ?
    -
  • 영화: {ticketInfo.title}
  • -
  • 극장: {ticketInfo.theater}
  • -
  • 일시: 2021/07/21 10:00
  • -
  • 상영관: 3관
  • -
  • 좌석: {selectedSeats}
  • -
- :
} +
+
+ +
+
+
+ + 선택됨 +
+
+ + 선택불가 +
+
-
-
결제하기
- {ticketInfo - ? - - 예매하기 - - : - 예매하기 +
+
+ {ticketInfo + ? 영화포스터 + :
영화선택
} +
+
+ {ticketInfo + ?
    +
  • 영화: {ticketInfo.title}
  • +
  • 극장: {ticketInfo.cinema}
  • +
  • 일시: 2021/07/21 10:00
  • +
  • 상영관: {ticketInfo.selectedTheater}관
  • +
  • 좌석: {selectedSeats.map(el => String.fromCharCode(parseInt(el.split('-')[0]) + 64) + el.split('-')[1]) + ' '}
  • +
+ : +
극장선택
+ } +
+
+ {selectedSeats + ? +
    +
  • 성인: {count.adult}명
  • +
  • 청소년: {count.youth}명
  • +
  • 경로: {count.senior}명
  • +
  • 총 결제금액: {count.adult * ticketFee.adult + count.youth * ticketFee.youth + count.senior * ticketFee.senior}원
  • +
+ : <>} +
+
+
결제하기
+ {selectedSeats.length > 0 && count.adult + count.youth + count.senior === selectedSeats.length + ? + + : + - } + } +
-
+ ) } diff --git a/client/src/utils/auth.js b/client/src/utils/auth.js index 6cf8c67..16ea0d2 100644 --- a/client/src/utils/auth.js +++ b/client/src/utils/auth.js @@ -1,13 +1,13 @@ -import config from "./clientConfig"; +import clientConfig from "./clientConfig"; export function handleLogin(user) { if (user) { - localStorage.setItem(config.loginUser, JSON.stringify(user)); + localStorage.setItem(clientConfig.loginUser, JSON.stringify(user)); } } export function getLocalUser() { - const userData = localStorage.getItem(config.loginUser); + const userData = localStorage.getItem(clientConfig.loginUser); let user = null; if (userData) { user = JSON.parse(userData); diff --git a/package-lock.json b/package-lock.json index f691755..2fa437d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,29 +39,6 @@ "negotiator": "0.6.2" } }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "requires": { - "debug": "4" - }, - "dependencies": { - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, "ansi-align": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", @@ -124,11 +101,6 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" - }, "axios": { "version": "0.21.1", "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", @@ -267,15 +239,6 @@ } } }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -458,17 +421,17 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" + }, "crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "dev": true }, - "dayjs": { - "version": "1.10.6", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.6.tgz", - "integrity": "sha512-AztC/IOW4L1Q41A86phW5Thhcrco3xuAA+YX/BLpLWWjRcTj5TOt/QImBLmCKlrF7u7k47arTnOyL6GnbG8Hvw==" - }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -666,6 +629,11 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, + "fs": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", + "integrity": "sha1-invTcYa23d84E/I4WLV+yq9eQdQ=" + }, "fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -673,21 +641,6 @@ "dev": true, "optional": true }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - } - }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -740,25 +693,12 @@ "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", "dev": true }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, - "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" - }, "has-yarn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", @@ -783,30 +723,6 @@ "toidentifier": "1.0.0" } }, - "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "requires": { - "agent-base": "6", - "debug": "4" - }, - "dependencies": { - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1247,11 +1163,6 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, - "object-inspect": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==" - }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -1378,11 +1289,6 @@ "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", "dev": true }, - "pop-iterate": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pop-iterate/-/pop-iterate-1.0.1.tgz", - "integrity": "sha1-zqz9q0q/NT16DyqqLB/Hs/lBO6M=" - }, "postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -1451,26 +1357,11 @@ "escape-goat": "^2.0.0" } }, - "q": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/q/-/q-2.0.3.tgz", - "integrity": "sha1-dbjbAlWhpa+C9Yw/Oqoe/sfQ0TQ=", - "requires": { - "asap": "^2.0.0", - "pop-iterate": "^1.0.1", - "weak-map": "^1.0.5" - } - }, "qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, - "querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" - }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -1537,11 +1428,6 @@ "rc": "^1.2.8" } }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" - }, "responselike": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", @@ -1559,11 +1445,6 @@ "any-promise": "^1.3.0" } }, - "rootpath": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/rootpath/-/rootpath-0.1.2.tgz", - "integrity": "sha1-Wzeah9ypBum5HWkKWZQ5vvJn6ms=" - }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -1574,11 +1455,6 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "scmp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", - "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==" - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -1668,6 +1544,11 @@ "requires": { "lru-cache": "^6.0.0" } + }, + "validator": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz", + "integrity": "sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==" } } }, @@ -1692,16 +1573,6 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", @@ -1860,34 +1731,6 @@ "nopt": "~1.0.10" } }, - "twilio": { - "version": "3.66.0", - "resolved": "https://registry.npmjs.org/twilio/-/twilio-3.66.0.tgz", - "integrity": "sha512-2jek7akXcRMusoR20EWA1+e5TQp9Ahosvo81wTUoeS7H24A1xbVQJV4LfSWQN4DLUY1oZ4d6tH2oCe/+ELcpNA==", - "requires": { - "axios": "^0.21.1", - "dayjs": "^1.8.29", - "https-proxy-agent": "^5.0.0", - "jsonwebtoken": "^8.5.1", - "lodash": "^4.17.21", - "q": "2.0.x", - "qs": "^6.9.4", - "rootpath": "^0.1.2", - "scmp": "^2.1.0", - "url-parse": "^1.5.0", - "xmlbuilder": "^13.0.2" - }, - "dependencies": { - "qs": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", - "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", - "requires": { - "side-channel": "^1.0.4" - } - } - } - }, "type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", @@ -1966,15 +1809,6 @@ "xdg-basedir": "^4.0.0" } }, - "url-parse": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz", - "integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==", - "requires": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "url-parse-lax": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", @@ -2000,20 +1834,15 @@ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, "validator": { - "version": "10.11.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz", - "integrity": "sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==" + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.6.0.tgz", + "integrity": "sha512-gVgKbdbHgtxpRyR8K0O6oFZPhhB5tT1jeEHZR0Znr9Svg03U0+r9DXWMrnRAB+HtCStDQKlaIZm42tVsVjqtjg==" }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, - "weak-map": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/weak-map/-/weak-map-1.0.5.tgz", - "integrity": "sha1-eWkVhNmGB/UHC9O3CkDmuyLkAes=" - }, "widest-line": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", @@ -2055,11 +1884,6 @@ "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", "dev": true }, - "xmlbuilder": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", - "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==" - }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 60832a5..ae45c1b 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,10 @@ "axios": "^0.21.1", "bcryptjs": "^2.4.3", "cookie-parser": "^1.4.5", + "crypto-js": "^4.1.1", "dotenv": "^10.0.0", "express": "^4.17.1", + "fs": "0.0.1-security", "jsonwebtoken": "^8.5.1", "moment": "^2.29.1", "multer": "^1.4.2", @@ -28,7 +30,7 @@ "pg": "^8.6.0", "pg-hstore": "^2.3.4", "sequelize": "^6.6.4", - "twilio": "^3.66.0" + "validator": "^13.6.0" }, "devDependencies": { "nodemon": "^2.0.12" diff --git a/server/app.js b/server/app.js index 4f40321..430cf6c 100644 --- a/server/app.js +++ b/server/app.js @@ -7,8 +7,7 @@ const app = express() app.use(express.json()) app.use(express.urlencoded({ extended: true })) app.use(cookieParser()) - +app.use('/upload', express.static('upload')) app.use('/api', mainRouter) - export default app \ No newline at end of file diff --git a/server/config/app.config.js b/server/config/app.config.js index 6ad02e9..020f5db 100644 --- a/server/config/app.config.js +++ b/server/config/app.config.js @@ -4,6 +4,7 @@ const config = { jwtSecret: 'dfkasf23i$efksdfks!', jwtExpires: '7d', cookieName: 'butterStudio', + cookieNameMb: 'confirmNum', cookieMaxAge: 60 * 60 * 24 * 7 * 1000, kakaoAdminKey: 'e3ce7106688a35e072e2630daa9d7250', } diff --git a/server/controllers/email.controller.js b/server/controllers/email.controller.js index 7b96848..61c46ec 100644 --- a/server/controllers/email.controller.js +++ b/server/controllers/email.controller.js @@ -1,49 +1,55 @@ import nodemailer from "nodemailer" -const SendMail = async (req,res) => { - // console.log(req.body) - const {email} = req.body - console.log(email) - const sendMail = async (email) => { - // 메일을 전달해줄 객체 - const transporter = nodemailer.createTransport({ - // service: "gmail", - host: 'smtp.gmail.com', - port: 465, - secure: true, - auth: { - type: "OAuth2", - user: "angelayoon99@gmail.com", - clientId: process.env.GMAIL_CLIENTID, - clientSecret: process.env.GMAIL_CLIENTSECRET, - accessToken: process.env.GMAIL_ACCESS_TOKEN, - refreshToken: process.env.GMAIL_REFRESH_TOKEN, - }, - tls: { - rejectUnauthorized: false, - }, - }); - - // 메일 옵션 - const mailOptions = { - from: `윤지원 `, - to: "jiwon5393@naver.com", - subject: "사용자 계정 확인용 메일.", - text: "Test Mail from Test Server.", - }; - - // 메일 전송 - try { - const mailResult = await transporter.sendMail(mailOptions); - console.log(`Mail sent - ID : ${mailResult.messageId}`); - } catch (err) { - console.log("Mail Sending Failuer."); - console.log(err); - } - } - - sendMail(email); -} +const SendMail = async (req, res) => { + const { email, name, nickname } = req.body.userData + const { title, cinema, time, theater } = req.body + const selectedSeats = req.body.reservationData.map(el => String.fromCharCode(el.row + 64) + el.col) + const sendMail = async (email, title, cinema, theater, time, name, selectedSeats, nickname) => { + const transporter = nodemailer.createTransport({ + host: 'smtp.gmail.com', + port: 465, + secure: true, + auth: { + type: "OAuth2", + user: "angelayoon99@gmail.com", + clientId: process.env.GMAIL_CLIENTID, + clientSecret: process.env.GMAIL_CLIENTSECRET, + refreshToken: process.env.GMAIL_REFRESH_TOKEN, + }, + tls: { + rejectUnauthorized: false, + }, + }); + // 메일 옵션 + const mailOptions = { + from: `${cinema} `, + to: `${email}`, + subject: `${cinema} 예매확인내역: ${title}`, + html: `
+

+ ${name || nickname}님의 예매 +

+
+ 영화: ${title} +
+
+ 장소: ${cinema} ${theater}관 +
+
+ 일시 및 좌석: ${time} / ${selectedSeats.map(el => el + ' ')} +
+
` + }; + try { + const mailResult = await transporter.sendMail(mailOptions); + console.log(`Mail sent - ID : ${mailResult.messageId}`); + } catch (err) { + console.log("Mail Sending Failuer."); + console.log(err); + } + } + sendMail(email, title, cinema, theater, time, name, selectedSeats, nickname); +} export default { SendMail } \ No newline at end of file diff --git a/server/controllers/kakaopay.controller.js b/server/controllers/kakaopay.controller.js index 1a7bf37..ad55e2e 100644 --- a/server/controllers/kakaopay.controller.js +++ b/server/controllers/kakaopay.controller.js @@ -36,12 +36,10 @@ const singleTest = async (req, res) => { }, }) const resp = response.data - console.log('resp', resp) res.json({ redirect_url: resp.next_redirect_pc_url }) } catch (error) { console.log(error) } - } export default { success, fail, cancel, singleTest } \ No newline at end of file diff --git a/server/controllers/movie.controller.js b/server/controllers/movie.controller.js index e89336f..3f6e96a 100644 --- a/server/controllers/movie.controller.js +++ b/server/controllers/movie.controller.js @@ -12,48 +12,6 @@ const getListfromDB = async (req, res) => { } } -const getMovieByCategory = async (req, res, next, category) => { - try { - const TMDBmovieIds = [] - const movieIds = [] - const response = await axios.get(`https://api.themoviedb.org/3/movie/${category}?api_key=${process.env.TMDB_APP_KEY}&language=ko-KR&page=1`) - const TMDBmovies = response.data.results - - TMDBmovies.forEach(element => { - TMDBmovieIds.push(element.id) - }) - const responseAfterCompare = await Movie.findAll({ - where: { - movieId: { - [Op.or]: TMDBmovieIds - } - } - }) - responseAfterCompare.forEach(el => { - movieIds.push(el.movieId) - }) - req.movieIds = movieIds - next() - } catch (error) { - return res.status(500).send(error.message || "영화 가져오기 중 에러 발생"); - } -} - -const getMovieById = async (req, res) => { - try { - const movieIds = req.movieIds - const elements = await Promise.all( - movieIds.map(async (movieId) => { - const movie = await axios.get(`https://api.themoviedb.org/3/movie/${movieId}?api_key=${process.env.TMDB_APP_KEY}&language=ko-KR`) - return movie.data - }) - ) - res.json(elements) - } catch (error) { - return res.status(500).send(error.message || "영화 가져오기 중 에러 발생"); - } -} - const movieforAdmin = async (req, res) => { try { const TMDBmovieIds = [] @@ -111,6 +69,7 @@ const getAllMovie = async (req, res, next) => { const getMovieList = async (req, res) => { try { + const { category } = req.params const movieList = await Movie.findAll() const movieIds = [] movieList.forEach(el => { @@ -119,10 +78,45 @@ const getMovieList = async (req, res) => { const elements = await Promise.all( movieIds.map(async (movieId) => { const movie = await axios.get(`https://api.themoviedb.org/3/movie/${movieId}?api_key=${process.env.TMDB_APP_KEY}&language=ko-KR`) - return movie.data + const cols = await Movie.findOne({ + where: { "movieId": movieId }, + attributes: ["ticket_sales", "vote_average"] + }) + const totalReservationRate = await Movie.findAll({ + attributes: [[sequelize.fn('SUM', sequelize.col('ticket_sales')), 'totalReservationRate']] + }); + return { ...movie.data, ticket_sales: cols.ticket_sales, vote_average: cols.vote_average, totalReservationRate: totalReservationRate[0] } + }) ) - res.json(elements) + if (category === "popular") { + for (let i = 0; i < elements.length; i++) { + if (new Date(elements[i].release_date) > new Date()) { + elements.splice(i, 1); + i--; + } + } + elements.sort(function (a, b) { + return b.popularity - a.popularity + }) + res.json(elements) + } else if (category === "upcoming") { + for (let i = 0; i < elements.length; i++) { + if (new Date(elements[i].release_date) <= new Date()) { + elements.splice(i, 1); + i--; + } + } + elements.sort(function (a, b) { + return a.release_date - b.release_date + }) + res.json(elements) + } else { + elements.sort(function (a, b) { + return a.title - b.title + }) + res.json(elements) + } } catch (error) { return res.status(500).send(error.message || "영화 정보 가져오는 중 에러 발생") } @@ -165,7 +159,14 @@ const findonlyTitle = async (req, res) => { const elements = await Promise.all( movieIds.map(async (movieId) => { const movie = await axios.get(`https://api.themoviedb.org/3/movie/${movieId}?api_key=${process.env.TMDB_APP_KEY}&language=ko-KR`) - return movie.data + const cols = await Movie.findOne({ + where: { "movieId": movieId }, + attributes: ["ticket_sales", "vote_average"] + }) + const totalReservationRate = await Movie.findAll({ + attributes: [[sequelize.fn('SUM', sequelize.col('ticket_sales')), 'totalReservationRate']] + }); + return { ...movie.data, ticket_sales: cols.ticket_sales, vote_average: cols.vote_average, totalReservationRate: totalReservationRate[0] } }) ) return res.json({ count: movieIds.length, results: elements }) @@ -189,8 +190,6 @@ const findaboutAll = async (req, res, next) => { export default { getListfromDB, - getMovieByCategory, - getMovieById, getAllMovie, getMovieList, create, diff --git a/server/controllers/reservation.controller.js b/server/controllers/reservation.controller.js new file mode 100644 index 0000000..88fb62e --- /dev/null +++ b/server/controllers/reservation.controller.js @@ -0,0 +1,84 @@ +import axios from 'axios' +import { Movie, Reservation, Theater } from '../db/index.js' +import sequelize from 'sequelize' +const { Op } = sequelize + +const findReservedSeats = async (req, res) => { + const { timetable } = req.body + try { + const reservedSeats = await Reservation.findAll({ + where: { + timetable: timetable + } + }) + res.json(reservedSeats) + } catch (error) { + res.status(500).send(error.message || "이미 예매되어있는 좌석을 찾는 중 오류발생") + } +} +const findReservation = async (req, res) => { + const { user } = req.body + try { + const reservation = await Reservation.findAll({ + where: { + user: user + } + }) + res.json(reservation) + } catch (error) { + res.status(500).send(error.message || "예매 내역들을 찾는 중 오류발생") + } +} +const findOneReservation = async (req, res) => { + const { userType, user } = req.body + try { + const reservation = await Reservation.findAll({ + where: { + userType: userType, + user: user + }, + }) + // console.log(reservation) + res.json(reservation) + } catch (error) { + res.status(500).send(error.message || "예매 내역을 찾는 중 오류 발생") + } +} +const saveReservation = async (req, res) => { + const { movieId, selectedTheater, timetable, payment, user, userType, totalFee } = req.body + const rows = req.body.selectedSeats.map(el => el.split('-')[0]) + const cols = req.body.selectedSeats.map(el => el.split('-')[1]) + try { + for (let index = 0; index < rows.length; index++) { + const reservation = await Reservation.create({ + user: user, + userType: userType, + movieId: movieId, + theater: selectedTheater, + row: rows[index], + col: cols[index], + timetable: timetable, + payment: payment, + totalFee: totalFee + }) + } + const movie = await Movie.findOne({ + where: { + movieId: movieId + } + }) + movie.ticket_sales++ + await movie.save(); + res.json({ message: '200 OK' }) + } catch (error) { + console.log(error) + res.status(500).send(error.message || "예매DB에 저장 실패") + } +} + +export default { + findReservedSeats, + findReservation, + findOneReservation, + saveReservation +} \ No newline at end of file diff --git a/server/controllers/theater.controller.js b/server/controllers/theater.controller.js index dbee3dc..484fb09 100644 --- a/server/controllers/theater.controller.js +++ b/server/controllers/theater.controller.js @@ -1,5 +1,18 @@ import { Theater, TheaterType } from "../db/index.js"; +const getTheaterInfo = async (req, res) => { + const { theaterName } = req.body + try { + const theaterInfo = await Theater.findOne({ + where: { theaterName: String(theaterName) }, + attributes: ['theaterName', 'rows', 'columns'] + }) + // console.log("theaterInfo====",theaterInfo) + return res.json(theaterInfo) + } catch (error){ + console.log(error) + } +} const getAll = async (req, res) => { try { const findList = await Theater.findAll({ attributes: { exclude: ['createdAt', 'updatedAt'] }, include: [TheaterType], order: [['theaterName']] }) @@ -64,5 +77,6 @@ export default { getOne, getTypes, submit, - remove -} \ No newline at end of file + remove, + getTheaterInfo +} diff --git a/server/controllers/user.controller.js b/server/controllers/user.controller.js index fa4cb6c..0781df4 100644 --- a/server/controllers/user.controller.js +++ b/server/controllers/user.controller.js @@ -1,14 +1,31 @@ import jwt from "jsonwebtoken"; import config from "../config/app.config.js"; -import { User, Role } from '../db/index.js'; -import Twilio from "twilio"; +import { User, Role, Guest, ConfirmNum } from '../db/index.js'; +import fs from "fs"; +import CryptoJS from "crypto-js"; +import validator from "validator"; +// 현재 유저 상태 결정 +const getUser = async (req, res) => { + try { + if (req.cookies.butterStudio) { + const token = req.cookies.butterStudio; + const decoded = jwt.verify(token, config.jwtSecret); + res.json(decoded); + } else { + res.json({ id: 0, role: "user" }); + } + } catch (error) { + console.error(error); + return res.status(500).send("유저를 가져오지 못했습니다."); + } +} +// 로그인 const login = async (req, res) => { try { const { id, password } = req.body; //사용자 존재 확인 const user = await User.scope("withPassword").findOne({ where: { userId: id } }); - console.log("user : ", user); if (!user) { return res.status(422).send(`사용자가 존재하지 않습니다`); } @@ -17,19 +34,13 @@ const login = async (req, res) => { if (passwordMatch) { // 3) 비밀번호가 맞으면 토큰 생성 const userRole = await user.getRole(); - // const userId = await user.getId(); - console.log("userRole1111 : ", userRole); - // console.log("userId : ", userId); - const signData = { id: user.id, role: userRole.name, }; - console.log("signData : ", signData); const token = jwt.sign(signData, config.jwtSecret, { expiresIn: config.jwtExpires, }); - console.log(token); // 4) 토큰을 쿠키에 저장 res.cookie(config.cookieName, token, { maxAge: config.cookieMaxAge, @@ -50,91 +61,421 @@ const login = async (req, res) => { console.error(error); return res.status(500).send("로그인 에러"); } - } - +// 로그아웃 const logout = async (req, res) => { try { - res.cookie(config.cookieName,"") + res.clearCookie(config.cookieName); + res.json({ + id: 0, + role: "user", + }) + res.send('successfully cookie cleared.') } catch (error) { console.error(error); return res.status(500).send("로그인 에러"); } } -const compareId = async (req, res) => { - const id = req.params.userId; - const userid = await User.findOne({ where: { userId: id } }); - if (userid !== null) { - return res.json(true); - } else { - return res.json(false); +// 인증번호 발송 +const confirmMbnum = async (req, res) => { + try { + + // 휴대폰 인증 + const NCP_serviceID = process.env.NCP_serviceID; + const NCP_accessKey = process.env.NCP_accessKey; + const NCP_secretKey = process.env.NCP_secretKey; + + const date = Date.now().toString(); + const uri = NCP_serviceID; + const accessKey = NCP_accessKey; + const secretKey = NCP_secretKey; + const method = 'POST'; + const space = " "; + const newLine = "\n"; + const url = `https://sens.apigw.ntruss.com/sms/v2/services/${uri}/messages`; + const url2 = `/sms/v2/services/${uri}/messages`; + + //시크릿 키를 암호화하는 작업 + const hmac = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256, secretKey); + + hmac.update(method); + hmac.update(space); + hmac.update(url2); + hmac.update(newLine); + hmac.update(date); + hmac.update(newLine); + hmac.update(accessKey); + + const hash = hmac.finalize(); + const signature = hash.toString(CryptoJS.enc.Base64); + + const phoneNumber = req.params.phone; + console.log(phoneNumber); + + //인증번호 생성 + const verifyCode = Math.floor(Math.random() * (999999 - 100000)) + 100000; + console.log("verifyCode : ", verifyCode); + let today = new Date(); + let time = String(today.getTime()); + // let result = await axios({ + // method: method, + // json: true, + // url: url, + // headers: { + // 'Content-Type': "application/json", + // 'x-ncp-apigw-timestamp': date, + // 'x-ncp-iam-access-key': accessKey, + // 'x-ncp-apigw-signature-v2': signature, + // }, + // data: { + // type: 'SMS', + // contentType: 'COMM', + // countryCode: '82', + // from: '01086074580', + // content: `[본인 확인] 인증번호 [${verifyCode}]를 입력해주세요.`, + // messages: [ + // { + // to: `${phoneNumber}`, + // }, + // ], + // }, + // }); + + // const resultMs = result.data.messages; + // console.log('resultMs', resultMs); + + // console.log('response', res.data, res['data']); + const confirm = await ConfirmNum.findOne({ where: { phone: phoneNumber } }); + if (confirm) { + await confirm.destroy(); + // 5분 유효시간 설정 + await ConfirmNum.create({ + confirmNum: String(verifyCode), + phone: phoneNumber, + startTime: time, + }); + } else { + await ConfirmNum.create({ + confirmNum: String(verifyCode), + phone: phoneNumber, + startTime: time, + } + ); + } + res.json({ startTime: time, isSuccess: true, code: 202, message: "본인인증 문자 발송 성공", result: res.data }); + } catch (error) { + console.log("error: ", error); + if (error.res == undefined) { + res.json({ isSuccess: true, code: 200, message: "본인인증 문자 발송 성공", result: res.data }); + } + else res.json({ isSuccess: true, code: 204, message: "본인인증 문자 발송에 문제가 있습니다.", result: error.res }); } -} +}; -const confirmMbnum = async (req, res) => { - const id = req.params.id; - const token = req.params.token; - - const client = Twilio(id, token); - // console.log(client); - client.messages - .create({ - to: '+8201086074580', - from: '+14159428621', - body: '[ButterStudio] 인증번호[1234]를 입력해주세요', - }) - .then(message => console.log(message.sid)) - .catch(e => console.log(error)); - // console.log("id = ", id, "token = ", token); - return res.json(true); -} +// 인증번호 확인 +const confirmNum = async (req, res) => { + try { + const { userMbnum, number, startTime } = req.body; + const confirm = await ConfirmNum.findOne({ where: { phone: userMbnum, startTime: startTime } }); + let today = new Date(); + let time = today.getTime(); + const elapsedMSec = time - confirm.startTime; + const elapsedMin = String(elapsedMSec / 1000 / 60); + if (elapsedMin <= 5) { + if (number !== confirm.confirmNum) { + res.send("실패"); + } else { + await confirm.destroy(); + res.send("성공"); + } + } else { + res.send("재전송") + } + } catch (error) { + console.error("error : ", error.message); + res.status(500).send("잘못된 접근입니다."); + } +}; + +//유효성 검사 +const validation = (errorMsg, data, minLength, maxLength, dataType) => { + if (validator.isLength(data, minLength, maxLength)) { + errorMsg[dataType] = false; + } else { + errorMsg[dataType] = true; + } + if (dataType === "userEmail") { + if (validator.isEmail(data, minLength, maxLength)) { + errorMsg[dataType] = false; + } else { + errorMsg[dataType] = true; + } + + } +}; +// 회원정보 const signup = async (req, res) => { - const { userId, userEmail, userNickName, userBirthday, userPassword } = req.body; - // 휴대폰 중복 확인 - const userMbnum = String(req.body.userMbnum); + const { userId, userName, userEmail, userNickName, userBirthday, userMbnum, userPassword } = req.body; try { + let errorMsg = { + errorId: false, + errorName: false, + errorEmail: false, + errorBirthday: false, + errorNickName: false, + errorMbnum: false, + errorPassword: false, + }; + + //유효성 검사 + validation(errorMsg, userId, 5, 10, "errorId"); + validation(errorMsg, userName, 1, 10, "errorName"); + validation(errorMsg, userEmail, 3, 20, "errorEmail"); + validation(errorMsg, userBirthday, 6, 6, "errorBirthday"); + validation(errorMsg, userNickName, 1, 10, "errorNickName"); + validation(errorMsg, userMbnum, 11, 11, "errorMbnum"); + validation(errorMsg, userPassword, 8, 11, "errorPassword"); + + let valid = !(Object.values(errorMsg).some((element) => (element))); + // db에서 데이터 중복검사 + const id = await User.findOne({ where: { userId: userId } }); const mbnum = await User.findOne({ where: { phoneNumber: userMbnum } }); - if (mbnum) { - return res.status(422).send(`이미 있는 휴대폰번호입니다.`); + const email = await User.findOne({ where: { email: userEmail } }); + if (!valid) { + res.json(errorMsg); + } else { + if (id) { + return res.status(401).send(`이미 있는 아이디입니다.`); + } else if (email) { + return res.status(401).send(`이미 있는 이메일입니다.`); + } else if (mbnum) { + return res.status(401).send(`이미 있는 휴대폰번호입니다.`); + } else{ + const role = await Role.findOne({ where: { name: "member" } }) + await User.create({ + userId: userId, + name: userName, + email: userEmail, + nickname: userNickName, + birth: userBirthday, + phoneNumber: userMbnum, + password: userPassword, + img: "", + roleId: role.id + }); + res.json("성공"); + } } - const role = await Role.findOne({ where: { name: "member" } }) - const newUser = await User.create({ - userId: userId, - email: userEmail, - nickname: userNickName, - birth: userBirthday, - phoneNumber: userMbnum, - password: userPassword, - roleId: role.id - }); - res.json(newUser); } catch (error) { console.error(error.message); res.status(500).send("회원가입 에러. 나중에 다시 시도 해주세요"); } }; -const getNickName = async (req, res) => { - console.log("여기여기"); - const id = req.params.id; - console.log("id : ", id); +const getMember = async (req, res) => { try { - const userNickName = await User.findOne({ where: { id: id }, attributes:["nickname"] }); - console.log("userNickName: ", userNickName); - return res.json(userNickName.nickname) + const token = req.cookies.butterStudio; + const decoded = jwt.verify(token, config.jwtSecret); + if (decoded.role === "member") { + const user = await User.findOne({ where: { id: decoded.id } }); + res.json({ nickname: user.nickname, img: user.img }); + } else { + res.status(500).send("잘못된 접근입니다."); + } } catch (error) { - console.error("error : ",error.message); - res.status(500).send("회원가입 에러. 나중에 다시 시도 해주세요"); + console.error("error : ", error.message); + res.status(500).send("잘못된 접근입니다."); } } +// 프로필 변경 +const uploadProfile = async (req, res) => { + try { + const image = req.file.filename; + const token = req.cookies.butterStudio; + const decoded = jwt.verify(token, config.jwtSecret); + + if (decoded) { + const img = await User.findOne({ where: { id: decoded.id }, attributes: ["img"] }); + fs.unlink("upload" + `\\${img.img}`, function (data) { console.log(data); }); + const user = await User.update({ + img: image + }, { where: { id: decoded.id } }); + if (user) { + const success = await User.findOne({ where: { id: decoded.id }, attributes: ["img"] }); + res.json(success) + } else { + throw new Error("프로필 등록 실패") + } + } + } catch (error) { + console.error(error.message); + res.status(500).send("프로필 에러"); + } +} +// 기본 비밀번호인지 확인 +const comparePw = async (req, res) => { + try { + //쿠키 안 토큰에서 id추출 + const token = req.cookies.butterStudio; + const decoded = jwt.verify(token, config.jwtSecret); + //해당 id의 행 추출 + const user = await User.scope("withPassword").findOne({ where: { id: decoded.id } }); + //입력한 비번과 해당 행 비번을 비교 + const passwordMatch = await user.comparePassword(req.params.pw); + //클라이언트로 동일여부를 전송 + if (passwordMatch) { + return res.json(true) + } else { + return res.json(false) + } + } catch (error) { + console.error("error : ", error.message); + res.status(500).send("인증 에러"); + } +} +// 회원정보 수정할 때 쓰는 함수 +const overlap = async (decoded, dataType, data) => { + try { + let overlap = await User.findOne({ where: { id: decoded.id } }); + // 변경할 데이터가 자기자신이면 true + if (overlap[dataType] === data) { + return true + } else { + // 그렇지 않으면 다른 데이터들 중에서 중복되는지 검사 + let overlap2 = await User.findOne({ attributes: [dataType] }); + if (overlap2[dataType] === data) { + return false + } else { + return true + } + } + } catch (error) { + console.error(error.message); + } +} +// 회원정보 수정 +const modifyUser = async (req, res) => { + try { + const token = req.cookies.butterStudio; + const decoded = jwt.verify(token, config.jwtSecret); + const { userName, userEmail, userNickName, userMbnum, userPassword } = req.body; + + let errorMsg = { + errorName: false, + errorEmail: false, + errorNickName: false, + errorMbnum: false, + errorPassword: false, + }; + + //유효성 검사 + validation(errorMsg, userName, 1, 10, "errorName"); + validation(errorMsg, userEmail, 3, 20, "errorEmail"); + validation(errorMsg, userNickName, 1, 10, "errorNickName"); + validation(errorMsg, userMbnum, 11, 11, "errorMbnum"); + validation(errorMsg, userPassword, 8, 11, "errorPassword"); + + let valid = !(Object.values(errorMsg).some((element) => (element))); + const overlapEmail = await overlap(decoded, "email", userEmail); + const overlapMbnum = await overlap(decoded, "phoneNumber", userMbnum); + if (!valid) { + res.json(errorMsg); + } else { + if (overlapEmail && overlapMbnum) { + await User.update({ + name: userName, + email: userEmail, + nickname: userNickName, + phoneNumber: userMbnum, + password: userPassword, + }, { where: { id: decoded.id }, individualHooks: true }); + res.json("성공"); + } else if (!overlapEmail && overlapMbnum) { + res.status(500).send("이미 있는 이메일입니다."); + } else if (overlapEmail && !overlapMbnum) { + res.status(500).send("이미 있는 핸드폰번호입니다."); + } else { + res.status(500).send("이미 있는 이메일, 핸드폰번호입니다."); + } + } + } catch (error) { + console.error(error.message); + res.status(500).send("수정 에러. 나중에 다시 시도 해주세요"); + } +}; + +const getUserInfo = async (req, res) => { + const { id } = req.body + // console.log(id) + try { + const userInfo = await User.findOne({ + where: { id: id }, + attributes: ["id", "userId", "email", "nickname", "birth", "phoneNumber"] + }) + // console.log(userInfo) + res.json(userInfo) + } catch (error) { + res.status(500).send("회원정보 불러오기 실패"); + } +} + +const saveGuestInfo = async (req, res) => { + const { name, email, birth, phoneNumber, password } = req.body + try { + const newGuest = await Guest.create({ + name: name, + email: email, + birth: birth, + phoneNumber: phoneNumber, + password: password, + }); + // console.log(newGuest) + res.clearCookie(config.cookieName); + const token = jwt.sign({id: newGuest.id, role: "user"}, config.jwtSecret, { + expiresIn: config.jwtExpires, + }); + res.cookie(config.cookieName,token , { + maxAge: config.cookieMaxAge, + path: "/", + httpOnly: config.env === "production", + secure: config.env === "production", + }) + res.json(newGuest); + } catch (error) { + res.status(500).send("비회원정보 등록 실패"); + } +} + +const getGuestInfo = async (req,res) => { + const {guestId} = req.params + // console.log(req.body) + try { + const guestInfo = await Guest.findOne({ + where: { + id:guestId + } + }) + // console.log("guestInfo====", guestInfo) + res.json(guestInfo) + } catch (error) { + res.status(500).send("비회원정보 불러오기 실패"); + } +} export default { + getUser, login, logout, - compareId, confirmMbnum, + confirmNum, signup, - getNickName + comparePw, + modifyUser, + saveGuestInfo, + getMember, + uploadProfile, + getUserInfo, + getGuestInfo } diff --git a/server/db/index.js b/server/db/index.js index 0c9b68b..4435022 100644 --- a/server/db/index.js +++ b/server/db/index.js @@ -8,6 +8,8 @@ import TheaterTypeModel from "../models/theatertype.model.js"; import TicketFeeModel from "../models/ticketfee.model.js"; import TimeTableModel from '../models/timetable.model.js'; import ReservationModel from '../models/reservation.model.js'; +import GuestModel from '../models/guest.model.js' +import ConfirmNumModel from '../models/confirmnum.model.js' import dbConfig from "../config/db.config.js"; const sequelize = new Sequelize( @@ -35,6 +37,8 @@ const TheaterType = TheaterTypeModel(sequelize) const TicketFee = TicketFeeModel(sequelize) const TimeTable = TimeTableModel(sequelize) const Reservation = ReservationModel(sequelize) +const Guest = GuestModel(sequelize) +const ConfirmNum = ConfirmNumModel(sequelize) User.belongsTo(Role); Role.hasOne(User); @@ -53,5 +57,7 @@ export { TheaterType, TicketFee, TimeTable, - Reservation + Reservation, + Guest, + ConfirmNum } \ No newline at end of file diff --git a/server/index.js b/server/index.js index d6c86f2..6880f50 100644 --- a/server/index.js +++ b/server/index.js @@ -1,7 +1,7 @@ import dotenv from "dotenv"; -import { sequelize, User, Role } from "./db/index.js"; import app from "./app.js"; import appConfig from "./config/app.config.js"; +import { sequelize, User, Role } from "./db/index.js"; import { ROLE_NAME } from './models/role.model.js'; dotenv.config({ @@ -10,7 +10,7 @@ dotenv.config({ }); sequelize - .sync({ force: true }) + .sync({ force: false }) .then(async () => { await Promise.all( Object.keys(ROLE_NAME).map((name) => { @@ -19,17 +19,19 @@ sequelize ); const adminRole = await Role.findOne({ where: { name: "admin" } }); - // if (!adminRole) { + if (!adminRole) { await User.create({ userId: "admin", + name: "관리자", email: "han35799@naver.com", nickname: "haha", birth: "990926", phoneNumber: "01086074580", password: "admin!", + img: "970aaa79673a39331d45d4b55ca05d25", roleId: adminRole?.id, }); - // } + } else { } app.listen(appConfig.port, () => { console.log(`Server is running on port ${appConfig.port}`); @@ -39,4 +41,4 @@ sequelize console.log(err); }); - export default {} \ No newline at end of file +export default {} \ No newline at end of file diff --git a/server/models/confirmnum.model.js b/server/models/confirmnum.model.js new file mode 100644 index 0000000..3f926e6 --- /dev/null +++ b/server/models/confirmnum.model.js @@ -0,0 +1,33 @@ +import Sequelize from "sequelize"; + +const { DataTypes } = Sequelize; + +const ConfirmNumModel = (sequelize) => { + const ConfirmNum = sequelize.define( + "confirmnum", + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + confirmNum: { + type: DataTypes.STRING, + }, + phone: { + type: DataTypes.STRING + }, + startTime: { + type: DataTypes.STRING + }, + }, + { + timestamps: true, + freezeTableName: true, + tableName: "confirmnum" + } + ); + return ConfirmNum; +}; + +export default ConfirmNumModel; \ No newline at end of file diff --git a/server/models/guest.model.js b/server/models/guest.model.js new file mode 100644 index 0000000..e48fb12 --- /dev/null +++ b/server/models/guest.model.js @@ -0,0 +1,40 @@ +import Sequelize from "sequelize"; + +const { DataTypes } = Sequelize; + +const GuestModel = (sequelize) => { + const Guest = sequelize.define( + "guest", + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + name: { + type: DataTypes.STRING, + }, + email: { + type: DataTypes.STRING, + }, + birth: { + type: DataTypes.STRING, + }, + phoneNumber: { + type: DataTypes.STRING + }, + password: { + type: DataTypes.STRING, + }, + }, + { + timestamps: true, + freezeTableName: true, + tableName: "guests", + } + ); + + return Guest +}; + +export default GuestModel \ No newline at end of file diff --git a/server/models/reservation.model.js b/server/models/reservation.model.js index 562ce83..5db3008 100644 --- a/server/models/reservation.model.js +++ b/server/models/reservation.model.js @@ -18,7 +18,7 @@ const ReservationModel = (sequelize) => { type: DataTypes.INTEGER, }, row: { - type: DataTypes.STRING, + type: DataTypes.INTEGER, }, col: { type: DataTypes.INTEGER, @@ -26,10 +26,16 @@ const ReservationModel = (sequelize) => { timetable:{ type: DataTypes.INTEGER, }, + userType:{ + type: DataTypes.STRING, + }, user:{ type: DataTypes.INTEGER, }, payment:{ + type: DataTypes.STRING, + }, + totalFee:{ type: DataTypes.INTEGER, } }, diff --git a/server/models/theater.model.js b/server/models/theater.model.js index bdb4908..9f77ea4 100644 --- a/server/models/theater.model.js +++ b/server/models/theater.model.js @@ -15,7 +15,7 @@ const TheaterModel = (sequelize) => { type: DataTypes.STRING }, rows: { - type: DataTypes.STRING, + type: DataTypes.INTEGER, }, columns: { type: DataTypes.INTEGER, diff --git a/server/models/user.model.js b/server/models/user.model.js index a6ef230..ab097be 100644 --- a/server/models/user.model.js +++ b/server/models/user.model.js @@ -14,23 +14,29 @@ const UserModel = (sequelize) => { autoIncrement: true, }, userId: { - type: DataTypes.STRING, + type: DataTypes.STRING + }, + name: { + type: DataTypes.STRING }, email: { - type: DataTypes.STRING, + type: DataTypes.STRING }, nickname: { - type: DataTypes.STRING, + type: DataTypes.STRING }, birth: { - type: DataTypes.STRING, + type: DataTypes.STRING }, phoneNumber: { type: DataTypes.STRING }, password: { - type: DataTypes.STRING, + type: DataTypes.STRING }, + img: { + type: DataTypes.STRING + } }, { timestamps: true, diff --git a/server/routes/index.js b/server/routes/index.js index 658d2bb..17b64bb 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -6,6 +6,7 @@ import timetableRouter from "./timetable.route.js"; import cinemaRouter from "./cinema.route.js"; import kakaopayRouter from "./kakaopay.route.js"; import emailRouter from './email.route.js' +import reservationRouter from './reservation.route.js' const router = express.Router(); @@ -15,6 +16,7 @@ router.use('/kakaopay',kakaopayRouter) router.use('/email',emailRouter) router.use('/info', cinemaRouter) router.use('/theater', theaterRouter) +router.use('/reservation', reservationRouter) router.use('/timetable', timetableRouter) export default router; \ No newline at end of file diff --git a/server/routes/movie.route.js b/server/routes/movie.route.js index 57f0da7..5746285 100644 --- a/server/routes/movie.route.js +++ b/server/routes/movie.route.js @@ -7,12 +7,9 @@ router .route("/") .get(movieCtrl.getListfromDB) -router - .route('/showmovies/:category') - .get(movieCtrl.getMovieById) router - .route('/movielist') + .route('/movielist/:category') .get(movieCtrl.getMovieList) router @@ -38,6 +35,5 @@ router .post(movieCtrl.create) .delete(movieCtrl.remove) -router.param('category', movieCtrl.getMovieByCategory) export default router; \ No newline at end of file diff --git a/server/routes/reservation.route.js b/server/routes/reservation.route.js new file mode 100644 index 0000000..1ca833b --- /dev/null +++ b/server/routes/reservation.route.js @@ -0,0 +1,17 @@ +import express from "express"; +import ReservationCtrl from "../controllers/reservation.controller.js"; + +const router = express.Router(); + + +router.route('/findreservation') + .post(ReservationCtrl.findReservedSeats) + .get(ReservationCtrl.findReservation) + +router.route('/findonereservation') + .post(ReservationCtrl.findOneReservation) + +router.route('/save') + .post(ReservationCtrl.saveReservation) + +export default router; \ No newline at end of file diff --git a/server/routes/theater.route.js b/server/routes/theater.route.js index f52282f..9f11cb1 100644 --- a/server/routes/theater.route.js +++ b/server/routes/theater.route.js @@ -3,6 +3,9 @@ import theaterCtrl from "../controllers/theater.controller.js"; const router = express.Router(); +router.route("/getInfo") + .post(theaterCtrl.getTheaterInfo) + router .route("/") .get(theaterCtrl.getAll) @@ -17,4 +20,4 @@ router .get(theaterCtrl.getOne) .delete(theaterCtrl.remove) -export default router; \ No newline at end of file +export default router; diff --git a/server/routes/user.route.js b/server/routes/user.route.js index 6dc9ef3..1ce9e8a 100644 --- a/server/routes/user.route.js +++ b/server/routes/user.route.js @@ -1,8 +1,17 @@ import express from "express"; +import multer from "multer"; import userCtrl from "../controllers/user.controller.js"; const router = express.Router(); +const upload = multer({ + dest: "upload/" +}) + +router + .route("/user") + .get(userCtrl.getUser) + router .route("/login") .post(userCtrl.login) @@ -16,15 +25,46 @@ router .post(userCtrl.signup) router - .route("/:userId") - .get(userCtrl.compareId) + .route("/profile") + .post( + upload.single("image"), + userCtrl.uploadProfile + ) + +router + .route("/modify") + .post(userCtrl.modifyUser) + +router + .route("/member") + .get(userCtrl.getMember) + +router + .route("/num") + .post(userCtrl.confirmNum) router - .route("/:id/:token") - .get(userCtrl.confirmMbnum) + .route("/pw/:pw") + .get(userCtrl.comparePw) router - .route("/:iddd") - .get(userCtrl.getNickName) + .route("/phone/:phone") + .post(userCtrl.confirmMbnum) + +router + .route('/getuserinfo') + .post(userCtrl.getUserInfo) + +router + .route('/guest/save') + .post(userCtrl.saveGuestInfo) + +router + .route('/guestinfo/:guestId') + .get(userCtrl.getGuestInfo) + +router + .route("/:userId") + .get(userCtrl.compareId) export default router; \ No newline at end of file -- GitLab
상영관 종류청소년 일반 경로
{priceToString(info.weekdays + info.morning + info.youth + info.defaultPrice)}원 {priceToString(info.weekdays + info.morning + info.adult + info.defaultPrice)}원 {priceToString(info.weekdays + info.morning + info.senior + info.defaultPrice)}원 -
- - -
-
+
+ + +
+
일반 (11:00 ~ ){priceToString(info.weekend + info.night + info.youth + info.defaultPrice)}원 {priceToString(info.weekend + info.night + info.adult + info.defaultPrice)}원 {priceToString(info.weekend + info.night + info.senior + info.defaultPrice)}원 -
- - -
-
+
+ + +
+