# 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("로그인")