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

[feat #16] : 질문 게시글 등록, 상세조회 #17

Merged
merged 37 commits into from
Aug 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
e8135d1
[chore] : redis 의존성 추가
hyun2371 Aug 1, 2024
3f8d660
[chore] : redis 설정 파일 추가
hyun2371 Aug 1, 2024
278c16b
[feat] : 요구사항 변경에 따른 엔티티 수정
hyun2371 Aug 1, 2024
15c0d2f
[feat] : QuestionPost Repository 추가
hyun2371 Aug 1, 2024
2bb2463
[feat] : memberRepository 추가
hyun2371 Aug 2, 2024
7fad0e2
[feat] : 정적 팩토리 메서드 추가 및 양방향 편의 메서드 추가
hyun2371 Aug 2, 2024
8050daf
[feat] : 질문글 등록 관련 DTO 추가
hyun2371 Aug 2, 2024
d1895ff
[feat] : DTO <-> 엔티티 변환QuestionPost Mapper 추가
hyun2371 Aug 2, 2024
f39a1d6
[feat] : questionPost 등록 비즈니스 로직 추가
hyun2371 Aug 2, 2024
a1ab1ef
[feat] : questionPost 등록 컨트롤러 코드 추가
hyun2371 Aug 2, 2024
e66fa5f
[style] : dto 변수명 수정
hyun2371 Aug 2, 2024
aaef6a9
[fix] : 누락된 저장 코드 추가
hyun2371 Aug 2, 2024
7c048e0
[test] : test용 fixture 추가
hyun2371 Aug 2, 2024
8a4bf9b
[test] : 질문글 등록 단위 테스트 추가
hyun2371 Aug 2, 2024
53bd098
[test] : 제약 조건에 따른 memberFixture 수정
hyun2371 Aug 3, 2024
a65380e
[test] : 질문글 등록 통합 테스트 작성
hyun2371 Aug 3, 2024
7a8617b
[feat] : 아이디로 질문글 조회 비즈니스 로직 작성
hyun2371 Aug 3, 2024
043dd4d
[feat] : 아이디로 질문 조회 controller 코드 작성
hyun2371 Aug 3, 2024
bca93f1
[feat] : 도메인 내 테스트용 생성자 추가
hyun2371 Aug 3, 2024
a76a831
[test] : 테스트 픽스처에 아이디 필드 추가
hyun2371 Aug 3, 2024
1a171eb
[test] : 질문글 아이디로 조회 단위 테스트 작성
hyun2371 Aug 3, 2024
454898e
[test] : 질문글 아이디로 조회 통합 테스트 수행
hyun2371 Aug 3, 2024
1c74c64
[style] : 코드 리포멧팅
hyun2371 Aug 3, 2024
d16c138
Merge branch 'dev' into feat/#16/question-register-get-api
hyun2371 Aug 3, 2024
c3e32fe
[fix] : 병합 시 충돌 사항 해결
hyun2371 Aug 3, 2024
8ea4dee
[fix] : dto->entity 올바른 값 할당
hyun2371 Aug 4, 2024
bd6d931
[feat] : request dto validation 적용
hyun2371 Aug 4, 2024
93bf56f
[refactor] : DTO 내 정적 팩토리 메서드 삭제
hyun2371 Aug 4, 2024
671162c
[feat] : 엔티티 내 테스트 생성자 삭제 -> ReflectionTestUtils로 대체
hyun2371 Aug 4, 2024
5da1fcb
[fix] : CI 테스트 실패 해결
hyun2371 Aug 4, 2024
74c7c60
[fix] : dev 브랜치 간 충돌 해결
hyun2371 Aug 4, 2024
105e201
[test] : ApiTestSupport 내 memberRepository 접근 제어자 수정
hyun2371 Aug 4, 2024
a91a3e3
[feat] : 인증 멤버 객체 주입하도록 변경
hyun2371 Aug 4, 2024
e285056
[test] : 인증 멤버 객체 주입에 따른 테스트 코드 수정
hyun2371 Aug 4, 2024
5bd1013
[style] : 코드 리포멧팅
hyun2371 Aug 4, 2024
bec784f
[chore] : application.yml 인코딩 없이 추가
hyun2371 Aug 4, 2024
77ecdd2
[chore] : 컨테이너 버전 수정
hyun2371 Aug 4, 2024
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
- name: Generate application.yml
run: |
mkdir -p ./src/test/resources
echo "${{ secrets.TEST_APPLICATION_YML }}" | base64 -d > ./src/test/resources/application.yml
echo "${{ secrets.TEST_APPLICATION_YML }}" > ./src/test/resources/application.yml

# gradle 권한 부여
- name: Grant execute permission for gradlew
Expand Down
8 changes: 5 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@ dependencies {
testImplementation "org.junit.jupiter:junit-jupiter:5.8.1"

// TestContainer
testImplementation "org.testcontainers:testcontainers:1.19.5"
testImplementation "org.testcontainers:junit-jupiter:1.19.5"
testImplementation "org.testcontainers:testcontainers:1.20.1"
testImplementation "org.testcontainers:junit-jupiter:1.20.1"
// mysql 컨테이너
testImplementation "org.testcontainers:mysql:1.19.2"
testImplementation "org.testcontainers:mysql:1.20.1"
// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
Expand Down
34 changes: 34 additions & 0 deletions src/main/java/com/dnd/gongmuin/common/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.dnd.gongmuin.common.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

@Value("${spring.data.redis.host}")
private String host;

@Value("${spring.data.redis.port}")
private int port;

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}

@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());

return redisTemplate;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.dnd.gongmuin.question_post.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.dnd.gongmuin.member.domain.Member;
import com.dnd.gongmuin.question_post.dto.QuestionPostDetailResponse;
import com.dnd.gongmuin.question_post.dto.RegisterQuestionPostRequest;
import com.dnd.gongmuin.question_post.service.QuestionPostService;

import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/question-posts")
public class QuestionPostController {

private final QuestionPostService questionPostService;

@PostMapping
public ResponseEntity<QuestionPostDetailResponse> registerQuestionPost(
@RequestBody RegisterQuestionPostRequest request,
@AuthenticationPrincipal Member member
) {
QuestionPostDetailResponse response = questionPostService.registerQuestionPost(request, member);
return ResponseEntity.ok(response);
}

@GetMapping("/{questionPostId}")
public ResponseEntity<QuestionPostDetailResponse> getQuestionPostById(
@PathVariable("questionPostId") Long questionPostId
) {
QuestionPostDetailResponse response = questionPostService.getQuestionPostById(questionPostId);
return ResponseEntity.ok(response);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.List;

import com.dnd.gongmuin.common.entity.TimeBaseEntity;
import com.dnd.gongmuin.member.domain.JobGroup;
import com.dnd.gongmuin.member.domain.Member;

import jakarta.persistence.CascadeType;
Expand All @@ -22,7 +23,6 @@
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

Expand All @@ -35,41 +35,47 @@ public class QuestionPost extends TimeBaseEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "question_post_id", nullable = false)
private Long id;

@Column(name = "title", nullable = false)
private String title;

@Column(name = "content", nullable = false)
private String content;

@Column(name = "reward", nullable = false)
private int reward;

@Enumerated(EnumType.STRING)
@Column(name = "category", nullable = false)
QuestionCategory category;

@Column(name = "job_group", nullable = false)
private JobGroup jobGroup;
@Column(name = "is_chosen", nullable = false)
private Boolean isChosen;

@OneToMany(mappedBy = "questionPost", cascade = CascadeType.ALL)
private List<QuestionPostImage> images = new ArrayList<>();

private final List<QuestionPostImage> images = new ArrayList<>();
hyun2371 marked this conversation as resolved.
Show resolved Hide resolved
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "member_id",
nullable = false,
foreignKey = @ForeignKey(NO_CONSTRAINT))
private Member member;

@Builder
public QuestionPost(String title, String content, int reward, QuestionCategory category,
private QuestionPost(String title, String content, int reward, JobGroup jobGroup,
List<QuestionPostImage> images, Member member) {
this.isChosen = false;
this.title = title;
this.content = content;
this.reward = reward;
this.category = category;
this.images = images;
this.jobGroup = jobGroup;
this.member = member;
addImages(images);
}

public static QuestionPost of(String title, String content, int reward, JobGroup jobGroup,
List<QuestionPostImage> images, Member member) {
return new QuestionPost(title, content, reward, jobGroup, images, member);
}

//==양방향 편의 메서드==//
private void addImages(List<QuestionPostImage> images) {
images.forEach(image -> {
this.images.add(image);
image.addQuestionPost(this);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

Expand All @@ -37,9 +36,16 @@ public class QuestionPostImage extends TimeBaseEntity {
foreignKey = @ForeignKey(NO_CONSTRAINT))
private QuestionPost questionPost;

@Builder
public QuestionPostImage(String imageUrl, QuestionPost questionPost) {
private QuestionPostImage(String imageUrl, QuestionPost questionPost) {
this.imageUrl = imageUrl;
this.questionPost = questionPost;
}

public static QuestionPostImage from(String imageUrl) {
return new QuestionPostImage(imageUrl, null);
}

public void addQuestionPost(QuestionPost questionPost) {
this.questionPost = questionPost;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.dnd.gongmuin.question_post.dto;

public record MemberInfo(
Long memberId,
String nickname,
String memberJobGroup
// TODO: 추후 프로필 이미지 타입 추가
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.dnd.gongmuin.question_post.dto;

import java.util.List;

public record QuestionPostDetailResponse(
Long questionPostId,
String title,
String content,
List<String> imageUrls,
int reward,
String targetJobGroup,
MemberInfo memberInfo,
String createdAt
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.dnd.gongmuin.question_post.dto;

import java.util.List;

import com.dnd.gongmuin.member.domain.JobGroup;
import com.dnd.gongmuin.member.domain.Member;
import com.dnd.gongmuin.question_post.domain.QuestionPost;
import com.dnd.gongmuin.question_post.domain.QuestionPostImage;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class QuestionPostMapper {

public static QuestionPost toQuestionPost(RegisterQuestionPostRequest request, Member member) {
JobGroup jobGroup = JobGroup.of(request.targetJobGroup());
List<QuestionPostImage> images = request.imageUrls().stream()
.map(QuestionPostImage::from)
.toList();
return QuestionPost.of(request.title(), request.content(), request.reward(), jobGroup, images, member);
}

public static QuestionPostDetailResponse toQuestionPostDetailResponse(QuestionPost questionPost) {
Member member = questionPost.getMember();
return new QuestionPostDetailResponse(
questionPost.getId(),
questionPost.getTitle(),
questionPost.getContent(),
questionPost.getImages().stream()
.map(QuestionPostImage::getImageUrl).toList(),
questionPost.getReward(),
questionPost.getJobGroup().getLabel(),
new MemberInfo(
member.getId(),
member.getNickname(),
member.getJobGroup().getLabel()
),
questionPost.getCreatedAt().toString()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.dnd.gongmuin.question_post.dto;

import java.util.List;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record RegisterQuestionPostRequest(

@Size(min = 2, max = 20, message = "제목은 2자 이상 20자 이하여야 합니다.")
String title,
@Size(min = 10, max = 200, message = "본문은 10자 이상 200자 이하여야 합니다.")
String content,
List<String> imageUrls,
@Min(value = 2_000, message = "리워드는 2000 이상이어야 합니다.")
@Max(value = 10_000, message = "리워드는 10000 이하여야 합니다.")
int reward,
@NotBlank(message = "직군을 입력해주세요.")
String targetJobGroup
) {
public static RegisterQuestionPostRequest of(
String title,
String content,
List<String> imageUrls,
int reward,
String targetJobGroup
) {
return new RegisterQuestionPostRequest(
title, content, imageUrls, reward, targetJobGroup
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.dnd.gongmuin.question_post.exception;

import com.dnd.gongmuin.common.exception.ErrorCode;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum QuestionPostErrorCode implements ErrorCode {

NOT_FOUND_QUESTION_POST("해당 아이디의 질문 포스트가 존재하지 않습니다.", "QP_001");

private final String message;
private final String code;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.dnd.gongmuin.question_post.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.dnd.gongmuin.question_post.domain.QuestionPost;

public interface QuestionPostRepository extends JpaRepository<QuestionPost, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.dnd.gongmuin.question_post.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.dnd.gongmuin.common.exception.runtime.NotFoundException;
import com.dnd.gongmuin.member.domain.Member;
import com.dnd.gongmuin.question_post.domain.QuestionPost;
import com.dnd.gongmuin.question_post.dto.QuestionPostDetailResponse;
import com.dnd.gongmuin.question_post.dto.QuestionPostMapper;
import com.dnd.gongmuin.question_post.dto.RegisterQuestionPostRequest;
import com.dnd.gongmuin.question_post.exception.QuestionPostErrorCode;
import com.dnd.gongmuin.question_post.repository.QuestionPostRepository;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class QuestionPostService {

private final QuestionPostRepository questionPostRepository;

@Transactional
public QuestionPostDetailResponse registerQuestionPost(@Valid RegisterQuestionPostRequest request, Member member) {
QuestionPost questionPost = QuestionPostMapper.toQuestionPost(request, member);
return QuestionPostMapper.toQuestionPostDetailResponse(questionPostRepository.save(questionPost));
}

@Transactional(readOnly = true)
public QuestionPostDetailResponse getQuestionPostById(Long questionPostId) {
QuestionPost questionPost = questionPostRepository.findById(questionPostId)
.orElseThrow(() -> new NotFoundException(QuestionPostErrorCode.NOT_FOUND_QUESTION_POST));
return QuestionPostMapper.toQuestionPostDetailResponse(questionPost);
}
}
Loading
Loading