PyQt기반 회원가입, 로그인 앱 제작

개요

이번에는 Python, PyQt5를 이용해 간단한 회원가입, 로그인을 진행하는 앱을 만들어 보겠습니다.

[회원 가입]
 
[로그인]

 

  

주요 기능은 아래와 같습니다.

  • QTabWidget을 활용해 회원가입, 로그인 위젯 분리

  • 회원가입된 ID, PW는 파일에 저장

  • 로그인시 파일에서 저장된 ID, PW를 읽어와 로그인 여부 판단

 

SQLite DB를 사용하는 것보다 코드를 이해하는데 부담이 적을 것으로 판단합니다.

Database를 이해하는 분들은 이 코드를 수정해 DB를 적용 해보기 바랍니다.


소스코드

전체 코드는 main.py, join.py, login.py 3개로 구성되어 있으며 있으며 main.py를 실행하면 join.py, login.py가 모듈형태로 불려와 실행되는 구조입니다.


main.py 소스코드

앱의 시작부 입니다.

from PyQt5.QtWidgets import QApplication, QTabWidget
from join import JoinWidget
from login import LoginWidget
import sys

class Form(QTabWidget):

    def __init__(self):
        super().__init__()
        self.setWindowTitle('Ocean Coding School')

        join = JoinWidget()
        login = LoginWidget(self, JoinWidget.FILE_NAME)
        self.addTab(join, join.windowTitle())
        self.addTab(login, login.windowTitle())

    def onFail(self):
        self.close()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = Form()
    w.show()
    sys.exit(app.exec_())


[라인 1~4], import 구문

앱에 필요한 모듈을 불러오는 부분입니다.

2, 3번 라인의 join, login은 뒤에 설명할 join.py, login.py 모듈을 의미합니다.


[라인 6~15], __init__()

Qt의 QTabWidget에서 상속받은 Form Class의 생성자 함수 부분입니다.

뒤에 설명할 join.py의 JoinWidget, login.py의 LoginWidget의 객체 생성 후, addTab() 함수를 이용 탭에 표시할 위젯으로 추가합니다.

이때 JoinWidget, LoginWidget 클래스의 생성자 함수가 호출되게 됩니다.

 

[라인 17~18], onFail()

차후에 로그인 3회 이상 실패시, 위젯을 닫는 역할을 수행합니다.


[라인 20~24], main()

파이썬의 메인함수이며, C++의 main()함수와 같은 역할을 수행합니다.

Qt 앱 클래스 객체를 생성하고 Form( QTabWidget )과 연결해 앱을 시작합니다.


join.py 소스코드

회원 가입을 담당하는 위젯입니다.

from PyQt5.QtWidgets import (QWidget, QLabel, QLineEdit, QTableWidget, QTableWidgetItem,
                             QPushButton, QVBoxLayout, QHBoxLayout, QGroupBox, QMessageBox)
class JoinWidget(QWidget):

    FILE_NAME = 'Member.txt'

    def __init__(self):
        super().__init__()
        self.setWindowTitle('회원가입')

        self.initUi()
        self.readFile()

        # signal
        self.btn.clicked.connect(self.onJoin)

    def initUi(self):
        # join
        joinbox = QGroupBox('ID, Password 설정')

        # id
        idbox = QHBoxLayout()
        idbox.addWidget(QLabel('ID'))
        self.id = QLineEdit()
        idbox.addWidget(self.id)

        # pw
        pwbox = QHBoxLayout()
        pwbox.addWidget(QLabel('PW'))
        self.pw = QLineEdit()
        pwbox.addWidget(self.pw)

        vbox = QVBoxLayout()
        vbox.addLayout(idbox)
        vbox.addLayout(pwbox)
        self.btn = QPushButton('회원가입')
        vbox.addWidget(self.btn)
        joinbox.setLayout(vbox)

        # check member
        membox = QGroupBox('가입된 회원 정보')
        self.table = QTableWidget()
        label = ('ID', 'PW')
        self.table.setColumnCount(len(label))
        self.table.setHorizontalHeaderLabels(label)
        self.table.setAlternatingRowColors(True)
        vbox = QVBoxLayout()
        vbox.addWidget(self.table)
        membox.setLayout(vbox)

        # all layout
        vbox = QVBoxLayout()
        vbox.addWidget(joinbox)
        vbox.addWidget(membox)

        self.setLayout(vbox)

    def readFile(self):
        try:
            f = open(JoinWidget.FILE_NAME, 'r', encoding='utf-8')
        except FileNotFoundError as e:
                print(e)
                f = open(JoinWidget.FILE_NAME, 'w', encoding='utf-8')
                f.close()
        else:
            row = self.table.rowCount()
            while True:
                line = f.readline()

                if not line:
                    break

                line = line.replace('\n', '')
                id, pw = line.split(' ')
                self.table.setRowCount(row + 1)
                self.table.setItem(row, 0, QTableWidgetItem(id))
                self.table.setItem(row, 1, QTableWidgetItem(pw))
                row += 1
            f.close()

    def writeFile(self, id, pw):
        with open(JoinWidget.FILE_NAME, 'a', encoding='utf-8') as f:
            member = f'{id} {pw}\n'
            f.write(member)

    def onJoin(self):
        id = self.id.text()
        pw = self.pw.text()

        if id and pw and not self.findIDs(id):
            row = self.table.rowCount()
            self.table.setRowCount(row+1)
            self.table.setItem(row, 0, QTableWidgetItem(id))
            self.table.setItem(row, 1, QTableWidgetItem(pw))
            self.writeFile(id,pw)
            self.id.clear()
            self.pw.clear()
        else:
            QMessageBox.information(self, 'Error!', '아이디(중복), 비번을 입력하세요!', QMessageBox.Ok)

    def findIDs(self, id):
        row = self.table.rowCount()
        bOverlap = False
        for r in range(row):
            id_item = self.table.item(r, 0)
            if id==id_item.text():
                bOverlap = True
                break

        return bOverlap


[라인 1~2], import 구문

회원가입을 진행하는데 필요한 컨트롤 (버튼, ID 입력창 등) 을 불러오는 부분입니다.


[라인 3~15], __init__()

QWidget에서 상속받은 JoinWidget의 생성자 함수입니다.

회원가입시 받은 ID, PW를 저장할 파일 경로 Class 변수, FILE_NAME을 선언합니다.

참고로 클래스 변수는 해당 클래스의 모든 객체가 공유하게 됩니다. C++을 공부한 경험이 있다면 클래스 Static 변수와 비슷합니다.

initUi() 함수를 호출해 위젯에 컨트롤을 생성해 배치하고, readFile() 함수를 이용 기존에 저장된 ID, PW가 있다면 파일에서 불러와 QTableWidget (가입된 회원정보) 에 표시합니다.


[라인 17~56], initUi()

회원가입에 필요한 컨트롤을 생성해 위젯에 배치하는 함수이며, Qt Designer를 쓰지 않고 코드에서 컨트롤을 직접 생성하는 방식이라 코드가 좀 길어보입니다.

ID입력, PW입력을 담당하는 QLineEdit 및 로그인버튼 역할의 QPushButton 을 생성해 QGroupBox에 배치합니다.

이어서 가입된 회원정보를 표시하기 위한 QTableWidget을 추가합니다.

여기서 생성된 모든 컨트롤은 위젯 Resize이벤트 발생시 자동 크기 변경을 위해 수평, 수직 레이아웃 박스에 배치해 구성하도록 구현하였습니다.


[라인 58~79], readFile()

앱이 실행될때 ID와 PW를 저장할 "Member.txt" 파일이 없다면 생성하고, 이미 존재한다면 파일에 쓰여진 ID, PW를 읽어와 QTableWidget에 표시합니다.

앱이 최초 실행될 때 파일이 존재하지 않을수도 있으므로, 파이썬의 오류처리 구문 (try, except, else) 을 이용해 처리하도록 합니다.


[라인 81~84], writeFile()

회원가입시 설정한 ID, PW를 전달인자로 받아 파일에 기록하는 역할을 담당합니다.

ID, PW는 아래와 같이 공백을 기준으로 구분해 저장됩니다.

[Member.txt 파일]


[라인 86~99], onJoin()

ID, PW를 설정하고 회원가입 버튼을 누를때 호출되는 슬롯함수이며, 중복된 아이디가 있는지 확인하고 없다면 해당 ID, PW를 QTableWidget에 추가하고 Member.txt 파일에 기록하는 역할을 담당합니다.

참고로 슬롯함수는 Qt에서 Signal 발생시 호출되는 함수를 의미합니다.


[라인 101~110], findIDs()

회원 가입시 중복된 아이디가 있는지 검사해 그 여부를 리턴해주는 함수입니다.


login.py 소스코드

로그인을 담당하는 위젯입니다.

from PyQt5.QtWidgets import (QWidget, QLabel, QLineEdit, QPushButton, QSpacerItem,
                             QSizePolicy, QHBoxLayout, QVBoxLayout, QMessageBox)
from PyQt5.QtCore import pyqtSignal


class LoginWidget(QWidget):

    close_signal = pyqtSignal()

    def __init__(self, parent, filename):
        super().__init__()
        self.setWindowTitle('로그인')
        self.parent = parent
        self.fn = filename
        self.cnt = 0

        self.initUi()

        #signal
        self.btn.clicked.connect(self.onLogin)
        self.close_signal.connect(self.parent.onFail)

    def initUi(self):
        # id
        idbox = QHBoxLayout()
        idbox.addWidget(QLabel('ID'))
        self.id = QLineEdit()
        idbox.addWidget(self.id)

        # pw
        pwbox = QHBoxLayout()
        pwbox.addWidget(QLabel('PW'))
        self.pw = QLineEdit()
        self.pw.setEchoMode(QLineEdit.Password)
        pwbox.addWidget(self.pw)

        vbox = QVBoxLayout()
        vbox.addLayout(idbox)
        vbox.addLayout(pwbox)

        self.btn = QPushButton('로그인')
        vbox.addWidget(self.btn)

        spacer = QSpacerItem(0, 0, QSizePolicy.Minimum, QSizePolicy.Expanding)
        vbox.addItem(spacer)
        self.setLayout(vbox)

    def onLogin(self):
        id = self.id.text()
        pw = self.pw.text()

        if id and pw:
            member = self.readFile()
            bFind = False
            for _id, _pw in member:
                if id == _id and pw == _pw:
                    bFind = True
                    break

            if bFind:
                QMessageBox.information(self, '환영합니다!', '로그인 성공', QMessageBox.Ok)
            else:
                self.cnt += 1
                txt = f'로그인 {self.cnt} 회 실패'
                QMessageBox.critical(self, 'ID, PW가 맞지 않습니다!', txt, QMessageBox.Ok)
                if self.cnt>=3:
                    self.close_signal.emit()

    def readFile(self):
        member = []
        with open(self.fn, 'r', encoding='utf-8') as f:
            while True:
                line = f.readline()

                if not line:
                    break

                line = line.replace('\n', '')
                id, pw = line.split(' ')
                member.append((id, pw))

        return member

 

[라인 1~3], import 구문

로그인을 진행하는데 필요한 컨트롤 (버튼, ID 입력창 등) 을 불러오는 부분입니다. 

pyqtSignal Class는 사용자 정의 시그널 (User Defined Signal)을 생성하는데 사용되는 클래스이며, 로그인 3회 실패 시 앱을 종료할 목적으로 사용할 계획입니다.

차후 로그인 3회 실패시, main.py에 선언된 Form 클래스로 사용자 정의 신호를 송출(Emit) 하게 됩니다.


[라인 6~21], __init__()

LoginWidget 클래스의 생성자 함수입니다.

8번 라인에서 로그인 3회 실패시 사용할 사용자 정의 신호를 생성합니다.

이어지는 생성자 함수에서 전달인자로 받은 Form 위젯을 향후 사용자 정의 신호를 연결하기 위해 parent라는 이름으로 저장하고 파일이름도 저장해 둡니다.

계속해서 initUi() 함수를 호출해 로그인에 필요한 컨트롤을 생성합니다.

 

[라인 23~46], initUi()

ID, PW를 입력할 QLineEidt, QPushButton 등을 생성하고 레이아웃 클래스를 이용, 위젯에 배치합니다.

로그인 위젯은 회원가입 위젯에 비해 컨트롤의 수가 적어 횡~한 느낌이 들지 않도록 QSpacerItem을 이용해 위젯의 아래부분에 공백을 추가합니다.

[QSpacerItem 예시]

[라인 48~67], onLogin()

로그인 버튼을 눌렀을때 호출되는 슬롯함수입니다.

먼저 ID, PW 가 적힌 QLineEidt의 text() 함수를 이용해 사용자가 적은 ID, PW를 얻습니다.

이어서 Member.txt 파일에 저장된 ID, PW와 입력한 ID, PW가 일치하는지 비교해 로그인 여부를 결정합니다.

3회 이상 틀린 경우는 보안 강화를 위해 앞서 만들어둔 사용자 정의 신호를 Form 클래스로 보내 앱이 종료되도록 합니다.


[라인 69~82], readFile()

Member.txt 파일에서 ID, PW를 문자열 파싱, 리스트로 변환해 리턴합니다.

이 리스트는 로그인 시도시 일치하는 ID, PW가 있는지 비교하는 용도로 사용됩니다.


이상으로 모든 설명을 마칩니다.

감사합니다.


개발 환경

  • Windows 10 Pro 64bit

  • Python 3.8.8 64bit, Pycharm

  • PyQt5 5.15.3

댓글

  1. 아이디와 비밀번호 말고 또 다른 것을 추가하고 싶을 땐 어떻게 해야 하나요?

    답글삭제
    답글
    1. 위 예제에서 아이디, 비번을 추가한 것과 같은 원리로 필요한 정보 (성별, 나이, 등) 를 코드에 추가해 진행하면 됩니다.

      삭제
    2. 혹시 어떻게 추가해야 하는지 정확히 알려주실 수 있을까요? 제가 해봤는데 자꾸 에러만 떠서요,,ㅜ

      삭제
  2. 혹시 어떻게 추가해야 하는지 정확히 알려주실 수 있을까요? 제가 해봤는데 자꾸 에러만 떠서요,,ㅜ

    답글삭제
  3. ValueError: not enough values to unpack (expected 3, got 2) 이렇게 에러가 뜹니다

    답글삭제
  4. 선생님 그대로 복붙해서 입력하니 에러가 납니다. 혹시 코드에 문제가 있는 걸까요?

    답글삭제
    답글
    1. 위 예제를 보고 아이디, 비번 외 다른 기능 (성별, 취미 등) 을 추가하는데 어려움을 겪는다는 것은 예제를 제대로 이해하지 못했다는 의미이며, 이 상황에서 다른 기능을 추가하는 것은 의미가 없습니다.

      위 예제부터 완벽히 이해하고 기능을 추가해 보기 바랍니다.

      그리고 unpack 에러는 대입연산자 좌변 변수들의 갯수가 우변의 Iterable 한 객체들을 모두 풀 수 없을 때 나는 에러 입니다.

      예) a, b ,c = [1, 2]

      삭제
    2. 그대로 했는데도, unpack 에러가 계속 뜨는데 해결 방법 없을까요?

      삭제
    3. 회원가입시 저장된 ID,PW는 File I/O를 통해 파일에 쓰여집니다.

      이때, ID or PW 에 공백이 들어가면 나중에 파일을 불러올 때 split() 구문에서 오류가 발생 할 수 있습니다.

      ID, PW는 공백이 없어야 하며, Memaber.txt 를 수정/삭제후 재시도 바랍니다.

      삭제
  5. 일정한 기간동안만 로그인 가능하게 하려면 어떻게 해야 하는걸까요 ??

    답글삭제
    답글
    1. 1. 아이디의 로그인 시점(시간)을 저장합니다.

      2. Thread를 생성하고 무한 반복하며 아래를 체크합니다.

      3. 무한 반복하며, 로그인된 사용자의 활동시간(예 1초씩)을 감소시킵니다.

      4. 활동시간이 경과한(활동시간 <= 0) 아이디를 찾아 로그아웃 합니다.

      삭제
  6. 어려웠는데 감사해요

    답글삭제

댓글 쓰기

이 블로그의 인기 게시물

Qt Designer 설치하기

PyQt5 기반 동영상 플레이어앱 만들기