Skip to content

Commit

Permalink
✨ feat: 모각밥 모집 리스트 cursor base pagination (#47)
Browse files Browse the repository at this point in the history
  • Loading branch information
yanggwangseong committed Nov 24, 2024
1 parent 63d1e38 commit a46af6e
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 16 deletions.
14 changes: 14 additions & 0 deletions src/controllers/articles.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Patch,
Post,
Put,
Query,
UploadedFile,
UseInterceptors,
} from "@nestjs/common";
Expand All @@ -22,6 +23,19 @@ import { ArticlesService } from "@APP/services/articles.service";
export class ArticlesController {
constructor(private readonly articlesService: ArticlesService) {}

@Get()
async getArticles(
@Query("cursor", new ParseIntPipe()) cursor: number,
@Query("limit", new ParseIntPipe()) limit: number,
@CurrentMemberDecorator("id") currentMemberId: number,
) {
return await this.articlesService.findAll(
cursor,
limit,
currentMemberId,
);
}

@Get(":articleId")
async getArticle(
@Param("articleId", new ParseIntPipe()) articleId: number,
Expand Down
1 change: 1 addition & 0 deletions src/dtos/create-article.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export class CreateArticleDto extends PickType(ArticleEntity, [
"categoryId",
"districtId",
"regionId",
"articleImage",
]) {}
26 changes: 18 additions & 8 deletions src/entities/article.entity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { Transform, Type } from "class-transformer";
import { IsDate, IsNotEmpty, IsNumber, IsString } from "class-validator";
import {
IsDate,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
} from "class-validator";
import {
Column,
CreateDateColumn,
Expand Down Expand Up @@ -45,14 +51,18 @@ export class ArticleEntity {
@Column({ type: "datetime" })
endTime!: Date;

@Transform(({ value }) => {
if (!value) return null;
@Transform(
({ value }) => {
if (!value) return null;

return new URL(
`/public/articles/${value}`,
process.env["API_BASE_URL"],
).toString();
})
return new URL(
`/public/articles/${value}`,
process.env["API_BASE_URL"],
).toString();
},
{ toPlainOnly: true },
)
@IsOptional()
@Column({ type: "varchar", length: 2048, nullable: true })
articleImage?: string;

Expand Down
26 changes: 18 additions & 8 deletions src/entities/member.entity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { Exclude, Transform } from "class-transformer";
import { IsEmail, IsNotEmpty, IsString, Length } from "class-validator";
import {
IsEmail,
IsNotEmpty,
IsOptional,
IsString,
Length,
} from "class-validator";
import {
Column,
CreateDateColumn,
Expand Down Expand Up @@ -43,14 +49,18 @@ export class MemberEntity {
@Column({ type: "varchar", length: 100, nullable: false, unique: true })
email!: string;

@Transform(({ value }) => {
if (!value) return null;
@Transform(
({ value }) => {
if (!value) return null;

return new URL(
`/public/members/${value}`,
process.env["API_BASE_URL"],
).toString();
})
return new URL(
`/public/members/${value}`,
process.env["API_BASE_URL"],
).toString();
},
{ toPlainOnly: true },
)
@IsOptional()
@Column({ type: "varchar", length: 2048, nullable: true })
profileImage?: string;

Expand Down
105 changes: 105 additions & 0 deletions src/services/articles.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import {
Injectable,
NotFoundException,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";

import { ENV_API_BASE_URL } from "@APP/common/constants/env-keys.const";
import { CreateArticleDto } from "@APP/dtos/create-article.dto";
import { UpdateArticleDto } from "@APP/dtos/update-article.dto";
import { ArticleLikesRepository } from "@APP/repositories/article-likes.repository";
Expand All @@ -20,8 +22,111 @@ export class ArticlesService {
private readonly districtsRepository: DistrictsRepository,
private readonly regionsRepository: RegionsRepository,
private readonly articleLikesRepository: ArticleLikesRepository,
private readonly configService: ConfigService,
) {}

async findAll(cursor: number, limit: number = 10, currentMemberId: number) {
const query = this.articlesRepository
.createQueryBuilder("article")
.select([
'article.id AS "articleId"',
'article.title AS "title"',
'article.content AS "content"',
'article.startTime AS "startTime"',
'article.endTime AS "endTime"',
'article.articleImage AS "articleImage"',
'article.createdAt AS "createdAt"',
'article.updatedAt AS "updatedAt"',
'member.id AS "memberId"',
'member.name AS "memberName"',
'member.nickname AS "memberNickname"',
'member.profileImage AS "memberProfileImage"',
'category.id AS "categoryId"',
'category.name AS "categoryName"',
'region.id AS "regionId"',
'region.name AS "regionName"',
'district.id AS "districtId"',
'district.name AS "districtName"',
])
.addSelect((qb) => {
return qb
.select("COUNT(*)")
.from("article_likes", "al")
.where("al.articleId = article.id");
}, "likeCount")
.addSelect((qb) => {
return qb
.select("COUNT(*)")
.from("participation", "p")
.where("p.articleId = article.id")
.andWhere("p.status = :status", { status: "ACTIVE" });
}, "participantCount")
.addSelect((qb) => {
return qb
.select("CASE WHEN COUNT(*) > 0 THEN TRUE ELSE FALSE END")
.from("article_likes", "al")
.where("al.articleId = article.id")
.andWhere("al.memberId = :currentMemberId", {
currentMemberId,
});
}, "isLiked")
.innerJoin("article.member", "member")
.innerJoin("article.category", "category")
.innerJoin("article.region", "region")
.innerJoin("article.district", "district")
.orderBy("article.id", "DESC")
.take(limit + 1);

if (cursor) {
query.where("article.id < :cursor", { cursor });
}

const articles = await query.getRawMany();

const transformImageUrl = (
filename: string | null,
type: "articles" | "members",
) => {
if (!filename) return null;
return new URL(
`/public/${type}/${filename}`,
this.configService.get(ENV_API_BASE_URL),
).toString();
};

const transformedArticles = articles.map((article) => ({
...article,
articleImage: transformImageUrl(article.articleImage, "articles"),
memberProfileImage: transformImageUrl(
article.memberProfileImage,
"members",
),
}));

const hasNextPage = transformedArticles.length > limit;
const results = hasNextPage
? transformedArticles.slice(0, -1)
: transformedArticles;

const lastItem = results[results.length - 1];

const nextUrl =
lastItem && hasNextPage
? new URL(
`${this.configService.get(ENV_API_BASE_URL)}/articles?cursor=${lastItem.id}&limit=${limit}`,
)
: null;

return {
data: results,
cursor: {
after: lastItem?.id,
},
count: results.length,
next: nextUrl?.toString(),
};
}

async findById(articleId: number) {
const article = await this.articlesRepository.findOne({
where: { id: articleId },
Expand Down

0 comments on commit a46af6e

Please sign in to comment.