improvement: 가입 계약 확장 (#202) #203
|
|
@ -24,9 +24,9 @@ public class AuthController : ControllerBase
|
|||
[AllowAnonymous]
|
||||
[SwaggerOperation(
|
||||
Summary = "회원가입",
|
||||
Description = "새로운 관리자 계정을 생성합니다. 이메일 인증이 필요합니다.")]
|
||||
Description = "새로운 관리자 계정을 생성합니다. 약관/개인정보 동의(agreeTerms, agreePrivacy)가 필수이며, 성공 시 이메일 인증 세션(verifySessionId)과 메일 발송 결과(emailSent)를 반환합니다.")]
|
||||
[SwaggerResponse(200, "회원가입 성공", typeof(ApiResponse<SignupResponseDto>))]
|
||||
[SwaggerResponse(400, "잘못된 요청")]
|
||||
[SwaggerResponse(400, "잘못된 요청 또는 동의 미체크")]
|
||||
[SwaggerResponse(409, "이미 사용 중인 이메일")]
|
||||
public async Task<IActionResult> SignupAsync([FromBody] SignupRequestDto request)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -19,4 +19,10 @@ public class SignupRequestDto
|
|||
[Required(ErrorMessage = "전화번호는 필수입니다.")]
|
||||
[StringLength(20, ErrorMessage = "전화번호는 20자 이내여야 합니다.")]
|
||||
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")]
|
||||
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)
|
||||
{
|
||||
// 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))
|
||||
{
|
||||
throw new SpmsException(
|
||||
|
|
@ -47,10 +53,10 @@ public class AuthService : IAuthService
|
|||
409);
|
||||
}
|
||||
|
||||
// 2. AdminCode 생성 (UUID 12자)
|
||||
// 3. AdminCode 생성 (UUID 12자)
|
||||
var adminCode = Guid.NewGuid().ToString("N")[..12].ToUpper();
|
||||
|
||||
// 3. Admin 엔티티 생성
|
||||
// 4. Admin 엔티티 생성 (동의 필드 포함)
|
||||
var admin = new Admin
|
||||
{
|
||||
AdminCode = adminCode,
|
||||
|
|
@ -61,26 +67,48 @@ public class AuthService : IAuthService
|
|||
Role = AdminRole.User,
|
||||
EmailVerified = false,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
IsDeleted = false
|
||||
IsDeleted = false,
|
||||
AgreeTerms = true,
|
||||
AgreePrivacy = true,
|
||||
AgreedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// 4. 저장
|
||||
// 5. 저장
|
||||
await _adminRepository.AddAsync(admin);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
// 5. 이메일 인증 코드 생성/저장/발송
|
||||
// 6. 이메일 인증 코드 생성/저장
|
||||
var verificationCode = Random.Shared.Next(100000, 999999).ToString();
|
||||
await _tokenStore.StoreAsync(
|
||||
$"email_verify:{request.Email}",
|
||||
verificationCode,
|
||||
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
|
||||
{
|
||||
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 LoginFailed = "112";
|
||||
public const string LoginAttemptExceeded = "113";
|
||||
public const string TermsNotAgreed = "114";
|
||||
public const string PrivacyNotAgreed = "115";
|
||||
|
||||
// === Account (2) ===
|
||||
public const string PasswordValidationFailed = "121";
|
||||
|
|
|
|||
|
|
@ -18,4 +18,7 @@ public class Admin : BaseEntity
|
|||
public DateTime? RefreshTokenExpiresAt { get; set; }
|
||||
public bool IsDeleted { 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)
|
||||
.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")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ public class AdminConfiguration : IEntityTypeConfiguration<Admin>
|
|||
builder.Property(e => e.RefreshTokenExpiresAt);
|
||||
builder.Property(e => e.IsDeleted).HasColumnType("tinyint(1)").IsRequired().HasDefaultValue(false);
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user