diff --git a/02-rendering-lists/README.md b/02-rendering-lists/README.md deleted file mode 100644 index 55bbadb..0000000 --- a/02-rendering-lists/README.md +++ /dev/null @@ -1,74 +0,0 @@ -# 02. 목록 UI 구현하기: Props와 State - -## 🎯 요구 사항 -- `RestaurantList` 가 restaurants 배열을 받아서 그릴 수 있도록 변경해 보세요. - - restaurants 배열을 `RestaurantList` 의 props로 내려받도록 변경해 보세요. -- 카테고리 필터에 따라 필터된 음식점 목록을 보여줄 수 있도록 변경해 보세요. - -### 구현 결과 예시 -```javascript -// App.jsx - - -``` -```javascript -const restaurants = [ - { - id: "a01", - name: "피양콩할마니", - description: - "평양 출신의 할머니가 수십 년간 운영해온 비지 전문점 피양콩 할마니. 두부를 빼지 않은 되비지를 맛볼 수 있는 곳으로, ‘피양’은 평안도 사투리로 ‘평양’을 의미한다. 딸과 함께 운영하는 이곳에선 맷돌로 직접 간 콩만을 사용하며, 일체의 조미료를 넣지 않은 건강식을 선보인다. 콩비지와 피양 만두가 이곳의 대표 메뉴지만, 할머니가 옛날 방식을 고수하며 만들어내는 비지전골 또한 이 집의 역사를 느낄 수 있는 특별한 메뉴다. 반찬은 손님들이 먹고 싶은 만큼 덜어 먹을 수 있게 준비돼 있다.", - category: "한식", - }, - { - id: "a02", - name: "친친", - description: "Since 2004 편리한 교통과 주차, 그리고 관록만큼 깊은 맛과 정성으로 정통 중식의 세계를 펼쳐갑니다", - category: "중식", - }, - { - id: "a03", - name: "잇쇼우", - description: - "잇쇼우는 정통 자가제면 사누끼 우동이 대표메뉴입니다. 기술은 정성을 이길 수 없다는 신념으로 모든 음식에 최선을 다하는 잇쇼우는 고객 한분 한분께 최선을 다하겠습니다", - category: "일식", - }, - { - id: "a04", - name: "이태리키친", - description: "늘 변화를 추구하는 이태리키친입니다.", - category: "양식", - }, - { - id: "a05", - name: "호아빈 삼성점", - description: "푸짐한 양에 국물이 일품인 쌀국수", - category: "아시안", - }, - { - id: "a06", - name: "도스타코스 선릉점", - description: "멕시칸 캐주얼 그릴", - category: "기타", - }, -]; -``` - - -## ✅ 키워드 -- Props -- State - - useState -- Keys - -> [Rendering Lists](https://react.dev/learn/rendering-lists) 문서에 ['Why does React need keys?'](https://react.dev/learn/rendering-lists#why-does-react-need-keys)는 지금 꼭 이해하지 않아도 괜찮습니다. 그냥 React에서 목록을 동적으로 그릴 때에는 이런 것들을 사용해야 하는구나~ 정도로만 알고 일단 넘어가세요. 우선 사용하는 법에 익숙해지는 것이 먼저입니다 :) - -## 🧙‍♀️ 진행 가이드 -- 진행 시간: 1시간 내에 완료하는 것을 목표로 합니다. - -## 🔗 참고 문서 -- [Thinking in React](https://react.dev/learn/thinking-in-react)의 Step3-4 -- [Passing Props to a Component](https://react.dev/learn/passing-props-to-a-component) -- [Rendering Lists](https://react.dev/learn/rendering-lists) -- [State: A Component's Memory](https://react.dev/learn/state-a-components-memory) - - [API Reference: useState](https://react.dev/reference/react/useState) diff --git a/03-modal/README.md b/03-modal/README.md deleted file mode 100644 index 6ad102e..0000000 --- a/03-modal/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# 03. 모달 UI 구현하기: side-effect(feat. event handler) - -## 🎯 요구 사항 -- `RestaurantList` 의 아이템을 클릭하면, 클릭한 아이템의 정보를 보여주는 모달이 뜨도록 변경해 주세요. '확인' 버튼을 클릭하거나 모달 뒤의 backdrop을 클릭하면 모달이 닫혀야 합니다. - - (작은 단계로 구현해보기 1) 아이템을 클릭하면 정해진 텍스트를 그대로 보여주는 모달을 열고 닫습니다. - - (작은 단계로 구현해보기 2) 클릭한 아이템의 정보를 모달에 내려줄 수 있도록 개선합니다. - -### 구현 결과 예시 -```javascript -// App.jsx -{isModalOpen && } -``` - -## ✅ 키워드 -- event handler (feat. side effect) -- conditional rendering -- lifting state up - -## 🧙‍♀️ 진행 가이드 -- 진행 시간: 1시간 내에 완료하는 것을 목표로 합니다. - -## 🔗 참고 문서 -- [Thinking in React](https://react.dev/learn/thinking-in-react)의 Step5 -- [Responding to Events](https://react.dev/learn/responding-to-events) diff --git a/04-form/README.md b/04-form/README.md deleted file mode 100644 index 45a5bd0..0000000 --- a/04-form/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# 04. 폼 UI 구현하기: controlled vs uncontrolled - -## 🎯 요구 사항 -- `Header`의 레스토랑 추가 버튼을 클릭하면 레스토랑 추가 폼이 모달로 뜨도록 구현해 주세요 - - 이전 단계에서 만들어두었던 `AddRestaurantModal`을 그대로 사용합니다. -- 카테고리를 선택하고, ``, ` + + 메뉴 등 추가 정보를 입력해 주세요. + + + {/* 추가 버튼 */} +
+ +
+ + + + ); +} diff --git a/src/App.css b/src/App.css index e69de29..6bfcc47 100644 --- a/src/App.css +++ b/src/App.css @@ -0,0 +1,52 @@ +* { + padding: 0; + margin: 0; + box-sizing: border-box; +} + +ul, +li { + list-style: none; +} + +html, +body { + font-family: sans-serif; + font-size: 16px; +} + +/* Colors *****************************************/ +:root { + --primary-color: #ec4a0a; + --lighten-color: #f6a88a; + --grey-100: #ffffff; + --grey-200: #d0d5dd; + --grey-300: #667085; + --grey-400: #344054; + --grey-500: #000000; +} + +/* Typography *************************************/ +.text-title { + font-size: 20px; + line-height: 24px; + font-weight: 600; +} + +.text-subtitle { + font-size: 18px; + line-height: 28px; + font-weight: 600; +} + +.text-body { + font-size: 16px; + line-height: 24px; + font-weight: 400; +} + +.text-caption { + font-size: 14px; + line-height: 20px; + font-weight: 400; +} diff --git a/src/App.jsx b/src/App.jsx index 0f5bcc1..57d97d8 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,7 +1,24 @@ import "./App.css"; +import Header from "./Header"; +import CategoryFilter from "./CategoryFilter"; +import RestaurantList from "./RestaurantList"; +import RestaurantDetailModal from "./RestaurantDetailModal"; +import AddRestaurantModal from "./AddRestaurantModal"; function App() { - return

Self-Paced React

; + return ( + <> +
+
+ + +
+ + + ); } export default App; diff --git a/src/CategoryFilter.jsx b/src/CategoryFilter.jsx new file mode 100644 index 0000000..243d3c5 --- /dev/null +++ b/src/CategoryFilter.jsx @@ -0,0 +1,22 @@ +import styles from "./CategoryFilter.module.css"; + +export default function CategoryFilter() { + return ( +
+ +
+ ); +} diff --git a/src/CategoryFilter.module.css b/src/CategoryFilter.module.css new file mode 100644 index 0000000..4c3ac4d --- /dev/null +++ b/src/CategoryFilter.module.css @@ -0,0 +1,22 @@ +.restaurant-filter-container { + display: flex; + justify-content: space-between; + + padding: 0 16px; + margin-top: 24px; +} + +.restaurant-filter-container select { + height: 44px; + min-width: 125px; + + border: 1px solid #d0d5dd; + border-radius: 8px; + background: transparent; + + font-size: 16px; +} + +.restaurant-filter { + padding: 8px; +} diff --git a/src/Header.jsx b/src/Header.jsx new file mode 100644 index 0000000..6cfe3b8 --- /dev/null +++ b/src/Header.jsx @@ -0,0 +1,16 @@ +import styles from "./Header.module.css"; + +export default function Header() { + return ( +
+

점심 뭐 먹지

+ +
+ ); +} diff --git a/src/Header.module.css b/src/Header.module.css new file mode 100644 index 0000000..32df2e1 --- /dev/null +++ b/src/Header.module.css @@ -0,0 +1,32 @@ +.gnb { + display: flex; + justify-content: space-between; + align-items: center; + height: 64px; + + padding: 0 16px; + + background-color: var(--primary-color); +} + +.gnb__title { + color: #fcfcfd; +} + +.gnb__button { + height: 40px; + + border: none; + border-radius: 8px; + background: transparent; + + font-size: 24px; + cursor: pointer; +} + +.gnb__button img { + display: block; + width: 40px; + height: 40px; + object-fit: contain; +} diff --git a/src/Modal.module.css b/src/Modal.module.css new file mode 100644 index 0000000..a7e0aab --- /dev/null +++ b/src/Modal.module.css @@ -0,0 +1,125 @@ +.modal { + display: none; +} + +.modal--open { + display: block; +} + +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + + background: rgba(0, 0, 0, 0.35); +} + +.modal-container { + position: fixed; + bottom: 0; + width: 100%; + + padding: 32px 16px; + + border-radius: 8px 8px 0px 0px; + background: var(--grey-100); +} + +.modal-title { + margin-bottom: 36px; +} + +.form-item { + display: flex; + flex-direction: column; + + margin-bottom: 36px; +} + +.form-item label { + color: var(--grey-400); + font-size: 14px; +} + +.form-item--required label::after { + padding-left: 4px; + + color: var(--primary-color); + content: "*"; +} + +.form-item .help-text { + color: var(--grey-300); +} + +.form-item input, +.form-item textarea, +.form-item select { + padding: 8px; + margin: 6px 0; + + border: 1px solid var(--grey-200); + border-radius: 8px; + + font-size: 16px; +} + +.form-item textarea { + resize: none; +} + +.form-item select { + height: 44px; + + padding: 8px; + + border: 1px solid var(--grey-200); + border-radius: 8px; + + color: var(--grey-300); +} + +input[name="name"], +input[name="link"] { + height: 44px; +} + +.button-container { + display: flex; +} + +.button { + width: 100%; + height: 44px; + + margin-right: 16px; + + border: none; + border-radius: 8px; + + font-weight: 600; + cursor: pointer; +} + +.button:last-child { + margin-right: 0; +} + +.button--secondary { + border: 1px solid var(--grey-300); + background: transparent; + + color: var(--grey-300); +} + +.button--primary { + background: var(--primary-color); + + color: var(--grey-100); +} + +.restaurant-info { + margin-bottom: 24px; +} diff --git a/src/RestaurantDetailModal.jsx b/src/RestaurantDetailModal.jsx new file mode 100644 index 0000000..871cff3 --- /dev/null +++ b/src/RestaurantDetailModal.jsx @@ -0,0 +1,25 @@ +import styles from "./Modal.module.css"; + +export default function RestaurantDetailModal() { + return ( +
+
+
+

음식점 이름

+
+

+ 음식점 소개 문구 +

+
+ {/* 닫기 버튼 */} +
+ +
+
+
+ ); +} diff --git a/src/RestaurantList.jsx b/src/RestaurantList.jsx new file mode 100644 index 0000000..3e38a33 --- /dev/null +++ b/src/RestaurantList.jsx @@ -0,0 +1,125 @@ +import styles from "./RestaurantList.module.css"; + +export default function RestaurantList() { + return ( +
+
    +
  • +
    + 한식 +
    +
    +

    + 피양콩할마니 +

    +

    + 평양 출신의 할머니가 수십 년간 운영해온 비지 전문점 피양콩 할마니. + 두부를 빼지 않은 되비지를 맛볼 수 있는 곳으로, ‘피양’은 평안도 + 사투리로 ‘평양’을 의미한다. 딸과 함께 운영하는 이곳에선 맷돌로 + 직접 간 콩만을 사용하며, 일체의 조미료를 넣지 않은 건강식을 + 선보인다. 콩비지와 피양 만두가 이곳의 대표 메뉴지만, 할머니가 옛날 + 방식을 고수하며 만들어내는 비지전골 또한 이 집의 역사를 느낄 수 + 있는 특별한 메뉴다. 반찬은 손님들이 먹고 싶은 만큼 덜어 먹을 수 + 있게 준비돼 있다. +

    +
    +
  • + +
  • +
    + 중식 +
    +
    +

    친친

    +

    + Since 2004 편리한 교통과 주차, 그리고 관록만큼 깊은 맛과 정성으로 + 정통 중식의 세계를 펼쳐갑니다 +

    +
    +
  • + +
  • +
    + 일식 +
    +
    +

    + 잇쇼우 +

    +

    + 잇쇼우는 정통 자가제면 사누끼 우동이 대표메뉴입니다. 기술은 정성을 + 이길 수 없다는 신념으로 모든 음식에 최선을 다하는 잇쇼우는 고객 + 한분 한분께 최선을 다하겠습니다 +

    +
    +
  • + +
  • +
    + 양식 +
    +
    +

    + 이태리키친 +

    +

    + 늘 변화를 추구하는 이태리키친입니다. +

    +
    +
  • + +
  • +
    + 아시안 +
    +
    +

    + 호아빈 삼성점 +

    +

    + 푸짐한 양에 국물이 일품인 쌀국수 +

    +
    +
  • + +
  • +
    + 기타 +
    +
    +

    + 도스타코스 선릉점 +

    +

    + 멕시칸 캐주얼 그릴 +

    +
    +
  • +
+
+ ); +} diff --git a/src/RestaurantList.module.css b/src/RestaurantList.module.css new file mode 100644 index 0000000..296f812 --- /dev/null +++ b/src/RestaurantList.module.css @@ -0,0 +1,61 @@ +.restaurant-list-container { + display: flex; + flex-direction: column; + + padding: 0 16px; + margin: 16px 0; +} + +.restaurant { + display: flex; + align-items: flex-start; + + padding: 16px 8px; + + border-bottom: 1px solid #e9eaed; +} + +.restaurant__category { + display: flex; + justify-content: center; + align-items: center; + width: 64px; + height: 64px; + min-width: 64px; + min-height: 64px; + + margin-right: 16px; + + border-radius: 50%; + background: var(--lighten-color); +} + +.category-icon { + width: 36px; + height: 36px; +} + +.restaurant__info { + display: flex; + flex-direction: column; + justify-content: flex-start; +} + +.restaurant__name { + margin: 0; +} + +.restaurant__distance { + color: var(--primary-color); +} + +.restaurant__description { + display: -webkit-box; + + padding-top: 8px; + + overflow: hidden; + text-overflow: ellipsis; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +}