5  Database - MySQL

5.1 개요

CS 작업 관련 모든 구글시트, 아카라 카페, 기술지원 내역을 MySQL DB에 데이터 베이스화 하였다.

5.2 DB Schema

Database: cs

5.2.1 아카라 카페

5.2.2 기술 CS 업무

5.2.3 수거 신청 대장

5.2.4 커튼/블라인드 설치

5.2.5 도어락 설치기사 배정

5.2.6 도어락 설치 파트너

5.2.7 도어락 불량 대장

5.2.8 업무일지

5.3 MySQL 테이블 생성 방법

도어락 설치 기사 배정 테이블 예제

#CREATE TABLE doorlock_installation_ledger (
#    id INT AUTO_INCREMENT PRIMARY KEY,
#    registered_date DATETIME,
#    주문처 VARCHAR(255),
#    지역1 VARCHAR(255),
#    지역2 VARCHAR(255),
#    지점 VARCHAR(255),
#    설치 VARCHAR(255),
#    기사연락처 VARCHAR(20),
#    비용 DECIMAL(10, 2),
#    청구월 VARCHAR(50),
#    증빙유형 VARCHAR(50),
#    추가비용 DECIMAL(10, 2),
#    청구월2 VARCHAR(50),
#    지급기안 VARCHAR(50),
#    설치여부 BOOLEAN,
#    이름 VARCHAR(255),
#    연락처 VARCHAR(20),
#    주소 VARCHAR(255),
#    상품명 VARCHAR(255),
#    상품옵션 VARCHAR(255),
#    배송메시지 TEXT,
#    특이사항 TEXT,
#    UNIQUE KEY unique_record (registered_date, 연락처,주소)
#) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
  • Unique Key 설정: 위 예제에서 보며 registered_date, 연락처와 주소 3개의 칼럼이 같을 경우는 새로운 row를 추가하지 않고 업데이트하도록 unique key를 설정합니다.

5.4 코드 분석

5.4.1 MySQL DB 연결

#conn = mysql.connector.connect(
#    user=os.getenv('SQL_USER'),
#    password=os.getenv('SQL_PASSWORD'),
#    host=os.getenv('SQL_HOST'),
#    database=os.getenv('SQL_DATABASE'),
#    charset='utf8mb4',
#    collation='utf8mb4_general_ci'
#)
  • 환경 변수에서 가져온 자격 증명으로 MySQL 데이터베이스에 연결을 설정한다. charset과 collation을 UTF-8로 설정해 한국어와 같은 문자를 적절히 처리할 수 있게 한다.

5.4.2 아카라 카페

5.4.2.1 데이터 저장

#def save_to_mysql(self, df):
#    """Save DataFrame to MySQL database."""
#    try:
#        # Connect to MySQL database
#       conn = mysql.connector.connect(**self.db_config)
#       cursor = conn.cursor()
#
#        # Replace NaN with None for MySQL compatibility
#        df = df.where(pd.notnull(df), None)
#        # Insert data into MySQL, checking for duplicates
#       for _, row in df.iterrows():
#            # Check if the row already exists
#            check_sql = """
#            SELECT COUNT(*) FROM aqara_cafe WHERE registered_date = %s AND title = %s
#            """
#            cursor.execute(check_sql, (row['registered_date'], row['title']))
#            result = cursor.fetchone()
#
#            if result[0] == 0:  # If no existing row found
#                insert_sql = """
#                INSERT INTO aqara_cafe (registered_date, devices, title, question, answers)
#                VALUES (%s, %s, %s, %s, %s)
#                """
#               cursor.execute(insert_sql, tuple(row))
#        
#        conn.commit()
#       cursor.close()
#        conn.close()
#        print("Data saved to MySQL successfully!")
#   except mysql.connector.Error as err:
#        print(f"Error: {err}")
  • save_to_mysql() 함수는 크롤링한 데이터를 MySQL 데이터베이스에 저장하는 역할을 한다.

  • NaN처리: df.where(pd.notnull(df), None): MySQL에서는 NaN 값을 NULL로 처리해야 합니다. 이 부분에서 DataFrame의 NaN 값을 MySQL에 호환되는 None으로 변환합니다.

5.4.2.2 중복 데이터 확인및 처리

SELECT COUNT(*) FROM aqara_cafe WHERE registered_date = %s AND title = %s
  • 이 쿼리는 테이블에서 등록된 날짜와 제목이 같은 데이터를 확인하는 쿼리입니다. 이를 통해 중복된 데이터가 있는지 확인한다.

  • cursor.execute(check_sql, (row[‘registered_date’], row[‘title’])): row[‘registered_date’]와 row[‘title’] 값을 사용하여 중복 여부를 검사한다.

5.4.2.3 데이터 삽입

#INSERT INTO aqara_cafe (registered_date, devices, title, question, answers)
#VALUES (%s, %s, %s, %s, %s)
  • 이 쿼리는 MySQL 테이블 aqara_cafe에 데이터를 삽입하는 SQL 문이다. 각 행의 데이터를 테이블에 삽입한다.

  • cursor.execute(insert_sql, tuple(row)): 각 행의 데이터를 SQL 쿼리의 %s 자리에 삽입해 MySQL에 추가한다.

5.4.3

수거 신청 대장

5.4.3.1 기존 데이터 유무 확인

#check_query = """
#SELECT COUNT(*) FROM service_ledger 
#WHERE registered_date = %s AND 고객명 = %s AND 주문번호 = %s AND 제품 = %s
#"""
#cursor.execute(check_query, (row['registered_date'], row['고객명'], row['주문번호'], row['제품']))
#result = cursor.fetchone()
  • 동일한 registered_date, 고객명, 주문번호, 그리고 제품을 가진 레코드가 이미 존재하는지 확인한다.

  • cursor.execute()로 쿼리를 실행하고, cursor.fetchone()으로 결과를 가져온다.

5.4.3.2 업데이트 로직

#if result[0] > 0:
#    update_query = """
#    UPDATE service_ledger 
#    SET 완료 = %s, 작성자 = %s, 구분 = %s, 사유 = %s, 배송비 = %s, ...
#    WHERE registered_date = %s AND 고객명 = %s AND 주문번호 = %s AND 제품 = %s
#    """
#    cursor.execute(update_query, ...)
#else:
#    insert_query = """
#    INSERT INTO service_ledger (완료, registered_date, 작성자, 구분, 사유, ...) 
#    VALUES (%s, %s, %s, %s, ...)
#    """
#    cursor.execute(insert_query, ...)
  • 업데이트 로직: 만약 동일한 레코드가 존재하면, 기존의 레코드를 UPDATE 쿼리를 사용해 업데이트합니다.

  • 삽입 로직: 동일한 레코드가 존재하지 않으면, 새로운 레코드를 INSERT 쿼리를 사용해 삽입합니다.

5.4.4 설치 기사 배정

5.4.4.1 데이터 삽입 및 업데이트 쿼리

#for index, row in df.iterrows():
#    if row['주문번호'] and pd.notna(row['주문번호']):
#        sql = """
#            INSERT INTO installation_ledger 
#            (registered_date, 출고날짜, 고객명, 연락처, 주문번호, 주소, 구매품목,
#            도어락, 도어벨, 조명스위치, 커튼, 내용확인, 기사님성함, 해피콜예정일, 
#            설치예정일, 설치완료여부, 유상, 비고_아카라, 비고_피엘)
#            VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, #%s)
#           ON DUPLICATE KEY UPDATE
#            registered_date = VALUES(registered_date),
#            출고날짜 = VALUES(출고날짜),
#            고객명 = VALUES(고객명),
#           연락처 = VALUES(연락처),
#            주소 = VALUES(주소),
#            구매품목 = VALUES(구매품목),
#            도어락 = VALUES(도어락),
#            도어벨 = VALUES(도어벨),
#            조명스위치 = VALUES(조명스위치),
#            커튼 = VALUES(커튼),
#            내용확인 = VALUES(내용확인),
#            기사님성함 = VALUES(기사님성함),
#            해피콜예정일 = VALUES(해피콜예정일),
#            설치예정일 = VALUES(설치예정일),
#            설치완료여부 = VALUES(설치완료여부),
#            유상 = VALUES(유상),
#            비고_아카라 = VALUES(비고_아카라),
#            비고_피엘 = VALUES(비고_피엘)
#        """
#        values = (
#            row['registered_date'], row['출고날짜'], row['고객명'], row['연락처'], #row['주문번호'], row['주소'], row['구매품목'],
#            row['도어락'], row['도어벨'], row['조명스위치'], row['커튼'], row['내용확인'], #row['기사님성함'], row['해피콜예정일'],
#            row['설치예정일'], row['설치완료여부'], row['유상'], row['비고_아카라'], #row['비고_피엘']
#        )
#        
#       cursor.execute(sql, values)
  • df.iterrows()를 사용해 DataFrame의 각 행을 순회하며, 주문번호가 존재하는 경우에만 MySQL에 데이터를 삽입 또는 업데이트합니다.

  • 쿼리 설명:

    • INSERT INTO installation_ledger: 데이터를 삽입하는 SQL 쿼리입니다.

    • ON DUPLICATE KEY UPDATE: 만약 중복된 키(예: 주문번호)가 있으면 기존 데이터를 업데이트합니다.

    • VALUES(): 각 행의 데이터를 SQL 쿼리로 전달합니다.

5.4.5 커튼/블라인드 설치

#insert_query = """
#    INSERT INTO curtain_ledger 
#    (registered_date, 플랫폼, 상품주문번호, 상품명, 수량, 수취인명, 수취인연락처1, 
#     수취인연락처2, 배송지, 구매자연락처, 우편번호, 배송메세지, 옵션정보, 
#     옵션관리코드, 배송방법, 택배사, 송장번호)
#    VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
#    ON DUPLICATE KEY UPDATE
#    registered_date = VALUES(registered_date),
#    플랫폼 = VALUES(플랫폼),
#    상품명 = VALUES(상품명),
#    수량 = VALUES(수량),
#    수취인명 = VALUES(수취인명),
#    수취인연락처1 = VALUES(수취인연락처1),
#   수취인연락처2 = VALUES(수취인연락처2),
#    배송지 = VALUES(배송지),
#    구매자연락처 = VALUES(구매자연락처),
#    우편번호 = VALUES(우편번호),
#    배송메세지 = VALUES(배송메세지),
#    옵션정보 = VALUES(옵션정보),
#    옵션관리코드 = VALUES(옵션관리코드),
#    배송방법 = VALUES(배송방법),
#    택배사 = VALUES(택배사),
#    송장번호 = VALUES(송장번호)
#"""
  • SQL 쿼리: INSERT INTO 구문을 사용하여 데이터를 삽입합니다. 동시에 ON DUPLICATE KEY UPDATE를 사용하여 중복된 키가 있을 경우 기존 데이터를 업데이트합니다.

5.4.5.1 트랜잭션 재시도 및 커밋

#retry_count = 3
#for index, row in df.iterrows():
#    # 수량이 빈 값이거나 숫자로 변환할 수 없으면 0으로 처리
#    try:
#        quantity = int(row['수량']) if row['수량'].strip() else 0
#    except ValueError:
#        quantity = 0
#    
#    values = (row['registered_date'], row['플랫폼'], row['상품주문번호'], row['상품명'], #uantity, ...)
#   
#    for attempt in range(retry_count):
#        try:
#            cursor.execute(insert_query, values)
#            break  # 성공 시 루프 탈출
#        except mysql.connector.Error as err:
#            if err.errno == 1205:  # Lock wait timeout
#                st.write(f"Lock wait timeout, 재시도 중: {attempt + 1}")
#               time.sleep(2)  # 잠시 대기 후 재시도
#           else:
#                raise  # 다른 오류 발생 시
#
#cursor.close()
#conn.close()
  • 재시도 로직: MySQL에서 트랜잭션이 실패할 경우(예: 잠금 대기 시간 초과) 최대 3번까지 재시도합니다.

  • 트랜잭션 처리: 각 데이터 행을 MySQL에 삽입 또는 업데이트합니다.

5.4.6 도어락 설치 기사 배정

5.4.6.1 데이터 삽입 또는 업데이트 쿼리

#query = """
#INSERT INTO doorlock_installation_ledger (
#    registered_date, 주문처, 지역1, 지역2, 지점, 설치, 기사연락처, 비용, 청구월, 증빙유형, #추가비용, 청구월2, 지급기안, 설치여부, 이름, 연락처, 주소, 상품명, 상품옵션, 배송메시지, #특이사항
#) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
#ON DUPLICATE KEY UPDATE
#    주문처 = VALUES(주문처),
#    지역1 = VALUES(지역1),
#    지역2 = VALUES(지역2),
#    지점 = VALUES(지점),
#    설치 = VALUES(설치),
#    기사연락처 = VALUES(기사연락처),
#    비용 = VALUES(비용),
#    청구월 = VALUES(청구월),
#    증빙유형 = VALUES(증빙유형),
#    추가비용 = VALUES(추가비용),
#    청구월2 = VALUES(청구월2),
#    지급기안 = VALUES(지급기안),
#    설치여부 = VALUES(설치여부),
#    이름 = VALUES(이름),
#    상품명 = VALUES(상품명),
#    상품옵션 = VALUES(상품옵션),
#    배송메시지 = VALUES(배송메시지),
#    특이사항 = VALUES(특이사항)
#"""
  • SQL 쿼리: INSERT INTO 구문을 사용하여 데이터를 삽입하고, 중복 키가 발생할 경우 기존 데이터를 업데이트하는 ON DUPLICATE KEY UPDATE 구문을 사용합니다.

5.4.6.2 데이터 삽입 또는 업데이트 처리

#def insert_or_update_data(df):
#    try:
#        for index, row in df.iterrows():
#            values = [
#                row['registered_date'].strftime('%Y-%m-%d %H:%M:%S') if #row['registered_date'] else None,
#                row['주문처'], row['지역1'], row['지역2'], row['지점'], row['설치'], #row['기사연락처'],
#                clean_cost_value(row['비용']), row['청구월'], row['증빙유형'], #clean_cost_value(row['추가비용']),
#                row['청구월2'], row['지급기안'], 1 if row['설치여부'] == 'TRUE' else 0,
#                row['이름'], row['연락처'], row['주소'], row['상품명'], row['상품옵션'], #row['배송메시지'], row['특이사항']
#            ]
#            cursor.execute(query, values)
#        conn.commit()
#        st.write("데이터가 성공적으로 MySQL에 저장되었거나 업데이트되었습니다.")
#    except Error as e:
#        st.write(f"Error while connecting to MySQL: {e}")
#    finally:
#        cursor.close()
#        conn.close()
  • 데이터 삽입/업데이트: 각 데이터 행을 INSERT INTO 쿼리를 통해 MySQL에 삽입하거나 업데이트한다.

5.4.7 도어락 설치 파트너

5.4.7.1 데이터 삽입 및 업데이트 쿼리

#query = """
#INSERT INTO doorlock_installation_partners (
#    registered_date, 지역1, 지역2, 대리점, 담당자코드, 대표, 연락처, 주소,
#    사업자등록번호, 은행, 계좌, 이름, 세금계산서, 플라자, 기타
#) VALUES (
#    %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
#) ON DUPLICATE KEY UPDATE
#    지역1 = VALUES(지역1),
#    담당자코드 = VALUES(담당자코드),
#    대표 = VALUES(대표),
#    연락처 = VALUES(연락처),
#   사업자등록번호 = VALUES(사업자등록번호),
#   은행 = VALUES(은행),
#    계좌 = VALUES(계좌),
#   이름 = VALUES(이름),
#    세금계산서 = VALUES(세금계산서),
#   플라자 = VALUES(플라자),
#   기타 = VALUES(기타);
#""
#
#def insert_or_update_data(df):
#   try:
#        for index, row in df.iterrows():
#            values = [
#                row['registered_date'].strftime('%Y-%m-%d %H:%M:%S') if #row['registered_date'] else None,
#                row['지역1'], row['지역2'], row['대리점'], row['담당자코드'], row['대표'], #row['연락처'], 
#               row['주소'], row['사업자등록번호'], row['은행'], row['계좌'], row['소유자명'],row['세금계산서'], row['플라자'], row['기타']
#            ]
#            cursor.execute(query, values)
#        conn.commit()
#        st.write("데이터가 성공적으로 MySQL에 저장되었거나 업데이트되었습니다.")
#    except Error as e:
#        st.write(f"Error while connecting to MySQL: {e}")
#    finally:
#        cursor.close()
#        conn.close()
  • INSERT INTO ON DUPLICATE KEY UPDATE: 데이터베이스에 데이터를 삽입할 때, 만약 기존에 동일한 키(예: 연락처)가 존재하면 해당 데이터를 업데이트한다. 그렇지 않으면 새로운 데이터를 삽입한다.

  • 데이터 삽입: 각 행의 데이터를 SQL 쿼리에 맞춰 준비하고, 반복문을 통해 각 행의 데이터를 MySQL 테이블에 삽입하거나 업데이트한다.

  • 트랜잭션 커밋: conn.commit()을 통해 데이터베이스에 변경 사항을 커밋하여 저장한다.

5.4.8 도어락 불량 대장

5.4.8.1 데이터 삽입 및 업데이트 쿼리

#query = """
#INSERT INTO doorlock_malfunction_ledger (registered_date, ..., 비고)
#VALUES (%s, %s, ..., %s)
#ON DUPLICATE KEY UPDATE 접수채널 = VALUES(접수채널), ..., 비고 = VALUES(비고);
#"""
  • INSERT INTO … ON DUPLICATE KEY UPDATE 구문을 사용하여, 동일한 고객명과 고객연락처에 해당하는 데이터가 이미 존재할 경우, 기존 데이터를 업데이트하고, 없으면 새 데이터를 삽입한다.
#for index, row in df.iterrows():
#    values = [
#        row['registered_date'].strftime('%Y-%m-%d %H:%M:%S') if row['registered_date'] else #None,
#        ...
#    ]
#    cursor.execute(query, values)
#conn.commit()
  • 데이터프레임의 각 행에 대해 쿼리를 실행하여, MySQL에 데이터를 삽입하거나 업데이트한다.

5.4.9 업무일지

5.4.9.1 주요기능

  • 데이터 업데이트: 동일한 날짜와 업무 유형의 데이터가 있을 경우, 기존 데이터를 업데이트하고 없으면 새로 추가한다.

  • 데이터 조회 및 자동 입력: 기존 데이터를 조회하여 폼에 미리 입력해 사용자 경험을 개선한다.

5.4.9.2 MySQL 데이터 검색

#cursor.execute("""
#    SELECT 업무유형, 작업자, 업무일지, 비고 
#    FROM work_journal 
#    WHERE DATE(registered_date) = %s AND 업무유형 = %s
#""", (registered_date_for_query, task_type))
#existing_data = cursor.fetchone()
  • 데이터베이스에서 선택된 날짜와 업무 유형에 해당하는 기존 데이터를 조회하여 있으면 폼에 자동으로 채운다.

5.4.9.3 MySQL 데이터 저장

#cursor.execute("""
#    INSERT INTO work_journal (registered_date, 업무유형, 작업자, 업무일지, 비고) 
#    VALUES (%s, %s, %s, %s, %s)
#""", (registered_date_for_db, task_type, worker, work_journal, note))
  • MySQL에도 동일한 방식으로 데이터를 삽입하거나 업데이트한다.