# /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"" # 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)