feat: 알림톡 발송 로그 시스템 + 현영 표시 + 문서화

- 알림톡 발송 로그: alimtalk_logs SQLite 테이블 + DB 자동 기록
- /admin/alimtalk 페이지: 서버 로그, NHN Cloud 내역 조회, 수동 발송 테스트
- 적립일시 포맷 수정: %Y-%m-%d %H:%M (16자 초과) → %m/%d %H:%M (11자)
- POS GUI 현금영수증(현영) 표시: 청록색 볼드
- 결제수납구조.md: CD_SUNAB/PS_main/SALE_MAIN 3테이블 관계 문서
- 실행구조.md: Flask 서버 + Qt GUI 실행 가이드

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
thug0bin
2026-02-26 19:28:29 +09:00
parent 0c52542713
commit a3ff69b67f
10 changed files with 1117 additions and 82 deletions

View File

@@ -1,22 +1,217 @@
# PIT3000 제/수납/할인 데이터 구조
# PIT3000 판매/조제/수납 데이터 구조
## 핵심 테이블 관계
```
SALE_MAIN (판매)
└── SL_NO_order (PK, 주문번호)
CD_SUNAB (수납/결제) ─── 모든 거래의 결제 기록 (130건/일 기준)
├── PS_main (처방접수) ─── 조제 건만 (89건/일 기준)
│ │ 조인: PS_main.PreSerial = CD_SUNAB.PRESERIAL
│ │ 조인: PS_main.Indate = CD_SUNAB.INDATE
│ │
│ ├── PS_sub_hosp (처방 의약품 상세)
│ └── PS_sub_pharm (조제 의약품 상세)
└── SALE_MAIN (OTC 판매) ─── OTC 직접 판매만 (39건/일 기준)
│ 조인: SALE_MAIN.SL_NO_order = CD_SUNAB.PRESERIAL
── SALE_SUB (품목 상세) SL_NO_order로 조인
└── CD_SUNAB (수납/결제) — CD_SUNAB.PRESERIAL = SALE_MAIN.SL_NO_order
── SALE_SUB (판매 품목 상세) ─── SL_NO_order로 조인
```
**주의**: `CD_SUNAB.PRESERIAL``SALE_MAIN.SL_NO_order`(주문번호)와 매칭됨.
`SALE_MAIN.PRESERIAL`(처방번호)과는 다른 키임.
## 테이블별 역할
### 1. CD_SUNAB — 수납/결제 (모든 거래 포함)
- **역할**: 조제 + OTC 모든 거래의 결제/수납 기록
- **1주문 = 1행** (복수행 없음)
- **키**: `PRESERIAL` (주문번호), `INDATE` (수납일)
- **건수**: 하루 약 130건 (조제 91 + OTC 39)
| 컬럼 | 설명 |
|------|------|
| `PRESERIAL` | 주문번호 (PS_main.PreSerial 또는 SALE_MAIN.SL_NO_order와 매칭) |
| `INDATE` | 수납일 (YYYYMMDD) |
| `DAY_SERIAL` | 일련번호 |
| `CUSCODE` | 고객코드 |
| `ETC_CARD` | 조제 카드결제 금액 |
| `ETC_CASH` | 조제 현금결제 금액 |
| `ETC_PAPER` | 조제 외상 금액 |
| `OTC_CARD` | 일반약 카드결제 금액 |
| `OTC_CASH` | 일반약 현금결제 금액 |
| `OTC_PAPER` | 일반약 외상 금액 |
| `pAPPROVAL_NUM` | 카드 승인번호 |
| `pMCHDATA` | 카드사 이름 |
| `pCARDINMODE` | 카드 입력방식 (1=IC칩) |
| `pTRDTYPE` | 거래유형 (D1=일반승인) |
| `nCASHINMODE` | 현금영수증 모드 (1=발행, 2=카드거래 자동세팅) |
| `nAPPROVAL_NUM` | 현금영수증 승인번호 |
| `Appr_Gubun` | 승인구분 (1, 2, 9 등) |
| `APPR_DATE` | 승인일시 (YYYYMMDDHHmmss) |
| `DaeRiSunab` | 대리수납 여부 |
| `YOHUDATE` | 요후일 |
| 총 **54개 컬럼** | |
### 2. PS_main — 처방전 접수 (조제 전용)
- **역할**: 처방전 기반 조제 접수 기록
- **키**: `PreSerial` (처방번호 = CD_SUNAB.PRESERIAL)
- **건수**: 하루 약 89건
- **SALE_MAIN에는 없음** — 조제건은 SALE_MAIN을 거치지 않음
| 컬럼 | 설명 |
|------|------|
| `PreSerial` | 처방번호 (= CD_SUNAB.PRESERIAL) |
| `Day_Serial` | 일일 접수 순번 (1~89) |
| `Indate` | 접수일 (YYYYMMDD) |
| `CusCode` | 환자 코드 |
| `Paname` | 환자명 |
| `PaNum` | 주민번호 |
| `InsName` | 보험구분 (건강보험, 의료급여 등) |
| `OrderName` | 의료기관명 |
| `Drname` | 처방의사명 |
| `PresTime` | 접수 시간 |
| `PRICE_T` | 총금액 |
| `PRICE_P` | 본인부담금 |
| `PRICE_C` | 보험자부담금 |
| `Pre_State` | 처방 상태 |
| `InsertTime` | 입력 시간 |
| 총 **58개 컬럼** | |
### 3. SALE_MAIN — OTC 직접 판매
- **역할**: 일반의약품(OTC) 직접 판매 기록
- **키**: `SL_NO_order` (주문번호 = CD_SUNAB.PRESERIAL)
- **건수**: 하루 약 39건
- **조제건은 포함되지 않음**
| 컬럼 | 설명 |
|------|------|
| `SL_NO_order` | 주문번호 (= CD_SUNAB.PRESERIAL) |
| `SL_DT_appl` | 판매일 (YYYYMMDD) |
| `SL_NM_custom` | 고객명 (대부분 빈값 → `[비고객]`) |
| `SL_MY_total` | 원가 (할인 전) |
| `SL_MY_discount` | 할인 금액 |
| `SL_MY_sale` | 실판매가 (= total - discount) |
| `InsertTime` | 입력 시간 |
| `PRESERIAL` | 처방번호 (OTC는 'V' 고정, 의미 없음) |
| 총 **30개 컬럼** | |
---
## SALE_MAIN 금액 컬럼
## 데이터 흐름 정리
### 조제 (처방전 기반)
```
처방전 접수 → PS_main 생성 → 조제 → CD_SUNAB 수납 기록
(ETC_CARD/ETC_CASH에 금액)
```
- SALE_MAIN에는 **기록되지 않음**
- SALE_SUB에도 품목이 **들어가지 않음**
- 환자명은 PS_main.Paname에 있음
### OTC 판매 (직접 판매)
```
POS에서 품목 선택 → SALE_MAIN + SALE_SUB 생성 → CD_SUNAB 수납 기록
(OTC_CARD/OTC_CASH에 금액)
```
- PS_main에는 **기록되지 않음**
- 고객명은 보통 빈값 (`[비고객]`)
### 조제 + OTC 동시 (하루 약 10건)
```
처방전 조제 + 일반약 동시 구매
→ PS_main (조제 부분)
→ SALE_MAIN + SALE_SUB (OTC 부분)
→ CD_SUNAB 1행에 ETC + OTC 금액 모두 기록
```
---
## 조인 키 관계
```
CD_SUNAB.PRESERIAL = PS_main.PreSerial (조제건)
CD_SUNAB.PRESERIAL = SALE_MAIN.SL_NO_order (OTC건)
```
**주의**: `SALE_MAIN.PRESERIAL`은 OTC에서 항상 `'V'`로, 조인키가 아님.
실제 조인키는 `SALE_MAIN.SL_NO_order`임.
---
## 건수 관계 (2025-02-25 기준)
| 구분 | 건수 | 설명 |
|------|------|------|
| CD_SUNAB | 130 | 모든 수납 기록 |
| PS_main | 89 | 처방전 접수 (= 조제) |
| SALE_MAIN | 39 | OTC 직접 판매 |
| CD_SUNAB에만 존재 | 91 | 조제건 (SALE_MAIN 없음) |
| PS_main 매칭 | 89 | 91건 중 PS_main과 매칭 |
| 미매칭 | 2 | PS_main 없이 수납만 존재 (미수금 수납 등 특수 케이스) |
### 130건 = 39 (OTC) + 89 (조제) + 2 (특수)
---
## 조제/OTC 구분 방법
CD_SUNAB의 ETC/OTC 금액으로 판별:
```python
etc_total = ETC_CARD + ETC_CASH # 조제 금액
otc_total = OTC_CARD + OTC_CASH # 일반약 금액
if etc_total > 0 and otc_total > 0:
구분 = "조제+판매"
elif etc_total > 0:
구분 = "조제"
elif otc_total > 0:
구분 = "판매(OTC)"
else:
구분 = "본인부담금 없음" # 건강보험 전액 부담
```
---
## 결제수단 판별
```python
card_total = ETC_CARD + OTC_CARD
cash_total = ETC_CASH + OTC_CASH
# 현금영수증 판별 (nCASHINMODE=2는 카드거래 자동세팅이므로 제외)
has_cash_receipt = (nCASHINMODE == '1' and nAPPROVAL_NUM != '')
if card_total > 0 and cash_total > 0:
결제 = "카드+현금"
elif card_total > 0:
결제 = "카드"
elif cash_total > 0:
결제 = "현영" if has_cash_receipt else "현금"
else:
결제 = "-"
```
---
## GUI 표시 색상
### 결제 컬럼
- **카드**: 파란색 (#1976D2)
- **현영**: 청록색 볼드 (#00897B) — 현금영수증 발행
- **현금**: 주황색 (#E65100) — 현금영수증 미발행
- **카드+현금**: 보라색 (#7B1FA2)
- **-**: 회색 (수납 없음)
### 수납 컬럼
- **✓**: 녹색 (#4CAF50)
- **-**: 회색 (미수납)
### 할인 표시
- 할인 없음: `12,000원`
- 할인 있음: `54,000원 (-6,000)` 주황색 볼드 + 툴팁
---
## SALE_MAIN 금액 컬럼 상세
| 컬럼 | 설명 | 예시 |
|------|------|------|
@@ -40,30 +235,7 @@ SL_MY_recive ≈ SL_MY_sale / 1.1 (부가세 제외 금액 추정)
---
## CD_SUNAB 결제수단 컬럼
### 금액 기반 결제수단 구분
단일 구분 컬럼이 없음. **금액이 0보다 크면 해당 결제수단 사용**.
| 구분 | 카드 | 현금 | 외상 |
|------|------|------|------|
| 조제(ETC, 전문의약품) | `ETC_CARD` | `ETC_CASH` | `ETC_PAPER` |
| OTC(일반의약품) | `OTC_CARD` | `OTC_CASH` | `OTC_PAPER` |
### 결제수단 판별 로직
```python
card_total = ETC_CARD + OTC_CARD
cash_total = ETC_CASH + OTC_CASH
if card_total > 0 and cash_total > 0:
결제수단 = "카드+현금"
elif card_total > 0:
결제수단 = "카드"
elif cash_total > 0:
결제수단 = "현금"
else:
결제수단 = "-" (미수납 또는 외상)
```
## CD_SUNAB 카드/현금 상세 컬럼
### 카드 상세 정보
| 컬럼 | 설명 | 예시 |
@@ -79,32 +251,13 @@ else:
### 현금 상세 정보
| 컬럼 | 설명 | 예시 |
|------|------|------|
| `nCASHINMODE` | 현금영수증 입력 방식 | 1, 2 (빈값=미발행) |
| `nAPPROVAL_NUM` | 현금영수증 승인번호 | |
| `nCHK_GUBUN` | 현금 체크 구분 | TASA |
| `nCASHINMODE` | 현금영수증 입력 방식 | 1=실제발행, 2=카드거래 자동세팅 |
| `nAPPROVAL_NUM` | 현금영수증 승인번호 | 116624870 |
| `nCHK_GUBUN` | 현금 체크 구분 | KOV, TASA |
---
## GUI 표시 방식
### 결제 컬럼
- **카드**: 파란색 (#1976D2)
- **현금**: 주황색 (#E65100)
- **카드+현금**: 보라색 (#7B1FA2)
- **-**: 회색 (수납 정보 없음)
### 수납 컬럼
- **✓**: 녹색 (card + cash > 0)
- **-**: 회색 (미수납)
### 할인 표시
- 할인 없는 건: `12,000원` (기본)
- 할인 있는 건: `54,000원 (-6,000)` 주황색 볼드
- 마우스 툴팁: 원가 / 할인 / 결제 상세
---
## SQL 쿼리 (GUI에서 사용)
## SQL 쿼리 (현재 GUI에서 사용)
```sql
SELECT
@@ -115,12 +268,16 @@ SELECT
ISNULL(S.card_total, 0) AS card_total,
ISNULL(S.cash_total, 0) AS cash_total,
ISNULL(M.SL_MY_total, 0) AS total_amount,
ISNULL(M.SL_MY_discount, 0) AS discount
ISNULL(M.SL_MY_discount, 0) AS discount,
S.cash_receipt_mode,
S.cash_receipt_num
FROM SALE_MAIN M
OUTER APPLY (
SELECT TOP 1
ISNULL(ETC_CARD, 0) + ISNULL(OTC_CARD, 0) AS card_total,
ISNULL(ETC_CASH, 0) + ISNULL(OTC_CASH, 0) AS cash_total
ISNULL(ETC_CASH, 0) + ISNULL(OTC_CASH, 0) AS cash_total,
nCASHINMODE AS cash_receipt_mode,
nAPPROVAL_NUM AS cash_receipt_num
FROM CD_SUNAB
WHERE PRESERIAL = M.SL_NO_order
) S
@@ -128,6 +285,10 @@ WHERE M.SL_DT_appl = ?
ORDER BY M.InsertTime DESC
```
**한계**: SALE_MAIN 기준이므로 OTC 판매(39건)만 표시됨.
조제건(~89건)은 표시되지 않음. 조제건까지 보려면 CD_SUNAB을
기본 테이블로 사용하거나 PS_main과 조인하는 쿼리 재설계 필요.
---
## 카드사 분포 (전체 데이터 기준)

91
docs/실행구조.md Normal file
View File

@@ -0,0 +1,91 @@
# 청춘약국 마일리지 시스템 — 실행 구조
## 실행해야 할 프로그램 (2개)
### 1. Flask 서버 (`backend/app.py`)
```bash
cd c:\Users\청춘약국\source\pharmacy-pos-qr-system
python backend/app.py
```
- **포트**: 7001 (0.0.0.0)
- **외부 도메인**: `mile.0bin.in` (→ 내부 7001 포트로 프록시)
- **역할**: 웹 서비스 전체 담당
#### 제공하는 페이지/API
| 경로 | 설명 |
|------|------|
| `/` | 메인 페이지 |
| `/signup` | 회원가입 |
| `/claim` | QR 적립 (폰번호 방식) |
| `/claim/kakao/start` | QR 적립 (카카오 로그인) |
| `/my-page` | 마이페이지 |
| `/kiosk` | **키오스크 대기 화면** (약국 내 태블릿) |
| `/admin` | 관리자 페이지 |
| `/admin/transaction/<id>` | 거래 상세 |
| `/admin/user/<id>` | 회원 상세 |
| `/admin/search/user` | 회원 검색 |
| `/admin/search/product` | 상품 검색 |
| `/api/kiosk/trigger` | 키오스크 QR 트리거 (POST) |
| `/api/kiosk/current` | 키오스크 현재 상태 |
| `/api/kiosk/claim` | 키오스크 적립 처리 (POST) |
#### 사용하는 DB
- **SQLite** (`backend/db/mileage.db`) — 회원, 적립, QR 토큰
- **MSSQL** (`192.168.0.4\PM2014`, DB: `PM_PRES`) — POS 판매 데이터 (읽기 전용)
---
### 2. Qt POS GUI (`backend/gui/pos_sales_gui.py`)
```bash
cd c:\Users\청춘약국\source\pharmacy-pos-qr-system
python backend/gui/pos_sales_gui.py
```
- **역할**: POS 판매 내역 조회 + QR 라벨 발행
- **PyQt5 기반** 데스크톱 앱
- Flask 서버와 **독립적으로 실행** (별도 프로세스)
#### 주요 기능
- 일자별 판매 내역 조회 (SALE_MAIN + CD_SUNAB)
- 결제수단 표시 (카드/현금/현영)
- 할인 표시
- QR 라벨 프린터 출력 (Zebra / POS 프린터)
- 적립자 클릭 → 회원 적립 내역 팝업
#### 사용하는 DB
- **MSSQL** — SALE_MAIN, SALE_SUB, CD_SUNAB 조회
- **SQLite** — claim_tokens, users 조회 (적립 정보)
---
## 실행 순서
```
1. Flask 서버 먼저 실행 (키오스크, 웹 서비스 제공)
2. Qt POS GUI 실행 (판매 내역 조회, QR 발행)
```
순서는 상관없으나, Flask가 먼저 떠 있어야 키오스크(`mile.0bin.in/kiosk`)와
웹 서비스(`mile.0bin.in`)가 접속 가능.
---
## 프로세스 확인
```bash
# 실행 중인 Python 프로세스 확인
tasklist /FI "IMAGENAME eq python.exe"
# 정상 상태: Python 프로세스 3개
# - Flask 서버 (메인)
# - Flask 서버 (debug reloader 워커)
# - Qt POS GUI
```
---
## 주의사항
- `taskkill /F /IM python.exe` 사용 시 **Flask + GUI 모두 종료됨**
- GUI만 재시작하려면 해당 PID만 종료할 것
- Flask 서버는 `debug=True`로 실행되어 코드 변경 시 자동 리로드
- Python 경로: `C:\Users\청춘약국\AppData\Local\Programs\Python\Python312\python.exe`