diff --git a/package.json b/package.json index 0540aa5f..b35f9d6c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "dutying-web", "private": true, - "version": "0.5.0", + "version": "0.6.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src/assets/svg/SuccessCircleIcon.tsx b/src/assets/svg/SuccessCircleIcon.tsx new file mode 100644 index 00000000..32049a12 --- /dev/null +++ b/src/assets/svg/SuccessCircleIcon.tsx @@ -0,0 +1,15 @@ +import type { SVGProps } from 'react'; +const SvgSuccessCircleIcon = (props: SVGProps) => ( + + + + + +); +export default SvgSuccessCircleIcon; diff --git a/src/assets/svg/index.ts b/src/assets/svg/index.ts index 8758eb10..08109169 100644 --- a/src/assets/svg/index.ts +++ b/src/assets/svg/index.ts @@ -62,3 +62,4 @@ export { default as UncheckedIcon } from './UncheckedIcon'; export { default as UncheckedIcon2 } from './UncheckedIcon2'; export { default as UnlinkedIcon } from './UnlinkedIcon'; export { default as XIcon } from './XIcon'; +export { default as SuccessCircleIcon } from './SuccessCircleIcon'; diff --git a/src/hooks/auth/useAuth/index.ts b/src/hooks/auth/useAuth/index.ts index 1fad2e40..182b6cf1 100644 --- a/src/hooks/auth/useAuth/index.ts +++ b/src/hooks/auth/useAuth/index.ts @@ -1,7 +1,6 @@ import { shallow } from 'zustand/shallow'; import useAuthStore from './store'; import axiosInstance from '@libs/api/client'; -import { useMutation } from '@tanstack/react-query'; import { demoStart, getAccountMe } from '@libs/api/auth'; import { useNavigate } from 'react-router'; import ROUTE from '@libs/constant/path'; @@ -48,24 +47,21 @@ const useAuth = (activeEffect = false) => { setState('isAuth', true); setState('accessToken', accessToken); initEditShiftStore(); - sendEvent(events.auth.login); axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`; - navigate(nextPageUrl || '/make'); + location.replace(nextPageUrl || '/make'); sendEvent(events.auth.login); }; - const { mutate: demoTry } = useMutation(demoStart(), { - onSuccess: (data) => { - handleLogin(data.accessToken); - setState('accessToken', data.accessToken); - setState('accountId', data.accountResDto.accountId); - setState('nurseId', data.accountResDto.nurseId); - setState('wardId', data.accountResDto.wardId); - setState('isAuth', true); - setState('demoStartDate', new Date()); - navigate(ROUTE.MAKE); - }, - }); + const demoTry = async () => { + const data = await demoStart(); + setState('accessToken', data.accessToken); + setState('accountId', data.accountResDto.accountId); + setState('nurseId', data.accountResDto.nurseId); + setState('wardId', data.accountResDto.wardId); + setState('isAuth', true); + setState('demoStartDate', new Date()); + navigate(ROUTE.MAKE); + }; const handleGetAccountMe = async () => { const account = await getAccountMe(); diff --git a/src/hooks/shift/useEditShift/index.ts b/src/hooks/shift/useEditShift/index.ts index eb1eb5a1..eb2d108b 100644 --- a/src/hooks/shift/useEditShift/index.ts +++ b/src/hooks/shift/useEditShift/index.ts @@ -311,6 +311,12 @@ const useEditShift = (activeEffect = false) => { const changeFocusedShift = useCallback( (shiftTypeId: number | null) => { if (!wardId || !focus || !shift) return; + console.log( + shift.divisionShiftNurses + .flatMap((x) => x) + .find((x) => x.shiftNurse.shiftNurseId === focus.shiftNurseId)!.wardShiftList[focus.day], + shiftTypeId + ); if ( shift.divisionShiftNurses .flatMap((x) => x) diff --git a/src/hooks/ward/useEditShiftTeam/index.ts b/src/hooks/ward/useEditShiftTeam/index.ts index a32e28f4..ce716561 100644 --- a/src/hooks/ward/useEditShiftTeam/index.ts +++ b/src/hooks/ward/useEditShiftTeam/index.ts @@ -1,28 +1,14 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { - UpdateNurseDTO, - updateNurse as patchNurse, - updateNurseOrder, - updateNurseShiftType, - updateNurseShiftTypeRequest, - updateShiftTeamDivision, -} from '@libs/api/nurse'; -import useEditNurseStore from './store'; +import * as nurseApi from '@libs/api/nurse'; +import * as shiftTeamApi from '@libs/api/shiftTeam'; import { shallow } from 'zustand/shallow'; -import { - UpdateShiftTeamDTO, - addNurseIntoShiftTeam, - createShiftTeam, - deleteShiftTeam, - removeNurseFromShiftTeam, - updateShiftTeam, -} from '@libs/api/shiftTeam'; import { getWard } from '@libs/api/ward'; import { produce } from 'immer'; +import useEditNurseStore from './store'; import useEditShift from '@hooks/shift/useEditShift'; -import useAuth from '@hooks/auth/useAuth'; import useRequestShift from '@hooks/shift/useRequestShift'; +import useAuth from '@hooks/auth/useAuth'; const useEditShiftTeam = () => { const [selectedNurseId, setState] = useEditNurseStore( @@ -47,8 +33,8 @@ const useEditShiftTeam = () => { }); const { mutate: updateNurseMutate } = useMutation( - ({ nurseId, updateNurseDTO }: { nurseId: number; updateNurseDTO: UpdateNurseDTO }) => - patchNurse(nurseId, updateNurseDTO), + ({ nurseId, updateNurseDTO }: { nurseId: number; updateNurseDTO: nurseApi.UpdateNurseDTO }) => + nurseApi.updateNurse(nurseId, updateNurseDTO), { onSuccess: () => { queryClient.invalidateQueries(getWardQueryKey); @@ -63,7 +49,7 @@ const useEditShiftTeam = () => { const { mutate: addNurseMutate } = useMutation( ({ wardId, shiftTeamId }: { wardId: number; shiftTeamId: number }) => - addNurseIntoShiftTeam(wardId, shiftTeamId, { + shiftTeamApi.addNurseIntoShiftTeam(wardId, shiftTeamId, { name: `간호사${Math.floor(Math.random() * 10000)}`, phoneNum: '01012345678', gender: '여', @@ -83,7 +69,7 @@ const useEditShiftTeam = () => { const { mutate: deleteNurseMutate } = useMutation( ({ wardId, nurseId, shiftTeamId }: { wardId: number; nurseId: number; shiftTeamId: number }) => - removeNurseFromShiftTeam(wardId, shiftTeamId, nurseId), + shiftTeamApi.removeNurseFromShiftTeam(wardId, shiftTeamId, nurseId), { onSuccess: () => queryClient.invalidateQueries(getWardQueryKey), } @@ -97,15 +83,15 @@ const useEditShiftTeam = () => { }: { nurseId: number; nurseShiftTypeId: number; - change: updateNurseShiftTypeRequest; - }) => updateNurseShiftType(nurseId, nurseShiftTypeId, change), + change: nurseApi.updateNurseShiftTypeRequest; + }) => nurseApi.updateNurseShiftType(nurseId, nurseShiftTypeId, change), { onSuccess: () => queryClient.invalidateQueries(getWardQueryKey), } ); const { mutate: createShiftTeamMutate } = useMutation( - (wardId: number) => createShiftTeam(wardId), + (wardId: number) => shiftTeamApi.createShiftTeam(wardId), { onSuccess: () => queryClient.invalidateQueries(getWardQueryKey), } @@ -113,7 +99,7 @@ const useEditShiftTeam = () => { const { mutate: deleteShiftTeamMutate } = useMutation( ({ wardId, shiftTeamId }: { wardId: number; shiftTeamId: number }) => - deleteShiftTeam(wardId, shiftTeamId), + shiftTeamApi.deleteShiftTeam(wardId, shiftTeamId), { onSuccess: () => queryClient.invalidateQueries(getWardQueryKey), } @@ -130,7 +116,7 @@ const useEditShiftTeam = () => { prevPriority: number; changeValue: number; patchYearMonth: string; - }) => updateShiftTeamDivision(shiftTeamId, prevPriority, changeValue, patchYearMonth), + }) => nurseApi.updateShiftTeamDivision(shiftTeamId, prevPriority, changeValue, patchYearMonth), { onSuccess: () => { queryClient.invalidateQueries(getWardQueryKey); @@ -158,7 +144,7 @@ const useEditShiftTeam = () => { nextPriority: number; patchYearMonth: string; }) => - updateNurseOrder( + nurseApi.updateNurseOrder( nurseId, shiftTeamId, nextShiftTeamId, @@ -310,8 +296,8 @@ const useEditShiftTeam = () => { }: { wardId: number; shiftTeamId: number; - updateShiftTeamDTO: UpdateShiftTeamDTO; - }) => updateShiftTeam(wardId, shiftTeamId, updateShiftTeamDTO), + updateShiftTeamDTO: shiftTeamApi.UpdateShiftTeamDTO; + }) => shiftTeamApi.updateShiftTeam(wardId, shiftTeamId, updateShiftTeamDTO), { onMutate: ({ shiftTeamId, updateShiftTeamDTO }) => { const oldWard = queryClient.getQueryData(getWardQueryKey); @@ -333,6 +319,65 @@ const useEditShiftTeam = () => { } ); + const addNurse = (shiftTeamId: number) => { + wardId && addNurseMutate({ wardId, shiftTeamId }); + }; + const deleteNurse = (shiftTeamId: number, nurseId: number) => { + wardId && deleteNurseMutate({ wardId, nurseId, shiftTeamId }); + }; + const selectNurse = (nurseId: number | null) => { + setState('selectedNurseId', nurseId); + }; + const updateNurse = (nurseId: number, updateNurseDTO: nurseApi.UpdateNurseDTO) => { + updateNurseMutate({ nurseId, updateNurseDTO }); + }; + const updateNurseShift = ( + nurseId: number, + nurseShiftTypeId: number, + change: nurseApi.updateNurseShiftTypeRequest + ) => { + updateNurseShiftTypeMutate({ nurseId, nurseShiftTypeId, change }); + }; + const createShiftTeam = () => { + wardId && createShiftTeamMutate(wardId); + }; + const deleteShiftTeam = (shiftTeamId: number) => { + wardId && deleteShiftTeamMutate({ wardId, shiftTeamId }); + }; + const editDivision = ( + shiftTeamId: number, + prevPriority: number, + changeValue: number, + patchYearMonth: string + ) => { + editDivisionMutate({ shiftTeamId, prevPriority, changeValue, patchYearMonth }); + }; + const moveNurseOrder = ( + nurseId: number, + shiftTeamId: number, + nextShiftTeamId: number, + divisionNum: number, + prevPriority: number, + nextPriority: number, + patchYearMonth: string + ) => { + moveNurseOrderMutate({ + nurseId, + shiftTeamId, + nextShiftTeamId, + divisionNum, + prevPriority, + nextPriority, + patchYearMonth, + }); + }; + const updateShiftTeam = ( + shiftTeamId: number, + updateShiftTeamDTO: shiftTeamApi.UpdateShiftTeamDTO + ) => { + wardId && updateShiftTeamMutate({ wardId, shiftTeamId, updateShiftTeamDTO }); + }; + return { state: { ward, @@ -342,46 +387,16 @@ const useEditShiftTeam = () => { shiftTeams: ward?.shiftTeams, }, actions: { - addNurse: (shiftTeamId: number) => wardId && addNurseMutate({ wardId, shiftTeamId }), - deleteNurse: (shiftTeamId: number, nurseId: number) => - wardId && deleteNurseMutate({ wardId, nurseId, shiftTeamId }), - selectNurse: (nurseId: number | null) => setState('selectedNurseId', nurseId), - updateNurse: (nurseId: number, updateNurseDTO: UpdateNurseDTO) => - updateNurseMutate({ nurseId, updateNurseDTO }), - updateNurseShift: ( - nurseId: number, - nurseShiftTypeId: number, - change: updateNurseShiftTypeRequest - ) => updateNurseShiftTypeMutate({ nurseId, nurseShiftTypeId, change }), - createShiftTeam: () => wardId && createShiftTeamMutate(wardId), - deleteShiftTeam: (shiftTeamId: number) => - wardId && deleteShiftTeamMutate({ wardId, shiftTeamId }), - editDivision: ( - shiftTeamId: number, - prevPriority: number, - changeValue: number, - patchYearMonth: string - ) => editDivisionMutate({ shiftTeamId, prevPriority, changeValue, patchYearMonth }), - moveNurseOrder: ( - nurseId: number, - shiftTeamId: number, - nextShiftTeamId: number, - divisionNum: number, - prevPriority: number, - nextPriority: number, - patchYearMonth: string - ) => - moveNurseOrderMutate({ - nurseId, - shiftTeamId, - nextShiftTeamId, - divisionNum, - prevPriority, - nextPriority, - patchYearMonth, - }), - updateShiftTeam: (shiftTeamId: number, updateShiftTeamDTO: UpdateShiftTeamDTO) => - wardId && updateShiftTeamMutate({ wardId, shiftTeamId, updateShiftTeamDTO }), + addNurse, + deleteNurse, + selectNurse, + updateNurse, + updateNurseShift, + createShiftTeam, + deleteShiftTeam, + editDivision, + moveNurseOrder, + updateShiftTeam, }, }; }; diff --git a/src/hooks/ward/useEditWard/index.ts b/src/hooks/ward/useEditWard/index.ts index 909add0f..1a24ff29 100644 --- a/src/hooks/ward/useEditWard/index.ts +++ b/src/hooks/ward/useEditWard/index.ts @@ -1,12 +1,7 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { EditWardDTO, editWard, getWard } from '@libs/api/ward'; +import * as wardApi from '@libs/api/ward'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { - CreateShiftTypeDTO, - createShiftType, - deleteShiftType, - updateShiftType, -} from '@libs/api/shiftType'; +import * as shiftTypeApi from '@libs/api/shiftType'; import useAuth from '@hooks/auth/useAuth'; import useEditShift from '@hooks/shift/useEditShift'; @@ -16,16 +11,27 @@ const useEditWard = () => { } = useAuth(); const getWardQueryKey = ['ward', wardId]; + const getWardWaitingNursesQueryKey = ['waitingNurses', wardId]; const queryClient = useQueryClient(); const { queryKey: { shiftQueryKey }, } = useEditShift(); - const { data: ward } = useQuery(getWardQueryKey, () => getWard(wardId!), { + + const { data: ward } = useQuery(getWardQueryKey, () => wardApi.getWard(wardId!), { enabled: wardId !== null, }); + + const { data: watingNurses } = useQuery( + getWardWaitingNursesQueryKey, + () => wardApi.getWatingNurses(wardId!), + { + enabled: wardId !== null, + } + ); + const { mutate: updateWardMutate } = useMutation( - ({ wardId, editWardDTO }: { wardId: number; editWardDTO: EditWardDTO }) => - editWard(wardId, editWardDTO), + ({ wardId, editWardDTO }: { wardId: number; editWardDTO: wardApi.EditWardDTO }) => + wardApi.editWard(wardId, editWardDTO), { onSuccess: () => { queryClient.invalidateQueries(getWardQueryKey); @@ -37,8 +43,13 @@ const useEditWard = () => { ); const { mutate: createShiftTypeMutate } = useMutation( - ({ wardId, createShiftTypeDTO }: { wardId: number; createShiftTypeDTO: CreateShiftTypeDTO }) => - createShiftType(wardId, createShiftTypeDTO), + ({ + wardId, + createShiftTypeDTO, + }: { + wardId: number; + createShiftTypeDTO: shiftTypeApi.CreateShiftTypeDTO; + }) => shiftTypeApi.createShiftType(wardId, createShiftTypeDTO), { onSuccess: () => { queryClient.invalidateQueries(getWardQueryKey); @@ -54,8 +65,8 @@ const useEditWard = () => { }: { wardId: number; shiftTypeId: number; - createShiftTypeDTO: CreateShiftTypeDTO; - }) => updateShiftType(wardId, shiftTypeId, createShiftTypeDTO), + createShiftTypeDTO: shiftTypeApi.CreateShiftTypeDTO; + }) => shiftTypeApi.updateShiftType(wardId, shiftTypeId, createShiftTypeDTO), { onSuccess: () => { queryClient.invalidateQueries(getWardQueryKey); @@ -63,9 +74,10 @@ const useEditWard = () => { }, } ); + const { mutate: deleteShiftTypeMutate } = useMutation( ({ wardId, shiftTypeId }: { wardId: number; shiftTypeId: number }) => - deleteShiftType(wardId, shiftTypeId), + shiftTypeApi.deleteShiftType(wardId, shiftTypeId), { onSuccess: () => { queryClient.invalidateQueries(getWardQueryKey); @@ -73,15 +85,65 @@ const useEditWard = () => { } ); - const editWardSetting = (editWardDTO: EditWardDTO) => { + const { mutate: approveWatingNursesMutate } = useMutation( + ({ + wardId, + waitingNurseId, + shiftTeamId, + }: { + wardId: number; + waitingNurseId: number; + shiftTeamId: number; + }) => wardApi.approveWatingNurses(wardId, waitingNurseId, shiftTeamId), + { + onSuccess: () => { + queryClient.invalidateQueries(getWardQueryKey); + queryClient.invalidateQueries(getWardWaitingNursesQueryKey); + }, + } + ); + + const { mutate: connectWatingNursesMutate } = useMutation( + ({ + wardId, + waitingNurseId, + targetNurseId, + }: { + wardId: number; + waitingNurseId: number; + targetNurseId: number; + }) => wardApi.connectWatingNurses(wardId, waitingNurseId, targetNurseId), + { + onSuccess: () => { + queryClient.invalidateQueries(getWardQueryKey); + queryClient.invalidateQueries(getWardWaitingNursesQueryKey); + }, + } + ); + + const { mutate: cancelWaitingMutate } = useMutation( + ({ wardId, nurseId }: { wardId: number; nurseId: number }) => + wardApi.deleteWatingNurses(wardId, nurseId), + { + onSuccess: () => { + queryClient.invalidateQueries(getWardQueryKey); + queryClient.invalidateQueries(getWardWaitingNursesQueryKey); + }, + } + ); + + const editWardSetting = (editWardDTO: wardApi.EditWardDTO) => { if (wardId) updateWardMutate({ wardId, editWardDTO }); }; - const addShiftType = (createShiftTypeDTO: CreateShiftTypeDTO) => { + const addShiftType = (createShiftTypeDTO: shiftTypeApi.CreateShiftTypeDTO) => { if (wardId) createShiftTypeMutate({ wardId, createShiftTypeDTO }); }; - const editShiftType = (shiftTypeId: number, createShiftTypeDTO: CreateShiftTypeDTO) => { + const editShiftType = ( + shiftTypeId: number, + createShiftTypeDTO: shiftTypeApi.CreateShiftTypeDTO + ) => { if (wardId) updateShiftTypeMutate({ wardId, shiftTypeId, createShiftTypeDTO }); }; @@ -89,15 +151,31 @@ const useEditWard = () => { if (wardId) deleteShiftTypeMutate({ wardId, shiftTypeId }); }; + const approveWatingNurses = (waitingNurseId: number, shiftTeamId: number) => { + wardId && approveWatingNursesMutate({ wardId, waitingNurseId, shiftTeamId }); + }; + + const connectWatingNurses = (waitingNurseId: number, targetNurseId: number) => { + wardId && connectWatingNursesMutate({ wardId, waitingNurseId, targetNurseId }); + }; + + const cancelWaiting = (nurseId: number) => { + wardId && cancelWaitingMutate({ wardId, nurseId }); + }; + return { state: { ward, + watingNurses, }, actions: { editWardSetting, removeShiftType, editShiftType, addShiftType, + approveWatingNurses, + connectWatingNurses, + cancelWaiting, }, }; }; diff --git a/src/libs/api/auth.ts b/src/libs/api/auth.ts index 8e77818e..10cbc268 100644 --- a/src/libs/api/auth.ts +++ b/src/libs/api/auth.ts @@ -1,7 +1,7 @@ import axiosInstance from './client'; const getAccountMe = async () => (await axiosInstance.get('/accounts/me')).data; -const demoStart = () => async () => +const demoStart = async () => ( await axiosInstance.post<{ wardResDto: Ward; accountResDto: Account; accessToken: string }>( '/demo/start' diff --git a/src/libs/api/ward.ts b/src/libs/api/ward.ts index c2b2ae7e..6bb49b4b 100644 --- a/src/libs/api/ward.ts +++ b/src/libs/api/ward.ts @@ -40,16 +40,25 @@ const getWardByCode = async (code: string) => (await axiosInstance.get(`/wards/search?code=${code}`)).data; const getWatingNurses = async (wardId: number) => - (await axiosInstance.get(`/wards/${wardId}/waiting-nurses`)).data; + (await axiosInstance.get<{ nurses: WaitingNurse[] }>(`/wards/${wardId}/waiting-nurses`)).data + .nurses; const addMeToWatingNurses = async (wardId: number) => (await axiosInstance.post(`/wards/${wardId}/waiting-nurses`)).data; -const connectWatingNurses = async (wardId: number, waitingNurseId: number) => - (await axiosInstance.post(`/wards/${wardId}/waiting-nurses/${waitingNurseId}/connect`)).data; +const connectWatingNurses = async (wardId: number, waitingNurseId: number, targetNurseId: number) => + ( + await axiosInstance.post( + `/wards/${wardId}/waiting-nurses/${waitingNurseId}/connect?targetNurseId=${targetNurseId}` + ) + ).data; -const approveWatingNurses = async (wardId: number, waitingNurseId: number) => - (await axiosInstance.post(`/wards/${wardId}/waiting-nurses/${waitingNurseId}/approve`)).data; +const approveWatingNurses = async (wardId: number, waitingNurseId: number, shiftTeamId: number) => + ( + await axiosInstance.post( + `/wards/${wardId}/waiting-nurses/${waitingNurseId}/approve?shiftTeamId=${shiftTeamId}` + ) + ).data; const deleteWatingNurses = async (wardId: number, nurseId: number) => (await axiosInstance.delete(`/wards/${wardId}/waiting-nurses?nurseId=${nurseId}`)).data; diff --git a/src/pages/LandingPage/index.tsx b/src/pages/LandingPage/index.tsx index 1548998f..af676843 100644 --- a/src/pages/LandingPage/index.tsx +++ b/src/pages/LandingPage/index.tsx @@ -134,21 +134,47 @@ function LandingPage() {
- Web + App

- 근무표를 더 쉽고 빠르게 작성할 수 있도록 도와드립니다. + 일정 관리의 모든 여정이 더 편리해지는 경험을 제공합니다.

- App + Web

- 일정 관리의 모든 여정이 더 편리해지는 경험을 제공합니다. + 근무표를 더 쉽고 빠르게 작성할 수 있도록 도와드립니다.

+
+ App +
+

+ 근무 일정 관리 (일반 간호사 용) +

+
+
+ + + Google Play + + + + App Store + +
+
Web
@@ -156,7 +182,7 @@ function LandingPage() { 근무표 작성 (수간호사 용)

-
+
{accountMe?.status === 'DEMO' ? (
- 데모 테스트 해보기 + 근무표 작성 체험하기
)}
)}
-
-
- App -
-

- 근무 일정 관리 (일반 간호사 용) -

-
-
- {/*메인 2*/} + {/*메인 4*/}
-
+
-
- Web +
+ App
-

- 근무표 만들기 -

+

-

- 복잡한 근무표 작성을
간편하게 자동으로! +

+ 근무관리부터 +
+ 개인 일정까지 한번에

-

- 직접 편집한 제약 조건들에 딱 맞는 +

+ 매월 근무 등록하고
- 근무표를 작성해드릴게요. + 개인 일정을 유형별로 관리해보세요.

- {/*메인 3*/} -
-
+ {/*메인 5*/} +
+
-
- Web +
+ App

- 근무표 만들기 + 소셜 (친구 · 모임)

- 더 꼼꼼하게, + 동료의 근무 일정을
- 하지만 더 편리하게 + 한눈에

- 근무표 작성을 돕기 위한 + 동료와 친구를 맺어
- 여러 보조 기능들이 마련되어 있습니다. + 일정을 편하게 조율해보세요.

- {/*메인 4*/} + {/*메인 2*/}
-
+
-
- App +
+ Web
-

+

+ 근무표 만들기 +

-

- 근무관리부터 -
- 개인 일정까지 한번에 +

+ 복잡한 근무표 작성을
간편하게 자동으로!

-

- 매월 근무 등록하고 +

+ 직접 편집한 제약 조건들에 딱 맞는
- 개인 일정을 유형별로 관리해보세요. + 근무표를 작성해드릴게요.

- {/*메인 5*/} -
-
+ {/*메인 3*/} +
+
-
- App +
+ Web

- 소셜 (친구 · 모임) + 근무표 만들기

- 동료의 근무 일정을 + 더 꼼꼼하게,
- 한눈에 + 하지만 더 편리하게

- 동료와 친구를 맺어 + 근무표 작성을 돕기 위한
- 일정을 편하게 조율해보세요. + 여러 보조 기능들이 마련되어 있습니다.

@@ -334,6 +334,30 @@ function LandingPage() {

듀팅 다운로드

+
+
+ 모바일 앱 +
+ +
+
웹 @@ -351,14 +375,20 @@ function LandingPage() { {!accountMe && (
demoTry()} + onClick={() => { + demoTry(); + sendEvent(events.landingPage.demoStart); + }} > - 데모 테스트 해보기 + 근무표 작성 체험하기
)}
navigate(ROUTE.MAKE)} + onClick={() => { + navigate(ROUTE.MAKE); + sendEvent(events.landingPage.makeDuty); + }} > 근무표 만들기 @@ -367,30 +397,6 @@ function LandingPage() { )}
- -
-
- 모바일 앱 -
- -
diff --git a/src/pages/LoginPage/index.tsx b/src/pages/LoginPage/index.tsx index b96c0d53..537b4630 100644 --- a/src/pages/LoginPage/index.tsx +++ b/src/pages/LoginPage/index.tsx @@ -5,8 +5,8 @@ import { useNavigate } from 'react-router'; const LoginPage = () => { const navigate = useNavigate(); return ( -
-
+
+
navigate(ROUTE.ROOT)}> @@ -23,12 +23,7 @@ const LoginPage = () => {
카카오로 3초 만에 시작하기
-
-
- 아직 듀팅을 시작하지 않으셨나요? -
-
회원가입
-
+
); diff --git a/src/pages/MemberPage/components/ConnectionManage.tsx b/src/pages/MemberPage/components/ConnectionManage.tsx new file mode 100644 index 00000000..f03c8718 --- /dev/null +++ b/src/pages/MemberPage/components/ConnectionManage.tsx @@ -0,0 +1,402 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { + CancelIcon, + CheckedIcon, + MoreIcon, + PersonIcon, + SuccessCircleIcon, + UncheckedIcon2, + UnlinkedIcon, +} from '@assets/svg'; +import useEditShiftTeam from '@hooks/ward/useEditShiftTeam'; +import useEditWard from '@hooks/ward/useEditWard'; +import { groupBy } from 'lodash-es'; +import { useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { twMerge } from 'tailwind-merge'; +import { match } from 'ts-pattern'; + +interface ConnectionManageProps { + open: boolean; + setOpen: (open: boolean) => void; +} + +function ConnectionManage({ open, setOpen }: ConnectionManageProps) { + const { + state: { watingNurses }, + actions: { cancelWaiting, approveWatingNurses, connectWatingNurses }, + } = useEditWard(); + + const { + state: { shiftTeams }, + } = useEditShiftTeam(); + + const [step, setStep] = useState(0); + const [currentWaitingNurse, setCurrentWaitingNurse] = useState(null); + const [connectMode, setConnectMode] = useState<'link' | 'add'>('link'); + const [toLinkNurseId, setToLinkNurseId] = useState(null); + const [toAddShiftTeamId, setToAddShiftTeamId] = useState(null); + + const initialize = () => { + setStep(0); + setCurrentWaitingNurse(null); + setConnectMode('link'); + setToLinkNurseId(null); + setToAddShiftTeamId(null); + }; + + useEffect(() => { + if (open === false) { + initialize(); + } + }, [open]); + + return open + ? createPortal( +
setOpen(false)} + > + {match(step) + .with(0, () => ( +
e.stopPropagation()} + > +
+

연동 관리

+ setOpen(false)} + /> +
+
+

신청 내역

+
+ {watingNurses?.map((waitingNurse) => ( +
+ +

+ {waitingNurse.name} +

+
+ {waitingNurse.gender} +
+

+ {waitingNurse.phoneNum.slice(0, 3) + + '-' + + waitingNurse.phoneNum.slice(3, 7) + + '-' + + waitingNurse.phoneNum.slice(7, 11)} +

+
+ + +
+
+ ))} +
+
+
+ )) + .with(1, () => ( +
e.stopPropagation()} + > +
+

+ 간호사 계정을 어떻게 생성할까요? +

+
+ + +
+
+
+

연동할 간호사

+
+ +

+ {currentWaitingNurse?.name} +

+
+ {currentWaitingNurse?.gender} +
+

+ {currentWaitingNurse?.phoneNum.slice(0, 3) + + '-' + + currentWaitingNurse?.phoneNum.slice(3, 7) + + '-' + + currentWaitingNurse?.phoneNum.slice(7, 11)} +

+
+
+
+ + +
+

+ *기존 간호사와 연동 시, 미연동 상태인 간호사 목록에서 일치하는 계정을 연결시킬 수 + 있어요. +

+
+ )) + .with(2, () => ( +
e.stopPropagation()} + > +
+

+ {connectMode === 'link' + ? '연동할 간호사를 선택해 주세요.' + : '팀을 선택해 주세요.'} +

+
+ + +
+
+

+ {connectMode === 'link' + ? '미연동 상태인 간호사 목록 중에 일치하는 계정을 선택해주세요.' + : '팀을 선택해주시면 해당 팀에 계정이 추가됩니다.'} +

+
+ {shiftTeams?.map((shiftTeam) => ( +
+ {connectMode === 'add' ? ( + toAddShiftTeamId === shiftTeam.shiftTeamId ? ( + + ) : ( + setToAddShiftTeamId(shiftTeam.shiftTeamId)} + /> + ) + ) : null} +
+
+

+ {shiftTeam.name} +

+ +
+ +

+ {shiftTeam.nurses.length} +

+
+
+ +
+ {shiftTeam.nurses.length === 0 && ( +
+

+ 아직 간호사가 없습니다! +

+
+ )} + {Object.entries(groupBy(shiftTeam.nurses, 'divisionNum')) + .sort((a, b) => parseInt(a[0]) - parseInt(b[0])) + .map(([, divisionNurses], divisionIndex) => ( +
+ {divisionNurses.map((nurse, index) => ( +
x.nurseId === nurse.nurseId) === + shiftTeam.nurses.length - 1 + ? 'rounded-b-[.9375rem]' + : 'border-b-[.0313rem] border-b-sub-4.5' + }`} + onClick={() => { + !nurse.isConnected && setToLinkNurseId(nurse.nurseId); + }} + > +
+ {nurse.name} + {!nurse.isConnected && ( +
+ )} +
+ {!nurse.isConnected && ( +
+
+ 연동 되지 않은 가상의 프로필입니다. + +
+ )} +
+ ))} +
+ ))} +
+ ))} +
+
+ )) + .with(3, () => ( +
e.stopPropagation()} + > + +

+ 간호사 계정이 연동되었습니다. +

+

+ 연동된 간호사의 계정은{' '} + setOpen(false)} + > + 간호사 관리 탭 + + 에서 확인하실 수 있어요! +

+ +
+ + +
+
+ )) + .otherwise(() => null)} +
, + document.getElementById('modal-root')! + ) + : null; +} + +export default ConnectionManage; diff --git a/src/pages/MemberPage/components/ShiftTeamList.tsx b/src/pages/MemberPage/components/ShiftTeamList.tsx index 4825bcfd..19b87ce1 100644 --- a/src/pages/MemberPage/components/ShiftTeamList.tsx +++ b/src/pages/MemberPage/components/ShiftTeamList.tsx @@ -355,7 +355,7 @@ function ShiftTeamList() { borderBottom: '.625rem solid none', }} /> - 연동 되지 않은 가상의 프로필입니다. + 연동 되지 않은 가상의 간호사입니다.
{index !== divisionNurses.length - 1 ? ( diff --git a/src/pages/MemberPage/components/WardInfo.tsx b/src/pages/MemberPage/components/WardInfo.tsx index f0335908..28c9fdb9 100644 --- a/src/pages/MemberPage/components/WardInfo.tsx +++ b/src/pages/MemberPage/components/WardInfo.tsx @@ -1,9 +1,17 @@ +import { LinkedIcon } from '@assets/svg'; import useEditShiftTeam from '@hooks/ward/useEditShiftTeam'; +import useEditWard from '@hooks/ward/useEditWard'; +import { useState } from 'react'; +import ConnectionManage from './ConnectionManage'; function WardInfo() { const { state: { ward, shiftTeams }, } = useEditShiftTeam(); + const { + state: { watingNurses }, + } = useEditWard(); + const [open, setOpen] = useState(false); return (
@@ -13,7 +21,7 @@ function WardInfo() {

병원

{ward?.hospitalName}

-
+

병동

{ward?.name}

@@ -29,6 +37,41 @@ function WardInfo() {

+
+

연동 상태

+
+
+

연동됨

+

+ {shiftTeams?.flatMap((x) => x.nurses).filter((x) => x.isConnected).length} +

+

+
+
+
+

미연동

+

+ {shiftTeams?.flatMap((x) => x.nurses).filter((x) => !x.isConnected).length} +

+

+
+
+
+
+

연동 관리

+
setOpen(true)} + > + + {watingNurses?.length ? ( +
+

{watingNurses?.length}

+
+ ) : null} +
+
+
); } diff --git a/src/pages/RegisterPage/EnterWard.tsx b/src/pages/RegisterPage/EnterWard.tsx index 2cbd6550..a2ab172f 100644 --- a/src/pages/RegisterPage/EnterWard.tsx +++ b/src/pages/RegisterPage/EnterWard.tsx @@ -23,14 +23,25 @@ function EnterWard() { const navigate = useNavigate(); const clickAwayRef = useOnclickOutside(() => setFocusedIndex(-1)); - const handleKeyDown = (e: KeyboardEvent) => { + const handleKeyDown = async (e: KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && (e.key === 'v' || e.key === 'V')) { + e.preventDefault(); + const code = (await navigator.clipboard.readText()) + .trim() + .toUpperCase() + .replace(/[^0-9a-zA-Z]/g, '') + .slice(0, 6); + setCodeList(code.split('')); + return; + } match(e.key) .with('ArrowRight', 'ArrowDown', () => setFocusedIndex(Math.min(5, focusedIndex + 1))) .with('ArrowLeft', 'ArrowUp', () => setFocusedIndex(Math.max(0, focusedIndex - 1))) .with('Backspace', () => { setCodeList(codeList.map((code, index) => (index === focusedIndex ? null : code))); + setFocusedIndex(Math.max(0, focusedIndex - 1)); }) - .with(Pattern.string.regex(/[0-9a-zA-Z]/), () => { + .with(Pattern.string.regex(/[0-9a-zA-Z]/).maxLength(1), () => { setCodeList( codeList.map((code, index) => (index === focusedIndex ? e.key.toUpperCase() : code)) ); diff --git a/src/types/nurse.ts b/src/types/nurse.ts index b9a23485..a83ead7f 100644 --- a/src/types/nurse.ts +++ b/src/types/nurse.ts @@ -46,3 +46,13 @@ type Nurse = { /** 구분 내 인덱스 */ priority: number; }; + +type WaitingNurse = { + waitingNurseId: number; + nurseId: number; + name: string; + gender: string; + phoneNum: string; + employmentDate: boolean; + profileImgBase64: string; +};