🍔 핵심 내용
🥑 comment 부분을 수정 및 추가해주자.
comment 백엔드 부분 일부 내용 수정 및 프론트에 적용해주자.
🍔 코드 리뷰
<백엔드>
🥑 photo.typeDefs.js
...
export default gql`
type Photo {
id: Int!
user: User
file: String!
caption: String
likes: Int!
commentNumber: Int!
comments: [Comment]
isMine: Boolean!
hashtags: [Hashtag]
createdAt: String!
updatedAt: String!
isLiked: Boolean!
}
...
commentNumber 및 comments 변경
🥑 photo.resolvers.js
...
commentNumber: ({ id }) => {
return client.comment.count({ where: { photoId: id } });
},
comments: ({ id }) =>
client.comment.findMany({
where: { photoId: id },
include: { user: true },
}),
...
comment 총 양과 comment 배열 값을 가지고 오는 로직이다. 그리고 user값도 받아 와야 하기 때문에 include 시켜 주었다.
<프론트>
🥑 photo.js
import { gql, useMutation } from "@apollo/client";
import {
faBookmark,
faComment,
faPaperPlane,
faHeart,
} from "@fortawesome/free-regular-svg-icons";
import { faHeart as SolidHeart } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import PropTypes from "prop-types";
import styled from "styled-components";
import Avatar from "../Avatar";
import { FatText } from "../shared";
const TOGGLE_LIKE_MUTATION = gql`
mutation toggleLike($id: Int!) {
toggleLike(id: $id) {
ok
error
}
}
`;
const PhotoContainer = styled.div`
background-color: white;
border-radius: 4px;
border: 1px solid ${(props) => props.theme.borderColor};
margin-bottom: 60px;
max-width: 615px;
`;
const PhotoHeader = styled.div`
padding: 15px;
display: flex;
align-items: center;
border-bottom: 1px solid rgb(239, 239, 239);
`;
const Username = styled(FatText)`
margin-left: 15px;
`;
const PhotoFile = styled.img`
min-width: 100%;
max-width: 100%;
`;
const PhotoData = styled.div`
padding: 12px 15px;
`;
const PhotoActions = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
div {
display: flex;
align-items: center;
}
svg {
font-size: 20px;
}
`;
const PhotoAction = styled.div`
margin-right: 10px;
cursor: pointer;
`;
const Likes = styled(FatText)`
margin-top: 15px;
display: block;
`;
const Comments = styled.div`
margin-top: 20px;
`;
const Comment = styled.div``;
const CommentCaption = styled.span`
margin-left: 10px;
`;
const CommentCount = styled.span`
opacity: 0.7;
margin: 10px 0px;
display: block;
font-weight: 600;
font-size: 10px;
`;
function Photo({
id,
user,
file,
isLiked,
likes,
caption,
commentNumber,
comments,
}) {
const updateToggleLike = (cache, result) => {
const {
data: {
toggleLike: { ok },
},
} = result;
if (ok) {
const fragmentId = `Photo:${id}`;
const fragment = gql`
fragment BSName on Photo {
isLiked
likes
}
`;
const result = cache.readFragment({
id: fragmentId,
fragment,
});
if ("isLiked" in result && "likes" in result) {
const { isLiked: cacheIsLiked, likes: cacheLikes } = result;
cache.writeFragment({
id: fragmentId,
fragment,
data: {
isLiked: !cacheIsLiked,
likes: cacheIsLiked ? cacheLikes - 1 : cacheLikes + 1,
},
});
}
}
};
const [toggleLikeMutation] = useMutation(TOGGLE_LIKE_MUTATION, {
variables: {
id,
},
update: updateToggleLike,
});
return (
<PhotoContainer key={id}>
<PhotoHeader>
<Avatar lg url={user.avatar} />
<Username>{user.userName}</Username>
</PhotoHeader>
<PhotoFile src={file} />
<PhotoData>
<PhotoActions>
<div>
<PhotoAction onClick={toggleLikeMutation}>
<FontAwesomeIcon
style={{ color: isLiked ? "tomato" : "inherit" }}
icon={isLiked ? SolidHeart : faHeart}
/>
</PhotoAction>
<PhotoAction>
<FontAwesomeIcon icon={faComment} />
</PhotoAction>
<PhotoAction>
<FontAwesomeIcon icon={faPaperPlane} />
</PhotoAction>
</div>
<div>
<FontAwesomeIcon icon={faBookmark} />
</div>
</PhotoActions>
<Likes>{likes === 1 ? "1 like" : `${likes} likes`}</Likes>
<Comments>
<Comment>
<FatText>{user.username}</FatText>
<CommentCaption>{caption}</CommentCaption>
</Comment>
<CommentCount>
{commentNumber === 1 ? "1 comment" : `${commentNumber} comments`}
</CommentCount>
</Comments>
</PhotoData>
</PhotoContainer>
);
}
Photo.propTypes = {
id: PropTypes.number.isRequired,
user: PropTypes.shape({
avatar: PropTypes.string,
userName: PropTypes.string.isRequired,
}),
caption: PropTypes.string,
file: PropTypes.string.isRequired,
isLiked: PropTypes.bool.isRequired,
likes: PropTypes.number.isRequired,
commentNumber: PropTypes.number.isRequired,
comments: PropTypes.arrayOf(PropTypes.shape({})),
};
export default Photo;
comment 부분 추가 진행 중이다. 코드 줄이 너무 길어지기 때문에 comment를 별도 컴포넌트로 분리해줄것이다.
🥑 Home.js
import { gql, useQuery } from "@apollo/client";
import Photo from "../components/feed/Photo";
import PageTitle from "../components/PageTitle";
const FEED_QUERY = gql`
query seeFeed {
seeFeed {
id
user {
userName
avatar
}
file
caption
likes
comments {
id
user {
userName
avatar
}
payload
isMine
createdAt
}
commentNumber
createdAt
isMine
isLiked
}
}
`;
function Home() {
const { data } = useQuery(FEED_QUERY);
return (
<div>
<PageTitle title="Home" />
{data?.seeFeed?.map((photo) => (
<Photo key={photo.id} {...photo} />
))}
</div>
);
}
export default Home;
commentNumber와 comments 추가
🍔 핵심 내용
🥑 comment를 컴포넌트화 해주자.
내용이 너무 길어지기 때문에, 컴포넌트화 한다.
Photo.js 에서 Comments를 쓰고 Comments에서 Comment를 사용한다.
🍔 코드 리뷰
🥑 components/feed/Comments.js
import PropTypes from "prop-types";
import styled from "styled-components";
import Comment from "./Comment";
const CommentsContainer = styled.div`
margin-top: 20px;
`;
const CommentCount = styled.span`
opacity: 0.7;
margin: 10px 0px;
display: block;
font-weight: 600;
font-size: 10px;
`;
function Comments({ author, caption, commentNumber, comments }) {
return (
<CommentsContainer>
<Comment author={author} payload={caption} />
<CommentCount>
{commentNumber === 1 ? "1 comment" : `${commentNumber} comments`}
</CommentCount>
{comments?.map((comment) => (
<Comment
key={comment.id}
author={comment.user.username}
payload={comment.payload}
/>
))}
</CommentsContainer>
);
}
Comments.propTypes = {
author: PropTypes.string.isRequired,
caption: PropTypes.string,
commentNumber: PropTypes.number.isRequired,
comments: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
user: PropTypes.shape({
avatar: PropTypes.string,
username: PropTypes.string.isRequired,
}),
payload: PropTypes.string.isRequired,
isMine: PropTypes.bool.isRequired,
createdAt: PropTypes.string.isRequired,
})
),
};
export default Comments;
🥑 components/feed/Comment.js
import PropTypes from "prop-types";
import styled from "styled-components";
import { FatText } from "../shared";
const CommentContainer = styled.div``;
const CommentCaption = styled.span`
margin-left: 10px;
`;
function Comment({ author, payload }) {
return (
<CommentContainer>
<FatText>{author}</FatText>
<CommentCaption>{payload}</CommentCaption>
</CommentContainer>
);
}
Comment.propTypes = {
author: PropTypes.string.isRequired,
payload: PropTypes.string.isRequired,
};
export default Comment;
🥑 Photo.js
import { gql, useMutation } from "@apollo/client";
import {
faBookmark,
faComment,
faPaperPlane,
faHeart,
} from "@fortawesome/free-regular-svg-icons";
import { faHeart as SolidHeart } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import PropTypes from "prop-types";
import styled from "styled-components";
import Avatar from "../Avatar";
import { FatText } from "../shared";
import Comments from "./Comments";
const TOGGLE_LIKE_MUTATION = gql`
mutation toggleLike($id: Int!) {
toggleLike(id: $id) {
ok
error
}
}
`;
const PhotoContainer = styled.div`
background-color: white;
border-radius: 4px;
border: 1px solid ${(props) => props.theme.borderColor};
margin-bottom: 60px;
max-width: 615px;
`;
const PhotoHeader = styled.div`
padding: 15px;
display: flex;
align-items: center;
border-bottom: 1px solid rgb(239, 239, 239);
`;
const Username = styled(FatText)`
margin-left: 15px;
`;
const PhotoFile = styled.img`
min-width: 100%;
max-width: 100%;
`;
const PhotoData = styled.div`
padding: 12px 15px;
`;
const PhotoActions = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
div {
display: flex;
align-items: center;
}
svg {
font-size: 20px;
}
`;
const PhotoAction = styled.div`
margin-right: 10px;
cursor: pointer;
`;
const Likes = styled(FatText)`
margin-top: 15px;
display: block;
`;
function Photo({
id,
user,
file,
isLiked,
likes,
caption,
commentNumber,
comments,
}) {
const updateToggleLike = (cache, result) => {
const {
data: {
toggleLike: { ok },
},
} = result;
if (ok) {
const fragmentId = `Photo:${id}`;
const fragment = gql`
fragment BSName on Photo {
isLiked
likes
}
`;
const result = cache.readFragment({
id: fragmentId,
fragment,
});
if ("isLiked" in result && "likes" in result) {
const { isLiked: cacheIsLiked, likes: cacheLikes } = result;
cache.writeFragment({
id: fragmentId,
fragment,
data: {
isLiked: !cacheIsLiked,
likes: cacheIsLiked ? cacheLikes - 1 : cacheLikes + 1,
},
});
}
}
};
const [toggleLikeMutation] = useMutation(TOGGLE_LIKE_MUTATION, {
variables: {
id,
},
update: updateToggleLike,
});
return (
<PhotoContainer key={id}>
<PhotoHeader>
<Avatar lg url={user.avatar} />
<Username>{user.username}</Username>
</PhotoHeader>
<PhotoFile src={file} />
<PhotoData>
<PhotoActions>
<div>
<PhotoAction onClick={toggleLikeMutation}>
<FontAwesomeIcon
style={{ color: isLiked ? "tomato" : "inherit" }}
icon={isLiked ? SolidHeart : faHeart}
/>
</PhotoAction>
<PhotoAction>
<FontAwesomeIcon icon={faComment} />
</PhotoAction>
<PhotoAction>
<FontAwesomeIcon icon={faPaperPlane} />
</PhotoAction>
</div>
<div>
<FontAwesomeIcon icon={faBookmark} />
</div>
</PhotoActions>
<Likes>{likes === 1 ? "1 like" : `${likes} likes`}</Likes>
<Comments
author={user.username}
caption={caption}
commentNumber={commentNumber}
comments={comments}
/>
</PhotoData>
</PhotoContainer>
);
}
Photo.propTypes = {
id: PropTypes.number.isRequired,
user: PropTypes.shape({
avatar: PropTypes.string,
username: PropTypes.string.isRequired,
}),
caption: PropTypes.string,
file: PropTypes.string.isRequired,
isLiked: PropTypes.bool.isRequired,
likes: PropTypes.number.isRequired,
commentNumber: PropTypes.number.isRequired,
};
export default Photo;
🍔 핵심 내용
🥑 hashtag 기능을 활성화 시켜보자.
1) 정규표현식으로 내가 원하는 문자 구문을 따온다.
2) 해당 문자 구문 앞뒤로 html 태그를 붙여 넣는다.
3) html 태그가 바로 적용되면 위험하기 때문에 sanitizeHtml 를 설치하여, 내가 원하는 태그명만 보여지게 만든다.
큰 개념은 위와 같다. 하지만 이렇게 하게 되면 결국 링크를 만들어야 하는데, 최종적으로 a href ~~ 가 들어가야 하고 a 링크를 허용하게 되면 결국 사용자가 임의로 뭔가 만들 수 있게 된다. 이러면 보안적으로 또 문제가 되기 때문에 다른 방법을 알아 볼 것이다. 이번 내용은 그냥 이런것도 있구나 하고 개념만 보고 넘어가자.
🍔 코드 리뷰
🥑 components/feed/Comment.js
import sanitizeHtml from "sanitize-html";
import PropTypes from "prop-types";
import styled from "styled-components";
import { FatText } from "../shared";
const CommentContainer = styled.div``;
const CommentCaption = styled.span`
margin-left: 10px;
mark {
background-color: inherit;
color: ${(props) => props.theme.accent};
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
`;
function Comment({ author, payload }) {
const cleanedPayload = sanitizeHtml(
payload.replace(/#[\w]+/g, "<mark>$&</mark>"),
{
allowedTags: ["mark"],
}
);
return (
<CommentContainer>
<FatText>{author}</FatText>
<CommentCaption
dangerouslySetInnerHTML={{
__html: cleanedPayload,
}}
/>
</CommentContainer>
);
}
Comment.propTypes = {
author: PropTypes.string.isRequired,
payload: PropTypes.string.isRequired,
};
export default Comment;
(정규 표현식 한글 버전은 다르게 해야함 위 코드는 영문버전)
🍔 핵심 내용
🥑 hashtag 기능을 활성화 시켜보자 두번째. 최종
자바스크립트 기능을 이용해서 진행해 보자.
1) split(" ") 를 사용하여 긴 문장의 각 각의 단어를 배열로 만들어 준다. 그리고 map 함수를 활용하여 배열에 담겨 있는
각 값에, 조건을 걸어 줄 것이다. (참고로 map의 두번째 인자 값을 index 값이다)
2) 각 각의 단어중에 내가 원하는 단어를 가지고 오기 위해 정규표현식을 사용한다.
3) 삼항조건연산자를 활용하여, 내가 원하는 단어가 들어가면(#해쉬태그) 링크걸어주고, 안들어가면 그냥 단어로 보여준다.
4) <> </> 로 하게되면 key 값을 넣을 수 없기 때문에 React.Fragment로 감싸준다.
🍔 코드 리뷰
🥑 components/feed/Comment.js
import React from "react";
import PropTypes from "prop-types";
import styled from "styled-components";
import { FatText } from "../shared";
import { Link } from "react-router-dom";
const CommentContainer = styled.div``;
const CommentCaption = styled.span`
margin-left: 10px;
a {
background-color: inherit;
color: ${(props) => props.theme.accent};
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
`;
function Comment({ author, payload }) {
return (
<CommentContainer>
<FatText>{author}</FatText>
<CommentCaption>
{payload.split(" ").map((word, index) =>
/#[\w]+/.test(word) ? (
<React.Fragment key={index}>
<Link to={`/hashtags/${word}`}>{word}</Link>{" "}
</React.Fragment>
) : (
<React.Fragment key={index}>{word} </React.Fragment>
)
)}
</CommentCaption>
</CommentContainer>
);
}
Comment.propTypes = {
author: PropTypes.string.isRequired,
payload: PropTypes.string.isRequired,
};
export default Comment;
🍔 핵심 내용
🥑 cache 부분 재수정 - 매우 쉽게
이전에 read, write 캐쉬 부분을 더 쉽게 수정해 줄 것이다. apollo3 에서 새롭게 업데이트된 내용이다.
modify 함수를 활용하여 id값과 업데이트할 field명만 넣으면 된다. 이때 filed명에 들어가는 인자 값은 이전 필드명에 대한 결과 값이다.
🍔 코드 리뷰
🥑Photo.js
...
function Photo({
id,
user,
file,
isLiked,
likes,
caption,
commentNumber,
comments,
}) {
const updateToggleLike = (cache, result) => {
const {
data: {
toggleLike: { ok },
},
} = result;
if (ok) {
const photoId = `Photo:${id}`;
cache.modify({
id: photoId,
fields: {
isLiked(prev) {
return !prev;
},
likes(prev) {
if (isLiked) {
return prev - 1;
}
return prev + 1;
},
},
});
}
};
...
'코딩강의 > 인스타그램클론(expo-노마드코더)' 카테고리의 다른 글
Profile (0) | 2021.06.20 |
---|---|
FEED (3) (0) | 2021.06.16 |
FEED (1) (0) | 2021.05.26 |
LOGIN AND SIGNUP (2) (0) | 2021.05.05 |
LOGIN AND SIGNUP (1) (0) | 2021.04.28 |