diff --git a/client/.eslintrc.json b/client/.eslintrc.json
index b43c292b..b6ce022b 100644
--- a/client/.eslintrc.json
+++ b/client/.eslintrc.json
@@ -26,6 +26,7 @@
"SharedArrayBuffer": "readonly"
},
"rules": {
+ "@typescript-eslint/no-unused-vars": "error",
"jest/no-disabled-tests": "warn",
"jest/no-focused-tests": "error",
"jest/no-identical-title": "error",
diff --git a/client/cypress/integration/eventjoin.spec.ts b/client/cypress/integration/eventjoin.spec.ts
index d3aa9232..7ef391da 100644
--- a/client/cypress/integration/eventjoin.spec.ts
+++ b/client/cypress/integration/eventjoin.spec.ts
@@ -186,6 +186,6 @@ context('이벤트 예약 페이지', () => {
expect(alertStub.getCall(0)).to.be.calledWith(RESERVE_COMPLETE);
});
- cy.location('pathname').should('eq', '/');
+ cy.location('pathname').should('eq', '/my/tickets');
});
});
diff --git a/client/src/App.tsx b/client/src/App.tsx
index d3d8d62a..68b5f1b1 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -25,6 +25,7 @@ import {
import GlobalStoreProvider from 'stores';
import { UserAccountState, UserAccountAction } from 'stores/accountStore';
import { defaultAccountState } from 'stores/accountStore/reducer';
+import { AfterLoginAction } from 'stores/afterLoginStore';
import { useIsMount } from 'hooks';
const { REACT_APP_TEST_UID_TOKEN } = process.env;
@@ -36,11 +37,11 @@ const App: React.FC = () => {
-
-
-
+
+
+
- {
component={MyPage}
/>
-
+
@@ -64,6 +65,16 @@ const App: React.FC = () => {
export default App;
+function PublicRoute({ ...rest }: any): React.ReactElement {
+ const { setLoginCallback } = useContext(AfterLoginAction);
+
+ useEffect(() => {
+ setLoginCallback('/');
+ }, [setLoginCallback]);
+
+ return ;
+}
+
function PrivateRoute({
component: TargetPage,
...rest
@@ -72,11 +83,17 @@ function PrivateRoute({
const accountState = useContext(UserAccountState);
const { setLoginState } = useContext(UserAccountAction);
const [isLoginCheck, setIsLoginCheck] = useState(false);
+ const { setLoginCallback } = useContext(AfterLoginAction);
+ const path = window.location.pathname;
useEffect(() => {
setLoginState(true);
}, [setLoginState]);
+ useEffect(() => {
+ if (isLoginCheck && !accountState.isLogin) setLoginCallback(path);
+ }, [rest, accountState.isLogin, setLoginCallback, isLoginCheck, path]);
+
useIsMount(() => {
if (defaultAccountState !== accountState) setIsLoginCheck(true);
}, accountState);
diff --git a/client/src/commons/constants/string.ts b/client/src/commons/constants/string.ts
index 9c05625a..60cbf125 100644
--- a/client/src/commons/constants/string.ts
+++ b/client/src/commons/constants/string.ts
@@ -5,6 +5,8 @@ export const LOGIN_SOCIAL = '소셜 계정을 사용해서 로그인';
export const BEFORE_LOGIN = '가입 혹은 로그인';
export const FOOTER_INFO =
'대표 이메일 boostcamp@festa.io\n북어스(BookUs) | 대표 부캠이 | 서울특별시 서울구 서울동 서울로 123-1234 | 통신판매업 12345678 | 대표전화 123-1234-1234 (문의는 이메일 바랍니다)';
+export const INTERNAL_SERVER_ERROR =
+ '서버 요청에 실패했습니다. 잠시 후에 다시 시도해주세요';
export const SIGNUP_EMAIL = '이메일';
export const SIGNUP_LAST_NAME = '성';
@@ -131,12 +133,15 @@ export const BOUGHT_TICKET_EVENT_TITLE_CAPTION = '현재 구매한 티켓 목록
export const HISTORY_METHOD_PUSH = 'PUSH';
export const HISTORY_METHOD_REPLACE = 'REPLACE';
-
export const REFUND_TICKET_SUCCESS = '환불이 완료되었습니다.';
export const REFUND_TICKET_FAILURE = '환불이 실패했습니다.';
export const NOT_FOUND_BOUGHT_TICKET = '아직 구매한 티켓이 없네요..😅';
export const NOT_FOUND_CREATED_EVENT = '주최한 이벤트가 없네요..🤣';
+export const TICKET_REMAIN_DAYS = '일 후에 판매마감';
+export const TICKET_INVALID_DATE = '판매기간이 지났습니다';
+export const TICKET_COMMING_SOON = '일 후 판매시작';
+
export const FORM_NAME: any = {
event: {
isPublic: '공개 여부',
@@ -164,4 +169,3 @@ export const FORM_NAME: any = {
refundEndAt: '티켓 환불 마감 날짜',
},
};
-
diff --git a/client/src/components/molecules/IconBtn/index.tsx b/client/src/components/molecules/IconBtn/index.tsx
index b877a066..9ee076d6 100644
--- a/client/src/components/molecules/IconBtn/index.tsx
+++ b/client/src/components/molecules/IconBtn/index.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React from 'react';
import * as S from './style';
import { Props as BtnProps } from 'components/atoms/Btn';
diff --git a/client/src/components/organisms/EventHeader/index.tsx b/client/src/components/organisms/EventHeader/index.tsx
index 0b924c0d..6ac956d9 100644
--- a/client/src/components/organisms/EventHeader/index.tsx
+++ b/client/src/components/organisms/EventHeader/index.tsx
@@ -14,7 +14,7 @@ interface Props {
endAt: string;
user: User;
ticketType: TicketType;
- doneEvent?: boolean;
+ doneEventType?: number;
}
function EventHeader({
@@ -26,12 +26,13 @@ function EventHeader({
endAt,
user,
ticketType,
- doneEvent,
+ doneEventType,
}: Props): React.ReactElement {
const ticketInfo = ticketType;
const { firstName, lastName } = user;
const profileImgUrl =
'https://kr.object.ncloudstorage.com/bookus/defaultProfileImg.png';
+ const doneTypes = ['등록', '종료되었습니다.', '매진되었습니다.'];
return (
@@ -71,9 +72,9 @@ function EventHeader({
{ticketInfo.leftCnt}명
diff --git a/client/src/components/organisms/Ticket/index.tsx b/client/src/components/organisms/Ticket/index.tsx
index c55597d8..b3a838f5 100644
--- a/client/src/components/organisms/Ticket/index.tsx
+++ b/client/src/components/organisms/Ticket/index.tsx
@@ -6,6 +6,11 @@ import { IconLabel, Price } from 'components';
import { calculateDiffDaysOfDateRange } from 'utils/dateCalculator';
import { FaTicketAlt, FaCheck, FaRegCalendarAlt } from 'react-icons/fa';
+import {
+ TICKET_INVALID_DATE,
+ TICKET_REMAIN_DAYS,
+ TICKET_COMMING_SOON,
+} from 'commons/constants/string';
interface Prop extends TicketType {
count?: number;
@@ -29,6 +34,26 @@ function Ticket({
Date().toString(),
salesEndAt,
);
+
+ function makeLabelContent(remainDays: number, salesStartAt: string) {
+ if (!doneEvent) {
+ return `${remainDays}${TICKET_REMAIN_DAYS}`;
+ }
+
+ if (remainDays <= 0) {
+ return TICKET_INVALID_DATE;
+ }
+
+ const convertToUTCDate = new Date();
+ convertToUTCDate.setHours(-9);
+
+ const commingDays = calculateDiffDaysOfDateRange(
+ convertToUTCDate.toString(),
+ salesStartAt,
+ );
+ return `${commingDays}${TICKET_COMMING_SOON}`;
+ }
+
return (
<>
티켓
@@ -39,21 +64,20 @@ function Ticket({
{`${name} ${count ? `* ${count}` : ''}`}
{desc}
- }
- labelContent={`${remainCnt}개 남음`}
- />
+ {leftCnt !== -1 && (
+ }
+ labelContent={`${remainCnt}개 남음`}
+ />
+ )}
+
}
labelContent={`1인당 ${maxCntPerPerson}개 구입 가능`}
/>
}
- labelContent={
- remainDays <= 0
- ? '판매기간이 종료되었습니다.'
- : `${remainDays}일 후에 판매마감`
- }
+ labelContent={makeLabelContent(remainDays, salesStartAt)}
/>
diff --git a/client/src/components/organisms/Ticket/style.ts b/client/src/components/organisms/Ticket/style.ts
index be9a0a76..7e84318e 100644
--- a/client/src/components/organisms/Ticket/style.ts
+++ b/client/src/components/organisms/Ticket/style.ts
@@ -1,5 +1,5 @@
import styled from 'styled-components';
-import { theme, palette } from 'styled-tools';
+import { theme, palette, ifProp } from 'styled-tools';
export const TicketLabel = styled.div`
${theme('fontStyle.h6')};
@@ -22,7 +22,11 @@ interface TicketContentWrapContainerProps {
export const TicketContentWrapContainer = styled.div<
TicketContentWrapContainerProps
>`
- color: ${palette('grayscale', 4)}
+ color: ${ifProp(
+ 'disabled',
+ palette('grayscale', 4),
+ palette('grayscale', 1),
+ )};
padding-left: 2rem;
padding-top: 1rem;
padding-bottom: 1rem;
diff --git a/client/src/hooks/useFetch.spec.tsx b/client/src/hooks/useFetch.spec.tsx
index 033be13f..f2c063e3 100644
--- a/client/src/hooks/useFetch.spec.tsx
+++ b/client/src/hooks/useFetch.spec.tsx
@@ -40,7 +40,7 @@ describe('Hooks', () => {
return <>>;
}
// when
- const wrapper = mount();
+ mount();
// then
await new Promise(resolve => {
@@ -80,7 +80,7 @@ describe('Hooks', () => {
return <>>;
}
// when
- const wrapper = mount();
+ mount();
// then
await new Promise(resolve => {
diff --git a/client/src/pages/EventCreate/store.tsx b/client/src/pages/EventCreate/store.tsx
index 483852c3..30098b5f 100644
--- a/client/src/pages/EventCreate/store.tsx
+++ b/client/src/pages/EventCreate/store.tsx
@@ -106,7 +106,7 @@ const TicketFormDefaultState: TicketFormState = {
value: '',
},
isPublicLeftCnt: {
- valid: false,
+ valid: true,
value: false,
},
maxCntPerPerson: {
diff --git a/client/src/pages/EventDetail/index.tsx b/client/src/pages/EventDetail/index.tsx
index da859ecc..c834ae96 100644
--- a/client/src/pages/EventDetail/index.tsx
+++ b/client/src/pages/EventDetail/index.tsx
@@ -1,4 +1,4 @@
-import React, { useContext, useEffect, useState } from 'react';
+import React, { useContext, useEffect, useState, useRef } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { NOT_FOUND, INTERNAL_SERVER_ERROR } from 'http-status';
@@ -52,6 +52,7 @@ function EventDetailView(): React.ReactElement {
const [internalServerError, setInternalError] = useState(false);
const history = useHistory();
const isEventInState = checkIfEventIsInState(eventsState.events!, +eventId!);
+ const remainDays = useRef(0);
const events = isEventInState
? eventsState.events!.get(+eventId!)!
@@ -73,19 +74,33 @@ function EventDetailView(): React.ReactElement {
longitude,
} = events;
- const remainDays = calculateDiffDaysOfDateRange(
- Date().toString(),
- ticketType.salesEndAt,
- );
+ function doneEventType() {
+ remainDays.current = calculateDiffDaysOfDateRange(
+ Date().toString(),
+ ticketType.salesEndAt,
+ );
+
+ if (remainDays.current <= 0) return 1;
+
+ const remainTickets = ticketType.quantity - ticketType.leftCnt;
+ if (remainTickets <= 0) return 2;
+
+ return 0;
+ }
useEffect(() => {
- if (!isEventInState)
+ if (!isEventInState) {
eventFetchDispatcher({
type: 'EVENT',
params: {
eventId: +eventId!,
},
});
+ remainDays.current = calculateDiffDaysOfDateRange(
+ Date().toString(),
+ ticketType.salesEndAt,
+ );
+ }
if (eventsState.status === NOT_FOUND) {
history.replace('/NOT_FOUND');
@@ -101,6 +116,7 @@ function EventDetailView(): React.ReactElement {
eventsState.status,
history,
isEventInState,
+ ticketType.salesEndAt,
]);
return (
@@ -117,11 +133,11 @@ function EventDetailView(): React.ReactElement {
place,
ticketType,
}}
- doneEvent={remainDays <= 0}
+ doneEventType={doneEventType()}
/>
}
eventContent={}
- ticket={}
+ ticket={}
place={
,
+ eventId: number,
+ ) {
+ return events.get(eventId);
+ }
+
const [isReserved, setisReserved] = useState(false);
const [isTicketChecked, setIsTicketChecked] = useState(false);
const [ticketCount, setTicketCount] = useState(1);
@@ -90,8 +97,6 @@ function EventJoin(): React.ReactElement {
if (typeof originEventId === 'undefined') {
history.push('/404');
}
- const eventId = +originEventId!;
-
const getInEventState = useCallback(
(eventData: EventDetail) => {
setEventState(eventData);
@@ -99,22 +104,41 @@ function EventJoin(): React.ReactElement {
[setEventState],
);
+ const eventId = +originEventId!;
+ const { title, mainImg, startAt, endAt, user, ticketType } = eventState;
+ const { maxCntPerPerson } = ticketType;
+ const eventDataFromStore = getEventIfEventIsInState(
+ eventsState.events!,
+ eventId,
+ );
+ const isEventInState = !!eventDataFromStore;
+ const [isLoading, setLoading] = useState(true);
+
useEffect(() => {
- if (eventsState && eventsState.events) {
- const gettedEventData = eventsState.events.get(eventId);
- if (gettedEventData) {
- getInEventState(gettedEventData);
- } else {
- eventFetchDispatcher({
- type: 'EVENT',
- params: { eventId },
- });
- }
+ if (isEventInState) {
+ getInEventState(eventDataFromStore!);
+ setLoading(false);
+ return;
}
- }, [eventFetchDispatcher, eventId, eventsState, getInEventState]);
- const { title, mainImg, startAt, endAt, user, ticketType } = eventState;
- const { maxCntPerPerson } = ticketType;
+ eventFetchDispatcher({
+ type: 'EVENT',
+ params: { eventId },
+ });
+ setLoading(false);
+ }, [
+ eventDataFromStore,
+ eventFetchDispatcher,
+ eventId,
+ eventsState,
+ getInEventState,
+ isEventInState,
+ ]);
+
+ useEffect(() => {
+ window.scrollTo(0, 0);
+ }, []);
+
const isVisibleCounter = () => {
return maxCntPerPerson > minTicketCount && isTicketChecked;
};
@@ -137,9 +161,14 @@ function EventJoin(): React.ReactElement {
await joinEvent(eventId, ticketCount);
setisReserved(true);
alert(RESERVE_COMPLETE);
- history.push(ROUTES.HOME);
+ history.replace(ROUTES.MYPAGE_TICKETS);
} catch (err) {
const { response } = err;
+ if (!response) {
+ alert(INTERNAL_SERVER_ERROR);
+ history.push(ROUTES.HOME);
+ return;
+ }
const { status: statusCode, data } = response;
if (statusCode === UNAUTHORIZED) {
@@ -171,6 +200,7 @@ function EventJoin(): React.ReactElement {
return (
}
eventSection={
+
{stepList}
@@ -37,7 +39,7 @@ function EventJoinTemplate({
)}
- {eventSection}
+ {eventSection}
{place}
diff --git a/client/src/pages/EventJoin/template/style.ts b/client/src/pages/EventJoin/template/style.ts
index b4d1e4b6..d46dbc03 100644
--- a/client/src/pages/EventJoin/template/style.ts
+++ b/client/src/pages/EventJoin/template/style.ts
@@ -25,6 +25,12 @@ export const EventContainer = styled.div`
margin-left: 10rem;
`;
+export const EventSectionWrapper = styled.div`
+ & > div {
+ padding: 0rem;
+ }
+`;
+
export const PlaceWrapper = styled.div`
margin-top: 3rem;
`;
diff --git a/client/src/pages/Login/index.tsx b/client/src/pages/Login/index.tsx
index 3f53ff97..84b1b7a9 100644
--- a/client/src/pages/Login/index.tsx
+++ b/client/src/pages/Login/index.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useContext, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { IconBtn } from 'components';
@@ -6,13 +6,20 @@ import { OAUTH_GOOGLE, LOGIN_SOCIAL } from 'commons/constants/string';
import googleSvg from 'assets/img/google.svg';
import LogoSvg from 'assets/img/logo.svg';
import LoginTemplate from './templates';
+import { AfterLoginState } from 'stores/afterLoginStore';
const { REACT_APP_SERVER_URL } = process.env;
-const AuthURL = `${REACT_APP_SERVER_URL}/api/auth?returnTo=/`;
+const AuthURL = (returnTo = '/'): string =>
+ `${REACT_APP_SERVER_URL}/api/auth?returnTo=${returnTo}`;
function Login(): React.ReactElement {
const history = useHistory();
+ const loginCallbackState = useContext(AfterLoginState);
+
+ useEffect(() => {
+ console.log(loginCallbackState);
+ }, [loginCallbackState]);
return (
{
- window.location.href = AuthURL;
+ window.location.href = AuthURL(loginCallbackState);
},
}}
fullid
diff --git a/client/src/stores/afterLoginStore/index.tsx b/client/src/stores/afterLoginStore/index.tsx
index 4a1321c0..de1a3644 100644
--- a/client/src/stores/afterLoginStore/index.tsx
+++ b/client/src/stores/afterLoginStore/index.tsx
@@ -1,14 +1,16 @@
import React, { createContext, useState, Dispatch } from 'react';
export const AfterLoginState = createContext('');
-export const AfterLoginAction = createContext>(() => {});
+export const AfterLoginAction = createContext<{
+ setLoginCallback: Dispatch;
+}>({ setLoginCallback: () => {} });
function AccountStoreProvider({ children }: { children: React.ReactElement }) {
const [loginCallbackState, setLoginCallback] = useState('');
return (
-
+
{children}
diff --git a/reserve-server/src/common/constants/index.ts b/reserve-server/src/common/constants/index.ts
index e563b203..52a29bbc 100644
--- a/reserve-server/src/common/constants/index.ts
+++ b/reserve-server/src/common/constants/index.ts
@@ -1,21 +1,27 @@
export const SOLD_OUT = {
+ status: 403,
state: 1,
message: 'ticket sold out',
};
export const NOT_OPEN = {
+ status: 403,
state: 0,
message: 'wrong date',
};
export const EXCEED_LIMIT = {
+ status: 403,
state: 2,
message: 'limit exceed ticket per person',
};
export const SUCCESS = {
+ status: 200,
message: 'success',
};
export const UNAUTH = {
+ status: 401,
message: 'unauthorized',
};
export const NOT_EXIST = {
+ status: 404,
message: 'wrong number of ticket',
};
diff --git a/reserve-server/src/routes/api/controllers/orderTicket.ts b/reserve-server/src/routes/api/controllers/orderTicket.ts
index 928acb7b..6d07d116 100644
--- a/reserve-server/src/routes/api/controllers/orderTicket.ts
+++ b/reserve-server/src/routes/api/controllers/orderTicket.ts
@@ -2,7 +2,7 @@ import { Response } from 'express';
import { sequelize } from 'utils/sequelize';
import { Transaction } from 'sequelize/types';
import { orderTransaction } from 'services';
-import { FORBIDDEN, NOT_FOUND } from 'http-status';
+import { FORBIDDEN, NOT_FOUND, INTERNAL_SERVER_ERROR } from 'http-status';
import { SUCCESS } from 'common/constants';
export default async (req: any, res: Response) => {
@@ -15,7 +15,8 @@ export default async (req: any, res: Response) => {
);
res.send(SUCCESS);
} catch (err) {
- if (err.state === 404) return res.status(NOT_FOUND).send(err);
- return res.status(FORBIDDEN).send(err);
+ if (err.status === 404) return res.status(NOT_FOUND).send(err);
+ if (err.status === 403) return res.status(FORBIDDEN).send(err);
+ return res.sendStatus(INTERNAL_SERVER_ERROR);
}
};
diff --git a/server/src/routes/api/events/validators/createEvent.ts b/server/src/routes/api/events/validators/createEvent.ts
index 9d92b71a..ee499b94 100644
--- a/server/src/routes/api/events/validators/createEvent.ts
+++ b/server/src/routes/api/events/validators/createEvent.ts
@@ -1,12 +1,12 @@
import { checkSchema, CustomValidator } from 'express-validator';
import { resolveObject } from 'utils/objectResolver';
-const isLessThan = (key: string): { options: CustomValidator } => ({
- options: (value, { req }) => resolveObject(req.body, key) >= value,
+const isLessThanOrEqualTo = (key: string): { options: CustomValidator } => ({
+ options: (value, { req }): boolean => resolveObject(req.body, key) >= value,
});
-const isGreaterThan = (key: string): { options: CustomValidator } => ({
- options: (value, { req }) => resolveObject(req.body, key) <= value,
+const isGreaterThanOrEqualTo = (key: string): { options: CustomValidator } => ({
+ options: (value, { req }): boolean => resolveObject(req.body, key) <= value,
});
const isBetweenTodayAnd = (endKey: string): { options: CustomValidator } => ({
@@ -55,7 +55,7 @@ export default checkSchema({
isISO8601: true,
exists: true,
toDate: true,
- custom: isGreaterThan('startAt'),
+ custom: isGreaterThanOrEqualTo('startAt'),
},
place: {
in: 'body',
@@ -103,7 +103,7 @@ export default checkSchema({
},
'ticket.price': {
in: 'body',
- isInt: { options: { gt: 0 } },
+ isInt: { options: { min: 0 } },
toInt: true,
exists: true,
},
@@ -124,7 +124,7 @@ export default checkSchema({
isInt: true,
toInt: true,
exists: true,
- custom: isLessThan('ticket.quantity'),
+ custom: isLessThanOrEqualTo('ticket.quantity'),
},
'ticket.salesStartAt': {
in: 'body',
diff --git a/server/src/services/events/getEventById.ts b/server/src/services/events/getEventById.ts
index f8b55daf..20fe79c2 100644
--- a/server/src/services/events/getEventById.ts
+++ b/server/src/services/events/getEventById.ts
@@ -18,6 +18,6 @@ export default async (id: number): Promise => {
const event = await Event.findOne({ where, attributes, include });
if (!event) throw Error('Not Found');
-
+ if (!event.ticketType.isPublicLeftCnt) event.ticketType.leftCnt = -1;
return event;
};
diff --git a/server/src/services/events/getEvents.ts b/server/src/services/events/getEvents.ts
index 11eb0a65..cc100014 100644
--- a/server/src/services/events/getEvents.ts
+++ b/server/src/services/events/getEvents.ts
@@ -25,5 +25,15 @@ export default async (limit = 20, startAt: Date): Promise => {
{ model: User, attributes: ['id', 'lastName', 'firstName'] },
];
- return await Event.findAll({ where, attributes, limit, order, include });
+ const events = await Event.findAll({
+ where,
+ attributes,
+ limit,
+ order,
+ include,
+ });
+ return events.map(event => {
+ if (!event.ticketType.isPublicLeftCnt) event.ticketType.leftCnt = -1;
+ return event;
+ });
};