From 6f0d55a8a2e177fb2a75912851a17f4ffcca7b0f Mon Sep 17 00:00:00 2001 From: hwinkr Date: Thu, 14 Mar 2024 11:27:47 +0900 Subject: [PATCH 01/70] =?UTF-8?q?docs(README.md):=20=EC=8A=A4=ED=85=9D2?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=B6=94=EA=B0=80=EB=90=9C=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=EC=82=AC=ED=95=AD=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 103 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 74 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index c576f5568..cd5bdfc3d 100644 --- a/README.md +++ b/README.md @@ -4,39 +4,95 @@ # 기능 요구사항 -## 1. UI +# Domain -### header +- [x] 음식점을 카테고리별로 필터링 한다. +- [x] 음식점을 이름순, 거리순으로 정렬한다. +- [x] 음식점 등록 폼을 제출하면 추가한다. +- [ ] 추가하기 버튼을 클릭할 경우 입력에 대한 유효성 검증을 한다 + - 음식점 이름은 10글자 이하여야한다. + - 음식점 설명은 300자 이하여야한다. + - 유효한 참고 링크인지 검증한다. +- [ ] 유효한 입력일 경우 localStorage에 추가한다. +- [ ] 전체 음식점 목록에서 자주 가는 음식점을 추가하거나, 되돌린다. +- [ ] 전체 음식점 목록, 자주 가는 음식점을 필터링한다 +- [ ] 음식점 목록에 있는 음식점을 삭제한다. + +```ts +addFavoriteRestaurant(restaurantName ; string) : boolean +getFavoriteRestaurant() : Restaurant[] +delete(restaurantName : string) : boolean +``` -- [x] 점심 뭐 먹지 타이틀 -- [x] 음식점 추가 버튼 +# UI -#### event +## header -- [x] 음식점 추가 버튼을 클릭하면 음식점 추가 폼 모달이 보여져야한다. +- UI + - [x] 점심 뭐 먹지 타이틀 + - [x] 음식점 추가 버튼 +- Event + - [x] 음식점 추가 버튼을 클릭하면 음식점 추가 폼 모달이 보여져야한다. -### option-selector +## option-selector + +- UI - [x] 카테고리를 필터링할 수 있는 옵션 버튼 - [x] 이름, 거리순을 정렬할 수 있는 옵션 버튼 -#### event +- Event - [x] 카테코리를 선택하면 선택된 카테고리에 해당되는 레스토랑 리스트를 보여준다 - [x] 이름, 거리순을 정렬하는 버튼을 클릭하면 정렬된 레스토랑 리스트를 보여준다. -### restaurant-list +## restaurant-list + +- UI + +- restaurant-item + + - 음식점 목록을 확인할 수 있다. + - 카테고리 + - 레스토랑 이름 + - (캠퍼스로 부터의) 거리 + - 설명, 설명은 2줄 이상이 넘어가면 ...으로 내용을 숨긴다 + +- Event + +- [x] 카테고리별 필터링, 정렬 옵션이 변경되는 이벤트가 발생하면 그에 맞춰 보여줘야 할 음식점 목록을 변경한다. +- [ ] 모든 음식점, 자주 가는 음식점을 클릭할 경우 그에 맞춰 보여줘야 할 음식점 목록을 변경한다. + +## restaurant-detail + +- UI -- [x] 선택된 옵션에 따라 레스토랑 리스트를 보여준다 + - 음식점 목록 중 하나를 클릭하면 해당 음식점에 대한 세부 정보를 나타낸다. + - [ ] 음식점 이름, 카테고리, 캠퍼스로부터의 거리, 자주 가는 음식점인지의 여부 + - [ ] 음식점 상세 설명, ...으로 표시하지 않고 모든 내용을 보여준다, + - [ ] 참고링크 -- 음식점 목록을 확인할 수 있다. -- 카테고리 -- 레스토랑 이름 -- (캠퍼스로 부터의) 거리 -- 설명 - - 설명은 2줄 이상이 넘어가면 ...으로 내용을 숨긴다 +- Event -### restaurant-add-form +- [ ] 음식점 세부 정보의 바깥 영역을 클릭하거나, 닫기 버튼을 클릭하면 `restaurant-detail` 컴포넌트는 사라져야한다. +- [ ] 음식점 세부 정보에서 자주 가는 음식점인지의 여부를 변경하면, 변경이 반영되어야한다. +- [ ] 참고 링크를 클릭하면 해당 링크로 이동시킨다. +- [ ] 삭제하기 버튼을 클릭할 경우, 해당 음식점을 삭제한 후 `restaurant-detail` 컴포넌트는 사라져야한다. + +## 모든 음식점, 자주 가는 음식점 구분 버튼 (이름 미정) + +- UI + +- [ ] 식당 목록 선택 옵션 (모든 음식점, 자주 가는 음식점) +- [ ] 클릭한 버튼에 액티브 스타일이 적용되어야 하며, 이전에 클릭한 버튼의 액티브 스타일은 사라져야한다. + +- Event + +- [ ] 특정 버튼을 클릭할 때마다, 보여줘야 할 음식점 목록의 변경을 트리거한다. + +## restaurant-add-form + +- UI - [x] 새로운 음식점 타이틀 - [x] 카테코리 선택 옵션 (한식, 중식, 일식, 양식, 아시안, 기타) @@ -46,19 +102,8 @@ - [x] 취소, 추가 버튼 - [ ] 유효하지 않은 입력값이 들어온 경우 해당 입력값 아래에 빨간색 에러 메시지를 보여준다. -#### event +- Event - [x] 취소 버튼을 클릭하면 추가 폼을 리셋하고 닫는다. - [x] 추가하기 버튼을 클릭하면 추가 폼을 닫고, 추가된 레스토랑을 레스토랑 리스트에 반영한다. - [x] 추가 폼 외부를 클릭 시 폼을 닫는다. 외부를 클릭할 경우 입력한 값들을 리셋하지 않고 닫기만 한다. - -## 2. Domain - -- [x] 음식점을 카테고리별로 필터링 한다. -- [x] 음식점을 이름순, 거리순으로 정렬한다. -- [x] 음식점 등록 폼을 제출하면 추가한다. -- [ ] 추가하기 버튼을 클릭할 경우 입력에 대한 유효성 검증을 한다 - - 음식점 이름은 10글자 이하여야한다. - - 음식점 설명은 300자 이하여야한다. - - 유효한 참고 링크인지 검증한다. -- [ ] 유효한 입력일 경우 localStorage에 추가한다. From 13e87fa6a734b67d342843cbba88d8e9c8d4a389 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Thu, 14 Mar 2024 12:04:58 +0900 Subject: [PATCH 02/70] =?UTF-8?q?docs(README.md):=20=EC=8A=A4=ED=85=9D2?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=B6=94=EA=B0=80=EB=90=9C=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=EC=82=AC=ED=95=AD=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CategoryIcon 컴포넌트 분리 내용 추가 --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index cd5bdfc3d..be0068791 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,12 @@ delete(restaurantName : string) : boolean - [x] 카테고리별 필터링, 정렬 옵션이 변경되는 이벤트가 발생하면 그에 맞춰 보여줘야 할 음식점 목록을 변경한다. - [ ] 모든 음식점, 자주 가는 음식점을 클릭할 경우 그에 맞춰 보여줘야 할 음식점 목록을 변경한다. +## category-icon + +- UI + + - 전체, 한식, 중식,,, 등 카테고리에 따라 다른 이미지를 보여준다. + ## restaurant-detail - UI From 5eb7e3e62928b885fb50255c7e6f25b6b288f1d7 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Fri, 15 Mar 2024 22:25:33 +0900 Subject: [PATCH 03/70] =?UTF-8?q?chore(delete=20js=20file):=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20js=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BaseComponent.js | 25 ---- src/components/Header.js | 28 ----- src/components/MenuApp.js | 32 ------ src/components/OptionSelector.js | 43 ------- .../RestaurantAddForm/RestaurantNameInput.js | 52 --------- .../RestaurantAddForm/RestaurantOption.js | 67 ----------- src/components/RestaurantAddForm/index.js | 107 ------------------ src/components/RestaurantItem.js | 61 ---------- src/components/RestaurantList.js | 70 ------------ src/utils/dom.js | 1 - 10 files changed, 486 deletions(-) delete mode 100644 src/components/BaseComponent.js delete mode 100644 src/components/Header.js delete mode 100644 src/components/MenuApp.js delete mode 100644 src/components/OptionSelector.js delete mode 100644 src/components/RestaurantAddForm/RestaurantNameInput.js delete mode 100644 src/components/RestaurantAddForm/RestaurantOption.js delete mode 100644 src/components/RestaurantAddForm/index.js delete mode 100644 src/components/RestaurantItem.js delete mode 100644 src/components/RestaurantList.js delete mode 100644 src/utils/dom.js diff --git a/src/components/BaseComponent.js b/src/components/BaseComponent.js deleted file mode 100644 index 22d75fd4c..000000000 --- a/src/components/BaseComponent.js +++ /dev/null @@ -1,25 +0,0 @@ -export default class BaseComponent extends HTMLElement { - constructor() { - super(); - } - - connectedCallback() { - this.render(); - this.setEvent(); - } - - disconnectedCallback() {} - - render() {} - - setEvent() {} - - emitEvent(event, data) { - this.dispatchEvent( - new CustomEvent(event, { - bubbles: true, - detail: data, - }) - ); - } -} diff --git a/src/components/Header.js b/src/components/Header.js deleted file mode 100644 index ccbcaee92..000000000 --- a/src/components/Header.js +++ /dev/null @@ -1,28 +0,0 @@ -import { addButton } from "../assets/index.js"; -import { $ } from "../utils/dom.js"; -import BaseComponent from "./BaseComponent.js"; - -class Header extends BaseComponent { - constructor() { - super(); - } - - render() { - this.innerHTML = ` -
-

점심 뭐 먹지

- -
- `; - } - - setEvent() { - $(".gnb__button").addEventListener("click", (e) => { - this.emitEvent("modal-open"); - }); - } -} - -customElements.define("app-header", Header); diff --git a/src/components/MenuApp.js b/src/components/MenuApp.js deleted file mode 100644 index 9f0f52690..000000000 --- a/src/components/MenuApp.js +++ /dev/null @@ -1,32 +0,0 @@ -import { CATEGORIES } from "../constants/menu"; -import { initRestaurantStorage } from "../domains/Restaurants"; -import BaseComponent from "./BaseComponent.js"; - -class MenuApp extends BaseComponent { - constructor() { - initRestaurantStorage(); - super(); - } - - render() { - this.innerHTML = ` - -
-
- - -
- - -
- `; - } -} - -customElements.define("menu-app", MenuApp); diff --git a/src/components/OptionSelector.js b/src/components/OptionSelector.js deleted file mode 100644 index 742819317..000000000 --- a/src/components/OptionSelector.js +++ /dev/null @@ -1,43 +0,0 @@ -import BaseComponent from "./BaseComponent.js"; - -class OptionSelector extends BaseComponent { - #selectType = null; - - constructor() { - super(); - this.#selectType = this.getAttribute("type"); - } - - #createOptionHTML(options) { - return options.reduce( - (accOptions, currOption) => - accOptions + `;`, - "" - ); - } - - render() { - const options = this.getAttribute("options").split(","); - - this.innerHTML = ` - - `; - } - - setEvent() { - this.addEventListener("change", (e) => { - const selectedValue = e.target.value; - - this.emitEvent("select-change", { - type: this.#selectType, - option: selectedValue, - }); - }); - } -} - -customElements.define("option-selector", OptionSelector); diff --git a/src/components/RestaurantAddForm/RestaurantNameInput.js b/src/components/RestaurantAddForm/RestaurantNameInput.js deleted file mode 100644 index 1376f878a..000000000 --- a/src/components/RestaurantAddForm/RestaurantNameInput.js +++ /dev/null @@ -1,52 +0,0 @@ -import { $ } from "../../utils/dom"; -import restaurantValidator from "../../validators/restaurantValidator"; -import BaseComponent from "../BaseComponent"; - -class RestaurantNameInput extends BaseComponent { - #errorMessageClassList = null; - - constructor() { - super(); - } - - render() { - this.innerHTML = ` -
- - - -
`; - - this.#errorMessageClassList = $("#name-error-message").classList; - } - - #isValidName(value) { - return restaurantValidator.isInRange(value, 0, 10); - } - - #renderErrorMessage() { - this.#errorMessageClassList.remove("hidden"); - } - - #hideErrorMessage() { - this.#errorMessageClassList.add("hidden"); - } - - #handleErrorMessage(value) { - this.#isValidName(value) - ? this.#hideErrorMessage() - : this.#renderErrorMessage(); - } - - setEvent() { - document.addEventListener("add-form-submit", () => { - this.#handleErrorMessage($("#name").value); - }); - - $("#name").addEventListener("focusout", (e) => { - this.#handleErrorMessage(e.target.value); - }); - } -} - -customElements.define("restaurant-name-input", RestaurantNameInput); diff --git a/src/components/RestaurantAddForm/RestaurantOption.js b/src/components/RestaurantAddForm/RestaurantOption.js deleted file mode 100644 index 85712aaf9..000000000 --- a/src/components/RestaurantAddForm/RestaurantOption.js +++ /dev/null @@ -1,67 +0,0 @@ -import { $ } from "../../utils/dom"; -import restaurantValidator from "../../validators/restaurantValidator"; -import BaseComponent from "../BaseComponent"; - -class RestaurantOptions extends BaseComponent { - #errorMessageClassList = null; - - constructor() { - super(); - } - - #getOptionText(id) { - if (id === "category") return "카테고리"; - if (id === "distance") return "거리(도보 이동 시간)"; - } - - #isSelected() { - const id = this.getAttribute("id"); - return restaurantValidator.isSelected($(`#${id}-select`).value); - } - - #renderErrorMessage() { - this.#errorMessageClassList.remove("hidden"); - } - - #hideErrorMessage() { - this.#errorMessageClassList.add("hidden"); - } - - #handleErrorMessage() { - this.#isSelected() ? this.#hideErrorMessage() : this.#renderErrorMessage(); - } - - #createOptionHTML(options, values) { - return options.reduce( - (accOptions, currOption, index) => - accOptions + `;`, - "" - ); - } - - render() { - const options = this.getAttribute("options").split(","); - const values = this.getAttribute("values").split(","); - const id = this.getAttribute("id"); - const text = this.#getOptionText(id); - - this.innerHTML = ` -
- - - -
`; - - this.#errorMessageClassList = $(`#${id}-select-error-message`).classList; - } - - setEvent() { - document.addEventListener("add-form-submit", () => { - this.#handleErrorMessage(); - }); - } -} - -customElements.define("restaurant-option", RestaurantOptions); diff --git a/src/components/RestaurantAddForm/index.js b/src/components/RestaurantAddForm/index.js deleted file mode 100644 index 8d0c15d33..000000000 --- a/src/components/RestaurantAddForm/index.js +++ /dev/null @@ -1,107 +0,0 @@ -import { CATEGORIES, DISTANCES } from "../../constants/menu"; -import { add } from "../../domains/Restaurants"; -import { $ } from "../../utils/dom"; -import BaseComponent from "../BaseComponent"; - -class RestaurantAddForm extends BaseComponent { - constructor() { - super(); - } - - render() { - this.innerHTML = ` - - `; - } - - #getFormData() { - const formData = new FormData($("#restaurant-add-form")); - return Object.fromEntries(formData.entries()); - } - - #resetFormData() { - $("#restaurant-add-form").reset(); - } - - #renderFormModal() { - $(".modal").classList.add("modal--open"); - document.body.classList.add("stop-scroll"); - } - - #hideFormModal() { - document.body.classList.remove("stop-scroll"); - $(".modal").classList.remove("modal--open"); - } - - setEvent() { - document.addEventListener("modal-open", () => { - this.#renderFormModal(); - }); - - $("#restaurant-add-form").addEventListener("submit", (e) => { - e.preventDefault(); - this.emitEvent("add-form-submit"); - - const formData = this.#getFormData(); - - try { - if (add(formData)) { - this.emitEvent("add-restaurant"); - this.#hideFormModal(); - this.#resetFormData(); - } - } catch (error) { - alert(error.message); - } - }); - - $("#reset-button").addEventListener("click", () => { - this.#hideFormModal(); - }); - - $(".modal-backdrop").addEventListener("click", (event) => { - if (event.target === event.currentTarget) { - this.#hideFormModal(); - } - }); - } -} - -customElements.define("restaurant-add-form", RestaurantAddForm); diff --git a/src/components/RestaurantItem.js b/src/components/RestaurantItem.js deleted file mode 100644 index f1cfa9d19..000000000 --- a/src/components/RestaurantItem.js +++ /dev/null @@ -1,61 +0,0 @@ -import { - categoryAsian, - categoryChinese, - categoryEtc, - categoryJapanese, - categoryKorean, - categoryWestern, -} from "../assets/index.js"; -import BaseComponent from "./BaseComponent.js"; - -class RestaurantItem extends BaseComponent { - constructor() { - super(); - } - - #categoryToImg(category) { - switch (category) { - case "한식": - return categoryKorean; - case "중식": - return categoryChinese; - case "아시안": - return categoryAsian; - case "일식": - return categoryJapanese; - case "양식": - return categoryWestern; - case "기타": - return categoryEtc; - default: - break; - } - } - - render() { - const category = this.getAttribute("category"); - const name = this.getAttribute("name"); - const distance = this.getAttribute("distance"); - const description = this.getAttribute("description"); - const img = this.#categoryToImg(category); - - this.innerHTML = ` -
-
- 한식 -
-
- ${name} - 캠퍼스부터 ${distance}분 내 - ${ - description - ? `

${description}

` - : "" - } -
-
- `; - } -} - -customElements.define("restaurant-item", RestaurantItem); diff --git a/src/components/RestaurantList.js b/src/components/RestaurantList.js deleted file mode 100644 index 797f0b82d..000000000 --- a/src/components/RestaurantList.js +++ /dev/null @@ -1,70 +0,0 @@ -import { filterByCategory, sortByType } from "../domains/Restaurants"; -import BaseComponent from "./BaseComponent.js"; - -class RestaurantList extends BaseComponent { - #currentCategory; - - constructor() { - super(); - this.#currentCategory = "전체"; - } - - #getCurrentList(sortOption) { - return sortOption - ? sortByType(this.#currentCategory, sortOption) - : filterByCategory(this.#currentCategory); - } - - #sortRestaurantList(sortOption) { - this.render(sortOption); - } - - #filterRestaurantList(option) { - this.#currentCategory = option; - this.render(); - } - - #createRestaurantListHTML(restaurantList) { - return restaurantList.reduce((accRestaurants, currentRestaurant) => { - const { name, category, distance, description } = currentRestaurant; - - return ( - accRestaurants + - ` - - ` - ); - }, ""); - } - - render(sortOption) { - const currentList = this.#getCurrentList(sortOption); - - this.innerHTML = ` -
- ${this.#createRestaurantListHTML(currentList)} -
- `; - } - - setEvent() { - document.addEventListener("select-change", (event) => { - const { type, option } = event.detail; - - type === "sort" - ? this.#sortRestaurantList(option) - : this.#filterRestaurantList(option); - }); - - document.addEventListener("add-restaurant", () => { - this.render(); - }); - } -} - -customElements.define("restaurant-list", RestaurantList); diff --git a/src/utils/dom.js b/src/utils/dom.js deleted file mode 100644 index 8df86b003..000000000 --- a/src/utils/dom.js +++ /dev/null @@ -1 +0,0 @@ -export const $ = (selector) => document.querySelector(selector); From ec3f0552446a375da76e703ceda41d82d5f97071 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Fri, 15 Mar 2024 22:27:07 +0900 Subject: [PATCH 04/70] =?UTF-8?q?feat(Restauran):=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=EB=90=9C=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=9A=94=EA=B5=AC?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 음식점 이름으로 식당 정보를 찾을 수 있다. - 유효한 입력일 경우 로컬 스토리지에 식당을 추가한다. - 음식점 목록에 있는 한 식당을 삭제한다 --- README.md | 5 ++- src/domains/Restaurants.ts | 86 ++++++++++++++++++++++++++------------ 2 files changed, 62 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index be0068791..8cd840f32 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,11 @@ - 음식점 이름은 10글자 이하여야한다. - 음식점 설명은 300자 이하여야한다. - 유효한 참고 링크인지 검증한다. -- [ ] 유효한 입력일 경우 localStorage에 추가한다. +- [x] 음식점 이름으로 식당의 정보를 찾을 수 있도록 한다. +- [x] 유효한 입력일 경우 localStorage에 추가한다. - [ ] 전체 음식점 목록에서 자주 가는 음식점을 추가하거나, 되돌린다. - [ ] 전체 음식점 목록, 자주 가는 음식점을 필터링한다 -- [ ] 음식점 목록에 있는 음식점을 삭제한다. +- [x] 음식점 목록에 있는 음식점을 삭제한다. ```ts addFavoriteRestaurant(restaurantName ; string) : boolean diff --git a/src/domains/Restaurants.ts b/src/domains/Restaurants.ts index b03b77c25..cf999325a 100644 --- a/src/domains/Restaurants.ts +++ b/src/domains/Restaurants.ts @@ -1,14 +1,23 @@ -import { DEFAULT_DATA, ERROR_MESSAGES } from "../constants/menu"; -import { Category, RestaurantItem } from "../types/menu"; +import { + CATEGORIES, + DEFAULT_DATA, + ERROR_MESSAGES, + SORT_TYPE, +} from "../constants/menu"; +import { + CategoryString, + RestaurantAddItem, + RestaurantItem, + SortOptionString, +} from "../types/menu"; import restaurantValidator from "../validators/restaurantValidator"; -type SortType = "이름순" | "거리순"; const RESTAURANT_KEY = "restaurants"; const getRestaurantFromStorage = () => { const storedRestaurants = localStorage.getItem(RESTAURANT_KEY); if (!storedRestaurants) return []; - + // TODO : type assertion을 사용하지 않는 방법을 고민해본다. return JSON.parse(storedRestaurants) as RestaurantItem[]; }; @@ -20,7 +29,29 @@ const isAlreadyExist = (newRestaurantName: string) => { ); }; -export const validateRestaurantData = (restaurantInfo: RestaurantItem) => { +const sortByName = (restaurants: RestaurantItem[]) => { + return [...restaurants.sort((a, b) => (a.name < b.name ? -1 : 1))]; +}; + +const sortByDistance = (restaurants: RestaurantItem[]) => { + return [...restaurants.sort((a, b) => a.distance - b.distance)]; +}; + +const trimAllSpace = (str: string): string => { + return str.replaceAll(" ", ""); +}; + +export const initRestaurantStorage = () => { + if (getRestaurantFromStorage().length > 0) { + return; + } + + DEFAULT_DATA.forEach((data: RestaurantItem) => { + add(data); + }); +}; + +export const validateRestaurantData = (restaurantInfo: RestaurantAddItem) => { const { name, category, distance, description, link } = restaurantInfo; if (!restaurantValidator.isSelected(category)) { @@ -43,52 +74,53 @@ export const validateRestaurantData = (restaurantInfo: RestaurantItem) => { } }; -const sortByName = (restaurants: RestaurantItem[]) => { - return [...restaurants.sort((a, b) => (a.name < b.name ? -1 : 1))]; -}; +export const findRestaurantByName = ( + restaurantName: string +): RestaurantItem | undefined => { + const storedRestaurants = getRestaurantFromStorage(); -const sortByDistance = (restaurants: RestaurantItem[]) => { - return [...restaurants.sort((a, b) => a.distance - b.distance)]; + return storedRestaurants.find(({ name }) => name === restaurantName); }; -const trimAllSpace = (str: string): string => { - return str.replaceAll(" ", ""); -}; - -export const initRestaurantStorage = () => { - if (getRestaurantFromStorage().length > 0) { - return; - } +export const deleteRestaurantByName = (restaurantName: string): void => { + const storedRestaurants = getRestaurantFromStorage(); + const filteredRestaurants = storedRestaurants.filter( + ({ name }) => restaurantName !== name + ); - DEFAULT_DATA.forEach((data: RestaurantItem) => { - add(data); - }); + localStorage.setItem(RESTAURANT_KEY, JSON.stringify(filteredRestaurants)); }; -export const add = (restaurantInfo: RestaurantItem) => { +export const add = (restaurantInfo: RestaurantAddItem) => { const storedRestaurants = getRestaurantFromStorage(); validateRestaurantData(restaurantInfo); localStorage.setItem( RESTAURANT_KEY, - JSON.stringify([...storedRestaurants, restaurantInfo]) + JSON.stringify([ + ...storedRestaurants, + { ...restaurantInfo, isFavorite: false }, + ]) ); return true; }; -export const filterByCategory = (category: Category) => { +export const filterByCategory = (category: CategoryString) => { const restaurants: RestaurantItem[] = getRestaurantFromStorage(); - if (category === "전체") return restaurants; + if (category === CATEGORIES.all) return restaurants; return restaurants.filter((item) => item.category === category); }; -export const sortByType = (category: Category, type: SortType) => { +export const sortByType = ( + category: CategoryString, + type: SortOptionString +) => { const filteredRestaurants = filterByCategory(category); - return type === "이름순" + return type === SORT_TYPE.name ? sortByName(filteredRestaurants) : sortByDistance(filteredRestaurants); }; From 72a62ee430ceb7bb124438e24870c29be1f9bf14 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Fri, 15 Mar 2024 22:30:01 +0900 Subject: [PATCH 05/70] =?UTF-8?q?refactor(BaseComponent):=20TS=EB=A1=9C=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 자식 클래스에게 상속할 메서드 protected 키워드 사용 - 커스텀 이벤트를 실행시키는 추상화 메서드 제네릭 타입 적용 --- src/components/BaseComponent.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/components/BaseComponent.ts diff --git a/src/components/BaseComponent.ts b/src/components/BaseComponent.ts new file mode 100644 index 000000000..67090a8fd --- /dev/null +++ b/src/components/BaseComponent.ts @@ -0,0 +1,31 @@ +import { MENU_APP_EVENTS } from "../constants/event"; + +type MenuAppEvent = + | keyof HTMLElementEventMap + | (typeof MENU_APP_EVENTS)[keyof typeof MENU_APP_EVENTS]; + +export default class BaseComponent extends HTMLElement { + constructor() { + super(); + } + + protected connectedCallback() { + this.render(); + this.setEvent(); + } + + protected disconnectedCallback() {} + + protected render() {} + + protected setEvent() {} + + protected emitEvent(event: MenuAppEvent, data?: T) { + this.dispatchEvent( + new CustomEvent(event, { + bubbles: true, + detail: data, + }) + ); + } +} From c09a809fa993325052c4af7d95a85969475a04d1 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Fri, 15 Mar 2024 22:31:29 +0900 Subject: [PATCH 06/70] =?UTF-8?q?refactor(MenuApp):=20UI=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20TS=EB=A1=9C=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/MenuApp.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/components/MenuApp.ts diff --git a/src/components/MenuApp.ts b/src/components/MenuApp.ts new file mode 100644 index 000000000..c68ec14a1 --- /dev/null +++ b/src/components/MenuApp.ts @@ -0,0 +1,33 @@ +import { MENU_APP_EVENTS } from "../constants/event"; +import { CATEGORIES, SORT_TYPE } from "../constants/menu"; +import { initRestaurantStorage } from "../domains/Restaurants"; +import BaseComponent from "./BaseComponent"; + +class MenuApp extends BaseComponent { + constructor() { + initRestaurantStorage(); + super(); + } + + render() { + this.innerHTML = /*html*/ ` + +
+
+ + +
+ +
+ + + + `; + } +} + +customElements.define("menu-app", MenuApp); From db3dea75786812bd0bf090e9d2118d77083b5ca8 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sun, 17 Mar 2024 11:08:11 +0900 Subject: [PATCH 07/70] =?UTF-8?q?feat(Restaurants):=20=EC=9D=8C=EC=8B=9D?= =?UTF-8?q?=EC=A0=90=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 자주가는 음식점인지 아닌지에 대한 상태 변경 - 자주가는 음식점 목록만 필터링 --- README.md | 4 +-- src/domains/Restaurants.ts | 67 +++++++++++++++++++------------------- 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 8cd840f32..dc5207c0f 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ - 유효한 참고 링크인지 검증한다. - [x] 음식점 이름으로 식당의 정보를 찾을 수 있도록 한다. - [x] 유효한 입력일 경우 localStorage에 추가한다. -- [ ] 전체 음식점 목록에서 자주 가는 음식점을 추가하거나, 되돌린다. -- [ ] 전체 음식점 목록, 자주 가는 음식점을 필터링한다 +- [x] 자주 가는 음식점인지 아닌지에 대해 상태를 변경(토글)한다. +- [x] 자주 가는 음식점 목록을 필터링한다. - [x] 음식점 목록에 있는 음식점을 삭제한다. ```ts diff --git a/src/domains/Restaurants.ts b/src/domains/Restaurants.ts index cf999325a..4d307bbd1 100644 --- a/src/domains/Restaurants.ts +++ b/src/domains/Restaurants.ts @@ -1,15 +1,5 @@ -import { - CATEGORIES, - DEFAULT_DATA, - ERROR_MESSAGES, - SORT_TYPE, -} from "../constants/menu"; -import { - CategoryString, - RestaurantAddItem, - RestaurantItem, - SortOptionString, -} from "../types/menu"; +import { CATEGORIES, DEFAULT_DATA, ERROR_MESSAGES, SORT_TYPE } from "../constants/menu"; +import { CategoryString, RestaurantAddItem, RestaurantItem, SortOptionString } from "../types/menu"; import restaurantValidator from "../validators/restaurantValidator"; const RESTAURANT_KEY = "restaurants"; @@ -17,16 +7,14 @@ const RESTAURANT_KEY = "restaurants"; const getRestaurantFromStorage = () => { const storedRestaurants = localStorage.getItem(RESTAURANT_KEY); if (!storedRestaurants) return []; - // TODO : type assertion을 사용하지 않는 방법을 고민해본다. + return JSON.parse(storedRestaurants) as RestaurantItem[]; }; const isAlreadyExist = (newRestaurantName: string) => { const storedRestaurants = getRestaurantFromStorage(); - return storedRestaurants.some( - ({ name }) => trimAllSpace(newRestaurantName) === trimAllSpace(name) - ); + return storedRestaurants.some(({ name }) => trimAllSpace(newRestaurantName) === trimAllSpace(name)); }; const sortByName = (restaurants: RestaurantItem[]) => { @@ -74,33 +62,49 @@ export const validateRestaurantData = (restaurantInfo: RestaurantAddItem) => { } }; -export const findRestaurantByName = ( - restaurantName: string -): RestaurantItem | undefined => { +export const findRestaurantByName = (restaurantName: string): RestaurantItem | null => { + const storedRestaurants = getRestaurantFromStorage(); + const foundRestaurant = storedRestaurants.find(({ name }) => name === restaurantName); + + return foundRestaurant ? foundRestaurant : null; +}; + +export const toggleFavoriteStateByName = (targetName: string) => { const storedRestaurants = getRestaurantFromStorage(); + const updatedRestaurants = storedRestaurants.map((restaurant) => { + return restaurant.name !== targetName + ? { + ...restaurant, + } + : { + ...restaurant, + isFavorite: restaurant.isFavorite ? false : true, + }; + }); - return storedRestaurants.find(({ name }) => name === restaurantName); + localStorage.setItem(RESTAURANT_KEY, JSON.stringify(updatedRestaurants)); }; export const deleteRestaurantByName = (restaurantName: string): void => { const storedRestaurants = getRestaurantFromStorage(); - const filteredRestaurants = storedRestaurants.filter( - ({ name }) => restaurantName !== name - ); + const filteredRestaurants = storedRestaurants.filter(({ name }) => restaurantName !== name); localStorage.setItem(RESTAURANT_KEY, JSON.stringify(filteredRestaurants)); }; +export const getFavoriteRestaurants = (): RestaurantItem[] | [] => { + const storedRestaurants = getRestaurantFromStorage(); + + return storedRestaurants.filter(({ isFavorite }) => isFavorite); +}; + export const add = (restaurantInfo: RestaurantAddItem) => { const storedRestaurants = getRestaurantFromStorage(); validateRestaurantData(restaurantInfo); localStorage.setItem( RESTAURANT_KEY, - JSON.stringify([ - ...storedRestaurants, - { ...restaurantInfo, isFavorite: false }, - ]) + JSON.stringify([...storedRestaurants, { ...restaurantInfo, isFavorite: false }]) ); return true; @@ -114,13 +118,8 @@ export const filterByCategory = (category: CategoryString) => { return restaurants.filter((item) => item.category === category); }; -export const sortByType = ( - category: CategoryString, - type: SortOptionString -) => { +export const sortByType = (category: CategoryString, type: SortOptionString) => { const filteredRestaurants = filterByCategory(category); - return type === SORT_TYPE.name - ? sortByName(filteredRestaurants) - : sortByDistance(filteredRestaurants); + return type === SORT_TYPE.name ? sortByName(filteredRestaurants) : sortByDistance(filteredRestaurants); }; From 46653ca6679d40699d09fc5a2fbd8bf59e535d2d Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sun, 17 Mar 2024 11:15:39 +0900 Subject: [PATCH 08/70] =?UTF-8?q?feat(style.css):=20=EC=8A=A4=ED=83=80?= =?UTF-8?q?=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 음식점 상세 정보 - 모든 음식점, 자주 가는 음식점 구분 탭 - 레이아웃, 폰트 크기, 굵기 공통 CSS 속성 추가 --- templates/style.css | 127 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 104 insertions(+), 23 deletions(-) diff --git a/templates/style.css b/templates/style.css index 1feff2bbc..72ee1adbc 100644 --- a/templates/style.css +++ b/templates/style.css @@ -34,7 +34,6 @@ select { } /* Typography *************************************/ - .text-title { font-size: 20px; line-height: 24px; @@ -97,11 +96,7 @@ select { /* 카테고리/정렬 필터 */ .restaurant-filter-container { - display: flex; - justify-content: space-between; - padding: 0 16px; - margin-top: 24px; } .restaurant-filter-select { @@ -121,15 +116,7 @@ select { background-size: 0.75rem auto; } -.arrow-down-grey { - background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%20viewBox%3D%220%200%2012%2012%22%3E%3Ctitle%3Edown-arrow%3C%2Ftitle%3E%3Cg%20fill%3D%22%%22%3E%3Cpath%20d%3D%22M10.293%2C3.293%2C6%2C7.586%2C1.707%2C3.293A1%2C1%2C0%2C0%2C0%2C.293%2C4.707l5%2C5a1%2C1%2C0%2C0%2C0%2C1.414%2C0l5-5a1%2C1%2C0%2C1%2C0-1.414-1.414Z%22%20fill%3D%22%23667085%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E"); -} - -.arrow-down-black { - background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%20viewBox%3D%220%200%2012%2012%22%3E%3Ctitle%3Edown-arrow%3C%2Ftitle%3E%3Cg%20fill%3D%22%%22%3E%3Cpath%20d%3D%22M10.293%2C3.293%2C6%2C7.586%2C1.707%2C3.293A1%2C1%2C0%2C0%2C0%2C.293%2C4.707l5%2C5a1%2C1%2C0%2C0%2C0%2C1.414%2C0l5-5a1%2C1%2C0%2C1%2C0-1.414-1.414Z%22%20fill%3D%22%23000000%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E"); -} - -/* 음식점 목록 */ +/* 음식점 목록, 음식점 상세 정보 */ .restaurant-list-container { display: flex; flex-direction: column; @@ -139,14 +126,18 @@ select { } .restaurant { + min-height: 7rem; display: flex; align-items: flex-start; padding: 16px 8px; border-bottom: 1px solid #e9eaed; +} - min-height: 7rem; +.restaurant-detail { + display: flex; + flex-direction: column; } .restaurant__category { @@ -164,17 +155,45 @@ select { background: var(--lighten-color); } +.detail-link { + color: var(--grey-500); +} + +.font-orange { + color: var(--primary-color); +} + +.font-gray { + color: var(--grey-300); +} + .category-icon { width: 36px; height: 36px; } +.restaurant__info__layout { + width: 100%; + display: flex; + justify-content: space-between; +} + +.restaurant__info__layout > favorite-icon { + height: 32px; +} + .restaurant__info { display: flex; flex-direction: column; justify-content: flex-start; } +.restaurant__detail__info { + display: flex; + flex-direction: column; + justify-content: flex-start; +} + .restaurant__name { margin: 0; } @@ -195,6 +214,21 @@ select { word-break: break-all; } +.tab-container { + padding: 32px 16px; + width: 100%; +} + +.restaurant-tab { + display: flex; + width: 100%; +} + +.tab-border-bottom { + width: 100%; + margin-top: 9px; +} + /* 음식점 추가 모달 *****************************************/ .modal { display: none; @@ -226,11 +260,6 @@ select { background: var(--grey-100); } -.stop-scroll { - height: 100%; - overflow: hidden; -} - .modal-title { margin-bottom: 36px; } @@ -291,6 +320,7 @@ input[name="link"] { } .button-container { + margin-top: 32px; display: flex; } @@ -324,10 +354,61 @@ input[name="link"] { color: var(--grey-100); } -.hidden { - visibility: hidden; -} +/* 공통 CSS 속성 *****************************************/ .error-message { color: var(--error-color); } + +.stop-scroll { + height: 100%; + overflow: hidden; +} + +.w-full { + width: 100%; +} + +.flex { + display: flex; +} + +.flex-col { + flex-direction: column; +} + +.justify-between { + justify-content: space-between; +} + +.hidden { + display: none; +} + +.gap-4 { + gap: 1rem; +} + +.font-bold { + font-weight: bold; +} + +.text-center { + text-align: center; +} + +.border-orange { + border: 2px solid var(--primary-color); +} + +.border-gray { + border: 2px solid var(--grey-300); +} + +.arrow-down-grey { + background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%20viewBox%3D%220%200%2012%2012%22%3E%3Ctitle%3Edown-arrow%3C%2Ftitle%3E%3Cg%20fill%3D%22%%22%3E%3Cpath%20d%3D%22M10.293%2C3.293%2C6%2C7.586%2C1.707%2C3.293A1%2C1%2C0%2C0%2C0%2C.293%2C4.707l5%2C5a1%2C1%2C0%2C0%2C0%2C1.414%2C0l5-5a1%2C1%2C0%2C1%2C0-1.414-1.414Z%22%20fill%3D%22%23667085%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E"); +} + +.arrow-down-black { + background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%20viewBox%3D%220%200%2012%2012%22%3E%3Ctitle%3Edown-arrow%3C%2Ftitle%3E%3Cg%20fill%3D%22%%22%3E%3Cpath%20d%3D%22M10.293%2C3.293%2C6%2C7.586%2C1.707%2C3.293A1%2C1%2C0%2C0%2C0%2C.293%2C4.707l5%2C5a1%2C1%2C0%2C0%2C0%2C1.414%2C0l5-5a1%2C1%2C0%2C1%2C0-1.414-1.414Z%22%20fill%3D%22%23000000%22%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fsvg%3E"); +} From 41f1b08eebadddc0cadc1fd84feda6eb3c468999 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sun, 17 Mar 2024 11:20:47 +0900 Subject: [PATCH 09/70] =?UTF-8?q?feat(RestaurantTab):=20=EB=AA=A8=EB=93=A0?= =?UTF-8?q?=20=EC=9D=8C=EC=8B=9D=EC=A0=90,=20=EC=9E=90=EC=A3=BC=20?= =?UTF-8?q?=EA=B0=80=EB=8A=94=20=EC=9D=8C=EC=8B=9D=EC=A0=90=20=EA=B5=AC?= =?UTF-8?q?=EB=B6=84=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 탭이 변경될 경우, currentTab 상태를 변경시키고 다시 렌더링 - 음식점 목록 컴포넌트에서 변경된 상태를 전달받고, 상황에 맞게 목록을 렌더링 --- README.md | 6 +-- .../RestaurantTap/RestaurantTabContainer.ts | 50 +++++++++++++++++++ src/components/RestaurantTap/index.ts | 19 +++++++ 3 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 src/components/RestaurantTap/RestaurantTabContainer.ts create mode 100644 src/components/RestaurantTap/index.ts diff --git a/README.md b/README.md index dc5207c0f..d4f3e7ba9 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ - [x] 음식점을 카테고리별로 필터링 한다. - [x] 음식점을 이름순, 거리순으로 정렬한다. - [x] 음식점 등록 폼을 제출하면 추가한다. -- [ ] 추가하기 버튼을 클릭할 경우 입력에 대한 유효성 검증을 한다 +- [x] 추가하기 버튼을 클릭할 경우 입력에 대한 유효성 검증을 한다 - 음식점 이름은 10글자 이하여야한다. - 음식점 설명은 300자 이하여야한다. - 유효한 참고 링크인지 검증한다. @@ -51,7 +51,7 @@ delete(restaurantName : string) : boolean - UI -- restaurant-item +- [x]restaurant-item - 음식점 목록을 확인할 수 있다. - 카테고리 @@ -62,7 +62,7 @@ delete(restaurantName : string) : boolean - Event - [x] 카테고리별 필터링, 정렬 옵션이 변경되는 이벤트가 발생하면 그에 맞춰 보여줘야 할 음식점 목록을 변경한다. -- [ ] 모든 음식점, 자주 가는 음식점을 클릭할 경우 그에 맞춰 보여줘야 할 음식점 목록을 변경한다. +- [x] 모든 음식점, 자주 가는 음식점을 클릭할 경우 그에 맞춰 보여줘야 할 음식점 목록을 변경한다. ## category-icon diff --git a/src/components/RestaurantTap/RestaurantTabContainer.ts b/src/components/RestaurantTap/RestaurantTabContainer.ts new file mode 100644 index 000000000..fd9201735 --- /dev/null +++ b/src/components/RestaurantTap/RestaurantTabContainer.ts @@ -0,0 +1,50 @@ +import BaseComponent from "../BaseComponent"; +import { MENU_APP_EVENTS } from "../../constants/event"; +import { RESTAURANT_TABS } from "../../constants/menu"; +import { RestaurantTab } from "../../types/menu"; + +class RestaurantTabContainer extends BaseComponent { + private currentTab: RestaurantTab = "all"; + + protected render() { + this.innerHTML = /*html*/ ` + + + `; + } + + private getChangedTabType(tabText: string): RestaurantTab { + if (tabText === RESTAURANT_TABS.all) return "all"; + if (tabText === RESTAURANT_TABS.favorite) return "favorite"; + } + + private shouldChangeTab(tabType: RestaurantTab): boolean | undefined { + return tabType && tabType !== this.currentTab; + } + + private handleTabChange(tabText: string) { + const changedTabType = this.getChangedTabType(tabText); + if (!this.shouldChangeTab(changedTabType)) return; + + this.currentTab = changedTabType; + this.render(); + this.emitEvent<{ tab: RestaurantTab }>(MENU_APP_EVENTS.changeTabState, { + tab: this.currentTab, + }); + } + + protected setEvent() { + this.addEventListener("click", (event) => { + if (!(event.target instanceof HTMLSpanElement)) return; + this.handleTabChange(event.target.innerText); + }); + } +} + +customElements.define("restaurant-tab-container", RestaurantTabContainer); diff --git a/src/components/RestaurantTap/index.ts b/src/components/RestaurantTap/index.ts new file mode 100644 index 000000000..d4c5c126b --- /dev/null +++ b/src/components/RestaurantTap/index.ts @@ -0,0 +1,19 @@ +import BaseComponent from "../BaseComponent"; + +class RestaurantTab extends BaseComponent { + protected render() { + const text = this.getAttribute("text") ?? ""; + const isActive = this.getAttribute("is-active") === "true" ? true : false; + + this.innerHTML = /*html*/ ` + ${text} +
+ `; + } +} + +customElements.define("restaurant-tab", RestaurantTab); From a3b60ad1c6ace0dcfe9322556ad865d023c4d4f6 Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sun, 17 Mar 2024 11:24:15 +0900 Subject: [PATCH 10/70] =?UTF-8?q?feat(CategoryImage):=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 음식점 아이템, 음식점 상세 정보 컴포넌트에서 동일한 카테고리 이미지 UI를 사용하기 때문에, 재사용할 수 있도록 컴포넌트 분리 --- README.md | 2 +- src/components/CategoryImage/index.ts | 52 +++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 src/components/CategoryImage/index.ts diff --git a/README.md b/README.md index d4f3e7ba9..09d12c75a 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ delete(restaurantName : string) : boolean - UI - - 전체, 한식, 중식,,, 등 카테고리에 따라 다른 이미지를 보여준다. +- [x] 전체, 한식, 중식,,, 등 카테고리에 따라 다른 이미지를 보여준다. ## restaurant-detail diff --git a/src/components/CategoryImage/index.ts b/src/components/CategoryImage/index.ts new file mode 100644 index 000000000..03039ca6e --- /dev/null +++ b/src/components/CategoryImage/index.ts @@ -0,0 +1,52 @@ +import BaseComponent from "../BaseComponent"; +import { CATEGORIES } from "../../constants/menu"; +import { CategoryStringWithoutAll } from "../../types/menu"; +import { + categoryKorean, + categoryAsian, + categoryChinese, + categoryJapanese, + categoryWestern, + categoryEtc, +} from "../../assets"; + +class CategoryImage extends BaseComponent { + private isCategoryType(category: string): category is CategoryStringWithoutAll { + return ["한식", "아시안", "일식", "중식", "양식", "기타"].includes(category); + } + + private categoryToImg(category: CategoryStringWithoutAll) { + if (!category) return; + + switch (category) { + case CATEGORIES.korean: + return categoryKorean; + case CATEGORIES.asian: + return categoryAsian; + case CATEGORIES.japanese: + return categoryJapanese; + case CATEGORIES.chinese: + return categoryChinese; + case CATEGORIES.western: + return categoryWestern; + case CATEGORIES.etc: + return categoryEtc; + default: + break; + } + } + + render() { + const category = this.getAttribute("category") ?? ""; + if (!this.isCategoryType(category)) return; + + const categoryImage = this.categoryToImg(category); + this.innerHTML = /*html*/ ` +
+ ${category} +
+ `; + } +} + +customElements.define("category-image", CategoryImage); From 6d9f200fba5fdc57606894107a7eed43b71760fd Mon Sep 17 00:00:00 2001 From: hwinkr Date: Sun, 17 Mar 2024 11:31:31 +0900 Subject: [PATCH 11/70] =?UTF-8?q?feat(RestaurantDetail):=20=EC=9D=8C?= =?UTF-8?q?=EC=8B=9D=EC=A0=90=20=EC=83=81=EC=84=B8=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 음식점 상세 정보를 보여준다 - 음식점 목록 컴포넌트에서 이벤트를 보내고, 이벤트와 함께 온 세부 정보를 사용해서 렌더링한다 - 음식점 상세 정보를 여는 이벤트가 발생할 때마다, document에 이벤트 핸들러가 쌓이는 문제가 있어, addEventListener의 3번째 인자에 once를 true로 설정 --- README.md | 20 +++--- src/components/RestaurantDetail/index.ts | 85 ++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 src/components/RestaurantDetail/index.ts diff --git a/README.md b/README.md index 09d12c75a..380e46d65 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ delete(restaurantName : string) : boolean - UI -- [x]restaurant-item +- [x] restaurant-item - 음식점 목록을 확인할 수 있다. - 카테고리 @@ -64,7 +64,7 @@ delete(restaurantName : string) : boolean - [x] 카테고리별 필터링, 정렬 옵션이 변경되는 이벤트가 발생하면 그에 맞춰 보여줘야 할 음식점 목록을 변경한다. - [x] 모든 음식점, 자주 가는 음식점을 클릭할 경우 그에 맞춰 보여줘야 할 음식점 목록을 변경한다. -## category-icon +## category-image - UI @@ -73,18 +73,18 @@ delete(restaurantName : string) : boolean ## restaurant-detail - UI +- 음식점 목록 중 하나를 클릭하면 해당 음식점에 대한 세부 정보를 나타낸다. - - 음식점 목록 중 하나를 클릭하면 해당 음식점에 대한 세부 정보를 나타낸다. - - [ ] 음식점 이름, 카테고리, 캠퍼스로부터의 거리, 자주 가는 음식점인지의 여부 - - [ ] 음식점 상세 설명, ...으로 표시하지 않고 모든 내용을 보여준다, - - [ ] 참고링크 + - [x] 음식점 이름, 카테고리, 캠퍼스로부터의 거리, 자주 가는 음식점인지의 여부 + - [x] 음식점 상세 설명, ...으로 표시하지 않고 모든 내용을 보여준다, + - [x] 참고링크 - Event -- [ ] 음식점 세부 정보의 바깥 영역을 클릭하거나, 닫기 버튼을 클릭하면 `restaurant-detail` 컴포넌트는 사라져야한다. -- [ ] 음식점 세부 정보에서 자주 가는 음식점인지의 여부를 변경하면, 변경이 반영되어야한다. -- [ ] 참고 링크를 클릭하면 해당 링크로 이동시킨다. -- [ ] 삭제하기 버튼을 클릭할 경우, 해당 음식점을 삭제한 후 `restaurant-detail` 컴포넌트는 사라져야한다. +- [x] 음식점 세부 정보의 바깥 영역을 클릭하거나, 닫기 버튼을 클릭하면 `restaurant-detail` 컴포넌트는 사라져야한다. +- [x] 음식점 세부 정보에서 자주 가는 음식점인지의 여부를 변경하면, 변경이 반영되어야한다. +- [x] 참고 링크를 클릭하면 해당 링크로 이동시킨다. +- [x] 삭제하기 버튼을 클릭할 경우, 해당 음식점을 삭제한 후 `restaurant-detail` 컴포넌트는 사라져야한다. ## 모든 음식점, 자주 가는 음식점 구분 버튼 (이름 미정) diff --git a/src/components/RestaurantDetail/index.ts b/src/components/RestaurantDetail/index.ts new file mode 100644 index 000000000..84d49d9b8 --- /dev/null +++ b/src/components/RestaurantDetail/index.ts @@ -0,0 +1,85 @@ +import BaseComponent from "../BaseComponent"; +import { MENU_APP_EVENTS } from "../../constants/event"; +import { RestaurantItem } from "../../types/menu"; +import { deleteRestaurantByName } from "../../domains/Restaurants"; +import { $ } from "../../utils/dom"; +import { isRestaurantItemType } from "../../utils/types"; + +const RESTAURANT_KEYS = ["name", "category", "distance", "isFavorite", "description", "link"]; + +class RestaurantDetail extends BaseComponent { + private detailInfo: RestaurantItem | null = null; + + private getDetailTemplate(): string { + if (!this.detailInfo) return ``; + + const { name, category, distance, description, isFavorite, link } = this.detailInfo; + + return /*html*/ ` +
+
+
+ +

${name}

+ 캠퍼스부터 ${distance}분 내 +
+ +
+

${description}

+ ${ + link + ? `${link}` + : '

참고 링크가 존재하지 않습니다.

' + } +
+ `; + } + + render() { + this.innerHTML = /*html*/ ` +