Skip to content

Commit 482dd4c

Browse files
authored
Merge pull request #33 from DDD-Community/dev
2차 MVP 기능 운영 브랜치 API S도입
2 parents 41107b9 + 2d22a4b commit 482dd4c

23 files changed

+595
-8
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package be.ddd.api.dto.req;
2+
3+
import be.ddd.common.validation.NotFutureDate;
4+
import jakarta.validation.constraints.NotNull;
5+
import java.time.LocalDateTime;
6+
import java.util.UUID;
7+
8+
public record IntakeRegistrationRequestDto(
9+
@NotNull(message = "음료 ID는 필수입니다.") UUID productId,
10+
@NotNull(message = "섭취 날짜는 필수입니다.") @NotFutureDate LocalDateTime intakeTime) {}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package be.ddd.api.dto.res;
2+
3+
import java.time.LocalDateTime;
4+
import java.util.List;
5+
6+
public record DailyIntakeDto(
7+
LocalDateTime date,
8+
List<IntakeRecordDto> records,
9+
int totalKcal,
10+
int totalSugarGrams,
11+
int totalCaffeine) {
12+
13+
public DailyIntakeDto(LocalDateTime date, List<IntakeRecordDto> records) {
14+
this(
15+
date,
16+
records,
17+
records.stream().mapToInt(r -> r.nutrition().getServingKcal()).sum(),
18+
records.stream().mapToInt(r -> r.nutrition().getSugarG()).sum(),
19+
records.stream().mapToInt(r -> r.nutrition().getCaffeineMg()).sum());
20+
}
21+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package be.ddd.api.dto.res;
2+
3+
import be.ddd.domain.entity.crawling.BeverageNutrition;
4+
import be.ddd.domain.entity.crawling.CafeBrand;
5+
import be.ddd.domain.entity.crawling.SugarLevel;
6+
import com.querydsl.core.annotations.QueryProjection;
7+
import java.time.LocalDateTime;
8+
9+
public record IntakeRecordDto(
10+
Long intakeHistoryId,
11+
Long beverageId,
12+
String beverageName,
13+
CafeBrand cafeBrand,
14+
LocalDateTime intakeTime,
15+
BeverageNutrition nutrition,
16+
SugarLevel sugarLevel) {
17+
18+
@QueryProjection
19+
public IntakeRecordDto {
20+
// Compact constructor
21+
}
22+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package be.ddd.api.member.intake;
2+
3+
import be.ddd.api.dto.req.IntakeRegistrationRequestDto;
4+
import be.ddd.api.dto.res.DailyIntakeDto;
5+
import be.ddd.application.member.intake.IntakeHistoryCommandService;
6+
import be.ddd.application.member.intake.IntakeHistoryQueryService;
7+
import be.ddd.common.dto.ApiResponse;
8+
import be.ddd.common.validation.NotFutureDate;
9+
import jakarta.validation.Valid;
10+
import java.time.LocalDateTime;
11+
import java.util.List;
12+
import lombok.RequiredArgsConstructor;
13+
import org.springframework.format.annotation.DateTimeFormat;
14+
import org.springframework.http.HttpStatus;
15+
import org.springframework.validation.annotation.Validated;
16+
import org.springframework.web.bind.annotation.*;
17+
18+
@RestController
19+
@RequestMapping("/api/intake-histories")
20+
@RequiredArgsConstructor
21+
@Validated
22+
public class IntakeHistoryAPI {
23+
24+
private final IntakeHistoryCommandService intakeHistoryCommand;
25+
private final IntakeHistoryQueryService intakeHistoryQuery;
26+
private final Long MEMBER_ID = 1L; // TODO: 추후 실제 회원 데이터로 변경
27+
28+
@PostMapping
29+
@ResponseStatus(HttpStatus.CREATED)
30+
public ApiResponse<?> registerIntake(
31+
@RequestBody @Valid IntakeRegistrationRequestDto requestDto) {
32+
Long historyId = intakeHistoryCommand.registerIntake(MEMBER_ID, requestDto);
33+
return ApiResponse.success(historyId);
34+
}
35+
36+
@GetMapping("/daily")
37+
public ApiResponse<DailyIntakeDto> getDailyIntake(
38+
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) @NotFutureDate
39+
LocalDateTime intakeTime) {
40+
DailyIntakeDto dailyIntake =
41+
intakeHistoryQuery.getDailyIntakeHistory(MEMBER_ID, intakeTime);
42+
return ApiResponse.success(dailyIntake);
43+
}
44+
45+
@GetMapping("/weekly")
46+
public ApiResponse<List<DailyIntakeDto>> getWeeklyIntake(
47+
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) @NotFutureDate
48+
LocalDateTime dateInWeek) {
49+
List<DailyIntakeDto> weeklyIntake =
50+
intakeHistoryQuery.getWeeklyIntakeHistory(MEMBER_ID, dateInWeek);
51+
return ApiResponse.success(weeklyIntake);
52+
}
53+
54+
@GetMapping("/monthly")
55+
public ApiResponse<List<DailyIntakeDto>> getMonthlyIntake(
56+
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) @NotFutureDate
57+
LocalDateTime dateInMonth) {
58+
List<DailyIntakeDto> monthlyIntake =
59+
intakeHistoryQuery.getMonthlyIntakeHistory(MEMBER_ID, dateInMonth);
60+
return ApiResponse.success(monthlyIntake);
61+
}
62+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package be.ddd.application.member.intake;
2+
3+
import be.ddd.api.dto.req.IntakeRegistrationRequestDto;
4+
5+
public interface IntakeHistoryCommandService {
6+
Long registerIntake(Long memberId, IntakeRegistrationRequestDto requestDto);
7+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package be.ddd.application.member.intake;
2+
3+
import be.ddd.api.dto.req.IntakeRegistrationRequestDto;
4+
import be.ddd.common.util.CustomClock;
5+
import be.ddd.domain.entity.crawling.CafeBeverage;
6+
import be.ddd.domain.entity.member.Member;
7+
import be.ddd.domain.entity.member.intake.IntakeHistory;
8+
import be.ddd.domain.exception.CafeBeverageNotFoundException;
9+
import be.ddd.domain.exception.FutureDateNotAllowedException;
10+
import be.ddd.domain.exception.MemberNotFoundException;
11+
import be.ddd.domain.repo.CafeBeverageRepository;
12+
import be.ddd.domain.repo.IntakeHistoryRepository;
13+
import be.ddd.domain.repo.MemberRepository;
14+
import java.time.LocalDateTime;
15+
import lombok.RequiredArgsConstructor;
16+
import org.springframework.stereotype.Service;
17+
import org.springframework.transaction.annotation.Transactional;
18+
19+
@Service
20+
@Transactional
21+
@RequiredArgsConstructor
22+
public class IntakeHistoryCommandServiceImpl implements IntakeHistoryCommandService {
23+
24+
private final IntakeHistoryRepository intakeHistoryRepository;
25+
private final MemberRepository memberRepository;
26+
private final CafeBeverageRepository cafeBeverageRepository;
27+
28+
@Override
29+
public Long registerIntake(Long memberId, IntakeRegistrationRequestDto requestDto) {
30+
if (isFuture(requestDto.intakeTime())) {
31+
throw new FutureDateNotAllowedException();
32+
}
33+
34+
Member member =
35+
memberRepository.findById(memberId).orElseThrow(MemberNotFoundException::new);
36+
CafeBeverage beverage =
37+
cafeBeverageRepository
38+
.findByProductId(requestDto.productId())
39+
.orElseThrow(CafeBeverageNotFoundException::new);
40+
41+
IntakeHistory intakeHistory = new IntakeHistory(member, requestDto.intakeTime(), beverage);
42+
IntakeHistory history = intakeHistoryRepository.save(intakeHistory);
43+
44+
return history.getId();
45+
}
46+
47+
private boolean isFuture(LocalDateTime intakeTime) {
48+
System.out.println("!!!!!!!!!!!!!!!!!!!");
49+
return intakeTime.toLocalDate().isAfter(CustomClock.now().toLocalDate());
50+
}
51+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package be.ddd.application.member.intake;
2+
3+
import be.ddd.api.dto.res.DailyIntakeDto;
4+
import java.time.LocalDateTime;
5+
import java.util.List;
6+
7+
public interface IntakeHistoryQueryService {
8+
DailyIntakeDto getDailyIntakeHistory(Long memberId, LocalDateTime date);
9+
10+
List<DailyIntakeDto> getWeeklyIntakeHistory(Long memberId, LocalDateTime dateInWeek);
11+
12+
List<DailyIntakeDto> getMonthlyIntakeHistory(Long memberId, LocalDateTime dateInMonth);
13+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package be.ddd.application.member.intake;
2+
3+
import be.ddd.api.dto.res.DailyIntakeDto;
4+
import be.ddd.api.dto.res.IntakeRecordDto;
5+
import be.ddd.common.util.CustomClock;
6+
import be.ddd.domain.exception.FutureDateNotAllowedException;
7+
import be.ddd.domain.repo.CafeBeverageRepository;
8+
import be.ddd.domain.repo.IntakeHistoryRepository;
9+
import be.ddd.domain.repo.MemberRepository;
10+
import java.time.DayOfWeek;
11+
import java.time.LocalDateTime;
12+
import java.time.temporal.ChronoUnit;
13+
import java.time.temporal.TemporalAdjusters;
14+
import java.util.ArrayList;
15+
import java.util.List;
16+
import java.util.Map;
17+
import java.util.stream.Collectors;
18+
import lombok.RequiredArgsConstructor;
19+
import org.springframework.stereotype.Service;
20+
import org.springframework.transaction.annotation.Transactional;
21+
22+
@Service
23+
@Transactional(readOnly = true)
24+
@RequiredArgsConstructor
25+
public class IntakeHistoryQueryServiceImpl implements IntakeHistoryQueryService {
26+
27+
private final IntakeHistoryRepository intakeHistoryRepository;
28+
private final MemberRepository memberRepository;
29+
private final CafeBeverageRepository cafeBeverageRepository;
30+
31+
@Override
32+
public DailyIntakeDto getDailyIntakeHistory(Long memberId, LocalDateTime date) {
33+
validateFutureDate(date);
34+
List<IntakeRecordDto> records =
35+
intakeHistoryRepository.findByMemberIdAndDate(memberId, date);
36+
return new DailyIntakeDto(date, records);
37+
}
38+
39+
@Override
40+
public List<DailyIntakeDto> getWeeklyIntakeHistory(Long memberId, LocalDateTime dateInWeek) {
41+
validateFutureDate(dateInWeek);
42+
LocalDateTime startOfWeek =
43+
dateInWeek.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
44+
LocalDateTime endOfWeek = dateInWeek.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY));
45+
46+
List<IntakeRecordDto> records =
47+
intakeHistoryRepository.findByMemberIdAndDateBetween(
48+
memberId, startOfWeek, endOfWeek);
49+
50+
Map<LocalDateTime, List<IntakeRecordDto>> recordsByDate =
51+
records.stream()
52+
.collect(
53+
Collectors.groupingBy(
54+
record ->
55+
record.intakeTime().truncatedTo(ChronoUnit.DAYS)));
56+
57+
List<DailyIntakeDto> dailyIntakeList = new ArrayList<>();
58+
for (LocalDateTime date = startOfWeek.toLocalDate().atStartOfDay();
59+
!date.isAfter(endOfWeek.toLocalDate().atStartOfDay());
60+
date = date.plusDays(1)) {
61+
dailyIntakeList.add(
62+
new DailyIntakeDto(date, recordsByDate.getOrDefault(date, new ArrayList<>())));
63+
}
64+
return dailyIntakeList;
65+
}
66+
67+
@Override
68+
public List<DailyIntakeDto> getMonthlyIntakeHistory(Long memberId, LocalDateTime dateInMonth) {
69+
validateFutureDate(dateInMonth);
70+
LocalDateTime firstDayOfMonth = dateInMonth.with(TemporalAdjusters.firstDayOfMonth());
71+
LocalDateTime lastDayOfMonth = dateInMonth.with(TemporalAdjusters.lastDayOfMonth());
72+
73+
// Calculate start and end dates for the calendar view (considering previous/next month's
74+
// days)
75+
LocalDateTime calendarStartDate =
76+
firstDayOfMonth.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
77+
LocalDateTime calendarEndDate =
78+
lastDayOfMonth.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY));
79+
80+
List<IntakeRecordDto> records =
81+
intakeHistoryRepository.findByMemberIdAndDateBetween(
82+
memberId, calendarStartDate, calendarEndDate);
83+
84+
Map<LocalDateTime, List<IntakeRecordDto>> recordsByDate =
85+
records.stream()
86+
.collect(
87+
Collectors.groupingBy(
88+
record ->
89+
record.intakeTime().truncatedTo(ChronoUnit.DAYS)));
90+
91+
List<DailyIntakeDto> dailyIntakeList = new ArrayList<>();
92+
for (LocalDateTime date = calendarStartDate.toLocalDate().atStartOfDay();
93+
!date.isAfter(calendarEndDate.toLocalDate().atStartOfDay());
94+
date = date.plusDays(1)) {
95+
dailyIntakeList.add(
96+
new DailyIntakeDto(date, recordsByDate.getOrDefault(date, new ArrayList<>())));
97+
}
98+
return dailyIntakeList;
99+
}
100+
101+
private void validateFutureDate(LocalDateTime date) {
102+
if (date.isAfter(CustomClock.now())) {
103+
throw new FutureDateNotAllowedException();
104+
}
105+
}
106+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package be.ddd.application.member.intake;
2+
3+
import be.ddd.api.dto.res.IntakeRecordDto;
4+
import be.ddd.api.dto.res.QIntakeRecordDto;
5+
import be.ddd.domain.entity.member.intake.QIntakeHistory;
6+
import be.ddd.domain.repo.IntakeHistoryRepositoryCustom;
7+
import com.querydsl.jpa.impl.JPAQueryFactory;
8+
import java.time.LocalDateTime;
9+
import java.time.LocalTime;
10+
import java.util.List;
11+
import lombok.RequiredArgsConstructor;
12+
import org.springframework.stereotype.Repository;
13+
14+
@Repository
15+
@RequiredArgsConstructor
16+
public class IntakeHistoryRepositoryImpl implements IntakeHistoryRepositoryCustom {
17+
18+
private final JPAQueryFactory queryFactory;
19+
private final QIntakeHistory intakeHistory = QIntakeHistory.intakeHistory;
20+
21+
@Override
22+
public List<IntakeRecordDto> findByMemberIdAndDate(Long memberId, LocalDateTime intakeTime) {
23+
LocalDateTime startOfDay = intakeTime.toLocalDate().atStartOfDay();
24+
LocalDateTime endOfDay = intakeTime.toLocalDate().atTime(LocalTime.MAX);
25+
26+
return queryFactory
27+
.select(
28+
new QIntakeRecordDto(
29+
intakeHistory.id,
30+
intakeHistory.cafeBeverage.id,
31+
intakeHistory.cafeBeverage.name,
32+
intakeHistory.cafeBeverage.cafeStore.cafeBrand,
33+
intakeHistory.intakeTime,
34+
intakeHistory.cafeBeverage.beverageNutrition,
35+
intakeHistory.cafeBeverage.sugarLevel))
36+
.from(intakeHistory)
37+
.where(
38+
intakeHistory.member.id.eq(memberId),
39+
intakeHistory.intakeTime.between(startOfDay, endOfDay))
40+
.orderBy(intakeHistory.intakeTime.asc())
41+
.fetch();
42+
}
43+
44+
@Override
45+
public List<IntakeRecordDto> findByMemberIdAndDateBetween(
46+
Long memberId, LocalDateTime startDate, LocalDateTime endDate) {
47+
LocalDateTime startDateTime = startDate.toLocalDate().atStartOfDay();
48+
LocalDateTime endDateTime = endDate.toLocalDate().atTime(LocalTime.MAX);
49+
50+
return queryFactory
51+
.select(
52+
new QIntakeRecordDto(
53+
intakeHistory.id,
54+
intakeHistory.cafeBeverage.id,
55+
intakeHistory.cafeBeverage.name,
56+
intakeHistory.cafeBeverage.cafeStore.cafeBrand,
57+
intakeHistory.intakeTime,
58+
intakeHistory.cafeBeverage.beverageNutrition,
59+
intakeHistory.cafeBeverage.sugarLevel))
60+
.from(intakeHistory)
61+
.where(
62+
intakeHistory.member.id.eq(memberId),
63+
intakeHistory.intakeTime.between(startDateTime, endDateTime))
64+
.orderBy(intakeHistory.intakeTime.asc())
65+
.fetch();
66+
}
67+
}

0 commit comments

Comments
 (0)