Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
60916ee
chore: 기본 환경 설정 셋팅
kkojae91 Mar 8, 2022
df20d80
docs: 구현할 기능 목록 작성
kkojae91 Mar 8, 2022
048f29c
test: UserLibrary에 영상이 저장되어야 한다.
kkojae91 Mar 8, 2022
4f5d36e
feat: UserLibrary 클래스 구현
kkojae91 Mar 8, 2022
f30a718
test: UserLibrary에 101개 이상의 동영상이 저장되면 alert을 띄운다
kkojae91 Mar 8, 2022
d791d0c
feat: UserLibrary 101개 이상의 동영상이 저장되면 alert 띄우는 기능 구현
kkojae91 Mar 8, 2022
8238d7b
chore: 바벨 폴리필 설정
kkojae91 Mar 8, 2022
378aae2
test: 동영상의 Id값이 같은지 확인한다.
kkojae91 Mar 8, 2022
1bf55ff
feat: 중복 요소를 판별하는 isDuplicate 함수 구현
kkojae91 Mar 8, 2022
7d72ce7
chore: 파일 이름 변경
kkojae91 Mar 8, 2022
2ab2d6b
test: 메인 화면에서 검색 버튼을 누르면 검색 모달창이 나타난다.
kkojae91 Mar 8, 2022
c56ca53
feat: 메인 화면에서 검색 버튼을 누르면 검색 모달창이 보이는 기능 구현
kkojae91 Mar 8, 2022
412033e
test: 데이터를 불러오는 동안 skeleton UI를 보여준다
kkojae91 Mar 9, 2022
17d5427
feat: skeleton ui 마크업 작성
kkojae91 Mar 9, 2022
1f78151
fix: 테스트 코드 수정, CSS 수정, template 분리
kkojae91 Mar 9, 2022
7ce79a4
feat: 검색 결과가 없는 경우 결과 없음 이미지를 보여주는 기능 구현
kkojae91 Mar 9, 2022
bdad918
chore: 웹팩 설정, CSS 수정
kkojae91 Mar 9, 2022
189068d
test: 내가 보고 싶은 영상들을 검색하는 테스트 작성
kkojae91 Mar 9, 2022
9a148bb
feat: 내가 보고 싶은 영상들을 검색할 수 있는 기능 추가
kkojae91 Mar 9, 2022
2241d3a
test: 브라우저 스크롤 바를 끝까지 내려 그 다음 10개 아이템을 추가로 불러오는지 확인하는 테스트 케이스 작성
kkojae91 Mar 9, 2022
b73f012
feat: 브라우저 스크롤 바를 끝까지 내려 그 다음 10개 아이템을 추가로 불러오는 기능 구현
kkojae91 Mar 9, 2022
b1154d2
test: 내가 검색한 영상들 중 저장 버튼을 누르면 저장 버튼이 사라지는지 확인하는 테스트 작성
kkojae91 Mar 9, 2022
f4f2774
chore: jest localstorage mock 설치
kkojae91 Mar 9, 2022
f3d943d
test: 테스트케이스 수정
kkojae91 Mar 9, 2022
b8351d9
feat: 저장 기능 구현
kkojae91 Mar 9, 2022
6a3f1b3
chore: jest-fetch-mock 설치, 테스트 코드 실험
kkojae91 Mar 9, 2022
c134734
test: 응답받은 날짜 데이터를 정해진 형식으로 변경하는 테스트 케이스 작성
kkojae91 Mar 10, 2022
6a7ecb6
feat: 응답받은 날짜 데이터를 정해진 형식으로 변경하는 기능 구현
kkojae91 Mar 10, 2022
af8ded0
feat: client-side에서 API key 숨기는 기능 추가
kkojae91 Mar 10, 2022
8ca4f80
fix: skeleton loading UI 버그 수정
kkojae91 Mar 10, 2022
909570d
test: 이미 저장한 비디오 아이디인지 확인하는 테스트 케이스 작성
kkojae91 Mar 10, 2022
7818b16
feat: 저장된 비디오 아이디인지 확인하는 기능 구현
kkojae91 Mar 10, 2022
6a3126c
test: 이미 저장된 영상이라면 저장 버튼이 보이지 않도록 한다.
kkojae91 Mar 10, 2022
9bc2a23
test: 이미 저장된 영상이라면 저장 버튼이 보이지 않도록 한다. 테스트 수정
kkojae91 Mar 10, 2022
addde22
feat: 저장된 영상은 버튼이 보이지 않도록 하는 기능 구현
kkojae91 Mar 10, 2022
ec11a12
test: 모달창이 열린 상태에서 모달창 뒤의 음영된 부분을 누르면 모달창이 사라지는 테스트 케이스 작성
kkojae91 Mar 10, 2022
0a8dcf0
feat: 모달창이 열린 상태에서 모달창 뒤의 음영된 부분을 누르면 모달창이 사라지는 기능 구현
kkojae91 Mar 10, 2022
a4423aa
fix: 재검색시 발생하는 이슈 해결
kkojae91 Mar 10, 2022
f43eb80
refactor: 테스트 케이스 중복 코드 제거 및 분리, 에러 메시지 상수화
kkojae91 Mar 10, 2022
4941993
refactor: YoutubeApp 함수 분리, 상수처리
kkojae91 Mar 10, 2022
5264937
refactor: 파일명 수정, 메서드명 수정, 매직넘버 상수화
kkojae91 Mar 10, 2022
bcf46d0
refactor: 테스트 코드 파일명 변경 및 분리
kkojae91 Mar 10, 2022
6258c0b
chore: 주석 추가, gh-pages 설치
kkojae91 Mar 10, 2022
1d9ed1f
fix: 모달창을 다시 열었을 경우 버그 수정
kkojae91 Mar 11, 2022
abec8a4
feat: userStorage에 데이터가 100개를 넘을 경우 에러 메시지를 보여주는 기능 추가
kkojae91 Mar 11, 2022
7e4a3cf
test: 검색하려는 입력값이 유효한 값인지 검증한다.
kkojae91 Mar 11, 2022
22049bb
feat: 검색하려는 입력값이 유효한 값인지 검증하는 함수 구현.
kkojae91 Mar 11, 2022
7415521
test: 보고싶은 영상 찾기 모달창 안에서 검색창에 검색어를 입력하지 않으면 에러 메시지를 보여주는 테스트 케이스 작성
kkojae91 Mar 11, 2022
8c1343d
feat: 보고싶은 영상 찾기 모달창 안에서 검색창에 검색어를 입력하지 않으면 에러 메시지를 보여주는 기능 구현
kkojae91 Mar 11, 2022
77a5892
chore: Redirect 주소 변경
usageness Mar 11, 2022
ace54d1
fix: 검색어가 빈 값일때 검색 로직이 진행되는 버그 수정
usageness Mar 11, 2022
026c53d
docs: README.md 수정
usageness Mar 11, 2022
2ac7d26
chore: line feed change
usageness Mar 12, 2022
92381a7
refactor: 외부로 노출되지 않는 메소드 private 화
usageness Mar 12, 2022
1c59287
chore: userStorage -> videoStorage 파일/메서드 이름 변경
usageness Mar 12, 2022
8382312
chore: 잘못된 test 케이스 설명 수정
usageness Mar 12, 2022
e668a6b
refactor: YoutubeApp의 역할을 나누어 가지는 view 생성
usageness Mar 12, 2022
567ddf8
refactor: 역할 분리에 따른 기존 메서드 삭제
usageness Mar 12, 2022
a74895c
chore: console.log 삭제
usageness Mar 12, 2022
a8b149b
chore: 에러메시지 프로퍼티 명 변경
usageness Mar 12, 2022
c2b123e
Merge branch 'usageness-step1' of https://github.com/usageness/javasc…
usageness Mar 12, 2022
6eef332
refactor: 테스트 코드 추상화
usageness Mar 13, 2022
456f524
chore: 매직넘버를 정의된 상수로 대체
usageness Mar 13, 2022
f0bec40
fix: skeleton이 정상적으로 표시되지 않던 버그 수정
usageness Mar 13, 2022
1ff298b
chore: insertImageSrc 분리 revert
usageness Mar 13, 2022
4345d09
chore: validateInput 메서드 이름 변경
usageness Mar 13, 2022
6f992a8
chore: mock 데이터 연결 해제, api 통신 방법을 사용
usageness Mar 13, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
src/js/__tests__/*
cypress/*
26 changes: 26 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"airbnb-base",
"plugin:prettier/recommended"
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {
"no-var": "error",
"max-depth": ["error", 2],
"no-console": "warn",
"no-param-reassign": "error",
"no-undefined": 0,
"no-constant-condition": 0,
"no-unused-private-class-members": 0,
"lines-between-class-members": 0,
"no-useless-escape": 0
}
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,6 @@ dist

# TernJS port file
.tern-port

# ide file
.vscode/
3 changes: 3 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"printWidth": 80
}
47 changes: 28 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,36 +11,45 @@
<img src="https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square"/>
</p>

## ⚙️ Before Started
### 데모 페이지
[데모 페이지](https://usageness.github.io/javascript-youtube-classroom/)

#### <img alt="Tip" src="https://img.shields.io/static/v1.svg?label=&message=Tip&style=flat-square&color=673ab8"> 로컬에서 서버 띄워서 손쉽게 static resources 변경 및 확인하는 방법
---

로컬에서 웹서버를 띄워 html, css, js 등을 실시간으로 손쉽게 테스트해 볼 수 있습니다. 이를 위해서는 우선 npm이 설치되어 있어야 합니다. 구글에 `npm install` 이란 키워드로 각자의 운영체제에 맞게끔 npm을 설치해주세요. 이후 아래의 명령어를 통해 실시간으로 웹페이지를 테스트해볼 수 있습니다.
## 구현할 기능 목록

```
npm install -g live-server
```
---

실행은 아래의 커맨드로 할 수 있습니다.
- [x] 메인 화면에서 검색 버튼을 누르면 검색 모달창이 나타난다.
- [x] 유튜브 검색 API를 사용해 내가 보고 싶은 영상들을 검색할 수 있다.
- 엔터키를 눌러 검색할 수 있다.
- 검색 버튼을 클릭해 검색할 수 있다.
- 디바운스를 사용하여 API호출을 최소화한다.
- [x] 데이터를 불러오는 동안 현재 데이터를 불러오는 중임을 skeleton UI로 보여준다.
- [x] 검색 결과가 없는 경우 결과 없음 이미지를 보여준다.
- [x] 최초 검색 결과는 10개까지만 보여준다.
- 브라우저 스크롤 바를 끝까지 내려 그 다음 10개 아이템을 추가로 불러온다.
- 스로틀를 사용하여 이벤트 콜백함수 호출을 최소화한다.
- [x] 내가 검색한 영상들의 JSON 데이터를 localStorage에 저장한다.
- 저장 가능한 최대 동영상의 갯수는 100개이다.
- [x] 이미 저장된 영상이라면 저장 버튼이 보이지 않도록 한다.

```
live-server 폴더명
```
## 테스트 요구사항

<br>
---

## 👏 Contributing
- [x] 단위 테스트를 Jest로 작성한다.
- [x] E2E 테스트를 Cypress로 작성한다.

만약 미션 수행 중에 개선사항이 보인다면, 언제든 자유롭게 PR을 보내주세요.
## 배포

<br>
---

## 🐞 Bug Report

버그를 발견한다면, [Issues](https://github.com/woowacourse/javascript-youtube-classroom/issues)에 등록해주세요.

<br>
- [x] 실행 가능한 페이지에 접근할 수 있도록 github page 기능을 이용하고, 해당 링크를 PR과 README에 작성한다.
- API key를 public repo에 올리지 않은 채로 데모 페이지를 배포하려면, 별도의 설정이 추가로 필요합니다.

## 📝 License

---

This project is [MIT](https://github.com/woowacourse/javascript-youtube-classroom/blob/main/LICENSE) licensed.
12 changes: 12 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "current",
},
},
],
],
};
5 changes: 5 additions & 0 deletions cypress.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"baseUrl": "http://localhost:9000",
"video": false,
"screenshotOnRunFailure": false
}
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"
}
64 changes: 64 additions & 0 deletions cypress/integration/app.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { ITEMS_PER_REQUEST } from "../../src/js/constants/constants";

describe("보고싶은 영상 찾기 모달창 전체 로직 테스트", () => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cypress도 custom command를 이용해 추상화를 해보는것도 좋겠네요 😄

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추상화를 진행하니 코드가 더 깔끔해보이네요! 적용했습니다 😄

before(() => {
cy.visit("./index.html");
});

const searchKeyword = "xooos";
const errorSearchKeyword = `\!\@\!\@\$\!\%\@\$\^\%\&\$\^\*\%\!\@\!\$\!\%\&\(\^\*\%\$\!\@!@$$!#@!#)_)&_%^_)&%_^)&_@!@#!#$@#$%$@#^%&$%^&#$@$^#%&$%^$^%*$^&^@#$@#$@#%@#$^#%&^**#^#$%@#$@#$^@#$!$@#%@#$%#$^#$%^$%@#$!@#!@#%)^_&)%_^$%#$%#$^#%^#%^#^&_%^_)&_#$)%_)#_$)%#_$%!@#!@$#$!#@!#)_)&_%^_)&%_^)&_%)^_&)%_^&_%^_)&_#$)%_)#_$)%#_$\%`;

it("초기 화면에서 검색 버튼을 누르면 보고싶은 영상 찾기 모달창이 나타난다.", () => {
cy.openSearchModal();
cy.get(".modal-container").should("be.visible");
});

/**
* youtube에 검색한 결과가 없는 경우를 찾기 어려움... (테스트 통과가 되지 않을 확률이 높다.)
*/
// it("보고싶은 영상 찾기 모달창 안에서 원하는 영상을 검색한 결과가 없는 경우 검색 결과 없음 이미지를 보여준다.", () => {
// cy.get("#search-input-keyword").type(errorSearchKeyword);
// cy.get("#search-button").click();
// cy.get(".search-result--no-result").should("be.visible");
// });

/**
* 실제 API 호출 했을 경우 주석을 제거 후 테스트를 돌려주세요.
*/
// it("보고싶은 영상 찾기 모달창 안에서 검색된 영상을 불러오는 동안 로딩 이미지를 보여준다.", () => {
// cy.get("#search-input-keyword").clear().type(searchKeyword);
// cy.get("#search-button").click();
// cy.get(".skeleton").should("be.visible");
// });

it("보고싶은 영상 찾기 모달창 안에서 검색창에 검색어를 입력하지 않으면 에러 메시지를 보여준다.", () => {
cy.searchWithNoKeyword();
});

it("보고싶은 영상 찾기 모달창 안에서 원하는 영상을 검색할 수 있다.", () => {
cy.searchWithKeyword(searchKeyword);
cy.get(".video-item").should("be.visible");
});

it("보고싶은 영상 찾기 모달창 안에서 검색된 영상의 저장 버튼을 누르면 저장 버튼은 보이지 않아야 한다.", () => {
cy.clickSaveVideoButton();
cy.get(".video-item__save-button").eq(0).should("be.not.visible");
});

it("보고싶은 영상 찾기 모달창 안에서 검색 결과 영역 스크롤 바를 끝까지 내리면 새로운 영상 10개를 추가로 사용자에게 보여준다.", () => {
cy.loadMoreVideos();
cy.get(".video-list")
.children()
.should("have.length", ITEMS_PER_REQUEST * 2);
});

it("보고싶은 영상 찾기 모달창 안에서 검색된 영상이 이미 저장된 영상이라면 저장 버튼이 보이지 않아야 한다.", () => {
cy.searchWithKeyword(searchKeyword);
cy.get(".video-item__save-button").eq(0).should("be.not.visible");
});

it("보고싶은 영상 찾기 모달창이 열린 상태에서 모달창 뒤의 음영 된 부분을 누르면 모달창이 사라진다.", () => {
cy.closeSearchModal();
cy.get(".modal-container").should("be.not.visible");
});
});
22 changes: 22 additions & 0 deletions cypress/plugins/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************

// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)

/**
* @type {Cypress.PluginConfig}
*/
// eslint-disable-next-line no-unused-vars
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}
32 changes: 32 additions & 0 deletions cypress/support/commands.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ERROR_MESSAGE } from "../../src/js/constants/constants";

Cypress.Commands.add("openSearchModal", () => {
cy.get("#search-modal-button").click();
});

Cypress.Commands.add("searchWithNoKeyword", () => {
const alertStub = cy.stub();

cy.on("window:alert", alertStub);
cy.get("#search-input-keyword").clear().type(" ");
cy.get("#search-button").click(() => {
expect(alertStub).to.be.calledWith(ERROR_MESSAGE.SEARCH_INPUT_IS_EMPTY);
});
});

Cypress.Commands.add("searchWithKeyword", (keyword) => {
cy.get("#search-input-keyword").clear().type(keyword);
cy.get("#search-button").click();
});

Cypress.Commands.add("clickSaveVideoButton", () => {
cy.get(".video-item__save-button").eq(0).click();
});

Cypress.Commands.add("loadMoreVideos", () => {
cy.get(".video-list").scrollTo("bottom");
});

Cypress.Commands.add("closeSearchModal", () => {
cy.get(".dimmer").click({ force: true });
});
1 change: 1 addition & 0 deletions cypress/support/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import "./commands";
103 changes: 48 additions & 55 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,59 +1,52 @@
<!DOCTYPE html>
<html lang="ko">

<head>
<meta charset="UTF-8" />
<title>나만의 유튜브 강의실</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="./src/css/index.css">
</head>

<body>
<main id="app" class="classroom-container">
<h1 class="classroom-container__title">👩🏻‍💻 나만의 유튜브 강의실 👨🏻‍💻</h1>
<nav class="nav">
<button id="search-modal-button" class="button nav__button">🔍 검색</button>
</nav>
</main>
<!-- 1 검색 버튼을 누르면 아래와 같은 검색 모달을 보여줍니다.-->
<div class="modal-container hide">
<div class="dimmer"></div>
<div class="search-modal" role="dialog" aria-labelledby="search-modal-title">
<h2 id="search-modal-title" class="search-modal__title">🔍 보고싶은 영상 찾기 🔍</h2>
<section class="search-input">
<h3 hidden>검색어 입력</h3>
<input id="search-input-keyword" type="text" placeholder="검색" class="search-input__keyword">
<button id="search-button" class="search-input__search-button button">검색</button>
</section>
<!-- 2-1 검색 결과가 있을 경우, 아래와 같은 검색 결과 목록을 보여줍니다.-->
<section class="search-result">
<h3 hidden>검색 결과</h3>
<ul class="video-list">
<!-- <li class="video-item" data-video-id="">
<img
src="https://i.ytimg.com/vi/ECfuKi5-Cfs/hq720.jpg?sqp=-oaymwEcCOgCEMoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLDvmIcX-TgdcH2g_Bd4AUxw6hjmvQ"
alt="video-item-thumbnail" class="video-item__thumbnail">
<h4 class="video-item__title">[Playlist] 너무 좋은데 괜찮으시겠어요?</h4>
<p class="video-item__channel-name">essential;</p>
<p class="video-item__published-date">2022년 3월 2일</p>
<button class="video-item__save-button button">⬇ 저장</button>
</li> -->
</ul>
</section>
<!-- 2-2 검색 결과가 없을 경우, `search-result--no-result` class를 붙이고 아래와 같은 no-result를 보여줍니다.-->
<!-- <section class="search-result search-result--no-result">
<h3 hidden>검색 결과</h3>
<div class="no-result">
<img src="./src/assets/images/not_found.png" alt="no result image" class="no-result__image">
<p class="no-result__description">
검색 결과가 없습니다<br />
다른 키워드로 검색해보세요
</p>
</div>
</section> -->
<head>
<meta charset="UTF-8" />
<title>나만의 유튜브 강의실</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<main id="app" class="classroom-container">
<h1 class="classroom-container__title">👩🏻‍💻 나만의 유튜브 강의실 👨🏻‍💻</h1>
<nav class="nav">
<button id="search-modal-button" class="button nav__button">
🔍 검색
</button>
</nav>
</main>
<!-- 1 검색 버튼을 누르면 아래와 같은 검색 모달을 보여줍니다.-->
<div class="modal-container hide">
<div class="dimmer"></div>
<div
class="search-modal"
role="dialog"
aria-labelledby="search-modal-title"
>
<h2 id="search-modal-title" class="search-modal__title">
🔍 보고싶은 영상 찾기 🔍
</h2>
<section class="search-input">
<h3 hidden>검색어 입력</h3>
<form id="search-form">
<input
id="search-input-keyword"
type="text"
placeholder="검색"
class="search-input__keyword"
/>
<button
id="search-button"
class="search-input__search-button button"
>
검색
</button>
</form>
</section>
<section class="search-result">
<h3 hidden>검색 결과</h3>
<ul class="video-list"></ul>
</section>
</div>
</div>
</div>
<script type="module" src="./src/js/index.js"></script>
</body>

</body>
</html>
Loading