Skip to content

Commit

Permalink
Feat/#409-K: 선택된 회의록 id URL에 반영 (#411)
Browse files Browse the repository at this point in the history
* feat: 선택된 회의록 id url params에 추가

* feat: URL 통한 회의록 선택 기능 추가

* refactor: navigation, 회의록 상태 및 이벤트 로직 위치 이동

* chore: recoil 설치

* feat: Mom 로딩 상태 및 DefaultMom 렌더링 로직의 workspace.moms에 recoil 적용

* docs: 회의록 선택에 따른 URL 변경 및 SELECT 이벤트 흐름 주석 추가

* refactor: 회의록 선택 로직 MomList로 이동

* del: 중복 로직 제거

* refactor: 회의록 없는 경우의 분기 Mom에서 Workspace 컴포넌트로 이동

* refactor: Sidebar의 workspace recoil atom으로 변경

* feat: 선택 회의록 정보 MomList 스타일에 반영

* feat: MOM_EVENT.SELECT emit 위치 Workspace로 이동

- 초기 접속한 URL에 momId가 지정되어 있을 경우도 커버하기 위해 URL 변경에 소켓 이벤트가 발생하도록 변경
- MomList는 navigate 역할만 수행

* feat: 워크스페이스 이동 로직 개선

- 이동할 워크스페이스 정보가 로드되기 전 렌더링 로직을 위해 workspace atom null로 리셋
- 이전과 값이 동일한 경우에는 변동이 없도록 수정

* refactor: MOM_EVENT.SELECT 이벤트 관련 코드 Workspace로 이동

* docs: 정규식 주석 추가

* refactor: momId 추출에 정규식 대신 react-router useParams 사용

* chore: react-query 설치

Refactor: 상태 관리에 react-query 적용 #412

* feat: workspace 상태에 useQuery 사용

recoil 관련 코드 제거 및 react-query로 대체
  • Loading branch information
se030 authored Jun 12, 2023
1 parent 4fbf651 commit c3ff94e
Show file tree
Hide file tree
Showing 12 changed files with 300 additions and 154 deletions.
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"dependencies": {
"@react-icons/all-files": "^4.1.0",
"@tanstack/react-query": "^4.29.12",
"@types/react-helmet": "^6.1.6",
"axios": "^1.1.3",
"classnames": "^2.3.2",
Expand Down
5 changes: 3 additions & 2 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ function App() {
const [user, setUser] = useState<User | null>(null);
const [isLoaded, setIsLoaded] = useState<boolean>(false);

const location = useLocation();
const { pathname } = useLocation();
const navigate = useNavigate();

const autoLogin = async () => {
Expand All @@ -25,7 +25,8 @@ function App() {
setIsLoaded(true);
setUser(user);

if (user && !/^\/workspace(\/\d+)?$/.test(location.pathname)) {
const validPathPattern = /^\/workspace(\/\d+(\/.+)?)?$/; // /workspace(/숫자(/아무거나)) 와 처음부터 끝까지 일치하는 패턴
if (user && !validPathPattern.test(pathname)) {
navigate('/workspace');
}
};
Expand Down
4 changes: 3 additions & 1 deletion client/src/apis/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ export const postWorkspaceJoin = async ({

export const getWorkspaceInfo = async ({
id,
}: GetInfoParams): Promise<WorkspaceInfo> => {
}: Partial<GetInfoParams>): Promise<WorkspaceInfo | null> => {
if (!id) return null;

const res = await http.get(`/workspace/${id}`);

if (res.status !== OK) throw new Error();
Expand Down
26 changes: 1 addition & 25 deletions client/src/components/Mom/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { useCRDT } from 'src/hooks/useCRDT';
import useDebounce from 'src/hooks/useDebounce';
import { v4 as uuid } from 'uuid';

import DefaultMom from './DefaultMom';
import ee from './EventEmitter';
import style from './style.module.scss';

Expand Down Expand Up @@ -37,8 +36,6 @@ function Mom() {
const focusIndex = useRef<number>();

const [blocks, setBlocks] = useState<string[]>([]);
const [isLoaded, setIsLoaded] = useState<boolean>(false);
const [isMomsEmpty, setIsMomsEmpty] = useState(false);

const onTitleUpdate: React.FormEventHandler<HTMLHeadingElement> = useDebounce(
() => {
Expand Down Expand Up @@ -230,35 +227,14 @@ function Mom() {
BLOCK_EVENT.UPDATE_TYPE,
].forEach((event) => socket.off(event));
};
}, [selectedMom]);

useEffect(() => {
ee.on(MOM_EVENT.LOADED, (momsLength: number) => {
setIsLoaded(true);
setIsMomsEmpty(momsLength === 0);
});

ee.emit(MOM_EVENT.REQUEST_LOADED);

return () => {
ee.off(MOM_EVENT.LOADED);
};
}, [socket]);
}, [selectedMom, socket]);

const registerRef =
(index: number) => (ref: React.RefObject<HTMLElement>) => {
blockRefs.current[index] = ref;
setBlockFocus(index);
};

if (!isLoaded) {
return <div className={style['mom-container']}></div>;
}

if (isMomsEmpty && !selectedMom) {
return <DefaultMom />;
}

return (
<div className={style['mom-container']}>
{selectedMom && (
Expand Down
54 changes: 24 additions & 30 deletions client/src/components/Sidebar/MomList.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,49 @@
import { RiFileAddLine } from '@react-icons/all-files/ri/RiFileAddLine';
import * as MomMessage from '@wabinar/api-types/mom';
import { MOM_EVENT } from '@wabinar/constants/socket-message';
import { memo, useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import ee from 'src/components/Mom/EventEmitter';
import useSelectedMomContext from 'src/hooks/context/useSelectedMomContext';
import useSocketContext from 'src/hooks/context/useSocketContext';
import { TMom } from 'src/types/mom';

import ee from '../Mom/EventEmitter';
import style from './style.module.scss';

interface MomListProps {
moms: TMom[];
selectedMom: TMom | null;
setSelectedMom: React.Dispatch<React.SetStateAction<TMom | null>>;
}

function MomList({ moms, selectedMom, setSelectedMom }: MomListProps) {
function MomList({ moms }: MomListProps) {
const { selectedMom, setSelectedMom } = useSelectedMomContext();

const { momSocket: socket } = useSocketContext();
const [momList, setMomList] = useState<TMom[]>(moms);

const navigate = useNavigate();

const onCreateMom = () => {
socket.emit(MOM_EVENT.CREATE);
};

const onSelect = (id: string) => {
const message: MomMessage.Select = { id };
socket.emit(MOM_EVENT.SELECT, message);
setSelectedMom(null);
navigate(id);
};

useEffect(() => {
if (moms.length) {
const message: MomMessage.Select = { id: moms[0]._id };
socket.emit(MOM_EVENT.SELECT, message);
}

setMomList(moms);
}, [moms]);

ee.on(MOM_EVENT.REQUEST_LOADED, () => {
ee.emit(MOM_EVENT.LOADED, moms ? moms.length : 0);
});

useEffect(() => {
socket.on(MOM_EVENT.CREATE, ({ mom }: MomMessage.Created) =>
setMomList((prev) => [...prev, mom]),
);

socket.on(MOM_EVENT.SELECT, ({ mom }: MomMessage.Selected) => {
setSelectedMom(mom);
});

return () => {
socket.off(MOM_EVENT.CREATE);
socket.off(MOM_EVENT.SELECT);
ee.off(MOM_EVENT.REQUEST_LOADED);
};
}, [moms]);
}, [socket]);

useEffect(() => {
ee.on(MOM_EVENT.UPDATE_TITLE, (title) => {
Expand All @@ -71,7 +62,7 @@ function MomList({ moms, selectedMom, setSelectedMom }: MomListProps) {
return () => {
ee.off(MOM_EVENT.UPDATE_TITLE);
};
}, []);
}, [selectedMom]);

return (
<div className={style['mom-list-container']}>
Expand All @@ -89,7 +80,14 @@ function MomList({ moms, selectedMom, setSelectedMom }: MomListProps) {
</div>
<ul className={style['mom-list']}>
{momList.map(({ _id: id, title }) => (
<li key={id} onClick={() => onSelect(id)} role="button">
<li
key={id}
className={
selectedMom?._id === id ? style['mom-list-item__selected'] : ''
}
onClick={() => onSelect(id)}
role="button"
>
{title}
</li>
))}
Expand All @@ -98,8 +96,4 @@ function MomList({ moms, selectedMom, setSelectedMom }: MomListProps) {
);
}

const isMemoized = (prevProps: MomListProps, nextProps: MomListProps) => {
return prevProps.moms === nextProps.moms;
};

export default memo(MomList, isMemoized);
export default MomList;
31 changes: 17 additions & 14 deletions client/src/components/Sidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
import useSelectedMomContext from 'src/hooks/context/useSelectedMomContext';
import { WorkspaceInfo } from 'src/types/workspace';
import { useQuery } from '@tanstack/react-query';
import { useParams } from 'react-router-dom';
import { getWorkspaceInfo } from 'src/apis/workspace';

import Header from './Header';
import MeetingButton from './MeetingButton';
import MemberList from './MemberList';
import MomList from './MomList';
import style from './style.module.scss';

interface SidebarProps {
workspace: WorkspaceInfo;
}
function Sidebar() {
const { id } = useParams();
const { data: workspace } = useQuery({
queryKey: ['workspace', id],
queryFn: () => getWorkspaceInfo({ id }),
});

if (!workspace) {
return <div className={style['sidebar-container']}></div>;
}

function Sidebar({ workspace }: SidebarProps) {
const { selectedMom, setSelectedMom } = useSelectedMomContext();
const { name, members, moms } = workspace;

return (
<div className={style['sidebar-container']}>
<Header name={workspace.name} />
<Header name={name} />
<div className={style['sidebar-container__scrollable']}>
<MemberList members={workspace.members} />
<MomList
moms={workspace.moms}
selectedMom={selectedMom}
setSelectedMom={setSelectedMom}
/>
<MemberList members={members} />
<MomList moms={moms} />
</div>
<MeetingButton />
</div>
Expand Down
5 changes: 5 additions & 0 deletions client/src/components/Sidebar/style.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@
}
}

.mom-list-item__selected {
color: $black;
font-weight: 500;
}

.meeting-button {
position: absolute;
bottom: 15px;
Expand Down
63 changes: 47 additions & 16 deletions client/src/components/Workspace/index.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,72 @@
import { WORKSPACE_EVENT } from '@wabinar/constants/socket-message';
import { useQuery } from '@tanstack/react-query';
import * as MomMessage from '@wabinar/api-types/mom';
import { MOM_EVENT, WORKSPACE_EVENT } from '@wabinar/constants/socket-message';
import Mom from 'components/Mom';
import DefaultMom from 'components/Mom/DefaultMom';
import Sidebar from 'components/Sidebar';
import { useEffect, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';
import { getWorkspaceInfo } from 'src/apis/workspace';
import MeetingMediaBar from 'src/components/MeetingMediaBar';
import MeetingContext from 'src/contexts/meeting';
import { SelectedMomContext } from 'src/contexts/selected-mom';
import { SocketContext } from 'src/contexts/socket';
import useSocket from 'src/hooks/useSocket';
import { TMom } from 'src/types/mom';
import { WorkspaceInfo } from 'src/types/workspace';

function Workspace() {
const { id } = useParams();
const [isOnGoing, setIsOnGoing] = useState(false);
const navigate = useNavigate();

const params = useParams();
const momId = params['*'];

const { data: workspace } = useQuery({
queryKey: ['workspace', id],
queryFn: () => getWorkspaceInfo({ id }),
});

const [workspace, setWorkspace] = useState<WorkspaceInfo | null>(null);
const [selectedMom, setSelectedMom] = useState<TMom | null>(null);
const [isOnGoing, setIsOnGoing] = useState(false);

const momSocket = useSocket(`/workspace-mom/${id}`);
const workspaceSocket = useSocket(`/workspace/${id}`);

const loadWorkspaceInfo = async () => {
if (id) {
const workspaceInfo = await getWorkspaceInfo({ id });
useEffect(() => {
setIsOnGoing(false);
}, [id]);

useEffect(() => {
if (!workspace) return;
const { moms } = workspace;

if (!momId && moms.length) {
navigate(moms[0]._id);
}

setWorkspace(workspaceInfo);
if (!moms.length) {
setSelectedMom(null);
}
}, [workspace, momId]);

if (!workspaceInfo.moms[0]) setSelectedMom(null);
useEffect(() => {
if (momId && momSocket) {
const message: MomMessage.Select = { id: momId };
momSocket.emit(MOM_EVENT.SELECT, message);
}
};
}, [momId, momSocket]);

useEffect(() => {
loadWorkspaceInfo();
setIsOnGoing(false);
}, [id]);
if (!momSocket) return;

momSocket.on(MOM_EVENT.SELECT, ({ mom }: MomMessage.Selected) => {
setSelectedMom(mom);
});

return () => {
momSocket.off(MOM_EVENT.SELECT);
};
}, [momSocket]);

useEffect(() => {
if (!workspaceSocket) {
Expand Down Expand Up @@ -72,8 +103,8 @@ function Workspace() {
<MeetingContext.Provider value={memoizedOnGoingValue}>
{workspace && (
<SelectedMomContext.Provider value={{ selectedMom, setSelectedMom }}>
<Sidebar workspace={workspace} />
<Mom />
<Sidebar />
{workspace.moms.length ? <Mom /> : <DefaultMom />}
</SelectedMomContext.Provider>
)}
{isOnGoing && <MeetingMediaBar />}
Expand Down
9 changes: 8 additions & 1 deletion client/src/components/WorkspaceThumbnailList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useNavigate } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { useNavigate, useParams } from 'react-router-dom';
import { Workspace } from 'src/types/workspace';

import style from './style.module.scss';
Expand All @@ -9,9 +10,15 @@ interface WorkspaceThumbnailListProps {
}

function WorkspaceThumbnailList({ workspaces }: WorkspaceThumbnailListProps) {
const queryClient = useQueryClient();

const { id: currentId } = useParams();
const navigate = useNavigate();

const onClick = (targetId: number) => {
if (Number(currentId) === targetId) return;

queryClient.invalidateQueries({ queryKey: ['workspace', currentId] });
navigate(`/workspace/${targetId}`);
};

Expand Down
Loading

0 comments on commit c3ff94e

Please sign in to comment.