Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
41e4f15
[FE] setting: socket.io 추가 (#74)
zelkovaria Feb 25, 2025
77cfcf1
[FE] setting: mkcert 추가 (#74)
zelkovaria Feb 26, 2025
a6e89c7
[FE] fix: guildId가 존재하는 경우만 쿼리 실행 조건 추가 (#74)
zelkovaria Feb 28, 2025
cf149c6
[FE] feat: 채널에 따른 채팅 혹은 미디어창 렌더링 로직 추가 (#74)
zelkovaria Feb 28, 2025
c0bfdcd
[FE] fix: 채널 정보를 전역 값으로 대체 및 실시간 화면 갱신 (#74)
zelkovaria Feb 28, 2025
19e0302
[FE] feat: 참여자가 없는 비디오 페이지 기본 레이아웃 구현 (#74)
zelkovaria Feb 28, 2025
b8866f8
[FE] feat: theme color 추가 (#74)
zelkovaria Feb 28, 2025
4cf2273
feat: 빈 채널 레이아웃에 참여중인 채널명 출력 (#74)
zelkovaria Feb 28, 2025
2351bcd
[FE] feat: CategorySection에 VoiceChannelController 배치 (#74)
zelkovaria Feb 28, 2025
cdff6a3
[FE] feat: guild의 name을 전역상태 관리에 추가 (#74)
zelkovaria Feb 28, 2025
aa66dee
[FE] feat: 길드 선택시 저장되는 guild 정보를 수정 (#74)
zelkovaria Feb 28, 2025
969cd74
[FE] refactor: 폴더 경로 수정 (#74)
zelkovaria Feb 28, 2025
3924686
[FE] feat: 음성 채널 컨트롤러 컴포넌트 상단 UI 구현 (#74)
zelkovaria Feb 28, 2025
39321ec
[FE] feat: voice channel action 관련한 전역 상태 추가 (#74)
zelkovaria Feb 28, 2025
5b5749d
[FE] feat: voice channel controller actions UI 구현 (#74)
zelkovaria Feb 28, 2025
5a2c029
[FE] feat: voice type 채널 선택시 전역상태 업데이트 (#74)
zelkovaria Feb 28, 2025
f51baa1
[FE] fix: 채널 클릭마다 전역상태 값이 바뀌는 오류 수정 (#74)
zelkovaria Feb 28, 2025
17ad368
[FE] feat: 음성 채널 참여시에만 VoiceChannelController 컴포넌트 노출 (#74)
zelkovaria Feb 28, 2025
49762fe
[FE] feat: VoiceChannelController에 actions 관련한 버튼 컴포넌트 추가 (#74)
zelkovaria Feb 28, 2025
39dfab6
[FE] refactor: channelAction 상태 관리 함수에 매개변수 추가 (#74)
zelkovaria Mar 1, 2025
113437c
[FE] feat: Actions 아이콘들에 애니메이션 추가 (#74)
zelkovaria Mar 1, 2025
96ad675
[FE] feat: 아이콘 hover시 커서 추가 (#74)
zelkovaria Mar 1, 2025
8964370
[FE] feat: 컨트롤러 컴포넌트 연결 아이콘 클릭시 상태 변경 (#74)
zelkovaria Mar 3, 2025
0041a68
[FE] feat: Socket을 사용한 WebRTC 임시 구현 (#74)
zelkovaria Mar 5, 2025
3520a4e
[FE] feat: stomp 방식을 사용하여 테스트용 WebRTC 구현 (#74)
zelkovaria Mar 5, 2025
ee80e43
[FE] feat: createAnswer 로직 제거 (#74)
zelkovaria Mar 5, 2025
f6eec5f
[FE] feat; useStompWebRTC hook 추가 (#74)
zelkovaria Mar 6, 2025
69453b6
[FE] feat: 불필요한 offer 요청 삭제 (#74)
zelkovaria Mar 6, 2025
97c23cb
[FE] refactor: 중복 구독 방지 및 token 로직 개선 (#74)
zelkovaria Mar 6, 2025
6754f1f
[FE] feat: candidate 요청시 조건 및 대기 중인 후보 전송 시도 추가 (#74)
zelkovaria Mar 6, 2025
58439f3
[FE] feat: userId 조회를 위한 api 함수 구현 (#74)
zelkovaria Mar 8, 2025
50df3fe
[FE] refactor: userId 조회 경로 수정 (#74)
zelkovaria Mar 8, 2025
55eb1e3
[FE] feat: N:M WebRTC 임시 구현 (#74)
zelkovaria Mar 8, 2025
39f4f8f
[FE] feat: 로그인시 userId를 전역 상태에 저장 (#74)
zelkovaria Mar 8, 2025
c6011e3
[FE] feat: WebRTC 사용시 필요한 userId를 전역 상태 값으로 사용 (#74)
zelkovaria Mar 8, 2025
fae956e
[FE] feat: 나간 유저에 대한 정보를 subscribe 및 불필요한 로직 개선 (#74)
zelkovaria Mar 8, 2025
ca7859d
[FE] feat: Video 컴포넌트 구현 (#74)
zelkovaria Mar 9, 2025
92e56f1
[FE] feat: webRTCStore 구현 (#74)
zelkovaria Mar 9, 2025
57d59af
[FE] feat: controller 종료 아이콘 클릭시 stomp 연결 종료 구현 (#74)
zelkovaria Mar 9, 2025
d98b1bf
[FE] fix: 통화 종료시에도 VoiceChannelController가 사라지지 않던 오류 수정 (#74)
zelkovaria Mar 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"react-dom": "^18.3.1",
"react-icons": "^5.4.0",
"react-router-dom": "^7.1.5",
"socket.io-client": "^4.8.1",
"styled-components": "^6.1.14",
"vite-plugin-mkcert": "^1.17.6",
"websocket": "^1.0.35",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { BiHash } from 'react-icons/bi';
import { BsFillMicFill } from 'react-icons/bs';
import { TbPlus } from 'react-icons/tb';

import { useChannelActionStore } from '@/stores/channelAction';
import { GuildChannelInfo, useChannelInfoStore } from '@/stores/channelInfo';
import { useGuildInfoStore } from '@/stores/guildInfo';
import useModalStore from '@/stores/modalStore';
Expand All @@ -20,13 +21,18 @@ const GuildCategoriesList = ({ categories, channels }: CategoriesListProps) => {
const { openModal } = useModalStore();
const { guildId } = useGuildInfoStore();
const { selectedChannel, setSelectedChannel } = useChannelInfoStore();
const { isInVoiceChannel, setIsInVoiceChannel } = useChannelActionStore();

const handleOpenModal = (categoryId: string, guildId: string) => {
openModal('withFooter', <CreateChannelModal categoryId={categoryId} guildId={guildId} />);
};

const handleChannelClick = (channelInfo: GuildChannelInfo) => {
setSelectedChannel({ id: channelInfo.id, name: channelInfo.name, type: channelInfo.type });

if (channelInfo.type === 'VOICE') {
if (!isInVoiceChannel) setIsInVoiceChannel(true);
}
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const GuildCategory = () => {
const { data } = useQuery<GuildResultData>({
queryKey: ['guildInfo', guildId],
queryFn: () => getGuild(guildId),
enabled: !!guildId,
});

const dropdownItems: DropdownItem[] = [
Expand Down
9 changes: 7 additions & 2 deletions src/frontend/src/components/guild/GuildList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@ import * as S from './styles';

const GuildList = () => {
const { openModal } = useModalStore();
const { setGuildId } = useGuildInfoStore();
const { setGuildId, setGuildName } = useGuildInfoStore();

const { data } = useQuery<GuildResponse[]>({ queryKey: ['guildList'], queryFn: getGuilds });

const handleChangeModal = () => {
openModal('basic', <CreateGuildModalContent />);
};

const handleStoreGuildInfo = (guild: GuildResponse) => {
setGuildId(guild.guildId);
setGuildName(guild.name);
};

return (
<S.GuildList>
<S.DMButton onClick={() => setGuildId('')}>
Expand All @@ -29,7 +34,7 @@ const GuildList = () => {
key={guild.guildId}
data-tooltip={guild.name}
$imageUrl={guild.profileImageUrl}
onClick={() => setGuildId(guild.guildId)}
onClick={() => handleStoreGuildInfo(guild)}
/>
))}
<S.AddGuildButton onClick={handleChangeModal}>
Expand Down
41 changes: 41 additions & 0 deletions src/frontend/src/components/guild/VoiceChannelActions/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { motion } from 'framer-motion';
import { BiSolidVideo, BiSolidVideoOff } from 'react-icons/bi';
import { LuScreenShare } from 'react-icons/lu';
import { TbConfetti, TbTriangleSquareCircle } from 'react-icons/tb';

import { useChannelActionStore } from '@/stores/channelAction';

import * as S from './styles';

const VoiceChannelActions = () => {
const { isInVoiceChannel } = useChannelActionStore();

const actions = {
video: isInVoiceChannel ? <BiSolidVideo size={24} /> : <BiSolidVideoOff size={24} />,
screenSharing: <LuScreenShare size={24} />,
startActions: <TbTriangleSquareCircle size={24} />,
soundBoard: <TbConfetti size={24} />,
};

const bounceAnimation = {
y: [0, -5, 0],
transition: {
duration: 0.6,
repeat: 3,
repeatType: 'reverse' as const,
ease: 'easeInOut',
},
};

return (
<S.VoiceChannelActions>
{Object.entries(actions).map(([key, value]) => (
<motion.div key={key} initial={{ y: 0 }} whileHover={bounceAnimation}>
<S.Action key={key}>{value}</S.Action>
</motion.div>
))}
</S.VoiceChannelActions>
);
};

export default VoiceChannelActions;
28 changes: 28 additions & 0 deletions src/frontend/src/components/guild/VoiceChannelActions/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import styled from 'styled-components';

export const VoiceChannelActions = styled.div`
display: flex;
gap: 0.5rem;
align-items: center;
justify-content: space-evenly;

margin-top: 1rem;

svg {
color: ${({ theme }) => theme.colors.white};
}
`;

export const Action = styled.div`
cursor: pointer;

display: flex;
align-items: center;
justify-content: center;

width: 5rem;
height: 3rem;
border-radius: 0.8rem;

background-color: ${({ theme }) => theme.colors.dark[500]};
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { BsFillTelephoneXFill } from 'react-icons/bs';

import { useChannelActionStore } from '@/stores/channelAction';
import { useChannelInfoStore } from '@/stores/channelInfo';
import { useGuildInfoStore } from '@/stores/guildInfo';
import { useWebRTCStore } from '@/stores/webRTCStore';
import { tokenAxios } from '@/utils/axios';

import VoiceChannelActions from '../VoiceChannelActions';

import * as S from './styles';

const VoiceChannelController = () => {
const { selectedChannel } = useChannelInfoStore();
const { setIsInVoiceChannel } = useChannelActionStore();
const { guildName } = useGuildInfoStore();
const { setIsStompConnected, disconnectStomp } = useWebRTCStore();

const roomId = useChannelInfoStore((state) => state.selectedChannel?.name);

const handleLeaveRoom = async () => {
setIsInVoiceChannel(false);

if (!roomId) {
alert('방 ID를 입력해주세요!');
return;
}

try {
const response = await tokenAxios.delete(`https://api.jungeunjipi.com/room/${roomId}/leave`);
console.log('방 나가기 성공: ', response);

setIsStompConnected(false);

disconnectStomp();
} catch (error) {
console.error('🚨 방 나가기 오류:', error);
}
};

return (
<S.VoiceChannelController>
<S.ConnectStatusWrapper>
<S.InfoText>
<S.ConnectStatusText>음성 연결됨</S.ConnectStatusText>
<S.ChannelInfoText>
{selectedChannel?.name} / {guildName}
</S.ChannelInfoText>
</S.InfoText>
<BsFillTelephoneXFill size={20} onClick={handleLeaveRoom} />
</S.ConnectStatusWrapper>
<VoiceChannelActions />
</S.VoiceChannelController>
);
};

export default VoiceChannelController;
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import styled from 'styled-components';

import { ChipText, SmallText } from '@/styles/Typography';

export const VoiceChannelController = styled.div`
display: flex;
flex-direction: column;

padding: 1rem;
border-bottom: 1px solid ${({ theme }) => theme.colors.dark[450]};

background-color: ${({ theme }) => theme.colors.dark[750]};
`;

export const InfoText = styled.div`
display: flex;
flex-direction: column;
`;

export const ConnectStatusText = styled(ChipText)`
font-size: 1.5rem;
color: ${({ theme }) => theme.colors.lightGreen};
`;

export const ChannelInfoText = styled(SmallText)`
color: ${({ theme }) => theme.colors.dark[350]};
`;

export const ConnectStatusWrapper = styled.div`
display: flex;
align-items: center;
justify-content: space-between;

svg {
cursor: pointer;
color: ${({ theme }) => theme.colors.white};
}
`;
1 change: 1 addition & 0 deletions src/frontend/src/constants/endPoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const endPoint = {
POST_AUTHENTICATION_CODE: '/users/validation/authentication-code',
POST_SIGN_UP: '/users/sign-up',
POST_SIGN_IN: '/users/sign-in',
GET_USER_ID: '/users/user/id',
},

friends: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as S from './styles';

interface VideoCardProps {
userId: string;
stream?: MediaStream;
localRef?: React.MutableRefObject<HTMLVideoElement | null>;
}

const VideoCard = ({ userId, stream, localRef }: VideoCardProps) => {
return (
<S.VideoCard>
<S.UserName>{userId}</S.UserName>
<S.Video
autoPlay
playsInline
ref={(videoElement) => {
if (localRef) {
localRef.current = videoElement;
}

if (stream && videoElement && videoElement.srcObject !== stream) {
videoElement.srcObject = stream;
}
}}
/>
</S.VideoCard>
);
};

export default VideoCard;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import styled from 'styled-components';

import { BodyMediumText } from '@/styles/Typography';

export const VideoCard = styled.div`
position: relative;
width: fit-content;
`;

export const Video = styled.video`
min-width: 32rem;
max-width: 48rem;
border-radius: 1rem;
`;

export const UserName = styled(BodyMediumText)`
position: absolute;
bottom: 5%;
left: 3%;

width: fit-content;

color: ${({ theme }) => theme.colors.white};
`;
Loading