diff --git a/src/main/java/com/example/want/api/block/controller/BlockController.java b/src/main/java/com/example/want/api/block/controller/BlockController.java index 3b8f07e..91e4d03 100644 --- a/src/main/java/com/example/want/api/block/controller/BlockController.java +++ b/src/main/java/com/example/want/api/block/controller/BlockController.java @@ -136,7 +136,7 @@ public ResponseEntity deleteBlock(@AuthenticationPrincipal UserInf return new ResponseEntity<>(commonResDto, HttpStatus.OK); } - @GetMapping("/city/{stateId}") + @GetMapping("/city/stateId") public ResponseEntity getBlocksByCity(@PathVariable Long stateId) { List blocks = blockService.getBlocksByState(stateId); return new ResponseEntity<>(new CommonResDto(HttpStatus.OK, "Success", blocks), HttpStatus.OK); diff --git a/src/main/java/com/example/want/api/block/domain/Block.java b/src/main/java/com/example/want/api/block/domain/Block.java index 67ad909..390ef3d 100644 --- a/src/main/java/com/example/want/api/block/domain/Block.java +++ b/src/main/java/com/example/want/api/block/domain/Block.java @@ -1,6 +1,7 @@ package com.example.want.api.block.domain; import com.example.want.api.block.dto.BlockDetailRsDto; +import com.example.want.api.location.domain.Location; import com.example.want.api.project.domain.Project; import com.example.want.common.BaseEntity; import lombok.AllArgsConstructor; @@ -29,6 +30,10 @@ public class Block extends BaseEntity { @Enumerated(EnumType.STRING) private Category category; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "location_id") + private Location location; + private Double latitude; private Double longitude; @@ -36,8 +41,9 @@ public class Block extends BaseEntity { private LocalDateTime endTime; private String isActivated; private Long heartCount; + private Long popularCount; private String isDeleted; - + @Builder.Default private boolean isHearted = false; @@ -54,12 +60,13 @@ public BlockDetailRsDto toDetailDto() { .content(this.content) .placeName(this.placeName) .category(this.category) - .latitude(this.latitude) - .longitude(this.longitude) + .latitude(this.getLatitude()) + .longitude(this.getLongitude()) .startTime(this.startTime != null ? this.startTime.toString() : null) .endTime(this.endTime != null ? this.endTime.toString() : null) .isActivated(this.isActivated) .heartCount(this.heartCount) + .popularCount(this.getPopularCount()) .isHearted(this.isHearted) .projectId(this.project.getId()) .build(); @@ -99,9 +106,10 @@ public void changeIsActivated(String isActivated) { this.isActivated = isActivated; } - public void updatePoint(Double latitude, Double longitude) { - this.latitude = latitude; - this.longitude = longitude; + public void updatePoint(Double latitude, Double longitude, String placeName) { + this.latitude = latitude; + this.longitude = longitude; + this.placeName = placeName; } @@ -114,4 +122,8 @@ public void initializeFields() { public void changeIsDelete() { this.isDeleted = "Y"; } + + public void updateLocation(Location location) { + this.location = location; + } } diff --git a/src/main/java/com/example/want/api/block/dto/BlockActiveListRsDto.java b/src/main/java/com/example/want/api/block/dto/BlockActiveListRsDto.java index efeb9ad..79ac40e 100644 --- a/src/main/java/com/example/want/api/block/dto/BlockActiveListRsDto.java +++ b/src/main/java/com/example/want/api/block/dto/BlockActiveListRsDto.java @@ -21,6 +21,7 @@ public class BlockActiveListRsDto { private String startTime; private String endTime; private Long heartCount; + private Long popularCount; private Category category; private String isActivated; private Boolean isHearted; @@ -38,6 +39,7 @@ public static BlockActiveListRsDto fromEntity(Block block) { .startTime(block.getStartTime() != null ? block.getStartTime().toString() : null) .endTime(block.getEndTime() != null ? block.getEndTime().toString() : null) .heartCount(block.getHeartCount()) + .popularCount(block.getPopularCount()) .isActivated(block.getIsActivated()) .build(); } diff --git a/src/main/java/com/example/want/api/block/dto/BlockDetailRsDto.java b/src/main/java/com/example/want/api/block/dto/BlockDetailRsDto.java index 9e1396a..abf62b2 100644 --- a/src/main/java/com/example/want/api/block/dto/BlockDetailRsDto.java +++ b/src/main/java/com/example/want/api/block/dto/BlockDetailRsDto.java @@ -1,6 +1,7 @@ package com.example.want.api.block.dto; import com.example.want.api.block.domain.Category; +import com.example.want.api.location.domain.Location; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -23,6 +24,7 @@ public class BlockDetailRsDto { private String endTime; private String isActivated; private Long heartCount; + private Long popularCount; private Boolean isHearted; private Long projectId; } diff --git a/src/main/java/com/example/want/api/block/service/BlockService.java b/src/main/java/com/example/want/api/block/service/BlockService.java index 87d6643..0538ffc 100644 --- a/src/main/java/com/example/want/api/block/service/BlockService.java +++ b/src/main/java/com/example/want/api/block/service/BlockService.java @@ -6,6 +6,8 @@ import com.example.want.api.block.repository.BlockRepository; import com.example.want.api.heart.domain.Heart; import com.example.want.api.heart.repository.HeartRepository; +import com.example.want.api.location.domain.Location; +import com.example.want.api.location.repository.LocationRepository; import com.example.want.api.member.domain.Member; import com.example.want.api.member.login.UserInfo; import com.example.want.api.member.repository.MemberRepository; @@ -17,7 +19,6 @@ import com.example.want.api.sse.NotificationService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -48,12 +49,15 @@ public class BlockService { private final ProjectRepository projectRepository; private final ProjectMemberRepository projectMemberRepository; private final StateRepository stateRepository; + private final LocationRepository locationRepository; private final StringRedisTemplate stringRedisTemplate; private final NotificationService notificationService; @Qualifier("heart") private final RedisTemplate heartRedisTemplate; + @Qualifier("popular") + private final RedisTemplate popularRedisTemplate; @Transactional public Block createBlock(CreateBlockRqDto request, UserInfo userInfo) { @@ -182,7 +186,6 @@ private Long getLikesCountFromCache(String key, String hashKey) { } else if (cachedValue instanceof Integer) { return ((Integer) cachedValue).longValue(); } - return null; } @@ -324,7 +327,15 @@ public BlockDetailRsDto updateBlock(Long id, UpdateBlockRqDto updateBlockRqDto, block.updatePlaceName(updateBlockRqDto.getPlaceName()); } if (updateBlockRqDto.getLatitude() != null && updateBlockRqDto.getLongitude() != null) { - block.updatePoint(updateBlockRqDto.getLatitude(), updateBlockRqDto.getLongitude()); + block.updatePoint(updateBlockRqDto.getLatitude(), updateBlockRqDto.getLongitude(), updateBlockRqDto.getPlaceName()); + String redisKey = updateBlockRqDto.getLatitude() + ":" + updateBlockRqDto.getLongitude(); + + // Redis에서 값을 가져오거나 초기화 + if (popularRedisTemplate.opsForValue().get(redisKey) == null) { + popularRedisTemplate.opsForValue().set(redisKey, 0L); + } + // 레디스에서 해당 위치의 popularCount +1 + popularRedisTemplate.opsForValue().increment(redisKey, 1L); } if (updateBlockRqDto.getStartTime() != null && updateBlockRqDto.getEndTime() != null) { @@ -393,7 +404,15 @@ public Block importBlock(UserInfo userInfo, ImportBlockRqDto importDto) { Project project = validateProjectMember(importDto.getProjectId(), userInfo.getEmail()); Block findBlock = blockRepository.findById(importDto.getBlockId()).orElseThrow(() -> new EntityNotFoundException("해당 블록을 찾을 수 없습니다.")); Block block = importDto.toImport(findBlock, project); - System.out.println(block); + + String redisKey = findBlock.getLatitude() + ":" + findBlock.getLongitude(); + + // Redis에서 값을 가져오거나 초기화 + if (popularRedisTemplate.opsForValue().get(redisKey) == null) { + popularRedisTemplate.opsForValue().set(redisKey, 0L); + } + // 레디스에서 해당 위치의 popularCount +1 + popularRedisTemplate.opsForValue().increment(redisKey, 1L); return blockRepository.save(block); } diff --git a/src/main/java/com/example/want/api/location/controller/LocationController.java b/src/main/java/com/example/want/api/location/controller/LocationController.java new file mode 100644 index 0000000..011ada6 --- /dev/null +++ b/src/main/java/com/example/want/api/location/controller/LocationController.java @@ -0,0 +1,39 @@ +package com.example.want.api.location.controller; + +import com.example.want.api.location.domain.Location; +import com.example.want.api.location.dto.LocationResDto; +import com.example.want.api.location.repository.LocationRepository; +import com.example.want.api.location.service.LocationService; +import com.example.want.common.CommonResDto; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1") +public class LocationController { + + private final LocationService locationService; + private final LocationRepository locationRepository; + + // 추천 블럭 리스트에 사용할 코드 + @GetMapping("/city/{stateId}") + public ResponseEntity getPopularLocations(@PathVariable Long stateId) { + List popularLocations = locationService.getPopularLocations(stateId); + CommonResDto commonResDto = new CommonResDto(HttpStatus.OK, "조회 완료", popularLocations); + return new ResponseEntity<>(commonResDto, HttpStatus.OK); + } + + @GetMapping("/pop") + public ResponseEntity getPopularCountFromCache() { + locationService.getPopularCountFromCache(); + List popularLocations = locationRepository.findAll(); + CommonResDto commonResDto = new CommonResDto(HttpStatus.OK, "조회 완료", popularLocations); + return new ResponseEntity<>(commonResDto, HttpStatus.OK); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/want/api/location/domain/Location.java b/src/main/java/com/example/want/api/location/domain/Location.java new file mode 100644 index 0000000..5acbbb1 --- /dev/null +++ b/src/main/java/com/example/want/api/location/domain/Location.java @@ -0,0 +1,46 @@ +package com.example.want.api.location.domain; + +import com.example.want.api.location.dto.LocationResDto; +import com.example.want.api.state.domain.State; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import javax.persistence.*; +import javax.validation.constraints.NotNull; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Location { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Double latitude; + private Double longitude; + + private Long popularCount; + private String placeName; + + @ManyToOne(fetch = FetchType.LAZY) + @NotNull + @JoinColumn(name = "state_id") + private State state; + + public void updatePopularCount(Long popularCount) { + this.popularCount += popularCount; + } + + public LocationResDto fromEntity(Location location) { + LocationResDto dto = LocationResDto.builder() + .latitude(location.getLatitude()) + .longitude(location.getLongitude()) + .popularCount(location.getPopularCount()) + .placeName(location.getPlaceName()) + .build(); + return dto; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/want/api/location/dto/LocationReqDto.java b/src/main/java/com/example/want/api/location/dto/LocationReqDto.java new file mode 100644 index 0000000..3ce8ac6 --- /dev/null +++ b/src/main/java/com/example/want/api/location/dto/LocationReqDto.java @@ -0,0 +1,29 @@ +package com.example.want.api.location.dto; + + +import com.example.want.api.location.domain.Location; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class LocationReqDto { + private Double latitude; + private Double longitude; + + public static LocationReqDto toEntity(Location location) { + LocationReqDto dto = LocationReqDto.builder() + .latitude(location.getLatitude()) + .longitude(location.getLongitude()) + .build(); + return dto; + } + + + +} \ No newline at end of file diff --git a/src/main/java/com/example/want/api/location/dto/LocationResDto.java b/src/main/java/com/example/want/api/location/dto/LocationResDto.java new file mode 100644 index 0000000..4a6e3c6 --- /dev/null +++ b/src/main/java/com/example/want/api/location/dto/LocationResDto.java @@ -0,0 +1,17 @@ +package com.example.want.api.location.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class LocationResDto { + private Double latitude; + private Double longitude; + private Long popularCount; + private String placeName; +} diff --git a/src/main/java/com/example/want/api/location/repository/LocationRepository.java b/src/main/java/com/example/want/api/location/repository/LocationRepository.java new file mode 100644 index 0000000..5766b98 --- /dev/null +++ b/src/main/java/com/example/want/api/location/repository/LocationRepository.java @@ -0,0 +1,14 @@ +package com.example.want.api.location.repository; + +import com.example.want.api.location.domain.Location; +import com.example.want.api.location.dto.LocationResDto; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface LocationRepository extends JpaRepository { + Location findByLatitudeAndLongitude(Double latitude, Double longitude); + List findAllByStateIdOrderByPopularCountDesc(Long stateId); +} \ No newline at end of file diff --git a/src/main/java/com/example/want/api/location/service/LocationService.java b/src/main/java/com/example/want/api/location/service/LocationService.java new file mode 100644 index 0000000..ac0f8c3 --- /dev/null +++ b/src/main/java/com/example/want/api/location/service/LocationService.java @@ -0,0 +1,84 @@ +package com.example.want.api.location.service; + +import com.example.want.api.block.repository.BlockRepository; +import com.example.want.api.location.domain.Location; +import com.example.want.api.location.dto.LocationResDto; +import com.example.want.api.location.repository.LocationRepository; +import com.example.want.api.project.repository.ProjectRepository; +import com.example.want.api.state.repository.StateRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class LocationService { + + private final BlockRepository blockRepository; + private final LocationRepository locationRepository; + private final ProjectRepository projectRepository; + private final StateRepository stateRepository; + + @Qualifier("popular") + private final RedisTemplate popularRedisTemplate; + + @Transactional + public List getPopularLocations(Long stateId) { + List findLocation = locationRepository.findAllByStateIdOrderByPopularCountDesc(stateId); + List locations = new ArrayList<>(); + for (Location location : findLocation) { + LocationResDto dto = location.fromEntity(location); + locations.add(dto); + } + return locations; + } + + @Scheduled(cron = "0 0 4 * * *") // 매일 오전 4시 스케줄링 돌기 + @Transactional + public void getPopularCountFromCache() { + ValueOperations valueOperations = popularRedisTemplate.opsForValue(); + + // Redis 에 저장된 모든 key 를 가져옴. + Set keys = popularRedisTemplate.keys("*"); + + if (keys != null) { + for (String cacheKey : keys) { + Long cacheValue = valueOperations.get(cacheKey); + if (cacheValue != null) { + // key는 "위도:경도" 형식. 이를 분리. + String[] findKey = cacheKey.split(":"); + + Double latitude = Double.parseDouble(findKey[0]); + Double longitude = Double.parseDouble(findKey[1]); + + // 해당 위치를 LocationRepository 에서 찾기 + Location location = locationRepository.findByLatitudeAndLongitude(latitude, longitude); + if (location == null) { + Location newLocation = Location.builder() + .latitude(latitude) + .longitude(longitude) + .popularCount(cacheValue) + .build(); + locationRepository.save(newLocation); + } + else{ + location.updatePopularCount(cacheValue); + } + popularRedisTemplate.delete(cacheKey); + } + + } + + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/want/api/project/repository/ProjectRepository.java b/src/main/java/com/example/want/api/project/repository/ProjectRepository.java index 793df00..dc0112e 100644 --- a/src/main/java/com/example/want/api/project/repository/ProjectRepository.java +++ b/src/main/java/com/example/want/api/project/repository/ProjectRepository.java @@ -27,4 +27,5 @@ public interface ProjectRepository extends JpaRepository { @Query("select p from Project p join fetch p.projectMembers pm join fetch pm.member where p.id = :projectId") Optional findProjectWithDetails(Long projectId); + } \ No newline at end of file diff --git a/src/main/java/com/example/want/api/state/domain/Location.java b/src/main/java/com/example/want/api/state/domain/Location.java deleted file mode 100644 index ff76e30..0000000 --- a/src/main/java/com/example/want/api/state/domain/Location.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.want.api.state.domain; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import javax.annotation.security.DenyAll; -import javax.persistence.*; - -@Entity -@Getter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class Location { -// 블록에 추가되면 로케이션에 갱신 - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String country; - private String city; - - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "state_id") - private State state; - - -} diff --git a/src/main/java/com/example/want/api/state/repository/StateRepository.java b/src/main/java/com/example/want/api/state/repository/StateRepository.java index 62bc84b..1042ef3 100644 --- a/src/main/java/com/example/want/api/state/repository/StateRepository.java +++ b/src/main/java/com/example/want/api/state/repository/StateRepository.java @@ -16,4 +16,5 @@ public interface StateRepository extends JpaRepository { Optional findByCountryAndCity(String country, String city); List findAllByCityIsNotNull(); + } diff --git a/src/main/java/com/example/want/config/RedisConfig.java b/src/main/java/com/example/want/config/RedisConfig.java index b284ed4..12ea99b 100644 --- a/src/main/java/com/example/want/config/RedisConfig.java +++ b/src/main/java/com/example/want/config/RedisConfig.java @@ -20,6 +20,7 @@ import org.springframework.data.redis.listener.ChannelTopic; import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.data.redis.listener.adapter.MessageListenerAdapter; +import org.springframework.data.redis.serializer.GenericToStringSerializer; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @@ -183,4 +184,42 @@ public void onMessage(Message message, byte[] pattern) { public ChannelTopic topic() { return new ChannelTopic("project:notifications"); } + + + @Bean + @Qualifier("popular") + public LettuceConnectionFactory popularConnectionFactory() { + final SocketOptions socketOptions = SocketOptions.builder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + + final ClientOptions clientOptions = ClientOptions.builder() + .socketOptions(socketOptions) + .build(); + + LettuceClientConfiguration lettuceClientConfiguration = LettuceClientConfiguration.builder() + .clientOptions(clientOptions) + .commandTimeout(Duration.ofMinutes(1)) + .shutdownTimeout(Duration.ZERO) + .build(); + + RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(host, port); + redisStandaloneConfiguration.setDatabase(3); // 3번 데이터베이스 사용 + + return new LettuceConnectionFactory(redisStandaloneConfiguration, lettuceClientConfiguration); + } + + @Bean + @Qualifier("popular") + public RedisTemplate popularRedisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(popularConnectionFactory()); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new GenericToStringSerializer<>(Long.class)); // Long 타입에 맞게 설정 + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashValueSerializer(new GenericToStringSerializer<>(Long.class)); // Long 타입에 맞게 설정 + redisTemplate.afterPropertiesSet(); + return redisTemplate; + } + }