diff --git a/.hintrc b/.hintrc new file mode 100644 index 00000000..8d8d549e --- /dev/null +++ b/.hintrc @@ -0,0 +1,13 @@ +{ + "extends": [ + "development" + ], + "hints": { + "axe/structure": [ + "default", + { + "list": "off" + } + ] + } +} \ No newline at end of file diff --git a/public/images/send.svg b/public/images/send.svg new file mode 100644 index 00000000..559483e1 --- /dev/null +++ b/public/images/send.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/app/(root)/(routes)/chat/components/ChatInput.tsx b/src/app/(root)/(routes)/chat/components/ChatInput.tsx new file mode 100644 index 00000000..fd52c0df --- /dev/null +++ b/src/app/(root)/(routes)/chat/components/ChatInput.tsx @@ -0,0 +1,53 @@ +import React, { useState } from 'react' +import Image from 'next/image' +import Button from '@/components/ui/button' +import Input from '@/components/ui/input' +import Assets from '@/config/assets' + +const ChatInput = ({ + onSubmit, +}: { + onSubmit: (_newMessage: string) => void +}) => { + const [newMessage, setNewMessage] = useState('') + + const onChange = (e: React.ChangeEvent) => { + e.preventDefault() + setNewMessage(e.target.value) + } + + const onSubmitMessage = (e: React.FormEvent) => { + e.preventDefault() + if (!newMessage.trim()) return + + onSubmit(newMessage) + setNewMessage('') + } + + return ( +
+
+ +
+ +
+ + ) +} + +export default ChatInput diff --git a/src/app/(root)/(routes)/chat/components/ChatList.tsx b/src/app/(root)/(routes)/chat/components/ChatList.tsx new file mode 100644 index 00000000..f425cf7f --- /dev/null +++ b/src/app/(root)/(routes)/chat/components/ChatList.tsx @@ -0,0 +1,79 @@ +import React, { forwardRef, memo } from 'react' +import { TYPOGRAPHY } from '@/styles/sizes' +import type { Message } from '@/types/message' +import { cn } from '@/utils' + +type ChatListProps = { + messages: Message[] + currentUserNickname?: string +} + +type ChatProps = { + message: Message + isMyMessage: boolean +} + +const ChatList = forwardRef( + ({ messages, currentUserNickname }, ref) => { + return ( +
    + {messages.map((message: Message) => { + return ( + + ) + })} + {messages.length === 0 && ( +

    채팅을 시작해보세요!

    + )} +
    +
+ ) + }, +) +ChatList.displayName = 'ChatList' + +export default memo(ChatList) + +const Chat = ({ message, isMyMessage }: ChatProps) => { + return isMyMessage ? ( + + ) : ( + + ) +} + +const MyChat = ({ message }: Pick) => { + return ( +
  • +

    + {message.createdAt.toDate().toLocaleTimeString().slice(0, -3)} +

    +
    + {message.text} +
    +
  • + ) +} + +const OtherChat = ({ message }: Pick) => { + return ( +
  • +
    + {message.text} +
    +

    + {message.createdAt.toDate().toLocaleTimeString().slice(0, -3)} +

    +
  • + ) +} diff --git a/src/app/(root)/(routes)/chat/page.tsx b/src/app/(root)/(routes)/chat/page.tsx index 3f7931e2..9d92ff89 100644 --- a/src/app/(root)/(routes)/chat/page.tsx +++ b/src/app/(root)/(routes)/chat/page.tsx @@ -1,61 +1,64 @@ 'use client' -import React from 'react' -import { collection, limit, orderBy, query, addDoc } from 'firebase/firestore' -import useFirestoreQuery, { Message } from '@/hooks/useFirestoreQuery' -import { db, getMessageRef } from '@/lib/firebase' +import React, { useEffect, useRef } from 'react' +import { limit, orderBy, query, addDoc } from 'firebase/firestore' +import PageTitle from '@/components/domain/page-title' +import { CHAT_LIMIT } from '@/config/firebaseConfig' +import { useAuth } from '@/contexts/AuthProvider' +import useFirestoreQuery from '@/hooks/useFirestoreQuery' +import { useToast } from '@/hooks/useToast' +import { getMessageRef } from '@/lib/firebase' +import ChatInput from './components/ChatInput' +import ChatList from './components/ChatList' const ChatPage = () => { - const messageRef = getMessageRef('room2') + const { currentUser } = useAuth() + const { toast } = useToast() + + const messageRef = getMessageRef('room2') // TODO: room id를 받아서 처리 const messages = useFirestoreQuery( - query(messageRef, orderBy('createdAt', 'asc'), limit(200)), + query(messageRef, orderBy('createdAt', 'asc'), limit(CHAT_LIMIT)), ) - const [newMessage, setNewMessage] = React.useState('') + const chatBottomRef = useRef(null) + + useEffect(() => { + if (chatBottomRef.current) { + chatBottomRef.current.scrollIntoView({ behavior: 'smooth' }) + } + }, [messageRef]) + + const onSubmitMessage = async (message: string) => { + chatBottomRef.current?.scrollIntoView({ behavior: 'smooth' }) - const onChange = (e: React.ChangeEvent) => { - e.preventDefault() - setNewMessage(e.target.value) + try { + await addDoc(messageRef, { + text: message, + sender: currentUser?.nickname ?? '익명', + createdAt: new Date(), + }) + } catch (e) { + toast({ + title: '메세지 전송에 실패했습니다.', + variant: 'destructive', + duration: 3000, + }) + } } return ( -
    -

    chat

    -

    new message

    -
    ) => { - e.preventDefault() - console.log(newMessage) - addDoc(collection(db, 'chats', 'room2', 'messages'), { - text: newMessage, - sender: 'me', - createdAt: new Date().toISOString(), - }).then(() => { - setNewMessage('') - }) - }} - > - + +
    + - - -
    - {messages.map((item: Message, idx: number) => { - console.log(item) - return ( -
    -

    {`${item.sender}:`}

    -

    {item.text}

    -
    - ) - })} - {messages.length === 0 &&

    no data

    } -
    -
    + + +
    + ) } diff --git a/src/components/domain/buttons/new-card-button/NewCardButton.tsx b/src/components/domain/buttons/new-card-button/NewCardButton.tsx index 6120d68a..b54f43bc 100644 --- a/src/components/domain/buttons/new-card-button/NewCardButton.tsx +++ b/src/components/domain/buttons/new-card-button/NewCardButton.tsx @@ -17,6 +17,7 @@ const NewCardButton = () => { if (isLoggedIn) router.push(AppPath.newCard()) } + // TODO: 챗 페이지에서는 안보이게 처리 if (!isLoggedIn || path === AppPath.newCard()) return <> return ( diff --git a/src/components/domain/page-title/PageTitle.tsx b/src/components/domain/page-title/PageTitle.tsx index dd9ccf51..8042e9dd 100644 --- a/src/components/domain/page-title/PageTitle.tsx +++ b/src/components/domain/page-title/PageTitle.tsx @@ -3,7 +3,7 @@ import Assets from '@/config/assets' const PageTitle = ({ title }: { title: string }) => { return ( -
    +
    이전 아이콘 { return ( diff --git a/src/lib/firebase/index.ts b/src/lib/firebase/index.ts index 8869066d..3c156bfb 100644 --- a/src/lib/firebase/index.ts +++ b/src/lib/firebase/index.ts @@ -4,15 +4,7 @@ import { getFirestore, collection, } from 'firebase/firestore' - -export const firebaseConfig = { - apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, - authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, - projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, - storageBucket: process.env.NEXT_PUBLIC_STORAGE_BUCKET, - messagingSenderId: process.env.NEXT_PUBLIC_MESSAGING_SENDER_ID, - appId: process.env.NEXT_PUBLIC_APP_ID, -} +import firebaseConfig from '@/config/firebaseConfig' const fb = initializeApp(firebaseConfig) const db = getFirestore(fb) @@ -21,11 +13,4 @@ const getMessageRef = (roomId: string): CollectionReference => { return collection(db, 'chats', roomId, 'messages') } -// async function getData(db: Firestore) { -// const dataCol = collection(db, 'chats', 'room2', 'messages') -// const dataSnap = await getDocs(dataCol) -// const dataList = dataSnap.docs.map((doc) => doc.data()) -// return dataList -// } - export { db, getMessageRef } diff --git a/src/styles/globals.css b/src/styles/globals.css index f0199365..df9986a1 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -5,6 +5,9 @@ @layer base { :root { --nav-height: 3.5rem; + --page-height: calc(100vh - var(--nav-height)); + --chat-input-height: 4.5rem; + --page-min-width: 320px; --page-max-width: 640px; } diff --git a/src/types/message.ts b/src/types/message.ts new file mode 100644 index 00000000..4b646b13 --- /dev/null +++ b/src/types/message.ts @@ -0,0 +1,10 @@ +import { Timestamp } from 'firebase/firestore' + +interface Message { + text: string + createdAt: Timestamp + sender: string + id: string +} + +export type { Message } diff --git a/tailwind.config.js b/tailwind.config.js index 6c0fd02c..260b94fe 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -24,6 +24,8 @@ module.exports = { height: { ...HEIGHT, nav: 'var(--nav-height)', + page: 'var(--page-height)', + chat_input: 'var(--chat-input-height)', }, borderRadius: { ...BORDER_RADIUS, @@ -36,6 +38,9 @@ module.exports = { fontFamily: { pretendard: ['Pretendard'], }, + padding: { + chat_input: 'var(--chat-input-height)', + }, }, }, plugins: [