카테고리 없음

Fiesta Tour_7일차 ( 리뷰 별점 및 1:1 채팅 기능 )

juni_shin 2024. 7. 24. 21:28

검색해서 정리해보기

  • CORS
  • "gen": "npx supabase gen types typescript --project-id netvvmmkvqmzzmsgzjgx --schema public > src/types/supabase.ts"-> supabase.ts (type폴더) supabase type 파일 만들었는데 혹시 테이블 내용이 수정되면 'yarn gen' 하시면 업데이트 됩니다. 그런데 그 전에 yarn 하시고 .. yarn add supabase --dev 한번씩만 해주세요!!
  • if (e.key === 'Enter' && tagInput.trim() !== '')
  • script.onload

 

마이페이지 리뷰 별점 구현

별점 0~5점으로 평균값 구하기 가능

 

'use client';

import axios from 'axios';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import Rating from 'react-rating-stars-component';
import { API_MYPAGE_REVIEWS } from '@/utils/apiConstants';

type Review = {
  id: string;
  created_at: string;
  post_id: string;
  user_id: string;
  content: string;
  rating: number;
};

const ReviewList = ({ userId }: { userId: string }) => {
  const [reviews, setReviews] = useState<Review[]>([]);

  const handleDelete = async (id: string) => {
    await axios.delete(API_MYPAGE_REVIEWS(userId), { data: { id } });
    setReviews(reviews.filter((review) => review.id !== id));
  };

  const fetchReviews = async () => {
    const response = await axios.get(API_MYPAGE_REVIEWS(userId));
    setReviews(response.data);
  };

  useEffect(() => {
    fetchReviews();
  }, [userId]);

  return (
    <div>
      <Link href={`/${userId}/review-page`}>
        <button>Create New Review</button>
      </Link>
      {reviews.length === 0 ? (
        <div>Loading...</div>
      ) : (
        reviews.map((item) => (
          <div key={item.id}>
            <Rating count={5} value={item.rating} size={24} edit={false} activeColor="#ffd700" />
            <p>{item.content}</p>
            <div className="mt-2 flex justify-around">
              <Link href={`/${userId}/review-page?id=${item.id}`}>
                <button>Edit</button>
              </Link>
              <button onClick={() => handleDelete(item.id)}>Delete</button>
            </div>
          </div>
        ))
      )}
    </div>
  );
};

export default ReviewList;

 

'use client';

import axios from 'axios';
import { useEffect, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { v4 as uuidv4 } from 'uuid';
import Rating from 'react-rating-stars-component';
import { API_MYPAGE_REVIEWS } from '@/utils/apiConstants';

type Review = {
  id: string;
  created_at: string;
  post_id: string;
  user_id: string;
  content: string;
  rating: number;
};

const ReviewForm = ({ userId }: { userId: string }) => {
  const [review, setReview] = useState<Review | null>(null);
  const [content, setContent] = useState('');
  const [rating, setRating] = useState(0);
  const router = useRouter();
  const searchParams = useSearchParams();
  const id = searchParams.get('id');

  const handleSubmit = async (event: React.FormEvent) => {
    event.preventDefault();
    if (id) {
      await axios.put(API_MYPAGE_REVIEWS(userId), { id, content, rating });
    } else {
      const postId = uuidv4();
      await axios.post(API_MYPAGE_REVIEWS(userId), { content, rating, post_id: postId, user_id: userId });
    }
    router.back();
  };

  const handleBack = () => {
    router.back();
  };

  const fetchReview = async () => {
    if (id) {
      const response = await axios.get(`${API_MYPAGE_REVIEWS(userId)}?id=${id}`);
      const reviewData = response.data[0];
      setReview(reviewData);
      setContent(reviewData.content);
      setRating(reviewData.rating);
    }
  };

  const ratingChanged = (newRating: number) => {
    setRating(newRating);
  };

  useEffect(() => {
    fetchReview();
  }, [id]);

  return (
    <div>
      <h1>{id ? 'Edit Review' : 'New Review'}</h1>
      <form onSubmit={handleSubmit}>
        <button className="mt-4" onClick={handleBack}>
          Go Back
        </button>
        <p className="mt-4">별점</p>
        <Rating count={5} value={rating} onChange={ratingChanged} size={24} activeColor="#ffd700" />
        <p className="mt-4">내용</p>
        <input
          className="text-black"
          type="text"
          value={content}
          onChange={(e) => setContent(e.target.value)}
          placeholder="Content"
        />
        <div className="mt-10 flex max-w-full items-center justify-center">
          <button type="submit">{id ? 'Update Review' : 'Add Review'}</button>
        </div>
      </form>
    </div>
  );
};

export default ReviewForm;

 

1:1 채팅 기능

기술적 의사결정

리얼타임 (Real-time)

  1. 폴링(Polling):
    • 클라이언트가 주기적으로 서버에 요청을 보내서 새로운 메시지가 있는지 확인하는 방식입니다.
    • 예를 들어, 매 5초마다 서버에 요청을 보내서 새로운 메시지가 있는지 체크합니다.
    • 구현이 상대적으로 간단하지만, 서버에 부하를 줄 수 있고, 실시간성에서 조금 떨어질 수 있습니다.
  2. 롱 폴링(Long Polling):
    • 클라이언트가 서버에 요청을 보내고, 서버가 새로운 메시지가 있을 때까지 응답을 지연시키는 방식입니다.
    • 새로운 메시지가 도착하면 서버가 응답하고, 클라이언트는 즉시 또 다른 요청을 보냅니다.
    • 폴링보다 실시간성이 높지만, 여전히 서버에 부하가 있을 수 있습니다.

웹소켓 (WebSocket)

  • 웹소켓은 클라이언트와 서버 간의 상호작용을 실시간으로 유지하기 위해 지속적인 연결을 유지하는 프로토콜입니다.
  • 클라이언트와 서버가 처음 연결을 설정한 후, 양방향 통신이 가능하며, 메시지가 있을 때마다 실시간으로 주고받을 수 있습니다.
  • 실시간성에서 가장 우수하고, 서버 부하도 폴링 방식보다 낮습니다.
  • 설정과 관리가 복잡할 수 있지만, 최적의 실시간 통신을 제공합니다.

주요 차이점 요약

  • 실시간성: 웹소켓이 리얼타임(폴링, 롱 폴링) 방식보다 실시간성이 뛰어납니다.
  • 서버 부하: 웹소켓은 지속적인 연결을 통해 효율적인 통신을 가능하게 하며, 폴링 방식은 주기적인 요청으로 인해 서버 부하가 더 많을 수 있습니다.
  • 구현 난이도: 폴링 방식은 구현이 상대적으로 쉬운 반면, 웹소켓은 설정과 관리가 조금 더 복잡할 수 있습니다.

리얼타임 방식 채택하여 ui 및 유저들간의 채팅이 잘 되는지 테스트 중

네비바에서 공통적으로 접근하고 사용하기 위해 폴더 구조 공통 구역에 할당

 

import { createClient } from '@/utils/supabase/client';

const supabase = createClient();

export const fetchMessages = async (senderId: string, receiverId: string) => {
  const { data, error } = await supabase
    .from('messages')
    .select('*')
    .or(
      `and(sender_id.eq.${senderId},receiver_id.eq.${receiverId}),and(sender_id.eq.${receiverId},receiver_id.eq.${senderId})`
    )
    .order('created_at', { ascending: true });

  if (error) {
    console.error(error);
    return [];
  }

  return data;
};

export const sendMessage = async (senderId: string, receiverId: string, content: string) => {
  const { data, error } = await supabase
    .from('messages')
    .insert([{ sender_id: senderId, receiver_id: receiverId, content }]);

  if (error) {
    console.error(error);
    return null;
  }

  return data;
};

 

import { useEffect, useState } from 'react';
import { fetchMessages, sendMessage } from '../services/chatService';

type Message = {
  id: string;
  sender_id: string;
  receiver_id: string;
  content: string;
  created_at: string;
};

type ChatProps = {
  senderId: string;
  receiverId: string;
};

const Chat: React.FC<ChatProps> = ({ senderId, receiverId }) => {
  const [messages, setMessages] = useState<Message[]>([]);
  const [newMessage, setNewMessage] = useState<string>('');

  useEffect(() => {
    const interval = setInterval(async () => {
      const fetchedMessages = await fetchMessages(senderId, receiverId);
      setMessages(fetchedMessages);
    }, 3000);

    return () => clearInterval(interval);
  }, [senderId, receiverId]);

  const handleSend = async () => {
    if (newMessage.trim()) {
      await sendMessage(senderId, receiverId, newMessage);
      setNewMessage('');
      const fetchedMessages = await fetchMessages(senderId, receiverId);
      setMessages(fetchedMessages);
    }
  };

  return (
    <div className="flex h-screen flex-col border border-gray-300">
      <div className="flex-1 overflow-y-auto p-4">
        {messages.map((msg) => (
          <div
            key={msg.id}
            className={`mb-2 rounded p-2 ${
              msg.sender_id === senderId ? 'self-end bg-green-200' : 'self-start bg-white'
            }`}
          >
            {msg.content}
          </div>
        ))}
      </div>
      <div className="flex border-t border-gray-300 p-4">
        <input
          type="text"
          value={newMessage}
          onChange={(e) => setNewMessage(e.target.value)}
          className="flex-1 rounded border border-gray-300 p-2"
        />
        <button onClick={handleSend} className="ml-2 rounded bg-blue-500 p-2 text-white">
          Send
        </button>
      </div>
    </div>
  );
};

export default Chat;

 

우선 마이페이지 내에서 사용 중

받는 사람 id값 및 채팅 리스트 관리에대해 설계 중

'use client';

import { useState } from 'react';
import PostList from './_components/PostList';
import ProfileView from './_components/ProfileView';
import ReviewList from './_components/ReviewList';
import { useParams, useRouter } from 'next/navigation';
import Chat from '@/components/Chat';

const MyPage = () => {
  const { id } = useParams() as { id: string };
  const router = useRouter();
  const [selectedComponent, setSelectedComponent] = useState<string | null>(null);

  const handleBack = () => {
    router.back();
  };

  const senderId = id;
  const receiverId = '105c1498-4dd0-421c-ad1b-d698f2217c67';

  return (
    <div>
      <h1 className="mb-4">My Page</h1>
      <Chat senderId={senderId} receiverId={receiverId} />
      <button onClick={handleBack}>Go Back</button>
      <ProfileView userId={id} />
      <div className="mb-2 mt-4 flex justify-around">
        <button onClick={() => setSelectedComponent('reviews')}>Reviews</button>
        <button onClick={() => setSelectedComponent('posts')}>Posts</button>
      </div>
      {selectedComponent === 'reviews' && <ReviewList userId={id} />}
      {selectedComponent === 'posts' && <PostList />}
    </div>
  );
};

export default MyPage;

 

 

회의 내용

 

공통 컴포넌트에서 필요한 부분들을 미리 분류해서 만들어둬야 리소스 낭비를 막을 것이다
퍼널 패턴 ( 디자인 패턴 ) 앱에서 페이지가 너무 많기때문에 사용하는 방법
step을 상태 관리하여 페이지가 넘어가는듯한 효과를 줌
매개변수를 3개 받지만 타입을 옵셔널로 선언하면 조율도 가능하다

컴포넌트 기준도
organisms / molecules / atoms 이런식으로 세세하게 분류도 필요할듯
리스트 / 다수의 버튼 / 하나의 버튼 이렇게 분류까지해야 재사용이 용이하다
좀 늦더라도 코드 한줄 한줄 퀄리티를 올려야한다
그래야 포폴에 쓰일 수준이된다 -> 튜터님 말씀
useState에서 제네릭을 쓰냐 안쓰냐도 있고
정말 세세한 컨벤션도 많은데 이런 것들은 정말 많은 회의로 조정해야 퀄리티가 올라간다

커밋 , 특히 pr 내용 구체화해서 자세히 쓰기 A10조가 정말 좋은 참고자료인듯

 

오후 튜터님
발제 때 나온 여러 문제들에대해서
본인의 언어로 표현하는게 가장 중요하므로
모범 답안도 있지만 자기의 생각을 정리하는 시간을 꼭 가져야한다
코어 자바스크립트 ( 딥다이브는 너무 딥하다 ) 가 면접을 위한 공부로는 충분할것이다
다음주 부터 주1번씩
2주 담당 튜터님 ( 예상 ) 1주 기업 지원 튜터님 ( 이 때가 빡쌤 )
코어 자바스크립트 3회독 ( 대충 200페이지 조금 넘음 ) -> 이러면 학습 많이 됨
실제 면접에서 JS 기술 면접 질문이 70%이상이다

리뷰 테이블에 상세페이지에서 리뷰 닉네임도 알아야하기때문에 닉네임 컬럼 추가는 어떨지?
유저테이블에 있긴하지만 비효율적인가?

좋아요는 상태관리가 돼야 구현이 가능해서 유저 정보를 zustand로 관리가 우선적으로 필요함
게시글 테이블도 날짜별 컬럼을 상세하게 분리 기획 중 ( 날짜 및 장소 별도 관리하게끔 )

수파베이스 테이블마다 컬럼의 타입을 선언하는걸 Constants.ts처럼 모아서 관리하도록하기
Query에서 loLoading 대신 isPending

리뷰를 볼 때 닉네임까지 보려면 리뷰,유저 2개의 테이블 정보가 필요한데 2개를 불러와서 필요한 값을 쓰는게
관계형? 맞는지 모르겠으나 이 점을 고려하면 해야하는지 / 혹은 그냥 테이블1개에 컬럼을 추가해야하는지 확인 필요

리뷰 api 직접 어떻게 나오는지 코드 다시 한 번 검토해보기