Post

데브코스 TIL - Day 53

24년 2월 5일 강의를 들은 내용과 추가로 학습한 내용을 기록한 글입니다.

실습 - 커피샵 홈페이지 만들기

패키지

  • react
  • typescript
  • vite
  • react-bootstrap
  • styled-components

구현

커피아이템

  • 커피 아이템 3개씩 보여주기
  • 더보기 버튼 클릭 시 커피아이템 3개 추가
  • 커피 아이템을 다 보여주면 더보기 버튼 안보이게 만들기

전체 코드

[깃허브 코드] [배포사이트]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// App.tsx

import { BrowserRouter, Route, Routes } from "react-router-dom";
import Home from "./pages/Home";
import Detail from "./pages/Detail";
import CoffeePage from "./pages/CoffeePage";
import NotFound from "./pages/NotFound";

import "./assets/css/bootstrap.min.css";

function App() {
  return (
    <>
      <BrowserRouter basename={import.meta.env.BASE_URL}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/coffee" element={<CoffeePage />} />
          <Route path="/coffee/:id" element={<Detail />} />
          <Route path="/*" element={<NotFound />} />
        </Routes>
      </BrowserRouter>
    </>
  );
}

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// Home.tsx

import { useState } from "react";
import styled from "styled-components";
import { Button } from "react-bootstrap";
import Layout from "../components/Layout";
import Reward from "../components/Reward";
import CoffeeList from "../components/CoffeeList";
import { CoffeeProps, coffeeDB } from "../data";
import coffeeBG from "../assets/images/coffeebg.jpg";

function Home() {
  const limit = 3;
  const [page, setPage] = useState(0);
  const [list, setList] = useState<CoffeeProps[]>(coffeeDB.slice(0, 6));

  const getMoreCoffee = () => {
    const nextPage = page + 1;
    const offset = limit * (nextPage + 1) + 3;
    const startItem = page === 0 ? 6 : nextPage * limit;

    const newItems: CoffeeProps[] = coffeeDB.slice(startItem, offset);
    setList([...list, ...newItems]);
    setPage(nextPage);
  };

  return (
    <Layout>
      <HomeImageContainer />
      <Reward />

      <Inner>
        <CoffeeList list={list} />
      </Inner>

      {list.length < coffeeDB.length && (
        <ButtonRow>
          <Button variant="outline-success" onClick={getMoreCoffee}>
            더보기
          </Button>
        </ButtonRow>
      )}
    </Layout>
  );
}
const HomeImageContainer = styled.div`
  height: 400px;
  background-image: url(${coffeeBG});
  background-position: center;
  background-size: cover;
`;
const Inner = styled.div`
  max-width: 1100px;
  margin: 0 auto;
`;

const ButtonRow = styled.div`
  display: flex;
  justify-content: center;
  padding: 2em;
`;

export default Home;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// CoffeeList.tsx

import { Container, Row } from "react-bootstrap";
import styled from "styled-components";
import CoffeeItem from "./CoffeeItem";
import { CoffeeProps } from "../data";

const CoffeeList = ({ list }: { list: CoffeeProps[] }) => {
  return (
    <ListContainerStyle>
      <Row>
        {list.map((coffee) => (
          <CoffeeItem key={coffee.name} {...coffee} />
        ))}
      </Row>
    </ListContainerStyle>
  );
};
const ListContainerStyle = styled(Container)`
  padding: 2em 0;
`;
export default CoffeeList;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// CoffeeItem.tsx

import { Col } from "react-bootstrap";
import styled from "styled-components";
import { Link, useLocation } from "react-router-dom";
import { CoffeeProps } from "../data";

const CoffeeItem = ({ image, name, id }: CoffeeProps) => {
  const { pathname } = useLocation();
  return (
    <CoffeeItemStyle $isHomeScreen={pathname === "/"} xs={12} sm={6} md={4}>
      <Link to={`/coffee/${id}`}>
        <img src={image} alt={name} />
      </Link>

      <h4>{name}</h4>
    </CoffeeItemStyle>
  );
};

const CoffeeItemStyle = styled(Col)<{ $isHomeScreen: boolean }>`
  flex-grow: 1;
  a {
    display: block;
    width: 100%;
    overflow: hidden;
    img {
      width: 100%;
      transition: all 0.3s;
      &:hover {
        transform: scale(1.05);
      }
    }
  }
  h4 {
    font-size: 1.2em;
    text-align: center;
    padding: 0.5em 0;
  }
  @media (min-width: 576px) {
    max-width: 50%;
  }
  @media (min-width: 768px) {
    max-width: 33.333333%;
  }
  /* 홈화면에서는 최대 아이템개수 3으로 고정 */
  ${({ $isHomeScreen }) =>
    !$isHomeScreen &&
    `@media (min-width: 1200px) {
		max-width: 25%;
	}`}
`;
export default CoffeeItem;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Layout.tsx

import React from "react";
import NavComponents from "./Nav";
import styled from "styled-components";

const Layout = ({
  children,
  $maxWidth = false
}: {
  children: React.ReactNode;
  $maxWidth?: boolean;
}) => {
  const thisYear = new Date().getFullYear();

  return (
    <>
      <NavComponents />
      <Inner $maxWidth={$maxWidth}>{children}</Inner>
      <Footer>{thisYear} starbuck Hyemin. All Rights Reserved. </Footer>
    </>
  );
};

const Inner = styled.div<{ $maxWidth: boolean }>`
  max-width: ${({ $maxWidth }) => ($maxWidth ? "1100px" : "100%")};
  margin: 0 auto;

  padding: ${({ $maxWidth }) => ($maxWidth ? "3em 0" : 0)};
`;

const Footer = styled.footer`
  padding: 2em;
  background-color: #2c2a29;
  color: #999;
  text-align: center;
`;

export default Layout;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Nav.tsx

import { Container, Nav, Navbar } from "react-bootstrap";

const NavComponents = () => {
  return (
    <Navbar bg="dark" data-bs-theme="dark">
      <Container>
        <Navbar.Brand href="/">starbuck</Navbar.Brand>
        <Nav className="me-auto">
          <Nav.Link href="/coffee">coffee</Nav.Link>
          <Nav.Link href="/members">Members</Nav.Link>
        </Nav>
      </Container>
    </Navbar>
  );
};

export default NavComponents;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
// data.ts

import coffee1 from "./assets/images/coffee1.jpg";
import coffee2 from "./assets/images/coffee2.jpg";
import coffee3 from "./assets/images/coffee3.jpg";
import coffee4 from "./assets/images/coffee4.jpg";
import coffee5 from "./assets/images/coffee5.jpg";
import coffee6 from "./assets/images/coffee6.jpg";
import coffee7 from "./assets/images/coffee7.jpg";
import coffee8 from "./assets/images/coffee8.jpg";
import coffee9 from "./assets/images/coffee9.jpg";
import coffee10 from "./assets/images/coffee10.jpg";
import coffee11 from "./assets/images/coffee11.jpg";
import coffee12 from "./assets/images/coffee12.jpg";

export interface CoffeeProps {
  id: number;
  image: string;
  name: string;
  nameEn: string;
  price: number;
  description: string;
  category: "coffee" | "blended" | "seasonalMenu";
}

export const coffeeDB: CoffeeProps[] = [
  {
    id: 0,
    name: "아이스 카페 아메리카노",
    nameEn: "Iced Caffe Americano",
    price: 4100,
    image: coffee1,
    description:
      "진한 에스프레소에 시원한 정수물과 얼음을 더하여 스타벅스의 깔끔하고 강렬한 에스프레소를 가장 부드럽고 시원하게 즐길 수 있는 커피",
    category: "coffee"
  },
  {
    id: 1,
    name: "아이스 카페 라떼",
    nameEn: "Iced Caffe Latte",
    price: 4100,
    image: coffee2,
    description:
      "풍부하고 진한 농도의 에스프레소가 시원한 우유와 얼음을 만나 고소함과 시원함을 즐길 수 있는 대표적인 커피 라떼",
    category: "coffee"
  },
  {
    id: 2,
    name: "아이스 카라멜마끼야또",
    nameEn: "Iced Caramel Macchiato",
    price: 4800,
    image: coffee3,
    description:
      "향긋한 바닐라 시럽과 시원한 우유와 얼음을 넣고 점을 찍듯이 에스프레소를 부은 후 벌집 모양으로 카라멜 드리즐을 올린 달콤한 커피 음료",
    category: "coffee"
  },
  {
    id: 3,
    name: "돌체 콜드브루",
    nameEn: "Dolce Cold Brew",
    price: 4500,
    image: coffee5,
    description:
      "무더운 여름철,\n 동남아 휴가지에서 즐기는 커피를 떠오르게 하는\n 스타벅스 음료의 베스트 x 베스트 조합인\n 돌체 콜드 브루를 만나보세요!",
    category: "coffee"
  },
  {
    id: 4,
    name: "아이스 카푸치노",
    nameEn: "Iced Cappuccino",
    price: 4500,
    image: coffee4,
    description:
      "풍부하고 진한 에스프레소에 신선한 우유와 우유 거품이 얼음과 함께 들어간 시원하고 부드러운 커피 음료",
    category: "coffee"
  },
  {
    id: 5,
    name: "자바 칩 프라푸치노",
    nameEn: "Java Chip Frappuccino",
    price: 4100,
    image: coffee6,
    description:
      "커피, 모카 소스, 진한 초콜릿 칩이 입안 가득 느껴지는 스타벅스에서만 맛볼 수 있는 프라푸치노",
    category: "blended"
  },
  {
    id: 6,
    name: "망고 패션 티 블렌디드",
    nameEn: "Mango Passion Tea Blended",
    price: 5000,
    image: coffee7,
    description:
      "망고 패션 프루트 주스와 패션 탱고 티가\n 상큼하게 어우러진 과일 블렌디드",
    category: "blended"
  },
  {
    id: 7,
    name: "딸기 딜라이트 요거트 블렌디드",
    nameEn: "Strawberry Delight Yogurt Blended",
    price: 5000,
    image: coffee8,
    description:
      "유산균이 살아있는 리얼 요거트와 풍성한 딸기 과육이\n 더욱 상큼하게 어우러진 과일 요거트 블렌디드",
    category: "blended"
  },
  {
    id: 8,
    name: "여수 바다 유자 블렌디드",
    nameEn: "Yeosu Sea Yuja Blended",
    price: 4000,
    image: coffee9,
    description:
      "맑고 깨끗한 여수 경도의 낮 바다 풍경을 형상화한 음료로\n 상큼하게 즐길 수 있는 유자 블렌디드 음료 (유자:국내산)",
    category: "blended"
  },
  {
    id: 9,
    name: "민트 콜드브루",
    nameEn: "Mint Cold Brew",
    price: 4800,
    image: coffee10,
    description:
      "상쾌한 민트향 시럽과 잘게 갈린 얼음이\n 어우러져 시원함이 강렬하게 느껴지는 리저브만의\n 콜드 브루 음료",
    category: "seasonalMenu"
  },
  {
    id: 10,
    name: "콜드브루",
    nameEn: "Cold Brew",
    price: 4500,
    image: coffee11,
    description:
      "스타벅스 바리스타의 정성으로 탄생한 콜드 브루!\n \n 콜드 브루 전용 원두를 차가운 물로 추출하여 한정된 양만 제공됩니다.\n 깊은 풍미의 새로운 커피 경험을 즐겨보세요.",
    category: "coffee"
  },
  {
    id: 11,
    name: "콜드브루 몰트",
    nameEn: "Cold Brew Malt",
    price: 5000,
    image: coffee12,
    description:
      "[리저브 전용음료] 리저브 콜드 브루, 바닐라 아이스크림, 몰트가 블렌딩된 리저브만의 쉐이크",
    category: "seasonalMenu"
  }
];
This post is licensed under CC BY 4.0 by the author.