현재 Q&A는 더미데이터를 이용하여 렌더링하였다.
GraphQl을 이용해서 실시간 데이터를 받아오는 것을 구현 중...
구현완료하면 블로깅할 예정🔥🔥🔥
Typescript+React초기셋팅
npx create-react-app my-app --template typescript
프로젝트하며 느낀점
공부할때는 typescript기본 문법만 공부해서 이걸 어떻게 쓰나 했는데
확실히 프로젝트를 하면서 배운 내용들을 토대로 type오류를 해결하는 것이 신기했다.
typescript를 사용하므로 type을 언제나 설정해야 하는데,
특히 props로 받아온 요소들에 대해서는 무조건 타입 설정을 해주어야 했고
처음엔 type을 정확히 설정했다고 생각했는데 계속 오류가 나서 any를 남발했지만,
나중에 알고보니 내가 생각한 type이 아닐때가 많아서 정확한 type으로 다 고쳐주었다.
그리고 검색하면 어느 요소나 어느 상황에 쓰는 type의 종류에 대해 잘 나와있었기 자꾸 오류가 뜨면 혼자 해결할라하지 말고 검색을 하자!
그리고 type을 기본적으로 설정하는 것 외에도 typescript관련 모듈이나 데코레이터, 제네릭을 쓰면 더 어썸한 코드가 될것같아서 typescript를 꾸준히 공부해서 코드를 보강할 계획이다.
import './App.css';
import React, {
useEffect,
useState,
} from 'react';
import DiscussionsRender from './Components/DiscussionsRender';
import Form from './Components/Form';
import { agoraStatesDiscussions } from './Data';
import {
Discussion,
Discussions,
} from './TypeDiscussion';
const App = () => {
/*const [discussionData, setDiscussionData] = useState<Discussions>([...agoraStatesDiscussions]);*/
//localstorage기능 추가
const [discussionData, setDiscussionData] = useState<Discussions>(() =>
JSON.parse(window.localStorage.getItem("discussionData")!) || [...agoraStatesDiscussions]);
useEffect(() => {
window.localStorage.setItem("discussionData", JSON.stringify(discussionData));
}, [discussionData]);
//새로운질문이 추가되면 전체discussions에 추가하기
const handleSubmitClick = (newSingleData: Discussion): void => {
setDiscussionData([newSingleData, ...discussionData]);
};
//삭제클릭한 discussion의 id와 동일한 id를 가진 discussion을 discussion목록에서 지운다.
const handleDeleteClick = (discussionId: string) => {
setDiscussionData(discussion => {
return discussion.filter(discussion => discussion.id !== discussionId);
});
};
return (
<div className='container'>
<div><img className='cloud1' alt="" src="/images/cloud.png"/></div>
<div><img className='cloud2' alt="" src="/images/cloud.png"/></div>
<div><img className='cloud3' alt="" src="/images/cloud.png"/></div>
<main>
<div className="splash-screen">My Agora States</div>{/*첫화면 로고 띄우는 컴포넌트*/}
<h1>My Agora States</h1>{/*header 컴포넌트*/}
<Form handleSubmitClick={handleSubmitClick} />{/*질문하기 컴포넌트*/}
<DiscussionsRender discussionData={discussionData} handleDeleteClick={handleDeleteClick}/>{/*Q&A 컴포넌트*/}
</main>
</div>
);
};
export default App;
export type Discussions = Discussion[];
export interface Discussion {
id: string;
createdAt: string;
title: string;
url: string;
author: string;
answer?: object | null;
bodyHTML: string;
avatarUrl:string;
}
export const agoraStatesDiscussions = [
{
id: "D_kwDOHOApLM4APjJi",
createdAt: "2022-05-16T01:02:17Z",
title: "koans 과제 진행 중 npm install 오류로 인해 정상 작동 되지 않습니다",
url: "https://github.com/codestates-seb/agora-states-fe/discussions/45",
author: "dubipy",
answer: {
id: "DC_kwDOHOApLM4AKg6M",
createdAt: "2022-05-16T02:09:52Z",
url: "https://github.com/codestates-seb/agora-states-fe/discussions/45#discussioncomment-2756236",
author: "Kingsenal",
bodyHTML:
'<p dir="auto">안녕하세요. <a class="user-mention notranslate" data-hovercard-type="user" data-hovercard-url="/users/dubipy/hovercard" data-octo-click="hovercard-link-click" data-octo-dimensions="link_type:self" href="https://github.com/dubipy">@dubipy</a> 님!<br>\n코드스테이츠 교육 엔지니어 권준혁 입니다. <g-emoji class="g-emoji" alias="raised_hands" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f64c.png">🙌</g-emoji></p>\n<p dir="auto">질문 주신 내용은 노드 환경이 구성되어 있지 않기 때문에 발생되는 문제로 확인됩니다.</p>\n<p dir="auto"><code class="notranslate">brew unlink node && brew link node</code></p>\n<p dir="auto">노드를 연결해 보시고 안된다면</p>\n<p dir="auto"><code class="notranslate">brew link --overwrite node</code></p>\n<p dir="auto">이 명령어를 그 다음에도 안된다면 접근권한 문제일 가능성이 큽니다.</p>\n<p dir="auto"><code class="notranslate">$ sudo chmod 776 /usr/local/lib</code> 접근 권한 변경 후<br>\n<code class="notranslate">$ brew link --overwrite node</code> 다시 연결을 해보세요 !</p>\n<p dir="auto">그럼에도 안된다면 다시 한 번 더 질문을 남겨주세요 !</p>\n<p dir="auto">답변이 되셨다면 내용을 간략하게 정리해서 코멘트를 남기고 answered를 마크해주세요 <g-emoji class="g-emoji" alias="white_check_mark" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/2705.png">✅</g-emoji><br>\n감사합니다.<g-emoji class="g-emoji" alias="rocket" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f680.png">🚀</g-emoji><br>\n코드스테이츠 교육 엔지니어 권준혁</p>',
avatarUrl: "https://avatars.githubusercontent.com/u/79903256?s=64&v=4",
},
bodyHTML:
'<p dir="auto">--------------- 여기서부터 복사하세요 ---------------</p>\n<p dir="auto">운영 체제: 예) macOS</p>\n<p dir="auto">현재 어떤 챕터/연습문제/과제를 진행 중이고, 어떤 문제에 부딪혔나요?<br>\nPair 과제 / JavaScript Koans</p>\n<p dir="auto">npm install 명령어 입력 시 env: node: No such file or directory 라고 뜹니다</p>\n<p dir="auto">에러 발생하여 아래 명령어 실행 했는데도 불구하고 똑같은 에러가 발생했습니다<br>\nnpm cache clean --force</p>\n<p dir="auto">rm package-lock.json</p>\n<p dir="auto">rm -rf ./node_modules/</p>\n<p dir="auto">npm --verbose install</p>\n<p dir="auto">폴더 자체가 문제가 있다고 생각하여 github에서 다시 fork 후 진행했는데도 같은 에러가 발생했습니다<br>\n리눅스 기초 챕터 때 npm 설치해서 마지막 submit까지는 잘 됐는데 현재 짝수 생성기 폴더도 똑같이 npm install 시 no such file or directory가 발생합니다</p>\n<p dir="auto">에러가 출력된 곳에서, 이유라고 생각하는 부분을 열 줄 이내로 붙여넣기 해 주세요. (잘 모르겠으면 에러라고 생각하는 곳을 넣어주세요)</p>\n<div class="highlight highlight-source-js position-relative overflow-auto" data-snippet-clipboard-copy-content="minjun@dubi fe-sprint-javascript-koans-main % pwd \n/Users/minjun/Documents/fe_frontand_39/fe-sprint-javascript-koans-main\nminjun@dubi fe-sprint-javascript-koans-main % npm install \nenv: node: No such file or directory"><pre><span class="pl-s1">minjun</span>@<span class="pl-s1">dubi</span> <span class="pl-s1">fe</span><span class="pl-c1">-</span><span class="pl-s1">sprint</span><span class="pl-c1">-</span><span class="pl-s1">javascript</span><span class="pl-c1">-</span><span class="pl-s1">koans</span><span class="pl-c1">-</span><span class="pl-s1">main</span> <span class="pl-c1">%</span> <span class="pl-s1">pwd</span> \n<span class="pl-c1">/</span><span class="pl-v">Users</span><span class="pl-c1">/</span><span class="pl-s1">minjun</span><span class="pl-c1">/</span><span class="pl-v">Documents</span><span class="pl-c1">/</span><span class="pl-s1">fe_frontand_39</span><span class="pl-c1">/</span><span class="pl-s1">fe</span><span class="pl-c1">-</span><span class="pl-s1">sprint</span><span class="pl-c1">-</span><span class="pl-s1">javascript</span><span class="pl-c1">-</span><span class="pl-s1">koans</span><span class="pl-c1">-</span><span class="pl-s1">main</span>\n<span class="pl-s1">minjun</span><span class="pl-kos"></span>@<span class="pl-s1">dubi</span> <span class="pl-s1">fe</span><span class="pl-c1">-</span><span class="pl-s1">sprint</span><span class="pl-c1">-</span><span class="pl-s1">javascript</span><span class="pl-c1">-</span><span class="pl-s1">koans</span><span class="pl-c1">-</span><span class="pl-s1">main</span> <span class="pl-c1">%</span> <span class="pl-s1">npm</span> <span class="pl-s1">install</span> \nenv: node: <span class="pl-v">No</span> <span class="pl-s1">such</span> <span class="pl-s1">file</span> <span class="pl-s1">or</span> <span class="pl-s1">directory</span></pre></div>\n<p dir="auto">검색했던 링크가 있다면 첨부해 주세요.<br>\n<a href="https://mia-dahae.tistory.com/89" rel="nofollow">https://mia-dahae.tistory.com/89</a></p>\n<p dir="auto"><a href="https://stackoverflow.com/questions/38143558/npm-install-resulting-in-enoent-no-such-file-or-directory" rel="nofollow">https://stackoverflow.com/questions/38143558/npm-install-resulting-in-enoent-no-such-file-or-directory</a></p>\n<p dir="auto"><a href="https://velog.io/@hn04147/npm-install-%ED%95%A0-%EB%95%8C-tar-ENOENT-no-such-file-or-directory-lstat-%EC%97%90%EB%9F%AC%EB%82%A0-%EA%B2%BD%EC%9A%B0" rel="nofollow">https://velog.io/@hn04147/npm-install-%ED%95%A0-%EB%95%8C-tar-ENOENT-no-such-file-or-directory-lstat-%EC%97%90%EB%9F%AC%EB%82%A0-%EA%B2%BD%EC%9A%B0</a></p>\n<p dir="auto"><a href="https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=chandong83&logNo=221064506346" rel="nofollow">https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=chandong83&logNo=221064506346</a></p>\n<p dir="auto"><a href="https://webisfree.com/2021-07-15/npm-install-%EC%97%90%EB%9F%AC-%EB%B0%9C%EC%83%9D-rename-no-such-file-or-directory-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B0%80" rel="nofollow">https://webisfree.com/2021-07-15/npm-install-%EC%97%90%EB%9F%AC-%EB%B0%9C%EC%83%9D-rename-no-such-file-or-directory-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B0%80</a></p>\n<p dir="auto"><a href="https://hellowworlds.tistory.com/57" rel="nofollow">https://hellowworlds.tistory.com/57</a></p>',
avatarUrl:
"https://avatars.githubusercontent.com/u/97888923?s=64&u=12b18768cdeebcf358b70051283a3ef57be6a20f&v=4",
},...]
import React, { useState } from 'react';
import { Discussion } from '../TypeDiscussion';
type FormSubmitProps = {
handleSubmitClick: (todoText: Discussion) => void
};
const Form: React.FC<FormSubmitProps> = props => {
const [username, setUsername] = useState("");
const [userTitle, setUserTitle] = useState("");
const [msg, setMsg] = useState("");
//새로운 discussion의 이름 저장
const handleChangeName = (event: React.ChangeEvent<HTMLInputElement>) => {
setUsername(event.target.value);
};
//새로운 discussion의 제목 저장
const handleChangeTitle = (event: React.ChangeEvent<HTMLInputElement>) => {
setUserTitle(event.target.value);
};
//새로운 discussion의 내용 저장
const handleChangeMsg = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setMsg(event.target.value);
};
//새로운 discussion의 form을 저장해서 App.tsx의 handleSubmitClick으로 전달인자를 보낸다.
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
props.handleSubmitClick({
id: Math.random().toString(),
createdAt: new Date().toString(),
title: userTitle,
url: "https://github.com/codestates-seb/agora-states-fe/discussions/",
author: username,
bodyHTML: msg,
avatarUrl: "https://mblogthumb-phinf.pstatic.net/MjAyMTA3MDJfMjg0/MDAxNjI1MjM3MDEwMDEx.16ZkPZkXZmj6MQyJIpZlTidJmYGFnehv2QoiaIWVHAsg.louS2WVp9f5dzxMHdh1MdS-3bZgOIm68sJhcToobTPAg.JPEG.yyabbj/IMG_3332.JPG?type=w800",
});
};
return (
<section className="form__container">
<h2>질문하기</h2>
<form action="" method="get" className="form" onSubmit={handleSubmit}>
<div className="form__input--wrapper">
<div className="form__input--name">
<label htmlFor="name">Enter your name: </label>
<input
type="text"
name="name"
id="name"
value={username}
onChange={handleChangeName}
required
/>
</div>
<div className="form__input--title">
<label htmlFor="name">Enter your title: </label>
<input
type="text"
name="title"
id="title"
value={userTitle}
onChange={handleChangeTitle}
required
/>
</div>
<div className="form__textbox">
<label htmlFor="story">Your question: </label>
<textarea
id="story"
name="story"
placeholder="질문을 작성하세요"
value={msg}
onChange={handleChangeMsg}
required
></textarea>
</div>
</div>
<div className="form__submit">
<button type="submit">질문 등록하기</button>
</div>
</form>
</section>
);
};
export default Form;
import React, { useState } from 'react';
import { Discussion } from '../TypeDiscussion';
import SingleDiscussion from './SingleDiscussion';
type DiscussionData = {
discussionData: Discussion[];
//원래는 object[]로 타입 설정을 하고 item:object로 타입 설정했더니 item props부분에서 오류가 났다. 해당 키가 없다고 함
//item:Discussion으로 타입 설정했더니 item props는 정상, 아래에 SingleDiscussionRender(el)의 el에서 얘는 타입이 object이고, Discussion타입이 아니다라는 오류발생
//item은 object이면서 discussion타입인데 2가지 타입을 할당할수도 없었는데,,, 해결방법을 찾았다!
//일단 자동으로 el이 object로 타입추론하게 된 이유는 위의 discussionData를 object[]로 타입설정했기때문에,
//아래에서 map으로 풀어줬을 때 el은 object가 되는 것이다.
//그럼 discussionData: Discussion[]로 설정하면 el의 타입은 Discussion이 될것이므로 오류가 안생긴다.
handleDeleteClick:(discussionId: string) => void;
}
//Disscussion컴포넌트 : Q&A컴포넌트로 ul element까지
const DiscussionsRender: React.FC<DiscussionData> = props => {
const SingleDiscussionRender = (item: Discussion) => { //매개변수 item은 아래의 el을 전달인자로 받는다.
return ( //하나의 discussion을 만들기 위해 props로 Discussion의 키들을 내려준다.
<SingleDiscussion
key={item.id}
date={item.createdAt}
title={item.title}
url={item.url}
author={item.author}
imgSrc={item.avatarUrl}
answer={item.answer}
Delete={props.handleDeleteClick.bind(null,item.id)}
/>
);
};
const [show, setShow] = useState(false);
const showDiscussionClick = () => {
//클릭 시 true일때 내려오는 style이 있는 className추가
const ul = document.querySelector("ul")!;
const btn = document.querySelector(".clickDown")!;
const body = document.querySelector("body")!;
if(show===false){
body.classList.add("paddingLeft");
btn.textContent = "질문 닫기";
ul.classList.remove("hidden");
setShow(!show);
}
else{
body.classList.remove("paddingLeft");
btn.textContent = "질문 보기";
ul?.classList.add("hidden");
setShow(!show);
}
}
return (
<section className="discussion__wrapper">
<h2>
Q&A <button className='clickDown' onClick={showDiscussionClick}>질문 보기</button>
</h2>
<ul className="discussions__container hidden">
{props.discussionData.map((el) => SingleDiscussionRender(el))}{/*li태그로 discussion을 하나씩 만들 컴포넌트*/}
</ul>
</section>
);
};
export default DiscussionsRender;
import React from 'react';
import {
AiFillCheckCircle,
AiOutlineCheckCircle,
} from 'react-icons/ai';
//이 컴포넌트들의 style을 바꾸려면 className은 불가, size와 color속성만 가능하다!
interface DiscussionData {
key: string,
date: string,
title: string,
url: string,
author: string,
imgSrc: string,
answer?: object | null,
Delete?: (discussionId: string) => void;
//discussion삭제하는 이벤트핸들러함수
//내 질문은 imgSrc를 전부 하나로 지정해놨으므로, 지정한 url를 가졌다면 삭제함수를 가진 삭제버튼을 렌더링
};
//discussion각각의 li컴포넌트
const SingleDiscussion: React.FC<DiscussionData> = props => {
return (
<li className="discussion__container">
<div className="discussion__avatar--wrapper">
<img
className="discussion__avatar--image"
src={props.imgSrc}
alt={`avatar of ${props.author}`}
/>
</div>
<div className="discussion__content">
<h3 className="discussion__title">
<a href={props.url}>{props.title}</a>
</h3>
<div className="discussion__information">{/*현지시간 구하기*/}
{`${props.author} / ${new Date(props.date).getFullYear()}.${new Date(props.date).getMonth()+1}.${new Date(props.date).getDate()}`}
{props.imgSrc==="https://mblogthumb-phinf.pstatic.net/MjAyMTA3MDJfMjg0/MDAxNjI1MjM3MDEwMDEx.16ZkPZkXZmj6MQyJIpZlTidJmYGFnehv2QoiaIWVHAsg.louS2WVp9f5dzxMHdh1MdS-3bZgOIm68sJhcToobTPAg.JPEG.yyabbj/IMG_3332.JPG?type=w800"
? <button className='deleteBtn' onClick={props.Delete?.bind(null,props.key)}>삭제</button>:null}
</div>
</div>
<div className="discussion__answered">
{props.answer ? <AiFillCheckCircle size="20px" color="rgba(0,113,227,1)"/>:<AiOutlineCheckCircle size="20px" color="rgba(0,0,0,0.3)"/>}
</div>
</li>
);
};
export default SingleDiscussion;
@import url("https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500&display=swap");
@import url('https://fonts.googleapis.com/css2?family=Bitter:wght@900&family=Gaegu:wght@700&family=Lato&family=Lobster&family=Merriweather:wght@900&family=Nanum+Myeongjo&family=Ubuntu&display=swap');
/* reset */
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
li {
list-style-type: none;
}
a {
text-decoration: none;
color: #313132;
}
button {
background: none;
border: 0 none;
}
body {
display: flex;
justify-content: center;
padding: 48px 0;
font-family: "Noto Sans KR", sans-serif;
font-weight: 400;
background-color: rgba(0,0,0,0.05);
overflow-x: hidden;
}
main {
width: 540px;
background: rgba(255,255,255,0.5);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19);
overflow: hidden;
}
/*구름 애니메이션*/
.cloud1{
position:fixed;
top:50px;
left:-100px;
animation: cloud1 27s ease-in-out infinite;
z-index: -1;
width:130px;
}
.cloud2{
position:fixed;
top:500px;
right:-200px;
animation:cloud2 30s ease-in-out infinite;
animation-delay: 2s;
z-index: -1;
width:180px;
}
.cloud3{
position:fixed;
top:800px;
left:-200px;
animation: cloud1 30s ease-in-out infinite;
animation-delay: 5s;
z-index: -1;
width:150px;
}
@keyframes cloud1{
0%{
transform : translateX(-50px);
}
100%{
transform : translateX(2300px);
}
}
@keyframes cloud2{
0%{
transform : translateX(50px);
}
100%{
transform : translateX(-2300px);
}
}
/*header*/
h1{
font-family: 'Lobster', cursive;
width: 100%;
height: 56px;
text-align: center;
line-height: 52px;
background-color: #313132;
color:white;
font-size: 24px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19);
}
/*newDiscussion*/
h2 {
margin: 8px 0;
font-size: 25px;
font-weight: bold;
color: #313132;
}
/* 질문 영역 */
.form__container {
padding: 32px 16px;
}
label {
display: block;
margin-top: 12px;
font-size: 12px;
font-weight: 400;
color: #666666;
}
input,
textarea,
button {
display: block;
width: 100%;
height: 48px;
padding: 0 16px;
margin-top: 8px;
border-radius: 4px;
border: 1px solid #dddddd;
font-family: "Noto Sans KR";
font-size: 16px;
color: black;
}
textarea {
height: 96px;
padding: 16px;
resize: none;
}
input:focus,
textarea:focus{
outline:none;
border: 3px solid rgba(0,113,227,1);
}
/*submit버튼*/
.form__submit {
margin-top: 16px;
}
.form__submit button {
background-color: rgba(0,113,227,1);
border: none;
color: white;
box-shadow: 0px 3px 6px rgb(0 0 0 / 16%);
font-weight: 500;
}
.form__submit button:hover{
cursor: pointer;
background-color: rgba(0,113,227,0.8);
}
/* 질문 목록 영역 */
.clickDown:hover{
cursor: pointer;
border: 3px solid #313132;
}
/*ul*/
.discussions__wrapper {
width:100px;
}
/*li*/
.discussion__wrapper h2 {
padding: 0 16px;
}
.discussion__container {
display: flex;
align-items: center;
padding: 16px;
border-bottom: 1px solid #dddddd;
}
.discussion__avatar--wrapper {
flex-shrink: 0;
width: 48px;
height: 48px;
margin-right: 16px;
border-radius: 50%;
overflow: hidden;
background-color: #9e9e9e;
}
.discussion__avatar--wrapper > img {
width: 100%;
}
.discussion__content {
width: calc(100% - 104px);
margin-right: 16px;
}
.discussion__title {
width: 100%;
line-height: 24px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-size: 16px;
font-weight: 500;
}
.discussion__information {
width: 100%;
line-height: 20px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-size: 12px;
font-weight: 300;
color: #666666;
display: flex;
align-items: baseline;
}
/*내질문 삭제버튼*/
.deleteBtn{
font-size: 9px;
width:30px;
height: 20px;
padding: 0;
margin: 0px;
margin-left: 5px;
}
.deleteBtn:hover{
cursor: pointer;
background-color: #313132;
color:white;
}
.discussion__answered {
flex-shrink: 0;
width: 24px;
height: 24px;
margin-left: auto;
line-height: 24px;
}
/* 페이지네이션 */
.pagination {
display: flex;
justify-content: center;
align-items: center;
margin: 32px 0;
}
.pagination > button {
width: 24px;
height: 24px;
text-align: center;
line-height: 24px;
font-size: 12px;
border-radius: 4px;
}
.pagination > button:hover {
background-color: rgba(0,113,227,0.8);
}
.pagination > button.active {
background-color: rgba(0,113,227,1);
color: white;
}
/*첫화면 로고띄우기*/
@keyframes hideSplashScreen{
0%{
opacity: 1;
}
25%{
opacity: 1;
}
50%{
opacity: 1;
}
75%{
opacity: 1;
}
100%{
opacity: 0;
visibility: hidden;
}
}
.splash-screen{
background-color: #313132;
color:#d6d6d6;
text-align: center;
padding-top: 300px;
position: absolute;
top:0;
left:0;
z-index: 99;
height: 100vh;
width: 100vw;
justify-content: center;
align-items: center;
font-size: 70px;
font-family: 'Lobster', cursive;
animation: hideSplashScreen 1s ease-in-out forwards;
}
/*질문 보기를 클릭하면 사라지고 닫기를 클릭해서 추가되는 클래스*/
.hidden{
height: 0px;
visibility: hidden;
}
/*질문보기/닫기를 클릭하면 body padding이 줄어듦으로 추가하는 클래스*/
.paddingLeft{
padding-left: 17px;
}
GraphQL을 이용하여 아고라스테이츠의 실시간 Disscusion데이터를 받아오는 것까진 완료
(TypeGraphQL 프레임워크, 아폴로, 데코레이터를 이용하면 더 쉽게 GraphQL을 사용할 수 있다고 해서 공부중!)
//App.tsx
import './App.css';
import React, {
useEffect,
useState,
} from 'react';
import { graphql } from '@octokit/graphql';
import DiscussionsRender from './Components/DiscussionsRender';
import Form from './Components/Form';
import { agoraStatesDiscussions } from './Data';
import {
Discussion,
Discussions,
} from './TypeDiscussion';
type query = {
repository:{discussions:{edges:{node:field[]}}};
}
type field = string|Tcategory[]|Tanswer[]|Tauthor[]
type Tcategory = {name: string;}
type Tauthor = {login:string; avatarUtl:string;}
type Tanswer = {author:Tauthor;}
const App = () => {
async function getData(){
const {repository}:query = await graphql(
`{
repository(owner: "codestates-seb", name: "agora-states-fe") {
discussions(first:30) {
edges {
node {
author {
login
avatarUrl
}
createdAt
title
id
url
bodyHTML
answer {
author {
login
avatarUrl
}
}
}
}
}
}
}
`,
{
headers: {
authorization: `token ${My_Token}`,
},
}
);
return {repository};
}
const [repo, setRepo] = useState({});
useEffect(()=>{
getData()
.then((res) => {
setRepo(res.repository);
})
.catch((err) =>{
console.log(err);
})
}, [])
console.log(repo);
/*const [discussionData, setDiscussionData] = useState<Discussions>([...agoraStatesDiscussions]);*/
//localstorage기능 추가
const [discussionData, setDiscussionData] = useState<Discussions>(() =>
JSON.parse(window.localStorage.getItem("discussionData")!) || [...agoraStatesDiscussions]);
useEffect(() => {
window.localStorage.setItem("discussionData", JSON.stringify(discussionData));
}, [discussionData]);
//새로운질문이 추가되면 전체discussions에 추가하기
const handleSubmitClick = (newSingleData: Discussion): void => {
setDiscussionData([newSingleData, ...discussionData]);
};
//삭제클릭한 discussion의 id와 동일한 id를 가진 discussion을 discussion목록에서 지운다.
const handleDeleteClick = (discussionId: string) => {
setDiscussionData(discussion => {
return discussion.filter(discussion => discussion.id !== discussionId);
});
};
return (
<div className='container'>
<div><img className='cloud1' alt="" src="/images/cloud.png"/></div>
<div><img className='cloud2' alt="" src="/images/cloud.png"/></div>
<div><img className='cloud3' alt="" src="/images/cloud.png"/></div>
<main>
<div className="splash-screen">My Agora States</div>{/*첫화면 로고 띄우는 컴포넌트*/}
<h1>My Agora States</h1>{/*header 컴포넌트*/}
<Form handleSubmitClick={handleSubmitClick} />{/*질문하기 컴포넌트*/}
<DiscussionsRender discussionData={discussionData} handleDeleteClick={handleDeleteClick}/>{/*Q&A 컴포넌트*/}
</main>
</div>
);
};
export default App;