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
188 changes: 172 additions & 16 deletions src/cardset/application/cardset.use-case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import type { ICardSetMetadataRepository } from '../domain/repository/cardset-me
import { CardsetCardDomainService } from '../domain/service/cardset-card.domain-service';
import { GroupGrpcClient } from '../infrastructure/grpc/group-grpc.client';
import { ImageGrpcClient } from '../infrastructure/grpc/image-grpc.client';
import { ReactionGrpcClient } from '../infrastructure/grpc/reaction-grpc.client';
import { UserGrpcClient } from '../infrastructure/grpc/user-grpc.client';
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';

Expand All @@ -31,6 +34,8 @@ export class CardsetUseCase {
private readonly cardsetCardDomainService: CardsetCardDomainService,
private readonly groupGrpcClient: GroupGrpcClient,
private readonly imageGrpcClient: ImageGrpcClient,
private readonly reactionGrpcClient: ReactionGrpcClient,
private readonly userGrpcClient: UserGrpcClient,
private readonly dataSource: DataSource,
@Inject(CARDSET_METADATA_REPOSITORY)
private readonly metadataRepository: ICardSetMetadataRepository,
Expand All @@ -53,25 +58,36 @@ export class CardsetUseCase {
async create(userId: number, dto: CreateCardsetRequest): Promise<Cardset> {
await this.groupGrpcClient.checkUserInGroup(dto.groupId, userId);

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const additionalManagerIds: number[] = dto.managerIds ?? [];
for (const managerId of additionalManagerIds) {
await this.groupGrpcClient.checkUserInGroup(dto.groupId, managerId);
}

return this.dataSource.transaction(async (manager) => {
const cardset = Cardset.create(dto);
const savedCardset = await this.cardsetRepository.save(cardset, manager);

const cardCount = dto.cardCount ?? 10;
const cardsToAdd = this.cardsetCardDomainService.buildCardsToAdd(
savedCardset.id,
0,
cardCount,
10,
);
for (const card of cardsToAdd) {
await this.cardRepository.save(card, manager);
}

const cardsetManager = CardsetManager.create({
const managerIds = [
userId,
cardSetId: savedCardset.id,
});
await this.cardsetManagerRepository.save(cardsetManager, manager);
...additionalManagerIds.filter((id) => id !== userId),
];
for (const managerId of managerIds) {
const cardsetManager = CardsetManager.create({
userId: managerId,
cardSetId: savedCardset.id,
});
await this.cardsetManagerRepository.save(cardsetManager, manager);
}

if (dto.imageRefId) {
await this.imageGrpcClient.activateImage(
Expand All @@ -87,12 +103,40 @@ export class CardsetUseCase {
private readonly defaultImageUrl =
process.env.DEFAULT_CARDSET_IMAGE_URL ?? '';

private readonly skipUserGrpc = process.env.SKIP_USER_GRPC === 'true';
private readonly skipReactionGrpc = process.env.SKIP_REACTION_GRPC === 'true';

private async getManagersForCardSets(
cardSetIds: number[],
): Promise<Map<number, UserInfo[]>> {
if (cardSetIds.length === 0 || this.skipUserGrpc) return new Map();
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */
const managers: CardsetManager[] =
await this.cardsetManagerRepository.findByCardSetIds(cardSetIds);
const userIds: number[] = [...new Set(managers.map((m) => m.userId))];
/* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */
const users = await this.userGrpcClient.getUsersByIds(userIds);
const userMap = new Map(users.map((u) => [Number(u.id), u]));
const result = new Map<number, UserInfo[]>();
for (const m of managers) {
const user = userMap.get(m.userId);
if (!user) continue;
const list = result.get(m.cardSetId) ?? [];
list.push(user);
result.set(m.cardSetId, list);
}
return result;
}

async findAll(userId: number): Promise<
{
cardset: Cardset;
imageUrl: string;
likeCount: number;
bookmarkCount: number;
liked: boolean;
bookmarked: boolean;
managers: UserInfo[];
}[]
> {
const cardsets = await this.cardsetRepository.findAll();
Expand All @@ -104,15 +148,27 @@ export class CardsetUseCase {
if (canView) visibleCardsets.push(cardset);
}

const metadataMap = await this.metadataRepository.findByCardSetIds(
visibleCardsets.map((c) => c.id),
);
const ids = visibleCardsets.map((c) => c.id);
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
Expand All @@ -124,6 +180,9 @@ export class CardsetUseCase {
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 result;
Expand All @@ -137,6 +196,9 @@ export class CardsetUseCase {
imageUrl: string;
likeCount: number;
bookmarkCount: number;
liked: boolean;
bookmarked: boolean;
managers: UserInfo[];
} | null> {
const cardset = await this.cardsetRepository.findById(id);
if (!cardset) return null;
Expand All @@ -148,18 +210,88 @@ export class CardsetUseCase {
if (!inGroup)
throw new BusinessException(ErrorCode.CARDSET_ACCESS_DENIED);
}
const imageUrl = cardset.imageRefId
? await this.imageGrpcClient.getImageUrl(cardset.id)
: this.defaultImageUrl;
const meta = await this.metadataRepository.findByCardSetId(id);
const [imageUrl, meta, liked, bookmarked, managersMap] = await Promise.all([
cardset.imageRefId
? this.imageGrpcClient.getImageUrl(cardset.id)
: Promise.resolve(this.defaultImageUrl),
this.metadataRepository.findByCardSetId(id),
this.skipReactionGrpc
? Promise.resolve(false)
: this.reactionGrpcClient.isLiked(id, userId),
this.skipReactionGrpc
? Promise.resolve(false)
: this.reactionGrpcClient.isBookmarked(id, userId),
this.getManagersForCardSets([id]),
]);
return {
cardset,
imageUrl,
likeCount: meta?.likeCount ?? 0,
bookmarkCount: meta?.bookmarkCount ?? 0,
liked,
bookmarked,
managers: managersMap.get(id) ?? [],
};
}

async findByGroupId(
groupId: number,
userId: number,
): Promise<
{
cardset: Cardset;
imageUrl: string;
likeCount: number;
bookmarkCount: number;
liked: boolean;
bookmarked: boolean;
managers: UserInfo[];
}[]
> {
await this.groupGrpcClient.checkUserInGroup(groupId, userId);
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */
const cardsets: Cardset[] =
await this.cardsetRepository.findByGroupId(groupId);
const ids: number[] = cardsets.map((c) => c.id);
/* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */
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 cardsets) {
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 result;
}

async update(
id: number,
userId: number,
Expand All @@ -170,9 +302,33 @@ export class CardsetUseCase {

await this.checkIsManager(id, userId);

return this.dataSource.transaction(async () => {
if (dto.imageRefId !== undefined) {
await this.imageGrpcClient.changeImage(dto.imageRefId, id);
if (dto.imageRefId !== undefined) {
await this.imageGrpcClient.changeImage(dto.imageRefId, id);
}

return this.dataSource.transaction(async (manager) => {
if (dto.managerIds !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const newManagerIds: number[] = dto.managerIds;
for (const managerId of newManagerIds) {
await this.groupGrpcClient.checkUserInGroup(
cardset.groupId,
managerId,
);
}

const existing =
await this.cardsetManagerRepository.findAllByCardSetId(id);
for (const m of existing) {
await this.cardsetManagerRepository.delete(m.id);
}
for (const managerId of newManagerIds) {
const cardsetManager = CardsetManager.create({
userId: managerId,
cardSetId: id,
});
await this.cardsetManagerRepository.save(cardsetManager, manager);
}
}

return this.cardsetRepository.update(id, dto);
Expand Down
4 changes: 2 additions & 2 deletions src/cardset/application/dto/request/create-cardset.request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ export class CreateCardsetRequest {
@ApiPropertyOptional({ example: 1001 })
imageRefId?: number | null;

@ApiPropertyOptional({ example: 10 })
cardCount?: number;
@ApiPropertyOptional({ example: [1, 2], type: [Number] })
managerIds?: number[];
}
3 changes: 3 additions & 0 deletions src/cardset/application/dto/request/update-cardset.request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ export class UpdateCardsetRequest {

@ApiPropertyOptional({ example: 1001 })
imageRefId?: number;

@ApiPropertyOptional({ example: [1, 2], type: [Number] })
managerIds?: number[];
}
74 changes: 74 additions & 0 deletions src/cardset/application/dto/response/cardset-list-item.response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Cardset } from '../../../domain/model/cardset';
import { ManagerInfoResponse } from './manager-info.response';
import type { UserInfo } from '../../../infrastructure/grpc/user-grpc.client';

export class CardsetListItemResponse {
@ApiProperty({ example: 1 })
cardSetId!: number;

@ApiProperty({ example: 1 })
groupId!: number;

@ApiProperty({ example: '영어 단어장' })
name!: string;

@ApiProperty({ example: '언어' })
category!: string;

@ApiPropertyOptional({ example: '#영어#단어' })
hashtag!: string | null;

@ApiProperty({ example: 'https://example.com/image.png' })
imageUrl!: string;

@ApiPropertyOptional({ example: 1001 })
imageRefId!: number | null;

@ApiProperty({ example: 0 })
likeCount!: number;

@ApiProperty({ example: 0 })
bookmarkCount!: number;

@ApiProperty({ example: false })
liked!: boolean;

@ApiProperty({ example: false })
bookmarked!: boolean;

@ApiProperty({ type: [ManagerInfoResponse] })
managers!: ManagerInfoResponse[];

static from(
cardset: Cardset,
imageUrl = '',
liked = false,
bookmarked = false,
managers: UserInfo[] = [],
likeCount = 0,
bookmarkCount = 0,
): CardsetListItemResponse {
const res = new CardsetListItemResponse();
res.cardSetId = cardset.id;
res.groupId = cardset.groupId;
res.name = cardset.name;
res.category = cardset.category;
res.hashtag = cardset.hashtag;
res.imageUrl = imageUrl;
res.imageRefId = cardset.imageRefId;
res.likeCount = likeCount;
res.bookmarkCount = bookmarkCount;
res.liked = liked;
res.bookmarked = bookmarked;
res.managers = managers.map((m) => {
const info = new ManagerInfoResponse();
info.id = m.id;
info.email = m.email;
info.nickname = m.nickname;
info.profileImageUrl = m.profileImageUrl;
return info;
});
return res;
}
}
Loading
Loading