프로젝트, 연구/도서관 관리 사이트

[도서관 관리 사이트] 5. 회원가입, 관리자페이지, 책/카테고리 수정

CSE 2025. 10. 20. 20:58

이전 글에서 홈페이지와 로그인 페이지를 만들었다.

이번 글은 로그인에 이어 회원가입과 관리자페이지를 구현한다.


1. 회원가입 페이지 (signup.html)

1-1. templates/auth/signup.html

html코드는 아래 그림에서 보듯이 로그인 페이지의 코드와 거의 똑같다.

로그인 페이지(좌), 회원가입 페이지(우)

다만 계정 유형이 관리자로 클릭된다면 아래 그림처럼 추가 코드를 입력 받는 칸이 나오게끔 js를 사용했다.

해당 부분에 대한 코드이다.

                        <div class="form-group">
                        <label>계정 유형</label>
                        <div class="role-options">
                            <div class="role-option">
                                <input type="radio" id="role_user" name="role" value="0" checked>
                                <label for="role_user">일반 사용자</tlabel>
                            </div>
                            <div class="role-option">
                                <input type="radio" id="role_admin" name="role" value="1">
                                <label for="role_admin">관리자</label>
                            </div>
                        </div>
                    </div>
                    <div class="form-group" id="admin-code-group" style="display: none;">
                        <label for="admin_code">추가 코드</label>
                        <input type="password" id="admin_code" name="admin_code">
                    </div>
                    <button type="submit" class="btn">회원가입</button>
                    
...
    
    <script>
        //관리자 선택시 추가 코드 입력란 보이기
        const roleRadios = document.querySelectorAll('input[name="role"]');
        const adminCodeGroup = document.getElementById('admin-code-group');
        const adminCodeInput = document.getElementById('admin_code');

        roleRadios.forEach(radio => {
            radio.addEventListener('change', function() {
                if (this.value === '1') {
                    adminCodeGroup.style.display = 'block'; 
                    adminCodeInput.required = true;        
                } else { 
                    adminCodeGroup.style.display = 'none'; 
                    adminCodeInput.required = false;      
                }
            });
        });
    </script>

 

1-2. auth/routes.py

이 html파일의 동작을 처리하는 routes파일을 보면

1) 비밀번호와 비밀번호 재확인 칸의 값이 일치하는지 확인

2) 회원가입하려는 아이디가 이미 사용중인지 확인

3) 사용중인 아이디가 아니고 일반 유저로 회원가입 시 DB에 등록

4) 사용중인 아이디가 아니고 관리자로 회원가입 시 추가 코드도 일치하는지 확인 후 DB에 등록

이렇게 총 4가지의 동작을 수행한다.

@auth_bp.route('/signup', methods=['GET', 'POST'])
def signup():
    if request.method == 'POST':
        user_name = request.form.get('user_name')
        user_pw = request.form.get('user_pw')
        user_pw_confirm = request.form.get('user_pw_confirm')
        user_role = int(request.form.get('role'))

        print('회원가입 시도:', user_name, user_pw, user_pw_confirm, user_role)
        
        if user_pw != user_pw_confirm:
            flash("비밀번호가 일치하지 않습니다. 다시 확인해주세요.")
            return redirect(url_for('auth.signup'))

        if db.is_user_exists(user_name):
            flash("이미 사용 중인 아이디입니다.")
            return redirect(url_for('auth.signup'))
        
        if user_role == 0:  # 일반 사용자
            print('일반 사용자로 회원가입')
            db.create_user(user_name, user_pw, user_role)
            flash("회원가입에 성공했습니다. 로그인해주세요.")
            return redirect(url_for('auth.login'))
        elif user_role == 1:  # 관리자
            print('관리자로 회원가입')
            input_admin_code = request.form.get('admin_code')
            additional_key = os.environ.get('ADDITIONAL_KEY')
            if input_admin_code != additional_key:
                flash("관리자 코드가 올바르지 않습니다.")
                return redirect(url_for('auth.signup'))
            db.create_user(user_name, user_pw, user_role)
            flash("회원가입에 성공했습니다. 로그인해주세요.")
            return redirect(url_for('auth.login'))


    return render_template('auth/signup.html')

 

1-3. 관련된 sql 쿼리들 (db.py)

회원가입 페이지와 관련해서는 is_user_exists(), create_user() 두 개의 함수가 db.py에 작성되었다.

#유저 있는지 확인
def is_user_exists(name):
    print('is_user_exists 호출:', name)
    query = '''
        SELECT user_id FROM users
        WHERE user_name = %s;
    '''
    params = (name,)
    result = execute_query(query, params=params, fetch='one')
    print('is_user_exists 호출:', result)
    return result is not None


#유저 등록
def create_user(name, pw, user_role):
    print('create_user 호출:', name, pw, user_role)
    query = '''
        INSERT INTO users(user_name, user_pw, user_role) VALUES (%s, %s, %s);
    '''
    params = (name, pw, user_role)
    execute_query(query, params=params)

2. 관리자 페이지 (adminpage.html)

2-1. templates/mypage/adminpage.html

html 코드는 테이블 두 개를 가로로 배치하는 방식을 사용했다.

왼쪽 테이블은 모든 유저를 볼 수 있게끔 되어있고, 아이디, 이름, 역할을 볼 수 있다.

오른쪽 테이블은 대출 내역이 한번도 없는 유저의 리스트를 보여준다.

2-2. mypage/routes.py

아래코드는 이 파일에서 관리자페이지와 관련된 부분이다.

이 페이지는 사용자의 조작에 따라 바뀌는 부분이 없기 때문에 간단하다.

@mypage_bp.route('/admin')
def adminpage():
    users_list = db.get_all_users()
    ghost_users_list = db.get_ghost_users()
    return render_template('mypage/adminpage.html', users = users_list, ghost_users = ghost_users_list)

 

2-3. 관련된 sql쿼리들 (db.py)

위 코드에서 보이는 두 개의 함수 get_all_users(), get_ghost_users()의 코드이다.

get_ghost_users()는 대출내역을 기록하는 table에 존재하지 않는 id를 고르는 방식으로 동작한다.

#모든 유저 받아오기
def get_all_users():
    query = '''
        SELECT user_id, user_name, user_role
        FROM users
        ORDER BY user_id ASC;
    '''
    print('get_all_users 호출')
    return execute_query(query, fetch='all')

#유령회원 리스트 받기
def get_ghost_users():
    query = '''
        SELECT user_id, user_name, user_role
        FROM users
        WHERE user_id NOT IN (SELECT DISTINCT user_id FROM rent);
    '''
    print('get_ghost_users 호출')
    return execute_query(query, fetch='all')

3. 책/카테고리 수정 페이지 (dbeditpage.html)

3-1. templates/mypage/dbeditpage.html

가장 화면에 표시되는 것들이 많은 페이지이다.

이 페이지의 기본 기능은 책과 카테고리를 수정하는 것이다.

수정이 잘 되었는지를 편하게 확인하기 위해 전체 책 목록과 카테고리 목록을 볼 수 있게끔 했고, 삭제 버튼도 바로 옆에 있어 실수 없이 편하게 삭제 가능하다.

책은 대여중일 경우는 삭제가 불가능하고, 카테고리도 해당 카테고리로 분류된 책이 있다면 삭제가 불가능 하다.

따라서 이 정보들도 각각 화면에 표시되도록 해놓았다.

3-2. mypage/routes.py

책 추가 버튼이 눌리면 add_book()이라는 함수가 호출된다.

이떄 카테고리들은 중복이 가능하기 때문에 리스트 형식으로 인자가 전달된다.

전체 책 목록 부분에서 삭제버튼을 누르면 해당 책을 삭제한다.

카테고리 추가 버튼을 누르면 해당 이름이 이미 있는 카테고리인지를 확인하고 아닐 때만 카테고리를 추가한다.

카테고리 삭제를 누르면 삭제된다.

마지막으로 페이지 하단의 목록들은 별도의 함수를 통해 불러와 화면에 출력된다.

@mypage_bp.route('admin/dbedit', methods=['GET', 'POST'])
def dbeditpage():

    if request.method == 'POST':
        form_type = request.form.get('form_type')

        if form_type == 'add_book':
            title = request.form.get('title')
            author = request.form.get('author')
            category_ids = request.form.getlist('categories') 
            db.add_book(title, author, category_ids)
            flash("새로운 책이 추가되었습니다.")
            print('책 추가', title, author, category_ids)
        
        elif form_type == 'delete_book':
            book_id = request.form.get('book_id')
            db.delete_book(book_id)
            flash("책이 삭제되었습니다.")
            print('책 삭제, id=', book_id)

        elif form_type == 'add_category':
            if db.is_category_exists(request.form.get('category_name')):
                flash("이미 존재하는 카테고리입니다.")
                return redirect(url_for('mypage.dbeditpage'))
            
            category_name = request.form.get('category_name')
            db.add_category(category_name)
            flash("새로운 카테고리가 추가되었습니다.")
            print('카테고리 추가', category_name)
            
        elif form_type == 'delete_category':
            category_id = request.form.get('category_id')
            db.delete_category(category_id)
            flash("카테고리가 삭제되었습니다.")
            print('카테고리 삭제, id=', category_id)
        
        return redirect(url_for('mypage.dbeditpage'))

    all_books = db.get_all_books_with_categories()
    all_categories = db.get_all_categories_with_count()
    
    return render_template('mypage/dbeditpage.html', books=all_books, categories=all_categories)

 

3-3. 관련된 sql 쿼리들(db.py)

1. add_book()

먼저 find_book_query로 관리자가 추가하고자 하는 책의 제목과 작가를 통해 책의 코드를 알아낸다.

있던 종류의 책이라면 책 재고를 하나추가한다.

없는 책이라면 제목과 작가 정보를 추가하고 할당된 책 코드를 받아온다.

그 후 재고를 추가한다.

그리고 함수의 하단 부분에는 리스트로 전달되는 카테고리 아이디들을 파싱해서 책과 연결해 정보를 저장하게끔 한다.

def add_book(title, author, category_ids):
    print('add_book 호출:', title, author, category_ids)
    #있는 책 종류인지?
    find_book_query = "SELECT book_code FROM book_data WHERE title = %s AND author = %s;"
    existing_book = execute_query(find_book_query, params=(title, author), fetch='one')

    #있는 종류면 재고 하나 추가, 대여는 기본 False
    if existing_book:
        book_code = existing_book['book_code']
        add_copy_query = "INSERT INTO book_status (book_code) VALUES (%s);"
        execute_query(add_copy_query, params=(book_code,))
        
    #없는 거면 book_data에 추가하고 code받아옴
    else:
        add_book_query = "INSERT INTO book_data (title, author) VALUES (%s, %s);"
        new_book_code = execute_query(add_book_query, params=(title, author), fetch='lastrowid')

        #book_status에 재고 추가
        add_copy_query = "INSERT INTO book_status (book_code) VALUES (%s);"
        execute_query(add_copy_query, params=(new_book_code,))

        #여러개 카테고리 처리
        if category_ids: #category_ids = ['1', '2']
            
            category_query_parts = []
            params = []
            for cat_id in category_ids:
                category_query_parts.append("(%s, %s)")
                params.extend([new_book_code, cat_id])
                
                #category_query_parts : ['(%s, %s)', '(%s, %s)']
                #[101, '1', 101, '2'] book_code=101가정
            
            # 최종 쿼리 문자열 생성 "INSERT INTO book_category (book_code, category_id) VALUES (%s, %s), (%s, %s);"
            category_query = f"INSERT INTO book_category (book_code, category_id) VALUES {', '.join(category_query_parts)};"
            execute_query(category_query, params=tuple(params))

 

2. delete_book()

먼저 삭제할 id를 가진 책의 code를 찾고 삭제한다.

이 책이 해당 종류의 마지막 책이었을 경우 연관된 데이터까지 지우는 과정을 실행한다.

book_category에서 해당 책 종류의 카테고리 정보도 지우고, book_data에서 제목과 작가 정보도 삭제한다.

def delete_book(book_id):
    print('delete_book 호출:', book_id, end=' ')
    #삭제할 id를 가진 책의 code찾기
    find_code_query = "SELECT book_code FROM book_status WHERE book_id = %s;"
    book = execute_query(find_code_query, params=(book_id,), fetch='one')
    

    book_code = book['book_code']

    #삭제
    delete_copy_query = "DELETE FROM book_status WHERE book_id = %s;"
    execute_query(delete_copy_query, params=(book_id,))

    # 같은 종류 책 더있는지?
    count_query = "SELECT COUNT(*) AS copy_count FROM book_status WHERE book_code = %s;"
    remaining = execute_query(count_query, params=(book_code,), fetch='one')

    # 더 없으면 다 지워
    if remaining and remaining['copy_count'] == 0:
        print(f"Book code {book_code}의 마지막 책 삭제. 관련 데이터도 삭제.")
        
        #1
        delete_category_link_query = "DELETE FROM book_category WHERE book_code = %s;"
        execute_query(delete_category_link_query, params=(book_code,))
        
        #2
        delete_book_data_query = "DELETE FROM book_data WHERE book_code = %s;"
        execute_query(delete_book_data_query, params=(book_code,))

 

3. 카테고리 관련

이 함수들은 한줄의 쿼리로 동작하는 간단한 함수들이다.

def add_category(category_name):
    print('add_category 호출:', category_name)
    query = "INSERT INTO category (category_name) VALUES (%s);"
    execute_query(query, params=(category_name,))

def is_category_exists(category_name):
    print('is_category_exists 호출:', category_name)
    query = "SELECT category_id FROM category WHERE category_name = %s;"
    result = execute_query(query, params=(category_name,), fetch='one')
    return result is not None

def delete_category(category_id):
    print('delete_category 호출:', category_id)
    query = "DELETE FROM category WHERE category_id = %s;"
    execute_query(query, params=(category_id,))

 

4. get_all_books_with_categories()

페이지의 '전체 책 목록'에 사용될 데이터를 뽑아오는 쿼리이다.

쿼리가 긴 것은 select와 join이 많기 때문이고 식이 복잡한 것은 아니다.

다만 카테고리를 select할 때는 여러개를 하나로 묶어서 보여주기 위해 여러 행을 하나의 문자열로 합쳐주는 group_concat함수를 사용했다.

def get_all_books_with_categories():
    query = """
        SELECT book_id, title, author, GROUP_CONCAT(category_name SEPARATOR ', ') AS categories, is_rent
        FROM book_status LEFT JOIN book_data ON book_status.book_code = book_data.book_code
        LEFT JOIN book_category ON book_data.book_code = book_category.book_code
        LEFT JOIN category ON book_category.category_id = category.category_id
        GROUP BY book_data.book_code, book_id
        ORDER BY book_id ASC;
    """
    print('get_all_books_with_categories 호출')
    return execute_query(query, fetch='all')

 

5. get_all_categories_with_count()

'카테고리 목록'부분에 필요한 데이터를 가져오기 위한 쿼리이다.

def get_all_categories_with_count():
    print('get_all_categories_with_count 호출')
    query = """
        SELECT c.category_id, c.category_name, COUNT(bc.book_code) AS book_count
        FROM category AS c LEFT JOIN book_category AS bc ON c.category_id = bc.category_id
        GROUP BY c.category_id
        ORDER BY c.category_id;
    """
    return execute_query(query, fetch='all')