Skip to content

Commit

Permalink
Feat: 튜토리얼 기능 개발 (#79)
Browse files Browse the repository at this point in the history
* Feat: 오버레이 가이드 기능 개발

* HOTFIX: FaultLayer z-index 수정

* Feat: 튜토리얼 개발
  • Loading branch information
sjsjsj1246 authored Nov 13, 2023
1 parent f98897a commit 5127981
Show file tree
Hide file tree
Showing 36 changed files with 1,012 additions and 74 deletions.
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<div id="edit-modal-root"></div>
<div id="info-modal-root"></div>
<div id="nurse-modal-root"></div>
<div id="tutorial" class="ignore-onclickoutside"></div>
<script type="module" src="/src/main.tsx"></script>
<script>
(function (a_, i_, r_, _b, _r, _i, _d, _g, _e) {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "dutying-web",
"private": true,
"version": "0.7.1",
"version": "0.7.2",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
2 changes: 1 addition & 1 deletion src/assets/svg/CancelIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { SVGProps } from 'react';
const SvgCancelIcon = (props: SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 30 30" {...props}>
<path
stroke="#ABABB4"
stroke={props.stroke || '#ABABB4'}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
Expand Down
24 changes: 24 additions & 0 deletions src/assets/svg/HelpIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { SVGProps } from 'react';
const SvgHelpIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
fill="none"
viewBox="0 0 45 45"
{...props}
>
<path fill="url(#help_icon_svg__a)" d="M0 0h45v45H0z" />
<defs>
<pattern id="help_icon_svg__a" width={1} height={1} patternContentUnits="objectBoundingBox">
<use xlinkHref="#help_icon_svg__b" transform="scale(.01)" />
</pattern>
<image
xlinkHref="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKh0lEQVR4nO1da4wlRRUuwHcCCPrL+FoUE43iG3zviq8gEoOy62NZlYTV6OKTICri+EIxiET4IROBAcbh4jl959Hn3Osmqw6J4EpcAy7hseKCsGEhoKOLsrvjDlxzbvfivdVVd27f211d3XO/pJOdme2q0326zqk69Z1TSo0wwggjjDBCRTExMf8MAH4NIq9DpPMRaQqAtiLSdgDeicgLiLwYXwvR72g7Iv9e/m90D6+TNqStop+ndJifn38KAL8ekc8F4C2ItA+RW1lcAHQAkbcB8IVBwO9uNptPL/p5vcTY2Nih8oIQ6VpE/ndWCujjkr6uQQzfJTKolQ6A8IXytQLQLodKaFlGzy5E+qHIpFYagoCOBeArEem//b0s3gnAIQBdhEgbg4DfUa/TKwHmVk1N8VHj49ueKpf8W34X/S1cLf9X7gEgiv1NP8pZRKQrAOZeqqqOIOBjELmGyEvLvJT7EWkCgDfU683nZ9W/tAVAnwDgq/sYlUvRBGJulaoa5OsFoC8i0n/sL4D2iA8RX9JqtQ5xIZdMHgDopwD89x6K2YtI367MBAAxfC8A/6WHIm6Pvlp4ZlEySt8ig8jSQ84d8rGoMk9f5ctC5MctfuHWWBGHKU/QarUOCYLwFES+2eL4n4hGFDxNlQm12q9ebHsoRH4AIPyI8hyI/DGR1aKYrUHAL1JlAGLjJAD+p+EhDgDQT2ZnZw9XJcHkZPMIRL4kXlDqilkIAnqf8hmxHU5MZcWH1Ovh61RJAVHk4G6DX5FnPV35CET+gslfAFB9Zmbm2arkmJ2dPTyeshv8Cp+jfAIifd9ioj6rKgbExiazCaPvKR8AQGcZlLEfkT+kKoogCE8B4McMz312oYIB0HrdTIlDBwjfpioORD4BkR8xmK9PFSRQ4ySDA19AnDtOrRAgzh0X78d0OXrnsy+JiCLSP/QQw0oYGeaRkggJLchaTLmKS0W7cd2BOAA6Va1QIDY+YHD0NztZ0cvizuDMKjebymhyc1HOnTbeI45L6/j6XDstERAZdScvu5G5dCbhZz1qKz9LeEE5Rq3GL48XotMAfBsA7Y7D5A/GPxNA+BWAxqtcygWw5UhE+qvm5O/KxXQB8Lf0hZ/rcAi0Ryj/usfehSkQeJPMCF3JWK+Hb0xuwNE3Mu1EIpuGhdDFyhHCMHyWhL3TKMJwzbkK4Rhk3ZvpzqMhhvOAq6jt1FT4XDFDQyrjSRNbq828wJHpelAbqZOZNF6rNV6mD0FX+xmTk80jAPhPWSijQym3uviY4ihGl4mv1ZovyaBhYYd0PdQ25QiIPL78S6YdADwj/CphpkQEiWWVcrWL3UdEukWT9edZrMj18IiToCFAuNowxe68AKD5CptjBeDNPe59XKileT8DIq/V+l0cymQKiU3T8O2uGH7Q44UC0Jf6+UIB+IIebfwy72eQdwXAd2p9/2CIxrq5S7IjqJwR6cgyOujSftuJlEKzFrP1qAtqj0R/tb7vG+ijjri2XS/iX66oOgB8jkUZD6d1yPEi0mK6clpFJ6bs/Gj3x9A4MXVDMfG582VcoRwB7F/1hYO0h0h3WBS8MXvpjf1PaP1OpGpAlvo6C134tMoREGmH5as+YbD2eNqikPOVA8iI0PrdI1HzvhtApLdrwt/vit75/8gArY+Y8dwQu4tI9wr5Tg0AWZRZFDyYg00J8Rk6xysIGm/tu4GYcdgp+FWqxABL/Ev4xq5k0F0AAH8zxc18g3bzBlVSQGR+FywKOc2dHN2zLQD6bRri8f7Om7NMCXANRP6gxVwtSZzMlRzRIrvLj+zrK+cxTrDsHB07VUkx1rbdevjiyet3ruURP9gtQx+EEAkcagoJVUmBSBvtaxBeV4A8rMmwdpCNqHz3hXNl4NMey3rmtiKSPHU+AgCft+xNiPwLbVV5pioZxqIY0rzFkee3z70MAPjTmh+5to+bZMuz8wHC1apkAKCv20wVAP+sKLmCgN+Z2o/pu3OS0apKhHq9+aYe2b33FJmfIqQL7eP4cx830d+0kEk5MoXUwa1Tvsdiqg4AhG8uVr65VfoHsuxNOkUUYPPRqiTAdm0Tq6n6WtHyybpHk+uRZW+Ki7h0PEg5EhwBGmf2mOLe4ENyqey/aKN2fyUVEgR0rL1GCj183XWzz1MeYCCF6En009PTz1Hex6rIuBqP8zVOVp5A3uUAJqt7ee+7U8dkZLrzukT5ly6e2qlvL8u0dyoi0SVSyw5OKX0rhzHotPfGsiwMAehsm6nyMQ17oIWhHjpxte+cUbCuFSuElIcA4M+kDp3ENQo7H/DHylMg0l0Wc/U55SEGDS6uK8PXJkDkh8zO3F36wTAjuq8dy3qdXl2WDSq0bM0KSUN5CH0G21dCUVSWtXsL1wWFP0uFBGkYHY4gywfto9nX9ywwSXJwQyFNC1sluKKDiCYAhGdoCvlNipu7dw1TM+0cAZFPj7lbndd5PhZM1rlhqWhAUgBAGyG7XBLlqoaYuL5bG8lvSRUfShKE/V0g+g7ZMh6KSho1IhWfiyFbVw2SsaWZqyuzSEfYI9T6XCSuMEJDOgIirSlVwk6VkFnCjkBqoReV0rYcEJuv1ddLHR/OV5UHMKW0SZrdwA3KglDfQfSlUhwAn+wj1acXC1TYMEMX+xdnrjV6iw9TYPBcIXHSqZZjT5cP3bAku+u1oADoo6pggOcKkRQOTa6lzE5c0Ok1ssgpogpQWRQS88MeSr33kbKAQFcZO4ntZ9ZBxRSCyJdp8jyWeck/iRFpCjkgFZ9VQQgCer+PCgGg45O1YXIg6UWcokR27N1Fma6pKT5KWCVxPZSOiy6Xl1KETFL+KXmSD92RG7dNVu+GCguQS2clBAAFmhV5QogNuXYqe+xJM9HYpFY4MCo5qJvPHzk6Z7A7hyS2mV4sGIsrPZ5YGvwhdUR3mBW8Ybdur6972XnnoxjIeu4KKR+ElNMelRqfM5Qa50Up1qkKPBYoUYx/JYwUtBTjR6RPFixYY5Mh2ro/CBofVhVFEB0cttfgxL+sfAAAf9dyoMtZqpqzqaWkMug7yicA8OfNRx7xjCziVMkxGR0Qdn0pjjzqpOWYM2Cl/HbjDaqkAKDjLWfpigP/uPIZ8exLP1skDj/LoYxbjlQlAUQFkS81mSiZ9hc2mxqwANlWc/BPCubTeh82uZbZXNqgV6fuMFM3+UqvHfjoVcnU8u3o1bGxsUPjVfcfex296mwFnh9JzJzHEQ/9O4WZUSTFKGxTdcIzDDV2u6K2uQcK3R/fbUtdPhh6YYi+0PxHTcQIaVNmx5O8qe7NpUod390Jie8I6dhytmyrwzTsli1PGTlDszQSvk1GQlsGnWubWEdFMmTXv7cQ4oQUpjdQjFoWc3FvXJ304qjEEa2RamxBwMdI6Y+IiyzX5qPld9FxdrQmzumTexrJam7WS2Qal3bUSkMcNb6gnxMNMP/rPpGlzPUlM7bnjRMjUrK5Clw+V7uvq2Q0+cLI9A4AcFh8XPa5ALxFUr+yUkDsu7ZJYk+0He1/HRfv0GyTK9r7DmuF+RI75BvlpJwonNGOCojdX5R/y++iv0kBhPbEQO45Tdqo5ExphBFGGGGEEVSE/wG5vwQDa3lcDQAAAABJRU5ErkJggg=="
id="help_icon_svg__b"
width={100}
height={100}
/>
</defs>
</svg>
);
export default SvgHelpIcon;
1 change: 1 addition & 0 deletions src/assets/svg/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,4 @@ export { default as XIcon } from './XIcon';
export { default as SuccessCircleIcon } from './SuccessCircleIcon';
export { default as RandomIcon } from './RandomIcon';
export { default as CameraIcon } from './CameraIcon';
export { default as HelpIcon } from './HelpIcon';
2 changes: 1 addition & 1 deletion src/Loading.tsx → src/components/Loading.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import useLoading from '@hooks/ui/useLoading';
import Lottie from 'react-lottie';
import loadingLottie from './assets/animation/loading.json';
import loadingLottie from '../assets/animation/loading.json';

const Loading = () => {
const { loading } = useLoading();
Expand Down
45 changes: 41 additions & 4 deletions src/components/NavigationBar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { FoldIcon } from '@assets/svg';
import { FoldIcon, HelpIcon } from '@assets/svg';
import { useState, useEffect } from 'react';
import NavigationBarItemGroups from './NavigationBarItemGroup';
import { events, sendEvent } from 'analytics';
import useAuth from '@hooks/auth/useAuth';
import useTutorial from '@hooks/ui/useTutorial';
import ROUTE from '@libs/constant/path';

interface Props {
isFold: boolean;
Expand All @@ -14,6 +16,26 @@ const NavigationBar = ({ isFold, setIsFold }: Props) => {
state: { accountMe },
} = useAuth();
const [canHover, setCanHover] = useState(true);
const {
actions: { setMakeTutorial, setMemberTutorial, setRequestTutorial },
} = useTutorial();

const handleResetTutorial = () => {
console.log(window.location.pathname);
switch (window.location.pathname) {
case ROUTE.MAKE:
setMakeTutorial(true);
break;
case ROUTE.MEMBER:
setMemberTutorial(true);
break;
case ROUTE.REQUEST:
setRequestTutorial(true);
break;
default:
break;
}
};

useEffect(() => {
setCanHover(false);
Expand All @@ -23,7 +45,7 @@ const NavigationBar = ({ isFold, setIsFold }: Props) => {
}, [isFold]);

return (
<div className="group fixed left-0 z-[1000]">
<div className="group fixed left-0 z-[997]">
<div
className={`z-10 ${canHover && 'group-hover:translate-x-0'} ${
!isFold ? 'sticky' : 'fixed'
Expand All @@ -46,8 +68,23 @@ const NavigationBar = ({ isFold, setIsFold }: Props) => {
/>
</div>
<NavigationBarItemGroups />
<div className="absolute bottom-[1.875rem] mt-[3.125rem] flex cursor-pointer flex-col items-center">
<div className="mt-2 ">{accountMe?.name}</div>
<div className="mb-[3.125rem] mt-auto flex flex-col items-center gap-8 pt-8">
<div className="flex cursor-pointer flex-col items-center" onClick={handleResetTutorial}>
<HelpIcon className="h-[3.125rem] w-[3.125rem] rounded-full" />
<div className="mt-2 text-[1rem] text-sub-3">가이드</div>
</div>
<div className="flex cursor-pointer flex-col items-center">
<img
src={
accountMe?.profileImgBase64
? 'data:image/png;base64,' + accountMe?.profileImgBase64
: ''
}
alt=""
className="h-[3.125rem] w-[3.125rem] rounded-full"
/>
<div className="mt-2 text-[1rem] text-sub-3">{accountMe?.name}</div>
</div>
</div>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions src/components/TimeInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ function TimeInput({ initTime, onTimeChange, className, ...props }: Props) {
if (value == time) {
return;
}
if (value === '') onTimeChange?.(value);
if (isValid(value)) {
if (value.length === 2 && lastValue.current.length === 3) value = value.slice(0, 1);
if (value.length === 2 && lastValue.current.length === 1) value += ':';
Expand Down
87 changes: 87 additions & 0 deletions src/components/Tutorial/MakeTutorial.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import useTutorial from '@hooks/ui/useTutorial';
import { createPortal } from 'react-dom';
import { StepConfig, StepsConfig, TutorialOverlay } from './TutorialOverlay';
import useEditShiftStore from '@hooks/shift/useEditShift/store';
import { useEffect } from 'react';
import useEditShift from '@hooks/shift/useEditShift';

const MakeTutorial = () => {
const {
state: { showMakeTutorial },
actions: { setMakeTutorial },
} = useTutorial();
const {
actions: { toggleEditMode },
} = useEditShift();
const { setState } = useEditShiftStore();

const config: StepsConfig = {
steps: new Map<number, StepConfig>(),
infoBoxHeight: 150,
infoBoxMargin: 20,
scrollLock: true,
};

config.steps.set(1, {
highlightIds: ['toolbar'],
title: '근무표 만들기',
info: '이곳은 툴바입니다. 근무표 작성에 도움이 되는 여러 설정을 변경할 수 있어요',
infoBoxAlignment: 'left',
});

config.steps.set(2, {
highlightIds: ['calendar'],
title: '근무표 만들기',
info: '이곳은 근무표입니다. 툴바의 "수정하기" 버튼을 누른 후 셀을 클릭하여 근무를 작성할 수 있어요',
infoBoxAlignment: 'left',
});

config.steps.set(3, {
highlightIds: ['count_by_day'],
title: '근무표 만들기',
info: '이곳은 날짜별 근무 수입니다. 날짜별로 각 근무가 얼마나 채워져있는지 볼 수 있어요',
infoBoxAlignment: 'left',
});

config.steps.set(4, {
highlightIds: ['count_by_nurse'],
title: '근무표 만들기',
info: '이곳은 간호사별 근무 수입니다. 간호사별로 근무가 얼마나 채워져있는지 볼 수 있어요. WO는 주말 OFF를 의미합니다.',
infoBoxAlignment: 'center',
});

config.steps.set(5, {
highlightIds: ['editButton'],
title: '근무표 만들기',
info: '수정하기 버튼을 눌러서 근무표를 만들 수 있어요',
infoBoxAlignment: 'center',
onNextStep: toggleEditMode,
});

config.steps.set(6, {
highlightIds: ['cell_sample'],
title: '근무표 만들기',
info: '셀을 클릭하시고 D E N O를 입력하시면 근무를 작성하실 수 있어요! \n더 자세한 가이드는 메뉴얼 문서를 참고해주세요!',
infoBoxAlignment: 'center',
onPrevStep: toggleEditMode,
ctaText: '메뉴얼 보러가기',
ctaUrl: 'https://gom3.notion.site/68d3ad01e68d4d6a8b4cb8c2409353a3?pvs=4',
});

useEffect(() => {
if (showMakeTutorial) {
setState('readonly', true);
}
}, [showMakeTutorial]);

return (
showMakeTutorial &&
createPortal(
<TutorialOverlay config={config} closeCallback={() => setMakeTutorial(false)} />,
document.getElementById('tutorial')!
)
);
};

export default MakeTutorial;
80 changes: 80 additions & 0 deletions src/components/Tutorial/MemberTutorial.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import useTutorial from '@hooks/ui/useTutorial';
import { createPortal } from 'react-dom';
import { StepConfig, StepsConfig, TutorialOverlay } from './TutorialOverlay';
import { useEffect } from 'react';
import useEditShiftTeam from '@hooks/ward/useEditShiftTeam';

const MemberTutorial = () => {
const {
state: { showMemberTutorial },
actions: { setMemberTutorial },
} = useTutorial();
const {
state: { shiftTeams },
actions: { selectNurse },
} = useEditShiftTeam();

const config: StepsConfig = {
steps: new Map<number, StepConfig>(),
infoBoxHeight: 150,
infoBoxMargin: 20,
scrollLock: true,
};

config.steps.set(1, {
highlightIds: ['ward_info'],
title: '간호사 관리하기',
info: '이곳에서 병동의 정보를 확인할 수 있어요',
infoBoxAlignment: 'left',
});

config.steps.set(2, {
highlightIds: ['shift_team_list'],
title: '간호사 관리하기',
info: '이곳에서 근무팀에 속한 간호사의 정보를 확인할 수 있어요.',
infoBoxAlignment: 'left',
onNextStep: () => {
shiftTeams && selectNurse(shiftTeams[0].nurses[0].nurseId);
},
});

config.steps.set(3, {
highlightIds: ['nurse_sample'],
title: '간호사 관리하기',
info: '간호사 이름을 눌러 편집해보세요!',
infoBoxAlignment: 'center',
onPrevStep: () => {
selectNurse(null);
},
});

config.steps.set(4, {
highlightIds: ['nurse_edit_drawer'],
title: '간호사 관리하기',
info: '편집을 완료하고 하단에 저장을 눌러주세요! \n더 자세한 가이드는 메뉴얼 문서를 참고해주세요!',
ctaText: '메뉴얼 보러가기',
ctaUrl: 'https://gom3.notion.site/befb4602f83241ed896a1700eb592b35?pvs=4',

infoBoxAlignment: 'right',
onNextStep: () => {
selectNurse(null);
},
});

useEffect(() => {
if (showMemberTutorial) {
selectNurse(null);
}
}, [showMemberTutorial]);

return (
showMemberTutorial &&
createPortal(
<TutorialOverlay config={config} closeCallback={() => setMemberTutorial(false)} />,
document.getElementById('tutorial')!
)
);
};

export default MemberTutorial;
80 changes: 80 additions & 0 deletions src/components/Tutorial/RequestTutorial.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import useTutorial from '@hooks/ui/useTutorial';
import { createPortal } from 'react-dom';
import { StepConfig, StepsConfig, TutorialOverlay } from './TutorialOverlay';
import { useEffect } from 'react';
import { useRequestShiftStore } from '@hooks/shift/useRequestShift/store';
import useRequestShift from '@hooks/shift/useRequestShift';

const RequestTutorial = () => {
const {
state: { showRequestTutorial },
actions: { setRequestTutorial },
} = useTutorial();
const {
actions: { toggleEditMode },
} = useRequestShift();
const { setState } = useRequestShiftStore();

const config: StepsConfig = {
steps: new Map<number, StepConfig>(),
infoBoxHeight: 150,
infoBoxMargin: 20,
scrollLock: true,
};

config.steps.set(1, {
highlightIds: ['toolbar'],
title: '신청근무 작성하기',
info: '이곳은 툴바입니다. 신청근무 작성에 도움이 되는 여러 설정을 변경할 수 있어요',
infoBoxAlignment: 'left',
});

config.steps.set(2, {
highlightIds: ['calendar'],
title: '신청근무 작성하기',
info: '이곳은 신청 근무표입니다. 툴바의 "수정하기" 버튼을 누른 후 셀을 클릭하여 신청 근무를 작성할 수 있어요',
infoBoxAlignment: 'left',
});

config.steps.set(3, {
highlightIds: ['nurse_request_list'],
title: '신청근무 작성하기',
info: '이곳에서는 연동된 간호사의 신청 근무를 볼 수 있어요',
infoBoxAlignment: 'right',
});

config.steps.set(4, {
highlightIds: ['editButton'],
title: '신청근무 작성하기',
info: '수정하기 버튼을 눌러서 신청 근무표를 만들 수 있어요',
infoBoxAlignment: 'right',
onNextStep: toggleEditMode,
});

config.steps.set(5, {
highlightIds: ['cell_sample'],
title: '신청근무 작성하기',
info: '셀을 클릭하시고 D E N O를 입력하시면 신청근무를 작성하실 수 있어요! \n더 자세한 가이드는 메뉴얼 문서를 참고해주세요!',
infoBoxAlignment: 'center',
onPrevStep: toggleEditMode,
ctaText: '메뉴얼 보러가기',
ctaUrl: 'https://gom3.notion.site/befb4602f83241ed896a1700eb592b35?pvs=4',
});

useEffect(() => {
if (showRequestTutorial) {
setState('readonly', true);
}
}, [showRequestTutorial]);

return (
showRequestTutorial &&
createPortal(
<TutorialOverlay config={config} closeCallback={() => setRequestTutorial(false)} />,
document.getElementById('tutorial')!
)
);
};

export default RequestTutorial;
Loading

0 comments on commit 5127981

Please sign in to comment.