유저 권한 변경
기존에는 별다른 설정해주지 않아서, 유저의 권한을 가져와서 selected 해주지 않고, 하드코딩 되어 있었다.
<li class="list-group-item" th:each="user : ${users}">
<span th:text="${user.username}">username</span>
<select class="form-select w-auto d-inline-block" th:id="|roleType${user.id}|">
<option selected value="default">일반 유저</option>
<option value="admin">관리자</option>
</select>
<div class="btn-group">
<button class="btn btn-primary" th:value="${user.id}" onclick="setRole(this)">권한 변경</button>
<button class="btn btn-danger" th:value="${user.id}" onclick="deleteUser(this.value)">삭제</button>
</div>
</li>
유저 선택시, 유저의 권한을 가져오게 해주고 싶었는데, Role을 유저에게 저장하지 않고, 별도의 UserRoleRepository 인스턴스에 저장하고 있기 때문에, 의외로 번거로웠다.
RoleType을 돌려서 option 채우기
이번에는 option 태그를 하드코딩이 아닌, RoleType Enum Class를 돌려서 채우고자 한다. AdminPageController.class에서 @ModelAttribute로 추가해주고, 타임리프에서 반복문을 돌리기로 한다.
@ModelAttribute("roleTypes")
public RoleType[] roleTypes(){
return RoleType.values();
}
SetUserDto.class
그렇다고 기존 설계를 바꿔서 User에 Role을 저장하는 것은 도메인 설계 원칙에 위배되기도 하고, 기존 설계를 바꾸고 싶지 않았다. 그래서, 별도의 Dto에 Role의 Name을 저장해서 넘겨주기로 했다.
편의성을 위해, @Builder 롬복 애노테이션을 추가해서, 이번에는 빌더 패턴을 사용해보기로 한다.
@Getter
@Builder
@RequiredArgsConstructor
@ToString
public class SetUserDto {
private final Long id;
private final String username;
private final String roleTypeName;
}
그리고, users @ModelAttribute를 추가해서, 실제로는 user에 List<SetUserDto>를 담아서 보내려고 한다.
@ModelAttribute("users")
public List users() {
/* 모든 유저를 가지고 온다 */
List findUsers = adminService.findAllUsers();
/* dto를 담을 list */
List dtos = new ArrayList<>();
/* 찾은 유저 반복문 */
for (User user : findUsers) {
/* 찾은 유저의 가장 높은 RoleType 가져오기 */
RoleType roleType = adminService.findUserRoleType(user.getId());
SetUserDto userDto = SetUserDto.builder()
.id(user.getId())
.username(user.getUsername())
.roleTypeName(roleType.name())
.build();
dtos.add(userDto);
}
return dtos;
}
이때 주의할 점은, 권한을 통째로 보내지 않고 루프를 도는 유저가 가진 가장 높은 권한을 넣어서 SetUserDto 인스턴스를 만들어서 추가해야 한다는 것이다.
가장 높은 RoleType 확인하기
그러려면, 이 유저가 가진 가장 큰 권한을 확인해야 했다. 어떤 클래스의 인스턴스의 대소를 비교하려면, Comparable<T> 인스턴스를 구현하면 된다.
public class Role implements Comparable{
private final Long id;
private final Long userId;
private final RoleType roleType;
@Override
public int compareTo(Role other) {
return roleType.compareTo(other.roleType);
}
}
Enum 인스턴스를 무작정 compareTo 메서드로 비교하면, 내장된 ordinal 필드를 비교해준다. 현재는 RoleType에 ADMIN, DEFAULT 딱 두 개의 필드밖에 없으므로, ordinal 필드를 이용해서 비교하기로 했다. Enum 클래스가 복잡해지면, 별도의 필드를 만들어서 비교하는 것이 권장된다
compareTo는 기본적으로 오름차순으로 정렬된다. 따라서 가장 높은 권한을 가장 앞쪽으로(ordinal 필드가 적은 값이 되도록) 배치해두면 된다. 내림 차순으로 정렬하고 싶다면, return 값에 *-1을 반환한다.
compareTo를 저렇게 구현해두면, List<Role>을 .sort(null)을 사용해서 간단하게 비교할 수 있다.
public RoleType findUserRoleType(Long userId) {
List roles = userRoleRepository.findByUserId(userId);
/* 가장 높은 권한 순서로 sort <- Comparable */
roles.sort(null);
/* 첫번째 (가장 높은 권한을 반환) Role의 RoleType 반환 */
return roles.get(0).getRoleType();
}
이런 식으로, 가장 높은 권한을 쉽게 반환할 수 있어진다.
thymeleaf 수정
최종 수정된 thymeleaf li 태그 안이다. th:object를 사용해서 좀 더 가독성을 높였고, selected에서 루프된 option값과 현재 루프도는 유저의 roletype을 비교해서 selected 한다.
주의
th:selected=”” 사용시, th:selected=”${roleType.name()} == *{roleTypeName}}” 은 오류가 발생한다. 반드시 th:selected=”${roleType.name()} == *{roleTypeName}” 으로 비교해야 한다.
<li class="list-group-item" th:each="user : ${users}" th:object="${user}">
<span th:text="*{username}">username</span>
<select class="form-select w-auto d-inline-block" th:id="|roleType*{id}|">
<option th:each="roleType : ${roleTypes}"
th:value="${roleType.name()}"
th:selected="${roleType.name()} == *{roleTypeName}"
th:text="${roleType.description}">일반 유저</option>
</select>
<div class="btn-group">
<button class="btn btn-primary" th:value="*{id}" onclick="setRole(this)">권한 변경</button>
<button class="btn btn-danger" th:value="*{id}" onclick="deleteUser(this.value)">삭제</button>
</div>
</li>