파이썬 개발 환경 준비
PyCharm에서 pymysql 패키지 설치
DROP DATABASE IF EXISTS soloDB;
CREATE DATABASE soloDB;

파이썬에서 데이터 입력
MySQL 연결=>커서 생성=>테이블 만들기=>데이터 입력=>입력한 데이터 저장=>MySQL 연결 종료
import pymysql
conn = pymysql.connect(
host='127.0.0.1', user='root', password='0000', db='solodb', charset='utf8'
)
커서 만들기
cur = conn.cursor()
테이블 만들기
cur.execute("CREATE TABLE userTable (id char(4),"
"userName char(15), email char(20),"
" birthYear int)")
데이터 입력
r1=cur.execute("INSERT INTO userTable VALUES('hong', '홍지윤', 'hong@naver.com', 1996)")
r2=cur.execute("INSERT INTO userTable VALUES('kim', '김태연', 'kim@daum.net', 2011)")
r3=cur.execute("INSERT INTO userTable VALUES('star', '별사랑', 'star@paran.com', 1990)")
r4=cur.execute("INSERT INTO userTable VALUES('yang', '양지은', 'yang@gmail.com', 1993)")
print(r1,r2,r3,r4)
1 1 1 1
커밋(연결)+닫기
conn.commit()
conn.close()
MySQL 확인

완전한 데이터 조회 프로그램의 완성
import pymysql
# 전역변수 선언부
con, cur = None, None
data1, data2, data3, data4 = "", "", "", ""
row=None
# 메인 코드
conn = pymysql.connect(host='127.0.0.1', user='root', password='0000', db='solodb', charset='utf8')
cur = conn.cursor()
cur.execute('SELECT * FROM userTable') # SELECT문으로 조회, 조회된 결과는 cur 변수에 저장
print("사용자ID 사용자이름 이메일 출생연도")
print("-------------------------------------------------------------")
while True:
row = cur.fetchone() # fetchone() 함수로 결과를 한 행씩 추출
if row == None:
break # 조회된 결과가 없으면 None 반환후 while 나오기
data1 = row[0]
data2 = row[1]
data3 = row[2]
data4 = row[3]
print("%5s %15s %20s %d" % (data1, data2, data3, data4)) # fetchone()함수를 사용하여 조회된 결과에 한 행씩 접근
conn.close()
사용자ID 사용자이름 이메일 출생연도
-------------------------------------------------------------
hong 홍지윤 hong@naver.com 1996
kim 김태연 kim@daum.net 2011
star 별사랑 star@paran.com 1990
yang 양지은 yang@gmail.com 1993
정리
MySQL과 파이썬의 연결을 위해 pymysql.connect()로 연결자를 생성하고, 연결 통로인 커서를 통해 파이썬에서 MySQL로 SQL을 전송
데이터를 변경한 후에는 커밋을 수행해야 변경된 내용 확정
파이썬에서 SELECT문으로 데이터를 조회한 뒤에 fetchone() 함수를 통해 데이터를 한행씩 가져옴
SQL 인젝션은 웹 애플리케이션의 보안 취약점을 악용하여 악의적인 SQL 구문을 삽입, 실행시켜 데이터베이스를 비정상적으로 조작하는 코드 인젝션 공격 기법
SQL 인젝션 코드
import pymysql
df_config ={
'host': 'localhost',
'user': 'root',
'password': '0000',
'charset': 'utf8',
}
DB_NAME='system_db'
def init_database():
conn = pymysql.connect(**df_config) # ** => 딕셔너리 언패킹
cur = conn.cursor()
cur.execute(f"DROP DATABASE IF EXISTS {DB_NAME}")
cur.execute(f"CREATE DATABASE {DB_NAME}")
cur.execute(f"USE {DB_NAME}")
cur.execute("""
CREATE TABLE users (
userid VARCHAR(20) PRIMARY KEY,
userpw VARCHAR(20) NOT NULL,
username VARCHAR(20),
role VARCHAR(10))
""")
sql="INSERT INTO users VALUES (%s, %s, %s, %s)"
data=[
('admin','p@ssword_master','시스템관리자','ADMIN'),
('user01','1234','일반사용자01','USER')
]
cur.executemany(sql,data) #executemany 는 여러 행 데이터 묶음을 실행
conn.commit()
conn.close()
print("db init 초기화 작업, 계정생성 작업 완료")
def login(u_id, u_pw):
conn = pymysql.connect(**df_config, db=DB_NAME)
cur = conn.cursor()
sql=f"SELECT username, role FROM users WHERE userid='{u_id}' AND userpw='{u_pw}'"
print(f"실행한문장:{sql}")
cur.execute(sql)
user_info=cur.fetchone()
conn.close()
if user_info:
print(f"로그인 성공 {user_info[0]} {user_info[1]}")
else:
print("로그인 실패")
if __name__ == "__main__":
init_database()
login('user01', '1234')
login('admin', 'p@ssword_master')
login('xxxx', '1234')

db init 초기화 작업, 계정생성 작업 완료
실행한문장:SELECT username, role FROM users WHERE userid='user01' AND userpw='1234'
로그인 성공 일반사용자01 USER
실행한문장:SELECT username, role FROM users WHERE userid='admin' AND userpw='p@ssword_master'
로그인 성공 시스템관리자 ADMIN
실행한문장:SELECT username, role FROM users WHERE userid='xxxx' AND userpw='1234'
로그인 실패
print("_"*30)
A_ID='admin'
A_PW="' OR '1'='1"
login(A_ID, A_PW)
______________________________
실행한문장:SELECT username, role FROM users WHERE userid='admin' AND userpw='' OR '1'='1'
로그인 성공 시스템관리자 ADMIN
A_ID='admin'
A_PW="123' OR '1'='1"
login(A_ID, A_PW)
실행한문장:SELECT username, role FROM users WHERE userid='admin' AND userpw='123' OR '1'='1'
로그인 성공 시스템관리자 ADMIN
해결 방법
def safe_login(u_id, u_pw):
conn = pymysql.connect(**df_config, db=DB_NAME)
cur = conn.cursor()
sql="SELECT username, role FROM users WHERE userid=%s AND userpw=%s"
print(f"실행한문장(safe):{sql}")
cur.execute(sql, (u_id, u_pw))
user_info=cur.fetchone()
conn.close()
if user_info:
print(f"로그인 성공{user_info[0]} {user_info[1]}")
else:
print("로그인 실패")
safe_login(A_ID, A_PW)
# sql 구성에서 f-string 방식으로 매개변수 넣기 X 인젝션 발생
# sql 구성에서는 매개변수 입력 받는 칸 %s
# execute 함수에서 sql문과 사용자입력값 따로 전달하는 방식으로 인젝션 방지
실행한문장(safe):SELECT username, role FROM users WHERE userid=%s AND userpw=%s
로그인 실패
import pymysql
from pymysql.constants import CLIENT
df_config ={
'host': 'localhost',
'user': 'root',
'password': '0000',
'charset': 'utf8',
'client_flag':CLIENT.MULTI_STATEMENTS # 멀티 여러줄 sql을 한번에 전송가능 하도록 설정
}
DB_NAME='system_db'
def init_database():
conn = pymysql.connect(**df_config) # ** => 딕셔너리 언패킹
cur = conn.cursor()
cur.execute(f"DROP DATABASE IF EXISTS {DB_NAME}")
cur.execute(f"CREATE DATABASE {DB_NAME}")
cur.execute(f"USE {DB_NAME}")
cur.execute("""
CREATE TABLE users (
userid VARCHAR(20) PRIMARY KEY,
userpw VARCHAR(20) NOT NULL,
username VARCHAR(20),
role VARCHAR(10))
""")
sql="INSERT INTO users VALUES (%s, %s, %s, %s)"
data=[
('admin','p@ssword_master','시스템관리자','ADMIN'),
('user01','1234','일반사용자01','USER')
]
cur.executemany(sql,data) #executemany 는 여러 행 데이터 묶음을 실행
conn.commit()
conn.close()
print("db init 초기화 작업, 계정생성 작업 완료")
def login(u_id, u_pw):
conn = pymysql.connect(**df_config, db=DB_NAME)
cur = conn.cursor()
sql=f"SELECT username, role FROM users WHERE userid='{u_id}' AND userpw='{u_pw}'"
print(f"실행한문장:{sql}")
cur.execute(sql)
user_info=cur.fetchone()
conn.close()
if user_info:
print(f"로그인 성공 {user_info[0]} {user_info[1]}")
else:
print("로그인 실패")
def safe_login(u_id, u_pw):
conn = pymysql.connect(**df_config, db=DB_NAME)
cur = conn.cursor()
sql="SELECT username, role FROM users WHERE userid=%s AND userpw=%s"
print(f"실행한문장(safe):{sql}")
cur.execute(sql, (u_id, u_pw))
user_info=cur.fetchone()
conn.close()
if user_info:
print(f"로그인 성공{user_info[0]} {user_info[1]}")
else:
print("로그인 실패")
if __name__ == "__main__":
init_database()
login('user01', '1234')
login('admin', 'p@ssword_master')
login('xxxx', '1234')
print("_"*30)
A_ID='admin'
A_PW="123' OR '1'='1"
login(A_ID, A_PW)
safe_login(A_ID, A_PW)
# sql 구성에서 f-string 방식으로 매개변수 넣기 X 인젝션 발생
# sql 구성에서는 매개변수 입력 받는 칸 %s
# execute 함수에서 sql문과 사용자입력값 따로 전달하는 방식으로 인젝션 방지
B_ID='admin'
B_PW="'; DROP TABLE users;"
login(B_ID, B_PW)
db init 초기화 작업, 계정생성 작업 완료
실행한문장:SELECT username, role FROM users WHERE userid='user01' AND userpw='1234'
로그인 성공 일반사용자01 USER
실행한문장:SELECT username, role FROM users WHERE userid='admin' AND userpw='p@ssword_master'
로그인 성공 시스템관리자 ADMIN
실행한문장:SELECT username, role FROM users WHERE userid='xxxx' AND userpw='1234'
로그인 실패
______________________________
실행한문장:SELECT username, role FROM users WHERE userid='admin' AND userpw='123' OR '1'='1'
로그인 성공 시스템관리자 ADMIN
실행한문장(safe):SELECT username, role FROM users WHERE userid=%s AND userpw=%s
로그인 실패
실행한문장:SELECT username, role FROM users WHERE userid='admin' AND userpw=''; DROP TABLE users;'
로그인 실패
로그인은 실패했지만 테이블이 삭제됨

C_ID='xxxx'
C_PW="' UNION SELECT userid, userpw FROM users -- "
login(C_ID, C_PW)
실행한문장:SELECT username, role FROM users WHERE userid='xxxx' AND userpw='' UNION SELECT userid, userpw FROM users -- '
로그인 성공 admin p@ssword_master
방지하기 위해 SHA-256 사용
SHA-256: 임의의 길이의 데이터를 고정된 256비트(32바이트) 길이의 고유한 값(해시)으로 변환하는 암호화 해시 함수
import hashlib
def encode_pw(password):
return hashlib.sha256(password.encode()).hexdigest()
# 입력받은 문자열 password를 encode를 통해 바이트로 바꾸고
# 16진수로 hexdigest 변환
# sha-256방식 섞어서
db_id='admin'
db_pw = encode_pw('p@ssword123')
print(f"db에 저장되는 비번 {db_pw}")
input_pw="p@ssword123"
hased_input=encode_pw(input_pw)
print(f"사용자가 입력 비번 암호화 결과 {hased_input}")
if hased_input == db_pw:
print("값 일치(로그인 성공 경우)")
db에 저장되는 비번 f55d184f3df1b47eca0d5390baf23ba2299bc626bf27209b6fb534faf8af258b
사용자가 입력 비번 암호화 결과 f55d184f3df1b47eca0d5390baf23ba2299bc626bf27209b6fb534faf8af258b
값 일치(로그인 성공 경우)
ORM: 객체 지향 프로그래밍의 객체와 관계형 데이터베이스의 테이블 간의 데이터 변환을 자동화하여 , 개발자가 SQL 대신 객체로 데이터베이스를 조작할 수 있게 돕는 기술
SELECT * FROM system_db.users;
ALTER TABLE users MODIFY userpw VARCHAR(64);
DEsc users;

#sql ORM - python sqlalchemy
# from sqlalchemy import create_engine, Column, String # DB 연결, 컬럼 설정, 문자열 타입 사용 도구
# from sqlalchemy.ext.declarative import declarative_base # 테이블 만들기 위한 도구
# from sqlalchemy.orm import sessionmaker # DB와 소통 세션
# import hashlib # 해시 알고리즘
from sqlalchemy import create_engine, Column, String
from sqlalchemy.orm import declarative_base, sessionmaker
import hashlib
engine=create_engine('mysql+pymysql://root:0000@localhost:3306/system_db')
Base=declarative_base() # 모든 테이블 클래스의 부모가 될 base 생성
class User(Base):
__tablename__ = 'users'
userid=Column(String(20), primary_key=True) # id 컬럼
userpw=Column(String(64), nullable=False)
username=Column(String(20))
Session=sessionmaker(bind=engine)
session=Session()
def encode_pw(password):
return hashlib.sha256(password.encode()).hexdigest()
def orm_login(u_id, u_pw):
hashed_pw = encode_pw(u_pw) #sha 256 암호화
user=(session.query(User)
.filter(User.userid==u_id, User.userpw==hashed_pw).first())
#.query(User) user테이블에서 찾아라
#.filter(~~~~) where 조건문과 동일
#.first() 가장 먼저 찾은 데이터 1개 가져옴
if user:
print(f"로그인 성공{user.username}")
else:
print("로그인 실패")
if __name__ == "__main__":
orm_login("user01", "1234")
orm_login("admin", "p@ssword_master")
orm_login("admin", "wrongpw")
import pymysql
df_config ={
'host': 'localhost',
'user': 'root',
'password': '0000',
'charset': 'utf8',
}
DB_NAME='system_db'
def update_db_to_sha256():
conn=pymysql.connect(**df_config, db=DB_NAME)
cur=conn.cursor()
cur.execute('select userid, userpw from users')
users=cur.fetchall()
for user_id, raw_pw in users:
if len(raw_pw)==64:
continue
hashed_pw = encode_pw(raw_pw)
update_sql='update users set userpw=%s where userid=%s'
cur.execute(update_sql, (hashed_pw, user_id))
print("해시암호로 갱신 됨")
conn.commit()
conn.close()
update_db_to_sha256()
def orm_signup(u_id,u_pw,u_name):
hashed_pw=encode_pw(u_pw)
new_user=User(userid=u_id,userpw=hashed_pw,username=u_name)
try:
session.add(new_user)
session.commit()
print(f"{u_name}회원가입 완료")
except: Exception as e:
session.rollback()
print("회원가입 완료")
if __name__ == "__main__":
orm_signup("test", "test123", "test")
#orm_login("admin", "p@ssword_master")
def orm_update_pw(u_id, new_pw):
hashed_new_pw = encode_pw(new_pw)
user=session.query(User).filter(User.userid==u_id.first())
if user:
try:
user.userpw=hashed_new_pw
session.commit()
print(f"{u_id}의 비밀번호 변경 완료")
except Exception as e:
session.rollback()
print("pw변경 실패")
else:
print("존재하지 않는 아이디")

import pymysql
from tkinter import *
from tkinter import messagebox
def insertData() :
conn, cur = None, None
data1, data2, data3, data4 ="","","",""
sql=""
conn = pymysql.connect(host='127.0.0.1', user='root',
password='0000', db='soloDB', charset='utf8')
cur = conn.cursor()
data1 = edt1.get(); data2= edt2.get()
data3 = edt3.get(); data4= edt4.get()
sql = "INSERT INTO userTable VALUES('" + data1 + "', '" + data2 + "', '" + data3 + "', " + data4 + ")"
cur.execute(sql)
conn.commit()
conn.close()
messagebox.showinfo('성공', '데이터 입력 성공')
def selectData():
strData1, strData2, strData3, strData4 =[],[],[],[]
conn = pymysql.connect(host='127.0.0.1', user='root',
password='0000', db='soloDB', charset='utf8')
cur = conn.cursor()
cur.execute("select * from userTable")
strData1.append("사용자 ID"); strData2.append("사용자 이름")
strData3.append("사용자 이메일"); strData4.append("사용자 출생연도")
strData1.append("-----------"); strData2.append("-----------")
strData3.append("-----------"); strData4.append("-----------")
while (True):
row = cur.fetchone()
if row == None :
break;
strData1.append(row[0]); strData2.append(row[1])
strData3.append(row[2]); strData4.append(row[3])
listData1.delete(0, listData1.size() - 1)
listData2.delete(0, listData2.size() - 1)
listData3.delete(0, listData3.size() - 1)
listData4.delete(0, listData4.size() - 1)
for item1, item2, item3, item4 in zip(strData1, strData2, strData3, strData4):
listData1.insert(END, item1); listData2.insert(END, item2)
listData3.insert(END, item3); listData4.insert(END, item4)
conn.close()
## 메인 코드부
root = Tk()
root.geometry('600x300')
root.title("완전한 GUI 응용 프로그램")
edtFrame = Frame(root);
edtFrame.pack()
listFrame = Frame(root)
listFrame.pack(side = BOTTOM,fill=BOTH, expand=1)
edt1= Entry(edtFrame, width=10); edt1.pack(side=LEFT,padx=10,pady=10)
edt2= Entry(edtFrame, width=10); edt2.pack(side=LEFT,padx=10,pady=10)
edt3= Entry(edtFrame, width=10); edt3.pack(side=LEFT,padx=10,pady=10)
edt4= Entry(edtFrame, width=10); edt4.pack(side=LEFT,padx=10,pady=10)
btnInsert = Button(edtFrame, text="입력", command=insertData)
btnInsert.pack(side=LEFT,padx=10,pady=10)
btnSelect = Button(edtFrame, text="조회", command=selectData)
btnSelect.pack(side=LEFT,padx=10,pady=10)
listData1 = Listbox(listFrame, bg = 'yellow');
listData1.pack(side=LEFT,fill=BOTH, expand=1)
listData2 = Listbox(listFrame, bg = 'yellow');
listData2.pack(side=LEFT,fill=BOTH, expand=1)
listData3 = Listbox(listFrame, bg = 'yellow');
listData3.pack(side=LEFT,fill=BOTH, expand=1)
listData4 = Listbox(listFrame, bg = 'yellow');
listData4.pack(side=LEFT,fill=BOTH, expand=1)
root.mainloop()

+ 사용자 비밀번호 암호화되어 DB에 저장
+ 로그인/회원가입/비밀번호 변경 로그인창 UI
# mysql sql
# hash
# orm
# GUI tkinter
# 사용자 비밀번호가 암호화되어 DB에 저장되는
# 로그인/회원가입/비밀번호 변경 로그인창 UI
import pymysql
import os, hashlib
from tkinter import *
from tkinter import messagebox
# DB 설정
DB_CFG = dict(host='127.0.0.1', user='root', password='0000', db='soloDB', charset='utf8')
TABLE = "userTable"
def db_connect():
return pymysql.connect(**DB_CFG)
# 테이블 스키마 자동 보정
# serTable: id, userName, email, birthYear
# userpw 컬럼 없으면 추가
def ensure_userpw_column():
conn = db_connect()
cur = conn.cursor()
# 테이블 존재 여부 확인
cur.execute("""
SELECT COUNT(*)
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name = %s
""", (TABLE,))
table_exists = cur.fetchone()[0] == 1
if not table_exists:
# 테이블이 없으면 처음부터 올바른 스키마로 생성
cur.execute(f"""
CREATE TABLE {TABLE} (
id VARCHAR(20) PRIMARY KEY,
userName VARCHAR(50),
email VARCHAR(50),
birthYear INT,
userpw VARCHAR(200) NOT NULL
)
""")
conn.commit()
conn.close()
return
# 테이블이 이미 있으면 컬럼만 확인
cur.execute(f"SHOW COLUMNS FROM {TABLE}")
cols = {row[0] for row in cur.fetchall()}
# userpw 컬럼만 보정
if 'userpw' not in cols:
cur.execute(
f"ALTER TABLE {TABLE} ADD COLUMN userpw VARCHAR(200) NOT NULL DEFAULT ''"
)
conn.commit()
conn.close()
# 비밀번호 해시( salt + sha256 )
def make_pw_hash(password, salt=None):
if salt is None:
salt = os.urandom(16)
h = hashlib.sha256(salt + password.encode("utf-8")).hexdigest()
return salt.hex() + "$" + h
def verify_pw(password, stored):
try:
salt_hex, hash_hex = stored.split("$", 1)
salt = bytes.fromhex(salt_hex)
h = hashlib.sha256(salt + password.encode("utf-8")).hexdigest()
return h == hash_hex
except Exception:
return False
# 회원가입 / 로그인 / 비밀번호 변경
# userTable 컬럼: id, userName, email, birthYear, userpw
def signup_user(u_id, u_pw, u_name, u_email, u_birth):
conn = db_connect()
cur = conn.cursor()
pw_store = make_pw_hash(u_pw)
try:
cur.execute(f"SELECT id FROM {TABLE} WHERE id=%s", (u_id,))
if cur.fetchone():
messagebox.showerror("실패", "이미 존재하는 ID입니다.")
return False
if not str(u_birth).isdigit():
messagebox.showerror("실패", "출생연도는 숫자여야 합니다.")
return False
cur.execute(
f"INSERT INTO {TABLE} (id, userpw, userName, email, birthYear) VALUES (%s,%s,%s,%s,%s)",
(u_id, pw_store, u_name, u_email, int(u_birth))
)
conn.commit()
messagebox.showinfo("성공", "회원가입 완료")
return True
except Exception as e:
conn.rollback()
messagebox.showerror("실패", f"회원가입 실패: {e}")
return False
finally:
conn.close()
def login_user(u_id, u_pw):
conn = db_connect()
cur = conn.cursor()
try:
cur.execute(f"SELECT userpw, userName FROM {TABLE} WHERE id=%s", (u_id,))
row = cur.fetchone()
if not row:
return (False, None)
stored_pw, username = row
if verify_pw(u_pw, stored_pw):
return (True, username)
return (False, None)
finally:
conn.close()
def change_password(u_id, old_pw, new_pw):
conn = db_connect()
cur = conn.cursor()
try:
cur.execute(f"SELECT userpw FROM {TABLE} WHERE id=%s", (u_id,))
row = cur.fetchone()
if not row:
messagebox.showerror("실패", "존재하지 않는 ID입니다.")
return False
stored_pw = row[0]
if not verify_pw(old_pw, stored_pw):
messagebox.showerror("실패", "기존 비밀번호가 올바르지 않습니다.")
return False
new_store = make_pw_hash(new_pw)
cur.execute(f"UPDATE {TABLE} SET userpw=%s WHERE id=%s", (new_store, u_id))
conn.commit()
messagebox.showinfo("성공", "비밀번호 변경 완료")
return True
except Exception as e:
conn.rollback()
messagebox.showerror("실패", f"비밀번호 변경 실패: {e}")
return False
finally:
conn.close()
# 기존 조회 UI 기능
def selectData():
strData1, strData2, strData3, strData4 = [], [], [], []
conn = db_connect()
cur = conn.cursor()
cur.execute(f"SELECT id, userName, email, birthYear FROM {TABLE}")
strData1.append("사용자 ID"); strData2.append("사용자 이름")
strData3.append("사용자 이메일"); strData4.append("사용자 출생연도")
strData1.append("-----------"); strData2.append("-----------")
strData3.append("-----------"); strData4.append("-----------")
while True:
row = cur.fetchone()
if row is None:
break
strData1.append(row[0]); strData2.append(row[1])
strData3.append(row[2]); strData4.append(row[3])
if listData1.size() > 0: listData1.delete(0, listData1.size() - 1)
if listData2.size() > 0: listData2.delete(0, listData2.size() - 1)
if listData3.size() > 0: listData3.delete(0, listData3.size() - 1)
if listData4.size() > 0: listData4.delete(0, listData4.size() - 1)
for item1, item2, item3, item4 in zip(strData1, strData2, strData3, strData4):
listData1.insert(END, item1); listData2.insert(END, item2)
listData3.insert(END, item3); listData4.insert(END, item4)
conn.close()
# UI: 회원가입 / 비밀번호 변경
def open_signup_window():
win = Toplevel(root)
win.title("회원가입")
win.geometry("360x220")
Label(win, text="ID").grid(row=0, column=0, padx=10, pady=5, sticky="e")
Label(win, text="PW").grid(row=1, column=0, padx=10, pady=5, sticky="e")
Label(win, text="이름").grid(row=2, column=0, padx=10, pady=5, sticky="e")
Label(win, text="이메일").grid(row=3, column=0, padx=10, pady=5, sticky="e")
Label(win, text="출생연도").grid(row=4, column=0, padx=10, pady=5, sticky="e")
e_id = Entry(win, width=25); e_id.grid(row=0, column=1, padx=10, pady=5)
e_pw = Entry(win, width=25, show="*"); e_pw.grid(row=1, column=1, padx=10, pady=5)
e_nm = Entry(win, width=25); e_nm.grid(row=2, column=1, padx=10, pady=5)
e_em = Entry(win, width=25); e_em.grid(row=3, column=1, padx=10, pady=5)
e_by = Entry(win, width=25); e_by.grid(row=4, column=1, padx=10, pady=5)
def do_signup():
u_id = e_id.get().strip()
u_pw = e_pw.get()
u_nm = e_nm.get().strip()
u_em = e_em.get().strip()
u_by = e_by.get().strip()
if not u_id or not u_pw:
messagebox.showerror("실패", "ID/PW는 필수입니다.")
return
if not u_by.isdigit():
messagebox.showerror("실패", "출생연도는 숫자여야 합니다.")
return
if signup_user(u_id, u_pw, u_nm, u_em, u_by):
win.destroy()
Button(win, text="가입", command=do_signup).grid(row=5, column=1, padx=10, pady=10, sticky="e")
def open_change_pw_window():
win = Toplevel(root)
win.title("비밀번호 변경")
win.geometry("360x180")
Label(win, text="ID").grid(row=0, column=0, padx=10, pady=5, sticky="e")
Label(win, text="기존 PW").grid(row=1, column=0, padx=10, pady=5, sticky="e")
Label(win, text="새 PW").grid(row=2, column=0, padx=10, pady=5, sticky="e")
e_id = Entry(win, width=25); e_id.grid(row=0, column=1, padx=10, pady=5)
e_old = Entry(win, width=25, show="*"); e_old.grid(row=1, column=1, padx=10, pady=5)
e_new = Entry(win, width=25, show="*"); e_new.grid(row=2, column=1, padx=10, pady=5)
def do_change():
u_id = e_id.get().strip()
old_pw = e_old.get()
new_pw = e_new.get()
if not u_id or not old_pw or not new_pw:
messagebox.showerror("실패", "모든 값을 입력하세요.")
return
if change_password(u_id, old_pw, new_pw):
win.destroy()
Button(win, text="변경", command=do_change).grid(row=3, column=1, padx=10, pady=10, sticky="e")
# UI: 로그인
def do_login():
u_id = login_id.get().strip()
u_pw = login_pw.get()
if not u_id or not u_pw:
messagebox.showerror("실패", "ID/PW를 입력하세요.")
return
ok, username = login_user(u_id, u_pw)
if ok:
messagebox.showinfo("성공", f"로그인 성공: {username}")
btnSelect.config(state=NORMAL)
else:
messagebox.showerror("실패", "로그인 실패")
# 메인
ensure_userpw_column()
root = Tk()
root.geometry('700x360')
root.title("GUI LOGIN")
loginFrame = Frame(root)
loginFrame.pack(fill=X, padx=10, pady=10)
Label(loginFrame, text="ID").pack(side=LEFT, padx=5)
login_id = Entry(loginFrame, width=12)
login_id.pack(side=LEFT, padx=5)
Label(loginFrame, text="PW").pack(side=LEFT, padx=5)
login_pw = Entry(loginFrame, width=12, show="*")
login_pw.pack(side=LEFT, padx=5)
btnLogin = Button(loginFrame, text="로그인", command=do_login)
btnLogin.pack(side=LEFT, padx=5)
btnSignup = Button(loginFrame, text="회원가입", command=open_signup_window)
btnSignup.pack(side=LEFT, padx=5)
btnChange = Button(loginFrame, text="비밀번호 변경", command=open_change_pw_window)
btnChange.pack(side=LEFT, padx=5)
edtFrame = Frame(root)
edtFrame.pack()
listFrame = Frame(root)
listFrame.pack(side=BOTTOM, fill=BOTH, expand=1)
btnSelect = Button(edtFrame, text="조회", command=selectData, state=DISABLED)
btnSelect.pack(side=LEFT, padx=10, pady=10)
listData1 = Listbox(listFrame, bg='white')
listData1.pack(side=LEFT, fill=BOTH, expand=1)
listData2 = Listbox(listFrame, bg='white')
listData2.pack(side=LEFT, fill=BOTH, expand=1)
listData3 = Listbox(listFrame, bg='white')
listData3.pack(side=LEFT, fill=BOTH, expand=1)
listData4 = Listbox(listFrame, bg='white')
listData4.pack(side=LEFT, fill=BOTH, expand=1)
root.mainloop()


# ORM은 반영 못함
전동헌님 코드
USE system_db;
DROP TABLE IF EXISTS user;
CREATE TABLE user(
id VARCHAR(20) NOT NULL,
userpw VARCHAR(64) NOT NULL,
username VARCHAR(20) NOT NULL,
role VARCHAR(20),
PRIMARY KEY (id)
);
DESC user;

import tkinter as tk
from tkinter import ttk, messagebox
from sqlalchemy import create_engine, MetaData, Table, column, String, Column, DateTime
from sqlalchemy.orm import sessionmaker
import hashlib
from sqlalchemy import create_engine, MetaData, Table, column, String, Column
from sqlalchemy.orm import declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = 'user'
id = Column(String(20), primary_key=True)
userpw = Column(String(64), nullable=False)
username = Column(String(20), nullable=False)
role = Column(String(20))
def encode_pw(password):
return hashlib.sha256(password.encode('utf-8')).hexdigest()
class Login(tk.Frame):
def __init__(self, master):
super().__init__(master)
id_lb = ttk.Label(self, text="ID")
self.id_ent = tk.Entry(self)
pw_lb = ttk.Label(self, text="Password")
self.pw_ent = PasswordField(self, tk.StringVar())
self.msg_lb = ttk.Label(self, text="")
self.login_btn = ttk.Button(self, text="Login")
self.login_btn.bind("<Button>", lambda e: self.check_filled())
self.sign_up_btn = ttk.Button(self, text="Sign Up")
id_lb.pack()
self.id_ent.pack()
pw_lb.pack()
self.pw_ent.pack()
self.msg_lb.pack(expand=True, fill="x")
self.login_btn.pack()
self.sign_up_btn.pack()
def check_filled(self):
if self.id_ent.get() == "" or self.pw_ent.get() == "":
self.set_message("Please fill all fields.")
def get_credentials(self):
return self.id_ent.get(), self.pw_ent.get()
def set_message(self, msg):
self.msg_lb.config(text=msg)
def clear(self):
self.id_ent.delete(0, tk.END)
self.pw_ent.delete(0, tk.END)
self.msg_lb.config(text="")
class MemberPage(ttk.Frame):
def __init__(self, master):
super().__init__(master)
self.name_lb = ttk.Label(self, text="ID")
self.reset_password_btn = ttk.Button(self, text="Reset Password")
self.logout_btn = ttk.Button(self, text="Logout")
self.name_lb.pack(expand=True, fill="x")
self.reset_password_btn.pack()
self.logout_btn.pack()
def set_info(self, info):
self.name_lb.config(text=f"Hello {info["username"]}.")
class PasswordField(ttk.Frame):
def __init__(self, master, textvariable, *args, **kwargs):
super().__init__(master, *args, **kwargs)
self.is_showing = False
self.ent = ttk.Entry(self, show="*", textvariable=textvariable)
self.ent.grid(row=0, column=0, sticky="ew")
self.show_btn = ttk.Button(self, text="show")
self.show_btn.grid(row=0, column=1)
self.columnconfigure(0, weight=1)
self.show_btn.bind("<Button>", lambda e: self.show())
def get(self):
return self.ent.get()
def delete(self, *args, **kwargs):
return self.ent.delete(*args, **kwargs)
def show(self):
self.is_showing = not self.is_showing
if self.is_showing:
self.ent.config(show = "")
else:
self.ent.config(show="*")
class SignUpPage(ttk.Frame):
def __init__(self, master):
super().__init__(master)
pw = tk.StringVar()
repeat = tk.StringVar()
pw.trace_add(mode="write", callback=lambda e1, e2, e3: self.pw_changed())
repeat.trace_add(mode="write", callback=lambda e1, e2, e3: self.pw_changed())
id_lb = ttk.Label(self, text="ID")
self.id_ent = tk.Entry(self)
name_lb = ttk.Label(self, text="Name")
self.name_ent = tk.Entry(self)
pw_lb = ttk.Label(self, text="Password")
self.pw_ent = PasswordField(self, pw)
repeat_lb = ttk.Label(self, text="Repeat password")
self.repeat_ent = PasswordField(self, repeat)
self.msg_lb = ttk.Label(self)
self.submit_btn = ttk.Button(self, text="Sign Up")
id_lb.pack()
self.id_ent.pack()
name_lb.pack()
self.name_ent.pack()
pw_lb.pack()
self.pw_ent.pack()
repeat_lb.pack()
self.repeat_ent.pack()
self.msg_lb.pack()
self.submit_btn.pack()
self.pw_ent.bind("<<")
def is_pw_same(self) -> bool:
pw_a, pw_b = self.pw_ent.get(), self.repeat_ent.get()
return pw_a == pw_b
def get(self):
return self.id_ent.get(), self.name_ent.get(), self.pw_ent.get()
def pw_changed(self):
if not self.is_pw_same():
self.msg_lb.config(text="Passwords do not match.")
else:
self.msg_lb.config(text="")
def set_message(self, msg):
self.msg_lb.config(text=msg)
class ResetPassword(ttk.Frame):
def __init__(self, master):
super().__init__(master)
lb1 = ttk.Label(self, text="Current password")
self.current_ent = PasswordField(self, tk.StringVar())
lb2 = ttk.Label(self, text="New password")
self.new_ent = PasswordField(self, tk.StringVar())
lb3 = ttk.Label(self, text="Repeat new password")
self.repeat_ent = PasswordField(self, tk.StringVar())
self.reset_btn = ttk.Button(self, text="reset")
self.reset_btn.pack()
lb1.pack()
self.current_ent.pack()
lb2.pack()
self.new_ent.pack()
lb3.pack()
self.repeat_ent.pack()
def get(self):
return self.current_ent.get(), self.new_ent.get()
def is_pw_same(self) -> bool:
pw_a, pw_b = self.new_ent.get(), self.repeat_ent.get()
return pw_a == pw_b
class UserManager:
def __init__(self, url):
engine = create_engine(url, echo=False)
Session = sessionmaker(bind=engine)
self.session = Session()
def login(self, user_id, user_pw) -> tuple[dict, str]:
user = self.session.query(User).where(User.id == user_id, User.userpw == user_pw).first()
if user:
return {
"user_id": user.id,
"username": user.username
}, ""
else:
return {}, "There's no matched user."
def sign_up(self, user_id, user_pw, user_name, user_role = None) -> tuple[dict, str]:
user = self.session.query(User).where(User.id == user_id).first()
if user:
return {}, "That user ID is already exists."
else:
new_user = User(id=user_id, userpw=user_pw, username=user_name, role=user_role)
try:
self.session.add(new_user)
self.session.commit()
return {
"username": new_user.username
}, ""
except:
self.session.rollback()
return {}, "DB update failed."
def reset_password(self, user_id, user_pw, new_password) -> tuple[bool, str]:
user = self.session.query(User).where(User.id == user_id, User.userpw == user_pw).first()
if user:
try:
user.userpw = new_password
self.session.commit()
return True, ""
except:
self.session.rollback()
return False, "DB update failed."
else:
return False, "There's no matched user."
class MainWindow(tk.Tk):
def __init__(self, db_manager, *args, **kwargs):
super().__init__(*args, **kwargs)
self.title("Login")
self.geometry("400x500")
self.db = db_manager
self.login_page = Login(self)
self.login_page.pack()
self.member_page = MemberPage(self)
self.sign_up_page = SignUpPage(self)
self.reset_password_page = ResetPassword(self)
self.current_member = None
self.login_page.login_btn.bind("<Button>", lambda e: self.try_login())
self.login_page.sign_up_btn.bind("<Button>", lambda e: self.show_sign_up())
self.member_page.logout_btn.bind("<Button>", lambda e: self.logout())
self.member_page.reset_password_btn.bind("<Button>", lambda e: self.show_reset_password())
self.sign_up_page.submit_btn.bind("<Button>", lambda e: self.try_sign_up())
self.reset_password_page.reset_btn.bind("<Button>", lambda e: self.try_reset_password())
def show_reset_password(self):
self.member_page.pack_forget()
self.reset_password_page.pack()
def try_reset_password(self):
if self.reset_password_page.is_pw_same():
oldpw, newpw = self.reset_password_page.get()
hashed_old = encode_pw(oldpw)
hashed_new = encode_pw(newpw)
succeed, err = self.db.reset_password(self.current_member["user_id"], hashed_old, hashed_new)
if succeed:
self.reset_password_page.pack_forget()
self.member_page.pack()
else:
messagebox.showwarning("error", err)
def try_login(self):
user_id, user_pw = self.login_page.get_credentials()
hashed_pw = encode_pw(user_pw)
info, err = self.db.login(user_id, hashed_pw)
if err:
self.login_page.set_message(err)
else:
self.login_page.clear()
self.login_page.pack_forget()
self.member_page.set_info(info)
self.member_page.pack()
self.current_member = info
def try_sign_up(self):
if not self.sign_up_page.is_pw_same():
self.sign_up_page.set_message("Password do not match.")
return
user_id, user_name, user_pw = self.sign_up_page.get()
hashed_pw = encode_pw(user_pw)
info, err = self.db.sign_up(user_id, hashed_pw, user_name)
if err:
self.sign_up_page.set_message(err)
else:
self.sign_up_page.pack_forget()
self.login_page.pack()
def show_sign_up(self):
self.login_page.clear()
self.login_page.pack_forget()
self.sign_up_page.pack()
def logout(self):
self.member_page.pack_forget()
self.login_page.pack()
self.current_member = None
def run(self):
self.mainloop()
if __name__ == "__main__":
db_session = UserManager('mysql+pymysql://root:0000@localhost:3306/system_db')
mw = MainWindow(db_session)
mw.run()
추가
class UserManager:
def __init__(self, url):
engine = create_engine(url, echo=False)
Base.metadata.create_all(engine) #base user 구성대로 테이블이 자동 생성
Session = sessionmaker(bind=engine)
self.session = Session()

1) 데이터 계층 (ORM / DB)
Base = declarative_base()
SQLAlchemy ORM 모델의 기반 클래스 생성.
class User(Base)
DB 테이블 매핑 모델.
__tablename__ = 'user'
컬럼:
id (PK, String(20))
userpw (String(64), 해시 저장)
username (String(20))
role (String(20), optional)
encode_pw(password)
입력 비밀번호를 sha256 해시로 변환.
DB에는 평문이 아니라 해시 문자열(64자) 저장/비교.
2) UI 계층 (Tkinter 페이지/컴포넌트)
UI는 여러 화면을 Frame 단위로 만들어 두고, 화면 전환 시 pack()/pack_forget()으로 교체합니다.
(1) Login(tk.Frame)
로그인 화면
입력: id_ent, pw_ent(PasswordField)
버튼: login_btn, sign_up_btn
기능:
check_filled() : 빈 입력 검사
get_credentials() : ID/PW 반환
set_message() : 메시지 표시
clear() : 입력 초기화
(2) MemberPage(ttk.Frame)
로그인 성공 후 멤버 페이지
표시: name_lb (사용자 이름 출력)
버튼: reset_password_btn, logout_btn
set_info(info)로 사용자 정보를 화면에 반영
(3) PasswordField(ttk.Frame)
비밀번호 입력 컴포넌트(재사용)
Entry + show 버튼(마스킹 토글)
메서드:
get(), delete()
show() : show="*" ↔ show="" 전환
(4) SignUpPage(ttk.Frame)
회원가입 페이지
입력: ID, Name, Password, Repeat Password
기능:
is_pw_same() : 비번 일치 여부
pw_changed() : 비번 불일치 메시지 표시
get() : (id, name, pw) 반환
(5) ResetPassword(ttk.Frame)
비밀번호 변경 페이지
입력: current/new/repeat
기능:
is_pw_same() : 새 비번 일치 여부
get() : (current, new) 반환
3) 서비스/DB 로직 계층
class UserManager
ORM 세션을 가지고 DB 작업 수행
__init__(url)에서 엔진 생성, 세션 생성
메서드:
login(user_id, user_pw) -> (dict, str)
id + pw(해시)로 조회 후 성공 시 info dict 반환
sign_up(user_id, user_pw, user_name, user_role=None) -> (dict, str)
id 중복 체크 후 insert
reset_password(user_id, user_pw, new_password) -> (bool, str)
기존 id+pw 확인 후 pw 업데이트
4) 앱 컨트롤러/라우팅 계층
class MainWindow(tk.Tk)
앱 전체를 통제하는 컨트롤러 역할
self.db에 UserManager 주입
화면(Frame)들을 멤버로 들고 있다가 전환
핵심 흐름:
시작: login_page.pack()
로그인 성공 시:
login_page.pack_forget()
member_page.set_info(info) 후 member_page.pack()
current_member에 로그인 사용자 저장
회원가입 화면 전환:
show_sign_up()
비번 변경 화면 전환:
show_reset_password()
로그아웃:
logout()
5) 실행 진입점
if __name__ == "__main__":
db_session = UserManager('mysql+pymysql://root:0000@localhost:3306/system_db')
mw = MainWindow(db_session)
mw.run()
UserManager가 DB 접근 담당
MainWindow가 UI/흐름 담당
'로보테크AI' 카테고리의 다른 글
| 융합_로보테크 AI 자율주행 로봇 개발자 과정-26/01/26 (0) | 2026.01.26 |
|---|---|
| 융합_로보테크 AI 자율주행 로봇 개발자 과정-26/01/23[TCP] (1) | 2026.01.23 |
| 융합_로보테크 AI 자율주행 로봇 개발자 과정-26/01/21 (1) | 2026.01.21 |
| 융합_로보테크 AI 자율주행 로봇 개발자 과정-26/01/20[SQL] (1) | 2026.01.20 |
| 융합_로보테크 AI 자율주행 로봇 개발자 과정-26/01/19 (0) | 2026.01.19 |

