Skip to content

Commit

Permalink
[2단계 - 자주 가는 음식점] - 초코(강다빈) 미션 제출합니다. (#176)
Browse files Browse the repository at this point in the history
* docs: 기능 요구 사항 정리

Co-Authored-By: 00kang <[email protected]>

* chore: 프로젝트 초기 설정

- eslint
- prettier
- tsconfig(경로 별칭)
- package.json(typescript-eslint 설치)

Co-Authored-By: 00kang <[email protected]>

* feat: 카테고리, 이름, 거리(도보 이동 시간), 설명, 참고 링크를 저장한다.

Co-Authored-By: 00kang <[email protected]>

* feat: 음식점 목록에 새로운 음식점을 추가

Co-Authored-By: 00kang <[email protected]>

* feat: 음식점 목록을 확인

Co-Authored-By: 00kang <[email protected]>

* chore: typescript 모듈 테스트를 위한 jest 설정

Co-Authored-By: 00kang <[email protected]>

* test: RestaurantManager 기능 테스트

Co-Authored-By: 00kang <[email protected]>

* chore: template 이미지 리소스 view로 복사

Co-Authored-By: 00kang <[email protected]>

* feat: 모달 열고 닫는 기능

Co-Authored-By: 00kang <[email protected]>

* feat: 식당 등록 폼 커스텀 이벤트

Co-Authored-By: 00kang <[email protected]>

* feat: Form 제출 시 localStorage에 저장

Co-Authored-By: 00kang <[email protected]>

* chore: 정적 파일 import

Co-Authored-By: 00kang <[email protected]>

* feat: Select 컴포넌트 제작

Co-Authored-By: 00kang <[email protected]>

* feat: GNB를 웹 컴포넌트로 전환

Co-Authored-By: 00kang <[email protected]>

* chore: Select 컴포넌트 위치 변경

Co-Authored-By: 00kang <[email protected]>

* feat: RestaurantForm를 웹 컴포넌트로 전환

Co-Authored-By: 00kang <[email protected]>

* feat: Modal을 웹 컴포넌트 AppModal로 전환

Co-Authored-By: 00kang <[email protected]>

* chore: AppModal의 이름을 Modal로 변경

Co-Authored-By: 00kang <[email protected]>

* chore: 레스토랑 필터 select 박스 배치

Co-Authored-By: 00kang <[email protected]>

* feat: localStorage와 도메인 연동

Co-Authored-By: 00kang <[email protected]>

* feat: localStorage와 도메인 연동

- 페이지가 로드됐을 때
- 필터를 변경할 때

Co-Authored-By: 00kang <[email protected]>

* feat: 음식점 렌더링 컴포넌트

Co-Authored-By: 00kang <[email protected]>

* feat: 레스토랑 목록을 렌더링하는 컴포넌트

Co-Authored-By: 00kang <[email protected]>

* feat: 식당 데이터를 웹 뷰에 렌더링

Co-Authored-By: 00kang <[email protected]>

* test: RestaurantManger filteredAndSortedByOptions 테스트 케이스 추가

Co-Authored-By: 00kang <[email protected]>

* chore: eslint 적용에 따른 코드 수정

Co-Authored-By: 00kang <[email protected]>

* refactor: WebController 함수 분리

Co-Authored-By: 00kang <[email protected]>

* docs: 기능 요구사항 문서 업데이트

Co-Authored-By: 00kang <[email protected]>

* fix: 폼이 제출되었을 때 input이 초기화되지 않는 버그 수정

Co-Authored-By: 00kang <[email protected]>

* feat: 링크 유효성 검사

Co-Authored-By: 00kang <[email protected]>

* chore: console.log 제거

Co-Authored-By: 00kang <[email protected]>

* docs: 실행 결과 gif

Co-Authored-By: 00kang <[email protected]>

* refacotr: 오타 수정

* chore: webpack 재설치 업데이트

* refactor: filtering과 sorting 관련 수정

- 원본 배열이 아닌 복사본으로 sorting
- filteredAndSortedByOptions 메서드의 반환타입 명시

* refactor: CSS 파일 수정

- 임포트 파일명 수정
- global.css 에서 분리
- 필요없는 주석 삭제

* refactor: 불필요한 async 삭제, 오타 수정

* refactor: innerHTML 대신 textContent 사용

* refactor: attributeChangedCallback 수정

* docs: step2 미션에 맞게 REQUIREMENTS.md 업데이트

* feat: 탭바, 즐겨찾기 아이콘 추가

* feat: 음식점 목록에서 즐겨찾기 등록

* feat: 전체 음식점과 즐겨찾는 음식점 탭 구현

* feat: 레스토랑 아이템 클릭시 디테일 모달 띄우

* feat: 디테일 모달 버튼 기능 구현

- 모달에 스타일도, 기능도 아무것도 적용되고 있지 않는 문제 발생!

* feat: 디테일 모달 내 링크 클릭시 이동

* test(e2e) : 탭 클릭으로 화면 전환 테스트

* style: 레스토랑 디테일 모달 스타일링

* fix: 레스토랑 디테일 모달 내의 컨텐츠 로드 에러 해결

- template 내의 style css 모듈로 분리
- RestaurantItem에서 렌더링하던 데이터, RestaurntDetail로 넘겨주기

* fix: } 추가

* feat: 불필요한 코드 삭제

---------

Co-authored-by: Kim Da Eun <[email protected]>
  • Loading branch information
00kang and llqqssttyy authored Mar 19, 2024
1 parent 40da872 commit 483cf1a
Show file tree
Hide file tree
Showing 22 changed files with 652 additions and 49 deletions.
7 changes: 5 additions & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@
"browser": true,
"es6": true,
"node": true,
"jest": true
"jest": true,
"cypress/globals": true

},
"plugins": ["cypress"],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"extends": ["airbnb-base", "prettier"],
"extends": ["airbnb-base", "prettier", "eslint:recommended", "plugin:cypress/recommended"],
"ignorePatterns": ["dist"]
}
10 changes: 10 additions & 0 deletions cypress.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from 'cypress';

export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
},
baseUrl: 'http://localhost:8080/',
},
});
15 changes: 15 additions & 0 deletions cypress/e2e/cypress-test.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
describe('화면 전환 확인', () => {
beforeEach(() => {
cy.visit('/'); // 웹 페이지로 이동
});

it('메뉴 탭 클릭으로 화면 전환 확인', () => {
// '모든 음식점' 탭을 클릭하고 활성화 여부를 확인합니다.
cy.contains('모든 음식점').click();
cy.get('.tab__item.active').should('contain', '모든 음식점');

// '자주 가는 음식점' 탭을 클릭하고 활성화 여부를 확인합니다.
cy.contains('자주 가는 음식점').click();
cy.get('.tab__item.active').should('contain', '자주 가는 음식점');
});
});
5 changes: 5 additions & 0 deletions cypress/fixtures/example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "[email protected]",
"body": "Fixtures are a great way to mock data for responses to routes"
}
37 changes: 37 additions & 0 deletions cypress/support/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/// <reference types="cypress" />
// ***********************************************
// 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<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }
20 changes: 20 additions & 0 deletions cypress/support/e2e.ts
Original file line number Diff line number Diff line change
@@ -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')
30 changes: 27 additions & 3 deletions docs/REQUIREMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,45 @@
## 음식점

- [x] 음식점을 추가할 수 있다.

- 카테고리, 이름, 거리는 필수 정보다.
- 설명, 참고 링크는 옵션이다. 정보가 없어도 음식점 추가가 가능하다.
- 카테고리는 "한식", "중식", "일식", "아시안", "양식", "기타" 중 하나를 선택한다.
- 거리는 캠퍼스로부터 도보로 걸리는 시간(분). 5, 10, 15, 20, 30 중 하나를 선택한다.

- [ ] 음식점의 상세 정보를 확인할 수 있다.

- 음식점을 삭제할 수 있다.

- [ ] 자주 가는 음식점을 추가하고 목록으로 확인할 수 있다.

- 음식점 목록에서 추가할 수 있다.
- 음식점 상세 정보에서 추가할 수 있다.


## UI

- [x] 음식점 목록을 확인할 수 있다.
- 카테고리별로 필터링해서 확인할 수 있다.
- 이름순/거리순으로 정렬해서 확인할 수 있다.
- 추가된 음식점은 localStorage에 저장해 새로고침해도 추가한 음식점 정보들이 유지된다.

- [x] 음식점 추가 form

- 카테고리, 거리는 셀렉트 박스, 이름/설명/참고 링크는 텍스트 인풋을 사용한다.
- 유효하지 않은 링크가 입력되면 '유효하지 않은 링크입니다.'를 노출한다.

## Bug🐞
- [ ] 유효하지 않은 링크를 입력한 뒤 유효한 링크를 입력했을 때 정상제출 되지 않는 버그 해결
- [ ] 음식점의 상세 정보를 확인할 수 있다.
- 카테고리, 이름, 거리, 설명, 참고 링크를 확인할 수 있다.

## TEST

- [ ] 개발한 앱에서 핵심이 되는 기능이라고 생각하는 기능 플로우를 선정하고 그에 대한 E2E 테스트를 추가한다.

## 개선할 점

- [ ] connectedCallback에서 추가한 이벤트, disconnectedCallback에서 제거하기
- [ ] 입력 폼에 대한 각종 validator 추가하기
- [ ] 전체적인 코드 구조 수정하기

- [ ] 유효하지 않은 링크를 입력한 뒤 유효한 링크를 입력했을 때 정상제출 되지 않는 버그 해결
- [ ] 음식점 아이템 추가 후 다시 폼 진입 시 scroll이 최상단으로 가지 않는 문제 해결
63 changes: 57 additions & 6 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
Expand Down Expand Up @@ -95,7 +95,6 @@ <h1 class="gnb__title text-title">점심 뭐 먹지</h1>
position: fixed;
bottom: 0;
width: 100%;
height: 90%;

padding: 32px 16px;
box-sizing: border-box;
Expand All @@ -106,7 +105,7 @@ <h1 class="gnb__title text-title">점심 뭐 먹지</h1>
</style>
<div class="modal">
<div class="modal-backdrop"></div>
<div class="modal-container">
<div class="modal-container" id="modal-container">
<slot name="title"></slot>
<slot name="body"></slot>
</div>
Expand All @@ -118,15 +117,62 @@ <h1 class="gnb__title text-title">점심 뭐 먹지</h1>
<img src="" alt="" class="category-icon" />
</div>
<div class="restaurant__info">
<h3 class="restaurant__name text-subtitle"></h3>
<span class="restaurant__distance text-body"></span>
<div class="restaurant__header">
<div>
<h3 class="restaurant__name text-subtitle"></h3>
<span class="restaurant__distance text-body"></span>
</div>
<button type="button" class="restaurant__favorite-button" aria-label="즐겨찾기">
<img src="" alt="즐겨찾기" />
</button>
</div>
<p class="restaurant__description text-body"></p>
</div>
</template>

<template id="template-restaurant-detail">
<div class="detail-container">
<!-- 레스토랑 정보 -->
<div class="restaurant-detail__header">
<div class="restaurant-detail__category">
<img src="" alt="" class="category-icon" />
</div>
<button type="button" class="restaurant-detail__favorite-button" aria-label="즐겨찾기">
<img src="./favorite-icon-lined.png" alt="즐겨찾기" />
</button>
</div>

<div class="restaurant-detail__body">
<h1 class="restaurant-detail__name text-title"></h1>
<span class="restaurant-detail__distance text-subtitle"></span>
<p class="restaurant-detail__description text-body"></p>
<a class="restaurant-detail__link text-body"></a>
</div>

<!-- 삭제 / 닫기 버튼 -->
<div class="button-container">
<button type="button" class="button button--secondary text-caption">삭제하기</button>
<button type="button" class="button button--primary text-caption" id="close">닫기</button>
</div>
</div>
</template>

<!-- GNB -->
<app-gnb></app-gnb>

<!-- 메뉴 탭 바 -->
<section>
<ul class="tab text-title">
<li class="tab__item active">
<a href="#tab1">모든 음식점</a>
</li>
<li class="tab__item">
<a href="#tab2">자주 가는 음식점</a>
</li>
</ul>
</section>

<!-- 정렬 -->
<section class="restaurant-filter-container">
<select
is="app-select"
Expand All @@ -152,9 +198,14 @@ <h3 class="restaurant__name text-subtitle"></h3>
</section>

<!-- 음식점 추가 모달 -->
<app-modal>
<app-modal id="addModal" height="90%">
<h2 slot="title" class="modal-title text-title">새로운 음식점</h2>
<form is="app-restaurant-form" slot="body" id="restaurant-form"></form>
</app-modal>

<!-- 음식점 상세 정보 모달 -->
<app-modal id="detailModal" height="75%">
<app-restaurant-detail slot="body" id="restaurant-detail"></app-restaurant-detail>
</app-modal>
</body>
</html>
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"@babel/preset-typescript": "^7.18.6",
"babel-jest": "^29.7.0",
"css-loader": "^6.7.3",
"cypress": "^13.6.6",
"cypress": "^13.7.0",
"eslint": "^8.34.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-base": "^15.0.0",
Expand Down
20 changes: 20 additions & 0 deletions src/domain/RestaurantManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface Restaurant {
distance: 5 | 10 | 15 | 20 | 30;
description?: string;
link?: string;
favorite?: boolean;
}

class RestaurantManager {
Expand Down Expand Up @@ -46,6 +47,25 @@ class RestaurantManager {
#sortByDistance(restaurants: Restaurant[]): Restaurant[] {
return [...restaurants].sort((a, b) => a.distance - b.distance);
}

toggleFavorite(restaurantName: string): void {
const restaurantIndex = this.#restaurants.findIndex((restaurant) => restaurant.name === restaurantName);
if (restaurantIndex !== -1) {
this.#restaurants[restaurantIndex].favorite = !this.#restaurants[restaurantIndex].favorite;
this.updateLocalStorage();
}
}

updateLocalStorage(): void {
localStorage.setItem('restaurants', JSON.stringify(this.#restaurants));
}

loadRestaurantsFromLocalStorage() {
const storedRestaurants = localStorage.getItem('restaurants');
if (storedRestaurants) {
this.#restaurants = JSON.parse(storedRestaurants);
}
}
}

export default RestaurantManager;
9 changes: 9 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Select from './view/components/Select';
import RestaurantForm from './view/components/RestaurantForm';
import RestaurantList from './view/components/RestaurantList';
import RestaurantItem from './view/components/RestaurantItem';
import RestaurantDetail from './view/components/RestaurantDetail';

// styles
import './view/styles/global.css';
Expand All @@ -14,6 +15,9 @@ import './view/styles/Modal.css';
import './view/styles/Select.css';
import './view/styles/RestaurantForm.css';
import './view/styles/RestaurantList.css';
import './view/styles/Tab.css';
import './view/styles/RestaurantDetail.css';


// imgs
import './view/imgs/add-button.png';
Expand All @@ -23,13 +27,18 @@ import './view/imgs/category-chinese.png';
import './view/imgs/category-japanese.png';
import './view/imgs/category-western.png';
import './view/imgs/category-etc.png';
import './view/imgs/favorite-icon-filled.png';
import './view/imgs/favorite-icon-lined.png';


window.customElements.define('app-gnb', GNB);
window.customElements.define('app-modal', Modal);
window.customElements.define('app-select', Select, { extends: 'select' });
window.customElements.define('app-restaurant-form', RestaurantForm, { extends: 'form' });
window.customElements.define('app-restaurant-item', RestaurantItem, { extends: 'li' });
window.customElements.define('app-restaurant-list', RestaurantList, { extends: 'ul' });
window.customElements.define('app-restaurant-detail', RestaurantDetail);


const controller = new WebController();
controller.start();
Loading

0 comments on commit 483cf1a

Please sign in to comment.