Skip to content

Commit

Permalink
feat : 버스 긴급 공지 API 구현 (#1136)
Browse files Browse the repository at this point in the history
* feat : 버스 긴급공지 API 구현

* chore : gradle 잘못 포함된 부분 제거

* chore : 머지 충돌 해결

* refactor : 리뷰 반영
  • Loading branch information
Choon0414 authored Dec 15, 2024
1 parent b4a0e4c commit 23c5f96
Show file tree
Hide file tree
Showing 11 changed files with 216 additions and 63 deletions.
11 changes: 11 additions & 0 deletions src/main/java/in/koreatech/koin/domain/bus/controller/BusApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -139,4 +140,14 @@ ResponseEntity<BusScheduleResponse> 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<BusNoticeResponse> getNotice();
}
Original file line number Diff line number Diff line change
@@ -1,34 +1,22 @@
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;
import in.koreatech.koin.domain.bus.model.enums.BusType;
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
Expand Down Expand Up @@ -113,4 +101,10 @@ public ResponseEntity<BusScheduleResponse> getBusRouteSchedule(
BusScheduleResponse busSchedule = busService.getBusSchedule(request);
return ResponseEntity.ok().body(busSchedule);
}

@GetMapping("/notice")
public ResponseEntity<BusNoticeResponse> getNotice() {
BusNoticeResponse busNoticeResponse = busService.getNotice();
return ResponseEntity.ok().body(busNoticeResponse);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Object> redisTemplate;

public Map<Object, Object> getBusNotice() {
Map<Object, Object> article = redisTemplate.opsForHash().entries(BUS_NOTICE_KEY);

if (article.isEmpty()) {
return null;
}

return article;
}
}
49 changes: 24 additions & 25 deletions src/main/java/in/koreatech/koin/domain/bus/service/BusService.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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)
Expand All @@ -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;
Expand Down Expand Up @@ -255,4 +241,17 @@ public BusScheduleResponse getBusSchedule(BusRouteCommand request) {
scheduleInfoList
);
}

public BusNoticeResponse getNotice() {
Map<Object, Object> article = busNoticeRepository.getBusNotice();

if (article == null || article.isEmpty()) {
return null;
}

return BusNoticeResponse.of(
(Integer) article.get("id"),
(String) article.get("title")
);
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Article> findBusArticlesTop5OrderByCreatedAtDesc();
}
Original file line number Diff line number Diff line change
@@ -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<String, Object> redisTemplate;

public void save(BusNoticeArticle article) {
Map<Object, Object> 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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -259,4 +257,32 @@ public void updateHotArticles() {
}
hotArticleRepository.saveArticlesWithHitToRedis(articlesIdWithHit, HOT_ARTICLE_LIMIT);
}

@Transactional
public void updateBusNoticeArticle() {
List<Article> articles = articleRepository.findBusArticlesTop5OrderByCreatedAtDesc();
LocalDate latestDate = articles.get(0).getCreatedAt().toLocalDate();
List<Article> 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)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connec
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setConnectionFactory(connectionFactory);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
return template;
}

Expand Down

0 comments on commit 23c5f96

Please sign in to comment.