Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FE] 행사 생성 페이지에 퍼널 방식 적용 #591

Merged
merged 23 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
34ba35e
feat: 행사 생성 페이지의 path 수정
pakxe Sep 23, 2024
703714d
fix: 컴포넌트 구현이 바뀜에 따라 다른 컴포넌트를 사용해 제목을 그리도록 수정
pakxe Sep 23, 2024
e36c678
feat: 행사 생성 페이지를 퍼널 방식을 이용해 렌더링 하도록 수정
pakxe Sep 23, 2024
9f3671b
feat: 퍼널 구조에서 사용되는 공통적인 로직과 컴포넌트를 위한 useFunnel 훅 구현
pakxe Sep 23, 2024
072b2d1
rename: 퍼널 방식으로 변경됨에 따른 Page -> Step 으로의 이름 변경
pakxe Sep 23, 2024
ec8beb5
feat: 행사 생성 퍼널에서 여러 step에 걸쳐 사용되는 상태를 선언해 내려주는 useCreateEventData 훅 구현
pakxe Sep 23, 2024
25f93ad
feat: 행사 생성 퍼널을 router에서 호출
pakxe Sep 23, 2024
4b0e1bc
feat: 뒤로가기 버튼인 Back에 onClick인자가 주어지면 그 함수를 사용하고 주어지지 않으면 -1로 뒤로가도록 함
pakxe Sep 23, 2024
888d32e
feat: Funnel 컴포넌트에서 현재의 step을 위한 Step 컴포넌트를 찾지 못했다면 에러를 던지도록 함
pakxe Sep 23, 2024
3e7e507
feat: 응답이 성공해야만 다음 로직을 실행할 수 있도록 mutateAsync 사용해 순서 보장
pakxe Sep 23, 2024
fe3f795
feat: 퍼널 구조에서 사용되는 공통적인 로직과 컴포넌트를 위한 useFunnel 훅 구현
pakxe Sep 23, 2024
8e0a7c5
style: return 위 개행 추가
pakxe Sep 23, 2024
979dd73
style: 가독성을 위해 Step간 개행을 넣음
pakxe Sep 23, 2024
ea487e5
test: 퍼널이 도입되어 행사 생성 과정에서 url이 변경되지 않기 때문에 url이 올바르게 변경되었는지 판단하는 라인을 제거
pakxe Sep 23, 2024
8f53bdf
test: 행사 생성 url이 바뀐 것을 cypress에도 적용
pakxe Sep 23, 2024
39b70ff
feat: 불필요해진 행사 생성 완료 path 제거
pakxe Sep 23, 2024
359dfcc
test: url을 수정된 값으로 변경
pakxe Sep 23, 2024
1f4dc28
test: 테스트 임시 비활성화
pakxe Sep 23, 2024
9b20499
feat: 조건부로 기본 이벤트 동작을 막던 것에서 조건 제거
pakxe Sep 24, 2024
9f317eb
feat: 불필요한 함수 제거
pakxe Sep 24, 2024
f92acbb
feat: 키다운 핸들러를 없애 onSubmit으로 통합
pakxe Sep 24, 2024
e4e49c4
test: 주석처리했던 테스트 복구
pakxe Sep 24, 2024
b721f5f
chore: 충돌 해결
pakxe Sep 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 7 additions & 9 deletions client/cypress/e2e/createEvent.cy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {ROUTER_URLS} from '@constants/routerUrls';
import CONSTANTS from '../constants/constants';

beforeEach(() => {
cy.blockSentry();
cy.blockKakao();
Expand All @@ -8,18 +10,17 @@ describe('Flow: 랜딩 페이지에서부터 이벤트를 생성 완료하는 fl
it('랜딩페이지에서 "행사 생성하기" 버튼을 눌러 행사 이름 입력 페이지로 이동해야 한다.', () => {
cy.visit('/');
cy.get('header').find('button').click();
cy.url().should('include', '/event/create/name');
cy.url().should('include', ROUTER_URLS.createEvent);
});

context('행사 이름 입력 페이지', () => {
beforeEach(() => {
cy.visit('/event/create/name');
cy.visit(ROUTER_URLS.createEvent);
});

it('행사 이름 입력 페이지에서 input이 포커싱 되어 있고, "다음" 버튼이 비활성화 되어 있어야 한다.', () => {
cy.get('input').focused();
cy.get('button').contains('다음').should('have.attr', 'disabled');
cy.url().should('include', '/event/create/name');
});

it('행사 이름이 1자 이상 입력된 경우 "다음" 버튼이 활성화 되고, 값이 없는 경우 "다음" 버튼이 비활성화 되어야 한다.', () => {
Expand All @@ -28,13 +29,14 @@ describe('Flow: 랜딩 페이지에서부터 이벤트를 생성 완료하는 fl
cy.get('input').clear();
cy.get('input').should('have.value', '');
cy.get('button').contains('다음').should('have.attr', 'disabled');
cy.url().should('include', '/event/create/name');
});

it('행사 이름을 입력한 후 "다음" 버튼을 누르면 행사 비밀번호 설정 화면으로 이동해야 한다.', () => {
cy.get('input').type(CONSTANTS.eventName);
cy.get('button').contains('다음').click();
cy.url().should('include', '/event/create/password');

// 다음 버튼을 클릭하면 /create/event 경로가 아니라 /create/event/?로 가네요.. 그래서 일단 제거함.
cy.contains('비밀번호').should('exist');
});
});

Expand All @@ -46,7 +48,6 @@ describe('Flow: 랜딩 페이지에서부터 이벤트를 생성 완료하는 fl
it('행사 비밀번호 입력 페이지에서 input이 포커싱 되어 있고, "행동 개시!" 버튼이 비활성화 되어 있어야 한다.', () => {
cy.get('input').focused();
cy.get('button').contains('행동 개시!').should('have.attr', 'disabled');
cy.url().should('include', '/event/create/password');
});

it('행사 비밀번호에 숫자가 아닌 입력을 할 경우 값이 입력되지 않아야 한다.', () => {
Expand All @@ -65,16 +66,13 @@ describe('Flow: 랜딩 페이지에서부터 이벤트를 생성 완료하는 fl
cy.get('input').clear();
cy.get('input').should('have.value', '');
cy.get('button').contains('행동 개시!').should('have.attr', 'disabled');
cy.url().should('include', '/event/create/password');
});

it('행사 비밀번호을 입력한 후 "행동 개시!" 버튼을 누르면 행사 생성 완료 화면으로 이동해야 한다.', () => {
cy.interceptAPI({type: 'postEvent', statusCode: 200});
cy.interceptAPI({type: 'getEventName', statusCode: 200});
cy.get('input').type(CONSTANTS.eventPassword);
cy.get('button').contains('행동 개시!').click();

cy.url().should('include', '/event/create/complete');
});
});
});
4 changes: 2 additions & 2 deletions client/cypress/support/commands.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {ROUTER_URLS} from '@constants/routerUrls';
import CONSTANTS from '../constants/constants';

type APIType = 'sentry' | 'postEvent' | 'getEventName';
Expand Down Expand Up @@ -46,10 +47,9 @@ Cypress.Commands.add('interceptAPI', ({type, delay = 0, statusCode = 200}: Inter
});

Cypress.Commands.add('createEventName', (eventName: string) => {
cy.visit('/event/create/name');
cy.visit(ROUTER_URLS.createEvent);
cy.get('input').type(eventName);
cy.get('button').contains('다음').click();
cy.url().should('include', '/event/create/password');
});

declare global {
Expand Down
9 changes: 6 additions & 3 deletions client/src/components/Design/components/TopNav/Back.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
/** @jsxImportSource @emotion/react */
import React from 'react';
import {useNavigate} from 'react-router-dom';

import TextButton from '@HDcomponents/TextButton/TextButton';

function Back() {
type BackProps = {
onClick?: () => void;
};

function Back({onClick}: BackProps) {
const navigate = useNavigate();

return (
<TextButton onClick={() => navigate(-1)} textSize="bodyBold" textColor="gray">
<TextButton onClick={() => (onClick ? onClick() : navigate(-1))} textSize="bodyBold" textColor="gray">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

back onClick의 기본값을 navigate(-1)기능을 넣어준 것은 컴포넌트의 이름이 back이기 때문에 ㄷ로가기 기능을 default로 넣어준걸까요?

뒤로가기
</TextButton>
);
Expand Down
6 changes: 2 additions & 4 deletions client/src/constants/routerUrls.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
export const ROUTER_URLS = {
main: '/',
eventCreateName: '/event/create/name',
eventCreatePassword: '/event/create/password',
eventCreateComplete: '/event/create/complete',
event: '/event', // TODO: (@weadie) baseurl을 어떻게 관리할 것인가?
createEvent: '/event/create',
event: '/event',
eventLogin: '/event/:eventId/login',
eventManage: '/event/:eventId/admin',
home: '/event/:eventId/home',
Expand Down
5 changes: 3 additions & 2 deletions client/src/hooks/queries/event/useRequestPostEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import {useMutation} from '@tanstack/react-query';
import {RequestPostEvent, requestPostEvent} from '@apis/request/event';

const useRequestPostEvent = () => {
const {mutate, ...rest} = useMutation({
const {mutate, mutateAsync, ...rest} = useMutation({
mutationFn: ({eventName, password}: RequestPostEvent) => requestPostEvent({eventName, password}),
});

// 실행 순서를 await으로 보장하기 위해 mutateAsync 사용
return {
postEvent: mutate,
postEvent: mutateAsync,
isPostEventPending: rest.isPending,
...rest,
};
Expand Down
17 changes: 17 additions & 0 deletions client/src/hooks/useCreateEventData.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {useState} from 'react';

import useSetEventNameStep from './useSetEventNameStep';

// 행사 생성 페이지에서 여러 스텝에 걸쳐 사용되는 상태를 선언해 내려주는 용도의 훅입니다.
const useCreateEventData = () => {
const eventNameProps = useSetEventNameStep();
const [eventToken, setEventToken] = useState('');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이벤트 토큰은 행사가 생성된 후 백엔드로부터 값을 받아오는 값이니깐 마지막 퍼널에서만 필요할 것 같아요.
즉 여러 상태에 걸쳐 사용되는 데이터는 아닌 것 같아서 다른 곳으로 이동해도 되지 않을까하는 생각입니다.


return {
eventNameProps,
eventToken,
setEventToken,
};
};

export default useCreateEventData;
58 changes: 58 additions & 0 deletions client/src/hooks/useFunnel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {useState} from 'react';

type UseFunnel = {
defaultStep: string;
stepList: string[];
};
Comment on lines +3 to +6
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우리가 정의한 step과 여기의 step이 다른 의미라서 조금 헷갈릴 수 있을 것 같아요.


type StepProps = {
children: React.ReactNode;
name: string;
};

type FunnelProps = {
children: React.ReactElement<StepProps>[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Funnel안에 Step밖에 못 오게 한 것 넘 좋아잉

};

const useFunnel = ({defaultStep, stepList}: UseFunnel) => {
const [step, setStep] = useState(defaultStep);

const moveToNextStep = () => {
const curStepIndex = stepList.indexOf(step);

if (curStepIndex === stepList.length - 1) return;

setStep(stepList[curStepIndex + 1]);
Comment on lines +23 to +25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (curStepIndex === stepList.length - 1) return;
setStep(stepList[curStepIndex + 1]);
setStep(stepList[Math.min(stepList.length - 1, curStepIndex + 1)]));

};

const moveToPrevStep = () => {
const curStepIndex = stepList.indexOf(step);

if (curStepIndex === 0) return;

setStep(stepList[curStepIndex - 1]);
Comment on lines +31 to +33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (curStepIndex === 0) return;
setStep(stepList[curStepIndex - 1]);
setStep(stepList[Math.max(0, curStepIndex - 1)]));

};

const Step = (stepProps: StepProps) => {
return <>{stepProps.children}</>;
};

const Funnel = ({children}: FunnelProps) => {
const targetStep = children.find(curStep => curStep.props.name === step);

if (!targetStep)
throw new Error(`현재 ${step} 단계에 보여줄 컴포넌트가 존재하지 않습니다. Step 컴포넌트를 호출해 사용해주세요.`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

역시 웨디 에러까지 꼼꼼히 👍👍


return <>{targetStep}</>;
};

return {
Step,
step,
Funnel,
moveToNextStep,
moveToPrevStep,
};
};

export default useFunnel;
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import {useState} from 'react';

import validateEventName from '@utils/validate/validateEventName';

const useSetEventNamePage = () => {
export type UseSetEventNameStepReturnType = ReturnType<typeof useSetEventNameStep>;

const useSetEventNameStep = () => {
const [eventName, setEventName] = useState('');
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [canSubmit, setCanSubmit] = useState(false);
Expand All @@ -29,4 +31,4 @@ const useSetEventNamePage = () => {
};
};

export default useSetEventNamePage;
export default useSetEventNameStep;
65 changes: 0 additions & 65 deletions client/src/hooks/useSetEventPasswordPage.ts

This file was deleted.

72 changes: 72 additions & 0 deletions client/src/hooks/useSetEventPasswordStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {useState} from 'react';
import {useNavigate} from 'react-router-dom';

import validateEventPassword from '@utils/validate/validateEventPassword';

import RULE from '@constants/rule';

import useRequestPostEvent from './queries/event/useRequestPostEvent';

export type UseSetEventPasswordStepReturnType = ReturnType<typeof useSetEventPasswordStep>;

const useSetEventPasswordStep = () => {
const [password, setPassword] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const [canSubmit, setCanSubmit] = useState(false);
const {postEvent: requestPostEvent, isPostEventPending} = useRequestPostEvent();

const submitDataForPostEvent = async ({
event,
eventName,
setEventToken,
}: {
event: React.FormEvent<HTMLFormElement>;
eventName: string;
setEventToken: (eventToken: string) => void;
}) => {
event.preventDefault();

await postEvent(eventName, setEventToken);
};

const getPasswordWithPad = () => {
return String(password).padStart(4, '0');
};

const postEvent = async (eventName: string, updateEventToken: (eventToken: string) => void) => {
await requestPostEvent(
{eventName, password: getPasswordWithPad()},
{
onSuccess: ({eventId}) => {
updateEventToken(eventId);
},
},
);
};

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value;
const validation = validateEventPassword(newValue);

setCanSubmit(newValue.length === RULE.maxEventPasswordLength);

if (validation.isValid) {
setPassword(newValue);
setErrorMessage('');
} else {
event.target.value = password;
setErrorMessage(validation.errorMessage ?? '');
Comment on lines +53 to +58
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ErrorMessage type이 string | null 이었던 걸로 기억합니다. 그래서 빈 문자열 대신에 null을 넣는 것이 더 좋을 것 같아요! 하지만 중요한 것은 아니라 패쑤해도 괜찮아유

}
};

return {
submitDataForPostEvent,
errorMessage,
handleChange,
canSubmit,
isPostEventPending,
password,
};
};

export default useSetEventPasswordStep;
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import {FixedButton, MainLayout, Title, TopNav} from '@HDesign/index';

import {ROUTER_URLS} from '@constants/routerUrls';

const CompleteCreateEventPage = () => {
const navigate = useNavigate();
const location = useLocation();
type CompleteCreateEventStepProps = {
eventToken: string;
};

const params = new URLSearchParams(location.search);
const eventId = params.get('eventId');
const CompleteCreateEventStep = ({eventToken}: CompleteCreateEventStepProps) => {
const navigate = useNavigate();

return (
<MainLayout backgroundColor="white">
Expand All @@ -32,9 +32,9 @@ const CompleteCreateEventPage = () => {
</Top>
<RunningDog />
</div>
<FixedButton onClick={() => navigate(`${ROUTER_URLS.event}/${eventId}/admin`)}>관리 페이지로 이동</FixedButton>
<FixedButton onClick={() => navigate(`${ROUTER_URLS.event}/${eventToken}/admin`)}>관리 페이지로 이동</FixedButton>
</MainLayout>
);
};

export default CompleteCreateEventPage;
export default CompleteCreateEventStep;
Loading
Loading