diff --git a/packages/backend/src/chat/chat.gateway.ts b/packages/backend/src/chat/chat.gateway.ts index 0fd6a1f0..121804f9 100644 --- a/packages/backend/src/chat/chat.gateway.ts +++ b/packages/backend/src/chat/chat.gateway.ts @@ -18,20 +18,22 @@ import { WebsocketSessionService } from '@/auth/session/websocketSession.service import { MEMORY_STORE } from '@/auth/session.module'; import { ChatService } from '@/chat/chat.service'; import { Chat } from '@/chat/domain/chat.entity'; -import { ChatScrollQuery, isChatScrollQuery } from '@/chat/dto/chat.request'; +import { + ChatMessage, + ChatScrollQuery, + isChatScrollQuery, +} from '@/chat/dto/chat.request'; import { LikeResponse } from '@/chat/dto/like.response'; +import { MentionService } from '@/chat/mention.service'; import { WebSocketExceptionFilter } from '@/middlewares/filter/webSocketException.filter'; import { StockService } from '@/stock/stock.service'; - -interface chatMessage { - room: string; - content: string; -} +import { User } from '@/user/domain/user.entity'; interface chatResponse { likeCount: number; message: string; type: string; + mentioned: boolean; createdAt: Date; } @@ -39,13 +41,15 @@ interface chatResponse { @UseFilters(WebSocketExceptionFilter) export class ChatGateway implements OnGatewayConnection { @WebSocketServer() - server: Server; - websocketSessionService: WebsocketSessionService; + private server: Server; + private websocketSessionService: WebsocketSessionService; + private users = new Map(); constructor( @Inject('winston') private readonly logger: Logger, private readonly stockService: StockService, private readonly chatService: ChatService, + private readonly mentionService: MentionService, @Inject(MEMORY_STORE) sessionStore: MemoryStore, ) { this.websocketSessionService = new WebsocketSessionService(sessionStore); @@ -54,10 +58,10 @@ export class ChatGateway implements OnGatewayConnection { @UseGuards(WebSocketSessionGuard) @SubscribeMessage('chat') async handleConnectStock( - @MessageBody() message: chatMessage, + @MessageBody() message: ChatMessage, @ConnectedSocket() client: SessionSocket, ) { - const { room, content } = message; + const { room, content, mention } = message; if (!client.rooms.has(room)) { client.emit('error', 'You are not in the room'); this.logger.warn(`client is not in the room ${room}`); @@ -72,6 +76,17 @@ export class ChatGateway implements OnGatewayConnection { stockId: room, message: content, }); + if (mention) { + await this.mentionService.createMention(savedChat.id, mention); + const mentionedSocket = this.users.get(Number(mention)); + if (mentionedSocket) { + const chatResponse = this.toResponse(savedChat); + this.server.to(room).except(mentionedSocket).emit('chat', chatResponse); + chatResponse.mentioned = true; + this.server.to(mentionedSocket).emit('chat', chatResponse); + return; + } + } this.server.to(room).emit('chat', this.toResponse(savedChat)); } @@ -86,15 +101,12 @@ export class ChatGateway implements OnGatewayConnection { const { stockId, pageSize } = await this.getChatScrollQuery(client); await this.validateExistStock(stockId); client.join(stockId); - const messages = await this.chatService.scrollChat( - { - stockId, - pageSize, - }, - user?.id, - ); + const messages = await this.scrollChat(stockId, user, pageSize); this.logger.info(`client joined room ${stockId}`); client.emit('chat', messages); + if (user) { + this.users.set(user.id, client.id); + } } catch (e) { const error = e as Error; this.logger.warn(error.message); @@ -103,6 +115,20 @@ export class ChatGateway implements OnGatewayConnection { } } + private async scrollChat( + stockId: string, + user: User | null, + pageSize?: number, + ) { + return await this.chatService.scrollChat( + { + stockId, + pageSize, + }, + user?.id, + ); + } + private async validateExistStock(stockId: string): Promise { if (!(await this.stockService.checkStockExist(stockId))) { throw new Error(`Stock does not exist: ${stockId}`); @@ -126,6 +152,7 @@ export class ChatGateway implements OnGatewayConnection { likeCount: chat.likeCount, message: chat.message, type: chat.type, + mentioned: false, createdAt: chat.date?.createdAt || new Date(), }; } diff --git a/packages/backend/src/chat/chat.module.ts b/packages/backend/src/chat/chat.module.ts index 62dc1c29..b58dc484 100644 --- a/packages/backend/src/chat/chat.module.ts +++ b/packages/backend/src/chat/chat.module.ts @@ -6,12 +6,18 @@ import { ChatGateway } from '@/chat/chat.gateway'; import { ChatService } from '@/chat/chat.service'; import { Chat } from '@/chat/domain/chat.entity'; import { Like } from '@/chat/domain/like.entity'; +import { Mention } from '@/chat/domain/mention.entity'; import { LikeService } from '@/chat/like.service'; +import { MentionService } from '@/chat/mention.service'; import { StockModule } from '@/stock/stock.module'; @Module({ - imports: [TypeOrmModule.forFeature([Chat, Like]), StockModule, SessionModule], + imports: [ + TypeOrmModule.forFeature([Chat, Like, Mention]), + StockModule, + SessionModule, + ], controllers: [ChatController], - providers: [ChatGateway, ChatService, LikeService], + providers: [ChatGateway, ChatService, LikeService, MentionService], }) export class ChatModule {} diff --git a/packages/backend/src/chat/chat.service.ts b/packages/backend/src/chat/chat.service.ts index 80417656..03bdaa17 100644 --- a/packages/backend/src/chat/chat.service.ts +++ b/packages/backend/src/chat/chat.service.ts @@ -102,27 +102,41 @@ export class ChatService { userId?: number, order: Order = ORDER.LATEST, ) { - const queryBuilder = this.dataSource.createQueryBuilder(Chat, 'chat'); const { stockId, latestChatId, pageSize } = chatScrollQuery; const size = pageSize ? pageSize : DEFAULT_PAGE_SIZE; + const queryBuilder = await this.buildInitialChatScrollQuery( + stockId, + size, + userId, + ); + if (order === ORDER.LIKE) { + return this.buildLikeCountQuery(queryBuilder, latestChatId); + } + return this.buildLatestChatIdQuery(queryBuilder, latestChatId); + } - queryBuilder + private async buildInitialChatScrollQuery( + stockId: string, + size: number, + userId?: number, + ) { + console.log('stockId', stockId); + return this.dataSource + .createQueryBuilder(Chat, 'chat') .leftJoinAndSelect('chat.likes', 'like', 'like.user_id = :userId', { userId, }) + .leftJoinAndSelect( + 'chat.mentions', + 'mention', + 'mention.user_id = :userId', + { + userId, + }, + ) .leftJoinAndSelect('chat.user', 'user') .where('chat.stock_id = :stockId', { stockId }) .take(size + 1); - - if (order === ORDER.LIKE) { - return this.buildLikeCountQuery(queryBuilder, latestChatId); - } - queryBuilder.orderBy('chat.id', 'DESC'); - if (latestChatId) { - queryBuilder.andWhere('chat.id < :latestChatId', { latestChatId }); - } - - return queryBuilder; } private async buildLikeCountQuery( @@ -150,4 +164,15 @@ export class ChatService { } return queryBuilder; } + + private async buildLatestChatIdQuery( + queryBuilder: SelectQueryBuilder, + latestChatId?: number, + ) { + queryBuilder.orderBy('chat.id', 'DESC'); + if (latestChatId) { + queryBuilder.andWhere('chat.id < :latestChatId', { latestChatId }); + } + return queryBuilder; + } } diff --git a/packages/backend/src/chat/domain/chat.entity.ts b/packages/backend/src/chat/domain/chat.entity.ts index 2a5ab380..ccaec4c8 100644 --- a/packages/backend/src/chat/domain/chat.entity.ts +++ b/packages/backend/src/chat/domain/chat.entity.ts @@ -12,6 +12,7 @@ import { Like } from '@/chat/domain/like.entity'; import { DateEmbedded } from '@/common/dateEmbedded.entity'; import { Stock } from '@/stock/domain/stock.entity'; import { User } from '@/user/domain/user.entity'; +import { Mention } from '@/chat/domain/mention.entity'; @Entity() export class Chat { @@ -41,4 +42,7 @@ export class Chat { @Column(() => DateEmbedded, { prefix: '' }) date: DateEmbedded; + + @OneToMany(() => Mention, (mention) => mention.chat) + mentions: Mention[]; } diff --git a/packages/backend/src/chat/domain/like.entity.ts b/packages/backend/src/chat/domain/like.entity.ts index 84238b15..261e96e8 100644 --- a/packages/backend/src/chat/domain/like.entity.ts +++ b/packages/backend/src/chat/domain/like.entity.ts @@ -23,6 +23,6 @@ export class Like { @JoinColumn({ name: 'user_id' }) user: User; - @CreateDateColumn({ name: 'created_at' }) + @CreateDateColumn({ name: 'created_at', type: 'timestamp' }) createdAt: Date; } diff --git a/packages/backend/src/chat/domain/mention.entity.ts b/packages/backend/src/chat/domain/mention.entity.ts new file mode 100644 index 00000000..c68ca8fb --- /dev/null +++ b/packages/backend/src/chat/domain/mention.entity.ts @@ -0,0 +1,28 @@ +import { + CreateDateColumn, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { Chat } from '@/chat/domain/chat.entity'; +import { User } from '@/user/domain/user.entity'; + +@Index('chat_user_unique', ['chat', 'user']) +@Entity() +export class Mention { + @PrimaryGeneratedColumn() + id: number; + + @ManyToOne(() => Chat, (chat) => chat.id) + @JoinColumn({ name: 'chat_id' }) + chat: Chat; + + @ManyToOne(() => User, (user) => user.id) + @JoinColumn({ name: 'user_id' }) + user: User; + + @CreateDateColumn({ name: 'created_at', type: 'timestamp' }) + createdAt: Date; +} diff --git a/packages/backend/src/chat/dto/chat.request.ts b/packages/backend/src/chat/dto/chat.request.ts index 0c68943b..3d970fdd 100644 --- a/packages/backend/src/chat/dto/chat.request.ts +++ b/packages/backend/src/chat/dto/chat.request.ts @@ -33,17 +33,20 @@ export function isChatScrollQuery(object: unknown): object is ChatScrollQuery { if (typeof object !== 'object' || object === null) { return false; } - if (!('stockId' in object) || typeof object.stockId !== 'string') { return false; } - if ( 'latestChatId' in object && !Number.isInteger(Number(object.latestChatId)) ) { return false; } - return !('pageSize' in object && !Number.isInteger(Number(object.pageSize))); } + +export interface ChatMessage { + room: string; + content: string; + mention?: number; +} diff --git a/packages/backend/src/chat/dto/chat.response.ts b/packages/backend/src/chat/dto/chat.response.ts index bf42c903..68a81b4d 100644 --- a/packages/backend/src/chat/dto/chat.response.ts +++ b/packages/backend/src/chat/dto/chat.response.ts @@ -9,6 +9,7 @@ interface ChatResponse { type: string; liked: boolean; nickname: string; + mentioned: boolean; createdAt: Date; } @@ -43,6 +44,7 @@ export class ChatScrollResponse { type: chat.type, createdAt: chat.date!.createdAt, liked: !!(chat.likes && chat.likes.length > 0), + mentioned: chat.mentions && chat.mentions.length > 0, nickname: chat.user.nickname, })); this.hasMore = hasMore; diff --git a/packages/backend/src/chat/mention.service.ts b/packages/backend/src/chat/mention.service.ts new file mode 100644 index 00000000..f5e49133 --- /dev/null +++ b/packages/backend/src/chat/mention.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@nestjs/common'; +import { DataSource, EntityManager } from 'typeorm'; +import { Chat } from '@/chat/domain/chat.entity'; +import { Mention } from '@/chat/domain/mention.entity'; +import { User } from '@/user/domain/user.entity'; + +@Injectable() +export class MentionService { + constructor(private readonly dataSource: DataSource) {} + + async createMention(chatId: number, userId: number) { + return this.dataSource.transaction(async (manager) => { + if (!(await this.existsChatAndUser(chatId, userId, manager))) { + return null; + } + return await manager.save(Mention, { + chat: { id: chatId }, + user: { id: userId }, + }); + }); + } + + async existsChatAndUser( + chatId: number, + userId: number, + manager: EntityManager, + ) { + if (!(await manager.exists(User, { where: { id: userId } }))) { + return false; + } + return await manager.exists(Chat, { + where: { id: chatId }, + }); + } +} diff --git a/packages/backend/src/stock/stock.controller.ts b/packages/backend/src/stock/stock.controller.ts index 5ce27bf9..6f18c657 100644 --- a/packages/backend/src/stock/stock.controller.ts +++ b/packages/backend/src/stock/stock.controller.ts @@ -177,6 +177,40 @@ export class StockController { return await this.stockService.searchStock(request.name); } + @Get('topViews') + @ApiGetStocks('조회수 기반 주식 리스트 조회 API') + async getTopStocksByViews(@LimitQuery(5) limit: number) { + return await this.stockService.getTopStocksByViews(limit); + } + + @Get('topGainers') + @ApiGetStocks('가격 상승률 기반 주식 리스트 조회 API') + async getTopStocksByGainers(@LimitQuery(20) limit: number) { + return await this.stockService.getTopStocksByGainers(limit); + } + + @Get('topLosers') + @ApiGetStocks('가격 하락률 기반 주식 리스트 조회 API') + async getTopStocksByLosers(@LimitQuery(20) limit: number) { + return await this.stockService.getTopStocksByLosers(limit); + } + + @ApiOperation({ + summary: '주식 상세 정보 조회 API', + description: '시가 총액, EPS, PER, 52주 최고가, 52주 최저가를 조회합니다', + }) + @ApiOkResponse({ + description: '주식 상세 정보 조회 성공', + type: StockDetailResponse, + }) + @ApiParam({ name: 'stockId', required: true, description: '주식 ID' }) + @Get(':stockId/detail') + async getStockDetail( + @Param('stockId') stockId: string, + ): Promise { + return await this.stockDetailService.getStockDetailByStockId(stockId); + } + @Get('/:stockId') @ApiGetStockData('주식 시간 단위 데이터 조회 API', '일') async getStockDataDaily( @@ -198,40 +232,6 @@ export class StockController { } } - @ApiOperation({ - summary: '주식 상세 정보 조회 API', - description: '시가 총액, EPS, PER, 52주 최고가, 52주 최저가를 조회합니다', - }) - @ApiOkResponse({ - description: '주식 상세 정보 조회 성공', - type: StockDetailResponse, - }) - @ApiParam({ name: 'stockId', required: true, description: '주식 ID' }) - @Get(':stockId/detail') - async getStockDetail( - @Param('stockId') stockId: string, - ): Promise { - return await this.stockDetailService.getStockDetailByStockId(stockId); - } - - @Get('topViews') - @ApiGetStocks('조회수 기반 주식 리스트 조회 API') - async getTopStocksByViews(@LimitQuery(5) limit: number) { - return await this.stockService.getTopStocksByViews(limit); - } - - @Get('topGainers') - @ApiGetStocks('가격 상승률 기반 주식 리스트 조회 API') - async getTopStocksByGainers(@LimitQuery(20) limit: number) { - return await this.stockService.getTopStocksByGainers(limit); - } - - @Get('topLosers') - @ApiGetStocks('가격 하락률 기반 주식 리스트 조회 API') - async getTopStocksByLosers(@LimitQuery(20) limit: number) { - return await this.stockService.getTopStocksByLosers(limit); - } - private getStockDataYearly( stockId: string, lastStartTime: string | undefined, diff --git a/packages/backend/src/user/domain/user.entity.ts b/packages/backend/src/user/domain/user.entity.ts index 26cdb345..9f93e968 100644 --- a/packages/backend/src/user/domain/user.entity.ts +++ b/packages/backend/src/user/domain/user.entity.ts @@ -9,7 +9,9 @@ import { DateEmbedded } from '@/common/dateEmbedded.entity'; import { UserStock } from '@/stock/domain/userStock.entity'; import { OauthType } from '@/user/domain/ouathType'; import { Role } from '@/user/domain/role'; +import { Mention } from '@/chat/domain/mention.entity'; +@Index('nickname_sub_name', ['nickname', 'subName'], { unique: true }) @Index('type_oauth_id', ['type', 'oauthId'], { unique: true }) @Entity({ name: 'users' }) export class User { @@ -19,6 +21,9 @@ export class User { @Column({ length: 50 }) nickname: string; + @Column({ length: 10, default: '0001' }) + subName: string; + @Column({ length: 50 }) email: string; @@ -39,4 +44,7 @@ export class User { @OneToMany(() => UserStock, (userStock) => userStock.user) userStocks: UserStock[]; + + @OneToMany(() => Mention, (mention) => mention.user) + mentions: Mention[]; } diff --git a/packages/backend/src/user/dto/User.response.ts b/packages/backend/src/user/dto/User.response.ts new file mode 100644 index 00000000..98f37930 --- /dev/null +++ b/packages/backend/src/user/dto/User.response.ts @@ -0,0 +1,33 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { User } from '@/user/domain/user.entity'; + +interface UserResponse { + id: number; + nickname: string; + subName: string; + createdAt: Date; +} + +export class UserSearchResult { + @ApiProperty({ + description: '유저 검색 결과', + example: [ + { + id: 1, + nickname: 'nickname', + subName: 'subName', + createdAt: new Date(), + }, + ], + }) + result: UserResponse[]; + + constructor(users: User[]) { + this.result = users.map((user) => ({ + id: user.id, + nickname: user.nickname, + subName: user.subName, + createdAt: user.date.createdAt, + })); + } +} diff --git a/packages/backend/src/user/user.controller.ts b/packages/backend/src/user/user.controller.ts index 748ae2b7..17d88491 100644 --- a/packages/backend/src/user/user.controller.ts +++ b/packages/backend/src/user/user.controller.ts @@ -1,11 +1,12 @@ import { - Controller, - Patch, - Param, Body, + Controller, + Get, HttpCode, HttpStatus, - Get, + Param, + Patch, + Query, } from '@nestjs/common'; import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; import { UpdateUserThemeResponse } from './dto/userTheme.response'; @@ -15,6 +16,23 @@ import { UserService } from './user.service'; export class UserController { constructor(private readonly userService: UserService) {} + @Get() + @ApiOperation({ + summary: '유저 닉네임과 서브 닉네임으로 유저 조회 API', + description: '유저 닉네임과 서브 닉네임으로 유저를 조회합니다.', + }) + @ApiParam({ name: 'nickname', type: 'string', description: '유저 닉네임' }) + @ApiParam({ name: 'subName', type: 'string', description: '유저 서브네임' }) + async searchUser( + @Query('nickname') nickname: string, + @Query('subName') subName: string, + ) { + return await this.userService.searchUserByNicknameAndSubName( + nickname, + subName, + ); + } + @Patch(':id/theme') @HttpCode(HttpStatus.OK) @ApiOperation({ diff --git a/packages/backend/src/user/user.service.spec.ts b/packages/backend/src/user/user.service.spec.ts index 6af944a1..e4907618 100644 --- a/packages/backend/src/user/user.service.spec.ts +++ b/packages/backend/src/user/user.service.spec.ts @@ -5,15 +5,15 @@ import { User } from './domain/user.entity'; import { OauthType } from '@/user/domain/ouathType'; import { UserService } from '@/user/user.service'; +const defaultManagerMock: Partial = { + findOne: jest.fn(), + save: jest.fn(), + exists: jest.fn(), +}; + export function createDataSourceMock( managerMock?: Partial, ): Partial { - const defaultManagerMock: Partial = { - findOne: jest.fn(), - save: jest.fn(), - exists: jest.fn(), - }; - return { getRepository: managerMock?.getRepository, transaction: jest.fn().mockImplementation(async (work) => { @@ -22,6 +22,14 @@ export function createDataSourceMock( }; } +export function createManagerDataSourceMock( + managerMock?: Partial, +) { + return { + manager: managerMock, + }; +} + describe('UserService 테스트', () => { const registerRequest = { email: 'test@naver.com', @@ -58,6 +66,45 @@ describe('UserService 테스트', () => { ).rejects.toThrow('user already exists'); }); + test('같은 닉네임이 없을 때 기본 서브 닉네임을 생성한다.', async () => { + const managerMock = { + exists: jest.fn().mockResolvedValueOnce(false), + save: jest.fn().mockResolvedValue(registerRequest), + }; + const dataSource = createDataSourceMock(managerMock); + const userService = new UserService(dataSource as DataSource); + + const subName = await userService.createSubName('test'); + + expect(subName).toBe('0001'); + }); + + test.each([ + ['0001', '0002'], + ['0009', '0010'], + ['0099', '0100'], + ['0999', '1000'], + ])( + '같은 닉네임이 있을 때 현제 서브네임 최대 값에서 1을 더한 값이 생성', + async (maxSubName, newSubName) => { + const managerMock = { + exists: jest.fn().mockResolvedValue(true), + save: jest.fn().mockResolvedValue(registerRequest), + createQueryBuilder: jest.fn().mockReturnValue({ + select: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + getRawOne: jest.fn().mockResolvedValue({ max: maxSubName }), + }), + }; + const dataSource = createDataSourceMock(managerMock); + const userService = new UserService(dataSource as DataSource); + + const subName = await userService.createSubName('test'); + + expect(subName).toBe(newSubName); + }, + ); + test('유저 테마를 업데이트한다', async () => { const userId = 1; const isLight = false; @@ -117,7 +164,7 @@ describe('UserService 테스트', () => { const managerMock = { findOne: jest.fn().mockResolvedValue(mockUser), }; - const dataSource = createDataSourceMock(managerMock); + const dataSource = createManagerDataSourceMock(managerMock); const userService = new UserService(dataSource as DataSource); const result = await userService.getUserTheme(userId); @@ -135,7 +182,7 @@ describe('UserService 테스트', () => { const managerMock = { findOne: jest.fn().mockResolvedValue(null), }; - const dataSource = createDataSourceMock(managerMock); + const dataSource = createManagerDataSourceMock(managerMock); const userService = new UserService(dataSource as DataSource); await expect(userService.getUserTheme(userId)).rejects.toThrow( diff --git a/packages/backend/src/user/user.service.ts b/packages/backend/src/user/user.service.ts index dbef92dd..a132262d 100644 --- a/packages/backend/src/user/user.service.ts +++ b/packages/backend/src/user/user.service.ts @@ -3,10 +3,11 @@ import { Injectable, NotFoundException, } from '@nestjs/common'; -import { DataSource, EntityManager } from 'typeorm'; +import { DataSource, EntityManager, Like } from 'typeorm'; import { OauthType } from './domain/ouathType'; import { User } from './domain/user.entity'; import { status, subject } from '@/user/constants/randomNickname'; +import { UserSearchResult } from '@/user/dto/User.response'; type RegisterRequest = Required< Pick @@ -19,25 +20,52 @@ export class UserService { async register({ nickname, email, type, oauthId }: RegisterRequest) { return await this.dataSource.transaction(async (manager) => { await this.validateUserExists(type, oauthId, manager); + const subName = await this.createSubName(nickname); return await manager.save(User, { nickname, email, type, oauthId, + subName, }); }); } + async searchUserByNicknameAndSubName(nickname: string, subName?: string) { + const users = await this.dataSource.manager.find(User, { + where: { nickname: Like(`%${nickname}%`), subName: Like(`${subName}%`) }, + take: 10, + }); + return new UserSearchResult(users); + } + + async createSubName(nickname: string) { + return this.dataSource.transaction(async (manager) => { + console.log(await this.existsUserByNickname(nickname, manager)); + if (!(await this.existsUserByNickname(nickname, manager))) { + return '0001'; + } + + const maxSubName = await manager + .createQueryBuilder(User, 'user') + .select('MAX(user.subName)', 'max') + .where('user.nickname = :nickname', { nickname }) + .getRawOne(); + console.log(maxSubName); + return (parseInt(maxSubName.max, 10) + 1).toString().padStart(4, '0'); + }); + } + + existsUserByNickname(nickname: string, manager: EntityManager) { + return manager.exists(User, { where: { nickname } }); + } + async registerTester() { - return await this.dataSource.transaction(async (manager) => { - return await manager.save(User, { - nickname: this.generateRandomNickname(), - email: 'tester@nav', - type: OauthType.LOCAL, - oauthId: String( - (await this.getMaxOauthId(OauthType.LOCAL, manager)) + 1, - ), - }); + return this.register({ + nickname: this.generateRandomNickname(), + email: 'tester@nav', + type: OauthType.LOCAL, + oauthId: String((await this.getMaxOauthId(OauthType.LOCAL)) + 1), }); } @@ -83,8 +111,8 @@ export class UserService { return `${statusName}${subjectName}`; } - private async getMaxOauthId(oauthType: OauthType, manager: EntityManager) { - const result = await manager + private async getMaxOauthId(oauthType: OauthType) { + const result = await this.dataSource.manager .createQueryBuilder(User, 'user') .select('MAX(user.oauthId)', 'max') .where('user.type = :oauthType', { oauthType })