Compare commits
No commits in common. "bfc5c992dea67a5d1ea571cf06825c920b8a49aa" and "dbd6f4f841216cd06cd3793e4b5e02e453e3e49f" have entirely different histories.
bfc5c992de
...
dbd6f4f841
265
app.py
265
app.py
@ -64,11 +64,6 @@ def init_db():
|
|||||||
def index():
|
def index():
|
||||||
return render_template('index.html')
|
return render_template('index.html')
|
||||||
|
|
||||||
@app.route('/survey/<survey_token>')
|
|
||||||
def survey_page(survey_token):
|
|
||||||
"""문진표 페이지 (모바일)"""
|
|
||||||
return render_template('survey.html')
|
|
||||||
|
|
||||||
# ==================== 환자 관리 API ====================
|
# ==================== 환자 관리 API ====================
|
||||||
|
|
||||||
@app.route('/api/patients', methods=['GET'])
|
@app.route('/api/patients', methods=['GET'])
|
||||||
@ -325,57 +320,18 @@ def create_formula():
|
|||||||
|
|
||||||
@app.route('/api/formulas/<int:formula_id>/ingredients', methods=['GET'])
|
@app.route('/api/formulas/<int:formula_id>/ingredients', methods=['GET'])
|
||||||
def get_formula_ingredients(formula_id):
|
def get_formula_ingredients(formula_id):
|
||||||
"""처방 구성 약재 조회 (ingredient_code 기반, 사용 가능한 모든 제품 포함)"""
|
"""처방 구성 약재 조회"""
|
||||||
try:
|
try:
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# 처방 구성 약재 조회 (ingredient_code 기반)
|
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT
|
SELECT fi.*, h.herb_name, h.insurance_code
|
||||||
fi.ingredient_id,
|
|
||||||
fi.formula_id,
|
|
||||||
fi.ingredient_code,
|
|
||||||
fi.grams_per_cheop,
|
|
||||||
fi.notes,
|
|
||||||
fi.sort_order,
|
|
||||||
hm.herb_name,
|
|
||||||
hm.herb_name_hanja
|
|
||||||
FROM formula_ingredients fi
|
FROM formula_ingredients fi
|
||||||
LEFT JOIN herb_masters hm ON fi.ingredient_code = hm.ingredient_code
|
JOIN herb_items h ON fi.herb_item_id = h.herb_item_id
|
||||||
WHERE fi.formula_id = ?
|
WHERE fi.formula_id = ?
|
||||||
ORDER BY fi.sort_order
|
ORDER BY fi.sort_order
|
||||||
""", (formula_id,))
|
""", (formula_id,))
|
||||||
|
ingredients = [dict(row) for row in cursor.fetchall()]
|
||||||
ingredients = []
|
|
||||||
for row in cursor.fetchall():
|
|
||||||
ingredient = dict(row)
|
|
||||||
ingredient_code = ingredient['ingredient_code']
|
|
||||||
|
|
||||||
# 해당 주성분을 가진 사용 가능한 모든 제품 찾기
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT
|
|
||||||
h.herb_item_id,
|
|
||||||
h.herb_name,
|
|
||||||
h.insurance_code,
|
|
||||||
h.specification,
|
|
||||||
COALESCE(SUM(il.quantity_onhand), 0) as stock,
|
|
||||||
COALESCE(AVG(il.unit_price_per_g), 0) as avg_price
|
|
||||||
FROM herb_items h
|
|
||||||
LEFT JOIN inventory_lots il ON h.herb_item_id = il.herb_item_id AND il.is_depleted = 0
|
|
||||||
WHERE h.ingredient_code = ?
|
|
||||||
GROUP BY h.herb_item_id
|
|
||||||
HAVING stock > 0
|
|
||||||
ORDER BY stock DESC
|
|
||||||
""", (ingredient_code,))
|
|
||||||
|
|
||||||
available_products = [dict(row) for row in cursor.fetchall()]
|
|
||||||
ingredient['available_products'] = available_products
|
|
||||||
ingredient['total_available_stock'] = sum(p['stock'] for p in available_products)
|
|
||||||
ingredient['product_count'] = len(available_products)
|
|
||||||
|
|
||||||
ingredients.append(ingredient)
|
|
||||||
|
|
||||||
return jsonify({'success': True, 'data': ingredients})
|
return jsonify({'success': True, 'data': ingredients})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
@ -1643,219 +1599,6 @@ def create_stock_adjustment():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
# ==================== 문진표 API ====================
|
|
||||||
|
|
||||||
@app.route('/api/surveys/templates', methods=['GET'])
|
|
||||||
def get_survey_templates():
|
|
||||||
"""문진표 템플릿 조회"""
|
|
||||||
try:
|
|
||||||
category = request.args.get('category')
|
|
||||||
|
|
||||||
with get_db() as conn:
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
if category:
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT * FROM survey_templates
|
|
||||||
WHERE category = ? AND is_active = 1
|
|
||||||
ORDER BY sort_order
|
|
||||||
""", (category,))
|
|
||||||
else:
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT * FROM survey_templates
|
|
||||||
WHERE is_active = 1
|
|
||||||
ORDER BY sort_order
|
|
||||||
""")
|
|
||||||
|
|
||||||
templates = [dict(row) for row in cursor.fetchall()]
|
|
||||||
|
|
||||||
# JSON 파싱
|
|
||||||
for template in templates:
|
|
||||||
if template['options']:
|
|
||||||
template['options'] = json.loads(template['options'])
|
|
||||||
|
|
||||||
return jsonify({'success': True, 'data': templates})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
|
||||||
|
|
||||||
@app.route('/api/surveys/categories', methods=['GET'])
|
|
||||||
def get_survey_categories():
|
|
||||||
"""문진표 카테고리 목록"""
|
|
||||||
try:
|
|
||||||
with get_db() as conn:
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT DISTINCT category, category_name,
|
|
||||||
MIN(sort_order) as min_order,
|
|
||||||
COUNT(*) as question_count
|
|
||||||
FROM survey_templates
|
|
||||||
WHERE is_active = 1
|
|
||||||
GROUP BY category
|
|
||||||
ORDER BY min_order
|
|
||||||
""")
|
|
||||||
|
|
||||||
categories = [dict(row) for row in cursor.fetchall()]
|
|
||||||
return jsonify({'success': True, 'data': categories})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
|
||||||
|
|
||||||
@app.route('/api/surveys', methods=['POST'])
|
|
||||||
def create_survey():
|
|
||||||
"""새 문진표 생성"""
|
|
||||||
try:
|
|
||||||
data = request.json
|
|
||||||
patient_id = data.get('patient_id')
|
|
||||||
|
|
||||||
# 고유 토큰 생성
|
|
||||||
import secrets
|
|
||||||
survey_token = secrets.token_urlsafe(16)
|
|
||||||
|
|
||||||
with get_db() as conn:
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute("""
|
|
||||||
INSERT INTO patient_surveys (patient_id, survey_token, status)
|
|
||||||
VALUES (?, ?, 'PENDING')
|
|
||||||
""", (patient_id, survey_token))
|
|
||||||
|
|
||||||
survey_id = cursor.lastrowid
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
'success': True,
|
|
||||||
'survey_id': survey_id,
|
|
||||||
'survey_token': survey_token,
|
|
||||||
'survey_url': f'/survey/{survey_token}'
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
|
||||||
|
|
||||||
@app.route('/api/surveys/<survey_token>', methods=['GET'])
|
|
||||||
def get_survey(survey_token):
|
|
||||||
"""문진표 조회 (토큰으로)"""
|
|
||||||
try:
|
|
||||||
with get_db() as conn:
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
# 문진표 기본 정보
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT s.*, p.name as patient_name, p.phone as patient_phone
|
|
||||||
FROM patient_surveys s
|
|
||||||
LEFT JOIN patients p ON s.patient_id = p.patient_id
|
|
||||||
WHERE s.survey_token = ?
|
|
||||||
""", (survey_token,))
|
|
||||||
|
|
||||||
survey_row = cursor.fetchone()
|
|
||||||
if not survey_row:
|
|
||||||
return jsonify({'success': False, 'error': '문진표를 찾을 수 없습니다'}), 404
|
|
||||||
|
|
||||||
survey = dict(survey_row)
|
|
||||||
|
|
||||||
# 진행 상태 조회
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT * FROM survey_progress
|
|
||||||
WHERE survey_id = ?
|
|
||||||
""", (survey['survey_id'],))
|
|
||||||
|
|
||||||
progress = [dict(row) for row in cursor.fetchall()]
|
|
||||||
survey['progress'] = progress
|
|
||||||
|
|
||||||
# 기존 응답 조회
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT * FROM survey_responses
|
|
||||||
WHERE survey_id = ?
|
|
||||||
""", (survey['survey_id'],))
|
|
||||||
|
|
||||||
responses = [dict(row) for row in cursor.fetchall()]
|
|
||||||
survey['responses'] = responses
|
|
||||||
|
|
||||||
return jsonify({'success': True, 'data': survey})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
|
||||||
|
|
||||||
@app.route('/api/surveys/<survey_token>/responses', methods=['POST'])
|
|
||||||
def save_survey_responses(survey_token):
|
|
||||||
"""문진 응답 저장"""
|
|
||||||
try:
|
|
||||||
data = request.json
|
|
||||||
responses = data.get('responses', [])
|
|
||||||
|
|
||||||
with get_db() as conn:
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
# 문진표 확인
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT survey_id FROM patient_surveys
|
|
||||||
WHERE survey_token = ?
|
|
||||||
""", (survey_token,))
|
|
||||||
|
|
||||||
survey_row = cursor.fetchone()
|
|
||||||
if not survey_row:
|
|
||||||
return jsonify({'success': False, 'error': '문진표를 찾을 수 없습니다'}), 404
|
|
||||||
|
|
||||||
survey_id = survey_row['survey_id']
|
|
||||||
|
|
||||||
# 기존 응답 삭제 후 새로 저장 (upsert 방식)
|
|
||||||
for response in responses:
|
|
||||||
# 기존 응답 삭제
|
|
||||||
cursor.execute("""
|
|
||||||
DELETE FROM survey_responses
|
|
||||||
WHERE survey_id = ? AND question_code = ?
|
|
||||||
""", (survey_id, response['question_code']))
|
|
||||||
|
|
||||||
# 새 응답 저장
|
|
||||||
cursor.execute("""
|
|
||||||
INSERT INTO survey_responses
|
|
||||||
(survey_id, category, question_code, question_text, answer_value, answer_type)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
|
||||||
""", (
|
|
||||||
survey_id,
|
|
||||||
response['category'],
|
|
||||||
response['question_code'],
|
|
||||||
response.get('question_text'),
|
|
||||||
json.dumps(response['answer_value'], ensure_ascii=False) if isinstance(response['answer_value'], (list, dict)) else response['answer_value'],
|
|
||||||
response.get('answer_type', 'SINGLE')
|
|
||||||
))
|
|
||||||
|
|
||||||
# 상태 업데이트
|
|
||||||
cursor.execute("""
|
|
||||||
UPDATE patient_surveys
|
|
||||||
SET status = 'IN_PROGRESS',
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE survey_id = ?
|
|
||||||
""", (survey_id,))
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
'success': True,
|
|
||||||
'message': f'{len(responses)}개 응답 저장 완료'
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
|
||||||
|
|
||||||
@app.route('/api/surveys/<survey_token>/complete', methods=['POST'])
|
|
||||||
def complete_survey(survey_token):
|
|
||||||
"""문진표 제출 완료"""
|
|
||||||
try:
|
|
||||||
with get_db() as conn:
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
cursor.execute("""
|
|
||||||
UPDATE patient_surveys
|
|
||||||
SET status = 'COMPLETED',
|
|
||||||
completed_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE survey_token = ?
|
|
||||||
""", (survey_token,))
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
'success': True,
|
|
||||||
'message': '문진표가 제출되었습니다'
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# 데이터베이스 초기화
|
# 데이터베이스 초기화
|
||||||
if not os.path.exists(app.config['DATABASE']):
|
if not os.path.exists(app.config['DATABASE']):
|
||||||
|
|||||||
@ -1,70 +0,0 @@
|
|||||||
-- 환자 사전 문진표 시스템 테이블
|
|
||||||
|
|
||||||
-- 1. 문진 메인 테이블
|
|
||||||
CREATE TABLE IF NOT EXISTS patient_surveys (
|
|
||||||
survey_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
patient_id INTEGER,
|
|
||||||
survey_token TEXT UNIQUE NOT NULL, -- QR/링크용 고유 토큰
|
|
||||||
survey_date DATE DEFAULT (date('now')),
|
|
||||||
status TEXT DEFAULT 'PENDING' CHECK(status IN ('PENDING', 'IN_PROGRESS', 'COMPLETED', 'REVIEWED')),
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
completed_at DATETIME,
|
|
||||||
reviewed_at DATETIME,
|
|
||||||
reviewed_by TEXT,
|
|
||||||
notes TEXT,
|
|
||||||
FOREIGN KEY (patient_id) REFERENCES patients(patient_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_survey_token ON patient_surveys(survey_token);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_survey_patient ON patient_surveys(patient_id);
|
|
||||||
|
|
||||||
-- 2. 문진 응답 테이블
|
|
||||||
CREATE TABLE IF NOT EXISTS survey_responses (
|
|
||||||
response_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
survey_id INTEGER NOT NULL,
|
|
||||||
category TEXT NOT NULL, -- 카테고리 (TEMPERATURE, BODY_TEMP, DIGESTION, etc)
|
|
||||||
question_code TEXT NOT NULL, -- 질문 코드
|
|
||||||
question_text TEXT, -- 질문 내용
|
|
||||||
answer_value TEXT, -- 응답 값 (JSON 또는 TEXT)
|
|
||||||
answer_type TEXT, -- SINGLE, MULTIPLE, TEXT, RANGE
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (survey_id) REFERENCES patient_surveys(survey_id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_response_survey ON survey_responses(survey_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_response_category ON survey_responses(category);
|
|
||||||
|
|
||||||
-- 3. 문진 템플릿 테이블 (질문 정의)
|
|
||||||
CREATE TABLE IF NOT EXISTS survey_templates (
|
|
||||||
template_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
category TEXT NOT NULL,
|
|
||||||
category_name TEXT NOT NULL, -- 카테고리 한글명
|
|
||||||
question_code TEXT NOT NULL UNIQUE,
|
|
||||||
question_text TEXT NOT NULL,
|
|
||||||
question_subtext TEXT, -- 부가 설명
|
|
||||||
input_type TEXT NOT NULL CHECK(input_type IN ('RADIO', 'CHECKBOX', 'RANGE', 'TEXT', 'TEXTAREA', 'NUMBER')),
|
|
||||||
options TEXT, -- JSON 배열 형태의 선택지
|
|
||||||
is_required INTEGER DEFAULT 1,
|
|
||||||
sort_order INTEGER DEFAULT 0,
|
|
||||||
is_active INTEGER DEFAULT 1,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_template_category ON survey_templates(category);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_template_active ON survey_templates(is_active);
|
|
||||||
|
|
||||||
-- 4. 문진 진행 상태 추적 테이블
|
|
||||||
CREATE TABLE IF NOT EXISTS survey_progress (
|
|
||||||
progress_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
survey_id INTEGER NOT NULL,
|
|
||||||
category TEXT NOT NULL,
|
|
||||||
total_questions INTEGER DEFAULT 0,
|
|
||||||
answered_questions INTEGER DEFAULT 0,
|
|
||||||
is_completed INTEGER DEFAULT 0,
|
|
||||||
last_updated DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (survey_id) REFERENCES patient_surveys(survey_id) ON DELETE CASCADE,
|
|
||||||
UNIQUE(survey_id, category)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_progress_survey ON survey_progress(survey_id);
|
|
||||||
@ -1,596 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
문진표 템플릿 데이터 입력 스크립트
|
|
||||||
"""
|
|
||||||
import sqlite3
|
|
||||||
import json
|
|
||||||
|
|
||||||
# 문진 항목 데이터
|
|
||||||
survey_data = [
|
|
||||||
# 1. 체온 감각 (TEMPERATURE)
|
|
||||||
{
|
|
||||||
'category': 'TEMPERATURE',
|
|
||||||
'category_name': '체온 감각',
|
|
||||||
'questions': [
|
|
||||||
{
|
|
||||||
'code': 'COLD_SENSITIVITY',
|
|
||||||
'text': '추위를 타시나요?',
|
|
||||||
'type': 'RADIO',
|
|
||||||
'options': ['심하게 탄다', '타는 편', '약간 탄다', '안탄다', '겨울이 싫다'],
|
|
||||||
'required': True,
|
|
||||||
'order': 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'HEAT_SENSITIVITY',
|
|
||||||
'text': '더위를 타시나요?',
|
|
||||||
'type': 'RADIO',
|
|
||||||
'options': ['심하게 탄다', '타는 편', '약간 탄다', '안탄다', '선풍기/에어컨 바람이 싫다'],
|
|
||||||
'required': True,
|
|
||||||
'order': 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'SWEAT_AMOUNT',
|
|
||||||
'text': '땀을 흘리는 정도는?',
|
|
||||||
'type': 'RADIO',
|
|
||||||
'options': ['건조하다/없다', '약간', '보통', '많다', '아주 많다'],
|
|
||||||
'required': True,
|
|
||||||
'order': 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'SWEAT_LOCATION',
|
|
||||||
'text': '땀이 나는 부위는? (복수선택)',
|
|
||||||
'type': 'CHECKBOX',
|
|
||||||
'options': ['얼굴', '몸전체', '손발', '하체', '머리', '이마', '겨드랑이'],
|
|
||||||
'required': False,
|
|
||||||
'order': 4
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'SWEAT_TIMING',
|
|
||||||
'text': '주로 언제 땀이 나나요? (복수선택)',
|
|
||||||
'type': 'CHECKBOX',
|
|
||||||
'options': ['잘 때', '식사할 때', '긴장할 때', '여름에', '일할 때', '수시로'],
|
|
||||||
'required': False,
|
|
||||||
'order': 5
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
# 2. 신체 부위별 온도 (BODY_TEMP)
|
|
||||||
{
|
|
||||||
'category': 'BODY_TEMP',
|
|
||||||
'category_name': '신체 부위별 온도',
|
|
||||||
'questions': [
|
|
||||||
{
|
|
||||||
'code': 'HAND_TEMP',
|
|
||||||
'text': '손의 온도는?',
|
|
||||||
'type': 'RADIO',
|
|
||||||
'options': ['매우 차다', '약간 차다', '보통', '따뜻하다', '뜨겁다', '화끈거림', '저림', '쥐'],
|
|
||||||
'required': True,
|
|
||||||
'order': 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'FOOT_TEMP',
|
|
||||||
'text': '발의 온도는?',
|
|
||||||
'type': 'RADIO',
|
|
||||||
'options': ['매우 차다', '약간 차다', '보통', '따뜻하다', '뜨겁다', '화끈거림', '저림', '쥐'],
|
|
||||||
'required': True,
|
|
||||||
'order': 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'UPPER_ABDOMEN_TEMP',
|
|
||||||
'text': '윗배의 온도는?',
|
|
||||||
'type': 'RADIO',
|
|
||||||
'options': ['매우 차다', '약간 차다', '보통', '따뜻하다', '뜨겁다', '시림'],
|
|
||||||
'required': True,
|
|
||||||
'order': 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'LOWER_ABDOMEN_TEMP',
|
|
||||||
'text': '아랫배의 온도는?',
|
|
||||||
'type': 'RADIO',
|
|
||||||
'options': ['매우 차다', '약간 차다', '보통', '따뜻하다', '뜨겁다', '복부비만'],
|
|
||||||
'required': True,
|
|
||||||
'order': 4
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'WHOLE_BODY_TEMP',
|
|
||||||
'text': '몸 전체의 느낌은?',
|
|
||||||
'type': 'CHECKBOX',
|
|
||||||
'options': ['매우 차다', '약간 차다', '보통', '따뜻하다', '뜨겁다', '무겁다', '아프다', '부종'],
|
|
||||||
'required': True,
|
|
||||||
'order': 5
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
# 3. 식성 및 소화 (DIGESTION)
|
|
||||||
{
|
|
||||||
'category': 'DIGESTION',
|
|
||||||
'category_name': '식성 및 소화',
|
|
||||||
'questions': [
|
|
||||||
{
|
|
||||||
'code': 'FOOD_TEMP_PREF',
|
|
||||||
'text': '음식 온도 선호는?',
|
|
||||||
'type': 'RADIO',
|
|
||||||
'options': ['찬 것', '시원한 것', '보통', '따뜻한 것', '뜨거운 것', '모두'],
|
|
||||||
'required': True,
|
|
||||||
'order': 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'TASTE_PREF',
|
|
||||||
'text': '맛 선호는?',
|
|
||||||
'type': 'RADIO',
|
|
||||||
'options': ['신 것', '단 것', '매운 것', '짠 것', '쓴 것', '담백한 것', '모두'],
|
|
||||||
'required': True,
|
|
||||||
'order': 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'FOOD_HABITS',
|
|
||||||
'text': '식습관 (복수선택)',
|
|
||||||
'subtext': '주 1회 이상 섭취하는 것을 선택하세요',
|
|
||||||
'type': 'CHECKBOX',
|
|
||||||
'options': ['된장', '채식', '육류', '해물', '커피', '술', '담배'],
|
|
||||||
'required': False,
|
|
||||||
'order': 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'WATER_INTAKE',
|
|
||||||
'text': '물을 마시는 정도는?',
|
|
||||||
'type': 'RADIO',
|
|
||||||
'options': ['많이 마심', '자주', '보통', '거의 안마심'],
|
|
||||||
'required': True,
|
|
||||||
'order': 4
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'APPETITE',
|
|
||||||
'text': '식욕은 어떠신가요?',
|
|
||||||
'type': 'RADIO',
|
|
||||||
'options': ['없다', '별로', '보통', '좋다', '왕성', '아침 생략'],
|
|
||||||
'required': True,
|
|
||||||
'order': 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'MEAL_AMOUNT',
|
|
||||||
'text': '식사량은?',
|
|
||||||
'type': 'RADIO',
|
|
||||||
'options': ['적다', '보통', '많다', '1공기 이하', '1공기 이상', '일정치 않다', '저녁에 많이'],
|
|
||||||
'required': True,
|
|
||||||
'order': 6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'DIGESTION_POWER',
|
|
||||||
'text': '소화력은?',
|
|
||||||
'type': 'RADIO',
|
|
||||||
'options': ['잘 된다', '보통', '약하다', '잘 안됨', '잘 체한다'],
|
|
||||||
'required': True,
|
|
||||||
'order': 7
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'INDIGESTION_SYMPTOMS',
|
|
||||||
'text': '소화불량 증상 (복수선택)',
|
|
||||||
'type': 'CHECKBOX',
|
|
||||||
'options': ['막힌듯함', '답답함', '걸린듯함', '더부룩함', '그득함', '속쓰림',
|
|
||||||
'헛배부름', '가스참', '느글거림', '트림', '구토', '헛구역', '방귀',
|
|
||||||
'꾸룩소리남', '명치아픔', '복통', '딸꾹질', '하품'],
|
|
||||||
'required': False,
|
|
||||||
'order': 8
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
# 4. 대소변 (EXCRETION)
|
|
||||||
{
|
|
||||||
'category': 'EXCRETION',
|
|
||||||
'category_name': '대소변',
|
|
||||||
'questions': [
|
|
||||||
{
|
|
||||||
'code': 'STOOL_FREQUENCY',
|
|
||||||
'text': '대변 빈도는?',
|
|
||||||
'subtext': '일 _회 / 매일 / 아침 / 불규칙',
|
|
||||||
'type': 'TEXT',
|
|
||||||
'required': True,
|
|
||||||
'order': 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'STOOL_CONDITION',
|
|
||||||
'text': '대변 상태는? (복수선택)',
|
|
||||||
'type': 'CHECKBOX',
|
|
||||||
'options': ['변비', '된편', '굵다', '토끼똥', '설사', '물변', '보통', '가늘다', '퍼진다', '냄새심함'],
|
|
||||||
'required': True,
|
|
||||||
'order': 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'STOOL_DIFFICULTY',
|
|
||||||
'text': '배변 시 불편함은? (복수선택)',
|
|
||||||
'type': 'CHECKBOX',
|
|
||||||
'options': ['잘 나옴', '잘 안나옴', '시원치 않다', '오래봄', '힘들게 나옴',
|
|
||||||
'조금 나옴', '남아있는 듯함', '조금씩 자주', '지림', '못 참음'],
|
|
||||||
'required': False,
|
|
||||||
'order': 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'URINE_FREQUENCY_DAY',
|
|
||||||
'text': '낮 소변 빈도는?',
|
|
||||||
'subtext': '낮에 몇 시간마다 1회?',
|
|
||||||
'type': 'NUMBER',
|
|
||||||
'required': True,
|
|
||||||
'order': 4
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'URINE_FREQUENCY_NIGHT',
|
|
||||||
'text': '밤 소변 빈도는?',
|
|
||||||
'subtext': '자다가 몇 회?',
|
|
||||||
'type': 'NUMBER',
|
|
||||||
'required': True,
|
|
||||||
'order': 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'URINE_FREQUENCY_LEVEL',
|
|
||||||
'text': '전체적인 소변 빈도는?',
|
|
||||||
'type': 'RADIO',
|
|
||||||
'options': ['거의 안봄', '가끔', '보통', '자주', '매우 자주', '밤에 오줌 싼다'],
|
|
||||||
'required': True,
|
|
||||||
'order': 6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'URINE_COLOR',
|
|
||||||
'text': '소변 색은?',
|
|
||||||
'type': 'RADIO',
|
|
||||||
'options': ['붉다', '노랗다', '보통', '탁하다', '맑다', '커피색'],
|
|
||||||
'required': True,
|
|
||||||
'order': 7
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'URINE_ABNORMAL',
|
|
||||||
'text': '소변 이상 증상 (복수선택)',
|
|
||||||
'type': 'CHECKBOX',
|
|
||||||
'options': ['거품이 남', '기름이 뜸', '단내남', '뿌옇다', '정액이 나옴'],
|
|
||||||
'required': False,
|
|
||||||
'order': 8
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
# 5. 수면 (SLEEP)
|
|
||||||
{
|
|
||||||
'category': 'SLEEP',
|
|
||||||
'category_name': '수면',
|
|
||||||
'questions': [
|
|
||||||
{
|
|
||||||
'code': 'SLEEP_HOURS',
|
|
||||||
'text': '하루 수면 시간은?',
|
|
||||||
'subtext': '예: 7시간',
|
|
||||||
'type': 'NUMBER',
|
|
||||||
'required': True,
|
|
||||||
'order': 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'SLEEP_TIME',
|
|
||||||
'text': '수면 시간대는?',
|
|
||||||
'subtext': '예: 23시 ~ 06시',
|
|
||||||
'type': 'TEXT',
|
|
||||||
'required': False,
|
|
||||||
'order': 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'SLEEP_SATISFACTION',
|
|
||||||
'text': '수면이 충분한가요?',
|
|
||||||
'type': 'RADIO',
|
|
||||||
'options': ['잠 부족', '잠 충분'],
|
|
||||||
'required': True,
|
|
||||||
'order': 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'SLEEP_QUALITY',
|
|
||||||
'text': '수면 상태는? (복수선택)',
|
|
||||||
'type': 'CHECKBOX',
|
|
||||||
'options': ['잘잠', '잘못잠', '거의 못잠', '가끔 못잠', '뒤척임', '곧 잔다',
|
|
||||||
'잠들기 어렵다', '깊이 잠', '얕은 잠', '잠귀 밝음', '잘 깸', '깨면 안옴'],
|
|
||||||
'required': True,
|
|
||||||
'order': 4
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'DREAM_FREQUENCY',
|
|
||||||
'text': '꿈을 꾸는 빈도는?',
|
|
||||||
'type': 'RADIO',
|
|
||||||
'options': ['밤새 꿈', '자주 꾼다', '가끔 꿈', '거의 없다', '안꾼다', '잠꼬대'],
|
|
||||||
'required': True,
|
|
||||||
'order': 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'DREAM_TYPE',
|
|
||||||
'text': '꾸는 꿈의 종류는? (복수선택)',
|
|
||||||
'type': 'CHECKBOX',
|
|
||||||
'options': ['무서운 꿈', '죽은 사람 꿈', '쫓기는 꿈', '개꿈', '기억 안남', '기억 남'],
|
|
||||||
'required': False,
|
|
||||||
'order': 6
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
# 6. 순환 및 정신 (CIRCULATION)
|
|
||||||
{
|
|
||||||
'category': 'CIRCULATION',
|
|
||||||
'category_name': '순환 및 정신 증상',
|
|
||||||
'questions': [
|
|
||||||
{
|
|
||||||
'code': 'HEART_SYMPTOMS',
|
|
||||||
'text': '심장 관련 증상 (복수선택)',
|
|
||||||
'type': 'CHECKBOX',
|
|
||||||
'options': ['가슴뜀', '가슴답답', '가슴뻐근', '한숨쉼', '호흡곤란', '숨참', '뒷목뻐근'],
|
|
||||||
'required': False,
|
|
||||||
'order': 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'HEAT_FLASH',
|
|
||||||
'text': '열이 달아오르는 증상이 있나요?',
|
|
||||||
'subtext': '있다면 빈도를 알려주세요 (예: 일 3회, 주 2회)',
|
|
||||||
'type': 'TEXT',
|
|
||||||
'required': False,
|
|
||||||
'order': 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'MENTAL_SYMPTOMS',
|
|
||||||
'text': '정신적 증상 (복수선택)',
|
|
||||||
'type': 'CHECKBOX',
|
|
||||||
'options': ['잘 놀람', '불안', '초조', '우울', '비관', '신경질', '짜증',
|
|
||||||
'매사 귀찮다', '손떨림', '졸도', '가슴 막힌 듯', '조이는 듯',
|
|
||||||
'기억력 격감', '건망증', '현기증', '눈 피로감'],
|
|
||||||
'required': False,
|
|
||||||
'order': 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'FATIGUE_SYMPTOMS',
|
|
||||||
'text': '피로 증상 (복수선택)',
|
|
||||||
'type': 'CHECKBOX',
|
|
||||||
'options': ['피로', '기운이 없다', '아침에 잘 못일어난다', '의욕이 없다', '무겁다'],
|
|
||||||
'required': False,
|
|
||||||
'order': 4
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
# 7. 여성 건강 (WOMEN_HEALTH)
|
|
||||||
{
|
|
||||||
'category': 'WOMEN_HEALTH',
|
|
||||||
'category_name': '여성 건강',
|
|
||||||
'questions': [
|
|
||||||
{
|
|
||||||
'code': 'MARITAL_STATUS',
|
|
||||||
'text': '결혼 상태는?',
|
|
||||||
'type': 'RADIO',
|
|
||||||
'options': ['미혼', '기혼'],
|
|
||||||
'required': False,
|
|
||||||
'order': 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'MARRIAGE_YEARS',
|
|
||||||
'text': '결혼한 지 몇 년?',
|
|
||||||
'type': 'NUMBER',
|
|
||||||
'required': False,
|
|
||||||
'order': 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'BIRTH_COUNT',
|
|
||||||
'text': '출산 횟수',
|
|
||||||
'type': 'NUMBER',
|
|
||||||
'required': False,
|
|
||||||
'order': 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'INFERTILITY_YEARS',
|
|
||||||
'text': '불임 기간 (년)',
|
|
||||||
'type': 'NUMBER',
|
|
||||||
'required': False,
|
|
||||||
'order': 4
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'MISCARRIAGE',
|
|
||||||
'text': '유산 경험',
|
|
||||||
'subtext': '자연유산 _회, 인공유산 _회',
|
|
||||||
'type': 'TEXT',
|
|
||||||
'required': False,
|
|
||||||
'order': 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'MENSTRUAL_CYCLE',
|
|
||||||
'text': '생리 주기는?',
|
|
||||||
'subtext': '_일 간격',
|
|
||||||
'type': 'NUMBER',
|
|
||||||
'required': False,
|
|
||||||
'order': 6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'MENSTRUAL_REGULARITY',
|
|
||||||
'text': '생리 규칙성은?',
|
|
||||||
'type': 'RADIO',
|
|
||||||
'options': ['정상', '부정확', '건넘', '중단', '폐경', '계속 나옴'],
|
|
||||||
'required': False,
|
|
||||||
'order': 7
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'MENSTRUAL_DURATION',
|
|
||||||
'text': '생리 기간은?',
|
|
||||||
'subtext': '_일간 (_일 많고 _일 적다)',
|
|
||||||
'type': 'TEXT',
|
|
||||||
'required': False,
|
|
||||||
'order': 8
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'MENSTRUAL_AMOUNT',
|
|
||||||
'text': '생리량은?',
|
|
||||||
'type': 'RADIO',
|
|
||||||
'options': ['너무 많다', '많다', '보통', '약간 적다', '아주 적다', '줄어듬', '늦어짐', '빨라짐'],
|
|
||||||
'required': False,
|
|
||||||
'order': 9
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'MENSTRUAL_COLOR',
|
|
||||||
'text': '생리 색은? (복수선택)',
|
|
||||||
'type': 'CHECKBOX',
|
|
||||||
'options': ['검붉다', '검다', '일부 덩어리', '찌꺼기', '묽다'],
|
|
||||||
'required': False,
|
|
||||||
'order': 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'MENSTRUAL_PAIN',
|
|
||||||
'text': '생리통은?',
|
|
||||||
'type': 'RADIO',
|
|
||||||
'options': ['없다', '약간', '심하다', '극심'],
|
|
||||||
'required': False,
|
|
||||||
'order': 11
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'MENSTRUAL_PAIN_TIMING',
|
|
||||||
'text': '생리통 시기는?',
|
|
||||||
'subtext': '생리 _일 부터 _일간',
|
|
||||||
'type': 'TEXT',
|
|
||||||
'required': False,
|
|
||||||
'order': 12
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'MENSTRUAL_PAIN_LOCATION',
|
|
||||||
'text': '생리통 부위 (복수선택)',
|
|
||||||
'type': 'CHECKBOX',
|
|
||||||
'options': ['아랫배', '허리', '허벅지', '가슴', '머리', '전신', '몸살', '과민'],
|
|
||||||
'required': False,
|
|
||||||
'order': 13
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'VAGINAL_DISCHARGE',
|
|
||||||
'text': '냉대하는?',
|
|
||||||
'type': 'RADIO',
|
|
||||||
'options': ['없다', '약간', '많다', '심하다'],
|
|
||||||
'required': False,
|
|
||||||
'order': 14
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'DISCHARGE_SYMPTOMS',
|
|
||||||
'text': '냉대하 증상 (복수선택)',
|
|
||||||
'type': 'CHECKBOX',
|
|
||||||
'options': ['투명', '누렇다', '희다', '묽다', '냄새', '악취', '가렵다'],
|
|
||||||
'required': False,
|
|
||||||
'order': 15
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
# 8. 피부 (SKIN)
|
|
||||||
{
|
|
||||||
'category': 'SKIN',
|
|
||||||
'category_name': '피부',
|
|
||||||
'questions': [
|
|
||||||
{
|
|
||||||
'code': 'SKIN_COLOR',
|
|
||||||
'text': '피부 색은?',
|
|
||||||
'type': 'RADIO',
|
|
||||||
'options': ['보통', '약간 황색', '약간 검다', '희다', '창백', '누렇다', '약간 붉다'],
|
|
||||||
'required': True,
|
|
||||||
'order': 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'SKIN_TYPE',
|
|
||||||
'text': '피부 타입은?',
|
|
||||||
'type': 'RADIO',
|
|
||||||
'options': ['보통', '섬세', '얇다', '약간 두텁다', '두텁다', '지성', '중성', '건성'],
|
|
||||||
'required': True,
|
|
||||||
'order': 2
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
# 9. 성품/체질 (PERSONALITY)
|
|
||||||
{
|
|
||||||
'category': 'PERSONALITY',
|
|
||||||
'category_name': '성품 및 체질',
|
|
||||||
'questions': [
|
|
||||||
{
|
|
||||||
'code': 'PERSONALITY_TYPE1',
|
|
||||||
'text': '성격 유형 1 - 태양인 성향 (복수선택)',
|
|
||||||
'type': 'CHECKBOX',
|
|
||||||
'options': ['저돌적이다', '기세가 강하다', '남의 말을 잘 듣지 않는다',
|
|
||||||
'거침이 없다', '독불장군이다', '안하무인이다', '뚜렷하다',
|
|
||||||
'말과 행동 빠름', '음식을 빨리 먹음', '눈매 예리/날카롭다',
|
|
||||||
'부지런함', '적극적', '활동적', '소변 자주 본다', '일을 안미룬다',
|
|
||||||
'나다니기를 좋아함', '분명하다', '나서기 잘함', '질투가 심하다'],
|
|
||||||
'required': False,
|
|
||||||
'order': 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'PERSONALITY_TYPE2',
|
|
||||||
'text': '성격 유형 2 - 태음인 성향 (복수선택)',
|
|
||||||
'type': 'CHECKBOX',
|
|
||||||
'options': ['눕기 좋아함', '엉덩이 무겁다', '느긋함', '땀이 많다', '과묵하다',
|
|
||||||
'사람 좋다', '무던하다', '부드럽다', '가정적이다', '씻기를 싫어함',
|
|
||||||
'원만', '정중', '은근', '꾸준함', '우유부단', '된장/쓴 것 좋아함'],
|
|
||||||
'required': False,
|
|
||||||
'order': 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'PERSONALITY_TYPE3',
|
|
||||||
'text': '성격 유형 3 - 소양인/소음인 성향 (복수선택)',
|
|
||||||
'type': 'CHECKBOX',
|
|
||||||
'options': ['약해 보인다', '겁이 많음', '세심하다', '소심', '차분함', '자상함',
|
|
||||||
'연약', '영민', '잘 미룬다', '깐깐', '치밀', '궁리는 많으나 실행은 적음'],
|
|
||||||
'required': False,
|
|
||||||
'order': 3
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
def insert_survey_templates():
|
|
||||||
conn = sqlite3.connect('/root/kdrug/database/kdrug.db')
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
# 기존 템플릿 삭제
|
|
||||||
cursor.execute("DELETE FROM survey_templates")
|
|
||||||
|
|
||||||
inserted_count = 0
|
|
||||||
|
|
||||||
for category_data in survey_data:
|
|
||||||
category = category_data['category']
|
|
||||||
category_name = category_data['category_name']
|
|
||||||
|
|
||||||
for question in category_data['questions']:
|
|
||||||
cursor.execute("""
|
|
||||||
INSERT INTO survey_templates
|
|
||||||
(category, category_name, question_code, question_text, question_subtext,
|
|
||||||
input_type, options, is_required, sort_order)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
""", (
|
|
||||||
category,
|
|
||||||
category_name,
|
|
||||||
question['code'],
|
|
||||||
question['text'],
|
|
||||||
question.get('subtext'),
|
|
||||||
question['type'],
|
|
||||||
json.dumps(question.get('options', []), ensure_ascii=False),
|
|
||||||
1 if question.get('required', False) else 0,
|
|
||||||
question['order']
|
|
||||||
))
|
|
||||||
inserted_count += 1
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
# 결과 확인
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT category, category_name, COUNT(*) as cnt
|
|
||||||
FROM survey_templates
|
|
||||||
GROUP BY category
|
|
||||||
ORDER BY MIN(template_id)
|
|
||||||
""")
|
|
||||||
|
|
||||||
results = cursor.fetchall()
|
|
||||||
|
|
||||||
print(f"✅ 문진 템플릿 {inserted_count}개 항목 입력 완료\n")
|
|
||||||
print("카테고리별 질문 수:")
|
|
||||||
for row in results:
|
|
||||||
print(f" {row[0]:20s} ({row[1]:15s}): {row[2]:2d}개")
|
|
||||||
|
|
||||||
conn.close()
|
|
||||||
return inserted_count
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
insert_survey_templates()
|
|
||||||
113
docs/셀프체크.md
113
docs/셀프체크.md
@ -1,113 +0,0 @@
|
|||||||
|
|
||||||
우리가 상담을 하기전에 사전조사가 필요해
|
|
||||||
|
|
||||||
카카오 알림톡이나 , 약국에 QR코드를 인쇄해놔서
|
|
||||||
환자 개인상태를 받고싶어
|
|
||||||
|
|
||||||
즉 모바일에서 1,2,3,4,5
|
|
||||||
도는 각각에 예시 값들이 있어서 정보를 받아서 DB에 저장하고싶어
|
|
||||||
|
|
||||||
일단은 먼저 예시 를 만들고싶어
|
|
||||||
|
|
||||||
|
|
||||||
일단 우리 페이지에 목업으로 이것을 화면을 만들어줘
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
아래를 읽고 불편한 곳에 표시하여 주십시오. 불편한점○ 극심한곳◎ 약간□ 가끔△ 자주M 불쾌× 전증상
|
|
||||||
추위
|
|
||||||
더위
|
|
||||||
땀
|
|
||||||
심하게 탄다, 타는 편,약간 탄다 ,안탄다 , 겨울이 싫다.
|
|
||||||
심하게 탄다, 타는 편, 약간 탄다, 안탄다,선풍기에어컨 바람 ·
|
|
||||||
건조하다없다약간보통많다아주많다, , , , ,
|
|
||||||
얼굴몸전체손발하체머리이마겨드랑이, , , , , , ,
|
|
||||||
잘 때식사할 때긴장할 때여름에일할 때수시로, , , , ,
|
|
||||||
손
|
|
||||||
발
|
|
||||||
윗 배
|
|
||||||
아랫배
|
|
||||||
몸전체
|
|
||||||
매우차다,약간차다,보통,따뜻하다,뜨겁다,화끈거림,저림,쥐
|
|
||||||
매우차다,약간차다,보통,따뜻하다,뜨겁다,화끈거림,저림쥐,
|
|
||||||
매우차다,약간차다,보통,따뜻하다,뜨겁다 시림
|
|
||||||
매우차다,약간차다,보통,따뜻하다,뜨겁다복부비만,
|
|
||||||
매우차다,약간차다,보통,따뜻하다,뜨겁다,무겁다,아프다,부종
|
|
||||||
식성
|
|
||||||
기호
|
|
||||||
물
|
|
||||||
식 성
|
|
||||||
식사량
|
|
||||||
소화력
|
|
||||||
소화불량
|
|
||||||
찬 것시원한 것보통따뜻한 것뜨거운 것모두, , , , ,
|
|
||||||
신 것단 것매운 것짠 것쓴 것담백한 것모두, , , , , ,
|
|
||||||
된장채식육류해물커피술 주일 , , , , , 1 회담배 일 , 1 갑
|
|
||||||
많이 마심자주보통거의 안마심, , ,
|
|
||||||
식욕없다별로보통좋다왕성일 , , , , , 1 끼 먹음아침 생략,
|
|
||||||
적다보통많다공기이하이상일정치않다일정함저녁많이, , , 1 , , , ∙ ∙
|
|
||||||
잘된다보통약하다잘 안됨잘 체한다, , , , .
|
|
||||||
막힌듯함답답함걸린듯함더부룩함그득함, , , ,
|
|
||||||
속쓰림헛배부름가스참느글거림트림구토헛구역, , , , , ,
|
|
||||||
방귀꾸룩소리남명치아픔복통딸꾹질하품, , , , ,
|
|
||||||
대 변
|
|
||||||
대변상태
|
|
||||||
소변상태
|
|
||||||
소변빈도
|
|
||||||
소변색
|
|
||||||
소변이상
|
|
||||||
일 회매일아침불규칙음주, , , 다음날설사 회
|
|
||||||
변비,된편,굵다,토끼똥,설사,물변,보통,가늘다,퍼진다,냄새심
|
|
||||||
잘나옴잘안나옴시원치 않다오래봄힘들게나옴조금나옴, , , , ,
|
|
||||||
남아있는 듯 함조금씩 자주지, , 림못 참음,
|
|
||||||
자다가 회낮 , 회, 시간마다 회1
|
|
||||||
거의 안봄가끔보통자주매우자주밤에 오줌싼다, , , , ,
|
|
||||||
붉 다노랗다보 통탁하다맑 다커피색, , , , ,
|
|
||||||
거품이 남기름이 뜸단내남뿌옇다정액이 나옴, , , ,
|
|
||||||
잠
|
|
||||||
수면상태
|
|
||||||
꿈
|
|
||||||
하루 시간, ~ 시잠부족잠충분, ,
|
|
||||||
잘잠잘못잠거의못잠가끔못잠뒤척임, , , ,
|
|
||||||
곧잔다잠들기 어렵다깊이 잠옅은 잠잠귀밝음잘깸깨면 안옴, , , , , ,
|
|
||||||
밤새꿈자주꾼다가끔꿈거의없다안꾼다잠꼬대, , , , ,
|
|
||||||
무서운꿈죽은사람 꿈쫓기는 꿈개꿈기억안남기억남, , , , ,
|
|
||||||
심장
|
|
||||||
기울증상
|
|
||||||
전 신
|
|
||||||
가슴뜀가슴답답가슴뻐근한숨쉼호흡곤란숨참뒷목뻐근, , , , , ,
|
|
||||||
열달아오름 자주: ( 일 회가끔), ( 주 회)
|
|
||||||
잘놀람,불안,초조,우울,비관,신경질,짜증,매사귀찮다,손떨림,졸도
|
|
||||||
가슴막힌 듯조이는 듯기억력격감건망증현기증눈피로감, , , , ,
|
|
||||||
피로기운이 없다아침에 잘 못일어난다의욕이 없다무겁다, , , ,
|
|
||||||
결 혼
|
|
||||||
유 산
|
|
||||||
생리주기
|
|
||||||
생리량
|
|
||||||
생리통
|
|
||||||
냉대하
|
|
||||||
미혼, 결 혼 년, 출 산 회,불 임 년
|
|
||||||
유 산자연 ( 회인공 , 회제 왕 ), 회
|
|
||||||
자궁근종적 출 , 년전초산시난산순산, ( , )
|
|
||||||
일 간격정상부정확건넘중단폐경계속나옴, , , , , ,
|
|
||||||
일간 일 많고 일 적다늦어짐빨라짐, ,
|
|
||||||
너무많다, 많다 , 보통 , 약간적다 , 아주적다 , 줄어듬
|
|
||||||
검붉다검다일부덩어리찌꺼기묽다, , , ,
|
|
||||||
없다약간심하다극심생리전중, , , , ( ) ∙ 일부터 일간
|
|
||||||
아랫배허리허벅지가슴머리전신몸살과민, , , , , ,
|
|
||||||
없다약간많다심하다투명누렇다, , , , ,
|
|
||||||
희다묽다냄새악취가렵다, , , ,
|
|
||||||
피 부
|
|
||||||
성 품
|
|
||||||
보통약간황색약간검다희다창백누렇다약간붉다, , , , , ,
|
|
||||||
보통섬세얇다약간두텁다두텁다지성중성건성, , , , , , ,
|
|
||||||
○ 저돌적이다기세가 강하다남의 말을 잘 듣지 않는다, ,
|
|
||||||
거침이 없다독불장군이다안하무인이다뚜렷하다, , ,
|
|
||||||
○ 말과 행동 빠름, 음식을 빨리먹음 , 눈매예리, 날카롭다
|
|
||||||
부지런함적극적활동적소변자주본다일을 안미룬다 , , , ,
|
|
||||||
나 다니기를 좋아함분명하다나서기 잘함질투가 심하다 , , ,
|
|
||||||
○ 눕기 좋아함엉덩이 무겁다느긋함땀이많다과묵하다, , , ,
|
|
||||||
사람좋다무던하다부드럽다가정적이다씻기를 싫어함, , , ,
|
|
||||||
원만정중은근꾸준함우유부단된장쓴 것 좋아함, , , , , ,
|
|
||||||
○ 약해보인다겁이많음세심하다소심차분함자상함, , , , ,
|
|
||||||
연약영민잘미룬다깐깐치밀궁리는 많으나 실행은 적
|
|
||||||
@ -595,39 +595,20 @@ $(document).ready(function() {
|
|||||||
const cheopTotal = parseFloat($('#cheopTotal').val()) || 0;
|
const cheopTotal = parseFloat($('#cheopTotal').val()) || 0;
|
||||||
const totalGrams = ing.grams_per_cheop * cheopTotal;
|
const totalGrams = ing.grams_per_cheop * cheopTotal;
|
||||||
|
|
||||||
// 제품 선택 옵션 생성
|
|
||||||
let productOptions = '<option value="">제품 선택</option>';
|
|
||||||
if (ing.available_products && ing.available_products.length > 0) {
|
|
||||||
ing.available_products.forEach(product => {
|
|
||||||
const specInfo = product.specification ? ` [${product.specification}]` : '';
|
|
||||||
productOptions += `<option value="${product.herb_item_id}">${product.herb_name}${specInfo} (재고: ${product.stock.toFixed(0)}g)</option>`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#compoundIngredients').append(`
|
$('#compoundIngredients').append(`
|
||||||
<tr data-ingredient-code="${ing.ingredient_code}" data-herb-id="">
|
<tr data-herb-id="${ing.herb_item_id}">
|
||||||
<td>
|
<td>${ing.herb_name}</td>
|
||||||
${ing.herb_name}
|
|
||||||
${ing.total_available_stock > 0
|
|
||||||
? `<small class="text-success">(총 ${ing.total_available_stock.toFixed(0)}g 사용 가능)</small>`
|
|
||||||
: '<small class="text-danger">(재고 없음)</small>'}
|
|
||||||
</td>
|
|
||||||
<td>
|
<td>
|
||||||
<input type="number" class="form-control form-control-sm grams-per-cheop"
|
<input type="number" class="form-control form-control-sm grams-per-cheop"
|
||||||
value="${ing.grams_per_cheop}" min="0.1" step="0.1">
|
value="${ing.grams_per_cheop}" min="0.1" step="0.1">
|
||||||
</td>
|
</td>
|
||||||
<td class="total-grams">${totalGrams.toFixed(1)}</td>
|
<td class="total-grams">${totalGrams.toFixed(1)}</td>
|
||||||
<td class="product-select-cell">
|
|
||||||
<select class="form-control form-control-sm product-select" ${ing.available_products.length === 0 ? 'disabled' : ''}>
|
|
||||||
${productOptions}
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td class="origin-select-cell">
|
<td class="origin-select-cell">
|
||||||
<select class="form-control form-control-sm origin-select" disabled>
|
<select class="form-control form-control-sm origin-select" disabled>
|
||||||
<option value="">제품 먼저 선택</option>
|
<option value="">로딩중...</option>
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td class="stock-status">대기중</td>
|
<td class="stock-status">확인중...</td>
|
||||||
<td>
|
<td>
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger remove-compound-ingredient">
|
<button type="button" class="btn btn-sm btn-outline-danger remove-compound-ingredient">
|
||||||
<i class="bi bi-x"></i>
|
<i class="bi bi-x"></i>
|
||||||
@ -636,54 +617,15 @@ $(document).ready(function() {
|
|||||||
</tr>
|
</tr>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// 첫 번째 제품 자동 선택 및 원산지 로드
|
// 각 약재별로 원산지별 재고 확인
|
||||||
const tr = $(`tr[data-ingredient-code="${ing.ingredient_code}"]`);
|
loadOriginOptions(ing.herb_item_id, totalGrams);
|
||||||
if (ing.available_products && ing.available_products.length > 0) {
|
|
||||||
const firstProduct = ing.available_products[0];
|
|
||||||
tr.find('.product-select').val(firstProduct.herb_item_id);
|
|
||||||
tr.attr('data-herb-id', firstProduct.herb_item_id);
|
|
||||||
// 원산지/로트 옵션 로드
|
|
||||||
loadOriginOptions(firstProduct.herb_item_id, totalGrams);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 재고 확인
|
// 재고 확인
|
||||||
checkStockForCompound();
|
checkStockForCompound();
|
||||||
|
|
||||||
// 제품 선택 변경 이벤트
|
|
||||||
$('.product-select').on('change', function() {
|
|
||||||
const herbId = $(this).val();
|
|
||||||
const row = $(this).closest('tr');
|
|
||||||
|
|
||||||
if (herbId) {
|
|
||||||
row.attr('data-herb-id', herbId);
|
|
||||||
const cheopTotal = parseFloat($('#cheopTotal').val()) || 0;
|
|
||||||
const gramsPerCheop = parseFloat(row.find('.grams-per-cheop').val()) || 0;
|
|
||||||
const totalGrams = gramsPerCheop * cheopTotal;
|
|
||||||
|
|
||||||
// 원산지/로트 옵션 로드
|
|
||||||
loadOriginOptions(herbId, totalGrams);
|
|
||||||
} else {
|
|
||||||
row.attr('data-herb-id', '');
|
|
||||||
row.find('.origin-select').empty().append('<option value="">제품 먼저 선택</option>').prop('disabled', true);
|
|
||||||
row.find('.stock-status').text('대기중');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 용량 변경 이벤트
|
// 용량 변경 이벤트
|
||||||
$('.grams-per-cheop').on('input', function() {
|
$('.grams-per-cheop').on('input', updateIngredientTotals);
|
||||||
updateIngredientTotals();
|
|
||||||
|
|
||||||
// 원산지 옵션 다시 로드
|
|
||||||
const row = $(this).closest('tr');
|
|
||||||
const herbId = row.attr('data-herb-id');
|
|
||||||
if (herbId) {
|
|
||||||
const cheopTotal = parseFloat($('#cheopTotal').val()) || 0;
|
|
||||||
const gramsPerCheop = parseFloat($(this).val()) || 0;
|
|
||||||
const totalGrams = gramsPerCheop * cheopTotal;
|
|
||||||
loadOriginOptions(herbId, totalGrams);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 삭제 버튼 이벤트
|
// 삭제 버튼 이벤트
|
||||||
$('.remove-compound-ingredient').on('click', function() {
|
$('.remove-compound-ingredient').on('click', function() {
|
||||||
|
|||||||
@ -1,881 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ko">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
||||||
<title>건강 상담 사전 문진표</title>
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css">
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--primary-color: #4CAF50;
|
|
||||||
--secondary-color: #2196F3;
|
|
||||||
--accent-color: #FF9800;
|
|
||||||
--text-dark: #333;
|
|
||||||
--bg-light: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.survey-container {
|
|
||||||
max-width: 600px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0;
|
|
||||||
min-height: 100vh;
|
|
||||||
background: white;
|
|
||||||
box-shadow: 0 0 30px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.survey-header {
|
|
||||||
background: linear-gradient(135deg, var(--primary-color) 0%, #45a049 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 20px;
|
|
||||||
text-align: center;
|
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.survey-header h1 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
margin: 0;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.survey-header p {
|
|
||||||
margin: 5px 0 0 0;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
opacity: 0.95;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar-container {
|
|
||||||
background: white;
|
|
||||||
padding: 15px 20px;
|
|
||||||
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
|
||||||
position: sticky;
|
|
||||||
top: 90px;
|
|
||||||
z-index: 99;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress {
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar {
|
|
||||||
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
|
|
||||||
border-radius: 10px;
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-text {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: #666;
|
|
||||||
margin-top: 5px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-section {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
border-bottom: 2px solid var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-icon {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
background: linear-gradient(135deg, var(--primary-color), #45a049);
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: white;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-title {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-card {
|
|
||||||
background: white;
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-card:hover {
|
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-text {
|
|
||||||
font-size: 1.05rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-dark);
|
|
||||||
margin-bottom: 5px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-required {
|
|
||||||
color: #f44336;
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-subtext {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: #757575;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Radio/Checkbox 스타일 */
|
|
||||||
.option-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px 15px;
|
|
||||||
background: #f9f9f9;
|
|
||||||
border: 2px solid #e0e0e0;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-label:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-label input {
|
|
||||||
margin-right: 12px;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-label:hover {
|
|
||||||
background: #f0f7ff;
|
|
||||||
border-color: var(--secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-label input:checked + span {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-label:has(input:checked) {
|
|
||||||
background: #e8f5e9;
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Text/Number 입력 */
|
|
||||||
.form-control {
|
|
||||||
border: 2px solid #e0e0e0;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 12px 15px;
|
|
||||||
font-size: 1rem;
|
|
||||||
transition: border-color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control:focus {
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 버튼 */
|
|
||||||
.btn-group-sticky {
|
|
||||||
position: sticky;
|
|
||||||
bottom: 0;
|
|
||||||
background: white;
|
|
||||||
padding: 15px 20px;
|
|
||||||
box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
z-index: 98;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
flex: 1;
|
|
||||||
padding: 14px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: linear-gradient(135deg, var(--primary-color), #45a049);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: #f5f5f5;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-save {
|
|
||||||
background: linear-gradient(135deg, var(--accent-color), #f57c00);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-complete {
|
|
||||||
background: linear-gradient(135deg, var(--secondary-color), #1976d2);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 로딩 스피너 */
|
|
||||||
.loading {
|
|
||||||
text-align: center;
|
|
||||||
padding: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner-border {
|
|
||||||
width: 3rem;
|
|
||||||
height: 3rem;
|
|
||||||
border-width: 0.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 완료 화면 */
|
|
||||||
.completion-screen {
|
|
||||||
text-align: center;
|
|
||||||
padding: 60px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.completion-icon {
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
background: linear-gradient(135deg, var(--primary-color), #45a049);
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 0 auto 20px;
|
|
||||||
animation: scaleIn 0.5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.completion-icon i {
|
|
||||||
font-size: 3rem;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes scaleIn {
|
|
||||||
from {
|
|
||||||
transform: scale(0);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.completion-title {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-dark);
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.completion-message {
|
|
||||||
font-size: 1rem;
|
|
||||||
color: #757575;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 카테고리 네비게이션 */
|
|
||||||
.category-nav {
|
|
||||||
display: flex;
|
|
||||||
overflow-x: auto;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 15px 20px;
|
|
||||||
background: white;
|
|
||||||
border-bottom: 1px solid #e0e0e0;
|
|
||||||
position: sticky;
|
|
||||||
top: 90px;
|
|
||||||
z-index: 97;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-nav::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-nav-item {
|
|
||||||
flex-shrink: 0;
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 20px;
|
|
||||||
background: #f5f5f5;
|
|
||||||
color: #666;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-nav-item.active {
|
|
||||||
background: var(--primary-color);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-nav-item.completed {
|
|
||||||
background: #e8f5e9;
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 반응형 */
|
|
||||||
@media (max-width: 576px) {
|
|
||||||
.survey-header h1 {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-card {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-label {
|
|
||||||
padding: 10px 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="survey-container">
|
|
||||||
<!-- 헤더 -->
|
|
||||||
<div class="survey-header">
|
|
||||||
<h1><i class="bi bi-clipboard2-pulse"></i> 건강 상담 사전 문진표</h1>
|
|
||||||
<p id="patientInfo">환자명: <span id="patientName">-</span></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 진행률 -->
|
|
||||||
<div class="progress-bar-container">
|
|
||||||
<div class="progress">
|
|
||||||
<div class="progress-bar" role="progressbar" style="width: 0%" id="progressBar"></div>
|
|
||||||
</div>
|
|
||||||
<div class="progress-text">
|
|
||||||
<span id="progressText">0 / 0</span> 완료
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 카테고리 네비게이션 -->
|
|
||||||
<div class="category-nav" id="categoryNav" style="display: none;">
|
|
||||||
<!-- 동적 생성 -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 메인 컨텐츠 -->
|
|
||||||
<div id="surveyContent">
|
|
||||||
<!-- 로딩 -->
|
|
||||||
<div class="loading" id="loadingScreen">
|
|
||||||
<div class="spinner-border text-primary" role="status">
|
|
||||||
<span class="visually-hidden">Loading...</span>
|
|
||||||
</div>
|
|
||||||
<p class="mt-3">문진표를 불러오는 중...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 문진 폼 -->
|
|
||||||
<div id="surveyForm" style="display: none;">
|
|
||||||
<!-- 동적으로 생성됨 -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 완료 화면 -->
|
|
||||||
<div id="completionScreen" style="display: none;">
|
|
||||||
<div class="completion-screen">
|
|
||||||
<div class="completion-icon">
|
|
||||||
<i class="bi bi-check-circle-fill"></i>
|
|
||||||
</div>
|
|
||||||
<h2 class="completion-title">제출 완료!</h2>
|
|
||||||
<p class="completion-message">
|
|
||||||
문진표가 성공적으로 제출되었습니다.<br>
|
|
||||||
담당 한의사가 확인 후 연락드리겠습니다.<br>
|
|
||||||
감사합니다.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 하단 버튼 -->
|
|
||||||
<div class="btn-group-sticky" id="buttonGroup" style="display: none;">
|
|
||||||
<button class="btn btn-save" id="saveBtn">
|
|
||||||
<i class="bi bi-bookmark"></i> 임시저장
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-complete" id="completeBtn">
|
|
||||||
<i class="bi bi-check-circle"></i> 제출하기
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script>
|
|
||||||
const surveyToken = window.location.pathname.split('/').pop();
|
|
||||||
let surveyData = null;
|
|
||||||
let categories = [];
|
|
||||||
let templates = [];
|
|
||||||
let responses = {};
|
|
||||||
let currentCategoryIndex = 0;
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
|
||||||
loadSurvey();
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadSurvey() {
|
|
||||||
try {
|
|
||||||
// 문진표 정보 로드
|
|
||||||
const surveyResponse = await $.get(`/api/surveys/${surveyToken}`);
|
|
||||||
surveyData = surveyResponse.data;
|
|
||||||
|
|
||||||
// 환자 정보 표시
|
|
||||||
if (surveyData.patient_name) {
|
|
||||||
$('#patientName').text(surveyData.patient_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 카테고리 목록 로드
|
|
||||||
const categoriesResponse = await $.get('/api/surveys/categories');
|
|
||||||
categories = categoriesResponse.data;
|
|
||||||
|
|
||||||
// 템플릿 로드
|
|
||||||
const templatesResponse = await $.get('/api/surveys/templates');
|
|
||||||
templates = templatesResponse.data;
|
|
||||||
|
|
||||||
// 기존 응답 로드
|
|
||||||
if (surveyData.responses && surveyData.responses.length > 0) {
|
|
||||||
surveyData.responses.forEach(resp => {
|
|
||||||
try {
|
|
||||||
responses[resp.question_code] = JSON.parse(resp.answer_value);
|
|
||||||
} catch (e) {
|
|
||||||
responses[resp.question_code] = resp.answer_value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// UI 렌더링
|
|
||||||
renderCategoryNav();
|
|
||||||
renderSurveyForm();
|
|
||||||
updateProgress();
|
|
||||||
|
|
||||||
$('#loadingScreen').hide();
|
|
||||||
$('#surveyForm').show();
|
|
||||||
$('#buttonGroup').show();
|
|
||||||
$('#categoryNav').show();
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading survey:', error);
|
|
||||||
alert('문진표를 불러오는데 실패했습니다.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderCategoryNav() {
|
|
||||||
const nav = $('#categoryNav');
|
|
||||||
nav.empty();
|
|
||||||
|
|
||||||
categories.forEach((cat, index) => {
|
|
||||||
const navItem = $(`
|
|
||||||
<div class="category-nav-item" data-index="${index}">
|
|
||||||
${cat.category_name}
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
|
|
||||||
navItem.on('click', function() {
|
|
||||||
scrollToCategory(index);
|
|
||||||
});
|
|
||||||
|
|
||||||
nav.append(navItem);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderSurveyForm() {
|
|
||||||
const form = $('#surveyForm');
|
|
||||||
form.empty();
|
|
||||||
|
|
||||||
categories.forEach((cat, catIndex) => {
|
|
||||||
const categoryQuestions = templates.filter(t => t.category === cat.category);
|
|
||||||
|
|
||||||
const section = $(`
|
|
||||||
<div class="category-section" id="category-${catIndex}">
|
|
||||||
<div class="category-header">
|
|
||||||
<div class="category-icon">
|
|
||||||
<i class="bi bi-${getCategoryIcon(cat.category)}"></i>
|
|
||||||
</div>
|
|
||||||
<div class="category-title">${cat.category_name}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
|
|
||||||
categoryQuestions.forEach(question => {
|
|
||||||
const questionCard = renderQuestion(question);
|
|
||||||
section.append(questionCard);
|
|
||||||
});
|
|
||||||
|
|
||||||
form.append(section);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderQuestion(question) {
|
|
||||||
const card = $(`
|
|
||||||
<div class="question-card" data-code="${question.question_code}">
|
|
||||||
<div class="question-text">
|
|
||||||
${question.question_text}
|
|
||||||
${question.is_required ? '<span class="question-required">*</span>' : ''}
|
|
||||||
</div>
|
|
||||||
${question.question_subtext ? `<div class="question-subtext">${question.question_subtext}</div>` : ''}
|
|
||||||
<div class="answer-area"></div>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
|
|
||||||
const answerArea = card.find('.answer-area');
|
|
||||||
const savedValue = responses[question.question_code];
|
|
||||||
|
|
||||||
switch (question.input_type) {
|
|
||||||
case 'RADIO':
|
|
||||||
answerArea.append(renderRadio(question, savedValue));
|
|
||||||
break;
|
|
||||||
case 'CHECKBOX':
|
|
||||||
answerArea.append(renderCheckbox(question, savedValue));
|
|
||||||
break;
|
|
||||||
case 'TEXT':
|
|
||||||
case 'TEXTAREA':
|
|
||||||
answerArea.append(renderText(question, savedValue));
|
|
||||||
break;
|
|
||||||
case 'NUMBER':
|
|
||||||
answerArea.append(renderNumber(question, savedValue));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return card;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderRadio(question, savedValue) {
|
|
||||||
const group = $('<div class="option-group"></div>');
|
|
||||||
|
|
||||||
question.options.forEach((option, index) => {
|
|
||||||
const isChecked = savedValue === option ? 'checked' : '';
|
|
||||||
const label = $(`
|
|
||||||
<label class="option-label">
|
|
||||||
<input type="radio"
|
|
||||||
name="${question.question_code}"
|
|
||||||
value="${option}"
|
|
||||||
${isChecked}
|
|
||||||
data-code="${question.question_code}"
|
|
||||||
data-category="${question.category}">
|
|
||||||
<span>${option}</span>
|
|
||||||
</label>
|
|
||||||
`);
|
|
||||||
|
|
||||||
label.find('input').on('change', function() {
|
|
||||||
saveResponse(question.question_code, question.category, this.value, 'SINGLE');
|
|
||||||
updateProgress();
|
|
||||||
});
|
|
||||||
|
|
||||||
group.append(label);
|
|
||||||
});
|
|
||||||
|
|
||||||
return group;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderCheckbox(question, savedValue) {
|
|
||||||
const group = $('<div class="option-group"></div>');
|
|
||||||
const savedArray = Array.isArray(savedValue) ? savedValue : [];
|
|
||||||
|
|
||||||
question.options.forEach((option, index) => {
|
|
||||||
const isChecked = savedArray.includes(option) ? 'checked' : '';
|
|
||||||
const label = $(`
|
|
||||||
<label class="option-label">
|
|
||||||
<input type="checkbox"
|
|
||||||
name="${question.question_code}[]"
|
|
||||||
value="${option}"
|
|
||||||
${isChecked}
|
|
||||||
data-code="${question.question_code}"
|
|
||||||
data-category="${question.category}">
|
|
||||||
<span>${option}</span>
|
|
||||||
</label>
|
|
||||||
`);
|
|
||||||
|
|
||||||
label.find('input').on('change', function() {
|
|
||||||
const checked = $(`input[name="${question.question_code}[]"]:checked`).map(function() {
|
|
||||||
return $(this).val();
|
|
||||||
}).get();
|
|
||||||
saveResponse(question.question_code, question.category, checked, 'MULTIPLE');
|
|
||||||
updateProgress();
|
|
||||||
});
|
|
||||||
|
|
||||||
group.append(label);
|
|
||||||
});
|
|
||||||
|
|
||||||
return group;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderText(question, savedValue) {
|
|
||||||
const isTextarea = question.input_type === 'TEXTAREA';
|
|
||||||
const input = $(`
|
|
||||||
<${isTextarea ? 'textarea' : 'input'}
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
placeholder="답변을 입력하세요"
|
|
||||||
data-code="${question.question_code}"
|
|
||||||
data-category="${question.category}"
|
|
||||||
${isTextarea ? 'rows="3"' : ''}
|
|
||||||
>${savedValue || ''}</${isTextarea ? 'textarea' : 'input'}>
|
|
||||||
`);
|
|
||||||
|
|
||||||
input.on('blur', function() {
|
|
||||||
const value = $(this).val().trim();
|
|
||||||
if (value) {
|
|
||||||
saveResponse(question.question_code, question.category, value, 'TEXT');
|
|
||||||
updateProgress();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderNumber(question, savedValue) {
|
|
||||||
const input = $(`
|
|
||||||
<input type="number"
|
|
||||||
class="form-control"
|
|
||||||
placeholder="숫자를 입력하세요"
|
|
||||||
value="${savedValue || ''}"
|
|
||||||
data-code="${question.question_code}"
|
|
||||||
data-category="${question.category}">
|
|
||||||
`);
|
|
||||||
|
|
||||||
input.on('blur', function() {
|
|
||||||
const value = $(this).val();
|
|
||||||
if (value) {
|
|
||||||
saveResponse(question.question_code, question.category, value, 'NUMBER');
|
|
||||||
updateProgress();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveResponse(questionCode, category, value, answerType) {
|
|
||||||
responses[questionCode] = value;
|
|
||||||
|
|
||||||
// 로컬 스토리지에도 저장 (임시저장용)
|
|
||||||
localStorage.setItem(`survey_${surveyToken}`, JSON.stringify(responses));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveToServer() {
|
|
||||||
try {
|
|
||||||
const responseArray = [];
|
|
||||||
|
|
||||||
Object.keys(responses).forEach(code => {
|
|
||||||
const template = templates.find(t => t.question_code === code);
|
|
||||||
if (template) {
|
|
||||||
responseArray.push({
|
|
||||||
category: template.category,
|
|
||||||
question_code: code,
|
|
||||||
question_text: template.question_text,
|
|
||||||
answer_value: responses[code],
|
|
||||||
answer_type: Array.isArray(responses[code]) ? 'MULTIPLE' : 'SINGLE'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await $.ajax({
|
|
||||||
url: `/api/surveys/${surveyToken}/responses`,
|
|
||||||
method: 'POST',
|
|
||||||
contentType: 'application/json',
|
|
||||||
data: JSON.stringify({ responses: responseArray })
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Save error:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateProgress() {
|
|
||||||
const totalQuestions = templates.filter(t => t.is_required).length;
|
|
||||||
const answeredQuestions = templates.filter(t =>
|
|
||||||
t.is_required && responses[t.question_code] !== undefined && responses[t.question_code] !== ''
|
|
||||||
).length;
|
|
||||||
|
|
||||||
const percentage = totalQuestions > 0 ? (answeredQuestions / totalQuestions * 100) : 0;
|
|
||||||
|
|
||||||
$('#progressBar').css('width', percentage + '%');
|
|
||||||
$('#progressText').text(`${answeredQuestions} / ${totalQuestions}`);
|
|
||||||
|
|
||||||
// 카테고리별 완료 상태 업데이트
|
|
||||||
updateCategoryStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCategoryStatus() {
|
|
||||||
categories.forEach((cat, index) => {
|
|
||||||
const catQuestions = templates.filter(t => t.category === cat.category && t.is_required);
|
|
||||||
const catAnswered = catQuestions.filter(q => responses[q.question_code] !== undefined).length;
|
|
||||||
const isCompleted = catAnswered === catQuestions.length && catQuestions.length > 0;
|
|
||||||
|
|
||||||
const navItem = $(`.category-nav-item[data-index="${index}"]`);
|
|
||||||
if (isCompleted) {
|
|
||||||
navItem.addClass('completed');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function scrollToCategory(index) {
|
|
||||||
const target = $(`#category-${index}`);
|
|
||||||
if (target.length) {
|
|
||||||
$('html, body').animate({
|
|
||||||
scrollTop: target.offset().top - 160
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
$('.category-nav-item').removeClass('active');
|
|
||||||
$(`.category-nav-item[data-index="${index}"]`).addClass('active');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCategoryIcon(category) {
|
|
||||||
const icons = {
|
|
||||||
'TEMPERATURE': 'thermometer-half',
|
|
||||||
'BODY_TEMP': 'heart-pulse',
|
|
||||||
'DIGESTION': 'cup-straw',
|
|
||||||
'EXCRETION': 'water',
|
|
||||||
'SLEEP': 'moon-stars',
|
|
||||||
'CIRCULATION': 'activity',
|
|
||||||
'WOMEN_HEALTH': 'gender-female',
|
|
||||||
'SKIN': 'palette',
|
|
||||||
'PERSONALITY': 'person-circle'
|
|
||||||
};
|
|
||||||
return icons[category] || 'question-circle';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 임시저장 버튼
|
|
||||||
$('#saveBtn').on('click', async function() {
|
|
||||||
const btn = $(this);
|
|
||||||
btn.prop('disabled', true).html('<i class="bi bi-hourglass-split"></i> 저장중...');
|
|
||||||
|
|
||||||
const success = await saveToServer();
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
btn.html('<i class="bi bi-check"></i> 저장됨');
|
|
||||||
setTimeout(() => {
|
|
||||||
btn.prop('disabled', false).html('<i class="bi bi-bookmark"></i> 임시저장');
|
|
||||||
}, 2000);
|
|
||||||
} else {
|
|
||||||
alert('저장에 실패했습니다.');
|
|
||||||
btn.prop('disabled', false).html('<i class="bi bi-bookmark"></i> 임시저장');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 제출 버튼
|
|
||||||
$('#completeBtn').on('click', async function() {
|
|
||||||
// 필수 항목 체크
|
|
||||||
const requiredQuestions = templates.filter(t => t.is_required);
|
|
||||||
const unanswered = requiredQuestions.filter(q =>
|
|
||||||
responses[q.question_code] === undefined || responses[q.question_code] === ''
|
|
||||||
);
|
|
||||||
|
|
||||||
if (unanswered.length > 0) {
|
|
||||||
alert(`필수 항목 ${unanswered.length}개가 미응답 상태입니다.\n모든 필수 항목을 작성해주세요.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!confirm('문진표를 제출하시겠습니까?')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const btn = $(this);
|
|
||||||
btn.prop('disabled', true).html('<i class="bi bi-hourglass-split"></i> 제출중...');
|
|
||||||
|
|
||||||
// 서버에 저장
|
|
||||||
const saveSuccess = await saveToServer();
|
|
||||||
if (!saveSuccess) {
|
|
||||||
alert('저장에 실패했습니다.');
|
|
||||||
btn.prop('disabled', false).html('<i class="bi bi-check-circle"></i> 제출하기');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 완료 처리
|
|
||||||
try {
|
|
||||||
await $.ajax({
|
|
||||||
url: `/api/surveys/${surveyToken}/complete`,
|
|
||||||
method: 'POST'
|
|
||||||
});
|
|
||||||
|
|
||||||
// 완료 화면 표시
|
|
||||||
$('#surveyForm').hide();
|
|
||||||
$('#buttonGroup').hide();
|
|
||||||
$('#categoryNav').hide();
|
|
||||||
$('#completionScreen').show();
|
|
||||||
|
|
||||||
// 로컬 스토리지 삭제
|
|
||||||
localStorage.removeItem(`survey_${surveyToken}`);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
alert('제출에 실패했습니다.');
|
|
||||||
btn.prop('disabled', false).html('<i class="bi bi-check-circle"></i> 제출하기');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 스크롤 이벤트로 현재 카테고리 하이라이트
|
|
||||||
$(window).on('scroll', function() {
|
|
||||||
const scrollPos = $(window).scrollTop() + 200;
|
|
||||||
|
|
||||||
$('.category-section').each(function(index) {
|
|
||||||
const top = $(this).offset().top;
|
|
||||||
const bottom = top + $(this).outerHeight();
|
|
||||||
|
|
||||||
if (scrollPos >= top && scrollPos < bottom) {
|
|
||||||
$('.category-nav-item').removeClass('active');
|
|
||||||
$(`.category-nav-item[data-index="${index}"]`).addClass('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 로컬 스토리지에서 임시 저장된 데이터 복원
|
|
||||||
$(document).ready(function() {
|
|
||||||
const saved = localStorage.getItem(`survey_${surveyToken}`);
|
|
||||||
if (saved) {
|
|
||||||
try {
|
|
||||||
const savedResponses = JSON.parse(saved);
|
|
||||||
if (Object.keys(savedResponses).length > Object.keys(responses).length) {
|
|
||||||
if (confirm('이전에 작성 중이던 내용이 있습니다. 불러오시겠습니까?')) {
|
|
||||||
responses = savedResponses;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to restore saved data:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,165 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
E2E 테스트: 조제 화면에서 쌍화탕 선택 후 인삼 선택 가능 확인
|
|
||||||
"""
|
|
||||||
|
|
||||||
from playwright.sync_api import sync_playwright, expect
|
|
||||||
import time
|
|
||||||
|
|
||||||
def test_compound_ginseng_selection():
|
|
||||||
"""쌍화탕 조제 시 인삼 선택 가능 테스트"""
|
|
||||||
|
|
||||||
with sync_playwright() as p:
|
|
||||||
# 브라우저 실행 (headless 모드)
|
|
||||||
browser = p.chromium.launch(headless=True)
|
|
||||||
page = browser.new_page()
|
|
||||||
|
|
||||||
try:
|
|
||||||
print("=" * 80)
|
|
||||||
print("E2E 테스트: 쌍화탕 조제 시 인삼 선택 가능 확인")
|
|
||||||
print("=" * 80)
|
|
||||||
|
|
||||||
# 1. 메인 페이지 접속
|
|
||||||
print("\n[1] 메인 페이지 접속...")
|
|
||||||
page.goto('http://localhost:5001')
|
|
||||||
page.wait_for_load_state('networkidle')
|
|
||||||
print("✓ 페이지 로드 완료")
|
|
||||||
|
|
||||||
# 2. 조제관리 메뉴 클릭
|
|
||||||
print("\n[2] 조제관리 메뉴 클릭...")
|
|
||||||
|
|
||||||
# 사이드바에서 조제 관리 클릭
|
|
||||||
compound_menu = page.locator('text=조제 관리').first
|
|
||||||
compound_menu.click()
|
|
||||||
time.sleep(2)
|
|
||||||
print("✓ 조제관리 화면 진입")
|
|
||||||
|
|
||||||
# 3. 현재 화면 상태 확인
|
|
||||||
print("\n[3] 화면 상태 확인...")
|
|
||||||
|
|
||||||
# 스크린샷 저장
|
|
||||||
page.screenshot(path='/tmp/compound_screen_after_menu_click.png')
|
|
||||||
print("✓ 스크린샷: /tmp/compound_screen_after_menu_click.png")
|
|
||||||
|
|
||||||
# 페이지에 select 요소가 있는지 확인
|
|
||||||
all_selects = page.locator('select').all()
|
|
||||||
print(f"✓ 페이지 내 select 요소: {len(all_selects)}개")
|
|
||||||
|
|
||||||
for idx, sel in enumerate(all_selects):
|
|
||||||
sel_id = sel.get_attribute('id')
|
|
||||||
sel_name = sel.get_attribute('name')
|
|
||||||
print(f" [{idx}] id={sel_id}, name={sel_name}")
|
|
||||||
|
|
||||||
# 처방 선택 시도
|
|
||||||
print("\n[4] 처방 선택...")
|
|
||||||
|
|
||||||
# 처방 드롭다운 찾기 (유연하게)
|
|
||||||
formula_select = page.locator('select').first
|
|
||||||
|
|
||||||
if formula_select.count() > 0:
|
|
||||||
# 옵션 확인
|
|
||||||
options = formula_select.locator('option').all()
|
|
||||||
print(f"✓ 드롭다운 옵션: {len(options)}개")
|
|
||||||
for opt in options:
|
|
||||||
print(f" - {opt.text_content()}")
|
|
||||||
|
|
||||||
# 쌍화탕 선택
|
|
||||||
try:
|
|
||||||
formula_select.select_option(label='쌍화탕')
|
|
||||||
time.sleep(3)
|
|
||||||
print("✓ 쌍화탕 선택 완료")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"⚠️ label로 선택 실패: {e}")
|
|
||||||
# index로 시도
|
|
||||||
formula_select.select_option(index=1)
|
|
||||||
time.sleep(3)
|
|
||||||
print("✓ 첫 번째 처방 선택 완료")
|
|
||||||
else:
|
|
||||||
print("❌ 처방 드롭다운을 찾을 수 없음")
|
|
||||||
|
|
||||||
# 4. 약재 목록 확인
|
|
||||||
print("\n[4] 약재 목록 확인...")
|
|
||||||
|
|
||||||
# 약재 테이블이나 목록이 나타날 때까지 대기
|
|
||||||
page.wait_for_selector('table, .ingredient-list', timeout=10000)
|
|
||||||
|
|
||||||
# 페이지 스크린샷
|
|
||||||
page.screenshot(path='/tmp/compound_screen_1.png')
|
|
||||||
print("✓ 스크린샷 저장: /tmp/compound_screen_1.png")
|
|
||||||
|
|
||||||
# 5. 인삼 항목 찾기
|
|
||||||
print("\n[5] 인삼 항목 찾기...")
|
|
||||||
|
|
||||||
# 인삼을 포함하는 행 찾기
|
|
||||||
ginseng_row = page.locator('tr:has-text("인삼"), div:has-text("인삼")').first
|
|
||||||
|
|
||||||
if ginseng_row.count() > 0:
|
|
||||||
print("✓ 인삼 항목 발견")
|
|
||||||
|
|
||||||
# 6. 제품 선택 드롭다운 확인
|
|
||||||
print("\n[6] 제품 선택 드롭다운 확인...")
|
|
||||||
|
|
||||||
# 인삼 행에서 select 요소 찾기
|
|
||||||
product_select = ginseng_row.locator('select').first
|
|
||||||
|
|
||||||
if product_select.count() > 0:
|
|
||||||
print("✓ 제품 선택 드롭다운 발견")
|
|
||||||
|
|
||||||
# 옵션 개수 확인
|
|
||||||
options = product_select.locator('option').all()
|
|
||||||
print(f"✓ 사용 가능한 제품: {len(options)}개")
|
|
||||||
|
|
||||||
# 각 옵션 출력
|
|
||||||
for idx, option in enumerate(options):
|
|
||||||
text = option.text_content()
|
|
||||||
value = option.get_attribute('value')
|
|
||||||
print(f" [{idx}] {text} (value: {value})")
|
|
||||||
|
|
||||||
# 신흥인삼 또는 세화인삼 선택 가능한지 확인
|
|
||||||
has_shinheung = any('신흥인삼' in opt.text_content() for opt in options)
|
|
||||||
has_sehwa = any('세화인삼' in opt.text_content() for opt in options)
|
|
||||||
|
|
||||||
if has_shinheung or has_sehwa:
|
|
||||||
print("\n✅ 인삼 제품 선택 가능!")
|
|
||||||
|
|
||||||
# 첫 번째 제품 선택 시도
|
|
||||||
if len(options) > 0:
|
|
||||||
product_select.select_option(index=0)
|
|
||||||
print(f"✓ '{options[0].text_content()}' 선택 완료")
|
|
||||||
else:
|
|
||||||
print("\n❌ 인삼 대체 제품이 드롭다운에 없음")
|
|
||||||
else:
|
|
||||||
print("❌ 제품 선택 드롭다운을 찾을 수 없음")
|
|
||||||
print("페이지 HTML 일부:")
|
|
||||||
print(ginseng_row.inner_html()[:500])
|
|
||||||
else:
|
|
||||||
print("❌ 인삼 항목을 찾을 수 없음")
|
|
||||||
print("\n페이지 내용:")
|
|
||||||
print(page.content()[:2000])
|
|
||||||
|
|
||||||
# 7. 최종 스크린샷
|
|
||||||
page.screenshot(path='/tmp/compound_screen_final.png')
|
|
||||||
print("\n✓ 최종 스크린샷: /tmp/compound_screen_final.png")
|
|
||||||
|
|
||||||
print("\n" + "=" * 80)
|
|
||||||
print("테스트 완료")
|
|
||||||
print("=" * 80)
|
|
||||||
|
|
||||||
# 완료
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"\n❌ 에러 발생: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
# 에러 스크린샷
|
|
||||||
page.screenshot(path='/tmp/compound_error.png')
|
|
||||||
print("에러 스크린샷: /tmp/compound_error.png")
|
|
||||||
|
|
||||||
finally:
|
|
||||||
browser.close()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
test_compound_ginseng_selection()
|
|
||||||
Loading…
Reference in New Issue
Block a user