🍔 핵심 내용
🥑 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 |