본문 바로가기

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

Photos Module(1)

🍔 핵심 내용

 

🥑 Photo 모듈을 만들어보자.

스키마에 관련 모델을 추가해주어야 하는데, relation이나 다른 모델과 엮이는 부분은 DB에 저장 되는 개념이 아니라 프리즈마에서 자동으로 그 관계를 따로 관리해주는 개념으로 봐야한다.

 

🍔 코드 리뷰

 

🥑 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[]
  followers  User[]   @relation("FollowRelation", references: [id])
  followings User[]   @relation("FollowRelation", references: [id])
  createdAt  DateTime @default(now())
  updatedAt  DateTime @updatedAt
}

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
}

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

Photo와 Hashtag 모델 추가 하였음.

 


 

🍔 핵심 내용

 

🥑 사진 업로드와 #해쉬태그 기능을 만들어보자.

추 후에 AWS로 연동 시킬 예정이다. (현재는 테스트용)

 

 

🍔 코드 리뷰

 

🥑 photos.typeDefs.js

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

export default gql`
  type Photo {
    id: Int!
    user: User
    file: String!
    caption: String
    hashtags: [Hashtag]
    createdAt: String!
    updatedAt: String!
  }
  type Hashtag {
    id: Int!
    createdAt: String!
    updatedAt: String!
    hashtag: String!
    photos: [Photo]
  }
`;

type Photo와 Hashtag 추가. 1개 모듈에 여러개 타입을 두는 경우에는 상관성이 높은것 끼리 묶어 놔야 한다.

 

🥑 uploadPhoto.typeDefs.js

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

export default gql`
  type Mutation {
    uploadPhoto(file: String!, caption: String): Photo
  }
`;

 file 인자값은 upload가 되어야 하지만, 현재는 테스트로 String으로 간단히 진행해봅시다.

결과 값은 Photo를 받도록 하자.

 

🥑 uploadPhoto.resolvers.js

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

export default {
  Mutation: {
    uploadPhoto: protectedResolver(
      async (_, { file, caption }, { loggedInUser }) => {
        let hashtagObj = [];
        if (caption) {
          const hashtags = caption.match(/#[ㄱ-ㅎ|ㅏ-ㅣ|가-힣|\w]+/g);
          hashtagObj = hashtags.map((hashtag) => ({
            where: { hashtag },
            create: { hashtag },
          }));
          console.log(hashtagObj);
        }
        return client.photo.create({
          data: {
            file,
            caption,
            User: {
              connect: { id: loggedInUser.id },
            },
            ...(hashtagObj.length > 0 && {
              hashtags: { connectOrCreate: hashtagObj },
            }),
          },
        });
      }
    ),
  },
};

① 사진 업로드는 로그인 되어있는 상황이어야 하기 때문에, loggedInUser context값이 필요하다.

② hashtagObj 배열 값을 if 함수 밖에서도 사용하기 위해 let 으로 빼놓았다.

③ 정규표현식(js에서는 ~~match(정규표현식)을 이용하여 # 이 붙은 텍스트값만 따로 배열에 담아 두었다.

④ map 함수는 아래와 같은 기능, 이를 이용하여 스키마에서 제공하는 connectOrCreate 기능을 자동화 하였다.

 

 

connectOrCreate 는 찾고자 하는 where 값이 있으면 connect 해주고, 없으면 create해주는 기능이다.

(위에서는, hashtag가 where 값임)

 


🍔 핵심 내용

 

🥑 seePhoto 기능 추가

computed fields 사용하여, photo 결과값에 user와 hashtags 값을 확인해보자.

 

🍔 코드 리뷰

 

🥑 seePhoto.typeDefs.js

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

export default gql`
  type Query {
    seePhoto(id: Int!): Photo
  }
`;

 

🥑 seePhoto.resolvers.js

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

export default {
  Query: {
    seePhoto: (_, { id }) => client.photo.findUnique({ where: { id } }),
  },
};

 

 해당 결과 값인 Photo로는 아직까지 user와 hashtags의 값을 확인 할 수가 없다. 따라서 아래 computed fields를

활용해보자.

 

🥑 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 } } } });
    },
  },
};

 위와 같이 활용 할 수 있다. (지금까지 보면 computed fields의 활용은 크게 2가지로 보인다. 첫 번째는 이전에 활용했던, 기존 db값을 활용한 또 다른 값 구하기와 두 번째는 이것과 같이, 값이 확인이 안되는경우 (아마도 값 꼬리 무는 거를 방지하기 위해?) 이와 같이 강제로 확인 할 수가 있다. 여기서 user의 userId는 parent값인 Photo의 userId값을 말한다.

hashtags의 id 역시 Photo의 id를 뜻함.

 


🍔 핵심 내용

 

🥑 seeHashtag 기능 만들기

해쉬 태그 값을 넣으면, 해쉬 태그 모델값이 출력 되는 기능을 만들어 보자.

여기에 추가로, 1) 해당 해쉬 태그 값안에 어떤 photos들과 연결 되어있는지와 2) 총 몇개의 photos들이 있는지도 확인해볼 수 있다.

 

 

 

🍔 코드 리뷰

 

🥑 seeHashtag.typeDefs.js

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

export default gql`
  type Query {
    seeHashtag(hashtag: String!): Hashtag
  }
`;

 

 

🥑 seeHashtag.resolvers.js

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

export default {
  Query: {
    seeHashtag: (_, { hashtag }) =>
      client.hashtag.findUnique({ where: { hashtag } }),
  },
};

 

 여기까지는 일반적이다. 하지만 아직 photos와 photos가 총 몇개 있는지 알 수가 없다. 따라서 아래와 같이 추가해주자.

 

 

🥑 photos.typeDefs.js

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

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

photos에 page라는 인자값을 넣어주었다. 보통의 인자값은 Query나 Mutation의 인자값으로 넣어주는데 이렇게 필드에 넣어 줄수도 있다. photos라는 필드값에 page라는 인자 값을 넣어주게 되면, 이를 나중에 필드 단계에서 활용 할 수 있게 된다. (아래와 같이) 이렇게 하게 되면, photos가 만약 몇억개가 있고 이를 호출할 때 엄청난 부하가 생길 수 있는데, 이전에 배웠던 pagination이나 cursor 를 통해 일부 값만 받아오게 할 수 있다. (보통 pagination 많이 사용)

 

또 한, 다른 곳에서 photos를 호출 할 때도 재사용이 가능 하다.

 

 

마지막으로, resolver에서 사용할 totalPhotos를 추가해 주었음.

 

🥑 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 } } } });
    },
  },
  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,
            },
          },
        },
      }),
  },
};

 photos 필드값에 page 라는 인자값을 설정해주었기 때문에, 인자값을 받아 올 수 있게 되었고 이를 활용해 pagination이 활용 가능하다. 또한 로그인 유저만 해당 기능을 확인 할 수 있도록 context값인 loggedInUser를 사용 할 수도 있다. 

(예시용 이기 때문에, 추가 작업은 X)

 

totalPhotos의 총 개수를 파악하는 로직은 이전 total 팔로잉을 확인 하는 로직과 유사 하다.

 

**type User에 필드값photos: [Photo] 추가해주자.

 


🍔 핵심 내용

 

🥑 editPhoto 기능 만들기

editUser 부분과 많이 유사하다.

 

🍔 코드 리뷰

 

🥑 editPhoto.typeDefs.js

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

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

  type Mutation {
    editPhoto(id: Int!, caption: String!): editPhotoResult!
  }
`;

 보통 mutation의 결과 값은 따로 ok, error 정도의 result값으로 반환해준다. (기능 처리는, resolver 중간에 처리 되기 때문에)

 

 

🥑 editPhoto.resolvers.js

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

export default {
  Mutation: {
    editPhoto: protectedResolver(
      async (_, { id, caption }, { loggedInUser }) => {
        const ok = await client.photo.findFirst({
          where: {
            id,
            userId: loggedInUser.id,
          },
        });
        if (!ok) {
          return {
            ok: false,
            error: "사진을 찾을 수 없습니다.",
          };
        }
        const photo = await client.photo.update({
          where: {
            id,
          },
          data: {
            caption,
          },
        });
        console.log(photo);
      }
    ),
  },
};

 

1) photo 모델에는 userId값이 중복으로 들어 가기 때문에 unique보다는 findfirst로 찾는게 좋다.

2) 업데이트 부분은, 위 내용 대로 하면 큰 문제가 없으나 hashtag 값 역시 바뀌어야 하지만 hashtag값은 그대로 들어가 있다. 예를 들면 caption 내용에 #봉제로 되어있다가 수정을 통해 #기계 로 바꾸게 된다면 기존 해당 photo 값에 들어가 있는 #봉제 는 사라져야 한다.(연결이 끊켜야 한다) 이 부분은 다음 내용에..!

 


 

🍔 핵심 내용

 

🥑 hashtag 연결 끊고 새로 만들어 주기

caption 내용에 hashtag 내용이 바뀌게 되면 기존 hashtag 연결을 끊고 새롭게 추가 되거나 삭제된 hashtag와 연결이 되어야 한다. 이 부분을 만들어 보자.

 

🍔 코드 리뷰

 

🥑 editPhoto.typeDefs.js

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

export default {
  Mutation: {
    editPhoto: protectedResolver(
      async (_, { id, caption }, { loggedInUser }) => {
        const oldPhoto = await client.photo.findFirst({
          where: {
            id,
            userId: loggedInUser.id,
          },
          include: {
            hashtags: {
              select: {
                hashtag: true,
              },
            },
          },
        });
        console.log(oldPhoto);
        if (!oldPhoto) {
          return {
            ok: false,
            error: "사진을 찾을 수 없습니다.",
          };
        }
        const photo = await client.photo.update({
          where: {
            id,
          },
          data: {
            caption,
            hashtags: {
              disconnect: oldPhoto.hashtags,
              connectOrCreate: processHashtags(caption),
            },
          },
        });
        console.log(photo);
      }
    ),
  },
};

 

 

caption 을 수정하고, 기존 caption에 달려있는 해쉬태그 값을 때어버린 후, 새로운 해쉬태그 값을 연결해주는 로직이다.

 

🥑 photos.utils.js

export const processHashtags = (caption) => {
  const hashtags = caption.match(/#[ㄱ-ㅎ|ㅏ-ㅣ|가-힣|\w]+/g) || [];
  return hashtags.map((hashtag) => ({
    where: { hashtag },
    create: { hashtag },
  }));
};

 uploadPhoto와 editPhoto에 사용될 hashtags 배열 값을 utils로 따로 만들어 주었다. (재 사용)

그리고 null 값이 들어오면 에러가 뜨기 때문에, || [] 값을 추가 하였음.

 


🍔 핵심 내용

 

🥑 좋아요 / 좋아요 취소 기능 만들기 ( + 좋아요 총 몇개인지  )

토글 기능을 만들어서 좋아요와 좋아요 취소 기능을 만들어 보자. 로직은 간단 하다.

로그인한 유저가, 특정 photo에 아직 좋아요가 없으면 db에 추가가 되고, 이미 좋아요가 들어가 있으면 좋아요가 취소 된다.

 

🍔 코드 리뷰

 

🥑 toggleLike.typeDefs.js

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

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

 

🥑 toggleLike.resolvers.js

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

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

스키마에서 @@unique([userId, photoId])묶어 주었기 때문에  userId_photoId가 자동으로 나오고 이를 연결 해주었다.

로그인한 유저가 특정 photoId를 찾는 로직임. 

 

🥑 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 } });
    }, // <--추가
  },
  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,
            },
          },
        },
      }),
  },
};

 특정 photo값에 like가 몇개 있는지 확인 할 수 있도록 likes 기능 추가


🍔 핵심 내용

 

🥑 seePhotoLikes 기능 만들기 (누가 좋아요를 눌렀는지 확인)

그리고, select와 include의 차이점을 알아 보자.

 

🍔 코드 리뷰

 

🥑 seePhotoLikes.typeDefs.js

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

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

 리턴값은 User이다.

 

🥑 seePhotoLikes.resolvers.js

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

export default {
  Query: {
    seePhotoLikes: async (_, { id }) => {
      const likes = await client.like.findMany({
        where: {
          photoId: id,
        },
        select: {
          user: true,
        },
      });
      return likes.map((like) => like.user);
    },
  },
};

select는 고른 값만 보겠다는 것이고, include는 고른 값을 포함한다는 것이다. 위 내용에서 likes는 user 값만 담기게 된다. select { user: { select : {userName: true} } } 이렇게 한 단계 더 들어갈 수도 있다.

 

여기서 문제는 우리의 리턴 값은 배열안에 User의 값만 담겨야한다. [ {id: xx, userName: xx~~}, {id:xx, userName:xx ~~}]이렇게,, 하지만 likes의 값은 [ user: {id: xx, userName: xx~~}, {id:xx, userName:xx ~~}] 이렇게 담긴다. 그렇기 때문에 map 함수를 통해 user 하위 값만 받아 오자.

 


🍔 핵심 내용

 

🥑 seeFeed 기능 만들기

내가 올린 사진과 내가 팔로잉하고 있는 user의 사진을 볼 수 있는 기능을 만들어 보자.

 

🍔 코드 리뷰

 

🥑 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",
        },
      })
    ),
  },
};

먼저, photo 모델에 접근 후, findMany를 통해 여러 photo들을 가지고 올 것이다. 어떤 것들을 가지고 올까? 조건을 보자.

 

where -> OR을 통해, photo 모델에 user 에 접근 후, followers의 id가 로그인된 id가 포함된 모든 photo를 가지고온다. 또는 photo모델에 userId가 로그인된 유저인 photo를 가지고 온다. 이를 통해, 로그인한 유저 photo와 로그인한 유저가 팔로잉한 사진들을 한번에 볼 수 있게 된다. 그리고 이는 마지막에 올린 것을 기준 순서 대로 볼 수 있다.

 

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

Photos Module(3) - AWS upload 세팅  (0) 2021.04.07
Photos Module(2)  (0) 2021.04.04
User module(3)  (0) 2021.03.27
User module(2)  (0) 2021.03.20
User module(1)  (0) 2021.02.19