Skip to content

Commit

Permalink
[feat/#58] 나의 크루 기능 구현
Browse files Browse the repository at this point in the history
- 나의 크루 관련 API 연동 구현
- 컴포넌트 분리
- 대시보드 레이아웃 개선
  • Loading branch information
G-hoon committed Sep 17, 2024
1 parent 80aa6dd commit aa53760
Show file tree
Hide file tree
Showing 13 changed files with 269 additions and 94 deletions.
2 changes: 1 addition & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const App = (): React.ReactElement => {
return (
<QueryClientProvider client={queryClient}>
<ModalsProvider>
<Router></Router>
<Router />
</ModalsProvider>
</QueryClientProvider>
)
Expand Down
45 changes: 45 additions & 0 deletions src/api/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export interface groupUserRank {
score: number
}

export interface GroupUserRankData {
groupId: number
ranks: groupUserRank[]
}

export interface groupsReq {
page: number
size: number
Expand All @@ -54,6 +59,15 @@ export interface groupJoinRes {
groupUserId: number
}

export interface MyGroupData {
id: string
name: string
description: string
userCount: number
userCapacity: number
ownerNickname: string
}

export const getGroups = async (groupsReq: groupsReq): Promise<groupsRes> => {
try {
const res = await axiosInstance.get(`/groups?${qs.stringify(groupsReq)}`)
Expand Down Expand Up @@ -92,3 +106,34 @@ export const checkGroupName = async (name: string): Promise<boolean> => {
throw e
}
}

export const getGroupScores = async (groupdId: string | number): Promise<{ data: GroupUserRankData }> => {
try {
const res = await axiosInstance.get(`/group-scores?groupId=${groupdId}`)
return res.data
} catch (e) {
throw e
}
}

export const getMyGroup = async (): Promise<{ data: MyGroupData }> => {
try {
const res = await axiosInstance.get(`/groups/my-group`)
return res.data
} catch (e) {
throw e
}
}

export const withdrawMyGroup = async (id: string | number | undefined): Promise<any> => {
if (!id) {
throw new Error("잘못된 크루 id 입니다.")
}
try {
const res = await axiosInstance.delete(`/groups/${id}/withdraw`)
console.log("res: ", res)
return res
} catch (e) {
throw e
}
}
15 changes: 15 additions & 0 deletions src/assets/icons/crew-my-crew-leader-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 24 additions & 14 deletions src/components/Crew/CrewList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,23 @@ import { group, groupsReq, sort } from "@/api"
import EmptyCrewImage from "@/assets/images/crew-empty.png"
import CrewItem from "@/components/Crew/CrewItem"
import { useGetGroups } from "@/hooks/useGroupMutation"
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 { useModals } from "@/hooks/useModals"
import MyCrewRankingContainer from "./MyCrewRankingContainer"
import { modals } from "../Modal/Modals"
import MyCrewRankingContainer from "./MyCrew/MyCrewRankingContainer"

const SORT_LIST = [
{ sort: "userCount,desc", label: "크루원 많은 순" },
{ sort: "createdAt,desc", label: "최신 생성 크루 순" },
]

const CrewList = (): ReactElement => {
const [mode] = useState<"my" | "list">("my")
const { myGroupData, ranks, myRank } = useMyGroup()
const [isDropdownOpen, setIsDropdownOpen] = useState<boolean>(false)

console.log("myGroupData: ", myGroupData)
const [params, setParams] = useState<groupsReq>({
page: 0,
size: 10000,
Expand Down Expand Up @@ -110,22 +111,31 @@ const CrewList = (): ReactElement => {

return (
<div className="flex h-full w-full flex-col">
{mode === "my" && <MyCrewRankingContainer openCreateModal={openCreateModal} openInviteModal={openInviteModal} />}
{myGroupData && Object.keys(myGroupData).length > 0 && (
<MyCrewRankingContainer
myGroupData={myGroupData}
ranks={ranks}
myRank={myRank}
openCreateModal={openCreateModal}
openInviteModal={openInviteModal}
/>
)}
{/* header */}
<div className="mb-[24px] flex w-full items-center">
<div className="flex-grow text-[22px] font-bold text-zinc-900">
<span>전체크루</span>
<span>{isLoading ? "" : `(${data?.totalCount})`}</span>
</div>
{mode === "list" && (
<div
className="flex w-[138px] cursor-pointer items-center justify-center gap-[10px] rounded-[33px] bg-zinc-800 p-[10px] text-sm font-semibold text-white"
onClick={openCreateModal}
>
<CreateCrewIcon />
<div>크루 만들기</div>
</div>
)}
{!myGroupData ||
(myGroupData && Object.keys(myGroupData).length === 0 && (
<div
className="flex w-[138px] cursor-pointer items-center justify-center gap-[10px] rounded-[33px] bg-zinc-800 p-[10px] text-sm font-semibold text-white"
onClick={openCreateModal}
>
<CreateCrewIcon />
<div>크루 만들기</div>
</div>
))}
</div>

{/* sort */}
Expand Down
File renamed without changes.
21 changes: 21 additions & 0 deletions src/components/Crew/MyCrew/MyCrewHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import CreateCrewIcon from "@assets/icons/crew-create-button-icon.svg?react"

interface MyCrewHeaderProps {
openCreateModal: () => void
}

export default function MyCrewHeader(props: MyCrewHeaderProps) {
const { openCreateModal } = props
return (
<div className="mb-[24px] flex w-full items-center">
<div className="flex-grow text-[22px] font-bold text-zinc-900">나의 크루</div>
<div
className="flex w-[138px] cursor-pointer items-center justify-center gap-[10px] rounded-[33px] bg-zinc-800 p-[10px] text-sm font-semibold text-white"
onClick={openCreateModal}
>
<CreateCrewIcon />
<div>크루 만들기</div>
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -1,37 +1,35 @@
import CreateCrewIcon from "@assets/icons/crew-create-button-icon.svg?react"
import SendInvitationIcon from "@assets/icons/crew-send-invitation.svg?react"
import CrewUserIcon from "@assets/icons/crew-user-icon.svg?react"
import { Link } from "react-router-dom"
import CrewRanking from "./CrewRanking"

import RoutePath from "@/constants/routes.json"
import MyCrewHeader from "./MyCrewHeader"
import { groupUserRank, MyGroupData } from "@/api"
import dayjs from "dayjs"

interface MyCrewRankingContainerProps {
myGroupData: MyGroupData
ranks: groupUserRank[]
myRank: groupUserRank | undefined
openCreateModal: () => void
openInviteModal: () => void
}

export default function MyCrewRankingContainer(props: MyCrewRankingContainerProps) {
const { openCreateModal, openInviteModal } = props
const { myGroupData, ranks, myRank, openCreateModal, openInviteModal } = props
return (
<div className="mb-12">
<div className="mb-[24px] flex w-full items-center">
<div className="flex-grow text-[22px] font-bold text-zinc-900">나의 크루</div>
<div
className="flex w-[138px] cursor-pointer items-center justify-center gap-[10px] rounded-[33px] bg-zinc-800 p-[10px] text-sm font-semibold text-white"
onClick={openCreateModal}
>
<CreateCrewIcon />
<div>크루 만들기</div>
</div>
</div>
<MyCrewHeader openCreateModal={openCreateModal} />
<div className="h-[382px] w-full rounded-[12px] bg-white px-8 pt-9">
<div className="flex justify-between">
<div className="flex items-center gap-4">
<div className="text-sm font-bold">주인공 다 모여랏</div>
<div className="text-sm font-bold">{myGroupData.name}</div>
<div className="flex items-center gap-1">
<CrewUserIcon />
<span className="text-sm font-medium text-zinc-500">7/30명</span>
<span className="text-sm font-medium text-zinc-500">
{myGroupData.userCount}/{myGroupData.userCapacity}
</span>
</div>
<button
className="rounded-full border-[1px] border-solid border-gray-200 bg-white"
Expand All @@ -54,30 +52,14 @@ export default function MyCrewRankingContainer(props: MyCrewRankingContainerProp
<div className="flex gap-2 text-[13px] font-normal text-zinc-400">
<span>최근 1시간</span>
<span>|</span>
<span>2024.08.20 21:00 기준</span>
<span>{dayjs().format("YYYY.MM.DD HH:mm")} 기준</span>
</div>
</div>
</div>

{/* 랭킹 표시 */}
<div className="mt-4 h-[220px]">
<CrewRanking
rankings={[
{ rank: 1, name: "뚝딱이", score: 1 },
{ rank: 2, name: "뚝딱이2", score: 5 },
{ rank: 3, name: "뚝딱이3", score: 10 },
{ rank: 4, name: "뚝딱이4", score: 11 },
{ rank: 5, name: "뚝딱이4", score: 11 },
{ rank: 6, name: "뚝딱이4", score: 16 },
{ rank: 7, name: "뚝딱이4", score: 17 },
{ rank: 8, name: "뚝딱이4", score: 22 },
{ rank: 9, name: "뚝딱이4", score: 26 },
{ rank: 10, name: "뚝딱이4", score: 32 },
{ rank: 11, name: "뚝딱이4", score: 40 },
// ... more rankings
]}
myRank={{ rank: 12, name: "나", score: 50 }}
/>
{ranks.length > 0 && myRank && <CrewRanking rankings={ranks} myRank={myRank} />}
</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/SideNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export default function SideNav(): React.ReactElement {
<nav className="mt-10">
<ul>
{navItems.map(({ icon: Icon, label, link }) => {
const isActive = location.pathname === link
const isActive = location.pathname.includes(link)
return (
<li key={label} className={`mb-1 rounded-r-md ${isActive ? "bg-gray-700" : "hover:bg-gray-700"}`}>
<Link to={link} className={`nav-item flex w-full items-center p-3 ${isActive ? "active" : ""}`}>
Expand Down
76 changes: 76 additions & 0 deletions src/hooks/useMyGroup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { getGroupScores, getMyGroup, groupUserRank, MyGroupData, withdrawMyGroup } from "@/api"
import { useAuthStore } from "@/store"
import { useMyGroupStore } from "@/store/MyGroup"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { useCallback, useEffect, useMemo } from "react"

export default function useMyGroup() {
const queryClient = useQueryClient()
const myName = useAuthStore((state) => state.user.nickname)
const { myGroupData, setMyGroupData } = useMyGroupStore()

// Fetch group data
const { data, isLoading, error } = useQuery<{ data: MyGroupData } | null, Error>({
queryKey: ["myGroup"],
queryFn: getMyGroup,
staleTime: 60 * 1000,
retry: false,
})

// Fetch group scores
const myGroupScoresQuery = useQuery<{ data: { ranks: groupUserRank[] } }, Error>({
queryKey: ["groupScores", myGroupData?.id],
queryFn: () => getGroupScores(myGroupData!.id),
enabled: !!myGroupData?.id,
staleTime: 60 * 1000, // Consider data fresh for 1 minute
})

useEffect(() => {
if (data) {
setMyGroupData(data.data)
}
}, [data, setMyGroupData])

const myRank = useMemo(() => {
return myGroupScoresQuery.data?.data.ranks.find((item) => item.name === myName)
}, [myGroupScoresQuery.data, myName])

const refetchAll = () => {
queryClient.invalidateQueries({ queryKey: ["myGroup"] })
queryClient.invalidateQueries({ queryKey: ["groupScores"] })
}

const withdrawMutation = useMutation({
mutationFn: withdrawMyGroup,
onSuccess: (res) => {
if (res.status === 204) {
setMyGroupData(null)
queryClient.setQueryData(["myGroup"], null)
queryClient.removeQueries({ queryKey: ["groupScores"] })
}
},
})

const withdrawFromGroup = useCallback(async () => {
if (myGroupData) {
try {
await withdrawMutation.mutateAsync(myGroupData.id)
return { success: true }
} catch (error) {
console.error("Error during group withdrawal:", error)
return { success: false, error }
}
}
return { success: false, error: new Error("No group data available") }
}, [myGroupData, withdrawMutation])

return {
myGroupData: myGroupData ?? data?.data,
ranks: myGroupScoresQuery.data?.data.ranks ?? [],
myRank,
isLoading: isLoading || myGroupScoresQuery.isLoading,
error: error || myGroupScoresQuery.error,
withdrawFromGroup,
refetchAll,
}
}
2 changes: 1 addition & 1 deletion src/layouts/AnalysisLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Outlet } from "react-router-dom"

export default function AnalysisLayout() {
return (
<div className="h-full bg-[#F9F9FD] px-28 py-12">
<div className="min-h-full bg-[#F9F9FD] px-28 py-12">
<Outlet />
</div>
)
Expand Down
4 changes: 4 additions & 0 deletions src/pages/AnalysisDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import TurtleNeckImage from "@/assets/images/tutle-neck.png"
import PoseAnalysisChart from "@/components/Dashboard/Chart"
import Datepicker, { DateValueType } from "react-tailwindcss-datepicker"

const START_FROM = new Date()
START_FROM.setMonth(START_FROM.getMonth() - 1)

const AnalysisDashboard = () => {
const carouselRef = useRef(null)
const [currentIndex, setCurrentIndex] = useState(0)
Expand Down Expand Up @@ -140,6 +143,7 @@ const AnalysisDashboard = () => {
<div className="text-sm text-gray-600">
<Datepicker
inputClassName="w-[270px] py-2 rounded-full bg-zinc-800 text-white px-[24px]"
startFrom={START_FROM}
maxDate={new Date()}
value={dateRange}
onChange={(value) => setDateRange(value)}
Expand Down
Loading

0 comments on commit aa53760

Please sign in to comment.