Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions src/cardset/application/cardset.use-case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type { UserInfo } from '../infrastructure/grpc/user-grpc.client';
import { CreateCardsetRequest } from './dto/request/create-cardset.request';
import { UpdateCardsetRequest } from './dto/request/update-cardset.request';
import { CollaborationUseCase } from '../../collaboration/application/collaboration.use-case';
import { CardsetSearchRequest } from './dto/request/cardset-search.request';

@Injectable()
export class CardsetUseCase {
Expand Down Expand Up @@ -129,6 +130,98 @@ export class CardsetUseCase {
return result;
}

async findAllPaged(
req: CardsetSearchRequest,
userId: number,
): Promise<{
items: {
cardset: Cardset;
imageUrl: string;
likeCount: number;
bookmarkCount: number;
liked: boolean;
bookmarked: boolean;
managers: UserInfo[];
}[];
total: number;
page: number;
size: number;
}> {
const page = req.page ?? 1;
const size = req.size ?? 10;

const { items, total } = await this.cardsetRepository.findAllPaged({
page: page - 1,
size,
sortBy: req.sortBy,
order: (req.order?.toUpperCase() as 'ASC' | 'DESC') ?? 'DESC',
keyword: req.keyword,
category: req.category,
});

const visibleCardsets: Cardset[] = [];
for (const cardset of items) {
if (cardset.visibility === Visibility.PUBLIC) {
visibleCardsets.push(cardset);
continue;
}
try {
const inGroup = await this.groupGrpcClient.isUserInGroup(
cardset.groupId,
userId,
);
if (inGroup) visibleCardsets.push(cardset);
} catch {
// 그룹 조회 실패 시 해당 카드셋 제외
}
}

const ids = visibleCardsets.map((c) => c.id);

if (ids.length === 0) {
return { items: [], total, page, size };
}

const [metadataMap, likedMap, bookmarkedMap, managersMap] =
await Promise.all([
this.metadataRepository.findByCardSetIds(ids),
this.skipReactionGrpc
? Promise.resolve(new Map<number, boolean>())
: this.reactionGrpcClient.areLiked(ids, userId),
this.skipReactionGrpc
? Promise.resolve(new Map<number, boolean>())
: this.reactionGrpcClient.areBookmarked(ids, userId),
this.getManagersForCardSets(ids),
]);

const result: {
cardset: Cardset;
imageUrl: string;
likeCount: number;
bookmarkCount: number;
liked: boolean;
bookmarked: boolean;
managers: UserInfo[];
}[] = [];
for (const cardset of visibleCardsets) {
const imageUrl = cardset.imageRefId
? await this.imageGrpcClient.getImageUrl(cardset.id)
: this.defaultImageUrl;
const meta = metadataMap.get(cardset.id);
result.push({
cardset,
imageUrl,
likeCount: meta?.likeCount ?? 0,
bookmarkCount: meta?.bookmarkCount ?? 0,
liked: likedMap.get(cardset.id) ?? false,
bookmarked: bookmarkedMap.get(cardset.id) ?? false,
managers: managersMap.get(cardset.id) ?? [],
});
}

return { items: result, total, page, size };
}

async findAll(userId: number): Promise<
{
cardset: Cardset;
Expand Down Expand Up @@ -189,6 +282,85 @@ export class CardsetUseCase {
return result;
}

async findByGroupIdPaged(
groupId: number,
userId: number,
req: CardsetSearchRequest,
): Promise<{
items: {
cardset: Cardset;
imageUrl: string;
likeCount: number;
bookmarkCount: number;
liked: boolean;
bookmarked: boolean;
managers: UserInfo[];
}[];
total: number;
page: number;
size: number;
}> {
await this.groupGrpcClient.checkUserInGroup(groupId, userId);

const page = req.page ?? 1;
const size = req.size ?? 10;

const { items, total } = await this.cardsetRepository.findAllPaged({
page: page - 1,
size,
sortBy: req.sortBy,
order: (req.order?.toUpperCase() as 'ASC' | 'DESC') ?? 'DESC',
keyword: req.keyword,
category: req.category,
groupId,
});

const ids = items.map((c) => c.id);

if (ids.length === 0) {
return { items: [], total, page, size };
}

const [metadataMap, likedMap, bookmarkedMap, managersMap] =
await Promise.all([
this.metadataRepository.findByCardSetIds(ids),
this.skipReactionGrpc
? Promise.resolve(new Map<number, boolean>())
: this.reactionGrpcClient.areLiked(ids, userId),
this.skipReactionGrpc
? Promise.resolve(new Map<number, boolean>())
: this.reactionGrpcClient.areBookmarked(ids, userId),
this.getManagersForCardSets(ids),
]);

const result: {
cardset: Cardset;
imageUrl: string;
likeCount: number;
bookmarkCount: number;
liked: boolean;
bookmarked: boolean;
managers: UserInfo[];
}[] = [];
for (const cardset of items) {
const imageUrl = cardset.imageRefId
? await this.imageGrpcClient.getImageUrl(cardset.id)
: this.defaultImageUrl;
const meta = metadataMap.get(cardset.id);
result.push({
cardset,
imageUrl,
likeCount: meta?.likeCount ?? 0,
bookmarkCount: meta?.bookmarkCount ?? 0,
liked: likedMap.get(cardset.id) ?? false,
bookmarked: bookmarkedMap.get(cardset.id) ?? false,
managers: managersMap.get(cardset.id) ?? [],
});
}

return { items: result, total, page, size };
}

async findOne(
id: number,
userId: number,
Expand Down
29 changes: 29 additions & 0 deletions src/cardset/application/dto/request/cardset-search.request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ApiPropertyOptional } from '@nestjs/swagger';

export class CardsetSearchRequest {
@ApiPropertyOptional({ example: 1, description: '페이지 번호 (1부터 시작)', default: 1 })
page?: number = 1;

@ApiPropertyOptional({ example: 10, description: '페이지당 항목 수 (최대 30)', default: 10 })
size?: number = 10;

@ApiPropertyOptional({
example: 'createdAt',
description: '정렬 기준 (createdAt, name, cardCount)',
})
sortBy?: string;

@ApiPropertyOptional({
example: 'desc',
enum: ['asc', 'desc'],
description: '정렬 방향',
default: 'desc',
})
order?: string = 'desc';

@ApiPropertyOptional({ example: '영어', description: '카드셋 이름 검색 키워드' })
keyword?: string;

@ApiPropertyOptional({ example: '언어', description: '카테고리 필터' })
category?: string;
}
1 change: 1 addition & 0 deletions src/cardset/domain/repository/cardset.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type CardsetPageOptions = {
order?: 'ASC' | 'DESC';
keyword?: string;
category?: string;
groupId?: number;
};

export type CardsetPageResult = {
Expand Down
68 changes: 45 additions & 23 deletions src/cardset/infrastructure/http/cardset.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Delete,
Body,
Param,
Query,
Headers,
} from '@nestjs/common';

Expand All @@ -14,16 +15,20 @@ import {
ApiOperation,
ApiResponse as SwaggerApiResponse,
ApiParam,
ApiQuery,
ApiExtraModels,
} from '@nestjs/swagger';
import { CardsetUseCase } from '../../application/cardset.use-case';
import { CardsetSearchRequest } from '../../application/dto/request/cardset-search.request';
import { CreateCardsetRequest } from '../../application/dto/request/create-cardset.request';
import { UpdateCardsetRequest } from '../../application/dto/request/update-cardset.request';
import { CardsetCreateResponse } from '../../application/dto/response/cardset-create.response';
import { CardsetListItemResponse } from '../../application/dto/response/cardset-list-item.response';
import { CardsetResponse } from '../../application/dto/response/cardset.response';
import { ManagerInfoResponse } from '../../application/dto/response/manager-info.response';
import { YjsCardResponse } from '../../application/dto/response/yjs-card.response';
import { ApiResponse } from '../../../shared/common/api-response';
import { PagedResponse } from '../../../shared/common/paged-response';

@ApiExtraModels(CardsetResponse, ManagerInfoResponse)
@ApiTags('card-sets')
Expand All @@ -48,37 +53,54 @@ export class CardsetController {
}

@Get()
@ApiOperation({ summary: '카드셋 목록 조회' })
@SwaggerApiResponse({
status: 200,
description: '조회 성공',
type: [CardsetResponse],
@ApiOperation({ summary: '카드셋 목록 조회 (페이징)' })
@ApiQuery({ name: 'page', required: false, example: 1 })
@ApiQuery({ name: 'size', required: false, example: 10 })
@ApiQuery({
name: 'sortBy',
required: false,
example: 'id',
description: 'id | like | book',
})
@ApiQuery({
name: 'order',
required: false,
example: 'desc',
enum: ['asc', 'desc'],
})
@ApiQuery({ name: 'keyword', required: false, example: '영어' })
@ApiQuery({ name: 'category', required: false, example: '언어' })
@SwaggerApiResponse({ status: 200, description: '조회 성공' })
async findAll(
@Headers('X-USER-ID') userId: string,
): Promise<ApiResponse<CardsetResponse[]>> {
const results = await this.cardsetUseCase.findAll(parseInt(userId));
return ApiResponse.success(
results.map(
({
@Query() query: CardsetSearchRequest,
): Promise<ApiResponse<PagedResponse<CardsetListItemResponse>>> {
const { items, total, page, size } = await this.cardsetUseCase.findAllPaged(
query,
parseInt(userId),
);
const responseItems = items.map(
({
cardset,
imageUrl,
likeCount,
bookmarkCount,
liked,
bookmarked,
managers,
}) =>
CardsetListItemResponse.from(
cardset,
imageUrl,
likeCount,
bookmarkCount,
liked,
bookmarked,
managers,
}) =>
CardsetResponse.from(
cardset,
imageUrl,
likeCount,
bookmarkCount,
liked,
bookmarked,
managers,
),
),
likeCount,
bookmarkCount,
),
);
return ApiResponse.success(
PagedResponse.of(responseItems, total, page, size),
);
}

Expand Down
Loading
Loading