Skip to content

Commit 6bf41e9

Browse files
authored
답변 생성 수정 및 테스트 코드 작성 (#74)
1 parent 7e0a491 commit 6bf41e9

File tree

10 files changed

+92
-102
lines changed

10 files changed

+92
-102
lines changed

src/main/java/com/oronaminc/join/answer/dto/AnswerCreateResponse.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,13 @@
1010
@Builder
1111
@Schema(description = "WebSocket STOMP 통신 답변 응답 DTO")
1212
public record AnswerCreateResponse(
13-
//TODO: QuestionCreateResponse와 유사-> 둘중 하나만?
1413
@Schema(description = "답변이 생성될 질문 ID")
1514
Long questionId,
1615
@Schema(description = "답변 생성/삭제/수정 상태", example = "CREATE")
1716
String event,
1817
@Schema(description = "답변 ID", example = "11")
1918
Long answerId,
2019
@Schema(description = "답변 내용", example = "답변입니다.")
21-
@NotBlank(message = "답변 내용을 입력해주시기 바랍니다.")
22-
@Size(max = 300, message = "답변 내용은 최대 300자까지 입력할 수 있습니다.")
2320
String content,
2421
@Schema(description = "답변 내용에 대한 공감 수", example = "23")
2522
int emojiCount,

src/main/java/com/oronaminc/join/answer/dto/AnswerRequest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
@Schema(description = "답변 생성/수정 요청 DTO")
88
public record AnswerRequest(
9-
//TODO: 빈값 or " " (space) 처리
109
@NotBlank(message = "답변 내용을 입력해주시기 바랍니다.")
1110
@Size(max = 300, message = "답변 내용은 최대 300자까지 입력할 수 있습니다.")
1211
@Schema(description = "답변 내용", example = "답변입니다.")

src/main/java/com/oronaminc/join/answer/service/AnswerService.java

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
package com.oronaminc.join.answer.service;
22

3-
import static com.oronaminc.join.global.exception.ErrorCode.BADREQUEST_DUPLICATION_ANSWER;
43

54
import com.oronaminc.join.answer.dao.AnswerRepository;
65
import com.oronaminc.join.answer.domain.Answer;
7-
import com.oronaminc.join.answer.dto.AnswerRequest;
86
import com.oronaminc.join.answer.dto.AnswerGetResponse;
7+
import com.oronaminc.join.answer.dto.AnswerRequest;
98
import com.oronaminc.join.answer.mapper.AnswerMapper;
9+
import com.oronaminc.join.answer.util.PermissionValidator;
1010
import com.oronaminc.join.emoji.domain.TargetType;
1111
import com.oronaminc.join.emoji.service.EmojiReader;
12-
import com.oronaminc.join.global.exception.ErrorException;
1312
import com.oronaminc.join.member.domain.Member;
1413
import com.oronaminc.join.member.service.MemberReader;
1514
import com.oronaminc.join.participant.service.ParticipantService;
@@ -28,32 +27,25 @@
2827
public class AnswerService {
2928

3029
private final AnswerRepository answerRepository;
31-
private final ParticipantService participantService;
3230
private final QuestionReader questionReader;
3331
private final MemberReader memberReader;
3432
private final AnswerReader answerReader;
3533
private final RoomReader roomReader;
3634
private final EmojiReader emojiReader;
35+
private final PermissionValidator permissionValidator;
3736

3837
@Transactional
3938
public Answer create(Long roomId, Long memberId, Long questionId,
4039
AnswerRequest request) {
4140

4241
Member member = memberReader.getById(memberId);
4342
Room room = roomReader.getById(roomId);
44-
Question question = questionReader.getByIdAndRoomId(questionId, roomId);
45-
46-
participantService.validateParticipant(member.getId(), room.getId());
47-
48-
if (answerReader.existsByQuestionIdAndMemberId(question.getId(), member.getId())) {
49-
throw new ErrorException(BADREQUEST_DUPLICATION_ANSWER);
50-
}
51-
43+
Question question = questionReader.getByIdAndRoomId(questionId, room.getId());
44+
permissionValidator.validateAnswerCreatePermission(room.getId(), member.getId(), question);
5245
Answer answer = AnswerMapper.toEntity(question, member, request);
5346

54-
answerRepository.save(answer);
47+
return answerRepository.save(answer);
5548

56-
return answer;
5749
}
5850

5951
@Transactional
@@ -71,17 +63,17 @@ public AnswerGetResponse getAnswer(Long roomId, Long questionId, Long memberId)
7163
}
7264

7365
@Transactional
74-
public Answer update(Long answerId, AnswerRequest request) {
75-
Answer answer = answerReader.getById(answerId);
66+
public Answer update(Long answerId, Long memberId, AnswerRequest request) {
67+
Answer answer = permissionValidator.validateAnswerUpdatePermission(answerId, memberId);
7668

7769
answer.updataContent(request.content());
7870

7971
return answer;
8072
}
8173

8274
@Transactional
83-
public void delete(Long answerId) {
84-
Answer answer = answerReader.getById(answerId);
75+
public void delete(Long answerId, Long memberId) {
76+
Answer answer = permissionValidator.validateAnswerDeletePermission(answerId, memberId);
8577
answerRepository.delete(answer);
8678
}
8779

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.oronaminc.join.answer.util;
2+
3+
import com.oronaminc.join.global.exception.ErrorCode;
4+
import lombok.Getter;
5+
6+
7+
@Getter
8+
public enum PermissionType {
9+
CREATE, DELETE;
10+
11+
public ErrorCode toErrorCode() {
12+
return switch (this) {
13+
case CREATE -> ErrorCode.UNAUTHORIZED_ROLE_ANSWER;
14+
case DELETE -> ErrorCode.UNAUTHORIZED_DELETE_ANSWER;
15+
};
16+
}
17+
18+
}
Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package com.oronaminc.join.answer.util;
22

3-
import static com.oronaminc.join.global.exception.ErrorCode.UNAUTHORIZED_DELETE_ANSWER;
43
import static com.oronaminc.join.global.exception.ErrorCode.UNAUTHORIZED_EDIT_ANSWER;
5-
import static com.oronaminc.join.global.exception.ErrorCode.UNAUTHORIZED_ROLE_ANSWER;
64

75
import com.oronaminc.join.answer.domain.Answer;
86
import com.oronaminc.join.answer.service.AnswerReader;
@@ -22,40 +20,40 @@ public class PermissionValidator {
2220
private final ParticipantReader participantReader;
2321
private final AnswerReader answerReader;
2422

25-
public void validateAnswerPermission(Long roomId, Long memberId) {
26-
// TODO: 생성시 조건 ( null 이면 안됨, " " 안됨, 팀원이나 발표자, 질문 작성자 본인만 생성가능 )
27-
Participant participant = participantReader.getByRoomIdAndMemberId(roomId, memberId);
28-
ParticipantType type = participant.getParticipantType();
29-
30-
if (type == ParticipantType.GUEST) {
31-
throw new ErrorException(UNAUTHORIZED_ROLE_ANSWER);
32-
}
23+
public void validateAnswerCreatePermission(Long roomId, Long memberId, Question question) {
24+
validatePermission(roomId, memberId, question, PermissionType.CREATE);
3325
}
3426

35-
public void validateAnswerUpdatePermission(Long answerId, Long memberId) {
27+
public Answer validateAnswerUpdatePermission(Long answerId, Long memberId) {
3628
Answer answer = answerReader.getById(answerId);
3729

3830
if (!answer.getMember().getId().equals(memberId)) {
3931
throw new ErrorException(UNAUTHORIZED_EDIT_ANSWER);
4032
}
33+
34+
return answer;
4135
}
4236

43-
public void validateAnswerDeletePermission(Long answerId, Long memberId) {
37+
public Answer validateAnswerDeletePermission(Long answerId, Long memberId) {
4438
Answer answer = answerReader.getById(answerId);
45-
4639
Room room = answer.getQuestion().getRoom();
4740
Question question = answer.getQuestion();
4841

49-
Participant participant = participantReader.getByRoomIdAndMemberId(room.getId(), memberId);
50-
ParticipantType type = participant.getParticipantType();
42+
validatePermission(room.getId(), memberId, question, PermissionType.DELETE);
43+
44+
return answer;
45+
}
5146

47+
private void validatePermission(Long roomId, Long memberId, Question question,
48+
PermissionType permissionType) {
49+
Participant participant = participantReader.getByRoomIdAndMemberId(roomId, memberId);
50+
ParticipantType type = participant.getParticipantType();
5251
boolean isQuestionWriter = question.getMember().getId().equals(memberId);
5352

5453
if (!(isQuestionWriter || type == ParticipantType.TEAM
5554
|| type == ParticipantType.PRESENTER)) {
56-
throw new ErrorException(UNAUTHORIZED_DELETE_ANSWER);
55+
throw new ErrorException(permissionType.toErrorCode());
5756
}
58-
5957
}
6058

6159
}

src/main/java/com/oronaminc/join/global/exception/ErrorCode.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,12 @@ public enum ErrorCode {
4444
UNAUTHORIZED_DELETE_QUESTION("QUESTION-004", "작성자 및 관리자만 질문을 삭제할 수 있습니다.", UNAUTHORIZED),
4545
TOO_MANY_REQUESTS_QUESTION("QUESTION-005", "잠시 후 다시 시도해주세요.", TOO_MANY_REQUESTS),
4646

47-
UNAUTHORIZED_ROLE_ANSWER("ANSWER-001", "팀원 또는 발표자만 댓글을 작성할 수 있습니다.", UNAUTHORIZED),
47+
UNAUTHORIZED_ROLE_ANSWER("ANSWER-001", "질문 작성자 또는 팀원과 발표자만 댓글을 작성할 수 있습니다.", UNAUTHORIZED),
4848
NOT_FOUND_EXIST_ANSWER("ANSWER-002", "해당 질문에 대한 답변이 존재하지 않습니다.", NOT_FOUND),
4949
NOT_FOUND_ANSWER("ANSWER-003", "답변이 존재하지 않습니다.", NOT_FOUND),
50-
BADREQUEST_DUPLICATION_ANSWER("ANSWER-004", "이미 답변한 질문입니다.", BAD_REQUEST),
51-
UNAUTHORIZED_EDIT_ANSWER("ANSWER-005", "작성자가 아니면 해당 댓글을 수정할 수 없습니다.", UNAUTHORIZED),
52-
UNAUTHORIZED_DELETE_ANSWER("ANSWER-006", "작성자 혹은 팀원, 발표자가 아니면 해당 댓글을 삭제할 수 없습니다.", UNAUTHORIZED),
50+
UNAUTHORIZED_EDIT_ANSWER("ANSWER-004", "작성자가 아니면 해당 댓글을 수정할 수 없습니다.", UNAUTHORIZED),
51+
UNAUTHORIZED_DELETE_ANSWER("ANSWER-005", "작성자 혹은 팀원, 발표자가 아니면 해당 댓글을 삭제할 수 없습니다.", UNAUTHORIZED),
52+
TOO_MANY_REQUESTS_ANSWER("ANSWER-006", "잠시 후 다시 시도해주세요.", UNAUTHORIZED),
5353

5454

5555
ACCESS_DENIED_SESSION("SESSION-1201", "접근 권한이 없습니다.", FORBIDDEN),

src/main/java/com/oronaminc/join/global/ratelimit/RateLimitType.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ public enum RateLimitType {
1919
.capacity(3)
2020
.refillIntervally(3, Duration.ofSeconds(1))
2121
.build()
22+
),
23+
24+
CREATE_ANSWER(
25+
"CREATE_ANSWER:{}:{}:{}",
26+
Bandwidth.builder()
27+
.capacity(5)
28+
.refillIntervally(5, Duration.ofSeconds(10))
29+
.build()
2230
);
2331

2432
private final String format;

src/main/java/com/oronaminc/join/websocket/api/AnswerWebsocketController.java

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.oronaminc.join.websocket.api;
22

3+
import static com.oronaminc.join.global.exception.ErrorCode.TOO_MANY_REQUESTS_ANSWER;
34
import static com.oronaminc.join.global.exception.ErrorCode.UNAUTHORIZED_MEMBER;
45

56
import com.oronaminc.join.answer.domain.Answer;
@@ -9,8 +10,10 @@
910
import com.oronaminc.join.answer.dto.AnswerUpdateResponse;
1011
import com.oronaminc.join.answer.mapper.AnswerMapper;
1112
import com.oronaminc.join.answer.service.AnswerService;
12-
import com.oronaminc.join.answer.util.PermissionValidator;
1313
import com.oronaminc.join.global.exception.ErrorException;
14+
import com.oronaminc.join.global.ratelimit.RateLimitService;
15+
import com.oronaminc.join.global.ratelimit.RateLimitType;
16+
import io.github.bucket4j.Bucket;
1417
import jakarta.validation.Valid;
1518
import java.security.Principal;
1619
import lombok.RequiredArgsConstructor;
@@ -27,7 +30,7 @@
2730
public class AnswerWebsocketController {
2831

2932
private final AnswerService answerService;
30-
private final PermissionValidator permissionValidator;
33+
private final RateLimitService rateLimitService;
3134

3235
@MessageMapping("/rooms/{roomId}/question/{questionId}/answers/create")
3336
@SendTo("/topic/rooms/{roomId}/answers")
@@ -39,11 +42,15 @@ public AnswerCreateResponse create(
3942
) {
4043
Long memberId = getMemberId(principal);
4144

42-
permissionValidator.validateAnswerPermission(roomId, memberId);
45+
Bucket bucket = rateLimitService.getBucket(RateLimitType.CREATE_ANSWER, roomId, memberId, questionId);
46+
47+
if (!bucket.tryConsume(1)) {
48+
throw new ErrorException(TOO_MANY_REQUESTS_ANSWER);
49+
}
4350

4451
Answer answer = answerService.create(roomId, memberId, questionId, request);
4552

46-
log.info("답변 메세지 = {}", request.content());
53+
log.info("답변 메세지 = {}", answer.getContent());
4754

4855
return AnswerMapper.toAnswerCreateResponse(answer);
4956
}
@@ -58,26 +65,24 @@ public AnswerUpdateResponse update(
5865

5966
Long memberId = getMemberId(principal);
6067

61-
permissionValidator.validateAnswerUpdatePermission(answerId, memberId);
68+
Answer answer = answerService.update(answerId, memberId, request);
6269

63-
Answer answer = answerService.update(answerId, request);
70+
log.info("수정 메세지 = {}", answer.getContent());
6471

6572
return AnswerMapper.toAnswerUpdateResponse(answer);
6673
}
6774

6875
@MessageMapping("/answers/{answerId}/delete")
6976
@SendTo("/topic/rooms/{roomId}/answers")
7077
public AnswerDeleteResponse delete(
71-
@DestinationVariable Long roomId,
72-
@DestinationVariable Long questionId,
7378
@DestinationVariable Long answerId,
7479
Principal principal
7580
) {
7681
Long memberId = getMemberId(principal);
7782

78-
permissionValidator.validateAnswerDeletePermission(answerId, memberId);
83+
answerService.delete(answerId, memberId);
7984

80-
answerService.delete(answerId);
85+
log.info("삭제되었습니다.");
8186

8287
return new AnswerDeleteResponse(answerId, "DELETE");
8388
}

src/test/java/com/oronaminc/join/answer/api/PermissionValidTests.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,13 @@ void setUp() {
6565
}
6666

6767
@Test
68-
@DisplayName("TEAM or PRESENTER가 아닌 GUEST가 답변시 예외 발생")
69-
void validateAnswerPermission_fail_not_team_or_presenter() {
68+
@DisplayName("TEAM or PRESENTER or 작성자가 아닌 참여자가 답변시 예외 발생")
69+
void validateAnswerPermission_fail_not_team_or_presenter_orWriter() {
7070
// given
7171
given(participantReader.getByRoomIdAndMemberId(1L, 1L)).willReturn(participant);
7272

7373
// when & then
74-
assertThatThrownBy(() -> permissionValidator.validateAnswerPermission(1L, 1L))
74+
assertThatThrownBy(() -> permissionValidator.validateAnswerCreatePermission(1L, 1L, question))
7575
.isInstanceOf(ErrorException.class)
7676
.hasFieldOrPropertyWithValue("errorCode", ErrorCode.UNAUTHORIZED_ROLE_ANSWER);
7777
}
@@ -84,7 +84,7 @@ void validateAnswerPermission_fail_not_found_participant() {
8484
.willThrow(new ErrorException(ErrorCode.NOT_FOUND_PARTICIPANT));
8585

8686
// when & then
87-
assertThatThrownBy(() -> permissionValidator.validateAnswerPermission(1L, 1L))
87+
assertThatThrownBy(() -> permissionValidator.validateAnswerCreatePermission(1L, 1L, question))
8888
.isInstanceOf(ErrorException.class)
8989
.hasFieldOrPropertyWithValue("errorCode", ErrorCode.NOT_FOUND_PARTICIPANT);
9090
}

0 commit comments

Comments
 (0)