from flask import Flask, render_template, request, jsonify, session, redirect, url_for import os import logging from functools import wraps from datetime import datetime from utils.word_processor import WordProcessor from utils.google_drive import GoogleDriveUploader from config import Config logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) app = Flask(__name__) app.config.from_object(Config) # 세션 설정 app.secret_key = app.config['SECRET_KEY'] # 인증 데코레이터 def require_auth(f): @wraps(f) def decorated_function(*args, **kwargs): if not session.get('authenticated', False): return redirect(url_for('auth')) return f(*args, **kwargs) return decorated_function # 전역 객체 초기화 word_processor = WordProcessor(app.config['WORD_TEMPLATE_DIR']) # 구글 드라이브 업로더 초기화 (선택사항) drive_uploader = None try: # credentials.json 파일이 있는 경우에만 초기화 시도 if os.path.exists(app.config['GOOGLE_DRIVE_CREDENTIALS_FILE']): logger.info( "구글 드라이브 업로더 초기화 시도 | credentials=%s token=%s", app.config['GOOGLE_DRIVE_CREDENTIALS_FILE'], app.config['GOOGLE_DRIVE_TOKEN_FILE'], ) drive_uploader = GoogleDriveUploader( app.config['GOOGLE_DRIVE_CREDENTIALS_FILE'], app.config['GOOGLE_DRIVE_TOKEN_FILE'] ) logger.info("구글 드라이브 업로더 초기화 성공") else: logger.info("credentials.json이 없어 구글 드라이브 업로더를 초기화하지 않았습니다.") except FileNotFoundError: logger.warning("구글 드라이브 인증 파일을 찾을 수 없습니다. 업로드 기능 비활성화.") except Exception as e: # 브라우저 관련 오류나 기타 오류는 조용히 처리 # 서버 환경에서는 브라우저가 없을 수 있으므로 정상적인 상황 logger.warning("구글 드라이브 초기화 중 오류 발생: %s", str(e)) @app.route('/auth', methods=['GET', 'POST']) def auth(): """인증 페이지""" if request.method == 'POST': access_key = request.form.get('access_key', '') if access_key == app.config['ACCESS_KEY']: session['authenticated'] = True return redirect(url_for('index')) else: return render_template('auth.html', error='인증키가 올바르지 않습니다.') return render_template('auth.html') @app.route('/logout') def logout(): """로그아웃 - 세션 전체 삭제""" session.clear() return redirect(url_for('auth')) @app.route('/') @require_auth def index(): """메인 페이지""" return render_template('index.html') @app.route('/step1', methods=['GET', 'POST']) @require_auth def step1(): """1단계: 기본 정보 입력""" if request.method == 'POST': data = request.get_json() session['basic_info'] = data return jsonify({'success': True, 'next_step': '/step2'}) basic_info = session.get('basic_info', {}) return render_template('step1.html', districts=app.config['DISTRICTS'], cults=app.config['CULTS'], basic_info=basic_info) @app.route('/step2', methods=['GET', 'POST']) @require_auth def step2(): """2단계: 기독교대한감리회 신앙고백 교리 점검""" if request.method == 'POST': data = request.get_json() session['methodist_doctrine'] = data return jsonify({'success': True, 'next_step': '/step3'}) methodist_doctrine = session.get('methodist_doctrine', {}) return render_template('step2.html', methodist_doctrine=methodist_doctrine) @app.route('/step3', methods=['GET', 'POST']) @require_auth def step3(): """3단계: 이단 일반 교리 점검""" if request.method == 'POST': data = request.get_json() # 7번 문항 검증 (기타의견란에 작성 필요) other_opinions = data.get('other_opinions', '').strip() if not other_opinions or '7번:' not in other_opinions: return jsonify({ 'success': False, 'message': '7번 문항은 기타의견란에 "7번: "으로 시작하여 답변해주세요.' }), 400 session['general_cult_doctrine'] = data # 기본 정보에서 이단교단 확인 basic_info = session.get('basic_info', {}) cult_name = basic_info.get('cult', '') # "기타" 선택 시 step4 건너뛰고 step5로 if cult_name == '기타 (위 선택지에 없을 경우)': session['specific_cult_doctrine'] = {} # 빈 딕셔너리로 설정 return jsonify({'success': True, 'next_step': '/step5'}) return jsonify({'success': True, 'next_step': '/step4'}) general_cult_doctrine = session.get('general_cult_doctrine', {}) return render_template('step3.html', general_cult_doctrine=general_cult_doctrine) @app.route('/step4', methods=['GET', 'POST']) @require_auth def step4(): """4단계: 출신 이단별 교리 점검""" if request.method == 'POST': data = request.get_json() # 세션에서 출신 이단 정보 가져오기 basic_info = session.get('basic_info', {}) cult_name = basic_info.get('cult', '') # 이단별 상세점검 문항 가져오기 questions = app.config.get('CULT_DETAIL_QUESTIONS', {}).get(cult_name, []) # 기타의견란에 작성해야 하는 문항 번호 찾기 required_question_numbers = [] for i, question in enumerate(questions, 1): if '기타의견란' in question or '기타의견' in question: required_question_numbers.append(i) # 기타의견란 검증 if required_question_numbers: other_opinions = data.get('other_opinions', '').strip() if not other_opinions: return jsonify({ 'success': False, 'message': f'{", ".join([str(n) + "번" for n in required_question_numbers])} 문항은 기타의견란에 작성해주세요.' }), 400 # 각 필수 문항 번호가 기타의견란에 포함되어 있는지 확인 missing_questions = [] for q_num in required_question_numbers: if f'{q_num}번:' not in other_opinions: missing_questions.append(q_num) if missing_questions: return jsonify({ 'success': False, 'message': f'{", ".join([str(n) + "번" for n in missing_questions])} 문항은 기타의견란에 "{", ".join([str(n) + "번:" for n in missing_questions])}"으로 시작하여 답변해주세요.' }), 400 session['specific_cult_doctrine'] = data return jsonify({'success': True, 'next_step': '/step5'}) # 세션에서 출신 이단 정보 가져오기 basic_info = session.get('basic_info', {}) cult_name = basic_info.get('cult', '') # 이단별 상세점검 문항 가져오기 questions = app.config.get('CULT_DETAIL_QUESTIONS', {}).get(cult_name, []) # 문항이 없으면 step5로 리다이렉트 (안전장치) if not questions: return redirect(url_for('step5')) specific_cult_doctrine = session.get('specific_cult_doctrine', {}) return render_template('step4.html', cult_name=cult_name, questions=questions, specific_cult_doctrine=specific_cult_doctrine) @app.route('/step5', methods=['GET', 'POST']) @require_auth def step5(): """5단계: 간증문 입력""" if request.method == 'GET': testimony = session.get('testimony', {}) return render_template('step5.html', testimony=testimony) if request.method == 'POST': data = request.get_json() session['testimony'] = data # 모든 데이터 수집 (specific_cult_doctrine는 없을 수 있음) all_data = { 'basic_info': session.get('basic_info', {}), 'methodist_doctrine': session.get('methodist_doctrine', {}), 'general_cult_doctrine': session.get('general_cult_doctrine', {}), 'specific_cult_doctrine': session.get('specific_cult_doctrine', {}), 'testimony': session.get('testimony', {}) } # specific_cult_doctrine가 없으면 빈 딕셔너리로 설정 if not all_data.get('specific_cult_doctrine'): all_data['specific_cult_doctrine'] = {} # 워드 문서 생성 try: cult_name = all_data['basic_info'].get('cult', '기타') # 이단별 상세점검 문항 가져오기 cult_questions = app.config.get('CULT_DETAIL_QUESTIONS', {}).get(cult_name, []) output_path = word_processor.generate_document(all_data, cult_name, cult_questions) # 구글 드라이브 업로드 upload_success = False if drive_uploader and app.config['GOOGLE_DRIVE_FOLDER_ID']: upload_filename = f"교리점검표_{all_data['basic_info'].get('name', '무명')}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.docx" logger.info("구글 드라이브 업로드 시도 | file=%s, folder=%s", upload_filename, app.config['GOOGLE_DRIVE_FOLDER_ID']) try: file_id = drive_uploader.upload_file( output_path, upload_filename, app.config['GOOGLE_DRIVE_FOLDER_ID'] ) logger.info("구글 드라이브 업로드 성공 | file_id=%s", file_id) upload_success = True # 업로드 성공 시 로컬 파일 삭제 try: os.remove(output_path) logger.info("로컬 파일 삭제 완료 | file=%s", output_path) except Exception as e: logger.warning("로컬 파일 삭제 실패: %s", str(e)) # 세션은 complete 페이지에서 로그아웃 시 삭제 return jsonify({ 'success': True, 'message': '제출이 완료되었습니다. 구글 드라이브에 업로드되었습니다.', 'file_id': file_id }) except Exception as e: logger.warning("구글 드라이브 업로드 실패: %s", str(e)) # 업로드 실패 시 로컬 파일 유지 # 세션은 유지 (재시도 가능하도록) return jsonify({ 'success': True, 'message': f'제출이 완료되었습니다. (구글 드라이브 업로드 실패: {str(e)})', 'file_path': output_path }) else: # 구글 드라이브 설정이 없는 경우 로컬 파일 유지 # 세션은 유지 return jsonify({ 'success': True, 'message': f'제출이 완료되었습니다. 파일: {output_path}', 'file_path': output_path }) except Exception as e: return jsonify({ 'success': False, 'message': f'오류가 발생했습니다: {str(e)}' }), 500 return render_template('step5.html') @app.route('/complete') @require_auth def complete(): """완료 페이지""" return render_template('complete.html') if __name__ == '__main__': # 필요한 디렉토리 생성 os.makedirs(app.config['WORD_TEMPLATE_DIR'], exist_ok=True) os.makedirs('output', exist_ok=True) app.run(host='0.0.0.0', port=5000, debug=True)