diff --git a/src/main/java/in/koreatech/koin/domain/bus/controller/BusApi.java b/src/main/java/in/koreatech/koin/domain/bus/controller/BusApi.java index c13d74fa8..ad97209ad 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/controller/BusApi.java +++ b/src/main/java/in/koreatech/koin/domain/bus/controller/BusApi.java @@ -4,6 +4,7 @@ import java.util.List; import org.springframework.format.annotation.DateTimeFormat; +import in.koreatech.koin.domain.bus.dto.*; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -139,4 +140,14 @@ ResponseEntity getBusRouteSchedule( @RequestParam BusStation depart, @RequestParam BusStation arrival ); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), + } + ) + @Operation(summary = "버스 긴급 공지 조회") + @GetMapping("/notice") + ResponseEntity getNotice(); } diff --git a/src/main/java/in/koreatech/koin/domain/bus/controller/BusController.java b/src/main/java/in/koreatech/koin/domain/bus/controller/BusController.java index bfa8bd567..56399fa81 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/controller/BusController.java +++ b/src/main/java/in/koreatech/koin/domain/bus/controller/BusController.java @@ -1,26 +1,6 @@ package in.koreatech.koin.domain.bus.controller; -import java.time.LocalDate; -import java.time.LocalTime; -import java.util.List; - -import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import in.koreatech.koin.domain.bus.dto.BusCourseResponse; -import in.koreatech.koin.domain.bus.dto.BusRemainTimeResponse; -import in.koreatech.koin.domain.bus.dto.BusRouteCommand; -import in.koreatech.koin.domain.bus.dto.BusScheduleResponse; -import in.koreatech.koin.domain.bus.dto.BusTimetableResponse; -import in.koreatech.koin.domain.bus.dto.CityBusTimetableResponse; -import in.koreatech.koin.domain.bus.dto.ShuttleBusRoutesResponse; -import in.koreatech.koin.domain.bus.dto.ShuttleBusTimetableResponse; -import in.koreatech.koin.domain.bus.dto.SingleBusTimeResponse; +import in.koreatech.koin.domain.bus.dto.*; import in.koreatech.koin.domain.bus.model.BusTimetable; import in.koreatech.koin.domain.bus.model.enums.BusRouteType; import in.koreatech.koin.domain.bus.model.enums.BusStation; @@ -28,7 +8,15 @@ import in.koreatech.koin.domain.bus.model.enums.CityBusDirection; import in.koreatech.koin.domain.bus.service.BusService; import in.koreatech.koin.domain.bus.service.ShuttleBusService; +import in.koreatech.koin.domain.community.article.service.ArticleService; import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; @RestController @RequiredArgsConstructor @@ -113,4 +101,10 @@ public ResponseEntity getBusRouteSchedule( BusScheduleResponse busSchedule = busService.getBusSchedule(request); return ResponseEntity.ok().body(busSchedule); } + + @GetMapping("/notice") + public ResponseEntity getNotice() { + BusNoticeResponse busNoticeResponse = busService.getNotice(); + return ResponseEntity.ok().body(busNoticeResponse); + } } diff --git a/src/main/java/in/koreatech/koin/domain/bus/dto/BusNoticeResponse.java b/src/main/java/in/koreatech/koin/domain/bus/dto/BusNoticeResponse.java new file mode 100644 index 000000000..8c8b2dad5 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/bus/dto/BusNoticeResponse.java @@ -0,0 +1,21 @@ +package in.koreatech.koin.domain.bus.dto; + +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import io.swagger.v3.oas.annotations.media.Schema; + +import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; + +@JsonNaming(value = SnakeCaseStrategy.class) +public record BusNoticeResponse( + @Schema(description = "공지글 번호", example = "17153", requiredMode = NOT_REQUIRED) + Integer id, + + @Schema(description = "공지글 제목", example = "[긴급][총무팀]2024.11.27.(수, 오늘) 천안, 청주 야간 셔틀 20시 지연운행", requiredMode = NOT_REQUIRED) + String title +) { + + public static BusNoticeResponse of(Integer id, String title) { + return new BusNoticeResponse(id, title); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/bus/repository/BusNoticeRepository.java b/src/main/java/in/koreatech/koin/domain/bus/repository/BusNoticeRepository.java new file mode 100644 index 000000000..dbf38e63b --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/bus/repository/BusNoticeRepository.java @@ -0,0 +1,25 @@ +package in.koreatech.koin.domain.bus.repository; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import java.util.Map; + +@Repository +@RequiredArgsConstructor +public class BusNoticeRepository { + + public static final String BUS_NOTICE_KEY = "busNoticeArticle"; + private final RedisTemplate redisTemplate; + + public Map getBusNotice() { + Map article = redisTemplate.opsForHash().entries(BUS_NOTICE_KEY); + + if (article.isEmpty()) { + return null; + } + + return article; + } +} diff --git a/src/main/java/in/koreatech/koin/domain/bus/service/BusService.java b/src/main/java/in/koreatech/koin/domain/bus/service/BusService.java index 0bad5142f..f080681a5 100644 --- a/src/main/java/in/koreatech/koin/domain/bus/service/BusService.java +++ b/src/main/java/in/koreatech/koin/domain/bus/service/BusService.java @@ -1,31 +1,7 @@ package in.koreatech.koin.domain.bus.service; -import static in.koreatech.koin.domain.bus.model.enums.BusStation.getDirection; - -import java.time.Clock; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.ZonedDateTime; -import java.time.format.TextStyle; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Locale; -import java.util.Set; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import in.koreatech.koin.domain.bus.dto.BusCourseResponse; -import in.koreatech.koin.domain.bus.dto.BusRemainTimeResponse; -import in.koreatech.koin.domain.bus.dto.BusRouteCommand; -import in.koreatech.koin.domain.bus.dto.BusScheduleResponse; +import in.koreatech.koin.domain.bus.dto.*; import in.koreatech.koin.domain.bus.dto.BusScheduleResponse.ScheduleInfo; -import in.koreatech.koin.domain.bus.dto.BusTimetableResponse; -import in.koreatech.koin.domain.bus.dto.CityBusTimetableResponse; -import in.koreatech.koin.domain.bus.dto.SingleBusTimeResponse; import in.koreatech.koin.domain.bus.exception.BusIllegalStationException; import in.koreatech.koin.domain.bus.exception.BusTypeNotFoundException; import in.koreatech.koin.domain.bus.exception.BusTypeNotSupportException; @@ -39,6 +15,7 @@ import in.koreatech.koin.domain.bus.model.mongo.BusCourse; import in.koreatech.koin.domain.bus.model.mongo.CityBusTimetable; import in.koreatech.koin.domain.bus.model.mongo.Route; +import in.koreatech.koin.domain.bus.repository.BusNoticeRepository; import in.koreatech.koin.domain.bus.repository.BusRepository; import in.koreatech.koin.domain.bus.repository.CityBusTimetableRepository; import in.koreatech.koin.domain.bus.service.route.BusRouteStrategy; @@ -49,6 +26,14 @@ import in.koreatech.koin.domain.version.service.VersionService; import in.koreatech.koin.global.exception.KoinIllegalArgumentException; import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.*; +import java.time.format.TextStyle; +import java.util.*; + +import static in.koreatech.koin.domain.bus.model.enums.BusStation.getDirection; @Service @Transactional(readOnly = true) @@ -57,6 +42,7 @@ public class BusService { private final Clock clock; private final BusRepository busRepository; + private final BusNoticeRepository busNoticeRepository; private final CityBusTimetableRepository cityBusTimetableRepository; private final CityBusClient cityBusClient; private final ExpressBusService expressBusService; @@ -255,4 +241,17 @@ public BusScheduleResponse getBusSchedule(BusRouteCommand request) { scheduleInfoList ); } + + public BusNoticeResponse getNotice() { + Map article = busNoticeRepository.getBusNotice(); + + if (article == null || article.isEmpty()) { + return null; + } + + return BusNoticeResponse.of( + (Integer) article.get("id"), + (String) article.get("title") + ); + } } diff --git a/src/main/java/in/koreatech/koin/domain/community/article/model/redis/BusNoticeArticle.java b/src/main/java/in/koreatech/koin/domain/community/article/model/redis/BusNoticeArticle.java new file mode 100644 index 000000000..4c3e062d5 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/community/article/model/redis/BusNoticeArticle.java @@ -0,0 +1,30 @@ +package in.koreatech.koin.domain.community.article.model.redis; + +import in.koreatech.koin.domain.community.article.model.Article; +import org.springframework.data.annotation.Id; +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.redis.core.RedisHash; + +@Getter +@RedisHash(value = "busNoticeArticle") +public class BusNoticeArticle { + + @Id + private Integer id; + + private String title; + + @Builder + private BusNoticeArticle(Integer id, String title) { + this.id = id; + this.title = title; + } + + public static BusNoticeArticle from(Article article) { + return BusNoticeArticle.builder() + .id(article.getId()) + .title(article.getTitle()) + .build(); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/community/article/repository/ArticleRepository.java b/src/main/java/in/koreatech/koin/domain/community/article/repository/ArticleRepository.java index 339d4b78e..eb560a30b 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/repository/ArticleRepository.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/repository/ArticleRepository.java @@ -120,4 +120,9 @@ default Article getNextArticle(Board board, Article article) { @Query("SELECT a.title FROM Article a WHERE a.id = :id") String getTitleById(@Param("id") Integer id); + + @Query(value = "SELECT * FROM koreatech_articles a " + + "WHERE a.title REGEXP '통학버스|등교버스|셔틀버스|하교버스' " + + "ORDER BY a.created_at DESC LIMIT 5", nativeQuery = true) + List
findBusArticlesTop5OrderByCreatedAtDesc(); } diff --git a/src/main/java/in/koreatech/koin/domain/community/article/repository/redis/BusArticleRepository.java b/src/main/java/in/koreatech/koin/domain/community/article/repository/redis/BusArticleRepository.java new file mode 100644 index 000000000..8396ec6b1 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/community/article/repository/redis/BusArticleRepository.java @@ -0,0 +1,31 @@ +package in.koreatech.koin.domain.community.article.repository.redis; + +import in.koreatech.koin.domain.community.article.model.redis.BusNoticeArticle; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import java.util.Map; + +@Repository +@RequiredArgsConstructor +public class BusArticleRepository { + + public static final String BUS_NOTICE_KEY = "busNoticeArticle"; + private final RedisTemplate redisTemplate; + + public void save(BusNoticeArticle article) { + Map existingArticle = redisTemplate.opsForHash().entries(BUS_NOTICE_KEY); + + if (!existingArticle.isEmpty()) { + Object existingId = existingArticle.get("id"); + if (existingId.equals(article.getId())) { + return; + } + } + + redisTemplate.delete(BUS_NOTICE_KEY); + redisTemplate.opsForHash().put(BUS_NOTICE_KEY, "id", article.getId()); + redisTemplate.opsForHash().put(BUS_NOTICE_KEY, "title", article.getTitle()); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/community/article/scheduler/ArticleScheduler.java b/src/main/java/in/koreatech/koin/domain/community/article/scheduler/ArticleScheduler.java index 579172cd3..1c481a234 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/scheduler/ArticleScheduler.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/scheduler/ArticleScheduler.java @@ -31,4 +31,13 @@ public void resetOldKeywordsAndIpMaps() { log.error("많이 검색한 키워드 초기화 중에 오류가 발생했습니다.", e); } } + + @Scheduled(cron = "0 0 * * * *") + public void getBusNoticeArticle() { + try { + articleService.updateBusNoticeArticle(); + } catch (Exception e) { + log.error("버스 공지 게시글 조회 중에 오류가 발생했습니다.", e); + } + } } diff --git a/src/main/java/in/koreatech/koin/domain/community/article/service/ArticleService.java b/src/main/java/in/koreatech/koin/domain/community/article/service/ArticleService.java index a98721dfb..198769340 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/service/ArticleService.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/service/ArticleService.java @@ -1,22 +1,5 @@ package in.koreatech.koin.domain.community.article.service; -import java.time.Clock; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Collectors; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - import in.koreatech.koin.domain.community.article.dto.ArticleHotKeywordResponse; import in.koreatech.koin.domain.community.article.dto.ArticleResponse; import in.koreatech.koin.domain.community.article.dto.ArticlesResponse; @@ -28,17 +11,31 @@ import in.koreatech.koin.domain.community.article.model.Board; import in.koreatech.koin.domain.community.article.model.redis.ArticleHit; import in.koreatech.koin.domain.community.article.model.redis.ArticleHitUser; +import in.koreatech.koin.domain.community.article.model.redis.BusNoticeArticle; import in.koreatech.koin.domain.community.article.repository.ArticleRepository; import in.koreatech.koin.domain.community.article.repository.ArticleSearchKeywordIpMapRepository; import in.koreatech.koin.domain.community.article.repository.ArticleSearchKeywordRepository; import in.koreatech.koin.domain.community.article.repository.BoardRepository; import in.koreatech.koin.domain.community.article.repository.redis.ArticleHitRepository; import in.koreatech.koin.domain.community.article.repository.redis.ArticleHitUserRepository; +import in.koreatech.koin.domain.community.article.repository.redis.BusArticleRepository; import in.koreatech.koin.domain.community.article.repository.redis.HotArticleRepository; import in.koreatech.koin.global.concurrent.ConcurrencyGuard; import in.koreatech.koin.global.exception.KoinIllegalArgumentException; import in.koreatech.koin.global.model.Criteria; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Clock; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -64,6 +61,7 @@ public class ArticleService { private final HotArticleRepository hotArticleRepository; private final ArticleHitUserRepository articleHitUserRepository; private final Clock clock; + private final BusArticleRepository busArticleRepository; @Transactional public ArticleResponse getArticle(Integer boardId, Integer articleId, String publicIp) { @@ -259,4 +257,32 @@ public void updateHotArticles() { } hotArticleRepository.saveArticlesWithHitToRedis(articlesIdWithHit, HOT_ARTICLE_LIMIT); } + + @Transactional + public void updateBusNoticeArticle() { + List
articles = articleRepository.findBusArticlesTop5OrderByCreatedAtDesc(); + LocalDate latestDate = articles.get(0).getCreatedAt().toLocalDate(); + List
latestArticles = articles.stream() + .filter(article -> article.getCreatedAt().toLocalDate().isEqual(latestDate)) + .toList(); + + if (latestArticles.size() >= 2) { + latestArticles = latestArticles.stream() + .sorted((first, second) -> { + int firstWeight = 0; + int secondWeight = 0; + + // 제목(title)에 "사과"가 들어가면 후순위, "긴급"이 포함되면 우선순위 + if (first.getTitle().contains("사과")) firstWeight++; + if (first.getTitle().contains("긴급")) firstWeight--; + + if (second.getTitle().contains("사과")) secondWeight++; + if (second.getTitle().contains("긴급")) secondWeight--; + + return Integer.compare(firstWeight, secondWeight); + }) + .toList(); + } + busArticleRepository.save(BusNoticeArticle.from(latestArticles.get(0))); + } } diff --git a/src/main/java/in/koreatech/koin/global/config/RedisConfig.java b/src/main/java/in/koreatech/koin/global/config/RedisConfig.java index 8f58e0565..74bb68cba 100644 --- a/src/main/java/in/koreatech/koin/global/config/RedisConfig.java +++ b/src/main/java/in/koreatech/koin/global/config/RedisConfig.java @@ -34,6 +34,8 @@ public RedisTemplate redisTemplate(RedisConnectionFactory connec template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(serializer); template.setConnectionFactory(connectionFactory); + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(serializer); return template; }