개발은 아름다워

[ bible Cash ] 주간 읽은 양 랭킹 기능 추가 본문

프로젝트

[ bible Cash ] 주간 읽은 양 랭킹 기능 추가

do_it_zero 2025. 2. 5. 14:22

[ 요구 사항 ]

주일 ~ 토요일까지 가장 많이 읽은 순위를 보여주는 기능을 추가 해달라고 하였습니다.

 

[ 구현 ]

- 매주 읽은 양을 각자가 읽은 양이 업데이트 되는 weeklyRankingBoard entity를 만들었습니다.

- 스케쥴러로 weeklyRankingBoard의 읽은 양은 매주 주일 00:00분에 0이 되도록 했습니다.

- 기존 읽은 양 저장할 때 로직에 weeklyRankingBoard 에도 읽은 양이 저장되도록 했습니다.

- 조회 당시의 기준 날짜로 해당 주에 주일날짜와 토요일 날짜 안에서 데이터를 조회 되도록 하였습니다.

- 스케쥴러로 읽은 양이 초기화 되어 0이 된 경우, 화면에서 읽은 양이 0인 경우는 보이지 않도록 만들었습니다.

[ 코드 ]

- weeklyRankingBoard  entity 생성

@Entity
@NoArgsConstructor
@Getter
public class WeeklyRankingBoard {
    @Id
    @GeneratedValue
    private Long idx;

    @ManyToOne
    private Member member;

    @Column(nullable = false)
    private int readCount = 0;

    @Column(columnDefinition = "DATE")
    private LocalDate readTime;

    public static WeeklyRankingBoard create(Member member,int readCount,LocalDate readTime){
        WeeklyRankingBoard weeklyRankingBoard = new WeeklyRankingBoard();
        weeklyRankingBoard.member = member;
        weeklyRankingBoard.readCount = readCount;
        weeklyRankingBoard.readTime = readTime;
        return weeklyRankingBoard;
    }
}

 

 

- WeeklyRankingBoardRepository 생성

public interface WeeklyRankingBoardRepository extends JpaRepository<WeeklyRankingBoard,Long> {
    @Modifying
    @Transactional
    @Query("UPDATE WeeklyRankingBoard w SET w.readCount = w.readCount + :readCount, w.readTime = :readTime WHERE w.member.idx = :memberIdx")
    int updateReadCountAndTime(Long memberIdx, int readCount, LocalDate readTime);

    Optional<WeeklyRankingBoard> findByMemberIdx(Long idx);

    @Transactional
    @Modifying
    @Query("UPDATE WeeklyRankingBoard w SET w.readCount = 0")
    void resetWeeklyRankingBoard();

    @Query(value = """
    WITH Ranked AS (
        SELECT m.name, m.member_group, w.read_count,
               DENSE_RANK() OVER (ORDER BY w.read_count DESC) AS ranking
        FROM weekly_ranking_board w
        JOIN member m ON w.member_idx = m.idx
        WHERE w.read_time BETWEEN :startOfWeek AND :endOfWeek
    )
    SELECT name, member_group, ranking,read_count
    FROM Ranked
    WHERE ranking <= 5
    ORDER BY ranking
""", nativeQuery = true)
    List<Object[]> findTop5ByReadCountWithRanking(
            @Param("startOfWeek") LocalDate startOfWeek,
            @Param("endOfWeek") LocalDate endOfWeek
    );

}

 

 

- 스케쥴러 코드 추가

/**
 * 매주 주일 00시 마다 readCount 초기화
 * */
@Scheduled(cron = "0 0 0 * * SUN")// 매주 주일 00:00 실행
public void resetWeeklyRankingBoard(){
    weeklyRankingBoardRepository.resetWeeklyRankingBoard();
}

 

 

- HistoryService의 코드 추가

@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class HistoryService {
    private final HistoryRepository historyRepository;
    private final MemberRepository memberRepository;
    private final WeeklyRankingBoardRepository weeklyRankingBoardRepository;

    public final static int STANDARD_READ_COUNT = 5;

    public void saveHistory(Member member){
        // member readCount 업데이트 회원가입 할 때부터 기본적으로 생성되어있음
        memberRepository.incrementReadCount(member.getIdx(),STANDARD_READ_COUNT);


        // history에 기록 저장
        History history = History.create(member, STANDARD_READ_COUNT, LocalDate.now());
        historyRepository.save(history);


        // WeeklyRankingBoard 기록 저장
        Optional<WeeklyRankingBoard> memberOptional = weeklyRankingBoardRepository.findByMemberIdx(member.getIdx());
        if(memberOptional.isEmpty()){
            WeeklyRankingBoard weeklyRankingBoard = WeeklyRankingBoard.create(member, STANDARD_READ_COUNT, LocalDate.now());
            weeklyRankingBoardRepository.save(weeklyRankingBoard);
        } else {
            weeklyRankingBoardRepository.updateReadCountAndTime(member.getIdx(),STANDARD_READ_COUNT,LocalDate.now());
        }

    }

    public void saveAdd(Member member,int additionalVerse){
        // member readCount 업데이트
        memberRepository.incrementReadCount(member.getIdx(),additionalVerse);

        // history에 기록 저장
        History history = History.create(member, additionalVerse, LocalDate.now());
        historyRepository.save(history);

        // WeeklyRankingBoard 기록 저장
        Optional<WeeklyRankingBoard> memberOptional = weeklyRankingBoardRepository.findByMemberIdx(member.getIdx());
        if(memberOptional.isEmpty()){
            WeeklyRankingBoard weeklyRankingBoard = WeeklyRankingBoard.create(member, additionalVerse, LocalDate.now());
            weeklyRankingBoardRepository.save(weeklyRankingBoard);
        } else {
            weeklyRankingBoardRepository.updateReadCountAndTime(member.getIdx(),additionalVerse,LocalDate.now());

        }
    }


}

 

 

- HistoryController 수정

@Controller
@Slf4j
@RequiredArgsConstructor
public class HistoryController {

    private final HistoryService historyService;

    @PostMapping("/history")
    public String saveHistory(HttpSession session){
        Member member = (Member) session.getAttribute("member");
        log.info("member: {}",member.getUserId());
        historyService.saveHistory(member);
        return "redirect:/home";
    }

    @PostMapping("/add")
    public String addAdditionalVerse(@RequestParam("additionalVerse") int additionalVerse, HttpSession session) {
        log.info("추가 읽은 구절 수: {}", additionalVerse);
        Member member = (Member)session.getAttribute("member");
        log.info("member : {}",member.getName());
        historyService.saveAdd(member,additionalVerse);
        return "redirect:/home"; // 홈 화면으로 리디렉트
    }
}

 

- HomeController

@Slf4j
@Controller
@RequiredArgsConstructor
public class HomeController {

    private final HistoryRepository historyRepository;
    private final MemberRepository memberRepository;
    private final WeeklyRankingBoardRepository weeklyRankingBoardRepository;

    @GetMapping("/home")
    public String home(HttpSession session, Model model){
        // 오늘까지 내가 읽은 양
        Member member =(Member) session.getAttribute("member");
        int readCount = member.getReadCount();
        // model.addAttribute("MyReadCount",readCount);

        // 이번주 가장 많이 읽은 순위
        LocalDate now = LocalDate.now();
        LocalDate startOfWeek = now.with(TemporalAdjusters.previousOrSame(DayOfWeek.SUNDAY));
        LocalDate endOfWeek = now.with(TemporalAdjusters.nextOrSame(DayOfWeek.SATURDAY));
        log.info("startOfWeek : {} ,endOfWeek : {} ",startOfWeek,endOfWeek);
        List<Object[]> weeklyRanking = weeklyRankingBoardRepository.findTop5ByReadCountWithRanking(startOfWeek, endOfWeek);
        List<RankDto> weeklyRankinList = weeklyRanking.stream()
                .map(row -> new RankDto(
                        (String) row[0], // name
                        Group.valueOf((String) row[1]), // memberGroup
                        ((Number) row[2]).intValue(), // readCount
                        ((Number) row[3]).intValue() // ranking
                )).toList();
        log.info("weeklyRankinList : {}",weeklyRankinList);
        Map<Integer, List<RankDto>> weeklyRankinMap = weeklyRankinList.stream()
                .collect(Collectors.groupingBy(RankDto::getRanking));
        log.info("weeklyRankinMap : {}",weeklyRankinMap);
        model.addAttribute("weeklyRankinMap",weeklyRankinMap);


        // 현재까지 읽은 양이 제일 많은 순위 데이터 가져오기
        List<Object[]> results = memberRepository.findTop5ByReadCountWithRanking();
        List<RankDto> rankDtoList = results.stream()
                .map(row -> new RankDto(
                        (String) row[0],
                        Group.valueOf((String) row[1]),
                        ((Number) row[2]).intValue(),
                        ((Number) row[3]).intValue()
                ))
                .collect(Collectors.toList());

        Map<Integer, List<RankDto>> groupedRanking = rankDtoList.stream()
                .collect(Collectors.groupingBy(RankDto::getRanking, LinkedHashMap::new, Collectors.toList()));
        model.addAttribute("groupedRanking", groupedRanking);


        //
        List<Object[]> countSumByGroup = memberRepository.getReadCountSumByGroup();
        List<GroupReadCountDto> groupReadCountDtos = countSumByGroup.stream()
                .map(row -> new GroupReadCountDto(
                        Group.valueOf((String) row[0]), // Enum 변환
                        ((Number) row[1]).intValue()   // readCount 합계 변환
                ))
                .collect(Collectors.toList());
        model.addAttribute("groupReadCountDtos", groupReadCountDtos);
        return "home";
    }
}

 

- home.html 

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>오늘의 말씀</title>
    <link rel="stylesheet" th:href="@{/css/style.css}">
</head>
<body>
<div class="container">
    <div class="calendar-header">
        <div class="current-month" id="yearText">2025년</div>
        <select id="monthSelect" class="dropdown"></select>
        <button class="nav-button" id="todayButton">오늘</button>
    </div>

    <div id="daysOfWeek"></div>
    <div id="calendar"></div>

    <div class="footer">
        <!-- '오늘의 말씀' 버튼을 link로 감싸지 않고, 클릭 이벤트로 처리하도록 수정 -->
        <button class="verse-button" id="verseButton">오늘의 말씀</button>

        <h2>현재까지 가장 많이 읽은 순위</h2>
        <div th:each="entry : ${groupedRanking}">
            <span th:text="|${entry.key}등 |"></span>

            <!-- 순위 내 멤버들의 이름(그룹) 및 읽은 횟수 출력 -->
            <span th:each="member, iterStat : ${entry.value}">
        <span th:text="|${member.memberGroup} ${member.name} ${member.readCount}장|"></span>

                <!-- 마지막 멤버가 아니면 쉼표 추가 -->
        <span th:if="!${iterStat.last}">, </span>
    </span>
            <br/>
        </div>

        <h2>이번주 순위</h2>
        <div th:each="entry : ${weeklyRankinMap}">
            <span th:text="|${entry.key}등 |"></span>

            <span th:each="member, iterStat : ${entry.value}" th:if="${member.readCount > 0}">
        <span th:text="|${member.name} ${member.memberGroup} ${member.readCount}장|"></span>
        <span th:if="!${iterStat.last}">, </span>
    </span>
            <br/>
        </div>

        <h2>그룹별 읽은 장 수</h2>
        <div th:each="groupReadCount : ${groupReadCountDtos}">
            <span th:text="${groupReadCount.memberGroup} + ' 총 ' + ${groupReadCount.totalReadCount} + '장'"></span>
            <br/>
        </div>

        <h3>추가로 읽은 말씀</h3>
        <form id="additionalVerseForm" th:action="@{/add}" method="post">
            <div class="additional-verse">
                <input type="number" id="additionalVerseInput" name="additionalVerse" class="number-input" required>
                <button type="submit" class="verse-button" id="additionalVerseButton">추가</button>
            </div>
        </form>
    </div>

</div>

<!-- JavaScript -->
<script th:src="@{/js/calendar.js}"></script>
</body>
</html>