Skip to content

Commit

Permalink
Merge pull request #96 from 9uttery/feat/achievement-api-#76
Browse files Browse the repository at this point in the history
[Feature] 소확행 플레이리스트 관련 API 구현
  • Loading branch information
mingeun0507 authored Feb 17, 2024
2 parents cf46651 + 436f7eb commit 46b05b3
Show file tree
Hide file tree
Showing 19 changed files with 524 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ public enum ErrorDetails {
NOT_FOUND_BOOKMARK("A003", HttpStatus.BAD_REQUEST.value(), "저장한 앨범이 아닙니다."),
NOT_MY_ALBUM("A004", HttpStatus.BAD_REQUEST.value(), "내가 만든 앨범이 아닙니다."),

JOY_NOT_FOUND_IN_TODAY_PLAYLIST("P001", HttpStatus.NOT_FOUND.value(), "오늘의 플레이리스트에서 해당 소확행을 찾을 수 없습니다."),
ACHIEVEMENT_NOT_FOUND("P001", HttpStatus.NOT_FOUND.value(), "오늘의 플레이리스트에서 해당 실천을 찾을 수 없습니다."),
INVALID_SATISFACTION_ENUM("P002", HttpStatus.BAD_REQUEST.value(), "만족도 ENUM이 유효하지 않습니다. BAD, SO_SO, GOOD, GREAT, EXCELLENT 중 하나여야 합니다."),
ACHIEVEMENT_ACHIEVER_NOT_MATCH("P003", HttpStatus.FORBIDDEN.value(), "해당 실천을 만든 사용자가 아닙니다."),

FILE_UPLOAD_FAILED("F001", HttpStatus.INTERNAL_SERVER_ERROR.value(), "파일 업로드에 실패했습니다."),

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.guttery.madii.domain.achievement.application.dto;

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

@Schema(description = "소확행 플레이리스트에 추가 요청")
public record AddAchievementRequest(
@NotNull
@Schema(description = "소확행 ID", example = "1")
Long joyId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.guttery.madii.domain.achievement.application.dto;

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

@Schema(description = "소확행 취소 요청")
public record CancelAchievementRequest(
@NotNull
@Schema(description = "소확행 ID", example = "1")
Long achievementId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.guttery.madii.domain.achievement.application.dto;

import io.swagger.v3.oas.annotations.media.Schema;

import java.time.LocalDate;
import java.util.List;

@Schema(description = "일일 소확행 플레이리스트")
public record DailyJoyPlaylist(
@Schema(description = "소확행 플레이리스트 날짜", example = "2021-08-01")
LocalDate date,
@Schema(description = "소확행 목록")
List<JoyAchievementInfo> joyAchievementInfos
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.guttery.madii.domain.achievement.application.dto;

import com.guttery.madii.domain.achievement.domain.model.Satisfaction;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;

@Schema(description = "실천 완료 요청")
public record FinishAchievementRequest(
@NotNull
@Schema(description = "실천 ID", example = "1")
Long achievementId,
@NotNull
@Schema(description = "만족도", example = "VERY_SATISFIED")
Satisfaction satisfaction
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.guttery.madii.domain.achievement.application.dto;

import com.guttery.madii.domain.achievement.domain.model.Satisfaction;
import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "소확행 및 실천 정보")
public record JoyAchievementInfo(
@Schema(description = "소확행 아이디", example = "11")
Long joyId,
@Schema(description = "소확행 썸네일 아이콘 번호", example = "3")
Long achievementId,
@Schema(description = "소확행 썸네일 아이콘 번호", example = "3")
Integer joyIconNum,
@Schema(description = "소확행 내용", example = "낮잠자기")
String contents,
@Schema(description = "소확행 실천 여부", example = "true")
Boolean isAchieved,
@Schema(description = "만족도 평가", example = "SO_SO")
Satisfaction satisfaction
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.guttery.madii.domain.achievement.application.dto;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "어제와 오늘의 소확행 플레이리스트 응답")
public record JoyPlaylistResponse(
@Schema(description = "오늘의 소확행 플레이리스트")
DailyJoyPlaylist todayJoyPlayList,
@Schema(description = "어제의 소확행 플레이리스트")
DailyJoyPlaylist yesterdayJoyPlayList
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.guttery.madii.domain.achievement.application.dto;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "실천을 오늘로 이동 요청")
public record MoveAchievementToTodayRequest(
@Schema(description = "실천 ID", example = "1")
Long achievementId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.guttery.madii.domain.achievement.application.dto;

import com.guttery.madii.domain.achievement.domain.model.Satisfaction;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;

@Schema(description = "실천 만족도 수정 요청")
public record RateAchievementRequest(
@NotNull
@Schema(description = "실천 ID", example = "1")
Long achievementId,
@NotNull
@Schema(description = "만족도", example = "SO_SO")
Satisfaction satisfaction
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.guttery.madii.domain.achievement.application.service;

import com.guttery.madii.common.exception.CustomException;
import com.guttery.madii.common.exception.ErrorDetails;
import com.guttery.madii.domain.achievement.domain.model.Achievement;
import com.guttery.madii.domain.achievement.domain.repository.AchievementRepository;
import com.guttery.madii.domain.user.domain.model.UserPrincipal;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class AchievementServiceHelper {
public static Achievement findExistingAchievement(final AchievementRepository achievementRepository, final Long achievementId) {
return achievementRepository.findById(achievementId)
.orElseThrow(() -> CustomException.of(ErrorDetails.ACHIEVEMENT_NOT_FOUND));
}

public static void validateAchievementAchiever(final Achievement achievement, final Long userId) {
if (!achievement.isAchievedBy(userId)) {
throw CustomException.of(ErrorDetails.ACHIEVEMENT_ACHIEVER_NOT_MATCH);
}
}

public static Achievement findValidAchievement(final AchievementRepository achievementRepository, final Long achievementId, final UserPrincipal userPrincipal) {
final Achievement foundAchievement = findExistingAchievement(achievementRepository, achievementId);
validateAchievementAchiever(foundAchievement, userPrincipal.id());

return foundAchievement;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.guttery.madii.domain.achievement.application.service;

import com.guttery.madii.domain.achievement.application.dto.AddAchievementRequest;
import com.guttery.madii.domain.achievement.application.dto.JoyPlaylistResponse;
import com.guttery.madii.domain.achievement.application.dto.MoveAchievementToTodayRequest;
import com.guttery.madii.domain.achievement.domain.model.Achievement;
import com.guttery.madii.domain.achievement.domain.model.FinishInfo;
import com.guttery.madii.domain.achievement.domain.repository.AchievementRepository;
import com.guttery.madii.domain.joy.application.service.JoyServiceHelper;
import com.guttery.madii.domain.joy.domain.model.Joy;
import com.guttery.madii.domain.joy.domain.repository.JoyRepository;
import com.guttery.madii.domain.user.application.service.UserServiceHelper;
import com.guttery.madii.domain.user.domain.model.User;
import com.guttery.madii.domain.user.domain.model.UserPrincipal;
import com.guttery.madii.domain.user.domain.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;

@RequiredArgsConstructor
@Slf4j
@Service
public class JoyPlaylistService {
private final AchievementRepository achievementRepository;
private final UserRepository userRepository;
private final JoyRepository joyRepository;

@Transactional
public void addAchievementInPlaylist(final AddAchievementRequest addAchievementRequest, final UserPrincipal userPrincipal) {
final User user = UserServiceHelper.findExistingUser(userRepository, userPrincipal);
final Joy joy = JoyServiceHelper.findExistingJoy(joyRepository, addAchievementRequest.joyId());
final Achievement newAchievement = Achievement.create(user, joy, FinishInfo.createNotFinished());
// TODO: 중복 추가 방지를 위한 로직 필요 -> 테스트 이후에 구현 (복합 unique key 고려 중)

achievementRepository.save(newAchievement);
}

@Transactional(readOnly = true)
public JoyPlaylistResponse getAchievementsInPlaylist(final UserPrincipal userPrincipal) {
final User user = UserServiceHelper.findExistingUser(userRepository, userPrincipal);

return achievementRepository.getAchievementsInPlaylist(user.getUserId(), LocalDate.now());
}

@Transactional
public void deleteAchievementInPlaylist(final Long achievementId, final UserPrincipal userPrincipal) {
final Achievement foundAchievement = AchievementServiceHelper.findValidAchievement(achievementRepository, achievementId, userPrincipal);

achievementRepository.delete(foundAchievement);
}

@Transactional
public void moveAchievementInPlaylist(final MoveAchievementToTodayRequest moveAchievementToTodayRequest, final UserPrincipal userPrincipal) {
final Achievement foundAchievement = AchievementServiceHelper.findValidAchievement(achievementRepository, moveAchievementToTodayRequest.achievementId(), userPrincipal);
final Achievement newAchievement = foundAchievement.copyForMove();

achievementRepository.delete(foundAchievement);
achievementRepository.save(newAchievement);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.guttery.madii.domain.achievement.application.service;

import com.guttery.madii.domain.achievement.application.dto.CancelAchievementRequest;
import com.guttery.madii.domain.achievement.application.dto.FinishAchievementRequest;
import com.guttery.madii.domain.achievement.application.dto.RateAchievementRequest;
import com.guttery.madii.domain.achievement.domain.model.Achievement;
import com.guttery.madii.domain.achievement.domain.repository.AchievementRepository;
import com.guttery.madii.domain.user.domain.model.UserPrincipal;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Slf4j
@Service
public class UpdateAchievementStatusService {
private final AchievementRepository achievementRepository;

@Transactional
public void finishAchievement(final FinishAchievementRequest finishAchievementRequest, final UserPrincipal userPrincipal) {
final Achievement foundAchievement = AchievementServiceHelper.findValidAchievement(achievementRepository, finishAchievementRequest.achievementId(), userPrincipal);
foundAchievement.finish(finishAchievementRequest.satisfaction());

achievementRepository.save(foundAchievement);
}

@Transactional
public void rateAchievement(final RateAchievementRequest rateAchievementRequest, final UserPrincipal userPrincipal) {
final Achievement foundAchievement = AchievementServiceHelper.findValidAchievement(achievementRepository, rateAchievementRequest.achievementId(), userPrincipal);

foundAchievement.rate(rateAchievementRequest.satisfaction());

achievementRepository.save(foundAchievement);
}

@Transactional
public void cancelAchievement(final CancelAchievementRequest cancelAchievementRequest, final UserPrincipal userPrincipal) {
final Achievement foundAchievement = AchievementServiceHelper.findValidAchievement(achievementRepository, cancelAchievementRequest.achievementId(), userPrincipal);
foundAchievement.cancel();

achievementRepository.save(foundAchievement);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "t_album")
@Table(name = "t_achievement")
@Access(AccessType.FIELD)
public class Achievement extends BaseTimeEntity {
@Id
Expand All @@ -47,7 +47,19 @@ public void finish(final Satisfaction satisfaction) {
this.finishInfo = FinishInfo.createFinished(satisfaction);
}

public void rate(final Satisfaction satisfaction) {
this.finishInfo = FinishInfo.createFinished(satisfaction);
}

public void cancel() {
this.finishInfo = FinishInfo.createNotFinished();
}

public boolean isAchievedBy(final Long userId) {
return this.achiever.matchesUserId(userId);
}

public Achievement copyForMove() {
return new Achievement(this.achiever, this.joy, FinishInfo.createNotFinished());
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
package com.guttery.madii.domain.achievement.domain.model;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import com.guttery.madii.common.exception.CustomException;
import com.guttery.madii.common.exception.ErrorDetails;

import java.util.stream.Stream;

public enum Satisfaction {
BAD, SO_SO, GOOD, GREAT, EXCELLENT
BAD, SO_SO, GOOD, GREAT, EXCELLENT;

@JsonCreator
public static Satisfaction fromString(final String value) {
return Stream.of(Satisfaction.values())
.filter(s -> s.name().equalsIgnoreCase(value))
.findFirst()
.orElseThrow(() -> CustomException.of(ErrorDetails.INVALID_SATISFACTION_ENUM));
}

@JsonValue
public String toJson() {
return name();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
package com.guttery.madii.domain.achievement.domain.repository;

import com.guttery.madii.domain.achievement.application.dto.JoyPlaylistResponse;

import java.time.LocalDate;

public interface AchievementQueryDslRepository {
JoyPlaylistResponse getAchievementsInPlaylist(Long userId, LocalDate date);
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,50 @@
package com.guttery.madii.domain.achievement.infrastructure;

import com.guttery.madii.common.domain.repository.BaseQueryDslRepository;
import com.guttery.madii.domain.achievement.application.dto.DailyJoyPlaylist;
import com.guttery.madii.domain.achievement.application.dto.JoyAchievementInfo;
import com.guttery.madii.domain.achievement.application.dto.JoyPlaylistResponse;
import com.guttery.madii.domain.achievement.domain.repository.AchievementQueryDslRepository;
import com.guttery.madii.domain.achievement.domain.repository.AchievementRepository;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.stereotype.Repository;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;

import static com.guttery.madii.domain.achievement.domain.model.QAchievement.achievement;
import static com.guttery.madii.domain.joy.domain.model.QJoy.joy;
import static com.guttery.madii.domain.user.domain.model.QUser.user;


@Repository
public class AchievementRepositoryImpl extends BaseQueryDslRepository<AchievementRepository> implements AchievementQueryDslRepository {
public AchievementRepositoryImpl(JPAQueryFactory queryFactory) {
super(queryFactory);
}

@Override
public JoyPlaylistResponse getAchievementsInPlaylist(final Long userId, final LocalDate date) {
final List<JoyAchievementInfo> yesterdayAchievementInfos = queryJoyAchievementInfos(userId, date.minusDays(1));
final List<JoyAchievementInfo> todayAchievementInfos = queryJoyAchievementInfos(userId, date);

return new JoyPlaylistResponse(
new DailyJoyPlaylist(date, todayAchievementInfos),
new DailyJoyPlaylist(date.minusDays(1), yesterdayAchievementInfos)
);
}

private List<JoyAchievementInfo> queryJoyAchievementInfos(final Long userId, final LocalDate date) {
final LocalDateTime startOfDay = date.atStartOfDay();
final LocalDateTime endOfDay = date.atStartOfDay().plusDays(1).minusSeconds(1);

return select(JoyAchievementInfo.class, joy.joyId, achievement.achievementId, joy.joyIconNum, joy.contents, achievement.finishInfo.isFinished, achievement.finishInfo.satisfaction)
.from(achievement)
.join(achievement.joy, joy)
.join(achievement.achiever, user)
.where(achievement.achiever.userId.eq(userId), achievement.createdAt.between(startOfDay, endOfDay))
.orderBy(achievement.finishInfo.isFinished.asc(), achievement.createdAt.asc())
.fetch();
}
}
Loading

0 comments on commit 46b05b3

Please sign in to comment.