Skip to content

Commit

Permalink
�feat: 루틴 인증 및 이미지 업로드 기능 구현 (#63)
Browse files Browse the repository at this point in the history
* feat: 서버 시간 체크 컨트롤러 구현

* feat: 루틴 인증 기능 및 ClockHolder 구현

* feat: UrlSubstringParser 구현

* test: 루틴 인증 관련 테스트 구현

* refactor: 방 공지 길이 수정

* feat: constant 및 error 작성

* feat: s3 이미지 업로드 기능 구현

* test: s3 이미지 업로드 테스트

* chore: build.gradle s3 추가

* Merge branch 'develop' into feature/#8-upload-image

* refactor: build 오류 수정

* test: CertificationsSearchRepository 테스트

* chore: s3Manager 커버리지 제외

* refactor: UrlParser 코드스멜 제거

* refactor: 코드 리뷰 반영

---------

Co-authored-by: ymkim97 <[email protected]>
Co-authored-by: Youngmyung Kim <[email protected]>
  • Loading branch information
3 people authored Nov 13, 2023
1 parent 0d084fa commit 43d18ce
Show file tree
Hide file tree
Showing 32 changed files with 1,153 additions and 14 deletions.
8 changes: 7 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ dependencies {

// RestDocs
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'

// S3
implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.0.2")
implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3'
}

tasks.named('test') {
Expand Down Expand Up @@ -121,6 +125,7 @@ jacocoTestReport {
"**/*DynamicQuery*",
"**/*BaseTimeEntity*",
"**/*HealthCheckController*",
"**/*S3Manager*",
] + Qdomains)
})
)
Expand Down Expand Up @@ -152,7 +157,8 @@ sonar {
property 'sonar.coverage.jacoco.xmlReportPaths', 'build/reports/jacoco/test/jacocoTestReport.xml'
property 'sonar.coverage.exclusions', '**/test/**, **/Q*.java, **/*Doc*.java, **/resources/** ' +
',**/*Application*.java , **/*Config*.java, **/*Request*.java, **/*Response*.java ,**/*Exception*.java ' +
',**/*ErrorMessage*.java, **/*Mapper*.java, **/*DynamicQuery*, **/*BaseTimeEntity*, **/*HealthCheckController*'
',**/*ErrorMessage*.java, **/*Mapper*.java, **/*DynamicQuery*, **/*BaseTimeEntity*, **/*HealthCheckController* ' +
', **/*S3Manager*.java'
property 'sonar.java.checkstyle.reportPaths', 'build/reports/checkstyle/main.xml'
}
}
Expand Down
54 changes: 54 additions & 0 deletions src/main/java/com/moabam/api/application/ImageService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.moabam.api.application;

import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import com.moabam.api.domain.resizedimage.ImageName;
import com.moabam.api.domain.resizedimage.ImageResizer;
import com.moabam.api.domain.resizedimage.ImageType;
import com.moabam.api.infrastructure.s3.S3Manager;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ImageService {

private final S3Manager s3Manager;

@Transactional
public List<String> uploadImages(List<MultipartFile> multipartFiles, ImageType imageType) {

List<String> result = new ArrayList<>();

List<ImageResizer> imageResizers = multipartFiles.stream()
.map(multipartFile -> this.toImageResizer(multipartFile, imageType))
.toList();

imageResizers.forEach(resizer -> {
resizer.resizeImageToFixedSize(imageType);
result.add(s3Manager.uploadImage(resizer.getResizedImage().getName(), resizer.getResizedImage()));
});

return result;
}

private ImageResizer toImageResizer(MultipartFile multipartFile, ImageType imageType) {
ImageName imageName = ImageName.of(multipartFile, imageType);

return ImageResizer.builder()
.image(multipartFile)
.fileName(imageName.getFileName())
.build();
}

@Transactional
public void deleteImage(String imageUrl) {
s3Manager.deleteImage(imageUrl);
}
}
116 changes: 116 additions & 0 deletions src/main/java/com/moabam/api/application/RoomService.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
package com.moabam.api.application;

import static com.moabam.api.domain.entity.enums.RoomType.*;
import static com.moabam.api.domain.resizedimage.ImageType.*;
import static com.moabam.global.error.model.ErrorMessage.*;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Period;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import com.moabam.api.domain.entity.Certification;
import com.moabam.api.domain.entity.DailyMemberCertification;
Expand All @@ -20,8 +25,13 @@
import com.moabam.api.domain.entity.Participant;
import com.moabam.api.domain.entity.Room;
import com.moabam.api.domain.entity.Routine;
import com.moabam.api.domain.entity.enums.BugType;
import com.moabam.api.domain.entity.enums.RequireExp;
import com.moabam.api.domain.entity.enums.RoomType;
import com.moabam.api.domain.repository.CertificationRepository;
import com.moabam.api.domain.repository.CertificationsSearchRepository;
import com.moabam.api.domain.repository.DailyMemberCertificationRepository;
import com.moabam.api.domain.repository.DailyRoomCertificationRepository;
import com.moabam.api.domain.repository.ParticipantRepository;
import com.moabam.api.domain.repository.ParticipantSearchRepository;
import com.moabam.api.domain.repository.RoomRepository;
Expand All @@ -37,6 +47,8 @@
import com.moabam.api.dto.RoutineMapper;
import com.moabam.api.dto.RoutineResponse;
import com.moabam.api.dto.TodayCertificateRankResponse;
import com.moabam.global.common.util.ClockHolder;
import com.moabam.global.common.util.UrlSubstringParser;
import com.moabam.global.error.exception.BadRequestException;
import com.moabam.global.error.exception.ForbiddenException;
import com.moabam.global.error.exception.NotFoundException;
Expand All @@ -54,7 +66,12 @@ public class RoomService {
private final ParticipantRepository participantRepository;
private final ParticipantSearchRepository participantSearchRepository;
private final CertificationsSearchRepository certificationsSearchRepository;
private final DailyMemberCertificationRepository dailyMemberCertificationRepository;
private final DailyRoomCertificationRepository dailyRoomCertificationRepository;
private final CertificationRepository certificationRepository;
private final MemberService memberService;
private final ImageService imageService;
private final ClockHolder clockHolder;

@Transactional
public Long createRoom(Long memberId, CreateRoomRequest createRoomRequest) {
Expand Down Expand Up @@ -151,6 +168,63 @@ public RoomDetailsResponse getRoomDetails(Long memberId, Long roomId) {
todayCertificateRankResponses, completePercentage);
}

@Transactional
public void certifyRoom(Long memberId, Long roomId, List<MultipartFile> multipartFiles) {
LocalDate today = LocalDate.now();
Participant participant = getParticipant(memberId, roomId);
Room room = participant.getRoom();
Member member = memberService.getById(memberId);
BugType bugType = switch (room.getRoomType()) {
case MORNING -> BugType.MORNING;
case NIGHT -> BugType.NIGHT;
};
int roomLevel = room.getLevel();

validateCertifyTime(clockHolder.times(), room.getCertifyTime());
validateAlreadyCertified(memberId, roomId, today);

DailyMemberCertification dailyMemberCertification = CertificationsMapper.toDailyMemberCertification(memberId,
roomId, participant);
dailyMemberCertificationRepository.save(dailyMemberCertification);

member.increaseTotalCertifyCount();

List<String> result = imageService.uploadImages(multipartFiles, CERTIFICATION);
saveNewCertifications(result, memberId);

Optional<DailyRoomCertification> dailyRoomCertification =
certificationsSearchRepository.findDailyRoomCertification(roomId, today);

if (dailyRoomCertification.isEmpty()) {
List<DailyMemberCertification> dailyMemberCertifications =
certificationsSearchRepository.findSortedDailyMemberCertifications(roomId, today);
double completePercentage = calculateCompletePercentage(dailyMemberCertifications.size(),
room.getCurrentUserCount());

if (completePercentage >= 75) {
DailyRoomCertification createDailyRoomCertification = CertificationsMapper.toDailyRoomCertification(
roomId, today);

dailyRoomCertificationRepository.save(createDailyRoomCertification);

int expAppliedRoomLevel = getRoomLevelAfterExpApply(roomLevel, room);

List<Long> memberIds = dailyMemberCertifications.stream()
.map(DailyMemberCertification::getMemberId)
.toList();

memberService.getRoomMembers(memberIds)
.forEach(completedMember -> completedMember.getBug().increaseBug(bugType, expAppliedRoomLevel));

return;
}
}

if (dailyRoomCertification.isPresent()) {
member.getBug().increaseBug(bugType, roomLevel);
}
}

public void validateRoomById(Long roomId) {
if (!roomRepository.existsById(roomId)) {
throw new NotFoundException(ROOM_NOT_FOUND);
Expand Down Expand Up @@ -307,4 +381,46 @@ private double calculateCompletePercentage(int certifiedMembersCount, int curren

return Math.round(completePercentage * 100) / 100.0;
}

private void validateCertifyTime(LocalDateTime now, int certifyTime) {
LocalTime targetTime = LocalTime.of(certifyTime, 0);
LocalDateTime minusTenMinutes = LocalDateTime.of(now.toLocalDate(), targetTime).minusMinutes(10);
LocalDateTime plusTenMinutes = LocalDateTime.of(now.toLocalDate(), targetTime).plusMinutes(10);

if (now.isBefore(minusTenMinutes) || now.isAfter(plusTenMinutes)) {
throw new BadRequestException(INVALID_CERTIFY_TIME);
}
}

private void validateAlreadyCertified(Long memberId, Long roomId, LocalDate today) {
if (certificationsSearchRepository.findDailyMemberCertification(memberId, roomId, today).isPresent()) {
throw new BadRequestException(DUPLICATED_DAILY_MEMBER_CERTIFICATION);
}
}

private void saveNewCertifications(List<String> imageUrls, Long memberId) {
List<Certification> certifications = new ArrayList<>();

for (String imageUrl : imageUrls) {
Long routineId = Long.parseLong(UrlSubstringParser.parseUrl(imageUrl, "_"));
Routine routine = routineRepository.findById(routineId).orElseThrow(() -> new NotFoundException(
ROUTINE_NOT_FOUND));

Certification certification = CertificationsMapper.toCertification(routine, memberId, imageUrl);
certifications.add(certification);
}

certificationRepository.saveAll(certifications);
}

private int getRoomLevelAfterExpApply(int roomLevel, Room room) {
int requireExp = RequireExp.of(roomLevel).getTotalExp();
room.gainExp();

if (room.getExp() == requireExp) {
room.levelUp();
}

return room.getLevel();
}
}
4 changes: 4 additions & 0 deletions src/main/java/com/moabam/api/domain/entity/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,8 @@ public void exitNightRoom() {
public int getLevel() {
return (int)(totalCertifyCount / LEVEL_DIVISOR) + 1;
}

public void increaseTotalCertifyCount() {
this.totalCertifyCount++;
}
}
13 changes: 12 additions & 1 deletion src/main/java/com/moabam/api/domain/entity/Room.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,14 @@ public class Room extends BaseTimeEntity {
@Column(name = "password", length = 8)
private String password;

@ColumnDefault("0")
@Column(name = "level", nullable = false)
private int level;

@ColumnDefault("0")
@Column(name = "exp", nullable = false)
private int exp;

@Enumerated(value = EnumType.STRING)
@Column(name = "room_type")
private RoomType roomType;
Expand All @@ -65,7 +70,7 @@ public class Room extends BaseTimeEntity {
@Column(name = "max_user_count", nullable = false)
private int maxUserCount;

@Column(name = "announcement", length = 255)
@Column(name = "announcement", length = 100)
private String announcement;

@ColumnDefault(ROOM_LEVEL_0_IMAGE)
Expand All @@ -78,6 +83,7 @@ private Room(Long id, String title, String password, RoomType roomType, int cert
this.title = requireNonNull(title);
this.password = password;
this.level = 0;
this.exp = 0;
this.roomType = requireNonNull(roomType);
this.certifyTime = validateCertifyTime(roomType, certifyTime);
this.currentUserCount = 1;
Expand All @@ -87,6 +93,11 @@ private Room(Long id, String title, String password, RoomType roomType, int cert

public void levelUp() {
this.level += 1;
this.exp = 0;
}

public void gainExp() {
this.exp += 1;
}

public void changeAnnouncement(String announcement) {
Expand Down
43 changes: 43 additions & 0 deletions src/main/java/com/moabam/api/domain/entity/enums/RequireExp.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.moabam.api.domain.entity.enums;

import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

/**
* 방 경험치
* 방 레벨 - 현재 경험치 / 전체 경험치
* 레벨0 - 0 / 1
* 레벨1 - 0 / 3
* 레벨2 - 0 / 5
* 레벨3 - 0 / 10
*/

@Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public enum RequireExp {

ROOM_LEVEL_0(0, 1),
ROOM_LEVEL_1(1, 5),
ROOM_LEVEL_2(2, 10),
ROOM_LEVEL_3(3, 20),
ROOM_LEVEL_4(4, 40),
ROOM_LEVEL_5(5, 80);

private static final Map<Integer, String> requireExpMap = Collections.unmodifiableMap(
Stream.of(values())
.collect(Collectors.toMap(RequireExp::getLevel, RequireExp::name))
);

private final int level;
private final int totalExp;

public static RequireExp of(int level) {
return RequireExp.valueOf(requireExpMap.get(level));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.List;
import java.util.Optional;

import org.springframework.stereotype.Repository;

Expand All @@ -33,6 +34,18 @@ public List<Certification> findCertifications(Long roomId, LocalDate date) {
.fetch();
}

public Optional<DailyMemberCertification> findDailyMemberCertification(Long memberId, Long roomId, LocalDate date) {
return Optional.ofNullable(jpaQueryFactory
.selectFrom(dailyMemberCertification)
.where(
dailyMemberCertification.memberId.eq(memberId),
dailyMemberCertification.roomId.eq(roomId),
dailyMemberCertification.createdAt.between(date.atStartOfDay(), date.atTime(LocalTime.MAX))
)
.fetchOne()
);
}

public List<DailyMemberCertification> findSortedDailyMemberCertifications(Long roomId, LocalDate date) {
return jpaQueryFactory
.selectFrom(dailyMemberCertification)
Expand All @@ -47,6 +60,16 @@ public List<DailyMemberCertification> findSortedDailyMemberCertifications(Long r
.fetch();
}

public Optional<DailyRoomCertification> findDailyRoomCertification(Long roomId, LocalDate date) {
return Optional.ofNullable(jpaQueryFactory
.selectFrom(dailyRoomCertification)
.where(
dailyRoomCertification.roomId.eq(roomId),
dailyRoomCertification.certifiedAt.eq(date)
)
.fetchOne());
}

public List<DailyRoomCertification> findDailyRoomCertifications(Long roomId, LocalDate date) {
return jpaQueryFactory
.selectFrom(dailyRoomCertification)
Expand Down
Loading

0 comments on commit 43d18ce

Please sign in to comment.