fix: 미인증 계정 재가입 허용 및 로그인 화면 인증 메일 재발송

인증 미완료 상태에서 재가입/로그인이 모두 막혀 데드락되던 문제 해결.
- signup: 인증 완료 계정만 409로 거부하고, 미인증 로컬 계정은 입력값으로 갱신 후 인증 메일 재발송(중복 계정 생성 안 함)
- 로그인 화면: 미인증(403) 응답 시 '인증 메일 다시 보내기' 인라인 액션 노출

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 13:42:34 +09:00
parent afed0d087a
commit 5e742e5c8b
3 changed files with 114 additions and 5 deletions
+19 -4
View File
@@ -59,11 +59,13 @@ export class AuthService {
private readonly refreshRepo: Repository<RefreshSession>,
) {}
// 회원가입 — 이메일 중복 검사 후 미인증 계정 생성 + 인증 메일 발송(자동 로그인 X)
// 회원가입 — 인증 완료 계정만 중복 거부. 미인증(가입 대기) 계정은 재가입을 허용한다.
// (인증 메일을 받기 전 사이트를 닫아 가입을 못 끝낸 경우, 같은 이메일로 다시 가입해
// 새 인증 메일을 받아 가입을 완료할 수 있게 한다. 자동 로그인은 하지 않음)
async signup(dto: SignupDto): Promise<SignupResult> {
const exists = await this.userService.findByEmail(dto.email);
if (exists) {
// 409 + 비즈니스 코드/문구 명시 → 필터가 그대로 노출
const existing = await this.userService.findByEmail(dto.email);
// 이미 인증을 마친 계정(로컬·소셜)은 재가입 불가
if (existing && (existing.emailVerified || existing.provider !== 'local')) {
throw new BusinessException(
'BIZ_001',
'이미 가입된 이메일입니다.',
@@ -71,6 +73,19 @@ export class AuthService {
);
}
const passwordHash = await bcrypt.hash(dto.password, BCRYPT_ROUNDS);
// 미인증 로컬 계정이 있으면 입력값으로 갱신 후 인증 메일만 재발송(중복 계정 생성 X)
if (existing) {
await this.userService.updateRegistration(existing.id, {
passwordHash,
name: dto.name,
role: dto.role ?? null,
});
existing.name = dto.name; // 메일 인사말에 최신 이름 반영
await this.sendVerification(existing);
return { email: existing.email, verificationPending: true };
}
const user = await this.userService.create({
email: dto.email,
passwordHash,
+13
View File
@@ -113,6 +113,19 @@ export class UserService implements OnApplicationBootstrap {
return this.userRepository.save(user);
}
// 미인증(가입 대기) 로컬 계정 재가입 — 비밀번호/이름/역할을 새 입력으로 갱신.
// (인증 완료 전 계정에 한해 호출; 직후 인증 메일을 재발송한다)
async updateRegistration(
userId: string,
payload: { passwordHash: string; name: string; role?: string | null },
): Promise<void> {
await this.userRepository.update(userId, {
passwordHash: payload.passwordHash,
name: payload.name,
role: payload.role ?? null,
});
}
// 소셜(OAuth) 사용자 생성 — 비밀번호 없음, 제공자가 이메일을 검증하므로 인증 완료 상태.
createOAuthUser(payload: {
email: string;