Skip to content

Commit

Permalink
Merge pull request #605 from boostcampwm2023/BE-SeparateUserService-#599
Browse files Browse the repository at this point in the history
[BE/#599] userService 관심사 분리
  • Loading branch information
namewhat99 authored Jan 12, 2024
2 parents 3dbb785 + 094ab83 commit b15592f
Show file tree
Hide file tree
Showing 14 changed files with 321 additions and 187 deletions.
2 changes: 2 additions & 0 deletions BE/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { CacheModule } from '@nestjs/cache-manager';
import { RedisConfigProvider } from './config/redis.config';
import { ReportModule } from './report/report.module';
import { ImageModule } from './image/image.module';
import { NotificationModule } from './notification/notification.module';

@Module({
imports: [
Expand All @@ -42,6 +43,7 @@ import { ImageModule } from './image/image.module';
ChatModule,
ReportModule,
ImageModule,
NotificationModule,
],
controllers: [AppController],
providers: [
Expand Down
52 changes: 0 additions & 52 deletions BE/src/common/S3Handler.ts

This file was deleted.

2 changes: 1 addition & 1 deletion BE/src/entities/blockUser.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class BlockUserEntity {
delete_date: Date;

@ManyToOne(() => UserEntity, (blocker) => blocker.user_hash)
@JoinColumn({ name: 'blocker' })
@JoinColumn({ name: 'blocker', referencedColumnName: 'user_hash' })
blockerUser: UserEntity;

@ManyToOne(() => UserEntity, (blocked) => blocked.user_hash)
Expand Down
8 changes: 6 additions & 2 deletions BE/src/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,17 @@ export class UserEntity {
@OneToMany(() => PostEntity, (post) => post.user)
posts: PostEntity[];

@OneToMany(() => BlockUserEntity, (blockUser) => blockUser.blocker)
@OneToMany(() => BlockUserEntity, (blockUser) => blockUser.blockerUser, {
cascade: ['soft-remove'],
})
blocker: BlockUserEntity[];

@OneToMany(() => BlockUserEntity, (blockUser) => blockUser.blocked_user)
blocked: BlockUserEntity[];

@OneToMany(() => BlockPostEntity, (blockUser) => blockUser.blocker)
@OneToMany(() => BlockPostEntity, (blockUser) => blockUser.blockerUser, {
cascade: ['soft-remove'],
})
blocker_post: BlockPostEntity[];

@OneToOne(
Expand Down
9 changes: 9 additions & 0 deletions BE/src/notification/notification.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { NotificationService } from './notification.service';
import { RegistrationTokenRepository } from './registrationToken.repository';

@Module({
exports: [NotificationService],
providers: [NotificationService, RegistrationTokenRepository],
})
export class NotificationModule {}
102 changes: 102 additions & 0 deletions BE/src/notification/notification.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Test, TestingModule } from '@nestjs/testing';
import { NotificationService } from './notification.service';
import { ConfigService } from '@nestjs/config';
import { RegistrationTokenRepository } from './registrationToken.repository';
import { RegistrationTokenEntity } from '../entities/registrationToken.entity';
import { PushMessage } from '../common/fcmHandler';

const mockRepository = {
save: jest.fn(),
delete: jest.fn(),
findOne: jest.fn(),
update: jest.fn(),
};

const mockAdmin = jest.requireMock('firebase-admin');
jest.mock('firebase-admin');
mockAdmin.apps = [];

describe('NotificationService', () => {
let service: NotificationService;
let repository;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
NotificationService,
{
provide: RegistrationTokenRepository,
useValue: mockRepository,
},
{
provide: ConfigService,
useValue: { get: jest.fn((key: string) => 'mocked-value') },
},
],
}).compile();

service = module.get<NotificationService>(NotificationService);
repository = module.get(RegistrationTokenRepository);
});

it('should be defined', () => {
expect(service).toBeDefined();
});

describe('getRegistrationToken', function () {
it('should return null when token does not exist', async function () {
repository.findOne.mockResolvedValue(null);
const res = await service.getRegistrationToken('user');
expect(res).toEqual(null);
});

it('should return registration token', async function () {
const registrationToken = new RegistrationTokenEntity();
registrationToken.registration_token = 'test';
repository.findOne.mockResolvedValue(registrationToken);
const res = await service.getRegistrationToken('user');
expect(res).toEqual('test');
});
});

describe('registerToken', function () {
it('should create token', async function () {
repository.findOne.mockResolvedValue(null);
await service.registerToken('user', 'token');
expect(repository.save).toHaveBeenCalled();
});

it('should update token', async function () {
repository.findOne.mockResolvedValue(new RegistrationTokenEntity());
await service.registerToken('user', 'token');
expect(repository.update).toHaveBeenCalled();
});
});

describe('removeRegistrationToken', function () {
it('should remove', async function () {
await service.removeRegistrationToken('userId');
expect(repository.delete).toHaveBeenCalled();
});
});

describe('createChatNotificationMessage', function () {
it('should return message', function () {
const pushMessage: PushMessage = {
body: 'message',
data: {
room_id: '123',
},
title: 'nickname',
};
const result = service.createChatNotificationMessage(
'token',
pushMessage,
);
expect(result.token).toEqual('token');
expect(result.notification.title).toEqual('nickname');
expect(result.notification.body).toEqual('message');
expect(result.data.room_id).toEqual('123');
});
});
});
90 changes: 90 additions & 0 deletions BE/src/notification/notification.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import admin from 'firebase-admin';
import { PushMessage } from '../common/fcmHandler';
import { RegistrationTokenRepository } from './registrationToken.repository';

@Injectable()
export class NotificationService {
private readonly logger = new Logger('ChatsGateway');
constructor(
private configService: ConfigService,
private registrationTokenRepository: RegistrationTokenRepository,
) {
if (admin.apps.length === 0) {
admin.initializeApp({
credential: admin.credential.cert(
this.configService.get('GOOGLE_APPLICATION_CREDENTIALS'),
),
});
this.logger.log('Firebase Admin initialized');
}
}

async sendChatNotification(userId: string, pushMessage: PushMessage) {
const registrationToken = await this.getRegistrationToken(userId);
if (!registrationToken) {
throw new Error('no registration token');
}
const message = this.createChatNotificationMessage(
registrationToken,
pushMessage,
);
try {
const response = await admin.messaging().send(message);
this.logger.debug(
`Push Notification Success : ${response} `,
'FcmHandler',
);
} catch (e) {
throw new Error('fail to send chat notification');
}
}

createChatNotificationMessage(
registrationToken: string,
pushMessage: PushMessage,
) {
return {
token: registrationToken,
notification: {
title: pushMessage.title,
body: pushMessage.body,
},
apns: {
payload: {
aps: {
sound: 'default',
},
},
},
data: {
...pushMessage.data,
},
};
}

async getRegistrationToken(userId: string): Promise<string> {
const registrationToken =
await this.registrationTokenRepository.findOne(userId);
if (registrationToken === null) {
this.logger.error('토큰이 없습니다.', 'FcmHandler');
return null;
}
return registrationToken.registration_token;
}

async registerToken(userId, registrationToken) {
const registrationTokenEntity =
await this.registrationTokenRepository.findOne(userId);
if (registrationTokenEntity === null) {
await this.registrationTokenRepository.save(userId, registrationToken);
} else {
await this.registrationTokenRepository.update(userId, registrationToken);
}
}

async removeRegistrationToken(userId: string) {
await this.registrationTokenRepository.delete(userId);
}
}
39 changes: 39 additions & 0 deletions BE/src/notification/registrationToken.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Inject, Injectable, Scope } from '@nestjs/common';
import { BaseRepository } from '../common/base.repository';
import { DataSource } from 'typeorm';
import { REQUEST } from '@nestjs/core';
import { RegistrationTokenEntity } from '../entities/registrationToken.entity';

@Injectable({ scope: Scope.REQUEST })
export class RegistrationTokenRepository extends BaseRepository {
constructor(dataSource: DataSource, @Inject(REQUEST) req: Request) {
super(dataSource, req);
}
async findOne(userId: string): Promise<RegistrationTokenEntity> {
return await this.getRepository(RegistrationTokenEntity).findOne({
where: { user_hash: userId },
});
}

async save(userId: string, registrationToken: string) {
await this.getRepository(RegistrationTokenEntity).save({
user_hash: userId,
registration_token: registrationToken,
});
}

async update(userId: string, registrationToken: string) {
await this.getRepository(RegistrationTokenEntity).update(
{
user_hash: userId,
},
{ registration_token: registrationToken },
);
}

async delete(userId: string) {
await this.getRepository(RegistrationTokenEntity).delete({
user_hash: userId,
});
}
}
3 changes: 1 addition & 2 deletions BE/src/post/post.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { PostController } from './post.controller';
import { PostService } from './post.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PostEntity } from '../entities/post.entity';
import { S3Handler } from '../common/S3Handler';
import { PostImageEntity } from '../entities/postImage.entity';
import { BlockUserEntity } from '../entities/blockUser.entity';
import { BlockPostEntity } from '../entities/blockPost.entity';
Expand All @@ -22,6 +21,6 @@ import { ImageModule } from '../image/image.module';
ImageModule,
],
controllers: [PostController],
providers: [PostService, S3Handler, AuthGuard, PostRepository],
providers: [PostService, AuthGuard, PostRepository],
})
export class PostModule {}
6 changes: 3 additions & 3 deletions BE/src/users/dto/usersUpdate.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export class UpdateUsersDto {
@IsString()
nickname?: string;

@IsOptional() // 이 필드는 선택적으로 업데이트할 수 있도록 설정
@IsBoolean()
is_image_changed?: string;
// @IsOptional() // 이 필드는 선택적으로 업데이트할 수 있도록 설정
// @IsBoolean()
// is_image_changed?: string;
}
20 changes: 20 additions & 0 deletions BE/src/users/user.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Inject, Injectable, Scope } from '@nestjs/common';
import { BaseRepository } from '../common/base.repository';
import { DataSource } from 'typeorm';
import { REQUEST } from '@nestjs/core';
import { UserEntity } from '../entities/user.entity';

@Injectable({ scope: Scope.REQUEST })
export class UserRepository extends BaseRepository {
constructor(dataSource: DataSource, @Inject(REQUEST) req: Request) {
super(dataSource, req);
}

async softDeleteCascade(userId: string) {
const user = await this.getRepository(UserEntity).findOne({
where: { user_hash: userId },
relations: ['blocker_post', 'blocker'],
});
await this.getRepository(UserEntity).softRemove(user);
}
}
Loading

0 comments on commit b15592f

Please sign in to comment.