글 작성자: Maumpharm
반응형

1. POS 프로그램을 사용하여 고객관리를 해야 하는 이유

1-1. 고객관리의 필요성

사업을 운영함에 있어서 모든 고객의 기록을 관리하는 것은 매우 중요합니다. 영리 사업인 경우 매출과 수익에도 중요한 영향을 끼칠 수 있습니다. 미용실이나 정육점을 운영한다고 가정해 봅시다. 단골 고객의 외적 취향이나, 선호하는 고기의 부위를 잘 기억해 놓는다면 손님의 만족도는 더욱 높아질 것이고, 매출이 늘어나 성공적인 경영을 할 수 있을 것입니다.

병/의원, 약국과 같은 사업은 위의 사업과는 약간 다릅니다. 사람의 생명과 관계된 일이기 때문에 단순히 경영적인 측면보다는 환자의 삶의 질(Quality of Life)과 수명 증가라는 공익적인 사항에 훨씬 더 큰 목적을 두고 있습니다. 약국은 지역사회에서 물리적, 심리적으로 접할 수 있는 가장 가까운 1차적인 (보건) 의료기관으로 환자의 건강상담과 약력관리를 얼마든지 할 수 있는 곳입니다.

처방의약품의 경우, 조제프로그램(Pharm3000, Upharm, 이팜 등)으로 대부분의 약사님들이 약력관리가 가능하지만, 건강기능식품과 의약품 같은 비처방 의약품은 연동이 되지 않거나 POS를 사용하지 않는 약국도 많아 실제 약력관리가 쉽지 않습니다. 고객을 장기적으로 관리하고 건강정보를 Follow-Up 할 수 있다면 약국에서는 환자의 건강을 위한 심도 깊은 서비스를 제공할 수 있습니다.

1-2. 약국에서의 임상 사례

실제로 Clopidogrel을 지속적으로 복용하는 환자가 있었습니다. 피부에 멍이 자주 생기는 증상이 최근 잦아진 경우가 있었지만, 환자는 당장 통증이 없기 때문에 심각하다고 생각하지 않아 병원에 이러한 증상을 문의할 생각도 하지 않았습니다. 적절한 용량조절 혹은 약제 변경이 필요할 수 있는 상황임에도, 병원이라는 물리적, 심리적 장벽으로 인해 환자가 의사에게 모든 정보를 이야기하지 않는 경우가 임상적으로는 상당히 많습니다. 혹은 별 거 아니라고 스스로 생각하여 중요한 임상징후들을 놓치는 경우가 많습니다.

따라서, 약사는 환자의 기저질환과 건강상태와 같은 복합적인 상황을 통해 최선의 건강관리를 할 수 있도록 도와야 합니다. 병세가 약화되어 적절한 병원에 진료를 제안할 수 있고, 환자의 임상증상기록을 모두 활용한다면 심각한 질병을 조기에 발견할 수도 있습니다. 환자는 사소하다고 느끼는 질환이나 징후들을 약국에서는 쉽게 이야기하는 경우가 많기 때문에 이러한 상담활동으로 예상치 못한 질환을 발견하거나 환자의 건강을 증진시킬 수 있습니다. 

이렇게 중요한 건강관리 업무를 대부분의 약사님들은 단골 손님들을 기억하거나, 조제프로그램을 이용하는 경우가 많습니다. 별도의 엑셀 파일을 이용하여 약력관리를 하기도 하지만 사용하기 번거로운 경우가 많습니다. 아래 소개할 프로그램은 엑셀(csv) 파일의 데이터를 기반으로 아래 사진과 같이 고객의 약력관리를 할 수 있습니다.

Pharmanage 고객관리 프로그램

(환자의 개인정보는 최소한으로 활용해야 하며, 고객의 동의는 필수적으로 확인해야 합니다.)

 

2. 프로그램 소개

이 프로그램은 약국에 방문한 고객들을 관리하기 위해 만들어졌습니다.

또한 엑셀파일로 DB가 저장되기 때문에 손쉽게 Office 프로그램을 이용하여 데이터를 수정/가공할 수 있습니다.

물론, 약국 업종이 아니더라도 이를 응용하면 여러 손님을 응대하는 매장에서 사용할 수 있습니다.

미용실, 편의점, 슈퍼, 정육점 등 단골고객들을 관리하기 위한 간단한 프로그램으로 얼마든지 사용할 수 있습니다.

아래의 소스코드를 자유롭게 변경해서 사용해보세요

프로그램 설치 링크

 

GitHub - ssrihappy/pharmanage

Contribute to ssrihappy/pharmanage development by creating an account on GitHub.

github.com

 

3. 소스코드 원본

import csv
from tkinter import ttk
from tkinter import *
from tkinter import filedialog
from tkinter import font as tkFont
import pandas as pd
import datetime

# Original Code from by JW.Seo pharmacist
'''
Minor update added by ssrihappy
V_0.1

-입력 칸을 단순화 하고 신규환자 입력 시 환자번호 및 최초내방일을 자동 입력하도록 개선하였습니다.
-신규환자 등록 및 기존환자 업데이트를 분리하여 중복 및 잠재적 오류를 개선하였습니다.
-exe file로 build하여 Window 운영체제에서 일반 사용자가 사용할 수 있도록 하였습니다.
-아이콘을 신규로 적용하였습니다.

'''


today = str(datetime.datetime.now().strftime('%Y-%m-%d'))


# 설정 파일 경로
config_file = 'config.txt'

def save_config(path):
    """ 설정 파일에 파일 경로 저장 """
    with open(config_file, 'w') as f:
        f.write(path)

def load_config():
    """ 설정 파일에서 파일 경로 불러오기 """
    try:
        with open(config_file, 'r') as f:
            return f.read().strip()
    except FileNotFoundError:
        return None
        


root = Tk()
root.title("팜매니지(Pharmanage) 약국 고객 관리 프로그램 V_0.1")
root.geometry("1200x1000")
style = ttk.Style()
style.theme_use('winnative')


# 변수 선언 부분
cust_num = StringVar()
cust_name = StringVar()
birthdate = StringVar()
contact = StringVar()
consult_date = StringVar()
search_var = StringVar()
selected_row_index = None  # 선택된 행의 인덱스를 저장하는 전역 변수
file_name = None  # 파일 이름을 저장하는 전역 변수
file_name = load_config()  # 프로그램 시작 시 파일 경로 불러오기


content_frame = Frame(root)
content = Text(content_frame, width=int(30*1.5), height=35)
content_scroll = Scrollbar(content_frame, command=content.yview)
content['yscrollcommand'] = content_scroll.set
content.pack(side='left')
content_scroll.pack(side='right', fill='y')

disease_frame = Frame(root)
disease = Text(disease_frame, width=int(30*1.5), height=10)
disease_scroll = Scrollbar(disease_frame, command=disease.yview)
disease['yscrollcommand'] = disease_scroll.set
disease.pack(side='left')
disease_scroll.pack(side='right', fill='y')

prescription_frame = Frame(root)
prescription = Text(prescription_frame, width=int(30*1.5), height=10)
prescription_scroll = Scrollbar(prescription_frame, command=prescription.yview)
prescription['yscrollcommand'] = prescription_scroll.set
prescription.pack(side='left')
prescription_scroll.pack(side='right', fill='y')

prescription_content_frame = Frame(root)
prescription_content = Text(prescription_content_frame, width=int(30*1.5), height=10)
prescription_content_scroll = Scrollbar(prescription_content_frame, command=prescription_content.yview)
prescription_content['yscrollcommand'] = prescription_content_scroll.set
prescription_content.pack(side='left')
prescription_content_scroll.pack(side='right', fill='y')

notes_frame = Frame(root)
notes = Text(notes_frame, width=int(30*1.5), height=7)
notes_scroll = Scrollbar(notes_frame, command=notes.yview)
notes['yscrollcommand'] = notes_scroll.set
notes.pack(side='left')
notes_scroll.pack(side='right', fill='y')



def load_csv():
    if file_name:
        df = pd.read_csv(file_name, encoding='cp949')
        df = df.fillna('')  # NaN 값을 공백으로 대체
        return df
    else:
        return pd.DataFrame()  # 파일 이름이 없는 경우 빈 데이터프레임 반환


# 이벤트 바인딩
def on_right_click(event):
    # 우클릭 이벤트를 처리하는 함수
    pass
    
# 모든 관련 위젯에 대해 우클릭 이벤트 바인딩
content.bind("<Button-3>", on_right_click)
disease.bind("<Button-3>", on_right_click)
prescription.bind("<Button-3>", on_right_click)
prescription_content.bind("<Button-3>", on_right_click)
notes.bind("<Button-3>", on_right_click)



def search():
    df = load_csv()
    search_results.delete(0, END)
    search_text = search_var.get()
    results = df[df.applymap(lambda x: search_text in str(x)).any(axis=1)]
    result_dict = {}
    for idx, row in results.iterrows():
        for col in results.columns:
            if search_text in str(row[col]):
                key = row['고객명 (김마음)'] + ' ' + str(row['최초방문일 (자동입력)'])  # 고객명과 최초방문일를 연결하여 고유한 키 생성
                if key not in result_dict:
                    result_dict[key] = []
                result_dict[key].append(col)
    for name_date, cols in result_dict.items():
        search_results.insert(END, f"{name_date} ({', '.join(cols)})")

def show_info(event):
    global selected_row_index
    df = load_csv()
    selected = search_results.curselection()
    if not selected:
        return
    selected_name_date = search_results.get(selected).split(' (열:')[0]
    selected_name, selected_date = selected_name_date.split(' ')[:2]
    selected_df = df[(df['고객명 (김마음)'] == selected_name) & (df['최초방문일 (자동입력)'] == selected_date)]
    if selected_df.empty:
        print(f"{selected_name_date}에 해당하는 고객명과 최초방문일가 데이터에 없습니다.")
        return
    selected_row_index = selected_df.index[0]
    info = selected_df.iloc[0]

    # 기존의 텍스트 필드에 정보 표시
    cust_num.set(info['고객번호 (자동입력)'])
    cust_name.set(info['고객명 (김마음)'])
    birthdate.set(info['생년월일 (930117-1)'])
    contact.set(info['연락처 (01012342345)'])
    consult_date.set(info['최초방문일 (자동입력)'])
    content.delete("1.0", "end")
    content.insert("1.0", info['상담내용'])
    disease.delete("1.0", "end")
    disease.insert("1.0", info['질환/질병 정보'])
    prescription.delete("1.0", "end")
    prescription.insert("1.0", info['의약품/영양제 정보'])
    prescription_content.delete("1.0", "end")
    prescription_content.insert("1.0", info['임상증상'])
    notes.delete("1.0", "end")
    notes.insert("1.0", info['기타 참고사항'])

def update_csv():
    global selected_row_index
    df = load_csv()
    if selected_row_index is None:
        print("선택된 고객 정보가 없습니다.")
        return

    # 입력된 정보로 데이터 프레임 업데이트
    df.at[selected_row_index, '고객번호 (자동입력)'] = cust_num.get()
    df.at[selected_row_index, '고객명 (김마음)'] = cust_name.get()
    df.at[selected_row_index, '생년월일 (930117-1)'] = birthdate.get()
    df.at[selected_row_index, '연락처 (01012342345)'] = contact.get()
    df.at[selected_row_index, '최초방문일 (자동입력)'] = consult_date.get()
    df.at[selected_row_index, '상담내용'] = content.get("1.0", "end-1c")
    df.at[selected_row_index, '질환/질병 정보'] = disease.get("1.0", "end-1c")
    df.at[selected_row_index, '의약품/영양제 정보'] = prescription.get("1.0", "end-1c")
    df.at[selected_row_index, '임상증상'] = prescription_content.get("1.0", "end-1c")
    df.at[selected_row_index, '기타 참고사항'] = notes.get("1.0", "end-1c")

    # 변경된 내용을 CSV 파일에 저장
    df.to_csv('customer_data.csv', index=False, encoding='cp949')

def read_No():
    df = load_csv()
    new_num = (len(df)+1)
    return new_num

# 검색어 입력 칸 생성
search_entry = ttk.Entry(root, textvariable=search_var, width=40)
search_entry.grid(row=0, column=1, padx=10)
ttk.Button(root, text="기존고객 검색(2-1)", command=search).grid(row=0, column=2, padx=(5, 10), pady=1)

# 엔터 키 이벤트 바인딩
search_entry.bind('<Return>', lambda event: search())

# 검색 결과 리스트
search_results = Listbox(root, width=80)
search_results.grid(row=1, column=1, padx=10, pady=(1, 50), columnspan=2)
search_results.bind('<Double-1>', show_info)

fields = [('고객번호 (자동입력)', cust_num), ('고객명 (김마음)', cust_name), ('생년월일 (930117-1)', birthdate), ('연락처 (01012342345)', contact), ('최초방문일 (자동입력)', consult_date)]
for i, (text, var) in enumerate(fields):
    ttk.Label(root, text=text).grid(row=i+2, column=0, padx=10, pady=1)
    ttk.Entry(root, textvariable=var, width=60).grid(row=i+2, column=1, padx=10, pady=1)

ttk.Label(root, text="상담내용").grid(row=len(fields)+2, column=0, padx=10, pady=1)
content_frame.grid(row=len(fields)+2, column=1, rowspan=4, padx=10, pady=1)

ttk.Label(root, text="질환/질병 정보").grid(row=len(fields)+2, column=2, padx=10, pady=1)
disease_frame.grid(row=len(fields)+2, column=3, padx=10, pady=1)

ttk.Label(root, text="의약품/영양제 정보").grid(row=len(fields)+3, column=2, padx=10, pady=1)
prescription_frame.grid(row=len(fields)+3, column=3, padx=10, pady=1)

ttk.Label(root, text="임상증상").grid(row=len(fields)+4, column=2, padx=10, pady=1)
prescription_content_frame.grid(row=len(fields)+4, column=3, padx=10, pady=1)

ttk.Label(root, text="기타 참고사항").grid(row=len(fields)+5, column=2, padx=10, pady=1)
notes_frame.grid(row=len(fields)+5, column=3, padx=10, pady=1)

def clear_all():
    num = read_No()
    cust_num.set(str(num))
    cust_name.set('')
    birthdate.set('')
    contact.set('')
    consult_date.set(today)
    content.delete("1.0", "end")
    disease.delete("1.0", "end")
    prescription.delete("1.0", "end")
    prescription_content.delete("1.0", "end")
    notes.delete("1.0", "end")
    
def open_file():
    global file_name
    file_path = filedialog.askopenfilename(filetypes=[("CSV 파일", "*.csv")])
    if file_path:
        file_name = file_path
        file_path_label.config(text="DB 파일: " + file_name)
        save_config(file_name)  # 설정 파일에 파일 경로 저장

# 파일 경로 라벨 및 파일 열기 버튼
file_path_label = ttk.Label(root, text="DB 파일: " + (file_name if file_name else ""))
file_path_label.grid(row=0, column=3, padx=10)
ttk.Button(root, text="DB 파일 열기", command=open_file).grid(row=0, column=4, padx=(5, 10), pady=1)

def save_new_customer():
    # 입력된 정보 가져오기
    
    new_cust_num = cust_num.get()
    new_cust_name = cust_name.get()
    new_birthdate = birthdate.get()
    new_contact = contact.get()
    new_consult_date = consult_date.get()
    new_content = content.get("1.0", "end-1c")
    new_disease = disease.get("1.0", "end-1c")
    new_prescription = prescription.get("1.0", "end-1c")
    new_prescription_content = prescription_content.get("1.0", "end-1c")
    new_notes = notes.get("1.0", "end-1c")

    # CSV 파일에 새로운 고객 정보 추가
    with open('customer_data.csv', 'a', newline='', encoding='cp949') as file:
        writer = csv.writer(file)
        writer.writerow([new_cust_num, new_cust_name, new_birthdate, new_contact,
                         new_consult_date, new_content, new_disease, new_prescription,
                         new_prescription_content, new_notes])

# 신규 고객 저장 버튼 생성
ttk.Button(root, text="신규 고객 저장(1-2)", command=save_new_customer).grid(row=len(fields)+6, column=1, padx=10, pady=1)



# 업데이트 버튼 생성
ttk.Button(root, text="기존고객 업데이트(2-2)", command=update_csv).grid(row=len(fields)+6, column=2, padx=10, pady=1)

# 모든 필드 비우기 버튼 생성
ttk.Button(root, text="신규등록 - 초기화(1-1)", command=clear_all).grid(row=len(fields)+6, column=0, padx=10, pady=1)




root.mainloop()
반응형
마음약국 대표전화: 02-964-0675 Email: 0stjpharm@gmail.com 주소: 서울시 동대문구 왕산로 183