본문 바로가기

카테고리 없음

TeamProject_240620 (아웃소싱 프로젝트)

import { useEffect, useState } from 'react';
import { useAddVideo } from '../lib/supabase/videoApi';
import { searchYouTubeVideos } from '../lib/api/youtubeAPI';
import { ToastContainer, toast } from 'react-toastify';
import { supabase } from '../lib/supabase/supabase';

const MainPage = () => {
  const [query, setQuery] = useState('');
  const [searchResults, setSearchResults] = useState([]);
  const [pageToken, setPageToken] = useState('');
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);
  const addVideoMutation = useAddVideo();

  const searchVideos = async (query, pageToken = '', reset = false) => {
    setLoading(true);
    try {
      const youtubeVideos = await searchYouTubeVideos(query, pageToken);
      setSearchResults((prevResults) =>
        reset
          ? youtubeVideos.items.map((video) => ({
              video_title: video.snippet.title,
              video_id: video.id.videoId
            }))
          : [
              ...prevResults,
              ...youtubeVideos.items.map((video) => ({
                video_title: video.snippet.title,
                video_id: video.id.videoId
              }))
            ]
      );
      setPageToken(youtubeVideos.nextPageToken || '');
    } catch (error) {
      console.error('Error fetching data from YouTube API:', error);
    }
    setLoading(false);
  };

  const handleSearch = async (e) => {
    e.preventDefault();
    searchVideos(query, '', true);
  };

  const handleAddVideo = (video) => {
    if (!user) {
      toast.error('로그인이 필요합니다.');
      return;
    }

    const videoLike = {
      ...video,
      video_like: user.id
    };
    try {
      if (!toast.isActive('addVideo')) {
        toast.success('해당 영상이 저장되었습니다.', {
          toastId: 'addVideo'
        });
        addVideoMutation.mutate(videoLike);
      }
    } catch (error) {
      console.error('Error adding video:', error);
    }
  };

  useEffect(() => {
    const fetchUserAndSelection = async () => {
      try {
        const {
          data: { user }
        } = await supabase.auth.getUser();
        if (!user) {
          toast.error('로그인이 필요합니다.');
          return;
        }
        const { data, error } = await supabase.from('users').select('selection').eq('id', user.id).single();
        if (error || !data) {
          toast.error('설문조사를 완료해야 합니다.');
          return;
        }

        const selectionQuery = `${data.selection.level} ${data.selection.topics.join('|')}`;
        setUser({ ...user, selection: data.selection });
        setQuery(selectionQuery);
        searchVideos(selectionQuery, '', true); // Reset results and search
      } catch (error) {
        console.error('Error in fetchUserAndSelection:', error);
        toast.error('사용자 정보를 가져오는 중 오류가 발생했습니다.');
      }
    };

    fetchUserAndSelection();
  }, []);

  return (
    <div>
      <form onSubmit={handleSearch}>
        <h1 className="mb-5 flex justify-center font-['DungGeunMo'] text-6xl">DevTube</h1>
        <h2 className="mb-5 flex justify-center font-['DungGeunMo'] text-xl">더 많은 내용을 검색하세요!</h2>
        <div className="mb-8 flex items-center justify-center">
          <input
            autoFocus
            className="mb-2 box-border rounded border-2 p-1"
            type="text"
            onChange={(e) => setQuery(e.target.value)}
            placeholder=" 오늘은 무슨 공부를 할까?"
          />
          <button
            className="border-3 mb-2 ml-4 flex cursor-pointer items-center justify-center rounded bg-yellow-300 p-2 text-sm font-bold text-black no-underline hover:underline"
            type="submit"
          >
            &nbsp;검색&nbsp;
          </button>
        </div>
      </form>
      <div className="grid grid-cols-3 gap-4">
        {searchResults.map((video) => (
          <div key={video.video_id}>
            <h3 dangerouslySetInnerHTML={{ __html: video.video_title }} className="mb-2 w-full truncate font-bold"></h3>
            <div className="aspect-h-9 aspect-w-16">
              <iframe
                className="h-full w-full"
                src={`https://www.youtube.com/embed/${video.video_id}`}
                allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
                allowFullScreen
              ></iframe>
            </div>
            <button className="mt-2 font-bold text-green-500" onClick={() => handleAddVideo(video)}>
              저장하기
            </button>
          </div>
        ))}
      </div>
      {pageToken && (
        <div className="mt-4 flex justify-center">
          <button
            className="border-3 flex cursor-pointer items-center justify-center rounded bg-black p-2 text-sm font-bold text-white no-underline hover:underline"
            onClick={() => searchVideos(query, pageToken)}
            disabled={loading}
          >
            {loading ? '로딩 중...' : '더보기'}
          </button>
        </div>
      )}
    </div>
  );
};

export default MainPage;

 

영상을 6개 단위로 더보기 기능을 추가하여 많은 정보를 효과적으로 보여주기 성공

import axios from 'axios';

const apiKey = import.meta.env.VITE_YOUTUBE_API_KEY;
const cgi_1 = 27;
const cgi_2 = 28;

export const searchYouTubeVideos = async (query, pageToken = '') => {
  const url = `https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&maxResults=6&videoCategoryId=${(cgi_1, cgi_2)}&q=${query}&key=${apiKey}&pageToken=${pageToken}`;

  try {
    const response = await axios.get(url);
    return response.data;
  } catch (error) {
    console.error('Error fetching data from YouTube API:', error);
    throw error;
  }
};

 

import { ToastContainer, toast } from 'react-toastify';
import { useVideos, useDeleteVideo } from '../lib/supabase/videoApi';
import { Link, useNavigate } from 'react-router-dom';
import { useEffect, useRef, useState } from 'react';
import { supabase } from '../lib/supabase/supabase';
import useIdStore from '../zustand/idStore';
import { updateUserNickname } from '../lib/supabase/userApi';

const MyPage = () => {
  const [user, setUser] = useState(null);
  const { data: videos, error: fetchError, isLoading } = useVideos();
  const deleteVideoMutation = useDeleteVideo();
  const { id } = useIdStore((state) => state);
  const [nickname, setNickname] = useState('');
  const nicknameInput = useRef();
  const navigate = useNavigate();

  const likeVideos = videos ? videos.filter((video) => video.video_like === user?.id) : [];

  const handleDelete = (id) => {
    try {
      toast.success('해당 영상이 삭제되었습니다.');
      deleteVideoMutation.mutate(id);
    } catch (error) {
      console.error('Error deleting video:', error);
    }
  };

  const updateNickname = async (value, userId) => {
    if (!(value.length >= 4 && value.length <= 10)) {
      toast.error('닉네임은 4자리 이상, 10자리 이하여야 합니다.');
      nicknameInput.current.focus();
      return;
    }
    if (!confirm(`닉네임을 '${value}'로(으로) 변경하시겠습니까?`)) {
      nicknameInput.current.focus();
      return;
    }
    const { data, error } = await updateUserNickname(value, userId);
    if (error) {
      toast.error('닉네임 변경에 실패했습니다. 다시 로그인하시길 바랍니다.');
      nicknameInput.current.focus();
      return;
    }
    if (data) {
      navigate(0);
      return;
    }
  };

  useEffect(() => {
    const fetchUser = async () => {
      const {
        data: { user }
      } = await supabase.auth.getUser();
      setUser(user);
    };
    fetchUser();
  }, []);

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (fetchError) {
    return <div>Error: {fetchError.message}</div>;
  }

  return (
    <>
      <div className="mb-20 flex w-full flex-col gap-8">
        <h1 className="font-['DungGeunMo'] text-4xl font-bold">닉네임 변경</h1>
        <div className="flex gap-5">
          <input
            className="min-w-72 border-2 border-black p-2 outline-none"
            type="text"
            placeholder="닉네임을 입력해주세요"
            value={nickname}
            ref={nicknameInput}
            onChange={(e) => {
              setNickname(e.target.value);
            }}
          />
          <button
            className="border-2 border-slate-300 bg-slate-100 pl-5 pr-5 hover:bg-slate-200"
            onClick={() => updateNickname(nickname, id)}
          >
            완료
          </button>
          <button
            className="border-2 border-red-500 bg-red-300 pl-5 pr-5 hover:bg-red-400"
            onClick={() => navigate('/')}
          >
            메인으로
          </button>
        </div>
      </div>
      <div className="mt-8">
        {/* <h1 className="flex justify-center font-bold">Saved Videos</h1> */}
        <h1 className="mb-8 flex justify-start font-['DungGeunMo'] text-4xl font-bold">저장한 영상</h1>

        <div className="grid grid-cols-3 gap-10">
          {likeVideos.map((video) => (
            <div key={video.id}>
              {/* <h3 dangerouslySetInnerHTML={{ __html: video.video_title }} className="w-full truncate"></h3> */}
              <h3
                dangerouslySetInnerHTML={{ __html: video.video_title }}
                className="mb-2 w-full truncate font-bold"
              ></h3>

              <div className="aspect-h-9 aspect-w-16">
                <iframe
                  className="h-full w-full"
                  src={`https://www.youtube.com/embed/${video.video_id}`}
                  allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
                  allowFullScreen
                ></iframe>
              </div>
              <button className="mt-2 font-bold text-red-500" onClick={() => handleDelete(video.id)}>
                삭제하기
              </button>
            </div>
          ))}
        </div>
      </div>
    </>
  );
};

export default MyPage;

 

닉네임 수정도 가능