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] 댓글 기능 구현 #232

Open
wants to merge 25 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
75c8ed0
feat: NoticeDto에서 공지의 uniqueId도 함께 반환하도록 변경
zbqmgldjfh Feb 18, 2025
5f5ccd5
test(NoticeAcceptanceTest): 공지에 댓글을 추가하는 인수테스트 작성
zbqmgldjfh Feb 18, 2025
0af35fa
feat: Comment Create DDL문 작성
zbqmgldjfh Feb 18, 2025
7624eff
feat(NoticeCommandService): 공지에 댓글 추가 유스케이스 작성
zbqmgldjfh Feb 18, 2025
e85098f
refactor(Comment): Comment를 공용 페키지로 이동
zbqmgldjfh Feb 18, 2025
56ee913
test: 공지 조회시 댓글 수 까지 함께 조회하는지 확인하는 인수테스트 작성
zbqmgldjfh Feb 18, 2025
10679cd
feat(NoticeQueryService): 공지 조회시 댓글 수도 함께 반환하도록 구현
zbqmgldjfh Feb 18, 2025
c7a0d1f
feat(Comment): 대댓글과 삭제를 위한 Comment 필드 추가
zbqmgldjfh Feb 20, 2025
04684db
feat(CursorBasedList): CursorBasedList 구현
zbqmgldjfh Feb 20, 2025
e8c21aa
test: 대댓글을 추가하는 인수테스트 작성
zbqmgldjfh Feb 20, 2025
19ee378
feat(CursorBasedList): 사용하지 않는 메서드 제거
zbqmgldjfh Feb 20, 2025
1211cb5
feat(NoticeCommentReadingUseCase): 대댓글까지 함게 조회하는 로직 구현
zbqmgldjfh Feb 20, 2025
c00c9b1
test: 공지 수정 인수테스트 작성
zbqmgldjfh Feb 20, 2025
b66dea7
feat(NoticeCommentEditingUseCase): NoticeCommentEditingUseCase 구현
zbqmgldjfh Feb 20, 2025
4f7872c
feat(NoticeCommandApiV2): 응답 타입 수정
zbqmgldjfh Feb 20, 2025
e9396b7
refactor(NoticeQueryApiV2): 커서가 1부터 시작해도 0번부터 조회하도록 변경
zbqmgldjfh Feb 20, 2025
19b5840
refactor: 메서드 이름 오타 수정
zbqmgldjfh Feb 21, 2025
dd4ce8f
refactor(NoticeCommentReadingUseCase): 파라미터 설명 추가
zbqmgldjfh Feb 21, 2025
a23c8af
refactor(Comment): 삭제 SQLDelete 에너테이션 추가
zbqmgldjfh Feb 21, 2025
d967b49
test: 댓글 삭제 인수테스트 작성
zbqmgldjfh Feb 21, 2025
ee2eb0d
feat: 댓글 삭제 기능 추가
zbqmgldjfh Feb 21, 2025
a9f6a0b
refactor(NoticeQueryService): findComments 메서드에서 자식댓글 조회시 공지id도 같이 사용…
zbqmgldjfh Feb 21, 2025
d2dc191
refactor(NoticeQueryApiV2): Cursor 객체를 사용하도록 변경
zbqmgldjfh Feb 22, 2025
e86c5fc
refactor(NoticeCommandService): 자신의 댓글이 아닌 경우에 대한 예외 추가
zbqmgldjfh Feb 22, 2025
929a1d2
refactor(NoticeCommandService): 로그 추가
zbqmgldjfh Feb 22, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,12 @@ public ResponseEntity<BaseResponse<String>> createAlert(
@Secured(AdminRole.ROLE_ROOT)
@ResponseStatus(HttpStatus.NO_CONTENT)
@DeleteMapping("/alerts/{id}")
public void cancelAlert(
public ResponseEntity cancelAlert(
@Parameter(description = "알림 아이디") @NotNull @PathVariable("id") Long id
) {
adminCommandUseCase.cancelAlertSchedule(id);

return ResponseEntity.noContent().build();
}

@Operation(summary = "파일 임베딩", description = "어드민이 원하는 파일을 임베딩 하여 쿠링봇에서 사용할 수 있다")
Expand Down
29 changes: 29 additions & 0 deletions src/main/java/com/kustacks/kuring/common/data/Cursor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.kustacks.kuring.common.data;

public class Cursor {

private static final String AUTO_ADJUST_DEFAULT_CURSOR = "1";
private static final String DEFAULT_CURSOR = "0";

private final String stringCursor;

private Cursor(String stringCursor) {
this.stringCursor = stringCursor;
}

public String getStringCursor() {
return stringCursor;
}

public static Cursor from(String cursor) {
if (cursor != null && cursor.equals(AUTO_ADJUST_DEFAULT_CURSOR)) {
return new Cursor(DEFAULT_CURSOR);
}

if (cursor == null) {
return new Cursor(DEFAULT_CURSOR);
}

return new Cursor(cursor);
}
}
169 changes: 169 additions & 0 deletions src/main/java/com/kustacks/kuring/common/data/CursorBasedList.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package com.kustacks.kuring.common.data;

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.function.Function;

public class CursorBasedList<T> implements List<T> {

private final List<T> contents;
private final String endCursor;
private final boolean hasNext;

private static final int NEXT_CURSOR_SIZE = 1;

public CursorBasedList(List<T> contents, String endCursor, boolean hasNext) {
this.contents = contents;
this.endCursor = endCursor;
this.hasNext = hasNext;
}

public List<T> getContents() {
return contents;
}

public String getEndCursor() {
return endCursor;
}

public boolean hasNext() {
return hasNext;
}

@Override
public int size() {
return contents.size();
}

@Override
public boolean isEmpty() {
return contents.isEmpty();
}

@Override
public boolean contains(Object o) {
return contents.contains(o);
}

@Override
public Iterator<T> iterator() {
return contents.listIterator();
}

@Override
public Object[] toArray() {
return contents.toArray();
}

@Override
public <T1> T1[] toArray(T1[] a) {
return contents.toArray(a);
}

@Override
public boolean add(T t) {
return contents.add(t);
}

@Override
public boolean remove(Object o) {
return contents.remove(o);
}

@Override
public boolean containsAll(Collection<?> c) {
return false;
}

@Override
public boolean addAll(Collection<? extends T> c) {
return false;
}

@Override
public boolean addAll(int index, Collection<? extends T> c) {
return false;
}

@Override
public boolean removeAll(Collection<?> c) {
return false;
}

@Override
public boolean retainAll(Collection<?> c) {
return false;
}

@Override
public void clear() {
contents.clear();
}

@Override
public T get(int index) {
return contents.get(index);
}

@Override
public T set(int index, T element) {
return contents.set(index, element);
}

@Override
public void add(int index, T element) {
contents.add(index, element);
}

@Override
public T remove(int index) {
return contents.remove(index);
}

@Override
public int indexOf(Object o) {
return contents.indexOf(o);
}

@Override
public int lastIndexOf(Object o) {
return contents.lastIndexOf(o);
}

@Override
public ListIterator<T> listIterator() {
return contents.listIterator();
}

@Override
public ListIterator<T> listIterator(int index) {
return contents.listIterator(index);
}

@Override
public List<T> subList(int fromIndex, int toIndex) {
return contents.subList(fromIndex, toIndex);
}

public static <T> CursorBasedList<T> empty() {
return new CursorBasedList<>(List.of(), null, false);
}

public static <T> CursorBasedList<T> of(
int limit,
CursorGenerator<T> cursorGenerator,
Function<Integer, List<T>> sourceContentsLoader
) {
List<T> sourceContents = sourceContentsLoader.apply(limit + NEXT_CURSOR_SIZE);

int subListSize = Math.min(limit, sourceContents.size());
List<T> contents = sourceContents.subList(0, subListSize);

boolean hasNext = limit < sourceContents.size();
String endCursor = hasNext ? cursorGenerator.generate(contents.get(limit - 1)) : null;

return new CursorBasedList<>(contents, endCursor, hasNext);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.kustacks.kuring.common.data;

@FunctionalInterface
public interface CursorGenerator<T> {
String generate(T content);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package com.kustacks.kuring.user.domain;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
package com.kustacks.kuring.common.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public enum ResponseCodeAndMessages {
FEEDBACK_SAVE_SUCCESS(HttpStatus.OK.value(), "피드백 저장에 성공하였습니다"),
FEEDBACK_SEARCH_SUCCESS(HttpStatus.OK.value(), "피드백 조회에 성공하였습니다"),
ASK_COUNT_LOOKUP_SUCCESS(HttpStatus.OK.value(), "질문 가능 횟수 조회에 성공하였습니다"),
NOTICE_COMMENT_SAVE_SUCCESS(HttpStatus.OK.value(), "공지에 댓글 추가를 성공하였습니다"),
NOTICE_COMMENT_EDIT_SUCCESS(HttpStatus.OK.value(), "공지에 댓글 편집을 성공하였습니다"),

/* Alert */
ALERT_SEARCH_SUCCESS(HttpStatus.OK.value(), "예약 알림 조회에 성공하였습니다"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.kustacks.kuring.common.exception;

import com.kustacks.kuring.common.exception.code.ErrorCode;

public class NoPermissionException extends BusinessException {

public NoPermissionException(ErrorCode errorCode) {
super(errorCode);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ public enum ErrorCode {

NOTICE_SCRAPER_CANNOT_SCRAP("학과 홈페이지가 불안정합니다. 공지 정보를 가져올 수 없습니다."),
NOTICE_SCRAPER_CANNOT_PARSE("공지 페이지 HTML 파싱에 실패했습니다."),
NOTICE_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 공지를 찾을 수 없습니다."),


FB_FAIL_SUBSCRIBE(HttpStatus.INTERNAL_SERVER_ERROR, "카테고리 구독에 실패했습니다."),
FB_FAIL_UNSUBSCRIBE(HttpStatus.INTERNAL_SERVER_ERROR, "카테고리 구독 해제에 실패했습니다."),
Expand All @@ -75,6 +77,7 @@ public enum ErrorCode {
AD_UNAUTHENTICATED("관리자가 아닙니다."),

USER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 사용자를 찾을 수 없습니다."),
COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 댓글을 찾을 수 없습니다."),

EMAIL_NO_SUCH_ALGORITHM(HttpStatus.INTERNAL_SERVER_ERROR, "랜덤 숫자 생성 간 알고리즘을 찾을 수 없습니다."),
EMAIL_INVALID_SUFFIX(HttpStatus.BAD_REQUEST, "건국대학교 이메일 도메인이 아닙니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.kustacks.kuring.notice.adapter.in.web;

import com.kustacks.kuring.common.annotation.RestWebAdapter;
import com.kustacks.kuring.common.dto.BaseResponse;
import com.kustacks.kuring.notice.adapter.in.web.dto.NoticeCommentCreateRequest;
import com.kustacks.kuring.notice.adapter.in.web.dto.NoticeCommentEditRequest;
import com.kustacks.kuring.notice.application.port.in.NoticeCommentDeletingUseCase;
import com.kustacks.kuring.notice.application.port.in.NoticeCommentDeletingUseCase.DeleteCommentCommand;
import com.kustacks.kuring.notice.application.port.in.NoticeCommentEditingUseCase;
import com.kustacks.kuring.notice.application.port.in.NoticeCommentEditingUseCase.EditCommentCommand;
import com.kustacks.kuring.notice.application.port.in.NoticeCommentWritingUseCase;
import com.kustacks.kuring.notice.application.port.in.NoticeCommentWritingUseCase.WriteCommentCommand;
import com.kustacks.kuring.notice.application.port.in.NoticeCommentWritingUseCase.WriteReplyCommand;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.NOTICE_COMMENT_EDIT_SUCCESS;
import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.NOTICE_COMMENT_SAVE_SUCCESS;

@Tag(name = "Notice-Command", description = "공지 가공")
@Validated
@RequiredArgsConstructor
@RestWebAdapter(path = "/api/v2/notices")
public class NoticeCommandApiV2 {

private static final String USER_TOKEN_HEADER_KEY = "User-Token";

private final NoticeCommentWritingUseCase noticeCommentWritingUseCase;
private final NoticeCommentEditingUseCase noticeCommentEditingUseCase;
private final NoticeCommentDeletingUseCase noticeCommentDeletingUseCase;
Comment on lines +34 to +36
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

하나의 CommandUseCase로 묶지 않고 Create, Update, Delete를 구분한 이유가 궁금함니닷
(가독성과 관리가 편해서인가 싶은 생각이 들었슴니당)

Copy link
Member Author

@zbqmgldjfh zbqmgldjfh Feb 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사실 CommandUseCase 는 너무 뭉둥그려진 유스케이스에 해당합니다. 거의 모든 유스케이스를 CommandUseCase 와 QueryUseCase로 이분법적으로 나눌 수 있을탠데... 이러면 인터페이스의 명세라는 의미가 사실 조금 사라진다 생각합니다.

가급적 유스케이스도 분리하여 의미를 전달해주는 것 이 좋아요, 다만 그럼 이전에는 왜? CommandUseCase 로 했냐? 라고 물어본다면, 그건 그당시 기존 레이어드 아키텍처에서 헥사고날로 전환하면서... 작업량이 많아... 일단 하나로 퉁쳤습니다...

언젠가 기존의 유스케이스도 명확하게 분리해야지라고 생각 중입나다~


@Operation(summary = "공지 댓글 추가", description = "공지에 댓글을 추가합니다")
@SecurityRequirement(name = USER_TOKEN_HEADER_KEY)
@PostMapping("/{id}/comments")
public ResponseEntity<BaseResponse> createComment(
@Parameter(description = "공지 ID") @PathVariable("id") Long id,
@RequestHeader(USER_TOKEN_HEADER_KEY) String userToken,
@RequestBody NoticeCommentCreateRequest request
) {
if (request.parentId() == null) {
var command = new WriteCommentCommand(
userToken,
id,
request.content()
);

noticeCommentWritingUseCase.process(command);
Comment on lines +41 to +53
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var가 자바에도 있다는건 알고 있었는데 실제로 보니 상당히 코틀린같으면서도 자바같은 요 느낌....ㅋㅋㅋㅎㅎㅋㅋㅎ

이걸 보니 값을 객체로 변환하는 과정에서만큼은 굳이 변수 선언부에 객체이름을 다 적지 않아도 충분하다는 것을 느끼게 되네유

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅋㅋㅋ 사실 맘같아서는 코틀린 쓰고 싶은데... 그다음 서버 학생분 물려드릴거 생각하여... 자바로 유지중...

} else {
var command = new WriteReplyCommand(
userToken,
id,
request.content(),
request.parentId()
);

noticeCommentWritingUseCase.process(command);
}

return ResponseEntity.ok().body(new BaseResponse<>(NOTICE_COMMENT_SAVE_SUCCESS, null));
}

@Operation(summary = "공지 댓글 편집", description = "공지에 있는 기존의 댓글을 수정합니다")
@SecurityRequirement(name = USER_TOKEN_HEADER_KEY)
@PostMapping("/{id}/comments/{commentId}")
public ResponseEntity<BaseResponse> editComment(
@Parameter(description = "공지 ID") @PathVariable("id") Long id,
@Parameter(description = "댓글 ID") @PathVariable("commentId") Long commentId,
@RequestHeader(USER_TOKEN_HEADER_KEY) String userToken,
@RequestBody NoticeCommentEditRequest request
) {
var command = new EditCommentCommand(userToken, id, commentId, request.content());

noticeCommentEditingUseCase.process(command);

return ResponseEntity.ok().body(new BaseResponse<>(NOTICE_COMMENT_EDIT_SUCCESS, null));
}

@Operation(summary = "공지 댓글 삭제", description = "공지에 있는 기존의 댓글을 삭제합니다")
@SecurityRequirement(name = USER_TOKEN_HEADER_KEY)
@DeleteMapping("/{id}/comments/{commentId}")
public ResponseEntity deleteComment(
@Parameter(description = "공지 ID") @PathVariable("id") Long id,
@Parameter(description = "댓글 ID") @PathVariable("commentId") Long commentId,
@RequestHeader(USER_TOKEN_HEADER_KEY) String userToken
) {
var command = new DeleteCommentCommand(userToken, id, commentId);

noticeCommentDeletingUseCase.process(command);

return ResponseEntity.noContent().build();
}
}
Loading
Loading