diff --git a/.gitignore b/.gitignore index 9876f894..397eb575 100644 --- a/.gitignore +++ b/.gitignore @@ -24,7 +24,8 @@ yarn-debug.log* yarn-error.log* /k8s/secret.yaml +/test-results/ +/playwright-report/ +/playwright/ /tests/tests-examples/ -/tests/tests-results/ -/tests/playwright-report/ -/playwright/.cache/ +/tests/logintest.ts \ No newline at end of file diff --git a/playwright.config.ts b/playwright.config.ts index 301801ee..fa8072fb 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,15 +1,15 @@ import { defineConfig, devices } from '@playwright/test'; +import path from 'path'; +require('dotenv').config(); -/** - * Read environment variables from file. - * https://github.com/motdotla/dotenv - */ -// require('dotenv').config(); - +export const ADMINSTATE = path.join(__dirname, './playwright/.auth/admin.json'); +export const ALBASTATE = path.join(__dirname, './playwright/.auth/alba.json'); /** * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ + // globalSetup: require.resolve('./tests/globalSetup.ts'), + testDir: './tests', /* Run tests in files in parallel */ fullyParallel: true, @@ -24,39 +24,74 @@ export default defineConfig({ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - // baseURL: 'http://127.0.0.1:3000', + baseURL: `${process.env.REACT_APP_BASE_URL}`, /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', }, - + timeout: 10000, /* Configure projects for major browsers */ projects: [ + // { + // name: 'setup', + // testMatch: /globalAdmin.setup\.ts/, + // use: { storageState: ADMINSTATE }, + // }, + // { + // name: 'setupAlba', + // testMatch: /globalAlba.setup\.ts/, + // use: { storageState: ALBASTATE }, + // }, { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, + name: 'admin', + // dependencies: ['setup'], + use: { + ...devices['Desktop Chrome'], + storageState: ADMINSTATE, + }, + testMatch: '**/tests/admin/**', + testIgnore: '**/tests/alba/**', }, - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, + name: 'alba', + // dependencies: ['setupAlba'], + use: { + ...devices['Desktop Chrome'], + storageState: ALBASTATE, + }, + testMatch: '**/tests/alba/**', + testIgnore: '**/tests/admin/**', }, - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, + name: 'logout', + use: { ...devices['Desktop Chrome'] }, + testMatch: /login.spec\.ts/, }, + // { + // name: 'chromium', + // use: { ...devices['Desktop Chrome'] }, + // }, - /* Test against mobile viewports. */ // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, // }, + // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, // }, + /* Test against mobile viewports. */ + { + name: 'Mobile Chrome', + use: { ...devices['Pixel 5'] }, + }, + { + name: 'Mobile Safari', + use: { ...devices['iPhone 12'] }, + }, + /* Test against branded browsers. */ // { // name: 'Microsoft Edge', diff --git a/src/App.tsx b/src/App.tsx index 90b7b01e..67bbbc37 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,7 @@ -import { QueryCache, QueryClient, QueryClientProvider, useQueryErrorResetBoundary } from '@tanstack/react-query'; +import { QueryClientProvider, useQueryErrorResetBoundary } from '@tanstack/react-query'; import { convertPath } from 'apis/convertURI'; import ViewPortContainer from 'components/@commons/ViewPortContainer'; import ErrorFallback from 'error/ErrorFallback'; -import { defaultErrorHandler } from 'error/defaultErrorHandler'; import { Provider } from 'jotai'; import HomeIndex from 'pages/HomeIndex'; import KakaoAuthPage from 'pages/KakaoAuthPage'; @@ -19,27 +18,7 @@ import { ErrorBoundary } from 'react-error-boundary'; import { Route, Routes } from 'react-router-dom'; import { ThemeProvider } from 'styled-components'; import { myTheme } from 'styles/myTheme'; - -const queryClient = new QueryClient({ - queryCache: new QueryCache({ - onError(error, query) { - defaultErrorHandler(error || { name: 'unknownError' }); - setTimeout(() => { - queryClient.removeQueries(query.queryKey); - }, 1000); - }, - }), - defaultOptions: { - queries: { - useErrorBoundary: true, - retry: 0, - refetchOnWindowFocus: false, - }, - mutations: { - onError: (err) => defaultErrorHandler(err || { name: 'unknownError' }), - }, - }, -}); +import { queryClient } from 'utils/queryClient'; function App(): JSX.Element { const { reset } = useQueryErrorResetBoundary(); diff --git a/src/apis/convertURI.ts b/src/apis/convertURI.ts index de42ed88..36c035e1 100644 --- a/src/apis/convertURI.ts +++ b/src/apis/convertURI.ts @@ -4,3 +4,5 @@ export const apiURL: string = process.env.REACT_APP_API_URL; export const convertPath = (path: string): string => { return staticServerUrl_ + path; }; + +export const baseURL = new URL(window.location.href).origin; diff --git a/src/apis/schedule/getMonthly.ts b/src/apis/schedule/getMonthly.ts index 90fe9b4b..747c0198 100644 --- a/src/apis/schedule/getMonthly.ts +++ b/src/apis/schedule/getMonthly.ts @@ -6,17 +6,18 @@ import { dateToString } from 'utils/dateToString'; export const getMonthly = async (info: Info): Promise => { const { year, month } = { ...info }; + const strMonth = String(month + 1).padStart(2, '0'); let params = {}; if (info.isAdmin) { params = { - month: `${year}-${month + 1}`, + month: `${year}-${strMonth}`, userId: info.userId, }; } else { params = { - month: `${year}-${month + 1}`, + month: `${year}-${strMonth}`, }; } diff --git a/src/components/Calendar/CalendarStyle.tsx b/src/components/Calendar/CalendarStyle.tsx index 06a19971..d0af153d 100644 --- a/src/components/Calendar/CalendarStyle.tsx +++ b/src/components/Calendar/CalendarStyle.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import FlexContainer from '../@commons/FlexContainer'; import styled from 'styled-components'; +import FlexContainer from '../@commons/FlexContainer'; export const DateCircle = styled.div<{ $isToday: boolean }>` background-color: ${(props) => (props.$isToday ? props.theme.color.yellow : null)}; @@ -20,7 +20,7 @@ export const BadgeText = styled.span` } `; -export const Badge = styled.div<{ $color?: string }>` +export const Badge = styled.ol<{ $color?: string }>` background-color: ${(props) => props.$color}; display: flex; diff --git a/src/components/Calendar/CalenderOutter.tsx b/src/components/Calendar/CalenderOutter.tsx index 2f235e5a..b139acf1 100644 --- a/src/components/Calendar/CalenderOutter.tsx +++ b/src/components/Calendar/CalenderOutter.tsx @@ -1,59 +1,63 @@ import FlexContainer from 'components/@commons/FlexContainer'; import Text from 'components/@commons/Text'; import { NextButton, PrevButton } from 'components/@commons/icons/buttons'; -import { PrimitiveAtom, useAtom } from 'jotai'; -import React from 'react'; -import { WeekGrid } from './CalendarStyle'; import weekdayArray from 'utils/weekdayArray'; +import { WeekGrid } from './CalendarStyle'; -interface Props { - monthDataAtom: PrimitiveAtom<{ - year: number; - month: number; - }>; -} +const CalenderOutter = ({ selectedMonth, setMonth }: Props): JSX.Element => { + return ( + + + + + ); +}; -const CalenderOutter = ({ monthDataAtom }: Props): JSX.Element => { - const [nowMonth, setNowMonth] = useAtom(monthDataAtom); - const { year, month } = nowMonth; +export default CalenderOutter; +const MonthSelectBar = ({ selectedMonth, setMonth }: Props) => { + const { year, month } = selectedMonth; const monthMoveHandler = (dm: number) => { - const newDateObj = new Date(nowMonth.year, nowMonth.month + dm, 1); - const newObj = { year: newDateObj.getFullYear(), month: newDateObj.getMonth() }; - setNowMonth(newObj); + const newDateObj = new Date(year, month + dm, 1); + setMonth({ year: newDateObj.getFullYear(), month: newDateObj.getMonth() }); }; return ( - - - monthMoveHandler(-1)} /> + + monthMoveHandler(-1)} /> - - {`${year} 년`} - {`${month + 1} 월`} - - - monthMoveHandler(+1)} /> - - - + + {`${year} 년`} + {`${month + 1} 월`} + + monthMoveHandler(+1)} /> ); }; -export default CalenderOutter; - const DayTitle = () => { return ( - - {weekdayArray.map((e) => ( - - - {e.eng} - - - ))} - + + + {weekdayArray.map((e) => ( + + + {e.eng} + + + ))} + + ); }; + +export interface MonthData { + year: number; + month: number; +} + +interface Props { + selectedMonth: MonthData; + setMonth: (monthdata: MonthData) => void; +} diff --git a/src/components/DailyWorkersTable/index.tsx b/src/components/DailyWorkersTable/index.tsx index d30e71c0..9b90e667 100644 --- a/src/components/DailyWorkersTable/index.tsx +++ b/src/components/DailyWorkersTable/index.tsx @@ -1,14 +1,13 @@ -import React from 'react'; +import { TimeWorkerListData, UserData } from 'apis/types'; import FlexContainer from 'components/@commons/FlexContainer'; +import GrayBox from 'components/@commons/GrayBox'; import Text from 'components/@commons/Text'; -import { NameBox, TitleBox } from './styles'; -import { TimeWorkerListData, UserData } from 'apis/types'; import { strTimeProcessor } from 'utils/strTimeProcessor'; -import GrayBox from 'components/@commons/GrayBox'; +import { NameBox, TitleBox } from './styles'; export const DailyWorkersTable = ({ dailyData }: { dailyData: TimeWorkerListData[] | undefined }): JSX.Element => { return ( - + {dailyData?.map((timeData: TimeWorkerListData, timeindex) => ( @@ -34,7 +33,7 @@ export const DailyWorkersTable = ({ dailyData }: { dailyData: TimeWorkerListData export const NotFixedDateBox = (): JSX.Element => { return ( - + 아직 확정된 스케줄이 없습니다 diff --git a/src/components/HeaderNB/HeaderNB.tsx b/src/components/HeaderNB/HeaderNB.tsx index 69bdb67b..27241f32 100644 --- a/src/components/HeaderNB/HeaderNB.tsx +++ b/src/components/HeaderNB/HeaderNB.tsx @@ -1,15 +1,15 @@ -import React, { useState } from 'react'; +import Text from 'components/@commons/Text'; +import { Hamburger } from 'components/@commons/icons'; import { + HeaderButton, HeaderContainer, HeaderInnerBox, - HeaderButton, HeaderLeftMenuGroup, HeaderTitleCont, } from 'components/HeaderNB/HeaderNBStyels'; import Sidebar from 'components/Sidebar'; -import Text from 'components/@commons/Text'; +import { useState } from 'react'; import { useLocation } from 'react-router-dom'; -import { Hamburger } from 'components/@commons/icons'; import { getLoginData } from 'utils/loginDatahandlers'; const isAdmin = getLoginData().isAdmin; @@ -29,7 +29,9 @@ const HeaderNB = (): JSX.Element => { - {isAdmin ? adminTitle[nowPath] : albaTitle[nowPath]} + + {isAdmin ? adminTitle[nowPath] : albaTitle[nowPath]} + diff --git a/src/components/PageStyledComponents/admin/MainPage/index.tsx b/src/components/PageStyledComponents/admin/MainPage/index.tsx index 4c3cee2f..ada118ce 100644 --- a/src/components/PageStyledComponents/admin/MainPage/index.tsx +++ b/src/components/PageStyledComponents/admin/MainPage/index.tsx @@ -12,3 +12,11 @@ export const DropDownCont = styled.div` background-color: ${({ theme }) => theme.color.lightGray}; z-index: 20; `; + +export const DropdownBtn = styled.button` + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + padding: 2px 0; +`; diff --git a/src/components/Sidebar/index.tsx b/src/components/Sidebar/index.tsx index 0b6f74f2..c2dea600 100644 --- a/src/components/Sidebar/index.tsx +++ b/src/components/Sidebar/index.tsx @@ -1,14 +1,14 @@ -import React, { Suspense } from 'react'; -import useLogin from 'hooks/auth/useLogin'; -import useModal from 'hooks/useModal'; -import GetInviteKeyModal from 'components/modals/GetInviteKeyModal'; -import { HorizontalLine, SidebarBackground, SidebarBox } from './styles'; -import Text from '../@commons/Text'; -import FlexContainer from '../@commons/FlexContainer'; import { UserData } from 'apis/types'; -import { getLoginData } from 'utils/loginDatahandlers'; import Loader from 'components/Suspenses/Loader'; +import GetInviteKeyModal from 'components/modals/GetInviteKeyModal'; +import useLogin from 'hooks/auth/useLogin'; import useGetMyInfo from 'hooks/useGetMyInfo'; +import useModal from 'hooks/useModal'; +import { Suspense } from 'react'; +import { getLoginData } from 'utils/loginDatahandlers'; +import FlexContainer from '../@commons/FlexContainer'; +import Text from '../@commons/Text'; +import { HorizontalLine, SidebarBackground, SidebarBox } from './styles'; const Sidebar = ({ closeHandler }: { closeHandler: () => void }): JSX.Element => { return ( @@ -73,13 +73,13 @@ const SideBarButtons = ({ isAdmin }: { isAdmin: boolean }) => { return ( {isAdmin && ( - modalOnHandler()}> + )} - + ); }; diff --git a/src/components/Suspenses/DefferedSuspense.tsx b/src/components/Suspenses/DefferedSuspense.tsx index cbb51d42..01c4725a 100644 --- a/src/components/Suspenses/DefferedSuspense.tsx +++ b/src/components/Suspenses/DefferedSuspense.tsx @@ -1,12 +1,13 @@ -import React, { PropsWithChildren, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; -const DefferedSuspense = ({ children }: PropsWithChildren<{}>) => { +const DefferedSuspense = ({ children, deffered = true }: { children: React.ReactNode; deffered?: boolean }) => { const [isDeferred, setIsDeferred] = useState(false); + const time = deffered ? 200 : 0; useEffect(() => { const timeout = setTimeout(() => { setIsDeferred(true); - }, 200); + }, time); return () => clearTimeout(timeout); }, []); diff --git a/src/components/Suspenses/Loader.tsx b/src/components/Suspenses/Loader.tsx index fe6aba2d..00f5f3ba 100644 --- a/src/components/Suspenses/Loader.tsx +++ b/src/components/Suspenses/Loader.tsx @@ -1,16 +1,9 @@ -import React from 'react'; import { styled } from 'styled-components'; import DefferedSuspense from './DefferedSuspense'; -const StyledLoader = styled.div<{ $size?: string }>` - margin: auto; - width: ${(props) => (props.$size ? props.$size : '40px')}; - height: ${(props) => (props.$size ? props.$size : '40px')}; -`; - -const Loader = ({ size }: { size?: string }) => { +const Loader = ({ size, isDeffered = true }: { size?: string; isDeffered?: boolean }) => { return ( - + { }; export default Loader; + +const StyledLoader = styled.div<{ $size?: string }>` + margin: auto; + width: ${(props) => (props.$size ? props.$size : '40px')}; + height: ${(props) => (props.$size ? props.$size : '40px')}; +`; diff --git a/src/components/Suspenses/Skeleton.tsx b/src/components/Suspenses/Skeleton.tsx index 8292a89d..09edbf40 100644 --- a/src/components/Suspenses/Skeleton.tsx +++ b/src/components/Suspenses/Skeleton.tsx @@ -1,7 +1,23 @@ -import React from 'react'; import styled, { keyframes } from 'styled-components'; import DefferedSuspense from './DefferedSuspense'; +interface Props { + width?: string; + height?: string; + aspectRatio?: string; + isDeffered?: boolean; +} + +const Skeleton = ({ width, height, aspectRatio, isDeffered = true }: Props) => { + return ( + + + + ); +}; + +export default Skeleton; + const skeletonAnimation = keyframes` 0% { background-position: 200% 0; @@ -25,23 +41,3 @@ const SkeletonBox = styled.div<{ border-radius: 4px; object-fit: cover; `; - -const Skeleton = ({ width, height, aspectRatio, isDeffered }: Props) => { - if (isDeffered) { - return ( - - - - ); - } - return ; -}; - -export default Skeleton; - -interface Props { - width?: string; - height?: string; - aspectRatio?: string; - isDeffered?: boolean; -} diff --git a/src/components/modals/GetInviteKeyModal/index.tsx b/src/components/modals/GetInviteKeyModal/index.tsx index ad8244cb..454f26f7 100644 --- a/src/components/modals/GetInviteKeyModal/index.tsx +++ b/src/components/modals/GetInviteKeyModal/index.tsx @@ -1,50 +1,58 @@ import { useQuery } from '@tanstack/react-query'; import { getInviteKey } from 'apis/admin/manageGroup'; - +import { baseURL } from 'apis/convertURI'; import FlexContainer from 'components/@commons/FlexContainer'; import SubmitButton from 'components/@commons/SubmitButton'; import Text from 'components/@commons/Text'; +import { CheckIcon } from 'components/@commons/icons'; +import Loader from 'components/Suspenses/Loader'; +import { LinkBox } from 'components/modals/GetInviteKeyModal/styles'; import useModal from 'hooks/useModal'; -import React from 'react'; +import { useState } from 'react'; import CopyToClipboard from 'react-copy-to-clipboard'; -import styled from 'styled-components'; +import { myTheme } from 'styles/myTheme'; const GetInviteKeyModal = (): JSX.Element => { - const { data: inviteKeyData } = useQuery(['inviteKey'], getInviteKey); + const { data: inviteKeyData, isFetching } = useQuery(['inviteKey'], getInviteKey); const { modalOffHandler } = useModal(); + const [isCopied, setIsCopied] = useState(false); + const link = `${baseURL}/invited/${inviteKeyData?.data.invitationKey}`; + return ( - + 초대 링크 아래 링크에 접속하면 그룹에 가입됩니다. - - {inviteKeyData?.data.invitationKey} - + + {isFetching ? : } + - - 복사하기 - - + {isFetching && ( + + + + )} + {!isFetching && ( + + {!isCopied ? ( + setIsCopied(true)}>복사하기 + ) : ( + + 복사됨 + + + )} + + )} + modalOffHandler()}> + 닫기 + ); }; export default GetInviteKeyModal; - -const Box = styled.div` - display: flex; - justify-content: center; - padding: 10px; - border: 1px solid; - border-color: ${({ theme }) => theme.color.gray}; -`; - -const Button = styled(SubmitButton)` - background-color: ${({ theme }) => theme.color.backgroundColor}; - border: 2px solid; - border-color: ${({ theme }) => theme.color.yellow}; -`; diff --git a/src/components/modals/GetInviteKeyModal/styles.tsx b/src/components/modals/GetInviteKeyModal/styles.tsx new file mode 100644 index 00000000..c607f119 --- /dev/null +++ b/src/components/modals/GetInviteKeyModal/styles.tsx @@ -0,0 +1,13 @@ +import styled from 'styled-components'; + +export const LinkBox = styled.input` + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; + border: 1px solid; + border-color: ${({ theme }) => theme.color.gray}; + overflow-x: scroll; + text-align: center; + height: 100%; +`; diff --git a/src/error/defaultErrorHandler.ts b/src/error/defaultErrorHandler.ts index 2dfa91e8..d32f9f0a 100644 --- a/src/error/defaultErrorHandler.ts +++ b/src/error/defaultErrorHandler.ts @@ -11,6 +11,7 @@ export const defaultErrorHandler = (error: ErrorData) => { if (error.response === undefined) { alert('서버 오류'); + removeLoginData(); return; } @@ -71,6 +72,7 @@ export const defaultErrorHandler = (error: ErrorData) => { default: alert(`잘못된 접근입니다`); + removeLoginData(); redirect(convertPath('/')); return; } diff --git a/src/hooks/auth/useLogin.ts b/src/hooks/auth/useLogin.ts index 9848ca06..edc6abfa 100644 --- a/src/hooks/auth/useLogin.ts +++ b/src/hooks/auth/useLogin.ts @@ -1,7 +1,6 @@ -import { useNavigate } from 'react-router-dom'; -import { convertPath } from 'apis/convertURI'; import { SignupRequest } from 'apis/auth'; -import React from 'react'; +import { baseURL, convertPath } from 'apis/convertURI'; +import { useNavigate } from 'react-router-dom'; import { removeLoginData } from 'utils/loginDatahandlers'; import { useLoginFetch } from './fetch'; @@ -12,7 +11,7 @@ const useLogin = (redirectPage?: string) => { const loginBtnHandler = (): void => { sessionStorage.setItem('beforeLoginURL', redirectPage || '/'); - const redirectURI = new URL(window.location.href).origin + '/login/kakao'; + const redirectURI = baseURL + '/login/kakao'; location.href = `https://kauth.kakao.com/oauth/authorize?client_id=${process.env.REACT_APP_KAKAO_API_KEY}&redirect_uri=${redirectURI}&response_type=code`; }; diff --git a/src/pages/KakaoAuthPage.tsx b/src/pages/KakaoAuthPage.tsx index 935ae8aa..2da57fa7 100644 --- a/src/pages/KakaoAuthPage.tsx +++ b/src/pages/KakaoAuthPage.tsx @@ -12,14 +12,18 @@ const KakaoAuthPage = (): JSX.Element => { const navigate = useNavigate(); React.useEffect(() => { - navigate(convertPath('/')); - if (code === null) { alert('다시 시도하세요'); + navigate(convertPath('/')); + return; + } + if (code === sessionStorage.getItem('code')) { + navigate(convertPath('/')); return; } login(code); + sessionStorage.setItem('code', code); }, [code]); return ( diff --git a/src/pages/SchedulePage/CalendarSection/CalenderConents.tsx b/src/pages/SchedulePage/CalendarSection/CalenderConents.tsx index e269e2e0..a1094356 100644 --- a/src/pages/SchedulePage/CalendarSection/CalenderConents.tsx +++ b/src/pages/SchedulePage/CalendarSection/CalenderConents.tsx @@ -1,11 +1,10 @@ -import React from 'react'; import { DailyWorkTimeData } from 'apis/types'; import { MonthBox, WeekGrid } from 'components/Calendar/CalendarStyle'; -import CalendarDayBox from './CalendarDayBox'; -import useSchedule from 'hooks/SchedulePage/useSchedule'; import { useGetMonthly } from 'hooks/SchedulePage/fetch'; +import useSchedule from 'hooks/SchedulePage/useSchedule'; import { useAtomValue } from 'jotai'; import { dateAtom } from '../states'; +import CalendarDayBox from './CalendarDayBox'; const CalenderConents = (): JSX.Element => { const { scheduleData } = useGetMonthly(); @@ -13,7 +12,7 @@ const CalenderConents = (): JSX.Element => { const selectedDate = useAtomValue(dateAtom); return ( - + {scheduleData?.table.map((weekArray: DailyWorkTimeData[], i) => ( {weekArray.map((e: DailyWorkTimeData) => ( diff --git a/src/pages/SchedulePage/HeaderSection/Dropdown.tsx b/src/pages/SchedulePage/HeaderSection/Dropdown.tsx index 54757050..7126ac01 100644 --- a/src/pages/SchedulePage/HeaderSection/Dropdown.tsx +++ b/src/pages/SchedulePage/HeaderSection/Dropdown.tsx @@ -1,11 +1,11 @@ -import React, { useState } from 'react'; +import { UserData } from 'apis/types'; import FlexContainer from 'components/@commons/FlexContainer'; import Text from 'components/@commons/Text'; +import { DropDown, DropUp } from 'components/@commons/icons'; +import { DropDownCont, DropdownBtn } from 'components/PageStyledComponents/admin/MainPage'; import { useAtom } from 'jotai'; -import { DropDownCont } from 'components/PageStyledComponents/admin/MainPage'; import { memberAtom } from 'pages/SchedulePage/states'; -import { UserData } from 'apis/types'; -import { DropDown, DropUp } from 'components/@commons/icons'; +import { useState } from 'react'; const Dropdown = ({ members }: { members: UserData[] }): JSX.Element => { const [member, setMember] = useAtom(memberAtom); @@ -23,27 +23,22 @@ const Dropdown = ({ members }: { members: UserData[] }): JSX.Element => { return ( - + {member.name || '선택'} {isOpen ? : } - + {isOpen && ( - + {members.map((member: UserData, index) => ( - contentOnClick(member)} key={member.name + index}> - - {member.name} - - +
    contentOnClick(member)}> + + + {member.name} + + +
))}
)} diff --git a/src/pages/SchedulePage/index.tsx b/src/pages/SchedulePage/index.tsx index 01d89c3a..a8770c9f 100644 --- a/src/pages/SchedulePage/index.tsx +++ b/src/pages/SchedulePage/index.tsx @@ -1,19 +1,17 @@ -import React, { Suspense } from 'react'; -import { useAtomValue } from 'jotai'; -import { memberAtom, monthAtom } from './states'; - -import PageContainer from 'components/@commons/PageContainer'; +import { UserData } from 'apis/types'; import FlexContainer from 'components/@commons/FlexContainer'; -import CalenderOutter from 'components/Calendar/CalenderOutter'; - +import PageContainer from 'components/@commons/PageContainer'; +import CalenderOutter, { MonthData } from 'components/Calendar/CalenderOutter'; +import Loader from 'components/Suspenses/Loader'; +import Skeleton from 'components/Suspenses/Skeleton'; +import { useAtom, useAtomValue } from 'jotai'; import CalenderConents from 'pages/SchedulePage/CalendarSection/CalenderConents'; import DailyWorkers from 'pages/SchedulePage/DailyWorkerSection/DailyWorkers'; import Dropdown from 'pages/SchedulePage/HeaderSection/Dropdown'; import TotalWorkTime from 'pages/SchedulePage/HeaderSection/TotalWorkTime'; -import { UserData } from 'apis/types'; -import Loader from 'components/Suspenses/Loader'; -import Skeleton from 'components/Suspenses/Skeleton'; +import { Suspense } from 'react'; import { getLoginData } from 'utils/loginDatahandlers'; +import { memberAtom, monthAtom } from './states'; const SchedulePage = ({ members }: { members?: UserData[] }): JSX.Element => { const isAdmin = getLoginData().isAdmin; @@ -32,7 +30,7 @@ const SchedulePage = ({ members }: { members?: UserData[] }): JSX.Element => { {nowMember.isSelected && ( - + }> @@ -49,3 +47,11 @@ const SchedulePage = ({ members }: { members?: UserData[] }): JSX.Element => { }; export default SchedulePage; + +const MonthSelector = () => { + const [selectedMonth, setter] = useAtom(monthAtom); + const setMonth = (newMonth: MonthData) => { + setter(newMonth); + }; + return ; +}; diff --git a/src/pages/SelectWeekPage/index.tsx b/src/pages/SelectWeekPage/index.tsx index d6171956..89008aa5 100644 --- a/src/pages/SelectWeekPage/index.tsx +++ b/src/pages/SelectWeekPage/index.tsx @@ -1,7 +1,8 @@ import FlexContainer from 'components/@commons/FlexContainer'; import PageContainer from 'components/@commons/PageContainer'; -import CalenderOutter from 'components/Calendar/CalenderOutter'; +import CalenderOutter, { MonthData } from 'components/Calendar/CalenderOutter'; import Skeleton from 'components/Suspenses/Skeleton'; +import { useAtom } from 'jotai'; import AdminDetailSect from 'pages/SelectWeekPage/AdminDetailSection'; import AlbaSubmitButton from 'pages/SelectWeekPage/AlbaSubmitButton'; import StatusCalendar from 'pages/SelectWeekPage/Calendar/StatusCalendar'; @@ -12,10 +13,9 @@ const SelectWeekPage = ({ isAdmin }: { isAdmin: boolean }): JSX.Element => { return ( - + }> - {isAdmin ? : } @@ -26,3 +26,11 @@ const SelectWeekPage = ({ isAdmin }: { isAdmin: boolean }): JSX.Element => { }; export default SelectWeekPage; + +const MonthSelector = () => { + const [selectedMonth, setter] = useAtom(weekStatusMonthAtom); + const setMonth = (newMonth: MonthData) => { + setter(newMonth); + }; + return ; +}; diff --git a/src/utils/loginDatahandlers.ts b/src/utils/loginDatahandlers.ts index e66c0693..895e1597 100644 --- a/src/utils/loginDatahandlers.ts +++ b/src/utils/loginDatahandlers.ts @@ -2,6 +2,7 @@ const storage: Storage = localStorage; export const saveLoginData = (token: string, userData: UserDataType) => { sessionStorage.removeItem('beforeLoginURL'); + sessionStorage.removeItem('code'); const loginData = { token: token, diff --git a/src/utils/queryClient.ts b/src/utils/queryClient.ts new file mode 100644 index 00000000..db4044c8 --- /dev/null +++ b/src/utils/queryClient.ts @@ -0,0 +1,23 @@ +import { QueryCache, QueryClient } from '@tanstack/query-core'; +import { defaultErrorHandler } from 'error/defaultErrorHandler'; + +export const queryClient = new QueryClient({ + queryCache: new QueryCache({ + onError(error, query) { + defaultErrorHandler(error || { name: 'unknownError' }); + setTimeout(() => { + queryClient.removeQueries(query.queryKey); + }, 1000); + }, + }), + defaultOptions: { + queries: { + useErrorBoundary: true, + retry: 0, + refetchOnWindowFocus: false, + }, + mutations: { + onError: (err) => defaultErrorHandler(err || { name: 'unknownError' }), + }, + }, +}); diff --git a/tests/admin/addGroup.spec.ts b/tests/admin/addGroup.spec.ts new file mode 100644 index 00000000..fddbcf26 --- /dev/null +++ b/tests/admin/addGroup.spec.ts @@ -0,0 +1,50 @@ +import { expect, test } from '@playwright/test'; +import { mockMapper, mockResponse } from '../mock/mockResponse'; +import { getMyinfoNoGroup } from '../mock/responseBody/getMyInfo'; + +test.beforeEach(async ({ page }) => { + mockMapper({ page, url: 'group', method: 'GET', response: mockResponse(getMyinfoNoGroup) }); + mockMapper({ page, url: 'group', method: 'POST', response: mockResponse(null) }); +}); + +test.describe('그룹 생성 페이지', () => { + test('그룹 생성', async ({ page, baseURL }) => { + // 1. 접속 + await page.goto(`${baseURL}/addGroup`); + expect(page.getByText('매장 등록하기')).toBeVisible(); + + const marketName = page.getByLabel('상호명'); + const marketNumber = page.getByLabel('사업자 번호'); + const address = page.getByLabel('상세 주소'); + const mainAddress = page.getByLabel('주소', { exact: true }); + + // 2. 매장명 입력 + await marketName.focus(); + await marketName.fill('카카오 프렌즈샵'); + + // 3. 사업자번호 입력 + await marketNumber.focus(); + await marketNumber.fill('0123456789'); + + // 4. 상세주소 입력 + await address.focus(); + await address.fill('상세 주소'); + + // 5. 다음 주소 + await mainAddress.click(); + + // 5. 다음 주소 - 검색 + const searchBox = page.locator('//fieldset/div/span'); + await searchBox.click(); + await searchBox.fill('강남구'); + + // 5. 다음 주소 - 첫번째 결과 클릭 + await page.locator('//div[@id="postCodeSuggestLayer"]//li[1]').click(); + await page.locator('//ul/li[1]/dl/dd[1]/span/button').click(); + + // 6. 제출 + await page.getByRole('button', { name: '그룹 생성하기' }).click(); + + await expect(page.getByText('매장 등록에 성공했습니다')).toBeVisible(); + }); +}); diff --git a/tests/admin/checkSchedule.spec.ts b/tests/admin/checkSchedule.spec.ts new file mode 100644 index 00000000..b706704b --- /dev/null +++ b/tests/admin/checkSchedule.spec.ts @@ -0,0 +1,49 @@ +import { expect, test } from '@playwright/test'; +import { CheckRequest, requestParamGetter } from '../mock/CheckRequest'; +import { mockMapper, mockResponse } from '../mock/mockResponse'; +import { getMyinfo } from '../mock/responseBody/getMyInfo'; +import { getDailyWorker, getMonthly } from '../mock/responseBody/getSchedule'; + +test('스케줄 확인', async ({ page, baseURL }) => { + await mockMapper({ page, url: 'group', method: 'GET', response: mockResponse(getMyinfo) }); + await mockMapper({ page, url: 'schedule/fix/month*', method: 'GET', response: mockResponse(getMonthly) }); + await mockMapper({ page, url: 'schedule/fix/day*', method: 'GET', response: mockResponse(getDailyWorker) }); + await requestParamGetter({ page, url: 'schedule/fix/month*' }); + + const check = new CheckRequest({ page, url: 'schedule/fix/month*' }); + await check.requestParamGetter(); + await page.goto(`${baseURL}`); + + // 1. 드롭다운에서 멤버를 선택하면 이번달 캘린더가 표시된다. + await page.getByRole('button', { name: '선택' }).click(); + await page.getByTestId('멤버리스트').locator('ol').nth(1).click(); + const calendar = page.getByTestId('월간스케줄'); + await expect(page.getByTestId('월간스케줄')).toBeVisible(); + + // 2. 캘린더 날짜를 누르면 + const firstDate = calendar.locator('div').nth(1).locator('div').nth(1); + await firstDate.click(); + const badge = firstDate.locator('div').locator('ol'); + + if (badge === null) { + // 확정 스케줄이 없으면 아직 스케줄이 확정되지 않았습니다 가 표시된다. + const notFixed = page.getByTestId('미확정일간근무표'); + await expect(notFixed).toBeVisible(); + } else { + // 확정 스케줄이 있으면 근무표가 표시된다 + const dailyTable = page.getByTestId('일간근무표'); + await expect(dailyTable).toBeVisible(); + } + + // 3. 캘린더 달을 이동하면 이전/다음달 캘린더가 표시된다 + const iter = 5; + for (let i = 0; i < iter; i++) { + await page.getByLabel('이전').click(); + } + + const today = new Date(); + const param = check.getRequestParam(); + const prevMonth = String(today.getMonth() + 1 - iter).padStart(2, '0'); + + expect(param).toContain(`${today.getFullYear()}-${prevMonth}`); +}); diff --git a/tests/admin/getInviteKey.spec.ts b/tests/admin/getInviteKey.spec.ts new file mode 100644 index 00000000..98949a84 --- /dev/null +++ b/tests/admin/getInviteKey.spec.ts @@ -0,0 +1,43 @@ +import { expect, test } from '@playwright/test'; +import { mockMapper, mockResponse } from '../mock/mockResponse'; +import { getMyinfoNoMember } from '../mock/responseBody/getMyInfo'; + +test.beforeEach(async ({ page, baseURL }) => { + await mockMapper({ page: page, url: 'group', method: 'GET', response: mockResponse(getMyinfoNoMember) }); + await mockMapper({ + page: page, + url: 'group/invitation', + method: 'GET', + response: mockResponse({ invitationKey: 'ABC' }), + }); + await page.goto(`${baseURL}`); +}); + +test.describe('초대 링크 발급', () => { + test('사이드바에서 초대 링크 발급', async ({ page }) => { + // 1. 사이드바에서 초대하기 버튼을 누르면 모달이 뜬다. + await page.getByLabel('메뉴').click(); + await page.getByRole('button', { name: '직원 초대하기' }).click(); + const modal = page.getByTestId('초대링크모달'); + await expect(modal).toBeVisible(); + + // 2. 링크가 발급된다 + const link = await modal.locator('input').getAttribute('value'); + expect(link).toContain('ABC'); + + // 3. 복사하기 버튼을 누르면 복사된다. + await page.getByRole('button', { name: '복사하기' }).click(); + await expect(page.getByRole('button', { name: '복사됨' })).toBeVisible(); + + // 4. 닫기 버튼을 누르면 사라진다. + await page.getByRole('button', { name: '닫기' }).click(); + await expect(page.getByRole('button', { name: '닫기' })).not.toBeVisible(); + }); + + test('메인에서 초대 링크 발급', async ({ page }) => { + // 1. 메인에서 초대하기 버튼을 누르면 모달이 뜬다. + await page.getByRole('button', { name: '초대링크 발급받기' }).click(); + const modal = page.getByTestId('초대링크모달'); + await expect(modal).toBeVisible(); + }); +}); diff --git a/tests/admin/mainPage.spec.ts b/tests/admin/mainPage.spec.ts new file mode 100644 index 00000000..ec9e4fc7 --- /dev/null +++ b/tests/admin/mainPage.spec.ts @@ -0,0 +1,29 @@ +import { expect, test } from '@playwright/test'; +import { mockMapper, mockResponse } from '../mock/mockResponse'; +import { getMyinfo, getMyinfoNoGroup, getMyinfoNoMember } from '../mock/responseBody/getMyInfo'; + +test.describe('매니저 메인 페이지', () => { + // 그룹이 있고 멤버가 있으면 스케줄 화면이 표시된다. + test('메인 : 그룹/멤버 있음', async ({ page, baseURL }) => { + await mockMapper({ page: page, url: 'group', method: 'GET', response: mockResponse(getMyinfo) }); + + await page.goto(`${baseURL}`); + await expect(page.locator('//span[contains(text(), "확정 스케줄")]')).toBeVisible(); + }); + + // 그룹이 있고 멤버가 없으면 초대하기 화면이 표시된다. + test('메인 : 멤버 없음', async ({ page, baseURL }) => { + await mockMapper({ page: page, url: 'group', method: 'GET', response: mockResponse(getMyinfoNoMember) }); + + await page.goto(`${baseURL}`); + await expect(page.getByRole('button', { name: '초대링크 발급받기' })).toBeVisible(); + }); + + // 그룹이 없으면 그룹 생성하기 화면이 표시된다. + test('메인 : 그룹 없음', async ({ page, baseURL }) => { + await mockMapper({ page: page, url: 'group', method: 'GET', response: mockResponse(getMyinfoNoGroup) }); + + await page.goto(`${baseURL}`); + await expect(page.getByText('등록된 매장이 없습니다')).toBeVisible(); + }); +}); diff --git a/tests/admin/sidebar.spec.ts b/tests/admin/sidebar.spec.ts new file mode 100644 index 00000000..95c0b98f --- /dev/null +++ b/tests/admin/sidebar.spec.ts @@ -0,0 +1,23 @@ +import { expect, test } from '@playwright/test'; +import { mockMapper, mockResponse } from '../mock/mockResponse'; +import { getMyinfo, getMyinfoNoGroup } from '../mock/responseBody/getMyInfo'; + +test.describe('사이드바', () => { + test('사이드바 : 그룹 있음', async ({ page, baseURL }) => { + await mockMapper({ page: page, url: 'group', method: 'GET', response: mockResponse(getMyinfo) }); + + await page.goto(`${baseURL}`); + await page.getByLabel('메뉴').click(); + + await expect(page.getByText('우리 매장 직원 목록')).toBeVisible(); + }); + + test('사이드바 : 그룹 없음', async ({ page, baseURL }) => { + await mockMapper({ page: page, url: 'group', method: 'GET', response: mockResponse(getMyinfoNoGroup) }); + + await page.goto(`${baseURL}`); + await page.getByLabel('메뉴').click(); + + await expect(page.getByText('우리 매장 직원 목록')).not.toBeVisible(); + }); +}); diff --git a/tests/alba/checkSchedule.spec.ts b/tests/alba/checkSchedule.spec.ts new file mode 100644 index 00000000..7a2d3d2a --- /dev/null +++ b/tests/alba/checkSchedule.spec.ts @@ -0,0 +1,47 @@ +import { expect, test } from '@playwright/test'; +import { CheckRequest, requestParamGetter } from '../mock/CheckRequest'; +import { mockMapper, mockResponse } from '../mock/mockResponse'; +import { getMyinfo } from '../mock/responseBody/getMyInfo'; +import { getDailyWorker, getMonthly } from '../mock/responseBody/getSchedule'; + +test('스케줄 확인', async ({ page, baseURL }) => { + await mockMapper({ page, url: 'group', method: 'GET', response: mockResponse(getMyinfo) }); + await mockMapper({ page, url: 'schedule/fix/month*', method: 'GET', response: mockResponse(getMonthly) }); + await mockMapper({ page, url: 'schedule/fix/day*', method: 'GET', response: mockResponse(getDailyWorker) }); + await requestParamGetter({ page, url: 'schedule/fix/month*' }); + + const check = new CheckRequest({ page, url: 'schedule/fix/month*' }); + await check.requestParamGetter(); + + // 1. 접속하면 이번달 캘린더가 표시된다. + await page.goto(`${baseURL}`); + const calendar = page.getByTestId('월간스케줄'); + await expect(page.getByTestId('월간스케줄')).toBeVisible(); + + // 2. 캘린더 날짜를 누르면 + const firstDate = calendar.locator('div').nth(1).locator('div').nth(1); + await firstDate.click(); + const badge = firstDate.locator('div').locator('ol'); + + if (badge === null) { + // 2-1. 확정 스케줄이 없으면 아직 스케줄이 확정되지 않았습니다 가 표시된다. + const notFixed = page.getByTestId('미확정일간근무표'); + await expect(notFixed).toBeVisible(); + } else { + // 2-2. 확정 스케줄이 있으면 근무표가 표시된다 + const dailyTable = page.getByTestId('일간근무표'); + await expect(dailyTable).toBeVisible(); + } + + // 3. 캘린더 달을 이동하면 이전/다음달 캘린더가 표시된다 + const iter = 5; + for (let i = 0; i < iter; i++) { + await page.getByLabel('이전').click(); + } + + const today = new Date(); + const prevMonth = String(today.getMonth() + 1 - iter).padStart(2, '0'); + + const param = check.getRequestParam(); + expect(param).toContain(`${today.getFullYear()}-${prevMonth}`); +}); diff --git a/tests/alba/invitation.spec.ts b/tests/alba/invitation.spec.ts new file mode 100644 index 00000000..37efa814 --- /dev/null +++ b/tests/alba/invitation.spec.ts @@ -0,0 +1,30 @@ +import { expect, test } from '@playwright/test'; +import { mockMapper, mockResponse } from '../mock/mockResponse'; +import { getGroupInfo } from '../mock/responseBody/getInvitation'; + +test.describe('초대장', () => { + test('접속', async ({ page, baseURL }) => { + await mockMapper({ + page: page, + url: 'group/invitation/information*', + method: 'GET', + response: mockResponse(getGroupInfo), + }); + await mockMapper({ + page: page, + url: 'group/invitation', + method: 'POST', + response: mockResponse(null), + }); + + // 접속 + await page.goto(`${baseURL}/invited/123`); + const marketName = page.getByText('라이언 월드'); + await expect(marketName).toBeVisible(); + + // 승인 + await page.getByRole('button', { name: '승인하기' }).click(); + const successMessage = page.getByText('그룹 가입에 성공했어요'); + await expect(successMessage).toBeVisible(); + }); +}); diff --git a/tests/alba/mainPage.spec.ts b/tests/alba/mainPage.spec.ts new file mode 100644 index 00000000..8a41514d --- /dev/null +++ b/tests/alba/mainPage.spec.ts @@ -0,0 +1,23 @@ +import { expect, test } from '@playwright/test'; +import { mockMapper, mockResponse } from '../mock/mockResponse'; +import { getMyinfo, getMyinfoNoGroup } from '../mock/responseBody/getMyInfo'; +import { getMonthly } from './../mock/responseBody/getSchedule'; + +test.describe('알바 메인 페이지', () => { + // 그룹이 있으면 스케줄 화면이 표시된다. + test('메인 : 그룹 있음', async ({ page, baseURL }) => { + await mockMapper({ page: page, url: 'group', method: 'GET', response: mockResponse(getMyinfo) }); + await mockMapper({ page: page, url: 'schedule/fix/month*', method: 'GET', response: mockResponse(getMonthly) }); + + await page.goto(`${baseURL}`); + await expect(page.getByTestId('월간스케줄')).toBeVisible(); + }); + + // 그룹이 없으면 그룹없음 화면이 표시된다. + test('메인 : 그룹 없음', async ({ page, baseURL }) => { + await mockMapper({ page: page, url: 'group', method: 'GET', response: mockResponse(getMyinfoNoGroup) }); + + await page.goto(`${baseURL}`); + await expect(page.getByText('매니저에게 초대링크를 요청하세요')).toBeVisible(); + }); +}); diff --git a/tests/mock/CheckRequest.ts b/tests/mock/CheckRequest.ts new file mode 100644 index 00000000..540074d8 --- /dev/null +++ b/tests/mock/CheckRequest.ts @@ -0,0 +1,73 @@ +import { Page } from '@playwright/test'; + +class CheckRequest { + // public requestParam: string | null; + // public requestBody: string | null; + page: Page; + url: string; + requestParam: string[] = []; + requestBody: string[] = []; + + constructor({ page, url }: { page: Page; url: string }) { + this.page = page; + this.url = url; + } + + public getRequestParam = () => { + if (this.requestParam.length === 0) return null; + return this.requestParam.at(-1); + }; + + public getRequestBody = () => { + if (this.requestBody.length === 0) return null; + return this.requestBody.at(-1); + }; + + public requestParamGetter = async () => { + await this.page.route(`*/**/${this.url}`, async (route) => { + if (route.request().method() === 'GET') { + this.requestParam.push(route.request().url()); + route.continue(); + return; + } + return; + }); + }; + + public requestBodyGetter = async () => { + await this.page.route(`*/**/${this.url}`, async (route) => { + if (route.request().method() === 'POST' || route.request().method() === 'PUT') { + const response = route.request().postData(); + if (response !== null) { + this.requestBody.push(response); + route.continue(); + return; + } + } + }); + }; +} + +export { CheckRequest }; + +export const requestParamGetter = async ({ page, url }: { page: Page; url: string }) => { + await page.route(`*/**/${url}`, async (route) => { + if (route.request().method() === 'GET') { + const request = route.request().url(); + route.continue(); + return request; + } + return null; + }); +}; + +export const requestBodyGetter = async ({ page, url }: { page: Page; url: string }) => { + await page.route(`*/**/${url}`, async (route) => { + if (route.request().method() === 'POST' || route.request().method() === 'PUT') { + const request = route.request().postData(); + route.continue(); + return request; + } + return null; + }); +}; diff --git a/tests/mock/mockResponse.ts b/tests/mock/mockResponse.ts new file mode 100644 index 00000000..db2b4947 --- /dev/null +++ b/tests/mock/mockResponse.ts @@ -0,0 +1,33 @@ +import { Page } from 'playwright-core'; + +export const mockResponse = (responseBody) => { + if (responseBody === null) { + return { status: 200 }; + } + return { status: 200, contentType: 'application/json', body: JSON.stringify(responseBody) }; +}; + +export const mockMapper = async ({ + page, + response, + url, + method, +}: { + page: Page; + response: Response; + url: string; + method: 'GET' | 'POST' | 'PUT' | 'DELETE'; +}) => { + await page.route(`*/**/${url}`, async (route) => { + if (route.request().method() === method) { + await route.fulfill(response); + } else { + await route.continue(); + } + }); +}; + +interface Response { + body?: string | Buffer | undefined; + status?: number | undefined; +} diff --git a/tests/mock/responseBody/getInvitation.ts b/tests/mock/responseBody/getInvitation.ts new file mode 100644 index 00000000..6a8d45fe --- /dev/null +++ b/tests/mock/responseBody/getInvitation.ts @@ -0,0 +1,3 @@ +export const getGroupInfo = { + marketName: '라이언 월드', +}; diff --git a/tests/mock/responseBody/getMyInfo.ts b/tests/mock/responseBody/getMyInfo.ts new file mode 100644 index 00000000..2e6335e3 --- /dev/null +++ b/tests/mock/responseBody/getMyInfo.ts @@ -0,0 +1,37 @@ +export const getMyinfoNoGroup = { + userName: '라이언', + groupName: null, + members: [], +}; + +export const getMyinfoNoMember = { + userName: '라이언', + groupName: '라이언 월드', + members: [{ name: '라이언', userId: 1 }], +}; + +export const getMyinfo = { + userName: '라이언', + groupName: '라이언 월드', + members: [ + { name: '라이언', userId: 1 }, + { name: '라이언', userId: 2 }, + { name: '라이언', userId: 3 }, + { name: '라이언', userId: 4 }, + { name: '라이언', userId: 5 }, + { name: '라이언', userId: 6 }, + { name: '라이언', userId: 7 }, + { name: '라이언', userId: 8 }, + { name: '라이언', userId: 9 }, + { name: '라이언', userId: 10 }, + { name: '라이언', userId: 11 }, + { name: '라이언', userId: 12 }, + { name: '라이언', userId: 13 }, + { name: '라이언', userId: 14 }, + { name: '라이언', userId: 15 }, + { name: '라이언', userId: 16 }, + { name: '라이언', userId: 17 }, + { name: '라이언', userId: 18 }, + { name: '라이언', userId: 19 }, + ], +}; diff --git a/tests/mock/responseBody/getSchedule.ts b/tests/mock/responseBody/getSchedule.ts new file mode 100644 index 00000000..580afa09 --- /dev/null +++ b/tests/mock/responseBody/getSchedule.ts @@ -0,0 +1,142 @@ +export const getMonthly = { + schedule: [ + { + date: '2023-10-30', + workTime: ['오픈', '미들'], + }, + { + date: '2023-10-31', + workTime: ['오픈', '미들'], + }, + { + date: '2023-11-01', + workTime: ['오픈', '미들'], + }, + { + date: '2023-11-02', + workTime: ['오픈', '미들'], + }, + { + date: '2023-11-03', + workTime: ['오픈', '미들'], + }, + { + date: '2023-11-04', + workTime: ['오픈', '미들'], + }, + { + date: '2023-11-05', + workTime: ['오픈', '미들'], + }, + { + date: '2023-11-06', + workTime: ['오픈', '미들'], + }, + { + date: '2023-11-07', + workTime: ['오픈', '미들'], + }, + { + date: '2023-11-08', + workTime: ['미들'], + }, + { + date: '2023-11-09', + workTime: ['오픈', '미들'], + }, + { + date: '2023-11-10', + workTime: ['오픈', '미들'], + }, + { + date: '2023-11-11', + workTime: ['오픈', '미들'], + }, + { + date: '2023-11-12', + workTime: ['미들'], + }, + { + date: '2023-11-13', + workTime: ['오픈', '미들'], + }, + { + date: '2023-11-14', + workTime: ['오픈', '미들'], + }, + { + date: '2023-11-15', + workTime: ['오픈', '미들'], + }, + { + date: '2023-11-16', + workTime: ['미들'], + }, + { + date: '2023-11-17', + workTime: ['오픈', '미들'], + }, + { + date: '2023-11-18', + workTime: [], + }, + { + date: '2023-11-19', + workTime: ['오픈', '미들'], + }, + ], + work_summary: { + weekly: 15.5, + monthly: 61.5, + }, +}; + +export const getDailyWorker = { + schedule: [ + { + title: '오픈', + startTime: '10:00:00', + endTime: '12:00:00', + workerList: [ + { + userId: 1, + name: '라이언', + }, + { + userId: 2, + name: '어피치', + }, + ], + }, + { + title: '미들', + startTime: '12:00:00', + endTime: '16:00:00', + workerList: [ + { + userId: 3, + name: '무지', + }, + { + userId: 2, + name: '어피치', + }, + ], + }, + { + title: '마감', + startTime: '16:00:00', + endTime: '20:00:00', + workerList: [ + { + userId: 3, + name: '무지', + }, + { + userId: 4, + name: '춘식이', + }, + ], + }, + ], +}; diff --git a/tests/mock/responseBody/getTimeTemplate.ts b/tests/mock/responseBody/getTimeTemplate.ts new file mode 100644 index 00000000..60402730 --- /dev/null +++ b/tests/mock/responseBody/getTimeTemplate.ts @@ -0,0 +1,15 @@ +import { TimeData } from './../../../src/apis/types'; +// schedule/worktime +export const getTimeTemplate = {}; + +interface GetReturn { + template: TimeData[]; +} +// /schedule/worktime +export const postOpenApplication = {}; + +interface PostRequest { + weekStartDate: string; + template: TimeData[]; + amount: number[][]; +} diff --git a/tests/mock/responseBody/postAuth.ts b/tests/mock/responseBody/postAuth.ts new file mode 100644 index 00000000..5de9a1fe --- /dev/null +++ b/tests/mock/responseBody/postAuth.ts @@ -0,0 +1,15 @@ +export const postLoginNoUser = { + status: 404, + body: JSON.stringify({ + code: -10006, + }), +}; + +export const postLoginAdmin = { + token: 'Bearer ABC', + isAdmin: true, +}; +export const postLoginAlba = { + token: 'Bearer ABC', + isAdmin: false, +};