diff --git a/cypress.config.ts b/cypress.config.ts new file mode 100644 index 000000000..17161e32e --- /dev/null +++ b/cypress.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "cypress"; + +export default defineConfig({ + e2e: { + setupNodeEvents(on, config) { + // implement node event listeners here + }, + }, +}); diff --git a/cypress/e2e/spec.cy.js b/cypress/e2e/spec.cy.js new file mode 100644 index 000000000..6c720b689 --- /dev/null +++ b/cypress/e2e/spec.cy.js @@ -0,0 +1,175 @@ +const restaurants = [ + { + category: "한식", + name: "한식집1", + distance: "5", + like: "true", + description: + "한식집1 더미 설명입니다. 한식집1 더미 설명입니다. 한식집1 더미 설명입니다.", + link: "", + id: "1", + }, + { + category: "한식", + name: "한식집2", + distance: "10", + like: "true", + description: + "한식집2 더미 설명입니다. 한식집2 더미 설명입니다. 한식집2 더미 설명입니다.", + link: "", + id: "2", + }, + { + category: "중식", + name: "중식집1", + distance: "15", + like: "false", + description: + "중식집1 더미 설명입니다. 중식집1 더미 설명입니다. 중식집1 더미 설명입니다.", + link: "", + id: "3", + }, + { + category: "양식", + name: "양식집1", + distance: "20", + like: "true", + description: + "양식집1 더미 설명입니다. 양식집1 더미 설명입니다. 양식집1 더미 설명입니다.", + link: "", + id: "4", + }, + { + category: "일식", + name: "일식집1", + distance: "30", + like: "false", + description: + "일식집1 더미 설명입니다. 일식집1 더미 설명입니다. 일식집1 더미 설명입니다.", + link: "", + id: "5", + }, +]; + +beforeEach(() => { + cy.visit("http://localhost:8080/", { + onBeforeLoad(win) { + win.localStorage.setItem("restaurants", JSON.stringify(restaurants)); + }, + }); +}); + +describe("음식점 추가 모달 기능 테스트", () => { + it("값 입력 후 제출시 화면에 추가된 음식점이 렌더링 된다.", () => { + cy.contains("한식집3").should("not.exist"); + + cy.get(".gnb__button").click(); + + cy.get("#category").select("한식"); + cy.get("#name").type("한식집3"); + cy.get("#distance").select("5"); + cy.get("#description").type( + "한식집3 더미 설명입니다. 한식집3 더미 설명입니다. 한식집3 더미 설명입니다." + ); + + cy.get("form").submit(); + + cy.contains("한식집3").should("be.visible"); + }); + + it("필수 값을 입력하지 않고 제출시 모달이 열린채로 유지되며, 제출되지 않는다.", () => { + cy.get(".gnb__button").click(); + + cy.get("#name").type("한식집3"); + cy.get("#distance").select("5"); + cy.get("#description").type( + "한식집3 더미 설명입니다. 한식집3 더미 설명입니다. 한식집3 더미 설명입니다." + ); + + cy.get(".submit-button").click(); + + cy.get("form").should("be.visible"); + cy.contains("한식집3").should("not.exist"); + }); +}); + +describe("음식점 리스트 렌더링 테스트", () => { + it("카테고리 필터 선택시 해당하는 카테고리를 가진 음식점만 렌더링 된다.", () => { + cy.get("#category-filter").select("한식"); + + cy.get(".restaurant-list").should((elem) => + elem.children().each((_, li) => { + expect(li.getAttribute("category")).to.equal("한식"); + }) + ); + }); + + it("정렬 필터 선택시 해당 정렬 기준에 의해 렌더링 된다.", () => { + const sortedTitleByDistance = [ + "한식집1", + "한식집2", + "중식집1", + "양식집1", + "일식집1", + ]; + const sortedTitleByName = [ + "양식집1", + "일식집1", + "중식집1", + "한식집1", + "한식집2", + ]; + + cy.get("#sorting-filter").select("distance"); + + cy.get(".restaurant-list").should((elem) => { + elem.children().each((index, li) => { + expect(li.querySelector(".restaurant__name").innerText).to.equal( + sortedTitleByDistance[index] + ); + }); + }); + + cy.get("#sorting-filter").select("name"); + + cy.get(".restaurant-list").should((elem) => { + elem.children().each((index, li) => { + expect(li.querySelector(".restaurant__name").innerText).to.equal( + sortedTitleByName[index] + ); + }); + }); + }); + + it("즐겨찾기 탭 클릭시 즐겨찾기한 음식점만 렌더링 된다.", () => { + cy.get("#like-button").click(); + + cy.get(".restaurant-list").should((elem) => + elem.children().each((_, li) => { + expect(li.getAttribute("like")).to.equal("true"); + }) + ); + }); +}); + +describe("음식점 상세 모달 기능 테스트", () => { + it("음식점 카드 클릭시 해당 음식점의 상세 모달이 렌더링 된다.", () => { + cy.contains("일식집1").click(); + + cy.get("form").should("not.be.visible"); + cy.get(".modal-detail").should("be.visible"); + cy.get(".modal-detail").contains("일식집1").should("be.visible"); + }); + + it("삭제하기 버튼 클릭시 음식점 리스트에서 해당 항목이 삭제된다.", () => { + cy.contains("일식집1").click(); + + cy.contains("삭제하기").click(); + + cy.get(".restaurant-list").contains("한식집1").should("be.visible"); + cy.get(".restaurant-list").contains("한식집2").should("be.visible"); + cy.get(".restaurant-list").contains("양식집1").should("be.visible"); + cy.get(".restaurant-list").contains("중식집1").should("be.visible"); + cy.get(".restaurant-list").contains("일식집1").should("not.exist"); + }); +}); diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json new file mode 100644 index 000000000..02e425437 --- /dev/null +++ b/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts new file mode 100644 index 000000000..698b01a42 --- /dev/null +++ b/cypress/support/commands.ts @@ -0,0 +1,37 @@ +/// +// *********************************************** +// This example commands.ts shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) +// +// declare global { +// namespace Cypress { +// interface Chainable { +// login(email: string, password: string): Chainable +// drag(subject: string, options?: Partial): Chainable +// dismiss(subject: string, options?: Partial): Chainable +// visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable +// } +// } +// } \ No newline at end of file diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts new file mode 100644 index 000000000..f80f74f8e --- /dev/null +++ b/cypress/support/e2e.ts @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/e2e.ts is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') \ No newline at end of file diff --git a/src/App.ts b/src/App.ts index af7c44c2f..6f9c2822f 100644 --- a/src/App.ts +++ b/src/App.ts @@ -16,6 +16,11 @@ import { RestaurantAddForm, } from "./components/modal/form"; import { createInputBox } from "./components/modal/form/InputBox"; +import { + createModalDetailContent, + DetailModal, +} from "./components/modal/detail"; +import { createDetailInfo, Info } from "./components/modal/detail/info"; class App { #restaurants; @@ -23,6 +28,7 @@ class App { #showState: { filter: CategoryOption; sort: SortOption; + like: boolean; }; constructor() { @@ -31,6 +37,7 @@ class App { this.#showState = { filter: "전체", sort: "name", + like: false, }; this.#restaurants = new Restaurants(restaurants); @@ -45,6 +52,8 @@ class App { createHeader(); createRestaurantCard(); createRestaurantCardList(); + createModalDetailContent(); + createDetailInfo(); this.renderContainer(); this.renderRestaurantList(); @@ -63,12 +72,43 @@ class App { document .querySelector("form") ?.bindEvent(this.addNewRestaurant.bind(this)); + + document + .querySelector(".modal-detail") + ?.bindEvent( + this.onClickRestaurantRemove.bind(this), + this.onClickDetailLikeButton.bind(this) + ); + + document + .querySelector(".restaurant-list") + ?.bindEvent( + this.onClickRestaurantLikeButton.bind(this), + this.onClickRestaurantCard.bind(this) + ); + + document + .querySelector(".like-filter-container") + ?.addEventListener("click", (event: MouseEvent) => { + const likeState = JSON.parse( + `${(event.target as HTMLElement).getAttribute("like")}` + ); + + this.#showState = { ...this.#showState, like: likeState }; + + this.toggleLikeButtonStyle(); + this.renderRestaurantList(); + }); } renderContainer() { document.body.innerHTML = `
+
@@ -97,13 +137,67 @@ class App { this.renderRestaurantList(); } + onClickRestaurantCard(restaurantId: string) { + const restaurantInfo = this.#restaurants.getRestaurantById(restaurantId); + + if (restaurantInfo) + document.querySelector(".modal")?.openDetailModal(restaurantInfo); + } + + onClickRestaurantLikeButton(restaurantId: string) { + this.#restaurants.toggleLike(restaurantId); + + localStorage.setItem( + "restaurants", + JSON.stringify(this.#restaurants.getList()) + ); + + this.renderRestaurantList(); + } + + onClickDetailLikeButton(restaurantId: string) { + this.onClickRestaurantLikeButton(restaurantId); + + const restaurantInfo = this.#restaurants.getRestaurantById(restaurantId); + + if (restaurantInfo) + document + .querySelector(".restaurant-detail-container") + ?.renderContent(restaurantInfo); + } + + onClickRestaurantRemove(restaurantId: string) { + this.#restaurants.removeById(restaurantId); + + localStorage.setItem( + "restaurants", + JSON.stringify(this.#restaurants.getList()) + ); + + this.renderRestaurantList(); + document.querySelector(".modal")?.closeModal(); + } + + toggleLikeButtonStyle() { + const likeState = this.#showState.like; + const allLikeButton = document.querySelector("#all-like-button"); + const likeButton = document.querySelector("#like-button"); + + if (likeState) { + allLikeButton?.classList.remove("like-filter-button--activated"); + likeButton?.classList.add("like-filter-button--activated"); + return; + } + allLikeButton?.classList.add("like-filter-button--activated"); + likeButton?.classList.remove("like-filter-button--activated"); + } + addNewRestaurant(restaurant: Restaurant) { this.#restaurants.add(restaurant); + localStorage.setItem( "restaurants", - JSON.stringify( - this.#restaurants.getListByOption({ filter: "전체", sort: "name" }) - ) + JSON.stringify(this.#restaurants.getList()) ); this.renderRestaurantList(); diff --git a/src/components/modal/detail/index.ts b/src/components/modal/detail/index.ts new file mode 100644 index 000000000..fbb7ae548 --- /dev/null +++ b/src/components/modal/detail/index.ts @@ -0,0 +1,53 @@ +import { Modal } from ".."; +import { Info } from "./info"; + +export class DetailModal extends HTMLDivElement { + constructor() { + super(); + + this.init(); + } + + init() { + this.innerHTML = ` +
+
+
+ + +
+
+ `; + } + + bindEvent( + handleClickRemove: (restaurantId: string) => void, + handleClickLikeIcon: (restaurantId: string) => void + ) { + this.querySelector(".button--secondary")?.addEventListener("click", () => { + const restaurantId = this.getRestaurantId(); + + handleClickRemove(restaurantId); + }); + + this.querySelector(".button--primary")?.addEventListener("click", () => { + document.querySelector(".modal")?.closeModal(); + }); + + this.querySelector(".detail-like-image")?.addEventListener("click", () => { + const restaurantId = this.getRestaurantId(); + + handleClickLikeIcon(restaurantId); + }); + } + + getRestaurantId() { + return `${this.querySelector( + ".restaurant-detail-container" + )?.getId()}`; + } +} + +export const createModalDetailContent = () => { + customElements.define("modal-detail", DetailModal, { extends: "div" }); +}; diff --git a/src/components/modal/detail/info.ts b/src/components/modal/detail/info.ts new file mode 100644 index 000000000..113ce442c --- /dev/null +++ b/src/components/modal/detail/info.ts @@ -0,0 +1,69 @@ +import { + getCategoryImage, + getLikeImage, +} from "../../../constants/categoryImage"; +import { Restaurant } from "../../../types/restaurant"; + +export class Info extends HTMLDivElement { + constructor() { + super(); + + this.init(); + } + + init() { + this.innerHTML = ` +
+
+ +
+ +
+
+

+ +
+
+ + `; + } + + renderContent(restaurant: Restaurant) { + this.id = restaurant.id; + + ( + this.querySelector(".detail-category-image") as HTMLImageElement + ).src = `${getCategoryImage(restaurant.category)}`; + + ( + this.querySelector(".detail-like-image") as HTMLImageElement + ).src = `${getLikeImage(restaurant.like)}`; + + ( + this.querySelector("h3") as HTMLHeadingElement + ).innerText = `${restaurant.name}`; + + ( + this.querySelector(".restaurant__distance") as HTMLSpanElement + ).innerText = `캠퍼스부터 ${restaurant.distance}분 내`; + + ( + this.querySelector(".detail-text") as HTMLDivElement + ).innerText = `${restaurant.description}`; + + ( + this.querySelector(".detail-link") as HTMLAnchorElement + ).innerText = `${restaurant.link}`; + ( + this.querySelector(".detail-link") as HTMLAnchorElement + ).href = `${restaurant.link}`; + } + + getId() { + return this.id; + } +} + +export const createDetailInfo = () => { + customElements.define("detail-info", Info, { extends: "div" }); +}; diff --git a/src/components/modal/form/InputBox.ts b/src/components/modal/form/InputBox.ts index 799db1300..56f3f4150 100644 --- a/src/components/modal/form/InputBox.ts +++ b/src/components/modal/form/InputBox.ts @@ -30,6 +30,8 @@ export class InputBox extends HTMLDivElement { name=${name} id=${inputId} ${caption === null && "required"} + ${name === "name" && 'maxlength="15"'} + class="input-handle-element" /> ` : isSelect === null @@ -40,6 +42,7 @@ export class InputBox extends HTMLDivElement { cols="30" rows="5" ${caption === null && "required"} + class="input-handle-element" > ` : ` @@ -48,6 +51,7 @@ export class InputBox extends HTMLDivElement { name=${name} id=${inputId} ${caption === null && "required"} + class="input-handle-element" > ` } @@ -60,11 +64,8 @@ export class InputBox extends HTMLDivElement { } getValue() { - return (this.children[1] as InputHandleElement).value; - } - - resetValue() { - (this.children[1] as InputHandleElement).value = ""; + return this.querySelector(".input-handle-element") + ?.value; } } diff --git a/src/components/modal/form/index.ts b/src/components/modal/form/index.ts index 03695ea0f..d3d3989e3 100644 --- a/src/components/modal/form/index.ts +++ b/src/components/modal/form/index.ts @@ -2,6 +2,7 @@ import type { Category, Distance, Restaurant } from "../../../types/restaurant"; import { Modal } from ".."; import { InputBox } from "./InputBox"; +import Random from "../../../utils/Random"; export class RestaurantAddForm extends HTMLFormElement { constructor() { @@ -26,7 +27,7 @@ export class RestaurantAddForm extends HTMLFormElement { > 취소하기 - @@ -50,9 +51,8 @@ export class RestaurantAddForm extends HTMLFormElement { const category = this.querySelector( "[inputid='category']" )?.getValue() as Category; - const name = this.querySelector( - "[inputid='name']" - )?.getValue() as string; + const name = + this.querySelector("[inputid='name']")?.getValue() ?? ""; const distance = Number( this.querySelector("[inputid='distance']")?.getValue() ) as Distance; @@ -60,17 +60,14 @@ export class RestaurantAddForm extends HTMLFormElement { "[inputid='description']" )?.getValue(); const link = this.querySelector("[inputid='link']")?.getValue(); + const like = false; + const id = Random.generateUniqueId(); - return { category, name, distance, description, link }; + return { category, name, distance, description, link, like, id }; } resetFormValues() { - this.querySelector("[inputid='category']")?.resetValue(); - (this.querySelector("#category") as HTMLSelectElement).value = ""; - (this.querySelector("#name") as HTMLInputElement).value = ""; - (this.querySelector("#distance") as HTMLSelectElement).value = ""; - (this.querySelector("#description") as HTMLTextAreaElement).value = ""; - (this.querySelector("#link") as HTMLInputElement).value = ""; + this.reset(); } } diff --git a/src/components/modal/index.ts b/src/components/modal/index.ts index 7b4da3da0..56b692fe7 100644 --- a/src/components/modal/index.ts +++ b/src/components/modal/index.ts @@ -1,3 +1,6 @@ +import type { Restaurant } from "../../types/restaurant"; +import { Info } from "./detail/info"; + import { RestaurantAddForm } from "./form"; export class Modal extends HTMLDivElement { @@ -14,8 +17,9 @@ export class Modal extends HTMLDivElement { this.innerHTML = ` `; } @@ -33,11 +37,23 @@ export class Modal extends HTMLDivElement { openFormModal() { this.classList.add("modal--open"); this.querySelector("form")?.removeAttribute("hidden"); + this.querySelector("h2")?.removeAttribute("hidden"); + } + + openDetailModal(restaurantInfo: Restaurant) { + this.classList.add("modal--open"); + this.querySelector(".modal-detail")?.removeAttribute("hidden"); + + this.querySelector(".restaurant-detail-container")?.renderContent( + restaurantInfo + ); } closeModal() { this.classList.remove("modal--open"); this.querySelector("form")?.setAttribute("hidden", "true"); + this.querySelector("h2")?.setAttribute("hidden", "true"); + this.querySelector(".modal-detail")?.setAttribute("hidden", "true"); this.querySelector("form")?.resetFormValues(); } } diff --git a/src/components/restaurant/RestaurantCard.ts b/src/components/restaurant/RestaurantCard.ts index addf24c7f..cde740fc5 100644 --- a/src/components/restaurant/RestaurantCard.ts +++ b/src/components/restaurant/RestaurantCard.ts @@ -1,6 +1,6 @@ import type { Category } from "../../types/restaurant"; -import { getCategoryImage } from "../../constants/categoryImage"; +import { getCategoryImage, getLikeImage } from "../../constants/categoryImage"; class RestaurantCard extends HTMLLIElement { constructor() { @@ -16,6 +16,7 @@ class RestaurantCard extends HTMLLIElement { const name = this.getAttribute("name"); const distance = this.getAttribute("distance"); const description = this.getAttribute("description"); + const like = JSON.parse(`${this.getAttribute("like")}`); this.innerHTML = `
@@ -31,8 +32,14 @@ class RestaurantCard extends HTMLLIElement { 캠퍼스부터 ${distance}분 내

- ${description ?? ""} + ${description ? description : ""}

+
`; } diff --git a/src/components/restaurant/RestaurantCardList.ts b/src/components/restaurant/RestaurantCardList.ts index eb1710d37..835e5b64a 100644 --- a/src/components/restaurant/RestaurantCardList.ts +++ b/src/components/restaurant/RestaurantCardList.ts @@ -11,17 +11,38 @@ export class RestaurantCardList extends HTMLUListElement { .map( (restaurant) => `
  • ` ) .join("")} `; } + + bindEvent( + handleClickLikeIcon: (restaurantName: string) => void, + handleClickCard: (restaurantName: string) => void + ) { + this.addEventListener("click", (event: MouseEvent) => { + if (!(event.target instanceof HTMLElement)) return; + + const restaurantId = event.target.closest("li")?.getAttribute("id") ?? ""; + + if (event.target.className === "like-icon") { + handleClickLikeIcon(restaurantId); + return; + } + + handleClickCard(restaurantId); + }); + } } export const createRestaurantCardList = () => { diff --git a/src/constants/categoryImage.ts b/src/constants/categoryImage.ts index f855a5848..cb02003b3 100644 --- a/src/constants/categoryImage.ts +++ b/src/constants/categoryImage.ts @@ -6,6 +6,8 @@ import categoryChineseImage from "../../templates/category-chinese.png"; import categoryJapaneseImage from "../../templates/category-japanese.png"; import categoryWesternImage from "../../templates/category-western.png"; import categoryEtcImage from "../../templates/category-etc.png"; +import restaurant_unlike from "../../templates/restaurant_unlike.svg"; +import restaurant_like from "../../templates/restaurant_like.svg"; const categoryImages = { 한식: categoryKoreanImage, @@ -18,3 +20,6 @@ const categoryImages = { export const getCategoryImage = (category: Category) => categoryImages[category] ?? categoryImages["기타"]; + +export const getLikeImage = (like: boolean) => + like ? restaurant_like : restaurant_unlike; diff --git a/src/domain/Restaurants.ts b/src/domain/Restaurants.ts index 75e816eba..c1c3baa31 100644 --- a/src/domain/Restaurants.ts +++ b/src/domain/Restaurants.ts @@ -8,27 +8,72 @@ class Restaurants { this.#list = list; } - getListByOption(showState: { filter: CategoryOption; sort: SortOption }) { - const filteredList = this.filterByCategory(showState.filter); - const sortedList = this.sortBySortOption(filteredList, showState.sort); + getList() { + return this.#list; + } + + getListByOption(showState: { + filter: CategoryOption; + sort: SortOption; + like: boolean; + }) { + const likeFilteredList = this.filterByLike(showState.like); + const categoryFilteredList = this.filterByCategory( + likeFilteredList, + showState.filter + ); + const sortedList = this.sortBySortOption( + categoryFilteredList, + showState.sort + ); return sortedList; } + getRestaurantById(restaurantId: string) { + return this.#list.find((restaurant) => restaurant.id === restaurantId); + } + add(restaurant: Restaurant) { this.#list = [...this.#list, restaurant]; } sortBySortOption(restaurantList: Restaurant[], sortOption: SortOption) { + if (sortOption === "distance") + return [...restaurantList].sort((first, second) => + Number(first[sortOption]) > Number(second[sortOption]) ? 1 : -1 + ); return [...restaurantList].sort((first, second) => first[sortOption] > second[sortOption] ? 1 : -1 ); } - filterByCategory(category: CategoryOption) { - if (category === "전체") return [...this.#list]; + filterByCategory(restaurantList: Restaurant[], category: CategoryOption) { + if (category === "전체") return [...restaurantList]; + + return [...restaurantList].filter( + (restaurant) => restaurant.category === category + ); + } + + filterByLike(likeOption: boolean) { + if (likeOption) + return this.#list.filter((restaurant) => `${restaurant.like}` === "true"); + return this.#list; + } - return this.#list.filter((restaurant) => restaurant.category === category); + toggleLike(restaurantId: string) { + this.#list = this.#list.map((restaurant) => { + if (restaurant.id === restaurantId) + return { ...restaurant, like: !restaurant.like }; + return restaurant; + }); + } + + removeById(restaurantId: string) { + this.#list = this.#list.filter( + (restaurant) => restaurant.id !== restaurantId + ); } } diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 49977505d..08f5f4f61 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1 +1,2 @@ declare module "*.png"; +declare module "*.svg"; diff --git a/src/types/restaurant.d.ts b/src/types/restaurant.d.ts index e1b1f85ee..2d7701045 100644 --- a/src/types/restaurant.d.ts +++ b/src/types/restaurant.d.ts @@ -3,9 +3,11 @@ export type Category = "한식" | "중식" | "일식" | "아시안" | "양식" | export type Distance = 5 | 10 | 15 | 20 | 30; export interface Restaurant { + id: string; category: Category; name: string; distance: Distance; + like: boolean; description?: string; link?: string; } diff --git a/src/utils/Random.ts b/src/utils/Random.ts new file mode 100644 index 000000000..2e4d2af26 --- /dev/null +++ b/src/utils/Random.ts @@ -0,0 +1,7 @@ +const Random = { + generateUniqueId() { + return Math.random().toString(36).substring(2, 15); + }, +}; + +export default Random; diff --git a/templates/restaurant_like.svg b/templates/restaurant_like.svg new file mode 100644 index 000000000..6134378dc --- /dev/null +++ b/templates/restaurant_like.svg @@ -0,0 +1,3 @@ + + + diff --git a/templates/restaurant_unlike.svg b/templates/restaurant_unlike.svg new file mode 100644 index 000000000..828894509 --- /dev/null +++ b/templates/restaurant_unlike.svg @@ -0,0 +1,3 @@ + + + diff --git a/templates/style.css b/templates/style.css index 8f2570a34..fef7cf0e7 100644 --- a/templates/style.css +++ b/templates/style.css @@ -2,6 +2,8 @@ padding: 0; margin: 0; box-sizing: border-box; + overflow-x: hidden; + overflow-y: auto; } ul, @@ -15,6 +17,10 @@ body { font-size: 16px; } +h1 { + overflow: hidden; +} + /* Colors *****************************************/ :root { --primary-color: #ec4a0a; @@ -87,6 +93,34 @@ body { /* 음식점 목록 *****************************************/ +.like-filter-container { + height: 60px; + display: flex; + align-items: center; + justify-content: center; + margin-top: 20px; +} + +.like-filter-button { + border-bottom: 2px solid var(--grey-200); + color: var(--grey-300); + + flex: 1; + height: 60px; + display: flex; + align-items: center; + justify-content: center; + + font-weight: bold; + + cursor: pointer; +} + +.like-filter-button--activated { + border-bottom: 2px solid var(--primary-color); + color: var(--primary-color); +} + /* 카테고리/정렬 필터 */ .restaurant-filter-container { display: flex; @@ -127,6 +161,10 @@ body { padding: 16px 8px; border-bottom: 1px solid #e9eaed; + + position: relative; + + cursor: pointer; } .restaurant__category { @@ -149,6 +187,15 @@ body { height: 36px; } +.like-icon { + width: 30px; + height: 30px; + position: absolute; + right: 15px; + top: 15px; + cursor: pointer; +} + .restaurant__info { display: flex; flex-direction: column; @@ -296,3 +343,29 @@ input[name="link"] { color: var(--grey-100); } + +/* 음식점 세부정보 모달 *****************************************/ + +.restaurant-detail-container { + display: flex; + flex-direction: column; + gap: 10px; + width: calc(100vw - 32px); +} + +.image-container { + position: relative; + width: calc(100% - 16px); +} + +.detail-like-image { + position: absolute; + top: 0; + right: 0; +} + +.detail-text { + overflow: scroll; + overflow-x: hidden; + max-height: 50vh; +}