pharmacy-pos-qr-system/docs/ARCHITECTURE.md
thug0bin 513c082cc6 docs: PS_sub_pharm 테이블 및 PS_Type 대체조제 구분 문서화
- PS_sub_pharm 테이블 컬럼 설명
- PS_Type 값별 의미 (0,1=일반, 4=대체실제, 9=대체원본)
- 대체조제 데이터 패턴 (4→9 순서)
- 쿼리 예시 추가
2026-03-05 13:45:03 +09:00

605 lines
25 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 🏗️ 약국 통합 솔루션 아키텍처
## 📋 개요
본 시스템은 **동물약 도매상(애니팜)**, **개별 약국 POS**, **마일리지 솔루션**을 통합하는 멀티 데이터베이스 아키텍처입니다.
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 🏢 애니팜 (동물약 도매상) │
│ PostgreSQL Database │
│ 제품 마스터, 재고, 주문, 거래처 │
└─────────────────────────────────────────────────────────────────────────────┘
│ 제품 정보 / 발주
┌─────────────────────────────────────────────────────────────────────────────┐
│ 💊 개별 약국 (청춘약국 등) │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ MSSQL (팜IT3000) │ │ SQLite (솔루션) │ │
│ │ - 제품 마스터 │ │ - 마일리지 │ │
│ │ - 판매 내역 │◄──►│ - AI 추천 │ │
│ │ - 조제 이력 │ │ - 알림톡 로그 │ │
│ │ - 회원 정보 │ │ - 동물약 태그 │ │
│ └──────────────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│ API / 웹 인터페이스
┌─────────────────────────────────────────────────────────────────────────────┐
│ 🌐 Flask 웹 서버 (7001) │
│ QR 적립 | AI 챗봇 | 관리자 | 회원 조회 | 알림톡 │
└─────────────────────────────────────────────────────────────────────────────┘
│ 외부 서비스
┌─────────────────────────────────────────────────────────────────────────────┐
│ 🔌 외부 API 연동 │
│ - OpenAI GPT (동물약 챗봇, AI 업셀링) │
│ - 카카오 OAuth (로그인) │
│ - NHN Cloud 알림톡 │
│ - Clawdbot Gateway (AI 에이전트) │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## 🗄️ 데이터베이스 구조
### 1⃣ PostgreSQL (애니팜 - 동물약 도매상)
> **역할**: 동물약 도매 사업의 핵심 DB. 제품 마스터, 거래처(약국), 주문/발주 관리
| 테이블 | 설명 | 주요 컬럼 |
|--------|------|-----------|
| `products` | 제품 마스터 | id, name, barcode, price, category |
| `customers` | 거래처 (약국) | id, pharmacy_name, owner, phone |
| `orders` | 주문 내역 | id, customer_id, order_date, status |
| `order_items` | 주문 상세 | order_id, product_id, qty, price |
| `inventory` | 재고 현황 | product_id, stock_qty, location |
```sql
-- 예시: 인기 동물약 TOP 10 조회
SELECT p.name, SUM(oi.qty) as total_sold
FROM order_items oi
JOIN products p ON oi.product_id = p.id
WHERE oi.created_at >= NOW() - INTERVAL '30 days'
GROUP BY p.name
ORDER BY total_sold DESC
LIMIT 10;
```
---
### 2⃣ MSSQL (팜IT3000 - 약국 POS)
> **역할**: 약국 청구/POS 프로그램의 DB. 제품, 판매, 조제, 회원 정보
#### 주요 데이터베이스
| DB명 | 설명 |
|------|------|
| `PM_DRUG` | 제품 마스터 (의약품/건기식) |
| `PM_PRES` | 판매/조제 내역 |
| `PM_BASE` | 회원/거래처 기본 정보 |
#### 핵심 테이블
**PM_DRUG.dbo.CD_GOODS** - 제품 마스터
| 컬럼 | 설명 |
|------|------|
| `DrugCode` | 제품 코드 (PK) |
| `GoodsName` | 제품명 |
| `BARCODE` | 바코드 |
| `Saleprice` | 판매가 |
| `Price` | 원가 |
| `POS_BOON` | 분류코드 (010103 = 동물약) |
| `GoodsSelCode` | 판매상태 (B = 판매중) |
**PM_PRES.dbo.SALE_MAIN** - 판매 헤더
| 컬럼 | 설명 |
|------|------|
| `SL_NO_order` | 거래번호 (PK) |
| `InsertTime` | 거래 일시 |
| `SL_MY_total` | 총 금액 |
| `SL_CD_custom` | 고객 코드 |
**PM_PRES.dbo.SALE_SUB** - 판매 상세
| 컬럼 | 설명 |
|------|------|
| `SL_NO_order` | 거래번호 (FK) |
| `DrugCode` | 제품 코드 |
| `SL_NM_item` | 수량 |
| `SL_TOTAL_PRICE` | 금액 |
**PM_BASE.dbo.CD_PERSON** - 회원 정보
| 컬럼 | 설명 |
|------|------|
| `CUSCODE` | 고객 코드 (PK) |
| `PANAME` | 이름 |
| `PHONE` | 전화번호 |
| `PANUM` | 주민번호 |
**PM_PRES.dbo.PS_sub_pharm** - 조제 약품 상세 ⭐
| 컬럼 | 설명 |
|------|------|
| `PreSerial` | 처방번호 (FK) |
| `SUB_SERIAL` | 약품 순번 |
| `DrugCode` | 제품 코드 |
| `Days` | 복용일수 |
| `QUAN` | 1회 복용량 |
| `QUAN_TIME` | 1일 복용횟수 |
| `INV_QUAN` | 총 투약량 |
| `PS_Type` | **조제 유형** (아래 참고) |
#### PS_Type 값 (대체조제 구분) ⭐
| PS_Type | 의미 | 표시 |
|---------|------|------|
| **0** | 일반 처방 (급여) | ✅ 표시 |
| **1** | 일반 처방 (비급여) | ✅ 표시 |
| **4** | 대체조제 - **실제 조제약** | ✅ 표시 + `대)` 배지 |
| **9** | 대체조제 - **원본 처방약** | ❌ 숨김 |
**대체조제 데이터 패턴:**
```
SUB_SERIAL 순서로 4(실제) → 9(원본) 쌍으로 저장됨
예시 (김현지 처방):
PS_Type=4 | 사이톱신정 ← 실제 조제 (표시)
PS_Type=9 | 씨프러스정 ← 원본 처방 (숨김, 사이톱신의 원처방)
PS_Type=4 | 티로파정 ← 실제 조제 (표시)
PS_Type=9 | 티램정 ← 원본 처방 (숨김, 티로파의 원처방)
```
**쿼리 예시:**
```sql
-- 실제 조제약만 조회 (대체조제 원본 제외)
SELECT * FROM PS_sub_pharm WHERE PreSerial = '처방번호' AND PS_Type != '9'
-- 대체조제 쌍 확인
SELECT
s1.DrugCode AS 실제조제,
s2.DrugCode AS 원본처방
FROM PS_sub_pharm s1
JOIN PS_sub_pharm s2 ON s1.PreSerial = s2.PreSerial
AND s1.SUB_SERIAL + 1 = s2.SUB_SERIAL
WHERE s1.PS_Type = '4' AND s2.PS_Type = '9'
```
```sql
-- 예시: 오늘 판매 내역 + 제품명 조회
SELECT
M.SL_NO_order AS 거래번호,
M.InsertTime AS 거래일시,
G.GoodsName AS 제품명,
S.SL_NM_item AS 수량,
S.SL_TOTAL_PRICE AS 금액
FROM PM_PRES.dbo.SALE_MAIN M
JOIN PM_PRES.dbo.SALE_SUB S ON M.SL_NO_order = S.SL_NO_order
JOIN PM_DRUG.dbo.CD_GOODS G ON S.DrugCode = G.DrugCode
WHERE CONVERT(DATE, M.InsertTime) = CONVERT(DATE, GETDATE())
ORDER BY M.InsertTime DESC;
```
```sql
-- 예시: 동물약 목록 조회 (POS_BOON = '010103')
SELECT DrugCode, GoodsName, Saleprice, BARCODE
FROM PM_DRUG.dbo.CD_GOODS
WHERE POS_BOON = '010103' AND GoodsSelCode = 'B'
ORDER BY GoodsName;
```
---
### 3⃣ SQLite (마일리지 솔루션)
> **역할**: 약국별 마일리지 적립, AI 추천, 알림톡 로그 등 부가 기능
**경로**: `backend/db/mileage.db`
#### 핵심 테이블
**users** - 마일리지 회원
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `id` | INTEGER | PK |
| `nickname` | TEXT | 이름 |
| `phone` | TEXT | 전화번호 (UNIQUE) |
| `mileage_balance` | INTEGER | 포인트 잔액 |
| `birthday` | TEXT | 생년월일 |
| `created_at` | TIMESTAMP | 가입일 |
**claim_tokens** - QR 적립 토큰
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `id` | INTEGER | PK |
| `transaction_id` | TEXT | POS 거래번호 (UNIQUE) |
| `token_hash` | TEXT | 토큰 해시 |
| `total_amount` | REAL | 구매 금액 |
| `claimable_points` | INTEGER | 적립 가능 포인트 |
| `claimed_at` | TIMESTAMP | 적립 완료 시각 |
| `claimed_by_user_id` | INTEGER | 적립한 회원 ID |
**mileage_ledger** - 포인트 원장
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `id` | INTEGER | PK |
| `user_id` | INTEGER | 회원 ID |
| `transaction_id` | TEXT | 거래번호 |
| `points` | INTEGER | 적립/차감 포인트 |
| `balance_after` | INTEGER | 변동 후 잔액 |
| `reason` | TEXT | CLAIM / USE / ADMIN |
**ai_recommendations** - AI 업셀링 추천
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `id` | INTEGER | PK |
| `user_id` | INTEGER | 회원 ID |
| `recommended_product` | TEXT | 추천 제품 |
| `recommendation_message` | TEXT | 추천 메시지 |
| `status` | TEXT | active / interested / dismissed |
**drug_tags** - 동물약 태그 (별도 DB: `drug_tags.db`)
| 컬럼 | 타입 | 설명 |
|------|------|------|
| `drug_code` | TEXT | 제품 코드 |
| `drug_name` | TEXT | 제품명 |
| `tag_type` | TEXT | animal_drug 등 |
| `tag_value` | TEXT | all / dog / cat |
```sql
-- 예시: 회원별 적립 내역 조회
SELECT
u.nickname, u.phone, u.mileage_balance,
ml.points, ml.reason, ml.created_at
FROM users u
JOIN mileage_ledger ml ON u.id = ml.user_id
WHERE u.phone = '01012345678'
ORDER BY ml.created_at DESC;
```
---
## 🔄 데이터 흐름 예시
### 📱 시나리오 1: QR 마일리지 적립
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ POS 결제 │────►│ QR 발행 │────►│ 고객 스캔 │────►│ 적립 완료 │
│ (MSSQL) │ │ (SQLite) │ │ (Flask) │ │ (SQLite) │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│ │ │ │
│ │ │ │
▼ ▼ ▼ ▼
SALE_MAIN claim_tokens users 조회 mileage_ledger
SALE_SUB 생성 & 저장 /생성 적립 기록
```
**쿼리 흐름:**
```sql
-- 1. POS 판매 완료 시 (MSSQL)
INSERT INTO SALE_MAIN (SL_NO_order, SL_MY_total, ...) VALUES (...)
-- 2. QR 토큰 생성 (SQLite)
INSERT INTO claim_tokens (transaction_id, total_amount, claimable_points, ...)
VALUES ('20260228001234', 50000, 1500, ...)
-- 3. 고객 QR 스캔 → 회원 조회/생성 (SQLite)
SELECT * FROM users WHERE phone = '01012345678'
-- 없으면:
INSERT INTO users (nickname, phone, mileage_balance) VALUES ('홍길동', '01012345678', 0)
-- 4. 적립 처리 (SQLite)
UPDATE users SET mileage_balance = mileage_balance + 1500 WHERE id = 1
INSERT INTO mileage_ledger (user_id, transaction_id, points, balance_after, reason)
VALUES (1, '20260228001234', 1500, 1500, 'CLAIM')
-- 5. 토큰 사용 완료 표시 (SQLite)
UPDATE claim_tokens SET claimed_at = datetime('now'), claimed_by_user_id = 1
WHERE transaction_id = '20260228001234'
```
---
### 🐾 시나리오 2: 동물약 AI 챗봇
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 사용자 질문 │────►│ 동물약 조회 │────►│ OpenAI API │────►│ 응답 생성 │
│ "구충제 추천" │ │ (MSSQL) │ │ (RAG) │ │ + 제품 매칭 │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│ │
│ │
▼ ▼
CD_GOODS에서 지식 베이스 +
동물약 38개 제품 목록 전달
가격 포함 조회
```
**쿼리 흐름:**
```sql
-- 1. 동물약 목록 조회 (MSSQL → RAG 컨텍스트)
SELECT DrugCode, GoodsName, Saleprice, BARCODE
FROM PM_DRUG.dbo.CD_GOODS
WHERE POS_BOON = '010103' AND GoodsSelCode = 'B'
ORDER BY GoodsName;
-- 결과: 안텔민(5000원), 넥스가드L(84000원), ... 38개
-- 2. OpenAI API 호출 (Python)
# System Prompt에 포함:
# - 동물약 지식 (심장사상충, 구충제, 외부기생충 )
# - 현재 보유 제품 목록 + 가격
# User: "구충제 추천해줘"
# AI 응답: "구충제로는 **안텔민**을 추천드려요! 프라지콴텔+피란텔 성분으로..."
-- 3. 응답에서 제품명 매칭 (Python)
# AI 응답에 "안텔민" 포함 가격 5000 표시
```
---
### 👤 시나리오 3: 회원 상세 조회 (통합)
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 전화번호 │────►│ DB 3곳 │────►│ 통합 응답 │
│ 입력 │ │ 동시 조회 │ │ 반환 │
└─────────────┘ └─────────────┘ └─────────────┘
┌───────────────┼───────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ SQLite │ │ MSSQL │ │ MSSQL │
│ users │ │PM_BASE │ │PM_PRES │
│마일리지 │ │회원정보 │ │조제이력 │
└─────────┘ └─────────┘ └─────────┘
```
**쿼리 흐름:**
```sql
-- 1. 마일리지 회원 조회 (SQLite)
SELECT id, nickname, phone, mileage_balance, created_at
FROM users WHERE phone = '01012345678'
-- 2. 적립 이력 조회 (SQLite)
SELECT points, balance_after, reason, created_at, transaction_id
FROM mileage_ledger WHERE user_id = 1
ORDER BY created_at DESC LIMIT 50
-- 3. POS 고객 코드 조회 (MSSQL PM_BASE)
SELECT CUSCODE, PANAME FROM CD_PERSON
WHERE REPLACE(PHONE, '-', '') = '01012345678'
-- 4. 조제 이력 조회 (MSSQL PM_PRES)
SELECT P.PreSerial, P.Indate, P.Drname, P.OrderName
FROM PS_main P
WHERE P.CusCode = 'C00001234'
ORDER BY P.Indate DESC
-- 5. 구매 상세 조회 (MSSQL PM_PRES + PM_DRUG)
SELECT G.GoodsName, S.SL_NM_item, S.SL_TOTAL_PRICE
FROM SALE_SUB S
JOIN PM_DRUG.dbo.CD_GOODS G ON S.DrugCode = G.DrugCode
WHERE S.SL_NO_order = '20260228001234'
```
---
## 🛠️ 기술 스택
| 계층 | 기술 | 용도 |
|------|------|------|
| **Frontend** | HTML/CSS/JS | 관리자 페이지, 키오스크, 마이페이지 |
| **Backend** | Flask (Python) | REST API, 템플릿 렌더링 |
| **Database** | PostgreSQL | 애니팜 (도매상) |
| | MSSQL | 팜IT3000 (약국 POS) |
| | SQLite | 마일리지 솔루션 |
| **AI** | OpenAI GPT-4o-mini | 동물약 챗봇, 업셀링 추천 |
| **인증** | 카카오 OAuth | 소셜 로그인 |
| **알림** | NHN Cloud | 알림톡/SMS |
| **프로세스** | PM2 | 서버 관리 |
| **도메인** | Cloudflare | SSL, 프록시 |
---
## 📁 프로젝트 구조
```
pharmacy-pos-qr-system/
├── backend/
│ ├── app.py # Flask 메인 앱
│ ├── db/
│ │ ├── dbsetup.py # DB 연결 관리
│ │ ├── mileage.db # SQLite (마일리지)
│ │ └── drug_tags.db # SQLite (동물약 태그)
│ ├── templates/ # HTML 템플릿
│ │ ├── admin.html
│ │ ├── admin_products.html # 제품 검색 + AI 챗봇
│ │ ├── admin_members.html
│ │ ├── kiosk.html
│ │ └── my_page.html
│ ├── services/
│ │ ├── kakao_client.py # 카카오 OAuth
│ │ ├── nhn_alimtalk.py # 알림톡
│ │ └── clawdbot_client.py # AI 에이전트
│ ├── utils/
│ │ └── qr_token_generator.py
│ └── .env # 환경 변수
├── docs/
│ └── ARCHITECTURE.md # 이 문서
├── logs/
└── ecosystem.config.js # PM2 설정
```
---
## 🔐 환경 변수 (.env)
```env
# 카카오 OAuth
KAKAO_CLIENT_ID=xxx
KAKAO_CLIENT_SECRET=xxx
KAKAO_REDIRECT_URI=https://mile.0bin.in/claim/kakao/callback
# OpenAI API
OPENAI_API_KEY=sk-xxx
OPENAI_MODEL=gpt-4o-mini
# MSSQL 연결 (dbsetup.py에서 설정)
# SQLite 경로 (backend/db/)
```
---
## 📊 주요 API 엔드포인트
| 경로 | 메서드 | 설명 | DB |
|------|--------|------|-----|
| `/api/products` | GET | 제품 검색 | MSSQL |
| `/api/animal-chat` | POST | 동물약 AI 챗봇 | MSSQL + OpenAI |
| `/api/animal-drugs` | GET | 동물약 목록 | MSSQL |
| `/api/claim` | POST | 마일리지 적립 | SQLite |
| `/api/members/search` | GET | 회원 검색 | MSSQL |
| `/api/members/history/:phone` | GET | 회원 이력 통합 | 전체 |
| `/admin/user/:id` | GET | 회원 상세 (적립+구매+조제) | 전체 |
---
---
## 🤖 PAAI 시스템 (처방 AI 분석)
### 개요
**PAAI (Prescription AI Analysis)**는 처방 접수 시 자동으로 AI 분석을 수행하고,
분석 결과를 영수증 프린터로 출력하는 시스템입니다.
### 아키텍처
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ PAAI 시스템 흐름 │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ POS 접수 │────►│ PM_PRES_LOG │────►│ Trigger Module │
│ (처방입력) │ │ (MSSQL) │ │ (폴링 감지) │
└─────────────┘ └─────────────────┘ └─────────────────┘
┌───────────────────────────────┤
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ WebSocket 알림 │ │ PAAI 분석 요청 │
│ (ws://8765) │ │ Flask API │
└─────────────────┘ └─────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ 프론트엔드 │ │ Claude API │
│ pmr.html │ │ (분석 수행) │
└─────────────────┘ └─────────────────┘
│ │
│◄──────────────────────────────┤
│ analysis_completed 이벤트
┌─────────────────┐
│ 자동 인쇄 │
│ ESC/POS 프린터 │
└─────────────────┘
```
### 구성 요소
| 모듈 | 위치 | 역할 |
|------|------|------|
| **Trigger Module** | `prescription-trigger/prescription_trigger.py` | PM_PRES_LOG 폴링, 처방 감지, 분석 요청 |
| **WebSocket Server** | Trigger 내장 (port 8765) | 프론트엔드에 실시간 이벤트 전송 |
| **PAAI API** | `backend/pmr_api.py` | 분석 요청 처리, Claude API 호출, 결과 저장 |
| **프론트엔드** | `backend/templates/pmr.html` | 조제관리 UI, 자동인쇄 토글 |
| **프린터 모듈** | `backend/paai_printer.py` | ESC/POS 영수증 프린터 출력 |
### WebSocket 이벤트
| 이벤트 | 방향 | 설명 |
|--------|------|------|
| `prescription_detected` | Server → Client | 새 처방 감지됨 |
| `analysis_started` | Server → Client | AI 분석 시작 |
| `analysis_completed` | Server → Client | 분석 완료 (결과 포함) |
| `analysis_failed` | Server → Client | 분석 실패 (에러 포함) |
### 자동 인쇄 흐름
```javascript
// 1. WebSocket으로 analysis_completed 수신
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.event === 'analysis_completed') {
// 2. 자동인쇄 ON 상태면 인쇄
if (window.autoPrintEnabled) {
printPaaiResult(data.pre_serial, data.patient_name, data);
}
}
};
// 3. 인쇄 API 호출
POST /pmr/api/paai/print
{
"pre_serial": "20260305000099",
"patient_name": "홍길동",
"result": { "analysis": {...}, "kims_summary": {...} }
}
// 4. ESC/POS 프린터로 출력
```
### 중복 방지
```javascript
// window.printedSerials (Set) 으로 중복 인쇄 방지
if (window.printedSerials.has(preSerial)) {
console.log('[AutoPrint] 이미 인쇄됨, 스킵:', preSerial);
return;
}
window.printedSerials.add(preSerial); // 요청 전에 추가 (race condition 방지)
```
### 자동 재시도
| 시도 | 대기 시간 | 상태 |
|------|----------|------|
| 1회차 | - | 최초 시도 |
| 2회차 | 2초 | 첫 번째 재시도 |
| 3회차 | 4초 | 두 번째 재시도 |
| 실패 | - | `analysis_failed` 이벤트 발송 |
### 로그 파일
| 파일 | 위치 | 내용 |
|------|------|------|
| `print_history.log` | `backend/logs/` | 인쇄 성공/실패 기록 |
| `analysis_failures.log` | `prescription-trigger/logs/` | 분석 실패 상세 기록 |
| `paai_logs.db` | `backend/db/` | 분석 결과 SQLite 저장 |
### 관련 문서
- `docs/PAAI_AUTO_PRINT_TROUBLESHOOTING.md` - 자동인쇄 트러블슈팅 가이드
---
## 📝 버전 이력
| 날짜 | 버전 | 변경 내용 |
|------|------|----------|
| 2026-02-28 | 1.0 | 초기 아키텍처 문서 작성 |
| | | 동물약 AI 챗봇 추가 |
| | | 플로팅 챗봇 UI 구현 |
| 2026-03-05 | 1.1 | PAAI 시스템 아키텍처 추가 |
| | | 자동인쇄, WebSocket, 재시도 로직 |
---
*작성: Clawdbot AI | 청춘약국 통합 솔루션*