Skip to content

Commit

Permalink
랜덤 매칭 기능 구현 (#247)
Browse files Browse the repository at this point in the history
* 💄 #194 - style 수정

* ✨ #194 - audio 태그 추가 및 사용자 오디오 데이터 조회 및 주입 로직 추가

* ➕ #194 - socket.io-client 패키지 설치

* ✨ #194 - 사용자  인증 정보를 조회하는 훅 개발 및 적용

* 🏷️ #194 - next auth의 session 타입 수정

- accessToken -> expires

* 🩹 #194 - 사용자 정보 조회 hook에서 useEffect 의존성 배열 수정

- session -> session.status

* ✨ #194 - 매칭 소켓 연결 로직 추가

* ✨ #194 - 매칭 성공 이벤트 추가 및 playing 페이지로 redirection

* 🚚 #194 - matching socket provider 이름변경

- matchingSocketProvider -> matchingRTCProvider

* ✨ #194 - webRTC을 활용한 랜덤 매칭 기능 개발

* ♻️ #194 - 랜덤 매칭 기능 리펙토링

- socket, peerConnection 로직 분리

* 🐛 #194 - 오디오 권한을 처음 설정하는 유저의 경우 오디오 스트림이 전달되지 않는 버그 해결

* ♻️ #194 - event 초기화 로직 추가

* 🚚 #194 - matching 관련 클래스명 변경

- matchingRTC -> randomMatching

* ✨ #194 - 매칭이 실패되는 경우 예외처리

* 🧑‍💻 #194 - startSignaling 개발자 예외처리 로직 추가

* ♻️ #194 - 페이지 리다이렉션 주소 상수 변경

* 🧑‍💻 #194 - 매칭 관련 상수 정리

* 🔥 #194 - 불필요한 console.log 제거

* 🩹 #194 - matching playing 매칭 유저 정보 적용

* ♻️ #194 - 소켓 IP, stun 서버 ip 목록 환경 변수 처리

* ♻️ #194 - inline 함수 분리 및 PATH_PATH 상수 처리

* ✏️ #194 - 매칭 관련 소켓 이벤트 명 수정

* 🚚 #194 - 인증 관련 hook 이름 변경

* ♻️ #194 - 인증 관련 hook session의 모든 케이스 처리 구문 추가

* 🏷️ #194 - matching 관련 정보 타입 key 값 수정

* 🏷️ #194 - next auth session 타입 확장 적용

* ✏️ #194 - userInformation 변수명 변경

- userInformation -> user

* ♻️ #194 - Microphone 관련 변수명 및 반환 조건 변경

- audioPermission -> MicrophonePermission

* 🐛 #194 - 매칭 성공 시 사용자의 소켓, 유저 아이디가 넘어오지 않는 경우 예외 처리

* ♻️ #194 - 소켓으로 받은 데이터 구조 변경

* ✏️ #194 - random matching 관련 메소드명 변경

- startMatching -> joinQueue
- startSignaling -> signaling

* ✏️ #194 - 매칭 관련 변수명 통일

- RandomMatching -> Matching

* ♻️ #194 - matching context 구조 개선

* 🐛 #194 - matching/playing 페이지 접근 시 에러가 발생하는 경우 예외처리

* 🩹 #194 - 매칭 관련 컴포넌트에서 router 이동 시 메소드 변경

- push -> replace

* 🚸 #194 - 매칭 페이지에서 페이지 이동 시 매칭 종료 알림창 적용

* 🔥 #194 - useAuthentication 훅으로 대체로 인한 useUser 훅 제거

* 🚚 #194 -  매칭 대기열, 매칭 완료 페이지 이름(route path) 변경
  • Loading branch information
yws1502 authored Jul 10, 2024
1 parent b3cf202 commit 47968ff
Show file tree
Hide file tree
Showing 21 changed files with 517 additions and 73 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@
"react-dom": "18.2.0",
"react-hook-form": "^7.43.7",
"react-tooltip": "^5.11.1",
"socket.io-client": "^4.7.4",
"typescript": "4.9.4"
},
"devDependencies": {
"@next/eslint-plugin-next": "^13.1.1",
"@types/react-calendar-heatmap": "^1.6.3",
"@types/socket.io-client": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"eslint-config-prettier": "^8.5.0",
Expand Down
112 changes: 92 additions & 20 deletions src/components/matching/MatchingController.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,90 @@
import styled from '@emotion/styled';

import { useRouter } from 'next/router';
import { useEffect, useRef } from 'react';
import type { MatchingInformation } from 'types/matching';
import { MicrophoneOffIcon, EndCallIcon } from 'assets/icons';
import { AlertModal } from 'components/common';
import { PAGE_PATH } from 'constants/common';
import { colors } from 'constants/styles';
import { useMatching } from 'contexts/MatchingProvider';
import { useModal } from 'hooks/common';
import { ScreenReaderOnly } from 'styles';

const MatchingController = () => {
const router = useRouter();

const audioRef = useRef<HTMLAudioElement>(null);

const matching = useMatching();

const { isVisible, handleModal } = useModal();

const signaling = async () => {
const { query } = router;

const { current: audioElement } = audioRef;

if (audioElement === null) return;

try {
await matching.signaling(audioElement, {
role: query.r as MatchingInformation['role'],
socketId: query.ms as MatchingInformation['socketId'],
userId: query.mu as MatchingInformation['userId'],
});
} catch (error) {
handleModal.open();
}
};

useEffect(() => {
void signaling();

return () => {
matching.disconnect();
};
}, []);

const handleEndMatching = () => {
// FIXME: 매칭 설문 페이지로 이동할 예정입니다.
matching.disconnect();
void router.replace(PAGE_PATH.main);
};

const handleCloseAlert = () => {
handleModal.close();
void router.replace(PAGE_PATH.main);
};

return (
<Container>
<SubTitle>통화 제어</SubTitle>
<CircleButton type="button" backgroundColor={colors.bg_02}>
<Tooltip>마이크를 켜주세요!</Tooltip>
<MicrophoneOffIcon />
<span>마이크 off</span>
</CircleButton>
<CircleButton type="button" backgroundColor={colors.red}>
<EndCallIcon />
<span>통화 종료</span>
</CircleButton>
</Container>
<>
<Container>
<SubTitle>통화 제어</SubTitle>
<audio ref={audioRef} muted={false} autoPlay>
<track kind="captions" />
</audio>
<CircleButton type="button" backgroundColor={colors.bg_02}>
<Tooltip>마이크를 켜주세요!</Tooltip>
<MicrophoneOffIcon />
<span>마이크 off</span>
</CircleButton>
<CircleButton
type="button"
backgroundColor={colors.red}
onClick={handleEndMatching}
>
<EndCallIcon />
<span>통화 종료</span>
</CircleButton>
</Container>
{/* FIXME: 디자인이 없어 임시로 디자인한 모달입니다. 추후 변경 예정 */}
<AlertModal isVisible={isVisible} onClose={handleCloseAlert}>
<ModalContent>
<p>의도하지 않은 에러가 발생했습니다.</p>
<p>메인 페이지도 이동합니다.</p>
</ModalContent>
</AlertModal>
</>
);
};

Expand All @@ -37,24 +104,21 @@ const CircleButton = styled.button<{
backgroundColor: string;
}>`
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 60px;
height: 60px;
border-radius: 100%;
background-color: ${(props) => props.backgroundColor};
svg {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
span {
${({ theme }) => theme.fonts.body_07}
position: absolute;
bottom: -26px;
left: 50%;
transform: translateX(-50%);
width: 60px;
color: ${({ theme }) => theme.colors.gray_00};
${({ theme }) => theme.fonts.body_07}
}
`;

Expand All @@ -79,3 +143,11 @@ const Tooltip = styled.div`
border-top-color: ${({ theme }) => theme.colors.primary_00};
}
`;

const ModalContent = styled.div`
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 40px 32px 30px;
`;
18 changes: 15 additions & 3 deletions src/components/matching/MatchingUserInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
import styled from '@emotion/styled';
import Image from 'next/image';

import { useRouter } from 'next/router';
import { DEFAULT_PROFILE_IMAGES } from 'constants/profile';
import { useTimer } from 'hooks/common/useTimer';
import { useProfile } from 'hooks/services';
import { ScreenReaderOnly } from 'styles';

const MatchingUserInfo = () => {
const { minutes, seconds } = useTimer();

const router = useRouter();

const { query } = router;

const { profileData: matchingUserProfile } = useProfile(
query.mu as string, // matching username
);

return (
<Container>
<SubTitle>사용자 프로필</SubTitle>
{/* FIXME: useProfile의 isLoading으로 스켈레톤 UI로 대체 논의 */}
<Image
src="http://add.bucket.s3.amazonaws.com/default/dd_blue.PNG"
src={matchingUserProfile?.imgUrl ?? DEFAULT_PROFILE_IMAGES[0].url}
alt="프로필 사진"
width={80}
height={80}
placeholder="blur"
blurDataURL="http://add.bucket.s3.amazonaws.com/default/dd_blue.PNG"
blurDataURL={DEFAULT_PROFILE_IMAGES[0].url}
/>
<strong>username</strong>
<strong>{matchingUserProfile?.username}</strong>
<span>
{minutes}:{seconds}
</span>
Expand Down
3 changes: 2 additions & 1 deletion src/constants/common/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ export const PAGE_PATH = {

matching: {
index: '/matching',
loading: '/matching/loading',
queue: '/matching/queue',
matchUp: '/matching/match-up',
},

diary: {
Expand Down
2 changes: 2 additions & 0 deletions src/constants/matching/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './socketEvent';
export * from './message';
5 changes: 5 additions & 0 deletions src/constants/matching/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const EXCEPTION_MESSAGE = {
rejectMicrophone:
'매칭 서비스 이용을 위해 마이크 권한을 설정해주세요.\nChrome 우측 상단 더보기 > 설정 > 개인 정보 및 보안 > 사이트 설정 > 마이크에서 설정할 수 있습니다.\n\n메인 페이지로 이동합니다.',
failedMatching: '랜덤 매칭에 실패하였습니다.\n메인 페이지로 이동합니다.',
} as const;
14 changes: 14 additions & 0 deletions src/constants/matching/socketEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const MATCHING_SOCKET_EVENT = {
client: {
joinQueue: 'joinQueue',
offer: 'offer',
answer: 'answer',
ice: 'ice',
},
server: {
success: 'success',
offer: 'offer',
answer: 'answer',
ice: 'ice',
},
} as const;
1 change: 1 addition & 0 deletions src/constants/modal/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export const MODAL_MESSAGE = {
beforeLeave:
'등록하지 않고 나갈 시 작성한 일기가 모두 삭제됩니다. 나가시겠습니까?',
delete: '삭제하시겠습니까?',
leaveMatching: '페이지 이동 시 매칭이 종료될 수 있습니다. 이동하시겠습니까?',
};

export const MODAL_BUTTON = {
Expand Down
30 changes: 30 additions & 0 deletions src/contexts/MatchingProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { createContext, useContext, useRef } from 'react';
import type { ReactNode } from 'react';
import { Matching } from 'utils';

const MatchingContext = createContext<Matching | null>(null);

interface MatchingProviderProps {
children: ReactNode;
}

const MatchingProvider = ({ children }: MatchingProviderProps) => {
const { current: matching } = useRef<Matching>(new Matching());

return (
<MatchingContext.Provider value={matching}>
{children}
</MatchingContext.Provider>
);
};

export const useMatching = () => {
const context = useContext(MatchingContext);

if (context === null) {
throw new Error('MatchingContext must be used within a MatchingProvider');
}
return context;
};

export default MatchingProvider;
31 changes: 0 additions & 31 deletions src/hooks/common/useUser.ts

This file was deleted.

34 changes: 34 additions & 0 deletions src/hooks/services/common/useAuthentication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useRouter } from 'next/router';
import { useSession } from 'next-auth/react';
import { useEffect, useState } from 'react';
import type { Authentication } from 'types/authentication';
import { PAGE_PATH } from 'constants/common';

export const useAuthentication = () => {
const router = useRouter();

const session = useSession({
required: true,
onUnauthenticated: () => {
void router.replace(PAGE_PATH.account.login);
alert('인증이 필요한 페이지입니다.'); // FIXME: 문구 변경 예정입니다.
},
});

const [authentication, setAuthentication] = useState<Authentication>({
user: undefined,
status: 'loading',
});

useEffect(() => {
if (session.status === 'loading') return;

setAuthentication({
update: session.update,
user: session.data.user,
status: 'authenticated',
});
}, [session.status]);

return authentication;
};
5 changes: 4 additions & 1 deletion src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { SessionProvider } from 'next-auth/react';
import { useState } from 'react';
import type { AppProps } from 'next/app';
import { Layout } from 'components/layouts';
import MatchingProvider from 'contexts/MatchingProvider';
import { theme, GlobalStyle } from 'styles';

export default function App({ Component, pageProps }: AppProps) {
Expand All @@ -25,7 +26,9 @@ export default function App({ Component, pageProps }: AppProps) {
<ThemeProvider theme={theme}>
<Global styles={GlobalStyle} />
<Layout>
<Component {...pageProps} />
<MatchingProvider>
<Component {...pageProps} />
</MatchingProvider>
</Layout>
</ThemeProvider>
</SessionProvider>
Expand Down
8 changes: 5 additions & 3 deletions src/pages/matching/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import { ScreenReaderOnly } from 'styles';
const MatchingRule: NextPage = () => {
const router = useRouter();

const handleGoToMatchingQueue = () => {
void router.push(PAGE_PATH.matching.queue);
};

return (
<>
<Seo title="랜덤 매칭 규칙 | a daily diary" />
Expand Down Expand Up @@ -65,9 +69,7 @@ const MatchingRule: NextPage = () => {
type="button"
shape="round"
size="xl"
onClick={async () => {
await router.push(PAGE_PATH.matching.loading);
}}
onClick={handleGoToMatchingQueue}
text="랜덤매칭 시작"
/>
</Article>
Expand Down
Loading

0 comments on commit 47968ff

Please sign in to comment.