from django.shortcuts import render, redirect from django.contrib.auth.decorators import login_required from django.contrib import messages from django.contrib.auth import get_user_model from django.http import JsonResponse from .forms import ( ProfileFullEditForm, PasswordChangeStep1Form, PasswordChangeStep2Form, PasswordResetStep1Form, PasswordChangeLoggedInForm, ForcePasswordSetForm, WithdrawalRequestForm ) from B_main.models import Person, WithdrawalRequest from A_core.sms_utils import send_verification_sms from B_main.log_utils import log_profile_update, log_password_change, log_phone_verification, log_withdrawal_request import random import time User = get_user_model() @login_required def profile_edit(request): """프로필 편집 뷰""" # 현재 사용자의 Person 인스턴스 가져오기 try: person = Person.objects.get(user=request.user) except Person.DoesNotExist: # Person 인스턴스가 없으면 새로 생성 person = Person.objects.create(user=request.user) if request.method == 'POST': form = ProfileFullEditForm(request.POST, request.FILES, user=request.user, instance=person) if form.is_valid(): # 변경된 필드와 변경 전/후 값 추적 changed_fields = [] field_changes = {} if form.has_changed(): changed_fields = form.changed_data # 각 변경된 필드의 이전 값과 새 값 기록 for field_name in changed_fields: # 한국어 필드명으로 매핑 field_display_names = { 'keyword1': '검색키워드', '소개글': '소개글', } display_name = field_display_names.get(field_name, field_name) # 이전 값 (form.initial에서 가져오기) old_value = form.initial.get(field_name, '') # 새 값 (cleaned_data에서 가져오기) new_value = form.cleaned_data.get(field_name, '') # 빈 값 처리 if old_value is None: old_value = '' if new_value is None: new_value = '' field_changes[display_name] = { 'old': str(old_value), 'new': str(new_value) } form.save() # 프로필 수정 로그 기록 log_profile_update(request, request.user, changed_fields, field_changes) messages.success(request, '프로필이 성공적으로 업데이트되었습니다.') return redirect('accounts:custom_profile_edit') else: form = ProfileFullEditForm(user=request.user, instance=person) return render(request, 'C_accounts/profile_edit.html', {'form': form}) @login_required def password_change(request): """비밀번호 변경 뷰 (2단계 프로세스)""" # 세션 초기화 if 'password_change_step' not in request.session: request.session['password_change_step'] = 1 request.session['password_change_code'] = None request.session['password_change_phone'] = None request.session['password_change_verified'] = False step = request.session.get('password_change_step', 1) code_sent = request.session.get('password_change_code') is not None verified = request.session.get('password_change_verified', False) phone = request.session.get('password_change_phone') error = None message = None if step == 1: if request.method == 'POST': action = request.POST.get('action') if action == 'send_code': form1 = PasswordChangeStep1Form(request.POST, user=request.user) if form1.is_valid(): phone = form1.cleaned_data['phone'] # 인증번호 생성 및 실제 SMS 발송 verification_code = str(random.randint(100000, 999999)) # 실제 SMS 발송 sms_result = send_verification_sms(phone, verification_code) if sms_result['success']: request.session['password_change_code'] = verification_code request.session['password_change_phone'] = phone request.session['password_change_step'] = 1 request.session['password_change_code_sent_at'] = int(time.time()) message = '인증번호가 발송되었습니다.' code_sent = True print(f"[DEBUG] 비밀번호 변경 SMS 발송 성공: {phone} - {verification_code}") else: error = '인증번호 발송에 실패했습니다. 잠시 후 다시 시도해주세요.' print(f"[DEBUG] 비밀번호 변경 SMS 발송 실패: {sms_result['error']}") else: error = '전화번호를 확인해주세요.' elif action == 'verify_code': form1 = PasswordChangeStep1Form(request.POST, user=request.user) if form1.is_valid(): input_code = form1.cleaned_data['verification_code'] stored_code = request.session.get('password_change_code') code_sent_at = request.session.get('password_change_code_sent_at', 0) current_time = int(time.time()) # 인증번호 만료 시간 체크 (3분) if current_time - code_sent_at > 180: error = '인증번호가 만료되었습니다. 다시 발송해주세요.' elif input_code == stored_code: request.session['password_change_verified'] = True request.session['password_change_step'] = 2 return redirect('accounts:password_change') else: error = '인증번호가 일치하지 않습니다.' else: error = '인증번호를 확인해주세요.' else: form1 = PasswordChangeStep1Form(user=request.user) return render(request, 'C_accounts/password_change.html', { 'step': 1, 'form1': form1, 'code_sent': code_sent, 'error': error, 'message': message }) elif step == 2: # 세션이 만료되어 인증 정보가 없는 경우 if not verified or not phone: # 세션 초기화 request.session['password_change_step'] = 1 request.session['password_change_verified'] = False for key in ['password_change_code', 'password_change_phone', 'password_change_code_sent_at']: request.session.pop(key, None) form1 = PasswordChangeStep1Form(user=request.user) return render(request, 'C_accounts/password_change.html', { 'step': 1, 'form1': form1, 'code_sent': False, 'error': '세션이 만료되었습니다. 다시 인증해주세요.', 'message': None }) if request.method == 'POST': form2 = PasswordChangeStep2Form(request.POST) if form2.is_valid(): new_password = form2.cleaned_data['new_password1'] request.user.set_password(new_password) request.user.save() # 비밀번호 변경 로그 기록 log_password_change(request, request.user) # 세션 정리 del request.session['password_change_step'] del request.session['password_change_code'] del request.session['password_change_phone'] del request.session['password_change_verified'] messages.success(request, '비밀번호가 성공적으로 변경되었습니다.') return redirect('accounts:custom_profile_edit') else: return render(request, 'C_accounts/password_change.html', { 'step': 2, 'form2': form2, 'phone': phone }) else: form2 = PasswordChangeStep2Form() return render(request, 'C_accounts/password_change.html', { 'step': 2, 'form2': form2, 'phone': phone }) # 기본: 1단계로 초기화 request.session['password_change_step'] = 1 request.session['password_change_verified'] = False return redirect('accounts:password_change') # 모드1: 비밀번호 찾기 (로그인하지 않은 상태) def password_reset(request): """비밀번호 찾기 뷰""" # GET 요청 시 세션 상태에 따른 처리 if request.method == 'GET': # 강제 리셋 파라미터 확인 force_reset = request.GET.get('reset', '').lower() == 'true' current_step = request.session.get('password_reset_step', 1) current_verified = request.session.get('password_reset_verified', False) # 강제 리셋이거나, 2단계 인증된 상태가 아닌 경우 세션 초기화 if force_reset or not (current_step == 2 and current_verified): # 기존 비밀번호 찾기 세션 모두 제거 for key in ['password_reset_step', 'password_reset_code', 'password_reset_phone', 'password_reset_verified', 'password_reset_code_sent_at']: request.session.pop(key, None) # 새로운 세션 시작 request.session['password_reset_step'] = 1 request.session['password_reset_code'] = None request.session['password_reset_phone'] = None request.session['password_reset_verified'] = False if force_reset: print("[DEBUG] 비밀번호 찾기 세션 강제 초기화됨 (reset=true)") else: print("[DEBUG] 비밀번호 찾기 세션 초기화됨 (GET 요청)") else: print("[DEBUG] 비밀번호 찾기 2단계 인증된 상태 - 세션 유지") # 세션 초기화 (POST 요청 시에도 세션이 없으면 초기화) elif 'password_reset_step' not in request.session: request.session['password_reset_step'] = 1 request.session['password_reset_code'] = None request.session['password_reset_phone'] = None request.session['password_reset_verified'] = False step = request.session.get('password_reset_step', 1) code_sent = request.session.get('password_reset_code') is not None verified = request.session.get('password_reset_verified', False) phone = request.session.get('password_reset_phone') error = None message = None if step == 1: if request.method == 'POST': action = request.POST.get('action') if action == 'send_code': form1 = PasswordResetStep1Form(request.POST) if form1.is_valid(): phone = form1.cleaned_data['phone'] # 먼저 해당 전화번호로 가입된 사용자가 있는지 확인 try: user = User.objects.get(username=phone) print(f"[DEBUG] 비밀번호 찾기: 등록된 사용자 확인됨 - {phone}") except User.DoesNotExist: print(f"[DEBUG] 비밀번호 찾기: 등록되지 않은 전화번호 - {phone}") # 미등록 사용자 시도에 대한 텔레그램 알림 전송 from A_core.telegram_utils import send_password_reset_notification send_password_reset_notification(phone, request, user_exists=False) error = '등록되지 않은 전화번호입니다. 회원가입을 먼저 진행해주세요.' form1 = PasswordResetStep1Form(request.POST) return render(request, 'C_accounts/password_reset.html', { 'step': 1, 'form1': form1, 'code_sent': False, 'error': error, 'message': None }) # 등록된 사용자인 경우에만 인증번호 생성 및 SMS 발송 verification_code = str(random.randint(100000, 999999)) # 실제 SMS 발송 sms_result = send_verification_sms(phone, verification_code) if sms_result['success']: request.session['password_reset_code'] = verification_code request.session['password_reset_phone'] = phone request.session['password_reset_step'] = 1 request.session['password_reset_code_sent_at'] = int(time.time()) message = '인증번호가 발송되었습니다.' code_sent = True print(f"[DEBUG] 비밀번호 찾기 SMS 발송 성공: {phone} - {verification_code}") # 텔레그램 알림 전송 (비동기) - 등록된 사용자 from A_core.telegram_utils import send_password_reset_notification send_password_reset_notification(phone, request, user_exists=True) else: error = '인증번호 발송에 실패했습니다. 잠시 후 다시 시도해주세요.' print(f"[DEBUG] 비밀번호 찾기 SMS 발송 실패: {sms_result['error']}") else: error = '전화번호를 확인해주세요.' elif action == 'verify_code': form1 = PasswordResetStep1Form(request.POST) if form1.is_valid(): input_code = form1.cleaned_data['verification_code'] stored_code = request.session.get('password_reset_code') code_sent_at = request.session.get('password_reset_code_sent_at', 0) current_time = int(time.time()) # 인증번호 만료 시간 체크 (3분) if current_time - code_sent_at > 180: error = '인증번호가 만료되었습니다. 다시 발송해주세요.' elif input_code == stored_code: request.session['password_reset_verified'] = True request.session['password_reset_step'] = 2 print(f"[DEBUG] 비밀번호 찾기 인증 성공! 2단계로 직접 이동: {phone}") # 리다이렉트 대신 직접 2단계 렌더링 form2 = ForcePasswordSetForm() return render(request, 'C_accounts/password_reset.html', { 'step': 2, 'form2': form2, 'phone': phone }) else: error = '인증번호가 일치하지 않습니다.' else: error = '인증번호를 확인해주세요.' else: form1 = PasswordResetStep1Form() return render(request, 'C_accounts/password_reset.html', { 'step': 1, 'form1': form1, 'code_sent': code_sent, 'error': error, 'message': message }) elif step == 2: # 세션이 만료되어 인증 정보가 없는 경우 if not verified or not phone: # 세션 초기화 request.session['password_reset_step'] = 1 request.session['password_reset_verified'] = False for key in ['password_reset_code', 'password_reset_phone', 'password_reset_code_sent_at']: request.session.pop(key, None) form1 = PasswordResetStep1Form() return render(request, 'C_accounts/password_reset.html', { 'step': 1, 'form1': form1, 'code_sent': False, 'error': '세션이 만료되었습니다. 다시 인증해주세요.', 'message': None }) if request.method == 'POST': form2 = ForcePasswordSetForm(request.POST) if form2.is_valid(): new_password = form2.cleaned_data['new_password1'] # 해당 전화번호의 사용자 찾기 try: user = User.objects.get(username=phone) user.set_password(new_password) user.save() # 세션 정리 del request.session['password_reset_step'] del request.session['password_reset_code'] del request.session['password_reset_phone'] del request.session['password_reset_verified'] messages.success(request, '비밀번호가 성공적으로 재설정되었습니다. 새 비밀번호로 로그인해주세요.') return redirect('account_login') except User.DoesNotExist: error = '사용자를 찾을 수 없습니다.' else: return render(request, 'C_accounts/password_reset.html', { 'step': 2, 'form2': form2, 'phone': phone }) else: form2 = ForcePasswordSetForm() return render(request, 'C_accounts/password_reset.html', { 'step': 2, 'form2': form2, 'phone': phone }) # 기본: 1단계로 초기화 request.session['password_reset_step'] = 1 request.session['password_reset_verified'] = False return redirect('accounts:password_reset') # 모드2: 로그인 상태에서 비밀번호 변경 @login_required def password_change_logged_in(request): """로그인 상태에서 비밀번호 변경 뷰""" if request.method == 'POST': form = PasswordChangeLoggedInForm(request.POST, user=request.user) if form.is_valid(): new_password = form.cleaned_data['new_password1'] request.user.set_password(new_password) request.user.save() # 비밀번호 변경 로그 기록 log_password_change(request, request.user) messages.success(request, '비밀번호가 성공적으로 변경되었습니다.') return redirect('accounts:custom_profile_edit') else: form = PasswordChangeLoggedInForm(user=request.user) return render(request, 'C_accounts/password_change_logged_in.html', {'form': form}) # 모드3: 강제 비밀번호 설정 @login_required def force_password_set(request): """강제 비밀번호 설정 뷰""" # 현재 사용자의 Person 인스턴스 확인 try: person = Person.objects.get(user=request.user) if not person.비밀번호설정필요: return redirect('main') except Person.DoesNotExist: return redirect('main') if request.method == 'POST': form = ForcePasswordSetForm(request.POST) if form.is_valid(): new_password = form.cleaned_data['new_password1'] request.user.set_password(new_password) request.user.save() # 비밀번호 변경 로그 기록 log_password_change(request, request.user) # 비밀번호 설정 필요 플래그 해제 person.비밀번호설정필요 = False person.save() # 로그아웃 처리 from django.contrib.auth import logout logout(request) # 로그아웃 후 세션에 메시지 저장 (로그인 페이지에서 표시) request.session['password_set_message'] = '비밀번호가 성공적으로 설정되었습니다. 새 비밀번호로 로그인해주세요.' return redirect('account_login') else: form = ForcePasswordSetForm() return render(request, 'C_accounts/force_password_set.html', {'form': form}) @login_required def withdrawal_request(request): """회원탈퇴 요청 뷰""" # 이미 탈퇴 요청이 있는지 확인 existing_request = WithdrawalRequest.objects.filter( user=request.user, status='PENDING' ).first() if existing_request: messages.info(request, '이미 탈퇴 요청이 진행 중입니다. 관리자 승인을 기다려주세요.') return redirect('accounts:custom_profile_edit') if request.method == 'POST': form = WithdrawalRequestForm(request.POST, user=request.user) if form.is_valid(): withdrawal_request = form.save() # 탈퇴 요청 로그 기록 log_withdrawal_request(request, request.user, withdrawal_request.id) # 백그라운드에서 관리자에게 이메일 발송 try: from B_main.email_utils import send_withdrawal_request_notification send_withdrawal_request_notification( user=request.user, person=request.user.person, reason=withdrawal_request.reason ) except Exception as e: print(f"[EMAIL_ERROR] 탈퇴 요청 이메일 발송 실패: {e}") messages.success(request, '탈퇴 요청이 접수되었습니다. 관리자 승인 후 처리됩니다.') return redirect('accounts:custom_profile_edit') else: form = WithdrawalRequestForm(user=request.user) return render(request, 'C_accounts/withdrawal_request.html', {'form': form})