diff --git a/src/app/friendList/page.tsx b/src/app/friendList/page.tsx index 7652651..d3140ac 100644 --- a/src/app/friendList/page.tsx +++ b/src/app/friendList/page.tsx @@ -20,18 +20,11 @@ const FriendListPage = () => { const handleFriendPlus = () => { setIsModalOpen(true) } - const friendData = [ - { id: 1, name: '신혜민', tag: 1111, profile: user.user?.profileImage }, - { id: 2, name: '김의진', tag: 1112, profile: user.user?.profileImage }, - { id: 3, name: '강보석', tag: 1113, profile: user.user?.profileImage }, - { id: 4, name: '송수빈', tag: 1114, profile: user.user?.profileImage }, - { id: 5, name: '유성현', tag: 1115, profile: user.user?.profileImage }, - { id: 6, name: '짱똘', tag: 1116, profile: user.user?.profileImage }, - ] + const fetchFriendList = async () => { console.log(user.user?.id) if (user) { - const friendList = await getFriendList(user.user?.id || null) + const friendList = await getFriendList(user.user?.id || null,null) console.log('제발용',friendList) setFriendsData(friendList) } diff --git a/src/app/notification/page.tsx b/src/app/notification/page.tsx index 32eb0d1..e816b57 100644 --- a/src/app/notification/page.tsx +++ b/src/app/notification/page.tsx @@ -4,13 +4,10 @@ import DetailHeader from '@/components/common/header/DetailHeader' import { getNotification } from '@/lib/notification' import useUserStore from '@/stores/useAuthStore' import { Notification } from '@/types/notification' +import Icon from '@/components/common/icon/Icon' import Spinner from '@/components/common/spinner/Spinner' -import { useToast } from '@/components/common/toast/Toast' -import { useRouter } from 'next/navigation' -import { Json } from '@/types/supabase' -import { getRelativeTime } from '@/utils/getRelativeTime' - import FriendAcceptModal from '@/components/friend/FriendAcceptModal' +import InviteAcceptModal from '@/components/invite/InviteAcceptModal' const Page = () => { const [notificationData, setNotificationData] = useState([]) const [selectedNotifycationId, setSelectedNotifycationId] = useState< @@ -25,9 +22,8 @@ const Page = () => { useState(null) const [isLoading, setIsLoading] = useState(true) const userData = useUserStore((state) => state.user) - const router = useRouter() - const [isAcceptModalOpen, setIsAcceptModalOpen] = useState(false) + const [isInviteAcceptModalOpen, setIsInviteAcceptModalOpen] = useState(false) const fetchNotifications = async () => { setIsLoading(true) const data = await getNotification(userData!.id) @@ -35,35 +31,28 @@ const Page = () => { (a, b) => Number(new Date(b.created_at)) - Number(new Date(a.created_at)), ) setNotificationData(sortedDataByCreatedAt) - console.log(sortedDataByCreatedAt) setIsLoading(false) } - // -------------- 알림 아이템 클릭 시 알림 타입에 따라 처리하는 함수 ------------- // - function notificationOnclickHandler(type: string, data: Json | null) { - const parsedData = data as Record + function getRelativeTime(utcDateString: string): string { + const KST_OFFSET = 9 * 60 * 60 * 1000 // 한국은 UTC+9 + const now = new Date(Date.now() + KST_OFFSET) + const past = new Date(new Date(utcDateString).getTime() + KST_OFFSET) - console.log(type, data) - if (!type || !data) { - useToast.error('잘못된 요청입니다. 다시 시도해주세요.') - return - } + const diff = now.getTime() - past.getTime() - switch (type) { - // chat, payment : 해당 엔빵 페이지로 이동 - case 'chat': - case 'payment': - const nbreadId = parsedData['nbreadId'] as string - router.replace(`/nbread/${nbreadId}`) - break - case 'nbread-invite': - // NOTE 엔빵 초대 관련 로직 추가 - break + const minutes = Math.floor(diff / (1000 * 60)) + const hours = Math.floor(diff / (1000 * 60 * 60)) + const days = Math.floor(diff / (1000 * 60 * 60 * 24)) + const month = Math.floor(diff / (1000 * 60 * 60 * 24 * 30)) + const year = Math.floor(diff / (1000 * 60 * 60 * 24 * 30 * 12)) - case 'friend-request': - // NOTE 친구 초대 관련 로직 추가 - break - } + if (minutes < 1) return '방금 전' + if (minutes < 60) return `${minutes}분 전` + if (hours < 24) return `${hours}시간 전` + if (days < 30) return `${days}일 전` + if (month < 12) return `${month}개월 전` + return `${year}년 전` } useEffect(() => { @@ -74,6 +63,8 @@ const Page = () => { useEffect(() => { if (selectedNotifycationType === 'friend_request') { setIsAcceptModalOpen(true) + } else if (selectedNotifycationType === 'invite') { + setIsInviteAcceptModalOpen(true) } }, [selectedNotifycationType]) if (isLoading) { @@ -81,30 +72,71 @@ const Page = () => { } return ( -
+
알림
+
null // TODO 알림 모두 지우기 함수 추가 + } + > + 모두 지우기 +
-
- {notificationData.map((item, _) => ( +
+ {notificationData.map((data, index) => (
notificationOnclickHandler(item.type, item.data)} + key={data.id} + onClick={() => { + setSelectedNotifycationId(data.id) + setSelectedNotifycationType(data.type) + if (data.type === 'invite') { + // 엔빵 초대 알림일 경우 + setSelectedNotifycationSenderName( + (data.data as any)?.nbreadTitle ?? null, + ) + setSelectedNotifycationSenderId( + (data.data as any)?.nbreadId ?? null, + ) + } else if (data.type === 'friend_request') { + // 친구 요청 알림일 경우 + setSelectedNotifycationSenderName( + (data.data as any)?.sender_name ?? null, + ) + setSelectedNotifycationSenderId( + (data.data as any)?.sender_id ?? null, + ) + } + }} >
-
{item.title}
+
{data.title}
-
{item.message}
+
{data.message}
- {getRelativeTime(item.created_at)} + {getRelativeTime(data.created_at)}
+
null // TODO 알림 삭제 함수 추가 + } + > + +
))}
@@ -119,6 +151,17 @@ const Page = () => { senderUserName={selectedNotifycationSenderName} receiverId={userData?.id as string} /> + { + setIsInviteAcceptModalOpen(false) + setSelectedNotifycationType(null) + }} + senderNbreadId={selectedNotifycationSenderId} + senderNbreadTitle={selectedNotifycationSenderName} + receiverId={userData?.id as string} + />
) } diff --git a/src/components/friend/FriendCard.tsx b/src/components/friend/FriendCard.tsx index 20d7e53..3739f6b 100644 --- a/src/components/friend/FriendCard.tsx +++ b/src/components/friend/FriendCard.tsx @@ -25,6 +25,7 @@ const FriendCard = ({ profile, name, tag }: friendProps) => { )}

{name}

+

#{tag}

void + senderNbreadTitle: string | null + senderNbreadId: string | null + receiverId: string +} +const InviteAcceptModal = ({isOpen,onClose,senderNbreadTitle,senderNbreadId,receiverId} : InviteAcceptModalProps) => { + const router = useRouter() + const fetchInviteAccept = async () => { + const acceptInviteData = await updateAcceptInvite(receiverId,senderNbreadId) + onClose() + router.push(`/nbread/${senderNbreadId}`) + + } + const fetchInviteReject = async () => { + const rejectInviteData = await updateRejectedInvite(receiverId,senderNbreadId) + onClose() + } + return ( + +
+
+
엔빵 초대를 수락하시겠어요?
+

+ {senderNbreadTitle}의 초대를 수락하시겠어요? +

+
+
+ + +
+
+
+ ) +} +export default InviteAcceptModal diff --git a/src/components/invite/InviteBottomSheet.tsx b/src/components/invite/InviteBottomSheet.tsx index 798634d..0ab057d 100644 --- a/src/components/invite/InviteBottomSheet.tsx +++ b/src/components/invite/InviteBottomSheet.tsx @@ -5,102 +5,45 @@ import InviteUserListItem from './InviteUserListItem' import DefaultAvatar from '@/assets/avatar.svg' import { getInviteUser } from '@/lib/invite/getInviteUser' import { useParams } from 'next/navigation' +import { getFriendList } from '@/lib/friend/getSearchFriend' +import { getInviteFriendList } from '@/lib/friend/getSearchFriend' interface InviteBottomSheetProps { isOpen: boolean onClose: () => void + user: string | null } interface User { avatar: any name: string status: string + userId: string } -const InviteBottomSheet = ({ isOpen, onClose }: InviteBottomSheetProps) => { +interface Friend { + name: string + profileImage: string + inviteState?:string + tag: string + id:string + +} +const InviteBottomSheet = ({ isOpen, onClose,user }: InviteBottomSheetProps) => { const [searchData, setSearchData] = useState('') // 검색칸 입력 데이터 const [fetchSearchData, setFetchSearchData] = useState([]) // Api 반환 데이터 const searchCache = useRef>({}) + const [friendListData, setFriendData] = useState([]) const params = useParams() const [nbreadId, setNbreadId] = useState('') - - const userFollowingData = [ - { id: 0, avatar: DefaultAvatar, name: '유성현', status: '초대 하기' }, - { id: 1, avatar: DefaultAvatar, name: '신혜민', status: '초대 하기' }, - { id: 2, avatar: DefaultAvatar, name: '강보석', status: '초대 하기' }, - { - id: 3, - avatar: DefaultAvatar, - name: '송수빈', - status: '초대 하기', - }, - { - id: 4, - avatar: DefaultAvatar, - name: '빌게이츠', - status: '초대 완료', - }, - { - id: 5, - avatar: DefaultAvatar, - name: '이재용', - status: '참여 중', - }, - { - id: 6, - avatar: DefaultAvatar, - name: '머스크', - status: '초대 하기', - }, - { - id: 7, - avatar: DefaultAvatar, - name: '머스크', - status: '초대 하기', - }, - { - id: 8, - avatar: DefaultAvatar, - name: '머스크', - status: '초대 하기', - }, - { - id: 9, - avatar: DefaultAvatar, - name: '머스크', - status: '초대 하기', - }, - { - id: 10, - avatar: DefaultAvatar, - name: '머스크', - status: '초대 하기', - }, - { - id: 11, - avatar: DefaultAvatar, - name: '머스크', - status: '초대 하기', - }, - { - id: 12, - avatar: DefaultAvatar, - name: '머스크', - status: '초대 하기', - }, - { - id: 13, - avatar: DefaultAvatar, - name: '머스크', - status: '초대 하기', - }, - { - id: 14, - avatar: DefaultAvatar, - name: '머스크', - status: '초대 하기', - }, - ] + const [invitedUserId,setInvitedUserId] = useState('') + const fetchFriendList = async () => { + const fetchFriendListData = await getFriendList(user, params.nbreadId as string) + setFriendData(fetchFriendListData) + console.log('친구ㅡ 목 : ',friendListData) + } + useEffect(() => { if (isOpen) { setSearchData('') + fetchFriendList() } }, [isOpen]) @@ -118,12 +61,15 @@ const InviteBottomSheet = ({ isOpen, onClose }: InviteBottomSheetProps) => { } const apiDataRaw = (await getInviteUser(searchData, params.nbreadId as string)) || [] + console.log('검색 유저! : ',apiDataRaw) const apiData: User[] = apiDataRaw.map((u) => ({ avatar: u.profile_image, // profile_image → avatar로 매핑 name: u.name, status: u.status, - })) + userId:u.id, + })) +console.log('제발 상태야~~ :',apiData) // 캐시에 저장 console.log(apiData) searchCache.current[searchData] = apiData @@ -133,7 +79,23 @@ const InviteBottomSheet = ({ isOpen, onClose }: InviteBottomSheetProps) => { return () => clearTimeout(typingSearchData) } }, [searchData]) +const refreshLists = async () => { + // ✅ 1. 팔로잉 갱신 + await fetchFriendList() + // ✅ 2. 검색 결과 갱신 (검색어가 있을 때만) + if (searchData.length === 4) { + const apiDataRaw = await getInviteUser(searchData, params.nbreadId as string) || [] + const apiData: User[] = apiDataRaw.map((u) => ({ + avatar: u.profile_image, + name: u.name, + status: u.status, + userId: u.id, + })) + + setFetchSearchData(apiData) + } +} return ( <> @@ -165,9 +127,12 @@ const InviteBottomSheet = ({ isOpen, onClose }: InviteBottomSheetProps) => { {fetchSearchData.map((user, index) => ( ))}
@@ -177,12 +142,15 @@ const InviteBottomSheet = ({ isOpen, onClose }: InviteBottomSheetProps) => {

팔로잉

- {userFollowingData.map((user) => ( + {friendListData?.map((user) => ( ))}
diff --git a/src/components/invite/InviteUserListItem.tsx b/src/components/invite/InviteUserListItem.tsx index 37714d9..082d650 100644 --- a/src/components/invite/InviteUserListItem.tsx +++ b/src/components/invite/InviteUserListItem.tsx @@ -1,28 +1,67 @@ import DefaultAvatar from '@/assets/avatar.svg' import Avatar from '../common/avatar/avatar' +import { sendInviteRequest } from '@/lib/invite/sendInviteRequest' +import { useEffect } from 'react' +import { useState } from 'react' interface InviteUserData { avatar: string | null name: string status: string + nbreadId: string + invitedUserId: string + onRefresh?: () => void } -const InviteUserListItem = ({ avatar, name, status }: InviteUserData) => { - const getInviteUserStatus = (status: string) => { - switch (status) { - case '초대 하기': - return { color: 'text-system-blue01', cursor: 'cursor-pointer' } - case '참여 중': - return { color: 'text-gray-400', cursor: null } - case '초대 완료': - return { color: 'text-gray-400', cursor: null } - default: - return { color: 'text-black', cursor: 'cursor-pointer' } +const InviteUserListItem = ({ avatar, name, status,nbreadId,invitedUserId,onRefresh }: InviteUserData) => { + const [sendStatus, setSendStatus] = useState(status) + const [color, setColor] = useState('') + const [cursor, setCursor] = useState('') + // const getInviteUserStatus = (status: string) => { + // switch (status) { + // case '초대 하기': + // return { color: 'text-system-blue01', cursor: 'cursor-pointer' } + // case '참여 중': + // return { color: 'text-gray-400', cursor: null } + // case '초대 완료': + // return { color: 'text-gray-400', cursor: null } + // default: + // return { color: 'text-black', cursor: 'cursor-pointer' } + // } + // } + useEffect(() => { + if (status == '초대 완료') { + setSendStatus('초대 완료') + setColor('text-gray-400') + setCursor('cursor-default') } - } - const { color, cursor } = getInviteUserStatus(status) + else if(status =='초대 하기'){ + setSendStatus('초대 하기') + setColor('text-system-blue01') + setCursor('cursor-pointer') + } + else if(status =='rejected') { + setSendStatus('초대 하기') + setColor('text-system-blue01') + setCursor('cursor-pointer') + } + else { + setSendStatus('') + setColor('') + setCursor('') + } + }, [status]) + // const { color, cursor } = getInviteUserStatus(status) - const handleClick = (status: string) => { - if (status == '초대 하기') { + const handleClick = async(status: string) => { + if (status == '초대 하기' || status == 'rejected') { + console.log('glgl',status) + console.log('현재 엔빵 아디: ',nbreadId) + console.log('초대보낼 유저 아이디 : ', invitedUserId) + const fetchInvite = await sendInviteRequest(nbreadId,invitedUserId,'pending') console.log('초대 보냄') + setSendStatus('요청 완료') + setColor('text-gray-400') + setCursor('cursor-default') + if (onRefresh) onRefresh() } } return ( @@ -45,7 +84,7 @@ const InviteUserListItem = ({ avatar, name, status }: InviteUserData) => { className={`${color} text-body01 ${cursor}`} onClick={() => handleClick(status)} > - {status} + {sendStatus}

diff --git a/src/components/nbread/NbreadDetail.tsx b/src/components/nbread/NbreadDetail.tsx index af4d1c2..08d9518 100644 --- a/src/components/nbread/NbreadDetail.tsx +++ b/src/components/nbread/NbreadDetail.tsx @@ -19,7 +19,7 @@ import useUserStore from '@/stores/useAuthStore' import QuitNbreadModal from '@/components/common/modal/QuitNbreadModal' import Spinner from '@/components/common/spinner/Spinner' import InviteBottomSheet from '@/components/invite/InviteBottomSheet' - +import { getFriendList } from '@/lib/friend/getSearchFriend' interface nbreadDetailProps { nbreadData: Nbread setNbreadData: Dispatch> @@ -207,6 +207,7 @@ const nbreadDetail = ({ nbreadData, setNbreadData }: nbreadDetailProps) => { setIsInviteBottomSheetOpen(false)} + user={userData?.id ?? null} /> { })) } catch (error) {} } -export const getFriendList = async (user: string | null) => { +export const getFriendList = async ( + user: string | null, + nbreadId: string | null, +) => { if (!user) return try { const { data, error } = await supabase @@ -42,31 +45,79 @@ export const getFriendList = async (user: string | null) => { const friends = data?.map((f) => (f.user_id_1 === user ? f.user_id_2 : f.user_id_1)) || [] - console.log('친구리스트 목록', friends) - // return friends if (friends) { const { data, error } = await supabase .from('user') - .select('name,profile_image,tag') + .select('name,profile_image,tag,id') .eq('id', friends) - if(error) { - console.error('error~~~~ : ',error) + if (error) { + console.error('error~~~~ : ', error) + return + } + const friendInfoList = data || [] + + // 필요에 따라 map으로 구조 변환 + let processedFriends = friendInfoList.map((f) => ({ + name: f.name, + profileImage: f.profile_image, + tag: f.tag, + id: f.id, + })) + + const friendIds = processedFriends.map((f) => f.id) + console.log('친구id', friendIds) + console.log('nbread_id : ', nbreadId) + let inviteData: any[] = [] + if (nbreadId) { + const { data, error } = await supabase + .from('nbread_invite') + .select('state,invited_user_id') + .in('invited_user_id', friendIds) + .eq('nbread_id', nbreadId) + if (error) { + console.error('error~~', error) return } - const friendInfoList = data || [] + console.log('state : ', data) + inviteData = data || [] + const mergedFriends = processedFriends.map((friend) => { + const invite = inviteData.find((i) => i.invited_user_id === friend.id) + return { + ...friend, + inviteState: invite + ? invite.state === 'pending' + ? '초대 완료' // ✅ pending → 초대 완료로 변환 + : invite.state // 그 외 상태는 그대로 유지 + : '초대 하기', // 초대 기록 없으면 null + } + }) + return mergedFriends + } - // 필요에 따라 map으로 구조 변환 - const processedFriends = friendInfoList.map(f => ({ - name: f.name, - profileImage: f.profile_image, - tag: f.tag - })) - return processedFriends + return processedFriends } } catch (error) { console.error(error) return [] } - return [] + // return [] +} +export const getInviteFriendList = async ( + userId: string, + inviteNbreadId: string, +) => { + try { + const { data, error } = await supabase + .from('nbread_invite') + .select('invited_user_id,nbread_id,state') + .eq('invited_user_id', userId) + .eq('nbread_id', inviteNbreadId) + + if (!data) { + console.error('error : ', error) + return + } + return data + } catch (error) {} } diff --git a/src/lib/invite/getInviteUser.ts b/src/lib/invite/getInviteUser.ts index a84c9c1..a0833a4 100644 --- a/src/lib/invite/getInviteUser.ts +++ b/src/lib/invite/getInviteUser.ts @@ -27,18 +27,21 @@ export const getInviteUser = async (tag: string, nbreadId: string) => { console.log('nbreadData',nbreadData) const { data: inviteData } = await supabase - .from('invite') - .select('status') - .eq('user_id', user.id) + .from('nbread_invite') + .select('state') + .eq('invited_user_id', user.id) .eq('nbread_id', nbreadId) .maybeSingle() // 없으면 null 반환 - + console.log('검색한 유저 데이터 :' ,inviteData) let status = '초대 하기' - if(nbreadData) { + if(nbreadData === 'accept') { status = '참여 중' } - else if(inviteData) { - status = inviteData.status + else if(inviteData?.state == 'pending') { + status = '초대 완료' + } + else if(inviteData?.state == 'reject') { + status = '초대 하기' } const result = { ...user, diff --git a/src/lib/invite/sendInviteRequest.ts b/src/lib/invite/sendInviteRequest.ts new file mode 100644 index 0000000..91021f4 --- /dev/null +++ b/src/lib/invite/sendInviteRequest.ts @@ -0,0 +1,60 @@ +import { supabase } from "../supabaseClient"; + +export const sendInviteRequest = async (nbreadId:string,invitedUserId:string,state:string) => { + try { + // const {data, error} = await supabase.from('nbread_invite').insert( + // [ + // { + // nbread_id: nbreadId, + // invited_user_id: invitedUserId, + // state: "pending" + // }, + // ], + // ) + const { data, error } = await supabase + .from('nbread_invite') + .select('state') + .eq('nbread_id',nbreadId) + .eq('invited_user_id',invitedUserId) + if (error) { + console.error('error : ', error) + return + } + console.log('data', data) + // return data + if (!data || data.length === 0) { + // 데이터 없으면 insert + const { data: insertedData, error } = await supabase + .from('nbread_invite') + .insert([ + { + nbread_id: nbreadId, + invited_user_id: invitedUserId, + state: state, + }, + ]) + .select('state') + + if (error) console.error(error) + return insertedData + } else if (data.some((item) => item.state === 'rejected')) { + // 기존 데이터 중 rejected이면 update + const { data: updatedData, error } = await supabase + .from('nbread_invite') + .update({ state }) + .eq('nbread_id',nbreadId) + .eq('invited_user_id',invitedUserId) + .select('state') + + if (error) console.error(error) + return updatedData + } + + // 이미 pending이나 accepted 상태라면 그대로 반환 + return data + if(error) throw error + return data + } catch (error) { + console.error("초대 요청 중 오류 발생:", error) + } +} \ No newline at end of file diff --git a/src/lib/invite/updateInvite.ts b/src/lib/invite/updateInvite.ts new file mode 100644 index 0000000..84016a9 --- /dev/null +++ b/src/lib/invite/updateInvite.ts @@ -0,0 +1,53 @@ +import { supabase } from "../supabaseClient" +export const updateAcceptInvite = async ( + receiverId: string, + senderId: string | null, +) => { + let paymentDate = null; + const { data, error } = await supabase + .from('nbread_invite') + .update({ state: 'accepted' }) + .eq('nbread_id', senderId) + .eq('invited_user_id', receiverId) + + if (error) { + console.error('초대 수락 업데이트 실패!', error) + return null + } + console.log('data', data) + if(!error){ + const {data,error} = await supabase.from('nbread_records').select('payment_date').eq('nbread_id',senderId) + + if(error) { + console.error('error : ',error) + return + } + console.log('초대 받은 엔빵 데이터 : ',data) + paymentDate = data?.[0]?.payment_date ?? null + } + if (!error) { + const { data, error } = await supabase + .from('participant') + .insert([{ nbread_id:senderId,user_id:receiverId,is_leader:'FALSE'}]) + + if (error) { + console.error('엔빵 추가 에러 : ', error) + return null + } + console.log(data) + return data + } +} +export const updateRejectedInvite = async ( + receiverId: string, + senderId: string | null, +) => { + const { data, error } = await supabase + .from('nbread_invite') + .update({ state: 'rejected' }) + .eq('nbread_id', senderId) + .eq('invited_user_id', receiverId) + if(error) { + console.error('엔빵 거절 에러 : ',error) + } +} \ No newline at end of file diff --git a/src/lib/nbread/getUserNbread.ts b/src/lib/nbread/getUserNbread.ts index 55c00ad..b8d1388 100644 --- a/src/lib/nbread/getUserNbread.ts +++ b/src/lib/nbread/getUserNbread.ts @@ -10,12 +10,13 @@ export const getUserNbreads = async (userId: string): Promise => { .from('participant') .select('nbread_id') .eq('user_id', userId) - + if (participantError) { console.error( '❌ Failed to fetch participant entries:', participantError.message, ) + return [] } diff --git a/src/lib/notification/getNotifications.ts b/src/lib/notification/getNotifications.ts index 5f0c571..37610a1 100644 --- a/src/lib/notification/getNotifications.ts +++ b/src/lib/notification/getNotifications.ts @@ -32,6 +32,7 @@ export const getNotification = async (userId: string) => { is_read: notification.is_read, }), ) + console.log('notificationsType : ',notifications) return notifications } catch (error) { console.error('Error fetching nbread:', error)