diff --git a/app/(route)/admin/_components/ArtistReqList.tsx b/app/(route)/admin/_components/ArtistReqList.tsx new file mode 100644 index 00000000..89c23886 --- /dev/null +++ b/app/(route)/admin/_components/ArtistReqList.tsx @@ -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([]); + 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 ( +
+

아티스트/그룹 등록하기

+
+

*그룹 / 아티스트를 선택해주세요!!!

+ +

*이미지 (필수)

+
+ +
+ {artistProfile && 등록 요청할 아티스트 이미지} +
+
+ {option === "아티스트" && } + +

{option === "아티스트" ? "*생일(필수)" : "*데뷔일(필수)"}

+ + +
+
+

아티스트 등록 요청 목록

+ {isSuccess && + (data.pages[0].length > 0 ? ( +
+ {artistList.map(({ id, name }) => ( +
{name}
+ ))} +
+ ) : ( +

아티스트 요청 데이터가 없습니다.

+ ))} + {isLoading &&

로딩중...

} +
+
+ ); +}; + +export default ArtistReqList; diff --git a/app/(route)/admin/_components/EventClaimList.tsx b/app/(route)/admin/_components/EventClaimList.tsx new file mode 100644 index 00000000..14541848 --- /dev/null +++ b/app/(route)/admin/_components/EventClaimList.tsx @@ -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([]); + 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 ( +
+ {isSuccess && + (data.pages[0].length > 0 ? ( + claimList.map(({ id, claims }) => ( +
+
+
+

이벤트 id: {id}

+

신고 개수: {claims.length}

+ + 보러 가기 + +
+ {claims.map(({ content, user }: { content: string; user: { id: string; nickName: string } }) => ( +
+

신고 내용: {content}

+

신고자: {user?.nickName}

+
+ ))} +
+ +
+ )) + ) : ( +

신고 데이터가 없습니다.

+ ))} + {isLoading &&

로딩중...

} +
+
+ ); +}; + +export default EventClaimList; diff --git a/app/(route)/admin/_components/EventList.tsx b/app/(route)/admin/_components/EventList.tsx new file mode 100644 index 00000000..3aca851c --- /dev/null +++ b/app/(route)/admin/_components/EventList.tsx @@ -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 ( +
+ + +
+ ); +}; + +export default EventList; diff --git a/app/(route)/admin/_components/OptionList.tsx b/app/(route)/admin/_components/OptionList.tsx new file mode 100644 index 00000000..86051156 --- /dev/null +++ b/app/(route)/admin/_components/OptionList.tsx @@ -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 ( +
+ WARNING! WARNING! WARNING! WARNING! WARNING! + {ADMIN_LIST.map((item) => ( + + ))} + WARNING! WARNING! WARNING! WARNING! WARNING! +
+ ); +}; + +export default OptionList; diff --git a/app/(route)/admin/_components/ReviewClaimList.tsx b/app/(route)/admin/_components/ReviewClaimList.tsx new file mode 100644 index 00000000..a08d3368 --- /dev/null +++ b/app/(route)/admin/_components/ReviewClaimList.tsx @@ -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([]); + 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 ( +
+ {isSuccess && + (data.pages[0].length > 0 ? ( + claimList.map(({ id, claims }) => ( +
+
+
+

후기 id: {id}

+

신고 개수: {claims.length}

+
+ {claims.map(({ content, user }: { content: string; user: { id: string; nickName: string } }) => ( +
+

신고 내용: {content}

+

신고자: {user?.nickName}

+
+ ))} +
+ +
+ )) + ) : ( +

신고 데이터가 없습니다.

+ ))} + {isLoading &&

로딩중...

} +
+
+ ); +}; + +export default ReviewClaimList; diff --git a/app/(route)/admin/page.tsx b/app/(route)/admin/page.tsx new file mode 100644 index 00000000..2fc575ee --- /dev/null +++ b/app/(route)/admin/page.tsx @@ -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 = { + "선택 화면": , + "아티스트 요청 목록": , + "리뷰 신고 목록": , + "이벤트 신고 목록": , + "행사 삭제": , +}; + +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 ( + <> + + {isAdminLogin && ( +
+ {isAuth ? ( + <> + {option !== "선택 화면" && ( + + )} + {RENDER_ADMIN[option]} + + ) : ( +
+

+ WARNING! + 인증이 필요한 페이지입니다. +

+ + +
+ )} +
+ )} + + ); +}; + +export default Admin; diff --git a/app/(route)/artist/[artistId]/_components/MobileTab.tsx b/app/(route)/artist/[artistId]/_components/MobileTab.tsx new file mode 100644 index 00000000..81ee8b15 --- /dev/null +++ b/app/(route)/artist/[artistId]/_components/MobileTab.tsx @@ -0,0 +1,73 @@ +import SortButton from "@/(route)/search/_components/SortButton"; +import Image from "next/image"; +import { Dispatch, SetStateAction } from "react"; +import TimeFilter from "@/components/TimeFilter"; +import { MapCallbackType, MapVarType } from "@/hooks/useCustomMap"; +import { EventCardType } from "@/types/index"; +import SortIcon from "@/public/icon/sort.svg"; +import useArtistSheet from "../_hooks/useArtistSheet"; +import EventCard from "./EventCard"; + +interface Props { + mapVar: MapVarType; + mapCallback: MapCallbackType; + image: string; + name: string; + status: number; + setStatus: Dispatch>; + sort: "최신순" | "인기순"; + setSort: Dispatch>; + eventData: EventCardType[]; +} + +const MobileTab = ({ mapVar, mapCallback, image, name, status, setStatus, sort, setSort, eventData }: Props) => { + const isEmpty = eventData.length === 0; + + const { sheet, content } = useArtistSheet(); + + return ( +
+
+
+
+ 아티스트 이미지 +
+

+ {name} 행사 보기 +

+
+ +
+ + setSort("최신순")} selected={sort === "최신순"}> + 최신순 + + setSort("인기순")} selected={sort === "인기순"}> + 인기순 + +
+
+ {isEmpty ? ( +

행사가 없습니다.

+ ) : ( +
+ {eventData.map((event) => ( + mapCallback.handleCardClick(event)} + scrollRef={mapVar.selectedCard?.id === event.id ? mapCallback.scrollRefCallback : null} + /> + ))} +
+ )} +
+
+ ); +}; + +export default MobileTab; diff --git a/app/(route)/artist/[artistId]/_components/PcTab.tsx b/app/(route)/artist/[artistId]/_components/PcTab.tsx new file mode 100644 index 00000000..67425a53 --- /dev/null +++ b/app/(route)/artist/[artistId]/_components/PcTab.tsx @@ -0,0 +1,70 @@ +import SortButton from "@/(route)/search/_components/SortButton"; +import Image from "next/image"; +import { Dispatch, SetStateAction } from "react"; +import TimeFilter from "@/components/TimeFilter"; +import { MapCallbackType, MapVarType } from "@/hooks/useCustomMap"; +import { EventCardType } from "@/types/index"; +import SortIcon from "@/public/icon/sort.svg"; +import EventCard from "./EventCard"; + +interface Props { + mapVar: MapVarType; + mapCallback: MapCallbackType; + image: string; + name: string; + status: number; + setStatus: Dispatch>; + sort: "최신순" | "인기순"; + setSort: Dispatch>; + eventData: EventCardType[]; +} + +const PcTab = ({ mapVar, mapCallback, image, name, status, setStatus, sort, setSort, eventData }: Props) => { + const isEmpty = eventData.length === 0; + + return ( + <> + {mapVar.toggleTab && ( +
+
+
+ 아티스트 이미지 +
+

+ {name} 행사 보기 +

+
+ +
+ + setSort("최신순")} selected={sort === "최신순"}> + 최신순 + + setSort("인기순")} selected={sort === "인기순"}> + 인기순 + +
+
+ {isEmpty ? ( +

행사가 없습니다.

+ ) : ( +
+ {eventData.map((event) => ( + mapCallback.handleCardClick(event)} + scrollRef={mapVar.selectedCard?.id === event.id ? mapCallback.scrollRefCallback : null} + /> + ))} +
+ )} +
+
+ )} + + ); +}; + +export default PcTab; diff --git a/app/(route)/artist/[artistId]/_hooks/useArtistSheet.tsx b/app/(route)/artist/[artistId]/_hooks/useArtistSheet.tsx new file mode 100644 index 00000000..67442906 --- /dev/null +++ b/app/(route)/artist/[artistId]/_hooks/useArtistSheet.tsx @@ -0,0 +1,74 @@ +import { useCallback, useRef } from "react"; + +interface BottomSheetMetrics { + touchStart: number; + position: number; + isContentAreaTouched: boolean; +} + +const SNAP = { + top: 280, + bottom: 670, +}; + +const useArtistSheet = () => { + const metrics = useRef({ + touchStart: 0, + position: 0, + isContentAreaTouched: false, + }); + + const sheet = useCallback((node: HTMLElement | null) => { + if (node !== null) { + const handleTouchStart = (e: TouchEvent) => { + metrics.current.touchStart = e.touches[0].clientY; + }; + + const handleTouchMove = (e: TouchEvent) => { + const { touchStart, isContentAreaTouched, position } = metrics.current; + const currentTouch = e.touches[0]; + + if (!isContentAreaTouched) { + e.preventDefault(); + const touchOffset = currentTouch.clientY - touchStart + position; + node.style.setProperty("transform", `translateY(${touchOffset}px)`); + } + }; + + const handleTouchEnd = (e: TouchEvent) => { + const currentY = node.getBoundingClientRect().y; + + if (currentY > SNAP.bottom) { + node.style.setProperty("transform", `translateY(240px)`); + metrics.current.position = 240; + } else if (currentY < SNAP.bottom && currentY > SNAP.top) { + node.style.setProperty("transform", "translateY(0px)"); + metrics.current.position = 0; + } else if (currentY < SNAP.top) { + node.style.setProperty("transform", `translateY(-360px)`); + metrics.current.position = -360; + } + + metrics.current.isContentAreaTouched = false; + }; + + node.addEventListener("touchstart", handleTouchStart); + node.addEventListener("touchmove", handleTouchMove); + node.addEventListener("touchend", handleTouchEnd); + } + }, []); + + const content = useCallback((node: HTMLElement | null) => { + if (node !== null) { + const handleTouchStart = () => { + metrics.current.isContentAreaTouched = true; + }; + + node.addEventListener("touchstart", handleTouchStart); + } + }, []); + + return { sheet, content }; +}; + +export default useArtistSheet; diff --git a/app/(route)/artist/[artistId]/page.tsx b/app/(route)/artist/[artistId]/page.tsx index 52aa1fc8..ffab9a46 100644 --- a/app/(route)/artist/[artistId]/page.tsx +++ b/app/(route)/artist/[artistId]/page.tsx @@ -1,22 +1,19 @@ "use client"; -import SortButton from "@/(route)/search/_components/SortButton"; -import { useInfiniteQuery } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import Image from "next/image"; import { useParams } from "next/navigation"; import { useEffect, useState } from "react"; import KakaoMap from "@/components/KakaoMap"; -import TimeFilter from "@/components/TimeFilter"; +import MetaTag from "@/components/MetaTag"; import DottedLayout from "@/components/layout/DottedLayout"; import { instance } from "@/api/api"; import useCustomMap from "@/hooks/useCustomMap"; -import useInfiniteScroll from "@/hooks/useInfiniteScroll"; -import { getSession } from "@/store/session/cookies"; import { getArtist, getGroup } from "@/utils/getArtist"; import { Res_Get_Type } from "@/types/getResType"; import { SORT, STATUS, SortItem } from "@/constants/eventStatus"; -import SortIcon from "@/public/icon/sort.svg"; -import EventCard from "./_components/EventCard"; +import MobileTab from "./_components/MobileTab"; +import PcTab from "./_components/PcTab"; const SIZE = 9999; @@ -32,35 +29,19 @@ const ArtistIdPage = () => { const [sort, setSort] = useState(SORT[0]); const [status, setStatus] = useState(3); - const session = getSession(); - - const getArtistData = async ({ pageParam = 1 }) => { - const data: Res_Get_Type["eventSearch"] = await instance.get(`/event/artist/${artistId}`, { - sort, - size: SIZE, - page: pageParam, - status: STATUS[status], - userId: session?.user.userId ?? "", - artistId, - }); - return data; - }; - const { data: artistData, - fetchNextPage, isSuccess, refetch, - } = useInfiniteQuery({ - initialPageParam: 1, + } = useQuery({ queryKey: ["events", artistId], - queryFn: getArtistData, - getNextPageParam: (lastPage) => (lastPage.page * SIZE < lastPage.totalCount ? lastPage.page + 1 : null), - }); - - const containerRef = useInfiniteScroll({ - handleScroll: fetchNextPage, - deps: [artistData], + queryFn: () => + instance.get(`/event/artist/${artistId}`, { + sort, + size: SIZE, + status: STATUS[status], + artistId, + }), }); useEffect(() => { @@ -71,68 +52,39 @@ const ArtistIdPage = () => { if (!isSuccess) return; - const isEmpty = artistData.pages[0].eventList.length === 0; - const mapData = artistData.pages[0].eventList; + const eventData = artistData.eventList; return ( - -
-
- -
- - {mapVar.toggleTab && ( -
-
-
-
- 아티스트 이미지 -
-

- {name} 행사 보기 -

-
- -
- - setSort("최신순")} selected={sort === "최신순"}> - 최신순 - - setSort("인기순")} selected={sort === "인기순"}> - 인기순 - -
-
- {isEmpty ? ( -

행사가 없습니다.

- ) : ( -
- {artistData.pages.map((page) => - page.eventList.map((event) => ( - mapCallback.handleCardClick(event)} - scrollRef={mapVar.selectedCard?.id === event.id ? mapCallback.scrollRefCallback : null} - /> - )), - )} -
- )} -
-
+ <> + + +
+
+
- )} -
-
+ + + +
+ + ); }; diff --git a/app/(route)/event/[eventId]/_components/Banner.tsx b/app/(route)/event/[eventId]/_components/Banner.tsx index 07c4152a..5091a100 100644 --- a/app/(route)/event/[eventId]/_components/Banner.tsx +++ b/app/(route)/event/[eventId]/_components/Banner.tsx @@ -145,7 +145,7 @@ const Banner = ({ data, eventId }: Props) => { {hasEditApplication && }
-
+
수정하기 @@ -176,7 +176,7 @@ const MainDescription = ({ placeName, artists, eventType }: MainDescriptionProps return (
-

{placeName}

+

{placeName}

{formattedArtist} diff --git a/app/(route)/mypage/_components/UserProfile.tsx b/app/(route)/mypage/_components/UserProfile.tsx index c9d707fc..3897410e 100644 --- a/app/(route)/mypage/_components/UserProfile.tsx +++ b/app/(route)/mypage/_components/UserProfile.tsx @@ -3,7 +3,7 @@ import dynamic from "next/dynamic"; import Image from "next/image"; import { useAuth } from "@/hooks/useAuth"; -import { useBottomSheet } from "@/hooks/useBottomSheet"; +import useBottomSheet from "@/hooks/useBottomSheet"; import SettingList from "./SettingList"; const MyPageBottomSheet = dynamic(() => import("@/components/bottom-sheet/MyPageBottomSheet"), { ssr: false }); diff --git a/app/(route)/mypage/_components/tab/MyReviewTab/MyReview.tsx b/app/(route)/mypage/_components/tab/MyReviewTab/MyReview.tsx index 3dbe28d1..a3a3b455 100644 --- a/app/(route)/mypage/_components/tab/MyReviewTab/MyReview.tsx +++ b/app/(route)/mypage/_components/tab/MyReviewTab/MyReview.tsx @@ -6,7 +6,7 @@ import ControlMyDataBottomSheet from "@/components/bottom-sheet/ControlMyDataBot import KebabContents from "@/components/card/KebabContents"; import Chip from "@/components/chip/Chip"; import { instance } from "@/api/api"; -import { useBottomSheet } from "@/hooks/useBottomSheet"; +import useBottomSheet from "@/hooks/useBottomSheet"; import { formatAddress, formatDate } from "@/utils/formatString"; import { MyReviewType } from "@/types/index"; import HeartIcon from "@/public/icon/heart.svg"; diff --git a/app/(route)/page.tsx b/app/(route)/page.tsx index c9bb125c..d972242d 100644 --- a/app/(route)/page.tsx +++ b/app/(route)/page.tsx @@ -11,7 +11,7 @@ const Home = () => { <> -
+
diff --git a/app/(route)/post/_components/_inputs/MainInput.tsx b/app/(route)/post/_components/_inputs/MainInput.tsx index 636de551..76614885 100644 --- a/app/(route)/post/_components/_inputs/MainInput.tsx +++ b/app/(route)/post/_components/_inputs/MainInput.tsx @@ -2,7 +2,7 @@ import InitButton from "@/(route)/event/[eventId]/edit/_components/InitButton"; import dynamic from "next/dynamic"; import { useFormContext } from "react-hook-form"; import InputText from "@/components/input/InputText"; -import { useBottomSheet } from "@/hooks/useBottomSheet"; +import useBottomSheet from "@/hooks/useBottomSheet"; import { validateEdit } from "@/utils/editValidate"; import { handleEnterDown } from "@/utils/handleEnterDown"; import { PostType } from "../../page"; diff --git a/app/(route)/post/_components/_inputs/StarInput.tsx b/app/(route)/post/_components/_inputs/StarInput.tsx index a5f3d51d..5a5d8993 100644 --- a/app/(route)/post/_components/_inputs/StarInput.tsx +++ b/app/(route)/post/_components/_inputs/StarInput.tsx @@ -4,7 +4,7 @@ import { useEffect } from "react"; import { useFormContext } from "react-hook-form"; import EventTypeList from "@/components/bottom-sheet/content/EventTypeList"; import InputText from "@/components/input/InputText"; -import { useBottomSheet } from "@/hooks/useBottomSheet"; +import useBottomSheet from "@/hooks/useBottomSheet"; import useGetWindowWidth from "@/hooks/useGetWindowWidth"; import { useModal } from "@/hooks/useModal"; import { checkArrUpdate } from "@/utils/checkArrUpdate"; diff --git a/app/(route)/search/_components/Filter.tsx b/app/(route)/search/_components/Filter.tsx index 9d33ec71..5f8d4b36 100644 --- a/app/(route)/search/_components/Filter.tsx +++ b/app/(route)/search/_components/Filter.tsx @@ -37,7 +37,7 @@ const Filter = ({ visible, filter, resetFilter, sort, handleSort, status, handle return (
- diff --git a/app/(route)/search/_hooks/useSearch.tsx b/app/(route)/search/_hooks/useSearch.tsx index 953efd0d..5cd5387c 100644 --- a/app/(route)/search/_hooks/useSearch.tsx +++ b/app/(route)/search/_hooks/useSearch.tsx @@ -2,7 +2,7 @@ import dynamic from "next/dynamic"; import { ReadonlyURLSearchParams, usePathname, useRouter, useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; import { EVENTS } from "@/components/bottom-sheet/EventBottomSheet"; -import { useBottomSheet } from "@/hooks/useBottomSheet"; +import useBottomSheet from "@/hooks/useBottomSheet"; import { createQueryString } from "@/utils/handleQueryString"; import { EventType, GiftType, StatusType } from "@/types/index"; import { BIG_REGIONS } from "@/constants/regions"; diff --git a/app/(route)/search/page.tsx b/app/(route)/search/page.tsx index 21dc0853..3198b1bc 100644 --- a/app/(route)/search/page.tsx +++ b/app/(route)/search/page.tsx @@ -21,6 +21,7 @@ const SearchPage = () => {
diff --git a/app/_api/api.ts b/app/_api/api.ts index 3a01a5e1..6bfdaafb 100644 --- a/app/_api/api.ts +++ b/app/_api/api.ts @@ -125,10 +125,14 @@ export class Api { return res; } - async delete(endPoint: T, body?: DeleteBodyType) { + async delete(endPoint: T, queryObj?: any, body?: DeleteBodyType) { this.baseUrl = "/api" + endPoint; + if (queryObj) { + this.makeQueryString(queryObj); + } + + const newEndPoint = queryObj ? this.baseUrl + this.queryString : this.baseUrl; - const newEndPoint = this.baseUrl; const config = { method: "DELETE", body: JSON.stringify(body), @@ -141,7 +145,8 @@ export class Api { const refetchResult = await this.refetch(newEndPoint, config); return refetchResult; } - + const result = await res.json(); + this.makeError(result); return res; } } @@ -169,7 +174,9 @@ type GetEndPoint = | "artist/group/month" | `/event/user/${string}` | "/event/new" - | "artist/group/month"; + | "artist/group/month" + | "/users/new-artists" + | "/users/events/all"; type PostEndPoint = | "/event" | "/event/like" @@ -193,7 +200,14 @@ type PostEndPoint = | `/reviews/${string}/claims`; type DeleteEndPoint = `/users/${string}/artists` | `/reviews/${string}/images` | `/users/${string}` | "/auth" | `/event/${string}` | `/reviews/${string}/users/${string}`; -type PutEndPoint = `/event/${string}` | `/users/${string}/profile` | `/users/${string}/password` | `/users/${string}/artists` | "/users/password"; +type PutEndPoint = + | `/event/${string}` + | `/users/${string}/profile` + | `/users/${string}/password` + | `/users/${string}/artists` + | "/users/password" + | `/event/${string}` + | `/reviews/${string}/users/${string}`; type PostQueryType = T extends "/file/upload" ? { category: "event" | "artist" | "user" } : unknown; type PostBodyType = T extends "/event" @@ -272,7 +286,6 @@ type GetQueryType = T extends "/event" ? Req_Query_Type["닉네임"] : unknown; -// 사용하실 때 직접 추가 부탁드립니다! type PutBodyType = T extends `/event/${string}` ? Req_Post_Type["event"] : T extends `/users/${string}/profile` diff --git a/app/_components/bottom-sheet/BottomSheetMaterial.tsx b/app/_components/bottom-sheet/BottomSheetMaterial.tsx index bd8f8051..98057c8e 100644 --- a/app/_components/bottom-sheet/BottomSheetMaterial.tsx +++ b/app/_components/bottom-sheet/BottomSheetMaterial.tsx @@ -13,7 +13,7 @@ interface BottomSheetFrameProps { const BottomSheetFrame = forwardRef(({ children, closeBottomSheet }, ref) => { return ( -
+
e.stopPropagation()} diff --git a/app/_components/bottom-sheet/EventBottomSheet.tsx b/app/_components/bottom-sheet/EventBottomSheet.tsx index c30fc23e..a2a50858 100644 --- a/app/_components/bottom-sheet/EventBottomSheet.tsx +++ b/app/_components/bottom-sheet/EventBottomSheet.tsx @@ -23,7 +23,7 @@ const EventBottomSheet = ({ closeBottomSheet, refs, setEventFilter, selected }: 행사유형
{EVENTS.map((event) => ( - handleClick(event)} selected={selected === event}> + handleClick(event)} selected={selected === event} key={event}> {event} ))} diff --git a/app/_components/card/HorizontalEventCard.tsx b/app/_components/card/HorizontalEventCard.tsx index 7cd69cc6..6d7cd591 100644 --- a/app/_components/card/HorizontalEventCard.tsx +++ b/app/_components/card/HorizontalEventCard.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { SyntheticEvent, useState } from "react"; import HeartButton from "@/components/button/HeartButton"; import Chip from "@/components/chip/Chip"; -import { useBottomSheet } from "@/hooks/useBottomSheet"; +import useBottomSheet from "@/hooks/useBottomSheet"; import useLikeEvent from "@/hooks/useLikeEvent"; import { formatAddress, formatDate } from "@/utils/formatString"; import { EventCardType } from "@/types/index"; @@ -24,6 +24,10 @@ interface Props { const HorizontalEventCard = ({ data, onHeartClick, isGrow = false, isMypage = false, setDep }: Props) => { const formattedDate = formatDate(data.startDate, data.endDate); const formattedAddress = formatAddress(data.address); + const formattedTagsMobile = data.eventTags.sort((a, b) => TAG_ORDER[a.tagName] - TAG_ORDER[b.tagName]).slice(0, 5); + const extraTagNumberMobile = data.eventTags.length - 5 > 0 ? data.eventTags.length - 5 : 0; + const formattedTagsPc = data.eventTags.sort((a, b) => TAG_ORDER[a.tagName] - TAG_ORDER[b.tagName]).slice(0, 8); + const extraTagNumberPc = data.eventTags.length - 8 > 0 ? data.eventTags.length - 8 : 0; const { liked, likeCount, handleLikeEvent } = useLikeEvent({ eventId: data.id, initialLike: data.isLike, initialLikeCount: data.likeCount }); @@ -84,12 +88,17 @@ const HorizontalEventCard = ({ data, onHeartClick, isGrow = false, isMypage = fa {formattedDate} {formattedAddress}

-
    - {data.eventTags - .sort((a, b) => TAG_ORDER[a.tagName] - TAG_ORDER[b.tagName]) - .map((tag) => ( - - ))} +
      + {formattedTagsMobile.map((tag) => ( + + ))} + {!!extraTagNumberMobile && {`외 ${extraTagNumberMobile}개`}} +
    +
      + {formattedTagsPc.map((tag) => ( + + ))} + {!!extraTagNumberPc && {`외 ${extraTagNumberPc}개`}}
diff --git a/app/_components/header/PcHeader.tsx b/app/_components/header/PcHeader.tsx index efafdfb4..978664ff 100644 --- a/app/_components/header/PcHeader.tsx +++ b/app/_components/header/PcHeader.tsx @@ -3,21 +3,19 @@ import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; -import SearchInput from "@/components/input/SearchInput"; +import { ChangeEvent, FormEvent, useEffect, useRef, useState } from "react"; import { Session, getSession } from "@/store/session/cookies"; import AddIcon from "@/public/icon/add.svg"; import LogoIcon from "@/public/icon/logo.svg"; +import SearchIcon from "@/public/icon/search.svg"; const DEFAULT_PROFILE_SRC = "/icon/no-profile.svg"; const PcHeader = () => { - const router = useRouter(); const newSession = getSession(); const [session, setSession] = useState(); const profileHref = session ? "/mypage" : "/signin"; const profileSrc = session?.user.profileImage ?? DEFAULT_PROFILE_SRC; - const [keyword, setKeyword] = useState(""); useEffect(() => { if (!newSession) { @@ -27,13 +25,6 @@ const PcHeader = () => { setSession(newSession); }, [newSession?.user.profileImage]); - useEffect(() => { - if (!keyword) { - return; - } - router.push(`/search?sort=최신순&keyword=${keyword}`); - }, [keyword]); - return (
@@ -41,9 +32,7 @@ const PcHeader = () => {
-
- -
+ 행사 등록 @@ -58,3 +47,33 @@ const PcHeader = () => { }; export default PcHeader; + +const SearchInput = () => { + const [keyword, setKeyword] = useState(""); + const router = useRouter(); + + const handleChange = (e: ChangeEvent) => { + const nextValue = e.target.value; + setKeyword(nextValue); + }; + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + router.push(`/search?sort=최신순&keyword=${keyword}`); + setKeyword(""); + }; + + return ( +
+ + +
+ ); +}; diff --git a/app/_components/input/SearchInput.tsx b/app/_components/input/SearchInput.tsx index a3786b60..5f88b7cc 100644 --- a/app/_components/input/SearchInput.tsx +++ b/app/_components/input/SearchInput.tsx @@ -1,4 +1,4 @@ -import { usePathname, useRouter } from "next/navigation"; +import { usePathname } from "next/navigation"; import { Dispatch, SetStateAction, useEffect } from "react"; import { KeyboardEvent } from "react"; import { useForm } from "react-hook-form"; @@ -10,11 +10,9 @@ interface Props { setKeyword: Dispatch>; initialKeyword?: string; placeholder?: string; - size?: "lg" | "sm"; - href?: string; } -const SearchInput = ({ keyword, setKeyword, initialKeyword, href, placeholder = "검색어를 입력하세요.", size = "lg" }: Props) => { +const SearchInput = ({ keyword, setKeyword, initialKeyword, placeholder = "검색어를 입력하세요." }: Props) => { const { register, getValues, setValue, watch } = useForm({ defaultValues: { search: initialKeyword, @@ -29,13 +27,8 @@ const SearchInput = ({ keyword, setKeyword, initialKeyword, href, placeholder = } }; - const router = useRouter(); const handleSearchClick = () => { const newKeyword = getValues("search") ?? ""; - if (!newKeyword && href) { - router.push(href); - } - setKeyword(newKeyword); }; @@ -60,17 +53,17 @@ const SearchInput = ({ keyword, setKeyword, initialKeyword, href, placeholder =
- {search && ( - )} diff --git a/app/_constants/metaTag.ts b/app/_constants/metaTag.ts index bac24dd0..331f3ce7 100644 --- a/app/_constants/metaTag.ts +++ b/app/_constants/metaTag.ts @@ -52,6 +52,10 @@ export const META_TAG = { }, resetPassword: { title: "비밀번호 재설정", - description: "이메일 인증을 통해 비밀번호를 다시 설정하세요." - } + description: "이메일 인증을 통해 비밀번호를 다시 설정하세요.", + }, + admin: { + title: "관리자 페이지", + description: "관리자 기능 페이지입니다.", + }, }; diff --git a/app/_hooks/useBottomSheet.tsx b/app/_hooks/useBottomSheet.tsx index 63f748e1..3773c0ad 100644 --- a/app/_hooks/useBottomSheet.tsx +++ b/app/_hooks/useBottomSheet.tsx @@ -9,7 +9,7 @@ interface BottomSheetMetrics { isContentAreaTouched: boolean; } -export function useBottomSheet() { +const useBottomSheet = () => { const [bottomSheet, setBottomSheet] = useState(""); const openBottomSheet = (type: string) => { @@ -99,4 +99,6 @@ export function useBottomSheet() { }, [bottomSheet]); return { bottomSheet, openBottomSheet, closeBottomSheet, refs }; -} +}; + +export default useBottomSheet; diff --git a/app/_hooks/useCustomMap.tsx b/app/_hooks/useCustomMap.tsx index 5fa0473c..febe3445 100644 --- a/app/_hooks/useCustomMap.tsx +++ b/app/_hooks/useCustomMap.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; import { EventCardType } from "@/types/index"; const useCustomMap = () => { @@ -44,3 +44,16 @@ const useCustomMap = () => { }; export default useCustomMap; + +export interface MapVarType { + toggleTab: boolean; + setToggleTab: Dispatch>; + selectedCard: EventCardType | null; + setSelectedCard: Dispatch>; +} + +export interface MapCallbackType { + handleCardClick: (select: EventCardType) => void; + handleButtonClick: () => void; + scrollRefCallback: (el: HTMLDivElement) => void; +} diff --git a/app/_store/index.ts b/app/_store/index.ts index 7ae49783..b45f25d5 100644 --- a/app/_store/index.ts +++ b/app/_store/index.ts @@ -1,12 +1,14 @@ import { create } from "zustand"; +import { AdminSlice, createAdminSlice } from "./slice/adminSlice"; import { EventHeaderSlice, createEventHeaderSlice } from "./slice/eventHeaderSlice"; import { WarningSlice, createWarningSlice } from "./slice/warningSlice"; import { WriterSlice, createWriterSlice } from "./slice/writerIdSlice"; -type SliceType = WarningSlice & EventHeaderSlice & WriterSlice; +type SliceType = WarningSlice & EventHeaderSlice & WriterSlice & AdminSlice; export const useStore = create()((...a) => ({ ...createWarningSlice(...a), ...createEventHeaderSlice(...a), ...createWriterSlice(...a), + ...createAdminSlice(...a), })); diff --git a/app/_store/slice/adminSlice.ts b/app/_store/slice/adminSlice.ts new file mode 100644 index 00000000..56e8e3c7 --- /dev/null +++ b/app/_store/slice/adminSlice.ts @@ -0,0 +1,17 @@ +import { StateCreator } from "zustand"; + +export type AdminOptionType = "선택 화면" | "아티스트 요청 목록" | "리뷰 신고 목록" | "이벤트 신고 목록" | "행사 삭제"; + +export interface AdminSlice { + isAdminAuth: boolean; + setIsAdminAuth: (auth: boolean) => void; + adminOption: AdminOptionType; + setAdminOption: (selected: AdminOptionType) => void; +} + +export const createAdminSlice: StateCreator = (set) => ({ + isAdminAuth: false, + setIsAdminAuth: (auth) => set((state) => ({ ...state, isAdminAuth: auth })), + adminOption: "선택 화면", + setAdminOption: (selected) => set((state) => ({ ...state, adminOption: selected })), +}); diff --git a/app/layout.tsx b/app/layout.tsx index dbf408d4..a1bb6d2f 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -9,8 +9,6 @@ import PageLoading from "./_components/PageLoading"; import "./globals.css"; export const metadata = { - title: "Opener | K-pop 행사를 한 눈에", - description: "K-pop 팬을 위한 오프라인 행사 정보를 한 곳에서 쉽게 확인할 수 있는 웹사이트. 각종 카페 이벤트부터 팬광고, 포토부스 등 다양한 이벤트 정보를 한눈에 찾아보세요!", icons: { icon: "/icon/favicon.svg", }, diff --git a/robots.txt b/robots.txt new file mode 100644 index 00000000..d7cd58c9 --- /dev/null +++ b/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: /admin/ \ No newline at end of file