diff --git a/src/common/api/response-code.ts b/src/common/api/response-code.ts index 64c8151..2cce618 100644 --- a/src/common/api/response-code.ts +++ b/src/common/api/response-code.ts @@ -86,6 +86,14 @@ export class ResponseCode { code: HttpStatus.OK, message: '게시글 삭제 성공', }; + static readonly CHAT_READ_SUCCESS = { + code: HttpStatus.OK, + message: '채팅 조회 성공', + }; + static readonly CHAT_DELETE_SUCCESS = { + code: HttpStatus.OK, + message: '채팅 삭제 성공', + }; //201 static readonly USER_CREATED_SUCCESS = { @@ -138,6 +146,11 @@ export class ResponseCode { message: '다른 가족의 정보를 조회할 수 없습니다.', }; + static readonly CHAT_FORBIDDEN = { + code: HttpStatus.FORBIDDEN, + message: '다른 가족 채팅방의 정보를 조회할 수 없습니다.', + }; + //404 static readonly USER_NOT_FOUND = { code: HttpStatus.NOT_FOUND, @@ -173,6 +186,10 @@ export class ResponseCode { code: HttpStatus.NOT_FOUND, message: '게시글 조회 실패', }; + static readonly CHAT_NOT_FOUND = { + code: HttpStatus.NOT_FOUND, + message: '채팅 조회 실패', + }; //409 static readonly USER_ALREADY_EXIST = { diff --git a/src/common/util/interactionType.ts b/src/common/util/interactionType.ts index d8b01ec..3dc3c61 100644 --- a/src/common/util/interactionType.ts +++ b/src/common/util/interactionType.ts @@ -11,23 +11,23 @@ export class InteractionType { switch (type) { case 1: return new InteractionType( - '팔로우', - `${source}님이 ${target}님을 팔로우 하셨습니다.`, + '콕콕!', + `${source}님이 ${target}님을 찔렀어요.`, ); case 2: return new InteractionType( - '찌르기', - `${source}님이 ${target}님을 찌르셨습니다.`, + '하트!', + `${source}님이 ${target}님에게 하트를 보냈어요.`, ); case 3: return new InteractionType( - '좋아요', - `${source}님이 ${target}님의 게시물을 좋아합니다.`, + '우우우~!', + `${source}님이 ${target}에게 야유를 보냈어요.`, ); case 4: return new InteractionType( - '댓글', - `${source}님이 ${target}님의 게시물에 댓글을 남겼습니다.`, + '칭찬해요!', + `${source}님이 ${target}님을 칭찬해요.`, ); } } diff --git a/src/domain/chat/chat.controller.ts b/src/domain/chat/chat.controller.ts index 1300a2f..cdbd392 100644 --- a/src/domain/chat/chat.controller.ts +++ b/src/domain/chat/chat.controller.ts @@ -2,12 +2,14 @@ import { Controller, Delete, Get, Query, Req, UseGuards } from '@nestjs/common'; import { ChatService } from './chat.service'; import { CustomApiOKResponse } from '../../common/api/response-ok.decorator'; import { ResponseChatDto } from './dto/response-chat.dto'; -import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { JwtServiceAuthGuard } from '../../auth/guards/jwt-service-auth.guard'; +import { CustomApiResponse, ResponseCode } from '../../common'; @ApiTags('채팅 API') @UseGuards(JwtServiceAuthGuard) @Controller('api/chat') +@ApiBearerAuth('access-token') export class ChatController { constructor(private readonly chatService: ChatService) {} @Get('') @@ -16,8 +18,15 @@ export class ChatController { description: '가족의 모든 채팅 기록들을 조회한다.', }) @CustomApiOKResponse(ResponseChatDto, '가족 채팅 조회 성공') - getAllChat(@Req() req, @Query('familyId') familyId: number) { - return this.chatService.findAllChat(req.user.id, familyId); + async getAllChat(@Req() req, @Query('familyId') familyId: number) { + const chatMessages: ResponseChatDto[] = await this.chatService.findAllChat( + req.user.id, + familyId, + ); + return CustomApiResponse.success( + ResponseCode.CHAT_READ_SUCCESS, + chatMessages, + ); } @Delete('') @@ -26,7 +35,8 @@ export class ChatController { description: '가족이 삭제될 때, 가족의 채팅내역 또한 모두 삭제한다.', }) @CustomApiOKResponse(ResponseChatDto, '가족 채팅 삭제 성공') - deleteChat(@Query('familyId') familyId: number) { - return this.chatService.deleteAllChat(familyId); + async deleteChat(@Req() req, @Query('familyId') familyId: number) { + await this.chatService.deleteAllChat(req.user.id, familyId); + return CustomApiResponse.success(ResponseCode.CHAT_DELETE_SUCCESS, null); } } diff --git a/src/domain/chat/chat.service.ts b/src/domain/chat/chat.service.ts index cafa012..d4c9729 100644 --- a/src/domain/chat/chat.service.ts +++ b/src/domain/chat/chat.service.ts @@ -7,6 +7,7 @@ import { ResponseChatDto } from './dto/response-chat.dto'; import { FamilyMember } from '../../infra/entities'; import { ResponseCode } from '../../common'; import { FamilyException } from '../../common/exception/family.exception'; +import {FamilyMemberException} from "../../common/exception/family-member.exception"; @Injectable() export class ChatService { @@ -17,6 +18,8 @@ export class ChatService { private readonly familyMemberRepository: Repository, ) {} async saveChat(createChatDto: CreateChatDto, date: Date) { + await this.validateFamilyMember(parseInt(createChatDto.familyMemberId)); + const parsedFamilyId: number = parseInt(createChatDto.familyId); const parsedFamilyMemberId: number = parseInt(createChatDto.familyMemberId); @@ -34,31 +37,54 @@ export class ChatService { familyId: number, ): Promise { //해당 유저가 이 가족에 속해있는지 확인 - const familyMember = await this.familyMemberRepository.findOne({ - where: { user: { id: userId }, family: { id: familyId } }, - }); - if (!familyMember) { - throw new FamilyException(ResponseCode.FAMILY_FORBIDDEN); - } + await this.validateUser(userId, familyId); const chatMessages: ChatMessage[] = await this.chatRepository.find({ where: { family: { id: familyId } }, - relations: ['familyMember', 'user'], + relations: ['familyMember', 'familyMember.user'], order: { createdDate: 'ASC' }, }); return chatMessages.map((chatMessage) => { const responseChatDto = new ResponseChatDto(); - responseChatDto.familyMemberName = chatMessage.familyMember.user.username; + responseChatDto.nickname = chatMessage.familyMember.user.nickname; responseChatDto.message = chatMessage.content; responseChatDto.role = chatMessage.familyMember.role; + responseChatDto.createdTime = chatMessage.createdDate + .toLocaleTimeString('ko-KR', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }) + .slice(0, 5); return responseChatDto; }); } - async deleteAllChat(familyId: number) { + async deleteAllChat(userId: number, familyId: number) { + await this.validateUser(userId, familyId); const chatMessages = await this.chatRepository.find({ where: { family: { id: familyId } }, }); await this.chatRepository.remove(chatMessages); } + + async validateFamilyMember(familyMemberId: number) { + const family = await this.familyMemberRepository.findOne({ + where: { id: familyMemberId }, + }); + if (!family) { + throw new FamilyMemberException(ResponseCode.FAMILY_MEMBER_NOT_FOUND); + } + } + + async validateUser(userId: number, familyId: number) { + const familyMember = await this.familyMemberRepository.findOne({ + where: { user: { id: userId }, family: { id: familyId } }, + relations: ['user', 'family'], + }); + if (!familyMember) { + throw new FamilyException(ResponseCode.CHAT_FORBIDDEN); + } + } } diff --git a/src/domain/chat/dto/create-chat.dto.ts b/src/domain/chat/dto/create-chat.dto.ts index baca337..ef57a34 100644 --- a/src/domain/chat/dto/create-chat.dto.ts +++ b/src/domain/chat/dto/create-chat.dto.ts @@ -1,16 +1,21 @@ import { ApiProperty } from '@nestjs/swagger'; +import { IsNumberString } from 'class-validator'; export class CreateChatDto { @ApiProperty({ example: '1', description: '가족 ID', }) + @IsNumberString() readonly familyId: string; + @ApiProperty({ example: '1', description: '가족 구성원 ID', }) + @IsNumberString() readonly familyMemberId: string; + @ApiProperty({ example: '안녕하세요', description: '채팅 내용', diff --git a/src/domain/chat/dto/response-chat.dto.ts b/src/domain/chat/dto/response-chat.dto.ts index 822a44f..6a6e697 100644 --- a/src/domain/chat/dto/response-chat.dto.ts +++ b/src/domain/chat/dto/response-chat.dto.ts @@ -2,10 +2,10 @@ import { ApiProperty } from '@nestjs/swagger'; export class ResponseChatDto { @ApiProperty({ - example: '김철수', - description: '가족 구성원 이름', + example: '푸앙이', + description: '가족 구성원 별명', }) - familyMemberName: string; + nickname: string; @ApiProperty({ example: 1, description: '가족 구성원 역할', @@ -16,4 +16,9 @@ export class ResponseChatDto { description: '채팅 내용', }) message: string; + @ApiProperty({ + example: '12:00', + description: '채팅 생성 시간', + }) + createdTime: string; } diff --git a/src/domain/post/post.service.ts b/src/domain/post/post.service.ts index 16ecb50..7a4eb78 100644 --- a/src/domain/post/post.service.ts +++ b/src/domain/post/post.service.ts @@ -52,7 +52,10 @@ export class PostService { await this.validateFamily(familyId); const postList = await this.postRepository.find({ where: { family: { id: familyId } }, - relations: ['family', 'familyMember'], + relations: ['family', 'srcMember'], //데이터베이스 칼럼 명이 아닌, 연관관계 필드명 + }); + postList.forEach((post) => { + post.createdDate = new Date(post.createdDate); }); return postList.map((post) => ResponsePostDto.from(post)); } diff --git a/src/test/e2e/chat.e2e.spec.ts b/src/test/e2e/chat.e2e.spec.ts new file mode 100644 index 0000000..8bcb983 --- /dev/null +++ b/src/test/e2e/chat.e2e.spec.ts @@ -0,0 +1,112 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import { Family, FamilyMember, User } from '../../infra/entities'; +import { Repository } from 'typeorm'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import * as request from 'supertest'; +import { PassportModule } from '@nestjs/passport'; +import { JwtServiceAuthGuard } from '../../auth/guards/jwt-service-auth.guard'; +import { MockJwtAuthGuard } from './mockAuthGuard'; +import { ChatService } from '../../domain/chat/chat.service'; +import { ChatMessage } from '../../infra/entities/message.entity'; +import { ChatModule } from '../../module/chat.module'; + +describe('ChatController (e2e)', () => { + let app: INestApplication; + let mockChatService: Partial; + let mockChatRepository: Partial>; + let mockFamilyMemberRepository: Partial>; + const user: User = User.createUser( + 'test', + 'test', + 'test', + 'testNickname', + 1, + 1, + ); + const family: Family = Family.createFamily('test', 'test'); + const familyMember: FamilyMember = FamilyMember.createFamilyMember( + 1, + family, + user, + null, + ); + const chat: ChatMessage = ChatMessage.createMessage( + 'testContent', + new Date(11, 11, 11, 11, 11, 11), + familyMember, + Family.createFamily('test', 'test'), + ); + + beforeEach(async () => { + mockChatService = { + saveChat: jest.fn().mockResolvedValue(1), + findAllChat: jest.fn().mockResolvedValue([ + { + createdTime: chat.createdDate + .toLocaleTimeString('ko-KR', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }) + .slice(0, 5), + nickname: chat.familyMember.user.nickname, + role: 1, + message: chat.content, + }, + ]), + deleteAllChat: jest.fn(), + }; + mockChatRepository = { + findOne: jest.fn().mockResolvedValue(chat), + find: jest.fn().mockResolvedValue([chat]), + }; + mockFamilyMemberRepository = { + findOne: jest.fn().mockResolvedValue(familyMember), + find: jest.fn().mockResolvedValue([familyMember]), + }; + + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [ + ChatModule, + PassportModule.register({ defaultStrategy: 'jwt-service' }), + ], + }) + .overrideProvider(ChatService) + .useValue(mockChatService) + .overrideProvider(getRepositoryToken(ChatMessage)) + .useValue(mockChatRepository) + .overrideProvider(getRepositoryToken(FamilyMember)) + .useValue(mockFamilyMemberRepository) + .overrideGuard(JwtServiceAuthGuard) + .useClass(MockJwtAuthGuard) + .compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('should be defined', () => { + expect(app).toBeDefined(); + }); + + it('should delete all chat', async () => { + const response = await request(app.getHttpServer()) + .delete('/api/chat') + .query('familyId=1'); + expect(response.body.message).toBe('채팅 삭제 성공'); + }); + + it('should find chat list by family id', async () => { + const response = await request(app.getHttpServer()) + .get('/api/chat') + .query('familyId=1'); + + expect(response.body.message).toBe('채팅 조회 성공'); + expect(response.body.data[0].nickname).toBe('testNickname'); + expect(response.body.data[0].role).toBe(1); + expect(response.body.data[0].message).toBe('testContent'); + expect(response.body.data[0].createdTime).toBe('11:11'); + }); +}); diff --git a/src/test/service/chat.service.spec.ts b/src/test/service/chat.service.spec.ts index 62bb24c..cbfa3c1 100644 --- a/src/test/service/chat.service.spec.ts +++ b/src/test/service/chat.service.spec.ts @@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ChatService } from '../../domain/chat/chat.service'; import { getRepositoryToken } from '@nestjs/typeorm'; import { ChatMessage } from '../../infra/entities/message.entity'; -import { FamilyMember } from '../../infra/entities'; +import { Family, FamilyMember, User } from '../../infra/entities'; describe('ChatService', () => { const mockRepository = () => ({ @@ -10,10 +10,12 @@ describe('ChatService', () => { save: jest.fn(), find: jest.fn(), delete: jest.fn(), + findOne: jest.fn(), }); let service: ChatService; let chatRepository; + let familyMemberRepository; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -32,9 +34,66 @@ describe('ChatService', () => { service = module.get(ChatService); chatRepository = module.get(getRepositoryToken(ChatMessage)); + familyMemberRepository = module.get(getRepositoryToken(FamilyMember)); }); it('should be defined', () => { expect(service).toBeDefined(); }); + + it('should save chat', async () => { + const createChatDto = { + familyId: '1', + familyMemberId: '1', + message: 'hello', + }; + const family = Family.createFamily('test', 'test'); + const familyMember = FamilyMember.createFamilyMember(1, family, null, null); + + const date = new Date(); + jest + .spyOn(chatRepository, 'create') + .mockResolvedValue( + ChatMessage.createMessage( + createChatDto.message, + date, + familyMember, + family, + ), + ); + jest.spyOn(familyMemberRepository, 'findOne').mockResolvedValue(familyMember); + jest.spyOn(chatRepository, 'save').mockResolvedValue(createChatDto); + await service.saveChat(createChatDto, date); + expect(chatRepository.save).toHaveBeenCalled(); + }); + + it('should find all chat', async () => { + const userId = 1; + const user = User.createUser('test', 'test', 'test', 'testNickname', 1, 1); + const family = Family.createFamily('test', 'test'); + const familyMember = FamilyMember.createFamilyMember(1, family, user, null); + const chatMessage = ChatMessage.createMessage( + 'hello', + new Date(11, 11, 11, 11, 11, 11), + familyMember, + family, + ); + family.id = 1; + familyMember.id = 2; + user.id = 3; + + jest.spyOn(chatRepository, 'find').mockResolvedValue([chatMessage]); + jest + .spyOn(familyMemberRepository, 'findOne') + .mockResolvedValue(familyMember); + const chat = await service.findAllChat(userId, family.id); + expect(chat).toEqual([ + { + createdTime: '11:11', + nickname: 'testNickname', + role: 1, + message: 'hello', + }, + ]); + }); });