From e7ac5ad37ce2d04161e8aef60f3e5a984fd50179 Mon Sep 17 00:00:00 2001 From: krSeonghyeon Date: Sat, 17 May 2025 15:13:36 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20QNA=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EC=8B=9C=20=EC=A7=88=EB=AC=B8=EC=9D=80=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EB=A7=8C,=20=EB=8C=80=EB=8B=B5=EC=9D=80=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=EC=9E=90=EB=A7=8C=20=EA=B0=80=EB=8A=A5=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koin/domain/club/service/ClubService.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/in/koreatech/koin/domain/club/service/ClubService.java b/src/main/java/in/koreatech/koin/domain/club/service/ClubService.java index 165270a4d6..228ceb0b34 100644 --- a/src/main/java/in/koreatech/koin/domain/club/service/ClubService.java +++ b/src/main/java/in/koreatech/koin/domain/club/service/ClubService.java @@ -193,12 +193,20 @@ private ClubHotResponse getHotClubFromDBAndCache() { public void createQna(CreateQnaRequest request, Integer clubId, Integer studentId) { Club club = clubRepository.getById(clubId); Student student = studentRepository.getById(studentId); - ClubQna parentQna = request.parentId() == null ? null : clubQnaRepository.getById(request.parentId()); boolean isAdmin = clubAdminRepository.existsByClubIdAndUserId(clubId, studentId); + boolean isQuestion = request.parentId() == null; + validateQnaCreateAuthorization(studentId, isQuestion, isAdmin); + ClubQna parentQna = request.parentId() == null ? null : clubQnaRepository.getById(request.parentId()); ClubQna qna = request.toClubQna(club, student, parentQna, isAdmin); clubQnaRepository.save(qna); } + private static void validateQnaCreateAuthorization(Integer studentId, boolean isQuestion, boolean isAdmin) { + if (isQuestion == isAdmin) { + throw AuthorizationException.withDetail("studentId: " + studentId); + } + } + @Transactional public void deleteQna(Integer clubId, Integer qnaId, Integer studentId) { ClubQna qna = clubQnaRepository.getById(qnaId); From 94efdbe6a0e32c322b5f371bb3280c9f39b113c6 Mon Sep 17 00:00:00 2001 From: krSeonghyeon Date: Sat, 17 May 2025 15:34:21 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20QNA=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EC=8B=9C=20=EB=AC=B4=EC=A1=B0=EA=B1=B4=20=ED=95=98=EB=93=9C?= =?UTF-8?q?=EB=94=9C=EB=A6=AC=ED=8A=B8=20=EB=90=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koreatech/koin/domain/club/model/ClubQna.java | 15 +++++++++++++++ .../koin/domain/club/service/ClubService.java | 7 ++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/club/model/ClubQna.java b/src/main/java/in/koreatech/koin/domain/club/model/ClubQna.java index 15e6bbd877..93b1972885 100644 --- a/src/main/java/in/koreatech/koin/domain/club/model/ClubQna.java +++ b/src/main/java/in/koreatech/koin/domain/club/model/ClubQna.java @@ -81,6 +81,11 @@ private ClubQna( this.isDeleted = isDeleted; } + public void removeChild(ClubQna child) { + children.remove(child); + child.parent = null; + } + public void delete() { isDeleted = true; } @@ -88,4 +93,14 @@ public void delete() { public boolean isRoot() { return parent == null; } + + public boolean isChild() { + return parent != null; + } + + public void detachFromParentIfChild() { + if (isChild()) { + parent.removeChild(this); + } + } } diff --git a/src/main/java/in/koreatech/koin/domain/club/service/ClubService.java b/src/main/java/in/koreatech/koin/domain/club/service/ClubService.java index 228ceb0b34..9476c34950 100644 --- a/src/main/java/in/koreatech/koin/domain/club/service/ClubService.java +++ b/src/main/java/in/koreatech/koin/domain/club/service/ClubService.java @@ -211,11 +211,8 @@ private static void validateQnaCreateAuthorization(Integer studentId, boolean is public void deleteQna(Integer clubId, Integer qnaId, Integer studentId) { ClubQna qna = clubQnaRepository.getById(qnaId); validateQnaDeleteAuthorization(clubId, qna, studentId); - if (qna.isRoot()) { - clubQnaRepository.delete(qna); - } else { - qna.delete(); - } + qna.detachFromParentIfChild(); + clubQnaRepository.delete(qna); } private void validateQnaDeleteAuthorization(Integer clubId, ClubQna qna, Integer studentId) { From 0fa6d5a7b2e07adefa65ba055b34e0821a0f5d32 Mon Sep 17 00:00:00 2001 From: krSeonghyeon Date: Sat, 17 May 2025 15:35:06 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20QNA=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=8B=9C=20isDeleted=EC=99=80=20isAdmin=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koin/domain/club/dto/response/QnasResponse.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/club/dto/response/QnasResponse.java b/src/main/java/in/koreatech/koin/domain/club/dto/response/QnasResponse.java index 3577bbd43b..cdf15b9b4d 100644 --- a/src/main/java/in/koreatech/koin/domain/club/dto/response/QnasResponse.java +++ b/src/main/java/in/koreatech/koin/domain/club/dto/response/QnasResponse.java @@ -38,12 +38,6 @@ public record InnerQnaResponse( @Schema(description = "내용", example = "언제 모집하나요?", requiredMode = REQUIRED) String content, - @Schema(description = "삭제여부", example = "false", requiredMode = REQUIRED) - Boolean isDeleted, - - @Schema(description = "관리자여부", example = "true", requiredMode = REQUIRED) - Boolean isAdmin, - @Schema(description = "작성 일시", example = "2025.05.12 14:00", requiredMode = REQUIRED) @JsonFormat(pattern = "yyyy.MM.dd HH:mm") LocalDateTime createdAt, @@ -65,8 +59,6 @@ public static InnerQnaResponse from(ClubQna qna) { qna.getAuthor().getId(), nickname, qna.getContent(), - qna.getIsDeleted(), - qna.getIsAdmin(), qna.getCreatedAt(), children ); From 0350537b6db97b1a533531fb1a963da2207f91cc Mon Sep 17 00:00:00 2001 From: krSeonghyeon Date: Sat, 17 May 2025 15:37:23 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20=EC=9D=BC=EB=B6=80=20admin=20?= =?UTF-8?q?=ED=91=9C=ED=98=84=EC=9D=84=20manager=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koin/domain/club/service/ClubService.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/club/service/ClubService.java b/src/main/java/in/koreatech/koin/domain/club/service/ClubService.java index 9476c34950..27508b4137 100644 --- a/src/main/java/in/koreatech/koin/domain/club/service/ClubService.java +++ b/src/main/java/in/koreatech/koin/domain/club/service/ClubService.java @@ -67,7 +67,7 @@ public void createClubRequest(CreateClubRequest request, Integer studentId) { @Transactional public ClubResponse updateClub(Integer clubId, UpdateClubRequest request, Integer studentId) { Club club = clubRepository.getById(clubId); - isClubAdmin(clubId, studentId); + isClubManager(clubId, studentId); ClubCategory clubCategory = clubCategoryRepository.getById(request.clubCategoryId()); club.update(request.name(), request.imageUrl(), clubCategory, request.location(), request.description()); @@ -99,7 +99,7 @@ public ClubResponse updateClubIntroduction( Integer clubId, UpdateClubIntroductionRequest request, Integer studentId ) { Club club = clubRepository.getById(clubId); - isClubAdmin(clubId, studentId); + isClubManager(clubId, studentId); club.updateIntroduction(request.introduction()); List clubSNSs = club.getClubSNSs(); @@ -107,7 +107,7 @@ public ClubResponse updateClubIntroduction( return ClubResponse.from(club, clubSNSs); } - private void isClubAdmin(Integer clubId, Integer studentId) { + private void isClubManager(Integer clubId, Integer studentId) { if (!clubAdminRepository.existsByClubIdAndUserId(clubId, studentId)) { throw AuthorizationException.withDetail("studentId: " + studentId); } @@ -193,16 +193,16 @@ private ClubHotResponse getHotClubFromDBAndCache() { public void createQna(CreateQnaRequest request, Integer clubId, Integer studentId) { Club club = clubRepository.getById(clubId); Student student = studentRepository.getById(studentId); - boolean isAdmin = clubAdminRepository.existsByClubIdAndUserId(clubId, studentId); + boolean isManager = clubAdminRepository.existsByClubIdAndUserId(clubId, studentId); boolean isQuestion = request.parentId() == null; - validateQnaCreateAuthorization(studentId, isQuestion, isAdmin); + validateQnaCreateAuthorization(studentId, isQuestion, isManager); ClubQna parentQna = request.parentId() == null ? null : clubQnaRepository.getById(request.parentId()); - ClubQna qna = request.toClubQna(club, student, parentQna, isAdmin); + ClubQna qna = request.toClubQna(club, student, parentQna, isManager); clubQnaRepository.save(qna); } - private static void validateQnaCreateAuthorization(Integer studentId, boolean isQuestion, boolean isAdmin) { - if (isQuestion == isAdmin) { + private static void validateQnaCreateAuthorization(Integer studentId, boolean isQuestion, boolean isManager) { + if (isQuestion == isManager) { throw AuthorizationException.withDetail("studentId: " + studentId); } } From aa8cf2082de6bb19c2b2296ac4fcd500e10a75bf Mon Sep 17 00:00:00 2001 From: krSeonghyeon Date: Sat, 17 May 2025 15:46:57 +0900 Subject: [PATCH 5/6] =?UTF-8?q?chore:=20swagger=20=EB=AA=85=EC=84=B8=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 --- .../koin/domain/club/controller/ClubApi.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/club/controller/ClubApi.java b/src/main/java/in/koreatech/koin/domain/club/controller/ClubApi.java index f687e7f65e..c64eb44d37 100644 --- a/src/main/java/in/koreatech/koin/domain/club/controller/ClubApi.java +++ b/src/main/java/in/koreatech/koin/domain/club/controller/ClubApi.java @@ -158,8 +158,6 @@ ResponseEntity likeClubCancel( description = """ - authorId 확인하여 작성자 본인인 경우 삭제 버튼(x) 표시. - 닉네임은 존재 시 그대로 반환되며, 없는 경우 student의 익명 닉네임으로 반환. - - is_deleted 값이 false인 경우 "삭제된 댓글입니다"로 표현. - - is_admin 필드를 통해 관리자 댓글 여부를 알 수 있음. - 트리 구조는 대댓글 형태로 재귀적으로 구성됩니다. ```java @@ -192,7 +190,11 @@ ResponseEntity getQnas( } ) @Operation(summary = "특정 동아리의 QNA를 생성한다", - description = "parentId를 null 요청 시 첫 QNA, 부모 QNA의 id를 넣어서 요청하면 대댓글 형식으로 생성") + description = """ + 사용자의 경우 질문만 가능, 관리자의 경우 답변만 가능 + parentId를 null 요청 시 질문, 질문 QNA의 id를 넣어서 요청하면 답변 형식으로 생성 + """ + ) @PostMapping("/{clubId}/qna") ResponseEntity createQna( @RequestBody @Valid CreateQnaRequest request, @@ -212,8 +214,8 @@ ResponseEntity createQna( @Operation(summary = "특정 동아리의 QNA를 삭제한다", description = """ - 관리자는 모든 QNA 삭제 가능, 그 외에는 본인의 QNA만 삭제 가능 - - 부모 QNA(맨처음 QNA)인 경우, 그 아래 QNA들까지 모두 삭제 - - 자식 QNA인 경우, 삭제 시 삭제된 댓글입니다로만 표시하고 구조를 깨지 않음 + - 부모 QNA(질문 QNA)인 경우, 자식 QNA까지 모두 삭제 + - 자식 QNA(답변 QNA)인 경우, 그거만 삭제 """) @DeleteMapping("/{clubId}/qna/{qnaId}") ResponseEntity deleteQna( From 571451b7529daf498384d442f493d36ff7234a71 Mon Sep 17 00:00:00 2001 From: krSeonghyeon Date: Sat, 17 May 2025 15:49:03 +0900 Subject: [PATCH 6/6] =?UTF-8?q?chore:=20swagger=20=EB=AA=85=EC=84=B8=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 --- .../in/koreatech/koin/domain/club/controller/ClubApi.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/club/controller/ClubApi.java b/src/main/java/in/koreatech/koin/domain/club/controller/ClubApi.java index c64eb44d37..f4023040b4 100644 --- a/src/main/java/in/koreatech/koin/domain/club/controller/ClubApi.java +++ b/src/main/java/in/koreatech/koin/domain/club/controller/ClubApi.java @@ -214,8 +214,8 @@ ResponseEntity createQna( @Operation(summary = "특정 동아리의 QNA를 삭제한다", description = """ - 관리자는 모든 QNA 삭제 가능, 그 외에는 본인의 QNA만 삭제 가능 - - 부모 QNA(질문 QNA)인 경우, 자식 QNA까지 모두 삭제 - - 자식 QNA(답변 QNA)인 경우, 그거만 삭제 + - 부모 QNA(질문 QNA)인 경우, 답변 QNA까지 모두 삭제 + - 자식 QNA(답변 QNA)인 경우, 답변 QNA만 삭제 """) @DeleteMapping("/{clubId}/qna/{qnaId}") ResponseEntity deleteQna(