improvement: 가입 계약 확장 — 동의 필드/세션/메일 발송 안정화 (#202)
- Admin 엔티티에 AgreeTerms, AgreePrivacy, AgreedAt 필드 추가 - SignupRequestDto에 동의 필드 추가 (필수 검증) - SignupResponseDto에 verifySessionId, emailSent 응답 추가 - AuthService.SignupAsync: 동의 검증, verify session 생성, 메일 발송 try-catch - ErrorCodes에 TermsNotAgreed(114), PrivacyNotAgreed(115) 추가 - EF Core 마이그레이션 AddConsentFieldsToAdmin 생성/적용 Closes #202
This commit is contained in:
parent
10460b40c3
commit
8224c7a17b
|
|
@ -24,9 +24,9 @@ public class AuthController : ControllerBase
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[SwaggerOperation(
|
[SwaggerOperation(
|
||||||
Summary = "회원가입",
|
Summary = "회원가입",
|
||||||
Description = "새로운 관리자 계정을 생성합니다. 이메일 인증이 필요합니다.")]
|
Description = "새로운 관리자 계정을 생성합니다. 약관/개인정보 동의(agreeTerms, agreePrivacy)가 필수이며, 성공 시 이메일 인증 세션(verifySessionId)과 메일 발송 결과(emailSent)를 반환합니다.")]
|
||||||
[SwaggerResponse(200, "회원가입 성공", typeof(ApiResponse<SignupResponseDto>))]
|
[SwaggerResponse(200, "회원가입 성공", typeof(ApiResponse<SignupResponseDto>))]
|
||||||
[SwaggerResponse(400, "잘못된 요청")]
|
[SwaggerResponse(400, "잘못된 요청 또는 동의 미체크")]
|
||||||
[SwaggerResponse(409, "이미 사용 중인 이메일")]
|
[SwaggerResponse(409, "이미 사용 중인 이메일")]
|
||||||
public async Task<IActionResult> SignupAsync([FromBody] SignupRequestDto request)
|
public async Task<IActionResult> SignupAsync([FromBody] SignupRequestDto request)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -19,4 +19,10 @@ public class SignupRequestDto
|
||||||
[Required(ErrorMessage = "전화번호는 필수입니다.")]
|
[Required(ErrorMessage = "전화번호는 필수입니다.")]
|
||||||
[StringLength(20, ErrorMessage = "전화번호는 20자 이내여야 합니다.")]
|
[StringLength(20, ErrorMessage = "전화번호는 20자 이내여야 합니다.")]
|
||||||
public string Phone { get; set; } = string.Empty;
|
public string Phone { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "서비스 이용약관 동의는 필수입니다.")]
|
||||||
|
public bool AgreeTerms { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "개인정보 처리방침 동의는 필수입니다.")]
|
||||||
|
public bool AgreePrivacy { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,10 @@ public class SignupResponseDto
|
||||||
|
|
||||||
[JsonPropertyName("email")]
|
[JsonPropertyName("email")]
|
||||||
public string Email { get; set; } = string.Empty;
|
public string Email { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("verify_session_id")]
|
||||||
|
public string? VerifySessionId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("email_sent")]
|
||||||
|
public bool EmailSent { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,13 @@ public class AuthService : IAuthService
|
||||||
|
|
||||||
public async Task<SignupResponseDto> SignupAsync(SignupRequestDto request)
|
public async Task<SignupResponseDto> SignupAsync(SignupRequestDto request)
|
||||||
{
|
{
|
||||||
// 1. 이메일 중복 검사
|
// 1. 동의 검증
|
||||||
|
if (!request.AgreeTerms)
|
||||||
|
throw new SpmsException(ErrorCodes.TermsNotAgreed, "서비스 이용약관에 동의해야 합니다.", 400);
|
||||||
|
if (!request.AgreePrivacy)
|
||||||
|
throw new SpmsException(ErrorCodes.PrivacyNotAgreed, "개인정보 처리방침에 동의해야 합니다.", 400);
|
||||||
|
|
||||||
|
// 2. 이메일 중복 검사
|
||||||
if (await _adminRepository.EmailExistsAsync(request.Email))
|
if (await _adminRepository.EmailExistsAsync(request.Email))
|
||||||
{
|
{
|
||||||
throw new SpmsException(
|
throw new SpmsException(
|
||||||
|
|
@ -47,10 +53,10 @@ public class AuthService : IAuthService
|
||||||
409);
|
409);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. AdminCode 생성 (UUID 12자)
|
// 3. AdminCode 생성 (UUID 12자)
|
||||||
var adminCode = Guid.NewGuid().ToString("N")[..12].ToUpper();
|
var adminCode = Guid.NewGuid().ToString("N")[..12].ToUpper();
|
||||||
|
|
||||||
// 3. Admin 엔티티 생성
|
// 4. Admin 엔티티 생성 (동의 필드 포함)
|
||||||
var admin = new Admin
|
var admin = new Admin
|
||||||
{
|
{
|
||||||
AdminCode = adminCode,
|
AdminCode = adminCode,
|
||||||
|
|
@ -61,26 +67,48 @@ public class AuthService : IAuthService
|
||||||
Role = AdminRole.User,
|
Role = AdminRole.User,
|
||||||
EmailVerified = false,
|
EmailVerified = false,
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
IsDeleted = false
|
IsDeleted = false,
|
||||||
|
AgreeTerms = true,
|
||||||
|
AgreePrivacy = true,
|
||||||
|
AgreedAt = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
|
|
||||||
// 4. 저장
|
// 5. 저장
|
||||||
await _adminRepository.AddAsync(admin);
|
await _adminRepository.AddAsync(admin);
|
||||||
await _unitOfWork.SaveChangesAsync();
|
await _unitOfWork.SaveChangesAsync();
|
||||||
|
|
||||||
// 5. 이메일 인증 코드 생성/저장/발송
|
// 6. 이메일 인증 코드 생성/저장
|
||||||
var verificationCode = Random.Shared.Next(100000, 999999).ToString();
|
var verificationCode = Random.Shared.Next(100000, 999999).ToString();
|
||||||
await _tokenStore.StoreAsync(
|
await _tokenStore.StoreAsync(
|
||||||
$"email_verify:{request.Email}",
|
$"email_verify:{request.Email}",
|
||||||
verificationCode,
|
verificationCode,
|
||||||
TimeSpan.FromHours(1));
|
TimeSpan.FromHours(1));
|
||||||
await _emailService.SendVerificationCodeAsync(request.Email, verificationCode);
|
|
||||||
|
|
||||||
// 6. 응답 반환
|
// 7. Verify Session 생성 (sessionId → email 매핑)
|
||||||
|
var verifySessionId = Guid.NewGuid().ToString("N");
|
||||||
|
await _tokenStore.StoreAsync(
|
||||||
|
$"verify_session:{verifySessionId}",
|
||||||
|
request.Email,
|
||||||
|
TimeSpan.FromHours(1));
|
||||||
|
|
||||||
|
// 8. 메일 발송 (실패해도 가입은 유지)
|
||||||
|
var emailSent = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _emailService.SendVerificationCodeAsync(request.Email, verificationCode);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
emailSent = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. 응답 반환
|
||||||
return new SignupResponseDto
|
return new SignupResponseDto
|
||||||
{
|
{
|
||||||
AdminCode = admin.AdminCode,
|
AdminCode = admin.AdminCode,
|
||||||
Email = admin.Email
|
Email = admin.Email,
|
||||||
|
VerifySessionId = verifySessionId,
|
||||||
|
EmailSent = emailSent
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ public static class ErrorCodes
|
||||||
public const string VerificationCodeError = "111";
|
public const string VerificationCodeError = "111";
|
||||||
public const string LoginFailed = "112";
|
public const string LoginFailed = "112";
|
||||||
public const string LoginAttemptExceeded = "113";
|
public const string LoginAttemptExceeded = "113";
|
||||||
|
public const string TermsNotAgreed = "114";
|
||||||
|
public const string PrivacyNotAgreed = "115";
|
||||||
|
|
||||||
// === Account (2) ===
|
// === Account (2) ===
|
||||||
public const string PasswordValidationFailed = "121";
|
public const string PasswordValidationFailed = "121";
|
||||||
|
|
|
||||||
|
|
@ -18,4 +18,7 @@ public class Admin : BaseEntity
|
||||||
public DateTime? RefreshTokenExpiresAt { get; set; }
|
public DateTime? RefreshTokenExpiresAt { get; set; }
|
||||||
public bool IsDeleted { get; set; }
|
public bool IsDeleted { get; set; }
|
||||||
public DateTime? DeletedAt { get; set; }
|
public DateTime? DeletedAt { get; set; }
|
||||||
|
public bool AgreeTerms { get; set; }
|
||||||
|
public bool AgreePrivacy { get; set; }
|
||||||
|
public DateTime AgreedAt { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1111
SPMS.Infrastructure/Migrations/20260225002821_AddConsentFieldsToAdmin.Designer.cs
generated
Normal file
1111
SPMS.Infrastructure/Migrations/20260225002821_AddConsentFieldsToAdmin.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,52 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SPMS.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddConsentFieldsToAdmin : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "AgreePrivacy",
|
||||||
|
table: "Admin",
|
||||||
|
type: "tinyint(1)",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "AgreeTerms",
|
||||||
|
table: "Admin",
|
||||||
|
type: "tinyint(1)",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "AgreedAt",
|
||||||
|
table: "Admin",
|
||||||
|
type: "datetime(6)",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AgreePrivacy",
|
||||||
|
table: "Admin");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AgreeTerms",
|
||||||
|
table: "Admin");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AgreedAt",
|
||||||
|
table: "Admin");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -35,6 +35,15 @@ namespace SPMS.Infrastructure.Migrations
|
||||||
.HasMaxLength(8)
|
.HasMaxLength(8)
|
||||||
.HasColumnType("varchar(8)");
|
.HasColumnType("varchar(8)");
|
||||||
|
|
||||||
|
b.Property<bool>("AgreePrivacy")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("AgreeTerms")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("AgreedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
b.Property<DateTime>("CreatedAt")
|
||||||
.HasColumnType("datetime(6)");
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,9 @@ public class AdminConfiguration : IEntityTypeConfiguration<Admin>
|
||||||
builder.Property(e => e.RefreshTokenExpiresAt);
|
builder.Property(e => e.RefreshTokenExpiresAt);
|
||||||
builder.Property(e => e.IsDeleted).HasColumnType("tinyint(1)").IsRequired().HasDefaultValue(false);
|
builder.Property(e => e.IsDeleted).HasColumnType("tinyint(1)").IsRequired().HasDefaultValue(false);
|
||||||
builder.Property(e => e.DeletedAt);
|
builder.Property(e => e.DeletedAt);
|
||||||
|
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.HasQueryFilter(e => !e.IsDeleted);
|
builder.HasQueryFilter(e => !e.IsDeleted);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user