Skip to content

Commit d0f7d3d

Browse files
authored
[TNT-217] feat: 트레이니 - 식단 등록 기능 구현 (#57)
* [TNT-217] feat: 사진 방향 정상화 설정 * [TNT-217] feat: 불필요한 메서드 삭제 * [TNT-217] feat: 응답에 프사 추가 * [TNT-217] feat: ConnectWithTrainerRequest 위치 수정 * [TNT-217] refactor: 메모로 수정 * [TNT-217] refactor: 메모로 수정 * [TNT-217] feat: 식단 등록 기능 구현 * [TNT-217] feat: 식단 등록 기능 구현 * [TNT-217] feat: 다양한 수정 * [TNT-217] feat: 다양한 수정
1 parent a900df8 commit d0f7d3d

File tree

20 files changed

+359
-95
lines changed

20 files changed

+359
-95
lines changed

src/main/java/com/tnt/application/member/SignUpService.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.tnt.application.member;
22

3-
import static com.tnt.common.constant.ProfileConstant.TRAINEE_DEFAULT_IMAGE;
4-
import static com.tnt.common.constant.ProfileConstant.TRAINER_DEFAULT_IMAGE;
3+
import static com.tnt.common.constant.ImageConstant.TRAINEE_DEFAULT_IMAGE;
4+
import static com.tnt.common.constant.ImageConstant.TRAINER_DEFAULT_IMAGE;
55
import static com.tnt.domain.member.MemberType.TRAINEE;
66
import static com.tnt.domain.member.MemberType.TRAINER;
77
import static io.hypersistence.tsid.TSID.Factory.getTsid;

src/main/java/com/tnt/application/pt/PtService.java

+26-5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.springframework.stereotype.Service;
1212
import org.springframework.transaction.annotation.Transactional;
1313

14+
import com.tnt.application.trainee.DietService;
1415
import com.tnt.application.trainee.PtGoalService;
1516
import com.tnt.application.trainee.TraineeService;
1617
import com.tnt.application.trainer.TrainerService;
@@ -19,11 +20,13 @@
1920
import com.tnt.domain.member.Member;
2021
import com.tnt.domain.pt.PtLesson;
2122
import com.tnt.domain.pt.PtTrainerTrainee;
23+
import com.tnt.domain.trainee.Diet;
2224
import com.tnt.domain.trainee.PtGoal;
2325
import com.tnt.domain.trainee.Trainee;
2426
import com.tnt.domain.trainer.Trainer;
27+
import com.tnt.dto.trainee.request.ConnectWithTrainerRequest;
28+
import com.tnt.dto.trainee.request.CreateDietRequest;
2529
import com.tnt.dto.trainer.ConnectWithTrainerDto;
26-
import com.tnt.dto.trainer.request.ConnectWithTrainerRequest;
2730
import com.tnt.dto.trainer.request.CreatePtLessonRequest;
2831
import com.tnt.dto.trainer.response.ConnectWithTraineeResponse;
2932
import com.tnt.dto.trainer.response.ConnectWithTraineeResponse.ConnectTraineeInfo;
@@ -45,13 +48,15 @@
4548
@RequiredArgsConstructor
4649
public class PtService {
4750

48-
private final TraineeService traineeService;
4951
private final TrainerService trainerService;
52+
private final TraineeService traineeService;
5053
private final PtGoalService ptGoalService;
54+
private final DietService dietService;
55+
5156
private final PtTrainerTraineeRepository ptTrainerTraineeRepository;
57+
private final PtTrainerTraineeSearchRepository ptTrainerTraineeSearchRepository;
5258
private final PtLessonRepository ptLessonRepository;
5359
private final PtLessonSearchRepository ptLessonSearchRepository;
54-
private final PtTrainerTraineeSearchRepository ptTrainerTraineeSearchRepository;
5560

5661
@Transactional
5762
public ConnectWithTrainerDto connectWithTrainer(Long memberId, ConnectWithTrainerRequest request) {
@@ -152,9 +157,10 @@ public GetActiveTraineesResponse getActiveTrainees(Long memberId) {
152157
.map(PtGoal::getContent)
153158
.toList();
154159

160+
// Memo 추가 구현 필요
155161
return new TraineeInfo(trainee.getId(), trainee.getMember().getName(),
156-
ptTrainerTrainee.getFinishedPtCount(), ptTrainerTrainee.getTotalPtCount(), trainee.getCautionNote(),
157-
ptGoals);
162+
trainee.getMember().getProfileImageUrl(), ptTrainerTrainee.getFinishedPtCount(),
163+
ptTrainerTrainee.getTotalPtCount(), "", ptGoals);
158164
}).toList();
159165

160166
return new GetActiveTraineesResponse(trainees.size(), traineeInfo);
@@ -187,6 +193,21 @@ public void completePtLesson(Long memberId, Long ptLessonId) {
187193
ptLesson.completeLesson();
188194
}
189195

196+
@Transactional
197+
public void createDiet(Long memberId, CreateDietRequest request, String dietImageUrl) {
198+
Trainee trainee = traineeService.getTraineeWithMemberId(memberId);
199+
200+
Diet diet = Diet.builder()
201+
.traineeId(trainee.getId())
202+
.date(request.date())
203+
.dietImageUrl(dietImageUrl)
204+
.memo(request.memo())
205+
.dietType(request.dietType())
206+
.build();
207+
208+
dietService.save(diet);
209+
}
210+
190211
public boolean isPtTrainerTraineeExistWithTrainerId(Long trainerId) {
191212
return ptTrainerTraineeRepository.existsByTrainerIdAndDeletedAtIsNull(trainerId);
192213
}

src/main/java/com/tnt/application/s3/S3Service.java

+14-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package com.tnt.application.s3;
22

3-
import static com.tnt.common.constant.ProfileConstant.TRAINEE_DEFAULT_IMAGE;
4-
import static com.tnt.common.constant.ProfileConstant.TRAINEE_S3_PROFILE_PATH;
5-
import static com.tnt.common.constant.ProfileConstant.TRAINER_DEFAULT_IMAGE;
6-
import static com.tnt.common.constant.ProfileConstant.TRAINER_S3_PROFILE_PATH;
3+
import static com.tnt.common.constant.ImageConstant.TRAINEE_DEFAULT_IMAGE;
4+
import static com.tnt.common.constant.ImageConstant.TRAINEE_S3_PROFILE_IMAGE_PATH;
5+
import static com.tnt.common.constant.ImageConstant.TRAINER_DEFAULT_IMAGE;
6+
import static com.tnt.common.constant.ImageConstant.TRAINER_S3_PROFILE_IMAGE_PATH;
77
import static com.tnt.common.error.model.ErrorMessage.IMAGE_NOT_FOUND;
88
import static com.tnt.common.error.model.ErrorMessage.IMAGE_NOT_SUPPORT;
99
import static com.tnt.common.error.model.ErrorMessage.UNSUPPORTED_MEMBER_TYPE;
@@ -49,23 +49,27 @@ public String uploadProfileImage(@Nullable MultipartFile profileImage, MemberTyp
4949
switch (memberType) {
5050
case TRAINER -> {
5151
defaultImage = TRAINER_DEFAULT_IMAGE;
52-
folderPath = TRAINER_S3_PROFILE_PATH;
52+
folderPath = TRAINER_S3_PROFILE_IMAGE_PATH;
5353
}
5454
case TRAINEE -> {
5555
defaultImage = TRAINEE_DEFAULT_IMAGE;
56-
folderPath = TRAINEE_S3_PROFILE_PATH;
56+
folderPath = TRAINEE_S3_PROFILE_IMAGE_PATH;
5757
}
5858
default -> throw new IllegalArgumentException(UNSUPPORTED_MEMBER_TYPE.getMessage());
5959
}
6060

61-
if (isNull(profileImage)) {
61+
return uploadImage(defaultImage, folderPath, profileImage);
62+
}
63+
64+
public String uploadImage(String defaultImage, String folderPath, @Nullable MultipartFile image) {
65+
if (isNull(image)) {
6266
return defaultImage;
6367
}
6468

65-
String extension = validateImageFormat(profileImage);
69+
String extension = validateImageFormat(image);
6670

6771
try {
68-
byte[] processedImage = processImage(profileImage, extension);
72+
byte[] processedImage = processImage(image, extension);
6973

7074
return s3Adapter.uploadFile(processedImage, folderPath, extension);
7175
} catch (Exception e) {
@@ -126,6 +130,7 @@ private byte[] processImage(MultipartFile image, String extension) throws IOExce
126130
.size(MAX_WIDTH, MAX_HEIGHT)
127131
.keepAspectRatio(true)
128132
.outputQuality(IMAGE_QUALITY)
133+
.useExifOrientation(true)
129134
.outputFormat(extension)
130135
.toOutputStream(outputStream);
131136

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.tnt.application.trainee;
2+
3+
import org.springframework.stereotype.Service;
4+
import org.springframework.transaction.annotation.Transactional;
5+
6+
import com.tnt.domain.trainee.Diet;
7+
import com.tnt.infrastructure.mysql.repository.trainee.DietRepository;
8+
9+
import lombok.RequiredArgsConstructor;
10+
11+
@Service
12+
@RequiredArgsConstructor
13+
public class DietService {
14+
15+
private final DietRepository dietRepository;
16+
17+
@Transactional
18+
public Diet save(Diet diet) {
19+
return dietRepository.save(diet);
20+
}
21+
}

src/main/java/com/tnt/common/constant/ProfileConstant.java src/main/java/com/tnt/common/constant/ImageConstant.java

+6-3
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
import lombok.NoArgsConstructor;
55

66
@NoArgsConstructor(access = AccessLevel.PRIVATE)
7-
public class ProfileConstant {
7+
public class ImageConstant {
88

99
public static final String TRAINER_DEFAULT_IMAGE = "https://images.tntapp.co.kr/profiles/trainers/basic_trainer_image.png";
1010
public static final String TRAINEE_DEFAULT_IMAGE = "https://images.tntapp.co.kr/profiles/trainees/basic_trainee_image.png";
11-
public static final String TRAINER_S3_PROFILE_PATH = "profiles/trainers";
12-
public static final String TRAINEE_S3_PROFILE_PATH = "profiles/trainees";
11+
12+
public static final String TRAINER_S3_PROFILE_IMAGE_PATH = "profiles/trainers";
13+
public static final String TRAINEE_S3_PROFILE_IMAGE_PATH = "profiles/trainees";
14+
15+
public static final String DIET_S3_IMAGE_PATH = "diets/trainees";
1316
}

src/main/java/com/tnt/common/error/model/ErrorMessage.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,12 @@ public enum ErrorMessage {
7777

7878
PT_LESSON_INVALID_MEMO("수업 메모의 길이는 공백 포함 30자 이하이어야 합니다."),
7979
PT_LESSON_DUPLICATE_TIME("이미 예약된 시간대입니다."),
80-
PT_LESSON_NOT_FOUND("존재하지 않는 수업입니다.");
80+
PT_LESSON_NOT_FOUND("존재하지 않는 수업입니다."),
81+
82+
DIET_NULL_TRAINEE_ID("식단 트레이니 id가 null 입니다."),
83+
DIET_INVALID_IMAGE_URL("유효하지 않는 식단 사진입니다."),
84+
DIET_INVALID_MEMO("식단 메모가 올바르지 않습니다."),
85+
UNSUPPORTED_DIET_TYPE("지원하지 않는 식단 타입입니다.");
8186

8287
private final String message;
8388
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package com.tnt.domain.trainee;
2+
3+
import static com.tnt.common.error.model.ErrorMessage.DIET_INVALID_IMAGE_URL;
4+
import static com.tnt.common.error.model.ErrorMessage.DIET_INVALID_MEMO;
5+
import static io.micrometer.common.util.StringUtils.isBlank;
6+
import static java.util.Objects.requireNonNull;
7+
8+
import java.time.LocalDateTime;
9+
10+
import com.tnt.infrastructure.mysql.BaseTimeEntity;
11+
12+
import jakarta.persistence.Column;
13+
import jakarta.persistence.Entity;
14+
import jakarta.persistence.EnumType;
15+
import jakarta.persistence.Enumerated;
16+
import jakarta.persistence.GeneratedValue;
17+
import jakarta.persistence.GenerationType;
18+
import jakarta.persistence.Id;
19+
import jakarta.persistence.Table;
20+
import lombok.AccessLevel;
21+
import lombok.Builder;
22+
import lombok.Getter;
23+
import lombok.NoArgsConstructor;
24+
25+
@Entity
26+
@Getter
27+
@Table(name = "diet")
28+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
29+
public class Diet extends BaseTimeEntity {
30+
31+
public static final int DIET_IMAGE_URL_LENGTH = 255;
32+
public static final int MEMO_LENGTH = 100;
33+
public static final int DIET_TYPE_LENGTH = 20;
34+
35+
@Id
36+
@GeneratedValue(strategy = GenerationType.IDENTITY)
37+
@Column(name = "id", nullable = false, unique = true)
38+
private Long id;
39+
40+
@Column(name = "trainee_id", nullable = false)
41+
private Long traineeId;
42+
43+
@Column(name = "date", nullable = false)
44+
private LocalDateTime date;
45+
46+
@Column(name = "diet_image_url", nullable = true, length = DIET_IMAGE_URL_LENGTH)
47+
private String dietImageUrl;
48+
49+
@Column(name = "memo", nullable = false, length = MEMO_LENGTH)
50+
private String memo;
51+
52+
@Column(name = "deleted_at", nullable = true)
53+
private LocalDateTime deletedAt;
54+
55+
@Enumerated(EnumType.STRING)
56+
@Column(name = "diet_type", nullable = false, length = DIET_TYPE_LENGTH)
57+
private DietType dietType;
58+
59+
@Builder
60+
public Diet(Long id, Long traineeId, LocalDateTime date, String dietImageUrl, String memo, DietType dietType) {
61+
this.id = id;
62+
this.traineeId = requireNonNull(traineeId);
63+
this.date = requireNonNull(date);
64+
this.dietImageUrl = validateDietImageUrl(dietImageUrl);
65+
this.memo = validateMemo(memo);
66+
this.dietType = requireNonNull(dietType);
67+
}
68+
69+
private String validateDietImageUrl(String dietImageUrl) {
70+
if (!isBlank(dietImageUrl) && dietImageUrl.length() > DIET_IMAGE_URL_LENGTH) {
71+
throw new IllegalArgumentException(DIET_INVALID_IMAGE_URL.getMessage());
72+
}
73+
74+
return dietImageUrl;
75+
}
76+
77+
private String validateMemo(String memo) {
78+
if (isBlank(memo) || memo.length() > MEMO_LENGTH) {
79+
throw new IllegalArgumentException(DIET_INVALID_MEMO.getMessage());
80+
}
81+
82+
return memo;
83+
}
84+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.tnt.domain.trainee;
2+
3+
import static com.tnt.common.error.model.ErrorMessage.UNSUPPORTED_DIET_TYPE;
4+
5+
import com.fasterxml.jackson.annotation.JsonCreator;
6+
import com.tnt.common.error.exception.TnTException;
7+
8+
public enum DietType {
9+
BREAKFAST,
10+
LUNCH,
11+
DINNER,
12+
SNACK;
13+
14+
@JsonCreator
15+
public static DietType of(String value) {
16+
for (DietType type : DietType.values()) {
17+
if (type.name().equalsIgnoreCase(value)) { // 대소문자 구분 없이 처리
18+
return type;
19+
}
20+
}
21+
throw new TnTException(UNSUPPORTED_DIET_TYPE);
22+
}
23+
}

src/main/java/com/tnt/dto/trainer/request/ConnectWithTrainerRequest.java src/main/java/com/tnt/dto/trainee/request/ConnectWithTrainerRequest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.tnt.dto.trainer.request;
1+
package com.tnt.dto.trainee.request;
22

33
import java.time.LocalDate;
44

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.tnt.dto.trainee.request;
2+
3+
import java.time.LocalDateTime;
4+
5+
import com.tnt.domain.trainee.DietType;
6+
7+
import io.swagger.v3.oas.annotations.media.Schema;
8+
import jakarta.validation.constraints.NotBlank;
9+
import jakarta.validation.constraints.NotNull;
10+
import jakarta.validation.constraints.PastOrPresent;
11+
import jakarta.validation.constraints.Size;
12+
13+
@Schema(description = "식단 등록 API 요청")
14+
public record CreateDietRequest(
15+
@Schema(description = "식사 날짜", example = "2025-01-01T11:00:00", nullable = true)
16+
@PastOrPresent(message = "식사 날짜는 현재거나 과거 날짜여야 합니다.")
17+
LocalDateTime date,
18+
19+
@Schema(description = "식단 타입", example = "BREAKFAST", nullable = false)
20+
@NotNull(message = "식단 타입은 필수입니다.")
21+
DietType dietType,
22+
23+
@Schema(description = "메모", example = "아 배부르다.", nullable = false)
24+
@Size(min = 1, max = 100, message = "메모는 100자 이하여야 합니다.")
25+
@NotBlank(message = "메모는 필수입니다.")
26+
String memo
27+
) {
28+
29+
}

src/main/java/com/tnt/dto/trainer/response/GetActiveTraineesResponse.java

+7-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

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

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

23+
@Schema(description = "프로필 사진 URL", example = "https://images.tntapp.co.kr/profiles/trainees/basic_profile_trainer.svg", nullable = false)
24+
String profileImageUrl,
25+
2326
@Schema(description = "진행한 PT 횟수", example = "10", nullable = false)
2427
Integer finishedPtCount,
2528

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

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

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

3639
}

0 commit comments

Comments
 (0)