improvement: 가입 계약 확장 (#202)
All checks were successful
SPMS_API/pipeline/head This commit looks good

Reviewed-on: https://git.ipstein.myds.me/SPMS/SPMS_API/pulls/203
This commit is contained in:
김선규 2026-02-25 00:43:59 +00:00
commit 512585e7e7
10 changed files with 1231 additions and 11 deletions

View File

@ -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)
{

View File

@ -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; }
}

View File

@ -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; }
}

View File

@ -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
};
}

View File

@ -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";

View File

@ -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; }
}

File diff suppressed because it is too large Load Diff

View File

@ -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");
}
}
}

View File

@ -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)");

View File

@ -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);
}