Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FE] 프론트엔드 배포 #223

Merged
merged 15 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Binary file added frontEnd/public/cFile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontEnd/public/javaFile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontEnd/public/javascriptFile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontEnd/public/kotlinFile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontEnd/public/pythonFile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontEnd/public/swiftFile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion frontEnd/src/components/common/ChattingErrorToast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ interface ChattingErrorToastProps {

export default function ChattingErrorToast({ errorData, setErrorData }: ChattingErrorToastProps) {
return (
<div className="absolute z-10 flex flex-col items-start w-full bottom-24 ">
<div className="absolute z-10 flex flex-col items-start w-full bottom-28 ">
<div className="flex flex-col p-5 mb-5 ml-5 drop-shadow-lg bg-base">
<span className="text-sm">{errorData.text1}</span>
<span className="text-sm">{errorData.text2}</span>
Expand Down
96 changes: 29 additions & 67 deletions frontEnd/src/components/room/ChattingSection.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useParams } from 'react-router-dom';
import { useEffect, useRef, useState } from 'react';
import { memo, useEffect, useState } from 'react';
import { Socket, io } from 'socket.io-client/debug';
import { VITE_CHAT_URL } from '@/constants/env';
import { ErrorData, ErrorResponse, MessageData } from '@/types/chatting';
Expand All @@ -11,70 +11,26 @@ import { CHATTING_SOCKET_EMIT_EVNET, CHATTING_SOCKET_RECIEVE_EVNET } from '@/con
import Section from '../common/SectionWrapper';
import { CHATTING_ERROR_STATUS_CODE, CHATTING_ERROR_TEXT } from '@/constants/chattingErrorResponse';
import ChattingErrorToast from '../common/ChattingErrorToast';
import useRoomConfigData from '@/stores/useRoomConfigData';
import useScroll from '@/hooks/useScroll';

let socket: Socket;
let timer: NodeJS.Timeout | null;

export default function ChattingSection() {
const [message, setMessage] = useState('');
function ChattingSection() {
const [socket, setSocket] = useState<Socket | null>(null);
const [allMessages, setAllMessage] = useState<MessageData[]>([]);

const [usingAi, setUsingAi] = useState<boolean>(false);
const [postingAi, setPostingAi] = useState<boolean>(false);

const [errorData, setErrorData] = useState<ErrorData | null>(null);
const { roomId } = useParams();

const nickname = useRoomConfigData((state) => state.nickname);

const { isViewingLastMessage, isRecievedMessage, setScrollRatio, setIsRecievedMessage } = useLastMessageViewingState();

const messageAreaRef = useRef<HTMLDivElement | null>(null);

const handleScroll = () => {
if (!timer) {
timer = setTimeout(() => {
timer = null;

if (!messageAreaRef.current) return;

const { scrollTop, clientHeight, scrollHeight } = messageAreaRef.current;
setScrollRatio(((scrollTop + clientHeight) / scrollHeight) * 100);
}, 200);
}
};

const moveToBottom = (ref: React.RefObject<HTMLElement>) => {
if (ref.current) ref.current.scrollTop = ref.current.scrollHeight;
};

const handleMoveToBottom = () => {
moveToBottom(messageAreaRef);
setScrollRatio(100);
};

const handleInputMessage = (event: React.ChangeEvent<HTMLInputElement>) => {
setMessage(event.target.value);
};

const handleMessageSend = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();

if (!socket) return;

if (usingAi) {
setPostingAi(true);
socket.emit(CHATTING_SOCKET_EMIT_EVNET.SEND_MESSAGE, { room: roomId, message, nickname, ai: true });
} else socket.emit(CHATTING_SOCKET_EMIT_EVNET.SEND_MESSAGE, { room: roomId, message, nickname, ai: false });

setMessage('');
setScrollRatio(100);
};
const { ref: messageAreaRef, scrollRatio, handleScroll, moveToBottom } = useScroll<HTMLDivElement>();
const { isViewingLastMessage, isRecievedMessage, setIsRecievedMessage } = useLastMessageViewingState(scrollRatio);

const handleRecieveMessage = (recievedMessage: string) => {
const newMessage: MessageData | { using: boolean } = JSON.parse(recievedMessage);
const remoteUsingAi = 'using' in newMessage;

// 새로운 메시지가 AI 사용 여부에 관한 메시지인 경우
if ('using' in newMessage) {
if (remoteUsingAi) {
setPostingAi(newMessage.using);
return;
}
Expand All @@ -97,19 +53,23 @@ export default function ChattingSection() {
};

useEffect(() => {
socket = io(VITE_CHAT_URL, {
transports: ['websocket'],
});
setSocket(() => {
const newSocket = io(VITE_CHAT_URL, {
transports: ['websocket'],
});

socket.on(CHATTING_SOCKET_RECIEVE_EVNET.NEW_MESSAGE, handleRecieveMessage);
socket.on('exception', handleChattingSocketError);
socket.connect();
newSocket.on(CHATTING_SOCKET_RECIEVE_EVNET.NEW_MESSAGE, handleRecieveMessage);
newSocket.on('exception', handleChattingSocketError);
newSocket.connect();

socket.emit(CHATTING_SOCKET_EMIT_EVNET.JOIN_ROOM, { room: roomId });
newSocket.emit(CHATTING_SOCKET_EMIT_EVNET.JOIN_ROOM, { room: roomId });

return newSocket;
});
}, []);

useEffect(() => {
if (isViewingLastMessage) moveToBottom(messageAreaRef);
if (isViewingLastMessage) moveToBottom();
else setIsRecievedMessage(true);
}, [allMessages]);

Expand All @@ -122,20 +82,22 @@ export default function ChattingSection() {
onScroll={handleScroll}
>
{allMessages.map((messageData, index) => (
<ChattingMessage messageData={messageData} key={index} isMyMessage={messageData.socketId === socket.id} />
<ChattingMessage messageData={messageData} key={index} isMyMessage={messageData.socketId === socket?.id} />
))}
</div>
{isRecievedMessage && <ScrollDownButton handleMoveToBottom={handleMoveToBottom} />}
{isRecievedMessage && <ScrollDownButton handleMoveToBottom={moveToBottom} />}
{errorData && <ChattingErrorToast errorData={errorData} setErrorData={setErrorData} />}
<ChattingInput
handleMessageSend={handleMessageSend}
message={message}
handleInputMessage={handleInputMessage}
usingAi={usingAi}
setUsingAi={setUsingAi}
postingAi={postingAi}
socket={socket}
setPostingAi={setPostingAi}
moveToBottom={moveToBottom}
/>
</div>
</Section>
);
}

export default memo(ChattingSection);
63 changes: 45 additions & 18 deletions frontEnd/src/components/room/chatting/ChattingInput.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,72 @@
import { useParams } from 'react-router-dom';
import { Socket } from 'socket.io-client/debug';
import Spinner from '@/components/common/Spinner';
import ToggleAi from '@/components/common/ToggleAi';
import { CHATTING_SOCKET_EMIT_EVNET } from '@/constants/chattingSocketEvents';
import useInput from '@/hooks/useInput';
import useRoomConfigData from '@/stores/useRoomConfigData';

function SendButtonText({ usingAi, postingAi }: { usingAi: boolean; postingAi: boolean }) {
if (!usingAi) return '전송';

if (postingAi) return <Spinner />;

return '질문';
}

interface ChattingInputProps {
handleMessageSend: (event: React.FormEvent<HTMLFormElement>) => void;
message: string;
handleInputMessage: (event: React.ChangeEvent<HTMLInputElement>) => void;
usingAi: boolean;
setUsingAi: React.Dispatch<React.SetStateAction<boolean>>;
postingAi: boolean;
setPostingAi: React.Dispatch<React.SetStateAction<boolean>>;
socket: Socket | null;
moveToBottom: () => void;
}

export default function ChattingInput({
handleMessageSend,
message,
handleInputMessage,
usingAi,
setUsingAi,
postingAi,
}: ChattingInputProps) {
export default function ChattingInput({ usingAi, setUsingAi, postingAi, setPostingAi, socket, moveToBottom }: ChattingInputProps) {
const { inputValue: message, onChange, resetInput } = useInput<HTMLTextAreaElement>('');
const nickname = useRoomConfigData((state) => state.nickname);
const { roomId } = useParams();

const handleMessageSend = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();

if (!socket) return;

if (usingAi) {
setPostingAi(true);

socket.emit(CHATTING_SOCKET_EMIT_EVNET.SEND_MESSAGE, { room: roomId, message, nickname: `${nickname}의 질문`, ai: false });
socket.emit(CHATTING_SOCKET_EMIT_EVNET.SEND_MESSAGE, { room: roomId, message, nickname, ai: true });
} else socket.emit(CHATTING_SOCKET_EMIT_EVNET.SEND_MESSAGE, { room: roomId, message, nickname, ai: false });

resetInput();
moveToBottom();
};

const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
handleMessageSend(event as unknown as React.FormEvent<HTMLFormElement>);
}
};

return (
<form onSubmit={handleMessageSend} className="w-full p-2 rounded-b-lg bg-base">
<ToggleAi usingAi={usingAi} setUsingAi={setUsingAi} />
<div className="flex items-center w-full h-12 rounded-lg drop-shadow-lg">
<input
<div className="flex items-center w-full h-[72px] rounded-lg drop-shadow-lg">
<textarea
onKeyDown={handleKeyDown}
disabled={usingAi && postingAi}
type="text"
value={message}
onChange={handleInputMessage}
className={`w-full h-12 p-2 px-4 focus:outline-none rounded-s-lg ${usingAi ? 'border-point-blue border-2' : ''}`}
onChange={onChange}
className={`w-full h-full p-2 px-4 focus:outline-none rounded-s-lg resize-none border-2 custom-scroll ${
usingAi ? 'border-point-blue' : 'border-white'
}`}
placeholder={usingAi ? 'AI에게 질문해보세요' : 'Message'}
/>
<button
type="submit"
className={`h-full px-4 py-1 font-normal rounded-e-lg whitespace-nowrap w-16 flex items-center justify-center ${
className={`font-normal rounded-e-lg whitespace-nowrap w-16 flex items-center justify-center h-full ${
usingAi ? 'bg-point-blue text-white' : 'bg-primary text-black'
}`}
disabled={usingAi && postingAi}
Expand Down
23 changes: 18 additions & 5 deletions frontEnd/src/components/room/chatting/ChattingMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
import { memo } from 'react';
import { MessageData } from '@/types/chatting';

interface ChattingMessageProps {
messageData: MessageData;
isMyMessage: boolean;
}

export default function ChattingMessage({ messageData, isMyMessage }: ChattingMessageProps) {
function ChattingMessage({ messageData, isMyMessage }: ChattingMessageProps) {
const aiMessage = messageData.ai;
const myMessage = !aiMessage && isMyMessage;

const getMessageColor = () => {
if (aiMessage) return 'bg-point-blue text-white';
if (myMessage) return 'bg-blue-100';

return 'bg-yellow-100';
};

return (
<div className={`flex flex-col gap-0.5 ${isMyMessage ? 'items-end' : 'items-start'}`}>
<span className="mx-1 text-xs font-light ">{messageData.nickname}</span>
<div className={`px-4 py-2 rounded-lg w-fit ${isMyMessage ? 'bg-blue-100' : 'bg-yellow-100'}`}>
<span>{messageData.message}</span>
<div className={`flex flex-col gap-0.5 ${myMessage ? 'items-end' : 'items-start'}`}>
<span className="mx-1 text-xs font-light">{aiMessage ? '클로바 X' : messageData.nickname}</span>
<div className={`px-4 py-2 rounded-lg w-fit ${getMessageColor()}`}>
<span className="whitespace-pre-wrap">{messageData.message}</span>
</div>
</div>
);
}

export default memo(ChattingMessage);
1 change: 1 addition & 0 deletions frontEnd/src/components/room/modal/LoginModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default function LoginModal({ code }: { code: string }) {
const token = await getDevCookie();
document.cookie = `access_token=${token};`;
hide();
window.location.reload();
}
reactQueryClient.invalidateQueries({ queryKey: [QUERY_KEYS.LOAD_CODES] });
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default function CodeFileButton({
className="flex flex-col items-center justify-center col-span-1"
>
<div className="relative w-1/3 group">
<img src="/fileIcon.png" alt="fileIcon" />
<img src={`/${code.language}File.png`} alt="fileIcon" />

{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div className="absolute -top-3 -right-2 w-[50px] group-hover:block hidden" onClick={handleDeleteButton}>
Expand Down
10 changes: 10 additions & 0 deletions frontEnd/src/hooks/tests/useInput.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,14 @@ describe('useInput 기능테스트', () => {

expect(result.current.inputValue).toBe('changeValue');
});

it('resetInput을 실행시키면 입력값이 초기화된다.', () => {
const { result } = renderHook(() => useInput('initial'));

act(() => {
result.current.resetInput();
});

expect(result.current.inputValue).toBe('');
});
});
10 changes: 7 additions & 3 deletions frontEnd/src/hooks/useInput.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { useState } from 'react';

export default function useInput(initialValue: string) {
export default function useInput<T extends HTMLInputElement | HTMLTextAreaElement>(initialValue: string) {
const [inputValue, setInputValue] = useState(initialValue);

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

return { inputValue, onChange };
const resetInput = () => {
setInputValue('');
};

return { inputValue, onChange, resetInput };
}
5 changes: 2 additions & 3 deletions frontEnd/src/hooks/useLastMessageViewingState.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useEffect, useState } from 'react';

export default function useLastMessageViewingState() {
const [scrollRatio, setScrollRatio] = useState(100);
export default function useLastMessageViewingState(scrollRatio: number) {
const [isViewingLastMessage, setIsViewingLastMessage] = useState(true);
const [isRecievedMessage, setIsRecievedMessage] = useState(false);

Expand All @@ -12,5 +11,5 @@ export default function useLastMessageViewingState() {
} else setIsViewingLastMessage(false);
}, [scrollRatio]);

return { setScrollRatio, setIsRecievedMessage, isRecievedMessage, isViewingLastMessage };
return { setIsRecievedMessage, isRecievedMessage, isViewingLastMessage };
}
28 changes: 28 additions & 0 deletions frontEnd/src/hooks/useScroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useRef, useState } from 'react';

let timer: NodeJS.Timeout | null;

export default function useScroll<T extends HTMLElement>() {
const [scrollRatio, setScrollRatio] = useState(100);
const ref = useRef<T | null>(null);

const handleScroll = () => {
if (!timer) {
timer = setTimeout(() => {
timer = null;

if (!ref.current) return;

const { scrollTop, clientHeight, scrollHeight } = ref.current;
setScrollRatio(((scrollTop + clientHeight) / scrollHeight) * 100);
}, 200);
}
};

const moveToBottom = () => {
if (ref.current) ref.current.scrollTop = ref.current.scrollHeight;
setScrollRatio(100);
};

return { ref, scrollRatio, handleScroll, moveToBottom };
}
Loading