길드용 로스트아크 디스코드 봇 (3) DB 리팩터링

Jeuk Oh·2021년 7월 23일
3

지금까지 크롤러와 크롤러와 DB를 활용한 DB.call 함수, 디스코드 상에서 명령어를 쳤을 때 DB.call 함수를 활용하여 로아 계정과 디스코드 계정을 연결, 역할을 부여하는 link 함수를 구현하였습니다.

일단 급조해서 DB를 dict 자료형과 pickle 라이브러리를 사용해서 저장하였지만, 몇가지 불편함이 있습니다.
현재 DB 내 데이터의 자료형은 {discord Hash : [원정대 캐릭터 정보들:list]}으로 저장이 되어있습니다.
가장 큰 불편함은 추후 레이드 가능 인원 출력 기능을 만들면서 DB안에서 아이템 레벨을 조건으로 검색을 해야하는데 해쉬별로 모든 원정대 데이터를 모은 뒤 슬라이싱을 해야한다는 점에서 벌써 귀찮겠다는 생각이 듭니다.

python에 내장된 sqlite3을 이용하여 이러한 문제를 해결하였습니다.


DB코드 수정전

class DB():
    """
    혹시 모르닌까 folder를 받자
    init에서는 Hash_list를 로드해서 들고다니자
    """
    
    def __init__(self,folder):
        self.folder = folder
        self.data = self.load()
        
    def save(self):
    	...
    def load(self):
    	...

    계정연동, 계정검색에 사용
    데이터에 이미 해쉬가 있다면 그 값을 넘겨줌.
    해쉬가 없다면, 검색의 경우 (해쉬가 None) 크롤링정보를 넘겨주고
    연동의 경우 (해쉬값 존재) 데이터에 크롤링정보를 추가한 뒤 저장하고 넘겨줌
    """
    def call(self,Hash,Id):
        #print(f'Call {Hash}, {Id}, {self.data}')
        # return : {Hash : [[server,Id,직업,level],~]}
        if Hash in self.data:
            return self.data.get(Hash)
        else:
            if not Hash:
                return crawl(Id)
            self.data[Hash]=crawl(Id)
            self.data[Id]=Hash
            self.save()
            return self.data.get(Hash)

보시다시피 기존 DB는 단순히 호출될 시 data를 로드한 후 들고다니면서, Hash를 키로 원정대 데이터를 넘겨주고 있습니다. 좀 더 편리한 콜 기능을 만들기 위해 sqlite를 사용한 DB로 리펙토링을 하였습니다.


DB코드 수정후

import sqlite3

class DB():
    def __init__(self,folder):
        self.path = folder+'data.db'
        self.conn = sqlite3.connect(self.path)
        user_data_table = 'user_data(hash INTEGER,server TEXT,nick TEXT PRIMARY KEY,class TEXT,lv float)'
        raid_clear_data = 'clear_data(nick TEXT,raid TEXT)'
        self.conn.execute(f'CREATE TABLE if not exists {user_data_table}')
        self.conn.execute(f'CREATE TABLE if not exists {raid_clear_data}')
        self.conn.commit()

    #data를 user_data에 저장
    #[(hash,server,nick,class,lv),..]
    # overwrite 면 replace해줌
    def save(self,data,tablename='user_data',overwrite=False):
        cur = self.conn.cursor()
        print(f'{tablename}_save: data = \n{data}')
        if overwrite:
            cur.executemany(f'INSERT or Replace INTO {tablename} Values(?,?,?,?,?)', data)
        else:
            cur.executemany(f'INSERT INTO user_data Values(?,?,?,?,?)', data)
        self.conn.commit()
        cur.close()

init과 save입니다. init에선 먼저 db파일을 만들고 user_data를 저장할 테이블과 던전클리어 데이터를 저장할 테이블 2개를 선언합니다. 던전 클리어 테이블은 추후 레이드가 완료된 사람들 정보를 모아서 레이드 가능 인원을 표시할 때 제외해주기 위하여 따로 테이블을 만들었습니다. 로스트아크 주간 컨텐츠가 초기화되는 시간에 raid_clear 테이블도 초기화할 생각입니다.'

user_data는 디스코드에 있는 길드원들의 로스크아크 데이터를 저장합니다.
(디스코드해쉬, 서버, 로아닉, 직업, 아이템레벨) 튜플로 저장하게 됩니다.
다른 컬럼들은 중복 가능성이 있지만 로아닉만은 고유한 값이 될 것이므로 Primary key를 적용하여 중복 데이터가 생기지 않게 관리합니다.

save 함수는 테이블 이름과 데이터를 받아 저장해주며, 이미 등록된 길드원들의 정보를 업데이트 (이 또한 나중에 매일 봇이 자동으로 업데이트를 하도록 기능을 추가할 생각입니다.) 할 수 있도록 overwrithe 옵션을 주었습니다. 게임에서 변화를 준 뒤 업데이트를 시키면 DB에 변화된 정보가 잘 덮여쓰기가 됩니다.

당장은 봇이 24시간 가동되는 것이 아닌지라 print를 사용하여 터미널로 로그를 확인하고 있는데, 로깅파일을 추가하는 기능도 필요해보입니다.


    #arg에 맞게 data를 출력
    #return_hash=True 시 조건맞는 hash 단 하나 리턴
    #ex user_data_load({'hash':~, 'nick':, 'lv_low':float, 'lv_high':float})
    #return 형 -> [(server,nick,class,lv), (...)]
    def load(self,return_hash=False,tablename='user_data',output=['server','nick','class','lv'],**kwargs):
        cond = []
        for key,value in kwargs.items():
            #print(key,value)
            if key == 'lv_low':
                cond.append(f'lv >= {value}')
                continue
            if key == 'lv_high':
                cond.append(f'lv < {value}')
                continue
            cond.append(f'{key} == \"{value}\"')

        buffer = ' and '.join(x for x in cond)
        output = ', '.join(x for x in output)
        columns = output if not return_hash else "DISTINCT hash"
        sql = f"select {columns} from {tablename} where {buffer}"
        print(f'{tablename}_load sql {sql}')
        cur = self.conn.cursor()
        cur.execute(sql)
        rows = cur.fetchall()
        cur.close()
        print(f'{tablename}_load_ rows {rows}')
        if return_hash and rows:
            rows = rows[0][0]
        return rows

다음은 load 함수입니다. 파라미터가 굉장히 많은데, 마찬가지로 어느 테이블에 접근할지 정하는 tablename, output 칼럼들을 정하는 output 옵션, 조건을 정하는 키워드 파라미터가 있습니다.

return_hash 파라미터는 True일 시, 해당 조건을 만족하는 discord Hash만 리턴합니다.
예를 들어, X가 디스코드에서 길드원 A의 계정을 DB에 검색해보고 싶습니다. X가 아는 A의 정보는 A.discord_nick, A.loa_nick1 정도만 알고 있고, Hash 값은 따로 모릅니다. 사용자 입장에서 직관적인 명령어는 !계정검색 A의닉네임 입니다. 계정검색 명령어가 실행되면 A의 모든 캐릭터 정보가 DB에 있는지 서치해야 하는데, DB내의 A의 정보는 [(캐릭1),(캐릭2),..]가 있고 공통된 값은 discord hash 뿐입니다. 따라서 먼저 A닉네임으로 Hash값을 리턴하고 (return_hash 옵션을 통해) 다시 그 hash 값을 통해 캐릭터 정보를 호출하기 위해 해당 옵션을 추가하였습니다.

계정연동 함수에서 디스코드 닉네임을 로스트아크 대표캐릭터 닉네임으로 똑같이 바꿔주었기 때문에,
사실 해당 옵션 없이도 Member.id로 hash에 접근할 수 있지만, 계정연동이 자율적이라서 안하시는 분들도 많길래 그냥 일단 이렇게 하였습니다.

추후 KorLARK 디스코드 서버처럼 역할 부여가 안된 멤버들에게 인증 게시판에만 접근 권한을 주어, 인증을 강요하는 방법이 있을 듯 합니다.

함수를 호출할 때, 키워드 인자로 load(lv_low = 1370, lv_high = 1415)를 준다면 DB내의 아이템 레벨이 1370~1415 사이인 모든 데이터를 받을 수 있습니다. 이 전 DB에서는 Hash로 접근해서 데이터를 받아 조건을 적용해야 했다면 수정 후 DB load에서는 바로 원하는 데이터를 받을 수 있습니다. 굳굳


    """
    계정연동, 계정검색에 사용
    데이터에 이미 해쉬가 있다면 그 값을 넘겨줌.
    해쉬가 없다면, 검색의 경우 (해쉬가 None) 크롤링정보를 넘겨주고
    연동의 경우 (해쉬값 존재) 데이터에 크롤링정보를 추가한 뒤 저장하고 넘겨줌
    """
    def call(self,hash: int,Id: str,update=False):
        #hash가 없으면 검색기능
        if not hash:
            return crawl(Id)
        #hash가 있으면 연동기능
        #먼저 DB에 hash를 기반으로 데이터가 있나 찾아봄
        data = self.load(hash=hash)
        #있으면 그거 걍 줌
        if data and not update:
            return data
        #없으면
        else:
            #크롤링해서 데이터를 만든 뒤
            data = crawl(Id,hash)
            #저장하고
            self.save(data,overwrite=True)
            data = self.load(hash=hash)
            return data
            
	def delete():
    	...

call 함수는 load와 save를 활용하여 디스코드 인터페이스와 크롤러, DB를 연결시키는 것에 중점을 뒀습니다.
hash가 None이면 검색기능으로 판단하고 크롤러한 정보를 주며,
hash가 있으면 DB에서 hash를 조건으로 검색하여 data를 받습니다.
data가 존재하고 update 옵션이 꺼져있다면, 그 값을 그대로 리턴하고
data가 없거나 업데이트 옵션이 켜져있다면, 크롤링을 다시 하여 DB에 반영하고 data를 다시 로드하여 리턴합니다.

call 함수 하나를 디스코드 내 계정 검색과 계정 연동, 계정 업데이트 명령에 모두 사용할 수 있어 코드 작업이 편리해집니다.

delete 함수는 load와 비슷한 구조로 주로 hash를 받아서 DB를 지우는 함수로 구현할 것입니다.
계정연동에 오타나 오류가 났을 때, 초기화하는 용도로 사용하고, 매주 로요일에 clear_table을 초기화하는 용으로 사용할 것 같습니다.


sqlite3을 처음 사용해보았는데 굉장히 편리한 것 같습니다. 다음 글에서는 드디어 레이드 가능 인원을 디스코드 채팅창에 게시하는 raid_possible() 함수와 이쁜 출력을 위해 discord.embed을 쓰는 것에 대해 게시할 것입니다.

profile
개발을 재밌게 하고싶습니다.

0개의 댓글