diff --git a/backend/src/main/java/corea/auth/infrastructure/GithubOAuthClient.java b/backend/src/main/java/corea/auth/infrastructure/GithubOAuthClient.java index 66b2483f2..3d467d821 100644 --- a/backend/src/main/java/corea/auth/infrastructure/GithubOAuthClient.java +++ b/backend/src/main/java/corea/auth/infrastructure/GithubOAuthClient.java @@ -2,7 +2,6 @@ import corea.auth.dto.GithubAuthRequest; import corea.auth.dto.GithubAuthResponse; -import corea.auth.dto.GithubPullRequestReview; import corea.auth.dto.GithubUserInfo; import corea.exception.CoreaException; import lombok.RequiredArgsConstructor; @@ -59,13 +58,4 @@ public GithubUserInfo getUserInfo(String accessToken) { .retrieve() .body(GithubUserInfo.class); } - - public GithubPullRequestReview[] getReviewLink(String prLink) { - String url = GithubPullRequestUrlExchanger.pullRequestUrlToReview(prLink); - return restClient.get() - .uri(url) - .accept(APPLICATION_JSON) - .retrieve() - .body(GithubPullRequestReview[].class); - } } diff --git a/backend/src/main/java/corea/auth/service/GithubOAuthProvider.java b/backend/src/main/java/corea/auth/service/GithubOAuthProvider.java index cfb250561..85d7bce9d 100644 --- a/backend/src/main/java/corea/auth/service/GithubOAuthProvider.java +++ b/backend/src/main/java/corea/auth/service/GithubOAuthProvider.java @@ -1,6 +1,5 @@ package corea.auth.service; -import corea.auth.dto.GithubPullRequestReview; import corea.auth.dto.GithubUserInfo; import corea.auth.infrastructure.GithubOAuthClient; import lombok.RequiredArgsConstructor; @@ -16,8 +15,4 @@ public GithubUserInfo getUserInfo(String code) { String accessToken = githubOAuthClient.getAccessToken(code); return githubOAuthClient.getUserInfo(accessToken); } - - public GithubPullRequestReview[] getPullRequestReview(String prLink) { - return githubOAuthClient.getReviewLink(prLink); - } } diff --git a/backend/src/main/java/corea/exception/ExceptionType.java b/backend/src/main/java/corea/exception/ExceptionType.java index 2131ded57..4892f9336 100644 --- a/backend/src/main/java/corea/exception/ExceptionType.java +++ b/backend/src/main/java/corea/exception/ExceptionType.java @@ -26,7 +26,7 @@ public enum ExceptionType { TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "토큰이 만료되었습니다."), INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "토큰이 올바르지 않습니다."), AUTHORIZATION_ERROR(HttpStatus.UNAUTHORIZED, "인증에 실패했습니다."), - ROOM_DELETION_AUTHORIZATION_ERROR(HttpStatus.UNAUTHORIZED, "방 삭제 권한이 없습니다. 방 생성자만 방을 삭제할 수 있습니다."), + ROOM_MODIFY_AUTHORIZATION_ERROR(HttpStatus.UNAUTHORIZED, "방 정보 변경 권한이 없습니다. 방 생성자만 방 정보를 변경할 수 있습니다."), FEEDBACK_UPDATE_AUTHORIZATION_ERROR(HttpStatus.UNAUTHORIZED, "피드백 수정 권한이 없습니다. 피드백 작성자만 피드백을 수정할 수 있습니다."), MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 멤버를 찾을 수 없습니다."), diff --git a/backend/src/main/java/corea/feedback/controller/SocialFeedbackController.java b/backend/src/main/java/corea/feedback/controller/SocialFeedbackController.java index 2acab4096..62d2bcf4d 100644 --- a/backend/src/main/java/corea/feedback/controller/SocialFeedbackController.java +++ b/backend/src/main/java/corea/feedback/controller/SocialFeedbackController.java @@ -2,8 +2,9 @@ import corea.auth.annotation.LoginMember; import corea.auth.domain.AuthInfo; -import corea.feedback.dto.SocialFeedbackRequest; +import corea.feedback.dto.SocialFeedbackCreateRequest; import corea.feedback.dto.SocialFeedbackResponse; +import corea.feedback.dto.SocialFeedbackUpdateRequest; import corea.feedback.service.SocialFeedbackService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -16,25 +17,29 @@ public class SocialFeedbackController implements SocialFeedbackControllerSpecifi private final SocialFeedbackService socialFeedbackService; - @Override @PostMapping - public ResponseEntity create(@PathVariable long roomId, @LoginMember AuthInfo authInfo, @RequestBody SocialFeedbackRequest request) { + public ResponseEntity create(@PathVariable long roomId, + @LoginMember AuthInfo authInfo, + @RequestBody SocialFeedbackCreateRequest request) { socialFeedbackService.create(roomId, authInfo.getId(), request); return ResponseEntity.ok() .build(); } - @Override @GetMapping - public ResponseEntity socialFeedback(@PathVariable long roomId, @RequestParam String username, @LoginMember AuthInfo authInfo) { + public ResponseEntity socialFeedback(@PathVariable long roomId, + @RequestParam String username, + @LoginMember AuthInfo authInfo) { SocialFeedbackResponse response = socialFeedbackService.findSocialFeedback(roomId, authInfo.getId(), username); return ResponseEntity.ok() .body(response); } - @Override @PutMapping("/{feedbackId}") - public ResponseEntity update(@PathVariable long roomId, @PathVariable long feedbackId, @LoginMember AuthInfo authInfo, @RequestBody SocialFeedbackRequest request) { + public ResponseEntity update(@PathVariable long roomId, + @PathVariable long feedbackId, + @LoginMember AuthInfo authInfo, + @RequestBody SocialFeedbackUpdateRequest request) { socialFeedbackService.update(feedbackId, authInfo.getId(), request); return ResponseEntity.ok() .build(); diff --git a/backend/src/main/java/corea/feedback/controller/SocialFeedbackControllerSpecification.java b/backend/src/main/java/corea/feedback/controller/SocialFeedbackControllerSpecification.java index 686927e82..747731ae2 100644 --- a/backend/src/main/java/corea/feedback/controller/SocialFeedbackControllerSpecification.java +++ b/backend/src/main/java/corea/feedback/controller/SocialFeedbackControllerSpecification.java @@ -2,8 +2,9 @@ import corea.auth.domain.AuthInfo; import corea.exception.ExceptionType; -import corea.feedback.dto.SocialFeedbackRequest; +import corea.feedback.dto.SocialFeedbackCreateRequest; import corea.feedback.dto.SocialFeedbackResponse; +import corea.feedback.dto.SocialFeedbackUpdateRequest; import corea.global.annotation.ApiErrorResponses; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -25,7 +26,7 @@ public interface SocialFeedbackControllerSpecification { ResponseEntity create(@Parameter(description = "방 아이디", example = "1") long roomId, AuthInfo authInfo, - SocialFeedbackRequest request); + SocialFeedbackCreateRequest request); @Operation(summary = "커뮤니케이션 관련 피드백을 반환합니다.", description = "자신에게 사람들이 남긴 커뮤니케이션 능력 관련 피드백을 반환합니다.
" + @@ -58,5 +59,5 @@ ResponseEntity update(@Parameter(description = "방 아이디", example = @Parameter(description = "피드백 아이디", example = "2") long feedbackId, AuthInfo authInfo, - SocialFeedbackRequest request); + SocialFeedbackUpdateRequest request); } diff --git a/backend/src/main/java/corea/feedback/domain/DevelopFeedbackWriter.java b/backend/src/main/java/corea/feedback/domain/DevelopFeedbackWriter.java index 7f7c3acf4..8284952b5 100644 --- a/backend/src/main/java/corea/feedback/domain/DevelopFeedbackWriter.java +++ b/backend/src/main/java/corea/feedback/domain/DevelopFeedbackWriter.java @@ -32,7 +32,7 @@ private void validateAlreadyExist(long roomId, long deliverId, long receiverId) public void update(DevelopFeedback developFeedback, long deliverId, DevelopFeedbackUpdateInput input) { validateUpdateAuthority(developFeedback, deliverId); - log.info("개발 피드백 업데이트 [피드백 ID={}, 작성자 ID={}, 요청값={}]", developFeedback.getId(), developFeedback, input); + log.info("개발 피드백 업데이트 [피드백 ID={}, 작성자 ID={}, 요청값={}]", developFeedback.getId(), deliverId, input); developFeedback.update( input.evaluationPoint(), diff --git a/backend/src/main/java/corea/feedback/domain/SocialFeedback.java b/backend/src/main/java/corea/feedback/domain/SocialFeedback.java index 54efcb4c4..995068ece 100644 --- a/backend/src/main/java/corea/feedback/domain/SocialFeedback.java +++ b/backend/src/main/java/corea/feedback/domain/SocialFeedback.java @@ -47,6 +47,10 @@ public SocialFeedback(long roomId, Member deliver, Member receiver, int evaluate this(null, roomId, deliver, receiver, evaluatePoint, keywords, feedBackText); } + public boolean isNotMatchingDeliver(long deliverId) { + return deliver.isNotMatchingId(deliverId); + } + public void update(int evaluationPoint, List feedbackKeywords, String feedbackText) { this.evaluatePoint = evaluationPoint; this.keywords = FeedbackKeywordConverter.convertToKeywords(feedbackKeywords); diff --git a/backend/src/main/java/corea/feedback/domain/SocialFeedbackReader.java b/backend/src/main/java/corea/feedback/domain/SocialFeedbackReader.java index b401fcec1..89420e250 100644 --- a/backend/src/main/java/corea/feedback/domain/SocialFeedbackReader.java +++ b/backend/src/main/java/corea/feedback/domain/SocialFeedbackReader.java @@ -1,5 +1,7 @@ package corea.feedback.domain; +import corea.exception.CoreaException; +import corea.exception.ExceptionType; import corea.feedback.dto.FeedbackOutput; import corea.feedback.repository.SocialFeedbackRepository; import lombok.RequiredArgsConstructor; @@ -30,4 +32,14 @@ public Map> collectReceivedSocialFeedback(long feedba .map(FeedbackOutput::fromReceiver) .collect(Collectors.groupingBy(FeedbackOutput::roomId)); } + + public SocialFeedback findById(long feedbackId) { + return socialFeedbackRepository.findById(feedbackId) + .orElseThrow(() -> new CoreaException(ExceptionType.FEEDBACK_NOT_FOUND)); + } + + public SocialFeedback findSocialFeedback(long roomId, long deliverId, String username) { + return socialFeedbackRepository.findByRoomIdAndDeliverIdAndReceiverUsername(roomId, deliverId, username) + .orElseThrow(() -> new CoreaException(ExceptionType.FEEDBACK_NOT_FOUND)); + } } diff --git a/backend/src/main/java/corea/feedback/domain/SocialFeedbackWriter.java b/backend/src/main/java/corea/feedback/domain/SocialFeedbackWriter.java new file mode 100644 index 000000000..0082c1d8e --- /dev/null +++ b/backend/src/main/java/corea/feedback/domain/SocialFeedbackWriter.java @@ -0,0 +1,49 @@ +package corea.feedback.domain; + +import corea.exception.CoreaException; +import corea.exception.ExceptionType; +import corea.feedback.dto.SocialFeedbackUpdateInput; +import corea.feedback.repository.SocialFeedbackRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Component +@RequiredArgsConstructor +@Transactional +public class SocialFeedbackWriter { + + private final SocialFeedbackRepository socialFeedbackRepository; + + public SocialFeedback create(SocialFeedback socialFeedback, long roomId, long deliverId, long receiverId) { + validateAlreadyExist(roomId, deliverId, receiverId); + log.info("소셜 피드백 작성 [방 ID={}, 작성자 ID={}, 수신자 ID={}]", roomId, deliverId, receiverId); + + return socialFeedbackRepository.save(socialFeedback); + } + + private void validateAlreadyExist(long roomId, long deliverId, long receiverId) { + if (socialFeedbackRepository.existsByRoomIdAndDeliverIdAndReceiverId(roomId, deliverId, receiverId)) { + throw new CoreaException(ExceptionType.ALREADY_COMPLETED_FEEDBACK); + } + } + + public void update(SocialFeedback socialFeedback, long deliverId, SocialFeedbackUpdateInput input) { + validateUpdateAuthority(socialFeedback, deliverId); + log.info("소셜 피드백 업데이트 [피드백 ID={}, 작성자 ID={}, 요청값={}]", socialFeedback.getId(), deliverId, input); + + socialFeedback.update( + input.evaluationPoint(), + input.feedbackKeywords(), + input.feedbackText() + ); + } + + private void validateUpdateAuthority(SocialFeedback socialFeedback, long deliverId) { + if (socialFeedback.isNotMatchingDeliver(deliverId)) { + throw new CoreaException(ExceptionType.FEEDBACK_UPDATE_AUTHORIZATION_ERROR); + } + } +} diff --git a/backend/src/main/java/corea/feedback/dto/SocialFeedbackCreateRequest.java b/backend/src/main/java/corea/feedback/dto/SocialFeedbackCreateRequest.java new file mode 100644 index 000000000..7b848268d --- /dev/null +++ b/backend/src/main/java/corea/feedback/dto/SocialFeedbackCreateRequest.java @@ -0,0 +1,34 @@ +package corea.feedback.dto; + +import corea.feedback.domain.SocialFeedback; +import corea.feedback.util.FeedbackKeywordConverter; +import corea.member.domain.Member; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +@Schema(description = "커뮤니케이션 능력 관련 피드백 작성 요청") +public record SocialFeedbackCreateRequest(@Schema(description = "리뷰어 아이디", example = "2") + long receiverId, + + @Schema(description = "평가 점수", example = "4") + int evaluationPoint, + + @Schema(description = "선택한 피드백 키워드", example = "[\"이해가 잘 되게 설명을 잘해줘요(못해줘요)\", \"도움이 되었어요(아니에요)\"]") + List feedbackKeywords, + + @Schema(description = "부가 작성 가능한 피드백 텍스트", example = "말투가 너무 날카로운 것 같아요. ...") + String feedbackText) { + + public SocialFeedback toEntity(long roomId, Member deliver, Member receiver) { + return new SocialFeedback( + null, + roomId, + deliver, + receiver, + evaluationPoint, + FeedbackKeywordConverter.convertToKeywords(feedbackKeywords), + feedbackText + ); + } +} diff --git a/backend/src/main/java/corea/feedback/dto/SocialFeedbackRequest.java b/backend/src/main/java/corea/feedback/dto/SocialFeedbackRequest.java deleted file mode 100644 index a70c2a128..000000000 --- a/backend/src/main/java/corea/feedback/dto/SocialFeedbackRequest.java +++ /dev/null @@ -1,34 +0,0 @@ -package corea.feedback.dto; - -import corea.feedback.domain.SocialFeedback; -import corea.feedback.util.FeedbackKeywordConverter; -import corea.member.domain.Member; -import io.swagger.v3.oas.annotations.media.Schema; - -import java.util.List; - -@Schema(description = "커뮤니케이션 능력 관련 피드백 작성 요청") -public record SocialFeedbackRequest(@Schema(description = "리뷰어 아이디", example = "2") - long receiverId, - - @Schema(description = "평가 점수", example = "4") - int evaluationPoint, - - @Schema(description = "선택한 피드백 키워드", example = "[\"이해가 잘 되게 설명을 잘해줘요(못해줘요)\", \"도움이 되었어요(아니에요)\"]") - List feedbackKeywords, - - @Schema(description = "부가 작성 가능한 피드백 텍스트", example = "말투가 너무 날카로운 것 같아요. ...") - String feedbackText) { - - public SocialFeedback toEntity(long roomId, Member deliver, Member receiver) { - return new SocialFeedback( - null, - roomId, - deliver, - receiver, - evaluationPoint, - FeedbackKeywordConverter.convertToKeywords(feedbackKeywords), - feedbackText - ); - } -} diff --git a/backend/src/main/java/corea/feedback/dto/SocialFeedbackUpdateInput.java b/backend/src/main/java/corea/feedback/dto/SocialFeedbackUpdateInput.java new file mode 100644 index 000000000..66b64516a --- /dev/null +++ b/backend/src/main/java/corea/feedback/dto/SocialFeedbackUpdateInput.java @@ -0,0 +1,16 @@ +package corea.feedback.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +@Schema(description = "커뮤니케이션 능력 관련 피드백 업데이트 요청") +public record SocialFeedbackUpdateInput(@Schema(description = "업데이트할 평가 점수", example = "4") + int evaluationPoint, + + @Schema(description = "업데이트할 피드백 키워드", example = "[\"이해가 잘 되게 설명을 잘해줘요(못해줘요)\", \"도움이 되었어요(아니에요)\"]") + List feedbackKeywords, + + @Schema(description = "업데이트할 피드백 텍스트", example = "말투가 너무 날카로운 것 같아요. ...") + String feedbackText) { +} diff --git a/backend/src/main/java/corea/feedback/dto/SocialFeedbackUpdateRequest.java b/backend/src/main/java/corea/feedback/dto/SocialFeedbackUpdateRequest.java new file mode 100644 index 000000000..f7816cdf3 --- /dev/null +++ b/backend/src/main/java/corea/feedback/dto/SocialFeedbackUpdateRequest.java @@ -0,0 +1,16 @@ +package corea.feedback.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +@Schema(description = "커뮤니케이션 능력 관련 피드백 업데이트 요청") +public record SocialFeedbackUpdateRequest(@Schema(description = "업데이트할 평가 점수", example = "4") + int evaluationPoint, + + @Schema(description = "업데이트할 피드백 키워드", example = "[\"이해가 잘 되게 설명을 잘해줘요(못해줘요)\", \"도움이 되었어요(아니에요)\"]") + List feedbackKeywords, + + @Schema(description = "업데이트할 피드백 텍스트", example = "말투가 너무 날카로운 것 같아요. ...") + String feedbackText) { +} diff --git a/backend/src/main/java/corea/feedback/service/DevelopFeedbackService.java b/backend/src/main/java/corea/feedback/service/DevelopFeedbackService.java index fb5359674..55290ad13 100644 --- a/backend/src/main/java/corea/feedback/service/DevelopFeedbackService.java +++ b/backend/src/main/java/corea/feedback/service/DevelopFeedbackService.java @@ -7,7 +7,6 @@ import corea.feedback.dto.DevelopFeedbackResponse; import corea.feedback.dto.DevelopFeedbackUpdateInput; import corea.feedback.dto.DevelopFeedbackUpdateRequest; -import corea.feedback.util.FeedbackMapper; import corea.matchresult.domain.MatchResult; import corea.matchresult.domain.MatchResultWriter; import lombok.RequiredArgsConstructor; @@ -22,6 +21,7 @@ public class DevelopFeedbackService { private final DevelopFeedbackReader developFeedbackReader; private final DevelopFeedbackWriter developFeedbackWriter; private final MatchResultWriter matchResultWriter; + private final FeedbackMapper feedbackMapper; @Transactional public DevelopFeedbackResponse create(long roomId, long deliverId, DevelopFeedbackCreateRequest request) { @@ -37,7 +37,7 @@ public DevelopFeedbackResponse create(long roomId, long deliverId, DevelopFeedba public DevelopFeedbackResponse update(long feedbackId, long deliverId, DevelopFeedbackUpdateRequest request) { DevelopFeedback developFeedback = developFeedbackReader.findById(feedbackId); - DevelopFeedbackUpdateInput input = FeedbackMapper.toFeedbackInput(request); + DevelopFeedbackUpdateInput input = feedbackMapper.toFeedbackInput(request); developFeedbackWriter.update(developFeedback, deliverId, input); return DevelopFeedbackResponse.from(developFeedback); diff --git a/backend/src/main/java/corea/feedback/util/FeedbackMapper.java b/backend/src/main/java/corea/feedback/service/FeedbackMapper.java similarity index 55% rename from backend/src/main/java/corea/feedback/util/FeedbackMapper.java rename to backend/src/main/java/corea/feedback/service/FeedbackMapper.java index 7ddaac87e..b24263cd3 100644 --- a/backend/src/main/java/corea/feedback/util/FeedbackMapper.java +++ b/backend/src/main/java/corea/feedback/service/FeedbackMapper.java @@ -1,20 +1,16 @@ -package corea.feedback.util; +package corea.feedback.service; -import corea.feedback.dto.DevelopFeedbackUpdateInput; -import corea.feedback.dto.DevelopFeedbackUpdateRequest; -import corea.feedback.dto.FeedbackOutput; -import corea.feedback.dto.FeedbackResponse; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; +import corea.feedback.dto.*; +import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; import java.util.stream.Collectors; -@NoArgsConstructor(access = AccessLevel.PRIVATE) +@Component public class FeedbackMapper { - public static DevelopFeedbackUpdateInput toFeedbackInput(DevelopFeedbackUpdateRequest request) { + public DevelopFeedbackUpdateInput toFeedbackInput(DevelopFeedbackUpdateRequest request) { return new DevelopFeedbackUpdateInput( request.evaluationPoint(), request.feedbackKeywords(), @@ -23,7 +19,15 @@ public static DevelopFeedbackUpdateInput toFeedbackInput(DevelopFeedbackUpdateRe ); } - public static Map> toFeedbackResponseMap(Map> outputMap) { + public SocialFeedbackUpdateInput toFeedbackInput(SocialFeedbackUpdateRequest request) { + return new SocialFeedbackUpdateInput( + request.evaluationPoint(), + request.feedbackKeywords(), + request.feedbackText() + ); + } + + public Map> toFeedbackResponseMap(Map> outputMap) { return outputMap.entrySet() .stream() .collect(Collectors.toMap( @@ -32,13 +36,13 @@ public static Map> toFeedbackResponseMap(Map toFeedbackResponseList(List outputs) { + private List toFeedbackResponseList(List outputs) { return outputs.stream() - .map(FeedbackMapper::toFeedbackResponse) + .map(this::toFeedbackResponse) .toList(); } - private static FeedbackResponse toFeedbackResponse(FeedbackOutput output) { + private FeedbackResponse toFeedbackResponse(FeedbackOutput output) { return new FeedbackResponse( output.feedbackId(), output.roomId(), diff --git a/backend/src/main/java/corea/feedback/service/SocialFeedbackService.java b/backend/src/main/java/corea/feedback/service/SocialFeedbackService.java index 7c3bc66c1..1ee08055b 100644 --- a/backend/src/main/java/corea/feedback/service/SocialFeedbackService.java +++ b/backend/src/main/java/corea/feedback/service/SocialFeedbackService.java @@ -1,16 +1,15 @@ package corea.feedback.service; -import corea.exception.CoreaException; -import corea.exception.ExceptionType; import corea.feedback.domain.SocialFeedback; -import corea.feedback.dto.SocialFeedbackRequest; +import corea.feedback.domain.SocialFeedbackReader; +import corea.feedback.domain.SocialFeedbackWriter; +import corea.feedback.dto.SocialFeedbackCreateRequest; import corea.feedback.dto.SocialFeedbackResponse; -import corea.feedback.repository.SocialFeedbackRepository; +import corea.feedback.dto.SocialFeedbackUpdateInput; +import corea.feedback.dto.SocialFeedbackUpdateRequest; import corea.matchresult.domain.MatchResult; -import corea.matchresult.repository.MatchResultRepository; +import corea.matchresult.domain.MatchResultWriter; import lombok.RequiredArgsConstructor; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,58 +18,33 @@ @Transactional(readOnly = true) public class SocialFeedbackService { - private static final Logger log = LogManager.getLogger(SocialFeedbackService.class); - - private final SocialFeedbackRepository socialFeedbackRepository; - private final MatchResultRepository matchResultRepository; + private final SocialFeedbackReader socialFeedbackReader; + private final SocialFeedbackWriter socialFeedbackWriter; + private final MatchResultWriter matchResultWriter; + private final FeedbackMapper feedbackMapper; @Transactional - public SocialFeedbackResponse create(long roomId, long deliverId, SocialFeedbackRequest request) { - validateAlreadyExist(roomId, deliverId, request.receiverId()); - log.info("소설 피드백 작성[작성자({}), 요청값({})", deliverId, request); - - MatchResult matchResult = matchResultRepository.findByRoomIdAndReviewerIdAndRevieweeId(roomId, request.receiverId(), deliverId) - .orElseThrow(() -> new CoreaException(ExceptionType.NOT_MATCHED_MEMBER)); - matchResult.revieweeCompleteFeedback(); - - SocialFeedback feedback = saveSocialFeedback(roomId, request, matchResult); - return SocialFeedbackResponse.from(feedback); - } - - private void validateAlreadyExist(long roomId, long deliverId, long receiverId) { - if (socialFeedbackRepository.existsByRoomIdAndDeliverIdAndReceiverId(roomId, deliverId, receiverId)) { - throw new CoreaException(ExceptionType.ALREADY_COMPLETED_FEEDBACK); - } - } + public SocialFeedbackResponse create(long roomId, long deliverId, SocialFeedbackCreateRequest request) { + MatchResult matchResult = matchResultWriter.completeSocialFeedback(roomId, deliverId, request.receiverId()); - private SocialFeedback saveSocialFeedback(long roomId, SocialFeedbackRequest request, MatchResult matchResult) { SocialFeedback feedback = request.toEntity(roomId, matchResult.getReviewee(), matchResult.getReviewer()); - return socialFeedbackRepository.save(feedback); + SocialFeedback createdFeedback = socialFeedbackWriter.create(feedback, roomId, deliverId, request.receiverId()); + + return SocialFeedbackResponse.from(createdFeedback); } @Transactional - public SocialFeedbackResponse update(long feedbackId, long deliverId, SocialFeedbackRequest request) { - log.info("소설 피드백 업데이트[작성자({}), 피드백 ID({}), 요청값({})", deliverId, feedbackId, request); + public SocialFeedbackResponse update(long feedbackId, long deliverId, SocialFeedbackUpdateRequest request) { + SocialFeedback socialFeedback = socialFeedbackReader.findById(feedbackId); - SocialFeedback feedback = socialFeedbackRepository.findById(feedbackId) - .orElseThrow(() -> new CoreaException(ExceptionType.FEEDBACK_NOT_FOUND)); - updateFeedback(feedback, request); + SocialFeedbackUpdateInput input = feedbackMapper.toFeedbackInput(request); + socialFeedbackWriter.update(socialFeedback, deliverId, input); - return SocialFeedbackResponse.from(feedback); - } - - private void updateFeedback(SocialFeedback feedback, SocialFeedbackRequest request) { - feedback.update( - request.evaluationPoint(), - request.feedbackKeywords(), - request.feedbackText() - ); + return SocialFeedbackResponse.from(socialFeedback); } public SocialFeedbackResponse findSocialFeedback(long roomId, long deliverId, String username) { - SocialFeedback feedback = socialFeedbackRepository.findByRoomIdAndDeliverIdAndReceiverUsername(roomId, deliverId, username) - .orElseThrow(() -> new CoreaException(ExceptionType.FEEDBACK_NOT_FOUND)); - - return SocialFeedbackResponse.from(feedback); + SocialFeedback socialFeedback = socialFeedbackReader.findSocialFeedback(roomId, deliverId, username); + return SocialFeedbackResponse.from(socialFeedback); } } diff --git a/backend/src/main/java/corea/feedback/service/UserFeedbackService.java b/backend/src/main/java/corea/feedback/service/UserFeedbackService.java index fe32fce75..cf713dcd2 100644 --- a/backend/src/main/java/corea/feedback/service/UserFeedbackService.java +++ b/backend/src/main/java/corea/feedback/service/UserFeedbackService.java @@ -6,7 +6,6 @@ import corea.feedback.dto.FeedbackResponse; import corea.feedback.dto.FeedbacksResponse; import corea.feedback.dto.UserFeedbackResponse; -import corea.feedback.util.FeedbackMapper; import corea.room.domain.Room; import corea.room.repository.RoomRepository; import lombok.RequiredArgsConstructor; @@ -29,6 +28,7 @@ public class UserFeedbackService { private final RoomRepository roomRepository; private final DevelopFeedbackReader developFeedbackReader; private final SocialFeedbackReader socialFeedbackReader; + private final FeedbackMapper feedbackMapper; public UserFeedbackResponse getDeliveredFeedback(long feedbackDeliverId) { Map> developFeedbackOutput = developFeedbackReader.collectDeliverDevelopFeedback(feedbackDeliverId); @@ -45,8 +45,8 @@ public UserFeedbackResponse getReceivedFeedback(long feedbackReceiverId) { } private UserFeedbackResponse getUserFeedbackResponse(Map> developFeedbackOutput, Map> socialFeedbackOutput, Predicate predicate) { - Map> developFeedbacks = FeedbackMapper.toFeedbackResponseMap(developFeedbackOutput); - Map> socialFeedbacks = FeedbackMapper.toFeedbackResponseMap(socialFeedbackOutput); + Map> developFeedbacks = feedbackMapper.toFeedbackResponseMap(developFeedbackOutput); + Map> socialFeedbacks = feedbackMapper.toFeedbackResponseMap(socialFeedbackOutput); List feedbacksResponses = getFeedbacksResponses(developFeedbacks, socialFeedbacks, predicate); return new UserFeedbackResponse(feedbacksResponses); diff --git a/backend/src/main/java/corea/global/jpa/DataSourceConfig.java b/backend/src/main/java/corea/global/jpa/DataSourceConfig.java index a93ecb9cd..598ae6bac 100644 --- a/backend/src/main/java/corea/global/jpa/DataSourceConfig.java +++ b/backend/src/main/java/corea/global/jpa/DataSourceConfig.java @@ -58,7 +58,6 @@ public DataSource dataSource(DataSource routeDataSource) { } @Bean - @Primary public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { return new JpaTransactionManager(entityManagerFactory); } diff --git a/backend/src/main/java/corea/matchresult/domain/MatchResult.java b/backend/src/main/java/corea/matchresult/domain/MatchResult.java index 3ef1575de..27e6c6aab 100644 --- a/backend/src/main/java/corea/matchresult/domain/MatchResult.java +++ b/backend/src/main/java/corea/matchresult/domain/MatchResult.java @@ -78,4 +78,8 @@ public boolean isReviewed() { public void updateReviewLink(String reviewLink) { this.reviewLink = reviewLink; } + + public String getReviewerGithubId() { + return reviewer.getGithubUserId(); + } } diff --git a/backend/src/main/java/corea/matchresult/domain/MatchResultReader.java b/backend/src/main/java/corea/matchresult/domain/MatchResultReader.java new file mode 100644 index 000000000..b2296cc45 --- /dev/null +++ b/backend/src/main/java/corea/matchresult/domain/MatchResultReader.java @@ -0,0 +1,19 @@ +package corea.matchresult.domain; + +import corea.exception.CoreaException; +import corea.exception.ExceptionType; +import corea.matchresult.repository.MatchResultRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class MatchResultReader { + + private final MatchResultRepository matchResultRepository; + + public MatchResult findOne(long roomId,long reviewerId,long revieweeId) { + return matchResultRepository.findByRoomIdAndReviewerIdAndRevieweeId(roomId, reviewerId, revieweeId) + .orElseThrow(() -> new CoreaException(ExceptionType.NOT_MATCHED_MEMBER)); + } +} diff --git a/backend/src/main/java/corea/matchresult/domain/MatchResultWriter.java b/backend/src/main/java/corea/matchresult/domain/MatchResultWriter.java index 96bc150ee..8c89de909 100644 --- a/backend/src/main/java/corea/matchresult/domain/MatchResultWriter.java +++ b/backend/src/main/java/corea/matchresult/domain/MatchResultWriter.java @@ -14,6 +14,11 @@ public class MatchResultWriter { private final MatchResultRepository matchResultRepository; + public void reviewComplete(MatchResult matchResult, String prLink) { + matchResult.reviewComplete(); + matchResult.updateReviewLink(prLink); + } + public MatchResult completeDevelopFeedback(long roomId, long deliverId, long receiverId) { MatchResult matchResult = matchResultRepository.findByRoomIdAndReviewerIdAndRevieweeId(roomId, deliverId, receiverId) .orElseThrow(() -> new CoreaException(ExceptionType.NOT_MATCHED_MEMBER)); @@ -21,4 +26,12 @@ public MatchResult completeDevelopFeedback(long roomId, long deliverId, long rec matchResult.reviewerCompleteFeedback(); return matchResult; } + + public MatchResult completeSocialFeedback(long roomId, long deliverId, long receiverId) { + MatchResult matchResult = matchResultRepository.findByRoomIdAndReviewerIdAndRevieweeId(roomId, receiverId, deliverId) + .orElseThrow(() -> new CoreaException(ExceptionType.NOT_MATCHED_MEMBER)); + + matchResult.revieweeCompleteFeedback(); + return matchResult; + } } diff --git a/backend/src/main/java/corea/member/domain/MemberReader.java b/backend/src/main/java/corea/member/domain/MemberReader.java new file mode 100644 index 000000000..62749fa63 --- /dev/null +++ b/backend/src/main/java/corea/member/domain/MemberReader.java @@ -0,0 +1,21 @@ +package corea.member.domain; + +import corea.exception.CoreaException; +import corea.exception.ExceptionType; +import corea.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MemberReader { + + private final MemberRepository memberRepository; + + public Member findOne(long memberId) { + return memberRepository.findById(memberId) + .orElseThrow(() -> new CoreaException(ExceptionType.MEMBER_NOT_FOUND)); + } +} diff --git a/backend/src/main/java/corea/participation/domain/ParticipationReader.java b/backend/src/main/java/corea/participation/domain/ParticipationReader.java new file mode 100644 index 000000000..37dc2cae2 --- /dev/null +++ b/backend/src/main/java/corea/participation/domain/ParticipationReader.java @@ -0,0 +1,19 @@ +package corea.participation.domain; + +import corea.member.domain.MemberRole; +import corea.participation.repository.ParticipationRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ParticipationReader { + + private final ParticipationRepository participationRepository; + + public MemberRole findMemberRole(long roomId, long memberId) { + return participationRepository.findByRoomIdAndMemberId(roomId, memberId) + .map(Participation::getMemberRole) + .orElse(MemberRole.NONE); + } +} diff --git a/backend/src/main/java/corea/participation/domain/ParticipationWriter.java b/backend/src/main/java/corea/participation/domain/ParticipationWriter.java index e06f17f42..142ce1719 100644 --- a/backend/src/main/java/corea/participation/domain/ParticipationWriter.java +++ b/backend/src/main/java/corea/participation/domain/ParticipationWriter.java @@ -20,7 +20,6 @@ public class ParticipationWriter { private final ParticipationRepository participationRepository; public Participation create(Room room, Member member, MemberRole memberRole, ParticipationStatus participationStatus) { - return create(room, member, memberRole, participationStatus, room.getMatchingSize()); } @@ -45,6 +44,13 @@ public void delete(long roomId, long memberId) { participationRepository.delete(participation); } + public void deleteAllByRoom(Room room) { + if (room.isNotOpened()) { + throw new CoreaException(ExceptionType.ROOM_STATUS_INVALID); + } + participationRepository.deleteAllByRoomId(room.getId()); + } + private void logCreateParticipation(Participation participation) { log.info("방에 참가했습니다. id={}, 방 id={}, 참가한 사용자 id={}, 역할={}, 원하는 매칭 인원={}", participation.getId(), participation.getRoomsId(), participation.getMembersId(), participation.getMemberRole(), participation.getMatchingSize()); } diff --git a/backend/src/main/java/corea/auth/dto/GithubPullRequestReview.java b/backend/src/main/java/corea/review/dto/GithubPullRequestReview.java similarity index 66% rename from backend/src/main/java/corea/auth/dto/GithubPullRequestReview.java rename to backend/src/main/java/corea/review/dto/GithubPullRequestReview.java index cc303fd18..3a48f2d5e 100644 --- a/backend/src/main/java/corea/auth/dto/GithubPullRequestReview.java +++ b/backend/src/main/java/corea/review/dto/GithubPullRequestReview.java @@ -1,6 +1,7 @@ -package corea.auth.dto; +package corea.review.dto; import com.fasterxml.jackson.annotation.JsonProperty; +import corea.auth.dto.GithubUserInfo; public record GithubPullRequestReview( @@ -13,4 +14,7 @@ public record GithubPullRequestReview( @JsonProperty("html_url") String html_url ) { + public String getGithubUserId() { + return user.id(); + } } diff --git a/backend/src/main/java/corea/review/dto/GithubPullRequestReviewInfo.java b/backend/src/main/java/corea/review/dto/GithubPullRequestReviewInfo.java new file mode 100644 index 000000000..040075381 --- /dev/null +++ b/backend/src/main/java/corea/review/dto/GithubPullRequestReviewInfo.java @@ -0,0 +1,11 @@ +package corea.review.dto; + +import java.util.Map; +import java.util.Optional; + +public record GithubPullRequestReviewInfo(Map data) { + + public Optional findWithGithubUserId(String id) { + return Optional.ofNullable(data.get(id)); + } +} diff --git a/backend/src/main/java/corea/review/infrastructure/GithubReviewClient.java b/backend/src/main/java/corea/review/infrastructure/GithubReviewClient.java new file mode 100644 index 000000000..f5f8cbdd0 --- /dev/null +++ b/backend/src/main/java/corea/review/infrastructure/GithubReviewClient.java @@ -0,0 +1,28 @@ +package corea.review.infrastructure; + +import corea.auth.infrastructure.GithubProperties; +import corea.auth.infrastructure.GithubPullRequestUrlExchanger; +import corea.review.dto.GithubPullRequestReview; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +import static org.springframework.http.MediaType.APPLICATION_JSON; + +@EnableConfigurationProperties(GithubProperties.class) +@Component +@RequiredArgsConstructor +public class GithubReviewClient { + + private final RestClient restClient; + + public GithubPullRequestReview[] getReviewLink(String prLink) { + String url = GithubPullRequestUrlExchanger.pullRequestUrlToReview(prLink); + return restClient.get() + .uri(url) + .accept(APPLICATION_JSON) + .retrieve() + .body(GithubPullRequestReview[].class); + } +} diff --git a/backend/src/main/java/corea/review/infrastructure/GithubReviewProvider.java b/backend/src/main/java/corea/review/infrastructure/GithubReviewProvider.java new file mode 100644 index 000000000..0b3aca546 --- /dev/null +++ b/backend/src/main/java/corea/review/infrastructure/GithubReviewProvider.java @@ -0,0 +1,27 @@ +package corea.review.infrastructure; + +import corea.review.dto.GithubPullRequestReview; +import corea.review.dto.GithubPullRequestReviewInfo; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Component +@RequiredArgsConstructor +public class GithubReviewProvider { + + private final GithubReviewClient reviewClient; + + public GithubPullRequestReviewInfo getReviewWithPrLink(String prLink) { + final GithubPullRequestReview[] result = reviewClient.getReviewLink(prLink); + return new GithubPullRequestReviewInfo(Arrays.stream(result) + .collect(Collectors.toMap( + GithubPullRequestReview::getGithubUserId, + Function.identity(), + (x, y) -> x + ))); + } +} diff --git a/backend/src/main/java/corea/review/service/ReviewService.java b/backend/src/main/java/corea/review/service/ReviewService.java index 1831d963e..2f095ce8c 100644 --- a/backend/src/main/java/corea/review/service/ReviewService.java +++ b/backend/src/main/java/corea/review/service/ReviewService.java @@ -1,23 +1,20 @@ package corea.review.service; -import corea.auth.dto.GithubPullRequestReview; -import corea.auth.service.GithubOAuthProvider; import corea.exception.CoreaException; import corea.exception.ExceptionType; import corea.matchresult.domain.MatchResult; -import corea.matchresult.repository.MatchResultRepository; -import corea.member.domain.Member; -import corea.member.repository.MemberRepository; -import corea.room.domain.Room; -import corea.room.repository.RoomRepository; +import corea.matchresult.domain.MatchResultReader; +import corea.matchresult.domain.MatchResultWriter; +import corea.review.dto.GithubPullRequestReview; +import corea.review.infrastructure.GithubReviewProvider; +import corea.room.domain.RoomReader; +import corea.room.domain.RoomStatus; import lombok.RequiredArgsConstructor; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.stream.Stream; - @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -25,53 +22,28 @@ public class ReviewService { private static final Logger log = LogManager.getLogger(ReviewService.class); - private final GithubOAuthProvider githubOAuthProvider; - private final RoomRepository roomRepository; - private final MemberRepository memberRepository; - private final MatchResultRepository matchResultRepository; + private final GithubReviewProvider githubReviewProvider; + private final RoomReader roomReader; + private final MatchResultReader matchResultReader; + private final MatchResultWriter matchResultWriter; @Transactional public void completeReview(long roomId, long reviewerId, long revieweeId) { - Room room = getRoom(roomId); - validateRoomStatus(room); - - MatchResult matchResult = getMatchResult(roomId, reviewerId, revieweeId); - matchResult.reviewComplete(); - updateReviewLink(matchResult, reviewerId); - - log.info("리뷰 완료[{매칭 ID({}), 리뷰어 ID({}, 리뷰이 ID({})", matchResult.getId(), reviewerId, revieweeId); - } - - private Room getRoom(long roomId) { - return roomRepository.findById(roomId) - .orElseThrow(() -> new CoreaException(ExceptionType.ROOM_NOT_FOUND, String.format("해당 Id의 방이 없습니다. 입력된 Id=%d", roomId))); - } - - private void validateRoomStatus(Room room) { - if (room.isNotProgress()) { + boolean isNotProgress = roomReader.isNotStatus(roomId, RoomStatus.PROGRESS); + if (isNotProgress) { throw new CoreaException(ExceptionType.ROOM_STATUS_INVALID); } - } + MatchResult matchResult = matchResultReader.findOne(roomId, reviewerId, revieweeId); + String prLink = getPrReviewLink(matchResult.getPrLink(), matchResult.getReviewerGithubId()); + matchResultWriter.reviewComplete(matchResult, prLink); - private void updateReviewLink(MatchResult matchResult, long reviewerId) { - Member reviewer = memberRepository.findById(reviewerId) - .orElseThrow(() -> new CoreaException(ExceptionType.MEMBER_NOT_FOUND)); - String userName = reviewer.getUsername(); - String newReviewLink = findReviewLink(userName, matchResult.getPrLink()); - matchResult.updateReviewLink(newReviewLink); + log.info("리뷰 완료[{매칭 ID({}), 리뷰어 ID({}, 리뷰이 ID({})", matchResult.getId(), reviewerId, revieweeId); } - private String findReviewLink(String userName, String prLink) { - GithubPullRequestReview[] githubPullRequestReviews = githubOAuthProvider.getPullRequestReview(prLink); - return Stream.of(githubPullRequestReviews) - .filter(review -> review.user().login().equals(userName)) - .findFirst() + private String getPrReviewLink(String prLink, String reviewerGithubId) { + return githubReviewProvider.getReviewWithPrLink(prLink) + .findWithGithubUserId(reviewerGithubId) .map(GithubPullRequestReview::html_url) .orElseThrow(() -> new CoreaException(ExceptionType.NOT_COMPLETE_GITHUB_REVIEW)); } - - private MatchResult getMatchResult(long roomId, long reviewerId, long revieweeId) { - return matchResultRepository.findByRoomIdAndReviewerIdAndRevieweeId(roomId, reviewerId, revieweeId) - .orElseThrow(() -> new CoreaException(ExceptionType.NOT_MATCHED_MEMBER)); - } } diff --git a/backend/src/main/java/corea/room/controller/RoomControllerSpecification.java b/backend/src/main/java/corea/room/controller/RoomControllerSpecification.java index a61e3597a..216575c39 100644 --- a/backend/src/main/java/corea/room/controller/RoomControllerSpecification.java +++ b/backend/src/main/java/corea/room/controller/RoomControllerSpecification.java @@ -32,7 +32,7 @@ public interface RoomControllerSpecification { "JWT 토큰에서 추출된 사용자 정보는 피드백 작성에 필요한 인증된 사용자 정보를 제공합니다. " + "

**참고:** 이 API를 사용하기 위해서는 유효한 JWT 토큰이 필요하며, " + "토큰이 없거나 유효하지 않은 경우 인증 오류가 발생합니다.") - @ApiErrorResponses(value = ExceptionType.MEMBER_NOT_FOUND) + @ApiErrorResponses(value = {ExceptionType.MEMBER_NOT_FOUND, ExceptionType.ROOM_MODIFY_AUTHORIZATION_ERROR}) ResponseEntity update(AuthInfo authInfo, RoomUpdateRequest request); @Operation(summary = "방 참가자들의 정보를 반환합니다.", @@ -54,7 +54,7 @@ ResponseEntity participants(@Parameter(description = " "JWT 토큰에서 추출된 사용자 정보는 방을 생성한 사용자와 일치하는지 파악합니다. " + "

**참고:** 이 API를 사용하기 위해서는 유효한 JWT 토큰이 필요하며, " + "토큰이 없거나 유효하지 않은 경우 인증 오류가 발생합니다.") - @ApiErrorResponses(value = ExceptionType.ROOM_DELETION_AUTHORIZATION_ERROR) + @ApiErrorResponses(value = ExceptionType.ROOM_MODIFY_AUTHORIZATION_ERROR) ResponseEntity delete(@Parameter(description = "방 아이디", example = "1") long id, AuthInfo authInfo); diff --git a/backend/src/main/java/corea/room/domain/Room.java b/backend/src/main/java/corea/room/domain/Room.java index c64339ee8..5c06aa332 100644 --- a/backend/src/main/java/corea/room/domain/Room.java +++ b/backend/src/main/java/corea/room/domain/Room.java @@ -115,8 +115,8 @@ public boolean isNotClosed() { return !isClosed(); } - public boolean isNotProgress() { - return status.isNotProgress(); + public boolean isStatus(RoomStatus status) { + return this.status == status; } public boolean isNotMatchingManager(long memberId) { diff --git a/backend/src/main/java/corea/room/domain/RoomReader.java b/backend/src/main/java/corea/room/domain/RoomReader.java new file mode 100644 index 000000000..e47f86f9a --- /dev/null +++ b/backend/src/main/java/corea/room/domain/RoomReader.java @@ -0,0 +1,24 @@ +package corea.room.domain; + +import corea.exception.CoreaException; +import corea.exception.ExceptionType; +import corea.room.repository.RoomRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class RoomReader { + + private final RoomRepository roomRepository; + + public boolean isNotStatus(long roomId, RoomStatus status) { + Room room = find(roomId); + return !room.isStatus(status); + } + + public Room find(long roomId) { + return roomRepository.findById(roomId) + .orElseThrow(() -> new CoreaException(ExceptionType.ROOM_NOT_FOUND, String.format("해당 Id의 방이 없습니다. 입력된 Id=%d", roomId))); + } +} diff --git a/backend/src/main/java/corea/room/domain/RoomWriter.java b/backend/src/main/java/corea/room/domain/RoomWriter.java new file mode 100644 index 000000000..4343f6f00 --- /dev/null +++ b/backend/src/main/java/corea/room/domain/RoomWriter.java @@ -0,0 +1,71 @@ +package corea.room.domain; + +import corea.exception.CoreaException; +import corea.exception.ExceptionType; +import corea.member.domain.Member; +import corea.participation.domain.ParticipationWriter; +import corea.room.dto.RoomCreateRequest; +import corea.room.dto.RoomUpdateRequest; +import corea.room.repository.RoomRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Component +@RequiredArgsConstructor +@Transactional +public class RoomWriter { + + private static final int PLUS_HOURS_TO_MINIMUM_RECRUITMENT_DEADLINE = 1; + private static final int PLUS_DAYS_TO_MINIMUM_REVIEW_DEADLINE = 1; + + private final RoomRepository roomRepository; + private final ParticipationWriter participationWriter; + + // TODO 서비스 용 DTO 만들어야함 + public Room create(Member manager, RoomCreateRequest request) { +// validateDeadLine(request.recruitmentDeadline(), request.reviewDeadline()); + Room room = roomRepository.save(request.toEntity(manager)); + log.info("방을 생성했습니다. 방 생성자 id={}, 요청한 사용자 id={}", room.getId(), room.getManagerId()); + return room; + } + + public Room update(Room room, Member manager, RoomUpdateRequest request) { + validate(room, manager); + return roomRepository.save(request.toEntity(room, manager)); + } + + public void delete(Room room, Member manager) { + validate(room, manager); + roomRepository.delete(room); + participationWriter.deleteAllByRoom(room); + log.info("방을 삭제했습니다. 방 id={}, 사용자 iD={}", room.getId(), manager.getId()); + } + + private void validate(Room room, Member member) { + if (room.isNotMatchingManager(member.getId())) { + log.warn("인증되지 않은 방 변경 시도 방 생성자 id={}, 요청한 사용자 id={}", room.getId(), member.getId()); + throw new CoreaException(ExceptionType.ROOM_MODIFY_AUTHORIZATION_ERROR); + } + if (room.isNotOpened()) { + throw new CoreaException(ExceptionType.ROOM_STATUS_INVALID); + } + } + +// private void validateDeadLine(LocalDateTime recruitmentDeadline, LocalDateTime reviewDeadline) { +// LocalDateTime currentDateTime = LocalDateTime.now(); +// +// LocalDateTime minimumRecruitmentDeadline = currentDateTime.plusHours(PLUS_HOURS_TO_MINIMUM_RECRUITMENT_DEADLINE); +// if (recruitmentDeadline.isBefore(minimumRecruitmentDeadline)) { +// throw new CoreaException(ExceptionType.INVALID_RECRUITMENT_DEADLINE, +// String.format("모집 마감 시간은 현재 시간보다 %d시간 이후여야 합니다.", PLUS_HOURS_TO_MINIMUM_RECRUITMENT_DEADLINE)); +// } +// LocalDateTime minimumReviewDeadline = recruitmentDeadline.plusDays(PLUS_DAYS_TO_MINIMUM_REVIEW_DEADLINE); +// if (reviewDeadline.isBefore(minimumReviewDeadline)) { +// throw new CoreaException(ExceptionType.INVALID_REVIEW_DEADLINE, +// String.format("리뷰 마감 시간은 모집 마감 시간보다 %d일 이후여야 합니다.", PLUS_DAYS_TO_MINIMUM_REVIEW_DEADLINE)); +// } +// } +} diff --git a/backend/src/main/java/corea/room/dto/RoomUpdateRequest.java b/backend/src/main/java/corea/room/dto/RoomUpdateRequest.java index e5d099e30..b1a0a586a 100644 --- a/backend/src/main/java/corea/room/dto/RoomUpdateRequest.java +++ b/backend/src/main/java/corea/room/dto/RoomUpdateRequest.java @@ -57,19 +57,16 @@ public record RoomUpdateRequest(@Schema(description = "방 ID", example = "99") RoomClassification classification ) { - private static final int INITIAL_PARTICIPANTS_SIZE = 1; - private static final RoomStatus INITIAL_ROOM_STATUS = RoomStatus.OPEN; - - public Room toEntity(Member manager) { + public Room toEntity(Room room, Member manager) { return new Room( roomId, title, content, matchingSize, repositoryLink, thumbnailLink, keywords, - INITIAL_PARTICIPANTS_SIZE, limitedParticipants, + room.getCurrentParticipantsSize(), limitedParticipants, manager, recruitmentDeadline, reviewDeadline, classification, - INITIAL_ROOM_STATUS + room.getStatus() ); } } diff --git a/backend/src/main/java/corea/room/service/RoomService.java b/backend/src/main/java/corea/room/service/RoomService.java index 6aa860ac8..c6dc1d52e 100644 --- a/backend/src/main/java/corea/room/service/RoomService.java +++ b/backend/src/main/java/corea/room/service/RoomService.java @@ -4,21 +4,22 @@ import corea.exception.ExceptionType; import corea.matchresult.repository.MatchResultRepository; import corea.member.domain.Member; +import corea.member.domain.MemberReader; import corea.member.domain.MemberRole; -import corea.member.repository.MemberRepository; import corea.participation.domain.Participation; +import corea.participation.domain.ParticipationReader; import corea.participation.domain.ParticipationStatus; import corea.participation.domain.ParticipationWriter; import corea.participation.repository.ParticipationRepository; import corea.room.domain.Room; +import corea.room.domain.RoomReader; +import corea.room.domain.RoomWriter; import corea.room.dto.*; -import corea.room.repository.RoomRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -29,84 +30,49 @@ @Transactional(readOnly = true) public class RoomService { - private static final int PLUS_HOURS_TO_MINIMUM_RECRUITMENT_DEADLINE = 1; - private static final int PLUS_DAYS_TO_MINIMUM_REVIEW_DEADLINE = 1; private static final int RANDOM_DISPLAY_PARTICIPANTS_SIZE = 6; - private final RoomRepository roomRepository; - private final MemberRepository memberRepository; private final MatchResultRepository matchResultRepository; private final ParticipationRepository participationRepository; private final RoomAutomaticService roomAutomaticService; private final ParticipationWriter participationWriter; + private final MemberReader memberReader; + private final RoomWriter roomWriter; + private final ParticipationReader participationReader; + private final RoomReader roomReader; @Transactional public RoomResponse create(long memberId, RoomCreateRequest request) { -// validateDeadLine(request.recruitmentDeadline(), request.reviewDeadline()); - - Member manager = memberRepository.findById(memberId) - .orElseThrow(() -> new CoreaException(ExceptionType.MEMBER_NOT_FOUND)); - Room room = roomRepository.save(request.toEntity(manager)); - log.info("방을 생성했습니다. 방 생성자 id={}, 요청한 사용자 id={}", room.getManagerId(), memberId); + Member manager = memberReader.findOne(memberId); + Room room = roomWriter.create(manager, request); Participation participation = participationWriter.create(room, manager, MemberRole.REVIEWER, ParticipationStatus.MANAGER); - participationRepository.save(participation); roomAutomaticService.createAutomatic(room); - return RoomResponse.of(room, participation.getMemberRole(), ParticipationStatus.MANAGER); } - private void validateDeadLine(LocalDateTime recruitmentDeadline, LocalDateTime reviewDeadline) { - LocalDateTime currentDateTime = LocalDateTime.now(); - - LocalDateTime minimumRecruitmentDeadline = currentDateTime.plusHours(PLUS_HOURS_TO_MINIMUM_RECRUITMENT_DEADLINE); - if (recruitmentDeadline.isBefore(minimumRecruitmentDeadline)) { - throw new CoreaException(ExceptionType.INVALID_RECRUITMENT_DEADLINE, - String.format("모집 마감 시간은 현재 시간보다 %d시간 이후여야 합니다.", PLUS_HOURS_TO_MINIMUM_RECRUITMENT_DEADLINE)); - } - LocalDateTime minimumReviewDeadline = recruitmentDeadline.plusDays(PLUS_DAYS_TO_MINIMUM_REVIEW_DEADLINE); - if (reviewDeadline.isBefore(minimumReviewDeadline)) { - throw new CoreaException(ExceptionType.INVALID_REVIEW_DEADLINE, - String.format("리뷰 마감 시간은 모집 마감 시간보다 %d일 이후여야 합니다.", PLUS_DAYS_TO_MINIMUM_REVIEW_DEADLINE)); - } - } - @Transactional public RoomResponse update(long memberId, RoomUpdateRequest request) { - Room room = getRoom(request.roomId()); - if (room.isNotMatchingManager(memberId)) { - throw new CoreaException(ExceptionType.MEMBER_IS_NOT_MANAGER); - } - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new CoreaException(ExceptionType.MEMBER_NOT_FOUND)); + Room room = roomReader.find(request.roomId()); + Member member = memberReader.findOne(memberId); - Room updatedRoom = roomRepository.save(request.toEntity(member)); - Participation participation = participationRepository.findByRoomIdAndMemberId(updatedRoom.getId(), memberId) - .orElseThrow(() -> new CoreaException(ExceptionType.NOT_PARTICIPATED_ROOM)); + Room updatedRoom = roomWriter.update(room, member, request); + MemberRole memberRole = participationReader.findMemberRole(room.getId(), memberId); roomAutomaticService.updateTime(updatedRoom); - return RoomResponse.of(updatedRoom, participation.getMemberRole(), ParticipationStatus.MANAGER); + return RoomResponse.of(updatedRoom, memberRole, ParticipationStatus.MANAGER); } @Transactional public void delete(long roomId, long memberId) { - Room room = getRoom(roomId); - validateDeletionAuthority(room, memberId); + Room room = roomReader.find(roomId); + Member member = memberReader.findOne(memberId); - log.info("방을 삭제했습니다. 방 id={}, 사용자 iD={}", roomId, memberId); - roomRepository.delete(room); - participationRepository.deleteAllByRoomId(roomId); + roomWriter.delete(room, member); roomAutomaticService.deleteAutomatic(room); } - private void validateDeletionAuthority(Room room, long memberId) { - if (room.isNotMatchingManager(memberId)) { - log.warn("방 삭제 권한이 없습니다. 방 생성자만 방을 삭제할 수 있습니다. 방 생성자 id={}, 요청한 사용자 id={}", room.getManagerId(), memberId); - throw new CoreaException(ExceptionType.ROOM_DELETION_AUTHORIZATION_ERROR); - } - } - public RoomParticipantResponses findParticipants(long roomId, long memberId) { List participants = new ArrayList<>(participationRepository.findAllByRoomId(roomId) .stream() @@ -140,11 +106,6 @@ private RoomParticipantResponse getRoomParticipantResponse(long roomId, Particip } public RoomResponse getRoomById(long roomId) { - return RoomResponse.from(getRoom(roomId)); - } - - private Room getRoom(long roomId) { - return roomRepository.findById(roomId) - .orElseThrow(() -> new CoreaException(ExceptionType.ROOM_NOT_FOUND, String.format("해당 Id의 방이 없습니다. 입력된 Id=%d", roomId))); + return RoomResponse.from(roomReader.find(roomId)); } } diff --git a/backend/src/test/java/corea/auth/infrastructure/GithubOAuthClientTest.java b/backend/src/test/java/corea/auth/infrastructure/GithubPullRequestReviewClientTest.java similarity index 87% rename from backend/src/test/java/corea/auth/infrastructure/GithubOAuthClientTest.java rename to backend/src/test/java/corea/auth/infrastructure/GithubPullRequestReviewClientTest.java index e2ddcd180..3a38c0897 100644 --- a/backend/src/test/java/corea/auth/infrastructure/GithubOAuthClientTest.java +++ b/backend/src/test/java/corea/auth/infrastructure/GithubPullRequestReviewClientTest.java @@ -1,6 +1,7 @@ package corea.auth.infrastructure; -import corea.auth.dto.GithubPullRequestReview; +import corea.review.dto.GithubPullRequestReview; +import corea.review.infrastructure.GithubReviewClient; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -9,10 +10,10 @@ import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest -class GithubOAuthClientTest { +class GithubPullRequestReviewClientTest { @Autowired - private GithubOAuthClient client; + private GithubReviewClient client; @Test @DisplayName("해당 PR 링크에 존재하는 리뷰들을 가져온다.") diff --git a/backend/src/test/java/corea/feedback/controller/SocialFeedbackFeedbackControllerTest.java b/backend/src/test/java/corea/feedback/controller/SocialFeedbackFeedbackControllerTest.java index d6ecdc790..ed07ced56 100644 --- a/backend/src/test/java/corea/feedback/controller/SocialFeedbackFeedbackControllerTest.java +++ b/backend/src/test/java/corea/feedback/controller/SocialFeedbackFeedbackControllerTest.java @@ -2,7 +2,7 @@ import config.ControllerTest; import corea.auth.service.TokenService; -import corea.feedback.dto.SocialFeedbackRequest; +import corea.feedback.dto.SocialFeedbackCreateRequest; import corea.fixture.MatchResultFixture; import corea.fixture.MemberFixture; import corea.fixture.RoomFixture; @@ -48,7 +48,7 @@ void create() { reviewee )); - SocialFeedbackRequest request = new SocialFeedbackRequest( + SocialFeedbackCreateRequest request = new SocialFeedbackCreateRequest( reviewer.getId(), 4, List.of("방의 목적에 맞게 코드를 작성했어요", "코드를 이해하기 쉬웠어요"), diff --git a/backend/src/test/java/corea/feedback/service/SocialFeedbackServiceTest.java b/backend/src/test/java/corea/feedback/service/SocialFeedbackServiceTest.java index f1bd9a6f4..20dcab54e 100644 --- a/backend/src/test/java/corea/feedback/service/SocialFeedbackServiceTest.java +++ b/backend/src/test/java/corea/feedback/service/SocialFeedbackServiceTest.java @@ -2,8 +2,10 @@ import config.ServiceTest; import corea.exception.CoreaException; -import corea.feedback.dto.SocialFeedbackRequest; +import corea.exception.ExceptionType; +import corea.feedback.dto.SocialFeedbackCreateRequest; import corea.feedback.dto.SocialFeedbackResponse; +import corea.feedback.dto.SocialFeedbackUpdateRequest; import corea.fixture.MatchResultFixture; import corea.fixture.MemberFixture; import corea.fixture.RoomFixture; @@ -13,6 +15,7 @@ import corea.member.repository.MemberRepository; import corea.room.domain.Room; import corea.room.repository.RoomRepository; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -20,7 +23,9 @@ import java.util.List; -import static org.assertj.core.api.AssertionsForClassTypes.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; @ServiceTest class SocialFeedbackServiceTest { @@ -64,8 +69,31 @@ void throw_exception_when_not_exist_match_result() { Member reviewer = memberRepository.save(MemberFixture.MEMBER_PORORO()); Member reviewee = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); + assertThatThrownBy(() -> socialFeedbackService.create(room.getId(), reviewee.getId(), createRequest(reviewer.getId()))) + .asInstanceOf(InstanceOfAssertFactories.type(CoreaException.class)) + .extracting(CoreaException::getExceptionType) + .isEqualTo(ExceptionType.NOT_MATCHED_MEMBER); + } + + @Test + @DisplayName("소셜(리뷰이 -> 리뷰어) 에 대한 피드백이 이미 있다면 피드백을 생성할 때 예외를 발생한다.") + void throw_exception_when_already_feedback_exist() { + Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); + Room room = roomRepository.save(RoomFixture.ROOM_DOMAIN(manager)); + Member reviewer = memberRepository.save(MemberFixture.MEMBER_PORORO()); + Member reviewee = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); + matchResultRepository.save(MatchResultFixture.MATCH_RESULT_DOMAIN( + room.getId(), + reviewer, + reviewee + )); + + socialFeedbackService.create(room.getId(), reviewee.getId(), createRequest(reviewer.getId())); + assertThatCode(() -> socialFeedbackService.create(room.getId(), reviewee.getId(), createRequest(reviewer.getId()))) - .isInstanceOf(CoreaException.class); + .asInstanceOf(InstanceOfAssertFactories.type(CoreaException.class)) + .extracting(CoreaException::getExceptionType) + .isEqualTo(ExceptionType.ALREADY_COMPLETED_FEEDBACK); } @Test @@ -99,9 +127,9 @@ void update() { reviewee )); SocialFeedbackResponse createResponse = socialFeedbackService.create(room.getId(), reviewee.getId(), createRequest(reviewer.getId())); - SocialFeedbackResponse updateResponse = socialFeedbackService.update(createResponse.feedbackId(), reviewee.getId(), createRequest(reviewer.getId())); + SocialFeedbackResponse updateResponse = socialFeedbackService.update(createResponse.feedbackId(), reviewee.getId(), updateRequest()); - assertThat(createResponse).isEqualTo(updateResponse); + assertThat(updateResponse.evaluationPoint()).isEqualTo(2); } @Test @@ -117,16 +145,45 @@ void throw_exception_when_update_with_not_exist_feedback() { reviewee )); - assertThatThrownBy(() -> socialFeedbackService.update(room.getId(), reviewer.getId(), createRequest(reviewee.getId()))) + assertThatThrownBy(() -> socialFeedbackService.update(room.getId(), reviewer.getId(), updateRequest())) .isInstanceOf(CoreaException.class); } - private SocialFeedbackRequest createRequest(long revieweeId) { - return new SocialFeedbackRequest( - revieweeId, + @Test + @DisplayName("소셜(리뷰어 -> 리뷰이) 피드백 작성자가 아닌 사람이 업데이트시 예외를 발생한다.") + void throw_exception_when_anonymous_updates_feedback() { + Member manager = memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()); + Room room = roomRepository.save(RoomFixture.ROOM_DOMAIN(manager)); + Member reviewer = memberRepository.save(MemberFixture.MEMBER_PORORO()); + Member reviewee = memberRepository.save(MemberFixture.MEMBER_YOUNGSU()); + matchResultRepository.save(MatchResultFixture.MATCH_RESULT_DOMAIN( + room.getId(), + reviewer, + reviewee + )); + + SocialFeedbackResponse createResponse = socialFeedbackService.create(room.getId(), reviewee.getId(), createRequest(reviewer.getId())); + + assertThatThrownBy(() -> socialFeedbackService.update(createResponse.feedbackId(), reviewer.getId(), updateRequest())) + .asInstanceOf(InstanceOfAssertFactories.type(CoreaException.class)) + .extracting(CoreaException::getExceptionType) + .isEqualTo(ExceptionType.FEEDBACK_UPDATE_AUTHORIZATION_ERROR); + } + + private SocialFeedbackCreateRequest createRequest(long receiverId) { + return new SocialFeedbackCreateRequest( + receiverId, 4, List.of("방의 목적에 맞게 코드를 작성했어요", "코드를 이해하기 쉬웠어요"), "처음 자바를 접해봤다고 했는데 \n 생각보다 매우 구성되어 있는 코드 였던거 같습니다. ..." ); } + + private SocialFeedbackUpdateRequest updateRequest() { + return new SocialFeedbackUpdateRequest( + 2, + List.of("설명이 부족해요"), + "설명이 너무 부족해요..." + ); + } } diff --git a/backend/src/test/java/corea/review/infrastructure/GithubReviewProviderTest.java b/backend/src/test/java/corea/review/infrastructure/GithubReviewProviderTest.java new file mode 100644 index 000000000..72dfef61d --- /dev/null +++ b/backend/src/test/java/corea/review/infrastructure/GithubReviewProviderTest.java @@ -0,0 +1,38 @@ +package corea.review.infrastructure; + +import corea.review.dto.GithubPullRequestReview; +import corea.review.dto.GithubPullRequestReviewInfo; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class GithubReviewProviderTest { + + @Autowired + private GithubReviewProvider githubReviewProvider; + + @Test + @DisplayName("리뷰한 사람의 리뷰 링크를 찾을 수 있다.") + void getReviewWithPrLink() { + GithubPullRequestReviewInfo result = githubReviewProvider.getReviewWithPrLink("https://github.com/youngsu5582/github-api-test/pull/5"); + + // 입력된 pr에 무빈이 남긴 리뷰 -> 1개만 존재 + GithubPullRequestReview review = result.findWithGithubUserId("80106238").get(); + + Assertions.assertThat(review.html_url()).isEqualTo("https://github.com/youngsu5582/github-api-test/pull/5#pullrequestreview-2327171078"); + } + + @Test + @DisplayName("한 사람이 여러 리뷰를 남겼을 시, 첫번째 리뷰 링크를 찾는다.") + void getReviewWithPrLink_duplicatedKey() { + GithubPullRequestReviewInfo result = githubReviewProvider.getReviewWithPrLink("https://github.com/youngsu5582/github-api-test/pull/5"); + + // 입력된 pr에 뽀로로가 남긴 리뷰 -> 2개 존재 + GithubPullRequestReview review = result.findWithGithubUserId("119468757").get(); + + Assertions.assertThat(review.html_url()).isEqualTo("https://github.com/youngsu5582/github-api-test/pull/5#pullrequestreview-2327172283"); + } +} diff --git a/backend/src/test/java/corea/review/service/ReviewServiceTest.java b/backend/src/test/java/corea/review/service/ReviewServiceTest.java index 2740bbc8d..eac9ef099 100644 --- a/backend/src/test/java/corea/review/service/ReviewServiceTest.java +++ b/backend/src/test/java/corea/review/service/ReviewServiceTest.java @@ -1,9 +1,7 @@ package corea.review.service; import config.ServiceTest; -import corea.auth.dto.GithubPullRequestReview; import corea.auth.dto.GithubUserInfo; -import corea.auth.service.GithubOAuthProvider; import corea.exception.CoreaException; import corea.exception.ExceptionType; import corea.fixture.MatchResultFixture; @@ -14,6 +12,8 @@ import corea.matchresult.repository.MatchResultRepository; import corea.member.domain.Member; import corea.member.repository.MemberRepository; +import corea.review.dto.GithubPullRequestReview; +import corea.review.infrastructure.GithubReviewClient; import corea.room.domain.Room; import corea.room.repository.RoomRepository; import org.assertj.core.api.InstanceOfAssertFactories; @@ -44,7 +44,7 @@ class ReviewServiceTest { private MatchResultRepository matchResultRepository; @MockBean - private GithubOAuthProvider githubOAuthProvider; + private GithubReviewClient githubReviewClient; @Test @Transactional @@ -55,7 +55,7 @@ void completeReview() { Room room = roomRepository.save(RoomFixture.ROOM_DOMAIN_WITH_PROGRESS(memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()))); MatchResult matchResult = matchResultRepository.save(MatchResultFixture.MATCH_RESULT_DOMAIN(room.getId(), reviewer, reviewee)); - when(githubOAuthProvider.getPullRequestReview(anyString())) + when(githubReviewClient.getReviewLink(anyString())) .thenReturn(new GithubPullRequestReview[]{ new GithubPullRequestReview( "id", @@ -64,9 +64,9 @@ void completeReview() { reviewer.getName(), reviewer.getThumbnailUrl(), reviewer.getEmail(), - String.valueOf(reviewer.getId())), - "html_url") - }); + String.valueOf(reviewer.getGithubUserId())), + "html_url")} + ); reviewService.completeReview(room.getId(), reviewer.getId(), reviewee.getId()); assertThat(matchResult.getReviewStatus()).isEqualTo(ReviewStatus.COMPLETE); @@ -80,7 +80,7 @@ void notCompleteReview() { Room room = roomRepository.save(RoomFixture.ROOM_DOMAIN_WITH_PROGRESS(memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON()))); matchResultRepository.save(MatchResultFixture.MATCH_RESULT_DOMAIN(room.getId(), reviewer, reviewee)); - when(githubOAuthProvider.getPullRequestReview(anyString())).thenReturn(new GithubPullRequestReview[]{}); + when(githubReviewClient.getReviewLink(anyString())).thenReturn(new GithubPullRequestReview[]{}); assertThatThrownBy(() -> reviewService.completeReview(room.getId(), reviewer.getId(), reviewee.getId())) .asInstanceOf(InstanceOfAssertFactories.type(CoreaException.class)) diff --git a/backend/src/test/java/corea/room/acceptance/RoomReviewerAcceptanceTest.java b/backend/src/test/java/corea/room/acceptance/RoomReviewerAcceptanceTest.java index ec0944b09..15cbf11af 100644 --- a/backend/src/test/java/corea/room/acceptance/RoomReviewerAcceptanceTest.java +++ b/backend/src/test/java/corea/room/acceptance/RoomReviewerAcceptanceTest.java @@ -4,7 +4,7 @@ import corea.auth.service.LoginService; import corea.auth.service.TokenService; import corea.feedback.dto.DevelopFeedbackCreateRequest; -import corea.feedback.dto.SocialFeedbackRequest; +import corea.feedback.dto.SocialFeedbackCreateRequest; import corea.fixture.MatchResultFixture; import corea.fixture.MemberFixture; import corea.fixture.RoomFixture; @@ -103,7 +103,7 @@ void reviewee_match_result_should_be_writed() { reviewee )); - SocialFeedbackRequest request = new SocialFeedbackRequest( + SocialFeedbackCreateRequest request = new SocialFeedbackCreateRequest( reviewer.getId(), 4, List.of("방의 목적에 맞게 코드를 작성했어요", "코드를 이해하기 쉬웠어요"), diff --git a/backend/src/test/java/corea/room/service/RoomServiceTest.java b/backend/src/test/java/corea/room/service/RoomServiceTest.java index a6d2e0182..82be4aeda 100644 --- a/backend/src/test/java/corea/room/service/RoomServiceTest.java +++ b/backend/src/test/java/corea/room/service/RoomServiceTest.java @@ -80,7 +80,8 @@ void manager() { @Test @DisplayName("방을 생성할 때 모집 마감 시간은 현재 시간보다 1시간 이후가 아니라면 예외가 발생한다.") void invalidRecruitmentDeadline() { - RoomCreateRequest request = RoomFixture.ROOM_CREATE_REQUEST_WITH_RECRUITMENT_DEADLINE(LocalDateTime.now().plusMinutes(59)); + RoomCreateRequest request = RoomFixture.ROOM_CREATE_REQUEST_WITH_RECRUITMENT_DEADLINE(LocalDateTime.now() + .plusMinutes(59)); assertThatThrownBy(() -> roomService.create(manager.getId(), request)) .asInstanceOf(InstanceOfAssertFactories.type(CoreaException.class)) @@ -92,7 +93,9 @@ void invalidRecruitmentDeadline() { @Test @DisplayName("방을 생성할 때 리뷰 마감 시간은 모집 마감 시간보다 1일 이후가 아니라면 예외가 발생한다.") void invalidReviewDeadline() { - RoomCreateRequest request = RoomFixture.ROOM_CREATE_REQUEST(LocalDateTime.now().plusHours(2), LocalDateTime.now().plusDays(1)); + RoomCreateRequest request = RoomFixture.ROOM_CREATE_REQUEST(LocalDateTime.now() + .plusHours(2), LocalDateTime.now() + .plusDays(1)); assertThatThrownBy(() -> roomService.create(manager.getId(), request)) .asInstanceOf(InstanceOfAssertFactories.type(CoreaException.class)) @@ -111,6 +114,22 @@ void delete() { assertThat(roomRepository.findById(roomId)).isEmpty(); } + @Test + @DisplayName("방이 닫힌 상태면 삭제할 수 없다.") + void throw_exception_when_delete_with_room_is_closed() { + Room room = roomRepository.save(RoomFixture.ROOM_DOMAIN_WITH_CLOSED(manager)); + assertThatThrownBy(() -> roomService.delete(room.getId(), manager.getId())) + .isInstanceOf(CoreaException.class); + } + + @Test + @DisplayName("방이 진행 상태면 삭제할 수 없다.") + void throw_exception_when_delete_with_room_is_progress() { + Room room = roomRepository.save(RoomFixture.ROOM_DOMAIN_WITH_PROGRESS(manager)); + assertThatThrownBy(() -> roomService.delete(room.getId(), manager.getId())) + .isInstanceOf(CoreaException.class); + } + @Test @DisplayName("방을 생성한 유저가 아닌 사람이 방을 삭제하려고 하면 예외가 발생한다.") void invalidDelete() { @@ -121,7 +140,7 @@ void invalidDelete() { assertThatThrownBy(() -> roomService.delete(response.id(), member.getId())) .asInstanceOf(InstanceOfAssertFactories.type(CoreaException.class)) .extracting(CoreaException::getExceptionType) - .isEqualTo(ExceptionType.ROOM_DELETION_AUTHORIZATION_ERROR); + .isEqualTo(ExceptionType.ROOM_MODIFY_AUTHORIZATION_ERROR); } @Test @@ -133,6 +152,22 @@ void throw_exception_when_update_with_not_manager() { .isInstanceOf(CoreaException.class); } + @Test + @DisplayName("방이 닫힌 상태면 수정할 수 없다.") + void throw_exception_when_update_with_room_is_closed() { + Room room = roomRepository.save(RoomFixture.ROOM_DOMAIN_WITH_CLOSED(manager)); + assertThatThrownBy(() -> roomService.update(room.getId(), RoomFixture.ROOM_UPDATE_REQUEST(room.getId()))) + .isInstanceOf(CoreaException.class); + } + + @Test + @DisplayName("방이 진행 상태면 수정할 수 없다.") + void throw_exception_when_update_with_room_is_progress() { + Room room = roomRepository.save(RoomFixture.ROOM_DOMAIN_WITH_PROGRESS(manager)); + assertThatThrownBy(() -> roomService.update(room.getId(), RoomFixture.ROOM_UPDATE_REQUEST(room.getId()))) + .isInstanceOf(CoreaException.class); + } + @Test @DisplayName("존재하지 않는 방이면, 예외를 발생합니다.") void throw_exception_when_update_with_not_exist_room() { @@ -153,9 +188,13 @@ void findParticipants() { List members = memberRepository.saveAll(MemberFixture.SEVEN_MEMBERS()); participationRepository.save(new Participation(room, manager, MemberRole.REVIEWER, ParticipationStatus.MANAGER, room.getMatchingSize())); - participationRepository.saveAll(members.stream().map(member -> new Participation(room, member, MemberRole.BOTH, 2)).toList()); + participationRepository.saveAll(members.stream() + .map(member -> new Participation(room, member, MemberRole.BOTH, 2)) + .toList()); - matchResultRepository.saveAll(members.stream().map(member -> MatchResultFixture.MATCH_RESULT_DOMAIN(room.getId(), manager, member)).toList()); + matchResultRepository.saveAll(members.stream() + .map(member -> MatchResultFixture.MATCH_RESULT_DOMAIN(room.getId(), manager, member)) + .toList()); matchResultRepository.save(MatchResultFixture.MATCH_RESULT_DOMAIN(room.getId(), members.get(0), manager)); RoomParticipantResponses participants = roomService.findParticipants(room.getId(), manager.getId()); @@ -178,9 +217,13 @@ void findParticipants_withNoPullRequestParticipants() { List members = memberRepository.saveAll(MemberFixture.SEVEN_MEMBERS()); - participationRepository.saveAll(members.stream().map(member -> new Participation(room, member, MemberRole.BOTH, 2)).toList()); + participationRepository.saveAll(members.stream() + .map(member -> new Participation(room, member, MemberRole.BOTH, 2)) + .toList()); - matchResultRepository.saveAll(members.stream().map(member -> MatchResultFixture.MATCH_RESULT_DOMAIN(room.getId(), manager, member)).toList()); + matchResultRepository.saveAll(members.stream() + .map(member -> MatchResultFixture.MATCH_RESULT_DOMAIN(room.getId(), manager, member)) + .toList()); matchResultRepository.save(MatchResultFixture.MATCH_RESULT_DOMAIN(room.getId(), members.get(0), manager)); RoomParticipantResponses participants = assertDoesNotThrow(() -> roomService.findParticipants(room.getId(), manager.getId())); diff --git a/frontend/src/components/common/button/Button.style.ts b/frontend/src/components/common/button/Button.style.ts index b5cabf22a..8b580429f 100644 --- a/frontend/src/components/common/button/Button.style.ts +++ b/frontend/src/components/common/button/Button.style.ts @@ -39,7 +39,8 @@ const variantStyles = { background-color: ${({ theme }) => theme.COLOR.grey1}; `, confirm: css` - background-color: ${({ theme }) => theme.COLOR.primary3}; + color: ${({ theme }) => theme.COLOR.black}; + background-color: ${({ theme }) => theme.COLOR.lightGrass}; `, error: css` background-color: ${({ theme }) => theme.COLOR.error}; diff --git a/frontend/src/components/common/button/Button.tsx b/frontend/src/components/common/button/Button.tsx index 1c36ff6af..d7d49cc17 100644 --- a/frontend/src/components/common/button/Button.tsx +++ b/frontend/src/components/common/button/Button.tsx @@ -21,6 +21,7 @@ const Button = ({ $variant={disabled ? "disable" : variant} $size={size} disabled={disabled} + tabIndex={disabled ? -1 : 0} {...rest} > {children} diff --git a/frontend/src/components/common/carousel/Carousel.style.ts b/frontend/src/components/common/carousel/Carousel.style.ts index aa89c55ae..3137a96e5 100644 --- a/frontend/src/components/common/carousel/Carousel.style.ts +++ b/frontend/src/components/common/carousel/Carousel.style.ts @@ -22,11 +22,8 @@ export const CarouselItem = styled.div` export const CarouselLeftButton = styled.button<{ isLast: boolean }>` position: absolute; top: 50%; - color: ${({ theme, isLast }) => (isLast ? theme.COLOR.grey1 : theme.COLOR.black)}; - background: transparent; - outline: none; `; export const CarouselRightButton = styled.button<{ isLast: boolean }>` @@ -37,5 +34,4 @@ export const CarouselRightButton = styled.button<{ isLast: boolean }>` color: ${({ theme, isLast }) => (isLast ? theme.COLOR.grey1 : theme.COLOR.black)}; background: transparent; - outline: none; `; diff --git a/frontend/src/components/common/carousel/Carousel.tsx b/frontend/src/components/common/carousel/Carousel.tsx index 34040729c..a1e8e49c5 100644 --- a/frontend/src/components/common/carousel/Carousel.tsx +++ b/frontend/src/components/common/carousel/Carousel.tsx @@ -1,4 +1,4 @@ -import React, { Children, ReactNode, isValidElement, useState } from "react"; +import React, { Children, ReactNode, isValidElement, useEffect, useState } from "react"; import * as S from "@/components/common/carousel/Carousel.style"; import Icon from "@/components/common/icon/Icon"; @@ -18,25 +18,27 @@ const Carousel = ({ children }: CarouselProps) => { return ( - - {validChildren.map((child, index) => ( - {child} - ))} - - + - + + + {validChildren.map((child, index) => ( + {child} + ))} + ); }; diff --git a/frontend/src/components/common/checkbox/Checkbox.tsx b/frontend/src/components/common/checkbox/Checkbox.tsx index 3ff31e15b..0c1faa9c8 100644 --- a/frontend/src/components/common/checkbox/Checkbox.tsx +++ b/frontend/src/components/common/checkbox/Checkbox.tsx @@ -14,7 +14,7 @@ const Checkbox = ({ id, label, checked, onChange }: CheckboxProps) => { - {checked && } + {checked && } {label} diff --git a/frontend/src/components/common/dropdown/Dropdown.style.ts b/frontend/src/components/common/dropdown/Dropdown.style.ts index 3d4c8ca2a..acc822827 100644 --- a/frontend/src/components/common/dropdown/Dropdown.style.ts +++ b/frontend/src/components/common/dropdown/Dropdown.style.ts @@ -18,7 +18,7 @@ export const DropdownContainer = styled.div` height: 40px; `; -export const DropdownToggle = styled.div<{ $error: boolean }>` +export const DropdownToggle = styled.button<{ $error: boolean }>` display: flex; align-items: center; justify-content: space-between; @@ -30,6 +30,7 @@ export const DropdownToggle = styled.div<{ $error: boolean }>` font: ${({ theme }) => theme.TEXT.small}; color: ${({ theme }) => theme.COLOR.grey4}; + background-color: transparent; border: 1px solid ${(props) => (props.$error ? props.theme.COLOR.error : props.theme.COLOR.grey1)}; border-radius: 6px; `; diff --git a/frontend/src/components/common/dropdown/Dropdown.tsx b/frontend/src/components/common/dropdown/Dropdown.tsx index ed6715a0c..7cdae3f1e 100644 --- a/frontend/src/components/common/dropdown/Dropdown.tsx +++ b/frontend/src/components/common/dropdown/Dropdown.tsx @@ -1,5 +1,6 @@ import useDropdown from "@/hooks/common/useDropdown"; import * as S from "@/components/common/dropdown/Dropdown.style"; +import FocusTrap from "@/components/common/focusTrap/FocusTrap"; import Icon from "@/components/common/icon/Icon"; export interface DropdownItem { @@ -36,17 +37,23 @@ const Dropdown = ({ {isDropdownOpen && ( - - {dropdownItems.map((item) => ( - handleDropdownItemClick(item.value)} - $isSelected={item.value === selectedCategory} - > - {item.text} - - ))} - + handleToggleDropdown()}> + + {dropdownItems.map((item) => ( + handleDropdownItemClick(item.value)} + $isSelected={item.value === selectedCategory} + tabIndex={0} + onKeyDown={(e) => { + if (e.key === "Enter") handleDropdownItemClick(item.value); + }} + > + {item.text} + + ))} + + )} diff --git a/frontend/src/components/common/focusTrap/FocusTrap.tsx b/frontend/src/components/common/focusTrap/FocusTrap.tsx new file mode 100644 index 000000000..2282ccdd3 --- /dev/null +++ b/frontend/src/components/common/focusTrap/FocusTrap.tsx @@ -0,0 +1,95 @@ +import { Children, cloneElement, useEffect, useRef } from "react"; + +interface FocusTrapProps extends React.HTMLAttributes { + children: React.ReactElement; + onEscapeFocusTrap: () => void; +} + +const getFocusableElements = ( + element: HTMLElement | ChildNode | null, + result: HTMLElement[] = [], +) => { + if (!element || !element.childNodes) return result; + + for (const childNode of element.childNodes) { + const childElement = childNode as HTMLElement; + if (childElement.tabIndex >= 0) { + result.push(childElement); + } + getFocusableElements(childElement, result); + } + + return result; +}; + +const FocusTrap = (props: FocusTrapProps) => { + const { children, onEscapeFocusTrap, ...others } = props; + const child = Children.only(children); + + const focusTrapRef = useRef(null); + const focusableElements = useRef<(HTMLElement | null)[]>([]); + const currentFocusIndex = useRef(-1); + + const Compo = cloneElement(child, { + ...{ ...others, ...child?.props }, + tabIndex: -1, + ref: focusTrapRef, + }); + + const focusNextElement = () => { + currentFocusIndex.current = (currentFocusIndex.current + 1) % focusableElements.current.length; + focusableElements.current[currentFocusIndex.current]?.focus(); + }; + + const focusPrevElement = () => { + currentFocusIndex.current = + (currentFocusIndex.current - 1 + focusableElements.current.length) % + focusableElements.current.length; + + focusableElements.current[currentFocusIndex.current]?.focus(); + }; + + const handleTabKeyDown = (event: KeyboardEvent) => { + const isTabKeyDown = !event.shiftKey && event.key === "Tab"; + if (!isTabKeyDown) return; + + event.preventDefault(); + focusNextElement(); + }; + + const handleShiftTabKeyDown = (event: KeyboardEvent) => { + const isShiftTabKeyDown = event.shiftKey && event.key === "Tab"; + if (!isShiftTabKeyDown) return; + + event.preventDefault(); + focusPrevElement(); + }; + + const handleEscapeKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + onEscapeFocusTrap(); + } + }; + + useEffect(() => { + const handleKeyPress = (event: KeyboardEvent) => { + handleTabKeyDown(event); + handleEscapeKeyDown(event); + handleShiftTabKeyDown(event); + }; + if (focusTrapRef.current) { + focusableElements.current = getFocusableElements(focusTrapRef.current); + } + + document.addEventListener("keydown", handleKeyPress); + + return () => { + focusableElements.current = []; + document.removeEventListener("keydown", handleKeyPress); + }; + }, []); + + return <>{Compo}; +}; + +export default FocusTrap; diff --git a/frontend/src/components/common/header/Header.style.ts b/frontend/src/components/common/header/Header.style.ts index 6e2981f46..981d174fd 100644 --- a/frontend/src/components/common/header/Header.style.ts +++ b/frontend/src/components/common/header/Header.style.ts @@ -30,7 +30,7 @@ export const HeaderContainer = styled.header<{ $isMain: boolean }>` `; // 서비스 로고 -export const HeaderLogo = styled.button` +export const HeaderLogo = styled.div` display: flex; gap: 0.8rem; align-items: center; diff --git a/frontend/src/components/common/header/Header.tsx b/frontend/src/components/common/header/Header.tsx index 291f021ab..04c7301d1 100644 --- a/frontend/src/components/common/header/Header.tsx +++ b/frontend/src/components/common/header/Header.tsx @@ -1,6 +1,6 @@ import ProfileDropdown from "./ProfileDropdown"; import { useEffect, useState } from "react"; -import { useLocation, useNavigate } from "react-router-dom"; +import { Link, useLocation } from "react-router-dom"; import * as S from "@/components/common/header/Header.style"; import { githubAuthUrl } from "@/config/githubAuthUrl"; @@ -17,16 +17,10 @@ const headerItems = [ const Header = () => { const { pathname } = useLocation(); - const navigate = useNavigate(); const [isSelect, setIsSelect] = useState(""); const isLoggedIn = !!localStorage.getItem("accessToken"); const isMain = pathname === "/"; - const handlePage = (path: string, name: string) => { - setIsSelect(name); - navigate(path); - }; - useEffect(() => { const currentItem = headerItems.find((item) => item.path === pathname); if (currentItem) { @@ -36,31 +30,28 @@ const Header = () => { } }, [pathname, headerItems]); - const handleLogin = () => { - window.open(githubAuthUrl, "_self"); - }; - return ( - handlePage("/", "")}> - CoReA + + + CoReA + {headerItems.map((item) => ( handlePage(item.path, item.name)} className={isSelect === item.name ? "selected" : ""} > - {item.name} + {item.name} ))} {isLoggedIn ? ( ) : ( - - 로그인 + + 로그인 )} diff --git a/frontend/src/components/common/header/ProfileDropdown.style.ts b/frontend/src/components/common/header/ProfileDropdown.style.ts index 5ffa6f122..d922b8d5a 100644 --- a/frontend/src/components/common/header/ProfileDropdown.style.ts +++ b/frontend/src/components/common/header/ProfileDropdown.style.ts @@ -15,12 +15,12 @@ export const ProfileContainer = styled.div` position: relative; `; -export const DropdownMenu = styled.div<{ show: boolean }>` +export const DropdownMenu = styled.div` position: absolute; z-index: 1; right: 0; - display: ${({ show }) => (show ? "flex" : "none")}; + display: flex; flex-direction: column; min-width: 200px; diff --git a/frontend/src/components/common/header/ProfileDropdown.tsx b/frontend/src/components/common/header/ProfileDropdown.tsx index c7be4c238..6cd89e7d4 100644 --- a/frontend/src/components/common/header/ProfileDropdown.tsx +++ b/frontend/src/components/common/header/ProfileDropdown.tsx @@ -2,6 +2,7 @@ import Profile from "../profile/Profile"; import { useNavigate } from "react-router-dom"; import useDropdown from "@/hooks/common/useDropdown"; import useMutateAuth from "@/hooks/mutations/useMutateAuth"; +import FocusTrap from "@/components/common/focusTrap/FocusTrap"; import * as S from "@/components/common/header/ProfileDropdown.style"; const dropdownItems = [ @@ -40,24 +41,43 @@ const ProfileDropdown = () => { - - - - - {userInfo.name} - {userInfo.email !== "" ? userInfo.email : "email 비공개"} - - + {isDropdownOpen && ( + + + + + {userInfo.name} + {userInfo.email !== "" ? userInfo.email : "email 비공개"} + + - - {dropdownItems.map((item) => ( - handleDropdownItemClick(item.path)}> - {item.name} - - ))} - 로그아웃 - - + handleToggleDropdown()}> + + {dropdownItems.map((item) => ( + handleDropdownItemClick(item.path)} + tabIndex={0} + onKeyDown={(e) => { + if (e.key === "Enter") handleDropdownItemClick(item.path); + }} + > + {item.name} + + ))} + { + if (e.key === "Enter") handleLogoutClick(); + }} + > + 로그아웃 + + + + + )} ); }; diff --git a/frontend/src/components/common/iconRadioButton/IconRadioButton.style.ts b/frontend/src/components/common/iconRadioButton/IconRadioButton.style.ts index 2dd75ab83..e55ca427a 100644 --- a/frontend/src/components/common/iconRadioButton/IconRadioButton.style.ts +++ b/frontend/src/components/common/iconRadioButton/IconRadioButton.style.ts @@ -39,5 +39,5 @@ export const IconRadioButtonBox = styled.div` `; export const IconRadioButtonText = styled.span` - font: ${({ theme }) => theme.TEXT.xSmall}; + font: ${({ theme }) => theme.TEXT.semiSmall}; `; diff --git a/frontend/src/components/common/iconRadioButton/IconRadioButton.tsx b/frontend/src/components/common/iconRadioButton/IconRadioButton.tsx index b79b8400a..90b217208 100644 --- a/frontend/src/components/common/iconRadioButton/IconRadioButton.tsx +++ b/frontend/src/components/common/iconRadioButton/IconRadioButton.tsx @@ -32,6 +32,7 @@ const IconRadioButton = ({ checked={isSelected} onChange={handleChange} {...rest} + tabIndex={-1} /> { - const [isFallback, setIsFallback] = useState(false); + const [isFallback, setIsFallback] = useState(!src); const handleError = (e: React.SyntheticEvent) => { const img = e.target as HTMLImageElement; @@ -24,7 +24,13 @@ const ImageWithFallback = ({ }; return ( - + ); }; diff --git a/frontend/src/components/common/modal/Modal.tsx b/frontend/src/components/common/modal/Modal.tsx index 45f1d7f3c..9b762a9f8 100644 --- a/frontend/src/components/common/modal/Modal.tsx +++ b/frontend/src/components/common/modal/Modal.tsx @@ -1,5 +1,6 @@ import { CSSProperties, MouseEvent, ReactNode, useEffect, useState } from "react"; import { createPortal } from "react-dom"; +import FocusTrap from "@/components/common/focusTrap/FocusTrap"; import * as S from "@/components/common/modal/Modal.style"; const portalElement = document.getElementById("modal") as HTMLElement; @@ -51,8 +52,17 @@ const Modal = ({ isOpen, onClose, hasCloseButton = true, style, children }: Moda onClick={handleModalContainerClick} style={style} > - {hasCloseButton && ×} - {children} + { + handleModalClose(); + }} + > +
+ {children} + {hasCloseButton && ×} +
+
+ , , portalElement, diff --git a/frontend/src/components/common/optionSelect/OptionSelect.style.ts b/frontend/src/components/common/optionSelect/OptionSelect.style.ts index e7755816a..6888d44e4 100644 --- a/frontend/src/components/common/optionSelect/OptionSelect.style.ts +++ b/frontend/src/components/common/optionSelect/OptionSelect.style.ts @@ -16,7 +16,6 @@ export const Option = styled.button<{ $isSelected: boolean }>` color: ${({ $isSelected, theme }) => ($isSelected ? theme.COLOR.black : theme.COLOR.grey3)}; background: transparent; - outline: none; @media screen and (max-width: 520px) { flex: 1%; diff --git a/frontend/src/components/common/optionSelect/OptionSelect.tsx b/frontend/src/components/common/optionSelect/OptionSelect.tsx index 3b45cbd5b..0c4d69d24 100644 --- a/frontend/src/components/common/optionSelect/OptionSelect.tsx +++ b/frontend/src/components/common/optionSelect/OptionSelect.tsx @@ -1,7 +1,7 @@ import * as S from "@/components/common/optionSelect/OptionSelect.style"; import { NonEmptyArray } from "@/@types/NonEmptyArray"; -interface OptionSelect> { +interface OptionSelectProps> { selected: T[number]; options: T; handleSelectedOption: (option: T[number]) => void; @@ -11,7 +11,7 @@ const OptionSelect = >({ selected, options, handleSelectedOption, -}: OptionSelect) => { +}: OptionSelectProps) => { const selectedIndex = options.indexOf(selected); return ( diff --git a/frontend/src/components/common/textarea/Textarea.style.ts b/frontend/src/components/common/textarea/Textarea.style.ts index 518a570e7..aeedfc9d0 100644 --- a/frontend/src/components/common/textarea/Textarea.style.ts +++ b/frontend/src/components/common/textarea/Textarea.style.ts @@ -1,7 +1,11 @@ import styled from "styled-components"; export const TextareaWrapper = styled.div` - position: relative; + display: flex; + flex-direction: column; + gap: 0.8rem; + align-items: flex-end; + width: 100%; `; @@ -36,10 +40,6 @@ export const StyledTextarea = styled.textarea<{ $error: boolean }>` `; export const CharCount = styled.div` - position: absolute; - right: 10px; - bottom: -20px; - font: ${({ theme }) => theme.TEXT.xSmall}; color: ${({ theme }) => theme.COLOR.grey2}; `; diff --git a/frontend/src/components/feedback/evaluationPointBar/EvaluationPointBar.style.ts b/frontend/src/components/feedback/evaluationPointBar/EvaluationPointBar.style.ts index 09c120023..18078db24 100644 --- a/frontend/src/components/feedback/evaluationPointBar/EvaluationPointBar.style.ts +++ b/frontend/src/components/feedback/evaluationPointBar/EvaluationPointBar.style.ts @@ -2,8 +2,8 @@ import styled from "styled-components"; export const BarContainer = styled.div` display: flex; - gap: 1.5rem; - align-items: center; + gap: 1.6rem; + align-items: flex-start; justify-content: center; width: 100%; diff --git a/frontend/src/components/feedback/evaluationPointBar/EvaluationPointBar.tsx b/frontend/src/components/feedback/evaluationPointBar/EvaluationPointBar.tsx index c5e7425f1..8ae03f901 100644 --- a/frontend/src/components/feedback/evaluationPointBar/EvaluationPointBar.tsx +++ b/frontend/src/components/feedback/evaluationPointBar/EvaluationPointBar.tsx @@ -11,11 +11,11 @@ interface EvaluationOption { } const evaluationOptions: EvaluationOption[] = [ - { text: "나쁨", value: 1, icon: "bad" }, - { text: "아쉬움", value: 2, icon: "disappointing" }, - { text: "보통", value: 3, icon: "average" }, - { text: "만족", value: 4, icon: "satisfied" }, - { text: "매우 만족", value: 5, icon: "verySatisfied" }, + { text: "아쉬움", value: 1, icon: "bad" }, + { text: "", value: 2, icon: "disappointing" }, + { text: "", value: 3, icon: "average" }, + { text: "", value: 4, icon: "satisfied" }, + { text: "만족", value: 5, icon: "verySatisfied" }, ]; interface EvaluationPointBarProps { @@ -23,6 +23,7 @@ interface EvaluationPointBarProps { readonly?: boolean; onChange?: (value: number) => void; color?: string; + isTabFocusable?: boolean; } const EvaluationPointBar = ({ @@ -30,6 +31,7 @@ const EvaluationPointBar = ({ readonly = false, color, onChange, + isTabFocusable = true, }: EvaluationPointBarProps) => { const [selectedOptionId, setSelectedOptionId] = useState(initialOptionId); @@ -56,6 +58,10 @@ const EvaluationPointBar = ({ isSelected={selectedOptionId === option.value} color={color} onChange={handleRadioChange} + tabIndex={isTabFocusable ? 0 : -1} + onKeyDown={(e) => { + if (e.key === "Enter") handleRadioChange(option.value); + }} > diff --git a/frontend/src/components/feedback/feedbackCard/FeedbackCard.style.ts b/frontend/src/components/feedback/feedbackCard/FeedbackCard.style.ts index 5d29f09fa..bf0b0d152 100644 --- a/frontend/src/components/feedback/feedbackCard/FeedbackCard.style.ts +++ b/frontend/src/components/feedback/feedbackCard/FeedbackCard.style.ts @@ -1,4 +1,5 @@ import styled from "styled-components"; +import { EllipsisText } from "@/styles/common"; import media from "@/styles/media"; export const FeedbackCardContainer = styled.div<{ $isTypeDevelop: boolean }>` @@ -18,6 +19,7 @@ export const FeedbackCardContainer = styled.div<{ $isTypeDevelop: boolean }>` ${media.medium` width: 100%; + max-width: 420px; `} ${media.small` @@ -28,18 +30,23 @@ export const FeedbackCardContainer = styled.div<{ $isTypeDevelop: boolean }>` export const FeedbackScoreContainer = styled.div` display: flex; flex-direction: column; - gap: 1.2rem; + gap: 1.6rem; `; export const FeedbackKeywordContainer = styled.div` - height: 130px; + display: flex; + flex-direction: column; + gap: 1.6rem; + height: fit-content; `; export const FeedbackKeywordWrapper = styled.div` display: flex; flex-wrap: wrap; gap: 1rem; - margin-top: 1rem; + align-content: flex-start; + + height: 122px; `; export const FeedbackHeader = styled.div` @@ -85,8 +92,11 @@ export const FeedbackSubTitle = styled.span` `; export const FeedbackKeyword = styled.div` + height: fit-content; padding: 1rem; - font: ${({ theme }) => theme.TEXT.small}; + + font: ${({ theme }) => theme.TEXT.semiSmall}; + background: ${({ theme }) => theme.COLOR.grey0}; border-radius: 5px; `; @@ -95,18 +105,15 @@ export const FeedbackDetailContainer = styled.div` overflow: hidden; display: flex; flex-direction: column; - gap: 1rem; + gap: 1.6rem; height: 200px; `; export const FeedbackDetail = styled.p` - overflow: hidden auto; - height: 172px; - font: ${({ theme }) => theme.TEXT.small}; line-height: 2.2rem; - text-overflow: ellipsis; - white-space: break-spaces; + + ${EllipsisText} `; diff --git a/frontend/src/components/feedback/feedbackCard/FeedbackCard.tsx b/frontend/src/components/feedback/feedbackCard/FeedbackCard.tsx index e546eeec7..3cc737daf 100644 --- a/frontend/src/components/feedback/feedbackCard/FeedbackCard.tsx +++ b/frontend/src/components/feedback/feedbackCard/FeedbackCard.tsx @@ -1,7 +1,8 @@ -import * as S from "./FeedbackCard.style"; import { FeedbackType } from "@/hooks/feedback/useSelectedFeedbackData"; import Profile from "@/components/common/profile/Profile"; +import { Textarea } from "@/components/common/textarea/Textarea"; import EvaluationPointBar from "@/components/feedback/evaluationPointBar/EvaluationPointBar"; +import * as S from "@/components/feedback/feedbackCard/FeedbackCard.style"; import { FeedbackCardData } from "@/@types/feedback"; import { HoverStyledLink } from "@/styles/common"; import { theme } from "@/styles/theme"; @@ -22,9 +23,9 @@ const FeedbackCard = ({ return ( - + - + {feedbackCardData.username} @@ -42,14 +43,17 @@ const FeedbackCard = ({ )} + 피드백 점수 + 피드백 키워드 @@ -58,11 +62,16 @@ const FeedbackCard = ({ ))} + 세부 피드백 - - {feedbackCardData.feedbackText.length ? feedbackCardData.feedbackText : "없음"} - +