feat: 특이(참고)사항 조회/수정 기능 구현
- 사용자 상세 모달에 특이사항 표시 (생일 옆 칸)
- 인라인 수정 UI (수정 버튼 → textarea → 저장/취소)
- PUT /api/members/{cuscode}/cusetc API 추가
- CD_PERSON.CUSETC 직접 UPDATE
Docs: MEMBER_MEMO_SYSTEM.md 문서 추가
- DB 구조, API 명세, 구현 현황 정리
This commit is contained in:
parent
acf8e44aa5
commit
50825c597e
@ -1550,9 +1550,9 @@ def admin_user_detail(user_id):
|
||||
base_session = db_manager.get_session('PM_BASE')
|
||||
pres_session = db_manager.get_session('PM_PRES')
|
||||
|
||||
# 전화번호로 CUSCODE 조회
|
||||
# 전화번호로 CUSCODE 조회 (특이사항 CUSETC 포함)
|
||||
cuscode_query = text("""
|
||||
SELECT TOP 1 CUSCODE, PANAME
|
||||
SELECT TOP 1 CUSCODE, PANAME, CUSETC
|
||||
FROM CD_PERSON
|
||||
WHERE REPLACE(REPLACE(PHONE, '-', ''), ' ', '') = :phone
|
||||
OR REPLACE(REPLACE(TEL_NO, '-', ''), ' ', '') = :phone
|
||||
@ -1562,7 +1562,11 @@ def admin_user_detail(user_id):
|
||||
|
||||
if cus_row:
|
||||
cuscode = cus_row.CUSCODE
|
||||
pos_customer = {'cuscode': cuscode, 'name': cus_row.PANAME}
|
||||
pos_customer = {
|
||||
'cuscode': cuscode,
|
||||
'name': cus_row.PANAME,
|
||||
'cusetc': cus_row.CUSETC or '' # 특이(참고)사항
|
||||
}
|
||||
|
||||
# 조제 이력 조회
|
||||
rx_query = text("""
|
||||
@ -3889,6 +3893,42 @@ def api_member_detail(cuscode):
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/members/<cuscode>/cusetc', methods=['PUT'])
|
||||
def api_update_cusetc(cuscode):
|
||||
"""특이(참고)사항 수정 API"""
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
new_cusetc = data.get('cusetc', '').strip()
|
||||
|
||||
# 길이 제한 (2000자)
|
||||
if len(new_cusetc) > 2000:
|
||||
return jsonify({'success': False, 'error': '특이사항은 2000자를 초과할 수 없습니다.'}), 400
|
||||
|
||||
base_session = db_manager.get_session('PM_BASE')
|
||||
|
||||
# CUSETC 업데이트
|
||||
update_query = text("""
|
||||
UPDATE CD_PERSON
|
||||
SET CUSETC = :cusetc
|
||||
WHERE CUSCODE = :cuscode
|
||||
""")
|
||||
result = base_session.execute(update_query, {'cusetc': new_cusetc, 'cuscode': cuscode})
|
||||
base_session.commit()
|
||||
|
||||
if result.rowcount == 0:
|
||||
return jsonify({'success': False, 'error': '해당 고객을 찾을 수 없습니다.'}), 404
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '특이사항이 저장되었습니다.',
|
||||
'cusetc': new_cusetc
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"특이사항 수정 오류: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/members/history/<phone>')
|
||||
def api_member_history(phone):
|
||||
"""
|
||||
|
||||
@ -886,6 +886,63 @@
|
||||
function closeUserModal() {
|
||||
document.getElementById('userDetailModal').style.display = 'none';
|
||||
}
|
||||
|
||||
// 특이사항 펼치기/접기 (클릭 시)
|
||||
function toggleCusetc(el) {
|
||||
if (el.style.maxHeight === 'none' || el.style.maxHeight === '') {
|
||||
el.style.maxHeight = '40px';
|
||||
el.style.overflow = 'hidden';
|
||||
} else {
|
||||
el.style.maxHeight = 'none';
|
||||
el.style.overflow = 'visible';
|
||||
}
|
||||
}
|
||||
|
||||
// 특이사항 수정 모드
|
||||
function editCusetc(cuscode, btn) {
|
||||
document.getElementById('cusetc-view').style.display = 'none';
|
||||
document.getElementById('cusetc-edit').style.display = 'block';
|
||||
document.getElementById('cusetc-textarea').focus();
|
||||
btn.style.display = 'none';
|
||||
}
|
||||
|
||||
// 특이사항 저장
|
||||
async function saveCusetc(cuscode) {
|
||||
const textarea = document.getElementById('cusetc-textarea');
|
||||
const newValue = textarea.value.trim();
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/members/${cuscode}/cusetc`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ cusetc: newValue })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
// 뷰 업데이트
|
||||
const viewEl = document.getElementById('cusetc-view');
|
||||
viewEl.innerHTML = newValue || '<span style="color: #9ca3af; font-weight: normal;">없음</span>';
|
||||
viewEl.style.maxHeight = newValue.length > 30 ? '40px' : 'none';
|
||||
|
||||
cancelCusetc();
|
||||
alert('✅ 저장되었습니다.');
|
||||
} else {
|
||||
alert('❌ ' + (data.error || '저장 실패'));
|
||||
}
|
||||
} catch (err) {
|
||||
alert('❌ 오류: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 특이사항 수정 취소
|
||||
function cancelCusetc() {
|
||||
document.getElementById('cusetc-view').style.display = 'block';
|
||||
document.getElementById('cusetc-edit').style.display = 'none';
|
||||
// 수정 버튼 다시 표시
|
||||
const editBtn = document.querySelector('#cusetc-view').parentElement.querySelector('button');
|
||||
if (editBtn) editBtn.style.display = 'inline-block';
|
||||
}
|
||||
|
||||
function renderUserDetail(data) {
|
||||
// 전역 변수에 데이터 저장
|
||||
@ -924,6 +981,25 @@
|
||||
<div style="color: #ec4899; font-size: 16px; font-weight: 600;">${user.birthday.includes('-') ? user.birthday.split('-')[0] + '월 ' + user.birthday.split('-')[1] + '일' : user.birthday.slice(0,2) + '월 ' + user.birthday.slice(2,4) + '일'}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
<!-- 특이(참고)사항 - 생일 옆 칸 -->
|
||||
${data.pos_customer ? `
|
||||
<div>
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;">
|
||||
<span style="color: #d97706; font-size: 13px;">⚠️ 특이사항</span>
|
||||
<button onclick="editCusetc('${data.pos_customer.cuscode}', this)" style="background: none; border: 1px solid #d97706; color: #d97706; font-size: 11px; padding: 2px 8px; border-radius: 4px; cursor: pointer;">✏️ 수정</button>
|
||||
</div>
|
||||
<div id="cusetc-view" onclick="toggleCusetc(this)" style="color: #92400e; font-size: 14px; font-weight: 500; cursor: ${(data.pos_customer.cusetc || '').length > 30 ? 'pointer' : 'default'}; ${(data.pos_customer.cusetc || '').length > 30 ? 'max-height: 40px; overflow: hidden;' : ''}" title="${(data.pos_customer.cusetc || '').length > 30 ? '클릭하여 펼치기' : ''}">
|
||||
${data.pos_customer.cusetc || '<span style="color: #9ca3af; font-weight: normal;">없음</span>'}
|
||||
</div>
|
||||
<div id="cusetc-edit" style="display: none;">
|
||||
<textarea id="cusetc-textarea" style="width: 100%; min-height: 60px; padding: 8px; border: 1px solid #d97706; border-radius: 6px; font-size: 13px; resize: vertical;">${data.pos_customer.cusetc || ''}</textarea>
|
||||
<div style="display: flex; gap: 6px; margin-top: 6px;">
|
||||
<button onclick="saveCusetc('${data.pos_customer.cuscode}')" style="background: #d97706; color: white; border: none; padding: 4px 12px; border-radius: 4px; font-size: 12px; cursor: pointer;">저장</button>
|
||||
<button onclick="cancelCusetc()" style="background: #e5e7eb; color: #374151; border: none; padding: 4px 12px; border-radius: 4px; font-size: 12px; cursor: pointer;">취소</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
<div style="text-align: right; display: flex; gap: 8px; justify-content: flex-end;">
|
||||
<button onclick="showAIAnalysisModal(${user.id})" style="padding: 10px 24px; background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%); color: white; border: none; border-radius: 10px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s;">
|
||||
|
||||
272
docs/MEMBER_MEMO_SYSTEM.md
Normal file
272
docs/MEMBER_MEMO_SYSTEM.md
Normal file
@ -0,0 +1,272 @@
|
||||
# 환자 메모/특이사항 시스템 설계 문서
|
||||
|
||||
## 📅 작성일: 2026-03-04
|
||||
|
||||
---
|
||||
|
||||
## 1. DB 접속 정보
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 서버 | `192.168.0.4\PM2014` |
|
||||
| 드라이버 | ODBC Driver 17 for SQL Server |
|
||||
| 인증 | Windows 인증 (Trusted_Connection) |
|
||||
| 데이터베이스 | PM_BASE (환자정보), PM_PRES (처방), PM_DRUG (약품) |
|
||||
|
||||
### 접속 코드 (pharmacy-pos-qr-system)
|
||||
```python
|
||||
from db.dbsetup import DatabaseManager
|
||||
db = DatabaseManager()
|
||||
session = db.get_session('PM_BASE')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 테이블 구조
|
||||
|
||||
### 2.1 CD_PERSON.CUSETC (특이참고사항)
|
||||
|
||||
**용도:** 단일 필드, 간단한 메모 (덮어쓰기 방식)
|
||||
|
||||
| 칼럼 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| CUSETC | VARCHAR(2000) | 특이/참고사항 텍스트 |
|
||||
|
||||
**특징:**
|
||||
- 한 환자당 하나의 값만 저장
|
||||
- 새로 입력하면 기존 값 덮어씀
|
||||
- 주로 미수금, 간단한 주의사항 등 기록
|
||||
|
||||
---
|
||||
|
||||
### 2.2 CD_PERSON_MEMO (메모 - 날짜별 누적)
|
||||
|
||||
**용도:** 별도 테이블, 상세 메모 이력 관리
|
||||
|
||||
| 칼럼 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| CUSCODE | VARCHAR(10) | 고객코드 (PK) |
|
||||
| MEMO_CODE | VARCHAR(5) | 메모코드 (PK) - 00001, 00002... |
|
||||
| PHARMA_ID | VARCHAR(10) | 작성자명 (약사 이름 직접 저장) |
|
||||
| MEMO_DATE | VARCHAR(8) | 작성일 (YYYYMMDD) |
|
||||
| MEMO_TITLE | VARCHAR(40) | 메모 제목 |
|
||||
| MEMO_Item | TEXT | 메모 내용 |
|
||||
|
||||
**특징:**
|
||||
- 한 환자당 여러 메모 가능 (날짜별 누적)
|
||||
- 복합 PK: CUSCODE + MEMO_CODE
|
||||
- 작성자/날짜 추적 가능
|
||||
|
||||
---
|
||||
|
||||
## 3. PHARMA_ID 분석
|
||||
|
||||
### 현재 저장된 값 (2026-03-04 기준)
|
||||
```
|
||||
[김영빈] - 2448건
|
||||
[박혜령] - 63건
|
||||
[이충섭] - 4건
|
||||
[시스템] - 2건
|
||||
[이수지] - 1건
|
||||
[지민구] - 1건
|
||||
[PHARM001] - 1건
|
||||
```
|
||||
|
||||
### 결론
|
||||
- **직접 이름 저장 방식** (마스터 테이블 조인 불필요)
|
||||
- 대부분 한글 이름, 일부 코드 형태 존재
|
||||
- 별도 약사 마스터 테이블 연결 없이 독립적으로 저장
|
||||
|
||||
---
|
||||
|
||||
## 4. 실제 데이터 예시
|
||||
|
||||
### 예시 1: 김미성 (0000014615)
|
||||
```
|
||||
[특이사항 - CD_PERSON.CUSETC]
|
||||
25/1 미수금:200
|
||||
|
||||
[메모 - CD_PERSON_MEMO]
|
||||
메모코드: 00001
|
||||
작성자: 김영빈
|
||||
날짜: 20260304
|
||||
제목: (없음)
|
||||
내용: 신장투석.이식 가족력
|
||||
```
|
||||
|
||||
### 예시 2: 박상호 (0000024142)
|
||||
```
|
||||
[특이사항 - CD_PERSON.CUSETC]
|
||||
25/1 미수금:1400
|
||||
|
||||
[메모 - CD_PERSON_MEMO]
|
||||
메모코드: 00003
|
||||
작성자: 김영빈
|
||||
날짜: 20260303
|
||||
제목: 가루약
|
||||
내용: 사미온만 아침,저녁
|
||||
나머지 저녁으로
|
||||
카나브는 알약으로 포장
|
||||
```
|
||||
|
||||
### 예시 3: 안동옥 (0000001030)
|
||||
```
|
||||
[특이사항 - CD_PERSON.CUSETC]
|
||||
25/1 미수금:200
|
||||
|
||||
[메모 - CD_PERSON_MEMO]
|
||||
메모코드: 00001
|
||||
작성자: 김영빈
|
||||
날짜: 20260224
|
||||
제목: (없음)
|
||||
내용: 26.2.23-에터미 해모임과 고지혀약 피타로우에프 먹은지 한달 만에
|
||||
간수피가 20대에서 120대로 수치가 오름.
|
||||
고덱스 처방과 약 끊고 변화 확인 요망(010-6209-0796)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 기존 API 현황
|
||||
|
||||
### GET /api/members/search?q={검색어}
|
||||
- 회원 검색 (이름 2자 이상, 전화번호)
|
||||
- 응답에 `memo` (CUSETC 100자 미리보기) 포함
|
||||
|
||||
### GET /api/members/{cuscode}
|
||||
- 회원 상세 조회
|
||||
- `member.memo`: CUSETC 전체
|
||||
- `memos[]`: CD_PERSON_MEMO 배열 (author, date, title, content)
|
||||
|
||||
---
|
||||
|
||||
## 6. 구현 계획
|
||||
|
||||
### 6.1 약사/직원 테이블 (신규 - SQLite)
|
||||
|
||||
**위치:** `backend/db/pharmacy_staff.db`
|
||||
|
||||
```sql
|
||||
CREATE TABLE staff (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(20) NOT NULL, -- 이름 (PHARMA_ID에 저장될 값)
|
||||
role VARCHAR(20), -- 역할 (약사, 직원 등)
|
||||
is_active BOOLEAN DEFAULT 1, -- 활성 여부
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 초기 데이터
|
||||
INSERT INTO staff (name, role) VALUES ('김영빈', '약사');
|
||||
INSERT INTO staff (name, role) VALUES ('박혜령', '약사');
|
||||
```
|
||||
|
||||
**용도:**
|
||||
- 메모 작성 시 드롭다운 목록 제공
|
||||
- 향후 로그인 시스템 확장 대비
|
||||
- 기본 작성자 설정 가능
|
||||
|
||||
### 6.2 UI 구현 방향
|
||||
|
||||
```
|
||||
[회원 상세 페이지]
|
||||
├── 기본 정보 (이름, 전화번호, 주민번호 등)
|
||||
├── 특이(참고)사항
|
||||
│ └── [단일 텍스트 영역] - 저장 시 덮어쓰기
|
||||
└── 메모 (날짜별 누적)
|
||||
├── [메모 목록] - 날짜순 정렬
|
||||
│ └── 각 메모: 날짜, 작성자, 제목, 내용 미리보기
|
||||
├── [새 메모 추가]
|
||||
│ ├── 작성자 드롭다운 (staff 테이블에서)
|
||||
│ ├── 제목 입력
|
||||
│ └── 내용 입력
|
||||
└── [메모 수정/삭제]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 현재 구현 현황 (2026-03-04)
|
||||
|
||||
### 7.1 특이(참고)사항 - 구현 완료 ✅
|
||||
|
||||
**위치:** https://mile.0bin.in/admin → 사용자 클릭 → 상세 모달
|
||||
|
||||
#### UI
|
||||
```
|
||||
┌──────────────────┬──────────────────┐
|
||||
│ 🎂 생일 │ ⚠️ 특이사항 [✏️ 수정]│
|
||||
│ 07월 12일 │ 개발약사2 │
|
||||
└──────────────────┴──────────────────┘
|
||||
```
|
||||
|
||||
- 생일 옆 칸에 표시 (공간 효율적 활용)
|
||||
- 30자 초과 시 truncate, 클릭하면 펼침
|
||||
- [✏️ 수정] 버튼 → 인라인 textarea → [저장] / [취소]
|
||||
|
||||
#### API
|
||||
|
||||
**조회:** `GET /admin/user/{userId}`
|
||||
```json
|
||||
{
|
||||
"pos_customer": {
|
||||
"cuscode": "0000000004",
|
||||
"name": "김영빈",
|
||||
"cusetc": "개발약사2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**수정:** `PUT /api/members/{cuscode}/cusetc`
|
||||
```json
|
||||
// Request
|
||||
{ "cusetc": "새로운 특이사항" }
|
||||
|
||||
// Response
|
||||
{
|
||||
"success": true,
|
||||
"message": "특이사항이 저장되었습니다.",
|
||||
"cusetc": "새로운 특이사항"
|
||||
}
|
||||
```
|
||||
|
||||
#### E2E 테스트 완료
|
||||
```bash
|
||||
# 검색
|
||||
GET /api/members/search?q=김영빈 → cuscode: 0000000004
|
||||
|
||||
# 수정
|
||||
PUT /api/members/0000000004/cusetc
|
||||
Body: { "cusetc": "개발약사2 - 테스트 수정" }
|
||||
→ 성공 ✅
|
||||
|
||||
# 확인
|
||||
GET /api/members/search?q=김영빈
|
||||
→ memo: "개발약사2 - 테스트 수정" ✅
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7.2 메모 (날짜별 누적) - 미구현 ⏳
|
||||
|
||||
**다음 단계:**
|
||||
1. staff 테이블 생성 (SQLite)
|
||||
2. 메모 CRUD API 구현
|
||||
3. UI 구현 (메모 목록, 추가, 수정, 삭제)
|
||||
|
||||
---
|
||||
|
||||
## 8. 관련 파일
|
||||
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `backend/app.py` | Flask API (3740행~ 회원 관련, CUSETC 수정 API 포함) |
|
||||
| `backend/db/dbsetup.py` | DB 연결 설정 |
|
||||
| `backend/templates/admin.html` | 어드민 대시보드 (사용자 상세 모달, 특이사항 UI) |
|
||||
| `backend/templates/admin_members.html` | 회원 관리 페이지 |
|
||||
| `person-lookup-web-local/models.py` | SQLAlchemy 모델 정의 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 참고사항
|
||||
|
||||
- PIT3000 원본 테이블은 직접 수정 (INSERT/UPDATE)
|
||||
- 마일리지 시스템(SQLite)과 별개로 MSSQL에 저장
|
||||
- CD_PERSON_MEMO는 복합키(CUSCODE + MEMO_CODE) 주의
|
||||
Loading…
Reference in New Issue
Block a user