Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import be.ddd.domain.repo.CafeStoreRepository;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
Expand All @@ -25,8 +24,7 @@ public class CafeBeverageBatchService {
private final CafeBeverageRepository repository;
private final WebClient.Builder webClientBuilder;

private final String lambdaUrl =
"https://u6wvrcscqwe7rdbblr3xebajf40avfxz.lambda-url.ap-northeast-2.on.aws/";
private final String lambdaUrl = "http://127.0.0.1:8000/api/v1/beverages";
private final CafeStoreRepository cafeStoreRepository;

public List<LambdaBeverageDto> fetchAll() {
Expand All @@ -47,7 +45,7 @@ public CafeBeverage toEntity(LambdaBeverageDto dto) {
log.info(
"[DEBUG] Processing DTO for beverage: '{}'. Sizes received: {}",
dto.name(),
dto.beverageNutritions().stream().map(n -> n.size()).collect(Collectors.toList()));
dto.beverageNutritions() == null ? List.of() : dto.beverageNutritions().keySet());
Objects.requireNonNull(dto.name(), "Beverage name required");
List<CafeBeverage> existingBeverages = repository.findAllByName(dto.name());
if (!existingBeverages.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,16 @@ private Optional<Double> parseDouble(String value) {
return Optional.empty();
}
}

public BeverageNutritionDto withSize(String size) {
return new BeverageNutritionDto(
size,
servingMl,
servingKcal,
saturatedFatG,
proteinG,
sodiumMg,
sugarG,
caffeineMg);
}
}
22 changes: 8 additions & 14 deletions src/main/java/be/ddd/application/batch/dto/LambdaBeverageDto.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
package be.ddd.application.batch.dto;

import be.ddd.domain.entity.crawling.*;
import java.util.List;
import be.ddd.application.batch.dto.deserializer.BeverageNutritionsDeserializer;
import be.ddd.domain.entity.crawling.BeverageType;
import be.ddd.domain.entity.crawling.CafeBeverage;
import be.ddd.domain.entity.crawling.CafeStore;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;

public record LambdaBeverageDto(
String brand,
String name,
String image,
String beverageType,
String beverageTemperature,
List<BeverageNutritionDto> beverageNutritions) {
@JsonDeserialize(using = BeverageNutritionsDeserializer.class)
Map<String, BeverageNutritionDto> beverageNutritions) {

public CafeBeverage toEntity(CafeStore cafeStore) {
BeverageType type =
Expand All @@ -23,21 +25,13 @@ public CafeBeverage toEntity(CafeStore cafeStore) {
.map(BeverageType::valueOf)
.orElse(BeverageType.ANY);

Map<String, BeverageNutritionDto> nutritionsMap =
beverageNutritions.stream()
.collect(
Collectors.toMap(
BeverageNutritionDto::size,
Function.identity(),
(existing, replacement) -> existing));

return CafeBeverage.of(
name,
UUID.randomUUID(),
cafeStore,
image,
type,
beverageTemperature,
nutritionsMap);
beverageNutritions == null ? Map.of() : beverageNutritions);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package be.ddd.application.batch.dto.deserializer;

import be.ddd.application.batch.dto.BeverageNutritionDto;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* Jackson deserializer that accepts both array- and map-shaped beverage nutrition payloads.
*
* <p>Lambda 응답이 {@code ["size": "..."]} 리스트이든 {@code {"TALL": {...}}} 맵이든 동일하게 파싱한다. 맵 형태일 경우 키를
* size 값으로 주입한다.
*/
public class BeverageNutritionsDeserializer
extends JsonDeserializer<Map<String, BeverageNutritionDto>> {

@Override
public Map<String, BeverageNutritionDto> deserialize(
JsonParser parser, DeserializationContext ctxt) throws IOException {
JsonNode node = parser.getCodec().readTree(parser);
if (node == null || node.isNull()) {
return Map.of();
}

ObjectMapper mapper = (ObjectMapper) parser.getCodec();
Map<String, BeverageNutritionDto> result = new LinkedHashMap<>();

if (node.isArray()) {
for (JsonNode element : node) {
BeverageNutritionDto dto = mapper.treeToValue(element, BeverageNutritionDto.class);
String sizeKey =
element.hasNonNull("size") ? element.get("size").asText() : dto.size();
if (sizeKey == null || sizeKey.isBlank()) {
continue;
}
result.put(sizeKey, dto.withSize(sizeKey));
}
} else if (node.isObject()) {
node.fields()
.forEachRemaining(
entry -> {
try {
BeverageNutritionDto dto =
mapper.treeToValue(
entry.getValue(), BeverageNutritionDto.class);
result.put(entry.getKey(), dto.withSize(entry.getKey()));
} catch (IOException e) {
throw new RuntimeException(e);
}
});
} else if (node.isValueNode() && node.asToken() == JsonToken.VALUE_NULL) {
return Map.of();
}

return result;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package be.ddd.application.notification;

import be.ddd.application.discord.DiscordNotificationService;
import be.ddd.common.util.CustomClock;
import be.ddd.common.util.KoreanTimeService;
import be.ddd.domain.entity.member.Member;
import be.ddd.domain.repo.IntakeHistoryRepository;
import be.ddd.domain.repo.MemberRepository;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
Expand All @@ -27,12 +27,14 @@ public class NotificationSchedulingService {
private final IntakeHistoryRepository intakeHistoryRepository;
private final FCMService fcmService;
private final DiscordNotificationService discordNotificationService;
private final KoreanTimeService koreanTimeService;

@Async
@Scheduled(cron = "0 * * * * *")
public void sendSugarIntakeNotifications() {
LocalTime now = CustomClock.now().toLocalTime();
log.info("[NotificationTest] 1. Current fake time: {}", now);
ZonedDateTime koreaNow = koreanTimeService.now();
LocalTime now = koreaNow.toLocalTime();
log.info("[NotificationTest] 1. Current KST time: {}", koreaNow);

List<Member> members = memberRepository.findAllByNotificationEnabledAndReminderTime(now);
log.info(
Expand All @@ -49,7 +51,8 @@ public void sendSugarIntakeNotifications() {
members.stream().map(Member::getProviderId).collect(Collectors.toList());

Map<Long, Double> totalSugars =
intakeHistoryRepository.sumSugarByMemberIdsAndDate(memberIds, LocalDateTime.now());
intakeHistoryRepository.sumSugarByMemberIdsAndDate(
memberIds, koreaNow.toLocalDateTime());
log.info(
"[NotificationTest] 3. Found sugar intake data for {} members.",
totalSugars.size());
Expand Down
36 changes: 36 additions & 0 deletions src/main/java/be/ddd/common/util/KoreanTimeService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package be.ddd.common.util;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import org.springframework.stereotype.Component;

/**
* Provides the current time in Korean Standard Time (Asia/Seoul).
*
* <p>This service wraps {@link CustomClock} so tests that fix the clock keep working while giving
* callers stable {@link ZoneId} aware timestamps.
*/
@Component
public class KoreanTimeService {

private static final ZoneId KST = ZoneId.of("Asia/Seoul");

public ZonedDateTime now() {
return CustomClock.now().atZone(KST);
}

public LocalDateTime currentDateTime() {
return now().toLocalDateTime();
}

public LocalDate currentDate() {
return now().toLocalDate();
}

public LocalTime currentTime() {
return now().toLocalTime();
}
}
75 changes: 44 additions & 31 deletions src/main/java/be/ddd/domain/entity/crawling/CafeBeverage.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public class CafeBeverage extends BaseTimeEntity {
private BeverageType beverageType;

@Enumerated(EnumType.STRING)
private SugarLevel sugarLevel;
private SugarLevel sugarLevel = SugarLevel.HIGH;

@OneToMany(mappedBy = "cafeBeverage", cascade = CascadeType.ALL, orphanRemoval = true)
private List<BeverageSizeInfo> sizes = new ArrayList<>();
Expand All @@ -50,7 +50,9 @@ public void updateFromDto(LambdaBeverageDto dto) {
/* log.info(
"[DEBUG] Updating beverage: '{}'. DTO contains sizes: {}. DB entity has sizes: {}",
this.name,
dto.beverageNutritions().stream().map(n -> n.size()).collect(Collectors.toList()),
dto.beverageNutritions() == null
? List.of()
: dto.beverageNutritions().keySet(),
this.sizes.stream().map(BeverageSizeInfo::getSizeType).collect(Collectors.toList()));
*/
if (dto.image() != null) {
Expand All @@ -62,33 +64,34 @@ public void updateFromDto(LambdaBeverageDto dto) {
BeverageType.valueOf(dto.beverageType().toUpperCase().replace(" ", "_"));
}

if (dto.beverageNutritions() != null && !dto.beverageNutritions().isEmpty()) {
Map<String, BeverageNutritionDto> nutritionMap = dto.beverageNutritions();
if (nutritionMap != null && !nutritionMap.isEmpty()) {
Map<BeverageSize, BeverageSizeInfo> existingSizes =
this.sizes.stream()
.collect(
Collectors.toMap(
BeverageSizeInfo::getSizeType, Function.identity()));

dto.beverageNutritions()
.forEach(
nutritionDto -> {
BeverageSize size = BeverageSize.fromString(nutritionDto.size());
if (size == null) {
// Log a warning or handle the unknown size as appropriate
return;
}
BeverageNutrition nutrition = BeverageNutrition.from(nutritionDto);
if (existingSizes.containsKey(size)) {
existingSizes.get(size).updateBeverageNutrition(nutrition);
} else {
addSizeInfo(new BeverageSizeInfo(this, size, nutrition));
}
});

// 대표 당 레벨 업데이트 (예: TALL 사이즈 기준)
nutritionMap.forEach(
(sizeKey, nutritionDto) -> {
BeverageSize size = BeverageSize.fromString(sizeKey);
if (size == null) {
// Log a warning or handle the unknown size as appropriate
return;
}
BeverageNutrition nutrition = BeverageNutrition.from(nutritionDto);
if (existingSizes.containsKey(size)) {
existingSizes.get(size).updateBeverageNutrition(nutrition);
} else {
addSizeInfo(new BeverageSizeInfo(this, size, nutrition));
}
});

// 대표 당 레벨 업데이트 (예: TALL 우선, 없으면 첫 사이즈)
this.sizes.stream()
.filter(s -> s.getSizeType() == BeverageSize.TALL)
.findFirst()
.or(() -> this.sizes.stream().findFirst())
.ifPresent(
sizeInfo -> {
BeverageNutrition nutrition = sizeInfo.getBeverageNutrition();
Expand All @@ -99,6 +102,10 @@ public void updateFromDto(LambdaBeverageDto dto) {
sizeInfo.getSizeType().getVolume());
}
});

if (this.sugarLevel == null) {
this.sugarLevel = SugarLevel.HIGH;
}
}
}

Expand Down Expand Up @@ -127,17 +134,19 @@ public static CafeBeverage of(
beverage.imgUrl = imgUrl;
beverage.beverageType = beverageType;

beverageNutritions.forEach(
(sizeStr, nutritionDto) -> {
BeverageSize size = BeverageSize.fromString(sizeStr);
if (size == null) {
// Log a warning or handle the unknown size as appropriate
return;
}
BeverageNutrition nutrition = BeverageNutrition.from(nutritionDto);
BeverageSizeInfo sizeInfo = new BeverageSizeInfo(beverage, size, nutrition);
beverage.sizes.add(sizeInfo);
});
if (beverageNutritions != null) {
beverageNutritions.forEach(
(sizeStr, nutritionDto) -> {
BeverageSize size = BeverageSize.fromString(sizeStr);
if (size == null) {
// Log a warning or handle the unknown size as appropriate
return;
}
BeverageNutrition nutrition = BeverageNutrition.from(nutritionDto);
BeverageSizeInfo sizeInfo = new BeverageSizeInfo(beverage, size, nutrition);
beverage.sizes.add(sizeInfo);
});
}

beverage.sizes.stream()
.filter(s -> s.getSizeType() == BeverageSize.TALL)
Expand All @@ -154,6 +163,10 @@ public static CafeBeverage of(
}
});

if (beverage.sugarLevel == null) {
beverage.sugarLevel = SugarLevel.HIGH;
}

return beverage;
}

Expand Down