first commit

This commit is contained in:
peregr1nus
2025-12-10 13:27:48 +09:00
commit 8019a7e4ba
19 changed files with 2028 additions and 0 deletions

170
README.md Normal file
View File

@@ -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`로 변경할 수 있습니다
- 사용자는 서비스 접속 시 인증 페이지에서 인증키를 입력해야 합니다
- 인증 후 세션에 저장되며, 로그아웃 시 인증이 해제됩니다
## 라이선스
이 프로젝트는 기독교대한감리회를 위한 내부 사용 목적으로 제작되었습니다.

202
app.py Normal file
View File

@@ -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)

204
config.py Normal file
View File

@@ -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. 구원이란 인간을 억누르고 있는 귀신의 세력으로부터 자유함을 입는 것이라고 생각하십니까?'
]
}

8
requirements.txt Normal file
View File

@@ -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

27
static/script.js Normal file
View File

@@ -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';
}
}

475
static/style.css Normal file
View File

@@ -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%;
}
}

29
templates/auth.html Normal file
View File

@@ -0,0 +1,29 @@
{% extends "base.html" %}
{% block content %}
<div class="step-container">
<div class="auth-container">
<h2>접근 인증</h2>
<p class="auth-description">이 서비스는 인증키가 있는 사용자만 이용할 수 있습니다.</p>
<p class="auth-description">인증키를 입력해주세요.</p>
{% if error %}
<div class="error-message">
{{ error }}
</div>
{% endif %}
<form method="POST" class="auth-form">
<div class="form-group">
<label for="access_key">인증키 *</label>
<input type="text" id="access_key" name="access_key" required autofocus placeholder="인증키를 입력하세요">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">인증하기</button>
</div>
</form>
</div>
</div>
{% endblock %}

34
templates/base.html Normal file
View File

@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>이단 탈퇴자 교리점검표</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="container">
<header>
<h1>이단 탈퇴자 교리점검표</h1>
<p class="subtitle">기독교대한감리회 신앙고백에 따른 교리 점검</p>
{% if session.get('authenticated') %}
<div class="header-actions">
<a href="{{ url_for('logout') }}" class="logout-link">로그아웃</a>
</div>
{% endif %}
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<p>&copy; 2024 기독교대한감리회. All rights reserved.</p>
</footer>
</div>
<script src="{{ url_for('static', filename='script.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>

17
templates/complete.html Normal file
View File

@@ -0,0 +1,17 @@
{% extends "base.html" %}
{% block content %}
<div class="step-container">
<div class="complete-message">
<div class="checkmark"></div>
<h2>제출이 완료되었습니다!</h2>
<p>교리점검표가 성공적으로 제출되었습니다.</p>
<p>담당자가 검토 후 연락드리겠습니다.</p>
<div class="form-actions">
<button type="button" class="btn btn-primary" onclick="window.location.href='/'">처음으로</button>
</div>
</div>
</div>
{% endblock %}

43
templates/index.html Normal file
View File

@@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block content %}
<div class="step-container">
<div class="welcome-message">
<h2>환영합니다</h2>
<p>이단 탈퇴자를 위한 교리점검표 설문에 오신 것을 환영합니다.</p>
<p>기독교대한감리회 신앙고백에 따른 교리 점검을 진행합니다.</p>
<div class="info-box description-box">
<p class="description-text">총 4개의 섹션으로 되어 있습니다.</p>
<ol class="section-list">
<li>감리회 신앙고백 점검</li>
<li>이단 일반 점검</li>
<li>이단 상세 점검</li>
<li>상담 소감문</li>
</ol>
<p class="description-note">
본 점검표는 평가의 목적이 아니며, 점수를 매기지도 않습니다.
10주 양육에 들어가시기 전에 기본교리를 스스로 점검하는 차원에서 진행하는 것이니
부담없이 임해 주시면 감사하겠습니다.
</p>
</div>
<div class="info-box">
<h3>설문 안내</h3>
<ul>
<li>총 5단계로 구성되어 있습니다</li>
<li>각 단계는 약 5-10분 정도 소요됩니다</li>
<li>진행 중 이전 단계로 돌아갈 수 있습니다</li>
<li>모든 항목은 필수 입력 사항입니다</li>
</ul>
</div>
<div class="form-actions">
<button type="button" class="btn btn-primary" onclick="window.location.href='/step1'">설문 시작하기</button>
</div>
</div>
</div>
{% endblock %}

81
templates/step1.html Normal file
View File

@@ -0,0 +1,81 @@
{% extends "base.html" %}
{% block content %}
<div class="step-container">
<div class="progress-bar">
<div class="progress-step active">1</div>
<div class="progress-line"></div>
<div class="progress-step">2</div>
<div class="progress-line"></div>
<div class="progress-step">3</div>
<div class="progress-line"></div>
<div class="progress-step">4</div>
<div class="progress-line"></div>
<div class="progress-step">5</div>
</div>
<h2>1단계: 기본 정보 입력</h2>
<form id="step1Form" class="form-container">
<div class="form-group">
<label for="name">이름 *</label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-group">
<label for="district">교구 *</label>
<select id="district" name="district" required>
<option value="">선택하세요</option>
{% for district in districts %}
<option value="{{ district }}">{{ district }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="cult">이단교단 *</label>
<select id="cult" name="cult" required>
<option value="">선택하세요</option>
{% for cult in cults %}
<option value="{{ cult }}">{{ cult }}</option>
{% endfor %}
</select>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">다음 단계</button>
</div>
</form>
</div>
{% endblock %}
{% block scripts %}
<script>
document.getElementById('step1Form').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = Object.fromEntries(formData);
try {
const response = await fetch('/step1', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
window.location.href = result.next_step;
} else {
alert('오류가 발생했습니다: ' + (result.message || '알 수 없는 오류'));
}
} catch (error) {
alert('오류가 발생했습니다: ' + error.message);
}
});
</script>
{% endblock %}

145
templates/step2.html Normal file
View File

@@ -0,0 +1,145 @@
{% extends "base.html" %}
{% block content %}
<div class="step-container">
<div class="progress-bar">
<div class="progress-step completed">1</div>
<div class="progress-line"></div>
<div class="progress-step active">2</div>
<div class="progress-line"></div>
<div class="progress-step">3</div>
<div class="progress-line"></div>
<div class="progress-step">4</div>
<div class="progress-line"></div>
<div class="progress-step">5</div>
</div>
<h2>2단계: 기독교대한감리회 신앙고백 교리 점검</h2>
<form id="step2Form" class="form-container">
<div class="doctrine-section">
<div class="form-group">
<label>1. 우주 만물을 창조하시고 섭리하시며 주관하시는 거룩하시고 자비하시며 오직 한 분이신 아버지 하나님을 믿습니까?</label>
<div class="radio-group">
<label><input type="radio" name="q1_father_god" value="예" required></label>
<label><input type="radio" name="q1_father_god" value="아니오"> 아니오</label>
<label><input type="radio" name="q1_father_god" value="모르겠습니다"> 모르겠습니다</label>
</div>
</div>
</div>
<div class="doctrine-section">
<div class="form-group">
<label>2. 말씀이 육신이 되어 우리 가운데 오셔서 하나님의 나라를 선포하시고 십자가에 달려 죽으셨다가 부활승천 하심으로 대속자가 되시고 구세주가 되시는 예수 그리스도를 믿습니까?</label>
<div class="radio-group">
<label><input type="radio" name="q2_jesus_christ" value="예" required></label>
<label><input type="radio" name="q2_jesus_christ" value="아니오"> 아니오</label>
<label><input type="radio" name="q2_jesus_christ" value="모르겠습니다"> 모르겠습니다</label>
</div>
</div>
</div>
<div class="doctrine-section">
<div class="form-group">
<label>3. 우리와 함께 계셔서 우리를 거듭나게 하시고 거룩하게 하시며 완전하게 하시며 위안과 힘이 되시는 성령을 믿습니까?</label>
<div class="radio-group">
<label><input type="radio" name="q3_holy_spirit" value="예" required></label>
<label><input type="radio" name="q3_holy_spirit" value="아니오"> 아니오</label>
<label><input type="radio" name="q3_holy_spirit" value="모르겠습니다"> 모르겠습니다</label>
</div>
</div>
</div>
<div class="doctrine-section">
<div class="form-group">
<label>4. 성령의 감동으로 기록된 하나님의 말씀인 성경이 구원에 이르는 도리와 신앙생활에 충분한 표준이 됨을 믿습니까?</label>
<div class="radio-group">
<label><input type="radio" name="q4_bible" value="예" required></label>
<label><input type="radio" name="q4_bible" value="아니오"> 아니오</label>
<label><input type="radio" name="q4_bible" value="모르겠습니다"> 모르겠습니다</label>
</div>
</div>
</div>
<div class="doctrine-section">
<div class="form-group">
<label>5. 하나님의 은혜로 믿음을 통해 죄사함을 받아 거룩해지며 하나님의 구원의 역사에 동참하도록 부름받음을 믿습니까?</label>
<div class="radio-group">
<label><input type="radio" name="q5_salvation" value="예" required></label>
<label><input type="radio" name="q5_salvation" value="아니오"> 아니오</label>
<label><input type="radio" name="q5_salvation" value="모르겠습니다"> 모르겠습니다</label>
</div>
</div>
</div>
<div class="doctrine-section">
<div class="form-group">
<label>6. 예배와 친교, 교육과 봉사, 전도와 선교를 위해 하나가 된 그리스도의 몸인 교회를 믿습니까?</label>
<div class="radio-group">
<label><input type="radio" name="q6_church" value="예" required></label>
<label><input type="radio" name="q6_church" value="아니오"> 아니오</label>
<label><input type="radio" name="q6_church" value="모르겠습니다"> 모르겠습니다</label>
</div>
</div>
</div>
<div class="doctrine-section">
<div class="form-group">
<label>7. 만민에게 복음을 전파함으로 하나님의 정의와 사랑을 나누고 평화의 세계를 이루는 모든 사람들이 하나님 앞에 형제됨을 믿습니까?</label>
<div class="radio-group">
<label><input type="radio" name="q7_mission" value="예" required></label>
<label><input type="radio" name="q7_mission" value="아니오"> 아니오</label>
<label><input type="radio" name="q7_mission" value="모르겠습니다"> 모르겠습니다</label>
</div>
</div>
</div>
<div class="doctrine-section">
<div class="form-group">
<label>8. 예수 그리스도의 재림과 심판 우리 몸의 부활과 영생 그리고 의의 최후 승리와 영원한 하나님 나라를 믿습니까?</label>
<div class="radio-group">
<label><input type="radio" name="q8_second_coming" value="예" required></label>
<label><input type="radio" name="q8_second_coming" value="아니오"> 아니오</label>
<label><input type="radio" name="q8_second_coming" value="모르겠습니다"> 모르겠습니다</label>
</div>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="window.location.href='/step1'">이전</button>
<button type="submit" class="btn btn-primary">다음 단계</button>
</div>
</form>
</div>
{% endblock %}
{% block scripts %}
<script>
document.getElementById('step2Form').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = Object.fromEntries(formData);
try {
const response = await fetch('/step2', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
window.location.href = result.next_step;
} else {
alert('오류가 발생했습니다: ' + (result.message || '알 수 없는 오류'));
}
} catch (error) {
alert('오류가 발생했습니다: ' + error.message);
}
});
</script>
{% endblock %}

247
templates/step3.html Normal file
View File

@@ -0,0 +1,247 @@
{% extends "base.html" %}
{% block content %}
<div class="step-container">
<div class="progress-bar">
<div class="progress-step completed">1</div>
<div class="progress-line"></div>
<div class="progress-step completed">2</div>
<div class="progress-line"></div>
<div class="progress-step active">3</div>
<div class="progress-line"></div>
<div class="progress-step">4</div>
<div class="progress-line"></div>
<div class="progress-step">5</div>
</div>
<h2>3단계: 이단 일반 교리 점검</h2>
<form id="step3Form" class="form-container">
<div class="doctrine-section">
<div class="form-group">
<label>1. 우리의 구원을 위해서 성경 이외의 가르침이 필요하다고 생각하십니까?</label>
<div class="radio-group">
<label><input type="radio" name="q1_additional_teaching" value="예" required></label>
<label><input type="radio" name="q1_additional_teaching" value="아니오"> 아니오</label>
<label><input type="radio" name="q1_additional_teaching" value="모르겠습니다"> 모르겠습니다</label>
</div>
</div>
</div>
<div class="doctrine-section">
<div class="form-group">
<label>2. 어떤 특별한 성경 번역본만이 진리이고 다른 번역본에는 문제가 있다고 생각하십니까?</label>
<div class="radio-group">
<label><input type="radio" name="q2_special_bible" value="예" required></label>
<label><input type="radio" name="q2_special_bible" value="아니오"> 아니오</label>
<label><input type="radio" name="q2_special_bible" value="모르겠습니다"> 모르겠습니다</label>
</div>
</div>
</div>
<div class="doctrine-section">
<div class="form-group">
<label>3. 성경의 진리 가운데 그동안 숨겨져 온 부분이 있고, 그 내용을 계시 받은 특별한 사람이 있다고 믿고 있습니까?</label>
<div class="radio-group">
<label><input type="radio" name="q3_hidden_truth" value="예" required></label>
<label><input type="radio" name="q3_hidden_truth" value="아니오"> 아니오</label>
<label><input type="radio" name="q3_hidden_truth" value="모르겠습니다"> 모르겠습니다</label>
</div>
</div>
</div>
<div class="doctrine-section">
<div class="form-group">
<label>4. 성경을 해석하기 위해서 비유가 매우 중요하다고 생각하십니까? 비유를 깨닫기 위해서 별도의 성경공부가 필요하다고 생각하십니까?</label>
<div class="radio-group">
<label><input type="radio" name="q4_parable" value="예" required></label>
<label><input type="radio" name="q4_parable" value="아니오"> 아니오</label>
<label><input type="radio" name="q4_parable" value="모르겠습니다"> 모르겠습니다</label>
</div>
</div>
</div>
<div class="doctrine-section">
<div class="form-group">
<label>5. 성경의 내용들 가운데 모든 것이 서로 짝이 있습니까?</label>
<div class="radio-group">
<label><input type="radio" name="q5_pairs" value="예" required></label>
<label><input type="radio" name="q5_pairs" value="아니오"> 아니오</label>
<label><input type="radio" name="q5_pairs" value="모르겠습니다"> 모르겠습니다</label>
</div>
</div>
</div>
<div class="doctrine-section">
<div class="form-group">
<label>6. 구세주 되시는 예수님은 모든 인류의 구원을 위해서 죽으셨는데 아직도 특별한 사람을 대리자로 세워서 구원사역을 이루실 필요가 있습니까?</label>
<div class="radio-group">
<label><input type="radio" name="q6_mediator" value="예" required></label>
<label><input type="radio" name="q6_mediator" value="아니오"> 아니오</label>
<label><input type="radio" name="q6_mediator" value="모르겠습니다"> 모르겠습니다</label>
</div>
</div>
</div>
<div class="doctrine-section">
<div class="form-group">
<label>7. 나는 죄인이라고 생각하십니까? 나의 구원은 확실하십니까? 그 이유는 무엇입니까? (이 문항은 아래 기타의견란에 문항번호를 적은 후 서술해 주세요)</label>
<p class="help-text">※ 이 문항은 아래 "기타의견"란에 "7번: "으로 시작하여 답변해주세요.</p>
</div>
</div>
<div class="doctrine-section">
<div class="form-group">
<label>8. 우리는 구원 받았을 때 회개했습니다. 그리고 이후에 다시 회개가 필요합니까?</label>
<div class="radio-group">
<label><input type="radio" name="q8_repentance" value="예" required></label>
<label><input type="radio" name="q8_repentance" value="아니오"> 아니오</label>
<label><input type="radio" name="q8_repentance" value="모르겠습니다"> 모르겠습니다</label>
</div>
</div>
</div>
<div class="doctrine-section">
<div class="form-group">
<label>9. 우리가 주일을 성수하는 것은 성경적입니까?</label>
<div class="radio-group">
<label><input type="radio" name="q9_sunday" value="예" required></label>
<label><input type="radio" name="q9_sunday" value="아니오"> 아니오</label>
<label><input type="radio" name="q9_sunday" value="모르겠습니다"> 모르겠습니다</label>
</div>
</div>
</div>
<div class="doctrine-section">
<div class="form-group">
<label>10. 안식일을 지키는 것이 구원의 조건입니까?</label>
<div class="radio-group">
<label><input type="radio" name="q10_sabbath" value="예" required></label>
<label><input type="radio" name="q10_sabbath" value="아니오"> 아니오</label>
<label><input type="radio" name="q10_sabbath" value="모르겠습니다"> 모르겠습니다</label>
</div>
</div>
</div>
<div class="doctrine-section">
<div class="form-group">
<label>11. 성탄절은 예수님이 탄생한 날입니다. 12월 25일에 기념해도 되는 겁니까?</label>
<div class="radio-group">
<label><input type="radio" name="q11_christmas" value="예" required></label>
<label><input type="radio" name="q11_christmas" value="아니오"> 아니오</label>
<label><input type="radio" name="q11_christmas" value="모르겠습니다"> 모르겠습니다</label>
</div>
</div>
</div>
<div class="doctrine-section">
<div class="form-group">
<label>12. 예수 그리스도의 재림의 때가 구체적으로 언제인지 정해져 있다고 믿으십니까?</label>
<div class="radio-group">
<label><input type="radio" name="q12_second_coming_date" value="예" required></label>
<label><input type="radio" name="q12_second_coming_date" value="아니오"> 아니오</label>
<label><input type="radio" name="q12_second_coming_date" value="모르겠습니다"> 모르겠습니다</label>
</div>
</div>
</div>
<div class="doctrine-section">
<div class="form-group">
<label>13. 성경에서 말하는 동방은 한국을 말하는 것이 맞습니까?</label>
<div class="radio-group">
<label><input type="radio" name="q13_east" value="예" required></label>
<label><input type="radio" name="q13_east" value="아니오"> 아니오</label>
<label><input type="radio" name="q13_east" value="모르겠습니다"> 모르겠습니다</label>
</div>
</div>
</div>
<div class="doctrine-section">
<div class="form-group">
<label>14. 당신의 종교적 신념을 지키는 것이 가정을 지키는 것보다 더 중요합니까?</label>
<div class="radio-group">
<label><input type="radio" name="q14_family" value="예" required></label>
<label><input type="radio" name="q14_family" value="아니오"> 아니오</label>
<label><input type="radio" name="q14_family" value="모르겠습니다"> 모르겠습니다</label>
</div>
</div>
</div>
<div class="doctrine-section">
<div class="form-group">
<label>15. 당신이 믿는 바를 전하고 사명을 감당하기 위해서 모략을 사용하는 것이 바람직합니까?</label>
<div class="radio-group">
<label><input type="radio" name="q15_deception" value="예" required></label>
<label><input type="radio" name="q15_deception" value="아니오"> 아니오</label>
<label><input type="radio" name="q15_deception" value="모르겠습니다"> 모르겠습니다</label>
</div>
</div>
</div>
<div class="doctrine-section">
<div class="form-group">
<label>16. 주일성수, 새벽기도, 십일조 등 기존의 교회제도는 율법의 산물이기에 지킬 필요가 없다고 생각하십니까?</label>
<div class="radio-group">
<label><input type="radio" name="q16_church_system" value="예" required></label>
<label><input type="radio" name="q16_church_system" value="아니오"> 아니오</label>
<label><input type="radio" name="q16_church_system" value="모르겠습니다"> 모르겠습니다</label>
</div>
</div>
</div>
<div class="doctrine-section">
<div class="form-group">
<label>17. 사람이 질병에 걸리는 이유는 귀신의 영향 때문입니까?</label>
<div class="radio-group">
<label><input type="radio" name="q17_disease" value="예" required></label>
<label><input type="radio" name="q17_disease" value="아니오"> 아니오</label>
<label><input type="radio" name="q17_disease" value="모르겠습니다"> 모르겠습니다</label>
</div>
</div>
</div>
<div class="doctrine-section">
<div class="form-group">
<label for="other_opinions">기타의견 (7번 문항 답변 포함)</label>
<textarea id="other_opinions" name="other_opinions" rows="5" placeholder="7번 문항 답변 및 기타 의견을 작성해주세요. 7번 문항은 '7번: '으로 시작하여 답변해주세요."></textarea>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="window.location.href='/step2'">이전</button>
<button type="submit" class="btn btn-primary">다음 단계</button>
</div>
</form>
</div>
{% endblock %}
{% block scripts %}
<script>
document.getElementById('step3Form').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = Object.fromEntries(formData);
try {
const response = await fetch('/step3', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
window.location.href = result.next_step;
} else {
alert('오류가 발생했습니다: ' + (result.message || '알 수 없는 오류'));
}
} catch (error) {
alert('오류가 발생했습니다: ' + error.message);
}
});
</script>
{% endblock %}

78
templates/step4.html Normal file
View File

@@ -0,0 +1,78 @@
{% extends "base.html" %}
{% block content %}
<div class="step-container">
<div class="progress-bar">
<div class="progress-step completed">1</div>
<div class="progress-line"></div>
<div class="progress-step completed">2</div>
<div class="progress-line"></div>
<div class="progress-step completed">3</div>
<div class="progress-line"></div>
<div class="progress-step active">4</div>
<div class="progress-line"></div>
<div class="progress-step">5</div>
</div>
<h2>4단계: 이단 상세 점검</h2>
<p class="cult-name">출신 이단: <strong>{{ cult_name }}</strong></p>
<form id="step4Form" class="form-container">
{% for question in questions %}
<div class="doctrine-section">
<div class="form-group">
<label>{{ question }}</label>
{% if '기타의견란' in question or '기타의견' in question %}
<p class="help-text">※ 이 문항은 아래 "기타의견"란에 "{{ loop.index }}번: "으로 시작하여 답변해주세요.</p>
{% else %}
<textarea name="q{{ loop.index }}" rows="4" required placeholder="답변을 작성해주세요"></textarea>
{% endif %}
</div>
</div>
{% endfor %}
<div class="doctrine-section">
<div class="form-group">
<label for="other_opinions">기타의견 (3번 문항 답변 포함)</label>
<textarea id="other_opinions" name="other_opinions" rows="5" placeholder="3번 문항 답변 및 기타 의견을 작성해주세요. 3번 문항은 '3번: '으로 시작하여 답변해주세요."></textarea>
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="window.location.href='/step3'">이전</button>
<button type="submit" class="btn btn-primary">다음 단계</button>
</div>
</form>
</div>
{% endblock %}
{% block scripts %}
<script>
document.getElementById('step4Form').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = Object.fromEntries(formData);
try {
const response = await fetch('/step4', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
window.location.href = result.next_step;
} else {
alert('오류가 발생했습니다: ' + (result.message || '알 수 없는 오류'));
}
} catch (error) {
alert('오류가 발생했습니다: ' + error.message);
}
});
</script>
{% endblock %}

79
templates/step5.html Normal file
View File

@@ -0,0 +1,79 @@
{% extends "base.html" %}
{% block content %}
<div class="step-container">
<div class="progress-bar">
<div class="progress-step completed">1</div>
<div class="progress-line"></div>
<div class="progress-step completed">2</div>
<div class="progress-line"></div>
<div class="progress-step completed">3</div>
<div class="progress-line"></div>
<div class="progress-step completed">4</div>
<div class="progress-line"></div>
<div class="progress-step active">5</div>
</div>
<h2>5단계: 간증문 입력</h2>
<form id="step5Form" class="form-container">
<div class="form-group">
<label for="testimony">간증문 *</label>
<p class="help-text">이단에서 탈퇴하고 정통 기독교로 돌아온 과정과 소감을 자유롭게 작성해주세요.</p>
<textarea id="testimony" name="content" rows="15" required placeholder="간증문을 작성해주세요..."></textarea>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="window.location.href='/step4'">이전</button>
<button type="submit" class="btn btn-primary">제출하기</button>
</div>
</form>
</div>
<div id="loadingModal" class="modal" style="display: none;">
<div class="modal-content">
<div class="spinner"></div>
<p>문서를 생성하고 업로드하는 중입니다...</p>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.getElementById('step5Form').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = Object.fromEntries(formData);
// 로딩 모달 표시
document.getElementById('loadingModal').style.display = 'flex';
try {
const response = await fetch('/step5', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
const result = await response.json();
// 로딩 모달 숨기기
document.getElementById('loadingModal').style.display = 'none';
if (result.success) {
alert(result.message);
window.location.href = '/complete';
} else {
alert('오류가 발생했습니다: ' + (result.message || '알 수 없는 오류'));
}
} catch (error) {
document.getElementById('loadingModal').style.display = 'none';
alert('오류가 발생했습니다: ' + error.message);
}
});
</script>
{% endblock %}

2
utils/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
# Utils package

66
utils/google_drive.py Normal file
View File

@@ -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')

118
utils/word_processor.py Normal file
View File

@@ -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)

3
word_templates/.gitkeep Normal file
View File

@@ -0,0 +1,3 @@
# 이 디렉토리에 워드 템플릿 파일을 저장하세요
# 예: 신천지_template.docx, 구원파_template.docx 등