본문 바로가기

코딩강의/인스타그램클론(expo-노마드코더)

Photos Module(2)

🍔 핵심 내용

 

🥑 comment기능을 만들어 보자.

 

🍔 코드 리뷰

 

🥑 schema.prisma

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id         Int       @id @default(autoincrement())
  firstName  String
  lastName   String?
  userName   String    @unique
  email      String    @unique
  password   String
  bio        String?
  avatar     String?
  photos     Photo[]
  likes      Like[]
  followers  User[]    @relation("FollowRelation", references: [id])
  followings User[]    @relation("FollowRelation", references: [id])
  createdAt  DateTime  @default(now())
  updatedAt  DateTime  @updatedAt
  comments    Comment[]
}

model Photo {
  id        Int       @id @default(autoincrement())
  user      User      @relation(fields: [userId], references: [id])
  userId    Int
  file      String
  caption   String?
  hashtags  Hashtag[]
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  likes     Like[]
  comments   Comment[]
}

model Hashtag {
  id        Int      @id @default(autoincrement())
  hashtag   String   @unique
  photos    Photo[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Like {
  id        Int      @id @default(autoincrement())
  user      User     @relation(fields: [userId], references: [id])
  photo     Photo    @relation(fields: [photoId], references: [id])
  userId    Int
  photoId   Int
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@unique([userId, photoId])
}

model Comment {
  id        Int      @id @default(autoincrement())
  user      User     @relation(fields: [userId], references: [id])
  photo     Photo    @relation(fields: [photoId], references: [id])
  payload   String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  userId    Int
  photoId   Int
}

 

스키마에 comment 모델 추가

 

🥑 comments.typeDefs.js

import { gql } from "apollo-server-core";

export default gql`
  type comments {
    id: Int!
    user: User!
    photo: Photo!
    payload: String!
    isMine: Boolean!
    createdAt: String!
    updatedAt: String!
  }
`;

내가 작성한 코멘트라면, 삭제나 수정을 할 수 있어야 하기 때문에 isMine을 추가 하였다.  (photo에도 추가 함)

여기서 payload는 작성할 text다.

 

🥑 createComment.typeDefs.js

import { gql } from "apollo-server-core";

export default gql`
  type createCommentResult {
    ok: Boolean!
    error: String
  }
  type Mutation {
    createComment(photoId: Int!, payload: String!): createCommentResult
  }
`;

 

 

 

🥑 createComment.resolvers.js

import client from "../../client";
import { protectedResolver } from "../../users/users.utils";

export default {
  Mutation: {
    createComment: protectedResolver(
      async (_, { photoId, payload }, { loggedInUser }) => {
        const ok = await client.photo.findUnique({
          where: {
            id: photoId,
          },
          select: {
            id: true,
          },
        });
        if (!ok) {
          return {
            ok: false,
            error: "사진을 찾을 수 없습니다.",
          };
        }
        await client.comment.create({
          data: {
            payload,
            photo: {
              connect: {
                id: photoId,
              },
            },
            user: {
              connect: {
                id: loggedInUser.id,
              },
            },
          },
        });
        return {
          ok: true,
        };
      }
    ),
  },
};

 특이 사항은 없다. comment 모델에 인자로 받은 payload값을 넣고 photo와 user를 연결해주었다.  

 


🍔 핵심 내용

 

🥑 seePhotoComments 기능을 만들어보자.

photo의 리턴값에서 comment도 확인 할 수 있도록 해보자.

 

 

🍔 코드 리뷰

 

🥑 seePhotoComments.typeDefs.js

import { gql } from "apollo-server-core";

export default gql`
  type Query {
    seePhotoComments(id: Int!): [Comment]
  }
`;

특정 photoId 값을 넣으면 해당 photo에 달려있는 comment 값이 출력 되도록 해보자. 

 

 

🥑 seePhotoComments.resolvers.js

import client from "../../client";

//pagination으로도 가능
export default {
  Query: {
    seePhotoComments: (_, { id }) =>
      client.comment.findMany({
        where: {
          photoId: id,
        },
        orderBy: "asc",
      }),
  },
};

//이건 전체 호출
// export default {
//     Query: {
//       seePhotoComments: (_, { id }) =>
//         client.photo
//           .findUnique({
//             where: {
//               id,
//             },
//           })
//           .comment(),
//     },
//   };

다른 큰 특이사항은 없다.


🍔 핵심 내용

 

🥑 isMine 기능을 만들어 보자.

코멘트나 photo에서 삭제나 수정 기능을 사용하려면 본인이 작성한 것이어야만 한다. 이를 위해 이전 user의 isMe와 유사하게, isMine 기능을 만들어 보자.

 

🍔 코드 리뷰

 

🥑 photos.typeDefs.js

import { gql } from "apollo-server-core";

export default gql`
  type Photo {
    id: Int!
    user: User
    file: String!
    caption: String
    likes: Int!
    comments: Int!
    isMine: Boolean! <--추가
    hashtags: [Hashtag]
    createdAt: String!
    updatedAt: String!
  }
  type Hashtag {
    id: Int!
    createdAt: String!
    updatedAt: String!
    hashtag: String!
    photos(page: Int!): [Photo] #일부 필드값에만 args 추가 가능함
    totalPhotos: Int!
  }
  type Like {
    id: Int!
    photo: Photo!
    createdAt: String!
    updatedAt: String!
  }
`;

isMine 추가

 

🥑 photos.resolvers.js

import client from "../client";

export default {
  Photo: {
    user: ({ userId }) => {
      return client.user.findUnique({ where: { id: userId } });
    },
    hashtags: ({ id }) => {
      return client.hashtag.findMany({ where: { photos: { some: { id } } } });
    },
    likes: ({ id }) => {
      return client.like.count({ where: { photoId: id } });
    },
    comments: ({ id }) => {
      return client.comment.count({ where: { photoId: id } });
    },
    isMine: ({ userId }, _, { loggedInUser }) => { <--추가
      if (!loggedInUser) {
        return false;
      }
      return userId === loggedInUser.id;
    },
  },
  Hashtag: {
    photos: ({ id }, { page }, { loggedInUser }) => {
      console.log(page);
      return client.hashtag.findUnique({ where: { id } }).photos();
    }, //페이지네이션 기능 가능, 일부 필드값만 context값 쓸 수 있음
    totalPhotos: ({ id }) =>
      client.photo.count({
        where: {
          hashtags: {
            some: {
              id,
            },
          },
        },
      }),
  },
};

resolver에 isMine 추가. loggedInUser 값이 null일수도 있기 때문에 if 구절을 사용하자.

 

위와 같은 작업을 똑같이 comment부분에도 추가해주면 된다.

 

🥑 comments.typeDefs.js

import { gql } from "apollo-server-core";

export default gql`
  type Comment {
    id: Int!
    user: User!
    photo: Photo!
    payload: String!
    isMine: Boolean! <--추가
    createdAt: String!
    updatedAt: String!
  }
`;

🥑 comments.resolvers.js

export default {
  Comment: {
    isMine: ({ userId }, _, { loggedInUser }) => {
      if (!loggedInUser) {
        return false;
      }
      return userId === loggedInUser.id;
    },
  },
};

추가 완료.

 


🍔 핵심 내용

 

🥑 deletePhoto와  deleteComment를 만들어 보자.

 

먼저 유저 본인이 작성한 사진이나 코멘트만 지울 수 있고, 여기서 유의점은 photo 삭제 시, 해당 photo에 연결 되어 있는 like와 comment를 먼저 매뉴얼로 지워주어야 한다. 코멘트는 그런 에러가 없는 것 같다. (아마도 코멘트는 photo에 종속적 성질이 있어서 그런 것 같다.)

 

🍔 코드 리뷰

 

🥑 deletePhoto.typeDefs.js

import { gql } from "apollo-server-core";

export default gql`
  type deletePhotoResult {
    ok: Boolean!
    error: String
  }

  type Mutation {
    deletePhoto(id: Int!): deletePhotoResult!
  }
`;

🥑 deletePhoto.resolvers.js

import client from "../../client";
import { protectedResolver } from "../../users/users.utils";

export default {
  Mutation: {
    deletePhoto: protectedResolver(async (_, { id }, { loggedInUser }) => {
      const photo = await client.photo.findUnique({
        where: {
          id,
        },
        select: {
          userId: true,
        },
      });
      if (!photo) {
        return {
          ok: false,
          error: "사진을 찾을 수 없습니다.",
        };
      } else if (photo.userId !== loggedInUser.id) {
        return {
          ok: false,
          error: "권한이 없습니다.",
        };
      } else {
        await client.like.deleteMany({
          where: {
            photoId: id,
          },
        });
        await client.comment.deleteMany({
          where: {
            photoId: id,
          },
        });
        await client.photo.delete({
          where: {
            id,
          },
        });
        return {
          ok: true,
        };
      }
    }),
  },
};

 마지막 단계를 보면, 해당 photo의 like와 comment를 먼저 삭제해주고 그리고 마지막으로 해당 photo를 삭제해주는 로직이다.

 

🥑 deleteComment.typeDefs.js

import { gql } from "apollo-server-core";

export default gql`
  type deleteCommentResult {
    ok: Boolean!
    error: String
  }
  type Mutation {
    deleteComment(id: Int!): deleteCommentResult!
  }
`;

🥑 deleteComment.resolvers.js

import client from "../../client";
import { protectedResolver } from "../../users/users.utils";

export default {
  Mutation: {
    deleteComment: protectedResolver(async (_, { id }, { loggedInUser }) => {
      const comment = await client.comment.findUnique({
        where: {
          id,
        },
        select: {
          userId: true,
        },
      });
      if (!comment) {
        return {
          ok: false,
          error: "코멘트를 찾을 수 없습니다.",
        };
      } else if (comment.userId !== loggedInUser.id) {
        return {
          ok: false,
          error: "권한이 없습니다.",
        };
      } else {
        await client.comment.delete({
          where: {
            id,
          },
        });
        return {
          ok: true,
        };
      }
    }),
  },
};

 deletePhoto 와 거의 똑같은 로직이다.  


🍔 핵심 내용

 

🥑 Mutation의 결과 값(ok, error)은 똑같다. 코드 재사용성

재사용 코드로 하나 만들어주자

 

🥑 editComment를 만들어 보자.

크게 특이 사항 없다.

 

 

 

🍔 코드 리뷰

 

🥑 shared.typeDefs.js

import { gql } from "apollo-server-core";

export default gql`
  type MutationResponse {
    ok: Boolean!
    error: String
  }
`;

Mutation의 결과 값은 이거 고정 이다. 따라서, 재활용성을 위해 하나 만들어 주자.

 

🥑 editComment.typeDefs.js

import { gql } from "apollo-server-core";

export default gql`
  type Mutation {
    editComment(id: Int!, payload: String!): MutationResponse!
  }
`;

 

🥑 editComment.resolvers.js

import client from "../../client";
import { protectedResolver } from "../../users/users.utils";

export default {
  Mutation: {
    editComment: protectedResolver(
      async (_, { id, payload }, { loggedInUser }) => {
        const comment = await client.comment.findUnique({
          where: {
            id,
          },
          select: {
            userId: true,
          },
        });
        if (!comment) {
          return {
            ok: false,
            error: "코멘트를 찾을 수 없습니다.",
          };
        } else if (comment.userId !== loggedInUser.id) {
          return {
            ok: false,
            error: "권한이 없습니다",
          };
        } else {
          await client.comment.update({
            where: {
              id,
            },
            data: {
              payload,
            },
          });
          return {
            ok: true,
          };
        }
      }
    ),
  },
};

 delete 로직과 유사하다.


🍔 핵심 내용

 

🥑 protectedResolver 리팩토링

해당 함수에는 사실 문제가 하나 있었다. 대부분 Mutation값에 로그인이 필요한 부분에 쓰였고, 해당 결과 값은 ok와 error이다. 하지만, protectedResolver가 query에 한 곳에 쓰이고 있다. 그곳은 바로 seeFeed다.

 

먼저 아래 코드를 보고 확인 해보자.

 

🍔 코드 리뷰

 

🥑 seeFeed.typeDefs.js

import { gql } from "apollo-server-core";

export default gql`
  type Query {
    seeFeed: [Photo]
    }
`;

 

🥑 seeFeed.resolvers.js

import client from "../../client";
import { protectedResolver } from "../../users/users.utils";

export default {
  Query: {
    seeFeed: protectedResolver(async (_, __, { loggedInUser }) =>
      client.photo.findMany({
        where: {
          OR: [
            {
              user: {
                followers: {
                  some: {
                    id: loggedInUser.id,
                  },
                },
              },
            },
            {
              userId: loggedInUser.id,
            },
          ],
        },
        orderBy: {
          createdAt: "desc",
        },
      })
    ),
  },
};

 이렇게 query 구문에서 쓰이고 있는데, 결과값은 Photo배열 값이다.

 

🥑 users.utils.js

export const protectedResolver = (ourResolver) => (
  root,
  args,
  context,
  info
) => {
  if (!context.loggedInUser) {
    const query = info.operation.operation === "query";
    if (query) {
      return null;
    } else {
      return {
        ok: false,
        error: "로그인이 필요합니다. 로그인 해주세요.",
      };
    }
  }
  return ourResolver(root, args, context, info);
};

 info 안에는 많은 내용들이 들어가 있다. info.operation.operation 값에는 query가 들어가 있기 때문에 해당 결과 값을 null로 해놓으면 query에서 쓰이는 protectedResolver 결과값은 null로 에러가 생기지 않는다.

 

다시 말해 원래 ok: false 와 error: "로그인이 필요합니다. 로그인 해주세요." 를 뱉어야 하는데, query의 결과 값은 ok와 error가 아니기 때문에 에러가 생기는 거였는데, 이를 null값으로 처리해줌으로써 에러를 잡은 것이다.

 

'코딩강의 > 인스타그램클론(expo-노마드코더)' 카테고리의 다른 글

Direct Messages(1)  (0) 2021.04.07
Photos Module(3) - AWS upload 세팅  (0) 2021.04.07
Photos Module(1)  (0) 2021.03.31
User module(3)  (0) 2021.03.27
User module(2)  (0) 2021.03.20