프로젝트

[프로젝트] Python과 MySQL을 이용한 맛집 추천 게시판

caramel-bottle 2023. 9. 30.

소개

MySQL과 Python을 사용하여 간단한 게시판 프로그램을 만들어 보았다.

MVC 디자인 패턴과 DB활용법을 익히기 위한 간단한 프로젝트이다.

 

최근 일본여행을 자주 다녀왔다. 출국하기 전엔 항상 맛집을 찾곤 했는데 찾아놓은 가게의 간단한 정보를 다시보기 위해서 항상 구글맵을 켜야했다. 맛집에 대한 간단한 정보를 메모하고 공유할 수 있는 작은 프로그램이 있다면 좋을 것 같다는 생각을 바탕으로 제작하였다.

UI는 구현하지 않았고 주피터로 실행한 후 입력창에서 모든 동작을 수행하게끔 했다. 추후 여유가 생긴다면 웹이나 어플리케이션으로 제작해도 재밌을 것 같다.

 

프로그램 이름은 JBR(Japanese Best Restaurant).

 

* MVC 패턴을 아직 공부중이기에 패턴에 어긋나는 부분이 있을 수 있음.

프로그램 구성

  1. 맛집 등록하기
    • 등록 형식 안내 문구 출력하기.
  2. 맛집 찾아보기
    •  카테고리
      • 카테고리 입력
      • 해당하는 맛집 리스트 출력
  3. 검색
    • 내용이 포함된 모든 결과 출력해줌.
  4. 베스트
    • 평점이 높은 10개 출력
  5. 마이페이지
    • 내가 쓴 글
    • 닉네임 변경 -> 실패
    • 비밀번호 변경
    • 로그아웃
  6. 프로그램 종료

코드

이미 작성한 코드라 더 수정하지 않고 이대로 기록하려 한다.

스스로 간단한 피드백 한 줄 작성해본다.

Connect With DB

import MySQLdb

# 닉네임은 중복이 안됨
# 닉네임 - 비밀번호 입력 후 간단한 로그인 

class Connect:
    def __init__(self):
        self.db = None

    def connect(self):
        self.db = MySQLdb.connect('localhost', 'root', '12345678', 'matzip')

    def disconnect(self):
        self.db.close()

mysql과 파이썬을 연동하기 위한 모듈로 PyMySQL, mysqlclient 등이 있다.

이 중 mysqlclient를 사용한 이유는 상대적으로 오류에 있어서 더 안정적이이 때문이다.

 

db에 연결하기 위한 Connect 클래스를 따로 작성하였다.

 

아래 링크를 통해 mysqlclient의 User's Guide를 볼 수 있다.

https://mysqlclient.readthedocs.io/user_guide.html

 

MySQLdb User’s Guide — MySQLdb 1.2.4b4 documentation

If you want to write applications which are portable across databases, use MySQLdb, and avoid using this module directly. MySQLdb._mysql provides an interface which mostly implements the MySQL C API. For more information, see the MySQL documentation. The d

mysqlclient.readthedocs.io


DTO

Member DTO

class Member:
    def __init__(self, userid, userpw, age=0, gender=''):
        self.userid = userid
        self.userpw = userpw
        self.age = age
        self.gender = gender

    def setUserid(self, userid):
        self.userid = userid

    def getUserid(self):
        return self.userid

    def setUserpw(self, userpw):
        self.userpw = userpw

    def getUserpw(self):
        return self.userpw

    def setAge(self, age):
        self.age = age

    def getAge(self):
        return self.age

    def setGender(self, gender):
        self.gender = gender

    def getGender(self):
        return self.gender

 

Res DTO

class Res:
    def __init__(self, resname, location, star=0, cate='', link=''):
        self.resname = resname
        self.location = location
        self.star = star
        self.cate = cate
        self.link = link

    # resname
    def setResname(self, resname):
        self.resname = resname

    def getResname(self):
        return self.resname

    # location
    def setLocation(self, location):
        self.location = location

    def getLocation(self):
        return self.location

    # star
    def setStar(self, star):
        self.star = star

    def getStar(self):
        return self.star

    # cate
    def setCate(self, cate):
        self.cate = cate

    def getCate(self):
        return self.cate

    # link
    def setLink(self, link):
        self.link = link

    def getLink(self):
        return self.link

member, res 두 개의 Table을 사용하였다.

각각의 테이블은 userid를 PK-FK로 사용한다.

 

모든 필드에 대해 DTO를 작성해보았다. 굳이 모든 필드를 get, set 할 필요는 없을 것 같다. 특히나 이번 프로젝트의 경우 데이터의 이동이 단순하고 양이 많지 않기 때문에 적절한 DTO 사용을 더 고려해봐야할 것 같다. 


DAO

SignUp DAO

회원가입을 위해 member 정보를 DTO 객체에 담아서 가져오도록 하였다.

간단하게 table에 insert하는 쿼리를 사용하였고 회원탈퇴 기능은 구현하지 못했다.

그리고 회원가입을 하기 전 입력한 아이디가 이미 db에 존재하는지 확인하기 위해 메서드 하나를 선언하였다. 내용은 아직 구현하지 못했다.

변명이지만 회원가입 부분보다 맛집 정보 관련한 부분에 신경을 썼던 것 같다..

# 회원가입 Dao
class SignUpDao:
    def __init__(self):
        self.connect = Connect()

    def signUp(self, member):
        self.connect.connect()
        cur = self.connect.db.cursor()
        sql = 'insert into member (userid, userpw, age, gender) values (%s, %s, %s, %s)'
        data = (member.getUserid(), member.getUserpw(), member.getAge(), member.getGender())
        cur.execute(sql, data)

        self.connect.db.commit()

        cur.close()
        self.connect.disconnect()

    def signOut(self):
        pass

    # 아이디 중복 확인
    def isUseridExist(self, userid):
        return False

 

Login DAO

이미 가입한 사용자에 한해 로그인을 도와주는 DAO이다.

입력받은 member 정보를 db와 비교하여 아이디와 패스워드가 일치하면 True를 반환하도록 구현하였다.

# 로그인 Dao
class LoginDao:
    def __init__(self):
        self.connect = Connect()

    def login(self, member):
        try:
            self.connect.connect()

            cur = self.connect.db.cursor()

            sql = 'select userid from member where userid=%s and userpw=%s'
            data = (member.getUserid(), member.getUserpw())
            result = cur.execute(sql, data)

            cur.close()
            self.connect.disconnect()

        except Exception as e:
            print(e)

        if result > 0:
            return True
        else:
            return False

Res DAO

가게정보를 추가, 검색, 삭제하는 DAO이다. 

클래스 하나의 정체성을 확실하게 하기 위해 가게정보에 관한 동작만 구현했는데 프로그램 규모가 커진다면 가게등록, 가게검색, 가게삭제 각각을 클래스로 나눠도 되지 않을까 생각이 든다.

 

# 가게 정보 Dao
class ResDao:

    def __init__(self):
        self.connect = Connect()

    # db res 테이블에 등록하기
    def regist(self, res, userid):
        self.connect.connect()
        cur = self.connect.db.cursor()
        sql = 'insert into res (userid, resname, location, star, cate, link) values (%s, %s, %s, %s, %s, %s)'
        data = (userid, res.getResname(), res.getLocation(), res.getStar(), res.getCate(), res.getLink())
        cur.execute(sql, data)
        self.connect.db.commit()
        cur.close()
        self.connect.disconnect()


    # 존재하는 모든 카테고리 리스트를 반환, 없으면 반환 x
    def getCate(self):
        self.connect.connect()
        cur = self.connect.db.cursor()
        sql = 'select cate from res'
        cur.execute(sql)
        result = cur.fetchall()

        cur.close()
        self.connect.disconnect()

        return result


    # 카테고리별 리스트
    def cateSearch(self, cate):
        self.connect.connect()
        cur = self.connect.db.cursor()
        sql = 'select resname, location, star, cate, link from res where cate=%s'

        data = (cate, )
        cur.execute(sql, data)
        result = cur.fetchall()
        cur.close()
        self.connect.disconnect()
        return result


    # 이름으로 검색
    def nameSearch(self, name):
        self.connect.connect()
        cur = self.connect.db.cursor()
        sql = 'select resname, location, star, cate, link from res where resname like %s'
        data = ('%' + name + '%', )
        cur.execute(sql, data)
        result = cur.fetchall()
        cur.close()
        self.connect.disconnect()
        return result


    # 베스트 10 보여주기
    def recommend(self):
        self.connect.connect()
        cur = self.connect.db.cursor()
        sql = 'select * from res order by star desc limit 10'
        cur.execute(sql)
        resnameList = cur.fetchall()
        # {카메스시:4.5}, {이치란라멘:4.0}
        cur.close()
        self.connect.disconnect()
        return resnameList

MyPage DAO

회원정보에 관한 DAO이다. 내가 쓴 게시물, 아이디 변경, 비밀번호 변경을 구현하였다.

# 회원정보 Dao
class MyPageDao:
    def __init__(self):
        self.connect = Connect()

    def myPost(self, userid):
        self.connect.connect()
        cur = self.connect.db.cursor()
        sql = 'select * from res where userid=%s'
        data = (userid, )
        result = cur.execute(sql, data)
        mypostlist = cur.fetchall()
        cur.close()
        self.connect.disconnect()

        if result > 0:
            return mypostlist
        else:
            return False

    def idChange(self, userid, newname):
        self.connect.connect()
        cur = self.connect.db.cursor()
        sql1 = 'update member set userid=%s where userid=%s'
        sql2 = 'update res set userid=%s where userid=%s'
        data = (newname, userid)
        cur.execute(sql1, data)
        self.connect.db.commit()
        cur.execute(sql2, data)
        self.connect.db.commit()
        cur.close()
        self.connect.disconnect()


    def isCorrect(self, userpw):
        self.connect.connect()
        cur = self.connect.db.cursor()
        sql = 'select userpw from member where userpw=%s'
        data = (userpw, )
        result = cur.execute(sql, data)
        cur.close()
        self.connect.disconnect()

        if result > 0:
            return True # 비밀번호 맞음
        else:
            return False # 비밀번호 틀림


    def pwChange(self, userid, newpw):
        self.connect.connect()
        cur = self.connect.db.cursor()
        sql = 'update member set userpw=%s where userid=%s'
        data = (newpw, userid)
        result = cur.execute(sql, data)
        self.connect.db.commit()
        cur.close()
        self.connect.disconnect()


    def logout():
        pass

Service

사용자에게 보여지고 어떤 입력을 받을지 결정되는 부분이다.

DAO 부분에 관여하지 않도록 작성하였다.

Signup Service

# 회원가입 UI
class SignUpService:
    def __init__(self):
        self.dao = SignUpDao()

    def signUpService(self):
        print('\n* 어서오세요 JBR입니다. 회원가입을 진행하겠습니다. ')
        print('* 닉네임, 비밀번호, 생년월일, 성별을 입력해주세요. ')

        while True:
            try:
                userid = input('닉네임: ')
                if self.dao.isUseridExist(userid):
                    print('이미 존재하는 닉네임입니다.')
                else:
                    userpw = input('비밀번호: ')
                    age = input('생년월일: ')
                    gender = input('성별: ')
                    member = Member(userid, userpw, age, gender)
                    self.dao.signUp(member)
                    print('* 회원가입 성공!! ')
                    print('* 프로그램을 다시 실행해주세요. ')
                    break

            except Exception as e:
                print(e)
                print('에러발생')


# 로그인 UI
class LoginService:
    def __init__(self):
        self.dao = LoginDao()

    def loginService(self):
        print('\n* 어서오세요 JBR입니다. 로그인을 진행하겠습니다. ')
        ans = input('* 회원가입 여부를 입력하세요. (y/n) ')

        if ans.upper() == 'Y':
            print('\n* 닉네임, 비밀번호를 입력하세요.')

            userid = input('닉네임: ')
            userpw = input('비밀번호: ')
            member = Member(userid, userpw)
            result = self.dao.login(member)
            if result:
                return member.getUserid()

        elif ans.upper() == 'N':
            return 'N'


    # 회원 탈퇴 서비스
    def signOutService(self):
        pass


# 게시물 UI
class ResService:
    def __init__(self):
        self.dao = ResDao()

    # 가게이름, 위치, 평점, 카테고리, 링크 입력
    def registRes(self, userid):
        print('\n* 추천해주실 맛집의 정보를 입력해주세요.')
        print('가게이름: 추천해주실 맛집의 이름을 적어주세요.\n가게위치: 대략적인 지역 이름을 적어주세요.\n평점: 1.0 ~ 5.0\n카테고리: 스시, 우동, 와규, 야끼, 덮밥, 라멘, 튀김, 코스, 기타\n링크: 구글 링크 혹은 참고할 링크를 입력해주세요.')
        print('예시) 카메스시, 오사카, 4.5, 스시, https://maps.app.goo.gl/dvj2RQzirF61jvnn6\n')
        resname = input('가게이름: ')
        location = input('가게위치: ')
        star = input('평점: ')
        cate = input('카테고리: ')
        link = input('링크: ')
        res = Res(resname, location, star, cate, link)

        self.dao.regist(res, userid)


    def searchRes(self):
        print('\n* 공유된 다양한 맛집을 찾아보세요.\n')
        select = input('1. 카테고리 2. 검색 3. 베스트')

        if select == '1': # 카테고리별 리스트
            cateList = set(self.dao.getCate()) # set으로 중복 제거

            # (('스시',), ('라멘',), ('라멘',), ('카페',))
            print()
            for catelist in cateList:
                print(f'{catelist[0]}\n')

            cateSel = input('카테고리 선택: ')
            selectedCateList = self.dao.cateSearch(cateSel) # 카테고리별 리스트 출력 DAO
            for selectedcatelist in selectedCateList:
                print(f'\n{selectedcatelist[0]} {selectedcatelist[1]} {selectedcatelist[2]} {selectedcatelist[3]} {selectedcatelist[4]}')

        elif select == '2': # 이름으로 검색
            name = input('검색: ')
            searchResult = self.dao.nameSearch(name)
            for searchResult in searchResult:
                print(f'\n{searchResult[1]} {searchResult[2]} {searchResult[3]} {searchResult[4]} {searchResult[5]}')


        elif select == '3': # 베스트 10
            sortedStarList = self.dao.recommend()
            for sortedstarlist in sortedStarList:
                print(f'\n{sortedstarlist[1]} {sortedstarlist[2]} {sortedstarlist[3]} {sortedstarlist[4]} {sortedstarlist[5]}')

# 마이페이지 UI
class MyPageService:
    def __init__(self):
        self.dao = MyPageDao()
        self.userid = None

    def myPage(self, userid):
        self.userid = userid
        try:
            select = input('1. 내가 쓴 글 2. 닉네임 변경 3. 비밀번호 변경')
            if select == '1':
                result = self.dao.myPost(self.userid)
                if result:
                    for mypost in result:
                        print(f'\n{mypost[0]} {mypost[1]} {mypost[2]} {mypost[3]} {mypost[4]} {mypost[5]}') 
                else:
                    print('No Post')

            elif select == '2':
                #try:
                print(f'현재 닉네임은 "{userid}"입니다.')
                newname = input('새로운 닉네임을 입력하세요. ')
                result = self.dao.idChange(self.userid, newname)
                print('성공적으로 변경되었습니다.')
                #except:
                  #  print('변경에 실패하였습니다.')


            elif select == '3':
                result = input('현재 비밀번호를 입력하세요')
                iscorrect = self.dao.isCorrect(result)
                if iscorrect: # 비밀번호 맞음
                    newpw = input('새로운 비밀번호를 입력하세요')
                    self.dao.pwChange(self.userid, newpw)
                    print('비밀번호가 변경되었습니다.')
                else:
                    print('비밀번호가 틀렸습니다.')


        except Exception as e:
            print(e)
            print('에러발생')

View

class Home: # 홈 화면 
    def __init__(self):
        print('=========JBR(Japanes Best Restaurant)==========')
        # self.login = Login()
        self.select = None
        self.id = None
        self.ss = SignUpService()
        self.ls = LoginService()
        self.rs = ResService()
        self.ms = MyPageService()


    def run(self):
        self.select = int(input('\n1. 로그인 2. 회원가입\n'))

        if self.select == 2: # 회원가입
            self.ss.signUpService()

        elif self.select == 1: # 로그인

            self.id = self.ls.loginService()

            if  self.id != 'N' and self.id != None:
                while True:
                    isQuit = self.mainPage()

                    if not isQuit:
                        break

            elif self.id == 'N':
                print('프로그램을 다시 실행하여 회원가입해주세요.')

            else:
                print('로그인에 실패하였습니다..\n')

        print('\n프로그램을 종료합니다.')



    def mainPage(self):
        try:
            menu = int(input('\n1.등록하기 2.찾기 3.마이페이지 4.종료하기'))
            if menu == 1: # 등록하기
                self.rs.registRes(self.id)
                return True

            elif menu == 2: # 검색하기
                self.rs.searchRes()
                return True

            elif menu == 3: # 마이페이지
                self.ms.myPage(self.id) # 로그인한 아이디 전달
                return True

            elif menu == 4: # 종료하기
                return False

        except Exception as e:
            print('에러발생')


Run

JBR = Home()
JBR.run()

 

고찰

  1. Service 부분을 View라고 생각해도 될 지 모호하다. MVC 패턴을 더 깊게 공부하며 다음 프로젝트에서 더 명확한 구분과 완성도 있는 프로그램을 만들어야겠다.
  2. DTO도 마찬가지로 목적을 정확하게 이해할 필요가 있다.
  3. DAO에서 MySQL과 데이터를 주고받는 쿼리문을 더 효율적으로 작성할 필요가 있다.
  4. member, res 두 테이블을 사용했지만 정규화를 고려하면 테이블을 어떻게 더 나눌 수 있을지 생각.
  5. 패턴의 무결성, 데이터의 무결성, 일관성

앞으로 공부하고 정리해야할 내용이 많은 것 같다. 파이팅.

 

댓글