Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: 상품 목록 페이지 컴포넌트로 분리, 공통 Layout에 Header 수정 #13

Merged
merged 20 commits into from
Aug 1, 2024

Conversation

aquaman122
Copy link
Contributor

@aquaman122 aquaman122 commented Jul 24, 2024

💡 작업 내용

  • 상품 목록 페이지 컴포넌트로 분리
  • 공통 Layout에 Header 수정
  • 공통 컴포넌트 Popup.tsx 생성
  • 프로필 수정 페이지 마크업
  • 프로필 인풋 모듈 생성

💡 자세한 설명

🛠️ 공통 Layout에 Header 수정

// Layout.tsx
import { ReactNode } from 'react';

interface LayoutProps {
  header: ReactNode;
  children: ReactNode;
  footer?: ReactNode;
}

const Layout = ({ header, children, footer }: LayoutProps) => {
  return (
    <div className="flex justify-center w-full h-screen">
      <div className="w-[46rem] min-w-[23rem] flex flex-col justify-between h-full">
        {header && header}
        <main className="flex flex-col w-full gap-4 px-8 py-4 flex-grow min-h-0 overflow-y-scroll">
          {children}
        </main>
        {footer && footer}
      </div>
    </div>
  );
};

Layout.defaultProps = {
  footer: null,
};

export default Layout;
// Header.tsx
interface HeaderProps {
  children?: ReactNode;
  path: string;
  handleModal?: () => void;
}

const Header = ({ children, path, handleModal }: HeaderProps) => {
  const navigate = useNavigate();
  return (
    <header className="flex items-center justify-center p-4 border-b bg-white relative">
      <button
        className="text-gray-500 absolute left-2"
        aria-label="뒤로 가기"
        onClick={() => navigate(path)}
      >
        <AiOutlineLeft />
      </button>
      {children && <h1 className="text-lg font-semibold">{children}</h1>}
      {handleModal && (
        <BsThreeDotsVertical
          className="absolute right-2"
          onClick={handleModal}
        />
      )}
    </header>
  );
};

Header.defaultProps = {
  children: null,
  handleModal: null,
};

🛠️ 레이아웃 사용할 때

<Layout
  header={
     <Header></Header>
    }
  footer={<footer />}
>

🛠️ 상품 목록 페이지 컴포넌트로 분리

// ProductList.tsx
const ProductList = () => {
  const [activeTab, setActiveTab] = useState('ongoing');
  return (
    <Layout header="상품 경매 목록">
      <ProductListTabs activeTab={activeTab} setActiveTab={setActiveTab} />
      <ProductButtons />
      <div className="p-4 h-[calc(100vh-100px)] overflow-y-auto">
        {activeTab === 'ongoing'
          ? ongoingProducts.map((product) => (
              <OngoingProduct key={product.id} product={product} />
            ))
          : upcomingProducts.map((product) => (
              <UpcomingProduct key={product.id} product={product} />
            ))}
      </div>
    </Layout>
  );
};

🛠️ 공통 컴포넌트 Popup.tsx 생성

// Popup.tsx
interface PopupProps {
  children: ReactNode;
  width?: string;
  height?: string;
}

const Popup = ({ children, width, height }: PopupProps) => {

  return ReactDOM.createPortal(
    <div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-10">
      <div
        className={'bg-white p-5 rounded-lg shadow-lg'}
        style={{ width, height }}
        onClick={(e) => e.stopPropagation()}
      >
        {children}
      </div>
    </div>,
    document.getElementById('root')!
  );
};

🛠️ 팝업 컴포넌트 사용 방법

const Test = () => {
  return (
    <Popup width={'500px'} height={'400px'}>
      <PopupTitle>경매로 전환하시겠습니까?</PopupTitle>
      <PopupBody isParticipate={true} />
    </Popup>
  );
}

🛠️ 프로필 수정 페이지 마크업

const ProfileEditPage = () => {
  const [profileName, setProfileName] = useState<string>('최대열다섯글자');
  const [profileIntro, setProfileIntro] = useState<string>(
    '안녕하세요. 나이키 직영 조던 정품 취급 전문가입니다.',
  );
  const [profileRegion, setProfileRegion] = useState<string>('서울');
  const [activeButtonSheet, setActiveButtonSheet] = useState(false);
  const [link, setLink] = useState('');

  const onCloseBottomSheet = () => {
    setActiveButtonSheet(!activeButtonSheet);
  };

  return (
    <Layout header={<Header>프로필 수정</Header>}>
      <div className="flex flex-col px-4 py-6 space-y-4">
        <h2 className="text-lg font-bold pb-4">프로필 정보</h2>
        <ProfileInput title="닉네임" value={profileName} onChange={() => {}} />
        <div className="w-full">
          <p className="text-gray-600">자기소개</p>
          <textarea className="w-full py-2 h-[60px] border-b" rows={2}>
            {profileIntro}
          </textarea>
        </div>
        <div className="w-full">
          <p className="text-gray-600">지역</p>
          <div
            className="w-full py-2 h-[40px] border-b hover:cursor-pointer"
            onClick={() => setActiveButtonSheet(!activeButtonSheet)}
          >
            {profileRegion}
          </div>
        </div>
        {activeButtonSheet && <SelectCountry onClose={onCloseBottomSheet} />}
        <ProfileInput
          title="링크"
          value={link}
          placeholder="http://"
          onChange={() => {}}
        />
        <Button className="w-full h-[47px] rounded-lg" color="black">
          프로필 수정 완료
        </Button>
      </div>
    </Layout>
  );
};

🛠️#### 프로필 인풋 생성

interface Props {
  title: string;
  value: string;
  placeholder?: string;
  onChange: () => void;
}

const ProfileInput = ({ title, value, placeholder, onChange }: Props) => {
  return (
    <div className="w-full">
      <p className="text-gray-600">{title}</p>
      <input
        className="w-full py-2 h-[40px] border-b"
        value={value}
        placeholder={placeholder}
        onChange={onChange}
      />
    </div>
  );
};

ProfileInput.defaultProps = {
  placeholder: null,
};

export default ProfileInput;

모달

// profile/Modal.tsx
interface ModalProps {
  children: ReactNode;
  isOpen: boolean;
  onClose: () => void;
}

const Modal = ({ isOpen, onClose, children }: ModalProps) => {
  if (!isOpen) return null;
  return ReactDOM.createPortal(
    <div className="fixed inset-0 bg-black bg-opacity-50">
      <div className="h-full flex justify-center items-end">
        <div className="w-[46rem] min-w-[23rem] h-3/5 bg-white rounded-t-lg shadow-lg p-4 flex flex-col justify-between">
          <div className="flex justify-between">
            <h2 className="text-lg font-bold">지역 선택</h2>
            <button className="text-2xl" onClick={onClose}>
              X
            </button>
          </div>
          {children}
        </div>
      </div>
    </div>,
    document.body,
  );
};
// profile/SelectCountry.tsx

const countries = [
  { id: 1, name: '전체' },
  { id: 2, name: '서울' },
  { id: 3, name: '인천' },
  { id: 4, name: '경기' },
  { id: 5, name: '부산' },
  { id: 6, name: '대구' },
  { id: 7, name: '광주' },
  { id: 8, name: '대전' },
  { id: 9, name: '울산' },
  { id: 10, name: '강원' },
  { id: 11, name: '충청' },
  { id: 12, name: '세종' },
  { id: 13, name: '경남' },
  { id: 14, name: '경북' },
  { id: 15, name: '제주' },
];

interface Props {
  onClose: () => void;
}

const SelectCountry = ({ onClose }: Props) => {
  return (
    <Modal isOpen onClose={onClose}>
      <div className="grid grid-cols-3 gap-2">
        {countries.map((item) => (
          <Button
            key={item.id}
            size="medium"
            className="rounded-lg"
            color="white"
          >
            {item.name}
          </Button>
        ))}
      </div>
      <Button size="medium" className="rounded-lg" color="black">
        지역 선택 완료
      </Button>
    </Modal>
  );
};
스크린샷 2024-08-01 오후 12 41 50 스크린샷 2024-08-01 오후 12 41 40



스크린샷 2024-07-25 오후 8 03 58

✅ 셀프 체크리스트

  • PR 제목을 형식에 맞게 작성했나요?
  • 브랜치 전략에 맞는 브랜치에 PR을 올리고 있나요? (master/main이 아닙니다.)
  • 이슈는 close 했나요?
  • Reviewers, Labels, Projects를 등록했나요?
  • 작업 도중 문서 수정이 필요한 경우 잘 수정했나요?
  • 테스트는 잘 통과했나요?
  • 불필요한 코드는 제거했나요?

closes #이슈번호

@aquaman122 aquaman122 added the ✨feature 구현, 개선 사항 관련 부분 label Jul 24, 2024
@aquaman122 aquaman122 self-assigned this Jul 24, 2024
@aquaman122 aquaman122 marked this pull request as draft July 24, 2024 05:31
Copy link
Contributor

@CLOUDoort CLOUDoort left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다!


interface LayoutProps {
header: ReactNode;
header: string;
children: ReactNode;
footer: ReactNode;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

footer를 optional로 사용하면 어떨까요?
그러면 footer를 사용하지 않아도 굳이 null값을 넣지 않아도 될 것 같아요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오오 넵 optional로 바꿔놓을게요!

const heightClass = height ? `h-${height}` : '210px';

return (
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-10">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

popup 또한 modal처럼 createPortal을 사용해보는것도 좋을 것 같아요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이부분도 createPortal을 써서 코드 수정하겠습니다! 감사합니다

isBidding: boolean;
}

const OngoingProduct: React.FC<{ product: ProductProps }> = ({ product }) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React.FC를 명시하는 이유가 있을까요?
궁금합니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

배웠을때 이렇게 배웠었는데 요즘은 React.FC를 사용하는걸 지양하라고 하는군요!! 수정하겠습니다!

@@ -1,8 +1,11 @@
const ROUTERS = Object.freeze({
MAIN: '/',
HOME: '/home',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 만든 Home페이지와 main 페이지에 차이가 있나요?
차이가 없다면 home의 Route를 /로 하면 되지 않을까요?

Copy link
Contributor Author

@aquaman122 aquaman122 Jul 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 넵 오르트님이 만드신 Home이 메인페이지로 사용되면 기존에 있던 MainPage 삭제시키고 Home을 메인으로 사용하겠습니다!

@aquaman122 aquaman122 changed the base branch from main to dev July 29, 2024 13:21
@aquaman122 aquaman122 marked this pull request as ready for review August 1, 2024 12:39
@aquaman122 aquaman122 merged commit bf57470 into dev Aug 1, 2024
@aquaman122 aquaman122 deleted the feat/product-list-page branch August 2, 2024 04:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
✨feature 구현, 개선 사항 관련 부분
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants