Merge remote-tracking branch 'origin/feat/dev_251201' into feat/dev_251201

# Conflicts:
#	src/main/java/com/kamco/cd/kamcoback/mapsheet/service/MapSheetMngService.java
This commit is contained in:
Moon
2025-12-24 13:59:32 +09:00
18 changed files with 352 additions and 386 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.CustomAuthenticationProvider;
import com.kamco.cd.kamcoback.auth.JwtAuthenticationFilter; import com.kamco.cd.kamcoback.auth.JwtAuthenticationFilter;
import com.kamco.cd.kamcoback.auth.RedisAuthorizationManager;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@@ -29,7 +28,6 @@ public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter; private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final CustomAuthenticationProvider customAuthenticationProvider; private final CustomAuthenticationProvider customAuthenticationProvider;
private final RedisAuthorizationManager redisAuthorizationManager;
@Bean @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

View File

@@ -177,7 +177,8 @@ public class MapSheetMngApiController {
@RequestPart("tif") MultipartFile tifFile, @RequestPart("tif") MultipartFile tifFile,
@RequestParam(value = "hstUid", required = false) Long hstUid, @RequestParam(value = "hstUid", required = false) Long hstUid,
@RequestParam(value = "overwrite", required = false) boolean overwrite) { @RequestParam(value = "overwrite", required = false) boolean overwrite) {
return ApiResponseDto.createOK(mapSheetMngService.uploadPair(tfwFile, tifFile, hstUid, overwrite)); return ApiResponseDto.createOK(
mapSheetMngService.uploadPair(tfwFile, tifFile, hstUid, overwrite));
} }
@Operation(summary = "영상관리 > 파일조회", description = "영상관리 > 파일조회") @Operation(summary = "영상관리 > 파일조회", description = "영상관리 > 파일조회")

View File

@@ -1,7 +1,6 @@
package com.kamco.cd.kamcoback.menu; package com.kamco.cd.kamcoback.menu;
import com.fasterxml.jackson.databind.ObjectMapper; 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.ApiLogFunction;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto; import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
import com.kamco.cd.kamcoback.menu.dto.MenuDto; 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.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping; 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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@Tag(name = "메뉴 관리", description = "메뉴 관리 API")
@RestController @RestController
@RequiredArgsConstructor @RequiredArgsConstructor
@RequestMapping("/api/menu") @RequestMapping("/api/menu")
@@ -68,43 +64,4 @@ public class MenuApiController {
return ApiResponseDto.ok(ApiLogFunction.getUriMenuInfo(result, apiUri)); 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

@@ -45,8 +45,7 @@ public class MenuDto {
Long updatedUid, Long updatedUid,
List<MenuDto.Basic> children, List<MenuDto.Basic> children,
ZonedDateTime createdDttm, ZonedDateTime createdDttm,
ZonedDateTime updatedDttm, ZonedDateTime updatedDttm) {
String menuApiUrl) {
this.menuUid = menuUid; this.menuUid = menuUid;
this.menuNm = menuNm; this.menuNm = menuNm;
this.menuUrl = menuUrl; this.menuUrl = menuUrl;
@@ -59,7 +58,6 @@ public class MenuDto {
this.children = children; this.children = children;
this.createdDttm = createdDttm; this.createdDttm = createdDttm;
this.updatedDttm = updatedDttm; this.updatedDttm = updatedDttm;
this.menuApiUrl = menuApiUrl;
} }
} }
@@ -70,7 +68,6 @@ public class MenuDto {
private String menuUid; private String menuUid;
private String menuNm; private String menuNm;
private String menuUrl; private String menuUrl;
private String menuApiUri;
private String roles; // "ROLE_A,ROLE_B" private String roles; // "ROLE_A,ROLE_B"
} }
} }

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; 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.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.common.utils.UserUtil;
import com.kamco.cd.kamcoback.menu.dto.MenuDto; import com.kamco.cd.kamcoback.menu.dto.MenuDto;
import com.kamco.cd.kamcoback.postgres.core.MenuCoreService; import com.kamco.cd.kamcoback.postgres.core.MenuCoreService;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
@@ -27,60 +23,4 @@ public class MenuService {
public List<MenuDto.Basic> getFindAll() { public List<MenuDto.Basic> getFindAll() {
return menuCoreService.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

@@ -13,7 +13,12 @@ import jakarta.transaction.Transactional;
import java.time.LocalDate; import java.time.LocalDate;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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") @Tag(name = "모델 관리", description = "모델 관리 API")
@RequiredArgsConstructor @RequiredArgsConstructor

View File

@@ -16,15 +16,34 @@ import org.springframework.data.domain.Sort;
public class ModelMngDto { public class ModelMngDto {
@CodeExpose
@Getter
@AllArgsConstructor
public enum ModelType implements EnumType {
M1("모델 M1"),
M2("모델 M2"),
M3("모델 M3");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}
@Schema(name = "ModelMgmtDto Basic", description = "모델관리 엔티티 기본 정보") @Schema(name = "ModelMgmtDto Basic", description = "모델관리 엔티티 기본 정보")
@Getter @Getter
@NoArgsConstructor @NoArgsConstructor
public static class Basic { public static class Basic {
private Long modelUid;
private String modelVer; private String modelVer;
private String hyperVer;
private String epochVer;
private String dockerFileNm;
@JsonFormatDttm private ZonedDateTime createCompleteDttm; @JsonFormatDttm private ZonedDateTime createCompleteDttm;
@JsonFormatDttm private ZonedDateTime recentUseDttm; @JsonFormatDttm private ZonedDateTime recentUseDttm;
@@ -35,22 +54,27 @@ public class ModelMngDto {
@JsonFormatDttm private ZonedDateTime updatedDttm; @JsonFormatDttm private ZonedDateTime updatedDttm;
private Long updatedUid; private Long updatedUid;
private String modelType;
private String filePath;
private String fileName;
private String memo;
public Basic( public Basic(
Long modelUid,
String modelVer, String modelVer,
String hyperVer,
String epochVer,
String dockerFileNm,
ZonedDateTime createCompleteDttm, ZonedDateTime createCompleteDttm,
ZonedDateTime recentUseDttm, ZonedDateTime recentUseDttm,
Boolean deleted, Boolean deleted,
ZonedDateTime createdDttm, ZonedDateTime createdDttm,
Long createdUid, Long createdUid,
ZonedDateTime updatedDttm, ZonedDateTime updatedDttm,
Long updatedUid) { Long updatedUid,
String modelType,
String filePath,
String fileName,
String memo) {
this.modelUid = modelUid;
this.modelVer = modelVer; this.modelVer = modelVer;
this.hyperVer = hyperVer;
this.epochVer = epochVer;
this.dockerFileNm = dockerFileNm;
this.createCompleteDttm = createCompleteDttm; this.createCompleteDttm = createCompleteDttm;
this.recentUseDttm = recentUseDttm; this.recentUseDttm = recentUseDttm;
this.deleted = deleted; this.deleted = deleted;
@@ -58,6 +82,10 @@ public class ModelMngDto {
this.createdUid = createdUid; this.createdUid = createdUid;
this.updatedDttm = updatedDttm; this.updatedDttm = updatedDttm;
this.updatedUid = updatedUid; this.updatedUid = updatedUid;
this.modelType = modelType;
this.filePath = filePath;
this.fileName = fileName;
this.memo = memo;
} }
} }
@@ -67,17 +95,18 @@ public class ModelMngDto {
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public static class ModelList { public static class ModelList {
private Integer rowNum; private Integer rowNum;
private String modelVer; private String modelVer;
private String fileName; private String fileName;
private String modelType; private String modelType;
private String createCompleteDttm; private String createCompleteDttm;
private String recentUseDttm;
private BigDecimal f1Score; private BigDecimal f1Score;
private BigDecimal precision; private BigDecimal precision;
private BigDecimal recall; private BigDecimal recall;
private BigDecimal accuracy; private BigDecimal accuracy;
private BigDecimal iou; private BigDecimal iou;
private String memo;
private Boolean deleted; private Boolean deleted;
} }
@@ -118,25 +147,4 @@ public class ModelMngDto {
return PageRequest.of(page, size); return PageRequest.of(page, size);
} }
} }
@CodeExpose
@Getter
@AllArgsConstructor
public enum ModelType implements EnumType {
M1("모델 M1"),
M2("모델 M2"),
M3("모델 M3");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}
} }

View File

@@ -1,8 +1,10 @@
package com.kamco.cd.kamcoback.postgres.core; package com.kamco.cd.kamcoback.postgres.core;
import com.kamco.cd.kamcoback.menu.dto.MenuDto; 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.entity.MenuEntity;
import com.kamco.cd.kamcoback.postgres.repository.menu.MenuRepository; import com.kamco.cd.kamcoback.postgres.repository.menu.MenuRepository;
import java.util.Comparator;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -23,16 +25,50 @@ public class MenuCoreService {
* @param role * @param role
* @return * @return
*/ */
public List<MenuDto.Basic> getFindByRole(String role) { public List<MyMenuDto.Basic> getFindByRole(String role) {
return menuRepository.getFindByRole(role).stream().map(MenuEntity::toDto).toList(); 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 * @return
*/ */
public List<MenuDto.MenuWithRolesDto> getMenuWithRoles() { public List<String> findAllowedMenuUrlsByRole(String role) {
return menuRepository.getFindByMenuWithRoles(); return menuRepository.findAllowedMenuUrlsByRole(role).stream()
.map(MenuEntity::getMenuUrl)
.filter(url -> url != null && !url.isBlank())
.distinct()
.toList();
} }
} }

View File

@@ -2,7 +2,15 @@ package com.kamco.cd.kamcoback.postgres.entity;
import com.kamco.cd.kamcoback.menu.dto.MenuDto; import com.kamco.cd.kamcoback.menu.dto.MenuDto;
import com.kamco.cd.kamcoback.postgres.CommonDateEntity; import com.kamco.cd.kamcoback.postgres.CommonDateEntity;
import jakarta.persistence.*; import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -15,6 +23,7 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "tb_menu") @Table(name = "tb_menu")
public class MenuEntity extends CommonDateEntity { public class MenuEntity extends CommonDateEntity {
@Id @Id
@Column(name = "menu_uid") @Column(name = "menu_uid")
private String menuUid; private String menuUid;
@@ -49,9 +58,6 @@ public class MenuEntity extends CommonDateEntity {
@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL) @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<MenuEntity> children = new ArrayList<>(); private List<MenuEntity> children = new ArrayList<>();
@Column(name = "menu_api_uri")
private String menuApiUri;
public MenuDto.Basic toDto() { public MenuDto.Basic toDto() {
return new MenuDto.Basic( return new MenuDto.Basic(
this.menuUid, this.menuUid,
@@ -65,7 +71,6 @@ public class MenuEntity extends CommonDateEntity {
this.updatedUid, this.updatedUid,
this.children.stream().map(MenuEntity::toDto).toList(), this.children.stream().map(MenuEntity::toDto).toList(),
this.getCreatedDate(), this.getCreatedDate(),
this.getModifiedDate(), this.getModifiedDate());
this.menuApiUri);
} }
} }

View File

@@ -1,7 +1,12 @@
package com.kamco.cd.kamcoback.postgres.entity; package com.kamco.cd.kamcoback.postgres.entity;
import com.kamco.cd.kamcoback.postgres.CommonDateEntity; import com.kamco.cd.kamcoback.postgres.CommonDateEntity;
import jakarta.persistence.*; import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@@ -11,6 +16,7 @@ import lombok.Setter;
@Entity @Entity
@Table(name = "tb_model_mng") @Table(name = "tb_model_mng")
public class ModelMngEntity extends CommonDateEntity { public class ModelMngEntity extends CommonDateEntity {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "model_uid") @Column(name = "model_uid")
@@ -43,6 +49,9 @@ public class ModelMngEntity extends CommonDateEntity {
@Column(name = "file_name") @Column(name = "file_name")
private String fileName; private String fileName;
@Column(name = "memo")
private String memo;
public void deleted() { public void deleted() {
this.deleted = true; this.deleted = true;
} }

View File

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

View File

@@ -9,7 +9,10 @@ import com.kamco.cd.kamcoback.postgres.entity.ModelMngEntity;
import com.querydsl.core.BooleanBuilder; import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Expression; import com.querydsl.core.types.Expression;
import com.querydsl.core.types.Projections; import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.*; import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.NumberPath;
import com.querydsl.core.types.dsl.StringExpression;
import com.querydsl.jpa.impl.JPAQueryFactory; import com.querydsl.jpa.impl.JPAQueryFactory;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
@@ -71,13 +74,12 @@ public class ModelMngRepositoryImpl extends QuerydslRepositorySupport
modelMngEntity.modelType, modelMngEntity.modelType,
Expressions.stringTemplate( Expressions.stringTemplate(
"to_char({0}, 'YYYY-MM-DD')", modelMngEntity.createCompleteDttm), "to_char({0}, 'YYYY-MM-DD')", modelMngEntity.createCompleteDttm),
Expressions.stringTemplate(
"to_char({0}, 'YYYY-MM-DD')", modelMngEntity.recentUseDttm),
roundNumericToPercent(modelResultMetricEntity.f1Score), roundNumericToPercent(modelResultMetricEntity.f1Score),
roundNumericToPercent(modelResultMetricEntity.precision), roundNumericToPercent(modelResultMetricEntity.precision),
roundNumericToPercent(modelResultMetricEntity.recall), roundNumericToPercent(modelResultMetricEntity.recall),
roundNumericToPercent(modelResultMetricEntity.loss), roundNumericToPercent(modelResultMetricEntity.loss),
roundNumericToPercent(modelResultMetricEntity.iou), roundNumericToPercent(modelResultMetricEntity.iou),
modelMngEntity.memo,
modelMngEntity.deleted)) modelMngEntity.deleted))
.from(modelMngEntity) .from(modelMngEntity)
.innerJoin(modelResultMetricEntity) .innerJoin(modelResultMetricEntity)