Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

typescript-youtube-classroom #2

Open
wants to merge 43 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
8b7cb50
docs: README.md 테스트케이스 추가
jwon42 Jun 7, 2021
dcfad81
chore: tsconfig.json 설정
jwon42 Jun 7, 2021
9b08df8
chore: cypress 설정 적용
jwon42 Jun 7, 2021
679647e
chore: eslint, prettier 설정
jwon42 Jun 7, 2021
c98ff89
test: git push test
Jun 7, 2021
1b40896
test: three buttons in first page
Jun 7, 2021
ddd2372
test: step1 - 검색모달 중 저장버튼 삭제 테스트까지 적용
jwon42 Jun 7, 2021
7de69c1
test: step1 test
Jun 7, 2021
9388301
refactor: separate function in step1 test
Jun 7, 2021
3d84d34
feat: README.md 화면 구조 추가
jwon42 Jun 7, 2021
ec16114
feat: add render file
Jun 7, 2021
cdb82e0
feat: render to pagee
jwon42 Jun 7, 2021
a1e53f1
refactor: modalArticle change template
jwon42 Jun 8, 2021
aa22251
delete: test.txt
jwon42 Jun 8, 2021
ec6a47a
feat: add modal show/close in MainController
jwon42 Jun 8, 2021
2ef6547
feat: display youtube card after search
jwon42 Jun 8, 2021
77cf1d1
feat: add 'convert date format' & 'search input validation'
jwon42 Jun 8, 2021
d4d4a68
feat: add recent keyword
jwon42 Jun 8, 2021
ccd0989
refactor: separate render article, keyword
jwon42 Jun 8, 2021
4dbbbc4
feat: add not found result
jwon42 Jun 8, 2021
90f9d9a
refactor: remove comment in index.html
jwon42 Jun 8, 2021
3cf5d85
feat: add skeleton
jwon42 Jun 8, 2021
9391448
refactor: add IVideoInfo
jwon42 Jun 8, 2021
2f58630
feat: add 'modify modal-inner height'
jwon42 Jun 9, 2021
51bca06
feat: add new articles when scroll down to end
jwon42 Jun 9, 2021
d94c8ce
refactor: using interface IVideoInfo
jwon42 Jun 9, 2021
34963ef
feat: add 'video save feature'
jwon42 Jun 9, 2021
29aa986
feat: add 'show saved videos'
jwon42 Jun 9, 2021
058a0dd
feat: add 'recent keyword save to localstorage'
jwon42 Jun 10, 2021
4cd03c2
refactor: change class to function
jwon42 Jun 10, 2021
3306b28
refactor: video search page
Jun 14, 2021
a442395
refactor: check eslint prettier
jwon42 Jun 14, 2021
0da67dc
refactor: videoSearchPage
jwon42 Jun 14, 2021
62fba13
feat: watched video list & unwatched video list
Jun 14, 2021
2de937d
feat: watched video list & unwatched video list
Jun 14, 2021
cc06cb7
refactor: change function name
Jun 14, 2021
0edc8b8
refactor: change function name
Jun 14, 2021
b1c9934
feat: snackbar
Jun 15, 2021
ffc4216
refactor: return type
Jun 15, 2021
5cc22c7
refactor: video saved status
Jun 15, 2021
a442406
refactor: change file/dir name
jwon42 Jun 15, 2021
b4e1500
chore: separate api key
jwon42 Jun 15, 2021
15f3ddb
refactor: hide api key
jwon42 Jun 15, 2021
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
19 changes: 19 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"airbnb-base"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
}
}
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# dist
src/ts/controller/uriParameter.ts
dist/controller/uriParameter.js

# Logs
logs
*.log
Expand Down Expand Up @@ -80,7 +84,6 @@ typings/

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
Expand Down
5 changes: 5 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"parser": "typescript",
"singleQuote": true,
"printWidth": 120
}
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,60 @@ live-server 폴더명
## 📝 License

This project is [MIT](https://github.com/woowacourse/javascript-youtube-classroom/blob/main/LICENSE) licensed.

## Test case
Given : 시나리오 진행에 필요한 값을 설정합니다.
When : 시나리오를 진행하는데 필요한 조건을 명시합니다
Then : 시나리오를 완료했을 때 보장해야 하는 결과를 명시합니다.

### 초기 페이지
- [ ] 볼 영상, 본 영상, 동영상 검색 버튼이 존재

### step1 - 검색 모달
- [ ] '검색버튼'을 클릭하면, 검색 결과 최대 10개를 출력한다.
- [ ] '엔터키'를 입력하면, 검색 결과 최대 10개를 출력한다.
- [ ] 스켈레톤 UI 적용 여부 확인
- [ ] 검색 결과가 존재하지 않는 경우, 검색 결과 없음 이미지를 출력한다.
- [ ] 스크롤 바를 화면 최하단으로 이동하는 경우, 추가 검색을 진행 후 결과를 출력한다 (추가 된 결과의 갯수는 최대 10개이다)
- [ ] '저장 버튼'을 클릭하면, '저장 버튼'이 삭제됩니다.
- [ ] '저장 버튼'을 클릭하면 해당 영상 id가 웹 스토리지에 저장된다.
- [ ] 저장된 영상의 최대 갯수는 100개이다.
- [ ] 검색을 진행했을 때, 검색어가 최근 검색어에 포함된다.
- [ ] 검색 모달이 다시 로딩된 경우, 최근에 검색한 결과를 보여준다.
- [ ] 검색을 4번 진행했을 때, 가장 최근의 3개 검색어를 보여준다.

### step2 - 강의실 관리
- [ ] 초기 페이지를 띄웠을 때, '저장된 영상이 없습니다' 문구가 출력된다.
- [ ] 저장된 영상이 있는 경우, 볼 영상의 리스트에 출력된다.
- [ ] 저장된 영상은 상태 변경이 가능해야 한다.
- [ ] '본 영상' 버튼을 클릭하면, 본 영상 탭으로 이동한다.
- [ ] '삭제' 버튼을 클릭하면, 리스트에서 삭제한다.
- [ ] 사용자가 버튼 클릭하면, `snackbar` 형식으로 안내메세지를 출력한다.
- [ ] '본 영상' 버튼을 클릭하면, `snackbar` 메세지가 출력된다.
- [ ] '좋아요' 버튼을 클릭하면, `snackbar` 메세지가 출력된다.
- [ ] '삭제' 버튼을 클릭하면, `snackbar` 메세지가 출력된다.
- [ ] '볼 영상' 탭을 클릭하면, '본 영상'을 제외한 영상들이 필터링되어 출력된다.
- [ ] '본 영상' 탭을 클릭하면, '본 영상' 버튼을 클릭한 영상들이 필터링되어 출력된다.

### step3 - UX 증가를 위한 기능 구현
- [ ] '좋아요' 버튼을 눌렀을 때, 실제 유튜브 영상에도 '좋아요' 적용여부 확인 (테스트 케이스 적용 가능여부 검토 필요)
- [ ] 댓글을 입력했을 때, 실제 유튜브 영상의 댓글에도 입력된다(테스트 케이스 적용 가능여부 검토 필요)

- [ ] '좋아요' 버튼을 클릭하면, '좋아요 한 영상' 탭으로 이동한다.
- [ ] '다크모드' 버튼을 눌렀을 때, 배경 색상은 #000, 글자 색상은 #fff로 변경된다.
- [ ] 창 크기가 600px 일 때 row에 영상 2개가 출력된다.
- [ ] 창 크기가 500px 일 때 row에 영상 1개가 출력된다.
- [ ] Lazy loading 적용 여부(테스트 케이스 적용 가능여부 검토 필요)

## 화면 구조

- [ ] title section
- [ ] article section
- [ ] article

- [ ] modal
- [ ] modal title section
- [ ] modal search form section
- [ ] modal recent keyword section
- [ ] modal article section
- [ ] modal article
4 changes: 4 additions & 0 deletions cypress.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"viewportWidth": 500,
"viewportHeight": 300
}
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"
}
6 changes: 6 additions & 0 deletions cypress/integration/basic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
describe('유튜브 강의실 테스트', () => {
it ('샘플 테스트 - 구글 접속', () => {
cy.visit('https://google.com');
cy.get('[name="q"]').type('유튜브').type('{enter}');
});
})
96 changes: 96 additions & 0 deletions cypress/integration/step1_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
const searchByClickButton = (searchKeyword: string) => {
cy.get("#search-input").type(searchKeyword);
cy.get("#search-submit-button").click();
}

describe("step1 test", () => {
beforeEach(() => {
cy.visit("http://localhost:5500/");
});

// it("볼 영상, 본 영상, 동영상 검색 버튼이 존재", () => {
// cy.get("nav.d-flex").children().should("have.length", 3);
// cy.get("nav.d-flex button:nth-child(1)").should("have.text", "👁️ 볼 영상");
// cy.get("nav.d-flex button:nth-child(2)").should("have.text", "✅ 본 영상");
// cy.get("nav.d-flex button:nth-child(3)").should("have.text", "🔍 동영상 검색");
// });

// it("'검색버튼'을 클릭하면, 검색 결과 최대 10개를 출력한다.", () => {
// cy.get("#search-button").click();
// searchByClickButton("축구");
// cy.get("div.modal article").should("have.length", 1);
// }); // 입력 값, 길이 수정 필요

// it("'엔터키'를 입력하면, 검색 결과 최대 10개를 출력한다.", () => {
// cy.get("#search-button").click();
// cy.get("#search-input").type("축구").type("{enter}");
// cy.get("div.modal article").should("have.length", 1);
// }); // 입력 값, 길이 수정 필요

// it("스켈레톤 UI 적용 여부 확인", () => {
// }); // 수정 필요

// it("검색 결과가 존재하지 않는 경우, 검색 결과 없음 이미지를 출력한다.", () => {
// }); // 수정 필요

// it("스크롤 바를 화면 최하단으로 이동하는 경우, 추가 검색을 진행 후 결과를 출력한다 (추가 된 결과의 갯수는 최대 10개이다)", () => {
// cy.get("#search-button").click();
// cy.get("div.modal-inner").scrollTo('bottom');
// // 추가 검색 및 결과 출력 여부 확인 필요
// });

// it("'저장 버튼'을 클릭하면, '저장 버튼'이 삭제된다.", () => {
// cy.get("#search-button").click();
// searchByClickButton("축구");
// cy.get("div.modal article:nth-child(1) button").click();
// cy.get("div.modal article:nth-child(1)").children('button').should("not.exist");
// });

// it("'저장 버튼'을 클릭하면 해당 영상 id가 웹 스토리지에 저장된다.", () => {
// cy.get("#search-button").click();
// searchByClickButton("축구");
// cy.get("div.modal article:nth-child(1) button").click();
// //const temp = localStorage.getItem("data");
// //expect(temp).to.not.equal('temp');
// }); // 수정 필요

// it("저장된 영상의 최대 갯수는 100개이다.", () => {
// cy.get("#search-button").click();
// searchByClickButton("축구");
// for (let i = 1; i <= 101; i++) {
// if (i % 10 === 0) {
// cy.get("div.modal-inner").scrollTo("bottom");
// }
// cy.get(`div.modal article:nth-child(${i}) button`).click();
// }
// // const numOfSavedItem = localStorage.getItem('savedItemCount');
// // expect(numOfSavedItem).to.equal(100);
// }); // 수정 및 확인 필요

// it("검색을 진행했을 때, 검색어가 최근 검색어에 포함된다.", () => {
// cy.get("#search-button").click();
// searchByClickButton("축구");
// cy.get("div.modal section:nth-child(4) a:nth-child(2)").should("have.text", "축구");
// });

// it("검색 모달이 다시 로딩된 경우, 최근에 검색한 결과를 보여준다.", () => {
// cy.get("#search-button").click();
// searchByClickButton("축구");
// cy.get("path.close-x").click();
// cy.get("#search-button").click();
// cy.get("#search-input").should("have.value", "축구");
// });

// it("검색을 4번 진행했을 때, 가장 최근의 3개 검색어를 보여준다.", ()=>{
// cy.get("#search-button").click();
// searchByClickButton("축구");
// searchByClickButton("야구");
// searchByClickButton("농구");
// searchByClickButton("배구");
// cy.get("#search-submit-button").click();
// cy.get("div.modal section:nth-child(4)").children().should("have.length", 4);
// cy.get("div.modal section:nth-child(4) a:nth-child(2)").should("have.text", "배구");
// cy.get("div.modal section:nth-child(4) a:nth-child(3)").should("have.text", "농구");
// cy.get("div.modal section:nth-child(4) a:nth-child(4)").should("have.text", "야구");
// });
});
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
}
25 changes: 25 additions & 0 deletions cypress/support/commands.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// ***********************************************
// This example commands.js 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) => { ... })
20 changes: 20 additions & 0 deletions cypress/support/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// ***********************************************************
// This example support/index.js 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')
7 changes: 7 additions & 0 deletions dist/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { navigatorController } from './controller/navigator.js';
import { navigatorRenderer } from './view/navigator.js';
export const app = () => {
navigatorRenderer();
navigatorController();
};
app();
33 changes: 33 additions & 0 deletions dist/controller/navigator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { $ } from '../util.js';
import { videoSearchPageRenderer } from '../view/videoSearchPage.js';
import { videoSearchPageController } from './videoSearchPage.js';
import { savedVideoPageRenderer } from '../view/savedVideoPage.js';
import { savedVideoPageController } from './savedVideoPage.js';
import { watchedVideoPageRenderer } from '../view/watchedVideoPage.js';
import { watchedVideoPageController } from './watchedVideoPage.js';
const onModalShow = ($modal) => {
var _a;
$modal.classList.add('open');
(_a = $('#search-input')) === null || _a === void 0 ? void 0 : _a.focus();
};
const addClickEventToSavedVideoButton = () => {
$('#watched-video-button').classList.remove('bg-cyan-100');
$('#saved-video-button').classList.add('bg-cyan-100');
savedVideoPageRenderer();
savedVideoPageController();
};
const addClickEventToWatchedVideoButton = () => {
$('#saved-video-button').classList.remove('bg-cyan-100');
$('#watched-video-button').classList.add('bg-cyan-100');
watchedVideoPageRenderer();
watchedVideoPageController();
};
export const navigatorController = () => {
videoSearchPageRenderer();
videoSearchPageController();
const $modal = $('.modal');
$('#search-button').addEventListener('click', () => onModalShow($modal));
$('#saved-video-button').addEventListener('click', () => addClickEventToSavedVideoButton());
$('#watched-video-button').addEventListener('click', () => addClickEventToWatchedVideoButton());
$('#saved-video-button').click();
};
51 changes: 51 additions & 0 deletions dist/controller/savedVideoPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { $$ } from '../util.js';
import { unsaveVideo, changeWatchedState, getSavedVideos } from '../model/handleLocalStorage/articleManager.js';
import { removeArticle, renderEmptyVideoMessage } from '../view/savedVideoPage.js';
import { reRenderSavedButtonText, reRenderNumOfSavedVideos } from '../view/videoSearchPage.js';
const checkEmptyUnwatchedVideo = () => {
let unwatchedVideoList = getSavedVideos().filter((video) => video.isWatched === false);
if (unwatchedVideoList.length === 0) {
return true;
}
return false;
};
const addEventToWatchedButton = () => {
$$('span.video-watched-button').forEach((elem) => {
elem.addEventListener('click', () => {
const article = elem.closest('article');
changeWatchedState(article.dataset.videoId);
removeArticle(article);
if (checkEmptyUnwatchedVideo()) {
renderEmptyVideoMessage();
}
});
});
};
const addEventToRemoveButton = () => {
$$('span.video-remove-button').forEach((elem) => {
elem.addEventListener('click', () => {
const article = elem.closest('article');
if (confirm('정말 삭제하시겠습니까?')) {
unsaveVideo(article.dataset.videoId);
removeArticle(article);
changeButtonInSearchPage(article);
reRenderNumOfSavedVideos();
}
if (checkEmptyUnwatchedVideo()) {
renderEmptyVideoMessage();
}
});
});
};
const changeButtonInSearchPage = (article) => {
$$('#searched-article-wrapper button').forEach((elem) => {
if (elem.dataset.videoId === article.dataset.videoId) {
elem.dataset.saved = 'false';
reRenderSavedButtonText(elem);
}
});
};
export const savedVideoPageController = () => {
addEventToWatchedButton();
addEventToRemoveButton();
};
11 changes: 11 additions & 0 deletions dist/controller/snackbar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { snackbarRenderer } from '../view/snackbar.js';
export const showSnackbar = (text) => {
const $snackbar = document.createElement('div');
$snackbar.classList.add('snackbar');
$snackbar.textContent = text;
snackbarRenderer($snackbar);
$snackbar.classList.add('show');
setTimeout(() => {
$snackbar.remove();
}, 2900);
};
Loading