diff --git a/front/src/api/server.ts b/front/src/api/server.ts index 4c797879..d85c42c8 100644 --- a/front/src/api/server.ts +++ b/front/src/api/server.ts @@ -70,3 +70,20 @@ export const createServer = async ( throw new Error(""); } }; + +// 서버 탈퇴 +export const leaveServer = async (serverId: string, token: string) => { + console.log(serverId, token); + try { + await axiosInstance({ + ...SERVER_API.DELETE_REQUEST.leaveServer(serverId), + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + console.log("서버탈퇴 성공"); + } catch (error) { + throw new Error("탈퇴실패"); + } +}; diff --git a/front/src/api/user.ts b/front/src/api/user.ts index c5c54773..e11a16be 100644 --- a/front/src/api/user.ts +++ b/front/src/api/user.ts @@ -90,6 +90,34 @@ export const searchUser = async ( console.log(error); } }; +// 사용자 프로필 수정 +export const editUserProfile = async ( + userId: string, + token: string, + editValues: UserTypes.EditProfileFormValues +) => { + try { + const response = await axiosInstance({ + ...USER_API.PATCH_REQUEST.editProfile, + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "multipart/form-data", + }, + data: { + userId, + email: editValues.email, + username: editValues.nickname, + password: editValues.password, + newPassword: editValues.newPassword, + }, + }); + console.log(response.data); + + // return response.data; + } catch (error) { + console.log(error); + } +}; // 사용자 프로필 조회 export const getUserProfile = async ( @@ -188,3 +216,19 @@ export const deleteFriend = async ( console.log(error); } }; + +// 회원 탈퇴 +export const deleteUser = async (userId: string, token: string) => { + try { + const response = await axiosInstance({ + ...USER_API.DELETE_REQUEST.deleteAccount(userId), + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + console.log(response.data); + } catch (error) { + console.log(error); + } +}; diff --git a/front/src/components/Phaser/MapTheme/BeachMap.ts b/front/src/components/Phaser/MapTheme/BeachMap.ts index 042a51b5..ce061bb1 100644 --- a/front/src/components/Phaser/MapTheme/BeachMap.ts +++ b/front/src/components/Phaser/MapTheme/BeachMap.ts @@ -1,16 +1,27 @@ import { Scene } from "phaser"; -import { createRandomAvatar, randomSkin } from "../Scenes/Avatar"; -import { controlAvatarAnimations } from "../Avatar/controlAvatar"; +import { createRandomAvatar } from "../Scenes/Avatar"; +import { setupCameraControls } from "../Scenes/cameraControls"; export class BeachMap extends Scene { private avatar!: Phaser.GameObjects.Container; private keyboards!: Phaser.Types.Input.Keyboard.CursorKeys | null; + private spaceKey!: Phaser.Input.Keyboard.Key | undefined; + private isJumping: boolean = false; + private nickname: string; - constructor() { + constructor(nickname: string) { super("BeachMap"); + this.nickname = nickname; + } + + init(data: { nickname: string }) { + this.nickname = data.nickname; } create() { + this.spaceKey = this.input.keyboard?.addKey( + Phaser.Input.Keyboard.KeyCodes.SPACE + ); const beachMap = this.make.tilemap({ key: "beach" }); const beachTilesets = beachMap.addTilesetImage( "beach_tilesets", @@ -33,15 +44,21 @@ export class BeachMap extends Scene { faceColor: new Phaser.Display.Color(48, 38, 37, 255), }); - this.avatar = createRandomAvatar(this, 550, 350); + this.avatar = createRandomAvatar(this, 550, 350, this.nickname); + this.cameras.main.startFollow(this.avatar); + this.cameras.main.setZoom(2); + this.avatar.setDepth(10); + this.add.existing(this.avatar); this.avatar.setScale(2); - this.cameras.main.startFollow(this.avatar); if (bottomGroundLayer) this.physics.add.collider(this.avatar, bottomGroundLayer); } + setupCameraControls(this, this.avatar); + this.isJumping = false; + if (this.input.keyboard) { this.keyboards = this.input.keyboard.createCursorKeys(); } else { diff --git a/front/src/components/Phaser/MapTheme/CampingMap.ts b/front/src/components/Phaser/MapTheme/CampingMap.ts index 11f2692c..9d353484 100644 --- a/front/src/components/Phaser/MapTheme/CampingMap.ts +++ b/front/src/components/Phaser/MapTheme/CampingMap.ts @@ -7,13 +7,53 @@ export class CampingMap extends Scene { private avatar!: Phaser.GameObjects.Container; private keyboards!: Phaser.Types.Input.Keyboard.CursorKeys | null; private spaceKey!: Phaser.Input.Keyboard.Key | undefined; + private chatText!: Phaser.GameObjects.Text | null; private isJumping: boolean = false; + private nickname: string = ""; + private boundHandleChatMessage: (event: Event) => void; - constructor() { + constructor(nickname: string) { super("CampingMap"); + this.nickname = nickname; + this.boundHandleChatMessage = this.handleChatMessage.bind(this); + } + + handleChatMessage = (event: Event) => { + const customEvent = event as CustomEvent; + const message = customEvent.detail; + + if (!this.scene.isActive() || !this.avatar) { + return; + } + + if (this.chatText) { + this.chatText.destroy(); + } + + this.chatText = this.add.text(this.avatar.x, this.avatar.y - 50, message, { + font: "14px Arial", + color: "#ffffff", + backgroundColor: "#000000", + padding: { x: 10, y: 5 }, + }); + + this.chatText.setOrigin(0.5, 1); + + this.time.delayedCall(3000, () => { + if (this.chatText) { + this.chatText.destroy(); + this.chatText = null; + } + }); + }; + + init(data: { nickname: string }) { + this.nickname = data.nickname || "Guest"; } create() { + window.addEventListener("chatMessage", this.boundHandleChatMessage); + // this.avatar = this.add.container(300, 300); this.spaceKey = this.input.keyboard?.addKey( Phaser.Input.Keyboard.KeyCodes.SPACE ); @@ -22,7 +62,6 @@ export class CampingMap extends Scene { "camping_tilesets", "camping_tilesets" ); - if (campingTilesets) { const layers = [ "bottom_ground_layer", @@ -42,9 +81,9 @@ export class CampingMap extends Scene { "rv_layer", ]; - this.avatar = createRandomAvatar(this, 520, 350); - + this.avatar = createRandomAvatar(this, 520, 350, this.nickname); this.add.existing(this.avatar); + this.cameras.main.startFollow(this.avatar); this.cameras.main.setZoom(2); this.avatar.setDepth(10); @@ -140,4 +179,8 @@ export class CampingMap extends Scene { ); } } + + shutdown() { + window.removeEventListener("chatMessage", this.boundHandleChatMessage); + } } diff --git a/front/src/components/Phaser/Scenes/Avatar.ts b/front/src/components/Phaser/Scenes/Avatar.ts index 573e6b9c..0a6e7a05 100644 --- a/front/src/components/Phaser/Scenes/Avatar.ts +++ b/front/src/components/Phaser/Scenes/Avatar.ts @@ -13,7 +13,8 @@ export const randomSkin = getRandomAssets(skins); export const createRandomAvatar = ( scene: Scene, x: number, - y: number + y: number, + nickname: string ): Phaser.GameObjects.Container => { if (scene.textures.exists(randomSkin)) { const avatarContainer = scene.add.container(x, y); @@ -85,7 +86,7 @@ export const createRandomAvatar = ( const userNickName = scene.add.text( 0, skinSprite.height / 2 + 10, - "nickname", + nickname, { font: "10px Arial", color: "#ffffff", diff --git a/front/src/components/Phaser/ServerMap.tsx b/front/src/components/Phaser/ServerMap.tsx index bf380791..01bcb296 100644 --- a/front/src/components/Phaser/ServerMap.tsx +++ b/front/src/components/Phaser/ServerMap.tsx @@ -7,31 +7,43 @@ import { EventBus } from "./EventBus"; import { ServerMapProps, ServerMapTypes } from "../../types/map"; import VideoCallBoxList from "../VideoCall/VideoCallBoxList"; import VideoCallToolBar from "../VideoCall/VideoCallToolBar"; +import { useThemeStore } from "@/store/useThemeStore"; +import useUserProfile from "@/hooks/user/useUserProfile"; +import ChatInput from "../organisms/ChatBox"; +import ChatBox from "../organisms/ChatBox"; export const ServerMap = forwardRef( - function ServerMap({ currentActiveScene, selectedTheme }, ref) { + function ServerMap({ currentActiveScene }, ref) { const mapRef = useRef(null!); + const nickname = useUserProfile(); + console.log(nickname.userProfile?.data.username); + + const selectedTheme = useThemeStore((state) => state.selectedTheme); useLayoutEffect(() => { - if (mapRef.current === null) { - mapRef.current = StartGame(selectedTheme, "map-container"); + if (mapRef.current) { + mapRef.current.destroy(true); + mapRef.current = null; + } - if (typeof ref === "function") { - ref({ server: mapRef.current, scene: null }); - } else if (ref) { - ref.current = { server: mapRef.current, scene: null }; - } + mapRef.current = StartGame( + selectedTheme, + "map-container", + nickname.userProfile?.data.username || "" + ); + if (typeof ref === "function") { + ref({ server: mapRef.current, scene: null }); + } else if (ref) { + ref.current = { server: mapRef.current, scene: null }; } return () => { if (mapRef.current) { mapRef.current.destroy(true); - if (mapRef.current !== null) { - mapRef.current = null; - } + mapRef.current = null; } }; - }, [ref]); + }, [selectedTheme, ref]); useEffect(() => { EventBus.on("current-scene-ready", (scene_instance: Phaser.Scene) => { @@ -52,10 +64,9 @@ export const ServerMap = forwardRef( return (
-
- +
+
-
diff --git a/front/src/components/Phaser/main.ts b/front/src/components/Phaser/main.ts index 1e9f47ff..7d325b45 100644 --- a/front/src/components/Phaser/main.ts +++ b/front/src/components/Phaser/main.ts @@ -34,15 +34,27 @@ const config: Phaser.Types.Core.GameConfig = { }, }; -export const EnterServer = (theme: ThemeType | null, parent: string) => { - let scenes: Phaser.Types.Scenes.SceneType[] = []; +export const EnterServer = ( + theme: ThemeType | null, + parent: string, + nickname: string +) => { + const game = new Game({ ...config, parent, scene: [] }); if (theme?.name === "숲") { - scenes = [CampingPreloader, CampingMap]; + game.scene.add("CampingPreloader", CampingPreloader, true); + game.scene.add("CampingMap", CampingMap, false); + + game.scene.start("CampingPreloader"); + game.scene.start("CampingMap", { nickname }); } else if (theme?.name === "오피스") { - scenes = [BeachPreloader, BeachMap]; + game.scene.add("BeachPreloader", BeachPreloader, true); + game.scene.add("BeachMap", BeachMap, false); + + game.scene.start("BeachPreloader"); + game.scene.start("BeachMap", { nickname }); } - return new Game({ ...config, parent, scene: scenes }); + return game; }; export default EnterServer; diff --git a/front/src/components/atoms/BookmarkStar.tsx b/front/src/components/atoms/BookmarkStar.tsx index f36fc891..8df10e04 100644 --- a/front/src/components/atoms/BookmarkStar.tsx +++ b/front/src/components/atoms/BookmarkStar.tsx @@ -17,7 +17,7 @@ const FavoriteStar: React.FC = ({ id }) => { }; return (
- {!isBookmark ? : } + {!isBookmark ? : }
); }; diff --git a/front/src/components/atoms/Button.tsx b/front/src/components/atoms/Button.tsx index cd93d370..64311f1a 100644 --- a/front/src/components/atoms/Button.tsx +++ b/front/src/components/atoms/Button.tsx @@ -4,8 +4,9 @@ type ButtonProps< children: React.ReactNode; icon?: React.ReactNode; onClick?: (e: E) => void; - color: string; + style?: string; disabled?: boolean; + type?: "button" | "submit" | "reset"; }; // TODO: 스타일 변경 @@ -13,13 +14,15 @@ const Button: React.FC = ({ children, icon, onClick, - color, + style, + type, disabled = false, }) => { return ( diff --git a/front/src/components/atoms/Text.tsx b/front/src/components/atoms/Text.tsx index f89f74cc..12b147ab 100644 --- a/front/src/components/atoms/Text.tsx +++ b/front/src/components/atoms/Text.tsx @@ -2,12 +2,11 @@ import React from "react"; type TextProps = { children: React.ReactNode; - color: string; - size: string; + styles: string; }; -const Text: React.FC = ({ children, color, size }) => { - return

{children}

; +const Text: React.FC = ({ children, styles }) => { + return

{children}

; }; export default Text; diff --git a/front/src/components/atoms/TextInput.tsx b/front/src/components/atoms/TextInput.tsx index 2dd00faf..c95eae8b 100644 --- a/front/src/components/atoms/TextInput.tsx +++ b/front/src/components/atoms/TextInput.tsx @@ -7,6 +7,7 @@ type TextInputProps = { label?: string; placeholder?: string; onChange: (e: React.ChangeEvent) => void; + style?: string; }; // TODO: 스타일 변경 @@ -17,11 +18,12 @@ const TextInput: React.FC = ({ onChange, placeholder, name, + style, }) => { const id = `input-${label || Math.random().toString(36).substring(2, 5)}`; return ( -
); diff --git a/front/src/components/molecules/AvatarWithAddServer.tsx b/front/src/components/molecules/AvatarWithAddServer.tsx index 37655c28..044fd114 100644 --- a/front/src/components/molecules/AvatarWithAddServer.tsx +++ b/front/src/components/molecules/AvatarWithAddServer.tsx @@ -13,9 +13,28 @@ const AvatarWithAddServer: React.FC = ({ onAddServerClick, }) => { return ( -
- - +
+
+ + + {nickname} + +
+
+ + + 서버 추가 + +
); }; diff --git a/front/src/components/molecules/CategorySelector.tsx b/front/src/components/molecules/CategorySelector.tsx index bee356f7..96ba2333 100644 --- a/front/src/components/molecules/CategorySelector.tsx +++ b/front/src/components/molecules/CategorySelector.tsx @@ -19,10 +19,8 @@ const CategorySelector: React.FC = ({ return (
- - 카테고리 - -
+ 카테고리 +
{categories?.map((category) => ( { - const chatInputValue = useChatInputStore((state) => state.inputValue); - const setChatInputValue = useChatInputStore((state) => state.setInputValue); - - const handleSendMessage = (e: React.FormEvent) => { - e.preventDefault(); - if (chatInputValue.trim() !== "") { - setChatInputValue(chatInputValue); - - const event = new CustomEvent("updateBalloonText", { - detail: chatInputValue, - }); - window.dispatchEvent(event); - - setChatInputValue(""); - } - }; - - const onChangeChat = () => { - // TODO: 채팅 내용 - }; - - return ( -
- - - - ); -}; - -export default ChatInput; diff --git a/front/src/components/molecules/FormField.tsx b/front/src/components/molecules/FormField.tsx index 4c78c35d..933f0474 100644 --- a/front/src/components/molecules/FormField.tsx +++ b/front/src/components/molecules/FormField.tsx @@ -9,6 +9,7 @@ type FormFieldProps = { placeholder?: string; onChange: (e: React.ChangeEvent) => void; message: string; + style?: string; }; const FormField: React.FC = ({ @@ -19,9 +20,10 @@ const FormField: React.FC = ({ onChange, message, name, + style, }) => { return ( -
+
= ({ label={label} placeholder={placeholder} onChange={onChange} + style={style} />
diff --git a/front/src/components/molecules/FriendItem.tsx b/front/src/components/molecules/FriendItem.tsx index 16896d83..05139667 100644 --- a/front/src/components/molecules/FriendItem.tsx +++ b/front/src/components/molecules/FriendItem.tsx @@ -3,19 +3,30 @@ import React from "react"; type FriendItemProps = { username: string; + email?: string; + userId: string; + selectecUserId: string; onAvatarClick: () => void; }; -const FriendItem: React.FC = ({ username, onAvatarClick }) => { +const FriendItem: React.FC = ({ + username, + email, + userId, + selectecUserId, + onAvatarClick, +}) => { return (
  • - {username} + {username}
  • ); }; diff --git a/front/src/components/molecules/PreviewServerMap.tsx b/front/src/components/molecules/PreviewServerMap.tsx deleted file mode 100644 index 647b4479..00000000 --- a/front/src/components/molecules/PreviewServerMap.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import useEmblaCarousel from "embla-carousel-react"; -import Autoplay from "embla-carousel-autoplay"; -import { useThemeStore } from "@/store/useThemeStore"; -import { useNavigate } from "react-router"; -import { ThemeType } from "@/types/server"; - -const PreviewServerMap = () => { - const [emblaRef] = useEmblaCarousel({ loop: true }, [Autoplay()]); - const setTheme = useThemeStore((state) => state.setTheme); - const navigate = useNavigate(); - - const onClickTheme = (selectedtheme: ThemeType, themeTitle: string) => { - setTheme(selectedtheme); - navigate(`/${themeTitle}`); - }; - - return ( -
    -
    -
    -
    - onClickTheme({ id: "SERVER_THEME_001", name: "숲" }, "camping") - }>
    -
    - onClickTheme({ id: "SERVER_THEME_002", name: "오피스" }, "beach") - }>
    -
    -
    -
    - ); -}; - -export default PreviewServerMap; diff --git a/front/src/components/molecules/ServerCardItem.tsx b/front/src/components/molecules/ServerCardItem.tsx index adf20334..4f6cf908 100644 --- a/front/src/components/molecules/ServerCardItem.tsx +++ b/front/src/components/molecules/ServerCardItem.tsx @@ -1,8 +1,10 @@ -import Avatar from "boring-avatars"; import React from "react"; import Text from "../atoms/Text"; +import FavoriteStar from "../atoms/BookmarkStar"; +import Button from "../atoms/Button"; type ServerCardItemProps = { + id: string; serverName: string; categories: { id: string; name: string }[] | undefined; }; @@ -10,18 +12,35 @@ type ServerCardItemProps = { const ServerCardItem: React.FC = ({ serverName, categories, + id, }) => { return ( -
    - - - - - {serverName} - {categories - ? ` - ${categories.map((category) => category.name).join(", ")}` - : ""} - +
    +
    +
    +
    +
    + + {serverName} + + +
    + + {categories && + `${categories.map((category) => category.name).join(", ")}`} + +
    + + 입장하기 + +
    ); }; diff --git a/front/src/components/molecules/ServerInfo.tsx b/front/src/components/molecules/ServerInfo.tsx index 4b6866f2..c8d4a44a 100644 --- a/front/src/components/molecules/ServerInfo.tsx +++ b/front/src/components/molecules/ServerInfo.tsx @@ -3,6 +3,8 @@ import Text from "../atoms/Text"; import Button from "../atoms/Button"; import { CategoriesType } from "@/types/server"; import FavoriteStar from "../atoms/BookmarkStar"; +import { useLoginStore } from "@/store/useLoginStore"; +import { leaveServer } from "@/api/server"; type ServerInfoProps = { hostName: string; @@ -19,22 +21,36 @@ const ServerInfo: React.FC = ({ onDelete, onEnter, }) => { + const { accessToken } = useLoginStore(); + + const handleOnLeaveServer = async () => { + try { + if (serverId && accessToken) { + await leaveServer(serverId, accessToken); + + console.log("서버탈퇴 성공"); + } + } catch (err) { + console.error(err); + } + }; return (
    - - {categories?.map((category) => category.name).join(", ")} - - - {hostName} + + 카테고리: {categories?.map((category) => category.name).join(", ")} + 서버장: {hostName} - - +
    ); }; diff --git a/front/src/components/molecules/ServerList.tsx b/front/src/components/molecules/ServerList.tsx index 92380969..1208a1dc 100644 --- a/front/src/components/molecules/ServerList.tsx +++ b/front/src/components/molecules/ServerList.tsx @@ -1,65 +1,70 @@ import useServers from "@/hooks/server/useServers"; import Avatar from "boring-avatars"; import Modal from "../organisms/Modal"; -import useModal from "@/hooks/common/useModal"; import ServerInfo from "./ServerInfo"; import { useState } from "react"; import { GetServerType } from "@/types/server"; import { deleteServer } from "@/api/server"; import { useLoginStore } from "@/store/useLoginStore"; import { useNavigate } from "react-router"; +import { useThemeStore } from "@/store/useThemeStore"; +import useModalStore from "@/store/useModalStore"; const ServerList: React.FC = () => { const { accessToken } = useLoginStore(); const { servers } = useServers(); - const { isOpen, closeModal, openModal } = useModal(); const [selectedServer, setSelectedServer] = useState(); const navigate = useNavigate(); + const { selectedTheme, setSelectedTheme } = useThemeStore(); + const { openModal, closeModal, modalType, isOpen } = useModalStore(); - console.log(servers); const openSelectedServer = (id: string) => { - openModal("server"); + openModal("showServer"); const filtered = servers?.filter((server) => server.id === id); setSelectedServer(filtered); }; const enterServerHandler = (id: string) => { navigate(`server/${id}`); - closeModal("server"); + closeModal(); + const server = servers?.find((server) => server.id === id); + if (server) { + setSelectedTheme(server.theme); + } }; const deleteServerHandler = (id: string) => { deleteServer(id, accessToken); - closeModal("server"); + closeModal(); }; return ( -
    +
    {servers?.map((server) => ( - openSelectedServer(server.id)} - /> + onClick={() => openSelectedServer(server.id)}> + + + {server.name} + +
    ))} - {selectedServer?.map((server) => ( - closeModal("server")}> - deleteServerHandler(server.id)} - onEnter={() => enterServerHandler(server.id)} - /> - - ))} + {isOpen && + modalType === "showServer" && + selectedServer?.map((server) => ( + + deleteServerHandler(server.id)} + onEnter={() => enterServerHandler(server.id)} + /> + + ))}
    ); }; diff --git a/front/src/components/molecules/SideBarHaeder.tsx b/front/src/components/molecules/SideBarHaeder.tsx index 83bea99f..578ec3f0 100644 --- a/front/src/components/molecules/SideBarHaeder.tsx +++ b/front/src/components/molecules/SideBarHaeder.tsx @@ -11,10 +11,14 @@ const SideBarHaeder: React.FC = ({ }) => { return (
    - {activeSideBar === "friends" ?

    Friends

    :

    Massages

    } + {activeSideBar === "friends" ? ( + Friends + ) : ( + Massages + )}
    ); diff --git a/front/src/components/molecules/TabHeader.tsx b/front/src/components/molecules/TabHeader.tsx index e5b43aea..b4bc2205 100644 --- a/front/src/components/molecules/TabHeader.tsx +++ b/front/src/components/molecules/TabHeader.tsx @@ -12,7 +12,7 @@ const TabHeader: React.FC = ({ onTabChange, }) => { return ( -
    +
    {tabs.map((tab) => ( = ({ }) => { return (
    - - 테마 - -
    + 맵 테마 +
    {themeObj.map((theme) => ( void; + email?: string | undefined; + selectedUserId: string | undefined; onLogout?: () => void; - onDeleteFriend?: () => void; onSendDM?: () => void; }; const UserProfile: React.FC = ({ nickname, + email, + selectedUserId, isCurrentUser, - onEditProfile, onLogout, - onDeleteFriend, onSendDM, }) => { + const navigate = useNavigate(); + const location = useLocation(); + const { closeModal } = useModalStore(); + const userId = Cookies.get("userId"); + const { accessToken } = useLoginStore(); + + const handleNavigate = () => { + navigate("/profile"); + }; + + const handleOnDeleteFriend = async () => { + try { + if (userId && selectedUserId) { + await deleteFriend(userId, selectedUserId, accessToken); + + console.log(userId, selectedUserId, accessToken); + } + console.log("친구 삭제 성공"); + } catch (error) { + throw new Error("친구 삭제에 실패했습니다."); + } + }; + + useEffect(() => { + if (location.pathname === "/profile") { + closeModal(); + } + }, [location.pathname, closeModal]); + return ( -
    -
    - - - {nickname} - -
    +
    +
    +
    + +
    + {nickname} + {email} +
    +
    -
    - {isCurrentUser ? ( - - ) : ( - - )} +
    + {isCurrentUser ? ( + + ) : ( + + )} +
    + +
    {isCurrentUser ? ( - + <> + + ) : ( - )} diff --git a/front/src/components/organisms/ChatBox.tsx b/front/src/components/organisms/ChatBox.tsx new file mode 100644 index 00000000..28ec619b --- /dev/null +++ b/front/src/components/organisms/ChatBox.tsx @@ -0,0 +1,105 @@ +import useChatInputStore from "@/store/useChatInputStore"; +import TextInput from "../atoms/TextInput"; +import { BsSend } from "react-icons/bs"; +import { useState } from "react"; +import { RiArrowDownWideFill, RiArrowUpWideFill } from "react-icons/ri"; + +const ChatBox = () => { + const chatInputValue = useChatInputStore((state) => state.inputValue); + const setChatInputValue = useChatInputStore((state) => state.setInputValue); + const addChatMessage = useChatInputStore((state) => state.addChatMessage); + const [expandChatBox, setExpandChatBox] = useState(true); + + // 챗 박스 드래그로 옮기기 + const [position, setPosition] = useState({ x: 100, y: 100 }); + const [dragging, setDragging] = useState(false); + const [offset, setOffset] = useState({ x: 0, y: 0 }); + + const handleMouseDown = (e: React.MouseEvent) => { + setDragging(true); + setOffset({ + x: e.clientX - position.x, + y: e.clientY - position.y, + }); + }; + + const handleMouseMove = (e: React.MouseEvent) => { + if (dragging) { + const newX = Math.max( + 0, + Math.min(window.innerWidth - 300, e.clientX - offset.x) + ); + const newY = Math.max( + 0, + Math.min(window.innerHeight - 500, e.clientY - offset.y) + ); + setPosition({ + x: newX, + y: newY, + }); + } + }; + + const handleMouseUp = () => { + setDragging(false); + }; + + const handleSendMessage = (e: React.FormEvent) => { + e.preventDefault(); + const message = chatInputValue.trim(); + if (message !== "") { + addChatMessage(message); + const event = new CustomEvent("chatMessage", { detail: message }); + window.dispatchEvent(event); + setChatInputValue(""); + } + }; + + const handleExpandChatBox = () => { + setExpandChatBox((prev) => !prev); + }; + + return ( +
    +
    + {expandChatBox ? ( + + ) : ( + + )} +
      + {/* TODO: 채팅내용 */} +
    • Hello
    • +
    +
    + setChatInputValue(e.target.value)} + name="chat" + /> + + +
    +
    + ); +}; + +export default ChatBox; diff --git a/front/src/components/organisms/CreateServerForm.tsx b/front/src/components/organisms/CreateServerForm.tsx index 0aa88099..00b56d40 100644 --- a/front/src/components/organisms/CreateServerForm.tsx +++ b/front/src/components/organisms/CreateServerForm.tsx @@ -8,11 +8,13 @@ import useUserProfile from "@/hooks/user/useUserProfile"; import { createServer } from "@/api/server"; import { useLoginStore } from "@/store/useLoginStore"; import { CreateServerRequest } from "@/types/server"; +import useModalStore from "@/store/useModalStore"; const CreateServerForm: React.FC = () => { const userId = Cookies.get("userId"); const { accessToken } = useLoginStore(); const { userProfile } = useUserProfile(); + const { closeModal } = useModalStore(); const [serverFormValues, setServerFormValues] = useState( { @@ -46,9 +48,11 @@ const CreateServerForm: React.FC = () => { const onSubmitForm = async (e: React.FormEvent) => { e.preventDefault(); + closeModal(); try { const response = await createServer(accessToken, serverFormValues); + return response; } catch (error) { console.error(error); @@ -56,11 +60,12 @@ const CreateServerForm: React.FC = () => { }; return ( -
    + onChangeServerValues("serverName", e.target.value)} message="" /> @@ -77,7 +82,7 @@ const CreateServerForm: React.FC = () => { }} selectedTheme={serverFormValues.theme} /> - + ); }; diff --git a/front/src/components/organisms/EditProfileForm.tsx b/front/src/components/organisms/EditProfileForm.tsx new file mode 100644 index 00000000..fdc19fe2 --- /dev/null +++ b/front/src/components/organisms/EditProfileForm.tsx @@ -0,0 +1,115 @@ +import { useEditProfileValidator } from "@/hooks/user/useEditProfileValidator"; +import React, { useEffect } from "react"; +import FormField from "../molecules/FormField"; +import Button from "../atoms/Button"; +import { useNavigate } from "react-router"; +import Cookies from "js-cookie"; +import { editUserProfile } from "@/api/user"; +import { useLoginStore } from "@/store/useLoginStore"; + +const EditProfileForm: React.FC<{ + userData: { email: string; username: string }; +}> = ({ userData }) => { + const navigate = useNavigate(); + const userId = Cookies.get("userId"); + const { accessToken } = useLoginStore(); + const { values, setValues, errors, validateFieldAndSetError, isFormValid } = + useEditProfileValidator(userData); + + useEffect(() => { + setValues({ + email: userData.email, + nickname: userData.username, + password: "", + newPassword: "", + }); + }, [userData, setValues]); + + const onChangeHandler = + (field: keyof typeof values) => + (event: React.ChangeEvent) => { + const value = event.target.value; + setValues((prevValues) => ({ ...prevValues, [field]: value })); + validateFieldAndSetError(field, value); + }; + + const handleEditProfile = async (e: React.FormEvent) => { + e.preventDefault(); + + if (isFormValid()) { + try { + const response = await editUserProfile( + userId || "", + accessToken, + values + ); + + console.log(response); + // TODO: CORS 문제 해결 필요 + // if (response) { + // console.log(response); + // navigate("/profile"); + // } + } catch (error) { + throw new Error("프로필 수정 실패"); + } + } else { + console.log(""); + } + }; + + return ( +
    + + + + + + + + + ); +}; + +export default EditProfileForm; diff --git a/front/src/components/organisms/FriendsSidebar.tsx b/front/src/components/organisms/FriendsSidebar.tsx index c2c52312..de1bc9a3 100644 --- a/front/src/components/organisms/FriendsSidebar.tsx +++ b/front/src/components/organisms/FriendsSidebar.tsx @@ -3,7 +3,11 @@ import React from "react"; import FriendItem from "../molecules/FriendItem"; type FriendsSideBarProps = { - onAvatarClick: () => void; + onAvatarClick: (friend: { + friendId: string; + username: string; + email: string; + }) => void; }; const FriendsSideBar: React.FC = ({ onAvatarClick }) => { @@ -12,13 +16,18 @@ const FriendsSideBar: React.FC = ({ onAvatarClick }) => { return (
      - {friends?.friends.map((friend) => ( - - ))} + {friends?.friends + .sort((a, b) => a.username.localeCompare(b.username)) + .map((friend) => ( + onAvatarClick(friend)} + /> + ))}
    ); diff --git a/front/src/components/organisms/Header.tsx b/front/src/components/organisms/Header.tsx index 34dd5669..3ad0b9db 100644 --- a/front/src/components/organisms/Header.tsx +++ b/front/src/components/organisms/Header.tsx @@ -3,12 +3,14 @@ import { BsChatQuoteFill } from "react-icons/bs"; import { RiGroup2Fill } from "react-icons/ri"; import RightSideBar from "./RightSideBar"; import { IoPersonAddSharp } from "react-icons/io5"; -import useModal from "@/hooks/common/useModal"; import Modal from "./Modal"; import AddFriendForm from "../molecules/AddFriendForm"; +import { useNavigate } from "react-router"; +import useModalStore from "@/store/useModalStore"; const Header: React.FC = () => { - const { isOpen, openModal, closeModal } = useModal(); + const navigate = useNavigate(); + const { isOpen, openModal, closeModal, modalType } = useModalStore(); const [activeSideBar, setActiveSideBar] = useState< "friends" | "messages" | null >(null); @@ -19,24 +21,26 @@ const Header: React.FC = () => { const handleAddFriend = () => openModal("addFriend"); return ( -