From 1cebb02ec6df0ee01841e4c2876865849fcfcbf6 Mon Sep 17 00:00:00 2001 From: thug0bin Date: Mon, 2 Mar 2026 13:56:22 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B0=98=EB=A0=A4=EB=8F=99=EB=AC=BC=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EA=B8=B0=EB=8A=A5=20=EB=B0=8F=20=ED=99=95?= =?UTF-8?q?=EC=9E=A5=20=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pets 테이블 추가 (이름, 종류, 품종, 사진 등) - 반려동물 CRUD API (/api/pets) - 확장 마이페이지 (/mypage) - 카카오 로그인 기반 - 기존 마이페이지에 퀵 메뉴 추가 (반려동물/쿠폰/구매내역/내정보) - 카카오 로그인 시 세션에 user_id 저장 - 동물약 APC 매핑 가이드 문서 추가 --- backend/app.py | 435 ++++++++- backend/db/dbsetup.py | 82 +- backend/db/mileage_schema.sql | 25 + .../static/uploads/pets/pet_1_d4ffe983.png | Bin 0 -> 56234 bytes backend/templates/my_page.html | 63 ++ backend/templates/mypage_v2.html | 891 ++++++++++++++++++ docs/ANIMAL_DRUG_APC_MAPPING.md | 176 ++++ 7 files changed, 1656 insertions(+), 16 deletions(-) create mode 100644 backend/static/uploads/pets/pet_1_d4ffe983.png create mode 100644 backend/templates/mypage_v2.html create mode 100644 docs/ANIMAL_DRUG_APC_MAPPING.md diff --git a/backend/app.py b/backend/app.py index 8d5a8d4..7ae0edc 100644 --- a/backend/app.py +++ b/backend/app.py @@ -492,8 +492,8 @@ def normalize_kakao_phone(kakao_phone): return None -def link_kakao_identity(user_id, kakao_id, kakao_data): - """카카오 계정을 customer_identities에 연결""" +def link_kakao_identity(user_id, kakao_id, kakao_data, token_data=None): + """카카오 계정을 customer_identities에 연결 (토큰 포함)""" conn = db_manager.get_sqlite_connection() cursor = conn.cursor() @@ -504,13 +504,33 @@ def link_kakao_identity(user_id, kakao_id, kakao_data): is_new_link = cursor.fetchone() is None + store_data = {k: v for k, v in kakao_data.items() if k != 'raw_data'} + if is_new_link: - # raw_data 제외 (세션 크기 절약) - store_data = {k: v for k, v in kakao_data.items() if k != 'raw_data'} cursor.execute(""" - INSERT INTO customer_identities (user_id, provider, provider_user_id, provider_data) - VALUES (?, 'kakao', ?, ?) - """, (user_id, kakao_id, json.dumps(store_data, ensure_ascii=False))) + INSERT INTO customer_identities + (user_id, provider, provider_user_id, provider_data, access_token, refresh_token, token_expires_at) + VALUES (?, 'kakao', ?, ?, ?, ?, ?) + """, ( + user_id, kakao_id, + json.dumps(store_data, ensure_ascii=False), + token_data.get('access_token') if token_data else None, + token_data.get('refresh_token') if token_data else None, + token_data.get('expires_at') if token_data else None, + )) + elif token_data: + # 기존 레코드: 토큰 + 프로필 데이터 업데이트 + cursor.execute(""" + UPDATE customer_identities + SET access_token = ?, refresh_token = ?, token_expires_at = ?, provider_data = ? + WHERE provider = 'kakao' AND provider_user_id = ? + """, ( + token_data.get('access_token'), + token_data.get('refresh_token'), + token_data.get('expires_at'), + json.dumps(store_data, ensure_ascii=False), + kakao_id, + )) # 프로필 이미지, 이메일 업데이트 updates = [] @@ -545,6 +565,52 @@ def find_user_by_kakao_id(kakao_id): return row['user_id'] if row else None +def get_kakao_tokens(user_id): + """사용자의 저장된 카카오 토큰 조회""" + conn = db_manager.get_sqlite_connection() + cursor = conn.cursor() + cursor.execute(""" + SELECT provider_user_id, access_token, refresh_token, token_expires_at + FROM customer_identities + WHERE provider = 'kakao' AND user_id = ? + """, (user_id,)) + row = cursor.fetchone() + if row and row['access_token']: + return { + 'kakao_id': row['provider_user_id'], + 'access_token': row['access_token'], + 'refresh_token': row['refresh_token'], + 'token_expires_at': row['token_expires_at'], + } + return None + + +def update_kakao_tokens(kakao_id, token_data): + """카카오 토큰 업데이트 (갱신 후 저장용)""" + conn = db_manager.get_sqlite_connection() + cursor = conn.cursor() + + updates = ["access_token = ?"] + params = [token_data['access_token']] + + if 'expires_at' in token_data: + updates.append("token_expires_at = ?") + params.append(token_data['expires_at']) + + # refresh_token은 갱신 응답에 포함된 경우에만 업데이트 + if 'refresh_token' in token_data: + updates.append("refresh_token = ?") + params.append(token_data['refresh_token']) + + params.append(kakao_id) + cursor.execute(f""" + UPDATE customer_identities + SET {', '.join(updates)} + WHERE provider = 'kakao' AND provider_user_id = ? + """, params) + conn.commit() + + # ============================================================================ # 라우트 # ============================================================================ @@ -807,7 +873,7 @@ def claim_kakao_start(): return redirect(auth_url) -def _handle_mypage_kakao_callback(code, kakao_client): +def _handle_mypage_kakao_callback(code, kakao_client, redirect_to=None): """ 마이페이지 카카오 콜백 처리 - 카카오 연동(머지) + 마이페이지 이동 @@ -816,6 +882,9 @@ def _handle_mypage_kakao_callback(code, kakao_client): B) 미연결 + 카카오 전화번호로 기존 유저 발견 → 카카오 연동 후 이동 C) 미연결 + 기존 유저 없음 → 신규 생성 + 카카오 연동 D) 전화번호 없음 → 에러 안내 + + Args: + redirect_to: 리다이렉트할 URL (None이면 기본 /my-page?phone=xxx) """ success, token_data = kakao_client.get_access_token(code) if not success: @@ -843,9 +912,15 @@ def _handle_mypage_kakao_callback(code, kakao_client): "UPDATE users SET nickname = ? WHERE id = ? AND nickname = '고객'", (kakao_name, existing_user_id)) conn.commit() - cursor.execute("SELECT phone FROM users WHERE id = ?", (existing_user_id,)) + cursor.execute("SELECT phone, nickname FROM users WHERE id = ?", (existing_user_id,)) row = cursor.fetchone() if row and row['phone']: + # 세션에 로그인 정보 저장 + session['logged_in_user_id'] = existing_user_id + session['logged_in_phone'] = row['phone'] + session['logged_in_name'] = row['nickname'] or kakao_name + if redirect_to: + return redirect(redirect_to) return redirect(f"/my-page?phone={row['phone']}") # 전화번호 없으면 연동 불가 @@ -860,7 +935,7 @@ def _handle_mypage_kakao_callback(code, kakao_client): if phone_user: # Case B: 기존 전화번호 유저 → 카카오 연동 (머지) user_id = phone_user['id'] - link_kakao_identity(user_id, kakao_id, user_info) + link_kakao_identity(user_id, kakao_id, user_info, token_data) # "고객" 이름이면 카카오 실명으로 업데이트 if phone_user['nickname'] == '고객' and kakao_name and kakao_name != '고객': cursor.execute("UPDATE users SET nickname = ? WHERE id = ?", @@ -870,9 +945,17 @@ def _handle_mypage_kakao_callback(code, kakao_client): else: # Case C: 신규 유저 생성 + 카카오 연동 user_id, _ = get_or_create_user(kakao_phone, kakao_name) - link_kakao_identity(user_id, kakao_id, user_info) + link_kakao_identity(user_id, kakao_id, user_info, token_data) logging.info(f"마이페이지 카카오 신규: user_id={user_id}, kakao_id={kakao_id}") + # 세션에 로그인 정보 저장 (mypage_v2용) + session['logged_in_user_id'] = user_id + session['logged_in_phone'] = kakao_phone + session['logged_in_name'] = kakao_name + + # 지정된 리다이렉트 URL이 있으면 그쪽으로 + if redirect_to: + return redirect(redirect_to) return redirect(f"/my-page?phone={kakao_phone}") @@ -904,8 +987,9 @@ def claim_kakao_callback(): return render_template('error.html', message="보안 검증에 실패했습니다. 다시 시도해주세요.") # 2.5 마이페이지 조회 목적이면 별도 처리 - if state_data.get('purpose') == 'mypage': - return _handle_mypage_kakao_callback(code, get_kakao_client()) + if state_data.get('purpose') in ('mypage', 'mypage_v2'): + redirect_to = '/mypage' if state_data.get('purpose') == 'mypage_v2' else None + return _handle_mypage_kakao_callback(code, get_kakao_client(), redirect_to=redirect_to) # 3. claim 컨텍스트 복원 token_param = state_data.get('t', '') @@ -942,6 +1026,8 @@ def claim_kakao_callback(): # 카카오에서 받은 생년월일 조합 kakao_birthday = None kakao_bday = user_info.get('birthday') # MMDD 형식 + print(f"[KAKAO DEBUG] user_info keys: {list(user_info.keys())}") + print(f"[KAKAO DEBUG] birthday={kakao_bday}, birthyear={user_info.get('birthyear')}") if kakao_bday and len(kakao_bday) == 4: if user_info.get('birthyear'): kakao_birthday = f"{user_info['birthyear']}-{kakao_bday[:2]}-{kakao_bday[2:]}" # YYYY-MM-DD @@ -963,7 +1049,7 @@ def claim_kakao_callback(): else: user_id, is_new = get_or_create_user(kakao_phone, kakao_name, birthday=kakao_birthday) - link_kakao_identity(user_id, kakao_id, user_info) + link_kakao_identity(user_id, kakao_id, user_info, token_data) success, msg, new_balance = claim_mileage(user_id, token_info) if not success: @@ -1077,6 +1163,67 @@ def mypage_kakao_start(): return redirect(auth_url) +@app.route('/mypage') +def mypage_v2(): + """확장 마이페이지 (카카오 로그인 필수)""" + user_id = session.get('logged_in_user_id') + + if not user_id: + # 로그인 필요 - 카카오 로그인으로 리다이렉트 + csrf_token = secrets.token_hex(16) + state_data = {'purpose': 'mypage_v2', 'csrf': csrf_token} + kakao_state = base64.urlsafe_b64encode( + json.dumps(state_data).encode() + ).decode() + session['kakao_csrf'] = csrf_token + return render_template('my_page_login.html', kakao_state=kakao_state, redirect_to='/mypage') + + # 사용자 정보 조회 + conn = db_manager.get_sqlite_connection() + cursor = conn.cursor() + + cursor.execute(""" + SELECT id, nickname, phone, profile_image_url, mileage_balance, created_at + FROM users WHERE id = ? + """, (user_id,)) + user_raw = cursor.fetchone() + + if not user_raw: + session.pop('logged_in_user_id', None) + return redirect('/mypage') + + user = dict(user_raw) + + # 반려동물 목록 조회 + cursor.execute(""" + SELECT id, name, species, breed, gender, photo_url, created_at + FROM pets WHERE user_id = ? AND is_active = TRUE + ORDER BY created_at DESC + """, (user_id,)) + + pets = [] + for row in cursor.fetchall(): + species_label = '강아지 🐕' if row['species'] == 'dog' else ('고양이 🐈' if row['species'] == 'cat' else '기타') + pets.append({ + 'id': row['id'], + 'name': row['name'], + 'species': row['species'], + 'species_label': species_label, + 'breed': row['breed'], + 'gender': row['gender'], + 'photo_url': row['photo_url'] + }) + + # 구매 횟수 (적립 내역 수) + cursor.execute("SELECT COUNT(*) FROM mileage_ledger WHERE user_id = ?", (user_id,)) + purchase_count = cursor.fetchone()[0] + + return render_template('mypage_v2.html', + user=user, + pets=pets, + purchase_count=purchase_count) + + @app.route('/my-page') def my_page(): """마이페이지 (전화번호로 조회)""" @@ -4412,6 +4559,266 @@ def api_kims_interaction_check(): }), 500 +# ============================================================================== +# 반려동물 API +# ============================================================================== + +# 견종/묘종 데이터 +DOG_BREEDS = [ + '말티즈', '푸들', '포메라니안', '치와와', '시츄', '요크셔테리어', + '비숑프리제', '골든리트리버', '래브라도리트리버', '진돗개', '시바견', + '웰시코기', '닥스훈트', '비글', '보더콜리', '프렌치불독', '불독', + '슈나우저', '사모예드', '허스키', '믹스견', '기타' +] + +CAT_BREEDS = [ + '코리안숏헤어', '페르시안', '러시안블루', '샴', '먼치킨', '랙돌', + '브리티쉬숏헤어', '아메리칸숏헤어', '스코티쉬폴드', '노르웨이숲', + '메인쿤', '뱅갈', '아비시니안', '터키쉬앙고라', '믹스묘', '기타' +] + +@app.route('/api/pets', methods=['GET']) +def get_pets(): + """사용자의 반려동물 목록 조회""" + # 세션에서 로그인 유저 확인 + user_id = session.get('logged_in_user_id') + if not user_id: + return jsonify({'success': False, 'error': '로그인이 필요합니다.'}), 401 + + try: + conn = db_manager.get_sqlite_connection() + cursor = conn.cursor() + cursor.execute(""" + SELECT id, name, species, breed, gender, birth_date, + age_months, weight, photo_url, notes, created_at + FROM pets + WHERE user_id = ? AND is_active = TRUE + ORDER BY created_at DESC + """, (user_id,)) + + pets = [] + for row in cursor.fetchall(): + pets.append({ + 'id': row['id'], + 'name': row['name'], + 'species': row['species'], + 'species_label': '강아지 🐕' if row['species'] == 'dog' else ('고양이 🐈' if row['species'] == 'cat' else '기타'), + 'breed': row['breed'], + 'gender': row['gender'], + 'birth_date': row['birth_date'], + 'age_months': row['age_months'], + 'weight': float(row['weight']) if row['weight'] else None, + 'photo_url': row['photo_url'], + 'notes': row['notes'], + 'created_at': utc_to_kst_str(row['created_at']) + }) + + return jsonify({'success': True, 'pets': pets, 'count': len(pets)}) + except Exception as e: + logging.error(f"반려동물 조회 오류: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + +@app.route('/api/pets', methods=['POST']) +def create_pet(): + """반려동물 등록""" + user_id = session.get('logged_in_user_id') + if not user_id: + return jsonify({'success': False, 'error': '로그인이 필요합니다.'}), 401 + + try: + data = request.get_json() + name = data.get('name', '').strip() + species = data.get('species', '').strip() # dog, cat, other + breed = data.get('breed', '').strip() + gender = data.get('gender') # male, female, unknown + + if not name: + return jsonify({'success': False, 'error': '이름을 입력해주세요.'}), 400 + if species not in ['dog', 'cat', 'other']: + return jsonify({'success': False, 'error': '종류를 선택해주세요.'}), 400 + + conn = db_manager.get_sqlite_connection() + cursor = conn.cursor() + + cursor.execute(""" + INSERT INTO pets (user_id, name, species, breed, gender) + VALUES (?, ?, ?, ?, ?) + """, (user_id, name, species, breed, gender)) + + pet_id = cursor.lastrowid + conn.commit() + + logging.info(f"반려동물 등록: user_id={user_id}, pet_id={pet_id}, name={name}, species={species}") + + return jsonify({ + 'success': True, + 'pet_id': pet_id, + 'message': f'{name}이(가) 등록되었습니다!' + }) + except Exception as e: + logging.error(f"반려동물 등록 오류: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + +@app.route('/api/pets/', methods=['PUT']) +def update_pet(pet_id): + """반려동물 정보 수정""" + user_id = session.get('logged_in_user_id') + if not user_id: + return jsonify({'success': False, 'error': '로그인이 필요합니다.'}), 401 + + try: + data = request.get_json() + + conn = db_manager.get_sqlite_connection() + cursor = conn.cursor() + + # 소유권 확인 + cursor.execute("SELECT id FROM pets WHERE id = ? AND user_id = ?", (pet_id, user_id)) + if not cursor.fetchone(): + return jsonify({'success': False, 'error': '반려동물을 찾을 수 없습니다.'}), 404 + + # 업데이트 필드 구성 + updates = [] + params = [] + + if 'name' in data: + updates.append("name = ?") + params.append(data['name'].strip()) + if 'species' in data: + updates.append("species = ?") + params.append(data['species']) + if 'breed' in data: + updates.append("breed = ?") + params.append(data['breed']) + if 'gender' in data: + updates.append("gender = ?") + params.append(data['gender']) + if 'birth_date' in data: + updates.append("birth_date = ?") + params.append(data['birth_date']) + if 'age_months' in data: + updates.append("age_months = ?") + params.append(data['age_months']) + if 'weight' in data: + updates.append("weight = ?") + params.append(data['weight']) + if 'notes' in data: + updates.append("notes = ?") + params.append(data['notes']) + + if updates: + updates.append("updated_at = CURRENT_TIMESTAMP") + params.append(pet_id) + + cursor.execute(f""" + UPDATE pets SET {', '.join(updates)} WHERE id = ? + """, params) + conn.commit() + + return jsonify({'success': True, 'message': '수정되었습니다.'}) + except Exception as e: + logging.error(f"반려동물 수정 오류: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + +@app.route('/api/pets/', methods=['DELETE']) +def delete_pet(pet_id): + """반려동물 삭제 (soft delete)""" + user_id = session.get('logged_in_user_id') + if not user_id: + return jsonify({'success': False, 'error': '로그인이 필요합니다.'}), 401 + + try: + conn = db_manager.get_sqlite_connection() + cursor = conn.cursor() + + # 소유권 확인 및 삭제 + cursor.execute(""" + UPDATE pets SET is_active = FALSE, updated_at = CURRENT_TIMESTAMP + WHERE id = ? AND user_id = ? + """, (pet_id, user_id)) + + if cursor.rowcount == 0: + return jsonify({'success': False, 'error': '반려동물을 찾을 수 없습니다.'}), 404 + + conn.commit() + return jsonify({'success': True, 'message': '삭제되었습니다.'}) + except Exception as e: + logging.error(f"반려동물 삭제 오류: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + +@app.route('/api/pets//photo', methods=['POST']) +def upload_pet_photo(pet_id): + """반려동물 사진 업로드""" + user_id = session.get('logged_in_user_id') + if not user_id: + return jsonify({'success': False, 'error': '로그인이 필요합니다.'}), 401 + + try: + conn = db_manager.get_sqlite_connection() + cursor = conn.cursor() + + # 소유권 확인 + cursor.execute("SELECT id, name FROM pets WHERE id = ? AND user_id = ?", (pet_id, user_id)) + pet = cursor.fetchone() + if not pet: + return jsonify({'success': False, 'error': '반려동물을 찾을 수 없습니다.'}), 404 + + if 'photo' not in request.files: + return jsonify({'success': False, 'error': '사진 파일이 없습니다.'}), 400 + + file = request.files['photo'] + if file.filename == '': + return jsonify({'success': False, 'error': '파일을 선택해주세요.'}), 400 + + # 파일 확장자 체크 + allowed = {'png', 'jpg', 'jpeg', 'gif', 'webp'} + ext = file.filename.rsplit('.', 1)[-1].lower() if '.' in file.filename else '' + if ext not in allowed: + return jsonify({'success': False, 'error': '지원하지 않는 이미지 형식입니다.'}), 400 + + # 파일 저장 + import uuid + filename = f"pet_{pet_id}_{uuid.uuid4().hex[:8]}.{ext}" + upload_dir = Path(app.root_path) / 'static' / 'uploads' / 'pets' + upload_dir.mkdir(parents=True, exist_ok=True) + + filepath = upload_dir / filename + file.save(str(filepath)) + + # DB 업데이트 + photo_url = f"/static/uploads/pets/{filename}" + cursor.execute(""" + UPDATE pets SET photo_url = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? + """, (photo_url, pet_id)) + conn.commit() + + logging.info(f"반려동물 사진 업로드: pet_id={pet_id}, filename={filename}") + + return jsonify({ + 'success': True, + 'photo_url': photo_url, + 'message': f'{pet["name"]} 사진이 등록되었습니다!' + }) + except Exception as e: + logging.error(f"반려동물 사진 업로드 오류: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + +@app.route('/api/pets/breeds/') +def get_breeds(species): + """종류별 품종 목록 조회""" + if species == 'dog': + return jsonify({'success': True, 'breeds': DOG_BREEDS}) + elif species == 'cat': + return jsonify({'success': True, 'breeds': CAT_BREEDS}) + else: + return jsonify({'success': True, 'breeds': ['기타']}) + + if __name__ == '__main__': import os diff --git a/backend/db/dbsetup.py b/backend/db/dbsetup.py index 2a3e12e..ab6886f 100644 --- a/backend/db/dbsetup.py +++ b/backend/db/dbsetup.py @@ -154,11 +154,46 @@ class DatabaseManager: return self.engines[database] def get_session(self, database='PM_BASE'): - """특정 데이터베이스 세션 반환""" + """특정 데이터베이스 세션 반환 (자동 복구 포함)""" if database not in self.sessions: engine = self.get_engine(database) Session = sessionmaker(bind=engine) self.sessions[database] = Session() + else: + # 🔥 기존 세션 상태 체크 및 자동 복구 + session = self.sessions[database] + try: + # 세션이 유효한지 간단한 쿼리로 테스트 + session.execute(text("SELECT 1")) + except Exception as e: + error_msg = str(e).lower() + # 연결 끊김 또는 트랜잭션 에러 감지 + if any(keyword in error_msg for keyword in [ + 'invalid transaction', 'rollback', 'connection', + 'closed', 'lost', 'timeout', 'network', 'disconnect' + ]): + print(f"[DB Manager] {database} 세션 복구 시도: {e}") + try: + session.rollback() + print(f"[DB Manager] {database} 롤백 성공, 세션 재사용") + except Exception as rollback_err: + print(f"[DB Manager] {database} 롤백 실패, 세션 재생성: {rollback_err}") + try: + session.close() + except: + pass + del self.sessions[database] + # 새 세션 생성 + engine = self.get_engine(database) + Session = sessionmaker(bind=engine) + self.sessions[database] = Session() + print(f"[DB Manager] {database} 새 세션 생성 완료") + else: + # 다른 종류의 에러면 롤백만 시도 + try: + session.rollback() + except: + pass return self.sessions[database] def rollback_session(self, database='PM_BASE'): @@ -237,7 +272,13 @@ class DatabaseManager: self.init_sqlite_schema() self.sqlite_conn = old_conn print(f"[DB Manager] SQLite 신규 DB 생성 완료: {self.sqlite_db_path}") - + else: + # 기존 DB: 마이그레이션 실행 + old_conn = self.sqlite_conn + self.sqlite_conn = conn + self._migrate_sqlite() + self.sqlite_conn = old_conn + return conn def init_sqlite_schema(self): @@ -319,6 +360,43 @@ class DatabaseManager: self.sqlite_conn.commit() print("[DB Manager] SQLite 마이그레이션: ai_recommendations 테이블 생성") + # customer_identities 토큰 저장 컬럼 추가 + cursor.execute("PRAGMA table_info(customer_identities)") + ci_columns = [row[1] for row in cursor.fetchall()] + if 'access_token' not in ci_columns: + cursor.execute("ALTER TABLE customer_identities ADD COLUMN access_token TEXT") + cursor.execute("ALTER TABLE customer_identities ADD COLUMN refresh_token TEXT") + cursor.execute("ALTER TABLE customer_identities ADD COLUMN token_expires_at DATETIME") + self.sqlite_conn.commit() + print("[DB Manager] SQLite 마이그레이션: customer_identities 토큰 컬럼 추가") + + # pets 테이블 생성 (반려동물) + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='pets'") + if not cursor.fetchone(): + cursor.executescript(""" + CREATE TABLE IF NOT EXISTS pets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + name VARCHAR(50) NOT NULL, + species VARCHAR(20) NOT NULL, + breed VARCHAR(50), + gender VARCHAR(10), + birth_date DATE, + age_months INTEGER, + weight DECIMAL(5,2), + photo_url TEXT, + notes TEXT, + is_active BOOLEAN DEFAULT TRUE, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) + ); + CREATE INDEX IF NOT EXISTS idx_pets_user ON pets(user_id); + CREATE INDEX IF NOT EXISTS idx_pets_species ON pets(species); + """) + self.sqlite_conn.commit() + print("[DB Manager] SQLite 마이그레이션: pets 테이블 생성") + def test_connection(self, database='PM_BASE'): """연결 테스트""" try: diff --git a/backend/db/mileage_schema.sql b/backend/db/mileage_schema.sql index 6430c7c..64e5174 100644 --- a/backend/db/mileage_schema.sql +++ b/backend/db/mileage_schema.sql @@ -22,6 +22,9 @@ CREATE TABLE IF NOT EXISTS customer_identities ( provider VARCHAR(20) NOT NULL, provider_user_id VARCHAR(100) NOT NULL, provider_data TEXT, + access_token TEXT, + refresh_token TEXT, + token_expires_at DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id), UNIQUE(provider, provider_user_id) @@ -120,3 +123,25 @@ CREATE TABLE IF NOT EXISTS ai_recommendations ( CREATE INDEX IF NOT EXISTS idx_rec_user_status ON ai_recommendations(user_id, status); CREATE INDEX IF NOT EXISTS idx_rec_expires ON ai_recommendations(expires_at); + +-- 8. 반려동물 테이블 +CREATE TABLE IF NOT EXISTS pets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + name VARCHAR(50) NOT NULL, -- 이름 (예: 뽀삐, 나비) + species VARCHAR(20) NOT NULL, -- 종류: dog, cat, other + breed VARCHAR(50), -- 품종 (말티즈, 페르시안 등) + gender VARCHAR(10), -- male, female, unknown + birth_date DATE, -- 생년월일 (나중에 사용) + age_months INTEGER, -- 월령 (나중에 사용) + weight DECIMAL(5,2), -- 체중 kg (나중에 사용) + photo_url TEXT, -- 사진 URL + notes TEXT, -- 특이사항/메모 + is_active BOOLEAN DEFAULT TRUE, -- 활성 상태 + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) +); + +CREATE INDEX IF NOT EXISTS idx_pets_user ON pets(user_id); +CREATE INDEX IF NOT EXISTS idx_pets_species ON pets(species); diff --git a/backend/static/uploads/pets/pet_1_d4ffe983.png b/backend/static/uploads/pets/pet_1_d4ffe983.png new file mode 100644 index 0000000000000000000000000000000000000000..09d486cf621e4a79229f0a38d2d75f75bf72ef69 GIT binary patch literal 56234 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�MPqsOg@F?9k>wDe*keGK$vi>QLlx0`<_9W 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약국에서 1P = 1원으로 사용 가능 + + +
적립 내역
diff --git a/backend/templates/mypage_v2.html b/backend/templates/mypage_v2.html new file mode 100644 index 0000000..b16ec3b --- /dev/null +++ b/backend/templates/mypage_v2.html @@ -0,0 +1,891 @@ + + + + + + + + + + + + 마이페이지 - 청춘약국 + + + + + + +
+ +
+
+

마이페이지

+ 로그아웃 +
+
+ + +
+
+
+ {% if user.profile_image_url %} + 프로필 + {% else %} + 😊 + {% endif %} +
+
+

{{ user.nickname or '회원' }}님

+

{{ user.phone or '전화번호 미등록' }}

+
+
+
+
+
🎁
+
{{ '{:,}'.format(user.mileage_balance or 0) }}
+
포인트
+
+
+
📦
+
{{ purchase_count or 0 }}
+
구매
+
+
+
🐾
+
{{ pets|length }}
+
반려동물
+
+
+
+ + +
+
+

🐾 내 반려동물

+
+ +
+ {% if pets %} + {% for pet in pets %} +
+
+ {% if pet.photo_url %} + {{ pet.name }} + {% else %} + {{ '🐕' if pet.species == 'dog' else ('🐈' if pet.species == 'cat' else '🐾') }} + {% endif %} +
+
+
{{ pet.name }}
+
+ {{ pet.species_label }} + {% if pet.breed %}· {{ pet.breed }}{% endif %} + {% if pet.gender %}· {{ '♂' if pet.gender == 'male' else ('♀' if pet.gender == 'female' else '') }}{% endif %} +
+
+ +
+ {% endfor %} + {% else %} +
+
🐾
+

등록된 반려동물이 없습니다

+
+ {% endif %} +
+ + +
+ + +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/ANIMAL_DRUG_APC_MAPPING.md b/docs/ANIMAL_DRUG_APC_MAPPING.md new file mode 100644 index 0000000..c25a2cb --- /dev/null +++ b/docs/ANIMAL_DRUG_APC_MAPPING.md @@ -0,0 +1,176 @@ +# 동물약 APC 매핑 가이드 + +> 최종 업데이트: 2026-03-02 + +## 개요 + +POS(PIT3000)의 동물약 제품을 APDB의 APC 코드와 매핑하여 제품 정보(용법, 용량, 주의사항) 및 이미지를 표시하기 위한 작업 가이드. + +--- + +## 현재 상태 + +### 매핑 현황 + +| 구분 | 개수 | 비율 | +|------|------|------| +| 동물약 총 | 39개 | 100% | +| APC 매핑됨 | 7개 | 18% | +| **APC 미매핑** | **32개** | **82%** | + +### 매핑 완료 제품 + +| POS 제품명 | DrugCode | APC | +|------------|----------|-----| +| (판)복합개시딘 | LB000003140 | 0231093520106 | +| 안텔민킹(5kg이상) | LB000003158 | 0230237810109 | +| 안텔민뽀삐(5kg이하) | LB000003157 | 0230237010107 | +| 파라캅L(5kg이상) | LB000003159 | 0230338510101 | +| 파라캅S(5kg이하) | LB000003160 | 0230347110106 | +| 세레니아정16mg(개멀미약) | LB000003353 | 0231884610109 | +| 세레니아정24mg(개멀미약) | LB000003354 | 0231884620107 | + +--- + +## 매핑 구조 + +### 데이터베이스 연결 + +``` +MSSQL (192.168.0.4\PM2014) PostgreSQL (192.168.0.87:5432) +┌─────────────────────────┐ ┌─────────────────────────┐ +│ PM_DRUG.CD_GOODS │ │ apdb_master.apc │ +│ - DrugCode │ │ - apc (PK) │ +│ - GoodsName │ │ - product_name │ +│ - BARCODE │ │ - image_url1 │ +│ │ │ - llm_pharm (JSONB) │ +├─────────────────────────┤ └─────────────────────────┘ +│ PM_DRUG.CD_ITEM_UNIT_ │ +│ MEMBER │ +│ - DRUGCODE (FK) │ +│ - CD_CD_BARCODE ◀───────┼── APC 코드 저장 (023%로 시작) +│ - CHANGE_DATE │ +└─────────────────────────┘ +``` + +### APC 매핑 방식 + +1. `CD_ITEM_UNIT_MEMBER` 테이블에 **추가 바코드**로 APC 등록 +2. 기존 바코드는 유지, APC를 별도 레코드로 INSERT +3. APC 코드는 `023%`로 시작 (식별자) + +--- + +## 1:1 매핑 가능 후보 + +### ✅ 확실한 매핑 (1개) + +| POS 제품명 | DrugCode | APC | APDB 제품명 | 이미지 | +|------------|----------|-----|-------------|--------| +| **제스타제(10정)** | LB000003146 | 8809720800455 | 제스타제 | ✅ 있음 | + +### ⚠️ 검토 필요 (1개) + +| POS 제품명 | DrugCode | APC 후보 | 비고 | +|------------|----------|----------|------| +| 안텔민 | S0000001 | 0230237800003 | "안텔민킹"과 "안텔민뽀삐"는 이미 별도 매핑됨. 이 제품이 무엇인지 확인 필요 | + +### ❌ APDB에 없음 (3개) + +| POS 제품명 | 사유 | +|------------|------| +| (판)클라펫정50(100정) | APDB엔 "클라펫 정"만 있음 (함량 불일치) | +| 넥스가드xs(2~3.5kg) | 사이즈별 APC 없음 | +| 캐치원캣(2.5~7.5kg)/고양이 | APDB에 캐치원 자체가 없음 | + +--- + +## 1:N 매핑 필요 제품 (27개) + +사이즈별로 세분화된 제품들. 하나의 APDB APC에 여러 POS 제품을 매핑해야 함. + +### 브랜드별 현황 + +| 브랜드 | POS 제품 수 | APDB 존재 | 비고 | +|--------|-------------|-----------|------| +| 다이로하트 | 3개 (SS/S/M) | ✅ | 다이로하트 츄어블 정 | +| 하트세이버 | 4개 (mini/S/M/L) | ✅ | 하트세이버 플러스 츄어블 | +| 하트웜솔루션 | 2개 (S/M) | ❌ | APDB에 없음 | +| 리펠로 | 2개 (S/M) | ✅ | 리펠로액 (이미지 있음!) | +| 캐치원 | 5개 (SS/S/M/L/캣) | ❌ | APDB에 없음 | +| 셀라이트 | 5개 (SS/S/M/L/XL) | ✅ | 셀라이트 액 | +| 넥스가드 | 2개 (xs/L) | ✅ | 넥스가드 스펙트라 | +| 가드닐 | 3개 (S/M/L) | ✅ | 가드닐 액 | +| 심피드 | 2개 (M/L) | ❌ | APDB에 없음 | +| 하트캅 | 1개 | ✅ | 하트캅-츄어블 정 | + +--- + +## APDB 통계 + +| 항목 | 수치 | +|------|------| +| 전체 APC | 16,326개 | +| 이미지 있음 | 73개 (0.4%) | +| LLM 정보 있음 | 81개 (0.5%) | +| 동물 관련 키워드 | ~200개 | + +⚠️ **주의:** APDB에 이미지가 거의 없음. 이미지 표시가 목적이라면 다른 소스 필요. + +--- + +## 매핑 스크립트 + +### 매핑 후보 찾기 +```bash +python backend/scripts/batch_apc_matching.py +``` + +### 1:1 매핑 가능 후보 추출 +```bash +python backend/scripts/find_1to1_candidates.py +``` + +### 매핑 실행 (수동) +```python +# backend/scripts/batch_insert_apc.py 참고 +MAPPINGS = [ + ('제스타제(10정)', 'LB000003146', '8809720800455'), +] +``` + +### INSERT 쿼리 예시 +```sql +INSERT INTO CD_ITEM_UNIT_MEMBER ( + DRUGCODE, CD_CD_UNIT, CD_NM_UNIT, CD_MY_UNIT, CD_IN_UNIT, + CD_CD_BARCODE, CD_CD_POS, CHANGE_DATE +) VALUES ( + 'LB000003146', -- DrugCode + '015', -- 단위코드 + 1.0, -- 단위명 + <기존값>, -- CD_MY_UNIT (기존 레코드에서 복사) + <기존값>, -- CD_IN_UNIT (기존 레코드에서 복사) + '8809720800455', -- APC 바코드 + '', + '20260302' -- 변경일자 +) +``` + +--- + +## 다음 단계 + +1. **제스타제** 1:1 매핑 실행 +2. **안텔민(S0000001)** 제품 확인 후 결정 +3. 1:N 매핑 정책 결정 (사이즈별 제품 → 동일 APC?) +4. 이미지 소스 대안 검토 (필요시) + +--- + +## 관련 파일 + +- `backend/db/dbsetup.py` - DB 연결 설정 +- `backend/scripts/batch_apc_matching.py` - 매칭 후보 찾기 +- `backend/scripts/batch_insert_apc.py` - 매핑 실행 +- `backend/scripts/find_1to1_candidates.py` - 1:1 후보 추출 +- `backend/app.py` - `_get_animal_drugs()`, `_get_animal_drug_rag()`