Skip to content

Commit cd4d704

Browse files
authored
Merge pull request #104 from JECT-Study/feat/admin-search-keyword-stats
feat: 관리자 검색 키워드 통계 화면 추가
2 parents c73c835 + f95617c commit cd4d704

17 files changed

Lines changed: 346 additions & 0 deletions

scripts/db/03-keyword-counts.sql

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
CREATE TABLE IF NOT EXISTS keyword_counts (
2+
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
3+
keyword VARCHAR(100) NOT NULL,
4+
search_count BIGINT NOT NULL DEFAULT 0,
5+
CONSTRAINT uk_keyword_counts_keyword UNIQUE (keyword)
6+
);
7+
8+
CREATE INDEX IF NOT EXISTS idx_keyword_counts_keyword
9+
ON keyword_counts (keyword);
10+
11+
CREATE TABLE IF NOT EXISTS keyword_daily_counts (
12+
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
13+
keyword VARCHAR(100) NOT NULL,
14+
stat_date DATE NOT NULL,
15+
search_count BIGINT NOT NULL DEFAULT 0,
16+
CONSTRAINT uk_keyword_daily_counts_keyword_date UNIQUE (keyword, stat_date)
17+
);
18+
19+
CREATE INDEX IF NOT EXISTS idx_keyword_daily_counts_stat_date
20+
ON keyword_daily_counts (stat_date);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.zimdugo.admin.application;
2+
3+
import com.zimdugo.admin.application.dto.AdminSearchKeywordStatisticsResult;
4+
import java.time.LocalDate;
5+
import java.time.ZoneId;
6+
import java.util.ArrayList;
7+
import java.util.List;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.stereotype.Service;
10+
import org.springframework.transaction.annotation.Transactional;
11+
import com.zimdugo.locker.domain.search.SearchKeywordStatistic;
12+
import com.zimdugo.locker.domain.search.SearchKeywordStatisticsReader;
13+
14+
@Service
15+
@Transactional(readOnly = true)
16+
@RequiredArgsConstructor
17+
public class AdminSearchKeywordStatisticsService {
18+
19+
private static final ZoneId SEOUL_ZONE = ZoneId.of("Asia/Seoul");
20+
21+
private final SearchKeywordStatisticsReader searchKeywordStatisticsReader;
22+
23+
public AdminSearchKeywordStatisticsResult getStatistics() {
24+
LocalDate today = LocalDate.now(SEOUL_ZONE);
25+
List<SearchKeywordStatistic> rows = searchKeywordStatisticsReader.readStatistics(today);
26+
List<AdminSearchKeywordStatisticsResult.Item> items = new ArrayList<>(rows.size());
27+
for (int index = 0; index < rows.size(); index++) {
28+
SearchKeywordStatistic row = rows.get(index);
29+
items.add(new AdminSearchKeywordStatisticsResult.Item(
30+
index + 1,
31+
row.keyword(),
32+
row.totalCount(),
33+
row.todayCount()
34+
));
35+
}
36+
return new AdminSearchKeywordStatisticsResult(today, items);
37+
}
38+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.zimdugo.admin.application.dto;
2+
3+
import java.time.LocalDate;
4+
import java.util.List;
5+
6+
public record AdminSearchKeywordStatisticsResult(
7+
LocalDate today,
8+
List<Item> items
9+
) {
10+
11+
public record Item(
12+
int rank,
13+
String keyword,
14+
long totalCount,
15+
long todayCount
16+
) {
17+
}
18+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.zimdugo.admin.entrypoint;
2+
3+
import com.zimdugo.admin.application.AdminSearchKeywordStatisticsService;
4+
import com.zimdugo.admin.application.dto.AdminSearchKeywordStatisticsResult;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.stereotype.Controller;
7+
import org.springframework.ui.Model;
8+
import org.springframework.web.bind.annotation.GetMapping;
9+
import org.springframework.web.bind.annotation.RequestMapping;
10+
11+
@Controller
12+
@RequestMapping("/admin/statistics/search-keywords")
13+
@RequiredArgsConstructor
14+
public class AdminSearchKeywordStatisticsController {
15+
16+
private final AdminSearchKeywordStatisticsService adminSearchKeywordStatisticsService;
17+
18+
@GetMapping
19+
public String list(Model model) {
20+
AdminSearchKeywordStatisticsResult result = adminSearchKeywordStatisticsService.getStatistics();
21+
model.addAttribute("statistics", result);
22+
model.addAttribute("activeMenu", "search-keywords");
23+
return "admin/search-keyword-statistics";
24+
}
25+
}

src/main/java/com/zimdugo/locker/application/search/SearchKeywordCountCommandService.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package com.zimdugo.locker.application.search;
22

3+
import com.zimdugo.locker.domain.search.SearchKeywordDailyCountStore;
34
import com.zimdugo.locker.domain.search.SearchKeywordCountStore;
5+
import java.time.LocalDate;
6+
import java.time.ZoneId;
47
import lombok.RequiredArgsConstructor;
58
import org.springframework.stereotype.Service;
69
import org.springframework.transaction.annotation.Propagation;
@@ -10,7 +13,10 @@
1013
@RequiredArgsConstructor
1114
public class SearchKeywordCountCommandService {
1215

16+
private static final ZoneId SEOUL_ZONE = ZoneId.of("Asia/Seoul");
17+
1318
private final SearchKeywordCountStore searchKeywordCountStore;
19+
private final SearchKeywordDailyCountStore searchKeywordDailyCountStore;
1420

1521
@Transactional(propagation = Propagation.REQUIRES_NEW)
1622
public void increase(String keyword) {
@@ -19,6 +25,7 @@ public void increase(String keyword) {
1925
return;
2026
}
2127
searchKeywordCountStore.increase(normalizedKeyword);
28+
searchKeywordDailyCountStore.increase(normalizedKeyword, LocalDate.now(SEOUL_ZONE));
2229
}
2330

2431
private String normalize(String keyword) {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.zimdugo.locker.domain.search;
2+
3+
import java.time.LocalDate;
4+
5+
public interface SearchKeywordDailyCountStore {
6+
void increase(String keyword, LocalDate statDate);
7+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.zimdugo.locker.domain.search;
2+
3+
public record SearchKeywordStatistic(
4+
String keyword,
5+
long totalCount,
6+
long todayCount
7+
) {
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.zimdugo.locker.domain.search;
2+
3+
import java.time.LocalDate;
4+
import java.util.List;
5+
6+
public interface SearchKeywordStatisticsReader {
7+
List<SearchKeywordStatistic> readStatistics(LocalDate today);
8+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.zimdugo.locker.infrastructure.adapter;
2+
3+
import com.zimdugo.locker.domain.search.SearchKeywordStatistic;
4+
import com.zimdugo.locker.domain.search.SearchKeywordStatisticsReader;
5+
import com.zimdugo.locker.infrastructure.persistence.AdminSearchKeywordStatisticsProjection;
6+
import com.zimdugo.locker.infrastructure.persistence.SearchKeywordCountRepository;
7+
import java.time.LocalDate;
8+
import java.util.List;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.stereotype.Component;
11+
12+
@Component
13+
@RequiredArgsConstructor
14+
public class AdminSearchKeywordStatisticsReaderAdapter implements SearchKeywordStatisticsReader {
15+
16+
private final SearchKeywordCountRepository searchKeywordCountRepository;
17+
18+
@Override
19+
public List<SearchKeywordStatistic> readStatistics(LocalDate today) {
20+
return searchKeywordCountRepository.findStatistics(today).stream()
21+
.map(this::toStatistic)
22+
.toList();
23+
}
24+
25+
private SearchKeywordStatistic toStatistic(AdminSearchKeywordStatisticsProjection projection) {
26+
return new SearchKeywordStatistic(
27+
projection.getKeyword(),
28+
projection.getTotalCount(),
29+
projection.getTodayCount()
30+
);
31+
}
32+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.zimdugo.locker.infrastructure.adapter;
2+
3+
import com.zimdugo.locker.domain.search.SearchKeywordDailyCountStore;
4+
import com.zimdugo.locker.infrastructure.persistence.SearchKeywordDailyCountRepository;
5+
import java.time.LocalDate;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.stereotype.Component;
8+
9+
@Component
10+
@RequiredArgsConstructor
11+
public class SearchKeywordDailyCountStoreAdapter implements SearchKeywordDailyCountStore {
12+
13+
private final SearchKeywordDailyCountRepository searchKeywordDailyCountRepository;
14+
15+
@Override
16+
public void increase(String keyword, LocalDate statDate) {
17+
searchKeywordDailyCountRepository.increase(keyword, statDate);
18+
}
19+
}

0 commit comments

Comments
 (0)