-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #200 from P1Z7/develop
Ver 2.0 배포 6차 (아티스트 페이지 모바일 호환성 & 관리자 페이지)
- Loading branch information
Showing
32 changed files
with
753 additions
and
148 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { useInfiniteQuery } from "@tanstack/react-query"; | ||
import classNames from "classnames"; | ||
import Image from "next/image"; | ||
import { FormEvent, useState } from "react"; | ||
import { useForm } from "react-hook-form"; | ||
import InputFile from "@/components/input/InputFile"; | ||
import { instance } from "@/api/api"; | ||
import useInfiniteScroll from "@/hooks/useInfiniteScroll"; | ||
import { makeImgUrlList } from "@/utils/changeImgUrl"; | ||
import { openToast } from "@/utils/toast"; | ||
|
||
const SIZE = 12; | ||
|
||
const ArtistReqList = () => { | ||
const [artistList, setArtistList] = useState<any[]>([]); | ||
const [isSubmitLoading, setIsSubmitLoading] = useState(false); | ||
const { data, fetchNextPage, isSuccess, isLoading } = useInfiniteQuery({ | ||
queryKey: ["admin_artist"], | ||
queryFn: async ({ pageParam }) => { | ||
const res = await instance.get("/users/new-artists", { size: SIZE, cursorId: pageParam }); | ||
pageParam === 500 ? setArtistList(res) : setArtistList((prev) => [...prev, ...res]); | ||
return res; | ||
}, | ||
initialPageParam: 500, | ||
getNextPageParam: (lastPage) => (lastPage.length < SIZE ? null : lastPage.at(-1)?.cursorId), | ||
}); | ||
const containerRef = useInfiniteScroll({ handleScroll: fetchNextPage, deps: [data] }); | ||
|
||
const { control, register, watch, getValues } = useForm(); | ||
const { artistProfile, artistName, birthday, option } = watch(); | ||
const isDisabled = !(artistProfile && artistProfile.length > 0 && artistName && birthday); | ||
|
||
const handleArtistSubmit = async (event: FormEvent) => { | ||
event.preventDefault(); | ||
try { | ||
setIsSubmitLoading(true); | ||
const imgUrl = await makeImgUrlList(artistProfile, instance); | ||
const res = | ||
option === "아티스트" | ||
? await instance.post("/artist", { artistImage: imgUrl[0], artistName, groups: [getValues("groupName")], birthday }) | ||
: await instance.post("/group", { groupImage: imgUrl[0], debutDate: birthday, groupName: artistName }); | ||
openToast.success("아티스트 등록 성공!"); | ||
} catch (err) { | ||
openToast.error("아티스트 등록 실패!"); | ||
} finally { | ||
setIsSubmitLoading(false); | ||
} | ||
}; | ||
|
||
return ( | ||
<div className="flex h-full w-full flex-col overflow-y-scroll pr-4"> | ||
<h1 className="text-20 font-600">아티스트/그룹 등록하기</h1> | ||
<form className="flex flex-col gap-8 text-black-white" onSubmit={handleArtistSubmit}> | ||
<p className="text-white-white">*그룹 / 아티스트를 선택해주세요!!!</p> | ||
<select className="rounded-sm p-8" {...register("option")}> | ||
<option>그룹</option> | ||
<option>아티스트</option> | ||
</select> | ||
<p className="text-white-white">*이미지 (필수)</p> | ||
<div className="flex gap-4"> | ||
<InputFile control={control} {...register("artistProfile")} /> | ||
<div className="relative h-120 w-120"> | ||
{artistProfile && <Image alt="등록 요청할 아티스트 이미지" src={URL.createObjectURL(artistProfile[0])} fill sizes="120, 120" className="object-cover" />} | ||
</div> | ||
</div> | ||
{option === "아티스트" && <input placeholder="그룹 ID(선택)" className="rounded-sm p-8" {...register("groupName")} />} | ||
<input placeholder={option === "아티스트" ? "아티스트 이름(필수)" : "그룹 이름(필수)"} className="rounded-sm p-8" {...register("artistName")} /> | ||
<p className="text-white-white">{option === "아티스트" ? "*생일(필수)" : "*데뷔일(필수)"}</p> | ||
<input type="date" placeholder="생일(필수)" className="rounded-sm p-8" {...register("birthday")} /> | ||
<button disabled={isDisabled || isSubmitLoading} className={classNames("rounded-sm bg-red px-12 py-8 text-white-white", { "!bg-gray-300": isDisabled || isSubmitLoading })}> | ||
등록 | ||
</button> | ||
</form> | ||
<div className="my-16 border border-white-white" /> | ||
<h1 className="text-20 font-600">아티스트 등록 요청 목록</h1> | ||
{isSuccess && | ||
(data.pages[0].length > 0 ? ( | ||
<div> | ||
{artistList.map(({ id, name }) => ( | ||
<div key={id}>{name}</div> | ||
))} | ||
</div> | ||
) : ( | ||
<p>아티스트 요청 데이터가 없습니다.</p> | ||
))} | ||
{isLoading && <p>로딩중...</p>} | ||
<div ref={containerRef} className="h-4" /> | ||
</div> | ||
); | ||
}; | ||
|
||
export default ArtistReqList; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { useInfiniteQuery } from "@tanstack/react-query"; | ||
import Link from "next/link"; | ||
import { useState } from "react"; | ||
import { instance } from "@/api/api"; | ||
import useInfiniteScroll from "@/hooks/useInfiniteScroll"; | ||
import { getSession } from "@/store/session/cookies"; | ||
import { openToast } from "@/utils/toast"; | ||
|
||
const SIZE = 12; | ||
|
||
const EventClaimList = () => { | ||
const [claimList, setClaimList] = useState<any[]>([]); | ||
const { data, fetchNextPage, isSuccess, isLoading } = useInfiniteQuery({ | ||
queryKey: ["admin_review_claim"], | ||
queryFn: async ({ pageParam }) => { | ||
const res = await instance.get("/users/events/all", { size: SIZE, cursorId: pageParam }); | ||
pageParam === 1 ? setClaimList(res) : setClaimList((prev) => [...prev, ...res]); | ||
return res; | ||
}, | ||
initialPageParam: 9999, | ||
getNextPageParam: (lastPage) => (lastPage.length < SIZE ? null : lastPage.at(-1)?.cursorId), | ||
}); | ||
const containerRef = useInfiniteScroll({ handleScroll: fetchNextPage, deps: [data] }); | ||
|
||
const session = getSession(); | ||
const deleteEvent = async (id: string) => { | ||
try { | ||
await instance.delete(`/event/${id}`, { userId: session?.user.userId }); | ||
openToast.success("이벤트 삭제 완료!"); | ||
} catch (error) { | ||
openToast.error("이벤트 삭제 실패!"); | ||
} | ||
}; | ||
|
||
return ( | ||
<div className="flex h-full w-full flex-col overflow-y-scroll pr-4"> | ||
{isSuccess && | ||
(data.pages[0].length > 0 ? ( | ||
claimList.map(({ id, claims }) => ( | ||
<div key={claims.id} className="flex justify-between gap-4 border-b border-white-white px-4 py-12"> | ||
<div className="flex flex-col gap-12"> | ||
<div> | ||
<p>이벤트 id: {id}</p> | ||
<p>신고 개수: {claims.length}</p> | ||
<Link href={`/event/${id}`} className="underline"> | ||
보러 가기 | ||
</Link> | ||
</div> | ||
{claims.map(({ content, user }: { content: string; user: { id: string; nickName: string } }) => ( | ||
<div className="border border-white-white p-8"> | ||
<p>신고 내용: {content}</p> | ||
<p>신고자: {user?.nickName}</p> | ||
</div> | ||
))} | ||
</div> | ||
<button onClick={() => deleteEvent(id)} className="rounded-sm bg-red p-4"> | ||
이벤트 삭제 | ||
</button> | ||
</div> | ||
)) | ||
) : ( | ||
<p>신고 데이터가 없습니다.</p> | ||
))} | ||
{isLoading && <p>로딩중...</p>} | ||
<div ref={containerRef} className="h-4" /> | ||
</div> | ||
); | ||
}; | ||
|
||
export default EventClaimList; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { FormEvent } from "react"; | ||
import { useForm } from "react-hook-form"; | ||
import { instance } from "@/api/api"; | ||
import { getSession } from "@/store/session/cookies"; | ||
import { openToast } from "@/utils/toast"; | ||
|
||
const EventList = () => { | ||
const { register, getValues, setValue } = useForm(); | ||
const session = getSession(); | ||
|
||
const submitDelete = async (event: FormEvent) => { | ||
event.preventDefault(); | ||
const id = getValues("eventId"); | ||
try { | ||
await instance.delete(`/event/${id}`, { userId: session?.user.userId }); | ||
openToast.success("삭제 완료!"); | ||
} catch (error) { | ||
openToast.error("존재하지 않는 이벤트입니다."); | ||
} finally { | ||
setValue("eventId", ""); | ||
} | ||
}; | ||
|
||
return ( | ||
<form className="flex w-full flex-col gap-8" onSubmit={submitDelete}> | ||
<input {...register("eventId")} className="rounded-sm p-12 text-gray-900" placeholder="이벤트 id 입력" /> | ||
<button type="submit" className="rounded-sm bg-red px-12 py-8"> | ||
삭제 | ||
</button> | ||
</form> | ||
); | ||
}; | ||
|
||
export default EventList; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { useStore } from "@/store/index"; | ||
import { AdminOptionType } from "@/store/slice/adminSlice"; | ||
|
||
const ADMIN_LIST = ["아티스트 요청 목록", "리뷰 신고 목록", "이벤트 신고 목록", "행사 삭제"] as AdminOptionType[]; | ||
|
||
const OptionList = () => { | ||
const { setOption } = useStore((state) => ({ setOption: state.setAdminOption })); | ||
|
||
return ( | ||
<div className="flex w-full max-w-[51rem] flex-col gap-12"> | ||
<span className="overflow-hidden whitespace-nowrap text-center text-20 font-600 text-red">WARNING! WARNING! WARNING! WARNING! WARNING!</span> | ||
{ADMIN_LIST.map((item) => ( | ||
<button key={item} type="button" onClick={() => setOption(item)} className="rounded-sm bg-black-white p-16 text-center text-white-black hover:bg-red"> | ||
{item} | ||
</button> | ||
))} | ||
<span className="overflow-hidden whitespace-nowrap text-center text-20 font-600 text-red">WARNING! WARNING! WARNING! WARNING! WARNING!</span> | ||
</div> | ||
); | ||
}; | ||
|
||
export default OptionList; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { useInfiniteQuery } from "@tanstack/react-query"; | ||
import { useState } from "react"; | ||
import { instance } from "@/api/api"; | ||
import useInfiniteScroll from "@/hooks/useInfiniteScroll"; | ||
import { getSession } from "@/store/session/cookies"; | ||
import { openToast } from "@/utils/toast"; | ||
|
||
const SIZE = 12; | ||
|
||
const ReviewClaimList = () => { | ||
const [claimList, setClaimList] = useState<any[]>([]); | ||
const { data, fetchNextPage, isSuccess, isLoading } = useInfiniteQuery({ | ||
queryKey: ["admin_review_claim"], | ||
queryFn: async ({ pageParam }) => { | ||
const res = await instance.get("/reviews/claims/all", { size: SIZE, cursorId: pageParam }); | ||
pageParam === 1 ? setClaimList(res) : setClaimList((prev) => [...prev, ...res]); | ||
return res; | ||
}, | ||
initialPageParam: 9999, | ||
getNextPageParam: (lastPage) => (lastPage.length < SIZE ? null : lastPage.at(-1)?.cursorId), | ||
}); | ||
const containerRef = useInfiniteScroll({ handleScroll: fetchNextPage, deps: [data] }); | ||
|
||
const session = getSession(); | ||
const deleteReview = async (id: string) => { | ||
try { | ||
await instance.delete(`/reviews/${id}/users/${session?.user.userId}`); | ||
openToast.success("후기 삭제 완료!"); | ||
} catch (error) { | ||
openToast.error("후기 삭제 실패!"); | ||
} | ||
}; | ||
|
||
return ( | ||
<div className="flex h-full w-full flex-col overflow-y-scroll pr-4"> | ||
{isSuccess && | ||
(data.pages[0].length > 0 ? ( | ||
claimList.map(({ id, claims }) => ( | ||
<div key={claims.id} className="flex justify-between gap-4 border-b border-white-white px-4 py-12"> | ||
<div className="flex flex-col gap-12"> | ||
<div> | ||
<p>후기 id: {id}</p> | ||
<p>신고 개수: {claims.length}</p> | ||
</div> | ||
{claims.map(({ content, user }: { content: string; user: { id: string; nickName: string } }) => ( | ||
<div className="border border-white-white p-8"> | ||
<p>신고 내용: {content}</p> | ||
<p>신고자: {user?.nickName}</p> | ||
</div> | ||
))} | ||
</div> | ||
<button onClick={() => deleteReview(id)} className="rounded-sm bg-red p-4"> | ||
후기 삭제 | ||
</button> | ||
</div> | ||
)) | ||
) : ( | ||
<p>신고 데이터가 없습니다.</p> | ||
))} | ||
{isLoading && <p>로딩중...</p>} | ||
<div ref={containerRef} className="h-4" /> | ||
</div> | ||
); | ||
}; | ||
|
||
export default ReviewClaimList; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
"use client"; | ||
|
||
import { useRouter } from "next/navigation"; | ||
import { FormEvent, useEffect, useState } from "react"; | ||
import { useForm } from "react-hook-form"; | ||
import MetaTag from "@/components/MetaTag"; | ||
import { useStore } from "@/store/index"; | ||
import { getSession } from "@/store/session/cookies"; | ||
import { META_TAG } from "@/constants/metaTag"; | ||
import ArtistReqList from "./_components/ArtistReqList"; | ||
import EventClaimList from "./_components/EventClaimList"; | ||
import EventList from "./_components/EventList"; | ||
import OptionList from "./_components/OptionList"; | ||
import ReviewClaimList from "./_components/ReviewClaimList"; | ||
|
||
const RENDER_ADMIN = { | ||
"선택 화면": <OptionList />, | ||
"아티스트 요청 목록": <ArtistReqList />, | ||
"리뷰 신고 목록": <ReviewClaimList />, | ||
"이벤트 신고 목록": <EventClaimList />, | ||
"행사 삭제": <EventList />, | ||
}; | ||
|
||
const Admin = () => { | ||
const router = useRouter(); | ||
const session = getSession(); | ||
const [isAdminLogin, setIsAdminLogin] = useState(false); | ||
const { isAuth, setIsAuth, option, setOption } = useStore((state) => ({ | ||
isAuth: state.isAdminAuth, | ||
setIsAuth: state.setIsAdminAuth, | ||
option: state.adminOption, | ||
setOption: state.setAdminOption, | ||
})); | ||
const { register, getValues } = useForm(); | ||
|
||
const handleSubmit = (event: FormEvent) => { | ||
event.preventDefault(); | ||
if (process.env.NEXT_PUBLIC_ADMIN_PW === getValues("adminPw")) setIsAuth(true); | ||
}; | ||
|
||
useEffect(() => { | ||
if (session?.user.userId === process.env.NEXT_PUBLIC_ADMIN_USERID) { | ||
setIsAdminLogin(true); | ||
} else router.replace("/404"); | ||
}, []); | ||
|
||
return ( | ||
<> | ||
<MetaTag title={META_TAG.admin.title} description={META_TAG.admin.description} /> | ||
{isAdminLogin && ( | ||
<div className="relative flex h-[calc(100vh-7.2rem)] items-center justify-center bg-black-white px-20 pb-12 pt-56 text-14 text-white-white pc:h-[calc(100vh-6.4rem)]"> | ||
{isAuth ? ( | ||
<> | ||
{option !== "선택 화면" && ( | ||
<button className="absolute left-20 top-20" onClick={() => setOption("선택 화면")}> | ||
뒤로가기 | ||
</button> | ||
)} | ||
{RENDER_ADMIN[option]} | ||
</> | ||
) : ( | ||
<form className="flex flex-col items-center gap-16" onSubmit={handleSubmit}> | ||
<p className="flex flex-col items-center gap-4"> | ||
<span className="text-20 font-600 text-red">WARNING!</span> | ||
인증이 필요한 페이지입니다. | ||
</p> | ||
<input {...register("adminPw")} type="text" className="rounded-sm bg-gray-50 p-12 text-gray-800" /> | ||
<button type="submit" className="hover:text-red"> | ||
확인 | ||
</button> | ||
</form> | ||
)} | ||
</div> | ||
)} | ||
</> | ||
); | ||
}; | ||
|
||
export default Admin; |
Oops, something went wrong.