-
Notifications
You must be signed in to change notification settings - Fork 6
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
base: yunseeo
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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~'어쩌고를 사용해라. | ||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 단락과 fetch 부분은 따로 분리해볼 수 있을 것 같네요 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import Modal from "./Modal"; | ||
|
||
class ExistingModal extends Modal { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 고민을 많이한 흔적이 보이는 코드였습니다👍 코드의 재사용성을 좀 더 개선해보고 싶다면 Modal 컴포넌트는 아래 사진과 같이 내용이 비어있도록 하고 외부에서 contents 또는 element를 주입할 수 있도록 개선해볼 수 있을 것 같아요 여러 페이지에서도 반복적으로 재사용할 수 있는 컴포넌트를 |
||
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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
class Modal { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 메소드를 재사용해 모달의 backdrop을 클릭했을 때나 esc키를 눌렀을 때 모달이 닫히는 기능도 고려해볼 수 있겠네요! |
||
} | ||
|
||
export default Modal; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
github에 올라가는 코드에 직접적으로 apiKey값이 노출되는 건 위험할 수 있습니다.