improvement: 마이페이지 조회 확장 (#249) #250

Merged
seonkyu.kim merged 1 commits from improvement/#249-mypage-profile-extension into develop 2026-02-26 01:03:24 +00:00
12 changed files with 1420 additions and 2 deletions

View File

@ -53,4 +53,20 @@ public class ProfileController : ControllerBase
var result = await _authService.UpdateProfileAsync(adminId, request);
return Ok(ApiResponse<ProfileResponseDto>.Success(result));
}
[HttpPost("activity/list")]
[SwaggerOperation(
Summary = "활동 내역 조회",
Description = "현재 로그인된 관리자의 활동 내역을 페이징 조회합니다.")]
[SwaggerResponse(200, "조회 성공", typeof(ApiResponse<ActivityListResponseDto>))]
[SwaggerResponse(401, "인증되지 않은 요청")]
public async Task<IActionResult> GetActivityListAsync([FromBody] ActivityListRequestDto request)
{
var adminIdClaim = User.FindFirst("adminId")?.Value;
if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId))
throw SpmsException.Unauthorized("인증 정보가 유효하지 않습니다.");
var result = await _authService.GetActivityListAsync(adminId, request);
return Ok(ApiResponse<ActivityListResponseDto>.Success(result));
}
}

View File

@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace SPMS.Application.DTOs.Account;
public class ActivityListRequestDto
{
[JsonPropertyName("page")]
public int Page { get; set; } = 1;
[JsonPropertyName("size")]
public int Size { get; set; } = 10;
[JsonPropertyName("from")]
public DateTime? From { get; set; }
[JsonPropertyName("to")]
public DateTime? To { get; set; }
}

View File

@ -0,0 +1,31 @@
using System.Text.Json.Serialization;
using SPMS.Application.DTOs.Notice;
namespace SPMS.Application.DTOs.Account;
public class ActivityListResponseDto
{
[JsonPropertyName("items")]
public List<ActivityItemDto> Items { get; set; } = new();
[JsonPropertyName("pagination")]
public PaginationDto Pagination { get; set; } = new();
}
public class ActivityItemDto
{
[JsonPropertyName("activity_type")]
public string ActivityType { get; set; } = string.Empty;
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
[JsonPropertyName("description")]
public string? Description { get; set; }
[JsonPropertyName("ip_address")]
public string? IpAddress { get; set; }
[JsonPropertyName("occurred_at")]
public DateTime OccurredAt { get; set; }
}

View File

@ -21,4 +21,10 @@ public class ProfileResponseDto
[JsonPropertyName("created_at")]
public DateTime CreatedAt { get; set; }
[JsonPropertyName("last_login_at")]
public DateTime? LastLoginAt { get; set; }
[JsonPropertyName("organization")]
public string? Organization { get; set; }
}

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
namespace SPMS.Application.DTOs.Account;
@ -10,4 +11,8 @@ public class UpdateProfileRequestDto
[Phone(ErrorMessage = "올바른 전화번호 형식이 아닙니다.")]
[StringLength(20, ErrorMessage = "전화번호는 20자 이내여야 합니다.")]
public string? Phone { get; set; }
[JsonPropertyName("organization")]
[StringLength(100, ErrorMessage = "소속은 100자 이내여야 합니다.")]
public string? Organization { get; set; }
}

View File

@ -18,4 +18,5 @@ public interface IAuthService
Task IssueTempPasswordAsync(TempPasswordRequestDto request);
Task<ProfileResponseDto> GetProfileAsync(long adminId);
Task<ProfileResponseDto> UpdateProfileAsync(long adminId, UpdateProfileRequestDto request);
Task<ActivityListResponseDto> GetActivityListAsync(long adminId, ActivityListRequestDto request);
}

View File

@ -21,6 +21,7 @@ public class AuthService : IAuthService
private readonly JwtSettings _jwtSettings;
private readonly ITokenStore _tokenStore;
private readonly IEmailService _emailService;
private readonly IRepository<SystemLog> _systemLogRepository;
private readonly ILogger<AuthService> _logger;
public AuthService(
@ -30,6 +31,7 @@ public class AuthService : IAuthService
IOptions<JwtSettings> jwtSettings,
ITokenStore tokenStore,
IEmailService emailService,
IRepository<SystemLog> systemLogRepository,
ILogger<AuthService> logger)
{
_adminRepository = adminRepository;
@ -38,6 +40,7 @@ public class AuthService : IAuthService
_jwtSettings = jwtSettings.Value;
_tokenStore = tokenStore;
_emailService = emailService;
_systemLogRepository = systemLogRepository;
_logger = logger;
}
@ -610,7 +613,9 @@ public class AuthService : IAuthService
Name = admin.Name,
Phone = admin.Phone,
Role = (int)admin.Role,
CreatedAt = admin.CreatedAt
CreatedAt = admin.CreatedAt,
LastLoginAt = admin.LastLoginAt,
Organization = admin.Organization
};
}
@ -639,6 +644,12 @@ public class AuthService : IAuthService
hasChange = true;
}
if (request.Organization is not null && request.Organization != admin.Organization)
{
admin.Organization = request.Organization;
hasChange = true;
}
if (!hasChange)
{
throw new SpmsException(
@ -657,7 +668,48 @@ public class AuthService : IAuthService
Name = admin.Name,
Phone = admin.Phone,
Role = (int)admin.Role,
CreatedAt = admin.CreatedAt
CreatedAt = admin.CreatedAt,
LastLoginAt = admin.LastLoginAt,
Organization = admin.Organization
};
}
public async Task<ActivityListResponseDto> GetActivityListAsync(long adminId, ActivityListRequestDto request)
{
var page = request.Page > 0 ? request.Page : 1;
var size = request.Size > 0 ? request.Size : 10;
// 기간 필터 + AdminId 조건 조합
System.Linq.Expressions.Expression<Func<SystemLog, bool>> predicate = log =>
log.AdminId == adminId
&& (request.From == null || log.CreatedAt >= request.From.Value)
&& (request.To == null || log.CreatedAt <= request.To.Value);
var (items, totalCount) = await _systemLogRepository.GetPagedAsync(
page, size,
predicate,
log => log.CreatedAt,
descending: true);
var totalPages = (int)Math.Ceiling((double)totalCount / size);
return new ActivityListResponseDto
{
Items = items.Select(log => new ActivityItemDto
{
ActivityType = log.Action,
Title = log.TargetType ?? log.Action,
Description = log.Details,
IpAddress = log.IpAddress,
OccurredAt = log.CreatedAt
}).ToList(),
Pagination = new DTOs.Notice.PaginationDto
{
Page = page,
Size = size,
TotalCount = totalCount,
TotalPages = totalPages
}
};
}
}

View File

@ -23,4 +23,5 @@ public class Admin : BaseEntity
public DateTime AgreedAt { get; set; }
public bool MustChangePassword { get; set; }
public DateTime? TempPasswordIssuedAt { get; set; }
public string? Organization { get; set; }
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SPMS.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddOrganizationToAdmin : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Organization",
table: "Admin",
type: "varchar(100)",
maxLength: 100,
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Organization",
table: "Admin");
}
}
}

View File

@ -79,6 +79,10 @@ namespace SPMS.Infrastructure.Migrations
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<string>("Organization")
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("Password")
.IsRequired()
.HasMaxLength(64)

View File

@ -34,6 +34,7 @@ public class AdminConfiguration : IEntityTypeConfiguration<Admin>
builder.Property(e => e.AgreeTerms).HasColumnType("tinyint(1)").IsRequired();
builder.Property(e => e.AgreePrivacy).HasColumnType("tinyint(1)").IsRequired();
builder.Property(e => e.AgreedAt).IsRequired();
builder.Property(e => e.Organization).HasMaxLength(100);
builder.HasQueryFilter(e => !e.IsDeleted);
}