diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 000000000..3dbed94f6
--- /dev/null
+++ b/.eslintrc.json
@@ -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": {
+ }
+}
diff --git a/.gitignore b/.gitignore
index 67045665d..c16595953 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,7 @@
+# dist
+src/ts/controller/uriParameter.ts
+dist/controller/uriParameter.js
+
# Logs
logs
*.log
@@ -80,7 +84,6 @@ typings/
# Nuxt.js build / generate output
.nuxt
-dist
# Gatsby files
.cache/
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 000000000..ce79bfa69
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,5 @@
+{
+ "parser": "typescript",
+ "singleQuote": true,
+ "printWidth": 120
+ }
\ No newline at end of file
diff --git a/README.md b/README.md
index f526b6c44..f3a7eec45 100644
--- a/README.md
+++ b/README.md
@@ -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
\ No newline at end of file
diff --git a/cypress.json b/cypress.json
new file mode 100644
index 000000000..74ca975a4
--- /dev/null
+++ b/cypress.json
@@ -0,0 +1,4 @@
+{
+ "viewportWidth": 500,
+ "viewportHeight": 300
+}
\ No newline at end of file
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/integration/basic.ts b/cypress/integration/basic.ts
new file mode 100644
index 000000000..1b6d4f71c
--- /dev/null
+++ b/cypress/integration/basic.ts
@@ -0,0 +1,6 @@
+describe('μ νλΈ κ°μμ€ ν
μ€νΈ', () => {
+ it ('μν ν
μ€νΈ - κ΅¬κΈ μ μ', () => {
+ cy.visit('https://google.com');
+ cy.get('[name="q"]').type('μ νλΈ').type('{enter}');
+ });
+})
\ No newline at end of file
diff --git a/cypress/integration/step1_test.ts b/cypress/integration/step1_test.ts
new file mode 100644
index 000000000..c8cd4b63f
--- /dev/null
+++ b/cypress/integration/step1_test.ts
@@ -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", "μΌκ΅¬");
+ // });
+});
\ No newline at end of file
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
new file mode 100644
index 000000000..59b2bab6e
--- /dev/null
+++ b/cypress/plugins/index.js
@@ -0,0 +1,22 @@
+///
+// ***********************************************************
+// 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
+}
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
new file mode 100644
index 000000000..119ab03f7
--- /dev/null
+++ b/cypress/support/commands.js
@@ -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) => { ... })
diff --git a/cypress/support/index.js b/cypress/support/index.js
new file mode 100644
index 000000000..d68db96df
--- /dev/null
+++ b/cypress/support/index.js
@@ -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')
diff --git a/dist/app.js b/dist/app.js
new file mode 100644
index 000000000..12f7e430f
--- /dev/null
+++ b/dist/app.js
@@ -0,0 +1,7 @@
+import { navigatorController } from './controller/navigator.js';
+import { navigatorRenderer } from './view/navigator.js';
+export const app = () => {
+ navigatorRenderer();
+ navigatorController();
+};
+app();
diff --git a/dist/controller/navigator.js b/dist/controller/navigator.js
new file mode 100644
index 000000000..83d9a4642
--- /dev/null
+++ b/dist/controller/navigator.js
@@ -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();
+};
diff --git a/dist/controller/savedVideoPage.js b/dist/controller/savedVideoPage.js
new file mode 100644
index 000000000..d5fbf4f4f
--- /dev/null
+++ b/dist/controller/savedVideoPage.js
@@ -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();
+};
diff --git a/dist/controller/snackbar.js b/dist/controller/snackbar.js
new file mode 100644
index 000000000..6b2cbe600
--- /dev/null
+++ b/dist/controller/snackbar.js
@@ -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);
+};
diff --git a/dist/controller/uriParameter.js b/dist/controller/uriParameter.js
new file mode 100644
index 000000000..acf0c7994
--- /dev/null
+++ b/dist/controller/uriParameter.js
@@ -0,0 +1,5 @@
+export const uriParameter = {
+ part: "snippet",
+ maxResults: 10,
+ type: "video"
+};
diff --git a/dist/controller/videoSearchPage.js b/dist/controller/videoSearchPage.js
new file mode 100644
index 000000000..d3fc2a962
--- /dev/null
+++ b/dist/controller/videoSearchPage.js
@@ -0,0 +1,152 @@
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+ return new (P || (P = Promise))(function (resolve, reject) {
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
+ });
+};
+import { $, $$ } from '../util.js';
+import { getRecentKeywords, saveRecentKeywordList } from '../model/handleLocalStorage/recentKeywordManager.js';
+import { renderNotFoundImage, renderRecentKeyword, renderSearchedArticle, renderSkeleton, reRenderSavedButtonText, reRenderNumOfSavedVideos, removeModalArticles, removeDuplicateRecentKeyword, removeOldSearchKeyword, removeSkeletons, } from '../view/videoSearchPage.js';
+import { saveVideo, unsaveVideo, isSavedVideo, getSavedVideos } from '../model/handleLocalStorage/articleManager.js';
+import { uriParameter } from './uriParameter.js';
+const onModalClose = ($modal) => {
+ $modal.classList.remove('open');
+ $('#saved-video-button').click();
+};
+const searchOnYoutube = () => {
+ var _a;
+ const inputValue = $('#search-input').value;
+ if (inputValue.trim() === '') {
+ $('#search-input').value = '';
+ (_a = $('#search-input')) === null || _a === void 0 ? void 0 : _a.focus();
+ alert('κ²μμ΄λ₯Ό μ
λ ₯ν΄μ£ΌμΈμ.');
+ return;
+ }
+ $('#search-input').dataset.nextPageToken = '';
+ $('#search-input').dataset.isFirstSearch = 'true';
+ renderArticles(inputValue);
+};
+const searchInfiniteScroll = () => {
+ if ($('.modal-inner').offsetHeight + $('.modal-inner').scrollTop >= $('.modal-inner').scrollHeight) {
+ if ($('#search-input').dataset.nextPageToken === 'undefined') {
+ addRecentKeyword($('#search-input').value);
+ return;
+ }
+ $('#search-input').dataset.isFirstSearch = 'false';
+ renderArticles($('#search-input').value);
+ }
+};
+const addRecentKeyword = (searchValue) => {
+ var _a;
+ removeDuplicateRecentKeyword(searchValue);
+ renderRecentKeyword(searchValue);
+ removeOldSearchKeyword();
+ saveRecentKeywordListToLocalStorage();
+ addClickEventToRecentKeyword((_a = $('#recent-keyword > span')) === null || _a === void 0 ? void 0 : _a.nextSibling, searchValue);
+};
+const saveRecentKeywordListToLocalStorage = () => {
+ let recentKeywordList = [];
+ $$('a.chip').forEach((keyword) => {
+ recentKeywordList.push(keyword.innerText);
+ });
+ saveRecentKeywordList(recentKeywordList);
+};
+const addClickEventToRecentKeyword = ($keywordElement, searchValue) => {
+ $keywordElement.addEventListener('click', () => {
+ removeModalArticles();
+ $('#search-input').dataset.nextPageToken = '';
+ $('#search-input').dataset.isFirstSearch = 'true';
+ $('#search-input').value = searchValue;
+ renderArticles(searchValue);
+ });
+};
+const renderArticles = (searchValue) => {
+ var _a;
+ // when first search, delete articles
+ if (((_a = $('#search-input')) === null || _a === void 0 ? void 0 : _a.dataset.isFirstSearch) === 'true') {
+ $('#searched-article-wrapper').innerHTML = '';
+ }
+ renderSkeleton();
+ const searchResult = getSearchResult(searchValue);
+ searchResult
+ .then((result) => {
+ removeSkeletons();
+ const resultJSON = JSON.parse(result);
+ if (resultJSON.pageInfo.totalResults === 0) {
+ renderNotFoundImage();
+ return;
+ }
+ // search result exist
+ resultJSON.items.forEach((video) => {
+ const videoInfo = {
+ videoId: video.id.videoId,
+ videoTitle: video.snippet.title,
+ publishedAt: convertDateFormat(video.snippet.publishedAt),
+ channelId: video.snippet.channelId,
+ channelTitle: video.snippet.channelTitle,
+ isSaved: isSavedVideo(video.id.videoId) ? true : false,
+ isWatched: false,
+ };
+ renderSearchedArticle(videoInfo);
+ addClickEventToSaveButton(videoInfo);
+ });
+ addRecentKeyword(searchValue);
+ })
+ .catch((error) => {
+ console.error(`ERROR: ${error}`);
+ });
+};
+const getSearchResult = (searchTarget) => __awaiter(void 0, void 0, void 0, function* () {
+ const URI = `https://wizardly-almeida-959c19.netlify.app/.netlify/functions/myFunction/search?part=${uriParameter.part}&q=${searchTarget}&maxResults=${uriParameter.maxResults}&type=${uriParameter.type}&pageToken=${$('#search-input').dataset.nextPageToken}`;
+ const response = yield fetch(URI);
+ const body = JSON.stringify(yield response.json());
+ $('#search-input').dataset.nextPageToken = JSON.parse(body).nextPageToken;
+ return body;
+});
+const addClickEventToSaveButton = (videoInfo) => {
+ const $saveButton = $('div.modal-inner article.clip:last-child button');
+ $saveButton.addEventListener('click', () => {
+ if ($saveButton.dataset.saved === 'false') {
+ if (getSavedVideos().length === 100) {
+ alert('μ μ₯λ λμμμ κ°―μκ° 100κ°λ₯Ό λμ μ μμ΅λλ€.');
+ return;
+ }
+ saveVideo(videoInfo);
+ $saveButton.dataset.saved = 'true';
+ }
+ else {
+ if (!confirm('μ λ§ μμ νμκ² μ΅λκΉ?')) {
+ return;
+ }
+ unsaveVideo($saveButton.dataset.videoId);
+ $saveButton.dataset.saved = 'false';
+ }
+ reRenderSavedButtonText($saveButton);
+ reRenderNumOfSavedVideos();
+ });
+};
+const convertDateFormat = (originalFormat) => {
+ const temp = new Date(originalFormat);
+ return `${temp.getFullYear()}λ
${temp.getMonth() + 1}μ ${temp.getDate() + 1}μΌ`;
+};
+const showRecentKeywords = () => {
+ getRecentKeywords()
+ .reverse()
+ .forEach((keyword) => {
+ var _a;
+ renderRecentKeyword(keyword);
+ addClickEventToRecentKeyword((_a = $('#recent-keyword > span')) === null || _a === void 0 ? void 0 : _a.nextSibling, keyword);
+ });
+};
+export const videoSearchPageController = () => {
+ var _a;
+ // add event close button
+ const $modal = $('.modal');
+ $('.modal-close').addEventListener('click', () => onModalClose($modal));
+ $('#search-submit-button').addEventListener('click', () => searchOnYoutube());
+ (_a = $('.modal-inner')) === null || _a === void 0 ? void 0 : _a.addEventListener('scroll', () => searchInfiniteScroll());
+ showRecentKeywords();
+};
diff --git a/dist/controller/watchedVideoPage.js b/dist/controller/watchedVideoPage.js
new file mode 100644
index 000000000..63a2b38d7
--- /dev/null
+++ b/dist/controller/watchedVideoPage.js
@@ -0,0 +1,51 @@
+import { $$ } from '../util.js';
+import { changeWatchedState, unsaveVideo, getSavedVideos } from '../model/handleLocalStorage/articleManager.js';
+import { removeArticle, renderEmptyVideoMessage } from '../view/watchedVideoPage.js';
+import { reRenderSavedButtonText, reRenderNumOfSavedVideos } from '../view/videoSearchPage.js';
+const checkEmptyWatchedVideo = () => {
+ let watchedVideoList = getSavedVideos().filter((video) => video.isWatched === true);
+ if (watchedVideoList.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 (checkEmptyWatchedVideo()) {
+ 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 (checkEmptyWatchedVideo()) {
+ 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 watchedVideoPageController = () => {
+ addEventToWatchedButton();
+ addEventToRemoveButton();
+};
diff --git a/dist/model/IVideoInfo.js b/dist/model/IVideoInfo.js
new file mode 100644
index 000000000..cb0ff5c3b
--- /dev/null
+++ b/dist/model/IVideoInfo.js
@@ -0,0 +1 @@
+export {};
diff --git a/dist/model/IYoutubeJSON.js b/dist/model/IYoutubeJSON.js
new file mode 100644
index 000000000..cb0ff5c3b
--- /dev/null
+++ b/dist/model/IYoutubeJSON.js
@@ -0,0 +1 @@
+export {};
diff --git a/dist/model/handleLocalStorage/articleManager.js b/dist/model/handleLocalStorage/articleManager.js
new file mode 100644
index 000000000..e27c8e02d
--- /dev/null
+++ b/dist/model/handleLocalStorage/articleManager.js
@@ -0,0 +1,48 @@
+import { showSnackbar } from '../../controller/snackbar.js';
+export const getSavedVideos = () => {
+ const savedVideosStr = localStorage.getItem('savedVideos');
+ if (!savedVideosStr) {
+ return [];
+ }
+ const savedVideosList = JSON.parse(savedVideosStr);
+ return savedVideosList;
+};
+export const saveVideo = (video) => {
+ const savedVideosList = getSavedVideos();
+ video.isSaved = true;
+ savedVideosList.push(video);
+ localStorage.setItem('savedVideos', JSON.stringify(savedVideosList));
+ showSnackbar('λμμμ΄ μ μ₯λμμ΅λλ€.');
+};
+export const unsaveVideo = (videoId) => {
+ let savedVideosList = getSavedVideos();
+ savedVideosList = savedVideosList.filter((video) => video.videoId !== videoId);
+ localStorage.setItem('savedVideos', JSON.stringify(savedVideosList));
+ showSnackbar('λμμμ΄ μμ λμμ΅λλ€.');
+};
+export const isSavedVideo = (videoId) => {
+ const savedVideosList = getSavedVideos();
+ let ret = false;
+ savedVideosList.forEach((video) => {
+ if (video.videoId === videoId) {
+ ret = true;
+ }
+ });
+ return ret;
+};
+export const changeWatchedState = (videoId) => {
+ let savedVideosList = getSavedVideos();
+ for (const video of savedVideosList) {
+ if (video.videoId !== videoId) {
+ continue;
+ }
+ video.isWatched = !video.isWatched;
+ if (video.isWatched) {
+ showSnackbar('λ³Έ μμ λͺ©λ‘μΌλ‘ μ΄λλμμ΅λλ€.');
+ }
+ else {
+ showSnackbar('λ³Ό μμ λͺ©λ‘μΌλ‘ μ΄λλμμ΅λλ€.');
+ }
+ }
+ localStorage.setItem('savedVideos', JSON.stringify(savedVideosList));
+};
diff --git a/dist/model/handleLocalStorage/recentKeywordManager.js b/dist/model/handleLocalStorage/recentKeywordManager.js
new file mode 100644
index 000000000..fb945c173
--- /dev/null
+++ b/dist/model/handleLocalStorage/recentKeywordManager.js
@@ -0,0 +1,11 @@
+export const getRecentKeywords = () => {
+ const recentKeywords = localStorage.getItem('recentKeywords');
+ if (!recentKeywords) {
+ return [];
+ }
+ const recentKeywordList = JSON.parse(recentKeywords);
+ return recentKeywordList;
+};
+export const saveRecentKeywordList = (recentKeywordList) => {
+ localStorage.setItem('recentKeywords', JSON.stringify(recentKeywordList));
+};
diff --git a/dist/util.js b/dist/util.js
new file mode 100644
index 000000000..bfea08cd6
--- /dev/null
+++ b/dist/util.js
@@ -0,0 +1,3 @@
+const $ = (selector) => document.querySelector(selector);
+const $$ = (selector) => document.querySelectorAll(selector);
+export { $, $$ };
diff --git a/dist/view/navigator.js b/dist/view/navigator.js
new file mode 100644
index 000000000..8253976e4
--- /dev/null
+++ b/dist/view/navigator.js
@@ -0,0 +1,20 @@
+import { $ } from '../util.js';
+const navigatorDiv = () => {
+ return `
+
+
+
+ π©π»βπ» λλ§μ μ νλΈ κ°μμ€ π¨π»βπ»
+
+
+
+
`;
+};
+export const navigatorRenderer = () => {
+ var _a;
+ (_a = $('#app')) === null || _a === void 0 ? void 0 : _a.insertAdjacentHTML('beforeend', navigatorDiv());
+};
diff --git a/dist/view/savedVideoPage.js b/dist/view/savedVideoPage.js
new file mode 100644
index 000000000..5d108ed77
--- /dev/null
+++ b/dist/view/savedVideoPage.js
@@ -0,0 +1,25 @@
+import { $ } from '../util.js';
+import { getSavedVideos } from '../model/handleLocalStorage/articleManager.js';
+import { savedVideoArticle, savedVideoSection } from './videoPageTemplates/index.js';
+export const renderEmptyVideoMessage = () => {
+ $('#saved-video-section').insertAdjacentHTML('beforeend', 'λ³Ό μμμ΄ μμ΅λλ€.
');
+};
+export const renderArticle = (video) => {
+ $('#saved-video-section').insertAdjacentHTML('beforeend', savedVideoArticle(video));
+};
+export const savedVideoPageRenderer = () => {
+ var _a, _b;
+ (_a = $('#video-list')) === null || _a === void 0 ? void 0 : _a.remove();
+ (_b = $('header')) === null || _b === void 0 ? void 0 : _b.insertAdjacentHTML('afterend', savedVideoSection());
+ const savedVideoList = getSavedVideos();
+ let unwatchedVideoList = savedVideoList.filter((video) => video.isWatched === false);
+ if (unwatchedVideoList.length === 0) {
+ renderEmptyVideoMessage();
+ }
+ unwatchedVideoList.forEach((video) => {
+ renderArticle(video);
+ });
+};
+export const removeArticle = (article) => {
+ article.remove();
+};
diff --git a/dist/view/snackbar.js b/dist/view/snackbar.js
new file mode 100644
index 000000000..f2c28dbce
--- /dev/null
+++ b/dist/view/snackbar.js
@@ -0,0 +1,4 @@
+import { $ } from '../util.js';
+export const snackbarRenderer = ($snackbar) => {
+ $('#app').insertAdjacentElement('beforeend', $snackbar);
+};
diff --git a/dist/view/videoPageTemplates/index.js b/dist/view/videoPageTemplates/index.js
new file mode 100644
index 000000000..9569ef6a3
--- /dev/null
+++ b/dist/view/videoPageTemplates/index.js
@@ -0,0 +1,3 @@
+import { savedVideoSection } from './savedVideoSection.js';
+import { savedVideoArticle } from './savedVideoArticle.js';
+export { savedVideoArticle, savedVideoSection };
diff --git a/dist/view/videoPageTemplates/savedVideoArticle.js b/dist/view/videoPageTemplates/savedVideoArticle.js
new file mode 100644
index 000000000..6e286c60a
--- /dev/null
+++ b/dist/view/videoPageTemplates/savedVideoArticle.js
@@ -0,0 +1,36 @@
+export const savedVideoArticle = (video) => {
+ return `
+
+
+
+
+
+
${video.videoTitle}
+
+
+ `;
+};
diff --git a/dist/view/videoPageTemplates/savedVideoSection.js b/dist/view/videoPageTemplates/savedVideoSection.js
new file mode 100644
index 000000000..650fea7c5
--- /dev/null
+++ b/dist/view/videoPageTemplates/savedVideoSection.js
@@ -0,0 +1,8 @@
+export const savedVideoSection = () => {
+ return `
+
+
+ `;
+};
diff --git a/dist/view/videoSearchPage.js b/dist/view/videoSearchPage.js
new file mode 100644
index 000000000..8fef18b60
--- /dev/null
+++ b/dist/view/videoSearchPage.js
@@ -0,0 +1,57 @@
+import { $, $$ } from '../util.js';
+import { modalDiv, modalCloseButton, modalHeader, modalSearchForm, modalRecentKeywordSection, modalArticleSection, modalArticle, skeleton, } from './videoSearchPageTemplates/index.js';
+import { getSavedVideos } from '../model/handleLocalStorage/articleManager.js';
+export const renderSearchedArticle = (videoInfo) => {
+ const $articleSection = $('div.modal-inner section.video-wrapper');
+ $articleSection.insertAdjacentHTML('beforeend', modalArticle(videoInfo));
+};
+export const renderRecentKeyword = (searchValue) => {
+ const $recentKeywordSpan = $('#recent-keyword > span');
+ $recentKeywordSpan.insertAdjacentHTML('afterend', `${searchValue}`);
+};
+export const removeModalArticles = () => {
+ $('div.modal-inner section.video-wrapper').innerHTML = '';
+};
+export const renderNotFoundImage = () => {
+ var _a;
+ removeModalArticles();
+ (_a = $('div.modal-inner section.video-wrapper')) === null || _a === void 0 ? void 0 : _a.insertAdjacentHTML('beforeend', '');
+};
+export const renderSkeleton = () => {
+ var _a;
+ (_a = $('div.modal-inner section.video-wrapper')) === null || _a === void 0 ? void 0 : _a.insertAdjacentHTML('beforeend', skeleton().repeat(10));
+};
+export const reRenderSavedButtonText = ($button) => {
+ $button.textContent = $button.textContent === 'β μ μ₯ μ·¨μ' ? 'β¬οΈ μ μ₯' : 'β μ μ₯ μ·¨μ';
+};
+export const reRenderNumOfSavedVideos = () => {
+ $('#num-of-videos').innerText = String(getSavedVideos().length);
+};
+export const removeDuplicateRecentKeyword = (searchValue) => {
+ $$('.chip').forEach((element) => {
+ if (element.innerText === searchValue) {
+ element.remove();
+ }
+ });
+};
+export const removeOldSearchKeyword = () => {
+ var _a;
+ if ($$('.chip').length === 4) {
+ (_a = $('a.chip:last-child')) === null || _a === void 0 ? void 0 : _a.remove();
+ }
+};
+export const removeSkeletons = () => {
+ $$('div.modal-inner section.video-wrapper .skeleton').forEach((element) => {
+ var _a;
+ (_a = element.parentElement) === null || _a === void 0 ? void 0 : _a.remove();
+ });
+};
+export const videoSearchPageRenderer = () => {
+ var _a, _b, _c, _d, _e, _f;
+ (_a = $('#app')) === null || _a === void 0 ? void 0 : _a.insertAdjacentHTML('beforeend', modalDiv());
+ (_b = $('div.modal-inner')) === null || _b === void 0 ? void 0 : _b.insertAdjacentHTML('beforeend', modalCloseButton());
+ (_c = $('div.modal-inner')) === null || _c === void 0 ? void 0 : _c.insertAdjacentHTML('beforeend', modalHeader());
+ (_d = $('div.modal-inner')) === null || _d === void 0 ? void 0 : _d.insertAdjacentHTML('beforeend', modalSearchForm());
+ (_e = $('div.modal-inner')) === null || _e === void 0 ? void 0 : _e.insertAdjacentHTML('beforeend', modalRecentKeywordSection());
+ (_f = $('div.modal-inner')) === null || _f === void 0 ? void 0 : _f.insertAdjacentHTML('beforeend', modalArticleSection());
+};
diff --git a/dist/view/videoSearchPageTemplates/index.js b/dist/view/videoSearchPageTemplates/index.js
new file mode 100644
index 000000000..157b3bf09
--- /dev/null
+++ b/dist/view/videoSearchPageTemplates/index.js
@@ -0,0 +1,9 @@
+import { modalDiv } from "./modalDiv.js";
+import { modalCloseButton } from "./modalCloseButton.js";
+import { modalHeader } from "./modalHeader.js";
+import { modalSearchForm } from "./modalSearchForm.js";
+import { modalRecentKeywordSection } from "./modalRecentKeywordSection.js";
+import { modalArticleSection } from "./modalArticleSection.js";
+import { modalArticle } from "./modalArticle.js";
+import { skeleton } from "./skeleton.js";
+export { modalDiv, modalCloseButton, modalHeader, modalSearchForm, modalRecentKeywordSection, modalArticleSection, modalArticle, skeleton };
diff --git a/dist/view/videoSearchPageTemplates/modalArticle.js b/dist/view/videoSearchPageTemplates/modalArticle.js
new file mode 100644
index 000000000..e6d65c6b0
--- /dev/null
+++ b/dist/view/videoSearchPageTemplates/modalArticle.js
@@ -0,0 +1,26 @@
+export const modalArticle = (obj) => {
+ const buttonText = obj.isSaved ? 'β μ μ₯ μ·¨μ' : 'β¬οΈ μ μ₯';
+ return `
+
+
+
+
+
+ `;
+};
diff --git a/dist/view/videoSearchPageTemplates/modalArticleSection.js b/dist/view/videoSearchPageTemplates/modalArticleSection.js
new file mode 100644
index 000000000..845c61aa9
--- /dev/null
+++ b/dist/view/videoSearchPageTemplates/modalArticleSection.js
@@ -0,0 +1,12 @@
+import { getSavedVideos } from "../../model/handleLocalStorage/articleManager.js";
+export const modalArticleSection = () => {
+ return `
+
+
+ μ μ₯λ μμ κ°―μ: ${getSavedVideos().length}κ°
+
+
+ `;
+};
diff --git a/dist/view/videoSearchPageTemplates/modalCloseButton.js b/dist/view/videoSearchPageTemplates/modalCloseButton.js
new file mode 100644
index 000000000..a2fce706a
--- /dev/null
+++ b/dist/view/videoSearchPageTemplates/modalCloseButton.js
@@ -0,0 +1,8 @@
+export const modalCloseButton = () => {
+ return `
+ `;
+};
diff --git a/dist/view/videoSearchPageTemplates/modalDiv.js b/dist/view/videoSearchPageTemplates/modalDiv.js
new file mode 100644
index 000000000..1dfc3368f
--- /dev/null
+++ b/dist/view/videoSearchPageTemplates/modalDiv.js
@@ -0,0 +1,7 @@
+export const modalDiv = () => {
+ return `
+ `;
+};
diff --git a/dist/view/videoSearchPageTemplates/modalHeader.js b/dist/view/videoSearchPageTemplates/modalHeader.js
new file mode 100644
index 000000000..515d27e53
--- /dev/null
+++ b/dist/view/videoSearchPageTemplates/modalHeader.js
@@ -0,0 +1,6 @@
+export const modalHeader = () => {
+ return `
+
+ π μ νλΈ κ²μ
+ `;
+};
diff --git a/dist/view/videoSearchPageTemplates/modalRecentKeywordSection.js b/dist/view/videoSearchPageTemplates/modalRecentKeywordSection.js
new file mode 100644
index 000000000..b60f11c12
--- /dev/null
+++ b/dist/view/videoSearchPageTemplates/modalRecentKeywordSection.js
@@ -0,0 +1,5 @@
+export const modalRecentKeywordSection = () => {
+ return ``;
+};
diff --git a/dist/view/videoSearchPageTemplates/modalSearchForm.js b/dist/view/videoSearchPageTemplates/modalSearchForm.js
new file mode 100644
index 000000000..732c5f21e
--- /dev/null
+++ b/dist/view/videoSearchPageTemplates/modalSearchForm.js
@@ -0,0 +1,7 @@
+export const modalSearchForm = () => {
+ return `
+ `;
+};
diff --git a/dist/view/videoSearchPageTemplates/skeleton.js b/dist/view/videoSearchPageTemplates/skeleton.js
new file mode 100644
index 000000000..6cfe8be54
--- /dev/null
+++ b/dist/view/videoSearchPageTemplates/skeleton.js
@@ -0,0 +1,17 @@
+export const skeleton = () => {
+ return `
+
+
+
+
+ `;
+};
diff --git a/dist/view/watchedVideoPage.js b/dist/view/watchedVideoPage.js
new file mode 100644
index 000000000..9a7977629
--- /dev/null
+++ b/dist/view/watchedVideoPage.js
@@ -0,0 +1,28 @@
+import { $, $$ } from '../util.js';
+import { getSavedVideos } from '../model/handleLocalStorage/articleManager.js';
+import { savedVideoArticle, savedVideoSection } from './videoPageTemplates/index.js';
+export const renderEmptyVideoMessage = () => {
+ $('#saved-video-section').insertAdjacentHTML('beforeend', 'λ³Έ μμμ΄ μμ΅λλ€.
');
+};
+export const renderArticle = (video) => {
+ $('#saved-video-section').insertAdjacentHTML('beforeend', savedVideoArticle(video));
+};
+export const watchedVideoPageRenderer = () => {
+ var _a, _b;
+ (_a = $('#video-list')) === null || _a === void 0 ? void 0 : _a.remove();
+ (_b = $('header')) === null || _b === void 0 ? void 0 : _b.insertAdjacentHTML('afterend', savedVideoSection());
+ const savedVideoList = getSavedVideos();
+ let watchedVideoList = savedVideoList.filter((video) => video.isWatched === true);
+ if (watchedVideoList.length === 0) {
+ renderEmptyVideoMessage();
+ }
+ watchedVideoList.forEach((video) => {
+ renderArticle(video);
+ });
+ $$('span.video-watched-button').forEach((elem) => {
+ elem.classList.remove('opacity-hover');
+ });
+};
+export const removeArticle = (article) => {
+ article.remove();
+};
diff --git a/index.html b/index.html
index c5e27ed2f..afb7a7020 100644
--- a/index.html
+++ b/index.html
@@ -1,124 +1,17 @@
-
-
- π©π»βπ» μ νλΈ κ°μμ€
-
-
-
-
-
-
-
-
- π©π»βπ» λλ§μ μ νλΈ κ°μμ€ π¨π»βπ»
-
-
-
-
-
-
-
-
-
-
μλμ΄λ
Έ 무λλ±
-
-
-
-
-
-
-
-
-
-
-
- π μ νλΈ κ²μ
-
-
-
-
-
- μ μ₯λ μμ κ°―μ: 50κ°
-
-
-
-
-
-
-
-
μλμ΄λ
Έ 무λλ±
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ π©π»βπ» μ νλΈ κ°μμ€
+
+
+
+
+
+
+
+
+
+
+