Skip to content

Commit

Permalink
Merge pull request #73 from Fam-Story/feat/72-interaction-chat
Browse files Browse the repository at this point in the history
Feat: 상호작용 내용 구체화 및 채팅 로직 버그 수정
  • Loading branch information
synoti21 authored Dec 5, 2023
2 parents 3d51ed7 + e513d8f commit fafa27f
Show file tree
Hide file tree
Showing 9 changed files with 264 additions and 27 deletions.
17 changes: 17 additions & 0 deletions src/common/api/response-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 = {
Expand Down
16 changes: 8 additions & 8 deletions src/common/util/interactionType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}님을 칭찬해요.`,
);
}
}
Expand Down
20 changes: 15 additions & 5 deletions src/domain/chat/chat.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('')
Expand All @@ -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('')
Expand All @@ -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);
}
}
44 changes: 35 additions & 9 deletions src/domain/chat/chat.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -17,6 +18,8 @@ export class ChatService {
private readonly familyMemberRepository: Repository<FamilyMember>,
) {}
async saveChat(createChatDto: CreateChatDto, date: Date) {
await this.validateFamilyMember(parseInt(createChatDto.familyMemberId));

const parsedFamilyId: number = parseInt(createChatDto.familyId);
const parsedFamilyMemberId: number = parseInt(createChatDto.familyMemberId);

Expand All @@ -34,31 +37,54 @@ export class ChatService {
familyId: number,
): Promise<ResponseChatDto[]> {
//해당 유저가 이 가족에 속해있는지 확인
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);
}
}
}
5 changes: 5 additions & 0 deletions src/domain/chat/dto/create-chat.dto.ts
Original file line number Diff line number Diff line change
@@ -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: '채팅 내용',
Expand Down
11 changes: 8 additions & 3 deletions src/domain/chat/dto/response-chat.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '가족 구성원 역할',
Expand All @@ -16,4 +16,9 @@ export class ResponseChatDto {
description: '채팅 내용',
})
message: string;
@ApiProperty({
example: '12:00',
description: '채팅 생성 시간',
})
createdTime: string;
}
5 changes: 4 additions & 1 deletion src/domain/post/post.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
112 changes: 112 additions & 0 deletions src/test/e2e/chat.e2e.spec.ts
Original file line number Diff line number Diff line change
@@ -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<ChatService>;
let mockChatRepository: Partial<Repository<ChatMessage>>;
let mockFamilyMemberRepository: Partial<Repository<FamilyMember>>;
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');
});
});
Loading

0 comments on commit fafa27f

Please sign in to comment.