Skip to content
Merged
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
@@ -0,0 +1,10 @@
package devkor.com.teamcback.domain.ble.event;

import java.time.LocalDateTime;

public record PlaceBecameVacantEvent(
Long placeId,
Long bleDataId,
LocalDateTime occurredAt
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@

public interface BLEDataRepository extends JpaRepository<BLEData, Long> {
Optional<BLEData> findTopByDeviceOrderByLastTimeDesc(BLEDevice device);
Optional<BLEData> findTopByDeviceOrderByLastTimeDescIdDesc(BLEDevice device);
List<BLEData> findAllByDeviceAndLastTimeBetweenOrderByLastTimeAsc(BLEDevice device, LocalDateTime start, LocalDateTime end);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import devkor.com.teamcback.domain.ble.entity.BLEData;
import devkor.com.teamcback.domain.ble.entity.BLEDevice;
import devkor.com.teamcback.domain.ble.entity.BLEstatus;
import devkor.com.teamcback.domain.ble.event.PlaceBecameVacantEvent;
import devkor.com.teamcback.domain.ble.repository.BLEDataRepository;
import devkor.com.teamcback.domain.ble.repository.BLEDeviceRepository;
import devkor.com.teamcback.domain.place.entity.Place;
Expand All @@ -17,6 +18,7 @@
import devkor.com.teamcback.global.response.ResultCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -34,6 +36,7 @@ public class BLEService {
private final BLEDeviceRepository bledeviceRepository;
private final BLEDataRepository bleDataRepository;
private final PlaceRepository placeRepository;
private final ApplicationEventPublisher eventPublisher;

// 평균 구해올 시간대 라벨
private static final int[] TIME_SLOTS = {7, 10, 13, 16, 19, 22};
Expand All @@ -46,6 +49,11 @@ public class BLEService {
@Transactional
public UpdateBLERes updateBLE(UpdateBLEReq updateBLEReq) {
BLEDevice bleDevice = bledeviceRepository.findByDeviceName(updateBLEReq.getDeviceName());
if (bleDevice == null) {
throw new GlobalException(ResultCode.NOT_FOUND_DEVICE_NAME);
}
BLEData previousData = bleDataRepository.findTopByDeviceOrderByLastTimeDescIdDesc(bleDevice)
.orElse(null);
int capacity = bleDevice.getCapacity();
int people = getBlEPeople(updateBLEReq.getLastCount(), bleDevice);
double final_ratio = (double) people / capacity;
Expand All @@ -61,9 +69,29 @@ public UpdateBLERes updateBLE(UpdateBLEReq updateBLEReq) {
bleData.setLastTime(updateBLEReq.getLastTime());
bleDataRepository.save(bleData);

if (becameVacant(previousData, status) && bleDevice.getPlace() != null) {
eventPublisher.publishEvent(new PlaceBecameVacantEvent(
bleDevice.getPlace().getId(),
bleData.getId(),
bleData.getLastTime()
));
}

return new UpdateBLERes(bleData);
}

private boolean becameVacant(
BLEData previousData,
BLEstatus newStatus
) {
if (!BLEstatus.VACANT.equals(newStatus) || previousData == null) {
return false;
}

return BLEstatus.AVAILABLE.equals(previousData.getLastStatus())
|| BLEstatus.CROWDED.equals(previousData.getLastStatus());
}

private int getBlEPeople(int lastCount, BLEDevice bleDevice) {
if (bleDevice == null) throw new GlobalException(ResultCode.NOT_FOUND_DEVICE_NAME);
double ratio = bleDevice.getRatio();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,15 @@ List<Category> findCategoriesByUserAndLocationTypeAndLocationId(

@Query("SELECT c FROM Category c LEFT JOIN FETCH c.categoryBookmarkList WHERE c.user = :user")
List<Category> findByUser(@Param("user") User user);

@Query("""
SELECT DISTINCT c.user.userId FROM CategoryBookmark cb
JOIN cb.category c
JOIN cb.bookmark b
WHERE c.user IS NOT NULL AND b.locationType = :locationType AND b.locationId = :locationId
""")
List<Long> findDistinctUserIdsByLocationTypeAndLocationId(
@Param("locationType") LocationType locationType,
@Param("locationId") Long locationId
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package devkor.com.teamcback.domain.character.event;

public record CharacterUnlockedEvent(
Long userId,
Long characterId,
Long userCharacterId,
String characterName
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import devkor.com.teamcback.domain.character.dto.response.ModifyCharacterRes;
import devkor.com.teamcback.domain.character.entity.KoCharacter;
import devkor.com.teamcback.domain.character.entity.UserCharacter;
import devkor.com.teamcback.domain.character.event.CharacterUnlockedEvent;
import devkor.com.teamcback.domain.character.repository.CharacterRepository;
import devkor.com.teamcback.domain.character.repository.UserCharacterRepository;
import devkor.com.teamcback.domain.user.entity.Level;
Expand All @@ -26,6 +27,7 @@
import devkor.com.teamcback.infra.s3.S3Util;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
Expand All @@ -37,6 +39,7 @@ public class AdminStoreService {
private final UserCharacterRepository userCharacterRepository;
private final UserRepository userRepository;
private final S3Util s3Util;
private final ApplicationEventPublisher eventPublisher;

/**
* 캐릭터 목록 조회 (비활성 포함)
Expand Down Expand Up @@ -119,7 +122,13 @@ public GrantCharacterRes grantCharacter(Long characterId, Long userId) {
throw new GlobalException(ALREADY_OWNED_CHARACTER);
}

UserCharacter userCharacter = userCharacterRepository.save(new UserCharacter(user, character));
UserCharacter userCharacter = userCharacterRepository.saveAndFlush(new UserCharacter(user, character));
eventPublisher.publishEvent(new CharacterUnlockedEvent(
user.getUserId(),
character.getCharacterId(),
userCharacter.getUserCharacterId(),
character.getName()
));

return new GrantCharacterRes(userCharacter.getUserCharacterId());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import devkor.com.teamcback.domain.character.entity.KoCharacter;
import devkor.com.teamcback.domain.character.entity.PurchaseStatus;
import devkor.com.teamcback.domain.character.entity.UserCharacter;
import devkor.com.teamcback.domain.character.event.CharacterUnlockedEvent;
import devkor.com.teamcback.domain.character.repository.CharacterRepository;
import devkor.com.teamcback.domain.character.repository.UserCharacterRepository;
import devkor.com.teamcback.domain.user.entity.Level;
Expand All @@ -30,6 +31,7 @@
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -40,6 +42,7 @@ public class StoreService {
private final CharacterRepository characterRepository;
private final UserCharacterRepository userCharacterRepository;
private final UserRepository userRepository;
private final ApplicationEventPublisher eventPublisher;

/**
* 스토어 조회 (보유 포인트 + 캐릭터 목록)
Expand Down Expand Up @@ -118,6 +121,12 @@ public PurchaseCharacterRes purchaseCharacter(Long userId, Long characterId) {

try {
UserCharacter userCharacter = userCharacterRepository.saveAndFlush(new UserCharacter(user, character));
eventPublisher.publishEvent(new CharacterUnlockedEvent(
user.getUserId(),
character.getCharacterId(),
userCharacter.getUserCharacterId(),
character.getName()
));
return new PurchaseCharacterRes(userCharacter, user.getPoint());
} catch (DataIntegrityViolationException e) { // 동시 중복 구매는 UNIQUE 제약으로 차단 (롤백으로 차감 복구)
throw new GlobalException(ALREADY_OWNED_CHARACTER);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
package devkor.com.teamcback.domain.notification.controller;

import devkor.com.teamcback.domain.notification.dto.request.AdminPushDispatchReq;
import devkor.com.teamcback.domain.notification.dto.request.UpdatePushEventFlagReq;
import devkor.com.teamcback.domain.notification.dto.response.AdminPushDispatchDetailRes;
import devkor.com.teamcback.domain.notification.dto.response.AdminPushDispatchPreviewRes;
import devkor.com.teamcback.domain.notification.dto.response.AdminPushDispatchSummaryRes;
import devkor.com.teamcback.domain.notification.dto.response.AdminPushEventFlagRes;
import devkor.com.teamcback.domain.notification.dto.response.AdminPushInstallationRes;
import devkor.com.teamcback.domain.notification.dto.response.PushDispatchEnqueueRes;
import devkor.com.teamcback.domain.notification.entity.type.AppVariant;
import devkor.com.teamcback.domain.notification.entity.type.PushEventType;
import devkor.com.teamcback.domain.notification.entity.type.PushDispatchStatus;
import devkor.com.teamcback.domain.notification.service.AdminNotificationService;
import devkor.com.teamcback.domain.notification.service.PushEventFlagService;
import devkor.com.teamcback.global.exception.exception.GlobalException;
import devkor.com.teamcback.global.response.CommonResponse;
import devkor.com.teamcback.global.response.ResultCode;
import devkor.com.teamcback.global.security.UserDetailsImpl;
import java.util.List;

Expand All @@ -20,6 +25,7 @@
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
Expand All @@ -38,6 +44,7 @@ public class AdminNotificationController {
private static final String DEFAULT_SIZE = "20";

private final AdminNotificationService adminNotificationService;
private final PushEventFlagService pushEventFlagService;

@Operation(
summary = "푸시 대상 installation 검색",
Expand Down Expand Up @@ -133,4 +140,21 @@ public CommonResponse<AdminPushDispatchDetailRes> getDispatch(
) {
return CommonResponse.success(adminNotificationService.getDispatch(dispatchId));
}

@GetMapping("/event-flags")
public CommonResponse<List<AdminPushEventFlagRes>> getEventFlags() {
return CommonResponse.success(pushEventFlagService.getFlags());
}

@PatchMapping("/event-flags/{eventType}")
public CommonResponse<AdminPushEventFlagRes> updateEventFlag(
@PathVariable PushEventType eventType,
@RequestBody UpdatePushEventFlagReq request
) {
if (request == null || request.enabled() == null) {
throw new GlobalException(ResultCode.INVALID_INPUT);
}

return CommonResponse.success(pushEventFlagService.updateFlag(eventType, request.enabled()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package devkor.com.teamcback.domain.notification.dto.request;

public record UpdatePushEventFlagReq(
Boolean enabled
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package devkor.com.teamcback.domain.notification.dto.response;

import devkor.com.teamcback.domain.notification.entity.type.PushEventType;

public record AdminPushEventFlagRes(
PushEventType eventType,
boolean enabled
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ public enum PushActionType {
BUS_STOP,
BUILDING_DETAIL,
PLACE_DETAIL,
CHARACTER_STORE,
TEST
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package devkor.com.teamcback.domain.notification.entity.type;

public enum PushEventType {
CROWD("push:event:crowd-enabled"),
REPORT("push:event:report-enabled"),
CHARACTER("push:event:character-enabled");

private final String redisKey;

PushEventType(String redisKey) {
this.redisKey = redisKey;
}

public String redisKey() {
return redisKey;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package devkor.com.teamcback.domain.notification.listener;

import devkor.com.teamcback.domain.character.event.CharacterUnlockedEvent;
import devkor.com.teamcback.domain.notification.dto.request.PushDispatchCommand;
import devkor.com.teamcback.domain.notification.entity.type.AppVariant;
import devkor.com.teamcback.domain.notification.entity.type.NotificationType;
import devkor.com.teamcback.domain.notification.entity.type.PushActionType;
import devkor.com.teamcback.domain.notification.entity.type.PushEventType;
import devkor.com.teamcback.domain.notification.entity.type.PushMode;
import devkor.com.teamcback.domain.notification.entity.type.PushTargetType;
import devkor.com.teamcback.domain.notification.repository.PushInstallationRepository;
import devkor.com.teamcback.domain.notification.service.PushDispatchService;
import devkor.com.teamcback.domain.notification.service.PushEventFlagService;
import devkor.com.teamcback.domain.notification.template.DomainPushContentFactory;
import devkor.com.teamcback.domain.notification.template.PushContent;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

@Slf4j
@Component
@RequiredArgsConstructor
public class CharacterUnlockedPushEventListener {

private static final Long SYSTEM_CREATED_BY = 0L;

private final PushInstallationRepository pushInstallationRepository;
private final PushDispatchService pushDispatchService;
private final PushEventFlagService pushEventFlagService;

@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handle(CharacterUnlockedEvent event) {
if (!pushEventFlagService.isEnabled(PushEventType.CHARACTER)) {
return;
}

try {
if (!pushInstallationRepository.existsByUserIdAndAppVariantAndActiveTrue(
event.userId(),
AppVariant.PRODUCTION
)) {
return;
}

PushContent content = DomainPushContentFactory.characterUnlocked(event.characterName());
pushDispatchService.enqueue(new PushDispatchCommand(
NotificationType.GENERAL,
PushMode.ACTUAL,
AppVariant.PRODUCTION,
PushTargetType.USER,
String.valueOf(event.userId()),
content.title(),
content.body(),
PushActionType.CHARACTER_STORE,
Map.of(),
"character-unlock:%d:%d:%d".formatted(
event.userId(),
event.characterId(),
event.userCharacterId()
),
SYSTEM_CREATED_BY
));
} catch (Exception e) {
log.warn(
"character unlock push failed: userId={}, characterId={}, userCharacterId={}, error={}",
event.userId(),
event.characterId(),
event.userCharacterId(),
e.getMessage()
);
}
}
}
Loading
Loading