메뉴 추가 및 spotless 적용

This commit is contained in:
2025-12-24 11:04:50 +09:00
parent 3cf45319d3
commit 079774531d
14 changed files with 562 additions and 350 deletions

View File

@@ -0,0 +1,101 @@
package com.kamco.cd.kamcoback.auth;
import com.kamco.cd.kamcoback.postgres.entity.MenuEntity;
import com.kamco.cd.kamcoback.postgres.repository.menu.MenuRepository;
import jakarta.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.function.Supplier;
import lombok.RequiredArgsConstructor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.stereotype.Component;
/**
* DB 기반 메뉴 권한 AuthorizationManager
*
* <p>- Redis 사용 안 함 - ADMIN 예외 없음 (DB 매핑 기준) - 한 계정 = role 1개 - menu_url(prefix) 기반 API 접근 제어
*/
@Component
@RequiredArgsConstructor
public class MenuAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
private static final Logger log = LogManager.getLogger(MenuAuthorizationManager.class);
private final MenuRepository menuAuthQueryRepository;
@Override
public AuthorizationDecision check(
Supplier<Authentication> authenticationSupplier, RequestAuthorizationContext context) {
HttpServletRequest request = context.getRequest();
String requestPath = normalizePath(request.getRequestURI());
Authentication authentication = authenticationSupplier.get();
if (authentication == null || !authentication.isAuthenticated()) {
return new AuthorizationDecision(false);
}
String role = extractSingleRole(authentication);
if (role == null) {
return new AuthorizationDecision(false);
}
// DB에서 role에 허용된 메뉴 조회
List<MenuEntity> allowedMenus = menuAuthQueryRepository.findAllowedMenuUrlsByRole(role);
if (allowedMenus == null || allowedMenus.isEmpty()) {
return new AuthorizationDecision(false);
}
// menu_url(prefix) 기반 접근 허용 판단
for (MenuEntity menu : allowedMenus) {
String baseUri = menu.getMenuUrl();
if (baseUri == null || baseUri.isBlank()) {
continue;
}
if (matchUri(baseUri, requestPath)) {
return new AuthorizationDecision(true);
}
}
return new AuthorizationDecision(false);
}
/* =========================
* role 추출
* ========================= */
private String extractSingleRole(Authentication authentication) {
GrantedAuthority ga = authentication.getAuthorities().stream().findFirst().orElse(null);
if (ga == null || ga.getAuthority() == null || ga.getAuthority().isBlank()) {
return null;
}
String auth = ga.getAuthority();
return auth.startsWith("ROLE_") ? auth.substring(5) : auth;
}
/* =========================
* URI prefix 매칭
* ========================= */
private boolean matchUri(String baseUri, String requestPath) {
String base = normalizePath(baseUri);
String req = normalizePath(requestPath);
return req.equals(base) || req.startsWith(base + "/");
}
/* =========================
* URI 정규화
* ========================= */
private String normalizePath(String path) {
if (path == null || path.isBlank()) {
return "/";
}
return path.replaceAll("//+", "/");
}
}

View File

@@ -1,188 +0,0 @@
package com.kamco.cd.kamcoback.auth;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.kamcoback.common.enums.RoleType;
import com.kamco.cd.kamcoback.menu.dto.MenuDto;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;
import lombok.RequiredArgsConstructor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.stereotype.Component;
/** redis에 등록된 메뉴별 권한 확인 */
@Component
@RequiredArgsConstructor
public class RedisAuthorizationManager
implements AuthorizationManager<RequestAuthorizationContext> {
private static final Logger log = LogManager.getLogger(RedisAuthorizationManager.class);
private static final String REDIS_KEY = "auth:api:role";
private final StringRedisTemplate redisTemplate;
private final ObjectMapper objectMapper;
@Override
public AuthorizationDecision check(
Supplier<Authentication> authenticationSupplier, RequestAuthorizationContext context) {
HttpServletRequest request = context.getRequest();
String requestPath = normalizePath(request.getRequestURI());
Authentication authentication = authenticationSupplier.get();
if (authentication == null || !authentication.isAuthenticated()) {
return new AuthorizationDecision(false);
}
// ADMIN 은 무조건 허용
if (hasAdmin(authentication)) {
return new AuthorizationDecision(true);
}
// 사용자 role 목록
List<String> userRoles = extractRoles(authentication);
if (userRoles.isEmpty()) {
return new AuthorizationDecision(false);
}
// Redis 조회 (1회)
String json = redisTemplate.opsForValue().get(REDIS_KEY);
if (json == null || json.isBlank()) {
return new AuthorizationDecision(false);
}
// JSON 파싱 (1회)
List<MenuDto.MenuWithRolesDto> menus = parseMenus(json);
if (menus.isEmpty()) {
return new AuthorizationDecision(false);
}
// role + prefix URI 매칭
for (String role : userRoles) {
if (isAllowed(menus, role, requestPath)) {
return new AuthorizationDecision(true);
}
}
return new AuthorizationDecision(false);
}
/* =========================
* 핵심 권한 판별 로직
* ========================= */
private boolean isAllowed(List<MenuDto.MenuWithRolesDto> menus, String role, String requestPath) {
for (MenuDto.MenuWithRolesDto menu : menus) {
if (menu == null) {
continue;
}
String baseUri = menu.getMenuApiUri();
if (baseUri == null || baseUri.isBlank()) {
continue;
}
// role 포함 여부
if (!hasRole(menu.getRoles(), role)) {
continue;
}
// prefix URI 허용
if (matchUri(baseUri, requestPath)) {
return true;
}
}
return false;
}
/* =========================
* URI prefix 매칭
* ========================= */
private boolean matchUri(String baseUri, String requestPath) {
String base = normalizePath(baseUri);
String req = normalizePath(requestPath);
// /api/log/audit → /api/log/audit/**
return req.equals(base) || req.startsWith(base + "/");
}
/* =========================
* role 문자열 처리
* ========================= */
private boolean hasRole(String rolesCsv, String targetRole) {
if (rolesCsv == null || rolesCsv.isBlank()) {
return false;
}
if (targetRole == null || targetRole.isBlank()) {
return false;
}
for (String r : rolesCsv.split(",")) {
if (targetRole.equalsIgnoreCase(r.trim())) {
return true;
}
}
return false;
}
/* =========================
* Redis JSON 파싱
* ========================= */
private List<MenuDto.MenuWithRolesDto> parseMenus(String json) {
try {
return objectMapper.readValue(json, new TypeReference<List<MenuDto.MenuWithRolesDto>>() {});
} catch (Exception e) {
log.warn("Failed to parse redis menu json. key={}", REDIS_KEY, e);
return Collections.emptyList();
}
}
/* =========================
* ADMIN 판별
* ========================= */
private boolean hasAdmin(Authentication authentication) {
for (GrantedAuthority ga : authentication.getAuthorities()) {
if (ga == null || ga.getAuthority() == null) {
continue;
}
String auth = ga.getAuthority();
String admin = RoleType.ADMIN.getId();
if (auth.equals(admin) || auth.equals("ROLE_" + admin)) {
return true;
}
}
return false;
}
/* =========================
* ROLE 목록 추출
* ========================= */
private List<String> extractRoles(Authentication authentication) {
return authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.filter(a -> a != null && !a.isBlank())
.map(a -> a.startsWith("ROLE_") ? a.substring(5) : a)
.toList();
}
/* =========================
* URI 정규화
* ========================= */
private String normalizePath(String path) {
if (path == null || path.isBlank()) {
return "/";
}
return path.replaceAll("//+", "/");
}
}

View File

@@ -2,7 +2,6 @@ package com.kamco.cd.kamcoback.config;
import com.kamco.cd.kamcoback.auth.CustomAuthenticationProvider;
import com.kamco.cd.kamcoback.auth.JwtAuthenticationFilter;
import com.kamco.cd.kamcoback.auth.RedisAuthorizationManager;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
@@ -29,7 +28,6 @@ public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final CustomAuthenticationProvider customAuthenticationProvider;
private final RedisAuthorizationManager redisAuthorizationManager;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

View File

@@ -16,14 +16,7 @@ import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@Tag(name = "영상 관리", description = "영상 관리 API")
@@ -112,6 +105,25 @@ public class MapSheetMngApiController {
return ApiResponseDto.ok(mapSheetMngService.mngComplete(mngYyyy));
}
@Operation(summary = "영상 데이터 관리 년도 목록", description = "영상 데이터 관리 년도 목록")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/mng-year-list")
public ApiResponseDto<List<Integer>> findMapSheetMngYyyyList() {
return ApiResponseDto.ok(mapSheetMngService.findMapSheetMngYyyyList());
}
@Operation(summary = "영상 데이터 관리 오류 목록", description = "영상 데이터 관리 오류 목록")
@ApiResponses(
value = {
@@ -163,8 +175,10 @@ public class MapSheetMngApiController {
public ApiResponseDto<MapSheetMngDto.DmlReturn> uploadPair(
@RequestPart("tfw") MultipartFile tfwFile,
@RequestPart("tif") MultipartFile tifFile,
@RequestParam(value = "hstUid", required = false) Long hstUid) {
return ApiResponseDto.createOK(mapSheetMngService.uploadPair(tfwFile, tifFile, hstUid));
@RequestParam(value = "hstUid", required = false) Long hstUid,
@RequestParam(value = "overwrite", required = false) boolean overwrite) {
return ApiResponseDto.createOK(
mapSheetMngService.uploadPair(tfwFile, tifFile, hstUid, overwrite));
}
@Operation(summary = "영상관리 > 파일조회", description = "영상관리 > 파일조회")

View File

@@ -0,0 +1,259 @@
package com.kamco.cd.kamcoback.mapsheet.service;
import com.kamco.cd.kamcoback.common.utils.FIleChecker;
import com.kamco.cd.kamcoback.config.FileConfig;
import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto;
import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.AddReq;
import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.DmlReturn;
import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.ErrorDataDto;
import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.ErrorSearchReq;
import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.MngDto;
import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.MngFilesDto;
import com.kamco.cd.kamcoback.postgres.core.MapSheetMngCoreService;
import jakarta.validation.Valid;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MapSheetMngService {
private final MapSheetMngCoreService mapSheetMngCoreService;
private final FileConfig fileConfig;
public List<MngDto> findMapSheetMngList() {
return mapSheetMngCoreService.findMapSheetMngList();
}
public List<Integer> findMapSheetMngYyyyList() {
return mapSheetMngCoreService.findMapSheetMngYyyyList();
}
public MngDto findMapSheetMng(int mngYyyy) {
return mapSheetMngCoreService.findMapSheetMng(mngYyyy);
}
@Transactional
public DmlReturn mngComplete(int mngYyyy) {
mapSheetMngCoreService.MapSheetMngComplete(mngYyyy);
return new DmlReturn("success", "작업완료 처리되었습니다.");
}
public Page<ErrorDataDto> findMapSheetErrorList(@Valid ErrorSearchReq searchReq) {
return mapSheetMngCoreService.findMapSheetErrorList(searchReq);
}
public ErrorDataDto findMapSheetError(Long hstUid) {
return mapSheetMngCoreService.findMapSheetError(hstUid);
}
@Transactional
public DmlReturn mngDataSave(AddReq AddReq) {
return mapSheetMngCoreService.mngDataSave(AddReq);
}
public DmlReturn uploadProcess(@Valid List<Long> hstUidList) {
return mapSheetMngCoreService.uploadProcess(hstUidList);
}
public DmlReturn updateExceptUseInference(@Valid List<Long> hstUidList) {
return mapSheetMngCoreService.updateExceptUseInference(hstUidList);
}
@Transactional
public DmlReturn uploadPair(
MultipartFile tfwFile, MultipartFile tifFile, Long hstUid, Boolean overwrite) {
String rootPath = fileConfig.getRootSyncDir();
String tmpPath = fileConfig.getTmpSyncDir();
ErrorDataDto errDto = mapSheetMngCoreService.findMapSheetError(hstUid);
if (errDto == null) {
return new DmlReturn("fail", "NO hstUid Data");
}
// 파일검증용 임시저장 폴더 확인
if (!FIleChecker.mkDir(tmpPath)) {
return new DmlReturn("fail", "CREATE TEMP FOLDER ERROR");
}
// 파일 유효성 검증
if (tfwFile == null || tfwFile.isEmpty() || tfwFile.getSize() == 0) {
return new DmlReturn("fail", "TFW SIZE 오류");
} else if (tifFile == null || tifFile.isEmpty() || tifFile.getSize() == 0) {
return new DmlReturn("fail", "TIF SIZE 오류");
}
// 확장자명 체크
if (!FIleChecker.checkExtensions(tfwFile.getOriginalFilename(), "tfw")) {
return new DmlReturn("fail", "TFW FILENAME ERROR");
} else if (!FIleChecker.checkExtensions(tifFile.getOriginalFilename(), "tif")) {
return new DmlReturn("fail", "TIF FILENAME ERROR");
}
MngDto mngDto = mapSheetMngCoreService.findMapSheetMng(errDto.getMngYyyy());
String targetYearDir = mngDto.getMngPath();
// 중복체크
List<FIleChecker.Basic> basicTfwList =
FIleChecker.getFilesFromAllDepth(targetYearDir, tfwFile.getOriginalFilename(), "tfw");
List<FIleChecker.Basic> basicTifList =
FIleChecker.getFilesFromAllDepth(targetYearDir, tifFile.getOriginalFilename(), "tif");
int tfwCnt =
(int)
basicTfwList.stream()
.filter(dto -> dto.getExtension().toString().equals("tfw"))
.count();
int tifCnt =
(int)
basicTifList.stream()
.filter(dto -> dto.getExtension().toString().equals("tif"))
.count();
if (!overwrite) {
if (tfwCnt > 0 || tifCnt > 0) {
String tfwtifMsg = "";
if (tfwCnt > 0) tfwtifMsg = tfwFile.getOriginalFilename();
if (tifCnt > 0) {
if (tfwCnt > 0) tfwtifMsg = "," + tifFile.getOriginalFilename();
else tfwtifMsg = tifFile.getOriginalFilename();
}
return new DmlReturn("duplicate", tfwtifMsg);
}
}
File directory = new File(tmpPath);
String tfwTmpPath = tmpPath + tfwFile.getOriginalFilename();
Path tfwTmpSavePath = Paths.get(tfwTmpPath);
String tifTmpPath = tmpPath + tifFile.getOriginalFilename();
Path tifTmpSavePath = Paths.get(tifTmpPath);
boolean fileUpload = true;
try {
tfwFile.transferTo(tfwTmpSavePath);
tifFile.transferTo(tifTmpSavePath);
} catch (IOException e) {
// throw new RuntimeException(e);
return new DmlReturn("fail", "UPLOAD ERROR");
}
if (!FIleChecker.cmmndGdalInfo(tifTmpPath)) return new DmlReturn("fail", "TIF TYPE ERROR");
if (!FIleChecker.checkTfw(tfwTmpPath)) return new DmlReturn("fail", "TFW TYPE ERROR");
// 싱크파일목록으로 업로드 경로 확인
List<MngFilesDto> mngFiles = mapSheetMngCoreService.findIdToMapSheetFileList(hstUid);
String uploadPath = "";
for (MngFilesDto dto : mngFiles) {
uploadPath = dto.getFilePath();
break;
}
Path tfwTargetPath = null;
Path tifTargetPath = null;
// 파일이 존재하지 않을 경우(0개) 해당년도 다른 파일경로로 참조
if (uploadPath.isEmpty()) {
MngFilesDto filesDto =
mapSheetMngCoreService.findYyyyToMapSheetFilePathRefer(errDto.getMngYyyy());
String referPath = filesDto.getFilePath();
uploadPath = Paths.get(referPath).getParent().toString() + "/" + errDto.getRefMapSheetNum();
}
// 업로드 경로 확인(없으면 생성)
if (!FIleChecker.mkDir(uploadPath)) {
return new DmlReturn("fail", "CREATE FOLDER ERROR");
}
tfwTargetPath = Paths.get(uploadPath).resolve(tfwFile.getOriginalFilename());
tifTargetPath = Paths.get(uploadPath).resolve(tifFile.getOriginalFilename());
try {
Files.move(tfwTmpSavePath, tfwTargetPath, StandardCopyOption.REPLACE_EXISTING);
Files.move(tifTmpSavePath, tifTargetPath, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new RuntimeException(e);
}
// hst업데이트
MapSheetMngDto.SyncCheckStateReqUpdateDto updReqSyncCheckState =
new MapSheetMngDto.SyncCheckStateReqUpdateDto();
updReqSyncCheckState.setHstUid(hstUid);
updReqSyncCheckState.setFilePath(uploadPath);
updReqSyncCheckState.setSyncCheckTfwFileName(tfwFile.getOriginalFilename());
updReqSyncCheckState.setSyncCheckTifFileName(tifFile.getOriginalFilename());
updReqSyncCheckState.setSyncCheckState("DONE");
mapSheetMngCoreService.updateMapSheetMngHstSyncCheckState(updReqSyncCheckState);
// 파일정보 업데이트
mapSheetMngCoreService.deleteByHstUidMngFile(hstUid);
MapSheetMngDto.MngFileAddReq addReq = new MapSheetMngDto.MngFileAddReq();
addReq.setMngYyyy(errDto.getMngYyyy());
addReq.setMapSheetNum(errDto.getMapSheetNum());
addReq.setRefMapSheetNum(errDto.getRefMapSheetNum());
addReq.setFilePath(uploadPath);
addReq.setFileName(tfwFile.getOriginalFilename());
addReq.setFileExt("tfw");
addReq.setFileSize(tfwFile.getSize());
addReq.setHstUid(errDto.getHstUid());
addReq.setFileState("DONE");
mapSheetMngCoreService.mngFileSave(addReq);
addReq.setFileExt("tif");
addReq.setFileSize(tifFile.getSize());
mapSheetMngCoreService.mngFileSave(addReq);
return new DmlReturn("success", "파일 업로드 완료되었습니다.");
}
public List<MngFilesDto> findHstUidToMapSheetFileList(Long hstUid) {
return mapSheetMngCoreService.findHstUidToMapSheetFileList(hstUid);
}
@Transactional
public DmlReturn deleteByFileUidMngFile(List<Long> fileUids) {
long hstUid = 0;
// hstUid = 149049;
for (Long uid : fileUids) {
MapSheetMngDto.MngFilesDto dto = mapSheetMngCoreService.findIdToMapSheetFile(uid);
hstUid = dto.getHstUid();
String filePath = dto.getFilePath() + "/" + dto.getFileName();
Path path = Paths.get(filePath);
try {
boolean isDeleted = Files.deleteIfExists(path);
if (isDeleted) {
System.out.println("파일 삭제 성공: " + filePath);
} else {
System.out.println("삭제 실패: 파일이 존재하지 않습니다.");
}
} catch (IOException e) {
System.err.println("파일 삭제 중 오류 발생: " + e.getMessage());
}
DmlReturn dmlReturn = mapSheetMngCoreService.deleteByFileUidMngFile(uid);
}
// 중복제거 확인후 처리상태(DONE)변경
if (hstUid > 0) mapSheetMngCoreService.updateByHstUidSyncCheckState(hstUid);
return new DmlReturn("success", fileUids.size() + "개 파일이 삭제되었습니다.");
}
}

View File

@@ -1,7 +1,6 @@
package com.kamco.cd.kamcoback.menu;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.kamcoback.common.utils.UserUtil;
import com.kamco.cd.kamcoback.config.api.ApiLogFunction;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
import com.kamco.cd.kamcoback.menu.dto.MenuDto;
@@ -11,17 +10,14 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.LinkedHashMap;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "메뉴 관리", description = "메뉴 관리 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/menu")
@@ -68,43 +64,4 @@ public class MenuApiController {
return ApiResponseDto.ok(ApiLogFunction.getUriMenuInfo(result, apiUri));
}
@Operation(summary = "권한별 메뉴 레디스 저장", description = "권한별 메뉴 레디스 저장")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "201",
description = "등록 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = MenuDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/auth")
public ApiResponseDto<String> getFindByRoleRedis() {
menuService.getFindByRoleRedis();
return ApiResponseDto.createOK("ok");
}
@Operation(summary = "권한별 메뉴 조회", description = "로그인 권한별 메뉴 목록")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = MenuDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/auth")
public ApiResponseDto<List<MenuDto.Basic>> getFindAllByRole() {
UserUtil userUtil = new UserUtil();
String role = userUtil.getRole();
return ApiResponseDto.ok(menuService.getFindByRole(role));
}
}

View File

@@ -0,0 +1,47 @@
package com.kamco.cd.kamcoback.menu;
import com.kamco.cd.kamcoback.common.utils.UserUtil;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
import com.kamco.cd.kamcoback.menu.dto.MenuDto;
import com.kamco.cd.kamcoback.menu.dto.MyMenuDto;
import com.kamco.cd.kamcoback.menu.service.MyMenuService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "메뉴 조회", description = "메뉴 조회 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/my/menus")
public class MyMenuApiController {
private final MyMenuService myMenuService;
@Operation(summary = "사용자별 메뉴 조회", description = "로그인 사용자별 권한 메뉴 목록")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = MenuDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping
public ApiResponseDto<List<MyMenuDto.Basic>> getFindAllByRole() {
UserUtil userUtil = new UserUtil();
String role = userUtil.getRole();
return ApiResponseDto.ok(myMenuService.getFindByRole(role));
}
}

View File

@@ -0,0 +1,29 @@
package com.kamco.cd.kamcoback.menu.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.ArrayList;
import java.util.List;
import lombok.Getter;
import lombok.NoArgsConstructor;
public class MyMenuDto {
@Schema(name = "My Menu Basic", description = "사용자별 권한 메뉴 목록")
@Getter
@NoArgsConstructor
public static class Basic {
private String id;
private String name;
private String menuUrl;
private Long order;
private List<Basic> children = new ArrayList<>();
public Basic(String menuUid, String menuNm, String menuUrl, Long menuOrder) {
this.id = menuUid;
this.name = menuNm;
this.menuUrl = menuUrl;
this.order = menuOrder;
}
}
}

View File

@@ -1,13 +1,9 @@
package com.kamco.cd.kamcoback.menu.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.kamcoback.common.enums.RoleType;
import com.kamco.cd.kamcoback.common.utils.UserUtil;
import com.kamco.cd.kamcoback.menu.dto.MenuDto;
import com.kamco.cd.kamcoback.postgres.core.MenuCoreService;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable;
@@ -27,60 +23,4 @@ public class MenuService {
public List<MenuDto.Basic> getFindAll() {
return menuCoreService.getFindAll();
}
/**
* 권한별 메뉴 목록 redis 등록
*
* @return
*/
public void getFindByRoleRedis() {
for (RoleType role : RoleType.values()) {
List<MenuDto.Basic> menus = menuCoreService.getFindByRole(role.name());
try {
String key = "menu:role:" + role.name();
String value = objectMapper.writeValueAsString(menus);
redisTemplate.opsForValue().set(key, value);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
List<MenuDto.MenuWithRolesDto> menusWithRoles = menuCoreService.getMenuWithRoles();
try {
String key = "auth:api:role";
String value = objectMapper.writeValueAsString(menusWithRoles);
redisTemplate.opsForValue().set(key, value);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
/**
* 권한별 메뉴 목록 조회
*
* @param role
* @return
*/
public List<MenuDto.Basic> getFindByRole(String role) {
String key = "menu:role:" + role;
String json = redisTemplate.opsForValue().get(key);
if (json == null) {
return new ArrayList<>();
}
JavaType type =
objectMapper.getTypeFactory().constructCollectionType(List.class, MenuDto.Basic.class);
List<MenuDto.Basic> cached;
try {
cached = objectMapper.readValue(json, type);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
return cached;
}
}

View File

@@ -0,0 +1,26 @@
package com.kamco.cd.kamcoback.menu.service;
import com.kamco.cd.kamcoback.menu.dto.MyMenuDto;
import com.kamco.cd.kamcoback.postgres.core.MenuCoreService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MyMenuService {
private final MenuCoreService menuCoreService;
/**
* 권한별 메뉴 목록 조회
*
* @param role
* @return
*/
public List<MyMenuDto.Basic> getFindByRole(String role) {
return menuCoreService.getFindByRole(role);
}
}

View File

@@ -1,8 +1,10 @@
package com.kamco.cd.kamcoback.postgres.core;
import com.kamco.cd.kamcoback.menu.dto.MenuDto;
import com.kamco.cd.kamcoback.menu.dto.MyMenuDto;
import com.kamco.cd.kamcoback.postgres.entity.MenuEntity;
import com.kamco.cd.kamcoback.postgres.repository.menu.MenuRepository;
import java.util.Comparator;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -23,16 +25,50 @@ public class MenuCoreService {
* @param role
* @return
*/
public List<MenuDto.Basic> getFindByRole(String role) {
return menuRepository.getFindByRole(role).stream().map(MenuEntity::toDto).toList();
public List<MyMenuDto.Basic> getFindByRole(String role) {
List<MenuEntity> entities = menuRepository.getFindByRole(role);
return entities.stream()
.map(
parent -> {
MyMenuDto.Basic p =
new MyMenuDto.Basic(
parent.getMenuUid(),
parent.getMenuNm(),
parent.getMenuUrl(),
parent.getMenuOrder());
parent.getChildren().stream()
.filter(
c ->
Boolean.TRUE.equals(c.getIsUse()) && Boolean.FALSE.equals(c.getDeleted()))
.sorted(
Comparator.comparing(
MenuEntity::getMenuOrder, Comparator.nullsLast(Long::compareTo)))
.map(
c ->
new MyMenuDto.Basic(
c.getMenuUid(), c.getMenuNm(), c.getMenuUrl(), c.getMenuOrder()))
.forEach(childDto -> p.getChildren().add(childDto));
return p;
})
.sorted(
Comparator.comparing(MyMenuDto.Basic::getOrder, Comparator.nullsLast(Long::compareTo)))
.toList();
}
/**
* 메뉴별 권한 조회
* 사용자 role 기준으로 접근 가능한 메뉴 URL 목록 조회
*
* @param role
* @return
*/
public List<MenuDto.MenuWithRolesDto> getMenuWithRoles() {
return menuRepository.getFindByMenuWithRoles();
public List<String> findAllowedMenuUrlsByRole(String role) {
return menuRepository.findAllowedMenuUrlsByRole(role).stream()
.map(MenuEntity::getMenuUrl)
.filter(url -> url != null && !url.isBlank())
.distinct()
.toList();
}
}

View File

@@ -1,6 +1,5 @@
package com.kamco.cd.kamcoback.postgres.repository.menu;
import com.kamco.cd.kamcoback.menu.dto.MenuDto.MenuWithRolesDto;
import com.kamco.cd.kamcoback.postgres.entity.MenuEntity;
import java.util.List;
@@ -17,9 +16,10 @@ public interface MenuRepositoryCustom {
List<MenuEntity> getFindByRole(String role);
/**
* 메뉴별 권한 조회
* 사용자 role 기준으로 접근 가능한 메뉴 URL 목록 조회
*
* @param role
* @return
*/
List<MenuWithRolesDto> getFindByMenuWithRoles();
List<MenuEntity> findAllowedMenuUrlsByRole(String role);
}

View File

@@ -3,13 +3,8 @@ package com.kamco.cd.kamcoback.postgres.repository.menu;
import static com.kamco.cd.kamcoback.postgres.entity.QMenuEntity.menuEntity;
import static com.kamco.cd.kamcoback.postgres.entity.QMenuMappEntity.menuMappEntity;
import com.kamco.cd.kamcoback.menu.dto.MenuDto.MenuWithRolesDto;
import com.kamco.cd.kamcoback.postgres.entity.MenuEntity;
import com.kamco.cd.kamcoback.postgres.entity.QMenuEntity;
import com.kamco.cd.kamcoback.postgres.entity.QMenuMappEntity;
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List;
import lombok.RequiredArgsConstructor;
@@ -34,12 +29,14 @@ public class MenuRepositoryImpl implements MenuRepositoryCustom {
@Override
public List<MenuEntity> getFindByRole(String role) {
QMenuEntity parent = QMenuEntity.menuEntity;
QMenuEntity child = new QMenuEntity("child");
List<MenuEntity> content =
queryFactory
.selectDistinct(menuEntity)
.from(menuEntity)
.leftJoin(menuEntity.children, child)
.selectDistinct(parent)
.from(parent)
.leftJoin(parent.children, child)
.fetchJoin()
.leftJoin(menuMappEntity)
.on(
@@ -47,43 +44,39 @@ public class MenuRepositoryImpl implements MenuRepositoryCustom {
.roleCode
.eq(role)
.and(menuMappEntity.deleted.isFalse())
.and(
menuMappEntity.menuUid.eq(menuEntity).or(menuMappEntity.menuUid.eq(child))))
.and(menuMappEntity.menuUid.eq(child)))
.where(
menuEntity.parent.isNull(),
menuEntity.deleted.isFalse(),
menuEntity.isUse.isTrue(),
parent.parent.isNull(),
parent.deleted.isFalse(),
parent.isUse.isTrue(),
menuMappEntity.id.isNotNull())
.orderBy(menuEntity.menuOrder.asc().nullsLast(), child.menuOrder.asc().nullsLast())
.orderBy(parent.menuOrder.asc().nullsLast(), child.menuOrder.asc().nullsLast())
.fetch();
return content;
}
/**
* 사용자 role 기준으로 접근 가능한 메뉴 URL 목록 조회
*
* @param role
* @return
*/
@Override
public List<MenuWithRolesDto> getFindByMenuWithRoles() {
QMenuEntity tm = menuEntity;
QMenuMappEntity tmm = menuMappEntity;
public List<MenuEntity> findAllowedMenuUrlsByRole(String role) {
Expression<String> roleAgg =
Expressions.stringTemplate("string_agg({0}, {1})", tmm.roleCode, Expressions.constant(","));
List<MenuWithRolesDto> content =
queryFactory
.select(
Projections.constructor(
MenuWithRolesDto.class,
tm.menuUid,
tm.menuNm,
tm.menuUrl,
tm.menuApiUri,
roleAgg))
.from(tm)
.leftJoin(tmm)
.on(tmm.menuUid.eq(tm).and(tmm.deleted.isFalse()))
.where(tm.deleted.isFalse())
.groupBy(tm.menuUid, tm.menuNm, tm.menuUrl, tm.menuApiUri)
.fetch();
return content;
return queryFactory
.selectDistinct(menuEntity)
.from(menuMappEntity)
.join(menuMappEntity.menuUid, menuEntity)
.where(
menuMappEntity.roleCode.eq(role),
menuMappEntity.deleted.isFalse(),
menuEntity.deleted.isFalse(),
menuEntity.isUse.isTrue(),
menuEntity.menuUrl.isNotNull(),
menuEntity.menuUrl.isNotEmpty())
.orderBy(menuEntity.menuOrder.asc().nullsLast())
.fetch();
}
}

View File

@@ -25,9 +25,9 @@ spring:
data:
redis:
host: localhost
host: 192.168.2.109
port: 6379
password: 1234
password: kamco
servlet:
multipart: