using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using bitforum.DTOs.Request; using bitforum.DTOs.Response; using bitforum.Repository; using bitforum.Services; using bitforum.Helpers; using bitforum.Constants; using bitforum.Models.Log; using bitforum.Extensions; namespace bitforum.Controllers.API { [ApiController] [Route("api/[controller]")] [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] // JWT 인증 적용 public class AccountController: ControllerBase { private readonly ILogger _logger; private readonly DefaultDbContext _db; private readonly IMemberRepository _memberRepository; private readonly IMailService _mailService; private readonly IConfigService _configService; private ResultDto _result = new ResultDto(); public AccountController(ILogger logger, DefaultDbContext db, IMemberRepository memberRepository, IMailService mailService, IConfigService configService) { _logger = logger; _db = db; _memberRepository = memberRepository; _mailService = mailService; _configService = configService; } // 회원 조회 [HttpGet("Info/{email}")] public async Task> Info([FromRoute] string email) { try { if (string.IsNullOrEmpty(email)) { throw new Exception("이메일을 입력해주세요."); } var member = await _memberRepository.FindMemberByEmail(email); if (member is null) { throw new Exception("회원 정보를 찾을 수 없습니다."); } _result.Data = member; } catch (Exception e) { _logger.LogError(e, e.Message); _result.Ok = false; _result.Status = StatusCodes.Status400BadRequest; _result.Message = e.Message; } return _result; } // 별명 수정 [HttpPost("change-nickname")] public async Task> ChangeNickname([FromBody] ChangeNicknameDto request) { try { if (!ModelState.IsValid) { _result.Errors = ModelState.GetErrors(); throw new Exception("유효성 검사에 실패했습니다."); } // 중복 여부 확인 var isExist = await _memberRepository.IsExistNickname(request.Name); if (isExist) { throw new Exception("이미 사용중인 별명입니다."); } // 회원 조회 var member = await _memberRepository.FindMemberByID(request.ID); if (member is null) { throw new Exception("회원 정보를 찾을 수 없습니다."); } // 별명 변경 가능 기간 계산 (현재 날짜 - 설정된 변경 기간) var changeNicknameDay = Config.Register.ChangeNicknameDay ?? 0; if (changeNicknameDay > 0) { // 사용자의 마지막 별명 변경 날짜가 설정된 제한일 이내인지 확인 if (member.LastNameChangedAt.HasValue) { int daysSinceLastChange = (int)(DateTime.UtcNow - member.LastNameChangedAt.Value).TotalDays; // 남은 일수 계산 if (daysSinceLastChange < changeNicknameDay) { throw new Exception($"별명 변경은 {changeNicknameDay - daysSinceLastChange}일 후에 가능합니다."); } } } await _db.NameChangeLog.AddAsync(new NameChangeLog { MemberID = member.ID, BeforeName = member.Name, AfterName = request.Name, CreatedAt = DateTime.UtcNow }); member.Name = request.Name; member.LastNameChangedAt = DateTime.UtcNow; await _db.SaveChangesAsync(); _result.Message = "별명이 변경되었습니다."; } catch (Exception e) { _logger.LogError(e, e.Message); _result.Ok = false; _result.Status = StatusCodes.Status400BadRequest; _result.Message = e.Message; } return _result; } // 이메일 변경 [HttpPost("change-email")] public async Task> ChangeEmail([FromBody] ChangeEmailDto request) { try { if (!ModelState.IsValid) { _result.Errors = ModelState.GetErrors(); throw new Exception("유효성 검사에 실패했습니다."); } // 중복 여부 확인 var isExist = await _memberRepository.IsExistEmail(request.Email); if (isExist) { throw new Exception("이미 사용중인 이메일입니다."); } // 회원 조회 var member = await _memberRepository.FindMemberByID(request.ID); if (member is null) { throw new Exception("회원 정보를 찾을 수 없습니다."); } // 이메일 변경 가능 기간 계산 (현재 날짜 - 설정된 변경 기간) var changeEmailDay = Config.Register.ChangeEmailDay ?? 0; if (changeEmailDay > 0) { // 사용자의 마지막 이메일 변경 날짜가 설정된 제한일 이내인지 확인 if (member.LastEmailChangedAt.HasValue) { int daysSinceLastChange = (int)(DateTime.UtcNow - member.LastEmailChangedAt.Value).TotalDays; // 남은 일수 계산 if (daysSinceLastChange < changeEmailDay) { throw new Exception($"별명 변경은 {changeEmailDay - daysSinceLastChange}일 후에 가능합니다."); } } } // 이메일 인증 메일 발송 await _mailService.SendEmailVerifyAsync(request.Email, member, new AdditionalData { Email = member.Email }); _result.Message = "이메일 확인을 위해 인증 메일이 전송되었습니다."; } catch (Exception e) { _logger.LogError(e, e.Message); _result.Ok = false; _result.Status = StatusCodes.Status400BadRequest; _result.Message = e.Message; } return _result; } // 비밀번호 변경 [HttpPost("change-password")] public async Task> ChangePassword([FromBody] ChangePasswordDto request) { try { if (!ModelState.IsValid) { _result.Errors = ModelState.GetErrors(); throw new Exception("유효성 검사에 실패하였습니다."); } var memberID = User.GetID(); // 회원 확인 var isExist = await _memberRepository.IsExistID(memberID); if (!isExist) { throw new Exception("존재하지 않는 회원입니다."); } // 회원 조회 var member = await _memberRepository.FindMemberByID(memberID); if (member is null) { throw new Exception("회원 정보를 찾을 수 없습니다."); } // 현재 비밀번호가 맞는지 확인 var isValidPassword = BCrypt.Net.BCrypt.Verify(request.CurrentPassword, member.Password); if (!isValidPassword) { throw new Exception("현재 비밀번호가 일치하지 않습니다."); } var newPassword = BCrypt.Net.BCrypt.HashPassword(request.NewPassword); if (member.Password == newPassword) { throw new Exception("기존 비밀번호와 동일합니다."); } var isValidPolicy = _configService.IsPasswordPolicyValid(request.ConfirmPassword); if (!isValidPolicy) { string message = ""; int minLengthConfig = Config.Register.PasswordMinLength ?? 6; int uppercaseLengthConfig = Config.Register.PasswordUppercaseLength ?? 0; int numbersLengthConfig = Config.Register.PasswordNumbersLength ?? 0; int specialCharsLengthConfig = Config.Register.PasswordSpecialcharsLength ?? 0; // 위에 데이터로 안내 문구 만들어줘 if (minLengthConfig > 0) { message += $"최소 {minLengthConfig}자 이상, "; } if (uppercaseLengthConfig > 0) { message += $"대문자 {uppercaseLengthConfig}자 이상, "; } if (numbersLengthConfig > 0) { message += $"숫자 {numbersLengthConfig}자 이상, "; } if (specialCharsLengthConfig > 0) { message += $"특수문자 {specialCharsLengthConfig}자 이상, "; } message = message.TrimEnd(',', ' '); throw new Exception($"입력하신 비밀번호는 사용하실 수 없습니다. 다음 조건을 충족해주세요.\n\n{message}"); } member.Password = newPassword; member.PasswordUpdatedAt = DateTime.UtcNow; await _db.SaveChangesAsync(); await _mailService.SendChangedPasswordEmailAsync(member); _result.Message = "비밀번호가 변경되었습니다."; } catch (Exception e) { _logger.LogError(e, e.Message); _result.Ok = false; _result.Status = StatusCodes.Status400BadRequest; _result.Message = e.Message; } return _result; } // 회원탈퇴 [HttpPost("withdraw")] public async Task> Withdraw() { try { var memberID = User.GetID(); // 회원 확인 var isExist = await _memberRepository.IsExistID(memberID); if (!isExist) { throw new Exception("존재하지 않는 회원입니다."); } // 회원 조회 var member = await _memberRepository.FindMemberByID(memberID); if (member is null) { throw new Exception("회원 정보를 찾을 수 없습니다."); } if (member.IsWithdraw) { throw new Exception("이미 탈퇴한 회원입니다."); } member.IsWithdraw = true; member.DeletedAt = DateTime.UtcNow; // 탈퇴 완료 메일 발송 await _mailService.SendWithdrawEmailAsync(member); // 로그인 강제 로그아웃(쿠키 삭제) Response.Cookies.Delete("accessToken"); Response.Cookies.Delete("refreshToken"); _result.Message = "정상적으로 탈퇴되었습니다. 이용해 주셔서 감사합니다."; } catch (Exception e) { _logger.LogError(e, e.Message); _result.Ok = false; _result.Status = StatusCodes.Status400BadRequest; _result.Message = e.Message; } return _result; } // 로그인 기록 [HttpGet("login-log")] public async Task> LoginLog([FromQuery] int page = 1, [FromQuery] string type = LoginLogType.Today) { try { var memberID = User.GetID(); // 회원 확인 var isExist = await _memberRepository.IsExistID(memberID); if (!isExist) { throw new Exception("존재하지 않는 회원입니다."); } if (!LoginLogType.ValidTypes.Contains(type)) { throw new ArgumentException("유효하지 않은 타입입니다."); } DateTime now = DateTime.UtcNow; DateTime startDate = now.Date; DateTime endOfDate = type switch { LoginLogType.Today => now.Date.AddDays(1).AddTicks(-1), LoginLogType.Week => now.AddDays(-7), LoginLogType.Month => now.AddMonths(-1), LoginLogType.HalfYear => now.AddMonths(-6), _ => DateTime.MinValue // 전체 조회 }; var query = _db.LoginLog.Where(c => c.MemberID == memberID && c.CreatedAt >= startDate && c.CreatedAt <= endOfDate) .Select(c => new { c.ID, c.MemberID, c.Success, c.IpAddress, c.UserAgent, CreatedAt = c.CreatedAt.ToLocalTime() }) .OrderByDescending(c => c.ID) .Skip(page * 10) .Take(10); var logs = await query.ToListAsync(); if (logs.Count == 0) { throw new Exception("로그인 기록이 없습니다."); } _result.Data = logs; } catch (Exception e) { _logger.LogError(e, e.Message); _result.Ok = false; _result.Status = StatusCodes.Status400BadRequest; _result.Message = e.Message; } return _result; } } }