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 0000000..a0b1e3f Binary files /dev/null and b/src/assets/images/crew-empty.png differ 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 */} +
+
크루 소개
+
+