Skip to content

Commit

Permalink
feat: 피드 탭 구현 (#277)
Browse files Browse the repository at this point in the history
* feat: feed 탭 리스트 구현

* feat: 프로필 피드 탭 구현

* feat: 피드 탭 닉네임 추가

* feat: 피드 탭 v2

* fix: 마이프로필 피드 공개 여부 예외 처리

* refactor: FeedOneResponse class -> record

* refactor: feed/me API 개선

* refactor: 피드 탭 리스트 limit 100 제한

* fix: 리뷰 반영

* fix: targetId -> memberId

* fix: 타인 프로필 공개여부 조건 추가

* fix: Transactional 추가

* fix: getter id가 아닌 member 프록시 객체 리스트로 수정

* fix: @uwoobeat 리뷰 반영

* fix: 주석 삭제
  • Loading branch information
char-yb authored Feb 7, 2024
1 parent f1d578c commit 0fe3634
Show file tree
Hide file tree
Showing 10 changed files with 337 additions and 0 deletions.
34 changes: 34 additions & 0 deletions src/main/java/com/depromeet/domain/feed/api/FeedController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.depromeet.domain.feed.api;

import com.depromeet.domain.feed.application.FeedService;
import com.depromeet.domain.feed.dto.response.FeedOneByProfileResponse;
import com.depromeet.domain.feed.dto.response.FeedOneResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Tag(name = "6. [피드]", description = "피드 관련 API입니다.")
@RequestMapping("/feed")
@RequiredArgsConstructor
public class FeedController {

private final FeedService feedService;

@Operation(summary = "피드 탭", description = "피드 탭을 조회합니다.")
@GetMapping("/me")
public List<FeedOneResponse> feedFindAll() {
return feedService.findAllFeed();
}

@Operation(summary = "프로필 피드", description = "피드 탭을 조회합니다.")
@GetMapping("/{memberId}")
public List<FeedOneByProfileResponse> feedFindAllByTargetId(@PathVariable Long memberId) {
return feedService.findAllFeedByTargetId(memberId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.depromeet.domain.feed.application;

import com.depromeet.domain.feed.dto.response.FeedOneByProfileResponse;
import com.depromeet.domain.feed.dto.response.FeedOneResponse;
import com.depromeet.domain.follow.dao.MemberRelationRepository;
import com.depromeet.domain.follow.domain.MemberRelation;
import com.depromeet.domain.member.domain.Member;
import com.depromeet.domain.mission.domain.MissionVisibility;
import com.depromeet.domain.missionRecord.dao.MissionRecordRepository;
import com.depromeet.domain.missionRecord.domain.MissionRecord;
import com.depromeet.global.util.MemberUtil;
import com.depromeet.global.util.SecurityUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

// TODO: Redis 사용해서 캐싱 작업 필요
@Service
@RequiredArgsConstructor
@Transactional
public class FeedService {
private final MemberUtil memberUtil;
private final MissionRecordRepository missionRecordRepository;
private final MemberRelationRepository memberRelationRepository;
private final SecurityUtil securityUtil;

@Transactional(readOnly = true)
public List<FeedOneResponse> findAllFeed() {
final Member currentMember = memberUtil.getCurrentMember();
List<Member> members =
memberRelationRepository.findAllBySourceId(currentMember.getId()).stream()
.map(MemberRelation::getTarget)
.collect(Collectors.toList());
members.add(currentMember);

return missionRecordRepository.findFeedAll(members);
}

@Transactional(readOnly = true)
public List<FeedOneByProfileResponse> findAllFeedByTargetId(Long targetId) {
final Long sourceId = securityUtil.getCurrentMemberId();

if (isMyFeedRequired(targetId, sourceId)) {
return findFeedByOtherMember(sourceId, targetId);
}
return findFeedByCurrentMember(sourceId);
}

private boolean isMyFeedRequired(Long targetId, Long sourceId) {
return !targetId.equals(sourceId);
}

private List<FeedOneByProfileResponse> findFeedByOtherMember(Long sourceId, Long targetId) {
final Member targetMember = memberUtil.getMemberByMemberId(targetId);

// 팔로우 관계 true: visibility.FOLLOW and ALL, false: visibility.ALL only
boolean isMemberRelationExistsWithMe =
memberRelationRepository.existsBySourceIdAndTargetId(
sourceId, targetMember.getId());
List<MissionVisibility> visibilities =
determineVisibilityConditionsByRelationsWithMe(isMemberRelationExistsWithMe);
List<MissionRecord> feedAllByMemberId =
missionRecordRepository.findFeedAllByMemberId(targetId, visibilities);
return extractFeedResponses(feedAllByMemberId);
}

private List<FeedOneByProfileResponse> findFeedByCurrentMember(Long sourceId) {
List<MissionVisibility> visibilities =
List.of(MissionVisibility.NONE, MissionVisibility.FOLLOWER, MissionVisibility.ALL);
List<MissionRecord> feedAllByMemberId =
missionRecordRepository.findFeedAllByMemberId(sourceId, visibilities);
return extractFeedResponses(feedAllByMemberId);
}

private List<FeedOneByProfileResponse> extractFeedResponses(List<MissionRecord> records) {
return records.stream().map(FeedOneByProfileResponse::of).toList();
}

private List<MissionVisibility> determineVisibilityConditionsByRelationsWithMe(
boolean isMemberRelationExistsWithMe) {
List<MissionVisibility> visibilities = new ArrayList<>();
visibilities.add(MissionVisibility.ALL);

if (isMemberRelationExistsWithMe) {
visibilities.add(MissionVisibility.FOLLOWER);
}
return visibilities;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.depromeet.domain.feed.dto.response;

import com.depromeet.domain.missionRecord.domain.MissionRecord;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Duration;
import java.time.LocalDateTime;

public record FeedOneByProfileResponse(
@Schema(description = "미션 ID", defaultValue = "1") Long missionId,
@Schema(description = "미션 기록 ID", defaultValue = "1") Long recordId,
@Schema(description = "미션 이름", defaultValue = "default name") String name,
@Schema(
description = "미션 기록 인증 사진 Url",
defaultValue = "https://image.10mm.today/default.png")
String recordImageUrl,
@Schema(description = "미션 수행한 시간", defaultValue = "21") long duration,
@Schema(description = "미션 시작한 지 N일차", defaultValue = "3") long sinceDay,
@JsonFormat(
shape = JsonFormat.Shape.STRING,
pattern = "yyyy-MM-dd HH:mm:ss",
timezone = "Asia/Seoul")
@Schema(
description = "미션 기록 시작 시간",
defaultValue = "2023-01-06 00:00:00",
type = "string")
LocalDateTime startedAt,
@JsonFormat(
shape = JsonFormat.Shape.STRING,
pattern = "yyyy-MM-dd HH:mm:ss",
timezone = "Asia/Seoul")
@Schema(
description = "미션 기록 종료 시간",
defaultValue = "2024-01-20 00:34:00",
type = "string")
LocalDateTime finishedAt) {

public static FeedOneByProfileResponse of(MissionRecord record) {
return new FeedOneByProfileResponse(
record.getMission().getId(),
record.getId(),
record.getMission().getName(),
record.getImageUrl(),
record.getDuration().toMinutes(),
Duration.between(record.getStartedAt(), LocalDateTime.now()).toDays() + 1,
record.getStartedAt(),
record.getFinishedAt());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.depromeet.domain.feed.dto.response;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.querydsl.core.annotations.QueryProjection;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Duration;
import java.time.LocalDateTime;

public record FeedOneResponse(
@Schema(description = "작성자 ID", defaultValue = "1") Long memberId,
@Schema(description = "작성자 닉네임", defaultValue = "default name") String nickname,
@Schema(description = "작성자 프로필 이미지", defaultValue = "https://image.10mm.today/default.png")
String profileImage,
@Schema(description = "미션 ID", defaultValue = "1") Long missionId,
@Schema(description = "미션 이름", defaultValue = "default name") String name,
@Schema(description = "미션 기록 ID", defaultValue = "1") Long recordId,
@Schema(description = "미션 일지 내용", defaultValue = "default remark") String remark,
@Schema(
description = "미션 기록 인증 사진 Url",
defaultValue = "https://image.10mm.today/default.png")
String recordImageUrl,
@Schema(description = "미션 수행한 시간", defaultValue = "21") long duration,
@Schema(description = "미션 시작한 지 N일차", defaultValue = "3") long sinceDay,
@JsonFormat(
shape = JsonFormat.Shape.STRING,
pattern = "yyyy-MM-dd HH:mm:ss",
timezone = "Asia/Seoul")
@Schema(
description = "미션 기록 시작 시간",
defaultValue = "2024-01-06 00:00:00",
type = "string")
LocalDateTime startedAt,
@JsonFormat(
shape = JsonFormat.Shape.STRING,
pattern = "yyyy-MM-dd HH:mm:ss",
timezone = "Asia/Seoul")
@Schema(
description = "미션 기록 종료 시간",
defaultValue = "2024-01-20 00:34:00",
type = "string")
LocalDateTime finishedAt) {
@QueryProjection
public FeedOneResponse(
Long memberId,
String nickname,
String profileImage,
Long missionId,
String name,
Long recordId,
String remark,
String recordImageUrl,
Duration duration,
LocalDateTime startedAt,
LocalDateTime finishedAt) {
this(
memberId,
nickname,
profileImage,
missionId,
name,
recordId,
remark,
recordImageUrl,
duration.toMinutes(),
Duration.between(startedAt, LocalDateTime.now()).toDays() + 1,
startedAt,
finishedAt);
}

public static FeedOneResponse of(
Long memberId,
String nickname,
String profileImage,
Long missionId,
String name,
Long recordId,
String remark,
String recordImageUrl,
Duration duration,
LocalDateTime startedAt,
LocalDateTime finishedAt) {
return new FeedOneResponse(
memberId,
nickname,
profileImage,
missionId,
name,
recordId,
remark,
recordImageUrl,
duration.toMinutes(),
Duration.between(startedAt, LocalDateTime.now()).toDays() + 1,
startedAt,
finishedAt);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ private Member getTargetMember(Long targetId) {
return targetMember;
}

@Transactional(readOnly = true)
public FollowListResponse findFollowList(Long targetId) {
final Member currentMember = memberUtil.getCurrentMember();
Member targetMember = getTargetMember(targetId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public List<MemberRelation> findAllBySourceId(Long memberId) {
return jpaQueryFactory
.selectFrom(memberRelation)
.leftJoin(memberRelation.target, member)
.fetchJoin()
.leftJoin(memberRelation.target.missions, mission)
.leftJoin(mission.missionRecords, missionRecord)
.where(sourceIdEq(memberId))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.depromeet.domain.mission.dao;

import static com.depromeet.domain.member.domain.QMember.*;
import static com.depromeet.domain.mission.domain.QMission.*;
import static com.depromeet.domain.missionRecord.domain.QMissionRecord.*;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.depromeet.domain.missionRecord.dao;

import com.depromeet.domain.feed.dto.response.FeedOneResponse;
import com.depromeet.domain.member.domain.Member;
import com.depromeet.domain.mission.domain.MissionVisibility;
import com.depromeet.domain.missionRecord.domain.MissionRecord;
import java.time.YearMonth;
import java.util.List;
Expand All @@ -8,6 +11,10 @@ public interface MissionRecordRepositoryCustom {

List<MissionRecord> findAllByMissionIdAndYearMonth(Long missionId, YearMonth yearMonth);

List<FeedOneResponse> findFeedAll(List<Member> members);

List<MissionRecord> findFeedAllByMemberId(Long memberId, List<MissionVisibility> visibilities);

boolean isCompletedMissionExistsToday(Long missionId);

void deleteByMissionRecordId(Long missionRecordId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
package com.depromeet.domain.missionRecord.dao;

import static com.depromeet.domain.member.domain.QMember.*;
import static com.depromeet.domain.mission.domain.QMission.*;
import static com.depromeet.domain.missionRecord.domain.QMissionRecord.*;

import com.depromeet.domain.feed.dto.response.FeedOneResponse;
import com.depromeet.domain.member.domain.Member;
import com.depromeet.domain.mission.domain.MissionVisibility;
import com.depromeet.domain.missionRecord.domain.ImageUploadStatus;
import com.depromeet.domain.missionRecord.domain.MissionRecord;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.time.LocalDate;
Expand All @@ -16,6 +23,7 @@
public class MissionRecordRepositoryImpl implements MissionRecordRepositoryCustom {

private final JPAQueryFactory jpaQueryFactory;
private static final long FEED_TAB_LIMIT = 100;

@Override
public List<MissionRecord> findAllByMissionIdAndYearMonth(Long missionId, YearMonth yearMonth) {
Expand Down Expand Up @@ -44,6 +52,53 @@ public boolean isCompletedMissionExistsToday(Long missionId) {
return missionRecordFetchOne != null;
}

@Override
public List<FeedOneResponse> findFeedAll(List<Member> members) {
return jpaQueryFactory
.select(
Projections.constructor(
FeedOneResponse.class,
member.id,
member.profile.nickname,
member.profile.profileImageUrl,
mission.id,
mission.name,
missionRecord.id,
missionRecord.remark,
missionRecord.imageUrl,
missionRecord.duration,
missionRecord.startedAt,
missionRecord.finishedAt))
.from(missionRecord)
.leftJoin(missionRecord.mission, mission)
.on(mission.id.eq(missionRecord.mission.id))
.leftJoin(mission.member, member)
.on(mission.member.id.eq(missionRecord.mission.member.id))
.where(
missionRecord.mission.member.in(members),
missionRecord.mission.visibility.in(
MissionVisibility.FOLLOWER, MissionVisibility.ALL),
missionRecord.uploadStatus.eq(ImageUploadStatus.COMPLETE))
.orderBy(missionRecord.startedAt.desc())
.limit(FEED_TAB_LIMIT)
.fetch();
}

@Override
public List<MissionRecord> findFeedAllByMemberId(
Long memberId, List<MissionVisibility> visibilities) {
return jpaQueryFactory
.selectFrom(missionRecord)
.leftJoin(missionRecord.mission, mission)
.fetchJoin()
.where(
mission.visibility.in(visibilities),
mission.member.id.eq(memberId),
missionRecord.uploadStatus.eq(ImageUploadStatus.COMPLETE))
.orderBy(missionRecord.startedAt.desc())
.fetch();
}

@Override
public void deleteByMissionRecordId(Long missionRecordId) {
jpaQueryFactory.delete(missionRecord).where(missionRecord.id.eq(missionRecordId)).execute();
Expand Down
Loading

0 comments on commit 0fe3634

Please sign in to comment.