From 3b317c4d0f43f7183fbe9c174261869bf6dc5df2 Mon Sep 17 00:00:00 2001 From: soonduck-dreams Date: Mon, 17 Nov 2025 13:06:54 +0900 Subject: [PATCH 1/6] =?UTF-8?q?chore:=20Kakao=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20API=20=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apu/saerok_admin/infra/auth/BackendAuthClient.java | 7 +++---- .../saerok_admin/infra/auth/BackendAuthClientTest.java | 8 ++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/java/apu/saerok_admin/infra/auth/BackendAuthClient.java b/src/main/java/apu/saerok_admin/infra/auth/BackendAuthClient.java index bf820c7..a9f9ab4 100644 --- a/src/main/java/apu/saerok_admin/infra/auth/BackendAuthClient.java +++ b/src/main/java/apu/saerok_admin/infra/auth/BackendAuthClient.java @@ -25,7 +25,6 @@ public class BackendAuthClient { private static final Logger log = LoggerFactory.getLogger(BackendAuthClient.class); - private static final String ADMIN_CHANNEL = "admin"; private final RestClient authRestClient; private final List missingPrefixSegments; @@ -39,10 +38,10 @@ public BackendAuthClient( } public LoginSuccess kakaoLogin(String authorizationCode) { - KakaoLoginPayload payload = new KakaoLoginPayload(authorizationCode, ADMIN_CHANNEL); + KakaoLoginPayload payload = new KakaoLoginPayload(authorizationCode); log.info("Requesting Kakao login from backend with authorization code length {}", authorizationCode == null ? 0 : authorizationCode.length()); ResponseEntity response = authRestClient.post() - .uri(uriBuilder -> buildUri(uriBuilder, "auth", "kakao", "login")) + .uri(uriBuilder -> buildUri(uriBuilder, "admin", "auth", "kakao", "login")) .contentType(MediaType.APPLICATION_JSON) .body(payload) .retrieve() @@ -112,7 +111,7 @@ private URI buildUri(UriBuilder builder, String... segments) { return builder.build(); } - private record KakaoLoginPayload(String authorizationCode, String channel) { + private record KakaoLoginPayload(String authorizationCode) { } private record AppleLoginPayload(String authorizationCode) { diff --git a/src/test/java/apu/saerok_admin/infra/auth/BackendAuthClientTest.java b/src/test/java/apu/saerok_admin/infra/auth/BackendAuthClientTest.java index 5bec7c7..0195e41 100644 --- a/src/test/java/apu/saerok_admin/infra/auth/BackendAuthClientTest.java +++ b/src/test/java/apu/saerok_admin/infra/auth/BackendAuthClientTest.java @@ -42,7 +42,7 @@ void tearDown() { @Test void kakaoLoginSendsRequestIncludingApiPrefix() { - wireMockServer.stubFor(post(urlEqualTo("/api/v1/auth/kakao/login")) + wireMockServer.stubFor(post(urlEqualTo("/api/v1/admin/auth/kakao/login")) .withRequestBody(equalToJson("{" + "\"authorizationCode\":\"auth-code\"," + "\"channel\":\"admin\"" + @@ -60,7 +60,7 @@ void kakaoLoginSendsRequestIncludingApiPrefix() { assertThat(loginSuccess.accessToken()).isEqualTo("backend-token"); assertThat(loginSuccess.refreshCookies()).contains("refreshToken=abc; Path=/; HttpOnly"); - wireMockServer.verify(postRequestedFor(urlEqualTo("/api/v1/auth/kakao/login"))); + wireMockServer.verify(postRequestedFor(urlEqualTo("/api/v1/admin/auth/kakao/login"))); } @Test @@ -73,7 +73,7 @@ void kakaoLoginAddsMissingPrefixWhenBaseUrlOmitsIt() { "http://localhost:" + wireMockServer.port(), "/api/v1" )); - wireMockServer.stubFor(post(urlEqualTo("/api/v1/auth/kakao/login")) + wireMockServer.stubFor(post(urlEqualTo("/api/v1/admin/auth/kakao/login")) .withRequestBody(equalToJson("{" + "\"authorizationCode\":\"auth-code\"," + "\"channel\":\"admin\"" + @@ -88,6 +88,6 @@ void kakaoLoginAddsMissingPrefixWhenBaseUrlOmitsIt() { BackendAuthClient.LoginSuccess loginSuccess = client.kakaoLogin("auth-code"); assertThat(loginSuccess.accessToken()).isEqualTo("backend-token"); - wireMockServer.verify(postRequestedFor(urlEqualTo("/api/v1/auth/kakao/login"))); + wireMockServer.verify(postRequestedFor(urlEqualTo("/api/v1/admin/auth/kakao/login"))); } } From fe0e5de0e85bf922dc2a52cd124022cc47bc3eb3 Mon Sep 17 00:00:00 2001 From: soonduck-dreams Date: Mon, 17 Nov 2025 15:59:32 +0900 Subject: [PATCH 2/6] =?UTF-8?q?chore:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EB=B0=8F=20=EC=97=AD=ED=95=A0=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81,=20=ED=8D=BC?= =?UTF-8?q?=EB=AF=B8=EC=85=98=20=EA=B8=B0=EB=B0=98=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infra/CurrentAdminClient.java | 105 +++++++++++++----- .../infra/auth/BackendAuthClient.java | 6 +- .../apu/saerok_admin/web/AdController.java | 36 +++--- .../web/view/CurrentAdminProfile.java | 56 +++++++++- src/main/resources/templates/ads/list.html | 21 ++-- .../templates/fragments/_navbar.html | 10 +- .../resources/templates/reports/detail.html | 36 +++--- .../resources/templates/reports/list.html | 3 +- 8 files changed, 187 insertions(+), 86 deletions(-) diff --git a/src/main/java/apu/saerok_admin/infra/CurrentAdminClient.java b/src/main/java/apu/saerok_admin/infra/CurrentAdminClient.java index 58d2a88..3831466 100644 --- a/src/main/java/apu/saerok_admin/infra/CurrentAdminClient.java +++ b/src/main/java/apu/saerok_admin/infra/CurrentAdminClient.java @@ -6,7 +6,6 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.Optional; import java.util.Set; import org.slf4j.Logger; @@ -22,12 +21,6 @@ public class CurrentAdminClient { private static final Logger log = LoggerFactory.getLogger(CurrentAdminClient.class); - private static final Map ROLE_DESCRIPTION_MAP = Map.of( - "ADMIN_VIEWER", "열람자", - "ADMIN_EDITOR", "운영자" - ); - private static final String UNKNOWN_ROLE_DESCRIPTION = "알 수 없는 관리자 권한"; - private final RestClient saerokRestClient; private final List missingPrefixSegments; private final LoginSessionManager loginSessionManager; @@ -57,14 +50,19 @@ public Optional fetchCurrentAdminProfile() { return Optional.empty(); } - List roles = response.roles() != null ? List.copyOf(response.roles()) : List.of(); + List backendRoles = response.roles() != null ? List.copyOf(response.roles()) : List.of(); + AdminRoleInfo roleInfo = fetchAdminRoleInfo(); + List roleCodes = !roleInfo.roleCodes().isEmpty() + ? roleInfo.roleCodes() + : normalizeRoleCodes(backendRoles); return Optional.of(new CurrentAdminProfile( response.nickname(), response.email(), response.profileImageUrl(), - toRoleDescriptions(roles), - normalizeRoleCodes(roles) + roleInfo.roleDisplayNames(), + roleCodes, + roleInfo.permissionKeys() )); } catch (RestClientResponseException exception) { log.warn( @@ -96,30 +94,79 @@ private record BackendUserProfileResponse( ) { } - private List toRoleDescriptions(List roles) { - if (roles == null || roles.isEmpty()) { - return List.of(); - } - - Set descriptions = new LinkedHashSet<>(); - for (String role : roles) { - if (!StringUtils.hasText(role)) { - continue; - } + private AdminRoleInfo fetchAdminRoleInfo() { + try { + AdminMyRoleResponse response = saerokRestClient.get() + .uri(uriBuilder -> buildUri(uriBuilder, "admin", "role", "me")) + .retrieve() + .body(AdminMyRoleResponse.class); - String normalized = role.toUpperCase(Locale.ROOT); - String description = ROLE_DESCRIPTION_MAP.get(normalized); - if (description != null) { - descriptions.add(description); - continue; + if (response == null) { + return AdminRoleInfo.empty(); } - if (normalized.startsWith("ADMIN_")) { - descriptions.add(UNKNOWN_ROLE_DESCRIPTION); - } + List roleDisplayNames = response.roles() == null + ? List.of() + : response.roles().stream() + .map(RoleSummaryResponse::displayName) + .filter(StringUtils::hasText) + .map(String::trim) + .toList(); + List roleCodes = response.roles() == null + ? List.of() + : response.roles().stream() + .map(RoleSummaryResponse::code) + .filter(StringUtils::hasText) + .toList(); + List permissionKeys = response.permissions() == null + ? List.of() + : response.permissions().stream() + .map(PermissionSummaryResponse::key) + .filter(StringUtils::hasText) + .toList(); + return new AdminRoleInfo(roleDisplayNames, roleCodes, permissionKeys); + } catch (RestClientResponseException exception) { + log.warn( + "Failed to fetch current admin roles. status={}, body={}", + exception.getStatusCode(), + exception.getResponseBodyAsString(), + exception + ); + } catch (RestClientException exception) { + log.warn("Failed to fetch current admin roles.", exception); } + return AdminRoleInfo.empty(); + } + + private record AdminMyRoleResponse( + List roles, + List permissions + ) { + } - return descriptions.isEmpty() ? List.of() : List.copyOf(descriptions); + private record RoleSummaryResponse( + Long id, + String code, + String displayName, + String description, + Boolean builtin + ) { + } + + private record PermissionSummaryResponse( + String key, + String description + ) { + } + + private record AdminRoleInfo( + List roleDisplayNames, + List roleCodes, + List permissionKeys + ) { + private static AdminRoleInfo empty() { + return new AdminRoleInfo(List.of(), List.of(), List.of()); + } } private List normalizeRoleCodes(List roles) { diff --git a/src/main/java/apu/saerok_admin/infra/auth/BackendAuthClient.java b/src/main/java/apu/saerok_admin/infra/auth/BackendAuthClient.java index a9f9ab4..551c0c4 100644 --- a/src/main/java/apu/saerok_admin/infra/auth/BackendAuthClient.java +++ b/src/main/java/apu/saerok_admin/infra/auth/BackendAuthClient.java @@ -111,7 +111,11 @@ private URI buildUri(UriBuilder builder, String... segments) { return builder.build(); } - private record KakaoLoginPayload(String authorizationCode) { + private record KakaoLoginPayload(String authorizationCode, String channel) { + + private KakaoLoginPayload(String authorizationCode) { + this(authorizationCode, "admin"); + } } private record AppleLoginPayload(String authorizationCode) { diff --git a/src/main/java/apu/saerok_admin/web/AdController.java b/src/main/java/apu/saerok_admin/web/AdController.java index 510d4c2..2e2c442 100644 --- a/src/main/java/apu/saerok_admin/web/AdController.java +++ b/src/main/java/apu/saerok_admin/web/AdController.java @@ -58,6 +58,8 @@ public class AdController { private static final Logger log = LoggerFactory.getLogger(AdController.class); + private static final String PERMISSION_ADMIN_AD_WRITE = "ADMIN_AD_WRITE"; + private static final String PERMISSION_ADMIN_SLOT_DELETE = "ADMIN_SLOT_DELETE"; private final AdminAdClient adminAdClient; private final Clock clock; @@ -168,7 +170,7 @@ public String index(@RequestParam(name = "tab", defaultValue = "ads") String tab public String newAdForm(@ModelAttribute("currentAdminProfile") CurrentAdminProfile currentAdminProfile, Model model, RedirectAttributes redirectAttributes) { - if (!currentAdminProfile.isAdminEditor()) { + if (!currentAdminProfile.hasPermission(PERMISSION_ADMIN_AD_WRITE)) { redirectAttributes.addFlashAttribute("flashStatus", "error"); redirectAttributes.addFlashAttribute("flashMessage", "광고를 등록할 권한이 없습니다."); return "redirect:/ads"; @@ -194,7 +196,7 @@ public String createAd(@ModelAttribute("currentAdminProfile") CurrentAdminProfil @RequestParam(name = "objectKey") String objectKey, @RequestParam(name = "contentType") String contentType, RedirectAttributes redirectAttributes) { - if (!currentAdminProfile.isAdminEditor()) { + if (!currentAdminProfile.hasPermission(PERMISSION_ADMIN_AD_WRITE)) { redirectAttributes.addFlashAttribute("flashStatus", "error"); redirectAttributes.addFlashAttribute("flashMessage", "광고를 등록할 권한이 없습니다."); return "redirect:/ads?tab=ads"; @@ -229,7 +231,7 @@ public String editAdForm(@ModelAttribute("currentAdminProfile") CurrentAdminProf @RequestParam("id") Long adId, Model model, RedirectAttributes redirectAttributes) { - if (!currentAdminProfile.isAdminEditor()) { + if (!currentAdminProfile.hasPermission(PERMISSION_ADMIN_AD_WRITE)) { redirectAttributes.addFlashAttribute("flashStatus", "error"); redirectAttributes.addFlashAttribute("flashMessage", "광고를 수정할 권한이 없습니다."); return "redirect:/ads"; @@ -282,7 +284,7 @@ public String updateAd(@ModelAttribute("currentAdminProfile") CurrentAdminProfil @RequestParam(name = "objectKey", required = false) String objectKey, @RequestParam(name = "contentType", required = false) String contentType, RedirectAttributes redirectAttributes) { - if (!currentAdminProfile.isAdminEditor()) { + if (!currentAdminProfile.hasPermission(PERMISSION_ADMIN_AD_WRITE)) { redirectAttributes.addFlashAttribute("flashStatus", "error"); redirectAttributes.addFlashAttribute("flashMessage", "광고를 수정할 권한이 없습니다."); return "redirect:/ads?tab=ads"; @@ -317,7 +319,7 @@ public String updateAd(@ModelAttribute("currentAdminProfile") CurrentAdminProfil public String deleteAd(@ModelAttribute("currentAdminProfile") CurrentAdminProfile currentAdminProfile, @RequestParam("id") Long adId, RedirectAttributes redirectAttributes) { - if (!currentAdminProfile.isAdminEditor()) { + if (!currentAdminProfile.hasPermission(PERMISSION_ADMIN_AD_WRITE)) { redirectAttributes.addFlashAttribute("flashStatus", "error"); redirectAttributes.addFlashAttribute("flashMessage", "광고를 삭제할 권한이 없습니다."); return "redirect:/ads?tab=ads"; @@ -344,7 +346,7 @@ public String deleteAd(@ModelAttribute("currentAdminProfile") CurrentAdminProfil public String newSlotForm(@ModelAttribute("currentAdminProfile") CurrentAdminProfile currentAdminProfile, Model model, RedirectAttributes redirectAttributes) { - if (!currentAdminProfile.isAdminEditor()) { + if (!currentAdminProfile.hasPermission(PERMISSION_ADMIN_AD_WRITE)) { redirectAttributes.addFlashAttribute("flashStatus", "error"); redirectAttributes.addFlashAttribute("flashMessage", "광고 위치를 등록할 권한이 없습니다."); return "redirect:/ads?tab=schedule"; @@ -369,7 +371,7 @@ public String createSlot(@ModelAttribute("currentAdminProfile") CurrentAdminProf @RequestParam("fallbackRatio") Double fallbackRatioPercent, @RequestParam("ttlSeconds") Integer ttlSeconds, RedirectAttributes redirectAttributes) { - if (!currentAdminProfile.isAdminEditor()) { + if (!currentAdminProfile.hasPermission(PERMISSION_ADMIN_AD_WRITE)) { redirectAttributes.addFlashAttribute("flashStatus", "error"); redirectAttributes.addFlashAttribute("flashMessage", "광고 위치를 등록할 권한이 없습니다."); return "redirect:/ads?tab=schedule"; @@ -405,7 +407,7 @@ public String editSlotForm(@ModelAttribute("currentAdminProfile") CurrentAdminPr @RequestParam("id") Long slotId, Model model, RedirectAttributes redirectAttributes) { - if (!currentAdminProfile.isAdminEditor()) { + if (!currentAdminProfile.hasPermission(PERMISSION_ADMIN_AD_WRITE)) { redirectAttributes.addFlashAttribute("flashStatus", "error"); redirectAttributes.addFlashAttribute("flashMessage", "광고 위치를 수정할 권한이 없습니다."); return "redirect:/ads?tab=schedule"; @@ -472,7 +474,7 @@ public String updateSlot(@ModelAttribute("currentAdminProfile") CurrentAdminProf failureRedirect = "redirect:/ads/slots/edit?id=" + slotId; } - if (!currentAdminProfile.isAdminEditor()) { + if (!currentAdminProfile.hasPermission(PERMISSION_ADMIN_AD_WRITE)) { redirectAttributes.addFlashAttribute("flashStatus", "error"); redirectAttributes.addFlashAttribute("flashMessage", "광고 위치를 수정할 권한이 없습니다."); return successRedirect; @@ -507,7 +509,7 @@ public String updateSlot(@ModelAttribute("currentAdminProfile") CurrentAdminProf public String deleteSlot(@ModelAttribute("currentAdminProfile") CurrentAdminProfile currentAdminProfile, @RequestParam("id") Long slotId, RedirectAttributes redirectAttributes) { - if (!currentAdminProfile.isAdminEditor()) { + if (!currentAdminProfile.hasPermission(PERMISSION_ADMIN_SLOT_DELETE)) { redirectAttributes.addFlashAttribute("flashStatus", "error"); redirectAttributes.addFlashAttribute("flashMessage", "광고 위치를 삭제할 권한이 없습니다."); return "redirect:/ads?tab=schedule"; @@ -535,7 +537,7 @@ public String newPlacementForm(@ModelAttribute("currentAdminProfile") CurrentAdm @RequestParam(name = "slotId", required = false) Long preselectedSlotId, Model model, RedirectAttributes redirectAttributes) { - if (!currentAdminProfile.isAdminEditor()) { + if (!currentAdminProfile.hasPermission(PERMISSION_ADMIN_AD_WRITE)) { redirectAttributes.addFlashAttribute("flashStatus", "error"); redirectAttributes.addFlashAttribute("flashMessage", "광고 노출 스케줄을 등록할 권한이 없습니다."); return "redirect:/ads?tab=schedule"; @@ -567,7 +569,7 @@ public String createPlacement(@ModelAttribute("currentAdminProfile") CurrentAdmi @RequestParam("weight") Short weight, @RequestParam(name = "enabled", defaultValue = "false") boolean enabled, RedirectAttributes redirectAttributes) { - if (!currentAdminProfile.isAdminEditor()) { + if (!currentAdminProfile.hasPermission(PERMISSION_ADMIN_AD_WRITE)) { redirectAttributes.addFlashAttribute("flashStatus", "error"); redirectAttributes.addFlashAttribute("flashMessage", "광고 노출 스케줄을 등록할 권한이 없습니다."); return "redirect:/ads?tab=schedule"; @@ -600,7 +602,7 @@ public String editPlacementForm(@ModelAttribute("currentAdminProfile") CurrentAd @RequestParam("id") Long placementId, Model model, RedirectAttributes redirectAttributes) { - if (!currentAdminProfile.isAdminEditor()) { + if (!currentAdminProfile.hasPermission(PERMISSION_ADMIN_AD_WRITE)) { redirectAttributes.addFlashAttribute("flashStatus", "error"); redirectAttributes.addFlashAttribute("flashMessage", "광고 노출 스케줄을 수정할 권한이 없습니다."); return "redirect:/ads?tab=schedule"; @@ -663,7 +665,7 @@ public String updatePlacement(@ModelAttribute("currentAdminProfile") CurrentAdmi @RequestParam("weight") Short weight, @RequestParam(name = "enabled", defaultValue = "false") boolean enabled, RedirectAttributes redirectAttributes) { - if (!currentAdminProfile.isAdminEditor()) { + if (!currentAdminProfile.hasPermission(PERMISSION_ADMIN_AD_WRITE)) { redirectAttributes.addFlashAttribute("flashStatus", "error"); redirectAttributes.addFlashAttribute("flashMessage", "광고 노출 스케줄을 수정할 권한이 없습니다."); return "redirect:/ads?tab=schedule"; @@ -694,7 +696,7 @@ public String updatePlacement(@ModelAttribute("currentAdminProfile") CurrentAdmi public String deletePlacement(@ModelAttribute("currentAdminProfile") CurrentAdminProfile currentAdminProfile, @RequestParam("id") Long placementId, RedirectAttributes redirectAttributes) { - if (!currentAdminProfile.isAdminEditor()) { + if (!currentAdminProfile.hasPermission(PERMISSION_ADMIN_AD_WRITE)) { redirectAttributes.addFlashAttribute("flashStatus", "error"); redirectAttributes.addFlashAttribute("flashMessage", "광고 노출 스케줄을 삭제할 권한이 없습니다."); return "redirect:/ads?tab=schedule"; @@ -722,7 +724,7 @@ public String togglePlacement(@ModelAttribute("currentAdminProfile") CurrentAdmi @RequestParam("id") Long placementId, @RequestParam("enabled") boolean enabled, RedirectAttributes redirectAttributes) { - if (!currentAdminProfile.isAdminEditor()) { + if (!currentAdminProfile.hasPermission(PERMISSION_ADMIN_AD_WRITE)) { redirectAttributes.addFlashAttribute("flashStatus", "error"); redirectAttributes.addFlashAttribute("flashMessage", "광고 노출 상태를 변경할 권한이 없습니다."); return "redirect:/ads?tab=schedule"; @@ -770,7 +772,7 @@ public String togglePlacement(@ModelAttribute("currentAdminProfile") CurrentAdmi @ResponseBody public ResponseEntity presignImage(@ModelAttribute("currentAdminProfile") CurrentAdminProfile currentAdminProfile, @RequestBody Map payload) { - if (!currentAdminProfile.isAdminEditor()) { + if (!currentAdminProfile.hasPermission(PERMISSION_ADMIN_AD_WRITE)) { return ResponseEntity.status(HttpStatus.FORBIDDEN) .body(Map.of("message", "이미지 업로드 권한이 없습니다.")); } diff --git a/src/main/java/apu/saerok_admin/web/view/CurrentAdminProfile.java b/src/main/java/apu/saerok_admin/web/view/CurrentAdminProfile.java index d87da27..f03b2dd 100644 --- a/src/main/java/apu/saerok_admin/web/view/CurrentAdminProfile.java +++ b/src/main/java/apu/saerok_admin/web/view/CurrentAdminProfile.java @@ -10,8 +10,9 @@ public record CurrentAdminProfile( String nickname, String email, String profileImageUrl, - List roleDescriptions, - List roleCodes + List roleDisplayNames, + List roleCodes, + List permissionKeys ) { private static final String DEFAULT_PROFILE_IMAGE_URL = @@ -23,12 +24,13 @@ public record CurrentAdminProfile( nickname = StringUtils.hasText(nickname) ? nickname : DEFAULT_NICKNAME; email = StringUtils.hasText(email) ? email : DEFAULT_EMAIL; profileImageUrl = StringUtils.hasText(profileImageUrl) ? profileImageUrl : DEFAULT_PROFILE_IMAGE_URL; - roleDescriptions = roleDescriptions != null ? List.copyOf(roleDescriptions) : List.of(); + roleDisplayNames = normalizeRoleDisplayNames(roleDisplayNames); roleCodes = normalizeRoleCodes(roleCodes); + permissionKeys = normalizePermissionKeys(permissionKeys); } - public boolean hasRoleDescriptions() { - return !roleDescriptions.isEmpty(); + public boolean hasRoleDisplayNames() { + return !roleDisplayNames.isEmpty(); } public boolean hasRole(String roleCode) { @@ -48,7 +50,15 @@ public boolean isAdminViewerOnly() { } public static CurrentAdminProfile placeholder() { - return new CurrentAdminProfile(null, null, null, List.of(), List.of()); + return new CurrentAdminProfile(null, null, null, List.of(), List.of(), List.of()); + } + + public boolean hasPermission(String permissionKey) { + if (!StringUtils.hasText(permissionKey)) { + return false; + } + String normalized = permissionKey.toUpperCase(Locale.ROOT); + return permissionKeys.contains(normalized); } private static List normalizeRoleCodes(List rawRoles) { @@ -67,4 +77,38 @@ private static List normalizeRoleCodes(List rawRoles) { } return List.copyOf(normalized); } + + private static List normalizePermissionKeys(List rawKeys) { + if (rawKeys == null || rawKeys.isEmpty()) { + return List.of(); + } + Set normalized = new LinkedHashSet<>(); + for (String key : rawKeys) { + if (!StringUtils.hasText(key)) { + continue; + } + normalized.add(key.toUpperCase(Locale.ROOT)); + } + if (normalized.isEmpty()) { + return List.of(); + } + return List.copyOf(normalized); + } + + private static List normalizeRoleDisplayNames(List names) { + if (names == null || names.isEmpty()) { + return List.of(); + } + Set normalized = new LinkedHashSet<>(); + for (String name : names) { + if (!StringUtils.hasText(name)) { + continue; + } + normalized.add(name.trim()); + } + if (normalized.isEmpty()) { + return List.of(); + } + return List.copyOf(normalized); + } } diff --git a/src/main/resources/templates/ads/list.html b/src/main/resources/templates/ads/list.html index e45fbfc..f1b955a 100644 --- a/src/main/resources/templates/ads/list.html +++ b/src/main/resources/templates/ads/list.html @@ -67,7 +67,7 @@

광고

광고 목록

-
+ -
+
수정
@@ -162,16 +162,19 @@

HOME_TOP

광고 위치 설명

-
+
새 스케줄 등록 광고 위치 설정 - +
+ th:if="${currentAdminProfile.hasPermission('ADMIN_AD_WRITE') && group.slotId() != null && group.slotCode() != null}">
-
+
@@ -290,7 +293,7 @@

HOME_TOP

-
+