GraphQL도 REST API와 마찬가지로 JSON 데이터로 Client 서버와 통신합니다. 따라서 multer를 이용해서 로직을 구현해봅시다.
-
app.js
-
post-imageput 요청 (auth 같이 설정하기) GraphQL에는 별도의 라우터가 존재하지 않습니다 .따라서 app.js에 기존 REST API 방식처럼 라우터를 추가해줍니다.app.use(auth); app.put('/post-image', (req, res, next) => { if (!req.isAuth) { throw new Error('Not authentication!'); } // 1. 파일이 보내졌는지 확인하기 (업데이트 할 때 새 파일을 안넣어도 되기 때문에 200 code을 보낸다.) if (!req.file) { return res.status(200).json({ message: 'No file provided!' }); } if (req.body.oldPath) { clearImage(req.body.oldPath); } return res .status(201) .json({ message: 'File stored.', filePath: req.file.path }); });
-
clearImage 함수 추가하기
const clearImage = filePath => { filePath = path.join(__dirname, '..', filePath); fs.unlink(filePath, err => console.log(err)); };
-
-
Front: .../Feed.js
-
finishEditHandler수정하기finishEditHandler = (postData) => { this.setState({ editLoading: true, }); // 1. fromData 객체 생성 const formData = new FormData(); formData.append('image', postData.image); if (this.state.editPost) { formData.append('oldPath', this.state.editPost.imagePath); } // 2. PUT 요청으로 BackEnd로 formData에 사진 넣어서 보내기 fetch('http://localhost:8080/post-image', { method: 'PUT', headers: { Authorization: 'Bearer ' + this.props.token, }, body: formData, }) .then((res) => res.json()) // 3. BackEnd 서버로 새로 생성한(업데이트한) post 쿼리 보내기 .then((fileResData) => { console.log(fileResData); const imageUrl = fileResData.filePath; const graphqlQuery = { query: ` mutation { createPost(postInput: {title: "${postData.title}", content: "${postData.contetn}", imageUrl: "${imageUrl}"}) { _id title content imageUrl creator { name } createdAt } } `, }; return fetch('http://localhost:8080/graphql', { method: 'POST', body: JSON.stringify(graphqlQuery), headers: { Authorization: 'Bearer ' + this.props.token, 'Content-Type': 'application/json', }, }); }) .then((res) => { return res.json(); }) .then((resData) => { if (resData.errors && resData.errors[0].status === 422) { throw new Error( "Validation failed. Make sure the email address isn't used yet!" ); } if (resData.errors) { throw new Error('Create user failed!'); } console.log(resData); const post = { _id: resData.data.createPost._id, title: resData.data.createPost.title, content: resData.data.createPost.content, creator: resData.data.createPost.creator, createdAt: resData.data.createPost.createdAt, imagePath: resData.data.createPost.imageUrl, }; this.setState((prevState) => { let updatedPosts = [...prevState.posts]; if (prevState.editPost) { const postIndex = prevState.posts.findIndex( (p) => p._id === prevState.editPosts._id ); updatedPosts[postIndex] = post; } else { updatedPosts.pop(); updatedPosts.unshift(post); } return { posts: updatedPosts, isEditing: false, editPost: null, editLoading: false, }; }); }) .catch((err) => { console.log(err); this.setState({ isEditing: false, editPost: null, editLoading: false, error: err, }); }); };
-
-
graphql/schema.js
-
쿼리 만들기
` type RootQuery { login(email: String!, password: String!): AuthData! posts(page:Int): PostData! getPost(id: ID!): Post! } `
-
-
graphql/resolvers.js
-
getPostresolver 만들기getPost: async function ({ id }, req) { if (!req.isAuth) { const error = new Error('권한이 없습니다.'); error.code = 401; throw error; } console.log('여기 통과'); const post = await Post.findById(id).populate('creator'); if (!post) { const error = new Error('No post found!'); error.code = 404; throw error; } return { ...post._doc, _id: post._id.toString(), createdAt: post.createdAt.toISOString(), updatedAt: post.updatedAt.toISOString(), }; },
-
-
Front : .../SinglePost.js
-
componentDidMount()주의: fetch 함수에 POST 요청(method:'POST' ) 안보내줘서 1시간 동안 고생함.componentDidMount() { const postId = this.props.match.params.postId; console.log(postId); const graphqlQuery = { query: ` { getPost(id: "${postId}") { title content imageUrl creator { name } createdAt } } `, }; fetch('http://localhost:8080/graphql', { method: 'POST', headers: { Authorization: 'Bearer ' + this.props.token, 'Content-Type': 'application/json', }, body: JSON.stringify(graphqlQuery), }) .then((res) => { if (res.status !== 200) { throw new Error('Failed to fetch status'); } return res.json(); }) .then((resData) => { console.log(resData); if (resData.errors) { throw new Error('Fetching failed!'); } this.setState({ title: resData.data.getPost.title, author: resData.data.getPost.creator.name, image: 'http://localhost:8080/' + resData.data.getPost.imageUrl, date: new Date(resData.data.getPost.createdAt).toLocaleDateString( 'en-US' ), content: resData.data.getPost.content, }); }) .catch((err) => { console.log(err); }); }
-
-
graphql/schema.js
-
RootMutation에updatePost추가` type RootMutation { createUser(userInput: UserInputData): User! createPost(postInput: PostInputData): Post! updatePost(id: ID!, postInput: PostInputData): Post! } `
-
-
graphql/resolvers.js
-
updatePost작성하기updatePost: async function ({ id, postInput }, req) { // 1. 권한 체크 if (!req.isAuth) { const error = new Error('권한이 없습니다.'); error.code = 401; throw error; } // 2. mongoDB에서 수정할 post 객체 가져오기 const post = await Post.findById(id).populate('creator'); if (!post) { const error = new Error('No post found!'); error.code = 404; throw error; } // 3. 자기가 작성한 post만 수정가능하게 권한 설정 if (post.creator._id.toString() !== req.userId.toString()) { const error = new Error('다른 사람의 post를 수정할 권한이 없습니다.'); error.code = 403; throw error; } // 4. 사용자 Input 체크 const errors = []; if ( validator.isEmpty(postInput.title) || !validator.isLength(postInput.title, { min: 3 }) ) { errors.push({ message: 'Title is invalid.' }); } if ( validator.isEmpty(postInput.content) || !validator.isLength(postInput.content, { min: 3 }) ) { errors.push({ message: 'content is invalid.' }); } if (errors.length > 0) { const error = new Error('Invalid Input'); error.data = errors; error.code = 422; throw error; } // 5. post 객체 update하기 post.title = postInput.title; post.content = postInput.content; // 사용자가 새로운 사진을 업데이트했다면 if (postInput.imageUrl !== 'undefined') { post.imageUrl = postInput.imageUrl; } // 6. mongoDB에 저장 const updatedPost = await post.save(); return { ...updatedPost._doc, _id: updatedPost._id.totring(), createdAt: updatedPost.createdAt.toISOString(), updatedAt: updatedPost.updatedAt.toISOString(), }; },
-
-
Front: .../Feed.js
-
finshEditHandlerfinishEditHandler = (postData) => { this.setState({ editLoading: true, }); const formData = new FormData(); formData.append('image', postData.image); if (this.state.editPost) { formData.append('oldPath', this.state.editPost.imagePath); } fetch('http://localhost:8080/post-image', { method: 'PUT', headers: { Authorization: 'Bearer ' + this.props.token, }, body: formData, }) .then((res) => res.json()) .then((fileResData) => { console.log(fileResData); let imageUrl; if (fileResData.filePath) { imageUrl = fileResData.filePath.replace('\\', '/'); } // const imageUrl = fileResData.filePath; let graphqlQuery = { query: ` mutation { createPost(postInput: {title: "${postData.title}", content: "${postData.content}", imageUrl: "${imageUrl}"}) { _id title content imageUrl creator { name } createdAt } } `, }; if (this.state.editPost) { graphqlQuery = { query: ` mutation { updatePost(id:"${this.state.editPost._id}" ,postInput: {title: "${postData.title}", content: "${postData.content}", imageUrl: "${imageUrl}"}) { _id title content imageUrl creator { name } createdAt } } `, }; } return fetch('http://localhost:8080/graphql', { method: 'POST', body: JSON.stringify(graphqlQuery), headers: { Authorization: 'Bearer ' + this.props.token, 'Content-Type': 'application/json', }, }); }) .then((res) => { return res.json(); }) .then((resData) => { if (resData.errors && resData.errors[0].status === 422) { throw new Error( "Validation failed. Make sure the email address isn't used yet!" ); } if (resData.errors) { throw new Error('Create user failed!'); } let resDataFiled = 'createPost'; if (this.state.editPost) { resDataFiled = 'updatePost'; } const post = { _id: resData.data[resDataFiled]._id, title: resData.data[resDataFiled].title, content: resData.data[resDataFiled].content, creator: resData.data[resDataFiled].creator, createdAt: resData.data[resDataFiled].createdAt, }; this.setState((prevState) => { let updatedPosts = [...prevState.posts]; if (prevState.editPost) { const postIndex = prevState.posts.findIndex( (p) => p._id === prevState.editPost._id ); updatedPosts[postIndex] = post; } else { updatedPosts.pop(); updatedPosts.unshift(post); } return { posts: updatedPosts, isEditing: false, editPost: null, editLoading: false, }; }); }) .catch((err) => { console.log(err); this.setState({ isEditing: false, editPost: null, editLoading: false, error: err, }); }); };
-
-
graphql/schema.js
-
deletePost생성하기` type RootMutation { createUser(userInput: UserInputData): User! createPost(postInput: PostInputData): Post! updatePost(id: ID!, postInput: PostInputData): Post! deletePost(id: ID!): Boolean } `
-
-
grpahql/resolvers.js
-
deletePost생성하기deletePost: async function ({ id }, req) { // 1. 권한 확인 if (!req.auth) { const error = new Error('권한이 없습니다.'); error.code = 401; throw error; } // 2. 삭제하고자 하는 post 객체 가져오기 const post = await Post.findById(id); // 없으면 오류! console.log(post); if (!post) { const error = new Error('포스트가 없습니다.'); error.state = 404; throw error; } // 3. post의 userId와 요청하는 유저의 id가 일치하는지 확인하기 if (req.userId.toString() !== post.creator.toString()) { const error = new Error('다른 사람의 post를 삭제할 권한이 없습니다.'); error.code = 403; throw error; } // 4. post DB와 이미지 삭제하기 clearImage(post.imageUrl); await Post.findByIdAndRemove(id); // 5. user객체 가져와서 posts에 해당 post지우기 const user = await User.findById(req.userId); user.posts.pull(id); await user.save(); return true; }, };
-
clearImage함수 가져오기const path = require('path'); const fs = require('fs'); const clearImage = (filePath) => { filePath = path.join(__dirname, '..', filePath); fs.unlink(filePath, (err) => console.log(err)); };
-
-
Front : .../Feed.js
-
deletePostHandlerdeletePostHandler = (postId) => { this.setState({ postsLoading: true }); const graphqlQuery = { query: ` mutation{ deletePost(id: "${postId}") } `, }; fetch('http://localhost:8080/graphql', { method: 'POST', headers: { Authorization: 'Bearer ' + this.props.token, 'Content-Type': 'application/json', }, body: JSON.stringify(graphqlQuery), }) .then((res) => { if (res.status !== 200 && res.status !== 201) { throw new Error('Deleting a post failed!'); } return res.json(); }) .then((resData) => { console.log(resData); this.loadPosts(); }) .catch((err) => { console.log(err); this.setState({ postsLoading: false }); }); };
-
graphql/schema.js
-
query 만들기
` type RootQuery { login(email: String!, password: String!): AuthData! posts(page:Int): PostData! getPost(id: ID!): Post! status: User! } `
-
-
graphql/resolvers.js
-
status
status: async function (args, req) { if (!req.isAuth) { const error = new Error('권한이 없습니다.'); error.code = 401; throw error; } // console.log(req.); // 2. status db에서 찾기 const user = await User.findById(req.userId); if (!user) { const error = new Error(); error.status = 404; throw error; } return { ...user._doc, _id: user._id.toString() }; },
-
-
Front : .../Feed.js
-
componentDidMount()componentDidMount() { const graphqlQuery = { query: ` { status { status } } `, }; fetch('http://localhost:8080/graphql', { method: 'POST', headers: { Authorization: 'Bearer ' + this.props.token, 'Content-Type': 'application/json', }, body: JSON.stringify(graphqlQuery), }) .then((res) => { return res.json(); }) .then((resData) => { if(resData.errors){ throw new Error('Fetching status failed!') } this.setState({ status: resData.data.status.status }); }) .catch(this.catchError); this.loadPosts(); }
-
graphql/shcema.js
-
mutation 정의하기
` type RootMutation { createUser(userInput: UserInputData): User! createPost(postInput: PostInputData): Post! updatePost(id: ID!, postInput: PostInputData): Post! deletePost(id: ID!): Boolean, updateStatus(status: String!): User! } `
-
-
graphql/resolvers.js
-
updateStatus작성하기updateStatus: async function ({ status }, req) { if (!req.isAuth) { const error = new Error('권한 없디?'); error.code = 401; throw error; } const user = await User.findById(req.userId); if (!user) { const error = new Error(); error.status = 404; throw error; } user.status = status; await user.save(); return { ...user._doc }; },
-
-
Front : .../Feed.js
-
statusUpdateHandler()statusUpdateHandler = (event) => { const graphqlQuery = { query: ` mutation{ updateStatus(status: "${this.state.status}") { status } } `, }; event.preventDefault(); fetch('http://localhost:8080/graphql', { method: 'POST', headers: { Authorization: 'Bearer ' + this.props.token, 'Content-Type': 'application/json', }, body: JSON.stringify(graphqlQuery), }) .then((res) => { return res.json(); }) .then((resData) => { if (resData.errors) { throw new Error('Fetching status failed!'); } console.log(resData); }) .catch(this.catchError); };
-
-
-
-
기존에 동적으로 인자를 바꾸기 위해 아래와 같이 백틱 ``
과$`사인을 사용하여 동적으로 인자를 넣어줬습니다.const graphqlQuery = { query: ` { posts(page: ${page}){ posts{ _id title imageUrl content creator{ name } createdAt } totalPosts } } `, };
-
물론 이 방법도 가능하지만 GraphQL에서 권장하는 방법이 아닙니다.
-
동적으로 인자를 넣는 방법
const graphqlQuery = { query: ` query FetchPosts($page: Int) { posts(page: $page) { posts{ _id title imageUrl content creator{ name } createdAt } totalPosts } } `, variables: { page: page } };
-
다른 query도 인자 바꾸기
const graphqlQuery = { query: ` mutation UpdateUserStatus($userStatus: String){ updateStatus(status: $userStatus) { status } } `, variables: { userStatus: this.state.status, }, };
-
참고로 BackEnd에서 타입 뒤에
!마크를 붙였다면 FrontEnd에서도!마크를 붙어야한다.