feat(badge): W4 배지 표시면 배선 — 게임카드 creator 칩 + 프로필 myBadges 주입
GameReviewController authorBadges 정본 패턴 답습. UserBadgesQueryMapper 배치조회(listActiveBadgeKeysByUserIds) 재사용 — 신규 SQL 0. - WebMvcController: UserBadgesQueryMapper 주입. profile→myBadges, indexModelAndView→creatorBadges(userId 그룹핑) 모델 주입 - SearchController: /games/search→creatorBadges 주입 - index.jsp: 게임카드 creator 배지 칩 렌더(badgeLabelIndex, HtmlUtils.htmlEscape) - 테스트 5건 신규(그룹핑/빈맵/myBadges). L1 352/352 GREEN, 회귀 0 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3FeMrbtxfTScjrwUukyHD
This commit is contained in:
parent
af298ce8a0
commit
3d10449e67
|
|
@ -3,8 +3,10 @@ package com.pandoli365.bibimbap.controller;
|
||||||
import com.pandoli365.bibimbap.data.GameData;
|
import com.pandoli365.bibimbap.data.GameData;
|
||||||
import com.pandoli365.bibimbap.data.SearchCriteria;
|
import com.pandoli365.bibimbap.data.SearchCriteria;
|
||||||
import com.pandoli365.bibimbap.data.TagData;
|
import com.pandoli365.bibimbap.data.TagData;
|
||||||
|
import com.pandoli365.bibimbap.data.UserBadgeKeyRow;
|
||||||
import com.pandoli365.bibimbap.mapper.GamesMapper;
|
import com.pandoli365.bibimbap.mapper.GamesMapper;
|
||||||
import com.pandoli365.bibimbap.mapper.TagsMapper;
|
import com.pandoli365.bibimbap.mapper.TagsMapper;
|
||||||
|
import com.pandoli365.bibimbap.mapper.UserBadgesQueryMapper;
|
||||||
import com.pandoli365.bibimbap.util.TagSanitizer;
|
import com.pandoli365.bibimbap.util.TagSanitizer;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
|
|
@ -12,8 +14,10 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
|
|
@ -24,10 +28,13 @@ public class SearchController {
|
||||||
|
|
||||||
private final GamesMapper gamesMapper;
|
private final GamesMapper gamesMapper;
|
||||||
private final TagsMapper tagsMapper;
|
private final TagsMapper tagsMapper;
|
||||||
|
private final UserBadgesQueryMapper userBadgesQueryMapper;
|
||||||
|
|
||||||
public SearchController(GamesMapper gamesMapper, TagsMapper tagsMapper) {
|
public SearchController(GamesMapper gamesMapper, TagsMapper tagsMapper,
|
||||||
|
UserBadgesQueryMapper userBadgesQueryMapper) {
|
||||||
this.gamesMapper = gamesMapper;
|
this.gamesMapper = gamesMapper;
|
||||||
this.tagsMapper = tagsMapper;
|
this.tagsMapper = tagsMapper;
|
||||||
|
this.userBadgesQueryMapper = userBadgesQueryMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/games/search")
|
@GetMapping("/games/search")
|
||||||
|
|
@ -63,6 +70,18 @@ public class SearchController {
|
||||||
List<TagData> availableTags = tagsMapper.listActiveByType("GAME", null);
|
List<TagData> availableTags = tagsMapper.listActiveByType("GAME", null);
|
||||||
|
|
||||||
model.addAttribute("games", games);
|
model.addAttribute("games", games);
|
||||||
|
List<Long> creatorUserIds = games.stream()
|
||||||
|
.map(GameData::getUserId)
|
||||||
|
.filter(java.util.Objects::nonNull)
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
Map<Long, List<String>> creatorBadges = new LinkedHashMap<>();
|
||||||
|
if (!creatorUserIds.isEmpty()) {
|
||||||
|
for (UserBadgeKeyRow row : userBadgesQueryMapper.listActiveBadgeKeysByUserIds(creatorUserIds)) {
|
||||||
|
creatorBadges.computeIfAbsent(row.getUserId(), k -> new ArrayList<>()).add(row.getBadgeKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
model.addAttribute("creatorBadges", creatorBadges);
|
||||||
model.addAttribute("searching", true);
|
model.addAttribute("searching", true);
|
||||||
model.addAttribute("searchKeyword", normalizedKeyword == null ? "" : normalizedKeyword);
|
model.addAttribute("searchKeyword", normalizedKeyword == null ? "" : normalizedKeyword);
|
||||||
model.addAttribute("searchCreator", normalizedCreator == null ? "" : normalizedCreator);
|
model.addAttribute("searchCreator", normalizedCreator == null ? "" : normalizedCreator);
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@ package com.pandoli365.bibimbap.controller;
|
||||||
|
|
||||||
import com.pandoli365.bibimbap.data.GameData;
|
import com.pandoli365.bibimbap.data.GameData;
|
||||||
import com.pandoli365.bibimbap.data.JamData;
|
import com.pandoli365.bibimbap.data.JamData;
|
||||||
|
import com.pandoli365.bibimbap.data.UserBadgeKeyRow;
|
||||||
import com.pandoli365.bibimbap.mapper.GamesMapper;
|
import com.pandoli365.bibimbap.mapper.GamesMapper;
|
||||||
import com.pandoli365.bibimbap.mapper.JamsMapper;
|
import com.pandoli365.bibimbap.mapper.JamsMapper;
|
||||||
|
import com.pandoli365.bibimbap.mapper.UserBadgesQueryMapper;
|
||||||
import jakarta.servlet.RequestDispatcher;
|
import jakarta.servlet.RequestDispatcher;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
|
@ -21,7 +23,9 @@ import java.time.OffsetDateTime;
|
||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
//이곳에서는 뷰만 제어
|
//이곳에서는 뷰만 제어
|
||||||
@Controller
|
@Controller
|
||||||
|
|
@ -31,10 +35,13 @@ public class WebMvcController implements WebMvcConfigurer, ErrorController {
|
||||||
|
|
||||||
private final GamesMapper gamesMapper;
|
private final GamesMapper gamesMapper;
|
||||||
private final JamsMapper jamsMapper;
|
private final JamsMapper jamsMapper;
|
||||||
|
private final UserBadgesQueryMapper userBadgesQueryMapper;
|
||||||
|
|
||||||
public WebMvcController(GamesMapper gamesMapper, JamsMapper jamsMapper) {
|
public WebMvcController(GamesMapper gamesMapper, JamsMapper jamsMapper,
|
||||||
|
UserBadgesQueryMapper userBadgesQueryMapper) {
|
||||||
this.gamesMapper = gamesMapper;
|
this.gamesMapper = gamesMapper;
|
||||||
this.jamsMapper = jamsMapper;
|
this.jamsMapper = jamsMapper;
|
||||||
|
this.userBadgesQueryMapper = userBadgesQueryMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping("/error")
|
@RequestMapping("/error")
|
||||||
|
|
@ -95,6 +102,10 @@ public class WebMvcController implements WebMvcConfigurer, ErrorController {
|
||||||
return new ModelAndView("redirect:/login");
|
return new ModelAndView("redirect:/login");
|
||||||
}
|
}
|
||||||
mv.addObject("myGames", gamesMapper.getGamesByUserId(sessionUserId(session)));
|
mv.addObject("myGames", gamesMapper.getGamesByUserId(sessionUserId(session)));
|
||||||
|
List<String> myBadges = userBadgesQueryMapper
|
||||||
|
.listActiveBadgeKeysByUserIds(java.util.List.of(sessionUserId(session)))
|
||||||
|
.stream().map(UserBadgeKeyRow::getBadgeKey).toList();
|
||||||
|
mv.addObject("myBadges", myBadges);
|
||||||
mv.setViewName("profile");
|
mv.setViewName("profile");
|
||||||
break;
|
break;
|
||||||
case "signup":
|
case "signup":
|
||||||
|
|
@ -133,6 +144,18 @@ public class WebMvcController implements WebMvcConfigurer, ErrorController {
|
||||||
|
|
||||||
ModelAndView mv = new ModelAndView("index");
|
ModelAndView mv = new ModelAndView("index");
|
||||||
mv.addObject("games", rows);
|
mv.addObject("games", rows);
|
||||||
|
List<Long> creatorUserIds = rows.stream()
|
||||||
|
.map(GameData::getUserId)
|
||||||
|
.filter(java.util.Objects::nonNull)
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
Map<Long, List<String>> creatorBadges = new LinkedHashMap<>();
|
||||||
|
if (!creatorUserIds.isEmpty()) {
|
||||||
|
for (UserBadgeKeyRow row : userBadgesQueryMapper.listActiveBadgeKeysByUserIds(creatorUserIds)) {
|
||||||
|
creatorBadges.computeIfAbsent(row.getUserId(), k -> new ArrayList<>()).add(row.getBadgeKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mv.addObject("creatorBadges", creatorBadges);
|
||||||
mv.addObject("searchQuery", normalizedQuery);
|
mv.addObject("searchQuery", normalizedQuery);
|
||||||
mv.addObject("nextCursor", nextCursor); // nullable — JSP 가 null 시 더보기 미표시
|
mv.addObject("nextCursor", nextCursor); // nullable — JSP 가 null 시 더보기 미표시
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,15 @@
|
||||||
<%@ page import="java.util.HashSet" %>
|
<%@ page import="java.util.HashSet" %>
|
||||||
<%@ page import="java.util.List" %>
|
<%@ page import="java.util.List" %>
|
||||||
<%@ page import="java.util.Set" %>
|
<%@ page import="java.util.Set" %>
|
||||||
|
<%@ page import="java.util.Map" %>
|
||||||
|
<%@ page import="java.util.ArrayList" %>
|
||||||
|
<%!
|
||||||
|
private String badgeLabelIndex(String key) {
|
||||||
|
if ("REVIEWER".equals(key)) return "리뷰어";
|
||||||
|
if ("TECHNICIAN".equals(key)) return "테크니션";
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
%>
|
||||||
<%
|
<%
|
||||||
String ctx = request.getContextPath();
|
String ctx = request.getContextPath();
|
||||||
jakarta.servlet.http.HttpSession homeSession = request.getSession(false);
|
jakarta.servlet.http.HttpSession homeSession = request.getSession(false);
|
||||||
|
|
@ -85,6 +94,21 @@
|
||||||
activeJamCount = (Integer) activeJamCountAttr;
|
activeJamCount = (Integer) activeJamCountAttr;
|
||||||
}
|
}
|
||||||
boolean showJamBanner = activeJamCount > 0 && activeJam != null && activeJam.getSlug() != null && !activeJam.getSlug().isBlank();
|
boolean showJamBanner = activeJamCount > 0 && activeJam != null && activeJam.getSlug() != null && !activeJam.getSlug().isBlank();
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<Long, List<String>> creatorBadges = new java.util.LinkedHashMap<>();
|
||||||
|
Object creatorBadgesAttr = request.getAttribute("creatorBadges");
|
||||||
|
if (creatorBadgesAttr instanceof Map<?, ?>) {
|
||||||
|
for (Map.Entry<?, ?> entry : ((Map<?, ?>) creatorBadgesAttr).entrySet()) {
|
||||||
|
if (entry.getKey() instanceof Long && entry.getValue() instanceof List<?>) {
|
||||||
|
List<String> badgeList = new ArrayList<>();
|
||||||
|
for (Object b : (List<?>) entry.getValue()) {
|
||||||
|
if (b instanceof String) badgeList.add((String) b);
|
||||||
|
}
|
||||||
|
creatorBadges.put((Long) entry.getKey(), badgeList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
%>
|
%>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ko">
|
<html lang="ko">
|
||||||
|
|
@ -590,6 +614,22 @@
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
.card__badges {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.2rem;
|
||||||
|
margin: 0.1rem 0 0;
|
||||||
|
}
|
||||||
|
.card__badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.1rem 0.35rem;
|
||||||
|
font-size: 0.625rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent);
|
||||||
|
border: 1px solid rgba(232, 165, 75, 0.4);
|
||||||
|
border-radius: 4px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
.home-empty {
|
.home-empty {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
min-height: 11rem;
|
min-height: 11rem;
|
||||||
|
|
@ -823,6 +863,18 @@
|
||||||
<div class="card__body">
|
<div class="card__body">
|
||||||
<h2 class="card__game-name" id="<%= titleId %>"><%= gameName %></h2>
|
<h2 class="card__game-name" id="<%= titleId %>"><%= gameName %></h2>
|
||||||
<p class="card__creator"><%= creator %></p>
|
<p class="card__creator"><%= creator %></p>
|
||||||
|
<%
|
||||||
|
List<String> cardBadges = game.getUserId() != null
|
||||||
|
? creatorBadges.getOrDefault(game.getUserId(), java.util.Collections.emptyList())
|
||||||
|
: java.util.Collections.emptyList();
|
||||||
|
if (!cardBadges.isEmpty()) {
|
||||||
|
%>
|
||||||
|
<div class="card__badges">
|
||||||
|
<% for (String bk : cardBadges) { %>
|
||||||
|
<span class="card__badge"><%= HtmlUtils.htmlEscape(badgeLabelIndex(bk)) %></span>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
<p class="card__likes">좋아요 <%= String.format("%,d", likeCount) %></p>
|
<p class="card__likes">좋아요 <%= String.format("%,d", likeCount) %></p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@ package com.pandoli365.bibimbap.controller;
|
||||||
import com.pandoli365.bibimbap.data.GameData;
|
import com.pandoli365.bibimbap.data.GameData;
|
||||||
import com.pandoli365.bibimbap.data.SearchCriteria;
|
import com.pandoli365.bibimbap.data.SearchCriteria;
|
||||||
import com.pandoli365.bibimbap.data.TagData;
|
import com.pandoli365.bibimbap.data.TagData;
|
||||||
|
import com.pandoli365.bibimbap.data.UserBadgeKeyRow;
|
||||||
import com.pandoli365.bibimbap.mapper.GamesMapper;
|
import com.pandoli365.bibimbap.mapper.GamesMapper;
|
||||||
import com.pandoli365.bibimbap.mapper.TagsMapper;
|
import com.pandoli365.bibimbap.mapper.TagsMapper;
|
||||||
|
import com.pandoli365.bibimbap.mapper.UserBadgesQueryMapper;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
|
|
@ -14,6 +16,7 @@ import org.springframework.ui.ExtendedModelMap;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
|
@ -29,6 +32,9 @@ class SearchControllerTest {
|
||||||
@Mock
|
@Mock
|
||||||
private TagsMapper tagsMapper;
|
private TagsMapper tagsMapper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private UserBadgesQueryMapper userBadgesQueryMapper;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void searchReturnsIndexViewWithDefaults() {
|
void searchReturnsIndexViewWithDefaults() {
|
||||||
SearchController controller = controller();
|
SearchController controller = controller();
|
||||||
|
|
@ -150,10 +156,45 @@ class SearchControllerTest {
|
||||||
assertThat(model.getAttribute("availableTags")).isEqualTo(tags);
|
assertThat(model.getAttribute("availableTags")).isEqualTo(tags);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void search_creatorBadges_groupedByUserId() {
|
||||||
|
GameData g1 = gameWithUser(1L, 10L);
|
||||||
|
GameData g2 = gameWithUser(2L, 20L);
|
||||||
|
when(gamesMapper.searchGamesAdvanced(any(SearchCriteria.class))).thenReturn(List.of(g1, g2));
|
||||||
|
when(tagsMapper.listActiveByType("GAME", null)).thenReturn(List.<TagData>of());
|
||||||
|
|
||||||
|
UserBadgeKeyRow row1 = badgeRow(10L, "REVIEWER");
|
||||||
|
UserBadgeKeyRow row2 = badgeRow(20L, "TECHNICIAN");
|
||||||
|
when(userBadgesQueryMapper.listActiveBadgeKeysByUserIds(List.of(10L, 20L)))
|
||||||
|
.thenReturn(List.of(row1, row2));
|
||||||
|
|
||||||
|
Model model = new ExtendedModelMap();
|
||||||
|
controller().search(null, null, null, "and", "latest", null, model);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<Long, List<String>> creatorBadges = (Map<Long, List<String>>) model.getAttribute("creatorBadges");
|
||||||
|
assertThat(creatorBadges).isNotNull();
|
||||||
|
assertThat(creatorBadges.get(10L)).containsExactly("REVIEWER");
|
||||||
|
assertThat(creatorBadges.get(20L)).containsExactly("TECHNICIAN");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void search_creatorBadges_emptyMapWhenNoGames() {
|
||||||
|
when(gamesMapper.searchGamesAdvanced(any(SearchCriteria.class))).thenReturn(List.<GameData>of());
|
||||||
|
when(tagsMapper.listActiveByType("GAME", null)).thenReturn(List.<TagData>of());
|
||||||
|
|
||||||
|
Model model = new ExtendedModelMap();
|
||||||
|
controller().search(null, null, null, "and", "latest", null, model);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<Long, List<String>> creatorBadges = (Map<Long, List<String>>) model.getAttribute("creatorBadges");
|
||||||
|
assertThat(creatorBadges).isNotNull().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
// ---- helpers ----
|
// ---- helpers ----
|
||||||
|
|
||||||
private SearchController controller() {
|
private SearchController controller() {
|
||||||
return new SearchController(gamesMapper, tagsMapper);
|
return new SearchController(gamesMapper, tagsMapper, userBadgesQueryMapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void stubEmptyResults() {
|
private void stubEmptyResults() {
|
||||||
|
|
@ -174,4 +215,19 @@ class SearchControllerTest {
|
||||||
t.setSlug(slug);
|
t.setSlug(slug);
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private GameData gameWithUser(long id, Long userId) {
|
||||||
|
GameData g = new GameData();
|
||||||
|
g.setId(id);
|
||||||
|
g.setName("game-" + id);
|
||||||
|
g.setUserId(userId);
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserBadgeKeyRow badgeRow(Long userId, String key) {
|
||||||
|
UserBadgeKeyRow r = new UserBadgeKeyRow();
|
||||||
|
r.setUserId(userId);
|
||||||
|
r.setBadgeKey(key);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,11 @@ package com.pandoli365.bibimbap.controller;
|
||||||
|
|
||||||
import com.pandoli365.bibimbap.data.GameData;
|
import com.pandoli365.bibimbap.data.GameData;
|
||||||
import com.pandoli365.bibimbap.data.JamData;
|
import com.pandoli365.bibimbap.data.JamData;
|
||||||
|
import com.pandoli365.bibimbap.data.UserBadgeKeyRow;
|
||||||
import com.pandoli365.bibimbap.mapper.GamesMapper;
|
import com.pandoli365.bibimbap.mapper.GamesMapper;
|
||||||
import com.pandoli365.bibimbap.mapper.JamsMapper;
|
import com.pandoli365.bibimbap.mapper.JamsMapper;
|
||||||
|
import com.pandoli365.bibimbap.mapper.UserBadgesQueryMapper;
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
|
|
@ -16,12 +19,14 @@ import java.time.OffsetDateTime;
|
||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.ArgumentMatchers.isNull;
|
import static org.mockito.ArgumentMatchers.isNull;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
@ -38,6 +43,9 @@ class WebMvcControllerTest {
|
||||||
@Mock
|
@Mock
|
||||||
private JamsMapper jamsMapper;
|
private JamsMapper jamsMapper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private UserBadgesQueryMapper userBadgesQueryMapper;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void firstPage_returnsPageSizeAndNextCursor() {
|
void firstPage_returnsPageSizeAndNextCursor() {
|
||||||
when(jamsMapper.countActive()).thenReturn(0);
|
when(jamsMapper.countActive()).thenReturn(0);
|
||||||
|
|
@ -171,10 +179,69 @@ class WebMvcControllerTest {
|
||||||
verify(jamsMapper, never()).listActive(anyInt());
|
verify(jamsMapper, never()).listActive(anyInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void indexView_creatorBadges_groupedByUserId() {
|
||||||
|
when(jamsMapper.countActive()).thenReturn(0);
|
||||||
|
// games: userId=10L, userId=20L, userId=10L (10L 중복)
|
||||||
|
List<GameData> rows = List.of(
|
||||||
|
gameWithUser(1L, 10L),
|
||||||
|
gameWithUser(2L, 20L),
|
||||||
|
gameWithUser(3L, 10L)
|
||||||
|
);
|
||||||
|
when(gamesMapper.listVisibleKeyset(isNull(), isNull(), isNull(), eq(FETCH_LIMIT)))
|
||||||
|
.thenReturn(rows);
|
||||||
|
|
||||||
|
UserBadgeKeyRow row1 = badgeRow(10L, "REVIEWER");
|
||||||
|
UserBadgeKeyRow row2 = badgeRow(10L, "TECHNICIAN");
|
||||||
|
UserBadgeKeyRow row3 = badgeRow(20L, "REVIEWER");
|
||||||
|
when(userBadgesQueryMapper.listActiveBadgeKeysByUserIds(List.of(10L, 20L)))
|
||||||
|
.thenReturn(List.of(row1, row2, row3));
|
||||||
|
|
||||||
|
ModelAndView mv = controller().indexView(null, null);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<Long, List<String>> creatorBadges = (Map<Long, List<String>>) mv.getModel().get("creatorBadges");
|
||||||
|
assertThat(creatorBadges).isNotNull();
|
||||||
|
assertThat(creatorBadges.get(10L)).containsExactly("REVIEWER", "TECHNICIAN");
|
||||||
|
assertThat(creatorBadges.get(20L)).containsExactly("REVIEWER");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void indexView_creatorBadges_emptyMapWhenNoGames() {
|
||||||
|
when(jamsMapper.countActive()).thenReturn(0);
|
||||||
|
when(gamesMapper.listVisibleKeyset(isNull(), isNull(), isNull(), eq(FETCH_LIMIT)))
|
||||||
|
.thenReturn(new ArrayList<>());
|
||||||
|
|
||||||
|
ModelAndView mv = controller().indexView(null, null);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<Long, List<String>> creatorBadges = (Map<Long, List<String>>) mv.getModel().get("creatorBadges");
|
||||||
|
assertThat(creatorBadges).isNotNull().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void profileView_myBadges_injectedIntoModel() {
|
||||||
|
HttpSession session = mock(HttpSession.class);
|
||||||
|
when(session.getAttribute("userId")).thenReturn(42L);
|
||||||
|
when(gamesMapper.getGamesByUserId(42L)).thenReturn(List.of());
|
||||||
|
|
||||||
|
UserBadgeKeyRow row = badgeRow(42L, "REVIEWER");
|
||||||
|
when(userBadgesQueryMapper.listActiveBadgeKeysByUserIds(List.of(42L)))
|
||||||
|
.thenReturn(List.of(row));
|
||||||
|
|
||||||
|
org.springframework.mock.web.MockHttpServletRequest request =
|
||||||
|
new org.springframework.mock.web.MockHttpServletRequest();
|
||||||
|
ModelAndView mv = controller().mainView("profile", null, null, session, request);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> myBadges = (List<String>) mv.getModel().get("myBadges");
|
||||||
|
assertThat(myBadges).containsExactly("REVIEWER");
|
||||||
|
}
|
||||||
|
|
||||||
// ---- helpers ----
|
// ---- helpers ----
|
||||||
|
|
||||||
private WebMvcController controller() {
|
private WebMvcController controller() {
|
||||||
return new WebMvcController(gamesMapper, jamsMapper);
|
return new WebMvcController(gamesMapper, jamsMapper, userBadgesQueryMapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<GameData> games(int count) {
|
private List<GameData> games(int count) {
|
||||||
|
|
@ -203,4 +270,22 @@ class WebMvcControllerTest {
|
||||||
j.setCreatedAt(OffsetDateTime.ofInstant(Instant.ofEpochMilli(1_700_000_000_000L), ZoneOffset.UTC));
|
j.setCreatedAt(OffsetDateTime.ofInstant(Instant.ofEpochMilli(1_700_000_000_000L), ZoneOffset.UTC));
|
||||||
return j;
|
return j;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private GameData gameWithUser(long id, Long userId) {
|
||||||
|
GameData g = new GameData();
|
||||||
|
g.setId(id);
|
||||||
|
g.setSortOrder((int) id);
|
||||||
|
g.setCreatedAt(OffsetDateTime.ofInstant(
|
||||||
|
Instant.ofEpochMilli(1_700_000_000_000L + id), ZoneOffset.UTC));
|
||||||
|
g.setName("game-" + id);
|
||||||
|
g.setUserId(userId);
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserBadgeKeyRow badgeRow(Long userId, String key) {
|
||||||
|
UserBadgeKeyRow r = new UserBadgeKeyRow();
|
||||||
|
r.setUserId(userId);
|
||||||
|
r.setBadgeKey(key);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue