Skip to content

Commit

Permalink
Merge pull request #200 from P1Z7/develop
Browse files Browse the repository at this point in the history
Ver 2.0 배포 6차 (아티스트 페이지 모바일 호환성 & 관리자 페이지)
  • Loading branch information
gw-lim authored Mar 19, 2024
2 parents d9d841e + a332190 commit 260abe4
Show file tree
Hide file tree
Showing 32 changed files with 753 additions and 148 deletions.
92 changes: 92 additions & 0 deletions app/(route)/admin/_components/ArtistReqList.tsx
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;
70 changes: 70 additions & 0 deletions app/(route)/admin/_components/EventClaimList.tsx
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;
34 changes: 34 additions & 0 deletions app/(route)/admin/_components/EventList.tsx
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;
22 changes: 22 additions & 0 deletions app/(route)/admin/_components/OptionList.tsx
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;
66 changes: 66 additions & 0 deletions app/(route)/admin/_components/ReviewClaimList.tsx
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;
79 changes: 79 additions & 0 deletions app/(route)/admin/page.tsx
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;
Loading

0 comments on commit 260abe4

Please sign in to comment.