diff --git a/src/app/actions.ts b/src/app/actions.ts index 1bd0c900..188ee322 100644 --- a/src/app/actions.ts +++ b/src/app/actions.ts @@ -1,6 +1,7 @@ 'use server' import { cookies } from 'next/headers' +import { revalidateTag } from 'next/cache' const setCookie = async (name: string, value: string) => { cookies().set(name, value) @@ -10,4 +11,8 @@ const deleteCookie = async (name: string) => { cookies().delete(name) } -export { setCookie, deleteCookie } +const revalidatePlaces = async (mapId: string) => { + revalidateTag(`places-${mapId}`) +} + +export { setCookie, deleteCookie, revalidatePlaces } diff --git a/src/app/map/[mapId]/place-list-bottom-sheet.tsx b/src/app/map/[mapId]/place-list-bottom-sheet.tsx index ad21055b..98816ffb 100644 --- a/src/app/map/[mapId]/place-list-bottom-sheet.tsx +++ b/src/app/map/[mapId]/place-list-bottom-sheet.tsx @@ -13,6 +13,7 @@ import { APIError } from '@/models/api/index' import type { PlaceType } from '@/models/api/place' import { useInfiniteScroll } from '@/hooks/use-infinite-scroll' import { api } from '@/utils/api' +import { revalidatePlaces } from '@/app/actions' interface PlaceListBottomSheetProps { places: PlaceType[] | null @@ -30,16 +31,17 @@ const PlaceListBottomSheet = forwardRef< { places, mapId, selectedFilter, onClickFilterButton, onRefreshOldPlace }, ref, ) => { - const [placeList, setPlaceList] = useState(places || []) + const [placeList, setPlaceList] = useState([]) const { data: slicedPlaceList, listRef } = useInfiniteScroll({ totalData: places || [], itemsPerPage: 10, }) - const { data: user, revalidate } = useFetch(api.users.me.get, { + const { data: user } = useFetch(api.users.me.get, { key: ['user'], }) const userId = user?.id + const numOfSelectedFilter = (selectedFilter?.category !== 'all' ? 1 : 0) + (selectedFilter?.tags.length ?? 0) @@ -77,7 +79,7 @@ const PlaceListBottomSheet = forwardRef< placeId: place.place.id, }) } - revalidate(['places', mapId]) + revalidatePlaces(mapId) } catch (error) { if (error instanceof APIError) { notify.error(error.message) diff --git a/src/app/my-map/[mapId]/crew-info-list.tsx b/src/app/my-map/[mapId]/crew-info-list.tsx index ac39b32e..2e49a383 100644 --- a/src/app/my-map/[mapId]/crew-info-list.tsx +++ b/src/app/my-map/[mapId]/crew-info-list.tsx @@ -5,36 +5,13 @@ import type { User } from '@/models/user' import cn from '@/utils/cn' import CrewInfoReadOnlyItem from './crew-info-read-only-item' import CrewInfoEditableItem from './crew-info-editable-item' +import { getColorForName } from '@/utils/avatar-color' interface CrewInfoListProps extends ClassName { user: User mapInfo: MapInfo refetchMapInfo: VoidFunction } -const memberColors = [ - 'coral', - 'dark-blue', - 'sky-blue', - 'violet', - 'green', -] as const -type AvatarColor = (typeof memberColors)[number] - -const hashString = (str: string): number => { - let hash = 0 - for (let i = 0; i < str.length; i++) { - hash = str.charCodeAt(i) + ((hash << 5) - hash) - } - return Math.abs(hash) -} - -const getColorForName = (name: string): AvatarColor => { - const hash = hashString(name) - - const colorIndex = hash % memberColors.length - return memberColors[colorIndex] -} - const CrewInfoList = ({ mapInfo, className, diff --git a/src/app/my-map/[mapId]/page.tsx b/src/app/my-map/[mapId]/page.tsx index 83acfd0f..b6a5db6d 100644 --- a/src/app/my-map/[mapId]/page.tsx +++ b/src/app/my-map/[mapId]/page.tsx @@ -39,6 +39,7 @@ const MyMap = ({ params: { mapId } }: { params: { mapId: string } }) => { refetchMapInfo={refetch} /> { const router = useRouter() + const pathname = usePathname() + + if (pathname.includes('register')) { + return <>{children} + } return (
diff --git a/src/app/place/[placeId]/like-tooltip.tsx b/src/app/place/[placeId]/like-tooltip.tsx index b5f680d0..c96cff76 100644 --- a/src/app/place/[placeId]/like-tooltip.tsx +++ b/src/app/place/[placeId]/like-tooltip.tsx @@ -4,6 +4,7 @@ import { AnimatePresence, motion } from 'framer-motion' import cn from '@/utils/cn' import type { ClassName } from '@/models/common' import Avatar from '@/components/common/avatar' +import { getColorForName } from '@/utils/avatar-color' interface LikeToolTipProps extends ClassName { likeMembers: string[] @@ -70,6 +71,7 @@ const LikeToolTip = ({ likeMembers, className, onClick }: LikeToolTipProps) => { { const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) const [isRecentlyLike, setIsRecentlyLike] = useState(null) const router = useSafeRouter() - const [isAlreadyPick, setIsAlreadyPick] = useState(place.isRegisteredPlace) + const isAlreadyPick = place.isRegisteredPlace const { userLocation } = useUserGeoLocation() const isAllowPosition = allowUserPositionStorage.getValueOrNull() const diffDistance = getDistance( @@ -50,10 +51,9 @@ const PlaceBox = ({ place, mapId }: PlaceBoxProps) => { place.x, ) - const { data: user, revalidate } = useFetch(api.users.me.get, { + const { data: user } = useFetch(api.users.me.get, { key: ['user'], }) - const { data: mapInfo, isFetching } = useFetch(() => api.maps.id.get(mapId), { enabled: !!mapId, }) @@ -76,6 +76,10 @@ const PlaceBox = ({ place, mapId }: PlaceBoxProps) => { return likedUserCount + recentlyLikedBonus })() + useEffect(() => { + router.refresh() + }, [router]) + useEffect(() => { if (!place || !user) return setIsLikePlace( @@ -93,7 +97,7 @@ const PlaceBox = ({ place, mapId }: PlaceBoxProps) => { placeId: place.id, mapId, }) - revalidate(['places', mapId]) + revalidatePlaces(mapId) } catch (error) { setIsLikePlace(false) setIsRecentlyLike( @@ -115,7 +119,7 @@ const PlaceBox = ({ place, mapId }: PlaceBoxProps) => { placeId: place.id, mapId, }) - revalidate(['places', mapId]) + revalidatePlaces(mapId) } catch (error) { setIsLikePlace(true) setIsRecentlyLike( @@ -136,9 +140,8 @@ const PlaceBox = ({ place, mapId }: PlaceBoxProps) => { mapId, }) - setIsAlreadyPick(false) + revalidatePlaces(mapId) setIsDeleteModalOpen(false) - window.location.reload() } catch (error) { if (error instanceof APIError || error instanceof Error) { notify.error(error.message) @@ -154,7 +157,6 @@ const PlaceBox = ({ place, mapId }: PlaceBoxProps) => { label: 'register', }) router.push(`/place/${place.kakaoId}/register`) - revalidate(['places', mapId]) } catch (error) { if (error instanceof APIError || error instanceof Error) { notify.error(error.message) diff --git a/src/app/place/[placeId]/place-liked-users.tsx b/src/app/place/[placeId]/place-liked-users.tsx index 749d04a9..1ef92206 100644 --- a/src/app/place/[placeId]/place-liked-users.tsx +++ b/src/app/place/[placeId]/place-liked-users.tsx @@ -4,6 +4,7 @@ import type { LikeUser } from '@/components/place/types' import type { ClassName } from '@/models/common' import type { User } from '@/models/user' import { getMapId } from '@/services/map-id' +import { getColorForName } from '@/utils/avatar-color' import cn from '@/utils/cn' import { useRouter } from 'next/navigation' @@ -32,6 +33,7 @@ const PlaceLikedUser = ({ likedUser, className, me }: PlaceLikedUserProps) => { diff --git a/src/app/place/[placeId]/register/register-box.tsx b/src/app/place/[placeId]/register/register-box.tsx index 61b95a8f..5ea95768 100644 --- a/src/app/place/[placeId]/register/register-box.tsx +++ b/src/app/place/[placeId]/register/register-box.tsx @@ -15,7 +15,7 @@ import type { TagItem } from '@/models/api/maps' import type { PlaceDetail } from '@/models/api/place' import { api } from '@/utils/api' import get조사 from '@/utils/조사' -import useFetch from '@/hooks/use-fetch' +import { revalidatePlaces } from '@/app/actions' const toTagNames = (tags: TagItem[]): TagItem['name'][] => tags.map((tag) => tag.name) @@ -29,7 +29,6 @@ const RegisterBox = ({ tags: TagItem[] mapId: string }) => { - const { revalidate } = useFetch() const router = useSafeRouter() const [selectedTags, setSelectedTags] = useState([]) const [isOpenBackModal, setIsOpenBackModal] = useState(false) @@ -46,7 +45,8 @@ const RegisterBox = ({ tagNames: toTagNames(selectedTags), }) - revalidate(['map-list']) + revalidatePlaces(mapId) + notify.success('맛집 등록이 완료되었습니다.') router.safeBack({ defaultHref: `/place/${place.kakaoId}` }) } catch (error) { diff --git a/src/app/profile/[mapId]/[id]/page.tsx b/src/app/profile/[mapId]/[id]/page.tsx index d7a627f5..f633df7c 100644 --- a/src/app/profile/[mapId]/[id]/page.tsx +++ b/src/app/profile/[mapId]/[id]/page.tsx @@ -12,6 +12,7 @@ import { useState } from 'react' import LikedPlacePanel from './liked-place-panel' import RegisterededPlacePanel from './registered-place-panel' import type { MapInfo } from '@/models/map' +import { getColorForName } from '@/utils/avatar-color' type PlaceFilter = 'register' | 'liked' @@ -49,6 +50,7 @@ const Profile = ({ {userData?.nickname} diff --git a/src/app/profile/[mapId]/[id]/place-item.tsx b/src/app/profile/[mapId]/[id]/place-item.tsx index 09adff37..008a4747 100644 --- a/src/app/profile/[mapId]/[id]/place-item.tsx +++ b/src/app/profile/[mapId]/[id]/place-item.tsx @@ -19,6 +19,7 @@ import { api } from '@/utils/api' import cn from '@/utils/cn' import { roundOnePoint } from '@/utils/number' import { getStarByScore } from '@/utils/score' +import { revalidatePlaces } from '@/app/actions' interface PlaceItemProps extends ClassName { selectedPlace: PlaceType @@ -29,7 +30,7 @@ interface PlaceItemProps extends ClassName { const PlaceItem = forwardRef( ({ selectedPlace, className, mapId, onRefreshOldPlace }, ref) => { const [isLikePlace, setIsLikePlace] = useState(false) - const { data: user, revalidate } = useFetch(api.users.me.get, { + const { data: user } = useFetch(api.users.me.get, { key: ['user'], }) @@ -56,7 +57,7 @@ const PlaceItem = forwardRef( placeId: place.id, mapId, }) - revalidate(['places', mapId]) + revalidatePlaces(mapId) } catch (error) { setIsLikePlace(false) if (error instanceof APIError || error instanceof Error) { @@ -78,7 +79,7 @@ const PlaceItem = forwardRef( placeId: place.id, mapId, }) - revalidate(['places', mapId]) + revalidatePlaces(mapId) } catch (error) { setIsLikePlace(true) if (error instanceof APIError || error instanceof Error) { diff --git a/src/app/recommendation/ai-recommendation.ts b/src/app/recommendation/ai-recommendation.ts index fa1f0ebf..41e0721d 100644 --- a/src/app/recommendation/ai-recommendation.ts +++ b/src/app/recommendation/ai-recommendation.ts @@ -8,6 +8,7 @@ import { } from './guide' import { api } from '@/utils/api' import type { LocationType } from '@/models/kakao-map' +import { recommendationChatsStorage } from '@/utils/storage' interface AIRecommendationProps { authorization: boolean @@ -58,10 +59,7 @@ export const handleAIRecommendation = async ({ try { const x = String(userLocation?.longitude || '') const y = String(userLocation?.latitude || '') - const recommendationApi = - process.env.NODE_ENV === 'production' - ? api.gpt.restaurants.recommend - : api.gpt.restaurants.recommend.test + const recommendationApi = api.gpt.restaurants.recommend const response = await recommendationApi.get(question, x, y) const reader = response.body.getReader() @@ -140,6 +138,8 @@ export const handleAIRecommendation = async ({ setChats((prev) => prev.map((chat, index) => { if (isLastChat(index)) { + recommendationChatsStorage.set(prev) + return { ...chat, suggestionKeywords: ['처음으로'], diff --git a/src/app/recommendation/chat-box.tsx b/src/app/recommendation/chat-box.tsx index f0100367..bfebb03f 100644 --- a/src/app/recommendation/chat-box.tsx +++ b/src/app/recommendation/chat-box.tsx @@ -20,6 +20,7 @@ const ChatBox = ({ chats, className, onClickSuggestion }: ChatBoxProps) => { chat={chat} type={chat.type} isFirst={index === 0} + isLast={index === chats.length - 1} onClickSuggestion={(suggestion) => { onClickSuggestion(suggestion) }} diff --git a/src/app/recommendation/chat-item.tsx b/src/app/recommendation/chat-item.tsx index b720d8db..dad85b53 100644 --- a/src/app/recommendation/chat-item.tsx +++ b/src/app/recommendation/chat-item.tsx @@ -16,18 +16,20 @@ interface ChatItemProps extends ClassName { export const AISuggestion = ({ chat, isFirst, + isLast, type, className, onClickSuggestion, }: ChatItemProps & { isFirst: boolean + isLast: boolean type: 'gpt-typing' | 'gpt-stream' onClickSuggestion: (suggestion: string) => void }) => { const { typingText, typingStart, typingComplete } = useTypewriter( chat.value, 100, - type === 'gpt-typing', + type === 'gpt-typing' && isLast, ) if (chat.type === 'user') return null @@ -67,8 +69,7 @@ export const AISuggestion = ({ color="neutral-000" className="whitespace-pre-line" > - {chat.type === 'gpt-stream' && chat.value} - {chat.type === 'gpt-typing' && typingText} + {chat.type === 'gpt-typing' && isLast ? typingText : chat.value} )}
@@ -90,11 +91,13 @@ export const AISuggestion = ({ {typingComplete && chat.suggestionKeywords.length > 0 && (
    {chat.suggestionKeywords.map((suggestion) => { if ( diff --git a/src/app/recommendation/guide.ts b/src/app/recommendation/guide.ts index 2089cb86..c40a9916 100644 --- a/src/app/recommendation/guide.ts +++ b/src/app/recommendation/guide.ts @@ -2,7 +2,7 @@ import type { Chat } from './type' export const initialRecommendChat: Chat = { type: 'gpt-typing', - value: `어떤 맛집을 찾고 계시나요?`, + value: `어떤 맛집을 찾고 계신가요?`, suggestionKeywords: [ '강남에 맛있는 돈까스 식당 추천해줘', '성수에 인기있는 양식 식당 추천해줘', diff --git a/src/app/recommendation/page.tsx b/src/app/recommendation/page.tsx index 82e5c91b..ac3ebc39 100644 --- a/src/app/recommendation/page.tsx +++ b/src/app/recommendation/page.tsx @@ -24,9 +24,10 @@ import VisuallyHidden from '@/components/common/visually-hidden' import Icon from '@/components/common/icon' import { handleAIRecommendation } from './ai-recommendation' import AiRecommendLottie from './ai-recommend-lottie' +import { recommendationChatsStorage } from '@/utils/storage' const Recommendation = () => { - const { data: user } = useFetch(api.users.me.get, { + const { data: user, status: userStatus } = useFetch(api.users.me.get, { key: ['user'], }) const { @@ -40,8 +41,8 @@ const Recommendation = () => { const availableCount = recommendationUsage ? recommendationUsage.maxLimit - recommendationUsage.usageCount : 0 + const [chats, setChats] = useState([]) const [input, setInput] = useState('') - const [chats, setChats] = useState([initialRecommendChat]) const [isFinish, setIsFinish] = useState(false) const [isFetching, setIsFetching] = useState(false) const [isOpenModal, setIsOpenModal] = useState(false) @@ -160,6 +161,18 @@ const Recommendation = () => { } } + useEffect(() => { + if (userStatus === 'pending') return + + const savedChats = recommendationChatsStorage.getValueOrNull() + if (user && savedChats) { + setChats(savedChats) + } + + bottomChat.current?.scrollIntoView({ behavior: 'smooth' }) + setChats((prev) => [...prev, initialRecommendChat]) + }, [user, userStatus]) + useEffect(() => { bottomChat.current?.scrollIntoView({ behavior: 'smooth' }) }, [chats, isFetching]) diff --git a/src/app/search/[query]/result-place-map-popup.tsx b/src/app/search/[query]/result-place-map-popup.tsx index 518f20e6..8804fcb1 100644 --- a/src/app/search/[query]/result-place-map-popup.tsx +++ b/src/app/search/[query]/result-place-map-popup.tsx @@ -22,6 +22,7 @@ import { api } from '@/utils/api' import cn from '@/utils/cn' import { roundOnePoint } from '@/utils/number' import { getStarByScore } from '@/utils/score' +import { revalidatePlaces } from '@/app/actions' interface ResultPlaceMapPopupProps extends ClassName { mapId: MapInfo['id'] @@ -35,7 +36,7 @@ const ResultPlaceMapPopup = forwardRef( const [isLoading, setIsLoading] = useState(false) const [place, setPlace] = useState(null) - const { data: user, revalidate } = useFetch(api.users.me.get, { + const { data: user } = useFetch(api.users.me.get, { onLoadEnd: (userData) => { setIsLikePlace( !!place?.likedUser?.find((liked) => liked.id === userData.id), @@ -92,7 +93,7 @@ const ResultPlaceMapPopup = forwardRef( placeId: place.id, mapId, }) - revalidate(['places', mapId]) + revalidatePlaces(mapId) } catch (error) { setIsLikePlace(false) setIsRecentlyLike( @@ -112,7 +113,7 @@ const ResultPlaceMapPopup = forwardRef( placeId: place.id, mapId, }) - revalidate(['places', mapId]) + revalidatePlaces(mapId) } catch (error) { setIsLikePlace(true) setIsRecentlyLike( diff --git a/src/app/search/[query]/result-search-list.tsx b/src/app/search/[query]/result-search-list.tsx index 216f39b7..3db70ed9 100644 --- a/src/app/search/[query]/result-search-list.tsx +++ b/src/app/search/[query]/result-search-list.tsx @@ -15,6 +15,7 @@ import { api } from '@/utils/api' import cn from '@/utils/cn' import { formatDistance, getDistance } from '@/utils/location' import { allowUserPositionStorage } from '@/utils/storage' +import { revalidatePlaces } from '@/app/actions' interface ResultSearchListBoxProps extends ClassName { places: SearchPlace[] @@ -26,7 +27,7 @@ const ResultSearchListBox = ({ places, mapId, }: ResultSearchListBoxProps) => { - const { data: user, revalidate } = useFetch(api.users.me.get, { + const { data: user } = useFetch(api.users.me.get, { key: ['user'], }) const { userLocation } = useUserGeoLocation() @@ -87,7 +88,7 @@ const ResultSearchListBox = ({ if (!mapId) return optimisticUpdateLikeOrUnLike(placeId) await api.place.mapId.placeId.like.put({ placeId, mapId }) - revalidate(['places', mapId]) + revalidatePlaces(mapId) } catch (error) {} } @@ -96,7 +97,7 @@ const ResultSearchListBox = ({ if (!mapId) return optimisticUpdateLikeOrUnLike(placeId) await api.place.mapId.placeId.like.delete({ placeId, mapId }) - revalidate(['places', mapId]) + revalidatePlaces(mapId) } catch (error) {} } diff --git "a/src/components/common/icons/U+Chef-\360\237\247\221\342\200\215\360\237\215\263.svg" "b/src/components/common/icons/U+Chef-\360\237\247\221\342\200\215\360\237\215\263.svg" new file mode 100644 index 00000000..3e0a42af --- /dev/null +++ "b/src/components/common/icons/U+Chef-\360\237\247\221\342\200\215\360\237\215\263.svg" @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/common/icons/index.ts b/src/components/common/icons/index.ts index d9b7cf69..23228c7c 100644 --- a/src/components/common/icons/index.ts +++ b/src/components/common/icons/index.ts @@ -63,6 +63,7 @@ import U1F924 from './U+1F924-🤤.svg' import U2728 from './U+2728-✨.svg' import UFamily from './U+Family-👨‍👩‍👦‍👦.svg' import UParking from './U+Parking-🅿️.svg' +import UChef from './U+Chef-🧑‍🍳.svg' import UploadSimple from './UploadSimple.svg' import 고기 from './고기.svg' import 기타 from './기타.svg' @@ -145,6 +146,7 @@ export const icons = { U2728, UFamily, UParking, + UChef, uploadSimple: UploadSimple, 고기, 기타, diff --git a/src/components/kakao-map/gpt-intro-modal.tsx b/src/components/kakao-map/gpt-intro-modal.tsx index c9f2d42f..71bea93e 100644 --- a/src/components/kakao-map/gpt-intro-modal.tsx +++ b/src/components/kakao-map/gpt-intro-modal.tsx @@ -23,6 +23,7 @@ const GptIntroModal = ({ }) const availableCount = data ? data.maxLimit - data.usageCount : 0 + const maxLimit = data?.maxLimit || 3 return ( @@ -46,7 +47,7 @@ const GptIntroModal = ({ {`AI 추천은 매일 ${data?.maxLimit}회까지 가능해요.\n추천 횟수는 매일 초기화 돼요.`} + >{`AI 추천은 매일 ${maxLimit}회까지 가능해요.\n추천 횟수는 자정에 초기화 돼요.`} 오늘 남은 횟수 {availableCount}회