diff --git a/README.md b/README.md index e70ffbfe..d046b293 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@

BookUs!

travis - Version + Release License: MIT

+### [HomePage](http://www.foorg.xyz/) + [Bookus!](http://www.foorg.xyz/)는 이벤트 예약 서비스 [Festa!](https://festa.io/) 클론 프로젝트입니다. 순간적으로 많은 트래픽이 몰리더라도 중단되지 않는 **안정적인** 선착순 예약 서비스를 목표로 하고 있습니다. 따라서 다음과 같은 도전과제를 갖고 있습니다. ### 재사용성이 높고 테스트로 검증된 UI Component diff --git a/client/.env.template b/client/.env.template index dfa9ea05..1d360400 100644 --- a/client/.env.template +++ b/client/.env.template @@ -1,2 +1,3 @@ REACT_APP_SERVER_URL= +REACT_APP_SERVER_RESERVE_URL= REACT_APP_GOOGLE_MAP_API_KEY= \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx index b159fd85..f0dddaac 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,17 +1,17 @@ import React from 'react'; import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; import { ThemeProvider } from 'styled-components'; -import defaultTheme from '../src/commons/style/themes/default'; -import Normalize from '../src/commons/style/Normalize'; -import GlobalStyles from '../src/commons/style/GlobalStyle'; -import Login from './pages/Login'; -import SignUp from './pages/SignUp'; -import Main from './pages/Main'; -import EventDetail from './pages/EventDetail'; -import EventJoin from './pages/EventJoin'; - -import GlobalStoreProvider from './stores'; +import defaultTheme from 'commons/style/themes/default'; +import Normalize from 'commons/style/Normalize'; +import GlobalStyles from 'commons/style/GlobalStyle'; +import Login from 'pages/Login'; +import SignUp from 'pages/SignUp'; +import Main from 'pages/Main'; +import EventDetail from 'pages/EventDetail'; +import EventJoin from 'pages/EventJoin'; +import NotFound from 'pages/NotFound'; +import GlobalStoreProvider from 'stores'; const App: React.FC = () => ( @@ -32,9 +32,7 @@ const App: React.FC = () => ( path="/events/:eventId([0-9]+)/register/tickets" component={EventJoin} /> - -
404 page
-
+ diff --git a/client/src/commons/constants/number.ts b/client/src/commons/constants/number.ts index 92176f24..472c82c8 100644 --- a/client/src/commons/constants/number.ts +++ b/client/src/commons/constants/number.ts @@ -1 +1 @@ -export const EVENT_NAME_MAX_LENGTH = 45; +export const EVENT_NAME_MAX_LENGTH = 40; diff --git a/client/src/commons/constants/routes.ts b/client/src/commons/constants/routes.ts index 133ec5cd..d49af6f0 100644 --- a/client/src/commons/constants/routes.ts +++ b/client/src/commons/constants/routes.ts @@ -3,8 +3,8 @@ export default { SIGNUP: '/signup', LOGIN: '/login', USER: '/user', - EVENT_CREATE: 'event/create', - EVENT_DETAIL: 'event/detail', + EVENT_CREATE: '/event/create', + EVENT_DETAIL: '/events', MYPAGE: '/', // 추후에 mypage로 변경해야함. 현재 MYPAGE로 이동해야하는 Route 는 전부 이것을 사용. // TODO : Modify google api to server api using google api diff --git a/client/src/components/molecules/Card/index.tsx b/client/src/components/molecules/Card/index.tsx index cfdf642c..354dd86e 100644 --- a/client/src/components/molecules/Card/index.tsx +++ b/client/src/components/molecules/Card/index.tsx @@ -20,6 +20,11 @@ export interface Props { price: number; } +const shortenTitle = (title: string) => + title.length >= EVENT_NAME_MAX_LENGTH + ? `${title.slice(0, EVENT_NAME_MAX_LENGTH)}...` + : title; + function Card({ to, imgSrc, @@ -28,11 +33,7 @@ function Card({ host, price, }: Props): React.ReactElement { - const eventTitle = - title.length >= EVENT_NAME_MAX_LENGTH - ? `${title.slice(0, EVENT_NAME_MAX_LENGTH)}...` - : title; - + const eventTitle = shortenTitle(title); return ( diff --git a/client/src/components/organisms/CardGrid/index.tsx b/client/src/components/organisms/CardGrid/index.tsx index 60e9c145..1fe2c0b8 100644 --- a/client/src/components/organisms/CardGrid/index.tsx +++ b/client/src/components/organisms/CardGrid/index.tsx @@ -1,7 +1,9 @@ import React from 'react'; + import * as S from './style'; -import Card from '../../molecules/Card'; -import { Event } from '../../../types/Event'; +import Card from 'components/molecules/Card'; +import { Event } from 'types/Event'; +import ROUTES from 'commons/constants/routes'; interface Props { cards: Event[]; @@ -10,7 +12,7 @@ interface Props { function CardGrid({ cards, setRef }: Props): React.ReactElement { return ( - + <> {cards.map(card => ( ))}
-
+ ); } diff --git a/client/src/components/organisms/CardGrid/style.ts b/client/src/components/organisms/CardGrid/style.ts index d8465e0f..06a03b90 100644 --- a/client/src/components/organisms/CardGrid/style.ts +++ b/client/src/components/organisms/CardGrid/style.ts @@ -1,13 +1,17 @@ import styled from 'styled-components'; -export const CardGridWrapper = styled.div``; export const CardGridContainer = styled.div` display: grid; - grid-template-columns: repeat(2, 49%); justify-content: space-between; column-gap: 1%; row-gap: 2rem; + @media screen and (min-width: 32rem) { + grid-template-columns: repeat(2, 49%); + column-gap: 1%; + row-gap: 2rem; + } + @media screen and (min-width: 64rem) { grid-template-columns: repeat(4, 24%); column-gap: 1%; diff --git a/client/src/hooks/base/useIntersect/index.ts b/client/src/hooks/base/useIntersect/index.ts index ac861960..ca09d64d 100644 --- a/client/src/hooks/base/useIntersect/index.ts +++ b/client/src/hooks/base/useIntersect/index.ts @@ -1,7 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; interface OptionProps { - root?: null; + root?: HTMLElement | null; threshold?: number; rootMargin?: string; } @@ -23,7 +23,6 @@ export const useIntersect = ( React.Dispatch>, ] => { const [ref, setRef] = useState(null); - // intersecting이 있을 때 target 엔트리와 observer를 넘겨주자. const checkIntersect = useCallback( ([entry]: IntersectionObserverEntry[], observer: IntersectionObserver) => { if (entry.isIntersecting) { @@ -32,7 +31,6 @@ export const useIntersect = ( }, [onIntersect], ); - // ref나 option이 바뀔 경우 observer를 새로 등록한다. useEffect(() => { let observer: IntersectionObserver; if (ref) { @@ -40,7 +38,6 @@ export const useIntersect = ( ...baseOption, ...option, }); - // start to observe ref observer.observe(ref); } return (): void => observer && observer.disconnect(); @@ -52,6 +49,5 @@ export const useIntersect = ( checkIntersect, option, ]); - // setRef를 넘겨주어서 ref를 변경시킬 수 있도록 한다. return [ref, setRef]; }; diff --git a/client/src/pages/EventDetail/store.tsx b/client/src/pages/EventDetail/store.tsx index 6d2e35cc..59921fa6 100644 --- a/client/src/pages/EventDetail/store.tsx +++ b/client/src/pages/EventDetail/store.tsx @@ -13,10 +13,8 @@ const defaultState: EventDetailState = { place: '', address: '', placeDesc: '', - location: { - latitude: 0, - longitude: 0, - }, + latitude: 37.5662952, + longitude: 126.9779451, mainImg: '', desc: '', diff --git a/client/src/pages/EventDetail/view.tsx b/client/src/pages/EventDetail/view.tsx index d4f903b0..9a7568b3 100644 --- a/client/src/pages/EventDetail/view.tsx +++ b/client/src/pages/EventDetail/view.tsx @@ -28,7 +28,8 @@ function EventDetailView({ eventId }: Props): React.ReactElement { place, address, placeDesc, - location, + latitude, + longitude, } = eventData; const requestFetch = useFetch({ @@ -84,7 +85,11 @@ function EventDetailView({ eventId }: Props): React.ReactElement { // TODO: eventContent will change to contentViewer component eventContent={
} ticket={} - place={} + place={ + + } /> ); } diff --git a/client/src/pages/EventJoin/view.tsx b/client/src/pages/EventJoin/view.tsx index 0afa3ab3..06c1553e 100644 --- a/client/src/pages/EventJoin/view.tsx +++ b/client/src/pages/EventJoin/view.tsx @@ -1,5 +1,4 @@ -import React from 'react'; - +import React, { useState } from 'react'; import axios from 'axios'; import EventJoinTemplate from './template'; @@ -7,6 +6,8 @@ import TicketBox from 'components/organisms/TicketBox'; import Counter from 'components/molecules/Counter'; import Btn from 'components/atoms/Btn'; import * as S from './style'; +import { useHistory } from 'react-router-dom'; +import httpStatus from 'http-status'; const { REACT_APP_SERVER_RESERVE_URL } = process.env; @@ -28,24 +29,26 @@ interface Props { } function EventJoin({ eventId }: Props): React.ReactElement { + const [isReserved, setisReserved] = useState(false); + + const history = useHistory(); let ticketCount = 0; - /** - * .post('/api/users/ticket') - .set({ - Cookie: `UID=${token}`, - Accept: 'application/json', - }) - .send({ - ticketId: 2, - orderTicketNum: 1, - }) - */ + const counterHandler = (count: number) => { ticketCount = count; }; const requestOrder = async () => { - console.log(REACT_APP_SERVER_RESERVE_URL); + if (isReserved) { + return; + } + if (ticketCount <= 0) { + alert('티켓 개수는 1개 이상이어야 합니다.'); + return; + } + // 401 : 로그인 + // 403, 404 : ban + await axios({ url: `${REACT_APP_SERVER_RESERVE_URL}/api/users/ticket`, method: 'POST', @@ -57,7 +60,28 @@ function EventJoin({ eventId }: Props): React.ReactElement { orderTicketNum: ticketCount, }, withCredentials: true, - }); + }) + .then(res => { + const { status } = res; + if (status === httpStatus.OK) { + setisReserved(true); + + alert('예약이 완료되었습니다.'); + history.push('/'); + } + }) + .catch(err => { + const { response } = err; + const { status } = response; + if (status === httpStatus.UNAUTHORIZED) { + history.push('/login'); + } else if ( + status === httpStatus.FORBIDDEN || + status === httpStatus.NOT_FOUND + ) { + alert('티켓 구매에 실패했습니다. 😔'); + } + }); }; return ( diff --git a/client/src/pages/Main/index.tsx b/client/src/pages/Main/index.tsx index 84bb6ba2..924c4d09 100644 --- a/client/src/pages/Main/index.tsx +++ b/client/src/pages/Main/index.tsx @@ -6,18 +6,12 @@ import MainBanner from 'components/organisms/MainBanner'; import CardGrid from 'components/organisms/CardGrid'; import { Event } from '../../types/Event'; import { useIntersect } from '../../hooks'; +import delay from '../../utils/delay'; function Main(): React.ReactElement { const [events, setEvents] = useState([]); const { REACT_APP_SERVER_URL: SERVER_URL } = process.env; - const delay = (seconds: number): Promise => - new Promise(resolve => - setTimeout(() => { - resolve(); - }, seconds * 1000), - ); - const fetchItems = useCallback(async () => { const startAt = events.length === 0 @@ -28,7 +22,7 @@ function Main(): React.ReactElement { url: `${SERVER_URL}/api/events?cnt=12${startAt}`, withCredentials: true, }); - await delay(0.5); + await delay(100); setEvents([...events, ...data]); }, [SERVER_URL, events]); @@ -40,7 +34,9 @@ function Main(): React.ReactElement { await fetchItems(); }, { + root: null, threshold: 1.0, + rootMargin: '0% 0% 25% 0%', }, ); diff --git a/client/src/pages/NotFound/index.stories.tsx b/client/src/pages/NotFound/index.stories.tsx new file mode 100644 index 00000000..91469b89 --- /dev/null +++ b/client/src/pages/NotFound/index.stories.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import NotFound from '.'; + +export default { + title: 'Pages / NotFound', +}; + +export const notFound: React.FC = () => { + return ; +}; diff --git a/client/src/pages/NotFound/index.tsx b/client/src/pages/NotFound/index.tsx new file mode 100644 index 00000000..d2796f47 --- /dev/null +++ b/client/src/pages/NotFound/index.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +import NotFoundTemplate from './template'; +import ImgBtn from 'components/molecules/ImgBtn'; +import ROUTES from 'commons/constants/routes'; + +function NotFound(): React.ReactElement { + return ( + + } + /> + ); +} + +export default NotFound; diff --git a/client/src/pages/NotFound/template/index.tsx b/client/src/pages/NotFound/template/index.tsx new file mode 100644 index 00000000..daaa4181 --- /dev/null +++ b/client/src/pages/NotFound/template/index.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import * as S from './style'; +import BasedTemplate from 'pages/BasedTemplate/templates'; + +interface Props { + content: React.ReactNode; +} + +function NotFoundTemplate({ content }: Props): React.ReactElement { + return ( + + {content} + + ); +} + +export default NotFoundTemplate; diff --git a/client/src/pages/NotFound/template/style.ts b/client/src/pages/NotFound/template/style.ts new file mode 100644 index 00000000..0bd439fb --- /dev/null +++ b/client/src/pages/NotFound/template/style.ts @@ -0,0 +1,10 @@ +import styled from 'styled-components'; + +export const ContentContainer = styled.div` + height: 55rem; + a { + width: 100%; + height: 100%; + object-fit: contain; + } +`; diff --git a/client/src/types/Data.ts b/client/src/types/Data.ts index e86a4ec4..e3ac7ae0 100644 --- a/client/src/types/Data.ts +++ b/client/src/types/Data.ts @@ -30,7 +30,8 @@ export interface EventDetail { place: string; address: string; placeDesc: string; - location: Location; + latitude: number; + longitude: number; mainImg: string; desc: string; ticketType: TicketType; diff --git a/client/src/utils/delay.ts b/client/src/utils/delay.ts new file mode 100644 index 00000000..7a944ba8 --- /dev/null +++ b/client/src/utils/delay.ts @@ -0,0 +1,6 @@ +export default (miliSeconds: number): Promise => + new Promise(resolve => + setTimeout(() => { + resolve(); + }, miliSeconds), + ); diff --git a/reserve-server/src/services/ticketType.ts b/reserve-server/src/services/ticketType.ts index 2ae2716d..e01c3d21 100644 --- a/reserve-server/src/services/ticketType.ts +++ b/reserve-server/src/services/ticketType.ts @@ -1,18 +1,19 @@ -import { Transaction, literal } from 'sequelize'; +import { Transaction, literal, WhereOptions } from 'sequelize'; import { TicketType } from '../models'; -export const getTicketType = (transaction: Transaction, ticketId: number) => - TicketType.findOne({ - where: { id: ticketId }, - transaction, - }); +export const getTicketType = (transaction: Transaction, ticketId: number) => { + const where: WhereOptions = { id: ticketId }; + return TicketType.findOne({ where, transaction }); +}; export const updateTicketType = ( transaction: Transaction, ticketId: number, orderTicketNum: number, -) => - TicketType.update( +) => { + const where: WhereOptions = { id: ticketId }; + return TicketType.update( { leftCnt: literal(`left_cnt - ${orderTicketNum}`) }, - { where: { id: ticketId }, transaction }, + { where, transaction }, ); +}; diff --git a/reserve-server/src/services/userTicket.ts b/reserve-server/src/services/userTicket.ts index ec9b4f5e..8610f28c 100644 --- a/reserve-server/src/services/userTicket.ts +++ b/reserve-server/src/services/userTicket.ts @@ -1,4 +1,4 @@ -import { Transaction } from 'sequelize'; +import { Transaction, WhereOptions } from 'sequelize'; import { UserTicket } from '../models'; export const updateUserTicket = ( @@ -22,8 +22,7 @@ export const countUserTicket = ( transaction: Transaction, userId: number, ticketId: number, -) => - UserTicket.count({ - where: { userId, ticketTypeId: ticketId }, - transaction, - }); +) => { + const where: WhereOptions = { userId, ticketTypeId: ticketId }; + return UserTicket.count({ where, transaction }); +}; diff --git a/server/src/services/users.ts b/server/src/services/users.ts index a4ee7a3d..840b9230 100644 --- a/server/src/services/users.ts +++ b/server/src/services/users.ts @@ -1,13 +1,14 @@ import { User } from '../models'; +import { WhereOptions } from 'sequelize'; export async function getUserById(id: number): Promise { - const where = { id }; + const where: WhereOptions = { id }; return await User.findOne({ where }); } export async function getUserByGoogleId( googleId: number, ): Promise { - const where = { googleId: +googleId }; + const where: WhereOptions = { googleId: +googleId }; return await User.findOne({ where }); } @@ -16,7 +17,8 @@ export async function setUser( googleId: number, email: string, ): Promise<[User, boolean]> { - return await User.findOrCreate({ where: { googleId, email } }); + const where: WhereOptions = { googleId, email }; + return await User.findOrCreate({ where }); } // User가 회원가입을 할 때 사용되는 Service export async function setUserInfo( @@ -31,6 +33,6 @@ export async function setUserInfo( lastName, phoneNumber, }; - const where = { id, googleId }; + const where: WhereOptions = { id, googleId }; return await User.update(values, { where }); }