FRONTENDSCHOOL6 / ready-act

멋쟁이사자처럼 파이널프로젝트 16조
MIT License
0 stars 4 forks source link

[R09M App] 채팅 페이지 - 구현 방식 이슈 #56

Closed jellyjoji closed 1 year ago

jellyjoji commented 1 year ago

내용

문제 상황

image

image

정리하자면

  1. 포켓호스트에 어떻게 데이터를 쌓아야 하는지
  2. 리얼타임을 활용하여 실시간으로 채팅 기능 구현을 어떻게 하는지
  3. 관계성을 어떻게 가져가야 하는지
  4. 코드에서 action 이랑 message 가 무엇을 의미하는지 아직 이해가 되지 않습니다.

참고 이미지 (선택)

16조의 채팅 기능 시안 입니다. 게시글_채팅_주문 금액(주문 받기)

※ 댓글에 이슈 해결 완료 후 결과 또는 해결 과정 이미지 첨부

jellyjoji commented 1 year ago

리얼타임 데이터베이스

PocketBase는 실시간(realtime) 데이터베이스 기능을 제공합니다. 먼저 채팅 콜렉션을 생성한 후, 아래와 같이 필드를 구성하고, API 규칙을 정해 로그인 사용자만 입력할 수 있게 제한할 수 있습니다. 😊

chats 콜렉션

image

chats 콜렉션 필드

image

chats 콜렉션 API 규칙

image

채팅 앱 코드를 공유합니다. 😀 Chat.jsx

import { motion } from 'framer-motion';
import { useEffect, useRef, useState } from 'react';
import { pb } from '@/api/pocketbase';
import { getPbImageURL } from '@/utils/getPbImageURL';

function Chats() {
    // 로그인 사용자
  const user = pb.authStore.model;
    // 채팅 앱 섹션 프레임 요소 참조
  const chatsFrameRef = useRef(null);

    // 채팅 앱 상태
  const [isLoading, setIsLoading] = useState(true);
  const [data, setData] = useState([]);

    // PB 데이터베이스 chats 콜렉션 데이터 요청
  useEffect(() => {
    async function getChats() {
      try {
        const data = await pb.collection('chats').getList(1, 10, {
          sord: '-created',
          expand: 'messenger',
        });

        setData(data);
        setIsLoading(false);
      } catch (error) {
        console.error(error);
        setIsLoading(false);
      }
    }

    getChats();
  }, []);

    // chats 데이터베이스 변경 실시간 확인
  useEffect(() => {
        // 구독
    pb.collection('chats').subscribe('*', async ({ action, record }) => {
      if (action === 'create') {
        const messenger = await pb.collection('users').getOne(record.messenger);
        record.expand = { messenger };

        setData((data) => {
          return {
            ...data,
            items: [...data.items, record],
          };
        });
      }
    });

        // 구독 취소
    return () => {
      pb.collection('chats').unsubscribe('*');
    };
  }, []);

    // 섹션 프레임 높이 변경
  useEffect(() => {
    const frame = chatsFrameRef.current;
    const topPosition = frame.getBoundingClientRect().bottom + 1000;
    frame.scroll(0, topPosition);
  }, [data.items]);

    // 메시지 입력 상태 및 업데이트 함수
  const [message, setMessage] = useState('');
  const handleMessage = (e) => {
    setMessage(e.target.value);
  };

    // 메시지 보내기 함수
    // 새로운 메시지 생성 요청
  const handleSendMessage = async (e) => {
    e.preventDefault();

    const newMessageInfo = {
      message,
      messenger: user.id,
    };

    try {
      await pb.collection('chats').create(newMessageInfo);
      setMessage('');
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <div>
      <h2 className="text-4xl font-extralight">Chats</h2>
      <section
        ref={chatsFrameRef}
        className="overflow-auto flex flex-col gap-y-2 h-[400px] my-6 p-6 border-4 border-sky-600/20 rounded-lg"
      >
        <h3 className="sr-only">채팅 메시지</h3>
        {isLoading && (
          <div className="text-sm text-sky-700/70">메시지 로딩 중...</div>
        )}
        {isLoading ||
          data.items.map((messageInfo) => (
            <Message
              key={messageInfo.id}
              type={messageInfo.messenger === user?.id ? 'me' : 'other'}
              messageInfo={messageInfo}
            />
          ))}
      </section>
      <form
        onSubmit={handleSendMessage}
        className="flex space-x-2 items-center justify-between"
      >
        <img
          src={getPbImageURL(user, 'photo')}
          className="w-9 h-9 object-cover rounded-full"
          alt={user.name}
          title={user.name}
        />
        <input
          type="text"
          aria-label="메시지"
          placeholder="메시지를 입력하세요."
          className="flex-1 py-1 px-4 border border-sky-600 rounded-full"
          value={message}
          onChange={handleMessage}
        />
        <button
          type="submit"
          aria-label="보내기"
          className="w-8 h-8 grid place-content-center"
        >
          <motion.svg
            whileHover={{
              scale: 1.6,
            }}
            className="text-sky-700"
            width={24}
            height={24}
            viewBox="0 0 24 24"
            strokeWidth={1}
            stroke="currentColor"
            fill="none"
            strokeLinecap="round"
            strokeLinejoin="round"
          >
            <path stroke="none" d="M0 0h24v24H0z" fill="none" />
            <path d="M15 10l-4 4l6 6l4 -16l-18 7l4 2l2 6l3 -4" />
          </motion.svg>
        </button>
      </form>
    </div>
  );
}

// Message 컴포넌트
function Message({ type = 'me', messageInfo }) {
    // 인터페이스 상 메시지 박스 위치 조정을 위한 조건
  const isMeMessage = type === 'me';
  const messengerTypeClasses = isMeMessage ? `justify-end` : `justify-start`;

    // 메신저 이름 및 사진
  const messenger = messageInfo.expand?.messenger;
  const messengerPhoto = getPbImageURL(messenger, 'photo');

    // 시간 정보
  const time = new Date(messageInfo.created)
    .toLocaleTimeString('ko-KR')
    .split(':')
    .slice(0, 2)
    .join(':');

  return (
    <article className={`flex gap-x-2 ${messengerTypeClasses}`}>
      <img
        src={messengerPhoto}
        alt=""
        className={`${
          isMeMessage ? 'sr-only' : ''
        } w-10 h-10 object-cover rounded-full`}
      />
      <div className="flex flex-col space-y-1">
        <h4 className={isMeMessage ? 'sr-only' : ''}>{messenger.name}</h4>
        <p className="flex gap-x-3 items-end justify-end">
          <span
            className={`${
              isMeMessage ? 'bg-sky-200 order-1' : 'bg-zinc-200'
            } inline-block py-3 px-6 max-w-[240px] rounded-full text-sm text-left`}
          >
            {messageInfo.message}
          </span>
          <time
            dateTime={messageInfo.created}
            className="text-xs text-zinc-400"
          >
            {time}
          </time>
        </p>
      </div>
    </article>
  );
}

export default Chats;