From fdb1e8eeae379bea7c9cd48bee8470d5b140b494 Mon Sep 17 00:00:00 2001 From: yeon Date: Wed, 27 Nov 2019 15:33:15 +0900 Subject: [PATCH] =?UTF-8?q?[Add]=20like=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20#32?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why like 버튼 동작 시 디비에 저장되어야 함 how - cookie로 사용자 정보 캐치 - 피드 정보에 feedid추가 - feedId로 해당 피드와 사용자 간의 like relation 연결 (중복으로 되지 않도록 merge 로 연결) - cors 설정 참고 unlike는 아직 안함 --- client/src/apollo/ApolloClient.ts | 6 +- client/src/composition/Feed/Feed.tsx | 12 ++- client/src/composition/Feed/FeedContainer.tsx | 15 ++-- client/src/composition/Feed/FeedFooter.tsx | 28 +++++- client/src/composition/Feed/feed.type.ts | 41 +++++++++ client/src/composition/Feed/index.tsx | 88 +++++++++++++------ server/src/api/feed/feed.graphql | 21 +++-- server/src/api/feed/feed.resolvers.ts | 38 ++++++-- server/src/app.ts | 26 +++++- server/src/init.ts | 20 +++-- server/src/schema/feed/query.ts | 28 +++++- 11 files changed, 260 insertions(+), 63 deletions(-) create mode 100644 client/src/composition/Feed/feed.type.ts diff --git a/client/src/apollo/ApolloClient.ts b/client/src/apollo/ApolloClient.ts index 5664e3ba..21d78d1c 100644 --- a/client/src/apollo/ApolloClient.ts +++ b/client/src/apollo/ApolloClient.ts @@ -7,7 +7,11 @@ const cache = new InMemoryCache(); const client = new ApolloClient({ cache, - link: createUploadLink({ uri: 'http://localhost:4000/graphql' }), + link: createUploadLink({ + uri: 'http://localhost:4000/graphql', + credentials: 'include', + fetchOptions: { credentials: 'include' } + }), typeDefs, resolvers }); diff --git a/client/src/composition/Feed/Feed.tsx b/client/src/composition/Feed/Feed.tsx index 10a4c8f2..8e3fa754 100644 --- a/client/src/composition/Feed/Feed.tsx +++ b/client/src/composition/Feed/Feed.tsx @@ -1,9 +1,10 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; import FeedHeader from './FeedHeader'; import FeedBody from './FeedBody'; import FeedFooter from './FeedFooter'; import Comment from './Comment'; +import { IFeedItem } from './feed.type'; const FeedDiv = styled.div` ${props => props.theme.borders.feedBorder}; @@ -30,11 +31,17 @@ const FeedEditDiv = styled.span` interface Iprops { content: string; createdAt: string; + feedinfo: IFeedItem; } -function Feed({ content, createdAt }: Iprops) { +function Feed({ content, createdAt, feedinfo }: Iprops) { const [likeCnt, setLikeCnt] = useState(0); const [hasLiked, setHasLiked] = useState(false); + useEffect(() => { + setLikeCnt(feedinfo.totallikes); + setHasLiked(feedinfo.hasLiked ? true : false); + }, []); + return ( <> @@ -47,6 +54,7 @@ function Feed({ content, createdAt }: Iprops) { setLikeCnt={setLikeCnt} hasLiked={hasLiked} setHasLiked={setHasLiked} + feedId={feedinfo.feedId} /> diff --git a/client/src/composition/Feed/FeedContainer.tsx b/client/src/composition/Feed/FeedContainer.tsx index d203c332..061314f0 100644 --- a/client/src/composition/Feed/FeedContainer.tsx +++ b/client/src/composition/Feed/FeedContainer.tsx @@ -1,13 +1,18 @@ import React from 'react'; -import FeedPresentor from './FeedPresentor'; +import FeedList from './index'; import WritingFeedContainer from './WritingFeed'; +import styled from 'styled-components'; -const FeedContainer: React.FC = () => { +const CenterContainer = styled.div` + margin: 0 auto; +`; + +const FeedContainer = () => { return ( - <> + - - + + ); }; diff --git a/client/src/composition/Feed/FeedFooter.tsx b/client/src/composition/Feed/FeedFooter.tsx index 44f6e966..0e6ecfd3 100644 --- a/client/src/composition/Feed/FeedFooter.tsx +++ b/client/src/composition/Feed/FeedFooter.tsx @@ -4,6 +4,8 @@ import ThumbLikeIcon from '../../components/Icon/ThumbLikeIcon'; import RoundThumbIcon from '../../components/Icon/RoundThumbIcon'; import CommentIcon from '../../components/Icon/CommentIcon'; import ShareIcon from '../../components/Icon/ShareIcon'; +import { useMutation } from '@apollo/react-hooks'; +import gql from 'graphql-tag'; const FeedActionDiv = styled.div` border-radius: 0 0 3px 3px; @@ -64,18 +66,42 @@ interface Iprops { setLikeCnt: any; hasLiked: boolean; setHasLiked: any; + feedId: number; } -const FeedFooter = ({ likeCnt, setLikeCnt, hasLiked, setHasLiked }: Iprops) => { + +const SEND_LIKE = gql` + mutation updateLike($feedId: Int) { + updateLike(feedId: $feedId) + } +`; + +const FeedFooter = ({ + likeCnt, + setLikeCnt, + hasLiked, + setHasLiked, + feedId +}: Iprops) => { + const [updateLike] = useMutation(SEND_LIKE); + const ToggleLike = () => { setLikeCnt((props: number) => { setHasLiked((props: boolean) => !props); if (!hasLiked) { + sendLike(1); return props + 1; } else { + // cancleLike(-1); return props - 1; } }); }; + + const sendLike = (count: number) => { + // send count + updateLike({ variables: { feedId } }); + console.log(count); + }; return ( <> diff --git a/client/src/composition/Feed/feed.type.ts b/client/src/composition/Feed/feed.type.ts new file mode 100644 index 00000000..4e973d9d --- /dev/null +++ b/client/src/composition/Feed/feed.type.ts @@ -0,0 +1,41 @@ +export interface Feeds { + feedItems: IFeedItem[]; +} + +export interface Idate { + year: number; + month: number; + day: number; + hour: number; + minute: number; + second: number; + nanosecond: number; + timeZoneOffsetSeconds: number; + timeZoneId: string; +} + +export interface IUser { + nickname: string; + hometown: string; + thumbnail: string; + residence: string; + email: string; +} +export interface Content { + createdAt: Idate; + content: string; +} + +export interface Comment { + id: string; + content: string; +} + +export interface IFeedItem { + searchUser: IUser; + feed: Content; + feedId: number; + totallikes: number; + hasLiked: number; + comments: [Comment]; +} diff --git a/client/src/composition/Feed/index.tsx b/client/src/composition/Feed/index.tsx index 99562e58..1c5d80b3 100644 --- a/client/src/composition/Feed/index.tsx +++ b/client/src/composition/Feed/index.tsx @@ -2,19 +2,9 @@ import React, { useState, useRef, useEffect, useCallback } from 'react'; import Feed from './Feed'; import { useQuery } from '@apollo/react-hooks'; import gql from 'graphql-tag'; -import useScrollEnd from '../../hooks/useScrollEnd'; -import WritingFeedContainer from './WritingFeed'; import useIntersect from '../../hooks/useIntersectObserver'; import styled from 'styled-components'; - -interface IFeed { - content: string; - createdAt: string; -} - -interface Feeds { - feeds: IFeed[]; -} +import { Feeds, Idate, IFeedItem } from './feed.type'; interface FeedVars { first: number; @@ -30,15 +20,57 @@ const GET_FEEDS = gql` } `; +const GET_FEEDS2 = gql` + query getfeeds($first: Int, $currentCursor: String) { + feedItems(first: $first, cursor: $currentCursor) { + searchUser { + nickname + } + feed { + createdAt { + year + month + day + hour + minute + second + nanosecond + } + content + } + feedId + totallikes + hasLiked + comments { + id + content + } + } + } +`; + const LoadCheckContainer = styled.div` height: 50px; position: relative; top: -50px; `; +// 모듈로 빼자 new Date(year, month, day, hours, minutes, seconds, milliseconds) +const getDate = (date: Idate): Date => { + const dateob = new Date( + date.year, + date.month - 1, + date.day, + date.hour + 9, + date.minute, + date.second, + Number(String(date.nanosecond).substr(0, 3)) + ); + return dateob; +}; const OFFSET = 4; -const FeedContainer = () => { - const [feeds, setFeeds] = useState([]); +const FeedList = () => { + const [feeds, setFeeds] = useState([]); const [cursor, setCursor] = useState('9999-12-31T09:29:26.050Z'); const [isLoading, setIsLoading] = useState(false); const [isEnd, setIsEnd] = useState(false); @@ -46,7 +78,7 @@ const FeedContainer = () => { const [ref, setRef] = useIntersect(checkIsEnd, {}); // hooks 에서 useQuery 1 부터 시작 - const { loading, data, fetchMore } = useQuery(GET_FEEDS, { + const { loading, data, fetchMore } = useQuery(GET_FEEDS2, { variables: { first: OFFSET, currentCursor: cursor } }); @@ -61,15 +93,20 @@ const FeedContainer = () => { if (!fetchMoreResult) { return prev; } - if (!fetchMoreResult.feeds.length) { + console.log('cursor ', cursor); + if (!fetchMoreResult.feedItems.length) { setIsEnd(true); return prev; } - const { feeds: feedItems } = fetchMoreResult; + const { feedItems } = fetchMoreResult; const lastFeedItem = feedItems[feedItems.length - 1]; - // console.log('lastFeedItem ', fetchMoreResult); - setCursor(lastFeedItem.createdAt); + console.log( + 'lastFeedItem ', + getDate(lastFeedItem.feed.createdAt).toISOString() + ); + + setCursor(getDate(lastFeedItem.feed.createdAt).toISOString()); return Object.assign({}, prev, { feeds: [...feedItems] @@ -77,7 +114,7 @@ const FeedContainer = () => { } }); - setFeeds([...feeds, ...value.feeds]); + setFeeds([...feeds, ...value.feedItems]); setIsLoading(false); } @@ -89,23 +126,24 @@ const FeedContainer = () => { return ( <> - {feeds.map(feed => ( ))} -
+
{isLoading ? 'LOADING' : ''} {isEnd ? '마지막 글입니다' : ''} + aa
); }; -export default FeedContainer; +export default FeedList; diff --git a/server/src/api/feed/feed.graphql b/server/src/api/feed/feed.graphql index d10c4097..536ee170 100644 --- a/server/src/api/feed/feed.graphql +++ b/server/src/api/feed/feed.graphql @@ -22,18 +22,11 @@ type Idate { hour: Int minute: Int second: Int - nanosecond: Int + nanosecond: String timeZoneOffsetSeconds: Int timeZoneId: String } -type Query { - getFeeds: [Feed]! - feeds(first: Int, cursor: String): [Feed]! - pageInfo: PageInfo - feedItems(first: Int, cursor: String): [IFeed] -} - type IUser { nickname: String hometown: String @@ -54,7 +47,19 @@ type Comment { type IFeed { searchUser: IUser feed: Content + feedId: Int totallikes: Int hasLiked: Int comments: [Comment] } + +type Query { + getFeeds: [Feed]! + feeds(first: Int, cursor: String): [Feed]! + pageInfo: PageInfo + feedItems(first: Int, cursor: String): [IFeed] +} + +type Mutation { + updateLike(feedId: Int): Boolean +} diff --git a/server/src/api/feed/feed.resolvers.ts b/server/src/api/feed/feed.resolvers.ts index a01301ec..048a4103 100644 --- a/server/src/api/feed/feed.resolvers.ts +++ b/server/src/api/feed/feed.resolvers.ts @@ -1,5 +1,9 @@ import db from '../../db'; -import { MATCH_NEW_FEEDS, MATCH_FEEDS } from '../../schema/feed/query'; +import { + MATCH_NEW_FEEDS, + MATCH_FEEDS, + UPDATE_LIKE +} from '../../schema/feed/query'; import { IKey } from '../../schema/commonTypes'; import neo4j from 'neo4j-driver'; @@ -35,7 +39,9 @@ const Datetransform = object => { if (object.hasOwnProperty(property)) { const propertyValue = object[property]; if (neo4j.isInt(propertyValue)) { - returnobj[property] = Number(propertyValue); + // console.log(neo4j.integer.toNumber(propertyValue)); + // console.log('tet', propertyValue); + returnobj[property] = String(propertyValue); } else if (toString.call(propertyValue) === '[object String]') { let temp = {}; temp[property] = propertyValue; @@ -65,7 +71,8 @@ const ParseResultRecords = records => { arr = { ...arr, ...temp }; } else if (node instanceof neo4j.types.Integer) { const temp = {}; - temp[nodeKey] = Number(node); + + temp[nodeKey] = String(node); arr = { ...arr, ...temp }; } else if (toString.call(node) === '[object Array]') { let temp: { [key: string]: any } = {}; @@ -97,11 +104,30 @@ export default { const parsedResult = parseResult(result.records); return parsedResult; }, - feedItems: async (_, { first, cursor = DEFAUT_MAX_DATE }: IPageParam) => { + feedItems: async (_, { first, cursor }: IPageParam) => { + console.log('---cursor1 ', cursor); const result = await session.run(MATCH_FEEDS, { cursor, first }); - // console.log(result.records); - // console.log('INPUT VAL : ', result.records); return ParseResultRecords(result.records); } + }, + + Mutation: { + updateLike: async (_, { feedId }, { req }) => { + let userEmail1 = 'dasom@naver.com'; + if (!req.user) { + console.log('사용자 정보가 없습니다 다시 로그인해 주세요'); + return false; + } + userEmail1 = req.user.email; + console.log(req.user); + + const result = await session.run(UPDATE_LIKE, { + useremail: userEmail1, + feedId + }); + + console.log('true', result); + return true; + } } }; diff --git a/server/src/app.ts b/server/src/app.ts index 1adbc673..002abdea 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -6,7 +6,7 @@ import logger from 'morgan'; import { signInWithEmail, checkToken } from './middleware/authController'; import passport from './middleware/passport'; import schema from './schema'; - +import { decodeJWT } from './utils/jwt'; const PRODUCTION: string = 'PRODUCTION'; const NODE_ENV: string = process.env.NODE_ENV || ''; const LOCAL_CLIENT_HOST_ADDRESS = process.env.LOCAL_CLIENT_HOST_ADDRESS || ''; @@ -20,12 +20,20 @@ const CLIENT_HOST_ADDRESS: string = class App { public app: GraphQLServer; constructor() { - this.app = new GraphQLServer({ schema }); + this.app = new GraphQLServer({ + schema, + context: ({ response, request }) => ({ res: response, req: request }) + }); this.middlewares(); } private middlewares = (): void => { - this.app.express.use(cors()); + this.app.express.use( + cors({ + credentials: true, + origin: 'http://localhost:3000' + }) + ); this.app.express.use(logger('dev')); this.app.express.use(helmet()); this.app.express.use(cookieParser()); @@ -42,6 +50,18 @@ class App { }), signInWithEmail ); + // 쿠키에서 토큰 있는지 확인하고 + // 이메일을 request객체에 넣는다 + this.app.express.use((req, res, next) => { + console.log('token!!!', req.cookies); + if (req.cookies.token) { + const token = req.cookies.token; + const email = decodeJWT(token); + console.log('token!!!', email); + req['user'] = email; + } + return next(); + }); }; } diff --git a/server/src/init.ts b/server/src/init.ts index 799fd9f9..00d8c01f 100644 --- a/server/src/init.ts +++ b/server/src/init.ts @@ -1,19 +1,23 @@ -import dotenv from "dotenv"; +import dotenv from 'dotenv'; dotenv.config(); -import { Options } from "graphql-yoga"; +import { Options } from 'graphql-yoga'; -import app from "./app"; -import "./db"; +import app from './app'; +import './db'; const PORT: string | number = process.env.PORT || 4000; -const ENDPOINT: string = "/graphql"; -const PLAYGROUND: string = "/playground"; - +const ENDPOINT: string = '/graphql'; +const PLAYGROUND: string = '/playground'; +const corsOptions = { + origin: 'http://localhost:3000', + credentials: true +}; const appOptions: Options = { port: PORT, endpoint: ENDPOINT, - playground: PLAYGROUND + playground: PLAYGROUND, + cors: corsOptions }; const handleStart = () => { diff --git a/server/src/schema/feed/query.ts b/server/src/schema/feed/query.ts index 25c00c94..081bc6ca 100644 --- a/server/src/schema/feed/query.ts +++ b/server/src/schema/feed/query.ts @@ -9,12 +9,32 @@ OPTIONAL MATCH (likeUser:User)-[:LIKE]->(feed) OPTIONAL MATCH (feed)-[:HAS]->(com:Comment) WITH searchUser, feed, COLLECT(likeUser) AS cp , COLLECT(com) as comments where feed.createdAt < datetime({cursor}) -RETURN searchUser , feed , length(cp) AS totallikes, -length(filter(x IN cp WHERE x.email='dasom@naver.com')) AS hasLiked, comments -LIMIT {first} `; +RETURN searchUser , feed, ID(feed) as feedId , length(cp) AS totallikes, +length(filter(x IN cp WHERE x.email='vantovan7414@gmail.com')) AS hasLiked, comments +order by feed.createdAt desc +LIMIT {first} +`; const MATCH_NEW_FEEDS2 = `MATCH (user:User { email: 'abc@naver.com' })-[:AUTHOR]->(feed:Feed) where feed.createdAt < datetime('9999-12-31T09:29:26.050Z') RETURN feed`; -export { MATCH_NEW_FEEDS, MATCH_NEW_FEEDS2, MATCH_FEEDS }; +const UPDATE_LIKE = ` +MATCH (u:User),(f:Feed) +WHERE u.email = {useremail} AND ID(f) = {feedId} +MERGE (u)-[r:LIKE]->(f) +RETURN type(r)`; + +const DELETE_LIKE = ` +MATCH (u:User),(f:Feed) +WHERE u.email = {useremail} AND ID(f) = {feedId} +MERGE (u)-[r:LIKE]->(f) +RETURN type(r)`; + +export { + MATCH_NEW_FEEDS, + MATCH_NEW_FEEDS2, + MATCH_FEEDS, + UPDATE_LIKE, + DELETE_LIKE +};