Skip to content

Commit

Permalink
[NDD-116, NDD-117]: 질문 단건에 대한 답변 리스트 반환 로직 구현 & 답변 삭제 로직 구현 (1+1h / 1…
Browse files Browse the repository at this point in the history
…+1h) (#94)

* test: 비즈니스 로직 테스트 추가

* feat: 테스트를 통과하는 비즈니스 로직 구현

* if-isEmpty-throw의 반복되는 구조 분리

* refactor: isEmpty throw의 케이스 로직 분리

* feat: 컨트롤러 로직 구현 && dto 생성 및 문서화 데코레이터 추가

* feat: 컨트롤러 단위 테스트 구현 및 이를 통과하는 로직 구현

* test: Forbidden => CategoryForbidden으로 수정

* test: 삭제에 대한 비즈니스 로직 테스트코드 추가

* feat: 삭제시 member검증을 위한 eager추가 && 리포지토리 로직 추가

* feat: 삭제를 위한 비즈니스 로직 구현

* feat: 댓글 존재 여부에 대한 검증 기능 추가 && 해당 케이스에 대한 테스트코드 추가

* test: 삭제 로직에 대한 컨트롤러 테스트 추가

* feat: 컨트롤러 테스트를 통과하는 컨트로럴 로직 구현

* test: 삭제시 답변이 없는 경우에 대한 테스트코드 추가
  • Loading branch information
JangAJang authored Nov 23, 2023
1 parent c30e748 commit 42794e0
Show file tree
Hide file tree
Showing 17 changed files with 526 additions and 54 deletions.
179 changes: 179 additions & 0 deletions BE/src/answer/controller/answer.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,4 +362,183 @@ describe('AnswerController 통합테스트', () => {
.expect(404);
});
});

describe('질문의 답변 조회', () => {
it('질문을 조회할 때 회원 정보가 없으면 모든 답변이 최신순으로 정렬된다. ', async () => {
//given
const member = await memberRepository.save(memberFixture);
const category = await categoryRepository.save(
Category.from(beCategoryFixture, member),
);

const question = await questionRepository.save(
Question.of(category, null, 'question'),
);
const answer = await answerRepository.save(
Answer.of('test', member, question),
);
for (let index = 1; index <= 10; index++) {
await answerRepository.save(
Answer.of(`test${index}`, member, question),
);
}

//when&then
const token = await authService.login(memberFixturesOAuthRequest);
const agent = request.agent(app.getHttpServer());
await agent
.get(`/api/answer/${question.id}`)
.set('Cookie', [`accessToken=${token}`])
.expect(200);
});

it('질문을 조회할 때 회원 정보가 있으면 모든 등록한 DefaultAnswer부터 정렬된다. ', async () => {
//given
const member = await memberRepository.save(memberFixture);
const category = await categoryRepository.save(
Category.from(beCategoryFixture, member),
);

const question = await questionRepository.save(
Question.of(category, null, 'question'),
);
const answer = await answerRepository.save(
Answer.of('test', member, question),
);
question.setDefaultAnswer(answer);
await questionRepository.save(question);
for (let index = 1; index <= 10; index++) {
await answerRepository.save(
Answer.of(`test${index}`, member, question),
);
}

//when&then
const token = await authService.login(memberFixturesOAuthRequest);
const agent = request.agent(app.getHttpServer());
await agent
.get(`/api/answer/${question.id}`)
.set('Cookie', [`accessToken=${token}`])
.expect(200)
.then((response) => console.log(response.body));
});

it('존재하지 않는 질문의 id를 조회하면 404에러를 반환한다. ', async () => {
//given
const member = await memberRepository.save(memberFixture);
const category = await categoryRepository.save(
Category.from(beCategoryFixture, member),
);

const question = await questionRepository.save(
Question.of(category, null, 'question'),
);
const answer = await answerRepository.save(
Answer.of('test', member, question),
);
for (let index = 1; index <= 10; index++) {
await answerRepository.save(
Answer.of(`test${index}`, member, question),
);
}

//when&then
const token = await authService.login(memberFixturesOAuthRequest);
const agent = request.agent(app.getHttpServer());
await agent
.get(`/api/answer/130998`)
.set('Cookie', [`accessToken=${token}`])
.expect(404);
});
});

describe('답변 삭제', () => {
it('답변을 삭제할 때 자신의 댓글을 삭제하려고 하면 204코드와 함께 성공한다. ', async () => {
//given
const member = await memberRepository.save(memberFixture);
const category = await categoryRepository.save(
Category.from(beCategoryFixture, member),
);

const question = await questionRepository.save(
Question.of(category, null, 'question'),
);
const answer = await answerRepository.save(
Answer.of('test', member, question),
);

//when&then
const token = await authService.login(memberFixturesOAuthRequest);
const agent = request.agent(app.getHttpServer());
await agent
.delete(`/api/answer/${answer.id}`)
.set('Cookie', [`accessToken=${token}`])
.expect(204);
});

it('쿠키가 없으면 401에러를 반환한다.', async () => {
//given
const member = await memberRepository.save(memberFixture);
const category = await categoryRepository.save(
Category.from(beCategoryFixture, member),
);

const question = await questionRepository.save(
Question.of(category, null, 'question'),
);
const answer = await answerRepository.save(
Answer.of('test', member, question),
);

//when&then
const agent = request.agent(app.getHttpServer());
await agent.delete(`/api/answer/${answer.id}`).expect(401);
});

it('다른 사람의 답변을 삭제하면 403에러를 반환한다.', async () => {
//given
const member = await memberRepository.save(memberFixture);
const category = await categoryRepository.save(
Category.from(beCategoryFixture, member),
);

const question = await questionRepository.save(
Question.of(category, null, 'question'),
);
const answer = await answerRepository.save(
Answer.of('test', member, question),
);

//when&then
const token = await authService.login(oauthRequestFixture);
const agent = request.agent(app.getHttpServer());
await agent
.delete(`/api/answer/${answer.id}`)
.set('Cookie', [`accessToken=${token}`])
.expect(403);
});

it('답변이 없으면 404에러를 반환한다.', async () => {
//given
const member = await memberRepository.save(memberFixture);
const category = await categoryRepository.save(
Category.from(beCategoryFixture, member),
);

const question = await questionRepository.save(
Question.of(category, null, 'question'),
);
const answer = await answerRepository.save(
Answer.of('test', member, question),
);

//when&then
const token = await authService.login(oauthRequestFixture);
const agent = request.agent(app.getHttpServer());
await agent
.delete(`/api/answer/${100000}`)
.set('Cookie', [`accessToken=${token}`])
.expect(404);
});
});
});
47 changes: 45 additions & 2 deletions BE/src/answer/controller/answer.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { Body, Controller, Post, Req, UseGuards } from '@nestjs/common';
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Req,
Res,
UseGuards,
} from '@nestjs/common';
import {
ApiBody,
ApiCookieAuth,
Expand All @@ -8,12 +18,13 @@ import {
} from '@nestjs/swagger';
import { AnswerService } from '../service/answer.service';
import { CreateAnswerRequest } from '../dto/createAnswerRequest';
import { Request } from 'express';
import { Request, Response } from 'express';
import { AuthGuard } from '@nestjs/passport';
import { createApiResponseOption } from '../../util/swagger.util';
import { Member } from '../../member/entity/member';
import { AnswerResponse } from '../dto/answerResponse';
import { DefaultAnswerRequest } from '../dto/defaultAnswerRequest';
import { AnswerListResponse } from '../dto/answerListResponse';

@ApiTags('answer')
@Controller('/api/answer')
Expand Down Expand Up @@ -55,4 +66,36 @@ export class AnswerController {
req.user as Member,
);
}

@Get(':questionId')
@ApiOperation({
summary: '질문의 답변 리스트 반환',
})
@ApiResponse(
createApiResponseOption(
200,
'답변 리스트 캡슐화해 반환',
AnswerListResponse,
),
)
async getQuestionAnswers(@Param('questionId') questionId: number) {
const answerList = await this.answerService.getAnswerList(questionId);
return AnswerListResponse.of(answerList);
}

@Delete(':answerId')
@UseGuards(AuthGuard('jwt'))
@ApiCookieAuth()
@ApiOperation({
summary: '답변 삭제',
})
@ApiResponse(createApiResponseOption(204, '답변 삭제 완료', null))
async deleteAnswer(
@Param('answerId') answerId: number,
@Req() req: Request,
@Res() res: Response,
) {
await this.answerService.deleteAnswer(answerId, req.user as Member);
res.status(204).send();
}
}
30 changes: 30 additions & 0 deletions BE/src/answer/dto/answerListResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ApiProperty } from '@nestjs/swagger';
import { createPropertyOption } from '../../util/swagger.util';
import { AnswerResponse } from './answerResponse';

export class AnswerListResponse {
@ApiProperty(
createPropertyOption(
[
new AnswerResponse(
1,
'answerContent',
1,
'이장희',
'https://jangsarchive.tistory.com',
),
],
'답변 ID',
[AnswerResponse],
),
)
answerResponseList: AnswerResponse[];

constructor(answerResponseList: AnswerResponse[]) {
this.answerResponseList = answerResponseList;
}

static of(answerResponseList: AnswerResponse[]) {
return new AnswerListResponse(answerResponseList);
}
}
2 changes: 1 addition & 1 deletion BE/src/answer/entity/answer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export class Answer extends DefaultEntity {
@Column()
content: string;

@ManyToOne(() => Member, { onDelete: 'CASCADE' })
@ManyToOne(() => Member, { onDelete: 'CASCADE', eager: true })
@JoinColumn()
member: Member;

Expand Down
8 changes: 7 additions & 1 deletion BE/src/answer/exception/answer.exception.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,10 @@ class AnswerNotFoundException extends HttpException {
}
}

export { AnswerNotFoundException };
class AnswerForbiddenException extends HttpException {
constructor() {
super('답변에 대한 권한이 없습니다', 403);
}
}

export { AnswerNotFoundException, AnswerForbiddenException };
2 changes: 1 addition & 1 deletion BE/src/answer/fixture/answer.fixture.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Answer } from '../entity/answer';
import { memberFixture } from '../../member/fixture/member.fixture';
import { questionFixture } from '../../question/util/question.util';
import { questionFixture } from '../../question/fixture/question.fixture';
import { CreateAnswerRequest } from '../dto/createAnswerRequest';
import { DefaultAnswerRequest } from '../dto/defaultAnswerRequest';

Expand Down
14 changes: 14 additions & 0 deletions BE/src/answer/repository/answer.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,18 @@ export class AnswerRepository {
question: { id: questionId },
});
}

async findAllByQuestionId(questionId: number) {
return this.repository
.createQueryBuilder('answer')
.leftJoinAndSelect('answer.member', 'member')
.leftJoinAndSelect('answer.question', 'question')
.where('question.id = :questionId', { questionId })
.orderBy('answer.createdAt', 'DESC')
.getMany();
}

async remove(answer: Answer) {
await this.repository.remove(answer);
}
}
Loading

0 comments on commit 42794e0

Please sign in to comment.