전 날 기준 좋아요를 가장 많이 받은 플레이리스트 상위 4개
- Spring Boot Version : 2.6.6
- JDK : Open JDK 1.8
- IDE : IntelliJ
- Build Tool : Gradle
- DBMS : MongoDB
- Test Tool : Junit5
/* starter */
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
/* MongoDB */
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
/* lombok */
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
/* Test */
testImplementation group: 'junit', name: 'junit', version: '4.13.2'
/* REST API 호출 */
implementation('com.mashape.unirest:unirest-java:1.4.9')
/* Common Util */
implementation 'org.apache.commons:commons-lang3:3.5' -> StringUtils 사용
MongoTemplate를 사용하기 위한 빈 등록 및 MongoDB 설정
@Configuration
@EnableMongoRepositories(basePackages = "Document 객체가 있는 패키지 주소", mongoTemplateRef = "testMongoTemplate")
@EnableMongoAuditing // EnableJpaAuditing과 같은 역할, 밑의 BaseTimeException을 위해서 사용됩니다.
public class MongoConfig {
@Bean
public MongoTemplate testMongoTemplate(MongoClient mongoClient) {
MongoDatabaseFactory factory = new SimpleMongoClientDatabaseFactory(mongoClient, "local");
return new MongoTemplate(factory);
}
}
다른 API 개발에 필요하여 현재 기능과는 상관없는 필드가 존재합니다!!
@Getter
@Document(collection = "playList") // Document명
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 빈 생성자가 생기는 것을 방지합니다.
public class Playlist extends BaseTimeEntity {
@Id
private ObjectId _id;
private String playlistTitle;
private Long memberId;
private String playlistDescription;
private List<Tag> tags;
private List<Integer> movieIds;
private boolean display;
@Builder
public Playlist(ObjectId _id, String playListTitle, Long memberId,
String playlistDescription, List<Tag> tags, List<Integer> movieIds, boolean display) {
this._id = _id;
this.playlistTitle = playlistTitle;
this.memberId = memberId;
this.playListDescription = playListDescription;
this.tags = tags;
this.movieIds = movieIds;
this.display = display;
}
}
@Getter
@Document(collection = "tag")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Tag extends BaseTimeEntity {
@Id
private ObjectId _id;
private String tagName;
@Builder
public Tag(ObjectId _id, String tagName) {
this._id = _id;
this.tagName = tagName;
}
}
@Getter
@Document(collection = "like")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Like extends BaseTimeEntity {
@Id
private ObjectId _id;
private Long memberId;
private ObjectId targetId; // 영화 Document의 ObjectId와 매핑
private String likeType;
@Builder
public Like(ObjectId _id, Long memberId, ObjectId targetId, String likeType) {
this._id = _id;
this.memberId = memberId;
this.targetId = targetId;
this.likeType = likeType;
}
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PlaylistLikeJoin {
private List<Playlist> playlist;
private int likeCount;
@Builder
public PlaylistLikeJoin(int likeCount, List<Playlist> playlist) {
this.likeCount = likeCount;
this.playlist = playlist;
}
}
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PlaylistOrderByLikeDto {
@Id
private ObjectId _id;
private String playlistTitle;
private String memberName;
private List<String> representativeImagePath;
private List<Tag> tags;
private int moiveCount;
private int likeCount;
@Builder
public PlaylistOrderByLikeDto(ObjectId _id, String playlistTitle, String memberName, List<String> representativeImagePath,
List<Tag> tags, int movieCount, int likeCount) {
this._id = _id;
this.playlistTitle = playlistTitle;
this.memberName = memberName;
this.representativeImagePath = representativeImagePath;
this.tags = tags;
this.moiveCount = movieCount;
this.likeCount = likeCount;
}
}
데이터를 저장 혹은 수정할 때 등록일이나 수정일을 수동으로 셋팅하지 않기 위한 클래스입니다.
사용 하기 위해서 MongoConfig 클래스에 @EnableMongoAuditing 어노테이션을 등록해야합니다.
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
@CreatedDate
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Column(name = "reg_date")
private LocalDateTime regDate; // 등록일
@LastModifiedDate
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Column(name = "mod_date")
private LocalDateTime modDate; // 수정일
public LocalDateTime getRegDate() {
return regDate;
}
public LocalDateTime getModDate() {
return modDate;
}
}
MongoRepository 외에 조금 더 상세한 작업이 가능한 MongoTemplate를 사용하기 위해서 커스텀된 레포지토리도 상속받습니다.
@Repository
public interface PlaylistRepository extends MongoRepository<Like, ObjectId>, CustomizedLikeRepository {
}
위에서 언급한 커스텀 레포지토리의 Interface
public interface CustomizedLikeRepository {
AggregationResults<PlaylistLikeJoin> filterByTypeAndGroupByTargetId(Aggregation likeAggregation, String collection);
}
커스텀 레포지토리의 구현 클래스, MongoTemplate를 사용할 수 있습니다.
public class CustomizedLikeRepositoryImpl implements CustomizedLikeRepository {
MongoTemplate mongoTemplate;
public CustomizedLikeRepositoryImpl(MongoTemplate mongoTemplate){
this.mongoTemplate = mongoTemplate;
}
@Override
public AggregationResults<PlaylistLikeJoin> filterByTypeAndGroupByTargetId(Aggregation likeAggregation, String collection) {
return mongoTemplate.aggregate(likeAggregation, collection, PlaylistLikeJoin.class);
}
}
테스트 코드에서의 BaseException와 MovieException은 커스텀으로 만들어진 Exception 클래스입니다.
@Slf4j
@SpringBootTest
class PlaylistApiServerApplicationTests {
@Autowired
PlaylistRepository playlistRepository;
@Autowired
TagRepository tagRepository;
@Autowired
LikeRepository likeRepository;
@Autowired
MovieApi movieApi;
// Open API 사이트에 회원가입 후 발급받을 수 있습니다. 자세한 설명은 구글링해주세요.
String API_KEY = "";
// Open API 사용시 기본 URL입니다.
String BASE_URL = "https://api.themoviedb.org/3/";
// 영화의 이미지 주소에 대한 기본 URL입니다. 뒤에 w500은 이미지 사이즈입니다(변경 가능, 필수)
String IMG_BASE_URL = "https://image.tmdb.org/t/p/w500";
// JSON 데이터를 자바 객체로 변경하거나 반대의 상황 시 사용
@Autowired
ObjectMapper om;
@Test
@DisplayName("좋아요 순 플레이리스트 조회 API테스트")
void aggregationTest(){
Map<String, Object> param = new HashMap<>();
param.put("api_key", movieApi.getAPI_KEY());
HttpClientRequest request = new HttpClientRequest();
// like Document와 playlist Document를 JOIN하는 LookupOperation
AggregationOperation lookupOperation = context -> new Document(
"$lookup",
new Document("from", "playlist")
.append("let", new Document("targetId", "$_id"))
.append("pipeline", Collections.singletonList(new Document("$match", new Document("$expr", new Document("$and",
Arrays.asList(
new Document("$eq", Arrays.asList("$_id", "$$targetId")),
new Document("$eq", Arrays.asList("$display", true))
))))
))
.append("as","playlist")
);
Aggregation likeAggregation = Aggregation.newAggregation(
Aggregation.project("targetId", "regDate", "likeType"),
Aggregation.match(Criteria.where("regDate").gte(LocalDate.now().minusDays(1)).and("likeType").is("M")),
Aggregation.group("targetId").count().as("likeCount"),
Aggregation.sort(Sort.Direction.DESC, "likeCount"),
Aggregation.limit(4),
lookupOperation,
Aggregation.match(Criteria.where("playlist").not().size(0))
);
List<PlaylistOrderByLikeDto> filterByTypeAndGroupByTargetId = likeRepository.filterByTypeAndGroupByTargetId(likeAggregation, "like")
.getMappedResults()
.stream()
.map( playlistLikeJoin -> {
Playlist playlistInfo = playlistLikeJoin.getPlaylist().get(0);
List<String> imgPathList = new ArrayList<>();
List<Integer> movieIds = playlistInfo.getMovieIds();
for(int movieId : movieIds){
String img = movieApi.getIMG_BASE_URL();
request.setUrl(String.format("%s/movie/%d/images", movieApi.getBASE_URL(), movieId));
request.setData(param);
try {
Map<String, Object> response = om.readValue(HttpClient.get(request), HashMap.class);
if(response.get("status_code") != null){
if(StringUtils.equals(response.get("status_code").toString(), "7") ){
throw new MovieApiException(response.get("status_massage").toString(), ErrorCode.INVALID_API_KEY);
}
continue;
}
List<Map<String, Object>> posterData = (List<Map<String, Object>>) response.get("posters");
imgPathList.add(movieApi.getIMG_BASE_URL() + posterData.get(0).get("file_path").toString());
} catch (Exception e) {
throw new BaseException(ErrorCode.COMMON_SYSTEM_ERROR);
}
}
return PlaylistOrderByLikeDto.builder()
._id(playlistInfo.get_id())
.playlistTitle(playlistInfo.getPlaylistTitle())
.memberName(memberRepository.findByMemberId(playlistInfo.getMemberId())
.orElseThrow(() -> new BaseException(ErrorCode.USER_NOT_FOUND))
.getMemberName())
.representativeImagePath(imgPathList)
.tags(playlistInfo.getTags())
.movieCount(playlistInfo.getMovieIds().size())
.likeCount(playlistLikeJoin.getLikeCount())
.build();
})
.collect(Collectors.toList());
for(PlayListOrderByLikeDto playlist : filterByTypeAndGroupByTargetId){
log.info("플레이리스트 : {}", playlist.toString());
}
}
}