commit 8019a7e4bae833238baf00ddf0f68ab33952f79a Author: peregr1nus Date: Wed Dec 10 13:27:48 2025 +0900 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..8b6a5a5 --- /dev/null +++ b/README.md @@ -0,0 +1,170 @@ +# 이단 탈퇴자 교리점검표 웹서비스 + +기독교대한감리회 신앙고백에 따른 이단 탈퇴자를 위한 교리점검표 설문 폼 서비스입니다. + +## 주요 기능 + +1. **설문응답자 기본 정보 입력** - 이름, 생년월일, 연락처, 출신 이단, 재적 기간 +2. **기독교대한감리회 신앙고백 교리 점검** - 삼위일체, 예수 그리스도, 성경, 구원 등 +3. **이단 일반 교리 점검** - 추가 계시, 지도자 신격화, 구원관, 세계관 +4. **출신 이단별 교리 점검** - 각 이단별 특정 교리 점검 +5. **간증문 입력** - 탈퇴 과정과 소감 작성 +6. **워드 문서 자동 생성** - 응답 내용을 워드 템플릿에 자동 입력 +7. **구글 드라이브 자동 업로드** - 생성된 문서를 구글 드라이브에 업로드 + +## 설치 방법 + +### 1. 의존성 설치 + +```bash +pip install -r requirements.txt +``` + +### 2. 환경 변수 설정 + +`.env` 파일을 생성하고 다음 내용을 입력하세요: + +```env +SECRET_KEY=your-secret-key-here +ACCESS_KEY=your-access-key-here +GOOGLE_DRIVE_CREDENTIALS_FILE=credentials.json +GOOGLE_DRIVE_TOKEN_FILE=token.json +GOOGLE_DRIVE_FOLDER_ID=your-google-drive-folder-id +``` + +**인증키 설정:** +- `ACCESS_KEY`: 서비스 접근을 위한 인증키입니다. +- 기본값: `Kx9mP2vQ7nR4tY8wZ3bC6hJ1fL5dN0sA8uE2iM7o` (프로덕션에서는 반드시 변경하세요) +- 사용자는 이 인증키를 입력해야만 설문에 접근할 수 있습니다. +- `.env` 파일에서 `ACCESS_KEY`를 설정하면 기본값을 덮어씁니다. + +### 3. 구글 드라이브 API 설정 + +1. [Google Cloud Console](https://console.cloud.google.com/)에서 프로젝트 생성 +2. Google Drive API 활성화 +3. OAuth 2.0 클라이언트 ID 생성 (데스크톱 애플리케이션) +4. `credentials.json` 파일을 프로젝트 루트에 저장 +5. 구글 드라이브에 업로드할 폴더를 생성하고 폴더 ID를 `.env`에 설정 + +### 4. 워드 템플릿 준비 (선택사항) + +`word_templates/` 디렉토리에 각 이단별 템플릿 파일을 준비할 수 있습니다: +- `신천지_template.docx` +- `구원파_template.docx` +- `하나님의교회_template.docx` +- `통일교_template.docx` +- `기타_template.docx` +- `default_template.docx` (기본 템플릿) + +템플릿 파일이 없으면 자동으로 기본 템플릿이 생성됩니다. + +## 실행 방법 + +```bash +python app.py +``` + +서버가 실행되면 브라우저에서 `http://localhost:5000`으로 접속하세요. + +## 프로젝트 구조 + +``` +. +├── app.py # Flask 메인 애플리케이션 +├── config.py # 설정 파일 +├── requirements.txt # Python 의존성 +├── .env # 환경 변수 (생성 필요) +├── credentials.json # 구글 드라이브 인증 파일 (생성 필요) +├── token.json # 구글 드라이브 토큰 (자동 생성) +├── templates/ # HTML 템플릿 +│ ├── base.html +│ ├── step1.html +│ ├── step2.html +│ ├── step3.html +│ ├── step4.html +│ ├── step5.html +│ └── complete.html +├── static/ # 정적 파일 +│ ├── style.css +│ └── script.js +├── word_templates/ # 워드 템플릿 파일 +├── output/ # 생성된 문서 저장 폴더 (자동 생성) +└── utils/ # 유틸리티 모듈 + ├── word_processor.py + └── google_drive.py +``` + +## 리눅스 서버 배포 + +### 1. Gunicorn 설치 + +```bash +pip install gunicorn +``` + +### 2. Gunicorn으로 실행 + +```bash +gunicorn -w 4 -b 0.0.0.0:5000 app:app +``` + +### 3. systemd 서비스로 등록 (선택사항) + +`/etc/systemd/system/doctrine-check.service` 파일 생성: + +```ini +[Unit] +Description=Doctrine Check Web Service +After=network.target + +[Service] +User=your-user +WorkingDirectory=/path/to/doctrine-check +Environment="PATH=/path/to/venv/bin" +ExecStart=/path/to/venv/bin/gunicorn -w 4 -b 0.0.0.0:5000 app:app + +[Install] +WantedBy=multi-user.target +``` + +서비스 시작: + +```bash +sudo systemctl start doctrine-check +sudo systemctl enable doctrine-check +``` + +### 4. Nginx 리버스 프록시 설정 (선택사항) + +```nginx +server { + listen 80; + server_name your-domain.com; + + location / { + proxy_pass http://127.0.0.1:5000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} +``` + +## 주의사항 + +- 프로덕션 환경에서는 `SECRET_KEY`와 `ACCESS_KEY`를 안전하게 설정하세요 +- 인증키는 링크를 받은 사람에게만 전달하세요 +- 구글 드라이브 API 인증 정보는 절대 공개하지 마세요 +- `output/` 디렉토리의 파일은 정기적으로 정리하세요 +- HTTPS를 사용하여 데이터 보안을 강화하세요 + +## 인증키 관리 + +- 기본 인증키: `Kx9mP2vQ7nR4tY8wZ3bC6hJ1fL5dN0sA8uE2iM7o` +- 인증키는 `.env` 파일의 `ACCESS_KEY`로 변경할 수 있습니다 +- 사용자는 서비스 접속 시 인증 페이지에서 인증키를 입력해야 합니다 +- 인증 후 세션에 저장되며, 로그아웃 시 인증이 해제됩니다 + +## 라이선스 + +이 프로젝트는 기독교대한감리회를 위한 내부 사용 목적으로 제작되었습니다. + diff --git a/app.py b/app.py new file mode 100644 index 0000000..4eaf40d --- /dev/null +++ b/app.py @@ -0,0 +1,202 @@ +from flask import Flask, render_template, request, jsonify, session, redirect, url_for +import os +from functools import wraps +from datetime import datetime +from utils.word_processor import WordProcessor +from utils.google_drive import GoogleDriveUploader +from config import Config + +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: + drive_uploader = GoogleDriveUploader( + app.config['GOOGLE_DRIVE_CREDENTIALS_FILE'], + app.config['GOOGLE_DRIVE_TOKEN_FILE'] + ) +except FileNotFoundError as e: + print(f"경고: 구글 드라이브 인증 파일을 찾을 수 없습니다. 구글 드라이브 업로드 기능이 비활성화됩니다.") + print(f"상세: {str(e)}") +except Exception as e: + print(f"경고: 구글 드라이브 초기화 중 오류가 발생했습니다: {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.pop('authenticated', None) + 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'}) + return render_template('step1.html', + districts=app.config['DISTRICTS'], + cults=app.config['CULTS']) + +@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'}) + return render_template('step2.html') + +@app.route('/step3', methods=['GET', 'POST']) +@require_auth +def step3(): + """3단계: 이단 일반 교리 점검""" + if request.method == 'POST': + data = request.get_json() + 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'}) + return render_template('step3.html') + +@app.route('/step4', methods=['GET', 'POST']) +@require_auth +def step4(): + """4단계: 출신 이단별 교리 점검""" + if request.method == 'POST': + data = request.get_json() + 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')) + + return render_template('step4.html', cult_name=cult_name, questions=questions) + +@app.route('/step5', methods=['GET', 'POST']) +@require_auth +def step5(): + """5단계: 간증문 입력""" + 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', '기타') + output_path = word_processor.generate_document(all_data, cult_name) + + # 구글 드라이브 업로드 + if drive_uploader and app.config['GOOGLE_DRIVE_FOLDER_ID']: + try: + file_id = drive_uploader.upload_file( + output_path, + f"교리점검표_{all_data['basic_info'].get('name', '무명')}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.docx", + app.config['GOOGLE_DRIVE_FOLDER_ID'] + ) + # 업로드 후 로컬 파일 삭제 (선택사항) + # os.remove(output_path) + return jsonify({ + 'success': True, + 'message': '제출이 완료되었습니다. 구글 드라이브에 업로드되었습니다.', + 'file_id': file_id + }) + except Exception as 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) + diff --git a/config.py b/config.py new file mode 100644 index 0000000..e5a7de8 --- /dev/null +++ b/config.py @@ -0,0 +1,204 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + +class Config: + SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production' + + # 접근 인증키 (링크를 받은 사람만 사용 가능) + ACCESS_KEY = os.environ.get('ACCESS_KEY') or 'Kx9mP2vQ7nR4tY8wZ3bC6hJ1fL5dN0sA8uE2iM7o' + + # Google Drive API 설정 + GOOGLE_DRIVE_CREDENTIALS_FILE = os.environ.get('GOOGLE_DRIVE_CREDENTIALS_FILE', 'credentials.json') + GOOGLE_DRIVE_TOKEN_FILE = os.environ.get('GOOGLE_DRIVE_TOKEN_FILE', 'token.json') + GOOGLE_DRIVE_FOLDER_ID = os.environ.get('GOOGLE_DRIVE_FOLDER_ID', '') + + # 워드 템플릿 경로 + WORD_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), 'word_templates') + + # 교구 목록 + DISTRICTS = [ + '성남교구', + '서울교구', + '경기남교구', + '경기북교구', + '송파교구', + '위례교구', + '바울교구', + '다윗공동체', + '요셉1교구', + '요셉2교구', + '요셉3교구', + '온라인교회', + '교회학교', + '젊은이교회', + '갈렙교회', + '기타' + ] + + # 이단교단 목록 + CULTS = [ + '신천지', + '하나님의교회', + '다락방', + '구원파 (박옥수,이요한,유병언)', + 'JMS', + '성락교회 (베뢰아)', + '통일교', + '몰몬교', + '안식교', + '사랑하는교회', + '예수중심교회 (이초석)', + '기타 (위 선택지에 없을 경우)' + ] + + # 이단별 상세점검 문항 (기타는 제외) + CULT_DETAIL_QUESTIONS = { + '신천지': [ + '1. 성경 내용을 \'역사, 교훈, 예언, 성취\'로 구분해 가르쳤다.', + '2. \'성경은 <계시록 시대>등 8개 시대로 구분되며 반드시 예언을 깨달아야 한다\'고 가르쳤다.', + '3. \'죄 사함은 예수를 믿고 비유를 깨달으며 새 언약을 지킬 때 가능하다\'고 가르쳤다.', + '4. \'사단이 성전에 앉아 하나님으로 가장해 신앙인들을 미혹한다\'고 말했다.', + '5. \'천국 비밀은 감춰져 있으며 비유로 된 계시의 말씀을 깨달아야 한다\'고 가르쳤다.', + '6. \'시대별 예언과 성취가 있으며, 일반 교회에서 봉함된 말씀을 계속 배우는 것으로는 구원받을 수 없다\'고 충고했다.', + '7. \'육적 이스라엘, 영적 이스라엘, 영적 새 이스라엘(영적 새 선민)\'에 대해 가르쳤다.', + '8. \'재림의 때 출현하는 약속의 목자, 이긴 자가 있다\'고 강조했다.', + '9. 성경공부를 시작한 뒤 주일설교가 잘 들리지 않고 목사님이 거짓 목자처럼 느껴진 적이 있다.', + '10. 성경공부 후 기성 교회가 \'바벨론 교회\'라는 느낌이 든 적이 있다.', + '11. 이만희교주의 육체 영생을 믿으십니까?', + '12. 비유풀이 성경해석이 옳다고 믿으십니까?', + '13. 성경에 짝이 있다고 생각하십니까?', + '14. \'모략\'을 사용하는 신천지가 잘못된 성경해석을 주장한다고 생각하십니까?', + '15. 144,000이 숫자 그대로의 144,000명을 의미한다고 믿으십니까?', + '16. 세례요한은 \'배도자\'라고 생각하십니까?', + '17. 성경에 의하면 예수 이후로 새 구도자를 보내신다는 언약이 있다고 생각하십니까?' + ], + '하나님의교회': [ + '1. 창세기1장27절에서 \'하나님이 자기 형상 곧 하나님의 형상대로 사람을 창조하시되 남자와 여자를 창조하시고\'를 근거로 하나님이\'아버지하나님\' 어머니하나님\'으로 계시다고 생각하십니까?', + '2. 물이 얼음이 되고 수증기가 되듯이 \'성부\'가 \'성자\'가되고 \'성자\'가 \'성령\'이 되는 것이 삼위일체에 대한 바른 설명이라고 생각하십니까?', + '3. 성령시대의 보혜사가 이땅에 사람의 몸으로 오셨다고 생각하십니까?', + '4. 갈라디아서 4장 26절 \'예루살렘은 자유자니 곧 우리 어머니라\' 이 구절에서 \'어머니\'는 \'여자하나님\'을 말하는 것이라 생각하십니까?', + '5. 계시록 22장 17절에서 \'성령과 신부가 말씀하시기를 오라 하시는도다\'에서 신부는 \'어머니 하나님\' 이라고 생각하십니까?', + '6. 유월절을 지켜야 구원을 받을 수 있다고 생각하십니까?', + '7. 지금도 안식일(토요일)을 지켜야한다고 생각하십니까?', + '8. 주일은 태양신 숭배사상의 유물이라고 생각하십니까?', + '9. 성탄절을 지키고 십자가를 세운 것이 우상숭배라고 생각하십니까?' + ], + '다락방': [ + '1. 이 땅에 인간의 몸을 입고 오신 하나님이 예수님이며, 죽으신 예수님이 부활하여 우리 안에 성령님으로 계신다고 생각하십니까?', + '2. 성경에는 마귀계시 및 성경 외의 계시가 있다고 생각하십니까?', + '3. 예수께서 십자가에서 고난 받으신 것은 사탄에게 우리의 모든 실패의 대가를 갚아버리는 것이라고 생각하십니까?', + '4. 예수는 마귀를 멸하러 오셨습니까?', + '5. 예수를 믿고 마귀를 쫓아내면 모든 문제가 해결된다고 생각하십니까?', + '6. 영접이란 \'하나님의 아들이 오신 것은 마귀 일을 멸하려 하심이라는 진리를 받아들이는 것\'이라고 믿으십니까?', + '7. 기존 교인들은 새로운 복음으로 예수를 다시 영접해야 한다고 생각하십니까?', + '8. 올바른 신자는 사탄을 결박하는 힘이 있고 기도할 때 천사가 움직인다고 생각하십니까?', + '9. 성도들에게 내주하는 성령과 예수의 성육신은 동일한 개념인가요?', + '10. 예수그리스도를 영접했다면 더 이상의 회개가 필요할까요?', + '11. 렘넌트 운동은 성도님의 신앙과 생활에 있어 어떤 의미였습니까?', + '12. 예수님이 십자가에서 죽으신 것이 사단과 어떤 상관이 있다고 보시나요?' + ], + '구원파 (박옥수,이요한,유병언)': [ + '1. 구원 받은 날짜를 정확히 알아야 진짜 구원 받았다고 생각하십니까?', + '2. 죄사함과 거듭남의 비밀을 \'깨달음\'으로 구원 받는다고 생각하십니까?', + '3. 예수 그리스도가 우리의 마음을 지배하시면 더 이상 우리 자신이 죄와 싸울 필요가 없다고 생각하십니까?', + '4. 예수의 십자가 사건으로 과거와 현재와 미래의 죄까지 다 사함 받았기 때문에 구원을 받은 후에 다시 회개할 필요가 없다고 생각하십니까?', + '5. 회개하는 자는 죄가 있다는 증거고, 죄가 있으면 구원 받지 못한다고 생각하십니까?', + '6. 고백으로 회개를 대신할 수 있다고 생각하십니까?', + '7. 예수 시대에는 기도가 필요했으나 지금은 기도가 필요 없다고 믿습니까?', + '8. 구원 받기 전의 기도는 하나님께서 받지 않으신다고 생각하십니까?', + '9. 새벽기도나 철야기도는 형식이므로 중요하지 않다고 보십니까?', + '10. 성도의 교제가 곧 기도이며 예배라고 생각하십니까?', + '11. 주일성수, 십일조 등 기존의 교회 제도는 율법의 산물이기에 지킬 필요가 없다고 생각하십니까?', + '12. 예배는 형식이 없으므로 축도와 신앙고백도 필요 없다고 생각하십니까?', + '13. 우리는 죽지 않고 살아 있을 때 주님이 오실 것이라고 믿습니까?' + ], + 'JMS': [ + '1. 예수님은 인간이지만 성자의 영이 임한 메시아입니까?', + '2. 오늘 날 재림주가 온다면 초림때의 구세주처럼 육을 입은 존재입니까?', + '3. 예수님이 타고 오신다는 구름은 무엇을 의미합니까? (이 문항은 아래 기타의견란에 문항번호를 적은 후 서술 부탁 드립니다.)', + '4. 동방은 한국이며 그땅의 의인은 재림주입니까?', + '5. 성령은 모성체 격의 신입니까?', + '6. 선악과는 여성의 생식기(하와)를, 생명나무 열매는 남성(아담)의 생식기를 상징합니까?', + '7. <30개론>, <70개론>은 교주가 하나님께 직접 계시받은 진리라는 주장에 동의하십니까?', + '8. 성경은 비유와 상징이며 모두 짝이 있습니까?', + '9. 인류의 역사는 구약 시대(주인과 종의 시대), 신약 시대(아버지와 자녀의 시대), 성약 시대(신랑과 신부의 시대)등 삼시대로 나뉘어집니까?', + '10. 로뎀나무 아래에 있던 엘리야를 하나님이 먹여 살리신 사건, 여호수아서에서 왕벌 사건은 단지 비유입니까?', + '11. 해당 단체를 나왔기에 영이 총에 맞을까 두려우십니까?', + '12. 이성교제 자체가 죄입니까?', + '13. 각 시대마다 구원자들이 존재했습니까?', + '14. 때로는 해당 단체에 대해서 그리움과 후회가 남습니까?' + ], + '성락교회 (베뢰아)': [ + '1. 예수님이 이땅에 오신 목적과 사람을 창조한 이유가 마귀를 멸하기 위함이라고 생각하고 계십니까?', + '2. 창세기 1장의 사람과 창세기 2장의 사람(아담)이 다르다고 생각하고 계십니까?', + '3. 창세기2장에서 \'사람이 생령이 되었다\' 부분에서 \'생령\'은 영적인 존재라는 뜻이라고 생각하십니까?', + '4. 선과 악을 아는 나무의 실과를 먹은 아담의 죄와 하와의 죄가 다르다고 생각하십니까?', + '5. 사람의 죄의 종류가 원죄,본죄,자범죄로 나뉜다고 생각하고 계십니까?', + '6. 불신자의 사후존재가 귀신이라고 생각하십니까?', + '7. 삼위일체 하나님의 이름이 \'예수\'라고 생각하십니까?', + '8. 여호와의 이름은 누구의 이름이라고 생각하십니까? (이 문항은 아래 기타의견란에 문항번호를 적고 서술 부탁드립니다.)', + '9. 모든 질병이 귀신으로부터 온다고 생각하십니까?', + '10. 하나님은 우리의 심령에 거하며 귀신은 육체에 거한다고 생각하십니까?' + ], + '통일교': [ + '1. 사탄이며 뱀인 타락한 천사 루시엘과 하와 사이의 성적 관계가 영적 타락이며, 하와와 아담의 성적관계가 육적 타락이라고 생각하십니까?', + '2. 인간은 오직 재림주와의 피가름 법칙을 통해서만 육체', + '3. 제3의 아담이 사탄의 타락한 피를 회복시켜 온 인류의 참 부모가 되며, 세계의 왕이 되어 한국을 중심으로 지상천국을 건설한다고 생각하십니까?', + '4. 예수의 십자가 구속사업은 영적인 면에서만 완성되', + '5. 전역사 노정의 복귀섭리를 담당한 중심인물은 네 사람이 있는데, \'기대시대\'의 아담, \'구약시대\'의 아브라함, \'신약시대\'의 예수, 그리고 \'성약시대\'의 재림주가 있다고 생각하십니까?', + '6. 구름은 타락한 인간이 중생(重生)하여 그 마음이 항상 땅에 있지 않고 하늘에 있는 독실한 성도들을 의미하는 것이라고 생각하십니까?', + '7. 예수님의 재림이 지상에서 육신을 쓰고 탄생하시는 것으로써 이루어진다고 생각하십니까?', + '8. 예수님이 재림하실 동방의 나라가 바로 \'한국\'이라고 생각하십니까?' + ], + '몰몬교': [ + '1. 몰몬교회만이 유일한 예수 그리스도의 교회라 생각하십니까?', + '2. 모든 인간은 하늘의 부모에게서 받은 영을 가지고 있으며, 하나님처럼 되기 위해 이땅에 태어났다고 생각하십니까?', + '3. 몰몬경도 하나님 말씀이라고 생각하십니까?', + '4. 에스겔 37장 15-17절에 나오는 두 개의 막대기가 \'성경\'과 \'몰몬경\'을 가리킨다고 생각하십니까?', + '5. 몰몬경은 아메리카 인디언들의 조상이 기원전 600년에 예루살렘으로부터 배를 타고 이주해 왔다고 생각하십니까?', + '6. 요셉스미스는 계시를 받아 황금판의 고대 히브리어를 영어로 번역하였다고 생각하십니까?', + '7. 엘로힘 하나님이 구약에서는 여호와로, 신약에서는 예수로 나타났다고 생각하십니까?', + '8. 예수그리스도는 하나님의 독생자가 아니며 창조주로서 신성을 가진 분도 아니며, 어느 때에 하나님의 불꽃으로 감화되어 신적인 존재가 된 것에 불과하다고 생각하십니까?', + '9. 그리스도의 신성이나 십자가에서 대속사건은 아무 의미가 없다고 생각하십니까?', + '10. 인간은 하나님이 창조하신 것이 아니다. 인간의 영은 영원부터 하나님과 함께힌 존재라고 생각하십니까?', + '11. 아담의 죄로 인해 인류가 벌 받는 것이 아니며, 자기 자신의 죄로 인해 벌 받는 것이라고 생각하십니까?', + '12. 온 인류는 모두 구원을 받을 수 있다는 보편적 구원관. 지옥이나 죄에 대한 형벌이 없다고 생각하십니까?' + ], + '안식교': [ + '1. 안식교가 시한부 종말론에 의해 시작되었다는 것을 알고 계십니까?', + '2. 엘렌 G. 화이트 여사의 말과 저서와 예언들은 성경과 동일한 권위를 지니고 있습니까?', + '3. 믿음으로 얻어지는 구원과 율법을 지키는 행위로 얻어지는 구원이 구분되어 있다고 생각하십니까?', + '4. 믿음으로 말미암는 의는 그리스도의 공로를 의지하여 의롭다고 선언 받는 칭의를 말하는 것이라고 생각하십니까?', + '5. 우리의 품성 가운데 점이나 흠이 있는 한 우리 중 아무라도 하나님의 인을 결코 받지 못한다고 생각하십니까?', + '6. 인간이 율법을 완전하게 지킬 수 있다고 생각하십니까?', + '7. 예수님이 성소에서 지성소로 들어가신 이유는 우리가 정결한 성도가 되길 원하신 것이라고 생각하십니까?', + '8. 인간의 몸을 입으신 그리스도께서는 인간의 죄스러운 성품을 그대로 지니고 있습니까?', + '9. 토요일인 안식일을 지키는 것이 구원의 조건이라고 생각하십니까?', + '10. 율법의 행위는 구원의 조건이며 현세의 안전한 성화를 주장하고 품성의 변화를 위해서 육식을 금하고 채식을 강조해야 합니까?', + '11. 지옥은 존재하지 않으며 불신자가 죽으면 그 영혼은 멸절됩니까?', + '12. 2300주야는 2300년으로 계산하는 것이 성경적이라고 생각하십니까?' + ], + '사랑하는교회': [ + '1. 진짜 구원받은 사람도 진짜 버림받을 수 있다고 생각하십니까?', + '2. 사도와 선지자가 오늘 날도 존재한다고 생각하십니까?', + '3. 직통계시가 여전히 존재한다고 생각하십니까?', + '4. 구원에 이르는 통로로써 성경 외에 예언이나 치유 등 다른 은사도 중요하다고 생각하십니까?', + '5. 누구든지 훈련을 통해 예언 사역이 가능하다고 생각하십니까?', + '6. 은사 사역을 하지 않고 제자훈련, 강해설교만 하는 교회들은 종교적인 교회라고 생각하십니까?', + '7. 하늘나라 전략회의에 참여해 새로운 전략을 받아온 분이 계시다고 믿으십니까?', + '8. 사랑하는 교회(구 큰믿음교회)에서 주관한 예전전도학교 (구 선지자학교)에 참여한 적이 있으신지요?' + ], + '예수중심교회 (이초석)': [ + '1. 성부와 성자와 성령의 이름이 예수라고 생각하고 계십니까?', + '2. 이땅이 마귀가 갇혀 있는 음부라고 생각하십니까?', + '3. 귀신의 정체가 불신자의 사후 존재라고 생각하십니까?', + '4. 모든 질병의 원인이 귀신에 의해 생긴다고 생각하십니까?', + '5. 귀신을 추방함으로 신자의 이름이 하늘나라에 기록된다고 생각하십니까?', + '6. 예수께서 영의 육체를 입고 오셨다고 생각하십니까?', + '7. 예수님의 오신 목적중 가장 큰 것이 마귀를 멸하기 위해 오셨다고 생각하십니까?', + '8. 구원이란 인간을 억누르고 있는 귀신의 세력으로부터 자유함을 입는 것이라고 생각하십니까?' + ] + } + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5c463b9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +Flask==3.0.0 +python-docx==1.1.0 +google-api-python-client==2.108.0 +google-auth-httplib2==0.1.1 +google-auth-oauthlib==1.1.0 +python-dotenv==1.0.0 +Werkzeug==3.0.1 + diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..0cfdb00 --- /dev/null +++ b/static/script.js @@ -0,0 +1,27 @@ +// 공통 JavaScript 함수들 + +// 폼 유효성 검사 +function validateForm(formId) { + const form = document.getElementById(formId); + if (!form.checkValidity()) { + form.reportValidity(); + return false; + } + return true; +} + +// 로딩 표시 +function showLoading() { + const modal = document.getElementById('loadingModal'); + if (modal) { + modal.style.display = 'flex'; + } +} + +function hideLoading() { + const modal = document.getElementById('loadingModal'); + if (modal) { + modal.style.display = 'none'; + } +} + diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..c7307de --- /dev/null +++ b/static/style.css @@ -0,0 +1,475 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Malgun Gothic', '맑은 고딕', Arial, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + padding: 20px; + color: #333; +} + +.container { + max-width: 900px; + margin: 0 auto; + background: white; + border-radius: 10px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); + overflow: hidden; +} + +header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 30px; + text-align: center; + position: relative; +} + +.header-actions { + position: absolute; + top: 20px; + right: 20px; +} + +.logout-link { + color: white; + text-decoration: none; + padding: 8px 15px; + background: rgba(255, 255, 255, 0.2); + border-radius: 5px; + font-size: 0.9em; + transition: background 0.3s; +} + +.logout-link:hover { + background: rgba(255, 255, 255, 0.3); +} + +header h1 { + font-size: 2em; + margin-bottom: 10px; +} + +.subtitle { + font-size: 1.1em; + opacity: 0.9; +} + +main { + padding: 40px; +} + +.step-container { + max-width: 800px; + margin: 0 auto; +} + +.progress-bar { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 40px; + padding: 20px 0; +} + +.progress-step { + width: 40px; + height: 40px; + border-radius: 50%; + background: #e0e0e0; + color: #999; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + transition: all 0.3s; +} + +.progress-step.active { + background: #667eea; + color: white; + transform: scale(1.1); +} + +.progress-step.completed { + background: #4caf50; + color: white; +} + +.progress-line { + width: 80px; + height: 3px; + background: #e0e0e0; + margin: 0 10px; +} + +h2 { + color: #333; + margin-bottom: 30px; + font-size: 1.8em; +} + +.cult-name { + background: #f5f5f5; + padding: 15px; + border-radius: 5px; + margin-bottom: 30px; + font-size: 1.1em; +} + +.form-container { + background: #fafafa; + padding: 30px; + border-radius: 8px; +} + +.form-group { + margin-bottom: 25px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: 600; + color: #555; +} + +.form-group input[type="text"], +.form-group input[type="date"], +.form-group input[type="tel"], +.form-group select, +.form-group textarea { + width: 100%; + padding: 12px; + border: 2px solid #e0e0e0; + border-radius: 5px; + font-size: 1em; + font-family: inherit; + transition: border-color 0.3s; +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + outline: none; + border-color: #667eea; +} + +.form-group textarea { + resize: vertical; + min-height: 100px; +} + +.radio-group { + display: flex; + gap: 20px; + margin-top: 10px; + flex-wrap: wrap; +} + +.radio-group label { + display: flex; + align-items: center; + gap: 8px; + font-weight: normal; + cursor: pointer; +} + +.radio-group input[type="radio"] { + width: auto; + cursor: pointer; +} + +.doctrine-section { + background: white; + padding: 20px; + border-radius: 5px; + margin-bottom: 25px; + border-left: 4px solid #667eea; +} + +.doctrine-section h3 { + color: #667eea; + margin-bottom: 15px; + font-size: 1.3em; +} + +.help-text { + font-size: 0.9em; + color: #666; + margin-bottom: 10px; + font-style: italic; +} + +.form-actions { + display: flex; + gap: 15px; + justify-content: flex-end; + margin-top: 30px; + padding-top: 20px; + border-top: 2px solid #e0e0e0; +} + +.btn { + padding: 12px 30px; + border: none; + border-radius: 5px; + font-size: 1em; + font-weight: 600; + cursor: pointer; + transition: all 0.3s; +} + +.btn-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); +} + +.btn-secondary { + background: #e0e0e0; + color: #333; +} + +.btn-secondary:hover { + background: #d0d0d0; +} + +.complete-message { + text-align: center; + padding: 60px 20px; +} + +.checkmark { + width: 100px; + height: 100px; + border-radius: 50%; + background: #4caf50; + color: white; + font-size: 60px; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 30px; + animation: scaleIn 0.5s ease-out; +} + +@keyframes scaleIn { + from { + transform: scale(0); + } + to { + transform: scale(1); + } +} + +.complete-message h2 { + color: #4caf50; + margin-bottom: 15px; +} + +.complete-message p { + font-size: 1.1em; + color: #666; + margin-bottom: 10px; +} + +footer { + background: #f5f5f5; + padding: 20px; + text-align: center; + color: #666; + font-size: 0.9em; +} + +/* 모달 스타일 */ +.modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.modal-content { + background: white; + padding: 40px; + border-radius: 10px; + text-align: center; + max-width: 400px; +} + +.spinner { + border: 4px solid #f3f3f3; + border-top: 4px solid #667eea; + border-radius: 50%; + width: 50px; + height: 50px; + animation: spin 1s linear infinite; + margin: 0 auto 20px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.auth-container { + max-width: 500px; + margin: 0 auto; + padding: 40px 20px; + text-align: center; +} + +.auth-description { + color: #666; + margin-bottom: 10px; + font-size: 1em; +} + +.auth-form { + background: #fafafa; + padding: 30px; + border-radius: 8px; + margin-top: 30px; +} + +.error-message { + background: #ffebee; + color: #c62828; + padding: 15px; + border-radius: 5px; + margin: 20px 0; + border-left: 4px solid #c62828; + text-align: left; +} + +.welcome-message { + text-align: center; + padding: 40px 20px; +} + +.welcome-message h2 { + color: #667eea; + margin-bottom: 20px; +} + +.welcome-message p { + font-size: 1.1em; + color: #666; + margin-bottom: 15px; + line-height: 1.6; +} + +.info-box { + background: #f5f5f5; + border-left: 4px solid #667eea; + padding: 25px; + margin: 30px 0; + text-align: left; + border-radius: 5px; +} + +.info-box h3 { + color: #667eea; + margin-bottom: 15px; + font-size: 1.2em; +} + +.info-box ul { + list-style: none; + padding-left: 0; +} + +.info-box li { + padding: 8px 0; + padding-left: 25px; + position: relative; + color: #555; +} + +.info-box li:before { + content: "✓"; + position: absolute; + left: 0; + color: #4caf50; + font-weight: bold; +} + +.description-box { + margin-bottom: 30px; +} + +.description-text { + font-size: 1.1em; + font-weight: 600; + color: #333; + margin-bottom: 20px; +} + +.section-list { + list-style: decimal; + padding-left: 30px; + margin: 20px 0; + color: #555; +} + +.section-list li { + padding: 8px 0; + font-size: 1em; + line-height: 1.6; +} + +.description-note { + margin-top: 25px; + padding: 15px; + background: #fff3cd; + border-left: 4px solid #ffc107; + border-radius: 4px; + color: #856404; + font-size: 1em; + line-height: 1.8; + font-style: italic; +} + +/* 반응형 디자인 */ +@media (max-width: 768px) { + .container { + margin: 10px; + border-radius: 5px; + } + + main { + padding: 20px; + } + + .progress-bar { + flex-wrap: wrap; + } + + .progress-line { + width: 40px; + } + + .form-actions { + flex-direction: column; + } + + .btn { + width: 100%; + } +} + diff --git a/templates/auth.html b/templates/auth.html new file mode 100644 index 0000000..371c016 --- /dev/null +++ b/templates/auth.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

접근 인증

+

이 서비스는 인증키가 있는 사용자만 이용할 수 있습니다.

+

인증키를 입력해주세요.

+ + {% if error %} +
+ {{ error }} +
+ {% endif %} + +
+
+ + +
+ +
+ +
+
+
+
+{% endblock %} + diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..6f182d4 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,34 @@ + + + + + + 이단 탈퇴자 교리점검표 + + + +
+
+

이단 탈퇴자 교리점검표

+

기독교대한감리회 신앙고백에 따른 교리 점검

+ {% if session.get('authenticated') %} + + {% endif %} +
+ +
+ {% block content %}{% endblock %} +
+ +
+

© 2024 기독교대한감리회. All rights reserved.

+
+
+ + + {% block scripts %}{% endblock %} + + + diff --git a/templates/complete.html b/templates/complete.html new file mode 100644 index 0000000..0bed67f --- /dev/null +++ b/templates/complete.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+

제출이 완료되었습니다!

+

교리점검표가 성공적으로 제출되었습니다.

+

담당자가 검토 후 연락드리겠습니다.

+ +
+ +
+
+
+{% endblock %} + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..d9b2aef --- /dev/null +++ b/templates/index.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

환영합니다

+

이단 탈퇴자를 위한 교리점검표 설문에 오신 것을 환영합니다.

+

기독교대한감리회 신앙고백에 따른 교리 점검을 진행합니다.

+ +
+

총 4개의 섹션으로 되어 있습니다.

+ +
    +
  1. 감리회 신앙고백 점검
  2. +
  3. 이단 일반 점검
  4. +
  5. 이단 상세 점검
  6. +
  7. 상담 소감문
  8. +
+ +

+ 본 점검표는 평가의 목적이 아니며, 점수를 매기지도 않습니다. + 10주 양육에 들어가시기 전에 기본교리를 스스로 점검하는 차원에서 진행하는 것이니 + 부담없이 임해 주시면 감사하겠습니다. +

+
+ +
+

설문 안내

+
    +
  • 총 5단계로 구성되어 있습니다
  • +
  • 각 단계는 약 5-10분 정도 소요됩니다
  • +
  • 진행 중 이전 단계로 돌아갈 수 있습니다
  • +
  • 모든 항목은 필수 입력 사항입니다
  • +
+
+ +
+ +
+
+
+{% endblock %} + diff --git a/templates/step1.html b/templates/step1.html new file mode 100644 index 0000000..01baf6d --- /dev/null +++ b/templates/step1.html @@ -0,0 +1,81 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
1
+
+
2
+
+
3
+
+
4
+
+
5
+
+ +

1단계: 기본 정보 입력

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} + diff --git a/templates/step2.html b/templates/step2.html new file mode 100644 index 0000000..169426e --- /dev/null +++ b/templates/step2.html @@ -0,0 +1,145 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
1
+
+
2
+
+
3
+
+
4
+
+
5
+
+ +

2단계: 기독교대한감리회 신앙고백 교리 점검

+ +
+
+
+ +
+ + + +
+
+
+ +
+
+ +
+ + + +
+
+
+ +
+
+ +
+ + + +
+
+
+ +
+
+ +
+ + + +
+
+
+ +
+
+ +
+ + + +
+
+
+ +
+
+ +
+ + + +
+
+
+ +
+
+ +
+ + + +
+
+
+ +
+
+ +
+ + + +
+
+
+ +
+ + +
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} + diff --git a/templates/step3.html b/templates/step3.html new file mode 100644 index 0000000..2146c48 --- /dev/null +++ b/templates/step3.html @@ -0,0 +1,247 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
1
+
+
2
+
+
3
+
+
4
+
+
5
+
+ +

3단계: 이단 일반 교리 점검

+ +
+
+
+ +
+ + + +
+
+
+ +
+
+ +
+ + + +
+
+
+ +
+
+ +
+ + + +
+
+
+ +
+
+ +
+ + + +
+
+
+ +
+
+ +
+ + + +
+
+
+ +
+
+ +
+ + + +
+
+
+ +
+
+ +

※ 이 문항은 아래 "기타의견"란에 "7번: "으로 시작하여 답변해주세요.

+
+
+ +
+
+ +
+ + + +
+
+
+ +
+
+ +
+ + + +
+
+
+ +
+
+ +
+ + + +
+
+
+ +
+
+ +
+ + + +
+
+
+ +
+
+ +
+ + + +
+
+
+ +
+
+ +
+ + + +
+
+
+ +
+
+ +
+ + + +
+
+
+ +
+
+ +
+ + + +
+
+
+ +
+
+ +
+ + + +
+
+
+ +
+
+ +
+ + + +
+
+
+ +
+
+ + +
+
+ +
+ + +
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} + diff --git a/templates/step4.html b/templates/step4.html new file mode 100644 index 0000000..f617f71 --- /dev/null +++ b/templates/step4.html @@ -0,0 +1,78 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
1
+
+
2
+
+
3
+
+
4
+
+
5
+
+ +

4단계: 이단 상세 점검

+

출신 이단: {{ cult_name }}

+ +
+ {% for question in questions %} +
+
+ + {% if '기타의견란' in question or '기타의견' in question %} +

※ 이 문항은 아래 "기타의견"란에 "{{ loop.index }}번: "으로 시작하여 답변해주세요.

+ {% else %} + + {% endif %} +
+
+ {% endfor %} + +
+
+ + +
+
+ +
+ + +
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} + diff --git a/templates/step5.html b/templates/step5.html new file mode 100644 index 0000000..c21722f --- /dev/null +++ b/templates/step5.html @@ -0,0 +1,79 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
1
+
+
2
+
+
3
+
+
4
+
+
5
+
+ +

5단계: 간증문 입력

+ +
+
+ +

이단에서 탈퇴하고 정통 기독교로 돌아온 과정과 소감을 자유롭게 작성해주세요.

+ +
+ +
+ + +
+
+
+ + +{% endblock %} + +{% block scripts %} + +{% endblock %} + diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..13fa07c --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1,2 @@ +# Utils package + diff --git a/utils/google_drive.py b/utils/google_drive.py new file mode 100644 index 0000000..4774d65 --- /dev/null +++ b/utils/google_drive.py @@ -0,0 +1,66 @@ +import os +from google.oauth2.credentials import Credentials +from google_auth_oauthlib.flow import InstalledAppFlow +from google.auth.transport.requests import Request +from googleapiclient.discovery import build +from googleapiclient.http import MediaFileUpload +import pickle + +SCOPES = ['https://www.googleapis.com/auth/drive.file'] + +class GoogleDriveUploader: + def __init__(self, credentials_file, token_file): + self.credentials_file = credentials_file + self.token_file = token_file + self.service = None + self._authenticate() + + def _authenticate(self): + """구글 드라이브 인증""" + creds = None + + # 기존 토큰 파일이 있으면 로드 + if os.path.exists(self.token_file): + with open(self.token_file, 'rb') as token: + creds = pickle.load(token) + + # 유효한 인증 정보가 없으면 새로 인증 + if not creds or not creds.valid: + if creds and creds.expired and creds.refresh_token: + creds.refresh(Request()) + else: + if not os.path.exists(self.credentials_file): + raise FileNotFoundError( + f"구글 드라이브 인증 파일을 찾을 수 없습니다: {self.credentials_file}\n" + "Google Cloud Console에서 OAuth 2.0 클라이언트 ID를 다운로드하여 " + "credentials.json으로 저장해주세요." + ) + flow = InstalledAppFlow.from_client_secrets_file( + self.credentials_file, SCOPES) + creds = flow.run_local_server(port=0) + + # 토큰 저장 + with open(self.token_file, 'wb') as token: + pickle.dump(creds, token) + + self.service = build('drive', 'v3', credentials=creds) + + def upload_file(self, file_path, file_name, folder_id=None): + """파일을 구글 드라이브에 업로드""" + if not self.service: + raise Exception("구글 드라이브 서비스가 초기화되지 않았습니다.") + + file_metadata = {'name': file_name} + if folder_id: + file_metadata['parents'] = [folder_id] + + media = MediaFileUpload(file_path, mimetype='application/vnd.openxmlformats-officedocument.wordprocessingml.document') + + file = self.service.files().create( + body=file_metadata, + media_body=media, + fields='id' + ).execute() + + return file.get('id') + diff --git a/utils/word_processor.py b/utils/word_processor.py new file mode 100644 index 0000000..f56787a --- /dev/null +++ b/utils/word_processor.py @@ -0,0 +1,118 @@ +from docx import Document +from docx.shared import Pt +from docx.enum.text import WD_ALIGN_PARAGRAPH +import os +from datetime import datetime + +class WordProcessor: + def __init__(self, template_dir): + self.template_dir = template_dir + self.output_dir = 'output' + os.makedirs(self.output_dir, exist_ok=True) + + def generate_document(self, data, cult_name='기타'): + """데이터를 워드 문서로 생성""" + # 템플릿 파일 경로 + template_file = os.path.join(self.template_dir, f'{cult_name}_template.docx') + + # 템플릿이 없으면 기본 템플릿 사용 + if not os.path.exists(template_file): + template_file = os.path.join(self.template_dir, 'default_template.docx') + + # 템플릿이 있으면 사용, 없으면 새 문서 생성 + if os.path.exists(template_file): + doc = Document(template_file) + else: + doc = Document() + self._create_default_template(doc) + + # 데이터 채우기 + self._fill_document(doc, data) + + # 출력 파일명 + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + name = data.get('basic_info', {}).get('name', '무명') + output_filename = f'교리점검표_{name}_{timestamp}.docx' + output_path = os.path.join(self.output_dir, output_filename) + + doc.save(output_path) + return output_path + + def _create_default_template(self, doc): + """기본 템플릿 생성""" + # 제목 + title = doc.add_heading('이단 탈퇴자 교리점검표', 0) + title.alignment = WD_ALIGN_PARAGRAPH.CENTER + + # 기본 정보 섹션 + doc.add_heading('1. 기본 정보', level=1) + doc.add_paragraph('이름: {name}') + doc.add_paragraph('교구: {district}') + doc.add_paragraph('이단교단: {cult}') + + # 기독교대한감리회 신앙고백 + doc.add_heading('2. 기독교대한감리회 신앙고백 교리 점검', level=1) + doc.add_paragraph('{methodist_doctrine}') + + # 이단 일반 교리 점검 + doc.add_heading('3. 이단 일반 교리 점검', level=1) + doc.add_paragraph('{general_cult_doctrine}') + + # 출신 이단별 교리 점검 + doc.add_heading('4. 출신 이단별 교리 점검', level=1) + doc.add_paragraph('{specific_cult_doctrine}') + + # 간증문 + doc.add_heading('5. 간증문', level=1) + doc.add_paragraph('{testimony}') + + doc.add_paragraph('') + doc.add_paragraph(f'작성일: {datetime.now().strftime("%Y년 %m월 %d일")}') + + def _fill_document(self, doc, data): + """문서에 데이터 채우기""" + basic_info = data.get('basic_info', {}) + methodist = data.get('methodist_doctrine', {}) + general = data.get('general_cult_doctrine', {}) + specific = data.get('specific_cult_doctrine', {}) + testimony = data.get('testimony', {}) + + # 모든 단락을 순회하며 플레이스홀더 교체 + replacements = { + '{name}': basic_info.get('name', ''), + '{district}': basic_info.get('district', ''), + '{cult}': basic_info.get('cult', ''), + '{methodist_doctrine}': self._format_answers(methodist), + '{general_cult_doctrine}': self._format_answers(general), + '{specific_cult_doctrine}': self._format_answers(specific), + '{testimony}': testimony.get('content', '') + } + + for paragraph in doc.paragraphs: + for key, value in replacements.items(): + if key in paragraph.text: + paragraph.text = paragraph.text.replace(key, str(value)) + + # 테이블도 처리 + for table in doc.tables: + for row in table.rows: + for cell in row.cells: + for key, value in replacements.items(): + if key in cell.text: + cell.text = cell.text.replace(key, str(value)) + + def _format_answers(self, answers_dict): + """답변 딕셔너리를 텍스트로 포맷팅""" + if not answers_dict: + return '답변 없음' + + result = [] + for key, value in answers_dict.items(): + if isinstance(value, dict): + # 중첩된 구조 처리 + result.append(f"{key}: {self._format_answers(value)}") + else: + result.append(f"{key}: {value}") + + return '\n'.join(result) + diff --git a/word_templates/.gitkeep b/word_templates/.gitkeep new file mode 100644 index 0000000..781f09e --- /dev/null +++ b/word_templates/.gitkeep @@ -0,0 +1,3 @@ +# 이 디렉토리에 워드 템플릿 파일을 저장하세요 +# 예: 신천지_template.docx, 구원파_template.docx 등 +