스마트 객실 관리 시스템 프로젝트 완성

 

이번 프로젝트는 PySide6 GUIArduino 시리얼 통신을 연결해서 만든 스마트 객실 관리 시스템이다.

관리자가 로그인하면 객실 목록을 확인할 수 있고, 101호는 아두이노에서 조도 센서 값을 받아 객실 상태를 표시한다.

또한 관리 창에서 LED를 직접 켜고 끌 수 있도록 구성했다.

1. 프로젝트 전체 흐름

  1. main.py에서 프로그램을 실행한다.
  2. MainWindow가 생성되면서 로그인 탭과 객실 목록 탭을 만든다.
  3. 처음에는 로그인 상태가 아니므로 객실 목록 탭 접근이 제한된다.
  4. 관리자가 login.py 화면에서 아이디와 비밀번호를 입력한다.
  5. member_model.py에 저장된 관리자 정보와 비교해서 로그인 성공 여부를 확인한다.
  6. 로그인에 성공하면 객실 목록 탭으로 이동한다.
  7. 객실 목록에서 101호, 102호, 103호 중 하나를 선택한다.
  8. 101호는 아두이노 조도 값을 읽어 밝음, 보통, 어두움 상태를 표시한다.
  9. 관리하기 버튼을 누르면 LED를 켜고 끌 수 있는 관리 창이 열린다.

2. 파일별 역할 정리

순서 파일명 역할 설명
1 main.py 프로그램 실행 시작 파일 QApplication을 만들고 MainWindow를 실행한다.
2 member_model.py 관리자 정보 Model 로그인에 사용할 아이디, 비밀번호, 관리자 정보를 저장한다.
3 main_window.py 전체 화면과 탭 관리 로그인 탭과 객실 목록 탭을 만들고, 로그인하지 않으면 객실 목록 접근을 막는다.
4 login.py 관리자 로그인 화면 입력한 아이디와 비밀번호를 MemberModel과 비교하고 성공 시 객실 목록 탭으로 이동한다.
5 room_list.py 객실 목록 화면 101호, 102호, 103호 버튼을 보여주고 선택한 객실 상세창을 연다.
6 room_detail.py 객실 상세 화면 101호는 아두이노 조도 값을 읽어 객실 상태를 표시하고, 다른 객실은 미등록으로 표시한다.
7 arduino_manager.py 아두이노 연결 관리 시리얼 통신으로 조도 값을 읽고 LED 제어 명령을 보낸다.
8 set.py 객실 관리 창 LED 켜기/끄기 버튼을 통해 아두이노 LED를 제어한다.

3. 로직 구조 정리

실행 구조

main.py → MainWindow 생성 → 로그인 화면 생성 → 객실 목록 화면 생성

로그인 구조

login.py 입력값 → MemberModel.check_login() 검사 → 성공 시 is_login = True → 객실 목록 탭 이동

객실 관리 구조

room_list.py 객실 선택 → room_detail.py 상세 화면 → arduino_manager.py 조도 값 읽기 → set.py LED 제어

4. 각 파일별 주석 코드

main.py

# 프로그램 실행을 담당하는 시작 파일
# QApplication을 생성하고 MainWindow를 화면에 보여준다.
import sys
from PySide6.QtWidgets import QApplication
from main_window import MainWindow


# 이 파일을 직접 실행했을 때만 아래 코드가 실행된다.
if __name__ == "__main__":

    # PySide6 프로그램 실행을 위한 QApplication 객체 생성
    app = QApplication(sys.argv)

    # 메인 윈도우 객체 생성
    window = MainWindow()

    # 메인 윈도우 화면 출력
    window.show()

    # 프로그램 이벤트 루프 실행
    sys.exit(app.exec())

member_model.py

# 회원 정보를 저장하고 관리하는 Model 클래스
# 로그인 화면에서는 이 클래스에 저장된 아이디와 비밀번호를 기준으로 로그인 검사를 한다.
class MemberModel:

    # 객체가 생성될 때 자동으로 실행되는 생성자
    def __init__(self):

        # 관리자 아이디 저장
        self.user_id = "frontm"

        # 관리자 비밀번호 저장
        self.user_pw = "1234"

        # 관리자 이름 저장
        self.name = "프론트 마스터"

        # 관리자 이메일 저장
        self.email = "frontm@hotel.com"

        # 관리자 전화번호 저장
        self.phone = "010-1234-5678"

    # 로그인 검사를 위한 함수
    # 사용자가 입력한 아이디와 비밀번호를 전달받아
    # Model에 저장된 아이디, 비밀번호와 비교한다.
    def check_login(self, input_id, input_pw):

        # 입력한 아이디와 비밀번호가 모두 일치하면 로그인 성공
        if input_id == self.user_id and input_pw == self.user_pw:

            # 로그인 성공 결과를 Login View에 전달
            return True

        else:

            # 로그인 실패 결과를 Login View에 전달
            return False

    # 회원 정보를 반환하는 함수
    # 관리자 정보를 다른 화면에서 출력할 때 사용할 수 있다.
    def get_member_info(self):

        # 현재 Model에 저장된 회원 정보를 딕셔너리 형태로 반환한다.
        return {
            "id": self.user_id,
            "pw": self.user_pw,
            "name": self.name,
            "email": self.email,
            "phone": self.phone
        }

main_window.py

# 프로그램의 전체 창과 탭 이동을 관리하는 파일
# 로그인 탭과 객실 목록 탭을 만들고, 로그인 여부에 따라 접근을 제한한다.
from PySide6.QtWidgets import (
    QMainWindow,
    QTabWidget,
    QDialog,
    QLabel,
    QVBoxLayout,
    QPushButton
)

from PySide6.QtCore import Qt

from member_model import MemberModel
from login import login
from room_list import room


class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()

        # 메인 창 제목 설정
        self.setWindowTitle(
            "스마트 객실 관리 시스템"
        )

        # 메인 창 크기 설정
        self.resize(
            800,
            600
        )

        # 로그인 상태 저장 변수
        # 처음 실행할 때는 로그인하지 않은 상태이므로 False
        self.is_login = False

        # 회원 정보를 관리하는 Model 객체 생성
        self.member_model = MemberModel()

        # 탭 위젯 생성
        self.tab_widget = QTabWidget()

        # 생성한 탭 위젯을 메인 화면 중앙에 배치
        self.setCentralWidget(
            self.tab_widget
        )

        # 로그인 화면 생성
        # 로그인 화면에서 MemberModel과 탭 위젯, MainWindow 상태를 사용할 수 있도록 전달
        self.login_tab = login(
            self.member_model,
            self.tab_widget,
            self
        )

        # 객실 목록 화면 생성
        self.room_list_tab = room()

        # 첫 번째 탭에 로그인 화면 추가
        self.tab_widget.addTab(
            self.login_tab,
            "관리자 로그인"
        )

        # 두 번째 탭에 객실 목록 화면 추가
        self.tab_widget.addTab(
            self.room_list_tab,
            "객실 목록"
        )

        # 탭이 변경될 때마다 로그인 여부를 확인하는 함수 연결
        self.tab_widget.currentChanged.connect(
            self.check_tab_access
        )

    # 탭 이동 시 로그인 여부를 확인하는 함수
    def check_tab_access(self, index):

        # 로그인하지 않은 상태에서 로그인 탭이 아닌 다른 탭으로 이동하려고 하면 접근 제한
        if not self.is_login and index != 0:

            # 로그인 필요 안내창 생성
            dialog = QDialog(self)

            dialog.setWindowTitle(
                "로그인 필요"
            )

            dialog.resize(
                300,
                150
            )

            layout = QVBoxLayout()

            label = QLabel(
                "로그인 후 이용 가능합니다."
            )

            label.setAlignment(
                Qt.AlignCenter
            )

            button = QPushButton(
                "확인"
            )

            # 확인 버튼을 누르면 안내창 닫기
            button.clicked.connect(
                dialog.accept
            )

            layout.addWidget(
                label
            )

            layout.addWidget(
                button
            )

            dialog.setLayout(
                layout
            )

            # 안내창 실행
            dialog.exec()

            # 다시 로그인 탭으로 이동
            self.tab_widget.setCurrentIndex(
                0
            )

    # 로그인 성공 시 호출할 수 있는 함수
    def login_success(self):

        # 로그인 상태를 True로 변경
        self.is_login = True

        # 객실 목록 탭으로 이동
        self.tab_widget.setCurrentIndex(
            1
        )

login.py

# 관리자 로그인 화면을 구성하는 파일
# 사용자가 입력한 아이디와 비밀번호를 MemberModel과 비교한다.
from PySide6.QtWidgets import (
    QWidget,
    QVBoxLayout,
    QPushButton,
    QLabel,
    QLineEdit,
    QMessageBox
)


class login(QWidget):

    def __init__(self, member_model, tab_widget, main_window):
        super().__init__()

        # main_window.py에서 전달받은 MemberModel 저장
        # 로그인 검사 시 사용한다.
        self.member_model = member_model

        # 탭 이동을 위해 QTabWidget 저장
        self.tab_widget = tab_widget

        # 로그인 성공 상태를 MainWindow에 전달하기 위해 저장
        self.main_window = main_window

        # 아이디 입력 라벨과 입력창 생성
        label_id = QLabel("아이디 :")
        self.line_edit_id = QLineEdit()
        self.line_edit_id.setPlaceholderText("아이디를 입력하세요")

        # 비밀번호 입력 라벨과 입력창 생성
        label_pw = QLabel("비밀번호 :")
        self.line_edit_pw = QLineEdit()
        self.line_edit_pw.setPlaceholderText("비밀번호를 입력하세요")

        # 비밀번호 입력값이 화면에 그대로 보이지 않도록 설정
        self.line_edit_pw.setEchoMode(QLineEdit.EchoMode.Password)

        # 로그인 버튼 생성
        login_button = QPushButton("로그인")

        # 버튼 클릭 시 login_button_clicked 함수 실행
        login_button.clicked.connect(self.login_button_clicked)

        # 세로 레이아웃 생성
        layout = QVBoxLayout()

        # 레이아웃에 위젯 추가
        layout.addWidget(label_id)
        layout.addWidget(self.line_edit_id)
        layout.addWidget(label_pw)
        layout.addWidget(self.line_edit_pw)
        layout.addWidget(login_button)

        # 현재 Login View에 레이아웃 적용
        self.setLayout(layout)

    # 로그인 버튼을 눌렀을 때 실행되는 함수
    def login_button_clicked(self):

        # 아이디 입력창에 입력된 값을 가져온다.
        input_id = self.line_edit_id.text()

        # 비밀번호 입력창에 입력된 값을 가져온다.
        input_pw = self.line_edit_pw.text()

        # MemberModel의 check_login 함수로 로그인 성공 여부 확인
        if self.member_model.check_login(input_id, input_pw):

            # 로그인 성공 팝업 출력
            QMessageBox.information(
                self,
                "로그인 성공",
                "로그인에 성공하였습니다."
            )

            # MainWindow의 로그인 상태를 True로 변경
            self.main_window.is_login = True

            # 로그인 성공 후 객실 목록 탭으로 이동
            self.tab_widget.setCurrentIndex(1)

        else:

            # 로그인 실패 팝업 출력
            QMessageBox.warning(
                self,
                "로그인 실패",
                "아이디 또는 비밀번호가 올바르지 않습니다."
            )

            # 입력된 아이디와 비밀번호 삭제
            self.line_edit_id.clear()
            self.line_edit_pw.clear()

            # 다시 아이디 입력창에 커서 위치
            self.line_edit_id.setFocus()

room_list.py

# 객실 목록 화면을 구성하는 파일
# 객실 버튼을 누르면 해당 객실 상세 화면을 새 창으로 띄운다.
from PySide6.QtWidgets import (
    QWidget,
    QVBoxLayout,
    QLabel,
    QPushButton
)

from PySide6.QtCore import Qt

from room_detail import roomdetail


class room(QWidget):

    def __init__(self, parent=None):
        super().__init__(parent)

        # 세로 레이아웃 생성
        layout = QVBoxLayout()

        # 객실 목록 제목 생성
        title = QLabel("객실 목록")
        title.setAlignment(Qt.AlignCenter)
        title.setStyleSheet("font-size: 20px; font-weight: bold;")

        # 객실 버튼 생성
        # 101호는 아두이노 시스템이 등록된 객실로 사용
        self.btn_101 = QPushButton("101호 - 시스템 등록")
        self.btn_102 = QPushButton("102호 - 시스템 미등록")
        self.btn_103 = QPushButton("103호 - 시스템 미등록")

        # 버튼 높이 설정
        self.btn_101.setFixedHeight(40)
        self.btn_102.setFixedHeight(40)
        self.btn_103.setFixedHeight(40)

        # 각 버튼 클릭 시 해당 객실 상세 화면을 여는 함수 연결
        self.btn_101.clicked.connect(self.open_room_101)
        self.btn_102.clicked.connect(self.open_room_102)
        self.btn_103.clicked.connect(self.open_room_103)

        # 화면 가운데에 배치되도록 위젯 추가
        layout.addStretch()
        layout.addWidget(title)
        layout.addSpacing(30)
        layout.addWidget(self.btn_101)
        layout.addWidget(self.btn_102)
        layout.addWidget(self.btn_103)
        layout.addStretch()

        # 현재 객실 목록 화면에 레이아웃 적용
        self.setLayout(layout)

    # 101호 상세 화면 열기
    def open_room_101(self):
        self.detail = roomdetail("101")
        self.detail.show()

    # 102호 상세 화면 열기
    def open_room_102(self):
        self.detail = roomdetail("102")
        self.detail.show()

    # 103호 상세 화면 열기
    def open_room_103(self):
        self.detail = roomdetail("103")
        self.detail.show()

room_detail.py

# 객실 상세 화면을 구성하는 파일
# 101호는 아두이노에서 조도 값을 읽어오고, 나머지 객실은 미등록 상태로 표시한다.
from PySide6.QtWidgets import (
    QWidget,
    QVBoxLayout,
    QLabel,
    QPushButton
)

from arduino_manager import arduinomanager
from set import setwindow


class roomdetail(QWidget):

    def __init__(self, room_number):
        super().__init__()

        # 객실 번호 저장
        self.room_number = room_number

        # 아두이노 관리 객체 생성
        # 조도 값 읽기와 LED 제어에 사용한다.
        self.arduino = arduinomanager()

        # 세로 레이아웃 생성
        layout = QVBoxLayout()

        # 객실 번호 표시 라벨
        self.label_room = QLabel(
            f"객실 번호 : {room_number}"
        )

        # 조도 값 표시 라벨
        self.label_light = QLabel(
            "현재 조도 : "
        )

        # 객실 상태 표시 라벨
        self.label_status = QLabel(
            "객실 상태 : "
        )

        # 객실 관리 창을 여는 버튼
        self.btn_manage = QPushButton(
            "관리하기"
        )

        # 버튼 클릭 시 open_manage 함수 실행
        self.btn_manage.clicked.connect(
            self.open_manage
        )

        # 레이아웃에 위젯 추가
        layout.addWidget(self.label_room)
        layout.addWidget(self.label_light)
        layout.addWidget(self.label_status)
        layout.addWidget(self.btn_manage)

        # 현재 객실 상세 화면에 레이아웃 적용
        self.setLayout(layout)

        # 화면이 열릴 때 객실 정보를 불러온다.
        self.load_room_info()

    # 객실 번호에 따라 조도와 상태를 표시하는 함수
    def load_room_info(self):

        # 101호는 시스템 등록 객실이므로 아두이노 조도 값을 읽어온다.
        if self.room_number == "101":

            # 아두이노에서 조도 값 읽기
            light = self.arduino.get_light_value()

            # 조도 값 기준으로 객실 상태 분류
            if light >= 850:
                status = "밝음"

            elif light >= 600:
                status = "보통"

            else:
                status = "어두움"

            # 조도 값 화면에 출력
            self.label_light.setText(
                f"현재 조도 : {light}"
            )

            # 객실 상태 화면에 출력
            self.label_status.setText(
                f"객실 상태 : {status}"
            )

        else:

            # 102호, 103호는 시스템 미등록 객실로 표시
            self.label_light.setText(
                "현재 조도 : 시스템 미등록"
            )

            self.label_status.setText(
                "객실 상태 : 시스템 미등록"
            )

    # 관리하기 버튼 클릭 시 실행되는 함수
    def open_manage(self):

        # LED 제어 창 생성
        # 같은 아두이노 객체를 전달해서 연결을 재사용한다.
        self.manage = setwindow(
            self.arduino
        )

        # 관리 창 화면 출력
        self.manage.show()

arduino_manager.py

# 아두이노와 Python 프로그램을 연결하는 파일
# 시리얼 통신을 이용해 조도 값을 읽고 LED ON/OFF 명령을 보낸다.
import serial
import time


class arduinomanager:

    def __init__(self, port="/dev/ttyACM0", baudrate=9600):

        # 아두이노가 연결된 포트 저장
        self.port = port

        # 아두이노 통신 속도 저장
        self.baudrate = baudrate

        # 실제 아두이노 연결 객체를 저장할 변수
        # 처음에는 연결되지 않았으므로 None
        self.arduino = None

    # 아두이노와 시리얼 연결을 생성하는 함수
    def connect(self):

        # 아직 연결되지 않았을 때만 새로 연결한다.
        if self.arduino is None:
            self.arduino = serial.Serial(
                self.port,
                self.baudrate
            )

            # 아두이노 연결이 안정화될 때까지 잠시 대기
            time.sleep(3)

    # 아두이노에서 조도 센서 값을 읽어오는 함수
    def get_light_value(self):

        # 값을 읽기 전에 아두이노 연결 확인
        self.connect()

        # 숫자 값이 정상적으로 들어올 때까지 반복해서 읽는다.
        while True:
            value = (
                self.arduino.readline()
                .decode("utf-8", errors="ignore")
                .strip()
            )

            # 읽어온 값이 숫자라면 int로 변환해서 반환
            if value.isdigit():
                return int(value)

    # LED 켜기 명령을 아두이노로 전송
    def led_on(self):
        self.connect()
        self.arduino.write(b"1")

    # LED 끄기 명령을 아두이노로 전송
    def led_off(self):
        self.connect()
        self.arduino.write(b"0")

set.py

# 객실 관리 창을 구성하는 파일
# 버튼을 이용해 아두이노 LED를 켜고 끌 수 있다.
from PySide6.QtWidgets import (
    QWidget,
    QVBoxLayout,
    QLabel,
    QPushButton
)


class setwindow(QWidget):

    def __init__(self, arduino):
        super().__init__()

        # room_detail.py에서 전달받은 아두이노 객체 저장
        self.arduino = arduino

        # 관리 창 제목 설정
        self.setWindowTitle("객실 관리")

        # 세로 레이아웃 생성
        layout = QVBoxLayout()

        # 현재 관리 상태를 보여주는 라벨
        self.label_status = QLabel(
            "관리하기"
        )

        # LED ON/OFF 버튼 생성
        self.btn_on = QPushButton(
            "LED 켜기"
        )

        self.btn_off = QPushButton(
            "LED 끄기"
        )

        # 버튼 클릭 시 각각 led_on, led_off 함수 연결
        self.btn_on.clicked.connect(
            self.led_on
        )

        self.btn_off.clicked.connect(
            self.led_off
        )

        # 레이아웃에 위젯 추가
        layout.addWidget(self.label_status)
        layout.addWidget(self.btn_on)
        layout.addWidget(self.btn_off)

        # 현재 관리 창에 레이아웃 적용
        self.setLayout(layout)

    # LED 켜기 버튼 클릭 시 실행
    def led_on(self):

        # 아두이노로 LED ON 명령 전송
        self.arduino.led_on()

        # 화면 상태 문구 변경
        self.label_status.setText(
            "현재 LED 상태 : ON"
        )

    # LED 끄기 버튼 클릭 시 실행
    def led_off(self):

        # 아두이노로 LED OFF 명령 전송
        self.arduino.led_off()

        # 화면 상태 문구 변경
        self.label_status.setText(
            "현재 LED 상태 : OFF"
        )

 

5. 아두이노 코드

파이썬과 아두이노를 연동하기 

이번 프로젝트에서는 Python에서 Arduino 값을 읽어오기 위해 시리얼 통신 환경을 구성하였다.

Python에서 Arduino와 통신하기 위해서는 pyserial 라이브러리가 필요하기 때문에 가상 환경을 생성한 후 설치를 진행하였다.


1. 프로젝트 폴더 이동

먼저 프로젝트 폴더로 이동하였다.

cd 파일명

2. 가상 환경 생성

Python 패키지 관리를 위해 가상 환경을 생성하였다.

python3 -m venv .venv

3. 가상 환경 활성화

생성한 가상 환경을 활성화하였다.

source .venv/bin/activate

정상적으로 활성화되면 터미널 앞에 (.venv) 표시가 나타난다.

4. pyserial 설치

Arduino와 Python 간 시리얼 통신을 위해 pyserial 라이브러리를 설치하였다.

pip install pyserial

설치가 완료되면 다음과 같이 출력된다.

Successfully installed pyserial-3.5

5. 설치 확인

설치된 패키지를 확인하였다.

pip list

결과에서 pyserial 3.5가 확인되면 정상적으로 설치된 것이다.

 

6. Arduino 포트 확인

Arduino가 정상적으로 연결되었는지 확인하기 위해 연결된 시리얼 포트를 조회하였다.

ls /dev/ttyACM*

결과는 다음과 같이 출력되었다.

/dev/ttyACM0

현재 Arduino가 Linux에서 /dev/ttyACM0 포트로 인식되고 있음을 확인하였다.


7. Python에서 Arduino 값 읽기 예시

이후 Python 코드에서는 serial.Serial()을 사용하여 Arduino와 연결할 수 있다.

import serial

arduino = serial.Serial(
    "/dev/ttyACM0",
    9600
)

value = arduino.readline().decode().strip()

print(value)

위 코드를 이용하면 Arduino에서 전송한 센서 값을 Python에서 읽어올 수 있다.


정리

  • 프로젝트 폴더로 이동하였다.
  • Python 가상 환경을 생성하였다.
  • 가상 환경을 활성화하였다.
  • Arduino 통신을 위해 pyserial을 설치하였다.
  • pip list로 설치 여부를 확인하였다.
  • ls /dev/ttyACM* 명령어로 Arduino 포트를 확인하였다.

이번 작업을 통해 Python과 Arduino가 시리얼 통신을 할 수 있는 기본 환경을 구성하였다.

 

프로젝트 마무리 정리

이 프로젝트는 단순히 화면만 만든 것이 아니라, 로그인 기능과 객실 목록, 객실 상세 정보, 아두이노 센서 값 읽기, LED 제어까지 연결한 구조이다.

특히 101호만 시스템 등록 객실로 설정해서 실제 아두이노 조도 값을 받아오고, 102호와 103호는 시스템 미등록 상태로 구분했다.

전체적으로 Model, View, Arduino Manager를 나누어 구성했기 때문에 기능별 역할이 비교적 명확하게 분리되어 있다.

 

스크린캐스트 2026-06-08 16-32-27.webm
0.95MB