탭 구현을 위한 dynamic routing

김예린·2024년 5월 10일
0
post-custom-banner


처음에 이 부분의 헤더 작업을 맡게 되었음.
(현재는 북클럽 선택이 모달로 바뀌었지만, 처음에는 select 디자인을 받았음 -> 그래서 select 태그로 작업)

처음 시도했던 방법

하나의 app 폴더를 만들어서 그 안에서 해결
/myclubinfo url하나로 useState활용해서 탭을 구현했다

'use client';
import React from 'react';
import { getClubInfo, getUserId } from '@/utils/userAPIs/authAPI';
import { getUserClubIds } from '@/utils/userAPIs/authAPI';
import { useState } from 'react';
import { useEffect } from 'react';
import HomeTab from '@/components/myclubinfo2/HomeTab';
import Board from '@/components/myclubinfo/Board';
import { Tables } from '@/lib/types/supabase';
import SentenceStorage from '@/components/myclubinfo2/SentenceStorage';
import NonMyClub from '@/components/myclubinfo2/NonMyClub';
type Clubs = Tables<'clubs'>;
const MyClubInfo = () => {
  const [loading, setLoading] = useState(true);
  const [clubInfo, setClubInfo] = useState<Clubs[]>([]);
  const [userId, setUserId] = useState<string | null>(null);
  const [selectedTab, setSelectedTab] = useState('home');
  const [selectedClubId, setSelectedClubId] = useState<string>('');
  useEffect(() => {
    const fetchData = async () => {
      try {
        const fetchedUserId = await getUserId();
        setUserId(fetchedUserId);

        if (fetchedUserId) {
          const fetchedClubIds = await getUserClubIds(fetchedUserId);
          const fetchClubInfo = await getClubInfo(fetchedClubIds);
          setClubInfo(fetchClubInfo);
          if (fetchedClubIds.length > 0) {
            setSelectedClubId(fetchedClubIds[0]);
          }
        }
      } catch (error) {
        console.error('Error fetching data:', error);
      }
    };

    fetchData();
  }, []);

  const handleClubChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
    setSelectedClubId(event.target.value);
  };
  const handleTabChange = (tab: string) => {
    setSelectedTab(tab);
  };
  const getSelectClasses = () => {
    if (clubInfo.length <= 1) {
      return 'appearance-none w-[200px] font-bold p-2 text-lg';
    }
    return ' p-2 w-[200px] font-bold';
  };
  const renderSelectedTab = () => {
    const selectedClub = clubInfo.find((club) => club.id === selectedClubId);
    if (!selectedClub) {
      return <NonMyClub />;
    }
    switch (
      selectedTab //quiz tab 추가해야함.
    ) {
      case 'home':
        return <HomeTab club={selectedClub} />;
      case 'sentenceStorage':
        return <SentenceStorage clubId={selectedClubId} userId={userId} />;
      case 'board':
        return <Board club={selectedClub} />;
      default:
        return null;
    }
  };
  return (
    <div>
      <div className='sticky top-0 left-0 right-0 z-10 bg-white flex flex-col justify-between'>
        {/* 북클럽 셀렉트 박스 */}
        <select
          value={selectedClubId || ''}
          onChange={handleClubChange}
          className={getSelectClasses()}
          //   disabled={clubInfo.length <= 1}
        >
          {clubInfo.length === 0 && (
            <option value='' className='w-[200px]'>
              내 북클럽
            </option>
          )}
          {clubInfo.map((club) => (
            <option key={club.id} value={club.id} className='w-[200px]'>
              {club.name}
            </option>
          ))}
        </select>

        {/* 탭 버튼들 */}
        <div className='flex flex-row justify-between w-full border-b-2 border-gray-200 font-bold'>
          <button
            className={`flex-1 px-4 py-2 focus:outline-none ${
              selectedTab === 'home' ? ' border-b-2 border-black' : ''
            }`}
            onClick={() => handleTabChange('home')}></button>
          <button
            className={`flex-1 px-4 py-2 focus:outline-none ${
              selectedTab === 'sentenceStorage' ? 'border-b-2 border-black' : ''
            }`}
            onClick={() => handleTabChange('sentenceStorage')}>
            문장 저장소
          </button>
          <button
            className={`flex-1 px-4 py-2 focus:outline-none ${
              selectedTab === 'board' ? 'border-b-2 border-black' : ''
            }`}
            onClick={() => handleTabChange('board')}>
            자유 게시판
          </button>
        </div>
      </div>

      {/* 탭 컨텐츠 */}
      <div>{renderSelectedTab()}</div>
    </div>
  );
};

export default MyClubInfo;

선택한 클럽을 state로 관리하기 때문에 이 안에서 renderSelectedTab으로 관리해야했다.

내가 처음에 이 방법을 선택한 이유

  1. 로그인/회원가입 후에 처음 들어오는 화면이 이 화면이고,(첫 화면이기 때문에 선태한 북클럽이 없을때임) 선택한 북클럽이 없으니 첫번째 북클럽이 선택되어야함
    => 그래서 url하나로 하고 내 북클럽들을 가져올때의 로직에서 첫번째 북클럽을 선택해야한다고 생각했음. 다이나믹 라우트를 사용해서 Link로 넘기면 탭(정보, 문장저장소, 퀴즈, 자유게시판)을 선택해야만 된다고 생각했음(왜 그랬지?)
  2. 그러면 어쨌든 북클럽을 선택할때마다 탭들의 정보가 그에 맞는 정보들로 바뀌어야하니까 선택한 클럽아이디를 아래의 탭들에 뿌려줘야 정보들이 바뀐다고 생각.
  3. 북클럽이 없을때의 헤더와 화면이 디자인과 화면이 달라서(헤더의 색이 회색으로 헤더의 북클럽있는 부분이 내 북클럽으로 등등 조건부 스타일링과 조건걸어서 셀렉트바 부분을 바꿔야한다고 생각했음)

다이나믹 라우트로 바꾼 이유

  1. 일단 처음 방법이 문제가 많았음 ㅋ -> 새로고침하면 처음으로 돌아가는 이슈
    로컬스토리지 저장방법도 해봤지만 적합한 방법이 아니라고 생각.
  2. 다이나믹 라우트로 [clubId]를 넘겨서 url에 클럽아이디를 가지고 가면 그에 맞는 데이터들을 충분히 가져올수 있음
  3. 북클럽이 없을때의 화면을 직접 그냥 디자인했음.-> 북클럽이 없으면 "내 북클럽" 있으면 북클럽 보여준다. 이게 아니라 그냥 없을때 화면을 따로 만듬
    파일구조
    /my-clubs/[clubId]/info
    이런식으로 클럽아이디를 가져갈 수 있게 함
    제일 고민했던게 일단 /my-clubs로 들어가면 젤 상위의 /my-clubs의 page.tsx로 가는데 거기서 어떻게 이동시킬것인가...였다.. 그냥 간단하게
'use client';

import NonMyClub from '@/components/my-clubs/info/NonMyClub';
import { Tables } from '@/lib/types/supabase';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import useMyClubInfo from '@/hooks/info/useMyClubInfo';
import Image from 'next/image';
type Clubs = Tables<'clubs'>;
import Animation from '@/components/common/LoadingAnimation';
type Props = {};

const Page = () => {
  // TODO: 내 북클럽 찾아서 첫번째 녀석으로 리다이렉션

  const router = useRouter();
  const { clubs, isLoading } = useMyClubInfo();
  const nonArchivedClubs = clubs.filter((club) => !club.archive);
  const club = nonArchivedClubs[0];

  useEffect(() => {
    if (club) {
      router.push(`/my-clubs/${club.id}/info`);
    }
  }, [club, router]);

  if (isLoading) {
    return (
      <div className='h-screen flex justify-center items-center align-middle '>
        <div className='w-[250px]'>
          <Animation />
        </div>
      </div>
    );
  }

  if (!club) {
    return (
      <>
        <div className='flex items-center h-[56px] '>
          <p className='px-4 text-[22px] font-bold text-[#292929]'>내 북클럽</p>
        </div>
        <div className='border-b-2 h-[40px] flex'>
          <div className='flex-1 text-center py-2 border-gray-200'>
            <span className='text-[16px] text-[#3A3B42] text-opacity-50'>
              정보
            </span>
          </div>
          <div className='flex-1 text-center py-2 border-gray-200'>
            <span className='text-[16px] text-[#3A3B42] text-opacity-50'>
              문장 저장소
            </span>
          </div>
          <div className='flex-1 text-center py-2 border-gray-200'>
            <span className='text-[16px] text-[#3A3B42] text-opacity-50'>
              퀴즈
            </span>
          </div>
          <div className='flex-1 text-center py-2'>
            <span className='text-[16px] text-[#3A3B42] text-opacity-50'>
              자유 게시판
            </span>
          </div>
        </div>
        <NonMyClub />
      </>
    );
  }

  return null;
};

export default Page;

단순하게 클럽들 가져와서 진행중인 클럽중에 첫번째 선택해서 info 탭으로 보내버림

그리고 헤더탭은 Layout으로 my-clubs에 항상 있을 수 있게 했다!

'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import React from 'react';
import ClubSelector from './ClubSelector';
import useMyClubInfo from '@/hooks/info/useMyClubInfo';
import Animation from '@/components/common/LoadingAnimation';
type Props = {
  children: React.ReactNode;
  params: {
    clubId: string;
  };
};

const Layout = ({ children, params }: Props) => {
  const pathname = usePathname();
  const isSelected = (path: string) => pathname.includes(path);
  const { clubs, isLoading } = useMyClubInfo();
  if (isLoading) {
    return (
      <div className='h-screen flex justify-center items-center align-middle '>
        <div className='w-[250px]'>
          <Animation />
        </div>
      </div>
    );
  }
  return (
    <div>
      <div className='sticky top-0 left-0 right-0 z-10 bg-white flex flex-col justify-between'>
        {/* 북클럽 셀렉트 박스 */}
        <div className='relative inline-block'>
          <ClubSelector clubs={clubs} currentClubId={params.clubId} />
        </div>
        <div className='flex flex-row justify-between w-full font-bold'>
          <Link
            prefetch={true}
            href={`/my-clubs/${params.clubId}/info`}
            className={`flex flex-1 px-2 py-2 focus:outline-none justify-center ${
              isSelected('info')
                ? 'border-b-2 border-[#3A3B42] text-[#3A3B42]'
                : 'border-b-2 border-[#DBE3EB] text-[#3A3B42] opacity-50 font-medium'
            }`}>
            <span>정보</span>
          </Link>

          <Link
            prefetch={true}
            href={`/my-clubs/${params.clubId}/sentences`}
            className={`flex flex-1 px-2 py-2 focus:outline-none justify-center ${
              isSelected('sentences')
                ? 'border-b-2 border-[#3A3B42] text-[#3A3B42]'
                : 'border-b-2 border-[#DBE3EB] text-[#3A3B42] opacity-50 font-medium'
            }`}>
            <span>문장 저장소</span>
          </Link>

          <Link
            prefetch={true}
            href={`/my-clubs/${params.clubId}/quizzes`}
            className={`flex flex-1 px-2 py-2 focus:outline-none justify-center ${
              isSelected('quizzes')
                ? 'border-b-2 border-[#3A3B42] text-[#3A3B42]'
                : 'border-b-2 border-[#DBE3EB] text-[#3A3B42] opacity-50 font-medium'
            }`}>
            <span>퀴즈</span>
          </Link>

          <Link
            prefetch={true}
            href={`/my-clubs/${params.clubId}/posts`}
            className={`flex flex-1 px-2 py-2 focus:outline-none justify-center ${
              isSelected('posts')
                ? 'border-b-2 border-[#3A3B42] text-[#3A3B42]'
                : 'border-b-2 border-[#DBE3EB] text-[#3A3B42] opacity-50 font-medium'
            }`}>
            <span>자유 게시판</span>
          </Link>
        </div>
      </div>
      <div>
        {/* 탭 컨텐츠 */}
        {children}
      </div>
    </div>
  );
};

export default Layout;

북클럽을 선택하는 ClubSelector부분에서는 생각할 것들이 많았다.
1. 내 북클럽들 중에 진행중인 것이 있고, 종료인 것이 있다. (애초에 진행중인것만 가져올수가 없었던게, 마이페이지에서 종료된 북클럽도 누르면 이동할수있게 해야했기 때문!)
2. 북클럽선택 모달에는 진행중인 북클럽 목록들만 나와야했다.(가져오는 데이터에는 진행중,종료가 다 있음)
북클럽이 한개이면 화살표가 생기지 않고, 두개이상부터 화살표가 생겨야했다. 또한 북클럽 한개이면 셀렉트바 눌러도 모달이 뜨면 안됐다.
3. 마이페이지에서 종료된 북클럽을 눌러서 /my-clubs/[종료된 북클럽아이디]/info로 넘어오면, 색을 회색처리하고, 종료된 북클럽으로 들어가면 진행중북클럽이 여러개라도 셀렉트 모달이 나오면 안됐다.그니까 셀렉트바가 눌리면 안됐다!

'use client';

import { IoIosArrowDown } from 'react-icons/io';
import { useParams, useRouter } from 'next/navigation';
import React from 'react';
import ResignModal from '@/components/my-clubs/info/ResignModal';
import { useState } from 'react';
import SelectModal from '@/components/my-clubs/info/SelectModal';

type Props = {
  clubs: { id: string; name: string; archive: string }[];
  currentClubId: string;
};
const ClubSelector = ({ clubs, currentClubId }: Props) => {
  const router = useRouter();
  const [resignModalOpen, setResignModalOpen] = useState(false); // ResignModal 상태 추가
  const [selectModalOpen, setSelectModalOpen] = useState(false);
  const ActiveClubs = clubs.filter((club) => !club.archive);
  const nonActiveClub = !ActiveClubs.map((club) => club.id).includes(
    currentClubId
  );
  const currentClub = clubs.find((club) => club.id === currentClubId);
  const handleClubSelect = (clubId: string) => {
    router.push(`/my-clubs/${clubId}/info`);
    setSelectModalOpen(false); 
  };
  if (!clubs || clubs.length === 0) {
    return <div className='h-[49px]'></div>;
  }
  return (
    <div className='font-bold text-[22px] whitespace-nowrap '>
      <div className='px-4 py-2 flex max-w-[350px] '>
        <div
          className='flex flex-row items-center overflow-hidden'
          onClick={() => {
            //진행중클럽이 1개이상이여도 현재클럽이 종료되었으면 모달 안뜨게함
            if (ActiveClubs.length > 1 && !nonActiveClub) {
              setSelectModalOpen(true);
            }
          }}>
          <span
            className={`font-bold truncate cursor-pointer ${
              currentClub && currentClub.archive ? 'text-fontGray' : ''
            }`}>
            {currentClub?.name}
          </span>
          {
            //종료된거 눌렀을때 어캐할지 정해야함. 종료된 북클럽이면 진행중인 클럽이 1개 이상이어도 화살표안뜨게함.
            !nonActiveClub && ActiveClubs.length > 1 && (
              <div className='w-5 h-5 ml-1'>
                <IoIosArrowDown />
              </div>
            )
          }
        </div>
      </div>
      {!nonActiveClub && (
        <div
          className='absolute top-0 right-0 h-full flex items-center mr-2 cursor-pointer'
          onClick={() => {
            setResignModalOpen(true);
          }}>
          <svg
            width='22'
            height='22'
            viewBox='0 0 22 22'
            fill='none'
            xmlns='http://www.w3.org/2000/svg'>
            <circle cx='11' cy='5' r='2' fill='#8A9DB3' />
            <circle cx='11' cy='11' r='2' fill='#8A9DB3' />
            <circle cx='11' cy='17' r='2' fill='#8A9DB3' />
          </svg>
        </div>
      )}

      <ResignModal
        clubId={currentClubId}
        isModal={resignModalOpen}
        onClose={() => {
          setResignModalOpen(false);
        }}
      />
      <SelectModal
        isModal={selectModalOpen}
        onClose={() => {
          setSelectModalOpen(false);
        }}
        clubs={ActiveClubs}
        currentClubId={currentClubId}
        onSelectClub={handleClubSelect}
      />
    </div>
  );
};

export default ClubSelector;

생각할것들이 많아서 다 끝났나????? 해도 계속 이러면안되는데.. 이런것들이 많이 생겼다...
그래도 만들고 나니까 디자인도 이쁘고 맘에들어서 기분 좋았당 ㅎㅎ

profile
아자아자
post-custom-banner

0개의 댓글