Skip to content

Commit

Permalink
merge: [FE] 프론트엔드 배포 (#223)
Browse files Browse the repository at this point in the history
merge: [FE] 프론트엔드 배포 (#223)
  • Loading branch information
d0422 authored Dec 7, 2023
2 parents 45399ad + 1aacd24 commit dd06ffa
Show file tree
Hide file tree
Showing 17 changed files with 147 additions and 103 deletions.
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

0 comments on commit dd06ffa

Please sign in to comment.