테이블 모델에서의 참조를 용이하게 하기 위해서 위 처럼 인덱스를 사용한 get, set을 구현
db액션 수정
# 화주 명단 가져오기
def get_daycal_owner_list():
return session.query(DayCalOwner).order_by(DayCalOwner.id).all()
# 화주별 데이터 가져오기
def get_daycal_owner_values(today = None):
if today:
return session.query(DayCalOwnerValues).filter(DayCalOwnerValues.date == today).order_by(DayCalOwnerValues.owner_id).all()
today = date.today()
values = []
owner_list = get_daycal_owner_list()
for owner in owner_list:
id = owner.get(0)
name = owner.get(1)
value = session.query(DayCalOwnerValues).filter(and_(DayCalOwnerValues.owner_id == id, DayCalOwnerValues.date == today)).first()
if not value:
value = DayCalOwnerValues(today, id, name)
session.add(value)
session.commit()
values.append(value)
return values
# 기타 데이터 가져오기
def get_daycal_other_values(today = date.today()):
value = session.query(DayCalOtherValues).filter(DayCalOtherValues.date == today).first()
if not value:
value = DayCalOtherValues(today)
session.add(value)
session.commit()
return value
# 결과 데이터 가져오기
def get_daycal_result(today = date.today()):
value = session.query(DayCalResult).filter(DayCalResult.date == today).first()
if not value:
value = DayCalResult(today)
session.add(value)
session.commit()
return value
DB에서 쿼리를 통해 가져온 객체를 to_list를 호출하여 컬럼값이 들어있는 리스트의 형태로 반환하는 기존의 방식에서 객체 자체를 반환하는 방식으로 변경.
테이블 모델 수정
def data(self, index, role):
if index.isValid():
if role == Qt.DisplayRole:
value = self.table_data[index.column()].get(index.row())
return format(value, ',')
elif role == Qt.EditRole:
value = self.table_data[index.column()].get(index.row())
return value
elif role == Qt.TextAlignmentRole:
return int(Qt.AlignRight | Qt.AlignVCenter)
def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...):
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
return self.owner_list[section].get(1)
else:
return self.vertical_header[section]
return None
def setData(self, index: QModelIndex, value: int, role):
if role == Qt.EditRole:
r, c = index.row(), index.column()
self.changed(r, c, self.table_data[c].get(r), value)
self.table_data[c].set(r, value)
return True
return False
기존에 2차원 리스트 형태였던 데이터들이 객체의 배열로 변경되면서 참조 방식도 getter를 사용하도록 수정
이렇게 객체를 직접 수정하는 방식을 사용하는 것으로 모델을 사용하는 의미도 퇴색되지 않으며 저장기능의 구현도 훨씬 편해진다.
저장 액션 구현
def save():
session.commit()
테이블에서 쿼리로 가져온 객체를 직접 수정하도록 변경한 덕분에 단순히 session.commit()을 호출하는 것으로 모든 변경사항이 DB에 반영된다.
저장 액션 메뉴와 툴바에 추가
# 저장 액션 추가
save_data = QAction(QIcon('src/img/save_icon.png'), '저장하기', self)
save_data.setShortcut('Ctrl+S')
save_data.setStatusTip('화주 이름 변경')
save_data.triggered.connect(actions.save)
file_menu.addAction(save_data)
tool_bar.addAction(save_data)
네트워크 계층에서 봤던 추상화된 형태와는 달리 실제로 라우터와 호스트, 라우터와 라우터의 사이에는 전용 회선이 존재하는 것이 아닌 경우가 많음
실제로는 사이에 다른 호스트나 라우터와 함께 사용하는 공용 채널이 존재하는 경우가 대부분이며 이러한 매체를 Broadcast Medium 이라 한다.
2. MAC(Medium Access Control) 프로토콜
송신한 데이터가 공용 채널에서서로 충돌하여 쓰레기 데이터가 되는 것을 막을 방법이 필요 => Medium Access (Control) Protocol - MAC => WIFI 도 공기를 Broadcast Medium으로 하는 MAC 프로토콜의 일종(802.11)
이상적인 MAC 프로토콜의 지향점(링크 대역폭이 R일 때)
하나의 노드만이 통신을 수행할 경우 대역폭 전체(R)를 사용할 수 있다.
N 개의 노드가 통신을 수행할 경우 대역폭 전체를 N으로 나눈 R/N 만큼의 대역폭을 사용할 수 있다.
통신을 관리하는 특별한 노드가 존재하지 않는다(모든 노드가 동등한 입장)
단순하게 동작해야한다.
접근법
Partitioning
TDMA(Time Division Multiple Access)
각 노드에게 회선을 사용할 수 있는 시간 슬롯을 배정하는 방식
주어진 시간동안은 모든 주파수 대역을 사용할 수 있음
동시 사용자가 적을 경우 시간 슬롯이 낭비될 수 있음
FDMA(Frequency Division Multiple Access)
각 노드에게 주파수 대역을 나누어 배정하는 방식
아날로그 방식으로 구현이 간단함
인접채널간 간섭문제를 해결하기 위해 주파수 대역사이에 가드밴드가 필요해진다 => 가드밴드로 인해 낭비되는 주파수대역 생김
주파수 대역이나 시간 슬롯이 낭비되지 않는 경우(사용자가 많은 경우)에 유리
Random Access
Partition 방식의 경우 충돌방지는 확실하게 되지만 주파수대역 낭비나 시간슬롯 낭비의 문제가 있음
노드들이 원하는 타이밍에 통신을 수행하는 방식
사용자가 적을 경우 Partitioning에 비해 효율적
CSMA(Carrier Sense Multiple Access)
공용 채널이 사용되고 있지 않다고 판단되면 통신을 실시하는 방식
충돌(Collision)
한 노드가 다른 노드가 통신을 먼저 시작한것을 인식하기 전에 통신을 시작한 경우 발생하는 충돌을 미연에 방지하는 것은 불가능.
충돌이 발생하는 것을 전제로 충돌로 인한 피해를 최소화하는 방식을 사용 => CSMA/CD(Collision Detection) => 한쪽이 충돌발생을 탐지하고 바로 전송을 멈춘다 => 랜덤 타이머를 설정하고 타임아웃시 재전송 => 충돌이 발생할 때마다 타이머의 시간을 두 배씩 늘려간다
유선통신의 경우 충돌 감지 자체는 쉽게 가능
Taking Turns
polling
하나의 중심 호스트(master)가 연결된 다른 호스트(server)들에게 전송할 데이터가 있는지 확인하여 전송하도록 하는 방식
token passing
token을 가진 호스트만이 전송을 할 수 있도록 하는 방식
두 방법 모두 한가지 문제가 발생하면(master의 고장, token의 유실 등) 치명적인 문제가 발생하는 single point failure 문제로 실제로는 잘 사용되지 않음
3. 이더넷(Ethernet)
유선 케이블 상황에서의 MAC 프로토콜
Random Access 방식을 사용(CSMA/CD)
이더넷 프레임
Preamble / SFD
비트동기, 프레임동기를 위한 부분
Physical Layer의 헤더부분
DA(Desination Address) / SA(Source Address)
목적지와 출발지의 MAC 주소
Len/Type
값이 0x0600 미만이면 프레임의 데이터 영역(Data + Padding)의 길이로 해석
0x0600 이상이면 Data부분에 담겨있는 상위 프로토콜의 타입으로 해석 => ARP 쿼리, ARP 리스폰스 등
Data
IP 패킷이 들어있는 부분
뒤에 따라오는 Padding 부분은 0으로 채워진다.
FCS(Frame Check Sequence)
수납된 데이터의 에러 검출을 위한 CRC
신뢰성있는 전송
전송에 대한 피드백이 없는데 전송이 정상적으로 완료됐는지 확인하는방법 => 외부 간섭이 없는 유선연결이기에 회선(채널)내에서의 충돌만 없다면 전송된것으로 볼 수 있음
충돌을 탐지하기 전에 전송이 종료되면 전송에 실패한 데이터를 재전송하지 못함 => 전송측이 충돌을 탐지하기 위한 충분한 시간을 기다려줄 필요가 있음 => 전송할 프레임의 길이가 충분하면 충돌을 감지할 수 있음 => 이더넷은 이를 위한 프레임의 최소길이를 지정
보내려는 데이터의 크기가 프레임 최소길이보다 작다면? => 0으로 채워진 padding 부분을 붙여 최소길이를 만족시킴
MAC 주소와 ARP
MAC 주소
네트워크 랜카드가 가지고있는 고유 주소
랜카드가 제조된 시점부터 바뀌지 않는 값
ARP(Address Resolution Protocol)
IP 주소를 물리적 네트워크 주소에 바인딩하기 위한 프로토콜
모든 호스트는 포워딩 테이블을 통해 IP패킷을 보낼 다음 목적지(next hop)를 알 수 있지만 그 IP에 해당하는 MAC 주소를 알지 못해 링크 레이어 레벨에서의 전송이 불가능하다. 이를 해결하기 위해서 ARP 프로토콜을 통해 IP주소에 해당하는 MAC 주소를 알아내야한다.
절차
전송측은 프레임의 데이터/타입 영역에 IP패킷 대신 목적지 IP주소에 해당하는 MAC 주소를 요청하는 ARP 쿼리를 담아 네트워크상에 존재하는 모든 호스트들에게 브로드캐스트
전송측이 보낸 ARP 쿼리를 받은 호스트중 자신의 IP가 쿼리에서 원하는 IP주소와 일치하는 호스트는 자신의 프레임의 데이터/타입 영역에 ARP 응답(리스폰스)을 담아 전송하는 것으로 자신의 MAC 주소를 알림
전송측은 얻은 MAC 주소를 ARP 캐시(테이블)에 저장하고 이를 참조하여 목적지 IP에 해당하는 MAC 주소로 IP패킷을 담은 이더넷 프레임을 전송
4. 이더넷 네트워크의 물리적 형태
버스(Bus)
90년대 중반까지 주로 사용되었던 형태
모든 노드가 하나의 충돌 도메인을 가지기에 호스트들간에 충돌이 발생할 수 있음
스타(Star)
현재 주로 사용되고있는 형태
여러 호스트들의 중심에 스위치가 존재하며 스위치와 각 호스트들 사이에는 전용 회선이 존재
스위치는 연결된 호스트들로부터 전송된 프레임을 버퍼에 담아두고 처리해나간다.
스위치의 각 인터페이스가 어느 호스트와 연결되어있는지를 학습해나가며 스위치 포워딩 테이블을 생성하여 이를 토대로 프레임들의 전송을 처리한다.
학습의 절차
프레임을 전송받은 순간 전송한 호스트의 위치를 파악, 테이블에 저장
목적지의 위치를 모를 경우 flooding(브로드캐스트) 을 통해 위치를 알아내고 테이블에 저장
테이블을 참조하여 해당하는 목적지로 프레임을 전송
처음에는 테이블을 채워나가기 위해 flooding 이 발생하지만 시간이 조금 지나고나면 테이블이 완성되어 바로 포워딩을 할 수 있다.
화주가 추가되고 그에따라 화주별 데이터도 추가되어 데이터베이스에 반영되지만 데이터베이스에서 데이터를 읽어오는 것은 프로그램 실행시에만 수행하는 작업이며 반영된 데이터를 굳이 데이터베이스에서 다시 읽어오는것도 비효율적이기에 모델의 메소드를 호출하여 즉각 반영하도록 하였다.
added 와 removed 의 경우 테이블 자체가 변경되는 작업이기에 (열의 추가/삭제) beginInsertColumns, beginRemoveColumns 등 테이블의 변경작업이 시작됨을 알리는 메소드를 호출한 뒤 작업을 수행하고 마지막에 endInsertColumns, endRemoveColumns 등 테이블의 변경작업이 끝났음을 알리는 메소드를 호출해줘야 변경사항이 정상적으로 반영된다.
modified 의 경우 단순히 값을 수정하는 것 뿐이기에 header 리스트를 수정해주는 것만으로 간단하게 반영된다.
3. other_table 테이블 모델 작성
import operator
from PySide6.QtCore import QAbstractTableModel, Qt, SIGNAL, QModelIndex
from PySide6.QtGui import *
''' 생략 '''
class DayCalOthersTableModel(QAbstractTableModel):
def __init__(self, parent, data, *args):
QAbstractTableModel.__init__(self, parent, *args)
self.setParent(parent)
self.table_data = [data]
self.vertical_header = ['경매 사무실 입금', '가라경매 강동 입금', '직접 지출', '우리 경매', '강동 사입']
self.row_count = len(self.vertical_header)
self.column_count = 1
def rowCount(self, parent):
return self.row_count
def columnCount(self, parent):
return self.column_count
def data(self, index, role):
if index.isValid():
if role == Qt.DisplayRole or role == Qt.EditRole:
value = self.table_data[index.column()][index.row()]
return str(value)
def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...):
if role == Qt.DisplayRole:
if orientation == Qt.Vertical:
return self.vertical_header[section]
return None
def flags(self, index):
return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
def setData(self, index, value, role):
if role == Qt.EditRole:
self.table_data[index.column()][index.row()] = value
return True
return False
''' 생략 '''
other_table 의 테이블 모델도 기본적으로 input_table의 그것과 거의 동일하게 구현
horizontal header가 필요하지 않기 때문에 vertical header만을 설정
모두 사용자가 입력하는 데이터로만 이루어졌기에 특정 셀의 수정 불가등의 설정을 하지 않음
4. result_table 테이블 모델 작성
import operator
from PySide6.QtCore import QAbstractTableModel, Qt, SIGNAL, QModelIndex
from PySide6.QtGui import *
''' 생략 '''
class DayCalResultTableModel(QAbstractTableModel):
def __init__(self, parent, result_data, *args):
QAbstractTableModel.__init__(self, parent, *args)
self.setParent(parent)
self.horizontal_header = ['계']
self.vertical_header = ['강동총금액 합계', '강동운임 합계', '강동하차비 합계', '강동수수료 4% 합계', '공제후금액 합계',
'중매수수료 5% 합계', '화주운임 합계', '화주하차비 합계', '상장수수료 4% 합계', '경매확인',
'경매 차액', '중개수수료 5%', '경매 차익']
self.row_count = len(self.vertical_header)
self.column_count = 1
self.table_data = [result_data]
def rowCount(self, parent):
return self.row_count
def columnCount(self, parent):
return self.column_count
def data(self, index, role):
if index.isValid():
if role == Qt.DisplayRole or role == Qt.EditRole:
value = self.table_data[index.column()][index.row()]
return str(value)
def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...):
if role == Qt.DisplayRole:
if orientation == Qt.Vertical:
return self.vertical_header[section]
else:
return self.horizontal_header[section]
return None
def flags(self, index):
return Qt.ItemIsSelectable | Qt.ItemIsEnabled
마찬가지로 input_table과 거의 동일하게 구현
해당 테이블은 모든 셀을 수정 불가능하도록 설정
5. 테이블 뷰에 모델 세팅
from PySide6.QtWidgets import QTabWidget, QWidget, QGridLayout, QTableView
from controller import actions
from models.table_models import DayCalTableModel, DayCalOthersTableModel, DayCalResultTableModel
# 일일 정산서 계산서 위젯
class DayCal(QWidget):
# 생성자
def __init__(self):
super().__init__()
self.input_table = QTableView()
self.other_table = QTableView()
self.result_table = QTableView()
self.init_ui()
# ui 초기화
def init_ui(self):
# 화주별 데이터 입력테이블
self.data_model = DayCalTableModel(self, actions.get_daycal_owner_list(), actions.get_daycal_owner_values())
self.input_table.setModel(self.data_model)
# 기타 데이터 입력 테이블
self.other_data_model = DayCalOthersTableModel(self, actions.get_daycal_other_values())
self.other_table.setModel(self.other_data_model)
# 결과 테이블
self.result_data_model = DayCalResultTableModel(self, actions.get_daycal_result())
self.result_table.setModel(self.result_data_model)
# 그리드 레이아웃
grid = QGridLayout()
# 테이블위젯 추가
grid.addWidget(self.input_table, 0, 0)
grid.addWidget(self.other_table, 1, 0)
grid.addWidget(self.result_table, 0, 1, 2, 1)
grid.setRowStretch(0, 5)
grid.setColumnStretch(0, 5)
# 레이아웃 세팅
self.setLayout(grid)
# 화주 추가 반영
def owner_added(self, added_user):
self.data_model.owner_added(added_user)
# 화주 삭제 반영
def owner_removed(self, removed_name):
self.data_model.owner_removed(removed_name)
# 화주 이름 변경 반영
def owner_modified(self, org_name, chg_name):
self.data_model.owner_modified(org_name, chg_name)
''' 생략 '''
작성한 모델의 인스턴스를 생성(이 때, DB에서 데이터를 읽어와서 인자로 넘겨준다.)
테이블 뷰에 각각 해당하는 모델의 인스턴스를 세팅
레이아웃에 추가한 뒤 위젯에 레이아웃을 세팅한다.
DayCal 위젯은 owner_added, owner_removed, owner_modified 메소드를 각각 구현하여 화주 상태변경을 인식하고 각 테이블의 데이터모델의 메소드를 호출하여 이를 반영한다.
6. DB 작업을 위한 함수 작성
# 화주 명단 가져오기
def get_daycal_owner_list():
return [q.name for q in session.query(DayCalOwner).order_by(DayCalOwner.id)]
# 화주별 데이터 가져오기
def get_daycal_owner_values():
values = []
today = date.today()
for owner_id in session.query(DayCalOwner.id).order_by(DayCalOwner.id):
id = owner_id[0]
value = session.query(DayCalOwnerValues).filter(and_(DayCalOwnerValues.owner_id == id, DayCalOwnerValues.date == today)).first()
if not value:
value = DayCalOwnerValues(today, id)
session.add(value)
session.commit()
values.append(value.to_list())
return values
# 기타 데이터 가져오기
def get_daycal_other_values():
today = date.today()
value = session.query(DayCalOtherValues).filter(DayCalOtherValues.date == today).first()
if not value:
value = DayCalOtherValues(today)
session.add(value)
session.commit()
return value.to_list()
# 결과 데이터 가져오기
def get_daycal_result():
today = date.today()
value = session.query(DayCalResult).filter(DayCalResult.date == today).first()
if not value:
value = DayCalResult(today)
session.add(value)
session.commit()
return value.to_list()
데이터 모델의 인스턴스 생성에 필요한 데이터를 DB에서 가져오기 위한 함수 구현
get_daycal_owner_list
화주 이름 명단을 반환
화주 id를 기준으로 오름차순 정렬
get_daycal_owner_values
화주별 오늘의 데이터를 리스트 형태로 반환
화주 id를 기준으로 오름차순 정렬
오늘 날짜의 데이터가 존재하지 않을 경우 디폴트로 모든 값이 0인 기본 데이터를 생성하여 데이터베이스에 추가하고 해당 데이터를 반환한다.
get_daycal_other_values
오늘의 기타 데이터를 리스트 형태로 반환
오늘 날짜의 데이터가 존재하지 않을 경우 디폴트로 모든 값이 0인 기본 데이터를 생성하여 데이터베이스에 추가하고 해당 데이터를 반환한다.
get_daycal_result
오늘의 결과 데이터를 리스트 형태로 반환
오늘 날짜의 데이터가 존재하지 않을 경우 디폴트로 모든 값이 0인 기본 데이터를 생성하여 데이터베이스에 추가하고 해당 데이터를 반환한다.
전송계층에서 TCP 혹은 UDP 헤더를 붙여 만들어진 패킷에 IP 헤더를 붙여 IP 패킷을 생성
패킷은 헤더에 명시된 IP 주소를 기반으로 목적지로 전송된다.
IP 자체는 비연결형 프로토콜이며 신뢰성있는 전송을 보장하지 않는다.(이는 전송계층에서 담당)
2. IP 헤더(IPv6)
출처:https://en.wikipedia.org/wiki/IPv4
Version
현재 사용하는 IP의 버전
IHL(IP Header Length)
IP 헤더의 길이(데이터 포함 X)
TOS(Type of Service)
서비스의 우선순위를 표시하기 위한 영역
Total Length
데이터를 포함한 IP패킷 전체의 길이
Identification / Flags / Fragment Offset
라우터의 전송가능한 최대 패킷 크기(MTU)가 IP 패킷보다 작아 패킷이 쪼개질 경우 쪼개진 패킷을 다시 재조립하기 위해 필요한 정보를 저장하기 위한 영역
Identification(16bit) : 어느 패킷의 프래그먼트인지 나타내기 위한 영역
Flags(3bit) : 패킷 분할 정보를 나타내기 위한 영역. 예를들어 마지막 비트는 뒤에 다른 프래그먼트가 더 있는지 표시되기 위해 사용된다.
Offset(15bit) : 각 프래그먼트의 원본 데이터에서의 인덱스를 나타내는 영역
TTL(Time To Live)
초기 설정한 값에서 포워딩을 거칠 때마다 1씩 감소
패킷에 수명을 주어 무한루프에 빠진 패킷 등이 일정 횟수의 포워딩을 거치면 사라지도록 함
Protocol(Upper Layer)
상위 계층에서 사용될 프로토콜을 명시(TCP/UDP 등)
Header Checksum
ip 헤더의 체크섬을 저장 - 최소한의 에러검출
Source/Destination IP Address
출발지, 목적지 IP주소
Options
추가적인 처리 옵션을 정의하기 위한 영역
없을 수도 있으며 최대 40바이트까지 사용 가능
3. IP 주소
8비트로 표현되는 정수 4개(32비트)로 표현되는 주소체계 => IPv6의 경우 총 128비트로 주소를 표현한다. 즉, 2 ^ 128 개의 독립적인 주소를 표현할 수 있다.
IP 주소는 엄밀히 말해 특정 호스트를 가리키는 주소가 아닌 네트워크 인터페이스를 가리키는 주소 => 라우터와 같이 다수의 인터페이스를 가지는(다수의 IP주소를 가지는) 장비도 존재한다.
IP의 분배
단순히 IP 하나하나를 호스트에게 배분하는 방식을 사용할 경우 포워딩 테이블의 크기가 너무 커지며 포워딩에 걸리는 시간도 너무 길어진다.
IP 계층화
Network ID 부분과 Host ID 부분을 분리
ex) 12.34.158.0/24
상위 24비트(12.34.158) 까지는 네트워크 ID
나머지 8비트(0) 까지는 호스트 ID
같은 네트워크에 속하는 호스트들을 동일한 IP prefix로 묶을 수 있게 된다 => 포워딩 테이블을 보다 간단하게 표현하여 효율적인 포워딩을 가능하게 한다.
새 호스트에 IP주소를 할당하는 작업도 간단해짐
과거에는 네트워크 ID를 부여받는 기관의 규모에 따라 네트워크 ID 비트수를 조정
A클래스 - 상위 8비트를 네트워크 ID로 사용
B클래스 - 상위 16비트를 네트워크 ID로 사용
C클래스 - 상위 24비트를 네트워크 ID로 사용
현재는 서브네팅을 사용하여 유동적으로 prefix를 조절
서브넷 마스크(Subnet Mask)
네트워크 ID가 어디까지인지 컴퓨터가 이해할 수 있도록 표현하는 방식
ex) 125.23.87.0 이라는 네트워크 ID를 부여받았을 때, 각각 30개의 호스트를 가지는 8개의 서브넷으로 분리하려면 네트워크 ID인 24비트까지의 서브넷마스크는 모두 1, 그 이하로는 8개의 서브넷을 사용하기 위해 3개의 비트를 1로, 나머지를 0으로 하면 0인 부분이 5비트가 되어 32개의 주소를 나타낼 수 있게된다.
만약 125.23.87.0 에 서브넷마스크가 255.255.255.192라면
서브넷의 갯수는 최대 4개이다.
각 서브넷의 호스트 범위는 0~63, 64~127, 128~191, 192~255 가 된다.
이 중 세 번째 서브넷인 128~191 에서 대표주소는 125.23.87.128이며 브로드캐스트 주소는 125.23.87.191이 된다.
NAT(Network Address Translation)
20년 전에도 이미 IPv4가 지원하는 2 ^ 32 개의 주소만으론 부족할 것으로 예상하여 IPv6를 만들었지만 정작 현재까지도 사용중인것은 여전히 IPv4 이다
IPv6로의 이전을 위해서는 기존에 사용하던 모든 네트워크 장비를 교체해야하는데, 이는 아직까지는 현실적으로 어려움이 따름
결과적으로 IPv4를 그대로 사용하면서도 표현 가능한 주소의 갯수를 늘리기 위해 NAT(Network Address Translation)를 사용
NAT의 방식
로컬 네트워크에서 그 네트워크 내부에서만 유일성을 가지는 IP 주소를 분배
네트워크 내부 호스트가 외부 호스트에게 데이터를 전송할 경우 내부 호스트의 IP주소를 전 세계적으로도 유일한 게이트웨이 주소로 변경해주고 외부에서 내부의 호스트에게 데이터를 전송할 경우 내부 IP주소로 변환하여 내부 호스트에게 전달해준다.
기본적으로는 출발지 IP주소(게이트웨이 주소)와 도착지 IP주소를 통해 내부 IP를 특정하지만 이 방식으로는 다수의 내부 호스트가 같은 목적지와 통신할 경우 구분이 불가능해진다. 이를 해결하기 위해 추가적으로 포트를 구성하여 패킷을 구분하는 PAT 혹은 NAPT 방식을 사용한다.
NAT 테이블에 기록되는 것은 내부에서 패킷을 전송할 때이기 때문에 클라이언트로써는 문제가 없지만 상대의 요청을 기다려야하는 서버로서는 정상적으로 동작하기 어렵다. NAT 테이블에 직접 변환정보를 기재하여 서버로서 동작할 수 있게 하는 방법도 있지만 일반 사용자가 할 수 있는 작업은 아니다.
NAT의 또다른 문제는 계층화된 네트워크 구조를 어기고있다는 점이다. 네트워크 계층에서 동작하는 장비인 NAT 라우터가 IP 패킷의 데이터부분(TCP헤더의 포트)을 인지하고 수정까지 하는 규칙위반을 하고있다. (대부분의 사용자는 서버 역할을 할 경우가 없기 때문에 문제를 크게 인지하지 못한다.)
DHCP(Dynamic Host Configuration Protocol)
DHCP 서버가 네트워크에 연결된 호스트에게 자동으로 IP주소를 대여해주는 방식
절차
DHCP 클라이언트는 네트워크에 연결하기 위해 네트워크 내에서 브로드캐스트(255.255.255.255)로 DHCP 서버에게 IP 주소를 요청하는 패킷을 전송(discover)
DHCP 서버는 요청에 대한 응답으로 대여해줄 IP 주소를 담은 패킷을 전송(offer)
DHCP 클라이언트는 요청에 응답해준 DHCP 서버중 하나를 선택하여 해당 서버를 선택했음을 알리는 패킷을 브로드캐스트로 전송한다.(request) 이 때, 브로드캐스트를 사용하는 이유는 선택받지 못한 DHCP 서버가 자신의 요청을 더이상 신경 쓸 필요가 없도록 해주기 위해서이다.
선택받은 DHCP 서버는 request 패킷에 대해 ack 패킷을 보내고 프로토콜이 완료된다. 이후부터 클라이언트측은 대여받은 IP를 통해 통신이 가능해진다.
이 과정을 통해 네트워크에 연결된 호스트는 자신과 연결된 DNS 서버와 게이트웨이 라우터의 IP주소를 알게되고 이를 기반으로 포워딩 테이블을 생성하게 된다.
3. 라우터(Router)
네트워크 계층의 핵심 장비
IP 패킷들은 출발지와 목적지 사이를 연결하는 다수의 라우터를 거쳐 전송된다.
포워딩(Forwarding)
라우터의 가장 기본적인 기능
라우터가 가진 포워딩 테이블에 따라 목적지 주소에 매칭되는 방향으로 패킷을 전송
목적지 주소 하나하나에 매칭되는 방향을 따로 관리하는 것을 비효율적 => 엔트리를 주소의 범위 형태로 구성하여 목적지 주소와 가장 많이 일치하는 엔트리와 매칭
라우팅(Routing)
포워딩을 위한 포워딩 테이블을 생성하는 작업
목적지 주소까지의 모든 경로 중 최적의 경로를 탐색
라우팅 결과 만들어진 포워딩 테이블은 라우터의 입력 포트에 각각 저장되며 라우터에 도착한 패킷들의 포워딩은 이를 이용하여 각 포트에서 병렬적으로 수행된다.
4. 라우팅 알고리즘
연결 상태(Link State) 알고리즘
라우터간의 연결 상태정보를 모든 라우터에 전달하여 최단경로를 탐색하는 알고리즘
데이크스트라(Dijkstra)의 최단경로 알고리즘 사용
라우팅 정보 변경될 경우에만 변경정보를 브로드캐스팅으로 전달
최단경로를 구하기 위해 전체 링크의 상태를 알고있어야하기 때문에 추가적인 메모리가 필요
OSPF, IS-IS 등의 프로토콜이 이 알고리즘을 사용
대규모 네트워크에 적합
거리 벡터(Distance Vector) 알고리즘
최단경로 탐색을 재귀적으로 수행 => 브로드캐스팅이 아닌 이웃한 라우터와의 통신만으로 네트워크의 상태를 파악
벨만 포드(Bellman-Ford Algorithm)의 최단경로 알고리즘 사용
라우팅 정보 변경의 전달이 주기적으로 이루어짐
즉각적으로 발생하지 않기 때문에 잘못된 정보로 경로를 계산하게 될 수 있음
변화가 없어도 주기적으로 통신이 발생하기에 트래픽을 낭비
count to infinity 문제
인접한 노드의 목적지까지의 거리를 토대로 목적지까지의 최단경로를 계산
그런데 인접한 노드의 최단경로에 자기자신이 포함될경우 문제가 발생(역류)
인접한 노드 x에 의존하는 최단경로값을 노드 x에 넘겨줄 때는 무한값을 넘겨주는 것으로 역류를 미연에 방지할 수 있음
RIP, IGRP 등의 프로토콜이 이 알고리즘을 사용
소규모 네트워크에 적합
5. AS(Autonomous System) - 자율시스템
통일된 라우팅 프로토콜을 사용하고 동일한 관리자에 의해 관리되는 독립된 네트워크 영역
해당 네트워크에 속한 라우터와 서브 네트워크들의 집합
넓게 보면 ISP(Internet Service Provider)를 말하기도 한다.
필요성
전 세계의 라우터를 대상으로 라우팅 알고리즘을 실행하기에는 그 수가 너무 많음
계층화를 통해 라우팅의 규모를 분할 - 보다 효율적인 라우팅
라우팅 알고리즘을 AS별로 독립적으로 적용
AS 단위의 보안 유지
고장 및 오류를 AS단위로 관리 가능
AS 라우팅 프로토콜
IGP(Interior Gateway Protocol)
AS 내부의 라우터간의 라우팅
RIP, IGRP, EIGRP, OSPF, IS-IS 등 일반적으로 말하는 라우팅 알고리즘은 IGP를 의미
EGP(Exterior Gateway Protocol)
서로 다른 AS 간의 라우팅
BGP(Border Gateway Protocol)
서로 다른 AS간에 라우팅 경로를 설정하기 위해 AS 외곽의 라우터끼리 IP주소 정보를 교환
3-way handshaking 으로 연결을 수립, 4-way handshaking으로 연결을 해제
신뢰성있는 데이터 전송(Reliable Data Transfer)을 지원
데이터의 순서유지 보장
흐름제어(수신측의 처리속도에 맞춰 전송속도 조절) 지원
혼잡제어(망의 혼잡도에 따라 전송 패킷량 조절) 지원
양방향(Full Duplex), 1:1(Point to Poin) 통신
2. UDP(User Datagram Protocol)와의 비교
TCP
UDP
신뢰성 보장
O
X
순서 유지
O
X
에러 검출
O
O
흐름 제어
O
X
혼잡 제어
O
X
연결 과정
O
X
통신 형태
1:1(Point to Point), 양방향(Full Duplex)
1:1, 1:N(Broadcast), N:M, 단방향(Simplex)
통신 단위
바이트 스트림(데이터 유실이 없기 때문)
데이터그램
3. 헤더
① UDP
Port(32bit) : source(송신측) 포트와 dest(수신측) 포트 두 종류가 있으며 각각 16비트씩 차지
Segment Length(16bit): 헤더를 포함한 UDP 세그먼트의 길이 정보
Checksum(16bit) : 전송 도중에 에러가 발생했는지 판단하기 위한 값. UDP는 에러가 발생한 segment를 drop
Port(32bit) : source(송신측) 포트와 dest(수신측) 포트 두 종류가 있으며 각각 16비트씩 차지
Sequence Number(32bit): 전송되는 데이터의 순서. 데이터의 순서 유지를 보장하기 위한 값
Acknowledgement Number(32bit): 수신자가 받을 것으로 기대하는 다음 데이터의 시퀀스번호
Data Offset(= Header Length)(4bit): 헤더가 아닌 데이터(본문)가 시작되는 위치. 단위는 word(=32bit) => 맨 마지막 필드인 Option의 경우 길이가 정해져있지 않기 때문에 이 필드가 필요하다.
Reserved(3bit): 나중을 위해 예약된 필드. 현재는 그냥 0으로만 채워지는 상태
Flags(9bit): 세그먼트의 속성을 표현하는 플래그
URG: Urgent Pointer 필드에 값이 채워져있음을 표시
ACK: Acknowledgement 필드에 값이 채워져있음을 표시. 이 플래그가 0이라면 해당 필드를 무시
PSH(Push): 수신측에게 이 데이터를 최대한 빠르게 애플리케이션에 전달할 것을 요청. 이 플래그가 0이라면 수신측은 버퍼가 채워질 때 까지 대기
RST(Reset) : 이미 연결이 확립된 상대에게 연결을 강제로 리셋해달라고 요청
SYN(Synchronized): 상대방과 연결을 확립할 때 시퀀스 번호의 동기화를 맞추기 위한 세그먼트임을 표시
FIN(Finish): 연결 종료 요청을 위한 세그먼트임을 표시
NS, CWR, ECE: 명시적 혼잡 통보(ECN)를 위한 플래그. 혼잡 제어 기능의 향상을 위한 플래그
Window Size(16bit): 한번에 전송 가능한 데이터의 양을 의미. 즉, 윈도우의 최대 크기는 (2^16 - 1) 바이트
Checksum(16bit): 데이터 송신중 에러가 발생했는지 검출하기 위한 값. 에러 발생시 재전송을 요청하기 위함
Urgent Pointer(16bit): 긴급 포인터. URG 플래그가 1이라면 해당 포인터가 가리키는 데이터를 우선처리
Option(~40 bytes): TCP의 기능 확장을 위해 사용되는 필드. TCP 헤더의 최대 길이가 60 bytes 이고 고정 필드의 길이가 도합 20 bytes 이기 때문에 Option 필드의 최대 길이는 40bytes가 된다.
4. 신뢰성 있는 데이터 전송(Reliable Data Transfer) / 데이터의 순서유지 보장
송신측은 데이터 패킷을 전송한 뒤 타이머를 설정
수신측이 데이터를 받으면 응답으로 다음에 받아야할 패킷의 시퀀스 넘버를 명시한 ACK을 전송
이때, ACK를 보낼 때 응답 데이터를 보낼 가능성을 생각하여 수신 후 ACK을 보낼 때 까지 딜레이를 둔다.
송신측이 ACK를 받으면 데이터가 정상적으로 전달된 것을 확인, ACK에 명시된 다음 패킷을 전송
ACK를 받지 못하고 타임아웃이 발생하면 데이터가 전달되지 않은것으로 간주, 데이터 패킷을 재전송
5. 흐름 제어(Flow Control)
① Stop and Wait
하나의 패킷을 보낸 뒤 해당 패킷에 대한 ACK가 돌아오기까지 대기
패킷 전송마다 ACK를 기다리는 방식으로는 패킷 전송 이후 응답이 돌아오기 까지의 RTT(Round Trip Time)만큼 시간을 낭비
② Sliding Window
지정된 갯수(윈도우 크기)만큼의 패킷은 하나하나 ACK를 기다리지 않고 전송하는 Pipeline 방식을 사용하는 것으로 성능향상 가능
6. Pipeline 방식을 사용할 때의 신뢰성 보장
Pipelined 방식을 실제로 구현하려면 한번에얼마만큼의 패킷을 연속적으로 보낼 것인지와
오류가 발생한 패킷을 어떻게 재전송할 것인지를 생각해야한다. 이에 대한 두 가지 접근법에 대해 알아보자.
① Go Back N (GBN)
사전에윈도우(Window)의 크기를 임의로 정하여 윈도우의 크기만큼의 패킷을 연속으로 전송
수신측은 대기하고있던 순서에 해당하는 패킷이 아니면 discard, 마지막으로 받은 패킷에 대한 ack를 전송 => 여기서 ACK은 마지막으로 전달받은 패킷의 시퀀스넘버이다. 실제 TCP에서의 ACK는 GBN과는 달리 다음에 받아야할 패킷의 시퀀스넘버를 명시한다.
유실된 패킷에 대한 ack가 오지 않아 timeout이 발생하면 윈도우의 시작 위치를 timeout이 발생한 패킷으로 옮겨 윈도우의 크기만큼 재전송
수신은 정상적으로 됐지만 ACK만 유실된 경우 다음 ACK만으로도 이전 패킷이 정상적으로 전송됐음을 유추가능하기에 문제가 발생하지 않음
중간에 하나의 패킷만 유실되도 해당 패킷부터 다시 윈도우의 크기만큼 패킷들을 재송신하는 방식이기 때문에 그다지 효율적이지 못함
② Selective Repeat
수신측이 유실된 현재 기다리는중인 패킷이 아니어도 정상적으로 수신한 패킷을 버퍼에 저장하고 해당 패킷에 대한 ack를 전송
송신측은 timeout이 발생하면 해당 패킷을 재송신하고 이후 정상적으로 전송된 패킷에 대한 ack를 받더라도 timeout이 발생한 패킷들에 대한 ack가 도착하기 전까지는 윈도우를 이동하지 않으며 새로운 패킷을 보내지도 않음
수신측은 유실되었던 패킷을 수신하면 그동안 버퍼에 저장해두었던 다른 패킷들과 함께 정상적으로 패킷을 처리하고 유실되었던 패킷에 대한 ack를 전송
송신측은 timeout이 발생한 패킷에 대한 ack를 수신하면 윈도우를 이동하고 새로운 패킷을 전송 이때, 미리 ack를 받아두었던 패킷들도 모두 스킵하여 윈도우를 이동한다.
GBN에 비해 효율적이지만 구현이 복잡함, 수신측에도 버퍼 필요
Selective Repeat 프로토콜의 딜레마
Sequence Number 의 범위가 윈도우의 크기만큼일 경우 재전송한 데이터와 정상적으로 보낸 데이터를 수신측이 구분할 수 없게 되는 경우가 발생할 수 있음
Sequence Number 의 범위를 더 넓혀야함 - 얼마만큼 넓혀야하는가 => 윈도우 크기의 2배가량이 Sequence Number의 범위를 최소화하면서 문제를 해결할 수 있다.
실제 TCP는 윈도우 크기가 매우 크기때문에 모든 패킷에 일일히 타이머를 설정하는 것은 비현실적 => GBN을 기반으로 조금 더 개선된 방식을 사용
7. 혼잡 제어(Congestion Control)
패킷이 전송되는 과정에서 네트워크가 혼잡상태에 빠질 경우 이를 감지, 패킷 전송량을 조절
TCP의 경우 패킷의 유실을 통해 혼잡을 감지하고 혼잡 제어에 들어간다.
① AIMD(Additive Increase Multicative Decrease)
합증가/곱감소 알고리즘
처음에는 패킷을 주기마다 하나씩 보내는 것으로 시작
패킷이 정상적으로 보내질 경우 주기마다 1씩 전송량을 증가
윈도우 크기 증가여부는 이전에 윈도우 크기를 증가시킨 뒤 처음으로 보내진 패킷의 ACK로 판단한다.
패킷전송에 실패하거나 timeout이 발생할 경우 윈도우 크기를 절반으로 감소 => 단순 패킷 유실의 경우 일부 패킷을 제외한 다른 패킷의 ACK이 도달하기에 구분 가능
초반에 패킷 전송량이 늘어나는데 너무 많은 시간이 걸림
네트워크의 혼잡을 미리 감지하지 못하고 혼잡이 발생하고나서 비로소 윈도우 크기를 절반으로 줄이는 방식
② Slow Start(Multicative Increase)
AIMD와 마찬가지로 처음에는 패킷을 하나 보내는 것으로 시작
패킷이 정상적으로 보내질 경우 "모든 ACK마다" 1씩 전송량을 증가. 즉, 주기마다 윈도우 크기는 2배씩 증가
혼잡이 감지될 경우 해당 시점에서의 윈도우 크기를 기억하고 윈도우 크기를 1로 초기화
이후 혼잡이 감지된 시점의 윈도우 크기의 절반까지는 기존의 방식대로 주기마다 2배씩 증가
절반 이후부터는 선형적으로 주기마다 1씩 증가시키는 방식을 사용
초기 패킷 전송량을 빠르게 높여갈 수 있으며 한번 혼잡을 감지하고나면 AIMD보다 효율적인 혼잡제어가 가능
③ Fast Retransmit - 패킷유실에 대한 대처
수신측이 순서에 맞지 않는 패킷을 받았을 때도 ACK를 전송
이 때, ACK 값으로 마지막으로 순서를 지켜 들어온 패킷의 다음 패킷의 시퀀스 넘버를 담아 전송하는 것으로 송신측이 빠진 패킷만을 재전송하도록 할 수 있음
또한 재전송이 발생한 것으로 네트워크 혼잡을 감지하여 윈도우 사이즈를 조절할 수 있음
④ Fast Recovery - Slow Start의 윈도우 크기 복구에 걸리는 시간 개선
혼잡을 감지할 경우 윈도우 사이즈를 1로 줄이지 않고 반으로 줄이는 방식
이 방식에서는 혼잡이 발생하기 전까지는 Slow Start 방식을, 발생한 후에는 AIMD방식을 사용하게된다.
7. Nagle 알고리즘
if there is new data to send then
if the window size ≥ MSS and available data is ≥ MSS then
send complete MSS segment now
else
if there is unconfirmed data still in the pipe then
enqueue data in the buffer until an acknowledge is received
else
send data immediately
end if
end if
end if
Pipelined Protocol에서 패킷 하나로 전송되는 데이터의 크기가 너무 작을 경우 데이터크기 대비 패킷량이 너무 많아지는 오버헤드가 발생
지정한 사이즈의 버퍼를 사용하여 이전에 보낸 패킷의 ack가 돌아오기 전까지 전송해야할 데이터는 버퍼에 저장
ack가 돌아오면 버퍼에 넣어둔 데이터를 하나의 패킷으로 전송
이 방식을 사용할 경우 전송 속도는 느려지지만 전송되는 패킷의 수가 줄어들어 효율적인 통신이 가능하다.
빠른 응답속도가 요구되는 시스템에서는 nagle을 사용하지 않는 것이 좋을 수 있다.
WAN 과 같이 규모가 크고 느린 네트워크 통신에서는 nagle을 사용하는 것이 효과적이다.
화주 이름 리스트에서 화주의 인덱스를 찾아 해당 인덱스의 열을 삭제하고 화주 이름 리스트에서도 삭제
4. 화주 이름 변경
# 화주 이름 변경
def modify_owner(main_window, table_widget, name_data=None):
# 다이얼로그 위젯 생성
modify_owner_dialog = Dialog()
modify_owner_dialog.setWindowTitle('화주 이름 변경')
modify_owner_dialog.setGeometry(500, 500, 300, 50)
grid = QGridLayout()
# 화주 이름을 미리 입력받지 않은 경우(메뉴바나 툴바에서 변경액션에 접근한 경우)
org_name = QLineEdit()
if not name_data:
# 화주 이름을 입력받기 위한 다이얼로그 ui 세팅
org_name.setPlaceholderText('화주 이름')
grid.addWidget(QLabel('화주 이름: '), 0, 0, 1, 1)
grid.addWidget(org_name, 0, 1, 1, 2)
chg_name = QLineEdit()
chg_name.setPlaceholderText('바꿀 이름')
chg_name.returnPressed.connect(modify_owner_dialog.success)
submit = QPushButton('변경')
submit.clicked.connect(modify_owner_dialog.success)
grid.addWidget(QLabel('바꿀 이름: '), 1, 0, 1, 1)
grid.addWidget(chg_name, 1, 1, 1, 2)
grid.addWidget(submit, 2, 0, 1, 3)
modify_owner_dialog.setLayout(grid)
# 다이얼로그를 modal 하게 표시
modify_owner_dialog.show_modal()
# 입력이 취소된경우 (다이얼로그를 그냥 종료한 경우) 삭제절차 종료
if modify_owner_dialog.canceled:
return
# 대상 화주이름 name 과 바꿀 이름 changed 를 설정
name = name_data if name_data else org_name.text()
changed = chg_name.text()
# 화주 이름 변경
try:
target = session.query(DayCalOwner).filter(DayCalOwner.name == name).first()
target.name = changed
session.commit()
table_widget.owner_modified(name, changed)
except AttributeError:
main_window.statusBar().showMessage('>> 등록되지 않은 화주입니다.')
except DatabaseError:
main_window.statusBar().showMessage('>> 이미 등록된 화주입니다.')
session.rollback()
Dialog를 통해 변경할 화주의 이름(name)과 바꿀 이름(changed)을 입력받음
데이터베이스에서 name에 해당하는 데이터를 가져와 이름을 변경한 뒤 commit
이름 변경을 즉각 반영하기 위해 table_widget.owner_modified 실행
AttributeError 발생시 status bar에 이름을 바꾸려는 화주가 등록되지 않은 화주임을 표시
DatabaseError 발생시 status bar에 바꿀 이름이 이미 존재하는 다른 화주의 이름임을 표시
5. 화주 이름 변경 반영
# 화주 이름 변경 반영
def owner_modified(self, org_name, chg_name):
idx = self.owners.index(org_name)
self.owners[idx] = chg_name
self.input_table.setHorizontalHeaderItem(idx, QTableWidgetItem(chg_name))
# 화주 모델
class DayCalOwner(Base):
__tablename__ = 'daycal_owner'
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, unique=True)
def __init__(self, name):
self.name = name
autoincrement 가 설정된 id 속성을 추가
primary key를 name에서 id로 변경
name을 primaryk 중복 불가능한 값으로 하기 위해 unique 설정
2. actions.py
from PySide6.QtWidgets import QLabel, QGridLayout, QPushButton, QLineEdit
from sqlalchemy.exc import DatabaseError
from sqlalchemy.orm.exc import UnmappedInstanceError
from controller.db_manager import session
from models.models import DayCalOwner
from widgets.simple import Dialog
# 화주 추가
def create_owner(main_window, table_widget, name_data=None):
# 입력받은 이름이 이미 있는 경우
if name_data:
name = name_data
# 이름을 입력받아야 하는 경우
else:
# 다이얼로그 위젯 생성
create_owner_dialog = Dialog()
create_owner_dialog.setWindowTitle('화주 추가')
create_owner_dialog.setGeometry(500, 500, 300, 50)
# 화주 이름을 입력받기 위한 다이얼로그 ui 세팅
grid = QGridLayout()
input_name = QLineEdit()
input_name.setPlaceholderText('화주 이름')
input_name.returnPressed.connect(create_owner_dialog.success)
submit = QPushButton('추가')
submit.clicked.connect(create_owner_dialog.success)
grid.addWidget(QLabel('화주 이름: '), 0, 0, 1, 1)
grid.addWidget(input_name, 0, 1, 1, 2)
grid.addWidget(submit, 1, 0, 1, 3)
create_owner_dialog.setLayout(grid)
# 다이얼로그를 modal 하게 표시
create_owner_dialog.show_modal()
# 입력이 취소된경우 (다이얼로그를 그냥 종료한 경우) 추가절차 종료
if create_owner_dialog.canceled:
return
# 다이얼로그에서 입력이 완료되면 입력받은 이름을 name 으로 설정
name = input_name.text()
# 새 화주 추가
new_owner = DayCalOwner(name)
try:
session.add(new_owner)
session.commit()
table_widget.owner_added(name)
except DatabaseError:
main_window.statusBar().showMessage('>> 이미 등록된 화주입니다.')
session.rollback()
main_window 에 구현했던 화주 추가 액션을 controller 디렉토리의 actions.py 로 이동
self 대신 메인윈도우의 인스턴스와 화주 추가 결과를 반영할 table이 위치한 위젯의 인스턴스를 인자로 받음
dialog를 통하지않고 이름을 입력받을 상황을 대비하여 name_data 인자를 추가
name_data가 입력되었다면 다이얼로그로 이름을 입력받지 않고 name_data를 name으로 설정
중복된 이름일 경우 status bar에 이미 등록된 화주임을 표시
name이 더이상 primary key가 아닌 unique key이기 때문에 이름이 중복될 경우 add 가 아닌 commit단계에서 예외가 발생한다. 그러므로 예외 발생시 이미 적용된 add 를 취소하기 위해 rollback을 호출해줘야한다.
from controller import actions
``` 생략 ```
# 화주추가 액션 추가
create_owner = QAction(QIcon('src/img/create_owner_icon.png'), '화주 추가', self)
create_owner.setShortcut('Ctrl+Shift+A')
create_owner.setStatusTip('화주 추가')
create_owner.triggered.connect(lambda: actions.create_owner(self, self.central_widget.doc_tab.tab1))
file_menu.addAction(create_owner)
tool_bar.addAction(create_owner)
``` 생략 ```
contoller.actions를 import 한 뒤 create_owner 액션의 triggered 시그널에 lambda 함수의 형태로 연결해준다.
3. simple.py
from PySide6.QtCore import QDateTime, QTimer
from PySide6.QtWidgets import QLabel, QDialog
# 시간레이블 클래스(QLabel 상속)
# 1초마다 현재 날짜/시간을 갱신하여 표시하는 레이블
class TimeLabel(QLabel):
def __init__(self):
super().__init__()
self.setText(QDateTime.currentDateTime().toString('yyyy년 MM월 dd일 ddd hh:mm:ss'))
self.timer = QTimer(self)
self.timer.timeout.connect(self.timeout)
self.timer.start(100)
def timeout(self):
self.setText(QDateTime.currentDateTime().toString('yyyy년 MM월 dd일 ddd hh:mm:ss'))
# 다이얼로그(QDialog 상속)
class Dialog(QDialog):
def __init__(self):
super().__init__()
self.canceled = True
def show_modal(self):
return super().exec_()
def success(self):
self.canceled = False
self.close()
Dialog, TimeLabel 등의 구조가 간단한 위젯들을 widgets/simple.py 에 분리
4. DayCal 위젯
# 일일 정산서 계산서 위젯
class DayCal(QWidget):
# 생성자
def __init__(self):
super().__init__()
self.owners = [q.name for q in session.query(DayCalOwner).order_by(DayCalOwner.id)]
self.num_of_owners = len(self.owners)
self.input_table = QTableWidget()
self.input_table.setParent(self)
self.init_ui()
``` 생략 ```
화주 데이터의 id를 기반으로 순서를 유지하도록 쿼리시에 order_by 를 통해 id를 기준으로 오름차순 정렬
5. create_owner 수정
# 화주 추가
def create_owner(main_window, table_widget, name_data=None):
# 입력받은 이름이 이미 있는 경우
if name_data:
name = name_data
# 이름을 입력받아야 하는 경우
else:
# 다이얼로그 위젯 생성
create_owner_dialog = Dialog()
create_owner_dialog.setWindowTitle('화주 추가')
create_owner_dialog.setGeometry(500, 500, 300, 50)
# 화주 이름을 입력받기 위한 다이얼로그 ui 세팅
grid = QGridLayout()
input_name = QLineEdit()
input_name.setPlaceholderText('화주 이름')
input_name.returnPressed.connect(create_owner_dialog.success)
submit = QPushButton('추가')
submit.clicked.connect(create_owner_dialog.success)
grid.addWidget(QLabel('화주 이름: '), 0, 0, 1, 1)
grid.addWidget(input_name, 0, 1, 1, 2)
grid.addWidget(submit, 1, 0, 1, 3)
create_owner_dialog.setLayout(grid)
# 다이얼로그를 modal 하게 표시
create_owner_dialog.show_modal()
# 입력이 취소된경우 (다이얼로그를 그냥 종료한 경우) 추가절차 종료
if create_owner_dialog.canceled:
return
# 다이얼로그에서 입력이 완료되면 입력받은 이름을 name 으로 설정
name = input_name.text()
# 새 화주 추가
new_owner = DayCalOwner(name)
try:
session.add(new_owner)
session.commit()
table_widget.owner_added(name)
except DatabaseError:
main_window.statusBar().showMessage('>> 이미 등록된 화주입니다.')
session.rollback()
이름을 입력하기 위한 위젯을 QPlainText 에서 QLineEdit 으로 변경
returnPressed 시그널을 사용하여 엔터키에도 반응하도록 수정
다이얼로그에 canceled 속성을 추가하여 단순 종료와 데이터를 정상적으로 제출한 종료를 구분