- POST /v1/in/account/operator/create (계정 생성 + 비밀번호 설정 이메일) - POST /v1/in/account/operator/delete (Soft Delete, 자기 자신 삭제 방지) - POST /v1/in/account/operator/list (페이징 + role/is_active 필터) - POST /v1/in/account/operator/password/reset (비밀번호 초기화 + 세션 무효화) Closes #134
364 lines
11 KiB
C#
364 lines
11 KiB
C#
using System.Linq.Expressions;
|
|
using SPMS.Application.DTOs.Account;
|
|
using SPMS.Application.DTOs.Notice;
|
|
using SPMS.Application.Interfaces;
|
|
using SPMS.Domain.Common;
|
|
using SPMS.Domain.Entities;
|
|
using SPMS.Domain.Enums;
|
|
using SPMS.Domain.Exceptions;
|
|
using SPMS.Domain.Interfaces;
|
|
|
|
namespace SPMS.Application.Services;
|
|
|
|
public class AccountService : IAccountService
|
|
{
|
|
private readonly IAdminRepository _adminRepository;
|
|
private readonly IUnitOfWork _unitOfWork;
|
|
private readonly IEmailService _emailService;
|
|
private readonly ITokenStore _tokenStore;
|
|
|
|
public AccountService(
|
|
IAdminRepository adminRepository,
|
|
IUnitOfWork unitOfWork,
|
|
IEmailService emailService,
|
|
ITokenStore tokenStore)
|
|
{
|
|
_adminRepository = adminRepository;
|
|
_unitOfWork = unitOfWork;
|
|
_emailService = emailService;
|
|
_tokenStore = tokenStore;
|
|
}
|
|
|
|
public async Task<AccountResponseDto> CreateAsync(CreateAccountRequestDto request)
|
|
{
|
|
// 1. 이메일 중복 검사
|
|
if (await _adminRepository.EmailExistsAsync(request.Email))
|
|
{
|
|
throw new SpmsException(
|
|
ErrorCodes.Conflict,
|
|
"이미 사용 중인 이메일입니다.",
|
|
409);
|
|
}
|
|
|
|
// 2. AdminCode 생성 (UUID)
|
|
var adminCode = Guid.NewGuid().ToString("N")[..12].ToUpper();
|
|
|
|
// 3. Admin 엔티티 생성
|
|
var admin = new Admin
|
|
{
|
|
AdminCode = adminCode,
|
|
Email = request.Email,
|
|
Password = BCrypt.Net.BCrypt.HashPassword(request.Password),
|
|
Name = request.Name,
|
|
Phone = request.Phone ?? string.Empty,
|
|
Role = (AdminRole)request.Role,
|
|
EmailVerified = false,
|
|
CreatedAt = DateTime.UtcNow,
|
|
IsDeleted = false
|
|
};
|
|
|
|
// 4. 저장
|
|
await _adminRepository.AddAsync(admin);
|
|
await _unitOfWork.SaveChangesAsync();
|
|
|
|
// 5. 응답 반환
|
|
return MapToDto(admin);
|
|
}
|
|
|
|
public async Task<AccountListResponseDto> GetListAsync(AccountListRequestDto request)
|
|
{
|
|
// 필터 조건 생성 (Super Admin 제외)
|
|
Expression<Func<Admin, bool>> predicate = a => a.Role != AdminRole.Super;
|
|
|
|
// 검색어 필터
|
|
if (!string.IsNullOrWhiteSpace(request.SearchKeyword))
|
|
{
|
|
var keyword = request.SearchKeyword;
|
|
predicate = a => a.Role != AdminRole.Super &&
|
|
(a.Name.Contains(keyword) || a.Email.Contains(keyword));
|
|
}
|
|
|
|
// Role 필터
|
|
if (request.Role.HasValue)
|
|
{
|
|
var role = (AdminRole)request.Role.Value;
|
|
var basePredicate = predicate;
|
|
predicate = a => basePredicate.Compile()(a) && a.Role == role;
|
|
}
|
|
|
|
// 페이징 조회
|
|
var (items, totalCount) = await _adminRepository.GetPagedAsync(
|
|
request.Page,
|
|
request.PageSize,
|
|
predicate,
|
|
a => a.CreatedAt,
|
|
descending: true);
|
|
|
|
return new AccountListResponseDto
|
|
{
|
|
Items = items.Select(MapToDto).ToList(),
|
|
TotalCount = totalCount,
|
|
Page = request.Page,
|
|
PageSize = request.PageSize
|
|
};
|
|
}
|
|
|
|
public async Task<AccountResponseDto> GetByAdminCodeAsync(string adminCode)
|
|
{
|
|
var admin = await _adminRepository.GetByAdminCodeAsync(adminCode);
|
|
if (admin is null)
|
|
{
|
|
throw new SpmsException(
|
|
ErrorCodes.NotFound,
|
|
"운영자를 찾을 수 없습니다.",
|
|
404);
|
|
}
|
|
|
|
// Super Admin 조회 불가
|
|
if (admin.Role == AdminRole.Super)
|
|
{
|
|
throw new SpmsException(
|
|
ErrorCodes.NotFound,
|
|
"운영자를 찾을 수 없습니다.",
|
|
404);
|
|
}
|
|
|
|
return MapToDto(admin);
|
|
}
|
|
|
|
public async Task<AccountResponseDto> UpdateAsync(string adminCode, UpdateAccountRequestDto request)
|
|
{
|
|
var admin = await _adminRepository.GetByAdminCodeAsync(adminCode);
|
|
if (admin is null)
|
|
{
|
|
throw new SpmsException(
|
|
ErrorCodes.NotFound,
|
|
"운영자를 찾을 수 없습니다.",
|
|
404);
|
|
}
|
|
|
|
// Super Admin 수정 불가
|
|
if (admin.Role == AdminRole.Super)
|
|
{
|
|
throw new SpmsException(
|
|
ErrorCodes.Forbidden,
|
|
"Super Admin은 수정할 수 없습니다.",
|
|
403);
|
|
}
|
|
|
|
// 업데이트
|
|
admin.Name = request.Name;
|
|
admin.Phone = request.Phone ?? string.Empty;
|
|
admin.Role = (AdminRole)request.Role;
|
|
|
|
_adminRepository.Update(admin);
|
|
await _unitOfWork.SaveChangesAsync();
|
|
|
|
return MapToDto(admin);
|
|
}
|
|
|
|
public async Task DeleteAsync(string adminCode)
|
|
{
|
|
var admin = await _adminRepository.GetByAdminCodeAsync(adminCode);
|
|
if (admin is null)
|
|
{
|
|
throw new SpmsException(
|
|
ErrorCodes.NotFound,
|
|
"운영자를 찾을 수 없습니다.",
|
|
404);
|
|
}
|
|
|
|
// Super Admin 삭제 불가
|
|
if (admin.Role == AdminRole.Super)
|
|
{
|
|
throw new SpmsException(
|
|
ErrorCodes.Forbidden,
|
|
"Super Admin은 삭제할 수 없습니다.",
|
|
403);
|
|
}
|
|
|
|
// Soft Delete
|
|
admin.IsDeleted = true;
|
|
admin.DeletedAt = DateTime.UtcNow;
|
|
admin.RefreshToken = null;
|
|
admin.RefreshTokenExpiresAt = null;
|
|
|
|
_adminRepository.Update(admin);
|
|
await _unitOfWork.SaveChangesAsync();
|
|
}
|
|
|
|
public async Task<OperatorCreateResponseDto> CreateOperatorAsync(OperatorCreateRequestDto request)
|
|
{
|
|
// 1. 이메일 중복 검사
|
|
if (await _adminRepository.EmailExistsAsync(request.Email))
|
|
{
|
|
throw new SpmsException(
|
|
ErrorCodes.Conflict,
|
|
"이미 사용 중인 이메일입니다.",
|
|
409);
|
|
}
|
|
|
|
// 2. AdminCode 생성
|
|
var adminCode = Guid.NewGuid().ToString("N")[..12].ToUpper();
|
|
|
|
// 3. Admin 엔티티 생성 (비밀번호 없이)
|
|
var admin = new Admin
|
|
{
|
|
AdminCode = adminCode,
|
|
Email = request.Email,
|
|
Password = string.Empty,
|
|
Name = request.Name,
|
|
Phone = request.Phone ?? string.Empty,
|
|
Role = (AdminRole)request.Role,
|
|
EmailVerified = false,
|
|
CreatedAt = DateTime.UtcNow,
|
|
IsDeleted = false
|
|
};
|
|
|
|
// 4. 저장
|
|
await _adminRepository.AddAsync(admin);
|
|
await _unitOfWork.SaveChangesAsync();
|
|
|
|
// 5. 비밀번호 설정 토큰 생성 및 저장
|
|
var token = Guid.NewGuid().ToString("N");
|
|
await _tokenStore.StoreAsync($"password_setup:{request.Email}", token, TimeSpan.FromHours(24));
|
|
|
|
// 6. 이메일 발송
|
|
await _emailService.SendPasswordSetupTokenAsync(request.Email, token);
|
|
|
|
return new OperatorCreateResponseDto
|
|
{
|
|
AdminCode = adminCode,
|
|
Email = request.Email,
|
|
Role = request.Role
|
|
};
|
|
}
|
|
|
|
public async Task DeleteOperatorAsync(string adminCode, long requestingAdminId)
|
|
{
|
|
var admin = await _adminRepository.GetByAdminCodeAsync(adminCode);
|
|
if (admin is null)
|
|
{
|
|
throw new SpmsException(
|
|
ErrorCodes.NotFound,
|
|
"운영자를 찾을 수 없습니다.",
|
|
404);
|
|
}
|
|
|
|
// Super Admin 삭제 불가
|
|
if (admin.Role == AdminRole.Super)
|
|
{
|
|
throw new SpmsException(
|
|
ErrorCodes.Forbidden,
|
|
"Super Admin은 삭제할 수 없습니다.",
|
|
403);
|
|
}
|
|
|
|
// 자기 자신 삭제 불가
|
|
if (admin.Id == requestingAdminId)
|
|
{
|
|
throw new SpmsException(
|
|
ErrorCodes.Forbidden,
|
|
"자기 자신은 삭제할 수 없습니다.",
|
|
403);
|
|
}
|
|
|
|
// Soft Delete
|
|
admin.IsDeleted = true;
|
|
admin.DeletedAt = DateTime.UtcNow;
|
|
admin.RefreshToken = null;
|
|
admin.RefreshTokenExpiresAt = null;
|
|
|
|
_adminRepository.Update(admin);
|
|
await _unitOfWork.SaveChangesAsync();
|
|
}
|
|
|
|
public async Task<OperatorListResponseDto> GetOperatorListAsync(OperatorListRequestDto request)
|
|
{
|
|
AdminRole? roleFilter = request.Role.HasValue ? (AdminRole)request.Role.Value : null;
|
|
|
|
var (items, totalCount) = await _adminRepository.GetOperatorPagedAsync(
|
|
request.Page, request.Size, roleFilter, request.IsActive);
|
|
|
|
var totalPages = (int)Math.Ceiling((double)totalCount / request.Size);
|
|
|
|
return new OperatorListResponseDto
|
|
{
|
|
Items = items.Select(a => new OperatorItemDto
|
|
{
|
|
AdminCode = a.AdminCode,
|
|
Email = a.Email,
|
|
Name = a.Name,
|
|
Role = (int)a.Role,
|
|
IsActive = !a.IsDeleted,
|
|
LastLoginAt = a.LastLoginAt,
|
|
CreatedAt = a.CreatedAt
|
|
}).ToList(),
|
|
Pagination = new PaginationDto
|
|
{
|
|
Page = request.Page,
|
|
Size = request.Size,
|
|
TotalCount = totalCount,
|
|
TotalPages = totalPages
|
|
}
|
|
};
|
|
}
|
|
|
|
public async Task<OperatorCreateResponseDto> ResetOperatorPasswordAsync(string adminCode)
|
|
{
|
|
var admin = await _adminRepository.GetByAdminCodeAsync(adminCode);
|
|
if (admin is null)
|
|
{
|
|
throw new SpmsException(
|
|
ErrorCodes.NotFound,
|
|
"운영자를 찾을 수 없습니다.",
|
|
404);
|
|
}
|
|
|
|
// Super Admin 비밀번호 초기화 불가
|
|
if (admin.Role == AdminRole.Super)
|
|
{
|
|
throw new SpmsException(
|
|
ErrorCodes.Forbidden,
|
|
"Super Admin의 비밀번호는 초기화할 수 없습니다.",
|
|
403);
|
|
}
|
|
|
|
// 세션 무효화
|
|
admin.RefreshToken = null;
|
|
admin.RefreshTokenExpiresAt = null;
|
|
|
|
_adminRepository.Update(admin);
|
|
await _unitOfWork.SaveChangesAsync();
|
|
|
|
// 비밀번호 재설정 토큰 생성 및 저장
|
|
var token = Guid.NewGuid().ToString("N");
|
|
await _tokenStore.StoreAsync($"password_reset:{admin.Email}", token, TimeSpan.FromHours(1));
|
|
|
|
// 이메일 발송
|
|
await _emailService.SendPasswordResetTokenAsync(admin.Email, token);
|
|
|
|
return new OperatorCreateResponseDto
|
|
{
|
|
AdminCode = admin.AdminCode,
|
|
Email = admin.Email,
|
|
Role = (int)admin.Role
|
|
};
|
|
}
|
|
|
|
private static AccountResponseDto MapToDto(Admin admin)
|
|
{
|
|
return new AccountResponseDto
|
|
{
|
|
AdminCode = admin.AdminCode,
|
|
Email = admin.Email,
|
|
Name = admin.Name,
|
|
Phone = string.IsNullOrEmpty(admin.Phone) ? null : admin.Phone,
|
|
Role = admin.Role.ToString(),
|
|
EmailVerified = admin.EmailVerified,
|
|
CreatedAt = admin.CreatedAt,
|
|
LastLoginAt = admin.LastLoginAt
|
|
};
|
|
}
|
|
}
|