Skip to content

Commit

Permalink
[feat/#46] 크루 가입, 크루 생성 API 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
lkhoony committed Sep 17, 2024
1 parent d78b82e commit 2acf3fc
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 46 deletions.
50 changes: 35 additions & 15 deletions src/api/group.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import qs from "qs"
import axiosInstance from "./axiosInstance"
import { AxiosError } from "axios"

export type sort = "userCount,desc" | "createdAt,desc"

Expand All @@ -10,15 +11,17 @@ export interface sortRes {
}

export interface group {
id: number
name: string
description: string
ownerUid: number
isHidden: boolean
joinCode: string
userCount: number
userCapacity: number
ranks: groupUserRank[]
id?: number
name?: string
description?: string
ownerUid?: number
ownerName?: string
isHidden?: boolean
joinCode?: string
userCount?: number
userCapacity?: number
hasJoined?: boolean
ranks?: groupUserRank[]
}

export interface groupUserRank {
Expand All @@ -45,7 +48,7 @@ export interface groupsRes {

export interface groupJoinReq {
groupId: number
joinCode: string
joinCode?: string
}

export interface groupJoinRes {
Expand Down Expand Up @@ -74,9 +77,14 @@ export const getGroup = async (id: number | undefined): Promise<group> => {

export const joinGroup = async (groupJoinReq: groupJoinReq): Promise<groupJoinRes> => {
try {
// eslint-disable-next-line max-len
const res = await axiosInstance.post(`groups/${groupJoinReq.groupId}/join`, { joinCode: groupJoinReq.joinCode })
return res.data
const res = await axiosInstance.post(
`groups/${groupJoinReq.groupId}/join`,
{}, // POST 요청에 body가 없다면 빈 객체 전달
{
params: groupJoinReq.joinCode ? { joinCode: groupJoinReq.joinCode } : {}, // query string으로 joinCode 전달
}
)
return res.data.data
} catch (e) {
throw e
}
Expand All @@ -86,8 +94,20 @@ export const checkGroupName = async (name: string): Promise<boolean> => {
try {
// eslint-disable-next-line max-len
const res = await axiosInstance.post(`groups/check`, { name })
const errorMessage = res.data?.errorMessage
return errorMessage ? false : true
const errorCode = res.data?.errorCode
return !errorCode
} catch (e) {
const { response } = e as AxiosError
const data = response?.data as { errorCode: string; reason: string }
if (data.errorCode) return false
throw e
}
}

export const createGroup = async (group: group): Promise<group> => {
try {
const res = await axiosInstance.post(`groups`, { ...group })
return res.data.data
} catch (e) {
throw e
}
Expand Down
7 changes: 5 additions & 2 deletions src/components/Crew/CrewItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,13 @@ const CrewItem = (props: CrewItemProps): ReactElement => {
</div>
{/* detail button */}
<button
className="flex w-[114px] cursor-pointer justify-center rounded-full bg-[#1A75FF] py-[6px] text-sm font-semibold text-white"
className={`flex w-[114px] justify-center rounded-full py-[6px] text-sm font-semibold text-white ${
group.hasJoined ? "bg-zinc-800" : group.userCapacity === group.userCount ? "bg-gray-200" : "bg-[#1A75FF]"
}`}
onClick={onClickDetail}
disabled={group.hasJoined || group.userCapacity === group.userCount}
>
크루 상세보기
{group.hasJoined ? "나의 크루" : "크루 상세보기"}
</button>
</div>
)
Expand Down
4 changes: 2 additions & 2 deletions src/components/Crew/CrewList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ const CrewList = (): ReactElement => {
sort: "userCount,desc",
})

const { data, isLoading, isError } = useGetGroups(params)
const { data, isLoading, isError, refetch } = useGetGroups(params)

const { openModal } = useModals()

const openCreateModal = (): void => {
openModal(modals.createCrewModal, {
onSubmit: () => {
console.log("open")
refetch()
},
})
}
Expand Down
73 changes: 66 additions & 7 deletions src/components/Modal/CreateCrewModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import CheckedIcon from "@assets/icons/crew-checked-icon.svg?react"
import UnCheckedIcon from "@assets/icons/crew-unckecked-icon.svg?react"
import { useState } from "react"
import { ModalProps } from "@/contexts/ModalsContext"
import { useCheckGroupName, useCreateGroup } from "@/hooks/useGroupMutation"
import { group } from "@/api"

type TPossible = "POSSIBLE" | "IMPOSSIBLE" | "NONCHECKED"

const CreateCrewModal = (props: ModalProps): React.ReactElement => {
const { onClose, onSubmit } = props
Expand All @@ -11,9 +15,14 @@ const CreateCrewModal = (props: ModalProps): React.ReactElement => {
const [description, setDescription] = useState<string>("")
const [isHidden, setIsHidden] = useState<boolean>(false)
const [joinCode, setJoinCode] = useState<string>("")
const [isPossible, setIsPossible] = useState<TPossible | null>(null)

const checkGroupNameMutation = useCheckGroupName()
const createGroupMutation = useCreateGroup()

const onChangeName = (e: React.ChangeEvent<HTMLInputElement>): void => {
setName(e.target.value)
setIsPossible(null)
}

const onChangeDescription = (e: React.ChangeEvent<HTMLTextAreaElement>): void => {
Expand All @@ -40,6 +49,38 @@ const CreateCrewModal = (props: ModalProps): React.ReactElement => {
setJoinCode("")
}

const onCheckGroupName = async (): Promise<void> => {
const _isPossible = await checkGroupNameMutation.mutateAsync(name)
setIsPossible(_isPossible ? "POSSIBLE" : "IMPOSSIBLE")
}

const getNameCheckedMsg = (_isPossible: TPossible | null): string => {
if (_isPossible === "POSSIBLE") return "사용가능한 크루명입니다."
if (_isPossible === "IMPOSSIBLE") return "이미 사용중인 크루명이에요. 다른 크루명을 사용해주세요."
if (_isPossible === "NONCHECKED") return "중복체크를 해주세요."
return ""
}

const canCreate = (): string | boolean => {
return name && description && ((isHidden && joinCode.length === 4) || !isHidden)
}

const handleSubmit = (): void => {
if (isPossible === null) {
setIsPossible("NONCHECKED")
return
}
if (isPossible === "NONCHECKED") return

let newGroup: group = { name, description }
if (isHidden) newGroup = { ...newGroup, joinCode, isHidden }
createGroupMutation.mutate(newGroup, {
onSuccess: (): void => {
if (onSubmit && typeof onSubmit === "function") onSubmit()
},
})
}

return (
<ModalContainer onClose={onClose}>
<div className="flex flex-col items-center">
Expand All @@ -48,22 +89,37 @@ const CreateCrewModal = (props: ModalProps): React.ReactElement => {
<div className="text-xl font-bold text-zinc-900">{"크루 만들기"}</div>
</div>

<div className="mb-6 flex w-full flex-col gap-5 text-[15px]">
<div className="mb-6 flex w-full flex-col text-[15px]">
{/* crew owner */}
<div className="flex flex-col gap-1">
<div className="mb-4 flex flex-col gap-1">
<div className="font-semibold text-[#1A75FF]">크루명</div>
<div className="flex gap-4">
<div className="mb-1 flex gap-4">
<input
type="text"
className={`w-full rounded-xl border border-gray-200 px-3 py-2 outline-none`}
className={`w-full rounded-xl border border-gray-200 px-3 py-2 outline-none ${
isPossible === "IMPOSSIBLE" || isPossible === "NONCHECKED" ? "border-red-500" : "border-gray-200"
}`}
value={name}
onChange={onChangeName}
placeholder="크루명을 입력해주세요."
/>
<button className="h-[44px] w-[116px] rounded-[33px] bg-[#1A75FF] px-[22px] py-1.5 text-sm font-semibold text-white">
<button
className={`h-[44px] w-[116px] rounded-[33px] px-[22px] py-1.5 text-sm font-semibold text-white ${
name.length === 0 || isPossible === "POSSIBLE" ? "bg-gray-200" : "bg-[#1A75FF]"
}`}
onClick={onCheckGroupName}
disabled={name.length === 0 || isPossible === "POSSIBLE"}
>
중복체크
</button>
</div>
<div
className={`h-[24px] text-sm font-semibold ${
isPossible === "POSSIBLE" ? "text-[#1A75FF]" : "text-red-500"
}`}
>
{getNameCheckedMsg(isPossible)}
</div>
</div>

{/* crew description */}
Expand Down Expand Up @@ -105,8 +161,11 @@ const CreateCrewModal = (props: ModalProps): React.ReactElement => {

{/* button */}
<button
className="w-[256px] rounded-[40px] bg-[#1A75FF] px-10 py-3 text-base font-semibold text-white"
onClick={onSubmit}
className={`w-[256px] rounded-[40px] px-10 py-3 text-base font-semibold text-white ${
canCreate() ? "bg-[#1A75FF]" : "bg-gray-200"
}`}
onClick={handleSubmit}
disabled={!canCreate()}
>
크루 만들기
</button>
Expand Down
1 change: 1 addition & 0 deletions src/components/Modal/InviteCrewModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const InviteCrewModal = (props: ModalProps): React.ReactElement => {
>
초대 링크 복사하기
</button>
<div className="mt-3 text-sm font-medium text-[#1A75FF]">초대 링크가 복사되었어요.</div>
</div>
</ModalContainer>
)
Expand Down
58 changes: 40 additions & 18 deletions src/components/Modal/JoinCrewModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import CrewJoinUserIcon from "@assets/icons/crew-join-user-icon.svg?react"
import PrivateCrewIcon from "@assets/icons/crew-private-icon.svg?react"
import { ReactNode, useState } from "react"
import { ModalProps } from "@/contexts/ModalsContext"
import { useGetGroup } from "@/hooks/useGroupMutation"
import { useGetGroup, useJoinGroup } from "@/hooks/useGroupMutation"
import { groupJoinReq } from "@/api"

const JoinCrewModal = (props: ModalProps): React.ReactElement => {
const { onClose, onSubmit, id } = props
Expand All @@ -12,21 +13,49 @@ const JoinCrewModal = (props: ModalProps): React.ReactElement => {
const [isCodeError, setIsCodeError] = useState<boolean>(false)

const { data, isLoading, isError } = useGetGroup(id)
const joinGroupMutation = useJoinGroup()

const onChangeJoinCode = (e: React.ChangeEvent<HTMLInputElement>): void => {
if (isCodeError) setIsCodeError(false)
if (e.target.value.length <= 4) setJoinCode(e.target.value)
}

const handleSubmit = (): void => {
if (!data?.id) return
let groupJoinReq: groupJoinReq = { groupId: data.id }
if (data?.isHidden) groupJoinReq = { ...groupJoinReq, joinCode }
joinGroupMutation.mutate(groupJoinReq, {
onSuccess: (): void => {
if (onSubmit && typeof onSubmit === "function") onSubmit()
},
onError: (e): void => {
console.log(e)
},
})
}

const createRank = (): ReactNode => {
if (!data?.ranks) return
if (data.ranks.length === 0) return
return (
<div className="flex gap-[7px]">
{data?.ranks.map((r) => (
<div className="flex h-[108px] w-[106px] flex-col items-center justify-center rounded-xl border border-gray-200 p-3">
<div className="mb-2 text-sm font-semibold text-zinc-700">{`${r.rank}등`}</div>
<div className="mb-1 max-w-full overflow-hidden text-ellipsis whitespace-nowrap text-lg font-semibold text-zinc-700">{`${r.name}`}</div>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<div className="text-[15px] font-semibold">오늘 바른자세 랭킹</div>
<div className="text-[14px] font-medium text-[#1A75FF]">크루에 가입하면 볼 수 있어요</div>
</div>
<div className="flex gap-[7px]">
<div className="flex gap-[7px]">
{data?.ranks.map((r, i) => (
<div
key={`join-modal-rank-${i}`}
className="flex h-[108px] w-[106px] flex-col items-center justify-center rounded-xl border border-gray-200 p-3"
>
<div className="mb-2 text-sm font-semibold text-zinc-700">{`${r.rank}등`}</div>
<div className="mb-1 max-w-full overflow-hidden text-ellipsis whitespace-nowrap text-lg font-semibold text-zinc-700">{`${r.name}`}</div>
</div>
))}
</div>
))}
</div>
</div>
)
}
Expand Down Expand Up @@ -57,7 +86,7 @@ const JoinCrewModal = (props: ModalProps): React.ReactElement => {
<div className="flex flex-col gap-1">
<div className="text-[15px] font-semibold">크루장</div>
<div className="rounded-xl border border-gray-200 bg-zinc-100 p-[12px] text-[15px] font-normal text-zinc-900">
{data?.ownerUid}
{data?.ownerName}
</div>
</div>

Expand All @@ -70,15 +99,7 @@ const JoinCrewModal = (props: ModalProps): React.ReactElement => {
</div>

{/* crew rank */}
{data?.ranks && (
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<div className="text-[15px] font-semibold">오늘 바른자세 랭킹</div>
<div className="text-[14px] font-medium text-[#1A75FF]">크루에 가입하면 볼 수 있어요</div>
</div>
<div className="flex gap-[7px]">{createRank()}</div>
</div>
)}
{createRank()}
</div>
) : (
// private crew
Expand All @@ -101,10 +122,11 @@ const JoinCrewModal = (props: ModalProps): React.ReactElement => {
{/* button */}
<button
className="w-[256px] rounded-[40px] bg-[#1A75FF] px-10 py-3 text-base font-semibold text-white"
onClick={onSubmit}
onClick={handleSubmit}
>
크루 가입하기
</button>
<div className="mt-3 text-sm font-medium text-red-500 ">1개의 크루에만 가입할 수 있어요.</div>
</div>
)}
</ModalContainer>
Expand Down
17 changes: 16 additions & 1 deletion src/hooks/useGroupMutation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useMutation, UseMutationResult, useQuery, UseQueryResult } from "@tanstack/react-query"
import {
checkGroupName,
createGroup,
getGroup,
getGroups,
group,
Expand All @@ -13,7 +14,10 @@ import {

export const useGetGroups = (params: groupsReq): UseQueryResult<groupsRes, Error> => {
// eslint-disable-next-line max-len
return useQuery<groupsRes, Error>({ queryKey: ["groups", params], queryFn: () => getGroups(params) })
return useQuery<groupsRes, Error>({
queryKey: ["groups", params.page, params.size, params.sort],
queryFn: () => getGroups(params),
})
}

export const useGetGroup = (id: number | undefined): UseQueryResult<group, Error> => {
Expand Down Expand Up @@ -42,3 +46,14 @@ export const useCheckGroupName = (): UseMutationResult<boolean, unknown, string,
},
})
}

export const useCreateGroup = (): UseMutationResult<group, unknown, group, unknown> => {
return useMutation({
mutationFn: (group: group) => {
return createGroup(group)
},
onSuccess: (data) => {
console.log(data)
},
})
}
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="h-fit min-h-screen bg-[#F9F9FD] px-28 py-12">
<Outlet />
</div>
)
Expand Down

0 comments on commit 2acf3fc

Please sign in to comment.