From 1276f3bd01b211e3c9cdde487b27c5938d68a9d7 Mon Sep 17 00:00:00 2001 From: "lkh14011424@gmail.com" Date: Mon, 9 Sep 2024 16:51:05 +0900 Subject: [PATCH] =?UTF-8?q?[feat/#46]=20=ED=81=AC=EB=A3=A8=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=ED=8D=BC=EB=B8=94=EB=A6=AC=EC=8B=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80,=20=EA=B8=80=EA=BC=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/group.ts | 74 ++++ src/api/index.ts | 1 + src/assets/icons/crew-checked-icon.svg | 4 + src/assets/icons/crew-create-button-icon.svg | 10 + src/assets/icons/crew-join-user-icon.svg | 4 + src/assets/icons/crew-private-icon.svg | 10 + src/assets/icons/crew-sort-icon.svg | 3 + src/assets/icons/crew-unckecked-icon.svg | 3 + src/assets/icons/crew-user-icon.svg | 4 + src/assets/icons/modal-close-icon.svg | 10 + src/assets/images/crew-empty.png | Bin 0 -> 9062 bytes src/components/Crew/CrewItem.tsx | 43 +++ src/components/Crew/CrewList.tsx | 372 +++++++++++++++++++ src/components/Modal.tsx | 30 ++ src/components/Modal/CreateCrewModal.tsx | 123 ++++++ src/components/Modal/InviteCrewModal.tsx | 42 +++ src/components/Modal/JoinCrewModal.tsx | 101 +++++ src/components/Modal/WithdrawCrewModal.tsx | 44 +++ src/components/index.ts | 1 + src/hooks/useGroupMutation.ts | 18 + src/layouts/BaseLayout.tsx | 3 +- src/pages/Crew.tsx | 8 +- src/pages/HomePage.tsx | 4 +- tailwind.config.js | 5 +- 24 files changed, 911 insertions(+), 6 deletions(-) create mode 100644 src/api/group.ts create mode 100644 src/assets/icons/crew-checked-icon.svg create mode 100644 src/assets/icons/crew-create-button-icon.svg create mode 100644 src/assets/icons/crew-join-user-icon.svg create mode 100644 src/assets/icons/crew-private-icon.svg create mode 100644 src/assets/icons/crew-sort-icon.svg create mode 100644 src/assets/icons/crew-unckecked-icon.svg create mode 100644 src/assets/icons/crew-user-icon.svg create mode 100644 src/assets/icons/modal-close-icon.svg create mode 100644 src/assets/images/crew-empty.png create mode 100644 src/components/Crew/CrewItem.tsx create mode 100644 src/components/Crew/CrewList.tsx create mode 100644 src/components/Modal.tsx create mode 100644 src/components/Modal/CreateCrewModal.tsx create mode 100644 src/components/Modal/InviteCrewModal.tsx create mode 100644 src/components/Modal/JoinCrewModal.tsx create mode 100644 src/components/Modal/WithdrawCrewModal.tsx create mode 100644 src/hooks/useGroupMutation.ts diff --git a/src/api/group.ts b/src/api/group.ts new file mode 100644 index 0000000..d58ee32 --- /dev/null +++ b/src/api/group.ts @@ -0,0 +1,74 @@ +import qs from "qs" +import axiosInstance from "./axiosInstance" + +export type sort = "userCount,desc" | "createdAt,desc" + +export interface sortRes { + empty: boolean + sorted: boolean + unsorted: boolean +} + +export interface group { + id: number + name: string + description: string + ownerUid: number + isHidden: boolean + joinCode: string + userCount: number + userCapacity: number + ranks: groupUserRank[] +} + +export interface groupUserRank { + groupUserId: number + name: string + rank: number + score: number +} + +export interface groupsReq { + page: number + size: number + sort: sort +} + +export interface groupsRes { + data: group[] + page: number + size: number + totalPage: number + totalCount: number + sort: sortRes +} + +export interface groupJoinReq { + groupId: number + joinCode: string +} + +export interface groupJoinRes { + groupId: number + uid: number + groupUserId: number +} + +export const getGroups = async (groupsReq: groupsReq): Promise => { + try { + const res = await axiosInstance.get(`/groups?${qs.stringify(groupsReq)}`) + return res.data + } catch (e) { + throw e + } +} + +export const joinGroup = async (groupJoinReq: groupJoinReq): Promise => { + try { + // eslint-disable-next-line max-len + const res = await axiosInstance.post(`groups/${groupJoinReq.groupId}/join`, { joinCode: groupJoinReq.joinCode }) + return res.data + } catch (e) { + throw e + } +} diff --git a/src/api/index.ts b/src/api/index.ts index ab24654..6554a24 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,2 +1,3 @@ export * from "./auth" export * from "./snapshot" +export * from "./group" diff --git a/src/assets/icons/crew-checked-icon.svg b/src/assets/icons/crew-checked-icon.svg new file mode 100644 index 0000000..45d8244 --- /dev/null +++ b/src/assets/icons/crew-checked-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/crew-create-button-icon.svg b/src/assets/icons/crew-create-button-icon.svg new file mode 100644 index 0000000..100a930 --- /dev/null +++ b/src/assets/icons/crew-create-button-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/crew-join-user-icon.svg b/src/assets/icons/crew-join-user-icon.svg new file mode 100644 index 0000000..f557696 --- /dev/null +++ b/src/assets/icons/crew-join-user-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/crew-private-icon.svg b/src/assets/icons/crew-private-icon.svg new file mode 100644 index 0000000..c3f34e8 --- /dev/null +++ b/src/assets/icons/crew-private-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/crew-sort-icon.svg b/src/assets/icons/crew-sort-icon.svg new file mode 100644 index 0000000..ae8c5c3 --- /dev/null +++ b/src/assets/icons/crew-sort-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/crew-unckecked-icon.svg b/src/assets/icons/crew-unckecked-icon.svg new file mode 100644 index 0000000..6ebc409 --- /dev/null +++ b/src/assets/icons/crew-unckecked-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/crew-user-icon.svg b/src/assets/icons/crew-user-icon.svg new file mode 100644 index 0000000..8bb83dd --- /dev/null +++ b/src/assets/icons/crew-user-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/modal-close-icon.svg b/src/assets/icons/modal-close-icon.svg new file mode 100644 index 0000000..6551b5e --- /dev/null +++ b/src/assets/icons/modal-close-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/images/crew-empty.png b/src/assets/images/crew-empty.png new file mode 100644 index 0000000000000000000000000000000000000000..a0b1e3f590a17ec25ae0c916c7b0b68113a281e1 GIT binary patch literal 9062 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 literal 0 HcmV?d00001 diff --git a/src/components/Crew/CrewItem.tsx b/src/components/Crew/CrewItem.tsx new file mode 100644 index 0000000..fa2fef9 --- /dev/null +++ b/src/components/Crew/CrewItem.tsx @@ -0,0 +1,43 @@ +import { group } from "@/api" +import PrivateCrewIcon from "@assets/icons/crew-private-icon.svg?react" +import CrewUserIcon from "@assets/icons/crew-user-icon.svg?react" +import { ReactElement } from "react" + +interface CrewItemProps { + group: group + onClickDetail: () => void +} + +const CrewItem = (props: CrewItemProps): ReactElement => { + const { group, onClickDetail } = props + + return ( +
+ {/* crew name */} +
+ {group.isHidden && } +
{group.name}
+
+ {/* crew user cnt */} +
+ +
{`${group.userCount}/${group.userCapacity}명`}
+
+ {/* detail button */} + +
+ ) +} + +export default CrewItem diff --git a/src/components/Crew/CrewList.tsx b/src/components/Crew/CrewList.tsx new file mode 100644 index 0000000..5f34095 --- /dev/null +++ b/src/components/Crew/CrewList.tsx @@ -0,0 +1,372 @@ +// import { useState, useRef, useEffect, ReactElement } from "react" +// import CreateCrewIcon from "@assets/icons/crew-create-button-icon.svg?react" +// import SortCrewIcon from "@assets/icons/crew-sort-icon.svg?react" +// import PrivateCrewIcon from "@assets/icons/crew-private-icon.svg?react" +// import CrewUserIcon from "@assets/icons/crew-user-icon.svg?react" +// import EmptyCrewImage from "@/assets/images/crew-empty.png" +// import { useGetGroups } from "@/hooks/useGroupMutation" +// import { group, groupsReq, sort } from "@/api" +// import CrewItem from "@/components/Crew/CrewItem" +// import JoinCrewModal from "@/components/Modal/JoinCrewModal" +// import CreateCrewModal from "@/components/Modal/CreateCrewModal" +// import InviteCrewModal from "@/components/Modal/InviteCrewModal" +// import WithdrawCrewModal from "@/components/Modal/WithdrawCrewModal" + +// const SORT_LIST = [ +// { sort: "userCount,desc", label: "크루원 많은 순" }, +// { sort: "createdAt,desc", label: "최신 생성 크루 순" }, +// ] + +// const CrewList = (): ReactElement => { +// const [sort, setSort] = useState(0) +// const [isDropdownOpen, setIsDropdownOpen] = useState(false) +// const [isJoinModalOpen, setIsJoinModalOpen] = useState(false) +// const [isCreateModalOpen, setIsCreateModalOpen] = useState(false) +// const [isInviteModalOpen, setIsInviteModalOpen] = useState(false) + +// const [params] = useState({ +// page: 0, +// size: 10, +// sort: "userCount,desc", +// }) + +// const { data, isLoading, isError } = useGetGroups(params) + +// const openJoinModal = (): void => setIsJoinModalOpen(true) +// const closeJoinModal = (): void => setIsJoinModalOpen(false) + +// const openCreateModal = (): void => setIsCreateModalOpen(true) +// const closeCreateModal = (): void => setIsCreateModalOpen(false) + +// const openInviteModal = (): void => setIsInviteModalOpen(true) +// const closeInviteModal = (): void => setIsInviteModalOpen(false) + +// const dropdownRef = useRef(null) + +// const toggleDropdown = (): void => { +// setIsDropdownOpen((prev) => !prev) +// } + +// const createSortList = (): JSX.Element[] => { +// return SORT_LIST.map((s, i) => ( +// // eslint-disable-next-line max-len +//
{ +// setSort(i) +// setIsDropdownOpen(false) +// }} +// > +// {s.label} +//
+// )) +// } + +// const createGroupList = (_groups: group[] | undefined): JSX.Element | null => { +// if (!_groups) return null + +// if (_groups.length === 0) { +// return ( +//
+// empty crew +//
{"만들어진 크루가 아직 없습니다."}
+//
+// ) +// } +// return ( +//
+// {_groups.map((g) => ( +// +// ))} +//
+// ) +// } + +// useEffect(() => { +// const handleClickOutside = (event: MouseEvent): void => { +// if (dropdownRef.current && !dropdownRef.current.contains(event.target as HTMLElement)) { +// setIsDropdownOpen(false) +// } +// } + +// document.addEventListener("mousedown", handleClickOutside) +// return () => { +// document.removeEventListener("mousedown", handleClickOutside) +// } +// }, [dropdownRef]) + +// return ( +//
+// {/* header */} +//
+//
전체크루(0)
+//
+// +//
크루 만들기
+//
+//
+ +// {/* sort */} +//
+//
+// +//
{SORT_LIST[sort].label}
+//
+ +// {/* dropdown */} +//
+// {createSortList()} +//
+//
+ +// {/* list */} +//
+//
+// {/* crew name */} +//
+// +//
{"개발자들 다 모여"}
+//
+// {/* crew user cnt */} +//
+// +//
{`10/30명`}
+//
+// {/* detail button */} +//
+// 크루 상세보기 +//
+//
+//
+ +// {isLoading ? "로딩 중입니다..." : isError ? "데이터를 불러오는데 실패했습니다." : createGroupList(data?.data)} +//
asas
+//
asas
+// +// +// +//
+// ) +// } + +// export default CrewList + +import { useState, useRef, useEffect, ReactElement } from "react" +import CreateCrewIcon from "@assets/icons/crew-create-button-icon.svg?react" +import SortCrewIcon from "@assets/icons/crew-sort-icon.svg?react" +import PrivateCrewIcon from "@assets/icons/crew-private-icon.svg?react" +import CrewUserIcon from "@assets/icons/crew-user-icon.svg?react" +import JoinCrewModal from "@/components/Modal/JoinCrewModal" +import CreateCrewModal from "@/components/Modal/CreateCrewModal" + +const SORT_LIST = [ + { sort: "userCount,desc", label: "크루원 많은 순" }, + { sort: "createdAt,desc", label: "최신 생성 크루 순" }, +] + +const CrewList = (): ReactElement => { + const [sort, setSort] = useState(0) + const [isDropdownOpen, setIsDropdownOpen] = useState(false) + const [isJoinModalOpen, setIsJoinModalOpen] = useState(false) + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false) + + const openJoinModal = (): void => setIsJoinModalOpen(true) + const closeJoinModal = (): void => setIsJoinModalOpen(false) + + const openCreateModal = (): void => setIsCreateModalOpen(true) + const closeCreateModal = (): void => setIsCreateModalOpen(false) + + const dropdownRef = useRef(null) + + const toggleDropdown = (): void => { + setIsDropdownOpen((prev) => !prev) + } + + const createSortList = (): JSX.Element[] => { + return SORT_LIST.map((s, i) => ( + // eslint-disable-next-line max-len +
{ + setSort(i) + setIsDropdownOpen(false) + }} + > + {s.label} +
+ )) + } + + useEffect(() => { + const handleClickOutside = (event: MouseEvent): void => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as HTMLElement)) { + setIsDropdownOpen(false) + } + } + + document.addEventListener("mousedown", handleClickOutside) + return () => { + document.removeEventListener("mousedown", handleClickOutside) + } + }, [dropdownRef]) + + return ( +
+ {/* header */} +
+
전체크루(0)
+
+ +
크루 만들기
+
+
+ + {/* sort */} +
+
+ +
{SORT_LIST[sort].label}
+
+ + {/* dropdown */} +
+ {createSortList()} +
+
+ + {/* list */} +
+
+ {/* crew name */} +
+ +
{"개발자들 다 모여"}
+
+ {/* crew user cnt */} +
+ +
{`10/30명`}
+
+ {/* detail button */} + +
+
+ {/* crew name */} +
+ +
{"개발자들 다 모여"}
+
+ {/* crew user cnt */} +
+ +
{`10/30명`}
+
+ {/* detail button */} + +
+
+ {/* crew name */} +
+ +
{"개발자들 다 모여"}
+
+ {/* crew user cnt */} +
+ +
{`10/30명`}
+
+ {/* detail button */} + +
+
+ {/* crew name */} +
+ +
{"개발자들 다 모여"}
+
+ {/* crew user cnt */} +
+ +
{`10/30명`}
+
+ {/* detail button */} + +
+
+ {}} /> + {}} /> +
+ ) +} + +export default CrewList diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx new file mode 100644 index 0000000..75dc593 --- /dev/null +++ b/src/components/Modal.tsx @@ -0,0 +1,30 @@ +import React from "react" +import ReactDOM from "react-dom" +import CloseIcon from "@assets/icons/modal-close-icon.svg?react" + +interface ModalProps { + isOpen: boolean + onClose: () => void + children: React.ReactNode +} + +const Modal: React.FC = ({ isOpen, onClose, children }) => { + if (!isOpen) return null + + // Modal이 main 안에서 절대적으로 위치하도록 변경 + return ReactDOM.createPortal( +
+
+ {/* Close Button */} + + {/* Modal Content */} + {children} +
+
, + document.getElementById("modal-root") as HTMLElement // main 안의 #modal-root + ) +} + +export default Modal diff --git a/src/components/Modal/CreateCrewModal.tsx b/src/components/Modal/CreateCrewModal.tsx new file mode 100644 index 0000000..8ea577c --- /dev/null +++ b/src/components/Modal/CreateCrewModal.tsx @@ -0,0 +1,123 @@ +import Modal from "@components/Modal" +import CheckedIcon from "@assets/icons/crew-checked-icon.svg?react" +import UnCheckedIcon from "@assets/icons/crew-unckecked-icon.svg?react" +import { useState } from "react" + +interface CreateCrewModalProps { + isOpen: boolean + onClose: () => void + onSubmit: () => void +} + +const CreateCrewModal = (props: CreateCrewModalProps): React.ReactElement => { + const { isOpen, onClose, onSubmit } = props + + const [name, setName] = useState("") + const [description, setDescription] = useState("") + const [isHidden, setIsHidden] = useState(false) + const [joinCode, setJoinCode] = useState("") + + const onChangeName = (e: React.ChangeEvent): void => { + setName(e.target.value) + } + + const onChangeDescription = (e: React.ChangeEvent): void => { + if (e.target.value.length <= 300) setDescription(e.target.value) + } + + // Enter 키 입력을 막는 함수 + const handleKeyPress = (e: React.KeyboardEvent): void => { + if (e.key === "Enter") { + e.preventDefault() // Enter 키 입력 방지 + } + } + + const onChangeJoinCode = (e: React.ChangeEvent): void => { + const { value } = e.target + // 숫자만 남기고 업데이트 + if (/^\d*$/.test(value)) { + if (value.length <= 4) setJoinCode(value) + } + } + + const onCheckIsHidden = (): void => { + setIsHidden(!isHidden) + setJoinCode("") + } + + return ( + +
+ {/* header */} +
+
{"크루 만들기"}
+
+ +
+ {/* crew owner */} +
+
크루명
+
+ + +
+
+ + {/* crew description */} +
+
크루 소개
+
+