Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NDD-228]: 카테고리별 질문 조회 기능 구현 (1h / 1h) #73

Merged
merged 5 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions BE/src/app.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,8 @@ export class DefaultEntity extends BaseEntity {
static new(): DefaultEntity {
return new DefaultEntity(undefined, new Date());
}

getId() {
return this.id;
}
}
2 changes: 1 addition & 1 deletion BE/src/category/entity/category.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export class Category extends DefaultEntity {
name: string;

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

constructor(id: number, name: string, member: Member, createdAt: Date) {
Expand Down
9 changes: 8 additions & 1 deletion BE/src/category/repository/category.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,17 @@ export class CategoryRepository {
await this.repository.remove(category);
}

async findByCategoryId(categoryId) {
async findByCategoryId(categoryId: number) {
return await this.repository.findOneBy({ id: categoryId });
}

async findByNameAndMember(name: string, memberId: number) {
return await this.repository.findOneBy({
name: name,
member: { id: memberId },
});
}

async query(query: string) {
return await this.repository.query(query);
}
Expand Down
15 changes: 15 additions & 0 deletions BE/src/question/controller/question.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ import { Question } from '../entity/question';
import { Category } from '../../category/entity/category';
import { CreateQuestionRequest } from '../dto/createQuestionRequest';
import * as cookieParser from 'cookie-parser';
import { QuestionResponseList } from '../dto/questionResponseList';

describe('QuestionController', () => {
let controller: QuestionController;
const mockQuestionService = {
createQuestion: jest.fn(),
findAllByCategory: jest.fn(),
};
const mockTokenService = {};

Expand Down Expand Up @@ -68,6 +70,19 @@ describe('QuestionController', () => {
),
).resolves.toEqual(QuestionResponse.from(questionFixture));
});

it('조회시 QuestionResponseList객체를 반환한다.', async () => {
//given

//when
mockQuestionService.findAllByCategory.mockResolvedValue([
QuestionResponse.from(questionFixture),
]);
//then
await expect(controller.findCategoryQuestions(1)).resolves.toEqual(
QuestionResponseList.of([QuestionResponse.from(questionFixture)]),
);
});
});

describe('QuestionController 통합테스트', () => {
Expand Down
28 changes: 27 additions & 1 deletion BE/src/question/controller/question.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { Body, Controller, Post, Req, UseGuards } from '@nestjs/common';
import {
Body,
Controller,
Get,
Post,
Query,
Req,
UseGuards,
} from '@nestjs/common';
import { QuestionService } from '../service/question.service';
import { CreateQuestionRequest } from '../dto/createQuestionRequest';
import { Request } from 'express';
Expand All @@ -13,6 +21,7 @@ import {
import { createApiResponseOption } from '../../util/swagger.util';
import { QuestionResponse } from '../dto/questionResponse';
import { Member } from '../../member/entity/member';
import { QuestionResponseList } from '../dto/questionResponseList';

@ApiTags('question')
@Controller('/api/question')
Expand All @@ -38,4 +47,21 @@ export class QuestionController {
req.user as Member,
);
}

@Get()
@ApiOperation({
summary: '카테고리별 질문 리스트 조회',
})
@ApiResponse(
createApiResponseOption(
200,
'QuestionResponse 리스트',
QuestionResponseList,
),
)
async findCategoryQuestions(@Query('category') categoryId: number) {
const questionResponses =
await this.questionService.findAllByCategory(categoryId);
return QuestionResponseList.of(questionResponses);
}
}
13 changes: 13 additions & 0 deletions BE/src/question/dto/questionResponseList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { QuestionResponse } from './questionResponse';

export class QuestionResponseList {
questionResponses: QuestionResponse[];

constructor(questionResponses: QuestionResponse[]) {
this.questionResponses = questionResponses;
}

static of(questionResponses: QuestionResponse[]) {
return new QuestionResponseList(questionResponses);
}
}
8 changes: 7 additions & 1 deletion BE/src/question/exception/question.exception.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,10 @@ class ContentNotFoundException extends HttpException {
}
}

export { ContentNotFoundException };
class NeedToFindByCategoryIdException extends HttpException {
constructor() {
super('카테고리 id를 입력해주세요.', 400);
}
}

export { ContentNotFoundException, NeedToFindByCategoryIdException };
50 changes: 50 additions & 0 deletions BE/src/question/service/question.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ import { Member } from '../../member/entity/member';
import { MemberModule } from '../../member/member.module';
import { MemberRepository } from '../../member/repository/member.repository';
import { memberFixture } from '../../member/fixture/member.fixture';
import { NeedToFindByCategoryIdException } from '../exception/question.exception';

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

const mockQuestionRepository = {
save: jest.fn(),
findByCategoryId: jest.fn(),
};

const mockCategoryRepository = {
Expand Down Expand Up @@ -74,6 +76,32 @@ describe('QuestionService', () => {
service.createQuestion(createQuestionRequestFixture, memberFixture),
).rejects.toThrow(new CategoryNotFoundException());
});

// Todo: Answer API 구현시에 DefaultAnswer 까지 등록하기
it('카테고리 id로 질문들을 조회하면, 해당 카테고리 내부 질문들이 반환된다.', async () => {
//given

//when
mockQuestionRepository.findByCategoryId.mockResolvedValue([
questionFixture,
]);

//then
await expect(service.findAllByCategory(1)).resolves.toEqual([
QuestionResponse.from(questionFixture),
]);
});

it('카테고리 id가 isEmpty이면 NeedToFindByCategoryIdException을 발생시킨다..', async () => {
//given

//when

//then
await expect(service.findAllByCategory(null)).rejects.toThrow(
new NeedToFindByCategoryIdException(),
);
});
});

describe('QuestionService 통합 테스트', () => {
Expand Down Expand Up @@ -120,4 +148,26 @@ describe('QuestionService 통합 테스트', () => {
),
).resolves.toEqual(QuestionResponse.from(questionFixture));
});

it('카테고리의 질문을 조회하면 QuestionResponse의 배열로 반환된다.', async () => {
//given
const member = await memberRepository.save(memberFixture);
await categoryRepository.save(categoryFixtureWithId);
const response = await questionService.createQuestion(
createQuestionRequestFixture,
memberFixture,
);

//when

const category = await categoryRepository.findByNameAndMember(
categoryFixtureWithId.name,
member.id,
);

//then
await expect(
questionService.findAllByCategory(category.id),
).resolves.toEqual([response]);
});
});
15 changes: 13 additions & 2 deletions BE/src/question/service/question.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Question } from '../entity/question';
import { Category } from '../../category/entity/category';
import { QuestionResponse } from '../dto/questionResponse';
import { Member } from '../../member/entity/member';
import { NeedToFindByCategoryIdException } from '../exception/question.exception';

@Injectable()
export class QuestionService {
Expand All @@ -24,7 +25,7 @@ export class QuestionService {
createQuestionRequest.categoryId,
);

this.validateCreateRequest(category, createQuestionRequest.content);
this.validateCreateRequest(category);

if (!category.isOwnedBy(member)) {
throw new UnauthorizedException();
Expand All @@ -37,7 +38,17 @@ export class QuestionService {
return QuestionResponse.from(question);
}

private validateCreateRequest(category: Category, content: string) {
async findAllByCategory(categoryId: number) {
if (isEmpty(categoryId)) {
throw new NeedToFindByCategoryIdException();
}

const questions =
await this.questionRepository.findByCategoryId(categoryId);
return questions.map(QuestionResponse.from);
}

private validateCreateRequest(category: Category) {
if (isEmpty(category)) {
throw new CategoryNotFoundException();
}
Expand Down
1 change: 0 additions & 1 deletion BE/src/token/strategy/access.token.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ export class AccessTokenStrategy extends PassportStrategy(Strategy, 'jwt') {
}

async validate(payload: TokenPayload) {
console.log(`토큰 파싱 결과 : ${payload}`);
const id = payload.id;
const user = await this.memberRepository.findById(id);
if (!user) throw new InvalidTokenException(); // 회원이 조회가 되지 않았다면, 탈퇴한 회원의 token을 사용한 것이므로 유효하지 않은 토큰을 사용한 것임
Expand Down
Loading