Skip to content

Commit

Permalink
merge: [FE] 프론트엔드 배포 (#41)
Browse files Browse the repository at this point in the history
merge: [FE] 프론트엔드 배포 (#41)
  • Loading branch information
d0422 authored Nov 14, 2023
2 parents 362d4b2 + 01b277a commit f6bbbfd
Show file tree
Hide file tree
Showing 12 changed files with 281 additions and 36 deletions.
3 changes: 3 additions & 0 deletions frontEnd/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ dist-ssr
*.njsproj
*.sln
*.sw?

.env.production
.env.development
22 changes: 21 additions & 1 deletion frontEnd/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion frontEnd/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.18.0",
"socket.io-client": "^4.7.2"
"socket.io-client": "^4.7.2",
"uuid": "^9.0.1"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.1.4",
Expand All @@ -23,6 +24,7 @@
"@types/node": "^20.8.10",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.7",
"@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"@vitejs/plugin-react": "^4.0.3",
Expand Down
Binary file added frontEnd/public/main.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions frontEnd/src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ReactNode } from 'react';

type ButtonProps = {
children: ReactNode;
onClick: () => void;
fontSize: string;
};

export default function Button({ children, onClick, fontSize }: ButtonProps) {
return (
<button
type="button"
className="px-[1.6vw] py-[14px] bg-mainColor text-white rounded-[15px] font-Pretendard"
onClick={onClick}
style={{ fontSize }}
>
{children}
</button>
);
}
14 changes: 14 additions & 0 deletions frontEnd/src/hooks/useInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useState } from 'react';

export default function useInput(initialValue: string) {
const [inputValue, setInputValue] = useState(initialValue);

const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const {
target: { value },
} = event;
setInputValue(value);
};

return { inputValue, onChange };
}
116 changes: 116 additions & 0 deletions frontEnd/src/hooks/useRoom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { useState, useEffect, useRef } from 'react';
import { io } from 'socket.io-client/debug';
import { SOCKET_EMIT_EVENT, SOCKET_RECEIVE_EVENT } from '@/constants/socketEvents';

const useRoom = (roomId: string) => {
const [streamList, setStreamList] = useState<{ id: string; stream: MediaStream }[]>([]);
const videoRef = useRef<HTMLVideoElement>(null);
const localStream = useRef<MediaStream | null>(null);

const RTCConnections: Record<string, RTCPeerConnection> = {};

const socket = io(`${import.meta.env.VITE_SOCKET_URL}`);

useEffect(() => {
navigator.mediaDevices
.getUserMedia({
video: true,
})
.then((video) => {
if (videoRef.current) videoRef.current.srcObject = video;

localStream.current = video;

socket.connect();
socket.emit(SOCKET_EMIT_EVENT.JOIN_ROOM, {
room: roomId,
});
});
}, []);

const createPeerConnection = (socketId: string): RTCPeerConnection => {
const RTCConnection = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.1.google.com:19302' }],
});

if (localStream.current) {
localStream.current.getTracks().forEach((track) => {
RTCConnection.addTrack(track, localStream.current as MediaStream);
});
}

RTCConnection.addEventListener('icecandidate', (e) => {
if (e.candidate != null)
socket.emit('candidate', {
candidate: e.candidate,
candidateSendId: socket.id,
candidateReceiveId: socketId,
});
});

RTCConnection.addEventListener('track', (e) => {
setStreamList((prev) => [...prev, { id: socketId, stream: e.streams[0] }]);
});

return RTCConnection;
};

socket.on(SOCKET_RECEIVE_EVENT.ALL_USERS, async (data: { users: Array<{ id: string }> }) => {
data.users.forEach((user) => {
RTCConnections[user.id] = createPeerConnection(user.id);
});

Object.entries(RTCConnections).forEach(async ([key, value]) => {
const offer = await value.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true,
});

await value.setLocalDescription(new RTCSessionDescription(offer));

socket.emit(SOCKET_EMIT_EVENT.OFFER, {
sdp: offer,
offerSendId: socket.id,
offerReceiveId: key,
});
});
});

socket.on(SOCKET_RECEIVE_EVENT.OFFER, async (data: { sdp: RTCSessionDescription; offerSendId: string }) => {
RTCConnections[data.offerSendId] = createPeerConnection(data.offerSendId);

await RTCConnections[data.offerSendId].setRemoteDescription(new RTCSessionDescription(data.sdp));

const answer = await RTCConnections[data.offerSendId].createAnswer({
offerToReceiveAudio: true,
offerToReceiveVideo: true,
});

await RTCConnections[data.offerSendId].setLocalDescription(new RTCSessionDescription(answer));

socket.emit(SOCKET_EMIT_EVENT.ANSWER, {
sdp: answer,
answerSendId: socket.id,
answerReceiveId: data.offerSendId,
});
});

socket.on(SOCKET_RECEIVE_EVENT.ANSWER, (data: { sdp: RTCSessionDescription; answerSendId: string }) => {
RTCConnections[data.answerSendId].setRemoteDescription(new RTCSessionDescription(data.sdp));
});

socket.on(SOCKET_RECEIVE_EVENT.CANDIDATE, (data: { candidate: RTCIceCandidateInit; candidateSendId: string }) => {
RTCConnections[data.candidateSendId].addIceCandidate(new RTCIceCandidate(data.candidate));
});

socket.on(SOCKET_RECEIVE_EVENT.USER_EXIT, (data: { id: string }) => {
RTCConnections[data.id].close();
delete RTCConnections[data.id];

setStreamList((prev) => prev.filter((stream) => stream.id !== data.id));
});

return { videoRef, socket, RTCConnections, streamList };
};

export default useRoom;
58 changes: 57 additions & 1 deletion frontEnd/src/pages/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,59 @@
import { v4 as uuidv4 } from 'uuid';
import { useNavigate } from 'react-router-dom';
import Button from '@/components/Button';
import useInput from '@/hooks/useInput';

export default function Home() {
return <div>Home</div>;
const navigate = useNavigate();
const { inputValue, onChange } = useInput('');

const handleMakeRoomClick = () => {
const roomId = uuidv4();
navigate(`/${roomId}`);
};

const handleJoinRoomClick = () => {
if (inputValue) navigate(`/${inputValue}`);
};

const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') handleJoinRoomClick();
};

return (
<div className="h-screen w-screen bg-backgroundColor flex justify-center items-center py-[10vw] ">
<div className="flex flex-col basis-2/4 pc:gap-[130px] mobile:gap-[50px] justify-center items-center ">
<div className="flex flex-col justify-center">
<div className="text-[8.8vw] font-bold font-Pretendard ">AlgoITNi</div>
<div className="text-[1.4vw] font-Pretendard">AlgoITNi를 통해 동료, 친구와 함께 알고리즘을 학습해봐요!</div>
</div>
<div className="flex items-center justify-center w-[37vw] gap-3">
<Button onClick={handleMakeRoomClick} fontSize="1.5vw">
방생성
</Button>
<input
placeholder=""
className="rounded-2xl font-Pretendard px-[1.6vw] py-[14px] text-[1.5vw] w-3/5"
value={inputValue}
onChange={onChange}
onKeyDown={handleKeyDown}
/>

<button
type="button"
className="text-[1.5vw] font-bold text-mainColor font-Pretendard"
onClick={handleJoinRoomClick}
style={{
opacity: inputValue ? '1' : '0.4',
}}
>
참여
</button>
</div>
</div>
<div className="basis-2/4 max-w-[600px] max-h-[600px] mobile:hidden">
<img src="/main.png" alt="main" />
</div>
</div>
);
}
30 changes: 3 additions & 27 deletions frontEnd/src/pages/Room.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import { useEffect, useRef } from 'react';
import { useParams } from 'react-router-dom';
import { createSocket } from '@/services/Socket';
import { StreamObject, streamModel } from '@/stores/StreamModel';
import { SOCKET_EMIT_EVENT } from '@/constants/socketEvents';
import useRoom from '@/hooks/useRoom';

function StreamVideo({ stream }: { stream: MediaStream }) {
const videoRef = useRef<HTMLVideoElement>(null);
Expand All @@ -17,30 +15,8 @@ function StreamVideo({ stream }: { stream: MediaStream }) {
}

export default function Room() {
const videoRef = useRef<HTMLVideoElement>(null);
const [streamList, setStreamList] = useState<StreamObject[]>([]);
const { roomId } = useParams();

useEffect(() => {
navigator.mediaDevices
.getUserMedia({
video: true,
})
.then((video) => {
if (videoRef.current) videoRef.current.srcObject = video;

streamModel.subscribe(() => {
setStreamList(() => streamModel.getStream());
});

const socket = createSocket(video);

socket.connect();
socket.emit(SOCKET_EMIT_EVENT.JOIN_ROOM, {
room: roomId,
});
});
}, []);
const { videoRef, streamList } = useRoom(roomId as string);

return (
<div>
Expand Down
32 changes: 26 additions & 6 deletions frontEnd/src/pages/tests/Home.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
import * as Home from '@pages/Home';
import { fireEvent, render, screen } from '@testing-library/react';
import Home from '@pages/Home';

describe('Home 호출 테스트', () => {
it('Home이 제대로 실행된다.', () => {
jest.spyOn(Home, 'default');
Home.default();
expect(Home.default).toHaveBeenCalled();
jest.mock('uuid', () => {
return { v4: jest.fn(() => 'test') };
});

const mockNavigate = jest.fn();

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockNavigate,
}));

describe('Home 테스트', () => {
it('방생성 클릭시, uuid를 url로 갖는 새로운 방을 생성하고, 해당 방 url로 이동한다.', () => {
render(<Home />);
fireEvent.click(screen.getByText('방생성'));
expect(mockNavigate).toHaveBeenCalledWith('/test');
});

it('참가 클릭시, input입력창에 입력한 url을 가진 방으로 이동한다.', () => {
render(<Home />);
const input = screen.getByPlaceholderText('');
fireEvent.change(input, { target: { value: 'inputValue' } });
fireEvent.click(screen.getByText('참여'));
expect(mockNavigate).toHaveBeenCalledWith('/inputValue');
});
});
5 changes: 5 additions & 0 deletions frontEnd/src/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html,
body {
width: 100%;
height: 100%;
}
13 changes: 13 additions & 0 deletions frontEnd/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
colors: {
mainColor: '#37485D',
backgroundColor: '#F0F2F5',
black: '#0C151C',
white: '#FFFFFF',
},
fontFamily: {
Pretendard: 'Pretendard-Regular',
},
screens: {
mobile: { min: '360px', max: '767px' },
pc: { min: '768px' },
},
},
plugins: [],
};

0 comments on commit f6bbbfd

Please sign in to comment.