116 lines
4.6 KiB
Python
116 lines
4.6 KiB
Python
# backend/forms/auth_forms.py (refactor)
|
||
from __future__ import annotations
|
||
import re
|
||
import unicodedata
|
||
from flask_wtf import FlaskForm
|
||
from wtforms import StringField, PasswordField, SubmitField, BooleanField
|
||
from wtforms.validators import (
|
||
DataRequired, Length, Email, EqualTo, ValidationError, Regexp
|
||
)
|
||
from backend.models.user import User
|
||
|
||
# ─────────────────────────────────────────────────────────────
|
||
# 공통 필터/유틸
|
||
def strip_filter(x: str | None) -> str | None:
|
||
return x.strip() if isinstance(x, str) else x
|
||
|
||
def lower_strip(x: str | None) -> str | None:
|
||
return x.strip().lower() if isinstance(x, str) else x
|
||
|
||
def nfc_korean(x: str | None) -> str | None:
|
||
if not isinstance(x, str):
|
||
return x
|
||
# 한글 이름 등 유니코드 정규화 (NFC)
|
||
return unicodedata.normalize("NFC", x.strip())
|
||
|
||
# 비밀번호 정책: 8~64자, 대문자/소문자/숫자/특수문자 각 1개 이상
|
||
password_policy_validators = [
|
||
Length(min=8, max=64, message="비밀번호는 8~64자여야 합니다."),
|
||
Regexp(r".*[A-Z].*", message="비밀번호에 대문자가 1자 이상 포함되어야 합니다."),
|
||
Regexp(r".*[a-z].*", message="비밀번호에 소문자가 1자 이상 포함되어야 합니다."),
|
||
Regexp(r".*\d.*", message="비밀번호에 숫자가 1자 이상 포함되어야 합니다."),
|
||
Regexp(r".*[^A-Za-z0-9].*", message="비밀번호에 특수문자가 1자 이상 포함되어야 합니다."),
|
||
]
|
||
|
||
# ─────────────────────────────────────────────────────────────
|
||
|
||
class RegistrationForm(FlaskForm):
|
||
username = StringField(
|
||
"이름",
|
||
filters=[nfc_korean],
|
||
validators=[
|
||
DataRequired(message="이름을 입력해주세요."),
|
||
Length(min=2, max=20, message="이름은 2~20자 사이여야 합니다."),
|
||
],
|
||
render_kw={
|
||
"placeholder": "이름 (한글만 허용)",
|
||
"autocomplete": "name",
|
||
"autocapitalize": "off",
|
||
"autocorrect": "off",
|
||
"spellcheck": "false",
|
||
},
|
||
)
|
||
|
||
email = StringField(
|
||
"이메일",
|
||
filters=[lower_strip],
|
||
validators=[
|
||
DataRequired(message="이메일을 입력해주세요."),
|
||
Email(message="유효한 이메일을 입력하세요."),
|
||
],
|
||
render_kw={
|
||
"placeholder": "예: user@example.com",
|
||
"autocomplete": "email",
|
||
"inputmode": "email",
|
||
},
|
||
)
|
||
|
||
password = PasswordField(
|
||
"비밀번호",
|
||
validators=[DataRequired(message="비밀번호를 입력해주세요."), *password_policy_validators],
|
||
render_kw={"placeholder": "비밀번호", "autocomplete": "new-password"},
|
||
)
|
||
|
||
confirm_password = PasswordField(
|
||
"비밀번호 확인",
|
||
validators=[
|
||
DataRequired(message="비밀번호 확인을 입력해주세요."),
|
||
EqualTo("password", message="비밀번호가 일치하지 않습니다."),
|
||
],
|
||
render_kw={"placeholder": "비밀번호 다시 입력", "autocomplete": "new-password"},
|
||
)
|
||
|
||
submit = SubmitField("회원가입")
|
||
|
||
def validate_username(self, field):
|
||
# 한글만 허용(2~20자) – 기존 로직 유지
|
||
if not re.fullmatch(r"[가-힣]{2,20}", field.data or ""):
|
||
raise ValidationError("이름은 한글로만 2~20자 입력 가능합니다.")
|
||
# 중복 체크
|
||
user = User.query.filter_by(username=field.data).first()
|
||
if user:
|
||
raise ValidationError("이미 사용 중인 이름입니다.")
|
||
|
||
def validate_email(self, field):
|
||
# 이메일은 소문자 비교(필터로 이미 소문자화)
|
||
user = User.query.filter_by(email=field.data).first()
|
||
if user:
|
||
raise ValidationError("이미 등록된 이메일입니다.")
|
||
|
||
|
||
class LoginForm(FlaskForm):
|
||
email = StringField(
|
||
"이메일",
|
||
filters=[lower_strip],
|
||
validators=[DataRequired(message="이메일을 입력해주세요."), Email(message="유효한 이메일을 입력하세요.")],
|
||
render_kw={"placeholder": "이메일 주소", "autocomplete": "username", "inputmode": "email"},
|
||
)
|
||
|
||
password = PasswordField(
|
||
"비밀번호",
|
||
validators=[DataRequired(message="비밀번호를 입력해주세요.")],
|
||
render_kw={"placeholder": "비밀번호", "autocomplete": "current-password"},
|
||
)
|
||
|
||
remember = BooleanField("로그인 유지")
|
||
submit = SubmitField("로그인") |