Skip to content

Commit

Permalink
feature: 부스트 API (#130)
Browse files Browse the repository at this point in the history
* feature: 부스트 API

* test: 테스트 코드 추가

* fix: resolve review

* fix: ci 실패 대응

* fix: 테스트 수정

* fix: Active Profiles 추가
  • Loading branch information
kwanok authored Aug 16, 2024
1 parent 477274b commit e120cfd
Show file tree
Hide file tree
Showing 12 changed files with 321 additions and 38 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'

// Validator
implementation 'org.springframework.boot:spring-boot-starter-validation'

// Actuator
implementation 'org.springframework.boot:spring-boot-starter-actuator'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package com.depromeet.stonebed.domain.feed.api;

import com.depromeet.stonebed.domain.feed.application.FeedService;
import com.depromeet.stonebed.domain.feed.dto.request.FeedBoostRequest;
import com.depromeet.stonebed.domain.feed.dto.request.FeedGetRequest;
import com.depromeet.stonebed.domain.feed.dto.response.FeedGetResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@Tag(name = "7. [피드]", description = "피드 관련 API입니다.")
Expand All @@ -14,10 +19,18 @@
public class FeedController {
private final FeedService feedService;

@Operation(summary = "피드 조회", description = "내 피드를 조회하는 API입니다.")
@GetMapping
public FeedGetResponse getFeed(
@RequestParam(required = false) String cursor, @RequestParam int limit) {
FeedGetRequest request = new FeedGetRequest(cursor, limit);
return feedService.getFeed(request);
}

@Operation(summary = "부스트 생성", description = "미션 기록에 부스트를 생성하는 API입니다.")
@PostMapping("/boost")
public ResponseEntity<Void> postFeed(final @Valid @RequestBody FeedBoostRequest request) {
feedService.createBoost(request.missionRecordId(), request.count());
return ResponseEntity.status(HttpStatus.CREATED).build();
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
package com.depromeet.stonebed.domain.feed.application;

import com.depromeet.stonebed.domain.feed.dao.FeedRepository;
import com.depromeet.stonebed.domain.feed.dto.FindFeedDto;
import com.depromeet.stonebed.domain.feed.dto.request.FeedGetRequest;
import com.depromeet.stonebed.domain.feed.dto.response.FeedContentGetResponse;
import com.depromeet.stonebed.domain.feed.dto.response.FeedGetResponse;
import com.depromeet.stonebed.domain.member.domain.Member;
import com.depromeet.stonebed.domain.missionRecord.dao.MissionRecordBoostRepository;
import com.depromeet.stonebed.domain.missionRecord.dao.MissionRecordRepository;
import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord;
import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecordBoost;
import com.depromeet.stonebed.global.error.ErrorCode;
import com.depromeet.stonebed.global.error.exception.CustomException;
import com.depromeet.stonebed.global.util.MemberUtil;
import java.util.List;
import lombok.RequiredArgsConstructor;
Expand All @@ -18,33 +24,52 @@
public class FeedService {
private final FeedRepository feedRepository;
private final MemberUtil memberUtil;
private final MissionRecordRepository missionRecordRepository;
private final MissionRecordBoostRepository missionRecordBoostRepository;

@Transactional(readOnly = true)
public FeedGetResponse getFeed(FeedGetRequest request) {
Member currentMember = memberUtil.getCurrentMember();

List<MissionRecord> missionRecords =
getMissionRecords(request.cursor(), currentMember.getId(), request.limit());
List<FindFeedDto> feeds =
getFeeds(request.cursor(), currentMember.getId(), request.limit());

List<FeedContentGetResponse> feedContentList =
missionRecords.stream().map(FeedContentGetResponse::from).toList();
feeds.stream().map(FeedContentGetResponse::from).toList();

String nextCursor = getNextCursor(missionRecords, request.limit());
String nextCursor = getNextCursor(feeds, request.limit());

return FeedGetResponse.from(feedContentList, nextCursor);
}

private String getNextCursor(List<MissionRecord> records, int limit) {
public void createBoost(Long missionRecordId, Long boostCount) {
Member currentMember = memberUtil.getCurrentMember();
MissionRecord missionRecord =
missionRecordRepository
.findById(missionRecordId)
.orElseThrow(() -> new CustomException(ErrorCode.MISSION_RECORD_NOT_FOUND));

MissionRecordBoost missionRecordBoost =
MissionRecordBoost.builder()
.missionRecord(missionRecord)
.member(currentMember)
.count(boostCount)
.build();

missionRecordBoostRepository.save(missionRecordBoost);
}

private String getNextCursor(List<FindFeedDto> records, int limit) {
if (records.size() < limit) {
return null;
}

MissionRecord lastRecord = records.get(records.size() - 1);
Long lastId = lastRecord.getId();
FindFeedDto lastRecord = records.get(records.size() - 1);
Long lastId = lastRecord.missionRecord().getId();
return String.valueOf(lastId);
}

private List<MissionRecord> getMissionRecords(String cursor, Long memberId, int limit) {
private List<FindFeedDto> getFeeds(String cursor, Long memberId, int limit) {
if (cursor == null || cursor.isEmpty()) {
return feedRepository.getFeedContents(memberId, limit);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.depromeet.stonebed.domain.feed.dao;

import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord;
import com.depromeet.stonebed.domain.feed.dto.FindFeedDto;
import java.util.List;

public interface FeedRepositoryCustom {
List<MissionRecord> getFeedContentsUsingCursor(Long missionRecordId, Long memberId, int limit);
List<FindFeedDto> getFeedContentsUsingCursor(Long missionRecordId, Long memberId, int limit);

List<MissionRecord> getFeedContents(Long memberId, int limit);
List<FindFeedDto> getFeedContents(Long memberId, int limit);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
package com.depromeet.stonebed.domain.feed.dao;

import static com.depromeet.stonebed.domain.member.domain.QMember.member;
import static com.depromeet.stonebed.domain.mission.domain.QMission.mission;
import static com.depromeet.stonebed.domain.mission.domain.QMissionHistory.missionHistory;
import static com.depromeet.stonebed.domain.missionRecord.domain.QMissionRecord.missionRecord;
import static com.depromeet.stonebed.domain.missionRecord.domain.QMissionRecordBoost.missionRecordBoost;

import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord;
import com.depromeet.stonebed.domain.feed.dto.FindFeedDto;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List;
import lombok.RequiredArgsConstructor;
Expand All @@ -13,28 +20,48 @@
public class FeedRepositoryImpl implements FeedRepositoryCustom {
private final JPAQueryFactory queryFactory;

@Override
public List<MissionRecord> getFeedContentsUsingCursor(
Long missionRecordId, Long memberId, int limit) {
private JPAQuery<FindFeedDto> getFeedBaseQuery() {
return queryFactory
.select(missionRecord)
.select(
Projections.constructor(
FindFeedDto.class,
mission,
missionRecord,
member,
Expressions.asNumber(
missionRecordBoost.count.sumLong().coalesce(0L))
.as("totalBoostCount")))
.from(missionRecord)
.leftJoin(missionRecordBoost)
.on(missionRecordBoost.missionRecord.eq(missionRecord))
.leftJoin(member)
.on(missionRecord.member.eq(member))
.leftJoin(missionHistory)
.on(missionRecord.missionHistory.eq(missionHistory))
.leftJoin(mission)
.on(missionHistory.mission.eq(mission));
}

@Override
public List<FindFeedDto> getFeedContentsUsingCursor(
Long missionRecordId, Long memberId, int limit) {
return getFeedBaseQuery()
.where(
missionRecord
.id
.lt(missionRecordId)
.and(missionRecord.member.id.eq(memberId)))
.groupBy(missionRecord.id, member.id, mission.id, missionHistory.id)
.orderBy(missionRecord.id.desc())
.limit(limit)
.fetch();
}

@Override
public List<MissionRecord> getFeedContents(Long memberId, int limit) {
return queryFactory
.select(missionRecord)
.from(missionRecord)
public List<FindFeedDto> getFeedContents(Long memberId, int limit) {
return getFeedBaseQuery()
.where(missionRecord.member.id.eq(memberId))
.groupBy(missionRecord.id, member.id, mission.id, missionHistory.id)
.orderBy(missionRecord.id.desc())
.limit(limit)
.fetch();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.depromeet.stonebed.domain.feed.dto;

import com.depromeet.stonebed.domain.member.domain.Member;
import com.depromeet.stonebed.domain.mission.domain.Mission;
import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord;

public record FindFeedDto(
Mission mission, MissionRecord missionRecord, Member author, Long totalBoostCount) {
public static FindFeedDto from(
Mission mission, MissionRecord missionRecord, Member author, Long totalBoostCount) {
return new FindFeedDto(mission, missionRecord, author, totalBoostCount);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.depromeet.stonebed.domain.feed.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;

public record FeedBoostRequest(
@NotNull @Min(value = 1) @Max(value = 500) @Schema(description = "부스트 카운트", example = "1")
Long count,
@NotNull @Min(value = 1) @Schema(description = "미션 기록 ID", example = "1")
Long missionRecordId) {}
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
package com.depromeet.stonebed.domain.feed.dto.response;

import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecord;
import com.depromeet.stonebed.domain.feed.dto.FindFeedDto;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDate;

public record FeedContentGetResponse(
@Schema(description = "미션 ID", example = "1") Long missionId,
@Schema(description = "미션 기록 ID", example = "1") Long missionRecordId,
@Schema(description = "작성자 ID", example = "1") Long authorId,
@Schema(description = "미션 기록 이미지 URL", example = "example.jpeg")
String missionRecordImageUrl,
@Schema(description = "미션 기록 생성일") LocalDate createdDate) {
public static FeedContentGetResponse from(MissionRecord missionRecord) {
@Schema(description = "미션 기록 생성일") LocalDate createdDate,
@Schema(description = "부스트") Long totalBoostCount) {
public static FeedContentGetResponse from(FindFeedDto missionRecord) {
return new FeedContentGetResponse(
missionRecord.getId(),
missionRecord.getMember().getId(),
missionRecord.getImageUrl(),
missionRecord.getCreatedAt().toLocalDate());
missionRecord.mission().getId(),
missionRecord.missionRecord().getId(),
missionRecord.author().getId(),
missionRecord.missionRecord().getImageUrl(),
missionRecord.missionRecord().getCreatedAt().toLocalDate(),
missionRecord.totalBoostCount());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.depromeet.stonebed.domain.missionRecord.dao;

import com.depromeet.stonebed.domain.missionRecord.domain.MissionRecordBoost;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MissionRecordBoostRepository extends JpaRepository<MissionRecordBoost, Long> {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.depromeet.stonebed.domain.missionRecord.domain;

import com.depromeet.stonebed.domain.member.domain.Member;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MissionRecordBoost {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "boost_id")
private Long id;

@ManyToOne
@JoinColumn(name = "mission_record_id", nullable = false)
private MissionRecord missionRecord;

@ManyToOne
@JoinColumn(name = "member_id", nullable = false)
private Member member;

@Column(name = "count", nullable = false)
private Long count;

@Builder
public MissionRecordBoost(MissionRecord missionRecord, Member member, Long count) {
this.missionRecord = missionRecord;
this.member = member;
this.count = count;
}
}
Loading

0 comments on commit e120cfd

Please sign in to comment.