Skip to content

Commit

Permalink
Merge pull request #30 from Team-Plu/feat/#28
Browse files Browse the repository at this point in the history
[FEAT] 질문 답변 조회 기능
  • Loading branch information
PgmJun authored Mar 9, 2024
2 parents 14fdd88 + 1a5b5a9 commit 9a3c41c
Show file tree
Hide file tree
Showing 14 changed files with 250 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.th.plu.api.controller.answer

import com.th.plu.api.config.interceptor.Auth
import com.th.plu.api.config.resolver.MemberId
import com.th.plu.api.controller.answer.dto.response.AnswerInfoResponse
import com.th.plu.api.service.answer.AnswerService
import com.th.plu.common.dto.response.ApiResponse
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@Tag(name = "Answer")
@RestController
@RequestMapping("/api")
class AnswerController(
private val answerService: AnswerService
) {
@Auth
@Operation(summary = "답변 조회")
@GetMapping("/v1/answer/{answerId}")
fun findAnswerById(@PathVariable answerId: Long, @MemberId memberId: Long): ApiResponse<AnswerInfoResponse> {
return ApiResponse.success(answerService.findAnswerInfoById(answerId, memberId))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.th.plu.api.controller.answer.dto.response

import com.th.plu.domain.domain.answer.Answer
import com.th.plu.domain.domain.question.Question
import java.time.LocalDateTime

data class AnswerInfoResponse(
val questionDate: LocalDateTime,
val questionTitle: String,
val answer: String,
val likeCount: Int,
val elementImageUrl: String,
val colorCode: String
) {
companion object {
fun of(question: Question, answer: Answer): AnswerInfoResponse {
return AnswerInfoResponse(
questionDate = question.modifiedAt,
questionTitle = question.title,
answer = answer.content,
likeCount = answer.getLikeCount(),
elementImageUrl = question.elementType.elementImageUrl,
colorCode = question.elementType.colorCode
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.th.plu.api.service.answer

import com.th.plu.api.controller.answer.dto.response.AnswerInfoResponse
import com.th.plu.domain.domain.answer.explorer.AnswerExplorer
import com.th.plu.domain.domain.answer.explorer.QuestionExplorer
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
class AnswerService(
private val questionExplorer: QuestionExplorer,
private val answerExplorer: AnswerExplorer,
private val answerValidator: AnswerValidator
) {
@Transactional(readOnly = true)
fun findAnswerInfoById(answerId: Long, memberId: Long): AnswerInfoResponse {
val answer = answerExplorer.findAnswerById(answerId)
if (!answer.isPublic) {
answerValidator.validateIsMemberOwnerOfAnswer(answerId, memberId)
}
val question = questionExplorer.findQuestionById(answer.getQuestionId())

return AnswerInfoResponse.of(question, answer)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.th.plu.api.service.answer

import com.th.plu.common.exception.code.ErrorCode
import com.th.plu.common.exception.model.ValidationException
import com.th.plu.domain.domain.answer.explorer.AnswerExplorer
import com.th.plu.domain.domain.answer.repository.AnswerRepository
import org.springframework.stereotype.Component

@Component
class AnswerValidator(
private val answerExplorer: AnswerExplorer,
private val answerRepository: AnswerRepository
) {
fun validateIsMemberOwnerOfAnswer(answerId: Long, memberId: Long) {
val answer = answerExplorer.findAnswerById(answerId)
if (answer.member.id != memberId) {
throw ValidationException(ErrorCode.INVALID_ANSWER_OWNER,
"멤버 (ID: ${memberId})는 답변 (ID: ${answerId})의 답변자가 아니기 때문에 답변 정보에 접근할 수 없습니다.")
}
}
}
11 changes: 11 additions & 0 deletions plu-api/src/main/resources/sql/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ DROP TABLE IF EXISTS `settings`;
DROP TABLE IF EXISTS `questions`;
DROP TABLE IF EXISTS `answers`;
DROP TABLE IF EXISTS `onboardings`;
DROP TABLE IF EXISTS `likes`;


CREATE TABLE `members`
Expand Down Expand Up @@ -54,3 +55,13 @@ CREATE TABLE `onboardings`
`created_at` datetime NOT NULL,
`modified_at` datetime NOT NULL
);

CREATE TABLE `likes`
(
`like_id` bigint auto_increment primary key,
`question_id` bigint NOT NULL,
`answer_id` bigint NOT NULL,
`member_id` bigint NOT NULL,
`created_at` datetime NOT NULL,
`modified_at` datetime NOT NULL
);
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ enum class ErrorCode(val code: String, val message: String) {
BIND_EXCEPTION("V005", "요청 값을 바인딩하는 과정에서 오류가 발생하였습니다."),
METHOD_ARGUMENT_NOT_VALID_EXCEPTION("V006", "요청 값이 검증되지 않은 값 입니다."),
INVALID_FORMAT_EXCEPTION("V007", "요청 값이 유효하지 않은 데이터입니다."),
INVALID_ANSWER_OWNER("V008", "질문의 소유자가 아닙니다."),

// Unauthorized Exception
UNAUTHORIZED_EXCEPTION("U001", "토큰이 만료되었습니다. 다시 로그인 해주세요."),
Expand All @@ -21,6 +22,8 @@ enum class ErrorCode(val code: String, val message: String) {
// NotFound Exception
NOT_FOUND_EXCEPTION("N001", "존재하지 않습니다."),
NOT_FOUND_MEMBER_EXCEPTION("N002", "탈퇴했거나 존재하지 않는 회원입니다."),
NOT_FOUND_ANSWER_EXCEPTION("N003", "존재하지 않는 답변입니다."),
NOT_FOUND_QUESTION_EXCEPTION("N004", "존재하지 않는 질문입니다."),
NOT_FOUND_ARTICLE_CONTENT_EXCEPTION("N003", "아티클의 컨텐츠가 존재하지 않습니다."),
NOT_FOUND_CHALLENGE_EXCEPTION("N004", "존재하지 않는 챌린지입니다."),
NOT_FOUND_ARTICLE_EXCEPTION("N005", "삭제되었거나 존재하지 않는 아티클입니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.th.plu.domain.domain.answer

import com.th.plu.domain.domain.common.BaseEntity
import com.th.plu.domain.domain.like.Like
import com.th.plu.domain.domain.member.Member
import com.th.plu.domain.domain.question.Question
import jakarta.persistence.*
Expand All @@ -17,23 +18,34 @@ import lombok.NoArgsConstructor
@Builder(access = AccessLevel.PRIVATE)
class Answer(

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "answer_id")
private var id: Long? = null,
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "answer_id")
var id: Long? = null,

@ManyToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
@JoinColumn(name = "member_id", nullable = false)
private var member: Member,
@ManyToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
@JoinColumn(name = "member_id", nullable = false)
var member: Member,

@ManyToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
@JoinColumn(name = "question_id", nullable = false)
private var question: Question,
@ManyToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
@JoinColumn(name = "question_id", nullable = false)
var question: Question,

@Column(name = "answer_content", nullable = false)
private var content: String,
@Column(name = "answer_content", nullable = false)
var content: String,

@Column(name = "is_public", nullable = false)
private var isPublic: Boolean
@Column(name = "is_public", nullable = false)
var isPublic: Boolean,

@OneToMany(mappedBy = "answer", fetch = FetchType.LAZY, cascade = [CascadeType.ALL], orphanRemoval = true)
var likes: List<Like> = mutableListOf()

) : BaseEntity() {

fun getLikeCount(): Int {
return likes.size
}

fun getQuestionId(): Long {
return question.id!!
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.th.plu.domain.domain.answer.explorer

import com.th.plu.common.exception.code.ErrorCode
import com.th.plu.common.exception.model.NotFoundException
import com.th.plu.domain.domain.answer.Answer
import com.th.plu.domain.domain.answer.repository.AnswerRepository
import org.springframework.stereotype.Component

@Component
class AnswerExplorer(
private val answerRepository: AnswerRepository
) {
fun findAnswerById(id: Long): Answer {
return answerRepository.findAnswerById(id)
?: throw NotFoundException(ErrorCode.NOT_FOUND_ANSWER_EXCEPTION, "존재하지 않는 답변(ID: $id) 입니다")
}
}
43 changes: 43 additions & 0 deletions plu-domain/src/main/kotlin/com/th/plu/domain/domain/like/Like.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.th.plu.domain.domain.like

import com.th.plu.domain.domain.answer.Answer
import com.th.plu.domain.domain.common.BaseEntity
import com.th.plu.domain.domain.member.Member
import com.th.plu.domain.domain.question.Question
import jakarta.persistence.*

@Table(name = "likes")
@Entity
class Like(

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "like_id")
var id: Long? = null,

@ManyToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
@JoinColumn(name = "member_id", nullable = false)
var member: Member,

@ManyToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
@JoinColumn(name = "answer_id", nullable = false)
var answer: Answer,

@ManyToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
@JoinColumn(name = "question_id", nullable = false)
var question: Question

) : BaseEntity() {

companion object {
fun newInstance(
member: Member, answer: Answer, question: Question
): Like {
return Like(
member = member,
answer = answer,
question = question
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.th.plu.domain.domain.like.repository

import com.th.plu.domain.domain.member.Member
import org.springframework.data.jpa.repository.JpaRepository

interface LikeRepository : JpaRepository<Member, Long>, LikeRepositoryCustom {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.th.plu.domain.domain.like.repository

import com.th.plu.domain.domain.like.Like

interface LikeRepositoryCustom {

fun findLikeById(id: Long): Like?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.th.plu.domain.domain.like.repository

import com.querydsl.jpa.impl.JPAQueryFactory
import com.th.plu.domain.domain.like.Like
import com.th.plu.domain.domain.like.QLike.like
import org.springframework.stereotype.Repository

@Repository
class LikeRepositoryImpl(private val queryFactory: JPAQueryFactory) : LikeRepositoryCustom {
override fun findLikeById(id: Long): Like? {
return queryFactory
.selectFrom(like)
.where(like.id.eq(id))
.fetchOne();
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.th.plu.domain.domain.question

enum class ElementType(
private val characterImageUrl: String,
private val elementImageUrl: String,
private val colorCode: String
val characterImageUrl: String,
val elementImageUrl: String,
val colorCode: String
) {
// TODO: 엘리먼트 네이밍 체크 필요
WATER("", "", ""),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.th.plu.domain.domain.answer.explorer

import com.th.plu.common.exception.code.ErrorCode
import com.th.plu.common.exception.model.NotFoundException
import com.th.plu.domain.domain.question.Question
import com.th.plu.domain.domain.question.repository.QuestionRepository
import org.springframework.stereotype.Component

@Component
class QuestionExplorer(
private val questionRepository: QuestionRepository
) {
fun findQuestionById(id: Long): Question {
return questionRepository.findQuestionById(id)
?: throw NotFoundException(ErrorCode.NOT_FOUND_QUESTION_EXCEPTION, "존재하지 않는 질문(ID: $id) 입니다")
}
}

0 comments on commit 9a3c41c

Please sign in to comment.