-
Notifications
You must be signed in to change notification settings - Fork 0
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
base: develop
Are you sure you want to change the base?
[Feat] 댓글 기능 구현 #232
Changes from all commits
75c8ed0
5f5ccd5
0af35fa
7624eff
e85098f
56ee913
10679cd
c7a0d1f
04684db
e8c21aa
19ee378
1211cb5
c00c9b1
b66dea7
4f7872c
e9396b7
19b5840
dd4ce8f
a23c8af
d967b49
ee2eb0d
a9f6a0b
d2dc191
e86c5fc
929a1d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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); | ||
} | ||
} |
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 |
---|---|---|
@@ -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 |
---|---|---|
@@ -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; | ||
|
||
@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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. var가 자바에도 있다는건 알고 있었는데 실제로 보니 상당히 코틀린같으면서도 자바같은 요 느낌....ㅋㅋㅋㅎㅎㅋㅋㅎ 이걸 보니 값을 객체로 변환하는 과정에서만큼은 굳이 변수 선언부에 객체이름을 다 적지 않아도 충분하다는 것을 느끼게 되네유 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
하나의 CommandUseCase로 묶지 않고 Create, Update, Delete를 구분한 이유가 궁금함니닷
(가독성과 관리가 편해서인가 싶은 생각이 들었슴니당)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사실 CommandUseCase 는 너무 뭉둥그려진 유스케이스에 해당합니다. 거의 모든 유스케이스를 CommandUseCase 와 QueryUseCase로 이분법적으로 나눌 수 있을탠데... 이러면 인터페이스의 명세라는 의미가 사실 조금 사라진다 생각합니다.
가급적 유스케이스도 분리하여 의미를 전달해주는 것 이 좋아요, 다만 그럼 이전에는 왜? CommandUseCase 로 했냐? 라고 물어본다면, 그건 그당시 기존 레이어드 아키텍처에서 헥사고날로 전환하면서... 작업량이 많아... 일단 하나로 퉁쳤습니다...
언젠가 기존의 유스케이스도 명확하게 분리해야지라고 생각 중입나다~