Skip to content

Commit feb71ae

Browse files
authored
[2단계 - 나만의 유튜브 강의실] 유세지(김용래) 미션 제출합니다. (#140)
* chore: eslint 설정, CRLF -> LF 변경 * docs: todo list 작성 * refactor: 상수를 이용하여 mockdata 사용 여부 결정 - constants.js 의 DEVELOP_MODE 에 boolean 값을 주어 mockdata 사용 여부 조정 가능 * test: 처음 실행하면 저장된 영상이 없다는 안내 문구가 나타난다 * feat: 처음 실행하면 저장된 영상이 없다는 안내 문구가 나타나는 기능 구현 - localStorage를 확인하여 비어있는 경우에만 안내 문구 표시 * test: 초기 화면에서 저장된 영상을 확인할 수 있어야 한다. * feat: 초기 화면에서 저장된 영상을 확인하는 기능 구현 * test: 초기 화면에서 저장된 영상의 ✅ 버튼을 눌러 본 영상 섹션으로 옮길 수 있어야 한다. * feat: 초기 화면에서 저장된 영상의 ✅ 버튼을 눌러 본 영상 섹션으로 옮기는 기능 구현 * test: 초기 화면에서 본 영상 버튼을 눌러 본 영상 목록을 확인할 수 있어야 한다. * feat: 초기 화면에서 본 영상 / 볼 영상 버튼으로 각각의 목록을 불러오는 기능 구현 * chore: es-lint 설정 변경 - class-methods-use-this: 0 추가 * test: 본 영상 섹션에서 저장된 영상의 ✅ 버튼을 눌러 볼 영상 섹션으로 옮길 수 있어야 한다. - 짝 테스트 설명도 함께 변경 : "초기 화면에서" -> "볼 영상 섹션에서" * feat: ✅ 버튼으로 영상의 시청 상태 변경 기능 구현 * test: 초기 화면에서 저장된 영상의 🗑️ 버튼을 눌러 영상을 삭제할 수 있어야 한다. * feat: 초기 화면에서 저장된 영상의 🗑️ 버튼을 눌러 영상을 삭제하는 기능 구현 * fix: no result 페이지가 중복으로 표시되던 오류 수정 * fix: 비디오 상태에 따라 결과 없음 화면이 출력되지 않던 오류 수정 * test: 초기 화면에서 삭제시 사용자에게 물어보는지 확인한다. * feat: 영상을 삭제하기 전 사용자에게 물어보는 기능 구현 * chore: es-lint no-alert 확인 제거 * feat: 반응형 웹 구현 * chore: 주석, 콘솔로그 제거 * feat: 커스텀 에러 핸들러 추가 - fetch error 핸들링 추가 * feat: getSearchResult 함수가 실행중일때는 중복 실행되지 않도록 설정 * feat: 검색된 영상 클릭 시 유튜브로 바로가기 기능 추가 * test: 테스트 함수 추상화 * chore: 콘솔로그 제거 * fix: searchWithNoKeyword test에서 assert가 나오지 않는 코드 수정 * refactor: templates.js 코드 각 view 파일로 일원화 * refactor: searchModal의 no-result 페이지 표시 방식을 hide 클래스 추가/제거로 개선 * refactor: handleError 메서드를 api 내부로 이동 * refactor: VideoStorage의 getStorage()를 getVideos() 으로 변경 * fix: 사용되지 않던 메서드 isSavedVideoId 삭제 * chore: 미사용 테스트 삭제 * fix: 불필요한 재검색 로직 제거 * refactor: innerHTML 메서드의 대체로 깜빡임 현상 개선
1 parent fd9ea0d commit feb71ae

19 files changed

Lines changed: 633 additions & 247 deletions

.eslintrc.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,20 @@
1313
"sourceType": "module"
1414
},
1515
"rules": {
16+
"no-new": 0,
1617
"no-var": "error",
1718
"max-depth": ["error", 2],
19+
"no-alert": 0,
20+
"default-case": 0,
1821
"no-console": "warn",
1922
"no-param-reassign": "error",
2023
"no-undefined": 0,
2124
"no-constant-condition": 0,
2225
"no-unused-private-class-members": 0,
26+
"class-methods-use-this": 0,
2327
"lines-between-class-members": 0,
24-
"no-useless-escape": 0
28+
"no-useless-escape": 0,
29+
"import/no-unresolved": 0,
30+
"import/extensions": 0
2531
}
2632
}

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
</p>
1313

1414
### 데모 페이지
15+
1516
[데모 페이지](https://usageness.github.io/javascript-youtube-classroom/)
1617

1718
---
@@ -20,6 +21,8 @@
2021

2122
---
2223

24+
### Step 1
25+
2326
- [x] 메인 화면에서 검색 버튼을 누르면 검색 모달창이 나타난다.
2427
- [x] 유튜브 검색 API를 사용해 내가 보고 싶은 영상들을 검색할 수 있다.
2528
- 엔터키를 눌러 검색할 수 있다.
@@ -34,19 +37,33 @@
3437
- 저장 가능한 최대 동영상의 갯수는 100개이다.
3538
- [x] 이미 저장된 영상이라면 저장 버튼이 보이지 않도록 한다.
3639

40+
### Step 2
41+
42+
- [x] 가장 처음에는 저장된 영상이 없으므로, 비어있다는 것을 사용자에게 알려주는 상태를 보여준다.
43+
- [x] 이후 페이지를 방문했을 때 기본 메인 화면은 내가 볼 영상들의 리스트를 보여준다.
44+
- [x] 영상 카드의 이모지 버튼을 클릭하여 아래와 같은 상태 변경이 가능해야 한다.
45+
- ✅ 본 영상으로 체크
46+
- 🗑️ 버튼으로 저장된 리스트에서 삭제할 수 있다. (삭제 시 사용자에게 정말 삭제할 것인지 물어봅니다.)
47+
- [x] 본 영상, 볼 영상 버튼을 눌러 필터링 할 수 있다.
48+
- [x] 반응형 웹: 유저가 사용하는 디바이스의 가로 길이에 따라 검색결과의 row 당 column 갯수를 변경한다.
49+
- 1280px 이상: 4개
50+
- 960px이상~1280px 미만: 3개
51+
- 600px이상~960px 미만: 2개
52+
- 600px 미만: 1개
53+
3754
## 테스트 요구사항
3855

3956
---
4057

4158
- [x] 단위 테스트를 Jest로 작성한다.
4259
- [x] E2E 테스트를 Cypress로 작성한다.
60+
- 검증이 필요하다고 생각되는 플로우를 1가지 설정하고 이에 대한 검증을 E2E 테스트로 진행한다.
4361

4462
## 배포
4563

4664
---
4765

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

5168
## 📝 License
5269

cypress/integration/app.test.js

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,26 @@
11
import { ITEMS_PER_REQUEST } from "../../src/js/constants/constants";
22

3-
describe("보고싶은 영상 찾기 모달창 전체 로직 테스트", () => {
3+
describe("나만의 유튜브 강의실 전체 플로우 테스트", () => {
44
before(() => {
55
cy.visit("./index.html");
66
});
77

88
const searchKeyword = "xooos";
9-
const errorSearchKeyword = `\!\@\!\@\$\!\%\@\$\^\%\&\$\^\*\%\!\@\!\$\!\%\&\(\^\*\%\$\!\@!@$$!#@!#)_)&_%^_)&%_^)&_@!@#!#$@#$%$@#^%&$%^&#$@$^#%&$%^$^%*$^&^@#$@#$@#%@#$^#%&^**#^#$%@#$@#$^@#$!$@#%@#$%#$^#$%^$%@#$!@#!@#%)^_&)%_^$%#$%#$^#%^#%^#^&_%^_)&_#$)%_)#_$)%#_$%!@#!@$#$!#@!#)_)&_%^_)&%_^)&_%)^_&)%_^&_%^_)&_#$)%_)#_$)%#_$\%`;
9+
10+
it("처음 실행하면 저장된 영상이 없다는 안내 문구가 나타난다.", () => {
11+
cy.get(".no-result__description").should("be.visible");
12+
});
1013

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

16-
/**
17-
* youtube에 검색한 결과가 없는 경우를 찾기 어려움... (테스트 통과가 되지 않을 확률이 높다.)
18-
*/
19-
// it("보고싶은 영상 찾기 모달창 안에서 원하는 영상을 검색한 결과가 없는 경우 검색 결과 없음 이미지를 보여준다.", () => {
20-
// cy.get("#search-input-keyword").type(errorSearchKeyword);
21-
// cy.get("#search-button").click();
22-
// cy.get(".search-result--no-result").should("be.visible");
23-
// });
24-
25-
/**
26-
* 실제 API 호출 했을 경우 주석을 제거 후 테스트를 돌려주세요.
27-
*/
28-
// it("보고싶은 영상 찾기 모달창 안에서 검색된 영상을 불러오는 동안 로딩 이미지를 보여준다.", () => {
29-
// cy.get("#search-input-keyword").clear().type(searchKeyword);
30-
// cy.get("#search-button").click();
31-
// cy.get(".skeleton").should("be.visible");
32-
// });
19+
it("보고싶은 영상 찾기 모달창 안에서 검색된 영상을 불러오는 동안 로딩 이미지를 보여준다.", () => {
20+
cy.get("#search-input-keyword").clear().type(searchKeyword);
21+
cy.get("#search-button").click();
22+
cy.get(".skeleton").should("be.visible");
23+
});
3324

3425
it("보고싶은 영상 찾기 모달창 안에서 검색창에 검색어를 입력하지 않으면 에러 메시지를 보여준다.", () => {
3526
cy.searchWithNoKeyword();
@@ -53,12 +44,41 @@ describe("보고싶은 영상 찾기 모달창 전체 로직 테스트", () => {
5344
});
5445

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

6050
it("보고싶은 영상 찾기 모달창이 열린 상태에서 모달창 뒤의 음영 된 부분을 누르면 모달창이 사라진다.", () => {
6151
cy.closeSearchModal();
6252
cy.get(".modal-container").should("be.not.visible");
6353
});
54+
55+
it("초기 화면에서 저장된 영상을 확인할 수 있어야 한다.", () => {
56+
cy.get(".video-item").should("be.visible");
57+
});
58+
59+
it("볼 영상 섹션에서 저장된 영상의 ✅ 버튼을 눌러 본 영상 섹션으로 옮길 수 있어야 한다.", () => {
60+
cy.clickVideoWatchButton();
61+
cy.get(".video-item").should("be.not.visible");
62+
});
63+
64+
it("초기 화면에서 본 영상 버튼을 눌러 본 영상 목록을 확인할 수 있어야 한다.", () => {
65+
cy.clickWatchedVideoListTab();
66+
cy.get(".video-item").should("be.visible");
67+
});
68+
69+
it("본 영상 섹션에서 저장된 영상의 ✅ 버튼을 눌러 볼 영상 섹션으로 옮길 수 있어야 한다.", () => {
70+
cy.clickVideoWatchButton();
71+
cy.clickWatchLaterVideoListTab();
72+
cy.get(".video-item").should("be.visible");
73+
});
74+
75+
it("초기 화면에서 저장된 영상의 🗑️ 버튼을 눌러 영상을 삭제할지 확인할 수 있어야 한다.", () => {
76+
cy.clickVideoDeleteButton(false);
77+
cy.get(".video-item").should("be.visible");
78+
});
79+
80+
it("영상을 삭제할지 확인하는 화면에서 확인을 눌러 삭제할 수 있어야 한다.", () => {
81+
cy.clickVideoDeleteButton(true);
82+
cy.get(".video-item").should("be.not.visible");
83+
});
6484
});

cypress/support/commands.js

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ Cypress.Commands.add("searchWithNoKeyword", () => {
99

1010
cy.on("window:alert", alertStub);
1111
cy.get("#search-input-keyword").clear().type(" ");
12-
cy.get("#search-button").click(() => {
13-
expect(alertStub).to.be.calledWith(ERROR_MESSAGE.SEARCH_INPUT_IS_EMPTY);
14-
});
12+
cy.get("#search-button")
13+
.click()
14+
.then(() => {
15+
expect(alertStub).to.be.calledWith(ERROR_MESSAGE.SEARCH_INPUT_IS_EMPTY);
16+
});
1517
});
1618

1719
Cypress.Commands.add("searchWithKeyword", (keyword) => {
@@ -30,3 +32,25 @@ Cypress.Commands.add("loadMoreVideos", () => {
3032
Cypress.Commands.add("closeSearchModal", () => {
3133
cy.get(".dimmer").click({ force: true });
3234
});
35+
36+
Cypress.Commands.add("clickWatchedVideoListTab", () => {
37+
cy.get("#watched-video-button").click();
38+
});
39+
40+
Cypress.Commands.add("clickWatchLaterVideoListTab", () => {
41+
cy.get("#watch-later-video-button").click();
42+
});
43+
44+
Cypress.Commands.add("clickVideoWatchButton", () => {
45+
cy.get(".video-item__watched-button").eq(0).click();
46+
});
47+
48+
Cypress.Commands.add("clickVideoDeleteButton", (confirmButtonClick) => {
49+
cy.get(".video-item__delete-button").eq(0).click();
50+
51+
cy.on("window:confirm", (text) => {
52+
console.log(text);
53+
expect(text).to.contains("정말로 삭제하시겠습니까?");
54+
return confirmButtonClick;
55+
});
56+
});

index.html

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,34 @@
99
<main id="app" class="classroom-container">
1010
<h1 class="classroom-container__title">👩🏻‍💻 나만의 유튜브 강의실 👨🏻‍💻</h1>
1111
<nav class="nav">
12+
<button
13+
id="watch-later-video-button"
14+
class="button nav__button saved-video__button selected"
15+
>
16+
👁️ 볼 영상
17+
</button>
18+
<button
19+
id="watched-video-button"
20+
class="button nav__button saved-video__button"
21+
>
22+
✅ 본 영상
23+
</button>
1224
<button id="search-modal-button" class="button nav__button">
1325
🔍 검색
1426
</button>
1527
</nav>
28+
<section class="saved-video__section">
29+
<h3 hidden>저장된 영상</h3>
30+
<ul class="saved-video-list"></ul>
31+
<div class="no-result hide">
32+
<img src="" alt="no result image" class="no-result__image" />
33+
<p class="no-result__description">
34+
저장된 영상이 없습니다<br />나만의 영상을 검색하여 저장해보세요
35+
</p>
36+
</div>
37+
</section>
1638
</main>
17-
<!-- 1 검색 버튼을 누르면 아래와 같은 검색 모달을 보여줍니다.-->
39+
1840
<div class="modal-container hide">
1941
<div class="dimmer"></div>
2042
<div
@@ -45,6 +67,17 @@ <h3 hidden>검색어 입력</h3>
4567
<section class="search-result">
4668
<h3 hidden>검색 결과</h3>
4769
<ul class="video-list"></ul>
70+
<div class="no-result hide">
71+
<img
72+
src=""
73+
id="search-modal-no-result__image"
74+
alt="no result image"
75+
class="no-result__image"
76+
/>
77+
<p class="no-result__description">
78+
검색 결과가 없습니다<br />다른 키워드로 검색해보세요
79+
</p>
80+
</div>
4881
</section>
4982
</div>
5083
</div>

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@
1515
},
1616
"repository": {
1717
"type": "git",
18-
"url": "git+https://github.com/kkojae91/javascript-youtube-classroom.git"
18+
"url": "git+https://github.com/usageness/javascript-youtube-classroom.git"
1919
},
2020
"keywords": [],
2121
"author": "",
2222
"license": "ISC",
2323
"bugs": {
24-
"url": "https://github.com/kkojae91/javascript-youtube-classroom/issues"
24+
"url": "https://github.com/usageness/javascript-youtube-classroom/issues"
2525
},
26-
"homepage": "https://github.com/kkojae91/javascript-youtube-classroom#readme",
26+
"homepage": "https://usageness.github.io/javascript-youtube-classroom/",
2727
"devDependencies": {
2828
"@babel/core": "^7.17.5",
2929
"@babel/plugin-transform-runtime": "^7.17.0",

src/css/app.css

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,34 +11,56 @@ body {
1111
.classroom-container__title {
1212
text-align: center;
1313
font-weight: bold;
14-
font-size: 34px;
1514
line-height: 36px;
1615
margin-bottom: 64px;
1716
}
1817

1918
.nav {
2019
display: flex;
2120
justify-content: center;
21+
margin-bottom: 40px;
2222
}
2323

24-
.button {
25-
cursor: pointer;
26-
border-radius: 4px;
27-
border: none;
28-
font-style: normal;
29-
font-weight: bold;
30-
font-size: 14px;
31-
letter-spacing: 1.25px;
24+
.nav__button:hover {
25+
background: #ebebeb;
3226
}
3327

34-
.nav__button {
35-
width: 80px;
36-
height: 36px;
37-
background: #f5f5f5;
28+
#search-modal-button {
29+
margin-left: auto;
3830
}
3931

40-
.nav__button:hover {
41-
background: #ebebeb;
32+
.saved-video__button {
33+
border: 1px solid #cccccc;
34+
}
35+
36+
.saved-video__button:hover {
37+
background: rgba(0, 188, 212, 0.12);
38+
}
39+
40+
.selected {
41+
background: rgba(0, 188, 212, 0.12);
42+
}
43+
44+
.saved-video-list {
45+
width: 105%;
46+
display: flex;
47+
flex-direction: row;
48+
flex-wrap: wrap;
49+
gap: 32px 20px;
50+
overflow-y: hidden;
51+
}
52+
53+
.video-button__wrapper {
54+
text-align: right;
55+
}
56+
57+
.no-result {
58+
flex-grow: 1;
59+
60+
display: flex;
61+
flex-direction: column;
62+
justify-content: center;
63+
align-items: center;
4264
}
4365

4466
/* ******** */

0 commit comments

Comments
 (0)