commit b66129b5d0b33fae1845380fa84ac7045ec828ec Author: thug0bin Date: Fri Feb 27 22:10:38 2026 +0900 ๐Ÿพ ์ดˆ๊ธฐ ์ปค๋ฐ‹: ๋ฐ˜๋ ค๋™๋ฌผ ์•ฝํ’ˆ ์ถ”์ฒœ ์‹œ์Šคํ…œ - pet_recommend_app.py: Flask ๊ธฐ๋ฐ˜ ์•ฝํ’ˆ ์ถ”์ฒœ API (ํฌํŠธ 7001) - db_setup.py: PostgreSQL ORM ๋ชจ๋ธ (apdb_master) - requirements.txt: ํŒจํ‚ค์ง€ ์˜์กด์„ฑ Co-Authored-By: Claude Sonnet 4.6 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..82d9caa --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +__pycache__/ +*.pyc +*.pyo +.env +venv/ +.venv/ +*.log diff --git a/db_setup.py b/db_setup.py new file mode 100644 index 0000000..839d743 --- /dev/null +++ b/db_setup.py @@ -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"" + +# 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) \ No newline at end of file diff --git a/pet_recommend_app.py b/pet_recommend_app.py new file mode 100644 index 0000000..b158ec9 --- /dev/null +++ b/pet_recommend_app.py @@ -0,0 +1,3119 @@ +""" +๋™๋ฌผ์•ฝ ์ถ”์ฒœ MVP ์›น์•ฑ +- ๋ฐ˜๋ ค๋™๋ฌผ ์ •๋ณด + ์ฆ์ƒ ์ž…๋ ฅ โ†’ ์•ฝํ’ˆ ์ถ”์ฒœ +- ์‹คํ–‰: python pet_recommend_app.py +- ์ ‘์†: http://localhost:5001 +""" + +from flask import Flask, render_template_string, request, jsonify +from db_setup import ( + session, APDB, Inventory, ComponentCode, Symptoms, SymptomComponentMapping, DosageInfo, + SupplementaryProduct, UnifiedProductRecommendation, SymptomSupplementaryRecommendation, InventorySupplementary +) +from sqlalchemy import distinct, and_, or_ +from concurrent.futures import ThreadPoolExecutor +import openai +import re + +app = Flask(__name__) + +# ============================================================ +# OpenAI API ์„ค์ • +# ============================================================ +OPENAI_API_KEY = "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" +openai.api_key = OPENAI_API_KEY + +# ============================================================ +# MDR-1 ์œ ์ „์ž ๋ณ€์ด ๊ด€๋ จ ์„ค์ • +# ============================================================ +# MDR-1 ๋ฏผ๊ฐ ์„ฑ๋ถ„ ์ฝ”๋“œ (Avermectin ๊ณ„์—ด ๋“ฑ) +MDR1_SENSITIVE_COMPONENTS = { + 'IC2010131', # ์…€๋ผ๋ฉ•ํ‹ด (Selamectin) + 'IC2030110', # ์ด๋ฒ„๋ฉ•ํ‹ด+ํ”ผ๋ž€ํ…” (Ivermectin + Pyrantel) + 'IC2030132', # ์ด๋ฏธ๋‹คํด๋กœํ”„๋ฆฌ๋“œ+๋ชฉ์‹œ๋ฑํ‹ด (Imidacloprid + Moxidectin) +} + +# MDR-1 ๋ฏผ๊ฐ ๊ฒฌ์ข… ์ •๋ณด +MDR1_BREEDS = { + # ๊ณ ์œ„ํ—˜๊ตฐ (40%+) + 'collie': {'name': '์ฝœ๋ฆฌ', 'risk': 'high', 'frequency': '70%'}, + 'aussie': {'name': '์˜ค์ŠคํŠธ๋ ˆ์ผ๋ฆฌ์•ˆ ์…ฐํผ๋“œ', 'risk': 'high', 'frequency': '50%'}, + 'mini_aussie': {'name': '๋ฏธ๋‹ˆ์–ด์ฒ˜ ์˜ค์ŠคํŠธ๋ ˆ์ผ๋ฆฌ์•ˆ ์…ฐํผ๋“œ', 'risk': 'high', 'frequency': '50%'}, + 'longhair_whippet': {'name': '๋กฑํ—ค์–ด ํœ˜ํ•', 'risk': 'high', 'frequency': '65%'}, + 'silken_windhound': {'name': '์‹ค์ผ„ ์œˆ๋“œํ•˜์šด๋“œ', 'risk': 'high', 'frequency': '30-50%'}, + # ์ค‘์œ„ํ—˜๊ตฐ (10-40%) + 'sheltie': {'name': '์…ฐํ‹€๋žœ๋“œ ์‰ฝ๋…', 'risk': 'medium', 'frequency': '15%'}, + 'english_shepherd': {'name': '์ž‰๊ธ€๋ฆฌ์‰ฌ ์…ฐํผ๋“œ', 'risk': 'medium', 'frequency': '15%'}, + 'whippet': {'name': 'ํœ˜ํ•', 'risk': 'medium', 'frequency': '10-20%'}, + 'mcnab': {'name': '๋งฅ๋ƒ…', 'risk': 'medium', 'frequency': '17%'}, + 'german_shepherd': {'name': '์ €๋จผ ์…ฐํผ๋“œ', 'risk': 'medium', 'frequency': '10%'}, + 'white_shepherd': {'name': 'ํ™”์ดํŠธ ์…ฐํผ๋“œ', 'risk': 'medium', 'frequency': '14%'}, + # ์ €์œ„ํ—˜๊ตฐ (10% ๋ฏธ๋งŒ) + 'old_english': {'name': '์˜ฌ๋“œ ์ž‰๊ธ€๋ฆฌ์‰ฌ ์‰ฝ๋…', 'risk': 'low', 'frequency': '5%'}, + 'border_collie': {'name': '๋ณด๋” ์ฝœ๋ฆฌ', 'risk': 'low', 'frequency': '2-5%'}, + 'chinook': {'name': '์น˜๋ˆ…', 'risk': 'low', 'frequency': '๋ฏธํ™•์ธ'}, + # ๊ธฐํƒ€ + 'mix_herding': {'name': '๋ฏน์Šค๊ฒฌ (๋ชฉ์–‘๊ฒฌ ๊ณ„ํ†ต)', 'risk': 'unknown', 'frequency': '๋ณ€๋™'}, + 'mix_hound': {'name': '๋ฏน์Šค๊ฒฌ (ํ•˜์šด๋“œ ๊ณ„ํ†ต)', 'risk': 'unknown', 'frequency': '๋ณ€๋™'}, + 'other': {'name': '๊ธฐํƒ€ ๊ฒฌ์ข…', 'risk': 'none', 'frequency': '-'}, + 'unknown': {'name': '๋ชจ๋ฆ„', 'risk': 'unknown', 'frequency': '-'}, +} + +# ============================================================ +# ์ œํ’ˆ๊ตฐ(์นดํ…Œ๊ณ ๋ฆฌ) ์ •์˜ +# ============================================================ +PRODUCT_CATEGORIES = { + 'heartworm': { + 'name': '์‹ฌ์žฅ์‚ฌ์ƒ์ถฉ์ œ', + 'icon': '๐Ÿ’—', + 'keywords': ['์‹ฌ์žฅ์‚ฌ์ƒ์ถฉ', '์‚ฌ์ƒ์ถฉ', 'dirofilaria', 'ํ•˜ํŠธ์›œ'], + 'description': '์‹ฌ์žฅ์‚ฌ์ƒ์ถฉ ์˜ˆ๋ฐฉ์•ฝ' + }, + 'parasite': { + 'name': '๊ตฌ์ถฉ์ œ', + 'icon': '๐Ÿ›', + 'keywords': ['๊ตฌ์ถฉ', '๊ธฐ์ƒ์ถฉ', '๋‚ด๋ถ€๊ตฌ์ถฉ', '์ดŒ์ถฉ', 'ํšŒ์ถฉ', '์‹ญ์ด์ง€์žฅ์ถฉ', '์„ ์ถฉ', '๋ฒผ๋ฃฉ', '์ง„๋“œ๊ธฐ'], + 'description': '๋‚ด/์™ธ๋ถ€ ๊ธฐ์ƒ์ถฉ ์น˜๋ฃŒ' + }, + 'skin': { + 'name': 'ํ”ผ๋ถ€์•ฝ', + 'icon': '๐Ÿงด', + 'keywords': ['ํ”ผ๋ถ€', '์ง„๊ท ', 'ํ”ผ๋ถ€์—ผ', '์•Œ๋ ˆ๋ฅด๊ธฐ', '๊ฐ€๋ ค์›€', '์Šต์ง„', '๊ณฐํŒก์ด', '์•„ํ† ํ”ผ'], + 'description': 'ํ”ผ๋ถ€์งˆํ™˜ ์น˜๋ฃŒ' + }, + 'ear': { + 'name': '๊ท€์•ฝ', + 'icon': '๐Ÿ‘‚', + 'keywords': ['๊ท€', '์™ธ์ด๋„์—ผ', '๊ท“๋ณ‘', '์ด์—ผ', '์ด์ง„๋“œ๊ธฐ'], + 'description': '๊ท€์งˆํ™˜ ์น˜๋ฃŒ' + }, + 'eye': { + 'name': '์•ˆ์•ฝ', + 'icon': '๐Ÿ‘๏ธ', + 'keywords': ['์•ˆ์•ฝ', '๋ˆˆ', '๊ฒฐ๋ง‰์—ผ', '๊ฐ๋ง‰์—ผ', '์•ˆ๊ตฌ'], + 'description': '๋ˆˆ์งˆํ™˜ ์น˜๋ฃŒ' + }, + 'antibiotic': { + 'name': 'ํ•ญ์ƒ์ œ', + 'icon': '๐Ÿ’Š', + 'keywords': ['ํ•ญ์ƒ', '๊ฐ์—ผ', '์„ธ๊ท ', 'ํ•ญ๊ท '], + 'description': '์„ธ๊ท ๊ฐ์—ผ ์น˜๋ฃŒ' + }, + 'digestive': { + 'name': '์ •์žฅ์ œ', + 'icon': '๐Ÿ€', + 'keywords': ['์ •์žฅ', '์žฅ๋‚ด์„ธ๊ท ', '์„ค์‚ฌ', '์œ ์‚ฐ๊ท ', 'ํ”„๋กœ๋ฐ”์ด์˜คํ‹ฑ', '์†Œํ™”๋ถˆ๋Ÿ‰', '์†Œํ™”์žฅ์• '], + 'description': '์†Œํ™”๊ธฐ ๊ฑด๊ฐ•' + }, + 'painkiller': { + 'name': '์†Œ์—ผ์ง„ํ†ต์ œ', + 'icon': '๐Ÿ’ช', + 'keywords': ['์†Œ์—ผ', '์ง„ํ†ต', '๊ด€์ ˆ', '์—ผ์ฆ', 'ํ†ต์ฆ'], + 'description': 'ํ†ต์ฆ/์—ผ์ฆ ์™„ํ™”' + }, + 'vomit': { + 'name': '๊ตฌํ† ์–ต์ œ์ œ', + 'icon': '๐Ÿคข', + 'keywords': ['๊ตฌํ† ', '๋ฉ”์Šค๊บผ์›€', '์˜ค์‹ฌ'], + 'description': '๊ตฌํ†  ์–ต์ œ' + }, +} + + +# ============================================================ +# ์ฒด์ค‘ ๊ธฐ๋ฐ˜ ์šฉ๋Ÿ‰ ๊ณ„์‚ฐ ํ•จ์ˆ˜ +# ============================================================ +def format_tablets(tablets): + """์†Œ์ˆ˜์  ์ •์ œ ์ˆ˜๋ฅผ ์‚ฌ๋žŒ์ด ์ฝ๊ธฐ ์‰ฌ์šด ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜""" + if tablets is None: + return None + if tablets < 0.3: + return "1/4์ •" + elif tablets < 0.6: + return "1/2์ •" + elif tablets < 0.9: + return "3/4์ •" + elif tablets < 1.2: + return "1์ •" + elif tablets < 1.6: + return "1.5์ •" + elif tablets < 2.2: + return "2์ •" + elif tablets < 2.6: + return "2.5์ •" + elif tablets < 3.2: + return "3์ •" + else: + return f"{round(tablets)}์ •" + + +def get_tablets_numeric(tablets): + """ + format_tablets์˜ ์—ญ๋ฐฉํ–ฅ ํ•จ์ˆ˜ - ์ •์ œ ์ˆ˜๋ฅผ ์ˆซ์ž๋กœ ๋ฐ˜ํ™˜ + 1/4์ • โ†’ 0.25, 1/2์ • โ†’ 0.5, 3/4์ • โ†’ 0.75, 1์ • โ†’ 1.0, 1.5์ • โ†’ 1.5 ๋“ฑ + """ + if tablets < 0.3: + return 0.25 # 1/4์ • + elif tablets < 0.6: + return 0.5 # 1/2์ • + elif tablets < 0.9: + return 0.75 # 3/4์ • + elif tablets < 1.2: + return 1.0 # 1์ • + elif tablets < 1.6: + return 1.5 # 1.5์ • + elif tablets < 2.2: + return 2.0 # 2์ • + elif tablets < 2.6: + return 2.5 # 2.5์ • + elif tablets < 3.2: + return 3.0 # 3์ • + else: + return round(tablets) + + +def calculate_recommended_dosage(product_idx, weight_kg, animal_type): + """ + ์ฒด์ค‘ ๊ธฐ๋ฐ˜ ์ ์ • ์šฉ๋Ÿ‰ ๊ณ„์‚ฐ + + Args: + product_idx: APDB.idx (์ œํ’ˆ ๊ณ ์œ  ID) + weight_kg: ๋ฐ˜๋ ค๋™๋ฌผ ์ฒด์ค‘ (kg) + animal_type: 'dog' ๋˜๋Š” 'cat' + + Returns: + dict: ๊ณ„์‚ฐ๋œ ์šฉ๋Ÿ‰ ์ •๋ณด ๋˜๋Š” None + """ + if not weight_kg or weight_kg <= 0: + return None + + try: + # ๋จผ์ € ์ด ์ œํ’ˆ์˜ ๋ชจ๋“  DosageInfo ์กฐํšŒ (ํ”ผํŽซํ˜• ์—ฌ๋ถ€ ํŒ๋‹จ์šฉ) + all_dosage_infos = session.query(DosageInfo).filter( + DosageInfo.apdb_idx == product_idx, + or_( + DosageInfo.animal_type == animal_type, + DosageInfo.animal_type == 'all' + ) + ).all() + + # ํ”ผํŽซํ˜• ์ œํ’ˆ ์—ฌ๋ถ€ ํŒ๋‹จ (DosageInfo๊ฐ€ ํ•˜๋‚˜๋ผ๋„ ํ”ผํŽซ์ด๋ฉด ํ”ผํŽซํ˜•) + is_pipette = any(d.unit_type == 'ํ”ผํŽซ' for d in all_dosage_infos) + + # ์ฒด์ค‘ ๋ฒ”์œ„์— ๋งž๋Š” DosageInfo ์ฐพ๊ธฐ + dosage_info = None + for d in all_dosage_infos: + # ์ฒด์ค‘ ๋ฒ”์œ„๊ฐ€ ์ง€์ •๋œ ๊ฒฝ์šฐ + if d.weight_min_kg is not None and d.weight_max_kg is not None: + if d.weight_min_kg <= weight_kg <= d.weight_max_kg: + dosage_info = d + break + # ์ฒด์ค‘ ๋ฒ”์œ„๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ (์ „์ฒด ์ ์šฉ) + elif d.weight_min_kg is None and d.weight_max_kg is None: + dosage_info = d + break + + # ํ”ผํŽซํ˜• ์ œํ’ˆ์ธ๋ฐ ์ฒด์ค‘ ๋ฒ”์œ„์— ๋งž์ง€ ์•Š์œผ๋ฉด ํ•„ํ„ฐ๋ง์„ ์œ„ํ•ด ํŠน๋ณ„ ์‘๋‹ต ๋ฐ˜ํ™˜ + if is_pipette and not dosage_info: + return { + 'calculated': False, + 'is_pipette': True, # ํ”ผํŽซํ˜•์ด๋ฏ€๋กœ ํ•„ํ„ฐ๋ง ๋Œ€์ƒ + 'message': '', + 'details': {}, + 'warning': None + } + + if not dosage_info: + return None + + result = { + 'calculated': False, + 'is_pipette': is_pipette, # ํ”ผํŽซํ˜• ์ œํ’ˆ ์—ฌ๋ถ€ (์ฒด์ค‘ ํ•„ํ„ฐ๋ง์— ์‚ฌ์šฉ) + 'message': '', + 'details': {}, + 'warning': None + } + + # ์ผ€์ด์Šค 1: kg๋‹น ์šฉ๋Ÿ‰์ด ์žˆ๋Š” ๊ฒฝ์šฐ (์ •์ œํ˜• ๋“ฑ) + if dosage_info.dose_per_kg: + total_dose = weight_kg * dosage_info.dose_per_kg + result['calculated'] = True + result['details'] = { + 'total_dose': round(total_dose, 2), + 'dose_unit': dosage_info.dose_unit, + 'frequency': dosage_info.frequency, + 'route': dosage_info.route + } + + # ์ •์ œ ์ˆ˜ ๊ณ„์‚ฐ (์ •์ œ/์บก์Аํ˜•์ธ ๊ฒฝ์šฐ) + if dosage_info.unit_dose and dosage_info.unit_type in ['์ •', '์บก์А']: + tablets = total_dose / dosage_info.unit_dose + tablets_formatted = format_tablets(tablets) + # ์‹ค์ œ ๋ณต์šฉ๋Ÿ‰ ๊ณ„์‚ฐ (์ •์ œ ์ˆ˜ ร— 1์ •๋‹น ํ•จ๋Ÿ‰) + tablets_numeric = get_tablets_numeric(tablets) + actual_dose = tablets_numeric * dosage_info.unit_dose + + result['details']['tablets'] = round(tablets, 2) + result['details']['tablets_formatted'] = tablets_formatted + result['details']['unit_type'] = dosage_info.unit_type + result['details']['actual_dose'] = actual_dose # ์‹ค์ œ ๋ณต์šฉ๋Ÿ‰ + + result['message'] = f"์ฒด์ค‘ {weight_kg}kg ๊ธฐ์ค€: 1ํšŒ {tablets_formatted} ({round(actual_dose, 1)}{dosage_info.dose_unit})" + else: + result['message'] = f"์ฒด์ค‘ {weight_kg}kg ๊ธฐ์ค€: 1ํšŒ {round(total_dose, 1)}{dosage_info.dose_unit}" + + if dosage_info.frequency: + result['message'] += f", {dosage_info.frequency}" + + # ์ผ€์ด์Šค 2: ์ฒด์ค‘ ๋ฒ”์œ„๋ณ„ ๊ณ ์ • ์šฉ๋Ÿ‰ (ํ”ผํŽซํ˜• ๋“ฑ) + elif dosage_info.weight_min_kg and dosage_info.weight_max_kg: + result['calculated'] = True + result['details'] = { + 'weight_min': dosage_info.weight_min_kg, + 'weight_max': dosage_info.weight_max_kg, + 'unit_dose': dosage_info.unit_dose, + 'dose_unit': dosage_info.dose_unit, + 'frequency': dosage_info.frequency, + 'route': dosage_info.route + } + + result['message'] = f"์ฒด์ค‘ {dosage_info.weight_min_kg}~{dosage_info.weight_max_kg}kg์šฉ" + + if dosage_info.unit_dose and dosage_info.dose_unit: + result['message'] += f": {dosage_info.unit_dose}{dosage_info.dose_unit}" + + if dosage_info.frequency: + result['message'] += f", {dosage_info.frequency}" + + # ์ผ€์ด์Šค 3: ๊ธฐ๋ณธ ์ •๋ณด๋งŒ ์žˆ๋Š” ๊ฒฝ์šฐ + elif dosage_info.frequency or dosage_info.route: + result['calculated'] = True + parts = [] + if dosage_info.frequency: + parts.append(dosage_info.frequency) + if dosage_info.route: + parts.append(f"{dosage_info.route} ํˆฌ์—ฌ") + result['message'] = ', '.join(parts) + + return result + + except Exception as e: + print(f"[์šฉ๋Ÿ‰ ๊ณ„์‚ฐ ์˜ค๋ฅ˜] product_idx={product_idx}, error={e}") + return None + + +def generate_recommendation_reason(animal_type, symptom_descriptions, product_name, component_name, llm_pharm, efficacy_clean, component_code=None, symptom_codes=None): + """GPT-4o-mini๋กœ ์ถ”์ฒœ ์ด์œ  ์ƒ์„ฑ""" + try: + # ํ•œ๊ธ€ ๋™๋ฌผ ํƒ€์ž… + animal_ko = '๊ฐ•์•„์ง€' if animal_type == 'dog' else '๊ณ ์–‘์ด' + + # ํŠน์ˆ˜ ์ผ€์ด์Šค: ์•„์‹œ์—”๋กœ์ • + ์†Œํ™”๊ธฐ ์ฆ์ƒ โ†’ ์ค‘์ฆ ์„ธ๊ท ์„ฑ ์žฅ์—ผ ์„ค๋ช… ์ถ”๊ฐ€ + special_note = "" + digestive_symptoms = {'g01', 'g02', 'g03', 'g04', 'g05'} # ์†Œํ™”๊ธฐ ๊ด€๋ จ ์ฆ์ƒ ์ฝ”๋“œ + if component_code == 'IA5010124' and symptom_codes: # ์•„์‹œ์—”๋กœ์ • (์—”๋กœํ”Œ๋ก์‚ฌ์‹ ) + if any(s in digestive_symptoms for s in symptom_codes): + special_note = "\nโš ๏ธ ํŠน๋ณ„ ์ฐธ๊ณ : ์ด ์ œํ’ˆ์€ ์ผ๋ฐ˜ ์„ค์‚ฌ๊ฐ€ ์•„๋‹Œ ์ค‘์ฆ ์„ธ๊ท ์„ฑ ์žฅ์—ผ์— ์‚ฌ์šฉํ•˜๋Š” ํ•ญ์ƒ์ œ์ž…๋‹ˆ๋‹ค. ์ฆ์ƒ์ด ์‹ฌํ•˜๊ฑฐ๋‚˜ ์ ์•ก/ํ˜ˆ๋ณ€์ด ์žˆ๋Š” ๊ฒฝ์šฐ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค." + + # ๋ณตํ•ฉ ์„ฑ๋ถ„ ์—ฌ๋ถ€ ํ™•์ธ (+ ๊ธฐํ˜ธ๋กœ ์—ฐ๊ฒฐ๋œ ๊ฒฝ์šฐ) + is_complex_formula = '+' in component_name + + # llm_pharm์—์„œ ์ปจํ…์ŠคํŠธ ์ถ”์ถœ + if llm_pharm: + indication = llm_pharm.get('์–ด๋–ค์งˆ๋ณ‘์—์‚ฌ์šฉํ•˜๋‚˜์š”?', '') + description = llm_pharm.get('์ž์—ฐ์–ด์„ค๋ช…', '') or llm_pharm.get('LLM์„ค๋ช…', '') + usage = llm_pharm.get('๊ธฐ๊ฐ„/์šฉ๋ฒ•', '') + caution = llm_pharm.get('check', '') + else: + indication = '' + description = efficacy_clean[:200] if efficacy_clean else '' + usage = '' + caution = '' + + context = f""" +์ œํ’ˆ๋ช…: {product_name} +์„ฑ๋ถ„: {component_name} +์ ์‘์ฆ: {indication} +์„ค๋ช…: {description} +์šฉ๋ฒ•: {usage} +""" + + # ํŠน๋ณ„ ์ง€์‹œ ์‚ฌํ•ญ + special_instruction = "" + + # ๋ณตํ•ฉ ์„ฑ๋ถ„ ์ œํ’ˆ์ผ ๊ฒฝ์šฐ ๊ฐ ์„ฑ๋ถ„ ์—ญํ•  ์„ค๋ช… ์š”์ฒญ + if is_complex_formula: + special_instruction += "\n์ค‘์š”: ์ด ์ œํ’ˆ์€ ์—ฌ๋Ÿฌ ์„ฑ๋ถ„์ด ํ•จ๊ป˜ ๋“ค์–ด๊ฐ„ '๋ณตํ•ฉ์ œ'์ž…๋‹ˆ๋‹ค. ๊ฐ ์„ฑ๋ถ„์˜ ์—ญํ• ์„ ๊ฐ„๋‹จํžˆ ์„ค๋ช…ํ•ด์ฃผ์„ธ์š” (์˜ˆ: 'A์„ฑ๋ถ„์€ ์„ธ๊ท ์„ ์–ต์ œํ•˜๊ณ , B์„ฑ๋ถ„์€ ์—ผ์ฆ์„ ๊ฐ€๋ผ์•‰ํž™๋‹ˆ๋‹ค'). ๋‹จ, '์„ฑ๋ถ„์ด ๊ฒฐํ•ฉ๋˜์–ด ์žˆ๋‹ค'๋Š” ํ‘œํ˜„์€ ํ™”ํ•™์  ์˜คํ•ด๋ฅผ ์ค„ ์ˆ˜ ์žˆ์œผ๋‹ˆ ์‚ฌ์šฉํ•˜์ง€ ๋งˆ์„ธ์š”." + + # ์•„์‹œ์—”๋กœ์ • ์†Œํ™”๊ธฐ ์ฆ์ƒ์ผ ๊ฒฝ์šฐ ํ”„๋กฌํ”„ํŠธ์— ํŠน๋ณ„ ์ง€์‹œ ์ถ”๊ฐ€ + if component_code == 'IA5010124' and symptom_codes and any(s in digestive_symptoms for s in symptom_codes): + special_instruction += "\n์ฃผ์˜: ์ด ์ œํ’ˆ์€ ํ•ญ์ƒ์ œ๋กœ, ์ค‘์ฆ ์„ธ๊ท ์„ฑ ์žฅ์—ผ(์‹ฌํ•œ ์—ผ์ฆ์„ฑ ์žฅ์งˆํ™˜)์— ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์ธ ์„ค์‚ฌ๊ฐ€ ์•„๋‹Œ ์„ธ๊ท  ๊ฐ์—ผ์— ์˜ํ•œ ์žฅ์—ผ์— ํšจ๊ณผ์ ์ด๋ผ๋Š” ์ ์„ ๋ฐ˜๋“œ์‹œ ์–ธ๊ธ‰ํ•ด์ฃผ์„ธ์š”." + + # ๋ณตํ•ฉ ์„ฑ๋ถ„ ์ œํ’ˆ์€ ๋” ๊ธด ์„ค๋ช… ํ—ˆ์šฉ + sentence_limit = "3-4๋ฌธ์žฅ" if is_complex_formula else "2๋ฌธ์žฅ ์ด๋‚ด" + + prompt = f"""๋ฐ˜๋ ค์ธ์ด {animal_ko}์˜ ์ฆ์ƒ์œผ๋กœ "{', '.join(symptom_descriptions)}"๋ฅผ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค. + +์•„๋ž˜ ์ œํ’ˆ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ, ์™œ ์ด ์ œํ’ˆ์ด ํ•ด๋‹น ์ฆ์ƒ์— ์ ํ•ฉํ•œ์ง€ {sentence_limit}๋กœ ์นœ์ ˆํ•˜๊ณ  ๋”ฐ๋œปํ•˜๊ฒŒ ์„ค๋ช…ํ•ด์ฃผ์„ธ์š”. +์˜ˆ์ƒ ์งˆํ™˜๋ช…์„ ์–ธ๊ธ‰ํ•˜๊ณ , ์ œํ’ˆ์˜ ํšจ๋Šฅ์„ ๊ฐ„๋‹จํžˆ ์„ค๋ช…ํ•ด์ฃผ์„ธ์š”. +์ „๋ฌธ์šฉ์–ด๋Š” ์‰ฝ๊ฒŒ ํ’€์–ด์„œ ์„ค๋ช…ํ•ด์ฃผ์„ธ์š”. + +์ฃผ์˜์‚ฌํ•ญ: +- "๊ท€ํ•˜", "๊ณ ๊ฐ๋‹˜" ๊ฐ™์€ ๋”ฑ๋”ฑํ•œ ํ‘œํ˜„ ๋Œ€์‹  "๋ฐ˜๋ ค์ธ๋‹˜" ๋˜๋Š” ์ƒ๋žตํ•˜์„ธ์š”. +- ๋ณตํ•ฉ ์„ฑ๋ถ„ ์ œํ’ˆ์€ "A์™€ B๊ฐ€ ๊ฒฐํ•ฉ๋˜์–ด" ๋Œ€์‹  "A์™€ B๊ฐ€ ํ•จ๊ป˜ ๋“ค์–ด์žˆ์–ด" ๋˜๋Š” "A ์„ฑ๋ถ„๊ณผ B ์„ฑ๋ถ„์ด ๊ฐ๊ฐ" ํ‘œํ˜„์„ ์‚ฌ์šฉํ•˜์„ธ์š”.{special_instruction} + +{context} +""" + + # ๋ณตํ•ฉ ์„ฑ๋ถ„ ์ œํ’ˆ์€ ๋” ๊ธด ์‘๋‹ต ํ—ˆ์šฉ + max_tok = 280 if is_complex_formula else 120 + + client = openai.OpenAI(api_key=OPENAI_API_KEY) + response = client.chat.completions.create( + model="gpt-4o-mini", + messages=[{"role": "user", "content": prompt}], + max_tokens=max_tok, + temperature=0.7 + ) + + gpt_response = response.choices[0].message.content.strip() + + # ํŠน๋ณ„ ์ฐธ๊ณ ์‚ฌํ•ญ ์ถ”๊ฐ€ (์•„์‹œ์—”๋กœ์ • + ์†Œํ™”๊ธฐ ์ฆ์ƒ) + if special_note: + return gpt_response + special_note + return gpt_response + + except Exception as e: + print(f"OpenAI API Error: {e}") + # fallback: DB์˜ ์ž์—ฐ์–ด์„ค๋ช… ์‚ฌ์šฉ + if llm_pharm and llm_pharm.get('์ž์—ฐ์–ด์„ค๋ช…'): + fallback_msg = llm_pharm.get('์ž์—ฐ์–ด์„ค๋ช…') + if special_note: + return fallback_msg + special_note + return fallback_msg + return "ํ•ด๋‹น ์ฆ์ƒ์— ํšจ๊ณผ์ ์ธ ์„ฑ๋ถ„์ด ํฌํ•จ๋œ ์ œํ’ˆ์ž…๋‹ˆ๋‹ค." + +# ============================================================ +# HTML ํ…œํ”Œ๋ฆฟ (๋‹จ์ผ ํŒŒ์ผ๋กœ ํฌํ•จ) +# ============================================================ + +HTML_TEMPLATE = ''' + + + + + + ๋™๋ฌผ์•ฝ ์ถ”์ฒœ - ์• ๋‹ˆํŒœ + + + +
+ +
+
+
+ +

๋™๋ฌผ์•ฝ ์ถ”์ฒœ

+

์ฆ์ƒ ๋˜๋Š” ์ œํ’ˆ๊ตฐ์„ ์„ ํƒํ•˜๋ฉด ์ ํ•ฉํ•œ ์•ฝ์„ ์ถ”์ฒœํ•ด๋“œ๋ ค์š”

+
+ + +
+ + +
+ +
+ +
+ +
+
+
๊ฐ•์•„์ง€
+
๊ฐ•์•„์ง€
+
+
+
๊ณ ์–‘์ด
+
๊ณ ์–‘์ด
+
+
+ +
+ + + + + +
+ + +
+ + +
+ +
+ + + +
+
+ + +
+ +
+ {% for category, symptoms in symptoms_by_category.items() %} +
+
{{ category }}
+
+ {% for symptom in symptoms %} +
+ {{ symptom.description }} +
+ {% endfor %} +
+
+ {% endfor %} +
+
+ + + + + +
+
+
+ + +
+
+
+ +
+ +
+
+ + +
+ + + + +''' + +# ============================================================ +# ๋ผ์šฐํŠธ +# ============================================================ + +def get_product_image(product): + """์ œํ’ˆ ์ด๋ฏธ์ง€ URL ๋ฐ˜ํ™˜ (์—†์œผ๋ฉด ๋™์ผ ์„ฑ๋ถ„ ์ œํ’ˆ์—์„œ fallback)""" + # 1์ˆœ์œ„: ๊ณ ๋„๋ชฐ CDN ์ด๋ฏธ์ง€ + if product.godoimage_url_f: + return product.godoimage_url_f + + # 2์ˆœ์œ„: ani.0bin.in ์ด๋ฏธ์ง€ + if product.image_url1: + return product.image_url1 + + # 3์ˆœ์œ„: ๋™์ผ ์„ฑ๋ถ„์˜ ๋‹ค๋ฅธ ์ œํ’ˆ์—์„œ ์ด๋ฏธ์ง€ ๊ฐ€์ ธ์˜ค๊ธฐ + if product.component_code: + fallback = session.query(APDB).filter( + APDB.component_code == product.component_code, + APDB.idx != product.idx, + (APDB.godoimage_url_f != None) | (APDB.image_url1 != None) + ).first() + + if fallback: + return fallback.godoimage_url_f or fallback.image_url1 + + return None + + +@app.route('/') +def index(): + """๋ฉ”์ธ ํŽ˜์ด์ง€""" + # ์ฆ์ƒ ๋ชฉ๋ก ์กฐํšŒ (์นดํ…Œ๊ณ ๋ฆฌ๋ณ„) + symptoms = session.query(Symptoms).order_by(Symptoms.prefix, Symptoms.symptom_code).all() + + symptoms_by_category = {} + category_names = { + 'a': '๐Ÿ‘๏ธ ๋ˆˆ', + 'b': '๐Ÿฆท ๊ตฌ๊ฐ•/์น˜์•„', + 'c': '๐Ÿ‘‚ ๊ท€', + 'd': '๐Ÿพ ํ”ผ๋ถ€/ํ„ธ', + 'e': '๐Ÿฆต ๋‹ค๋ฆฌ/๋ฐœ', + 'f': '๐Ÿฆด ๋ผˆ/๊ด€์ ˆ', + 'g': '๐Ÿฝ๏ธ ์†Œํ™”๊ธฐ/๋ฐฐ๋ณ€' + } + + for symptom in symptoms: + category = category_names.get(symptom.prefix, symptom.prefix_description or symptom.prefix) + if category not in symptoms_by_category: + symptoms_by_category[category] = [] + symptoms_by_category[category].append({ + 'code': symptom.symptom_code, + 'description': symptom.symptom_description + }) + + return render_template_string(HTML_TEMPLATE, symptoms_by_category=symptoms_by_category) + + +@app.route('/api/categories', methods=['GET']) +def get_categories(): + """์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ œํ’ˆ ์ˆ˜ ์กฐํšŒ API""" + try: + animal_type = request.args.get('animal_type', 'dog') + animal_ko = '๊ฐœ' if animal_type == 'dog' else '๊ณ ์–‘์ด' + + # ์žฌ๊ณ ๊ฐ€ ์žˆ๋Š” ์ œํ’ˆ ์กฐํšŒ + inventory_products = session.query(APDB).join( + Inventory, Inventory.apdb_id == APDB.idx + ).filter( + Inventory.transaction_type == 'INBOUND' + ).distinct().all() + + # ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ œํ’ˆ ์ˆ˜ ๊ณ„์‚ฐ + categories = [] + for cat_code, cat_info in PRODUCT_CATEGORIES.items(): + count = 0 + keywords = cat_info.get('keywords', []) + + for product in inventory_products: + # ๋™๋ฌผ ํƒ€์ž… ์ฒดํฌ (ComponentCode์—์„œ) + component = session.query(ComponentCode).filter( + ComponentCode.code == product.component_code + ).first() + + if component: + target_animals = component.target_animals or [] + # JSONB ๋ฆฌ์ŠคํŠธ ํ˜•ํƒœ ์ฒ˜๋ฆฌ + if isinstance(target_animals, list): + target_str = ', '.join(target_animals).lower() + else: + target_str = str(target_animals).lower() + if animal_ko not in target_str and '๊ฐœ, ๊ณ ์–‘์ด' not in target_str and '๊ณ ์–‘์ด, ๊ฐœ' not in target_str: + continue + + # ํšจ๋Šฅํšจ๊ณผ์—์„œ ํ‚ค์›Œ๋“œ ๋งค์นญ + efficacy = (product.efficacy_effect or '').lower() + # HTML ํƒœ๊ทธ ์ œ๊ฑฐ + import re + efficacy_clean = re.sub(r'<[^>]+>', '', efficacy) + + if any(kw in efficacy_clean for kw in keywords): + count += 1 + + if count > 0: + categories.append({ + 'code': cat_code, + 'name': cat_info['name'], + 'icon': cat_info['icon'], + 'count': count, + 'description': cat_info['description'] + }) + + # ์ œํ’ˆ ์ˆ˜ ๊ธฐ์ค€ ์ •๋ ฌ + categories.sort(key=lambda x: x['count'], reverse=True) + + return jsonify({ + 'success': True, + 'categories': categories + }) + + except Exception as e: + print(f"์นดํ…Œ๊ณ ๋ฆฌ ์กฐํšŒ ์˜ค๋ฅ˜: {e}") + import traceback + traceback.print_exc() + return jsonify({ + 'success': False, + 'message': str(e), + 'categories': [] + }) + + +@app.route('/api/recommend', methods=['POST']) +def recommend(): + """์ถ”์ฒœ API - ์ฆ์ƒ-์„ฑ๋ถ„ ๋งคํ•‘ ํ…Œ์ด๋ธ” ํ™œ์šฉ""" + try: + data = request.get_json() + animal_type = data.get('animal_type') # 'dog' or 'cat' + breed = data.get('breed') # ๊ฒฌ์ข… (MDR-1 ์ฒดํฌ์šฉ) + weight = data.get('weight') + pregnancy = data.get('pregnancy', 'none') # ์ž„์‹ /์ˆ˜์œ  ์—ฌ๋ถ€ + symptom_codes = data.get('symptoms', []) + + if not animal_type or not symptom_codes: + return jsonify({ + 'success': False, + 'message': '๋™๋ฌผ ์ข…๋ฅ˜์™€ ์ฆ์ƒ์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”.' + }) + + # ํ•œ๊ธ€ ๋ณ€ํ™˜ + animal_ko = '๊ฐœ' if animal_type == 'dog' else '๊ณ ์–‘์ด' + + # 1. ์„ ํƒ๋œ ์ฆ์ƒ ์ •๋ณด ์กฐํšŒ + matched_symptoms = session.query(Symptoms).filter( + Symptoms.symptom_code.in_(symptom_codes) + ).all() + symptom_descriptions = [s.symptom_description for s in matched_symptoms] + + # 2. ์ฆ์ƒ-์„ฑ๋ถ„ ๋งคํ•‘์—์„œ ๊ด€๋ จ ์„ฑ๋ถ„ ์ฝ”๋“œ ์กฐํšŒ + mapped_components = session.query(SymptomComponentMapping.component_code).filter( + SymptomComponentMapping.symptom_code.in_(symptom_codes) + ).distinct().all() + mapped_component_codes = [m[0] for m in mapped_components] + + # ๋งคํ•‘๋œ ์„ฑ๋ถ„์ด ์—†์œผ๋ฉด ์ถ”์ฒœ ๋ถˆ๊ฐ€ + if not mapped_component_codes: + return jsonify({ + 'success': True, + 'animal_type': animal_type, + 'matched_symptoms': symptom_descriptions, + 'recommendations': [], + 'total_count': 0, + 'message': '์„ ํƒํ•˜์‹  ์ฆ์ƒ์— ๋งž๋Š” ์•ฝํ’ˆ ๋งคํ•‘์ด ์•„์ง ์—†์Šต๋‹ˆ๋‹ค. ์ˆ˜์˜์‚ฌ์™€ ์ƒ๋‹ด์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.' + }) + + # 3. ์žฌ๊ณ  ์ œํ’ˆ ์กฐํšŒ (INBOUND) + ๋งคํ•‘๋œ ์„ฑ๋ถ„ ํ•„ํ„ฐ + inventory_products = session.query(APDB).join( + Inventory, Inventory.apdb_id == APDB.idx + ).filter( + Inventory.transaction_type == 'INBOUND', + APDB.component_code.in_(mapped_component_codes) + ).distinct().all() + + # 4. ๋™๋ฌผ ์ข…๋ฅ˜์— ๋งž๋Š” ์ œํ’ˆ ํ•„ํ„ฐ๋ง + ์ ์ˆ˜ ๊ณ„์‚ฐ + recommendations = [] + + for product in inventory_products: + # ComponentCode์—์„œ target_animals ํ™•์ธ + comp = session.query(ComponentCode).filter( + ComponentCode.code == product.component_code + ).first() + + if not comp: + continue + + target_animals = comp.target_animals or [] + + # ๋Œ€์ƒ ๋™๋ฌผ ์ฒดํฌ (target_animals๊ฐ€ ์—†์œผ๋ฉด ์ „์ฒด ์‚ฌ์šฉ ๊ฐ€๋Šฅ์œผ๋กœ ๊ฐ„์ฃผ) + if target_animals and animal_ko not in target_animals: + continue # ํ•ด๋‹น ๋™๋ฌผ์—๊ฒŒ ์‚ฌ์šฉ ๋ถˆ๊ฐ€ + + # ์ด ์„ฑ๋ถ„์ด ๋ช‡ ๊ฐœ์˜ ์„ ํƒ ์ฆ์ƒ๊ณผ ๋งคํ•‘๋˜๋Š”์ง€ ์ ์ˆ˜ ๊ณ„์‚ฐ + matching_symptom_count = session.query(SymptomComponentMapping).filter( + SymptomComponentMapping.component_code == product.component_code, + SymptomComponentMapping.symptom_code.in_(symptom_codes) + ).count() + + # ํšจ๋Šฅํšจ๊ณผ์—์„œ ํ‚ค์›Œ๋“œ ์ถ”์ถœ + efficacy = product.efficacy_effect or '' + efficacy_clean = re.sub(r'<[^>]+>', '', efficacy).lower() + + # ์šฉ๋„ ๊ฐ„๋‹จ ์„ค๋ช… ์ถ”์ถœ + efficacy_short = '' + if '์™ธ์ด๋„์—ผ' in efficacy_clean or '๊ท“๋ณ‘' in efficacy_clean or '๊ท€' in efficacy_clean: + efficacy_short = '๊ท€ ์น˜๋ฃŒ์ œ' + elif '๊ตฌํ† ' in efficacy_clean: + efficacy_short = '๊ตฌํ†  ์–ต์ œ์ œ' + elif 'ํ”ผ๋ถ€' in efficacy_clean or '์ง„๊ท ' in efficacy_clean or 'ํ”ผ๋ถ€์—ผ' in efficacy_clean: + efficacy_short = 'ํ”ผ๋ถ€ ์น˜๋ฃŒ์ œ' + elif '๊ด€์ ˆ' in efficacy_clean or '์†Œ์—ผ' in efficacy_clean or '์ง„ํ†ต' in efficacy_clean: + efficacy_short = '์†Œ์—ผ์ง„ํ†ต์ œ' + elif '๊ตฌ์ถฉ' in efficacy_clean or '๊ธฐ์ƒ์ถฉ' in efficacy_clean or '์ง„๋“œ๊ธฐ' in efficacy_clean: + efficacy_short = '๊ตฌ์ถฉ์ œ' + elif 'ํ•ญ์ƒ' in efficacy_clean or '๊ฐ์—ผ' in efficacy_clean: + efficacy_short = 'ํ•ญ์ƒ์ œ' + elif '์žฅ' in efficacy_clean or '์„ค์‚ฌ' in efficacy_clean or '์ •์žฅ' in efficacy_clean: + efficacy_short = '์ •์žฅ์ œ' + + recommendations.append({ + 'idx': product.idx, # ์ถ”์ฒœ ์ œํ’ˆ API ํ˜ธ์ถœ์šฉ + 'name': product.product_name, + 'product_idx': product.idx, # ์šฉ๋Ÿ‰ ๊ณ„์‚ฐ์šฉ + 'component_code': product.component_code, # MDR-1 ์ฒดํฌ์šฉ + 'component_name': comp.component_name_ko or product.component_name_ko or '', + 'target_animals': ', '.join(target_animals) if target_animals else '์ „์ฒด', + 'efficacy': efficacy_short, + 'score': matching_symptom_count, # ๋งคํ•‘ ์ ์ˆ˜ + 'image_url': get_product_image(product), # ๊ณ ๋„๋ชฐ CDN ์ด๋ฏธ์ง€ (fallback ํฌํ•จ) + 'llm_pharm': product.llm_pharm, # AI ์ถ”์ฒœ ์ด์œ  ์ƒ์„ฑ์šฉ + 'efficacy_clean': efficacy_clean, # AI ์ถ”์ฒœ ์ด์œ  ์ƒ์„ฑ์šฉ fallback + 'usage_guide': product.usage_guide # ์ƒ์„ธ ์‚ฌ์šฉ ์•ˆ๋‚ด (ํฌ์„ ๋น„์œจ ๋“ฑ) + }) + + # ์ ์ˆ˜์ˆœ ์ •๋ ฌ (๋†’์€ ์ ์ˆ˜ = ๋” ๋งŽ์€ ์ฆ์ƒ๊ณผ ๋งคํ•‘) + recommendations.sort(key=lambda x: x['score'], reverse=True) + + # ์ค‘๋ณต ์ œ๊ฑฐ (๋™์ผ ์ œํ’ˆ๋ช…) + seen_names = set() + unique_recommendations = [] + for rec in recommendations: + if rec['name'] not in seen_names: + seen_names.add(rec['name']) + unique_recommendations.append(rec) + + # ์ƒ์œ„ 5๊ฐœ๋งŒ + unique_recommendations = unique_recommendations[:5] + + # AI ์ถ”์ฒœ ์ด์œ  ์ƒ์„ฑ (๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ) + def generate_reason_for_product(rec): + reason = generate_recommendation_reason( + animal_type=animal_type, + symptom_descriptions=symptom_descriptions, + product_name=rec['name'], + component_name=rec['component_name'], + llm_pharm=rec.get('llm_pharm'), + efficacy_clean=rec.get('efficacy_clean', ''), + component_code=rec.get('component_code'), + symptom_codes=symptom_codes + ) + return {**rec, 'reason': reason} + + with ThreadPoolExecutor(max_workers=5) as executor: + unique_recommendations = list(executor.map(generate_reason_for_product, unique_recommendations)) + + # MDR-1 ์ฒดํฌ (๊ฐ•์•„์ง€ + ๊ฒฌ์ข… ์„ ํƒ๋œ ๊ฒฝ์šฐ) + if animal_type == 'dog' and breed and breed != 'other': + breed_info = MDR1_BREEDS.get(breed, {}) + breed_risk = breed_info.get('risk', 'none') + breed_name = breed_info.get('name', '') + + for rec in unique_recommendations: + component_code = rec.get('component_code', '') + if component_code in MDR1_SENSITIVE_COMPONENTS: + if breed_risk == 'high': + rec['mdr1_warning'] = 'danger' + rec['mdr1_message'] = f"โš ๏ธ {breed_name}์€(๋Š”) MDR-1 ์œ ์ „์ž ๋ณ€์ด ๊ณ ์œ„ํ—˜ ๊ฒฌ์ข…์ž…๋‹ˆ๋‹ค. ์ด ์•ฝ๋ฌผ(์ด๋ฒ„๋ฉ•ํ‹ด/์…€๋ผ๋ฉ•ํ‹ด ๊ณ„์—ด)์€ ์‹ฌ๊ฐํ•œ ์‹ ๊ฒฝ๋…์„ฑ ๋ถ€์ž‘์šฉ์„ ์œ ๋ฐœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐ˜๋“œ์‹œ ์ˆ˜์˜์‚ฌ ์ƒ๋‹ด ํ›„ ์‚ฌ์šฉํ•˜์„ธ์š”." + elif breed_risk == 'medium': + rec['mdr1_warning'] = 'caution' + rec['mdr1_message'] = f"โš ๏ธ {breed_name}์€(๋Š”) MDR-1 ์œ ์ „์ž ๋ณ€์ด ์ค‘์œ„ํ—˜ ๊ฒฌ์ข…์ž…๋‹ˆ๋‹ค. ์ด ์•ฝ๋ฌผ ์‚ฌ์šฉ ์‹œ ์ €์šฉ๋Ÿ‰๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๊ณ , ๋ถ€์ž‘์šฉ ๋ฐœ์ƒ ์‹œ ์ฆ‰์‹œ ์ˆ˜์˜์‚ฌ์—๊ฒŒ ์—ฐ๋ฝํ•˜์„ธ์š”." + elif breed_risk == 'low': + rec['mdr1_warning'] = 'info' + rec['mdr1_message'] = f"โ„น๏ธ {breed_name}์€(๋Š”) MDR-1 ์ €์œ„ํ—˜ ๊ฒฌ์ข…์ด๋‚˜, ๊ฐœ์ฒด๋ณ„ ์ฐจ์ด๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฒ˜์Œ ์‚ฌ์šฉ ์‹œ ์ฃผ์˜๊นŠ๊ฒŒ ๊ด€์ฐฐํ•˜์„ธ์š”." + elif breed_risk == 'unknown': + rec['mdr1_warning'] = 'info' + rec['mdr1_message'] = "โ„น๏ธ ๊ฒฌ์ข…์— ๋”ฐ๋ผ ์ด ์•ฝ๋ฌผ(Avermectin ๊ณ„์—ด)์— ๋ฏผ๊ฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ˆ˜์˜์‚ฌ ์ƒ๋‹ด์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค." + + # ์ฒด์ค‘ ๊ธฐ๋ฐ˜ ์šฉ๋Ÿ‰ ๊ณ„์‚ฐ + ํ”ผํŽซํ˜• ์ œํ’ˆ ํ•„ํ„ฐ๋ง + weight_kg = None + try: + if weight: + weight_kg = float(weight) + except (ValueError, TypeError): + weight_kg = None + + # ํ”ผํŽซํ˜• ์ œํ’ˆ ์ฒด์ค‘ ํ•„ํ„ฐ๋ง ์ ์šฉ + weight_filtered_recommendations = [] + for rec in unique_recommendations: + if weight_kg and weight_kg > 0: + dosage_info = calculate_recommended_dosage( + product_idx=rec.get('product_idx'), + weight_kg=weight_kg, + animal_type=animal_type + ) + + # ํ”ผํŽซํ˜• ์ œํ’ˆ: ์ฒด์ค‘ ๋ฒ”์œ„์— ๋งž์ง€ ์•Š์œผ๋ฉด ์ œ์™ธ + if dosage_info and dosage_info.get('is_pipette'): + if dosage_info.get('calculated'): + rec['dosage_info'] = dosage_info + weight_filtered_recommendations.append(rec) + # else: ์ฒด์ค‘ ๋ฒ”์œ„ ๋ฐ–์ด๋ฏ€๋กœ ์ œ์™ธ (์ถ”๊ฐ€ํ•˜์ง€ ์•Š์Œ) + else: + # ํ”ผํŽซํ˜•์ด ์•„๋‹Œ ์ œํ’ˆ์€ ๊ทธ๋Œ€๋กœ ์œ ์ง€ + if dosage_info and dosage_info.get('calculated'): + rec['dosage_info'] = dosage_info + weight_filtered_recommendations.append(rec) + else: + # ์ฒด์ค‘ ๋ฏธ์ž…๋ ฅ ์‹œ ํ•„ํ„ฐ๋ง ์—†์ด ๋ชจ๋‘ ํฌํ•จ + weight_filtered_recommendations.append(rec) + + unique_recommendations = weight_filtered_recommendations + + # ์‘๋‹ต์—์„œ ๋ถˆํ•„์š”ํ•œ ํ•„๋“œ ์ œ๊ฑฐ + for rec in unique_recommendations: + rec.pop('llm_pharm', None) + rec.pop('efficacy_clean', None) + rec.pop('component_code', None) + rec.pop('product_idx', None) # ์šฉ๋Ÿ‰ ๊ณ„์‚ฐ ํ›„ ์ œ๊ฑฐ + + return jsonify({ + 'success': True, + 'animal_type': animal_type, + 'matched_symptoms': symptom_descriptions, + 'recommendations': unique_recommendations, + 'total_count': len(unique_recommendations), + 'weight_kg': weight_kg, # ์ž…๋ ฅ๋œ ์ฒด์ค‘ ๋ฐ˜ํ™˜ + 'pregnancy_status': pregnancy # ์ž„์‹ /์ˆ˜์œ  ์ƒํƒœ ๋ฐ˜ํ™˜ + }) + + except Exception as e: + print(f"Error: {e}") + return jsonify({ + 'success': False, + 'message': f'์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}' + }) + + +@app.route('/api/recommend_by_category', methods=['POST']) +def recommend_by_category(): + """์ œํ’ˆ๊ตฐ ๊ธฐ๋ฐ˜ ์ถ”์ฒœ API""" + try: + data = request.get_json() + animal_type = data.get('animal_type') # 'dog' or 'cat' + breed = data.get('breed') # ๊ฒฌ์ข… (MDR-1 ์ฒดํฌ์šฉ) + category = data.get('category') # ์ œํ’ˆ๊ตฐ ์ฝ”๋“œ + pregnancy = data.get('pregnancy', 'none') # ์ž„์‹ /์ˆ˜์œ  ์—ฌ๋ถ€ + + if not animal_type or not category: + return jsonify({ + 'success': False, + 'message': '๋™๋ฌผ ์ข…๋ฅ˜์™€ ์ œํ’ˆ๊ตฐ์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”.' + }) + + animal_ko = '๊ฐœ' if animal_type == 'dog' else '๊ณ ์–‘์ด' + + # ์นดํ…Œ๊ณ ๋ฆฌ ์ •๋ณด + category_info = PRODUCT_CATEGORIES.get(category, {}) + keywords = category_info.get('keywords', []) + category_name = category_info.get('name', category) + + if not keywords: + return jsonify({ + 'success': False, + 'message': '์ž˜๋ชป๋œ ์ œํ’ˆ๊ตฐ์ž…๋‹ˆ๋‹ค.' + }) + + # ์žฌ๊ณ ๊ฐ€ ์žˆ๋Š” ์ œํ’ˆ ์กฐํšŒ + inventory_products = session.query(APDB).join( + Inventory, Inventory.apdb_id == APDB.idx + ).filter( + Inventory.transaction_type == 'INBOUND' + ).distinct().all() + + recommendations = [] + import re + + for product in inventory_products: + # ๋™๋ฌผ ํƒ€์ž… ์ฒดํฌ (ComponentCode์—์„œ) + component = session.query(ComponentCode).filter( + ComponentCode.code == product.component_code + ).first() + + if component: + target_animals = component.target_animals or [] + # JSONB ๋ฆฌ์ŠคํŠธ ํ˜•ํƒœ ์ฒ˜๋ฆฌ + if isinstance(target_animals, list): + target_str = ', '.join(target_animals).lower() + else: + target_str = str(target_animals).lower() + if animal_ko not in target_str and '๊ฐœ, ๊ณ ์–‘์ด' not in target_str and '๊ณ ์–‘์ด, ๊ฐœ' not in target_str: + continue + + # ํšจ๋Šฅํšจ๊ณผ์—์„œ ํ‚ค์›Œ๋“œ ๋งค์นญ + efficacy = (product.efficacy_effect or '').lower() + efficacy_clean = re.sub(r'<[^>]+>', '', efficacy) + + if any(kw in efficacy_clean for kw in keywords): + # ํšจ๋Šฅํšจ๊ณผ ์งง๊ฒŒ ์ถ”์ถœ + efficacy_short = efficacy_clean[:100] + '...' if len(efficacy_clean) > 100 else efficacy_clean + + # target_animals ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + target_animals_str = ', '.join(component.target_animals) if component and isinstance(component.target_animals, list) else (component.target_animals if component else '') + + rec = { + 'idx': product.idx, # ์ถ”์ฒœ ์ œํ’ˆ API ํ˜ธ์ถœ์šฉ + 'name': product.product_name, + 'product_idx': product.idx, # ์šฉ๋Ÿ‰ ๊ณ„์‚ฐ์šฉ + 'component_name': component.component_name_ko if component else product.component_code, + 'component_code': product.component_code, + 'target_animals': target_animals_str, + 'efficacy': efficacy_short, + 'image_url': get_product_image(product), + 'usage_guide': product.usage_guide # ์ƒ์„ธ ์‚ฌ์šฉ ์•ˆ๋‚ด (ํฌ์„ ๋น„์œจ ๋“ฑ) + } + + # MDR-1 ์ฒดํฌ (๊ฐ•์•„์ง€์ด๊ณ  ๊ฒฌ์ข…์ด ์„ ํƒ๋œ ๊ฒฝ์šฐ) + if animal_type == 'dog' and breed and breed != 'other': + if product.component_code in MDR1_SENSITIVE_COMPONENTS: + breed_info = MDR1_BREEDS.get(breed, {}) + risk = breed_info.get('risk', 'none') + + if risk == 'high': + rec['mdr1_warning'] = 'danger' + rec['mdr1_message'] = f"โš ๏ธ {breed_info.get('name', breed)}์€(๋Š”) ์ด ์•ฝ๋ฌผ์— ์‹ฌ๊ฐํ•œ ์‹ ๊ฒฝ๋…์„ฑ ๋ถ€์ž‘์šฉ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐ˜๋“œ์‹œ ์ˆ˜์˜์‚ฌ ์ƒ๋‹ด ํ›„ ์‚ฌ์šฉํ•˜์„ธ์š”." + elif risk == 'medium': + rec['mdr1_warning'] = 'caution' + rec['mdr1_message'] = f"โš ๏ธ {breed_info.get('name', breed)}์€(๋Š”) ์ด ์•ฝ๋ฌผ์— ๋ฏผ๊ฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ €์šฉ๋Ÿ‰๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๊ณ  ๋ชจ๋‹ˆํ„ฐ๋งํ•˜์„ธ์š”." + elif risk == 'unknown': + rec['mdr1_warning'] = 'info' + rec['mdr1_message'] = "โ„น๏ธ ๊ฒฌ์ข…์— ๋”ฐ๋ผ ์ด ์•ฝ๋ฌผ์— ๋ฏผ๊ฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ˆ˜์˜์‚ฌ ์ƒ๋‹ด์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค." + + recommendations.append(rec) + + # ์ค‘๋ณต ์ œ๊ฑฐ (์ œํ’ˆ๋ช… ๊ธฐ์ค€) + seen_names = set() + unique_recommendations = [] + for rec in recommendations: + if rec['name'] not in seen_names: + seen_names.add(rec['name']) + unique_recommendations.append(rec) + + # ์ฒด์ค‘ ๊ธฐ๋ฐ˜ ์šฉ๋Ÿ‰ ๊ณ„์‚ฐ + ํ”ผํŽซํ˜• ์ œํ’ˆ ํ•„ํ„ฐ๋ง + weight = data.get('weight') + weight_kg = None + try: + if weight: + weight_kg = float(weight) + except (ValueError, TypeError): + weight_kg = None + + # ํ”ผํŽซํ˜• ์ œํ’ˆ ์ฒด์ค‘ ํ•„ํ„ฐ๋ง ์ ์šฉ + weight_filtered_recommendations = [] + for rec in unique_recommendations: + if weight_kg and weight_kg > 0: + dosage_info = calculate_recommended_dosage( + product_idx=rec.get('product_idx'), + weight_kg=weight_kg, + animal_type=animal_type + ) + + # ํ”ผํŽซํ˜• ์ œํ’ˆ: ์ฒด์ค‘ ๋ฒ”์œ„์— ๋งž์ง€ ์•Š์œผ๋ฉด ์ œ์™ธ + if dosage_info and dosage_info.get('is_pipette'): + if dosage_info.get('calculated'): + rec['dosage_info'] = dosage_info + weight_filtered_recommendations.append(rec) + # else: ์ฒด์ค‘ ๋ฒ”์œ„ ๋ฐ–์ด๋ฏ€๋กœ ์ œ์™ธ (์ถ”๊ฐ€ํ•˜์ง€ ์•Š์Œ) + else: + # ํ”ผํŽซํ˜•์ด ์•„๋‹Œ ์ œํ’ˆ์€ ๊ทธ๋Œ€๋กœ ์œ ์ง€ + if dosage_info and dosage_info.get('calculated'): + rec['dosage_info'] = dosage_info + weight_filtered_recommendations.append(rec) + else: + # ์ฒด์ค‘ ๋ฏธ์ž…๋ ฅ ์‹œ ํ•„ํ„ฐ๋ง ์—†์ด ๋ชจ๋‘ ํฌํ•จ + weight_filtered_recommendations.append(rec) + + unique_recommendations = weight_filtered_recommendations + + # component_code, product_idx ์ œ๊ฑฐ + for rec in unique_recommendations: + rec.pop('component_code', None) + rec.pop('product_idx', None) + + return jsonify({ + 'success': True, + 'animal_type': animal_type, + 'category_name': category_name, + 'recommendations': unique_recommendations, + 'total_count': len(unique_recommendations), + 'weight_kg': weight_kg, + 'pregnancy_status': pregnancy # ์ž„์‹ /์ˆ˜์œ  ์ƒํƒœ ๋ฐ˜ํ™˜ + }) + + except Exception as e: + print(f"์ œํ’ˆ๊ตฐ ์ถ”์ฒœ ์˜ค๋ฅ˜: {e}") + import traceback + traceback.print_exc() + return jsonify({ + 'success': False, + 'message': f'์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}' + }) + + +# ============================================================ +# ํ†ตํ•ฉ ์ถ”์ฒœ API (v2) - APDB + ๋ณด์กฐ์ œํ’ˆ +# ============================================================ + +def get_supplementary_image(supp): + """๋ณด์กฐ์ œํ’ˆ ์ด๋ฏธ์ง€ URL ๋ฐ˜ํ™˜""" + return supp.image_url or '/static/img/no-image.png' + + +def has_supplementary_stock(supplementary_id): + """๋ณด์กฐ์ œํ’ˆ ์žฌ๊ณ  ํ™•์ธ""" + inv = session.query(InventorySupplementary).filter( + InventorySupplementary.supplementary_id == supplementary_id, + InventorySupplementary.quantity > 0 + ).first() + return inv is not None + + +@app.route('/api/product//recommendations') +def get_product_recommendations(product_idx): + """ + ํŠน์ • APDB ์ œํ’ˆ์˜ ํ†ตํ•ฉ ์ถ”์ฒœ ๋ชฉ๋ก ์กฐํšŒ + - ๊ด€๋ จ APDB ์ œํ’ˆ (์•ฝํ’ˆ โ†’ ์•ฝํ’ˆ) + - ๊ด€๋ จ ๋ณด์กฐ์ œํ’ˆ (์•ฝํ’ˆ โ†’ ๊ฑด๊ธฐ์‹/๊ธฐ๊ตฌ/์ผ€์–ด) + """ + try: + result = { + "success": True, + "product_idx": product_idx, + "apdb_recommendations": [], + "supplementary_recommendations": [] + } + + # 1. APDB โ†’ APDB ์ถ”์ฒœ + apdb_recs = session.query(UnifiedProductRecommendation).filter( + UnifiedProductRecommendation.source_type == 'apdb', + UnifiedProductRecommendation.source_id == product_idx, + UnifiedProductRecommendation.target_type == 'apdb', + UnifiedProductRecommendation.is_active == True + ).order_by(UnifiedProductRecommendation.priority.desc()).all() + + for rec in apdb_recs: + product = session.query(APDB).filter(APDB.idx == rec.target_id).first() + if not product: + continue + + # ์žฌ๊ณ  ํ™•์ธ + has_stock = session.query(Inventory).filter( + Inventory.apdb_id == product.idx, + Inventory.transaction_type == 'INBOUND' + ).first() is not None + + if has_stock: + result["apdb_recommendations"].append({ + "idx": product.idx, + "name": product.product_name, + "image_url": get_product_image(product), + "relation_type": rec.relation_type, + "reason": rec.recommendation_reason, + "priority": rec.priority + }) + + # 2. APDB โ†’ Supplementary ์ถ”์ฒœ + supp_recs = session.query(UnifiedProductRecommendation).filter( + UnifiedProductRecommendation.source_type == 'apdb', + UnifiedProductRecommendation.source_id == product_idx, + UnifiedProductRecommendation.target_type == 'supplementary', + UnifiedProductRecommendation.is_active == True + ).order_by(UnifiedProductRecommendation.priority.desc()).all() + + for rec in supp_recs: + supp = session.query(SupplementaryProduct).filter( + SupplementaryProduct.id == rec.target_id, + SupplementaryProduct.is_active == True + ).first() + + if supp and has_supplementary_stock(supp.id): + result["supplementary_recommendations"].append({ + "id": supp.id, + "name": supp.product_name, + "brand": supp.brand, + "category": supp.category, + "sub_category": supp.sub_category, + "image_url": get_supplementary_image(supp), + "relation_type": rec.relation_type, + "reason": rec.recommendation_reason, + "priority": rec.priority, + "price": float(supp.price) if supp.price else None, + "efficacy_tags": supp.efficacy_tags + }) + + result["total_apdb"] = len(result["apdb_recommendations"]) + result["total_supplementary"] = len(result["supplementary_recommendations"]) + + return jsonify(result) + + except Exception as e: + print(f"ํ†ตํ•ฉ ์ถ”์ฒœ ์กฐํšŒ ์˜ค๋ฅ˜: {e}") + import traceback + traceback.print_exc() + return jsonify({ + 'success': False, + 'message': f'์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}' + }) + + +@app.route('/api/supplementary/') +def get_supplementary_detail(supp_id): + """๋ณด์กฐ์ œํ’ˆ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ""" + try: + supp = session.query(SupplementaryProduct).filter( + SupplementaryProduct.id == supp_id + ).first() + + if not supp: + return jsonify({ + 'success': False, + 'message': '์ œํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.' + }) + + # ์žฌ๊ณ  ์ •๋ณด + inv = session.query(InventorySupplementary).filter( + InventorySupplementary.supplementary_id == supp_id + ).first() + + return jsonify({ + 'success': True, + 'product': { + 'id': supp.id, + 'product_name': supp.product_name, + 'product_name_en': supp.product_name_en, + 'brand': supp.brand, + 'manufacturer': supp.manufacturer, + 'barcode': supp.barcode, + 'product_code': supp.product_code, + 'category': supp.category, + 'sub_category': supp.sub_category, + 'description': supp.description, + 'usage_instructions': supp.usage_instructions, + 'ingredients': supp.ingredients, + 'main_ingredient': supp.main_ingredient, + 'ingredient_details': supp.ingredient_details, + 'efficacy': supp.efficacy, + 'efficacy_tags': supp.efficacy_tags, + 'target_animal': supp.target_animal, + 'target_age': supp.target_age, + 'target_size': supp.target_size, + 'precautions': supp.precautions, + 'warnings': supp.warnings, + 'image_url': supp.image_url, + 'image_url_2': supp.image_url_2, + 'price': float(supp.price) if supp.price else None, + 'unit': supp.unit, + 'package_size': supp.package_size, + 'external_url': supp.external_url, + 'stock': inv.quantity if inv else 0 + } + }) + + except Exception as e: + print(f"๋ณด์กฐ์ œํ’ˆ ์ƒ์„ธ ์กฐํšŒ ์˜ค๋ฅ˜: {e}") + return jsonify({ + 'success': False, + 'message': f'์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}' + }) + + +@app.route('/api/supplementary') +def list_supplementary_products(): + """๋ณด์กฐ์ œํ’ˆ ๋ชฉ๋ก ์กฐํšŒ (์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ํ•„ํ„ฐ๋ง)""" + try: + category = request.args.get('category') # supplement, equipment, care + sub_category = request.args.get('sub_category') + target_animal = request.args.get('target_animal') + + query = session.query(SupplementaryProduct).filter( + SupplementaryProduct.is_active == True + ) + + if category: + query = query.filter(SupplementaryProduct.category == category) + if sub_category: + query = query.filter(SupplementaryProduct.sub_category == sub_category) + if target_animal: + query = query.filter( + or_( + SupplementaryProduct.target_animal == target_animal, + SupplementaryProduct.target_animal == 'all' + ) + ) + + products = query.order_by(SupplementaryProduct.product_name).all() + + result = [] + for supp in products: + if has_supplementary_stock(supp.id): + result.append({ + 'id': supp.id, + 'product_name': supp.product_name, + 'brand': supp.brand, + 'category': supp.category, + 'sub_category': supp.sub_category, + 'main_ingredient': supp.main_ingredient, + 'efficacy_tags': supp.efficacy_tags, + 'target_animal': supp.target_animal, + 'image_url': get_supplementary_image(supp), + 'price': float(supp.price) if supp.price else None, + 'package_size': supp.package_size + }) + + return jsonify({ + 'success': True, + 'products': result, + 'total': len(result) + }) + + except Exception as e: + print(f"๋ณด์กฐ์ œํ’ˆ ๋ชฉ๋ก ์กฐํšŒ ์˜ค๋ฅ˜: {e}") + return jsonify({ + 'success': False, + 'message': f'์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}' + }) + + +@app.route('/api/symptoms/supplementary-recommendations', methods=['POST']) +def get_symptom_supplementary_recommendations(): + """์ฆ์ƒ ์„ ํƒ ์‹œ ๋ณด์กฐ์ œํ’ˆ ์ถ”์ฒœ""" + try: + data = request.get_json() + symptom_codes = data.get('symptom_codes', []) + animal_type = data.get('animal_type') # dog, cat + + if not symptom_codes: + return jsonify({ + 'success': False, + 'message': '์ฆ์ƒ์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”.' + }) + + # ์ฆ์ƒ โ†’ ๋ณด์กฐ์ œํ’ˆ ๋งคํ•‘ ์กฐํšŒ + recommendations = session.query(SymptomSupplementaryRecommendation).filter( + SymptomSupplementaryRecommendation.symptom_code.in_(symptom_codes), + SymptomSupplementaryRecommendation.is_active == True + ).order_by(SymptomSupplementaryRecommendation.priority.desc()).all() + + result = [] + seen_ids = set() + + for rec in recommendations: + if rec.supplementary_id in seen_ids: + continue + + supp = session.query(SupplementaryProduct).filter( + SupplementaryProduct.id == rec.supplementary_id, + SupplementaryProduct.is_active == True + ).first() + + if not supp: + continue + + # ๋™๋ฌผ ํƒ€์ž… ํ•„ํ„ฐ๋ง + if animal_type and supp.target_animal: + if supp.target_animal != 'all' and supp.target_animal != animal_type: + continue + + # ์žฌ๊ณ  ํ™•์ธ + if not has_supplementary_stock(supp.id): + continue + + seen_ids.add(rec.supplementary_id) + + # ๋งค์นญ๋œ ์ฆ์ƒ ์ •๋ณด ์กฐํšŒ + symptom = session.query(Symptoms).filter( + Symptoms.symptom_code == rec.symptom_code + ).first() + + result.append({ + 'id': supp.id, + 'product_name': supp.product_name, + 'brand': supp.brand, + 'category': supp.category, + 'sub_category': supp.sub_category, + 'main_ingredient': supp.main_ingredient, + 'efficacy_tags': supp.efficacy_tags, + 'image_url': get_supplementary_image(supp), + 'price': float(supp.price) if supp.price else None, + 'relation_type': rec.relation_type, + 'reason': rec.recommendation_reason, + 'matched_symptom': symptom.description if symptom else rec.symptom_code + }) + + return jsonify({ + 'success': True, + 'supplementary_recommendations': result, + 'total': len(result) + }) + + except Exception as e: + print(f"์ฆ์ƒ ๊ธฐ๋ฐ˜ ๋ณด์กฐ์ œํ’ˆ ์ถ”์ฒœ ์˜ค๋ฅ˜: {e}") + import traceback + traceback.print_exc() + return jsonify({ + 'success': False, + 'message': f'์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}' + }) + + +# ============================================================ +# ์‹คํ–‰ +# ============================================================ + +if __name__ == '__main__': + print("=" * 60) + print("๐Ÿพ ๋™๋ฌผ์•ฝ ์ถ”์ฒœ MVP ์›น์•ฑ") + print("=" * 60) + print("์ ‘์† ์ฃผ์†Œ: http://localhost:7001") + print("์ข…๋ฃŒ: Ctrl+C") + print("=" * 60) + + app.run(host='0.0.0.0', port=7001, debug=True) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ca3ccc8 --- /dev/null +++ b/requirements.txt @@ -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