From 8f65cb6e96ee12144fc5f069ab4a089178464a83 Mon Sep 17 00:00:00 2001 From: kcwww Date: Sat, 14 Dec 2024 01:46:25 +0900 Subject: [PATCH] fix: private message api --- app/(protected)/main/_utils/fetchCrystal.ts | 18 +++++++ app/(protected)/main/_utils/fetchMessages.ts | 18 +++++++ .../visit/_components/Decorations.tsx | 4 +- .../visit/_components/PrivateButton.tsx | 20 +++++++ app/(public)/visit/_components/index.tsx | 11 ++-- app/api/visit/crystal/route.ts | 45 ++++++++++++++++ app/api/visit/messages/route.ts | 53 +++++++++++++++++++ shared/components/modals/PrivateInfoModal.tsx | 52 ++++++++++++++++++ shared/components/providers/ModalProvider.tsx | 2 + shared/constants/modal.ts | 1 + shared/constants/routes.ts | 2 + 11 files changed, 220 insertions(+), 6 deletions(-) create mode 100644 app/(public)/visit/_components/PrivateButton.tsx create mode 100644 app/api/visit/crystal/route.ts create mode 100644 app/api/visit/messages/route.ts create mode 100644 shared/components/modals/PrivateInfoModal.tsx diff --git a/app/(protected)/main/_utils/fetchCrystal.ts b/app/(protected)/main/_utils/fetchCrystal.ts index da225c4..440a779 100644 --- a/app/(protected)/main/_utils/fetchCrystal.ts +++ b/app/(protected)/main/_utils/fetchCrystal.ts @@ -19,4 +19,22 @@ const fetchCrystal = async (user_id: string) => { } }; +export const fetchVisitCrystal = async (user_id: string) => { + try { + const response = await clientComponentFetch(BACKEND_ROUTES.VISIT_CRYSTAL, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'X-User-Id': user_id, + }, + }); + if (!response.ok) + throw new Error('Failed to fetch crystal' + response.error); + + return response.data; + } catch (error) { + throw new Error('Failed to fetch crystal' + error); + } +}; + export default fetchCrystal; diff --git a/app/(protected)/main/_utils/fetchMessages.ts b/app/(protected)/main/_utils/fetchMessages.ts index a76bbd0..d089d1c 100644 --- a/app/(protected)/main/_utils/fetchMessages.ts +++ b/app/(protected)/main/_utils/fetchMessages.ts @@ -19,4 +19,22 @@ const fetchMessages = async (crystal_id: string) => { } }; +export const fetchVisitMessages = async (crystal_id: string) => { + try { + const response = await clientComponentFetch(BACKEND_ROUTES.VISIT_MESSAGES, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'X-Crystal-Id': crystal_id, + }, + }); + if (!response.ok) + throw new Error('Failed to fetch message' + response.error); + + return response.data; + } catch (error) { + throw new Error('Failed to fetch message' + error); + } +}; + export default fetchMessages; diff --git a/app/(public)/visit/_components/Decorations.tsx b/app/(public)/visit/_components/Decorations.tsx index 8d05f05..22f2071 100644 --- a/app/(public)/visit/_components/Decorations.tsx +++ b/app/(public)/visit/_components/Decorations.tsx @@ -1,4 +1,4 @@ -import fetchMessages from '@/app/(protected)/main/_utils/fetchMessages'; +import { fetchVisitMessages } from '@/app/(protected)/main/_utils/fetchMessages'; import Decoration from '@/app/(public)/visit/_components/Decoration'; import { getDecoPosition } from '@/shared/components/canvas/utils/canvas'; import { Crystal } from '@/shared/types/crystal'; @@ -12,7 +12,7 @@ const MemoizedDecoration = memo(Decoration); const Decorations = ({ crystal }: { crystal: Crystal['_id'] }) => { const { data, isLoading, isError } = useQuery({ queryKey: ['messages', crystal], - queryFn: () => fetchMessages(crystal), + queryFn: () => fetchVisitMessages(crystal), gcTime: 0, staleTime: 0, }); diff --git a/app/(public)/visit/_components/PrivateButton.tsx b/app/(public)/visit/_components/PrivateButton.tsx new file mode 100644 index 0000000..ab1edd4 --- /dev/null +++ b/app/(public)/visit/_components/PrivateButton.tsx @@ -0,0 +1,20 @@ +import { LockKeyhole } from 'lucide-react'; +import useModal from '@/shared/hooks/useModal'; +import MODAL_TYPE from '@/shared/constants/modal'; + +const PrivateButton = ({ isPrivate }: { isPrivate: Date | null }) => { + const { onOpen } = useModal(); + + return ( + + ); +}; + +export default PrivateButton; diff --git a/app/(public)/visit/_components/index.tsx b/app/(public)/visit/_components/index.tsx index 1ee78c6..38a279c 100644 --- a/app/(public)/visit/_components/index.tsx +++ b/app/(public)/visit/_components/index.tsx @@ -18,7 +18,9 @@ import { Button } from '@/components/ui/button'; import { ROUTES } from '@/shared/constants/routes'; import { UserType } from '@/shared/types/user'; import { Crystal } from '@/shared/types/crystal'; -import fetchCrystal from '@/app/(protected)/main/_utils/fetchCrystal'; +import { fetchVisitCrystal } from '@/app/(protected)/main/_utils/fetchCrystal'; +import ErrorPage from '@/app/not-found'; +import PrivateButton from '@/app/(public)/visit/_components/PrivateButton'; const Visit = ({ userData }: { userData: UserType }) => { const router = useRouter(); @@ -26,7 +28,7 @@ const Visit = ({ userData }: { userData: UserType }) => { const { data, isLoading, isError } = useQuery({ queryKey: ['crystal', userData._id], - queryFn: () => fetchCrystal(userData._id), + queryFn: () => fetchVisitCrystal(userData._id), gcTime: 0, }); @@ -51,8 +53,8 @@ const Visit = ({ userData }: { userData: UserType }) => { return true; }, [data, current]); - if (isLoading || isError) return null; - if (!data) return null; + if (isLoading) return null; + if (!data || isError) return ; return ( <> @@ -61,6 +63,7 @@ const Visit = ({ userData }: { userData: UserType }) => {
+
{ + const userId = req.headers.get('X-User-Id'); + + if (!userId) { + return NextResponse.json({ error: 'User ID is required' }, { status: 400 }); + } + // MongoDB에 연결 + await connectToMongoDB(); + + try { + // user_id에 해당하는 유저의 crystal_id 조회 + const crystal = ( + (await User.findOne({ _id: userId }).select('crystal_id')) as UserType + ).crystal_id?.get(CURRENT_YEAR)?.[CURRENT_SEASON]; + + if (!crystal) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + const crystalDatas = await Promise.all( + crystal.map(async (crystalId) => { + return await Crystal.findOne({ _id: crystalId }); + }) + ); + + return NextResponse.json({ data: crystalDatas, ok: true }); + } catch (error) { + console.error('Error fetching crystal:', error); + return NextResponse.json( + { error: 'Failed to fetch crystal' + error }, + { status: 500 } + ); + } +}; diff --git a/app/api/visit/messages/route.ts b/app/api/visit/messages/route.ts new file mode 100644 index 0000000..2aaf8dd --- /dev/null +++ b/app/api/visit/messages/route.ts @@ -0,0 +1,53 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import { connectToMongoDB } from '@/shared/database/mongodb/config'; +import Crystal from '@/shared/database/mongodb/models/crystalModel'; +import { Crystal as CrystalType } from '@/shared/types/crystal'; +import Message from '@/shared/database/mongodb/models/messageModel'; + +export const dynamic = 'force-dynamic'; + +export const GET = async (req: NextRequest) => { + const crystalId = req.headers.get('X-Crystal-Id'); + + if (!crystalId) { + return NextResponse.json( + { error: 'Crystal ID is required' }, + { status: 400 } + ); + } + + try { + // MongoDB에 연결 + await connectToMongoDB(); + + const crystal = (await Crystal.findOne({ _id: crystalId }).select( + 'message_id is_private' + )) as CrystalType; + + if (!crystal) { + return NextResponse.json({ error: 'Crystal not found' }, { status: 404 }); + } + const isPrivate = crystal.is_private; + const messages = crystal.message_id; + + const messageDatas = await Promise.all( + messages.map(async (messageId) => { + const message = await Message.findOne({ _id: messageId }); + if (message && isPrivate !== null) { + message.content = '비공개 메시지입니다.'; + message.sender = '익명'; + } + return message; + }) + ); + + return NextResponse.json({ data: messageDatas, ok: true }); + } catch (error) { + console.error('Error fetching messages:', error); + return NextResponse.json( + { error: 'Failed to fetch messages' + error }, + { status: 500 } + ); + } +}; diff --git a/shared/components/modals/PrivateInfoModal.tsx b/shared/components/modals/PrivateInfoModal.tsx new file mode 100644 index 0000000..935945e --- /dev/null +++ b/shared/components/modals/PrivateInfoModal.tsx @@ -0,0 +1,52 @@ +'use client'; + +import useModal from '@/shared/hooks/useModal'; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import MODAL_TYPE from '@/shared/constants/modal'; + +const PrivateInfoModal = () => { + const { isOpen, onClose, type } = useModal(); + + if (!isOpen || type !== MODAL_TYPE.PRIVATE_INFO) { + return null; + } + + return ( + + e.preventDefault()} + > + + 비공개 수정구슬 + + 현재 해당 수정구슬은 비공개로 설정되어 있습니다. +
+ 비공개 수정구슬은 수정구슬 주인만 메세지를 확인할 수 있어요. +
+
+ + + + +
+
+ ); +}; + +export default PrivateInfoModal; diff --git a/shared/components/providers/ModalProvider.tsx b/shared/components/providers/ModalProvider.tsx index e429c50..472ece4 100644 --- a/shared/components/providers/ModalProvider.tsx +++ b/shared/components/providers/ModalProvider.tsx @@ -8,6 +8,7 @@ import FormModal from '@/shared/components/modals/FormModal'; import MessageListModal from '@/shared/components/modals/MessageListModal'; import PrivateModal from '@/shared/components/modals/PrivateModal'; import MessageSubmitModal from '@/shared/components/modals/MessageSubmitModal'; +import PrivateInfoModal from '@/shared/components/modals/PrivateInfoModal'; const ModalProvider = () => { const [isMounted, setIsMounted] = useState(false); @@ -28,6 +29,7 @@ const ModalProvider = () => { + ); }; diff --git a/shared/constants/modal.ts b/shared/constants/modal.ts index 408e008..a8b873c 100644 --- a/shared/constants/modal.ts +++ b/shared/constants/modal.ts @@ -55,6 +55,7 @@ const MODAL_TYPE = { FORM: 'Form', ALL_MESSAGE: 'AllMessage', PRIVATE: 'Private', + PRIVATE_INFO: 'PrivateInfo', MESSAGE_SUBMIT: 'MessageSubmit', } as const; diff --git a/shared/constants/routes.ts b/shared/constants/routes.ts index 35c32ec..24cc05a 100644 --- a/shared/constants/routes.ts +++ b/shared/constants/routes.ts @@ -11,6 +11,8 @@ export const ROUTES = { export const BACKEND_ROUTES = { NICKNAME: '/api/user/nickname', CRYSTAL: '/api/crystal', + VISIT_CRYSTAL: '/api/visit/crystal', + VISIT_MESSAGES: '/api/visit/messages', MESSAGE: '/api/crystal/message', MESSAGES: '/api/crystal/messages', ALL_MESSAGES: '/api/user/messages',