diff --git a/README.md b/README.md
index e70ffbfe..d046b293 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,12 @@
BookUs!
-
+
+### [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 });
}