From 9eea2235bee5819bac488d5b3468f208ba4c1420 Mon Sep 17 00:00:00 2001 From: phjppo0918 Date: Thu, 26 Oct 2023 15:05:19 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=EB=8B=B5=EB=B3=80=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../answer/command/AnswerCommentRepo.kt | 6 ++++++ .../domain/answer/command/AnswerCtrl.kt | 6 ++++++ .../domain/answer/command/AnswerSvc.kt | 7 +++++++ .../command/dto/CreateAnswerCommentReq.kt | 8 +++++++ .../answer/command/entity/AnswerComment.kt | 11 ++++++++++ .../domain/answer/query/AnswerDao.kt | 21 ++++++++++++++++--- .../answer/query/dto/AnswerCommentRes.kt | 10 +++++++++ .../domain/answer/query/dto/AnswerRes.kt | 1 + .../domain/answer/query/AnswerDaoTest.kt | 19 ++++++++++++++++- .../answer/query/AnswerQueryCtrlTest.kt | 8 +++---- 10 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 src/main/kotlin/site/qbox/qboxserver/domain/answer/command/AnswerCommentRepo.kt create mode 100644 src/main/kotlin/site/qbox/qboxserver/domain/answer/command/dto/CreateAnswerCommentReq.kt create mode 100644 src/main/kotlin/site/qbox/qboxserver/domain/answer/command/entity/AnswerComment.kt create mode 100644 src/main/kotlin/site/qbox/qboxserver/domain/answer/query/dto/AnswerCommentRes.kt diff --git a/src/main/kotlin/site/qbox/qboxserver/domain/answer/command/AnswerCommentRepo.kt b/src/main/kotlin/site/qbox/qboxserver/domain/answer/command/AnswerCommentRepo.kt new file mode 100644 index 0000000..a6b24eb --- /dev/null +++ b/src/main/kotlin/site/qbox/qboxserver/domain/answer/command/AnswerCommentRepo.kt @@ -0,0 +1,6 @@ +package site.qbox.qboxserver.domain.answer.command + +import org.springframework.data.jpa.repository.JpaRepository +import site.qbox.qboxserver.domain.answer.command.entity.AnswerComment + +interface AnswerCommentRepo : JpaRepository \ No newline at end of file diff --git a/src/main/kotlin/site/qbox/qboxserver/domain/answer/command/AnswerCtrl.kt b/src/main/kotlin/site/qbox/qboxserver/domain/answer/command/AnswerCtrl.kt index 7afcc1a..ab54bfd 100644 --- a/src/main/kotlin/site/qbox/qboxserver/domain/answer/command/AnswerCtrl.kt +++ b/src/main/kotlin/site/qbox/qboxserver/domain/answer/command/AnswerCtrl.kt @@ -3,6 +3,7 @@ package site.qbox.qboxserver.domain.answer.command import org.springframework.http.HttpStatus import org.springframework.security.core.Authentication import org.springframework.web.bind.annotation.* +import site.qbox.qboxserver.domain.answer.command.dto.CreateAnswerCommentReq import site.qbox.qboxserver.domain.answer.command.dto.CreateAnswerReq @RestController @@ -14,4 +15,9 @@ class AnswerCtrl( @ResponseStatus(HttpStatus.CREATED) fun createAnswer(@RequestBody req: CreateAnswerReq, auth: Authentication) = answerSvc.create(req, auth.name) + + @PostMapping("comments") + @ResponseStatus(HttpStatus.CREATED) + fun createAnswerComment(@RequestBody req: CreateAnswerCommentReq, auth:Authentication) = + answerSvc.addAnswer(req, auth.name) } \ No newline at end of file diff --git a/src/main/kotlin/site/qbox/qboxserver/domain/answer/command/AnswerSvc.kt b/src/main/kotlin/site/qbox/qboxserver/domain/answer/command/AnswerSvc.kt index 519f78f..8eead3c 100644 --- a/src/main/kotlin/site/qbox/qboxserver/domain/answer/command/AnswerSvc.kt +++ b/src/main/kotlin/site/qbox/qboxserver/domain/answer/command/AnswerSvc.kt @@ -1,14 +1,21 @@ package site.qbox.qboxserver.domain.answer.command +import site.qbox.qboxserver.domain.answer.command.dto.CreateAnswerCommentReq import site.qbox.qboxserver.domain.answer.command.dto.CreateAnswerReq import site.qbox.qboxserver.domain.answer.command.entity.Answer +import site.qbox.qboxserver.domain.answer.command.entity.AnswerComment import site.qbox.qboxserver.global.annotation.CommandService @CommandService class AnswerSvc ( private val answerRepo: AnswerRepo, + private val answerCommentRepo: AnswerCommentRepo, ){ fun create(req: CreateAnswerReq, writer: String) { answerRepo.save(Answer(req.content, req.question, writer)) } + + fun addAnswer(req: CreateAnswerCommentReq, writer: String) { + answerCommentRepo.save(AnswerComment(req.content, writer, req.answer)) + } } \ No newline at end of file diff --git a/src/main/kotlin/site/qbox/qboxserver/domain/answer/command/dto/CreateAnswerCommentReq.kt b/src/main/kotlin/site/qbox/qboxserver/domain/answer/command/dto/CreateAnswerCommentReq.kt new file mode 100644 index 0000000..4bdb07a --- /dev/null +++ b/src/main/kotlin/site/qbox/qboxserver/domain/answer/command/dto/CreateAnswerCommentReq.kt @@ -0,0 +1,8 @@ +package site.qbox.qboxserver.domain.answer.command.dto + +import site.qbox.qboxserver.domain.answer.command.entity.AnswerId + +data class CreateAnswerCommentReq ( + val answer: AnswerId, + val content: String, +) diff --git a/src/main/kotlin/site/qbox/qboxserver/domain/answer/command/entity/AnswerComment.kt b/src/main/kotlin/site/qbox/qboxserver/domain/answer/command/entity/AnswerComment.kt new file mode 100644 index 0000000..431f74a --- /dev/null +++ b/src/main/kotlin/site/qbox/qboxserver/domain/answer/command/entity/AnswerComment.kt @@ -0,0 +1,11 @@ +package site.qbox.qboxserver.domain.answer.command.entity + +import jakarta.persistence.* + +@Entity +class AnswerComment( + @Lob var content: String, + var commentWriterId: String, + @Embedded val answer: AnswerId, + @Id @GeneratedValue val id: Long = 0L, +) \ No newline at end of file diff --git a/src/main/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerDao.kt b/src/main/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerDao.kt index 7eee803..86bbd3a 100644 --- a/src/main/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerDao.kt +++ b/src/main/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerDao.kt @@ -1,10 +1,13 @@ package site.qbox.qboxserver.domain.answer.query +import com.querydsl.core.group.GroupBy import com.querydsl.jpa.impl.JPAQueryFactory import site.qbox.qboxserver.domain.answer.command.entity.QAnswer.answer +import site.qbox.qboxserver.domain.answer.command.entity.QAnswerComment.answerComment import site.qbox.qboxserver.domain.answer.query.dto.AnswerRes +import site.qbox.qboxserver.domain.answer.query.dto.QAnswerCommentRes import site.qbox.qboxserver.domain.answer.query.dto.QAnswerRes -import site.qbox.qboxserver.domain.member.command.entity.QMember.member +import site.qbox.qboxserver.domain.member.command.entity.QMember import site.qbox.qboxserver.domain.member.query.dto.QMemberRes import site.qbox.qboxserver.global.annotation.QueryService @@ -13,17 +16,29 @@ class AnswerDao( private val queryFactory: JPAQueryFactory, ) { fun findAllByQuestion(questionId: Long): List { + val answerWriter = QMember.member + val commentWriter = QMember.member return queryFactory .select( QAnswerRes( answer.content, answer.id.questionId, - QMemberRes(member.email, member.nickname) + QMemberRes(answerWriter.email, answerWriter.nickname), + GroupBy.list( + QAnswerCommentRes( + answerComment.id, + answerComment.content, + QMemberRes(commentWriter.email, commentWriter.nickname) + ) + ) ) ) .from(answer) .where(answer.id.questionId.eq(questionId)) - .join(member).on(answer.id.writerId.eq(member.email)) + .leftJoin(answerComment).on(answerComment.answer.eq(answer.id)) + .join(answerWriter).on(answer.id.writerId.eq(answerWriter.email)) + .join(commentWriter).on(answerComment.commentWriterId.eq(commentWriter.email)) + .groupBy(answer.id) .fetch() } } \ No newline at end of file diff --git a/src/main/kotlin/site/qbox/qboxserver/domain/answer/query/dto/AnswerCommentRes.kt b/src/main/kotlin/site/qbox/qboxserver/domain/answer/query/dto/AnswerCommentRes.kt new file mode 100644 index 0000000..18cd057 --- /dev/null +++ b/src/main/kotlin/site/qbox/qboxserver/domain/answer/query/dto/AnswerCommentRes.kt @@ -0,0 +1,10 @@ +package site.qbox.qboxserver.domain.answer.query.dto + +import com.querydsl.core.annotations.QueryProjection +import site.qbox.qboxserver.domain.member.query.dto.MemberRes + +data class AnswerCommentRes @QueryProjection constructor( + val id: Long, + val content: String, + val writer: MemberRes, +) \ No newline at end of file diff --git a/src/main/kotlin/site/qbox/qboxserver/domain/answer/query/dto/AnswerRes.kt b/src/main/kotlin/site/qbox/qboxserver/domain/answer/query/dto/AnswerRes.kt index e5f6bbc..344acab 100644 --- a/src/main/kotlin/site/qbox/qboxserver/domain/answer/query/dto/AnswerRes.kt +++ b/src/main/kotlin/site/qbox/qboxserver/domain/answer/query/dto/AnswerRes.kt @@ -7,4 +7,5 @@ data class AnswerRes @QueryProjection constructor( val content: String, val questionId: Long, val writer: MemberRes, + val comments: List? ) \ No newline at end of file diff --git a/src/test/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerDaoTest.kt b/src/test/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerDaoTest.kt index 5e42216..e3c9fe9 100644 --- a/src/test/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerDaoTest.kt +++ b/src/test/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerDaoTest.kt @@ -8,8 +8,10 @@ import io.kotest.matchers.shouldBe import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.security.crypto.password.PasswordEncoder +import site.qbox.qboxserver.domain.answer.command.AnswerCommentRepo import site.qbox.qboxserver.domain.answer.command.AnswerRepo import site.qbox.qboxserver.domain.answer.command.entity.Answer +import site.qbox.qboxserver.domain.answer.command.entity.AnswerComment import site.qbox.qboxserver.domain.answer.command.entity.AnswerId import site.qbox.qboxserver.domain.member.command.MemberRepo import site.qbox.qboxserver.domain.member.command.entity.Member @@ -25,6 +27,8 @@ class AnswerDaoTest : DescribeSpec() { @Autowired lateinit var answerRepo: AnswerRepo @Autowired + lateinit var answerCommentRepo: AnswerCommentRepo + @Autowired lateinit var memberRepo: MemberRepo @Autowired lateinit var passwordEncoder: PasswordEncoder @@ -43,7 +47,7 @@ class AnswerDaoTest : DescribeSpec() { ) ) - answerRepo.saveAll( + val answers = answerRepo.saveAll( listOf( Answer("내용1", AnswerId(targetQuestion, members[0].email)), Answer("내용2", AnswerId(targetQuestion, members[1].email)), @@ -55,6 +59,19 @@ class AnswerDaoTest : DescribeSpec() { Answer("내용8", AnswerId(2, members[2].email)), ) ) + + answerCommentRepo.saveAll( + listOf( + AnswerComment("댓글1", members[0].email, answers[1].id), + AnswerComment("댓글2", members[1].email, answers[1].id), + AnswerComment("댓글3", members[1].email, answers[1].id), + AnswerComment("댓글4", members[2].email, answers[1].id), + AnswerComment("댓글5", members[1].email, answers[2].id), + AnswerComment("댓글6", members[3].email, answers[2].id), + ) + ) + + } it("question을 통한 목록 조회를 수행한다.") { diff --git a/src/test/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerQueryCtrlTest.kt b/src/test/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerQueryCtrlTest.kt index b076ee9..6346faa 100644 --- a/src/test/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerQueryCtrlTest.kt +++ b/src/test/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerQueryCtrlTest.kt @@ -26,10 +26,10 @@ class AnswerQueryCtrlTest : WebClientDocsTest() { val questionId = 5L every { answerDao.findAllByQuestion(questionId) } returns listOf( - AnswerRes("내용1", questionId, MemberRes("aaa@bb.com", "닉넴1")), - AnswerRes("내용2", questionId, MemberRes("bbb@bb.com", "닉넴2")), - AnswerRes("내용3", questionId, MemberRes("hhh@bb.com", "닉넴3")), - AnswerRes("내용4", questionId, MemberRes("jjj@bb.com", "닉넴4")), + AnswerRes("내용1", questionId, MemberRes("aaa@bb.com", "닉넴1"), listOf()), + AnswerRes("내용3", questionId, MemberRes("hhh@bb.com", "닉넴3"), listOf()), + AnswerRes("내용4", questionId, MemberRes("jjj@bb.com", "닉넴4"), listOf()), + AnswerRes("내용2", questionId, MemberRes("bbb@bb.com", "닉넴2"), listOf()), ) val params = LinkedMultiValueMap() From 5016804940cf788163416bbf7e09eed70faacadc Mon Sep 17 00:00:00 2001 From: phjppo0918 Date: Fri, 27 Oct 2023 16:29:38 +0900 Subject: [PATCH 2/5] =?UTF-8?q?test:=20AnswerComment=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../qbox/qboxserver/domain/answer/query/AnswerDaoTest.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/test/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerDaoTest.kt b/src/test/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerDaoTest.kt index e3c9fe9..d035597 100644 --- a/src/test/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerDaoTest.kt +++ b/src/test/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerDaoTest.kt @@ -68,6 +68,7 @@ class AnswerDaoTest : DescribeSpec() { AnswerComment("댓글4", members[2].email, answers[1].id), AnswerComment("댓글5", members[1].email, answers[2].id), AnswerComment("댓글6", members[3].email, answers[2].id), + AnswerComment("댓글7", members[3].email, answers[7].id), ) ) @@ -79,11 +80,18 @@ class AnswerDaoTest : DescribeSpec() { result.size shouldBe 5 result.map { it.content } shouldContainAll listOf("내용1", "내용2", "내용3", "내용4", "내용5") } + + it("answer에 해당하는 댓글들을 조회한다.") { + val result = answerDao.findAllByQuestion(targetQuestion) + result.size shouldBe 5 + result[0].comments.map { it.content }.containsAll(listOf("댓글1","댓글2", "댓글3", "댓글4")) + } } afterEach { memberRepo.deleteAll() answerRepo.deleteAll() + answerCommentRepo.deleteAll() } } } From 963c6d5e8ca1cdf9e5f62856b6bf6f430f50888e Mon Sep 17 00:00:00 2001 From: phjppo0918 Date: Fri, 27 Oct 2023 16:29:59 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat=20:=20AnswerRes=20=EA=B3=84=EC=B8=B5?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../qboxserver/domain/answer/query/dto/AnswerRes.kt | 10 ++++++---- .../domain/answer/query/dto/AnswerSummary.kt | 10 ++++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/site/qbox/qboxserver/domain/answer/query/dto/AnswerSummary.kt diff --git a/src/main/kotlin/site/qbox/qboxserver/domain/answer/query/dto/AnswerRes.kt b/src/main/kotlin/site/qbox/qboxserver/domain/answer/query/dto/AnswerRes.kt index 344acab..bccf9ae 100644 --- a/src/main/kotlin/site/qbox/qboxserver/domain/answer/query/dto/AnswerRes.kt +++ b/src/main/kotlin/site/qbox/qboxserver/domain/answer/query/dto/AnswerRes.kt @@ -1,11 +1,13 @@ package site.qbox.qboxserver.domain.answer.query.dto -import com.querydsl.core.annotations.QueryProjection import site.qbox.qboxserver.domain.member.query.dto.MemberRes -data class AnswerRes @QueryProjection constructor( +data class AnswerRes( val content: String, val questionId: Long, val writer: MemberRes, - val comments: List? -) \ No newline at end of file + val comments: List +) { + constructor(res: AnswerSummary, comments: List) : + this(res.content, res.questionId, res.writer, comments) +} diff --git a/src/main/kotlin/site/qbox/qboxserver/domain/answer/query/dto/AnswerSummary.kt b/src/main/kotlin/site/qbox/qboxserver/domain/answer/query/dto/AnswerSummary.kt new file mode 100644 index 0000000..56a91f1 --- /dev/null +++ b/src/main/kotlin/site/qbox/qboxserver/domain/answer/query/dto/AnswerSummary.kt @@ -0,0 +1,10 @@ +package site.qbox.qboxserver.domain.answer.query.dto + +import com.querydsl.core.annotations.QueryProjection +import site.qbox.qboxserver.domain.member.query.dto.MemberRes + +data class AnswerSummary @QueryProjection constructor( + val content: String, + val questionId: Long, + val writer: MemberRes +) \ No newline at end of file From a2bf4d05b1d6dd3ddbde8cd9fff1187c375398e3 Mon Sep 17 00:00:00 2001 From: phjppo0918 Date: Fri, 27 Oct 2023 16:30:11 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix=20:=20QueryDSL=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../site/qbox/qboxserver/global/querydsl/QuerydslConfig.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/site/qbox/qboxserver/global/querydsl/QuerydslConfig.kt b/src/main/kotlin/site/qbox/qboxserver/global/querydsl/QuerydslConfig.kt index fd1ee5c..dfb29cd 100644 --- a/src/main/kotlin/site/qbox/qboxserver/global/querydsl/QuerydslConfig.kt +++ b/src/main/kotlin/site/qbox/qboxserver/global/querydsl/QuerydslConfig.kt @@ -1,5 +1,6 @@ package site.qbox.qboxserver.global.querydsl +import com.querydsl.jpa.JPQLTemplates import com.querydsl.jpa.impl.JPAQueryFactory import jakarta.persistence.EntityManager @@ -12,6 +13,6 @@ class QuerydslConfig( ) { @Bean fun querydsl(): JPAQueryFactory { - return JPAQueryFactory(em) + return JPAQueryFactory(JPQLTemplates.DEFAULT, em) } } \ No newline at end of file From 85841f314d31ec22a69b8b36eec9083e602d0f82 Mon Sep 17 00:00:00 2001 From: phjppo0918 Date: Fri, 27 Oct 2023 16:30:39 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat=20:=20Answer=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=8B=9C=20=EB=8C=93=EA=B8=80=EB=8F=84=20=EA=B0=99=EC=9D=B4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EB=90=98=EB=8F=84=EB=A1=9D=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/answer/query/AnswerDao.kt | 61 +++++---- .../domain/answer/command/AnswerCtrlTest.kt | 19 +++ .../domain/answer/command/AnswerSvcTest.kt | 22 ++++ .../answer/query/AnswerDaoQueryDslTest.kt | 118 ++++++++++++++++++ .../answer/query/AnswerQueryCtrlTest.kt | 27 +++- 5 files changed, 219 insertions(+), 28 deletions(-) create mode 100644 src/test/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerDaoQueryDslTest.kt diff --git a/src/main/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerDao.kt b/src/main/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerDao.kt index 86bbd3a..a5070fb 100644 --- a/src/main/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerDao.kt +++ b/src/main/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerDao.kt @@ -1,13 +1,13 @@ package site.qbox.qboxserver.domain.answer.query -import com.querydsl.core.group.GroupBy +import com.querydsl.core.group.GroupBy.groupBy +import com.querydsl.core.group.GroupBy.list import com.querydsl.jpa.impl.JPAQueryFactory +import site.qbox.qboxserver.domain.answer.command.entity.AnswerId import site.qbox.qboxserver.domain.answer.command.entity.QAnswer.answer import site.qbox.qboxserver.domain.answer.command.entity.QAnswerComment.answerComment -import site.qbox.qboxserver.domain.answer.query.dto.AnswerRes -import site.qbox.qboxserver.domain.answer.query.dto.QAnswerCommentRes -import site.qbox.qboxserver.domain.answer.query.dto.QAnswerRes -import site.qbox.qboxserver.domain.member.command.entity.QMember +import site.qbox.qboxserver.domain.answer.query.dto.* +import site.qbox.qboxserver.domain.member.command.entity.QMember.member import site.qbox.qboxserver.domain.member.query.dto.QMemberRes import site.qbox.qboxserver.global.annotation.QueryService @@ -16,29 +16,44 @@ class AnswerDao( private val queryFactory: JPAQueryFactory, ) { fun findAllByQuestion(questionId: Long): List { - val answerWriter = QMember.member - val commentWriter = QMember.member - return queryFactory - .select( - QAnswerRes( - answer.content, - answer.id.questionId, - QMemberRes(answerWriter.email, answerWriter.nickname), - GroupBy.list( + val comments = getCommentsGroup(questionId) + val summary = getSummary(questionId) + + return mapToRes(summary, comments) + + } + + private fun getCommentsGroup(questionId: Long): MutableMap> = + queryFactory.from(answerComment) + .where(answerComment.answer.questionId.eq(questionId)) + .join(member).on(answerComment.commentWriterId.eq(member.email)) + .transform( + groupBy(answerComment.answer).`as`( + list( QAnswerCommentRes( answerComment.id, answerComment.content, - QMemberRes(commentWriter.email, commentWriter.nickname) - ) - ) - ) - ) + QMemberRes(member.email, member.nickname))))) + + private fun getSummary(questionId: Long): MutableList = + queryFactory.select( + QAnswerSummary( + answer.content, + answer.id.questionId, + QMemberRes(member.email, member.nickname) + )) .from(answer) .where(answer.id.questionId.eq(questionId)) - .leftJoin(answerComment).on(answerComment.answer.eq(answer.id)) - .join(answerWriter).on(answer.id.writerId.eq(answerWriter.email)) - .join(commentWriter).on(answerComment.commentWriterId.eq(commentWriter.email)) - .groupBy(answer.id) + .join(member).on(answer.id.writerId.eq(member.email)) .fetch() + + private fun mapToRes( + summary: MutableList, + comments: MutableMap> + ) = summary.map { + AnswerRes(it, comments[getAnswerId(it)] ?: emptyList()) } + + private fun getAnswerId(it: AnswerSummary) = + AnswerId(it.questionId, it.writer.email) } \ No newline at end of file diff --git a/src/test/kotlin/site/qbox/qboxserver/domain/answer/command/AnswerCtrlTest.kt b/src/test/kotlin/site/qbox/qboxserver/domain/answer/command/AnswerCtrlTest.kt index 2180b55..5000110 100644 --- a/src/test/kotlin/site/qbox/qboxserver/domain/answer/command/AnswerCtrlTest.kt +++ b/src/test/kotlin/site/qbox/qboxserver/domain/answer/command/AnswerCtrlTest.kt @@ -8,7 +8,9 @@ import org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath import org.springframework.restdocs.payload.PayloadDocumentation.requestFields import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import site.qbox.qboxserver.config.WebClientDocsTest +import site.qbox.qboxserver.domain.answer.command.dto.CreateAnswerCommentReq import site.qbox.qboxserver.domain.answer.command.dto.CreateAnswerReq +import site.qbox.qboxserver.domain.answer.command.entity.AnswerId @WebMvcTest(AnswerCtrl::class) @DisplayName("AnswerCtrl") @@ -32,6 +34,23 @@ class AnswerCtrlTest : WebClientDocsTest() { )) ) } + + it("answer에 대한 댓글을 생성한다.") { + val req = CreateAnswerCommentReq(AnswerId(4, "aaa@bb.com"), "댓글내용") + + val action = performPost("/answers/comments", req) + + action.andExpect(status().isCreated) + + action.andDo( + print("create-answer-comment", + requestFields( + fieldWithPath("answer.questionId").type(JsonFieldType.NUMBER).description("답변의 Question ID"), + fieldWithPath("answer.writerId").type(JsonFieldType.STRING).description("답변 작성자 email"), + fieldWithPath("content").type(JsonFieldType.STRING).description("댓글 내용"), + )) + ) + } } } diff --git a/src/test/kotlin/site/qbox/qboxserver/domain/answer/command/AnswerSvcTest.kt b/src/test/kotlin/site/qbox/qboxserver/domain/answer/command/AnswerSvcTest.kt index 3ee4a0c..18eb8f9 100644 --- a/src/test/kotlin/site/qbox/qboxserver/domain/answer/command/AnswerSvcTest.kt +++ b/src/test/kotlin/site/qbox/qboxserver/domain/answer/command/AnswerSvcTest.kt @@ -7,6 +7,7 @@ import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest +import site.qbox.qboxserver.domain.answer.command.dto.CreateAnswerCommentReq import site.qbox.qboxserver.domain.answer.command.dto.CreateAnswerReq import site.qbox.qboxserver.domain.answer.command.entity.AnswerId @@ -17,6 +18,8 @@ class AnswerSvcTest : DescribeSpec() { @Autowired lateinit var answerRepo: AnswerRepo + @Autowired + lateinit var answerCommentRepo: AnswerCommentRepo @Autowired lateinit var answerSvc: AnswerSvc @@ -36,5 +39,24 @@ class AnswerSvcTest : DescribeSpec() { result.id.questionId shouldBe question result.id.writerId shouldBe writer } + + it("answer comment 생성을 수행한다") { + val content = "댓글내용" + val req = CreateAnswerCommentReq(AnswerId(4, "aaa@bb.com"), content) + + answerSvc.addAnswer(req, "댓글작성자") + answerSvc.addAnswer(req, "댓글작성자") + answerSvc.addAnswer(req, "댓글작성자") + + val result = answerCommentRepo.findAll() + + result.size shouldBe 3 + result.map { it.content }.containsAll(listOf(content, content, content)) + } + + afterEach { + answerRepo.deleteAll() + answerCommentRepo.deleteAll() + } } } diff --git a/src/test/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerDaoQueryDslTest.kt b/src/test/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerDaoQueryDslTest.kt new file mode 100644 index 0000000..1a47f59 --- /dev/null +++ b/src/test/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerDaoQueryDslTest.kt @@ -0,0 +1,118 @@ +package site.qbox.qboxserver.domain.answer.query + +import com.querydsl.core.group.GroupBy.groupBy +import com.querydsl.core.group.GroupBy.list +import com.querydsl.jpa.impl.JPAQueryFactory +import io.kotest.core.spec.DisplayName +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.extensions.spring.SpringExtension +import io.kotest.matchers.shouldBe +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.security.crypto.password.PasswordEncoder +import site.qbox.qboxserver.domain.answer.command.AnswerCommentRepo +import site.qbox.qboxserver.domain.answer.command.AnswerRepo +import site.qbox.qboxserver.domain.answer.command.entity.Answer +import site.qbox.qboxserver.domain.answer.command.entity.AnswerComment +import site.qbox.qboxserver.domain.answer.command.entity.AnswerId +import site.qbox.qboxserver.domain.answer.command.entity.QAnswer.* +import site.qbox.qboxserver.domain.answer.command.entity.QAnswerComment.answerComment +import site.qbox.qboxserver.domain.answer.query.dto.QAnswerCommentRes +import site.qbox.qboxserver.domain.member.command.MemberRepo +import site.qbox.qboxserver.domain.member.command.entity.Member +import site.qbox.qboxserver.domain.member.command.entity.QMember.member +import site.qbox.qboxserver.domain.member.query.dto.QMemberRes + +@DisplayName("querydsl-answer") +@SpringBootTest +class AnswerDaoQueryDslTest : DescribeSpec() { + override fun extensions() = listOf(SpringExtension) + + @Autowired + lateinit var queryFactory: JPAQueryFactory + + @Autowired + lateinit var answerRepo: AnswerRepo + + @Autowired + lateinit var answerCommentRepo: AnswerCommentRepo + + @Autowired + lateinit var memberRepo: MemberRepo + + @Autowired + lateinit var passwordEncoder: PasswordEncoder + + lateinit var members: List + lateinit var answers: List + lateinit var answerComments: List + + init { + describe("querydsl 에서") { + beforeEach { + members = memberRepo.saveAll( + listOf( + Member("aaa@ccc.ac.kr", "n1", "p1", passwordEncoder), + Member("bbb@ccc.ac.kr", "n2", "p2", passwordEncoder), + Member("fff@ccc.ac.kr", "n3", "p4", passwordEncoder), + Member("ggg@ccc.ac.kr", "n4", "32", passwordEncoder), + Member("hhh@ccc.ac.kr", "n5", "42", passwordEncoder), + ) + ) + + answers = answerRepo.saveAll( + listOf( + Answer("내용1", AnswerId(1, members[0].email)), + Answer("내용2", AnswerId(1, members[1].email)), + Answer("내용3", AnswerId(1, members[2].email)), + Answer("내용4", AnswerId(1, members[3].email)), + Answer("내용5", AnswerId(1, members[4].email)), + Answer("내용6", AnswerId(2, members[0].email)), + Answer("내용7", AnswerId(2, members[1].email)), + Answer("내용8", AnswerId(2, members[2].email)), + ) + ) + + answerComments = answerCommentRepo.saveAll( + listOf( + AnswerComment("댓글1", members[0].email, answers[1].id), + AnswerComment("댓글2", members[1].email, answers[1].id), + AnswerComment("댓글3", members[1].email, answers[1].id), + AnswerComment("댓글4", members[2].email, answers[1].id), + AnswerComment("댓글5", members[1].email, answers[2].id), + AnswerComment("댓글6", members[3].email, answers[2].id), + AnswerComment("댓글7", members[3].email, answers[7].id), + ) + ) + } + + it("question별 answer comment 조회를 수행한다.") { + val result = queryFactory.from(answerComment) + .where(answerComment.answer.questionId.eq(1)) + .join(member).on(answerComment.commentWriterId.eq(member.email)) + .transform( + groupBy(answerComment.answer).`as`(list( + QAnswerCommentRes( + answerComment.id, + answerComment.content, + QMemberRes(member.email, member.nickname) + ) + )) + ) + + result[answers[1].id]?.size shouldBe 4 + result[answers[1].id]?.map { it.content }?.containsAll(listOf("댓글1", "댓글2","댓글3", "댓글4")) + result[answers[1].id]?.map { it.writer.nickname }?.containsAll(listOf("n1", "n2","n3", "n4")) + } + } + + + afterEach { + memberRepo.deleteAll() + answerRepo.deleteAll() + answerCommentRepo.deleteAll() + } + } + + +} diff --git a/src/test/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerQueryCtrlTest.kt b/src/test/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerQueryCtrlTest.kt index 6346faa..ab56f1a 100644 --- a/src/test/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerQueryCtrlTest.kt +++ b/src/test/kotlin/site/qbox/qboxserver/domain/answer/query/AnswerQueryCtrlTest.kt @@ -11,6 +11,7 @@ import org.springframework.restdocs.request.RequestDocumentation.parameterWithNa import org.springframework.restdocs.request.RequestDocumentation.queryParameters import org.springframework.util.LinkedMultiValueMap import site.qbox.qboxserver.config.WebClientDocsTest +import site.qbox.qboxserver.domain.answer.query.dto.AnswerCommentRes import site.qbox.qboxserver.domain.answer.query.dto.AnswerRes import site.qbox.qboxserver.domain.member.query.dto.MemberRes @@ -26,10 +27,21 @@ class AnswerQueryCtrlTest : WebClientDocsTest() { val questionId = 5L every { answerDao.findAllByQuestion(questionId) } returns listOf( - AnswerRes("내용1", questionId, MemberRes("aaa@bb.com", "닉넴1"), listOf()), - AnswerRes("내용3", questionId, MemberRes("hhh@bb.com", "닉넴3"), listOf()), - AnswerRes("내용4", questionId, MemberRes("jjj@bb.com", "닉넴4"), listOf()), - AnswerRes("내용2", questionId, MemberRes("bbb@bb.com", "닉넴2"), listOf()), + AnswerRes("내용1", questionId, MemberRes("aaa@bb.com", "닉넴1"), listOf( + AnswerCommentRes(1, "댓글1",MemberRes("kkk@bb.com", "댓글작성자")), + AnswerCommentRes(2, "댓글2",MemberRes("kkk2@bb.com", "댓글작성자2")) + )), + AnswerRes("내용3", questionId, MemberRes("hhh@bb.com", "닉넴3"), listOf( + AnswerCommentRes(3, "댓글1",MemberRes("kkk@bb.com", "댓글작성자")), + AnswerCommentRes(4, "댓글2",MemberRes("kkk2@bb.com", "댓글작성자2")) + )), + AnswerRes("내용4", questionId, MemberRes("jjj@bb.com", "닉넴4"), listOf( + AnswerCommentRes(5, "댓글1",MemberRes("kkk@bb.com", "댓글작성자")), + AnswerCommentRes(6, "댓글2",MemberRes("kkk2@bb.com", "댓글작성자2")) + )), + AnswerRes("내용2", questionId, MemberRes("bbb@bb.com", "닉넴2"), listOf( + AnswerCommentRes(7, "댓글1",MemberRes("kkk@bb.com", "댓글작성자")) + )), ) val params = LinkedMultiValueMap() @@ -48,7 +60,12 @@ class AnswerQueryCtrlTest : WebClientDocsTest() { fieldWithPath("[].questionId").type(JsonFieldType.NUMBER).description("question ID"), fieldWithPath("[].writer.email").type(JsonFieldType.STRING).description("작성자 email"), fieldWithPath("[].writer.nickname").type(JsonFieldType.STRING).description("작성자 Nickname"), - ) + fieldWithPath("[].comments.[].id").type(JsonFieldType.NUMBER).description("댓글 ID").optional(), + fieldWithPath("[].comments.[].content").type(JsonFieldType.STRING).description("댓글 내용").optional(), + fieldWithPath("[].comments.[].content").type(JsonFieldType.STRING).description("댓글 내용").optional(), + fieldWithPath("[].comments.[].writer.email").type(JsonFieldType.STRING).description("댓글 작성자 email").optional(), + fieldWithPath("[].comments.[].writer.nickname").type(JsonFieldType.STRING).description("댓글 작성자 Nickname").optional(), + ) ) ) }