🐾 초기 커밋: 반려동물 약품 추천 시스템

- pet_recommend_app.py: Flask 기반 약품 추천 API (포트 7001)
- db_setup.py: PostgreSQL ORM 모델 (apdb_master)
- requirements.txt: 패키지 의존성

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
thug0bin 2026-02-27 22:10:38 +09:00
commit b66129b5d0
4 changed files with 4099 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
__pycache__/
*.pyc
*.pyo
.env
venv/
.venv/
*.log

967
db_setup.py Normal file
View File

@ -0,0 +1,967 @@
# /Flask-backend/db_setup.py
from sqlalchemy import create_engine, Column, Integer, String, DECIMAL, Boolean, Text, ForeignKey, DateTime, Index, func, TIMESTAMP, Float, Date, Enum
from sqlalchemy.orm import sessionmaker, declarative_base, relationship, scoped_session
from sqlalchemy.dialects.postgresql import JSONB
from datetime import datetime
# 1. 데이터베이스 설정
# PostgreSQL 데이터베이스 연결 설정
DATABASE_URI = 'postgresql://admin:trajet6640@192.168.0.87:5432/apdb_master'
engine = create_engine(
DATABASE_URI,
pool_size=20, # 기본 연결 수를 20으로 설정 (사용 패턴에 따라 조정)
max_overflow=20, # 추가 연결 수를 20개까지 허용
pool_timeout=30, # 연결 획득 대기 시간 30초
pool_recycle=3600 # 1시간마다 오래된 연결 재생성 (연결 유실 방지)
)
SessionLocal = sessionmaker(bind=engine)
# 전역 세션 생성 (scoped_session)
session = scoped_session(SessionLocal)
# 2. ORM 기반 모델 정의
Base = declarative_base()
# 1. APDB 테이블 (제품 정보)
# 주의: 이 모델은 실제 테이블명이 'apc'로 되어 있습니다.
class APDB(Base):
__tablename__ = 'apc'
idx = Column(Integer, primary_key=True, comment='고유 IDX')
item_seq = Column(String(100), nullable=True, comment='제품 시퀀스')
product_name = Column(String(200), nullable=True, comment='제품명')
product_english_name = Column(String(200), nullable=True, comment='제품 영문명')
company_name = Column(String(200), nullable=True, comment='회사명')
item_code = Column(String(100), nullable=True, comment='아이템 코드')
approval_number = Column(String(100), nullable=True, comment='승인번호')
ac = Column(String(100), nullable=True, comment='AC')
pc = Column(String(100), nullable=True, comment='PC')
dosage_code = Column(String(100), nullable=True, comment='용량 코드')
packaging_code = Column(String(100), nullable=True, comment='포장 코드')
dosage = Column(String(100), nullable=True, comment='용량')
packaging = Column(String(100), nullable=True, comment='포장')
apc = Column(String(100), nullable=True, unique=True, comment='APC 코드')
approval_date = Column(String(100), nullable=True, comment='승인일')
product_type = Column(String(100), nullable=True, comment='제품 유형')
main_ingredient = Column(String(100), nullable=True, comment='주성분')
finished_material = Column(String(200), nullable=True, comment='완제품 정보')
approval_report = Column(String(100), nullable=True, comment='승인 보고서')
manufacture_import = Column(String(100), nullable=True, comment='제조/수입 구분')
country_of_manufacture = Column(String(100), nullable=True, comment='제조국')
basic_info = Column(Text, nullable=True, comment='기본 정보')
raw_material = Column(Text, nullable=True, comment='원료 정보')
efficacy_effect = Column(Text, nullable=True, comment='효능 효과')
dosage_instructions = Column(Text, nullable=True, comment='복용 지침')
precautions = Column(Text, nullable=True, comment='주의사항')
appearance_manufacturing = Column(Text, nullable=True, comment='외관 및 제조정보')
collection_status = Column(String(100), nullable=True, comment='수집 상태')
component_code = Column(String(100), nullable=True, comment='성분 코드')
component_name_ko = Column(String(200), nullable=True, comment='성분 한글명')
component_name_en = Column(String(200), nullable=True, comment='성분 영문명')
dosage_factor = Column(String(100), nullable=True, comment='용량 인자')
llm_pharm = Column(JSONB, nullable=True, comment='LLM 약국 JSON 데이터')
llm_user = Column(String(100), nullable=True, comment='LLM 사용자')
image_url1 = Column(String(500), nullable=True, comment='이미지 URL 1')
image_url2 = Column(String(500), nullable=True, comment='이미지 URL 2')
image_url3 = Column(String(500), nullable=True, comment='이미지 URL 3')
# 신규 추가: 고도몰 CDN URL을 저장 (각각 앞, 뒤, 상세(내부) 이미지)
godoimage_url_f = Column(String(500), nullable=True, comment='고도몰 CDN URL - 앞 이미지')
godoimage_url_b = Column(String(500), nullable=True, comment='고도몰 CDN URL - 뒤 이미지')
godoimage_url_d = Column(String(500), nullable=True, comment='고도몰 CDN URL - 상세(내부) 이미지')
# 네이밍 신규 추가 필드
pill_color = Column(String(100), nullable=True, comment='약 색상')
weight_min_kg = Column(Float, nullable=True, comment='적용 체중 하한 (kg)')
weight_max_kg = Column(Float, nullable=True, comment='적용 체중 상한 (kg)')
pet_size_label = Column(String(100), nullable=True, comment='반려동물 크기 분류명 (예: 초소형, 중형)')
pet_size_code = Column(String(10), nullable=True, comment='반려동물 크기 코드 (예: XS, S, M, L, XL)')
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment='업데이트 시간')
parent_item_id = Column(Integer, ForeignKey('apc.idx'), nullable=True, comment='부모 항목 (기본 항목의 idx)')
# 애완동물용 분류
for_pets = Column(Boolean, default=False, nullable=True, comment='반려동물용 여부')
# 처방대상 동물의약품 여부
prescription_target = Column(Boolean, default=False, nullable=True, comment='처방대상 동물의약품 여부')
# 의약품 아님 여부 (AZIT에 없는, 혼합사료 등으로 허가된 제품)
is_not_medicine = Column(Boolean, default=False, nullable=True, comment='의약품 아님 (AZIT에 없는 혼합사료 등으로 허가된 제품)')
# 제품별 상세 사용 안내 (희석비율, 도포방법 등) - llm_pharm과 분리
usage_guide = Column(JSONB, nullable=True, comment='제품별 상세 사용 안내 (희석비율, 도포방법 등)')
# 관계 설정
variants = relationship("APDB", backref="parent", remote_side=[idx])
inventories = relationship("Inventory", back_populates="apdb")
# 2. ComponentCode 테이블 (성분 코드 테이블)
class ComponentCode(Base):
__tablename__ = 'component_code'
__table_args__ = {'comment': '각 성분 코드와 관련된 정보를 저장하는 테이블'}
idx = Column(Integer, primary_key=True, autoincrement=True, comment='고유 ID')
code = Column(String(500), nullable=False, unique=True, comment='성분 코드 (중복되지 않는 고유 값, 외래키로 사용 가능)')
description = Column(String(500), nullable=True, comment='성분 설명')
efficacy = Column(Text, nullable=True, comment='효능효과 (성분의 주요 효능 및 효과)')
target_animals = Column(JSONB, nullable=True, comment='사용대상 동물 (개, 고양이, 조류 등)')
precautions = Column(Text, nullable=True, comment='주의사항 (기본적인 주의사항)')
additional_precautions = Column(Text, nullable=True, comment='추가 주의사항')
prohibited_breeds = Column(String(500), nullable=True, comment='금지 품종 (예: 불독, 시츄 등)')
offlabel = Column(Text, nullable=True, comment='오프레이블 효능 (비허가 용도)')
created_at = Column(TIMESTAMP(timezone=True), server_default=func.now(), comment='생성 날짜 및 시간')
updated_at = Column(TIMESTAMP(timezone=True), server_default=func.now(), onupdate=func.now(), comment='업데이트 날짜 및 시간')
# 추가 칼럼: 성분 한글명 및 성분 영문명 (APDB 테이블에서 추출된 데이터를 반영)
component_name_ko = Column(String(200), nullable=True, comment='성분 한글명')
component_name_en = Column(String(200), nullable=True, comment='성분 영문명')
# 관계 설정 (증상-성분코드 매핑 테이블과 연결)
symptom_matches = relationship("SymptomComponentMapping", back_populates="component")
# 3. Symptoms 테이블 (증상 정보 테이블)
class Symptoms(Base):
__tablename__ = 'symptoms'
__table_args__ = {'comment': '각 증상별 질병코드를 부여하는 테이블'}
idx = Column(Integer, primary_key=True, autoincrement=True, comment='고유 ID')
prefix = Column(String(1), nullable=False, comment='질병 코드 접두어 (예: A, B 등)')
prefix_description = Column(String(50), comment='접두어 설명')
symptom_code = Column(String(10), unique=True, nullable=False, comment='고유 질병 코드')
symptom_description = Column(String(255), nullable=False, comment='증상 설명')
disease_description = Column(String(255), nullable=False, comment='질병 설명')
created_at = Column(TIMESTAMP(timezone=True), server_default=func.now(), comment='생성 날짜 및 시간')
updated_at = Column(TIMESTAMP(timezone=True), server_default=func.now(), onupdate=func.now(), comment='업데이트 날짜 및 시간')
# 관계 설정: Symptoms 테이블과 SymptomComponentMapping 테이블 간의 관계
component_matches = relationship("SymptomComponentMapping", back_populates="symptoms")
# 4. SymptomComponentMapping 테이블 (증상-성분코드 매핑 테이블)
class SymptomComponentMapping(Base):
__tablename__ = 'symptom_component_mapping'
__table_args__ = (
Index('uq_symptom_component_mapping', 'symptom_code', 'component_code', unique=True),
{'comment': '각 성분 코드와 증상 코드 간의 매핑을 저장하는 테이블'}
)
idx = Column(Integer, primary_key=True, autoincrement=True, comment='고유 ID')
symptom_code = Column(String(10), ForeignKey('symptoms.symptom_code'), nullable=False, comment='증상 코드')
component_code = Column(String(500), ForeignKey('component_code.code'), nullable=False, comment='성분 코드')
created_at = Column(TIMESTAMP(timezone=True), server_default=func.now(), comment='생성 날짜 및 시간')
updated_at = Column(TIMESTAMP(timezone=True), server_default=func.now(), onupdate=func.now(), comment='업데이트 날짜 및 시간')
# 관계 설정: Symptoms 및 ComponentCode와 연결
symptoms = relationship("Symptoms", back_populates="component_matches")
component = relationship("ComponentCode", back_populates="symptom_matches")
# 5. Location 테이블 (물리적 위치 관리)
class Location(Base):
__tablename__ = 'location'
__table_args__ = (
Index('idx_location_code', 'location_code'),
{'comment': '재고의 물리적 위치(창고, 선반 등)를 관리하는 테이블'}
)
idx = Column(Integer, primary_key=True, comment='위치 고유 IDX')
location_code = Column(String(50), nullable=False, unique=True, comment='위치 코드 (예: WH1-A1)')
warehouse_name = Column(String(100), nullable=True, comment='창고 이름')
zone = Column(String(50), nullable=True, comment='구역 (예: Zone A)')
rack = Column(String(50), nullable=True, comment='랙 번호 (예: Rack 1)')
shelf = Column(String(50), nullable=True, comment='선반 번호 (예: Shelf 1)')
description = Column(Text, nullable=True, comment='위치 설명')
capacity = Column(Integer, nullable=True, comment='최대 수용 수량')
created_at = Column(DateTime, default=datetime.utcnow, server_default=func.now(), nullable=False, comment='레코드 생성 시간')
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, server_default=func.now(), nullable=False, comment='레코드 수정 시간')
inventories = relationship("Inventory", back_populates="location")
# 6. Inventory 테이블 (입출고 및 예약 관리)
class Inventory(Base):
__tablename__ = 'inventory'
__table_args__ = (
Index('idx_inventory_order_no', 'order_no'),
Index('idx_inventory_transaction_type', 'transaction_type'),
Index('idx_inventory_reserved_at', 'reserved_at'),
Index('idx_inventory_reservation_id', 'reservation_id'),
Index('idx_inventory_expiration_date', 'expiration_date'),
Index('idx_inventory_location_id', 'location_id'),
Index('idx_inventory_receipt_seq', 'receipt_id', 'receipt_seq'), # 신규 인덱스
{'comment': '입고/출고 및 예약 내역을 관리하는 테이블, es_goods와 동기화'}
)
id = Column(Integer, primary_key=True, comment='입고/출고/예약/반품 이력 고유 ID')
apdb_id = Column(Integer, ForeignKey('apc.idx'), nullable=False, comment='APDB 테이블 FK')
parent_id = Column(Integer, ForeignKey('inventory.id'), nullable=True, comment='원본 거래 ID (반품 시 참조)')
supplier_cost = Column(DECIMAL(12,2), nullable=False, comment='입고가 (도매상 사입가,도도매가)')
wholesaler_price = Column(DECIMAL(12,2), nullable=False, comment='도매가 (도매상->약국 판매가)')
retail_price = Column(DECIMAL(12,2), nullable=False, comment='소매가 (약국 소비자 판매가)')
quantity = Column(Integer, nullable=False, comment='입고/출고/반품 수량')
transaction_date = Column(DateTime, default=datetime.utcnow, nullable=False, comment='거래 일자')
remarks = Column(Text, nullable=True, comment='비고')
entity_id = Column(String(50), nullable=True, comment='공급처 ID (vendor_code 또는 pharmacy_id)')
entity_type = Column(String(20), nullable=True, comment='공급처 유형 (VENDOR, PHARMACY)')
receipt_id = Column(Integer, ForeignKey('receipt.idx', ondelete='SET NULL'), nullable=True, comment='입고장 ID')
return_document_id = Column(Integer, ForeignKey('return_document.idx', ondelete='SET NULL'), nullable=True, comment='반품장 ID')
location_id = Column(Integer, ForeignKey('location.idx', ondelete='SET NULL'), nullable=True, comment='물리적 위치 ID')
serial_number = Column(String(100), nullable=True, comment='일련번호')
expiration_date = Column(Date, nullable=True, comment='유효기간 (es_goods.expirationDate와 호환, date, YYYY-MM-DD)')
transaction_type = Column(String(20), nullable=False, comment='거래 유형 (INBOUND, OUTBOUND, RETURN_INBOUND, RETURN_OUTBOUND, DISPOSAL, RESERVED, RESERVED_RETURN)')
order_no = Column(String(100), nullable=True, comment='고도몰 주문번호 (예약 및 출고/반품 연계)')
order_date = Column(Date, nullable=True, comment='고도몰 주문 일자 (YYYY-MM-DD)')
reservation_id = Column(String(36), nullable=True, comment='예약 ID (UUID)')
reserved_at = Column(DateTime, nullable=True, comment='예약 시간 (RESERVED 또는 RESERVED_RETURN 시 사용)')
created_at = Column(DateTime, default=datetime.utcnow, server_default=func.now(), nullable=False, comment='레코드 생성 시간')
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, server_default=func.now(), nullable=False, comment='레코드 수정 시간')
actual_sale_price = Column(DECIMAL(12,2), nullable=True, comment='고도몰 실제 판매가 (주문 시 약국 판매가)')
goods_no = Column(Integer, nullable=True, comment='고도몰 상품 번호 (es_goods.goodsNo)')
receipt_seq = Column(Integer, nullable=False, default=0, comment='입고장 내 항목 정렬 순서') # 신규 컬럼
apdb = relationship("APDB", back_populates="inventories")
receipt = relationship("Receipt", back_populates="inventories")
return_document = relationship("ReturnDocument", back_populates="inventories")
location = relationship("Location", back_populates="inventories")
parent = relationship("Inventory", remote_side=[id], back_populates="children")
children = relationship("Inventory", back_populates="parent")
sales_transactions = relationship("SalesTransaction", back_populates="inventory")
returns = relationship("Return", back_populates="inventory")
# 7. Receipt 테이블 (입고장 정보)
class Receipt(Base):
__tablename__ = 'receipt'
__table_args__ = {
'comment': '입고장 정보를 관리하는 테이블, 공급자 입고장 번호와 실제 입고일 포함'
}
idx = Column(Integer, primary_key=True, comment='입고장 고유 IDX')
receipt_number = Column(String(100), nullable=False, unique=True, comment='입고장 번호')
receipt_date = Column(DateTime, default=datetime.utcnow, nullable=False, comment='입고일')
entity_id = Column(String(50), nullable=False, comment='공급처 ID (vendor_code 또는 pharmacy_id)')
entity_type = Column(String(20), nullable=False, comment='공급처 유형 (VENDOR, PHARMACY)')
total_quantity = Column(Integer, nullable=False, comment='총 입고 수량')
product_amount = Column(DECIMAL(10,2), nullable=False, comment='제품 금액')
shipping_cost = Column(DECIMAL(10,2), nullable=False, default=0.0, comment='배송비')
total_amount = Column(DECIMAL(10,2), nullable=False, comment='총 금액 (제품 금액 + 배송비)')
remarks = Column(Text, nullable=True, comment='비고')
vendor_receipt_number = Column(String(100), nullable=True, unique=True, comment='공급자 측 입고장 번호 (예: VEND-12345), 중복 불가')
actual_receipt_date = Column(Date, nullable=True, comment='실제 입고일 (YYYY-MM-DD), 물리적 입고 또는 공급자 지정 일자')
created_at = Column(DateTime, default=datetime.utcnow, server_default=func.now(), nullable=False, comment='레코드 생성 시간')
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, server_default=func.now(), nullable=False, comment='레코드 수정 시간')
inventories = relationship("Inventory", back_populates="receipt")
# 8. Return 테이블 (반품 관리)
class Return(Base):
__tablename__ = 'return'
__table_args__ = (
Index('idx_return_inventory_id', 'inventory_id'),
Index('idx_return_status', 'status'),
{'comment': '반품 요청 및 상태를 관리하는 테이블'}
)
idx = Column(Integer, primary_key=True, comment='반품 요청 고유 IDX')
inventory_id = Column(Integer, ForeignKey('inventory.id', ondelete='RESTRICT'), nullable=False, comment='반품 대상 Inventory ID')
return_document_id = Column(Integer, ForeignKey('return_document.idx', ondelete='SET NULL'), nullable=True, comment='반품장 ID')
return_type = Column(String(20), nullable=False, comment='반품 유형 (SUPPLIER, CUSTOMER)')
quantity = Column(Integer, nullable=False, comment='반품 수량')
reason = Column(Text, nullable=False, comment='반품 사유 (예: 품질 이슈, 잘못된 배송)')
status = Column(String(20), nullable=False, default='PENDING', comment='반품 상태 (PENDING, APPROVED, REJECTED, COMPLETED)')
requested_at = Column(DateTime, default=datetime.utcnow, nullable=False, comment='반품 요청 일자')
approved_at = Column(DateTime, nullable=True, comment='반품 승인 일자')
completed_at = Column(DateTime, nullable=True, comment='반품 완료 일자')
remarks = Column(Text, nullable=True, comment='비고')
created_at = Column(DateTime, default=datetime.utcnow, server_default=func.now(), nullable=False, comment='레코드 생성 시간')
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, server_default=func.now(), nullable=False, comment='레코드 수정 시간')
inventory = relationship("Inventory", back_populates="returns")
return_document = relationship("ReturnDocument", back_populates="returns")
# 9. ReturnDocument 테이블 (반품장 정보)
class ReturnDocument(Base):
__tablename__ = 'return_document'
__table_args__ = (
Index('idx_return_document_number', 'return_document_number'),
{'comment': '반품장 정보를 관리하는 테이블'}
)
idx = Column(Integer, primary_key=True, comment='반품장 고유 IDX')
return_document_number = Column(String(100), nullable=False, unique=True, comment='반품장 번호')
receipt_id = Column(Integer, ForeignKey('receipt.idx', ondelete='SET NULL'), nullable=True, comment='원본 입고장 IDX')
entity_id = Column(String(50), nullable=False, comment='반품 대상 ID (vendor_code 또는 pharmacy_id)')
entity_type = Column(String(20), nullable=False, comment='반품 대상 유형 (VENDOR, PHARMACY)')
total_quantity = Column(Integer, nullable=False, comment='총 반품 수량')
return_date = Column(DateTime, default=datetime.utcnow, nullable=False, comment='반품일')
remarks = Column(Text, nullable=True, comment='비고')
created_at = Column(DateTime, default=datetime.utcnow, server_default=func.now(), nullable=False, comment='레코드 생성 시간')
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, server_default=func.now(), nullable=False, comment='레코드 수정 시간')
receipt = relationship("Receipt")
returns = relationship("Return", back_populates="return_document")
inventories = relationship("Inventory", back_populates="return_document")
# 10. Pharmacy 테이블 (약국 정보)
class Pharmacy(Base):
__tablename__ = 'pharmacy'
__table_args__ = (
Index('uq_pharmacy_id', 'pharmacy_id', unique=True),
{'comment': '약국 정보'}
)
idx = Column(Integer, primary_key=True, autoincrement=True, comment='고유 IDX (기본 키)')
pharmacy_id = Column(String(50), nullable=False, comment='약국 고유값 (PXXXX)')
name = Column(String(200), nullable=False, comment='약국명')
address = Column(String(300), nullable=True, comment='주소')
latitude = Column(DECIMAL(9,6), nullable=True, comment='위도')
longitude = Column(DECIMAL(9,6), nullable=True, comment='경도')
contact_info = Column(String(100), nullable=True, comment='연락처')
region_id = Column(Integer, ForeignKey('region.idx'), nullable=True, comment='상위 지역 IDX')
subregion_id = Column(Integer, ForeignKey('subregion.idx'), nullable=True, comment='하위 지역 IDX')
region = relationship("Region", back_populates="pharmacies")
subregion = relationship("Subregion", back_populates="pharmacies")
pharmacy_inventories = relationship("PharmacyInventory", back_populates="pharmacy")
sales_transactions = relationship("SalesTransaction", back_populates="pharmacy")
pharmacy_members = relationship("PharmacyMember", back_populates="pharmacy")
# 11. PharmacyMember 테이블 (약국 회원 정보)
class PharmacyMember(Base):
__tablename__ = 'p_member'
__table_args__ = (
{'comment': '약국 회원 정보, 약국당 여러 약사가 허용됨 (pharmacy.pharmacy_id 참조, 1:N 관계)'}
)
idx = Column(Integer, primary_key=True, autoincrement=True, nullable=False, comment='고유 IDX')
memNo = Column("memno", Integer, nullable=True, comment='회원 번호 (es_member.memNo와 매핑)')
pharmacy_id = Column(String(50), ForeignKey('pharmacy.pharmacy_id', on_delete='RESTRICT'), nullable=True, comment='약국 ID (pharmacy.pharmacy_id 참조)')
mem_nm = Column(String(100), nullable=True, comment='약사 이름 (es_member.memNm)') # 추가
licenseNo = Column("licenseno", String(50), nullable=True, comment='면허번호')
institutionCode = Column("institutioncode", String(50), nullable=True, comment='요양기관부호')
pharmacyName = Column("pharmacyname", String(100), nullable=True, comment='약국 이름')
businessRegNo = Column("businessregno", String(20), nullable=True, comment='사업자등록번호')
kioskUsage = Column("kioskusage", Boolean, nullable=False, default=False, comment='키오스크 사용 여부')
kioskIdCreated = Column("kioskidcreated", Boolean, nullable=False, default=False, comment='키오스크 아이디 생성 여부')
kioskId = Column("kioskid", String(50), nullable=True, comment='키오스크 아이디')
kioskPw = Column("kioskpw", String(255), nullable=True, default='1234', comment="키오스크 비밀번호 (초기값 '1234')")
isKioskSetupComplete = Column("iskiosksetupcomplete", Boolean, nullable=False, default=False, comment='키오스크 설정 완료 여부')
lastLoginDt = Column("lastlogindt", DateTime, nullable=True, comment='최근 키오스크 로그인 시간')
loginMethod = Column("loginmethod", String(20), nullable=True, comment="로그인 방식 (예: 'kakao', 'licenseNo', 'kioskId')")
memo = Column("memo", Text, nullable=True, comment='메모')
sync_status = Column(String(50), nullable=True, comment='동기화 상태 (pending, synced, failed)')
pharmacy = relationship("Pharmacy", back_populates="pharmacy_members")
# 12. PharmacyInventory 테이블 (약국별 재고 관리)
class PharmacyInventory(Base):
__tablename__ = 'p_inventory'
__table_args__ = (
Index('idx_apc', 'apc'),
Index('idx_pharmacy_id', 'pharmacy_id'),
{'comment': '약국별 재고 관리'}
)
idx = Column(Integer, primary_key=True, autoincrement=True, comment='고유 ID')
pharmacy_id = Column(String(50), ForeignKey('pharmacy.pharmacy_id', on_delete='NO ACTION'), nullable=False, comment='약국 DB 고유 ID')
apc = Column(String(500), ForeignKey('apc.apc', on_delete='NO ACTION'), nullable=False, comment='APC 고유 코드')
expiration_date = Column(String(10), nullable=True, comment='유통기한')
lot_number = Column(String(50), nullable=True, comment='로트 번호')
quantity_in_stock = Column(Integer, nullable=True, comment='재고 수량')
minimum_quantity = Column(Integer, nullable=True, comment='최소 재고 수량')
purchase_price = Column(DECIMAL(10,2), nullable=True, comment='사입가')
list_price = Column(DECIMAL(10,2), nullable=True, comment='권장 판매가')
sale_price = Column(DECIMAL(10,2), nullable=True, comment='약국 판매 가격')
pharmacy = relationship("Pharmacy", back_populates="pharmacy_inventories")
apdb = relationship("APDB") # 단방향 관계로 수정, back_populates 제거
# 13. SalesTransaction 테이블 (약국 키오스크 판매 거래 내역)
class SalesTransaction(Base):
__tablename__ = 'salestransaction'
__table_args__ = {'comment': '약국 키오스크 판매 거래 내역'}
id = Column(Integer, primary_key=True, autoincrement=True, comment='판매 거래 고유 ID')
pharmacy_id = Column(String(50), ForeignKey('pharmacy.pharmacy_id', on_delete='RESTRICT'), nullable=False, comment='약국 ID (pharmacy.pharmacy_id 참조)')
inventory_id = Column(Integer, ForeignKey('inventory.id', on_delete='RESTRICT'), nullable=False, comment='Inventory 테이블 FK')
purchase_cost = Column(DECIMAL(10,2), nullable=True, comment='약국 입고가 (도매상 판매가)')
sale_price = Column(DECIMAL(10,2), nullable=True, comment='실제 소비자 판매가')
quantity = Column(Integer, nullable=True, comment='판매 수량')
transaction_date = Column(DateTime, nullable=True, default=datetime.utcnow, comment='거래 일자')
pharmacy = relationship("Pharmacy", back_populates="sales_transactions")
inventory = relationship("Inventory", back_populates="sales_transactions")
# 14. Vendor 테이블 (거래처 정보)
class Vendor(Base):
__tablename__ = 'vendor'
idx = Column(Integer, primary_key=True, autoincrement=True, comment='거래처 고유 IDX')
vendor_code = Column(String(50), nullable=True, comment='거래처 고유 코드')
vendor_type = Column(String(50), nullable=True, comment='거래처구분')
old_vendor_code = Column(String(50), nullable=True, comment='기존거래처코드')
name = Column(String(200), nullable=True, comment='거래처명')
ceo_name = Column(String(100), nullable=True, comment='대표자명')
business_type = Column(String(100), nullable=True, comment='업태')
item = Column(String(100), nullable=True, comment='종목')
business_reg_no = Column(String(50), nullable=True, comment='사업자등록번호')
postal_code = Column(String(20), nullable=True, comment='우편번호')
address_detail = Column(String(300), nullable=True, comment='상세주소')
phone = Column(String(50), nullable=True, comment='전화번호')
fax = Column(String(50), nullable=True, comment='팩스번호')
salesperson_name = Column(String(100), nullable=True, comment='영업사원명')
vendor_group_name = Column(String(100), nullable=True, comment='거래처그룹명')
vendor_type_name = Column(String(100), nullable=True, comment='거래처종류명')
facility_code = Column(String(50), nullable=True, comment='요양기관기호')
ceo_ssn = Column(String(50), nullable=True, comment='대표자주민번호')
contact_person = Column(String(100), nullable=True, comment='거래처업무담당자')
is_active = Column(Boolean, nullable=True, comment='거래여부')
abbreviation = Column(String(50), nullable=True, comment='약어명')
display_name = Column(String(100), nullable=True, comment='상호출력명')
remarks = Column(Text, nullable=True, comment='비고')
address = Column(String(300), nullable=True, comment='주소')
email = Column(String(100), nullable=True, comment='이메일')
mobile = Column(String(50), nullable=True, comment='핸드폰')
facility_name = Column(String(100), nullable=True, comment='요양기관명')
tax_invoice_issue = Column(String(50), nullable=True, comment='세금계산서발행구분')
store_code = Column(String(50), nullable=True, comment='점포코드')
store_name = Column(String(100), nullable=True, comment='점포명')
delivery_center = Column(String(100), nullable=True, comment='배송센터')
etc3 = Column(String(100), nullable=True, comment='기타3')
price_applicable_code = Column(String(50), nullable=True, comment='단가적용처코드')
stock_applicable_code = Column(String(50), nullable=True, comment='재고적용처코드')
supply_type = Column(String(50), nullable=True, comment='공급형태')
contract_type = Column(String(50), nullable=True, comment='계약구분')
road_postal_code = Column(String(20), nullable=True, comment='도로명우편번호')
road_address_detail = Column(String(300), nullable=True, comment='도로명상세주소')
# 15. User 테이블 (사용자)
class User(Base):
__tablename__ = 'user'
id = Column(String(100), primary_key=True, comment="이메일 주소 (고유 식별자)")
name = Column(String(100), nullable=False, comment="사용자 이름")
password_hash = Column(String(255), nullable=False, comment="비밀번호 해시")
role = Column(String(50), nullable=False, default="staff", comment="사용자 권한 (예: admin, staff)")
position = Column(String(100), nullable=True, comment="직급")
created_at = Column(DateTime, default=datetime.utcnow, comment="계정 생성일")
# 16. SyncStatus 테이블 (동기화 상태 기록)
class SyncStatus(Base):
"""
동기화 상태를 기록하는 테이블.
APDB의 inventory 테이블에서 es_goods 테이블로 retail_price와 expiration_date를 동기화한 결과를 저장.
es_goods의 retailPrice (decimal(12,2)) expirationDate (date) 호환.
"""
__tablename__ = 'sync_status'
__table_args__ = (
Index('idx_sync_status_apc', 'apc'),
Index('idx_sync_status_created_at', 'created_at'),
{'comment': 'APDB와 es_goods 동기화 상태 기록 (retail_price: decimal(12,2), expiration_date: date)'}
)
id = Column(Integer, primary_key=True, comment='고유 ID')
last_sync = Column(DateTime, nullable=False, comment='마지막 동기화 시간 (UTC)')
success_count = Column(Integer, nullable=False, default=0, comment='성공한 동기화 건수')
failed_count = Column(Integer, nullable=False, default=0, comment='실패한 동기화 건수')
apc = Column(String(30), nullable=True, comment='APC 코드 (es_goods.APC: varchar(30), inventory.apc: varchar(100))')
retail_price = Column(DECIMAL(12,2), nullable=True, comment='소비자가 (es_goods.retailPrice: decimal(12,2), inventory.retail_price: decimal(12,2))')
expiration_date = Column(Date, nullable=True, comment='유효기간 (es_goods.expirationDate: date, inventory.expiration_date: date, YYYY-MM-DD)')
created_at = Column(DateTime, default=datetime.utcnow, server_default=func.now(), comment='레코드 생성 시간 (UTC)')
def __repr__(self):
return f"<SyncStatus(id={self.id}, apc={self.apc}, retail_price={self.retail_price}, expiration_date={self.expiration_date})>"
# 17. CSMemo 테이블 (약국 회원별 CS 메모 기록)
class CSMemo(Base):
__tablename__ = 'cs_memo'
__table_args__ = (
Index('idx_cs_memo_memno', 'memno'),
Index('idx_cs_memo_orderno', 'orderno'),
Index('idx_cs_memo_created_at', 'created_at'),
{'comment': '약국 회원별 CS 메모 기록, memNo와 orderNo로 매핑'}
)
id = Column(Integer, primary_key=True, comment='CS 메모 고유 ID')
memno = Column(Integer, nullable=False, comment='약국 회원 번호 (es_member.memNo)')
orderno = Column(String(100), nullable=True, comment='연관 주문 번호 (es_order.orderNo, 선택)')
memo = Column(Text, nullable=False, comment='CS 메모 내용')
created_by = Column(String(100), nullable=False, comment='작성자 (User.id)')
created_at = Column(DateTime, default=datetime.utcnow, server_default=func.now(), nullable=False, comment='메모 등록 일시')
class MemberGroupChangeLog(Base):
__tablename__ = "member_group_change_logs"
idx = Column(Integer, primary_key=True, index=True, comment="Primary key")
mem_no = Column(Integer, nullable=False, index=True, comment="고도몰 회원 번호 (es_member.memNo)")
mem_nm = Column(String(100), nullable=True, comment="회원 이름 (es_member.memNm)")
old_group_sno = Column(String(50), nullable=True, comment="변경 전 그룹 번호")
new_group_sno = Column(String(50), nullable=True, comment="변경 후 그룹 번호")
changed_by = Column(String(100), nullable=False, comment="변경자 (예: 관리자 이메일 또는 ID)")
changed_at = Column(DateTime, default=datetime.utcnow, comment="변경 일시")
change_reason = Column(Text, nullable=True, comment="변경 사유 (옵션)")
class Region(Base):
__tablename__ = 'region'
__table_args__ = (
Index('idx_region_code', 'region_code'),
{'comment': '상위 지역(시/도) 정보를 저장하는 테이블'}
)
idx = Column(Integer, primary_key=True, comment='고유 IDX')
region_code = Column(String(50), unique=True, nullable=False, comment='지역 코드 (예: SEOUL, GANGWON)')
region_name = Column(String(100), nullable=False, comment='지역 이름 (예: 서울, 강원)')
latitude = Column(DECIMAL(9,6), nullable=True, comment='지역 중심 위도')
longitude = Column(DECIMAL(9,6), nullable=True, comment='지역 중심 경도')
zoom_level = Column(Integer, default=6, nullable=True, comment='기본 줌 레벨')
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
subregions = relationship("Subregion", back_populates="region")
pharmacies = relationship("Pharmacy", back_populates="region")
class Subregion(Base):
__tablename__ = 'subregion'
__table_args__ = (
Index('idx_subregion_code', 'subregion_code'),
Index('idx_region_id', 'region_id'),
{'comment': '하위 지역(시/군/구) 정보를 저장하는 테이블'}
)
idx = Column(Integer, primary_key=True, comment='고유 IDX')
subregion_code = Column(String(50), unique=True, nullable=False, comment='하위 지역 코드 (예: SEONGBUK, GANGBUK)')
subregion_name = Column(String(100), nullable=False, comment='하위 지역 이름 (예: 성북, 강북)')
region_id = Column(Integer, ForeignKey('region.idx'), nullable=False, comment='상위 지역 IDX')
latitude = Column(DECIMAL(9,6), nullable=True, comment='하위 지역 중심 위도')
longitude = Column(DECIMAL(9,6), nullable=True, comment='하위 지역 중심 경도')
zoom_level = Column(Integer, default=8, nullable=True, comment='기본 줌 레벨')
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
region = relationship("Region", back_populates="subregions")
pharmacies = relationship("Pharmacy", back_populates="subregion")
# ExcludedPharmacies 테이블 (예외 처리 약국 정보)
class ExcludedPharmacies(Base):
__tablename__ = 'excluded_pharmacies'
__table_args__ = (
Index('idx_excluded_pharmacies_memno', 'memno'),
{'comment': '예외 처리 약국 정보를 저장하는 테이블, es_member.memNo와 매핑'}
)
idx = Column(Integer, primary_key=True, autoincrement=True, comment='고유 IDX')
memno = Column(Integer, nullable=False, unique=True, comment='약국 회원 번호 (es_member.memNo)')
mem_nm = Column(String(100), nullable=False, comment='회원 이름 (es_member.memNm)')
pharmacy_name = Column(String(200), nullable=True, comment='약국명 (es_member.company, NULL 허용)')
reason = Column(Text, nullable=True, comment='예외 처리 사유 (예: 테스트 약국, 비활성)')
created_by = Column(String(100), nullable=False, comment='등록자 (예: 관리자 이메일 또는 ID)')
created_at = Column(DateTime, default=datetime.utcnow, server_default=func.now(), nullable=False, comment='레코드 생성 시간')
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, server_default=func.now(), nullable=False, comment='레코드 수정 시간')
# SystemLog 테이블 (시스템 로그)
class SystemLog(Base):
__tablename__ = 'system_log'
__table_args__ = (
Index('idx_system_log_action', 'action'),
Index('idx_system_log_entity', 'entity_type', 'entity_id'),
Index('idx_system_log_created', 'created_at'),
{'comment': '시스템 작업 로그를 저장하는 테이블'}
)
idx = Column(Integer, primary_key=True, autoincrement=True, comment='고유 IDX')
action = Column(String(50), nullable=False, comment='작업 유형 (예: DELETE_RECEIPT, UPDATE_INVENTORY)')
entity_type = Column(String(50), nullable=False, comment='대상 엔티티 유형 (예: receipt, inventory, product)')
entity_id = Column(String(100), nullable=False, comment='대상 엔티티 ID')
details = Column(JSONB, nullable=True, comment='상세 로그 정보 (JSON 형태)')
created_at = Column(DateTime, default=datetime.utcnow, nullable=False, comment='로그 생성 시간')
created_by = Column(String(100), nullable=False, comment='작업 수행자')
ip_address = Column(String(45), nullable=True, comment='요청 IP 주소')
user_agent = Column(String(500), nullable=True, comment='사용자 에이전트')
result = Column(String(20), default='SUCCESS', comment='작업 결과 (SUCCESS/FAILURE)')
error_message = Column(Text, nullable=True, comment='에러 메시지 (실패 시)')
# 18. OptimalStock 테이블 (적정 재고 관리)
class OptimalStock(Base):
__tablename__ = 'optimal_stock'
__table_args__ = (
Index('idx_optimal_stock_apc', 'apc'),
Index('idx_optimal_stock_goods_no', 'goods_no'),
Index('idx_optimal_stock_created_at', 'created_at'),
# APC 또는 goods_no 중 하나는 반드시 존재해야 함
# APC가 있으면 APC로 unique, 없으면 goods_no로 unique
{'comment': '제품별 적정 재고 관리 테이블 - APC 우선, goods_no 보조 식별'}
)
idx = Column(Integer, primary_key=True, autoincrement=True, comment='적정 재고 설정 고유 IDX')
# 제품 식별자 (APC 우선, goods_no 보조)
apc = Column(String(100), nullable=True, comment='APC 코드 (APDB.apc FK, 우선 식별자)')
goods_no = Column(Integer, nullable=True, comment='고도몰 상품 번호 (보조 식별자, APC 없을 때)')
# 적정 재고 값들
minimum_stock = Column(Integer, nullable=False, default=0, comment='최소 재고 (안전 재고)')
optimal_stock = Column(Integer, nullable=False, default=0, comment='적정 재고 (권장 보유량)')
maximum_stock = Column(Integer, nullable=True, comment='최대 재고 (과재고 방지, NULL 허용)')
# 재주문 관리
reorder_point = Column(Integer, nullable=True, comment='재주문 시점 (이 수량 이하 시 주문 필요)')
reorder_quantity = Column(Integer, nullable=True, comment='재주문 수량 (한 번에 주문할 수량)')
# 메타데이터
created_at = Column(DateTime, default=datetime.utcnow, server_default=func.now(), nullable=False, comment='설정 생성 시간')
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, server_default=func.now(), nullable=False, comment='설정 수정 시간')
created_by = Column(String(100), nullable=False, comment='설정자 (User.id 또는 관리자)')
notes = Column(Text, nullable=True, comment='비고 (설정 사유, 특이사항 등)')
# 활성화 상태
is_active = Column(Boolean, default=True, nullable=False, comment='활성화 상태 (비활성화 시 알림 제외)')
# 관계 설정
apdb = relationship("APDB", foreign_keys=[apc], primaryjoin="OptimalStock.apc == APDB.apc")
# 19. 재고 정정 이력 테이블 (InventoryAdjustment)
class InventoryAdjustment(Base):
"""
재고 정정 이력 테이블
모든 재고 정정 작업의 이력을 추적합니다.
- 재고 추가/차감/이동/취소
- 정정 사유 승인자 기록
- 변경 전후 수량 추적
"""
__tablename__ = 'inventory_adjustment'
__table_args__ = (
{'comment': '재고 정정 이력 - 모든 재고 수량 변경 작업 추적'}
)
# 기본 키
id = Column(Integer, primary_key=True, autoincrement=True, comment='정정 이력 ID')
# 관련 재고 정보
inventory_id = Column(Integer, ForeignKey('inventory.id'), nullable=False, comment='원본 재고 ID')
apdb_id = Column(Integer, ForeignKey('apc.idx'), nullable=False, comment='제품 ID (APDB.idx FK)')
# 정정 유형 및 수량
adjustment_type = Column(Enum('ADD', 'REMOVE', 'TRANSFER', 'CANCEL', 'CORRECTION', name='adjustment_type_enum'),
nullable=False, comment='정정 유형 (ADD:추가, REMOVE:차감, TRANSFER:이동, CANCEL:취소, CORRECTION:수정)')
before_quantity = Column(Integer, nullable=False, comment='정정 전 수량')
after_quantity = Column(Integer, nullable=False, comment='정정 후 수량')
adjustment_quantity = Column(Integer, nullable=False, comment='정정 수량 (양수/음수)')
# 시리얼번호 및 유효기간 (정정 대상 식별용)
serial_number = Column(String(100), nullable=True, comment='시리얼번호')
expiration_date = Column(Date, nullable=True, comment='유효기간')
# 이동 관련 (TRANSFER 시 사용)
source_location_id = Column(Integer, ForeignKey('location.idx'), nullable=True, comment='원본 위치 ID')
target_location_id = Column(Integer, ForeignKey('location.idx'), nullable=True, comment='대상 위치 ID')
# 정정 사유 및 승인
reason = Column(Text, nullable=False, comment='정정 사유')
reason_category = Column(Enum('STOCK_ERROR', 'DAMAGE', 'EXPIRY', 'THEFT', 'TRANSFER', 'SYSTEM_ERROR', 'OTHER',
name='reason_category_enum'),
nullable=True, comment='사유 분류')
# 작업자 및 승인자
created_by = Column(String(100), nullable=False, comment='정정 요청자')
approved_by = Column(String(100), nullable=True, comment='승인자 (필요시)')
approval_status = Column(Enum('PENDING', 'APPROVED', 'REJECTED', name='approval_status_enum'),
default='APPROVED', nullable=False, comment='승인 상태')
# 시스템 로그 연결
system_log_id = Column(Integer, ForeignKey('system_log.idx'), nullable=True, comment='연관 시스템 로그 ID')
# 메타데이터
created_at = Column(DateTime, default=datetime.utcnow, server_default=func.now(), nullable=False, comment='정정 실행 시간')
approved_at = Column(DateTime, nullable=True, comment='승인 시간')
# 추가 정보 (JSON)
adjustment_metadata = Column(JSONB, nullable=True, comment='추가 메타데이터 (JSON 형태)')
# 관계 설정
inventory = relationship("Inventory", foreign_keys=[inventory_id])
apdb = relationship("APDB", foreign_keys=[apdb_id])
source_location = relationship("Location", foreign_keys=[source_location_id])
target_location = relationship("Location", foreign_keys=[target_location_id])
system_log = relationship("SystemLog", foreign_keys=[system_log_id])
# 20. 동물약국 테이블 (Animal Pharmacy)
class AnimalPharmacy(Base):
__tablename__ = 'animal_pharmacies'
# 기본 정보
id = Column(Integer, primary_key=True, comment='고유 ID')
management_number = Column(String(50), unique=True, nullable=False, comment='관리번호 (고유)')
name = Column(String(200), nullable=False, comment='사업장명')
phone = Column(String(20), nullable=True, comment='전화번호')
# 주소 정보
address_old = Column(String(500), nullable=True, comment='구주소 (지번주소)')
address_new = Column(String(500), nullable=True, comment='신주소 (도로명주소)')
postal_code = Column(String(10), nullable=True, comment='우편번호')
# 좌표 정보
latitude = Column(DECIMAL(10,8), nullable=True, comment='위도 (WGS84)')
longitude = Column(DECIMAL(11,8), nullable=True, comment='경도 (WGS84)')
x_coordinate = Column(DECIMAL(12,3), nullable=True, comment='X좌표 (EPSG5174)')
y_coordinate = Column(DECIMAL(12,3), nullable=True, comment='Y좌표 (EPSG5174)')
# 분류 정보
business_status = Column(String(10), nullable=True, comment='영업상태')
license_date = Column(Date, nullable=True, comment='인허가일자')
region_code = Column(String(50), nullable=True, comment='지역코드')
province = Column(String(50), nullable=True, comment='시/도 (지역 필터용)')
city = Column(String(100), nullable=True, comment='시/군/구 (세부 지역용)')
# 타임스탬프
created_at = Column(TIMESTAMP, default=datetime.utcnow, server_default=func.now(), nullable=False, comment='생성일시')
updated_at = Column(TIMESTAMP, default=datetime.utcnow, onupdate=datetime.utcnow, server_default=func.now(), nullable=False, comment='수정일시')
# 21. DosageInfo 테이블 (체중 기반 용량 계산 정보)
class DosageInfo(Base):
"""
체중 기반 용량 계산을 위한 구조화된 용량 정보 테이블
정제형 (: 아시엔로정): dose_per_kg + unit_dose로 정제 계산
피펫형 (: 프론트라인): weight_min/max_kg + unit_dose로 적합 제품 매칭
"""
__tablename__ = 'dosage_info'
__table_args__ = (
Index('idx_dosage_info_apdb_idx', 'apdb_idx'),
Index('idx_dosage_info_animal_type', 'animal_type'),
Index('idx_dosage_info_weight_range', 'weight_min_kg', 'weight_max_kg'),
{'comment': '체중 기반 용량 계산을 위한 구조화된 용량 정보'}
)
id = Column(Integer, primary_key=True, autoincrement=True, comment='고유 ID')
apdb_idx = Column(Integer, ForeignKey('apc.idx'), nullable=False, comment='APDB 테이블 FK')
component_code = Column(String(100), nullable=True, comment='성분 코드 (특정 성분용 용량)')
# 핵심 용량 계산 필드
dose_per_kg = Column(Float, nullable=True, comment='kg당 용량 (예: 5.0 → 5mg/kg)')
dose_unit = Column(String(20), nullable=True, comment='용량 단위 (mg, ml, mcg)')
# 1회분 함량 (정제/캡슐/피펫 등)
unit_dose = Column(Float, nullable=True, comment='1정/1ml/1피펫 당 함량')
unit_type = Column(String(20), nullable=True, comment='제형 (정, 캡슐, ml, 피펫)')
# 투여 정보
frequency = Column(String(50), nullable=True, comment='투여 주기 (1일 1회, 월 1회 등)')
route = Column(String(30), nullable=True, comment='투여 경로 (경구, 도포, 점안)')
# 체중 범위 (피펫형 제품 등 체중별 제품 구분용)
weight_min_kg = Column(Float, nullable=True, comment='적용 최소 체중 (kg)')
weight_max_kg = Column(Float, nullable=True, comment='적용 최대 체중 (kg)')
# 동물 타입
animal_type = Column(String(10), nullable=True, comment='대상 동물 (dog, cat, all)')
# 용량 범위 (예: 5~10mg/kg)
dose_per_kg_min = Column(Float, nullable=True, comment='kg당 최소 용량')
dose_per_kg_max = Column(Float, nullable=True, comment='kg당 최대 용량')
# 메타데이터
source = Column(String(20), default='manual', comment='데이터 소스 (manual, ai_parsed)')
verified = Column(Boolean, default=False, comment='사람 검증 여부')
raw_text = Column(Text, nullable=True, comment='원본 용법용량 텍스트 (AI 파싱 시 참조용)')
created_at = Column(DateTime, default=datetime.utcnow, server_default=func.now(), nullable=False, comment='생성 시간')
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, server_default=func.now(), nullable=False, comment='수정 시간')
# 관계 설정
product = relationship("APDB", backref="dosage_infos")
# ============================================================
# 22. SupplementaryProduct 테이블 (보조제품 마스터)
# 동물용 건강기능식품, 보조기구, 케어용품 등
# ============================================================
from sqlalchemy import UniqueConstraint
class SupplementaryProduct(Base):
"""동물용 건기식, 보조기구, 케어용품 마스터 테이블"""
__tablename__ = 'supplementary_product'
__table_args__ = (
UniqueConstraint('product_name', 'brand', name='uq_supplementary_name'),
Index('ix_supplementary_category', 'category'),
Index('ix_supplementary_sub_category', 'sub_category'),
Index('ix_supplementary_target_animal', 'target_animal'),
{'comment': '동물용 건기식, 보조기구, 케어용품 마스터'}
)
id = Column(Integer, primary_key=True, comment='고유 ID')
# 기본 정보
product_name = Column(String(200), nullable=False, comment='제품명')
product_name_en = Column(String(200), nullable=True, comment='제품 영문명')
brand = Column(String(100), nullable=True, comment='브랜드')
manufacturer = Column(String(100), nullable=True, comment='제조사')
# 코드 정보 (nullable - 바코드/제품코드 없어도 등록 가능)
barcode = Column(String(50), nullable=True, comment='바코드 (GTIN/EAN)')
product_code = Column(String(50), nullable=True, comment='제품코드 (자체 관리용)')
sku = Column(String(50), nullable=True, comment='SKU (재고관리용)')
# 분류
category = Column(String(50), nullable=False, comment='대분류: supplement, equipment, care')
sub_category = Column(String(50), nullable=True, comment='소분류: vitamin, probiotics, collar, shampoo 등')
# 상세 정보
description = Column(Text, nullable=True, comment='제품 설명')
usage_instructions = Column(Text, nullable=True, comment='사용 방법')
# 성분 정보 (건기식용)
ingredients = Column(Text, nullable=True, comment='성분 목록 (텍스트)')
main_ingredient = Column(String(200), nullable=True, comment='주요 성분')
ingredient_details = Column(JSONB, nullable=True, comment='성분 상세 정보 (JSON)')
# ingredient_details 예시: [{"name": "글루코사민", "amount": "500mg", "effect": "관절 건강"}]
# 효능/효과 정보
efficacy = Column(Text, nullable=True, comment='효능/효과 설명')
efficacy_tags = Column(JSONB, nullable=True, comment='효능 태그 (JSON 배열)')
# efficacy_tags 예시: ["관절건강", "연골보호", "노령견"]
# 대상 동물
target_animal = Column(String(50), nullable=True, comment='대상 동물: dog, cat, all, rabbit 등')
target_age = Column(String(50), nullable=True, comment='대상 연령: puppy, adult, senior, all')
target_size = Column(String(50), nullable=True, comment='대상 크기: small, medium, large, all')
# 주의사항
precautions = Column(Text, nullable=True, comment='주의사항')
warnings = Column(JSONB, nullable=True, comment='경고 사항 (JSON 배열)')
# 이미지
image_url = Column(String(500), nullable=True, comment='대표 이미지 URL')
image_url_2 = Column(String(500), nullable=True, comment='추가 이미지 URL')
# 가격/규격
price = Column(DECIMAL(10, 2), nullable=True, comment='판매가')
unit = Column(String(50), nullable=True, comment='단위: 개, 봉, ml, g, 정 등')
package_size = Column(String(50), nullable=True, comment='포장 규격: 30정, 500ml 등')
# AI 분석 정보 (APDB의 llm_pharm과 유사)
llm_info = Column(JSONB, nullable=True, comment='AI 분석 정보 (JSON)')
# 외부 링크 (판매처 등)
external_url = Column(String(500), nullable=True, comment='외부 판매 링크')
# 메타 정보
is_active = Column(Boolean, default=True, comment='활성화 여부')
created_at = Column(DateTime, default=datetime.utcnow, server_default=func.now(), nullable=False, comment='생성 시간')
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, server_default=func.now(), nullable=False, comment='수정 시간')
# 관계 설정
inventories = relationship("InventorySupplementary", back_populates="supplementary")
# ============================================================
# 23. UnifiedProductRecommendation 테이블 (통합 제품 추천 매핑)
# APDB ↔ APDB, APDB ↔ Supplementary, Supplementary ↔ Supplementary 매핑
# ============================================================
class UnifiedProductRecommendation(Base):
"""통합 제품 추천 매핑 테이블"""
__tablename__ = 'unified_product_recommendation'
__table_args__ = (
UniqueConstraint('source_type', 'source_id', 'target_type', 'target_id',
name='uq_unified_recommendation'),
Index('ix_unified_source', 'source_type', 'source_id'),
Index('ix_unified_target', 'target_type', 'target_id'),
Index('ix_unified_relation', 'relation_type'),
{'comment': '통합 제품 추천 매핑 (APDB ↔ Supplementary)'}
)
id = Column(Integer, primary_key=True, comment='고유 ID')
# 소스 (추천 기준 제품)
source_type = Column(String(20), nullable=False, comment='소스 타입: apdb, supplementary')
source_id = Column(Integer, nullable=False, comment='소스 제품 ID (apc.idx 또는 supplementary_product.id)')
# 타겟 (추천 대상 제품)
target_type = Column(String(20), nullable=False, comment='타겟 타입: apdb, supplementary')
target_id = Column(Integer, nullable=False, comment='타겟 제품 ID')
# 추천 정보
relation_type = Column(String(50), nullable=False, comment='관계 유형: pre_use, post_use, combo, support, alternative, recovery, prevention')
recommendation_reason = Column(Text, nullable=True, comment='추천 이유')
priority = Column(Integer, default=0, comment='우선순위 (높을수록 먼저 표시)')
# 메타 정보
is_active = Column(Boolean, default=True, comment='활성화 여부')
created_at = Column(DateTime, default=datetime.utcnow, server_default=func.now(), nullable=False, comment='생성 시간')
created_by = Column(String(100), nullable=True, comment='등록자')
# ============================================================
# 24. SymptomSupplementaryRecommendation 테이블 (증상 → 보조제품 매핑)
# 증상 선택 시 보조제품 직접 추천
# ============================================================
class SymptomSupplementaryRecommendation(Base):
"""증상 → 보조제품 추천 매핑 테이블"""
__tablename__ = 'symptom_supplementary_recommendation'
__table_args__ = (
UniqueConstraint('symptom_code', 'supplementary_id', name='uq_symptom_supplementary'),
Index('ix_symptom_supp_code', 'symptom_code'),
Index('ix_symptom_supp_id', 'supplementary_id'),
{'comment': '증상 → 보조제품 추천 매핑'}
)
id = Column(Integer, primary_key=True, comment='고유 ID')
symptom_code = Column(String(20), ForeignKey('symptoms.symptom_code'), nullable=False, comment='증상 코드')
supplementary_id = Column(Integer, ForeignKey('supplementary_product.id'), nullable=False, comment='보조제품 ID')
# 추천 정보
relation_type = Column(String(50), nullable=False, comment='관계 유형: support, prevention, recovery')
recommendation_reason = Column(Text, nullable=True, comment='추천 이유')
priority = Column(Integer, default=0, comment='우선순위')
# 메타 정보
is_active = Column(Boolean, default=True, comment='활성화 여부')
created_at = Column(DateTime, default=datetime.utcnow, server_default=func.now(), nullable=False, comment='생성 시간')
# 관계 설정
symptom = relationship('Symptoms')
supplementary = relationship('SupplementaryProduct')
# ============================================================
# 25. InventorySupplementary 테이블 (보조제품 재고)
# ============================================================
class InventorySupplementary(Base):
"""보조제품 재고 테이블"""
__tablename__ = 'inventory_supplementary'
__table_args__ = (
Index('ix_inv_supp_id', 'supplementary_id'),
{'comment': '보조제품 재고 관리'}
)
id = Column(Integer, primary_key=True, comment='고유 ID')
supplementary_id = Column(Integer, ForeignKey('supplementary_product.id'), nullable=False, comment='보조제품 ID')
quantity = Column(Integer, default=0, comment='재고 수량')
location = Column(String(100), nullable=True, comment='보관 위치')
# 가격 정보
purchase_price = Column(DECIMAL(10, 2), nullable=True, comment='입고가')
retail_price = Column(DECIMAL(10, 2), nullable=True, comment='판매가')
# 유통기한
expiration_date = Column(Date, nullable=True, comment='유통기한')
# 메타 정보
last_updated = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment='최종 수정일')
# 관계 설정
supplementary = relationship('SupplementaryProduct', back_populates='inventories')
# 26. 테이블 생성
Base.metadata.create_all(engine)

3119
pet_recommend_app.py Normal file

File diff suppressed because it is too large Load Diff

6
requirements.txt Normal file
View File

@ -0,0 +1,6 @@
Flask==3.0.0
Flask-Cors==4.0.0
SQLAlchemy==2.0.25
openai==1.99.1
psycopg2-binary==2.9.9
gunicorn==23.0.0