package com.cloudhandson.vpdbackoffice.service; import com.cloudhandson.vpdbackoffice.domain.audit.AuditEvent; import com.cloudhandson.vpdbackoffice.domain.permission.AppRole; import com.cloudhandson.vpdbackoffice.domain.permission.PermissionRule; import com.cloudhandson.vpdbackoffice.domain.permission.PermissionSet; import com.cloudhandson.vpdbackoffice.domain.permission.PermissionSetCommand; import com.cloudhandson.vpdbackoffice.domain.permission.PermissionView; import com.cloudhandson.vpdbackoffice.domain.permission.RuleCommand; import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn; import com.cloudhandson.vpdbackoffice.mapper.PermissionMapper; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class PermissionService { private static final Set RULE_TYPES = Set.of("ALL", "MY_DEPT", "SELF", "REGION"); private final PermissionMapper permissionMapper; private final ProtectedObjectService protectedObjectService; private final AuditService auditService; public PermissionService( PermissionMapper permissionMapper, ProtectedObjectService protectedObjectService, AuditService auditService ) { this.permissionMapper = permissionMapper; this.protectedObjectService = protectedObjectService; this.auditService = auditService; } public List findRoles() { return permissionMapper.findRoles(); } public List findPermissionViews() { return permissionMapper.findPermissionViews(); } @Transactional public void createRole(String roleName, String description) { if (roleName == null || roleName.isBlank()) { throw new AppException("역할명은 필수입니다."); } long roleId = permissionMapper.nextRoleId(); permissionMapper.insertRole(roleId, roleName.trim(), description); auditService.record(new AuditEvent("ROLE_CREATED", null, null, "SUCCESS", null, null, roleName)); } @Transactional public void deleteRole(long roleId) { int deleted = permissionMapper.deleteRole(roleId); if (deleted == 0) { throw new AppException("삭제할 역할을 찾을 수 없습니다."); } auditService.record(new AuditEvent("ROLE_DELETED", null, null, "SUCCESS", null, null, "roleId=" + roleId)); } @Transactional public PermissionSet savePermissionSet(PermissionSetCommand command) { if (!"SELECT".equalsIgnoreCase(command.action())) { throw new AppException("초기 구현에서는 SELECT 권한만 저장할 수 있습니다."); } AppRole role = permissionMapper.findRole(command.roleId()); if (role == null) { throw new AppException("역할을 찾을 수 없습니다."); } protectedObjectService.assertEnabled(command.objectId()); validateRules(command.rules()); validateVisibleColumns(command.objectId(), command.visibleColumns()); Long existingId = permissionMapper.findPermissionId(command.roleId(), command.objectId()); long permissionId = existingId == null ? permissionMapper.nextPermissionId() : existingId; if (existingId == null) { permissionMapper.insertPermission(permissionId, command.roleId(), command.objectId(), "SELECT"); } else { permissionMapper.updatePermissionAction(permissionId, "SELECT"); } permissionMapper.deleteRules(permissionId); for (RuleCommand rule : command.rules()) { permissionMapper.insertRule(new PermissionRule( permissionMapper.nextRuleId(), permissionId, normalize(rule.ruleType()), clean(rule.ruleValue()) )); } permissionMapper.deleteVisibleColumns(permissionId); if (command.visibleColumns() != null) { for (String columnName : command.visibleColumns()) { permissionMapper.insertVisibleColumn(permissionId, columnName.trim().toUpperCase(Locale.ROOT)); } } auditService.record(new AuditEvent( "PERMISSION_SAVED", null, command.objectId(), "SUCCESS", null, null, "roleId=" + command.roleId() )); return new PermissionSet(permissionId, command.roleId(), command.objectId(), "SELECT", List.of(), List.of()); } @Transactional public void deletePermission(long permissionId) { permissionMapper.deleteRules(permissionId); permissionMapper.deleteVisibleColumns(permissionId); int deleted = permissionMapper.deletePermission(permissionId); if (deleted == 0) { throw new AppException("삭제할 권한을 찾을 수 없습니다."); } auditService.record(new AuditEvent("PERMISSION_DELETED", null, null, "SUCCESS", null, null, "permissionId=" + permissionId)); } private void validateRules(List rules) { if (rules == null || rules.isEmpty()) { throw new AppException("행 규칙은 하나 이상 필요합니다."); } Set seen = new HashSet<>(); boolean hasAll = false; for (RuleCommand rule : rules) { String type = normalize(rule.ruleType()); if (!RULE_TYPES.contains(type)) { throw new AppException("허용되지 않은 행 규칙입니다: " + type); } if (!seen.add(type + ":" + clean(rule.ruleValue()))) { throw new AppException("중복된 행 규칙이 있습니다."); } hasAll = hasAll || "ALL".equals(type); if (!"ALL".equals(type) && clean(rule.ruleValue()).isBlank()) { throw new AppException(type + " 규칙에는 값이 필요합니다."); } } if (hasAll && rules.size() > 1) { throw new AppException("ALL 규칙은 다른 규칙과 함께 저장할 수 없습니다."); } } private void validateVisibleColumns(long objectId, List visibleColumns) { if (visibleColumns == null || visibleColumns.isEmpty()) { return; } Set allowed = new HashSet<>(); for (ProtectedColumn column : protectedObjectService.findColumns(objectId)) { allowed.add(column.columnName().toUpperCase(Locale.ROOT)); } for (String columnName : visibleColumns) { if (!allowed.contains(columnName.trim().toUpperCase(Locale.ROOT))) { throw new AppException("등록되지 않은 컬럼입니다: " + columnName); } } } private String normalize(String value) { return clean(value).toUpperCase(Locale.ROOT); } private String clean(String value) { return value == null ? "" : value.trim(); } }