Skip to content

Commit

Permalink
Merge branch 'develop' into feature/#3
Browse files Browse the repository at this point in the history
suehub committed Nov 12, 2023
2 parents d0431c0 + dad6490 commit 8f8f8b3
Showing 8 changed files with 523 additions and 158 deletions.
213 changes: 213 additions & 0 deletions package-lock.json

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

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@
"@chakra-ui/react": "^2.8.1",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@joeattardi/emoji-button": "^4.6.4",
"@tanstack/react-query": "^5.7.2",
"@tanstack/react-query-devtools": "^5.7.4",
"axios": "^1.6.0",
@@ -24,8 +25,11 @@
"match-sorter": "^6.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.48.2",
"react-icons": "^4.11.0",
"react-router-dom": "^6.18.0",
"react-spinners": "^0.13.8",
"recoil": "^0.7.7",
"socket.io-client": "^4.7.2",
"sort-by": "^1.2.0",
"styled-components": "^6.1.0"
210 changes: 172 additions & 38 deletions src/components/Login/SignUpModal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,191 @@
import useInput from "../../../hooks/useInput";
import { Input } from "@chakra-ui/react";
import { Button } from "@chakra-ui/react";
import { useEffect, useState } from "react";
import React, { useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { Input, Button, Text, Alert, AlertIcon } from "@chakra-ui/react";
import useFetch from "../../../hooks/useFetch";
import axios from "axios";

interface Data {
interface FormData {
id: string;
password: string;
name: string;
picture?: string;
}

const SignUpModal = () => {
const idInput = useInput("");
const pwInput = useInput("");
const nameInput = useInput("");
const [data, setData] = useState<Data>({
id: "",
password: "",
name: "",
const {
control,
handleSubmit,
setError,
clearErrors,
formState: { errors },
} = useForm<FormData>();
const signUpFetch = useFetch({
url: "https://fastcampus-chat.net/signup",
method: "POST",
data: {},
start: false,
});
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [signUpStatus, setSignUpStatus] = useState<{
type: "success" | "error";
message: string;
} | null>(null);

useEffect(() => {
const copy = { ...data };
copy.id = idInput.value;
copy.password = pwInput.value;
copy.name = nameInput.value;

setData(copy);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [idInput.value, pwInput.value, nameInput.value]);

const handleSignup = () => {
axios
.post("https://fastcampus-chat.net/signup", data, {
headers: {
"content-type": "application/json",
serverId: "6603aca7",
const checkIdDuplication = async (id: string): Promise<boolean> => {
try {
const response = await axios.post(
"https://fastcampus-chat.net/check/id",
{ id },
);
return response.data.isDuplicated;
} catch (error) {
console.error("Error checking ID duplication", error);
return false;
}
};

// 회원가입 제출
const onSubmit = async (formData: FormData) => {
let pictureAsString: string | undefined;

// 파일을 Base64 문자열로 변환
const toBase64 = (file: File) =>
new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error);
});

if (selectedFile) {
pictureAsString = await toBase64(selectedFile);
}

const dataToSend = {
...formData,
picture: pictureAsString,
};
// 회원가입 요청 및 결과 처리
try {
const response = await axios.post(
"https://fastcampus-chat.net/signup",
dataToSend,
{
headers: {
"content-type": "application/json",
serverId: import.meta.env.VITE_APP_SERVER_ID,
},
},
})
.then((res) => console.log(res));
);
if (response.status === 200) {
console.log("회원가입 성공:", response.data);
setSignUpStatus({
type: "success",
message: "회원가입에 성공하였습니다.",
});
}
} catch (error) {
console.error("회원가입 실패:", error);
setSignUpStatus({ type: "error", message: "회원가입에 실패하였습니다." });
}
};

return (
<>
<div>회원가입</div>
<Input type="text" value={idInput.value} onChange={idInput.onChange} />
<Input type="text" value={pwInput.value} onChange={pwInput.onChange} />
<Input
type="text"
value={nameInput.value}
onChange={nameInput.onChange}
/>
<Button onClick={handleSignup}>가입</Button>
{signUpStatus && (
<Alert status={signUpStatus.type}>
<AlertIcon />
{signUpStatus.message}
</Alert>
)}
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="picture"
control={control}
render={({ field: { onChange } }) => (
<Input
type="file"
accept="image/*"
onChange={(event) => {
const file = event.target.files?.[0];
if (file) {
if (file.size > 1024 * 1024) {
console.error("File size exceeds 1MB");
} else {
setSelectedFile(file);
onChange(file);
}
}
}}
/>
)}
/>
<Controller
name="id"
control={control}
rules={{
required: "ID는 필수입니다.",
pattern: {
value: /^[A-Za-z0-9]+$/,
message: "ID는 영문자와 숫자만 사용할 수 있습니다.",
},
validate: async (id) => {
try {
const isDuplicated = await checkIdDuplication(id);
if (isDuplicated) {
setError("id", {
type: "manual",
message: "이미 사용중인 ID입니다.",
});
return false;
}
clearErrors("id");
return true;
} catch (error) {
console.error("ID 중복 확인 중 오류 발생:", error);
return "ID 중복 확인 중 오류가 발생했습니다.";
}
},
}}
render={({ field }) => (
<Input type="text" placeholder="ID" {...field} />
)}
/>
{errors.id && <Text color="red.500">{errors.id.message}</Text>}

<Controller
name="password"
control={control}
rules={{
required: "비밀번호는 필수입니다.",
minLength: {
value: 5,
message: "비밀번호는 5자리 이상이어야 합니다.",
},
}}
render={({ field }) => (
<Input type="password" placeholder="Password" {...field} />
)}
/>
{errors.password && (
<Text color="red.500">{errors.password.message}</Text>
)}

<Controller
name="name"
control={control}
rules={{ required: "이름은 필수입니다." }}
render={({ field }) => (
<Input type="text" placeholder="Name" {...field} />
)}
/>
{errors.name && <Text color="red.500">{errors.name.message}</Text>}

<Button type="submit" isLoading={signUpFetch.loading}>
가입
</Button>
</form>
</>
);
};
64 changes: 52 additions & 12 deletions src/components/Main/CreateGameModal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Button, Input } from "@chakra-ui/react";
import { EmojiButton } from "@joeattardi/emoji-button";
import { serverTimestamp } from "firebase/firestore";
import { ChangeEvent, useEffect, useState } from "react";
import { ChangeEvent, useEffect, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
// import { io } from "socket.io-client";
import styled from "styled-components";
import useFetch from "../../../hooks/useFetch";
import useFireFetch from "../../../hooks/useFireFetch";
import useInput from "../../../hooks/useInput";
import connect from "../../../socket/socket";
import Loader from "../../common/Loader";
import UserCard from "../../common/UserCard";
import useSocket from "../../../hooks/useSocket";

const Container = styled.div`
position: absolute;
@@ -78,7 +79,7 @@ const ImgBox = styled.div`
font-size: 3rem;
margin-bottom: 1.5rem;
margin-bottom: 0.5rem;
`;

const Empty = styled.div`
@@ -147,10 +148,34 @@ const CreateGameModal = ({ setModal }: Props) => {
const navigate = useNavigate();
const fireFetch = useFireFetch();

// 이모지 인스턴스 및 데이터 생성
const [emoji, setEmoji] = useState("⭐");
const pickerRef = useRef(null);
const picker = new EmojiButton({
showSearch: false,
showPreview: false,
showRecents: false,
theme: "dark",
zIndex: 10000,
position: {
top: "45%",
right: "50%",
},
});
picker.on("emoji", (selection) => {
// 선택된 이모지 처리
setEmoji(selection.emoji);
});

// 버튼을 클릭할 때 picker를 토글
const handleButtonClick = () => {
picker.togglePicker(pickerRef as unknown as HTMLElement);
};

const token = JSON.parse(localStorage.getItem("token") as string);

// 소켓 연결
const sendMessage = useSocket("9fe8a1af-9c60-4937-82dd-21d6da5b9cd9");
const socket = connect("9fe8a1af-9c60-4937-82dd-21d6da5b9cd9");

// 게임 데이터
const [roomData, setRoomData] = useState<ChatRoom>({
@@ -163,7 +188,9 @@ const CreateGameModal = ({ setModal }: Props) => {
// 방제목 빈값이면 true
const [inputAction, setInpuAction] = useState(false);

// 유저 데이터
const [userList, setUserList] = useState<UserType[]>([]);
const [userListSearch, setUserListSearch] = useState<UserType[]>([]);

// input 초기화
const titleInput = useInput("");
@@ -192,7 +219,9 @@ const CreateGameModal = ({ setModal }: Props) => {
const filter = users.result.filter(
(value: UserType) => value.id !== token.id,
);

setUserList(filter);
setUserListSearch(filter);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [users.result]);
@@ -208,12 +237,12 @@ const CreateGameModal = ({ setModal }: Props) => {
// 유저 검색 기능
useEffect(() => {
if (users.result) {
const filter = users.result.filter((value: UserType) =>
const filter = userList.filter((value: UserType) =>
value.name.includes(searchInput.value),
);
setUserList(filter);
setUserListSearch(filter);
}
}, [searchInput.value, users.result]);
}, [searchInput.value, users.result, userList]);

// 게임 생성 함수
const handleMakeRoom = () => {
@@ -240,10 +269,11 @@ const CreateGameModal = ({ setModal }: Props) => {
// 파이어베이스 게임 데이터 생성
const newData = {
...roomData,
users: [...roomData.users, token.id],
id: createGame.result.id,
host: token.id,
createdAt: serverTimestamp(),
bg: "⭐",
bg: emoji,
status: "대기중",
};

@@ -259,7 +289,7 @@ const CreateGameModal = ({ setModal }: Props) => {
const text = JSON.stringify(inviteUser);

// 초대 메시지 전달
sendMessage(text);
socket.emit("message-to-server", text);

// 해당 게임방으로 이동
navigate(`/game?gameId=${createGame.result.id}`);
@@ -272,7 +302,16 @@ const CreateGameModal = ({ setModal }: Props) => {
<Wrap>
<Section>
<div>
<ImgBox></ImgBox>
<ImgBox>{emoji}</ImgBox>
<Button
size="xs"
marginBottom="1.5rem"
className="trigger"
onClick={handleButtonClick}
ref={pickerRef}
>
이모티콘 선택
</Button>
<Input
marginBottom="1rem"
border={!inputAction ? "1px solid #c6c6c6" : "1px solid red"}
@@ -381,8 +420,9 @@ const CreateGameModal = ({ setModal }: Props) => {
onChange={searchInput.onChange}
/>
</div>
<Loader loading={users.loading}></Loader>
{users.result &&
userList.map((value: UserType) => {
userListSearch.map((value: UserType) => {
return (
<UserCard
key={value.id}
14 changes: 14 additions & 0 deletions src/components/common/Loader/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import ClockLoader from "react-spinners/ClockLoader";

const Loader = ({ loading }: { loading: boolean }) => {
return (
<ClockLoader
color="rgba(89, 89, 89, 1)"
loading={loading}
size={50}
speedMultiplier={2}
/>
);
};

export default Loader;
42 changes: 0 additions & 42 deletions src/hooks/useSocket.ts

This file was deleted.

113 changes: 47 additions & 66 deletions src/pages/Example/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { Button, Input } from "@chakra-ui/react";
import { serverTimestamp } from "firebase/firestore";
import { useEffect, useState } from "react";
// import { io } from "socket.io-client";
import CreateGameModal from "../../components/Main/CreateGameModal";
import ToastNotice from "../../components/common/ToastNotice";
import useFetch from "../../hooks/useFetch";
import useFireFetch from "../../hooks/useFireFetch";
import useInput from "../../hooks/useInput";
import useSocket from "../../hooks/useSocket";
import connect from "../../socket/socket";

interface MessageInfo {
id: string;
text: string;
}

const Example = () => {
const token = JSON.parse(localStorage.getItem("token") as string);
@@ -23,9 +27,10 @@ const Example = () => {
});

// 소켓 통신
const sendMessage = useSocket(
"9fe8a1af-9c60-4937-82dd-21d6da5b9cd9",
(messageObject) => {
const socket = connect("9fe8a1af-9c60-4937-82dd-21d6da5b9cd9");

useEffect(() => {
socket.on("message-to-client", (messageObject) => {
// 일반 채팅인지 초대 메시지인지 구별
if (messageObject.text.slice(-5, -2) === "*&^") {
// 초대 상태 저장
@@ -39,30 +44,34 @@ const Example = () => {
setRoomData(room);
} else {
// 메시지 데이터, 작성 유저 상태 저장
const copy = { ...message };
copy.id = messageObject.userId;
copy.text = messageObject.text;
setMessage(copy);
const message = {
id: messageObject.userId,
text: messageObject.text,
};

console.log(message);
setMessages((prev) => [...prev, message]);
}
},
);
});

// // 채팅 서버 연결
// const socket = io(
// `https://fastcampus-chat.net/chat?chatId=9fe8a1af-9c60-4937-82dd-21d6da5b9cd9`,
// {
// extraHeaders: {
// Authorization: `Bearer ${token.accessToken}`,
// serverId: import.meta.env.VITE_APP_SERVER_ID,
// },
// },
// );
// 채팅 기록 확인
socket.on("messages-to-client", (messageObject) => {
console.log(messageObject);
});

// 초대 메시지
socket.on("new-chat", (messageObject) => {
console.log(messageObject);
});

return () => {
socket.off("message-to-client");
};
}, [socket]);

// 메세지 데이터
const [message, setMessage] = useState({
id: "",
text: "",
});
const [messages, setMessages] = useState<MessageInfo[]>([]);

// 초대방 정보 데이터
const [roomData, setRoomData] = useState({
id: "",
@@ -96,48 +105,18 @@ const Example = () => {
url: "https://fastcampus-chat.net/chat/leave",
method: "PATCH",
data: {
chatId: "e6d8fd5b-00e3-4598-b826-11366c8c4676",
chatId: "535add19-c98f-4a9b-bc6f-c145c496cb91",
},
start: false,
});

// 메시지 input value 저장
const messageValue = useInput("");

// // 소켓 통신 시 메시지 데이터 저장
// useEffect(() => {
// socket.on("message-to-client", (messageObject) => {
// // 일반 채팅인지 초대 메시지인지 구별

// if (messageObject.text.slice(-5, -2) === "*&^") {
// // 초대 상태 저장
// const usersArr = JSON.parse(messageObject.text);
// const users = [...usersArr];
// users.pop();
// users.pop();
// const room = usersArr[usersArr.length - 2];

// setToastUser(users);
// setRoomData(room);
// } else {
// // 메시지 데이터, 작성 유저 상태 저장
// const copy = { ...message };
// copy.id = messageObject.userId;
// copy.text = messageObject.text;
// setMessage(copy);
// }

// // console.log(messageObject);
// });

// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, [socket]);

// 메시지 값 변화시(소켓 통신 시) 콘솔에 메시지 데이터 출력
useEffect(() => {
if (message.id !== "") console.log(message);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [message.text]);
console.log(messages);
}, [messages]);

//팝업 변화 감지
useEffect(() => {
@@ -189,30 +168,32 @@ const Example = () => {
body: "updated",
createdAt: serverTimestamp(),
};

fireFetch.updateData("notice", "asdasdasdasdasd", newData);

const copy = [...notice.data];
const index = copy.findIndex((v) => v.id === "asdasdasdasdasd");
copy[index] = newData;

notice.setData(copy);
};

const postData_A = () => {
console.log(1);
const getMessages = () => {
socket.emit("fetch-messages");
};

const deleteData_A = () => {
const liveChat = () => {
live.refresh();
};

// api get 요청으로 가져온 데이터 출력
const getData_A = () => {
const getFetchData = () => {
console.log(users.result, users.loading, users.statusCode);
};

// 메시지 보내는 함수
const submitMessage = () => {
sendMessage(messageValue.value);
socket.emit("message-to-server", messageValue.value);
};

return (
@@ -229,9 +210,9 @@ const Example = () => {

{/* 벡엔드 rest api 통신 */}
<div style={{ marginBottom: "2rem" }}>
<Button onClick={postData_A}>Post</Button>
<Button onClick={deleteData_A}>나가기</Button>
<Button onClick={getData_A}>Get</Button>
<Button onClick={getMessages}>채팅 메시지 기록 확인</Button>
<Button onClick={liveChat}>나가기</Button>
<Button onClick={getFetchData}>Get</Button>
</div>

{/* 메시지 소켓 통신 */}
21 changes: 21 additions & 0 deletions src/socket/socket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { io } from "socket.io-client";

const connect = (chatId: string) => {
const token = JSON.parse(localStorage.getItem("token") as string);

const socket = io(
`https://fastcampus-chat.net/${
chatId === "main" ? "server" : `chat?chatId=${chatId}`
}`,
{
extraHeaders: {
Authorization: `Bearer ${token.accessToken}`,
serverId: import.meta.env.VITE_APP_SERVER_ID,
},
},
);

return socket;
};

export default connect;

0 comments on commit 8f8f8b3

Please sign in to comment.