Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TNT-217] feat: 트레이니 - 식단 등록 기능 구현 #57

Merged
merged 11 commits into from
Feb 11, 2025
4 changes: 2 additions & 2 deletions src/main/java/com/tnt/application/member/SignUpService.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.tnt.application.member;

import static com.tnt.common.constant.ProfileConstant.TRAINEE_DEFAULT_IMAGE;
import static com.tnt.common.constant.ProfileConstant.TRAINER_DEFAULT_IMAGE;
import static com.tnt.common.constant.ImageConstant.TRAINEE_DEFAULT_IMAGE;
import static com.tnt.common.constant.ImageConstant.TRAINER_DEFAULT_IMAGE;
import static com.tnt.domain.member.MemberType.TRAINEE;
import static com.tnt.domain.member.MemberType.TRAINER;
import static io.hypersistence.tsid.TSID.Factory.getTsid;
Expand Down
31 changes: 26 additions & 5 deletions src/main/java/com/tnt/application/pt/PtService.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.tnt.application.trainee.DietService;
import com.tnt.application.trainee.PtGoalService;
import com.tnt.application.trainee.TraineeService;
import com.tnt.application.trainer.TrainerService;
Expand All @@ -19,11 +20,13 @@
import com.tnt.domain.member.Member;
import com.tnt.domain.pt.PtLesson;
import com.tnt.domain.pt.PtTrainerTrainee;
import com.tnt.domain.trainee.Diet;
import com.tnt.domain.trainee.PtGoal;
import com.tnt.domain.trainee.Trainee;
import com.tnt.domain.trainer.Trainer;
import com.tnt.dto.trainee.request.ConnectWithTrainerRequest;
import com.tnt.dto.trainee.request.CreateDietRequest;
import com.tnt.dto.trainer.ConnectWithTrainerDto;
import com.tnt.dto.trainer.request.ConnectWithTrainerRequest;
import com.tnt.dto.trainer.request.CreatePtLessonRequest;
import com.tnt.dto.trainer.response.ConnectWithTraineeResponse;
import com.tnt.dto.trainer.response.ConnectWithTraineeResponse.ConnectTraineeInfo;
Expand All @@ -45,13 +48,15 @@
@RequiredArgsConstructor
public class PtService {

private final TraineeService traineeService;
private final TrainerService trainerService;
private final TraineeService traineeService;
private final PtGoalService ptGoalService;
private final DietService dietService;

private final PtTrainerTraineeRepository ptTrainerTraineeRepository;
private final PtTrainerTraineeSearchRepository ptTrainerTraineeSearchRepository;
private final PtLessonRepository ptLessonRepository;
private final PtLessonSearchRepository ptLessonSearchRepository;
private final PtTrainerTraineeSearchRepository ptTrainerTraineeSearchRepository;

@Transactional
public ConnectWithTrainerDto connectWithTrainer(Long memberId, ConnectWithTrainerRequest request) {
Expand Down Expand Up @@ -153,8 +158,8 @@ public GetActiveTraineesResponse getActiveTrainees(Long memberId) {
.toList();

return new TraineeInfo(trainee.getId(), trainee.getMember().getName(),
ptTrainerTrainee.getFinishedPtCount(), ptTrainerTrainee.getTotalPtCount(), trainee.getCautionNote(),
ptGoals);
trainee.getMember().getProfileImageUrl(), ptTrainerTrainee.getFinishedPtCount(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

메모 부분은 추후에 넣어주어야 하니까 나중을 위해 위에다가 주석 하나 달아주세요!
ex) // Memo 추가 구현

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 반영하겠습니다 !

ptTrainerTrainee.getTotalPtCount(), "", ptGoals);
}).toList();

return new GetActiveTraineesResponse(trainees.size(), traineeInfo);
Expand Down Expand Up @@ -187,6 +192,22 @@ public void completePtLesson(Long memberId, Long ptLessonId) {
ptLesson.completeLesson();
}

@Transactional
public void createDiet(Long memberId, CreateDietRequest request, String dietImageUrl) {
Trainee trainee = traineeService.getTraineeWithMemberId(memberId);

Diet diet = Diet.builder()
.traineeId(trainee.getId())
.date(request.date())
.time(request.time())
.dietImageUrl(dietImageUrl)
.memo(request.memo())
.dietType(request.dietType())
.build();

dietService.save(diet);
}

public boolean isPtTrainerTraineeExistWithTrainerId(Long trainerId) {
return ptTrainerTraineeRepository.existsByTrainerIdAndDeletedAtIsNull(trainerId);
}
Expand Down
23 changes: 14 additions & 9 deletions src/main/java/com/tnt/application/s3/S3Service.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.tnt.application.s3;

import static com.tnt.common.constant.ProfileConstant.TRAINEE_DEFAULT_IMAGE;
import static com.tnt.common.constant.ProfileConstant.TRAINEE_S3_PROFILE_PATH;
import static com.tnt.common.constant.ProfileConstant.TRAINER_DEFAULT_IMAGE;
import static com.tnt.common.constant.ProfileConstant.TRAINER_S3_PROFILE_PATH;
import static com.tnt.common.constant.ImageConstant.TRAINEE_DEFAULT_IMAGE;
import static com.tnt.common.constant.ImageConstant.TRAINEE_S3_PROFILE_IMAGE_PATH;
import static com.tnt.common.constant.ImageConstant.TRAINER_DEFAULT_IMAGE;
import static com.tnt.common.constant.ImageConstant.TRAINER_S3_PROFILE_IMAGE_PATH;
import static com.tnt.common.error.model.ErrorMessage.IMAGE_NOT_FOUND;
import static com.tnt.common.error.model.ErrorMessage.IMAGE_NOT_SUPPORT;
import static com.tnt.common.error.model.ErrorMessage.UNSUPPORTED_MEMBER_TYPE;
Expand Down Expand Up @@ -49,23 +49,27 @@ public String uploadProfileImage(@Nullable MultipartFile profileImage, MemberTyp
switch (memberType) {
case TRAINER -> {
defaultImage = TRAINER_DEFAULT_IMAGE;
folderPath = TRAINER_S3_PROFILE_PATH;
folderPath = TRAINER_S3_PROFILE_IMAGE_PATH;
}
case TRAINEE -> {
defaultImage = TRAINEE_DEFAULT_IMAGE;
folderPath = TRAINEE_S3_PROFILE_PATH;
folderPath = TRAINEE_S3_PROFILE_IMAGE_PATH;
}
default -> throw new IllegalArgumentException(UNSUPPORTED_MEMBER_TYPE.getMessage());
}

if (isNull(profileImage)) {
return uploadImage(defaultImage, folderPath, profileImage);
}

public String uploadImage(String defaultImage, String folderPath, @Nullable MultipartFile image) {
if (isNull(image)) {
return defaultImage;
}

String extension = validateImageFormat(profileImage);
String extension = validateImageFormat(image);

try {
byte[] processedImage = processImage(profileImage, extension);
byte[] processedImage = processImage(image, extension);

return s3Adapter.uploadFile(processedImage, folderPath, extension);
} catch (Exception e) {
Expand Down Expand Up @@ -126,6 +130,7 @@ private byte[] processImage(MultipartFile image, String extension) throws IOExce
.size(MAX_WIDTH, MAX_HEIGHT)
.keepAspectRatio(true)
.outputQuality(IMAGE_QUALITY)
.useExifOrientation(true)
.outputFormat(extension)
.toOutputStream(outputStream);

Expand Down
21 changes: 21 additions & 0 deletions src/main/java/com/tnt/application/trainee/DietService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.tnt.application.trainee;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.tnt.domain.trainee.Diet;
import com.tnt.infrastructure.mysql.repository.trainee.DietRepository;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class DietService {

private final DietRepository dietRepository;

@Transactional
public Diet save(Diet diet) {
return dietRepository.save(diet);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ProfileConstant {
public class ImageConstant {

public static final String TRAINER_DEFAULT_IMAGE = "https://images.tntapp.co.kr/profiles/trainers/basic_trainer_image.png";
public static final String TRAINEE_DEFAULT_IMAGE = "https://images.tntapp.co.kr/profiles/trainees/basic_trainee_image.png";
public static final String TRAINER_S3_PROFILE_PATH = "profiles/trainers";
public static final String TRAINEE_S3_PROFILE_PATH = "profiles/trainees";

public static final String TRAINER_S3_PROFILE_IMAGE_PATH = "profiles/trainers";
public static final String TRAINEE_S3_PROFILE_IMAGE_PATH = "profiles/trainees";

public static final String DIET_S3_IMAGE_PATH = "diets/trainees";
}
7 changes: 6 additions & 1 deletion src/main/java/com/tnt/common/error/model/ErrorMessage.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,12 @@ public enum ErrorMessage {

PT_LESSON_INVALID_MEMO("수업 메모의 길이는 공백 포함 30자 이하이어야 합니다."),
PT_LESSON_DUPLICATE_TIME("이미 예약된 시간대입니다."),
PT_LESSON_NOT_FOUND("존재하지 않는 수업입니다.");
PT_LESSON_NOT_FOUND("존재하지 않는 수업입니다."),

DIET_NULL_TRAINEE_ID("식단 트레이니 id가 null 입니다."),
DIET_INVALID_IMAGE_URL("유효하지 않는 식단 사진입니다."),
DIET_INVALID_MEMO("식단 메모가 올바르지 않습니다."),
UNSUPPORTED_DIET_TYPE("지원하지 않는 식단 타입입니다.");

private final String message;
}
91 changes: 91 additions & 0 deletions src/main/java/com/tnt/domain/trainee/Diet.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.tnt.domain.trainee;

import static com.tnt.common.error.model.ErrorMessage.DIET_INVALID_IMAGE_URL;
import static com.tnt.common.error.model.ErrorMessage.DIET_INVALID_MEMO;
import static io.micrometer.common.util.StringUtils.isBlank;
import static java.util.Objects.requireNonNull;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;

import com.tnt.infrastructure.mysql.BaseTimeEntity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@Table(name = "diet")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Diet extends BaseTimeEntity {

public static final int DIET_IMAGE_URL_LENGTH = 255;
public static final int MEMO_LENGTH = 100;
public static final int DIET_TYPE_LENGTH = 5;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BREAKFAST가 5를 넘습니다!
넉넉하게 20 정도 어떨까요??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋습니다~ 반영하겠습니다 !


@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false, unique = true)
private Long id;

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

@Column(name = "date", nullable = false)
private LocalDate date;

@Column(name = "time", nullable = false)
private LocalTime time;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

따로 받도록 구현하신 이유가 있을까요!?
시간과 날짜는 한번에 LocalDateTime으로 받는건 어떨까요??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이것도 수정해서 반영하겠습니다 !


@Column(name = "diet_image_url", nullable = false, length = DIET_IMAGE_URL_LENGTH)
private String dietImageUrl;

@Column(name = "memo", nullable = false, length = MEMO_LENGTH)
private String memo;

@Column(name = "deleted_at", nullable = true)
private LocalDateTime deletedAt;

@Enumerated(EnumType.STRING)
@Column(name = "diet_type", nullable = false, length = DIET_TYPE_LENGTH)
private DietType dietType;

@Builder
public Diet(Long id, Long traineeId, LocalDate date, LocalTime time, String dietImageUrl, String memo,
DietType dietType) {
this.id = id;
this.traineeId = requireNonNull(traineeId);
this.date = requireNonNull(date);
this.time = requireNonNull(time);
this.dietImageUrl = validateDietImageUrl(dietImageUrl);
this.memo = validateMemo(memo);
this.dietType = requireNonNull(dietType);
}

private String validateDietImageUrl(String dietImageUrl) {
if (dietImageUrl.length() > DIET_IMAGE_URL_LENGTH) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isBlank()가 빠진거 같아요!!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사진 없으면 디폴트를 "" 로 저장하고 싶은데 isBlank가 막더라구요 그래서 뺐습니다 !

throw new IllegalArgumentException(DIET_INVALID_IMAGE_URL.getMessage());
}

return dietImageUrl;
}

private String validateMemo(String memo) {
if (isBlank(memo) || memo.length() > MEMO_LENGTH) {
throw new IllegalArgumentException(DIET_INVALID_MEMO.getMessage());
}

return memo;
}
}
23 changes: 23 additions & 0 deletions src/main/java/com/tnt/domain/trainee/DietType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.tnt.domain.trainee;

import static com.tnt.common.error.model.ErrorMessage.UNSUPPORTED_DIET_TYPE;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.tnt.common.error.exception.TnTException;

public enum DietType {
BREAKFAST,
LUNCH,
DINNER,
SNACK;

@JsonCreator
public static DietType of(String value) {
for (DietType type : DietType.values()) {
if (type.name().equalsIgnoreCase(value)) { // 대소문자 구분 없이 처리
return type;
}
}
throw new TnTException(UNSUPPORTED_DIET_TYPE);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.tnt.dto.trainer.request;
package com.tnt.dto.trainee.request;

import java.time.LocalDate;

Expand Down
32 changes: 32 additions & 0 deletions src/main/java/com/tnt/dto/trainee/request/CreateDietRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.tnt.dto.trainee.request;

import java.time.LocalDate;
import java.time.LocalTime;

import com.tnt.domain.trainee.DietType;

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

@Schema(description = "식단 등록 API 요청")
public record CreateDietRequest(
@Schema(description = "식사 날짜", example = "2025-01-01", nullable = true)
@PastOrPresent(message = "식사 날짜는 현재거나 과거 날짜여야 합니다.")
LocalDate date,

@Schema(description = "식사 시간", example = "19:30", nullable = true)
@PastOrPresent(message = "식사 시간은 현재거나 과거 시간이어야 합니다.")
LocalTime time,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기도 Entity 쪽 참고 부탁드려요!
그리고 기획쪽에서 이전 날짜는 안되도록 하지는 않았던걸로 기억해서,,
한번 확인 부탁드립니다. 사용자가 이전 시간 날짜에 기록 못하는 것이 어색한거 같아요.


@Schema(description = "식단 타입", example = "BREAKFAST", nullable = false)
@NotNull(message = "식단 타입은 필수입니다.")
DietType dietType,

@Schema(description = "메모", example = "아 배부르다.", nullable = false)
@NotBlank(message = "메모는 필수입니다.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

100자 이하로 받기 때문에
@Size(min = 1, max = 100, message = "메모는 100자 이하여야 합니다.") 추가 부탁드려요~!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 반영하겠습니다 !

String memo
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

@Schema(description = "관리중인 트레이니 목록 응답")
public record GetActiveTraineesResponse(
@Schema(description = "트레이니 회원 수", nullable = false)
@Schema(description = "트레이니 회원 수", example = "30", nullable = false)
Integer traineeCount,

@Schema(description = "트레이니 목록", nullable = false)
Expand All @@ -20,17 +20,20 @@ public record TraineeInfo(
@Schema(description = "트레이니 이름", example = "김정호", nullable = false)
String name,

@Schema(description = "프로필 사진 URL", example = "https://images.tntapp.co.kr/profiles/trainees/basic_profile_trainer.svg", nullable = false)
String profileImageUrl,

@Schema(description = "진행한 PT 횟수", example = "10", nullable = false)
Integer finishedPtCount,

@Schema(description = "총 PT 횟수", example = "100", nullable = false)
Integer totalPtCount,

@Schema(description = "주의사항", example = "가냘퍼요", nullable = true)
String cautionNote,
@Schema(description = "메모", example = "건강하지 않음", nullable = true)
String memo,

@Schema(description = "PT 목적들", example = "[\"체중 감량\", \"근력 향상\"]", nullable = false)
List<String> goalContents
List<String> ptGoals
) {

}
Expand Down
Loading