Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ public class NewsController {
@Operation(summary = "뉴스 헤드라인 생성 API", description = "뉴스 헤드라인을 생성하는 API입니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "뉴스 헤드라인 생성 성공",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = List.class))),
content = @Content(mediaType = "application/json",
schema = @Schema(type = "array", implementation = HeadLineRes.class))),
@ApiResponse(responseCode = "400", description = "뉴스 헤드라인 생성 실패",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
})
Expand Down Expand Up @@ -73,10 +74,11 @@ public ResponseEntity<?> createNews(
}


@Operation(summary = "뉴스 추천 기록 조회 API", description = "뉴스 생성 시 최근 생성된 5개의 동사-명사 쌍 목록을 조회합니다. ")
@Operation(summary = "뉴스 추천 키워드 조회 API", description = "뉴스 생성 시 최근 생성된 5개의 동사-명사 쌍 목록을 조회합니다. ")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "뉴스 추천 기록 조회 성공",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = List.class))),
content = @Content(mediaType = "application/json",
schema = @Schema(type = "array", implementation = RecentKeywordsRes.class))),
@ApiResponse(responseCode = "400", description = "뉴스 추천 기록 조회 실패",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
})
Expand All @@ -91,4 +93,7 @@ public ResponseEntity<?> getRecentKeywords(






}
4 changes: 2 additions & 2 deletions src/main/java/com/movelog/domain/record/domain/Keyword.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ public class Keyword extends BaseEntity {
@JoinColumn(name = "user_id")
private User user;

@OneToMany(mappedBy = "keyword")
@OneToMany(mappedBy = "keyword", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Record> records = new ArrayList<>();

@OneToMany(mappedBy = "keyword")
@OneToMany(mappedBy = "keyword", cascade = CascadeType.ALL, orphanRemoval = true)
private List<News> news = new ArrayList<>();

@Builder
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.movelog.domain.record.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class RecentRecordImagesRes {

@Schema( type = "String", example ="https://movelog.s3.ap-northeast-2.amazonaws.com/record/2021-08-01/1.jpg", description="최근 기록 이미지 URL")
private String imageUrl;

@Schema( type = "LocalDateTime", example ="2021-08-01T00:00:00", description="최근 기록 이미지 생성 시간")
private LocalDateTime createdAt;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.movelog.domain.record.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class TodayRecordStatus {

@Schema( type = "boolean", example ="true", description="했어요에 대한 기록이 있는지에 대한 여부")
private boolean isDo;

@Schema( type = "boolean", example ="false", description="먹었어요에 대한 기록이 있는지에 대한 여부")
private boolean isEat;

@Schema( type = "boolean", example ="true", description="갔어요에 대한 기록이 있는지에 대한 여부")
private boolean isGo;

}
Original file line number Diff line number Diff line change
@@ -1,46 +1,85 @@
package com.movelog.domain.record.presentation;

import com.movelog.domain.record.dto.request.CreateRecordReq;
import com.movelog.domain.record.dto.response.RecentRecordImagesRes;
import com.movelog.domain.record.dto.response.TodayRecordStatus;
import com.movelog.domain.record.service.RecordService;
import com.movelog.global.config.security.token.UserPrincipal;
import com.movelog.global.payload.ApiResponse;
import com.movelog.global.payload.Message;
import com.movelog.global.util.ApiResponseUtil;
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;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.ErrorResponse;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;
import java.util.Set;

@RestController
@RequestMapping("api/v1/record")
@RequiredArgsConstructor
public class RecordController {
private final RecordService recordService;
@Operation(summary = "기록 추가 API", description = "기록을 추가하는 API입니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "기록 추가 성공",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = Message.class))),
@ApiResponse(responseCode = "400", description = "기록 추가 실패",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(responseCode = "500", description = "기록 추가 실패(서버 에러), Request Body 내용을 확인해주세요.",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
})
@PostMapping
public ResponseEntity<ApiResponse> createRecord(
public ResponseEntity<?> createRecord(
@Parameter(description = "User의 토큰을 입력해주세요.", required = true) @AuthenticationPrincipal UserPrincipal userPrincipal,
@Parameter(description = "Schemas의 CreateRecordReq를 참고해주세요.", required = true) @RequestPart CreateRecordReq createRecordReq,
@RequestPart(value = "img", required = false) MultipartFile img) {
@RequestPart(value = "img", required = false) MultipartFile img
) {
recordService.createRecord(userPrincipal.getId(), createRecordReq, img);

ApiResponse result = ApiResponse.builder()
.check(true)
.information("기록을 추가했어요")
.build();
return ResponseEntity.ok(result);
return ResponseEntity.ok(ApiResponseUtil.success(Message.builder().message("기록이 생성되었습니다.").build()));
}

@Operation(summary = "오늘 기준 기록 현황 API", description = "오늘 기준 기록 확인하는 API입니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "오늘 기준 기록 현황 조회 성공",
content = @Content(mediaType = "application/json",
schema = @Schema(type = "array", implementation = TodayRecordStatus.class))),
@ApiResponse(responseCode = "400", description = "오늘 기준 기록 현황 조회 실패",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
})
@GetMapping("/today")
public ResponseEntity<ApiResponse> retrieveTodayRecord(
public ResponseEntity<?> retrieveTodayRecord(
@Parameter(description = "User의 토큰을 입력해주세요.", required = false) @AuthenticationPrincipal UserPrincipal userPrincipal
) {;
TodayRecordStatus result = recordService.retrieveTodayRecord(5L);
return ResponseEntity.ok(ApiResponseUtil.success(result));
}

ApiResponse result = ApiResponse.builder()
.check(true)
.information(recordService.retrieveTodayRecord(5L))
.build();
return ResponseEntity.ok(result);
@Operation(summary = "최근 기록 이미지 조회 API", description = "사용자가 선택한 명사-동사 쌍에 해당하는 최근 기록 이미지(5개)를 조회하는 API입니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "최근 기록 이미지 조회 성공",
content = @Content(mediaType = "application/json",
schema = @Schema(type = "array", implementation = RecentRecordImagesRes.class))),
@ApiResponse(responseCode = "400", description = "최근 기록 이미지 조회 실패",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
})
@GetMapping("/image/{keywordId}")
public ResponseEntity<?> retrieveRecentRecordImages(
@Parameter(description = "User의 토큰을 입력해주세요.", required = true) @AuthenticationPrincipal UserPrincipal userPrincipal,
@Parameter(description = "키워드 ID(동사-명사 쌍에 대한 ID)를 입력해주세요.", required = true) @PathVariable Long keywordId
) {
List<RecentRecordImagesRes> result = recordService.retrieveRecentRecordImages(userPrincipal, keywordId);
return ResponseEntity.ok(ApiResponseUtil.success(result));
}



}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.movelog.domain.record.domain.Keyword;
import com.movelog.domain.record.domain.Record;
import com.movelog.domain.record.domain.VerbType;
import com.movelog.domain.user.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
Expand All @@ -13,4 +14,8 @@ public interface KeywordRepository extends JpaRepository<Keyword,Long> {
List<Keyword> findByUser(User user);

List<Keyword> findTop5ByUserOrderByCreatedAtDesc(User user);

boolean existsByUserAndKeywordAndVerbType(User user, String noun, VerbType verbType);

Keyword findByUserAndKeywordAndVerbType(User user, String noun, VerbType verbType);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,8 @@

@Repository
public interface RecordRepository extends JpaRepository<Record,Long> {

List<Record> findByKeywordInAndActionTimeBetween(List<Keyword> keywords, LocalDateTime startTime, LocalDateTime endTime);

List<Record> findTop5ByKeywordOrderByActionTimeDesc(Keyword keyword);
}
83 changes: 67 additions & 16 deletions src/main/java/com/movelog/domain/record/service/RecordService.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@
import com.movelog.domain.record.domain.Record;
import com.movelog.domain.record.domain.VerbType;
import com.movelog.domain.record.dto.request.CreateRecordReq;
import com.movelog.domain.record.dto.response.RecentRecordImagesRes;
import com.movelog.domain.record.dto.response.TodayRecordStatus;
import com.movelog.domain.record.repository.KeywordRepository;
import com.movelog.domain.record.repository.RecordRepository;
import com.movelog.domain.user.domain.User;
import com.movelog.domain.user.domain.repository.UserRepository;
import com.movelog.global.config.security.token.UserPrincipal;
import com.movelog.global.util.S3Util;
import jakarta.validation.ConstraintViolation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
Expand All @@ -35,20 +39,31 @@ public class RecordService {
public void createRecord(Long userId, CreateRecordReq createRecordReq, MultipartFile img) {
User user = validUserById(userId);
// User user = validUserById(5L);
validateCreateRecordReq(createRecordReq);

String recordImgUrl = s3Util.uploadToRecordFolder(img);
log.info("recordImgUrl: {}", recordImgUrl);

String verb = createRecordReq.getVerbType();
try {
VerbType verbType = VerbType.fromValue(verb);
String noun = createRecordReq.getNoun();

Keyword keyword = Keyword.builder()
.user(user)
.keyword(createRecordReq.getNoun())
.verbType(verbType)
.build();
Keyword keyword;

keywordRepository.save(keyword);
// 사용자의 키워드에 존재하지 않는 경우 (새로 등록)
if (!keywordRepository.existsByUserAndKeywordAndVerbType(user, noun, verbType)) {
keyword = Keyword.builder()
.user(user)
.keyword(noun)
.verbType(verbType)
.build();

keywordRepository.save(keyword);
}
else{
keyword = keywordRepository.findByUserAndKeywordAndVerbType(user, noun, verbType);
}

Record record = Record.builder()
.keyword(keyword)
Expand All @@ -57,18 +72,15 @@ public void createRecord(Long userId, CreateRecordReq createRecordReq, Multipart
.build();

recordRepository.save(record);

} catch (IllegalArgumentException e) {
throw new RuntimeException("Invalid verb type: " + verb, e);
}

}

private User validUserById(Long userId) {
Optional<User> userOptional = userRepository.findById(userId);
return userOptional.get();
}

public Map<String, Boolean> retrieveTodayRecord(Long userId) {
public TodayRecordStatus retrieveTodayRecord(Long userId) {
// 유저 유효성 검사 및 조회
User user = validUserById(userId);

Expand All @@ -94,13 +106,52 @@ public Map<String, Boolean> retrieveTodayRecord(Long userId) {
log.info("Today VerbTypes: {}", todayVerbTypes);

// 모든 VerbType에 대해 존재 여부를 반환
return Arrays.stream(VerbType.values())
.collect(Collectors.toMap(
VerbType::getVerbType, // 키: VerbType의 문자열 값
todayVerbTypes::contains // 값: 오늘 VerbType에 포함 여부
));
TodayRecordStatus todayRecordStatus = TodayRecordStatus.builder()
.isDo(verbTypeExists(todayVerbTypes, VerbType.DO))
.isEat(verbTypeExists(todayVerbTypes, VerbType.EAT))
.isGo(verbTypeExists(todayVerbTypes, VerbType.GO))
.build();

return todayRecordStatus;

}

public List<RecentRecordImagesRes> retrieveRecentRecordImages(UserPrincipal userPrincipal, Long keywordId) {
User user = validUserById(userPrincipal.getId());
// User user = validUserById(5L);
Keyword keyword = validKeywordById(keywordId);
List<Record> records = recordRepository.findTop5ByKeywordOrderByActionTimeDesc(keyword);

return records.stream()
.map(record -> RecentRecordImagesRes.builder()
.imageUrl(record.getRecordImage())
.createdAt(record.getCreatedAt())
.build())
.collect(Collectors.toList());

}

private User validUserById(Long userId) {
Optional<User> userOptional = userRepository.findById(userId);
return userOptional.get();
}

private Keyword validKeywordById(Long keywordId) {
Optional<Keyword> keywordOptional = keywordRepository.findById(keywordId);
return keywordOptional.get();
}

private boolean verbTypeExists(Set<VerbType> todayVerbTypes, VerbType verbType) {
return todayVerbTypes.contains(verbType);
}

private void validateCreateRecordReq(CreateRecordReq createRecordReq) {
if (createRecordReq.getVerbType() == null || createRecordReq.getVerbType().isEmpty()) {
throw new IllegalArgumentException("verbType is required.");
}
if (createRecordReq.getNoun() == null || createRecordReq.getNoun().isEmpty()) {
throw new IllegalArgumentException("noun is required.");
}
}

}