Skip to content

Commit

Permalink
Merge pull request #57 from softeerbootcamp4th/feature/54-fix-api
Browse files Browse the repository at this point in the history
[fix] 더미 데이터 생성 및 주요 API 로컬에서 검증 (#54)
  • Loading branch information
win-luck authored Aug 13, 2024
2 parents bb8b776 + 525007b commit bc338ae
Show file tree
Hide file tree
Showing 15 changed files with 204 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;


@RequestMapping("/api/v1/admin/auth")
@RequiredArgsConstructor
@RestController
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@
import hyundai.softeer.orange.core.auth.Auth;
import hyundai.softeer.orange.core.auth.AuthRole;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;


@RequestMapping("/api/v1/admin/comments")
@RequiredArgsConstructor
@RestController
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

import hyundai.softeer.orange.comment.dto.CreateCommentDto;
import hyundai.softeer.orange.comment.dto.ResponseCommentsDto;
import hyundai.softeer.orange.comment.service.ApiService;
import hyundai.softeer.orange.comment.service.CommentService;
import hyundai.softeer.orange.common.ErrorResponse;
import hyundai.softeer.orange.core.auth.Auth;
import hyundai.softeer.orange.core.auth.AuthRole;
import hyundai.softeer.orange.eventuser.component.EventUserAnnotation;
import hyundai.softeer.orange.eventuser.dto.EventUserInfo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
Expand All @@ -28,7 +28,6 @@
public class CommentController {

private final CommentService commentService;
private final ApiService apiService;

@Tag(name = "Comment")
@GetMapping("/{eventFrameId}")
Expand All @@ -53,9 +52,8 @@ public ResponseEntity<ResponseCommentsDto> getComments(@PathVariable String even
@ApiResponse(responseCode = "409", description = "하루에 여러 번의 기대평을 작성하려 할 때",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
public ResponseEntity<Boolean> createComment(@EventUserAnnotation EventUserInfo userInfo, @PathVariable String eventFrameId, @RequestBody @Valid CreateCommentDto dto) {
boolean isPositive = apiService.analyzeComment(dto.getContent());
return ResponseEntity.ok(commentService.createComment(userInfo.getUserId(), eventFrameId, dto, isPositive));
public ResponseEntity<Boolean> createComment(@Parameter(hidden = true) @EventUserAnnotation EventUserInfo userInfo, @PathVariable String eventFrameId, @RequestBody @Valid CreateCommentDto dto) {
return ResponseEntity.ok(commentService.createComment(userInfo.getUserId(), eventFrameId, dto));
}

@Auth(AuthRole.event_user)
Expand All @@ -67,7 +65,7 @@ public ResponseEntity<Boolean> createComment(@EventUserAnnotation EventUserInfo
@ApiResponse(responseCode = "404", description = "해당 정보를 갖는 유저가 존재하지 않을 때",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
public ResponseEntity<Boolean> isCommentable(@EventUserAnnotation EventUserInfo userInfo) {
public ResponseEntity<Boolean> isCommentable(@Parameter(hidden = true) @EventUserAnnotation EventUserInfo userInfo) {
return ResponseEntity.ok(commentService.isCommentable(userInfo.getUserId()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
import lombok.Getter;
import lombok.NoArgsConstructor;

@AllArgsConstructor
import java.time.LocalDateTime;

@NoArgsConstructor
@Builder
@Getter
Expand All @@ -15,14 +16,22 @@ public class ResponseCommentDto {
private Long id;
private String content;
private String userName;
private String createdAt;
private LocalDateTime createdAt;

// CommentRepository에서 Projection으로 ResponseCommentDto를 생성할 때 사용, 추후 더 나은 방법으로 수정 필요
public ResponseCommentDto(Long id, String content, String userName, LocalDateTime createdAt) {
this.id = id;
this.content = content;
this.userName = userName;
this.createdAt = createdAt;
}

public static ResponseCommentDto from(Comment comment) {
return ResponseCommentDto.builder()
.id(comment.getId())
.content(comment.getContent())
.userName(comment.getEventUser().getUserName())
.createdAt(comment.getCreatedAt().toString())
.createdAt(comment.getCreatedAt())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package hyundai.softeer.orange.comment.repository;

import hyundai.softeer.orange.comment.dto.ResponseCommentDto;
import hyundai.softeer.orange.comment.dto.WriteCommentCountDto;
import hyundai.softeer.orange.comment.entity.Comment;
import org.springframework.data.domain.Page;
Expand All @@ -12,12 +13,16 @@

public interface CommentRepository extends JpaRepository<Comment, Long> {

// DB 상에서 무작위로 추출된 n개의 긍정 기대평 목록을 조회
@Query(value = "SELECT * FROM comment WHERE event_frame_id = :eventFrameId AND is_positive = true ORDER BY RAND() LIMIT :n", nativeQuery = true)
List<Comment> findRandomPositiveComments(Long eventFrameId, @Param("n") int n);
// DB 상에서 무작위로 추출된 n개의 긍정 기대평 목록을 조회하고 Dto로 반환, N+1 문제 방지 (JPQL)
@Query("select new hyundai.softeer.orange.comment.dto.ResponseCommentDto(c.id, c.content, cu.userName, c.createdAt) " +
"from Comment c " +
"JOIN c.eventUser cu " +
"where c.eventFrame.id = :eventFrameId and c.isPositive = true " +
"order by function('RAND')")
List<ResponseCommentDto> findRandomPositiveComments(Long eventFrameId, Pageable pageable);

// 오늘 날짜 기준으로 이미 유저의 기대평이 등록되어 있는지 확인
@Query(value = "SELECT COUNT(*) FROM comment WHERE event_user_id = :eventUserId AND DATE(createdAt) = CURDATE()", nativeQuery = true)
// 오늘 날짜 기준으로 이미 유저의 기대평이 등록되어 있는지 확인 (JPQL)
@Query("SELECT (COUNT(c) > 0) FROM Comment c WHERE c.eventUser.id = :eventUserId AND FUNCTION('DATE', c.createdAt) = CURRENT_DATE")
boolean existsByCreatedDateAndEventUser(@Param("eventUserId") Long eventUserId);

@Query(value = "SELECT c.* FROM comment c " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,34 +29,32 @@ public class CommentService {
private final CommentRepository commentRepository;
private final EventFrameRepository eventFrameRepository;
private final EventUserRepository eventUserRepository;
private final CommentValidator commentValidator;

// 주기적으로 무작위 추출되는 긍정 기대평 목록을 조회한다.
@Transactional(readOnly = true)
@Cacheable(value = "comments", key = ConstantUtil.COMMENTS_KEY + " + #eventFrameId")
public ResponseCommentsDto getComments(String eventFrameId) {
EventFrame frame = eventFrameRepository.findByFrameId(eventFrameId)
.orElseThrow(() -> new CommentException(ErrorCode.EVENT_FRAME_NOT_FOUND));
List<ResponseCommentDto> comments = commentRepository.findRandomPositiveComments(frame.getId(), ConstantUtil.COMMENTS_SIZE)
.stream()
.map(ResponseCommentDto::from)
.toList();
log.info("fetching comments of {}", eventFrameId);
EventFrame frame = getEventFrame(eventFrameId);
List<ResponseCommentDto> comments = commentRepository.findRandomPositiveComments(frame.getId(), PageRequest.of(0, ConstantUtil.COMMENTS_SIZE));
log.info("comments of {} fetched from DB to Redis", eventFrameId);
return new ResponseCommentsDto(comments);
}

// 신규 기대평을 등록한다.
@Transactional
public Boolean createComment(String userId, String eventFrameId, CreateCommentDto dto, Boolean isPositive) {
EventUser eventUser = eventUserRepository.findByUserId(userId)
.orElseThrow(() -> new CommentException(ErrorCode.EVENT_USER_NOT_FOUND));
EventFrame eventFrame = eventFrameRepository.findByFrameId(eventFrameId)
.orElseThrow(() -> new CommentException(ErrorCode.EVENT_FRAME_NOT_FOUND));
public Boolean createComment(String userId, String eventFrameId, CreateCommentDto dto) {
EventUser eventUser = getEventUser(userId);
EventFrame eventFrame = getEventFrame(eventFrameId);

// 하루에 여러 번의 기대평을 작성하려 할 때 예외처리
if(commentRepository.existsByCreatedDateAndEventUser(eventUser.getId())) {
throw new CommentException(ErrorCode.COMMENT_ALREADY_EXISTS);
}

boolean isPositive = commentValidator.analyzeComment(dto.getContent());

// TODO: 점수정책와 연계하여 기대평 등록 시 점수를 부여 추가해야함
Comment comment = Comment.of(dto.getContent(), eventFrame, eventUser, isPositive);
commentRepository.save(comment);
Expand All @@ -67,8 +65,7 @@ public Boolean createComment(String userId, String eventFrameId, CreateCommentDt
// 오늘 이 유저가 기대평을 작성할 수 있는지 여부를 조회한다.
@Transactional(readOnly = true)
public Boolean isCommentable(String userId) {
EventUser eventUser = eventUserRepository.findByUserId(userId)
.orElseThrow(() -> new CommentException(ErrorCode.EVENT_USER_NOT_FOUND));
EventUser eventUser = getEventUser(userId);
log.info("checking commentable of user {}", eventUser.getUserId());
return !commentRepository.existsByCreatedDateAndEventUser(eventUser.getId());
}
Expand All @@ -91,6 +88,7 @@ public void deleteComments(List<Long> commentIds) {
log.info("deleted comments: {}", commentIds);
}

@Transactional(readOnly = true)
public ResponseCommentsDto searchComments(String eventId, Integer page, Integer size) {
PageRequest pageInfo = PageRequest.of(page, size);

Expand All @@ -99,4 +97,14 @@ public ResponseCommentsDto searchComments(String eventId, Integer page, Integer
log.info("searched comments: {}", comments);
return new ResponseCommentsDto(comments);
}

private EventUser getEventUser(String userId) {
return eventUserRepository.findByUserId(userId)
.orElseThrow(() -> new CommentException(ErrorCode.EVENT_USER_NOT_FOUND));
}

private EventFrame getEventFrame(String eventFrameId) {
return eventFrameRepository.findByFrameId(eventFrameId)
.orElseThrow(() -> new CommentException(ErrorCode.EVENT_FRAME_NOT_FOUND));
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package hyundai.softeer.orange.comment.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import hyundai.softeer.orange.comment.exception.CommentException;
Expand All @@ -17,41 +16,66 @@
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.Map;

@RequiredArgsConstructor
@Service
public class ApiService {
public class CommentValidator {

private static final Logger log = LoggerFactory.getLogger(ApiService.class);
private static final Logger log = LoggerFactory.getLogger(CommentValidator.class);
private final RestTemplate restTemplate = new RestTemplate();
private final NaverApiConfig naverApiConfig;
private final ObjectMapper objectMapper;

public boolean analyzeComment(String content) {
String responseBody = sendSentimentAnalysisRequest(content);
return parseSentimentAnalysisResponse(responseBody, content);
}

private String sendSentimentAnalysisRequest(String content) {
HttpHeaders headers = createHeaders();
String requestJson = createRequestBody(content);

HttpEntity<String> requestEntity = new HttpEntity<>(requestJson, headers);
log.info("comment <{}> sentiment analysis request to Naver API", content);

ResponseEntity<String> responseEntity = restTemplate.postForEntity(naverApiConfig.getUrl(), requestEntity, String.class);
return responseEntity.getBody();
}

private HttpHeaders createHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.set(ConstantUtil.CLIENT_ID, naverApiConfig.getClientId());
headers.set(ConstantUtil.CLIENT_SECRET, naverApiConfig.getClientSecret());
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> requestEntity = new HttpEntity<>(content, headers);
return headers;
}

RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> responseEntity = restTemplate.postForEntity(naverApiConfig.getUrl(), requestEntity, String.class);
String responseBody = responseEntity.getBody();
boolean isPositive = true;
private String createRequestBody(String content) {
Map<String, String> requestBody = Map.of("content", content);
try {
return objectMapper.writeValueAsString(requestBody);
} catch (Exception e) {
throw new CommentException(ErrorCode.INVALID_JSON);
}
}

ObjectMapper objectMapper = new ObjectMapper();
private boolean parseSentimentAnalysisResponse(String responseBody, String content) {
try {
JsonNode rootNode = objectMapper.readTree(responseBody);

String sentiment = rootNode.path("document").path("sentiment").asText();
log.info("comment <{}> sentiment analysis result: {}", content, sentiment);

if (sentiment.equals("negative")) {
isPositive = false;
double documentNegativeConfidence = rootNode.path("document").path("confidence").path("negative").asDouble();
if (documentNegativeConfidence >= ConstantUtil.LIMIT_NEGATIVE_CONFIDENCE) { // 부정이며 확률이 99.5% 이상일 경우 재작성 요청
double negativeConfidence = rootNode.path("document").path("confidence").path("negative").asDouble();
if (negativeConfidence >= ConstantUtil.LIMIT_NEGATIVE_CONFIDENCE) {
throw new CommentException(ErrorCode.INVALID_COMMENT);
}
return false;
}
} catch (JsonProcessingException e) {
return true;
} catch (Exception e) {
throw new CommentException(ErrorCode.INVALID_JSON);
}
log.info("comment <{}> sentiment analysis result: {}", content, isPositive);
return isPositive;
}
}
9 changes: 6 additions & 3 deletions src/main/java/hyundai/softeer/orange/config/RedisConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import hyundai.softeer.orange.comment.dto.ResponseCommentsDto;
import hyundai.softeer.orange.core.ParseUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
Expand All @@ -18,9 +19,12 @@

import java.time.Duration;

@RequiredArgsConstructor
@EnableCaching
@Configuration
public class RedisConfig {
private final ObjectMapper objectMapper;

@Bean
public ParseUtil parseUtil(ObjectMapper objectMapper) {
return new ParseUtil(objectMapper);
Expand All @@ -33,7 +37,6 @@ public RedisTemplate<String, ResponseCommentsDto> redisTemplate(RedisConnectionF
template.setKeySerializer(new StringRedisSerializer());

// ObjectMapper 설정
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.activateDefaultTyping(
objectMapper.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL);
Expand Down Expand Up @@ -84,10 +87,10 @@ public RedisTemplate<String, Integer> numberRedisTemplate(RedisConnectionFactory
}

@Bean
public CacheManager diareatCacheManager(RedisConnectionFactory cf) {
public CacheManager cacheManager(RedisConnectionFactory cf) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)))
.entryTtl(Duration.ofMinutes(120L)); // 캐시 만료 시간 2시간
return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf).cacheDefaults(redisCacheConfiguration).build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
/**
* 이벤트 관련 CRUD를 다루는 API
*/

@Auth({AuthRole.admin})
@Slf4j
@RequiredArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@Tag(name = "DrawEvent", description = "인터렉션 및 추첨 이벤트 관련 API")
@RequiredArgsConstructor
@RequestMapping("/api/v1/event/draw")
@RestController
Expand Down
Loading

0 comments on commit bc338ae

Please sign in to comment.