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

[뉴스 뷰어 페이지 2단계] 정윤서 미션 제출합니다. #9

Open
wants to merge 4 commits into
base: yunseeo
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions src/component/Section/NewsItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import Image from "./Image";
import Title from "./Title";
import Content from "./Content";
import AuthorDate from "./AuthorDate";
import Modal from "../../modal/Modal";
import ExistingModal from "../../modal/ExistingModal";

class NewsItem {
constructor(article, index) {
Expand All @@ -21,6 +23,18 @@ class NewsItem {

const authorDateComponent = new AuthorDate(article.author, article.publishedAt);
this.element.appendChild(authorDateComponent.getElement());

this.element.addEventListener("click", () => this.handleArticleClick(article));
}

handleArticleClick(article) {
const existingData = JSON.parse(localStorage.getItem(`savedData-${article.title}`)) || {};
const modal = Object.keys(existingData).length > 0
? new ExistingModal(existingData, `savedData-${article.title}`)
: new Modal(`savedData-${article.title}`, article);

this.element.appendChild(modal.modalElement);
modal.openModal();
}

getElement() {
Expand Down
155 changes: 8 additions & 147 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,185 +1,46 @@
import "./styles";
require("dotenv").config();
//import TAP_NAME from "./assets/TAB_NAME";

/*class App {
constructor() {
this.apiKey = process.env.API_KEY || '40a4a566f0eb4cb5aa724df7ddc58ad7';
this.selectedTap = null;

const appElement = document.getElementById("app");

const headerElement = document.createElement("header");
headerElement.innerHTML = "<h1>LINKHU-news</h1>";
appElement.appendChild(headerElement);

const navElement = document.createElement("nav");
const navListElement = document.createElement("ul");

TAP_NAME.forEach((tap) => {
const tapItem = document.createElement("li");
tapItem.textContent = tap.ko;
tapItem.dataset.tap = tap.en;
tapItem.addEventListener("click", this.handleTapClick.bind(this));

const underline = document.createElement('div');
underline.classList.add('underline');
tapItem.appendChild(underline);

navListElement.appendChild(tapItem);

if (tap.en === 'all') {
tapItem.classList.add('active');
underline.style.width = '100%';
}
});

navElement.appendChild(navListElement);
appElement.appendChild(navElement);

this.sectionElement = document.createElement("section");
appElement.appendChild(this.sectionElement);

this.fetchData();
}

async fetchData() {
try {
let apiUrl;

if (this.selectedTap && this.selectedTap !== "all") {
apiUrl = `http://newsapi.org/v2/top-headlines?country=kr&category=${this.selectedTap}&apiKey=${this.apiKey}`;
} else {
apiUrl = `http://newsapi.org/v2/top-headlines?country=kr&apiKey=${this.apiKey}`
}

const response = await fetch(apiUrl);

if (!response.ok) {
throw new Error(`Failed to fetch data. Status: ${response.status}`);
}

const data = await response.json();
console.log("API Response:", data);
this.displayData(data);

} catch (error) {
console.error("Error fetching data:", error.message);
}
}

displayData(data) {
this.sectionElement.innerHTML = "";

const articles = data.articles;
if (articles && articles.length > 0) {

const shuffledArticles = this.shuffleArray(articles).slice(0, 2);

shuffledArticles.forEach((article, index) => {
const articleContainer = document.createElement("div");
articleContainer.id = `article-${index+1}`;


if (article.urlToImage) {
const imageElement = document.createElement("img");
imageElement.src = article.urlToImage;
const imageAltText = article.title ? article.title : "Article Image";
imageElement.alt = imageAltText;
articleContainer.appendChild(imageElement);
}

const titleElement = document.createElement("h2");
titleElement.textContent = article.title;
articleContainer.appendChild(titleElement);

const contentElement = document.createElement("p");
contentElement.textContent = article.description;
articleContainer.appendChild(contentElement);

const authorDateElement = document.createElement("p");
authorDateElement.innerHTML = `<strong>Author:</strong> ${article.author || 'Unknown'} | <strong>Date:</strong> ${new Date(article.publishedAt).toLocaleDateString()}`;
articleContainer.appendChild(authorDateElement);

this.sectionElement.appendChild(articleContainer);
});
} else {

const noDataElement = document.createElement("p");
noDataElement.textContent = "No articles available for this category.";
this.sectionElement.appendChild(noDataElement);
}
}


handleTapClick(event) {
this.selectedTap = event.target.dataset.tap;
console.log(`Selected Tap: ${this.selectedTap}`);

const navItems = document.querySelectorAll('nav li');
navItems.forEach(item => item.classList.remove('active'));

event.target.classList.add('active');

this.fetchData();

const activeItem = event.target;
const underline = activeItem.querySelector('.underline');
underline.style.width = '100%';
}

shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
}

const app = new App();*/


import Header from "./component/Header";
import TabList from "./component/Tabs/TabList";
import NewsList from "./component/Section/NewsList";

class App {
constructor() {
this.apiKey = process.env.API_KEY || '40a4a566f0eb4cb5aa724df7ddc58ad7';
this.apiKey = process.env.API_KEY || '40a4a566f0eb4cb5aa724df7ddc58ad7'; //process.env.API_KEY가 정의 안되어 있으면 '40a4~'어쩌고를 사용해라.
Copy link
Contributor

Choose a reason for hiding this comment

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

github에 올라가는 코드에 직접적으로 apiKey값이 노출되는 건 위험할 수 있습니다.

this.selectedTap = null;

this.header = new Header();
this.tabList = new TabList(this.fetchData.bind(this));
this.tabList = new TabList(this.fetchData.bind(this)); //this는 app을 가르키도록 bind. 고정.
this.newsList = new NewsList();

const appElement = document.getElementById("app");
appElement.appendChild(this.header.getElement());
appElement.appendChild(this.tabList.getElement());
appElement.appendChild(this.newsList.getElement());

this.fetchData();
this.fetchData(); //페이지 처음 혹은 새로고침 시에만 실행됨. 어떤 탭도 선택 X인 상태.
}

async fetchData(selectedTap) {
async fetchData(selectedTap) {
Copy link
Contributor

Choose a reason for hiding this comment

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

요거는 사소하지만 Tab이 맞지 않나요??

try {
let apiUrl;

if (selectedTap && selectedTap !== "all") {
apiUrl = `http://newsapi.org/v2/top-headlines?country=kr&category=${selectedTap}&apiKey=${this.apiKey}`;
} else {
} else { //selectedTap이 정의되지 않은 상태. undefined.
apiUrl = `http://newsapi.org/v2/top-headlines?country=kr&apiKey=${this.apiKey}`;
}

const response = await fetch(apiUrl);
const response = await fetch(apiUrl); //promise객체 반환 fetch니까.
Comment on lines 29 to +35
Copy link
Contributor

Choose a reason for hiding this comment

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

해당 단락과 fetch 부분은 따로 분리해볼 수 있을 것 같네요

Copy link
Contributor

Choose a reason for hiding this comment

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

promise 메소드도 한번 사용해보는 것을 추천드립니다.


if (!response.ok) {
throw new Error(`Failed to fetch data. Status: ${response.status}`);
}

const data = await response.json();
const data = await response.json(); //await 제거 시, 아직 response.json() 프로미스 다 json으로 반환할 때까지 완료 안했는데 다음줄 코드 실행해버리므로 화면에 에러메시지 뜨는 거임.
console.log("API Response:", data);
this.newsList.update(data.articles);
this.newsList.update(data.articles); //NewsList.js에 있는 update메서드 사용.

} catch (error) {
console.error("Error fetching data:", error.message);
Expand Down
48 changes: 48 additions & 0 deletions src/modal/ExistingModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Modal from "./Modal";

class ExistingModal extends Modal {
Copy link
Contributor

Choose a reason for hiding this comment

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

고민을 많이한 흔적이 보이는 코드였습니다👍
스토리지의 키값을 외부에서 받아와 접근할 수 있게한 점도 좋았고요

코드의 재사용성을 좀 더 개선해보고 싶다면 Modal 컴포넌트는 아래 사진과 같이 내용이 비어있도록 하고 외부에서 contents 또는 element를 주입할 수 있도록 개선해볼 수 있을 것 같아요

image

여러 페이지에서도 반복적으로 재사용할 수 있는 컴포넌트를 공통 컴포넌트라 합니다. Modal은 댓글 작성 뿐만 아니라 여러 곳에서 재사용이 될 수 있는 공통 컴포넌트로 사용할 수 있습니다. 만약 모달의 하위 컴포넌트를 외부에서 넘겨 받도록 설계할 경우 좀 더 재사용이 편해지겠죠?

constructor(existingData, storageKey) {
super();

this.storageKey = storageKey;

if (existingData) {
this.titleInput.value = existingData.title || "";
this.contentInput.value = existingData.content || "";

this.modalTitle.textContent = "의견 수정하기"

this.saveButton.textContent = "수정";

const deleteButton = document.createElement("button");
deleteButton.textContent = "삭제";
deleteButton.classList.add("delete-button");
deleteButton.addEventListener("click", () => {
this.handleDelete();
});
this.modalElement.appendChild(deleteButton);
}
}

handleSave() {
const updatedTitle = this.titleInput.value;
const updatedContent = this.contentInput.value;

const updatedData = {
title: updatedTitle,
content: updatedContent
};

localStorage.setItem(this.storageKey, JSON.stringify(updatedData));

this.closeModal();
}

handleDelete() {
localStorage.removeItem(this.storageKey);

this.closeModal();
}
}

export default ExistingModal;
69 changes: 69 additions & 0 deletions src/modal/Modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
class Modal {
Copy link
Contributor

@tkdrb12 tkdrb12 Feb 12, 2024

Choose a reason for hiding this comment

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

다른 클래스에서는 element 반환하는 getElement메소드가 있었는데 Modal에는 사용하지 않은 이유가 있나요?

추가적으로 질문드리면 모달에는 쓰기 기능이 있잖아요? 그렇다면 모달 컴포넌트에서 rest api를 사용할 때 fetch의 인자는 어떤값이 들어갈 수 있을까요?

constructor(storageKey, article) {
this.modalElement = document.createElement("div");
this.modalElement.className = "modal";

this.modalTitle = document.createElement("div");
this.modalTitle.textContent = "의견 남기기";
this.modalTitle.classList.add("modalTitle");
this.modalElement.appendChild(this.modalTitle);

this.titleInputLabel = document.createElement("div");
this.titleInputLabel.textContent = "제목";
this.titleInputLabel.classList.add("modalInputLabel");
this.modalElement.appendChild(this.titleInputLabel);

this.titleInput = document.createElement("input");
this.titleInput.classList.add("modalTitleInput");
this.modalElement.appendChild(this.titleInput);

this.contentInputLabel = document.createElement("div");
this.contentInputLabel.textContent = "내용";
this.contentInputLabel.classList.add("modalInputLabel");
this.modalElement.appendChild(this.contentInputLabel);

this.contentInput = document.createElement("textarea");
this.modalElement.appendChild(this.contentInput);

this.saveButton = document.createElement("button");
this.saveButton.textContent = "저장";
this.saveButton.addEventListener("click", this.handleSave.bind(this, storageKey, article));
this.modalElement.appendChild(this.saveButton);

this.closeButton = document.createElement("div");
this.closeButton.textContent = 'x';
this.closeButton.classList.add("modalCloseButton");
this.closeButton.addEventListener("click", this.closeModal.bind(this));
this.modalElement.appendChild(this.closeButton);
}

handleSave(storageKey, article) {
const title = this.titleInput.value;
const content = this.contentInput.value;

if (title && content) {
const savedData = JSON.parse(localStorage.getItem(storageKey)) || {};
const updatedData = { ...savedData, title, content };
localStorage.setItem(storageKey, JSON.stringify(updatedData));
this.closeModal();
} else {
this.closeModal();
}
}

openModal() {
const sectionElement = document.querySelector("section");
sectionElement.appendChild(this.modalElement);
this.modalElement.style.display = "block";
}

closeModal() {
const sectionElement = document.querySelector("section");
sectionElement.removeChild(this.modalElement);
this.modalElement.style.display = "none";
this.titleInput.value = "";
this.contentInput.value = "";
}
Comment on lines +60 to +66
Copy link
Contributor

Choose a reason for hiding this comment

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

해당 메소드를 재사용해 모달의 backdrop을 클릭했을 때나 esc키를 눌렀을 때 모달이 닫히는 기능도 고려해볼 수 있겠네요!

}

export default Modal;
Loading