From fffa9fc8139bdccda057627d675e93cffe852564 Mon Sep 17 00:00:00 2001 From: "lkh14011424@gmail.com" Date: Wed, 18 Sep 2024 08:58:25 +0900 Subject: [PATCH] =?UTF-8?q?[feat/#46]=20=EC=B4=88=EB=8C=80=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80,=20?= =?UTF-8?q?=EB=82=B4=20=EA=B7=B8=EB=A3=B9=20=EC=97=AC=EB=B6=80=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=9D=BC=20=ED=81=AC=EB=A3=A8=20=EC=83=9D=EC=84=B1,?= =?UTF-8?q?=20=EA=B0=80=EC=9E=85=20=EC=A0=9C=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Crew/CrewList.tsx | 154 +++++++++++++-------- src/components/Crew/CrewRanking.tsx | 85 ++++++++++++ src/components/Crew/MyCrew/CrewRanking.tsx | 6 +- src/components/Modal/InviteCrewModal.tsx | 29 +++- src/components/Modal/JoinCrewModal.tsx | 19 ++- src/components/Modal/Modals.tsx | 2 + src/components/Modal/ToWithdrawModal.tsx | 41 ++++++ src/components/ModalContainer.tsx | 2 +- src/pages/MyCrew.tsx | 12 +- 9 files changed, 280 insertions(+), 70 deletions(-) create mode 100644 src/components/Crew/CrewRanking.tsx create mode 100644 src/components/Modal/ToWithdrawModal.tsx diff --git a/src/components/Crew/CrewList.tsx b/src/components/Crew/CrewList.tsx index c650d9a..1bdf6fc 100644 --- a/src/components/Crew/CrewList.tsx +++ b/src/components/Crew/CrewList.tsx @@ -6,9 +6,11 @@ import { useModals } from "@/hooks/useModals" import useMyGroup from "@/hooks/useMyGroup" import CreateCrewIcon from "@assets/icons/crew-create-button-icon.svg?react" import SortCrewIcon from "@assets/icons/crew-sort-icon.svg?react" -import { ReactElement, useEffect, useRef, useState } from "react" +import { ReactElement, useCallback, useEffect, useRef, useState, useMemo } from "react" import { modals } from "../Modal/Modals" import MyCrewRankingContainer from "./MyCrew/MyCrewRankingContainer" +import { useNavigate, useSearchParams } from "react-router-dom" +import RoutePath from "@/constants/routes.json" const SORT_LIST = [ { sort: "userCount,desc", label: "크루원 많은 순" }, @@ -16,9 +18,11 @@ const SORT_LIST = [ ] const CrewList = (): ReactElement => { - const { myGroupData, ranks, myRank } = useMyGroup() + const navigate = useNavigate() + const { myGroupData, ranks, myRank, refetchAll } = useMyGroup() const [isDropdownOpen, setIsDropdownOpen] = useState(false) - console.log("myGroupData: ", myGroupData) + const [searchParams, setSearchParams] = useSearchParams() + const [params, setParams] = useState({ page: 0, size: 10000, @@ -26,75 +30,99 @@ const CrewList = (): ReactElement => { }) const { data, isLoading, isError, refetch } = useGetGroups(params) - const { openModal } = useModals() - const openCreateModal = (): void => { - openModal(modals.createCrewModal, { - onSubmit: () => { - refetch() - }, - }) - } - - const openJoinCrewModal = (id: number): void => { - openModal(modals.joinCrewModal, { - id, - onSubmit: () => { - console.log("open") - }, - }) - } + // openCreateModal 메모이제이션 + const openCreateModal = useCallback((): void => { + if (myGroupData) { + openModal(modals.ToWithdrawModal, { + onSubmit: () => { + navigate(RoutePath.MYCREW) + }, + }) + } else { + openModal(modals.createCrewModal, { + onSubmit: () => { + refetch() + refetchAll() + }, + }) + } + }, [myGroupData, navigate, openModal, refetch, refetchAll]) + + // openJoinCrewModal 메모이제이션 + const openJoinCrewModal = useCallback( + (id: number | undefined): void => { + openModal(modals.joinCrewModal, { + id, + onSubmit: () => { + console.log("open") + }, + }) + }, + [openModal] + ) - const openInviteModal = (): void => { + // openInviteModal 메모이제이션 + const openInviteModal = useCallback((): void => { openModal(modals.inviteCrewModal, { + id: Number(myGroupData?.id), onSubmit: () => { console.log("open") }, }) - } - - const dropdownRef = useRef(null) + }, [myGroupData, openModal]) + // toggleDropdown 함수 const toggleDropdown = (): void => { setIsDropdownOpen((prev) => !prev) } - const createSortList = (): JSX.Element[] => { + // createSortList 메모이제이션 + const createSortList = useMemo(() => { return SORT_LIST.map((s) => ( - // eslint-disable-next-line max-len
{ - setParams({ ...params, sort: s.sort as sort }) + setParams((prev) => ({ ...prev, sort: s.sort as sort })) setIsDropdownOpen(false) }} > {s.label}
)) - } - - const createGroupList = (_groups: group[] | undefined): JSX.Element | null => { - if (!_groups) return null + }, []) + + // createGroupList 메모이제이션 + const createGroupList = useCallback( + (_groups: group[] | undefined): JSX.Element | null => { + if (!_groups) return null + + if (_groups.length === 0) { + return ( +
+ empty crew +
+ {"만들어진 크루가 아직 없습니다."} +
+
+ ) + } - if (_groups.length === 0) { return ( -
- empty crew -
{"만들어진 크루가 아직 없습니다."}
+
+ {_groups.map((g) => ( + openJoinCrewModal(g.id)} /> + ))}
) - } - return ( -
- {_groups.map((g) => ( - openJoinCrewModal(g.id)} /> - ))} -
- ) - } + }, + [openJoinCrewModal] + ) + + // Dropdown 외부 클릭 감지 메모이제이션 + const dropdownRef = useRef(null) useEffect(() => { const handleClickOutside = (event: MouseEvent): void => { @@ -107,7 +135,21 @@ const CrewList = (): ReactElement => { return () => { document.removeEventListener("mousedown", handleClickOutside) } - }, [dropdownRef]) + }, []) + + // URL에서 groupId 추출 후 모달 열기 + useEffect(() => { + const groupId = searchParams.get("groupId") + + if (groupId) { + openJoinCrewModal(Number(groupId)) + const removeGroupIdFromUrl = (): void => { + searchParams.delete("groupId") + setSearchParams(searchParams) + } + removeGroupIdFromUrl() + } + }, [openJoinCrewModal, searchParams, setSearchParams]) return (
@@ -120,22 +162,22 @@ const CrewList = (): ReactElement => { openInviteModal={openInviteModal} /> )} + {/* header */}
전체크루 {isLoading ? "" : `(${data?.totalCount})`}
- {!myGroupData || - (myGroupData && Object.keys(myGroupData).length === 0 && ( -
- -
크루 만들기
-
- ))} + {(!myGroupData || (myGroupData && Object.keys(myGroupData).length === 0)) && ( +
+ +
크루 만들기
+
+ )}
{/* sort */} @@ -153,7 +195,7 @@ const CrewList = (): ReactElement => { : "pointer-events-none -translate-y-2 scale-95 opacity-0" }`} > - {createSortList()} + {createSortList}
diff --git a/src/components/Crew/CrewRanking.tsx b/src/components/Crew/CrewRanking.tsx new file mode 100644 index 0000000..f42b58d --- /dev/null +++ b/src/components/Crew/CrewRanking.tsx @@ -0,0 +1,85 @@ +import Crew1stCrownIcon from "@assets/icons/crew-1st-crown.svg?react" + +const RankPillar = ({ rank, name, score, height }: any) => { + const rankStyleMap: { gap: string; bgColor: string; fontSize: string }[] = [ + { + gap: "6", + bgColor: "#8BBAFE", + fontSize: "32px", + }, + { + gap: "6", + bgColor: "#DCEBFD", + fontSize: "22px", + }, + { + gap: "4", + bgColor: "#DCEBFD", + fontSize: "22px", + }, + ] + + const style = rankStyleMap[rank - 1] + + return ( +
+
+
+ {rank === 1 && } +
{rank}등
+
+
{name}
+
틀어짐 {score}회
+
+
+ ) +} + +const RankCard = ({ rank, name, score, isMe }: any) => ( +
+
+
{rank}
+ {isMe ? "나" : name} +
+ 자세경고 {score}회 +
+) + +const CrewRanking = ({ rankings, myRank }: { rankings: any[]; myRank: any }) => { + const topThree = rankings.slice(0, 3) + + return ( +
+ {/* 1, 2, 3등 랭킹 */} +
+ + + +
+ + {/* 전체 랭킹 목록 */} +
+
+
+ +
+
+ {rankings.map((rank, index) => ( + + ))} +
+
+
+
+ ) +} + +export default CrewRanking diff --git a/src/components/Crew/MyCrew/CrewRanking.tsx b/src/components/Crew/MyCrew/CrewRanking.tsx index f42b58d..e7ef7f7 100644 --- a/src/components/Crew/MyCrew/CrewRanking.tsx +++ b/src/components/Crew/MyCrew/CrewRanking.tsx @@ -60,9 +60,9 @@ const CrewRanking = ({ rankings, myRank }: { rankings: any[]; myRank: any }) =>
{/* 1, 2, 3등 랭킹 */}
- - - + {topThree[0] && } + {topThree[1] && } + {topThree[2] && }
{/* 전체 랭킹 목록 */} diff --git a/src/components/Modal/InviteCrewModal.tsx b/src/components/Modal/InviteCrewModal.tsx index b4e9832..d21174b 100644 --- a/src/components/Modal/InviteCrewModal.tsx +++ b/src/components/Modal/InviteCrewModal.tsx @@ -1,8 +1,27 @@ import { ModalProps } from "@/contexts/ModalsContext" import ModalContainer from "@components/ModalContainer" +import { useState } from "react" +import RoutePath from "@/constants/routes.json" -const InviteCrewModal = (props: ModalProps): React.ReactElement => { - const { onClose, onSubmit } = props +const InviteCrewModal = (props: ModalProps & { id: string }): React.ReactElement => { + const { onClose, id } = props + const [isCopied, setIsCopied] = useState(false) + + // 현재 URL을 기반으로 초대 링크 생성 + const currentUrl = `${window.location.protocol}//${window.location.hostname}${ + window.location.port ? `:${window.location.port}` : "" + }${RoutePath.CREW}?groupId=${id}` + + const handleCopy = (): void => { + navigator.clipboard + .writeText(currentUrl) + .then(() => { + setIsCopied(true) + }) + .catch((err) => { + console.error("Failed to copy: ", err) + }) + } return ( @@ -16,17 +35,17 @@ const InviteCrewModal = (props: ModalProps): React.ReactElement => {
아래 초대 링크를 복사해 크루에 초대해 보세요.
-
{"https://alignlab.site/"}
+
{currentUrl}
{/* button */} -
초대 링크가 복사되었어요.
+ {isCopied &&
초대 링크가 복사되었어요.
} ) diff --git a/src/components/Modal/JoinCrewModal.tsx b/src/components/Modal/JoinCrewModal.tsx index 4ec380c..ace8520 100644 --- a/src/components/Modal/JoinCrewModal.tsx +++ b/src/components/Modal/JoinCrewModal.tsx @@ -5,10 +5,11 @@ import { ReactNode, useState } from "react" import { ModalProps } from "@/contexts/ModalsContext" import { useGetGroup, useJoinGroup } from "@/hooks/useGroupMutation" import { groupJoinReq } from "@/api" +import useMyGroup from "@/hooks/useMyGroup" const JoinCrewModal = (props: ModalProps): React.ReactElement => { const { onClose, onSubmit, id } = props - + const { myGroupData } = useMyGroup() const [joinCode, setJoinCode] = useState("") const [isCodeError, setIsCodeError] = useState(false) @@ -17,7 +18,12 @@ const JoinCrewModal = (props: ModalProps): React.ReactElement => { const onChangeJoinCode = (e: React.ChangeEvent): void => { if (isCodeError) setIsCodeError(false) - if (e.target.value.length <= 4) setJoinCode(e.target.value) + + const { value } = e.target + // 숫자만 남기고 업데이트 + if (/^\d*$/.test(value)) { + if (value.length <= 4) setJoinCode(value) + } } const handleSubmit = (): void => { @@ -121,12 +127,17 @@ const JoinCrewModal = (props: ModalProps): React.ReactElement => { {/* button */} -
1개의 크루에만 가입할 수 있어요.
+ {myGroupData && ( +
1개의 크루에만 가입할 수 있어요.
+ )} )} diff --git a/src/components/Modal/Modals.tsx b/src/components/Modal/Modals.tsx index 10f0cbe..d99745b 100644 --- a/src/components/Modal/Modals.tsx +++ b/src/components/Modal/Modals.tsx @@ -4,12 +4,14 @@ import CreateCrewModal from "./CreateCrewModal" import InviteCrewModal from "./InviteCrewModal" import JoinCrewModal from "./JoinCrewModal" import WithdrawCrewModal from "./WithdrawCrewModal" +import ToWithdrawModal from "./ToWithdrawModal" export const modals = { createCrewModal: CreateCrewModal, inviteCrewModal: InviteCrewModal, joinCrewModal: JoinCrewModal, withdrawCrewModal: WithdrawCrewModal, + ToWithdrawModal: ToWithdrawModal, } const Modals = (): React.ReactNode => { diff --git a/src/components/Modal/ToWithdrawModal.tsx b/src/components/Modal/ToWithdrawModal.tsx new file mode 100644 index 0000000..4c619d8 --- /dev/null +++ b/src/components/Modal/ToWithdrawModal.tsx @@ -0,0 +1,41 @@ +import { ModalProps } from "@/contexts/ModalsContext" +import ModalContainer from "@components/ModalContainer" + +const ToWithdrawModal = (props: ModalProps): React.ReactElement => { + const { onClose, onSubmit } = props + + return ( + +
+ {/* header */} +
+
{"크루 만들기 불가"}
+
+ +
+
+ {"이미 속한 크루가 있어 크루를 만들 수 없어요.\n 탈퇴 후, 크루를 만들어주세요."} +
+
+ + {/* buttons */} +
+ + +
+
+
+ ) +} + +export default ToWithdrawModal diff --git a/src/components/ModalContainer.tsx b/src/components/ModalContainer.tsx index cd92083..bec8e6a 100644 --- a/src/components/ModalContainer.tsx +++ b/src/components/ModalContainer.tsx @@ -13,7 +13,7 @@ const ModalContainer: React.FC = ({ onClose, children }) => } // Modal이 main 안에서 절대적으로 위치하도록 변경 return ReactDOM.createPortal( -
+
{/* Close Button */}
-